aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/activitypub/client.ts223
-rw-r--r--server/controllers/activitypub/inbox.ts9
-rw-r--r--server/controllers/activitypub/outbox.ts4
-rw-r--r--server/controllers/api/accounts.ts97
-rw-r--r--server/controllers/api/config.ts44
-rw-r--r--server/controllers/api/index.ts4
-rw-r--r--server/controllers/api/oauth-clients.ts2
-rw-r--r--server/controllers/api/overviews.ts4
-rw-r--r--server/controllers/api/server/debug.ts25
-rw-r--r--server/controllers/api/server/follows.ts71
-rw-r--r--server/controllers/api/server/index.ts4
-rw-r--r--server/controllers/api/server/logs.ts95
-rw-r--r--server/controllers/api/server/redundancy.ts5
-rw-r--r--server/controllers/api/server/server-blocklist.ts10
-rw-r--r--server/controllers/api/server/stats.ts3
-rw-r--r--server/controllers/api/users/index.ts78
-rw-r--r--server/controllers/api/users/me.ts38
-rw-r--r--server/controllers/api/users/my-blocklist.ts19
-rw-r--r--server/controllers/api/users/my-history.ts6
-rw-r--r--server/controllers/api/users/my-notifications.ts15
-rw-r--r--server/controllers/api/users/my-subscriptions.ts20
-rw-r--r--server/controllers/api/users/my-video-playlists.ts46
-rw-r--r--server/controllers/api/video-channel.ts55
-rw-r--r--server/controllers/api/video-playlist.ts445
-rw-r--r--server/controllers/api/videos/abuse.ts12
-rw-r--r--server/controllers/api/videos/blacklist.ts34
-rw-r--r--server/controllers/api/videos/captions.ts11
-rw-r--r--server/controllers/api/videos/comment.ts23
-rw-r--r--server/controllers/api/videos/import.ts71
-rw-r--r--server/controllers/api/videos/index.ts122
-rw-r--r--server/controllers/api/videos/ownership.ts29
-rw-r--r--server/controllers/api/videos/rate.ts8
-rw-r--r--server/controllers/api/videos/watching.ts3
-rw-r--r--server/controllers/bots.ts16
-rw-r--r--server/controllers/client.ts16
-rw-r--r--server/controllers/feeds.ts24
-rw-r--r--server/controllers/services.ts13
-rw-r--r--server/controllers/static.ts38
-rw-r--r--server/controllers/tracker.ts49
-rw-r--r--server/controllers/webfinger.ts5
-rw-r--r--server/helpers/activitypub.ts65
-rw-r--r--server/helpers/audit-logger.ts10
-rw-r--r--server/helpers/captions-utils.ts2
-rw-r--r--server/helpers/core-utils.ts15
-rw-r--r--server/helpers/custom-jsonld-signature.ts68
-rw-r--r--server/helpers/custom-validators/accounts.ts27
-rw-r--r--server/helpers/custom-validators/activitypub/activity.ts3
-rw-r--r--server/helpers/custom-validators/activitypub/actor.ts2
-rw-r--r--server/helpers/custom-validators/activitypub/cache-file.ts12
-rw-r--r--server/helpers/custom-validators/activitypub/misc.ts5
-rw-r--r--server/helpers/custom-validators/activitypub/playlist.ts27
-rw-r--r--server/helpers/custom-validators/activitypub/video-comments.ts4
-rw-r--r--server/helpers/custom-validators/activitypub/videos.ts13
-rw-r--r--server/helpers/custom-validators/logs.ts14
-rw-r--r--server/helpers/custom-validators/misc.ts15
-rw-r--r--server/helpers/custom-validators/servers.ts2
-rw-r--r--server/helpers/custom-validators/users.ts9
-rw-r--r--server/helpers/custom-validators/video-abuses.ts6
-rw-r--r--server/helpers/custom-validators/video-blacklist.ts13
-rw-r--r--server/helpers/custom-validators/video-captions.ts6
-rw-r--r--server/helpers/custom-validators/video-channels.ts24
-rw-r--r--server/helpers/custom-validators/video-comments.ts2
-rw-r--r--server/helpers/custom-validators/video-imports.ts6
-rw-r--r--server/helpers/custom-validators/video-playlists.ts55
-rw-r--r--server/helpers/custom-validators/video-rates.ts5
-rw-r--r--server/helpers/custom-validators/videos.ts20
-rw-r--r--server/helpers/custom-validators/webfinger.ts4
-rw-r--r--server/helpers/database-utils.ts5
-rw-r--r--server/helpers/express-utils.ts10
-rw-r--r--server/helpers/ffmpeg-utils.ts140
-rw-r--r--server/helpers/image-utils.ts13
-rw-r--r--server/helpers/logger.ts13
-rw-r--r--server/helpers/peertube-crypto.ts2
-rw-r--r--server/helpers/requests.ts55
-rw-r--r--server/helpers/signup.ts3
-rw-r--r--server/helpers/utils.ts3
-rw-r--r--server/helpers/video.ts4
-rw-r--r--server/helpers/webfinger.ts4
-rw-r--r--server/helpers/webtorrent.ts2
-rw-r--r--server/helpers/youtube-dl.ts33
-rw-r--r--server/initializers/checker-after-init.ts13
-rw-r--r--server/initializers/checker-before-init.ts17
-rw-r--r--server/initializers/config.ts277
-rw-r--r--server/initializers/constants.ts381
-rw-r--r--server/initializers/database.ts31
-rw-r--r--server/initializers/index.ts2
-rw-r--r--server/initializers/installer.ts26
-rw-r--r--server/initializers/migrations/0075-video-resolutions.ts2
-rw-r--r--server/initializers/migrations/0080-video-channels.ts4
-rw-r--r--server/initializers/migrations/0100-activitypub.ts10
-rw-r--r--server/initializers/migrations/0135-video-channel-actor.ts3
-rw-r--r--server/initializers/migrations/0140-actor-url.ts6
-rw-r--r--server/initializers/migrations/0170-actor-follow-score.ts2
-rw-r--r--server/initializers/migrations/0210-video-language.ts2
-rw-r--r--server/initializers/migrations/0215-video-support-length.ts1
-rw-r--r--server/initializers/migrations/0235-delete-some-video-indexes.ts4
-rw-r--r--server/initializers/migrations/0240-drop-old-indexes.ts4
-rw-r--r--server/initializers/migrations/0330-video-streaming-playlist.ts51
-rw-r--r--server/initializers/migrations/0335-video-downloading-enabled.ts27
-rw-r--r--server/initializers/migrations/0340-add-originally-published-at.ts25
-rw-r--r--server/initializers/migrations/0345-video-playlists.ts88
-rw-r--r--server/initializers/migrations/0350-video-blacklist-type.ts64
-rw-r--r--server/initializers/migrations/0355-p2p-peer-version.ts41
-rw-r--r--server/initializers/migrations/0360-notification-instance-follower.ts40
-rw-r--r--server/initializers/migrations/0365-user-admin-flags.ts40
-rw-r--r--server/initializers/migrations/0370-thumbnail.ts50
-rw-r--r--server/initializers/migrations/0375-account-description.ts25
-rw-r--r--server/initializers/migrator.ts8
-rw-r--r--server/lib/activitypub/actor.ts30
-rw-r--r--server/lib/activitypub/audience.ts2
-rw-r--r--server/lib/activitypub/cache-file.ts25
-rw-r--r--server/lib/activitypub/crawl.ts16
-rw-r--r--server/lib/activitypub/index.ts1
-rw-r--r--server/lib/activitypub/playlist.ts213
-rw-r--r--server/lib/activitypub/process/process-create.ts19
-rw-r--r--server/lib/activitypub/process/process-delete.ts24
-rw-r--r--server/lib/activitypub/process/process-follow.ts36
-rw-r--r--server/lib/activitypub/process/process-undo.ts5
-rw-r--r--server/lib/activitypub/process/process-update.ts23
-rw-r--r--server/lib/activitypub/send/index.ts2
-rw-r--r--server/lib/activitypub/send/send-accept.ts2
-rw-r--r--server/lib/activitypub/send/send-create.ts99
-rw-r--r--server/lib/activitypub/send/send-delete.ts28
-rw-r--r--server/lib/activitypub/send/send-dislike.ts41
-rw-r--r--server/lib/activitypub/send/send-flag.ts39
-rw-r--r--server/lib/activitypub/send/send-follow.ts2
-rw-r--r--server/lib/activitypub/send/send-reject.ts40
-rw-r--r--server/lib/activitypub/send/send-undo.ts17
-rw-r--r--server/lib/activitypub/send/send-update.ts34
-rw-r--r--server/lib/activitypub/send/send-view.ts40
-rw-r--r--server/lib/activitypub/share.ts9
-rw-r--r--server/lib/activitypub/url.ts43
-rw-r--r--server/lib/activitypub/video-comments.ts12
-rw-r--r--server/lib/activitypub/video-rates.ts33
-rw-r--r--server/lib/activitypub/videos.ts212
-rw-r--r--server/lib/avatar.ts6
-rw-r--r--server/lib/cache/abstract-video-static-file-cache.ts52
-rw-r--r--server/lib/client-html.ts80
-rw-r--r--server/lib/emailer.ts94
-rw-r--r--server/lib/files-cache/abstract-video-static-file-cache.ts30
-rw-r--r--server/lib/files-cache/actor-follow-score-cache.ts (renamed from server/lib/cache/actor-follow-score-cache.ts)2
-rw-r--r--server/lib/files-cache/index.ts (renamed from server/lib/cache/index.ts)0
-rw-r--r--server/lib/files-cache/videos-caption-cache.ts (renamed from server/lib/cache/videos-caption-cache.ts)20
-rw-r--r--server/lib/files-cache/videos-preview-cache.ts (renamed from server/lib/cache/videos-preview-cache.ts)19
-rw-r--r--server/lib/hls.ts184
-rw-r--r--server/lib/job-queue/handlers/activitypub-follow.ts7
-rw-r--r--server/lib/job-queue/handlers/activitypub-http-broadcast.ts5
-rw-r--r--server/lib/job-queue/handlers/activitypub-http-fetcher.ts24
-rw-r--r--server/lib/job-queue/handlers/activitypub-http-unicast.ts4
-rw-r--r--server/lib/job-queue/handlers/activitypub-refresher.ts14
-rw-r--r--server/lib/job-queue/handlers/email.ts11
-rw-r--r--server/lib/job-queue/handlers/utils/activitypub-http-utils.ts4
-rw-r--r--server/lib/job-queue/handlers/video-file-import.ts78
-rw-r--r--server/lib/job-queue/handlers/video-import.ts48
-rw-r--r--server/lib/job-queue/handlers/video-transcoding.ts (renamed from server/lib/job-queue/handlers/video-file.ts)98
-rw-r--r--server/lib/job-queue/job-queue.ts18
-rw-r--r--server/lib/notifier.ts114
-rw-r--r--server/lib/oauth-model.ts15
-rw-r--r--server/lib/redis.ts25
-rw-r--r--server/lib/schedulers/abstract-scheduler.ts3
-rw-r--r--server/lib/schedulers/actor-follow-scheduler.ts4
-rw-r--r--server/lib/schedulers/remove-old-history-scheduler.ts32
-rw-r--r--server/lib/schedulers/remove-old-jobs-scheduler.ts2
-rw-r--r--server/lib/schedulers/remove-old-views-scheduler.ts33
-rw-r--r--server/lib/schedulers/update-videos-scheduler.ts5
-rw-r--r--server/lib/schedulers/videos-redundancy-scheduler.ts190
-rw-r--r--server/lib/schedulers/youtube-dl-update-scheduler.ts2
-rw-r--r--server/lib/thumbnail.ts151
-rw-r--r--server/lib/user.ts19
-rw-r--r--server/lib/video-blacklist.ts33
-rw-r--r--server/lib/video-comment.ts2
-rw-r--r--server/lib/video-playlist.ts29
-rw-r--r--server/lib/video-transcoding.ts81
-rw-r--r--server/middlewares/activitypub.ts33
-rw-r--r--server/middlewares/cache.ts72
-rw-r--r--server/middlewares/csp.ts14
-rw-r--r--server/middlewares/oauth.ts4
-rw-r--r--server/middlewares/pagination.ts2
-rw-r--r--server/middlewares/user-right.ts3
-rw-r--r--server/middlewares/validators/account.ts10
-rw-r--r--server/middlewares/validators/activitypub/activity.ts3
-rw-r--r--server/middlewares/validators/avatar.ts2
-rw-r--r--server/middlewares/validators/blocklist.ts31
-rw-r--r--server/middlewares/validators/config.ts21
-rw-r--r--server/middlewares/validators/feeds.ts16
-rw-r--r--server/middlewares/validators/follows.ts61
-rw-r--r--server/middlewares/validators/logs.ts31
-rw-r--r--server/middlewares/validators/oembed.ts8
-rw-r--r--server/middlewares/validators/redundancy.ts45
-rw-r--r--server/middlewares/validators/search.ts3
-rw-r--r--server/middlewares/validators/server.ts4
-rw-r--r--server/middlewares/validators/sort.ts10
-rw-r--r--server/middlewares/validators/user-notifications.ts14
-rw-r--r--server/middlewares/validators/user-subscriptions.ts7
-rw-r--r--server/middlewares/validators/users.ts38
-rw-r--r--server/middlewares/validators/videos/video-abuses.ts14
-rw-r--r--server/middlewares/validators/videos/video-blacklist.ts40
-rw-r--r--server/middlewares/validators/videos/video-captions.ts14
-rw-r--r--server/middlewares/validators/videos/video-channels.ts27
-rw-r--r--server/middlewares/validators/videos/video-comments.ts26
-rw-r--r--server/middlewares/validators/videos/video-imports.ts12
-rw-r--r--server/middlewares/validators/videos/video-playlists.ts408
-rw-r--r--server/middlewares/validators/videos/video-rates.ts24
-rw-r--r--server/middlewares/validators/videos/video-shares.ts7
-rw-r--r--server/middlewares/validators/videos/video-watch.ts7
-rw-r--r--server/middlewares/validators/videos/videos.ts54
-rw-r--r--server/models/account/account-blocklist.ts10
-rw-r--r--server/models/account/account-video-rate.ts83
-rw-r--r--server/models/account/account.ts93
-rw-r--r--server/models/account/user-notification-setting.ts22
-rw-r--r--server/models/account/user-notification.ts62
-rw-r--r--server/models/account/user-video-history.ts16
-rw-r--r--server/models/account/user.ts86
-rw-r--r--server/models/activitypub/actor-follow.ts42
-rw-r--r--server/models/activitypub/actor.ts71
-rw-r--r--server/models/application/application.ts9
-rw-r--r--server/models/avatar/avatar.ts3
-rw-r--r--server/models/migrations.ts16
-rw-r--r--server/models/oauth/oauth-token.ts20
-rw-r--r--server/models/redundancy/video-redundancy.ts192
-rw-r--r--server/models/server/server-blocklist.ts8
-rw-r--r--server/models/utils.ts88
-rw-r--r--server/models/video/schedule-video-update.ts12
-rw-r--r--server/models/video/tag.ts8
-rw-r--r--server/models/video/thumbnail.ts116
-rw-r--r--server/models/video/video-abuse.ts4
-rw-r--r--server/models/video/video-blacklist.ts49
-rw-r--r--server/models/video/video-caption.ts26
-rw-r--r--server/models/video/video-change-ownership.ts44
-rw-r--r--server/models/video/video-channel.ts122
-rw-r--r--server/models/video/video-comment.ts85
-rw-r--r--server/models/video/video-file.ts57
-rw-r--r--server/models/video/video-format-utils.ts115
-rw-r--r--server/models/video/video-import.ts16
-rw-r--r--server/models/video/video-playlist-element.ts230
-rw-r--r--server/models/video/video-playlist.ts531
-rw-r--r--server/models/video/video-share.ts27
-rw-r--r--server/models/video/video-streaming-playlist.ts172
-rw-r--r--server/models/video/video-views.ts15
-rw-r--r--server/models/video/video.ts764
-rw-r--r--server/tests/api/activitypub/client.ts6
-rw-r--r--server/tests/api/activitypub/fetch.ts10
-rw-r--r--server/tests/api/activitypub/helpers.ts2
-rw-r--r--server/tests/api/activitypub/index.ts2
-rw-r--r--server/tests/api/activitypub/refresher.ts136
-rw-r--r--server/tests/api/activitypub/security.ts13
-rw-r--r--server/tests/api/check-params/accounts.ts17
-rw-r--r--server/tests/api/check-params/blocklist.ts43
-rw-r--r--server/tests/api/check-params/config.ts54
-rw-r--r--server/tests/api/check-params/contact-form.ts30
-rw-r--r--server/tests/api/check-params/debug.ts71
-rw-r--r--server/tests/api/check-params/follows.ts144
-rw-r--r--server/tests/api/check-params/index.ts3
-rw-r--r--server/tests/api/check-params/jobs.ts24
-rw-r--r--server/tests/api/check-params/logs.ts111
-rw-r--r--server/tests/api/check-params/redundancy.ts21
-rw-r--r--server/tests/api/check-params/search.ts21
-rw-r--r--server/tests/api/check-params/services.ts34
-rw-r--r--server/tests/api/check-params/user-notifications.ts30
-rw-r--r--server/tests/api/check-params/user-subscriptions.ts44
-rw-r--r--server/tests/api/check-params/users.ts121
-rw-r--r--server/tests/api/check-params/video-abuses.ts22
-rw-r--r--server/tests/api/check-params/video-blacklist.ts31
-rw-r--r--server/tests/api/check-params/video-captions.ts22
-rw-r--r--server/tests/api/check-params/video-channels.ts25
-rw-r--r--server/tests/api/check-params/video-comments.ts30
-rw-r--r--server/tests/api/check-params/video-imports.ts27
-rw-r--r--server/tests/api/check-params/video-playlists.ts674
-rw-r--r--server/tests/api/check-params/videos-filter.ts72
-rw-r--r--server/tests/api/check-params/videos-history.ts18
-rw-r--r--server/tests/api/check-params/videos.ts92
-rw-r--r--server/tests/api/index-1.ts1
-rw-r--r--server/tests/api/notifications/index.ts1
-rw-r--r--server/tests/api/notifications/user-notifications.ts (renamed from server/tests/api/users/user-notifications.ts)346
-rw-r--r--server/tests/api/redundancy/redundancy.ts272
-rw-r--r--server/tests/api/search/search-activitypub-video-channels.ts21
-rw-r--r--server/tests/api/search/search-activitypub-videos.ts15
-rw-r--r--server/tests/api/search/search-videos.ts82
-rw-r--r--server/tests/api/server/config.ts50
-rw-r--r--server/tests/api/server/contact-form.ts23
-rw-r--r--server/tests/api/server/email.ts22
-rw-r--r--server/tests/api/server/follow-constraints.ts14
-rw-r--r--server/tests/api/server/follows-moderation.ts195
-rw-r--r--server/tests/api/server/follows.ts29
-rw-r--r--server/tests/api/server/handle-down.ts12
-rw-r--r--server/tests/api/server/index.ts2
-rw-r--r--server/tests/api/server/jobs.ts14
-rw-r--r--server/tests/api/server/logs.ts97
-rw-r--r--server/tests/api/server/no-client.ts19
-rw-r--r--server/tests/api/server/reverse-proxy.ts32
-rw-r--r--server/tests/api/server/stats.ts17
-rw-r--r--server/tests/api/server/tracker.ts42
-rw-r--r--server/tests/api/users/blocklist.ts28
-rw-r--r--server/tests/api/users/index.ts1
-rw-r--r--server/tests/api/users/user-subscriptions.ts15
-rw-r--r--server/tests/api/users/users-multiple-servers.ts35
-rw-r--r--server/tests/api/users/users-verification.ts20
-rw-r--r--server/tests/api/users/users.ts889
-rw-r--r--server/tests/api/videos/index.ts3
-rw-r--r--server/tests/api/videos/multiple-servers.ts41
-rw-r--r--server/tests/api/videos/services.ts18
-rw-r--r--server/tests/api/videos/single-server.ts21
-rw-r--r--server/tests/api/videos/video-abuse.ts9
-rw-r--r--server/tests/api/videos/video-blacklist.ts144
-rw-r--r--server/tests/api/videos/video-captions.ts19
-rw-r--r--server/tests/api/videos/video-change-ownership.ts56
-rw-r--r--server/tests/api/videos/video-channels.ts13
-rw-r--r--server/tests/api/videos/video-comments.ts16
-rw-r--r--server/tests/api/videos/video-description.ts9
-rw-r--r--server/tests/api/videos/video-hls.ts134
-rw-r--r--server/tests/api/videos/video-imports.ts14
-rw-r--r--server/tests/api/videos/video-nsfw.ts27
-rw-r--r--server/tests/api/videos/video-playlists.ts867
-rw-r--r--server/tests/api/videos/video-privacy.ts17
-rw-r--r--server/tests/api/videos/video-schedule-update.ts7
-rw-r--r--server/tests/api/videos/video-transcoder.ts8
-rw-r--r--server/tests/api/videos/videos-filter.ts28
-rw-r--r--server/tests/api/videos/videos-history.ts52
-rw-r--r--server/tests/api/videos/videos-overview.ts15
-rw-r--r--server/tests/api/videos/videos-views-cleaner.ts106
-rw-r--r--server/tests/cli/create-import-video-file-job.ts8
-rw-r--r--server/tests/cli/create-transcoding-job.ts9
-rw-r--r--server/tests/cli/optimize-old-videos.ts11
-rw-r--r--server/tests/cli/peertube.ts14
-rw-r--r--server/tests/cli/reset-password.ts15
-rw-r--r--server/tests/cli/update-host.ts35
-rw-r--r--server/tests/client.ts13
-rw-r--r--server/tests/feeds/feeds.ts16
-rw-r--r--server/tests/fixtures/thumbnail-playlist.jpgbin0 -> 2520 bytes
-rw-r--r--server/tests/helpers/index.ts1
-rw-r--r--server/tests/helpers/request.ts48
-rw-r--r--server/tests/misc-endpoints.ts17
-rw-r--r--server/tests/real-world/populate-database.ts9
-rw-r--r--server/tests/real-world/real-world.ts4
-rw-r--r--server/tools/cli.ts72
-rw-r--r--server/tools/peertube-auth.ts16
-rw-r--r--server/tools/peertube-get-access-token.ts2
-rw-r--r--server/tools/peertube-import-videos.ts106
-rw-r--r--server/tools/peertube-upload.ts98
-rw-r--r--server/typings/express.ts82
-rw-r--r--server/typings/sequelize.ts18
341 files changed, 13056 insertions, 4031 deletions
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts
index 1a4e28dc8..d36d10de1 100644
--- a/server/controllers/activitypub/client.ts
+++ b/server/controllers/activitypub/client.ts
@@ -2,28 +2,23 @@
2import * as express from 'express' 2import * as express from 'express'
3import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos' 3import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos'
4import { activityPubCollectionPagination, activityPubContextify } from '../../helpers/activitypub' 4import { activityPubCollectionPagination, activityPubContextify } from '../../helpers/activitypub'
5import { CONFIG, ROUTE_CACHE_LIFETIME } from '../../initializers' 5import { ROUTE_CACHE_LIFETIME, WEBSERVER } from '../../initializers/constants'
6import { buildAnnounceWithVideoAudience, buildDislikeActivity, buildLikeActivity } from '../../lib/activitypub/send' 6import { buildAnnounceWithVideoAudience, buildLikeActivity } from '../../lib/activitypub/send'
7import { audiencify, getAudience } from '../../lib/activitypub/audience' 7import { audiencify, getAudience } from '../../lib/activitypub/audience'
8import { buildCreateActivity } from '../../lib/activitypub/send/send-create' 8import { buildCreateActivity } from '../../lib/activitypub/send/send-create'
9import { 9import {
10 asyncMiddleware, 10 asyncMiddleware,
11 videosShareValidator,
12 executeIfActivityPub, 11 executeIfActivityPub,
13 localAccountValidator, 12 localAccountValidator,
14 localVideoChannelValidator, 13 localVideoChannelValidator,
15 videosCustomGetValidator 14 videosCustomGetValidator,
15 videosShareValidator
16} from '../../middlewares' 16} from '../../middlewares'
17import { 17import { getAccountVideoRateValidator, videoCommentGetValidator } from '../../middlewares/validators'
18 getAccountVideoRateValidator,
19 videoCommentGetValidator,
20 videosGetValidator
21} from '../../middlewares/validators'
22import { AccountModel } from '../../models/account/account' 18import { AccountModel } from '../../models/account/account'
23import { ActorModel } from '../../models/activitypub/actor' 19import { ActorModel } from '../../models/activitypub/actor'
24import { ActorFollowModel } from '../../models/activitypub/actor-follow' 20import { ActorFollowModel } from '../../models/activitypub/actor-follow'
25import { VideoModel } from '../../models/video/video' 21import { VideoModel } from '../../models/video/video'
26import { VideoChannelModel } from '../../models/video/video-channel'
27import { VideoCommentModel } from '../../models/video/video-comment' 22import { VideoCommentModel } from '../../models/video/video-comment'
28import { VideoShareModel } from '../../models/video/video-share' 23import { VideoShareModel } from '../../models/video/video-share'
29import { cacheRoute } from '../../middlewares/cache' 24import { cacheRoute } from '../../middlewares/cache'
@@ -37,87 +32,129 @@ import {
37 getVideoSharesActivityPubUrl 32 getVideoSharesActivityPubUrl
38} from '../../lib/activitypub' 33} from '../../lib/activitypub'
39import { VideoCaptionModel } from '../../models/video/video-caption' 34import { VideoCaptionModel } from '../../models/video/video-caption'
40import { videoRedundancyGetValidator } from '../../middlewares/validators/redundancy' 35import { videoFileRedundancyGetValidator, videoPlaylistRedundancyGetValidator } from '../../middlewares/validators/redundancy'
41import { getServerActor } from '../../helpers/utils' 36import { getServerActor } from '../../helpers/utils'
42import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' 37import { buildDislikeActivity } from '../../lib/activitypub/send/send-dislike'
38import { videoPlaylistElementAPGetValidator, videoPlaylistsGetValidator } from '../../middlewares/validators/videos/video-playlists'
39import { VideoPlaylistModel } from '../../models/video/video-playlist'
40import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
43 41
44const activityPubClientRouter = express.Router() 42const activityPubClientRouter = express.Router()
45 43
46activityPubClientRouter.get('/accounts?/:name', 44activityPubClientRouter.get('/accounts?/:name',
47 executeIfActivityPub(asyncMiddleware(localAccountValidator)), 45 executeIfActivityPub,
48 executeIfActivityPub(accountController) 46 asyncMiddleware(localAccountValidator),
47 accountController
49) 48)
50activityPubClientRouter.get('/accounts?/:name/followers', 49activityPubClientRouter.get('/accounts?/:name/followers',
51 executeIfActivityPub(asyncMiddleware(localAccountValidator)), 50 executeIfActivityPub,
52 executeIfActivityPub(asyncMiddleware(accountFollowersController)) 51 asyncMiddleware(localAccountValidator),
52 asyncMiddleware(accountFollowersController)
53) 53)
54activityPubClientRouter.get('/accounts?/:name/following', 54activityPubClientRouter.get('/accounts?/:name/following',
55 executeIfActivityPub(asyncMiddleware(localAccountValidator)), 55 executeIfActivityPub,
56 executeIfActivityPub(asyncMiddleware(accountFollowingController)) 56 asyncMiddleware(localAccountValidator),
57 asyncMiddleware(accountFollowingController)
58)
59activityPubClientRouter.get('/accounts?/:name/playlists',
60 executeIfActivityPub,
61 asyncMiddleware(localAccountValidator),
62 asyncMiddleware(accountPlaylistsController)
57) 63)
58activityPubClientRouter.get('/accounts?/:name/likes/:videoId', 64activityPubClientRouter.get('/accounts?/:name/likes/:videoId',
59 executeIfActivityPub(asyncMiddleware(getAccountVideoRateValidator('like'))), 65 executeIfActivityPub,
60 executeIfActivityPub(getAccountVideoRate('like')) 66 asyncMiddleware(getAccountVideoRateValidator('like')),
67 getAccountVideoRate('like')
61) 68)
62activityPubClientRouter.get('/accounts?/:name/dislikes/:videoId', 69activityPubClientRouter.get('/accounts?/:name/dislikes/:videoId',
63 executeIfActivityPub(asyncMiddleware(getAccountVideoRateValidator('dislike'))), 70 executeIfActivityPub,
64 executeIfActivityPub(getAccountVideoRate('dislike')) 71 asyncMiddleware(getAccountVideoRateValidator('dislike')),
72 getAccountVideoRate('dislike')
65) 73)
66 74
67activityPubClientRouter.get('/videos/watch/:id', 75activityPubClientRouter.get('/videos/watch/:id',
68 executeIfActivityPub(asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS))), 76 executeIfActivityPub,
69 executeIfActivityPub(asyncMiddleware(videosGetValidator)), 77 asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.ACTIVITY_PUB.VIDEOS)),
70 executeIfActivityPub(asyncMiddleware(videoController)) 78 asyncMiddleware(videosCustomGetValidator('only-video-with-rights')),
79 asyncMiddleware(videoController)
71) 80)
72activityPubClientRouter.get('/videos/watch/:id/activity', 81activityPubClientRouter.get('/videos/watch/:id/activity',
73 executeIfActivityPub(asyncMiddleware(videosGetValidator)), 82 executeIfActivityPub,
74 executeIfActivityPub(asyncMiddleware(videoController)) 83 asyncMiddleware(videosCustomGetValidator('only-video-with-rights')),
84 asyncMiddleware(videoController)
75) 85)
76activityPubClientRouter.get('/videos/watch/:id/announces', 86activityPubClientRouter.get('/videos/watch/:id/announces',
77 executeIfActivityPub(asyncMiddleware(videosCustomGetValidator('only-video'))), 87 executeIfActivityPub,
78 executeIfActivityPub(asyncMiddleware(videoAnnouncesController)) 88 asyncMiddleware(videosCustomGetValidator('only-video')),
89 asyncMiddleware(videoAnnouncesController)
79) 90)
80activityPubClientRouter.get('/videos/watch/:id/announces/:actorId', 91activityPubClientRouter.get('/videos/watch/:id/announces/:actorId',
81 executeIfActivityPub(asyncMiddleware(videosShareValidator)), 92 executeIfActivityPub,
82 executeIfActivityPub(asyncMiddleware(videoAnnounceController)) 93 asyncMiddleware(videosShareValidator),
94 asyncMiddleware(videoAnnounceController)
83) 95)
84activityPubClientRouter.get('/videos/watch/:id/likes', 96activityPubClientRouter.get('/videos/watch/:id/likes',
85 executeIfActivityPub(asyncMiddleware(videosCustomGetValidator('only-video'))), 97 executeIfActivityPub,
86 executeIfActivityPub(asyncMiddleware(videoLikesController)) 98 asyncMiddleware(videosCustomGetValidator('only-video')),
99 asyncMiddleware(videoLikesController)
87) 100)
88activityPubClientRouter.get('/videos/watch/:id/dislikes', 101activityPubClientRouter.get('/videos/watch/:id/dislikes',
89 executeIfActivityPub(asyncMiddleware(videosCustomGetValidator('only-video'))), 102 executeIfActivityPub,
90 executeIfActivityPub(asyncMiddleware(videoDislikesController)) 103 asyncMiddleware(videosCustomGetValidator('only-video')),
104 asyncMiddleware(videoDislikesController)
91) 105)
92activityPubClientRouter.get('/videos/watch/:id/comments', 106activityPubClientRouter.get('/videos/watch/:id/comments',
93 executeIfActivityPub(asyncMiddleware(videosCustomGetValidator('only-video'))), 107 executeIfActivityPub,
94 executeIfActivityPub(asyncMiddleware(videoCommentsController)) 108 asyncMiddleware(videosCustomGetValidator('only-video')),
109 asyncMiddleware(videoCommentsController)
95) 110)
96activityPubClientRouter.get('/videos/watch/:videoId/comments/:commentId', 111activityPubClientRouter.get('/videos/watch/:videoId/comments/:commentId',
97 executeIfActivityPub(asyncMiddleware(videoCommentGetValidator)), 112 executeIfActivityPub,
98 executeIfActivityPub(asyncMiddleware(videoCommentController)) 113 asyncMiddleware(videoCommentGetValidator),
114 asyncMiddleware(videoCommentController)
99) 115)
100activityPubClientRouter.get('/videos/watch/:videoId/comments/:commentId/activity', 116activityPubClientRouter.get('/videos/watch/:videoId/comments/:commentId/activity',
101 executeIfActivityPub(asyncMiddleware(videoCommentGetValidator)), 117 executeIfActivityPub,
102 executeIfActivityPub(asyncMiddleware(videoCommentController)) 118 asyncMiddleware(videoCommentGetValidator),
119 asyncMiddleware(videoCommentController)
103) 120)
104 121
105activityPubClientRouter.get('/video-channels/:name', 122activityPubClientRouter.get('/video-channels/:name',
106 executeIfActivityPub(asyncMiddleware(localVideoChannelValidator)), 123 executeIfActivityPub,
107 executeIfActivityPub(asyncMiddleware(videoChannelController)) 124 asyncMiddleware(localVideoChannelValidator),
125 asyncMiddleware(videoChannelController)
108) 126)
109activityPubClientRouter.get('/video-channels/:name/followers', 127activityPubClientRouter.get('/video-channels/:name/followers',
110 executeIfActivityPub(asyncMiddleware(localVideoChannelValidator)), 128 executeIfActivityPub,
111 executeIfActivityPub(asyncMiddleware(videoChannelFollowersController)) 129 asyncMiddleware(localVideoChannelValidator),
130 asyncMiddleware(videoChannelFollowersController)
112) 131)
113activityPubClientRouter.get('/video-channels/:name/following', 132activityPubClientRouter.get('/video-channels/:name/following',
114 executeIfActivityPub(asyncMiddleware(localVideoChannelValidator)), 133 executeIfActivityPub,
115 executeIfActivityPub(asyncMiddleware(videoChannelFollowingController)) 134 asyncMiddleware(localVideoChannelValidator),
135 asyncMiddleware(videoChannelFollowingController)
116) 136)
117 137
118activityPubClientRouter.get('/redundancy/videos/:videoId/:resolution([0-9]+)(-:fps([0-9]+))?', 138activityPubClientRouter.get('/redundancy/videos/:videoId/:resolution([0-9]+)(-:fps([0-9]+))?',
119 executeIfActivityPub(asyncMiddleware(videoRedundancyGetValidator)), 139 executeIfActivityPub,
120 executeIfActivityPub(asyncMiddleware(videoRedundancyController)) 140 asyncMiddleware(videoFileRedundancyGetValidator),
141 asyncMiddleware(videoRedundancyController)
142)
143activityPubClientRouter.get('/redundancy/streaming-playlists/:streamingPlaylistType/:videoId',
144 executeIfActivityPub,
145 asyncMiddleware(videoPlaylistRedundancyGetValidator),
146 asyncMiddleware(videoRedundancyController)
147)
148
149activityPubClientRouter.get('/video-playlists/:playlistId',
150 executeIfActivityPub,
151 asyncMiddleware(videoPlaylistsGetValidator),
152 asyncMiddleware(videoPlaylistController)
153)
154activityPubClientRouter.get('/video-playlists/:playlistId/:videoId',
155 executeIfActivityPub,
156 asyncMiddleware(videoPlaylistElementAPGetValidator),
157 asyncMiddleware(videoPlaylistElementController)
121) 158)
122 159
123// --------------------------------------------------------------------------- 160// ---------------------------------------------------------------------------
@@ -128,44 +165,52 @@ export {
128 165
129// --------------------------------------------------------------------------- 166// ---------------------------------------------------------------------------
130 167
131function accountController (req: express.Request, res: express.Response, next: express.NextFunction) { 168function accountController (req: express.Request, res: express.Response) {
132 const account: AccountModel = res.locals.account 169 const account = res.locals.account
133 170
134 return activityPubResponse(activityPubContextify(account.toActivityPubObject()), res) 171 return activityPubResponse(activityPubContextify(account.toActivityPubObject()), res)
135} 172}
136 173
137async function accountFollowersController (req: express.Request, res: express.Response, next: express.NextFunction) { 174async function accountFollowersController (req: express.Request, res: express.Response) {
138 const account: AccountModel = res.locals.account 175 const account = res.locals.account
139 const activityPubResult = await actorFollowers(req, account.Actor) 176 const activityPubResult = await actorFollowers(req, account.Actor)
140 177
141 return activityPubResponse(activityPubContextify(activityPubResult), res) 178 return activityPubResponse(activityPubContextify(activityPubResult), res)
142} 179}
143 180
144async function accountFollowingController (req: express.Request, res: express.Response, next: express.NextFunction) { 181async function accountFollowingController (req: express.Request, res: express.Response) {
145 const account: AccountModel = res.locals.account 182 const account = res.locals.account
146 const activityPubResult = await actorFollowing(req, account.Actor) 183 const activityPubResult = await actorFollowing(req, account.Actor)
147 184
148 return activityPubResponse(activityPubContextify(activityPubResult), res) 185 return activityPubResponse(activityPubContextify(activityPubResult), res)
149} 186}
150 187
188async function accountPlaylistsController (req: express.Request, res: express.Response) {
189 const account = res.locals.account
190 const activityPubResult = await actorPlaylists(req, account)
191
192 return activityPubResponse(activityPubContextify(activityPubResult), res)
193}
194
151function getAccountVideoRate (rateType: VideoRateType) { 195function getAccountVideoRate (rateType: VideoRateType) {
152 return (req: express.Request, res: express.Response) => { 196 return (req: express.Request, res: express.Response) => {
153 const accountVideoRate: AccountVideoRateModel = res.locals.accountVideoRate 197 const accountVideoRate = res.locals.accountVideoRate
154 198
155 const byActor = accountVideoRate.Account.Actor 199 const byActor = accountVideoRate.Account.Actor
156 const url = getRateUrl(rateType, byActor, accountVideoRate.Video) 200 const url = getRateUrl(rateType, byActor, accountVideoRate.Video)
157 const APObject = rateType === 'like' 201 const APObject = rateType === 'like'
158 ? buildLikeActivity(url, byActor, accountVideoRate.Video) 202 ? buildLikeActivity(url, byActor, accountVideoRate.Video)
159 : buildCreateActivity(url, byActor, buildDislikeActivity(url, byActor, accountVideoRate.Video)) 203 : buildDislikeActivity(url, byActor, accountVideoRate.Video)
160 204
161 return activityPubResponse(activityPubContextify(APObject), res) 205 return activityPubResponse(activityPubContextify(APObject), res)
162 } 206 }
163} 207}
164 208
165async function videoController (req: express.Request, res: express.Response) { 209async function videoController (req: express.Request, res: express.Response) {
166 const video: VideoModel = res.locals.video 210 // We need more attributes
211 const video = await VideoModel.loadForGetAPI(res.locals.video.id)
167 212
168 if (video.url.startsWith(CONFIG.WEBSERVER.URL) === false) return res.redirect(video.url) 213 if (video.url.startsWith(WEBSERVER.URL) === false) return res.redirect(video.url)
169 214
170 // We need captions to render AP object 215 // We need captions to render AP object
171 video.VideoCaptions = await VideoCaptionModel.listVideoCaptions(video.id) 216 video.VideoCaptions = await VideoCaptionModel.listVideoCaptions(video.id)
@@ -182,9 +227,9 @@ async function videoController (req: express.Request, res: express.Response) {
182} 227}
183 228
184async function videoAnnounceController (req: express.Request, res: express.Response) { 229async function videoAnnounceController (req: express.Request, res: express.Response) {
185 const share = res.locals.videoShare as VideoShareModel 230 const share = res.locals.videoShare
186 231
187 if (share.url.startsWith(CONFIG.WEBSERVER.URL) === false) return res.redirect(share.url) 232 if (share.url.startsWith(WEBSERVER.URL) === false) return res.redirect(share.url)
188 233
189 const { activity } = await buildAnnounceWithVideoAudience(share.Actor, share, res.locals.video, undefined) 234 const { activity } = await buildAnnounceWithVideoAudience(share.Actor, share, res.locals.video, undefined)
190 235
@@ -192,7 +237,7 @@ async function videoAnnounceController (req: express.Request, res: express.Respo
192} 237}
193 238
194async function videoAnnouncesController (req: express.Request, res: express.Response) { 239async function videoAnnouncesController (req: express.Request, res: express.Response) {
195 const video: VideoModel = res.locals.video 240 const video = res.locals.video
196 241
197 const handler = async (start: number, count: number) => { 242 const handler = async (start: number, count: number) => {
198 const result = await VideoShareModel.listAndCountByVideoId(video.id, start, count) 243 const result = await VideoShareModel.listAndCountByVideoId(video.id, start, count)
@@ -207,21 +252,21 @@ async function videoAnnouncesController (req: express.Request, res: express.Resp
207} 252}
208 253
209async function videoLikesController (req: express.Request, res: express.Response) { 254async function videoLikesController (req: express.Request, res: express.Response) {
210 const video: VideoModel = res.locals.video 255 const video = res.locals.video
211 const json = await videoRates(req, 'like', video, getVideoLikesActivityPubUrl(video)) 256 const json = await videoRates(req, 'like', video, getVideoLikesActivityPubUrl(video))
212 257
213 return activityPubResponse(activityPubContextify(json), res) 258 return activityPubResponse(activityPubContextify(json), res)
214} 259}
215 260
216async function videoDislikesController (req: express.Request, res: express.Response) { 261async function videoDislikesController (req: express.Request, res: express.Response) {
217 const video: VideoModel = res.locals.video 262 const video = res.locals.video
218 const json = await videoRates(req, 'dislike', video, getVideoDislikesActivityPubUrl(video)) 263 const json = await videoRates(req, 'dislike', video, getVideoDislikesActivityPubUrl(video))
219 264
220 return activityPubResponse(activityPubContextify(json), res) 265 return activityPubResponse(activityPubContextify(json), res)
221} 266}
222 267
223async function videoCommentsController (req: express.Request, res: express.Response) { 268async function videoCommentsController (req: express.Request, res: express.Response) {
224 const video: VideoModel = res.locals.video 269 const video = res.locals.video
225 270
226 const handler = async (start: number, count: number) => { 271 const handler = async (start: number, count: number) => {
227 const result = await VideoCommentModel.listAndCountByVideoId(video.id, start, count) 272 const result = await VideoCommentModel.listAndCountByVideoId(video.id, start, count)
@@ -236,29 +281,29 @@ async function videoCommentsController (req: express.Request, res: express.Respo
236} 281}
237 282
238async function videoChannelController (req: express.Request, res: express.Response) { 283async function videoChannelController (req: express.Request, res: express.Response) {
239 const videoChannel: VideoChannelModel = res.locals.videoChannel 284 const videoChannel = res.locals.videoChannel
240 285
241 return activityPubResponse(activityPubContextify(videoChannel.toActivityPubObject()), res) 286 return activityPubResponse(activityPubContextify(videoChannel.toActivityPubObject()), res)
242} 287}
243 288
244async function videoChannelFollowersController (req: express.Request, res: express.Response) { 289async function videoChannelFollowersController (req: express.Request, res: express.Response) {
245 const videoChannel: VideoChannelModel = res.locals.videoChannel 290 const videoChannel = res.locals.videoChannel
246 const activityPubResult = await actorFollowers(req, videoChannel.Actor) 291 const activityPubResult = await actorFollowers(req, videoChannel.Actor)
247 292
248 return activityPubResponse(activityPubContextify(activityPubResult), res) 293 return activityPubResponse(activityPubContextify(activityPubResult), res)
249} 294}
250 295
251async function videoChannelFollowingController (req: express.Request, res: express.Response) { 296async function videoChannelFollowingController (req: express.Request, res: express.Response) {
252 const videoChannel: VideoChannelModel = res.locals.videoChannel 297 const videoChannel = res.locals.videoChannel
253 const activityPubResult = await actorFollowing(req, videoChannel.Actor) 298 const activityPubResult = await actorFollowing(req, videoChannel.Actor)
254 299
255 return activityPubResponse(activityPubContextify(activityPubResult), res) 300 return activityPubResponse(activityPubContextify(activityPubResult), res)
256} 301}
257 302
258async function videoCommentController (req: express.Request, res: express.Response) { 303async function videoCommentController (req: express.Request, res: express.Response) {
259 const videoComment: VideoCommentModel = res.locals.videoComment 304 const videoComment = res.locals.videoComment
260 305
261 if (videoComment.url.startsWith(CONFIG.WEBSERVER.URL) === false) return res.redirect(videoComment.url) 306 if (videoComment.url.startsWith(WEBSERVER.URL) === false) return res.redirect(videoComment.url)
262 307
263 const threadParentComments = await VideoCommentModel.listThreadParentComments(videoComment, undefined) 308 const threadParentComments = await VideoCommentModel.listThreadParentComments(videoComment, undefined)
264 const isPublic = true // Comments are always public 309 const isPublic = true // Comments are always public
@@ -275,8 +320,8 @@ async function videoCommentController (req: express.Request, res: express.Respon
275} 320}
276 321
277async function videoRedundancyController (req: express.Request, res: express.Response) { 322async function videoRedundancyController (req: express.Request, res: express.Response) {
278 const videoRedundancy: VideoRedundancyModel = res.locals.videoRedundancy 323 const videoRedundancy = res.locals.videoRedundancy
279 if (videoRedundancy.url.startsWith(CONFIG.WEBSERVER.URL) === false) return res.redirect(videoRedundancy.url) 324 if (videoRedundancy.url.startsWith(WEBSERVER.URL) === false) return res.redirect(videoRedundancy.url)
280 325
281 const serverActor = await getServerActor() 326 const serverActor = await getServerActor()
282 327
@@ -291,6 +336,26 @@ async function videoRedundancyController (req: express.Request, res: express.Res
291 return activityPubResponse(activityPubContextify(object), res) 336 return activityPubResponse(activityPubContextify(object), res)
292} 337}
293 338
339async function videoPlaylistController (req: express.Request, res: express.Response) {
340 const playlist = res.locals.videoPlaylist
341
342 // We need more attributes
343 playlist.OwnerAccount = await AccountModel.load(playlist.ownerAccountId)
344
345 const json = await playlist.toActivityPubObject(req.query.page, null)
346 const audience = getAudience(playlist.OwnerAccount.Actor, playlist.privacy === VideoPlaylistPrivacy.PUBLIC)
347 const object = audiencify(json, audience)
348
349 return activityPubResponse(activityPubContextify(object), res)
350}
351
352async function videoPlaylistElementController (req: express.Request, res: express.Response) {
353 const videoPlaylistElement = res.locals.videoPlaylistElement
354
355 const json = videoPlaylistElement.toActivityPubObject()
356 return activityPubResponse(activityPubContextify(json), res)
357}
358
294// --------------------------------------------------------------------------- 359// ---------------------------------------------------------------------------
295 360
296async function actorFollowing (req: express.Request, actor: ActorModel) { 361async function actorFollowing (req: express.Request, actor: ActorModel) {
@@ -298,15 +363,23 @@ async function actorFollowing (req: express.Request, actor: ActorModel) {
298 return ActorFollowModel.listAcceptedFollowingUrlsForApi([ actor.id ], undefined, start, count) 363 return ActorFollowModel.listAcceptedFollowingUrlsForApi([ actor.id ], undefined, start, count)
299 } 364 }
300 365
301 return activityPubCollectionPagination(CONFIG.WEBSERVER.URL + req.path, handler, req.query.page) 366 return activityPubCollectionPagination(WEBSERVER.URL + req.path, handler, req.query.page)
302} 367}
303 368
304async function actorFollowers (req: express.Request, actor: ActorModel) { 369async function actorFollowers (req: express.Request, actor: ActorModel) {
305 const handler = (start: number, count: number) => { 370 const handler = (start: number, count: number) => {
306 return ActorFollowModel.listAcceptedFollowerUrlsForApi([ actor.id ], undefined, start, count) 371 return ActorFollowModel.listAcceptedFollowerUrlsForAP([ actor.id ], undefined, start, count)
372 }
373
374 return activityPubCollectionPagination(WEBSERVER.URL + req.path, handler, req.query.page)
375}
376
377async function actorPlaylists (req: express.Request, account: AccountModel) {
378 const handler = (start: number, count: number) => {
379 return VideoPlaylistModel.listPublicUrlsOfForAP(account.id, start, count)
307 } 380 }
308 381
309 return activityPubCollectionPagination(CONFIG.WEBSERVER.URL + req.path, handler, req.query.page) 382 return activityPubCollectionPagination(WEBSERVER.URL + req.path, handler, req.query.page)
310} 383}
311 384
312function videoRates (req: express.Request, rateType: VideoRateType, video: VideoModel, url: string) { 385function videoRates (req: express.Request, rateType: VideoRateType, video: VideoModel, url: string) {
diff --git a/server/controllers/activitypub/inbox.ts b/server/controllers/activitypub/inbox.ts
index f0e65015b..38d5c51df 100644
--- a/server/controllers/activitypub/inbox.ts
+++ b/server/controllers/activitypub/inbox.ts
@@ -5,8 +5,6 @@ import { logger } from '../../helpers/logger'
5import { processActivities } from '../../lib/activitypub/process/process' 5import { processActivities } from '../../lib/activitypub/process/process'
6import { asyncMiddleware, checkSignature, localAccountValidator, localVideoChannelValidator, signatureValidator } from '../../middlewares' 6import { asyncMiddleware, checkSignature, localAccountValidator, localVideoChannelValidator, signatureValidator } from '../../middlewares'
7import { activityPubValidator } from '../../middlewares/validators/activitypub/activity' 7import { activityPubValidator } from '../../middlewares/validators/activitypub/activity'
8import { VideoChannelModel } from '../../models/video/video-channel'
9import { AccountModel } from '../../models/account/account'
10import { queue } from 'async' 8import { queue } from 'async'
11import { ActorModel } from '../../models/activitypub/actor' 9import { ActorModel } from '../../models/activitypub/actor'
12 10
@@ -66,12 +64,7 @@ function inboxController (req: express.Request, res: express.Response) {
66 activities = activities.filter(a => isActivityValid(a)) 64 activities = activities.filter(a => isActivityValid(a))
67 logger.debug('We keep %d activities.', activities.length, { activities }) 65 logger.debug('We keep %d activities.', activities.length, { activities })
68 66
69 let accountOrChannel: VideoChannelModel | AccountModel 67 const accountOrChannel = res.locals.account || res.locals.videoChannel
70 if (res.locals.account) {
71 accountOrChannel = res.locals.account
72 } else if (res.locals.videoChannel) {
73 accountOrChannel = res.locals.videoChannel
74 }
75 68
76 logger.info('Receiving inbox requests for %d activities by %s.', activities.length, res.locals.signature.actor.url) 69 logger.info('Receiving inbox requests for %d activities by %s.', activities.length, res.locals.signature.actor.url)
77 70
diff --git a/server/controllers/activitypub/outbox.ts b/server/controllers/activitypub/outbox.ts
index bd0e4fe9d..38b6ec976 100644
--- a/server/controllers/activitypub/outbox.ts
+++ b/server/controllers/activitypub/outbox.ts
@@ -32,8 +32,8 @@ export {
32 32
33// --------------------------------------------------------------------------- 33// ---------------------------------------------------------------------------
34 34
35async function outboxController (req: express.Request, res: express.Response, next: express.NextFunction) { 35async function outboxController (req: express.Request, res: express.Response) {
36 const accountOrVideoChannel: AccountModel | VideoChannelModel = res.locals.account || res.locals.videoChannel 36 const accountOrVideoChannel = res.locals.account || res.locals.videoChannel
37 const actor = accountOrVideoChannel.Actor 37 const actor = accountOrVideoChannel.Actor
38 const actorOutboxUrl = actor.url + '/outbox' 38 const actorOutboxUrl = actor.url + '/outbox'
39 39
diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts
index 8c0237203..8d4db1e75 100644
--- a/server/controllers/api/accounts.ts
+++ b/server/controllers/api/accounts.ts
@@ -1,21 +1,32 @@
1import * as express from 'express' 1import * as express from 'express'
2import { getFormattedObjects } from '../../helpers/utils' 2import { getFormattedObjects, getServerActor } from '../../helpers/utils'
3import { 3import {
4 asyncMiddleware, 4 asyncMiddleware,
5 authenticate,
5 commonVideosFiltersValidator, 6 commonVideosFiltersValidator,
6 listVideoAccountChannelsValidator,
7 optionalAuthenticate, 7 optionalAuthenticate,
8 paginationValidator, 8 paginationValidator,
9 setDefaultPagination, 9 setDefaultPagination,
10 setDefaultSort 10 setDefaultSort,
11 videoPlaylistsSortValidator,
12 videoRatesSortValidator,
13 videoRatingValidator
11} from '../../middlewares' 14} from '../../middlewares'
12import { accountsNameWithHostGetValidator, accountsSortValidator, videosSortValidator } from '../../middlewares/validators' 15import {
16 accountNameWithHostGetValidator,
17 accountsSortValidator,
18 ensureAuthUserOwnsAccountValidator,
19 videosSortValidator
20} from '../../middlewares/validators'
13import { AccountModel } from '../../models/account/account' 21import { AccountModel } from '../../models/account/account'
22import { AccountVideoRateModel } from '../../models/account/account-video-rate'
14import { VideoModel } from '../../models/video/video' 23import { VideoModel } from '../../models/video/video'
15import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' 24import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
16import { VideoChannelModel } from '../../models/video/video-channel' 25import { VideoChannelModel } from '../../models/video/video-channel'
17import { JobQueue } from '../../lib/job-queue' 26import { JobQueue } from '../../lib/job-queue'
18import { logger } from '../../helpers/logger' 27import { logger } from '../../helpers/logger'
28import { VideoPlaylistModel } from '../../models/video/video-playlist'
29import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists'
19 30
20const accountsRouter = express.Router() 31const accountsRouter = express.Router()
21 32
@@ -28,12 +39,12 @@ accountsRouter.get('/',
28) 39)
29 40
30accountsRouter.get('/:accountName', 41accountsRouter.get('/:accountName',
31 asyncMiddleware(accountsNameWithHostGetValidator), 42 asyncMiddleware(accountNameWithHostGetValidator),
32 getAccount 43 getAccount
33) 44)
34 45
35accountsRouter.get('/:accountName/videos', 46accountsRouter.get('/:accountName/videos',
36 asyncMiddleware(accountsNameWithHostGetValidator), 47 asyncMiddleware(accountNameWithHostGetValidator),
37 paginationValidator, 48 paginationValidator,
38 videosSortValidator, 49 videosSortValidator,
39 setDefaultSort, 50 setDefaultSort,
@@ -44,8 +55,31 @@ accountsRouter.get('/:accountName/videos',
44) 55)
45 56
46accountsRouter.get('/:accountName/video-channels', 57accountsRouter.get('/:accountName/video-channels',
47 asyncMiddleware(listVideoAccountChannelsValidator), 58 asyncMiddleware(accountNameWithHostGetValidator),
48 asyncMiddleware(listVideoAccountChannels) 59 asyncMiddleware(listAccountChannels)
60)
61
62accountsRouter.get('/:accountName/video-playlists',
63 optionalAuthenticate,
64 asyncMiddleware(accountNameWithHostGetValidator),
65 paginationValidator,
66 videoPlaylistsSortValidator,
67 setDefaultSort,
68 setDefaultPagination,
69 commonVideoPlaylistFiltersValidator,
70 asyncMiddleware(listAccountPlaylists)
71)
72
73accountsRouter.get('/:accountName/ratings',
74 authenticate,
75 asyncMiddleware(accountNameWithHostGetValidator),
76 ensureAuthUserOwnsAccountValidator,
77 paginationValidator,
78 videoRatesSortValidator,
79 setDefaultSort,
80 setDefaultPagination,
81 videoRatingValidator,
82 asyncMiddleware(listAccountRatings)
49) 83)
50 84
51// --------------------------------------------------------------------------- 85// ---------------------------------------------------------------------------
@@ -56,8 +90,8 @@ export {
56 90
57// --------------------------------------------------------------------------- 91// ---------------------------------------------------------------------------
58 92
59function getAccount (req: express.Request, res: express.Response, next: express.NextFunction) { 93function getAccount (req: express.Request, res: express.Response) {
60 const account: AccountModel = res.locals.account 94 const account = res.locals.account
61 95
62 if (account.isOutdated()) { 96 if (account.isOutdated()) {
63 JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'actor', url: account.Actor.url } }) 97 JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'actor', url: account.Actor.url } })
@@ -67,20 +101,42 @@ function getAccount (req: express.Request, res: express.Response, next: express.
67 return res.json(account.toFormattedJSON()) 101 return res.json(account.toFormattedJSON())
68} 102}
69 103
70async function listAccounts (req: express.Request, res: express.Response, next: express.NextFunction) { 104async function listAccounts (req: express.Request, res: express.Response) {
71 const resultList = await AccountModel.listForApi(req.query.start, req.query.count, req.query.sort) 105 const resultList = await AccountModel.listForApi(req.query.start, req.query.count, req.query.sort)
72 106
73 return res.json(getFormattedObjects(resultList.data, resultList.total)) 107 return res.json(getFormattedObjects(resultList.data, resultList.total))
74} 108}
75 109
76async function listVideoAccountChannels (req: express.Request, res: express.Response, next: express.NextFunction) { 110async function listAccountChannels (req: express.Request, res: express.Response) {
77 const resultList = await VideoChannelModel.listByAccount(res.locals.account.id) 111 const resultList = await VideoChannelModel.listByAccount(res.locals.account.id)
78 112
79 return res.json(getFormattedObjects(resultList.data, resultList.total)) 113 return res.json(getFormattedObjects(resultList.data, resultList.total))
80} 114}
81 115
82async function listAccountVideos (req: express.Request, res: express.Response, next: express.NextFunction) { 116async function listAccountPlaylists (req: express.Request, res: express.Response) {
83 const account: AccountModel = res.locals.account 117 const serverActor = await getServerActor()
118
119 // Allow users to see their private/unlisted video playlists
120 let privateAndUnlisted = false
121 if (res.locals.oauth && res.locals.oauth.token.User.Account.id === res.locals.account.id) {
122 privateAndUnlisted = true
123 }
124
125 const resultList = await VideoPlaylistModel.listForApi({
126 followerActorId: serverActor.id,
127 start: req.query.start,
128 count: req.query.count,
129 sort: req.query.sort,
130 accountId: res.locals.account.id,
131 privateAndUnlisted,
132 type: req.query.playlistType
133 })
134
135 return res.json(getFormattedObjects(resultList.data, resultList.total))
136}
137
138async function listAccountVideos (req: express.Request, res: express.Response) {
139 const account = res.locals.account
84 const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined 140 const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined
85 141
86 const resultList = await VideoModel.listForApi({ 142 const resultList = await VideoModel.listForApi({
@@ -103,3 +159,16 @@ async function listAccountVideos (req: express.Request, res: express.Response, n
103 159
104 return res.json(getFormattedObjects(resultList.data, resultList.total)) 160 return res.json(getFormattedObjects(resultList.data, resultList.total))
105} 161}
162
163async function listAccountRatings (req: express.Request, res: express.Response) {
164 const account = res.locals.account
165
166 const resultList = await AccountVideoRateModel.listByAccountForApi({
167 accountId: account.id,
168 start: req.query.start,
169 count: req.query.count,
170 sort: req.query.sort,
171 type: req.query.rating
172 })
173 return res.json(getFormattedObjects(resultList.rows, resultList.count))
174}
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts
index 255026f46..40012c03b 100644
--- a/server/controllers/api/config.ts
+++ b/server/controllers/api/config.ts
@@ -1,10 +1,10 @@
1import * as express from 'express' 1import * as express from 'express'
2import { omit, snakeCase } from 'lodash' 2import { 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'
6import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup' 6import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup'
7import { CONFIG, CONSTRAINTS_FIELDS, reloadConfig } from '../../initializers' 7import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
8import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares' 8import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares'
9import { customConfigUpdateValidator } from '../../middlewares/validators/config' 9import { customConfigUpdateValidator } from '../../middlewares/validators/config'
10import { ClientHtml } from '../../lib/client-html' 10import { ClientHtml } from '../../lib/client-html'
@@ -14,6 +14,7 @@ import { getServerCommit } from '../../helpers/utils'
14import { Emailer } from '../../lib/emailer' 14import { Emailer } from '../../lib/emailer'
15import { isNumeric } from 'validator' 15import { isNumeric } from 'validator'
16import { objectConverter } from '../../helpers/core-utils' 16import { objectConverter } from '../../helpers/core-utils'
17import { CONFIG, reloadConfig } from '../../initializers/config'
17 18
18const packageJSON = require('../../../../package.json') 19const packageJSON = require('../../../../package.json')
19const configRouter = express.Router() 20const configRouter = express.Router()
@@ -58,6 +59,7 @@ async function getConfig (req: express.Request, res: express.Response) {
58 name: CONFIG.INSTANCE.NAME, 59 name: CONFIG.INSTANCE.NAME,
59 shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION, 60 shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
60 defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE, 61 defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE,
62 isNSFW: CONFIG.INSTANCE.IS_NSFW,
61 defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY, 63 defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
62 customizations: { 64 customizations: {
63 javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT, 65 javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT,
@@ -78,6 +80,9 @@ async function getConfig (req: express.Request, res: express.Response) {
78 requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION 80 requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION
79 }, 81 },
80 transcoding: { 82 transcoding: {
83 hls: {
84 enabled: CONFIG.TRANSCODING.HLS.ENABLED
85 },
81 enabledResolutions 86 enabledResolutions
82 }, 87 },
83 import: { 88 import: {
@@ -90,6 +95,13 @@ async function getConfig (req: express.Request, res: express.Response) {
90 } 95 }
91 } 96 }
92 }, 97 },
98 autoBlacklist: {
99 videos: {
100 ofUsers: {
101 enabled: CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED
102 }
103 }
104 },
93 avatar: { 105 avatar: {
94 file: { 106 file: {
95 size: { 107 size: {
@@ -125,13 +137,16 @@ async function getConfig (req: express.Request, res: express.Response) {
125 videos: { 137 videos: {
126 intervalDays: CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS 138 intervalDays: CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS
127 } 139 }
140 },
141 tracker: {
142 enabled: CONFIG.TRACKER.ENABLED
128 } 143 }
129 } 144 }
130 145
131 return res.json(json) 146 return res.json(json)
132} 147}
133 148
134function getAbout (req: express.Request, res: express.Response, next: express.NextFunction) { 149function getAbout (req: express.Request, res: express.Response) {
135 const about: About = { 150 const about: About = {
136 instance: { 151 instance: {
137 name: CONFIG.INSTANCE.NAME, 152 name: CONFIG.INSTANCE.NAME,
@@ -144,13 +159,13 @@ function getAbout (req: express.Request, res: express.Response, next: express.Ne
144 return res.json(about).end() 159 return res.json(about).end()
145} 160}
146 161
147async function getCustomConfig (req: express.Request, res: express.Response, next: express.NextFunction) { 162async function getCustomConfig (req: express.Request, res: express.Response) {
148 const data = customConfig() 163 const data = customConfig()
149 164
150 return res.json(data).end() 165 return res.json(data).end()
151} 166}
152 167
153async function deleteCustomConfig (req: express.Request, res: express.Response, next: express.NextFunction) { 168async function deleteCustomConfig (req: express.Request, res: express.Response) {
154 await remove(CONFIG.CUSTOM_FILE) 169 await remove(CONFIG.CUSTOM_FILE)
155 170
156 auditLogger.delete(getAuditIdFromRes(res), new CustomConfigAuditView(customConfig())) 171 auditLogger.delete(getAuditIdFromRes(res), new CustomConfigAuditView(customConfig()))
@@ -163,7 +178,7 @@ async function deleteCustomConfig (req: express.Request, res: express.Response,
163 return res.json(data).end() 178 return res.json(data).end()
164} 179}
165 180
166async function updateCustomConfig (req: express.Request, res: express.Response, next: express.NextFunction) { 181async function updateCustomConfig (req: express.Request, res: express.Response) {
167 const oldCustomConfigAuditKeys = new CustomConfigAuditView(customConfig()) 182 const oldCustomConfigAuditKeys = new CustomConfigAuditView(customConfig())
168 183
169 // camelCase to snake_case key + Force number conversion 184 // camelCase to snake_case key + Force number conversion
@@ -200,6 +215,7 @@ function customConfig (): CustomConfig {
200 shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION, 215 shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
201 description: CONFIG.INSTANCE.DESCRIPTION, 216 description: CONFIG.INSTANCE.DESCRIPTION,
202 terms: CONFIG.INSTANCE.TERMS, 217 terms: CONFIG.INSTANCE.TERMS,
218 isNSFW: CONFIG.INSTANCE.IS_NSFW,
203 defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE, 219 defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE,
204 defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY, 220 defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
205 customizations: { 221 customizations: {
@@ -246,6 +262,9 @@ function customConfig (): CustomConfig {
246 '480p': CONFIG.TRANSCODING.RESOLUTIONS[ '480p' ], 262 '480p': CONFIG.TRANSCODING.RESOLUTIONS[ '480p' ],
247 '720p': CONFIG.TRANSCODING.RESOLUTIONS[ '720p' ], 263 '720p': CONFIG.TRANSCODING.RESOLUTIONS[ '720p' ],
248 '1080p': CONFIG.TRANSCODING.RESOLUTIONS[ '1080p' ] 264 '1080p': CONFIG.TRANSCODING.RESOLUTIONS[ '1080p' ]
265 },
266 hls: {
267 enabled: CONFIG.TRANSCODING.HLS.ENABLED
249 } 268 }
250 }, 269 },
251 import: { 270 import: {
@@ -257,6 +276,19 @@ function customConfig (): CustomConfig {
257 enabled: CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED 276 enabled: CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED
258 } 277 }
259 } 278 }
279 },
280 autoBlacklist: {
281 videos: {
282 ofUsers: {
283 enabled: CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED
284 }
285 }
286 },
287 followers: {
288 instance: {
289 enabled: CONFIG.FOLLOWERS.INSTANCE.ENABLED,
290 manualApproval: CONFIG.FOLLOWERS.INSTANCE.MANUAL_APPROVAL
291 }
260 } 292 }
261 } 293 }
262} 294}
diff --git a/server/controllers/api/index.ts b/server/controllers/api/index.ts
index 8a58b5466..60a84036e 100644
--- a/server/controllers/api/index.ts
+++ b/server/controllers/api/index.ts
@@ -11,6 +11,7 @@ import { videoChannelRouter } from './video-channel'
11import * as cors from 'cors' 11import * as cors from 'cors'
12import { searchRouter } from './search' 12import { searchRouter } from './search'
13import { overviewsRouter } from './overviews' 13import { overviewsRouter } from './overviews'
14import { videoPlaylistRouter } from './video-playlist'
14 15
15const apiRouter = express.Router() 16const apiRouter = express.Router()
16 17
@@ -26,6 +27,7 @@ apiRouter.use('/config', configRouter)
26apiRouter.use('/users', usersRouter) 27apiRouter.use('/users', usersRouter)
27apiRouter.use('/accounts', accountsRouter) 28apiRouter.use('/accounts', accountsRouter)
28apiRouter.use('/video-channels', videoChannelRouter) 29apiRouter.use('/video-channels', videoChannelRouter)
30apiRouter.use('/video-playlists', videoPlaylistRouter)
29apiRouter.use('/videos', videosRouter) 31apiRouter.use('/videos', videosRouter)
30apiRouter.use('/jobs', jobsRouter) 32apiRouter.use('/jobs', jobsRouter)
31apiRouter.use('/search', searchRouter) 33apiRouter.use('/search', searchRouter)
@@ -39,6 +41,6 @@ export { apiRouter }
39 41
40// --------------------------------------------------------------------------- 42// ---------------------------------------------------------------------------
41 43
42function pong (req: express.Request, res: express.Response, next: express.NextFunction) { 44function pong (req: express.Request, res: express.Response) {
43 return res.send('pong').status(200).end() 45 return res.send('pong').status(200).end()
44} 46}
diff --git a/server/controllers/api/oauth-clients.ts b/server/controllers/api/oauth-clients.ts
index 3dcc023e6..b2de8bcf5 100644
--- a/server/controllers/api/oauth-clients.ts
+++ b/server/controllers/api/oauth-clients.ts
@@ -1,7 +1,7 @@
1import * as express from 'express' 1import * as express from 'express'
2import { OAuthClientLocal } from '../../../shared' 2import { OAuthClientLocal } from '../../../shared'
3import { logger } from '../../helpers/logger' 3import { logger } from '../../helpers/logger'
4import { CONFIG } from '../../initializers' 4import { CONFIG } from '../../initializers/config'
5import { asyncMiddleware } from '../../middlewares' 5import { asyncMiddleware } from '../../middlewares'
6import { OAuthClientModel } from '../../models/oauth/oauth-client' 6import { OAuthClientModel } from '../../models/oauth/oauth-client'
7 7
diff --git a/server/controllers/api/overviews.ts b/server/controllers/api/overviews.ts
index 8b6773056..37ac152db 100644
--- a/server/controllers/api/overviews.ts
+++ b/server/controllers/api/overviews.ts
@@ -4,7 +4,7 @@ import { VideoModel } from '../../models/video/video'
4import { asyncMiddleware } from '../../middlewares' 4import { asyncMiddleware } from '../../middlewares'
5import { TagModel } from '../../models/video/tag' 5import { TagModel } from '../../models/video/tag'
6import { VideosOverview } from '../../../shared/models/overviews' 6import { VideosOverview } from '../../../shared/models/overviews'
7import { MEMOIZE_TTL, OVERVIEWS, ROUTE_CACHE_LIFETIME } from '../../initializers' 7import { MEMOIZE_TTL, OVERVIEWS, ROUTE_CACHE_LIFETIME } from '../../initializers/constants'
8import { cacheRoute } from '../../middlewares/cache' 8import { cacheRoute } from '../../middlewares/cache'
9import * as memoizee from 'memoizee' 9import * as memoizee from 'memoizee'
10 10
@@ -94,7 +94,7 @@ async function getVideos (
94) { 94) {
95 const query = Object.assign({ 95 const query = Object.assign({
96 start: 0, 96 start: 0,
97 count: 10, 97 count: 12,
98 sort: '-createdAt', 98 sort: '-createdAt',
99 includeLocalVideos: true, 99 includeLocalVideos: true,
100 nsfw: buildNSFWFilter(res), 100 nsfw: buildNSFWFilter(res),
diff --git a/server/controllers/api/server/debug.ts b/server/controllers/api/server/debug.ts
new file mode 100644
index 000000000..4450038f6
--- /dev/null
+++ b/server/controllers/api/server/debug.ts
@@ -0,0 +1,25 @@
1import * as express from 'express'
2import { UserRight } from '../../../../shared/models/users'
3import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../../middlewares'
4
5const debugRouter = express.Router()
6
7debugRouter.get('/debug',
8 authenticate,
9 ensureUserHasRight(UserRight.MANAGE_DEBUG),
10 asyncMiddleware(getDebug)
11)
12
13// ---------------------------------------------------------------------------
14
15export {
16 debugRouter
17}
18
19// ---------------------------------------------------------------------------
20
21async function getDebug (req: express.Request, res: express.Response) {
22 return res.json({
23 ip: req.ip
24 }).end()
25}
diff --git a/server/controllers/api/server/follows.ts b/server/controllers/api/server/follows.ts
index 9fa6c34ba..d38ce91de 100644
--- a/server/controllers/api/server/follows.ts
+++ b/server/controllers/api/server/follows.ts
@@ -2,22 +2,29 @@ import * as express from 'express'
2import { UserRight } from '../../../../shared/models/users' 2import { UserRight } from '../../../../shared/models/users'
3import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
4import { getFormattedObjects, getServerActor } from '../../../helpers/utils' 4import { getFormattedObjects, getServerActor } from '../../../helpers/utils'
5import { sequelizeTypescript, SERVER_ACTOR_NAME } from '../../../initializers' 5import { SERVER_ACTOR_NAME } from '../../../initializers/constants'
6import { sendUndoFollow } from '../../../lib/activitypub/send' 6import { sendAccept, sendReject, sendUndoFollow } from '../../../lib/activitypub/send'
7import { 7import {
8 asyncMiddleware, 8 asyncMiddleware,
9 authenticate, 9 authenticate,
10 ensureUserHasRight, 10 ensureUserHasRight,
11 paginationValidator, 11 paginationValidator,
12 removeFollowingValidator,
13 setBodyHostsPort, 12 setBodyHostsPort,
14 setDefaultPagination, 13 setDefaultPagination,
15 setDefaultSort 14 setDefaultSort
16} from '../../../middlewares' 15} from '../../../middlewares'
17import { followersSortValidator, followingSortValidator, followValidator } from '../../../middlewares/validators' 16import {
17 acceptOrRejectFollowerValidator,
18 followersSortValidator,
19 followingSortValidator,
20 followValidator,
21 getFollowerValidator,
22 removeFollowingValidator
23} from '../../../middlewares/validators'
18import { ActorFollowModel } from '../../../models/activitypub/actor-follow' 24import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
19import { JobQueue } from '../../../lib/job-queue' 25import { JobQueue } from '../../../lib/job-queue'
20import { removeRedundancyOf } from '../../../lib/redundancy' 26import { removeRedundancyOf } from '../../../lib/redundancy'
27import { sequelizeTypescript } from '../../../initializers/database'
21 28
22const serverFollowsRouter = express.Router() 29const serverFollowsRouter = express.Router()
23serverFollowsRouter.get('/following', 30serverFollowsRouter.get('/following',
@@ -40,7 +47,7 @@ serverFollowsRouter.delete('/following/:host',
40 authenticate, 47 authenticate,
41 ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW), 48 ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW),
42 asyncMiddleware(removeFollowingValidator), 49 asyncMiddleware(removeFollowingValidator),
43 asyncMiddleware(removeFollow) 50 asyncMiddleware(removeFollowing)
44) 51)
45 52
46serverFollowsRouter.get('/followers', 53serverFollowsRouter.get('/followers',
@@ -51,6 +58,29 @@ serverFollowsRouter.get('/followers',
51 asyncMiddleware(listFollowers) 58 asyncMiddleware(listFollowers)
52) 59)
53 60
61serverFollowsRouter.delete('/followers/:nameWithHost',
62 authenticate,
63 ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW),
64 asyncMiddleware(getFollowerValidator),
65 asyncMiddleware(removeOrRejectFollower)
66)
67
68serverFollowsRouter.post('/followers/:nameWithHost/reject',
69 authenticate,
70 ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW),
71 asyncMiddleware(getFollowerValidator),
72 acceptOrRejectFollowerValidator,
73 asyncMiddleware(removeOrRejectFollower)
74)
75
76serverFollowsRouter.post('/followers/:nameWithHost/accept',
77 authenticate,
78 ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW),
79 asyncMiddleware(getFollowerValidator),
80 acceptOrRejectFollowerValidator,
81 asyncMiddleware(acceptFollower)
82)
83
54// --------------------------------------------------------------------------- 84// ---------------------------------------------------------------------------
55 85
56export { 86export {
@@ -59,7 +89,7 @@ export {
59 89
60// --------------------------------------------------------------------------- 90// ---------------------------------------------------------------------------
61 91
62async function listFollowing (req: express.Request, res: express.Response, next: express.NextFunction) { 92async function listFollowing (req: express.Request, res: express.Response) {
63 const serverActor = await getServerActor() 93 const serverActor = await getServerActor()
64 const resultList = await ActorFollowModel.listFollowingForApi( 94 const resultList = await ActorFollowModel.listFollowingForApi(
65 serverActor.id, 95 serverActor.id,
@@ -72,7 +102,7 @@ async function listFollowing (req: express.Request, res: express.Response, next:
72 return res.json(getFormattedObjects(resultList.data, resultList.total)) 102 return res.json(getFormattedObjects(resultList.data, resultList.total))
73} 103}
74 104
75async function listFollowers (req: express.Request, res: express.Response, next: express.NextFunction) { 105async function listFollowers (req: express.Request, res: express.Response) {
76 const serverActor = await getServerActor() 106 const serverActor = await getServerActor()
77 const resultList = await ActorFollowModel.listFollowersForApi( 107 const resultList = await ActorFollowModel.listFollowersForApi(
78 serverActor.id, 108 serverActor.id,
@@ -85,7 +115,7 @@ async function listFollowers (req: express.Request, res: express.Response, next:
85 return res.json(getFormattedObjects(resultList.data, resultList.total)) 115 return res.json(getFormattedObjects(resultList.data, resultList.total))
86} 116}
87 117
88async function followInstance (req: express.Request, res: express.Response, next: express.NextFunction) { 118async function followInstance (req: express.Request, res: express.Response) {
89 const hosts = req.body.hosts as string[] 119 const hosts = req.body.hosts as string[]
90 const follower = await getServerActor() 120 const follower = await getServerActor()
91 121
@@ -103,8 +133,8 @@ async function followInstance (req: express.Request, res: express.Response, next
103 return res.status(204).end() 133 return res.status(204).end()
104} 134}
105 135
106async function removeFollow (req: express.Request, res: express.Response, next: express.NextFunction) { 136async function removeFollowing (req: express.Request, res: express.Response) {
107 const follow: ActorFollowModel = res.locals.follow 137 const follow = res.locals.follow
108 138
109 await sequelizeTypescript.transaction(async t => { 139 await sequelizeTypescript.transaction(async t => {
110 if (follow.state === 'accepted') await sendUndoFollow(follow, t) 140 if (follow.state === 'accepted') await sendUndoFollow(follow, t)
@@ -123,3 +153,24 @@ async function removeFollow (req: express.Request, res: express.Response, next:
123 153
124 return res.status(204).end() 154 return res.status(204).end()
125} 155}
156
157async function removeOrRejectFollower (req: express.Request, res: express.Response) {
158 const follow = res.locals.follow
159
160 await sendReject(follow.ActorFollower, follow.ActorFollowing)
161
162 await follow.destroy()
163
164 return res.status(204).end()
165}
166
167async function acceptFollower (req: express.Request, res: express.Response) {
168 const follow = res.locals.follow
169
170 await sendAccept(follow)
171
172 follow.state = 'accepted'
173 await follow.save()
174
175 return res.status(204).end()
176}
diff --git a/server/controllers/api/server/index.ts b/server/controllers/api/server/index.ts
index 814248e5f..6b8793a19 100644
--- a/server/controllers/api/server/index.ts
+++ b/server/controllers/api/server/index.ts
@@ -4,6 +4,8 @@ import { 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' 6import { contactRouter } from './contact'
7import { logsRouter } from './logs'
8import { debugRouter } from './debug'
7 9
8const serverRouter = express.Router() 10const serverRouter = express.Router()
9 11
@@ -12,6 +14,8 @@ serverRouter.use('/', serverRedundancyRouter)
12serverRouter.use('/', statsRouter) 14serverRouter.use('/', statsRouter)
13serverRouter.use('/', serverBlocklistRouter) 15serverRouter.use('/', serverBlocklistRouter)
14serverRouter.use('/', contactRouter) 16serverRouter.use('/', contactRouter)
17serverRouter.use('/', logsRouter)
18serverRouter.use('/', debugRouter)
15 19
16// --------------------------------------------------------------------------- 20// ---------------------------------------------------------------------------
17 21
diff --git a/server/controllers/api/server/logs.ts b/server/controllers/api/server/logs.ts
new file mode 100644
index 000000000..e9d1f2efd
--- /dev/null
+++ b/server/controllers/api/server/logs.ts
@@ -0,0 +1,95 @@
1import * as express from 'express'
2import { UserRight } from '../../../../shared/models/users'
3import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../../middlewares'
4import { mtimeSortFilesDesc } from '../../../../shared/core-utils/logs/logs'
5import { readdir, readFile } from 'fs-extra'
6import { MAX_LOGS_OUTPUT_CHARACTERS } from '../../../initializers/constants'
7import { join } from 'path'
8import { getLogsValidator } from '../../../middlewares/validators/logs'
9import { LogLevel } from '../../../../shared/models/server/log-level.type'
10import { CONFIG } from '../../../initializers/config'
11
12const logsRouter = express.Router()
13
14logsRouter.get('/logs',
15 authenticate,
16 ensureUserHasRight(UserRight.MANAGE_LOGS),
17 getLogsValidator,
18 asyncMiddleware(getLogs)
19)
20
21// ---------------------------------------------------------------------------
22
23export {
24 logsRouter
25}
26
27// ---------------------------------------------------------------------------
28
29async function getLogs (req: express.Request, res: express.Response) {
30 const logFiles = await readdir(CONFIG.STORAGE.LOG_DIR)
31 const sortedLogFiles = await mtimeSortFilesDesc(logFiles, CONFIG.STORAGE.LOG_DIR)
32 let currentSize = 0
33
34 const startDate = new Date(req.query.startDate)
35 const endDate = req.query.endDate ? new Date(req.query.endDate) : new Date()
36 const level: LogLevel = req.query.level || 'info'
37
38 let output: string[] = []
39
40 for (const meta of sortedLogFiles) {
41 const path = join(CONFIG.STORAGE.LOG_DIR, meta.file)
42
43 const result = await getOutputFromFile(path, startDate, endDate, level, currentSize)
44 if (!result.output) break
45
46 output = result.output.concat(output)
47 currentSize = result.currentSize
48
49 if (currentSize > MAX_LOGS_OUTPUT_CHARACTERS || (result.logTime && result.logTime < startDate.getTime())) break
50 }
51
52 return res.json(output).end()
53}
54
55async function getOutputFromFile (path: string, startDate: Date, endDate: Date, level: LogLevel, currentSize: number) {
56 const startTime = startDate.getTime()
57 const endTime = endDate.getTime()
58 let logTime: number
59
60 const logsLevel: { [ id in LogLevel ]: number } = {
61 debug: 0,
62 info: 1,
63 warn: 2,
64 error: 3
65 }
66
67 const content = await readFile(path)
68 const lines = content.toString().split('\n')
69 const output: any[] = []
70
71 for (let i = lines.length - 1; i >= 0; i--) {
72 const line = lines[ i ]
73 let log: any
74
75 try {
76 log = JSON.parse(line)
77 } catch {
78 // Maybe there a multiple \n at the end of the file
79 continue
80 }
81
82 logTime = new Date(log.timestamp).getTime()
83 if (logTime >= startTime && logTime <= endTime && logsLevel[ log.level ] >= logsLevel[ level ]) {
84 output.push(log)
85
86 currentSize += line.length
87
88 if (currentSize > MAX_LOGS_OUTPUT_CHARACTERS) break
89 } else if (logTime < startTime) {
90 break
91 }
92 }
93
94 return { currentSize, output: output.reverse(), logTime }
95}
diff --git a/server/controllers/api/server/redundancy.ts b/server/controllers/api/server/redundancy.ts
index 4140c4991..f8109070d 100644
--- a/server/controllers/api/server/redundancy.ts
+++ b/server/controllers/api/server/redundancy.ts
@@ -2,7 +2,6 @@ import * as express from 'express'
2import { UserRight } from '../../../../shared/models/users' 2import { UserRight } from '../../../../shared/models/users'
3import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../../middlewares' 3import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../../middlewares'
4import { updateServerRedundancyValidator } from '../../../middlewares/validators/redundancy' 4import { updateServerRedundancyValidator } from '../../../middlewares/validators/redundancy'
5import { ServerModel } from '../../../models/server/server'
6import { removeRedundancyOf } from '../../../lib/redundancy' 5import { removeRedundancyOf } from '../../../lib/redundancy'
7import { logger } from '../../../helpers/logger' 6import { logger } from '../../../helpers/logger'
8 7
@@ -23,8 +22,8 @@ export {
23 22
24// --------------------------------------------------------------------------- 23// ---------------------------------------------------------------------------
25 24
26async function updateRedundancy (req: express.Request, res: express.Response, next: express.NextFunction) { 25async function updateRedundancy (req: express.Request, res: express.Response) {
27 const server = res.locals.server as ServerModel 26 const server = res.locals.server
28 27
29 server.redundancyAllowed = req.body.redundancyAllowed 28 server.redundancyAllowed = req.body.redundancyAllowed
30 29
diff --git a/server/controllers/api/server/server-blocklist.ts b/server/controllers/api/server/server-blocklist.ts
index 3cb3a96e2..d165db191 100644
--- a/server/controllers/api/server/server-blocklist.ts
+++ b/server/controllers/api/server/server-blocklist.ts
@@ -18,11 +18,9 @@ import {
18 unblockAccountByServerValidator, 18 unblockAccountByServerValidator,
19 unblockServerByServerValidator 19 unblockServerByServerValidator
20} from '../../../middlewares/validators' 20} from '../../../middlewares/validators'
21import { AccountModel } from '../../../models/account/account'
22import { AccountBlocklistModel } from '../../../models/account/account-blocklist' 21import { AccountBlocklistModel } from '../../../models/account/account-blocklist'
23import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist' 22import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist'
24import { ServerBlocklistModel } from '../../../models/server/server-blocklist' 23import { ServerBlocklistModel } from '../../../models/server/server-blocklist'
25import { ServerModel } from '../../../models/server/server'
26import { UserRight } from '../../../../shared/models/users' 24import { UserRight } from '../../../../shared/models/users'
27 25
28const serverBlocklistRouter = express.Router() 26const serverBlocklistRouter = express.Router()
@@ -91,7 +89,7 @@ async function listBlockedAccounts (req: express.Request, res: express.Response)
91 89
92async function blockAccount (req: express.Request, res: express.Response) { 90async function blockAccount (req: express.Request, res: express.Response) {
93 const serverActor = await getServerActor() 91 const serverActor = await getServerActor()
94 const accountToBlock: AccountModel = res.locals.account 92 const accountToBlock = res.locals.account
95 93
96 await addAccountInBlocklist(serverActor.Account.id, accountToBlock.id) 94 await addAccountInBlocklist(serverActor.Account.id, accountToBlock.id)
97 95
@@ -99,7 +97,7 @@ async function blockAccount (req: express.Request, res: express.Response) {
99} 97}
100 98
101async function unblockAccount (req: express.Request, res: express.Response) { 99async function unblockAccount (req: express.Request, res: express.Response) {
102 const accountBlock: AccountBlocklistModel = res.locals.accountBlock 100 const accountBlock = res.locals.accountBlock
103 101
104 await removeAccountFromBlocklist(accountBlock) 102 await removeAccountFromBlocklist(accountBlock)
105 103
@@ -116,7 +114,7 @@ async function listBlockedServers (req: express.Request, res: express.Response)
116 114
117async function blockServer (req: express.Request, res: express.Response) { 115async function blockServer (req: express.Request, res: express.Response) {
118 const serverActor = await getServerActor() 116 const serverActor = await getServerActor()
119 const serverToBlock: ServerModel = res.locals.server 117 const serverToBlock = res.locals.server
120 118
121 await addServerInBlocklist(serverActor.Account.id, serverToBlock.id) 119 await addServerInBlocklist(serverActor.Account.id, serverToBlock.id)
122 120
@@ -124,7 +122,7 @@ async function blockServer (req: express.Request, res: express.Response) {
124} 122}
125 123
126async function unblockServer (req: express.Request, res: express.Response) { 124async function unblockServer (req: express.Request, res: express.Response) {
127 const serverBlock: ServerBlocklistModel = res.locals.serverBlock 125 const serverBlock = res.locals.serverBlock
128 126
129 await removeServerFromBlocklist(serverBlock) 127 await removeServerFromBlocklist(serverBlock)
130 128
diff --git a/server/controllers/api/server/stats.ts b/server/controllers/api/server/stats.ts
index 89ffd1717..951b98209 100644
--- a/server/controllers/api/server/stats.ts
+++ b/server/controllers/api/server/stats.ts
@@ -6,9 +6,10 @@ import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
6import { VideoModel } from '../../../models/video/video' 6import { VideoModel } from '../../../models/video/video'
7import { VideoCommentModel } from '../../../models/video/video-comment' 7import { 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 { ROUTE_CACHE_LIFETIME } from '../../../initializers/constants'
10import { cacheRoute } from '../../../middlewares/cache' 10import { cacheRoute } from '../../../middlewares/cache'
11import { VideoFileModel } from '../../../models/video/video-file' 11import { VideoFileModel } from '../../../models/video/video-file'
12import { CONFIG } from '../../../initializers/config'
12 13
13const statsRouter = express.Router() 14const statsRouter = express.Router()
14 15
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts
index dbe0718d4..0aafba66e 100644
--- a/server/controllers/api/users/index.ts
+++ b/server/controllers/api/users/index.ts
@@ -3,10 +3,10 @@ import * as RateLimit from 'express-rate-limit'
3import { UserCreate, UserRight, UserRole, UserUpdate } from '../../../../shared' 3import { UserCreate, UserRight, UserRole, UserUpdate } from '../../../../shared'
4import { logger } from '../../../helpers/logger' 4import { logger } from '../../../helpers/logger'
5import { getFormattedObjects } from '../../../helpers/utils' 5import { getFormattedObjects } from '../../../helpers/utils'
6import { CONFIG, RATES_LIMIT, sequelizeTypescript } from '../../../initializers' 6import { RATES_LIMIT, WEBSERVER } from '../../../initializers/constants'
7import { Emailer } from '../../../lib/emailer' 7import { Emailer } from '../../../lib/emailer'
8import { Redis } from '../../../lib/redis' 8import { Redis } from '../../../lib/redis'
9import { createUserAccountAndChannel } from '../../../lib/user' 9import { createUserAccountAndChannelAndPlaylist } from '../../../lib/user'
10import { 10import {
11 asyncMiddleware, 11 asyncMiddleware,
12 asyncRetryTransactionMiddleware, 12 asyncRetryTransactionMiddleware,
@@ -38,23 +38,25 @@ 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 { myVideoPlaylistsRouter } from './my-video-playlists'
41import { myVideosHistoryRouter } from './my-history' 42import { myVideosHistoryRouter } from './my-history'
42import { myNotificationsRouter } from './my-notifications' 43import { myNotificationsRouter } from './my-notifications'
43import { Notifier } from '../../../lib/notifier' 44import { Notifier } from '../../../lib/notifier'
44import { mySubscriptionsRouter } from './my-subscriptions' 45import { mySubscriptionsRouter } from './my-subscriptions'
46import { CONFIG } from '../../../initializers/config'
47import { sequelizeTypescript } from '../../../initializers/database'
48import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
45 49
46const auditLogger = auditLoggerFactory('users') 50const auditLogger = auditLoggerFactory('users')
47 51
48const loginRateLimiter = new RateLimit({ 52const loginRateLimiter = new RateLimit({
49 windowMs: RATES_LIMIT.LOGIN.WINDOW_MS, 53 windowMs: RATES_LIMIT.LOGIN.WINDOW_MS,
50 max: RATES_LIMIT.LOGIN.MAX, 54 max: RATES_LIMIT.LOGIN.MAX
51 delayMs: 0
52}) 55})
53 56
54const askSendEmailLimiter = new RateLimit({ 57const askSendEmailLimiter = new RateLimit({
55 windowMs: RATES_LIMIT.ASK_SEND_EMAIL.WINDOW_MS, 58 windowMs: RATES_LIMIT.ASK_SEND_EMAIL.WINDOW_MS,
56 max: RATES_LIMIT.ASK_SEND_EMAIL.MAX, 59 max: RATES_LIMIT.ASK_SEND_EMAIL.MAX
57 delayMs: 0
58}) 60})
59 61
60const usersRouter = express.Router() 62const usersRouter = express.Router()
@@ -62,6 +64,7 @@ usersRouter.use('/', myNotificationsRouter)
62usersRouter.use('/', mySubscriptionsRouter) 64usersRouter.use('/', mySubscriptionsRouter)
63usersRouter.use('/', myBlocklistRouter) 65usersRouter.use('/', myBlocklistRouter)
64usersRouter.use('/', myVideosHistoryRouter) 66usersRouter.use('/', myVideosHistoryRouter)
67usersRouter.use('/', myVideoPlaylistsRouter)
65usersRouter.use('/', meRouter) 68usersRouter.use('/', meRouter)
66 69
67usersRouter.get('/autocomplete', 70usersRouter.get('/autocomplete',
@@ -173,10 +176,11 @@ async function createUser (req: express.Request, res: express.Response) {
173 autoPlayVideo: true, 176 autoPlayVideo: true,
174 role: body.role, 177 role: body.role,
175 videoQuota: body.videoQuota, 178 videoQuota: body.videoQuota,
176 videoQuotaDaily: body.videoQuotaDaily 179 videoQuotaDaily: body.videoQuotaDaily,
180 adminFlags: body.adminFlags || UserAdminFlag.NONE
177 }) 181 })
178 182
179 const { user, account } = await createUserAccountAndChannel(userToCreate) 183 const { user, account } = await createUserAccountAndChannelAndPlaylist(userToCreate)
180 184
181 auditLogger.create(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON())) 185 auditLogger.create(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()))
182 logger.info('User %s with its channel and account created.', body.username) 186 logger.info('User %s with its channel and account created.', body.username)
@@ -207,7 +211,7 @@ async function registerUser (req: express.Request, res: express.Response) {
207 emailVerified: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION ? false : null 211 emailVerified: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION ? false : null
208 }) 212 })
209 213
210 const { user } = await createUserAccountAndChannel(userToCreate) 214 const { user } = await createUserAccountAndChannelAndPlaylist(userToCreate)
211 215
212 auditLogger.create(body.username, new UserAuditView(user.toFormattedJSON())) 216 auditLogger.create(body.username, new UserAuditView(user.toFormattedJSON()))
213 logger.info('User %s with its channel and account registered.', body.username) 217 logger.info('User %s with its channel and account registered.', body.username)
@@ -221,16 +225,16 @@ async function registerUser (req: express.Request, res: express.Response) {
221 return res.type('json').status(204).end() 225 return res.type('json').status(204).end()
222} 226}
223 227
224async function unblockUser (req: express.Request, res: express.Response, next: express.NextFunction) { 228async function unblockUser (req: express.Request, res: express.Response) {
225 const user: UserModel = res.locals.user 229 const user = res.locals.user
226 230
227 await changeUserBlock(res, user, false) 231 await changeUserBlock(res, user, false)
228 232
229 return res.status(204).end() 233 return res.status(204).end()
230} 234}
231 235
232async function blockUser (req: express.Request, res: express.Response, next: express.NextFunction) { 236async function blockUser (req: express.Request, res: express.Response) {
233 const user: UserModel = res.locals.user 237 const user = res.locals.user
234 const reason = req.body.reason 238 const reason = req.body.reason
235 239
236 await changeUserBlock(res, user, true, reason) 240 await changeUserBlock(res, user, true, reason)
@@ -238,24 +242,24 @@ async function blockUser (req: express.Request, res: express.Response, next: exp
238 return res.status(204).end() 242 return res.status(204).end()
239} 243}
240 244
241function getUser (req: express.Request, res: express.Response, next: express.NextFunction) { 245function getUser (req: express.Request, res: express.Response) {
242 return res.json((res.locals.user as UserModel).toFormattedJSON()) 246 return res.json(res.locals.user.toFormattedJSON({ withAdminFlags: true }))
243} 247}
244 248
245async function autocompleteUsers (req: express.Request, res: express.Response, next: express.NextFunction) { 249async function autocompleteUsers (req: express.Request, res: express.Response) {
246 const resultList = await UserModel.autoComplete(req.query.search as string) 250 const resultList = await UserModel.autoComplete(req.query.search as string)
247 251
248 return res.json(resultList) 252 return res.json(resultList)
249} 253}
250 254
251async function listUsers (req: express.Request, res: express.Response, next: express.NextFunction) { 255async function listUsers (req: express.Request, res: express.Response) {
252 const resultList = await UserModel.listForApi(req.query.start, req.query.count, req.query.sort, req.query.search) 256 const resultList = await UserModel.listForApi(req.query.start, req.query.count, req.query.sort, req.query.search)
253 257
254 return res.json(getFormattedObjects(resultList.data, resultList.total)) 258 return res.json(getFormattedObjects(resultList.data, resultList.total, { withAdminFlags: true }))
255} 259}
256 260
257async function removeUser (req: express.Request, res: express.Response, next: express.NextFunction) { 261async function removeUser (req: express.Request, res: express.Response) {
258 const user: UserModel = res.locals.user 262 const user = res.locals.user
259 263
260 await user.destroy() 264 await user.destroy()
261 265
@@ -264,42 +268,44 @@ async function removeUser (req: express.Request, res: express.Response, next: ex
264 return res.sendStatus(204) 268 return res.sendStatus(204)
265} 269}
266 270
267async function updateUser (req: express.Request, res: express.Response, next: express.NextFunction) { 271async function updateUser (req: express.Request, res: express.Response) {
268 const body: UserUpdate = req.body 272 const body: UserUpdate = req.body
269 const userToUpdate = res.locals.user as UserModel 273 const userToUpdate = res.locals.user
270 const oldUserAuditView = new UserAuditView(userToUpdate.toFormattedJSON()) 274 const oldUserAuditView = new UserAuditView(userToUpdate.toFormattedJSON())
271 const roleChanged = body.role !== undefined && body.role !== userToUpdate.role 275 const roleChanged = body.role !== undefined && body.role !== userToUpdate.role
272 276
277 if (body.password !== undefined) userToUpdate.password = body.password
273 if (body.email !== undefined) userToUpdate.email = body.email 278 if (body.email !== undefined) userToUpdate.email = body.email
274 if (body.emailVerified !== undefined) userToUpdate.emailVerified = body.emailVerified 279 if (body.emailVerified !== undefined) userToUpdate.emailVerified = body.emailVerified
275 if (body.videoQuota !== undefined) userToUpdate.videoQuota = body.videoQuota 280 if (body.videoQuota !== undefined) userToUpdate.videoQuota = body.videoQuota
276 if (body.videoQuotaDaily !== undefined) userToUpdate.videoQuotaDaily = body.videoQuotaDaily 281 if (body.videoQuotaDaily !== undefined) userToUpdate.videoQuotaDaily = body.videoQuotaDaily
277 if (body.role !== undefined) userToUpdate.role = body.role 282 if (body.role !== undefined) userToUpdate.role = body.role
283 if (body.adminFlags !== undefined) userToUpdate.adminFlags = body.adminFlags
278 284
279 const user = await userToUpdate.save() 285 const user = await userToUpdate.save()
280 286
281 // Destroy user token to refresh rights 287 // Destroy user token to refresh rights
282 if (roleChanged) await deleteUserToken(userToUpdate.id) 288 if (roleChanged || body.password !== undefined) await deleteUserToken(userToUpdate.id)
283 289
284 auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()), oldUserAuditView) 290 auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()), oldUserAuditView)
285 291
286 // Don't need to send this update to followers, these attributes are not propagated 292 // Don't need to send this update to followers, these attributes are not federated
287 293
288 return res.sendStatus(204) 294 return res.sendStatus(204)
289} 295}
290 296
291async function askResetUserPassword (req: express.Request, res: express.Response, next: express.NextFunction) { 297async function askResetUserPassword (req: express.Request, res: express.Response) {
292 const user = res.locals.user as UserModel 298 const user = res.locals.user
293 299
294 const verificationString = await Redis.Instance.setResetPasswordVerificationString(user.id) 300 const verificationString = await Redis.Instance.setResetPasswordVerificationString(user.id)
295 const url = CONFIG.WEBSERVER.URL + '/reset-password?userId=' + user.id + '&verificationString=' + verificationString 301 const url = WEBSERVER.URL + '/reset-password?userId=' + user.id + '&verificationString=' + verificationString
296 await Emailer.Instance.addForgetPasswordEmailJob(user.email, url) 302 await Emailer.Instance.addPasswordResetEmailJob(user.email, url)
297 303
298 return res.status(204).end() 304 return res.status(204).end()
299} 305}
300 306
301async function resetUserPassword (req: express.Request, res: express.Response, next: express.NextFunction) { 307async function resetUserPassword (req: express.Request, res: express.Response) {
302 const user = res.locals.user as UserModel 308 const user = res.locals.user
303 user.password = req.body.password 309 user.password = req.body.password
304 310
305 await user.save() 311 await user.save()
@@ -309,21 +315,21 @@ async function resetUserPassword (req: express.Request, res: express.Response, n
309 315
310async function sendVerifyUserEmail (user: UserModel) { 316async function sendVerifyUserEmail (user: UserModel) {
311 const verificationString = await Redis.Instance.setVerifyEmailVerificationString(user.id) 317 const verificationString = await Redis.Instance.setVerifyEmailVerificationString(user.id)
312 const url = CONFIG.WEBSERVER.URL + '/verify-account/email?userId=' + user.id + '&verificationString=' + verificationString 318 const url = WEBSERVER.URL + '/verify-account/email?userId=' + user.id + '&verificationString=' + verificationString
313 await Emailer.Instance.addVerifyEmailJob(user.email, url) 319 await Emailer.Instance.addVerifyEmailJob(user.email, url)
314 return 320 return
315} 321}
316 322
317async function askSendVerifyUserEmail (req: express.Request, res: express.Response, next: express.NextFunction) { 323async function askSendVerifyUserEmail (req: express.Request, res: express.Response) {
318 const user = res.locals.user as UserModel 324 const user = res.locals.user
319 325
320 await sendVerifyUserEmail(user) 326 await sendVerifyUserEmail(user)
321 327
322 return res.status(204).end() 328 return res.status(204).end()
323} 329}
324 330
325async function verifyUserEmail (req: express.Request, res: express.Response, next: express.NextFunction) { 331async function verifyUserEmail (req: express.Request, res: express.Response) {
326 const user = res.locals.user as UserModel 332 const user = res.locals.user
327 user.emailVerified = true 333 user.emailVerified = true
328 334
329 await user.save() 335 await user.save()
@@ -331,7 +337,7 @@ async function verifyUserEmail (req: express.Request, res: express.Response, nex
331 return res.status(204).end() 337 return res.status(204).end()
332} 338}
333 339
334function success (req: express.Request, res: express.Response, next: express.NextFunction) { 340function success (req: express.Request, res: express.Response) {
335 res.end() 341 res.end()
336} 342}
337 343
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts
index 94a2b8732..ddb239e7b 100644
--- a/server/controllers/api/users/me.ts
+++ b/server/controllers/api/users/me.ts
@@ -2,7 +2,7 @@ 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, MIMETYPES, sequelizeTypescript } from '../../../initializers' 5import { MIMETYPES } from '../../../initializers/constants'
6import { sendUpdateActor } from '../../../lib/activitypub/send' 6import { sendUpdateActor } from '../../../lib/activitypub/send'
7import { 7import {
8 asyncMiddleware, 8 asyncMiddleware,
@@ -26,6 +26,8 @@ import { updateActorAvatarFile } from '../../../lib/avatar'
26import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger' 26import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger'
27import { VideoImportModel } from '../../../models/video/video-import' 27import { VideoImportModel } from '../../../models/video/video-import'
28import { AccountModel } from '../../../models/account/account' 28import { AccountModel } from '../../../models/account/account'
29import { CONFIG } from '../../../initializers/config'
30import { sequelizeTypescript } from '../../../initializers/database'
29 31
30const auditLogger = auditLoggerFactory('users-me') 32const auditLogger = auditLoggerFactory('users-me')
31 33
@@ -93,8 +95,8 @@ export {
93 95
94// --------------------------------------------------------------------------- 96// ---------------------------------------------------------------------------
95 97
96async function getUserVideos (req: express.Request, res: express.Response, next: express.NextFunction) { 98async function getUserVideos (req: express.Request, res: express.Response) {
97 const user = res.locals.oauth.token.User as UserModel 99 const user = res.locals.oauth.token.User
98 const resultList = await VideoModel.listUserVideosForApi( 100 const resultList = await VideoModel.listUserVideosForApi(
99 user.Account.id, 101 user.Account.id,
100 req.query.start as number, 102 req.query.start as number,
@@ -111,8 +113,8 @@ async function getUserVideos (req: express.Request, res: express.Response, next:
111 return res.json(getFormattedObjects(resultList.data, resultList.total, { additionalAttributes })) 113 return res.json(getFormattedObjects(resultList.data, resultList.total, { additionalAttributes }))
112} 114}
113 115
114async function getUserVideoImports (req: express.Request, res: express.Response, next: express.NextFunction) { 116async function getUserVideoImports (req: express.Request, res: express.Response) {
115 const user = res.locals.oauth.token.User as UserModel 117 const user = res.locals.oauth.token.User
116 const resultList = await VideoImportModel.listUserVideoImportsForApi( 118 const resultList = await VideoImportModel.listUserVideoImportsForApi(
117 user.id, 119 user.id,
118 req.query.start as number, 120 req.query.start as number,
@@ -123,14 +125,14 @@ async function getUserVideoImports (req: express.Request, res: express.Response,
123 return res.json(getFormattedObjects(resultList.data, resultList.total)) 125 return res.json(getFormattedObjects(resultList.data, resultList.total))
124} 126}
125 127
126async function getUserInformation (req: express.Request, res: express.Response, next: express.NextFunction) { 128async function getUserInformation (req: express.Request, res: express.Response) {
127 // We did not load channels in res.locals.user 129 // We did not load channels in res.locals.user
128 const user = await UserModel.loadByUsernameAndPopulateChannels(res.locals.oauth.token.user.username) 130 const user = await UserModel.loadByUsernameAndPopulateChannels(res.locals.oauth.token.user.username)
129 131
130 return res.json(user.toFormattedJSON()) 132 return res.json(user.toFormattedJSON({}))
131} 133}
132 134
133async function getUserVideoQuotaUsed (req: express.Request, res: express.Response, next: express.NextFunction) { 135async function getUserVideoQuotaUsed (req: express.Request, res: express.Response) {
134 // We did not load channels in res.locals.user 136 // We did not load channels in res.locals.user
135 const user = await UserModel.loadByUsernameAndPopulateChannels(res.locals.oauth.token.user.username) 137 const user = await UserModel.loadByUsernameAndPopulateChannels(res.locals.oauth.token.user.username)
136 const videoQuotaUsed = await UserModel.getOriginalVideoFileTotalFromUser(user) 138 const videoQuotaUsed = await UserModel.getOriginalVideoFileTotalFromUser(user)
@@ -143,7 +145,7 @@ async function getUserVideoQuotaUsed (req: express.Request, res: express.Respons
143 return res.json(data) 145 return res.json(data)
144} 146}
145 147
146async function getUserVideoRating (req: express.Request, res: express.Response, next: express.NextFunction) { 148async function getUserVideoRating (req: express.Request, res: express.Response) {
147 const videoId = res.locals.video.id 149 const videoId = res.locals.video.id
148 const accountId = +res.locals.oauth.token.User.Account.id 150 const accountId = +res.locals.oauth.token.User.Account.id
149 151
@@ -158,20 +160,20 @@ async function getUserVideoRating (req: express.Request, res: express.Response,
158} 160}
159 161
160async function deleteMe (req: express.Request, res: express.Response) { 162async function deleteMe (req: express.Request, res: express.Response) {
161 const user: UserModel = res.locals.oauth.token.User 163 const user = res.locals.oauth.token.User
162 164
163 await user.destroy() 165 await user.destroy()
164 166
165 auditLogger.delete(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON())) 167 auditLogger.delete(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON({})))
166 168
167 return res.sendStatus(204) 169 return res.sendStatus(204)
168} 170}
169 171
170async function updateMe (req: express.Request, res: express.Response, next: express.NextFunction) { 172async function updateMe (req: express.Request, res: express.Response) {
171 const body: UserUpdateMe = req.body 173 const body: UserUpdateMe = req.body
172 174
173 const user: UserModel = res.locals.oauth.token.user 175 const user = res.locals.oauth.token.user
174 const oldUserAuditView = new UserAuditView(user.toFormattedJSON()) 176 const oldUserAuditView = new UserAuditView(user.toFormattedJSON({}))
175 177
176 if (body.password !== undefined) user.password = body.password 178 if (body.password !== undefined) user.password = body.password
177 if (body.email !== undefined) user.email = body.email 179 if (body.email !== undefined) user.email = body.email
@@ -191,7 +193,7 @@ async function updateMe (req: express.Request, res: express.Response, next: expr
191 193
192 await sendUpdateActor(userAccount, t) 194 await sendUpdateActor(userAccount, t)
193 195
194 auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()), oldUserAuditView) 196 auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON({})), oldUserAuditView)
195 }) 197 })
196 198
197 return res.sendStatus(204) 199 return res.sendStatus(204)
@@ -199,14 +201,14 @@ async function updateMe (req: express.Request, res: express.Response, next: expr
199 201
200async function updateMyAvatar (req: express.Request, res: express.Response) { 202async function updateMyAvatar (req: express.Request, res: express.Response) {
201 const avatarPhysicalFile = req.files[ 'avatarfile' ][ 0 ] 203 const avatarPhysicalFile = req.files[ 'avatarfile' ][ 0 ]
202 const user: UserModel = res.locals.oauth.token.user 204 const user = res.locals.oauth.token.user
203 const oldUserAuditView = new UserAuditView(user.toFormattedJSON()) 205 const oldUserAuditView = new UserAuditView(user.toFormattedJSON({}))
204 206
205 const userAccount = await AccountModel.load(user.Account.id) 207 const userAccount = await AccountModel.load(user.Account.id)
206 208
207 const avatar = await updateActorAvatarFile(avatarPhysicalFile, userAccount) 209 const avatar = await updateActorAvatarFile(avatarPhysicalFile, userAccount)
208 210
209 auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()), oldUserAuditView) 211 auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON({})), oldUserAuditView)
210 212
211 return res.json({ avatar: avatar.toFormattedJSON() }) 213 return res.json({ avatar: avatar.toFormattedJSON() })
212} 214}
diff --git a/server/controllers/api/users/my-blocklist.ts b/server/controllers/api/users/my-blocklist.ts
index 9575eab46..713c16022 100644
--- a/server/controllers/api/users/my-blocklist.ts
+++ b/server/controllers/api/users/my-blocklist.ts
@@ -17,12 +17,9 @@ import {
17 serversBlocklistSortValidator, 17 serversBlocklistSortValidator,
18 unblockServerByAccountValidator 18 unblockServerByAccountValidator
19} from '../../../middlewares/validators' 19} from '../../../middlewares/validators'
20import { UserModel } from '../../../models/account/user'
21import { AccountModel } from '../../../models/account/account'
22import { AccountBlocklistModel } from '../../../models/account/account-blocklist' 20import { AccountBlocklistModel } from '../../../models/account/account-blocklist'
23import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist' 21import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist'
24import { ServerBlocklistModel } from '../../../models/server/server-blocklist' 22import { ServerBlocklistModel } from '../../../models/server/server-blocklist'
25import { ServerModel } from '../../../models/server/server'
26 23
27const myBlocklistRouter = express.Router() 24const myBlocklistRouter = express.Router()
28 25
@@ -75,7 +72,7 @@ export {
75// --------------------------------------------------------------------------- 72// ---------------------------------------------------------------------------
76 73
77async function listBlockedAccounts (req: express.Request, res: express.Response) { 74async function listBlockedAccounts (req: express.Request, res: express.Response) {
78 const user: UserModel = res.locals.oauth.token.User 75 const user = res.locals.oauth.token.User
79 76
80 const resultList = await AccountBlocklistModel.listForApi(user.Account.id, req.query.start, req.query.count, req.query.sort) 77 const resultList = await AccountBlocklistModel.listForApi(user.Account.id, req.query.start, req.query.count, req.query.sort)
81 78
@@ -83,8 +80,8 @@ async function listBlockedAccounts (req: express.Request, res: express.Response)
83} 80}
84 81
85async function blockAccount (req: express.Request, res: express.Response) { 82async function blockAccount (req: express.Request, res: express.Response) {
86 const user: UserModel = res.locals.oauth.token.User 83 const user = res.locals.oauth.token.User
87 const accountToBlock: AccountModel = res.locals.account 84 const accountToBlock = res.locals.account
88 85
89 await addAccountInBlocklist(user.Account.id, accountToBlock.id) 86 await addAccountInBlocklist(user.Account.id, accountToBlock.id)
90 87
@@ -92,7 +89,7 @@ async function blockAccount (req: express.Request, res: express.Response) {
92} 89}
93 90
94async function unblockAccount (req: express.Request, res: express.Response) { 91async function unblockAccount (req: express.Request, res: express.Response) {
95 const accountBlock: AccountBlocklistModel = res.locals.accountBlock 92 const accountBlock = res.locals.accountBlock
96 93
97 await removeAccountFromBlocklist(accountBlock) 94 await removeAccountFromBlocklist(accountBlock)
98 95
@@ -100,7 +97,7 @@ async function unblockAccount (req: express.Request, res: express.Response) {
100} 97}
101 98
102async function listBlockedServers (req: express.Request, res: express.Response) { 99async function listBlockedServers (req: express.Request, res: express.Response) {
103 const user: UserModel = res.locals.oauth.token.User 100 const user = res.locals.oauth.token.User
104 101
105 const resultList = await ServerBlocklistModel.listForApi(user.Account.id, req.query.start, req.query.count, req.query.sort) 102 const resultList = await ServerBlocklistModel.listForApi(user.Account.id, req.query.start, req.query.count, req.query.sort)
106 103
@@ -108,8 +105,8 @@ async function listBlockedServers (req: express.Request, res: express.Response)
108} 105}
109 106
110async function blockServer (req: express.Request, res: express.Response) { 107async function blockServer (req: express.Request, res: express.Response) {
111 const user: UserModel = res.locals.oauth.token.User 108 const user = res.locals.oauth.token.User
112 const serverToBlock: ServerModel = res.locals.server 109 const serverToBlock = res.locals.server
113 110
114 await addServerInBlocklist(user.Account.id, serverToBlock.id) 111 await addServerInBlocklist(user.Account.id, serverToBlock.id)
115 112
@@ -117,7 +114,7 @@ async function blockServer (req: express.Request, res: express.Response) {
117} 114}
118 115
119async function unblockServer (req: express.Request, res: express.Response) { 116async function unblockServer (req: express.Request, res: express.Response) {
120 const serverBlock: ServerBlocklistModel = res.locals.serverBlock 117 const serverBlock = res.locals.serverBlock
121 118
122 await removeServerFromBlocklist(serverBlock) 119 await removeServerFromBlocklist(serverBlock)
123 120
diff --git a/server/controllers/api/users/my-history.ts b/server/controllers/api/users/my-history.ts
index 6cd782c47..7025c0ff1 100644
--- a/server/controllers/api/users/my-history.ts
+++ b/server/controllers/api/users/my-history.ts
@@ -36,7 +36,7 @@ export {
36// --------------------------------------------------------------------------- 36// ---------------------------------------------------------------------------
37 37
38async function listMyVideosHistory (req: express.Request, res: express.Response) { 38async function listMyVideosHistory (req: express.Request, res: express.Response) {
39 const user: UserModel = res.locals.oauth.token.User 39 const user = res.locals.oauth.token.User
40 40
41 const resultList = await UserVideoHistoryModel.listForApi(user, req.query.start, req.query.count) 41 const resultList = await UserVideoHistoryModel.listForApi(user, req.query.start, req.query.count)
42 42
@@ -44,11 +44,11 @@ async function listMyVideosHistory (req: express.Request, res: express.Response)
44} 44}
45 45
46async function removeUserHistory (req: express.Request, res: express.Response) { 46async function removeUserHistory (req: express.Request, res: express.Response) {
47 const user: UserModel = res.locals.oauth.token.User 47 const user = res.locals.oauth.token.User
48 const beforeDate = req.body.beforeDate || null 48 const beforeDate = req.body.beforeDate || null
49 49
50 await sequelizeTypescript.transaction(t => { 50 await sequelizeTypescript.transaction(t => {
51 return UserVideoHistoryModel.removeHistoryBefore(user, beforeDate, t) 51 return UserVideoHistoryModel.removeUserHistoryBefore(user, beforeDate, t)
52 }) 52 })
53 53
54 // Do not send the delete to other instances, we delete OUR copy of this video abuse 54 // Do not send the delete to other instances, we delete OUR copy of this video abuse
diff --git a/server/controllers/api/users/my-notifications.ts b/server/controllers/api/users/my-notifications.ts
index 76cf97587..f146284e4 100644
--- a/server/controllers/api/users/my-notifications.ts
+++ b/server/controllers/api/users/my-notifications.ts
@@ -9,7 +9,6 @@ import {
9 setDefaultSort, 9 setDefaultSort,
10 userNotificationsSortValidator 10 userNotificationsSortValidator
11} from '../../../middlewares' 11} from '../../../middlewares'
12import { UserModel } from '../../../models/account/user'
13import { getFormattedObjects } from '../../../helpers/utils' 12import { getFormattedObjects } from '../../../helpers/utils'
14import { UserNotificationModel } from '../../../models/account/user-notification' 13import { UserNotificationModel } from '../../../models/account/user-notification'
15import { meRouter } from './me' 14import { meRouter } from './me'
@@ -57,8 +56,8 @@ export {
57// --------------------------------------------------------------------------- 56// ---------------------------------------------------------------------------
58 57
59async function updateNotificationSettings (req: express.Request, res: express.Response) { 58async function updateNotificationSettings (req: express.Request, res: express.Response) {
60 const user: UserModel = res.locals.oauth.token.User 59 const user = res.locals.oauth.token.User
61 const body = req.body 60 const body = req.body as UserNotificationSetting
62 61
63 const query = { 62 const query = {
64 where: { 63 where: {
@@ -70,12 +69,14 @@ async function updateNotificationSettings (req: express.Request, res: express.Re
70 newVideoFromSubscription: body.newVideoFromSubscription, 69 newVideoFromSubscription: body.newVideoFromSubscription,
71 newCommentOnMyVideo: body.newCommentOnMyVideo, 70 newCommentOnMyVideo: body.newCommentOnMyVideo,
72 videoAbuseAsModerator: body.videoAbuseAsModerator, 71 videoAbuseAsModerator: body.videoAbuseAsModerator,
72 videoAutoBlacklistAsModerator: body.videoAutoBlacklistAsModerator,
73 blacklistOnMyVideo: body.blacklistOnMyVideo, 73 blacklistOnMyVideo: body.blacklistOnMyVideo,
74 myVideoPublished: body.myVideoPublished, 74 myVideoPublished: body.myVideoPublished,
75 myVideoImportFinished: body.myVideoImportFinished, 75 myVideoImportFinished: body.myVideoImportFinished,
76 newFollow: body.newFollow, 76 newFollow: body.newFollow,
77 newUserRegistration: body.newUserRegistration, 77 newUserRegistration: body.newUserRegistration,
78 commentMention: body.commentMention 78 commentMention: body.commentMention,
79 newInstanceFollower: body.newInstanceFollower
79 } 80 }
80 81
81 await UserNotificationSettingModel.update(values, query) 82 await UserNotificationSettingModel.update(values, query)
@@ -84,7 +85,7 @@ async function updateNotificationSettings (req: express.Request, res: express.Re
84} 85}
85 86
86async function listUserNotifications (req: express.Request, res: express.Response) { 87async function listUserNotifications (req: express.Request, res: express.Response) {
87 const user: UserModel = res.locals.oauth.token.User 88 const user = res.locals.oauth.token.User
88 89
89 const resultList = await UserNotificationModel.listForApi(user.id, req.query.start, req.query.count, req.query.sort, req.query.unread) 90 const resultList = await UserNotificationModel.listForApi(user.id, req.query.start, req.query.count, req.query.sort, req.query.unread)
90 91
@@ -92,7 +93,7 @@ async function listUserNotifications (req: express.Request, res: express.Respons
92} 93}
93 94
94async function markAsReadUserNotifications (req: express.Request, res: express.Response) { 95async function markAsReadUserNotifications (req: express.Request, res: express.Response) {
95 const user: UserModel = res.locals.oauth.token.User 96 const user = res.locals.oauth.token.User
96 97
97 await UserNotificationModel.markAsRead(user.id, req.body.ids) 98 await UserNotificationModel.markAsRead(user.id, req.body.ids)
98 99
@@ -100,7 +101,7 @@ async function markAsReadUserNotifications (req: express.Request, res: express.R
100} 101}
101 102
102async function markAsReadAllUserNotifications (req: express.Request, res: express.Response) { 103async function markAsReadAllUserNotifications (req: express.Request, res: express.Response) {
103 const user: UserModel = res.locals.oauth.token.User 104 const user = res.locals.oauth.token.User
104 105
105 await UserNotificationModel.markAllAsRead(user.id) 106 await UserNotificationModel.markAllAsRead(user.id)
106 107
diff --git a/server/controllers/api/users/my-subscriptions.ts b/server/controllers/api/users/my-subscriptions.ts
index accca6d52..c52df3154 100644
--- a/server/controllers/api/users/my-subscriptions.ts
+++ b/server/controllers/api/users/my-subscriptions.ts
@@ -1,7 +1,7 @@
1import * as express from 'express' 1import * as express from 'express'
2import 'multer' 2import 'multer'
3import { getFormattedObjects } from '../../../helpers/utils' 3import { getFormattedObjects } from '../../../helpers/utils'
4import { CONFIG, sequelizeTypescript } from '../../../initializers' 4import { WEBSERVER } from '../../../initializers/constants'
5import { 5import {
6 asyncMiddleware, 6 asyncMiddleware,
7 asyncRetryTransactionMiddleware, 7 asyncRetryTransactionMiddleware,
@@ -14,13 +14,13 @@ import {
14 userSubscriptionGetValidator 14 userSubscriptionGetValidator
15} from '../../../middlewares' 15} from '../../../middlewares'
16import { areSubscriptionsExistValidator, userSubscriptionsSortValidator, videosSortValidator } from '../../../middlewares/validators' 16import { areSubscriptionsExistValidator, userSubscriptionsSortValidator, videosSortValidator } from '../../../middlewares/validators'
17import { UserModel } from '../../../models/account/user'
18import { VideoModel } from '../../../models/video/video' 17import { VideoModel } from '../../../models/video/video'
19import { buildNSFWFilter } from '../../../helpers/express-utils' 18import { buildNSFWFilter } from '../../../helpers/express-utils'
20import { VideoFilter } from '../../../../shared/models/videos/video-query.type' 19import { VideoFilter } from '../../../../shared/models/videos/video-query.type'
21import { ActorFollowModel } from '../../../models/activitypub/actor-follow' 20import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
22import { JobQueue } from '../../../lib/job-queue' 21import { JobQueue } from '../../../lib/job-queue'
23import { logger } from '../../../helpers/logger' 22import { logger } from '../../../helpers/logger'
23import { sequelizeTypescript } from '../../../initializers/database'
24 24
25const mySubscriptionsRouter = express.Router() 25const mySubscriptionsRouter = express.Router()
26 26
@@ -77,11 +77,11 @@ export {
77 77
78async function areSubscriptionsExist (req: express.Request, res: express.Response) { 78async function areSubscriptionsExist (req: express.Request, res: express.Response) {
79 const uris = req.query.uris as string[] 79 const uris = req.query.uris as string[]
80 const user = res.locals.oauth.token.User as UserModel 80 const user = res.locals.oauth.token.User
81 81
82 const handles = uris.map(u => { 82 const handles = uris.map(u => {
83 let [ name, host ] = u.split('@') 83 let [ name, host ] = u.split('@')
84 if (host === CONFIG.WEBSERVER.HOST) host = null 84 if (host === WEBSERVER.HOST) host = null
85 85
86 return { name, host, uri: u } 86 return { name, host, uri: u }
87 }) 87 })
@@ -107,7 +107,7 @@ async function areSubscriptionsExist (req: express.Request, res: express.Respons
107} 107}
108 108
109async function addUserSubscription (req: express.Request, res: express.Response) { 109async function addUserSubscription (req: express.Request, res: express.Response) {
110 const user = res.locals.oauth.token.User as UserModel 110 const user = res.locals.oauth.token.User
111 const [ name, host ] = req.body.uri.split('@') 111 const [ name, host ] = req.body.uri.split('@')
112 112
113 const payload = { 113 const payload = {
@@ -123,13 +123,13 @@ async function addUserSubscription (req: express.Request, res: express.Response)
123} 123}
124 124
125function getUserSubscription (req: express.Request, res: express.Response) { 125function getUserSubscription (req: express.Request, res: express.Response) {
126 const subscription: ActorFollowModel = res.locals.subscription 126 const subscription = res.locals.subscription
127 127
128 return res.json(subscription.ActorFollowing.VideoChannel.toFormattedJSON()) 128 return res.json(subscription.ActorFollowing.VideoChannel.toFormattedJSON())
129} 129}
130 130
131async function deleteUserSubscription (req: express.Request, res: express.Response) { 131async function deleteUserSubscription (req: express.Request, res: express.Response) {
132 const subscription: ActorFollowModel = res.locals.subscription 132 const subscription = res.locals.subscription
133 133
134 await sequelizeTypescript.transaction(async t => { 134 await sequelizeTypescript.transaction(async t => {
135 return subscription.destroy({ transaction: t }) 135 return subscription.destroy({ transaction: t })
@@ -139,7 +139,7 @@ async function deleteUserSubscription (req: express.Request, res: express.Respon
139} 139}
140 140
141async function getUserSubscriptions (req: express.Request, res: express.Response) { 141async function getUserSubscriptions (req: express.Request, res: express.Response) {
142 const user = res.locals.oauth.token.User as UserModel 142 const user = res.locals.oauth.token.User
143 const actorId = user.Account.Actor.id 143 const actorId = user.Account.Actor.id
144 144
145 const resultList = await ActorFollowModel.listSubscriptionsForApi(actorId, req.query.start, req.query.count, req.query.sort) 145 const resultList = await ActorFollowModel.listSubscriptionsForApi(actorId, req.query.start, req.query.count, req.query.sort)
@@ -147,8 +147,8 @@ async function getUserSubscriptions (req: express.Request, res: express.Response
147 return res.json(getFormattedObjects(resultList.data, resultList.total)) 147 return res.json(getFormattedObjects(resultList.data, resultList.total))
148} 148}
149 149
150async function getUserSubscriptionVideos (req: express.Request, res: express.Response, next: express.NextFunction) { 150async function getUserSubscriptionVideos (req: express.Request, res: express.Response) {
151 const user = res.locals.oauth.token.User as UserModel 151 const user = res.locals.oauth.token.User
152 const resultList = await VideoModel.listForApi({ 152 const resultList = await VideoModel.listForApi({
153 start: req.query.start, 153 start: req.query.start,
154 count: req.query.count, 154 count: req.query.count,
diff --git a/server/controllers/api/users/my-video-playlists.ts b/server/controllers/api/users/my-video-playlists.ts
new file mode 100644
index 000000000..15e92f4f3
--- /dev/null
+++ b/server/controllers/api/users/my-video-playlists.ts
@@ -0,0 +1,46 @@
1import * as express from 'express'
2import { asyncMiddleware, authenticate } from '../../../middlewares'
3import { doVideosInPlaylistExistValidator } from '../../../middlewares/validators/videos/video-playlists'
4import { VideoPlaylistModel } from '../../../models/video/video-playlist'
5import { VideoExistInPlaylist } from '../../../../shared/models/videos/playlist/video-exist-in-playlist.model'
6
7const myVideoPlaylistsRouter = express.Router()
8
9myVideoPlaylistsRouter.get('/me/video-playlists/videos-exist',
10 authenticate,
11 doVideosInPlaylistExistValidator,
12 asyncMiddleware(doVideosInPlaylistExist)
13)
14
15// ---------------------------------------------------------------------------
16
17export {
18 myVideoPlaylistsRouter
19}
20
21// ---------------------------------------------------------------------------
22
23async function doVideosInPlaylistExist (req: express.Request, res: express.Response) {
24 const videoIds = req.query.videoIds.map(i => parseInt(i + '', 10))
25 const user = res.locals.oauth.token.User
26
27 const results = await VideoPlaylistModel.listPlaylistIdsOf(user.Account.id, videoIds)
28
29 const existObject: VideoExistInPlaylist = {}
30
31 for (const videoId of videoIds) {
32 existObject[videoId] = []
33 }
34
35 for (const result of results) {
36 for (const element of result.VideoPlaylistElements) {
37 existObject[element.videoId].push({
38 playlistId: result.id,
39 startTimestamp: element.startTimestamp,
40 stopTimestamp: element.stopTimestamp
41 })
42 }
43 }
44
45 return res.json(existObject)
46}
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts
index db7602139..3d6dbfe70 100644
--- a/server/controllers/api/video-channel.ts
+++ b/server/controllers/api/video-channel.ts
@@ -12,7 +12,8 @@ import {
12 videoChannelsAddValidator, 12 videoChannelsAddValidator,
13 videoChannelsRemoveValidator, 13 videoChannelsRemoveValidator,
14 videoChannelsSortValidator, 14 videoChannelsSortValidator,
15 videoChannelsUpdateValidator 15 videoChannelsUpdateValidator,
16 videoPlaylistsSortValidator
16} from '../../middlewares' 17} from '../../middlewares'
17import { VideoChannelModel } from '../../models/video/video-channel' 18import { VideoChannelModel } from '../../models/video/video-channel'
18import { videoChannelsNameWithHostValidator, videosSortValidator } from '../../middlewares/validators' 19import { videoChannelsNameWithHostValidator, videosSortValidator } from '../../middlewares/validators'
@@ -22,15 +23,18 @@ import { createVideoChannel } from '../../lib/video-channel'
22import { buildNSFWFilter, createReqFiles, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' 23import { buildNSFWFilter, createReqFiles, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
23import { setAsyncActorKeys } from '../../lib/activitypub' 24import { setAsyncActorKeys } from '../../lib/activitypub'
24import { AccountModel } from '../../models/account/account' 25import { AccountModel } from '../../models/account/account'
25import { CONFIG, MIMETYPES, sequelizeTypescript } from '../../initializers' 26import { MIMETYPES } from '../../initializers/constants'
26import { logger } from '../../helpers/logger' 27import { logger } from '../../helpers/logger'
27import { VideoModel } from '../../models/video/video' 28import { VideoModel } from '../../models/video/video'
28import { updateAvatarValidator } from '../../middlewares/validators/avatar' 29import { updateAvatarValidator } from '../../middlewares/validators/avatar'
29import { updateActorAvatarFile } from '../../lib/avatar' 30import { updateActorAvatarFile } from '../../lib/avatar'
30import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger' 31import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger'
31import { resetSequelizeInstance } from '../../helpers/database-utils' 32import { resetSequelizeInstance } from '../../helpers/database-utils'
32import { UserModel } from '../../models/account/user'
33import { JobQueue } from '../../lib/job-queue' 33import { JobQueue } from '../../lib/job-queue'
34import { VideoPlaylistModel } from '../../models/video/video-playlist'
35import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists'
36import { CONFIG } from '../../initializers/config'
37import { sequelizeTypescript } from '../../initializers/database'
34 38
35const auditLogger = auditLoggerFactory('channels') 39const auditLogger = auditLoggerFactory('channels')
36const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR }) 40const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR })
@@ -77,6 +81,16 @@ videoChannelRouter.get('/:nameWithHost',
77 asyncMiddleware(getVideoChannel) 81 asyncMiddleware(getVideoChannel)
78) 82)
79 83
84videoChannelRouter.get('/:nameWithHost/video-playlists',
85 asyncMiddleware(videoChannelsNameWithHostValidator),
86 paginationValidator,
87 videoPlaylistsSortValidator,
88 setDefaultSort,
89 setDefaultPagination,
90 commonVideoPlaylistFiltersValidator,
91 asyncMiddleware(listVideoChannelPlaylists)
92)
93
80videoChannelRouter.get('/:nameWithHost/videos', 94videoChannelRouter.get('/:nameWithHost/videos',
81 asyncMiddleware(videoChannelsNameWithHostValidator), 95 asyncMiddleware(videoChannelsNameWithHostValidator),
82 paginationValidator, 96 paginationValidator,
@@ -96,16 +110,16 @@ export {
96 110
97// --------------------------------------------------------------------------- 111// ---------------------------------------------------------------------------
98 112
99async function listVideoChannels (req: express.Request, res: express.Response, next: express.NextFunction) { 113async function listVideoChannels (req: express.Request, res: express.Response) {
100 const serverActor = await getServerActor() 114 const serverActor = await getServerActor()
101 const resultList = await VideoChannelModel.listForApi(serverActor.id, req.query.start, req.query.count, req.query.sort) 115 const resultList = await VideoChannelModel.listForApi(serverActor.id, req.query.start, req.query.count, req.query.sort)
102 116
103 return res.json(getFormattedObjects(resultList.data, resultList.total)) 117 return res.json(getFormattedObjects(resultList.data, resultList.total))
104} 118}
105 119
106async function updateVideoChannelAvatar (req: express.Request, res: express.Response, next: express.NextFunction) { 120async function updateVideoChannelAvatar (req: express.Request, res: express.Response) {
107 const avatarPhysicalFile = req.files[ 'avatarfile' ][ 0 ] 121 const avatarPhysicalFile = req.files[ 'avatarfile' ][ 0 ]
108 const videoChannel = res.locals.videoChannel as VideoChannelModel 122 const videoChannel = res.locals.videoChannel
109 const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON()) 123 const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON())
110 124
111 const avatar = await updateActorAvatarFile(avatarPhysicalFile, videoChannel) 125 const avatar = await updateActorAvatarFile(avatarPhysicalFile, videoChannel)
@@ -123,7 +137,7 @@ async function addVideoChannel (req: express.Request, res: express.Response) {
123 const videoChannelInfo: VideoChannelCreate = req.body 137 const videoChannelInfo: VideoChannelCreate = req.body
124 138
125 const videoChannelCreated: VideoChannelModel = await sequelizeTypescript.transaction(async t => { 139 const videoChannelCreated: VideoChannelModel = await sequelizeTypescript.transaction(async t => {
126 const account = await AccountModel.load((res.locals.oauth.token.User as UserModel).Account.id, t) 140 const account = await AccountModel.load(res.locals.oauth.token.User.Account.id, t)
127 141
128 return createVideoChannel(videoChannelInfo, account, t) 142 return createVideoChannel(videoChannelInfo, account, t)
129 }) 143 })
@@ -143,7 +157,7 @@ async function addVideoChannel (req: express.Request, res: express.Response) {
143} 157}
144 158
145async function updateVideoChannel (req: express.Request, res: express.Response) { 159async function updateVideoChannel (req: express.Request, res: express.Response) {
146 const videoChannelInstance = res.locals.videoChannel as VideoChannelModel 160 const videoChannelInstance = res.locals.videoChannel
147 const videoChannelFieldsSave = videoChannelInstance.toJSON() 161 const videoChannelFieldsSave = videoChannelInstance.toJSON()
148 const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannelInstance.toFormattedJSON()) 162 const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannelInstance.toFormattedJSON())
149 const videoChannelInfoToUpdate = req.body as VideoChannelUpdate 163 const videoChannelInfoToUpdate = req.body as VideoChannelUpdate
@@ -183,9 +197,11 @@ async function updateVideoChannel (req: express.Request, res: express.Response)
183} 197}
184 198
185async function removeVideoChannel (req: express.Request, res: express.Response) { 199async function removeVideoChannel (req: express.Request, res: express.Response) {
186 const videoChannelInstance: VideoChannelModel = res.locals.videoChannel 200 const videoChannelInstance = res.locals.videoChannel
187 201
188 await sequelizeTypescript.transaction(async t => { 202 await sequelizeTypescript.transaction(async t => {
203 await VideoPlaylistModel.resetPlaylistsOfChannel(videoChannelInstance.id, t)
204
189 await videoChannelInstance.destroy({ transaction: t }) 205 await videoChannelInstance.destroy({ transaction: t })
190 206
191 auditLogger.delete(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannelInstance.toFormattedJSON())) 207 auditLogger.delete(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannelInstance.toFormattedJSON()))
@@ -195,7 +211,7 @@ async function removeVideoChannel (req: express.Request, res: express.Response)
195 return res.type('json').status(204).end() 211 return res.type('json').status(204).end()
196} 212}
197 213
198async function getVideoChannel (req: express.Request, res: express.Response, next: express.NextFunction) { 214async function getVideoChannel (req: express.Request, res: express.Response) {
199 const videoChannelWithVideos = await VideoChannelModel.loadAndPopulateAccountAndVideos(res.locals.videoChannel.id) 215 const videoChannelWithVideos = await VideoChannelModel.loadAndPopulateAccountAndVideos(res.locals.videoChannel.id)
200 216
201 if (videoChannelWithVideos.isOutdated()) { 217 if (videoChannelWithVideos.isOutdated()) {
@@ -206,8 +222,23 @@ async function getVideoChannel (req: express.Request, res: express.Response, nex
206 return res.json(videoChannelWithVideos.toFormattedJSON()) 222 return res.json(videoChannelWithVideos.toFormattedJSON())
207} 223}
208 224
209async function listVideoChannelVideos (req: express.Request, res: express.Response, next: express.NextFunction) { 225async function listVideoChannelPlaylists (req: express.Request, res: express.Response) {
210 const videoChannelInstance: VideoChannelModel = res.locals.videoChannel 226 const serverActor = await getServerActor()
227
228 const resultList = await VideoPlaylistModel.listForApi({
229 followerActorId: serverActor.id,
230 start: req.query.start,
231 count: req.query.count,
232 sort: req.query.sort,
233 videoChannelId: res.locals.videoChannel.id,
234 type: req.query.playlistType
235 })
236
237 return res.json(getFormattedObjects(resultList.data, resultList.total))
238}
239
240async function listVideoChannelVideos (req: express.Request, res: express.Response) {
241 const videoChannelInstance = res.locals.videoChannel
211 const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined 242 const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined
212 243
213 const resultList = await VideoModel.listForApi({ 244 const resultList = await VideoModel.listForApi({
diff --git a/server/controllers/api/video-playlist.ts b/server/controllers/api/video-playlist.ts
new file mode 100644
index 000000000..a17136401
--- /dev/null
+++ b/server/controllers/api/video-playlist.ts
@@ -0,0 +1,445 @@
1import * as express from 'express'
2import { getFormattedObjects, getServerActor } from '../../helpers/utils'
3import {
4 asyncMiddleware,
5 asyncRetryTransactionMiddleware,
6 authenticate,
7 commonVideosFiltersValidator,
8 optionalAuthenticate,
9 paginationValidator,
10 setDefaultPagination,
11 setDefaultSort
12} from '../../middlewares'
13import { videoPlaylistsSortValidator } from '../../middlewares/validators'
14import { buildNSFWFilter, createReqFiles, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
15import { MIMETYPES, VIDEO_PLAYLIST_PRIVACIES } from '../../initializers/constants'
16import { logger } from '../../helpers/logger'
17import { resetSequelizeInstance } from '../../helpers/database-utils'
18import { VideoPlaylistModel } from '../../models/video/video-playlist'
19import {
20 commonVideoPlaylistFiltersValidator,
21 videoPlaylistsAddValidator,
22 videoPlaylistsAddVideoValidator,
23 videoPlaylistsDeleteValidator,
24 videoPlaylistsGetValidator,
25 videoPlaylistsReorderVideosValidator,
26 videoPlaylistsUpdateOrRemoveVideoValidator,
27 videoPlaylistsUpdateValidator
28} from '../../middlewares/validators/videos/video-playlists'
29import { VideoPlaylistCreate } from '../../../shared/models/videos/playlist/video-playlist-create.model'
30import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
31import { join } from 'path'
32import { sendCreateVideoPlaylist, sendDeleteVideoPlaylist, sendUpdateVideoPlaylist } from '../../lib/activitypub/send'
33import { getVideoPlaylistActivityPubUrl, getVideoPlaylistElementActivityPubUrl } from '../../lib/activitypub/url'
34import { VideoPlaylistUpdate } from '../../../shared/models/videos/playlist/video-playlist-update.model'
35import { VideoModel } from '../../models/video/video'
36import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element'
37import { VideoPlaylistElementCreate } from '../../../shared/models/videos/playlist/video-playlist-element-create.model'
38import { VideoPlaylistElementUpdate } from '../../../shared/models/videos/playlist/video-playlist-element-update.model'
39import { AccountModel } from '../../models/account/account'
40import { VideoPlaylistReorder } from '../../../shared/models/videos/playlist/video-playlist-reorder.model'
41import { JobQueue } from '../../lib/job-queue'
42import { CONFIG } from '../../initializers/config'
43import { sequelizeTypescript } from '../../initializers/database'
44import { createPlaylistMiniatureFromExisting } from '../../lib/thumbnail'
45
46const reqThumbnailFile = createReqFiles([ 'thumbnailfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { thumbnailfile: CONFIG.STORAGE.TMP_DIR })
47
48const videoPlaylistRouter = express.Router()
49
50videoPlaylistRouter.get('/privacies', listVideoPlaylistPrivacies)
51
52videoPlaylistRouter.get('/',
53 paginationValidator,
54 videoPlaylistsSortValidator,
55 setDefaultSort,
56 setDefaultPagination,
57 commonVideoPlaylistFiltersValidator,
58 asyncMiddleware(listVideoPlaylists)
59)
60
61videoPlaylistRouter.get('/:playlistId',
62 asyncMiddleware(videoPlaylistsGetValidator),
63 getVideoPlaylist
64)
65
66videoPlaylistRouter.post('/',
67 authenticate,
68 reqThumbnailFile,
69 asyncMiddleware(videoPlaylistsAddValidator),
70 asyncRetryTransactionMiddleware(addVideoPlaylist)
71)
72
73videoPlaylistRouter.put('/:playlistId',
74 authenticate,
75 reqThumbnailFile,
76 asyncMiddleware(videoPlaylistsUpdateValidator),
77 asyncRetryTransactionMiddleware(updateVideoPlaylist)
78)
79
80videoPlaylistRouter.delete('/:playlistId',
81 authenticate,
82 asyncMiddleware(videoPlaylistsDeleteValidator),
83 asyncRetryTransactionMiddleware(removeVideoPlaylist)
84)
85
86videoPlaylistRouter.get('/:playlistId/videos',
87 asyncMiddleware(videoPlaylistsGetValidator),
88 paginationValidator,
89 setDefaultPagination,
90 optionalAuthenticate,
91 commonVideosFiltersValidator,
92 asyncMiddleware(getVideoPlaylistVideos)
93)
94
95videoPlaylistRouter.post('/:playlistId/videos',
96 authenticate,
97 asyncMiddleware(videoPlaylistsAddVideoValidator),
98 asyncRetryTransactionMiddleware(addVideoInPlaylist)
99)
100
101videoPlaylistRouter.post('/:playlistId/videos/reorder',
102 authenticate,
103 asyncMiddleware(videoPlaylistsReorderVideosValidator),
104 asyncRetryTransactionMiddleware(reorderVideosPlaylist)
105)
106
107videoPlaylistRouter.put('/:playlistId/videos/:videoId',
108 authenticate,
109 asyncMiddleware(videoPlaylistsUpdateOrRemoveVideoValidator),
110 asyncRetryTransactionMiddleware(updateVideoPlaylistElement)
111)
112
113videoPlaylistRouter.delete('/:playlistId/videos/:videoId',
114 authenticate,
115 asyncMiddleware(videoPlaylistsUpdateOrRemoveVideoValidator),
116 asyncRetryTransactionMiddleware(removeVideoFromPlaylist)
117)
118
119// ---------------------------------------------------------------------------
120
121export {
122 videoPlaylistRouter
123}
124
125// ---------------------------------------------------------------------------
126
127function listVideoPlaylistPrivacies (req: express.Request, res: express.Response) {
128 res.json(VIDEO_PLAYLIST_PRIVACIES)
129}
130
131async function listVideoPlaylists (req: express.Request, res: express.Response) {
132 const serverActor = await getServerActor()
133 const resultList = await VideoPlaylistModel.listForApi({
134 followerActorId: serverActor.id,
135 start: req.query.start,
136 count: req.query.count,
137 sort: req.query.sort,
138 type: req.query.type
139 })
140
141 return res.json(getFormattedObjects(resultList.data, resultList.total))
142}
143
144function getVideoPlaylist (req: express.Request, res: express.Response) {
145 const videoPlaylist = res.locals.videoPlaylist
146
147 if (videoPlaylist.isOutdated()) {
148 JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video-playlist', url: videoPlaylist.url } })
149 .catch(err => logger.error('Cannot create AP refresher job for playlist %s.', videoPlaylist.url, { err }))
150 }
151
152 return res.json(videoPlaylist.toFormattedJSON())
153}
154
155async function addVideoPlaylist (req: express.Request, res: express.Response) {
156 const videoPlaylistInfo: VideoPlaylistCreate = req.body
157 const user = res.locals.oauth.token.User
158
159 const videoPlaylist = new VideoPlaylistModel({
160 name: videoPlaylistInfo.displayName,
161 description: videoPlaylistInfo.description,
162 privacy: videoPlaylistInfo.privacy || VideoPlaylistPrivacy.PRIVATE,
163 ownerAccountId: user.Account.id
164 })
165
166 videoPlaylist.url = getVideoPlaylistActivityPubUrl(videoPlaylist) // We use the UUID, so set the URL after building the object
167
168 if (videoPlaylistInfo.videoChannelId) {
169 const videoChannel = res.locals.videoChannel
170
171 videoPlaylist.videoChannelId = videoChannel.id
172 videoPlaylist.VideoChannel = videoChannel
173 }
174
175 const thumbnailField = req.files['thumbnailfile']
176 const thumbnailModel = thumbnailField
177 ? await createPlaylistMiniatureFromExisting(thumbnailField[0].path, videoPlaylist)
178 : undefined
179
180 const videoPlaylistCreated: VideoPlaylistModel = await sequelizeTypescript.transaction(async t => {
181 const videoPlaylistCreated = await videoPlaylist.save({ transaction: t })
182
183 if (thumbnailModel) await videoPlaylistCreated.setAndSaveThumbnail(thumbnailModel, t)
184
185 // We need more attributes for the federation
186 videoPlaylistCreated.OwnerAccount = await AccountModel.load(user.Account.id, t)
187 await sendCreateVideoPlaylist(videoPlaylistCreated, t)
188
189 return videoPlaylistCreated
190 })
191
192 logger.info('Video playlist with uuid %s created.', videoPlaylist.uuid)
193
194 return res.json({
195 videoPlaylist: {
196 id: videoPlaylistCreated.id,
197 uuid: videoPlaylistCreated.uuid
198 }
199 }).end()
200}
201
202async function updateVideoPlaylist (req: express.Request, res: express.Response) {
203 const videoPlaylistInstance = res.locals.videoPlaylist
204 const videoPlaylistFieldsSave = videoPlaylistInstance.toJSON()
205 const videoPlaylistInfoToUpdate = req.body as VideoPlaylistUpdate
206 const wasPrivatePlaylist = videoPlaylistInstance.privacy === VideoPlaylistPrivacy.PRIVATE
207
208 const thumbnailField = req.files['thumbnailfile']
209 const thumbnailModel = thumbnailField
210 ? await createPlaylistMiniatureFromExisting(thumbnailField[0].path, videoPlaylistInstance)
211 : undefined
212
213 try {
214 await sequelizeTypescript.transaction(async t => {
215 const sequelizeOptions = {
216 transaction: t
217 }
218
219 if (videoPlaylistInfoToUpdate.videoChannelId !== undefined) {
220 if (videoPlaylistInfoToUpdate.videoChannelId === null) {
221 videoPlaylistInstance.videoChannelId = null
222 } else {
223 const videoChannel = res.locals.videoChannel
224
225 videoPlaylistInstance.videoChannelId = videoChannel.id
226 videoPlaylistInstance.VideoChannel = videoChannel
227 }
228 }
229
230 if (videoPlaylistInfoToUpdate.displayName !== undefined) videoPlaylistInstance.name = videoPlaylistInfoToUpdate.displayName
231 if (videoPlaylistInfoToUpdate.description !== undefined) videoPlaylistInstance.description = videoPlaylistInfoToUpdate.description
232
233 if (videoPlaylistInfoToUpdate.privacy !== undefined) {
234 videoPlaylistInstance.privacy = parseInt(videoPlaylistInfoToUpdate.privacy.toString(), 10)
235 }
236
237 const playlistUpdated = await videoPlaylistInstance.save(sequelizeOptions)
238
239 if (thumbnailModel) await playlistUpdated.setAndSaveThumbnail(thumbnailModel, t)
240
241 const isNewPlaylist = wasPrivatePlaylist && playlistUpdated.privacy !== VideoPlaylistPrivacy.PRIVATE
242
243 if (isNewPlaylist) {
244 await sendCreateVideoPlaylist(playlistUpdated, t)
245 } else {
246 await sendUpdateVideoPlaylist(playlistUpdated, t)
247 }
248
249 logger.info('Video playlist %s updated.', videoPlaylistInstance.uuid)
250
251 return playlistUpdated
252 })
253 } catch (err) {
254 logger.debug('Cannot update the video playlist.', { err })
255
256 // Force fields we want to update
257 // If the transaction is retried, sequelize will think the object has not changed
258 // So it will skip the SQL request, even if the last one was ROLLBACKed!
259 resetSequelizeInstance(videoPlaylistInstance, videoPlaylistFieldsSave)
260
261 throw err
262 }
263
264 return res.type('json').status(204).end()
265}
266
267async function removeVideoPlaylist (req: express.Request, res: express.Response) {
268 const videoPlaylistInstance = res.locals.videoPlaylist
269
270 await sequelizeTypescript.transaction(async t => {
271 await videoPlaylistInstance.destroy({ transaction: t })
272
273 await sendDeleteVideoPlaylist(videoPlaylistInstance, t)
274
275 logger.info('Video playlist %s deleted.', videoPlaylistInstance.uuid)
276 })
277
278 return res.type('json').status(204).end()
279}
280
281async function addVideoInPlaylist (req: express.Request, res: express.Response) {
282 const body: VideoPlaylistElementCreate = req.body
283 const videoPlaylist = res.locals.videoPlaylist
284 const video = res.locals.video
285
286 const playlistElement: VideoPlaylistElementModel = await sequelizeTypescript.transaction(async t => {
287 const position = await VideoPlaylistElementModel.getNextPositionOf(videoPlaylist.id, t)
288
289 const playlistElement = await VideoPlaylistElementModel.create({
290 url: getVideoPlaylistElementActivityPubUrl(videoPlaylist, video),
291 position,
292 startTimestamp: body.startTimestamp || null,
293 stopTimestamp: body.stopTimestamp || null,
294 videoPlaylistId: videoPlaylist.id,
295 videoId: video.id
296 }, { transaction: t })
297
298 videoPlaylist.changed('updatedAt', true)
299 await videoPlaylist.save({ transaction: t })
300
301 await sendUpdateVideoPlaylist(videoPlaylist, t)
302
303 return playlistElement
304 })
305
306 // If the user did not set a thumbnail, automatically take the video thumbnail
307 if (videoPlaylist.hasThumbnail() === false) {
308 logger.info('Generating default thumbnail to playlist %s.', videoPlaylist.url)
309
310 const inputPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getMiniature().filename)
311 const thumbnailModel = await createPlaylistMiniatureFromExisting(inputPath, videoPlaylist, true)
312
313 thumbnailModel.videoPlaylistId = videoPlaylist.id
314
315 await thumbnailModel.save()
316 }
317
318 logger.info('Video added in playlist %s at position %d.', videoPlaylist.uuid, playlistElement.position)
319
320 return res.json({
321 videoPlaylistElement: {
322 id: playlistElement.id
323 }
324 }).end()
325}
326
327async function updateVideoPlaylistElement (req: express.Request, res: express.Response) {
328 const body: VideoPlaylistElementUpdate = req.body
329 const videoPlaylist = res.locals.videoPlaylist
330 const videoPlaylistElement = res.locals.videoPlaylistElement
331
332 const playlistElement: VideoPlaylistElementModel = await sequelizeTypescript.transaction(async t => {
333 if (body.startTimestamp !== undefined) videoPlaylistElement.startTimestamp = body.startTimestamp
334 if (body.stopTimestamp !== undefined) videoPlaylistElement.stopTimestamp = body.stopTimestamp
335
336 const element = await videoPlaylistElement.save({ transaction: t })
337
338 videoPlaylist.changed('updatedAt', true)
339 await videoPlaylist.save({ transaction: t })
340
341 await sendUpdateVideoPlaylist(videoPlaylist, t)
342
343 return element
344 })
345
346 logger.info('Element of position %d of playlist %s updated.', playlistElement.position, videoPlaylist.uuid)
347
348 return res.type('json').status(204).end()
349}
350
351async function removeVideoFromPlaylist (req: express.Request, res: express.Response) {
352 const videoPlaylistElement = res.locals.videoPlaylistElement
353 const videoPlaylist = res.locals.videoPlaylist
354 const positionToDelete = videoPlaylistElement.position
355
356 await sequelizeTypescript.transaction(async t => {
357 await videoPlaylistElement.destroy({ transaction: t })
358
359 // Decrease position of the next elements
360 await VideoPlaylistElementModel.increasePositionOf(videoPlaylist.id, positionToDelete, null, -1, t)
361
362 videoPlaylist.changed('updatedAt', true)
363 await videoPlaylist.save({ transaction: t })
364
365 await sendUpdateVideoPlaylist(videoPlaylist, t)
366
367 logger.info('Video playlist element %d of playlist %s deleted.', videoPlaylistElement.position, videoPlaylist.uuid)
368 })
369
370 return res.type('json').status(204).end()
371}
372
373async function reorderVideosPlaylist (req: express.Request, res: express.Response) {
374 const videoPlaylist = res.locals.videoPlaylist
375 const body: VideoPlaylistReorder = req.body
376
377 const start: number = body.startPosition
378 const insertAfter: number = body.insertAfterPosition
379 const reorderLength: number = body.reorderLength || 1
380
381 if (start === insertAfter) {
382 return res.status(204).end()
383 }
384
385 // Example: if we reorder position 2 and insert after position 5 (so at position 6): # 1 2 3 4 5 6 7 8 9
386 // * increase position when position > 5 # 1 2 3 4 5 7 8 9 10
387 // * update position 2 -> position 6 # 1 3 4 5 6 7 8 9 10
388 // * decrease position when position position > 2 # 1 2 3 4 5 6 7 8 9
389 await sequelizeTypescript.transaction(async t => {
390 const newPosition = insertAfter + 1
391
392 // Add space after the position when we want to insert our reordered elements (increase)
393 await VideoPlaylistElementModel.increasePositionOf(videoPlaylist.id, newPosition, null, reorderLength, t)
394
395 let oldPosition = start
396
397 // We incremented the position of the elements we want to reorder
398 if (start >= newPosition) oldPosition += reorderLength
399
400 const endOldPosition = oldPosition + reorderLength - 1
401 // Insert our reordered elements in their place (update)
402 await VideoPlaylistElementModel.reassignPositionOf(videoPlaylist.id, oldPosition, endOldPosition, newPosition, t)
403
404 // Decrease positions of elements after the old position of our ordered elements (decrease)
405 await VideoPlaylistElementModel.increasePositionOf(videoPlaylist.id, oldPosition, null, -reorderLength, t)
406
407 videoPlaylist.changed('updatedAt', true)
408 await videoPlaylist.save({ transaction: t })
409
410 await sendUpdateVideoPlaylist(videoPlaylist, t)
411 })
412
413 logger.info(
414 'Reordered playlist %s (inserted after %d elements %d - %d).',
415 videoPlaylist.uuid, insertAfter, start, start + reorderLength - 1
416 )
417
418 return res.type('json').status(204).end()
419}
420
421async function getVideoPlaylistVideos (req: express.Request, res: express.Response) {
422 const videoPlaylistInstance = res.locals.videoPlaylist
423 const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined
424
425 const resultList = await VideoModel.listForApi({
426 followerActorId,
427 start: req.query.start,
428 count: req.query.count,
429 sort: 'VideoPlaylistElements.position',
430 includeLocalVideos: true,
431 categoryOneOf: req.query.categoryOneOf,
432 licenceOneOf: req.query.licenceOneOf,
433 languageOneOf: req.query.languageOneOf,
434 tagsOneOf: req.query.tagsOneOf,
435 tagsAllOf: req.query.tagsAllOf,
436 filter: req.query.filter,
437 nsfw: buildNSFWFilter(res, req.query.nsfw),
438 withFiles: false,
439 videoPlaylistId: videoPlaylistInstance.id,
440 user: res.locals.oauth ? res.locals.oauth.token.User : undefined
441 })
442
443 const additionalAttributes = { playlistInfo: true }
444 return res.json(getFormattedObjects(resultList.data, resultList.total, { additionalAttributes }))
445}
diff --git a/server/controllers/api/videos/abuse.ts b/server/controllers/api/videos/abuse.ts
index fe0a95cd5..ca70230a2 100644
--- a/server/controllers/api/videos/abuse.ts
+++ b/server/controllers/api/videos/abuse.ts
@@ -3,7 +3,6 @@ import { UserRight, VideoAbuseCreate, VideoAbuseState } from '../../../../shared
3import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
4import { getFormattedObjects } from '../../../helpers/utils' 4import { getFormattedObjects } from '../../../helpers/utils'
5import { sequelizeTypescript } from '../../../initializers' 5import { sequelizeTypescript } from '../../../initializers'
6import { sendVideoAbuse } from '../../../lib/activitypub/send'
7import { 6import {
8 asyncMiddleware, 7 asyncMiddleware,
9 asyncRetryTransactionMiddleware, 8 asyncRetryTransactionMiddleware,
@@ -18,11 +17,10 @@ import {
18 videoAbuseUpdateValidator 17 videoAbuseUpdateValidator
19} from '../../../middlewares' 18} from '../../../middlewares'
20import { AccountModel } from '../../../models/account/account' 19import { AccountModel } from '../../../models/account/account'
21import { VideoModel } from '../../../models/video/video'
22import { VideoAbuseModel } from '../../../models/video/video-abuse' 20import { VideoAbuseModel } from '../../../models/video/video-abuse'
23import { auditLoggerFactory, VideoAbuseAuditView } from '../../../helpers/audit-logger' 21import { auditLoggerFactory, VideoAbuseAuditView } from '../../../helpers/audit-logger'
24import { UserModel } from '../../../models/account/user'
25import { Notifier } from '../../../lib/notifier' 22import { Notifier } from '../../../lib/notifier'
23import { sendVideoAbuse } from '../../../lib/activitypub/send/send-flag'
26 24
27const auditLogger = auditLoggerFactory('abuse') 25const auditLogger = auditLoggerFactory('abuse')
28const abuseVideoRouter = express.Router() 26const abuseVideoRouter = express.Router()
@@ -69,7 +67,7 @@ async function listVideoAbuses (req: express.Request, res: express.Response) {
69} 67}
70 68
71async function updateVideoAbuse (req: express.Request, res: express.Response) { 69async function updateVideoAbuse (req: express.Request, res: express.Response) {
72 const videoAbuse: VideoAbuseModel = res.locals.videoAbuse 70 const videoAbuse = res.locals.videoAbuse
73 71
74 if (req.body.moderationComment !== undefined) videoAbuse.moderationComment = req.body.moderationComment 72 if (req.body.moderationComment !== undefined) videoAbuse.moderationComment = req.body.moderationComment
75 if (req.body.state !== undefined) videoAbuse.state = req.body.state 73 if (req.body.state !== undefined) videoAbuse.state = req.body.state
@@ -84,7 +82,7 @@ async function updateVideoAbuse (req: express.Request, res: express.Response) {
84} 82}
85 83
86async function deleteVideoAbuse (req: express.Request, res: express.Response) { 84async function deleteVideoAbuse (req: express.Request, res: express.Response) {
87 const videoAbuse: VideoAbuseModel = res.locals.videoAbuse 85 const videoAbuse = res.locals.videoAbuse
88 86
89 await sequelizeTypescript.transaction(t => { 87 await sequelizeTypescript.transaction(t => {
90 return videoAbuse.destroy({ transaction: t }) 88 return videoAbuse.destroy({ transaction: t })
@@ -96,11 +94,11 @@ async function deleteVideoAbuse (req: express.Request, res: express.Response) {
96} 94}
97 95
98async function reportVideoAbuse (req: express.Request, res: express.Response) { 96async function reportVideoAbuse (req: express.Request, res: express.Response) {
99 const videoInstance = res.locals.video as VideoModel 97 const videoInstance = res.locals.video
100 const body: VideoAbuseCreate = req.body 98 const body: VideoAbuseCreate = req.body
101 99
102 const videoAbuse: VideoAbuseModel = await sequelizeTypescript.transaction(async t => { 100 const videoAbuse: VideoAbuseModel = await sequelizeTypescript.transaction(async t => {
103 const reporterAccount = await AccountModel.load((res.locals.oauth.token.User as UserModel).Account.id, t) 101 const reporterAccount = await AccountModel.load(res.locals.oauth.token.User.Account.id, t)
104 102
105 const abuseToCreate = { 103 const abuseToCreate = {
106 reporterAccountId: reporterAccount.id, 104 reporterAccountId: reporterAccount.id,
diff --git a/server/controllers/api/videos/blacklist.ts b/server/controllers/api/videos/blacklist.ts
index 43b0516e7..27dcfb761 100644
--- a/server/controllers/api/videos/blacklist.ts
+++ b/server/controllers/api/videos/blacklist.ts
@@ -1,5 +1,5 @@
1import * as express from 'express' 1import * as express from 'express'
2import { VideoBlacklist, UserRight, VideoBlacklistCreate } from '../../../../shared' 2import { VideoBlacklist, UserRight, VideoBlacklistCreate, VideoBlacklistType } from '../../../../shared'
3import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
4import { getFormattedObjects } from '../../../helpers/utils' 4import { getFormattedObjects } from '../../../helpers/utils'
5import { 5import {
@@ -12,13 +12,13 @@ import {
12 setDefaultPagination, 12 setDefaultPagination,
13 videosBlacklistAddValidator, 13 videosBlacklistAddValidator,
14 videosBlacklistRemoveValidator, 14 videosBlacklistRemoveValidator,
15 videosBlacklistUpdateValidator 15 videosBlacklistUpdateValidator,
16 videosBlacklistFiltersValidator
16} from '../../../middlewares' 17} from '../../../middlewares'
17import { VideoBlacklistModel } from '../../../models/video/video-blacklist' 18import { VideoBlacklistModel } from '../../../models/video/video-blacklist'
18import { sequelizeTypescript } from '../../../initializers' 19import { sequelizeTypescript } from '../../../initializers'
19import { Notifier } from '../../../lib/notifier' 20import { Notifier } from '../../../lib/notifier'
20import { VideoModel } from '../../../models/video/video' 21import { sendDeleteVideo } from '../../../lib/activitypub/send'
21import { sendCreateVideo, sendDeleteVideo, sendUpdateVideo } from '../../../lib/activitypub/send'
22import { federateVideoIfNeeded } from '../../../lib/activitypub' 22import { federateVideoIfNeeded } from '../../../lib/activitypub'
23 23
24const blacklistRouter = express.Router() 24const blacklistRouter = express.Router()
@@ -37,6 +37,7 @@ blacklistRouter.get('/blacklist',
37 blacklistSortValidator, 37 blacklistSortValidator,
38 setBlacklistSort, 38 setBlacklistSort,
39 setDefaultPagination, 39 setDefaultPagination,
40 videosBlacklistFiltersValidator,
40 asyncMiddleware(listBlacklist) 41 asyncMiddleware(listBlacklist)
41) 42)
42 43
@@ -69,7 +70,8 @@ async function addVideoToBlacklist (req: express.Request, res: express.Response)
69 const toCreate = { 70 const toCreate = {
70 videoId: videoInstance.id, 71 videoId: videoInstance.id,
71 unfederated: body.unfederate === true, 72 unfederated: body.unfederate === true,
72 reason: body.reason 73 reason: body.reason,
74 type: VideoBlacklistType.MANUAL
73 } 75 }
74 76
75 const blacklist = await VideoBlacklistModel.create(toCreate) 77 const blacklist = await VideoBlacklistModel.create(toCreate)
@@ -87,7 +89,7 @@ async function addVideoToBlacklist (req: express.Request, res: express.Response)
87} 89}
88 90
89async function updateVideoBlacklistController (req: express.Request, res: express.Response) { 91async function updateVideoBlacklistController (req: express.Request, res: express.Response) {
90 const videoBlacklist = res.locals.videoBlacklist as VideoBlacklistModel 92 const videoBlacklist = res.locals.videoBlacklist
91 93
92 if (req.body.reason !== undefined) videoBlacklist.reason = req.body.reason 94 if (req.body.reason !== undefined) videoBlacklist.reason = req.body.reason
93 95
@@ -99,27 +101,39 @@ async function updateVideoBlacklistController (req: express.Request, res: expres
99} 101}
100 102
101async function listBlacklist (req: express.Request, res: express.Response, next: express.NextFunction) { 103async function listBlacklist (req: express.Request, res: express.Response, next: express.NextFunction) {
102 const resultList = await VideoBlacklistModel.listForApi(req.query.start, req.query.count, req.query.sort) 104 const resultList = await VideoBlacklistModel.listForApi(req.query.start, req.query.count, req.query.sort, req.query.type)
103 105
104 return res.json(getFormattedObjects<VideoBlacklist, VideoBlacklistModel>(resultList.data, resultList.total)) 106 return res.json(getFormattedObjects<VideoBlacklist, VideoBlacklistModel>(resultList.data, resultList.total))
105} 107}
106 108
107async function removeVideoFromBlacklistController (req: express.Request, res: express.Response, next: express.NextFunction) { 109async function removeVideoFromBlacklistController (req: express.Request, res: express.Response, next: express.NextFunction) {
108 const videoBlacklist = res.locals.videoBlacklist as VideoBlacklistModel 110 const videoBlacklist = res.locals.videoBlacklist
109 const video: VideoModel = res.locals.video 111 const video = res.locals.video
110 112
111 await sequelizeTypescript.transaction(async t => { 113 const videoBlacklistType = await sequelizeTypescript.transaction(async t => {
112 const unfederated = videoBlacklist.unfederated 114 const unfederated = videoBlacklist.unfederated
115 const videoBlacklistType = videoBlacklist.type
116
113 await videoBlacklist.destroy({ transaction: t }) 117 await videoBlacklist.destroy({ transaction: t })
114 118
115 // Re federate the video 119 // Re federate the video
116 if (unfederated === true) { 120 if (unfederated === true) {
117 await federateVideoIfNeeded(video, true, t) 121 await federateVideoIfNeeded(video, true, t)
118 } 122 }
123
124 return videoBlacklistType
119 }) 125 })
120 126
121 Notifier.Instance.notifyOnVideoUnblacklist(video) 127 Notifier.Instance.notifyOnVideoUnblacklist(video)
122 128
129 if (videoBlacklistType === VideoBlacklistType.AUTO_BEFORE_PUBLISHED) {
130 Notifier.Instance.notifyOnVideoPublishedAfterRemovedFromAutoBlacklist(video)
131
132 // Delete on object so new video notifications will send
133 delete video.VideoBlacklist
134 Notifier.Instance.notifyOnNewVideo(video)
135 }
136
123 logger.info('Video %s removed from blacklist.', res.locals.video.uuid) 137 logger.info('Video %s removed from blacklist.', res.locals.video.uuid)
124 138
125 return res.type('json').status(204).end() 139 return res.type('json').status(204).end()
diff --git a/server/controllers/api/videos/captions.ts b/server/controllers/api/videos/captions.ts
index 9b3661368..44c255232 100644
--- a/server/controllers/api/videos/captions.ts
+++ b/server/controllers/api/videos/captions.ts
@@ -2,13 +2,14 @@ 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, MIMETYPES, sequelizeTypescript } from '../../../initializers' 5import { MIMETYPES } from '../../../initializers/constants'
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'
9import { logger } from '../../../helpers/logger' 8import { logger } from '../../../helpers/logger'
10import { federateVideoIfNeeded } from '../../../lib/activitypub' 9import { federateVideoIfNeeded } from '../../../lib/activitypub'
11import { moveAndProcessCaptionFile } from '../../../helpers/captions-utils' 10import { moveAndProcessCaptionFile } from '../../../helpers/captions-utils'
11import { CONFIG } from '../../../initializers/config'
12import { sequelizeTypescript } from '../../../initializers/database'
12 13
13const reqVideoCaptionAdd = createReqFiles( 14const reqVideoCaptionAdd = createReqFiles(
14 [ 'captionfile' ], 15 [ 'captionfile' ],
@@ -52,7 +53,7 @@ async function listVideoCaptions (req: express.Request, res: express.Response) {
52 53
53async function addVideoCaption (req: express.Request, res: express.Response) { 54async function addVideoCaption (req: express.Request, res: express.Response) {
54 const videoCaptionPhysicalFile = req.files['captionfile'][0] 55 const videoCaptionPhysicalFile = req.files['captionfile'][0]
55 const video = res.locals.video as VideoModel 56 const video = res.locals.video
56 57
57 const videoCaption = new VideoCaptionModel({ 58 const videoCaption = new VideoCaptionModel({
58 videoId: video.id, 59 videoId: video.id,
@@ -74,8 +75,8 @@ async function addVideoCaption (req: express.Request, res: express.Response) {
74} 75}
75 76
76async function deleteVideoCaption (req: express.Request, res: express.Response) { 77async function deleteVideoCaption (req: express.Request, res: express.Response) {
77 const video = res.locals.video as VideoModel 78 const video = res.locals.video
78 const videoCaption = res.locals.videoCaption as VideoCaptionModel 79 const videoCaption = res.locals.videoCaption
79 80
80 await sequelizeTypescript.transaction(async t => { 81 await sequelizeTypescript.transaction(async t => {
81 await videoCaption.destroy({ transaction: t }) 82 await videoCaption.destroy({ transaction: t })
diff --git a/server/controllers/api/videos/comment.ts b/server/controllers/api/videos/comment.ts
index 70c1148ba..176ee8bd4 100644
--- a/server/controllers/api/videos/comment.ts
+++ b/server/controllers/api/videos/comment.ts
@@ -8,7 +8,8 @@ import { buildFormattedCommentTree, createVideoComment } from '../../../lib/vide
8import { 8import {
9 asyncMiddleware, 9 asyncMiddleware,
10 asyncRetryTransactionMiddleware, 10 asyncRetryTransactionMiddleware,
11 authenticate, optionalAuthenticate, 11 authenticate,
12 optionalAuthenticate,
12 paginationValidator, 13 paginationValidator,
13 setDefaultPagination, 14 setDefaultPagination,
14 setDefaultSort 15 setDefaultSort
@@ -21,11 +22,9 @@ import {
21 removeVideoCommentValidator, 22 removeVideoCommentValidator,
22 videoCommentThreadsSortValidator 23 videoCommentThreadsSortValidator
23} from '../../../middlewares/validators' 24} from '../../../middlewares/validators'
24import { VideoModel } from '../../../models/video/video'
25import { VideoCommentModel } from '../../../models/video/video-comment' 25import { 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'
29import { Notifier } from '../../../lib/notifier' 28import { Notifier } from '../../../lib/notifier'
30 29
31const auditLogger = auditLoggerFactory('comments') 30const auditLogger = auditLoggerFactory('comments')
@@ -70,9 +69,9 @@ export {
70 69
71// --------------------------------------------------------------------------- 70// ---------------------------------------------------------------------------
72 71
73async function listVideoThreads (req: express.Request, res: express.Response, next: express.NextFunction) { 72async function listVideoThreads (req: express.Request, res: express.Response) {
74 const video = res.locals.video as VideoModel 73 const video = res.locals.video
75 const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined 74 const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
76 75
77 let resultList: ResultList<VideoCommentModel> 76 let resultList: ResultList<VideoCommentModel>
78 77
@@ -88,9 +87,9 @@ async function listVideoThreads (req: express.Request, res: express.Response, ne
88 return res.json(getFormattedObjects(resultList.data, resultList.total)) 87 return res.json(getFormattedObjects(resultList.data, resultList.total))
89} 88}
90 89
91async function listVideoThreadComments (req: express.Request, res: express.Response, next: express.NextFunction) { 90async function listVideoThreadComments (req: express.Request, res: express.Response) {
92 const video = res.locals.video as VideoModel 91 const video = res.locals.video
93 const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined 92 const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
94 93
95 let resultList: ResultList<VideoCommentModel> 94 let resultList: ResultList<VideoCommentModel>
96 95
@@ -110,7 +109,7 @@ async function addVideoCommentThread (req: express.Request, res: express.Respons
110 const videoCommentInfo: VideoCommentCreate = req.body 109 const videoCommentInfo: VideoCommentCreate = req.body
111 110
112 const comment = await sequelizeTypescript.transaction(async t => { 111 const comment = await sequelizeTypescript.transaction(async t => {
113 const account = await AccountModel.load((res.locals.oauth.token.User as UserModel).Account.id, t) 112 const account = await AccountModel.load(res.locals.oauth.token.User.Account.id, t)
114 113
115 return createVideoComment({ 114 return createVideoComment({
116 text: videoCommentInfo.text, 115 text: videoCommentInfo.text,
@@ -132,7 +131,7 @@ async function addVideoCommentReply (req: express.Request, res: express.Response
132 const videoCommentInfo: VideoCommentCreate = req.body 131 const videoCommentInfo: VideoCommentCreate = req.body
133 132
134 const comment = await sequelizeTypescript.transaction(async t => { 133 const comment = await sequelizeTypescript.transaction(async t => {
135 const account = await AccountModel.load((res.locals.oauth.token.User as UserModel).Account.id, t) 134 const account = await AccountModel.load(res.locals.oauth.token.User.Account.id, t)
136 135
137 return createVideoComment({ 136 return createVideoComment({
138 text: videoCommentInfo.text, 137 text: videoCommentInfo.text,
@@ -149,7 +148,7 @@ async function addVideoCommentReply (req: express.Request, res: express.Response
149} 148}
150 149
151async function removeVideoComment (req: express.Request, res: express.Response) { 150async function removeVideoComment (req: express.Request, res: express.Response) {
152 const videoCommentInstance: VideoCommentModel = res.locals.videoComment 151 const videoCommentInstance = res.locals.videoComment
153 152
154 await sequelizeTypescript.transaction(async t => { 153 await sequelizeTypescript.transaction(async t => {
155 await videoCommentInstance.destroy({ transaction: t }) 154 await videoCommentInstance.destroy({ transaction: t })
diff --git a/server/controllers/api/videos/import.ts b/server/controllers/api/videos/import.ts
index 98366cd82..bfb690906 100644
--- a/server/controllers/api/videos/import.ts
+++ b/server/controllers/api/videos/import.ts
@@ -3,7 +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 { CONFIG, MIMETYPES, PREVIEWS_SIZE, sequelizeTypescript, THUMBNAILS_SIZE } from '../../../initializers' 6import { MIMETYPES } from '../../../initializers/constants'
7import { getYoutubeDLInfo, YoutubeDLInfo } from '../../../helpers/youtube-dl' 7import { getYoutubeDLInfo, YoutubeDLInfo } from '../../../helpers/youtube-dl'
8import { createReqFiles } from '../../../helpers/express-utils' 8import { createReqFiles } from '../../../helpers/express-utils'
9import { logger } from '../../../helpers/logger' 9import { logger } from '../../../helpers/logger'
@@ -13,15 +13,19 @@ import { getVideoActivityPubUrl } from '../../../lib/activitypub'
13import { TagModel } from '../../../models/video/tag' 13import { TagModel } from '../../../models/video/tag'
14import { VideoImportModel } from '../../../models/video/video-import' 14import { VideoImportModel } from '../../../models/video/video-import'
15import { JobQueue } from '../../../lib/job-queue/job-queue' 15import { JobQueue } from '../../../lib/job-queue/job-queue'
16import { processImage } from '../../../helpers/image-utils'
17import { join } from 'path' 16import { join } from 'path'
18import { isArray } from '../../../helpers/custom-validators/misc' 17import { isArray } from '../../../helpers/custom-validators/misc'
19import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model'
20import { VideoChannelModel } from '../../../models/video/video-channel' 18import { VideoChannelModel } from '../../../models/video/video-channel'
21import * as Bluebird from 'bluebird' 19import * as Bluebird from 'bluebird'
22import * as parseTorrent from 'parse-torrent' 20import * as parseTorrent from 'parse-torrent'
23import { getSecureTorrentName } from '../../../helpers/utils' 21import { getSecureTorrentName } from '../../../helpers/utils'
24import { readFile, move } from 'fs-extra' 22import { move, readFile } from 'fs-extra'
23import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
24import { CONFIG } from '../../../initializers/config'
25import { sequelizeTypescript } from '../../../initializers/database'
26import { createVideoMiniatureFromExisting } from '../../../lib/thumbnail'
27import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
28import { ThumbnailModel } from '../../../models/video/thumbnail'
25 29
26const auditLogger = auditLoggerFactory('video-imports') 30const auditLogger = auditLoggerFactory('video-imports')
27const videoImportsRouter = express.Router() 31const videoImportsRouter = express.Router()
@@ -87,8 +91,8 @@ async function addTorrentImport (req: express.Request, res: express.Response, to
87 91
88 const video = buildVideo(res.locals.videoChannel.id, body, { name: videoName }) 92 const video = buildVideo(res.locals.videoChannel.id, body, { name: videoName })
89 93
90 await processThumbnail(req, video) 94 const thumbnailModel = await processThumbnail(req, video)
91 await processPreview(req, video) 95 const previewModel = await processPreview(req, video)
92 96
93 const tags = body.tags || undefined 97 const tags = body.tags || undefined
94 const videoImportAttributes = { 98 const videoImportAttributes = {
@@ -97,7 +101,14 @@ async function addTorrentImport (req: express.Request, res: express.Response, to
97 state: VideoImportState.PENDING, 101 state: VideoImportState.PENDING,
98 userId: user.id 102 userId: user.id
99 } 103 }
100 const videoImport: VideoImportModel = await insertIntoDB(video, res.locals.videoChannel, tags, videoImportAttributes) 104 const videoImport = await insertIntoDB({
105 video,
106 thumbnailModel,
107 previewModel,
108 videoChannel: res.locals.videoChannel,
109 tags,
110 videoImportAttributes
111 })
101 112
102 // Create job to import the video 113 // Create job to import the video
103 const payload = { 114 const payload = {
@@ -130,8 +141,8 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response)
130 141
131 const video = buildVideo(res.locals.videoChannel.id, body, youtubeDLInfo) 142 const video = buildVideo(res.locals.videoChannel.id, body, youtubeDLInfo)
132 143
133 const downloadThumbnail = !await processThumbnail(req, video) 144 const thumbnailModel = await processThumbnail(req, video)
134 const downloadPreview = !await processPreview(req, video) 145 const previewModel = await processPreview(req, video)
135 146
136 const tags = body.tags || youtubeDLInfo.tags 147 const tags = body.tags || youtubeDLInfo.tags
137 const videoImportAttributes = { 148 const videoImportAttributes = {
@@ -139,15 +150,22 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response)
139 state: VideoImportState.PENDING, 150 state: VideoImportState.PENDING,
140 userId: user.id 151 userId: user.id
141 } 152 }
142 const videoImport: VideoImportModel = await insertIntoDB(video, res.locals.videoChannel, tags, videoImportAttributes) 153 const videoImport = await insertIntoDB({
154 video: video,
155 thumbnailModel,
156 previewModel,
157 videoChannel: res.locals.videoChannel,
158 tags,
159 videoImportAttributes
160 })
143 161
144 // Create job to import the video 162 // Create job to import the video
145 const payload = { 163 const payload = {
146 type: 'youtube-dl' as 'youtube-dl', 164 type: 'youtube-dl' as 'youtube-dl',
147 videoImportId: videoImport.id, 165 videoImportId: videoImport.id,
148 thumbnailUrl: youtubeDLInfo.thumbnailUrl, 166 thumbnailUrl: youtubeDLInfo.thumbnailUrl,
149 downloadThumbnail, 167 downloadThumbnail: !thumbnailModel,
150 downloadPreview 168 downloadPreview: !previewModel
151 } 169 }
152 await JobQueue.Instance.createJob({ type: 'video-import', payload }) 170 await JobQueue.Instance.createJob({ type: 'video-import', payload })
153 171
@@ -164,6 +182,7 @@ function buildVideo (channelId: number, body: VideoImportCreate, importData: You
164 licence: body.licence || importData.licence, 182 licence: body.licence || importData.licence,
165 language: body.language || undefined, 183 language: body.language || undefined,
166 commentsEnabled: body.commentsEnabled || true, 184 commentsEnabled: body.commentsEnabled || true,
185 downloadEnabled: body.downloadEnabled || true,
167 waitTranscoding: body.waitTranscoding || false, 186 waitTranscoding: body.waitTranscoding || false,
168 state: VideoState.TO_IMPORT, 187 state: VideoState.TO_IMPORT,
169 nsfw: body.nsfw || importData.nsfw || false, 188 nsfw: body.nsfw || importData.nsfw || false,
@@ -171,7 +190,8 @@ function buildVideo (channelId: number, body: VideoImportCreate, importData: You
171 support: body.support || null, 190 support: body.support || null,
172 privacy: body.privacy || VideoPrivacy.PRIVATE, 191 privacy: body.privacy || VideoPrivacy.PRIVATE,
173 duration: 0, // duration will be set by the import job 192 duration: 0, // duration will be set by the import job
174 channelId: channelId 193 channelId: channelId,
194 originallyPublishedAt: importData.originallyPublishedAt
175 } 195 }
176 const video = new VideoModel(videoData) 196 const video = new VideoModel(videoData)
177 video.url = getVideoActivityPubUrl(video) 197 video.url = getVideoActivityPubUrl(video)
@@ -183,32 +203,34 @@ async function processThumbnail (req: express.Request, video: VideoModel) {
183 const thumbnailField = req.files ? req.files['thumbnailfile'] : undefined 203 const thumbnailField = req.files ? req.files['thumbnailfile'] : undefined
184 if (thumbnailField) { 204 if (thumbnailField) {
185 const thumbnailPhysicalFile = thumbnailField[ 0 ] 205 const thumbnailPhysicalFile = thumbnailField[ 0 ]
186 await processImage(thumbnailPhysicalFile, join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName()), THUMBNAILS_SIZE)
187 206
188 return true 207 return createVideoMiniatureFromExisting(thumbnailPhysicalFile.path, video, ThumbnailType.MINIATURE)
189 } 208 }
190 209
191 return false 210 return undefined
192} 211}
193 212
194async function processPreview (req: express.Request, video: VideoModel) { 213async function processPreview (req: express.Request, video: VideoModel) {
195 const previewField = req.files ? req.files['previewfile'] : undefined 214 const previewField = req.files ? req.files['previewfile'] : undefined
196 if (previewField) { 215 if (previewField) {
197 const previewPhysicalFile = previewField[0] 216 const previewPhysicalFile = previewField[0]
198 await processImage(previewPhysicalFile, join(CONFIG.STORAGE.PREVIEWS_DIR, video.getPreviewName()), PREVIEWS_SIZE)
199 217
200 return true 218 return createVideoMiniatureFromExisting(previewPhysicalFile.path, video, ThumbnailType.PREVIEW)
201 } 219 }
202 220
203 return false 221 return undefined
204} 222}
205 223
206function insertIntoDB ( 224function insertIntoDB (parameters: {
207 video: VideoModel, 225 video: VideoModel,
226 thumbnailModel: ThumbnailModel,
227 previewModel: ThumbnailModel,
208 videoChannel: VideoChannelModel, 228 videoChannel: VideoChannelModel,
209 tags: string[], 229 tags: string[],
210 videoImportAttributes: FilteredModelAttributes<VideoImportModel> 230 videoImportAttributes: Partial<VideoImportModel>
211): Bluebird<VideoImportModel> { 231}): Bluebird<VideoImportModel> {
232 let { video, thumbnailModel, previewModel, videoChannel, tags, videoImportAttributes } = parameters
233
212 return sequelizeTypescript.transaction(async t => { 234 return sequelizeTypescript.transaction(async t => {
213 const sequelizeOptions = { transaction: t } 235 const sequelizeOptions = { transaction: t }
214 236
@@ -216,6 +238,11 @@ function insertIntoDB (
216 const videoCreated = await video.save(sequelizeOptions) 238 const videoCreated = await video.save(sequelizeOptions)
217 videoCreated.VideoChannel = videoChannel 239 videoCreated.VideoChannel = videoChannel
218 240
241 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
242 if (previewModel) await videoCreated.addAndSaveThumbnail(previewModel, t)
243
244 await autoBlacklistVideoIfNeeded(video, videoChannel.Account.User, t)
245
219 // Set tags to the video 246 // Set tags to the video
220 if (tags) { 247 if (tags) {
221 const tagInstances = await TagModel.findOrCreateTags(tags, t) 248 const tagInstances = await TagModel.findOrCreateTags(tags, t)
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index 2b2dfa7ca..1a18a8ae8 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -2,28 +2,17 @@ import * as express from 'express'
2import { extname, join } from 'path' 2import { extname, join } from 'path'
3import { VideoCreate, VideoPrivacy, VideoState, VideoUpdate } from '../../../../shared' 3import { VideoCreate, VideoPrivacy, VideoState, VideoUpdate } from '../../../../shared'
4import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' 4import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
5import { processImage } from '../../../helpers/image-utils'
6import { logger } from '../../../helpers/logger' 5import { logger } from '../../../helpers/logger'
7import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' 6import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
8import { getFormattedObjects, getServerActor } from '../../../helpers/utils' 7import { getFormattedObjects, getServerActor } from '../../../helpers/utils'
9import { 8import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
10 CONFIG, 9import { MIMETYPES, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../initializers/constants'
11 MIMETYPES,
12 PREVIEWS_SIZE,
13 sequelizeTypescript,
14 THUMBNAILS_SIZE,
15 VIDEO_CATEGORIES,
16 VIDEO_LANGUAGES,
17 VIDEO_LICENCES,
18 VIDEO_PRIVACIES
19} from '../../../initializers'
20import { 10import {
21 changeVideoChannelShare, 11 changeVideoChannelShare,
22 federateVideoIfNeeded, 12 federateVideoIfNeeded,
23 fetchRemoteVideoDescription, 13 fetchRemoteVideoDescription,
24 getVideoActivityPubUrl 14 getVideoActivityPubUrl
25} from '../../../lib/activitypub' 15} from '../../../lib/activitypub'
26import { sendCreateView } from '../../../lib/activitypub/send'
27import { JobQueue } from '../../../lib/job-queue' 16import { JobQueue } from '../../../lib/job-queue'
28import { Redis } from '../../../lib/redis' 17import { Redis } from '../../../lib/redis'
29import { 18import {
@@ -37,6 +26,7 @@ import {
37 setDefaultPagination, 26 setDefaultPagination,
38 setDefaultSort, 27 setDefaultSort,
39 videosAddValidator, 28 videosAddValidator,
29 videosCustomGetValidator,
40 videosGetValidator, 30 videosGetValidator,
41 videosRemoveValidator, 31 videosRemoveValidator,
42 videosSortValidator, 32 videosSortValidator,
@@ -59,6 +49,11 @@ import { resetSequelizeInstance } from '../../../helpers/database-utils'
59import { move } from 'fs-extra' 49import { move } from 'fs-extra'
60import { watchingRouter } from './watching' 50import { watchingRouter } from './watching'
61import { Notifier } from '../../../lib/notifier' 51import { Notifier } from '../../../lib/notifier'
52import { sendView } from '../../../lib/activitypub/send/send-view'
53import { CONFIG } from '../../../initializers/config'
54import { sequelizeTypescript } from '../../../initializers/database'
55import { createVideoMiniatureFromExisting, generateVideoMiniature } from '../../../lib/thumbnail'
56import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
62 57
63const auditLogger = auditLoggerFactory('videos') 58const auditLogger = auditLoggerFactory('videos')
64const videosRouter = express.Router() 59const videosRouter = express.Router()
@@ -123,9 +118,9 @@ videosRouter.get('/:id/description',
123) 118)
124videosRouter.get('/:id', 119videosRouter.get('/:id',
125 optionalAuthenticate, 120 optionalAuthenticate,
126 asyncMiddleware(videosGetValidator), 121 asyncMiddleware(videosCustomGetValidator('only-video-with-rights')),
127 asyncMiddleware(checkVideoFollowConstraints), 122 asyncMiddleware(checkVideoFollowConstraints),
128 getVideo 123 asyncMiddleware(getVideo)
129) 124)
130videosRouter.post('/:id/views', 125videosRouter.post('/:id/views',
131 asyncMiddleware(videosGetValidator), 126 asyncMiddleware(videosGetValidator),
@@ -181,15 +176,18 @@ async function addVideo (req: express.Request, res: express.Response) {
181 licence: videoInfo.licence, 176 licence: videoInfo.licence,
182 language: videoInfo.language, 177 language: videoInfo.language,
183 commentsEnabled: videoInfo.commentsEnabled || false, 178 commentsEnabled: videoInfo.commentsEnabled || false,
179 downloadEnabled: videoInfo.downloadEnabled || true,
184 waitTranscoding: videoInfo.waitTranscoding || false, 180 waitTranscoding: videoInfo.waitTranscoding || false,
185 state: CONFIG.TRANSCODING.ENABLED ? VideoState.TO_TRANSCODE : VideoState.PUBLISHED, 181 state: CONFIG.TRANSCODING.ENABLED ? VideoState.TO_TRANSCODE : VideoState.PUBLISHED,
186 nsfw: videoInfo.nsfw || false, 182 nsfw: videoInfo.nsfw || false,
187 description: videoInfo.description, 183 description: videoInfo.description,
188 support: videoInfo.support, 184 support: videoInfo.support,
189 privacy: videoInfo.privacy, 185 privacy: videoInfo.privacy || VideoPrivacy.PRIVATE,
190 duration: videoPhysicalFile['duration'], // duration was added by a previous middleware 186 duration: videoPhysicalFile['duration'], // duration was added by a previous middleware
191 channelId: res.locals.videoChannel.id 187 channelId: res.locals.videoChannel.id,
188 originallyPublishedAt: videoInfo.originallyPublishedAt
192 } 189 }
190
193 const video = new VideoModel(videoData) 191 const video = new VideoModel(videoData)
194 video.url = getVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object 192 video.url = getVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object
195 193
@@ -215,29 +213,27 @@ async function addVideo (req: express.Request, res: express.Response) {
215 213
216 // Process thumbnail or create it from the video 214 // Process thumbnail or create it from the video
217 const thumbnailField = req.files['thumbnailfile'] 215 const thumbnailField = req.files['thumbnailfile']
218 if (thumbnailField) { 216 const thumbnailModel = thumbnailField
219 const thumbnailPhysicalFile = thumbnailField[0] 217 ? await createVideoMiniatureFromExisting(thumbnailField[0].path, video, ThumbnailType.MINIATURE)
220 await processImage(thumbnailPhysicalFile, join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName()), THUMBNAILS_SIZE) 218 : await generateVideoMiniature(video, videoFile, ThumbnailType.MINIATURE)
221 } else {
222 await video.createThumbnail(videoFile)
223 }
224 219
225 // Process preview or create it from the video 220 // Process preview or create it from the video
226 const previewField = req.files['previewfile'] 221 const previewField = req.files['previewfile']
227 if (previewField) { 222 const previewModel = previewField
228 const previewPhysicalFile = previewField[0] 223 ? await createVideoMiniatureFromExisting(previewField[0].path, video, ThumbnailType.PREVIEW)
229 await processImage(previewPhysicalFile, join(CONFIG.STORAGE.PREVIEWS_DIR, video.getPreviewName()), PREVIEWS_SIZE) 224 : await generateVideoMiniature(video, videoFile, ThumbnailType.PREVIEW)
230 } else {
231 await video.createPreview(videoFile)
232 }
233 225
234 // Create the torrent file 226 // Create the torrent file
235 await video.createTorrentAndSetInfoHash(videoFile) 227 await video.createTorrentAndSetInfoHash(videoFile)
236 228
237 const videoCreated = await sequelizeTypescript.transaction(async t => { 229 const { videoCreated, videoWasAutoBlacklisted } = await sequelizeTypescript.transaction(async t => {
238 const sequelizeOptions = { transaction: t } 230 const sequelizeOptions = { transaction: t }
239 231
240 const videoCreated = await video.save(sequelizeOptions) 232 const videoCreated = await video.save(sequelizeOptions)
233
234 await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
235 await videoCreated.addAndSaveThumbnail(previewModel, t)
236
241 // Do not forget to add video channel information to the created video 237 // Do not forget to add video channel information to the created video
242 videoCreated.VideoChannel = res.locals.videoChannel 238 videoCreated.VideoChannel = res.locals.videoChannel
243 239
@@ -263,15 +259,23 @@ async function addVideo (req: express.Request, res: express.Response) {
263 }, { transaction: t }) 259 }, { transaction: t })
264 } 260 }
265 261
266 await federateVideoIfNeeded(video, true, t) 262 const videoWasAutoBlacklisted = await autoBlacklistVideoIfNeeded(video, res.locals.oauth.token.User, t)
263
264 if (!videoWasAutoBlacklisted) {
265 await federateVideoIfNeeded(video, true, t)
266 }
267 267
268 auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON())) 268 auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON()))
269 logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid) 269 logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid)
270 270
271 return videoCreated 271 return { videoCreated, videoWasAutoBlacklisted }
272 }) 272 })
273 273
274 Notifier.Instance.notifyOnNewVideo(videoCreated) 274 if (videoWasAutoBlacklisted) {
275 Notifier.Instance.notifyOnVideoAutoBlacklist(videoCreated)
276 } else {
277 Notifier.Instance.notifyOnNewVideo(videoCreated)
278 }
275 279
276 if (video.state === VideoState.TO_TRANSCODE) { 280 if (video.state === VideoState.TO_TRANSCODE) {
277 // Put uuid because we don't have id auto incremented for now 281 // Put uuid because we don't have id auto incremented for now
@@ -280,7 +284,7 @@ async function addVideo (req: express.Request, res: express.Response) {
280 isNewVideo: true 284 isNewVideo: true
281 } 285 }
282 286
283 await JobQueue.Instance.createJob({ type: 'video-file', payload: dataInput }) 287 await JobQueue.Instance.createJob({ type: 'video-transcoding', payload: dataInput })
284 } 288 }
285 289
286 return res.json({ 290 return res.json({
@@ -292,7 +296,7 @@ async function addVideo (req: express.Request, res: express.Response) {
292} 296}
293 297
294async function updateVideo (req: express.Request, res: express.Response) { 298async function updateVideo (req: express.Request, res: express.Response) {
295 const videoInstance: VideoModel = res.locals.video 299 const videoInstance = res.locals.video
296 const videoFieldsSave = videoInstance.toJSON() 300 const videoFieldsSave = videoInstance.toJSON()
297 const oldVideoAuditView = new VideoAuditView(videoInstance.toFormattedDetailsJSON()) 301 const oldVideoAuditView = new VideoAuditView(videoInstance.toFormattedDetailsJSON())
298 const videoInfoToUpdate: VideoUpdate = req.body 302 const videoInfoToUpdate: VideoUpdate = req.body
@@ -300,16 +304,13 @@ async function updateVideo (req: express.Request, res: express.Response) {
300 const wasUnlistedVideo = videoInstance.privacy === VideoPrivacy.UNLISTED 304 const wasUnlistedVideo = videoInstance.privacy === VideoPrivacy.UNLISTED
301 305
302 // Process thumbnail or create it from the video 306 // Process thumbnail or create it from the video
303 if (req.files && req.files['thumbnailfile']) { 307 const thumbnailModel = req.files && req.files['thumbnailfile']
304 const thumbnailPhysicalFile = req.files['thumbnailfile'][0] 308 ? await createVideoMiniatureFromExisting(req.files['thumbnailfile'][0].path, videoInstance, ThumbnailType.MINIATURE)
305 await processImage(thumbnailPhysicalFile, join(CONFIG.STORAGE.THUMBNAILS_DIR, videoInstance.getThumbnailName()), THUMBNAILS_SIZE) 309 : undefined
306 }
307 310
308 // Process preview or create it from the video 311 const previewModel = req.files && req.files['previewfile']
309 if (req.files && req.files['previewfile']) { 312 ? await createVideoMiniatureFromExisting(req.files['previewfile'][0].path, videoInstance, ThumbnailType.PREVIEW)
310 const previewPhysicalFile = req.files['previewfile'][0] 313 : undefined
311 await processImage(previewPhysicalFile, join(CONFIG.STORAGE.PREVIEWS_DIR, videoInstance.getPreviewName()), PREVIEWS_SIZE)
312 }
313 314
314 try { 315 try {
315 const videoInstanceUpdated = await sequelizeTypescript.transaction(async t => { 316 const videoInstanceUpdated = await sequelizeTypescript.transaction(async t => {
@@ -325,17 +326,26 @@ async function updateVideo (req: express.Request, res: express.Response) {
325 if (videoInfoToUpdate.support !== undefined) videoInstance.set('support', videoInfoToUpdate.support) 326 if (videoInfoToUpdate.support !== undefined) videoInstance.set('support', videoInfoToUpdate.support)
326 if (videoInfoToUpdate.description !== undefined) videoInstance.set('description', videoInfoToUpdate.description) 327 if (videoInfoToUpdate.description !== undefined) videoInstance.set('description', videoInfoToUpdate.description)
327 if (videoInfoToUpdate.commentsEnabled !== undefined) videoInstance.set('commentsEnabled', videoInfoToUpdate.commentsEnabled) 328 if (videoInfoToUpdate.commentsEnabled !== undefined) videoInstance.set('commentsEnabled', videoInfoToUpdate.commentsEnabled)
329 if (videoInfoToUpdate.downloadEnabled !== undefined) videoInstance.set('downloadEnabled', videoInfoToUpdate.downloadEnabled)
330
331 if (videoInfoToUpdate.originallyPublishedAt !== undefined && videoInfoToUpdate.originallyPublishedAt !== null) {
332 videoInstance.originallyPublishedAt = new Date(videoInfoToUpdate.originallyPublishedAt)
333 }
334
328 if (videoInfoToUpdate.privacy !== undefined) { 335 if (videoInfoToUpdate.privacy !== undefined) {
329 const newPrivacy = parseInt(videoInfoToUpdate.privacy.toString(), 10) 336 const newPrivacy = parseInt(videoInfoToUpdate.privacy.toString(), 10)
330 videoInstance.set('privacy', newPrivacy) 337 videoInstance.privacy = newPrivacy
331 338
332 if (wasPrivateVideo === true && newPrivacy !== VideoPrivacy.PRIVATE) { 339 if (wasPrivateVideo === true && newPrivacy !== VideoPrivacy.PRIVATE) {
333 videoInstance.set('publishedAt', new Date()) 340 videoInstance.publishedAt = new Date()
334 } 341 }
335 } 342 }
336 343
337 const videoInstanceUpdated = await videoInstance.save(sequelizeOptions) 344 const videoInstanceUpdated = await videoInstance.save(sequelizeOptions)
338 345
346 if (thumbnailModel) await videoInstanceUpdated.addAndSaveThumbnail(thumbnailModel, t)
347 if (previewModel) await videoInstanceUpdated.addAndSaveThumbnail(previewModel, t)
348
339 // Video tags update? 349 // Video tags update?
340 if (videoInfoToUpdate.tags !== undefined) { 350 if (videoInfoToUpdate.tags !== undefined) {
341 const tagInstances = await TagModel.findOrCreateTags(videoInfoToUpdate.tags, t) 351 const tagInstances = await TagModel.findOrCreateTags(videoInfoToUpdate.tags, t)
@@ -395,22 +405,24 @@ async function updateVideo (req: express.Request, res: express.Response) {
395 return res.type('json').status(204).end() 405 return res.type('json').status(204).end()
396} 406}
397 407
398function getVideo (req: express.Request, res: express.Response) { 408async function getVideo (req: express.Request, res: express.Response) {
399 const videoInstance = res.locals.video 409 // We need more attributes
410 const userId: number = res.locals.oauth ? res.locals.oauth.token.User.id : null
411 const video = await VideoModel.loadForGetAPI(res.locals.video.id, undefined, userId)
400 412
401 if (videoInstance.isOutdated()) { 413 if (video.isOutdated()) {
402 JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', url: videoInstance.url } }) 414 JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', url: video.url } })
403 .catch(err => logger.error('Cannot create AP refresher job for video %s.', videoInstance.url, { err })) 415 .catch(err => logger.error('Cannot create AP refresher job for video %s.', video.url, { err }))
404 } 416 }
405 417
406 return res.json(videoInstance.toFormattedDetailsJSON()) 418 return res.json(video.toFormattedDetailsJSON())
407} 419}
408 420
409async function viewVideo (req: express.Request, res: express.Response) { 421async function viewVideo (req: express.Request, res: express.Response) {
410 const videoInstance = res.locals.video 422 const videoInstance = res.locals.video
411 423
412 const ip = req.ip 424 const ip = req.ip
413 const exists = await Redis.Instance.isVideoIPViewExists(ip, videoInstance.uuid) 425 const exists = await Redis.Instance.doesVideoIPViewExist(ip, videoInstance.uuid)
414 if (exists) { 426 if (exists) {
415 logger.debug('View for ip %s and video %s already exists.', ip, videoInstance.uuid) 427 logger.debug('View for ip %s and video %s already exists.', ip, videoInstance.uuid)
416 return res.status(204).end() 428 return res.status(204).end()
@@ -422,7 +434,7 @@ async function viewVideo (req: express.Request, res: express.Response) {
422 ]) 434 ])
423 435
424 const serverActor = await getServerActor() 436 const serverActor = await getServerActor()
425 await sendCreateView(serverActor, videoInstance, undefined) 437 await sendView(serverActor, videoInstance, undefined)
426 438
427 return res.status(204).end() 439 return res.status(204).end()
428} 440}
@@ -461,7 +473,7 @@ async function listVideos (req: express.Request, res: express.Response) {
461} 473}
462 474
463async function removeVideo (req: express.Request, res: express.Response) { 475async function removeVideo (req: express.Request, res: express.Response) {
464 const videoInstance: VideoModel = res.locals.video 476 const videoInstance = res.locals.video
465 477
466 await sequelizeTypescript.transaction(async t => { 478 await sequelizeTypescript.transaction(async t => {
467 await videoInstance.destroy({ transaction: t }) 479 await videoInstance.destroy({ transaction: t })
diff --git a/server/controllers/api/videos/ownership.ts b/server/controllers/api/videos/ownership.ts
index 5ea7d7c6a..5272c1385 100644
--- a/server/controllers/api/videos/ownership.ts
+++ b/server/controllers/api/videos/ownership.ts
@@ -11,15 +11,13 @@ import {
11 videosChangeOwnershipValidator, 11 videosChangeOwnershipValidator,
12 videosTerminateChangeOwnershipValidator 12 videosTerminateChangeOwnershipValidator
13} from '../../../middlewares' 13} from '../../../middlewares'
14import { AccountModel } from '../../../models/account/account'
15import { VideoModel } from '../../../models/video/video'
16import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ownership' 14import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ownership'
17import { VideoChangeOwnershipStatus, VideoPrivacy, VideoState } from '../../../../shared/models/videos' 15import { VideoChangeOwnershipStatus, VideoPrivacy, VideoState } from '../../../../shared/models/videos'
18import { VideoChannelModel } from '../../../models/video/video-channel' 16import { VideoChannelModel } from '../../../models/video/video-channel'
19import { getFormattedObjects } from '../../../helpers/utils' 17import { getFormattedObjects } from '../../../helpers/utils'
20import { changeVideoChannelShare } from '../../../lib/activitypub' 18import { changeVideoChannelShare } from '../../../lib/activitypub'
21import { sendUpdateVideo } from '../../../lib/activitypub/send' 19import { sendUpdateVideo } from '../../../lib/activitypub/send'
22import { UserModel } from '../../../models/account/user' 20import { VideoModel } from '../../../models/video/video'
23 21
24const ownershipVideoRouter = express.Router() 22const ownershipVideoRouter = express.Router()
25 23
@@ -58,9 +56,9 @@ export {
58// --------------------------------------------------------------------------- 56// ---------------------------------------------------------------------------
59 57
60async function giveVideoOwnership (req: express.Request, res: express.Response) { 58async function giveVideoOwnership (req: express.Request, res: express.Response) {
61 const videoInstance = res.locals.video as VideoModel 59 const videoInstance = res.locals.video
62 const initiatorAccountId = (res.locals.oauth.token.User as UserModel).Account.id 60 const initiatorAccountId = res.locals.oauth.token.User.Account.id
63 const nextOwner = res.locals.nextOwner as AccountModel 61 const nextOwner = res.locals.nextOwner
64 62
65 await sequelizeTypescript.transaction(t => { 63 await sequelizeTypescript.transaction(t => {
66 return VideoChangeOwnershipModel.findOrCreate({ 64 return VideoChangeOwnershipModel.findOrCreate({
@@ -85,7 +83,7 @@ async function giveVideoOwnership (req: express.Request, res: express.Response)
85} 83}
86 84
87async function listVideoOwnership (req: express.Request, res: express.Response) { 85async function listVideoOwnership (req: express.Request, res: express.Response) {
88 const currentAccountId = (res.locals.oauth.token.User as UserModel).Account.id 86 const currentAccountId = res.locals.oauth.token.User.Account.id
89 87
90 const resultList = await VideoChangeOwnershipModel.listForApi( 88 const resultList = await VideoChangeOwnershipModel.listForApi(
91 currentAccountId, 89 currentAccountId,
@@ -99,13 +97,16 @@ async function listVideoOwnership (req: express.Request, res: express.Response)
99 97
100async function acceptOwnership (req: express.Request, res: express.Response) { 98async function acceptOwnership (req: express.Request, res: express.Response) {
101 return sequelizeTypescript.transaction(async t => { 99 return sequelizeTypescript.transaction(async t => {
102 const videoChangeOwnership = res.locals.videoChangeOwnership as VideoChangeOwnershipModel 100 const videoChangeOwnership = res.locals.videoChangeOwnership
103 const targetVideo = videoChangeOwnership.Video 101 const channel = res.locals.videoChannel
104 const channel = res.locals.videoChannel as VideoChannelModel 102
103 // We need more attributes for federation
104 const targetVideo = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoChangeOwnership.Video.id)
105 105
106 const oldVideoChannel = await VideoChannelModel.loadByIdAndPopulateAccount(targetVideo.channelId) 106 const oldVideoChannel = await VideoChannelModel.loadByIdAndPopulateAccount(targetVideo.channelId)
107 107
108 targetVideo.set('channelId', channel.id) 108 targetVideo.channelId = channel.id
109
109 const targetVideoUpdated = await targetVideo.save({ transaction: t }) 110 const targetVideoUpdated = await targetVideo.save({ transaction: t })
110 targetVideoUpdated.VideoChannel = channel 111 targetVideoUpdated.VideoChannel = channel
111 112
@@ -114,7 +115,7 @@ async function acceptOwnership (req: express.Request, res: express.Response) {
114 await sendUpdateVideo(targetVideoUpdated, t, oldVideoChannel.Account.Actor) 115 await sendUpdateVideo(targetVideoUpdated, t, oldVideoChannel.Account.Actor)
115 } 116 }
116 117
117 videoChangeOwnership.set('status', VideoChangeOwnershipStatus.ACCEPTED) 118 videoChangeOwnership.status = VideoChangeOwnershipStatus.ACCEPTED
118 await videoChangeOwnership.save({ transaction: t }) 119 await videoChangeOwnership.save({ transaction: t })
119 120
120 return res.sendStatus(204) 121 return res.sendStatus(204)
@@ -123,9 +124,9 @@ async function acceptOwnership (req: express.Request, res: express.Response) {
123 124
124async function refuseOwnership (req: express.Request, res: express.Response) { 125async function refuseOwnership (req: express.Request, res: express.Response) {
125 return sequelizeTypescript.transaction(async t => { 126 return sequelizeTypescript.transaction(async t => {
126 const videoChangeOwnership = res.locals.videoChangeOwnership as VideoChangeOwnershipModel 127 const videoChangeOwnership = res.locals.videoChangeOwnership
127 128
128 videoChangeOwnership.set('status', VideoChangeOwnershipStatus.REFUSED) 129 videoChangeOwnership.status = VideoChangeOwnershipStatus.REFUSED
129 await videoChangeOwnership.save({ transaction: t }) 130 await videoChangeOwnership.save({ transaction: t })
130 131
131 return res.sendStatus(204) 132 return res.sendStatus(204)
diff --git a/server/controllers/api/videos/rate.ts b/server/controllers/api/videos/rate.ts
index 53952a0a2..b65babedf 100644
--- a/server/controllers/api/videos/rate.ts
+++ b/server/controllers/api/videos/rate.ts
@@ -1,12 +1,12 @@
1import * as express from 'express' 1import * as express from 'express'
2import { UserVideoRateUpdate } from '../../../../shared' 2import { UserVideoRateUpdate } from '../../../../shared'
3import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
4import { sequelizeTypescript, VIDEO_RATE_TYPES } from '../../../initializers' 4import { VIDEO_RATE_TYPES } from '../../../initializers/constants'
5import { getRateUrl, sendVideoRateChange } from '../../../lib/activitypub' 5import { getRateUrl, sendVideoRateChange } from '../../../lib/activitypub'
6import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoUpdateRateValidator } from '../../../middlewares' 6import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoUpdateRateValidator } from '../../../middlewares'
7import { AccountModel } from '../../../models/account/account' 7import { AccountModel } from '../../../models/account/account'
8import { AccountVideoRateModel } from '../../../models/account/account-video-rate' 8import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
9import { VideoModel } from '../../../models/video/video' 9import { sequelizeTypescript } from '../../../initializers/database'
10 10
11const rateVideoRouter = express.Router() 11const rateVideoRouter = express.Router()
12 12
@@ -27,8 +27,8 @@ export {
27async function rateVideo (req: express.Request, res: express.Response) { 27async function rateVideo (req: express.Request, res: express.Response) {
28 const body: UserVideoRateUpdate = req.body 28 const body: UserVideoRateUpdate = req.body
29 const rateType = body.rating 29 const rateType = body.rating
30 const videoInstance: VideoModel = res.locals.video 30 const videoInstance = res.locals.video
31 const userAccount: AccountModel = res.locals.oauth.token.User.Account 31 const userAccount = res.locals.oauth.token.User.Account
32 32
33 await sequelizeTypescript.transaction(async t => { 33 await sequelizeTypescript.transaction(async t => {
34 const sequelizeOptions = { transaction: t } 34 const sequelizeOptions = { transaction: t }
diff --git a/server/controllers/api/videos/watching.ts b/server/controllers/api/videos/watching.ts
index e8876b47a..dcd1f070d 100644
--- a/server/controllers/api/videos/watching.ts
+++ b/server/controllers/api/videos/watching.ts
@@ -2,7 +2,6 @@ import * as express from 'express'
2import { UserWatchingVideo } from '../../../../shared' 2import { UserWatchingVideo } from '../../../../shared'
3import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoWatchingValidator } from '../../../middlewares' 3import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoWatchingValidator } from '../../../middlewares'
4import { UserVideoHistoryModel } from '../../../models/account/user-video-history' 4import { UserVideoHistoryModel } from '../../../models/account/user-video-history'
5import { UserModel } from '../../../models/account/user'
6 5
7const watchingRouter = express.Router() 6const watchingRouter = express.Router()
8 7
@@ -21,7 +20,7 @@ export {
21// --------------------------------------------------------------------------- 20// ---------------------------------------------------------------------------
22 21
23async function userWatchVideo (req: express.Request, res: express.Response) { 22async function userWatchVideo (req: express.Request, res: express.Response) {
24 const user = res.locals.oauth.token.User as UserModel 23 const user = res.locals.oauth.token.User
25 24
26 const body: UserWatchingVideo = req.body 25 const body: UserWatchingVideo = req.body
27 const { id: videoId } = res.locals.video as { id: number } 26 const { id: videoId } = res.locals.video as { id: number }
diff --git a/server/controllers/bots.ts b/server/controllers/bots.ts
index 2db86a2d8..e25d9c21b 100644
--- a/server/controllers/bots.ts
+++ b/server/controllers/bots.ts
@@ -1,6 +1,6 @@
1import * as express from 'express' 1import * as express from 'express'
2import { asyncMiddleware } from '../middlewares' 2import { asyncMiddleware } from '../middlewares'
3import { CONFIG, ROUTE_CACHE_LIFETIME } from '../initializers' 3import { ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants'
4import * as sitemapModule from 'sitemap' 4import * as sitemapModule from 'sitemap'
5import { logger } from '../helpers/logger' 5import { logger } from '../helpers/logger'
6import { VideoModel } from '../models/video/video' 6import { VideoModel } from '../models/video/video'
@@ -35,7 +35,7 @@ async function getSitemap (req: express.Request, res: express.Response) {
35 urls = urls.concat(await getSitemapAccountUrls()) 35 urls = urls.concat(await getSitemapAccountUrls())
36 36
37 const sitemap = sitemapModule.createSitemap({ 37 const sitemap = sitemapModule.createSitemap({
38 hostname: CONFIG.WEBSERVER.URL, 38 hostname: WEBSERVER.URL,
39 urls: urls 39 urls: urls
40 }) 40 })
41 41
@@ -54,7 +54,7 @@ async function getSitemapVideoChannelUrls () {
54 const rows = await VideoChannelModel.listLocalsForSitemap('createdAt') 54 const rows = await VideoChannelModel.listLocalsForSitemap('createdAt')
55 55
56 return rows.map(channel => ({ 56 return rows.map(channel => ({
57 url: CONFIG.WEBSERVER.URL + '/video-channels/' + channel.Actor.preferredUsername 57 url: WEBSERVER.URL + '/video-channels/' + channel.Actor.preferredUsername
58 })) 58 }))
59} 59}
60 60
@@ -62,7 +62,7 @@ async function getSitemapAccountUrls () {
62 const rows = await AccountModel.listLocalsForSitemap('createdAt') 62 const rows = await AccountModel.listLocalsForSitemap('createdAt')
63 63
64 return rows.map(channel => ({ 64 return rows.map(channel => ({
65 url: CONFIG.WEBSERVER.URL + '/accounts/' + channel.Actor.preferredUsername 65 url: WEBSERVER.URL + '/accounts/' + channel.Actor.preferredUsername
66 })) 66 }))
67} 67}
68 68
@@ -78,14 +78,14 @@ async function getSitemapLocalVideoUrls () {
78 }) 78 })
79 79
80 return resultList.data.map(v => ({ 80 return resultList.data.map(v => ({
81 url: CONFIG.WEBSERVER.URL + '/videos/watch/' + v.uuid, 81 url: WEBSERVER.URL + '/videos/watch/' + v.uuid,
82 video: [ 82 video: [
83 { 83 {
84 title: v.name, 84 title: v.name,
85 // Sitemap description should be < 2000 characters 85 // Sitemap description should be < 2000 characters
86 description: truncate(v.description || v.name, { length: 2000, omission: '...' }), 86 description: truncate(v.description || v.name, { length: 2000, omission: '...' }),
87 player_loc: CONFIG.WEBSERVER.URL + '/videos/embed/' + v.uuid, 87 player_loc: WEBSERVER.URL + '/videos/embed/' + v.uuid,
88 thumbnail_loc: CONFIG.WEBSERVER.URL + v.getThumbnailStaticPath() 88 thumbnail_loc: WEBSERVER.URL + v.getMiniatureStaticPath()
89 } 89 }
90 ] 90 ]
91 })) 91 }))
@@ -97,5 +97,5 @@ function getSitemapBasicUrls () {
97 '/videos/local' 97 '/videos/local'
98 ] 98 ]
99 99
100 return paths.map(p => ({ url: CONFIG.WEBSERVER.URL + p })) 100 return paths.map(p => ({ url: WEBSERVER.URL + p }))
101} 101}
diff --git a/server/controllers/client.ts b/server/controllers/client.ts
index f17f2a5d2..f51470b41 100644
--- a/server/controllers/client.ts
+++ b/server/controllers/client.ts
@@ -1,7 +1,7 @@
1import * as express from 'express' 1import * 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/constants'
5import { asyncMiddleware, embedCSP } 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'
@@ -17,6 +17,8 @@ const testEmbedPath = join(distPath, 'standalone', 'videos', 'test-embed.html')
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', asyncMiddleware(generateWatchHtmlPage)) 19clientsRouter.use('/videos/watch/:id', asyncMiddleware(generateWatchHtmlPage))
20clientsRouter.use('/accounts/:nameWithHost', asyncMiddleware(generateAccountHtmlPage))
21clientsRouter.use('/video-channels/:nameWithHost', asyncMiddleware(generateVideoChannelHtmlPage))
20 22
21clientsRouter.use( 23clientsRouter.use(
22 '/videos/embed', 24 '/videos/embed',
@@ -99,6 +101,18 @@ async function generateWatchHtmlPage (req: express.Request, res: express.Respons
99 return sendHTML(html, res) 101 return sendHTML(html, res)
100} 102}
101 103
104async function generateAccountHtmlPage (req: express.Request, res: express.Response) {
105 const html = await ClientHtml.getAccountHTMLPage(req.params.nameWithHost, req, res)
106
107 return sendHTML(html, res)
108}
109
110async function generateVideoChannelHtmlPage (req: express.Request, res: express.Response) {
111 const html = await ClientHtml.getVideoChannelHTMLPage(req.params.nameWithHost, req, res)
112
113 return sendHTML(html, res)
114}
115
102function sendHTML (html: string, res: express.Response) { 116function sendHTML (html: string, res: express.Response) {
103 res.set('Content-Type', 'text/html; charset=UTF-8') 117 res.set('Content-Type', 'text/html; charset=UTF-8')
104 118
diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts
index 960085af1..d3f581615 100644
--- a/server/controllers/feeds.ts
+++ b/server/controllers/feeds.ts
@@ -1,6 +1,5 @@
1import * as express from 'express' 1import * as express from 'express'
2import { CONFIG, FEEDS, ROUTE_CACHE_LIFETIME } from '../initializers/constants' 2import { FEEDS, ROUTE_CACHE_LIFETIME, THUMBNAILS_SIZE, WEBSERVER } from '../initializers/constants'
3import { THUMBNAILS_SIZE } from '../initializers'
4import { 3import {
5 asyncMiddleware, 4 asyncMiddleware,
6 commonVideosFiltersValidator, 5 commonVideosFiltersValidator,
@@ -11,11 +10,10 @@ import {
11} from '../middlewares' 10} from '../middlewares'
12import { VideoModel } from '../models/video/video' 11import { VideoModel } from '../models/video/video'
13import * as Feed from 'pfeed' 12import * as Feed from 'pfeed'
14import { AccountModel } from '../models/account/account'
15import { cacheRoute } from '../middlewares/cache' 13import { cacheRoute } from '../middlewares/cache'
16import { VideoChannelModel } from '../models/video/video-channel'
17import { VideoCommentModel } from '../models/video/video-comment' 14import { VideoCommentModel } from '../models/video/video-comment'
18import { buildNSFWFilter } from '../helpers/express-utils' 15import { buildNSFWFilter } from '../helpers/express-utils'
16import { CONFIG } from '../initializers/config'
19 17
20const feedsRouter = express.Router() 18const feedsRouter = express.Router()
21 19
@@ -42,10 +40,10 @@ export {
42 40
43// --------------------------------------------------------------------------- 41// ---------------------------------------------------------------------------
44 42
45async function generateVideoCommentsFeed (req: express.Request, res: express.Response, next: express.NextFunction) { 43async function generateVideoCommentsFeed (req: express.Request, res: express.Response) {
46 const start = 0 44 const start = 0
47 45
48 const video = res.locals.video as VideoModel 46 const video = res.locals.video
49 const videoId: number = video ? video.id : undefined 47 const videoId: number = video ? video.id : undefined
50 48
51 const comments = await VideoCommentModel.listForFeed(start, FEEDS.COUNT, videoId) 49 const comments = await VideoCommentModel.listForFeed(start, FEEDS.COUNT, videoId)
@@ -56,7 +54,7 @@ async function generateVideoCommentsFeed (req: express.Request, res: express.Res
56 54
57 // Adding video items to the feed, one at a time 55 // Adding video items to the feed, one at a time
58 comments.forEach(comment => { 56 comments.forEach(comment => {
59 const link = CONFIG.WEBSERVER.URL + comment.getCommentStaticPath() 57 const link = WEBSERVER.URL + comment.getCommentStaticPath()
60 58
61 feed.addItem({ 59 feed.addItem({
62 title: `${comment.Video.name} - ${comment.Account.getDisplayName()}`, 60 title: `${comment.Video.name} - ${comment.Account.getDisplayName()}`,
@@ -77,11 +75,11 @@ async function generateVideoCommentsFeed (req: express.Request, res: express.Res
77 return sendFeed(feed, req, res) 75 return sendFeed(feed, req, res)
78} 76}
79 77
80async function generateVideoFeed (req: express.Request, res: express.Response, next: express.NextFunction) { 78async function generateVideoFeed (req: express.Request, res: express.Response) {
81 const start = 0 79 const start = 0
82 80
83 const account: AccountModel = res.locals.account 81 const account = res.locals.account
84 const videoChannel: VideoChannelModel = res.locals.videoChannel 82 const videoChannel = res.locals.videoChannel
85 const nsfw = buildNSFWFilter(res, req.query.nsfw) 83 const nsfw = buildNSFWFilter(res, req.query.nsfw)
86 84
87 let name: string 85 let name: string
@@ -124,7 +122,7 @@ async function generateVideoFeed (req: express.Request, res: express.Response, n
124 feed.addItem({ 122 feed.addItem({
125 title: video.name, 123 title: video.name,
126 id: video.url, 124 id: video.url,
127 link: CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid, 125 link: WEBSERVER.URL + '/videos/watch/' + video.uuid,
128 description: video.getTruncatedDescription(), 126 description: video.getTruncatedDescription(),
129 content: video.description, 127 content: video.description,
130 author: [ 128 author: [
@@ -139,7 +137,7 @@ async function generateVideoFeed (req: express.Request, res: express.Response, n
139 torrent: torrents, 137 torrent: torrents,
140 thumbnail: [ 138 thumbnail: [
141 { 139 {
142 url: CONFIG.WEBSERVER.URL + video.getThumbnailStaticPath(), 140 url: WEBSERVER.URL + video.getMiniatureStaticPath(),
143 height: THUMBNAILS_SIZE.height, 141 height: THUMBNAILS_SIZE.height,
144 width: THUMBNAILS_SIZE.width 142 width: THUMBNAILS_SIZE.width
145 } 143 }
@@ -152,7 +150,7 @@ async function generateVideoFeed (req: express.Request, res: express.Response, n
152} 150}
153 151
154function initFeed (name: string, description: string) { 152function initFeed (name: string, description: string) {
155 const webserverUrl = CONFIG.WEBSERVER.URL 153 const webserverUrl = WEBSERVER.URL
156 154
157 return new Feed({ 155 return new Feed({
158 title: name, 156 title: name,
diff --git a/server/controllers/services.ts b/server/controllers/services.ts
index 352d0b19a..c1c53c3fc 100644
--- a/server/controllers/services.ts
+++ b/server/controllers/services.ts
@@ -1,8 +1,7 @@
1import * as express from 'express' 1import * as express from 'express'
2import { CONFIG, EMBED_SIZE, PREVIEWS_SIZE } from '../initializers' 2import { EMBED_SIZE, PREVIEWS_SIZE, WEBSERVER } from '../initializers/constants'
3import { asyncMiddleware, oembedValidator } from '../middlewares' 3import { asyncMiddleware, oembedValidator } from '../middlewares'
4import { accountsNameWithHostGetValidator } from '../middlewares/validators' 4import { accountNameWithHostGetValidator } from '../middlewares/validators'
5import { VideoModel } from '../models/video/video'
6 5
7const servicesRouter = express.Router() 6const servicesRouter = express.Router()
8 7
@@ -11,7 +10,7 @@ servicesRouter.use('/oembed',
11 generateOEmbed 10 generateOEmbed
12) 11)
13servicesRouter.use('/redirect/accounts/:accountName', 12servicesRouter.use('/redirect/accounts/:accountName',
14 asyncMiddleware(accountsNameWithHostGetValidator), 13 asyncMiddleware(accountNameWithHostGetValidator),
15 redirectToAccountUrl 14 redirectToAccountUrl
16) 15)
17 16
@@ -23,9 +22,9 @@ export {
23 22
24// --------------------------------------------------------------------------- 23// ---------------------------------------------------------------------------
25 24
26function generateOEmbed (req: express.Request, res: express.Response, next: express.NextFunction) { 25function generateOEmbed (req: express.Request, res: express.Response) {
27 const video = res.locals.video as VideoModel 26 const video = res.locals.video
28 const webserverUrl = CONFIG.WEBSERVER.URL 27 const webserverUrl = WEBSERVER.URL
29 const maxHeight = parseInt(req.query.maxheight, 10) 28 const maxHeight = parseInt(req.query.maxheight, 10)
30 const maxWidth = parseInt(req.query.maxwidth, 10) 29 const maxWidth = parseInt(req.query.maxwidth, 10)
31 30
diff --git a/server/controllers/static.ts b/server/controllers/static.ts
index 4fd58f70c..05019fcc2 100644
--- a/server/controllers/static.ts
+++ b/server/controllers/static.ts
@@ -1,16 +1,23 @@
1import * as cors from 'cors' 1import * as cors from 'cors'
2import * as express from 'express' 2import * as express from 'express'
3import { CONFIG, ROUTE_CACHE_LIFETIME, STATIC_DOWNLOAD_PATHS, STATIC_MAX_AGE, STATIC_PATHS } from '../initializers' 3import {
4import { VideosPreviewCache } from '../lib/cache' 4 HLS_STREAMING_PLAYLIST_DIRECTORY,
5 ROUTE_CACHE_LIFETIME,
6 STATIC_DOWNLOAD_PATHS,
7 STATIC_MAX_AGE,
8 STATIC_PATHS,
9 WEBSERVER
10} from '../initializers/constants'
11import { VideosCaptionCache, VideosPreviewCache } from '../lib/files-cache'
5import { cacheRoute } from '../middlewares/cache' 12import { cacheRoute } from '../middlewares/cache'
6import { asyncMiddleware, videosGetValidator } from '../middlewares' 13import { asyncMiddleware, videosGetValidator } from '../middlewares'
7import { VideoModel } from '../models/video/video' 14import { VideoModel } from '../models/video/video'
8import { VideosCaptionCache } from '../lib/cache/videos-caption-cache'
9import { UserModel } from '../models/account/user' 15import { UserModel } from '../models/account/user'
10import { VideoCommentModel } from '../models/video/video-comment' 16import { VideoCommentModel } from '../models/video/video-comment'
11import { HttpNodeinfoDiasporaSoftwareNsSchema20 } from '../../shared/models/nodeinfo' 17import { HttpNodeinfoDiasporaSoftwareNsSchema20 } from '../../shared/models/nodeinfo'
12import { join } from 'path' 18import { join } from 'path'
13import { root } from '../helpers/core-utils' 19import { root } from '../helpers/core-utils'
20import { CONFIG } from '../initializers/config'
14 21
15const packageJSON = require('../../../package.json') 22const packageJSON = require('../../../package.json')
16const staticRouter = express.Router() 23const staticRouter = express.Router()
@@ -51,6 +58,13 @@ staticRouter.use(
51 asyncMiddleware(downloadVideoFile) 58 asyncMiddleware(downloadVideoFile)
52) 59)
53 60
61// HLS
62staticRouter.use(
63 STATIC_PATHS.STREAMING_PLAYLISTS.HLS,
64 cors(),
65 express.static(HLS_STREAMING_PLAYLIST_DIRECTORY, { fallthrough: false }) // 404 if the file does not exist
66)
67
54// Thumbnails path for express 68// Thumbnails path for express
55const thumbnailsPhysicalPath = CONFIG.STORAGE.THUMBNAILS_DIR 69const thumbnailsPhysicalPath = CONFIG.STORAGE.THUMBNAILS_DIR
56staticRouter.use( 70staticRouter.use(
@@ -108,7 +122,7 @@ staticRouter.use('/.well-known/nodeinfo',
108 links: [ 122 links: [
109 { 123 {
110 rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0', 124 rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0',
111 href: CONFIG.WEBSERVER.URL + '/nodeinfo/2.0.json' 125 href: WEBSERVER.URL + '/nodeinfo/2.0.json'
112 } 126 }
113 ] 127 ]
114 }) 128 })
@@ -150,21 +164,21 @@ export {
150 164
151// --------------------------------------------------------------------------- 165// ---------------------------------------------------------------------------
152 166
153async function getPreview (req: express.Request, res: express.Response, next: express.NextFunction) { 167async function getPreview (req: express.Request, res: express.Response) {
154 const path = await VideosPreviewCache.Instance.getFilePath(req.params.uuid) 168 const result = await VideosPreviewCache.Instance.getFilePath(req.params.uuid)
155 if (!path) return res.sendStatus(404) 169 if (!result) return res.sendStatus(404)
156 170
157 return res.sendFile(path, { maxAge: STATIC_MAX_AGE }) 171 return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE })
158} 172}
159 173
160async function getVideoCaption (req: express.Request, res: express.Response) { 174async function getVideoCaption (req: express.Request, res: express.Response) {
161 const path = await VideosCaptionCache.Instance.getFilePath({ 175 const result = await VideosCaptionCache.Instance.getFilePath({
162 videoId: req.params.videoId, 176 videoId: req.params.videoId,
163 language: req.params.captionLanguage 177 language: req.params.captionLanguage
164 }) 178 })
165 if (!path) return res.sendStatus(404) 179 if (!result) return res.sendStatus(404)
166 180
167 return res.sendFile(path, { maxAge: STATIC_MAX_AGE }) 181 return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE })
168} 182}
169 183
170async function generateNodeinfo (req: express.Request, res: express.Response, next: express.NextFunction) { 184async function generateNodeinfo (req: express.Request, res: express.Response, next: express.NextFunction) {
@@ -231,7 +245,7 @@ async function downloadVideoFile (req: express.Request, res: express.Response, n
231 245
232function getVideoAndFile (req: express.Request, res: express.Response) { 246function getVideoAndFile (req: express.Request, res: express.Response) {
233 const resolution = parseInt(req.params.resolution, 10) 247 const resolution = parseInt(req.params.resolution, 10)
234 const video: VideoModel = res.locals.video 248 const video = res.locals.video
235 249
236 const videoFile = video.VideoFiles.find(f => f.resolution === resolution) 250 const videoFile = video.VideoFiles.find(f => f.resolution === resolution)
237 251
diff --git a/server/controllers/tracker.ts b/server/controllers/tracker.ts
index 1deb8c402..912f82b86 100644
--- a/server/controllers/tracker.ts
+++ b/server/controllers/tracker.ts
@@ -4,9 +4,11 @@ import * as http from 'http'
4import * as bitTorrentTracker from 'bittorrent-tracker' 4import * as bitTorrentTracker from 'bittorrent-tracker'
5import * as proxyAddr from 'proxy-addr' 5import * 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 { TRACKER_RATE_LIMITS } from '../initializers/constants'
8import { VideoFileModel } from '../models/video/video-file' 8import { VideoFileModel } from '../models/video/video-file'
9import { parse } from 'url' 9import { parse } from 'url'
10import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
11import { CONFIG } from '../initializers/config'
10 12
11const TrackerServer = bitTorrentTracker.Server 13const TrackerServer = bitTorrentTracker.Server
12 14
@@ -21,7 +23,11 @@ const trackerServer = new TrackerServer({
21 udp: false, 23 udp: false,
22 ws: false, 24 ws: false,
23 dht: false, 25 dht: false,
24 filter: function (infoHash, params, cb) { 26 filter: async function (infoHash, params, cb) {
27 if (CONFIG.TRACKER.ENABLED === false) {
28 return cb(new Error('Tracker is disabled on this instance.'))
29 }
30
25 let ip: string 31 let ip: string
26 32
27 if (params.type === 'ws') { 33 if (params.type === 'ws') {
@@ -32,29 +38,40 @@ const trackerServer = new TrackerServer({
32 38
33 const key = ip + '-' + infoHash 39 const key = ip + '-' + infoHash
34 40
35 peersIps[ip] = peersIps[ip] ? peersIps[ip] + 1 : 1 41 peersIps[ ip ] = peersIps[ ip ] ? peersIps[ ip ] + 1 : 1
36 peersIpInfoHash[key] = peersIpInfoHash[key] ? peersIpInfoHash[key] + 1 : 1 42 peersIpInfoHash[ key ] = peersIpInfoHash[ key ] ? peersIpInfoHash[ key ] + 1 : 1
37 43
38 if (peersIpInfoHash[key] > TRACKER_RATE_LIMITS.ANNOUNCES_PER_IP_PER_INFOHASH) { 44 if (CONFIG.TRACKER.REJECT_TOO_MANY_ANNOUNCES && peersIpInfoHash[ key ] > TRACKER_RATE_LIMITS.ANNOUNCES_PER_IP_PER_INFOHASH) {
39 return cb(new Error(`Too many requests (${peersIpInfoHash[ key ]} of ip ${ip} for torrent ${infoHash}`)) 45 return cb(new Error(`Too many requests (${peersIpInfoHash[ key ]} of ip ${ip} for torrent ${infoHash}`))
40 } 46 }
41 47
42 VideoFileModel.isInfohashExists(infoHash) 48 try {
43 .then(exists => { 49 if (CONFIG.TRACKER.PRIVATE === false) return cb()
44 if (exists === false) return cb(new Error(`Unknown infoHash ${infoHash}`)) 50
51 const videoFileExists = await VideoFileModel.doesInfohashExist(infoHash)
52 if (videoFileExists === true) return cb()
45 53
46 return cb() 54 const playlistExists = await VideoStreamingPlaylistModel.doesInfohashExist(infoHash)
47 }) 55 if (playlistExists === true) return cb()
56
57 return cb(new Error(`Unknown infoHash ${infoHash}`))
58 } catch (err) {
59 logger.error('Error in tracker filter.', { err })
60 return cb(err)
61 }
48 } 62 }
49}) 63})
50 64
51trackerServer.on('error', function (err) { 65if (CONFIG.TRACKER.ENABLED !== false) {
52 logger.error('Error in tracker.', { err })
53})
54 66
55trackerServer.on('warning', function (err) { 67 trackerServer.on('error', function (err) {
56 logger.warn('Warning in tracker.', { err }) 68 logger.error('Error in tracker.', { err })
57}) 69 })
70
71 trackerServer.on('warning', function (err) {
72 logger.warn('Warning in tracker.', { err })
73 })
74}
58 75
59const onHttpRequest = trackerServer.onHttpRequest.bind(trackerServer) 76const onHttpRequest = trackerServer.onHttpRequest.bind(trackerServer)
60trackerRouter.get('/tracker/announce', (req, res) => onHttpRequest(req, res, { action: 'announce' })) 77trackerRouter.get('/tracker/announce', (req, res) => onHttpRequest(req, res, { action: 'announce' }))
diff --git a/server/controllers/webfinger.ts b/server/controllers/webfinger.ts
index ed781c21b..f2ba3c826 100644
--- a/server/controllers/webfinger.ts
+++ b/server/controllers/webfinger.ts
@@ -1,7 +1,6 @@
1import * as express from 'express' 1import * as express from 'express'
2import { asyncMiddleware } from '../middlewares' 2import { asyncMiddleware } from '../middlewares'
3import { webfingerValidator } from '../middlewares/validators' 3import { webfingerValidator } from '../middlewares/validators'
4import { ActorModel } from '../models/activitypub/actor'
5 4
6const webfingerRouter = express.Router() 5const webfingerRouter = express.Router()
7 6
@@ -18,8 +17,8 @@ export {
18 17
19// --------------------------------------------------------------------------- 18// ---------------------------------------------------------------------------
20 19
21function webfingerController (req: express.Request, res: express.Response, next: express.NextFunction) { 20function webfingerController (req: express.Request, res: express.Response) {
22 const actor = res.locals.actor as ActorModel 21 const actor = res.locals.actor
23 22
24 const json = { 23 const json = {
25 subject: req.query.resource, 24 subject: req.query.resource,
diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts
index f1430055f..951a25669 100644
--- a/server/helpers/activitypub.ts
+++ b/server/helpers/activitypub.ts
@@ -2,7 +2,7 @@ import * as Bluebird from 'bluebird'
2import * as validator from 'validator' 2import * as validator from 'validator'
3import { ResultList } from '../../shared/models' 3import { ResultList } from '../../shared/models'
4import { Activity } from '../../shared/models/activitypub' 4import { Activity } from '../../shared/models/activitypub'
5import { ACTIVITY_PUB } from '../initializers' 5import { ACTIVITY_PUB } from '../initializers/constants'
6import { ActorModel } from '../models/activitypub/actor' 6import { ActorModel } from '../models/activitypub/actor'
7import { signJsonLDObject } from './peertube-crypto' 7import { signJsonLDObject } from './peertube-crypto'
8import { pageToStartAndCount } from './core-utils' 8import { pageToStartAndCount } from './core-utils'
@@ -15,7 +15,7 @@ function activityPubContextify <T> (data: T) {
15 'https://w3id.org/security/v1', 15 'https://w3id.org/security/v1',
16 { 16 {
17 RsaSignature2017: 'https://w3id.org/security#RsaSignature2017', 17 RsaSignature2017: 'https://w3id.org/security#RsaSignature2017',
18 pt: 'https://joinpeertube.org/ns', 18 pt: 'https://joinpeertube.org/ns#',
19 sc: 'http://schema.org#', 19 sc: 'http://schema.org#',
20 Hashtag: 'as:Hashtag', 20 Hashtag: 'as:Hashtag',
21 uuid: 'sc:identifier', 21 uuid: 'sc:identifier',
@@ -24,15 +24,54 @@ function activityPubContextify <T> (data: T) {
24 subtitleLanguage: 'sc:subtitleLanguage', 24 subtitleLanguage: 'sc:subtitleLanguage',
25 sensitive: 'as:sensitive', 25 sensitive: 'as:sensitive',
26 language: 'sc:inLanguage', 26 language: 'sc:inLanguage',
27 views: 'sc:Number',
28 state: 'sc:Number',
29 size: 'sc:Number',
30 fps: 'sc:Number',
31 commentsEnabled: 'sc:Boolean',
32 waitTranscoding: 'sc:Boolean',
33 expires: 'sc:expires', 27 expires: 'sc:expires',
34 support: 'sc:Text', 28 CacheFile: 'pt:CacheFile',
35 CacheFile: 'pt:CacheFile' 29 Infohash: 'pt:Infohash',
30 originallyPublishedAt: 'sc:datePublished',
31 views: {
32 '@type': 'sc:Number',
33 '@id': 'pt:views'
34 },
35 state: {
36 '@type': 'sc:Number',
37 '@id': 'pt:state'
38 },
39 size: {
40 '@type': 'sc:Number',
41 '@id': 'pt:size'
42 },
43 fps: {
44 '@type': 'sc:Number',
45 '@id': 'pt:fps'
46 },
47 startTimestamp: {
48 '@type': 'sc:Number',
49 '@id': 'pt:startTimestamp'
50 },
51 stopTimestamp: {
52 '@type': 'sc:Number',
53 '@id': 'pt:stopTimestamp'
54 },
55 position: {
56 '@type': 'sc:Number',
57 '@id': 'pt:position'
58 },
59 commentsEnabled: {
60 '@type': 'sc:Boolean',
61 '@id': 'pt:commentsEnabled'
62 },
63 downloadEnabled: {
64 '@type': 'sc:Boolean',
65 '@id': 'pt:downloadEnabled'
66 },
67 waitTranscoding: {
68 '@type': 'sc:Boolean',
69 '@id': 'pt:waitTranscoding'
70 },
71 support: {
72 '@type': 'sc:Text',
73 '@id': 'pt:support'
74 }
36 }, 75 },
37 { 76 {
38 likes: { 77 likes: {
@@ -43,6 +82,10 @@ function activityPubContextify <T> (data: T) {
43 '@id': 'as:dislikes', 82 '@id': 'as:dislikes',
44 '@type': '@id' 83 '@type': '@id'
45 }, 84 },
85 playlists: {
86 '@id': 'pt:playlists',
87 '@type': '@id'
88 },
46 shares: { 89 shares: {
47 '@id': 'as:shares', 90 '@id': 'as:shares',
48 '@type': '@id' 91 '@type': '@id'
@@ -64,7 +107,7 @@ async function activityPubCollectionPagination (baseUrl: string, handler: Activi
64 107
65 return { 108 return {
66 id: baseUrl, 109 id: baseUrl,
67 type: 'OrderedCollection', 110 type: 'OrderedCollectionPage',
68 totalItems: result.total, 111 totalItems: result.total,
69 first: baseUrl + '?page=1' 112 first: baseUrl + '?page=1'
70 } 113 }
diff --git a/server/helpers/audit-logger.ts b/server/helpers/audit-logger.ts
index 00311fce1..f536da439 100644
--- a/server/helpers/audit-logger.ts
+++ b/server/helpers/audit-logger.ts
@@ -4,15 +4,14 @@ import { diff } from 'deep-object-diff'
4import { chain } from 'lodash' 4import { chain } from 'lodash'
5import * as flatten from 'flat' 5import * as flatten from 'flat'
6import * as winston from 'winston' 6import * as winston from 'winston'
7import { CONFIG } from '../initializers'
8import { jsonLoggerFormat, labelFormatter } from './logger' 7import { jsonLoggerFormat, labelFormatter } from './logger'
9import { VideoDetails, User, VideoChannel, VideoAbuse, VideoImport } from '../../shared' 8import { User, VideoAbuse, VideoChannel, VideoDetails, VideoImport } from '../../shared'
10import { VideoComment } from '../../shared/models/videos/video-comment.model' 9import { VideoComment } from '../../shared/models/videos/video-comment.model'
11import { CustomConfig } from '../../shared/models/server/custom-config.model' 10import { CustomConfig } from '../../shared/models/server/custom-config.model'
12import { UserModel } from '../models/account/user' 11import { CONFIG } from '../initializers/config'
13 12
14function getAuditIdFromRes (res: express.Response) { 13function getAuditIdFromRes (res: express.Response) {
15 return (res.locals.oauth.token.User as UserModel).username 14 return res.locals.oauth.token.User.username
16} 15}
17 16
18enum AUDIT_TYPE { 17enum AUDIT_TYPE {
@@ -117,7 +116,8 @@ const videoKeysToKeep = [
117 'channel-uuid', 116 'channel-uuid',
118 'channel-name', 117 'channel-name',
119 'support', 118 'support',
120 'commentsEnabled' 119 'commentsEnabled',
120 'downloadEnabled'
121] 121]
122class VideoAuditView extends EntityAuditView { 122class VideoAuditView extends EntityAuditView {
123 constructor (private video: VideoDetails) { 123 constructor (private video: VideoDetails) {
diff --git a/server/helpers/captions-utils.ts b/server/helpers/captions-utils.ts
index 0fb11a125..7174d4654 100644
--- a/server/helpers/captions-utils.ts
+++ b/server/helpers/captions-utils.ts
@@ -1,5 +1,5 @@
1import { join } from 'path' 1import { join } from 'path'
2import { CONFIG } from '../initializers' 2import { CONFIG } from '../initializers/config'
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, move } from 'fs-extra' 5import { createReadStream, createWriteStream, remove, move } from 'fs-extra'
diff --git a/server/helpers/core-utils.ts b/server/helpers/core-utils.ts
index 3fb824e36..305d3b71e 100644
--- a/server/helpers/core-utils.ts
+++ b/server/helpers/core-utils.ts
@@ -11,14 +11,13 @@ 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 14
16const objectConverter = (oldObject: any, keyConverter: (e: string) => string, valueConverter: (e: any) => any) => { 15const objectConverter = (oldObject: any, keyConverter: (e: string) => string, valueConverter: (e: any) => any) => {
17 if (!oldObject || typeof oldObject !== 'object') { 16 if (!oldObject || typeof oldObject !== 'object') {
18 return valueConverter(oldObject) 17 return valueConverter(oldObject)
19 } 18 }
20 19
21 if (isArray(oldObject)) { 20 if (Array.isArray(oldObject)) {
22 return oldObject.map(e => objectConverter(e, keyConverter, valueConverter)) 21 return oldObject.map(e => objectConverter(e, keyConverter, valueConverter))
23 } 22 }
24 23
@@ -41,7 +40,7 @@ const timeTable = {
41 month: 3600000 * 24 * 30 40 month: 3600000 * 24 * 30
42} 41}
43 42
44export function parseDuration (duration: number | string): number { 43export function parseDurationToMs (duration: number | string): number {
45 if (typeof duration === 'number') return duration 44 if (typeof duration === 'number') return duration
46 45
47 if (typeof duration === 'string') { 46 if (typeof duration === 'string') {
@@ -58,7 +57,7 @@ export function parseDuration (duration: number | string): number {
58 } 57 }
59 } 58 }
60 59
61 throw new Error('Duration could not be properly parsed') 60 throw new Error(`Duration ${duration} could not be properly parsed`)
62} 61}
63 62
64export function parseBytes (value: string | number): number { 63export function parseBytes (value: string | number): number {
@@ -193,10 +192,14 @@ function peertubeTruncate (str: string, maxLength: number) {
193 return truncate(str, options) 192 return truncate(str, options)
194} 193}
195 194
196function sha256 (str: string, encoding: HexBase64Latin1Encoding = 'hex') { 195function sha256 (str: string | Buffer, encoding: HexBase64Latin1Encoding = 'hex') {
197 return createHash('sha256').update(str).digest(encoding) 196 return createHash('sha256').update(str).digest(encoding)
198} 197}
199 198
199function sha1 (str: string | Buffer, encoding: HexBase64Latin1Encoding = 'hex') {
200 return createHash('sha1').update(str).digest(encoding)
201}
202
200function promisify0<A> (func: (cb: (err: any, result: A) => void) => void): () => Promise<A> { 203function promisify0<A> (func: (cb: (err: any, result: A) => void) => void): () => Promise<A> {
201 return function promisified (): Promise<A> { 204 return function promisified (): Promise<A> {
202 return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => { 205 return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
@@ -262,7 +265,9 @@ export {
262 sanitizeHost, 265 sanitizeHost,
263 buildPath, 266 buildPath,
264 peertubeTruncate, 267 peertubeTruncate,
268
265 sha256, 269 sha256,
270 sha1,
266 271
267 promisify0, 272 promisify0,
268 promisify1, 273 promisify1,
diff --git a/server/helpers/custom-jsonld-signature.ts b/server/helpers/custom-jsonld-signature.ts
index 27a187db1..a3bceb047 100644
--- a/server/helpers/custom-jsonld-signature.ts
+++ b/server/helpers/custom-jsonld-signature.ts
@@ -1,13 +1,77 @@
1import * as AsyncLRU from 'async-lru' 1import * as AsyncLRU from 'async-lru'
2import * as jsonld from 'jsonld' 2import * as jsonld from 'jsonld'
3import * as jsig from 'jsonld-signatures' 3import * as jsig from 'jsonld-signatures'
4import { logger } from './logger'
5
6const CACHE = {
7 'https://w3id.org/security/v1': {
8 '@context': {
9 'id': '@id',
10 'type': '@type',
11
12 'dc': 'http://purl.org/dc/terms/',
13 'sec': 'https://w3id.org/security#',
14 'xsd': 'http://www.w3.org/2001/XMLSchema#',
15
16 'EcdsaKoblitzSignature2016': 'sec:EcdsaKoblitzSignature2016',
17 'Ed25519Signature2018': 'sec:Ed25519Signature2018',
18 'EncryptedMessage': 'sec:EncryptedMessage',
19 'GraphSignature2012': 'sec:GraphSignature2012',
20 'LinkedDataSignature2015': 'sec:LinkedDataSignature2015',
21 'LinkedDataSignature2016': 'sec:LinkedDataSignature2016',
22 'CryptographicKey': 'sec:Key',
23
24 'authenticationTag': 'sec:authenticationTag',
25 'canonicalizationAlgorithm': 'sec:canonicalizationAlgorithm',
26 'cipherAlgorithm': 'sec:cipherAlgorithm',
27 'cipherData': 'sec:cipherData',
28 'cipherKey': 'sec:cipherKey',
29 'created': { '@id': 'dc:created', '@type': 'xsd:dateTime' },
30 'creator': { '@id': 'dc:creator', '@type': '@id' },
31 'digestAlgorithm': 'sec:digestAlgorithm',
32 'digestValue': 'sec:digestValue',
33 'domain': 'sec:domain',
34 'encryptionKey': 'sec:encryptionKey',
35 'expiration': { '@id': 'sec:expiration', '@type': 'xsd:dateTime' },
36 'expires': { '@id': 'sec:expiration', '@type': 'xsd:dateTime' },
37 'initializationVector': 'sec:initializationVector',
38 'iterationCount': 'sec:iterationCount',
39 'nonce': 'sec:nonce',
40 'normalizationAlgorithm': 'sec:normalizationAlgorithm',
41 'owner': { '@id': 'sec:owner', '@type': '@id' },
42 'password': 'sec:password',
43 'privateKey': { '@id': 'sec:privateKey', '@type': '@id' },
44 'privateKeyPem': 'sec:privateKeyPem',
45 'publicKey': { '@id': 'sec:publicKey', '@type': '@id' },
46 'publicKeyBase58': 'sec:publicKeyBase58',
47 'publicKeyPem': 'sec:publicKeyPem',
48 'publicKeyWif': 'sec:publicKeyWif',
49 'publicKeyService': { '@id': 'sec:publicKeyService', '@type': '@id' },
50 'revoked': { '@id': 'sec:revoked', '@type': 'xsd:dateTime' },
51 'salt': 'sec:salt',
52 'signature': 'sec:signature',
53 'signatureAlgorithm': 'sec:signingAlgorithm',
54 'signatureValue': 'sec:signatureValue'
55 }
56 }
57}
4 58
5const nodeDocumentLoader = jsonld.documentLoaders.node() 59const nodeDocumentLoader = jsonld.documentLoaders.node()
6 60
7const lru = new AsyncLRU({ 61const lru = new AsyncLRU({
8 max: 10, 62 max: 10,
9 load: (key, cb) => { 63 load: (url, cb) => {
10 nodeDocumentLoader(key, cb) 64 if (CACHE[ url ] !== undefined) {
65 logger.debug('Using cache for JSON-LD %s.', url)
66
67 return cb(null, {
68 contextUrl: null,
69 document: CACHE[ url ],
70 documentUrl: url
71 })
72 }
73
74 nodeDocumentLoader(url, cb)
11 } 75 }
12}) 76})
13 77
diff --git a/server/helpers/custom-validators/accounts.ts b/server/helpers/custom-validators/accounts.ts
index 191de1496..146c7708e 100644
--- a/server/helpers/custom-validators/accounts.ts
+++ b/server/helpers/custom-validators/accounts.ts
@@ -5,7 +5,6 @@ import * as validator from 'validator'
5import { AccountModel } from '../../models/account/account' 5import { AccountModel } from '../../models/account/account'
6import { isUserDescriptionValid, isUserUsernameValid } from './users' 6import { isUserDescriptionValid, isUserUsernameValid } from './users'
7import { exists } from './misc' 7import { exists } from './misc'
8import { CONFIG } from '../../initializers'
9 8
10function isAccountNameValid (value: string) { 9function isAccountNameValid (value: string) {
11 return isUserUsernameValid(value) 10 return isUserUsernameValid(value)
@@ -19,7 +18,7 @@ function isAccountDescriptionValid (value: string) {
19 return isUserDescriptionValid(value) 18 return isUserDescriptionValid(value)
20} 19}
21 20
22function isAccountIdExist (id: number | string, res: Response, sendNotFound = true) { 21function doesAccountIdExist (id: number | string, res: Response, sendNotFound = true) {
23 let promise: Bluebird<AccountModel> 22 let promise: Bluebird<AccountModel>
24 23
25 if (validator.isInt('' + id)) { 24 if (validator.isInt('' + id)) {
@@ -28,26 +27,20 @@ function isAccountIdExist (id: number | string, res: Response, sendNotFound = tr
28 promise = AccountModel.loadByUUID('' + id) 27 promise = AccountModel.loadByUUID('' + id)
29 } 28 }
30 29
31 return isAccountExist(promise, res, sendNotFound) 30 return doesAccountExist(promise, res, sendNotFound)
32} 31}
33 32
34function isLocalAccountNameExist (name: string, res: Response, sendNotFound = true) { 33function doesLocalAccountNameExist (name: string, res: Response, sendNotFound = true) {
35 const promise = AccountModel.loadLocalByName(name) 34 const promise = AccountModel.loadLocalByName(name)
36 35
37 return isAccountExist(promise, res, sendNotFound) 36 return doesAccountExist(promise, res, sendNotFound)
38} 37}
39 38
40function isAccountNameWithHostExist (nameWithDomain: string, res: Response, sendNotFound = true) { 39function doesAccountNameWithHostExist (nameWithDomain: string, res: Response, sendNotFound = true) {
41 const [ accountName, host ] = nameWithDomain.split('@') 40 return doesAccountExist(AccountModel.loadByNameWithHost(nameWithDomain), res, sendNotFound)
42
43 let promise: Bluebird<AccountModel>
44 if (!host || host === CONFIG.WEBSERVER.HOST) promise = AccountModel.loadLocalByName(accountName)
45 else promise = AccountModel.loadByNameAndHost(accountName, host)
46
47 return isAccountExist(promise, res, sendNotFound)
48} 41}
49 42
50async function isAccountExist (p: Bluebird<AccountModel>, res: Response, sendNotFound: boolean) { 43async function doesAccountExist (p: Bluebird<AccountModel>, res: Response, sendNotFound: boolean) {
51 const account = await p 44 const account = await p
52 45
53 if (!account) { 46 if (!account) {
@@ -69,9 +62,9 @@ async function isAccountExist (p: Bluebird<AccountModel>, res: Response, sendNot
69 62
70export { 63export {
71 isAccountIdValid, 64 isAccountIdValid,
72 isAccountIdExist, 65 doesAccountIdExist,
73 isLocalAccountNameExist, 66 doesLocalAccountNameExist,
74 isAccountDescriptionValid, 67 isAccountDescriptionValid,
75 isAccountNameWithHostExist, 68 doesAccountNameWithHostExist,
76 isAccountNameValid 69 isAccountNameValid
77} 70}
diff --git a/server/helpers/custom-validators/activitypub/activity.ts b/server/helpers/custom-validators/activitypub/activity.ts
index b24590d9d..e0d170d9d 100644
--- a/server/helpers/custom-validators/activitypub/activity.ts
+++ b/server/helpers/custom-validators/activitypub/activity.ts
@@ -9,6 +9,7 @@ import { isViewActivityValid } from './view'
9import { exists } from '../misc' 9import { exists } from '../misc'
10import { isCacheFileObjectValid } from './cache-file' 10import { isCacheFileObjectValid } from './cache-file'
11import { isFlagActivityValid } from './flag' 11import { isFlagActivityValid } from './flag'
12import { isPlaylistObjectValid } from './playlist'
12 13
13function isRootActivityValid (activity: any) { 14function isRootActivityValid (activity: any) {
14 return Array.isArray(activity['@context']) && ( 15 return Array.isArray(activity['@context']) && (
@@ -78,6 +79,7 @@ function checkCreateActivity (activity: any) {
78 isViewActivityValid(activity.object) || 79 isViewActivityValid(activity.object) ||
79 isDislikeActivityValid(activity.object) || 80 isDislikeActivityValid(activity.object) ||
80 isFlagActivityValid(activity.object) || 81 isFlagActivityValid(activity.object) ||
82 isPlaylistObjectValid(activity.object) ||
81 83
82 isCacheFileObjectValid(activity.object) || 84 isCacheFileObjectValid(activity.object) ||
83 sanitizeAndCheckVideoCommentObject(activity.object) || 85 sanitizeAndCheckVideoCommentObject(activity.object) ||
@@ -89,6 +91,7 @@ function checkUpdateActivity (activity: any) {
89 return isBaseActivityValid(activity, 'Update') && 91 return isBaseActivityValid(activity, 'Update') &&
90 ( 92 (
91 isCacheFileObjectValid(activity.object) || 93 isCacheFileObjectValid(activity.object) ||
94 isPlaylistObjectValid(activity.object) ||
92 sanitizeAndCheckVideoTorrentObject(activity.object) || 95 sanitizeAndCheckVideoTorrentObject(activity.object) ||
93 sanitizeAndCheckActorObject(activity.object) 96 sanitizeAndCheckActorObject(activity.object)
94 ) 97 )
diff --git a/server/helpers/custom-validators/activitypub/actor.ts b/server/helpers/custom-validators/activitypub/actor.ts
index c05f60f14..deb331abb 100644
--- a/server/helpers/custom-validators/activitypub/actor.ts
+++ b/server/helpers/custom-validators/activitypub/actor.ts
@@ -1,5 +1,5 @@
1import * as validator from 'validator' 1import * as validator from 'validator'
2import { CONSTRAINTS_FIELDS } from '../../../initializers' 2import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
3import { exists, isArray } from '../misc' 3import { exists, isArray } from '../misc'
4import { truncate } from 'lodash' 4import { truncate } from 'lodash'
5import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc' 5import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc'
diff --git a/server/helpers/custom-validators/activitypub/cache-file.ts b/server/helpers/custom-validators/activitypub/cache-file.ts
index e2bd0c55e..21d5c53ca 100644
--- a/server/helpers/custom-validators/activitypub/cache-file.ts
+++ b/server/helpers/custom-validators/activitypub/cache-file.ts
@@ -8,9 +8,19 @@ function isCacheFileObjectValid (object: CacheFileObject) {
8 object.type === 'CacheFile' && 8 object.type === 'CacheFile' &&
9 isDateValid(object.expires) && 9 isDateValid(object.expires) &&
10 isActivityPubUrlValid(object.object) && 10 isActivityPubUrlValid(object.object) &&
11 isRemoteVideoUrlValid(object.url) 11 (isRemoteVideoUrlValid(object.url) || isPlaylistRedundancyUrlValid(object.url))
12} 12}
13 13
14// ---------------------------------------------------------------------------
15
14export { 16export {
15 isCacheFileObjectValid 17 isCacheFileObjectValid
16} 18}
19
20// ---------------------------------------------------------------------------
21
22function isPlaylistRedundancyUrlValid (url: any) {
23 return url.type === 'Link' &&
24 (url.mediaType || url.mimeType) === 'application/x-mpegURL' &&
25 isActivityPubUrlValid(url.href)
26}
diff --git a/server/helpers/custom-validators/activitypub/misc.ts b/server/helpers/custom-validators/activitypub/misc.ts
index f1762d11c..5afcfbedc 100644
--- a/server/helpers/custom-validators/activitypub/misc.ts
+++ b/server/helpers/custom-validators/activitypub/misc.ts
@@ -1,5 +1,5 @@
1import * as validator from 'validator' 1import * as validator from 'validator'
2import { CONSTRAINTS_FIELDS } from '../../../initializers' 2import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
3import { isTestInstance } from '../../core-utils' 3import { isTestInstance } from '../../core-utils'
4import { exists } from '../misc' 4import { exists } from '../misc'
5 5
@@ -25,8 +25,7 @@ function isActivityPubUrlValid (url: string) {
25} 25}
26 26
27function isBaseActivityValid (activity: any, type: string) { 27function isBaseActivityValid (activity: any, type: string) {
28 return (activity['@context'] === undefined || Array.isArray(activity['@context'])) && 28 return activity.type === type &&
29 activity.type === type &&
30 isActivityPubUrlValid(activity.id) && 29 isActivityPubUrlValid(activity.id) &&
31 isObjectValid(activity.actor) && 30 isObjectValid(activity.actor) &&
32 isUrlCollectionValid(activity.to) && 31 isUrlCollectionValid(activity.to) &&
diff --git a/server/helpers/custom-validators/activitypub/playlist.ts b/server/helpers/custom-validators/activitypub/playlist.ts
new file mode 100644
index 000000000..6c7bdb193
--- /dev/null
+++ b/server/helpers/custom-validators/activitypub/playlist.ts
@@ -0,0 +1,27 @@
1import { exists, isDateValid } from '../misc'
2import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object'
3import * as validator from 'validator'
4import { PlaylistElementObject } from '../../../../shared/models/activitypub/objects/playlist-element-object'
5import { isActivityPubUrlValid } from './misc'
6
7function isPlaylistObjectValid (object: PlaylistObject) {
8 return exists(object) &&
9 object.type === 'Playlist' &&
10 validator.isInt(object.totalItems + '') &&
11 isDateValid(object.published) &&
12 isDateValid(object.updated)
13}
14
15function isPlaylistElementObjectValid (object: PlaylistElementObject) {
16 return exists(object) &&
17 object.type === 'PlaylistElement' &&
18 validator.isInt(object.position + '') &&
19 isActivityPubUrlValid(object.url)
20}
21
22// ---------------------------------------------------------------------------
23
24export {
25 isPlaylistObjectValid,
26 isPlaylistElementObjectValid
27}
diff --git a/server/helpers/custom-validators/activitypub/video-comments.ts b/server/helpers/custom-validators/activitypub/video-comments.ts
index 0415db21c..26c8c4cc6 100644
--- a/server/helpers/custom-validators/activitypub/video-comments.ts
+++ b/server/helpers/custom-validators/activitypub/video-comments.ts
@@ -1,7 +1,7 @@
1import * as validator from 'validator' 1import * as validator from 'validator'
2import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers' 2import { ACTIVITY_PUB } from '../../../initializers/constants'
3import { exists, isArray, isDateValid } from '../misc' 3import { exists, isArray, isDateValid } from '../misc'
4import { isActivityPubUrlValid, isBaseActivityValid } from './misc' 4import { isActivityPubUrlValid } from './misc'
5 5
6function sanitizeAndCheckVideoCommentObject (comment: any) { 6function sanitizeAndCheckVideoCommentObject (comment: any) {
7 if (!comment || comment.type !== 'Note') return false 7 if (!comment || comment.type !== 'Note') return false
diff --git a/server/helpers/custom-validators/activitypub/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts
index 0f34aab21..3ba6b0744 100644
--- a/server/helpers/custom-validators/activitypub/videos.ts
+++ b/server/helpers/custom-validators/activitypub/videos.ts
@@ -1,7 +1,7 @@
1import * as validator from 'validator' 1import * as validator from 'validator'
2import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers' 2import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers/constants'
3import { peertubeTruncate } from '../../core-utils' 3import { peertubeTruncate } from '../../core-utils'
4import { exists, isBooleanValid, isDateValid, isUUIDValid } from '../misc' 4import { exists, isArray, isBooleanValid, isDateValid, isUUIDValid } from '../misc'
5import { 5import {
6 isVideoDurationValid, 6 isVideoDurationValid,
7 isVideoNameValid, 7 isVideoNameValid,
@@ -12,7 +12,6 @@ import {
12} from '../videos' 12} from '../videos'
13import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc' 13import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc'
14import { VideoState } from '../../../../shared/models/videos' 14import { VideoState } from '../../../../shared/models/videos'
15import { isVideoAbuseReasonValid } from '../video-abuses'
16 15
17function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) { 16function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) {
18 return isBaseActivityValid(activity, 'Update') && 17 return isBaseActivityValid(activity, 'Update') &&
@@ -40,6 +39,7 @@ function sanitizeAndCheckVideoTorrentObject (video: any) {
40 // Default attributes 39 // Default attributes
41 if (!isVideoStateValid(video.state)) video.state = VideoState.PUBLISHED 40 if (!isVideoStateValid(video.state)) video.state = VideoState.PUBLISHED
42 if (!isBooleanValid(video.waitTranscoding)) video.waitTranscoding = false 41 if (!isBooleanValid(video.waitTranscoding)) video.waitTranscoding = false
42 if (!isBooleanValid(video.downloadEnabled)) video.downloadEnabled = true
43 43
44 return isActivityPubUrlValid(video.id) && 44 return isActivityPubUrlValid(video.id) &&
45 isVideoNameValid(video.name) && 45 isVideoNameValid(video.name) &&
@@ -51,8 +51,10 @@ function sanitizeAndCheckVideoTorrentObject (video: any) {
51 isVideoViewsValid(video.views) && 51 isVideoViewsValid(video.views) &&
52 isBooleanValid(video.sensitive) && 52 isBooleanValid(video.sensitive) &&
53 isBooleanValid(video.commentsEnabled) && 53 isBooleanValid(video.commentsEnabled) &&
54 isBooleanValid(video.downloadEnabled) &&
54 isDateValid(video.published) && 55 isDateValid(video.published) &&
55 isDateValid(video.updated) && 56 isDateValid(video.updated) &&
57 (!video.originallyPublishedAt || isDateValid(video.originallyPublishedAt)) &&
56 (!video.content || isRemoteVideoContentValid(video.mediaType, video.content)) && 58 (!video.content || isRemoteVideoContentValid(video.mediaType, video.content)) &&
57 isRemoteVideoIconValid(video.icon) && 59 isRemoteVideoIconValid(video.icon) &&
58 video.url.length !== 0 && 60 video.url.length !== 0 &&
@@ -81,6 +83,11 @@ function isRemoteVideoUrlValid (url: any) {
81 ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.indexOf(url.mediaType || url.mimeType) !== -1 && 83 ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.indexOf(url.mediaType || url.mimeType) !== -1 &&
82 validator.isLength(url.href, { min: 5 }) && 84 validator.isLength(url.href, { min: 5 }) &&
83 validator.isInt(url.height + '', { min: 0 }) 85 validator.isInt(url.height + '', { min: 0 })
86 ) ||
87 (
88 (url.mediaType || url.mimeType) === 'application/x-mpegURL' &&
89 isActivityPubUrlValid(url.href) &&
90 isArray(url.tag)
84 ) 91 )
85} 92}
86 93
diff --git a/server/helpers/custom-validators/logs.ts b/server/helpers/custom-validators/logs.ts
new file mode 100644
index 000000000..30d0ce262
--- /dev/null
+++ b/server/helpers/custom-validators/logs.ts
@@ -0,0 +1,14 @@
1import { exists } from './misc'
2import { LogLevel } from '../../../shared/models/server/log-level.type'
3
4const logLevels: LogLevel[] = [ 'debug', 'info', 'warn', 'error' ]
5
6function isValidLogLevel (value: any) {
7 return exists(value) && logLevels.indexOf(value) !== -1
8}
9
10// ---------------------------------------------------------------------------
11
12export {
13 isValidLogLevel
14}
diff --git a/server/helpers/custom-validators/misc.ts b/server/helpers/custom-validators/misc.ts
index b6f0ebe6f..3a3deab0c 100644
--- a/server/helpers/custom-validators/misc.ts
+++ b/server/helpers/custom-validators/misc.ts
@@ -13,6 +13,10 @@ function isNotEmptyIntArray (value: any) {
13 return Array.isArray(value) && value.every(v => validator.isInt('' + v)) && value.length !== 0 13 return Array.isArray(value) && value.every(v => validator.isInt('' + v)) && value.length !== 0
14} 14}
15 15
16function isArrayOf (value: any, validator: (value: any) => boolean) {
17 return isArray(value) && value.every(v => validator(v))
18}
19
16function isDateValid (value: string) { 20function isDateValid (value: string) {
17 return exists(value) && validator.isISO8601(value) 21 return exists(value) && validator.isISO8601(value)
18} 22}
@@ -45,12 +49,19 @@ function toValueOrNull (value: string) {
45 return value 49 return value
46} 50}
47 51
48function toArray (value: string) { 52function toArray (value: any) {
49 if (value && isArray(value) === false) return [ value ] 53 if (value && isArray(value) === false) return [ value ]
50 54
51 return value 55 return value
52} 56}
53 57
58function toIntArray (value: any) {
59 if (!value) return []
60 if (isArray(value) === false) return [ validator.toInt(value) ]
61
62 return value.map(v => validator.toInt(v))
63}
64
54function isFileValid ( 65function isFileValid (
55 files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[], 66 files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[],
56 mimeTypeRegex: string, 67 mimeTypeRegex: string,
@@ -82,6 +93,7 @@ function isFileValid (
82 93
83export { 94export {
84 exists, 95 exists,
96 isArrayOf,
85 isNotEmptyIntArray, 97 isNotEmptyIntArray,
86 isArray, 98 isArray,
87 isIdValid, 99 isIdValid,
@@ -92,5 +104,6 @@ export {
92 isBooleanValid, 104 isBooleanValid,
93 toIntOrNull, 105 toIntOrNull,
94 toArray, 106 toArray,
107 toIntArray,
95 isFileValid 108 isFileValid
96} 109}
diff --git a/server/helpers/custom-validators/servers.ts b/server/helpers/custom-validators/servers.ts
index 18c80ec8f..5c8bf0d2d 100644
--- a/server/helpers/custom-validators/servers.ts
+++ b/server/helpers/custom-validators/servers.ts
@@ -3,7 +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' 6import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
7 7
8function isHostValid (host: string) { 8function isHostValid (host: string) {
9 const isURLOptions = { 9 const isURLOptions = {
diff --git a/server/helpers/custom-validators/users.ts b/server/helpers/custom-validators/users.ts
index 80652b479..56bc10b16 100644
--- a/server/helpers/custom-validators/users.ts
+++ b/server/helpers/custom-validators/users.ts
@@ -1,8 +1,8 @@
1import 'express-validator' 1import 'express-validator'
2import * as validator from 'validator' 2import * as validator from 'validator'
3import { UserRole } from '../../../shared' 3import { UserRole } from '../../../shared'
4import { CONSTRAINTS_FIELDS, NSFW_POLICY_TYPES } from '../../initializers' 4import { CONSTRAINTS_FIELDS, NSFW_POLICY_TYPES } from '../../initializers/constants'
5import { exists, isFileValid, isBooleanValid } from './misc' 5import { exists, isBooleanValid, isFileValid } from './misc'
6import { values } from 'lodash' 6import { values } from 'lodash'
7 7
8const USERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USERS 8const USERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USERS
@@ -54,6 +54,10 @@ function isUserAutoPlayVideoValid (value: any) {
54 return isBooleanValid(value) 54 return isBooleanValid(value)
55} 55}
56 56
57function isUserAdminFlagsValid (value: any) {
58 return exists(value) && validator.isInt('' + value)
59}
60
57function isUserBlockedValid (value: any) { 61function isUserBlockedValid (value: any) {
58 return isBooleanValid(value) 62 return isBooleanValid(value)
59} 63}
@@ -85,6 +89,7 @@ export {
85 isUserVideoQuotaValid, 89 isUserVideoQuotaValid,
86 isUserVideoQuotaDailyValid, 90 isUserVideoQuotaDailyValid,
87 isUserUsernameValid, 91 isUserUsernameValid,
92 isUserAdminFlagsValid,
88 isUserEmailVerifiedValid, 93 isUserEmailVerifiedValid,
89 isUserNSFWPolicyValid, 94 isUserNSFWPolicyValid,
90 isUserWebTorrentEnabledValid, 95 isUserWebTorrentEnabledValid,
diff --git a/server/helpers/custom-validators/video-abuses.ts b/server/helpers/custom-validators/video-abuses.ts
index 290efb149..a61dcee1c 100644
--- a/server/helpers/custom-validators/video-abuses.ts
+++ b/server/helpers/custom-validators/video-abuses.ts
@@ -1,6 +1,6 @@
1import { Response } from 'express' 1import { Response } from 'express'
2import * as validator from 'validator' 2import * as validator from 'validator'
3import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers' 3import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants'
4import { exists } from './misc' 4import { exists } from './misc'
5import { VideoAbuseModel } from '../../models/video/video-abuse' 5import { VideoAbuseModel } from '../../models/video/video-abuse'
6 6
@@ -18,7 +18,7 @@ function isVideoAbuseStateValid (value: string) {
18 return exists(value) && VIDEO_ABUSE_STATES[ value ] !== undefined 18 return exists(value) && VIDEO_ABUSE_STATES[ value ] !== undefined
19} 19}
20 20
21async function isVideoAbuseExist (abuseId: number, videoId: number, res: Response) { 21async function doesVideoAbuseExist (abuseId: number, videoId: number, res: Response) {
22 const videoAbuse = await VideoAbuseModel.loadByIdAndVideoId(abuseId, videoId) 22 const videoAbuse = await VideoAbuseModel.loadByIdAndVideoId(abuseId, videoId)
23 23
24 if (videoAbuse === null) { 24 if (videoAbuse === null) {
@@ -36,7 +36,7 @@ async function isVideoAbuseExist (abuseId: number, videoId: number, res: Respons
36// --------------------------------------------------------------------------- 36// ---------------------------------------------------------------------------
37 37
38export { 38export {
39 isVideoAbuseExist, 39 doesVideoAbuseExist,
40 isVideoAbuseStateValid, 40 isVideoAbuseStateValid,
41 isVideoAbuseReasonValid, 41 isVideoAbuseReasonValid,
42 isVideoAbuseModerationCommentValid 42 isVideoAbuseModerationCommentValid
diff --git a/server/helpers/custom-validators/video-blacklist.ts b/server/helpers/custom-validators/video-blacklist.ts
index b36b08d8b..3743f7023 100644
--- a/server/helpers/custom-validators/video-blacklist.ts
+++ b/server/helpers/custom-validators/video-blacklist.ts
@@ -1,7 +1,9 @@
1import { Response } from 'express' 1import { Response } from 'express'
2import * as validator from 'validator' 2import * as validator from 'validator'
3import { CONSTRAINTS_FIELDS } from '../../initializers' 3import { exists } from './misc'
4import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
4import { VideoBlacklistModel } from '../../models/video/video-blacklist' 5import { VideoBlacklistModel } from '../../models/video/video-blacklist'
6import { VideoBlacklistType } from '../../../shared/models/videos'
5 7
6const VIDEO_BLACKLIST_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_BLACKLIST 8const VIDEO_BLACKLIST_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_BLACKLIST
7 9
@@ -9,7 +11,7 @@ function isVideoBlacklistReasonValid (value: string) {
9 return value === null || validator.isLength(value, VIDEO_BLACKLIST_CONSTRAINTS_FIELDS.REASON) 11 return value === null || validator.isLength(value, VIDEO_BLACKLIST_CONSTRAINTS_FIELDS.REASON)
10} 12}
11 13
12async function isVideoBlacklistExist (videoId: number, res: Response) { 14async function doesVideoBlacklistExist (videoId: number, res: Response) {
13 const videoBlacklist = await VideoBlacklistModel.loadByVideoId(videoId) 15 const videoBlacklist = await VideoBlacklistModel.loadByVideoId(videoId)
14 16
15 if (videoBlacklist === null) { 17 if (videoBlacklist === null) {
@@ -24,9 +26,14 @@ async function isVideoBlacklistExist (videoId: number, res: Response) {
24 return true 26 return true
25} 27}
26 28
29function isVideoBlacklistTypeValid (value: any) {
30 return exists(value) && validator.isInt('' + value) && VideoBlacklistType[value] !== undefined
31}
32
27// --------------------------------------------------------------------------- 33// ---------------------------------------------------------------------------
28 34
29export { 35export {
30 isVideoBlacklistReasonValid, 36 isVideoBlacklistReasonValid,
31 isVideoBlacklistExist 37 isVideoBlacklistTypeValid,
38 doesVideoBlacklistExist
32} 39}
diff --git a/server/helpers/custom-validators/video-captions.ts b/server/helpers/custom-validators/video-captions.ts
index b33d90e18..3b6569a8a 100644
--- a/server/helpers/custom-validators/video-captions.ts
+++ b/server/helpers/custom-validators/video-captions.ts
@@ -1,4 +1,4 @@
1import { CONSTRAINTS_FIELDS, MIMETYPES, VIDEO_LANGUAGES } from '../../initializers' 1import { CONSTRAINTS_FIELDS, MIMETYPES, VIDEO_LANGUAGES } from '../../initializers/constants'
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'
@@ -16,7 +16,7 @@ function isVideoCaptionFile (files: { [ fieldname: string ]: Express.Multer.File
16 return isFileValid(files, videoCaptionTypesRegex, field, CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.FILE_SIZE.max) 16 return isFileValid(files, videoCaptionTypesRegex, field, CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.FILE_SIZE.max)
17} 17}
18 18
19async function isVideoCaptionExist (video: VideoModel, language: string, res: Response) { 19async function doesVideoCaptionExist (video: VideoModel, language: string, res: Response) {
20 const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(video.id, language) 20 const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(video.id, language)
21 21
22 if (!videoCaption) { 22 if (!videoCaption) {
@@ -36,5 +36,5 @@ async function isVideoCaptionExist (video: VideoModel, language: string, res: Re
36export { 36export {
37 isVideoCaptionFile, 37 isVideoCaptionFile,
38 isVideoCaptionLanguageValid, 38 isVideoCaptionLanguageValid,
39 isVideoCaptionExist 39 doesVideoCaptionExist
40} 40}
diff --git a/server/helpers/custom-validators/video-channels.ts b/server/helpers/custom-validators/video-channels.ts
index f13519c1d..fd56b9a70 100644
--- a/server/helpers/custom-validators/video-channels.ts
+++ b/server/helpers/custom-validators/video-channels.ts
@@ -2,7 +2,7 @@ import * as express from 'express'
2import 'express-validator' 2import 'express-validator'
3import 'multer' 3import 'multer'
4import * as validator from 'validator' 4import * as validator from 'validator'
5import { CONFIG, CONSTRAINTS_FIELDS } from '../../initializers' 5import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
6import { VideoChannelModel } from '../../models/video/video-channel' 6import { VideoChannelModel } from '../../models/video/video-channel'
7import { exists } from './misc' 7import { exists } from './misc'
8 8
@@ -20,29 +20,25 @@ function isVideoChannelSupportValid (value: string) {
20 return value === null || (exists(value) && validator.isLength(value, VIDEO_CHANNELS_CONSTRAINTS_FIELDS.SUPPORT)) 20 return value === null || (exists(value) && validator.isLength(value, VIDEO_CHANNELS_CONSTRAINTS_FIELDS.SUPPORT))
21} 21}
22 22
23async function isLocalVideoChannelNameExist (name: string, res: express.Response) { 23async function doesLocalVideoChannelNameExist (name: string, res: express.Response) {
24 const videoChannel = await VideoChannelModel.loadLocalByNameAndPopulateAccount(name) 24 const videoChannel = await VideoChannelModel.loadLocalByNameAndPopulateAccount(name)
25 25
26 return processVideoChannelExist(videoChannel, res) 26 return processVideoChannelExist(videoChannel, res)
27} 27}
28 28
29async function isVideoChannelIdExist (id: string, res: express.Response) { 29async function doesVideoChannelIdExist (id: number | string, res: express.Response) {
30 let videoChannel: VideoChannelModel 30 let videoChannel: VideoChannelModel
31 if (validator.isInt(id)) { 31 if (validator.isInt('' + id)) {
32 videoChannel = await VideoChannelModel.loadAndPopulateAccount(+id) 32 videoChannel = await VideoChannelModel.loadAndPopulateAccount(+id)
33 } else { // UUID 33 } else { // UUID
34 videoChannel = await VideoChannelModel.loadByUUIDAndPopulateAccount(id) 34 videoChannel = await VideoChannelModel.loadByUUIDAndPopulateAccount('' + id)
35 } 35 }
36 36
37 return processVideoChannelExist(videoChannel, res) 37 return processVideoChannelExist(videoChannel, res)
38} 38}
39 39
40async function isVideoChannelNameWithHostExist (nameWithDomain: string, res: express.Response) { 40async function doesVideoChannelNameWithHostExist (nameWithDomain: string, res: express.Response) {
41 const [ name, host ] = nameWithDomain.split('@') 41 const videoChannel = await VideoChannelModel.loadByNameWithHostAndPopulateAccount(nameWithDomain)
42 let videoChannel: VideoChannelModel
43
44 if (!host || host === CONFIG.WEBSERVER.HOST) videoChannel = await VideoChannelModel.loadLocalByNameAndPopulateAccount(name)
45 else videoChannel = await VideoChannelModel.loadByNameAndHostAndPopulateAccount(name, host)
46 42
47 return processVideoChannelExist(videoChannel, res) 43 return processVideoChannelExist(videoChannel, res)
48} 44}
@@ -50,12 +46,12 @@ async function isVideoChannelNameWithHostExist (nameWithDomain: string, res: exp
50// --------------------------------------------------------------------------- 46// ---------------------------------------------------------------------------
51 47
52export { 48export {
53 isVideoChannelNameWithHostExist, 49 doesVideoChannelNameWithHostExist,
54 isLocalVideoChannelNameExist, 50 doesLocalVideoChannelNameExist,
55 isVideoChannelDescriptionValid, 51 isVideoChannelDescriptionValid,
56 isVideoChannelNameValid, 52 isVideoChannelNameValid,
57 isVideoChannelSupportValid, 53 isVideoChannelSupportValid,
58 isVideoChannelIdExist 54 doesVideoChannelIdExist
59} 55}
60 56
61function processVideoChannelExist (videoChannel: VideoChannelModel, res: express.Response) { 57function processVideoChannelExist (videoChannel: VideoChannelModel, res: express.Response) {
diff --git a/server/helpers/custom-validators/video-comments.ts b/server/helpers/custom-validators/video-comments.ts
index 2b3f66063..0707e2af2 100644
--- a/server/helpers/custom-validators/video-comments.ts
+++ b/server/helpers/custom-validators/video-comments.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 } from '../../initializers' 4import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
5 5
6const VIDEO_COMMENTS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_COMMENTS 6const VIDEO_COMMENTS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_COMMENTS
7 7
diff --git a/server/helpers/custom-validators/video-imports.ts b/server/helpers/custom-validators/video-imports.ts
index ce9e9193c..f4235e2fa 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, MIMETYPES, VIDEO_IMPORT_STATES } from '../../initializers' 4import { CONSTRAINTS_FIELDS, MIMETYPES, VIDEO_IMPORT_STATES } from '../../initializers/constants'
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'
@@ -30,7 +30,7 @@ function isVideoImportTorrentFile (files: { [ fieldname: string ]: Express.Multe
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)
31} 31}
32 32
33async function isVideoImportExist (id: number, res: express.Response) { 33async function doesVideoImportExist (id: number, res: express.Response) {
34 const videoImport = await VideoImportModel.loadAndPopulateVideo(id) 34 const videoImport = await VideoImportModel.loadAndPopulateVideo(id)
35 35
36 if (!videoImport) { 36 if (!videoImport) {
@@ -50,6 +50,6 @@ async function isVideoImportExist (id: number, res: express.Response) {
50export { 50export {
51 isVideoImportStateValid, 51 isVideoImportStateValid,
52 isVideoImportTargetUrlValid, 52 isVideoImportTargetUrlValid,
53 isVideoImportExist, 53 doesVideoImportExist,
54 isVideoImportTorrentFile 54 isVideoImportTorrentFile
55} 55}
diff --git a/server/helpers/custom-validators/video-playlists.ts b/server/helpers/custom-validators/video-playlists.ts
new file mode 100644
index 000000000..2fe426560
--- /dev/null
+++ b/server/helpers/custom-validators/video-playlists.ts
@@ -0,0 +1,55 @@
1import { exists } from './misc'
2import * as validator from 'validator'
3import { CONSTRAINTS_FIELDS, VIDEO_PLAYLIST_PRIVACIES, VIDEO_PLAYLIST_TYPES } from '../../initializers/constants'
4import * as express from 'express'
5import { VideoPlaylistModel } from '../../models/video/video-playlist'
6
7const PLAYLISTS_CONSTRAINT_FIELDS = CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS
8
9function isVideoPlaylistNameValid (value: any) {
10 return exists(value) && validator.isLength(value, PLAYLISTS_CONSTRAINT_FIELDS.NAME)
11}
12
13function isVideoPlaylistDescriptionValid (value: any) {
14 return value === null || (exists(value) && validator.isLength(value, PLAYLISTS_CONSTRAINT_FIELDS.DESCRIPTION))
15}
16
17function isVideoPlaylistPrivacyValid (value: number) {
18 return validator.isInt(value + '') && VIDEO_PLAYLIST_PRIVACIES[ value ] !== undefined
19}
20
21function isVideoPlaylistTimestampValid (value: any) {
22 return value === null || (exists(value) && validator.isInt('' + value, { min: 0 }))
23}
24
25function isVideoPlaylistTypeValid (value: any) {
26 return exists(value) && VIDEO_PLAYLIST_TYPES[ value ] !== undefined
27}
28
29async function doesVideoPlaylistExist (id: number | string, res: express.Response, fetchType: 'summary' | 'all' = 'summary') {
30 const videoPlaylist = fetchType === 'summary'
31 ? await VideoPlaylistModel.loadWithAccountAndChannelSummary(id, undefined)
32 : await VideoPlaylistModel.loadWithAccountAndChannel(id, undefined)
33
34 if (!videoPlaylist) {
35 res.status(404)
36 .json({ error: 'Video playlist not found' })
37 .end()
38
39 return false
40 }
41
42 res.locals.videoPlaylist = videoPlaylist
43 return true
44}
45
46// ---------------------------------------------------------------------------
47
48export {
49 doesVideoPlaylistExist,
50 isVideoPlaylistNameValid,
51 isVideoPlaylistDescriptionValid,
52 isVideoPlaylistPrivacyValid,
53 isVideoPlaylistTimestampValid,
54 isVideoPlaylistTypeValid
55}
diff --git a/server/helpers/custom-validators/video-rates.ts b/server/helpers/custom-validators/video-rates.ts
new file mode 100644
index 000000000..f2b6f7cae
--- /dev/null
+++ b/server/helpers/custom-validators/video-rates.ts
@@ -0,0 +1,5 @@
1function isRatingValid (value: any) {
2 return value === 'like' || value === 'dislike'
3}
4
5export { isRatingValid }
diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts
index 95e256b8f..214db17a1 100644
--- a/server/helpers/custom-validators/videos.ts
+++ b/server/helpers/custom-validators/videos.ts
@@ -5,15 +5,16 @@ 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, MIMETYPES, 8 CONSTRAINTS_FIELDS,
9 MIMETYPES,
9 VIDEO_CATEGORIES, 10 VIDEO_CATEGORIES,
10 VIDEO_LICENCES, 11 VIDEO_LICENCES,
11 VIDEO_PRIVACIES, 12 VIDEO_PRIVACIES,
12 VIDEO_RATE_TYPES, 13 VIDEO_RATE_TYPES,
13 VIDEO_STATES 14 VIDEO_STATES
14} from '../../initializers' 15} from '../../initializers/constants'
15import { VideoModel } from '../../models/video/video' 16import { VideoModel } from '../../models/video/video'
16import { exists, isArray, isFileValid } from './misc' 17import { exists, isArray, isDateValid, isFileValid } from './misc'
17import { VideoChannelModel } from '../../models/video/video-channel' 18import { VideoChannelModel } from '../../models/video/video-channel'
18import { UserModel } from '../../models/account/user' 19import { UserModel } from '../../models/account/user'
19import * as magnetUtil from 'magnet-uri' 20import * as magnetUtil from 'magnet-uri'
@@ -115,6 +116,10 @@ function isScheduleVideoUpdatePrivacyValid (value: number) {
115 ) 116 )
116} 117}
117 118
119function isVideoOriginallyPublishedAtValid (value: string | null) {
120 return value === null || isDateValid(value)
121}
122
118function isVideoFileInfoHashValid (value: string | null | undefined) { 123function isVideoFileInfoHashValid (value: string | null | undefined) {
119 return exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.INFO_HASH) 124 return exists(value) && validator.isLength(value, VIDEOS_CONSTRAINTS_FIELDS.INFO_HASH)
120} 125}
@@ -161,7 +166,7 @@ function checkUserCanManageVideo (user: UserModel, video: VideoModel, right: Use
161 return true 166 return true
162} 167}
163 168
164async function isVideoExist (id: string, res: Response, fetchType: VideoFetchType = 'all') { 169async function doesVideoExist (id: number | string, res: Response, fetchType: VideoFetchType = 'all') {
165 const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined 170 const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined
166 171
167 const video = await fetchVideo(id, fetchType, userId) 172 const video = await fetchVideo(id, fetchType, userId)
@@ -178,7 +183,7 @@ async function isVideoExist (id: string, res: Response, fetchType: VideoFetchTyp
178 return true 183 return true
179} 184}
180 185
181async function isVideoChannelOfAccountExist (channelId: number, user: UserModel, res: Response) { 186async function doesVideoChannelOfAccountExist (channelId: number, user: UserModel, res: Response) {
182 if (user.hasRight(UserRight.UPDATE_ANY_VIDEO) === true) { 187 if (user.hasRight(UserRight.UPDATE_ANY_VIDEO) === true) {
183 const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId) 188 const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId)
184 if (videoChannel === null) { 189 if (videoChannel === null) {
@@ -220,6 +225,7 @@ export {
220 isVideoTagsValid, 225 isVideoTagsValid,
221 isVideoFPSResolutionValid, 226 isVideoFPSResolutionValid,
222 isScheduleVideoUpdatePrivacyValid, 227 isScheduleVideoUpdatePrivacyValid,
228 isVideoOriginallyPublishedAtValid,
223 isVideoFile, 229 isVideoFile,
224 isVideoMagnetUriValid, 230 isVideoMagnetUriValid,
225 isVideoStateValid, 231 isVideoStateValid,
@@ -231,9 +237,9 @@ export {
231 isVideoPrivacyValid, 237 isVideoPrivacyValid,
232 isVideoFileResolutionValid, 238 isVideoFileResolutionValid,
233 isVideoFileSizeValid, 239 isVideoFileSizeValid,
234 isVideoExist, 240 doesVideoExist,
235 isVideoImage, 241 isVideoImage,
236 isVideoChannelOfAccountExist, 242 doesVideoChannelOfAccountExist,
237 isVideoSupportValid, 243 isVideoSupportValid,
238 isVideoFilterValid 244 isVideoFilterValid
239} 245}
diff --git a/server/helpers/custom-validators/webfinger.ts b/server/helpers/custom-validators/webfinger.ts
index 80a7e4a9d..dd914341e 100644
--- a/server/helpers/custom-validators/webfinger.ts
+++ b/server/helpers/custom-validators/webfinger.ts
@@ -1,4 +1,4 @@
1import { CONFIG, REMOTE_SCHEME } from '../../initializers' 1import { REMOTE_SCHEME, WEBSERVER } from '../../initializers/constants'
2import { sanitizeHost } from '../core-utils' 2import { sanitizeHost } from '../core-utils'
3import { exists } from './misc' 3import { exists } from './misc'
4 4
@@ -11,7 +11,7 @@ function isWebfingerLocalResourceValid (value: string) {
11 if (actorParts.length !== 2) return false 11 if (actorParts.length !== 2) return false
12 12
13 const host = actorParts[1] 13 const host = actorParts[1]
14 return sanitizeHost(host, REMOTE_SCHEME.HTTP) === CONFIG.WEBSERVER.HOST 14 return sanitizeHost(host, REMOTE_SCHEME.HTTP) === WEBSERVER.HOST
15} 15}
16 16
17// --------------------------------------------------------------------------- 17// ---------------------------------------------------------------------------
diff --git a/server/helpers/database-utils.ts b/server/helpers/database-utils.ts
index 1005d2cf1..39c74b2fd 100644
--- a/server/helpers/database-utils.ts
+++ b/server/helpers/database-utils.ts
@@ -62,14 +62,13 @@ function updateInstanceWithAnother <T extends Model<T>> (instanceToUpdate: Model
62 const obj = baseInstance.toJSON() 62 const obj = baseInstance.toJSON()
63 63
64 for (const key of Object.keys(obj)) { 64 for (const key of Object.keys(obj)) {
65 instanceToUpdate.set(key, obj[key]) 65 instanceToUpdate[key] = obj[key]
66 } 66 }
67} 67}
68 68
69function resetSequelizeInstance (instance: Model<any>, savedFields: object) { 69function resetSequelizeInstance (instance: Model<any>, savedFields: object) {
70 Object.keys(savedFields).forEach(key => { 70 Object.keys(savedFields).forEach(key => {
71 const value = savedFields[key] 71 instance[key] = savedFields[key]
72 instance.set(key, value)
73 }) 72 })
74} 73}
75 74
diff --git a/server/helpers/express-utils.ts b/server/helpers/express-utils.ts
index 9a72ee96d..e0a1d56a5 100644
--- a/server/helpers/express-utils.ts
+++ b/server/helpers/express-utils.ts
@@ -1,11 +1,11 @@
1import * as express from 'express' 1import * as express from 'express'
2import * as multer from 'multer' 2import * as multer from 'multer'
3import { CONFIG, REMOTE_SCHEME } from '../initializers' 3import { REMOTE_SCHEME } from '../initializers/constants'
4import { logger } from './logger' 4import { logger } from './logger'
5import { deleteFileAsync, generateRandomString } from './utils' 5import { deleteFileAsync, generateRandomString } from './utils'
6import { extname } from 'path' 6import { extname } from 'path'
7import { isArray } from './custom-validators/misc' 7import { isArray } from './custom-validators/misc'
8import { UserModel } from '../models/account/user' 8import { CONFIG } from '../initializers/config'
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
@@ -13,7 +13,7 @@ function buildNSFWFilter (res?: express.Response, paramNSFW?: string) {
13 if (paramNSFW === 'both') return undefined 13 if (paramNSFW === 'both') return undefined
14 14
15 if (res && res.locals.oauth) { 15 if (res && res.locals.oauth) {
16 const user: UserModel = res.locals.oauth.token.User 16 const user = res.locals.oauth.token.User
17 17
18 // User does not want NSFW videos 18 // User does not want NSFW videos
19 if (user.nsfwPolicy === 'do_not_list') return false 19 if (user.nsfwPolicy === 'do_not_list') return false
@@ -59,7 +59,7 @@ function getHostWithPort (host: string) {
59 return host 59 return host
60} 60}
61 61
62function badRequest (req: express.Request, res: express.Response, next: express.NextFunction) { 62function badRequest (req: express.Request, res: express.Response) {
63 return res.type('json').status(400).end() 63 return res.type('json').status(400).end()
64} 64}
65 65
@@ -100,7 +100,7 @@ function createReqFiles (
100} 100}
101 101
102function isUserAbleToSearchRemoteURI (res: express.Response) { 102function isUserAbleToSearchRemoteURI (res: express.Response) {
103 const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined 103 const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
104 104
105 return CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true || 105 return CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true ||
106 (CONFIG.SEARCH.REMOTE_URI.USERS === true && user !== undefined) 106 (CONFIG.SEARCH.REMOTE_URI.USERS === true && user !== undefined)
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts
index 132f4690e..76b744de8 100644
--- a/server/helpers/ffmpeg-utils.ts
+++ b/server/helpers/ffmpeg-utils.ts
@@ -1,11 +1,12 @@
1import * as ffmpeg from 'fluent-ffmpeg' 1import * as ffmpeg from 'fluent-ffmpeg'
2import { join } from 'path' 2import { dirname, join } from 'path'
3import { getTargetBitrate, VideoResolution } from '../../shared/models/videos' 3import { getTargetBitrate, VideoResolution } from '../../shared/models/videos'
4import { CONFIG, FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers/constants' 4import { FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers/constants'
5import { processImage } from './image-utils' 5import { processImage } from './image-utils'
6import { logger } from './logger' 6import { logger } from './logger'
7import { checkFFmpegEncoders } from '../initializers/checker-before-init' 7import { checkFFmpegEncoders } from '../initializers/checker-before-init'
8import { remove } from 'fs-extra' 8import { readFile, remove, writeFile } from 'fs-extra'
9import { CONFIG } from '../initializers/config'
9 10
10function computeResolutionsToTranscode (videoFileHeight: number) { 11function computeResolutionsToTranscode (videoFileHeight: number) {
11 const resolutionsEnabled: number[] = [] 12 const resolutionsEnabled: number[] = []
@@ -29,12 +30,21 @@ function computeResolutionsToTranscode (videoFileHeight: number) {
29 return resolutionsEnabled 30 return resolutionsEnabled
30} 31}
31 32
32async function getVideoFileResolution (path: string) { 33async function getVideoFileSize (path: string) {
33 const videoStream = await getVideoFileStream(path) 34 const videoStream = await getVideoFileStream(path)
34 35
35 return { 36 return {
36 videoFileResolution: Math.min(videoStream.height, videoStream.width), 37 width: videoStream.width,
37 isPortraitMode: videoStream.height > videoStream.width 38 height: videoStream.height
39 }
40}
41
42async function getVideoFileResolution (path: string) {
43 const size = await getVideoFileSize(path)
44
45 return {
46 videoFileResolution: Math.min(size.height, size.width),
47 isPortraitMode: size.height > size.width
38 } 48 }
39} 49}
40 50
@@ -95,7 +105,7 @@ async function generateImageFromVideoFile (fromPath: string, folder: string, ima
95 }) 105 })
96 106
97 const destination = join(folder, imageName) 107 const destination = join(folder, imageName)
98 await processImage({ path: pendingImagePath }, destination, size) 108 await processImage(pendingImagePath, destination, size)
99 } catch (err) { 109 } catch (err) {
100 logger.error('Cannot generate image from video %s.', fromPath, { err }) 110 logger.error('Cannot generate image from video %s.', fromPath, { err })
101 111
@@ -110,52 +120,41 @@ async function generateImageFromVideoFile (fromPath: string, folder: string, ima
110type TranscodeOptions = { 120type TranscodeOptions = {
111 inputPath: string 121 inputPath: string
112 outputPath: string 122 outputPath: string
113 resolution?: VideoResolution 123 resolution: VideoResolution
114 isPortraitMode?: boolean 124 isPortraitMode?: boolean
125
126 hlsPlaylist?: {
127 videoFilename: string
128 }
115} 129}
116 130
117function transcode (options: TranscodeOptions) { 131function transcode (options: TranscodeOptions) {
118 return new Promise<void>(async (res, rej) => { 132 return new Promise<void>(async (res, rej) => {
119 try { 133 try {
120 let fps = await getVideoFileFPS(options.inputPath)
121 // On small/medium resolutions, limit FPS
122 if (
123 options.resolution !== undefined &&
124 options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN &&
125 fps > VIDEO_TRANSCODING_FPS.AVERAGE
126 ) {
127 fps = VIDEO_TRANSCODING_FPS.AVERAGE
128 }
129
130 let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING }) 134 let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING })
131 .output(options.outputPath) 135 .output(options.outputPath)
132 command = await presetH264(command, options.resolution, fps) 136
137 if (options.hlsPlaylist) {
138 command = await buildHLSCommand(command, options)
139 } else {
140 command = await buildx264Command(command, options)
141 }
133 142
134 if (CONFIG.TRANSCODING.THREADS > 0) { 143 if (CONFIG.TRANSCODING.THREADS > 0) {
135 // if we don't set any threads ffmpeg will chose automatically 144 // if we don't set any threads ffmpeg will chose automatically
136 command = command.outputOption('-threads ' + CONFIG.TRANSCODING.THREADS) 145 command = command.outputOption('-threads ' + CONFIG.TRANSCODING.THREADS)
137 } 146 }
138 147
139 if (options.resolution !== undefined) {
140 // '?x720' or '720x?' for example
141 const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}`
142 command = command.size(size)
143 }
144
145 if (fps) {
146 // Hard FPS limits
147 if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = VIDEO_TRANSCODING_FPS.MAX
148 else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN
149
150 command = command.withFPS(fps)
151 }
152
153 command 148 command
154 .on('error', (err, stdout, stderr) => { 149 .on('error', (err, stdout, stderr) => {
155 logger.error('Error in transcoding job.', { stdout, stderr }) 150 logger.error('Error in transcoding job.', { stdout, stderr })
156 return rej(err) 151 return rej(err)
157 }) 152 })
158 .on('end', res) 153 .on('end', () => {
154 return onTranscodingSuccess(options)
155 .then(() => res())
156 .catch(err => rej(err))
157 })
159 .run() 158 .run()
160 } catch (err) { 159 } catch (err) {
161 return rej(err) 160 return rej(err)
@@ -166,6 +165,7 @@ function transcode (options: TranscodeOptions) {
166// --------------------------------------------------------------------------- 165// ---------------------------------------------------------------------------
167 166
168export { 167export {
168 getVideoFileSize,
169 getVideoFileResolution, 169 getVideoFileResolution,
170 getDurationFromVideoFile, 170 getDurationFromVideoFile,
171 generateImageFromVideoFile, 171 generateImageFromVideoFile,
@@ -178,6 +178,71 @@ export {
178 178
179// --------------------------------------------------------------------------- 179// ---------------------------------------------------------------------------
180 180
181async function buildx264Command (command: ffmpeg.FfmpegCommand, options: TranscodeOptions) {
182 let fps = await getVideoFileFPS(options.inputPath)
183 // On small/medium resolutions, limit FPS
184 if (
185 options.resolution !== undefined &&
186 options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN &&
187 fps > VIDEO_TRANSCODING_FPS.AVERAGE
188 ) {
189 fps = VIDEO_TRANSCODING_FPS.AVERAGE
190 }
191
192 command = await presetH264(command, options.resolution, fps)
193
194 if (options.resolution !== undefined) {
195 // '?x720' or '720x?' for example
196 const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}`
197 command = command.size(size)
198 }
199
200 if (fps) {
201 // Hard FPS limits
202 if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = VIDEO_TRANSCODING_FPS.MAX
203 else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN
204
205 command = command.withFPS(fps)
206 }
207
208 return command
209}
210
211async function buildHLSCommand (command: ffmpeg.FfmpegCommand, options: TranscodeOptions) {
212 const videoPath = getHLSVideoPath(options)
213
214 command = await presetCopy(command)
215
216 command = command.outputOption('-hls_time 4')
217 .outputOption('-hls_list_size 0')
218 .outputOption('-hls_playlist_type vod')
219 .outputOption('-hls_segment_filename ' + videoPath)
220 .outputOption('-hls_segment_type fmp4')
221 .outputOption('-f hls')
222 .outputOption('-hls_flags single_file')
223
224 return command
225}
226
227function getHLSVideoPath (options: TranscodeOptions) {
228 return `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}`
229}
230
231async function onTranscodingSuccess (options: TranscodeOptions) {
232 if (!options.hlsPlaylist) return
233
234 // Fix wrong mapping with some ffmpeg versions
235 const fileContent = await readFile(options.outputPath)
236
237 const videoFileName = options.hlsPlaylist.videoFilename
238 const videoFilePath = getHLSVideoPath(options)
239
240 const newContent = fileContent.toString()
241 .replace(`#EXT-X-MAP:URI="${videoFilePath}",`, `#EXT-X-MAP:URI="${videoFileName}",`)
242
243 await writeFile(options.outputPath, newContent)
244}
245
181function getVideoFileStream (path: string) { 246function getVideoFileStream (path: string) {
182 return new Promise<any>((res, rej) => { 247 return new Promise<any>((res, rej) => {
183 ffmpeg.ffprobe(path, (err, metadata) => { 248 ffmpeg.ffprobe(path, (err, metadata) => {
@@ -348,3 +413,10 @@ async function presetH264 (command: ffmpeg.FfmpegCommand, resolution: VideoResol
348 413
349 return localCommand 414 return localCommand
350} 415}
416
417async function presetCopy (command: ffmpeg.FfmpegCommand): Promise<ffmpeg.FfmpegCommand> {
418 return command
419 .format('mp4')
420 .videoCodec('copy')
421 .audioCodec('copy')
422}
diff --git a/server/helpers/image-utils.ts b/server/helpers/image-utils.ts
index e43ea3f1d..bd81aa3ba 100644
--- a/server/helpers/image-utils.ts
+++ b/server/helpers/image-utils.ts
@@ -4,18 +4,19 @@ import { readFile, remove } from 'fs-extra'
4import { logger } from './logger' 4import { logger } from './logger'
5 5
6async function processImage ( 6async function processImage (
7 physicalFile: { path: string }, 7 path: string,
8 destination: string, 8 destination: string,
9 newSize: { width: number, height: number } 9 newSize: { width: number, height: number },
10 keepOriginal = false
10) { 11) {
11 if (physicalFile.path === destination) { 12 if (path === destination) {
12 throw new Error('Sharp needs an input path different that the output path.') 13 throw new Error('Sharp needs an input path different that the output path.')
13 } 14 }
14 15
15 logger.debug('Processing image %s to %s.', physicalFile.path, destination) 16 logger.debug('Processing image %s to %s.', path, destination)
16 17
17 // Avoid sharp cache 18 // Avoid sharp cache
18 const buf = await readFile(physicalFile.path) 19 const buf = await readFile(path)
19 const sharpInstance = sharp(buf) 20 const sharpInstance = sharp(buf)
20 21
21 await remove(destination) 22 await remove(destination)
@@ -24,7 +25,7 @@ async function processImage (
24 .resize(newSize.width, newSize.height) 25 .resize(newSize.width, newSize.height)
25 .toFile(destination) 26 .toFile(destination)
26 27
27 await remove(physicalFile.path) 28 if (keepOriginal !== true) await remove(path)
28} 29}
29 30
30// --------------------------------------------------------------------------- 31// ---------------------------------------------------------------------------
diff --git a/server/helpers/logger.ts b/server/helpers/logger.ts
index 203e637a8..734523b01 100644
--- a/server/helpers/logger.ts
+++ b/server/helpers/logger.ts
@@ -2,11 +2,13 @@
2import { mkdirpSync } from 'fs-extra' 2import { mkdirpSync } from 'fs-extra'
3import * as path from 'path' 3import * as path from 'path'
4import * as winston from 'winston' 4import * as winston from 'winston'
5import { CONFIG } from '../initializers' 5import { CONFIG } from '../initializers/config'
6import { omit } from 'lodash'
6 7
7const label = CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT 8const label = CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
8 9
9// Create the directory if it does not exist 10// Create the directory if it does not exist
11// FIXME: use async
10mkdirpSync(CONFIG.STORAGE.LOG_DIR) 12mkdirpSync(CONFIG.STORAGE.LOG_DIR)
11 13
12function loggerReplacer (key: string, value: any) { 14function loggerReplacer (key: string, value: any) {
@@ -22,13 +24,10 @@ function loggerReplacer (key: string, value: any) {
22} 24}
23 25
24const consoleLoggerFormat = winston.format.printf(info => { 26const consoleLoggerFormat = winston.format.printf(info => {
25 const obj = { 27 const obj = omit(info, 'label', 'timestamp', 'level', 'message')
26 meta: info.meta,
27 err: info.err,
28 sql: info.sql
29 }
30 28
31 let additionalInfos = JSON.stringify(obj, loggerReplacer, 2) 29 let additionalInfos = JSON.stringify(obj, loggerReplacer, 2)
30
32 if (additionalInfos === undefined || additionalInfos === '{}') additionalInfos = '' 31 if (additionalInfos === undefined || additionalInfos === '{}') additionalInfos = ''
33 else additionalInfos = ' ' + additionalInfos 32 else additionalInfos = ' ' + additionalInfos
34 33
@@ -57,7 +56,7 @@ const logger = winston.createLogger({
57 filename: path.join(CONFIG.STORAGE.LOG_DIR, 'peertube.log'), 56 filename: path.join(CONFIG.STORAGE.LOG_DIR, 'peertube.log'),
58 handleExceptions: true, 57 handleExceptions: true,
59 maxsize: 1024 * 1024 * 12, 58 maxsize: 1024 * 1024 * 12,
60 maxFiles: 5, 59 maxFiles: 20,
61 format: winston.format.combine( 60 format: winston.format.combine(
62 winston.format.timestamp(), 61 winston.format.timestamp(),
63 jsonLoggerFormat 62 jsonLoggerFormat
diff --git a/server/helpers/peertube-crypto.ts b/server/helpers/peertube-crypto.ts
index ab9ec077e..9148df2eb 100644
--- a/server/helpers/peertube-crypto.ts
+++ b/server/helpers/peertube-crypto.ts
@@ -1,5 +1,5 @@
1import { Request } from 'express' 1import { Request } from 'express'
2import { BCRYPT_SALT_SIZE, HTTP_SIGNATURE, PRIVATE_RSA_KEY_SIZE } from '../initializers' 2import { BCRYPT_SALT_SIZE, HTTP_SIGNATURE, PRIVATE_RSA_KEY_SIZE } from '../initializers/constants'
3import { ActorModel } from '../models/activitypub/actor' 3import { ActorModel } from '../models/activitypub/actor'
4import { bcryptComparePromise, bcryptGenSaltPromise, bcryptHashPromise, createPrivateKey, getPublicKey, sha256 } from './core-utils' 4import { bcryptComparePromise, bcryptGenSaltPromise, bcryptHashPromise, createPrivateKey, getPublicKey, sha256 } from './core-utils'
5import { jsig, jsonld } from './custom-jsonld-signature' 5import { jsig, jsonld } from './custom-jsonld-signature'
diff --git a/server/helpers/requests.ts b/server/helpers/requests.ts
index 3fc776f1a..2e30c94a1 100644
--- a/server/helpers/requests.ts
+++ b/server/helpers/requests.ts
@@ -1,13 +1,16 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { createWriteStream } from 'fs-extra' 2import { createWriteStream, remove } from 'fs-extra'
3import * as request from 'request' 3import * as request from 'request'
4import { ACTIVITY_PUB, CONFIG } from '../initializers' 4import { ACTIVITY_PUB } from '../initializers/constants'
5import { processImage } from './image-utils' 5import { processImage } from './image-utils'
6import { join } from 'path' 6import { join } from 'path'
7import { logger } from './logger'
8import { CONFIG } from '../initializers/config'
7 9
8function doRequest <T> ( 10function doRequest <T> (
9 requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean } 11 requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean },
10): Bluebird<{ response: request.RequestResponse, body: any }> { 12 bodyKBLimit = 1000 // 1MB
13): Bluebird<{ response: request.RequestResponse, body: T }> {
11 if (requestOptions.activityPub === true) { 14 if (requestOptions.activityPub === true) {
12 if (!Array.isArray(requestOptions.headers)) requestOptions.headers = {} 15 if (!Array.isArray(requestOptions.headers)) requestOptions.headers = {}
13 requestOptions.headers['accept'] = ACTIVITY_PUB.ACCEPT_HEADER 16 requestOptions.headers['accept'] = ACTIVITY_PUB.ACCEPT_HEADER
@@ -15,16 +18,29 @@ function doRequest <T> (
15 18
16 return new Bluebird<{ response: request.RequestResponse, body: T }>((res, rej) => { 19 return new Bluebird<{ response: request.RequestResponse, body: T }>((res, rej) => {
17 request(requestOptions, (err, response, body) => err ? rej(err) : res({ response, body })) 20 request(requestOptions, (err, response, body) => err ? rej(err) : res({ response, body }))
21 .on('data', onRequestDataLengthCheck(bodyKBLimit))
18 }) 22 })
19} 23}
20 24
21function doRequestAndSaveToFile (requestOptions: request.CoreOptions & request.UriOptions, destPath: string) { 25function doRequestAndSaveToFile (
26 requestOptions: request.CoreOptions & request.UriOptions,
27 destPath: string,
28 bodyKBLimit = 10000 // 10MB
29) {
22 return new Bluebird<void>((res, rej) => { 30 return new Bluebird<void>((res, rej) => {
23 const file = createWriteStream(destPath) 31 const file = createWriteStream(destPath)
24 file.on('finish', () => res()) 32 file.on('finish', () => res())
25 33
26 request(requestOptions) 34 request(requestOptions)
27 .on('error', err => rej(err)) 35 .on('data', onRequestDataLengthCheck(bodyKBLimit))
36 .on('error', err => {
37 file.close()
38
39 remove(destPath)
40 .catch(err => logger.error('Cannot remove %s after request failure.', destPath, { err }))
41
42 return rej(err)
43 })
28 .pipe(file) 44 .pipe(file)
29 }) 45 })
30} 46}
@@ -34,7 +50,14 @@ async function downloadImage (url: string, destDir: string, destName: string, si
34 await doRequestAndSaveToFile({ method: 'GET', uri: url }, tmpPath) 50 await doRequestAndSaveToFile({ method: 'GET', uri: url }, tmpPath)
35 51
36 const destPath = join(destDir, destName) 52 const destPath = join(destDir, destName)
37 await processImage({ path: tmpPath }, destPath, size) 53
54 try {
55 await processImage(tmpPath, destPath, size)
56 } catch (err) {
57 await remove(tmpPath)
58
59 throw err
60 }
38} 61}
39 62
40// --------------------------------------------------------------------------- 63// ---------------------------------------------------------------------------
@@ -44,3 +67,21 @@ export {
44 doRequestAndSaveToFile, 67 doRequestAndSaveToFile,
45 downloadImage 68 downloadImage
46} 69}
70
71// ---------------------------------------------------------------------------
72
73// Thanks to https://github.com/request/request/issues/2470#issuecomment-268929907 <3
74function onRequestDataLengthCheck (bodyKBLimit: number) {
75 let bufferLength = 0
76 const bytesLimit = bodyKBLimit * 1000
77
78 return function (chunk) {
79 bufferLength += chunk.length
80 if (bufferLength > bytesLimit) {
81 this.abort()
82
83 const error = new Error(`Response was too large - aborted after ${bytesLimit} bytes.`)
84 this.emit('error', error)
85 }
86 }
87}
diff --git a/server/helpers/signup.ts b/server/helpers/signup.ts
index cdce7989d..5eb56b3cf 100644
--- a/server/helpers/signup.ts
+++ b/server/helpers/signup.ts
@@ -1,6 +1,7 @@
1import { CONFIG } from '../initializers'
2import { UserModel } from '../models/account/user' 1import { UserModel } from '../models/account/user'
3import * as ipaddr from 'ipaddr.js' 2import * as ipaddr from 'ipaddr.js'
3import { CONFIG } from '../initializers/config'
4
4const isCidr = require('is-cidr') 5const isCidr = require('is-cidr')
5 6
6async function isSignupAllowed () { 7async function isSignupAllowed () {
diff --git a/server/helpers/utils.ts b/server/helpers/utils.ts
index 3c3406e38..94ceb15e0 100644
--- a/server/helpers/utils.ts
+++ b/server/helpers/utils.ts
@@ -1,5 +1,4 @@
1import { ResultList } from '../../shared' 1import { ResultList } from '../../shared'
2import { CONFIG } from '../initializers'
3import { ApplicationModel } from '../models/application/application' 2import { ApplicationModel } from '../models/application/application'
4import { execPromise, execPromise2, pseudoRandomBytesPromise, sha256 } from './core-utils' 3import { execPromise, execPromise2, pseudoRandomBytesPromise, sha256 } from './core-utils'
5import { logger } from './logger' 4import { logger } from './logger'
@@ -7,7 +6,7 @@ import { join } from 'path'
7import { Instance as ParseTorrent } from 'parse-torrent' 6import { Instance as ParseTorrent } from 'parse-torrent'
8import { remove } from 'fs-extra' 7import { remove } from 'fs-extra'
9import * as memoizee from 'memoizee' 8import * as memoizee from 'memoizee'
10import { isArray } from './custom-validators/misc' 9import { CONFIG } from '../initializers/config'
11 10
12function deleteFileAsync (path: string) { 11function deleteFileAsync (path: string) {
13 remove(path) 12 remove(path)
diff --git a/server/helpers/video.ts b/server/helpers/video.ts
index 1bd21467d..c90fe06c7 100644
--- a/server/helpers/video.ts
+++ b/server/helpers/video.ts
@@ -1,10 +1,12 @@
1import { VideoModel } from '../models/video/video' 1import { VideoModel } from '../models/video/video'
2 2
3type VideoFetchType = 'all' | 'only-video' | 'id' | 'none' 3type VideoFetchType = 'all' | 'only-video' | 'only-video-with-rights' | 'id' | 'none'
4 4
5function fetchVideo (id: number | string, fetchType: VideoFetchType, userId?: number) { 5function fetchVideo (id: number | string, fetchType: VideoFetchType, userId?: number) {
6 if (fetchType === 'all') return VideoModel.loadAndPopulateAccountAndServerAndTags(id, undefined, userId) 6 if (fetchType === 'all') return VideoModel.loadAndPopulateAccountAndServerAndTags(id, undefined, userId)
7 7
8 if (fetchType === 'only-video-with-rights') return VideoModel.loadWithRights(id)
9
8 if (fetchType === 'only-video') return VideoModel.load(id) 10 if (fetchType === 'only-video') return VideoModel.load(id)
9 11
10 if (fetchType === 'id' || fetchType === 'none') return VideoModel.loadOnlyId(id) 12 if (fetchType === 'id' || fetchType === 'none') return VideoModel.loadOnlyId(id)
diff --git a/server/helpers/webfinger.ts b/server/helpers/webfinger.ts
index 156376943..049808846 100644
--- a/server/helpers/webfinger.ts
+++ b/server/helpers/webfinger.ts
@@ -3,7 +3,7 @@ import { WebFingerData } from '../../shared'
3import { ActorModel } from '../models/activitypub/actor' 3import { ActorModel } from '../models/activitypub/actor'
4import { isTestInstance } from './core-utils' 4import { isTestInstance } from './core-utils'
5import { isActivityPubUrlValid } from './custom-validators/activitypub/misc' 5import { isActivityPubUrlValid } from './custom-validators/activitypub/misc'
6import { CONFIG } from '../initializers' 6import { WEBSERVER } from '../initializers/constants'
7 7
8const webfinger = new WebFinger({ 8const webfinger = new WebFinger({
9 webfist_fallback: false, 9 webfist_fallback: false,
@@ -19,7 +19,7 @@ async function loadActorUrlOrGetFromWebfinger (uriArg: string) {
19 const [ name, host ] = uri.split('@') 19 const [ name, host ] = uri.split('@')
20 let actor: ActorModel 20 let actor: ActorModel
21 21
22 if (host === CONFIG.WEBSERVER.HOST) { 22 if (host === WEBSERVER.HOST) {
23 actor = await ActorModel.loadLocalByName(name) 23 actor = await ActorModel.loadLocalByName(name)
24 } else { 24 } else {
25 actor = await ActorModel.loadByNameAndHost(name, host) 25 actor = await ActorModel.loadByNameAndHost(name, host)
diff --git a/server/helpers/webtorrent.ts b/server/helpers/webtorrent.ts
index 3c9a0b96a..14dfe0d28 100644
--- a/server/helpers/webtorrent.ts
+++ b/server/helpers/webtorrent.ts
@@ -2,7 +2,7 @@ import { logger } from './logger'
2import { generateVideoImportTmpPath } 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/config'
6import { dirname, join } from 'path' 6import { dirname, join } from 'path'
7 7
8async function downloadWebTorrentVideo (target: { magnetUri: string, torrentName?: string }, timeout: number) { 8async function downloadWebTorrentVideo (target: { magnetUri: string, torrentName?: string }, timeout: number) {
diff --git a/server/helpers/youtube-dl.ts b/server/helpers/youtube-dl.ts
index b74351b42..b3079370f 100644
--- a/server/helpers/youtube-dl.ts
+++ b/server/helpers/youtube-dl.ts
@@ -1,5 +1,5 @@
1import { truncate } from 'lodash' 1import { truncate } from 'lodash'
2import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES } from '../initializers' 2import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES } from '../initializers/constants'
3import { logger } from './logger' 3import { logger } from './logger'
4import { generateVideoImportTmpPath } from './utils' 4import { generateVideoImportTmpPath } from './utils'
5import { join } from 'path' 5import { join } from 'path'
@@ -16,6 +16,7 @@ export type YoutubeDLInfo = {
16 nsfw?: boolean 16 nsfw?: boolean
17 tags?: string[] 17 tags?: string[]
18 thumbnailUrl?: string 18 thumbnailUrl?: string
19 originallyPublishedAt?: Date
19} 20}
20 21
21const processOptions = { 22const processOptions = {
@@ -47,6 +48,11 @@ function downloadYoutubeDLVideo (url: string, timeout: number) {
47 48
48 const options = [ '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best', '-o', path ] 49 const options = [ '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best', '-o', path ]
49 50
51 if (process.env.FFMPEG_PATH) {
52 options.push('--ffmpeg-location')
53 options.push(process.env.FFMPEG_PATH)
54 }
55
50 return new Promise<string>(async (res, rej) => { 56 return new Promise<string>(async (res, rej) => {
51 const youtubeDL = await safeGetYoutubeDL() 57 const youtubeDL = await safeGetYoutubeDL()
52 youtubeDL.exec(url, options, processOptions, err => { 58 youtubeDL.exec(url, options, processOptions, err => {
@@ -142,13 +148,33 @@ async function safeGetYoutubeDL () {
142 return youtubeDL 148 return youtubeDL
143} 149}
144 150
151function buildOriginallyPublishedAt (obj: any) {
152 let originallyPublishedAt: Date = null
153
154 const uploadDateMatcher = /^(\d{4})(\d{2})(\d{2})$/.exec(obj.upload_date)
155 if (uploadDateMatcher) {
156 originallyPublishedAt = new Date()
157 originallyPublishedAt.setHours(0, 0, 0, 0)
158
159 const year = parseInt(uploadDateMatcher[1], 10)
160 // Month starts from 0
161 const month = parseInt(uploadDateMatcher[2], 10) - 1
162 const day = parseInt(uploadDateMatcher[3], 10)
163
164 originallyPublishedAt.setFullYear(year, month, day)
165 }
166
167 return originallyPublishedAt
168}
169
145// --------------------------------------------------------------------------- 170// ---------------------------------------------------------------------------
146 171
147export { 172export {
148 updateYoutubeDLBinary, 173 updateYoutubeDLBinary,
149 downloadYoutubeDLVideo, 174 downloadYoutubeDLVideo,
150 getYoutubeDLInfo, 175 getYoutubeDLInfo,
151 safeGetYoutubeDL 176 safeGetYoutubeDL,
177 buildOriginallyPublishedAt
152} 178}
153 179
154// --------------------------------------------------------------------------- 180// ---------------------------------------------------------------------------
@@ -180,7 +206,8 @@ function buildVideoInfo (obj: any) {
180 licence: getLicence(obj.license), 206 licence: getLicence(obj.license),
181 nsfw: isNSFW(obj), 207 nsfw: isNSFW(obj),
182 tags: getTags(obj.tags), 208 tags: getTags(obj.tags),
183 thumbnailUrl: obj.thumbnail || undefined 209 thumbnailUrl: obj.thumbnail || undefined,
210 originallyPublishedAt: buildOriginallyPublishedAt(obj)
184 } 211 }
185} 212}
186 213
diff --git a/server/initializers/checker-after-init.ts b/server/initializers/checker-after-init.ts
index 955d55206..db3115085 100644
--- a/server/initializers/checker-after-init.ts
+++ b/server/initializers/checker-after-init.ts
@@ -4,19 +4,20 @@ import { UserModel } from '../models/account/user'
4import { ApplicationModel } from '../models/application/application' 4import { ApplicationModel } from '../models/application/application'
5import { OAuthClientModel } from '../models/oauth/oauth-client' 5import { OAuthClientModel } from '../models/oauth/oauth-client'
6import { parse } from 'url' 6import { parse } from 'url'
7import { CONFIG } from './constants' 7import { CONFIG } from './config'
8import { logger } from '../helpers/logger' 8import { logger } from '../helpers/logger'
9import { getServerActor } from '../helpers/utils' 9import { 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' 13import { Emailer } from '../lib/emailer'
14import { WEBSERVER } from './constants'
14 15
15async function checkActivityPubUrls () { 16async function checkActivityPubUrls () {
16 const actor = await getServerActor() 17 const actor = await getServerActor()
17 18
18 const parsed = parse(actor.url) 19 const parsed = parse(actor.url)
19 if (CONFIG.WEBSERVER.HOST !== parsed.host) { 20 if (WEBSERVER.HOST !== parsed.host) {
20 const NODE_ENV = config.util.getEnv('NODE_ENV') 21 const NODE_ENV = config.util.getEnv('NODE_ENV')
21 const NODE_CONFIG_DIR = config.util.getEnv('NODE_CONFIG_DIR') 22 const NODE_CONFIG_DIR = config.util.getEnv('NODE_CONFIG_DIR')
22 23
@@ -34,6 +35,12 @@ async function checkActivityPubUrls () {
34// Return an error message, or null if everything is okay 35// Return an error message, or null if everything is okay
35function checkConfig () { 36function checkConfig () {
36 37
38 // Moved configuration keys
39 if (config.has('services.csp-logger')) {
40 logger.warn('services.csp-logger configuration has been renamed to csp.report_uri. Please update your configuration file.')
41 }
42
43 // Email verification
37 if (!Emailer.isEnabled()) { 44 if (!Emailer.isEnabled()) {
38 if (CONFIG.SIGNUP.ENABLED && CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) { 45 if (CONFIG.SIGNUP.ENABLED && CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) {
39 return 'Emailer is disabled but you require signup email verification.' 46 return 'Emailer is disabled but you require signup email verification.'
@@ -77,6 +84,8 @@ function checkConfig () {
77 if (recentlyAddedStrategy && isNaN(recentlyAddedStrategy.minViews)) { 84 if (recentlyAddedStrategy && isNaN(recentlyAddedStrategy.minViews)) {
78 return 'Min views in recently added strategy is not a number' 85 return 'Min views in recently added strategy is not a number'
79 } 86 }
87 } else {
88 return 'Videos redundancy should be an array (you must uncomment lines containing - too)'
80 } 89 }
81 90
82 // Check storage directory locations 91 // Check storage directory locations
diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts
index 7905d9ffa..622ad7d6b 100644
--- a/server/initializers/checker-before-init.ts
+++ b/server/initializers/checker-before-init.ts
@@ -1,6 +1,5 @@
1import * as config from 'config' 1import * as config from 'config'
2import { promisify0 } from '../helpers/core-utils' 2import { promisify0 } from '../helpers/core-utils'
3import { isArray } from '../helpers/custom-validators/misc'
4 3
5// ONLY USE CORE MODULES IN THIS FILE! 4// ONLY USE CORE MODULES IN THIS FILE!
6 5
@@ -12,19 +11,24 @@ function checkMissedConfig () {
12 'database.hostname', 'database.port', 'database.suffix', 'database.username', 'database.password', 'database.pool.max', 11 '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', 12 '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', 13 'storage.avatars', 'storage.videos', 'storage.logs', 'storage.previews', 'storage.thumbnails', 'storage.torrents', 'storage.cache',
15 'storage.redundancy', 'storage.tmp', 14 'storage.redundancy', 'storage.tmp', 'storage.streaming_playlists',
16 'log.level', 15 'log.level',
17 'user.video_quota', 'user.video_quota_daily', 16 'user.video_quota', 'user.video_quota_daily',
17 'csp.enabled', 'csp.report_only', 'csp.report_uri',
18 'cache.previews.size', 'admin.email', 'contact_form.enabled', 18 'cache.previews.size', 'admin.email', 'contact_form.enabled',
19 'signup.enabled', 'signup.limit', 'signup.requires_email_verification', 19 'signup.enabled', 'signup.limit', 'signup.requires_email_verification',
20 'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist', 20 'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist',
21 'redundancy.videos.strategies', 'redundancy.videos.check_interval', 21 'redundancy.videos.strategies', 'redundancy.videos.check_interval',
22 'transcoding.enabled', 'transcoding.threads', 'transcoding.allow_additional_extensions', 22 'transcoding.enabled', 'transcoding.threads', 'transcoding.allow_additional_extensions',
23 'import.videos.http.enabled', 'import.videos.torrent.enabled', 23 'import.videos.http.enabled', 'import.videos.torrent.enabled', 'auto_blacklist.videos.of_users.enabled',
24 'trending.videos.interval_days', 24 'trending.videos.interval_days',
25 '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',
26 'instance.default_nsfw_policy', 'instance.robots', 'instance.securitytxt', 26 'instance.is_nsfw', 'instance.default_nsfw_policy', 'instance.robots', 'instance.securitytxt',
27 'services.twitter.username', 'services.twitter.whitelisted' 27 'services.twitter.username', 'services.twitter.whitelisted',
28 'followers.instance.enabled', 'followers.instance.manual_approval',
29 'tracker.enabled', 'tracker.private', 'tracker.reject_too_many_announces',
30 'history.videos.max_age', 'views.videos.remote.max_age',
31 'rates_limit.login.window', 'rates_limit.login.max', 'rates_limit.ask_send_email.window', 'rates_limit.ask_send_email.max'
28 ] 32 ]
29 const requiredAlternatives = [ 33 const requiredAlternatives = [
30 [ // set 34 [ // set
@@ -41,7 +45,8 @@ function checkMissedConfig () {
41 } 45 }
42 46
43 const redundancyVideos = config.get<any>('redundancy.videos.strategies') 47 const redundancyVideos = config.get<any>('redundancy.videos.strategies')
44 if (isArray(redundancyVideos)) { 48
49 if (Array.isArray(redundancyVideos)) {
45 for (const r of redundancyVideos) { 50 for (const r of redundancyVideos) {
46 if (!r.size) miss.push('redundancy.videos.strategies.size') 51 if (!r.size) miss.push('redundancy.videos.strategies.size')
47 if (!r.min_lifetime) miss.push('redundancy.videos.strategies.min_lifetime') 52 if (!r.min_lifetime) miss.push('redundancy.videos.strategies.min_lifetime')
diff --git a/server/initializers/config.ts b/server/initializers/config.ts
new file mode 100644
index 000000000..4f77e144d
--- /dev/null
+++ b/server/initializers/config.ts
@@ -0,0 +1,277 @@
1import { IConfig } from 'config'
2import { dirname, join } from 'path'
3import { VideosRedundancy } from '../../shared/models'
4// Do not use barrels, remain constants as independent as possible
5import { buildPath, parseBytes, parseDurationToMs, root } from '../helpers/core-utils'
6import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type'
7import * as bytes from 'bytes'
8
9// Use a variable to reload the configuration if we need
10let config: IConfig = require('config')
11
12const configChangedHandlers: Function[] = []
13
14const CONFIG = {
15 CUSTOM_FILE: getLocalConfigFilePath(),
16 LISTEN: {
17 PORT: config.get<number>('listen.port'),
18 HOSTNAME: config.get<string>('listen.hostname')
19 },
20 DATABASE: {
21 DBNAME: 'peertube' + config.get<string>('database.suffix'),
22 HOSTNAME: config.get<string>('database.hostname'),
23 PORT: config.get<number>('database.port'),
24 USERNAME: config.get<string>('database.username'),
25 PASSWORD: config.get<string>('database.password'),
26 POOL: {
27 MAX: config.get<number>('database.pool.max')
28 }
29 },
30 REDIS: {
31 HOSTNAME: config.has('redis.hostname') ? config.get<string>('redis.hostname') : null,
32 PORT: config.has('redis.port') ? config.get<number>('redis.port') : null,
33 SOCKET: config.has('redis.socket') ? config.get<string>('redis.socket') : null,
34 AUTH: config.has('redis.auth') ? config.get<string>('redis.auth') : null,
35 DB: config.has('redis.db') ? config.get<number>('redis.db') : null
36 },
37 SMTP: {
38 HOSTNAME: config.get<string>('smtp.hostname'),
39 PORT: config.get<number>('smtp.port'),
40 USERNAME: config.get<string>('smtp.username'),
41 PASSWORD: config.get<string>('smtp.password'),
42 TLS: config.get<boolean>('smtp.tls'),
43 DISABLE_STARTTLS: config.get<boolean>('smtp.disable_starttls'),
44 CA_FILE: config.get<string>('smtp.ca_file'),
45 FROM_ADDRESS: config.get<string>('smtp.from_address')
46 },
47 STORAGE: {
48 TMP_DIR: buildPath(config.get<string>('storage.tmp')),
49 AVATARS_DIR: buildPath(config.get<string>('storage.avatars')),
50 LOG_DIR: buildPath(config.get<string>('storage.logs')),
51 VIDEOS_DIR: buildPath(config.get<string>('storage.videos')),
52 STREAMING_PLAYLISTS_DIR: buildPath(config.get<string>('storage.streaming_playlists')),
53 REDUNDANCY_DIR: buildPath(config.get<string>('storage.redundancy')),
54 THUMBNAILS_DIR: buildPath(config.get<string>('storage.thumbnails')),
55 PREVIEWS_DIR: buildPath(config.get<string>('storage.previews')),
56 CAPTIONS_DIR: buildPath(config.get<string>('storage.captions')),
57 TORRENTS_DIR: buildPath(config.get<string>('storage.torrents')),
58 CACHE_DIR: buildPath(config.get<string>('storage.cache'))
59 },
60 WEBSERVER: {
61 SCHEME: config.get<boolean>('webserver.https') === true ? 'https' : 'http',
62 WS: config.get<boolean>('webserver.https') === true ? 'wss' : 'ws',
63 HOSTNAME: config.get<string>('webserver.hostname'),
64 PORT: config.get<number>('webserver.port')
65 },
66 RATES_LIMIT: {
67 LOGIN: {
68 WINDOW_MS: parseDurationToMs(config.get<string>('rates_limit.login.window')),
69 MAX: config.get<number>('rates_limit.login.max')
70 },
71 ASK_SEND_EMAIL: {
72 WINDOW_MS: parseDurationToMs(config.get<string>('rates_limit.ask_send_email.window')),
73 MAX: config.get<number>('rates_limit.ask_send_email.max')
74 }
75 },
76 TRUST_PROXY: config.get<string[]>('trust_proxy'),
77 LOG: {
78 LEVEL: config.get<string>('log.level')
79 },
80 SEARCH: {
81 REMOTE_URI: {
82 USERS: config.get<boolean>('search.remote_uri.users'),
83 ANONYMOUS: config.get<boolean>('search.remote_uri.anonymous')
84 }
85 },
86 TRENDING: {
87 VIDEOS: {
88 INTERVAL_DAYS: config.get<number>('trending.videos.interval_days')
89 }
90 },
91 REDUNDANCY: {
92 VIDEOS: {
93 CHECK_INTERVAL: parseDurationToMs(config.get<string>('redundancy.videos.check_interval')),
94 STRATEGIES: buildVideosRedundancy(config.get<any[]>('redundancy.videos.strategies'))
95 }
96 },
97 CSP: {
98 ENABLED: config.get<boolean>('csp.enabled'),
99 REPORT_ONLY: config.get<boolean>('csp.report_only'),
100 REPORT_URI: config.get<boolean>('csp.report_uri')
101 },
102 TRACKER: {
103 ENABLED: config.get<boolean>('tracker.enabled'),
104 PRIVATE: config.get<boolean>('tracker.private'),
105 REJECT_TOO_MANY_ANNOUNCES: config.get<boolean>('tracker.reject_too_many_announces')
106 },
107 HISTORY: {
108 VIDEOS: {
109 MAX_AGE: parseDurationToMs(config.get('history.videos.max_age'))
110 }
111 },
112 VIEWS: {
113 VIDEOS: {
114 REMOTE: {
115 MAX_AGE: parseDurationToMs(config.get('views.videos.remote.max_age'))
116 }
117 }
118 },
119 ADMIN: {
120 get EMAIL () { return config.get<string>('admin.email') }
121 },
122 CONTACT_FORM: {
123 get ENABLED () { return config.get<boolean>('contact_form.enabled') }
124 },
125 SIGNUP: {
126 get ENABLED () { return config.get<boolean>('signup.enabled') },
127 get LIMIT () { return config.get<number>('signup.limit') },
128 get REQUIRES_EMAIL_VERIFICATION () { return config.get<boolean>('signup.requires_email_verification') },
129 FILTERS: {
130 CIDR: {
131 get WHITELIST () { return config.get<string[]>('signup.filters.cidr.whitelist') },
132 get BLACKLIST () { return config.get<string[]>('signup.filters.cidr.blacklist') }
133 }
134 }
135 },
136 USER: {
137 get VIDEO_QUOTA () { return parseBytes(config.get<number>('user.video_quota')) },
138 get VIDEO_QUOTA_DAILY () { return parseBytes(config.get<number>('user.video_quota_daily')) }
139 },
140 TRANSCODING: {
141 get ENABLED () { return config.get<boolean>('transcoding.enabled') },
142 get ALLOW_ADDITIONAL_EXTENSIONS () { return config.get<boolean>('transcoding.allow_additional_extensions') },
143 get THREADS () { return config.get<number>('transcoding.threads') },
144 RESOLUTIONS: {
145 get '240p' () { return config.get<boolean>('transcoding.resolutions.240p') },
146 get '360p' () { return config.get<boolean>('transcoding.resolutions.360p') },
147 get '480p' () { return config.get<boolean>('transcoding.resolutions.480p') },
148 get '720p' () { return config.get<boolean>('transcoding.resolutions.720p') },
149 get '1080p' () { return config.get<boolean>('transcoding.resolutions.1080p') }
150 },
151 HLS: {
152 get ENABLED () { return config.get<boolean>('transcoding.hls.enabled') }
153 }
154 },
155 IMPORT: {
156 VIDEOS: {
157 HTTP: {
158 get ENABLED () { return config.get<boolean>('import.videos.http.enabled') }
159 },
160 TORRENT: {
161 get ENABLED () { return config.get<boolean>('import.videos.torrent.enabled') }
162 }
163 }
164 },
165 AUTO_BLACKLIST: {
166 VIDEOS: {
167 OF_USERS: {
168 get ENABLED () { return config.get<boolean>('auto_blacklist.videos.of_users.enabled') }
169 }
170 }
171 },
172 CACHE: {
173 PREVIEWS: {
174 get SIZE () { return config.get<number>('cache.previews.size') }
175 },
176 VIDEO_CAPTIONS: {
177 get SIZE () { return config.get<number>('cache.captions.size') }
178 }
179 },
180 INSTANCE: {
181 get NAME () { return config.get<string>('instance.name') },
182 get SHORT_DESCRIPTION () { return config.get<string>('instance.short_description') },
183 get DESCRIPTION () { return config.get<string>('instance.description') },
184 get TERMS () { return config.get<string>('instance.terms') },
185 get IS_NSFW () { return config.get<boolean>('instance.is_nsfw') },
186 get DEFAULT_CLIENT_ROUTE () { return config.get<string>('instance.default_client_route') },
187 get DEFAULT_NSFW_POLICY () { return config.get<NSFWPolicyType>('instance.default_nsfw_policy') },
188 CUSTOMIZATIONS: {
189 get JAVASCRIPT () { return config.get<string>('instance.customizations.javascript') },
190 get CSS () { return config.get<string>('instance.customizations.css') }
191 },
192 get ROBOTS () { return config.get<string>('instance.robots') },
193 get SECURITYTXT () { return config.get<string>('instance.securitytxt') },
194 get SECURITYTXT_CONTACT () { return config.get<string>('admin.email') }
195 },
196 SERVICES: {
197 TWITTER: {
198 get USERNAME () { return config.get<string>('services.twitter.username') },
199 get WHITELISTED () { return config.get<boolean>('services.twitter.whitelisted') }
200 }
201 },
202 FOLLOWERS: {
203 INSTANCE: {
204 get ENABLED () { return config.get<boolean>('followers.instance.enabled') },
205 get MANUAL_APPROVAL () { return config.get<boolean>('followers.instance.manual_approval') }
206 }
207 }
208}
209
210function registerConfigChangedHandler (fun: Function) {
211 configChangedHandlers.push(fun)
212}
213
214// ---------------------------------------------------------------------------
215
216export {
217 CONFIG,
218 registerConfigChangedHandler
219}
220
221// ---------------------------------------------------------------------------
222
223function getLocalConfigFilePath () {
224 const configSources = config.util.getConfigSources()
225 if (configSources.length === 0) throw new Error('Invalid config source.')
226
227 let filename = 'local'
228 if (process.env.NODE_ENV) filename += `-${process.env.NODE_ENV}`
229 if (process.env.NODE_APP_INSTANCE) filename += `-${process.env.NODE_APP_INSTANCE}`
230
231 return join(dirname(configSources[ 0 ].name), filename + '.json')
232}
233
234function buildVideosRedundancy (objs: any[]): VideosRedundancy[] {
235 if (!objs) return []
236
237 if (!Array.isArray(objs)) return objs
238
239 return objs.map(obj => {
240 return Object.assign({}, obj, {
241 minLifetime: parseDurationToMs(obj.min_lifetime),
242 size: bytes.parse(obj.size),
243 minViews: obj.min_views
244 })
245 })
246}
247
248export function reloadConfig () {
249
250 function directory () {
251 if (process.env.NODE_CONFIG_DIR) {
252 return process.env.NODE_CONFIG_DIR
253 }
254
255 return join(root(), 'config')
256 }
257
258 function purge () {
259 for (const fileName in require.cache) {
260 if (-1 === fileName.indexOf(directory())) {
261 continue
262 }
263
264 delete require.cache[fileName]
265 }
266
267 delete require.cache[require.resolve('config')]
268 }
269
270 purge()
271
272 config = require('config')
273
274 for (const configChangedHandler of configChangedHandlers) {
275 configChangedHandler()
276 }
277}
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 6f3ebb9aa..62778ae58 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -1,22 +1,20 @@
1import { IConfig } from 'config' 1import { join } from 'path'
2import { dirname, join } from 'path' 2import { JobType, VideoRateType, VideoState } from '../../shared/models'
3import { JobType, VideoRateType, VideoState, VideosRedundancy } from '../../shared/models'
4import { ActivityPubActorType } from '../../shared/models/activitypub' 3import { ActivityPubActorType } from '../../shared/models/activitypub'
5import { FollowState } from '../../shared/models/actors' 4import { FollowState } from '../../shared/models/actors'
6import { VideoAbuseState, VideoImportState, VideoPrivacy, VideoTranscodingFPS } from '../../shared/models/videos' 5import { VideoAbuseState, VideoImportState, VideoPrivacy, VideoTranscodingFPS } from '../../shared/models/videos'
7// Do not use barrels, remain constants as independent as possible 6// Do not use barrels, remain constants as independent as possible
8import { buildPath, isTestInstance, parseDuration, parseBytes, root, sanitizeHost, sanitizeUrl } from '../helpers/core-utils' 7import { isTestInstance, sanitizeHost, sanitizeUrl } from '../helpers/core-utils'
9import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' 8import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type'
10import { invert } from 'lodash' 9import { invert } from 'lodash'
11import { CronRepeatOptions, EveryRepeatOptions } from 'bull' 10import { CronRepeatOptions, EveryRepeatOptions } from 'bull'
12import * as bytes from 'bytes' 11import { VideoPlaylistPrivacy } from '../../shared/models/videos/playlist/video-playlist-privacy.model'
13 12import { VideoPlaylistType } from '../../shared/models/videos/playlist/video-playlist-type.model'
14// Use a variable to reload the configuration if we need 13import { CONFIG, registerConfigChangedHandler } from './config'
15let config: IConfig = require('config')
16 14
17// --------------------------------------------------------------------------- 15// ---------------------------------------------------------------------------
18 16
19const LAST_MIGRATION_VERSION = 325 17const LAST_MIGRATION_VERSION = 375
20 18
21// --------------------------------------------------------------------------- 19// ---------------------------------------------------------------------------
22 20
@@ -30,6 +28,15 @@ const PAGINATION = {
30 } 28 }
31} 29}
32 30
31const WEBSERVER = {
32 URL: '',
33 HOST: '',
34 SCHEME: '',
35 WS: '',
36 HOSTNAME: '',
37 PORT: 0
38}
39
33// Sortable columns per schema 40// Sortable columns per schema
34const SORTABLE_COLUMNS = { 41const SORTABLE_COLUMNS = {
35 USERS: [ 'id', 'username', 'createdAt' ], 42 USERS: [ 'id', 'username', 'createdAt' ],
@@ -40,6 +47,7 @@ const SORTABLE_COLUMNS = {
40 VIDEO_CHANNELS: [ 'id', 'name', 'updatedAt', 'createdAt' ], 47 VIDEO_CHANNELS: [ 'id', 'name', 'updatedAt', 'createdAt' ],
41 VIDEO_IMPORTS: [ 'createdAt' ], 48 VIDEO_IMPORTS: [ 'createdAt' ],
42 VIDEO_COMMENT_THREADS: [ 'createdAt' ], 49 VIDEO_COMMENT_THREADS: [ 'createdAt' ],
50 VIDEO_RATES: [ 'createdAt' ],
43 BLACKLISTS: [ 'id', 'name', 'duration', 'views', 'likes', 'dislikes', 'uuid', 'createdAt' ], 51 BLACKLISTS: [ 'id', 'name', 'duration', 'views', 'likes', 'dislikes', 'uuid', 'createdAt' ],
44 FOLLOWERS: [ 'createdAt' ], 52 FOLLOWERS: [ 'createdAt' ],
45 FOLLOWING: [ 'createdAt' ], 53 FOLLOWING: [ 'createdAt' ],
@@ -52,7 +60,9 @@ const SORTABLE_COLUMNS = {
52 ACCOUNTS_BLOCKLIST: [ 'createdAt' ], 60 ACCOUNTS_BLOCKLIST: [ 'createdAt' ],
53 SERVERS_BLOCKLIST: [ 'createdAt' ], 61 SERVERS_BLOCKLIST: [ 'createdAt' ],
54 62
55 USER_NOTIFICATIONS: [ 'createdAt' ] 63 USER_NOTIFICATIONS: [ 'createdAt' ],
64
65 VIDEO_PLAYLISTS: [ 'displayName', 'createdAt', 'updatedAt' ]
56} 66}
57 67
58const OAUTH_LIFETIME = { 68const OAUTH_LIFETIME = {
@@ -96,36 +106,40 @@ const REMOTE_SCHEME = {
96 WS: 'wss' 106 WS: 'wss'
97} 107}
98 108
99const JOB_ATTEMPTS: { [ id in JobType ]: number } = { 109// TODO: remove 'video-file'
110const JOB_ATTEMPTS: { [id in (JobType | 'video-file')]: number } = {
100 'activitypub-http-broadcast': 5, 111 'activitypub-http-broadcast': 5,
101 'activitypub-http-unicast': 5, 112 'activitypub-http-unicast': 5,
102 'activitypub-http-fetcher': 5, 113 'activitypub-http-fetcher': 5,
103 'activitypub-follow': 5, 114 'activitypub-follow': 5,
104 'video-file-import': 1, 115 'video-file-import': 1,
116 'video-transcoding': 1,
105 'video-file': 1, 117 'video-file': 1,
106 'video-import': 1, 118 'video-import': 1,
107 'email': 5, 119 'email': 5,
108 'videos-views': 1, 120 'videos-views': 1,
109 'activitypub-refresher': 1 121 'activitypub-refresher': 1
110} 122}
111const JOB_CONCURRENCY: { [ id in JobType ]: number } = { 123const JOB_CONCURRENCY: { [id in (JobType | 'video-file')]: number } = {
112 'activitypub-http-broadcast': 1, 124 'activitypub-http-broadcast': 1,
113 'activitypub-http-unicast': 5, 125 'activitypub-http-unicast': 5,
114 'activitypub-http-fetcher': 1, 126 'activitypub-http-fetcher': 1,
115 'activitypub-follow': 3, 127 'activitypub-follow': 3,
116 'video-file-import': 1, 128 'video-file-import': 1,
129 'video-transcoding': 1,
117 'video-file': 1, 130 'video-file': 1,
118 'video-import': 1, 131 'video-import': 1,
119 'email': 5, 132 'email': 5,
120 'videos-views': 1, 133 'videos-views': 1,
121 'activitypub-refresher': 1 134 'activitypub-refresher': 1
122} 135}
123const JOB_TTL: { [ id in JobType ]: number } = { 136const JOB_TTL: { [id in (JobType | 'video-file')]: number } = {
124 'activitypub-http-broadcast': 60000 * 10, // 10 minutes 137 'activitypub-http-broadcast': 60000 * 10, // 10 minutes
125 'activitypub-http-unicast': 60000 * 10, // 10 minutes 138 'activitypub-http-unicast': 60000 * 10, // 10 minutes
126 'activitypub-http-fetcher': 60000 * 10, // 10 minutes 139 'activitypub-http-fetcher': 60000 * 10, // 10 minutes
127 'activitypub-follow': 60000 * 10, // 10 minutes 140 'activitypub-follow': 60000 * 10, // 10 minutes
128 'video-file-import': 1000 * 3600, // 1 hour 141 'video-file-import': 1000 * 3600, // 1 hour
142 'video-transcoding': 1000 * 3600 * 48, // 2 days, transcoding could be long
129 'video-file': 1000 * 3600 * 48, // 2 days, transcoding could be long 143 'video-file': 1000 * 3600 * 48, // 2 days, transcoding could be long
130 'video-import': 1000 * 3600 * 2, // hours 144 'video-import': 1000 * 3600 * 2, // hours
131 'email': 60000 * 10, // 10 minutes 145 'email': 60000 * 10, // 10 minutes
@@ -144,163 +158,13 @@ const JOB_REQUEST_TIMEOUT = 3000 // 3 seconds
144const JOB_COMPLETED_LIFETIME = 60000 * 60 * 24 * 2 // 2 days 158const JOB_COMPLETED_LIFETIME = 60000 * 60 * 24 * 2 // 2 days
145const VIDEO_IMPORT_TIMEOUT = 1000 * 3600 // 1 hour 159const VIDEO_IMPORT_TIMEOUT = 1000 * 3600 // 1 hour
146 160
147// 1 hour 161const SCHEDULER_INTERVALS_MS = {
148let SCHEDULER_INTERVALS_MS = {
149 actorFollowScores: 60000 * 60, // 1 hour 162 actorFollowScores: 60000 * 60, // 1 hour
150 removeOldJobs: 60000 * 60, // 1 hour 163 removeOldJobs: 60000 * 60, // 1 hour
151 updateVideos: 60000, // 1 minute 164 updateVideos: 60000, // 1 minute
152 youtubeDLUpdate: 60000 * 60 * 24 // 1 day 165 youtubeDLUpdate: 60000 * 60 * 24, // 1 day
153} 166 removeOldViews: 60000 * 60 * 24, // 1 day
154 167 removeOldHistory: 60000 * 60 * 24 // 1 day
155// ---------------------------------------------------------------------------
156
157const CONFIG = {
158 CUSTOM_FILE: getLocalConfigFilePath(),
159 LISTEN: {
160 PORT: config.get<number>('listen.port'),
161 HOSTNAME: config.get<string>('listen.hostname')
162 },
163 DATABASE: {
164 DBNAME: 'peertube' + config.get<string>('database.suffix'),
165 HOSTNAME: config.get<string>('database.hostname'),
166 PORT: config.get<number>('database.port'),
167 USERNAME: config.get<string>('database.username'),
168 PASSWORD: config.get<string>('database.password'),
169 POOL: {
170 MAX: config.get<number>('database.pool.max')
171 }
172 },
173 REDIS: {
174 HOSTNAME: config.has('redis.hostname') ? config.get<string>('redis.hostname') : null,
175 PORT: config.has('redis.port') ? config.get<number>('redis.port') : null,
176 SOCKET: config.has('redis.socket') ? config.get<string>('redis.socket') : null,
177 AUTH: config.has('redis.auth') ? config.get<string>('redis.auth') : null,
178 DB: config.has('redis.db') ? config.get<number>('redis.db') : null
179 },
180 SMTP: {
181 HOSTNAME: config.get<string>('smtp.hostname'),
182 PORT: config.get<number>('smtp.port'),
183 USERNAME: config.get<string>('smtp.username'),
184 PASSWORD: config.get<string>('smtp.password'),
185 TLS: config.get<boolean>('smtp.tls'),
186 DISABLE_STARTTLS: config.get<boolean>('smtp.disable_starttls'),
187 CA_FILE: config.get<string>('smtp.ca_file'),
188 FROM_ADDRESS: config.get<string>('smtp.from_address')
189 },
190 STORAGE: {
191 TMP_DIR: buildPath(config.get<string>('storage.tmp')),
192 AVATARS_DIR: buildPath(config.get<string>('storage.avatars')),
193 LOG_DIR: buildPath(config.get<string>('storage.logs')),
194 VIDEOS_DIR: buildPath(config.get<string>('storage.videos')),
195 REDUNDANCY_DIR: buildPath(config.get<string>('storage.redundancy')),
196 THUMBNAILS_DIR: buildPath(config.get<string>('storage.thumbnails')),
197 PREVIEWS_DIR: buildPath(config.get<string>('storage.previews')),
198 CAPTIONS_DIR: buildPath(config.get<string>('storage.captions')),
199 TORRENTS_DIR: buildPath(config.get<string>('storage.torrents')),
200 CACHE_DIR: buildPath(config.get<string>('storage.cache'))
201 },
202 WEBSERVER: {
203 SCHEME: config.get<boolean>('webserver.https') === true ? 'https' : 'http',
204 WS: config.get<boolean>('webserver.https') === true ? 'wss' : 'ws',
205 HOSTNAME: config.get<string>('webserver.hostname'),
206 PORT: config.get<number>('webserver.port'),
207 URL: '',
208 HOST: ''
209 },
210 TRUST_PROXY: config.get<string[]>('trust_proxy'),
211 LOG: {
212 LEVEL: config.get<string>('log.level')
213 },
214 SEARCH: {
215 REMOTE_URI: {
216 USERS: config.get<boolean>('search.remote_uri.users'),
217 ANONYMOUS: config.get<boolean>('search.remote_uri.anonymous')
218 }
219 },
220 TRENDING: {
221 VIDEOS: {
222 INTERVAL_DAYS: config.get<number>('trending.videos.interval_days')
223 }
224 },
225 REDUNDANCY: {
226 VIDEOS: {
227 CHECK_INTERVAL: parseDuration(config.get<string>('redundancy.videos.check_interval')),
228 STRATEGIES: buildVideosRedundancy(config.get<any[]>('redundancy.videos.strategies'))
229 }
230 },
231 ADMIN: {
232 get EMAIL () { return config.get<string>('admin.email') }
233 },
234 CONTACT_FORM: {
235 get ENABLED () { return config.get<boolean>('contact_form.enabled') }
236 },
237 SIGNUP: {
238 get ENABLED () { return config.get<boolean>('signup.enabled') },
239 get LIMIT () { return config.get<number>('signup.limit') },
240 get REQUIRES_EMAIL_VERIFICATION () { return config.get<boolean>('signup.requires_email_verification') },
241 FILTERS: {
242 CIDR: {
243 get WHITELIST () { return config.get<string[]>('signup.filters.cidr.whitelist') },
244 get BLACKLIST () { return config.get<string[]>('signup.filters.cidr.blacklist') }
245 }
246 }
247 },
248 USER: {
249 get VIDEO_QUOTA () { return parseBytes(config.get<number>('user.video_quota')) },
250 get VIDEO_QUOTA_DAILY () { return parseBytes(config.get<number>('user.video_quota_daily')) }
251 },
252 TRANSCODING: {
253 get ENABLED () { return config.get<boolean>('transcoding.enabled') },
254 get ALLOW_ADDITIONAL_EXTENSIONS () { return config.get<boolean>('transcoding.allow_additional_extensions') },
255 get THREADS () { return config.get<number>('transcoding.threads') },
256 RESOLUTIONS: {
257 get '240p' () { return config.get<boolean>('transcoding.resolutions.240p') },
258 get '360p' () { return config.get<boolean>('transcoding.resolutions.360p') },
259 get '480p' () { return config.get<boolean>('transcoding.resolutions.480p') },
260 get '720p' () { return config.get<boolean>('transcoding.resolutions.720p') },
261 get '1080p' () { return config.get<boolean>('transcoding.resolutions.1080p') }
262 }
263 },
264 IMPORT: {
265 VIDEOS: {
266 HTTP: {
267 get ENABLED () { return config.get<boolean>('import.videos.http.enabled') }
268 },
269 TORRENT: {
270 get ENABLED () { return config.get<boolean>('import.videos.torrent.enabled') }
271 }
272 }
273 },
274 CACHE: {
275 PREVIEWS: {
276 get SIZE () { return config.get<number>('cache.previews.size') }
277 },
278 VIDEO_CAPTIONS: {
279 get SIZE () { return config.get<number>('cache.captions.size') }
280 }
281 },
282 INSTANCE: {
283 get NAME () { return config.get<string>('instance.name') },
284 get SHORT_DESCRIPTION () { return config.get<string>('instance.short_description') },
285 get DESCRIPTION () { return config.get<string>('instance.description') },
286 get TERMS () { return config.get<string>('instance.terms') },
287 get DEFAULT_CLIENT_ROUTE () { return config.get<string>('instance.default_client_route') },
288 get DEFAULT_NSFW_POLICY () { return config.get<NSFWPolicyType>('instance.default_nsfw_policy') },
289 CUSTOMIZATIONS: {
290 get JAVASCRIPT () { return config.get<string>('instance.customizations.javascript') },
291 get CSS () { return config.get<string>('instance.customizations.css') }
292 },
293 get ROBOTS () { return config.get<string>('instance.robots') },
294 get SECURITYTXT () { return config.get<string>('instance.securitytxt') },
295 get SECURITYTXT_CONTACT () { return config.get<string>('admin.email') }
296 },
297 SERVICES: {
298 get 'CSP-LOGGER' () { return config.get<string>('services.csp-logger') },
299 TWITTER: {
300 get USERNAME () { return config.get<string>('services.twitter.username') },
301 get WHITELISTED () { return config.get<boolean>('services.twitter.whitelisted') }
302 }
303 }
304} 168}
305 169
306// --------------------------------------------------------------------------- 170// ---------------------------------------------------------------------------
@@ -377,6 +241,17 @@ let CONSTRAINTS_FIELDS = {
377 FILE_SIZE: { min: 10 }, 241 FILE_SIZE: { min: 10 },
378 URL: { min: 3, max: 2000 } // Length 242 URL: { min: 3, max: 2000 } // Length
379 }, 243 },
244 VIDEO_PLAYLISTS: {
245 NAME: { min: 1, max: 120 }, // Length
246 DESCRIPTION: { min: 3, max: 1000 }, // Length
247 URL: { min: 3, max: 2000 }, // Length
248 IMAGE: {
249 EXTNAME: [ '.jpg', '.jpeg' ],
250 FILE_SIZE: {
251 max: 2 * 1024 * 1024 // 2MB
252 }
253 }
254 },
380 ACTORS: { 255 ACTORS: {
381 PUBLIC_KEY: { min: 10, max: 5000 }, // Length 256 PUBLIC_KEY: { min: 10, max: 5000 }, // Length
382 PRIVATE_KEY: { min: 10, max: 5000 }, // Length 257 PRIVATE_KEY: { min: 10, max: 5000 }, // Length
@@ -406,12 +281,12 @@ let CONSTRAINTS_FIELDS = {
406 281
407const RATES_LIMIT = { 282const RATES_LIMIT = {
408 LOGIN: { 283 LOGIN: {
409 WINDOW_MS: 5 * 60 * 1000, // 5 minutes 284 WINDOW_MS: CONFIG.RATES_LIMIT.LOGIN.WINDOW_MS,
410 MAX: 15 // 15 attempts 285 MAX: CONFIG.RATES_LIMIT.LOGIN.MAX
411 }, 286 },
412 ASK_SEND_EMAIL: { 287 ASK_SEND_EMAIL: {
413 WINDOW_MS: 5 * 60 * 1000, // 5 minutes 288 WINDOW_MS: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.WINDOW_MS,
414 MAX: 3 // 3 attempts 289 MAX: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.MAX
415 } 290 }
416} 291}
417 292
@@ -467,30 +342,41 @@ const VIDEO_LICENCES = {
467 7: 'Public Domain Dedication' 342 7: 'Public Domain Dedication'
468} 343}
469 344
470const VIDEO_LANGUAGES = buildLanguages() 345let VIDEO_LANGUAGES: { [id: string]: string } = {}
471 346
472const VIDEO_PRIVACIES = { 347const VIDEO_PRIVACIES = {
473 [VideoPrivacy.PUBLIC]: 'Public', 348 [ VideoPrivacy.PUBLIC ]: 'Public',
474 [VideoPrivacy.UNLISTED]: 'Unlisted', 349 [ VideoPrivacy.UNLISTED ]: 'Unlisted',
475 [VideoPrivacy.PRIVATE]: 'Private' 350 [ VideoPrivacy.PRIVATE ]: 'Private'
476} 351}
477 352
478const VIDEO_STATES = { 353const VIDEO_STATES = {
479 [VideoState.PUBLISHED]: 'Published', 354 [ VideoState.PUBLISHED ]: 'Published',
480 [VideoState.TO_TRANSCODE]: 'To transcode', 355 [ VideoState.TO_TRANSCODE ]: 'To transcode',
481 [VideoState.TO_IMPORT]: 'To import' 356 [ VideoState.TO_IMPORT ]: 'To import'
482} 357}
483 358
484const VIDEO_IMPORT_STATES = { 359const VIDEO_IMPORT_STATES = {
485 [VideoImportState.FAILED]: 'Failed', 360 [ VideoImportState.FAILED ]: 'Failed',
486 [VideoImportState.PENDING]: 'Pending', 361 [ VideoImportState.PENDING ]: 'Pending',
487 [VideoImportState.SUCCESS]: 'Success' 362 [ VideoImportState.SUCCESS ]: 'Success'
488} 363}
489 364
490const VIDEO_ABUSE_STATES = { 365const VIDEO_ABUSE_STATES = {
491 [VideoAbuseState.PENDING]: 'Pending', 366 [ VideoAbuseState.PENDING ]: 'Pending',
492 [VideoAbuseState.REJECTED]: 'Rejected', 367 [ VideoAbuseState.REJECTED ]: 'Rejected',
493 [VideoAbuseState.ACCEPTED]: 'Accepted' 368 [ VideoAbuseState.ACCEPTED ]: 'Accepted'
369}
370
371const VIDEO_PLAYLIST_PRIVACIES = {
372 [ VideoPlaylistPrivacy.PUBLIC ]: 'Public',
373 [ VideoPlaylistPrivacy.UNLISTED ]: 'Unlisted',
374 [ VideoPlaylistPrivacy.PRIVATE ]: 'Private'
375}
376
377const VIDEO_PLAYLIST_TYPES = {
378 [ VideoPlaylistType.REGULAR ]: 'Regular',
379 [ VideoPlaylistType.WATCH_LATER ]: 'Watch later'
494} 380}
495 381
496const MIMETYPES = { 382const MIMETYPES = {
@@ -548,8 +434,9 @@ const ACTIVITY_PUB = {
548 MAGNET: [ 'application/x-bittorrent;x-scheme-handler/magnet' ] 434 MAGNET: [ 'application/x-bittorrent;x-scheme-handler/magnet' ]
549 }, 435 },
550 MAX_RECURSION_COMMENTS: 100, 436 MAX_RECURSION_COMMENTS: 100,
551 ACTOR_REFRESH_INTERVAL: 3600 * 24 * 1000, // 1 day 437 ACTOR_REFRESH_INTERVAL: 3600 * 24 * 1000 * 2, // 2 days
552 VIDEO_REFRESH_INTERVAL: 3600 * 24 * 1000 // 1 day 438 VIDEO_REFRESH_INTERVAL: 3600 * 24 * 1000 * 2, // 2 days
439 VIDEO_PLAYLIST_REFRESH_INTERVAL: 3600 * 24 * 1000 * 2 // 2 days
553} 440}
554 441
555const ACTIVITY_PUB_ACTOR_TYPES: { [ id: string ]: ActivityPubActorType } = { 442const ACTIVITY_PUB_ACTOR_TYPES: { [ id: string ]: ActivityPubActorType } = {
@@ -575,7 +462,7 @@ const USER_PASSWORD_RESET_LIFETIME = 60000 * 5 // 5 minutes
575 462
576const USER_EMAIL_VERIFY_LIFETIME = 60000 * 60 // 60 minutes 463const USER_EMAIL_VERIFY_LIFETIME = 60000 * 60 // 60 minutes
577 464
578const NSFW_POLICY_TYPES: { [ id: string]: NSFWPolicyType } = { 465const NSFW_POLICY_TYPES: { [ id: string ]: NSFWPolicyType } = {
579 DO_NOT_LIST: 'do_not_list', 466 DO_NOT_LIST: 'do_not_list',
580 BLUR: 'blur', 467 BLUR: 'blur',
581 DISPLAY: 'display' 468 DISPLAY: 'display'
@@ -590,6 +477,9 @@ const STATIC_PATHS = {
590 TORRENTS: '/static/torrents/', 477 TORRENTS: '/static/torrents/',
591 WEBSEED: '/static/webseed/', 478 WEBSEED: '/static/webseed/',
592 REDUNDANCY: '/static/redundancy/', 479 REDUNDANCY: '/static/redundancy/',
480 STREAMING_PLAYLISTS: {
481 HLS: '/static/streaming-playlists/hls'
482 },
593 AVATARS: '/static/avatars/', 483 AVATARS: '/static/avatars/',
594 VIDEO_CAPTIONS: '/static/video-captions/' 484 VIDEO_CAPTIONS: '/static/video-captions/'
595} 485}
@@ -603,8 +493,8 @@ let STATIC_MAX_AGE = '2h'
603 493
604// Videos thumbnail size 494// Videos thumbnail size
605const THUMBNAILS_SIZE = { 495const THUMBNAILS_SIZE = {
606 width: 200, 496 width: 223,
607 height: 110 497 height: 122
608} 498}
609const PREVIEWS_SIZE = { 499const PREVIEWS_SIZE = {
610 width: 560, 500 width: 560,
@@ -621,7 +511,7 @@ const EMBED_SIZE = {
621} 511}
622 512
623// Sub folders of cache directory 513// Sub folders of cache directory
624const CACHE = { 514const FILES_CACHE = {
625 PREVIEWS: { 515 PREVIEWS: {
626 DIRECTORY: join(CONFIG.STORAGE.CACHE_DIR, 'previews'), 516 DIRECTORY: join(CONFIG.STORAGE.CACHE_DIR, 'previews'),
627 MAX_AGE: 1000 * 3600 * 3 // 3 hours 517 MAX_AGE: 1000 * 3600 * 3 // 3 hours
@@ -632,6 +522,15 @@ const CACHE = {
632 } 522 }
633} 523}
634 524
525const CACHE = {
526 USER_TOKENS: {
527 MAX_SIZE: 10000
528 }
529}
530
531const HLS_STREAMING_PLAYLIST_DIRECTORY = join(CONFIG.STORAGE.STREAMING_PLAYLISTS_DIR, 'hls')
532const HLS_REDUNDANCY_DIRECTORY = join(CONFIG.STORAGE.REDUNDANCY_DIR, 'hls')
533
635const MEMOIZE_TTL = { 534const MEMOIZE_TTL = {
636 OVERVIEWS_SAMPLE: 1000 * 3600 * 4 // 4 hours 535 OVERVIEWS_SAMPLE: 1000 * 3600 * 4 // 4 hours
637} 536}
@@ -650,7 +549,7 @@ const CUSTOM_HTML_TAG_COMMENTS = {
650 TITLE: '<!-- title tag -->', 549 TITLE: '<!-- title tag -->',
651 DESCRIPTION: '<!-- description tag -->', 550 DESCRIPTION: '<!-- description tag -->',
652 CUSTOM_CSS: '<!-- custom css tag -->', 551 CUSTOM_CSS: '<!-- custom css tag -->',
653 OPENGRAPH_AND_OEMBED: '<!-- open graph and oembed tags -->' 552 META_TAGS: '<!-- meta tags -->'
654} 553}
655 554
656// --------------------------------------------------------------------------- 555// ---------------------------------------------------------------------------
@@ -659,6 +558,8 @@ const FEEDS = {
659 COUNT: 20 558 COUNT: 20
660} 559}
661 560
561const MAX_LOGS_OUTPUT_CHARACTERS = 10 * 1000 * 1000
562
662// --------------------------------------------------------------------------- 563// ---------------------------------------------------------------------------
663 564
664const TRACKER_RATE_LIMITS = { 565const TRACKER_RATE_LIMITS = {
@@ -667,6 +568,8 @@ const TRACKER_RATE_LIMITS = {
667 ANNOUNCES_PER_IP: 30 // maximum announces for all our torrents in the interval 568 ANNOUNCES_PER_IP: 30 // maximum announces for all our torrents in the interval
668} 569}
669 570
571const P2P_MEDIA_LOADER_PEER_VERSION = 2
572
670// --------------------------------------------------------------------------- 573// ---------------------------------------------------------------------------
671 574
672// Special constants for a test instance 575// Special constants for a test instance
@@ -683,38 +586,50 @@ if (isTestInstance() === true) {
683 ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE = 2 586 ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE = 2
684 ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL = 10 * 1000 // 10 seconds 587 ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL = 10 * 1000 // 10 seconds
685 ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL = 10 * 1000 // 10 seconds 588 ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL = 10 * 1000 // 10 seconds
589 ACTIVITY_PUB.VIDEO_PLAYLIST_REFRESH_INTERVAL = 10 * 1000 // 10 seconds
686 590
687 CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max = 100 * 1024 // 100KB 591 CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max = 100 * 1024 // 100KB
688 592
689 SCHEDULER_INTERVALS_MS.actorFollowScores = 1000 593 SCHEDULER_INTERVALS_MS.actorFollowScores = 1000
690 SCHEDULER_INTERVALS_MS.removeOldJobs = 10000 594 SCHEDULER_INTERVALS_MS.removeOldJobs = 10000
595 SCHEDULER_INTERVALS_MS.removeOldHistory = 5000
596 SCHEDULER_INTERVALS_MS.removeOldViews = 5000
691 SCHEDULER_INTERVALS_MS.updateVideos = 5000 597 SCHEDULER_INTERVALS_MS.updateVideos = 5000
692 REPEAT_JOBS['videos-views'] = { every: 5000 } 598 REPEAT_JOBS[ 'videos-views' ] = { every: 5000 }
693 599
694 REDUNDANCY.VIDEOS.RANDOMIZED_FACTOR = 1 600 REDUNDANCY.VIDEOS.RANDOMIZED_FACTOR = 1
695 601
696 VIDEO_VIEW_LIFETIME = 1000 // 1 second 602 VIDEO_VIEW_LIFETIME = 1000 // 1 second
697 CONTACT_FORM_LIFETIME = 1000 // 1 second 603 CONTACT_FORM_LIFETIME = 1000 // 1 second
698 604
699 JOB_ATTEMPTS['email'] = 1 605 JOB_ATTEMPTS[ 'email' ] = 1
700 606
701 CACHE.VIDEO_CAPTIONS.MAX_AGE = 3000 607 FILES_CACHE.VIDEO_CAPTIONS.MAX_AGE = 3000
702 MEMOIZE_TTL.OVERVIEWS_SAMPLE = 1 608 MEMOIZE_TTL.OVERVIEWS_SAMPLE = 1
703 ROUTE_CACHE_LIFETIME.OVERVIEWS.VIDEOS = '0ms' 609 ROUTE_CACHE_LIFETIME.OVERVIEWS.VIDEOS = '0ms'
610
611 RATES_LIMIT.LOGIN.MAX = 20
704} 612}
705 613
706updateWebserverUrls() 614updateWebserverUrls()
707 615
616registerConfigChangedHandler(() => {
617 updateWebserverUrls()
618 updateWebserverConfig()
619})
620
708// --------------------------------------------------------------------------- 621// ---------------------------------------------------------------------------
709 622
710export { 623export {
624 WEBSERVER,
711 API_VERSION, 625 API_VERSION,
626 HLS_REDUNDANCY_DIRECTORY,
627 P2P_MEDIA_LOADER_PEER_VERSION,
712 AVATARS_SIZE, 628 AVATARS_SIZE,
713 ACCEPT_HEADERS, 629 ACCEPT_HEADERS,
714 BCRYPT_SALT_SIZE, 630 BCRYPT_SALT_SIZE,
715 TRACKER_RATE_LIMITS, 631 TRACKER_RATE_LIMITS,
716 CACHE, 632 FILES_CACHE,
717 CONFIG,
718 CONSTRAINTS_FIELDS, 633 CONSTRAINTS_FIELDS,
719 EMBED_SIZE, 634 EMBED_SIZE,
720 REDUNDANCY, 635 REDUNDANCY,
@@ -733,12 +648,15 @@ export {
733 PRIVATE_RSA_KEY_SIZE, 648 PRIVATE_RSA_KEY_SIZE,
734 ROUTE_CACHE_LIFETIME, 649 ROUTE_CACHE_LIFETIME,
735 SORTABLE_COLUMNS, 650 SORTABLE_COLUMNS,
651 HLS_STREAMING_PLAYLIST_DIRECTORY,
736 FEEDS, 652 FEEDS,
737 JOB_TTL, 653 JOB_TTL,
738 NSFW_POLICY_TYPES, 654 NSFW_POLICY_TYPES,
739 STATIC_MAX_AGE, 655 STATIC_MAX_AGE,
740 STATIC_PATHS, 656 STATIC_PATHS,
741 VIDEO_IMPORT_TIMEOUT, 657 VIDEO_IMPORT_TIMEOUT,
658 VIDEO_PLAYLIST_TYPES,
659 MAX_LOGS_OUTPUT_CHARACTERS,
742 ACTIVITY_PUB, 660 ACTIVITY_PUB,
743 ACTIVITY_PUB_ACTOR_TYPES, 661 ACTIVITY_PUB_ACTOR_TYPES,
744 THUMBNAILS_SIZE, 662 THUMBNAILS_SIZE,
@@ -751,6 +669,7 @@ export {
751 VIDEO_TRANSCODING_FPS, 669 VIDEO_TRANSCODING_FPS,
752 FFMPEG_NICE, 670 FFMPEG_NICE,
753 VIDEO_ABUSE_STATES, 671 VIDEO_ABUSE_STATES,
672 CACHE,
754 JOB_REQUEST_TIMEOUT, 673 JOB_REQUEST_TIMEOUT,
755 USER_PASSWORD_RESET_LIFETIME, 674 USER_PASSWORD_RESET_LIFETIME,
756 MEMOIZE_TTL, 675 MEMOIZE_TTL,
@@ -767,22 +686,13 @@ export {
767 VIDEO_IMPORT_STATES, 686 VIDEO_IMPORT_STATES,
768 VIDEO_VIEW_LIFETIME, 687 VIDEO_VIEW_LIFETIME,
769 CONTACT_FORM_LIFETIME, 688 CONTACT_FORM_LIFETIME,
689 VIDEO_PLAYLIST_PRIVACIES,
690 loadLanguages,
770 buildLanguages 691 buildLanguages
771} 692}
772 693
773// --------------------------------------------------------------------------- 694// ---------------------------------------------------------------------------
774 695
775function getLocalConfigFilePath () {
776 const configSources = config.util.getConfigSources()
777 if (configSources.length === 0) throw new Error('Invalid config source.')
778
779 let filename = 'local'
780 if (process.env.NODE_ENV) filename += `-${process.env.NODE_ENV}`
781 if (process.env.NODE_APP_INSTANCE) filename += `-${process.env.NODE_APP_INSTANCE}`
782
783 return join(dirname(configSources[ 0 ].name), filename + '.json')
784}
785
786function buildVideoMimetypeExt () { 696function buildVideoMimetypeExt () {
787 const data = { 697 const data = {
788 'video/webm': '.webm', 698 'video/webm': '.webm',
@@ -805,8 +715,12 @@ function buildVideoMimetypeExt () {
805} 715}
806 716
807function updateWebserverUrls () { 717function updateWebserverUrls () {
808 CONFIG.WEBSERVER.URL = sanitizeUrl(CONFIG.WEBSERVER.SCHEME + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT) 718 WEBSERVER.URL = sanitizeUrl(CONFIG.WEBSERVER.SCHEME + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT)
809 CONFIG.WEBSERVER.HOST = sanitizeHost(CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT, REMOTE_SCHEME.HTTP) 719 WEBSERVER.HOST = sanitizeHost(CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT, REMOTE_SCHEME.HTTP)
720 WEBSERVER.SCHEME = CONFIG.WEBSERVER.SCHEME
721 WEBSERVER.WS = CONFIG.WEBSERVER.WS
722 WEBSERVER.HOSTNAME = CONFIG.WEBSERVER.HOSTNAME
723 WEBSERVER.PORT = CONFIG.WEBSERVER.PORT
810} 724}
811 725
812function updateWebserverConfig () { 726function updateWebserverConfig () {
@@ -822,16 +736,8 @@ function buildVideosExtname () {
822 : [ '.mp4', '.ogv', '.webm' ] 736 : [ '.mp4', '.ogv', '.webm' ]
823} 737}
824 738
825function buildVideosRedundancy (objs: any[]): VideosRedundancy[] { 739function loadLanguages () {
826 if (!objs) return [] 740 Object.assign(VIDEO_LANGUAGES, buildLanguages())
827
828 return objs.map(obj => {
829 return Object.assign({}, obj, {
830 minLifetime: parseDuration(obj.min_lifetime),
831 size: bytes.parse(obj.size),
832 minViews: obj.min_views
833 })
834 })
835} 741}
836 742
837function buildLanguages () { 743function buildLanguages () {
@@ -866,42 +772,13 @@ function buildLanguages () {
866 iso639 772 iso639
867 .filter(l => { 773 .filter(l => {
868 return (l.iso6391 !== null && l.type === 'living') || 774 return (l.iso6391 !== null && l.type === 'living') ||
869 additionalLanguages[l.iso6393] === true 775 additionalLanguages[ l.iso6393 ] === true
870 }) 776 })
871 .forEach(l => languages[l.iso6391 || l.iso6393] = l.name) 777 .forEach(l => languages[ l.iso6391 || l.iso6393 ] = l.name)
872 778
873 // Override Occitan label 779 // Override Occitan label
874 languages['oc'] = 'Occitan' 780 languages[ 'oc' ] = 'Occitan'
781 languages[ 'el' ] = 'Greek'
875 782
876 return languages 783 return languages
877} 784}
878
879export function reloadConfig () {
880
881 function directory () {
882 if (process.env.NODE_CONFIG_DIR) {
883 return process.env.NODE_CONFIG_DIR
884 }
885
886 return join(root(), 'config')
887 }
888
889 function purge () {
890 for (const fileName in require.cache) {
891 if (-1 === fileName.indexOf(directory())) {
892 continue
893 }
894
895 delete require.cache[fileName]
896 }
897
898 delete require.cache[require.resolve('config')]
899 }
900
901 purge()
902
903 config = require('config')
904
905 updateWebserverConfig()
906 updateWebserverUrls()
907}
diff --git a/server/initializers/database.ts b/server/initializers/database.ts
index 84ad2079b..142063a99 100644
--- a/server/initializers/database.ts
+++ b/server/initializers/database.ts
@@ -21,7 +21,7 @@ import { VideoCommentModel } from '../models/video/video-comment'
21import { VideoFileModel } from '../models/video/video-file' 21import { VideoFileModel } from '../models/video/video-file'
22import { VideoShareModel } from '../models/video/video-share' 22import { VideoShareModel } from '../models/video/video-share'
23import { VideoTagModel } from '../models/video/video-tag' 23import { VideoTagModel } from '../models/video/video-tag'
24import { CONFIG } from './constants' 24import { CONFIG } from './config'
25import { ScheduleVideoUpdateModel } from '../models/video/schedule-video-update' 25import { ScheduleVideoUpdateModel } from '../models/video/schedule-video-update'
26import { VideoCaptionModel } from '../models/video/video-caption' 26import { VideoCaptionModel } from '../models/video/video-caption'
27import { VideoImportModel } from '../models/video/video-import' 27import { VideoImportModel } from '../models/video/video-import'
@@ -33,6 +33,11 @@ import { 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' 34import { UserNotificationModel } from '../models/account/user-notification'
35import { UserNotificationSettingModel } from '../models/account/user-notification-setting' 35import { UserNotificationSettingModel } from '../models/account/user-notification-setting'
36import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
37import { VideoPlaylistModel } from '../models/video/video-playlist'
38import { VideoPlaylistElementModel } from '../models/video/video-playlist-element'
39import { ThumbnailModel } from '../models/video/thumbnail'
40import { QueryTypes, Transaction } from 'sequelize'
36 41
37require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string 42require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string
38 43
@@ -54,8 +59,7 @@ const sequelizeTypescript = new SequelizeTypescript({
54 max: poolMax 59 max: poolMax
55 }, 60 },
56 benchmark: isTestInstance(), 61 benchmark: isTestInstance(),
57 isolationLevel: SequelizeTypescript.Transaction.ISOLATION_LEVELS.SERIALIZABLE, 62 isolationLevel: Transaction.ISOLATION_LEVELS.SERIALIZABLE,
58 operatorsAliases: false,
59 logging: (message: string, benchmark: number) => { 63 logging: (message: string, benchmark: number) => {
60 if (process.env.NODE_DB_LOG === 'false') return 64 if (process.env.NODE_DB_LOG === 'false') return
61 65
@@ -82,6 +86,7 @@ async function initDatabaseModels (silent: boolean) {
82 AccountVideoRateModel, 86 AccountVideoRateModel,
83 UserModel, 87 UserModel,
84 VideoAbuseModel, 88 VideoAbuseModel,
89 VideoModel,
85 VideoChangeOwnershipModel, 90 VideoChangeOwnershipModel,
86 VideoChannelModel, 91 VideoChannelModel,
87 VideoShareModel, 92 VideoShareModel,
@@ -89,7 +94,6 @@ async function initDatabaseModels (silent: boolean) {
89 VideoCaptionModel, 94 VideoCaptionModel,
90 VideoBlacklistModel, 95 VideoBlacklistModel,
91 VideoTagModel, 96 VideoTagModel,
92 VideoModel,
93 VideoCommentModel, 97 VideoCommentModel,
94 ScheduleVideoUpdateModel, 98 ScheduleVideoUpdateModel,
95 VideoImportModel, 99 VideoImportModel,
@@ -99,7 +103,11 @@ async function initDatabaseModels (silent: boolean) {
99 AccountBlocklistModel, 103 AccountBlocklistModel,
100 ServerBlocklistModel, 104 ServerBlocklistModel,
101 UserNotificationModel, 105 UserNotificationModel,
102 UserNotificationSettingModel 106 UserNotificationSettingModel,
107 VideoStreamingPlaylistModel,
108 VideoPlaylistModel,
109 VideoPlaylistElementModel,
110 ThumbnailModel
103 ]) 111 ])
104 112
105 // Check extensions exist in the database 113 // Check extensions exist in the database
@@ -132,11 +140,16 @@ async function checkPostgresExtensions () {
132} 140}
133 141
134async function checkPostgresExtension (extension: string) { 142async function checkPostgresExtension (extension: string) {
135 const query = `SELECT true AS enabled FROM pg_available_extensions WHERE name = '${extension}' AND installed_version IS NOT NULL;` 143 const query = `SELECT 1 FROM pg_available_extensions WHERE name = '${extension}' AND installed_version IS NOT NULL;`
136 const [ res ] = await sequelizeTypescript.query(query, { raw: true }) 144 const options = {
145 type: QueryTypes.SELECT as QueryTypes.SELECT,
146 raw: true
147 }
148
149 const res = await sequelizeTypescript.query<object>(query, options)
137 150
138 if (!res || res.length === 0 || res[ 0 ][ 'enabled' ] !== true) { 151 if (!res || res.length === 0) {
139 // Try to create the extension ourself 152 // Try to create the extension ourselves
140 try { 153 try {
141 await sequelizeTypescript.query(`CREATE EXTENSION ${extension};`, { raw: true }) 154 await sequelizeTypescript.query(`CREATE EXTENSION ${extension};`, { raw: true })
142 155
diff --git a/server/initializers/index.ts b/server/initializers/index.ts
index fe9190a9c..0fc1a7363 100644
--- a/server/initializers/index.ts
+++ b/server/initializers/index.ts
@@ -1,5 +1,3 @@
1// Constants first, database in second!
2export * from './constants'
3export * from './database' 1export * from './database'
4export * from './installer' 2export * from './installer'
5export * from './migrator' 3export * from './migrator'
diff --git a/server/initializers/installer.ts b/server/initializers/installer.ts
index b9a9da183..127449577 100644
--- a/server/initializers/installer.ts
+++ b/server/initializers/installer.ts
@@ -1,14 +1,15 @@
1import * as passwordGenerator from 'password-generator' 1import * as passwordGenerator from 'password-generator'
2import { UserRole } from '../../shared' 2import { UserRole } from '../../shared'
3import { logger } from '../helpers/logger' 3import { logger } from '../helpers/logger'
4import { createApplicationActor, createUserAccountAndChannel } from '../lib/user' 4import { createApplicationActor, createUserAccountAndChannelAndPlaylist } from '../lib/user'
5import { UserModel } from '../models/account/user' 5import { UserModel } from '../models/account/user'
6import { ApplicationModel } from '../models/application/application' 6import { ApplicationModel } from '../models/application/application'
7import { OAuthClientModel } from '../models/oauth/oauth-client' 7import { OAuthClientModel } from '../models/oauth/oauth-client'
8import { applicationExist, clientsExist, usersExist } from './checker-after-init' 8import { applicationExist, clientsExist, usersExist } from './checker-after-init'
9import { CACHE, CONFIG, LAST_MIGRATION_VERSION } from './constants' 9import { FILES_CACHE, HLS_STREAMING_PLAYLIST_DIRECTORY, LAST_MIGRATION_VERSION } from './constants'
10import { sequelizeTypescript } from './database' 10import { sequelizeTypescript } from './database'
11import { remove, ensureDir } from 'fs-extra' 11import { ensureDir, remove } from 'fs-extra'
12import { CONFIG } from './config'
12 13
13async function installApplication () { 14async function installApplication () {
14 try { 15 try {
@@ -24,7 +25,7 @@ async function installApplication () {
24 }), 25 }),
25 26
26 // Directories 27 // Directories
27 removeCacheDirectories() 28 removeCacheAndTmpDirectories()
28 .then(() => createDirectoriesIfNotExist()) 29 .then(() => createDirectoriesIfNotExist())
29 ]) 30 ])
30 } catch (err) { 31 } catch (err) {
@@ -41,9 +42,9 @@ export {
41 42
42// --------------------------------------------------------------------------- 43// ---------------------------------------------------------------------------
43 44
44function removeCacheDirectories () { 45function removeCacheAndTmpDirectories () {
45 const cacheDirectories = Object.keys(CACHE) 46 const cacheDirectories = Object.keys(FILES_CACHE)
46 .map(k => CACHE[k].DIRECTORY) 47 .map(k => FILES_CACHE[k].DIRECTORY)
47 48
48 const tasks: Promise<any>[] = [] 49 const tasks: Promise<any>[] = []
49 50
@@ -53,13 +54,15 @@ function removeCacheDirectories () {
53 tasks.push(remove(dir)) 54 tasks.push(remove(dir))
54 } 55 }
55 56
57 tasks.push(remove(CONFIG.STORAGE.TMP_DIR))
58
56 return Promise.all(tasks) 59 return Promise.all(tasks)
57} 60}
58 61
59function createDirectoriesIfNotExist () { 62function createDirectoriesIfNotExist () {
60 const storage = CONFIG.STORAGE 63 const storage = CONFIG.STORAGE
61 const cacheDirectories = Object.keys(CACHE) 64 const cacheDirectories = Object.keys(FILES_CACHE)
62 .map(k => CACHE[k].DIRECTORY) 65 .map(k => FILES_CACHE[k].DIRECTORY)
63 66
64 const tasks: Promise<void>[] = [] 67 const tasks: Promise<void>[] = []
65 for (const key of Object.keys(storage)) { 68 for (const key of Object.keys(storage)) {
@@ -73,6 +76,9 @@ function createDirectoriesIfNotExist () {
73 tasks.push(ensureDir(dir)) 76 tasks.push(ensureDir(dir))
74 } 77 }
75 78
79 // Playlist directories
80 tasks.push(ensureDir(HLS_STREAMING_PLAYLIST_DIRECTORY))
81
76 return Promise.all(tasks) 82 return Promise.all(tasks)
77} 83}
78 84
@@ -138,7 +144,7 @@ async function createOAuthAdminIfNotExist () {
138 } 144 }
139 const user = new UserModel(userData) 145 const user = new UserModel(userData)
140 146
141 await createUserAccountAndChannel(user, validatePassword) 147 await createUserAccountAndChannelAndPlaylist(user, validatePassword)
142 logger.info('Username: ' + username) 148 logger.info('Username: ' + username)
143 logger.info('User password: ' + password) 149 logger.info('User password: ' + password)
144} 150}
diff --git a/server/initializers/migrations/0075-video-resolutions.ts b/server/initializers/migrations/0075-video-resolutions.ts
index 26a188e5e..e4f26cb77 100644
--- a/server/initializers/migrations/0075-video-resolutions.ts
+++ b/server/initializers/migrations/0075-video-resolutions.ts
@@ -1,6 +1,6 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import { join } from 'path' 2import { join } from 'path'
3import { CONFIG } from '../../initializers/constants' 3import { CONFIG } from '../../initializers/config'
4import { getVideoFileResolution } from '../../helpers/ffmpeg-utils' 4import { getVideoFileResolution } from '../../helpers/ffmpeg-utils'
5import { readdir, rename } from 'fs-extra' 5import { readdir, rename } from 'fs-extra'
6 6
diff --git a/server/initializers/migrations/0080-video-channels.ts b/server/initializers/migrations/0080-video-channels.ts
index f19721517..5512bdcf1 100644
--- a/server/initializers/migrations/0080-video-channels.ts
+++ b/server/initializers/migrations/0080-video-channels.ts
@@ -69,12 +69,12 @@ async function up (utils: {
69 const options = { 69 const options = {
70 type: Sequelize.QueryTypes.SELECT 70 type: Sequelize.QueryTypes.SELECT
71 } 71 }
72 const rawVideos = await utils.sequelize.query(query, options) 72 const rawVideos = await utils.sequelize.query(query, options) as any
73 73
74 for (const rawVideo of rawVideos) { 74 for (const rawVideo of rawVideos) {
75 const videoChannel = await utils.db.VideoChannel.findOne({ where: { authorId: rawVideo.authorId } }) 75 const videoChannel = await utils.db.VideoChannel.findOne({ where: { authorId: rawVideo.authorId } })
76 76
77 const video = await utils.db.Video.findById(rawVideo.id) 77 const video = await utils.db.Video.findByPk(rawVideo.id)
78 video.channelId = videoChannel.id 78 video.channelId = videoChannel.id
79 await video.save() 79 await video.save()
80 } 80 }
diff --git a/server/initializers/migrations/0100-activitypub.ts b/server/initializers/migrations/0100-activitypub.ts
index a7ebd804c..2880a97d9 100644
--- a/server/initializers/migrations/0100-activitypub.ts
+++ b/server/initializers/migrations/0100-activitypub.ts
@@ -21,7 +21,7 @@ async function up (utils: {
21 const options = { 21 const options = {
22 type: Sequelize.QueryTypes.SELECT 22 type: Sequelize.QueryTypes.SELECT
23 } 23 }
24 const res = await utils.sequelize.query(query, options) 24 const res = await utils.sequelize.query(query, options) as any
25 25
26 if (!res[0] || res[0].total !== 0) { 26 if (!res[0] || res[0].total !== 0) {
27 throw new Error('You need to quit friends.') 27 throw new Error('You need to quit friends.')
@@ -68,8 +68,8 @@ async function up (utils: {
68 const accountCreated = await createLocalAccountWithoutKeys(SERVER_ACTOR_NAME, null, applicationInstance.id, undefined) 68 const accountCreated = await createLocalAccountWithoutKeys(SERVER_ACTOR_NAME, null, applicationInstance.id, undefined)
69 69
70 const { publicKey, privateKey } = await createPrivateAndPublicKeys() 70 const { publicKey, privateKey } = await createPrivateAndPublicKeys()
71 accountCreated.set('publicKey', publicKey) 71 accountCreated.Actor.publicKey = publicKey
72 accountCreated.set('privateKey', privateKey) 72 accountCreated.Actor.privateKey = privateKey
73 73
74 await accountCreated.save() 74 await accountCreated.save()
75 } 75 }
@@ -86,8 +86,8 @@ async function up (utils: {
86 const account = await createLocalAccountWithoutKeys(user.username, user.id, null, undefined) 86 const account = await createLocalAccountWithoutKeys(user.username, user.id, null, undefined)
87 87
88 const { publicKey, privateKey } = await createPrivateAndPublicKeys() 88 const { publicKey, privateKey } = await createPrivateAndPublicKeys()
89 account.set('publicKey', publicKey) 89 account.Actor.publicKey = publicKey
90 account.set('privateKey', privateKey) 90 account.Actor.privateKey = privateKey
91 await account.save() 91 await account.save()
92 } 92 }
93 93
diff --git a/server/initializers/migrations/0135-video-channel-actor.ts b/server/initializers/migrations/0135-video-channel-actor.ts
index 033f43b68..5ace0f4d2 100644
--- a/server/initializers/migrations/0135-video-channel-actor.ts
+++ b/server/initializers/migrations/0135-video-channel-actor.ts
@@ -239,7 +239,8 @@ async function up (utils: {
239 239
240 { 240 {
241 const query = 'SELECT * FROM "actor" WHERE "serverId" IS NULL AND "publicKey" IS NULL' 241 const query = 'SELECT * FROM "actor" WHERE "serverId" IS NULL AND "publicKey" IS NULL'
242 const [ res ] = await utils.sequelize.query(query) 242 const options = { type: Sequelize.QueryTypes.SELECT as Sequelize.QueryTypes.SELECT }
243 const [ res ] = await utils.sequelize.query(query, options)
243 244
244 for (const actor of res) { 245 for (const actor of res) {
245 const { privateKey, publicKey } = await createPrivateAndPublicKeys() 246 const { privateKey, publicKey } = await createPrivateAndPublicKeys()
diff --git a/server/initializers/migrations/0140-actor-url.ts b/server/initializers/migrations/0140-actor-url.ts
index e64ee3487..020499391 100644
--- a/server/initializers/migrations/0140-actor-url.ts
+++ b/server/initializers/migrations/0140-actor-url.ts
@@ -1,13 +1,13 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import { CONFIG } from '../constants' 2import { WEBSERVER } from '../constants'
3 3
4async function up (utils: { 4async function up (utils: {
5 transaction: Sequelize.Transaction, 5 transaction: Sequelize.Transaction,
6 queryInterface: Sequelize.QueryInterface, 6 queryInterface: Sequelize.QueryInterface,
7 sequelize: Sequelize.Sequelize 7 sequelize: Sequelize.Sequelize
8}): Promise<void> { 8}): Promise<void> {
9 const toReplace = CONFIG.WEBSERVER.HOSTNAME + ':443' 9 const toReplace = WEBSERVER.HOSTNAME + ':443'
10 const by = CONFIG.WEBSERVER.HOST 10 const by = WEBSERVER.HOST
11 const replacer = column => `replace("${column}", '${toReplace}', '${by}')` 11 const replacer = column => `replace("${column}", '${toReplace}', '${by}')`
12 12
13 { 13 {
diff --git a/server/initializers/migrations/0170-actor-follow-score.ts b/server/initializers/migrations/0170-actor-follow-score.ts
index 2deabaf98..a12b35da9 100644
--- a/server/initializers/migrations/0170-actor-follow-score.ts
+++ b/server/initializers/migrations/0170-actor-follow-score.ts
@@ -1,5 +1,5 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import { ACTOR_FOLLOW_SCORE } from '../index' 2import { ACTOR_FOLLOW_SCORE } from '../constants'
3 3
4async function up (utils: { 4async function up (utils: {
5 transaction: Sequelize.Transaction, 5 transaction: Sequelize.Transaction,
diff --git a/server/initializers/migrations/0210-video-language.ts b/server/initializers/migrations/0210-video-language.ts
index b7ec90905..ca95c7527 100644
--- a/server/initializers/migrations/0210-video-language.ts
+++ b/server/initializers/migrations/0210-video-language.ts
@@ -1,5 +1,5 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import { CONSTRAINTS_FIELDS } from '../index' 2import { CONSTRAINTS_FIELDS } from '../constants'
3 3
4async function up (utils: { 4async function up (utils: {
5 transaction: Sequelize.Transaction, 5 transaction: Sequelize.Transaction,
diff --git a/server/initializers/migrations/0215-video-support-length.ts b/server/initializers/migrations/0215-video-support-length.ts
index 994eda60d..ba395050f 100644
--- a/server/initializers/migrations/0215-video-support-length.ts
+++ b/server/initializers/migrations/0215-video-support-length.ts
@@ -1,5 +1,4 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import { CONSTRAINTS_FIELDS } from '../index'
3 2
4async function up (utils: { 3async function up (utils: {
5 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction,
diff --git a/server/initializers/migrations/0235-delete-some-video-indexes.ts b/server/initializers/migrations/0235-delete-some-video-indexes.ts
index e362f240c..5964b0dc5 100644
--- a/server/initializers/migrations/0235-delete-some-video-indexes.ts
+++ b/server/initializers/migrations/0235-delete-some-video-indexes.ts
@@ -1,8 +1,4 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import { createClient } from 'redis'
3import { CONFIG } from '../constants'
4import { JobQueue } from '../../lib/job-queue'
5import { initDatabaseModels } from '../database'
6 2
7async function up (utils: { 3async function up (utils: {
8 transaction: Sequelize.Transaction 4 transaction: Sequelize.Transaction
diff --git a/server/initializers/migrations/0240-drop-old-indexes.ts b/server/initializers/migrations/0240-drop-old-indexes.ts
index ba961e3f9..39868fa2d 100644
--- a/server/initializers/migrations/0240-drop-old-indexes.ts
+++ b/server/initializers/migrations/0240-drop-old-indexes.ts
@@ -1,8 +1,4 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import { createClient } from 'redis'
3import { CONFIG } from '../constants'
4import { JobQueue } from '../../lib/job-queue'
5import { initDatabaseModels } from '../database'
6 2
7async function up (utils: { 3async function up (utils: {
8 transaction: Sequelize.Transaction 4 transaction: Sequelize.Transaction
diff --git a/server/initializers/migrations/0330-video-streaming-playlist.ts b/server/initializers/migrations/0330-video-streaming-playlist.ts
new file mode 100644
index 000000000..c85a762ab
--- /dev/null
+++ b/server/initializers/migrations/0330-video-streaming-playlist.ts
@@ -0,0 +1,51 @@
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 = `
11 CREATE TABLE IF NOT EXISTS "videoStreamingPlaylist"
12(
13 "id" SERIAL,
14 "type" INTEGER NOT NULL,
15 "playlistUrl" VARCHAR(2000) NOT NULL,
16 "p2pMediaLoaderInfohashes" VARCHAR(255)[] NOT NULL,
17 "segmentsSha256Url" VARCHAR(255) NOT NULL,
18 "videoId" INTEGER NOT NULL REFERENCES "video" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
19 "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL,
20 "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL,
21 PRIMARY KEY ("id")
22);`
23 await utils.sequelize.query(query)
24 }
25
26 {
27 const data = {
28 type: Sequelize.INTEGER,
29 allowNull: true,
30 defaultValue: null
31 }
32
33 await utils.queryInterface.changeColumn('videoRedundancy', 'videoFileId', data)
34 }
35
36 {
37 const query = 'ALTER TABLE "videoRedundancy" ADD COLUMN "videoStreamingPlaylistId" INTEGER NULL ' +
38 'REFERENCES "videoStreamingPlaylist" ("id") ON DELETE CASCADE ON UPDATE CASCADE'
39
40 await utils.sequelize.query(query)
41 }
42}
43
44function down (options) {
45 throw new Error('Not implemented.')
46}
47
48export {
49 up,
50 down
51}
diff --git a/server/initializers/migrations/0335-video-downloading-enabled.ts b/server/initializers/migrations/0335-video-downloading-enabled.ts
new file mode 100644
index 000000000..e79466447
--- /dev/null
+++ b/server/initializers/migrations/0335-video-downloading-enabled.ts
@@ -0,0 +1,27 @@
1import * as Sequelize from 'sequelize'
2import { Migration } from '../../models/migrations'
3
4async function up (utils: {
5 transaction: Sequelize.Transaction,
6 queryInterface: Sequelize.QueryInterface,
7 sequelize: Sequelize.Sequelize
8}): Promise<void> {
9 const data = {
10 type: Sequelize.BOOLEAN,
11 allowNull: false,
12 defaultValue: true
13 } as Migration.Boolean
14 await utils.queryInterface.addColumn('video', 'downloadEnabled', data)
15
16 data.defaultValue = null
17 return utils.queryInterface.changeColumn('video', 'downloadEnabled', data)
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/0340-add-originally-published-at.ts b/server/initializers/migrations/0340-add-originally-published-at.ts
new file mode 100644
index 000000000..fe4f4a5f9
--- /dev/null
+++ b/server/initializers/migrations/0340-add-originally-published-at.ts
@@ -0,0 +1,25 @@
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 const data = {
10 type: Sequelize.DATE,
11 allowNull: true,
12 defaultValue: null
13 }
14 await utils.queryInterface.addColumn('video', 'originallyPublishedAt', data)
15
16}
17
18function down (options) {
19 throw new Error('Not implemented.')
20}
21
22export {
23 up,
24 down
25}
diff --git a/server/initializers/migrations/0345-video-playlists.ts b/server/initializers/migrations/0345-video-playlists.ts
new file mode 100644
index 000000000..de69f5b9e
--- /dev/null
+++ b/server/initializers/migrations/0345-video-playlists.ts
@@ -0,0 +1,88 @@
1import * as Sequelize from 'sequelize'
2import { VideoPlaylistPrivacy, VideoPlaylistType } from '../../../shared/models/videos'
3import * as uuidv4 from 'uuid/v4'
4import { WEBSERVER } from '../constants'
5
6async function up (utils: {
7 transaction: Sequelize.Transaction,
8 queryInterface: Sequelize.QueryInterface,
9 sequelize: Sequelize.Sequelize
10}): Promise<void> {
11 const transaction = utils.transaction
12
13 {
14 const query = `
15CREATE TABLE IF NOT EXISTS "videoPlaylist"
16(
17 "id" SERIAL,
18 "name" VARCHAR(255) NOT NULL,
19 "description" VARCHAR(255),
20 "privacy" INTEGER NOT NULL,
21 "url" VARCHAR(2000) NOT NULL,
22 "uuid" UUID NOT NULL,
23 "type" INTEGER NOT NULL DEFAULT 1,
24 "ownerAccountId" INTEGER NOT NULL REFERENCES "account" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
25 "videoChannelId" INTEGER REFERENCES "videoChannel" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
26 "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL,
27 "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL,
28 PRIMARY KEY ("id")
29);`
30 await utils.sequelize.query(query, { transaction })
31 }
32
33 {
34 const query = `
35CREATE TABLE IF NOT EXISTS "videoPlaylistElement"
36(
37 "id" SERIAL,
38 "url" VARCHAR(2000) NOT NULL,
39 "position" INTEGER NOT NULL DEFAULT 1,
40 "startTimestamp" INTEGER,
41 "stopTimestamp" INTEGER,
42 "videoPlaylistId" INTEGER NOT NULL REFERENCES "videoPlaylist" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
43 "videoId" INTEGER NOT NULL REFERENCES "video" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
44 "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL,
45 "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL,
46 PRIMARY KEY ("id")
47);`
48
49 await utils.sequelize.query(query, { transaction })
50 }
51
52 {
53 const userQuery = 'SELECT "username" FROM "user";'
54
55 const options = { transaction, type: Sequelize.QueryTypes.SELECT as Sequelize.QueryTypes.SELECT }
56 const userResult = await utils.sequelize.query<{ username: string }>(userQuery, options)
57 const usernames = userResult.map(r => r.username)
58
59 for (const username of usernames) {
60 const uuid = uuidv4()
61
62 const baseUrl = WEBSERVER.URL + '/video-playlists/' + uuid
63 const query = `
64 INSERT INTO "videoPlaylist" ("url", "uuid", "name", "privacy", "type", "ownerAccountId", "createdAt", "updatedAt")
65 SELECT '${baseUrl}' AS "url",
66 '${uuid}' AS "uuid",
67 'Watch later' AS "name",
68 ${VideoPlaylistPrivacy.PRIVATE} AS "privacy",
69 ${VideoPlaylistType.WATCH_LATER} AS "type",
70 "account"."id" AS "ownerAccountId",
71 NOW() as "createdAt",
72 NOW() as "updatedAt"
73 FROM "user" INNER JOIN "account" ON "user"."id" = "account"."userId"
74 WHERE "user"."username" = '${username}'`
75
76 await utils.sequelize.query(query, { transaction })
77 }
78 }
79}
80
81function down (options) {
82 throw new Error('Not implemented.')
83}
84
85export {
86 up,
87 down
88}
diff --git a/server/initializers/migrations/0350-video-blacklist-type.ts b/server/initializers/migrations/0350-video-blacklist-type.ts
new file mode 100644
index 000000000..4849020ef
--- /dev/null
+++ b/server/initializers/migrations/0350-video-blacklist-type.ts
@@ -0,0 +1,64 @@
1import * as Sequelize from 'sequelize'
2import { VideoBlacklistType } from '../../../shared/models/videos'
3
4async function up (utils: {
5 transaction: Sequelize.Transaction,
6 queryInterface: Sequelize.QueryInterface,
7 sequelize: Sequelize.Sequelize,
8 db: any
9}): Promise<void> {
10 {
11 const data = {
12 type: Sequelize.INTEGER,
13 allowNull: true,
14 defaultValue: null
15 }
16
17 await utils.queryInterface.addColumn('videoBlacklist', 'type', data)
18 }
19
20 {
21 const query = 'UPDATE "videoBlacklist" SET "type" = ' + VideoBlacklistType.MANUAL
22 await utils.sequelize.query(query)
23 }
24
25 {
26 const data = {
27 type: Sequelize.INTEGER,
28 allowNull: false,
29 defaultValue: null
30 }
31 await utils.queryInterface.changeColumn('videoBlacklist', 'type', data)
32 }
33
34 {
35 const data = {
36 type: Sequelize.INTEGER,
37 defaultValue: null,
38 allowNull: true
39 }
40 await utils.queryInterface.addColumn('userNotificationSetting', 'videoAutoBlacklistAsModerator', data)
41 }
42
43 {
44 const query = 'UPDATE "userNotificationSetting" SET "videoAutoBlacklistAsModerator" = 3'
45 await utils.sequelize.query(query)
46 }
47
48 {
49 const data = {
50 type: Sequelize.INTEGER,
51 defaultValue: null,
52 allowNull: false
53 }
54 await utils.queryInterface.changeColumn('userNotificationSetting', 'videoAutoBlacklistAsModerator', data)
55 }
56}
57function down (options) {
58 throw new Error('Not implemented.')
59}
60
61export {
62 up,
63 down
64}
diff --git a/server/initializers/migrations/0355-p2p-peer-version.ts b/server/initializers/migrations/0355-p2p-peer-version.ts
new file mode 100644
index 000000000..18f23d9b7
--- /dev/null
+++ b/server/initializers/migrations/0355-p2p-peer-version.ts
@@ -0,0 +1,41 @@
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 {
11 const data = {
12 type: Sequelize.INTEGER,
13 allowNull: true,
14 defaultValue: null
15 }
16 await utils.queryInterface.addColumn('videoStreamingPlaylist', 'p2pMediaLoaderPeerVersion', data)
17 }
18
19 {
20 const query = `UPDATE "videoStreamingPlaylist" SET "p2pMediaLoaderPeerVersion" = 0;`
21 await utils.sequelize.query(query)
22 }
23
24 {
25 const data = {
26 type: Sequelize.INTEGER,
27 allowNull: false,
28 defaultValue: null
29 }
30 await utils.queryInterface.changeColumn('videoStreamingPlaylist', 'p2pMediaLoaderPeerVersion', data)
31 }
32}
33
34function down (options) {
35 throw new Error('Not implemented.')
36}
37
38export {
39 up,
40 down
41}
diff --git a/server/initializers/migrations/0360-notification-instance-follower.ts b/server/initializers/migrations/0360-notification-instance-follower.ts
new file mode 100644
index 000000000..05caf8e1d
--- /dev/null
+++ b/server/initializers/migrations/0360-notification-instance-follower.ts
@@ -0,0 +1,40 @@
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.INTEGER,
12 defaultValue: null,
13 allowNull: true
14 }
15 await utils.queryInterface.addColumn('userNotificationSetting', 'newInstanceFollower', data)
16 }
17
18 {
19 const query = 'UPDATE "userNotificationSetting" SET "newInstanceFollower" = 1'
20 await utils.sequelize.query(query)
21 }
22
23 {
24 const data = {
25 type: Sequelize.INTEGER,
26 defaultValue: null,
27 allowNull: false
28 }
29 await utils.queryInterface.changeColumn('userNotificationSetting', 'newInstanceFollower', data)
30 }
31}
32
33function down (options) {
34 throw new Error('Not implemented.')
35}
36
37export {
38 up,
39 down
40}
diff --git a/server/initializers/migrations/0365-user-admin-flags.ts b/server/initializers/migrations/0365-user-admin-flags.ts
new file mode 100644
index 000000000..20553100a
--- /dev/null
+++ b/server/initializers/migrations/0365-user-admin-flags.ts
@@ -0,0 +1,40 @@
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.INTEGER,
12 defaultValue: null,
13 allowNull: true
14 }
15 await utils.queryInterface.addColumn('user', 'adminFlags', data)
16 }
17
18 {
19 const query = 'UPDATE "user" SET "adminFlags" = 0'
20 await utils.sequelize.query(query)
21 }
22
23 {
24 const data = {
25 type: Sequelize.INTEGER,
26 defaultValue: null,
27 allowNull: false
28 }
29 await utils.queryInterface.changeColumn('user', 'adminFlags', data)
30 }
31}
32
33function down (options) {
34 throw new Error('Not implemented.')
35}
36
37export {
38 up,
39 down
40}
diff --git a/server/initializers/migrations/0370-thumbnail.ts b/server/initializers/migrations/0370-thumbnail.ts
new file mode 100644
index 000000000..384ca1a15
--- /dev/null
+++ b/server/initializers/migrations/0370-thumbnail.ts
@@ -0,0 +1,50 @@
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 = `
11CREATE TABLE IF NOT EXISTS "thumbnail"
12(
13 "id" SERIAL,
14 "filename" VARCHAR(255) NOT NULL,
15 "height" INTEGER DEFAULT NULL,
16 "width" INTEGER DEFAULT NULL,
17 "type" INTEGER NOT NULL,
18 "fileUrl" VARCHAR(255),
19 "videoId" INTEGER REFERENCES "video" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
20 "videoPlaylistId" INTEGER REFERENCES "videoPlaylist" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
21 "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL,
22 "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL,
23 PRIMARY KEY ("id")
24);`
25 await utils.sequelize.query(query)
26 }
27
28 {
29 // All video thumbnails
30 const query = 'INSERT INTO "thumbnail" ("filename", "type", "videoId", "height", "width", "createdAt", "updatedAt")' +
31 'SELECT uuid || \'.jpg\', 1, id, 110, 200, NOW(), NOW() FROM "video"'
32 await utils.sequelize.query(query)
33 }
34
35 {
36 // All video previews
37 const query = 'INSERT INTO "thumbnail" ("filename", "type", "videoId", "height", "width", "createdAt", "updatedAt")' +
38 'SELECT uuid || \'.jpg\', 2, id, 315, 560, NOW(), NOW() FROM "video"'
39 await utils.sequelize.query(query)
40 }
41}
42
43function down (options) {
44 throw new Error('Not implemented.')
45}
46
47export {
48 up,
49 down
50}
diff --git a/server/initializers/migrations/0375-account-description.ts b/server/initializers/migrations/0375-account-description.ts
new file mode 100644
index 000000000..1258563fd
--- /dev/null
+++ b/server/initializers/migrations/0375-account-description.ts
@@ -0,0 +1,25 @@
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 data = {
10 type: Sequelize.STRING(1000),
11 allowNull: true,
12 defaultValue: null
13 }
14
15 await utils.queryInterface.changeColumn('account', 'description', data)
16}
17
18function down (options) {
19 throw new Error('Not implemented.')
20}
21
22export {
23 up,
24 down
25}
diff --git a/server/initializers/migrator.ts b/server/initializers/migrator.ts
index adc2f9fb3..1cb0116b7 100644
--- a/server/initializers/migrator.ts
+++ b/server/initializers/migrator.ts
@@ -3,6 +3,7 @@ import { logger } from '../helpers/logger'
3import { LAST_MIGRATION_VERSION } from './constants' 3import { LAST_MIGRATION_VERSION } from './constants'
4import { sequelizeTypescript } from './database' 4import { sequelizeTypescript } from './database'
5import { readdir } from 'fs-extra' 5import { readdir } from 'fs-extra'
6import { QueryTypes } from 'sequelize'
6 7
7async function migrate () { 8async function migrate () {
8 const tables = await sequelizeTypescript.getQueryInterface().showAllTables() 9 const tables = await sequelizeTypescript.getQueryInterface().showAllTables()
@@ -13,7 +14,12 @@ async function migrate () {
13 14
14 let actualVersion: number | null = null 15 let actualVersion: number | null = null
15 16
16 const [ rows ] = await sequelizeTypescript.query('SELECT "migrationVersion" FROM "application"') 17 const query = 'SELECT "migrationVersion" FROM "application"'
18 const options = {
19 type: QueryTypes.SELECT as QueryTypes.SELECT
20 }
21
22 const rows = await sequelizeTypescript.query<{ migrationVersion: number }>(query, options)
17 if (rows && rows[0] && rows[0].migrationVersion) { 23 if (rows && rows[0] && rows[0].migrationVersion) {
18 actualVersion = rows[0].migrationVersion 24 actualVersion = rows[0].migrationVersion
19 } 25 }
diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts
index 8215840da..25cd40905 100644
--- a/server/lib/activitypub/actor.ts
+++ b/server/lib/activitypub/actor.ts
@@ -12,7 +12,7 @@ import { logger } from '../../helpers/logger'
12import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto' 12import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto'
13import { doRequest, downloadImage } from '../../helpers/requests' 13import { doRequest, downloadImage } from '../../helpers/requests'
14import { getUrlFromWebfinger } from '../../helpers/webfinger' 14import { getUrlFromWebfinger } from '../../helpers/webfinger'
15import { AVATARS_SIZE, CONFIG, MIMETYPES, sequelizeTypescript } from '../../initializers' 15import { AVATARS_SIZE, MIMETYPES, WEBSERVER } from '../../initializers/constants'
16import { AccountModel } from '../../models/account/account' 16import { AccountModel } from '../../models/account/account'
17import { ActorModel } from '../../models/activitypub/actor' 17import { ActorModel } from '../../models/activitypub/actor'
18import { AvatarModel } from '../../models/avatar/avatar' 18import { AvatarModel } from '../../models/avatar/avatar'
@@ -21,6 +21,8 @@ import { VideoChannelModel } from '../../models/video/video-channel'
21import { JobQueue } from '../job-queue' 21import { JobQueue } from '../job-queue'
22import { getServerActor } from '../../helpers/utils' 22import { getServerActor } from '../../helpers/utils'
23import { ActorFetchByUrlType, fetchActorByUrl } from '../../helpers/actor' 23import { ActorFetchByUrlType, fetchActorByUrl } from '../../helpers/actor'
24import { CONFIG } from '../../initializers/config'
25import { sequelizeTypescript } from '../../initializers/database'
24 26
25// Set account keys, this could be long so process after the account creation and do not block the client 27// Set account keys, this could be long so process after the account creation and do not block the client
26function setAsyncActorKeys (actor: ActorModel) { 28function setAsyncActorKeys (actor: ActorModel) {
@@ -44,6 +46,7 @@ async function getOrCreateActorAndServerAndModel (
44) { 46) {
45 const actorUrl = getAPId(activityActor) 47 const actorUrl = getAPId(activityActor)
46 let created = false 48 let created = false
49 let accountPlaylistsUrl: string
47 50
48 let actor = await fetchActorByUrl(actorUrl, fetchType) 51 let actor = await fetchActorByUrl(actorUrl, fetchType)
49 // Orphan actor (not associated to an account of channel) so recreate it 52 // Orphan actor (not associated to an account of channel) so recreate it
@@ -70,7 +73,8 @@ async function getOrCreateActorAndServerAndModel (
70 73
71 try { 74 try {
72 // Don't recurse another time 75 // Don't recurse another time
73 ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, 'all', false) 76 const recurseIfNeeded = false
77 ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, 'all', recurseIfNeeded)
74 } catch (err) { 78 } catch (err) {
75 logger.error('Cannot get or create account attributed to video channel ' + actor.url) 79 logger.error('Cannot get or create account attributed to video channel ' + actor.url)
76 throw new Error(err) 80 throw new Error(err)
@@ -79,6 +83,7 @@ async function getOrCreateActorAndServerAndModel (
79 83
80 actor = await retryTransactionWrapper(saveActorAndServerAndModelIfNotExist, result, ownerActor) 84 actor = await retryTransactionWrapper(saveActorAndServerAndModelIfNotExist, result, ownerActor)
81 created = true 85 created = true
86 accountPlaylistsUrl = result.playlists
82 } 87 }
83 88
84 if (actor.Account) actor.Account.Actor = actor 89 if (actor.Account) actor.Account.Actor = actor
@@ -92,6 +97,12 @@ async function getOrCreateActorAndServerAndModel (
92 await JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload }) 97 await JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
93 } 98 }
94 99
100 // We created a new account: fetch the playlists
101 if (created === true && actor.Account && accountPlaylistsUrl) {
102 const payload = { uri: accountPlaylistsUrl, accountId: actor.Account.id, type: 'account-playlists' as 'account-playlists' }
103 await JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
104 }
105
95 return actorRefreshed 106 return actorRefreshed
96} 107}
97 108
@@ -107,7 +118,7 @@ function buildActorInstance (type: ActivityPubActorType, url: string, preferredU
107 followingCount: 0, 118 followingCount: 0,
108 inboxUrl: url + '/inbox', 119 inboxUrl: url + '/inbox',
109 outboxUrl: url + '/outbox', 120 outboxUrl: url + '/outbox',
110 sharedInboxUrl: CONFIG.WEBSERVER.URL + '/inbox', 121 sharedInboxUrl: WEBSERVER.URL + '/inbox',
111 followersUrl: url + '/followers', 122 followersUrl: url + '/followers',
112 followingUrl: url + '/following' 123 followingUrl: url + '/following'
113 }) 124 })
@@ -259,7 +270,7 @@ async function refreshActorIfNeeded (
259 return { refreshed: true, actor } 270 return { refreshed: true, actor }
260 }) 271 })
261 } catch (err) { 272 } catch (err) {
262 logger.warn('Cannot refresh actor.', { err }) 273 logger.warn('Cannot refresh actor %s.', actor.url, { err })
263 return { actor, refreshed: false } 274 return { actor, refreshed: false }
264 } 275 }
265} 276}
@@ -333,6 +344,8 @@ function saveActorAndServerAndModelIfNotExist (
333 actorCreated.VideoChannel.Account = ownerActor.Account 344 actorCreated.VideoChannel.Account = ownerActor.Account
334 } 345 }
335 346
347 actorCreated.Server = server
348
336 return actorCreated 349 return actorCreated
337 } 350 }
338} 351}
@@ -342,6 +355,7 @@ type FetchRemoteActorResult = {
342 name: string 355 name: string
343 summary: string 356 summary: string
344 support?: string 357 support?: string
358 playlists?: string
345 avatarName?: string 359 avatarName?: string
346 attributedTo: ActivityPubAttributedTo[] 360 attributedTo: ActivityPubAttributedTo[]
347} 361}
@@ -355,17 +369,18 @@ async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: numbe
355 369
356 logger.info('Fetching remote actor %s.', actorUrl) 370 logger.info('Fetching remote actor %s.', actorUrl)
357 371
358 const requestResult = await doRequest(options) 372 const requestResult = await doRequest<ActivityPubActor>(options)
359 normalizeActor(requestResult.body) 373 normalizeActor(requestResult.body)
360 374
361 const actorJSON: ActivityPubActor = requestResult.body 375 const actorJSON = requestResult.body
362 if (isActorObjectValid(actorJSON) === false) { 376 if (isActorObjectValid(actorJSON) === false) {
363 logger.debug('Remote actor JSON is not valid.', { actorJSON }) 377 logger.debug('Remote actor JSON is not valid.', { actorJSON })
364 return { result: undefined, statusCode: requestResult.response.statusCode } 378 return { result: undefined, statusCode: requestResult.response.statusCode }
365 } 379 }
366 380
367 if (checkUrlsSameHost(actorJSON.id, actorUrl) !== true) { 381 if (checkUrlsSameHost(actorJSON.id, actorUrl) !== true) {
368 throw new Error('Actor url ' + actorUrl + ' has not the same host than its AP id ' + actorJSON.id) 382 logger.warn('Actor url %s has not the same host than its AP id %s', actorUrl, actorJSON.id)
383 return { result: undefined, statusCode: requestResult.response.statusCode }
369 } 384 }
370 385
371 const followersCount = await fetchActorTotalItems(actorJSON.followers) 386 const followersCount = await fetchActorTotalItems(actorJSON.followers)
@@ -398,6 +413,7 @@ async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: numbe
398 avatarName, 413 avatarName,
399 summary: actorJSON.summary, 414 summary: actorJSON.summary,
400 support: actorJSON.support, 415 support: actorJSON.support,
416 playlists: actorJSON.playlists,
401 attributedTo: actorJSON.attributedTo 417 attributedTo: actorJSON.attributedTo
402 } 418 }
403 } 419 }
diff --git a/server/lib/activitypub/audience.ts b/server/lib/activitypub/audience.ts
index 10277eca7..771a01366 100644
--- a/server/lib/activitypub/audience.ts
+++ b/server/lib/activitypub/audience.ts
@@ -1,6 +1,6 @@
1import { Transaction } from 'sequelize' 1import { Transaction } from 'sequelize'
2import { ActivityAudience } from '../../../shared/models/activitypub' 2import { ActivityAudience } from '../../../shared/models/activitypub'
3import { ACTIVITY_PUB } from '../../initializers' 3import { ACTIVITY_PUB } from '../../initializers/constants'
4import { ActorModel } from '../../models/activitypub/actor' 4import { ActorModel } from '../../models/activitypub/actor'
5import { VideoModel } from '../../models/video/video' 5import { VideoModel } from '../../models/video/video'
6import { VideoCommentModel } from '../../models/video/video-comment' 6import { VideoCommentModel } from '../../models/video/video-comment'
diff --git a/server/lib/activitypub/cache-file.ts b/server/lib/activitypub/cache-file.ts
index f6f068b45..de5cc54ac 100644
--- a/server/lib/activitypub/cache-file.ts
+++ b/server/lib/activitypub/cache-file.ts
@@ -2,10 +2,27 @@ import { CacheFileObject } from '../../../shared/index'
2import { VideoModel } from '../../models/video/video' 2import { VideoModel } from '../../models/video/video'
3import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' 3import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy'
4import { Transaction } from 'sequelize' 4import { Transaction } from 'sequelize'
5import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
5 6
6function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject, video: VideoModel, byActor: { id?: number }) { 7function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject, video: VideoModel, byActor: { id?: number }) {
7 const url = cacheFileObject.url
8 8
9 if (cacheFileObject.url.mediaType === 'application/x-mpegURL') {
10 const url = cacheFileObject.url
11
12 const playlist = video.VideoStreamingPlaylists.find(t => t.type === VideoStreamingPlaylistType.HLS)
13 if (!playlist) throw new Error('Cannot find HLS playlist of video ' + video.url)
14
15 return {
16 expiresOn: new Date(cacheFileObject.expires),
17 url: cacheFileObject.id,
18 fileUrl: url.href,
19 strategy: null,
20 videoStreamingPlaylistId: playlist.id,
21 actorId: byActor.id
22 }
23 }
24
25 const url = cacheFileObject.url
9 const videoFile = video.VideoFiles.find(f => { 26 const videoFile = video.VideoFiles.find(f => {
10 return f.resolution === url.height && f.fps === url.fps 27 return f.resolution === url.height && f.fps === url.fps
11 }) 28 })
@@ -15,7 +32,7 @@ function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject
15 return { 32 return {
16 expiresOn: new Date(cacheFileObject.expires), 33 expiresOn: new Date(cacheFileObject.expires),
17 url: cacheFileObject.id, 34 url: cacheFileObject.id,
18 fileUrl: cacheFileObject.url.href, 35 fileUrl: url.href,
19 strategy: null, 36 strategy: null,
20 videoFileId: videoFile.id, 37 videoFileId: videoFile.id,
21 actorId: byActor.id 38 actorId: byActor.id
@@ -51,8 +68,8 @@ function updateCacheFile (
51 68
52 const attributes = cacheFileActivityObjectToDBAttributes(cacheFileObject, video, byActor) 69 const attributes = cacheFileActivityObjectToDBAttributes(cacheFileObject, video, byActor)
53 70
54 redundancyModel.set('expires', attributes.expiresOn) 71 redundancyModel.expiresOn = attributes.expiresOn
55 redundancyModel.set('fileUrl', attributes.fileUrl) 72 redundancyModel.fileUrl = attributes.fileUrl
56 73
57 return redundancyModel.save({ transaction: t }) 74 return redundancyModel.save({ transaction: t })
58} 75}
diff --git a/server/lib/activitypub/crawl.ts b/server/lib/activitypub/crawl.ts
index 1b9b14c2e..686eef04d 100644
--- a/server/lib/activitypub/crawl.ts
+++ b/server/lib/activitypub/crawl.ts
@@ -1,10 +1,14 @@
1import { ACTIVITY_PUB, JOB_REQUEST_TIMEOUT } from '../../initializers' 1import { ACTIVITY_PUB, JOB_REQUEST_TIMEOUT, WEBSERVER } from '../../initializers/constants'
2import { doRequest } from '../../helpers/requests' 2import { doRequest } from '../../helpers/requests'
3import { logger } from '../../helpers/logger' 3import { logger } from '../../helpers/logger'
4import * as Bluebird from 'bluebird' 4import * as Bluebird from 'bluebird'
5import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub' 5import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub'
6import { parse } from 'url'
6 7
7async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Promise<any> | Bluebird<any>) { 8type HandlerFunction<T> = (items: T[]) => (Promise<any> | Bluebird<any>)
9type CleanerFunction = (startedDate: Date) => (Promise<any> | Bluebird<any>)
10
11async function crawlCollectionPage <T> (uri: string, handler: HandlerFunction<T>, cleaner?: CleanerFunction) {
8 logger.info('Crawling ActivityPub data on %s.', uri) 12 logger.info('Crawling ActivityPub data on %s.', uri)
9 13
10 const options = { 14 const options = {
@@ -15,6 +19,8 @@ async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Pr
15 timeout: JOB_REQUEST_TIMEOUT 19 timeout: JOB_REQUEST_TIMEOUT
16 } 20 }
17 21
22 const startDate = new Date()
23
18 const response = await doRequest<ActivityPubOrderedCollection<T>>(options) 24 const response = await doRequest<ActivityPubOrderedCollection<T>>(options)
19 const firstBody = response.body 25 const firstBody = response.body
20 26
@@ -22,6 +28,10 @@ async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Pr
22 let i = 0 28 let i = 0
23 let nextLink = firstBody.first 29 let nextLink = firstBody.first
24 while (nextLink && i < limit) { 30 while (nextLink && i < limit) {
31 // Don't crawl ourselves
32 const remoteHost = parse(nextLink).host
33 if (remoteHost === WEBSERVER.HOST) continue
34
25 options.uri = nextLink 35 options.uri = nextLink
26 36
27 const { body } = await doRequest<ActivityPubOrderedCollection<T>>(options) 37 const { body } = await doRequest<ActivityPubOrderedCollection<T>>(options)
@@ -35,6 +45,8 @@ async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Pr
35 await handler(items) 45 await handler(items)
36 } 46 }
37 } 47 }
48
49 if (cleaner) await cleaner(startDate)
38} 50}
39 51
40export { 52export {
diff --git a/server/lib/activitypub/index.ts b/server/lib/activitypub/index.ts
index 6906bf9d3..d8c7d83b7 100644
--- a/server/lib/activitypub/index.ts
+++ b/server/lib/activitypub/index.ts
@@ -2,6 +2,7 @@ export * from './process'
2export * from './send' 2export * from './send'
3export * from './actor' 3export * from './actor'
4export * from './share' 4export * from './share'
5export * from './playlist'
5export * from './videos' 6export * from './videos'
6export * from './video-comments' 7export * from './video-comments'
7export * from './video-rates' 8export * from './video-rates'
diff --git a/server/lib/activitypub/playlist.ts b/server/lib/activitypub/playlist.ts
new file mode 100644
index 000000000..36a91faec
--- /dev/null
+++ b/server/lib/activitypub/playlist.ts
@@ -0,0 +1,213 @@
1import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object'
2import { crawlCollectionPage } from './crawl'
3import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
4import { AccountModel } from '../../models/account/account'
5import { isArray } from '../../helpers/custom-validators/misc'
6import { getOrCreateActorAndServerAndModel } from './actor'
7import { logger } from '../../helpers/logger'
8import { VideoPlaylistModel } from '../../models/video/video-playlist'
9import { doRequest } from '../../helpers/requests'
10import { checkUrlsSameHost } from '../../helpers/activitypub'
11import * as Bluebird from 'bluebird'
12import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object'
13import { getOrCreateVideoAndAccountAndChannel } from './videos'
14import { isPlaylistElementObjectValid, isPlaylistObjectValid } from '../../helpers/custom-validators/activitypub/playlist'
15import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element'
16import { VideoModel } from '../../models/video/video'
17import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
18import { sequelizeTypescript } from '../../initializers/database'
19import { createPlaylistMiniatureFromUrl } from '../thumbnail'
20import { FilteredModelAttributes } from '../../typings/sequelize'
21
22function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: AccountModel, to: string[]) {
23 const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPlaylistPrivacy.PUBLIC : VideoPlaylistPrivacy.UNLISTED
24
25 return {
26 name: playlistObject.name,
27 description: playlistObject.content,
28 privacy,
29 url: playlistObject.id,
30 uuid: playlistObject.uuid,
31 ownerAccountId: byAccount.id,
32 videoChannelId: null,
33 createdAt: new Date(playlistObject.published),
34 updatedAt: new Date(playlistObject.updated)
35 }
36}
37
38function playlistElementObjectToDBAttributes (elementObject: PlaylistElementObject, videoPlaylist: VideoPlaylistModel, video: VideoModel) {
39 return {
40 position: elementObject.position,
41 url: elementObject.id,
42 startTimestamp: elementObject.startTimestamp || null,
43 stopTimestamp: elementObject.stopTimestamp || null,
44 videoPlaylistId: videoPlaylist.id,
45 videoId: video.id
46 }
47}
48
49async function createAccountPlaylists (playlistUrls: string[], account: AccountModel) {
50 await Bluebird.map(playlistUrls, async playlistUrl => {
51 try {
52 const exists = await VideoPlaylistModel.doesPlaylistExist(playlistUrl)
53 if (exists === true) return
54
55 // Fetch url
56 const { body } = await doRequest<PlaylistObject>({
57 uri: playlistUrl,
58 json: true,
59 activityPub: true
60 })
61
62 if (!isPlaylistObjectValid(body)) {
63 throw new Error(`Invalid playlist object when fetch account playlists: ${JSON.stringify(body)}`)
64 }
65
66 if (!isArray(body.to)) {
67 throw new Error('Playlist does not have an audience.')
68 }
69
70 return createOrUpdateVideoPlaylist(body, account, body.to)
71 } catch (err) {
72 logger.warn('Cannot add playlist element %s.', playlistUrl, { err })
73 }
74 }, { concurrency: CRAWL_REQUEST_CONCURRENCY })
75}
76
77async function createOrUpdateVideoPlaylist (playlistObject: PlaylistObject, byAccount: AccountModel, to: string[]) {
78 const playlistAttributes = playlistObjectToDBAttributes(playlistObject, byAccount, to)
79
80 if (isArray(playlistObject.attributedTo) && playlistObject.attributedTo.length === 1) {
81 const actor = await getOrCreateActorAndServerAndModel(playlistObject.attributedTo[0])
82
83 if (actor.VideoChannel) {
84 playlistAttributes.videoChannelId = actor.VideoChannel.id
85 } else {
86 logger.warn('Attributed to of video playlist %s is not a video channel.', playlistObject.id, { playlistObject })
87 }
88 }
89
90 const [ playlist ] = await VideoPlaylistModel.upsert<VideoPlaylistModel>(playlistAttributes, { returning: true })
91
92 let accItems: string[] = []
93 await crawlCollectionPage<string>(playlistObject.id, items => {
94 accItems = accItems.concat(items)
95
96 return Promise.resolve()
97 })
98
99 const refreshedPlaylist = await VideoPlaylistModel.loadWithAccountAndChannel(playlist.id, null)
100
101 if (playlistObject.icon) {
102 try {
103 const thumbnailModel = await createPlaylistMiniatureFromUrl(playlistObject.icon.url, refreshedPlaylist)
104 await refreshedPlaylist.setAndSaveThumbnail(thumbnailModel, undefined)
105 } catch (err) {
106 logger.warn('Cannot generate thumbnail of %s.', playlistObject.id, { err })
107 }
108 }
109
110 return resetVideoPlaylistElements(accItems, refreshedPlaylist)
111}
112
113async function refreshVideoPlaylistIfNeeded (videoPlaylist: VideoPlaylistModel): Promise<VideoPlaylistModel> {
114 if (!videoPlaylist.isOutdated()) return videoPlaylist
115
116 try {
117 const { statusCode, playlistObject } = await fetchRemoteVideoPlaylist(videoPlaylist.url)
118 if (statusCode === 404) {
119 logger.info('Cannot refresh remote video playlist %s: it does not exist anymore. Deleting it.', videoPlaylist.url)
120
121 await videoPlaylist.destroy()
122 return undefined
123 }
124
125 if (playlistObject === undefined) {
126 logger.warn('Cannot refresh remote playlist %s: invalid body.', videoPlaylist.url)
127
128 await videoPlaylist.setAsRefreshed()
129 return videoPlaylist
130 }
131
132 const byAccount = videoPlaylist.OwnerAccount
133 await createOrUpdateVideoPlaylist(playlistObject, byAccount, playlistObject.to)
134
135 return videoPlaylist
136 } catch (err) {
137 logger.warn('Cannot refresh video playlist %s.', videoPlaylist.url, { err })
138
139 await videoPlaylist.setAsRefreshed()
140 return videoPlaylist
141 }
142}
143
144// ---------------------------------------------------------------------------
145
146export {
147 createAccountPlaylists,
148 playlistObjectToDBAttributes,
149 playlistElementObjectToDBAttributes,
150 createOrUpdateVideoPlaylist,
151 refreshVideoPlaylistIfNeeded
152}
153
154// ---------------------------------------------------------------------------
155
156async function resetVideoPlaylistElements (elementUrls: string[], playlist: VideoPlaylistModel) {
157 const elementsToCreate: FilteredModelAttributes<VideoPlaylistElementModel>[] = []
158
159 await Bluebird.map(elementUrls, async elementUrl => {
160 try {
161 // Fetch url
162 const { body } = await doRequest<PlaylistElementObject>({
163 uri: elementUrl,
164 json: true,
165 activityPub: true
166 })
167
168 if (!isPlaylistElementObjectValid(body)) throw new Error(`Invalid body in video get playlist element ${elementUrl}`)
169
170 if (checkUrlsSameHost(body.id, elementUrl) !== true) {
171 throw new Error(`Playlist element url ${elementUrl} host is different from the AP object id ${body.id}`)
172 }
173
174 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: { id: body.url }, fetchType: 'only-video' })
175
176 elementsToCreate.push(playlistElementObjectToDBAttributes(body, playlist, video))
177 } catch (err) {
178 logger.warn('Cannot add playlist element %s.', elementUrl, { err })
179 }
180 }, { concurrency: CRAWL_REQUEST_CONCURRENCY })
181
182 await sequelizeTypescript.transaction(async t => {
183 await VideoPlaylistElementModel.deleteAllOf(playlist.id, t)
184
185 for (const element of elementsToCreate) {
186 await VideoPlaylistElementModel.create(element, { transaction: t })
187 }
188 })
189
190 logger.info('Reset playlist %s with %s elements.', playlist.url, elementsToCreate.length)
191
192 return undefined
193}
194
195async function fetchRemoteVideoPlaylist (playlistUrl: string): Promise<{ statusCode: number, playlistObject: PlaylistObject }> {
196 const options = {
197 uri: playlistUrl,
198 method: 'GET',
199 json: true,
200 activityPub: true
201 }
202
203 logger.info('Fetching remote playlist %s.', playlistUrl)
204
205 const { response, body } = await doRequest(options)
206
207 if (isPlaylistObjectValid(body) === false || checkUrlsSameHost(body.id, playlistUrl) !== true) {
208 logger.debug('Remote video playlist JSON is not valid.', { body })
209 return { statusCode: response.statusCode, playlistObject: undefined }
210 }
211
212 return { statusCode: response.statusCode, playlistObject: body }
213}
diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts
index 5f4d793a5..e882669ce 100644
--- a/server/lib/activitypub/process/process-create.ts
+++ b/server/lib/activitypub/process/process-create.ts
@@ -12,6 +12,8 @@ import { Notifier } from '../../notifier'
12import { processViewActivity } from './process-view' 12import { processViewActivity } from './process-view'
13import { processDislikeActivity } from './process-dislike' 13import { processDislikeActivity } from './process-dislike'
14import { processFlagActivity } from './process-flag' 14import { processFlagActivity } from './process-flag'
15import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object'
16import { createOrUpdateVideoPlaylist } from '../playlist'
15 17
16async function processCreateActivity (activity: ActivityCreate, byActor: ActorModel) { 18async function processCreateActivity (activity: ActivityCreate, byActor: ActorModel) {
17 const activityObject = activity.object 19 const activityObject = activity.object
@@ -38,7 +40,11 @@ async function processCreateActivity (activity: ActivityCreate, byActor: ActorMo
38 } 40 }
39 41
40 if (activityType === 'CacheFile') { 42 if (activityType === 'CacheFile') {
41 return retryTransactionWrapper(processCacheFile, activity, byActor) 43 return retryTransactionWrapper(processCreateCacheFile, activity, byActor)
44 }
45
46 if (activityType === 'Playlist') {
47 return retryTransactionWrapper(processCreatePlaylist, activity, byActor)
42 } 48 }
43 49
44 logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id }) 50 logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id })
@@ -63,7 +69,7 @@ async function processCreateVideo (activity: ActivityCreate) {
63 return video 69 return video
64} 70}
65 71
66async function processCacheFile (activity: ActivityCreate, byActor: ActorModel) { 72async function processCreateCacheFile (activity: ActivityCreate, byActor: ActorModel) {
67 const cacheFile = activity.object as CacheFileObject 73 const cacheFile = activity.object as CacheFileObject
68 74
69 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: cacheFile.object }) 75 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: cacheFile.object })
@@ -98,3 +104,12 @@ async function processCreateVideoComment (activity: ActivityCreate, byActor: Act
98 104
99 if (created === true) Notifier.Instance.notifyOnNewComment(comment) 105 if (created === true) Notifier.Instance.notifyOnNewComment(comment)
100} 106}
107
108async function processCreatePlaylist (activity: ActivityCreate, byActor: ActorModel) {
109 const playlistObject = activity.object as PlaylistObject
110 const byAccount = byActor.Account
111
112 if (!byAccount) throw new Error('Cannot create video playlist with the non account actor ' + byActor.url)
113
114 await createOrUpdateVideoPlaylist(playlistObject, byAccount, activity.to)
115}
diff --git a/server/lib/activitypub/process/process-delete.ts b/server/lib/activitypub/process/process-delete.ts
index 155d2ffcc..76f07fd8a 100644
--- a/server/lib/activitypub/process/process-delete.ts
+++ b/server/lib/activitypub/process/process-delete.ts
@@ -8,6 +8,7 @@ import { VideoModel } from '../../../models/video/video'
8import { VideoChannelModel } from '../../../models/video/video-channel' 8import { VideoChannelModel } from '../../../models/video/video-channel'
9import { VideoCommentModel } from '../../../models/video/video-comment' 9import { VideoCommentModel } from '../../../models/video/video-comment'
10import { forwardVideoRelatedActivity } from '../send/utils' 10import { forwardVideoRelatedActivity } from '../send/utils'
11import { VideoPlaylistModel } from '../../../models/video/video-playlist'
11 12
12async function processDeleteActivity (activity: ActivityDelete, byActor: ActorModel) { 13async function processDeleteActivity (activity: ActivityDelete, byActor: ActorModel) {
13 const objectUrl = typeof activity.object === 'string' ? activity.object : activity.object.id 14 const objectUrl = typeof activity.object === 'string' ? activity.object : activity.object.id
@@ -45,6 +46,15 @@ async function processDeleteActivity (activity: ActivityDelete, byActor: ActorMo
45 } 46 }
46 } 47 }
47 48
49 {
50 const videoPlaylist = await VideoPlaylistModel.loadByUrlAndPopulateAccount(objectUrl)
51 if (videoPlaylist) {
52 if (videoPlaylist.isOwned()) throw new Error(`Remote instance cannot delete owned playlist ${videoPlaylist.url}.`)
53
54 return retryTransactionWrapper(processDeleteVideoPlaylist, byActor, videoPlaylist)
55 }
56 }
57
48 return undefined 58 return undefined
49} 59}
50 60
@@ -70,6 +80,20 @@ async function processDeleteVideo (actor: ActorModel, videoToDelete: VideoModel)
70 logger.info('Remote video with uuid %s removed.', videoToDelete.uuid) 80 logger.info('Remote video with uuid %s removed.', videoToDelete.uuid)
71} 81}
72 82
83async function processDeleteVideoPlaylist (actor: ActorModel, playlistToDelete: VideoPlaylistModel) {
84 logger.debug('Removing remote video playlist "%s".', playlistToDelete.uuid)
85
86 await sequelizeTypescript.transaction(async t => {
87 if (playlistToDelete.OwnerAccount.Actor.id !== actor.id) {
88 throw new Error('Account ' + actor.url + ' does not own video playlist ' + playlistToDelete.url)
89 }
90
91 await playlistToDelete.destroy({ transaction: t })
92 })
93
94 logger.info('Remote video playlist with uuid %s removed.', playlistToDelete.uuid)
95}
96
73async function processDeleteAccount (accountToRemove: AccountModel) { 97async function processDeleteAccount (accountToRemove: AccountModel) {
74 logger.debug('Removing remote account "%s".', accountToRemove.Actor.uuid) 98 logger.debug('Removing remote account "%s".', accountToRemove.Actor.uuid)
75 99
diff --git a/server/lib/activitypub/process/process-follow.ts b/server/lib/activitypub/process/process-follow.ts
index 0cd537187..ed16ba172 100644
--- a/server/lib/activitypub/process/process-follow.ts
+++ b/server/lib/activitypub/process/process-follow.ts
@@ -4,9 +4,11 @@ import { logger } from '../../../helpers/logger'
4import { sequelizeTypescript } from '../../../initializers' 4import { 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, sendReject } from '../send'
8import { Notifier } from '../../notifier' 8import { Notifier } from '../../notifier'
9import { getAPId } from '../../../helpers/activitypub' 9import { getAPId } from '../../../helpers/activitypub'
10import { getServerActor } from '../../../helpers/utils'
11import { CONFIG } from '../../../initializers/config'
10 12
11async function processFollowActivity (activity: ActivityFollow, byActor: ActorModel) { 13async function processFollowActivity (activity: ActivityFollow, byActor: ActorModel) {
12 const activityObject = getAPId(activity.object) 14 const activityObject = getAPId(activity.object)
@@ -23,12 +25,23 @@ export {
23// --------------------------------------------------------------------------- 25// ---------------------------------------------------------------------------
24 26
25async function processFollow (actor: ActorModel, targetActorURL: string) { 27async function processFollow (actor: ActorModel, targetActorURL: string) {
26 const { actorFollow, created } = await sequelizeTypescript.transaction(async t => { 28 const { actorFollow, created, isFollowingInstance } = await sequelizeTypescript.transaction(async t => {
27 const targetActor = await ActorModel.loadByUrlAndPopulateAccountAndChannel(targetActorURL, t) 29 const targetActor = await ActorModel.loadByUrlAndPopulateAccountAndChannel(targetActorURL, t)
28 30
29 if (!targetActor) throw new Error('Unknown actor') 31 if (!targetActor) throw new Error('Unknown actor')
30 if (targetActor.isOwned() === false) throw new Error('This is not a local actor.') 32 if (targetActor.isOwned() === false) throw new Error('This is not a local actor.')
31 33
34 const serverActor = await getServerActor()
35 const isFollowingInstance = targetActor.id === serverActor.id
36
37 if (isFollowingInstance && CONFIG.FOLLOWERS.INSTANCE.ENABLED === false) {
38 logger.info('Rejecting %s because instance followers are disabled.', targetActor.url)
39
40 await sendReject(actor, targetActor)
41
42 return { actorFollow: undefined }
43 }
44
32 const [ actorFollow, created ] = await ActorFollowModel.findOrCreate({ 45 const [ actorFollow, created ] = await ActorFollowModel.findOrCreate({
33 where: { 46 where: {
34 actorId: actor.id, 47 actorId: actor.id,
@@ -37,15 +50,12 @@ async function processFollow (actor: ActorModel, targetActorURL: string) {
37 defaults: { 50 defaults: {
38 actorId: actor.id, 51 actorId: actor.id,
39 targetActorId: targetActor.id, 52 targetActorId: targetActor.id,
40 state: 'accepted' 53 state: CONFIG.FOLLOWERS.INSTANCE.MANUAL_APPROVAL ? 'pending' : 'accepted'
41 }, 54 },
42 transaction: t 55 transaction: t
43 }) 56 })
44 57
45 actorFollow.ActorFollower = actor 58 if (actorFollow.state !== 'accepted' && CONFIG.FOLLOWERS.INSTANCE.MANUAL_APPROVAL === false) {
46 actorFollow.ActorFollowing = targetActor
47
48 if (actorFollow.state !== 'accepted') {
49 actorFollow.state = 'accepted' 59 actorFollow.state = 'accepted'
50 await actorFollow.save({ transaction: t }) 60 await actorFollow.save({ transaction: t })
51 } 61 }
@@ -54,12 +64,18 @@ async function processFollow (actor: ActorModel, targetActorURL: string) {
54 actorFollow.ActorFollowing = targetActor 64 actorFollow.ActorFollowing = targetActor
55 65
56 // Target sends to actor he accepted the follow request 66 // Target sends to actor he accepted the follow request
57 await sendAccept(actorFollow) 67 if (actorFollow.state === 'accepted') await sendAccept(actorFollow)
58 68
59 return { actorFollow, created } 69 return { actorFollow, created, isFollowingInstance }
60 }) 70 })
61 71
62 if (created) Notifier.Instance.notifyOfNewFollow(actorFollow) 72 // Rejected
73 if (!actorFollow) return
74
75 if (created) {
76 if (isFollowingInstance) Notifier.Instance.notifyOfNewInstanceFollow(actorFollow)
77 else Notifier.Instance.notifyOfNewUserFollow(actorFollow)
78 }
63 79
64 logger.info('Actor %s is followed by actor %s.', targetActorURL, actor.url) 80 logger.info('Actor %s is followed by actor %s.', targetActorURL, actor.url)
65} 81}
diff --git a/server/lib/activitypub/process/process-undo.ts b/server/lib/activitypub/process/process-undo.ts
index ed0177a67..2d48848fe 100644
--- a/server/lib/activitypub/process/process-undo.ts
+++ b/server/lib/activitypub/process/process-undo.ts
@@ -108,7 +108,10 @@ async function processUndoCacheFile (byActor: ActorModel, activity: ActivityUndo
108 108
109 return sequelizeTypescript.transaction(async t => { 109 return sequelizeTypescript.transaction(async t => {
110 const cacheFile = await VideoRedundancyModel.loadByUrl(cacheFileObject.id) 110 const cacheFile = await VideoRedundancyModel.loadByUrl(cacheFileObject.id)
111 if (!cacheFile) throw new Error('Unknown video cache ' + cacheFileObject.id) 111 if (!cacheFile) {
112 logger.debug('Cannot undo unknown video cache %s.', cacheFileObject.id)
113 return
114 }
112 115
113 if (cacheFile.actorId !== byActor.id) throw new Error('Cannot delete redundancy ' + cacheFile.url + ' of another actor.') 116 if (cacheFile.actorId !== byActor.id) throw new Error('Cannot delete redundancy ' + cacheFile.url + ' of another actor.')
114 117
diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts
index c6b42d846..54a9234bb 100644
--- a/server/lib/activitypub/process/process-update.ts
+++ b/server/lib/activitypub/process/process-update.ts
@@ -12,6 +12,8 @@ import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-vali
12import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file' 12import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file'
13import { createOrUpdateCacheFile } from '../cache-file' 13import { createOrUpdateCacheFile } from '../cache-file'
14import { forwardVideoRelatedActivity } from '../send/utils' 14import { forwardVideoRelatedActivity } from '../send/utils'
15import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object'
16import { createOrUpdateVideoPlaylist } from '../playlist'
15 17
16async function processUpdateActivity (activity: ActivityUpdate, byActor: ActorModel) { 18async function processUpdateActivity (activity: ActivityUpdate, byActor: ActorModel) {
17 const objectType = activity.object.type 19 const objectType = activity.object.type
@@ -32,6 +34,10 @@ async function processUpdateActivity (activity: ActivityUpdate, byActor: ActorMo
32 return retryTransactionWrapper(processUpdateCacheFile, byActorFull, activity) 34 return retryTransactionWrapper(processUpdateCacheFile, byActorFull, activity)
33 } 35 }
34 36
37 if (objectType === 'Playlist') {
38 return retryTransactionWrapper(processUpdatePlaylist, byActor, activity)
39 }
40
35 return undefined 41 return undefined
36} 42}
37 43
@@ -114,9 +120,11 @@ async function processUpdateActor (actor: ActorModel, activity: ActivityUpdate)
114 120
115 await actor.save({ transaction: t }) 121 await actor.save({ transaction: t })
116 122
117 accountOrChannelInstance.set('name', actorAttributesToUpdate.name || actorAttributesToUpdate.preferredUsername) 123 accountOrChannelInstance.name = actorAttributesToUpdate.name || actorAttributesToUpdate.preferredUsername
118 accountOrChannelInstance.set('description', actorAttributesToUpdate.summary) 124 accountOrChannelInstance.description = actorAttributesToUpdate.summary
119 accountOrChannelInstance.set('support', actorAttributesToUpdate.support) 125
126 if (accountOrChannelInstance instanceof VideoChannelModel) accountOrChannelInstance.support = actorAttributesToUpdate.support
127
120 await accountOrChannelInstance.save({ transaction: t }) 128 await accountOrChannelInstance.save({ transaction: t })
121 }) 129 })
122 130
@@ -135,3 +143,12 @@ async function processUpdateActor (actor: ActorModel, activity: ActivityUpdate)
135 throw err 143 throw err
136 } 144 }
137} 145}
146
147async function processUpdatePlaylist (byActor: ActorModel, activity: ActivityUpdate) {
148 const playlistObject = activity.object as PlaylistObject
149 const byAccount = byActor.Account
150
151 if (!byAccount) throw new Error('Cannot update video playlist with the non account actor ' + byActor.url)
152
153 await createOrUpdateVideoPlaylist(playlistObject, byAccount, activity.to)
154}
diff --git a/server/lib/activitypub/send/index.ts b/server/lib/activitypub/send/index.ts
index 79ba6c7fe..028936810 100644
--- a/server/lib/activitypub/send/index.ts
+++ b/server/lib/activitypub/send/index.ts
@@ -1,8 +1,10 @@
1export * from './send-accept' 1export * from './send-accept'
2export * from './send-accept'
2export * from './send-announce' 3export * from './send-announce'
3export * from './send-create' 4export * from './send-create'
4export * from './send-delete' 5export * from './send-delete'
5export * from './send-follow' 6export * from './send-follow'
6export * from './send-like' 7export * from './send-like'
8export * from './send-reject'
7export * from './send-undo' 9export * from './send-undo'
8export * from './send-update' 10export * from './send-update'
diff --git a/server/lib/activitypub/send/send-accept.ts b/server/lib/activitypub/send/send-accept.ts
index b6abde13d..388a9ed23 100644
--- a/server/lib/activitypub/send/send-accept.ts
+++ b/server/lib/activitypub/send/send-accept.ts
@@ -17,7 +17,7 @@ async function sendAccept (actorFollow: ActorFollowModel) {
17 17
18 logger.info('Creating job to accept follower %s.', follower.url) 18 logger.info('Creating job to accept follower %s.', follower.url)
19 19
20 const followUrl = getActorFollowActivityPubUrl(actorFollow) 20 const followUrl = getActorFollowActivityPubUrl(follower, me)
21 const followData = buildFollowActivity(followUrl, follower, me) 21 const followData = buildFollowActivity(followUrl, follower, me)
22 22
23 const url = getActorFollowAcceptActivityPubUrl(actorFollow) 23 const url = getActorFollowAcceptActivityPubUrl(actorFollow)
diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts
index e3fca0a17..28f18595b 100644
--- a/server/lib/activitypub/send/send-create.ts
+++ b/server/lib/activitypub/send/send-create.ts
@@ -3,13 +3,14 @@ import { ActivityAudience, ActivityCreate } from '../../../../shared/models/acti
3import { VideoPrivacy } from '../../../../shared/models/videos' 3import { VideoPrivacy } from '../../../../shared/models/videos'
4import { ActorModel } from '../../../models/activitypub/actor' 4import { ActorModel } from '../../../models/activitypub/actor'
5import { VideoModel } from '../../../models/video/video' 5import { VideoModel } from '../../../models/video/video'
6import { VideoAbuseModel } from '../../../models/video/video-abuse'
7import { VideoCommentModel } from '../../../models/video/video-comment' 6import { VideoCommentModel } from '../../../models/video/video-comment'
8import { getVideoAbuseActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoViewActivityPubUrl } from '../url'
9import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' 7import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils'
10import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience' 8import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience'
11import { logger } from '../../../helpers/logger' 9import { logger } from '../../../helpers/logger'
12import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy' 10import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy'
11import { VideoPlaylistModel } from '../../../models/video/video-playlist'
12import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
13import { getServerActor } from '../../../helpers/utils'
13 14
14async function sendCreateVideo (video: VideoModel, t: Transaction) { 15async function sendCreateVideo (video: VideoModel, t: Transaction) {
15 if (video.privacy === VideoPrivacy.PRIVATE) return undefined 16 if (video.privacy === VideoPrivacy.PRIVATE) return undefined
@@ -25,34 +26,36 @@ async function sendCreateVideo (video: VideoModel, t: Transaction) {
25 return broadcastToFollowers(createActivity, byActor, [ byActor ], t) 26 return broadcastToFollowers(createActivity, byActor, [ byActor ], t)
26} 27}
27 28
28async function sendVideoAbuse (byActor: ActorModel, videoAbuse: VideoAbuseModel, video: VideoModel) { 29async function sendCreateCacheFile (byActor: ActorModel, video: VideoModel, fileRedundancy: VideoRedundancyModel) {
29 if (!video.VideoChannel.Account.Actor.serverId) return // Local
30
31 const url = getVideoAbuseActivityPubUrl(videoAbuse)
32
33 logger.info('Creating job to send video abuse %s.', url)
34
35 // Custom audience, we only send the abuse to the origin instance
36 const audience = { to: [ video.VideoChannel.Account.Actor.url ], cc: [] }
37 const createActivity = buildCreateActivity(url, byActor, videoAbuse.toActivityPubObject(), audience)
38
39 return unicastTo(createActivity, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl)
40}
41
42async function sendCreateCacheFile (byActor: ActorModel, fileRedundancy: VideoRedundancyModel) {
43 logger.info('Creating job to send file cache of %s.', fileRedundancy.url) 30 logger.info('Creating job to send file cache of %s.', fileRedundancy.url)
44 31
45 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(fileRedundancy.VideoFile.Video.id)
46 const redundancyObject = fileRedundancy.toActivityPubObject()
47
48 return sendVideoRelatedCreateActivity({ 32 return sendVideoRelatedCreateActivity({
49 byActor, 33 byActor,
50 video, 34 video,
51 url: fileRedundancy.url, 35 url: fileRedundancy.url,
52 object: redundancyObject 36 object: fileRedundancy.toActivityPubObject()
53 }) 37 })
54} 38}
55 39
40async function sendCreateVideoPlaylist (playlist: VideoPlaylistModel, t: Transaction) {
41 if (playlist.privacy === VideoPlaylistPrivacy.PRIVATE) return undefined
42
43 logger.info('Creating job to send create video playlist of %s.', playlist.url)
44
45 const byActor = playlist.OwnerAccount.Actor
46 const audience = getAudience(byActor, playlist.privacy === VideoPlaylistPrivacy.PUBLIC)
47
48 const object = await playlist.toActivityPubObject(null, t)
49 const createActivity = buildCreateActivity(playlist.url, byActor, object, audience)
50
51 const serverActor = await getServerActor()
52 const toFollowersOf = [ byActor, serverActor ]
53
54 if (playlist.VideoChannel) toFollowersOf.push(playlist.VideoChannel.Actor)
55
56 return broadcastToFollowers(createActivity, byActor, toFollowersOf, t)
57}
58
56async function sendCreateVideoComment (comment: VideoCommentModel, t: Transaction) { 59async function sendCreateVideoComment (comment: VideoCommentModel, t: Transaction) {
57 logger.info('Creating job to send comment %s.', comment.url) 60 logger.info('Creating job to send comment %s.', comment.url)
58 61
@@ -91,37 +94,6 @@ async function sendCreateVideoComment (comment: VideoCommentModel, t: Transactio
91 return unicastTo(createActivity, byActor, comment.Video.VideoChannel.Account.Actor.sharedInboxUrl) 94 return unicastTo(createActivity, byActor, comment.Video.VideoChannel.Account.Actor.sharedInboxUrl)
92} 95}
93 96
94async function sendCreateView (byActor: ActorModel, video: VideoModel, t: Transaction) {
95 logger.info('Creating job to send view of %s.', video.url)
96
97 const url = getVideoViewActivityPubUrl(byActor, video)
98 const viewActivity = buildViewActivity(url, byActor, video)
99
100 return sendVideoRelatedCreateActivity({
101 // Use the server actor to send the view
102 byActor,
103 video,
104 url,
105 object: viewActivity,
106 transaction: t
107 })
108}
109
110async function sendCreateDislike (byActor: ActorModel, video: VideoModel, t: Transaction) {
111 logger.info('Creating job to dislike %s.', video.url)
112
113 const url = getVideoDislikeActivityPubUrl(byActor, video)
114 const dislikeActivity = buildDislikeActivity(url, byActor, video)
115
116 return sendVideoRelatedCreateActivity({
117 byActor,
118 video,
119 url,
120 object: dislikeActivity,
121 transaction: t
122 })
123}
124
125function buildCreateActivity (url: string, byActor: ActorModel, object: any, audience?: ActivityAudience): ActivityCreate { 97function buildCreateActivity (url: string, byActor: ActorModel, object: any, audience?: ActivityAudience): ActivityCreate {
126 if (!audience) audience = getAudience(byActor) 98 if (!audience) audience = getAudience(byActor)
127 99
@@ -136,34 +108,13 @@ function buildCreateActivity (url: string, byActor: ActorModel, object: any, aud
136 ) 108 )
137} 109}
138 110
139function buildDislikeActivity (url: string, byActor: ActorModel, video: VideoModel) {
140 return {
141 id: url,
142 type: 'Dislike',
143 actor: byActor.url,
144 object: video.url
145 }
146}
147
148function buildViewActivity (url: string, byActor: ActorModel, video: VideoModel) {
149 return {
150 id: url,
151 type: 'View',
152 actor: byActor.url,
153 object: video.url
154 }
155}
156
157// --------------------------------------------------------------------------- 111// ---------------------------------------------------------------------------
158 112
159export { 113export {
160 sendCreateVideo, 114 sendCreateVideo,
161 sendVideoAbuse,
162 buildCreateActivity, 115 buildCreateActivity,
163 sendCreateView,
164 sendCreateDislike,
165 buildDislikeActivity,
166 sendCreateVideoComment, 116 sendCreateVideoComment,
117 sendCreateVideoPlaylist,
167 sendCreateCacheFile 118 sendCreateCacheFile
168} 119}
169 120
diff --git a/server/lib/activitypub/send/send-delete.ts b/server/lib/activitypub/send/send-delete.ts
index 18969433a..7bf5ca520 100644
--- a/server/lib/activitypub/send/send-delete.ts
+++ b/server/lib/activitypub/send/send-delete.ts
@@ -8,6 +8,8 @@ import { getDeleteActivityPubUrl } from '../url'
8import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' 8import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils'
9import { audiencify, getActorsInvolvedInVideo, getVideoCommentAudience } from '../audience' 9import { audiencify, getActorsInvolvedInVideo, getVideoCommentAudience } from '../audience'
10import { logger } from '../../../helpers/logger' 10import { logger } from '../../../helpers/logger'
11import { VideoPlaylistModel } from '../../../models/video/video-playlist'
12import { getServerActor } from '../../../helpers/utils'
11 13
12async function sendDeleteVideo (video: VideoModel, transaction: Transaction) { 14async function sendDeleteVideo (video: VideoModel, transaction: Transaction) {
13 logger.info('Creating job to broadcast delete of video %s.', video.url) 15 logger.info('Creating job to broadcast delete of video %s.', video.url)
@@ -29,7 +31,12 @@ async function sendDeleteActor (byActor: ActorModel, t: Transaction) {
29 const url = getDeleteActivityPubUrl(byActor.url) 31 const url = getDeleteActivityPubUrl(byActor.url)
30 const activity = buildDeleteActivity(url, byActor.url, byActor) 32 const activity = buildDeleteActivity(url, byActor.url, byActor)
31 33
32 const actorsInvolved = await VideoShareModel.loadActorsByVideoOwner(byActor.id, t) 34 const actorsInvolved = await VideoShareModel.loadActorsWhoSharedVideosOf(byActor.id, t)
35
36 // In case the actor did not have any videos
37 const serverActor = await getServerActor()
38 actorsInvolved.push(serverActor)
39
33 actorsInvolved.push(byActor) 40 actorsInvolved.push(byActor)
34 41
35 return broadcastToFollowers(activity, byActor, actorsInvolved, t) 42 return broadcastToFollowers(activity, byActor, actorsInvolved, t)
@@ -64,12 +71,29 @@ async function sendDeleteVideoComment (videoComment: VideoCommentModel, t: Trans
64 return unicastTo(activity, byActor, videoComment.Video.VideoChannel.Account.Actor.sharedInboxUrl) 71 return unicastTo(activity, byActor, videoComment.Video.VideoChannel.Account.Actor.sharedInboxUrl)
65} 72}
66 73
74async function sendDeleteVideoPlaylist (videoPlaylist: VideoPlaylistModel, t: Transaction) {
75 logger.info('Creating job to send delete of playlist %s.', videoPlaylist.url)
76
77 const byActor = videoPlaylist.OwnerAccount.Actor
78
79 const url = getDeleteActivityPubUrl(videoPlaylist.url)
80 const activity = buildDeleteActivity(url, videoPlaylist.url, byActor)
81
82 const serverActor = await getServerActor()
83 const toFollowersOf = [ byActor, serverActor ]
84
85 if (videoPlaylist.VideoChannel) toFollowersOf.push(videoPlaylist.VideoChannel.Actor)
86
87 return broadcastToFollowers(activity, byActor, toFollowersOf, t)
88}
89
67// --------------------------------------------------------------------------- 90// ---------------------------------------------------------------------------
68 91
69export { 92export {
70 sendDeleteVideo, 93 sendDeleteVideo,
71 sendDeleteActor, 94 sendDeleteActor,
72 sendDeleteVideoComment 95 sendDeleteVideoComment,
96 sendDeleteVideoPlaylist
73} 97}
74 98
75// --------------------------------------------------------------------------- 99// ---------------------------------------------------------------------------
diff --git a/server/lib/activitypub/send/send-dislike.ts b/server/lib/activitypub/send/send-dislike.ts
new file mode 100644
index 000000000..a88436f2c
--- /dev/null
+++ b/server/lib/activitypub/send/send-dislike.ts
@@ -0,0 +1,41 @@
1import { Transaction } from 'sequelize'
2import { ActorModel } from '../../../models/activitypub/actor'
3import { VideoModel } from '../../../models/video/video'
4import { getVideoDislikeActivityPubUrl } from '../url'
5import { logger } from '../../../helpers/logger'
6import { ActivityAudience, ActivityDislike } from '../../../../shared/models/activitypub'
7import { sendVideoRelatedActivity } from './utils'
8import { audiencify, getAudience } from '../audience'
9
10async function sendDislike (byActor: ActorModel, video: VideoModel, t: Transaction) {
11 logger.info('Creating job to dislike %s.', video.url)
12
13 const activityBuilder = (audience: ActivityAudience) => {
14 const url = getVideoDislikeActivityPubUrl(byActor, video)
15
16 return buildDislikeActivity(url, byActor, video, audience)
17 }
18
19 return sendVideoRelatedActivity(activityBuilder, { byActor, video, transaction: t })
20}
21
22function buildDislikeActivity (url: string, byActor: ActorModel, video: VideoModel, audience?: ActivityAudience): ActivityDislike {
23 if (!audience) audience = getAudience(byActor)
24
25 return audiencify(
26 {
27 id: url,
28 type: 'Dislike' as 'Dislike',
29 actor: byActor.url,
30 object: video.url
31 },
32 audience
33 )
34}
35
36// ---------------------------------------------------------------------------
37
38export {
39 sendDislike,
40 buildDislikeActivity
41}
diff --git a/server/lib/activitypub/send/send-flag.ts b/server/lib/activitypub/send/send-flag.ts
new file mode 100644
index 000000000..96a7311b9
--- /dev/null
+++ b/server/lib/activitypub/send/send-flag.ts
@@ -0,0 +1,39 @@
1import { ActorModel } from '../../../models/activitypub/actor'
2import { VideoModel } from '../../../models/video/video'
3import { VideoAbuseModel } from '../../../models/video/video-abuse'
4import { getVideoAbuseActivityPubUrl } from '../url'
5import { unicastTo } from './utils'
6import { logger } from '../../../helpers/logger'
7import { ActivityAudience, ActivityFlag } from '../../../../shared/models/activitypub'
8import { audiencify, getAudience } from '../audience'
9
10async function sendVideoAbuse (byActor: ActorModel, videoAbuse: VideoAbuseModel, video: VideoModel) {
11 if (!video.VideoChannel.Account.Actor.serverId) return // Local user
12
13 const url = getVideoAbuseActivityPubUrl(videoAbuse)
14
15 logger.info('Creating job to send video abuse %s.', url)
16
17 // Custom audience, we only send the abuse to the origin instance
18 const audience = { to: [ video.VideoChannel.Account.Actor.url ], cc: [] }
19 const flagActivity = buildFlagActivity(url, byActor, videoAbuse, audience)
20
21 return unicastTo(flagActivity, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl)
22}
23
24function buildFlagActivity (url: string, byActor: ActorModel, videoAbuse: VideoAbuseModel, audience: ActivityAudience): ActivityFlag {
25 if (!audience) audience = getAudience(byActor)
26
27 const activity = Object.assign(
28 { id: url, actor: byActor.url },
29 videoAbuse.toActivityPubObject()
30 )
31
32 return audiencify(activity, audience)
33}
34
35// ---------------------------------------------------------------------------
36
37export {
38 sendVideoAbuse
39}
diff --git a/server/lib/activitypub/send/send-follow.ts b/server/lib/activitypub/send/send-follow.ts
index 170b46b48..2c3d02014 100644
--- a/server/lib/activitypub/send/send-follow.ts
+++ b/server/lib/activitypub/send/send-follow.ts
@@ -14,7 +14,7 @@ function sendFollow (actorFollow: ActorFollowModel) {
14 14
15 logger.info('Creating job to send follow request to %s.', following.url) 15 logger.info('Creating job to send follow request to %s.', following.url)
16 16
17 const url = getActorFollowActivityPubUrl(actorFollow) 17 const url = getActorFollowActivityPubUrl(me, following)
18 const data = buildFollowActivity(url, me, following) 18 const data = buildFollowActivity(url, me, following)
19 19
20 return unicastTo(data, me, following.inboxUrl) 20 return unicastTo(data, me, following.inboxUrl)
diff --git a/server/lib/activitypub/send/send-reject.ts b/server/lib/activitypub/send/send-reject.ts
new file mode 100644
index 000000000..bac7ff556
--- /dev/null
+++ b/server/lib/activitypub/send/send-reject.ts
@@ -0,0 +1,40 @@
1import { ActivityFollow, ActivityReject } from '../../../../shared/models/activitypub'
2import { ActorModel } from '../../../models/activitypub/actor'
3import { getActorFollowActivityPubUrl, getActorFollowRejectActivityPubUrl } from '../url'
4import { unicastTo } from './utils'
5import { buildFollowActivity } from './send-follow'
6import { logger } from '../../../helpers/logger'
7
8async function sendReject (follower: ActorModel, following: ActorModel) {
9 if (!follower.serverId) { // This should never happen
10 logger.warn('Do not sending reject to local follower.')
11 return
12 }
13
14 logger.info('Creating job to reject follower %s.', follower.url)
15
16 const followUrl = getActorFollowActivityPubUrl(follower, following)
17 const followData = buildFollowActivity(followUrl, follower, following)
18
19 const url = getActorFollowRejectActivityPubUrl(follower, following)
20 const data = buildRejectActivity(url, following, followData)
21
22 return unicastTo(data, following, follower.inboxUrl)
23}
24
25// ---------------------------------------------------------------------------
26
27export {
28 sendReject
29}
30
31// ---------------------------------------------------------------------------
32
33function buildRejectActivity (url: string, byActor: ActorModel, followActivityData: ActivityFollow): ActivityReject {
34 return {
35 type: 'Reject',
36 id: url,
37 actor: byActor.url,
38 object: followActivityData
39 }
40}
diff --git a/server/lib/activitypub/send/send-undo.ts b/server/lib/activitypub/send/send-undo.ts
index bf1b6e117..8727a121e 100644
--- a/server/lib/activitypub/send/send-undo.ts
+++ b/server/lib/activitypub/send/send-undo.ts
@@ -2,7 +2,7 @@ import { Transaction } from 'sequelize'
2import { 2import {
3 ActivityAnnounce, 3 ActivityAnnounce,
4 ActivityAudience, 4 ActivityAudience,
5 ActivityCreate, 5 ActivityCreate, ActivityDislike,
6 ActivityFollow, 6 ActivityFollow,
7 ActivityLike, 7 ActivityLike,
8 ActivityUndo 8 ActivityUndo
@@ -13,13 +13,14 @@ import { VideoModel } from '../../../models/video/video'
13import { getActorFollowActivityPubUrl, getUndoActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from '../url' 13import { getActorFollowActivityPubUrl, getUndoActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from '../url'
14import { broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' 14import { broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils'
15import { audiencify, getAudience } from '../audience' 15import { audiencify, getAudience } from '../audience'
16import { buildCreateActivity, buildDislikeActivity } from './send-create' 16import { buildCreateActivity } from './send-create'
17import { buildFollowActivity } from './send-follow' 17import { buildFollowActivity } from './send-follow'
18import { buildLikeActivity } from './send-like' 18import { buildLikeActivity } from './send-like'
19import { VideoShareModel } from '../../../models/video/video-share' 19import { VideoShareModel } from '../../../models/video/video-share'
20import { buildAnnounceWithVideoAudience } from './send-announce' 20import { buildAnnounceWithVideoAudience } from './send-announce'
21import { logger } from '../../../helpers/logger' 21import { logger } from '../../../helpers/logger'
22import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy' 22import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy'
23import { buildDislikeActivity } from './send-dislike'
23 24
24async function sendUndoFollow (actorFollow: ActorFollowModel, t: Transaction) { 25async function sendUndoFollow (actorFollow: ActorFollowModel, t: Transaction) {
25 const me = actorFollow.ActorFollower 26 const me = actorFollow.ActorFollower
@@ -30,7 +31,7 @@ async function sendUndoFollow (actorFollow: ActorFollowModel, t: Transaction) {
30 31
31 logger.info('Creating job to send an unfollow request to %s.', following.url) 32 logger.info('Creating job to send an unfollow request to %s.', following.url)
32 33
33 const followUrl = getActorFollowActivityPubUrl(actorFollow) 34 const followUrl = getActorFollowActivityPubUrl(me, following)
34 const undoUrl = getUndoActivityPubUrl(followUrl) 35 const undoUrl = getUndoActivityPubUrl(followUrl)
35 36
36 const followActivity = buildFollowActivity(followUrl, me, following) 37 const followActivity = buildFollowActivity(followUrl, me, following)
@@ -65,15 +66,15 @@ async function sendUndoDislike (byActor: ActorModel, video: VideoModel, t: Trans
65 66
66 const dislikeUrl = getVideoDislikeActivityPubUrl(byActor, video) 67 const dislikeUrl = getVideoDislikeActivityPubUrl(byActor, video)
67 const dislikeActivity = buildDislikeActivity(dislikeUrl, byActor, video) 68 const dislikeActivity = buildDislikeActivity(dislikeUrl, byActor, video)
68 const createDislikeActivity = buildCreateActivity(dislikeUrl, byActor, dislikeActivity)
69 69
70 return sendUndoVideoRelatedActivity({ byActor, video, url: dislikeUrl, activity: createDislikeActivity, transaction: t }) 70 return sendUndoVideoRelatedActivity({ byActor, video, url: dislikeUrl, activity: dislikeActivity, transaction: t })
71} 71}
72 72
73async function sendUndoCacheFile (byActor: ActorModel, redundancyModel: VideoRedundancyModel, t: Transaction) { 73async function sendUndoCacheFile (byActor: ActorModel, redundancyModel: VideoRedundancyModel, t: Transaction) {
74 logger.info('Creating job to undo cache file %s.', redundancyModel.url) 74 logger.info('Creating job to undo cache file %s.', redundancyModel.url)
75 75
76 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(redundancyModel.VideoFile.Video.id) 76 const videoId = redundancyModel.getVideo().id
77 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId)
77 const createActivity = buildCreateActivity(redundancyModel.url, byActor, redundancyModel.toActivityPubObject()) 78 const createActivity = buildCreateActivity(redundancyModel.url, byActor, redundancyModel.toActivityPubObject())
78 79
79 return sendUndoVideoRelatedActivity({ byActor, video, url: redundancyModel.url, activity: createActivity, transaction: t }) 80 return sendUndoVideoRelatedActivity({ byActor, video, url: redundancyModel.url, activity: createActivity, transaction: t })
@@ -94,7 +95,7 @@ export {
94function undoActivityData ( 95function undoActivityData (
95 url: string, 96 url: string,
96 byActor: ActorModel, 97 byActor: ActorModel,
97 object: ActivityFollow | ActivityLike | ActivityCreate | ActivityAnnounce, 98 object: ActivityFollow | ActivityLike | ActivityDislike | ActivityCreate | ActivityAnnounce,
98 audience?: ActivityAudience 99 audience?: ActivityAudience
99): ActivityUndo { 100): ActivityUndo {
100 if (!audience) audience = getAudience(byActor) 101 if (!audience) audience = getAudience(byActor)
@@ -114,7 +115,7 @@ async function sendUndoVideoRelatedActivity (options: {
114 byActor: ActorModel, 115 byActor: ActorModel,
115 video: VideoModel, 116 video: VideoModel,
116 url: string, 117 url: string,
117 activity: ActivityFollow | ActivityLike | ActivityCreate | ActivityAnnounce, 118 activity: ActivityFollow | ActivityLike | ActivityDislike | ActivityCreate | ActivityAnnounce,
118 transaction: Transaction 119 transaction: Transaction
119}) { 120}) {
120 const activityBuilder = (audience: ActivityAudience) => { 121 const activityBuilder = (audience: ActivityAudience) => {
diff --git a/server/lib/activitypub/send/send-update.ts b/server/lib/activitypub/send/send-update.ts
index a68f03edf..7411c08d5 100644
--- a/server/lib/activitypub/send/send-update.ts
+++ b/server/lib/activitypub/send/send-update.ts
@@ -12,8 +12,13 @@ import { audiencify, getActorsInvolvedInVideo, getAudience } from '../audience'
12import { logger } from '../../../helpers/logger' 12import { logger } from '../../../helpers/logger'
13import { VideoCaptionModel } from '../../../models/video/video-caption' 13import { VideoCaptionModel } from '../../../models/video/video-caption'
14import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy' 14import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy'
15import { VideoPlaylistModel } from '../../../models/video/video-playlist'
16import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
17import { getServerActor } from '../../../helpers/utils'
15 18
16async function sendUpdateVideo (video: VideoModel, t: Transaction, overrodeByActor?: ActorModel) { 19async function sendUpdateVideo (video: VideoModel, t: Transaction, overrodeByActor?: ActorModel) {
20 if (video.privacy === VideoPrivacy.PRIVATE) return undefined
21
17 logger.info('Creating job to update video %s.', video.url) 22 logger.info('Creating job to update video %s.', video.url)
18 23
19 const byActor = overrodeByActor ? overrodeByActor : video.VideoChannel.Account.Actor 24 const byActor = overrodeByActor ? overrodeByActor : video.VideoChannel.Account.Actor
@@ -47,7 +52,7 @@ async function sendUpdateActor (accountOrChannel: AccountModel | VideoChannelMod
47 let actorsInvolved: ActorModel[] 52 let actorsInvolved: ActorModel[]
48 if (accountOrChannel instanceof AccountModel) { 53 if (accountOrChannel instanceof AccountModel) {
49 // Actors that shared my videos are involved too 54 // Actors that shared my videos are involved too
50 actorsInvolved = await VideoShareModel.loadActorsByVideoOwner(byActor.id, t) 55 actorsInvolved = await VideoShareModel.loadActorsWhoSharedVideosOf(byActor.id, t)
51 } else { 56 } else {
52 // Actors that shared videos of my channel are involved too 57 // Actors that shared videos of my channel are involved too
53 actorsInvolved = await VideoShareModel.loadActorsByVideoChannel(accountOrChannel.id, t) 58 actorsInvolved = await VideoShareModel.loadActorsByVideoChannel(accountOrChannel.id, t)
@@ -61,7 +66,7 @@ async function sendUpdateActor (accountOrChannel: AccountModel | VideoChannelMod
61async function sendUpdateCacheFile (byActor: ActorModel, redundancyModel: VideoRedundancyModel) { 66async function sendUpdateCacheFile (byActor: ActorModel, redundancyModel: VideoRedundancyModel) {
62 logger.info('Creating job to update cache file %s.', redundancyModel.url) 67 logger.info('Creating job to update cache file %s.', redundancyModel.url)
63 68
64 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(redundancyModel.VideoFile.Video.id) 69 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(redundancyModel.getVideo().id)
65 70
66 const activityBuilder = (audience: ActivityAudience) => { 71 const activityBuilder = (audience: ActivityAudience) => {
67 const redundancyObject = redundancyModel.toActivityPubObject() 72 const redundancyObject = redundancyModel.toActivityPubObject()
@@ -73,12 +78,35 @@ async function sendUpdateCacheFile (byActor: ActorModel, redundancyModel: VideoR
73 return sendVideoRelatedActivity(activityBuilder, { byActor, video }) 78 return sendVideoRelatedActivity(activityBuilder, { byActor, video })
74} 79}
75 80
81async function sendUpdateVideoPlaylist (videoPlaylist: VideoPlaylistModel, t: Transaction) {
82 if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) return undefined
83
84 const byActor = videoPlaylist.OwnerAccount.Actor
85
86 logger.info('Creating job to update video playlist %s.', videoPlaylist.url)
87
88 const url = getUpdateActivityPubUrl(videoPlaylist.url, videoPlaylist.updatedAt.toISOString())
89
90 const object = await videoPlaylist.toActivityPubObject(null, t)
91 const audience = getAudience(byActor, videoPlaylist.privacy === VideoPlaylistPrivacy.PUBLIC)
92
93 const updateActivity = buildUpdateActivity(url, byActor, object, audience)
94
95 const serverActor = await getServerActor()
96 const toFollowersOf = [ byActor, serverActor ]
97
98 if (videoPlaylist.VideoChannel) toFollowersOf.push(videoPlaylist.VideoChannel.Actor)
99
100 return broadcastToFollowers(updateActivity, byActor, toFollowersOf, t)
101}
102
76// --------------------------------------------------------------------------- 103// ---------------------------------------------------------------------------
77 104
78export { 105export {
79 sendUpdateActor, 106 sendUpdateActor,
80 sendUpdateVideo, 107 sendUpdateVideo,
81 sendUpdateCacheFile 108 sendUpdateCacheFile,
109 sendUpdateVideoPlaylist
82} 110}
83 111
84// --------------------------------------------------------------------------- 112// ---------------------------------------------------------------------------
diff --git a/server/lib/activitypub/send/send-view.ts b/server/lib/activitypub/send/send-view.ts
new file mode 100644
index 000000000..8ad126be0
--- /dev/null
+++ b/server/lib/activitypub/send/send-view.ts
@@ -0,0 +1,40 @@
1import { Transaction } from 'sequelize'
2import { ActivityAudience, ActivityView } from '../../../../shared/models/activitypub'
3import { ActorModel } from '../../../models/activitypub/actor'
4import { VideoModel } from '../../../models/video/video'
5import { getVideoLikeActivityPubUrl } from '../url'
6import { sendVideoRelatedActivity } from './utils'
7import { audiencify, getAudience } from '../audience'
8import { logger } from '../../../helpers/logger'
9
10async function sendView (byActor: ActorModel, video: VideoModel, t: Transaction) {
11 logger.info('Creating job to send view of %s.', video.url)
12
13 const activityBuilder = (audience: ActivityAudience) => {
14 const url = getVideoLikeActivityPubUrl(byActor, video)
15
16 return buildViewActivity(url, byActor, video, audience)
17 }
18
19 return sendVideoRelatedActivity(activityBuilder, { byActor, video, transaction: t })
20}
21
22function buildViewActivity (url: string, byActor: ActorModel, video: VideoModel, audience?: ActivityAudience): ActivityView {
23 if (!audience) audience = getAudience(byActor)
24
25 return audiencify(
26 {
27 id: url,
28 type: 'View' as 'View',
29 actor: byActor.url,
30 object: video.url
31 },
32 audience
33 )
34}
35
36// ---------------------------------------------------------------------------
37
38export {
39 sendView
40}
diff --git a/server/lib/activitypub/share.ts b/server/lib/activitypub/share.ts
index 1767df0ae..7f38402b6 100644
--- a/server/lib/activitypub/share.ts
+++ b/server/lib/activitypub/share.ts
@@ -10,7 +10,7 @@ import * as Bluebird from 'bluebird'
10import { doRequest } from '../../helpers/requests' 10import { 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/constants'
14import { checkUrlsSameHost, getAPId } 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) {
@@ -54,12 +54,7 @@ async function addVideoShares (shareUrls: string[], instance: VideoModel) {
54 url: shareUrl 54 url: shareUrl
55 } 55 }
56 56
57 await VideoShareModel.findOrCreate({ 57 await VideoShareModel.upsert(entry)
58 where: {
59 url: shareUrl
60 },
61 defaults: entry
62 })
63 } catch (err) { 58 } catch (err) {
64 logger.warn('Cannot add share %s.', shareUrl, { err }) 59 logger.warn('Cannot add share %s.', shareUrl, { err })
65 } 60 }
diff --git a/server/lib/activitypub/url.ts b/server/lib/activitypub/url.ts
index 38f15448c..bcb7a4ee2 100644
--- a/server/lib/activitypub/url.ts
+++ b/server/lib/activitypub/url.ts
@@ -1,35 +1,49 @@
1import { CONFIG } from '../../initializers' 1import { WEBSERVER } from '../../initializers/constants'
2import { ActorModel } from '../../models/activitypub/actor' 2import { ActorModel } from '../../models/activitypub/actor'
3import { ActorFollowModel } from '../../models/activitypub/actor-follow' 3import { ActorFollowModel } from '../../models/activitypub/actor-follow'
4import { VideoModel } from '../../models/video/video' 4import { VideoModel } from '../../models/video/video'
5import { VideoAbuseModel } from '../../models/video/video-abuse' 5import { VideoAbuseModel } from '../../models/video/video-abuse'
6import { VideoCommentModel } from '../../models/video/video-comment' 6import { VideoCommentModel } from '../../models/video/video-comment'
7import { VideoFileModel } from '../../models/video/video-file' 7import { VideoFileModel } from '../../models/video/video-file'
8import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist'
9import { VideoPlaylistModel } from '../../models/video/video-playlist'
8 10
9function getVideoActivityPubUrl (video: VideoModel) { 11function getVideoActivityPubUrl (video: VideoModel) {
10 return CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid 12 return WEBSERVER.URL + '/videos/watch/' + video.uuid
13}
14
15function getVideoPlaylistActivityPubUrl (videoPlaylist: VideoPlaylistModel) {
16 return WEBSERVER.URL + '/video-playlists/' + videoPlaylist.uuid
17}
18
19function getVideoPlaylistElementActivityPubUrl (videoPlaylist: VideoPlaylistModel, video: VideoModel) {
20 return WEBSERVER.URL + '/video-playlists/' + videoPlaylist.uuid + '/' + video.uuid
11} 21}
12 22
13function getVideoCacheFileActivityPubUrl (videoFile: VideoFileModel) { 23function getVideoCacheFileActivityPubUrl (videoFile: VideoFileModel) {
14 const suffixFPS = videoFile.fps && videoFile.fps !== -1 ? '-' + videoFile.fps : '' 24 const suffixFPS = videoFile.fps && videoFile.fps !== -1 ? '-' + videoFile.fps : ''
15 25
16 return `${CONFIG.WEBSERVER.URL}/redundancy/videos/${videoFile.Video.uuid}/${videoFile.resolution}${suffixFPS}` 26 return `${WEBSERVER.URL}/redundancy/videos/${videoFile.Video.uuid}/${videoFile.resolution}${suffixFPS}`
27}
28
29function getVideoCacheStreamingPlaylistActivityPubUrl (video: VideoModel, playlist: VideoStreamingPlaylistModel) {
30 return `${WEBSERVER.URL}/redundancy/streaming-playlists/${playlist.getStringType()}/${video.uuid}`
17} 31}
18 32
19function getVideoCommentActivityPubUrl (video: VideoModel, videoComment: VideoCommentModel) { 33function getVideoCommentActivityPubUrl (video: VideoModel, videoComment: VideoCommentModel) {
20 return CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid + '/comments/' + videoComment.id 34 return WEBSERVER.URL + '/videos/watch/' + video.uuid + '/comments/' + videoComment.id
21} 35}
22 36
23function getVideoChannelActivityPubUrl (videoChannelName: string) { 37function getVideoChannelActivityPubUrl (videoChannelName: string) {
24 return CONFIG.WEBSERVER.URL + '/video-channels/' + videoChannelName 38 return WEBSERVER.URL + '/video-channels/' + videoChannelName
25} 39}
26 40
27function getAccountActivityPubUrl (accountName: string) { 41function getAccountActivityPubUrl (accountName: string) {
28 return CONFIG.WEBSERVER.URL + '/accounts/' + accountName 42 return WEBSERVER.URL + '/accounts/' + accountName
29} 43}
30 44
31function getVideoAbuseActivityPubUrl (videoAbuse: VideoAbuseModel) { 45function getVideoAbuseActivityPubUrl (videoAbuse: VideoAbuseModel) {
32 return CONFIG.WEBSERVER.URL + '/admin/video-abuses/' + videoAbuse.id 46 return WEBSERVER.URL + '/admin/video-abuses/' + videoAbuse.id
33} 47}
34 48
35function getVideoViewActivityPubUrl (byActor: ActorModel, video: VideoModel) { 49function getVideoViewActivityPubUrl (byActor: ActorModel, video: VideoModel) {
@@ -60,11 +74,8 @@ function getVideoDislikesActivityPubUrl (video: VideoModel) {
60 return video.url + '/dislikes' 74 return video.url + '/dislikes'
61} 75}
62 76
63function getActorFollowActivityPubUrl (actorFollow: ActorFollowModel) { 77function getActorFollowActivityPubUrl (follower: ActorModel, following: ActorModel) {
64 const me = actorFollow.ActorFollower 78 return follower.url + '/follows/' + following.id
65 const following = actorFollow.ActorFollowing
66
67 return me.url + '/follows/' + following.id
68} 79}
69 80
70function getActorFollowAcceptActivityPubUrl (actorFollow: ActorFollowModel) { 81function getActorFollowAcceptActivityPubUrl (actorFollow: ActorFollowModel) {
@@ -74,6 +85,10 @@ function getActorFollowAcceptActivityPubUrl (actorFollow: ActorFollowModel) {
74 return follower.url + '/accepts/follows/' + me.id 85 return follower.url + '/accepts/follows/' + me.id
75} 86}
76 87
88function getActorFollowRejectActivityPubUrl (follower: ActorModel, following: ActorModel) {
89 return follower.url + '/rejects/follows/' + following.id
90}
91
77function getVideoAnnounceActivityPubUrl (byActor: ActorModel, video: VideoModel) { 92function getVideoAnnounceActivityPubUrl (byActor: ActorModel, video: VideoModel) {
78 return video.url + '/announces/' + byActor.id 93 return video.url + '/announces/' + byActor.id
79} 94}
@@ -92,6 +107,9 @@ function getUndoActivityPubUrl (originalUrl: string) {
92 107
93export { 108export {
94 getVideoActivityPubUrl, 109 getVideoActivityPubUrl,
110 getVideoPlaylistElementActivityPubUrl,
111 getVideoPlaylistActivityPubUrl,
112 getVideoCacheStreamingPlaylistActivityPubUrl,
95 getVideoChannelActivityPubUrl, 113 getVideoChannelActivityPubUrl,
96 getAccountActivityPubUrl, 114 getAccountActivityPubUrl,
97 getVideoAbuseActivityPubUrl, 115 getVideoAbuseActivityPubUrl,
@@ -103,6 +121,7 @@ export {
103 getVideoViewActivityPubUrl, 121 getVideoViewActivityPubUrl,
104 getVideoLikeActivityPubUrl, 122 getVideoLikeActivityPubUrl,
105 getVideoDislikeActivityPubUrl, 123 getVideoDislikeActivityPubUrl,
124 getActorFollowRejectActivityPubUrl,
106 getVideoCommentActivityPubUrl, 125 getVideoCommentActivityPubUrl,
107 getDeleteActivityPubUrl, 126 getDeleteActivityPubUrl,
108 getVideoSharesActivityPubUrl, 127 getVideoSharesActivityPubUrl,
diff --git a/server/lib/activitypub/video-comments.ts b/server/lib/activitypub/video-comments.ts
index e87301fe7..18f44d50e 100644
--- a/server/lib/activitypub/video-comments.ts
+++ b/server/lib/activitypub/video-comments.ts
@@ -2,7 +2,7 @@ import { VideoCommentObject } from '../../../shared/models/activitypub/objects/v
2import { sanitizeAndCheckVideoCommentObject } from '../../helpers/custom-validators/activitypub/video-comments' 2import { sanitizeAndCheckVideoCommentObject } from '../../helpers/custom-validators/activitypub/video-comments'
3import { logger } from '../../helpers/logger' 3import { logger } from '../../helpers/logger'
4import { doRequest } from '../../helpers/requests' 4import { doRequest } from '../../helpers/requests'
5import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers' 5import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
6import { ActorModel } from '../../models/activitypub/actor' 6import { ActorModel } from '../../models/activitypub/actor'
7import { VideoModel } from '../../models/video/video' 7import { VideoModel } from '../../models/video/video'
8import { VideoCommentModel } from '../../models/video/video-comment' 8import { VideoCommentModel } from '../../models/video/video-comment'
@@ -34,8 +34,7 @@ async function videoCommentActivityObjectToDBAttributes (video: VideoModel, acto
34 accountId: actor.Account.id, 34 accountId: actor.Account.id,
35 inReplyToCommentId, 35 inReplyToCommentId,
36 originCommentId, 36 originCommentId,
37 createdAt: new Date(comment.published), 37 createdAt: new Date(comment.published)
38 updatedAt: new Date(comment.updated)
39 } 38 }
40} 39}
41 40
@@ -74,12 +73,7 @@ async function addVideoComment (videoInstance: VideoModel, commentUrl: string) {
74 const entry = await videoCommentActivityObjectToDBAttributes(videoInstance, actor, body) 73 const entry = await videoCommentActivityObjectToDBAttributes(videoInstance, actor, body)
75 if (!entry) return { created: false } 74 if (!entry) return { created: false }
76 75
77 const [ comment, created ] = await VideoCommentModel.findOrCreate({ 76 const [ comment, created ] = await VideoCommentModel.upsert<VideoCommentModel>(entry, { returning: true })
78 where: {
79 url: body.id
80 },
81 defaults: entry
82 })
83 comment.Account = actor.Account 77 comment.Account = actor.Account
84 comment.Video = videoInstance 78 comment.Video = videoInstance
85 79
diff --git a/server/lib/activitypub/video-rates.ts b/server/lib/activitypub/video-rates.ts
index 45a2b22ea..cda5b2981 100644
--- a/server/lib/activitypub/video-rates.ts
+++ b/server/lib/activitypub/video-rates.ts
@@ -1,17 +1,18 @@
1import { Transaction } from 'sequelize' 1import { Transaction } from 'sequelize'
2import { AccountModel } from '../../models/account/account' 2import { AccountModel } from '../../models/account/account'
3import { VideoModel } from '../../models/video/video' 3import { VideoModel } from '../../models/video/video'
4import { sendCreateDislike, sendLike, sendUndoDislike, sendUndoLike } from './send' 4import { sendLike, sendUndoDislike, sendUndoLike } from './send'
5import { VideoRateType } from '../../../shared/models/videos' 5import { VideoRateType } from '../../../shared/models/videos'
6import * as Bluebird from 'bluebird' 6import * as Bluebird from 'bluebird'
7import { getOrCreateActorAndServerAndModel } from './actor' 7import { getOrCreateActorAndServerAndModel } from './actor'
8import { AccountVideoRateModel } from '../../models/account/account-video-rate' 8import { 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/constants'
11import { doRequest } from '../../helpers/requests' 11import { doRequest } from '../../helpers/requests'
12import { checkUrlsSameHost, getAPId } 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'
15import { sendDislike } from './send/send-dislike'
15 16
16async function createRates (ratesUrl: string[], video: VideoModel, rate: VideoRateType) { 17async function createRates (ratesUrl: string[], video: VideoModel, rate: VideoRateType) {
17 let rateCounts = 0 18 let rateCounts = 0
@@ -37,19 +38,14 @@ async function createRates (ratesUrl: string[], video: VideoModel, rate: VideoRa
37 38
38 const actor = await getOrCreateActorAndServerAndModel(actorUrl) 39 const actor = await getOrCreateActorAndServerAndModel(actorUrl)
39 40
40 const [ , created ] = await AccountVideoRateModel 41 const entry = {
41 .findOrCreate({ 42 videoId: video.id,
42 where: { 43 accountId: actor.Account.id,
43 videoId: video.id, 44 type: rate,
44 accountId: actor.Account.id 45 url: body.id
45 }, 46 }
46 defaults: { 47
47 videoId: video.id, 48 const created = await AccountVideoRateModel.upsert(entry)
48 accountId: actor.Account.id,
49 type: rate,
50 url: body.id
51 }
52 })
53 49
54 if (created) rateCounts += 1 50 if (created) rateCounts += 1
55 } catch (err) { 51 } catch (err) {
@@ -60,7 +56,10 @@ async function createRates (ratesUrl: string[], video: VideoModel, rate: VideoRa
60 logger.info('Adding %d %s to video %s.', rateCounts, rate, video.uuid) 56 logger.info('Adding %d %s to video %s.', rateCounts, rate, video.uuid)
61 57
62 // This is "likes" and "dislikes" 58 // This is "likes" and "dislikes"
63 if (rateCounts !== 0) await video.increment(rate + 's', { by: rateCounts }) 59 if (rateCounts !== 0) {
60 const field = rate === 'like' ? 'likes' : 'dislikes'
61 await video.increment(field, { by: rateCounts })
62 }
64 63
65 return 64 return
66} 65}
@@ -82,7 +81,7 @@ async function sendVideoRateChange (account: AccountModel,
82 // Like 81 // Like
83 if (likes > 0) await sendLike(actor, video, t) 82 if (likes > 0) await sendLike(actor, video, t)
84 // Dislike 83 // Dislike
85 if (dislikes > 0) await sendCreateDislike(actor, video, t) 84 if (dislikes > 0) await sendDislike(actor, video, t)
86} 85}
87 86
88function getRateUrl (rateType: VideoRateType, actor: ActorModel, video: VideoModel) { 87function getRateUrl (rateType: VideoRateType, actor: ActorModel, video: VideoModel) {
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index e1e523499..4f26cb6be 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -2,15 +2,28 @@ import * 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 * as request from 'request' 4import * as request from 'request'
5import { ActivityIconObject, ActivityUrlObject, ActivityVideoUrlObject, VideoState } from '../../../shared/index' 5import {
6 ActivityPlaylistSegmentHashesObject,
7 ActivityPlaylistUrlObject,
8 ActivityUrlObject,
9 ActivityVideoUrlObject,
10 VideoState
11} from '../../../shared/index'
6import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' 12import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
7import { VideoPrivacy } from '../../../shared/models/videos' 13import { VideoPrivacy } from '../../../shared/models/videos'
8import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos' 14import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos'
9import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' 15import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
10import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' 16import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils'
11import { logger } from '../../helpers/logger' 17import { logger } from '../../helpers/logger'
12import { doRequest, downloadImage } from '../../helpers/requests' 18import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
13import { ACTIVITY_PUB, CONFIG, MIMETYPES, REMOTE_SCHEME, sequelizeTypescript, THUMBNAILS_SIZE } from '../../initializers' 19import {
20 ACTIVITY_PUB,
21 MIMETYPES,
22 P2P_MEDIA_LOADER_PEER_VERSION,
23 PREVIEWS_SIZE,
24 REMOTE_SCHEME,
25 STATIC_PATHS
26} from '../../initializers/constants'
14import { ActorModel } from '../../models/activitypub/actor' 27import { ActorModel } from '../../models/activitypub/actor'
15import { TagModel } from '../../models/video/tag' 28import { TagModel } from '../../models/video/tag'
16import { VideoModel } from '../../models/video/video' 29import { VideoModel } from '../../models/video/video'
@@ -30,9 +43,20 @@ import { AccountModel } from '../../models/account/account'
30import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video' 43import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video'
31import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' 44import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
32import { Notifier } from '../notifier' 45import { Notifier } from '../notifier'
46import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist'
47import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
48import { AccountVideoRateModel } from '../../models/account/account-video-rate'
49import { VideoShareModel } from '../../models/video/video-share'
50import { VideoCommentModel } from '../../models/video/video-comment'
51import { sequelizeTypescript } from '../../initializers/database'
52import { createPlaceholderThumbnail, createVideoMiniatureFromUrl } from '../thumbnail'
53import { ThumbnailModel } from '../../models/video/thumbnail'
54import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
55import { join } from 'path'
56import { FilteredModelAttributes } from '../../typings/sequelize'
33 57
34async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) { 58async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) {
35 // If the video is not private and published, we federate it 59 // If the video is not private and is published, we federate it
36 if (video.privacy !== VideoPrivacy.PRIVATE && video.state === VideoState.PUBLISHED) { 60 if (video.privacy !== VideoPrivacy.PRIVATE && video.state === VideoState.PUBLISHED) {
37 // Fetch more attributes that we will need to serialize in AP object 61 // Fetch more attributes that we will need to serialize in AP object
38 if (isArray(video.VideoCaptions) === false) { 62 if (isArray(video.VideoCaptions) === false) {
@@ -84,19 +108,17 @@ async function fetchRemoteVideoDescription (video: VideoModel) {
84 return body.description ? body.description : '' 108 return body.description ? body.description : ''
85} 109}
86 110
87function fetchRemoteVideoStaticFile (video: VideoModel, path: string, reject: Function) { 111function fetchRemoteVideoStaticFile (video: VideoModel, path: string, destPath: string) {
88 const host = video.VideoChannel.Account.Actor.Server.host 112 const url = buildRemoteBaseUrl(video, path)
89 113
90 // We need to provide a callback, if no we could have an uncaught exception 114 // We need to provide a callback, if no we could have an uncaught exception
91 return request.get(REMOTE_SCHEME.HTTP + '://' + host + path, err => { 115 return doRequestAndSaveToFile({ uri: url }, destPath)
92 if (err) reject(err)
93 })
94} 116}
95 117
96function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject) { 118function buildRemoteBaseUrl (video: VideoModel, path: string) {
97 const thumbnailName = video.getThumbnailName() 119 const host = video.VideoChannel.Account.Actor.Server.host
98 120
99 return downloadImage(icon.url, CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName, THUMBNAILS_SIZE) 121 return REMOTE_SCHEME.HTTP + '://' + host + path
100} 122}
101 123
102function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) { 124function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) {
@@ -124,31 +146,43 @@ async function syncVideoExternalAttributes (video: VideoModel, fetchedVideo: Vid
124 const jobPayloads: ActivitypubHttpFetcherPayload[] = [] 146 const jobPayloads: ActivitypubHttpFetcherPayload[] = []
125 147
126 if (syncParam.likes === true) { 148 if (syncParam.likes === true) {
127 await crawlCollectionPage<string>(fetchedVideo.likes, items => createRates(items, video, 'like')) 149 const handler = items => createRates(items, video, 'like')
150 const cleaner = crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'like' as 'like', crawlStartDate)
151
152 await crawlCollectionPage<string>(fetchedVideo.likes, handler, cleaner)
128 .catch(err => logger.error('Cannot add likes of video %s.', video.uuid, { err })) 153 .catch(err => logger.error('Cannot add likes of video %s.', video.uuid, { err }))
129 } else { 154 } else {
130 jobPayloads.push({ uri: fetchedVideo.likes, videoId: video.id, type: 'video-likes' as 'video-likes' }) 155 jobPayloads.push({ uri: fetchedVideo.likes, videoId: video.id, type: 'video-likes' as 'video-likes' })
131 } 156 }
132 157
133 if (syncParam.dislikes === true) { 158 if (syncParam.dislikes === true) {
134 await crawlCollectionPage<string>(fetchedVideo.dislikes, items => createRates(items, video, 'dislike')) 159 const handler = items => createRates(items, video, 'dislike')
160 const cleaner = crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'dislike' as 'dislike', crawlStartDate)
161
162 await crawlCollectionPage<string>(fetchedVideo.dislikes, handler, cleaner)
135 .catch(err => logger.error('Cannot add dislikes of video %s.', video.uuid, { err })) 163 .catch(err => logger.error('Cannot add dislikes of video %s.', video.uuid, { err }))
136 } else { 164 } else {
137 jobPayloads.push({ uri: fetchedVideo.dislikes, videoId: video.id, type: 'video-dislikes' as 'video-dislikes' }) 165 jobPayloads.push({ uri: fetchedVideo.dislikes, videoId: video.id, type: 'video-dislikes' as 'video-dislikes' })
138 } 166 }
139 167
140 if (syncParam.shares === true) { 168 if (syncParam.shares === true) {
141 await crawlCollectionPage<string>(fetchedVideo.shares, items => addVideoShares(items, video)) 169 const handler = items => addVideoShares(items, video)
170 const cleaner = crawlStartDate => VideoShareModel.cleanOldSharesOf(video.id, crawlStartDate)
171
172 await crawlCollectionPage<string>(fetchedVideo.shares, handler, cleaner)
142 .catch(err => logger.error('Cannot add shares of video %s.', video.uuid, { err })) 173 .catch(err => logger.error('Cannot add shares of video %s.', video.uuid, { err }))
143 } else { 174 } else {
144 jobPayloads.push({ uri: fetchedVideo.shares, videoId: video.id, type: 'video-shares' as 'video-shares' }) 175 jobPayloads.push({ uri: fetchedVideo.shares, videoId: video.id, type: 'video-shares' as 'video-shares' })
145 } 176 }
146 177
147 if (syncParam.comments === true) { 178 if (syncParam.comments === true) {
148 await crawlCollectionPage<string>(fetchedVideo.comments, items => addVideoComments(items, video)) 179 const handler = items => addVideoComments(items, video)
180 const cleaner = crawlStartDate => VideoCommentModel.cleanOldCommentsOf(video.id, crawlStartDate)
181
182 await crawlCollectionPage<string>(fetchedVideo.comments, handler, cleaner)
149 .catch(err => logger.error('Cannot add comments of video %s.', video.uuid, { err })) 183 .catch(err => logger.error('Cannot add comments of video %s.', video.uuid, { err }))
150 } else { 184 } else {
151 jobPayloads.push({ uri: fetchedVideo.shares, videoId: video.id, type: 'video-shares' as 'video-shares' }) 185 jobPayloads.push({ uri: fetchedVideo.comments, videoId: video.id, type: 'video-comments' as 'video-comments' })
152 } 186 }
153 187
154 await Bluebird.map(jobPayloads, payload => JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })) 188 await Bluebird.map(jobPayloads, payload => JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload }))
@@ -170,8 +204,7 @@ async function getOrCreateVideoAndAccountAndChannel (options: {
170 204
171 let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType) 205 let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType)
172 if (videoFromDatabase) { 206 if (videoFromDatabase) {
173 207 if (videoFromDatabase.isOutdated() && allowRefresh === true) {
174 if (allowRefresh === true) {
175 const refreshOptions = { 208 const refreshOptions = {
176 video: videoFromDatabase, 209 video: videoFromDatabase,
177 fetchedType: fetchType, 210 fetchedType: fetchType,
@@ -210,6 +243,14 @@ async function updateVideoFromAP (options: {
210 const wasUnlistedVideo = options.video.privacy === VideoPrivacy.UNLISTED 243 const wasUnlistedVideo = options.video.privacy === VideoPrivacy.UNLISTED
211 244
212 try { 245 try {
246 let thumbnailModel: ThumbnailModel
247
248 try {
249 thumbnailModel = await createVideoMiniatureFromUrl(options.videoObject.icon.url, options.video, ThumbnailType.MINIATURE)
250 } catch (err) {
251 logger.warn('Cannot generate thumbnail of %s.', options.videoObject.id, { err })
252 }
253
213 await sequelizeTypescript.transaction(async t => { 254 await sequelizeTypescript.transaction(async t => {
214 const sequelizeOptions = { transaction: t } 255 const sequelizeOptions = { transaction: t }
215 256
@@ -233,17 +274,26 @@ async function updateVideoFromAP (options: {
233 options.video.set('support', videoData.support) 274 options.video.set('support', videoData.support)
234 options.video.set('nsfw', videoData.nsfw) 275 options.video.set('nsfw', videoData.nsfw)
235 options.video.set('commentsEnabled', videoData.commentsEnabled) 276 options.video.set('commentsEnabled', videoData.commentsEnabled)
277 options.video.set('downloadEnabled', videoData.downloadEnabled)
236 options.video.set('waitTranscoding', videoData.waitTranscoding) 278 options.video.set('waitTranscoding', videoData.waitTranscoding)
237 options.video.set('state', videoData.state) 279 options.video.set('state', videoData.state)
238 options.video.set('duration', videoData.duration) 280 options.video.set('duration', videoData.duration)
239 options.video.set('createdAt', videoData.createdAt) 281 options.video.set('createdAt', videoData.createdAt)
240 options.video.set('publishedAt', videoData.publishedAt) 282 options.video.set('publishedAt', videoData.publishedAt)
283 options.video.set('originallyPublishedAt', videoData.originallyPublishedAt)
241 options.video.set('privacy', videoData.privacy) 284 options.video.set('privacy', videoData.privacy)
242 options.video.set('channelId', videoData.channelId) 285 options.video.set('channelId', videoData.channelId)
243 options.video.set('views', videoData.views) 286 options.video.set('views', videoData.views)
244 287
245 await options.video.save(sequelizeOptions) 288 await options.video.save(sequelizeOptions)
246 289
290 if (thumbnailModel) if (thumbnailModel) await options.video.addAndSaveThumbnail(thumbnailModel, t)
291
292 // FIXME: use icon URL instead
293 const previewUrl = buildRemoteBaseUrl(options.video, join(STATIC_PATHS.PREVIEWS, options.video.getPreview().filename))
294 const previewModel = createPlaceholderThumbnail(previewUrl, options.video, ThumbnailType.PREVIEW, PREVIEWS_SIZE)
295 await options.video.addAndSaveThumbnail(previewModel, t)
296
247 { 297 {
248 const videoFileAttributes = videoFileActivityUrlToDBAttributes(options.video, options.videoObject) 298 const videoFileAttributes = videoFileActivityUrlToDBAttributes(options.video, options.videoObject)
249 const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a)) 299 const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a))
@@ -264,6 +314,29 @@ async function updateVideoFromAP (options: {
264 } 314 }
265 315
266 { 316 {
317 const streamingPlaylistAttributes = streamingPlaylistActivityUrlToDBAttributes(
318 options.video,
319 options.videoObject,
320 options.video.VideoFiles
321 )
322 const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a))
323
324 // Remove video files that do not exist anymore
325 const destroyTasks = options.video.VideoStreamingPlaylists
326 .filter(f => !newStreamingPlaylists.find(newPlaylist => newPlaylist.hasSameUniqueKeysThan(f)))
327 .map(f => f.destroy(sequelizeOptions))
328 await Promise.all(destroyTasks)
329
330 // Update or add other one
331 const upsertTasks = streamingPlaylistAttributes.map(a => {
332 return VideoStreamingPlaylistModel.upsert<VideoStreamingPlaylistModel>(a, { returning: true, transaction: t })
333 .then(([ streamingPlaylist ]) => streamingPlaylist)
334 })
335
336 options.video.VideoStreamingPlaylists = await Promise.all(upsertTasks)
337 }
338
339 {
267 // Update Tags 340 // Update Tags
268 const tags = options.videoObject.tag.map(tag => tag.name) 341 const tags = options.videoObject.tag.map(tag => tag.name)
269 const tagInstances = await TagModel.findOrCreateTags(tags, t) 342 const tagInstances = await TagModel.findOrCreateTags(tags, t)
@@ -296,12 +369,6 @@ async function updateVideoFromAP (options: {
296 logger.debug('Cannot update the remote video.', { err }) 369 logger.debug('Cannot update the remote video.', { err })
297 throw err 370 throw err
298 } 371 }
299
300 try {
301 await generateThumbnailFromUrl(options.video, options.videoObject.icon)
302 } catch (err) {
303 logger.warn('Cannot generate thumbnail of %s.', options.videoObject.id, { err })
304 }
305} 372}
306 373
307async function refreshVideoIfNeeded (options: { 374async function refreshVideoIfNeeded (options: {
@@ -361,29 +428,55 @@ export {
361 getOrCreateVideoAndAccountAndChannel, 428 getOrCreateVideoAndAccountAndChannel,
362 fetchRemoteVideoStaticFile, 429 fetchRemoteVideoStaticFile,
363 fetchRemoteVideoDescription, 430 fetchRemoteVideoDescription,
364 generateThumbnailFromUrl,
365 getOrCreateVideoChannelFromVideoObject 431 getOrCreateVideoChannelFromVideoObject
366} 432}
367 433
368// --------------------------------------------------------------------------- 434// ---------------------------------------------------------------------------
369 435
370function isActivityVideoUrlObject (url: ActivityUrlObject): url is ActivityVideoUrlObject { 436function isAPVideoUrlObject (url: ActivityUrlObject): url is ActivityVideoUrlObject {
371 const mimeTypes = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT) 437 const mimeTypes = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT)
372 438
373 const urlMediaType = url.mediaType || url.mimeType 439 const urlMediaType = url.mediaType || url.mimeType
374 return mimeTypes.indexOf(urlMediaType) !== -1 && urlMediaType.startsWith('video/') 440 return mimeTypes.indexOf(urlMediaType) !== -1 && urlMediaType.startsWith('video/')
375} 441}
376 442
443function isAPStreamingPlaylistUrlObject (url: ActivityUrlObject): url is ActivityPlaylistUrlObject {
444 const urlMediaType = url.mediaType || url.mimeType
445
446 return urlMediaType === 'application/x-mpegURL'
447}
448
449function isAPPlaylistSegmentHashesUrlObject (tag: any): tag is ActivityPlaylistSegmentHashesObject {
450 const urlMediaType = tag.mediaType || tag.mimeType
451
452 return tag.name === 'sha256' && tag.type === 'Link' && urlMediaType === 'application/json'
453}
454
377async function createVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) { 455async function createVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) {
378 logger.debug('Adding remote video %s.', videoObject.id) 456 logger.debug('Adding remote video %s.', videoObject.id)
379 457
458 const videoData = await videoActivityObjectToDBAttributes(channelActor.VideoChannel, videoObject, videoObject.to)
459 const video = VideoModel.build(videoData)
460
461 const promiseThumbnail = createVideoMiniatureFromUrl(videoObject.icon.url, video, ThumbnailType.MINIATURE)
462
463 let thumbnailModel: ThumbnailModel
464 if (waitThumbnail === true) {
465 thumbnailModel = await promiseThumbnail
466 }
467
380 const videoCreated: VideoModel = await sequelizeTypescript.transaction(async t => { 468 const videoCreated: VideoModel = await sequelizeTypescript.transaction(async t => {
381 const sequelizeOptions = { transaction: t } 469 const sequelizeOptions = { transaction: t }
382 470
383 const videoData = await videoActivityObjectToDBAttributes(channelActor.VideoChannel, videoObject, videoObject.to)
384 const video = VideoModel.build(videoData)
385
386 const videoCreated = await video.save(sequelizeOptions) 471 const videoCreated = await video.save(sequelizeOptions)
472 videoCreated.VideoChannel = channelActor.VideoChannel
473
474 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
475
476 // FIXME: use icon URL instead
477 const previewUrl = buildRemoteBaseUrl(videoCreated, join(STATIC_PATHS.PREVIEWS, video.generatePreviewName()))
478 const previewModel = createPlaceholderThumbnail(previewUrl, video, ThumbnailType.PREVIEW, PREVIEWS_SIZE)
479 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(previewModel, t)
387 480
388 // Process files 481 // Process files
389 const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject) 482 const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject)
@@ -392,10 +485,16 @@ async function createVideo (videoObject: VideoTorrentObject, channelActor: Actor
392 } 485 }
393 486
394 const videoFilePromises = videoFileAttributes.map(f => VideoFileModel.create(f, { transaction: t })) 487 const videoFilePromises = videoFileAttributes.map(f => VideoFileModel.create(f, { transaction: t }))
395 await Promise.all(videoFilePromises) 488 const videoFiles = await Promise.all(videoFilePromises)
489
490 const videoStreamingPlaylists = streamingPlaylistActivityUrlToDBAttributes(videoCreated, videoObject, videoFiles)
491 const playlistPromises = videoStreamingPlaylists.map(p => VideoStreamingPlaylistModel.create(p, { transaction: t }))
492 await Promise.all(playlistPromises)
396 493
397 // Process tags 494 // Process tags
398 const tags = videoObject.tag.map(t => t.name) 495 const tags = videoObject.tag
496 .filter(t => t.type === 'Hashtag')
497 .map(t => t.name)
399 const tagInstances = await TagModel.findOrCreateTags(tags, t) 498 const tagInstances = await TagModel.findOrCreateTags(tags, t)
400 await videoCreated.$set('Tags', tagInstances, sequelizeOptions) 499 await videoCreated.$set('Tags', tagInstances, sequelizeOptions)
401 500
@@ -407,14 +506,16 @@ async function createVideo (videoObject: VideoTorrentObject, channelActor: Actor
407 506
408 logger.info('Remote video with uuid %s inserted.', videoObject.uuid) 507 logger.info('Remote video with uuid %s inserted.', videoObject.uuid)
409 508
410 videoCreated.VideoChannel = channelActor.VideoChannel
411 return videoCreated 509 return videoCreated
412 }) 510 })
413 511
414 const p = generateThumbnailFromUrl(videoCreated, videoObject.icon) 512 if (waitThumbnail === false) {
415 .catch(err => logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err })) 513 promiseThumbnail.then(thumbnailModel => {
514 thumbnailModel = videoCreated.id
416 515
417 if (waitThumbnail === true) await p 516 return thumbnailModel.save()
517 })
518 }
418 519
419 return videoCreated 520 return videoCreated
420} 521}
@@ -456,12 +557,14 @@ async function videoActivityObjectToDBAttributes (
456 support, 557 support,
457 nsfw: videoObject.sensitive, 558 nsfw: videoObject.sensitive,
458 commentsEnabled: videoObject.commentsEnabled, 559 commentsEnabled: videoObject.commentsEnabled,
560 downloadEnabled: videoObject.downloadEnabled,
459 waitTranscoding: videoObject.waitTranscoding, 561 waitTranscoding: videoObject.waitTranscoding,
460 state: videoObject.state, 562 state: videoObject.state,
461 channelId: videoChannel.id, 563 channelId: videoChannel.id,
462 duration: parseInt(duration, 10), 564 duration: parseInt(duration, 10),
463 createdAt: new Date(videoObject.published), 565 createdAt: new Date(videoObject.published),
464 publishedAt: new Date(videoObject.published), 566 publishedAt: new Date(videoObject.published),
567 originallyPublishedAt: videoObject.originallyPublishedAt ? new Date(videoObject.originallyPublishedAt) : null,
465 // FIXME: updatedAt does not seems to be considered by Sequelize 568 // FIXME: updatedAt does not seems to be considered by Sequelize
466 updatedAt: new Date(videoObject.updated), 569 updatedAt: new Date(videoObject.updated),
467 views: videoObject.views, 570 views: videoObject.views,
@@ -473,13 +576,13 @@ async function videoActivityObjectToDBAttributes (
473} 576}
474 577
475function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: VideoTorrentObject) { 578function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: VideoTorrentObject) {
476 const fileUrls = videoObject.url.filter(u => isActivityVideoUrlObject(u)) as ActivityVideoUrlObject[] 579 const fileUrls = videoObject.url.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[]
477 580
478 if (fileUrls.length === 0) { 581 if (fileUrls.length === 0) {
479 throw new Error('Cannot find video files for ' + video.url) 582 throw new Error('Cannot find video files for ' + video.url)
480 } 583 }
481 584
482 const attributes: VideoFileModel[] = [] 585 const attributes: FilteredModelAttributes<VideoFileModel>[] = []
483 for (const fileUrl of fileUrls) { 586 for (const fileUrl of fileUrls) {
484 // Fetch associated magnet uri 587 // Fetch associated magnet uri
485 const magnet = videoObject.url.find(u => { 588 const magnet = videoObject.url.find(u => {
@@ -502,7 +605,38 @@ function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: Vid
502 size: fileUrl.size, 605 size: fileUrl.size,
503 videoId: video.id, 606 videoId: video.id,
504 fps: fileUrl.fps || -1 607 fps: fileUrl.fps || -1
505 } as VideoFileModel 608 }
609
610 attributes.push(attribute)
611 }
612
613 return attributes
614}
615
616function streamingPlaylistActivityUrlToDBAttributes (video: VideoModel, videoObject: VideoTorrentObject, videoFiles: VideoFileModel[]) {
617 const playlistUrls = videoObject.url.filter(u => isAPStreamingPlaylistUrlObject(u)) as ActivityPlaylistUrlObject[]
618 if (playlistUrls.length === 0) return []
619
620 const attributes: FilteredModelAttributes<VideoStreamingPlaylistModel>[] = []
621 for (const playlistUrlObject of playlistUrls) {
622 const segmentsSha256UrlObject = playlistUrlObject.tag
623 .find(t => {
624 return isAPPlaylistSegmentHashesUrlObject(t)
625 }) as ActivityPlaylistSegmentHashesObject
626 if (!segmentsSha256UrlObject) {
627 logger.warn('No segment sha256 URL found in AP playlist object.', { playlistUrl: playlistUrlObject })
628 continue
629 }
630
631 const attribute = {
632 type: VideoStreamingPlaylistType.HLS,
633 playlistUrl: playlistUrlObject.href,
634 segmentsSha256Url: segmentsSha256UrlObject.href,
635 p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrlObject.href, videoFiles),
636 p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION,
637 videoId: video.id
638 }
639
506 attributes.push(attribute) 640 attributes.push(attribute)
507 } 641 }
508 642
diff --git a/server/lib/avatar.ts b/server/lib/avatar.ts
index 021426a1a..09b4e38ca 100644
--- a/server/lib/avatar.ts
+++ b/server/lib/avatar.ts
@@ -1,6 +1,6 @@
1import 'multer' 1import 'multer'
2import { sendUpdateActor } from './activitypub/send' 2import { sendUpdateActor } from './activitypub/send'
3import { AVATARS_SIZE, CONFIG, sequelizeTypescript } from '../initializers' 3import { AVATARS_SIZE } from '../initializers/constants'
4import { updateActorAvatarInstance } from './activitypub' 4import { updateActorAvatarInstance } from './activitypub'
5import { processImage } from '../helpers/image-utils' 5import { processImage } from '../helpers/image-utils'
6import { AccountModel } from '../models/account/account' 6import { AccountModel } from '../models/account/account'
@@ -8,12 +8,14 @@ import { VideoChannelModel } from '../models/video/video-channel'
8import { extname, join } from 'path' 8import { extname, join } from 'path'
9import { retryTransactionWrapper } from '../helpers/database-utils' 9import { retryTransactionWrapper } from '../helpers/database-utils'
10import * as uuidv4 from 'uuid/v4' 10import * as uuidv4 from 'uuid/v4'
11import { CONFIG } from '../initializers/config'
12import { sequelizeTypescript } from '../initializers/database'
11 13
12async function updateActorAvatarFile (avatarPhysicalFile: Express.Multer.File, accountOrChannel: AccountModel | VideoChannelModel) { 14async function updateActorAvatarFile (avatarPhysicalFile: Express.Multer.File, accountOrChannel: AccountModel | VideoChannelModel) {
13 const extension = extname(avatarPhysicalFile.filename) 15 const extension = extname(avatarPhysicalFile.filename)
14 const avatarName = uuidv4() + extension 16 const avatarName = uuidv4() + extension
15 const destination = join(CONFIG.STORAGE.AVATARS_DIR, avatarName) 17 const destination = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)
16 await processImage(avatarPhysicalFile, destination, AVATARS_SIZE) 18 await processImage(avatarPhysicalFile.path, destination, AVATARS_SIZE)
17 19
18 return retryTransactionWrapper(() => { 20 return retryTransactionWrapper(() => {
19 return sequelizeTypescript.transaction(async t => { 21 return sequelizeTypescript.transaction(async t => {
diff --git a/server/lib/cache/abstract-video-static-file-cache.ts b/server/lib/cache/abstract-video-static-file-cache.ts
deleted file mode 100644
index 7512f2b9d..000000000
--- a/server/lib/cache/abstract-video-static-file-cache.ts
+++ /dev/null
@@ -1,52 +0,0 @@
1import * as AsyncLRU from 'async-lru'
2import { createWriteStream, remove } from 'fs-extra'
3import { logger } from '../../helpers/logger'
4import { VideoModel } from '../../models/video/video'
5import { fetchRemoteVideoStaticFile } from '../activitypub'
6
7export abstract class AbstractVideoStaticFileCache <T> {
8
9 protected lru
10
11 abstract getFilePath (params: T): Promise<string>
12
13 // Load and save the remote file, then return the local path from filesystem
14 protected abstract loadRemoteFile (key: string): Promise<string>
15
16 init (max: number, maxAge: number) {
17 this.lru = new AsyncLRU({
18 max,
19 maxAge,
20 load: (key, cb) => {
21 this.loadRemoteFile(key)
22 .then(res => cb(null, res))
23 .catch(err => cb(err))
24 }
25 })
26
27 this.lru.on('evict', (obj: { key: string, value: string }) => {
28 remove(obj.value)
29 .then(() => logger.debug('%s evicted from %s', obj.value, this.constructor.name))
30 })
31 }
32
33 protected loadFromLRU (key: string) {
34 return new Promise<string>((res, rej) => {
35 this.lru.get(key, (err, value) => {
36 err ? rej(err) : res(value)
37 })
38 })
39 }
40
41 protected saveRemoteVideoFileAndReturnPath (video: VideoModel, remoteStaticPath: string, destPath: string) {
42 return new Promise<string>((res, rej) => {
43 const req = fetchRemoteVideoStaticFile(video, remoteStaticPath, rej)
44
45 const stream = createWriteStream(destPath)
46
47 req.pipe(stream)
48 .on('error', (err) => rej(err))
49 .on('finish', () => res(destPath))
50 })
51 }
52}
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts
index b2c376e20..516827a05 100644
--- a/server/lib/client-html.ts
+++ b/server/lib/client-html.ts
@@ -1,7 +1,6 @@
1import * as express from 'express' 1import * as express from 'express'
2import * as Bluebird from 'bluebird'
3import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/models/i18n/i18n' 2import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/models/i18n/i18n'
4import { CONFIG, CUSTOM_HTML_TAG_COMMENTS, EMBED_SIZE } from '../initializers' 3import { CUSTOM_HTML_TAG_COMMENTS, EMBED_SIZE, WEBSERVER } from '../initializers/constants'
5import { join } from 'path' 4import { join } from 'path'
6import { escapeHTML } from '../helpers/core-utils' 5import { escapeHTML } from '../helpers/core-utils'
7import { VideoModel } from '../models/video/video' 6import { VideoModel } from '../models/video/video'
@@ -9,10 +8,14 @@ import * as validator from 'validator'
9import { VideoPrivacy } from '../../shared/models/videos' 8import { VideoPrivacy } from '../../shared/models/videos'
10import { readFile } from 'fs-extra' 9import { readFile } from 'fs-extra'
11import { getActivityStreamDuration } from '../models/video/video-format-utils' 10import { getActivityStreamDuration } from '../models/video/video-format-utils'
11import { AccountModel } from '../models/account/account'
12import { VideoChannelModel } from '../models/video/video-channel'
13import * as Bluebird from 'bluebird'
14import { CONFIG } from '../initializers/config'
12 15
13export class ClientHtml { 16export class ClientHtml {
14 17
15 private static htmlCache: { [path: string]: string } = {} 18 private static htmlCache: { [ path: string ]: string } = {}
16 19
17 static invalidCache () { 20 static invalidCache () {
18 ClientHtml.htmlCache = {} 21 ClientHtml.htmlCache = {}
@@ -28,18 +31,14 @@ export class ClientHtml {
28 } 31 }
29 32
30 static async getWatchHTMLPage (videoId: string, req: express.Request, res: express.Response) { 33 static async getWatchHTMLPage (videoId: string, req: express.Request, res: express.Response) {
31 let videoPromise: Bluebird<VideoModel>
32
33 // Let Angular application handle errors 34 // Let Angular application handle errors
34 if (validator.isInt(videoId) || validator.isUUID(videoId, 4)) { 35 if (!validator.isInt(videoId) && !validator.isUUID(videoId, 4)) {
35 videoPromise = VideoModel.loadAndPopulateAccountAndServerAndTags(videoId)
36 } else {
37 return ClientHtml.getIndexHTML(req, res) 36 return ClientHtml.getIndexHTML(req, res)
38 } 37 }
39 38
40 const [ html, video ] = await Promise.all([ 39 const [ html, video ] = await Promise.all([
41 ClientHtml.getIndexHTML(req, res), 40 ClientHtml.getIndexHTML(req, res),
42 videoPromise 41 VideoModel.loadAndPopulateAccountAndServerAndTags(videoId)
43 ]) 42 ])
44 43
45 // Let Angular application handle errors 44 // Let Angular application handle errors
@@ -49,14 +48,44 @@ export class ClientHtml {
49 48
50 let customHtml = ClientHtml.addTitleTag(html, escapeHTML(video.name)) 49 let customHtml = ClientHtml.addTitleTag(html, escapeHTML(video.name))
51 customHtml = ClientHtml.addDescriptionTag(customHtml, escapeHTML(video.description)) 50 customHtml = ClientHtml.addDescriptionTag(customHtml, escapeHTML(video.description))
52 customHtml = ClientHtml.addOpenGraphAndOEmbedTags(customHtml, video) 51 customHtml = ClientHtml.addVideoOpenGraphAndOEmbedTags(customHtml, video)
52
53 return customHtml
54 }
55
56 static async getAccountHTMLPage (nameWithHost: string, req: express.Request, res: express.Response) {
57 return this.getAccountOrChannelHTMLPage(() => AccountModel.loadByNameWithHost(nameWithHost), req, res)
58 }
59
60 static async getVideoChannelHTMLPage (nameWithHost: string, req: express.Request, res: express.Response) {
61 return this.getAccountOrChannelHTMLPage(() => VideoChannelModel.loadByNameWithHostAndPopulateAccount(nameWithHost), req, res)
62 }
63
64 private static async getAccountOrChannelHTMLPage (
65 loader: () => Bluebird<AccountModel | VideoChannelModel>,
66 req: express.Request,
67 res: express.Response
68 ) {
69 const [ html, entity ] = await Promise.all([
70 ClientHtml.getIndexHTML(req, res),
71 loader()
72 ])
73
74 // Let Angular application handle errors
75 if (!entity) {
76 return ClientHtml.getIndexHTML(req, res)
77 }
78
79 let customHtml = ClientHtml.addTitleTag(html, escapeHTML(entity.getDisplayName()))
80 customHtml = ClientHtml.addDescriptionTag(customHtml, escapeHTML(entity.description))
81 customHtml = ClientHtml.addAccountOrChannelMetaTags(customHtml, entity)
53 82
54 return customHtml 83 return customHtml
55 } 84 }
56 85
57 private static async getIndexHTML (req: express.Request, res: express.Response, paramLang?: string) { 86 private static async getIndexHTML (req: express.Request, res: express.Response, paramLang?: string) {
58 const path = ClientHtml.getIndexPath(req, res, paramLang) 87 const path = ClientHtml.getIndexPath(req, res, paramLang)
59 if (ClientHtml.htmlCache[path]) return ClientHtml.htmlCache[path] 88 if (ClientHtml.htmlCache[ path ]) return ClientHtml.htmlCache[ path ]
60 89
61 const buffer = await readFile(path) 90 const buffer = await readFile(path)
62 91
@@ -64,7 +93,7 @@ export class ClientHtml {
64 93
65 html = ClientHtml.addCustomCSS(html) 94 html = ClientHtml.addCustomCSS(html)
66 95
67 ClientHtml.htmlCache[path] = html 96 ClientHtml.htmlCache[ path ] = html
68 97
69 return html 98 return html
70 } 99 }
@@ -78,7 +107,7 @@ export class ClientHtml {
78 107
79 // Save locale in cookies 108 // Save locale in cookies
80 res.cookie('clientLanguage', lang, { 109 res.cookie('clientLanguage', lang, {
81 secure: CONFIG.WEBSERVER.SCHEME === 'https', 110 secure: WEBSERVER.SCHEME === 'https',
82 sameSite: true, 111 sameSite: true,
83 maxAge: 1000 * 3600 * 24 * 90 // 3 months 112 maxAge: 1000 * 3600 * 24 * 90 // 3 months
84 }) 113 })
@@ -114,13 +143,13 @@ export class ClientHtml {
114 return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.CUSTOM_CSS, styleTag) 143 return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.CUSTOM_CSS, styleTag)
115 } 144 }
116 145
117 private static addOpenGraphAndOEmbedTags (htmlStringPage: string, video: VideoModel) { 146 private static addVideoOpenGraphAndOEmbedTags (htmlStringPage: string, video: VideoModel) {
118 const previewUrl = CONFIG.WEBSERVER.URL + video.getPreviewStaticPath() 147 const previewUrl = WEBSERVER.URL + video.getPreviewStaticPath()
119 const videoUrl = CONFIG.WEBSERVER.URL + video.getWatchStaticPath() 148 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
120 149
121 const videoNameEscaped = escapeHTML(video.name) 150 const videoNameEscaped = escapeHTML(video.name)
122 const videoDescriptionEscaped = escapeHTML(video.description) 151 const videoDescriptionEscaped = escapeHTML(video.description)
123 const embedUrl = CONFIG.WEBSERVER.URL + video.getEmbedStaticPath() 152 const embedUrl = WEBSERVER.URL + video.getEmbedStaticPath()
124 153
125 const openGraphMetaTags = { 154 const openGraphMetaTags = {
126 'og:type': 'video', 155 'og:type': 'video',
@@ -152,7 +181,7 @@ export class ClientHtml {
152 const oembedLinkTags = [ 181 const oembedLinkTags = [
153 { 182 {
154 type: 'application/json+oembed', 183 type: 'application/json+oembed',
155 href: CONFIG.WEBSERVER.URL + '/services/oembed?url=' + encodeURIComponent(videoUrl), 184 href: WEBSERVER.URL + '/services/oembed?url=' + encodeURIComponent(videoUrl),
156 title: videoNameEscaped 185 title: videoNameEscaped
157 } 186 }
158 ] 187 ]
@@ -174,7 +203,7 @@ export class ClientHtml {
174 203
175 // Opengraph 204 // Opengraph
176 Object.keys(openGraphMetaTags).forEach(tagName => { 205 Object.keys(openGraphMetaTags).forEach(tagName => {
177 const tagValue = openGraphMetaTags[tagName] 206 const tagValue = openGraphMetaTags[ tagName ]
178 207
179 tagsString += `<meta property="${tagName}" content="${tagValue}" />` 208 tagsString += `<meta property="${tagName}" content="${tagValue}" />`
180 }) 209 })
@@ -190,6 +219,17 @@ export class ClientHtml {
190 // SEO, use origin video url so Google does not index remote videos 219 // SEO, use origin video url so Google does not index remote videos
191 tagsString += `<link rel="canonical" href="${video.url}" />` 220 tagsString += `<link rel="canonical" href="${video.url}" />`
192 221
193 return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.OPENGRAPH_AND_OEMBED, tagsString) 222 return this.addOpenGraphAndOEmbedTags(htmlStringPage, tagsString)
223 }
224
225 private static addAccountOrChannelMetaTags (htmlStringPage: string, entity: AccountModel | VideoChannelModel) {
226 // SEO, use origin account or channel URL
227 const metaTags = `<link rel="canonical" href="${entity.Actor.url}" />`
228
229 return this.addOpenGraphAndOEmbedTags(htmlStringPage, metaTags)
230 }
231
232 private static addOpenGraphAndOEmbedTags (htmlStringPage: string, metaTags: string) {
233 return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.META_TAGS, metaTags)
194 } 234 }
195} 235}
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts
index 99010f473..8c06e9751 100644
--- a/server/lib/emailer.ts
+++ b/server/lib/emailer.ts
@@ -1,7 +1,7 @@
1import { createTransport, Transporter } from 'nodemailer' 1import { createTransport, Transporter } from 'nodemailer'
2import { isTestInstance } from '../helpers/core-utils' 2import { isTestInstance } from '../helpers/core-utils'
3import { bunyanLogger, logger } from '../helpers/logger' 3import { bunyanLogger, logger } from '../helpers/logger'
4import { CONFIG } from '../initializers' 4import { CONFIG } from '../initializers/config'
5import { UserModel } from '../models/account/user' 5import { UserModel } from '../models/account/user'
6import { VideoModel } from '../models/video/video' 6import { VideoModel } from '../models/video/video'
7import { JobQueue } from './job-queue' 7import { JobQueue } from './job-queue'
@@ -12,6 +12,16 @@ import { VideoAbuseModel } from '../models/video/video-abuse'
12import { VideoBlacklistModel } from '../models/video/video-blacklist' 12import { VideoBlacklistModel } from '../models/video/video-blacklist'
13import { VideoImportModel } from '../models/video/video-import' 13import { VideoImportModel } from '../models/video/video-import'
14import { ActorFollowModel } from '../models/activitypub/actor-follow' 14import { ActorFollowModel } from '../models/activitypub/actor-follow'
15import { WEBSERVER } from '../initializers/constants'
16
17type SendEmailOptions = {
18 to: string[]
19 subject: string
20 text: string
21
22 fromDisplayName?: string
23 replyTo?: string
24}
15 25
16class Emailer { 26class Emailer {
17 27
@@ -82,7 +92,7 @@ class Emailer {
82 92
83 addNewVideoFromSubscriberNotification (to: string[], video: VideoModel) { 93 addNewVideoFromSubscriberNotification (to: string[], video: VideoModel) {
84 const channelName = video.VideoChannel.getDisplayName() 94 const channelName = video.VideoChannel.getDisplayName()
85 const videoUrl = CONFIG.WEBSERVER.URL + video.getWatchStaticPath() 95 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
86 96
87 const text = `Hi dear user,\n\n` + 97 const text = `Hi dear user,\n\n` +
88 `Your subscription ${channelName} just published a new video: ${video.name}` + 98 `Your subscription ${channelName} just published a new video: ${video.name}` +
@@ -120,8 +130,26 @@ class Emailer {
120 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 130 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
121 } 131 }
122 132
133 addNewInstanceFollowerNotification (to: string[], actorFollow: ActorFollowModel) {
134 const awaitingApproval = actorFollow.state === 'pending' ? ' awaiting manual approval.' : ''
135
136 const text = `Hi dear admin,\n\n` +
137 `Your instance has a new follower: ${actorFollow.ActorFollower.url}${awaitingApproval}` +
138 `\n\n` +
139 `Cheers,\n` +
140 `PeerTube.`
141
142 const emailPayload: EmailPayload = {
143 to,
144 subject: 'New instance follower',
145 text
146 }
147
148 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
149 }
150
123 myVideoPublishedNotification (to: string[], video: VideoModel) { 151 myVideoPublishedNotification (to: string[], video: VideoModel) {
124 const videoUrl = CONFIG.WEBSERVER.URL + video.getWatchStaticPath() 152 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
125 153
126 const text = `Hi dear user,\n\n` + 154 const text = `Hi dear user,\n\n` +
127 `Your video ${video.name} has been published.` + 155 `Your video ${video.name} has been published.` +
@@ -141,7 +169,7 @@ class Emailer {
141 } 169 }
142 170
143 myVideoImportSuccessNotification (to: string[], videoImport: VideoImportModel) { 171 myVideoImportSuccessNotification (to: string[], videoImport: VideoImportModel) {
144 const videoUrl = CONFIG.WEBSERVER.URL + videoImport.Video.getWatchStaticPath() 172 const videoUrl = WEBSERVER.URL + videoImport.Video.getWatchStaticPath()
145 173
146 const text = `Hi dear user,\n\n` + 174 const text = `Hi dear user,\n\n` +
147 `Your video import ${videoImport.getTargetIdentifier()} is finished.` + 175 `Your video import ${videoImport.getTargetIdentifier()} is finished.` +
@@ -161,7 +189,7 @@ class Emailer {
161 } 189 }
162 190
163 myVideoImportErrorNotification (to: string[], videoImport: VideoImportModel) { 191 myVideoImportErrorNotification (to: string[], videoImport: VideoImportModel) {
164 const importUrl = CONFIG.WEBSERVER.URL + '/my-account/video-imports' 192 const importUrl = WEBSERVER.URL + '/my-account/video-imports'
165 193
166 const text = `Hi dear user,\n\n` + 194 const text = `Hi dear user,\n\n` +
167 `Your video import ${videoImport.getTargetIdentifier()} encountered an error.` + 195 `Your video import ${videoImport.getTargetIdentifier()} encountered an error.` +
@@ -183,7 +211,7 @@ class Emailer {
183 addNewCommentOnMyVideoNotification (to: string[], comment: VideoCommentModel) { 211 addNewCommentOnMyVideoNotification (to: string[], comment: VideoCommentModel) {
184 const accountName = comment.Account.getDisplayName() 212 const accountName = comment.Account.getDisplayName()
185 const video = comment.Video 213 const video = comment.Video
186 const commentUrl = CONFIG.WEBSERVER.URL + comment.getCommentStaticPath() 214 const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath()
187 215
188 const text = `Hi dear user,\n\n` + 216 const text = `Hi dear user,\n\n` +
189 `A new comment has been posted by ${accountName} on your video ${video.name}` + 217 `A new comment has been posted by ${accountName} on your video ${video.name}` +
@@ -205,7 +233,7 @@ class Emailer {
205 addNewCommentMentionNotification (to: string[], comment: VideoCommentModel) { 233 addNewCommentMentionNotification (to: string[], comment: VideoCommentModel) {
206 const accountName = comment.Account.getDisplayName() 234 const accountName = comment.Account.getDisplayName()
207 const video = comment.Video 235 const video = comment.Video
208 const commentUrl = CONFIG.WEBSERVER.URL + comment.getCommentStaticPath() 236 const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath()
209 237
210 const text = `Hi dear user,\n\n` + 238 const text = `Hi dear user,\n\n` +
211 `${accountName} mentioned you on video ${video.name}` + 239 `${accountName} mentioned you on video ${video.name}` +
@@ -225,10 +253,10 @@ class Emailer {
225 } 253 }
226 254
227 addVideoAbuseModeratorsNotification (to: string[], videoAbuse: VideoAbuseModel) { 255 addVideoAbuseModeratorsNotification (to: string[], videoAbuse: VideoAbuseModel) {
228 const videoUrl = CONFIG.WEBSERVER.URL + videoAbuse.Video.getWatchStaticPath() 256 const videoUrl = WEBSERVER.URL + videoAbuse.Video.getWatchStaticPath()
229 257
230 const text = `Hi,\n\n` + 258 const text = `Hi,\n\n` +
231 `${CONFIG.WEBSERVER.HOST} received an abuse for the following video ${videoUrl}\n\n` + 259 `${WEBSERVER.HOST} received an abuse for the following video ${videoUrl}\n\n` +
232 `Cheers,\n` + 260 `Cheers,\n` +
233 `PeerTube.` 261 `PeerTube.`
234 262
@@ -241,15 +269,38 @@ class Emailer {
241 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 269 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
242 } 270 }
243 271
272 addVideoAutoBlacklistModeratorsNotification (to: string[], video: VideoModel) {
273 const VIDEO_AUTO_BLACKLIST_URL = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list'
274 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
275
276 const text = `Hi,\n\n` +
277 `A recently added video was auto-blacklisted and requires moderator review before publishing.` +
278 `\n\n` +
279 `You can view it and take appropriate action on ${videoUrl}` +
280 `\n\n` +
281 `A full list of auto-blacklisted videos can be reviewed here: ${VIDEO_AUTO_BLACKLIST_URL}` +
282 `\n\n` +
283 `Cheers,\n` +
284 `PeerTube.`
285
286 const emailPayload: EmailPayload = {
287 to,
288 subject: '[PeerTube] An auto-blacklisted video is awaiting review',
289 text
290 }
291
292 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
293 }
294
244 addNewUserRegistrationNotification (to: string[], user: UserModel) { 295 addNewUserRegistrationNotification (to: string[], user: UserModel) {
245 const text = `Hi,\n\n` + 296 const text = `Hi,\n\n` +
246 `User ${user.username} just registered on ${CONFIG.WEBSERVER.HOST} PeerTube instance.\n\n` + 297 `User ${user.username} just registered on ${WEBSERVER.HOST} PeerTube instance.\n\n` +
247 `Cheers,\n` + 298 `Cheers,\n` +
248 `PeerTube.` 299 `PeerTube.`
249 300
250 const emailPayload: EmailPayload = { 301 const emailPayload: EmailPayload = {
251 to, 302 to,
252 subject: '[PeerTube] New user registration on ' + CONFIG.WEBSERVER.HOST, 303 subject: '[PeerTube] New user registration on ' + WEBSERVER.HOST,
253 text 304 text
254 } 305 }
255 306
@@ -258,10 +309,10 @@ class Emailer {
258 309
259 addVideoBlacklistNotification (to: string[], videoBlacklist: VideoBlacklistModel) { 310 addVideoBlacklistNotification (to: string[], videoBlacklist: VideoBlacklistModel) {
260 const videoName = videoBlacklist.Video.name 311 const videoName = videoBlacklist.Video.name
261 const videoUrl = CONFIG.WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath() 312 const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath()
262 313
263 const reasonString = videoBlacklist.reason ? ` for the following reason: ${videoBlacklist.reason}` : '' 314 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}.` 315 const blockedString = `Your video ${videoName} (${videoUrl} on ${WEBSERVER.HOST} has been blacklisted${reasonString}.`
265 316
266 const text = 'Hi,\n\n' + 317 const text = 'Hi,\n\n' +
267 blockedString + 318 blockedString +
@@ -279,10 +330,10 @@ class Emailer {
279 } 330 }
280 331
281 addVideoUnblacklistNotification (to: string[], video: VideoModel) { 332 addVideoUnblacklistNotification (to: string[], video: VideoModel) {
282 const videoUrl = CONFIG.WEBSERVER.URL + video.getWatchStaticPath() 333 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
283 334
284 const text = 'Hi,\n\n' + 335 const text = 'Hi,\n\n' +
285 `Your video ${video.name} (${videoUrl}) on ${CONFIG.WEBSERVER.HOST} has been unblacklisted.` + 336 `Your video ${video.name} (${videoUrl}) on ${WEBSERVER.HOST} has been unblacklisted.` +
286 '\n\n' + 337 '\n\n' +
287 'Cheers,\n' + 338 'Cheers,\n' +
288 `PeerTube.` 339 `PeerTube.`
@@ -296,9 +347,9 @@ class Emailer {
296 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 347 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
297 } 348 }
298 349
299 addForgetPasswordEmailJob (to: string, resetPasswordUrl: string) { 350 addPasswordResetEmailJob (to: string, resetPasswordUrl: string) {
300 const text = `Hi dear user,\n\n` + 351 const text = `Hi dear user,\n\n` +
301 `It seems you forgot your password on ${CONFIG.WEBSERVER.HOST}! ` + 352 `A reset password procedure for your account ${to} has been requested on ${WEBSERVER.HOST} ` +
302 `Please follow this link to reset it: ${resetPasswordUrl}\n\n` + 353 `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` + 354 `If you are not the person who initiated this request, please ignore this email.\n\n` +
304 `Cheers,\n` + 355 `Cheers,\n` +
@@ -315,7 +366,7 @@ class Emailer {
315 366
316 addVerifyEmailJob (to: string, verifyEmailUrl: string) { 367 addVerifyEmailJob (to: string, verifyEmailUrl: string) {
317 const text = `Welcome to PeerTube,\n\n` + 368 const text = `Welcome to PeerTube,\n\n` +
318 `To start using PeerTube on ${CONFIG.WEBSERVER.HOST} you must verify your email! ` + 369 `To start using PeerTube on ${WEBSERVER.HOST} you must verify your email! ` +
319 `Please follow this link to verify this email belongs to you: ${verifyEmailUrl}\n\n` + 370 `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` + 371 `If you are not the person who initiated this request, please ignore this email.\n\n` +
321 `Cheers,\n` + 372 `Cheers,\n` +
@@ -333,7 +384,7 @@ class Emailer {
333 addUserBlockJob (user: UserModel, blocked: boolean, reason?: string) { 384 addUserBlockJob (user: UserModel, blocked: boolean, reason?: string) {
334 const reasonString = reason ? ` for the following reason: ${reason}` : '' 385 const reasonString = reason ? ` for the following reason: ${reason}` : ''
335 const blockedWord = blocked ? 'blocked' : 'unblocked' 386 const blockedWord = blocked ? 'blocked' : 'unblocked'
336 const blockedString = `Your account ${user.username} on ${CONFIG.WEBSERVER.HOST} has been ${blockedWord}${reasonString}.` 387 const blockedString = `Your account ${user.username} on ${WEBSERVER.HOST} has been ${blockedWord}${reasonString}.`
337 388
338 const text = 'Hi,\n\n' + 389 const text = 'Hi,\n\n' +
339 blockedString + 390 blockedString +
@@ -378,7 +429,7 @@ class Emailer {
378 429
379 const fromDisplayName = options.fromDisplayName 430 const fromDisplayName = options.fromDisplayName
380 ? options.fromDisplayName 431 ? options.fromDisplayName
381 : CONFIG.WEBSERVER.HOST 432 : WEBSERVER.HOST
382 433
383 return this.transporter.sendMail({ 434 return this.transporter.sendMail({
384 from: `"${fromDisplayName}" <${CONFIG.SMTP.FROM_ADDRESS}>`, 435 from: `"${fromDisplayName}" <${CONFIG.SMTP.FROM_ADDRESS}>`,
@@ -402,5 +453,6 @@ class Emailer {
402// --------------------------------------------------------------------------- 453// ---------------------------------------------------------------------------
403 454
404export { 455export {
405 Emailer 456 Emailer,
457 SendEmailOptions
406} 458}
diff --git a/server/lib/files-cache/abstract-video-static-file-cache.ts b/server/lib/files-cache/abstract-video-static-file-cache.ts
new file mode 100644
index 000000000..1908cfb06
--- /dev/null
+++ b/server/lib/files-cache/abstract-video-static-file-cache.ts
@@ -0,0 +1,30 @@
1import { remove } from 'fs-extra'
2import { logger } from '../../helpers/logger'
3import * as memoizee from 'memoizee'
4
5type GetFilePathResult = { isOwned: boolean, path: string } | undefined
6
7export abstract class AbstractVideoStaticFileCache <T> {
8
9 getFilePath: (params: T) => Promise<GetFilePathResult>
10
11 abstract getFilePathImpl (params: T): Promise<GetFilePathResult>
12
13 // Load and save the remote file, then return the local path from filesystem
14 protected abstract loadRemoteFile (key: string): Promise<GetFilePathResult>
15
16 init (max: number, maxAge: number) {
17 this.getFilePath = memoizee(this.getFilePathImpl, {
18 maxAge,
19 max,
20 promise: true,
21 dispose: (result: GetFilePathResult) => {
22 if (result.isOwned !== true) {
23 remove(result.path)
24 .then(() => logger.debug('%s removed from %s', result.path, this.constructor.name))
25 .catch(err => logger.error('Cannot remove %s from cache %s.', result.path, this.constructor.name, { err }))
26 }
27 }
28 })
29 }
30}
diff --git a/server/lib/cache/actor-follow-score-cache.ts b/server/lib/files-cache/actor-follow-score-cache.ts
index d070bde09..5f8ee806f 100644
--- a/server/lib/cache/actor-follow-score-cache.ts
+++ b/server/lib/files-cache/actor-follow-score-cache.ts
@@ -1,4 +1,4 @@
1import { ACTOR_FOLLOW_SCORE } from '../../initializers' 1import { ACTOR_FOLLOW_SCORE } from '../../initializers/constants'
2import { logger } from '../../helpers/logger' 2import { logger } from '../../helpers/logger'
3 3
4// Cache follows scores, instead of writing them too often in database 4// Cache follows scores, instead of writing them too often in database
diff --git a/server/lib/cache/index.ts b/server/lib/files-cache/index.ts
index e921d04a7..e921d04a7 100644
--- a/server/lib/cache/index.ts
+++ b/server/lib/files-cache/index.ts
diff --git a/server/lib/cache/videos-caption-cache.ts b/server/lib/files-cache/videos-caption-cache.ts
index f240affbc..440c3fde8 100644
--- a/server/lib/cache/videos-caption-cache.ts
+++ b/server/lib/files-cache/videos-caption-cache.ts
@@ -1,8 +1,11 @@
1import { join } from 'path' 1import { join } from 'path'
2import { CACHE, CONFIG } from '../../initializers' 2import { FILES_CACHE } from '../../initializers/constants'
3import { VideoModel } from '../../models/video/video' 3import { VideoModel } from '../../models/video/video'
4import { VideoCaptionModel } from '../../models/video/video-caption' 4import { VideoCaptionModel } from '../../models/video/video-caption'
5import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache' 5import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache'
6import { CONFIG } from '../../initializers/config'
7import { logger } from '../../helpers/logger'
8import { fetchRemoteVideoStaticFile } from '../activitypub'
6 9
7type GetPathParam = { videoId: string, language: string } 10type GetPathParam = { videoId: string, language: string }
8 11
@@ -19,17 +22,19 @@ class VideosCaptionCache extends AbstractVideoStaticFileCache <GetPathParam> {
19 return this.instance || (this.instance = new this()) 22 return this.instance || (this.instance = new this())
20 } 23 }
21 24
22 async getFilePath (params: GetPathParam) { 25 async getFilePathImpl (params: GetPathParam) {
23 const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(params.videoId, params.language) 26 const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(params.videoId, params.language)
24 if (!videoCaption) return undefined 27 if (!videoCaption) return undefined
25 28
26 if (videoCaption.isOwned()) return join(CONFIG.STORAGE.CAPTIONS_DIR, videoCaption.getCaptionName()) 29 if (videoCaption.isOwned()) return { isOwned: true, path: join(CONFIG.STORAGE.CAPTIONS_DIR, videoCaption.getCaptionName()) }
27 30
28 const key = params.videoId + VideosCaptionCache.KEY_DELIMITER + params.language 31 const key = params.videoId + VideosCaptionCache.KEY_DELIMITER + params.language
29 return this.loadFromLRU(key) 32 return this.loadRemoteFile(key)
30 } 33 }
31 34
32 protected async loadRemoteFile (key: string) { 35 protected async loadRemoteFile (key: string) {
36 logger.debug('Loading remote caption file %s.', key)
37
33 const [ videoId, language ] = key.split(VideosCaptionCache.KEY_DELIMITER) 38 const [ videoId, language ] = key.split(VideosCaptionCache.KEY_DELIMITER)
34 39
35 const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(videoId, language) 40 const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(videoId, language)
@@ -41,10 +46,13 @@ class VideosCaptionCache extends AbstractVideoStaticFileCache <GetPathParam> {
41 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId) 46 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId)
42 if (!video) return undefined 47 if (!video) return undefined
43 48
49 // FIXME: use URL
44 const remoteStaticPath = videoCaption.getCaptionStaticPath() 50 const remoteStaticPath = videoCaption.getCaptionStaticPath()
45 const destPath = join(CACHE.VIDEO_CAPTIONS.DIRECTORY, videoCaption.getCaptionName()) 51 const destPath = join(FILES_CACHE.VIDEO_CAPTIONS.DIRECTORY, videoCaption.getCaptionName())
52
53 await fetchRemoteVideoStaticFile(video, remoteStaticPath, destPath)
46 54
47 return this.saveRemoteVideoFileAndReturnPath(video, remoteStaticPath, destPath) 55 return { isOwned: false, path: destPath }
48 } 56 }
49} 57}
50 58
diff --git a/server/lib/cache/videos-preview-cache.ts b/server/lib/files-cache/videos-preview-cache.ts
index a5d6f5b62..14be7f24a 100644
--- a/server/lib/cache/videos-preview-cache.ts
+++ b/server/lib/files-cache/videos-preview-cache.ts
@@ -1,7 +1,9 @@
1import { join } from 'path' 1import { join } from 'path'
2import { CACHE, CONFIG, STATIC_PATHS } from '../../initializers' 2import { FILES_CACHE, STATIC_PATHS } from '../../initializers/constants'
3import { VideoModel } from '../../models/video/video' 3import { VideoModel } from '../../models/video/video'
4import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache' 4import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache'
5import { CONFIG } from '../../initializers/config'
6import { fetchRemoteVideoStaticFile } from '../activitypub'
5 7
6class VideosPreviewCache extends AbstractVideoStaticFileCache <string> { 8class VideosPreviewCache extends AbstractVideoStaticFileCache <string> {
7 9
@@ -15,13 +17,13 @@ class VideosPreviewCache extends AbstractVideoStaticFileCache <string> {
15 return this.instance || (this.instance = new this()) 17 return this.instance || (this.instance = new this())
16 } 18 }
17 19
18 async getFilePath (videoUUID: string) { 20 async getFilePathImpl (videoUUID: string) {
19 const video = await VideoModel.loadByUUIDWithFile(videoUUID) 21 const video = await VideoModel.loadByUUIDWithFile(videoUUID)
20 if (!video) return undefined 22 if (!video) return undefined
21 23
22 if (video.isOwned()) return join(CONFIG.STORAGE.PREVIEWS_DIR, video.getPreviewName()) 24 if (video.isOwned()) return { isOwned: true, path: join(CONFIG.STORAGE.PREVIEWS_DIR, video.getPreview().filename) }
23 25
24 return this.loadFromLRU(videoUUID) 26 return this.loadRemoteFile(videoUUID)
25 } 27 }
26 28
27 protected async loadRemoteFile (key: string) { 29 protected async loadRemoteFile (key: string) {
@@ -30,10 +32,13 @@ class VideosPreviewCache extends AbstractVideoStaticFileCache <string> {
30 32
31 if (video.isOwned()) throw new Error('Cannot load remote preview of owned video.') 33 if (video.isOwned()) throw new Error('Cannot load remote preview of owned video.')
32 34
33 const remoteStaticPath = join(STATIC_PATHS.PREVIEWS, video.getPreviewName()) 35 // FIXME: use URL
34 const destPath = join(CACHE.PREVIEWS.DIRECTORY, video.getPreviewName()) 36 const remoteStaticPath = join(STATIC_PATHS.PREVIEWS, video.getPreview().filename)
37 const destPath = join(FILES_CACHE.PREVIEWS.DIRECTORY, video.getPreview().filename)
35 38
36 return this.saveRemoteVideoFileAndReturnPath(video, remoteStaticPath, destPath) 39 await fetchRemoteVideoStaticFile(video, remoteStaticPath, destPath)
40
41 return { isOwned: false, path: destPath }
37 } 42 }
38} 43}
39 44
diff --git a/server/lib/hls.ts b/server/lib/hls.ts
new file mode 100644
index 000000000..98da4dcd8
--- /dev/null
+++ b/server/lib/hls.ts
@@ -0,0 +1,184 @@
1import { VideoModel } from '../models/video/video'
2import { basename, dirname, join } from 'path'
3import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION } from '../initializers/constants'
4import { close, ensureDir, move, open, outputJSON, pathExists, read, readFile, remove, writeFile } from 'fs-extra'
5import { getVideoFileSize } from '../helpers/ffmpeg-utils'
6import { sha256 } from '../helpers/core-utils'
7import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
8import { logger } from '../helpers/logger'
9import { doRequest, doRequestAndSaveToFile } from '../helpers/requests'
10import { generateRandomString } from '../helpers/utils'
11import { flatten, uniq } from 'lodash'
12import { VideoFileModel } from '../models/video/video-file'
13import { CONFIG } from '../initializers/config'
14import { sequelizeTypescript } from '../initializers/database'
15
16async function updateStreamingPlaylistsInfohashesIfNeeded () {
17 const playlistsToUpdate = await VideoStreamingPlaylistModel.listByIncorrectPeerVersion()
18
19 // Use separate SQL queries, because we could have many videos to update
20 for (const playlist of playlistsToUpdate) {
21 await sequelizeTypescript.transaction(async t => {
22 const videoFiles = await VideoFileModel.listByStreamingPlaylist(playlist.id, t)
23
24 playlist.p2pMediaLoaderInfohashes = VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlist.playlistUrl, videoFiles)
25 playlist.p2pMediaLoaderPeerVersion = P2P_MEDIA_LOADER_PEER_VERSION
26 await playlist.save({ transaction: t })
27 })
28 }
29}
30
31async function updateMasterHLSPlaylist (video: VideoModel) {
32 const directory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)
33 const masterPlaylists: string[] = [ '#EXTM3U', '#EXT-X-VERSION:3' ]
34 const masterPlaylistPath = join(directory, VideoStreamingPlaylistModel.getMasterHlsPlaylistFilename())
35
36 for (const file of video.VideoFiles) {
37 // If we did not generated a playlist for this resolution, skip
38 const filePlaylistPath = join(directory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(file.resolution))
39 if (await pathExists(filePlaylistPath) === false) continue
40
41 const videoFilePath = video.getVideoFilePath(file)
42
43 const size = await getVideoFileSize(videoFilePath)
44
45 const bandwidth = 'BANDWIDTH=' + video.getBandwidthBits(file)
46 const resolution = `RESOLUTION=${size.width}x${size.height}`
47
48 let line = `#EXT-X-STREAM-INF:${bandwidth},${resolution}`
49 if (file.fps) line += ',FRAME-RATE=' + file.fps
50
51 masterPlaylists.push(line)
52 masterPlaylists.push(VideoStreamingPlaylistModel.getHlsPlaylistFilename(file.resolution))
53 }
54
55 await writeFile(masterPlaylistPath, masterPlaylists.join('\n') + '\n')
56}
57
58async function updateSha256Segments (video: VideoModel) {
59 const json: { [filename: string]: { [range: string]: string } } = {}
60
61 const playlistDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)
62
63 // For all the resolutions available for this video
64 for (const file of video.VideoFiles) {
65 const rangeHashes: { [range: string]: string } = {}
66
67 const videoPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsVideoName(video.uuid, file.resolution))
68 const playlistPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(file.resolution))
69
70 // Maybe the playlist is not generated for this resolution yet
71 if (!await pathExists(playlistPath)) continue
72
73 const playlistContent = await readFile(playlistPath)
74 const ranges = getRangesFromPlaylist(playlistContent.toString())
75
76 const fd = await open(videoPath, 'r')
77 for (const range of ranges) {
78 const buf = Buffer.alloc(range.length)
79 await read(fd, buf, 0, range.length, range.offset)
80
81 rangeHashes[`${range.offset}-${range.offset + range.length - 1}`] = sha256(buf)
82 }
83 await close(fd)
84
85 const videoFilename = VideoStreamingPlaylistModel.getHlsVideoName(video.uuid, file.resolution)
86 json[videoFilename] = rangeHashes
87 }
88
89 const outputPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsSha256SegmentsFilename())
90 await outputJSON(outputPath, json)
91}
92
93function getRangesFromPlaylist (playlistContent: string) {
94 const ranges: { offset: number, length: number }[] = []
95 const lines = playlistContent.split('\n')
96 const regex = /^#EXT-X-BYTERANGE:(\d+)@(\d+)$/
97
98 for (const line of lines) {
99 const captured = regex.exec(line)
100
101 if (captured) {
102 ranges.push({ length: parseInt(captured[1], 10), offset: parseInt(captured[2], 10) })
103 }
104 }
105
106 return ranges
107}
108
109function downloadPlaylistSegments (playlistUrl: string, destinationDir: string, timeout: number) {
110 let timer
111
112 logger.info('Importing HLS playlist %s', playlistUrl)
113
114 return new Promise<string>(async (res, rej) => {
115 const tmpDirectory = join(CONFIG.STORAGE.TMP_DIR, await generateRandomString(10))
116
117 await ensureDir(tmpDirectory)
118
119 timer = setTimeout(() => {
120 deleteTmpDirectory(tmpDirectory)
121
122 return rej(new Error('HLS download timeout.'))
123 }, timeout)
124
125 try {
126 // Fetch master playlist
127 const subPlaylistUrls = await fetchUniqUrls(playlistUrl)
128
129 const subRequests = subPlaylistUrls.map(u => fetchUniqUrls(u))
130 const fileUrls = uniq(flatten(await Promise.all(subRequests)))
131
132 logger.debug('Will download %d HLS files.', fileUrls.length, { fileUrls })
133
134 for (const fileUrl of fileUrls) {
135 const destPath = join(tmpDirectory, basename(fileUrl))
136
137 const bodyKBLimit = 10 * 1000 * 1000 // 10GB
138 await doRequestAndSaveToFile({ uri: fileUrl }, destPath, bodyKBLimit)
139 }
140
141 clearTimeout(timer)
142
143 await move(tmpDirectory, destinationDir, { overwrite: true })
144
145 return res()
146 } catch (err) {
147 deleteTmpDirectory(tmpDirectory)
148
149 return rej(err)
150 }
151 })
152
153 function deleteTmpDirectory (directory: string) {
154 remove(directory)
155 .catch(err => logger.error('Cannot delete path on HLS download error.', { err }))
156 }
157
158 async function fetchUniqUrls (playlistUrl: string) {
159 const { body } = await doRequest<string>({ uri: playlistUrl })
160
161 if (!body) return []
162
163 const urls = body.split('\n')
164 .filter(line => line.endsWith('.m3u8') || line.endsWith('.mp4'))
165 .map(url => {
166 if (url.startsWith('http://') || url.startsWith('https://')) return url
167
168 return `${dirname(playlistUrl)}/${url}`
169 })
170
171 return uniq(urls)
172 }
173}
174
175// ---------------------------------------------------------------------------
176
177export {
178 updateMasterHLSPlaylist,
179 updateSha256Segments,
180 downloadPlaylistSegments,
181 updateStreamingPlaylistsInfohashesIfNeeded
182}
183
184// ---------------------------------------------------------------------------
diff --git a/server/lib/job-queue/handlers/activitypub-follow.ts b/server/lib/job-queue/handlers/activitypub-follow.ts
index b4d381062..b3defb617 100644
--- a/server/lib/job-queue/handlers/activitypub-follow.ts
+++ b/server/lib/job-queue/handlers/activitypub-follow.ts
@@ -1,6 +1,6 @@
1import * as Bull from 'bull' 1import * as Bull from 'bull'
2import { logger } from '../../../helpers/logger' 2import { logger } from '../../../helpers/logger'
3import { CONFIG, REMOTE_SCHEME, sequelizeTypescript } from '../../../initializers' 3import { REMOTE_SCHEME, WEBSERVER } from '../../../initializers/constants'
4import { sendFollow } from '../../activitypub/send' 4import { sendFollow } from '../../activitypub/send'
5import { sanitizeHost } from '../../../helpers/core-utils' 5import { sanitizeHost } from '../../../helpers/core-utils'
6import { loadActorUrlOrGetFromWebfinger } from '../../../helpers/webfinger' 6import { loadActorUrlOrGetFromWebfinger } from '../../../helpers/webfinger'
@@ -9,6 +9,7 @@ import { 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' 11import { Notifier } from '../../notifier'
12import { sequelizeTypescript } from '../../../initializers/database'
12 13
13export type ActivitypubFollowPayload = { 14export type ActivitypubFollowPayload = {
14 followerActorId: number 15 followerActorId: number
@@ -23,7 +24,7 @@ async function processActivityPubFollow (job: Bull.Job) {
23 logger.info('Processing ActivityPub follow in job %d.', job.id) 24 logger.info('Processing ActivityPub follow in job %d.', job.id)
24 25
25 let targetActor: ActorModel 26 let targetActor: ActorModel
26 if (!host || host === CONFIG.WEBSERVER.HOST) { 27 if (!host || host === WEBSERVER.HOST) {
27 targetActor = await ActorModel.loadLocalByName(payload.name) 28 targetActor = await ActorModel.loadLocalByName(payload.name)
28 } else { 29 } else {
29 const sanitizedHost = sanitizeHost(host, REMOTE_SCHEME.HTTP) 30 const sanitizedHost = sanitizeHost(host, REMOTE_SCHEME.HTTP)
@@ -73,5 +74,5 @@ async function follow (fromActor: ActorModel, targetActor: ActorModel) {
73 return actorFollow 74 return actorFollow
74 }) 75 })
75 76
76 if (actorFollow.state === 'accepted') Notifier.Instance.notifyOfNewFollow(actorFollow) 77 if (actorFollow.state === 'accepted') Notifier.Instance.notifyOfNewUserFollow(actorFollow)
77} 78}
diff --git a/server/lib/job-queue/handlers/activitypub-http-broadcast.ts b/server/lib/job-queue/handlers/activitypub-http-broadcast.ts
index 9493945ff..0ff7b44a0 100644
--- a/server/lib/job-queue/handlers/activitypub-http-broadcast.ts
+++ b/server/lib/job-queue/handlers/activitypub-http-broadcast.ts
@@ -2,10 +2,9 @@ import * as Bull from 'bull'
2import * as Bluebird from 'bluebird' 2import * as Bluebird from 'bluebird'
3import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
4import { doRequest } from '../../../helpers/requests' 4import { doRequest } from '../../../helpers/requests'
5import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
6import { buildGlobalHeaders, buildSignedRequestOptions, computeBody } from './utils/activitypub-http-utils' 5import { buildGlobalHeaders, buildSignedRequestOptions, computeBody } from './utils/activitypub-http-utils'
7import { BROADCAST_CONCURRENCY, JOB_REQUEST_TIMEOUT } from '../../../initializers' 6import { BROADCAST_CONCURRENCY, JOB_REQUEST_TIMEOUT } from '../../../initializers/constants'
8import { ActorFollowScoreCache } from '../../cache' 7import { ActorFollowScoreCache } from '../../files-cache'
9 8
10export type ActivitypubHttpBroadcastPayload = { 9export type ActivitypubHttpBroadcastPayload = {
11 uris: string[] 10 uris: string[]
diff --git a/server/lib/job-queue/handlers/activitypub-http-fetcher.ts b/server/lib/job-queue/handlers/activitypub-http-fetcher.ts
index 67ccfa995..23d33c26f 100644
--- a/server/lib/job-queue/handlers/activitypub-http-fetcher.ts
+++ b/server/lib/job-queue/handlers/activitypub-http-fetcher.ts
@@ -1,17 +1,24 @@
1import * as Bull from 'bull' 1import * as Bull from 'bull'
2import * as Bluebird from 'bluebird'
2import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
3import { processActivities } from '../../activitypub/process' 4import { processActivities } from '../../activitypub/process'
4import { addVideoComments } from '../../activitypub/video-comments' 5import { addVideoComments } from '../../activitypub/video-comments'
5import { crawlCollectionPage } from '../../activitypub/crawl' 6import { crawlCollectionPage } from '../../activitypub/crawl'
6import { VideoModel } from '../../../models/video/video' 7import { VideoModel } from '../../../models/video/video'
7import { addVideoShares, createRates } from '../../activitypub' 8import { addVideoShares, createRates } from '../../activitypub'
9import { createAccountPlaylists } from '../../activitypub/playlist'
10import { AccountModel } from '../../../models/account/account'
11import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
12import { VideoShareModel } from '../../../models/video/video-share'
13import { VideoCommentModel } from '../../../models/video/video-comment'
8 14
9type FetchType = 'activity' | 'video-likes' | 'video-dislikes' | 'video-shares' | 'video-comments' 15type FetchType = 'activity' | 'video-likes' | 'video-dislikes' | 'video-shares' | 'video-comments' | 'account-playlists'
10 16
11export type ActivitypubHttpFetcherPayload = { 17export type ActivitypubHttpFetcherPayload = {
12 uri: string 18 uri: string
13 type: FetchType 19 type: FetchType
14 videoId?: number 20 videoId?: number
21 accountId?: number
15} 22}
16 23
17async function processActivityPubHttpFetcher (job: Bull.Job) { 24async function processActivityPubHttpFetcher (job: Bull.Job) {
@@ -22,15 +29,26 @@ async function processActivityPubHttpFetcher (job: Bull.Job) {
22 let video: VideoModel 29 let video: VideoModel
23 if (payload.videoId) video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoId) 30 if (payload.videoId) video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoId)
24 31
32 let account: AccountModel
33 if (payload.accountId) account = await AccountModel.load(payload.accountId)
34
25 const fetcherType: { [ id in FetchType ]: (items: any[]) => Promise<any> } = { 35 const fetcherType: { [ id in FetchType ]: (items: any[]) => Promise<any> } = {
26 'activity': items => processActivities(items, { outboxUrl: payload.uri }), 36 'activity': items => processActivities(items, { outboxUrl: payload.uri }),
27 'video-likes': items => createRates(items, video, 'like'), 37 'video-likes': items => createRates(items, video, 'like'),
28 'video-dislikes': items => createRates(items, video, 'dislike'), 38 'video-dislikes': items => createRates(items, video, 'dislike'),
29 'video-shares': items => addVideoShares(items, video), 39 'video-shares': items => addVideoShares(items, video),
30 'video-comments': items => addVideoComments(items, video) 40 'video-comments': items => addVideoComments(items, video),
41 'account-playlists': items => createAccountPlaylists(items, account)
42 }
43
44 const cleanerType: { [ id in FetchType ]?: (crawlStartDate: Date) => Bluebird<any> } = {
45 'video-likes': crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'like' as 'like', crawlStartDate),
46 'video-dislikes': crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'dislike' as 'dislike', crawlStartDate),
47 'video-shares': crawlStartDate => VideoShareModel.cleanOldSharesOf(video.id, crawlStartDate),
48 'video-comments': crawlStartDate => VideoCommentModel.cleanOldCommentsOf(video.id, crawlStartDate)
31 } 49 }
32 50
33 return crawlCollectionPage(payload.uri, fetcherType[payload.type]) 51 return crawlCollectionPage(payload.uri, fetcherType[payload.type], cleanerType[payload.type])
34} 52}
35 53
36// --------------------------------------------------------------------------- 54// ---------------------------------------------------------------------------
diff --git a/server/lib/job-queue/handlers/activitypub-http-unicast.ts b/server/lib/job-queue/handlers/activitypub-http-unicast.ts
index 3973dcdc8..c70ce3be9 100644
--- a/server/lib/job-queue/handlers/activitypub-http-unicast.ts
+++ b/server/lib/job-queue/handlers/activitypub-http-unicast.ts
@@ -2,8 +2,8 @@ import * 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 { buildGlobalHeaders, buildSignedRequestOptions, computeBody } from './utils/activitypub-http-utils' 4import { buildGlobalHeaders, buildSignedRequestOptions, computeBody } from './utils/activitypub-http-utils'
5import { JOB_REQUEST_TIMEOUT } from '../../../initializers' 5import { JOB_REQUEST_TIMEOUT } from '../../../initializers/constants'
6import { ActorFollowScoreCache } from '../../cache' 6import { ActorFollowScoreCache } from '../../files-cache'
7 7
8export type ActivitypubHttpUnicastPayload = { 8export type ActivitypubHttpUnicastPayload = {
9 uri: string 9 uri: string
diff --git a/server/lib/job-queue/handlers/activitypub-refresher.ts b/server/lib/job-queue/handlers/activitypub-refresher.ts
index 454b975fe..4d6c38cfa 100644
--- a/server/lib/job-queue/handlers/activitypub-refresher.ts
+++ b/server/lib/job-queue/handlers/activitypub-refresher.ts
@@ -1,11 +1,12 @@
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, refreshActorIfNeeded } from '../../activitypub' 4import { refreshActorIfNeeded, refreshVideoIfNeeded, refreshVideoPlaylistIfNeeded } from '../../activitypub'
5import { ActorModel } from '../../../models/activitypub/actor' 5import { ActorModel } from '../../../models/activitypub/actor'
6import { VideoPlaylistModel } from '../../../models/video/video-playlist'
6 7
7export type RefreshPayload = { 8export type RefreshPayload = {
8 type: 'video' | 'actor' 9 type: 'video' | 'video-playlist' | 'actor'
9 url: string 10 url: string
10} 11}
11 12
@@ -15,13 +16,13 @@ async function refreshAPObject (job: Bull.Job) {
15 logger.info('Processing AP refresher in job %d for %s.', job.id, payload.url) 16 logger.info('Processing AP refresher in job %d for %s.', job.id, payload.url)
16 17
17 if (payload.type === 'video') return refreshVideo(payload.url) 18 if (payload.type === 'video') return refreshVideo(payload.url)
19 if (payload.type === 'video-playlist') return refreshVideoPlaylist(payload.url)
18 if (payload.type === 'actor') return refreshActor(payload.url) 20 if (payload.type === 'actor') return refreshActor(payload.url)
19} 21}
20 22
21// --------------------------------------------------------------------------- 23// ---------------------------------------------------------------------------
22 24
23export { 25export {
24 refreshActor,
25 refreshAPObject 26 refreshAPObject
26} 27}
27 28
@@ -50,5 +51,12 @@ async function refreshActor (actorUrl: string) {
50 if (actor) { 51 if (actor) {
51 await refreshActorIfNeeded(actor, fetchType) 52 await refreshActorIfNeeded(actor, fetchType)
52 } 53 }
54}
55
56async function refreshVideoPlaylist (playlistUrl: string) {
57 const playlist = await VideoPlaylistModel.loadByUrlAndPopulateAccount(playlistUrl)
53 58
59 if (playlist) {
60 await refreshVideoPlaylistIfNeeded(playlist)
61 }
54} 62}
diff --git a/server/lib/job-queue/handlers/email.ts b/server/lib/job-queue/handlers/email.ts
index 2ba39a156..62701222c 100644
--- a/server/lib/job-queue/handlers/email.ts
+++ b/server/lib/job-queue/handlers/email.ts
@@ -1,15 +1,8 @@
1import * as Bull from 'bull' 1import * as Bull from 'bull'
2import { logger } from '../../../helpers/logger' 2import { logger } from '../../../helpers/logger'
3import { Emailer } from '../../emailer' 3import { Emailer, SendEmailOptions } from '../../emailer'
4 4
5export type EmailPayload = { 5export type EmailPayload = SendEmailOptions
6 to: string[]
7 subject: string
8 text: string
9
10 fromDisplayName?: string
11 replyTo?: string
12}
13 6
14async function processEmail (job: Bull.Job) { 7async function processEmail (job: Bull.Job) {
15 const payload = job.data as EmailPayload 8 const payload = job.data as EmailPayload
diff --git a/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts b/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts
index 4961d4502..cdee1f6fd 100644
--- a/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts
+++ b/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts
@@ -2,7 +2,7 @@ import { buildSignedActivity } from '../../../../helpers/activitypub'
2import { getServerActor } from '../../../../helpers/utils' 2import { getServerActor } from '../../../../helpers/utils'
3import { ActorModel } from '../../../../models/activitypub/actor' 3import { ActorModel } from '../../../../models/activitypub/actor'
4import { sha256 } from '../../../../helpers/core-utils' 4import { sha256 } from '../../../../helpers/core-utils'
5import { HTTP_SIGNATURE } from '../../../../initializers' 5import { HTTP_SIGNATURE } from '../../../../initializers/constants'
6 6
7type Payload = { body: any, signatureActorId?: number } 7type Payload = { body: any, signatureActorId?: number }
8 8
@@ -28,7 +28,7 @@ async function buildSignedRequestOptions (payload: Payload) {
28 actor = await getServerActor() 28 actor = await getServerActor()
29 } 29 }
30 30
31 const keyId = actor.getWebfingerUrl() 31 const keyId = actor.url
32 return { 32 return {
33 algorithm: HTTP_SIGNATURE.ALGORITHM, 33 algorithm: HTTP_SIGNATURE.ALGORITHM,
34 authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME, 34 authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME,
diff --git a/server/lib/job-queue/handlers/video-file-import.ts b/server/lib/job-queue/handlers/video-file-import.ts
new file mode 100644
index 000000000..921d9a083
--- /dev/null
+++ b/server/lib/job-queue/handlers/video-file-import.ts
@@ -0,0 +1,78 @@
1import * as Bull from 'bull'
2import { logger } from '../../../helpers/logger'
3import { VideoModel } from '../../../models/video/video'
4import { publishVideoIfNeeded } from './video-transcoding'
5import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
6import { copy, stat } from 'fs-extra'
7import { VideoFileModel } from '../../../models/video/video-file'
8import { extname } from 'path'
9
10export type VideoFileImportPayload = {
11 videoUUID: string,
12 filePath: string
13}
14
15async function processVideoFileImport (job: Bull.Job) {
16 const payload = job.data as VideoFileImportPayload
17 logger.info('Processing video file import in job %d.', job.id)
18
19 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoUUID)
20 // No video, maybe deleted?
21 if (!video) {
22 logger.info('Do not process job %d, video does not exist.', job.id)
23 return undefined
24 }
25
26 await updateVideoFile(video, payload.filePath)
27
28 await publishVideoIfNeeded(video)
29 return video
30}
31
32// ---------------------------------------------------------------------------
33
34export {
35 processVideoFileImport
36}
37
38// ---------------------------------------------------------------------------
39
40async function updateVideoFile (video: VideoModel, inputFilePath: string) {
41 const { videoFileResolution } = await getVideoFileResolution(inputFilePath)
42 const { size } = await stat(inputFilePath)
43 const fps = await getVideoFileFPS(inputFilePath)
44
45 let updatedVideoFile = new VideoFileModel({
46 resolution: videoFileResolution,
47 extname: extname(inputFilePath),
48 size,
49 fps,
50 videoId: video.id
51 })
52
53 const currentVideoFile = video.VideoFiles.find(videoFile => videoFile.resolution === updatedVideoFile.resolution)
54
55 if (currentVideoFile) {
56 // Remove old file and old torrent
57 await video.removeFile(currentVideoFile)
58 await video.removeTorrent(currentVideoFile)
59 // Remove the old video file from the array
60 video.VideoFiles = video.VideoFiles.filter(f => f !== currentVideoFile)
61
62 // Update the database
63 currentVideoFile.set('extname', updatedVideoFile.extname)
64 currentVideoFile.set('size', updatedVideoFile.size)
65 currentVideoFile.set('fps', updatedVideoFile.fps)
66
67 updatedVideoFile = currentVideoFile
68 }
69
70 const outputPath = video.getVideoFilePath(updatedVideoFile)
71 await copy(inputFilePath, outputPath)
72
73 await video.createTorrentAndSetInfoHash(updatedVideoFile)
74
75 await updatedVideoFile.save()
76
77 video.VideoFiles.push(updatedVideoFile)
78}
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts
index 12004dcd7..1650916a6 100644
--- a/server/lib/job-queue/handlers/video-import.ts
+++ b/server/lib/job-queue/handlers/video-import.ts
@@ -6,16 +6,20 @@ import { VideoImportState } from '../../../../shared/models/videos'
6import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' 6import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
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 { VIDEO_IMPORT_TIMEOUT } from '../../../initializers/constants'
10import { downloadImage } from '../../../helpers/requests'
11import { VideoState } from '../../../../shared' 10import { VideoState } from '../../../../shared'
12import { JobQueue } from '../index' 11import { JobQueue } from '../index'
13import { federateVideoIfNeeded } from '../../activitypub' 12import { federateVideoIfNeeded } from '../../activitypub'
14import { VideoModel } from '../../../models/video/video' 13import { VideoModel } from '../../../models/video/video'
15import { downloadWebTorrentVideo } from '../../../helpers/webtorrent' 14import { downloadWebTorrentVideo } from '../../../helpers/webtorrent'
16import { getSecureTorrentName } from '../../../helpers/utils' 15import { getSecureTorrentName } from '../../../helpers/utils'
17import { remove, move, stat } from 'fs-extra' 16import { move, remove, stat } from 'fs-extra'
18import { Notifier } from '../../notifier' 17import { Notifier } from '../../notifier'
18import { CONFIG } from '../../../initializers/config'
19import { sequelizeTypescript } from '../../../initializers/database'
20import { ThumbnailModel } from '../../../models/video/thumbnail'
21import { createVideoMiniatureFromUrl, generateVideoMiniature } from '../../thumbnail'
22import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
19 23
20type VideoImportYoutubeDLPayload = { 24type VideoImportYoutubeDLPayload = {
21 type: 'youtube-dl' 25 type: 'youtube-dl'
@@ -144,25 +148,19 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
144 tempVideoPath = null // This path is not used anymore 148 tempVideoPath = null // This path is not used anymore
145 149
146 // Process thumbnail 150 // Process thumbnail
147 if (options.downloadThumbnail) { 151 let thumbnailModel: ThumbnailModel
148 if (options.thumbnailUrl) { 152 if (options.downloadThumbnail && options.thumbnailUrl) {
149 await downloadImage(options.thumbnailUrl, CONFIG.STORAGE.THUMBNAILS_DIR, videoImport.Video.getThumbnailName(), THUMBNAILS_SIZE) 153 thumbnailModel = await createVideoMiniatureFromUrl(options.thumbnailUrl, videoImport.Video, ThumbnailType.MINIATURE)
150 } else { 154 } else if (options.generateThumbnail || options.downloadThumbnail) {
151 await videoImport.Video.createThumbnail(videoFile) 155 thumbnailModel = await generateVideoMiniature(videoImport.Video, videoFile, ThumbnailType.MINIATURE)
152 }
153 } else if (options.generateThumbnail) {
154 await videoImport.Video.createThumbnail(videoFile)
155 } 156 }
156 157
157 // Process preview 158 // Process preview
158 if (options.downloadPreview) { 159 let previewModel: ThumbnailModel
159 if (options.thumbnailUrl) { 160 if (options.downloadPreview && options.thumbnailUrl) {
160 await downloadImage(options.thumbnailUrl, CONFIG.STORAGE.PREVIEWS_DIR, videoImport.Video.getPreviewName(), PREVIEWS_SIZE) 161 previewModel = await createVideoMiniatureFromUrl(options.thumbnailUrl, videoImport.Video, ThumbnailType.PREVIEW)
161 } else { 162 } else if (options.generatePreview || options.downloadPreview) {
162 await videoImport.Video.createPreview(videoFile) 163 previewModel = await generateVideoMiniature(videoImport.Video, videoFile, ThumbnailType.PREVIEW)
163 }
164 } else if (options.generatePreview) {
165 await videoImport.Video.createPreview(videoFile)
166 } 164 }
167 165
168 // Create torrent 166 // Create torrent
@@ -182,6 +180,9 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
182 video.state = CONFIG.TRANSCODING.ENABLED ? VideoState.TO_TRANSCODE : VideoState.PUBLISHED 180 video.state = CONFIG.TRANSCODING.ENABLED ? VideoState.TO_TRANSCODE : VideoState.PUBLISHED
183 await video.save({ transaction: t }) 181 await video.save({ transaction: t })
184 182
183 if (thumbnailModel) await video.addAndSaveThumbnail(thumbnailModel, t)
184 if (previewModel) await video.addAndSaveThumbnail(previewModel, t)
185
185 // Now we can federate the video (reload from database, we need more attributes) 186 // Now we can federate the video (reload from database, we need more attributes)
186 const videoForFederation = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t) 187 const videoForFederation = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t)
187 await federateVideoIfNeeded(videoForFederation, true, t) 188 await federateVideoIfNeeded(videoForFederation, true, t)
@@ -196,9 +197,14 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
196 return videoImportUpdated 197 return videoImportUpdated
197 }) 198 })
198 199
199 Notifier.Instance.notifyOnNewVideo(videoImportUpdated.Video)
200 Notifier.Instance.notifyOnFinishedVideoImport(videoImportUpdated, true) 200 Notifier.Instance.notifyOnFinishedVideoImport(videoImportUpdated, true)
201 201
202 if (videoImportUpdated.Video.VideoBlacklist) {
203 Notifier.Instance.notifyOnVideoAutoBlacklist(videoImportUpdated.Video)
204 } else {
205 Notifier.Instance.notifyOnNewVideo(videoImportUpdated.Video)
206 }
207
202 // Create transcoding jobs? 208 // Create transcoding jobs?
203 if (videoImportUpdated.Video.state === VideoState.TO_TRANSCODE) { 209 if (videoImportUpdated.Video.state === VideoState.TO_TRANSCODE) {
204 // Put uuid because we don't have id auto incremented for now 210 // Put uuid because we don't have id auto incremented for now
@@ -207,7 +213,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
207 isNewVideo: true 213 isNewVideo: true
208 } 214 }
209 215
210 await JobQueue.Instance.createJob({ type: 'video-file', payload: dataInput }) 216 await JobQueue.Instance.createJob({ type: 'video-transcoding', payload: dataInput })
211 } 217 }
212 218
213 } catch (err) { 219 } catch (err) {
diff --git a/server/lib/job-queue/handlers/video-file.ts b/server/lib/job-queue/handlers/video-transcoding.ts
index 593e43cc5..48cac517e 100644
--- a/server/lib/job-queue/handlers/video-file.ts
+++ b/server/lib/job-queue/handlers/video-transcoding.ts
@@ -8,40 +8,20 @@ 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, optimizeVideofile, transcodeOriginalVideofile } from '../../video-transcoding' 11import { generateHlsPlaylist, optimizeVideofile, transcodeOriginalVideofile } from '../../video-transcoding'
12import { Notifier } from '../../notifier' 12import { Notifier } from '../../notifier'
13import { CONFIG } from '../../../initializers/config'
13 14
14export type VideoFilePayload = { 15export type VideoTranscodingPayload = {
15 videoUUID: string 16 videoUUID: string
16 isNewVideo?: boolean
17 resolution?: VideoResolution 17 resolution?: VideoResolution
18 isNewVideo?: boolean
18 isPortraitMode?: boolean 19 isPortraitMode?: boolean
20 generateHlsPlaylist?: boolean
19} 21}
20 22
21export type VideoFileImportPayload = { 23async function processVideoTranscoding (job: Bull.Job) {
22 videoUUID: string, 24 const payload = job.data as VideoTranscodingPayload
23 filePath: string
24}
25
26async function processVideoFileImport (job: Bull.Job) {
27 const payload = job.data as VideoFileImportPayload
28 logger.info('Processing video file import in job %d.', job.id)
29
30 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoUUID)
31 // No video, maybe deleted?
32 if (!video) {
33 logger.info('Do not process job %d, video does not exist.', job.id)
34 return undefined
35 }
36
37 await importVideoFile(video, payload.filePath)
38
39 await onVideoFileTranscoderOrImportSuccess(video)
40 return video
41}
42
43async function processVideoFile (job: Bull.Job) {
44 const payload = job.data as VideoFilePayload
45 logger.info('Processing video file in job %d.', job.id) 25 logger.info('Processing video file in job %d.', job.id)
46 26
47 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoUUID) 27 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoUUID)
@@ -51,23 +31,38 @@ async function processVideoFile (job: Bull.Job) {
51 return undefined 31 return undefined
52 } 32 }
53 33
54 // Transcoding in other resolution 34 if (payload.generateHlsPlaylist) {
55 if (payload.resolution) { 35 await generateHlsPlaylist(video, payload.resolution, payload.isPortraitMode || false)
36
37 await retryTransactionWrapper(onHlsPlaylistGenerationSuccess, video)
38 } else if (payload.resolution) { // Transcoding in other resolution
56 await transcodeOriginalVideofile(video, payload.resolution, payload.isPortraitMode || false) 39 await transcodeOriginalVideofile(video, payload.resolution, payload.isPortraitMode || false)
57 40
58 await retryTransactionWrapper(onVideoFileTranscoderOrImportSuccess, video) 41 await retryTransactionWrapper(publishVideoIfNeeded, video, payload)
59 } else { 42 } else {
60 await optimizeVideofile(video) 43 await optimizeVideofile(video)
61 44
62 await retryTransactionWrapper(onVideoFileOptimizerSuccess, video, payload.isNewVideo) 45 await retryTransactionWrapper(onVideoFileOptimizerSuccess, video, payload)
63 } 46 }
64 47
65 return video 48 return video
66} 49}
67 50
68async function onVideoFileTranscoderOrImportSuccess (video: VideoModel) { 51async function onHlsPlaylistGenerationSuccess (video: VideoModel) {
69 if (video === undefined) return undefined 52 if (video === undefined) return undefined
70 53
54 await sequelizeTypescript.transaction(async t => {
55 // Maybe the video changed in database, refresh it
56 let videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t)
57 // Video does not exist anymore
58 if (!videoDatabase) return undefined
59
60 // If the video was not published, we consider it is a new one for other instances
61 await federateVideoIfNeeded(videoDatabase, false, t)
62 })
63}
64
65async function publishVideoIfNeeded (video: VideoModel, payload?: VideoTranscodingPayload) {
71 const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => { 66 const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => {
72 // Maybe the video changed in database, refresh it 67 // Maybe the video changed in database, refresh it
73 let videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t) 68 let videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t)
@@ -93,11 +88,13 @@ async function onVideoFileTranscoderOrImportSuccess (video: VideoModel) {
93 88
94 if (videoPublished) { 89 if (videoPublished) {
95 Notifier.Instance.notifyOnNewVideo(videoDatabase) 90 Notifier.Instance.notifyOnNewVideo(videoDatabase)
96 Notifier.Instance.notifyOnPendingVideoPublished(videoDatabase) 91 Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(videoDatabase)
97 } 92 }
93
94 await createHlsJobIfEnabled(payload)
98} 95}
99 96
100async function onVideoFileOptimizerSuccess (videoArg: VideoModel, isNewVideo: boolean) { 97async function onVideoFileOptimizerSuccess (videoArg: VideoModel, payload: VideoTranscodingPayload) {
101 if (videoArg === undefined) return undefined 98 if (videoArg === undefined) return undefined
102 99
103 // Outside the transaction (IO on disk) 100 // Outside the transaction (IO on disk)
@@ -119,7 +116,7 @@ async function onVideoFileOptimizerSuccess (videoArg: VideoModel, isNewVideo: bo
119 let videoPublished = false 116 let videoPublished = false
120 117
121 if (resolutionsEnabled.length !== 0) { 118 if (resolutionsEnabled.length !== 0) {
122 const tasks: Bluebird<Bull.Job<any>>[] = [] 119 const tasks: (Bluebird<Bull.Job<any>> | Promise<Bull.Job<any>>)[] = []
123 120
124 for (const resolution of resolutionsEnabled) { 121 for (const resolution of resolutionsEnabled) {
125 const dataInput = { 122 const dataInput = {
@@ -127,7 +124,7 @@ async function onVideoFileOptimizerSuccess (videoArg: VideoModel, isNewVideo: bo
127 resolution 124 resolution
128 } 125 }
129 126
130 const p = JobQueue.Instance.createJob({ type: 'video-file', payload: dataInput }) 127 const p = JobQueue.Instance.createJob({ type: 'video-transcoding', payload: dataInput })
131 tasks.push(p) 128 tasks.push(p)
132 } 129 }
133 130
@@ -144,18 +141,37 @@ async function onVideoFileOptimizerSuccess (videoArg: VideoModel, isNewVideo: bo
144 logger.info('No transcoding jobs created for video %s (no resolutions).', videoDatabase.uuid, { privacy: videoDatabase.privacy }) 141 logger.info('No transcoding jobs created for video %s (no resolutions).', videoDatabase.uuid, { privacy: videoDatabase.privacy })
145 } 142 }
146 143
147 await federateVideoIfNeeded(videoDatabase, isNewVideo, t) 144 await federateVideoIfNeeded(videoDatabase, payload.isNewVideo, t)
148 145
149 return { videoDatabase, videoPublished } 146 return { videoDatabase, videoPublished }
150 }) 147 })
151 148
152 if (isNewVideo) Notifier.Instance.notifyOnNewVideo(videoDatabase) 149 if (payload.isNewVideo) Notifier.Instance.notifyOnNewVideo(videoDatabase)
153 if (videoPublished) Notifier.Instance.notifyOnPendingVideoPublished(videoDatabase) 150 if (videoPublished) Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(videoDatabase)
151
152 await createHlsJobIfEnabled(Object.assign({}, payload, { resolution: videoDatabase.getOriginalFile().resolution }))
154} 153}
155 154
156// --------------------------------------------------------------------------- 155// ---------------------------------------------------------------------------
157 156
158export { 157export {
159 processVideoFile, 158 processVideoTranscoding,
160 processVideoFileImport 159 publishVideoIfNeeded
160}
161
162// ---------------------------------------------------------------------------
163
164function createHlsJobIfEnabled (payload?: VideoTranscodingPayload) {
165 // Generate HLS playlist?
166 if (payload && CONFIG.TRANSCODING.HLS.ENABLED) {
167 const hlsTranscodingPayload = {
168 videoUUID: payload.videoUUID,
169 resolution: payload.resolution,
170 isPortraitMode: payload.isPortraitMode,
171
172 generateHlsPlaylist: true
173 }
174
175 return JobQueue.Instance.createJob({ type: 'video-transcoding', payload: hlsTranscodingPayload })
176 }
161} 177}
diff --git a/server/lib/job-queue/job-queue.ts b/server/lib/job-queue/job-queue.ts
index ba9cbe0d9..3c810da98 100644
--- a/server/lib/job-queue/job-queue.ts
+++ b/server/lib/job-queue/job-queue.ts
@@ -2,16 +2,17 @@ import * as Bull from 'bull'
2import { JobState, JobType } from '../../../shared/models' 2import { JobState, JobType } from '../../../shared/models'
3import { logger } from '../../helpers/logger' 3import { logger } from '../../helpers/logger'
4import { Redis } from '../redis' 4import { Redis } from '../redis'
5import { CONFIG, JOB_ATTEMPTS, JOB_COMPLETED_LIFETIME, JOB_CONCURRENCY, JOB_TTL, REPEAT_JOBS } from '../../initializers' 5import { JOB_ATTEMPTS, JOB_COMPLETED_LIFETIME, JOB_CONCURRENCY, JOB_TTL, REPEAT_JOBS, WEBSERVER } from '../../initializers/constants'
6import { ActivitypubHttpBroadcastPayload, processActivityPubHttpBroadcast } from './handlers/activitypub-http-broadcast' 6import { ActivitypubHttpBroadcastPayload, processActivityPubHttpBroadcast } from './handlers/activitypub-http-broadcast'
7import { ActivitypubHttpFetcherPayload, processActivityPubHttpFetcher } from './handlers/activitypub-http-fetcher' 7import { ActivitypubHttpFetcherPayload, processActivityPubHttpFetcher } from './handlers/activitypub-http-fetcher'
8import { ActivitypubHttpUnicastPayload, processActivityPubHttpUnicast } from './handlers/activitypub-http-unicast' 8import { ActivitypubHttpUnicastPayload, processActivityPubHttpUnicast } from './handlers/activitypub-http-unicast'
9import { EmailPayload, processEmail } from './handlers/email' 9import { EmailPayload, processEmail } from './handlers/email'
10import { processVideoFile, processVideoFileImport, VideoFileImportPayload, VideoFilePayload } from './handlers/video-file' 10import { processVideoTranscoding, VideoTranscodingPayload } from './handlers/video-transcoding'
11import { ActivitypubFollowPayload, processActivityPubFollow } from './handlers/activitypub-follow' 11import { ActivitypubFollowPayload, processActivityPubFollow } from './handlers/activitypub-follow'
12import { processVideoImport, VideoImportPayload } from './handlers/video-import' 12import { processVideoImport, VideoImportPayload } from './handlers/video-import'
13import { processVideosViews } from './handlers/video-views' 13import { processVideosViews } from './handlers/video-views'
14import { refreshAPObject, RefreshPayload } from './handlers/activitypub-refresher' 14import { refreshAPObject, RefreshPayload } from './handlers/activitypub-refresher'
15import { processVideoFileImport, VideoFileImportPayload } from './handlers/video-file-import'
15 16
16type CreateJobArgument = 17type CreateJobArgument =
17 { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } | 18 { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } |
@@ -19,19 +20,20 @@ type CreateJobArgument =
19 { type: 'activitypub-http-fetcher', payload: ActivitypubHttpFetcherPayload } | 20 { type: 'activitypub-http-fetcher', payload: ActivitypubHttpFetcherPayload } |
20 { type: 'activitypub-follow', payload: ActivitypubFollowPayload } | 21 { type: 'activitypub-follow', payload: ActivitypubFollowPayload } |
21 { type: 'video-file-import', payload: VideoFileImportPayload } | 22 { type: 'video-file-import', payload: VideoFileImportPayload } |
22 { type: 'video-file', payload: VideoFilePayload } | 23 { type: 'video-transcoding', payload: VideoTranscodingPayload } |
23 { type: 'email', payload: EmailPayload } | 24 { type: 'email', payload: EmailPayload } |
24 { type: 'video-import', payload: VideoImportPayload } | 25 { type: 'video-import', payload: VideoImportPayload } |
25 { type: 'activitypub-refresher', payload: RefreshPayload } | 26 { type: 'activitypub-refresher', payload: RefreshPayload } |
26 { type: 'videos-views', payload: {} } 27 { type: 'videos-views', payload: {} }
27 28
28const handlers: { [ id in JobType ]: (job: Bull.Job) => Promise<any>} = { 29const handlers: { [ id in (JobType | 'video-file') ]: (job: Bull.Job) => Promise<any>} = {
29 'activitypub-http-broadcast': processActivityPubHttpBroadcast, 30 'activitypub-http-broadcast': processActivityPubHttpBroadcast,
30 'activitypub-http-unicast': processActivityPubHttpUnicast, 31 'activitypub-http-unicast': processActivityPubHttpUnicast,
31 'activitypub-http-fetcher': processActivityPubHttpFetcher, 32 'activitypub-http-fetcher': processActivityPubHttpFetcher,
32 'activitypub-follow': processActivityPubFollow, 33 'activitypub-follow': processActivityPubFollow,
33 'video-file-import': processVideoFileImport, 34 'video-file-import': processVideoFileImport,
34 'video-file': processVideoFile, 35 'video-transcoding': processVideoTranscoding,
36 'video-file': processVideoTranscoding, // TODO: remove it (changed in 1.3)
35 'email': processEmail, 37 'email': processEmail,
36 'video-import': processVideoImport, 38 'video-import': processVideoImport,
37 'videos-views': processVideosViews, 39 'videos-views': processVideosViews,
@@ -44,7 +46,7 @@ const jobTypes: JobType[] = [
44 'activitypub-http-fetcher', 46 'activitypub-http-fetcher',
45 'activitypub-http-unicast', 47 'activitypub-http-unicast',
46 'email', 48 'email',
47 'video-file', 49 'video-transcoding',
48 'video-file-import', 50 'video-file-import',
49 'video-import', 51 'video-import',
50 'videos-views', 52 'videos-views',
@@ -66,10 +68,10 @@ class JobQueue {
66 if (this.initialized === true) return 68 if (this.initialized === true) return
67 this.initialized = true 69 this.initialized = true
68 70
69 this.jobRedisPrefix = 'bull-' + CONFIG.WEBSERVER.HOST 71 this.jobRedisPrefix = 'bull-' + WEBSERVER.HOST
70 const queueOptions = { 72 const queueOptions = {
71 prefix: this.jobRedisPrefix, 73 prefix: this.jobRedisPrefix,
72 redis: Redis.getRedisClient(), 74 redis: Redis.getRedisClientOptions(),
73 settings: { 75 settings: {
74 maxStalledCount: 10 // transcoding could be long, so jobs can often be interrupted by restarts 76 maxStalledCount: 10 // transcoding could be long, so jobs can often be interrupted by restarts
75 } 77 }
diff --git a/server/lib/notifier.ts b/server/lib/notifier.ts
index 2fa320cd7..c1e63fa8f 100644
--- a/server/lib/notifier.ts
+++ b/server/lib/notifier.ts
@@ -6,7 +6,7 @@ import { UserNotificationModel } from '../models/account/user-notification'
6import { VideoCommentModel } from '../models/video/video-comment' 6import { VideoCommentModel } from '../models/video/video-comment'
7import { UserModel } from '../models/account/user' 7import { UserModel } from '../models/account/user'
8import { PeerTubeSocket } from './peertube-socket' 8import { PeerTubeSocket } from './peertube-socket'
9import { CONFIG } from '../initializers/constants' 9import { CONFIG } from '../initializers/config'
10import { VideoPrivacy, VideoState } from '../../shared/models/videos' 10import { VideoPrivacy, VideoState } from '../../shared/models/videos'
11import { VideoAbuseModel } from '../models/video/video-abuse' 11import { VideoAbuseModel } from '../models/video/video-abuse'
12import { VideoBlacklistModel } from '../models/video/video-blacklist' 12import { VideoBlacklistModel } from '../models/video/video-blacklist'
@@ -23,19 +23,35 @@ class Notifier {
23 private constructor () {} 23 private constructor () {}
24 24
25 notifyOnNewVideo (video: VideoModel): void { 25 notifyOnNewVideo (video: VideoModel): void {
26 // Only notify on public and published videos 26 // Only notify on public and published videos which are not blacklisted
27 if (video.privacy !== VideoPrivacy.PUBLIC || video.state !== VideoState.PUBLISHED) return 27 if (video.privacy !== VideoPrivacy.PUBLIC || video.state !== VideoState.PUBLISHED || video.VideoBlacklist) return
28 28
29 this.notifySubscribersOfNewVideo(video) 29 this.notifySubscribersOfNewVideo(video)
30 .catch(err => logger.error('Cannot notify subscribers of new video %s.', video.url, { err })) 30 .catch(err => logger.error('Cannot notify subscribers of new video %s.', video.url, { err }))
31 } 31 }
32 32
33 notifyOnPendingVideoPublished (video: VideoModel): void { 33 notifyOnVideoPublishedAfterTranscoding (video: VideoModel): void {
34 // Only notify on public videos that has been published while the user waited transcoding/scheduled update 34 // don't notify if didn't wait for transcoding or video is still blacklisted/waiting for scheduled update
35 if (video.waitTranscoding === false && !video.ScheduleVideoUpdate) return 35 if (!video.waitTranscoding || video.VideoBlacklist || video.ScheduleVideoUpdate) return
36 36
37 this.notifyOwnedVideoHasBeenPublished(video) 37 this.notifyOwnedVideoHasBeenPublished(video)
38 .catch(err => logger.error('Cannot notify owner that its video %s has been published.', video.url, { err })) 38 .catch(err => logger.error('Cannot notify owner that its video %s has been published after transcoding.', video.url, { err }))
39 }
40
41 notifyOnVideoPublishedAfterScheduledUpdate (video: VideoModel): void {
42 // don't notify if video is still blacklisted or waiting for transcoding
43 if (video.VideoBlacklist || (video.waitTranscoding && video.state !== VideoState.PUBLISHED)) return
44
45 this.notifyOwnedVideoHasBeenPublished(video)
46 .catch(err => logger.error('Cannot notify owner that its video %s has been published after scheduled update.', video.url, { err }))
47 }
48
49 notifyOnVideoPublishedAfterRemovedFromAutoBlacklist (video: VideoModel): void {
50 // don't notify if video is still waiting for transcoding or scheduled update
51 if (video.ScheduleVideoUpdate || (video.waitTranscoding && video.state !== VideoState.PUBLISHED)) return
52
53 this.notifyOwnedVideoHasBeenPublished(video)
54 .catch(err => logger.error('Cannot notify owner that its video %s has been published after removed from auto-blacklist.', video.url, { err })) // tslint:disable-line:max-line-length
39 } 55 }
40 56
41 notifyOnNewComment (comment: VideoCommentModel): void { 57 notifyOnNewComment (comment: VideoCommentModel): void {
@@ -51,6 +67,11 @@ class Notifier {
51 .catch(err => logger.error('Cannot notify of new video abuse of video %s.', videoAbuse.Video.url, { err })) 67 .catch(err => logger.error('Cannot notify of new video abuse of video %s.', videoAbuse.Video.url, { err }))
52 } 68 }
53 69
70 notifyOnVideoAutoBlacklist (video: VideoModel): void {
71 this.notifyModeratorsOfVideoAutoBlacklist(video)
72 .catch(err => logger.error('Cannot notify of auto-blacklist of video %s.', video.url, { err }))
73 }
74
54 notifyOnVideoBlacklist (videoBlacklist: VideoBlacklistModel): void { 75 notifyOnVideoBlacklist (videoBlacklist: VideoBlacklistModel): void {
55 this.notifyVideoOwnerOfBlacklist(videoBlacklist) 76 this.notifyVideoOwnerOfBlacklist(videoBlacklist)
56 .catch(err => logger.error('Cannot notify video owner of new video blacklist of %s.', videoBlacklist.Video.url, { err })) 77 .catch(err => logger.error('Cannot notify video owner of new video blacklist of %s.', videoBlacklist.Video.url, { err }))
@@ -58,7 +79,7 @@ class Notifier {
58 79
59 notifyOnVideoUnblacklist (video: VideoModel): void { 80 notifyOnVideoUnblacklist (video: VideoModel): void {
60 this.notifyVideoOwnerOfUnblacklist(video) 81 this.notifyVideoOwnerOfUnblacklist(video)
61 .catch(err => logger.error('Cannot notify video owner of new video blacklist of %s.', video.url, { err })) 82 .catch(err => logger.error('Cannot notify video owner of unblacklist of %s.', video.url, { err }))
62 } 83 }
63 84
64 notifyOnFinishedVideoImport (videoImport: VideoImportModel, success: boolean): void { 85 notifyOnFinishedVideoImport (videoImport: VideoImportModel, success: boolean): void {
@@ -71,18 +92,25 @@ class Notifier {
71 .catch(err => logger.error('Cannot notify moderators of new user registration (%s).', user.username, { err })) 92 .catch(err => logger.error('Cannot notify moderators of new user registration (%s).', user.username, { err }))
72 } 93 }
73 94
74 notifyOfNewFollow (actorFollow: ActorFollowModel): void { 95 notifyOfNewUserFollow (actorFollow: ActorFollowModel): void {
75 this.notifyUserOfNewActorFollow(actorFollow) 96 this.notifyUserOfNewActorFollow(actorFollow)
76 .catch(err => { 97 .catch(err => {
77 logger.error( 98 logger.error(
78 'Cannot notify owner of channel %s of a new follow by %s.', 99 'Cannot notify owner of channel %s of a new follow by %s.',
79 actorFollow.ActorFollowing.VideoChannel.getDisplayName(), 100 actorFollow.ActorFollowing.VideoChannel.getDisplayName(),
80 actorFollow.ActorFollower.Account.getDisplayName(), 101 actorFollow.ActorFollower.Account.getDisplayName(),
81 err 102 { err }
82 ) 103 )
83 }) 104 })
84 } 105 }
85 106
107 notifyOfNewInstanceFollow (actorFollow: ActorFollowModel): void {
108 this.notifyAdminsOfNewInstanceFollow(actorFollow)
109 .catch(err => {
110 logger.error('Cannot notify administrators of new follower %s.', actorFollow.ActorFollower.url, { err })
111 })
112 }
113
86 private async notifySubscribersOfNewVideo (video: VideoModel) { 114 private async notifySubscribersOfNewVideo (video: VideoModel) {
87 // List all followers that are users 115 // List all followers that are users
88 const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId) 116 const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId)
@@ -147,10 +175,13 @@ class Notifier {
147 } 175 }
148 176
149 private async notifyOfCommentMention (comment: VideoCommentModel) { 177 private async notifyOfCommentMention (comment: VideoCommentModel) {
150 const usernames = comment.extractMentions() 178 const extractedUsernames = comment.extractMentions()
151 logger.debug('Extracted %d username from comment %s.', usernames.length, comment.url, { usernames, text: comment.text }) 179 logger.debug(
180 'Extracted %d username from comment %s.', extractedUsernames.length, comment.url,
181 { usernames: extractedUsernames, text: comment.text }
182 )
152 183
153 let users = await UserModel.listByUsernames(usernames) 184 let users = await UserModel.listByUsernames(extractedUsernames)
154 185
155 if (comment.Video.isOwned()) { 186 if (comment.Video.isOwned()) {
156 const userException = await UserModel.loadByVideoId(comment.videoId) 187 const userException = await UserModel.loadByVideoId(comment.videoId)
@@ -237,6 +268,33 @@ class Notifier {
237 return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender }) 268 return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender })
238 } 269 }
239 270
271 private async notifyAdminsOfNewInstanceFollow (actorFollow: ActorFollowModel) {
272 const admins = await UserModel.listWithRight(UserRight.MANAGE_SERVER_FOLLOW)
273
274 logger.info('Notifying %d administrators of new instance follower: %s.', admins.length, actorFollow.ActorFollower.url)
275
276 function settingGetter (user: UserModel) {
277 return user.NotificationSetting.newInstanceFollower
278 }
279
280 async function notificationCreator (user: UserModel) {
281 const notification = await UserNotificationModel.create({
282 type: UserNotificationType.NEW_INSTANCE_FOLLOWER,
283 userId: user.id,
284 actorFollowId: actorFollow.id
285 })
286 notification.ActorFollow = actorFollow
287
288 return notification
289 }
290
291 function emailSender (emails: string[]) {
292 return Emailer.Instance.addNewInstanceFollowerNotification(emails, actorFollow)
293 }
294
295 return this.notify({ users: admins, settingGetter, notificationCreator, emailSender })
296 }
297
240 private async notifyModeratorsOfNewVideoAbuse (videoAbuse: VideoAbuseModel) { 298 private async notifyModeratorsOfNewVideoAbuse (videoAbuse: VideoAbuseModel) {
241 const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_ABUSES) 299 const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_ABUSES)
242 if (moderators.length === 0) return 300 if (moderators.length === 0) return
@@ -265,6 +323,34 @@ class Notifier {
265 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) 323 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
266 } 324 }
267 325
326 private async notifyModeratorsOfVideoAutoBlacklist (video: VideoModel) {
327 const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_BLACKLIST)
328 if (moderators.length === 0) return
329
330 logger.info('Notifying %s moderators of video auto-blacklist %s.', moderators.length, video.url)
331
332 function settingGetter (user: UserModel) {
333 return user.NotificationSetting.videoAutoBlacklistAsModerator
334 }
335 async function notificationCreator (user: UserModel) {
336
337 const notification = await UserNotificationModel.create({
338 type: UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS,
339 userId: user.id,
340 videoId: video.id
341 })
342 notification.Video = video
343
344 return notification
345 }
346
347 function emailSender (emails: string[]) {
348 return Emailer.Instance.addVideoAutoBlacklistModeratorsNotification(emails, video)
349 }
350
351 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
352 }
353
268 private async notifyVideoOwnerOfBlacklist (videoBlacklist: VideoBlacklistModel) { 354 private async notifyVideoOwnerOfBlacklist (videoBlacklist: VideoBlacklistModel) {
269 const user = await UserModel.loadByVideoId(videoBlacklist.videoId) 355 const user = await UserModel.loadByVideoId(videoBlacklist.videoId)
270 if (!user) return 356 if (!user) return
@@ -436,7 +522,7 @@ class Notifier {
436 } 522 }
437 523
438 private isEmailEnabled (user: UserModel, value: UserNotificationSettingValue) { 524 private isEmailEnabled (user: UserModel, value: UserNotificationSettingValue) {
439 if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION === true && user.emailVerified !== true) return false 525 if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION === true && user.emailVerified === false) return false
440 526
441 return value & UserNotificationSettingValue.EMAIL 527 return value & UserNotificationSettingValue.EMAIL
442 } 528 }
diff --git a/server/lib/oauth-model.ts b/server/lib/oauth-model.ts
index 2cd2ae97c..45ac3e7c4 100644
--- a/server/lib/oauth-model.ts
+++ b/server/lib/oauth-model.ts
@@ -4,12 +4,13 @@ import { logger } from '../helpers/logger'
4import { UserModel } from '../models/account/user' 4import { UserModel } from '../models/account/user'
5import { OAuthClientModel } from '../models/oauth/oauth-client' 5import { OAuthClientModel } from '../models/oauth/oauth-client'
6import { OAuthTokenModel } from '../models/oauth/oauth-token' 6import { OAuthTokenModel } from '../models/oauth/oauth-token'
7import { CONFIG } from '../initializers/constants' 7import { CACHE } from '../initializers/constants'
8import { Transaction } from 'sequelize' 8import { Transaction } from 'sequelize'
9import { CONFIG } from '../initializers/config'
9 10
10type TokenInfo = { accessToken: string, refreshToken: string, accessTokenExpiresAt: Date, refreshTokenExpiresAt: Date } 11type TokenInfo = { accessToken: string, refreshToken: string, accessTokenExpiresAt: Date, refreshTokenExpiresAt: Date }
11const accessTokenCache: { [ accessToken: string ]: OAuthTokenModel } = {} 12let accessTokenCache: { [ accessToken: string ]: OAuthTokenModel } = {}
12const userHavingToken: { [ userId: number ]: string } = {} 13let userHavingToken: { [ userId: number ]: string } = {}
13 14
14// --------------------------------------------------------------------------- 15// ---------------------------------------------------------------------------
15 16
@@ -38,11 +39,19 @@ function clearCacheByToken (token: string) {
38function getAccessToken (bearerToken: string) { 39function getAccessToken (bearerToken: string) {
39 logger.debug('Getting access token (bearerToken: ' + bearerToken + ').') 40 logger.debug('Getting access token (bearerToken: ' + bearerToken + ').')
40 41
42 if (!bearerToken) return Bluebird.resolve(undefined)
43
41 if (accessTokenCache[bearerToken] !== undefined) return Bluebird.resolve(accessTokenCache[bearerToken]) 44 if (accessTokenCache[bearerToken] !== undefined) return Bluebird.resolve(accessTokenCache[bearerToken])
42 45
43 return OAuthTokenModel.getByTokenAndPopulateUser(bearerToken) 46 return OAuthTokenModel.getByTokenAndPopulateUser(bearerToken)
44 .then(tokenModel => { 47 .then(tokenModel => {
45 if (tokenModel) { 48 if (tokenModel) {
49 // Reinit our cache
50 if (Object.keys(accessTokenCache).length > CACHE.USER_TOKENS.MAX_SIZE) {
51 accessTokenCache = {}
52 userHavingToken = {}
53 }
54
46 accessTokenCache[ bearerToken ] = tokenModel 55 accessTokenCache[ bearerToken ] = tokenModel
47 userHavingToken[ tokenModel.userId ] = tokenModel.accessToken 56 userHavingToken[ tokenModel.userId ] = tokenModel.accessToken
48 } 57 }
diff --git a/server/lib/redis.ts b/server/lib/redis.ts
index 3628c0583..f77d0b62c 100644
--- a/server/lib/redis.ts
+++ b/server/lib/redis.ts
@@ -3,12 +3,13 @@ import { 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 { 5import {
6 CONFIG,
7 CONTACT_FORM_LIFETIME, 6 CONTACT_FORM_LIFETIME,
8 USER_EMAIL_VERIFY_LIFETIME, 7 USER_EMAIL_VERIFY_LIFETIME,
9 USER_PASSWORD_RESET_LIFETIME, 8 USER_PASSWORD_RESET_LIFETIME,
10 VIDEO_VIEW_LIFETIME 9 VIDEO_VIEW_LIFETIME,
11} from '../initializers' 10 WEBSERVER
11} from '../initializers/constants'
12import { CONFIG } from '../initializers/config'
12 13
13type CachedRoute = { 14type CachedRoute = {
14 body: string, 15 body: string,
@@ -30,7 +31,7 @@ class Redis {
30 if (this.initialized === true) return 31 if (this.initialized === true) return
31 this.initialized = true 32 this.initialized = true
32 33
33 this.client = createClient(Redis.getRedisClient()) 34 this.client = createClient(Redis.getRedisClientOptions())
34 35
35 this.client.on('error', err => { 36 this.client.on('error', err => {
36 logger.error('Error in Redis client.', { err }) 37 logger.error('Error in Redis client.', { err })
@@ -41,10 +42,10 @@ class Redis {
41 this.client.auth(CONFIG.REDIS.AUTH) 42 this.client.auth(CONFIG.REDIS.AUTH)
42 } 43 }
43 44
44 this.prefix = 'redis-' + CONFIG.WEBSERVER.HOST + '-' 45 this.prefix = 'redis-' + WEBSERVER.HOST + '-'
45 } 46 }
46 47
47 static getRedisClient () { 48 static getRedisClientOptions () {
48 return Object.assign({}, 49 return Object.assign({},
49 (CONFIG.REDIS.AUTH && CONFIG.REDIS.AUTH != null) ? { password: CONFIG.REDIS.AUTH } : {}, 50 (CONFIG.REDIS.AUTH && CONFIG.REDIS.AUTH != null) ? { password: CONFIG.REDIS.AUTH } : {},
50 (CONFIG.REDIS.DB) ? { db: CONFIG.REDIS.DB } : {}, 51 (CONFIG.REDIS.DB) ? { db: CONFIG.REDIS.DB } : {},
@@ -54,6 +55,14 @@ class Redis {
54 ) 55 )
55 } 56 }
56 57
58 getClient () {
59 return this.client
60 }
61
62 getPrefix () {
63 return this.prefix
64 }
65
57 /************* Forgot password *************/ 66 /************* Forgot password *************/
58 67
59 async setResetPasswordVerificationString (userId: number) { 68 async setResetPasswordVerificationString (userId: number) {
@@ -88,7 +97,7 @@ class Redis {
88 return this.setValue(this.generateContactFormKey(ip), '1', CONTACT_FORM_LIFETIME) 97 return this.setValue(this.generateContactFormKey(ip), '1', CONTACT_FORM_LIFETIME)
89 } 98 }
90 99
91 async isContactFormIpExists (ip: string) { 100 async doesContactFormIpExist (ip: string) {
92 return this.exists(this.generateContactFormKey(ip)) 101 return this.exists(this.generateContactFormKey(ip))
93 } 102 }
94 103
@@ -98,7 +107,7 @@ class Redis {
98 return this.setValue(this.generateViewKey(ip, videoUUID), '1', VIDEO_VIEW_LIFETIME) 107 return this.setValue(this.generateViewKey(ip, videoUUID), '1', VIDEO_VIEW_LIFETIME)
99 } 108 }
100 109
101 async isVideoIPViewExists (ip: string, videoUUID: string) { 110 async doesVideoIPViewExist (ip: string, videoUUID: string) {
102 return this.exists(this.generateViewKey(ip, videoUUID)) 111 return this.exists(this.generateViewKey(ip, videoUUID))
103 } 112 }
104 113
diff --git a/server/lib/schedulers/abstract-scheduler.ts b/server/lib/schedulers/abstract-scheduler.ts
index 86ea7aa38..0e6088911 100644
--- a/server/lib/schedulers/abstract-scheduler.ts
+++ b/server/lib/schedulers/abstract-scheduler.ts
@@ -1,4 +1,5 @@
1import { logger } from '../../helpers/logger' 1import { logger } from '../../helpers/logger'
2import * as Bluebird from 'bluebird'
2 3
3export abstract class AbstractScheduler { 4export abstract class AbstractScheduler {
4 5
@@ -30,5 +31,5 @@ export abstract class AbstractScheduler {
30 } 31 }
31 } 32 }
32 33
33 protected abstract internalExecute (): Promise<any> 34 protected abstract internalExecute (): Promise<any> | Bluebird<any>
34} 35}
diff --git a/server/lib/schedulers/actor-follow-scheduler.ts b/server/lib/schedulers/actor-follow-scheduler.ts
index 3967be7f8..fdd3ad5fa 100644
--- a/server/lib/schedulers/actor-follow-scheduler.ts
+++ b/server/lib/schedulers/actor-follow-scheduler.ts
@@ -2,8 +2,8 @@ import { isTestInstance } from '../../helpers/core-utils'
2import { logger } from '../../helpers/logger' 2import { 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/constants'
6import { ActorFollowScoreCache } from '../cache' 6import { ActorFollowScoreCache } from '../files-cache'
7 7
8export class ActorFollowScheduler extends AbstractScheduler { 8export class ActorFollowScheduler extends AbstractScheduler {
9 9
diff --git a/server/lib/schedulers/remove-old-history-scheduler.ts b/server/lib/schedulers/remove-old-history-scheduler.ts
new file mode 100644
index 000000000..1b5ff8394
--- /dev/null
+++ b/server/lib/schedulers/remove-old-history-scheduler.ts
@@ -0,0 +1,32 @@
1import { logger } from '../../helpers/logger'
2import { AbstractScheduler } from './abstract-scheduler'
3import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants'
4import { UserVideoHistoryModel } from '../../models/account/user-video-history'
5import { CONFIG } from '../../initializers/config'
6import { isTestInstance } from '../../helpers/core-utils'
7
8export class RemoveOldHistoryScheduler extends AbstractScheduler {
9
10 private static instance: AbstractScheduler
11
12 protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.removeOldHistory
13
14 private constructor () {
15 super()
16 }
17
18 protected internalExecute () {
19 if (CONFIG.HISTORY.VIDEOS.MAX_AGE === -1) return
20
21 logger.info('Removing old videos history.')
22
23 const now = new Date()
24 const beforeDate = new Date(now.getTime() - CONFIG.HISTORY.VIDEOS.MAX_AGE).toISOString()
25
26 return UserVideoHistoryModel.removeOldHistory(beforeDate)
27 }
28
29 static get Instance () {
30 return this.instance || (this.instance = new this())
31 }
32}
diff --git a/server/lib/schedulers/remove-old-jobs-scheduler.ts b/server/lib/schedulers/remove-old-jobs-scheduler.ts
index 4a4341ba9..0179a7618 100644
--- a/server/lib/schedulers/remove-old-jobs-scheduler.ts
+++ b/server/lib/schedulers/remove-old-jobs-scheduler.ts
@@ -2,7 +2,7 @@ import { isTestInstance } from '../../helpers/core-utils'
2import { logger } from '../../helpers/logger' 2import { logger } from '../../helpers/logger'
3import { JobQueue } from '../job-queue' 3import { JobQueue } from '../job-queue'
4import { AbstractScheduler } from './abstract-scheduler' 4import { AbstractScheduler } from './abstract-scheduler'
5import { SCHEDULER_INTERVALS_MS } from '../../initializers' 5import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants'
6 6
7export class RemoveOldJobsScheduler extends AbstractScheduler { 7export class RemoveOldJobsScheduler extends AbstractScheduler {
8 8
diff --git a/server/lib/schedulers/remove-old-views-scheduler.ts b/server/lib/schedulers/remove-old-views-scheduler.ts
new file mode 100644
index 000000000..39fbb9163
--- /dev/null
+++ b/server/lib/schedulers/remove-old-views-scheduler.ts
@@ -0,0 +1,33 @@
1import { logger } from '../../helpers/logger'
2import { AbstractScheduler } from './abstract-scheduler'
3import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants'
4import { UserVideoHistoryModel } from '../../models/account/user-video-history'
5import { CONFIG } from '../../initializers/config'
6import { isTestInstance } from '../../helpers/core-utils'
7import { VideoViewModel } from '../../models/video/video-views'
8
9export class RemoveOldViewsScheduler extends AbstractScheduler {
10
11 private static instance: AbstractScheduler
12
13 protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.removeOldViews
14
15 private constructor () {
16 super()
17 }
18
19 protected internalExecute () {
20 if (CONFIG.VIEWS.VIDEOS.REMOTE.MAX_AGE === -1) return
21
22 logger.info('Removing old videos views.')
23
24 const now = new Date()
25 const beforeDate = new Date(now.getTime() - CONFIG.VIEWS.VIDEOS.REMOTE.MAX_AGE).toISOString()
26
27 return VideoViewModel.removeOldRemoteViewsHistory(beforeDate)
28 }
29
30 static get Instance () {
31 return this.instance || (this.instance = new this())
32 }
33}
diff --git a/server/lib/schedulers/update-videos-scheduler.ts b/server/lib/schedulers/update-videos-scheduler.ts
index 2618a5857..80080a132 100644
--- a/server/lib/schedulers/update-videos-scheduler.ts
+++ b/server/lib/schedulers/update-videos-scheduler.ts
@@ -3,10 +3,11 @@ import { AbstractScheduler } from './abstract-scheduler'
3import { ScheduleVideoUpdateModel } from '../../models/video/schedule-video-update' 3import { ScheduleVideoUpdateModel } from '../../models/video/schedule-video-update'
4import { retryTransactionWrapper } from '../../helpers/database-utils' 4import { retryTransactionWrapper } from '../../helpers/database-utils'
5import { federateVideoIfNeeded } from '../activitypub' 5import { federateVideoIfNeeded } from '../activitypub'
6import { SCHEDULER_INTERVALS_MS, sequelizeTypescript } from '../../initializers' 6import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants'
7import { VideoPrivacy } from '../../../shared/models/videos' 7import { VideoPrivacy } from '../../../shared/models/videos'
8import { Notifier } from '../notifier' 8import { Notifier } from '../notifier'
9import { VideoModel } from '../../models/video/video' 9import { VideoModel } from '../../models/video/video'
10import { sequelizeTypescript } from '../../initializers/database'
10 11
11export class UpdateVideosScheduler extends AbstractScheduler { 12export class UpdateVideosScheduler extends AbstractScheduler {
12 13
@@ -57,7 +58,7 @@ export class UpdateVideosScheduler extends AbstractScheduler {
57 58
58 for (const v of publishedVideos) { 59 for (const v of publishedVideos) {
59 Notifier.Instance.notifyOnNewVideo(v) 60 Notifier.Instance.notifyOnNewVideo(v)
60 Notifier.Instance.notifyOnPendingVideoPublished(v) 61 Notifier.Instance.notifyOnVideoPublishedAfterScheduledUpdate(v)
61 } 62 }
62 } 63 }
63 64
diff --git a/server/lib/schedulers/videos-redundancy-scheduler.ts b/server/lib/schedulers/videos-redundancy-scheduler.ts
index f643ee226..01af1e9d2 100644
--- a/server/lib/schedulers/videos-redundancy-scheduler.ts
+++ b/server/lib/schedulers/videos-redundancy-scheduler.ts
@@ -1,5 +1,5 @@
1import { AbstractScheduler } from './abstract-scheduler' 1import { AbstractScheduler } from './abstract-scheduler'
2import { CONFIG, REDUNDANCY, VIDEO_IMPORT_TIMEOUT } from '../../initializers' 2import { HLS_REDUNDANCY_DIRECTORY, REDUNDANCY, VIDEO_IMPORT_TIMEOUT, WEBSERVER } from '../../initializers/constants'
3import { logger } from '../../helpers/logger' 3import { logger } from '../../helpers/logger'
4import { VideosRedundancy } from '../../../shared/models/redundancy' 4import { VideosRedundancy } from '../../../shared/models/redundancy'
5import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' 5import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy'
@@ -9,9 +9,20 @@ import { join } from 'path'
9import { move } 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, getVideoCacheStreamingPlaylistActivityPubUrl } from '../activitypub/url'
13import { removeVideoRedundancy } from '../redundancy' 13import { removeVideoRedundancy } from '../redundancy'
14import { getOrCreateVideoAndAccountAndChannel } from '../activitypub' 14import { getOrCreateVideoAndAccountAndChannel } from '../activitypub'
15import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist'
16import { VideoModel } from '../../models/video/video'
17import { downloadPlaylistSegments } from '../hls'
18import { CONFIG } from '../../initializers/config'
19
20type CandidateToDuplicate = {
21 redundancy: VideosRedundancy,
22 video: VideoModel,
23 files: VideoFileModel[],
24 streamingPlaylists: VideoStreamingPlaylistModel[]
25}
15 26
16export class VideosRedundancyScheduler extends AbstractScheduler { 27export class VideosRedundancyScheduler extends AbstractScheduler {
17 28
@@ -24,28 +35,32 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
24 } 35 }
25 36
26 protected async internalExecute () { 37 protected async internalExecute () {
27 for (const obj of CONFIG.REDUNDANCY.VIDEOS.STRATEGIES) { 38 for (const redundancyConfig of CONFIG.REDUNDANCY.VIDEOS.STRATEGIES) {
28 logger.info('Running redundancy scheduler for strategy %s.', obj.strategy) 39 logger.info('Running redundancy scheduler for strategy %s.', redundancyConfig.strategy)
29 40
30 try { 41 try {
31 const videoToDuplicate = await this.findVideoToDuplicate(obj) 42 const videoToDuplicate = await this.findVideoToDuplicate(redundancyConfig)
32 if (!videoToDuplicate) continue 43 if (!videoToDuplicate) continue
33 44
34 const videoFiles = videoToDuplicate.VideoFiles 45 const candidateToDuplicate = {
35 videoFiles.forEach(f => f.Video = videoToDuplicate) 46 video: videoToDuplicate,
47 redundancy: redundancyConfig,
48 files: videoToDuplicate.VideoFiles,
49 streamingPlaylists: videoToDuplicate.VideoStreamingPlaylists
50 }
36 51
37 await this.purgeCacheIfNeeded(obj, videoFiles) 52 await this.purgeCacheIfNeeded(candidateToDuplicate)
38 53
39 if (await this.isTooHeavy(obj, videoFiles)) { 54 if (await this.isTooHeavy(candidateToDuplicate)) {
40 logger.info('Video %s is too big for our cache, skipping.', videoToDuplicate.url) 55 logger.info('Video %s is too big for our cache, skipping.', videoToDuplicate.url)
41 continue 56 continue
42 } 57 }
43 58
44 logger.info('Will duplicate video %s in redundancy scheduler "%s".', videoToDuplicate.url, obj.strategy) 59 logger.info('Will duplicate video %s in redundancy scheduler "%s".', videoToDuplicate.url, redundancyConfig.strategy)
45 60
46 await this.createVideoRedundancy(obj, videoFiles) 61 await this.createVideoRedundancies(candidateToDuplicate)
47 } catch (err) { 62 } catch (err) {
48 logger.error('Cannot run videos redundancy %s.', obj.strategy, { err }) 63 logger.error('Cannot run videos redundancy %s.', redundancyConfig.strategy, { err })
49 } 64 }
50 } 65 }
51 66
@@ -63,25 +78,35 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
63 78
64 for (const redundancyModel of expired) { 79 for (const redundancyModel of expired) {
65 try { 80 try {
66 await this.extendsOrDeleteRedundancy(redundancyModel) 81 const redundancyConfig = CONFIG.REDUNDANCY.VIDEOS.STRATEGIES.find(s => s.strategy === redundancyModel.strategy)
82 const candidate = {
83 redundancy: redundancyConfig,
84 video: null,
85 files: [],
86 streamingPlaylists: []
87 }
88
89 // If the administrator disabled the redundancy or decreased the cache size, remove this redundancy instead of extending it
90 if (!redundancyConfig || await this.isTooHeavy(candidate)) {
91 logger.info('Destroying redundancy %s because the cache size %s is too heavy.', redundancyModel.url, redundancyModel.strategy)
92 await removeVideoRedundancy(redundancyModel)
93 } else {
94 await this.extendsRedundancy(redundancyModel)
95 }
67 } catch (err) { 96 } catch (err) {
68 logger.error('Cannot extend expiration of %s video from our redundancy system.', this.buildEntryLogId(redundancyModel)) 97 logger.error(
98 'Cannot extend or remove expiration of %s video from our redundancy system.', this.buildEntryLogId(redundancyModel),
99 { err }
100 )
69 } 101 }
70 } 102 }
71 } 103 }
72 104
73 private async extendsOrDeleteRedundancy (redundancyModel: VideoRedundancyModel) { 105 private async extendsRedundancy (redundancyModel: VideoRedundancyModel) {
74 // Refresh the video, maybe it was deleted
75 const video = await this.loadAndRefreshVideo(redundancyModel.VideoFile.Video.url)
76
77 if (!video) {
78 logger.info('Destroying existing redundancy %s, because the associated video does not exist anymore.', redundancyModel.url)
79
80 await redundancyModel.destroy()
81 return
82 }
83
84 const redundancy = CONFIG.REDUNDANCY.VIDEOS.STRATEGIES.find(s => s.strategy === redundancyModel.strategy) 106 const redundancy = CONFIG.REDUNDANCY.VIDEOS.STRATEGIES.find(s => s.strategy === redundancyModel.strategy)
107 // Redundancy strategy disabled, remove our redundancy instead of extending expiration
108 if (!redundancy) await removeVideoRedundancy(redundancyModel)
109
85 await this.extendsExpirationOf(redundancyModel, redundancy.minLifetime) 110 await this.extendsExpirationOf(redundancyModel, redundancy.minLifetime)
86 } 111 }
87 112
@@ -112,49 +137,93 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
112 } 137 }
113 } 138 }
114 139
115 private async createVideoRedundancy (redundancy: VideosRedundancy, filesToDuplicate: VideoFileModel[]) { 140 private async createVideoRedundancies (data: CandidateToDuplicate) {
116 const serverActor = await getServerActor() 141 const video = await this.loadAndRefreshVideo(data.video.url)
142
143 if (!video) {
144 logger.info('Video %s we want to duplicate does not existing anymore, skipping.', data.video.url)
117 145
118 for (const file of filesToDuplicate) { 146 return
119 const video = await this.loadAndRefreshVideo(file.Video.url) 147 }
120 148
149 for (const file of data.files) {
121 const existingRedundancy = await VideoRedundancyModel.loadLocalByFileId(file.id) 150 const existingRedundancy = await VideoRedundancyModel.loadLocalByFileId(file.id)
122 if (existingRedundancy) { 151 if (existingRedundancy) {
123 await this.extendsOrDeleteRedundancy(existingRedundancy) 152 await this.extendsRedundancy(existingRedundancy)
124 153
125 continue 154 continue
126 } 155 }
127 156
128 if (!video) { 157 await this.createVideoFileRedundancy(data.redundancy, video, file)
129 logger.info('Video %s we want to duplicate does not existing anymore, skipping.', file.Video.url) 158 }
159
160 for (const streamingPlaylist of data.streamingPlaylists) {
161 const existingRedundancy = await VideoRedundancyModel.loadLocalByStreamingPlaylistId(streamingPlaylist.id)
162 if (existingRedundancy) {
163 await this.extendsRedundancy(existingRedundancy)
130 164
131 continue 165 continue
132 } 166 }
133 167
134 logger.info('Duplicating %s - %d in videos redundancy with "%s" strategy.', video.url, file.resolution, redundancy.strategy) 168 await this.createStreamingPlaylistRedundancy(data.redundancy, video, streamingPlaylist)
169 }
170 }
135 171
136 const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() 172 private async createVideoFileRedundancy (redundancy: VideosRedundancy, video: VideoModel, file: VideoFileModel) {
137 const magnetUri = video.generateMagnetUri(file, baseUrlHttp, baseUrlWs) 173 file.Video = video
138 174
139 const tmpPath = await downloadWebTorrentVideo({ magnetUri }, VIDEO_IMPORT_TIMEOUT) 175 const serverActor = await getServerActor()
140 176
141 const destPath = join(CONFIG.STORAGE.REDUNDANCY_DIR, video.getVideoFilename(file)) 177 logger.info('Duplicating %s - %d in videos redundancy with "%s" strategy.', video.url, file.resolution, redundancy.strategy)
142 await move(tmpPath, destPath)
143 178
144 const createdModel = await VideoRedundancyModel.create({ 179 const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
145 expiresOn: this.buildNewExpiration(redundancy.minLifetime), 180 const magnetUri = video.generateMagnetUri(file, baseUrlHttp, baseUrlWs)
146 url: getVideoCacheFileActivityPubUrl(file),
147 fileUrl: video.getVideoRedundancyUrl(file, CONFIG.WEBSERVER.URL),
148 strategy: redundancy.strategy,
149 videoFileId: file.id,
150 actorId: serverActor.id
151 })
152 createdModel.VideoFile = file
153 181
154 await sendCreateCacheFile(serverActor, createdModel) 182 const tmpPath = await downloadWebTorrentVideo({ magnetUri }, VIDEO_IMPORT_TIMEOUT)
155 183
156 logger.info('Duplicated %s - %d -> %s.', video.url, file.resolution, createdModel.url) 184 const destPath = join(CONFIG.STORAGE.REDUNDANCY_DIR, video.getVideoFilename(file))
157 } 185 await move(tmpPath, destPath)
186
187 const createdModel = await VideoRedundancyModel.create({
188 expiresOn: this.buildNewExpiration(redundancy.minLifetime),
189 url: getVideoCacheFileActivityPubUrl(file),
190 fileUrl: video.getVideoRedundancyUrl(file, WEBSERVER.URL),
191 strategy: redundancy.strategy,
192 videoFileId: file.id,
193 actorId: serverActor.id
194 })
195
196 createdModel.VideoFile = file
197
198 await sendCreateCacheFile(serverActor, video, createdModel)
199
200 logger.info('Duplicated %s - %d -> %s.', video.url, file.resolution, createdModel.url)
201 }
202
203 private async createStreamingPlaylistRedundancy (redundancy: VideosRedundancy, video: VideoModel, playlist: VideoStreamingPlaylistModel) {
204 playlist.Video = video
205
206 const serverActor = await getServerActor()
207
208 logger.info('Duplicating %s streaming playlist in videos redundancy with "%s" strategy.', video.url, redundancy.strategy)
209
210 const destDirectory = join(HLS_REDUNDANCY_DIRECTORY, video.uuid)
211 await downloadPlaylistSegments(playlist.playlistUrl, destDirectory, VIDEO_IMPORT_TIMEOUT)
212
213 const createdModel = await VideoRedundancyModel.create({
214 expiresOn: this.buildNewExpiration(redundancy.minLifetime),
215 url: getVideoCacheStreamingPlaylistActivityPubUrl(video, playlist),
216 fileUrl: playlist.getVideoRedundancyUrl(WEBSERVER.URL),
217 strategy: redundancy.strategy,
218 videoStreamingPlaylistId: playlist.id,
219 actorId: serverActor.id
220 })
221
222 createdModel.VideoStreamingPlaylist = playlist
223
224 await sendCreateCacheFile(serverActor, video, createdModel)
225
226 logger.info('Duplicated playlist %s -> %s.', playlist.playlistUrl, createdModel.url)
158 } 227 }
159 228
160 private async extendsExpirationOf (redundancy: VideoRedundancyModel, expiresAfterMs: number) { 229 private async extendsExpirationOf (redundancy: VideoRedundancyModel, expiresAfterMs: number) {
@@ -168,8 +237,9 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
168 await sendUpdateCacheFile(serverActor, redundancy) 237 await sendUpdateCacheFile(serverActor, redundancy)
169 } 238 }
170 239
171 private async purgeCacheIfNeeded (redundancy: VideosRedundancy, filesToDuplicate: VideoFileModel[]) { 240 private async purgeCacheIfNeeded (candidateToDuplicate: CandidateToDuplicate) {
172 while (this.isTooHeavy(redundancy, filesToDuplicate)) { 241 while (this.isTooHeavy(candidateToDuplicate)) {
242 const redundancy = candidateToDuplicate.redundancy
173 const toDelete = await VideoRedundancyModel.loadOldestLocalThatAlreadyExpired(redundancy.strategy, redundancy.minLifetime) 243 const toDelete = await VideoRedundancyModel.loadOldestLocalThatAlreadyExpired(redundancy.strategy, redundancy.minLifetime)
174 if (!toDelete) return 244 if (!toDelete) return
175 245
@@ -177,11 +247,11 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
177 } 247 }
178 } 248 }
179 249
180 private async isTooHeavy (redundancy: VideosRedundancy, filesToDuplicate: VideoFileModel[]) { 250 private async isTooHeavy (candidateToDuplicate: CandidateToDuplicate) {
181 const maxSize = redundancy.size 251 const maxSize = candidateToDuplicate.redundancy.size
182 252
183 const totalDuplicated = await VideoRedundancyModel.getTotalDuplicated(redundancy.strategy) 253 const totalDuplicated = await VideoRedundancyModel.getTotalDuplicated(candidateToDuplicate.redundancy.strategy)
184 const totalWillDuplicate = totalDuplicated + this.getTotalFileSizes(filesToDuplicate) 254 const totalWillDuplicate = totalDuplicated + this.getTotalFileSizes(candidateToDuplicate.files, candidateToDuplicate.streamingPlaylists)
185 255
186 return totalWillDuplicate > maxSize 256 return totalWillDuplicate > maxSize
187 } 257 }
@@ -191,13 +261,15 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
191 } 261 }
192 262
193 private buildEntryLogId (object: VideoRedundancyModel) { 263 private buildEntryLogId (object: VideoRedundancyModel) {
194 return `${object.VideoFile.Video.url}-${object.VideoFile.resolution}` 264 if (object.VideoFile) return `${object.VideoFile.Video.url}-${object.VideoFile.resolution}`
265
266 return `${object.VideoStreamingPlaylist.playlistUrl}`
195 } 267 }
196 268
197 private getTotalFileSizes (files: VideoFileModel[]) { 269 private getTotalFileSizes (files: VideoFileModel[], playlists: VideoStreamingPlaylistModel[]) {
198 const fileReducer = (previous: number, current: VideoFileModel) => previous + current.size 270 const fileReducer = (previous: number, current: VideoFileModel) => previous + current.size
199 271
200 return files.reduce(fileReducer, 0) 272 return files.reduce(fileReducer, 0) * playlists.length
201 } 273 }
202 274
203 private async loadAndRefreshVideo (videoUrl: string) { 275 private async loadAndRefreshVideo (videoUrl: string) {
diff --git a/server/lib/schedulers/youtube-dl-update-scheduler.ts b/server/lib/schedulers/youtube-dl-update-scheduler.ts
index aa027116d..aefe6aba4 100644
--- a/server/lib/schedulers/youtube-dl-update-scheduler.ts
+++ b/server/lib/schedulers/youtube-dl-update-scheduler.ts
@@ -1,5 +1,5 @@
1import { AbstractScheduler } from './abstract-scheduler' 1import { AbstractScheduler } from './abstract-scheduler'
2import { SCHEDULER_INTERVALS_MS } from '../../initializers' 2import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants'
3import { updateYoutubeDLBinary } from '../../helpers/youtube-dl' 3import { updateYoutubeDLBinary } from '../../helpers/youtube-dl'
4 4
5export class YoutubeDlUpdateScheduler extends AbstractScheduler { 5export class YoutubeDlUpdateScheduler extends AbstractScheduler {
diff --git a/server/lib/thumbnail.ts b/server/lib/thumbnail.ts
new file mode 100644
index 000000000..950b14c3b
--- /dev/null
+++ b/server/lib/thumbnail.ts
@@ -0,0 +1,151 @@
1import { VideoFileModel } from '../models/video/video-file'
2import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils'
3import { CONFIG } from '../initializers/config'
4import { PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants'
5import { VideoModel } from '../models/video/video'
6import { ThumbnailModel } from '../models/video/thumbnail'
7import { ThumbnailType } from '../../shared/models/videos/thumbnail.type'
8import { processImage } from '../helpers/image-utils'
9import { join } from 'path'
10import { downloadImage } from '../helpers/requests'
11import { VideoPlaylistModel } from '../models/video/video-playlist'
12
13type ImageSize = { height: number, width: number }
14
15function createPlaylistMiniatureFromExisting (inputPath: string, playlist: VideoPlaylistModel, keepOriginal = false, size?: ImageSize) {
16 const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromPlaylist(playlist, size)
17 const type = ThumbnailType.MINIATURE
18
19 const thumbnailCreator = () => processImage(inputPath, outputPath, { width, height }, keepOriginal)
20 return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail })
21}
22
23function createPlaylistMiniatureFromUrl (fileUrl: string, playlist: VideoPlaylistModel, size?: ImageSize) {
24 const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromPlaylist(playlist, size)
25 const type = ThumbnailType.MINIATURE
26
27 const thumbnailCreator = () => downloadImage(fileUrl, basePath, filename, { width, height })
28 return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl })
29}
30
31function createVideoMiniatureFromUrl (fileUrl: string, video: VideoModel, type: ThumbnailType, size?: ImageSize) {
32 const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
33 const thumbnailCreator = () => downloadImage(fileUrl, basePath, filename, { width, height })
34
35 return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl })
36}
37
38function createVideoMiniatureFromExisting (inputPath: string, video: VideoModel, type: ThumbnailType, size?: ImageSize) {
39 const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
40 const thumbnailCreator = () => processImage(inputPath, outputPath, { width, height })
41
42 return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail })
43}
44
45function generateVideoMiniature (video: VideoModel, videoFile: VideoFileModel, type: ThumbnailType) {
46 const input = video.getVideoFilePath(videoFile)
47
48 const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type)
49 const thumbnailCreator = () => generateImageFromVideoFile(input, basePath, filename, { height, width })
50
51 return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail })
52}
53
54function createPlaceholderThumbnail (fileUrl: string, video: VideoModel, type: ThumbnailType, size: ImageSize) {
55 const { filename, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
56
57 const thumbnail = existingThumbnail ? existingThumbnail : new ThumbnailModel()
58
59 thumbnail.filename = filename
60 thumbnail.height = height
61 thumbnail.width = width
62 thumbnail.type = type
63 thumbnail.fileUrl = fileUrl
64
65 return thumbnail
66}
67
68// ---------------------------------------------------------------------------
69
70export {
71 generateVideoMiniature,
72 createVideoMiniatureFromUrl,
73 createVideoMiniatureFromExisting,
74 createPlaceholderThumbnail,
75 createPlaylistMiniatureFromUrl,
76 createPlaylistMiniatureFromExisting
77}
78
79function buildMetadataFromPlaylist (playlist: VideoPlaylistModel, size: ImageSize) {
80 const filename = playlist.generateThumbnailName()
81 const basePath = CONFIG.STORAGE.THUMBNAILS_DIR
82
83 return {
84 filename,
85 basePath,
86 existingThumbnail: playlist.Thumbnail,
87 outputPath: join(basePath, filename),
88 height: size ? size.height : THUMBNAILS_SIZE.height,
89 width: size ? size.width : THUMBNAILS_SIZE.width
90 }
91}
92
93function buildMetadataFromVideo (video: VideoModel, type: ThumbnailType, size?: ImageSize) {
94 const existingThumbnail = Array.isArray(video.Thumbnails)
95 ? video.Thumbnails.find(t => t.type === type)
96 : undefined
97
98 if (type === ThumbnailType.MINIATURE) {
99 const filename = video.generateThumbnailName()
100 const basePath = CONFIG.STORAGE.THUMBNAILS_DIR
101
102 return {
103 filename,
104 basePath,
105 existingThumbnail,
106 outputPath: join(basePath, filename),
107 height: size ? size.height : THUMBNAILS_SIZE.height,
108 width: size ? size.width : THUMBNAILS_SIZE.width
109 }
110 }
111
112 if (type === ThumbnailType.PREVIEW) {
113 const filename = video.generatePreviewName()
114 const basePath = CONFIG.STORAGE.PREVIEWS_DIR
115
116 return {
117 filename,
118 basePath,
119 existingThumbnail,
120 outputPath: join(basePath, filename),
121 height: size ? size.height : PREVIEWS_SIZE.height,
122 width: size ? size.width : PREVIEWS_SIZE.width
123 }
124 }
125
126 return undefined
127}
128
129async function createThumbnailFromFunction (parameters: {
130 thumbnailCreator: () => Promise<any>,
131 filename: string,
132 height: number,
133 width: number,
134 type: ThumbnailType,
135 fileUrl?: string,
136 existingThumbnail?: ThumbnailModel
137}) {
138 const { thumbnailCreator, filename, width, height, type, existingThumbnail, fileUrl = null } = parameters
139
140 const thumbnail = existingThumbnail ? existingThumbnail : new ThumbnailModel()
141
142 thumbnail.filename = filename
143 thumbnail.height = height
144 thumbnail.width = width
145 thumbnail.type = type
146 thumbnail.fileUrl = fileUrl
147
148 await thumbnailCreator()
149
150 return thumbnail
151}
diff --git a/server/lib/user.ts b/server/lib/user.ts
index a39ef6c3d..7badb3e72 100644
--- a/server/lib/user.ts
+++ b/server/lib/user.ts
@@ -1,18 +1,19 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import * as uuidv4 from 'uuid/v4' 2import * as uuidv4 from 'uuid/v4'
3import { ActivityPubActorType } from '../../shared/models/activitypub' 3import { ActivityPubActorType } from '../../shared/models/activitypub'
4import { sequelizeTypescript, SERVER_ACTOR_NAME } from '../initializers' 4import { SERVER_ACTOR_NAME } from '../initializers/constants'
5import { AccountModel } from '../models/account/account' 5import { AccountModel } from '../models/account/account'
6import { UserModel } from '../models/account/user' 6import { UserModel } from '../models/account/user'
7import { buildActorInstance, getAccountActivityPubUrl, setAsyncActorKeys } from './activitypub' 7import { buildActorInstance, getAccountActivityPubUrl, setAsyncActorKeys } from './activitypub'
8import { createVideoChannel } from './video-channel' 8import { 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'
11import { ActorModel } from '../models/activitypub/actor' 10import { ActorModel } from '../models/activitypub/actor'
12import { UserNotificationSettingModel } from '../models/account/user-notification-setting' 11import { UserNotificationSettingModel } from '../models/account/user-notification-setting'
13import { UserNotificationSetting, UserNotificationSettingValue } from '../../shared/models/users' 12import { UserNotificationSetting, UserNotificationSettingValue } from '../../shared/models/users'
13import { createWatchLaterPlaylist } from './video-playlist'
14import { sequelizeTypescript } from '../initializers/database'
14 15
15async function createUserAccountAndChannel (userToCreate: UserModel, validateUser = true) { 16async function createUserAccountAndChannelAndPlaylist (userToCreate: UserModel, validateUser = true) {
16 const { user, account, videoChannel } = await sequelizeTypescript.transaction(async t => { 17 const { user, account, videoChannel } = await sequelizeTypescript.transaction(async t => {
17 const userOptions = { 18 const userOptions = {
18 transaction: t, 19 transaction: t,
@@ -38,7 +39,9 @@ async function createUserAccountAndChannel (userToCreate: UserModel, validateUse
38 } 39 }
39 const videoChannel = await createVideoChannel(videoChannelInfo, accountCreated, t) 40 const videoChannel = await createVideoChannel(videoChannelInfo, accountCreated, t)
40 41
41 return { user: userCreated, account: accountCreated, videoChannel } 42 const videoPlaylist = await createWatchLaterPlaylist(accountCreated, t)
43
44 return { user: userCreated, account: accountCreated, videoChannel, videoPlaylist }
42 }) 45 })
43 46
44 const [ accountKeys, channelKeys ] = await Promise.all([ 47 const [ accountKeys, channelKeys ] = await Promise.all([
@@ -69,7 +72,7 @@ async function createLocalAccountWithoutKeys (
69 userId, 72 userId,
70 applicationId, 73 applicationId,
71 actorId: actorInstanceCreated.id 74 actorId: actorInstanceCreated.id
72 } as FilteredModelAttributes<AccountModel>) 75 })
73 76
74 const accountInstanceCreated = await accountInstance.save({ transaction: t }) 77 const accountInstanceCreated = await accountInstance.save({ transaction: t })
75 accountInstanceCreated.Actor = actorInstanceCreated 78 accountInstanceCreated.Actor = actorInstanceCreated
@@ -89,7 +92,7 @@ async function createApplicationActor (applicationId: number) {
89 92
90export { 93export {
91 createApplicationActor, 94 createApplicationActor,
92 createUserAccountAndChannel, 95 createUserAccountAndChannelAndPlaylist,
93 createLocalAccountWithoutKeys 96 createLocalAccountWithoutKeys
94} 97}
95 98
@@ -103,10 +106,12 @@ function createDefaultUserNotificationSettings (user: UserModel, t: Sequelize.Tr
103 myVideoImportFinished: UserNotificationSettingValue.WEB, 106 myVideoImportFinished: UserNotificationSettingValue.WEB,
104 myVideoPublished: UserNotificationSettingValue.WEB, 107 myVideoPublished: UserNotificationSettingValue.WEB,
105 videoAbuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 108 videoAbuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
109 videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
106 blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 110 blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
107 newUserRegistration: UserNotificationSettingValue.WEB, 111 newUserRegistration: UserNotificationSettingValue.WEB,
108 commentMention: UserNotificationSettingValue.WEB, 112 commentMention: UserNotificationSettingValue.WEB,
109 newFollow: UserNotificationSettingValue.WEB 113 newFollow: UserNotificationSettingValue.WEB,
114 newInstanceFollower: UserNotificationSettingValue.WEB
110 } 115 }
111 116
112 return UserNotificationSettingModel.create(values, { transaction: t }) 117 return UserNotificationSettingModel.create(values, { transaction: t })
diff --git a/server/lib/video-blacklist.ts b/server/lib/video-blacklist.ts
new file mode 100644
index 000000000..985b89e31
--- /dev/null
+++ b/server/lib/video-blacklist.ts
@@ -0,0 +1,33 @@
1import * as sequelize from 'sequelize'
2import { CONFIG } from '../initializers/config'
3import { UserRight, VideoBlacklistType } from '../../shared/models'
4import { VideoBlacklistModel } from '../models/video/video-blacklist'
5import { UserModel } from '../models/account/user'
6import { VideoModel } from '../models/video/video'
7import { logger } from '../helpers/logger'
8import { UserAdminFlag } from '../../shared/models/users/user-flag.model'
9
10async function autoBlacklistVideoIfNeeded (video: VideoModel, user: UserModel, transaction: sequelize.Transaction) {
11 if (!CONFIG.AUTO_BLACKLIST.VIDEOS.OF_USERS.ENABLED) return false
12
13 if (user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) || user.hasAdminFlag(UserAdminFlag.BY_PASS_VIDEO_AUTO_BLACKLIST)) return false
14
15 const sequelizeOptions = { transaction }
16 const videoBlacklistToCreate = {
17 videoId: video.id,
18 unfederated: true,
19 reason: 'Auto-blacklisted. Moderator review required.',
20 type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED
21 }
22 await VideoBlacklistModel.create(videoBlacklistToCreate, sequelizeOptions)
23
24 logger.info('Video %s auto-blacklisted.', video.uuid)
25
26 return true
27}
28
29// ---------------------------------------------------------------------------
30
31export {
32 autoBlacklistVideoIfNeeded
33}
diff --git a/server/lib/video-comment.ts b/server/lib/video-comment.ts
index 59bce7520..bfe22d225 100644
--- a/server/lib/video-comment.ts
+++ b/server/lib/video-comment.ts
@@ -28,7 +28,7 @@ async function createVideoComment (obj: {
28 videoId: obj.video.id, 28 videoId: obj.video.id,
29 accountId: obj.account.id, 29 accountId: obj.account.id,
30 url: 'fake url' 30 url: 'fake url'
31 }, { transaction: t, validate: false }) 31 }, { transaction: t, validate: false } as any) // FIXME: sequelize typings
32 32
33 comment.set('url', getVideoCommentActivityPubUrl(obj.video, comment)) 33 comment.set('url', getVideoCommentActivityPubUrl(obj.video, comment))
34 34
diff --git a/server/lib/video-playlist.ts b/server/lib/video-playlist.ts
new file mode 100644
index 000000000..6e214e60f
--- /dev/null
+++ b/server/lib/video-playlist.ts
@@ -0,0 +1,29 @@
1import * as Sequelize from 'sequelize'
2import { AccountModel } from '../models/account/account'
3import { VideoPlaylistModel } from '../models/video/video-playlist'
4import { VideoPlaylistPrivacy } from '../../shared/models/videos/playlist/video-playlist-privacy.model'
5import { getVideoPlaylistActivityPubUrl } from './activitypub'
6import { VideoPlaylistType } from '../../shared/models/videos/playlist/video-playlist-type.model'
7
8async function createWatchLaterPlaylist (account: AccountModel, t: Sequelize.Transaction) {
9 const videoPlaylist = new VideoPlaylistModel({
10 name: 'Watch later',
11 privacy: VideoPlaylistPrivacy.PRIVATE,
12 type: VideoPlaylistType.WATCH_LATER,
13 ownerAccountId: account.id
14 })
15
16 videoPlaylist.url = getVideoPlaylistActivityPubUrl(videoPlaylist) // We use the UUID, so set the URL after building the object
17
18 await videoPlaylist.save({ transaction: t })
19
20 videoPlaylist.OwnerAccount = account
21
22 return videoPlaylist
23}
24
25// ---------------------------------------------------------------------------
26
27export {
28 createWatchLaterPlaylist
29}
diff --git a/server/lib/video-transcoding.ts b/server/lib/video-transcoding.ts
index 4460f46e4..0fe0ff12a 100644
--- a/server/lib/video-transcoding.ts
+++ b/server/lib/video-transcoding.ts
@@ -1,11 +1,15 @@
1import { CONFIG } from '../initializers' 1import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSERVER } from '../initializers/constants'
2import { extname, join } from 'path' 2import { join } from 'path'
3import { getVideoFileFPS, getVideoFileResolution, transcode } from '../helpers/ffmpeg-utils' 3import { getVideoFileFPS, transcode } from '../helpers/ffmpeg-utils'
4import { copy, remove, move, stat } from 'fs-extra' 4import { ensureDir, move, remove, 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'
8import { VideoModel } from '../models/video/video' 8import { VideoModel } from '../models/video/video'
9import { updateMasterHLSPlaylist, updateSha256Segments } from './hls'
10import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
11import { VideoStreamingPlaylistType } from '../../shared/models/videos/video-streaming-playlist.type'
12import { CONFIG } from '../initializers/config'
9 13
10async function optimizeVideofile (video: VideoModel, inputVideoFileArg?: VideoFileModel) { 14async function optimizeVideofile (video: VideoModel, inputVideoFileArg?: VideoFileModel) {
11 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR 15 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
@@ -17,7 +21,8 @@ async function optimizeVideofile (video: VideoModel, inputVideoFileArg?: VideoFi
17 21
18 const transcodeOptions = { 22 const transcodeOptions = {
19 inputPath: videoInputPath, 23 inputPath: videoInputPath,
20 outputPath: videoTranscodedPath 24 outputPath: videoTranscodedPath,
25 resolution: inputVideoFile.resolution
21 } 26 }
22 27
23 // Could be very long! 28 // Could be very long!
@@ -47,7 +52,7 @@ async function optimizeVideofile (video: VideoModel, inputVideoFileArg?: VideoFi
47 } 52 }
48} 53}
49 54
50async function transcodeOriginalVideofile (video: VideoModel, resolution: VideoResolution, isPortraitMode: boolean) { 55async function transcodeOriginalVideofile (video: VideoModel, resolution: VideoResolution, isPortrait: boolean) {
51 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR 56 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
52 const extname = '.mp4' 57 const extname = '.mp4'
53 58
@@ -60,13 +65,13 @@ async function transcodeOriginalVideofile (video: VideoModel, resolution: VideoR
60 size: 0, 65 size: 0,
61 videoId: video.id 66 videoId: video.id
62 }) 67 })
63 const videoOutputPath = join(videosDirectory, video.getVideoFilename(newVideoFile)) 68 const videoOutputPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename(newVideoFile))
64 69
65 const transcodeOptions = { 70 const transcodeOptions = {
66 inputPath: videoInputPath, 71 inputPath: videoInputPath,
67 outputPath: videoOutputPath, 72 outputPath: videoOutputPath,
68 resolution, 73 resolution,
69 isPortraitMode 74 isPortraitMode: isPortrait
70 } 75 }
71 76
72 await transcode(transcodeOptions) 77 await transcode(transcodeOptions)
@@ -84,48 +89,44 @@ async function transcodeOriginalVideofile (video: VideoModel, resolution: VideoR
84 video.VideoFiles.push(newVideoFile) 89 video.VideoFiles.push(newVideoFile)
85} 90}
86 91
87async function importVideoFile (video: VideoModel, inputFilePath: string) { 92async function generateHlsPlaylist (video: VideoModel, resolution: VideoResolution, isPortraitMode: boolean) {
88 const { videoFileResolution } = await getVideoFileResolution(inputFilePath) 93 const baseHlsDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)
89 const { size } = await stat(inputFilePath) 94 await ensureDir(join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid))
90 const fps = await getVideoFileFPS(inputFilePath)
91 95
92 let updatedVideoFile = new VideoFileModel({ 96 const videoInputPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename(video.getOriginalFile()))
93 resolution: videoFileResolution, 97 const outputPath = join(baseHlsDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution))
94 extname: extname(inputFilePath),
95 size,
96 fps,
97 videoId: video.id
98 })
99
100 const currentVideoFile = video.VideoFiles.find(videoFile => videoFile.resolution === updatedVideoFile.resolution)
101 98
102 if (currentVideoFile) { 99 const transcodeOptions = {
103 // Remove old file and old torrent 100 inputPath: videoInputPath,
104 await video.removeFile(currentVideoFile) 101 outputPath,
105 await video.removeTorrent(currentVideoFile) 102 resolution,
106 // Remove the old video file from the array 103 isPortraitMode,
107 video.VideoFiles = video.VideoFiles.filter(f => f !== currentVideoFile)
108
109 // Update the database
110 currentVideoFile.set('extname', updatedVideoFile.extname)
111 currentVideoFile.set('size', updatedVideoFile.size)
112 currentVideoFile.set('fps', updatedVideoFile.fps)
113 104
114 updatedVideoFile = currentVideoFile 105 hlsPlaylist: {
106 videoFilename: VideoStreamingPlaylistModel.getHlsVideoName(video.uuid, resolution)
107 }
115 } 108 }
116 109
117 const outputPath = video.getVideoFilePath(updatedVideoFile) 110 await transcode(transcodeOptions)
118 await copy(inputFilePath, outputPath)
119 111
120 await video.createTorrentAndSetInfoHash(updatedVideoFile) 112 await updateMasterHLSPlaylist(video)
113 await updateSha256Segments(video)
121 114
122 await updatedVideoFile.save() 115 const playlistUrl = WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsMasterPlaylistStaticPath(video.uuid)
123 116
124 video.VideoFiles.push(updatedVideoFile) 117 await VideoStreamingPlaylistModel.upsert({
118 videoId: video.id,
119 playlistUrl,
120 segmentsSha256Url: WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsSha256SegmentsStaticPath(video.uuid),
121 p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrl, video.VideoFiles),
122 p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION,
123
124 type: VideoStreamingPlaylistType.HLS
125 })
125} 126}
126 127
127export { 128export {
129 generateHlsPlaylist,
128 optimizeVideofile, 130 optimizeVideofile,
129 transcodeOriginalVideofile, 131 transcodeOriginalVideofile
130 importVideoFile
131} 132}
diff --git a/server/middlewares/activitypub.ts b/server/middlewares/activitypub.ts
index 01e5dd24e..b1e5b5236 100644
--- a/server/middlewares/activitypub.ts
+++ b/server/middlewares/activitypub.ts
@@ -1,11 +1,9 @@
1import { eachSeries } from 'async' 1import { NextFunction, Request, Response } from 'express'
2import { NextFunction, Request, RequestHandler, Response } from 'express'
3import { ActivityPubSignature } from '../../shared' 2import { ActivityPubSignature } from '../../shared'
4import { logger } from '../helpers/logger' 3import { logger } from '../helpers/logger'
5import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../helpers/peertube-crypto' 4import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../helpers/peertube-crypto'
6import { ACCEPT_HEADERS, ACTIVITY_PUB, HTTP_SIGNATURE } from '../initializers' 5import { ACCEPT_HEADERS, ACTIVITY_PUB, HTTP_SIGNATURE } from '../initializers/constants'
7import { getOrCreateActorAndServerAndModel } from '../lib/activitypub' 6import { getOrCreateActorAndServerAndModel } from '../lib/activitypub'
8import { ActorModel } from '../models/activitypub/actor'
9import { loadActorUrlOrGetFromWebfinger } from '../helpers/webfinger' 7import { loadActorUrlOrGetFromWebfinger } from '../helpers/webfinger'
10 8
11async function checkSignature (req: Request, res: Response, next: NextFunction) { 9async function checkSignature (req: Request, res: Response, next: NextFunction) {
@@ -13,7 +11,7 @@ async function checkSignature (req: Request, res: Response, next: NextFunction)
13 const httpSignatureChecked = await checkHttpSignature(req, res) 11 const httpSignatureChecked = await checkHttpSignature(req, res)
14 if (httpSignatureChecked !== true) return 12 if (httpSignatureChecked !== true) return
15 13
16 const actor: ActorModel = res.locals.signature.actor 14 const actor = res.locals.signature.actor
17 15
18 // Forwarded activity 16 // Forwarded activity
19 const bodyActor = req.body.actor 17 const bodyActor = req.body.actor
@@ -30,23 +28,16 @@ async function checkSignature (req: Request, res: Response, next: NextFunction)
30 } 28 }
31} 29}
32 30
33function executeIfActivityPub (fun: RequestHandler | RequestHandler[]) { 31function executeIfActivityPub (req: Request, res: Response, next: NextFunction) {
34 return (req: Request, res: Response, next: NextFunction) => { 32 const accepted = req.accepts(ACCEPT_HEADERS)
35 const accepted = req.accepts(ACCEPT_HEADERS) 33 if (accepted === false || ACTIVITY_PUB.POTENTIAL_ACCEPT_HEADERS.indexOf(accepted) === -1) {
36 if (accepted === false || ACTIVITY_PUB.POTENTIAL_ACCEPT_HEADERS.indexOf(accepted) === -1) { 34 // Bypass this route
37 return next() 35 return next('route')
38 } 36 }
39
40 logger.debug('ActivityPub request for %s.', req.url)
41 37
42 if (Array.isArray(fun) === true) { 38 logger.debug('ActivityPub request for %s.', req.url)
43 return eachSeries(fun as RequestHandler[], (f, cb) => {
44 f(req, res, cb)
45 }, next)
46 }
47 39
48 return (fun as RequestHandler)(req, res, next) 40 return next()
49 }
50} 41}
51 42
52// --------------------------------------------------------------------------- 43// ---------------------------------------------------------------------------
@@ -83,6 +74,8 @@ async function checkHttpSignature (req: Request, res: Response) {
83 74
84 const verified = isHTTPSignatureVerified(parsed, actor) 75 const verified = isHTTPSignatureVerified(parsed, actor)
85 if (verified !== true) { 76 if (verified !== true) {
77 logger.warn('Signature from %s is invalid', actorUrl, { parsed })
78
86 res.sendStatus(403) 79 res.sendStatus(403)
87 return false 80 return false
88 } 81 }
diff --git a/server/middlewares/cache.ts b/server/middlewares/cache.ts
index 8ffe75700..ef8611875 100644
--- a/server/middlewares/cache.ts
+++ b/server/middlewares/cache.ts
@@ -1,72 +1,16 @@
1import * as express from 'express'
2import * as AsyncLock from 'async-lock'
3import { parseDuration } from '../helpers/core-utils'
4import { Redis } from '../lib/redis' 1import { Redis } from '../lib/redis'
5import { logger } from '../helpers/logger' 2import * as apicache from 'apicache'
6 3
7const lock = new AsyncLock({ timeout: 5000 }) 4// Ensure Redis is initialized
5Redis.Instance.init()
8 6
9function cacheRoute (lifetimeArg: string | number) { 7const options = {
10 return async function (req: express.Request, res: express.Response, next: express.NextFunction) { 8 redisClient: Redis.Instance.getClient(),
11 const redisKey = Redis.Instance.generateCachedRouteKey(req) 9 appendKey: () => Redis.Instance.getPrefix()
12
13 try {
14 await lock.acquire(redisKey, async (done) => {
15 const cached = await Redis.Instance.getCachedRoute(req)
16
17 // Not cached
18 if (!cached) {
19 logger.debug('No cached results for route %s.', req.originalUrl)
20
21 const sendSave = res.send.bind(res)
22 const redirectSave = res.redirect.bind(res)
23
24 res.send = (body) => {
25 if (res.statusCode >= 200 && res.statusCode < 400) {
26 const contentType = res.get('content-type')
27 const lifetime = parseDuration(lifetimeArg)
28
29 Redis.Instance.setCachedRoute(req, body, lifetime, contentType, res.statusCode)
30 .then(() => done())
31 .catch(err => {
32 logger.error('Cannot cache route.', { err })
33 return done(err)
34 })
35 } else {
36 done()
37 }
38
39 return sendSave(body)
40 }
41
42 res.redirect = url => {
43 done()
44
45 return redirectSave(url)
46 }
47
48 return next()
49 }
50
51 if (cached.contentType) res.set('content-type', cached.contentType)
52
53 if (cached.statusCode) {
54 const statusCode = parseInt(cached.statusCode, 10)
55 if (!isNaN(statusCode)) res.status(statusCode)
56 }
57
58 logger.debug('Use cached result for %s.', req.originalUrl)
59 res.send(cached.body).end()
60
61 return done()
62 })
63 } catch (err) {
64 logger.error('Cannot serve cached route.', { err })
65 return next()
66 }
67 }
68} 10}
69 11
12const cacheRoute = apicache.options(options).middleware
13
70// --------------------------------------------------------------------------- 14// ---------------------------------------------------------------------------
71 15
72export { 16export {
diff --git a/server/middlewares/csp.ts b/server/middlewares/csp.ts
index 8b919af0d..d484b3021 100644
--- a/server/middlewares/csp.ts
+++ b/server/middlewares/csp.ts
@@ -1,5 +1,5 @@
1import * as helmet from 'helmet' 1import * as helmet from 'helmet'
2import { CONFIG } from '../initializers/constants' 2import { CONFIG } from '../initializers/config'
3 3
4const baseDirectives = Object.assign({}, 4const baseDirectives = Object.assign({},
5 { 5 {
@@ -16,24 +16,22 @@ const baseDirectives = Object.assign({},
16 baseUri: ["'self'"], 16 baseUri: ["'self'"],
17 manifestSrc: ["'self'"], 17 manifestSrc: ["'self'"],
18 frameSrc: ["'self'"], // instead of deprecated child-src / self because of test-embed 18 frameSrc: ["'self'"], // instead of deprecated child-src / self because of test-embed
19 workerSrc: ["'self'"] // instead of deprecated child-src 19 workerSrc: ["'self'", 'blob:'] // instead of deprecated child-src
20 }, 20 },
21 CONFIG.SERVICES['CSP-LOGGER'] ? { reportUri: CONFIG.SERVICES['CSP-LOGGER'] } : {}, 21 CONFIG.CSP.REPORT_URI ? { reportUri: CONFIG.CSP.REPORT_URI } : {},
22 CONFIG.WEBSERVER.SCHEME === 'https' ? { upgradeInsecureRequests: true } : {} 22 CONFIG.WEBSERVER.SCHEME === 'https' ? { upgradeInsecureRequests: true } : {}
23) 23)
24 24
25const baseCSP = helmet.contentSecurityPolicy({ 25const baseCSP = helmet.contentSecurityPolicy({
26 directives: baseDirectives, 26 directives: baseDirectives,
27 browserSniff: false, 27 browserSniff: false,
28 reportOnly: true 28 reportOnly: CONFIG.CSP.REPORT_ONLY
29}) 29})
30 30
31const embedCSP = helmet.contentSecurityPolicy({ 31const embedCSP = helmet.contentSecurityPolicy({
32 directives: Object.assign(baseDirectives, { 32 directives: Object.assign({}, baseDirectives, { frameAncestors: ['*'] }),
33 frameAncestors: ['*']
34 }),
35 browserSniff: false, // assumes a modern browser, but allows CDN in front 33 browserSniff: false, // assumes a modern browser, but allows CDN in front
36 reportOnly: true 34 reportOnly: CONFIG.CSP.REPORT_ONLY
37}) 35})
38 36
39// --------------------------------------------------------------------------- 37// ---------------------------------------------------------------------------
diff --git a/server/middlewares/oauth.ts b/server/middlewares/oauth.ts
index 1d193d467..2b4e300e4 100644
--- a/server/middlewares/oauth.ts
+++ b/server/middlewares/oauth.ts
@@ -1,7 +1,7 @@
1import * as express from 'express' 1import * as express from 'express'
2import * as OAuthServer from 'express-oauth-server' 2import * as OAuthServer from 'express-oauth-server'
3import 'express-validator' 3import 'express-validator'
4import { OAUTH_LIFETIME } from '../initializers' 4import { OAUTH_LIFETIME } from '../initializers/constants'
5import { logger } from '../helpers/logger' 5import { logger } from '../helpers/logger'
6import { Socket } from 'socket.io' 6import { Socket } from 'socket.io'
7import { getAccessToken } from '../lib/oauth-model' 7import { getAccessToken } from '../lib/oauth-model'
@@ -35,6 +35,8 @@ function authenticateSocket (socket: Socket, next: (err?: any) => void) {
35 35
36 logger.debug('Checking socket access token %s.', accessToken) 36 logger.debug('Checking socket access token %s.', accessToken)
37 37
38 if (!accessToken) return next(new Error('No access token provided'))
39
38 getAccessToken(accessToken) 40 getAccessToken(accessToken)
39 .then(tokenDB => { 41 .then(tokenDB => {
40 const now = new Date() 42 const now = new Date()
diff --git a/server/middlewares/pagination.ts b/server/middlewares/pagination.ts
index 9b497b19e..83304940f 100644
--- a/server/middlewares/pagination.ts
+++ b/server/middlewares/pagination.ts
@@ -1,7 +1,7 @@
1import 'express-validator' 1import 'express-validator'
2import * as express from 'express' 2import * as express from 'express'
3 3
4import { PAGINATION } from '../initializers' 4import { PAGINATION } from '../initializers/constants'
5 5
6function setDefaultPagination (req: express.Request, res: express.Response, next: express.NextFunction) { 6function setDefaultPagination (req: express.Request, res: express.Response, next: express.NextFunction) {
7 if (!req.query.start) req.query.start = 0 7 if (!req.query.start) req.query.start = 0
diff --git a/server/middlewares/user-right.ts b/server/middlewares/user-right.ts
index 7cea7aa1e..498e3d677 100644
--- a/server/middlewares/user-right.ts
+++ b/server/middlewares/user-right.ts
@@ -2,11 +2,10 @@ import * as express from 'express'
2import 'express-validator' 2import 'express-validator'
3import { UserRight } from '../../shared' 3import { UserRight } from '../../shared'
4import { logger } from '../helpers/logger' 4import { logger } from '../helpers/logger'
5import { UserModel } from '../models/account/user'
6 5
7function ensureUserHasRight (userRight: UserRight) { 6function ensureUserHasRight (userRight: UserRight) {
8 return function (req: express.Request, res: express.Response, next: express.NextFunction) { 7 return function (req: express.Request, res: express.Response, next: express.NextFunction) {
9 const user = res.locals.oauth.token.user as UserModel 8 const user = res.locals.oauth.token.user
10 if (user.hasRight(userRight) === false) { 9 if (user.hasRight(userRight) === false) {
11 const message = `User ${user.username} does not have right ${UserRight[userRight]} to access to ${req.path}.` 10 const message = `User ${user.username} does not have right ${UserRight[userRight]} to access to ${req.path}.`
12 logger.info(message) 11 logger.info(message)
diff --git a/server/middlewares/validators/account.ts b/server/middlewares/validators/account.ts
index b3a51e631..96e120a38 100644
--- a/server/middlewares/validators/account.ts
+++ b/server/middlewares/validators/account.ts
@@ -1,6 +1,6 @@
1import * as express from 'express' 1import * as express from 'express'
2import { param } from 'express-validator/check' 2import { param } from 'express-validator/check'
3import { isAccountNameValid, isAccountNameWithHostExist, isLocalAccountNameExist } from '../../helpers/custom-validators/accounts' 3import { isAccountNameValid, doesAccountNameWithHostExist, doesLocalAccountNameExist } from '../../helpers/custom-validators/accounts'
4import { logger } from '../../helpers/logger' 4import { logger } from '../../helpers/logger'
5import { areValidationErrors } from './utils' 5import { areValidationErrors } from './utils'
6 6
@@ -11,20 +11,20 @@ const localAccountValidator = [
11 logger.debug('Checking localAccountValidator parameters', { parameters: req.params }) 11 logger.debug('Checking localAccountValidator parameters', { parameters: req.params })
12 12
13 if (areValidationErrors(req, res)) return 13 if (areValidationErrors(req, res)) return
14 if (!await isLocalAccountNameExist(req.params.name, res)) return 14 if (!await doesLocalAccountNameExist(req.params.name, res)) return
15 15
16 return next() 16 return next()
17 } 17 }
18] 18]
19 19
20const accountsNameWithHostGetValidator = [ 20const accountNameWithHostGetValidator = [
21 param('accountName').exists().withMessage('Should have an account name with host'), 21 param('accountName').exists().withMessage('Should have an account name with host'),
22 22
23 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 23 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
24 logger.debug('Checking accountsNameWithHostGetValidator parameters', { parameters: req.params }) 24 logger.debug('Checking accountsNameWithHostGetValidator parameters', { parameters: req.params })
25 25
26 if (areValidationErrors(req, res)) return 26 if (areValidationErrors(req, res)) return
27 if (!await isAccountNameWithHostExist(req.params.accountName, res)) return 27 if (!await doesAccountNameWithHostExist(req.params.accountName, res)) return
28 28
29 return next() 29 return next()
30 } 30 }
@@ -34,5 +34,5 @@ const accountsNameWithHostGetValidator = [
34 34
35export { 35export {
36 localAccountValidator, 36 localAccountValidator,
37 accountsNameWithHostGetValidator 37 accountNameWithHostGetValidator
38} 38}
diff --git a/server/middlewares/validators/activitypub/activity.ts b/server/middlewares/validators/activitypub/activity.ts
index 3f9057c0c..7582f65e7 100644
--- a/server/middlewares/validators/activitypub/activity.ts
+++ b/server/middlewares/validators/activitypub/activity.ts
@@ -2,7 +2,6 @@ import * as express from 'express'
2import { isRootActivityValid } from '../../../helpers/custom-validators/activitypub/activity' 2import { isRootActivityValid } from '../../../helpers/custom-validators/activitypub/activity'
3import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
4import { getServerActor } from '../../../helpers/utils' 4import { getServerActor } from '../../../helpers/utils'
5import { ActorModel } from '../../../models/activitypub/actor'
6 5
7async function activityPubValidator (req: express.Request, res: express.Response, next: express.NextFunction) { 6async function activityPubValidator (req: express.Request, res: express.Response, next: express.NextFunction) {
8 logger.debug('Checking activity pub parameters') 7 logger.debug('Checking activity pub parameters')
@@ -13,7 +12,7 @@ async function activityPubValidator (req: express.Request, res: express.Response
13 } 12 }
14 13
15 const serverActor = await getServerActor() 14 const serverActor = await getServerActor()
16 const remoteActor = res.locals.signature.actor as ActorModel 15 const remoteActor = res.locals.signature.actor
17 if (serverActor.id === remoteActor.id) { 16 if (serverActor.id === remoteActor.id) {
18 logger.error('Receiving request in INBOX by ourselves!', req.body) 17 logger.error('Receiving request in INBOX by ourselves!', req.body)
19 return res.status(409).end() 18 return res.status(409).end()
diff --git a/server/middlewares/validators/avatar.ts b/server/middlewares/validators/avatar.ts
index ddc14f531..bab3ed118 100644
--- a/server/middlewares/validators/avatar.ts
+++ b/server/middlewares/validators/avatar.ts
@@ -2,7 +2,7 @@ import * as express from 'express'
2import { body } from 'express-validator/check' 2import { body } from 'express-validator/check'
3import { isAvatarFile } from '../../helpers/custom-validators/users' 3import { isAvatarFile } from '../../helpers/custom-validators/users'
4import { areValidationErrors } from './utils' 4import { areValidationErrors } from './utils'
5import { CONSTRAINTS_FIELDS } from '../../initializers' 5import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
6import { logger } from '../../helpers/logger' 6import { logger } from '../../helpers/logger'
7import { cleanUpReqFiles } from '../../helpers/express-utils' 7import { cleanUpReqFiles } from '../../helpers/express-utils'
8 8
diff --git a/server/middlewares/validators/blocklist.ts b/server/middlewares/validators/blocklist.ts
index 109276c63..7c494de78 100644
--- a/server/middlewares/validators/blocklist.ts
+++ b/server/middlewares/validators/blocklist.ts
@@ -2,14 +2,13 @@ import { body, param } from 'express-validator/check'
2import * as express from 'express' 2import * as express from 'express'
3import { logger } from '../../helpers/logger' 3import { logger } from '../../helpers/logger'
4import { areValidationErrors } from './utils' 4import { areValidationErrors } from './utils'
5import { isAccountNameWithHostExist } from '../../helpers/custom-validators/accounts' 5import { doesAccountNameWithHostExist } from '../../helpers/custom-validators/accounts'
6import { UserModel } from '../../models/account/user'
7import { AccountBlocklistModel } from '../../models/account/account-blocklist' 6import { AccountBlocklistModel } from '../../models/account/account-blocklist'
8import { isHostValid } from '../../helpers/custom-validators/servers' 7import { isHostValid } from '../../helpers/custom-validators/servers'
9import { ServerBlocklistModel } from '../../models/server/server-blocklist' 8import { ServerBlocklistModel } from '../../models/server/server-blocklist'
10import { ServerModel } from '../../models/server/server' 9import { ServerModel } from '../../models/server/server'
11import { CONFIG } from '../../initializers'
12import { getServerActor } from '../../helpers/utils' 10import { getServerActor } from '../../helpers/utils'
11import { WEBSERVER } from '../../initializers/constants'
13 12
14const blockAccountValidator = [ 13const blockAccountValidator = [
15 body('accountName').exists().withMessage('Should have an account name with host'), 14 body('accountName').exists().withMessage('Should have an account name with host'),
@@ -18,9 +17,9 @@ const blockAccountValidator = [
18 logger.debug('Checking blockAccountByAccountValidator parameters', { parameters: req.body }) 17 logger.debug('Checking blockAccountByAccountValidator parameters', { parameters: req.body })
19 18
20 if (areValidationErrors(req, res)) return 19 if (areValidationErrors(req, res)) return
21 if (!await isAccountNameWithHostExist(req.body.accountName, res)) return 20 if (!await doesAccountNameWithHostExist(req.body.accountName, res)) return
22 21
23 const user = res.locals.oauth.token.User as UserModel 22 const user = res.locals.oauth.token.User
24 const accountToBlock = res.locals.account 23 const accountToBlock = res.locals.account
25 24
26 if (user.Account.id === accountToBlock.id) { 25 if (user.Account.id === accountToBlock.id) {
@@ -42,11 +41,11 @@ const unblockAccountByAccountValidator = [
42 logger.debug('Checking unblockAccountByAccountValidator parameters', { parameters: req.params }) 41 logger.debug('Checking unblockAccountByAccountValidator parameters', { parameters: req.params })
43 42
44 if (areValidationErrors(req, res)) return 43 if (areValidationErrors(req, res)) return
45 if (!await isAccountNameWithHostExist(req.params.accountName, res)) return 44 if (!await doesAccountNameWithHostExist(req.params.accountName, res)) return
46 45
47 const user = res.locals.oauth.token.User as UserModel 46 const user = res.locals.oauth.token.User
48 const targetAccount = res.locals.account 47 const targetAccount = res.locals.account
49 if (!await isUnblockAccountExists(user.Account.id, targetAccount.id, res)) return 48 if (!await doesUnblockAccountExist(user.Account.id, targetAccount.id, res)) return
50 49
51 return next() 50 return next()
52 } 51 }
@@ -59,11 +58,11 @@ const unblockAccountByServerValidator = [
59 logger.debug('Checking unblockAccountByServerValidator parameters', { parameters: req.params }) 58 logger.debug('Checking unblockAccountByServerValidator parameters', { parameters: req.params })
60 59
61 if (areValidationErrors(req, res)) return 60 if (areValidationErrors(req, res)) return
62 if (!await isAccountNameWithHostExist(req.params.accountName, res)) return 61 if (!await doesAccountNameWithHostExist(req.params.accountName, res)) return
63 62
64 const serverActor = await getServerActor() 63 const serverActor = await getServerActor()
65 const targetAccount = res.locals.account 64 const targetAccount = res.locals.account
66 if (!await isUnblockAccountExists(serverActor.Account.id, targetAccount.id, res)) return 65 if (!await doesUnblockAccountExist(serverActor.Account.id, targetAccount.id, res)) return
67 66
68 return next() 67 return next()
69 } 68 }
@@ -79,7 +78,7 @@ const blockServerValidator = [
79 78
80 const host: string = req.body.host 79 const host: string = req.body.host
81 80
82 if (host === CONFIG.WEBSERVER.HOST) { 81 if (host === WEBSERVER.HOST) {
83 return res.status(409) 82 return res.status(409)
84 .send({ error: 'You cannot block your own server.' }) 83 .send({ error: 'You cannot block your own server.' })
85 .end() 84 .end()
@@ -106,8 +105,8 @@ const unblockServerByAccountValidator = [
106 105
107 if (areValidationErrors(req, res)) return 106 if (areValidationErrors(req, res)) return
108 107
109 const user = res.locals.oauth.token.User as UserModel 108 const user = res.locals.oauth.token.User
110 if (!await isUnblockServerExists(user.Account.id, req.params.host, res)) return 109 if (!await doesUnblockServerExist(user.Account.id, req.params.host, res)) return
111 110
112 return next() 111 return next()
113 } 112 }
@@ -122,7 +121,7 @@ const unblockServerByServerValidator = [
122 if (areValidationErrors(req, res)) return 121 if (areValidationErrors(req, res)) return
123 122
124 const serverActor = await getServerActor() 123 const serverActor = await getServerActor()
125 if (!await isUnblockServerExists(serverActor.Account.id, req.params.host, res)) return 124 if (!await doesUnblockServerExist(serverActor.Account.id, req.params.host, res)) return
126 125
127 return next() 126 return next()
128 } 127 }
@@ -141,7 +140,7 @@ export {
141 140
142// --------------------------------------------------------------------------- 141// ---------------------------------------------------------------------------
143 142
144async function isUnblockAccountExists (accountId: number, targetAccountId: number, res: express.Response) { 143async function doesUnblockAccountExist (accountId: number, targetAccountId: number, res: express.Response) {
145 const accountBlock = await AccountBlocklistModel.loadByAccountAndTarget(accountId, targetAccountId) 144 const accountBlock = await AccountBlocklistModel.loadByAccountAndTarget(accountId, targetAccountId)
146 if (!accountBlock) { 145 if (!accountBlock) {
147 res.status(404) 146 res.status(404)
@@ -156,7 +155,7 @@ async function isUnblockAccountExists (accountId: number, targetAccountId: numbe
156 return true 155 return true
157} 156}
158 157
159async function isUnblockServerExists (accountId: number, host: string, res: express.Response) { 158async function doesUnblockServerExist (accountId: number, host: string, res: express.Response) {
160 const serverBlock = await ServerBlocklistModel.loadByAccountAndHost(accountId, host) 159 const serverBlock = await ServerBlocklistModel.loadByAccountAndHost(accountId, host)
161 if (!serverBlock) { 160 if (!serverBlock) {
162 res.status(404) 161 res.status(404)
diff --git a/server/middlewares/validators/config.ts b/server/middlewares/validators/config.ts
index 90108fa82..d015fa6fe 100644
--- a/server/middlewares/validators/config.ts
+++ b/server/middlewares/validators/config.ts
@@ -2,6 +2,8 @@ import * as express from 'express'
2import { body } from 'express-validator/check' 2import { body } from 'express-validator/check'
3import { isUserNSFWPolicyValid, isUserVideoQuotaValid, isUserVideoQuotaDailyValid } 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 { CustomConfig } from '../../../shared/models/server/custom-config.model'
6import { Emailer } from '../../lib/emailer'
5import { areValidationErrors } from './utils' 7import { areValidationErrors } from './utils'
6 8
7const customConfigUpdateValidator = [ 9const customConfigUpdateValidator = [
@@ -42,15 +44,34 @@ const customConfigUpdateValidator = [
42 body('import.videos.http.enabled').isBoolean().withMessage('Should have a valid import video http enabled boolean'), 44 body('import.videos.http.enabled').isBoolean().withMessage('Should have a valid import video http enabled boolean'),
43 body('import.videos.torrent.enabled').isBoolean().withMessage('Should have a valid import video torrent enabled boolean'), 45 body('import.videos.torrent.enabled').isBoolean().withMessage('Should have a valid import video torrent enabled boolean'),
44 46
47 body('followers.instance.enabled').isBoolean().withMessage('Should have a valid followers of instance boolean'),
48 body('followers.instance.manualApproval').isBoolean().withMessage('Should have a valid manual approval boolean'),
49
45 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 50 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
46 logger.debug('Checking customConfigUpdateValidator parameters', { parameters: req.body }) 51 logger.debug('Checking customConfigUpdateValidator parameters', { parameters: req.body })
47 52
48 if (areValidationErrors(req, res)) return 53 if (areValidationErrors(req, res)) return
54 if (!checkInvalidConfigIfEmailDisabled(req.body as CustomConfig, res)) return
49 55
50 return next() 56 return next()
51 } 57 }
52] 58]
53 59
60// ---------------------------------------------------------------------------
61
54export { 62export {
55 customConfigUpdateValidator 63 customConfigUpdateValidator
56} 64}
65
66function checkInvalidConfigIfEmailDisabled (customConfig: CustomConfig, res: express.Response) {
67 if (Emailer.isEnabled()) return true
68
69 if (customConfig.signup.requiresEmailVerification === true) {
70 res.status(400)
71 .send({ error: 'Emailer is disabled but you require signup email verification.' })
72 .end()
73 return false
74 }
75
76 return true
77}
diff --git a/server/middlewares/validators/feeds.ts b/server/middlewares/validators/feeds.ts
index 969ce2526..e4f5c98fe 100644
--- a/server/middlewares/validators/feeds.ts
+++ b/server/middlewares/validators/feeds.ts
@@ -1,12 +1,12 @@
1import * as express from 'express' 1import * as express from 'express'
2import { param, query } from 'express-validator/check' 2import { param, query } from 'express-validator/check'
3import { isAccountIdExist, isAccountNameValid, isAccountNameWithHostExist } from '../../helpers/custom-validators/accounts' 3import { doesAccountIdExist, isAccountNameValid, doesAccountNameWithHostExist } from '../../helpers/custom-validators/accounts'
4import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc' 4import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc'
5import { logger } from '../../helpers/logger' 5import { logger } from '../../helpers/logger'
6import { areValidationErrors } from './utils' 6import { areValidationErrors } from './utils'
7import { isValidRSSFeed } from '../../helpers/custom-validators/feeds' 7import { isValidRSSFeed } from '../../helpers/custom-validators/feeds'
8import { isVideoChannelIdExist, isVideoChannelNameWithHostExist } from '../../helpers/custom-validators/video-channels' 8import { doesVideoChannelIdExist, doesVideoChannelNameWithHostExist } from '../../helpers/custom-validators/video-channels'
9import { isVideoExist } from '../../helpers/custom-validators/videos' 9import { doesVideoExist } from '../../helpers/custom-validators/videos'
10import { isActorPreferredUsernameValid } from '../../helpers/custom-validators/activitypub/actor' 10import { isActorPreferredUsernameValid } from '../../helpers/custom-validators/activitypub/actor'
11 11
12const videoFeedsValidator = [ 12const videoFeedsValidator = [
@@ -22,10 +22,10 @@ const videoFeedsValidator = [
22 22
23 if (areValidationErrors(req, res)) return 23 if (areValidationErrors(req, res)) return
24 24
25 if (req.query.accountId && !await isAccountIdExist(req.query.accountId, res)) return 25 if (req.query.accountId && !await doesAccountIdExist(req.query.accountId, res)) return
26 if (req.query.videoChannelId && !await isVideoChannelIdExist(req.query.videoChannelId, res)) return 26 if (req.query.videoChannelId && !await doesVideoChannelIdExist(req.query.videoChannelId, res)) return
27 if (req.query.accountName && !await isAccountNameWithHostExist(req.query.accountName, res)) return 27 if (req.query.accountName && !await doesAccountNameWithHostExist(req.query.accountName, res)) return
28 if (req.query.videoChannelName && !await isVideoChannelNameWithHostExist(req.query.videoChannelName, res)) return 28 if (req.query.videoChannelName && !await doesVideoChannelNameWithHostExist(req.query.videoChannelName, res)) return
29 29
30 return next() 30 return next()
31 } 31 }
@@ -41,7 +41,7 @@ const videoCommentsFeedsValidator = [
41 41
42 if (areValidationErrors(req, res)) return 42 if (areValidationErrors(req, res)) return
43 43
44 if (req.query.videoId && !await isVideoExist(req.query.videoId, res)) return 44 if (req.query.videoId && !await doesVideoExist(req.query.videoId, res)) return
45 45
46 return next() 46 return next()
47 } 47 }
diff --git a/server/middlewares/validators/follows.ts b/server/middlewares/validators/follows.ts
index 73fa28be9..2e5a02307 100644
--- a/server/middlewares/validators/follows.ts
+++ b/server/middlewares/validators/follows.ts
@@ -4,16 +4,19 @@ import { isTestInstance } from '../../helpers/core-utils'
4import { isEachUniqueHostValid, isHostValid } from '../../helpers/custom-validators/servers' 4import { isEachUniqueHostValid, isHostValid } from '../../helpers/custom-validators/servers'
5import { logger } from '../../helpers/logger' 5import { logger } from '../../helpers/logger'
6import { getServerActor } from '../../helpers/utils' 6import { getServerActor } from '../../helpers/utils'
7import { CONFIG, SERVER_ACTOR_NAME } from '../../initializers' 7import { SERVER_ACTOR_NAME, WEBSERVER } from '../../initializers/constants'
8import { ActorFollowModel } from '../../models/activitypub/actor-follow' 8import { ActorFollowModel } from '../../models/activitypub/actor-follow'
9import { areValidationErrors } from './utils' 9import { areValidationErrors } from './utils'
10import { ActorModel } from '../../models/activitypub/actor'
11import { loadActorUrlOrGetFromWebfinger } from '../../helpers/webfinger'
12import { isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor'
10 13
11const followValidator = [ 14const followValidator = [
12 body('hosts').custom(isEachUniqueHostValid).withMessage('Should have an array of unique hosts'), 15 body('hosts').custom(isEachUniqueHostValid).withMessage('Should have an array of unique hosts'),
13 16
14 (req: express.Request, res: express.Response, next: express.NextFunction) => { 17 (req: express.Request, res: express.Response, next: express.NextFunction) => {
15 // Force https if the administrator wants to make friends 18 // Force https if the administrator wants to make friends
16 if (isTestInstance() === false && CONFIG.WEBSERVER.SCHEME === 'http') { 19 if (isTestInstance() === false && WEBSERVER.SCHEME === 'http') {
17 return res.status(500) 20 return res.status(500)
18 .json({ 21 .json({
19 error: 'Cannot follow on a non HTTPS web server.' 22 error: 'Cannot follow on a non HTTPS web server.'
@@ -33,7 +36,7 @@ const removeFollowingValidator = [
33 param('host').custom(isHostValid).withMessage('Should have a valid host'), 36 param('host').custom(isHostValid).withMessage('Should have a valid host'),
34 37
35 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 38 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
36 logger.debug('Checking unfollow parameters', { parameters: req.params }) 39 logger.debug('Checking unfollowing parameters', { parameters: req.params })
37 40
38 if (areValidationErrors(req, res)) return 41 if (areValidationErrors(req, res)) return
39 42
@@ -44,7 +47,7 @@ const removeFollowingValidator = [
44 return res 47 return res
45 .status(404) 48 .status(404)
46 .json({ 49 .json({
47 error: `Follower ${req.params.host} not found.` 50 error: `Following ${req.params.host} not found.`
48 }) 51 })
49 .end() 52 .end()
50 } 53 }
@@ -54,9 +57,57 @@ const removeFollowingValidator = [
54 } 57 }
55] 58]
56 59
60const getFollowerValidator = [
61 param('nameWithHost').custom(isValidActorHandle).withMessage('Should have a valid nameWithHost'),
62
63 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
64 logger.debug('Checking get follower parameters', { parameters: req.params })
65
66 if (areValidationErrors(req, res)) return
67
68 let follow: ActorFollowModel
69 try {
70 const actorUrl = await loadActorUrlOrGetFromWebfinger(req.params.nameWithHost)
71 const actor = await ActorModel.loadByUrl(actorUrl)
72
73 const serverActor = await getServerActor()
74 follow = await ActorFollowModel.loadByActorAndTarget(actor.id, serverActor.id)
75 } catch (err) {
76 logger.warn('Cannot get actor from handle.', { handle: req.params.nameWithHost, err })
77 }
78
79 if (!follow) {
80 return res
81 .status(404)
82 .json({
83 error: `Follower ${req.params.nameWithHost} not found.`
84 })
85 .end()
86 }
87
88 res.locals.follow = follow
89 return next()
90 }
91]
92
93const acceptOrRejectFollowerValidator = [
94 (req: express.Request, res: express.Response, next: express.NextFunction) => {
95 logger.debug('Checking accept/reject follower parameters', { parameters: req.params })
96
97 const follow = res.locals.follow
98 if (follow.state !== 'pending') {
99 return res.status(400).json({ error: 'Follow is not in pending state.' }).end()
100 }
101
102 return next()
103 }
104]
105
57// --------------------------------------------------------------------------- 106// ---------------------------------------------------------------------------
58 107
59export { 108export {
60 followValidator, 109 followValidator,
61 removeFollowingValidator 110 removeFollowingValidator,
111 getFollowerValidator,
112 acceptOrRejectFollowerValidator
62} 113}
diff --git a/server/middlewares/validators/logs.ts b/server/middlewares/validators/logs.ts
new file mode 100644
index 000000000..7380c6edd
--- /dev/null
+++ b/server/middlewares/validators/logs.ts
@@ -0,0 +1,31 @@
1import * as express from 'express'
2import { logger } from '../../helpers/logger'
3import { areValidationErrors } from './utils'
4import { isDateValid } from '../../helpers/custom-validators/misc'
5import { query } from 'express-validator/check'
6import { isValidLogLevel } from '../../helpers/custom-validators/logs'
7
8const getLogsValidator = [
9 query('startDate')
10 .custom(isDateValid).withMessage('Should have a valid start date'),
11 query('level')
12 .optional()
13 .custom(isValidLogLevel).withMessage('Should have a valid level'),
14 query('endDate')
15 .optional()
16 .custom(isDateValid).withMessage('Should have a valid end date'),
17
18 (req: express.Request, res: express.Response, next: express.NextFunction) => {
19 logger.debug('Checking getLogsValidator parameters.', { parameters: req.query })
20
21 if (areValidationErrors(req, res)) return
22
23 return next()
24 }
25]
26
27// ---------------------------------------------------------------------------
28
29export {
30 getLogsValidator
31}
diff --git a/server/middlewares/validators/oembed.ts b/server/middlewares/validators/oembed.ts
index cd9b27b16..0bb908d0b 100644
--- a/server/middlewares/validators/oembed.ts
+++ b/server/middlewares/validators/oembed.ts
@@ -3,12 +3,12 @@ import { query } from 'express-validator/check'
3import { join } from 'path' 3import { join } from 'path'
4import { isTestInstance } from '../../helpers/core-utils' 4import { isTestInstance } from '../../helpers/core-utils'
5import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc' 5import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc'
6import { isVideoExist } from '../../helpers/custom-validators/videos' 6import { doesVideoExist } from '../../helpers/custom-validators/videos'
7import { logger } from '../../helpers/logger' 7import { logger } from '../../helpers/logger'
8import { CONFIG } from '../../initializers'
9import { areValidationErrors } from './utils' 8import { areValidationErrors } from './utils'
9import { WEBSERVER } from '../../initializers/constants'
10 10
11const urlShouldStartWith = CONFIG.WEBSERVER.SCHEME + '://' + join(CONFIG.WEBSERVER.HOST, 'videos', 'watch') + '/' 11const urlShouldStartWith = WEBSERVER.SCHEME + '://' + join(WEBSERVER.HOST, 'videos', 'watch') + '/'
12const videoWatchRegex = new RegExp('([^/]+)$') 12const videoWatchRegex = new RegExp('([^/]+)$')
13const isURLOptions = { 13const isURLOptions = {
14 require_host: true, 14 require_host: true,
@@ -52,7 +52,7 @@ const oembedValidator = [
52 .end() 52 .end()
53 } 53 }
54 54
55 if (!await isVideoExist(videoId, res)) return 55 if (!await doesVideoExist(videoId, res)) return
56 56
57 return next() 57 return next()
58 } 58 }
diff --git a/server/middlewares/validators/redundancy.ts b/server/middlewares/validators/redundancy.ts
index c72ab78b2..76cf89c40 100644
--- a/server/middlewares/validators/redundancy.ts
+++ b/server/middlewares/validators/redundancy.ts
@@ -1,19 +1,15 @@
1import * as express from 'express' 1import * as express from 'express'
2import 'express-validator' 2import 'express-validator'
3import { param, body } from 'express-validator/check' 3import { body, param } from 'express-validator/check'
4import { exists, isBooleanValid, isIdOrUUIDValid, toIntOrNull } from '../../helpers/custom-validators/misc' 4import { exists, isBooleanValid, isIdOrUUIDValid, toIntOrNull } from '../../helpers/custom-validators/misc'
5import { isVideoExist } from '../../helpers/custom-validators/videos' 5import { doesVideoExist } from '../../helpers/custom-validators/videos'
6import { logger } from '../../helpers/logger' 6import { logger } from '../../helpers/logger'
7import { areValidationErrors } from './utils' 7import { areValidationErrors } from './utils'
8import { VideoModel } from '../../models/video/video'
9import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' 8import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy'
10import { isHostValid } from '../../helpers/custom-validators/servers' 9import { isHostValid } from '../../helpers/custom-validators/servers'
11import { getServerActor } from '../../helpers/utils'
12import { ActorFollowModel } from '../../models/activitypub/actor-follow'
13import { SERVER_ACTOR_NAME } from '../../initializers'
14import { ServerModel } from '../../models/server/server' 10import { ServerModel } from '../../models/server/server'
15 11
16const videoRedundancyGetValidator = [ 12const videoFileRedundancyGetValidator = [
17 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), 13 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'),
18 param('resolution') 14 param('resolution')
19 .customSanitizer(toIntOrNull) 15 .customSanitizer(toIntOrNull)
@@ -24,12 +20,12 @@ const videoRedundancyGetValidator = [
24 .custom(exists).withMessage('Should have a valid fps'), 20 .custom(exists).withMessage('Should have a valid fps'),
25 21
26 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 22 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
27 logger.debug('Checking videoRedundancyGetValidator parameters', { parameters: req.params }) 23 logger.debug('Checking videoFileRedundancyGetValidator parameters', { parameters: req.params })
28 24
29 if (areValidationErrors(req, res)) return 25 if (areValidationErrors(req, res)) return
30 if (!await isVideoExist(req.params.videoId, res)) return 26 if (!await doesVideoExist(req.params.videoId, res)) return
31 27
32 const video: VideoModel = res.locals.video 28 const video = res.locals.video
33 const videoFile = video.VideoFiles.find(f => { 29 const videoFile = video.VideoFiles.find(f => {
34 return f.resolution === req.params.resolution && (!req.params.fps || f.fps === req.params.fps) 30 return f.resolution === req.params.resolution && (!req.params.fps || f.fps === req.params.fps)
35 }) 31 })
@@ -38,7 +34,31 @@ const videoRedundancyGetValidator = [
38 res.locals.videoFile = videoFile 34 res.locals.videoFile = videoFile
39 35
40 const videoRedundancy = await VideoRedundancyModel.loadLocalByFileId(videoFile.id) 36 const videoRedundancy = await VideoRedundancyModel.loadLocalByFileId(videoFile.id)
41 if (!videoRedundancy)return res.status(404).json({ error: 'Video redundancy not found.' }) 37 if (!videoRedundancy) return res.status(404).json({ error: 'Video redundancy not found.' })
38 res.locals.videoRedundancy = videoRedundancy
39
40 return next()
41 }
42]
43
44const videoPlaylistRedundancyGetValidator = [
45 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'),
46 param('streamingPlaylistType').custom(exists).withMessage('Should have a valid streaming playlist type'),
47
48 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
49 logger.debug('Checking videoPlaylistRedundancyGetValidator parameters', { parameters: req.params })
50
51 if (areValidationErrors(req, res)) return
52 if (!await doesVideoExist(req.params.videoId, res)) return
53
54 const video = res.locals.video
55 const videoStreamingPlaylist = video.VideoStreamingPlaylists.find(p => p === req.params.streamingPlaylistType)
56
57 if (!videoStreamingPlaylist) return res.status(404).json({ error: 'Video playlist not found.' })
58 res.locals.videoStreamingPlaylist = videoStreamingPlaylist
59
60 const videoRedundancy = await VideoRedundancyModel.loadLocalByStreamingPlaylistId(videoStreamingPlaylist.id)
61 if (!videoRedundancy) return res.status(404).json({ error: 'Video redundancy not found.' })
42 res.locals.videoRedundancy = videoRedundancy 62 res.locals.videoRedundancy = videoRedundancy
43 63
44 return next() 64 return next()
@@ -75,6 +95,7 @@ const updateServerRedundancyValidator = [
75// --------------------------------------------------------------------------- 95// ---------------------------------------------------------------------------
76 96
77export { 97export {
78 videoRedundancyGetValidator, 98 videoFileRedundancyGetValidator,
99 videoPlaylistRedundancyGetValidator,
79 updateServerRedundancyValidator 100 updateServerRedundancyValidator
80} 101}
diff --git a/server/middlewares/validators/search.ts b/server/middlewares/validators/search.ts
index 6a95d6095..7816d229c 100644
--- a/server/middlewares/validators/search.ts
+++ b/server/middlewares/validators/search.ts
@@ -10,6 +10,9 @@ const videosSearchValidator = [
10 query('startDate').optional().custom(isDateValid).withMessage('Should have a valid start date'), 10 query('startDate').optional().custom(isDateValid).withMessage('Should have a valid start date'),
11 query('endDate').optional().custom(isDateValid).withMessage('Should have a valid end date'), 11 query('endDate').optional().custom(isDateValid).withMessage('Should have a valid end date'),
12 12
13 query('originallyPublishedStartDate').optional().custom(isDateValid).withMessage('Should have a valid published start date'),
14 query('originallyPublishedEndDate').optional().custom(isDateValid).withMessage('Should have a valid published end date'),
15
13 query('durationMin').optional().isInt().withMessage('Should have a valid min duration'), 16 query('durationMin').optional().isInt().withMessage('Should have a valid min duration'),
14 query('durationMax').optional().isInt().withMessage('Should have a valid max duration'), 17 query('durationMax').optional().isInt().withMessage('Should have a valid max duration'),
15 18
diff --git a/server/middlewares/validators/server.ts b/server/middlewares/validators/server.ts
index d85afc2ff..6eff8e9ee 100644
--- a/server/middlewares/validators/server.ts
+++ b/server/middlewares/validators/server.ts
@@ -7,7 +7,7 @@ import { body } from 'express-validator/check'
7import { isUserDisplayNameValid } from '../../helpers/custom-validators/users' 7import { isUserDisplayNameValid } from '../../helpers/custom-validators/users'
8import { Emailer } from '../../lib/emailer' 8import { Emailer } from '../../lib/emailer'
9import { Redis } from '../../lib/redis' 9import { Redis } from '../../lib/redis'
10import { CONFIG } from '../../initializers/constants' 10import { CONFIG } from '../../initializers/config'
11 11
12const serverGetValidator = [ 12const serverGetValidator = [
13 body('host').custom(isHostValid).withMessage('Should have a valid host'), 13 body('host').custom(isHostValid).withMessage('Should have a valid host'),
@@ -57,7 +57,7 @@ const contactAdministratorValidator = [
57 .end() 57 .end()
58 } 58 }
59 59
60 if (await Redis.Instance.isContactFormIpExists(req.ip)) { 60 if (await Redis.Instance.doesContactFormIpExist(req.ip)) {
61 logger.info('Refusing a contact form by %s: already sent one recently.', req.ip) 61 logger.info('Refusing a contact form by %s: already sent one recently.', req.ip)
62 62
63 return res 63 return res
diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts
index 5ceda845f..b497798d1 100644
--- a/server/middlewares/validators/sort.ts
+++ b/server/middlewares/validators/sort.ts
@@ -1,4 +1,4 @@
1import { SORTABLE_COLUMNS } from '../../initializers' 1import { SORTABLE_COLUMNS } from '../../initializers/constants'
2import { checkSort, createSortableColumns } from './utils' 2import { checkSort, createSortableColumns } from './utils'
3 3
4// Initialize constants here for better performances 4// Initialize constants here for better performances
@@ -11,6 +11,7 @@ const SORTABLE_VIDEOS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VI
11const SORTABLE_VIDEO_CHANNELS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS_SEARCH) 11const SORTABLE_VIDEO_CHANNELS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS_SEARCH)
12const SORTABLE_VIDEO_IMPORTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_IMPORTS) 12const SORTABLE_VIDEO_IMPORTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_IMPORTS)
13const SORTABLE_VIDEO_COMMENT_THREADS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_COMMENT_THREADS) 13const SORTABLE_VIDEO_COMMENT_THREADS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_COMMENT_THREADS)
14const SORTABLE_VIDEO_RATES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_RATES)
14const SORTABLE_BLACKLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.BLACKLISTS) 15const SORTABLE_BLACKLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.BLACKLISTS)
15const SORTABLE_VIDEO_CHANNELS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS) 16const SORTABLE_VIDEO_CHANNELS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS)
16const SORTABLE_FOLLOWERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWERS) 17const SORTABLE_FOLLOWERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWERS)
@@ -19,6 +20,7 @@ const SORTABLE_USER_SUBSCRIPTIONS_COLUMNS = createSortableColumns(SORTABLE_COLUM
19const SORTABLE_ACCOUNTS_BLOCKLIST_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.ACCOUNTS_BLOCKLIST) 20const SORTABLE_ACCOUNTS_BLOCKLIST_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.ACCOUNTS_BLOCKLIST)
20const SORTABLE_SERVERS_BLOCKLIST_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.SERVERS_BLOCKLIST) 21const SORTABLE_SERVERS_BLOCKLIST_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.SERVERS_BLOCKLIST)
21const SORTABLE_USER_NOTIFICATIONS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USER_NOTIFICATIONS) 22const SORTABLE_USER_NOTIFICATIONS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USER_NOTIFICATIONS)
23const SORTABLE_VIDEO_PLAYLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_PLAYLISTS)
22 24
23const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) 25const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS)
24const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS) 26const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS)
@@ -29,6 +31,7 @@ const videoImportsSortValidator = checkSort(SORTABLE_VIDEO_IMPORTS_COLUMNS)
29const videosSearchSortValidator = checkSort(SORTABLE_VIDEOS_SEARCH_COLUMNS) 31const videosSearchSortValidator = checkSort(SORTABLE_VIDEOS_SEARCH_COLUMNS)
30const videoChannelsSearchSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_SEARCH_COLUMNS) 32const videoChannelsSearchSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_SEARCH_COLUMNS)
31const videoCommentThreadsSortValidator = checkSort(SORTABLE_VIDEO_COMMENT_THREADS_COLUMNS) 33const videoCommentThreadsSortValidator = checkSort(SORTABLE_VIDEO_COMMENT_THREADS_COLUMNS)
34const videoRatesSortValidator = checkSort(SORTABLE_VIDEO_RATES_COLUMNS)
32const blacklistSortValidator = checkSort(SORTABLE_BLACKLISTS_COLUMNS) 35const blacklistSortValidator = checkSort(SORTABLE_BLACKLISTS_COLUMNS)
33const videoChannelsSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_COLUMNS) 36const videoChannelsSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_COLUMNS)
34const followersSortValidator = checkSort(SORTABLE_FOLLOWERS_COLUMNS) 37const followersSortValidator = checkSort(SORTABLE_FOLLOWERS_COLUMNS)
@@ -37,6 +40,7 @@ const userSubscriptionsSortValidator = checkSort(SORTABLE_USER_SUBSCRIPTIONS_COL
37const accountsBlocklistSortValidator = checkSort(SORTABLE_ACCOUNTS_BLOCKLIST_COLUMNS) 40const accountsBlocklistSortValidator = checkSort(SORTABLE_ACCOUNTS_BLOCKLIST_COLUMNS)
38const serversBlocklistSortValidator = checkSort(SORTABLE_SERVERS_BLOCKLIST_COLUMNS) 41const serversBlocklistSortValidator = checkSort(SORTABLE_SERVERS_BLOCKLIST_COLUMNS)
39const userNotificationsSortValidator = checkSort(SORTABLE_USER_NOTIFICATIONS_COLUMNS) 42const userNotificationsSortValidator = checkSort(SORTABLE_USER_NOTIFICATIONS_COLUMNS)
43const videoPlaylistsSortValidator = checkSort(SORTABLE_VIDEO_PLAYLISTS_COLUMNS)
40 44
41// --------------------------------------------------------------------------- 45// ---------------------------------------------------------------------------
42 46
@@ -53,9 +57,11 @@ export {
53 followingSortValidator, 57 followingSortValidator,
54 jobsSortValidator, 58 jobsSortValidator,
55 videoCommentThreadsSortValidator, 59 videoCommentThreadsSortValidator,
60 videoRatesSortValidator,
56 userSubscriptionsSortValidator, 61 userSubscriptionsSortValidator,
57 videoChannelsSearchSortValidator, 62 videoChannelsSearchSortValidator,
58 accountsBlocklistSortValidator, 63 accountsBlocklistSortValidator,
59 serversBlocklistSortValidator, 64 serversBlocklistSortValidator,
60 userNotificationsSortValidator 65 userNotificationsSortValidator,
66 videoPlaylistsSortValidator
61} 67}
diff --git a/server/middlewares/validators/user-notifications.ts b/server/middlewares/validators/user-notifications.ts
index 46486e081..3ded8d8cf 100644
--- a/server/middlewares/validators/user-notifications.ts
+++ b/server/middlewares/validators/user-notifications.ts
@@ -28,8 +28,22 @@ const updateNotificationSettingsValidator = [
28 .custom(isUserNotificationSettingValid).withMessage('Should have a valid new comment on my video notification setting'), 28 .custom(isUserNotificationSettingValid).withMessage('Should have a valid new comment on my video notification setting'),
29 body('videoAbuseAsModerator') 29 body('videoAbuseAsModerator')
30 .custom(isUserNotificationSettingValid).withMessage('Should have a valid new video abuse as moderator notification setting'), 30 .custom(isUserNotificationSettingValid).withMessage('Should have a valid new video abuse as moderator notification setting'),
31 body('videoAutoBlacklistAsModerator')
32 .custom(isUserNotificationSettingValid).withMessage('Should have a valid video auto blacklist notification setting'),
31 body('blacklistOnMyVideo') 33 body('blacklistOnMyVideo')
32 .custom(isUserNotificationSettingValid).withMessage('Should have a valid new blacklist on my video notification setting'), 34 .custom(isUserNotificationSettingValid).withMessage('Should have a valid new blacklist on my video notification setting'),
35 body('myVideoImportFinished')
36 .custom(isUserNotificationSettingValid).withMessage('Should have a valid video import finished video notification setting'),
37 body('myVideoPublished')
38 .custom(isUserNotificationSettingValid).withMessage('Should have a valid video published notification setting'),
39 body('commentMention')
40 .custom(isUserNotificationSettingValid).withMessage('Should have a valid comment mention notification setting'),
41 body('newFollow')
42 .custom(isUserNotificationSettingValid).withMessage('Should have a valid new follow notification setting'),
43 body('newUserRegistration')
44 .custom(isUserNotificationSettingValid).withMessage('Should have a valid new user registration notification setting'),
45 body('newInstanceFollower')
46 .custom(isUserNotificationSettingValid).withMessage('Should have a valid new instance follower notification setting'),
33 47
34 (req: express.Request, res: express.Response, next: express.NextFunction) => { 48 (req: express.Request, res: express.Response, next: express.NextFunction) => {
35 logger.debug('Checking updateNotificationSettingsValidator parameters', { parameters: req.body }) 49 logger.debug('Checking updateNotificationSettingsValidator parameters', { parameters: req.body })
diff --git a/server/middlewares/validators/user-subscriptions.ts b/server/middlewares/validators/user-subscriptions.ts
index c5f8d9d4c..2356745d7 100644
--- a/server/middlewares/validators/user-subscriptions.ts
+++ b/server/middlewares/validators/user-subscriptions.ts
@@ -5,9 +5,8 @@ import { logger } from '../../helpers/logger'
5import { areValidationErrors } from './utils' 5import { areValidationErrors } from './utils'
6import { ActorFollowModel } from '../../models/activitypub/actor-follow' 6import { ActorFollowModel } from '../../models/activitypub/actor-follow'
7import { areValidActorHandles, isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor' 7import { areValidActorHandles, isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor'
8import { UserModel } from '../../models/account/user'
9import { CONFIG } from '../../initializers'
10import { toArray } from '../../helpers/custom-validators/misc' 8import { toArray } from '../../helpers/custom-validators/misc'
9import { WEBSERVER } from '../../initializers/constants'
11 10
12const userSubscriptionAddValidator = [ 11const userSubscriptionAddValidator = [
13 body('uri').custom(isValidActorHandle).withMessage('Should have a valid URI to follow (username@domain)'), 12 body('uri').custom(isValidActorHandle).withMessage('Should have a valid URI to follow (username@domain)'),
@@ -44,9 +43,9 @@ const userSubscriptionGetValidator = [
44 if (areValidationErrors(req, res)) return 43 if (areValidationErrors(req, res)) return
45 44
46 let [ name, host ] = req.params.uri.split('@') 45 let [ name, host ] = req.params.uri.split('@')
47 if (host === CONFIG.WEBSERVER.HOST) host = null 46 if (host === WEBSERVER.HOST) host = null
48 47
49 const user: UserModel = res.locals.oauth.token.User 48 const user = res.locals.oauth.token.User
50 const subscription = await ActorFollowModel.loadByActorAndTargetNameAndHostForAPI(user.Account.Actor.id, name, host) 49 const subscription = await ActorFollowModel.loadByActorAndTargetNameAndHostForAPI(user.Account.Actor.id, name, host)
51 50
52 if (!subscription || !subscription.ActorFollowing.VideoChannel) { 51 if (!subscription || !subscription.ActorFollowing.VideoChannel) {
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts
index 1bb0bfb1b..6d8cd7894 100644
--- a/server/middlewares/validators/users.ts
+++ b/server/middlewares/validators/users.ts
@@ -5,6 +5,7 @@ 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 isUserAdminFlagsValid,
8 isUserAutoPlayVideoValid, 9 isUserAutoPlayVideoValid,
9 isUserBlockedReasonValid, 10 isUserBlockedReasonValid,
10 isUserDescriptionValid, 11 isUserDescriptionValid,
@@ -14,9 +15,10 @@ import {
14 isUserRoleValid, 15 isUserRoleValid,
15 isUserUsernameValid, 16 isUserUsernameValid,
16 isUserVideoQuotaDailyValid, 17 isUserVideoQuotaDailyValid,
17 isUserVideoQuotaValid, isUserVideosHistoryEnabledValid 18 isUserVideoQuotaValid,
19 isUserVideosHistoryEnabledValid
18} from '../../helpers/custom-validators/users' 20} from '../../helpers/custom-validators/users'
19import { isVideoExist } from '../../helpers/custom-validators/videos' 21import { doesVideoExist } from '../../helpers/custom-validators/videos'
20import { logger } from '../../helpers/logger' 22import { logger } from '../../helpers/logger'
21import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup' 23import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup'
22import { Redis } from '../../lib/redis' 24import { Redis } from '../../lib/redis'
@@ -31,6 +33,7 @@ const usersAddValidator = [
31 body('videoQuota').custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'), 33 body('videoQuota').custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'),
32 body('videoQuotaDaily').custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'), 34 body('videoQuotaDaily').custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'),
33 body('role').custom(isUserRoleValid).withMessage('Should have a valid role'), 35 body('role').custom(isUserRoleValid).withMessage('Should have a valid role'),
36 body('adminFlags').optional().custom(isUserAdminFlagsValid).withMessage('Should have a valid admin flags'),
34 37
35 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 38 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
36 logger.debug('Checking usersAdd parameters', { parameters: omit(req.body, 'password') }) 39 logger.debug('Checking usersAdd parameters', { parameters: omit(req.body, 'password') })
@@ -100,7 +103,7 @@ const usersBlockingValidator = [
100 103
101const deleteMeValidator = [ 104const deleteMeValidator = [
102 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 105 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
103 const user: UserModel = res.locals.oauth.token.User 106 const user = res.locals.oauth.token.User
104 if (user.username === 'root') { 107 if (user.username === 'root') {
105 return res.status(400) 108 return res.status(400)
106 .send({ error: 'You cannot delete your root account.' }) 109 .send({ error: 'You cannot delete your root account.' })
@@ -113,11 +116,13 @@ const deleteMeValidator = [
113 116
114const usersUpdateValidator = [ 117const usersUpdateValidator = [
115 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'), 118 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
119 body('password').optional().custom(isUserPasswordValid).withMessage('Should have a valid password'),
116 body('email').optional().isEmail().withMessage('Should have a valid email attribute'), 120 body('email').optional().isEmail().withMessage('Should have a valid email attribute'),
117 body('emailVerified').optional().isBoolean().withMessage('Should have a valid email verified attribute'), 121 body('emailVerified').optional().isBoolean().withMessage('Should have a valid email verified attribute'),
118 body('videoQuota').optional().custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'), 122 body('videoQuota').optional().custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'),
119 body('videoQuotaDaily').optional().custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'), 123 body('videoQuotaDaily').optional().custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'),
120 body('role').optional().custom(isUserRoleValid).withMessage('Should have a valid role'), 124 body('role').optional().custom(isUserRoleValid).withMessage('Should have a valid role'),
125 body('adminFlags').optional().custom(isUserAdminFlagsValid).withMessage('Should have a valid admin flags'),
121 126
122 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 127 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
123 logger.debug('Checking usersUpdate parameters', { parameters: req.body }) 128 logger.debug('Checking usersUpdate parameters', { parameters: req.body })
@@ -158,8 +163,7 @@ const usersUpdateMeValidator = [
158 .end() 163 .end()
159 } 164 }
160 165
161 const user: UserModel = res.locals.oauth.token.User 166 const user = res.locals.oauth.token.User
162
163 if (await user.isPasswordMatch(req.body.currentPassword) !== true) { 167 if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
164 return res.status(401) 168 return res.status(401)
165 .send({ error: 'currentPassword is invalid.' }) 169 .send({ error: 'currentPassword is invalid.' })
@@ -193,7 +197,7 @@ const usersVideoRatingValidator = [
193 logger.debug('Checking usersVideoRating parameters', { parameters: req.params }) 197 logger.debug('Checking usersVideoRating parameters', { parameters: req.params })
194 198
195 if (areValidationErrors(req, res)) return 199 if (areValidationErrors(req, res)) return
196 if (!await isVideoExist(req.params.videoId, res, 'id')) return 200 if (!await doesVideoExist(req.params.videoId, res, 'id')) return
197 201
198 return next() 202 return next()
199 } 203 }
@@ -233,6 +237,7 @@ const usersAskResetPasswordValidator = [
233 logger.debug('Checking usersAskResetPassword parameters', { parameters: req.body }) 237 logger.debug('Checking usersAskResetPassword parameters', { parameters: req.body })
234 238
235 if (areValidationErrors(req, res)) return 239 if (areValidationErrors(req, res)) return
240
236 const exists = await checkUserEmailExist(req.body.email, res, false) 241 const exists = await checkUserEmailExist(req.body.email, res, false)
237 if (!exists) { 242 if (!exists) {
238 logger.debug('User with email %s does not exist (asking reset password).', req.body.email) 243 logger.debug('User with email %s does not exist (asking reset password).', req.body.email)
@@ -255,7 +260,7 @@ const usersResetPasswordValidator = [
255 if (areValidationErrors(req, res)) return 260 if (areValidationErrors(req, res)) return
256 if (!await checkUserIdExist(req.params.id, res)) return 261 if (!await checkUserIdExist(req.params.id, res)) return
257 262
258 const user = res.locals.user as UserModel 263 const user = res.locals.user
259 const redisVerificationString = await Redis.Instance.getResetPasswordLink(user.id) 264 const redisVerificationString = await Redis.Instance.getResetPasswordLink(user.id)
260 265
261 if (redisVerificationString !== req.body.verificationString) { 266 if (redisVerificationString !== req.body.verificationString) {
@@ -297,7 +302,7 @@ const usersVerifyEmailValidator = [
297 if (areValidationErrors(req, res)) return 302 if (areValidationErrors(req, res)) return
298 if (!await checkUserIdExist(req.params.id, res)) return 303 if (!await checkUserIdExist(req.params.id, res)) return
299 304
300 const user = res.locals.user as UserModel 305 const user = res.locals.user
301 const redisVerificationString = await Redis.Instance.getVerifyEmailLink(user.id) 306 const redisVerificationString = await Redis.Instance.getVerifyEmailLink(user.id)
302 307
303 if (redisVerificationString !== req.body.verificationString) { 308 if (redisVerificationString !== req.body.verificationString) {
@@ -315,6 +320,20 @@ const userAutocompleteValidator = [
315 param('search').isString().not().isEmpty().withMessage('Should have a search parameter') 320 param('search').isString().not().isEmpty().withMessage('Should have a search parameter')
316] 321]
317 322
323const ensureAuthUserOwnsAccountValidator = [
324 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
325 const user = res.locals.oauth.token.User
326
327 if (res.locals.account.id !== user.Account.id) {
328 return res.status(403)
329 .send({ error: 'Only owner can access ratings list.' })
330 .end()
331 }
332
333 return next()
334 }
335]
336
318// --------------------------------------------------------------------------- 337// ---------------------------------------------------------------------------
319 338
320export { 339export {
@@ -333,7 +352,8 @@ export {
333 usersResetPasswordValidator, 352 usersResetPasswordValidator,
334 usersAskSendVerifyEmailValidator, 353 usersAskSendVerifyEmailValidator,
335 usersVerifyEmailValidator, 354 usersVerifyEmailValidator,
336 userAutocompleteValidator 355 userAutocompleteValidator,
356 ensureAuthUserOwnsAccountValidator
337} 357}
338 358
339// --------------------------------------------------------------------------- 359// ---------------------------------------------------------------------------
diff --git a/server/middlewares/validators/videos/video-abuses.ts b/server/middlewares/validators/videos/video-abuses.ts
index be26ca16a..d1910a992 100644
--- a/server/middlewares/validators/videos/video-abuses.ts
+++ b/server/middlewares/validators/videos/video-abuses.ts
@@ -2,11 +2,11 @@ import * as express from 'express'
2import 'express-validator' 2import 'express-validator'
3import { body, param } from 'express-validator/check' 3import { body, param } from 'express-validator/check'
4import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc' 4import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc'
5import { isVideoExist } from '../../../helpers/custom-validators/videos' 5import { doesVideoExist } from '../../../helpers/custom-validators/videos'
6import { logger } from '../../../helpers/logger' 6import { logger } from '../../../helpers/logger'
7import { areValidationErrors } from '../utils' 7import { areValidationErrors } from '../utils'
8import { 8import {
9 isVideoAbuseExist, 9 doesVideoAbuseExist,
10 isVideoAbuseModerationCommentValid, 10 isVideoAbuseModerationCommentValid,
11 isVideoAbuseReasonValid, 11 isVideoAbuseReasonValid,
12 isVideoAbuseStateValid 12 isVideoAbuseStateValid
@@ -20,7 +20,7 @@ const videoAbuseReportValidator = [
20 logger.debug('Checking videoAbuseReport parameters', { parameters: req.body }) 20 logger.debug('Checking videoAbuseReport parameters', { parameters: req.body })
21 21
22 if (areValidationErrors(req, res)) return 22 if (areValidationErrors(req, res)) return
23 if (!await isVideoExist(req.params.videoId, res)) return 23 if (!await doesVideoExist(req.params.videoId, res)) return
24 24
25 return next() 25 return next()
26 } 26 }
@@ -34,8 +34,8 @@ const videoAbuseGetValidator = [
34 logger.debug('Checking videoAbuseGetValidator parameters', { parameters: req.body }) 34 logger.debug('Checking videoAbuseGetValidator parameters', { parameters: req.body })
35 35
36 if (areValidationErrors(req, res)) return 36 if (areValidationErrors(req, res)) return
37 if (!await isVideoExist(req.params.videoId, res)) return 37 if (!await doesVideoExist(req.params.videoId, res)) return
38 if (!await isVideoAbuseExist(req.params.id, res.locals.video.id, res)) return 38 if (!await doesVideoAbuseExist(req.params.id, res.locals.video.id, res)) return
39 39
40 return next() 40 return next()
41 } 41 }
@@ -55,8 +55,8 @@ const videoAbuseUpdateValidator = [
55 logger.debug('Checking videoAbuseUpdateValidator parameters', { parameters: req.body }) 55 logger.debug('Checking videoAbuseUpdateValidator parameters', { parameters: req.body })
56 56
57 if (areValidationErrors(req, res)) return 57 if (areValidationErrors(req, res)) return
58 if (!await isVideoExist(req.params.videoId, res)) return 58 if (!await doesVideoExist(req.params.videoId, res)) return
59 if (!await isVideoAbuseExist(req.params.id, res.locals.video.id, res)) return 59 if (!await doesVideoAbuseExist(req.params.id, res.locals.video.id, res)) return
60 60
61 return next() 61 return next()
62 } 62 }
diff --git a/server/middlewares/validators/videos/video-blacklist.ts b/server/middlewares/validators/videos/video-blacklist.ts
index 2688f63ae..1d7ddb2e3 100644
--- a/server/middlewares/validators/videos/video-blacklist.ts
+++ b/server/middlewares/validators/videos/video-blacklist.ts
@@ -1,11 +1,14 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body, param } from 'express-validator/check' 2import { body, param, query } from 'express-validator/check'
3import { isBooleanValid, isIdOrUUIDValid } from '../../../helpers/custom-validators/misc' 3import { isBooleanValid, isIdOrUUIDValid } from '../../../helpers/custom-validators/misc'
4import { isVideoExist } from '../../../helpers/custom-validators/videos' 4import { doesVideoExist } 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 {
8import { VideoModel } from '../../../models/video/video' 8 doesVideoBlacklistExist,
9 isVideoBlacklistReasonValid,
10 isVideoBlacklistTypeValid
11} from '../../../helpers/custom-validators/video-blacklist'
9 12
10const videosBlacklistRemoveValidator = [ 13const videosBlacklistRemoveValidator = [
11 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), 14 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
@@ -14,8 +17,8 @@ const videosBlacklistRemoveValidator = [
14 logger.debug('Checking blacklistRemove parameters.', { parameters: req.params }) 17 logger.debug('Checking blacklistRemove parameters.', { parameters: req.params })
15 18
16 if (areValidationErrors(req, res)) return 19 if (areValidationErrors(req, res)) return
17 if (!await isVideoExist(req.params.videoId, res)) return 20 if (!await doesVideoExist(req.params.videoId, res)) return
18 if (!await isVideoBlacklistExist(res.locals.video.id, res)) return 21 if (!await doesVideoBlacklistExist(res.locals.video.id, res)) return
19 22
20 return next() 23 return next()
21 } 24 }
@@ -35,9 +38,9 @@ const videosBlacklistAddValidator = [
35 logger.debug('Checking videosBlacklistAdd parameters', { parameters: req.params }) 38 logger.debug('Checking videosBlacklistAdd parameters', { parameters: req.params })
36 39
37 if (areValidationErrors(req, res)) return 40 if (areValidationErrors(req, res)) return
38 if (!await isVideoExist(req.params.videoId, res)) return 41 if (!await doesVideoExist(req.params.videoId, res)) return
39 42
40 const video: VideoModel = res.locals.video 43 const video = res.locals.video
41 if (req.body.unfederate === true && video.remote === true) { 44 if (req.body.unfederate === true && video.remote === true) {
42 return res 45 return res
43 .status(409) 46 .status(409)
@@ -59,8 +62,22 @@ const videosBlacklistUpdateValidator = [
59 logger.debug('Checking videosBlacklistUpdate parameters', { parameters: req.params }) 62 logger.debug('Checking videosBlacklistUpdate parameters', { parameters: req.params })
60 63
61 if (areValidationErrors(req, res)) return 64 if (areValidationErrors(req, res)) return
62 if (!await isVideoExist(req.params.videoId, res)) return 65 if (!await doesVideoExist(req.params.videoId, res)) return
63 if (!await isVideoBlacklistExist(res.locals.video.id, res)) return 66 if (!await doesVideoBlacklistExist(res.locals.video.id, res)) return
67
68 return next()
69 }
70]
71
72const videosBlacklistFiltersValidator = [
73 query('type')
74 .optional()
75 .custom(isVideoBlacklistTypeValid).withMessage('Should have a valid video blacklist type attribute'),
76
77 (req: express.Request, res: express.Response, next: express.NextFunction) => {
78 logger.debug('Checking videos blacklist filters query', { parameters: req.query })
79
80 if (areValidationErrors(req, res)) return
64 81
65 return next() 82 return next()
66 } 83 }
@@ -71,5 +88,6 @@ const videosBlacklistUpdateValidator = [
71export { 88export {
72 videosBlacklistAddValidator, 89 videosBlacklistAddValidator,
73 videosBlacklistRemoveValidator, 90 videosBlacklistRemoveValidator,
74 videosBlacklistUpdateValidator 91 videosBlacklistUpdateValidator,
92 videosBlacklistFiltersValidator
75} 93}
diff --git a/server/middlewares/validators/videos/video-captions.ts b/server/middlewares/validators/videos/video-captions.ts
index 63d84fbec..d857ac3ec 100644
--- a/server/middlewares/validators/videos/video-captions.ts
+++ b/server/middlewares/validators/videos/video-captions.ts
@@ -1,12 +1,12 @@
1import * as express from 'express' 1import * as express from 'express'
2import { areValidationErrors } from '../utils' 2import { areValidationErrors } from '../utils'
3import { checkUserCanManageVideo, isVideoExist } from '../../../helpers/custom-validators/videos' 3import { checkUserCanManageVideo, doesVideoExist } from '../../../helpers/custom-validators/videos'
4import { isIdOrUUIDValid } from '../../../helpers/custom-validators/misc' 4import { isIdOrUUIDValid } from '../../../helpers/custom-validators/misc'
5import { body, param } from 'express-validator/check' 5import { body, param } from 'express-validator/check'
6import { CONSTRAINTS_FIELDS } from '../../../initializers' 6import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
7import { UserRight } from '../../../../shared' 7import { UserRight } from '../../../../shared'
8import { logger } from '../../../helpers/logger' 8import { logger } from '../../../helpers/logger'
9import { isVideoCaptionExist, isVideoCaptionFile, isVideoCaptionLanguageValid } from '../../../helpers/custom-validators/video-captions' 9import { doesVideoCaptionExist, isVideoCaptionFile, isVideoCaptionLanguageValid } from '../../../helpers/custom-validators/video-captions'
10import { cleanUpReqFiles } from '../../../helpers/express-utils' 10import { cleanUpReqFiles } from '../../../helpers/express-utils'
11 11
12const addVideoCaptionValidator = [ 12const addVideoCaptionValidator = [
@@ -22,7 +22,7 @@ const addVideoCaptionValidator = [
22 logger.debug('Checking addVideoCaption parameters', { parameters: req.body }) 22 logger.debug('Checking addVideoCaption parameters', { parameters: req.body })
23 23
24 if (areValidationErrors(req, res)) return cleanUpReqFiles(req) 24 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
25 if (!await isVideoExist(req.params.videoId, res)) return cleanUpReqFiles(req) 25 if (!await doesVideoExist(req.params.videoId, res)) return cleanUpReqFiles(req)
26 26
27 // Check if the user who did the request is able to update the video 27 // Check if the user who did the request is able to update the video
28 const user = res.locals.oauth.token.User 28 const user = res.locals.oauth.token.User
@@ -40,8 +40,8 @@ const deleteVideoCaptionValidator = [
40 logger.debug('Checking deleteVideoCaption parameters', { parameters: req.params }) 40 logger.debug('Checking deleteVideoCaption parameters', { parameters: req.params })
41 41
42 if (areValidationErrors(req, res)) return 42 if (areValidationErrors(req, res)) return
43 if (!await isVideoExist(req.params.videoId, res)) return 43 if (!await doesVideoExist(req.params.videoId, res)) return
44 if (!await isVideoCaptionExist(res.locals.video, req.params.captionLanguage, res)) return 44 if (!await doesVideoCaptionExist(res.locals.video, req.params.captionLanguage, res)) return
45 45
46 // Check if the user who did the request is able to update the video 46 // Check if the user who did the request is able to update the video
47 const user = res.locals.oauth.token.User 47 const user = res.locals.oauth.token.User
@@ -58,7 +58,7 @@ const listVideoCaptionsValidator = [
58 logger.debug('Checking listVideoCaptions parameters', { parameters: req.params }) 58 logger.debug('Checking listVideoCaptions parameters', { parameters: req.params })
59 59
60 if (areValidationErrors(req, res)) return 60 if (areValidationErrors(req, res)) return
61 if (!await isVideoExist(req.params.videoId, res, 'id')) return 61 if (!await doesVideoExist(req.params.videoId, res, 'id')) return
62 62
63 return next() 63 return next()
64 } 64 }
diff --git a/server/middlewares/validators/videos/video-channels.ts b/server/middlewares/validators/videos/video-channels.ts
index f039794e0..4b26f0bc4 100644
--- a/server/middlewares/validators/videos/video-channels.ts
+++ b/server/middlewares/validators/videos/video-channels.ts
@@ -1,12 +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 { UserRight } from '../../../../shared' 3import { UserRight } from '../../../../shared'
4import { isAccountNameWithHostExist } from '../../../helpers/custom-validators/accounts'
5import { 4import {
6 isLocalVideoChannelNameExist, 5 doesLocalVideoChannelNameExist,
6 doesVideoChannelNameWithHostExist,
7 isVideoChannelDescriptionValid, 7 isVideoChannelDescriptionValid,
8 isVideoChannelNameValid, 8 isVideoChannelNameValid,
9 isVideoChannelNameWithHostExist,
10 isVideoChannelSupportValid 9 isVideoChannelSupportValid
11} from '../../../helpers/custom-validators/video-channels' 10} from '../../../helpers/custom-validators/video-channels'
12import { logger } from '../../../helpers/logger' 11import { logger } from '../../../helpers/logger'
@@ -16,19 +15,6 @@ import { areValidationErrors } from '../utils'
16import { isActorPreferredUsernameValid } from '../../../helpers/custom-validators/activitypub/actor' 15import { isActorPreferredUsernameValid } from '../../../helpers/custom-validators/activitypub/actor'
17import { ActorModel } from '../../../models/activitypub/actor' 16import { ActorModel } from '../../../models/activitypub/actor'
18 17
19const listVideoAccountChannelsValidator = [
20 param('accountName').exists().withMessage('Should have a valid account name'),
21
22 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
23 logger.debug('Checking listVideoAccountChannelsValidator parameters', { parameters: req.body })
24
25 if (areValidationErrors(req, res)) return
26 if (!await isAccountNameWithHostExist(req.params.accountName, res)) return
27
28 return next()
29 }
30]
31
32const videoChannelsAddValidator = [ 18const videoChannelsAddValidator = [
33 body('name').custom(isActorPreferredUsernameValid).withMessage('Should have a valid channel name'), 19 body('name').custom(isActorPreferredUsernameValid).withMessage('Should have a valid channel name'),
34 body('displayName').custom(isVideoChannelNameValid).withMessage('Should have a valid display name'), 20 body('displayName').custom(isVideoChannelNameValid).withMessage('Should have a valid display name'),
@@ -62,7 +48,7 @@ const videoChannelsUpdateValidator = [
62 logger.debug('Checking videoChannelsUpdate parameters', { parameters: req.body }) 48 logger.debug('Checking videoChannelsUpdate parameters', { parameters: req.body })
63 49
64 if (areValidationErrors(req, res)) return 50 if (areValidationErrors(req, res)) return
65 if (!await isVideoChannelNameWithHostExist(req.params.nameWithHost, res)) return 51 if (!await doesVideoChannelNameWithHostExist(req.params.nameWithHost, res)) return
66 52
67 // We need to make additional checks 53 // We need to make additional checks
68 if (res.locals.videoChannel.Actor.isOwned() === false) { 54 if (res.locals.videoChannel.Actor.isOwned() === false) {
@@ -88,7 +74,7 @@ const videoChannelsRemoveValidator = [
88 logger.debug('Checking videoChannelsRemove parameters', { parameters: req.params }) 74 logger.debug('Checking videoChannelsRemove parameters', { parameters: req.params })
89 75
90 if (areValidationErrors(req, res)) return 76 if (areValidationErrors(req, res)) return
91 if (!await isVideoChannelNameWithHostExist(req.params.nameWithHost, res)) return 77 if (!await doesVideoChannelNameWithHostExist(req.params.nameWithHost, res)) return
92 78
93 if (!checkUserCanDeleteVideoChannel(res.locals.oauth.token.User, res.locals.videoChannel, res)) return 79 if (!checkUserCanDeleteVideoChannel(res.locals.oauth.token.User, res.locals.videoChannel, res)) return
94 if (!await checkVideoChannelIsNotTheLastOne(res)) return 80 if (!await checkVideoChannelIsNotTheLastOne(res)) return
@@ -105,7 +91,7 @@ const videoChannelsNameWithHostValidator = [
105 91
106 if (areValidationErrors(req, res)) return 92 if (areValidationErrors(req, res)) return
107 93
108 if (!await isVideoChannelNameWithHostExist(req.params.nameWithHost, res)) return 94 if (!await doesVideoChannelNameWithHostExist(req.params.nameWithHost, res)) return
109 95
110 return next() 96 return next()
111 } 97 }
@@ -118,7 +104,7 @@ const localVideoChannelValidator = [
118 logger.debug('Checking localVideoChannelValidator parameters', { parameters: req.params }) 104 logger.debug('Checking localVideoChannelValidator parameters', { parameters: req.params })
119 105
120 if (areValidationErrors(req, res)) return 106 if (areValidationErrors(req, res)) return
121 if (!await isLocalVideoChannelNameExist(req.params.name, res)) return 107 if (!await doesLocalVideoChannelNameExist(req.params.name, res)) return
122 108
123 return next() 109 return next()
124 } 110 }
@@ -127,7 +113,6 @@ const localVideoChannelValidator = [
127// --------------------------------------------------------------------------- 113// ---------------------------------------------------------------------------
128 114
129export { 115export {
130 listVideoAccountChannelsValidator,
131 videoChannelsAddValidator, 116 videoChannelsAddValidator,
132 videoChannelsUpdateValidator, 117 videoChannelsUpdateValidator,
133 videoChannelsRemoveValidator, 118 videoChannelsRemoveValidator,
diff --git a/server/middlewares/validators/videos/video-comments.ts b/server/middlewares/validators/videos/video-comments.ts
index 348d33082..ffde208b7 100644
--- a/server/middlewares/validators/videos/video-comments.ts
+++ b/server/middlewares/validators/videos/video-comments.ts
@@ -3,7 +3,7 @@ import { body, param } from 'express-validator/check'
3import { UserRight } from '../../../../shared' 3import { UserRight } from '../../../../shared'
4import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc' 4import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc'
5import { isValidVideoCommentText } from '../../../helpers/custom-validators/video-comments' 5import { isValidVideoCommentText } from '../../../helpers/custom-validators/video-comments'
6import { isVideoExist } from '../../../helpers/custom-validators/videos' 6import { doesVideoExist } from '../../../helpers/custom-validators/videos'
7import { logger } from '../../../helpers/logger' 7import { logger } from '../../../helpers/logger'
8import { UserModel } from '../../../models/account/user' 8import { UserModel } from '../../../models/account/user'
9import { VideoModel } from '../../../models/video/video' 9import { VideoModel } from '../../../models/video/video'
@@ -17,7 +17,7 @@ const listVideoCommentThreadsValidator = [
17 logger.debug('Checking listVideoCommentThreads parameters.', { parameters: req.params }) 17 logger.debug('Checking listVideoCommentThreads parameters.', { parameters: req.params })
18 18
19 if (areValidationErrors(req, res)) return 19 if (areValidationErrors(req, res)) return
20 if (!await isVideoExist(req.params.videoId, res, 'only-video')) return 20 if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return
21 21
22 return next() 22 return next()
23 } 23 }
@@ -31,8 +31,8 @@ const listVideoThreadCommentsValidator = [
31 logger.debug('Checking listVideoThreadComments parameters.', { parameters: req.params }) 31 logger.debug('Checking listVideoThreadComments parameters.', { parameters: req.params })
32 32
33 if (areValidationErrors(req, res)) return 33 if (areValidationErrors(req, res)) return
34 if (!await isVideoExist(req.params.videoId, res, 'only-video')) return 34 if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return
35 if (!await isVideoCommentThreadExist(req.params.threadId, res.locals.video, res)) return 35 if (!await doesVideoCommentThreadExist(req.params.threadId, res.locals.video, res)) return
36 36
37 return next() 37 return next()
38 } 38 }
@@ -46,7 +46,7 @@ const addVideoCommentThreadValidator = [
46 logger.debug('Checking addVideoCommentThread parameters.', { parameters: req.params, body: req.body }) 46 logger.debug('Checking addVideoCommentThread parameters.', { parameters: req.params, body: req.body })
47 47
48 if (areValidationErrors(req, res)) return 48 if (areValidationErrors(req, res)) return
49 if (!await isVideoExist(req.params.videoId, res)) return 49 if (!await doesVideoExist(req.params.videoId, res)) return
50 if (!isVideoCommentsEnabled(res.locals.video, res)) return 50 if (!isVideoCommentsEnabled(res.locals.video, res)) return
51 51
52 return next() 52 return next()
@@ -62,9 +62,9 @@ const addVideoCommentReplyValidator = [
62 logger.debug('Checking addVideoCommentReply parameters.', { parameters: req.params, body: req.body }) 62 logger.debug('Checking addVideoCommentReply parameters.', { parameters: req.params, body: req.body })
63 63
64 if (areValidationErrors(req, res)) return 64 if (areValidationErrors(req, res)) return
65 if (!await isVideoExist(req.params.videoId, res)) return 65 if (!await doesVideoExist(req.params.videoId, res)) return
66 if (!isVideoCommentsEnabled(res.locals.video, res)) return 66 if (!isVideoCommentsEnabled(res.locals.video, res)) return
67 if (!await isVideoCommentExist(req.params.commentId, res.locals.video, res)) return 67 if (!await doesVideoCommentExist(req.params.commentId, res.locals.video, res)) return
68 68
69 return next() 69 return next()
70 } 70 }
@@ -78,8 +78,8 @@ const videoCommentGetValidator = [
78 logger.debug('Checking videoCommentGetValidator parameters.', { parameters: req.params }) 78 logger.debug('Checking videoCommentGetValidator parameters.', { parameters: req.params })
79 79
80 if (areValidationErrors(req, res)) return 80 if (areValidationErrors(req, res)) return
81 if (!await isVideoExist(req.params.videoId, res, 'id')) return 81 if (!await doesVideoExist(req.params.videoId, res, 'id')) return
82 if (!await isVideoCommentExist(req.params.commentId, res.locals.video, res)) return 82 if (!await doesVideoCommentExist(req.params.commentId, res.locals.video, res)) return
83 83
84 return next() 84 return next()
85 } 85 }
@@ -93,8 +93,8 @@ const removeVideoCommentValidator = [
93 logger.debug('Checking removeVideoCommentValidator parameters.', { parameters: req.params }) 93 logger.debug('Checking removeVideoCommentValidator parameters.', { parameters: req.params })
94 94
95 if (areValidationErrors(req, res)) return 95 if (areValidationErrors(req, res)) return
96 if (!await isVideoExist(req.params.videoId, res)) return 96 if (!await doesVideoExist(req.params.videoId, res)) return
97 if (!await isVideoCommentExist(req.params.commentId, res.locals.video, res)) return 97 if (!await doesVideoCommentExist(req.params.commentId, res.locals.video, res)) return
98 98
99 // Check if the user who did the request is able to delete the video 99 // Check if the user who did the request is able to delete the video
100 if (!checkUserCanDeleteVideoComment(res.locals.oauth.token.User, res.locals.videoComment, res)) return 100 if (!checkUserCanDeleteVideoComment(res.locals.oauth.token.User, res.locals.videoComment, res)) return
@@ -116,7 +116,7 @@ export {
116 116
117// --------------------------------------------------------------------------- 117// ---------------------------------------------------------------------------
118 118
119async function isVideoCommentThreadExist (id: number, video: VideoModel, res: express.Response) { 119async function doesVideoCommentThreadExist (id: number, video: VideoModel, res: express.Response) {
120 const videoComment = await VideoCommentModel.loadById(id) 120 const videoComment = await VideoCommentModel.loadById(id)
121 121
122 if (!videoComment) { 122 if (!videoComment) {
@@ -147,7 +147,7 @@ async function isVideoCommentThreadExist (id: number, video: VideoModel, res: ex
147 return true 147 return true
148} 148}
149 149
150async function isVideoCommentExist (id: number, video: VideoModel, res: express.Response) { 150async function doesVideoCommentExist (id: number, video: VideoModel, res: express.Response) {
151 const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id) 151 const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id)
152 152
153 if (!videoComment) { 153 if (!videoComment) {
diff --git a/server/middlewares/validators/videos/video-imports.ts b/server/middlewares/validators/videos/video-imports.ts
index 48d20f904..452084a7c 100644
--- a/server/middlewares/validators/videos/video-imports.ts
+++ b/server/middlewares/validators/videos/video-imports.ts
@@ -3,14 +3,14 @@ import { body } from 'express-validator/check'
3import { isIdValid } from '../../../helpers/custom-validators/misc' 3import { isIdValid } from '../../../helpers/custom-validators/misc'
4import { logger } from '../../../helpers/logger' 4import { logger } from '../../../helpers/logger'
5import { areValidationErrors } from '../utils' 5import { areValidationErrors } from '../utils'
6import { getCommonVideoAttributes } from './videos' 6import { getCommonVideoEditAttributes } from './videos'
7import { isVideoImportTargetUrlValid, isVideoImportTorrentFile } from '../../../helpers/custom-validators/video-imports' 7import { isVideoImportTargetUrlValid, isVideoImportTorrentFile } from '../../../helpers/custom-validators/video-imports'
8import { cleanUpReqFiles } from '../../../helpers/express-utils' 8import { cleanUpReqFiles } from '../../../helpers/express-utils'
9import { isVideoChannelOfAccountExist, isVideoMagnetUriValid, isVideoNameValid } from '../../../helpers/custom-validators/videos' 9import { doesVideoChannelOfAccountExist, isVideoMagnetUriValid, isVideoNameValid } from '../../../helpers/custom-validators/videos'
10import { CONFIG } from '../../../initializers/constants' 10import { CONFIG } from '../../../initializers/config'
11import { CONSTRAINTS_FIELDS } from '../../../initializers' 11import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
12 12
13const videoImportAddValidator = getCommonVideoAttributes().concat([ 13const videoImportAddValidator = getCommonVideoEditAttributes().concat([
14 body('channelId') 14 body('channelId')
15 .toInt() 15 .toInt()
16 .custom(isIdValid).withMessage('Should have correct video channel id'), 16 .custom(isIdValid).withMessage('Should have correct video channel id'),
@@ -51,7 +51,7 @@ const videoImportAddValidator = getCommonVideoAttributes().concat([
51 .end() 51 .end()
52 } 52 }
53 53
54 if (!await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req) 54 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
55 55
56 // Check we have at least 1 required param 56 // Check we have at least 1 required param
57 if (!req.body.targetUrl && !req.body.magnetUri && !torrentFile) { 57 if (!req.body.targetUrl && !req.body.magnetUri && !torrentFile) {
diff --git a/server/middlewares/validators/videos/video-playlists.ts b/server/middlewares/validators/videos/video-playlists.ts
new file mode 100644
index 000000000..2c3f7e542
--- /dev/null
+++ b/server/middlewares/validators/videos/video-playlists.ts
@@ -0,0 +1,408 @@
1import * as express from 'express'
2import { body, param, query, ValidationChain } from 'express-validator/check'
3import { UserRight, VideoPlaylistCreate, VideoPlaylistUpdate } from '../../../../shared'
4import { logger } from '../../../helpers/logger'
5import { UserModel } from '../../../models/account/user'
6import { areValidationErrors } from '../utils'
7import { doesVideoExist, isVideoImage } from '../../../helpers/custom-validators/videos'
8import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
9import { isArrayOf, isIdOrUUIDValid, isIdValid, isUUIDValid, toIntArray, toValueOrNull } from '../../../helpers/custom-validators/misc'
10import {
11 doesVideoPlaylistExist,
12 isVideoPlaylistDescriptionValid,
13 isVideoPlaylistNameValid,
14 isVideoPlaylistPrivacyValid,
15 isVideoPlaylistTimestampValid,
16 isVideoPlaylistTypeValid
17} from '../../../helpers/custom-validators/video-playlists'
18import { VideoPlaylistModel } from '../../../models/video/video-playlist'
19import { cleanUpReqFiles } from '../../../helpers/express-utils'
20import { doesVideoChannelIdExist } from '../../../helpers/custom-validators/video-channels'
21import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-element'
22import { authenticatePromiseIfNeeded } from '../../oauth'
23import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
24import { VideoPlaylistType } from '../../../../shared/models/videos/playlist/video-playlist-type.model'
25
26const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([
27 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
28 logger.debug('Checking videoPlaylistsAddValidator parameters', { parameters: req.body })
29
30 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
31
32 const body: VideoPlaylistCreate = req.body
33 if (body.videoChannelId && !await doesVideoChannelIdExist(body.videoChannelId, res)) return cleanUpReqFiles(req)
34
35 if (body.privacy === VideoPlaylistPrivacy.PUBLIC && !body.videoChannelId) {
36 cleanUpReqFiles(req)
37 return res.status(400)
38 .json({ error: 'Cannot set "public" a playlist that is not assigned to a channel.' })
39 }
40
41 return next()
42 }
43])
44
45const videoPlaylistsUpdateValidator = getCommonPlaylistEditAttributes().concat([
46 param('playlistId')
47 .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'),
48
49 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
50 logger.debug('Checking videoPlaylistsUpdateValidator parameters', { parameters: req.body })
51
52 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
53
54 if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return cleanUpReqFiles(req)
55
56 const videoPlaylist = res.locals.videoPlaylist
57
58 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, res.locals.videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) {
59 return cleanUpReqFiles(req)
60 }
61
62 const body: VideoPlaylistUpdate = req.body
63
64 if (videoPlaylist.privacy !== VideoPlaylistPrivacy.PRIVATE && body.privacy === VideoPlaylistPrivacy.PRIVATE) {
65 cleanUpReqFiles(req)
66 return res.status(400)
67 .json({ error: 'Cannot set "private" a video playlist that was not private.' })
68 }
69
70 const newPrivacy = body.privacy || videoPlaylist.privacy
71 if (newPrivacy === VideoPlaylistPrivacy.PUBLIC &&
72 (
73 (!videoPlaylist.videoChannelId && !body.videoChannelId) ||
74 body.videoChannelId === null
75 )
76 ) {
77 cleanUpReqFiles(req)
78 return res.status(400)
79 .json({ error: 'Cannot set "public" a playlist that is not assigned to a channel.' })
80 }
81
82 if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) {
83 cleanUpReqFiles(req)
84 return res.status(400)
85 .json({ error: 'Cannot update a watch later playlist.' })
86 }
87
88 if (body.videoChannelId && !await doesVideoChannelIdExist(body.videoChannelId, res)) return cleanUpReqFiles(req)
89
90 return next()
91 }
92])
93
94const videoPlaylistsDeleteValidator = [
95 param('playlistId')
96 .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'),
97
98 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
99 logger.debug('Checking videoPlaylistsDeleteValidator parameters', { parameters: req.params })
100
101 if (areValidationErrors(req, res)) return
102
103 if (!await doesVideoPlaylistExist(req.params.playlistId, res)) return
104
105 const videoPlaylist = res.locals.videoPlaylist
106 if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) {
107 return res.status(400)
108 .json({ error: 'Cannot delete a watch later playlist.' })
109 }
110
111 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, res.locals.videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) {
112 return
113 }
114
115 return next()
116 }
117]
118
119const videoPlaylistsGetValidator = [
120 param('playlistId')
121 .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'),
122
123 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
124 logger.debug('Checking videoPlaylistsGetValidator parameters', { parameters: req.params })
125
126 if (areValidationErrors(req, res)) return
127
128 if (!await doesVideoPlaylistExist(req.params.playlistId, res)) return
129
130 const videoPlaylist = res.locals.videoPlaylist
131
132 // Video is unlisted, check we used the uuid to fetch it
133 if (videoPlaylist.privacy === VideoPlaylistPrivacy.UNLISTED) {
134 if (isUUIDValid(req.params.playlistId)) return next()
135
136 return res.status(404).end()
137 }
138
139 if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) {
140 await authenticatePromiseIfNeeded(req, res)
141
142 const user = res.locals.oauth ? res.locals.oauth.token.User : null
143 if (
144 !user ||
145 (videoPlaylist.OwnerAccount.userId !== user.id && !user.hasRight(UserRight.UPDATE_ANY_VIDEO_PLAYLIST))
146 ) {
147 return res.status(403)
148 .json({ error: 'Cannot get this private video playlist.' })
149 }
150
151 return next()
152 }
153
154 return next()
155 }
156]
157
158const videoPlaylistsAddVideoValidator = [
159 param('playlistId')
160 .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'),
161 body('videoId')
162 .custom(isIdOrUUIDValid).withMessage('Should have a valid video id/uuid'),
163 body('startTimestamp')
164 .optional()
165 .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid start timestamp'),
166 body('stopTimestamp')
167 .optional()
168 .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid stop timestamp'),
169
170 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
171 logger.debug('Checking videoPlaylistsAddVideoValidator parameters', { parameters: req.params })
172
173 if (areValidationErrors(req, res)) return
174
175 if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return
176 if (!await doesVideoExist(req.body.videoId, res, 'only-video')) return
177
178 const videoPlaylist = res.locals.videoPlaylist
179 const video = res.locals.video
180
181 const videoPlaylistElement = await VideoPlaylistElementModel.loadByPlaylistAndVideo(videoPlaylist.id, video.id)
182 if (videoPlaylistElement) {
183 res.status(409)
184 .json({ error: 'This video in this playlist already exists' })
185 .end()
186
187 return
188 }
189
190 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, res.locals.videoPlaylist, UserRight.UPDATE_ANY_VIDEO_PLAYLIST, res)) {
191 return
192 }
193
194 return next()
195 }
196]
197
198const videoPlaylistsUpdateOrRemoveVideoValidator = [
199 param('playlistId')
200 .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'),
201 param('videoId')
202 .custom(isIdOrUUIDValid).withMessage('Should have an video id/uuid'),
203 body('startTimestamp')
204 .optional()
205 .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid start timestamp'),
206 body('stopTimestamp')
207 .optional()
208 .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid stop timestamp'),
209
210 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
211 logger.debug('Checking videoPlaylistsRemoveVideoValidator parameters', { parameters: req.params })
212
213 if (areValidationErrors(req, res)) return
214
215 if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return
216 if (!await doesVideoExist(req.params.videoId, res, 'id')) return
217
218 const videoPlaylist = res.locals.videoPlaylist
219 const video = res.locals.video
220
221 const videoPlaylistElement = await VideoPlaylistElementModel.loadByPlaylistAndVideo(videoPlaylist.id, video.id)
222 if (!videoPlaylistElement) {
223 res.status(404)
224 .json({ error: 'Video playlist element not found' })
225 .end()
226
227 return
228 }
229 res.locals.videoPlaylistElement = videoPlaylistElement
230
231 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.UPDATE_ANY_VIDEO_PLAYLIST, res)) return
232
233 return next()
234 }
235]
236
237const videoPlaylistElementAPGetValidator = [
238 param('playlistId')
239 .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'),
240 param('videoId')
241 .custom(isIdOrUUIDValid).withMessage('Should have an video id/uuid'),
242
243 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
244 logger.debug('Checking videoPlaylistElementAPGetValidator parameters', { parameters: req.params })
245
246 if (areValidationErrors(req, res)) return
247
248 const videoPlaylistElement = await VideoPlaylistElementModel.loadByPlaylistAndVideoForAP(req.params.playlistId, req.params.videoId)
249 if (!videoPlaylistElement) {
250 res.status(404)
251 .json({ error: 'Video playlist element not found' })
252 .end()
253
254 return
255 }
256
257 if (videoPlaylistElement.VideoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) {
258 return res.status(403).end()
259 }
260
261 res.locals.videoPlaylistElement = videoPlaylistElement
262
263 return next()
264 }
265]
266
267const videoPlaylistsReorderVideosValidator = [
268 param('playlistId')
269 .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'),
270 body('startPosition')
271 .isInt({ min: 1 }).withMessage('Should have a valid start position'),
272 body('insertAfterPosition')
273 .isInt({ min: 0 }).withMessage('Should have a valid insert after position'),
274 body('reorderLength')
275 .optional()
276 .isInt({ min: 1 }).withMessage('Should have a valid range length'),
277
278 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
279 logger.debug('Checking videoPlaylistsReorderVideosValidator parameters', { parameters: req.params })
280
281 if (areValidationErrors(req, res)) return
282
283 if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return
284
285 const videoPlaylist = res.locals.videoPlaylist
286 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.UPDATE_ANY_VIDEO_PLAYLIST, res)) return
287
288 const nextPosition = await VideoPlaylistElementModel.getNextPositionOf(videoPlaylist.id)
289 const startPosition: number = req.body.startPosition
290 const insertAfterPosition: number = req.body.insertAfterPosition
291 const reorderLength: number = req.body.reorderLength
292
293 if (startPosition >= nextPosition || insertAfterPosition >= nextPosition) {
294 res.status(400)
295 .json({ error: `Start position or insert after position exceed the playlist limits (max: ${nextPosition - 1})` })
296 .end()
297
298 return
299 }
300
301 if (reorderLength && reorderLength + startPosition > nextPosition) {
302 res.status(400)
303 .json({ error: `Reorder length with this start position exceeds the playlist limits (max: ${nextPosition - startPosition})` })
304 .end()
305
306 return
307 }
308
309 return next()
310 }
311]
312
313const commonVideoPlaylistFiltersValidator = [
314 query('playlistType')
315 .optional()
316 .custom(isVideoPlaylistTypeValid).withMessage('Should have a valid playlist type'),
317
318 (req: express.Request, res: express.Response, next: express.NextFunction) => {
319 logger.debug('Checking commonVideoPlaylistFiltersValidator parameters', { parameters: req.params })
320
321 if (areValidationErrors(req, res)) return
322
323 return next()
324 }
325]
326
327const doVideosInPlaylistExistValidator = [
328 query('videoIds')
329 .customSanitizer(toIntArray)
330 .custom(v => isArrayOf(v, isIdValid)).withMessage('Should have a valid video ids array'),
331
332 (req: express.Request, res: express.Response, next: express.NextFunction) => {
333 logger.debug('Checking areVideosInPlaylistExistValidator parameters', { parameters: req.query })
334
335 if (areValidationErrors(req, res)) return
336
337 return next()
338 }
339]
340
341// ---------------------------------------------------------------------------
342
343export {
344 videoPlaylistsAddValidator,
345 videoPlaylistsUpdateValidator,
346 videoPlaylistsDeleteValidator,
347 videoPlaylistsGetValidator,
348
349 videoPlaylistsAddVideoValidator,
350 videoPlaylistsUpdateOrRemoveVideoValidator,
351 videoPlaylistsReorderVideosValidator,
352
353 videoPlaylistElementAPGetValidator,
354
355 commonVideoPlaylistFiltersValidator,
356
357 doVideosInPlaylistExistValidator
358}
359
360// ---------------------------------------------------------------------------
361
362function getCommonPlaylistEditAttributes () {
363 return [
364 body('thumbnailfile')
365 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
366 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: '
367 + CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS.IMAGE.EXTNAME.join(', ')
368 ),
369
370 body('displayName')
371 .custom(isVideoPlaylistNameValid).withMessage('Should have a valid display name'),
372 body('description')
373 .optional()
374 .customSanitizer(toValueOrNull)
375 .custom(isVideoPlaylistDescriptionValid).withMessage('Should have a valid description'),
376 body('privacy')
377 .optional()
378 .toInt()
379 .custom(isVideoPlaylistPrivacyValid).withMessage('Should have correct playlist privacy'),
380 body('videoChannelId')
381 .optional()
382 .customSanitizer(toValueOrNull)
383 .toInt()
384 ] as (ValidationChain | express.Handler)[]
385}
386
387function checkUserCanManageVideoPlaylist (user: UserModel, videoPlaylist: VideoPlaylistModel, right: UserRight, res: express.Response) {
388 if (videoPlaylist.isOwned() === false) {
389 res.status(403)
390 .json({ error: 'Cannot manage video playlist of another server.' })
391 .end()
392
393 return false
394 }
395
396 // Check if the user can manage the video playlist
397 // The user can delete it if s/he is an admin
398 // Or if s/he is the video playlist's owner
399 if (user.hasRight(right) === false && videoPlaylist.ownerAccountId !== user.Account.id) {
400 res.status(403)
401 .json({ error: 'Cannot manage video playlist of another user' })
402 .end()
403
404 return false
405 }
406
407 return true
408}
diff --git a/server/middlewares/validators/videos/video-rates.ts b/server/middlewares/validators/videos/video-rates.ts
index 793354520..204b4a78d 100644
--- a/server/middlewares/validators/videos/video-rates.ts
+++ b/server/middlewares/validators/videos/video-rates.ts
@@ -1,8 +1,9 @@
1import * as express from 'express' 1import * as express from 'express'
2import 'express-validator' 2import 'express-validator'
3import { body, param } from 'express-validator/check' 3import { body, param, query } from 'express-validator/check'
4import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc' 4import { isIdOrUUIDValid } from '../../../helpers/custom-validators/misc'
5import { isVideoExist, isVideoRatingTypeValid } from '../../../helpers/custom-validators/videos' 5import { isRatingValid } from '../../../helpers/custom-validators/video-rates'
6import { doesVideoExist, isVideoRatingTypeValid } from '../../../helpers/custom-validators/videos'
6import { logger } from '../../../helpers/logger' 7import { logger } from '../../../helpers/logger'
7import { areValidationErrors } from '../utils' 8import { areValidationErrors } from '../utils'
8import { AccountVideoRateModel } from '../../../models/account/account-video-rate' 9import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
@@ -17,7 +18,7 @@ const videoUpdateRateValidator = [
17 logger.debug('Checking videoRate parameters', { parameters: req.body }) 18 logger.debug('Checking videoRate parameters', { parameters: req.body })
18 19
19 if (areValidationErrors(req, res)) return 20 if (areValidationErrors(req, res)) return
20 if (!await isVideoExist(req.params.id, res)) return 21 if (!await doesVideoExist(req.params.id, res)) return
21 22
22 return next() 23 return next()
23 } 24 }
@@ -47,9 +48,22 @@ const getAccountVideoRateValidator = function (rateType: VideoRateType) {
47 ] 48 ]
48} 49}
49 50
51const videoRatingValidator = [
52 query('rating').optional().custom(isRatingValid).withMessage('Value must be one of "like" or "dislike"'),
53
54 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
55 logger.debug('Checking rating parameter', { parameters: req.params })
56
57 if (areValidationErrors(req, res)) return
58
59 return next()
60 }
61]
62
50// --------------------------------------------------------------------------- 63// ---------------------------------------------------------------------------
51 64
52export { 65export {
53 videoUpdateRateValidator, 66 videoUpdateRateValidator,
54 getAccountVideoRateValidator 67 getAccountVideoRateValidator,
68 videoRatingValidator
55} 69}
diff --git a/server/middlewares/validators/videos/video-shares.ts b/server/middlewares/validators/videos/video-shares.ts
index 646d7acb1..d5cbdb03e 100644
--- a/server/middlewares/validators/videos/video-shares.ts
+++ b/server/middlewares/validators/videos/video-shares.ts
@@ -2,11 +2,10 @@ import * as express from 'express'
2import 'express-validator' 2import 'express-validator'
3import { param } from 'express-validator/check' 3import { param } from 'express-validator/check'
4import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc' 4import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc'
5import { isVideoExist } from '../../../helpers/custom-validators/videos' 5import { doesVideoExist } from '../../../helpers/custom-validators/videos'
6import { logger } from '../../../helpers/logger' 6import { logger } from '../../../helpers/logger'
7import { VideoShareModel } from '../../../models/video/video-share' 7import { VideoShareModel } from '../../../models/video/video-share'
8import { areValidationErrors } from '../utils' 8import { areValidationErrors } from '../utils'
9import { VideoModel } from '../../../models/video/video'
10 9
11const videosShareValidator = [ 10const videosShareValidator = [
12 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), 11 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
@@ -16,9 +15,9 @@ const videosShareValidator = [
16 logger.debug('Checking videoShare parameters', { parameters: req.params }) 15 logger.debug('Checking videoShare parameters', { parameters: req.params })
17 16
18 if (areValidationErrors(req, res)) return 17 if (areValidationErrors(req, res)) return
19 if (!await isVideoExist(req.params.id, res)) return 18 if (!await doesVideoExist(req.params.id, res)) return
20 19
21 const video: VideoModel = res.locals.video 20 const video = res.locals.video
22 21
23 const share = await VideoShareModel.load(req.params.actorId, video.id) 22 const share = await VideoShareModel.load(req.params.actorId, video.id)
24 if (!share) { 23 if (!share) {
diff --git a/server/middlewares/validators/videos/video-watch.ts b/server/middlewares/validators/videos/video-watch.ts
index c38ad8a10..a3a800d14 100644
--- a/server/middlewares/validators/videos/video-watch.ts
+++ b/server/middlewares/validators/videos/video-watch.ts
@@ -1,10 +1,9 @@
1import { body, param } from 'express-validator/check' 1import { body, param } from 'express-validator/check'
2import * as express from 'express' 2import * as express from 'express'
3import { isIdOrUUIDValid } from '../../../helpers/custom-validators/misc' 3import { isIdOrUUIDValid } from '../../../helpers/custom-validators/misc'
4import { isVideoExist } from '../../../helpers/custom-validators/videos' 4import { doesVideoExist } 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'
8 7
9const videoWatchingValidator = [ 8const videoWatchingValidator = [
10 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), 9 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
@@ -16,9 +15,9 @@ const videoWatchingValidator = [
16 logger.debug('Checking videoWatching parameters', { parameters: req.body }) 15 logger.debug('Checking videoWatching parameters', { parameters: req.body })
17 16
18 if (areValidationErrors(req, res)) return 17 if (areValidationErrors(req, res)) return
19 if (!await isVideoExist(req.params.videoId, res, 'id')) return 18 if (!await doesVideoExist(req.params.videoId, res, 'id')) return
20 19
21 const user = res.locals.oauth.token.User as UserModel 20 const user = res.locals.oauth.token.User
22 if (user.videosHistoryEnabled === false) { 21 if (user.videosHistoryEnabled === false) {
23 logger.warn('Cannot set videos to watch by user %d: videos history is disabled.', user.id) 22 logger.warn('Cannot set videos to watch by user %d: videos history is disabled.', user.id)
24 return res.status(409).end() 23 return res.status(409).end()
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts
index 051a19e16..2b01f108d 100644
--- a/server/middlewares/validators/videos/videos.ts
+++ b/server/middlewares/validators/videos/videos.ts
@@ -14,38 +14,38 @@ import {
14} from '../../../helpers/custom-validators/misc' 14} from '../../../helpers/custom-validators/misc'
15import { 15import {
16 checkUserCanManageVideo, 16 checkUserCanManageVideo,
17 doesVideoChannelOfAccountExist,
18 doesVideoExist,
17 isScheduleVideoUpdatePrivacyValid, 19 isScheduleVideoUpdatePrivacyValid,
18 isVideoCategoryValid, 20 isVideoCategoryValid,
19 isVideoChannelOfAccountExist,
20 isVideoDescriptionValid, 21 isVideoDescriptionValid,
21 isVideoExist,
22 isVideoFile, 22 isVideoFile,
23 isVideoFilterValid, 23 isVideoFilterValid,
24 isVideoImage, 24 isVideoImage,
25 isVideoLanguageValid, 25 isVideoLanguageValid,
26 isVideoLicenceValid, 26 isVideoLicenceValid,
27 isVideoNameValid, 27 isVideoNameValid,
28 isVideoOriginallyPublishedAtValid,
28 isVideoPrivacyValid, 29 isVideoPrivacyValid,
29 isVideoSupportValid, 30 isVideoSupportValid,
30 isVideoTagsValid 31 isVideoTagsValid
31} from '../../../helpers/custom-validators/videos' 32} from '../../../helpers/custom-validators/videos'
32import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils' 33import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils'
33import { logger } from '../../../helpers/logger' 34import { logger } from '../../../helpers/logger'
34import { CONFIG, CONSTRAINTS_FIELDS } from '../../../initializers' 35import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
35import { authenticatePromiseIfNeeded } from '../../oauth' 36import { authenticatePromiseIfNeeded } from '../../oauth'
36import { areValidationErrors } from '../utils' 37import { areValidationErrors } from '../utils'
37import { cleanUpReqFiles } from '../../../helpers/express-utils' 38import { cleanUpReqFiles } from '../../../helpers/express-utils'
38import { VideoModel } from '../../../models/video/video' 39import { VideoModel } from '../../../models/video/video'
39import { UserModel } from '../../../models/account/user'
40import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership' 40import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership'
41import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model' 41import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model'
42import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ownership'
43import { AccountModel } from '../../../models/account/account' 42import { AccountModel } from '../../../models/account/account'
44import { VideoFetchType } from '../../../helpers/video' 43import { VideoFetchType } from '../../../helpers/video'
45import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search' 44import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
46import { getServerActor } from '../../../helpers/utils' 45import { getServerActor } from '../../../helpers/utils'
46import { CONFIG } from '../../../initializers/config'
47 47
48const videosAddValidator = getCommonVideoAttributes().concat([ 48const videosAddValidator = getCommonVideoEditAttributes().concat([
49 body('videofile') 49 body('videofile')
50 .custom((value, { req }) => isVideoFile(req.files)).withMessage( 50 .custom((value, { req }) => isVideoFile(req.files)).withMessage(
51 'This file is not supported or too large. Please, make sure it is of the following type: ' 51 'This file is not supported or too large. Please, make sure it is of the following type: '
@@ -65,9 +65,10 @@ const videosAddValidator = getCommonVideoAttributes().concat([
65 const videoFile: Express.Multer.File = req.files['videofile'][0] 65 const videoFile: Express.Multer.File = req.files['videofile'][0]
66 const user = res.locals.oauth.token.User 66 const user = res.locals.oauth.token.User
67 67
68 if (!await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req) 68 if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
69 69
70 const isAble = await user.isAbleToUploadVideo(videoFile) 70 const isAble = await user.isAbleToUploadVideo(videoFile)
71
71 if (isAble === false) { 72 if (isAble === false) {
72 res.status(403) 73 res.status(403)
73 .json({ error: 'The user video quota is exceeded with this video.' }) 74 .json({ error: 'The user video quota is exceeded with this video.' })
@@ -93,7 +94,7 @@ const videosAddValidator = getCommonVideoAttributes().concat([
93 } 94 }
94]) 95])
95 96
96const videosUpdateValidator = getCommonVideoAttributes().concat([ 97const videosUpdateValidator = getCommonVideoEditAttributes().concat([
97 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), 98 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
98 body('name') 99 body('name')
99 .optional() 100 .optional()
@@ -108,7 +109,7 @@ const videosUpdateValidator = getCommonVideoAttributes().concat([
108 109
109 if (areValidationErrors(req, res)) return cleanUpReqFiles(req) 110 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
110 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req) 111 if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
111 if (!await isVideoExist(req.params.id, res)) return cleanUpReqFiles(req) 112 if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req)
112 113
113 const video = res.locals.video 114 const video = res.locals.video
114 115
@@ -122,14 +123,14 @@ const videosUpdateValidator = getCommonVideoAttributes().concat([
122 .json({ error: 'Cannot set "private" a video that was not private.' }) 123 .json({ error: 'Cannot set "private" a video that was not private.' })
123 } 124 }
124 125
125 if (req.body.channelId && !await isVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req) 126 if (req.body.channelId && !await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
126 127
127 return next() 128 return next()
128 } 129 }
129]) 130])
130 131
131async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) { 132async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) {
132 const video: VideoModel = res.locals.video 133 const video = res.locals.video
133 134
134 // Anybody can watch local videos 135 // Anybody can watch local videos
135 if (video.isOwned() === true) return next() 136 if (video.isOwned() === true) return next()
@@ -161,15 +162,15 @@ const videosCustomGetValidator = (fetchType: VideoFetchType) => {
161 logger.debug('Checking videosGet parameters', { parameters: req.params }) 162 logger.debug('Checking videosGet parameters', { parameters: req.params })
162 163
163 if (areValidationErrors(req, res)) return 164 if (areValidationErrors(req, res)) return
164 if (!await isVideoExist(req.params.id, res, fetchType)) return 165 if (!await doesVideoExist(req.params.id, res, fetchType)) return
165 166
166 const video: VideoModel = res.locals.video 167 const video = res.locals.video
167 168
168 // Video private or blacklisted 169 // Video private or blacklisted
169 if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) { 170 if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) {
170 await authenticatePromiseIfNeeded(req, res) 171 await authenticatePromiseIfNeeded(req, res)
171 172
172 const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : null 173 const user = res.locals.oauth ? res.locals.oauth.token.User : null
173 174
174 // Only the owner or a user that have blacklist rights can see the video 175 // Only the owner or a user that have blacklist rights can see the video
175 if ( 176 if (
@@ -206,7 +207,7 @@ const videosRemoveValidator = [
206 logger.debug('Checking videosRemove parameters', { parameters: req.params }) 207 logger.debug('Checking videosRemove parameters', { parameters: req.params })
207 208
208 if (areValidationErrors(req, res)) return 209 if (areValidationErrors(req, res)) return
209 if (!await isVideoExist(req.params.id, res)) return 210 if (!await doesVideoExist(req.params.id, res)) return
210 211
211 // Check if the user who did the request is able to delete the video 212 // Check if the user who did the request is able to delete the video
212 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.REMOVE_ANY_VIDEO, res)) return 213 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.REMOVE_ANY_VIDEO, res)) return
@@ -222,7 +223,7 @@ const videosChangeOwnershipValidator = [
222 logger.debug('Checking changeOwnership parameters', { parameters: req.params }) 223 logger.debug('Checking changeOwnership parameters', { parameters: req.params })
223 224
224 if (areValidationErrors(req, res)) return 225 if (areValidationErrors(req, res)) return
225 if (!await isVideoExist(req.params.videoId, res)) return 226 if (!await doesVideoExist(req.params.videoId, res)) return
226 227
227 // Check if the user who did the request is able to change the ownership of the video 228 // Check if the user who did the request is able to change the ownership of the video
228 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return 229 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return
@@ -255,7 +256,7 @@ const videosTerminateChangeOwnershipValidator = [
255 return next() 256 return next()
256 }, 257 },
257 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 258 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
258 const videoChangeOwnership = res.locals.videoChangeOwnership as VideoChangeOwnershipModel 259 const videoChangeOwnership = res.locals.videoChangeOwnership
259 260
260 if (videoChangeOwnership.status === VideoChangeOwnershipStatus.WAITING) { 261 if (videoChangeOwnership.status === VideoChangeOwnershipStatus.WAITING) {
261 return next() 262 return next()
@@ -271,10 +272,10 @@ const videosTerminateChangeOwnershipValidator = [
271const videosAcceptChangeOwnershipValidator = [ 272const videosAcceptChangeOwnershipValidator = [
272 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 273 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
273 const body = req.body as VideoChangeOwnershipAccept 274 const body = req.body as VideoChangeOwnershipAccept
274 if (!await isVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return 275 if (!await doesVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return
275 276
276 const user = res.locals.oauth.token.User 277 const user = res.locals.oauth.token.User
277 const videoChangeOwnership = res.locals.videoChangeOwnership as VideoChangeOwnershipModel 278 const videoChangeOwnership = res.locals.videoChangeOwnership
278 const isAble = await user.isAbleToUploadVideo(videoChangeOwnership.Video.getOriginalFile()) 279 const isAble = await user.isAbleToUploadVideo(videoChangeOwnership.Video.getOriginalFile())
279 if (isAble === false) { 280 if (isAble === false) {
280 res.status(403) 281 res.status(403)
@@ -287,7 +288,7 @@ const videosAcceptChangeOwnershipValidator = [
287 } 288 }
288] 289]
289 290
290function getCommonVideoAttributes () { 291function getCommonVideoEditAttributes () {
291 return [ 292 return [
292 body('thumbnailfile') 293 body('thumbnailfile')
293 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage( 294 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
@@ -340,7 +341,14 @@ function getCommonVideoAttributes () {
340 .optional() 341 .optional()
341 .toBoolean() 342 .toBoolean()
342 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'), 343 .custom(isBooleanValid).withMessage('Should have comments enabled boolean'),
343 344 body('downloadEnabled')
345 .optional()
346 .toBoolean()
347 .custom(isBooleanValid).withMessage('Should have downloading enabled boolean'),
348 body('originallyPublishedAt')
349 .optional()
350 .customSanitizer(toValueOrNull)
351 .custom(isVideoOriginallyPublishedAtValid).withMessage('Should have a valid original publication date'),
344 body('scheduleUpdate') 352 body('scheduleUpdate')
345 .optional() 353 .optional()
346 .customSanitizer(toValueOrNull), 354 .customSanitizer(toValueOrNull),
@@ -387,7 +395,7 @@ const commonVideosFiltersValidator = [
387 395
388 if (areValidationErrors(req, res)) return 396 if (areValidationErrors(req, res)) return
389 397
390 const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined 398 const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
391 if (req.query.filter === 'all-local' && (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)) { 399 if (req.query.filter === 'all-local' && (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)) {
392 res.status(401) 400 res.status(401)
393 .json({ error: 'You are not allowed to see all local videos.' }) 401 .json({ error: 'You are not allowed to see all local videos.' })
@@ -413,7 +421,7 @@ export {
413 videosTerminateChangeOwnershipValidator, 421 videosTerminateChangeOwnershipValidator,
414 videosAcceptChangeOwnershipValidator, 422 videosAcceptChangeOwnershipValidator,
415 423
416 getCommonVideoAttributes, 424 getCommonVideoEditAttributes,
417 425
418 commonVideosFiltersValidator 426 commonVideosFiltersValidator
419} 427}
diff --git a/server/models/account/account-blocklist.ts b/server/models/account/account-blocklist.ts
index efd6ed59e..d5746ad76 100644
--- a/server/models/account/account-blocklist.ts
+++ b/server/models/account/account-blocklist.ts
@@ -8,22 +8,22 @@ enum ScopeNames {
8 WITH_ACCOUNTS = 'WITH_ACCOUNTS' 8 WITH_ACCOUNTS = 'WITH_ACCOUNTS'
9} 9}
10 10
11@Scopes({ 11@Scopes(() => ({
12 [ScopeNames.WITH_ACCOUNTS]: { 12 [ScopeNames.WITH_ACCOUNTS]: {
13 include: [ 13 include: [
14 { 14 {
15 model: () => AccountModel, 15 model: AccountModel,
16 required: true, 16 required: true,
17 as: 'ByAccount' 17 as: 'ByAccount'
18 }, 18 },
19 { 19 {
20 model: () => AccountModel, 20 model: AccountModel,
21 required: true, 21 required: true,
22 as: 'BlockedAccount' 22 as: 'BlockedAccount'
23 } 23 }
24 ] 24 ]
25 } 25 }
26}) 26}))
27 27
28@Table({ 28@Table({
29 tableName: 'accountBlocklist', 29 tableName: 'accountBlocklist',
@@ -83,7 +83,7 @@ export class AccountBlocklistModel extends Model<AccountBlocklistModel> {
83 attributes: [ 'accountId', 'id' ], 83 attributes: [ 'accountId', 'id' ],
84 where: { 84 where: {
85 accountId: { 85 accountId: {
86 [Op.any]: accountIds 86 [Op.in]: accountIds // FIXME: sequelize ANY seems broken
87 }, 87 },
88 targetAccountId 88 targetAccountId
89 }, 89 },
diff --git a/server/models/account/account-video-rate.ts b/server/models/account/account-video-rate.ts
index 18762f0c5..59f586b54 100644
--- a/server/models/account/account-video-rate.ts
+++ b/server/models/account/account-video-rate.ts
@@ -1,14 +1,15 @@
1import { values } from 'lodash' 1import { values } from 'lodash'
2import { Transaction } from 'sequelize' 2import { FindOptions, Op, Transaction } from 'sequelize'
3import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' 3import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
4import { IFindOptions } from 'sequelize-typescript/lib/interfaces/IFindOptions'
5import { VideoRateType } from '../../../shared/models/videos' 4import { VideoRateType } from '../../../shared/models/videos'
6import { CONSTRAINTS_FIELDS, VIDEO_RATE_TYPES } from '../../initializers' 5import { CONSTRAINTS_FIELDS, VIDEO_RATE_TYPES } from '../../initializers/constants'
7import { VideoModel } from '../video/video' 6import { VideoModel } from '../video/video'
8import { AccountModel } from './account' 7import { AccountModel } from './account'
9import { ActorModel } from '../activitypub/actor' 8import { ActorModel } from '../activitypub/actor'
10import { throwIfNotValid } from '../utils' 9import { getSort, throwIfNotValid } from '../utils'
11import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 10import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
11import { AccountVideoRate } from '../../../shared'
12import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from '../video/video-channel'
12 13
13/* 14/*
14 Account rates per video. 15 Account rates per video.
@@ -38,7 +39,7 @@ import { isActivityPubUrlValid } from '../../helpers/custom-validators/activityp
38export class AccountVideoRateModel extends Model<AccountVideoRateModel> { 39export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
39 40
40 @AllowNull(false) 41 @AllowNull(false)
41 @Column(DataType.ENUM(values(VIDEO_RATE_TYPES))) 42 @Column(DataType.ENUM(...values(VIDEO_RATE_TYPES)))
42 type: VideoRateType 43 type: VideoRateType
43 44
44 @AllowNull(false) 45 @AllowNull(false)
@@ -77,7 +78,7 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
77 Account: AccountModel 78 Account: AccountModel
78 79
79 static load (accountId: number, videoId: number, transaction?: Transaction) { 80 static load (accountId: number, videoId: number, transaction?: Transaction) {
80 const options: IFindOptions<AccountVideoRateModel> = { 81 const options: FindOptions = {
81 where: { 82 where: {
82 accountId, 83 accountId,
83 videoId 84 videoId
@@ -88,8 +89,40 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
88 return AccountVideoRateModel.findOne(options) 89 return AccountVideoRateModel.findOne(options)
89 } 90 }
90 91
92 static listByAccountForApi (options: {
93 start: number,
94 count: number,
95 sort: string,
96 type?: string,
97 accountId: number
98 }) {
99 const query: FindOptions = {
100 offset: options.start,
101 limit: options.count,
102 order: getSort(options.sort),
103 where: {
104 accountId: options.accountId
105 },
106 include: [
107 {
108 model: VideoModel,
109 required: true,
110 include: [
111 {
112 model: VideoChannelModel.scope({ method: [VideoChannelScopeNames.SUMMARY, true] }),
113 required: true
114 }
115 ]
116 }
117 ]
118 }
119 if (options.type) query.where['type'] = options.type
120
121 return AccountVideoRateModel.findAndCountAll(query)
122 }
123
91 static loadLocalAndPopulateVideo (rateType: VideoRateType, accountName: string, videoId: number, transaction?: Transaction) { 124 static loadLocalAndPopulateVideo (rateType: VideoRateType, accountName: string, videoId: number, transaction?: Transaction) {
92 const options: IFindOptions<AccountVideoRateModel> = { 125 const options: FindOptions = {
93 where: { 126 where: {
94 videoId, 127 videoId,
95 type: rateType 128 type: rateType
@@ -121,7 +154,7 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
121 } 154 }
122 155
123 static loadByUrl (url: string, transaction: Transaction) { 156 static loadByUrl (url: string, transaction: Transaction) {
124 const options: IFindOptions<AccountVideoRateModel> = { 157 const options: FindOptions = {
125 where: { 158 where: {
126 url 159 url
127 } 160 }
@@ -158,4 +191,38 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
158 191
159 return AccountVideoRateModel.findAndCountAll(query) 192 return AccountVideoRateModel.findAndCountAll(query)
160 } 193 }
194
195 static cleanOldRatesOf (videoId: number, type: VideoRateType, beforeUpdatedAt: Date) {
196 return AccountVideoRateModel.sequelize.transaction(async t => {
197 const query = {
198 where: {
199 updatedAt: {
200 [Op.lt]: beforeUpdatedAt
201 },
202 videoId,
203 type
204 },
205 transaction: t
206 }
207
208 const deleted = await AccountVideoRateModel.destroy(query)
209
210 const options = {
211 transaction: t,
212 where: {
213 id: videoId
214 }
215 }
216
217 if (type === 'like') await VideoModel.increment({ likes: -deleted }, options)
218 else if (type === 'dislike') await VideoModel.increment({ dislikes: -deleted }, options)
219 })
220 }
221
222 toFormattedJSON (): AccountVideoRate {
223 return {
224 video: this.Video.toFormattedJSON(),
225 rating: this.type
226 }
227 }
161} 228}
diff --git a/server/models/account/account.ts b/server/models/account/account.ts
index 84ef0b30d..2b04acd86 100644
--- a/server/models/account/account.ts
+++ b/server/models/account/account.ts
@@ -1,20 +1,20 @@
1import * as Sequelize from 'sequelize'
2import { 1import {
3 AllowNull, 2 AllowNull,
4 BeforeDestroy, 3 BeforeDestroy,
5 BelongsTo, 4 BelongsTo,
6 Column, 5 Column,
7 CreatedAt, 6 CreatedAt, DataType,
8 Default, 7 Default,
9 DefaultScope, 8 DefaultScope,
10 ForeignKey, 9 ForeignKey,
11 HasMany, 10 HasMany,
12 Is, 11 Is,
13 Model, 12 Model,
13 Scopes,
14 Table, 14 Table,
15 UpdatedAt 15 UpdatedAt
16} from 'sequelize-typescript' 16} from 'sequelize-typescript'
17import { Account } from '../../../shared/models/actors' 17import { Account, AccountSummary } from '../../../shared/models/actors'
18import { isAccountDescriptionValid } from '../../helpers/custom-validators/accounts' 18import { isAccountDescriptionValid } from '../../helpers/custom-validators/accounts'
19import { sendDeleteActor } from '../../lib/activitypub/send' 19import { sendDeleteActor } from '../../lib/activitypub/send'
20import { ActorModel } from '../activitypub/actor' 20import { ActorModel } from '../activitypub/actor'
@@ -24,15 +24,49 @@ import { getSort, throwIfNotValid } from '../utils'
24import { VideoChannelModel } from '../video/video-channel' 24import { VideoChannelModel } from '../video/video-channel'
25import { VideoCommentModel } from '../video/video-comment' 25import { VideoCommentModel } from '../video/video-comment'
26import { UserModel } from './user' 26import { UserModel } from './user'
27import { AvatarModel } from '../avatar/avatar'
28import { VideoPlaylistModel } from '../video/video-playlist'
29import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
30import { Op, Transaction, WhereOptions } from 'sequelize'
27 31
28@DefaultScope({ 32export enum ScopeNames {
33 SUMMARY = 'SUMMARY'
34}
35
36@DefaultScope(() => ({
29 include: [ 37 include: [
30 { 38 {
31 model: () => ActorModel, // Default scope includes avatar and server 39 model: ActorModel, // Default scope includes avatar and server
32 required: true 40 required: true
33 } 41 }
34 ] 42 ]
35}) 43}))
44@Scopes(() => ({
45 [ ScopeNames.SUMMARY ]: (whereActor?: WhereOptions) => {
46 return {
47 attributes: [ 'id', 'name' ],
48 include: [
49 {
50 attributes: [ 'id', 'uuid', 'preferredUsername', 'url', 'serverId', 'avatarId' ],
51 model: ActorModel.unscoped(),
52 required: true,
53 where: whereActor,
54 include: [
55 {
56 attributes: [ 'host' ],
57 model: ServerModel.unscoped(),
58 required: false
59 },
60 {
61 model: AvatarModel.unscoped(),
62 required: false
63 }
64 ]
65 }
66 ]
67 }
68 }
69}))
36@Table({ 70@Table({
37 tableName: 'account', 71 tableName: 'account',
38 indexes: [ 72 indexes: [
@@ -56,8 +90,8 @@ export class AccountModel extends Model<AccountModel> {
56 90
57 @AllowNull(true) 91 @AllowNull(true)
58 @Default(null) 92 @Default(null)
59 @Is('AccountDescription', value => throwIfNotValid(value, isAccountDescriptionValid, 'description')) 93 @Is('AccountDescription', value => throwIfNotValid(value, isAccountDescriptionValid, 'description', true))
60 @Column 94 @Column(DataType.STRING(CONSTRAINTS_FIELDS.USERS.DESCRIPTION.max))
61 description: string 95 description: string
62 96
63 @CreatedAt 97 @CreatedAt
@@ -111,6 +145,15 @@ export class AccountModel extends Model<AccountModel> {
111 }) 145 })
112 VideoChannels: VideoChannelModel[] 146 VideoChannels: VideoChannelModel[]
113 147
148 @HasMany(() => VideoPlaylistModel, {
149 foreignKey: {
150 allowNull: false
151 },
152 onDelete: 'cascade',
153 hooks: true
154 })
155 VideoPlaylists: VideoPlaylistModel[]
156
114 @HasMany(() => VideoCommentModel, { 157 @HasMany(() => VideoCommentModel, {
115 foreignKey: { 158 foreignKey: {
116 allowNull: false 159 allowNull: false
@@ -133,8 +176,8 @@ export class AccountModel extends Model<AccountModel> {
133 return undefined 176 return undefined
134 } 177 }
135 178
136 static load (id: number, transaction?: Sequelize.Transaction) { 179 static load (id: number, transaction?: Transaction) {
137 return AccountModel.findById(id, { transaction }) 180 return AccountModel.findByPk(id, { transaction })
138 } 181 }
139 182
140 static loadByUUID (uuid: string) { 183 static loadByUUID (uuid: string) {
@@ -153,18 +196,26 @@ export class AccountModel extends Model<AccountModel> {
153 return AccountModel.findOne(query) 196 return AccountModel.findOne(query)
154 } 197 }
155 198
199 static loadByNameWithHost (nameWithHost: string) {
200 const [ accountName, host ] = nameWithHost.split('@')
201
202 if (!host || host === WEBSERVER.HOST) return AccountModel.loadLocalByName(accountName)
203
204 return AccountModel.loadByNameAndHost(accountName, host)
205 }
206
156 static loadLocalByName (name: string) { 207 static loadLocalByName (name: string) {
157 const query = { 208 const query = {
158 where: { 209 where: {
159 [ Sequelize.Op.or ]: [ 210 [ Op.or ]: [
160 { 211 {
161 userId: { 212 userId: {
162 [ Sequelize.Op.ne ]: null 213 [ Op.ne ]: null
163 } 214 }
164 }, 215 },
165 { 216 {
166 applicationId: { 217 applicationId: {
167 [ Sequelize.Op.ne ]: null 218 [ Op.ne ]: null
168 } 219 }
169 } 220 }
170 ] 221 ]
@@ -208,7 +259,7 @@ export class AccountModel extends Model<AccountModel> {
208 return AccountModel.findOne(query) 259 return AccountModel.findOne(query)
209 } 260 }
210 261
211 static loadByUrl (url: string, transaction?: Sequelize.Transaction) { 262 static loadByUrl (url: string, transaction?: Transaction) {
212 const query = { 263 const query = {
213 include: [ 264 include: [
214 { 265 {
@@ -276,6 +327,20 @@ export class AccountModel extends Model<AccountModel> {
276 return Object.assign(actor, account) 327 return Object.assign(actor, account)
277 } 328 }
278 329
330 toFormattedSummaryJSON (): AccountSummary {
331 const actor = this.Actor.toFormattedJSON()
332
333 return {
334 id: this.id,
335 uuid: actor.uuid,
336 name: actor.name,
337 displayName: this.getDisplayName(),
338 url: actor.url,
339 host: actor.host,
340 avatar: actor.avatar
341 }
342 }
343
279 toActivityPubObject () { 344 toActivityPubObject () {
280 const obj = this.Actor.toActivityPubObject(this.name, 'Account') 345 const obj = this.Actor.toActivityPubObject(this.name, 'Account')
281 346
diff --git a/server/models/account/user-notification-setting.ts b/server/models/account/user-notification-setting.ts
index f1c3ac223..c2fbc6d23 100644
--- a/server/models/account/user-notification-setting.ts
+++ b/server/models/account/user-notification-setting.ts
@@ -59,6 +59,15 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM
59 @AllowNull(false) 59 @AllowNull(false)
60 @Default(null) 60 @Default(null)
61 @Is( 61 @Is(
62 'UserNotificationSettingVideoAutoBlacklistAsModerator',
63 value => throwIfNotValid(value, isUserNotificationSettingValid, 'videoAutoBlacklistAsModerator')
64 )
65 @Column
66 videoAutoBlacklistAsModerator: UserNotificationSettingValue
67
68 @AllowNull(false)
69 @Default(null)
70 @Is(
62 'UserNotificationSettingBlacklistOnMyVideo', 71 'UserNotificationSettingBlacklistOnMyVideo',
63 value => throwIfNotValid(value, isUserNotificationSettingValid, 'blacklistOnMyVideo') 72 value => throwIfNotValid(value, isUserNotificationSettingValid, 'blacklistOnMyVideo')
64 ) 73 )
@@ -95,6 +104,15 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM
95 @AllowNull(false) 104 @AllowNull(false)
96 @Default(null) 105 @Default(null)
97 @Is( 106 @Is(
107 'UserNotificationSettingNewInstanceFollower',
108 value => throwIfNotValid(value, isUserNotificationSettingValid, 'newInstanceFollower')
109 )
110 @Column
111 newInstanceFollower: UserNotificationSettingValue
112
113 @AllowNull(false)
114 @Default(null)
115 @Is(
98 'UserNotificationSettingNewFollow', 116 'UserNotificationSettingNewFollow',
99 value => throwIfNotValid(value, isUserNotificationSettingValid, 'newFollow') 117 value => throwIfNotValid(value, isUserNotificationSettingValid, 'newFollow')
100 ) 118 )
@@ -139,12 +157,14 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM
139 newCommentOnMyVideo: this.newCommentOnMyVideo, 157 newCommentOnMyVideo: this.newCommentOnMyVideo,
140 newVideoFromSubscription: this.newVideoFromSubscription, 158 newVideoFromSubscription: this.newVideoFromSubscription,
141 videoAbuseAsModerator: this.videoAbuseAsModerator, 159 videoAbuseAsModerator: this.videoAbuseAsModerator,
160 videoAutoBlacklistAsModerator: this.videoAutoBlacklistAsModerator,
142 blacklistOnMyVideo: this.blacklistOnMyVideo, 161 blacklistOnMyVideo: this.blacklistOnMyVideo,
143 myVideoPublished: this.myVideoPublished, 162 myVideoPublished: this.myVideoPublished,
144 myVideoImportFinished: this.myVideoImportFinished, 163 myVideoImportFinished: this.myVideoImportFinished,
145 newUserRegistration: this.newUserRegistration, 164 newUserRegistration: this.newUserRegistration,
146 commentMention: this.commentMention, 165 commentMention: this.commentMention,
147 newFollow: this.newFollow 166 newFollow: this.newFollow,
167 newInstanceFollower: this.newInstanceFollower
148 } 168 }
149 } 169 }
150} 170}
diff --git a/server/models/account/user-notification.ts b/server/models/account/user-notification.ts
index 6cdbb827b..a4f97037b 100644
--- a/server/models/account/user-notification.ts
+++ b/server/models/account/user-notification.ts
@@ -1,17 +1,4 @@
1import { 1import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
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' 2import { UserNotification, UserNotificationType } from '../../../shared'
16import { getSort, throwIfNotValid } from '../utils' 3import { getSort, throwIfNotValid } from '../utils'
17import { isBooleanValid } from '../../helpers/custom-validators/misc' 4import { isBooleanValid } from '../../helpers/custom-validators/misc'
@@ -19,7 +6,7 @@ import { isUserNotificationTypeValid } from '../../helpers/custom-validators/use
19import { UserModel } from './user' 6import { UserModel } from './user'
20import { VideoModel } from '../video/video' 7import { VideoModel } from '../video/video'
21import { VideoCommentModel } from '../video/video-comment' 8import { VideoCommentModel } from '../video/video-comment'
22import { Op } from 'sequelize' 9import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize'
23import { VideoChannelModel } from '../video/video-channel' 10import { VideoChannelModel } from '../video/video-channel'
24import { AccountModel } from './account' 11import { AccountModel } from './account'
25import { VideoAbuseModel } from '../video/video-abuse' 12import { VideoAbuseModel } from '../video/video-abuse'
@@ -37,17 +24,17 @@ enum ScopeNames {
37function buildActorWithAvatarInclude () { 24function buildActorWithAvatarInclude () {
38 return { 25 return {
39 attributes: [ 'preferredUsername' ], 26 attributes: [ 'preferredUsername' ],
40 model: () => ActorModel.unscoped(), 27 model: ActorModel.unscoped(),
41 required: true, 28 required: true,
42 include: [ 29 include: [
43 { 30 {
44 attributes: [ 'filename' ], 31 attributes: [ 'filename' ],
45 model: () => AvatarModel.unscoped(), 32 model: AvatarModel.unscoped(),
46 required: false 33 required: false
47 }, 34 },
48 { 35 {
49 attributes: [ 'host' ], 36 attributes: [ 'host' ],
50 model: () => ServerModel.unscoped(), 37 model: ServerModel.unscoped(),
51 required: false 38 required: false
52 } 39 }
53 ] 40 ]
@@ -57,7 +44,7 @@ function buildActorWithAvatarInclude () {
57function buildVideoInclude (required: boolean) { 44function buildVideoInclude (required: boolean) {
58 return { 45 return {
59 attributes: [ 'id', 'uuid', 'name' ], 46 attributes: [ 'id', 'uuid', 'name' ],
60 model: () => VideoModel.unscoped(), 47 model: VideoModel.unscoped(),
61 required 48 required
62 } 49 }
63} 50}
@@ -66,7 +53,7 @@ function buildChannelInclude (required: boolean, withActor = false) {
66 return { 53 return {
67 required, 54 required,
68 attributes: [ 'id', 'name' ], 55 attributes: [ 'id', 'name' ],
69 model: () => VideoChannelModel.unscoped(), 56 model: VideoChannelModel.unscoped(),
70 include: withActor === true ? [ buildActorWithAvatarInclude() ] : [] 57 include: withActor === true ? [ buildActorWithAvatarInclude() ] : []
71 } 58 }
72} 59}
@@ -75,12 +62,12 @@ function buildAccountInclude (required: boolean, withActor = false) {
75 return { 62 return {
76 required, 63 required,
77 attributes: [ 'id', 'name' ], 64 attributes: [ 'id', 'name' ],
78 model: () => AccountModel.unscoped(), 65 model: AccountModel.unscoped(),
79 include: withActor === true ? [ buildActorWithAvatarInclude() ] : [] 66 include: withActor === true ? [ buildActorWithAvatarInclude() ] : []
80 } 67 }
81} 68}
82 69
83@Scopes({ 70@Scopes(() => ({
84 [ScopeNames.WITH_ALL]: { 71 [ScopeNames.WITH_ALL]: {
85 include: [ 72 include: [
86 Object.assign(buildVideoInclude(false), { 73 Object.assign(buildVideoInclude(false), {
@@ -89,7 +76,7 @@ function buildAccountInclude (required: boolean, withActor = false) {
89 76
90 { 77 {
91 attributes: [ 'id', 'originCommentId' ], 78 attributes: [ 'id', 'originCommentId' ],
92 model: () => VideoCommentModel.unscoped(), 79 model: VideoCommentModel.unscoped(),
93 required: false, 80 required: false,
94 include: [ 81 include: [
95 buildAccountInclude(true, true), 82 buildAccountInclude(true, true),
@@ -99,56 +86,56 @@ function buildAccountInclude (required: boolean, withActor = false) {
99 86
100 { 87 {
101 attributes: [ 'id' ], 88 attributes: [ 'id' ],
102 model: () => VideoAbuseModel.unscoped(), 89 model: VideoAbuseModel.unscoped(),
103 required: false, 90 required: false,
104 include: [ buildVideoInclude(true) ] 91 include: [ buildVideoInclude(true) ]
105 }, 92 },
106 93
107 { 94 {
108 attributes: [ 'id' ], 95 attributes: [ 'id' ],
109 model: () => VideoBlacklistModel.unscoped(), 96 model: VideoBlacklistModel.unscoped(),
110 required: false, 97 required: false,
111 include: [ buildVideoInclude(true) ] 98 include: [ buildVideoInclude(true) ]
112 }, 99 },
113 100
114 { 101 {
115 attributes: [ 'id', 'magnetUri', 'targetUrl', 'torrentName' ], 102 attributes: [ 'id', 'magnetUri', 'targetUrl', 'torrentName' ],
116 model: () => VideoImportModel.unscoped(), 103 model: VideoImportModel.unscoped(),
117 required: false, 104 required: false,
118 include: [ buildVideoInclude(false) ] 105 include: [ buildVideoInclude(false) ]
119 }, 106 },
120 107
121 { 108 {
122 attributes: [ 'id' ], 109 attributes: [ 'id', 'state' ],
123 model: () => ActorFollowModel.unscoped(), 110 model: ActorFollowModel.unscoped(),
124 required: false, 111 required: false,
125 include: [ 112 include: [
126 { 113 {
127 attributes: [ 'preferredUsername' ], 114 attributes: [ 'preferredUsername' ],
128 model: () => ActorModel.unscoped(), 115 model: ActorModel.unscoped(),
129 required: true, 116 required: true,
130 as: 'ActorFollower', 117 as: 'ActorFollower',
131 include: [ 118 include: [
132 { 119 {
133 attributes: [ 'id', 'name' ], 120 attributes: [ 'id', 'name' ],
134 model: () => AccountModel.unscoped(), 121 model: AccountModel.unscoped(),
135 required: true 122 required: true
136 }, 123 },
137 { 124 {
138 attributes: [ 'filename' ], 125 attributes: [ 'filename' ],
139 model: () => AvatarModel.unscoped(), 126 model: AvatarModel.unscoped(),
140 required: false 127 required: false
141 }, 128 },
142 { 129 {
143 attributes: [ 'host' ], 130 attributes: [ 'host' ],
144 model: () => ServerModel.unscoped(), 131 model: ServerModel.unscoped(),
145 required: false 132 required: false
146 } 133 }
147 ] 134 ]
148 }, 135 },
149 { 136 {
150 attributes: [ 'preferredUsername' ], 137 attributes: [ 'preferredUsername' ],
151 model: () => ActorModel.unscoped(), 138 model: ActorModel.unscoped(),
152 required: true, 139 required: true,
153 as: 'ActorFollowing', 140 as: 'ActorFollowing',
154 include: [ 141 include: [
@@ -162,7 +149,7 @@ function buildAccountInclude (required: boolean, withActor = false) {
162 buildAccountInclude(false, true) 149 buildAccountInclude(false, true)
163 ] 150 ]
164 } 151 }
165}) 152}))
166@Table({ 153@Table({
167 tableName: 'userNotification', 154 tableName: 'userNotification',
168 indexes: [ 155 indexes: [
@@ -225,7 +212,7 @@ function buildAccountInclude (required: boolean, withActor = false) {
225 } 212 }
226 } 213 }
227 } 214 }
228 ] 215 ] as (ModelIndexesOptions & { where?: WhereOptions })[]
229}) 216})
230export class UserNotificationModel extends Model<UserNotificationModel> { 217export class UserNotificationModel extends Model<UserNotificationModel> {
231 218
@@ -344,7 +331,7 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
344 ActorFollow: ActorFollowModel 331 ActorFollow: ActorFollowModel
345 332
346 static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) { 333 static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) {
347 const query: IFindOptions<UserNotificationModel> = { 334 const query: FindOptions = {
348 offset: start, 335 offset: start,
349 limit: count, 336 limit: count,
350 order: getSort(sort), 337 order: getSort(sort),
@@ -370,7 +357,7 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
370 where: { 357 where: {
371 userId, 358 userId,
372 id: { 359 id: {
373 [Op.any]: notificationIds 360 [Op.in]: notificationIds // FIXME: sequelize ANY seems broken
374 } 361 }
375 } 362 }
376 } 363 }
@@ -418,6 +405,7 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
418 405
419 const actorFollow = this.ActorFollow ? { 406 const actorFollow = this.ActorFollow ? {
420 id: this.ActorFollow.id, 407 id: this.ActorFollow.id,
408 state: this.ActorFollow.state,
421 follower: { 409 follower: {
422 id: this.ActorFollow.ActorFollower.Account.id, 410 id: this.ActorFollow.ActorFollower.Account.id,
423 displayName: this.ActorFollow.ActorFollower.Account.getDisplayName(), 411 displayName: this.ActorFollow.ActorFollower.Account.getDisplayName(),
diff --git a/server/models/account/user-video-history.ts b/server/models/account/user-video-history.ts
index 15cb399c9..a862fc45f 100644
--- a/server/models/account/user-video-history.ts
+++ b/server/models/account/user-video-history.ts
@@ -67,7 +67,7 @@ export class UserVideoHistoryModel extends Model<UserVideoHistoryModel> {
67 }) 67 })
68 } 68 }
69 69
70 static removeHistoryBefore (user: UserModel, beforeDate: string, t: Transaction) { 70 static removeUserHistoryBefore (user: UserModel, beforeDate: string, t: Transaction) {
71 const query: DestroyOptions = { 71 const query: DestroyOptions = {
72 where: { 72 where: {
73 userId: user.id 73 userId: user.id
@@ -76,11 +76,23 @@ export class UserVideoHistoryModel extends Model<UserVideoHistoryModel> {
76 } 76 }
77 77
78 if (beforeDate) { 78 if (beforeDate) {
79 query.where.updatedAt = { 79 query.where['updatedAt'] = {
80 [Op.lt]: beforeDate 80 [Op.lt]: beforeDate
81 } 81 }
82 } 82 }
83 83
84 return UserVideoHistoryModel.destroy(query) 84 return UserVideoHistoryModel.destroy(query)
85 } 85 }
86
87 static removeOldHistory (beforeDate: string) {
88 const query: DestroyOptions = {
89 where: {
90 updatedAt: {
91 [Op.lt]: beforeDate
92 }
93 }
94 }
95
96 return UserVideoHistoryModel.destroy(query)
97 }
86} 98}
diff --git a/server/models/account/user.ts b/server/models/account/user.ts
index 017a96657..4a9acd703 100644
--- a/server/models/account/user.ts
+++ b/server/models/account/user.ts
@@ -1,4 +1,4 @@
1import * as Sequelize from 'sequelize' 1import { FindOptions, literal, Op, QueryTypes } from 'sequelize'
2import { 2import {
3 AfterDestroy, 3 AfterDestroy,
4 AfterUpdate, 4 AfterUpdate,
@@ -22,6 +22,7 @@ import {
22import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared' 22import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared'
23import { User, UserRole } from '../../../shared/models/users' 23import { User, UserRole } from '../../../shared/models/users'
24import { 24import {
25 isUserAdminFlagsValid,
25 isUserAutoPlayVideoValid, 26 isUserAutoPlayVideoValid,
26 isUserBlockedReasonValid, 27 isUserBlockedReasonValid,
27 isUserBlockedValid, 28 isUserBlockedValid,
@@ -42,45 +43,46 @@ import { VideoChannelModel } from '../video/video-channel'
42import { AccountModel } from './account' 43import { AccountModel } from './account'
43import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type' 44import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type'
44import { values } from 'lodash' 45import { values } from 'lodash'
45import { NSFW_POLICY_TYPES } from '../../initializers' 46import { NSFW_POLICY_TYPES } from '../../initializers/constants'
46import { clearCacheByUserId } from '../../lib/oauth-model' 47import { clearCacheByUserId } from '../../lib/oauth-model'
47import { UserNotificationSettingModel } from './user-notification-setting' 48import { UserNotificationSettingModel } from './user-notification-setting'
48import { VideoModel } from '../video/video' 49import { VideoModel } from '../video/video'
49import { ActorModel } from '../activitypub/actor' 50import { ActorModel } from '../activitypub/actor'
50import { ActorFollowModel } from '../activitypub/actor-follow' 51import { ActorFollowModel } from '../activitypub/actor-follow'
51import { VideoImportModel } from '../video/video-import' 52import { VideoImportModel } from '../video/video-import'
53import { UserAdminFlag } from '../../../shared/models/users/user-flag.model'
52 54
53enum ScopeNames { 55enum ScopeNames {
54 WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL' 56 WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL'
55} 57}
56 58
57@DefaultScope({ 59@DefaultScope(() => ({
58 include: [ 60 include: [
59 { 61 {
60 model: () => AccountModel, 62 model: AccountModel,
61 required: true 63 required: true
62 }, 64 },
63 { 65 {
64 model: () => UserNotificationSettingModel, 66 model: UserNotificationSettingModel,
65 required: true 67 required: true
66 } 68 }
67 ] 69 ]
68}) 70}))
69@Scopes({ 71@Scopes(() => ({
70 [ScopeNames.WITH_VIDEO_CHANNEL]: { 72 [ScopeNames.WITH_VIDEO_CHANNEL]: {
71 include: [ 73 include: [
72 { 74 {
73 model: () => AccountModel, 75 model: AccountModel,
74 required: true, 76 required: true,
75 include: [ () => VideoChannelModel ] 77 include: [ VideoChannelModel ]
76 }, 78 },
77 { 79 {
78 model: () => UserNotificationSettingModel, 80 model: UserNotificationSettingModel,
79 required: true 81 required: true
80 } 82 }
81 ] 83 ]
82 } 84 }
83}) 85}))
84@Table({ 86@Table({
85 tableName: 'user', 87 tableName: 'user',
86 indexes: [ 88 indexes: [
@@ -113,13 +115,13 @@ export class UserModel extends Model<UserModel> {
113 115
114 @AllowNull(true) 116 @AllowNull(true)
115 @Default(null) 117 @Default(null)
116 @Is('UserEmailVerified', value => throwIfNotValid(value, isUserEmailVerifiedValid, 'email verified boolean')) 118 @Is('UserEmailVerified', value => throwIfNotValid(value, isUserEmailVerifiedValid, 'email verified boolean', true))
117 @Column 119 @Column
118 emailVerified: boolean 120 emailVerified: boolean
119 121
120 @AllowNull(false) 122 @AllowNull(false)
121 @Is('UserNSFWPolicy', value => throwIfNotValid(value, isUserNSFWPolicyValid, 'NSFW policy')) 123 @Is('UserNSFWPolicy', value => throwIfNotValid(value, isUserNSFWPolicyValid, 'NSFW policy'))
122 @Column(DataType.ENUM(values(NSFW_POLICY_TYPES))) 124 @Column(DataType.ENUM(...values(NSFW_POLICY_TYPES)))
123 nsfwPolicy: NSFWPolicyType 125 nsfwPolicy: NSFWPolicyType
124 126
125 @AllowNull(false) 127 @AllowNull(false)
@@ -141,6 +143,12 @@ export class UserModel extends Model<UserModel> {
141 autoPlayVideo: boolean 143 autoPlayVideo: boolean
142 144
143 @AllowNull(false) 145 @AllowNull(false)
146 @Default(UserAdminFlag.NONE)
147 @Is('UserAdminFlags', value => throwIfNotValid(value, isUserAdminFlagsValid, 'user admin flags'))
148 @Column
149 adminFlags?: UserAdminFlag
150
151 @AllowNull(false)
144 @Default(false) 152 @Default(false)
145 @Is('UserBlocked', value => throwIfNotValid(value, isUserBlockedValid, 'blocked boolean')) 153 @Is('UserBlocked', value => throwIfNotValid(value, isUserBlockedValid, 'blocked boolean'))
146 @Column 154 @Column
@@ -148,7 +156,7 @@ export class UserModel extends Model<UserModel> {
148 156
149 @AllowNull(true) 157 @AllowNull(true)
150 @Default(null) 158 @Default(null)
151 @Is('UserBlockedReason', value => throwIfNotValid(value, isUserBlockedReasonValid, 'blocked reason')) 159 @Is('UserBlockedReason', value => throwIfNotValid(value, isUserBlockedReasonValid, 'blocked reason', true))
152 @Column 160 @Column
153 blockedReason: string 161 blockedReason: string
154 162
@@ -225,26 +233,26 @@ export class UserModel extends Model<UserModel> {
225 let where = undefined 233 let where = undefined
226 if (search) { 234 if (search) {
227 where = { 235 where = {
228 [Sequelize.Op.or]: [ 236 [Op.or]: [
229 { 237 {
230 email: { 238 email: {
231 [Sequelize.Op.iLike]: '%' + search + '%' 239 [Op.iLike]: '%' + search + '%'
232 } 240 }
233 }, 241 },
234 { 242 {
235 username: { 243 username: {
236 [ Sequelize.Op.iLike ]: '%' + search + '%' 244 [ Op.iLike ]: '%' + search + '%'
237 } 245 }
238 } 246 }
239 ] 247 ]
240 } 248 }
241 } 249 }
242 250
243 const query = { 251 const query: FindOptions = {
244 attributes: { 252 attributes: {
245 include: [ 253 include: [
246 [ 254 [
247 Sequelize.literal( 255 literal(
248 '(' + 256 '(' +
249 'SELECT COALESCE(SUM("size"), 0) ' + 257 'SELECT COALESCE(SUM("size"), 0) ' +
250 'FROM (' + 258 'FROM (' +
@@ -257,7 +265,7 @@ export class UserModel extends Model<UserModel> {
257 ')' 265 ')'
258 ), 266 ),
259 'videoQuotaUsed' 267 'videoQuotaUsed'
260 ] as any // FIXME: typings 268 ]
261 ] 269 ]
262 }, 270 },
263 offset: start, 271 offset: start,
@@ -283,7 +291,7 @@ export class UserModel extends Model<UserModel> {
283 const query = { 291 const query = {
284 where: { 292 where: {
285 role: { 293 role: {
286 [Sequelize.Op.in]: roles 294 [Op.in]: roles
287 } 295 }
288 } 296 }
289 } 297 }
@@ -341,7 +349,7 @@ export class UserModel extends Model<UserModel> {
341 } 349 }
342 350
343 static loadById (id: number) { 351 static loadById (id: number) {
344 return UserModel.findById(id) 352 return UserModel.findByPk(id)
345 } 353 }
346 354
347 static loadByUsername (username: string) { 355 static loadByUsername (username: string) {
@@ -379,7 +387,7 @@ export class UserModel extends Model<UserModel> {
379 387
380 const query = { 388 const query = {
381 where: { 389 where: {
382 [ Sequelize.Op.or ]: [ { username }, { email } ] 390 [ Op.or ]: [ { username }, { email } ]
383 } 391 }
384 } 392 }
385 393
@@ -502,7 +510,7 @@ export class UserModel extends Model<UserModel> {
502 const query = { 510 const query = {
503 where: { 511 where: {
504 username: { 512 username: {
505 [ Sequelize.Op.like ]: `%${search}%` 513 [ Op.like ]: `%${search}%`
506 } 514 }
507 }, 515 },
508 limit: 10 516 limit: 10
@@ -516,11 +524,15 @@ export class UserModel extends Model<UserModel> {
516 return hasUserRight(this.role, right) 524 return hasUserRight(this.role, right)
517 } 525 }
518 526
527 hasAdminFlag (flag: UserAdminFlag) {
528 return this.adminFlags & flag
529 }
530
519 isPasswordMatch (password: string) { 531 isPasswordMatch (password: string) {
520 return comparePassword(password, this.password) 532 return comparePassword(password, this.password)
521 } 533 }
522 534
523 toFormattedJSON (): User { 535 toFormattedJSON (parameters: { withAdminFlags?: boolean } = {}): User {
524 const videoQuotaUsed = this.get('videoQuotaUsed') 536 const videoQuotaUsed = this.get('videoQuotaUsed')
525 const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily') 537 const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily')
526 538
@@ -544,13 +556,17 @@ export class UserModel extends Model<UserModel> {
544 notificationSettings: this.NotificationSetting ? this.NotificationSetting.toFormattedJSON() : undefined, 556 notificationSettings: this.NotificationSetting ? this.NotificationSetting.toFormattedJSON() : undefined,
545 videoChannels: [], 557 videoChannels: [],
546 videoQuotaUsed: videoQuotaUsed !== undefined 558 videoQuotaUsed: videoQuotaUsed !== undefined
547 ? parseInt(videoQuotaUsed, 10) 559 ? parseInt(videoQuotaUsed + '', 10)
548 : undefined, 560 : undefined,
549 videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined 561 videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined
550 ? parseInt(videoQuotaUsedDaily, 10) 562 ? parseInt(videoQuotaUsedDaily + '', 10)
551 : undefined 563 : undefined
552 } 564 }
553 565
566 if (parameters.withAdminFlags) {
567 Object.assign(json, { adminFlags: this.adminFlags })
568 }
569
554 if (Array.isArray(this.Account.VideoChannels) === true) { 570 if (Array.isArray(this.Account.VideoChannels) === true) {
555 json.videoChannels = this.Account.VideoChannels 571 json.videoChannels = this.Account.VideoChannels
556 .map(c => c.toFormattedJSON()) 572 .map(c => c.toFormattedJSON())
@@ -575,15 +591,11 @@ export class UserModel extends Model<UserModel> {
575 591
576 const uploadedTotal = videoFile.size + totalBytes 592 const uploadedTotal = videoFile.size + totalBytes
577 const uploadedDaily = videoFile.size + totalBytesDaily 593 const uploadedDaily = videoFile.size + totalBytesDaily
578 if (this.videoQuotaDaily === -1) {
579 return uploadedTotal < this.videoQuota
580 }
581 if (this.videoQuota === -1) {
582 return uploadedDaily < this.videoQuotaDaily
583 }
584 594
585 return (uploadedTotal < this.videoQuota) && 595 if (this.videoQuotaDaily === -1) return uploadedTotal < this.videoQuota
586 (uploadedDaily < this.videoQuotaDaily) 596 if (this.videoQuota === -1) return uploadedDaily < this.videoQuotaDaily
597
598 return uploadedTotal < this.videoQuota && uploadedDaily < this.videoQuotaDaily
587 } 599 }
588 600
589 private static generateUserQuotaBaseSQL (where?: string) { 601 private static generateUserQuotaBaseSQL (where?: string) {
@@ -603,10 +615,10 @@ export class UserModel extends Model<UserModel> {
603 private static getTotalRawQuery (query: string, userId: number) { 615 private static getTotalRawQuery (query: string, userId: number) {
604 const options = { 616 const options = {
605 bind: { userId }, 617 bind: { userId },
606 type: Sequelize.QueryTypes.SELECT 618 type: QueryTypes.SELECT as QueryTypes.SELECT
607 } 619 }
608 620
609 return UserModel.sequelize.query(query, options) 621 return UserModel.sequelize.query<{ total: string }>(query, options)
610 .then(([ { total } ]) => { 622 .then(([ { total } ]) => {
611 if (total === null) return 0 623 if (total === null) return 0
612 624
diff --git a/server/models/activitypub/actor-follow.ts b/server/models/activitypub/actor-follow.ts
index 796e07a42..b0461b981 100644
--- a/server/models/activitypub/actor-follow.ts
+++ b/server/models/activitypub/actor-follow.ts
@@ -1,6 +1,5 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { values } from 'lodash' 2import { values } from 'lodash'
3import * as Sequelize from 'sequelize'
4import { 3import {
5 AfterCreate, 4 AfterCreate,
6 AfterDestroy, 5 AfterDestroy,
@@ -22,14 +21,13 @@ import { FollowState } from '../../../shared/models/actors'
22import { ActorFollow } from '../../../shared/models/actors/follow.model' 21import { ActorFollow } from '../../../shared/models/actors/follow.model'
23import { logger } from '../../helpers/logger' 22import { logger } from '../../helpers/logger'
24import { getServerActor } from '../../helpers/utils' 23import { getServerActor } from '../../helpers/utils'
25import { ACTOR_FOLLOW_SCORE } from '../../initializers' 24import { ACTOR_FOLLOW_SCORE, FOLLOW_STATES } from '../../initializers/constants'
26import { FOLLOW_STATES } from '../../initializers/constants'
27import { ServerModel } from '../server/server' 25import { ServerModel } from '../server/server'
28import { getSort } from '../utils' 26import { getSort } from '../utils'
29import { ActorModel, unusedActorAttributesForAPI } from './actor' 27import { ActorModel, unusedActorAttributesForAPI } from './actor'
30import { VideoChannelModel } from '../video/video-channel' 28import { VideoChannelModel } from '../video/video-channel'
31import { IIncludeOptions } from '../../../node_modules/sequelize-typescript/lib/interfaces/IIncludeOptions'
32import { AccountModel } from '../account/account' 29import { AccountModel } from '../account/account'
30import { IncludeOptions, Op, Transaction, QueryTypes } from 'sequelize'
33 31
34@Table({ 32@Table({
35 tableName: 'actorFollow', 33 tableName: 'actorFollow',
@@ -52,7 +50,7 @@ import { AccountModel } from '../account/account'
52export class ActorFollowModel extends Model<ActorFollowModel> { 50export class ActorFollowModel extends Model<ActorFollowModel> {
53 51
54 @AllowNull(false) 52 @AllowNull(false)
55 @Column(DataType.ENUM(values(FOLLOW_STATES))) 53 @Column(DataType.ENUM(...values(FOLLOW_STATES)))
56 state: FollowState 54 state: FollowState
57 55
58 @AllowNull(false) 56 @AllowNull(false)
@@ -127,7 +125,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
127 if (numberOfActorFollowsRemoved) logger.info('Removed bad %d actor follows.', numberOfActorFollowsRemoved) 125 if (numberOfActorFollowsRemoved) logger.info('Removed bad %d actor follows.', numberOfActorFollowsRemoved)
128 } 126 }
129 127
130 static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Sequelize.Transaction) { 128 static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Transaction) {
131 const query = { 129 const query = {
132 where: { 130 where: {
133 actorId, 131 actorId,
@@ -151,8 +149,8 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
151 return ActorFollowModel.findOne(query) 149 return ActorFollowModel.findOne(query)
152 } 150 }
153 151
154 static loadByActorAndTargetNameAndHostForAPI (actorId: number, targetName: string, targetHost: string, t?: Sequelize.Transaction) { 152 static loadByActorAndTargetNameAndHostForAPI (actorId: number, targetName: string, targetHost: string, t?: Transaction) {
155 const actorFollowingPartInclude: IIncludeOptions = { 153 const actorFollowingPartInclude: IncludeOptions = {
156 model: ActorModel, 154 model: ActorModel,
157 required: true, 155 required: true,
158 as: 'ActorFollowing', 156 as: 'ActorFollowing',
@@ -209,7 +207,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
209 .map(t => { 207 .map(t => {
210 if (t.host) { 208 if (t.host) {
211 return { 209 return {
212 [ Sequelize.Op.and ]: [ 210 [ Op.and ]: [
213 { 211 {
214 '$preferredUsername$': t.name 212 '$preferredUsername$': t.name
215 }, 213 },
@@ -221,7 +219,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
221 } 219 }
222 220
223 return { 221 return {
224 [ Sequelize.Op.and ]: [ 222 [ Op.and ]: [
225 { 223 {
226 '$preferredUsername$': t.name 224 '$preferredUsername$': t.name
227 }, 225 },
@@ -235,9 +233,9 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
235 const query = { 233 const query = {
236 attributes: [], 234 attributes: [],
237 where: { 235 where: {
238 [ Sequelize.Op.and ]: [ 236 [ Op.and ]: [
239 { 237 {
240 [ Sequelize.Op.or ]: whereTab 238 [ Op.or ]: whereTab
241 }, 239 },
242 { 240 {
243 actorId 241 actorId
@@ -289,7 +287,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
289 required: true, 287 required: true,
290 where: search ? { 288 where: search ? {
291 host: { 289 host: {
292 [Sequelize.Op.iLike]: '%' + search + '%' 290 [Op.iLike]: '%' + search + '%'
293 } 291 }
294 } : undefined 292 } : undefined
295 } 293 }
@@ -324,7 +322,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
324 required: true, 322 required: true,
325 where: search ? { 323 where: search ? {
326 host: { 324 host: {
327 [ Sequelize.Op.iLike ]: '%' + search + '%' 325 [ Op.iLike ]: '%' + search + '%'
328 } 326 }
329 } : undefined 327 } : undefined
330 } 328 }
@@ -407,11 +405,11 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
407 }) 405 })
408 } 406 }
409 407
410 static listAcceptedFollowerUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) { 408 static listAcceptedFollowerUrlsForAP (actorIds: number[], t: Transaction, start?: number, count?: number) {
411 return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count) 409 return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count)
412 } 410 }
413 411
414 static listAcceptedFollowerSharedInboxUrls (actorIds: number[], t: Sequelize.Transaction) { 412 static listAcceptedFollowerSharedInboxUrls (actorIds: number[], t: Transaction) {
415 return ActorFollowModel.createListAcceptedFollowForApiQuery( 413 return ActorFollowModel.createListAcceptedFollowForApiQuery(
416 'followers', 414 'followers',
417 actorIds, 415 actorIds,
@@ -423,7 +421,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
423 ) 421 )
424 } 422 }
425 423
426 static listAcceptedFollowingUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) { 424 static listAcceptedFollowingUrlsForApi (actorIds: number[], t: Transaction, start?: number, count?: number) {
427 return ActorFollowModel.createListAcceptedFollowForApiQuery('following', actorIds, t, start, count) 425 return ActorFollowModel.createListAcceptedFollowForApiQuery('following', actorIds, t, start, count)
428 } 426 }
429 427
@@ -448,7 +446,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
448 } 446 }
449 } 447 }
450 448
451 static updateFollowScore (inboxUrl: string, value: number, t?: Sequelize.Transaction) { 449 static updateFollowScore (inboxUrl: string, value: number, t?: Transaction) {
452 const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` + 450 const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` +
453 'WHERE id IN (' + 451 'WHERE id IN (' +
454 'SELECT "actorFollow"."id" FROM "actorFollow" ' + 452 'SELECT "actorFollow"."id" FROM "actorFollow" ' +
@@ -457,7 +455,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
457 ')' 455 ')'
458 456
459 const options = { 457 const options = {
460 type: Sequelize.QueryTypes.BULKUPDATE, 458 type: QueryTypes.BULKUPDATE,
461 transaction: t 459 transaction: t
462 } 460 }
463 461
@@ -467,7 +465,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
467 private static async createListAcceptedFollowForApiQuery ( 465 private static async createListAcceptedFollowForApiQuery (
468 type: 'followers' | 'following', 466 type: 'followers' | 'following',
469 actorIds: number[], 467 actorIds: number[],
470 t: Sequelize.Transaction, 468 t: Transaction,
471 start?: number, 469 start?: number,
472 count?: number, 470 count?: number,
473 columnUrl = 'url', 471 columnUrl = 'url',
@@ -503,7 +501,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
503 501
504 const options = { 502 const options = {
505 bind: { actorIds }, 503 bind: { actorIds },
506 type: Sequelize.QueryTypes.SELECT, 504 type: QueryTypes.SELECT,
507 transaction: t 505 transaction: t
508 } 506 }
509 tasks.push(ActorFollowModel.sequelize.query(query, options)) 507 tasks.push(ActorFollowModel.sequelize.query(query, options))
@@ -522,7 +520,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
522 const query = { 520 const query = {
523 where: { 521 where: {
524 score: { 522 score: {
525 [Sequelize.Op.lte]: 0 523 [Op.lte]: 0
526 } 524 }
527 }, 525 },
528 logging: false 526 logging: false
diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts
index dda57a8ba..4a466441c 100644
--- a/server/models/activitypub/actor.ts
+++ b/server/models/activitypub/actor.ts
@@ -30,11 +30,11 @@ import {
30 isActorPublicKeyValid 30 isActorPublicKeyValid
31} from '../../helpers/custom-validators/activitypub/actor' 31} from '../../helpers/custom-validators/activitypub/actor'
32import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 32import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
33import { ACTIVITY_PUB, ACTIVITY_PUB_ACTOR_TYPES, CONFIG, CONSTRAINTS_FIELDS } from '../../initializers' 33import { ACTIVITY_PUB, ACTIVITY_PUB_ACTOR_TYPES, CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
34import { AccountModel } from '../account/account' 34import { AccountModel } from '../account/account'
35import { AvatarModel } from '../avatar/avatar' 35import { AvatarModel } from '../avatar/avatar'
36import { ServerModel } from '../server/server' 36import { ServerModel } from '../server/server'
37import { throwIfNotValid } from '../utils' 37import { isOutdated, throwIfNotValid } from '../utils'
38import { VideoChannelModel } from '../video/video-channel' 38import { VideoChannelModel } from '../video/video-channel'
39import { ActorFollowModel } from './actor-follow' 39import { ActorFollowModel } from './actor-follow'
40import { VideoModel } from '../video/video' 40import { VideoModel } from '../video/video'
@@ -56,46 +56,46 @@ export const unusedActorAttributesForAPI = [
56 'updatedAt' 56 'updatedAt'
57] 57]
58 58
59@DefaultScope({ 59@DefaultScope(() => ({
60 include: [ 60 include: [
61 { 61 {
62 model: () => ServerModel, 62 model: ServerModel,
63 required: false 63 required: false
64 }, 64 },
65 { 65 {
66 model: () => AvatarModel, 66 model: AvatarModel,
67 required: false 67 required: false
68 } 68 }
69 ] 69 ]
70}) 70}))
71@Scopes({ 71@Scopes(() => ({
72 [ScopeNames.FULL]: { 72 [ScopeNames.FULL]: {
73 include: [ 73 include: [
74 { 74 {
75 model: () => AccountModel.unscoped(), 75 model: AccountModel.unscoped(),
76 required: false 76 required: false
77 }, 77 },
78 { 78 {
79 model: () => VideoChannelModel.unscoped(), 79 model: VideoChannelModel.unscoped(),
80 required: false, 80 required: false,
81 include: [ 81 include: [
82 { 82 {
83 model: () => AccountModel, 83 model: AccountModel,
84 required: true 84 required: true
85 } 85 }
86 ] 86 ]
87 }, 87 },
88 { 88 {
89 model: () => ServerModel, 89 model: ServerModel,
90 required: false 90 required: false
91 }, 91 },
92 { 92 {
93 model: () => AvatarModel, 93 model: AvatarModel,
94 required: false 94 required: false
95 } 95 }
96 ] 96 ]
97 } 97 }
98}) 98}))
99@Table({ 99@Table({
100 tableName: 'actor', 100 tableName: 'actor',
101 indexes: [ 101 indexes: [
@@ -131,7 +131,7 @@ export const unusedActorAttributesForAPI = [
131export class ActorModel extends Model<ActorModel> { 131export class ActorModel extends Model<ActorModel> {
132 132
133 @AllowNull(false) 133 @AllowNull(false)
134 @Column(DataType.ENUM(values(ACTIVITY_PUB_ACTOR_TYPES))) 134 @Column(DataType.ENUM(...values(ACTIVITY_PUB_ACTOR_TYPES)))
135 type: ActivityPubActorType 135 type: ActivityPubActorType
136 136
137 @AllowNull(false) 137 @AllowNull(false)
@@ -151,12 +151,12 @@ export class ActorModel extends Model<ActorModel> {
151 url: string 151 url: string
152 152
153 @AllowNull(true) 153 @AllowNull(true)
154 @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPublicKeyValid, 'public key')) 154 @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPublicKeyValid, 'public key', true))
155 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.PUBLIC_KEY.max)) 155 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.PUBLIC_KEY.max))
156 publicKey: string 156 publicKey: string
157 157
158 @AllowNull(true) 158 @AllowNull(true)
159 @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPrivateKeyValid, 'private key')) 159 @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPrivateKeyValid, 'private key', true))
160 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.PRIVATE_KEY.max)) 160 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.PRIVATE_KEY.max))
161 privateKey: string 161 privateKey: string
162 162
@@ -265,7 +265,7 @@ export class ActorModel extends Model<ActorModel> {
265 VideoChannel: VideoChannelModel 265 VideoChannel: VideoChannelModel
266 266
267 static load (id: number) { 267 static load (id: number) {
268 return ActorModel.unscoped().findById(id) 268 return ActorModel.unscoped().findByPk(id)
269 } 269 }
270 270
271 static loadAccountActorByVideoId (videoId: number, transaction: Sequelize.Transaction) { 271 static loadAccountActorByVideoId (videoId: number, transaction: Sequelize.Transaction) {
@@ -280,14 +280,16 @@ export class ActorModel extends Model<ActorModel> {
280 attributes: [ 'id' ], 280 attributes: [ 'id' ],
281 model: VideoChannelModel.unscoped(), 281 model: VideoChannelModel.unscoped(),
282 required: true, 282 required: true,
283 include: { 283 include: [
284 attributes: [ 'id' ], 284 {
285 model: VideoModel.unscoped(), 285 attributes: [ 'id' ],
286 required: true, 286 model: VideoModel.unscoped(),
287 where: { 287 required: true,
288 id: videoId 288 where: {
289 id: videoId
290 }
289 } 291 }
290 } 292 ]
291 } 293 }
292 ] 294 ]
293 } 295 }
@@ -295,7 +297,7 @@ export class ActorModel extends Model<ActorModel> {
295 transaction 297 transaction
296 } 298 }
297 299
298 return ActorModel.unscoped().findOne(query as any) // FIXME: typings 300 return ActorModel.unscoped().findOne(query)
299 } 301 }
300 302
301 static isActorUrlExist (url: string) { 303 static isActorUrlExist (url: string) {
@@ -389,8 +391,7 @@ export class ActorModel extends Model<ActorModel> {
389 } 391 }
390 392
391 static incrementFollows (id: number, column: 'followersCount' | 'followingCount', by: number) { 393 static incrementFollows (id: number, column: 'followersCount' | 'followingCount', by: number) {
392 // FIXME: typings 394 return ActorModel.increment(column, {
393 return (ActorModel as any).increment(column, {
394 by, 395 by,
395 where: { 396 where: {
396 id 397 id
@@ -444,6 +445,7 @@ export class ActorModel extends Model<ActorModel> {
444 id: this.url, 445 id: this.url,
445 following: this.getFollowingUrl(), 446 following: this.getFollowingUrl(),
446 followers: this.getFollowersUrl(), 447 followers: this.getFollowersUrl(),
448 playlists: this.getPlaylistsUrl(),
447 inbox: this.inboxUrl, 449 inbox: this.inboxUrl,
448 outbox: this.outboxUrl, 450 outbox: this.outboxUrl,
449 preferredUsername: this.preferredUsername, 451 preferredUsername: this.preferredUsername,
@@ -494,6 +496,10 @@ export class ActorModel extends Model<ActorModel> {
494 return this.url + '/followers' 496 return this.url + '/followers'
495 } 497 }
496 498
499 getPlaylistsUrl () {
500 return this.url + '/playlists'
501 }
502
497 getPublicKeyUrl () { 503 getPublicKeyUrl () {
498 return this.url + '#main-key' 504 return this.url + '#main-key'
499 } 505 }
@@ -511,7 +517,7 @@ export class ActorModel extends Model<ActorModel> {
511 } 517 }
512 518
513 getHost () { 519 getHost () {
514 return this.Server ? this.Server.host : CONFIG.WEBSERVER.HOST 520 return this.Server ? this.Server.host : WEBSERVER.HOST
515 } 521 }
516 522
517 getRedundancyAllowed () { 523 getRedundancyAllowed () {
@@ -521,17 +527,12 @@ export class ActorModel extends Model<ActorModel> {
521 getAvatarUrl () { 527 getAvatarUrl () {
522 if (!this.avatarId) return undefined 528 if (!this.avatarId) return undefined
523 529
524 return CONFIG.WEBSERVER.URL + this.Avatar.getWebserverPath() 530 return WEBSERVER.URL + this.Avatar.getWebserverPath()
525 } 531 }
526 532
527 isOutdated () { 533 isOutdated () {
528 if (this.isOwned()) return false 534 if (this.isOwned()) return false
529 535
530 const now = Date.now() 536 return isOutdated(this, ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL)
531 const createdAtTime = this.createdAt.getTime()
532 const updatedAtTime = this.updatedAt.getTime()
533
534 return (now - createdAtTime) > ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL &&
535 (now - updatedAtTime) > ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL
536 } 537 }
537} 538}
diff --git a/server/models/application/application.ts b/server/models/application/application.ts
index 854a5fb36..81320b9af 100644
--- a/server/models/application/application.ts
+++ b/server/models/application/application.ts
@@ -1,16 +1,17 @@
1import { AllowNull, Column, Default, DefaultScope, HasOne, IsInt, Model, Table } from 'sequelize-typescript' 1import { AllowNull, Column, Default, DefaultScope, HasOne, IsInt, Model, Table } from 'sequelize-typescript'
2import { AccountModel } from '../account/account' 2import { AccountModel } from '../account/account'
3 3
4@DefaultScope({ 4@DefaultScope(() => ({
5 include: [ 5 include: [
6 { 6 {
7 model: () => AccountModel, 7 model: AccountModel,
8 required: true 8 required: true
9 } 9 }
10 ] 10 ]
11}) 11}))
12@Table({ 12@Table({
13 tableName: 'application' 13 tableName: 'application',
14 timestamps: false
14}) 15})
15export class ApplicationModel extends Model<ApplicationModel> { 16export class ApplicationModel extends Model<ApplicationModel> {
16 17
diff --git a/server/models/avatar/avatar.ts b/server/models/avatar/avatar.ts
index 303aebcc2..aaf1b8bd9 100644
--- a/server/models/avatar/avatar.ts
+++ b/server/models/avatar/avatar.ts
@@ -1,9 +1,10 @@
1import { join } from 'path' 1import { join } from 'path'
2import { AfterDestroy, AllowNull, Column, CreatedAt, Model, Table, UpdatedAt } from 'sequelize-typescript' 2import { AfterDestroy, AllowNull, Column, CreatedAt, Model, Table, UpdatedAt } from 'sequelize-typescript'
3import { Avatar } from '../../../shared/models/avatars/avatar.model' 3import { Avatar } from '../../../shared/models/avatars/avatar.model'
4import { CONFIG, STATIC_PATHS } from '../../initializers' 4import { STATIC_PATHS } from '../../initializers/constants'
5import { logger } from '../../helpers/logger' 5import { logger } from '../../helpers/logger'
6import { remove } from 'fs-extra' 6import { remove } from 'fs-extra'
7import { CONFIG } from '../../initializers/config'
7 8
8@Table({ 9@Table({
9 tableName: 'avatar' 10 tableName: 'avatar'
diff --git a/server/models/migrations.ts b/server/models/migrations.ts
index 24badb166..6c11332a1 100644
--- a/server/models/migrations.ts
+++ b/server/models/migrations.ts
@@ -1,24 +1,24 @@
1import * as Sequelize from 'sequelize' 1import { ModelAttributeColumnOptions } from 'sequelize'
2 2
3declare namespace Migration { 3declare namespace Migration {
4 interface Boolean extends Sequelize.DefineAttributeColumnOptions { 4 interface Boolean extends ModelAttributeColumnOptions {
5 defaultValue: boolean | null 5 defaultValue: boolean | null
6 } 6 }
7 7
8 interface String extends Sequelize.DefineAttributeColumnOptions { 8 interface String extends ModelAttributeColumnOptions {
9 defaultValue: string | null 9 defaultValue: string | null
10 } 10 }
11 11
12 interface Integer extends Sequelize.DefineAttributeColumnOptions { 12 interface Integer extends ModelAttributeColumnOptions {
13 defaultValue: number | null 13 defaultValue: number | null
14 } 14 }
15 15
16 interface BigInteger extends Sequelize.DefineAttributeColumnOptions { 16 interface BigInteger extends ModelAttributeColumnOptions {
17 defaultValue: Sequelize.DataTypeBigInt | number | null 17 defaultValue: number | null
18 } 18 }
19 19
20 interface UUID extends Sequelize.DefineAttributeColumnOptions { 20 interface UUID extends ModelAttributeColumnOptions {
21 defaultValue: Sequelize.DataTypeUUIDv4 | null 21 defaultValue: null
22 } 22 }
23} 23}
24 24
diff --git a/server/models/oauth/oauth-token.ts b/server/models/oauth/oauth-token.ts
index 08d892da4..903d551df 100644
--- a/server/models/oauth/oauth-token.ts
+++ b/server/models/oauth/oauth-token.ts
@@ -34,21 +34,21 @@ enum ScopeNames {
34 WITH_USER = 'WITH_USER' 34 WITH_USER = 'WITH_USER'
35} 35}
36 36
37@Scopes({ 37@Scopes(() => ({
38 [ScopeNames.WITH_USER]: { 38 [ScopeNames.WITH_USER]: {
39 include: [ 39 include: [
40 { 40 {
41 model: () => UserModel.unscoped(), 41 model: UserModel.unscoped(),
42 required: true, 42 required: true,
43 include: [ 43 include: [
44 { 44 {
45 attributes: [ 'id' ], 45 attributes: [ 'id' ],
46 model: () => AccountModel.unscoped(), 46 model: AccountModel.unscoped(),
47 required: true, 47 required: true,
48 include: [ 48 include: [
49 { 49 {
50 attributes: [ 'id', 'url' ], 50 attributes: [ 'id', 'url' ],
51 model: () => ActorModel.unscoped(), 51 model: ActorModel.unscoped(),
52 required: true 52 required: true
53 } 53 }
54 ] 54 ]
@@ -57,7 +57,7 @@ enum ScopeNames {
57 } 57 }
58 ] 58 ]
59 } 59 }
60}) 60}))
61@Table({ 61@Table({
62 tableName: 'oAuthToken', 62 tableName: 'oAuthToken',
63 indexes: [ 63 indexes: [
@@ -167,11 +167,13 @@ export class OAuthTokenModel extends Model<OAuthTokenModel> {
167 } 167 }
168 } 168 }
169 169
170 return OAuthTokenModel.scope(ScopeNames.WITH_USER).findOne(query).then(token => { 170 return OAuthTokenModel.scope(ScopeNames.WITH_USER)
171 if (token) token['user'] = token.User 171 .findOne(query)
172 .then(token => {
173 if (token) token[ 'user' ] = token.User
172 174
173 return token 175 return token
174 }) 176 })
175 } 177 }
176 178
177 static getByRefreshTokenAndPopulateUser (refreshToken: string) { 179 static getByRefreshTokenAndPopulateUser (refreshToken: string) {
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts
index 8f2ef2d9a..eb2222256 100644
--- a/server/models/redundancy/video-redundancy.ts
+++ b/server/models/redundancy/video-redundancy.ts
@@ -13,9 +13,9 @@ import {
13 UpdatedAt 13 UpdatedAt
14} from 'sequelize-typescript' 14} from 'sequelize-typescript'
15import { ActorModel } from '../activitypub/actor' 15import { ActorModel } from '../activitypub/actor'
16import { getVideoSort, throwIfNotValid } from '../utils' 16import { getVideoSort, parseAggregateResult, 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, MIMETYPES } from '../../initializers' 18import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers/constants'
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'
@@ -27,28 +27,40 @@ import { ServerModel } from '../server/server'
27import { sample } from 'lodash' 27import { sample } from 'lodash'
28import { isTestInstance } from '../../helpers/core-utils' 28import { isTestInstance } from '../../helpers/core-utils'
29import * as Bluebird from 'bluebird' 29import * as Bluebird from 'bluebird'
30import * as Sequelize from 'sequelize' 30import { col, FindOptions, fn, literal, Op, Transaction } from 'sequelize'
31import { VideoStreamingPlaylistModel } from '../video/video-streaming-playlist'
32import { CONFIG } from '../../initializers/config'
31 33
32export enum ScopeNames { 34export enum ScopeNames {
33 WITH_VIDEO = 'WITH_VIDEO' 35 WITH_VIDEO = 'WITH_VIDEO'
34} 36}
35 37
36@Scopes({ 38@Scopes(() => ({
37 [ ScopeNames.WITH_VIDEO ]: { 39 [ ScopeNames.WITH_VIDEO ]: {
38 include: [ 40 include: [
39 { 41 {
40 model: () => VideoFileModel, 42 model: VideoFileModel,
41 required: true, 43 required: false,
42 include: [ 44 include: [
43 { 45 {
44 model: () => VideoModel, 46 model: VideoModel,
47 required: true
48 }
49 ]
50 },
51 {
52 model: VideoStreamingPlaylistModel,
53 required: false,
54 include: [
55 {
56 model: VideoModel,
45 required: true 57 required: true
46 } 58 }
47 ] 59 ]
48 } 60 }
49 ] 61 ]
50 } 62 }
51}) 63}))
52 64
53@Table({ 65@Table({
54 tableName: 'videoRedundancy', 66 tableName: 'videoRedundancy',
@@ -97,12 +109,24 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
97 109
98 @BelongsTo(() => VideoFileModel, { 110 @BelongsTo(() => VideoFileModel, {
99 foreignKey: { 111 foreignKey: {
100 allowNull: false 112 allowNull: true
101 }, 113 },
102 onDelete: 'cascade' 114 onDelete: 'cascade'
103 }) 115 })
104 VideoFile: VideoFileModel 116 VideoFile: VideoFileModel
105 117
118 @ForeignKey(() => VideoStreamingPlaylistModel)
119 @Column
120 videoStreamingPlaylistId: number
121
122 @BelongsTo(() => VideoStreamingPlaylistModel, {
123 foreignKey: {
124 allowNull: true
125 },
126 onDelete: 'cascade'
127 })
128 VideoStreamingPlaylist: VideoStreamingPlaylistModel
129
106 @ForeignKey(() => ActorModel) 130 @ForeignKey(() => ActorModel)
107 @Column 131 @Column
108 actorId: number 132 actorId: number
@@ -119,13 +143,25 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
119 static async removeFile (instance: VideoRedundancyModel) { 143 static async removeFile (instance: VideoRedundancyModel) {
120 if (!instance.isOwned()) return 144 if (!instance.isOwned()) return
121 145
122 const videoFile = await VideoFileModel.loadWithVideo(instance.videoFileId) 146 if (instance.videoFileId) {
147 const videoFile = await VideoFileModel.loadWithVideo(instance.videoFileId)
123 148
124 const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}` 149 const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}`
125 logger.info('Removing duplicated video file %s.', logIdentifier) 150 logger.info('Removing duplicated video file %s.', logIdentifier)
126 151
127 videoFile.Video.removeFile(videoFile, true) 152 videoFile.Video.removeFile(videoFile, true)
128 .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err })) 153 .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err }))
154 }
155
156 if (instance.videoStreamingPlaylistId) {
157 const videoStreamingPlaylist = await VideoStreamingPlaylistModel.loadWithVideo(instance.videoStreamingPlaylistId)
158
159 const videoUUID = videoStreamingPlaylist.Video.uuid
160 logger.info('Removing duplicated video streaming playlist %s.', videoUUID)
161
162 videoStreamingPlaylist.Video.removeStreamingPlaylist(true)
163 .catch(err => logger.error('Cannot delete video streaming playlist files of %s.', videoUUID, { err }))
164 }
129 165
130 return undefined 166 return undefined
131 } 167 }
@@ -143,7 +179,20 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
143 return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query) 179 return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query)
144 } 180 }
145 181
146 static loadByUrl (url: string, transaction?: Sequelize.Transaction) { 182 static async loadLocalByStreamingPlaylistId (videoStreamingPlaylistId: number) {
183 const actor = await getServerActor()
184
185 const query = {
186 where: {
187 actorId: actor.id,
188 videoStreamingPlaylistId
189 }
190 }
191
192 return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query)
193 }
194
195 static loadByUrl (url: string, transaction?: Transaction) {
147 const query = { 196 const query = {
148 where: { 197 where: {
149 url 198 url
@@ -191,7 +240,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
191 const ids = rows.map(r => r.id) 240 const ids = rows.map(r => r.id)
192 const id = sample(ids) 241 const id = sample(ids)
193 242
194 return VideoModel.loadWithFile(id, undefined, !isTestInstance()) 243 return VideoModel.loadWithFiles(id, undefined, !isTestInstance())
195 } 244 }
196 245
197 static async findMostViewToDuplicate (randomizedFactor: number) { 246 static async findMostViewToDuplicate (randomizedFactor: number) {
@@ -243,7 +292,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
243 where: { 292 where: {
244 privacy: VideoPrivacy.PUBLIC, 293 privacy: VideoPrivacy.PUBLIC,
245 views: { 294 views: {
246 [ Sequelize.Op.gte ]: minViews 295 [ Op.gte ]: minViews
247 } 296 }
248 }, 297 },
249 include: [ 298 include: [
@@ -266,7 +315,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
266 actorId: actor.id, 315 actorId: actor.id,
267 strategy, 316 strategy,
268 createdAt: { 317 createdAt: {
269 [ Sequelize.Op.lt ]: expiredDate 318 [ Op.lt ]: expiredDate
270 } 319 }
271 } 320 }
272 } 321 }
@@ -277,7 +326,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
277 static async getTotalDuplicated (strategy: VideoRedundancyStrategy) { 326 static async getTotalDuplicated (strategy: VideoRedundancyStrategy) {
278 const actor = await getServerActor() 327 const actor = await getServerActor()
279 328
280 const options = { 329 const query: FindOptions = {
281 include: [ 330 include: [
282 { 331 {
283 attributes: [], 332 attributes: [],
@@ -291,12 +340,8 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
291 ] 340 ]
292 } 341 }
293 342
294 return VideoFileModel.sum('size', options as any) // FIXME: typings 343 return VideoFileModel.aggregate('size', 'SUM', query)
295 .then(v => { 344 .then(result => parseAggregateResult(result))
296 if (!v || isNaN(v)) return 0
297
298 return v
299 })
300 } 345 }
301 346
302 static async listLocalExpired () { 347 static async listLocalExpired () {
@@ -306,7 +351,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
306 where: { 351 where: {
307 actorId: actor.id, 352 actorId: actor.id,
308 expiresOn: { 353 expiresOn: {
309 [ Sequelize.Op.lt ]: new Date() 354 [ Op.lt ]: new Date()
310 } 355 }
311 } 356 }
312 } 357 }
@@ -320,10 +365,10 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
320 const query = { 365 const query = {
321 where: { 366 where: {
322 actorId: { 367 actorId: {
323 [Sequelize.Op.ne]: actor.id 368 [Op.ne]: actor.id
324 }, 369 },
325 expiresOn: { 370 expiresOn: {
326 [ Sequelize.Op.lt ]: new Date() 371 [ Op.lt ]: new Date()
327 } 372 }
328 } 373 }
329 } 374 }
@@ -333,40 +378,44 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
333 378
334 static async listLocalOfServer (serverId: number) { 379 static async listLocalOfServer (serverId: number) {
335 const actor = await getServerActor() 380 const actor = await getServerActor()
336 381 const buildVideoInclude = () => ({
337 const query = { 382 model: VideoModel,
338 where: { 383 required: true,
339 actorId: actor.id
340 },
341 include: [ 384 include: [
342 { 385 {
343 model: VideoFileModel, 386 attributes: [],
387 model: VideoChannelModel.unscoped(),
344 required: true, 388 required: true,
345 include: [ 389 include: [
346 { 390 {
347 model: VideoModel, 391 attributes: [],
392 model: ActorModel.unscoped(),
348 required: true, 393 required: true,
349 include: [ 394 where: {
350 { 395 serverId
351 attributes: [], 396 }
352 model: VideoChannelModel.unscoped(),
353 required: true,
354 include: [
355 {
356 attributes: [],
357 model: ActorModel.unscoped(),
358 required: true,
359 where: {
360 serverId
361 }
362 }
363 ]
364 }
365 ]
366 } 397 }
367 ] 398 ]
368 } 399 }
369 ] 400 ]
401 })
402
403 const query = {
404 where: {
405 actorId: actor.id
406 },
407 include: [
408 {
409 model: VideoFileModel,
410 required: false,
411 include: [ buildVideoInclude() ]
412 },
413 {
414 model: VideoStreamingPlaylistModel,
415 required: false,
416 include: [ buildVideoInclude() ]
417 }
418 ]
370 } 419 }
371 420
372 return VideoRedundancyModel.findAll(query) 421 return VideoRedundancyModel.findAll(query)
@@ -375,12 +424,12 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
375 static async getStats (strategy: VideoRedundancyStrategy) { 424 static async getStats (strategy: VideoRedundancyStrategy) {
376 const actor = await getServerActor() 425 const actor = await getServerActor()
377 426
378 const query = { 427 const query: FindOptions = {
379 raw: true, 428 raw: true,
380 attributes: [ 429 attributes: [
381 [ Sequelize.fn('COALESCE', Sequelize.fn('SUM', Sequelize.col('VideoFile.size')), '0'), 'totalUsed' ], 430 [ fn('COALESCE', fn('SUM', col('VideoFile.size')), '0'), 'totalUsed' ],
382 [ Sequelize.fn('COUNT', Sequelize.fn('DISTINCT', Sequelize.col('videoId'))), 'totalVideos' ], 431 [ fn('COUNT', fn('DISTINCT', col('videoId'))), 'totalVideos' ],
383 [ Sequelize.fn('COUNT', Sequelize.col('videoFileId')), 'totalVideoFiles' ] 432 [ fn('COUNT', col('videoFileId')), 'totalVideoFiles' ]
384 ], 433 ],
385 where: { 434 where: {
386 strategy, 435 strategy,
@@ -395,19 +444,40 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
395 ] 444 ]
396 } 445 }
397 446
398 return VideoRedundancyModel.findOne(query as any) // FIXME: typings 447 return VideoRedundancyModel.findOne(query)
399 .then((r: any) => ({ 448 .then((r: any) => ({
400 totalUsed: parseInt(r.totalUsed.toString(), 10), 449 totalUsed: parseAggregateResult(r.totalUsed),
401 totalVideos: r.totalVideos, 450 totalVideos: r.totalVideos,
402 totalVideoFiles: r.totalVideoFiles 451 totalVideoFiles: r.totalVideoFiles
403 })) 452 }))
404 } 453 }
405 454
455 getVideo () {
456 if (this.VideoFile) return this.VideoFile.Video
457
458 return this.VideoStreamingPlaylist.Video
459 }
460
406 isOwned () { 461 isOwned () {
407 return !!this.strategy 462 return !!this.strategy
408 } 463 }
409 464
410 toActivityPubObject (): CacheFileObject { 465 toActivityPubObject (): CacheFileObject {
466 if (this.VideoStreamingPlaylist) {
467 return {
468 id: this.url,
469 type: 'CacheFile' as 'CacheFile',
470 object: this.VideoStreamingPlaylist.Video.url,
471 expires: this.expiresOn.toISOString(),
472 url: {
473 type: 'Link',
474 mimeType: 'application/x-mpegURL',
475 mediaType: 'application/x-mpegURL',
476 href: this.fileUrl
477 }
478 }
479 }
480
411 return { 481 return {
412 id: this.url, 482 id: this.url,
413 type: 'CacheFile' as 'CacheFile', 483 type: 'CacheFile' as 'CacheFile',
@@ -429,9 +499,9 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
429 private static async buildVideoFileForDuplication () { 499 private static async buildVideoFileForDuplication () {
430 const actor = await getServerActor() 500 const actor = await getServerActor()
431 501
432 const notIn = Sequelize.literal( 502 const notIn = literal(
433 '(' + 503 '(' +
434 `SELECT "videoFileId" FROM "videoRedundancy" WHERE "actorId" = ${actor.id}` + 504 `SELECT "videoFileId" FROM "videoRedundancy" WHERE "actorId" = ${actor.id} AND "videoFileId" IS NOT NULL` +
435 ')' 505 ')'
436 ) 506 )
437 507
@@ -441,7 +511,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
441 required: true, 511 required: true,
442 where: { 512 where: {
443 id: { 513 id: {
444 [ Sequelize.Op.notIn ]: notIn 514 [ Op.notIn ]: notIn
445 } 515 }
446 } 516 }
447 } 517 }
diff --git a/server/models/server/server-blocklist.ts b/server/models/server/server-blocklist.ts
index 450f27152..92c01f642 100644
--- a/server/models/server/server-blocklist.ts
+++ b/server/models/server/server-blocklist.ts
@@ -9,11 +9,11 @@ enum ScopeNames {
9 WITH_SERVER = 'WITH_SERVER' 9 WITH_SERVER = 'WITH_SERVER'
10} 10}
11 11
12@Scopes({ 12@Scopes(() => ({
13 [ScopeNames.WITH_ACCOUNT]: { 13 [ScopeNames.WITH_ACCOUNT]: {
14 include: [ 14 include: [
15 { 15 {
16 model: () => AccountModel, 16 model: AccountModel,
17 required: true 17 required: true
18 } 18 }
19 ] 19 ]
@@ -21,12 +21,12 @@ enum ScopeNames {
21 [ScopeNames.WITH_SERVER]: { 21 [ScopeNames.WITH_SERVER]: {
22 include: [ 22 include: [
23 { 23 {
24 model: () => ServerModel, 24 model: ServerModel,
25 required: true 25 required: true
26 } 26 }
27 ] 27 ]
28 } 28 }
29}) 29}))
30 30
31@Table({ 31@Table({
32 tableName: 'serverBlocklist', 32 tableName: 'serverBlocklist',
diff --git a/server/models/utils.ts b/server/models/utils.ts
index 5b4093aec..2b172f608 100644
--- a/server/models/utils.ts
+++ b/server/models/utils.ts
@@ -1,25 +1,29 @@
1import { Sequelize } from 'sequelize-typescript' 1import { Sequelize } from 'sequelize-typescript'
2import * as validator from 'validator'
3import { OrderItem } from 'sequelize'
4import { Col } from 'sequelize/types/lib/utils'
2 5
3type SortType = { sortModel: any, sortValue: string } 6type SortType = { sortModel: any, sortValue: string }
4 7
5// Translate for example "-name" to [ [ 'name', 'DESC' ], [ 'id', 'ASC' ] ] 8// Translate for example "-name" to [ [ 'name', 'DESC' ], [ 'id', 'ASC' ] ]
6function getSort (value: string, lastSort: string[] = [ 'id', 'ASC' ]) { 9function getSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] {
7 let { direction, field } = buildDirectionAndField(value) 10 const { direction, field } = buildDirectionAndField(value)
11
12 let finalField: string | Col
8 13
9 if (field.toLowerCase() === 'match') { // Search 14 if (field.toLowerCase() === 'match') { // Search
10 field = Sequelize.col('similarity') 15 finalField = Sequelize.col('similarity')
16 } else {
17 finalField = field
11 } 18 }
12 19
13 return [ [ field, direction ], lastSort ] 20 return [ [ finalField, direction ], lastSort ]
14} 21}
15 22
16function getVideoSort (value: string, lastSort: string[] = [ 'id', 'ASC' ]) { 23function getVideoSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] {
17 let { direction, field } = buildDirectionAndField(value) 24 const { direction, field } = buildDirectionAndField(value)
18 25
19 // Alias 26 if (field.toLowerCase() === 'trending') { // Sort by aggregation
20 if (field.toLowerCase() === 'match') { // Search
21 field = Sequelize.col('similarity')
22 } else if (field.toLowerCase() === 'trending') { // Sort by aggregation
23 return [ 27 return [
24 [ Sequelize.fn('COALESCE', Sequelize.fn('SUM', Sequelize.col('VideoViews.views')), '0'), direction ], 28 [ Sequelize.fn('COALESCE', Sequelize.fn('SUM', Sequelize.col('VideoViews.views')), '0'), direction ],
25 29
@@ -29,21 +33,40 @@ function getVideoSort (value: string, lastSort: string[] = [ 'id', 'ASC' ]) {
29 ] 33 ]
30 } 34 }
31 35
32 const firstSort = typeof field === 'string' ? 36 let finalField: string | Col
33 field.split('.').concat([ direction ]) : 37
34 [ field, direction ] 38 // Alias
39 if (field.toLowerCase() === 'match') { // Search
40 finalField = Sequelize.col('similarity')
41 } else {
42 finalField = field
43 }
44
45 const firstSort = typeof finalField === 'string'
46 ? finalField.split('.').concat([ direction ]) as any // FIXME: sequelize typings
47 : [ finalField, direction ]
35 48
36 return [ firstSort, lastSort ] 49 return [ firstSort, lastSort ]
37} 50}
38 51
39function getSortOnModel (model: any, value: string, lastSort: string[] = [ 'id', 'ASC' ]) { 52function getSortOnModel (model: any, value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] {
40 let [ firstSort ] = getSort(value) 53 const [ firstSort ] = getSort(value)
41 54
42 if (model) return [ [ model, firstSort[0], firstSort[1] ], lastSort ] 55 if (model) return [ [ model, firstSort[0], firstSort[1] ], lastSort ]
43 return [ firstSort, lastSort ] 56 return [ firstSort, lastSort ]
44} 57}
45 58
46function throwIfNotValid (value: any, validator: (value: any) => boolean, fieldName = 'value') { 59function isOutdated (model: { createdAt: Date, updatedAt: Date }, refreshInterval: number) {
60 const now = Date.now()
61 const createdAtTime = model.createdAt.getTime()
62 const updatedAtTime = model.updatedAt.getTime()
63
64 return (now - createdAtTime) > refreshInterval && (now - updatedAtTime) > refreshInterval
65}
66
67function throwIfNotValid (value: any, validator: (value: any) => boolean, fieldName = 'value', nullable = false) {
68 if (nullable && (value === null || value === undefined)) return
69
47 if (validator(value) === false) { 70 if (validator(value) === false) {
48 throw new Error(`"${value}" is not a valid ${fieldName}.`) 71 throw new Error(`"${value}" is not a valid ${fieldName}.`)
49 } 72 }
@@ -74,13 +97,34 @@ function buildBlockedAccountSQL (serverAccountId: number, userAccountId?: number
74 97
75 const blockerIdsString = blockerIds.join(', ') 98 const blockerIdsString = blockerIds.join(', ')
76 99
77 const query = 'SELECT "targetAccountId" AS "id" FROM "accountBlocklist" WHERE "accountId" IN (' + blockerIdsString + ')' + 100 return 'SELECT "targetAccountId" AS "id" FROM "accountBlocklist" WHERE "accountId" IN (' + blockerIdsString + ')' +
78 ' UNION ALL ' + 101 ' UNION ALL ' +
79 'SELECT "account"."id" AS "id" FROM account INNER JOIN "actor" ON account."actorId" = actor.id ' + 102 'SELECT "account"."id" AS "id" FROM account INNER JOIN "actor" ON account."actorId" = actor.id ' +
80 'INNER JOIN "serverBlocklist" ON "actor"."serverId" = "serverBlocklist"."targetServerId" ' + 103 'INNER JOIN "serverBlocklist" ON "actor"."serverId" = "serverBlocklist"."targetServerId" ' +
81 'WHERE "serverBlocklist"."accountId" IN (' + blockerIdsString + ')' 104 'WHERE "serverBlocklist"."accountId" IN (' + blockerIdsString + ')'
105}
106
107function buildServerIdsFollowedBy (actorId: any) {
108 const actorIdNumber = parseInt(actorId + '', 10)
109
110 return '(' +
111 'SELECT "actor"."serverId" FROM "actorFollow" ' +
112 'INNER JOIN "actor" ON actor.id = "actorFollow"."targetActorId" ' +
113 'WHERE "actorFollow"."actorId" = ' + actorIdNumber +
114 ')'
115}
116
117function buildWhereIdOrUUID (id: number | string) {
118 return validator.isInt('' + id) ? { id } : { uuid: id }
119}
120
121function parseAggregateResult (result: any) {
122 if (!result) return 0
123
124 const total = parseInt(result + '', 10)
125 if (isNaN(total)) return 0
82 126
83 return query 127 return total
84} 128}
85 129
86// --------------------------------------------------------------------------- 130// ---------------------------------------------------------------------------
@@ -93,7 +137,11 @@ export {
93 getSortOnModel, 137 getSortOnModel,
94 createSimilarityAttribute, 138 createSimilarityAttribute,
95 throwIfNotValid, 139 throwIfNotValid,
96 buildTrigramSearchIndex 140 buildServerIdsFollowedBy,
141 buildTrigramSearchIndex,
142 buildWhereIdOrUUID,
143 isOutdated,
144 parseAggregateResult
97} 145}
98 146
99// --------------------------------------------------------------------------- 147// ---------------------------------------------------------------------------
@@ -107,7 +155,7 @@ function searchTrigramNormalizeCol (col: string) {
107} 155}
108 156
109function buildDirectionAndField (value: string) { 157function buildDirectionAndField (value: string) {
110 let field: any 158 let field: string
111 let direction: 'ASC' | 'DESC' 159 let direction: 'ASC' | 'DESC'
112 160
113 if (value.substring(0, 1) === '-') { 161 if (value.substring(0, 1) === '-') {
diff --git a/server/models/video/schedule-video-update.ts b/server/models/video/schedule-video-update.ts
index 1e56562e1..603d55692 100644
--- a/server/models/video/schedule-video-update.ts
+++ b/server/models/video/schedule-video-update.ts
@@ -1,7 +1,7 @@
1import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Model, Sequelize, Table, UpdatedAt } from 'sequelize-typescript' 1import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
2import { ScopeNames as VideoScopeNames, VideoModel } from './video' 2import { ScopeNames as VideoScopeNames, VideoModel } from './video'
3import { VideoPrivacy } from '../../../shared/models/videos' 3import { VideoPrivacy } from '../../../shared/models/videos'
4import { Transaction } from 'sequelize' 4import { Op, Transaction } from 'sequelize'
5 5
6@Table({ 6@Table({
7 tableName: 'scheduleVideoUpdate', 7 tableName: 'scheduleVideoUpdate',
@@ -51,7 +51,7 @@ export class ScheduleVideoUpdateModel extends Model<ScheduleVideoUpdateModel> {
51 attributes: [ 'id' ], 51 attributes: [ 'id' ],
52 where: { 52 where: {
53 updateAt: { 53 updateAt: {
54 [Sequelize.Op.lte]: new Date() 54 [Op.lte]: new Date()
55 } 55 }
56 } 56 }
57 } 57 }
@@ -64,7 +64,7 @@ export class ScheduleVideoUpdateModel extends Model<ScheduleVideoUpdateModel> {
64 const query = { 64 const query = {
65 where: { 65 where: {
66 updateAt: { 66 updateAt: {
67 [Sequelize.Op.lte]: new Date() 67 [Op.lte]: new Date()
68 } 68 }
69 }, 69 },
70 include: [ 70 include: [
@@ -72,7 +72,9 @@ export class ScheduleVideoUpdateModel extends Model<ScheduleVideoUpdateModel> {
72 model: VideoModel.scope( 72 model: VideoModel.scope(
73 [ 73 [
74 VideoScopeNames.WITH_FILES, 74 VideoScopeNames.WITH_FILES,
75 VideoScopeNames.WITH_ACCOUNT_DETAILS 75 VideoScopeNames.WITH_ACCOUNT_DETAILS,
76 VideoScopeNames.WITH_BLACKLISTED,
77 VideoScopeNames.WITH_THUMBNAILS
76 ] 78 ]
77 ) 79 )
78 } 80 }
diff --git a/server/models/video/tag.ts b/server/models/video/tag.ts
index b39621eaf..0fc3cfd4c 100644
--- a/server/models/video/tag.ts
+++ b/server/models/video/tag.ts
@@ -1,5 +1,5 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import * as Sequelize from 'sequelize' 2import { QueryTypes, Transaction } from 'sequelize'
3import { AllowNull, BelongsToMany, Column, CreatedAt, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' 3import { AllowNull, BelongsToMany, Column, CreatedAt, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
4import { isVideoTagValid } from '../../helpers/custom-validators/videos' 4import { isVideoTagValid } from '../../helpers/custom-validators/videos'
5import { throwIfNotValid } from '../utils' 5import { throwIfNotValid } from '../utils'
@@ -37,7 +37,7 @@ export class TagModel extends Model<TagModel> {
37 }) 37 })
38 Videos: VideoModel[] 38 Videos: VideoModel[]
39 39
40 static findOrCreateTags (tags: string[], transaction: Sequelize.Transaction) { 40 static findOrCreateTags (tags: string[], transaction: Transaction) {
41 if (tags === null) return [] 41 if (tags === null) return []
42 42
43 const tasks: Bluebird<TagModel>[] = [] 43 const tasks: Bluebird<TagModel>[] = []
@@ -72,10 +72,10 @@ export class TagModel extends Model<TagModel> {
72 72
73 const options = { 73 const options = {
74 bind: { threshold, count, videoPrivacy: VideoPrivacy.PUBLIC, videoState: VideoState.PUBLISHED }, 74 bind: { threshold, count, videoPrivacy: VideoPrivacy.PUBLIC, videoState: VideoState.PUBLISHED },
75 type: Sequelize.QueryTypes.SELECT 75 type: QueryTypes.SELECT as QueryTypes.SELECT
76 } 76 }
77 77
78 return TagModel.sequelize.query(query, options) 78 return TagModel.sequelize.query<{ name: string }>(query, options)
79 .then(data => data.map(d => d.name)) 79 .then(data => data.map(d => d.name))
80 } 80 }
81} 81}
diff --git a/server/models/video/thumbnail.ts b/server/models/video/thumbnail.ts
new file mode 100644
index 000000000..206e9a3d6
--- /dev/null
+++ b/server/models/video/thumbnail.ts
@@ -0,0 +1,116 @@
1import { join } from 'path'
2import { AfterDestroy, AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
3import { STATIC_PATHS, WEBSERVER } from '../../initializers/constants'
4import { logger } from '../../helpers/logger'
5import { remove } from 'fs-extra'
6import { CONFIG } from '../../initializers/config'
7import { VideoModel } from './video'
8import { VideoPlaylistModel } from './video-playlist'
9import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
10
11@Table({
12 tableName: 'thumbnail',
13 indexes: [
14 {
15 fields: [ 'videoId' ]
16 },
17 {
18 fields: [ 'videoPlaylistId' ],
19 unique: true
20 }
21 ]
22})
23export class ThumbnailModel extends Model<ThumbnailModel> {
24
25 @AllowNull(false)
26 @Column
27 filename: string
28
29 @AllowNull(true)
30 @Default(null)
31 @Column
32 height: number
33
34 @AllowNull(true)
35 @Default(null)
36 @Column
37 width: number
38
39 @AllowNull(false)
40 @Column
41 type: ThumbnailType
42
43 @AllowNull(true)
44 @Column
45 fileUrl: string
46
47 @ForeignKey(() => VideoModel)
48 @Column
49 videoId: number
50
51 @BelongsTo(() => VideoModel, {
52 foreignKey: {
53 allowNull: true
54 },
55 onDelete: 'CASCADE'
56 })
57 Video: VideoModel
58
59 @ForeignKey(() => VideoPlaylistModel)
60 @Column
61 videoPlaylistId: number
62
63 @BelongsTo(() => VideoPlaylistModel, {
64 foreignKey: {
65 allowNull: true
66 },
67 onDelete: 'CASCADE'
68 })
69 VideoPlaylist: VideoPlaylistModel
70
71 @CreatedAt
72 createdAt: Date
73
74 @UpdatedAt
75 updatedAt: Date
76
77 private static types: { [ id in ThumbnailType ]: { label: string, directory: string, staticPath: string } } = {
78 [ThumbnailType.MINIATURE]: {
79 label: 'miniature',
80 directory: CONFIG.STORAGE.THUMBNAILS_DIR,
81 staticPath: STATIC_PATHS.THUMBNAILS
82 },
83 [ThumbnailType.PREVIEW]: {
84 label: 'preview',
85 directory: CONFIG.STORAGE.PREVIEWS_DIR,
86 staticPath: STATIC_PATHS.PREVIEWS
87 }
88 }
89
90 @AfterDestroy
91 static removeFilesAndSendDelete (instance: ThumbnailModel) {
92 logger.info('Removing %s file %s.', ThumbnailModel.types[instance.type].label, instance.filename)
93
94 // Don't block the transaction
95 instance.removeThumbnail()
96 .catch(err => logger.error('Cannot remove thumbnail file %s.', instance.filename, err))
97 }
98
99 static generateDefaultPreviewName (videoUUID: string) {
100 return videoUUID + '.jpg'
101 }
102
103 getFileUrl () {
104 if (this.fileUrl) return this.fileUrl
105
106 const staticPath = ThumbnailModel.types[this.type].staticPath
107 return WEBSERVER.URL + staticPath + this.filename
108 }
109
110 removeThumbnail () {
111 const directory = ThumbnailModel.types[this.type].directory
112 const thumbnailPath = join(directory, this.filename)
113
114 return remove(thumbnailPath)
115 }
116}
diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts
index cc47644f2..1ac7919b3 100644
--- a/server/models/video/video-abuse.ts
+++ b/server/models/video/video-abuse.ts
@@ -10,7 +10,7 @@ import { AccountModel } from '../account/account'
10import { getSort, throwIfNotValid } from '../utils' 10import { getSort, throwIfNotValid } from '../utils'
11import { VideoModel } from './video' 11import { VideoModel } from './video'
12import { VideoAbuseState } from '../../../shared' 12import { VideoAbuseState } from '../../../shared'
13import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers' 13import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants'
14 14
15@Table({ 15@Table({
16 tableName: 'videoAbuse', 16 tableName: 'videoAbuse',
@@ -39,7 +39,7 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
39 39
40 @AllowNull(true) 40 @AllowNull(true)
41 @Default(null) 41 @Default(null)
42 @Is('VideoAbuseModerationComment', value => throwIfNotValid(value, isVideoAbuseModerationCommentValid, 'moderationComment')) 42 @Is('VideoAbuseModerationComment', value => throwIfNotValid(value, isVideoAbuseModerationCommentValid, 'moderationComment', true))
43 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.MODERATION_COMMENT.max)) 43 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.MODERATION_COMMENT.max))
44 moderationComment: string 44 moderationComment: string
45 45
diff --git a/server/models/video/video-blacklist.ts b/server/models/video/video-blacklist.ts
index 3b567e488..d9fe9dfc9 100644
--- a/server/models/video/video-blacklist.ts
+++ b/server/models/video/video-blacklist.ts
@@ -1,9 +1,11 @@
1import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' 1import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
2import { getSortOnModel, SortType, throwIfNotValid } from '../utils' 2import { getSortOnModel, SortType, throwIfNotValid } from '../utils'
3import { VideoModel } from './video' 3import { VideoModel } from './video'
4import { isVideoBlacklistReasonValid } from '../../helpers/custom-validators/video-blacklist' 4import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel'
5import { VideoBlacklist } from '../../../shared/models/videos' 5import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../helpers/custom-validators/video-blacklist'
6import { CONSTRAINTS_FIELDS } from '../../initializers' 6import { VideoBlacklist, VideoBlacklistType } from '../../../shared/models/videos'
7import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
8import { FindOptions } from 'sequelize'
7 9
8@Table({ 10@Table({
9 tableName: 'videoBlacklist', 11 tableName: 'videoBlacklist',
@@ -17,7 +19,7 @@ import { CONSTRAINTS_FIELDS } from '../../initializers'
17export class VideoBlacklistModel extends Model<VideoBlacklistModel> { 19export class VideoBlacklistModel extends Model<VideoBlacklistModel> {
18 20
19 @AllowNull(true) 21 @AllowNull(true)
20 @Is('VideoBlacklistReason', value => throwIfNotValid(value, isVideoBlacklistReasonValid, 'reason')) 22 @Is('VideoBlacklistReason', value => throwIfNotValid(value, isVideoBlacklistReasonValid, 'reason', true))
21 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_BLACKLIST.REASON.max)) 23 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_BLACKLIST.REASON.max))
22 reason: string 24 reason: string
23 25
@@ -25,6 +27,12 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> {
25 @Column 27 @Column
26 unfederated: boolean 28 unfederated: boolean
27 29
30 @AllowNull(false)
31 @Default(null)
32 @Is('VideoBlacklistType', value => throwIfNotValid(value, isVideoBlacklistTypeValid, 'type'))
33 @Column
34 type: VideoBlacklistType
35
28 @CreatedAt 36 @CreatedAt
29 createdAt: Date 37 createdAt: Date
30 38
@@ -43,19 +51,29 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> {
43 }) 51 })
44 Video: VideoModel 52 Video: VideoModel
45 53
46 static listForApi (start: number, count: number, sort: SortType) { 54 static listForApi (start: number, count: number, sort: SortType, type?: VideoBlacklistType) {
47 const query = { 55 const query: FindOptions = {
48 offset: start, 56 offset: start,
49 limit: count, 57 limit: count,
50 order: getSortOnModel(sort.sortModel, sort.sortValue), 58 order: getSortOnModel(sort.sortModel, sort.sortValue),
51 include: [ 59 include: [
52 { 60 {
53 model: VideoModel, 61 model: VideoModel,
54 required: true 62 required: true,
63 include: [
64 {
65 model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, true ] }),
66 required: true
67 }
68 ]
55 } 69 }
56 ] 70 ]
57 } 71 }
58 72
73 if (type) {
74 query.where = { type }
75 }
76
59 return VideoBlacklistModel.findAndCountAll(query) 77 return VideoBlacklistModel.findAndCountAll(query)
60 .then(({ rows, count }) => { 78 .then(({ rows, count }) => {
61 return { 79 return {
@@ -76,26 +94,15 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> {
76 } 94 }
77 95
78 toFormattedJSON (): VideoBlacklist { 96 toFormattedJSON (): VideoBlacklist {
79 const video = this.Video
80
81 return { 97 return {
82 id: this.id, 98 id: this.id,
83 createdAt: this.createdAt, 99 createdAt: this.createdAt,
84 updatedAt: this.updatedAt, 100 updatedAt: this.updatedAt,
85 reason: this.reason, 101 reason: this.reason,
86 unfederated: this.unfederated, 102 unfederated: this.unfederated,
103 type: this.type,
87 104
88 video: { 105 video: this.Video.toFormattedJSON()
89 id: video.id,
90 name: video.name,
91 uuid: video.uuid,
92 description: video.description,
93 duration: video.duration,
94 views: video.views,
95 likes: video.likes,
96 dislikes: video.dislikes,
97 nsfw: video.nsfw
98 }
99 } 106 }
100 } 107 }
101} 108}
diff --git a/server/models/video/video-caption.ts b/server/models/video/video-caption.ts
index b4f17b481..76243bf48 100644
--- a/server/models/video/video-caption.ts
+++ b/server/models/video/video-caption.ts
@@ -1,4 +1,4 @@
1import * as Sequelize from 'sequelize' 1import { OrderItem, Transaction } from 'sequelize'
2import { 2import {
3 AllowNull, 3 AllowNull,
4 BeforeDestroy, 4 BeforeDestroy,
@@ -12,30 +12,31 @@ import {
12 Table, 12 Table,
13 UpdatedAt 13 UpdatedAt
14} from 'sequelize-typescript' 14} from 'sequelize-typescript'
15import { throwIfNotValid } from '../utils' 15import { buildWhereIdOrUUID, throwIfNotValid } from '../utils'
16import { VideoModel } from './video' 16import { VideoModel } from './video'
17import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions' 17import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions'
18import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model' 18import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model'
19import { CONFIG, STATIC_PATHS, VIDEO_LANGUAGES } from '../../initializers' 19import { STATIC_PATHS, VIDEO_LANGUAGES } from '../../initializers/constants'
20import { join } from 'path' 20import { join } from 'path'
21import { logger } from '../../helpers/logger' 21import { logger } from '../../helpers/logger'
22import { remove } from 'fs-extra' 22import { remove } from 'fs-extra'
23import { CONFIG } from '../../initializers/config'
23 24
24export enum ScopeNames { 25export enum ScopeNames {
25 WITH_VIDEO_UUID_AND_REMOTE = 'WITH_VIDEO_UUID_AND_REMOTE' 26 WITH_VIDEO_UUID_AND_REMOTE = 'WITH_VIDEO_UUID_AND_REMOTE'
26} 27}
27 28
28@Scopes({ 29@Scopes(() => ({
29 [ScopeNames.WITH_VIDEO_UUID_AND_REMOTE]: { 30 [ScopeNames.WITH_VIDEO_UUID_AND_REMOTE]: {
30 include: [ 31 include: [
31 { 32 {
32 attributes: [ 'uuid', 'remote' ], 33 attributes: [ 'uuid', 'remote' ],
33 model: () => VideoModel.unscoped(), 34 model: VideoModel.unscoped(),
34 required: true 35 required: true
35 } 36 }
36 ] 37 ]
37 } 38 }
38}) 39}))
39 40
40@Table({ 41@Table({
41 tableName: 'videoCaption', 42 tableName: 'videoCaption',
@@ -96,12 +97,9 @@ export class VideoCaptionModel extends Model<VideoCaptionModel> {
96 const videoInclude = { 97 const videoInclude = {
97 model: VideoModel.unscoped(), 98 model: VideoModel.unscoped(),
98 attributes: [ 'id', 'remote', 'uuid' ], 99 attributes: [ 'id', 'remote', 'uuid' ],
99 where: { } 100 where: buildWhereIdOrUUID(videoId)
100 } 101 }
101 102
102 if (typeof videoId === 'string') videoInclude.where['uuid'] = videoId
103 else videoInclude.where['id'] = videoId
104
105 const query = { 103 const query = {
106 where: { 104 where: {
107 language 105 language
@@ -114,19 +112,19 @@ export class VideoCaptionModel extends Model<VideoCaptionModel> {
114 return VideoCaptionModel.findOne(query) 112 return VideoCaptionModel.findOne(query)
115 } 113 }
116 114
117 static insertOrReplaceLanguage (videoId: number, language: string, transaction: Sequelize.Transaction) { 115 static insertOrReplaceLanguage (videoId: number, language: string, transaction: Transaction) {
118 const values = { 116 const values = {
119 videoId, 117 videoId,
120 language 118 language
121 } 119 }
122 120
123 return VideoCaptionModel.upsert<VideoCaptionModel>(values, { transaction, returning: true }) 121 return (VideoCaptionModel.upsert<VideoCaptionModel>(values, { transaction, returning: true }) as any) // FIXME: typings
124 .then(([ caption ]) => caption) 122 .then(([ caption ]) => caption)
125 } 123 }
126 124
127 static listVideoCaptions (videoId: number) { 125 static listVideoCaptions (videoId: number) {
128 const query = { 126 const query = {
129 order: [ [ 'language', 'ASC' ] ], 127 order: [ [ 'language', 'ASC' ] ] as OrderItem[],
130 where: { 128 where: {
131 videoId 129 videoId
132 } 130 }
@@ -139,7 +137,7 @@ export class VideoCaptionModel extends Model<VideoCaptionModel> {
139 return VIDEO_LANGUAGES[language] || 'Unknown' 137 return VIDEO_LANGUAGES[language] || 'Unknown'
140 } 138 }
141 139
142 static deleteAllCaptionsOfRemoteVideo (videoId: number, transaction: Sequelize.Transaction) { 140 static deleteAllCaptionsOfRemoteVideo (videoId: number, transaction: Transaction) {
143 const query = { 141 const query = {
144 where: { 142 where: {
145 videoId 143 videoId
diff --git a/server/models/video/video-change-ownership.ts b/server/models/video/video-change-ownership.ts
index 48c07728f..b545a2f8c 100644
--- a/server/models/video/video-change-ownership.ts
+++ b/server/models/video/video-change-ownership.ts
@@ -1,51 +1,52 @@
1import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' 1import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
2import { AccountModel } from '../account/account' 2import { AccountModel } from '../account/account'
3import { VideoModel } from './video' 3import { ScopeNames as VideoScopeNames, VideoModel } from './video'
4import { VideoChangeOwnership, VideoChangeOwnershipStatus } from '../../../shared/models/videos' 4import { VideoChangeOwnership, VideoChangeOwnershipStatus } from '../../../shared/models/videos'
5import { getSort } from '../utils' 5import { getSort } from '../utils'
6import { VideoFileModel } from './video-file'
7 6
8enum ScopeNames { 7enum ScopeNames {
9 FULL = 'FULL' 8 WITH_ACCOUNTS = 'WITH_ACCOUNTS',
9 WITH_VIDEO = 'WITH_VIDEO'
10} 10}
11 11
12@Table({ 12@Table({
13 tableName: 'videoChangeOwnership', 13 tableName: 'videoChangeOwnership',
14 indexes: [ 14 indexes: [
15 { 15 {
16 fields: ['videoId'] 16 fields: [ 'videoId' ]
17 }, 17 },
18 { 18 {
19 fields: ['initiatorAccountId'] 19 fields: [ 'initiatorAccountId' ]
20 }, 20 },
21 { 21 {
22 fields: ['nextOwnerAccountId'] 22 fields: [ 'nextOwnerAccountId' ]
23 } 23 }
24 ] 24 ]
25}) 25})
26@Scopes({ 26@Scopes(() => ({
27 [ScopeNames.FULL]: { 27 [ScopeNames.WITH_ACCOUNTS]: {
28 include: [ 28 include: [
29 { 29 {
30 model: () => AccountModel, 30 model: AccountModel,
31 as: 'Initiator', 31 as: 'Initiator',
32 required: true 32 required: true
33 }, 33 },
34 { 34 {
35 model: () => AccountModel, 35 model: AccountModel,
36 as: 'NextOwner', 36 as: 'NextOwner',
37 required: true 37 required: true
38 }, 38 }
39 ]
40 },
41 [ScopeNames.WITH_VIDEO]: {
42 include: [
39 { 43 {
40 model: () => VideoModel, 44 model: VideoModel.scope([ VideoScopeNames.WITH_THUMBNAILS, VideoScopeNames.WITH_FILES ]),
41 required: true, 45 required: true
42 include: [
43 { model: () => VideoFileModel }
44 ]
45 } 46 }
46 ] 47 ]
47 } 48 }
48}) 49}))
49export class VideoChangeOwnershipModel extends Model<VideoChangeOwnershipModel> { 50export class VideoChangeOwnershipModel extends Model<VideoChangeOwnershipModel> {
50 @CreatedAt 51 @CreatedAt
51 createdAt: Date 52 createdAt: Date
@@ -105,12 +106,15 @@ export class VideoChangeOwnershipModel extends Model<VideoChangeOwnershipModel>
105 } 106 }
106 } 107 }
107 108
108 return VideoChangeOwnershipModel.scope(ScopeNames.FULL).findAndCountAll(query) 109 return Promise.all([
109 .then(({ rows, count }) => ({ total: count, data: rows })) 110 VideoChangeOwnershipModel.scope(ScopeNames.WITH_ACCOUNTS).count(query),
111 VideoChangeOwnershipModel.scope([ ScopeNames.WITH_ACCOUNTS, ScopeNames.WITH_VIDEO ]).findAll(query)
112 ]).then(([ count, rows ]) => ({ total: count, data: rows }))
110 } 113 }
111 114
112 static load (id: number) { 115 static load (id: number) {
113 return VideoChangeOwnershipModel.scope(ScopeNames.FULL).findById(id) 116 return VideoChangeOwnershipModel.scope([ ScopeNames.WITH_ACCOUNTS, ScopeNames.WITH_VIDEO ])
117 .findByPk(id)
114 } 118 }
115 119
116 toFormattedJSON (): VideoChangeOwnership { 120 toFormattedJSON (): VideoChangeOwnership {
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts
index 5598d80f6..fb70e6625 100644
--- a/server/models/video/video-channel.ts
+++ b/server/models/video/video-channel.ts
@@ -17,23 +17,25 @@ import {
17 UpdatedAt 17 UpdatedAt
18} from 'sequelize-typescript' 18} from 'sequelize-typescript'
19import { ActivityPubActor } from '../../../shared/models/activitypub' 19import { ActivityPubActor } from '../../../shared/models/activitypub'
20import { VideoChannel } from '../../../shared/models/videos' 20import { VideoChannel, VideoChannelSummary } from '../../../shared/models/videos'
21import { 21import {
22 isVideoChannelDescriptionValid, 22 isVideoChannelDescriptionValid,
23 isVideoChannelNameValid, 23 isVideoChannelNameValid,
24 isVideoChannelSupportValid 24 isVideoChannelSupportValid
25} from '../../helpers/custom-validators/video-channels' 25} from '../../helpers/custom-validators/video-channels'
26import { sendDeleteActor } from '../../lib/activitypub/send' 26import { sendDeleteActor } from '../../lib/activitypub/send'
27import { AccountModel } from '../account/account' 27import { AccountModel, ScopeNames as AccountModelScopeNames } from '../account/account'
28import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' 28import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor'
29import { buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' 29import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils'
30import { VideoModel } from './video' 30import { VideoModel } from './video'
31import { CONSTRAINTS_FIELDS } from '../../initializers' 31import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
32import { ServerModel } from '../server/server' 32import { ServerModel } from '../server/server'
33import { DefineIndexesOptions } from 'sequelize' 33import { FindOptions, ModelIndexesOptions, Op } from 'sequelize'
34import { AvatarModel } from '../avatar/avatar'
35import { VideoPlaylistModel } from './video-playlist'
34 36
35// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation 37// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
36const indexes: DefineIndexesOptions[] = [ 38const indexes: ModelIndexesOptions[] = [
37 buildTrigramSearchIndex('video_channel_name_trigram', 'name'), 39 buildTrigramSearchIndex('video_channel_name_trigram', 'name'),
38 40
39 { 41 {
@@ -44,35 +46,62 @@ const indexes: DefineIndexesOptions[] = [
44 } 46 }
45] 47]
46 48
47enum ScopeNames { 49export enum ScopeNames {
48 AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST', 50 AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
49 WITH_ACCOUNT = 'WITH_ACCOUNT', 51 WITH_ACCOUNT = 'WITH_ACCOUNT',
50 WITH_ACTOR = 'WITH_ACTOR', 52 WITH_ACTOR = 'WITH_ACTOR',
51 WITH_VIDEOS = 'WITH_VIDEOS' 53 WITH_VIDEOS = 'WITH_VIDEOS',
54 SUMMARY = 'SUMMARY'
52} 55}
53 56
54type AvailableForListOptions = { 57type AvailableForListOptions = {
55 actorId: number 58 actorId: number
56} 59}
57 60
58@DefaultScope({ 61@DefaultScope(() => ({
59 include: [ 62 include: [
60 { 63 {
61 model: () => ActorModel, 64 model: ActorModel,
62 required: true 65 required: true
63 } 66 }
64 ] 67 ]
65}) 68}))
66@Scopes({ 69@Scopes(() => ({
67 [ScopeNames.AVAILABLE_FOR_LIST]: (options: AvailableForListOptions) => { 70 [ScopeNames.SUMMARY]: (withAccount = false) => {
68 const actorIdNumber = parseInt(options.actorId + '', 10) 71 const base: FindOptions = {
72 attributes: [ 'name', 'description', 'id', 'actorId' ],
73 include: [
74 {
75 attributes: [ 'uuid', 'preferredUsername', 'url', 'serverId', 'avatarId' ],
76 model: ActorModel.unscoped(),
77 required: true,
78 include: [
79 {
80 attributes: [ 'host' ],
81 model: ServerModel.unscoped(),
82 required: false
83 },
84 {
85 model: AvatarModel.unscoped(),
86 required: false
87 }
88 ]
89 }
90 ]
91 }
92
93 if (withAccount === true) {
94 base.include.push({
95 model: AccountModel.scope(AccountModelScopeNames.SUMMARY),
96 required: true
97 })
98 }
69 99
100 return base
101 },
102 [ScopeNames.AVAILABLE_FOR_LIST]: (options: AvailableForListOptions) => {
70 // Only list local channels OR channels that are on an instance followed by actorId 103 // Only list local channels OR channels that are on an instance followed by actorId
71 const inQueryInstanceFollow = '(' + 104 const inQueryInstanceFollow = buildServerIdsFollowedBy(options.actorId)
72 'SELECT "actor"."serverId" FROM "actorFollow" ' +
73 'INNER JOIN "actor" ON actor.id= "actorFollow"."targetActorId" ' +
74 'WHERE "actorFollow"."actorId" = ' + actorIdNumber +
75 ')'
76 105
77 return { 106 return {
78 include: [ 107 include: [
@@ -82,13 +111,13 @@ type AvailableForListOptions = {
82 }, 111 },
83 model: ActorModel, 112 model: ActorModel,
84 where: { 113 where: {
85 [Sequelize.Op.or]: [ 114 [Op.or]: [
86 { 115 {
87 serverId: null 116 serverId: null
88 }, 117 },
89 { 118 {
90 serverId: { 119 serverId: {
91 [ Sequelize.Op.in ]: Sequelize.literal(inQueryInstanceFollow) 120 [ Op.in ]: Sequelize.literal(inQueryInstanceFollow)
92 } 121 }
93 } 122 }
94 ] 123 ]
@@ -113,22 +142,22 @@ type AvailableForListOptions = {
113 [ScopeNames.WITH_ACCOUNT]: { 142 [ScopeNames.WITH_ACCOUNT]: {
114 include: [ 143 include: [
115 { 144 {
116 model: () => AccountModel, 145 model: AccountModel,
117 required: true 146 required: true
118 } 147 }
119 ] 148 ]
120 }, 149 },
121 [ScopeNames.WITH_VIDEOS]: { 150 [ScopeNames.WITH_VIDEOS]: {
122 include: [ 151 include: [
123 () => VideoModel 152 VideoModel
124 ] 153 ]
125 }, 154 },
126 [ScopeNames.WITH_ACTOR]: { 155 [ScopeNames.WITH_ACTOR]: {
127 include: [ 156 include: [
128 () => ActorModel 157 ActorModel
129 ] 158 ]
130 } 159 }
131}) 160}))
132@Table({ 161@Table({
133 tableName: 'videoChannel', 162 tableName: 'videoChannel',
134 indexes 163 indexes
@@ -142,13 +171,13 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
142 171
143 @AllowNull(true) 172 @AllowNull(true)
144 @Default(null) 173 @Default(null)
145 @Is('VideoChannelDescription', value => throwIfNotValid(value, isVideoChannelDescriptionValid, 'description')) 174 @Is('VideoChannelDescription', value => throwIfNotValid(value, isVideoChannelDescriptionValid, 'description', true))
146 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_CHANNELS.DESCRIPTION.max)) 175 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_CHANNELS.DESCRIPTION.max))
147 description: string 176 description: string
148 177
149 @AllowNull(true) 178 @AllowNull(true)
150 @Default(null) 179 @Default(null)
151 @Is('VideoChannelSupport', value => throwIfNotValid(value, isVideoChannelSupportValid, 'support')) 180 @Is('VideoChannelSupport', value => throwIfNotValid(value, isVideoChannelSupportValid, 'support', true))
152 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_CHANNELS.SUPPORT.max)) 181 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_CHANNELS.SUPPORT.max))
153 support: string 182 support: string
154 183
@@ -192,6 +221,15 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
192 }) 221 })
193 Videos: VideoModel[] 222 Videos: VideoModel[]
194 223
224 @HasMany(() => VideoPlaylistModel, {
225 foreignKey: {
226 allowNull: true
227 },
228 onDelete: 'CASCADE',
229 hooks: true
230 })
231 VideoPlaylists: VideoPlaylistModel[]
232
195 @BeforeDestroy 233 @BeforeDestroy
196 static async sendDeleteIfOwned (instance: VideoChannelModel, options) { 234 static async sendDeleteIfOwned (instance: VideoChannelModel, options) {
197 if (!instance.Actor) { 235 if (!instance.Actor) {
@@ -274,7 +312,7 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
274 limit: options.count, 312 limit: options.count,
275 order: getSort(options.sort), 313 order: getSort(options.sort),
276 where: { 314 where: {
277 [Sequelize.Op.or]: [ 315 [Op.or]: [
278 Sequelize.literal( 316 Sequelize.literal(
279 'lower(immutable_unaccent("VideoChannelModel"."name")) % lower(immutable_unaccent(' + escapedSearch + '))' 317 'lower(immutable_unaccent("VideoChannelModel"."name")) % lower(immutable_unaccent(' + escapedSearch + '))'
280 ), 318 ),
@@ -320,7 +358,7 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
320 static loadByIdAndPopulateAccount (id: number) { 358 static loadByIdAndPopulateAccount (id: number) {
321 return VideoChannelModel.unscoped() 359 return VideoChannelModel.unscoped()
322 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) 360 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
323 .findById(id) 361 .findByPk(id)
324 } 362 }
325 363
326 static loadByIdAndAccount (id: number, accountId: number) { 364 static loadByIdAndAccount (id: number, accountId: number) {
@@ -339,7 +377,7 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
339 static loadAndPopulateAccount (id: number) { 377 static loadAndPopulateAccount (id: number) {
340 return VideoChannelModel.unscoped() 378 return VideoChannelModel.unscoped()
341 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) 379 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
342 .findById(id) 380 .findByPk(id)
343 } 381 }
344 382
345 static loadByUUIDAndPopulateAccount (uuid: string) { 383 static loadByUUIDAndPopulateAccount (uuid: string) {
@@ -378,6 +416,14 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
378 .findOne(query) 416 .findOne(query)
379 } 417 }
380 418
419 static loadByNameWithHostAndPopulateAccount (nameWithHost: string) {
420 const [ name, host ] = nameWithHost.split('@')
421
422 if (!host || host === WEBSERVER.HOST) return VideoChannelModel.loadLocalByNameAndPopulateAccount(name)
423
424 return VideoChannelModel.loadByNameAndHostAndPopulateAccount(name, host)
425 }
426
381 static loadLocalByNameAndPopulateAccount (name: string) { 427 static loadLocalByNameAndPopulateAccount (name: string) {
382 const query = { 428 const query = {
383 include: [ 429 include: [
@@ -431,7 +477,7 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
431 477
432 return VideoChannelModel.unscoped() 478 return VideoChannelModel.unscoped()
433 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_VIDEOS ]) 479 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_VIDEOS ])
434 .findById(id, options) 480 .findByPk(id, options)
435 } 481 }
436 482
437 toFormattedJSON (): VideoChannel { 483 toFormattedJSON (): VideoChannel {
@@ -452,6 +498,20 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
452 return Object.assign(actor, videoChannel) 498 return Object.assign(actor, videoChannel)
453 } 499 }
454 500
501 toFormattedSummaryJSON (): VideoChannelSummary {
502 const actor = this.Actor.toFormattedJSON()
503
504 return {
505 id: this.id,
506 uuid: actor.uuid,
507 name: actor.name,
508 displayName: this.getDisplayName(),
509 url: actor.url,
510 host: actor.host,
511 avatar: actor.avatar
512 }
513 }
514
455 toActivityPubObject (): ActivityPubActor { 515 toActivityPubObject (): ActivityPubActor {
456 const obj = this.Actor.toActivityPubObject(this.name, 'VideoChannel') 516 const obj = this.Actor.toActivityPubObject(this.name, 'VideoChannel')
457 517
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts
index 1163f9a0e..fee11ec5f 100644
--- a/server/models/video/video-comment.ts
+++ b/server/models/video/video-comment.ts
@@ -1,4 +1,3 @@
1import * as Sequelize from 'sequelize'
2import { 1import {
3 AllowNull, 2 AllowNull,
4 BeforeDestroy, 3 BeforeDestroy,
@@ -7,7 +6,6 @@ import {
7 CreatedAt, 6 CreatedAt,
8 DataType, 7 DataType,
9 ForeignKey, 8 ForeignKey,
10 IFindOptions,
11 Is, 9 Is,
12 Model, 10 Model,
13 Scopes, 11 Scopes,
@@ -18,7 +16,7 @@ import { ActivityTagObject } from '../../../shared/models/activitypub/objects/co
18import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' 16import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object'
19import { VideoComment } from '../../../shared/models/videos/video-comment.model' 17import { VideoComment } from '../../../shared/models/videos/video-comment.model'
20import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 18import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
21import { CONFIG, CONSTRAINTS_FIELDS } from '../../initializers' 19import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
22import { sendDeleteVideoComment } from '../../lib/activitypub/send' 20import { sendDeleteVideoComment } from '../../lib/activitypub/send'
23import { AccountModel } from '../account/account' 21import { AccountModel } from '../account/account'
24import { ActorModel } from '../activitypub/actor' 22import { ActorModel } from '../activitypub/actor'
@@ -32,6 +30,7 @@ import { UserModel } from '../account/user'
32import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor' 30import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor'
33import { regexpCapture } from '../../helpers/regexp' 31import { regexpCapture } from '../../helpers/regexp'
34import { uniq } from 'lodash' 32import { uniq } from 'lodash'
33import { FindOptions, Op, Order, ScopeOptions, Sequelize, Transaction } from 'sequelize'
35 34
36enum ScopeNames { 35enum ScopeNames {
37 WITH_ACCOUNT = 'WITH_ACCOUNT', 36 WITH_ACCOUNT = 'WITH_ACCOUNT',
@@ -40,7 +39,7 @@ enum ScopeNames {
40 ATTRIBUTES_FOR_API = 'ATTRIBUTES_FOR_API' 39 ATTRIBUTES_FOR_API = 'ATTRIBUTES_FOR_API'
41} 40}
42 41
43@Scopes({ 42@Scopes(() => ({
44 [ScopeNames.ATTRIBUTES_FOR_API]: (serverAccountId: number, userAccountId?: number) => { 43 [ScopeNames.ATTRIBUTES_FOR_API]: (serverAccountId: number, userAccountId?: number) => {
45 return { 44 return {
46 attributes: { 45 attributes: {
@@ -64,22 +63,22 @@ enum ScopeNames {
64 ] 63 ]
65 ] 64 ]
66 } 65 }
67 } 66 } as FindOptions
68 }, 67 },
69 [ScopeNames.WITH_ACCOUNT]: { 68 [ScopeNames.WITH_ACCOUNT]: {
70 include: [ 69 include: [
71 { 70 {
72 model: () => AccountModel, 71 model: AccountModel,
73 include: [ 72 include: [
74 { 73 {
75 model: () => ActorModel, 74 model: ActorModel,
76 include: [ 75 include: [
77 { 76 {
78 model: () => ServerModel, 77 model: ServerModel,
79 required: false 78 required: false
80 }, 79 },
81 { 80 {
82 model: () => AvatarModel, 81 model: AvatarModel,
83 required: false 82 required: false
84 } 83 }
85 ] 84 ]
@@ -91,7 +90,7 @@ enum ScopeNames {
91 [ScopeNames.WITH_IN_REPLY_TO]: { 90 [ScopeNames.WITH_IN_REPLY_TO]: {
92 include: [ 91 include: [
93 { 92 {
94 model: () => VideoCommentModel, 93 model: VideoCommentModel,
95 as: 'InReplyToVideoComment' 94 as: 'InReplyToVideoComment'
96 } 95 }
97 ] 96 ]
@@ -99,19 +98,19 @@ enum ScopeNames {
99 [ScopeNames.WITH_VIDEO]: { 98 [ScopeNames.WITH_VIDEO]: {
100 include: [ 99 include: [
101 { 100 {
102 model: () => VideoModel, 101 model: VideoModel,
103 required: true, 102 required: true,
104 include: [ 103 include: [
105 { 104 {
106 model: () => VideoChannelModel.unscoped(), 105 model: VideoChannelModel.unscoped(),
107 required: true, 106 required: true,
108 include: [ 107 include: [
109 { 108 {
110 model: () => AccountModel, 109 model: AccountModel,
111 required: true, 110 required: true,
112 include: [ 111 include: [
113 { 112 {
114 model: () => ActorModel, 113 model: ActorModel,
115 required: true 114 required: true
116 } 115 }
117 ] 116 ]
@@ -122,7 +121,7 @@ enum ScopeNames {
122 } 121 }
123 ] 122 ]
124 } 123 }
125}) 124}))
126@Table({ 125@Table({
127 tableName: 'videoComment', 126 tableName: 'videoComment',
128 indexes: [ 127 indexes: [
@@ -244,8 +243,8 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
244 } 243 }
245 } 244 }
246 245
247 static loadById (id: number, t?: Sequelize.Transaction) { 246 static loadById (id: number, t?: Transaction) {
248 const query: IFindOptions<VideoCommentModel> = { 247 const query: FindOptions = {
249 where: { 248 where: {
250 id 249 id
251 } 250 }
@@ -256,8 +255,8 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
256 return VideoCommentModel.findOne(query) 255 return VideoCommentModel.findOne(query)
257 } 256 }
258 257
259 static loadByIdAndPopulateVideoAndAccountAndReply (id: number, t?: Sequelize.Transaction) { 258 static loadByIdAndPopulateVideoAndAccountAndReply (id: number, t?: Transaction) {
260 const query: IFindOptions<VideoCommentModel> = { 259 const query: FindOptions = {
261 where: { 260 where: {
262 id 261 id
263 } 262 }
@@ -270,8 +269,8 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
270 .findOne(query) 269 .findOne(query)
271 } 270 }
272 271
273 static loadByUrlAndPopulateAccount (url: string, t?: Sequelize.Transaction) { 272 static loadByUrlAndPopulateAccount (url: string, t?: Transaction) {
274 const query: IFindOptions<VideoCommentModel> = { 273 const query: FindOptions = {
275 where: { 274 where: {
276 url 275 url
277 } 276 }
@@ -282,8 +281,8 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
282 return VideoCommentModel.scope([ ScopeNames.WITH_ACCOUNT ]).findOne(query) 281 return VideoCommentModel.scope([ ScopeNames.WITH_ACCOUNT ]).findOne(query)
283 } 282 }
284 283
285 static loadByUrlAndPopulateReplyAndVideo (url: string, t?: Sequelize.Transaction) { 284 static loadByUrlAndPopulateReplyAndVideo (url: string, t?: Transaction) {
286 const query: IFindOptions<VideoCommentModel> = { 285 const query: FindOptions = {
287 where: { 286 where: {
288 url 287 url
289 } 288 }
@@ -307,15 +306,14 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
307 videoId, 306 videoId,
308 inReplyToCommentId: null, 307 inReplyToCommentId: null,
309 accountId: { 308 accountId: {
310 [Sequelize.Op.notIn]: Sequelize.literal( 309 [Op.notIn]: Sequelize.literal(
311 '(' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')' 310 '(' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')'
312 ) 311 )
313 } 312 }
314 } 313 }
315 } 314 }
316 315
317 // FIXME: typings 316 const scopes: (string | ScopeOptions)[] = [
318 const scopes: any[] = [
319 ScopeNames.WITH_ACCOUNT, 317 ScopeNames.WITH_ACCOUNT,
320 { 318 {
321 method: [ ScopeNames.ATTRIBUTES_FOR_API, serverAccountId, userAccountId ] 319 method: [ ScopeNames.ATTRIBUTES_FOR_API, serverAccountId, userAccountId ]
@@ -336,15 +334,15 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
336 const userAccountId = user ? user.Account.id : undefined 334 const userAccountId = user ? user.Account.id : undefined
337 335
338 const query = { 336 const query = {
339 order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ], 337 order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ] as Order,
340 where: { 338 where: {
341 videoId, 339 videoId,
342 [ Sequelize.Op.or ]: [ 340 [ Op.or ]: [
343 { id: threadId }, 341 { id: threadId },
344 { originCommentId: threadId } 342 { originCommentId: threadId }
345 ], 343 ],
346 accountId: { 344 accountId: {
347 [Sequelize.Op.notIn]: Sequelize.literal( 345 [Op.notIn]: Sequelize.literal(
348 '(' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')' 346 '(' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')'
349 ) 347 )
350 } 348 }
@@ -366,12 +364,12 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
366 }) 364 })
367 } 365 }
368 366
369 static listThreadParentComments (comment: VideoCommentModel, t: Sequelize.Transaction, order: 'ASC' | 'DESC' = 'ASC') { 367 static listThreadParentComments (comment: VideoCommentModel, t: Transaction, order: 'ASC' | 'DESC' = 'ASC') {
370 const query = { 368 const query = {
371 order: [ [ 'createdAt', order ] ], 369 order: [ [ 'createdAt', order ] ] as Order,
372 where: { 370 where: {
373 id: { 371 id: {
374 [ Sequelize.Op.in ]: Sequelize.literal('(' + 372 [ Op.in ]: Sequelize.literal('(' +
375 'WITH RECURSIVE children (id, "inReplyToCommentId") AS ( ' + 373 'WITH RECURSIVE children (id, "inReplyToCommentId") AS ( ' +
376 `SELECT id, "inReplyToCommentId" FROM "videoComment" WHERE id = ${comment.id} ` + 374 `SELECT id, "inReplyToCommentId" FROM "videoComment" WHERE id = ${comment.id} ` +
377 'UNION ' + 375 'UNION ' +
@@ -380,7 +378,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
380 ') ' + 378 ') ' +
381 'SELECT id FROM children' + 379 'SELECT id FROM children' +
382 ')'), 380 ')'),
383 [ Sequelize.Op.ne ]: comment.id 381 [ Op.ne ]: comment.id
384 } 382 }
385 }, 383 },
386 transaction: t 384 transaction: t
@@ -391,9 +389,9 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
391 .findAll(query) 389 .findAll(query)
392 } 390 }
393 391
394 static listAndCountByVideoId (videoId: number, start: number, count: number, t?: Sequelize.Transaction, order: 'ASC' | 'DESC' = 'ASC') { 392 static listAndCountByVideoId (videoId: number, start: number, count: number, t?: Transaction, order: 'ASC' | 'DESC' = 'ASC') {
395 const query = { 393 const query = {
396 order: [ [ 'createdAt', order ] ], 394 order: [ [ 'createdAt', order ] ] as Order,
397 offset: start, 395 offset: start,
398 limit: count, 396 limit: count,
399 where: { 397 where: {
@@ -407,7 +405,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
407 405
408 static listForFeed (start: number, count: number, videoId?: number) { 406 static listForFeed (start: number, count: number, videoId?: number) {
409 const query = { 407 const query = {
410 order: [ [ 'createdAt', 'DESC' ] ], 408 order: [ [ 'createdAt', 'DESC' ] ] as Order,
411 offset: start, 409 offset: start,
412 limit: count, 410 limit: count,
413 where: {}, 411 where: {},
@@ -453,6 +451,19 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
453 } 451 }
454 } 452 }
455 453
454 static cleanOldCommentsOf (videoId: number, beforeUpdatedAt: Date) {
455 const query = {
456 where: {
457 updatedAt: {
458 [Op.lt]: beforeUpdatedAt
459 },
460 videoId
461 }
462 }
463
464 return VideoCommentModel.destroy(query)
465 }
466
456 getCommentStaticPath () { 467 getCommentStaticPath () {
457 return this.Video.getWatchStaticPath() + ';threadId=' + this.getThreadId() 468 return this.Video.getWatchStaticPath() + ';threadId=' + this.getThreadId()
458 } 469 }
@@ -469,7 +480,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
469 let result: string[] = [] 480 let result: string[] = []
470 481
471 const localMention = `@(${actorNameAlphabet}+)` 482 const localMention = `@(${actorNameAlphabet}+)`
472 const remoteMention = `${localMention}@${CONFIG.WEBSERVER.HOST}` 483 const remoteMention = `${localMention}@${WEBSERVER.HOST}`
473 484
474 const mentionRegex = this.isOwned() 485 const mentionRegex = this.isOwned()
475 ? '(?:(?:' + remoteMention + ')|(?:' + localMention + '))' // Include local mentions? 486 ? '(?:(?:' + remoteMention + ')|(?:' + localMention + '))' // Include local mentions?
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts
index 1f1b76c1e..2203a7aba 100644
--- a/server/models/video/video-file.ts
+++ b/server/models/video/video-file.ts
@@ -19,10 +19,11 @@ import {
19 isVideoFileSizeValid, 19 isVideoFileSizeValid,
20 isVideoFPSResolutionValid 20 isVideoFPSResolutionValid
21} from '../../helpers/custom-validators/videos' 21} from '../../helpers/custom-validators/videos'
22import { throwIfNotValid } from '../utils' 22import { parseAggregateResult, throwIfNotValid } from '../utils'
23import { VideoModel } from './video' 23import { VideoModel } from './video'
24import * as Sequelize from 'sequelize'
25import { VideoRedundancyModel } from '../redundancy/video-redundancy' 24import { VideoRedundancyModel } from '../redundancy/video-redundancy'
25import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
26import { FindOptions, QueryTypes, Transaction } from 'sequelize'
26 27
27@Table({ 28@Table({
28 tableName: 'videoFile', 29 tableName: 'videoFile',
@@ -62,7 +63,7 @@ export class VideoFileModel extends Model<VideoFileModel> {
62 extname: string 63 extname: string
63 64
64 @AllowNull(false) 65 @AllowNull(false)
65 @Is('VideoFileSize', value => throwIfNotValid(value, isVideoFileInfoHashValid, 'info hash')) 66 @Is('VideoFileInfohash', value => throwIfNotValid(value, isVideoFileInfoHashValid, 'info hash'))
66 @Column 67 @Column
67 infoHash: string 68 infoHash: string
68 69
@@ -86,25 +87,23 @@ export class VideoFileModel extends Model<VideoFileModel> {
86 87
87 @HasMany(() => VideoRedundancyModel, { 88 @HasMany(() => VideoRedundancyModel, {
88 foreignKey: { 89 foreignKey: {
89 allowNull: false 90 allowNull: true
90 }, 91 },
91 onDelete: 'CASCADE', 92 onDelete: 'CASCADE',
92 hooks: true 93 hooks: true
93 }) 94 })
94 RedundancyVideos: VideoRedundancyModel[] 95 RedundancyVideos: VideoRedundancyModel[]
95 96
96 static isInfohashExists (infoHash: string) { 97 static doesInfohashExist (infoHash: string) {
97 const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1' 98 const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1'
98 const options = { 99 const options = {
99 type: Sequelize.QueryTypes.SELECT, 100 type: QueryTypes.SELECT,
100 bind: { infoHash }, 101 bind: { infoHash },
101 raw: true 102 raw: true
102 } 103 }
103 104
104 return VideoModel.sequelize.query(query, options) 105 return VideoModel.sequelize.query(query, options)
105 .then(results => { 106 .then(results => results.length === 1)
106 return results.length === 1
107 })
108 } 107 }
109 108
110 static loadWithVideo (id: number) { 109 static loadWithVideo (id: number) {
@@ -117,11 +116,34 @@ export class VideoFileModel extends Model<VideoFileModel> {
117 ] 116 ]
118 } 117 }
119 118
120 return VideoFileModel.findById(id, options) 119 return VideoFileModel.findByPk(id, options)
121 } 120 }
122 121
123 static async getStats () { 122 static listByStreamingPlaylist (streamingPlaylistId: number, transaction: Transaction) {
124 let totalLocalVideoFilesSize = await VideoFileModel.sum('size', { 123 const query = {
124 include: [
125 {
126 model: VideoModel.unscoped(),
127 required: true,
128 include: [
129 {
130 model: VideoStreamingPlaylistModel.unscoped(),
131 required: true,
132 where: {
133 id: streamingPlaylistId
134 }
135 }
136 ]
137 }
138 ],
139 transaction
140 }
141
142 return VideoFileModel.findAll(query)
143 }
144
145 static getStats () {
146 const query: FindOptions = {
125 include: [ 147 include: [
126 { 148 {
127 attributes: [], 149 attributes: [],
@@ -131,13 +153,12 @@ export class VideoFileModel extends Model<VideoFileModel> {
131 } 153 }
132 } 154 }
133 ] 155 ]
134 } as any)
135 // Sequelize could return null...
136 if (!totalLocalVideoFilesSize) totalLocalVideoFilesSize = 0
137
138 return {
139 totalLocalVideoFilesSize
140 } 156 }
157
158 return VideoFileModel.aggregate('size', 'SUM', query)
159 .then(result => ({
160 totalLocalVideoFilesSize: parseAggregateResult(result)
161 }))
141 } 162 }
142 163
143 hasSameUniqueKeysThan (other: VideoFileModel) { 164 hasSameUniqueKeysThan (other: VideoFileModel) {
diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts
index de0747f22..b947eb16f 100644
--- a/server/models/video/video-format-utils.ts
+++ b/server/models/video/video-format-utils.ts
@@ -1,8 +1,13 @@
1import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' 1import { 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 {
5import { CONFIG, MIMETYPES, THUMBNAILS_SIZE } from '../../initializers' 5 ActivityPlaylistInfohashesObject,
6 ActivityPlaylistSegmentHashesObject,
7 ActivityUrlObject,
8 VideoTorrentObject
9} from '../../../shared/models/activitypub/objects'
10import { MIMETYPES, WEBSERVER } from '../../initializers/constants'
6import { VideoCaptionModel } from './video-caption' 11import { VideoCaptionModel } from './video-caption'
7import { 12import {
8 getVideoCommentsActivityPubUrl, 13 getVideoCommentsActivityPubUrl,
@@ -11,6 +16,8 @@ import {
11 getVideoSharesActivityPubUrl 16 getVideoSharesActivityPubUrl
12} from '../../lib/activitypub' 17} from '../../lib/activitypub'
13import { isArray } from '../../helpers/custom-validators/misc' 18import { isArray } from '../../helpers/custom-validators/misc'
19import { VideoStreamingPlaylist } from '../../../shared/models/videos/video-streaming-playlist.model'
20import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
14 21
15export type VideoFormattingJSONOptions = { 22export type VideoFormattingJSONOptions = {
16 completeDescription?: boolean 23 completeDescription?: boolean
@@ -19,12 +26,10 @@ export type VideoFormattingJSONOptions = {
19 waitTranscoding?: boolean, 26 waitTranscoding?: boolean,
20 scheduledUpdate?: boolean, 27 scheduledUpdate?: boolean,
21 blacklistInfo?: boolean 28 blacklistInfo?: boolean
29 playlistInfo?: boolean
22 } 30 }
23} 31}
24function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormattingJSONOptions): Video { 32function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormattingJSONOptions): Video {
25 const formattedAccount = video.VideoChannel.Account.toFormattedJSON()
26 const formattedVideoChannel = video.VideoChannel.toFormattedJSON()
27
28 const userHistory = isArray(video.UserVideoHistories) ? video.UserVideoHistories[0] : undefined 33 const userHistory = isArray(video.UserVideoHistories) ? video.UserVideoHistories[0] : undefined
29 34
30 const videoObject: Video = { 35 const videoObject: Video = {
@@ -54,30 +59,16 @@ function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormatting
54 views: video.views, 59 views: video.views,
55 likes: video.likes, 60 likes: video.likes,
56 dislikes: video.dislikes, 61 dislikes: video.dislikes,
57 thumbnailPath: video.getThumbnailStaticPath(), 62 thumbnailPath: video.getMiniatureStaticPath(),
58 previewPath: video.getPreviewStaticPath(), 63 previewPath: video.getPreviewStaticPath(),
59 embedPath: video.getEmbedStaticPath(), 64 embedPath: video.getEmbedStaticPath(),
60 createdAt: video.createdAt, 65 createdAt: video.createdAt,
61 updatedAt: video.updatedAt, 66 updatedAt: video.updatedAt,
62 publishedAt: video.publishedAt, 67 publishedAt: video.publishedAt,
63 account: { 68 originallyPublishedAt: video.originallyPublishedAt,
64 id: formattedAccount.id, 69
65 uuid: formattedAccount.uuid, 70 account: video.VideoChannel.Account.toFormattedSummaryJSON(),
66 name: formattedAccount.name, 71 channel: video.VideoChannel.toFormattedSummaryJSON(),
67 displayName: formattedAccount.displayName,
68 url: formattedAccount.url,
69 host: formattedAccount.host,
70 avatar: formattedAccount.avatar
71 },
72 channel: {
73 id: formattedVideoChannel.id,
74 uuid: formattedVideoChannel.uuid,
75 name: formattedVideoChannel.name,
76 displayName: formattedVideoChannel.displayName,
77 url: formattedVideoChannel.url,
78 host: formattedVideoChannel.host,
79 avatar: formattedVideoChannel.avatar
80 },
81 72
82 userHistory: userHistory ? { 73 userHistory: userHistory ? {
83 currentTime: userHistory.currentTime 74 currentTime: userHistory.currentTime
@@ -107,6 +98,17 @@ function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormatting
107 videoObject.blacklisted = !!video.VideoBlacklist 98 videoObject.blacklisted = !!video.VideoBlacklist
108 videoObject.blacklistedReason = video.VideoBlacklist ? video.VideoBlacklist.reason : null 99 videoObject.blacklistedReason = video.VideoBlacklist ? video.VideoBlacklist.reason : null
109 } 100 }
101
102 if (options.additionalAttributes.playlistInfo === true) {
103 // We filtered on a specific videoId/videoPlaylistId, that is unique
104 const playlistElement = video.VideoPlaylistElements[0]
105
106 videoObject.playlistElement = {
107 position: playlistElement.position,
108 startTimestamp: playlistElement.startTimestamp,
109 stopTimestamp: playlistElement.stopTimestamp
110 }
111 }
110 } 112 }
111 113
112 return videoObject 114 return videoObject
@@ -120,7 +122,12 @@ function videoModelToFormattedDetailsJSON (video: VideoModel): VideoDetails {
120 } 122 }
121 }) 123 })
122 124
125 const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
126
123 const tags = video.Tags ? video.Tags.map(t => t.name) : [] 127 const tags = video.Tags ? video.Tags.map(t => t.name) : []
128
129 const streamingPlaylists = streamingPlaylistsModelToFormattedJSON(video, video.VideoStreamingPlaylists)
130
124 const detailsJson = { 131 const detailsJson = {
125 support: video.support, 132 support: video.support,
126 descriptionPath: video.getDescriptionAPIPath(), 133 descriptionPath: video.getDescriptionAPIPath(),
@@ -128,12 +135,17 @@ function videoModelToFormattedDetailsJSON (video: VideoModel): VideoDetails {
128 account: video.VideoChannel.Account.toFormattedJSON(), 135 account: video.VideoChannel.Account.toFormattedJSON(),
129 tags, 136 tags,
130 commentsEnabled: video.commentsEnabled, 137 commentsEnabled: video.commentsEnabled,
138 downloadEnabled: video.downloadEnabled,
131 waitTranscoding: video.waitTranscoding, 139 waitTranscoding: video.waitTranscoding,
132 state: { 140 state: {
133 id: video.state, 141 id: video.state,
134 label: VideoModel.getStateLabel(video.state) 142 label: VideoModel.getStateLabel(video.state)
135 }, 143 },
136 files: [] 144
145 trackerUrls: video.getTrackerUrls(baseUrlHttp, baseUrlWs),
146
147 files: [],
148 streamingPlaylists
137 } 149 }
138 150
139 // Format and sort video files 151 // Format and sort video files
@@ -142,6 +154,25 @@ function videoModelToFormattedDetailsJSON (video: VideoModel): VideoDetails {
142 return Object.assign(formattedJson, detailsJson) 154 return Object.assign(formattedJson, detailsJson)
143} 155}
144 156
157function streamingPlaylistsModelToFormattedJSON (video: VideoModel, playlists: VideoStreamingPlaylistModel[]): VideoStreamingPlaylist[] {
158 if (isArray(playlists) === false) return []
159
160 return playlists
161 .map(playlist => {
162 const redundancies = isArray(playlist.RedundancyVideos)
163 ? playlist.RedundancyVideos.map(r => ({ baseUrl: r.fileUrl }))
164 : []
165
166 return {
167 id: playlist.id,
168 type: playlist.type,
169 playlistUrl: playlist.playlistUrl,
170 segmentsSha256Url: playlist.segmentsSha256Url,
171 redundancies
172 } as VideoStreamingPlaylist
173 })
174}
175
145function videoFilesModelToFormattedJSON (video: VideoModel, videoFiles: VideoFileModel[]): VideoFile[] { 176function videoFilesModelToFormattedJSON (video: VideoModel, videoFiles: VideoFileModel[]): VideoFile[] {
146 const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() 177 const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
147 178
@@ -232,12 +263,34 @@ function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject {
232 }) 263 })
233 } 264 }
234 265
266 for (const playlist of (video.VideoStreamingPlaylists || [])) {
267 let tag: (ActivityPlaylistSegmentHashesObject | ActivityPlaylistInfohashesObject)[]
268
269 tag = playlist.p2pMediaLoaderInfohashes
270 .map(i => ({ type: 'Infohash' as 'Infohash', name: i }))
271 tag.push({
272 type: 'Link',
273 name: 'sha256',
274 mimeType: 'application/json' as 'application/json',
275 mediaType: 'application/json' as 'application/json',
276 href: playlist.segmentsSha256Url
277 })
278
279 url.push({
280 type: 'Link',
281 mimeType: 'application/x-mpegURL' as 'application/x-mpegURL',
282 mediaType: 'application/x-mpegURL' as 'application/x-mpegURL',
283 href: playlist.playlistUrl,
284 tag
285 })
286 }
287
235 // Add video url too 288 // Add video url too
236 url.push({ 289 url.push({
237 type: 'Link', 290 type: 'Link',
238 mimeType: 'text/html', 291 mimeType: 'text/html',
239 mediaType: 'text/html', 292 mediaType: 'text/html',
240 href: CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid 293 href: WEBSERVER.URL + '/videos/watch/' + video.uuid
241 }) 294 })
242 295
243 const subtitleLanguage = [] 296 const subtitleLanguage = []
@@ -248,6 +301,8 @@ function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject {
248 }) 301 })
249 } 302 }
250 303
304 const miniature = video.getMiniature()
305
251 return { 306 return {
252 type: 'Video' as 'Video', 307 type: 'Video' as 'Video',
253 id: video.url, 308 id: video.url,
@@ -263,7 +318,9 @@ function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject {
263 waitTranscoding: video.waitTranscoding, 318 waitTranscoding: video.waitTranscoding,
264 state: video.state, 319 state: video.state,
265 commentsEnabled: video.commentsEnabled, 320 commentsEnabled: video.commentsEnabled,
321 downloadEnabled: video.downloadEnabled,
266 published: video.publishedAt.toISOString(), 322 published: video.publishedAt.toISOString(),
323 originallyPublishedAt: video.originallyPublishedAt ? video.originallyPublishedAt.toISOString() : null,
267 updated: video.updatedAt.toISOString(), 324 updated: video.updatedAt.toISOString(),
268 mediaType: 'text/markdown', 325 mediaType: 'text/markdown',
269 content: video.getTruncatedDescription(), 326 content: video.getTruncatedDescription(),
@@ -271,10 +328,10 @@ function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject {
271 subtitleLanguage, 328 subtitleLanguage,
272 icon: { 329 icon: {
273 type: 'Image', 330 type: 'Image',
274 url: video.getThumbnailUrl(baseUrlHttp), 331 url: miniature.getFileUrl(),
275 mediaType: 'image/jpeg', 332 mediaType: 'image/jpeg',
276 width: THUMBNAILS_SIZE.width, 333 width: miniature.width,
277 height: THUMBNAILS_SIZE.height 334 height: miniature.height
278 }, 335 },
279 url, 336 url,
280 likes: getVideoLikesActivityPubUrl(video), 337 likes: getVideoLikesActivityPubUrl(video),
diff --git a/server/models/video/video-import.ts b/server/models/video/video-import.ts
index c723e57c0..480a671c8 100644
--- a/server/models/video/video-import.ts
+++ b/server/models/video/video-import.ts
@@ -13,7 +13,7 @@ import {
13 Table, 13 Table,
14 UpdatedAt 14 UpdatedAt
15} from 'sequelize-typescript' 15} from 'sequelize-typescript'
16import { CONSTRAINTS_FIELDS, VIDEO_IMPORT_STATES } from '../../initializers' 16import { CONSTRAINTS_FIELDS, VIDEO_IMPORT_STATES } from '../../initializers/constants'
17import { getSort, throwIfNotValid } from '../utils' 17import { getSort, throwIfNotValid } from '../utils'
18import { ScopeNames as VideoModelScopeNames, VideoModel } from './video' 18import { ScopeNames as VideoModelScopeNames, VideoModel } from './video'
19import { isVideoImportStateValid, isVideoImportTargetUrlValid } from '../../helpers/custom-validators/video-imports' 19import { isVideoImportStateValid, isVideoImportTargetUrlValid } from '../../helpers/custom-validators/video-imports'
@@ -21,18 +21,18 @@ import { VideoImport, VideoImportState } from '../../../shared'
21import { isVideoMagnetUriValid } from '../../helpers/custom-validators/videos' 21import { isVideoMagnetUriValid } from '../../helpers/custom-validators/videos'
22import { UserModel } from '../account/user' 22import { UserModel } from '../account/user'
23 23
24@DefaultScope({ 24@DefaultScope(() => ({
25 include: [ 25 include: [
26 { 26 {
27 model: () => UserModel.unscoped(), 27 model: UserModel.unscoped(),
28 required: true 28 required: true
29 }, 29 },
30 { 30 {
31 model: () => VideoModel.scope([ VideoModelScopeNames.WITH_ACCOUNT_DETAILS, VideoModelScopeNames.WITH_TAGS]), 31 model: VideoModel.scope([ VideoModelScopeNames.WITH_ACCOUNT_DETAILS, VideoModelScopeNames.WITH_TAGS]),
32 required: false 32 required: false
33 } 33 }
34 ] 34 ]
35}) 35}))
36 36
37@Table({ 37@Table({
38 tableName: 'videoImport', 38 tableName: 'videoImport',
@@ -55,13 +55,13 @@ export class VideoImportModel extends Model<VideoImportModel> {
55 55
56 @AllowNull(true) 56 @AllowNull(true)
57 @Default(null) 57 @Default(null)
58 @Is('VideoImportTargetUrl', value => throwIfNotValid(value, isVideoImportTargetUrlValid, 'targetUrl')) 58 @Is('VideoImportTargetUrl', value => throwIfNotValid(value, isVideoImportTargetUrlValid, 'targetUrl', true))
59 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_IMPORTS.URL.max)) 59 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_IMPORTS.URL.max))
60 targetUrl: string 60 targetUrl: string
61 61
62 @AllowNull(true) 62 @AllowNull(true)
63 @Default(null) 63 @Default(null)
64 @Is('VideoImportMagnetUri', value => throwIfNotValid(value, isVideoMagnetUriValid, 'magnetUri')) 64 @Is('VideoImportMagnetUri', value => throwIfNotValid(value, isVideoMagnetUriValid, 'magnetUri', true))
65 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_IMPORTS.URL.max)) // Use the same constraints than URLs 65 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_IMPORTS.URL.max)) // Use the same constraints than URLs
66 magnetUri: string 66 magnetUri: string
67 67
@@ -115,7 +115,7 @@ export class VideoImportModel extends Model<VideoImportModel> {
115 } 115 }
116 116
117 static loadAndPopulateVideo (id: number) { 117 static loadAndPopulateVideo (id: number) {
118 return VideoImportModel.findById(id) 118 return VideoImportModel.findByPk(id)
119 } 119 }
120 120
121 static listUserVideoImportsForApi (userId: number, start: number, count: number, sort: string) { 121 static listUserVideoImportsForApi (userId: number, start: number, count: number, sort: string) {
diff --git a/server/models/video/video-playlist-element.ts b/server/models/video/video-playlist-element.ts
new file mode 100644
index 000000000..eeb3d6bbd
--- /dev/null
+++ b/server/models/video/video-playlist-element.ts
@@ -0,0 +1,230 @@
1import {
2 AllowNull,
3 BelongsTo,
4 Column,
5 CreatedAt,
6 DataType,
7 Default,
8 ForeignKey,
9 Is,
10 IsInt,
11 Min,
12 Model,
13 Table,
14 UpdatedAt
15} from 'sequelize-typescript'
16import { VideoModel } from './video'
17import { VideoPlaylistModel } from './video-playlist'
18import { getSort, throwIfNotValid } from '../utils'
19import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
20import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
21import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object'
22import * as validator from 'validator'
23import { AggregateOptions, Op, Sequelize, Transaction } from 'sequelize'
24
25@Table({
26 tableName: 'videoPlaylistElement',
27 indexes: [
28 {
29 fields: [ 'videoPlaylistId' ]
30 },
31 {
32 fields: [ 'videoId' ]
33 },
34 {
35 fields: [ 'videoPlaylistId', 'videoId' ],
36 unique: true
37 },
38 {
39 fields: [ 'url' ],
40 unique: true
41 }
42 ]
43})
44export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel> {
45 @CreatedAt
46 createdAt: Date
47
48 @UpdatedAt
49 updatedAt: Date
50
51 @AllowNull(false)
52 @Is('VideoPlaylistUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
53 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS.URL.max))
54 url: string
55
56 @AllowNull(false)
57 @Default(1)
58 @IsInt
59 @Min(1)
60 @Column
61 position: number
62
63 @AllowNull(true)
64 @IsInt
65 @Min(0)
66 @Column
67 startTimestamp: number
68
69 @AllowNull(true)
70 @IsInt
71 @Min(0)
72 @Column
73 stopTimestamp: number
74
75 @ForeignKey(() => VideoPlaylistModel)
76 @Column
77 videoPlaylistId: number
78
79 @BelongsTo(() => VideoPlaylistModel, {
80 foreignKey: {
81 allowNull: false
82 },
83 onDelete: 'CASCADE'
84 })
85 VideoPlaylist: VideoPlaylistModel
86
87 @ForeignKey(() => VideoModel)
88 @Column
89 videoId: number
90
91 @BelongsTo(() => VideoModel, {
92 foreignKey: {
93 allowNull: false
94 },
95 onDelete: 'CASCADE'
96 })
97 Video: VideoModel
98
99 static deleteAllOf (videoPlaylistId: number, transaction?: Transaction) {
100 const query = {
101 where: {
102 videoPlaylistId
103 },
104 transaction
105 }
106
107 return VideoPlaylistElementModel.destroy(query)
108 }
109
110 static loadByPlaylistAndVideo (videoPlaylistId: number, videoId: number) {
111 const query = {
112 where: {
113 videoPlaylistId,
114 videoId
115 }
116 }
117
118 return VideoPlaylistElementModel.findOne(query)
119 }
120
121 static loadByPlaylistAndVideoForAP (playlistId: number | string, videoId: number | string) {
122 const playlistWhere = validator.isUUID('' + playlistId) ? { uuid: playlistId } : { id: playlistId }
123 const videoWhere = validator.isUUID('' + videoId) ? { uuid: videoId } : { id: videoId }
124
125 const query = {
126 include: [
127 {
128 attributes: [ 'privacy' ],
129 model: VideoPlaylistModel.unscoped(),
130 where: playlistWhere
131 },
132 {
133 attributes: [ 'url' ],
134 model: VideoModel.unscoped(),
135 where: videoWhere
136 }
137 ]
138 }
139
140 return VideoPlaylistElementModel.findOne(query)
141 }
142
143 static listUrlsOfForAP (videoPlaylistId: number, start: number, count: number, t?: Transaction) {
144 const query = {
145 attributes: [ 'url' ],
146 offset: start,
147 limit: count,
148 order: getSort('position'),
149 where: {
150 videoPlaylistId
151 },
152 transaction: t
153 }
154
155 return VideoPlaylistElementModel
156 .findAndCountAll(query)
157 .then(({ rows, count }) => {
158 return { total: count, data: rows.map(e => e.url) }
159 })
160 }
161
162 static getNextPositionOf (videoPlaylistId: number, transaction?: Transaction) {
163 const query: AggregateOptions<number> = {
164 where: {
165 videoPlaylistId
166 },
167 transaction
168 }
169
170 return VideoPlaylistElementModel.max('position', query)
171 .then(position => position ? position + 1 : 1)
172 }
173
174 static reassignPositionOf (
175 videoPlaylistId: number,
176 firstPosition: number,
177 endPosition: number,
178 newPosition: number,
179 transaction?: Transaction
180 ) {
181 const query = {
182 where: {
183 videoPlaylistId,
184 position: {
185 [Op.gte]: firstPosition,
186 [Op.lte]: endPosition
187 }
188 },
189 transaction,
190 validate: false // We use a literal to update the position
191 }
192
193 return VideoPlaylistElementModel.update({ position: Sequelize.literal(`${newPosition} + "position" - ${firstPosition}`) }, query)
194 }
195
196 static increasePositionOf (
197 videoPlaylistId: number,
198 fromPosition: number,
199 toPosition?: number,
200 by = 1,
201 transaction?: Transaction
202 ) {
203 const query = {
204 where: {
205 videoPlaylistId,
206 position: {
207 [Op.gte]: fromPosition
208 }
209 },
210 transaction
211 }
212
213 return VideoPlaylistElementModel.increment({ position: by }, query)
214 }
215
216 toActivityPubObject (): PlaylistElementObject {
217 const base: PlaylistElementObject = {
218 id: this.url,
219 type: 'PlaylistElement',
220
221 url: this.Video.url,
222 position: this.position
223 }
224
225 if (this.startTimestamp) base.startTimestamp = this.startTimestamp
226 if (this.stopTimestamp) base.stopTimestamp = this.stopTimestamp
227
228 return base
229 }
230}
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts
new file mode 100644
index 000000000..63b4a0715
--- /dev/null
+++ b/server/models/video/video-playlist.ts
@@ -0,0 +1,531 @@
1import {
2 AllowNull,
3 BelongsTo,
4 Column,
5 CreatedAt,
6 DataType,
7 Default,
8 ForeignKey,
9 HasMany,
10 HasOne,
11 Is,
12 IsUUID,
13 Model,
14 Scopes,
15 Table,
16 UpdatedAt
17} from 'sequelize-typescript'
18import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
19import { buildServerIdsFollowedBy, buildWhereIdOrUUID, getSort, isOutdated, throwIfNotValid } from '../utils'
20import {
21 isVideoPlaylistDescriptionValid,
22 isVideoPlaylistNameValid,
23 isVideoPlaylistPrivacyValid
24} from '../../helpers/custom-validators/video-playlists'
25import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
26import {
27 ACTIVITY_PUB,
28 CONSTRAINTS_FIELDS,
29 STATIC_PATHS,
30 THUMBNAILS_SIZE,
31 VIDEO_PLAYLIST_PRIVACIES,
32 VIDEO_PLAYLIST_TYPES,
33 WEBSERVER
34} from '../../initializers/constants'
35import { VideoPlaylist } from '../../../shared/models/videos/playlist/video-playlist.model'
36import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account'
37import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel'
38import { join } from 'path'
39import { VideoPlaylistElementModel } from './video-playlist-element'
40import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object'
41import { activityPubCollectionPagination } from '../../helpers/activitypub'
42import { VideoPlaylistType } from '../../../shared/models/videos/playlist/video-playlist-type.model'
43import { ThumbnailModel } from './thumbnail'
44import { ActivityIconObject } from '../../../shared/models/activitypub/objects'
45import { FindOptions, literal, Op, ScopeOptions, Transaction, WhereOptions } from 'sequelize'
46
47enum ScopeNames {
48 AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
49 WITH_VIDEOS_LENGTH = 'WITH_VIDEOS_LENGTH',
50 WITH_ACCOUNT_AND_CHANNEL_SUMMARY = 'WITH_ACCOUNT_AND_CHANNEL_SUMMARY',
51 WITH_ACCOUNT = 'WITH_ACCOUNT',
52 WITH_THUMBNAIL = 'WITH_THUMBNAIL',
53 WITH_ACCOUNT_AND_CHANNEL = 'WITH_ACCOUNT_AND_CHANNEL'
54}
55
56type AvailableForListOptions = {
57 followerActorId: number
58 type?: VideoPlaylistType
59 accountId?: number
60 videoChannelId?: number
61 privateAndUnlisted?: boolean
62}
63
64@Scopes(() => ({
65 [ ScopeNames.WITH_THUMBNAIL ]: {
66 include: [
67 {
68 model: ThumbnailModel,
69 required: false
70 }
71 ]
72 },
73 [ ScopeNames.WITH_VIDEOS_LENGTH ]: {
74 attributes: {
75 include: [
76 [
77 literal('(SELECT COUNT("id") FROM "videoPlaylistElement" WHERE "videoPlaylistId" = "VideoPlaylistModel"."id")'),
78 'videosLength'
79 ]
80 ]
81 }
82 } as FindOptions,
83 [ ScopeNames.WITH_ACCOUNT ]: {
84 include: [
85 {
86 model: AccountModel,
87 required: true
88 }
89 ]
90 },
91 [ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY ]: {
92 include: [
93 {
94 model: AccountModel.scope(AccountScopeNames.SUMMARY),
95 required: true
96 },
97 {
98 model: VideoChannelModel.scope(VideoChannelScopeNames.SUMMARY),
99 required: false
100 }
101 ]
102 },
103 [ ScopeNames.WITH_ACCOUNT_AND_CHANNEL ]: {
104 include: [
105 {
106 model: AccountModel,
107 required: true
108 },
109 {
110 model: VideoChannelModel,
111 required: false
112 }
113 ]
114 },
115 [ ScopeNames.AVAILABLE_FOR_LIST ]: (options: AvailableForListOptions) => {
116 // Only list local playlists OR playlists that are on an instance followed by actorId
117 const inQueryInstanceFollow = buildServerIdsFollowedBy(options.followerActorId)
118 const actorWhere = {
119 [ Op.or ]: [
120 {
121 serverId: null
122 },
123 {
124 serverId: {
125 [ Op.in ]: literal(inQueryInstanceFollow)
126 }
127 }
128 ]
129 }
130
131 const whereAnd: WhereOptions[] = []
132
133 if (options.privateAndUnlisted !== true) {
134 whereAnd.push({
135 privacy: VideoPlaylistPrivacy.PUBLIC
136 })
137 }
138
139 if (options.accountId) {
140 whereAnd.push({
141 ownerAccountId: options.accountId
142 })
143 }
144
145 if (options.videoChannelId) {
146 whereAnd.push({
147 videoChannelId: options.videoChannelId
148 })
149 }
150
151 if (options.type) {
152 whereAnd.push({
153 type: options.type
154 })
155 }
156
157 const where = {
158 [Op.and]: whereAnd
159 }
160
161 const accountScope = {
162 method: [ AccountScopeNames.SUMMARY, actorWhere ]
163 }
164
165 return {
166 where,
167 include: [
168 {
169 model: AccountModel.scope(accountScope),
170 required: true
171 },
172 {
173 model: VideoChannelModel.scope(VideoChannelScopeNames.SUMMARY),
174 required: false
175 }
176 ]
177 } as FindOptions
178 }
179}))
180
181@Table({
182 tableName: 'videoPlaylist',
183 indexes: [
184 {
185 fields: [ 'ownerAccountId' ]
186 },
187 {
188 fields: [ 'videoChannelId' ]
189 },
190 {
191 fields: [ 'url' ],
192 unique: true
193 }
194 ]
195})
196export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
197 @CreatedAt
198 createdAt: Date
199
200 @UpdatedAt
201 updatedAt: Date
202
203 @AllowNull(false)
204 @Is('VideoPlaylistName', value => throwIfNotValid(value, isVideoPlaylistNameValid, 'name'))
205 @Column
206 name: string
207
208 @AllowNull(true)
209 @Is('VideoPlaylistDescription', value => throwIfNotValid(value, isVideoPlaylistDescriptionValid, 'description', true))
210 @Column
211 description: string
212
213 @AllowNull(false)
214 @Is('VideoPlaylistPrivacy', value => throwIfNotValid(value, isVideoPlaylistPrivacyValid, 'privacy'))
215 @Column
216 privacy: VideoPlaylistPrivacy
217
218 @AllowNull(false)
219 @Is('VideoPlaylistUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
220 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS.URL.max))
221 url: string
222
223 @AllowNull(false)
224 @Default(DataType.UUIDV4)
225 @IsUUID(4)
226 @Column(DataType.UUID)
227 uuid: string
228
229 @AllowNull(false)
230 @Default(VideoPlaylistType.REGULAR)
231 @Column
232 type: VideoPlaylistType
233
234 @ForeignKey(() => AccountModel)
235 @Column
236 ownerAccountId: number
237
238 @BelongsTo(() => AccountModel, {
239 foreignKey: {
240 allowNull: false
241 },
242 onDelete: 'CASCADE'
243 })
244 OwnerAccount: AccountModel
245
246 @ForeignKey(() => VideoChannelModel)
247 @Column
248 videoChannelId: number
249
250 @BelongsTo(() => VideoChannelModel, {
251 foreignKey: {
252 allowNull: true
253 },
254 onDelete: 'CASCADE'
255 })
256 VideoChannel: VideoChannelModel
257
258 @HasMany(() => VideoPlaylistElementModel, {
259 foreignKey: {
260 name: 'videoPlaylistId',
261 allowNull: false
262 },
263 onDelete: 'CASCADE'
264 })
265 VideoPlaylistElements: VideoPlaylistElementModel[]
266
267 @HasOne(() => ThumbnailModel, {
268
269 foreignKey: {
270 name: 'videoPlaylistId',
271 allowNull: true
272 },
273 onDelete: 'CASCADE',
274 hooks: true
275 })
276 Thumbnail: ThumbnailModel
277
278 static listForApi (options: {
279 followerActorId: number
280 start: number,
281 count: number,
282 sort: string,
283 type?: VideoPlaylistType,
284 accountId?: number,
285 videoChannelId?: number,
286 privateAndUnlisted?: boolean
287 }) {
288 const query = {
289 offset: options.start,
290 limit: options.count,
291 order: getSort(options.sort)
292 }
293
294 const scopes: (string | ScopeOptions)[] = [
295 {
296 method: [
297 ScopeNames.AVAILABLE_FOR_LIST,
298 {
299 type: options.type,
300 followerActorId: options.followerActorId,
301 accountId: options.accountId,
302 videoChannelId: options.videoChannelId,
303 privateAndUnlisted: options.privateAndUnlisted
304 } as AvailableForListOptions
305 ]
306 },
307 ScopeNames.WITH_VIDEOS_LENGTH,
308 ScopeNames.WITH_THUMBNAIL
309 ]
310
311 return VideoPlaylistModel
312 .scope(scopes)
313 .findAndCountAll(query)
314 .then(({ rows, count }) => {
315 return { total: count, data: rows }
316 })
317 }
318
319 static listPublicUrlsOfForAP (accountId: number, start: number, count: number) {
320 const query = {
321 attributes: [ 'url' ],
322 offset: start,
323 limit: count,
324 where: {
325 ownerAccountId: accountId,
326 privacy: VideoPlaylistPrivacy.PUBLIC
327 }
328 }
329
330 return VideoPlaylistModel.findAndCountAll(query)
331 .then(({ rows, count }) => {
332 return { total: count, data: rows.map(p => p.url) }
333 })
334 }
335
336 static listPlaylistIdsOf (accountId: number, videoIds: number[]) {
337 const query = {
338 attributes: [ 'id' ],
339 where: {
340 ownerAccountId: accountId
341 },
342 include: [
343 {
344 attributes: [ 'videoId', 'startTimestamp', 'stopTimestamp' ],
345 model: VideoPlaylistElementModel.unscoped(),
346 where: {
347 videoId: {
348 [Op.in]: videoIds // FIXME: sequelize ANY seems broken
349 }
350 },
351 required: true
352 }
353 ]
354 }
355
356 return VideoPlaylistModel.findAll(query)
357 }
358
359 static doesPlaylistExist (url: string) {
360 const query = {
361 attributes: [],
362 where: {
363 url
364 }
365 }
366
367 return VideoPlaylistModel
368 .findOne(query)
369 .then(e => !!e)
370 }
371
372 static loadWithAccountAndChannelSummary (id: number | string, transaction: Transaction) {
373 const where = buildWhereIdOrUUID(id)
374
375 const query = {
376 where,
377 transaction
378 }
379
380 return VideoPlaylistModel
381 .scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY, ScopeNames.WITH_VIDEOS_LENGTH, ScopeNames.WITH_THUMBNAIL ])
382 .findOne(query)
383 }
384
385 static loadWithAccountAndChannel (id: number | string, transaction: Transaction) {
386 const where = buildWhereIdOrUUID(id)
387
388 const query = {
389 where,
390 transaction
391 }
392
393 return VideoPlaylistModel
394 .scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL, ScopeNames.WITH_VIDEOS_LENGTH, ScopeNames.WITH_THUMBNAIL ])
395 .findOne(query)
396 }
397
398 static loadByUrlAndPopulateAccount (url: string) {
399 const query = {
400 where: {
401 url
402 }
403 }
404
405 return VideoPlaylistModel.scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_THUMBNAIL ]).findOne(query)
406 }
407
408 static getPrivacyLabel (privacy: VideoPlaylistPrivacy) {
409 return VIDEO_PLAYLIST_PRIVACIES[privacy] || 'Unknown'
410 }
411
412 static getTypeLabel (type: VideoPlaylistType) {
413 return VIDEO_PLAYLIST_TYPES[type] || 'Unknown'
414 }
415
416 static resetPlaylistsOfChannel (videoChannelId: number, transaction: Transaction) {
417 const query = {
418 where: {
419 videoChannelId
420 },
421 transaction
422 }
423
424 return VideoPlaylistModel.update({ privacy: VideoPlaylistPrivacy.PRIVATE, videoChannelId: null }, query)
425 }
426
427 async setAndSaveThumbnail (thumbnail: ThumbnailModel, t: Transaction) {
428 thumbnail.videoPlaylistId = this.id
429
430 this.Thumbnail = await thumbnail.save({ transaction: t })
431 }
432
433 hasThumbnail () {
434 return !!this.Thumbnail
435 }
436
437 generateThumbnailName () {
438 const extension = '.jpg'
439
440 return 'playlist-' + this.uuid + extension
441 }
442
443 getThumbnailUrl () {
444 if (!this.hasThumbnail()) return null
445
446 return WEBSERVER.URL + STATIC_PATHS.THUMBNAILS + this.Thumbnail.filename
447 }
448
449 getThumbnailStaticPath () {
450 if (!this.hasThumbnail()) return null
451
452 return join(STATIC_PATHS.THUMBNAILS, this.Thumbnail.filename)
453 }
454
455 setAsRefreshed () {
456 this.changed('updatedAt', true)
457
458 return this.save()
459 }
460
461 isOwned () {
462 return this.OwnerAccount.isOwned()
463 }
464
465 isOutdated () {
466 if (this.isOwned()) return false
467
468 return isOutdated(this, ACTIVITY_PUB.VIDEO_PLAYLIST_REFRESH_INTERVAL)
469 }
470
471 toFormattedJSON (): VideoPlaylist {
472 return {
473 id: this.id,
474 uuid: this.uuid,
475 isLocal: this.isOwned(),
476
477 displayName: this.name,
478 description: this.description,
479 privacy: {
480 id: this.privacy,
481 label: VideoPlaylistModel.getPrivacyLabel(this.privacy)
482 },
483
484 thumbnailPath: this.getThumbnailStaticPath(),
485
486 type: {
487 id: this.type,
488 label: VideoPlaylistModel.getTypeLabel(this.type)
489 },
490
491 videosLength: this.get('videosLength') as number,
492
493 createdAt: this.createdAt,
494 updatedAt: this.updatedAt,
495
496 ownerAccount: this.OwnerAccount.toFormattedSummaryJSON(),
497 videoChannel: this.VideoChannel ? this.VideoChannel.toFormattedSummaryJSON() : null
498 }
499 }
500
501 toActivityPubObject (page: number, t: Transaction): Promise<PlaylistObject> {
502 const handler = (start: number, count: number) => {
503 return VideoPlaylistElementModel.listUrlsOfForAP(this.id, start, count, t)
504 }
505
506 let icon: ActivityIconObject
507 if (this.hasThumbnail()) {
508 icon = {
509 type: 'Image' as 'Image',
510 url: this.getThumbnailUrl(),
511 mediaType: 'image/jpeg' as 'image/jpeg',
512 width: THUMBNAILS_SIZE.width,
513 height: THUMBNAILS_SIZE.height
514 }
515 }
516
517 return activityPubCollectionPagination(this.url, handler, page)
518 .then(o => {
519 return Object.assign(o, {
520 type: 'Playlist' as 'Playlist',
521 name: this.name,
522 content: this.description,
523 uuid: this.uuid,
524 published: this.createdAt.toISOString(),
525 updated: this.updatedAt.toISOString(),
526 attributedTo: this.VideoChannel ? [ this.VideoChannel.Actor.url ] : [],
527 icon
528 })
529 })
530 }
531}
diff --git a/server/models/video/video-share.ts b/server/models/video/video-share.ts
index c87f71277..fda2d7cea 100644
--- a/server/models/video/video-share.ts
+++ b/server/models/video/video-share.ts
@@ -2,7 +2,7 @@ import * as Sequelize from 'sequelize'
2import * as Bluebird from 'bluebird' 2import * as Bluebird from 'bluebird'
3import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' 3import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
4import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 4import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
5import { CONSTRAINTS_FIELDS } from '../../initializers' 5import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
6import { AccountModel } from '../account/account' 6import { AccountModel } from '../account/account'
7import { ActorModel } from '../activitypub/actor' 7import { ActorModel } from '../activitypub/actor'
8import { throwIfNotValid } from '../utils' 8import { throwIfNotValid } from '../utils'
@@ -14,15 +14,15 @@ enum ScopeNames {
14 WITH_ACTOR = 'WITH_ACTOR' 14 WITH_ACTOR = 'WITH_ACTOR'
15} 15}
16 16
17@Scopes({ 17@Scopes(() => ({
18 [ScopeNames.FULL]: { 18 [ScopeNames.FULL]: {
19 include: [ 19 include: [
20 { 20 {
21 model: () => ActorModel, 21 model: ActorModel,
22 required: true 22 required: true
23 }, 23 },
24 { 24 {
25 model: () => VideoModel, 25 model: VideoModel,
26 required: true 26 required: true
27 } 27 }
28 ] 28 ]
@@ -30,12 +30,12 @@ enum ScopeNames {
30 [ScopeNames.WITH_ACTOR]: { 30 [ScopeNames.WITH_ACTOR]: {
31 include: [ 31 include: [
32 { 32 {
33 model: () => ActorModel, 33 model: ActorModel,
34 required: true 34 required: true
35 } 35 }
36 ] 36 ]
37 } 37 }
38}) 38}))
39@Table({ 39@Table({
40 tableName: 'videoShare', 40 tableName: 'videoShare',
41 indexes: [ 41 indexes: [
@@ -125,7 +125,7 @@ export class VideoShareModel extends Model<VideoShareModel> {
125 .then(res => res.map(r => r.Actor)) 125 .then(res => res.map(r => r.Actor))
126 } 126 }
127 127
128 static loadActorsByVideoOwner (actorOwnerId: number, t: Sequelize.Transaction): Bluebird<ActorModel[]> { 128 static loadActorsWhoSharedVideosOf (actorOwnerId: number, t: Sequelize.Transaction): Bluebird<ActorModel[]> {
129 const query = { 129 const query = {
130 attributes: [], 130 attributes: [],
131 include: [ 131 include: [
@@ -200,4 +200,17 @@ export class VideoShareModel extends Model<VideoShareModel> {
200 200
201 return VideoShareModel.findAndCountAll(query) 201 return VideoShareModel.findAndCountAll(query)
202 } 202 }
203
204 static cleanOldSharesOf (videoId: number, beforeUpdatedAt: Date) {
205 const query = {
206 where: {
207 updatedAt: {
208 [Sequelize.Op.lt]: beforeUpdatedAt
209 },
210 videoId
211 }
212 }
213
214 return VideoShareModel.destroy(query)
215 }
203} 216}
diff --git a/server/models/video/video-streaming-playlist.ts b/server/models/video/video-streaming-playlist.ts
new file mode 100644
index 000000000..31dc82c54
--- /dev/null
+++ b/server/models/video/video-streaming-playlist.ts
@@ -0,0 +1,172 @@
1import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, HasMany, Is, Model, Table, UpdatedAt, DataType } from 'sequelize-typescript'
2import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
3import { throwIfNotValid } from '../utils'
4import { VideoModel } from './video'
5import { VideoRedundancyModel } from '../redundancy/video-redundancy'
6import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
7import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
8import { CONSTRAINTS_FIELDS, STATIC_PATHS, P2P_MEDIA_LOADER_PEER_VERSION } from '../../initializers/constants'
9import { VideoFileModel } from './video-file'
10import { join } from 'path'
11import { sha1 } from '../../helpers/core-utils'
12import { isArrayOf } from '../../helpers/custom-validators/misc'
13import { QueryTypes, Op } from 'sequelize'
14
15@Table({
16 tableName: 'videoStreamingPlaylist',
17 indexes: [
18 {
19 fields: [ 'videoId' ]
20 },
21 {
22 fields: [ 'videoId', 'type' ],
23 unique: true
24 },
25 {
26 fields: [ 'p2pMediaLoaderInfohashes' ],
27 using: 'gin'
28 }
29 ]
30})
31export class VideoStreamingPlaylistModel extends Model<VideoStreamingPlaylistModel> {
32 @CreatedAt
33 createdAt: Date
34
35 @UpdatedAt
36 updatedAt: Date
37
38 @AllowNull(false)
39 @Column
40 type: VideoStreamingPlaylistType
41
42 @AllowNull(false)
43 @Is('PlaylistUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'playlist url'))
44 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max))
45 playlistUrl: string
46
47 @AllowNull(false)
48 @Is('VideoStreamingPlaylistInfoHashes', value => throwIfNotValid(value, v => isArrayOf(v, isVideoFileInfoHashValid), 'info hashes'))
49 @Column(DataType.ARRAY(DataType.STRING))
50 p2pMediaLoaderInfohashes: string[]
51
52 @AllowNull(false)
53 @Column
54 p2pMediaLoaderPeerVersion: number
55
56 @AllowNull(false)
57 @Is('VideoStreamingSegmentsSha256Url', value => throwIfNotValid(value, isActivityPubUrlValid, 'segments sha256 url'))
58 @Column
59 segmentsSha256Url: string
60
61 @ForeignKey(() => VideoModel)
62 @Column
63 videoId: number
64
65 @BelongsTo(() => VideoModel, {
66 foreignKey: {
67 allowNull: false
68 },
69 onDelete: 'CASCADE'
70 })
71 Video: VideoModel
72
73 @HasMany(() => VideoRedundancyModel, {
74 foreignKey: {
75 allowNull: false
76 },
77 onDelete: 'CASCADE',
78 hooks: true
79 })
80 RedundancyVideos: VideoRedundancyModel[]
81
82 static doesInfohashExist (infoHash: string) {
83 const query = 'SELECT 1 FROM "videoStreamingPlaylist" WHERE $infoHash = ANY("p2pMediaLoaderInfohashes") LIMIT 1'
84 const options = {
85 type: QueryTypes.SELECT as QueryTypes.SELECT,
86 bind: { infoHash },
87 raw: true
88 }
89
90 return VideoModel.sequelize.query<object>(query, options)
91 .then(results => results.length === 1)
92 }
93
94 static buildP2PMediaLoaderInfoHashes (playlistUrl: string, videoFiles: VideoFileModel[]) {
95 const hashes: string[] = []
96
97 // https://github.com/Novage/p2p-media-loader/blob/master/p2p-media-loader-core/lib/p2p-media-manager.ts#L115
98 for (let i = 0; i < videoFiles.length; i++) {
99 hashes.push(sha1(`${P2P_MEDIA_LOADER_PEER_VERSION}${playlistUrl}+V${i}`))
100 }
101
102 return hashes
103 }
104
105 static listByIncorrectPeerVersion () {
106 const query = {
107 where: {
108 p2pMediaLoaderPeerVersion: {
109 [Op.ne]: P2P_MEDIA_LOADER_PEER_VERSION
110 }
111 }
112 }
113
114 return VideoStreamingPlaylistModel.findAll(query)
115 }
116
117 static loadWithVideo (id: number) {
118 const options = {
119 include: [
120 {
121 model: VideoModel.unscoped(),
122 required: true
123 }
124 ]
125 }
126
127 return VideoStreamingPlaylistModel.findByPk(id, options)
128 }
129
130 static getHlsPlaylistFilename (resolution: number) {
131 return resolution + '.m3u8'
132 }
133
134 static getMasterHlsPlaylistFilename () {
135 return 'master.m3u8'
136 }
137
138 static getHlsSha256SegmentsFilename () {
139 return 'segments-sha256.json'
140 }
141
142 static getHlsVideoName (uuid: string, resolution: number) {
143 return `${uuid}-${resolution}-fragmented.mp4`
144 }
145
146 static getHlsMasterPlaylistStaticPath (videoUUID: string) {
147 return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getMasterHlsPlaylistFilename())
148 }
149
150 static getHlsPlaylistStaticPath (videoUUID: string, resolution: number) {
151 return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution))
152 }
153
154 static getHlsSha256SegmentsStaticPath (videoUUID: string) {
155 return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getHlsSha256SegmentsFilename())
156 }
157
158 getStringType () {
159 if (this.type === VideoStreamingPlaylistType.HLS) return 'hls'
160
161 return 'unknown'
162 }
163
164 getVideoRedundancyUrl (baseUrlHttp: string) {
165 return baseUrlHttp + STATIC_PATHS.REDUNDANCY + this.getStringType() + '/' + this.Video.uuid
166 }
167
168 hasSameUniqueKeysThan (other: VideoStreamingPlaylistModel) {
169 return this.type === other.type &&
170 this.videoId === other.videoId
171 }
172}
diff --git a/server/models/video/video-views.ts b/server/models/video/video-views.ts
index fde5f7056..40db5effd 100644
--- a/server/models/video/video-views.ts
+++ b/server/models/video/video-views.ts
@@ -4,6 +4,7 @@ import * as Sequelize from 'sequelize'
4 4
5@Table({ 5@Table({
6 tableName: 'videoView', 6 tableName: 'videoView',
7 updatedAt: false,
7 indexes: [ 8 indexes: [
8 { 9 {
9 fields: [ 'videoId' ] 10 fields: [ 'videoId' ]
@@ -41,4 +42,18 @@ export class VideoViewModel extends Model<VideoViewModel> {
41 }) 42 })
42 Video: VideoModel 43 Video: VideoModel
43 44
45 static removeOldRemoteViewsHistory (beforeDate: string) {
46 const query = {
47 where: {
48 startDate: {
49 [Sequelize.Op.lt]: beforeDate
50 },
51 videoId: {
52 [Sequelize.Op.in]: Sequelize.literal('(SELECT "id" FROM "video" WHERE "remote" IS TRUE)')
53 }
54 }
55 }
56
57 return VideoViewModel.destroy(query)
58 }
44} 59}
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 80a6c7832..c0a7892a4 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -3,7 +3,18 @@ import { maxBy } from 'lodash'
3import * as magnetUtil from 'magnet-uri' 3import * as magnetUtil from 'magnet-uri'
4import * as parseTorrent from 'parse-torrent' 4import * as parseTorrent from 'parse-torrent'
5import { join } from 'path' 5import { join } from 'path'
6import * as Sequelize from 'sequelize' 6import {
7 CountOptions,
8 FindOptions,
9 IncludeOptions,
10 ModelIndexesOptions,
11 Op,
12 QueryTypes,
13 ScopeOptions,
14 Sequelize,
15 Transaction,
16 WhereOptions
17} from 'sequelize'
7import { 18import {
8 AllowNull, 19 AllowNull,
9 BeforeDestroy, 20 BeforeDestroy,
@@ -16,8 +27,6 @@ import {
16 ForeignKey, 27 ForeignKey,
17 HasMany, 28 HasMany,
18 HasOne, 29 HasOne,
19 IFindOptions,
20 IIncludeOptions,
21 Is, 30 Is,
22 IsInt, 31 IsInt,
23 IsUUID, 32 IsUUID,
@@ -45,35 +54,43 @@ import {
45 isVideoStateValid, 54 isVideoStateValid,
46 isVideoSupportValid 55 isVideoSupportValid
47} from '../../helpers/custom-validators/videos' 56} from '../../helpers/custom-validators/videos'
48import { generateImageFromVideoFile, getVideoFileResolution } from '../../helpers/ffmpeg-utils' 57import { getVideoFileResolution } from '../../helpers/ffmpeg-utils'
49import { logger } from '../../helpers/logger' 58import { logger } from '../../helpers/logger'
50import { getServerActor } from '../../helpers/utils' 59import { getServerActor } from '../../helpers/utils'
51import { 60import {
52 ACTIVITY_PUB, 61 ACTIVITY_PUB,
53 API_VERSION, 62 API_VERSION,
54 CONFIG,
55 CONSTRAINTS_FIELDS, 63 CONSTRAINTS_FIELDS,
56 PREVIEWS_SIZE, 64 HLS_REDUNDANCY_DIRECTORY,
65 HLS_STREAMING_PLAYLIST_DIRECTORY,
57 REMOTE_SCHEME, 66 REMOTE_SCHEME,
58 STATIC_DOWNLOAD_PATHS, 67 STATIC_DOWNLOAD_PATHS,
59 STATIC_PATHS, 68 STATIC_PATHS,
60 THUMBNAILS_SIZE,
61 VIDEO_CATEGORIES, 69 VIDEO_CATEGORIES,
62 VIDEO_LANGUAGES, 70 VIDEO_LANGUAGES,
63 VIDEO_LICENCES, 71 VIDEO_LICENCES,
64 VIDEO_PRIVACIES, 72 VIDEO_PRIVACIES,
65 VIDEO_STATES 73 VIDEO_STATES,
66} from '../../initializers' 74 WEBSERVER
75} from '../../initializers/constants'
67import { sendDeleteVideo } from '../../lib/activitypub/send' 76import { sendDeleteVideo } from '../../lib/activitypub/send'
68import { AccountModel } from '../account/account' 77import { AccountModel } from '../account/account'
69import { AccountVideoRateModel } from '../account/account-video-rate' 78import { AccountVideoRateModel } from '../account/account-video-rate'
70import { ActorModel } from '../activitypub/actor' 79import { ActorModel } from '../activitypub/actor'
71import { AvatarModel } from '../avatar/avatar' 80import { AvatarModel } from '../avatar/avatar'
72import { ServerModel } from '../server/server' 81import { ServerModel } from '../server/server'
73import { buildBlockedAccountSQL, buildTrigramSearchIndex, createSimilarityAttribute, getVideoSort, throwIfNotValid } from '../utils' 82import {
83 buildBlockedAccountSQL,
84 buildTrigramSearchIndex,
85 buildWhereIdOrUUID,
86 createSimilarityAttribute,
87 getVideoSort,
88 isOutdated,
89 throwIfNotValid
90} from '../utils'
74import { TagModel } from './tag' 91import { TagModel } from './tag'
75import { VideoAbuseModel } from './video-abuse' 92import { VideoAbuseModel } from './video-abuse'
76import { VideoChannelModel } from './video-channel' 93import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel'
77import { VideoCommentModel } from './video-comment' 94import { VideoCommentModel } from './video-comment'
78import { VideoFileModel } from './video-file' 95import { VideoFileModel } from './video-file'
79import { VideoShareModel } from './video-share' 96import { VideoShareModel } from './video-share'
@@ -91,13 +108,17 @@ import {
91 videoModelToFormattedDetailsJSON, 108 videoModelToFormattedDetailsJSON,
92 videoModelToFormattedJSON 109 videoModelToFormattedJSON
93} from './video-format-utils' 110} from './video-format-utils'
94import * as validator from 'validator'
95import { UserVideoHistoryModel } from '../account/user-video-history' 111import { UserVideoHistoryModel } from '../account/user-video-history'
96import { UserModel } from '../account/user' 112import { UserModel } from '../account/user'
97import { VideoImportModel } from './video-import' 113import { VideoImportModel } from './video-import'
114import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
115import { VideoPlaylistElementModel } from './video-playlist-element'
116import { CONFIG } from '../../initializers/config'
117import { ThumbnailModel } from './thumbnail'
118import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
98 119
99// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation 120// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
100const indexes: Sequelize.DefineIndexesOptions[] = [ 121const indexes: (ModelIndexesOptions & { where?: WhereOptions })[] = [
101 buildTrigramSearchIndex('video_name_trigram', 'name'), 122 buildTrigramSearchIndex('video_name_trigram', 'name'),
102 123
103 { fields: [ 'createdAt' ] }, 124 { fields: [ 'createdAt' ] },
@@ -106,10 +127,18 @@ const indexes: Sequelize.DefineIndexesOptions[] = [
106 { fields: [ 'views' ] }, 127 { fields: [ 'views' ] },
107 { fields: [ 'channelId' ] }, 128 { fields: [ 'channelId' ] },
108 { 129 {
130 fields: [ 'originallyPublishedAt' ],
131 where: {
132 originallyPublishedAt: {
133 [Op.ne]: null
134 }
135 }
136 },
137 {
109 fields: [ 'category' ], // We don't care videos with an unknown category 138 fields: [ 'category' ], // We don't care videos with an unknown category
110 where: { 139 where: {
111 category: { 140 category: {
112 [Sequelize.Op.ne]: null 141 [Op.ne]: null
113 } 142 }
114 } 143 }
115 }, 144 },
@@ -117,7 +146,7 @@ const indexes: Sequelize.DefineIndexesOptions[] = [
117 fields: [ 'licence' ], // We don't care videos with an unknown licence 146 fields: [ 'licence' ], // We don't care videos with an unknown licence
118 where: { 147 where: {
119 licence: { 148 licence: {
120 [Sequelize.Op.ne]: null 149 [Op.ne]: null
121 } 150 }
122 } 151 }
123 }, 152 },
@@ -125,7 +154,7 @@ const indexes: Sequelize.DefineIndexesOptions[] = [
125 fields: [ 'language' ], // We don't care videos with an unknown language 154 fields: [ 'language' ], // We don't care videos with an unknown language
126 where: { 155 where: {
127 language: { 156 language: {
128 [Sequelize.Op.ne]: null 157 [Op.ne]: null
129 } 158 }
130 } 159 }
131 }, 160 },
@@ -159,11 +188,17 @@ export enum ScopeNames {
159 WITH_FILES = 'WITH_FILES', 188 WITH_FILES = 'WITH_FILES',
160 WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE', 189 WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE',
161 WITH_BLACKLISTED = 'WITH_BLACKLISTED', 190 WITH_BLACKLISTED = 'WITH_BLACKLISTED',
162 WITH_USER_HISTORY = 'WITH_USER_HISTORY' 191 WITH_USER_HISTORY = 'WITH_USER_HISTORY',
192 WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS',
193 WITH_USER_ID = 'WITH_USER_ID',
194 WITH_THUMBNAILS = 'WITH_THUMBNAILS'
163} 195}
164 196
165type ForAPIOptions = { 197type ForAPIOptions = {
166 ids: number[] 198 ids: number[]
199
200 videoPlaylistId?: number
201
167 withFiles?: boolean 202 withFiles?: boolean
168} 203}
169 204
@@ -171,6 +206,9 @@ type AvailableForListIDsOptions = {
171 serverAccountId: number 206 serverAccountId: number
172 followerActorId: number 207 followerActorId: number
173 includeLocalVideos: boolean 208 includeLocalVideos: boolean
209
210 withoutId?: boolean
211
174 filter?: VideoFilter 212 filter?: VideoFilter
175 categoryOneOf?: number[] 213 categoryOneOf?: number[]
176 nsfw?: boolean 214 nsfw?: boolean
@@ -178,72 +216,38 @@ type AvailableForListIDsOptions = {
178 languageOneOf?: string[] 216 languageOneOf?: string[]
179 tagsOneOf?: string[] 217 tagsOneOf?: string[]
180 tagsAllOf?: string[] 218 tagsAllOf?: string[]
219
181 withFiles?: boolean 220 withFiles?: boolean
221
182 accountId?: number 222 accountId?: number
183 videoChannelId?: number 223 videoChannelId?: number
224
225 videoPlaylistId?: number
226
184 trendingDays?: number 227 trendingDays?: number
185 user?: UserModel, 228 user?: UserModel,
186 historyOfUser?: UserModel 229 historyOfUser?: UserModel
187} 230}
188 231
189@Scopes({ 232@Scopes(() => ({
190 [ ScopeNames.FOR_API ]: (options: ForAPIOptions) => { 233 [ ScopeNames.FOR_API ]: (options: ForAPIOptions) => {
191 const accountInclude = { 234 const query: FindOptions = {
192 attributes: [ 'id', 'name' ], 235 where: {
193 model: AccountModel.unscoped(), 236 id: {
194 required: true, 237 [ Op.in ]: options.ids // FIXME: sequelize ANY seems broken
195 include: [
196 {
197 attributes: [ 'id', 'uuid', 'preferredUsername', 'url', 'serverId', 'avatarId' ],
198 model: ActorModel.unscoped(),
199 required: true,
200 include: [
201 {
202 attributes: [ 'host' ],
203 model: ServerModel.unscoped(),
204 required: false
205 },
206 {
207 model: AvatarModel.unscoped(),
208 required: false
209 }
210 ]
211 } 238 }
212 ] 239 },
213 }
214
215 const videoChannelInclude = {
216 attributes: [ 'name', 'description', 'id' ],
217 model: VideoChannelModel.unscoped(),
218 required: true,
219 include: [ 240 include: [
220 { 241 {
221 attributes: [ 'uuid', 'preferredUsername', 'url', 'serverId', 'avatarId' ], 242 model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, true ] }),
222 model: ActorModel.unscoped(), 243 required: true
223 required: true,
224 include: [
225 {
226 attributes: [ 'host' ],
227 model: ServerModel.unscoped(),
228 required: false
229 },
230 {
231 model: AvatarModel.unscoped(),
232 required: false
233 }
234 ]
235 }, 244 },
236 accountInclude 245 {
237 ] 246 attributes: [ 'type', 'filename' ],
238 } 247 model: ThumbnailModel,
239 248 required: false
240 const query: IFindOptions<VideoModel> = {
241 where: {
242 id: {
243 [ Sequelize.Op.any ]: options.ids
244 } 249 }
245 }, 250 ]
246 include: [ videoChannelInclude ]
247 } 251 }
248 252
249 if (options.withFiles === true) { 253 if (options.withFiles === true) {
@@ -253,24 +257,36 @@ type AvailableForListIDsOptions = {
253 }) 257 })
254 } 258 }
255 259
260 if (options.videoPlaylistId) {
261 query.include.push({
262 model: VideoPlaylistElementModel.unscoped(),
263 required: true,
264 where: {
265 videoPlaylistId: options.videoPlaylistId
266 }
267 })
268 }
269
256 return query 270 return query
257 }, 271 },
258 [ ScopeNames.AVAILABLE_FOR_LIST_IDS ]: (options: AvailableForListIDsOptions) => { 272 [ ScopeNames.AVAILABLE_FOR_LIST_IDS ]: (options: AvailableForListIDsOptions) => {
259 const query: IFindOptions<VideoModel> = { 273 const attributes = options.withoutId === true ? [] : [ 'id' ]
274
275 const query: FindOptions = {
260 raw: true, 276 raw: true,
261 attributes: [ 'id' ], 277 attributes,
262 where: { 278 where: {
263 id: { 279 id: {
264 [ Sequelize.Op.and ]: [ 280 [ Op.and ]: [
265 { 281 {
266 [ Sequelize.Op.notIn ]: Sequelize.literal( 282 [ Op.notIn ]: Sequelize.literal(
267 '(SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")' 283 '(SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")'
268 ) 284 )
269 } 285 }
270 ] 286 ]
271 }, 287 },
272 channelId: { 288 channelId: {
273 [ Sequelize.Op.notIn ]: Sequelize.literal( 289 [ Op.notIn ]: Sequelize.literal(
274 '(' + 290 '(' +
275 'SELECT id FROM "videoChannel" WHERE "accountId" IN (' + 291 'SELECT id FROM "videoChannel" WHERE "accountId" IN (' +
276 buildBlockedAccountSQL(options.serverAccountId, options.user ? options.user.Account.id : undefined) + 292 buildBlockedAccountSQL(options.serverAccountId, options.user ? options.user.Account.id : undefined) +
@@ -288,12 +304,12 @@ type AvailableForListIDsOptions = {
288 // Always list public videos 304 // Always list public videos
289 privacy: VideoPrivacy.PUBLIC, 305 privacy: VideoPrivacy.PUBLIC,
290 // Always list published videos, or videos that are being transcoded but on which we don't want to wait for transcoding 306 // Always list published videos, or videos that are being transcoded but on which we don't want to wait for transcoding
291 [ Sequelize.Op.or ]: [ 307 [ Op.or ]: [
292 { 308 {
293 state: VideoState.PUBLISHED 309 state: VideoState.PUBLISHED
294 }, 310 },
295 { 311 {
296 [ Sequelize.Op.and ]: { 312 [ Op.and ]: {
297 state: VideoState.TO_TRANSCODE, 313 state: VideoState.TO_TRANSCODE,
298 waitTranscoding: false 314 waitTranscoding: false
299 } 315 }
@@ -304,8 +320,21 @@ type AvailableForListIDsOptions = {
304 Object.assign(query.where, privacyWhere) 320 Object.assign(query.where, privacyWhere)
305 } 321 }
306 322
323 if (options.videoPlaylistId) {
324 query.include.push({
325 attributes: [],
326 model: VideoPlaylistElementModel.unscoped(),
327 required: true,
328 where: {
329 videoPlaylistId: options.videoPlaylistId
330 }
331 })
332
333 query.subQuery = false
334 }
335
307 if (options.filter || options.accountId || options.videoChannelId) { 336 if (options.filter || options.accountId || options.videoChannelId) {
308 const videoChannelInclude: IIncludeOptions = { 337 const videoChannelInclude: IncludeOptions = {
309 attributes: [], 338 attributes: [],
310 model: VideoChannelModel.unscoped(), 339 model: VideoChannelModel.unscoped(),
311 required: true 340 required: true
@@ -318,7 +347,7 @@ type AvailableForListIDsOptions = {
318 } 347 }
319 348
320 if (options.filter || options.accountId) { 349 if (options.filter || options.accountId) {
321 const accountInclude: IIncludeOptions = { 350 const accountInclude: IncludeOptions = {
322 attributes: [], 351 attributes: [],
323 model: AccountModel.unscoped(), 352 model: AccountModel.unscoped(),
324 required: true 353 required: true
@@ -358,8 +387,8 @@ type AvailableForListIDsOptions = {
358 387
359 // Force actorId to be a number to avoid SQL injections 388 // Force actorId to be a number to avoid SQL injections
360 const actorIdNumber = parseInt(options.followerActorId.toString(), 10) 389 const actorIdNumber = parseInt(options.followerActorId.toString(), 10)
361 query.where[ 'id' ][ Sequelize.Op.and ].push({ 390 query.where[ 'id' ][ Op.and ].push({
362 [ Sequelize.Op.in ]: Sequelize.literal( 391 [ Op.in ]: Sequelize.literal(
363 '(' + 392 '(' +
364 'SELECT "videoShare"."videoId" AS "id" FROM "videoShare" ' + 393 'SELECT "videoShare"."videoId" AS "id" FROM "videoShare" ' +
365 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' + 394 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' +
@@ -378,8 +407,8 @@ type AvailableForListIDsOptions = {
378 } 407 }
379 408
380 if (options.withFiles === true) { 409 if (options.withFiles === true) {
381 query.where[ 'id' ][ Sequelize.Op.and ].push({ 410 query.where[ 'id' ][ Op.and ].push({
382 [ Sequelize.Op.in ]: Sequelize.literal( 411 [ Op.in ]: Sequelize.literal(
383 '(SELECT "videoId" FROM "videoFile")' 412 '(SELECT "videoId" FROM "videoFile")'
384 ) 413 )
385 }) 414 })
@@ -393,8 +422,8 @@ type AvailableForListIDsOptions = {
393 } 422 }
394 423
395 if (options.tagsOneOf) { 424 if (options.tagsOneOf) {
396 query.where[ 'id' ][ Sequelize.Op.and ].push({ 425 query.where[ 'id' ][ Op.and ].push({
397 [ Sequelize.Op.in ]: Sequelize.literal( 426 [ Op.in ]: Sequelize.literal(
398 '(' + 427 '(' +
399 'SELECT "videoId" FROM "videoTag" ' + 428 'SELECT "videoId" FROM "videoTag" ' +
400 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' + 429 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' +
@@ -405,8 +434,8 @@ type AvailableForListIDsOptions = {
405 } 434 }
406 435
407 if (options.tagsAllOf) { 436 if (options.tagsAllOf) {
408 query.where[ 'id' ][ Sequelize.Op.and ].push({ 437 query.where[ 'id' ][ Op.and ].push({
409 [ Sequelize.Op.in ]: Sequelize.literal( 438 [ Op.in ]: Sequelize.literal(
410 '(' + 439 '(' +
411 'SELECT "videoId" FROM "videoTag" ' + 440 'SELECT "videoId" FROM "videoTag" ' +
412 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' + 441 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' +
@@ -424,19 +453,19 @@ type AvailableForListIDsOptions = {
424 453
425 if (options.categoryOneOf) { 454 if (options.categoryOneOf) {
426 query.where[ 'category' ] = { 455 query.where[ 'category' ] = {
427 [ Sequelize.Op.or ]: options.categoryOneOf 456 [ Op.or ]: options.categoryOneOf
428 } 457 }
429 } 458 }
430 459
431 if (options.licenceOneOf) { 460 if (options.licenceOneOf) {
432 query.where[ 'licence' ] = { 461 query.where[ 'licence' ] = {
433 [ Sequelize.Op.or ]: options.licenceOneOf 462 [ Op.or ]: options.licenceOneOf
434 } 463 }
435 } 464 }
436 465
437 if (options.languageOneOf) { 466 if (options.languageOneOf) {
438 query.where[ 'language' ] = { 467 query.where[ 'language' ] = {
439 [ Sequelize.Op.or ]: options.languageOneOf 468 [ Op.or ]: options.languageOneOf
440 } 469 }
441 } 470 }
442 471
@@ -463,36 +492,60 @@ type AvailableForListIDsOptions = {
463 492
464 return query 493 return query
465 }, 494 },
495 [ ScopeNames.WITH_THUMBNAILS ]: {
496 include: [
497 {
498 model: ThumbnailModel,
499 required: false
500 }
501 ]
502 },
503 [ ScopeNames.WITH_USER_ID ]: {
504 include: [
505 {
506 attributes: [ 'accountId' ],
507 model: VideoChannelModel.unscoped(),
508 required: true,
509 include: [
510 {
511 attributes: [ 'userId' ],
512 model: AccountModel.unscoped(),
513 required: true
514 }
515 ]
516 }
517 ]
518 },
466 [ ScopeNames.WITH_ACCOUNT_DETAILS ]: { 519 [ ScopeNames.WITH_ACCOUNT_DETAILS ]: {
467 include: [ 520 include: [
468 { 521 {
469 model: () => VideoChannelModel.unscoped(), 522 model: VideoChannelModel.unscoped(),
470 required: true, 523 required: true,
471 include: [ 524 include: [
472 { 525 {
473 attributes: { 526 attributes: {
474 exclude: [ 'privateKey', 'publicKey' ] 527 exclude: [ 'privateKey', 'publicKey' ]
475 }, 528 },
476 model: () => ActorModel.unscoped(), 529 model: ActorModel.unscoped(),
477 required: true, 530 required: true,
478 include: [ 531 include: [
479 { 532 {
480 attributes: [ 'host' ], 533 attributes: [ 'host' ],
481 model: () => ServerModel.unscoped(), 534 model: ServerModel.unscoped(),
482 required: false 535 required: false
483 }, 536 },
484 { 537 {
485 model: () => AvatarModel.unscoped(), 538 model: AvatarModel.unscoped(),
486 required: false 539 required: false
487 } 540 }
488 ] 541 ]
489 }, 542 },
490 { 543 {
491 model: () => AccountModel.unscoped(), 544 model: AccountModel.unscoped(),
492 required: true, 545 required: true,
493 include: [ 546 include: [
494 { 547 {
495 model: () => ActorModel.unscoped(), 548 model: ActorModel.unscoped(),
496 attributes: { 549 attributes: {
497 exclude: [ 'privateKey', 'publicKey' ] 550 exclude: [ 'privateKey', 'publicKey' ]
498 }, 551 },
@@ -500,11 +553,11 @@ type AvailableForListIDsOptions = {
500 include: [ 553 include: [
501 { 554 {
502 attributes: [ 'host' ], 555 attributes: [ 'host' ],
503 model: () => ServerModel.unscoped(), 556 model: ServerModel.unscoped(),
504 required: false 557 required: false
505 }, 558 },
506 { 559 {
507 model: () => AvatarModel.unscoped(), 560 model: AvatarModel.unscoped(),
508 required: false 561 required: false
509 } 562 }
510 ] 563 ]
@@ -516,38 +569,69 @@ type AvailableForListIDsOptions = {
516 ] 569 ]
517 }, 570 },
518 [ ScopeNames.WITH_TAGS ]: { 571 [ ScopeNames.WITH_TAGS ]: {
519 include: [ () => TagModel ] 572 include: [ TagModel ]
520 }, 573 },
521 [ ScopeNames.WITH_BLACKLISTED ]: { 574 [ ScopeNames.WITH_BLACKLISTED ]: {
522 include: [ 575 include: [
523 { 576 {
524 attributes: [ 'id', 'reason' ], 577 attributes: [ 'id', 'reason' ],
525 model: () => VideoBlacklistModel, 578 model: VideoBlacklistModel,
526 required: false 579 required: false
527 } 580 }
528 ] 581 ]
529 }, 582 },
530 [ ScopeNames.WITH_FILES ]: { 583 [ ScopeNames.WITH_FILES ]: (withRedundancies = false) => {
531 include: [ 584 let subInclude: any[] = []
532 { 585
533 model: () => VideoFileModel.unscoped(), 586 if (withRedundancies === true) {
534 // FIXME: typings 587 subInclude = [
535 [ 'separate' as any ]: true, // We may have multiple files, having multiple redundancies so let's separate this join 588 {
536 required: false, 589 attributes: [ 'fileUrl' ],
537 include: [ 590 model: VideoRedundancyModel.unscoped(),
538 { 591 required: false
539 attributes: [ 'fileUrl' ], 592 }
540 model: () => VideoRedundancyModel.unscoped(), 593 ]
541 required: false 594 }
542 } 595
543 ] 596 return {
544 } 597 include: [
545 ] 598 {
599 model: VideoFileModel.unscoped(),
600 separate: true, // We may have multiple files, having multiple redundancies so let's separate this join
601 required: false,
602 include: subInclude
603 }
604 ]
605 }
606 },
607 [ ScopeNames.WITH_STREAMING_PLAYLISTS ]: (withRedundancies = false) => {
608 let subInclude: any[] = []
609
610 if (withRedundancies === true) {
611 subInclude = [
612 {
613 attributes: [ 'fileUrl' ],
614 model: VideoRedundancyModel.unscoped(),
615 required: false
616 }
617 ]
618 }
619
620 return {
621 include: [
622 {
623 model: VideoStreamingPlaylistModel.unscoped(),
624 separate: true, // We may have multiple streaming playlists, having multiple redundancies so let's separate this join
625 required: false,
626 include: subInclude
627 }
628 ]
629 }
546 }, 630 },
547 [ ScopeNames.WITH_SCHEDULED_UPDATE ]: { 631 [ ScopeNames.WITH_SCHEDULED_UPDATE ]: {
548 include: [ 632 include: [
549 { 633 {
550 model: () => ScheduleVideoUpdateModel.unscoped(), 634 model: ScheduleVideoUpdateModel.unscoped(),
551 required: false 635 required: false
552 } 636 }
553 ] 637 ]
@@ -566,7 +650,7 @@ type AvailableForListIDsOptions = {
566 ] 650 ]
567 } 651 }
568 } 652 }
569}) 653}))
570@Table({ 654@Table({
571 tableName: 'video', 655 tableName: 'video',
572 indexes 656 indexes
@@ -586,19 +670,19 @@ export class VideoModel extends Model<VideoModel> {
586 670
587 @AllowNull(true) 671 @AllowNull(true)
588 @Default(null) 672 @Default(null)
589 @Is('VideoCategory', value => throwIfNotValid(value, isVideoCategoryValid, 'category')) 673 @Is('VideoCategory', value => throwIfNotValid(value, isVideoCategoryValid, 'category', true))
590 @Column 674 @Column
591 category: number 675 category: number
592 676
593 @AllowNull(true) 677 @AllowNull(true)
594 @Default(null) 678 @Default(null)
595 @Is('VideoLicence', value => throwIfNotValid(value, isVideoLicenceValid, 'licence')) 679 @Is('VideoLicence', value => throwIfNotValid(value, isVideoLicenceValid, 'licence', true))
596 @Column 680 @Column
597 licence: number 681 licence: number
598 682
599 @AllowNull(true) 683 @AllowNull(true)
600 @Default(null) 684 @Default(null)
601 @Is('VideoLanguage', value => throwIfNotValid(value, isVideoLanguageValid, 'language')) 685 @Is('VideoLanguage', value => throwIfNotValid(value, isVideoLanguageValid, 'language', true))
602 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.LANGUAGE.max)) 686 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.LANGUAGE.max))
603 language: string 687 language: string
604 688
@@ -614,13 +698,13 @@ export class VideoModel extends Model<VideoModel> {
614 698
615 @AllowNull(true) 699 @AllowNull(true)
616 @Default(null) 700 @Default(null)
617 @Is('VideoDescription', value => throwIfNotValid(value, isVideoDescriptionValid, 'description')) 701 @Is('VideoDescription', value => throwIfNotValid(value, isVideoDescriptionValid, 'description', true))
618 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max)) 702 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max))
619 description: string 703 description: string
620 704
621 @AllowNull(true) 705 @AllowNull(true)
622 @Default(null) 706 @Default(null)
623 @Is('VideoSupport', value => throwIfNotValid(value, isVideoSupportValid, 'support')) 707 @Is('VideoSupport', value => throwIfNotValid(value, isVideoSupportValid, 'support', true))
624 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.SUPPORT.max)) 708 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.SUPPORT.max))
625 support: string 709 support: string
626 710
@@ -665,6 +749,10 @@ export class VideoModel extends Model<VideoModel> {
665 749
666 @AllowNull(false) 750 @AllowNull(false)
667 @Column 751 @Column
752 downloadEnabled: boolean
753
754 @AllowNull(false)
755 @Column
668 waitTranscoding: boolean 756 waitTranscoding: boolean
669 757
670 @AllowNull(false) 758 @AllowNull(false)
@@ -680,10 +768,15 @@ export class VideoModel extends Model<VideoModel> {
680 updatedAt: Date 768 updatedAt: Date
681 769
682 @AllowNull(false) 770 @AllowNull(false)
683 @Default(Sequelize.NOW) 771 @Default(DataType.NOW)
684 @Column 772 @Column
685 publishedAt: Date 773 publishedAt: Date
686 774
775 @AllowNull(true)
776 @Default(null)
777 @Column
778 originallyPublishedAt: Date
779
687 @ForeignKey(() => VideoChannelModel) 780 @ForeignKey(() => VideoChannelModel)
688 @Column 781 @Column
689 channelId: number 782 channelId: number
@@ -703,6 +796,25 @@ export class VideoModel extends Model<VideoModel> {
703 }) 796 })
704 Tags: TagModel[] 797 Tags: TagModel[]
705 798
799 @HasMany(() => ThumbnailModel, {
800 foreignKey: {
801 name: 'videoId',
802 allowNull: true
803 },
804 hooks: true,
805 onDelete: 'cascade'
806 })
807 Thumbnails: ThumbnailModel[]
808
809 @HasMany(() => VideoPlaylistElementModel, {
810 foreignKey: {
811 name: 'videoId',
812 allowNull: false
813 },
814 onDelete: 'cascade'
815 })
816 VideoPlaylistElements: VideoPlaylistElementModel[]
817
706 @HasMany(() => VideoAbuseModel, { 818 @HasMany(() => VideoAbuseModel, {
707 foreignKey: { 819 foreignKey: {
708 name: 'videoId', 820 name: 'videoId',
@@ -722,6 +834,16 @@ export class VideoModel extends Model<VideoModel> {
722 }) 834 })
723 VideoFiles: VideoFileModel[] 835 VideoFiles: VideoFileModel[]
724 836
837 @HasMany(() => VideoStreamingPlaylistModel, {
838 foreignKey: {
839 name: 'videoId',
840 allowNull: false
841 },
842 hooks: true,
843 onDelete: 'cascade'
844 })
845 VideoStreamingPlaylists: VideoStreamingPlaylistModel[]
846
725 @HasMany(() => VideoShareModel, { 847 @HasMany(() => VideoShareModel, {
726 foreignKey: { 848 foreignKey: {
727 name: 'videoId', 849 name: 'videoId',
@@ -833,20 +955,19 @@ export class VideoModel extends Model<VideoModel> {
833 955
834 logger.info('Removing files of video %s.', instance.url) 956 logger.info('Removing files of video %s.', instance.url)
835 957
836 tasks.push(instance.removeThumbnail())
837
838 if (instance.isOwned()) { 958 if (instance.isOwned()) {
839 if (!Array.isArray(instance.VideoFiles)) { 959 if (!Array.isArray(instance.VideoFiles)) {
840 instance.VideoFiles = await instance.$get('VideoFiles') as VideoFileModel[] 960 instance.VideoFiles = await instance.$get('VideoFiles') as VideoFileModel[]
841 } 961 }
842 962
843 tasks.push(instance.removePreview())
844
845 // Remove physical files and torrents 963 // Remove physical files and torrents
846 instance.VideoFiles.forEach(file => { 964 instance.VideoFiles.forEach(file => {
847 tasks.push(instance.removeFile(file)) 965 tasks.push(instance.removeFile(file))
848 tasks.push(instance.removeTorrent(file)) 966 tasks.push(instance.removeTorrent(file))
849 }) 967 })
968
969 // Remove playlists file
970 tasks.push(instance.removeStreamingPlaylist())
850 } 971 }
851 972
852 // Do not wait video deletion because we could be in a transaction 973 // Do not wait video deletion because we could be in a transaction
@@ -858,10 +979,6 @@ export class VideoModel extends Model<VideoModel> {
858 return undefined 979 return undefined
859 } 980 }
860 981
861 static list () {
862 return VideoModel.scope(ScopeNames.WITH_FILES).findAll()
863 }
864
865 static listLocal () { 982 static listLocal () {
866 const query = { 983 const query = {
867 where: { 984 where: {
@@ -869,7 +986,11 @@ export class VideoModel extends Model<VideoModel> {
869 } 986 }
870 } 987 }
871 988
872 return VideoModel.scope(ScopeNames.WITH_FILES).findAll(query) 989 return VideoModel.scope([
990 ScopeNames.WITH_FILES,
991 ScopeNames.WITH_STREAMING_PLAYLISTS,
992 ScopeNames.WITH_THUMBNAILS
993 ]).findAll(query)
873 } 994 }
874 995
875 static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) { 996 static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) {
@@ -892,12 +1013,12 @@ export class VideoModel extends Model<VideoModel> {
892 distinct: true, 1013 distinct: true,
893 offset: start, 1014 offset: start,
894 limit: count, 1015 limit: count,
895 order: getVideoSort('createdAt', [ 'Tags', 'name', 'ASC' ]), 1016 order: getVideoSort('createdAt', [ 'Tags', 'name', 'ASC' ] as any), // FIXME: sequelize typings
896 where: { 1017 where: {
897 id: { 1018 id: {
898 [ Sequelize.Op.in ]: Sequelize.literal('(' + rawQuery + ')') 1019 [ Op.in ]: Sequelize.literal('(' + rawQuery + ')')
899 }, 1020 },
900 [ Sequelize.Op.or ]: [ 1021 [ Op.or ]: [
901 { privacy: VideoPrivacy.PUBLIC }, 1022 { privacy: VideoPrivacy.PUBLIC },
902 { privacy: VideoPrivacy.UNLISTED } 1023 { privacy: VideoPrivacy.UNLISTED }
903 ] 1024 ]
@@ -914,10 +1035,10 @@ export class VideoModel extends Model<VideoModel> {
914 required: false, 1035 required: false,
915 // We only want videos shared by this actor 1036 // We only want videos shared by this actor
916 where: { 1037 where: {
917 [ Sequelize.Op.and ]: [ 1038 [ Op.and ]: [
918 { 1039 {
919 id: { 1040 id: {
920 [ Sequelize.Op.not ]: null 1041 [ Op.not ]: null
921 } 1042 }
922 }, 1043 },
923 { 1044 {
@@ -961,9 +1082,8 @@ export class VideoModel extends Model<VideoModel> {
961 } 1082 }
962 1083
963 return Bluebird.all([ 1084 return Bluebird.all([
964 // FIXME: typing issue 1085 VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findAll(query),
965 VideoModel.findAll(query as any), 1086 VideoModel.sequelize.query<{ total: string }>(rawCountQuery, { type: QueryTypes.SELECT })
966 VideoModel.sequelize.query(rawCountQuery, { type: Sequelize.QueryTypes.SELECT })
967 ]).then(([ rows, totals ]) => { 1087 ]).then(([ rows, totals ]) => {
968 // totals: totalVideos + totalVideoShares 1088 // totals: totalVideos + totalVideoShares
969 let totalVideos = 0 1089 let totalVideos = 0
@@ -980,43 +1100,49 @@ export class VideoModel extends Model<VideoModel> {
980 } 1100 }
981 1101
982 static listUserVideosForApi (accountId: number, start: number, count: number, sort: string, withFiles = false) { 1102 static listUserVideosForApi (accountId: number, start: number, count: number, sort: string, withFiles = false) {
983 const query: IFindOptions<VideoModel> = { 1103 function buildBaseQuery (): FindOptions {
984 offset: start, 1104 return {
985 limit: count, 1105 offset: start,
986 order: getVideoSort(sort), 1106 limit: count,
987 include: [ 1107 order: getVideoSort(sort),
988 { 1108 include: [
989 model: VideoChannelModel, 1109 {
990 required: true, 1110 model: VideoChannelModel,
991 include: [ 1111 required: true,
992 { 1112 include: [
993 model: AccountModel, 1113 {
994 where: { 1114 model: AccountModel,
995 id: accountId 1115 where: {
996 }, 1116 id: accountId
997 required: true 1117 },
998 } 1118 required: true
999 ] 1119 }
1000 }, 1120 ]
1001 { 1121 }
1002 model: ScheduleVideoUpdateModel, 1122 ]
1003 required: false 1123 }
1004 },
1005 {
1006 model: VideoBlacklistModel,
1007 required: false
1008 }
1009 ]
1010 } 1124 }
1011 1125
1126 const countQuery = buildBaseQuery()
1127 const findQuery = buildBaseQuery()
1128
1129 const findScopes = [
1130 ScopeNames.WITH_SCHEDULED_UPDATE,
1131 ScopeNames.WITH_BLACKLISTED,
1132 ScopeNames.WITH_THUMBNAILS
1133 ]
1134
1012 if (withFiles === true) { 1135 if (withFiles === true) {
1013 query.include.push({ 1136 findQuery.include.push({
1014 model: VideoFileModel.unscoped(), 1137 model: VideoFileModel.unscoped(),
1015 required: true 1138 required: true
1016 }) 1139 })
1017 } 1140 }
1018 1141
1019 return VideoModel.findAndCountAll(query).then(({ rows, count }) => { 1142 return Promise.all([
1143 VideoModel.count(countQuery),
1144 VideoModel.scope(findScopes).findAll(findQuery)
1145 ]).then(([ count, rows ]) => {
1020 return { 1146 return {
1021 data: rows, 1147 data: rows,
1022 total: count 1148 total: count
@@ -1040,6 +1166,7 @@ export class VideoModel extends Model<VideoModel> {
1040 accountId?: number, 1166 accountId?: number,
1041 videoChannelId?: number, 1167 videoChannelId?: number,
1042 followerActorId?: number 1168 followerActorId?: number
1169 videoPlaylistId?: number,
1043 trendingDays?: number, 1170 trendingDays?: number,
1044 user?: UserModel, 1171 user?: UserModel,
1045 historyOfUser?: UserModel 1172 historyOfUser?: UserModel
@@ -1048,7 +1175,7 @@ export class VideoModel extends Model<VideoModel> {
1048 throw new Error('Try to filter all-local but no user has not the see all videos right') 1175 throw new Error('Try to filter all-local but no user has not the see all videos right')
1049 } 1176 }
1050 1177
1051 const query: IFindOptions<VideoModel> = { 1178 const query: FindOptions = {
1052 offset: options.start, 1179 offset: options.start,
1053 limit: options.count, 1180 limit: options.count,
1054 order: getVideoSort(options.sort) 1181 order: getVideoSort(options.sort)
@@ -1079,6 +1206,7 @@ export class VideoModel extends Model<VideoModel> {
1079 withFiles: options.withFiles, 1206 withFiles: options.withFiles,
1080 accountId: options.accountId, 1207 accountId: options.accountId,
1081 videoChannelId: options.videoChannelId, 1208 videoChannelId: options.videoChannelId,
1209 videoPlaylistId: options.videoPlaylistId,
1082 includeLocalVideos: options.includeLocalVideos, 1210 includeLocalVideos: options.includeLocalVideos,
1083 user: options.user, 1211 user: options.user,
1084 historyOfUser: options.historyOfUser, 1212 historyOfUser: options.historyOfUser,
@@ -1096,6 +1224,8 @@ export class VideoModel extends Model<VideoModel> {
1096 sort?: string 1224 sort?: string
1097 startDate?: string // ISO 8601 1225 startDate?: string // ISO 8601
1098 endDate?: string // ISO 8601 1226 endDate?: string // ISO 8601
1227 originallyPublishedStartDate?: string
1228 originallyPublishedEndDate?: string
1099 nsfw?: boolean 1229 nsfw?: boolean
1100 categoryOneOf?: number[] 1230 categoryOneOf?: number[]
1101 licenceOneOf?: number[] 1231 licenceOneOf?: number[]
@@ -1112,17 +1242,26 @@ export class VideoModel extends Model<VideoModel> {
1112 if (options.startDate || options.endDate) { 1242 if (options.startDate || options.endDate) {
1113 const publishedAtRange = {} 1243 const publishedAtRange = {}
1114 1244
1115 if (options.startDate) publishedAtRange[ Sequelize.Op.gte ] = options.startDate 1245 if (options.startDate) publishedAtRange[ Op.gte ] = options.startDate
1116 if (options.endDate) publishedAtRange[ Sequelize.Op.lte ] = options.endDate 1246 if (options.endDate) publishedAtRange[ Op.lte ] = options.endDate
1117 1247
1118 whereAnd.push({ publishedAt: publishedAtRange }) 1248 whereAnd.push({ publishedAt: publishedAtRange })
1119 } 1249 }
1120 1250
1251 if (options.originallyPublishedStartDate || options.originallyPublishedEndDate) {
1252 const originallyPublishedAtRange = {}
1253
1254 if (options.originallyPublishedStartDate) originallyPublishedAtRange[ Op.gte ] = options.originallyPublishedStartDate
1255 if (options.originallyPublishedEndDate) originallyPublishedAtRange[ Op.lte ] = options.originallyPublishedEndDate
1256
1257 whereAnd.push({ originallyPublishedAt: originallyPublishedAtRange })
1258 }
1259
1121 if (options.durationMin || options.durationMax) { 1260 if (options.durationMin || options.durationMax) {
1122 const durationRange = {} 1261 const durationRange = {}
1123 1262
1124 if (options.durationMin) durationRange[ Sequelize.Op.gte ] = options.durationMin 1263 if (options.durationMin) durationRange[ Op.gte ] = options.durationMin
1125 if (options.durationMax) durationRange[ Sequelize.Op.lte ] = options.durationMax 1264 if (options.durationMax) durationRange[ Op.lte ] = options.durationMax
1126 1265
1127 whereAnd.push({ duration: durationRange }) 1266 whereAnd.push({ duration: durationRange })
1128 } 1267 }
@@ -1134,7 +1273,7 @@ export class VideoModel extends Model<VideoModel> {
1134 whereAnd.push( 1273 whereAnd.push(
1135 { 1274 {
1136 id: { 1275 id: {
1137 [ Sequelize.Op.in ]: Sequelize.literal( 1276 [ Op.in ]: Sequelize.literal(
1138 '(' + 1277 '(' +
1139 'SELECT "video"."id" FROM "video" ' + 1278 'SELECT "video"."id" FROM "video" ' +
1140 'WHERE ' + 1279 'WHERE ' +
@@ -1160,7 +1299,7 @@ export class VideoModel extends Model<VideoModel> {
1160 ) 1299 )
1161 } 1300 }
1162 1301
1163 const query: IFindOptions<VideoModel> = { 1302 const query: FindOptions = {
1164 attributes: { 1303 attributes: {
1165 include: attributesInclude 1304 include: attributesInclude
1166 }, 1305 },
@@ -1168,7 +1307,7 @@ export class VideoModel extends Model<VideoModel> {
1168 limit: options.count, 1307 limit: options.count,
1169 order: getVideoSort(options.sort), 1308 order: getVideoSort(options.sort),
1170 where: { 1309 where: {
1171 [ Sequelize.Op.and ]: whereAnd 1310 [ Op.and ]: whereAnd
1172 } 1311 }
1173 } 1312 }
1174 1313
@@ -1190,18 +1329,32 @@ export class VideoModel extends Model<VideoModel> {
1190 return VideoModel.getAvailableForApi(query, queryOptions) 1329 return VideoModel.getAvailableForApi(query, queryOptions)
1191 } 1330 }
1192 1331
1193 static load (id: number | string, t?: Sequelize.Transaction) { 1332 static load (id: number | string, t?: Transaction) {
1194 const where = VideoModel.buildWhereIdOrUUID(id) 1333 const where = buildWhereIdOrUUID(id)
1195 const options = { 1334 const options = {
1196 where, 1335 where,
1197 transaction: t 1336 transaction: t
1198 } 1337 }
1199 1338
1200 return VideoModel.findOne(options) 1339 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
1201 } 1340 }
1202 1341
1203 static loadOnlyId (id: number | string, t?: Sequelize.Transaction) { 1342 static loadWithRights (id: number | string, t?: Transaction) {
1204 const where = VideoModel.buildWhereIdOrUUID(id) 1343 const where = buildWhereIdOrUUID(id)
1344 const options = {
1345 where,
1346 transaction: t
1347 }
1348
1349 return VideoModel.scope([
1350 ScopeNames.WITH_BLACKLISTED,
1351 ScopeNames.WITH_USER_ID,
1352 ScopeNames.WITH_THUMBNAILS
1353 ]).findOne(options)
1354 }
1355
1356 static loadOnlyId (id: number | string, t?: Transaction) {
1357 const where = buildWhereIdOrUUID(id)
1205 1358
1206 const options = { 1359 const options = {
1207 attributes: [ 'id' ], 1360 attributes: [ 'id' ],
@@ -1209,12 +1362,15 @@ export class VideoModel extends Model<VideoModel> {
1209 transaction: t 1362 transaction: t
1210 } 1363 }
1211 1364
1212 return VideoModel.findOne(options) 1365 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
1213 } 1366 }
1214 1367
1215 static loadWithFile (id: number, t?: Sequelize.Transaction, logging?: boolean) { 1368 static loadWithFiles (id: number, t?: Transaction, logging?: boolean) {
1216 return VideoModel.scope(ScopeNames.WITH_FILES) 1369 return VideoModel.scope([
1217 .findById(id, { transaction: t, logging }) 1370 ScopeNames.WITH_FILES,
1371 ScopeNames.WITH_STREAMING_PLAYLISTS,
1372 ScopeNames.WITH_THUMBNAILS
1373 ]).findByPk(id, { transaction: t, logging })
1218 } 1374 }
1219 1375
1220 static loadByUUIDWithFile (uuid: string) { 1376 static loadByUUIDWithFile (uuid: string) {
@@ -1224,52 +1380,85 @@ export class VideoModel extends Model<VideoModel> {
1224 } 1380 }
1225 } 1381 }
1226 1382
1227 return VideoModel 1383 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
1228 .scope([ ScopeNames.WITH_FILES ])
1229 .findOne(options)
1230 } 1384 }
1231 1385
1232 static loadByUrl (url: string, transaction?: Sequelize.Transaction) { 1386 static loadByUrl (url: string, transaction?: Transaction) {
1233 const query: IFindOptions<VideoModel> = { 1387 const query: FindOptions = {
1234 where: { 1388 where: {
1235 url 1389 url
1236 }, 1390 },
1237 transaction 1391 transaction
1238 } 1392 }
1239 1393
1240 return VideoModel.findOne(query) 1394 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(query)
1241 } 1395 }
1242 1396
1243 static loadByUrlAndPopulateAccount (url: string, transaction?: Sequelize.Transaction) { 1397 static loadByUrlAndPopulateAccount (url: string, transaction?: Transaction) {
1244 const query: IFindOptions<VideoModel> = { 1398 const query: FindOptions = {
1245 where: { 1399 where: {
1246 url 1400 url
1247 }, 1401 },
1248 transaction 1402 transaction
1249 } 1403 }
1250 1404
1251 return VideoModel.scope([ ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_FILES ]).findOne(query) 1405 return VideoModel.scope([
1406 ScopeNames.WITH_ACCOUNT_DETAILS,
1407 ScopeNames.WITH_FILES,
1408 ScopeNames.WITH_STREAMING_PLAYLISTS,
1409 ScopeNames.WITH_THUMBNAILS
1410 ]).findOne(query)
1252 } 1411 }
1253 1412
1254 static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Sequelize.Transaction, userId?: number) { 1413 static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Transaction, userId?: number) {
1255 const where = VideoModel.buildWhereIdOrUUID(id) 1414 const where = buildWhereIdOrUUID(id)
1256 1415
1257 const options = { 1416 const options = {
1258 order: [ [ 'Tags', 'name', 'ASC' ] ], 1417 order: [ [ 'Tags', 'name', 'ASC' ] ] as any,
1259 where, 1418 where,
1260 transaction: t 1419 transaction: t
1261 } 1420 }
1262 1421
1263 const scopes = [ 1422 const scopes: (string | ScopeOptions)[] = [
1264 ScopeNames.WITH_TAGS, 1423 ScopeNames.WITH_TAGS,
1265 ScopeNames.WITH_BLACKLISTED, 1424 ScopeNames.WITH_BLACKLISTED,
1425 ScopeNames.WITH_ACCOUNT_DETAILS,
1426 ScopeNames.WITH_SCHEDULED_UPDATE,
1266 ScopeNames.WITH_FILES, 1427 ScopeNames.WITH_FILES,
1428 ScopeNames.WITH_STREAMING_PLAYLISTS,
1429 ScopeNames.WITH_THUMBNAILS
1430 ]
1431
1432 if (userId) {
1433 scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] })
1434 }
1435
1436 return VideoModel
1437 .scope(scopes)
1438 .findOne(options)
1439 }
1440
1441 static loadForGetAPI (id: number | string, t?: Transaction, userId?: number) {
1442 const where = buildWhereIdOrUUID(id)
1443
1444 const options = {
1445 order: [ [ 'Tags', 'name', 'ASC' ] ] as any, // FIXME: sequelize typings
1446 where,
1447 transaction: t
1448 }
1449
1450 const scopes: (string | ScopeOptions)[] = [
1451 ScopeNames.WITH_TAGS,
1452 ScopeNames.WITH_BLACKLISTED,
1267 ScopeNames.WITH_ACCOUNT_DETAILS, 1453 ScopeNames.WITH_ACCOUNT_DETAILS,
1268 ScopeNames.WITH_SCHEDULED_UPDATE 1454 ScopeNames.WITH_SCHEDULED_UPDATE,
1455 ScopeNames.WITH_THUMBNAILS,
1456 { method: [ ScopeNames.WITH_FILES, true ] },
1457 { method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] }
1269 ] 1458 ]
1270 1459
1271 if (userId) { 1460 if (userId) {
1272 scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] } as any) // FIXME: typings 1461 scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] })
1273 } 1462 }
1274 1463
1275 return VideoModel 1464 return VideoModel
@@ -1317,7 +1506,7 @@ export class VideoModel extends Model<VideoModel> {
1317 'LIMIT 1' 1506 'LIMIT 1'
1318 1507
1319 const options = { 1508 const options = {
1320 type: Sequelize.QueryTypes.SELECT, 1509 type: QueryTypes.SELECT,
1321 bind: { followerActorId, videoId }, 1510 bind: { followerActorId, videoId },
1322 raw: true 1511 raw: true
1323 } 1512 }
@@ -1334,17 +1523,18 @@ export class VideoModel extends Model<VideoModel> {
1334 const scopeOptions: AvailableForListIDsOptions = { 1523 const scopeOptions: AvailableForListIDsOptions = {
1335 serverAccountId: serverActor.Account.id, 1524 serverAccountId: serverActor.Account.id,
1336 followerActorId, 1525 followerActorId,
1337 includeLocalVideos: true 1526 includeLocalVideos: true,
1527 withoutId: true // Don't break aggregation
1338 } 1528 }
1339 1529
1340 const query: IFindOptions<VideoModel> = { 1530 const query: FindOptions = {
1341 attributes: [ field ], 1531 attributes: [ field ],
1342 limit: count, 1532 limit: count,
1343 group: field, 1533 group: field,
1344 having: Sequelize.where(Sequelize.fn('COUNT', Sequelize.col(field)), { 1534 having: Sequelize.where(
1345 [ Sequelize.Op.gte ]: threshold 1535 Sequelize.fn('COUNT', Sequelize.col(field)), { [ Op.gte ]: threshold }
1346 }) as any, // FIXME: typings 1536 ),
1347 order: [ this.sequelize.random() ] 1537 order: [ (this.sequelize as any).random() ]
1348 } 1538 }
1349 1539
1350 return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST_IDS, scopeOptions ] }) 1540 return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST_IDS, scopeOptions ] })
@@ -1360,7 +1550,7 @@ export class VideoModel extends Model<VideoModel> {
1360 required: false, 1550 required: false,
1361 where: { 1551 where: {
1362 startDate: { 1552 startDate: {
1363 [ Sequelize.Op.gte ]: new Date(new Date().getTime() - (24 * 3600 * 1000) * trendingDays) 1553 [ Op.gte ]: new Date(new Date().getTime() - (24 * 3600 * 1000) * trendingDays)
1364 } 1554 }
1365 } 1555 }
1366 } 1556 }
@@ -1377,11 +1567,11 @@ export class VideoModel extends Model<VideoModel> {
1377 } 1567 }
1378 1568
1379 private static async getAvailableForApi ( 1569 private static async getAvailableForApi (
1380 query: IFindOptions<VideoModel>, 1570 query: FindOptions,
1381 options: AvailableForListIDsOptions, 1571 options: AvailableForListIDsOptions,
1382 countVideos = true 1572 countVideos = true
1383 ) { 1573 ) {
1384 const idsScope = { 1574 const idsScope: ScopeOptions = {
1385 method: [ 1575 method: [
1386 ScopeNames.AVAILABLE_FOR_LIST_IDS, options 1576 ScopeNames.AVAILABLE_FOR_LIST_IDS, options
1387 ] 1577 ]
@@ -1389,8 +1579,8 @@ export class VideoModel extends Model<VideoModel> {
1389 1579
1390 // Remove trending sort on count, because it uses a group by 1580 // Remove trending sort on count, because it uses a group by
1391 const countOptions = Object.assign({}, options, { trendingDays: undefined }) 1581 const countOptions = Object.assign({}, options, { trendingDays: undefined })
1392 const countQuery = Object.assign({}, query, { attributes: undefined, group: undefined }) 1582 const countQuery: CountOptions = Object.assign({}, query, { attributes: undefined, group: undefined })
1393 const countScope = { 1583 const countScope: ScopeOptions = {
1394 method: [ 1584 method: [
1395 ScopeNames.AVAILABLE_FOR_LIST_IDS, countOptions 1585 ScopeNames.AVAILABLE_FOR_LIST_IDS, countOptions
1396 ] 1586 ]
@@ -1404,18 +1594,7 @@ export class VideoModel extends Model<VideoModel> {
1404 1594
1405 if (ids.length === 0) return { data: [], total: count } 1595 if (ids.length === 0) return { data: [], total: count }
1406 1596
1407 // FIXME: typings 1597 const secondQuery: FindOptions = {
1408 const apiScope: any[] = [
1409 {
1410 method: [ ScopeNames.FOR_API, { ids, withFiles: options.withFiles } as ForAPIOptions ]
1411 }
1412 ]
1413
1414 if (options.user) {
1415 apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.user.id ] })
1416 }
1417
1418 const secondQuery = {
1419 offset: 0, 1598 offset: 0,
1420 limit: query.limit, 1599 limit: query.limit,
1421 attributes: query.attributes, 1600 attributes: query.attributes,
@@ -1425,6 +1604,23 @@ export class VideoModel extends Model<VideoModel> {
1425 ) 1604 )
1426 ] 1605 ]
1427 } 1606 }
1607
1608 const apiScope: (string | ScopeOptions)[] = []
1609
1610 if (options.user) {
1611 apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.user.id ] })
1612 }
1613
1614 apiScope.push({
1615 method: [
1616 ScopeNames.FOR_API, {
1617 ids,
1618 withFiles: options.withFiles,
1619 videoPlaylistId: options.videoPlaylistId
1620 } as ForAPIOptions
1621 ]
1622 })
1623
1428 const rows = await VideoModel.scope(apiScope).findAll(secondQuery) 1624 const rows = await VideoModel.scope(apiScope).findAll(secondQuery)
1429 1625
1430 return { 1626 return {
@@ -1453,10 +1649,6 @@ export class VideoModel extends Model<VideoModel> {
1453 return VIDEO_STATES[ id ] || 'Unknown' 1649 return VIDEO_STATES[ id ] || 'Unknown'
1454 } 1650 }
1455 1651
1456 static buildWhereIdOrUUID (id: number | string) {
1457 return validator.isInt('' + id) ? { id } : { uuid: id }
1458 }
1459
1460 getOriginalFile () { 1652 getOriginalFile () {
1461 if (Array.isArray(this.VideoFiles) === false) return undefined 1653 if (Array.isArray(this.VideoFiles) === false) return undefined
1462 1654
@@ -1464,19 +1656,41 @@ export class VideoModel extends Model<VideoModel> {
1464 return maxBy(this.VideoFiles, file => file.resolution) 1656 return maxBy(this.VideoFiles, file => file.resolution)
1465 } 1657 }
1466 1658
1659 async addAndSaveThumbnail (thumbnail: ThumbnailModel, transaction: Transaction) {
1660 thumbnail.videoId = this.id
1661
1662 const savedThumbnail = await thumbnail.save({ transaction })
1663
1664 if (Array.isArray(this.Thumbnails) === false) this.Thumbnails = []
1665
1666 // Already have this thumbnail, skip
1667 if (this.Thumbnails.find(t => t.id === savedThumbnail.id)) return
1668
1669 this.Thumbnails.push(savedThumbnail)
1670 }
1671
1467 getVideoFilename (videoFile: VideoFileModel) { 1672 getVideoFilename (videoFile: VideoFileModel) {
1468 return this.uuid + '-' + videoFile.resolution + videoFile.extname 1673 return this.uuid + '-' + videoFile.resolution + videoFile.extname
1469 } 1674 }
1470 1675
1471 getThumbnailName () { 1676 generateThumbnailName () {
1472 // We always have a copy of the thumbnail 1677 return this.uuid + '.jpg'
1473 const extension = '.jpg'
1474 return this.uuid + extension
1475 } 1678 }
1476 1679
1477 getPreviewName () { 1680 getMiniature () {
1478 const extension = '.jpg' 1681 if (Array.isArray(this.Thumbnails) === false) return undefined
1479 return this.uuid + extension 1682
1683 return this.Thumbnails.find(t => t.type === ThumbnailType.MINIATURE)
1684 }
1685
1686 generatePreviewName () {
1687 return this.uuid + '.jpg'
1688 }
1689
1690 getPreview () {
1691 if (Array.isArray(this.Thumbnails) === false) return undefined
1692
1693 return this.Thumbnails.find(t => t.type === ThumbnailType.PREVIEW)
1480 } 1694 }
1481 1695
1482 getTorrentFileName (videoFile: VideoFileModel) { 1696 getTorrentFileName (videoFile: VideoFileModel) {
@@ -1488,24 +1702,6 @@ export class VideoModel extends Model<VideoModel> {
1488 return this.remote === false 1702 return this.remote === false
1489 } 1703 }
1490 1704
1491 createPreview (videoFile: VideoFileModel) {
1492 return generateImageFromVideoFile(
1493 this.getVideoFilePath(videoFile),
1494 CONFIG.STORAGE.PREVIEWS_DIR,
1495 this.getPreviewName(),
1496 PREVIEWS_SIZE
1497 )
1498 }
1499
1500 createThumbnail (videoFile: VideoFileModel) {
1501 return generateImageFromVideoFile(
1502 this.getVideoFilePath(videoFile),
1503 CONFIG.STORAGE.THUMBNAILS_DIR,
1504 this.getThumbnailName(),
1505 THUMBNAILS_SIZE
1506 )
1507 }
1508
1509 getTorrentFilePath (videoFile: VideoFileModel) { 1705 getTorrentFilePath (videoFile: VideoFileModel) {
1510 return join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile)) 1706 return join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
1511 } 1707 }
@@ -1520,10 +1716,10 @@ export class VideoModel extends Model<VideoModel> {
1520 name: `${this.name} ${videoFile.resolution}p${videoFile.extname}`, 1716 name: `${this.name} ${videoFile.resolution}p${videoFile.extname}`,
1521 createdBy: 'PeerTube', 1717 createdBy: 'PeerTube',
1522 announceList: [ 1718 announceList: [
1523 [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ], 1719 [ WEBSERVER.WS + '://' + WEBSERVER.HOSTNAME + ':' + WEBSERVER.PORT + '/tracker/socket' ],
1524 [ CONFIG.WEBSERVER.URL + '/tracker/announce' ] 1720 [ WEBSERVER.URL + '/tracker/announce' ]
1525 ], 1721 ],
1526 urlList: [ CONFIG.WEBSERVER.URL + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) ] 1722 urlList: [ WEBSERVER.URL + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) ]
1527 } 1723 }
1528 1724
1529 const torrent = await createTorrentPromise(this.getVideoFilePath(videoFile), options) 1725 const torrent = await createTorrentPromise(this.getVideoFilePath(videoFile), options)
@@ -1545,12 +1741,19 @@ export class VideoModel extends Model<VideoModel> {
1545 return '/videos/embed/' + this.uuid 1741 return '/videos/embed/' + this.uuid
1546 } 1742 }
1547 1743
1548 getThumbnailStaticPath () { 1744 getMiniatureStaticPath () {
1549 return join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName()) 1745 const thumbnail = this.getMiniature()
1746 if (!thumbnail) return null
1747
1748 return join(STATIC_PATHS.THUMBNAILS, thumbnail.filename)
1550 } 1749 }
1551 1750
1552 getPreviewStaticPath () { 1751 getPreviewStaticPath () {
1553 return join(STATIC_PATHS.PREVIEWS, this.getPreviewName()) 1752 const preview = this.getPreview()
1753 if (!preview) return null
1754
1755 // We use a local cache, so specify our cache endpoint instead of potential remote URL
1756 return join(STATIC_PATHS.PREVIEWS, preview.filename)
1554 } 1757 }
1555 1758
1556 toFormattedJSON (options?: VideoFormattingJSONOptions): Video { 1759 toFormattedJSON (options?: VideoFormattingJSONOptions): Video {
@@ -1586,18 +1789,6 @@ export class VideoModel extends Model<VideoModel> {
1586 return `/api/${API_VERSION}/videos/${this.uuid}/description` 1789 return `/api/${API_VERSION}/videos/${this.uuid}/description`
1587 } 1790 }
1588 1791
1589 removeThumbnail () {
1590 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
1591 return remove(thumbnailPath)
1592 .catch(err => logger.warn('Cannot delete thumbnail %s.', thumbnailPath, { err }))
1593 }
1594
1595 removePreview () {
1596 const previewPath = join(CONFIG.STORAGE.PREVIEWS_DIR + this.getPreviewName())
1597 return remove(previewPath)
1598 .catch(err => logger.warn('Cannot delete preview %s.', previewPath, { err }))
1599 }
1600
1601 removeFile (videoFile: VideoFileModel, isRedundancy = false) { 1792 removeFile (videoFile: VideoFileModel, isRedundancy = false) {
1602 const baseDir = isRedundancy ? CONFIG.STORAGE.REDUNDANCY_DIR : CONFIG.STORAGE.VIDEOS_DIR 1793 const baseDir = isRedundancy ? CONFIG.STORAGE.REDUNDANCY_DIR : CONFIG.STORAGE.VIDEOS_DIR
1603 1794
@@ -1612,15 +1803,18 @@ export class VideoModel extends Model<VideoModel> {
1612 .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err })) 1803 .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err }))
1613 } 1804 }
1614 1805
1806 removeStreamingPlaylist (isRedundancy = false) {
1807 const baseDir = isRedundancy ? HLS_REDUNDANCY_DIRECTORY : HLS_STREAMING_PLAYLIST_DIRECTORY
1808
1809 const filePath = join(baseDir, this.uuid)
1810 return remove(filePath)
1811 .catch(err => logger.warn('Cannot delete playlist directory %s.', filePath, { err }))
1812 }
1813
1615 isOutdated () { 1814 isOutdated () {
1616 if (this.isOwned()) return false 1815 if (this.isOwned()) return false
1617 1816
1618 const now = Date.now() 1817 return isOutdated(this, ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL)
1619 const createdAtTime = this.createdAt.getTime()
1620 const updatedAtTime = this.updatedAt.getTime()
1621
1622 return (now - createdAtTime) > ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL &&
1623 (now - updatedAtTime) > ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL
1624 } 1818 }
1625 1819
1626 setAsRefreshed () { 1820 setAsRefreshed () {
@@ -1634,8 +1828,8 @@ export class VideoModel extends Model<VideoModel> {
1634 let baseUrlWs 1828 let baseUrlWs
1635 1829
1636 if (this.isOwned()) { 1830 if (this.isOwned()) {
1637 baseUrlHttp = CONFIG.WEBSERVER.URL 1831 baseUrlHttp = WEBSERVER.URL
1638 baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT 1832 baseUrlWs = WEBSERVER.WS + '://' + WEBSERVER.HOSTNAME + ':' + WEBSERVER.PORT
1639 } else { 1833 } else {
1640 baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + this.VideoChannel.Account.Actor.Server.host 1834 baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + this.VideoChannel.Account.Actor.Server.host
1641 baseUrlWs = REMOTE_SCHEME.WS + '://' + this.VideoChannel.Account.Actor.Server.host 1835 baseUrlWs = REMOTE_SCHEME.WS + '://' + this.VideoChannel.Account.Actor.Server.host
@@ -1646,7 +1840,7 @@ export class VideoModel extends Model<VideoModel> {
1646 1840
1647 generateMagnetUri (videoFile: VideoFileModel, baseUrlHttp: string, baseUrlWs: string) { 1841 generateMagnetUri (videoFile: VideoFileModel, baseUrlHttp: string, baseUrlWs: string) {
1648 const xs = this.getTorrentUrl(videoFile, baseUrlHttp) 1842 const xs = this.getTorrentUrl(videoFile, baseUrlHttp)
1649 const announce = [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ] 1843 const announce = this.getTrackerUrls(baseUrlHttp, baseUrlWs)
1650 let urlList = [ this.getVideoFileUrl(videoFile, baseUrlHttp) ] 1844 let urlList = [ this.getVideoFileUrl(videoFile, baseUrlHttp) ]
1651 1845
1652 const redundancies = videoFile.RedundancyVideos 1846 const redundancies = videoFile.RedundancyVideos
@@ -1663,8 +1857,8 @@ export class VideoModel extends Model<VideoModel> {
1663 return magnetUtil.encode(magnetHash) 1857 return magnetUtil.encode(magnetHash)
1664 } 1858 }
1665 1859
1666 getThumbnailUrl (baseUrlHttp: string) { 1860 getTrackerUrls (baseUrlHttp: string, baseUrlWs: string) {
1667 return baseUrlHttp + STATIC_PATHS.THUMBNAILS + this.getThumbnailName() 1861 return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
1668 } 1862 }
1669 1863
1670 getTorrentUrl (videoFile: VideoFileModel, baseUrlHttp: string) { 1864 getTorrentUrl (videoFile: VideoFileModel, baseUrlHttp: string) {
@@ -1686,4 +1880,8 @@ export class VideoModel extends Model<VideoModel> {
1686 getVideoFileDownloadUrl (videoFile: VideoFileModel, baseUrlHttp: string) { 1880 getVideoFileDownloadUrl (videoFile: VideoFileModel, baseUrlHttp: string) {
1687 return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + this.getVideoFilename(videoFile) 1881 return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + this.getVideoFilename(videoFile)
1688 } 1882 }
1883
1884 getBandwidthBits (videoFile: VideoFileModel) {
1885 return Math.ceil((videoFile.size * 8) / this.duration)
1886 }
1689} 1887}
diff --git a/server/tests/api/activitypub/client.ts b/server/tests/api/activitypub/client.ts
index 6d90d8643..edf588c16 100644
--- a/server/tests/api/activitypub/client.ts
+++ b/server/tests/api/activitypub/client.ts
@@ -11,7 +11,7 @@ import {
11 ServerInfo, 11 ServerInfo,
12 setAccessTokensToServers, 12 setAccessTokensToServers,
13 uploadVideo 13 uploadVideo
14} from '../../../../shared/utils' 14} from '../../../../shared/extra-utils'
15 15
16const expect = chai.expect 16const expect = chai.expect
17 17
@@ -22,8 +22,6 @@ describe('Test activitypub', function () {
22 before(async function () { 22 before(async function () {
23 this.timeout(30000) 23 this.timeout(30000)
24 24
25 await flushTests()
26
27 servers = await flushAndRunMultipleServers(2) 25 servers = await flushAndRunMultipleServers(2)
28 26
29 await setAccessTokensToServers(servers) 27 await setAccessTokensToServers(servers)
@@ -61,7 +59,7 @@ describe('Test activitypub', function () {
61 expect(res.header.location).to.equal('http://localhost:9001/videos/watch/' + videoUUID) 59 expect(res.header.location).to.equal('http://localhost:9001/videos/watch/' + videoUUID)
62 }) 60 })
63 61
64 after(async function () { 62 after(function () {
65 killallServers(servers) 63 killallServers(servers)
66 }) 64 })
67}) 65})
diff --git a/server/tests/api/activitypub/fetch.ts b/server/tests/api/activitypub/fetch.ts
index 03609c1a9..7240bb0fb 100644
--- a/server/tests/api/activitypub/fetch.ts
+++ b/server/tests/api/activitypub/fetch.ts
@@ -3,6 +3,7 @@
3import 'mocha' 3import 'mocha'
4 4
5import { 5import {
6 closeAllSequelize,
6 createUser, 7 createUser,
7 doubleFollow, 8 doubleFollow,
8 flushAndRunMultipleServers, 9 flushAndRunMultipleServers,
@@ -16,7 +17,7 @@ import {
16 uploadVideo, 17 uploadVideo,
17 userLogin, 18 userLogin,
18 waitJobs 19 waitJobs
19} from '../../../../shared/utils' 20} from '../../../../shared/extra-utils'
20import * as chai from 'chai' 21import * as chai from 'chai'
21import { Video } from '../../../../shared/models/videos' 22import { Video } from '../../../../shared/models/videos'
22 23
@@ -37,7 +38,7 @@ describe('Test ActivityPub fetcher', function () {
37 38
38 const user = { username: 'user1', password: 'password' } 39 const user = { username: 'user1', password: 'password' }
39 for (const server of servers) { 40 for (const server of servers) {
40 await createUser(server.url, server.accessToken, user.username, user.password) 41 await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
41 } 42 }
42 43
43 const userAccessToken = await userLogin(servers[0], user) 44 const userAccessToken = await userLogin(servers[0], user)
@@ -79,9 +80,6 @@ describe('Test ActivityPub fetcher', function () {
79 after(async function () { 80 after(async function () {
80 killallServers(servers) 81 killallServers(servers)
81 82
82 // Keep the logs if the test failed 83 await closeAllSequelize(servers)
83 if (this['ok']) {
84 await flushTests()
85 }
86 }) 84 })
87}) 85})
diff --git a/server/tests/api/activitypub/helpers.ts b/server/tests/api/activitypub/helpers.ts
index ac6e755c3..365d0e1ae 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 '../../../../shared/utils/miscs/stubs' 5import { buildRequestStub } from '../../../../shared/extra-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'
diff --git a/server/tests/api/activitypub/index.ts b/server/tests/api/activitypub/index.ts
index 450053309..92bd6f660 100644
--- a/server/tests/api/activitypub/index.ts
+++ b/server/tests/api/activitypub/index.ts
@@ -1,5 +1,5 @@
1import './client' 1import './client'
2import './fetch' 2import './fetch'
3import './helpers'
4import './refresher' 3import './refresher'
4import './helpers'
5import './security' 5import './security'
diff --git a/server/tests/api/activitypub/refresher.ts b/server/tests/api/activitypub/refresher.ts
index 62ad8a0b5..9be9aa495 100644
--- a/server/tests/api/activitypub/refresher.ts
+++ b/server/tests/api/activitypub/refresher.ts
@@ -2,92 +2,158 @@
2 2
3import 'mocha' 3import 'mocha'
4import { 4import {
5 createVideoPlaylist,
5 doubleFollow, 6 doubleFollow,
6 flushAndRunMultipleServers, 7 flushAndRunMultipleServers,
8 generateUserAccessToken,
7 getVideo, 9 getVideo,
8 killallServers, 10 getVideoPlaylist,
11 killallServers, rateVideo,
9 reRunServer, 12 reRunServer,
10 ServerInfo, 13 ServerInfo,
11 setAccessTokensToServers, 14 setAccessTokensToServers,
15 setActorField,
16 setDefaultVideoChannel,
17 setPlaylistField,
18 setVideoField,
12 uploadVideo, 19 uploadVideo,
20 uploadVideoAndGetId,
13 wait, 21 wait,
14 setVideoField,
15 waitJobs 22 waitJobs
16} from '../../../../shared/utils' 23} from '../../../../shared/extra-utils'
24import { getAccount } from '../../../../shared/extra-utils/users/accounts'
25import { VideoPlaylistPrivacy } from '../../../../shared/models/videos'
17 26
18describe('Test AP refresher', function () { 27describe('Test AP refresher', function () {
19 let servers: ServerInfo[] = [] 28 let servers: ServerInfo[] = []
20 let videoUUID1: string 29 let videoUUID1: string
21 let videoUUID2: string 30 let videoUUID2: string
22 let videoUUID3: string 31 let videoUUID3: string
32 let playlistUUID1: string
33 let playlistUUID2: string
23 34
24 before(async function () { 35 before(async function () {
25 this.timeout(60000) 36 this.timeout(60000)
26 37
27 servers = await flushAndRunMultipleServers(2) 38 servers = await flushAndRunMultipleServers(2, { transcoding: { enabled: false } })
28 39
29 // Get the access tokens 40 // Get the access tokens
30 await setAccessTokensToServers(servers) 41 await setAccessTokensToServers(servers)
42 await setDefaultVideoChannel(servers)
43
44 {
45 videoUUID1 = (await uploadVideoAndGetId({ server: servers[ 1 ], videoName: 'video1' })).uuid
46 videoUUID2 = (await uploadVideoAndGetId({ server: servers[ 1 ], videoName: 'video2' })).uuid
47 videoUUID3 = (await uploadVideoAndGetId({ server: servers[ 1 ], videoName: 'video3' })).uuid
48 }
31 49
32 { 50 {
33 const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video1' }) 51 const a1 = await generateUserAccessToken(servers[1], 'user1')
34 videoUUID1 = res.body.video.uuid 52 await uploadVideo(servers[1].url, a1, { name: 'video4' })
53
54 const a2 = await generateUserAccessToken(servers[1], 'user2')
55 await uploadVideo(servers[1].url, a2, { name: 'video5' })
35 } 56 }
36 57
37 { 58 {
38 const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video2' }) 59 const playlistAttrs = { displayName: 'playlist1', privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: servers[1].videoChannel.id }
39 videoUUID2 = res.body.video.uuid 60 const res = await createVideoPlaylist({ url: servers[1].url, token: servers[1].accessToken, playlistAttrs })
61 playlistUUID1 = res.body.videoPlaylist.uuid
40 } 62 }
41 63
42 { 64 {
43 const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video3' }) 65 const playlistAttrs = { displayName: 'playlist2', privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: servers[1].videoChannel.id }
44 videoUUID3 = res.body.video.uuid 66 const res = await createVideoPlaylist({ url: servers[1].url, token: servers[1].accessToken, playlistAttrs })
67 playlistUUID2 = res.body.videoPlaylist.uuid
45 } 68 }
46 69
47 await doubleFollow(servers[0], servers[1]) 70 await doubleFollow(servers[0], servers[1])
48 }) 71 })
49 72
50 it('Should remove a deleted remote video', async function () { 73 describe('Videos refresher', function () {
51 this.timeout(60000) 74
75 it('Should remove a deleted remote video', async function () {
76 this.timeout(60000)
77
78 await wait(10000)
79
80 // Change UUID so the remote server returns a 404
81 await setVideoField(2, videoUUID1, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b174f')
82
83 await getVideo(servers[ 0 ].url, videoUUID1)
84 await getVideo(servers[ 0 ].url, videoUUID2)
85
86 await waitJobs(servers)
52 87
53 await wait(10000) 88 await getVideo(servers[ 0 ].url, videoUUID1, 404)
89 await getVideo(servers[ 0 ].url, videoUUID2, 200)
90 })
54 91
55 // Change UUID so the remote server returns a 404 92 it('Should not update a remote video if the remote instance is down', async function () {
56 await setVideoField(2, videoUUID1, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b174f') 93 this.timeout(60000)
57 94
58 await getVideo(servers[0].url, videoUUID1) 95 killallServers([ servers[ 1 ] ])
59 await getVideo(servers[0].url, videoUUID2)
60 96
61 await waitJobs(servers) 97 await setVideoField(2, videoUUID3, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b174e')
62 98
63 await getVideo(servers[0].url, videoUUID1, 404) 99 // Video will need a refresh
64 await getVideo(servers[0].url, videoUUID2, 200) 100 await wait(10000)
101
102 await getVideo(servers[ 0 ].url, videoUUID3)
103 // The refresh should fail
104 await waitJobs([ servers[ 0 ] ])
105
106 await reRunServer(servers[ 1 ])
107
108 // Should not refresh the video, even if the last refresh failed (to avoir a loop on dead instances)
109 await getVideo(servers[ 0 ].url, videoUUID3)
110 await waitJobs(servers)
111
112 await getVideo(servers[ 0 ].url, videoUUID3, 200)
113 })
65 }) 114 })
66 115
67 it('Should not update a remote video if the remote instance is down', async function () { 116 describe('Actors refresher', function () {
68 this.timeout(60000) 117
118 it('Should remove a deleted actor', async function () {
119 this.timeout(60000)
120
121 await wait(10000)
122
123 // Change actor name so the remote server returns a 404
124 await setActorField(2, 'http://localhost:9002/accounts/user2', 'preferredUsername', 'toto')
125
126 await getAccount(servers[ 0 ].url, 'user1@localhost:9002')
127 await getAccount(servers[ 0 ].url, 'user2@localhost:9002')
128
129 await waitJobs(servers)
130
131 await getAccount(servers[ 0 ].url, 'user1@localhost:9002', 200)
132 await getAccount(servers[ 0 ].url, 'user2@localhost:9002', 404)
133 })
134 })
69 135
70 killallServers([ servers[1] ]) 136 describe('Playlist refresher', function () {
71 137
72 await setVideoField(2, videoUUID3, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b174e') 138 it('Should remove a deleted playlist', async function () {
139 this.timeout(60000)
73 140
74 // Video will need a refresh 141 await wait(10000)
75 await wait(10000)
76 142
77 await getVideo(servers[0].url, videoUUID3) 143 // Change UUID so the remote server returns a 404
78 // The refresh should fail 144 await setPlaylistField(2, playlistUUID2, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b178e')
79 await waitJobs([ servers[0] ])
80 145
81 await reRunServer(servers[1]) 146 await getVideoPlaylist(servers[ 0 ].url, playlistUUID1)
147 await getVideoPlaylist(servers[ 0 ].url, playlistUUID2)
82 148
83 // Should not refresh the video, even if the last refresh failed (to avoir a loop on dead instances) 149 await waitJobs(servers)
84 await getVideo(servers[0].url, videoUUID3)
85 await waitJobs(servers)
86 150
87 await getVideo(servers[0].url, videoUUID3, 200) 151 await getVideoPlaylist(servers[ 0 ].url, playlistUUID1, 200)
152 await getVideoPlaylist(servers[ 0 ].url, playlistUUID2, 404)
153 })
88 }) 154 })
89 155
90 after(async function () { 156 after(function () {
91 killallServers(servers) 157 killallServers(servers)
92 }) 158 })
93}) 159})
diff --git a/server/tests/api/activitypub/security.ts b/server/tests/api/activitypub/security.ts
index 342ae0fa1..11e6859bf 100644
--- a/server/tests/api/activitypub/security.ts
+++ b/server/tests/api/activitypub/security.ts
@@ -3,18 +3,18 @@
3import 'mocha' 3import 'mocha'
4 4
5import { 5import {
6 closeAllSequelize,
6 flushAndRunMultipleServers, 7 flushAndRunMultipleServers,
7 flushTests, 8 flushTests,
8 killallServers, 9 killallServers,
9 makeFollowRequest,
10 makePOSTAPRequest,
11 ServerInfo, 10 ServerInfo,
12 setActorField 11 setActorField
13} from '../../../../shared/utils' 12} from '../../../../shared/extra-utils'
14import { HTTP_SIGNATURE } from '../../../initializers' 13import { HTTP_SIGNATURE } from '../../../initializers/constants'
15import { buildDigest, buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils' 14import { buildDigest, buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils'
16import * as chai from 'chai' 15import * as chai from 'chai'
17import { activityPubContextify, buildSignedActivity } from '../../../helpers/activitypub' 16import { activityPubContextify, buildSignedActivity } from '../../../helpers/activitypub'
17import { makeFollowRequest, makePOSTAPRequest } from '../../../../shared/extra-utils/requests/activitypub'
18 18
19const expect = chai.expect 19const expect = chai.expect
20 20
@@ -179,9 +179,6 @@ describe('Test ActivityPub security', function () {
179 after(async function () { 179 after(async function () {
180 killallServers(servers) 180 killallServers(servers)
181 181
182 // Keep the logs if the test failed 182 await closeAllSequelize(servers)
183 if (this['ok']) {
184 await flushTests()
185 }
186 }) 183 })
187}) 184})
diff --git a/server/tests/api/check-params/accounts.ts b/server/tests/api/check-params/accounts.ts
index 68f9519c6..4f79685bd 100644
--- a/server/tests/api/check-params/accounts.ts
+++ b/server/tests/api/check-params/accounts.ts
@@ -2,13 +2,13 @@
2 2
3import 'mocha' 3import 'mocha'
4 4
5import { flushTests, killallServers, runServer, ServerInfo } from '../../../../shared/utils' 5import { cleanupTests, flushAndRunServer, ServerInfo } from '../../../../shared/extra-utils'
6import { 6import {
7 checkBadCountPagination, 7 checkBadCountPagination,
8 checkBadSortPagination, 8 checkBadSortPagination,
9 checkBadStartPagination 9 checkBadStartPagination
10} from '../../../../shared/utils/requests/check-api-params' 10} from '../../../../shared/extra-utils/requests/check-api-params'
11import { getAccount } from '../../../../shared/utils/users/accounts' 11import { getAccount } from '../../../../shared/extra-utils/users/accounts'
12 12
13describe('Test accounts API validators', function () { 13describe('Test accounts API validators', function () {
14 const path = '/api/v1/accounts/' 14 const path = '/api/v1/accounts/'
@@ -19,9 +19,7 @@ describe('Test accounts API validators', function () {
19 before(async function () { 19 before(async function () {
20 this.timeout(30000) 20 this.timeout(30000)
21 21
22 await flushTests() 22 server = await flushAndRunServer(1)
23
24 server = await runServer(1)
25 }) 23 })
26 24
27 describe('When listing accounts', function () { 25 describe('When listing accounts', function () {
@@ -45,11 +43,6 @@ describe('Test accounts API validators', function () {
45 }) 43 })
46 44
47 after(async function () { 45 after(async function () {
48 killallServers([ server ]) 46 await cleanupTests([ server ])
49
50 // Keep the logs if the test failed
51 if (this['ok']) {
52 await flushTests()
53 }
54 }) 47 })
55}) 48})
diff --git a/server/tests/api/check-params/blocklist.ts b/server/tests/api/check-params/blocklist.ts
index c20453c16..0661676ce 100644
--- a/server/tests/api/check-params/blocklist.ts
+++ b/server/tests/api/check-params/blocklist.ts
@@ -3,22 +3,22 @@
3import 'mocha' 3import 'mocha'
4 4
5import { 5import {
6 cleanupTests,
6 createUser, 7 createUser,
7 doubleFollow, 8 doubleFollow,
8 flushAndRunMultipleServers, 9 flushAndRunMultipleServers,
9 flushTests,
10 killallServers,
11 makeDeleteRequest, 10 makeDeleteRequest,
12 makeGetRequest, 11 makeGetRequest,
13 makePostBodyRequest, 12 makePostBodyRequest,
14 ServerInfo, 13 ServerInfo,
15 setAccessTokensToServers, userLogin 14 setAccessTokensToServers,
16} from '../../../../shared/utils' 15 userLogin
16} from '../../../../shared/extra-utils'
17import { 17import {
18 checkBadCountPagination, 18 checkBadCountPagination,
19 checkBadSortPagination, 19 checkBadSortPagination,
20 checkBadStartPagination 20 checkBadStartPagination
21} from '../../../../shared/utils/requests/check-api-params' 21} from '../../../../shared/extra-utils/requests/check-api-params'
22 22
23describe('Test blocklist API validators', function () { 23describe('Test blocklist API validators', function () {
24 let servers: ServerInfo[] 24 let servers: ServerInfo[]
@@ -28,15 +28,13 @@ describe('Test blocklist API validators', function () {
28 before(async function () { 28 before(async function () {
29 this.timeout(60000) 29 this.timeout(60000)
30 30
31 await flushTests()
32
33 servers = await flushAndRunMultipleServers(2) 31 servers = await flushAndRunMultipleServers(2)
34 await setAccessTokensToServers(servers) 32 await setAccessTokensToServers(servers)
35 33
36 server = servers[0] 34 server = servers[0]
37 35
38 const user = { username: 'user1', password: 'password' } 36 const user = { username: 'user1', password: 'password' }
39 await createUser(server.url, server.accessToken, user.username, user.password) 37 await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
40 38
41 userAccessToken = await userLogin(server, user) 39 userAccessToken = await userLogin(server, user)
42 40
@@ -192,7 +190,7 @@ describe('Test blocklist API validators', function () {
192 url: server.url, 190 url: server.url,
193 token: server.accessToken, 191 token: server.accessToken,
194 path, 192 path,
195 fields: { host: 'localhost:9001' }, 193 fields: { host: 'localhost:' + server.port },
196 statusCodeExpected: 409 194 statusCodeExpected: 409
197 }) 195 })
198 }) 196 })
@@ -202,7 +200,7 @@ describe('Test blocklist API validators', function () {
202 url: server.url, 200 url: server.url,
203 token: server.accessToken, 201 token: server.accessToken,
204 path, 202 path,
205 fields: { host: 'localhost:9002' }, 203 fields: { host: 'localhost:' + servers[1].port },
206 statusCodeExpected: 204 204 statusCodeExpected: 204
207 }) 205 })
208 }) 206 })
@@ -212,7 +210,7 @@ describe('Test blocklist API validators', function () {
212 it('Should fail with an unauthenticated user', async function () { 210 it('Should fail with an unauthenticated user', async function () {
213 await makeDeleteRequest({ 211 await makeDeleteRequest({
214 url: server.url, 212 url: server.url,
215 path: path + '/localhost:9002', 213 path: path + '/localhost:' + servers[1].port,
216 statusCodeExpected: 401 214 statusCodeExpected: 401
217 }) 215 })
218 }) 216 })
@@ -229,7 +227,7 @@ describe('Test blocklist API validators', function () {
229 it('Should succeed with the correct params', async function () { 227 it('Should succeed with the correct params', async function () {
230 await makeDeleteRequest({ 228 await makeDeleteRequest({
231 url: server.url, 229 url: server.url,
232 path: path + '/localhost:9002', 230 path: path + '/localhost:' + servers[1].port,
233 token: server.accessToken, 231 token: server.accessToken,
234 statusCodeExpected: 204 232 statusCodeExpected: 204
235 }) 233 })
@@ -402,7 +400,7 @@ describe('Test blocklist API validators', function () {
402 await makePostBodyRequest({ 400 await makePostBodyRequest({
403 url: server.url, 401 url: server.url,
404 path, 402 path,
405 fields: { host: 'localhost:9002' }, 403 fields: { host: 'localhost:' + servers[1].port },
406 statusCodeExpected: 401 404 statusCodeExpected: 401
407 }) 405 })
408 }) 406 })
@@ -412,7 +410,7 @@ describe('Test blocklist API validators', function () {
412 url: server.url, 410 url: server.url,
413 token: userAccessToken, 411 token: userAccessToken,
414 path, 412 path,
415 fields: { host: 'localhost:9002' }, 413 fields: { host: 'localhost:' + servers[1].port },
416 statusCodeExpected: 403 414 statusCodeExpected: 403
417 }) 415 })
418 }) 416 })
@@ -432,7 +430,7 @@ describe('Test blocklist API validators', function () {
432 url: server.url, 430 url: server.url,
433 token: server.accessToken, 431 token: server.accessToken,
434 path, 432 path,
435 fields: { host: 'localhost:9001' }, 433 fields: { host: 'localhost:' + server.port },
436 statusCodeExpected: 409 434 statusCodeExpected: 409
437 }) 435 })
438 }) 436 })
@@ -442,7 +440,7 @@ describe('Test blocklist API validators', function () {
442 url: server.url, 440 url: server.url,
443 token: server.accessToken, 441 token: server.accessToken,
444 path, 442 path,
445 fields: { host: 'localhost:9002' }, 443 fields: { host: 'localhost:' + servers[1].port },
446 statusCodeExpected: 204 444 statusCodeExpected: 204
447 }) 445 })
448 }) 446 })
@@ -452,7 +450,7 @@ describe('Test blocklist API validators', function () {
452 it('Should fail with an unauthenticated user', async function () { 450 it('Should fail with an unauthenticated user', async function () {
453 await makeDeleteRequest({ 451 await makeDeleteRequest({
454 url: server.url, 452 url: server.url,
455 path: path + '/localhost:9002', 453 path: path + '/localhost:' + servers[1].port,
456 statusCodeExpected: 401 454 statusCodeExpected: 401
457 }) 455 })
458 }) 456 })
@@ -460,7 +458,7 @@ describe('Test blocklist API validators', function () {
460 it('Should fail with a user without the appropriate rights', async function () { 458 it('Should fail with a user without the appropriate rights', async function () {
461 await makeDeleteRequest({ 459 await makeDeleteRequest({
462 url: server.url, 460 url: server.url,
463 path: path + '/localhost:9002', 461 path: path + '/localhost:' + servers[1].port,
464 token: userAccessToken, 462 token: userAccessToken,
465 statusCodeExpected: 403 463 statusCodeExpected: 403
466 }) 464 })
@@ -478,7 +476,7 @@ describe('Test blocklist API validators', function () {
478 it('Should succeed with the correct params', async function () { 476 it('Should succeed with the correct params', async function () {
479 await makeDeleteRequest({ 477 await makeDeleteRequest({
480 url: server.url, 478 url: server.url,
481 path: path + '/localhost:9002', 479 path: path + '/localhost:' + servers[1].port,
482 token: server.accessToken, 480 token: server.accessToken,
483 statusCodeExpected: 204 481 statusCodeExpected: 204
484 }) 482 })
@@ -488,11 +486,6 @@ describe('Test blocklist API validators', function () {
488 }) 486 })
489 487
490 after(async function () { 488 after(async function () {
491 killallServers(servers) 489 await cleanupTests(servers)
492
493 // Keep the logs if the test failed
494 if (this['ok']) {
495 await flushTests()
496 }
497 }) 490 })
498}) 491})
diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts
index 4038ecbf0..2a2ec606a 100644
--- a/server/tests/api/check-params/config.ts
+++ b/server/tests/api/check-params/config.ts
@@ -5,9 +5,9 @@ import 'mocha'
5import { CustomConfig } from '../../../../shared/models/server/custom-config.model' 5import { CustomConfig } from '../../../../shared/models/server/custom-config.model'
6 6
7import { 7import {
8 createUser, flushTests, killallServers, makeDeleteRequest, makeGetRequest, makePutBodyRequest, runServer, ServerInfo, 8 createUser, flushTests, killallServers, makeDeleteRequest, makeGetRequest, makePutBodyRequest, flushAndRunServer, ServerInfo,
9 setAccessTokensToServers, userLogin, immutableAssign 9 setAccessTokensToServers, userLogin, immutableAssign, cleanupTests
10} from '../../../../shared/utils' 10} from '../../../../shared/extra-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'
@@ -19,6 +19,7 @@ describe('Test config API validators', function () {
19 shortDescription: 'my short description', 19 shortDescription: 'my short description',
20 description: 'my super description', 20 description: 'my super description',
21 terms: 'my super terms', 21 terms: 'my super terms',
22 isNSFW: true,
22 defaultClientRoute: '/videos/recently-added', 23 defaultClientRoute: '/videos/recently-added',
23 defaultNSFWPolicy: 'blur', 24 defaultNSFWPolicy: 'blur',
24 customizations: { 25 customizations: {
@@ -65,6 +66,9 @@ describe('Test config API validators', function () {
65 '480p': true, 66 '480p': true,
66 '720p': false, 67 '720p': false,
67 '1080p': false 68 '1080p': false
69 },
70 hls: {
71 enabled: false
68 } 72 }
69 }, 73 },
70 import: { 74 import: {
@@ -76,6 +80,19 @@ describe('Test config API validators', function () {
76 enabled: false 80 enabled: false
77 } 81 }
78 } 82 }
83 },
84 autoBlacklist: {
85 videos: {
86 ofUsers: {
87 enabled: false
88 }
89 }
90 },
91 followers: {
92 instance: {
93 enabled: false,
94 manualApproval: true
95 }
79 } 96 }
80 } 97 }
81 98
@@ -84,8 +101,7 @@ describe('Test config API validators', function () {
84 before(async function () { 101 before(async function () {
85 this.timeout(30000) 102 this.timeout(30000)
86 103
87 await flushTests() 104 server = await flushAndRunServer(1)
88 server = await runServer(1)
89 105
90 await setAccessTokensToServers([ server ]) 106 await setAccessTokensToServers([ server ])
91 107
@@ -93,7 +109,7 @@ describe('Test config API validators', function () {
93 username: 'user1', 109 username: 'user1',
94 password: 'password' 110 password: 'password'
95 } 111 }
96 await createUser(server.url, server.accessToken, user.username, user.password) 112 await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
97 userAccessToken = await userLogin(server, user) 113 userAccessToken = await userLogin(server, user)
98 }) 114 })
99 115
@@ -164,6 +180,25 @@ describe('Test config API validators', function () {
164 }) 180 })
165 }) 181 })
166 182
183 it('Should fail if email disabled and signup requires email verification', async function () {
184 // opposite scenario - success when enable enabled - covered via tests/api/users/user-verification.ts
185 const newUpdateParams = immutableAssign(updateParams, {
186 signup: {
187 enabled: true,
188 limit: 5,
189 requiresEmailVerification: true
190 }
191 })
192
193 await makePutBodyRequest({
194 url: server.url,
195 path,
196 fields: newUpdateParams,
197 token: server.accessToken,
198 statusCodeExpected: 400
199 })
200 })
201
167 it('Should success with the correct parameters', async function () { 202 it('Should success with the correct parameters', async function () {
168 await makePutBodyRequest({ 203 await makePutBodyRequest({
169 url: server.url, 204 url: server.url,
@@ -195,11 +230,6 @@ describe('Test config API validators', function () {
195 }) 230 })
196 231
197 after(async function () { 232 after(async function () {
198 killallServers([ server ]) 233 await cleanupTests([ server ])
199
200 // Keep the logs if the test failed
201 if (this['ok']) {
202 await flushTests()
203 }
204 }) 234 })
205}) 235})
diff --git a/server/tests/api/check-params/contact-form.ts b/server/tests/api/check-params/contact-form.ts
index c7e014b1f..dbdd3a8a6 100644
--- a/server/tests/api/check-params/contact-form.ts
+++ b/server/tests/api/check-params/contact-form.ts
@@ -7,18 +7,18 @@ import {
7 immutableAssign, 7 immutableAssign,
8 killallServers, 8 killallServers,
9 reRunServer, 9 reRunServer,
10 runServer, 10 flushAndRunServer,
11 ServerInfo, 11 ServerInfo,
12 setAccessTokensToServers 12 setAccessTokensToServers, cleanupTests
13} from '../../../../shared/utils' 13} from '../../../../shared/extra-utils'
14import { 14import {
15 checkBadCountPagination, 15 checkBadCountPagination,
16 checkBadSortPagination, 16 checkBadSortPagination,
17 checkBadStartPagination 17 checkBadStartPagination
18} from '../../../../shared/utils/requests/check-api-params' 18} from '../../../../shared/extra-utils/requests/check-api-params'
19import { getAccount } from '../../../../shared/utils/users/accounts' 19import { getAccount } from '../../../../shared/extra-utils/users/accounts'
20import { sendContactForm } from '../../../../shared/utils/server/contact-form' 20import { sendContactForm } from '../../../../shared/extra-utils/server/contact-form'
21import { MockSmtpServer } from '../../../../shared/utils/miscs/email' 21import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email'
22 22
23describe('Test contact form API validators', function () { 23describe('Test contact form API validators', function () {
24 let server: ServerInfo 24 let server: ServerInfo
@@ -28,17 +28,17 @@ describe('Test contact form API validators', function () {
28 fromEmail: 'toto@example.com', 28 fromEmail: 'toto@example.com',
29 body: 'Hello, how are you?' 29 body: 'Hello, how are you?'
30 } 30 }
31 let emailPort: number
31 32
32 // --------------------------------------------------------------- 33 // ---------------------------------------------------------------
33 34
34 before(async function () { 35 before(async function () {
35 this.timeout(60000) 36 this.timeout(60000)
36 37
37 await flushTests() 38 emailPort = await MockSmtpServer.Instance.collectEmails(emails)
38 await MockSmtpServer.Instance.collectEmails(emails)
39 39
40 // Email is disabled 40 // Email is disabled
41 server = await runServer(1) 41 server = await flushAndRunServer(1)
42 }) 42 })
43 43
44 it('Should not accept a contact form if emails are disabled', async function () { 44 it('Should not accept a contact form if emails are disabled', async function () {
@@ -51,7 +51,7 @@ describe('Test contact form API validators', function () {
51 killallServers([ server ]) 51 killallServers([ server ])
52 52
53 // Contact form is disabled 53 // Contact form is disabled
54 await reRunServer(server, { smtp: { hostname: 'localhost' }, contact_form: { enabled: false } }) 54 await reRunServer(server, { smtp: { hostname: 'localhost', port: emailPort }, contact_form: { enabled: false } })
55 await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: 409 })) 55 await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: 409 }))
56 }) 56 })
57 57
@@ -61,7 +61,7 @@ describe('Test contact form API validators', function () {
61 killallServers([ server ]) 61 killallServers([ server ])
62 62
63 // Email & contact form enabled 63 // Email & contact form enabled
64 await reRunServer(server, { smtp: { hostname: 'localhost' } }) 64 await reRunServer(server, { smtp: { hostname: 'localhost', port: emailPort } })
65 65
66 await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: 400, fromEmail: 'badEmail' })) 66 await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: 400, fromEmail: 'badEmail' }))
67 await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: 400, fromEmail: 'badEmail@' })) 67 await sendContactForm(immutableAssign(defaultBody, { url: server.url, expectedStatus: 400, fromEmail: 'badEmail@' }))
@@ -86,11 +86,7 @@ describe('Test contact form API validators', function () {
86 86
87 after(async function () { 87 after(async function () {
88 MockSmtpServer.Instance.kill() 88 MockSmtpServer.Instance.kill()
89 killallServers([ server ])
90 89
91 // Keep the logs if the test failed 90 await cleanupTests([ server ])
92 if (this['ok']) {
93 await flushTests()
94 }
95 }) 91 })
96}) 92})
diff --git a/server/tests/api/check-params/debug.ts b/server/tests/api/check-params/debug.ts
new file mode 100644
index 000000000..8dad26723
--- /dev/null
+++ b/server/tests/api/check-params/debug.ts
@@ -0,0 +1,71 @@
1/* tslint:disable:no-unused-expression */
2
3import 'mocha'
4
5import {
6 createUser,
7 flushTests,
8 killallServers,
9 flushAndRunServer,
10 ServerInfo,
11 setAccessTokensToServers,
12 userLogin, cleanupTests
13} from '../../../../shared/extra-utils'
14import { makeGetRequest } from '../../../../shared/extra-utils/requests/requests'
15
16describe('Test debug API validators', function () {
17 const path = '/api/v1/server/debug'
18 let server: ServerInfo
19 let userAccessToken = ''
20
21 // ---------------------------------------------------------------
22
23 before(async function () {
24 this.timeout(120000)
25
26 server = await flushAndRunServer(1)
27
28 await setAccessTokensToServers([ server ])
29
30 const user = {
31 username: 'user1',
32 password: 'my super password'
33 }
34 await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
35 userAccessToken = await userLogin(server, user)
36 })
37
38 describe('When getting debug endpoint', function () {
39
40 it('Should fail with a non authenticated user', async function () {
41 await makeGetRequest({
42 url: server.url,
43 path,
44 statusCodeExpected: 401
45 })
46 })
47
48 it('Should fail with a non admin user', async function () {
49 await makeGetRequest({
50 url: server.url,
51 path,
52 token: userAccessToken,
53 statusCodeExpected: 403
54 })
55 })
56
57 it('Should succeed with the correct params', async function () {
58 await makeGetRequest({
59 url: server.url,
60 path,
61 token: server.accessToken,
62 query: { startDate: new Date().toISOString() },
63 statusCodeExpected: 200
64 })
65 })
66 })
67
68 after(async function () {
69 await cleanupTests([ server ])
70 })
71})
diff --git a/server/tests/api/check-params/follows.ts b/server/tests/api/check-params/follows.ts
index 2ad1575a3..2eb54cb0a 100644
--- a/server/tests/api/check-params/follows.ts
+++ b/server/tests/api/check-params/follows.ts
@@ -3,14 +3,20 @@
3import 'mocha' 3import 'mocha'
4 4
5import { 5import {
6 createUser, flushTests, killallServers, makeDeleteRequest, makePostBodyRequest, runServer, ServerInfo, setAccessTokensToServers, 6 cleanupTests,
7 createUser,
8 flushAndRunServer,
9 makeDeleteRequest,
10 makePostBodyRequest,
11 ServerInfo,
12 setAccessTokensToServers,
7 userLogin 13 userLogin
8} from '../../../../shared/utils' 14} from '../../../../shared/extra-utils'
9import { 15import {
10 checkBadCountPagination, 16 checkBadCountPagination,
11 checkBadSortPagination, 17 checkBadSortPagination,
12 checkBadStartPagination 18 checkBadStartPagination
13} from '../../../../shared/utils/requests/check-api-params' 19} from '../../../../shared/extra-utils/requests/check-api-params'
14 20
15describe('Test server follows API validators', function () { 21describe('Test server follows API validators', function () {
16 let server: ServerInfo 22 let server: ServerInfo
@@ -20,8 +26,7 @@ describe('Test server follows API validators', function () {
20 before(async function () { 26 before(async function () {
21 this.timeout(30000) 27 this.timeout(30000)
22 28
23 await flushTests() 29 server = await flushAndRunServer(1)
24 server = await runServer(1)
25 30
26 await setAccessTokensToServers([ server ]) 31 await setAccessTokensToServers([ server ])
27 }) 32 })
@@ -35,7 +40,7 @@ describe('Test server follows API validators', function () {
35 password: 'password' 40 password: 'password'
36 } 41 }
37 42
38 await createUser(server.url, server.accessToken, user.username, user.password) 43 await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
39 userAccessToken = await userLogin(server, user) 44 userAccessToken = await userLogin(server, user)
40 }) 45 })
41 46
@@ -144,6 +149,126 @@ describe('Test server follows API validators', function () {
144 }) 149 })
145 }) 150 })
146 151
152 describe('When removing a follower', function () {
153 const path = '/api/v1/server/followers'
154
155 it('Should fail with an invalid token', async function () {
156 await makeDeleteRequest({
157 url: server.url,
158 path: path + '/toto@localhost:9002',
159 token: 'fake_token',
160 statusCodeExpected: 401
161 })
162 })
163
164 it('Should fail if the user is not an administrator', async function () {
165 await makeDeleteRequest({
166 url: server.url,
167 path: path + '/toto@localhost:9002',
168 token: userAccessToken,
169 statusCodeExpected: 403
170 })
171 })
172
173 it('Should fail with an invalid follower', async function () {
174 await makeDeleteRequest({
175 url: server.url,
176 path: path + '/toto',
177 token: server.accessToken,
178 statusCodeExpected: 400
179 })
180 })
181
182 it('Should fail with an unknown follower', async function () {
183 await makeDeleteRequest({
184 url: server.url,
185 path: path + '/toto@localhost:9003',
186 token: server.accessToken,
187 statusCodeExpected: 404
188 })
189 })
190 })
191
192 describe('When accepting a follower', function () {
193 const path = '/api/v1/server/followers'
194
195 it('Should fail with an invalid token', async function () {
196 await makePostBodyRequest({
197 url: server.url,
198 path: path + '/toto@localhost:9002/accept',
199 token: 'fake_token',
200 statusCodeExpected: 401
201 })
202 })
203
204 it('Should fail if the user is not an administrator', async function () {
205 await makePostBodyRequest({
206 url: server.url,
207 path: path + '/toto@localhost:9002/accept',
208 token: userAccessToken,
209 statusCodeExpected: 403
210 })
211 })
212
213 it('Should fail with an invalid follower', async function () {
214 await makePostBodyRequest({
215 url: server.url,
216 path: path + '/toto/accept',
217 token: server.accessToken,
218 statusCodeExpected: 400
219 })
220 })
221
222 it('Should fail with an unknown follower', async function () {
223 await makePostBodyRequest({
224 url: server.url,
225 path: path + '/toto@localhost:9003/accept',
226 token: server.accessToken,
227 statusCodeExpected: 404
228 })
229 })
230 })
231
232 describe('When rejecting a follower', function () {
233 const path = '/api/v1/server/followers'
234
235 it('Should fail with an invalid token', async function () {
236 await makePostBodyRequest({
237 url: server.url,
238 path: path + '/toto@localhost:9002/reject',
239 token: 'fake_token',
240 statusCodeExpected: 401
241 })
242 })
243
244 it('Should fail if the user is not an administrator', async function () {
245 await makePostBodyRequest({
246 url: server.url,
247 path: path + '/toto@localhost:9002/reject',
248 token: userAccessToken,
249 statusCodeExpected: 403
250 })
251 })
252
253 it('Should fail with an invalid follower', async function () {
254 await makePostBodyRequest({
255 url: server.url,
256 path: path + '/toto/reject',
257 token: server.accessToken,
258 statusCodeExpected: 400
259 })
260 })
261
262 it('Should fail with an unknown follower', async function () {
263 await makePostBodyRequest({
264 url: server.url,
265 path: path + '/toto@localhost:9003/reject',
266 token: server.accessToken,
267 statusCodeExpected: 404
268 })
269 })
270 })
271
147 describe('When removing following', function () { 272 describe('When removing following', function () {
148 const path = '/api/v1/server/following' 273 const path = '/api/v1/server/following'
149 274
@@ -177,11 +302,6 @@ describe('Test server follows API validators', function () {
177 }) 302 })
178 303
179 after(async function () { 304 after(async function () {
180 killallServers([ server ]) 305 await cleanupTests([ server ])
181
182 // Keep the logs if the test failed
183 if (this['ok']) {
184 await flushTests()
185 }
186 }) 306 })
187}) 307})
diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts
index 77c17036a..844fa31c5 100644
--- a/server/tests/api/check-params/index.ts
+++ b/server/tests/api/check-params/index.ts
@@ -2,8 +2,10 @@ import './accounts'
2import './blocklist' 2import './blocklist'
3import './config' 3import './config'
4import './contact-form' 4import './contact-form'
5import './debug'
5import './follows' 6import './follows'
6import './jobs' 7import './jobs'
8import './logs'
7import './redundancy' 9import './redundancy'
8import './search' 10import './search'
9import './services' 11import './services'
@@ -16,6 +18,7 @@ import './video-captions'
16import './video-channels' 18import './video-channels'
17import './video-comments' 19import './video-comments'
18import './video-imports' 20import './video-imports'
21import './video-playlists'
19import './videos' 22import './videos'
20import './videos-filter' 23import './videos-filter'
21import './videos-history' 24import './videos-history'
diff --git a/server/tests/api/check-params/jobs.ts b/server/tests/api/check-params/jobs.ts
index 89760ff98..c70139514 100644
--- a/server/tests/api/check-params/jobs.ts
+++ b/server/tests/api/check-params/jobs.ts
@@ -6,17 +6,18 @@ import {
6 createUser, 6 createUser,
7 flushTests, 7 flushTests,
8 killallServers, 8 killallServers,
9 runServer, 9 flushAndRunServer,
10 ServerInfo, 10 ServerInfo,
11 setAccessTokensToServers, 11 setAccessTokensToServers,
12 userLogin 12 userLogin,
13} from '../../../../shared/utils' 13 cleanupTests
14} from '../../../../shared/extra-utils'
14import { 15import {
15 checkBadCountPagination, 16 checkBadCountPagination,
16 checkBadSortPagination, 17 checkBadSortPagination,
17 checkBadStartPagination 18 checkBadStartPagination
18} from '../../../../shared/utils/requests/check-api-params' 19} from '../../../../shared/extra-utils/requests/check-api-params'
19import { makeGetRequest } from '../../../../shared/utils/requests/requests' 20import { makeGetRequest } from '../../../../shared/extra-utils/requests/requests'
20 21
21describe('Test jobs API validators', function () { 22describe('Test jobs API validators', function () {
22 const path = '/api/v1/jobs/failed' 23 const path = '/api/v1/jobs/failed'
@@ -28,9 +29,7 @@ describe('Test jobs API validators', function () {
28 before(async function () { 29 before(async function () {
29 this.timeout(120000) 30 this.timeout(120000)
30 31
31 await flushTests() 32 server = await flushAndRunServer(1)
32
33 server = await runServer(1)
34 33
35 await setAccessTokensToServers([ server ]) 34 await setAccessTokensToServers([ server ])
36 35
@@ -38,7 +37,7 @@ describe('Test jobs API validators', function () {
38 username: 'user1', 37 username: 'user1',
39 password: 'my super password' 38 password: 'my super password'
40 } 39 }
41 await createUser(server.url, server.accessToken, user.username, user.password) 40 await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
42 userAccessToken = await userLogin(server, user) 41 userAccessToken = await userLogin(server, user)
43 }) 42 })
44 43
@@ -83,11 +82,6 @@ describe('Test jobs API validators', function () {
83 }) 82 })
84 83
85 after(async function () { 84 after(async function () {
86 killallServers([ server ]) 85 await cleanupTests([ server ])
87
88 // Keep the logs if the test failed
89 if (this['ok']) {
90 await flushTests()
91 }
92 }) 86 })
93}) 87})
diff --git a/server/tests/api/check-params/logs.ts b/server/tests/api/check-params/logs.ts
new file mode 100644
index 000000000..f9d96bcc0
--- /dev/null
+++ b/server/tests/api/check-params/logs.ts
@@ -0,0 +1,111 @@
1/* tslint:disable:no-unused-expression */
2
3import 'mocha'
4
5import {
6 createUser,
7 flushTests,
8 killallServers,
9 flushAndRunServer,
10 ServerInfo,
11 setAccessTokensToServers,
12 userLogin,
13 cleanupTests
14} from '../../../../shared/extra-utils'
15import { makeGetRequest } from '../../../../shared/extra-utils/requests/requests'
16
17describe('Test logs API validators', function () {
18 const path = '/api/v1/server/logs'
19 let server: ServerInfo
20 let userAccessToken = ''
21
22 // ---------------------------------------------------------------
23
24 before(async function () {
25 this.timeout(120000)
26
27 server = await flushAndRunServer(1)
28
29 await setAccessTokensToServers([ server ])
30
31 const user = {
32 username: 'user1',
33 password: 'my super password'
34 }
35 await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
36 userAccessToken = await userLogin(server, user)
37 })
38
39 describe('When getting logs', function () {
40
41 it('Should fail with a non authenticated user', async function () {
42 await makeGetRequest({
43 url: server.url,
44 path,
45 statusCodeExpected: 401
46 })
47 })
48
49 it('Should fail with a non admin user', async function () {
50 await makeGetRequest({
51 url: server.url,
52 path,
53 token: userAccessToken,
54 statusCodeExpected: 403
55 })
56 })
57
58 it('Should fail with a missing startDate query', async function () {
59 await makeGetRequest({
60 url: server.url,
61 path,
62 token: server.accessToken,
63 statusCodeExpected: 400
64 })
65 })
66
67 it('Should fail with a bad startDate query', async function () {
68 await makeGetRequest({
69 url: server.url,
70 path,
71 token: server.accessToken,
72 query: { startDate: 'toto' },
73 statusCodeExpected: 400
74 })
75 })
76
77 it('Should fail with a bad endDate query', async function () {
78 await makeGetRequest({
79 url: server.url,
80 path,
81 token: server.accessToken,
82 query: { startDate: new Date().toISOString(), endDate: 'toto' },
83 statusCodeExpected: 400
84 })
85 })
86
87 it('Should fail with a bad level parameter', async function () {
88 await makeGetRequest({
89 url: server.url,
90 path,
91 token: server.accessToken,
92 query: { startDate: new Date().toISOString(), level: 'toto' },
93 statusCodeExpected: 400
94 })
95 })
96
97 it('Should succeed with the correct params', async function () {
98 await makeGetRequest({
99 url: server.url,
100 path,
101 token: server.accessToken,
102 query: { startDate: new Date().toISOString() },
103 statusCodeExpected: 200
104 })
105 })
106 })
107
108 after(async function () {
109 await cleanupTests([ server ])
110 })
111})
diff --git a/server/tests/api/check-params/redundancy.ts b/server/tests/api/check-params/redundancy.ts
index ff4726ceb..6471da840 100644
--- a/server/tests/api/check-params/redundancy.ts
+++ b/server/tests/api/check-params/redundancy.ts
@@ -3,6 +3,7 @@
3import 'mocha' 3import 'mocha'
4 4
5import { 5import {
6 cleanupTests,
6 createUser, 7 createUser,
7 doubleFollow, 8 doubleFollow,
8 flushAndRunMultipleServers, 9 flushAndRunMultipleServers,
@@ -12,7 +13,7 @@ import {
12 ServerInfo, 13 ServerInfo,
13 setAccessTokensToServers, 14 setAccessTokensToServers,
14 userLogin 15 userLogin
15} from '../../../../shared/utils' 16} from '../../../../shared/extra-utils'
16 17
17describe('Test server redundancy API validators', function () { 18describe('Test server redundancy API validators', function () {
18 let servers: ServerInfo[] 19 let servers: ServerInfo[]
@@ -23,7 +24,6 @@ describe('Test server redundancy API validators', function () {
23 before(async function () { 24 before(async function () {
24 this.timeout(30000) 25 this.timeout(30000)
25 26
26 await flushTests()
27 servers = await flushAndRunMultipleServers(2) 27 servers = await flushAndRunMultipleServers(2)
28 28
29 await setAccessTokensToServers(servers) 29 await setAccessTokensToServers(servers)
@@ -34,7 +34,7 @@ describe('Test server redundancy API validators', function () {
34 password: 'password' 34 password: 'password'
35 } 35 }
36 36
37 await createUser(servers[0].url, servers[0].accessToken, user.username, user.password) 37 await createUser({ url: servers[ 0 ].url, accessToken: servers[ 0 ].accessToken, username: user.username, password: user.password })
38 userAccessToken = await userLogin(servers[0], user) 38 userAccessToken = await userLogin(servers[0], user)
39 }) 39 })
40 40
@@ -44,7 +44,7 @@ describe('Test server redundancy API validators', function () {
44 it('Should fail with an invalid token', async function () { 44 it('Should fail with an invalid token', async function () {
45 await makePutBodyRequest({ 45 await makePutBodyRequest({
46 url: servers[0].url, 46 url: servers[0].url,
47 path: path + '/localhost:9002', 47 path: path + '/localhost:' + servers[1].port,
48 fields: { redundancyAllowed: true }, 48 fields: { redundancyAllowed: true },
49 token: 'fake_token', 49 token: 'fake_token',
50 statusCodeExpected: 401 50 statusCodeExpected: 401
@@ -54,7 +54,7 @@ describe('Test server redundancy API validators', function () {
54 it('Should fail if the user is not an administrator', async function () { 54 it('Should fail if the user is not an administrator', async function () {
55 await makePutBodyRequest({ 55 await makePutBodyRequest({
56 url: servers[0].url, 56 url: servers[0].url,
57 path: path + '/localhost:9002', 57 path: path + '/localhost:' + servers[1].port,
58 fields: { redundancyAllowed: true }, 58 fields: { redundancyAllowed: true },
59 token: userAccessToken, 59 token: userAccessToken,
60 statusCodeExpected: 403 60 statusCodeExpected: 403
@@ -74,7 +74,7 @@ describe('Test server redundancy API validators', function () {
74 it('Should fail without de redundancyAllowed param', async function () { 74 it('Should fail without de redundancyAllowed param', async function () {
75 await makePutBodyRequest({ 75 await makePutBodyRequest({
76 url: servers[0].url, 76 url: servers[0].url,
77 path: path + '/localhost:9002', 77 path: path + '/localhost:' + servers[1].port,
78 fields: { blabla: true }, 78 fields: { blabla: true },
79 token: servers[0].accessToken, 79 token: servers[0].accessToken,
80 statusCodeExpected: 400 80 statusCodeExpected: 400
@@ -84,7 +84,7 @@ describe('Test server redundancy API validators', function () {
84 it('Should succeed with the correct parameters', async function () { 84 it('Should succeed with the correct parameters', async function () {
85 await makePutBodyRequest({ 85 await makePutBodyRequest({
86 url: servers[0].url, 86 url: servers[0].url,
87 path: path + '/localhost:9002', 87 path: path + '/localhost:' + servers[1].port,
88 fields: { redundancyAllowed: true }, 88 fields: { redundancyAllowed: true },
89 token: servers[0].accessToken, 89 token: servers[0].accessToken,
90 statusCodeExpected: 204 90 statusCodeExpected: 204
@@ -93,11 +93,6 @@ describe('Test server redundancy API validators', function () {
93 }) 93 })
94 94
95 after(async function () { 95 after(async function () {
96 killallServers(servers) 96 await cleanupTests(servers)
97
98 // Keep the logs if the test failed
99 if (this['ok']) {
100 await flushTests()
101 }
102 }) 97 })
103}) 98})
diff --git a/server/tests/api/check-params/search.ts b/server/tests/api/check-params/search.ts
index aa81965f3..8ad9d98bf 100644
--- a/server/tests/api/check-params/search.ts
+++ b/server/tests/api/check-params/search.ts
@@ -2,12 +2,12 @@
2 2
3import 'mocha' 3import 'mocha'
4 4
5import { flushTests, immutableAssign, killallServers, makeGetRequest, runServer, ServerInfo } from '../../../../shared/utils' 5import { cleanupTests, flushAndRunServer, immutableAssign, makeGetRequest, ServerInfo } from '../../../../shared/extra-utils'
6import { 6import {
7 checkBadCountPagination, 7 checkBadCountPagination,
8 checkBadSortPagination, 8 checkBadSortPagination,
9 checkBadStartPagination 9 checkBadStartPagination
10} from '../../../../shared/utils/requests/check-api-params' 10} from '../../../../shared/extra-utils/requests/check-api-params'
11 11
12describe('Test videos API validator', function () { 12describe('Test videos API validator', function () {
13 let server: ServerInfo 13 let server: ServerInfo
@@ -17,9 +17,7 @@ describe('Test videos API validator', function () {
17 before(async function () { 17 before(async function () {
18 this.timeout(30000) 18 this.timeout(30000)
19 19
20 await flushTests() 20 server = await flushAndRunServer(1)
21
22 server = await runServer(1)
23 }) 21 })
24 22
25 describe('When searching videos', function () { 23 describe('When searching videos', function () {
@@ -113,6 +111,12 @@ describe('Test videos API validator', function () {
113 111
114 const customQuery2 = immutableAssign(query, { endDate: 'hello' }) 112 const customQuery2 = immutableAssign(query, { endDate: 'hello' })
115 await makeGetRequest({ url: server.url, path, query: customQuery2, statusCodeExpected: 400 }) 113 await makeGetRequest({ url: server.url, path, query: customQuery2, statusCodeExpected: 400 })
114
115 const customQuery3 = immutableAssign(query, { originallyPublishedStartDate: 'hello' })
116 await makeGetRequest({ url: server.url, path, query: customQuery3, statusCodeExpected: 400 })
117
118 const customQuery4 = immutableAssign(query, { originallyPublishedEndDate: 'hello' })
119 await makeGetRequest({ url: server.url, path, query: customQuery4, statusCodeExpected: 400 })
116 }) 120 })
117 }) 121 })
118 122
@@ -141,11 +145,6 @@ describe('Test videos API validator', function () {
141 }) 145 })
142 146
143 after(async function () { 147 after(async function () {
144 killallServers([ server ]) 148 await cleanupTests([ server ])
145
146 // Keep the logs if the test failed
147 if (this['ok']) {
148 await flushTests()
149 }
150 }) 149 })
151}) 150})
diff --git a/server/tests/api/check-params/services.ts b/server/tests/api/check-params/services.ts
index 28591af9d..d15753aed 100644
--- a/server/tests/api/check-params/services.ts
+++ b/server/tests/api/check-params/services.ts
@@ -3,14 +3,13 @@
3import 'mocha' 3import 'mocha'
4 4
5import { 5import {
6 flushTests, 6 cleanupTests,
7 killallServers, 7 flushAndRunServer,
8 makeGetRequest, 8 makeGetRequest,
9 runServer,
10 ServerInfo, 9 ServerInfo,
11 setAccessTokensToServers, 10 setAccessTokensToServers,
12 uploadVideo 11 uploadVideo
13} from '../../../../shared/utils' 12} from '../../../../shared/extra-utils'
14 13
15describe('Test services API validators', function () { 14describe('Test services API validators', function () {
16 let server: ServerInfo 15 let server: ServerInfo
@@ -20,9 +19,7 @@ describe('Test services API validators', function () {
20 before(async function () { 19 before(async function () {
21 this.timeout(60000) 20 this.timeout(60000)
22 21
23 await flushTests() 22 server = await flushAndRunServer(1)
24
25 server = await runServer(1)
26 await setAccessTokensToServers([ server ]) 23 await setAccessTokensToServers([ server ])
27 24
28 const res = await uploadVideo(server.url, server.accessToken, { name: 'my super name' }) 25 const res = await uploadVideo(server.url, server.accessToken, { name: 'my super name' })
@@ -42,47 +39,47 @@ describe('Test services API validators', function () {
42 }) 39 })
43 40
44 it('Should fail with an invalid video id', async function () { 41 it('Should fail with an invalid video id', async function () {
45 const embedUrl = 'http://localhost:9001/videos/watch/blabla' 42 const embedUrl = `http://localhost:${server.port}/videos/watch/blabla`
46 await checkParamEmbed(server, embedUrl) 43 await checkParamEmbed(server, embedUrl)
47 }) 44 })
48 45
49 it('Should fail with an unknown video', async function () { 46 it('Should fail with an unknown video', async function () {
50 const embedUrl = 'http://localhost:9001/videos/watch/88fc0165-d1f0-4a35-a51a-3b47f668689c' 47 const embedUrl = `http://localhost:${server.port}/videos/watch/88fc0165-d1f0-4a35-a51a-3b47f668689c`
51 await checkParamEmbed(server, embedUrl, 404) 48 await checkParamEmbed(server, embedUrl, 404)
52 }) 49 })
53 50
54 it('Should fail with an invalid path', async function () { 51 it('Should fail with an invalid path', async function () {
55 const embedUrl = 'http://localhost:9001/videos/watchs/' + server.video.uuid 52 const embedUrl = `http://localhost:${server.port}/videos/watchs/${server.video.uuid}`
56 53
57 await checkParamEmbed(server, embedUrl) 54 await checkParamEmbed(server, embedUrl)
58 }) 55 })
59 56
60 it('Should fail with an invalid max height', async function () { 57 it('Should fail with an invalid max height', async function () {
61 const embedUrl = 'http://localhost:9001/videos/watch/' + server.video.uuid 58 const embedUrl = `http://localhost:${server.port}/videos/watch/${server.video.uuid}`
62 59
63 await checkParamEmbed(server, embedUrl, 400, { maxheight: 'hello' }) 60 await checkParamEmbed(server, embedUrl, 400, { maxheight: 'hello' })
64 }) 61 })
65 62
66 it('Should fail with an invalid max width', async function () { 63 it('Should fail with an invalid max width', async function () {
67 const embedUrl = 'http://localhost:9001/videos/watch/' + server.video.uuid 64 const embedUrl = `http://localhost:${server.port}/videos/watch/${server.video.uuid}`
68 65
69 await checkParamEmbed(server, embedUrl, 400, { maxwidth: 'hello' }) 66 await checkParamEmbed(server, embedUrl, 400, { maxwidth: 'hello' })
70 }) 67 })
71 68
72 it('Should fail with an invalid format', async function () { 69 it('Should fail with an invalid format', async function () {
73 const embedUrl = 'http://localhost:9001/videos/watch/' + server.video.uuid 70 const embedUrl = `http://localhost:${server.port}/videos/watch/${server.video.uuid}`
74 71
75 await checkParamEmbed(server, embedUrl, 400, { format: 'blabla' }) 72 await checkParamEmbed(server, embedUrl, 400, { format: 'blabla' })
76 }) 73 })
77 74
78 it('Should fail with a non supported format', async function () { 75 it('Should fail with a non supported format', async function () {
79 const embedUrl = 'http://localhost:9001/videos/watch/' + server.video.uuid 76 const embedUrl = `http://localhost:${server.port}/videos/watch/${server.video.uuid}`
80 77
81 await checkParamEmbed(server, embedUrl, 501, { format: 'xml' }) 78 await checkParamEmbed(server, embedUrl, 501, { format: 'xml' })
82 }) 79 })
83 80
84 it('Should succeed with the correct params', async function () { 81 it('Should succeed with the correct params', async function () {
85 const embedUrl = 'http://localhost:9001/videos/watch/' + server.video.uuid 82 const embedUrl = `http://localhost:${server.port}/videos/watch/${server.video.uuid}`
86 const query = { 83 const query = {
87 format: 'json', 84 format: 'json',
88 maxheight: 400, 85 maxheight: 400,
@@ -94,12 +91,7 @@ describe('Test services API validators', function () {
94 }) 91 })
95 92
96 after(async function () { 93 after(async function () {
97 killallServers([ server ]) 94 await cleanupTests([ server ])
98
99 // Keep the logs if the test failed
100 if (this['ok']) {
101 await flushTests()
102 }
103 }) 95 })
104}) 96})
105 97
diff --git a/server/tests/api/check-params/user-notifications.ts b/server/tests/api/check-params/user-notifications.ts
index 714f481e9..14ee20d45 100644
--- a/server/tests/api/check-params/user-notifications.ts
+++ b/server/tests/api/check-params/user-notifications.ts
@@ -4,22 +4,21 @@ import 'mocha'
4import * as io from 'socket.io-client' 4import * as io from 'socket.io-client'
5 5
6import { 6import {
7 flushTests, 7 cleanupTests,
8 flushAndRunServer,
8 immutableAssign, 9 immutableAssign,
9 killallServers,
10 makeGetRequest, 10 makeGetRequest,
11 makePostBodyRequest, 11 makePostBodyRequest,
12 makePutBodyRequest, 12 makePutBodyRequest,
13 runServer,
14 ServerInfo, 13 ServerInfo,
15 setAccessTokensToServers, 14 setAccessTokensToServers,
16 wait 15 wait
17} from '../../../../shared/utils' 16} from '../../../../shared/extra-utils'
18import { 17import {
19 checkBadCountPagination, 18 checkBadCountPagination,
20 checkBadSortPagination, 19 checkBadSortPagination,
21 checkBadStartPagination 20 checkBadStartPagination
22} from '../../../../shared/utils/requests/check-api-params' 21} from '../../../../shared/extra-utils/requests/check-api-params'
23import { UserNotificationSetting, UserNotificationSettingValue } from '../../../../shared/models/users' 22import { UserNotificationSetting, UserNotificationSettingValue } from '../../../../shared/models/users'
24 23
25describe('Test user notifications API validators', function () { 24describe('Test user notifications API validators', function () {
@@ -30,9 +29,7 @@ describe('Test user notifications API validators', function () {
30 before(async function () { 29 before(async function () {
31 this.timeout(30000) 30 this.timeout(30000)
32 31
33 await flushTests() 32 server = await flushAndRunServer(1)
34
35 server = await runServer(1)
36 33
37 await setAccessTokensToServers([ server ]) 34 await setAccessTokensToServers([ server ])
38 }) 35 })
@@ -168,12 +165,14 @@ describe('Test user notifications API validators', function () {
168 newVideoFromSubscription: UserNotificationSettingValue.WEB, 165 newVideoFromSubscription: UserNotificationSettingValue.WEB,
169 newCommentOnMyVideo: UserNotificationSettingValue.WEB, 166 newCommentOnMyVideo: UserNotificationSettingValue.WEB,
170 videoAbuseAsModerator: UserNotificationSettingValue.WEB, 167 videoAbuseAsModerator: UserNotificationSettingValue.WEB,
168 videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB,
171 blacklistOnMyVideo: UserNotificationSettingValue.WEB, 169 blacklistOnMyVideo: UserNotificationSettingValue.WEB,
172 myVideoImportFinished: UserNotificationSettingValue.WEB, 170 myVideoImportFinished: UserNotificationSettingValue.WEB,
173 myVideoPublished: UserNotificationSettingValue.WEB, 171 myVideoPublished: UserNotificationSettingValue.WEB,
174 commentMention: UserNotificationSettingValue.WEB, 172 commentMention: UserNotificationSettingValue.WEB,
175 newFollow: UserNotificationSettingValue.WEB, 173 newFollow: UserNotificationSettingValue.WEB,
176 newUserRegistration: UserNotificationSettingValue.WEB 174 newUserRegistration: UserNotificationSettingValue.WEB,
175 newInstanceFollower: UserNotificationSettingValue.WEB
177 } 176 }
178 177
179 it('Should fail with missing fields', async function () { 178 it('Should fail with missing fields', async function () {
@@ -234,7 +233,7 @@ describe('Test user notifications API validators', function () {
234 233
235 describe('When connecting to my notification socket', function () { 234 describe('When connecting to my notification socket', function () {
236 it('Should fail with no token', function (next) { 235 it('Should fail with no token', function (next) {
237 const socket = io('http://localhost:9001/user-notifications', { reconnection: false }) 236 const socket = io(`http://localhost:${server.port}/user-notifications`, { reconnection: false })
238 237
239 socket.on('error', () => { 238 socket.on('error', () => {
240 socket.removeListener('error', this) 239 socket.removeListener('error', this)
@@ -249,7 +248,7 @@ describe('Test user notifications API validators', function () {
249 }) 248 })
250 249
251 it('Should fail with an invalid token', function (next) { 250 it('Should fail with an invalid token', function (next) {
252 const socket = io('http://localhost:9001/user-notifications', { 251 const socket = io(`http://localhost:${server.port}/user-notifications`, {
253 query: { accessToken: 'bad_access_token' }, 252 query: { accessToken: 'bad_access_token' },
254 reconnection: false 253 reconnection: false
255 }) 254 })
@@ -267,7 +266,7 @@ describe('Test user notifications API validators', function () {
267 }) 266 })
268 267
269 it('Should success with the correct token', function (next) { 268 it('Should success with the correct token', function (next) {
270 const socket = io('http://localhost:9001/user-notifications', { 269 const socket = io(`http://localhost:${server.port}/user-notifications`, {
271 query: { accessToken: server.accessToken }, 270 query: { accessToken: server.accessToken },
272 reconnection: false 271 reconnection: false
273 }) 272 })
@@ -287,11 +286,6 @@ describe('Test user notifications API validators', function () {
287 }) 286 })
288 287
289 after(async function () { 288 after(async function () {
290 killallServers([ server ]) 289 await cleanupTests([ server ])
291
292 // Keep the logs if the test failed
293 if (this['ok']) {
294 await flushTests()
295 }
296 }) 290 })
297}) 291})
diff --git a/server/tests/api/check-params/user-subscriptions.ts b/server/tests/api/check-params/user-subscriptions.ts
index 8a9ced7c1..fa36c4078 100644
--- a/server/tests/api/check-params/user-subscriptions.ts
+++ b/server/tests/api/check-params/user-subscriptions.ts
@@ -3,24 +3,23 @@
3import 'mocha' 3import 'mocha'
4 4
5import { 5import {
6 cleanupTests,
6 createUser, 7 createUser,
7 flushTests, 8 flushAndRunServer,
8 killallServers,
9 makeDeleteRequest, 9 makeDeleteRequest,
10 makeGetRequest, 10 makeGetRequest,
11 makePostBodyRequest, 11 makePostBodyRequest,
12 runServer,
13 ServerInfo, 12 ServerInfo,
14 setAccessTokensToServers, 13 setAccessTokensToServers,
15 userLogin 14 userLogin
16} from '../../../../shared/utils' 15} from '../../../../shared/extra-utils'
17 16
18import { 17import {
19 checkBadCountPagination, 18 checkBadCountPagination,
20 checkBadSortPagination, 19 checkBadSortPagination,
21 checkBadStartPagination 20 checkBadStartPagination
22} from '../../../../shared/utils/requests/check-api-params' 21} from '../../../../shared/extra-utils/requests/check-api-params'
23import { waitJobs } from '../../../../shared/utils/server/jobs' 22import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
24 23
25describe('Test user subscriptions API validators', function () { 24describe('Test user subscriptions API validators', function () {
26 const path = '/api/v1/users/me/subscriptions' 25 const path = '/api/v1/users/me/subscriptions'
@@ -32,9 +31,7 @@ describe('Test user subscriptions API validators', function () {
32 before(async function () { 31 before(async function () {
33 this.timeout(30000) 32 this.timeout(30000)
34 33
35 await flushTests() 34 server = await flushAndRunServer(1)
36
37 server = await runServer(1)
38 35
39 await setAccessTokensToServers([ server ]) 36 await setAccessTokensToServers([ server ])
40 37
@@ -42,7 +39,7 @@ describe('Test user subscriptions API validators', function () {
42 username: 'user1', 39 username: 'user1',
43 password: 'my super password' 40 password: 'my super password'
44 } 41 }
45 await createUser(server.url, server.accessToken, user.username, user.password) 42 await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
46 userAccessToken = await userLogin(server, user) 43 userAccessToken = await userLogin(server, user)
47 }) 44 })
48 45
@@ -115,7 +112,7 @@ describe('Test user subscriptions API validators', function () {
115 await makePostBodyRequest({ 112 await makePostBodyRequest({
116 url: server.url, 113 url: server.url,
117 path, 114 path,
118 fields: { uri: 'user1_channel@localhost:9001' }, 115 fields: { uri: 'user1_channel@localhost:' + server.port },
119 statusCodeExpected: 401 116 statusCodeExpected: 401
120 }) 117 })
121 }) 118 })
@@ -153,7 +150,7 @@ describe('Test user subscriptions API validators', function () {
153 url: server.url, 150 url: server.url,
154 path, 151 path,
155 token: server.accessToken, 152 token: server.accessToken,
156 fields: { uri: 'user1_channel@localhost:9001' }, 153 fields: { uri: 'user1_channel@localhost:' + server.port },
157 statusCodeExpected: 204 154 statusCodeExpected: 204
158 }) 155 })
159 156
@@ -165,7 +162,7 @@ describe('Test user subscriptions API validators', function () {
165 it('Should fail with a non authenticated user', async function () { 162 it('Should fail with a non authenticated user', async function () {
166 await makeGetRequest({ 163 await makeGetRequest({
167 url: server.url, 164 url: server.url,
168 path: path + '/user1_channel@localhost:9001', 165 path: path + '/user1_channel@localhost:' + server.port,
169 statusCodeExpected: 401 166 statusCodeExpected: 401
170 }) 167 })
171 }) 168 })
@@ -196,7 +193,7 @@ describe('Test user subscriptions API validators', function () {
196 it('Should fail with an unknown subscription', async function () { 193 it('Should fail with an unknown subscription', async function () {
197 await makeGetRequest({ 194 await makeGetRequest({
198 url: server.url, 195 url: server.url,
199 path: path + '/root1@localhost:9001', 196 path: path + '/root1@localhost:' + server.port,
200 token: server.accessToken, 197 token: server.accessToken,
201 statusCodeExpected: 404 198 statusCodeExpected: 404
202 }) 199 })
@@ -205,14 +202,14 @@ describe('Test user subscriptions API validators', function () {
205 it('Should succeed with the correct parameters', async function () { 202 it('Should succeed with the correct parameters', async function () {
206 await makeGetRequest({ 203 await makeGetRequest({
207 url: server.url, 204 url: server.url,
208 path: path + '/user1_channel@localhost:9001', 205 path: path + '/user1_channel@localhost:' + server.port,
209 token: server.accessToken, 206 token: server.accessToken,
210 statusCodeExpected: 200 207 statusCodeExpected: 200
211 }) 208 })
212 }) 209 })
213 }) 210 })
214 211
215 describe('When checking if subscriptions exist', async function () { 212 describe('When checking if subscriptions exist', function () {
216 const existPath = path + '/exist' 213 const existPath = path + '/exist'
217 214
218 it('Should fail with a non authenticated user', async function () { 215 it('Should fail with a non authenticated user', async function () {
@@ -245,7 +242,7 @@ describe('Test user subscriptions API validators', function () {
245 await makeGetRequest({ 242 await makeGetRequest({
246 url: server.url, 243 url: server.url,
247 path: existPath, 244 path: existPath,
248 query: { 'uris[]': 'coucou@localhost:9001' }, 245 query: { 'uris[]': 'coucou@localhost:' + server.port },
249 token: server.accessToken, 246 token: server.accessToken,
250 statusCodeExpected: 200 247 statusCodeExpected: 200
251 }) 248 })
@@ -256,7 +253,7 @@ describe('Test user subscriptions API validators', function () {
256 it('Should fail with a non authenticated user', async function () { 253 it('Should fail with a non authenticated user', async function () {
257 await makeDeleteRequest({ 254 await makeDeleteRequest({
258 url: server.url, 255 url: server.url,
259 path: path + '/user1_channel@localhost:9001', 256 path: path + '/user1_channel@localhost:' + server.port,
260 statusCodeExpected: 401 257 statusCodeExpected: 401
261 }) 258 })
262 }) 259 })
@@ -287,7 +284,7 @@ describe('Test user subscriptions API validators', function () {
287 it('Should fail with an unknown subscription', async function () { 284 it('Should fail with an unknown subscription', async function () {
288 await makeDeleteRequest({ 285 await makeDeleteRequest({
289 url: server.url, 286 url: server.url,
290 path: path + '/root1@localhost:9001', 287 path: path + '/root1@localhost:' + server.port,
291 token: server.accessToken, 288 token: server.accessToken,
292 statusCodeExpected: 404 289 statusCodeExpected: 404
293 }) 290 })
@@ -296,7 +293,7 @@ describe('Test user subscriptions API validators', function () {
296 it('Should succeed with the correct parameters', async function () { 293 it('Should succeed with the correct parameters', async function () {
297 await makeDeleteRequest({ 294 await makeDeleteRequest({
298 url: server.url, 295 url: server.url,
299 path: path + '/user1_channel@localhost:9001', 296 path: path + '/user1_channel@localhost:' + server.port,
300 token: server.accessToken, 297 token: server.accessToken,
301 statusCodeExpected: 204 298 statusCodeExpected: 204
302 }) 299 })
@@ -304,11 +301,6 @@ describe('Test user subscriptions API validators', function () {
304 }) 301 })
305 302
306 after(async function () { 303 after(async function () {
307 killallServers([ server ]) 304 await cleanupTests([ server ])
308
309 // Keep the logs if the test failed
310 if (this['ok']) {
311 await flushTests()
312 }
313 }) 305 })
314}) 306})
diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts
index a3e8e2e9c..5935104a5 100644
--- a/server/tests/api/check-params/users.ts
+++ b/server/tests/api/check-params/users.ts
@@ -6,19 +6,38 @@ import { join } from 'path'
6import { UserRole, VideoImport, VideoImportState } from '../../../../shared' 6import { UserRole, VideoImport, VideoImportState } from '../../../../shared'
7 7
8import { 8import {
9 createUser, flushTests, getMyUserInformation, getMyUserVideoRating, getUsersList, immutableAssign, killallServers, makeGetRequest, 9 blockUser,
10 makePostBodyRequest, makeUploadRequest, makePutBodyRequest, registerUser, removeUser, runServer, ServerInfo, setAccessTokensToServers, 10 cleanupTests,
11 updateUser, uploadVideo, userLogin, deleteMe, unblockUser, blockUser 11 createUser,
12} from '../../../../shared/utils' 12 deleteMe,
13 flushAndRunServer,
14 getMyUserInformation,
15 getMyUserVideoRating,
16 getUsersList,
17 immutableAssign,
18 makeGetRequest,
19 makePostBodyRequest,
20 makePutBodyRequest,
21 makeUploadRequest,
22 registerUser,
23 removeUser,
24 ServerInfo,
25 setAccessTokensToServers,
26 unblockUser,
27 updateUser,
28 uploadVideo,
29 userLogin
30} from '../../../../shared/extra-utils'
13import { 31import {
14 checkBadCountPagination, 32 checkBadCountPagination,
15 checkBadSortPagination, 33 checkBadSortPagination,
16 checkBadStartPagination 34 checkBadStartPagination
17} from '../../../../shared/utils/requests/check-api-params' 35} from '../../../../shared/extra-utils/requests/check-api-params'
18import { getMagnetURI, getMyVideoImports, getYoutubeVideoUrl, importVideo } from '../../../../shared/utils/videos/video-imports' 36import { getMagnetURI, getMyVideoImports, getYoutubeVideoUrl, importVideo } from '../../../../shared/extra-utils/videos/video-imports'
19import { VideoPrivacy } from '../../../../shared/models/videos' 37import { VideoPrivacy } from '../../../../shared/models/videos'
20import { waitJobs } from '../../../../shared/utils/server/jobs' 38import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
21import { expect } from 'chai' 39import { expect } from 'chai'
40import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
22 41
23describe('Test users API validators', function () { 42describe('Test users API validators', function () {
24 const path = '/api/v1/users/' 43 const path = '/api/v1/users/'
@@ -39,15 +58,19 @@ describe('Test users API validators', function () {
39 before(async function () { 58 before(async function () {
40 this.timeout(30000) 59 this.timeout(30000)
41 60
42 await flushTests() 61 server = await flushAndRunServer(1)
43 62 serverWithRegistrationDisabled = await flushAndRunServer(2)
44 server = await runServer(1)
45 serverWithRegistrationDisabled = await runServer(2)
46 63
47 await setAccessTokensToServers([ server ]) 64 await setAccessTokensToServers([ server ])
48 65
49 const videoQuota = 42000000 66 const videoQuota = 42000000
50 await createUser(server.url, server.accessToken, user.username, user.password, videoQuota) 67 await createUser({
68 url: server.url,
69 accessToken: server.accessToken,
70 username: user.username,
71 password: user.password,
72 videoQuota: videoQuota
73 })
51 userAccessToken = await userLogin(server, user) 74 userAccessToken = await userLogin(server, user)
52 75
53 { 76 {
@@ -99,7 +122,8 @@ describe('Test users API validators', function () {
99 password: 'my super password', 122 password: 'my super password',
100 videoQuota: -1, 123 videoQuota: -1,
101 videoQuotaDaily: -1, 124 videoQuotaDaily: -1,
102 role: UserRole.USER 125 role: UserRole.USER,
126 adminFlags: UserAdminFlag.BY_PASS_VIDEO_AUTO_BLACKLIST
103 } 127 }
104 128
105 it('Should fail with a too small username', async function () { 129 it('Should fail with a too small username', async function () {
@@ -150,6 +174,12 @@ describe('Test users API validators', function () {
150 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) 174 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
151 }) 175 })
152 176
177 it('Should fail with invalid admin flags', async function () {
178 const fields = immutableAssign(baseCorrectParams, { adminFlags: 'toto' })
179
180 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
181 })
182
153 it('Should fail with an non authenticated user', async function () { 183 it('Should fail with an non authenticated user', async function () {
154 await makePostBodyRequest({ 184 await makePostBodyRequest({
155 url: server.url, 185 url: server.url,
@@ -464,6 +494,24 @@ describe('Test users API validators', function () {
464 await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields }) 494 await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields })
465 }) 495 })
466 496
497 it('Should fail with a too small password', async function () {
498 const fields = {
499 currentPassword: 'my super password',
500 password: 'bla'
501 }
502
503 await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields })
504 })
505
506 it('Should fail with a too long password', async function () {
507 const fields = {
508 currentPassword: 'my super password',
509 password: 'super'.repeat(61)
510 }
511
512 await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields })
513 })
514
467 it('Should fail with an non authenticated user', async function () { 515 it('Should fail with an non authenticated user', async function () {
468 const fields = { 516 const fields = {
469 videoQuota: 42 517 videoQuota: 42
@@ -480,6 +528,12 @@ describe('Test users API validators', function () {
480 await makePutBodyRequest({ url: server.url, path: path + rootId, token: server.accessToken, fields }) 528 await makePutBodyRequest({ url: server.url, path: path + rootId, token: server.accessToken, fields })
481 }) 529 })
482 530
531 it('Should fail with invalid admin flags', async function () {
532 const fields = { adminFlags: 'toto' }
533
534 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
535 })
536
483 it('Should succeed with the correct params', async function () { 537 it('Should succeed with the correct params', async function () {
484 const fields = { 538 const fields = {
485 email: 'email@example.com', 539 email: 'email@example.com',
@@ -520,6 +574,38 @@ describe('Test users API validators', function () {
520 }) 574 })
521 }) 575 })
522 576
577 describe('When retrieving my global ratings', function () {
578 const path = '/api/v1/accounts/user1/ratings'
579
580 it('Should fail with a bad start pagination', async function () {
581 await checkBadStartPagination(server.url, path, userAccessToken)
582 })
583
584 it('Should fail with a bad count pagination', async function () {
585 await checkBadCountPagination(server.url, path, userAccessToken)
586 })
587
588 it('Should fail with an incorrect sort', async function () {
589 await checkBadSortPagination(server.url, path, userAccessToken)
590 })
591
592 it('Should fail with a unauthenticated user', async function () {
593 await makeGetRequest({ url: server.url, path, statusCodeExpected: 401 })
594 })
595
596 it('Should fail with a another user', async function () {
597 await makeGetRequest({ url: server.url, path, token: server.accessToken, statusCodeExpected: 403 })
598 })
599
600 it('Should fail with a bad type', async function () {
601 await makeGetRequest({ url: server.url, path, token: userAccessToken, query: { rating: 'toto ' }, statusCodeExpected: 400 })
602 })
603
604 it('Should succeed with the correct params', async function () {
605 await makeGetRequest({ url: server.url, path, token: userAccessToken, statusCodeExpected: 200 })
606 })
607 })
608
523 describe('When blocking/unblocking/removing user', function () { 609 describe('When blocking/unblocking/removing user', function () {
524 it('Should fail with an incorrect id', async function () { 610 it('Should fail with an incorrect id', async function () {
525 await removeUser(server.url, 'blabla', server.accessToken, 400) 611 await removeUser(server.url, 'blabla', server.accessToken, 400)
@@ -627,7 +713,7 @@ describe('Test users API validators', function () {
627 }) 713 })
628 714
629 it('Should fail if we register a user with the same email', async function () { 715 it('Should fail if we register a user with the same email', async function () {
630 const fields = immutableAssign(baseCorrectParams, { email: 'admin1@example.com' }) 716 const fields = immutableAssign(baseCorrectParams, { email: 'admin' + server.internalServerNumber + '@example.com' })
631 717
632 await makePostBodyRequest({ 718 await makePostBodyRequest({
633 url: server.url, 719 url: server.url,
@@ -812,11 +898,6 @@ describe('Test users API validators', function () {
812 }) 898 })
813 899
814 after(async function () { 900 after(async function () {
815 killallServers([ server, serverWithRegistrationDisabled ]) 901 await cleanupTests([ server, serverWithRegistrationDisabled ])
816
817 // Keep the logs if the test failed
818 if (this['ok']) {
819 await flushTests()
820 }
821 }) 902 })
822}) 903})
diff --git a/server/tests/api/check-params/video-abuses.ts b/server/tests/api/check-params/video-abuses.ts
index 3b8f5f14d..bf29f8d4d 100644
--- a/server/tests/api/check-params/video-abuses.ts
+++ b/server/tests/api/check-params/video-abuses.ts
@@ -3,24 +3,23 @@
3import 'mocha' 3import 'mocha'
4 4
5import { 5import {
6 cleanupTests,
6 createUser, 7 createUser,
7 deleteVideoAbuse, 8 deleteVideoAbuse,
8 flushTests, 9 flushAndRunServer,
9 killallServers,
10 makeGetRequest, 10 makeGetRequest,
11 makePostBodyRequest, 11 makePostBodyRequest,
12 runServer,
13 ServerInfo, 12 ServerInfo,
14 setAccessTokensToServers, 13 setAccessTokensToServers,
15 updateVideoAbuse, 14 updateVideoAbuse,
16 uploadVideo, 15 uploadVideo,
17 userLogin 16 userLogin
18} from '../../../../shared/utils' 17} from '../../../../shared/extra-utils'
19import { 18import {
20 checkBadCountPagination, 19 checkBadCountPagination,
21 checkBadSortPagination, 20 checkBadSortPagination,
22 checkBadStartPagination 21 checkBadStartPagination
23} from '../../../../shared/utils/requests/check-api-params' 22} from '../../../../shared/extra-utils/requests/check-api-params'
24import { VideoAbuseState } from '../../../../shared/models/videos' 23import { VideoAbuseState } from '../../../../shared/models/videos'
25 24
26describe('Test video abuses API validators', function () { 25describe('Test video abuses API validators', function () {
@@ -33,15 +32,13 @@ describe('Test video abuses API validators', function () {
33 before(async function () { 32 before(async function () {
34 this.timeout(30000) 33 this.timeout(30000)
35 34
36 await flushTests() 35 server = await flushAndRunServer(1)
37
38 server = await runServer(1)
39 36
40 await setAccessTokensToServers([ server ]) 37 await setAccessTokensToServers([ server ])
41 38
42 const username = 'user1' 39 const username = 'user1'
43 const password = 'my super password' 40 const password = 'my super password'
44 await createUser(server.url, server.accessToken, username, password) 41 await createUser({ url: server.url, accessToken: server.accessToken, username: username, password: password })
45 userAccessToken = await userLogin(server, { username, password }) 42 userAccessToken = await userLogin(server, { username, password })
46 43
47 const res = await uploadVideo(server.url, server.accessToken, {}) 44 const res = await uploadVideo(server.url, server.accessToken, {})
@@ -191,11 +188,6 @@ describe('Test video abuses API validators', function () {
191 }) 188 })
192 189
193 after(async function () { 190 after(async function () {
194 killallServers([ server ]) 191 await cleanupTests([ server ])
195
196 // Keep the logs if the test failed
197 if (this['ok']) {
198 await flushTests()
199 }
200 }) 192 })
201}) 193})
diff --git a/server/tests/api/check-params/video-blacklist.ts b/server/tests/api/check-params/video-blacklist.ts
index 6b82643f4..6466888fb 100644
--- a/server/tests/api/check-params/video-blacklist.ts
+++ b/server/tests/api/check-params/video-blacklist.ts
@@ -3,6 +3,7 @@
3import 'mocha' 3import 'mocha'
4 4
5import { 5import {
6 cleanupTests,
6 createUser, 7 createUser,
7 doubleFollow, 8 doubleFollow,
8 flushAndRunMultipleServers, 9 flushAndRunMultipleServers,
@@ -18,13 +19,13 @@ import {
18 setAccessTokensToServers, 19 setAccessTokensToServers,
19 uploadVideo, 20 uploadVideo,
20 userLogin, waitJobs 21 userLogin, waitJobs
21} from '../../../../shared/utils' 22} from '../../../../shared/extra-utils'
22import { 23import {
23 checkBadCountPagination, 24 checkBadCountPagination,
24 checkBadSortPagination, 25 checkBadSortPagination,
25 checkBadStartPagination 26 checkBadStartPagination
26} from '../../../../shared/utils/requests/check-api-params' 27} from '../../../../shared/extra-utils/requests/check-api-params'
27import { VideoDetails } from '../../../../shared/models/videos' 28import { VideoDetails, VideoBlacklistType } from '../../../../shared/models/videos'
28import { expect } from 'chai' 29import { expect } from 'chai'
29 30
30describe('Test video blacklist API validators', function () { 31describe('Test video blacklist API validators', function () {
@@ -39,7 +40,6 @@ describe('Test video blacklist API validators', function () {
39 before(async function () { 40 before(async function () {
40 this.timeout(120000) 41 this.timeout(120000)
41 42
42 await flushTests()
43 servers = await flushAndRunMultipleServers(2) 43 servers = await flushAndRunMultipleServers(2)
44 44
45 await setAccessTokensToServers(servers) 45 await setAccessTokensToServers(servers)
@@ -48,14 +48,14 @@ describe('Test video blacklist API validators', function () {
48 { 48 {
49 const username = 'user1' 49 const username = 'user1'
50 const password = 'my super password' 50 const password = 'my super password'
51 await createUser(servers[0].url, servers[0].accessToken, username, password) 51 await createUser({ url: servers[ 0 ].url, accessToken: servers[ 0 ].accessToken, username: username, password: password })
52 userAccessToken1 = await userLogin(servers[0], { username, password }) 52 userAccessToken1 = await userLogin(servers[0], { username, password })
53 } 53 }
54 54
55 { 55 {
56 const username = 'user2' 56 const username = 'user2'
57 const password = 'my super password' 57 const password = 'my super password'
58 await createUser(servers[0].url, servers[0].accessToken, username, password) 58 await createUser({ url: servers[ 0 ].url, accessToken: servers[ 0 ].accessToken, username: username, password: password })
59 userAccessToken2 = await userLogin(servers[0], { username, password }) 59 userAccessToken2 = await userLogin(servers[0], { username, password })
60 } 60 }
61 61
@@ -220,11 +220,11 @@ describe('Test video blacklist API validators', function () {
220 const basePath = '/api/v1/videos/blacklist/' 220 const basePath = '/api/v1/videos/blacklist/'
221 221
222 it('Should fail with a non authenticated user', async function () { 222 it('Should fail with a non authenticated user', async function () {
223 await getBlacklistedVideosList(servers[0].url, 'fake token', 401) 223 await getBlacklistedVideosList({ url: servers[0].url, token: 'fake token', specialStatus: 401 })
224 }) 224 })
225 225
226 it('Should fail with a non admin user', async function () { 226 it('Should fail with a non admin user', async function () {
227 await getBlacklistedVideosList(servers[0].url, userAccessToken2, 403) 227 await getBlacklistedVideosList({ url: servers[0].url, token: userAccessToken2, specialStatus: 403 })
228 }) 228 })
229 229
230 it('Should fail with a bad start pagination', async function () { 230 it('Should fail with a bad start pagination', async function () {
@@ -238,14 +238,17 @@ describe('Test video blacklist API validators', function () {
238 it('Should fail with an incorrect sort', async function () { 238 it('Should fail with an incorrect sort', async function () {
239 await checkBadSortPagination(servers[0].url, basePath, servers[0].accessToken) 239 await checkBadSortPagination(servers[0].url, basePath, servers[0].accessToken)
240 }) 240 })
241
242 it('Should fail with an invalid type', async function () {
243 await getBlacklistedVideosList({ url: servers[0].url, token: servers[0].accessToken, type: 0, specialStatus: 400 })
244 })
245
246 it('Should succeed with the correct parameters', async function () {
247 await getBlacklistedVideosList({ url: servers[0].url, token: servers[0].accessToken, type: VideoBlacklistType.MANUAL })
248 })
241 }) 249 })
242 250
243 after(async function () { 251 after(async function () {
244 killallServers(servers) 252 await cleanupTests(servers)
245
246 // Keep the logs if the test failed
247 if (this['ok']) {
248 await flushTests()
249 }
250 }) 253 })
251}) 254})
diff --git a/server/tests/api/check-params/video-captions.ts b/server/tests/api/check-params/video-captions.ts
index e4d36fd4f..4a373d43d 100644
--- a/server/tests/api/check-params/video-captions.ts
+++ b/server/tests/api/check-params/video-captions.ts
@@ -2,20 +2,19 @@
2 2
3import 'mocha' 3import 'mocha'
4import { 4import {
5 cleanupTests,
5 createUser, 6 createUser,
6 flushTests, 7 flushAndRunServer,
7 killallServers,
8 makeDeleteRequest, 8 makeDeleteRequest,
9 makeGetRequest, 9 makeGetRequest,
10 makeUploadRequest, 10 makeUploadRequest,
11 runServer,
12 ServerInfo, 11 ServerInfo,
13 setAccessTokensToServers, 12 setAccessTokensToServers,
14 uploadVideo, 13 uploadVideo,
15 userLogin 14 userLogin
16} from '../../../../shared/utils' 15} from '../../../../shared/extra-utils'
17import { join } from 'path' 16import { join } from 'path'
18import { createVideoCaption } from '../../../../shared/utils/videos/video-captions' 17import { createVideoCaption } from '../../../../shared/extra-utils/videos/video-captions'
19 18
20describe('Test video captions API validator', function () { 19describe('Test video captions API validator', function () {
21 const path = '/api/v1/videos/' 20 const path = '/api/v1/videos/'
@@ -29,9 +28,7 @@ describe('Test video captions API validator', function () {
29 before(async function () { 28 before(async function () {
30 this.timeout(30000) 29 this.timeout(30000)
31 30
32 await flushTests() 31 server = await flushAndRunServer(1)
33
34 server = await runServer(1)
35 32
36 await setAccessTokensToServers([ server ]) 33 await setAccessTokensToServers([ server ])
37 34
@@ -45,7 +42,7 @@ describe('Test video captions API validator', function () {
45 username: 'user1', 42 username: 'user1',
46 password: 'my super password' 43 password: 'my super password'
47 } 44 }
48 await createUser(server.url, server.accessToken, user.username, user.password) 45 await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
49 userAccessToken = await userLogin(server, user) 46 userAccessToken = await userLogin(server, user)
50 } 47 }
51 }) 48 })
@@ -272,11 +269,6 @@ describe('Test video captions API validator', function () {
272 }) 269 })
273 270
274 after(async function () { 271 after(async function () {
275 killallServers([ server ]) 272 await cleanupTests([ server ])
276
277 // Keep the logs if the test failed
278 if (this['ok']) {
279 await flushTests()
280 }
281 }) 273 })
282}) 274})
diff --git a/server/tests/api/check-params/video-channels.ts b/server/tests/api/check-params/video-channels.ts
index 14e4deaf7..65bc20613 100644
--- a/server/tests/api/check-params/video-channels.ts
+++ b/server/tests/api/check-params/video-channels.ts
@@ -4,29 +4,25 @@ import * as chai from 'chai'
4import { omit } from 'lodash' 4import { omit } from 'lodash'
5import 'mocha' 5import 'mocha'
6import { 6import {
7 cleanupTests,
7 createUser, 8 createUser,
8 deleteVideoChannel, 9 deleteVideoChannel,
9 flushTests, 10 flushAndRunServer,
10 getAccountVideoChannelsList, 11 getAccountVideoChannelsList,
11 getMyUserInformation,
12 getVideoChannelsList,
13 immutableAssign, 12 immutableAssign,
14 killallServers,
15 makeGetRequest, 13 makeGetRequest,
16 makePostBodyRequest, 14 makePostBodyRequest,
17 makePutBodyRequest, 15 makePutBodyRequest,
18 makeUploadRequest, 16 makeUploadRequest,
19 runServer,
20 ServerInfo, 17 ServerInfo,
21 setAccessTokensToServers, 18 setAccessTokensToServers,
22 userLogin 19 userLogin
23} from '../../../../shared/utils' 20} from '../../../../shared/extra-utils'
24import { 21import {
25 checkBadCountPagination, 22 checkBadCountPagination,
26 checkBadSortPagination, 23 checkBadSortPagination,
27 checkBadStartPagination 24 checkBadStartPagination
28} from '../../../../shared/utils/requests/check-api-params' 25} from '../../../../shared/extra-utils/requests/check-api-params'
29import { User } from '../../../../shared/models/users'
30import { join } from 'path' 26import { join } from 'path'
31 27
32const expect = chai.expect 28const expect = chai.expect
@@ -41,9 +37,7 @@ describe('Test video channels API validator', function () {
41 before(async function () { 37 before(async function () {
42 this.timeout(30000) 38 this.timeout(30000)
43 39
44 await flushTests() 40 server = await flushAndRunServer(1)
45
46 server = await runServer(1)
47 41
48 await setAccessTokensToServers([ server ]) 42 await setAccessTokensToServers([ server ])
49 43
@@ -53,7 +47,7 @@ describe('Test video channels API validator', function () {
53 } 47 }
54 48
55 { 49 {
56 await createUser(server.url, server.accessToken, user.username, user.password) 50 await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
57 accessTokenUser = await userLogin(server, user) 51 accessTokenUser = await userLogin(server, user)
58 } 52 }
59 }) 53 })
@@ -313,11 +307,6 @@ describe('Test video channels API validator', function () {
313 }) 307 })
314 308
315 after(async function () { 309 after(async function () {
316 killallServers([ server ]) 310 await cleanupTests([ server ])
317
318 // Keep the logs if the test failed
319 if (this['ok']) {
320 await flushTests()
321 }
322 }) 311 })
323}) 312})
diff --git a/server/tests/api/check-params/video-comments.ts b/server/tests/api/check-params/video-comments.ts
index 5981780ed..5cf90bacc 100644
--- a/server/tests/api/check-params/video-comments.ts
+++ b/server/tests/api/check-params/video-comments.ts
@@ -3,16 +3,23 @@
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { 5import {
6 cleanupTests,
6 createUser, 7 createUser,
7 flushTests, killallServers, makeDeleteRequest, makeGetRequest, makePostBodyRequest, runServer, ServerInfo, setAccessTokensToServers, 8 flushAndRunServer,
8 uploadVideo, userLogin 9 makeDeleteRequest,
9} from '../../../../shared/utils' 10 makeGetRequest,
11 makePostBodyRequest,
12 ServerInfo,
13 setAccessTokensToServers,
14 uploadVideo,
15 userLogin
16} from '../../../../shared/extra-utils'
10import { 17import {
11 checkBadCountPagination, 18 checkBadCountPagination,
12 checkBadSortPagination, 19 checkBadSortPagination,
13 checkBadStartPagination 20 checkBadStartPagination
14} from '../../../../shared/utils/requests/check-api-params' 21} from '../../../../shared/extra-utils/requests/check-api-params'
15import { addVideoCommentThread } from '../../../../shared/utils/videos/video-comments' 22import { addVideoCommentThread } from '../../../../shared/extra-utils/videos/video-comments'
16 23
17const expect = chai.expect 24const expect = chai.expect
18 25
@@ -29,9 +36,7 @@ describe('Test video comments API validator', function () {
29 before(async function () { 36 before(async function () {
30 this.timeout(30000) 37 this.timeout(30000)
31 38
32 await flushTests() 39 server = await flushAndRunServer(1)
33
34 server = await runServer(1)
35 40
36 await setAccessTokensToServers([ server ]) 41 await setAccessTokensToServers([ server ])
37 42
@@ -52,7 +57,7 @@ describe('Test video comments API validator', function () {
52 username: 'user1', 57 username: 'user1',
53 password: 'my super password' 58 password: 'my super password'
54 } 59 }
55 await createUser(server.url, server.accessToken, user.username, user.password) 60 await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
56 userAccessToken = await userLogin(server, user) 61 userAccessToken = await userLogin(server, user)
57 } 62 }
58 }) 63 })
@@ -254,11 +259,6 @@ describe('Test video comments API validator', function () {
254 }) 259 })
255 260
256 after(async function () { 261 after(async function () {
257 killallServers([ server ]) 262 await cleanupTests([ server ])
258
259 // Keep the logs if the test failed
260 if (this['ok']) {
261 await flushTests()
262 }
263 }) 263 })
264}) 264})
diff --git a/server/tests/api/check-params/video-imports.ts b/server/tests/api/check-params/video-imports.ts
index 7bf187007..8ff115e7b 100644
--- a/server/tests/api/check-params/video-imports.ts
+++ b/server/tests/api/check-params/video-imports.ts
@@ -5,26 +5,25 @@ import 'mocha'
5import { join } from 'path' 5import { join } from 'path'
6import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum' 6import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum'
7import { 7import {
8 cleanupTests,
8 createUser, 9 createUser,
9 flushTests, 10 flushAndRunServer,
10 getMyUserInformation, 11 getMyUserInformation,
11 immutableAssign, 12 immutableAssign,
12 killallServers,
13 makeGetRequest, 13 makeGetRequest,
14 makePostBodyRequest, 14 makePostBodyRequest,
15 makeUploadRequest, 15 makeUploadRequest,
16 runServer,
17 ServerInfo, 16 ServerInfo,
18 setAccessTokensToServers, 17 setAccessTokensToServers,
19 updateCustomSubConfig, 18 updateCustomSubConfig,
20 userLogin 19 userLogin
21} from '../../../../shared/utils' 20} from '../../../../shared/extra-utils'
22import { 21import {
23 checkBadCountPagination, 22 checkBadCountPagination,
24 checkBadSortPagination, 23 checkBadSortPagination,
25 checkBadStartPagination 24 checkBadStartPagination
26} from '../../../../shared/utils/requests/check-api-params' 25} from '../../../../shared/extra-utils/requests/check-api-params'
27import { getMagnetURI, getYoutubeVideoUrl } from '../../../../shared/utils/videos/video-imports' 26import { getMagnetURI, getYoutubeVideoUrl } from '../../../../shared/extra-utils/videos/video-imports'
28 27
29describe('Test video imports API validator', function () { 28describe('Test video imports API validator', function () {
30 const path = '/api/v1/videos/imports' 29 const path = '/api/v1/videos/imports'
@@ -38,15 +37,13 @@ describe('Test video imports API validator', function () {
38 before(async function () { 37 before(async function () {
39 this.timeout(30000) 38 this.timeout(30000)
40 39
41 await flushTests() 40 server = await flushAndRunServer(1)
42
43 server = await runServer(1)
44 41
45 await setAccessTokensToServers([ server ]) 42 await setAccessTokensToServers([ server ])
46 43
47 const username = 'user1' 44 const username = 'user1'
48 const password = 'my super password' 45 const password = 'my super password'
49 await createUser(server.url, server.accessToken, username, password) 46 await createUser({ url: server.url, accessToken: server.accessToken, username: username, password: password })
50 userAccessToken = await userLogin(server, { username, password }) 47 userAccessToken = await userLogin(server, { username, password })
51 48
52 { 49 {
@@ -88,6 +85,7 @@ describe('Test video imports API validator', function () {
88 language: 'pt', 85 language: 'pt',
89 nsfw: false, 86 nsfw: false,
90 commentsEnabled: true, 87 commentsEnabled: true,
88 downloadEnabled: true,
91 waitTranscoding: true, 89 waitTranscoding: true,
92 description: 'my super description', 90 description: 'my super description',
93 support: 'my super support text', 91 support: 'my super support text',
@@ -166,7 +164,7 @@ describe('Test video imports API validator', function () {
166 username: 'fake', 164 username: 'fake',
167 password: 'fake_password' 165 password: 'fake_password'
168 } 166 }
169 await createUser(server.url, server.accessToken, user.username, user.password) 167 await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
170 168
171 const accessTokenUser = await userLogin(server, user) 169 const accessTokenUser = await userLogin(server, user)
172 const res = await getMyUserInformation(server.url, accessTokenUser) 170 const res = await getMyUserInformation(server.url, accessTokenUser)
@@ -313,11 +311,6 @@ describe('Test video imports API validator', function () {
313 }) 311 })
314 312
315 after(async function () { 313 after(async function () {
316 killallServers([ server ]) 314 await cleanupTests([ server ])
317
318 // Keep the logs if the test failed
319 if (this['ok']) {
320 await flushTests()
321 }
322 }) 315 })
323}) 316})
diff --git a/server/tests/api/check-params/video-playlists.ts b/server/tests/api/check-params/video-playlists.ts
new file mode 100644
index 000000000..b7b94c035
--- /dev/null
+++ b/server/tests/api/check-params/video-playlists.ts
@@ -0,0 +1,674 @@
1/* tslint:disable:no-unused-expression */
2
3import 'mocha'
4import {
5 addVideoInPlaylist,
6 cleanupTests,
7 createVideoPlaylist,
8 deleteVideoPlaylist,
9 flushAndRunServer,
10 generateUserAccessToken,
11 getAccountPlaylistsListWithToken,
12 getVideoPlaylist,
13 immutableAssign,
14 makeGetRequest,
15 removeVideoFromPlaylist,
16 reorderVideosPlaylist,
17 ServerInfo,
18 setAccessTokensToServers,
19 setDefaultVideoChannel,
20 updateVideoPlaylist,
21 updateVideoPlaylistElement,
22 uploadVideoAndGetId
23} from '../../../../shared/extra-utils'
24import {
25 checkBadCountPagination,
26 checkBadSortPagination,
27 checkBadStartPagination
28} from '../../../../shared/extra-utils/requests/check-api-params'
29import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
30import { VideoPlaylistType } from '../../../../shared/models/videos/playlist/video-playlist-type.model'
31
32describe('Test video playlists API validator', function () {
33 let server: ServerInfo
34 let userAccessToken: string
35 let playlistUUID: string
36 let privatePlaylistUUID: string
37 let watchLaterPlaylistId: number
38 let videoId: number
39 let videoId2: number
40
41 // ---------------------------------------------------------------
42
43 before(async function () {
44 this.timeout(30000)
45
46 server = await flushAndRunServer(1)
47
48 await setAccessTokensToServers([ server ])
49 await setDefaultVideoChannel([ server ])
50
51 userAccessToken = await generateUserAccessToken(server, 'user1')
52 videoId = (await uploadVideoAndGetId({ server, videoName: 'video 1' })).id
53 videoId2 = (await uploadVideoAndGetId({ server, videoName: 'video 2' })).id
54
55 {
56 const res = await getAccountPlaylistsListWithToken(server.url, server.accessToken, 'root',0, 5, VideoPlaylistType.WATCH_LATER)
57 watchLaterPlaylistId = res.body.data[0].id
58 }
59
60 {
61 const res = await createVideoPlaylist({
62 url: server.url,
63 token: server.accessToken,
64 playlistAttrs: {
65 displayName: 'super playlist',
66 privacy: VideoPlaylistPrivacy.PUBLIC,
67 videoChannelId: server.videoChannel.id
68 }
69 })
70 playlistUUID = res.body.videoPlaylist.uuid
71 }
72
73 {
74 const res = await createVideoPlaylist({
75 url: server.url,
76 token: server.accessToken,
77 playlistAttrs: {
78 displayName: 'private',
79 privacy: VideoPlaylistPrivacy.PRIVATE
80 }
81 })
82 privatePlaylistUUID = res.body.videoPlaylist.uuid
83 }
84 })
85
86 describe('When listing playlists', function () {
87 const globalPath = '/api/v1/video-playlists'
88 const accountPath = '/api/v1/accounts/root/video-playlists'
89 const videoChannelPath = '/api/v1/video-channels/root_channel/video-playlists'
90
91 it('Should fail with a bad start pagination', async function () {
92 await checkBadStartPagination(server.url, globalPath, server.accessToken)
93 await checkBadStartPagination(server.url, accountPath, server.accessToken)
94 await checkBadStartPagination(server.url, videoChannelPath, server.accessToken)
95 })
96
97 it('Should fail with a bad count pagination', async function () {
98 await checkBadCountPagination(server.url, globalPath, server.accessToken)
99 await checkBadCountPagination(server.url, accountPath, server.accessToken)
100 await checkBadCountPagination(server.url, videoChannelPath, server.accessToken)
101 })
102
103 it('Should fail with an incorrect sort', async function () {
104 await checkBadSortPagination(server.url, globalPath, server.accessToken)
105 await checkBadSortPagination(server.url, accountPath, server.accessToken)
106 await checkBadSortPagination(server.url, videoChannelPath, server.accessToken)
107 })
108
109 it('Should fail with a bad playlist type', async function () {
110 await makeGetRequest({ url: server.url, path: globalPath, query: { playlistType: 3 } })
111 await makeGetRequest({ url: server.url, path: accountPath, query: { playlistType: 3 } })
112 await makeGetRequest({ url: server.url, path: videoChannelPath, query: { playlistType: 3 } })
113 })
114
115 it('Should fail with a bad account parameter', async function () {
116 const accountPath = '/api/v1/accounts/root2/video-playlists'
117
118 await makeGetRequest({ url: server.url, path: accountPath, statusCodeExpected: 404, token: server.accessToken })
119 })
120
121 it('Should fail with a bad video channel parameter', async function () {
122 const accountPath = '/api/v1/video-channels/bad_channel/video-playlists'
123
124 await makeGetRequest({ url: server.url, path: accountPath, statusCodeExpected: 404, token: server.accessToken })
125 })
126
127 it('Should success with the correct parameters', async function () {
128 await makeGetRequest({ url: server.url, path: globalPath, statusCodeExpected: 200, token: server.accessToken })
129 await makeGetRequest({ url: server.url, path: accountPath, statusCodeExpected: 200, token: server.accessToken })
130 await makeGetRequest({ url: server.url, path: videoChannelPath, statusCodeExpected: 200, token: server.accessToken })
131 })
132 })
133
134 describe('When listing videos of a playlist', function () {
135 const path = '/api/v1/video-playlists'
136
137 it('Should fail with a bad start pagination', async function () {
138 await checkBadStartPagination(server.url, path, server.accessToken)
139 })
140
141 it('Should fail with a bad count pagination', async function () {
142 await checkBadCountPagination(server.url, path, server.accessToken)
143 })
144
145 it('Should fail with a bad filter', async function () {
146 await checkBadSortPagination(server.url, path, server.accessToken)
147 })
148 })
149
150 describe('When getting a video playlist', function () {
151 it('Should fail with a bad id or uuid', async function () {
152 await getVideoPlaylist(server.url, 'toto', 400)
153 })
154
155 it('Should fail with an unknown playlist', async function () {
156 await getVideoPlaylist(server.url, 42, 404)
157 })
158
159 it('Should fail to get an unlisted playlist with the number id', async function () {
160 const res = await createVideoPlaylist({
161 url: server.url,
162 token: server.accessToken,
163 playlistAttrs: {
164 displayName: 'super playlist',
165 privacy: VideoPlaylistPrivacy.UNLISTED
166 }
167 })
168 const playlist = res.body.videoPlaylist
169
170 await getVideoPlaylist(server.url, playlist.id, 404)
171 await getVideoPlaylist(server.url, playlist.uuid, 200)
172 })
173
174 it('Should succeed with the correct params', async function () {
175 await getVideoPlaylist(server.url, playlistUUID, 200)
176 })
177 })
178
179 describe('When creating/updating a video playlist', function () {
180 const getBase = (playlistAttrs: any = {}, wrapper: any = {}) => {
181 return Object.assign({
182 expectedStatus: 400,
183 url: server.url,
184 token: server.accessToken,
185 playlistAttrs: Object.assign({
186 displayName: 'display name',
187 privacy: VideoPlaylistPrivacy.UNLISTED,
188 thumbnailfile: 'thumbnail.jpg',
189 videoChannelId: server.videoChannel.id
190 }, playlistAttrs)
191 }, wrapper)
192 }
193 const getUpdate = (params: any, playlistId: number | string) => {
194 return immutableAssign(params, { playlistId: playlistId })
195 }
196
197 it('Should fail with an unauthenticated user', async function () {
198 const params = getBase({}, { token: null, expectedStatus: 401 })
199
200 await createVideoPlaylist(params)
201 await updateVideoPlaylist(getUpdate(params, playlistUUID))
202 })
203
204 it('Should fail without displayName', async function () {
205 const params = getBase({ displayName: undefined })
206
207 await createVideoPlaylist(params)
208 await updateVideoPlaylist(getUpdate(params, playlistUUID))
209 })
210
211 it('Should fail with an incorrect display name', async function () {
212 const params = getBase({ displayName: 's'.repeat(300) })
213
214 await createVideoPlaylist(params)
215 await updateVideoPlaylist(getUpdate(params, playlistUUID))
216 })
217
218 it('Should fail with an incorrect description', async function () {
219 const params = getBase({ description: 't' })
220
221 await createVideoPlaylist(params)
222 await updateVideoPlaylist(getUpdate(params, playlistUUID))
223 })
224
225 it('Should fail with an incorrect privacy', async function () {
226 const params = getBase({ privacy: 45 })
227
228 await createVideoPlaylist(params)
229 await updateVideoPlaylist(getUpdate(params, playlistUUID))
230 })
231
232 it('Should fail with an unknown video channel id', async function () {
233 const params = getBase({ videoChannelId: 42 }, { expectedStatus: 404 })
234
235 await createVideoPlaylist(params)
236 await updateVideoPlaylist(getUpdate(params, playlistUUID))
237 })
238
239 it('Should fail with an incorrect thumbnail file', async function () {
240 const params = getBase({ thumbnailfile: 'avatar.png' })
241
242 await createVideoPlaylist(params)
243 await updateVideoPlaylist(getUpdate(params, playlistUUID))
244 })
245
246 it('Should fail to set "public" a playlist not assigned to a channel', async function () {
247 const params = getBase({ privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: undefined })
248 const params2 = getBase({ privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: 'null' })
249 const params3 = getBase({ privacy: undefined, videoChannelId: 'null' })
250
251 await createVideoPlaylist(params)
252 await createVideoPlaylist(params2)
253 await updateVideoPlaylist(getUpdate(params, privatePlaylistUUID))
254 await updateVideoPlaylist(getUpdate(params2, playlistUUID))
255 await updateVideoPlaylist(getUpdate(params3, playlistUUID))
256 })
257
258 it('Should fail with an unknown playlist to update', async function () {
259 await updateVideoPlaylist(getUpdate(
260 getBase({}, { expectedStatus: 404 }),
261 42
262 ))
263 })
264
265 it('Should fail to update a playlist of another user', async function () {
266 await updateVideoPlaylist(getUpdate(
267 getBase({}, { token: userAccessToken, expectedStatus: 403 }),
268 playlistUUID
269 ))
270 })
271
272 it('Should fail to update to private a public/unlisted playlist', async function () {
273 const params = getBase({ privacy: VideoPlaylistPrivacy.PUBLIC }, { expectedStatus: 200 })
274
275 const res = await createVideoPlaylist(params)
276 const playlist = res.body.videoPlaylist
277
278 const paramsUpdate = getBase({ privacy: VideoPlaylistPrivacy.PRIVATE }, { expectedStatus: 400 })
279
280 await updateVideoPlaylist(getUpdate(paramsUpdate, playlist.id))
281 })
282
283 it('Should fail to update the watch later playlist', async function () {
284 await updateVideoPlaylist(getUpdate(
285 getBase({}, { expectedStatus: 400 }),
286 watchLaterPlaylistId
287 ))
288 })
289
290 it('Should succeed with the correct params', async function () {
291 {
292 const params = getBase({}, { expectedStatus: 200 })
293 await createVideoPlaylist(params)
294 }
295
296 {
297 const params = getBase({}, { expectedStatus: 204 })
298 await updateVideoPlaylist(getUpdate(params, playlistUUID))
299 }
300 })
301 })
302
303 describe('When adding an element in a playlist', function () {
304 const getBase = (elementAttrs: any = {}, wrapper: any = {}) => {
305 return Object.assign({
306 expectedStatus: 400,
307 url: server.url,
308 token: server.accessToken,
309 playlistId: playlistUUID,
310 elementAttrs: Object.assign({
311 videoId: videoId,
312 startTimestamp: 2,
313 stopTimestamp: 3
314 }, elementAttrs)
315 }, wrapper)
316 }
317
318 it('Should fail with an unauthenticated user', async function () {
319 const params = getBase({}, { token: null, expectedStatus: 401 })
320 await addVideoInPlaylist(params)
321 })
322
323 it('Should fail with the playlist of another user', async function () {
324 const params = getBase({}, { token: userAccessToken, expectedStatus: 403 })
325 await addVideoInPlaylist(params)
326 })
327
328 it('Should fail with an unknown or incorrect playlist id', async function () {
329 {
330 const params = getBase({}, { playlistId: 'toto' })
331 await addVideoInPlaylist(params)
332 }
333
334 {
335 const params = getBase({}, { playlistId: 42, expectedStatus: 404 })
336 await addVideoInPlaylist(params)
337 }
338 })
339
340 it('Should fail with an unknown or incorrect video id', async function () {
341 const params = getBase({ videoId: 42 }, { expectedStatus: 404 })
342 await addVideoInPlaylist(params)
343 })
344
345 it('Should fail with a bad start/stop timestamp', async function () {
346 {
347 const params = getBase({ startTimestamp: -42 })
348 await addVideoInPlaylist(params)
349 }
350
351 {
352 const params = getBase({ stopTimestamp: 'toto' as any })
353 await addVideoInPlaylist(params)
354 }
355 })
356
357 it('Succeed with the correct params', async function () {
358 const params = getBase({}, { expectedStatus: 200 })
359 await addVideoInPlaylist(params)
360 })
361
362 it('Should fail if the video was already added in the playlist', async function () {
363 const params = getBase({}, { expectedStatus: 409 })
364 await addVideoInPlaylist(params)
365 })
366 })
367
368 describe('When updating an element in a playlist', function () {
369 const getBase = (elementAttrs: any = {}, wrapper: any = {}) => {
370 return Object.assign({
371 url: server.url,
372 token: server.accessToken,
373 elementAttrs: Object.assign({
374 startTimestamp: 1,
375 stopTimestamp: 2
376 }, elementAttrs),
377 videoId: videoId,
378 playlistId: playlistUUID,
379 expectedStatus: 400
380 }, wrapper)
381 }
382
383 it('Should fail with an unauthenticated user', async function () {
384 const params = getBase({}, { token: null, expectedStatus: 401 })
385 await updateVideoPlaylistElement(params)
386 })
387
388 it('Should fail with the playlist of another user', async function () {
389 const params = getBase({}, { token: userAccessToken, expectedStatus: 403 })
390 await updateVideoPlaylistElement(params)
391 })
392
393 it('Should fail with an unknown or incorrect playlist id', async function () {
394 {
395 const params = getBase({}, { playlistId: 'toto' })
396 await updateVideoPlaylistElement(params)
397 }
398
399 {
400 const params = getBase({}, { playlistId: 42, expectedStatus: 404 })
401 await updateVideoPlaylistElement(params)
402 }
403 })
404
405 it('Should fail with an unknown or incorrect video id', async function () {
406 {
407 const params = getBase({}, { videoId: 'toto' })
408 await updateVideoPlaylistElement(params)
409 }
410
411 {
412 const params = getBase({}, { videoId: 42, expectedStatus: 404 })
413 await updateVideoPlaylistElement(params)
414 }
415 })
416
417 it('Should fail with a bad start/stop timestamp', async function () {
418 {
419 const params = getBase({ startTimestamp: 'toto' as any })
420 await updateVideoPlaylistElement(params)
421 }
422
423 {
424 const params = getBase({ stopTimestamp: -42 })
425 await updateVideoPlaylistElement(params)
426 }
427 })
428
429 it('Should fail with an unknown element', async function () {
430 const params = getBase({}, { videoId: videoId2, expectedStatus: 404 })
431 await updateVideoPlaylistElement(params)
432 })
433
434 it('Succeed with the correct params', async function () {
435 const params = getBase({}, { expectedStatus: 204 })
436 await updateVideoPlaylistElement(params)
437 })
438 })
439
440 describe('When reordering elements of a playlist', function () {
441 let videoId3: number
442 let videoId4: number
443
444 const getBase = (elementAttrs: any = {}, wrapper: any = {}) => {
445 return Object.assign({
446 url: server.url,
447 token: server.accessToken,
448 playlistId: playlistUUID,
449 elementAttrs: Object.assign({
450 startPosition: 1,
451 insertAfterPosition: 2,
452 reorderLength: 3
453 }, elementAttrs),
454 expectedStatus: 400
455 }, wrapper)
456 }
457
458 before(async function () {
459 videoId3 = (await uploadVideoAndGetId({ server, videoName: 'video 3' })).id
460 videoId4 = (await uploadVideoAndGetId({ server, videoName: 'video 4' })).id
461
462 for (let id of [ videoId3, videoId4 ]) {
463 await addVideoInPlaylist({
464 url: server.url,
465 token: server.accessToken,
466 playlistId: playlistUUID,
467 elementAttrs: { videoId: id }
468 })
469 }
470 })
471
472 it('Should fail with an unauthenticated user', async function () {
473 const params = getBase({}, { token: null, expectedStatus: 401 })
474 await reorderVideosPlaylist(params)
475 })
476
477 it('Should fail with the playlist of another user', async function () {
478 const params = getBase({}, { token: userAccessToken, expectedStatus: 403 })
479 await reorderVideosPlaylist(params)
480 })
481
482 it('Should fail with an invalid playlist', async function () {
483 {
484 const params = getBase({}, { playlistId: 'toto' })
485 await reorderVideosPlaylist(params)
486 }
487
488 {
489 const params = getBase({}, { playlistId: 42, expectedStatus: 404 })
490 await reorderVideosPlaylist(params)
491 }
492 })
493
494 it('Should fail with an invalid start position', async function () {
495 {
496 const params = getBase({ startPosition: -1 })
497 await reorderVideosPlaylist(params)
498 }
499
500 {
501 const params = getBase({ startPosition: 'toto' as any })
502 await reorderVideosPlaylist(params)
503 }
504
505 {
506 const params = getBase({ startPosition: 42 })
507 await reorderVideosPlaylist(params)
508 }
509 })
510
511 it('Should fail with an invalid insert after position', async function () {
512 {
513 const params = getBase({ insertAfterPosition: 'toto' as any })
514 await reorderVideosPlaylist(params)
515 }
516
517 {
518 const params = getBase({ insertAfterPosition: -2 })
519 await reorderVideosPlaylist(params)
520 }
521
522 {
523 const params = getBase({ insertAfterPosition: 42 })
524 await reorderVideosPlaylist(params)
525 }
526 })
527
528 it('Should fail with an invalid reorder length', async function () {
529 {
530 const params = getBase({ reorderLength: 'toto' as any })
531 await reorderVideosPlaylist(params)
532 }
533
534 {
535 const params = getBase({ reorderLength: -2 })
536 await reorderVideosPlaylist(params)
537 }
538
539 {
540 const params = getBase({ reorderLength: 42 })
541 await reorderVideosPlaylist(params)
542 }
543 })
544
545 it('Succeed with the correct params', async function () {
546 const params = getBase({}, { expectedStatus: 204 })
547 await reorderVideosPlaylist(params)
548 })
549 })
550
551 describe('When checking exists in playlist endpoint', function () {
552 const path = '/api/v1/users/me/video-playlists/videos-exist'
553
554 it('Should fail with an unauthenticated user', async function () {
555 await makeGetRequest({
556 url: server.url,
557 path,
558 query: { videoIds: [ 1, 2 ] },
559 statusCodeExpected: 401
560 })
561 })
562
563 it('Should fail with invalid video ids', async function () {
564 await makeGetRequest({
565 url: server.url,
566 token: server.accessToken,
567 path,
568 query: { videoIds: 'toto' }
569 })
570
571 await makeGetRequest({
572 url: server.url,
573 token: server.accessToken,
574 path,
575 query: { videoIds: [ 'toto' ] }
576 })
577
578 await makeGetRequest({
579 url: server.url,
580 token: server.accessToken,
581 path,
582 query: { videoIds: [ 1, 'toto' ] }
583 })
584 })
585
586 it('Should succeed with the correct params', async function () {
587 await makeGetRequest({
588 url: server.url,
589 token: server.accessToken,
590 path,
591 query: { videoIds: [ 1, 2 ] },
592 statusCodeExpected: 200
593 })
594 })
595 })
596
597 describe('When deleting an element in a playlist', function () {
598 const getBase = (wrapper: any = {}) => {
599 return Object.assign({
600 url: server.url,
601 token: server.accessToken,
602 videoId: videoId,
603 playlistId: playlistUUID,
604 expectedStatus: 400
605 }, wrapper)
606 }
607
608 it('Should fail with an unauthenticated user', async function () {
609 const params = getBase({ token: null, expectedStatus: 401 })
610 await removeVideoFromPlaylist(params)
611 })
612
613 it('Should fail with the playlist of another user', async function () {
614 const params = getBase({ token: userAccessToken, expectedStatus: 403 })
615 await removeVideoFromPlaylist(params)
616 })
617
618 it('Should fail with an unknown or incorrect playlist id', async function () {
619 {
620 const params = getBase({ playlistId: 'toto' })
621 await removeVideoFromPlaylist(params)
622 }
623
624 {
625 const params = getBase({ playlistId: 42, expectedStatus: 404 })
626 await removeVideoFromPlaylist(params)
627 }
628 })
629
630 it('Should fail with an unknown or incorrect video id', async function () {
631 {
632 const params = getBase({ videoId: 'toto' })
633 await removeVideoFromPlaylist(params)
634 }
635
636 {
637 const params = getBase({ videoId: 42, expectedStatus: 404 })
638 await removeVideoFromPlaylist(params)
639 }
640 })
641
642 it('Should fail with an unknown element', async function () {
643 const params = getBase({ videoId: videoId2, expectedStatus: 404 })
644 await removeVideoFromPlaylist(params)
645 })
646
647 it('Succeed with the correct params', async function () {
648 const params = getBase({ expectedStatus: 204 })
649 await removeVideoFromPlaylist(params)
650 })
651 })
652
653 describe('When deleting a playlist', function () {
654 it('Should fail with an unknown playlist', async function () {
655 await deleteVideoPlaylist(server.url, server.accessToken, 42, 404)
656 })
657
658 it('Should fail with a playlist of another user', async function () {
659 await deleteVideoPlaylist(server.url, userAccessToken, playlistUUID, 403)
660 })
661
662 it('Should fail with the watch later playlist', async function () {
663 await deleteVideoPlaylist(server.url, server.accessToken, watchLaterPlaylistId, 400)
664 })
665
666 it('Should succeed with the correct params', async function () {
667 await deleteVideoPlaylist(server.url, server.accessToken, playlistUUID)
668 })
669 })
670
671 after(async function () {
672 await cleanupTests([ server ])
673 })
674})
diff --git a/server/tests/api/check-params/videos-filter.ts b/server/tests/api/check-params/videos-filter.ts
index e998c8a3d..babef8223 100644
--- a/server/tests/api/check-params/videos-filter.ts
+++ b/server/tests/api/check-params/videos-filter.ts
@@ -1,27 +1,27 @@
1/* tslint:disable:no-unused-expression */ 1/* tslint:disable:no-unused-expression */
2 2
3import * as chai from 'chai'
4import 'mocha' 3import 'mocha'
5import { 4import {
5 cleanupTests,
6 createUser, 6 createUser,
7 flushTests, 7 createVideoPlaylist,
8 killallServers, 8 flushAndRunServer,
9 makeGetRequest, 9 makeGetRequest,
10 runServer,
11 ServerInfo, 10 ServerInfo,
12 setAccessTokensToServers, 11 setAccessTokensToServers,
12 setDefaultVideoChannel,
13 userLogin 13 userLogin
14} from '../../../../shared/utils' 14} from '../../../../shared/extra-utils'
15import { UserRole } from '../../../../shared/models/users' 15import { UserRole } from '../../../../shared/models/users'
16import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
16 17
17const expect = chai.expect 18async function testEndpoints (server: ServerInfo, token: string, filter: string, playlistUUID: string, statusCodeExpected: number) {
18
19async function testEndpoints (server: ServerInfo, token: string, filter: string, statusCodeExpected: number) {
20 const paths = [ 19 const paths = [
21 '/api/v1/video-channels/root_channel/videos', 20 '/api/v1/video-channels/root_channel/videos',
22 '/api/v1/accounts/root/videos', 21 '/api/v1/accounts/root/videos',
23 '/api/v1/videos', 22 '/api/v1/videos',
24 '/api/v1/search/videos' 23 '/api/v1/search/videos',
24 '/api/v1/video-playlists/' + playlistUUID + '/videos'
25 ] 25 ]
26 26
27 for (const path of paths) { 27 for (const path of paths) {
@@ -41,55 +41,68 @@ describe('Test videos filters', function () {
41 let server: ServerInfo 41 let server: ServerInfo
42 let userAccessToken: string 42 let userAccessToken: string
43 let moderatorAccessToken: string 43 let moderatorAccessToken: string
44 let playlistUUID: string
44 45
45 // --------------------------------------------------------------- 46 // ---------------------------------------------------------------
46 47
47 before(async function () { 48 before(async function () {
48 this.timeout(30000) 49 this.timeout(30000)
49 50
50 await flushTests() 51 server = await flushAndRunServer(1)
51
52 server = await runServer(1)
53 52
54 await setAccessTokensToServers([ server ]) 53 await setAccessTokensToServers([ server ])
54 await setDefaultVideoChannel([ server ])
55 55
56 const user = { username: 'user1', password: 'my super password' } 56 const user = { username: 'user1', password: 'my super password' }
57 await createUser(server.url, server.accessToken, user.username, user.password) 57 await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
58 userAccessToken = await userLogin(server, user) 58 userAccessToken = await userLogin(server, user)
59 59
60 const moderator = { username: 'moderator', password: 'my super password' } 60 const moderator = { username: 'moderator', password: 'my super password' }
61 await createUser( 61 await createUser(
62 server.url, 62 {
63 server.accessToken, 63 url: server.url,
64 moderator.username, 64 accessToken: server.accessToken,
65 moderator.password, 65 username: moderator.username,
66 undefined, 66 password: moderator.password,
67 undefined, 67 videoQuota: undefined,
68 UserRole.MODERATOR 68 videoQuotaDaily: undefined,
69 role: UserRole.MODERATOR
70 }
69 ) 71 )
70 moderatorAccessToken = await userLogin(server, moderator) 72 moderatorAccessToken = await userLogin(server, moderator)
73
74 const res = await createVideoPlaylist({
75 url: server.url,
76 token: server.accessToken,
77 playlistAttrs: {
78 displayName: 'super playlist',
79 privacy: VideoPlaylistPrivacy.PUBLIC,
80 videoChannelId: server.videoChannel.id
81 }
82 })
83 playlistUUID = res.body.videoPlaylist.uuid
71 }) 84 })
72 85
73 describe('When setting a video filter', function () { 86 describe('When setting a video filter', function () {
74 87
75 it('Should fail with a bad filter', async function () { 88 it('Should fail with a bad filter', async function () {
76 await testEndpoints(server, server.accessToken, 'bad-filter', 400) 89 await testEndpoints(server, server.accessToken, 'bad-filter', playlistUUID, 400)
77 }) 90 })
78 91
79 it('Should succeed with a good filter', async function () { 92 it('Should succeed with a good filter', async function () {
80 await testEndpoints(server, server.accessToken,'local', 200) 93 await testEndpoints(server, server.accessToken,'local', playlistUUID, 200)
81 }) 94 })
82 95
83 it('Should fail to list all-local with a simple user', async function () { 96 it('Should fail to list all-local with a simple user', async function () {
84 await testEndpoints(server, userAccessToken, 'all-local', 401) 97 await testEndpoints(server, userAccessToken, 'all-local', playlistUUID, 401)
85 }) 98 })
86 99
87 it('Should succeed to list all-local with a moderator', async function () { 100 it('Should succeed to list all-local with a moderator', async function () {
88 await testEndpoints(server, moderatorAccessToken, 'all-local', 200) 101 await testEndpoints(server, moderatorAccessToken, 'all-local', playlistUUID, 200)
89 }) 102 })
90 103
91 it('Should succeed to list all-local with an admin', async function () { 104 it('Should succeed to list all-local with an admin', async function () {
92 await testEndpoints(server, server.accessToken, 'all-local', 200) 105 await testEndpoints(server, server.accessToken, 'all-local', playlistUUID, 200)
93 }) 106 })
94 107
95 // Because we cannot authenticate the user on the RSS endpoint 108 // Because we cannot authenticate the user on the RSS endpoint
@@ -104,7 +117,7 @@ describe('Test videos filters', function () {
104 }) 117 })
105 }) 118 })
106 119
107 it('Should succed on the feeds endpoint with the local filter', async function () { 120 it('Should succeed on the feeds endpoint with the local filter', async function () {
108 await makeGetRequest({ 121 await makeGetRequest({
109 url: server.url, 122 url: server.url,
110 path: '/feeds/videos.json', 123 path: '/feeds/videos.json',
@@ -117,11 +130,6 @@ describe('Test videos filters', function () {
117 }) 130 })
118 131
119 after(async function () { 132 after(async function () {
120 killallServers([ server ]) 133 await cleanupTests([ server ])
121
122 // Keep the logs if the test failed
123 if (this['ok']) {
124 await flushTests()
125 }
126 }) 134 })
127}) 135})
diff --git a/server/tests/api/check-params/videos-history.ts b/server/tests/api/check-params/videos-history.ts
index 8c079a956..3739e3fad 100644
--- a/server/tests/api/check-params/videos-history.ts
+++ b/server/tests/api/check-params/videos-history.ts
@@ -5,16 +5,15 @@ import 'mocha'
5import { 5import {
6 checkBadCountPagination, 6 checkBadCountPagination,
7 checkBadStartPagination, 7 checkBadStartPagination,
8 flushTests, 8 cleanupTests,
9 killallServers, 9 flushAndRunServer,
10 makeGetRequest, 10 makeGetRequest,
11 makePostBodyRequest, 11 makePostBodyRequest,
12 makePutBodyRequest, 12 makePutBodyRequest,
13 runServer,
14 ServerInfo, 13 ServerInfo,
15 setAccessTokensToServers, 14 setAccessTokensToServers,
16 uploadVideo 15 uploadVideo
17} from '../../../../shared/utils' 16} from '../../../../shared/extra-utils'
18 17
19const expect = chai.expect 18const expect = chai.expect
20 19
@@ -29,9 +28,7 @@ describe('Test videos history API validator', function () {
29 before(async function () { 28 before(async function () {
30 this.timeout(30000) 29 this.timeout(30000)
31 30
32 await flushTests() 31 server = await flushAndRunServer(1)
33
34 server = await runServer(1)
35 32
36 await setAccessTokensToServers([ server ]) 33 await setAccessTokensToServers([ server ])
37 34
@@ -129,11 +126,6 @@ describe('Test videos history API validator', function () {
129 }) 126 })
130 127
131 after(async function () { 128 after(async function () {
132 killallServers([ server ]) 129 await cleanupTests([ server ])
133
134 // Keep the logs if the test failed
135 if (this['ok']) {
136 await flushTests()
137 }
138 }) 130 })
139}) 131})
diff --git a/server/tests/api/check-params/videos.ts b/server/tests/api/check-params/videos.ts
index f26b91435..51e592a15 100644
--- a/server/tests/api/check-params/videos.ts
+++ b/server/tests/api/check-params/videos.ts
@@ -6,15 +6,28 @@ import 'mocha'
6import { join } from 'path' 6import { join } from 'path'
7import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum' 7import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum'
8import { 8import {
9 createUser, flushTests, getMyUserInformation, getVideo, getVideosList, immutableAssign, killallServers, makeDeleteRequest, 9 cleanupTests,
10 makeGetRequest, makeUploadRequest, makePutBodyRequest, removeVideo, runServer, ServerInfo, setAccessTokensToServers, userLogin 10 createUser,
11} from '../../../../shared/utils' 11 flushAndRunServer,
12 getMyUserInformation,
13 getVideo,
14 getVideosList,
15 immutableAssign,
16 makeDeleteRequest,
17 makeGetRequest,
18 makePutBodyRequest,
19 makeUploadRequest,
20 removeVideo,
21 ServerInfo,
22 setAccessTokensToServers,
23 userLogin,
24 root
25} from '../../../../shared/extra-utils'
12import { 26import {
13 checkBadCountPagination, 27 checkBadCountPagination,
14 checkBadSortPagination, 28 checkBadSortPagination,
15 checkBadStartPagination 29 checkBadStartPagination
16} from '../../../../shared/utils/requests/check-api-params' 30} from '../../../../shared/extra-utils/requests/check-api-params'
17import { getAccountsList } from '../../../../shared/utils/users/accounts'
18 31
19const expect = chai.expect 32const expect = chai.expect
20 33
@@ -32,15 +45,13 @@ describe('Test videos API validator', function () {
32 before(async function () { 45 before(async function () {
33 this.timeout(30000) 46 this.timeout(30000)
34 47
35 await flushTests() 48 server = await flushAndRunServer(1)
36
37 server = await runServer(1)
38 49
39 await setAccessTokensToServers([ server ]) 50 await setAccessTokensToServers([ server ])
40 51
41 const username = 'user1' 52 const username = 'user1'
42 const password = 'my super password' 53 const password = 'my super password'
43 await createUser(server.url, server.accessToken, username, password) 54 await createUser({ url: server.url, accessToken: server.accessToken, username: username, password: password })
44 userAccessToken = await userLogin(server, { username, password }) 55 userAccessToken = await userLogin(server, { username, password })
45 56
46 { 57 {
@@ -167,7 +178,7 @@ describe('Test videos API validator', function () {
167 describe('When adding a video', function () { 178 describe('When adding a video', function () {
168 let baseCorrectParams 179 let baseCorrectParams
169 const baseCorrectAttaches = { 180 const baseCorrectAttaches = {
170 'videofile': join(__dirname, '..', '..', 'fixtures', 'video_short.webm') 181 'videofile': join(root(), 'server', 'tests', 'fixtures', 'video_short.webm')
171 } 182 }
172 183
173 before(function () { 184 before(function () {
@@ -179,12 +190,14 @@ describe('Test videos API validator', function () {
179 language: 'pt', 190 language: 'pt',
180 nsfw: false, 191 nsfw: false,
181 commentsEnabled: true, 192 commentsEnabled: true,
193 downloadEnabled: true,
182 waitTranscoding: true, 194 waitTranscoding: true,
183 description: 'my super description', 195 description: 'my super description',
184 support: 'my super support text', 196 support: 'my super support text',
185 tags: [ 'tag1', 'tag2' ], 197 tags: [ 'tag1', 'tag2' ],
186 privacy: VideoPrivacy.PUBLIC, 198 privacy: VideoPrivacy.PUBLIC,
187 channelId: channelId 199 channelId: channelId,
200 originallyPublishedAt: new Date().toISOString()
188 } 201 }
189 }) 202 })
190 203
@@ -262,7 +275,7 @@ describe('Test videos API validator', function () {
262 username: 'fake', 275 username: 'fake',
263 password: 'fake_password' 276 password: 'fake_password'
264 } 277 }
265 await createUser(server.url, server.accessToken, user.username, user.password) 278 await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
266 279
267 const accessTokenUser = await userLogin(server, user) 280 const accessTokenUser = await userLogin(server, user)
268 const res = await getMyUserInformation(server.url, accessTokenUser) 281 const res = await getMyUserInformation(server.url, accessTokenUser)
@@ -312,21 +325,28 @@ describe('Test videos API validator', function () {
312 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) 325 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
313 }) 326 })
314 327
328 it('Should fail with a bad originally published at attribute', async function () {
329 const fields = immutableAssign(baseCorrectParams, { 'originallyPublishedAt': 'toto' })
330 const attaches = baseCorrectAttaches
331
332 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
333 })
334
315 it('Should fail without an input file', async function () { 335 it('Should fail without an input file', async function () {
316 const fields = baseCorrectParams 336 const fields = baseCorrectParams
317 const attaches = {} 337 const attaches = {}
318 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) 338 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
319 }) 339 })
320 340
321 it('Should fail without an incorrect input file', async function () { 341 it('Should fail with an incorrect input file', async function () {
322 const fields = baseCorrectParams 342 const fields = baseCorrectParams
323 let attaches = { 343 let attaches = {
324 'videofile': join(__dirname, '..', '..', 'fixtures', 'video_short_fake.webm') 344 'videofile': join(root(), 'server', 'tests', 'fixtures', 'video_short_fake.webm')
325 } 345 }
326 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) 346 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
327 347
328 attaches = { 348 attaches = {
329 'videofile': join(__dirname, '..', '..', 'fixtures', 'video_short.mkv') 349 'videofile': join(root(), 'server', 'tests', 'fixtures', 'video_short.mkv')
330 } 350 }
331 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) 351 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
332 }) 352 })
@@ -334,8 +354,8 @@ describe('Test videos API validator', function () {
334 it('Should fail with an incorrect thumbnail file', async function () { 354 it('Should fail with an incorrect thumbnail file', async function () {
335 const fields = baseCorrectParams 355 const fields = baseCorrectParams
336 const attaches = { 356 const attaches = {
337 'thumbnailfile': join(__dirname, '..', '..', 'fixtures', 'avatar.png'), 357 'thumbnailfile': join(root(), 'server', 'tests', 'fixtures', 'avatar.png'),
338 'videofile': join(__dirname, '..', '..', 'fixtures', 'video_short.mp4') 358 'videofile': join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4')
339 } 359 }
340 360
341 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) 361 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
@@ -344,8 +364,8 @@ describe('Test videos API validator', function () {
344 it('Should fail with a big thumbnail file', async function () { 364 it('Should fail with a big thumbnail file', async function () {
345 const fields = baseCorrectParams 365 const fields = baseCorrectParams
346 const attaches = { 366 const attaches = {
347 'thumbnailfile': join(__dirname, '..', '..', 'fixtures', 'avatar-big.png'), 367 'thumbnailfile': join(root(), 'server', 'tests', 'fixtures', 'avatar-big.png'),
348 'videofile': join(__dirname, '..', '..', 'fixtures', 'video_short.mp4') 368 'videofile': join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4')
349 } 369 }
350 370
351 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) 371 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
@@ -354,8 +374,8 @@ describe('Test videos API validator', function () {
354 it('Should fail with an incorrect preview file', async function () { 374 it('Should fail with an incorrect preview file', async function () {
355 const fields = baseCorrectParams 375 const fields = baseCorrectParams
356 const attaches = { 376 const attaches = {
357 'previewfile': join(__dirname, '..', '..', 'fixtures', 'avatar.png'), 377 'previewfile': join(root(), 'server', 'tests', 'fixtures', 'avatar.png'),
358 'videofile': join(__dirname, '..', '..', 'fixtures', 'video_short.mp4') 378 'videofile': join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4')
359 } 379 }
360 380
361 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) 381 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
@@ -364,8 +384,8 @@ describe('Test videos API validator', function () {
364 it('Should fail with a big preview file', async function () { 384 it('Should fail with a big preview file', async function () {
365 const fields = baseCorrectParams 385 const fields = baseCorrectParams
366 const attaches = { 386 const attaches = {
367 'previewfile': join(__dirname, '..', '..', 'fixtures', 'avatar-big.png'), 387 'previewfile': join(root(), 'server', 'tests', 'fixtures', 'avatar-big.png'),
368 'videofile': join(__dirname, '..', '..', 'fixtures', 'video_short.mp4') 388 'videofile': join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4')
369 } 389 }
370 390
371 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) 391 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
@@ -390,7 +410,7 @@ describe('Test videos API validator', function () {
390 410
391 { 411 {
392 const attaches = immutableAssign(baseCorrectAttaches, { 412 const attaches = immutableAssign(baseCorrectAttaches, {
393 videofile: join(__dirname, '..', '..', 'fixtures', 'video_short.mp4') 413 videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4')
394 }) 414 })
395 415
396 await makeUploadRequest({ 416 await makeUploadRequest({
@@ -405,7 +425,7 @@ describe('Test videos API validator', function () {
405 425
406 { 426 {
407 const attaches = immutableAssign(baseCorrectAttaches, { 427 const attaches = immutableAssign(baseCorrectAttaches, {
408 videofile: join(__dirname, '..', '..', 'fixtures', 'video_short.ogv') 428 videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short.ogv')
409 }) 429 })
410 430
411 await makeUploadRequest({ 431 await makeUploadRequest({
@@ -428,6 +448,7 @@ describe('Test videos API validator', function () {
428 language: 'pt', 448 language: 'pt',
429 nsfw: false, 449 nsfw: false,
430 commentsEnabled: false, 450 commentsEnabled: false,
451 downloadEnabled: false,
431 description: 'my super description', 452 description: 'my super description',
432 privacy: VideoPrivacy.PUBLIC, 453 privacy: VideoPrivacy.PUBLIC,
433 tags: [ 'tag1', 'tag2' ] 454 tags: [ 'tag1', 'tag2' ]
@@ -532,10 +553,16 @@ describe('Test videos API validator', function () {
532 await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields }) 553 await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields })
533 }) 554 })
534 555
556 it('Should fail with a bad originally published at param', async function () {
557 const fields = immutableAssign(baseCorrectParams, { originallyPublishedAt: 'toto' })
558
559 await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields })
560 })
561
535 it('Should fail with an incorrect thumbnail file', async function () { 562 it('Should fail with an incorrect thumbnail file', async function () {
536 const fields = baseCorrectParams 563 const fields = baseCorrectParams
537 const attaches = { 564 const attaches = {
538 'thumbnailfile': join(__dirname, '..', '..', 'fixtures', 'avatar.png') 565 'thumbnailfile': join(root(), 'server', 'tests', 'fixtures', 'avatar.png')
539 } 566 }
540 567
541 await makeUploadRequest({ 568 await makeUploadRequest({
@@ -551,7 +578,7 @@ describe('Test videos API validator', function () {
551 it('Should fail with a big thumbnail file', async function () { 578 it('Should fail with a big thumbnail file', async function () {
552 const fields = baseCorrectParams 579 const fields = baseCorrectParams
553 const attaches = { 580 const attaches = {
554 'thumbnailfile': join(__dirname, '..', '..', 'fixtures', 'avatar-big.png') 581 'thumbnailfile': join(root(), 'server', 'tests', 'fixtures', 'avatar-big.png')
555 } 582 }
556 583
557 await makeUploadRequest({ 584 await makeUploadRequest({
@@ -567,7 +594,7 @@ describe('Test videos API validator', function () {
567 it('Should fail with an incorrect preview file', async function () { 594 it('Should fail with an incorrect preview file', async function () {
568 const fields = baseCorrectParams 595 const fields = baseCorrectParams
569 const attaches = { 596 const attaches = {
570 'previewfile': join(__dirname, '..', '..', 'fixtures', 'avatar.png') 597 'previewfile': join(root(), 'server', 'tests', 'fixtures', 'avatar.png')
571 } 598 }
572 599
573 await makeUploadRequest({ 600 await makeUploadRequest({
@@ -583,7 +610,7 @@ describe('Test videos API validator', function () {
583 it('Should fail with a big preview file', async function () { 610 it('Should fail with a big preview file', async function () {
584 const fields = baseCorrectParams 611 const fields = baseCorrectParams
585 const attaches = { 612 const attaches = {
586 'previewfile': join(__dirname, '..', '..', 'fixtures', 'avatar-big.png') 613 'previewfile': join(root(), 'server', 'tests', 'fixtures', 'avatar-big.png')
587 } 614 }
588 615
589 await makeUploadRequest({ 616 await makeUploadRequest({
@@ -714,11 +741,6 @@ describe('Test videos API validator', function () {
714 }) 741 })
715 742
716 after(async function () { 743 after(async function () {
717 killallServers([ server ]) 744 await cleanupTests([ server ])
718
719 // Keep the logs if the test failed
720 if (this['ok']) {
721 await flushTests()
722 }
723 }) 745 })
724}) 746})
diff --git a/server/tests/api/index-1.ts b/server/tests/api/index-1.ts
index 80d752f42..75cdd9025 100644
--- a/server/tests/api/index-1.ts
+++ b/server/tests/api/index-1.ts
@@ -1,2 +1,3 @@
1import './check-params' 1import './check-params'
2import './notifications'
2import './search' 3import './search'
diff --git a/server/tests/api/notifications/index.ts b/server/tests/api/notifications/index.ts
new file mode 100644
index 000000000..95ac8fc51
--- /dev/null
+++ b/server/tests/api/notifications/index.ts
@@ -0,0 +1 @@
export * from './user-notifications'
diff --git a/server/tests/api/users/user-notifications.ts b/server/tests/api/notifications/user-notifications.ts
index 72b6a0aa2..f479e1785 100644
--- a/server/tests/api/users/user-notifications.ts
+++ b/server/tests/api/notifications/user-notifications.ts
@@ -4,25 +4,30 @@ import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { 5import {
6 addVideoToBlacklist, 6 addVideoToBlacklist,
7 cleanupTests,
7 createUser, 8 createUser,
8 doubleFollow, 9 doubleFollow,
9 flushAndRunMultipleServers, 10 flushAndRunMultipleServers,
10 flushTests, 11 follow,
12 getCustomConfig,
11 getMyUserInformation, 13 getMyUserInformation,
14 getVideoCommentThreads,
15 getVideoThreadComments,
12 immutableAssign, 16 immutableAssign,
13 registerUser, 17 registerUser,
14 removeVideoFromBlacklist, 18 removeVideoFromBlacklist,
15 reportVideoAbuse, 19 reportVideoAbuse,
20 updateCustomConfig,
16 updateMyUser, 21 updateMyUser,
17 updateVideo, 22 updateVideo,
18 updateVideoChannel, 23 updateVideoChannel,
19 userLogin, 24 userLogin,
20 wait 25 wait
21} from '../../../../shared/utils' 26} from '../../../../shared/extra-utils'
22import { killallServers, ServerInfo, uploadVideo } from '../../../../shared/utils/index' 27import { ServerInfo, uploadVideo } from '../../../../shared/extra-utils/index'
23import { setAccessTokensToServers } from '../../../../shared/utils/users/login' 28import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login'
24import { waitJobs } from '../../../../shared/utils/server/jobs' 29import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
25import { getUserNotificationSocket } from '../../../../shared/utils/socket/socket-io' 30import { getUserNotificationSocket } from '../../../../shared/extra-utils/socket/socket-io'
26import { 31import {
27 checkCommentMention, 32 checkCommentMention,
28 CheckerBaseParams, 33 CheckerBaseParams,
@@ -30,16 +35,18 @@ import {
30 checkNewActorFollow, 35 checkNewActorFollow,
31 checkNewBlacklistOnMyVideo, 36 checkNewBlacklistOnMyVideo,
32 checkNewCommentOnMyVideo, 37 checkNewCommentOnMyVideo,
38 checkNewInstanceFollower,
33 checkNewVideoAbuseForModerators, 39 checkNewVideoAbuseForModerators,
34 checkNewVideoFromSubscription, 40 checkNewVideoFromSubscription,
35 checkUserRegistered, 41 checkUserRegistered,
42 checkVideoAutoBlacklistForModerators,
36 checkVideoIsPublished, 43 checkVideoIsPublished,
37 getLastNotification, 44 getLastNotification,
38 getUserNotifications, 45 getUserNotifications,
46 markAsReadAllNotifications,
39 markAsReadNotifications, 47 markAsReadNotifications,
40 updateMyNotificationSettings, 48 updateMyNotificationSettings
41 markAsReadAllNotifications 49} from '../../../../shared/extra-utils/users/user-notifications'
42} from '../../../../shared/utils/users/user-notifications'
43import { 50import {
44 User, 51 User,
45 UserNotification, 52 UserNotification,
@@ -47,13 +54,15 @@ import {
47 UserNotificationSettingValue, 54 UserNotificationSettingValue,
48 UserNotificationType 55 UserNotificationType
49} from '../../../../shared/models/users' 56} from '../../../../shared/models/users'
50import { MockSmtpServer } from '../../../../shared/utils/miscs/email' 57import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email'
51import { addUserSubscription, removeUserSubscription } from '../../../../shared/utils/users/user-subscriptions' 58import { addUserSubscription, removeUserSubscription } from '../../../../shared/extra-utils/users/user-subscriptions'
52import { VideoPrivacy } from '../../../../shared/models/videos' 59import { VideoPrivacy } from '../../../../shared/models/videos'
53import { getBadVideoUrl, getYoutubeVideoUrl, importVideo } from '../../../../shared/utils/videos/video-imports' 60import { getBadVideoUrl, getYoutubeVideoUrl, importVideo } from '../../../../shared/extra-utils/videos/video-imports'
54import { addVideoCommentReply, addVideoCommentThread } from '../../../../shared/utils/videos/video-comments' 61import { addVideoCommentReply, addVideoCommentThread } from '../../../../shared/extra-utils/videos/video-comments'
55import * as uuidv4 from 'uuid/v4' 62import * as uuidv4 from 'uuid/v4'
56import { addAccountToAccountBlocklist, removeAccountFromAccountBlocklist } from '../../../../shared/utils/users/blocklist' 63import { addAccountToAccountBlocklist, removeAccountFromAccountBlocklist } from '../../../../shared/extra-utils/users/blocklist'
64import { CustomConfig } from '../../../../shared/models/server'
65import { VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model'
57 66
58const expect = chai.expect 67const expect = chai.expect
59 68
@@ -92,12 +101,14 @@ describe('Test users notifications', function () {
92 newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 101 newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
93 newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 102 newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
94 videoAbuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 103 videoAbuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
104 videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
95 blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 105 blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
96 myVideoImportFinished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 106 myVideoImportFinished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
97 myVideoPublished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 107 myVideoPublished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
98 commentMention: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 108 commentMention: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
99 newFollow: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 109 newFollow: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
100 newUserRegistration: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL 110 newUserRegistration: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
111 newInstanceFollower: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
101 } 112 }
102 113
103 before(async function () { 114 before(async function () {
@@ -105,14 +116,12 @@ describe('Test users notifications', function () {
105 116
106 await MockSmtpServer.Instance.collectEmails(emails) 117 await MockSmtpServer.Instance.collectEmails(emails)
107 118
108 await flushTests()
109
110 const overrideConfig = { 119 const overrideConfig = {
111 smtp: { 120 smtp: {
112 hostname: 'localhost' 121 hostname: 'localhost'
113 } 122 }
114 } 123 }
115 servers = await flushAndRunMultipleServers(2, overrideConfig) 124 servers = await flushAndRunMultipleServers(3, overrideConfig)
116 125
117 // Get the access tokens 126 // Get the access tokens
118 await setAccessTokensToServers(servers) 127 await setAccessTokensToServers(servers)
@@ -126,7 +135,13 @@ describe('Test users notifications', function () {
126 username: 'user_1', 135 username: 'user_1',
127 password: 'super password' 136 password: 'super password'
128 } 137 }
129 await createUser(servers[0].url, servers[0].accessToken, user.username, user.password, 10 * 1000 * 1000) 138 await createUser({
139 url: servers[ 0 ].url,
140 accessToken: servers[ 0 ].accessToken,
141 username: user.username,
142 password: user.password,
143 videoQuota: 10 * 1000 * 1000
144 })
130 userAccessToken = await userLogin(servers[0], user) 145 userAccessToken = await userLogin(servers[0], user)
131 146
132 await updateMyNotificationSettings(servers[0].url, userAccessToken, allNotificationSettings) 147 await updateMyNotificationSettings(servers[0].url, userAccessToken, allNotificationSettings)
@@ -165,6 +180,8 @@ describe('Test users notifications', function () {
165 }) 180 })
166 181
167 it('Should not send notifications if the user does not follow the video publisher', async function () { 182 it('Should not send notifications if the user does not follow the video publisher', async function () {
183 this.timeout(10000)
184
168 await uploadVideoByLocalAccount(servers) 185 await uploadVideoByLocalAccount(servers)
169 186
170 const notification = await getLastNotification(servers[ 0 ].url, userAccessToken) 187 const notification = await getLastNotification(servers[ 0 ].url, userAccessToken)
@@ -214,7 +231,7 @@ describe('Test users notifications', function () {
214 }) 231 })
215 232
216 it('Should send a new video notification on a remote scheduled publication', async function () { 233 it('Should send a new video notification on a remote scheduled publication', async function () {
217 this.timeout(20000) 234 this.timeout(50000)
218 235
219 // In 2 seconds 236 // In 2 seconds
220 let updateAt = new Date(new Date().getTime() + 2000) 237 let updateAt = new Date(new Date().getTime() + 2000)
@@ -236,7 +253,7 @@ describe('Test users notifications', function () {
236 it('Should not send a notification before the video is published', async function () { 253 it('Should not send a notification before the video is published', async function () {
237 this.timeout(20000) 254 this.timeout(20000)
238 255
239 let updateAt = new Date(new Date().getTime() + 100000) 256 let updateAt = new Date(new Date().getTime() + 1000000)
240 257
241 const data = { 258 const data = {
242 privacy: VideoPrivacy.PRIVATE, 259 privacy: VideoPrivacy.PRIVATE,
@@ -303,7 +320,7 @@ describe('Test users notifications', function () {
303 }) 320 })
304 321
305 it('Should send a new video notification after a video import', async function () { 322 it('Should send a new video notification after a video import', async function () {
306 this.timeout(30000) 323 this.timeout(100000)
307 324
308 const name = 'video import ' + uuidv4() 325 const name = 'video import ' + uuidv4()
309 326
@@ -398,10 +415,14 @@ describe('Test users notifications', function () {
398 415
399 await waitJobs(servers) 416 await waitJobs(servers)
400 417
401 const resComment = await addVideoCommentThread(servers[1].url, servers[1].accessToken, uuid, 'comment') 418 await addVideoCommentThread(servers[1].url, servers[1].accessToken, uuid, 'comment')
402 const commentId = resComment.body.comment.id
403 419
404 await waitJobs(servers) 420 await waitJobs(servers)
421
422 const resComment = await getVideoCommentThreads(servers[0].url, uuid, 0, 5)
423 expect(resComment.body.data).to.have.lengthOf(1)
424 const commentId = resComment.body.data[0].id
425
405 await checkNewCommentOnMyVideo(baseParams, uuid, commentId, commentId, 'presence') 426 await checkNewCommentOnMyVideo(baseParams, uuid, commentId, commentId, 'presence')
406 }) 427 })
407 428
@@ -428,13 +449,24 @@ describe('Test users notifications', function () {
428 const uuid = resVideo.body.video.uuid 449 const uuid = resVideo.body.video.uuid
429 await waitJobs(servers) 450 await waitJobs(servers)
430 451
431 const resThread = await addVideoCommentThread(servers[1].url, servers[1].accessToken, uuid, 'comment') 452 {
432 const threadId = resThread.body.comment.id 453 const resThread = await addVideoCommentThread(servers[ 1 ].url, servers[ 1 ].accessToken, uuid, 'comment')
433 454 const threadId = resThread.body.comment.id
434 const resComment = await addVideoCommentReply(servers[1].url, servers[1].accessToken, uuid, threadId, 'reply') 455 await addVideoCommentReply(servers[ 1 ].url, servers[ 1 ].accessToken, uuid, threadId, 'reply')
435 const commentId = resComment.body.comment.id 456 }
436 457
437 await waitJobs(servers) 458 await waitJobs(servers)
459
460 const resThread = await getVideoCommentThreads(servers[0].url, uuid, 0, 5)
461 expect(resThread.body.data).to.have.lengthOf(1)
462 const threadId = resThread.body.data[0].id
463
464 const resComments = await getVideoThreadComments(servers[0].url, uuid, threadId)
465 const tree = resComments.body as VideoCommentThreadTree
466
467 expect(tree.children).to.have.lengthOf(1)
468 const commentId = tree.children[0].comment.id
469
438 await checkNewCommentOnMyVideo(baseParams, uuid, commentId, threadId, 'presence') 470 await checkNewCommentOnMyVideo(baseParams, uuid, commentId, threadId, 'presence')
439 }) 471 })
440 }) 472 })
@@ -547,17 +579,27 @@ describe('Test users notifications', function () {
547 579
548 await waitJobs(servers) 580 await waitJobs(servers)
549 const resThread = await addVideoCommentThread(servers[1].url, servers[1].accessToken, uuid, 'hello @user_1@localhost:9001 1') 581 const resThread = await addVideoCommentThread(servers[1].url, servers[1].accessToken, uuid, 'hello @user_1@localhost:9001 1')
550 const threadId = resThread.body.comment.id 582 const server2ThreadId = resThread.body.comment.id
551 583
552 await waitJobs(servers) 584 await waitJobs(servers)
553 await checkCommentMention(baseParams, uuid, threadId, threadId, 'super root 2 name', 'presence') 585
586 const resThread2 = await getVideoCommentThreads(servers[0].url, uuid, 0, 5)
587 expect(resThread2.body.data).to.have.lengthOf(1)
588 const server1ThreadId = resThread2.body.data[0].id
589 await checkCommentMention(baseParams, uuid, server1ThreadId, server1ThreadId, 'super root 2 name', 'presence')
554 590
555 const text = '@user_1@localhost:9001 hello 2 @root@localhost:9001' 591 const text = '@user_1@localhost:9001 hello 2 @root@localhost:9001'
556 const resComment = await addVideoCommentReply(servers[1].url, servers[1].accessToken, uuid, threadId, text) 592 await addVideoCommentReply(servers[1].url, servers[1].accessToken, uuid, server2ThreadId, text)
557 const commentId = resComment.body.comment.id
558 593
559 await waitJobs(servers) 594 await waitJobs(servers)
560 await checkCommentMention(baseParams, uuid, commentId, threadId, 'super root 2 name', 'presence') 595
596 const resComments = await getVideoThreadComments(servers[0].url, uuid, server1ThreadId)
597 const tree = resComments.body as VideoCommentThreadTree
598
599 expect(tree.children).to.have.lengthOf(1)
600 const commentId = tree.children[0].comment.id
601
602 await checkCommentMention(baseParams, uuid, commentId, server1ThreadId, 'super root 2 name', 'presence')
561 }) 603 })
562 }) 604 })
563 605
@@ -658,6 +700,8 @@ describe('Test users notifications', function () {
658 }) 700 })
659 701
660 it('Should not send a notification if transcoding is not enabled', async function () { 702 it('Should not send a notification if transcoding is not enabled', async function () {
703 this.timeout(10000)
704
661 const { name, uuid } = await uploadVideoByLocalAccount(servers) 705 const { name, uuid } = await uploadVideoByLocalAccount(servers)
662 await waitJobs(servers) 706 await waitJobs(servers)
663 707
@@ -731,6 +775,24 @@ describe('Test users notifications', function () {
731 await wait(6000) 775 await wait(6000)
732 await checkVideoIsPublished(baseParams, name, uuid, 'presence') 776 await checkVideoIsPublished(baseParams, name, uuid, 'presence')
733 }) 777 })
778
779 it('Should not send a notification before the video is published', async function () {
780 this.timeout(20000)
781
782 let updateAt = new Date(new Date().getTime() + 100000)
783
784 const data = {
785 privacy: VideoPrivacy.PRIVATE,
786 scheduleUpdate: {
787 updateAt: updateAt.toISOString(),
788 privacy: VideoPrivacy.PUBLIC
789 }
790 }
791 const { name, uuid } = await uploadVideoByRemoteAccount(servers, data)
792
793 await wait(6000)
794 await checkVideoIsPublished(baseParams, name, uuid, 'absence')
795 })
734 }) 796 })
735 797
736 describe('My video is imported', function () { 798 describe('My video is imported', function () {
@@ -795,6 +857,8 @@ describe('Test users notifications', function () {
795 }) 857 })
796 858
797 it('Should send a notification only to moderators when a user registers on the instance', async function () { 859 it('Should send a notification only to moderators when a user registers on the instance', async function () {
860 this.timeout(10000)
861
798 await registerUser(servers[0].url, 'user_45', 'password') 862 await registerUser(servers[0].url, 'user_45', 'password')
799 863
800 await waitJobs(servers) 864 await waitJobs(servers)
@@ -806,6 +870,32 @@ describe('Test users notifications', function () {
806 }) 870 })
807 }) 871 })
808 872
873 describe('New instance follower', function () {
874 let baseParams: CheckerBaseParams
875
876 before(async () => {
877 baseParams = {
878 server: servers[0],
879 emails,
880 socketNotifications: adminNotifications,
881 token: servers[0].accessToken
882 }
883 })
884
885 it('Should send a notification only to admin when there is a new instance follower', async function () {
886 this.timeout(20000)
887
888 await follow(servers[2].url, [ servers[0].url ], servers[2].accessToken)
889
890 await waitJobs(servers)
891
892 await checkNewInstanceFollower(baseParams, 'localhost:9003', 'presence')
893
894 const userOverride = { socketNotifications: userNotifications, token: userAccessToken, check: { web: true, mail: false } }
895 await checkNewInstanceFollower(immutableAssign(baseParams, userOverride), 'localhost:9003', 'absence')
896 })
897 })
898
809 describe('New actor follow', function () { 899 describe('New actor follow', function () {
810 let baseParams: CheckerBaseParams 900 let baseParams: CheckerBaseParams
811 let myChannelName = 'super channel name' 901 let myChannelName = 'super channel name'
@@ -863,6 +953,8 @@ describe('Test users notifications', function () {
863 }) 953 })
864 954
865 it('Should notify when a local account is following one of our channel', async function () { 955 it('Should notify when a local account is following one of our channel', async function () {
956 this.timeout(10000)
957
866 await addUserSubscription(servers[0].url, servers[0].accessToken, 'user_1@localhost:9001') 958 await addUserSubscription(servers[0].url, servers[0].accessToken, 'user_1@localhost:9001')
867 959
868 await waitJobs(servers) 960 await waitJobs(servers)
@@ -871,6 +963,8 @@ describe('Test users notifications', function () {
871 }) 963 })
872 964
873 it('Should notify when a remote account is following one of our channel', async function () { 965 it('Should notify when a remote account is following one of our channel', async function () {
966 this.timeout(10000)
967
874 await addUserSubscription(servers[1].url, servers[1].accessToken, 'user_1@localhost:9001') 968 await addUserSubscription(servers[1].url, servers[1].accessToken, 'user_1@localhost:9001')
875 969
876 await waitJobs(servers) 970 await waitJobs(servers)
@@ -879,6 +973,180 @@ describe('Test users notifications', function () {
879 }) 973 })
880 }) 974 })
881 975
976 describe('Video-related notifications when video auto-blacklist is enabled', function () {
977 let userBaseParams: CheckerBaseParams
978 let adminBaseParamsServer1: CheckerBaseParams
979 let adminBaseParamsServer2: CheckerBaseParams
980 let videoUUID: string
981 let videoName: string
982 let currentCustomConfig: CustomConfig
983
984 before(async () => {
985
986 adminBaseParamsServer1 = {
987 server: servers[0],
988 emails,
989 socketNotifications: adminNotifications,
990 token: servers[0].accessToken
991 }
992
993 adminBaseParamsServer2 = {
994 server: servers[1],
995 emails,
996 socketNotifications: adminNotificationsServer2,
997 token: servers[1].accessToken
998 }
999
1000 userBaseParams = {
1001 server: servers[0],
1002 emails,
1003 socketNotifications: userNotifications,
1004 token: userAccessToken
1005 }
1006
1007 const resCustomConfig = await getCustomConfig(servers[0].url, servers[0].accessToken)
1008 currentCustomConfig = resCustomConfig.body
1009 const autoBlacklistTestsCustomConfig = immutableAssign(currentCustomConfig, {
1010 autoBlacklist: {
1011 videos: {
1012 ofUsers: {
1013 enabled: true
1014 }
1015 }
1016 }
1017 })
1018 // enable transcoding otherwise own publish notification after transcoding not expected
1019 autoBlacklistTestsCustomConfig.transcoding.enabled = true
1020 await updateCustomConfig(servers[0].url, servers[0].accessToken, autoBlacklistTestsCustomConfig)
1021
1022 await addUserSubscription(servers[0].url, servers[0].accessToken, 'user_1_channel@localhost:9001')
1023 await addUserSubscription(servers[1].url, servers[1].accessToken, 'user_1_channel@localhost:9001')
1024
1025 })
1026
1027 it('Should send notification to moderators on new video with auto-blacklist', async function () {
1028 this.timeout(20000)
1029
1030 videoName = 'video with auto-blacklist ' + uuidv4()
1031 const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: videoName })
1032 videoUUID = resVideo.body.video.uuid
1033
1034 await waitJobs(servers)
1035 await checkVideoAutoBlacklistForModerators(adminBaseParamsServer1, videoUUID, videoName, 'presence')
1036 })
1037
1038 it('Should not send video publish notification if auto-blacklisted', async function () {
1039 await checkVideoIsPublished(userBaseParams, videoName, videoUUID, 'absence')
1040 })
1041
1042 it('Should not send a local user subscription notification if auto-blacklisted', async function () {
1043 await checkNewVideoFromSubscription(adminBaseParamsServer1, videoName, videoUUID, 'absence')
1044 })
1045
1046 it('Should not send a remote user subscription notification if auto-blacklisted', async function () {
1047 await checkNewVideoFromSubscription(adminBaseParamsServer2, videoName, videoUUID, 'absence')
1048 })
1049
1050 it('Should send video published and unblacklist after video unblacklisted', async function () {
1051 this.timeout(20000)
1052
1053 await removeVideoFromBlacklist(servers[0].url, servers[0].accessToken, videoUUID)
1054
1055 await waitJobs(servers)
1056
1057 // FIXME: Can't test as two notifications sent to same user and util only checks last one
1058 // One notification might be better anyways
1059 // await checkNewBlacklistOnMyVideo(userBaseParams, videoUUID, videoName, 'unblacklist')
1060 // await checkVideoIsPublished(userBaseParams, videoName, videoUUID, 'presence')
1061 })
1062
1063 it('Should send a local user subscription notification after removed from blacklist', async function () {
1064 await checkNewVideoFromSubscription(adminBaseParamsServer1, videoName, videoUUID, 'presence')
1065 })
1066
1067 it('Should send a remote user subscription notification after removed from blacklist', async function () {
1068 await checkNewVideoFromSubscription(adminBaseParamsServer2, videoName, videoUUID, 'presence')
1069 })
1070
1071 it('Should send unblacklist but not published/subscription notes after unblacklisted if scheduled update pending', async function () {
1072 this.timeout(20000)
1073
1074 let updateAt = new Date(new Date().getTime() + 100000)
1075
1076 const name = 'video with auto-blacklist and future schedule ' + uuidv4()
1077
1078 const data = {
1079 name,
1080 privacy: VideoPrivacy.PRIVATE,
1081 scheduleUpdate: {
1082 updateAt: updateAt.toISOString(),
1083 privacy: VideoPrivacy.PUBLIC
1084 }
1085 }
1086
1087 const resVideo = await uploadVideo(servers[0].url, userAccessToken, data)
1088 const uuid = resVideo.body.video.uuid
1089
1090 await removeVideoFromBlacklist(servers[0].url, servers[0].accessToken, uuid)
1091
1092 await waitJobs(servers)
1093 await checkNewBlacklistOnMyVideo(userBaseParams, uuid, name, 'unblacklist')
1094
1095 // FIXME: Can't test absence as two notifications sent to same user and util only checks last one
1096 // One notification might be better anyways
1097 // await checkVideoIsPublished(userBaseParams, name, uuid, 'absence')
1098
1099 await checkNewVideoFromSubscription(adminBaseParamsServer1, name, uuid, 'absence')
1100 await checkNewVideoFromSubscription(adminBaseParamsServer2, name, uuid, 'absence')
1101 })
1102
1103 it('Should not send publish/subscription notifications after scheduled update if video still auto-blacklisted', async function () {
1104 this.timeout(20000)
1105
1106 // In 2 seconds
1107 let updateAt = new Date(new Date().getTime() + 2000)
1108
1109 const name = 'video with schedule done and still auto-blacklisted ' + uuidv4()
1110
1111 const data = {
1112 name,
1113 privacy: VideoPrivacy.PRIVATE,
1114 scheduleUpdate: {
1115 updateAt: updateAt.toISOString(),
1116 privacy: VideoPrivacy.PUBLIC
1117 }
1118 }
1119
1120 const resVideo = await uploadVideo(servers[0].url, userAccessToken, data)
1121 const uuid = resVideo.body.video.uuid
1122
1123 await wait(6000)
1124 await checkVideoIsPublished(userBaseParams, name, uuid, 'absence')
1125 await checkNewVideoFromSubscription(adminBaseParamsServer1, name, uuid, 'absence')
1126 await checkNewVideoFromSubscription(adminBaseParamsServer2, name, uuid, 'absence')
1127 })
1128
1129 it('Should not send a notification to moderators on new video without auto-blacklist', async function () {
1130 this.timeout(20000)
1131
1132 const name = 'video without auto-blacklist ' + uuidv4()
1133
1134 // admin with blacklist right will not be auto-blacklisted
1135 const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, { name })
1136 const uuid = resVideo.body.video.uuid
1137
1138 await waitJobs(servers)
1139 await checkVideoAutoBlacklistForModerators(adminBaseParamsServer1, uuid, name, 'absence')
1140 })
1141
1142 after(async () => {
1143 await updateCustomConfig(servers[0].url, servers[0].accessToken, currentCustomConfig)
1144
1145 await removeUserSubscription(servers[0].url, servers[0].accessToken, 'user_1_channel@localhost:9001')
1146 await removeUserSubscription(servers[1].url, servers[1].accessToken, 'user_1_channel@localhost:9001')
1147 })
1148 })
1149
882 describe('Mark as read', function () { 1150 describe('Mark as read', function () {
883 it('Should mark as read some notifications', async function () { 1151 it('Should mark as read some notifications', async function () {
884 const res = await getUserNotifications(servers[ 0 ].url, userAccessToken, 2, 3) 1152 const res = await getUserNotifications(servers[ 0 ].url, userAccessToken, 2, 3)
@@ -940,6 +1208,8 @@ describe('Test users notifications', function () {
940 }) 1208 })
941 1209
942 it('Should not have notifications', async function () { 1210 it('Should not have notifications', async function () {
1211 this.timeout(20000)
1212
943 await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, { 1213 await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, {
944 newVideoFromSubscription: UserNotificationSettingValue.NONE 1214 newVideoFromSubscription: UserNotificationSettingValue.NONE
945 })) 1215 }))
@@ -957,6 +1227,8 @@ describe('Test users notifications', function () {
957 }) 1227 })
958 1228
959 it('Should only have web notifications', async function () { 1229 it('Should only have web notifications', async function () {
1230 this.timeout(20000)
1231
960 await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, { 1232 await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, {
961 newVideoFromSubscription: UserNotificationSettingValue.WEB 1233 newVideoFromSubscription: UserNotificationSettingValue.WEB
962 })) 1234 }))
@@ -981,6 +1253,8 @@ describe('Test users notifications', function () {
981 }) 1253 })
982 1254
983 it('Should only have mail notifications', async function () { 1255 it('Should only have mail notifications', async function () {
1256 this.timeout(20000)
1257
984 await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, { 1258 await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, {
985 newVideoFromSubscription: UserNotificationSettingValue.EMAIL 1259 newVideoFromSubscription: UserNotificationSettingValue.EMAIL
986 })) 1260 }))
@@ -1005,6 +1279,8 @@ describe('Test users notifications', function () {
1005 }) 1279 })
1006 1280
1007 it('Should have email and web notifications', async function () { 1281 it('Should have email and web notifications', async function () {
1282 this.timeout(20000)
1283
1008 await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, { 1284 await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(allNotificationSettings, {
1009 newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL 1285 newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
1010 })) 1286 }))
@@ -1026,6 +1302,6 @@ describe('Test users notifications', function () {
1026 after(async function () { 1302 after(async function () {
1027 MockSmtpServer.Instance.kill() 1303 MockSmtpServer.Instance.kill()
1028 1304
1029 killallServers(servers) 1305 await cleanupTests(servers)
1030 }) 1306 })
1031}) 1307})
diff --git a/server/tests/api/redundancy/redundancy.ts b/server/tests/api/redundancy/redundancy.ts
index 9d3ce8153..e31329c25 100644
--- a/server/tests/api/redundancy/redundancy.ts
+++ b/server/tests/api/redundancy/redundancy.ts
@@ -4,30 +4,36 @@ import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { VideoDetails } from '../../../../shared/models/videos' 5import { VideoDetails } from '../../../../shared/models/videos'
6import { 6import {
7 checkSegmentHash,
8 checkVideoFilesWereRemoved, cleanupTests,
7 doubleFollow, 9 doubleFollow,
8 flushAndRunMultipleServers, 10 flushAndRunMultipleServers,
9 getFollowingListPaginationAndSort, 11 getFollowingListPaginationAndSort,
10 getVideo, 12 getVideo,
13 getVideoWithToken,
11 immutableAssign, 14 immutableAssign,
12 killallServers, makeGetRequest, 15 killallServers,
16 makeGetRequest,
17 removeVideo,
18 reRunServer,
13 root, 19 root,
14 ServerInfo, 20 ServerInfo,
15 setAccessTokensToServers, unfollow, 21 setAccessTokensToServers,
22 unfollow,
16 uploadVideo, 23 uploadVideo,
17 viewVideo, 24 viewVideo,
18 wait, 25 wait,
19 waitUntilLog, 26 waitUntilLog
20 checkVideoFilesWereRemoved, removeVideo, getVideoWithToken 27} from '../../../../shared/extra-utils'
21} from '../../../../shared/utils' 28import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
22import { waitJobs } from '../../../../shared/utils/server/jobs'
23 29
24import * as magnetUtil from 'magnet-uri' 30import * as magnetUtil from 'magnet-uri'
25import { updateRedundancy } from '../../../../shared/utils/server/redundancy' 31import { updateRedundancy } from '../../../../shared/extra-utils/server/redundancy'
26import { ActorFollow } from '../../../../shared/models/actors' 32import { ActorFollow } from '../../../../shared/models/actors'
27import { readdir } from 'fs-extra' 33import { readdir } from 'fs-extra'
28import { join } from 'path' 34import { join } from 'path'
29import { VideoRedundancyStrategy } from '../../../../shared/models/redundancy' 35import { VideoRedundancyStrategy } from '../../../../shared/models/redundancy'
30import { getStats } from '../../../../shared/utils/server/stats' 36import { getStats } from '../../../../shared/extra-utils/server/stats'
31import { ServerStats } from '../../../../shared/models/server/server-stats.model' 37import { ServerStats } from '../../../../shared/models/server/server-stats.model'
32 38
33const expect = chai.expect 39const expect = chai.expect
@@ -46,8 +52,13 @@ function checkMagnetWebseeds (file: { magnetUri: string, resolution: { id: numbe
46 expect(parsed.urlList).to.have.lengthOf(baseWebseeds.length) 52 expect(parsed.urlList).to.have.lengthOf(baseWebseeds.length)
47} 53}
48 54
49async function runServers (strategy: VideoRedundancyStrategy, additionalParams: any = {}) { 55async function flushAndRunServers (strategy: VideoRedundancyStrategy, additionalParams: any = {}) {
50 const config = { 56 const config = {
57 transcoding: {
58 hls: {
59 enabled: true
60 }
61 },
51 redundancy: { 62 redundancy: {
52 videos: { 63 videos: {
53 check_interval: '5 seconds', 64 check_interval: '5 seconds',
@@ -85,7 +96,7 @@ async function runServers (strategy: VideoRedundancyStrategy, additionalParams:
85 await waitJobs(servers) 96 await waitJobs(servers)
86} 97}
87 98
88async function check1WebSeed (strategy: VideoRedundancyStrategy, videoUUID?: string) { 99async function check1WebSeed (videoUUID?: string) {
89 if (!videoUUID) videoUUID = video1Server2UUID 100 if (!videoUUID) videoUUID = video1Server2UUID
90 101
91 const webseeds = [ 102 const webseeds = [
@@ -93,47 +104,17 @@ async function check1WebSeed (strategy: VideoRedundancyStrategy, videoUUID?: str
93 ] 104 ]
94 105
95 for (const server of servers) { 106 for (const server of servers) {
96 { 107 // With token to avoid issues with video follow constraints
97 // With token to avoid issues with video follow constraints 108 const res = await getVideoWithToken(server.url, server.accessToken, videoUUID)
98 const res = await getVideoWithToken(server.url, server.accessToken, videoUUID)
99 109
100 const video: VideoDetails = res.body 110 const video: VideoDetails = res.body
101 for (const f of video.files) { 111 for (const f of video.files) {
102 checkMagnetWebseeds(f, webseeds, server) 112 checkMagnetWebseeds(f, webseeds, server)
103 }
104 } 113 }
105 } 114 }
106} 115}
107 116
108async function checkStatsWith2Webseed (strategy: VideoRedundancyStrategy) { 117async function check2Webseeds (videoUUID?: string) {
109 const res = await getStats(servers[0].url)
110 const data: ServerStats = res.body
111
112 expect(data.videosRedundancy).to.have.lengthOf(1)
113 const stat = data.videosRedundancy[0]
114
115 expect(stat.strategy).to.equal(strategy)
116 expect(stat.totalSize).to.equal(204800)
117 expect(stat.totalUsed).to.be.at.least(1).and.below(204801)
118 expect(stat.totalVideoFiles).to.equal(4)
119 expect(stat.totalVideos).to.equal(1)
120}
121
122async function checkStatsWith1Webseed (strategy: VideoRedundancyStrategy) {
123 const res = await getStats(servers[0].url)
124 const data: ServerStats = res.body
125
126 expect(data.videosRedundancy).to.have.lengthOf(1)
127
128 const stat = data.videosRedundancy[0]
129 expect(stat.strategy).to.equal(strategy)
130 expect(stat.totalSize).to.equal(204800)
131 expect(stat.totalUsed).to.equal(0)
132 expect(stat.totalVideoFiles).to.equal(0)
133 expect(stat.totalVideos).to.equal(0)
134}
135
136async function check2Webseeds (strategy: VideoRedundancyStrategy, videoUUID?: string) {
137 if (!videoUUID) videoUUID = video1Server2UUID 118 if (!videoUUID) videoUUID = video1Server2UUID
138 119
139 const webseeds = [ 120 const webseeds = [
@@ -158,7 +139,7 @@ async function check2Webseeds (strategy: VideoRedundancyStrategy, videoUUID?: st
158 await makeGetRequest({ 139 await makeGetRequest({
159 url: servers[1].url, 140 url: servers[1].url,
160 statusCodeExpected: 200, 141 statusCodeExpected: 200,
161 path: '/static/webseed/' + `${videoUUID}-${file.resolution.id}.mp4`, 142 path: `/static/webseed/${videoUUID}-${file.resolution.id}.mp4`,
162 contentType: null 143 contentType: null
163 }) 144 })
164 } 145 }
@@ -174,6 +155,85 @@ async function check2Webseeds (strategy: VideoRedundancyStrategy, videoUUID?: st
174 } 155 }
175} 156}
176 157
158async function check0PlaylistRedundancies (videoUUID?: string) {
159 if (!videoUUID) videoUUID = video1Server2UUID
160
161 for (const server of servers) {
162 // With token to avoid issues with video follow constraints
163 const res = await getVideoWithToken(server.url, server.accessToken, videoUUID)
164 const video: VideoDetails = res.body
165
166 expect(video.streamingPlaylists).to.be.an('array')
167 expect(video.streamingPlaylists).to.have.lengthOf(1)
168 expect(video.streamingPlaylists[0].redundancies).to.have.lengthOf(0)
169 }
170}
171
172async function check1PlaylistRedundancies (videoUUID?: string) {
173 if (!videoUUID) videoUUID = video1Server2UUID
174
175 for (const server of servers) {
176 const res = await getVideo(server.url, videoUUID)
177 const video: VideoDetails = res.body
178
179 expect(video.streamingPlaylists).to.have.lengthOf(1)
180 expect(video.streamingPlaylists[0].redundancies).to.have.lengthOf(1)
181
182 const redundancy = video.streamingPlaylists[0].redundancies[0]
183
184 expect(redundancy.baseUrl).to.equal(servers[0].url + '/static/redundancy/hls/' + videoUUID)
185 }
186
187 const baseUrlPlaylist = servers[1].url + '/static/streaming-playlists/hls'
188 const baseUrlSegment = servers[0].url + '/static/redundancy/hls'
189
190 const res = await getVideo(servers[0].url, videoUUID)
191 const hlsPlaylist = (res.body as VideoDetails).streamingPlaylists[0]
192
193 for (const resolution of [ 240, 360, 480, 720 ]) {
194 await checkSegmentHash(baseUrlPlaylist, baseUrlSegment, videoUUID, resolution, hlsPlaylist)
195 }
196
197 for (const directory of [ 'test1/redundancy/hls', 'test2/streaming-playlists/hls' ]) {
198 const files = await readdir(join(root(), directory, videoUUID))
199 expect(files).to.have.length.at.least(4)
200
201 for (const resolution of [ 240, 360, 480, 720 ]) {
202 const filename = `${videoUUID}-${resolution}-fragmented.mp4`
203
204 expect(files.find(f => f === filename)).to.not.be.undefined
205 }
206 }
207}
208
209async function checkStatsWith2Webseed (strategy: VideoRedundancyStrategy) {
210 const res = await getStats(servers[0].url)
211 const data: ServerStats = res.body
212
213 expect(data.videosRedundancy).to.have.lengthOf(1)
214 const stat = data.videosRedundancy[0]
215
216 expect(stat.strategy).to.equal(strategy)
217 expect(stat.totalSize).to.equal(204800)
218 expect(stat.totalUsed).to.be.at.least(1).and.below(204801)
219 expect(stat.totalVideoFiles).to.equal(4)
220 expect(stat.totalVideos).to.equal(1)
221}
222
223async function checkStatsWith1Webseed (strategy: VideoRedundancyStrategy) {
224 const res = await getStats(servers[0].url)
225 const data: ServerStats = res.body
226
227 expect(data.videosRedundancy).to.have.lengthOf(1)
228
229 const stat = data.videosRedundancy[0]
230 expect(stat.strategy).to.equal(strategy)
231 expect(stat.totalSize).to.equal(204800)
232 expect(stat.totalUsed).to.equal(0)
233 expect(stat.totalVideoFiles).to.equal(0)
234 expect(stat.totalVideos).to.equal(0)
235}
236
177async function enableRedundancyOnServer1 () { 237async function enableRedundancyOnServer1 () {
178 await updateRedundancy(servers[ 0 ].url, servers[ 0 ].accessToken, servers[ 1 ].host, true) 238 await updateRedundancy(servers[ 0 ].url, servers[ 0 ].accessToken, servers[ 1 ].host, true)
179 239
@@ -204,10 +264,6 @@ async function disableRedundancyOnServer1 () {
204 expect(server2.following.hostRedundancyAllowed).to.be.false 264 expect(server2.following.hostRedundancyAllowed).to.be.false
205} 265}
206 266
207async function cleanServers () {
208 killallServers(servers)
209}
210
211describe('Test videos redundancy', function () { 267describe('Test videos redundancy', function () {
212 268
213 describe('With most-views strategy', function () { 269 describe('With most-views strategy', function () {
@@ -216,11 +272,12 @@ describe('Test videos redundancy', function () {
216 before(function () { 272 before(function () {
217 this.timeout(120000) 273 this.timeout(120000)
218 274
219 return runServers(strategy) 275 return flushAndRunServers(strategy)
220 }) 276 })
221 277
222 it('Should have 1 webseed on the first video', async function () { 278 it('Should have 1 webseed on the first video', async function () {
223 await check1WebSeed(strategy) 279 await check1WebSeed()
280 await check0PlaylistRedundancies()
224 await checkStatsWith1Webseed(strategy) 281 await checkStatsWith1Webseed(strategy)
225 }) 282 })
226 283
@@ -229,31 +286,33 @@ describe('Test videos redundancy', function () {
229 }) 286 })
230 287
231 it('Should have 2 webseeds on the first video', async function () { 288 it('Should have 2 webseeds on the first video', async function () {
232 this.timeout(40000) 289 this.timeout(80000)
233 290
234 await waitJobs(servers) 291 await waitJobs(servers)
235 await waitUntilLog(servers[0], 'Duplicated ', 4) 292 await waitUntilLog(servers[0], 'Duplicated ', 5)
236 await waitJobs(servers) 293 await waitJobs(servers)
237 294
238 await check2Webseeds(strategy) 295 await check2Webseeds()
296 await check1PlaylistRedundancies()
239 await checkStatsWith2Webseed(strategy) 297 await checkStatsWith2Webseed(strategy)
240 }) 298 })
241 299
242 it('Should undo redundancy on server 1 and remove duplicated videos', async function () { 300 it('Should undo redundancy on server 1 and remove duplicated videos', async function () {
243 this.timeout(40000) 301 this.timeout(80000)
244 302
245 await disableRedundancyOnServer1() 303 await disableRedundancyOnServer1()
246 304
247 await waitJobs(servers) 305 await waitJobs(servers)
248 await wait(5000) 306 await wait(5000)
249 307
250 await check1WebSeed(strategy) 308 await check1WebSeed()
309 await check0PlaylistRedundancies()
251 310
252 await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ 'videos' ]) 311 await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ 'videos', join('playlists', 'hls') ])
253 }) 312 })
254 313
255 after(function () { 314 after(async function () {
256 return cleanServers() 315 return cleanupTests(servers)
257 }) 316 })
258 }) 317 })
259 318
@@ -263,11 +322,12 @@ describe('Test videos redundancy', function () {
263 before(function () { 322 before(function () {
264 this.timeout(120000) 323 this.timeout(120000)
265 324
266 return runServers(strategy) 325 return flushAndRunServers(strategy)
267 }) 326 })
268 327
269 it('Should have 1 webseed on the first video', async function () { 328 it('Should have 1 webseed on the first video', async function () {
270 await check1WebSeed(strategy) 329 await check1WebSeed()
330 await check0PlaylistRedundancies()
271 await checkStatsWith1Webseed(strategy) 331 await checkStatsWith1Webseed(strategy)
272 }) 332 })
273 333
@@ -276,31 +336,33 @@ describe('Test videos redundancy', function () {
276 }) 336 })
277 337
278 it('Should have 2 webseeds on the first video', async function () { 338 it('Should have 2 webseeds on the first video', async function () {
279 this.timeout(40000) 339 this.timeout(80000)
280 340
281 await waitJobs(servers) 341 await waitJobs(servers)
282 await waitUntilLog(servers[0], 'Duplicated ', 4) 342 await waitUntilLog(servers[0], 'Duplicated ', 5)
283 await waitJobs(servers) 343 await waitJobs(servers)
284 344
285 await check2Webseeds(strategy) 345 await check2Webseeds()
346 await check1PlaylistRedundancies()
286 await checkStatsWith2Webseed(strategy) 347 await checkStatsWith2Webseed(strategy)
287 }) 348 })
288 349
289 it('Should unfollow on server 1 and remove duplicated videos', async function () { 350 it('Should unfollow on server 1 and remove duplicated videos', async function () {
290 this.timeout(40000) 351 this.timeout(80000)
291 352
292 await unfollow(servers[0].url, servers[0].accessToken, servers[1]) 353 await unfollow(servers[0].url, servers[0].accessToken, servers[1])
293 354
294 await waitJobs(servers) 355 await waitJobs(servers)
295 await wait(5000) 356 await wait(5000)
296 357
297 await check1WebSeed(strategy) 358 await check1WebSeed()
359 await check0PlaylistRedundancies()
298 360
299 await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ 'videos' ]) 361 await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ 'videos' ])
300 }) 362 })
301 363
302 after(function () { 364 after(async function () {
303 return cleanServers() 365 await cleanupTests(servers)
304 }) 366 })
305 }) 367 })
306 368
@@ -310,11 +372,12 @@ describe('Test videos redundancy', function () {
310 before(function () { 372 before(function () {
311 this.timeout(120000) 373 this.timeout(120000)
312 374
313 return runServers(strategy, { min_views: 3 }) 375 return flushAndRunServers(strategy, { min_views: 3 })
314 }) 376 })
315 377
316 it('Should have 1 webseed on the first video', async function () { 378 it('Should have 1 webseed on the first video', async function () {
317 await check1WebSeed(strategy) 379 await check1WebSeed()
380 await check0PlaylistRedundancies()
318 await checkStatsWith1Webseed(strategy) 381 await checkStatsWith1Webseed(strategy)
319 }) 382 })
320 383
@@ -323,18 +386,19 @@ describe('Test videos redundancy', function () {
323 }) 386 })
324 387
325 it('Should still have 1 webseed on the first video', async function () { 388 it('Should still have 1 webseed on the first video', async function () {
326 this.timeout(40000) 389 this.timeout(80000)
327 390
328 await waitJobs(servers) 391 await waitJobs(servers)
329 await wait(15000) 392 await wait(15000)
330 await waitJobs(servers) 393 await waitJobs(servers)
331 394
332 await check1WebSeed(strategy) 395 await check1WebSeed()
396 await check0PlaylistRedundancies()
333 await checkStatsWith1Webseed(strategy) 397 await checkStatsWith1Webseed(strategy)
334 }) 398 })
335 399
336 it('Should view 2 times the first video to have > min_views config', async function () { 400 it('Should view 2 times the first video to have > min_views config', async function () {
337 this.timeout(40000) 401 this.timeout(80000)
338 402
339 await viewVideo(servers[ 0 ].url, video1Server2UUID) 403 await viewVideo(servers[ 0 ].url, video1Server2UUID)
340 await viewVideo(servers[ 2 ].url, video1Server2UUID) 404 await viewVideo(servers[ 2 ].url, video1Server2UUID)
@@ -344,13 +408,14 @@ describe('Test videos redundancy', function () {
344 }) 408 })
345 409
346 it('Should have 2 webseeds on the first video', async function () { 410 it('Should have 2 webseeds on the first video', async function () {
347 this.timeout(40000) 411 this.timeout(80000)
348 412
349 await waitJobs(servers) 413 await waitJobs(servers)
350 await waitUntilLog(servers[0], 'Duplicated ', 4) 414 await waitUntilLog(servers[0], 'Duplicated ', 5)
351 await waitJobs(servers) 415 await waitJobs(servers)
352 416
353 await check2Webseeds(strategy) 417 await check2Webseeds()
418 await check1PlaylistRedundancies()
354 await checkStatsWith2Webseed(strategy) 419 await checkStatsWith2Webseed(strategy)
355 }) 420 })
356 421
@@ -366,8 +431,8 @@ describe('Test videos redundancy', function () {
366 } 431 }
367 }) 432 })
368 433
369 after(function () { 434 after(async function () {
370 return cleanServers() 435 await cleanupTests(servers)
371 }) 436 })
372 }) 437 })
373 438
@@ -399,13 +464,13 @@ describe('Test videos redundancy', function () {
399 before(async function () { 464 before(async function () {
400 this.timeout(120000) 465 this.timeout(120000)
401 466
402 await runServers(strategy, { min_lifetime: '7 seconds', min_views: 0 }) 467 await flushAndRunServers(strategy, { min_lifetime: '7 seconds', min_views: 0 })
403 468
404 await enableRedundancyOnServer1() 469 await enableRedundancyOnServer1()
405 }) 470 })
406 471
407 it('Should still have 2 webseeds after 10 seconds', async function () { 472 it('Should still have 2 webseeds after 10 seconds', async function () {
408 this.timeout(40000) 473 this.timeout(80000)
409 474
410 await wait(10000) 475 await wait(10000)
411 476
@@ -420,7 +485,7 @@ describe('Test videos redundancy', function () {
420 }) 485 })
421 486
422 it('Should stop server 1 and expire video redundancy', async function () { 487 it('Should stop server 1 and expire video redundancy', async function () {
423 this.timeout(40000) 488 this.timeout(80000)
424 489
425 killallServers([ servers[0] ]) 490 killallServers([ servers[0] ])
426 491
@@ -429,8 +494,8 @@ describe('Test videos redundancy', function () {
429 await checkNotContains([ servers[1], servers[2] ], 'http%3A%2F%2Flocalhost%3A9001') 494 await checkNotContains([ servers[1], servers[2] ], 'http%3A%2F%2Flocalhost%3A9001')
430 }) 495 })
431 496
432 after(function () { 497 after(async function () {
433 return killallServers([ servers[1], servers[2] ]) 498 await cleanupTests(servers)
434 }) 499 })
435 }) 500 })
436 501
@@ -441,15 +506,16 @@ describe('Test videos redundancy', function () {
441 before(async function () { 506 before(async function () {
442 this.timeout(120000) 507 this.timeout(120000)
443 508
444 await runServers(strategy, { min_lifetime: '7 seconds', min_views: 0 }) 509 await flushAndRunServers(strategy, { min_lifetime: '7 seconds', min_views: 0 })
445 510
446 await enableRedundancyOnServer1() 511 await enableRedundancyOnServer1()
447 512
448 await waitJobs(servers) 513 await waitJobs(servers)
449 await waitUntilLog(servers[0], 'Duplicated ', 4) 514 await waitUntilLog(servers[0], 'Duplicated ', 5)
450 await waitJobs(servers) 515 await waitJobs(servers)
451 516
452 await check2Webseeds(strategy) 517 await check2Webseeds()
518 await check1PlaylistRedundancies()
453 await checkStatsWith2Webseed(strategy) 519 await checkStatsWith2Webseed(strategy)
454 520
455 const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, { name: 'video 2 server 2' }) 521 const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, { name: 'video 2 server 2' })
@@ -467,8 +533,10 @@ describe('Test videos redundancy', function () {
467 await wait(1000) 533 await wait(1000)
468 534
469 try { 535 try {
470 await check1WebSeed(strategy, video1Server2UUID) 536 await check1WebSeed(video1Server2UUID)
471 await check2Webseeds(strategy, video2Server2UUID) 537 await check0PlaylistRedundancies(video1Server2UUID)
538 await check2Webseeds(video2Server2UUID)
539 await check1PlaylistRedundancies(video2Server2UUID)
472 540
473 checked = true 541 checked = true
474 } catch { 542 } catch {
@@ -477,8 +545,28 @@ describe('Test videos redundancy', function () {
477 } 545 }
478 }) 546 })
479 547
480 after(function () { 548 it('Should disable strategy and remove redundancies', async function () {
481 return cleanServers() 549 this.timeout(80000)
550
551 await waitJobs(servers)
552
553 killallServers([ servers[ 0 ] ])
554 await reRunServer(servers[ 0 ], {
555 redundancy: {
556 videos: {
557 check_interval: '1 second',
558 strategies: []
559 }
560 }
561 })
562
563 await waitJobs(servers)
564
565 await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ join('redundancy', 'hls') ])
566 })
567
568 after(async function () {
569 await cleanupTests(servers)
482 }) 570 })
483 }) 571 })
484}) 572})
diff --git a/server/tests/api/search/search-activitypub-video-channels.ts b/server/tests/api/search/search-activitypub-video-channels.ts
index a411e973b..4d1ceb767 100644
--- a/server/tests/api/search/search-activitypub-video-channels.ts
+++ b/server/tests/api/search/search-activitypub-video-channels.ts
@@ -3,7 +3,7 @@
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { 5import {
6 addVideoChannel, 6 addVideoChannel, cleanupTests,
7 createUser, 7 createUser,
8 deleteVideoChannel, 8 deleteVideoChannel,
9 flushAndRunMultipleServers, 9 flushAndRunMultipleServers,
@@ -17,10 +17,10 @@ import {
17 uploadVideo, 17 uploadVideo,
18 userLogin, 18 userLogin,
19 wait 19 wait
20} from '../../../../shared/utils' 20} from '../../../../shared/extra-utils'
21import { waitJobs } from '../../../../shared/utils/server/jobs' 21import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
22import { VideoChannel } from '../../../../shared/models/videos' 22import { VideoChannel } from '../../../../shared/models/videos'
23import { searchVideoChannel } from '../../../../shared/utils/search/video-channels' 23import { searchVideoChannel } from '../../../../shared/extra-utils/search/video-channels'
24 24
25const expect = chai.expect 25const expect = chai.expect
26 26
@@ -33,14 +33,12 @@ describe('Test a ActivityPub video channels search', function () {
33 before(async function () { 33 before(async function () {
34 this.timeout(120000) 34 this.timeout(120000)
35 35
36 await flushTests()
37
38 servers = await flushAndRunMultipleServers(2) 36 servers = await flushAndRunMultipleServers(2)
39 37
40 await setAccessTokensToServers(servers) 38 await setAccessTokensToServers(servers)
41 39
42 { 40 {
43 await createUser(servers[0].url, servers[0].accessToken, 'user1_server1', 'password') 41 await createUser({ url: servers[ 0 ].url, accessToken: servers[ 0 ].accessToken, username: 'user1_server1', password: 'password' })
44 const channel = { 42 const channel = {
45 name: 'channel1_server1', 43 name: 'channel1_server1',
46 displayName: 'Channel 1 server 1' 44 displayName: 'Channel 1 server 1'
@@ -50,7 +48,7 @@ describe('Test a ActivityPub video channels search', function () {
50 48
51 { 49 {
52 const user = { username: 'user1_server2', password: 'password' } 50 const user = { username: 'user1_server2', password: 'password' }
53 await createUser(servers[1].url, servers[1].accessToken, user.username, user.password) 51 await createUser({ url: servers[ 1 ].url, accessToken: servers[ 1 ].accessToken, username: user.username, password: user.password })
54 userServer2Token = await userLogin(servers[1], user) 52 userServer2Token = await userLogin(servers[1], user)
55 53
56 const channel = { 54 const channel = {
@@ -208,11 +206,6 @@ describe('Test a ActivityPub video channels search', function () {
208 }) 206 })
209 207
210 after(async function () { 208 after(async function () {
211 killallServers(servers) 209 await cleanupTests(servers)
212
213 // Keep the logs if the test failed
214 if (this['ok']) {
215 await flushTests()
216 }
217 }) 210 })
218}) 211})
diff --git a/server/tests/api/search/search-activitypub-videos.ts b/server/tests/api/search/search-activitypub-videos.ts
index f881917e7..e039961cb 100644
--- a/server/tests/api/search/search-activitypub-videos.ts
+++ b/server/tests/api/search/search-activitypub-videos.ts
@@ -15,9 +15,9 @@ import {
15 updateVideo, 15 updateVideo,
16 uploadVideo, 16 uploadVideo,
17 wait, 17 wait,
18 searchVideo 18 searchVideo, cleanupTests
19} from '../../../../shared/utils' 19} from '../../../../shared/extra-utils'
20import { waitJobs } from '../../../../shared/utils/server/jobs' 20import { waitJobs } from '../../../../shared/extra-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
@@ -30,8 +30,6 @@ describe('Test a ActivityPub videos search', function () {
30 before(async function () { 30 before(async function () {
31 this.timeout(120000) 31 this.timeout(120000)
32 32
33 await flushTests()
34
35 servers = await flushAndRunMultipleServers(2) 33 servers = await flushAndRunMultipleServers(2)
36 34
37 await setAccessTokensToServers(servers) 35 await setAccessTokensToServers(servers)
@@ -152,11 +150,6 @@ describe('Test a ActivityPub videos search', function () {
152 }) 150 })
153 151
154 after(async function () { 152 after(async function () {
155 killallServers(servers) 153 await cleanupTests(servers)
156
157 // Keep the logs if the test failed
158 if (this['ok']) {
159 await flushTests()
160 }
161 }) 154 })
162}) 155})
diff --git a/server/tests/api/search/search-videos.ts b/server/tests/api/search/search-videos.ts
index 50da837da..1a086b33a 100644
--- a/server/tests/api/search/search-videos.ts
+++ b/server/tests/api/search/search-videos.ts
@@ -6,14 +6,15 @@ import {
6 advancedVideosSearch, 6 advancedVideosSearch,
7 flushTests, 7 flushTests,
8 killallServers, 8 killallServers,
9 runServer, 9 flushAndRunServer,
10 searchVideo, 10 searchVideo,
11 ServerInfo, 11 ServerInfo,
12 setAccessTokensToServers, 12 setAccessTokensToServers,
13 uploadVideo, 13 uploadVideo,
14 wait, 14 wait,
15 immutableAssign 15 immutableAssign,
16} from '../../../../shared/utils' 16 cleanupTests
17} from '../../../../shared/extra-utils'
17 18
18const expect = chai.expect 19const expect = chai.expect
19 20
@@ -24,9 +25,7 @@ describe('Test a videos search', function () {
24 before(async function () { 25 before(async function () {
25 this.timeout(30000) 26 this.timeout(30000)
26 27
27 await flushTests() 28 server = await flushAndRunServer(1)
28
29 server = await runServer(1)
30 29
31 await setAccessTokensToServers([ server ]) 30 await setAccessTokensToServers([ server ])
32 31
@@ -60,7 +59,10 @@ describe('Test a videos search', function () {
60 const attributes6 = immutableAssign(attributes1, { name: attributes1.name + ' - 6', tags: [ 't1', 't2 '] }) 59 const attributes6 = immutableAssign(attributes1, { name: attributes1.name + ' - 6', tags: [ 't1', 't2 '] })
61 await uploadVideo(server.url, server.accessToken, attributes6) 60 await uploadVideo(server.url, server.accessToken, attributes6)
62 61
63 const attributes7 = immutableAssign(attributes1, { name: attributes1.name + ' - 7' }) 62 const attributes7 = immutableAssign(attributes1, {
63 name: attributes1.name + ' - 7',
64 originallyPublishedAt: '2019-02-12T09:58:08.286Z'
65 })
64 await uploadVideo(server.url, server.accessToken, attributes7) 66 await uploadVideo(server.url, server.accessToken, attributes7)
65 67
66 const attributes8 = immutableAssign(attributes1, { name: attributes1.name + ' - 8', licence: 4 }) 68 const attributes8 = immutableAssign(attributes1, { name: attributes1.name + ' - 8', licence: 4 })
@@ -343,12 +345,68 @@ describe('Test a videos search', function () {
343 expect(videos[0].name).to.equal('1111 2222 3333') 345 expect(videos[0].name).to.equal('1111 2222 3333')
344 }) 346 })
345 347
346 after(async function () { 348 it('Should search on originally published date', async function () {
347 killallServers([ server ]) 349 const baseQuery = {
350 search: '1111 2222 3333',
351 languageOneOf: [ 'pl', 'fr' ],
352 durationMax: 4,
353 nsfw: 'false' as 'false',
354 licenceOneOf: [ 1, 4 ]
355 }
356
357 {
358 const query = immutableAssign(baseQuery, { originallyPublishedStartDate: '2019-02-11T09:58:08.286Z' })
359 const res = await advancedVideosSearch(server.url, query)
360
361 expect(res.body.total).to.equal(1)
362 expect(res.body.data[0].name).to.equal('1111 2222 3333 - 7')
363 }
364
365 {
366 const query = immutableAssign(baseQuery, { originallyPublishedEndDate: '2019-03-11T09:58:08.286Z' })
367 const res = await advancedVideosSearch(server.url, query)
368
369 expect(res.body.total).to.equal(1)
370 expect(res.body.data[0].name).to.equal('1111 2222 3333 - 7')
371 }
348 372
349 // Keep the logs if the test failed 373 {
350 if (this['ok']) { 374 const query = immutableAssign(baseQuery, { originallyPublishedEndDate: '2019-01-11T09:58:08.286Z' })
351 await flushTests() 375 const res = await advancedVideosSearch(server.url, query)
376
377 expect(res.body.total).to.equal(0)
378 }
379
380 {
381 const query = immutableAssign(baseQuery, { originallyPublishedStartDate: '2019-03-11T09:58:08.286Z' })
382 const res = await advancedVideosSearch(server.url, query)
383
384 expect(res.body.total).to.equal(0)
385 }
386
387 {
388 const query = immutableAssign(baseQuery, {
389 originallyPublishedStartDate: '2019-01-11T09:58:08.286Z',
390 originallyPublishedEndDate: '2019-01-10T09:58:08.286Z'
391 })
392 const res = await advancedVideosSearch(server.url, query)
393
394 expect(res.body.total).to.equal(0)
395 }
396
397 {
398 const query = immutableAssign(baseQuery, {
399 originallyPublishedStartDate: '2019-01-11T09:58:08.286Z',
400 originallyPublishedEndDate: '2019-04-11T09:58:08.286Z'
401 })
402 const res = await advancedVideosSearch(server.url, query)
403
404 expect(res.body.total).to.equal(1)
405 expect(res.body.data[0].name).to.equal('1111 2222 3333 - 7')
352 } 406 }
353 }) 407 })
408
409 after(async function () {
410 await cleanupTests([ server ])
411 })
354}) 412})
diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts
index bebfc7398..c0d11914b 100644
--- a/server/tests/api/server/config.ts
+++ b/server/tests/api/server/config.ts
@@ -5,18 +5,18 @@ import * 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 { 7import {
8 cleanupTests,
8 deleteCustomConfig, 9 deleteCustomConfig,
10 flushAndRunServer,
9 getAbout, 11 getAbout,
10 killallServers,
11 reRunServer,
12 flushTests,
13 getConfig, 12 getConfig,
14 getCustomConfig, 13 getCustomConfig,
14 killallServers,
15 registerUser, 15 registerUser,
16 runServer, 16 reRunServer,
17 setAccessTokensToServers, 17 setAccessTokensToServers,
18 updateCustomConfig 18 updateCustomConfig
19} from '../../../../shared/utils' 19} from '../../../../shared/extra-utils'
20import { ServerConfig } from '../../../../shared/models' 20import { ServerConfig } from '../../../../shared/models'
21 21
22const expect = chai.expect 22const expect = chai.expect
@@ -30,6 +30,7 @@ function checkInitialConfig (data: CustomConfig) {
30 expect(data.instance.description).to.equal('Welcome to this PeerTube instance!') 30 expect(data.instance.description).to.equal('Welcome to this PeerTube instance!')
31 expect(data.instance.terms).to.equal('No terms for now.') 31 expect(data.instance.terms).to.equal('No terms for now.')
32 expect(data.instance.defaultClientRoute).to.equal('/videos/trending') 32 expect(data.instance.defaultClientRoute).to.equal('/videos/trending')
33 expect(data.instance.isNSFW).to.be.false
33 expect(data.instance.defaultNSFWPolicy).to.equal('display') 34 expect(data.instance.defaultNSFWPolicy).to.equal('display')
34 expect(data.instance.customizations.css).to.be.empty 35 expect(data.instance.customizations.css).to.be.empty
35 expect(data.instance.customizations.javascript).to.be.empty 36 expect(data.instance.customizations.javascript).to.be.empty
@@ -57,8 +58,14 @@ function checkInitialConfig (data: CustomConfig) {
57 expect(data.transcoding.resolutions['480p']).to.be.true 58 expect(data.transcoding.resolutions['480p']).to.be.true
58 expect(data.transcoding.resolutions['720p']).to.be.true 59 expect(data.transcoding.resolutions['720p']).to.be.true
59 expect(data.transcoding.resolutions['1080p']).to.be.true 60 expect(data.transcoding.resolutions['1080p']).to.be.true
61 expect(data.transcoding.hls.enabled).to.be.true
62
60 expect(data.import.videos.http.enabled).to.be.true 63 expect(data.import.videos.http.enabled).to.be.true
61 expect(data.import.videos.torrent.enabled).to.be.true 64 expect(data.import.videos.torrent.enabled).to.be.true
65 expect(data.autoBlacklist.videos.ofUsers.enabled).to.be.false
66
67 expect(data.followers.instance.enabled).to.be.true
68 expect(data.followers.instance.manualApproval).to.be.false
62} 69}
63 70
64function checkUpdatedConfig (data: CustomConfig) { 71function checkUpdatedConfig (data: CustomConfig) {
@@ -67,6 +74,7 @@ function checkUpdatedConfig (data: CustomConfig) {
67 expect(data.instance.description).to.equal('my super description') 74 expect(data.instance.description).to.equal('my super description')
68 expect(data.instance.terms).to.equal('my super terms') 75 expect(data.instance.terms).to.equal('my super terms')
69 expect(data.instance.defaultClientRoute).to.equal('/videos/recently-added') 76 expect(data.instance.defaultClientRoute).to.equal('/videos/recently-added')
77 expect(data.instance.isNSFW).to.be.true
70 expect(data.instance.defaultNSFWPolicy).to.equal('blur') 78 expect(data.instance.defaultNSFWPolicy).to.equal('blur')
71 expect(data.instance.customizations.javascript).to.equal('alert("coucou")') 79 expect(data.instance.customizations.javascript).to.equal('alert("coucou")')
72 expect(data.instance.customizations.css).to.equal('body { background-color: red; }') 80 expect(data.instance.customizations.css).to.equal('body { background-color: red; }')
@@ -79,7 +87,7 @@ function checkUpdatedConfig (data: CustomConfig) {
79 87
80 expect(data.signup.enabled).to.be.false 88 expect(data.signup.enabled).to.be.false
81 expect(data.signup.limit).to.equal(5) 89 expect(data.signup.limit).to.equal(5)
82 expect(data.signup.requiresEmailVerification).to.be.true 90 expect(data.signup.requiresEmailVerification).to.be.false
83 91
84 expect(data.admin.email).to.equal('superadmin1@example.com') 92 expect(data.admin.email).to.equal('superadmin1@example.com')
85 expect(data.contactForm.enabled).to.be.false 93 expect(data.contactForm.enabled).to.be.false
@@ -95,9 +103,14 @@ function checkUpdatedConfig (data: CustomConfig) {
95 expect(data.transcoding.resolutions['480p']).to.be.true 103 expect(data.transcoding.resolutions['480p']).to.be.true
96 expect(data.transcoding.resolutions['720p']).to.be.false 104 expect(data.transcoding.resolutions['720p']).to.be.false
97 expect(data.transcoding.resolutions['1080p']).to.be.false 105 expect(data.transcoding.resolutions['1080p']).to.be.false
106 expect(data.transcoding.hls.enabled).to.be.false
98 107
99 expect(data.import.videos.http.enabled).to.be.false 108 expect(data.import.videos.http.enabled).to.be.false
100 expect(data.import.videos.torrent.enabled).to.be.false 109 expect(data.import.videos.torrent.enabled).to.be.false
110 expect(data.autoBlacklist.videos.ofUsers.enabled).to.be.true
111
112 expect(data.followers.instance.enabled).to.be.false
113 expect(data.followers.instance.manualApproval).to.be.true
101} 114}
102 115
103describe('Test config', function () { 116describe('Test config', function () {
@@ -105,9 +118,7 @@ describe('Test config', function () {
105 118
106 before(async function () { 119 before(async function () {
107 this.timeout(30000) 120 this.timeout(30000)
108 121 server = await flushAndRunServer(1)
109 await flushTests()
110 server = await runServer(1)
111 await setAccessTokensToServers([ server ]) 122 await setAccessTokensToServers([ server ])
112 }) 123 })
113 124
@@ -160,6 +171,7 @@ describe('Test config', function () {
160 description: 'my super description', 171 description: 'my super description',
161 terms: 'my super terms', 172 terms: 'my super terms',
162 defaultClientRoute: '/videos/recently-added', 173 defaultClientRoute: '/videos/recently-added',
174 isNSFW: true,
163 defaultNSFWPolicy: 'blur' as 'blur', 175 defaultNSFWPolicy: 'blur' as 'blur',
164 customizations: { 176 customizations: {
165 javascript: 'alert("coucou")', 177 javascript: 'alert("coucou")',
@@ -183,7 +195,7 @@ describe('Test config', function () {
183 signup: { 195 signup: {
184 enabled: false, 196 enabled: false,
185 limit: 5, 197 limit: 5,
186 requiresEmailVerification: true 198 requiresEmailVerification: false
187 }, 199 },
188 admin: { 200 admin: {
189 email: 'superadmin1@example.com' 201 email: 'superadmin1@example.com'
@@ -205,6 +217,9 @@ describe('Test config', function () {
205 '480p': true, 217 '480p': true,
206 '720p': false, 218 '720p': false,
207 '1080p': false 219 '1080p': false
220 },
221 hls: {
222 enabled: false
208 } 223 }
209 }, 224 },
210 import: { 225 import: {
@@ -216,6 +231,19 @@ describe('Test config', function () {
216 enabled: false 231 enabled: false
217 } 232 }
218 } 233 }
234 },
235 autoBlacklist: {
236 videos: {
237 ofUsers: {
238 enabled: true
239 }
240 }
241 },
242 followers: {
243 instance: {
244 enabled: false,
245 manualApproval: true
246 }
219 } 247 }
220 } 248 }
221 await updateCustomConfig(server.url, server.accessToken, newCustomConfig) 249 await updateCustomConfig(server.url, server.accessToken, newCustomConfig)
@@ -273,6 +301,6 @@ describe('Test config', function () {
273 }) 301 })
274 302
275 after(async function () { 303 after(async function () {
276 killallServers([ server ]) 304 await cleanupTests([ server ])
277 }) 305 })
278}) 306})
diff --git a/server/tests/api/server/contact-form.ts b/server/tests/api/server/contact-form.ts
index 06a2f89b0..ba51198b3 100644
--- a/server/tests/api/server/contact-form.ts
+++ b/server/tests/api/server/contact-form.ts
@@ -2,10 +2,18 @@
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { flushTests, killallServers, runServer, ServerInfo, setAccessTokensToServers, wait } from '../../../../shared/utils' 5import {
6import { MockSmtpServer } from '../../../../shared/utils/miscs/email' 6 flushTests,
7import { waitJobs } from '../../../../shared/utils/server/jobs' 7 killallServers,
8import { sendContactForm } from '../../../../shared/utils/server/contact-form' 8 flushAndRunServer,
9 ServerInfo,
10 setAccessTokensToServers,
11 wait,
12 cleanupTests
13} from '../../../../shared/extra-utils'
14import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email'
15import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
16import { sendContactForm } from '../../../../shared/extra-utils/server/contact-form'
9 17
10const expect = chai.expect 18const expect = chai.expect
11 19
@@ -18,14 +26,12 @@ describe('Test contact form', function () {
18 26
19 await MockSmtpServer.Instance.collectEmails(emails) 27 await MockSmtpServer.Instance.collectEmails(emails)
20 28
21 await flushTests()
22
23 const overrideConfig = { 29 const overrideConfig = {
24 smtp: { 30 smtp: {
25 hostname: 'localhost' 31 hostname: 'localhost'
26 } 32 }
27 } 33 }
28 server = await runServer(1, overrideConfig) 34 server = await flushAndRunServer(1, overrideConfig)
29 await setAccessTokensToServers([ server ]) 35 await setAccessTokensToServers([ server ])
30 }) 36 })
31 37
@@ -82,6 +88,7 @@ describe('Test contact form', function () {
82 88
83 after(async function () { 89 after(async function () {
84 MockSmtpServer.Instance.kill() 90 MockSmtpServer.Instance.kill()
85 killallServers([ server ]) 91
92 await cleanupTests([ server ])
86 }) 93 })
87}) 94})
diff --git a/server/tests/api/server/email.ts b/server/tests/api/server/email.ts
index f8f16f54f..bacdf1b1b 100644
--- a/server/tests/api/server/email.ts
+++ b/server/tests/api/server/email.ts
@@ -10,7 +10,7 @@ import {
10 createUser, removeVideoFromBlacklist, 10 createUser, removeVideoFromBlacklist,
11 reportVideoAbuse, 11 reportVideoAbuse,
12 resetPassword, 12 resetPassword,
13 runServer, 13 flushAndRunServer,
14 unblockUser, 14 unblockUser,
15 uploadVideo, 15 uploadVideo,
16 userLogin, 16 userLogin,
@@ -18,10 +18,10 @@ import {
18 flushTests, 18 flushTests,
19 killallServers, 19 killallServers,
20 ServerInfo, 20 ServerInfo,
21 setAccessTokensToServers 21 setAccessTokensToServers, cleanupTests
22} from '../../../../shared/utils' 22} from '../../../../shared/extra-utils'
23import { MockSmtpServer } from '../../../../shared/utils/miscs/email' 23import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email'
24import { waitJobs } from '../../../../shared/utils/server/jobs' 24import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
25 25
26const expect = chai.expect 26const expect = chai.expect
27 27
@@ -43,18 +43,16 @@ describe('Test emails', function () {
43 43
44 await MockSmtpServer.Instance.collectEmails(emails) 44 await MockSmtpServer.Instance.collectEmails(emails)
45 45
46 await flushTests()
47
48 const overrideConfig = { 46 const overrideConfig = {
49 smtp: { 47 smtp: {
50 hostname: 'localhost' 48 hostname: 'localhost'
51 } 49 }
52 } 50 }
53 server = await runServer(1, overrideConfig) 51 server = await flushAndRunServer(1, overrideConfig)
54 await setAccessTokensToServers([ server ]) 52 await setAccessTokensToServers([ server ])
55 53
56 { 54 {
57 const res = await createUser(server.url, server.accessToken, user.username, user.password) 55 const res = await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
58 userId = res.body.user.id 56 userId = res.body.user.id
59 57
60 userAccessToken = await userLogin(server, user) 58 userAccessToken = await userLogin(server, user)
@@ -142,7 +140,8 @@ describe('Test emails', function () {
142 }) 140 })
143 }) 141 })
144 142
145 describe('When blocking/unblocking user', async function () { 143 describe('When blocking/unblocking user', function () {
144
146 it('Should send the notification email when blocking a user', async function () { 145 it('Should send the notification email when blocking a user', async function () {
147 this.timeout(10000) 146 this.timeout(10000)
148 147
@@ -259,6 +258,7 @@ describe('Test emails', function () {
259 258
260 after(async function () { 259 after(async function () {
261 MockSmtpServer.Instance.kill() 260 MockSmtpServer.Instance.kill()
262 killallServers([ server ]) 261
262 await cleanupTests([ server ])
263 }) 263 })
264}) 264})
diff --git a/server/tests/api/server/follow-constraints.ts b/server/tests/api/server/follow-constraints.ts
index 8bb073c41..4285a9e7a 100644
--- a/server/tests/api/server/follow-constraints.ts
+++ b/server/tests/api/server/follow-constraints.ts
@@ -12,11 +12,11 @@ import {
12 killallServers, 12 killallServers,
13 ServerInfo, 13 ServerInfo,
14 setAccessTokensToServers, 14 setAccessTokensToServers,
15 uploadVideo 15 uploadVideo, cleanupTests
16} from '../../../../shared/utils' 16} from '../../../../shared/extra-utils'
17import { unfollow } from '../../../../shared/utils/server/follows' 17import { unfollow } from '../../../../shared/extra-utils/server/follows'
18import { userLogin } from '../../../../shared/utils/users/login' 18import { userLogin } from '../../../../shared/extra-utils/users/login'
19import { createUser } from '../../../../shared/utils/users/users' 19import { createUser } from '../../../../shared/extra-utils/users/users'
20 20
21const expect = chai.expect 21const expect = chai.expect
22 22
@@ -47,7 +47,7 @@ describe('Test follow constraints', function () {
47 username: 'user1', 47 username: 'user1',
48 password: 'super_password' 48 password: 'super_password'
49 } 49 }
50 await createUser(servers[0].url, servers[0].accessToken, user.username, user.password) 50 await createUser({ url: servers[ 0 ].url, accessToken: servers[ 0 ].accessToken, username: user.username, password: user.password })
51 userAccessToken = await userLogin(servers[0], user) 51 userAccessToken = await userLogin(servers[0], user)
52 52
53 await doubleFollow(servers[0], servers[1]) 53 await doubleFollow(servers[0], servers[1])
@@ -220,6 +220,6 @@ describe('Test follow constraints', function () {
220 }) 220 })
221 221
222 after(async function () { 222 after(async function () {
223 killallServers(servers) 223 await cleanupTests(servers)
224 }) 224 })
225}) 225})
diff --git a/server/tests/api/server/follows-moderation.ts b/server/tests/api/server/follows-moderation.ts
new file mode 100644
index 000000000..2a3a4d5c8
--- /dev/null
+++ b/server/tests/api/server/follows-moderation.ts
@@ -0,0 +1,195 @@
1/* tslint:disable:no-unused-expression */
2
3import * as chai from 'chai'
4import 'mocha'
5import {
6 acceptFollower, cleanupTests,
7 flushAndRunMultipleServers,
8 killallServers,
9 ServerInfo,
10 setAccessTokensToServers,
11 updateCustomSubConfig
12} from '../../../../shared/extra-utils/index'
13import {
14 follow,
15 getFollowersListPaginationAndSort,
16 getFollowingListPaginationAndSort,
17 removeFollower,
18 rejectFollower
19} from '../../../../shared/extra-utils/server/follows'
20import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
21import { ActorFollow } from '../../../../shared/models/actors'
22
23const expect = chai.expect
24
25async function checkServer1And2HasFollowers (servers: ServerInfo[], state = 'accepted') {
26 {
27 const res = await getFollowingListPaginationAndSort(servers[0].url, 0, 5, 'createdAt')
28 expect(res.body.total).to.equal(1)
29
30 const follow = res.body.data[0] as ActorFollow
31 expect(follow.state).to.equal(state)
32 expect(follow.follower.url).to.equal('http://localhost:9001/accounts/peertube')
33 expect(follow.following.url).to.equal('http://localhost:9002/accounts/peertube')
34 }
35
36 {
37 const res = await getFollowersListPaginationAndSort(servers[1].url, 0, 5, 'createdAt')
38 expect(res.body.total).to.equal(1)
39
40 const follow = res.body.data[0] as ActorFollow
41 expect(follow.state).to.equal(state)
42 expect(follow.follower.url).to.equal('http://localhost:9001/accounts/peertube')
43 expect(follow.following.url).to.equal('http://localhost:9002/accounts/peertube')
44 }
45}
46
47async function checkNoFollowers (servers: ServerInfo[]) {
48 {
49 const res = await getFollowingListPaginationAndSort(servers[ 0 ].url, 0, 5, 'createdAt')
50 expect(res.body.total).to.equal(0)
51 }
52
53 {
54 const res = await getFollowersListPaginationAndSort(servers[ 1 ].url, 0, 5, 'createdAt')
55 expect(res.body.total).to.equal(0)
56 }
57}
58
59describe('Test follows moderation', function () {
60 let servers: ServerInfo[] = []
61
62 before(async function () {
63 this.timeout(30000)
64
65 servers = await flushAndRunMultipleServers(3)
66
67 // Get the access tokens
68 await setAccessTokensToServers(servers)
69 })
70
71 it('Should have server 1 following server 2', async function () {
72 this.timeout(30000)
73
74 await follow(servers[0].url, [ servers[1].url ], servers[0].accessToken)
75
76 await waitJobs(servers)
77 })
78
79 it('Should have correct follows', async function () {
80 await checkServer1And2HasFollowers(servers)
81 })
82
83 it('Should remove follower on server 2', async function () {
84 await removeFollower(servers[1].url, servers[1].accessToken, servers[0])
85
86 await waitJobs(servers)
87 })
88
89 it('Should not not have follows anymore', async function () {
90 await checkNoFollowers(servers)
91 })
92
93 it('Should disable followers on server 2', async function () {
94 const subConfig = {
95 followers: {
96 instance: {
97 enabled: false,
98 manualApproval: false
99 }
100 }
101 }
102
103 await updateCustomSubConfig(servers[1].url, servers[1].accessToken, subConfig)
104
105 await follow(servers[0].url, [ servers[1].url ], servers[0].accessToken)
106 await waitJobs(servers)
107
108 await checkNoFollowers(servers)
109 })
110
111 it('Should re enable followers on server 2', async function () {
112 const subConfig = {
113 followers: {
114 instance: {
115 enabled: true,
116 manualApproval: false
117 }
118 }
119 }
120
121 await updateCustomSubConfig(servers[1].url, servers[1].accessToken, subConfig)
122
123 await follow(servers[0].url, [ servers[1].url ], servers[0].accessToken)
124 await waitJobs(servers)
125
126 await checkServer1And2HasFollowers(servers)
127 })
128
129 it('Should manually approve followers', async function () {
130 this.timeout(20000)
131
132 await removeFollower(servers[1].url, servers[1].accessToken, servers[0])
133 await waitJobs(servers)
134
135 const subConfig = {
136 followers: {
137 instance: {
138 enabled: true,
139 manualApproval: true
140 }
141 }
142 }
143
144 await updateCustomSubConfig(servers[1].url, servers[1].accessToken, subConfig)
145 await updateCustomSubConfig(servers[2].url, servers[2].accessToken, subConfig)
146
147 await follow(servers[0].url, [ servers[1].url ], servers[0].accessToken)
148 await waitJobs(servers)
149
150 await checkServer1And2HasFollowers(servers, 'pending')
151 })
152
153 it('Should accept a follower', async function () {
154 await acceptFollower(servers[1].url, servers[1].accessToken, 'peertube@localhost:9001')
155 await waitJobs(servers)
156
157 await checkServer1And2HasFollowers(servers)
158 })
159
160 it('Should reject another follower', async function () {
161 this.timeout(20000)
162
163 await follow(servers[0].url, [ servers[2].url ], servers[0].accessToken)
164 await waitJobs(servers)
165
166 {
167 const res = await getFollowingListPaginationAndSort(servers[0].url, 0, 5, 'createdAt')
168 expect(res.body.total).to.equal(2)
169 }
170
171 {
172 const res = await getFollowersListPaginationAndSort(servers[1].url, 0, 5, 'createdAt')
173 expect(res.body.total).to.equal(1)
174 }
175
176 {
177 const res = await getFollowersListPaginationAndSort(servers[2].url, 0, 5, 'createdAt')
178 expect(res.body.total).to.equal(1)
179 }
180
181 await rejectFollower(servers[2].url, servers[2].accessToken, 'peertube@localhost:9001')
182 await waitJobs(servers)
183
184 await checkServer1And2HasFollowers(servers)
185
186 {
187 const res = await getFollowersListPaginationAndSort(servers[ 2 ].url, 0, 5, 'createdAt')
188 expect(res.body.total).to.equal(0)
189 }
190 })
191
192 after(async function () {
193 await cleanupTests(servers)
194 })
195})
diff --git a/server/tests/api/server/follows.ts b/server/tests/api/server/follows.ts
index b0fc5d293..397093cdb 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 '../../../../shared/utils' 7import { cleanupTests, completeVideoCheck } from '../../../../shared/extra-utils'
8import { 8import {
9 flushAndRunMultipleServers, 9 flushAndRunMultipleServers,
10 getVideosList, 10 getVideosList,
@@ -12,26 +12,26 @@ import {
12 ServerInfo, 12 ServerInfo,
13 setAccessTokensToServers, 13 setAccessTokensToServers,
14 uploadVideo 14 uploadVideo
15} from '../../../../shared/utils/index' 15} from '../../../../shared/extra-utils/index'
16import { dateIsValid } from '../../../../shared/utils/miscs/miscs' 16import { dateIsValid } from '../../../../shared/extra-utils/miscs/miscs'
17import { 17import {
18 follow, 18 follow,
19 getFollowersListPaginationAndSort, 19 getFollowersListPaginationAndSort,
20 getFollowingListPaginationAndSort, 20 getFollowingListPaginationAndSort,
21 unfollow 21 unfollow
22} from '../../../../shared/utils/server/follows' 22} from '../../../../shared/extra-utils/server/follows'
23import { expectAccountFollows } from '../../../../shared/utils/users/accounts' 23import { expectAccountFollows } from '../../../../shared/extra-utils/users/accounts'
24import { userLogin } from '../../../../shared/utils/users/login' 24import { userLogin } from '../../../../shared/extra-utils/users/login'
25import { createUser } from '../../../../shared/utils/users/users' 25import { createUser } from '../../../../shared/extra-utils/users/users'
26import { 26import {
27 addVideoCommentReply, 27 addVideoCommentReply,
28 addVideoCommentThread, 28 addVideoCommentThread,
29 getVideoCommentThreads, 29 getVideoCommentThreads,
30 getVideoThreadComments 30 getVideoThreadComments
31} from '../../../../shared/utils/videos/video-comments' 31} from '../../../../shared/extra-utils/videos/video-comments'
32import { rateVideo } from '../../../../shared/utils/videos/videos' 32import { rateVideo } from '../../../../shared/extra-utils/videos/videos'
33import { waitJobs } from '../../../../shared/utils/server/jobs' 33import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
34import { createVideoCaption, listVideoCaptions, testCaptionFile } from '../../../../shared/utils/videos/video-captions' 34import { createVideoCaption, listVideoCaptions, testCaptionFile } from '../../../../shared/extra-utils/videos/video-captions'
35import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model' 35import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model'
36 36
37const expect = chai.expect 37const expect = chai.expect
@@ -241,7 +241,7 @@ describe('Test follows', function () {
241 expect(res.body.data[0].name).to.equal('server3') 241 expect(res.body.data[0].name).to.equal('server3')
242 }) 242 })
243 243
244 describe('Should propagate data on a new following', async function () { 244 describe('Should propagate data on a new following', function () {
245 let video4: Video 245 let video4: Video
246 246
247 before(async function () { 247 before(async function () {
@@ -263,7 +263,7 @@ describe('Test follows', function () {
263 263
264 { 264 {
265 const user = { username: 'captain', password: 'password' } 265 const user = { username: 'captain', password: 'password' }
266 await createUser(servers[ 2 ].url, servers[ 2 ].accessToken, user.username, user.password) 266 await createUser({ url: servers[ 2 ].url, accessToken: servers[ 2 ].accessToken, username: user.username, password: user.password })
267 const userAccessToken = await userLogin(servers[ 2 ], user) 267 const userAccessToken = await userLogin(servers[ 2 ], user)
268 268
269 const resVideos = await getVideosList(servers[ 2 ].url) 269 const resVideos = await getVideosList(servers[ 2 ].url)
@@ -348,6 +348,7 @@ describe('Test follows', function () {
348 }, 348 },
349 isLocal, 349 isLocal,
350 commentsEnabled: true, 350 commentsEnabled: true,
351 downloadEnabled: true,
351 duration: 5, 352 duration: 5,
352 tags: [ 'tag1', 'tag2', 'tag3' ], 353 tags: [ 'tag1', 'tag2', 'tag3' ],
353 privacy: VideoPrivacy.PUBLIC, 354 privacy: VideoPrivacy.PUBLIC,
@@ -435,6 +436,6 @@ describe('Test follows', function () {
435 }) 436 })
436 437
437 after(async function () { 438 after(async function () {
438 killallServers(servers) 439 await cleanupTests(servers)
439 }) 440 })
440}) 441})
diff --git a/server/tests/api/server/handle-down.ts b/server/tests/api/server/handle-down.ts
index cd7baadad..19010dbc1 100644
--- a/server/tests/api/server/handle-down.ts
+++ b/server/tests/api/server/handle-down.ts
@@ -7,6 +7,7 @@ import { VideoPrivacy } from '../../../../shared/models/videos'
7import { VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model' 7import { VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model'
8 8
9import { 9import {
10 cleanupTests,
10 completeVideoCheck, 11 completeVideoCheck,
11 flushAndRunMultipleServers, 12 flushAndRunMultipleServers,
12 getVideo, 13 getVideo,
@@ -20,15 +21,15 @@ import {
20 updateVideo, 21 updateVideo,
21 uploadVideo, 22 uploadVideo,
22 wait 23 wait
23} from '../../../../shared/utils' 24} from '../../../../shared/extra-utils'
24import { follow, getFollowersListPaginationAndSort } from '../../../../shared/utils/server/follows' 25import { follow, getFollowersListPaginationAndSort } from '../../../../shared/extra-utils/server/follows'
25import { getJobsListPaginationAndSort, waitJobs } from '../../../../shared/utils/server/jobs' 26import { getJobsListPaginationAndSort, waitJobs } from '../../../../shared/extra-utils/server/jobs'
26import { 27import {
27 addVideoCommentReply, 28 addVideoCommentReply,
28 addVideoCommentThread, 29 addVideoCommentThread,
29 getVideoCommentThreads, 30 getVideoCommentThreads,
30 getVideoThreadComments 31 getVideoThreadComments
31} from '../../../../shared/utils/videos/video-comments' 32} from '../../../../shared/extra-utils/videos/video-comments'
32 33
33const expect = chai.expect 34const expect = chai.expect
34 35
@@ -76,6 +77,7 @@ describe('Test handle downs', function () {
76 tags: [ 'tag1p1', 'tag2p1' ], 77 tags: [ 'tag1p1', 'tag2p1' ],
77 privacy: VideoPrivacy.PUBLIC, 78 privacy: VideoPrivacy.PUBLIC,
78 commentsEnabled: true, 79 commentsEnabled: true,
80 downloadEnabled: true,
79 channel: { 81 channel: {
80 name: 'root_channel', 82 name: 'root_channel',
81 displayName: 'Main root channel', 83 displayName: 'Main root channel',
@@ -296,6 +298,6 @@ describe('Test handle downs', function () {
296 }) 298 })
297 299
298 after(async function () { 300 after(async function () {
299 killallServers(servers) 301 await cleanupTests(servers)
300 }) 302 })
301}) 303})
diff --git a/server/tests/api/server/index.ts b/server/tests/api/server/index.ts
index 1f80cc6cf..94c15e0d0 100644
--- a/server/tests/api/server/index.ts
+++ b/server/tests/api/server/index.ts
@@ -3,8 +3,10 @@ import './contact-form'
3import './email' 3import './email'
4import './follow-constraints' 4import './follow-constraints'
5import './follows' 5import './follows'
6import './follows-moderation'
6import './handle-down' 7import './handle-down'
7import './jobs' 8import './jobs'
9import './logs'
8import './reverse-proxy' 10import './reverse-proxy'
9import './stats' 11import './stats'
10import './tracker' 12import './tracker'
diff --git a/server/tests/api/server/jobs.ts b/server/tests/api/server/jobs.ts
index 52948b1d6..634654626 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 '../../../../shared/utils/index' 5import { cleanupTests, killallServers, ServerInfo, setAccessTokensToServers } from '../../../../shared/extra-utils/index'
6import { doubleFollow } from '../../../../shared/utils/server/follows' 6import { doubleFollow } from '../../../../shared/extra-utils/server/follows'
7import { getJobsList, getJobsListPaginationAndSort, waitJobs } from '../../../../shared/utils/server/jobs' 7import { getJobsList, getJobsListPaginationAndSort, waitJobs } from '../../../../shared/extra-utils/server/jobs'
8import { flushAndRunMultipleServers } from '../../../../shared/utils/server/servers' 8import { flushAndRunMultipleServers } from '../../../../shared/extra-utils/server/servers'
9import { uploadVideo } from '../../../../shared/utils/videos/videos' 9import { uploadVideo } from '../../../../shared/extra-utils/videos/videos'
10import { dateIsValid } from '../../../../shared/utils/miscs/miscs' 10import { dateIsValid } from '../../../../shared/extra-utils/miscs/miscs'
11 11
12const expect = chai.expect 12const expect = chai.expect
13 13
@@ -57,6 +57,6 @@ describe('Test jobs', function () {
57 }) 57 })
58 58
59 after(async function () { 59 after(async function () {
60 killallServers(servers) 60 await cleanupTests(servers)
61 }) 61 })
62}) 62})
diff --git a/server/tests/api/server/logs.ts b/server/tests/api/server/logs.ts
new file mode 100644
index 000000000..3644fa0d3
--- /dev/null
+++ b/server/tests/api/server/logs.ts
@@ -0,0 +1,97 @@
1/* tslint:disable:no-unused-expression */
2
3import * as chai from 'chai'
4import 'mocha'
5import {
6 flushTests,
7 killallServers,
8 flushAndRunServer,
9 ServerInfo,
10 setAccessTokensToServers,
11 cleanupTests
12} from '../../../../shared/extra-utils/index'
13import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
14import { uploadVideo } from '../../../../shared/extra-utils/videos/videos'
15import { getLogs } from '../../../../shared/extra-utils/logs/logs'
16
17const expect = chai.expect
18
19describe('Test logs', function () {
20 let server: ServerInfo
21
22 before(async function () {
23 this.timeout(30000)
24
25 server = await flushAndRunServer(1)
26 await setAccessTokensToServers([ server ])
27 })
28
29 it('Should get logs with a start date', async function () {
30 this.timeout(10000)
31
32 await uploadVideo(server.url, server.accessToken, { name: 'video 1' })
33 await waitJobs([ server ])
34
35 const now = new Date()
36
37 await uploadVideo(server.url, server.accessToken, { name: 'video 2' })
38 await waitJobs([ server ])
39
40 const res = await getLogs(server.url, server.accessToken, now)
41 const logsString = JSON.stringify(res.body)
42
43 expect(logsString.includes('video 1')).to.be.false
44 expect(logsString.includes('video 2')).to.be.true
45 })
46
47 it('Should get logs with an end date', async function () {
48 this.timeout(10000)
49
50 await uploadVideo(server.url, server.accessToken, { name: 'video 3' })
51 await waitJobs([ server ])
52
53 const now1 = new Date()
54
55 await uploadVideo(server.url, server.accessToken, { name: 'video 4' })
56 await waitJobs([ server ])
57
58 const now2 = new Date()
59
60 await uploadVideo(server.url, server.accessToken, { name: 'video 5' })
61 await waitJobs([ server ])
62
63 const res = await getLogs(server.url, server.accessToken, now1, now2)
64 const logsString = JSON.stringify(res.body)
65
66 expect(logsString.includes('video 3')).to.be.false
67 expect(logsString.includes('video 4')).to.be.true
68 expect(logsString.includes('video 5')).to.be.false
69 })
70
71 it('Should get filter by level', async function () {
72 this.timeout(10000)
73
74 const now = new Date()
75
76 await uploadVideo(server.url, server.accessToken, { name: 'video 6' })
77 await waitJobs([ server ])
78
79 {
80 const res = await getLogs(server.url, server.accessToken, now, undefined, 'info')
81 const logsString = JSON.stringify(res.body)
82
83 expect(logsString.includes('video 6')).to.be.true
84 }
85
86 {
87 const res = await getLogs(server.url, server.accessToken, now, undefined, 'warn')
88 const logsString = JSON.stringify(res.body)
89
90 expect(logsString.includes('video 6')).to.be.false
91 }
92 })
93
94 after(async function () {
95 await cleanupTests([ server ])
96 })
97})
diff --git a/server/tests/api/server/no-client.ts b/server/tests/api/server/no-client.ts
index 3b95ce945..86edeb289 100644
--- a/server/tests/api/server/no-client.ts
+++ b/server/tests/api/server/no-client.ts
@@ -1,11 +1,7 @@
1import 'mocha' 1import 'mocha'
2import * as request from 'supertest' 2import * as request from 'supertest'
3import { 3import { ServerInfo } from '../../../../shared/extra-utils'
4 flushTests, 4import { cleanupTests, flushAndRunServer } from '../../../../shared/extra-utils/server/servers'
5 killallServers,
6 ServerInfo
7} from '../../../../shared/utils'
8import { runServer } from '../../../../shared/utils/server/servers'
9 5
10describe('Start and stop server without web client routes', function () { 6describe('Start and stop server without web client routes', function () {
11 let server: ServerInfo 7 let server: ServerInfo
@@ -13,9 +9,7 @@ describe('Start and stop server without web client routes', function () {
13 before(async function () { 9 before(async function () {
14 this.timeout(30000) 10 this.timeout(30000)
15 11
16 await flushTests() 12 server = await flushAndRunServer(1, {}, ['--no-client'])
17
18 server = await runServer(1, {}, ['--no-client'])
19 }) 13 })
20 14
21 it('Should fail getting the client', function () { 15 it('Should fail getting the client', function () {
@@ -26,11 +20,6 @@ describe('Start and stop server without web client routes', function () {
26 }) 20 })
27 21
28 after(async function () { 22 after(async function () {
29 killallServers([ server ]) 23 await cleanupTests([ server ])
30
31 // Keep the logs if the test failed
32 if (this['ok']) {
33 await flushTests()
34 }
35 }) 24 })
36}) 25})
diff --git a/server/tests/api/server/reverse-proxy.ts b/server/tests/api/server/reverse-proxy.ts
index d4c08c346..987538237 100644
--- a/server/tests/api/server/reverse-proxy.ts
+++ b/server/tests/api/server/reverse-proxy.ts
@@ -2,28 +2,10 @@
2 2
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { About } from '../../../../shared/models/server/about.model' 5import { cleanupTests, getVideo, uploadVideo, userLogin, viewVideo, wait } from '../../../../shared/extra-utils'
6import { CustomConfig } from '../../../../shared/models/server/custom-config.model' 6import { flushAndRunServer, setAccessTokensToServers } from '../../../../shared/extra-utils/index'
7import {
8 deleteCustomConfig,
9 getAbout,
10 getVideo,
11 killallServers,
12 login,
13 reRunServer,
14 uploadVideo,
15 userLogin,
16 viewVideo,
17 wait
18} from '../../../../shared/utils'
19const expect = chai.expect
20 7
21import { 8const expect = chai.expect
22 getConfig,
23 flushTests,
24 runServer,
25 registerUser, getCustomConfig, setAccessTokensToServers, updateCustomConfig
26} from '../../../../shared/utils/index'
27 9
28describe('Test application behind a reverse proxy', function () { 10describe('Test application behind a reverse proxy', function () {
29 let server = null 11 let server = null
@@ -31,9 +13,7 @@ describe('Test application behind a reverse proxy', function () {
31 13
32 before(async function () { 14 before(async function () {
33 this.timeout(30000) 15 this.timeout(30000)
34 16 server = await flushAndRunServer(1)
35 await flushTests()
36 server = await runServer(1)
37 await setAccessTokensToServers([ server ]) 17 await setAccessTokensToServers([ server ])
38 18
39 const { body } = await uploadVideo(server.url, server.accessToken, {}) 19 const { body } = await uploadVideo(server.url, server.accessToken, {})
@@ -95,7 +75,7 @@ describe('Test application behind a reverse proxy', function () {
95 it('Should rate limit logins', async function () { 75 it('Should rate limit logins', async function () {
96 const user = { username: 'root', password: 'fail' } 76 const user = { username: 'root', password: 'fail' }
97 77
98 for (let i = 0; i < 14; i++) { 78 for (let i = 0; i < 19; i++) {
99 await userLogin(server, user, 400) 79 await userLogin(server, user, 400)
100 } 80 }
101 81
@@ -103,6 +83,6 @@ describe('Test application behind a reverse proxy', function () {
103 }) 83 })
104 84
105 after(async function () { 85 after(async function () {
106 killallServers([ server ]) 86 await cleanupTests([ server ])
107 }) 87 })
108}) 88})
diff --git a/server/tests/api/server/stats.ts b/server/tests/api/server/stats.ts
index aaa6c62f7..a01cd4b38 100644
--- a/server/tests/api/server/stats.ts
+++ b/server/tests/api/server/stats.ts
@@ -4,6 +4,7 @@ import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { ServerStats } from '../../../../shared/models/server/server-stats.model' 5import { ServerStats } from '../../../../shared/models/server/server-stats.model'
6import { 6import {
7 cleanupTests,
7 createUser, 8 createUser,
8 doubleFollow, 9 doubleFollow,
9 flushAndRunMultipleServers, 10 flushAndRunMultipleServers,
@@ -13,11 +14,11 @@ import {
13 uploadVideo, 14 uploadVideo,
14 viewVideo, 15 viewVideo,
15 wait 16 wait
16} from '../../../../shared/utils' 17} from '../../../../shared/extra-utils'
17import { flushTests, setAccessTokensToServers } from '../../../../shared/utils/index' 18import { flushTests, setAccessTokensToServers } from '../../../../shared/extra-utils/index'
18import { getStats } from '../../../../shared/utils/server/stats' 19import { getStats } from '../../../../shared/extra-utils/server/stats'
19import { addVideoCommentThread } from '../../../../shared/utils/videos/video-comments' 20import { addVideoCommentThread } from '../../../../shared/extra-utils/videos/video-comments'
20import { waitJobs } from '../../../../shared/utils/server/jobs' 21import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
21 22
22const expect = chai.expect 23const expect = chai.expect
23 24
@@ -26,8 +27,6 @@ describe('Test stats (excluding redundancy)', function () {
26 27
27 before(async function () { 28 before(async function () {
28 this.timeout(60000) 29 this.timeout(60000)
29
30 await flushTests()
31 servers = await flushAndRunMultipleServers(3) 30 servers = await flushAndRunMultipleServers(3)
32 await setAccessTokensToServers(servers) 31 await setAccessTokensToServers(servers)
33 32
@@ -37,7 +36,7 @@ describe('Test stats (excluding redundancy)', function () {
37 username: 'user1', 36 username: 'user1',
38 password: 'super_password' 37 password: 'super_password'
39 } 38 }
40 await createUser(servers[0].url, servers[0].accessToken, user.username, user.password) 39 await createUser({ url: servers[ 0 ].url, accessToken: servers[ 0 ].accessToken, username: user.username, password: user.password })
41 40
42 const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, { fixture: 'video_short.webm' }) 41 const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, { fixture: 'video_short.webm' })
43 const videoUUID = resVideo.body.video.uuid 42 const videoUUID = resVideo.body.video.uuid
@@ -98,6 +97,6 @@ describe('Test stats (excluding redundancy)', function () {
98 }) 97 })
99 98
100 after(async function () { 99 after(async function () {
101 killallServers(servers) 100 await cleanupTests(servers)
102 }) 101 })
103}) 102})
diff --git a/server/tests/api/server/tracker.ts b/server/tests/api/server/tracker.ts
index 25ca00029..9d7eec8ca 100644
--- a/server/tests/api/server/tracker.ts
+++ b/server/tests/api/server/tracker.ts
@@ -2,8 +2,16 @@
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 '../../../../shared/utils' 5import {
6import { flushTests, setAccessTokensToServers } from '../../../../shared/utils/index' 6 cleanupTests,
7 flushAndRunServer,
8 getVideo,
9 killallServers,
10 reRunServer,
11 ServerInfo,
12 uploadVideo
13} from '../../../../shared/extra-utils'
14import { setAccessTokensToServers } from '../../../../shared/extra-utils/index'
7import { VideoDetails } from '../../../../shared/models/videos' 15import { VideoDetails } from '../../../../shared/models/videos'
8import * as WebTorrent from 'webtorrent' 16import * as WebTorrent from 'webtorrent'
9 17
@@ -14,9 +22,7 @@ describe('Test tracker', function () {
14 22
15 before(async function () { 23 before(async function () {
16 this.timeout(60000) 24 this.timeout(60000)
17 25 server = await flushAndRunServer(1)
18 await flushTests()
19 server = await runServer(1)
20 await setAccessTokensToServers([ server ]) 26 await setAccessTokensToServers([ server ])
21 27
22 { 28 {
@@ -34,7 +40,7 @@ describe('Test tracker', function () {
34 } 40 }
35 }) 41 })
36 42
37 it('Should return an error when adding an incorrect infohash', done => { 43 it('Should return an error when adding an incorrect infohash', function (done) {
38 this.timeout(10000) 44 this.timeout(10000)
39 const webtorrent = new WebTorrent() 45 const webtorrent = new WebTorrent()
40 46
@@ -49,7 +55,7 @@ describe('Test tracker', function () {
49 torrent.on('done', () => done(new Error('No error on infohash'))) 55 torrent.on('done', () => done(new Error('No error on infohash')))
50 }) 56 })
51 57
52 it('Should succeed with the correct infohash', done => { 58 it('Should succeed with the correct infohash', function (done) {
53 this.timeout(10000) 59 this.timeout(10000)
54 const webtorrent = new WebTorrent() 60 const webtorrent = new WebTorrent()
55 61
@@ -64,7 +70,27 @@ describe('Test tracker', function () {
64 torrent.on('done', done) 70 torrent.on('done', done)
65 }) 71 })
66 72
67 after(async function () { 73 it('Should disable the tracker', function (done) {
74 this.timeout(20000)
75
68 killallServers([ server ]) 76 killallServers([ server ])
77 reRunServer(server, { tracker: { enabled: false } })
78 .then(() => {
79 const webtorrent = new WebTorrent()
80
81 const torrent = webtorrent.add(goodMagnet)
82
83 torrent.on('error', done)
84 torrent.on('warning', warn => {
85 const message = typeof warn === 'string' ? warn : warn.message
86 if (message.indexOf('disabled ') !== -1) return done()
87 })
88
89 torrent.on('done', () => done(new Error('Tracker is enabled')))
90 })
91 })
92
93 after(async function () {
94 await cleanupTests([ server ])
69 }) 95 })
70}) 96})
diff --git a/server/tests/api/users/blocklist.ts b/server/tests/api/users/blocklist.ts
index 4bca27a94..fbc57e0ef 100644
--- a/server/tests/api/users/blocklist.ts
+++ b/server/tests/api/users/blocklist.ts
@@ -4,6 +4,7 @@ import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { AccountBlock, ServerBlock, Video } from '../../../../shared/index' 5import { AccountBlock, ServerBlock, Video } from '../../../../shared/index'
6import { 6import {
7 cleanupTests,
7 createUser, 8 createUser,
8 doubleFollow, 9 doubleFollow,
9 flushAndRunMultipleServers, 10 flushAndRunMultipleServers,
@@ -12,16 +13,16 @@ import {
12 ServerInfo, 13 ServerInfo,
13 uploadVideo, 14 uploadVideo,
14 userLogin 15 userLogin
15} from '../../../../shared/utils/index' 16} from '../../../../shared/extra-utils/index'
16import { setAccessTokensToServers } from '../../../../shared/utils/users/login' 17import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login'
17import { getVideosListWithToken, getVideosList } from '../../../../shared/utils/videos/videos' 18import { getVideosListWithToken, getVideosList } from '../../../../shared/extra-utils/videos/videos'
18import { 19import {
19 addVideoCommentReply, 20 addVideoCommentReply,
20 addVideoCommentThread, 21 addVideoCommentThread,
21 getVideoCommentThreads, 22 getVideoCommentThreads,
22 getVideoThreadComments 23 getVideoThreadComments
23} from '../../../../shared/utils/videos/video-comments' 24} from '../../../../shared/extra-utils/videos/video-comments'
24import { waitJobs } from '../../../../shared/utils/server/jobs' 25import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
25import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model' 26import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model'
26import { 27import {
27 addAccountToAccountBlocklist, 28 addAccountToAccountBlocklist,
@@ -36,7 +37,7 @@ import {
36 removeAccountFromServerBlocklist, 37 removeAccountFromServerBlocklist,
37 removeServerFromAccountBlocklist, 38 removeServerFromAccountBlocklist,
38 removeServerFromServerBlocklist 39 removeServerFromServerBlocklist
39} from '../../../../shared/utils/users/blocklist' 40} from '../../../../shared/extra-utils/users/blocklist'
40 41
41const expect = chai.expect 42const expect = chai.expect
42 43
@@ -79,14 +80,12 @@ describe('Test blocklist', function () {
79 before(async function () { 80 before(async function () {
80 this.timeout(60000) 81 this.timeout(60000)
81 82
82 await flushTests()
83
84 servers = await flushAndRunMultipleServers(2) 83 servers = await flushAndRunMultipleServers(2)
85 await setAccessTokensToServers(servers) 84 await setAccessTokensToServers(servers)
86 85
87 { 86 {
88 const user = { username: 'user1', password: 'password' } 87 const user = { username: 'user1', password: 'password' }
89 await createUser(servers[0].url, servers[0].accessToken, user.username, user.password) 88 await createUser({ url: servers[ 0 ].url, accessToken: servers[ 0 ].accessToken, username: user.username, password: user.password })
90 89
91 userToken1 = await userLogin(servers[0], user) 90 userToken1 = await userLogin(servers[0], user)
92 await uploadVideo(servers[0].url, userToken1, { name: 'video user 1' }) 91 await uploadVideo(servers[0].url, userToken1, { name: 'video user 1' })
@@ -94,14 +93,14 @@ describe('Test blocklist', function () {
94 93
95 { 94 {
96 const user = { username: 'moderator', password: 'password' } 95 const user = { username: 'moderator', password: 'password' }
97 await createUser(servers[0].url, servers[0].accessToken, user.username, user.password) 96 await createUser({ url: servers[ 0 ].url, accessToken: servers[ 0 ].accessToken, username: user.username, password: user.password })
98 97
99 userModeratorToken = await userLogin(servers[0], user) 98 userModeratorToken = await userLogin(servers[0], user)
100 } 99 }
101 100
102 { 101 {
103 const user = { username: 'user2', password: 'password' } 102 const user = { username: 'user2', password: 'password' }
104 await createUser(servers[1].url, servers[1].accessToken, user.username, user.password) 103 await createUser({ url: servers[ 1 ].url, accessToken: servers[ 1 ].accessToken, username: user.username, password: user.password })
105 104
106 userToken2 = await userLogin(servers[1], user) 105 userToken2 = await userLogin(servers[1], user)
107 await uploadVideo(servers[1].url, userToken2, { name: 'video user 2' }) 106 await uploadVideo(servers[1].url, userToken2, { name: 'video user 2' })
@@ -501,11 +500,6 @@ describe('Test blocklist', function () {
501 }) 500 })
502 501
503 after(async function () { 502 after(async function () {
504 killallServers(servers) 503 await cleanupTests(servers)
505
506 // Keep the logs if the test failed
507 if (this[ 'ok' ]) {
508 await flushTests()
509 }
510 }) 504 })
511}) 505})
diff --git a/server/tests/api/users/index.ts b/server/tests/api/users/index.ts
index 52ba6984e..fcd022429 100644
--- a/server/tests/api/users/index.ts
+++ b/server/tests/api/users/index.ts
@@ -1,5 +1,4 @@
1import './users-verification' 1import './users-verification'
2import './user-notifications'
3import './blocklist' 2import './blocklist'
4import './user-subscriptions' 3import './user-subscriptions'
5import './users' 4import './users'
diff --git a/server/tests/api/users/user-subscriptions.ts b/server/tests/api/users/user-subscriptions.ts
index 88a7187d6..48811e647 100644
--- a/server/tests/api/users/user-subscriptions.ts
+++ b/server/tests/api/users/user-subscriptions.ts
@@ -3,6 +3,7 @@
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { 5import {
6 cleanupTests,
6 createUser, 7 createUser,
7 doubleFollow, 8 doubleFollow,
8 flushAndRunMultipleServers, 9 flushAndRunMultipleServers,
@@ -11,18 +12,18 @@ import {
11 unfollow, 12 unfollow,
12 updateVideo, 13 updateVideo,
13 userLogin 14 userLogin
14} from '../../../../shared/utils' 15} from '../../../../shared/extra-utils'
15import { killallServers, ServerInfo, uploadVideo } from '../../../../shared/utils/index' 16import { killallServers, ServerInfo, uploadVideo } from '../../../../shared/extra-utils/index'
16import { setAccessTokensToServers } from '../../../../shared/utils/users/login' 17import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login'
17import { Video, VideoChannel } from '../../../../shared/models/videos' 18import { Video, VideoChannel } from '../../../../shared/models/videos'
18import { waitJobs } from '../../../../shared/utils/server/jobs' 19import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
19import { 20import {
20 addUserSubscription, 21 addUserSubscription,
21 listUserSubscriptions, 22 listUserSubscriptions,
22 listUserSubscriptionVideos, 23 listUserSubscriptionVideos,
23 removeUserSubscription, 24 removeUserSubscription,
24 getUserSubscription, areSubscriptionsExist 25 getUserSubscription, areSubscriptionsExist
25} from '../../../../shared/utils/users/user-subscriptions' 26} from '../../../../shared/extra-utils/users/user-subscriptions'
26 27
27const expect = chai.expect 28const expect = chai.expect
28 29
@@ -45,7 +46,7 @@ describe('Test users subscriptions', function () {
45 { 46 {
46 for (const server of servers) { 47 for (const server of servers) {
47 const user = { username: 'user' + server.serverNumber, password: 'password' } 48 const user = { username: 'user' + server.serverNumber, password: 'password' }
48 await createUser(server.url, server.accessToken, user.username, user.password) 49 await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
49 50
50 const accessToken = await userLogin(server, user) 51 const accessToken = await userLogin(server, user)
51 users.push({ accessToken }) 52 users.push({ accessToken })
@@ -369,6 +370,6 @@ describe('Test users subscriptions', function () {
369 }) 370 })
370 371
371 after(async function () { 372 after(async function () {
372 killallServers(servers) 373 await cleanupTests(servers)
373 }) 374 })
374}) 375})
diff --git a/server/tests/api/users/users-multiple-servers.ts b/server/tests/api/users/users-multiple-servers.ts
index 006d6cdf0..9a971adb3 100644
--- a/server/tests/api/users/users-multiple-servers.ts
+++ b/server/tests/api/users/users-multiple-servers.ts
@@ -4,7 +4,8 @@ import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { Account } from '../../../../shared/models/actors' 5import { Account } from '../../../../shared/models/actors'
6import { 6import {
7 checkVideoFilesWereRemoved, 7 checkTmpIsEmpty,
8 checkVideoFilesWereRemoved, cleanupTests,
8 createUser, 9 createUser,
9 doubleFollow, 10 doubleFollow,
10 flushAndRunMultipleServers, 11 flushAndRunMultipleServers,
@@ -13,13 +14,20 @@ import {
13 removeUser, 14 removeUser,
14 updateMyUser, 15 updateMyUser,
15 userLogin 16 userLogin
16} from '../../../../shared/utils' 17} from '../../../../shared/extra-utils'
17import { getMyUserInformation, killallServers, ServerInfo, testImage, updateMyAvatar, uploadVideo } from '../../../../shared/utils/index' 18import {
18import { checkActorFilesWereRemoved, getAccount, getAccountsList } from '../../../../shared/utils/users/accounts' 19 getMyUserInformation,
19import { setAccessTokensToServers } from '../../../../shared/utils/users/login' 20 killallServers,
21 ServerInfo,
22 testImage,
23 updateMyAvatar,
24 uploadVideo
25} from '../../../../shared/extra-utils/index'
26import { checkActorFilesWereRemoved, getAccount, getAccountsList } from '../../../../shared/extra-utils/users/accounts'
27import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login'
20import { User } from '../../../../shared/models/users' 28import { User } from '../../../../shared/models/users'
21import { VideoChannel } from '../../../../shared/models/videos' 29import { VideoChannel } from '../../../../shared/models/videos'
22import { waitJobs } from '../../../../shared/utils/server/jobs' 30import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
23 31
24const expect = chai.expect 32const expect = chai.expect
25 33
@@ -56,7 +64,12 @@ describe('Test users with multiple servers', function () {
56 username: 'user1', 64 username: 'user1',
57 password: 'password' 65 password: 'password'
58 } 66 }
59 const res = await createUser(servers[ 0 ].url, servers[ 0 ].accessToken, user.username, user.password) 67 const res = await createUser({
68 url: servers[ 0 ].url,
69 accessToken: servers[ 0 ].accessToken,
70 username: user.username,
71 password: user.password
72 })
60 userId = res.body.user.id 73 userId = res.body.user.id
61 userAccessToken = await userLogin(servers[ 0 ], user) 74 userAccessToken = await userLogin(servers[ 0 ], user)
62 } 75 }
@@ -216,7 +229,13 @@ describe('Test users with multiple servers', function () {
216 } 229 }
217 }) 230 })
218 231
232 it('Should have an empty tmp directory', async function () {
233 for (const server of servers) {
234 await checkTmpIsEmpty(server)
235 }
236 })
237
219 after(async function () { 238 after(async function () {
220 killallServers(servers) 239 await cleanupTests(servers)
221 }) 240 })
222}) 241})
diff --git a/server/tests/api/users/users-verification.ts b/server/tests/api/users/users-verification.ts
index babeda2b8..514acf2e7 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, wait 7 userLogin, login, flushAndRunServer, ServerInfo, verifyEmail, updateCustomSubConfig, wait, cleanupTests
8} from '../../../../shared/utils' 8} from '../../../../shared/extra-utils'
9import { setAccessTokensToServers } from '../../../../shared/utils/users/login' 9import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login'
10import { MockSmtpServer } from '../../../../shared/utils/miscs/email' 10import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email'
11import { waitJobs } from '../../../../shared/utils/server/jobs' 11import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
12 12
13const expect = chai.expect 13const expect = chai.expect
14 14
@@ -32,14 +32,12 @@ describe('Test users account verification', function () {
32 32
33 await MockSmtpServer.Instance.collectEmails(emails) 33 await MockSmtpServer.Instance.collectEmails(emails)
34 34
35 await flushTests()
36
37 const overrideConfig = { 35 const overrideConfig = {
38 smtp: { 36 smtp: {
39 hostname: 'localhost' 37 hostname: 'localhost'
40 } 38 }
41 } 39 }
42 server = await runServer(1, overrideConfig) 40 server = await flushAndRunServer(1, overrideConfig)
43 41
44 await setAccessTokensToServers([ server ]) 42 await setAccessTokensToServers([ server ])
45 }) 43 })
@@ -124,11 +122,7 @@ describe('Test users account verification', function () {
124 122
125 after(async function () { 123 after(async function () {
126 MockSmtpServer.Instance.kill() 124 MockSmtpServer.Instance.kill()
127 killallServers([ server ])
128 125
129 // Keep the logs if the test failed 126 await cleanupTests([ server ])
130 if (this[ 'ok' ]) {
131 await flushTests()
132 }
133 }) 127 })
134}) 128})
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts
index ad98ab1c7..c8e32f3f5 100644
--- a/server/tests/api/users/users.ts
+++ b/server/tests/api/users/users.ts
@@ -2,12 +2,14 @@
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { User, UserRole } from '../../../../shared/index' 5import { User, UserRole, Video } from '../../../../shared/index'
6import { 6import {
7 blockUser, 7 blockUser,
8 cleanupTests,
8 createUser, 9 createUser,
9 deleteMe, 10 deleteMe,
10 flushTests, 11 flushAndRunServer,
12 getAccountRatings,
11 getBlacklistedVideosList, 13 getBlacklistedVideosList,
12 getMyUserInformation, 14 getMyUserInformation,
13 getMyUserVideoQuotaUsed, 15 getMyUserVideoQuotaUsed,
@@ -16,14 +18,12 @@ import {
16 getUsersList, 18 getUsersList,
17 getUsersListPaginationAndSort, 19 getUsersListPaginationAndSort,
18 getVideosList, 20 getVideosList,
19 killallServers,
20 login, 21 login,
21 makePutBodyRequest, 22 makePutBodyRequest,
22 rateVideo, 23 rateVideo,
23 registerUser, 24 registerUser,
24 removeUser, 25 removeUser,
25 removeVideo, 26 removeVideo,
26 runServer,
27 ServerInfo, 27 ServerInfo,
28 testImage, 28 testImage,
29 unblockUser, 29 unblockUser,
@@ -32,10 +32,11 @@ import {
32 updateUser, 32 updateUser,
33 uploadVideo, 33 uploadVideo,
34 userLogin 34 userLogin
35} from '../../../../shared/utils/index' 35} from '../../../../shared/extra-utils'
36import { follow } from '../../../../shared/utils/server/follows' 36import { follow } from '../../../../shared/extra-utils/server/follows'
37import { setAccessTokensToServers } from '../../../../shared/utils/users/login' 37import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login'
38import { getMyVideos } from '../../../../shared/utils/videos/videos' 38import { getMyVideos } from '../../../../shared/extra-utils/videos/videos'
39import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
39 40
40const expect = chai.expect 41const expect = chai.expect
41 42
@@ -52,537 +53,633 @@ describe('Test users', function () {
52 53
53 before(async function () { 54 before(async function () {
54 this.timeout(30000) 55 this.timeout(30000)
55 56 server = await flushAndRunServer(1)
56 await flushTests()
57 server = await runServer(1)
58 57
59 await setAccessTokensToServers([ server ]) 58 await setAccessTokensToServers([ server ])
60 }) 59 })
61 60
62 it('Should create a new client') 61 describe('OAuth client', function () {
62 it('Should create a new client')
63 63
64 it('Should return the first client') 64 it('Should return the first client')
65 65
66 it('Should remove the last client') 66 it('Should remove the last client')
67 67
68 it('Should not login with an invalid client id', async function () { 68 it('Should not login with an invalid client id', async function () {
69 const client = { id: 'client', secret: server.client.secret } 69 const client = { id: 'client', secret: server.client.secret }
70 const res = await login(server.url, client, server.user, 400) 70 const res = await login(server.url, client, server.user, 400)
71 71
72 expect(res.body.error).to.contain('client is invalid') 72 expect(res.body.error).to.contain('client is invalid')
73 }) 73 })
74 74
75 it('Should not login with an invalid client secret', async function () { 75 it('Should not login with an invalid client secret', async function () {
76 const client = { id: server.client.id, secret: 'coucou' } 76 const client = { id: server.client.id, secret: 'coucou' }
77 const res = await login(server.url, client, server.user, 400) 77 const res = await login(server.url, client, server.user, 400)
78 78
79 expect(res.body.error).to.contain('client is invalid') 79 expect(res.body.error).to.contain('client is invalid')
80 })
80 }) 81 })
81 82
82 it('Should not login with an invalid username', async function () { 83 describe('Login', function () {
83 const user = { username: 'captain crochet', password: server.user.password }
84 const res = await login(server.url, server.client, user, 400)
85 84
86 expect(res.body.error).to.contain('credentials are invalid') 85 it('Should not login with an invalid username', async function () {
87 }) 86 const user = { username: 'captain crochet', password: server.user.password }
88 87 const res = await login(server.url, server.client, user, 400)
89 it('Should not login with an invalid password', async function () {
90 const user = { username: server.user.username, password: 'mew_three' }
91 const res = await login(server.url, server.client, user, 400)
92 88
93 expect(res.body.error).to.contain('credentials are invalid') 89 expect(res.body.error).to.contain('credentials are invalid')
94 }) 90 })
95 91
96 it('Should not be able to upload a video', async function () { 92 it('Should not login with an invalid password', async function () {
97 accessToken = 'my_super_token' 93 const user = { username: server.user.username, password: 'mew_three' }
94 const res = await login(server.url, server.client, user, 400)
98 95
99 const videoAttributes = {} 96 expect(res.body.error).to.contain('credentials are invalid')
100 await uploadVideo(server.url, accessToken, videoAttributes, 401) 97 })
101 })
102 98
103 it('Should not be able to follow', async function () { 99 it('Should not be able to upload a video', async function () {
104 accessToken = 'my_super_token' 100 accessToken = 'my_super_token'
105 await follow(server.url, [ 'http://example.com' ], accessToken, 401)
106 })
107 101
108 it('Should not be able to unfollow') 102 const videoAttributes = {}
103 await uploadVideo(server.url, accessToken, videoAttributes, 401)
104 })
109 105
110 it('Should be able to login', async function () { 106 it('Should not be able to follow', async function () {
111 const res = await login(server.url, server.client, server.user, 200) 107 accessToken = 'my_super_token'
108 await follow(server.url, [ 'http://example.com' ], accessToken, 401)
109 })
112 110
113 accessToken = res.body.access_token 111 it('Should not be able to unfollow')
114 })
115 112
116 it('Should upload the video with the correct token', async function () { 113 it('Should be able to login', async function () {
117 const videoAttributes = {} 114 const res = await login(server.url, server.client, server.user, 200)
118 await uploadVideo(server.url, accessToken, videoAttributes)
119 const res = await getVideosList(server.url)
120 const video = res.body.data[ 0 ]
121 115
122 expect(video.account.name).to.equal('root') 116 accessToken = res.body.access_token
123 videoId = video.id 117 })
124 }) 118 })
125 119
126 it('Should upload the video again with the correct token', async function () { 120 describe('Upload', function () {
127 const videoAttributes = {}
128 await uploadVideo(server.url, accessToken, videoAttributes)
129 })
130 121
131 it('Should retrieve a video rating', async function () { 122 it('Should upload the video with the correct token', async function () {
132 await rateVideo(server.url, accessToken, videoId, 'like') 123 const videoAttributes = {}
133 const res = await getMyUserVideoRating(server.url, accessToken, videoId) 124 await uploadVideo(server.url, accessToken, videoAttributes)
134 const rating = res.body 125 const res = await getVideosList(server.url)
126 const video = res.body.data[ 0 ]
135 127
136 expect(rating.videoId).to.equal(videoId) 128 expect(video.account.name).to.equal('root')
137 expect(rating.rating).to.equal('like') 129 videoId = video.id
138 }) 130 })
139 131
140 it('Should not be able to remove the video with an incorrect token', async function () { 132 it('Should upload the video again with the correct token', async function () {
141 await removeVideo(server.url, 'bad_token', videoId, 401) 133 const videoAttributes = {}
134 await uploadVideo(server.url, accessToken, videoAttributes)
135 })
142 }) 136 })
143 137
144 it('Should not be able to remove the video with the token of another account') 138 describe('Ratings', function () {
145 139
146 it('Should be able to remove the video with the correct token', async function () { 140 it('Should retrieve a video rating', async function () {
147 await removeVideo(server.url, accessToken, videoId) 141 await rateVideo(server.url, accessToken, videoId, 'like')
148 }) 142 const res = await getMyUserVideoRating(server.url, accessToken, videoId)
143 const rating = res.body
149 144
150 it('Should logout (revoke token)') 145 expect(rating.videoId).to.equal(videoId)
151 146 expect(rating.rating).to.equal('like')
152 it('Should not be able to get the user information') 147 })
153 148
154 it('Should not be able to upload a video') 149 it('Should retrieve ratings list', async function () {
150 await rateVideo(server.url, accessToken, videoId, 'like')
155 151
156 it('Should not be able to remove a video') 152 const res = await getAccountRatings(server.url, server.user.username, server.accessToken, null, 200)
153 const ratings = res.body
157 154
158 it('Should not be able to rate a video', async function () { 155 expect(ratings.total).to.equal(1)
159 const path = '/api/v1/videos/' 156 expect(ratings.data[ 0 ].video.id).to.equal(videoId)
160 const data = { 157 expect(ratings.data[ 0 ].rating).to.equal('like')
161 rating: 'likes' 158 })
162 }
163 159
164 const options = { 160 it('Should retrieve ratings list by rating type', async function () {
165 url: server.url, 161 {
166 path: path + videoId, 162 const res = await getAccountRatings(server.url, server.user.username, server.accessToken, 'like')
167 token: 'wrong token', 163 const ratings = res.body
168 fields: data, 164 expect(ratings.data.length).to.equal(1)
169 statusCodeExpected: 401 165 }
170 } 166
171 await makePutBodyRequest(options) 167 {
168 const res = await getAccountRatings(server.url, server.user.username, server.accessToken, 'dislike')
169 const ratings = res.body
170 expect(ratings.data.length).to.equal(0)
171 }
172 })
172 }) 173 })
173 174
174 it('Should be able to login again') 175 describe('Remove video', function () {
176 it('Should not be able to remove the video with an incorrect token', async function () {
177 await removeVideo(server.url, 'bad_token', videoId, 401)
178 })
175 179
176 it('Should have an expired access token') 180 it('Should not be able to remove the video with the token of another account')
177 181
178 it('Should refresh the token') 182 it('Should be able to remove the video with the correct token', async function () {
183 await removeVideo(server.url, accessToken, videoId)
184 })
185 })
179 186
180 it('Should be able to upload a video again') 187 describe('Logout', function () {
188 it('Should logout (revoke token)')
181 189
182 it('Should be able to create a new user', async function () { 190 it('Should not be able to get the user information')
183 await createUser(server.url, accessToken, user.username, user.password, 2 * 1024 * 1024)
184 })
185 191
186 it('Should be able to login with this user', async function () { 192 it('Should not be able to upload a video')
187 accessTokenUser = await userLogin(server, user)
188 })
189 193
190 it('Should be able to get the user information', async function () { 194 it('Should not be able to remove a video')
191 const res = await getMyUserInformation(server.url, accessTokenUser)
192 const user = res.body
193
194 expect(user.username).to.equal('user_1')
195 expect(user.email).to.equal('user_1@example.com')
196 expect(user.nsfwPolicy).to.equal('display')
197 expect(user.videoQuota).to.equal(2 * 1024 * 1024)
198 expect(user.roleLabel).to.equal('User')
199 expect(user.id).to.be.a('number')
200 expect(user.account.displayName).to.equal('user_1')
201 expect(user.account.description).to.be.null
202 })
203 195
204 it('Should be able to upload a video with this user', async function () { 196 it('Should not be able to rate a video', async function () {
205 this.timeout(5000) 197 const path = '/api/v1/videos/'
198 const data = {
199 rating: 'likes'
200 }
206 201
207 const videoAttributes = { 202 const options = {
208 name: 'super user video', 203 url: server.url,
209 fixture: 'video_short.webm' 204 path: path + videoId,
210 } 205 token: 'wrong token',
211 await uploadVideo(server.url, accessTokenUser, videoAttributes) 206 fields: data,
212 }) 207 statusCodeExpected: 401
208 }
209 await makePutBodyRequest(options)
210 })
213 211
214 it('Should have video quota updated', async function () { 212 it('Should be able to login again')
215 const res = await getMyUserVideoQuotaUsed(server.url, accessTokenUser)
216 const data = res.body
217 213
218 expect(data.videoQuotaUsed).to.equal(218910) 214 it('Should have an expired access token')
219 215
220 const resUsers = await getUsersList(server.url, server.accessToken) 216 it('Should refresh the token')
221 217
222 const users: User[] = resUsers.body.data 218 it('Should be able to upload a video again')
223 const tmpUser = users.find(u => u.username === user.username)
224 expect(tmpUser.videoQuotaUsed).to.equal(218910)
225 }) 219 })
226 220
227 it('Should be able to list my videos', async function () { 221 describe('Creating a user', function () {
228 const res = await getMyVideos(server.url, accessTokenUser, 0, 5)
229 expect(res.body.total).to.equal(1)
230 222
231 const videos = res.body.data 223 it('Should be able to create a new user', async function () {
232 expect(videos).to.have.lengthOf(1) 224 await createUser({
225 url: server.url,
226 accessToken: accessToken,
227 username: user.username,
228 password: user.password,
229 videoQuota: 2 * 1024 * 1024,
230 adminFlags: UserAdminFlag.BY_PASS_VIDEO_AUTO_BLACKLIST
231 })
232 })
233 233
234 expect(videos[ 0 ].name).to.equal('super user video') 234 it('Should be able to login with this user', async function () {
235 accessTokenUser = await userLogin(server, user)
236 })
237
238 it('Should be able to get user information', async function () {
239 const res1 = await getMyUserInformation(server.url, accessTokenUser)
240 const userMe: User = res1.body
241
242 const res2 = await getUserInformation(server.url, server.accessToken, userMe.id)
243 const userGet: User = res2.body
244
245 for (const user of [ userMe, userGet ]) {
246 expect(user.username).to.equal('user_1')
247 expect(user.email).to.equal('user_1@example.com')
248 expect(user.nsfwPolicy).to.equal('display')
249 expect(user.videoQuota).to.equal(2 * 1024 * 1024)
250 expect(user.roleLabel).to.equal('User')
251 expect(user.id).to.be.a('number')
252 expect(user.account.displayName).to.equal('user_1')
253 expect(user.account.description).to.be.null
254 }
255
256 expect(userMe.adminFlags).to.be.undefined
257 expect(userGet.adminFlags).to.equal(UserAdminFlag.BY_PASS_VIDEO_AUTO_BLACKLIST)
258 })
235 }) 259 })
236 260
237 it('Should list all the users', async function () { 261 describe('My videos & quotas', function () {
238 const res = await getUsersList(server.url, server.accessToken)
239 const result = res.body
240 const total = result.total
241 const users = result.data
242 262
243 expect(total).to.equal(2) 263 it('Should be able to upload a video with this user', async function () {
244 expect(users).to.be.an('array') 264 this.timeout(5000)
245 expect(users.length).to.equal(2)
246 265
247 const user = users[ 0 ] 266 const videoAttributes = {
248 expect(user.username).to.equal('user_1') 267 name: 'super user video',
249 expect(user.email).to.equal('user_1@example.com') 268 fixture: 'video_short.webm'
250 expect(user.nsfwPolicy).to.equal('display') 269 }
270 await uploadVideo(server.url, accessTokenUser, videoAttributes)
271 })
251 272
252 const rootUser = users[ 1 ] 273 it('Should have video quota updated', async function () {
253 expect(rootUser.username).to.equal('root') 274 const res = await getMyUserVideoQuotaUsed(server.url, accessTokenUser)
254 expect(rootUser.email).to.equal('admin1@example.com') 275 const data = res.body
255 expect(user.nsfwPolicy).to.equal('display')
256 276
257 userId = user.id 277 expect(data.videoQuotaUsed).to.equal(218910)
258 })
259 278
260 it('Should list only the first user by username asc', async function () { 279 const resUsers = await getUsersList(server.url, server.accessToken)
261 const res = await getUsersListPaginationAndSort(server.url, server.accessToken, 0, 1, 'username')
262 280
263 const result = res.body 281 const users: User[] = resUsers.body.data
264 const total = result.total 282 const tmpUser = users.find(u => u.username === user.username)
265 const users = result.data 283 expect(tmpUser.videoQuotaUsed).to.equal(218910)
284 })
266 285
267 expect(total).to.equal(2) 286 it('Should be able to list my videos', async function () {
268 expect(users.length).to.equal(1) 287 const res = await getMyVideos(server.url, accessTokenUser, 0, 5)
288 expect(res.body.total).to.equal(1)
289
290 const videos = res.body.data
291 expect(videos).to.have.lengthOf(1)
269 292
270 const user = users[ 0 ] 293 const video: Video = videos[ 0 ]
271 expect(user.username).to.equal('root') 294 expect(video.name).to.equal('super user video')
272 expect(user.email).to.equal('admin1@example.com') 295 expect(video.thumbnailPath).to.not.be.null
273 expect(user.roleLabel).to.equal('Administrator') 296 expect(video.previewPath).to.not.be.null
274 expect(user.nsfwPolicy).to.equal('display') 297 })
275 }) 298 })
276 299
277 it('Should list only the first user by username desc', async function () { 300 describe('Users listing', function () {
278 const res = await getUsersListPaginationAndSort(server.url, server.accessToken, 0, 1, '-username')
279 const result = res.body
280 const total = result.total
281 const users = result.data
282 301
283 expect(total).to.equal(2) 302 it('Should list all the users', async function () {
284 expect(users.length).to.equal(1) 303 const res = await getUsersList(server.url, server.accessToken)
304 const result = res.body
305 const total = result.total
306 const users = result.data
285 307
286 const user = users[ 0 ] 308 expect(total).to.equal(2)
287 expect(user.username).to.equal('user_1') 309 expect(users).to.be.an('array')
288 expect(user.email).to.equal('user_1@example.com') 310 expect(users.length).to.equal(2)
289 expect(user.nsfwPolicy).to.equal('display')
290 })
291 311
292 it('Should list only the second user by createdAt desc', async function () { 312 const user = users[ 0 ]
293 const res = await getUsersListPaginationAndSort(server.url, server.accessToken, 0, 1, '-createdAt') 313 expect(user.username).to.equal('user_1')
294 const result = res.body 314 expect(user.email).to.equal('user_1@example.com')
295 const total = result.total 315 expect(user.nsfwPolicy).to.equal('display')
296 const users = result.data
297 316
298 expect(total).to.equal(2) 317 const rootUser = users[ 1 ]
299 expect(users.length).to.equal(1) 318 expect(rootUser.username).to.equal('root')
319 expect(rootUser.email).to.equal('admin1@example.com')
320 expect(user.nsfwPolicy).to.equal('display')
300 321
301 const user = users[ 0 ] 322 userId = user.id
302 expect(user.username).to.equal('user_1') 323 })
303 expect(user.email).to.equal('user_1@example.com')
304 expect(user.nsfwPolicy).to.equal('display')
305 })
306 324
307 it('Should list all the users by createdAt asc', async function () { 325 it('Should list only the first user by username asc', async function () {
308 const res = await getUsersListPaginationAndSort(server.url, server.accessToken, 0, 2, 'createdAt') 326 const res = await getUsersListPaginationAndSort(server.url, server.accessToken, 0, 1, 'username')
309 const result = res.body
310 const total = result.total
311 const users = result.data
312 327
313 expect(total).to.equal(2) 328 const result = res.body
314 expect(users.length).to.equal(2) 329 const total = result.total
330 const users = result.data
315 331
316 expect(users[ 0 ].username).to.equal('root') 332 expect(total).to.equal(2)
317 expect(users[ 0 ].email).to.equal('admin1@example.com') 333 expect(users.length).to.equal(1)
318 expect(users[ 0 ].nsfwPolicy).to.equal('display')
319 334
320 expect(users[ 1 ].username).to.equal('user_1') 335 const user = users[ 0 ]
321 expect(users[ 1 ].email).to.equal('user_1@example.com') 336 expect(user.username).to.equal('root')
322 expect(users[ 1 ].nsfwPolicy).to.equal('display') 337 expect(user.email).to.equal('admin1@example.com')
323 }) 338 expect(user.roleLabel).to.equal('Administrator')
339 expect(user.nsfwPolicy).to.equal('display')
340 })
324 341
325 it('Should search user by username', async function () { 342 it('Should list only the first user by username desc', async function () {
326 const res = await getUsersListPaginationAndSort(server.url, server.accessToken, 0, 2, 'createdAt', 'oot') 343 const res = await getUsersListPaginationAndSort(server.url, server.accessToken, 0, 1, '-username')
327 const users = res.body.data as User[] 344 const result = res.body
345 const total = result.total
346 const users = result.data
328 347
329 expect(res.body.total).to.equal(1) 348 expect(total).to.equal(2)
330 expect(users.length).to.equal(1) 349 expect(users.length).to.equal(1)
331 350
332 expect(users[ 0 ].username).to.equal('root') 351 const user = users[ 0 ]
333 }) 352 expect(user.username).to.equal('user_1')
353 expect(user.email).to.equal('user_1@example.com')
354 expect(user.nsfwPolicy).to.equal('display')
355 })
334 356
335 it('Should search user by email', async function () { 357 it('Should list only the second user by createdAt desc', async function () {
336 { 358 const res = await getUsersListPaginationAndSort(server.url, server.accessToken, 0, 1, '-createdAt')
337 const res = await getUsersListPaginationAndSort(server.url, server.accessToken, 0, 2, 'createdAt', 'r_1@exam') 359 const result = res.body
338 const users = res.body.data as User[] 360 const total = result.total
361 const users = result.data
339 362
340 expect(res.body.total).to.equal(1) 363 expect(total).to.equal(2)
341 expect(users.length).to.equal(1) 364 expect(users.length).to.equal(1)
342 365
343 expect(users[ 0 ].username).to.equal('user_1') 366 const user = users[ 0 ]
344 expect(users[ 0 ].email).to.equal('user_1@example.com') 367 expect(user.username).to.equal('user_1')
345 } 368 expect(user.email).to.equal('user_1@example.com')
369 expect(user.nsfwPolicy).to.equal('display')
370 })
346 371
347 { 372 it('Should list all the users by createdAt asc', async function () {
348 const res = await getUsersListPaginationAndSort(server.url, server.accessToken, 0, 2, 'createdAt', 'example') 373 const res = await getUsersListPaginationAndSort(server.url, server.accessToken, 0, 2, 'createdAt')
349 const users = res.body.data as User[] 374 const result = res.body
375 const total = result.total
376 const users = result.data
350 377
351 expect(res.body.total).to.equal(2) 378 expect(total).to.equal(2)
352 expect(users.length).to.equal(2) 379 expect(users.length).to.equal(2)
353 380
354 expect(users[ 0 ].username).to.equal('root') 381 expect(users[ 0 ].username).to.equal('root')
355 expect(users[ 1 ].username).to.equal('user_1') 382 expect(users[ 0 ].email).to.equal('admin1@example.com')
356 } 383 expect(users[ 0 ].nsfwPolicy).to.equal('display')
357 })
358 384
359 it('Should update my password', async function () { 385 expect(users[ 1 ].username).to.equal('user_1')
360 await updateMyUser({ 386 expect(users[ 1 ].email).to.equal('user_1@example.com')
361 url: server.url, 387 expect(users[ 1 ].nsfwPolicy).to.equal('display')
362 accessToken: accessTokenUser,
363 currentPassword: 'super password',
364 newPassword: 'new password'
365 }) 388 })
366 user.password = 'new password'
367 389
368 await userLogin(server, user, 200) 390 it('Should search user by username', async function () {
369 }) 391 const res = await getUsersListPaginationAndSort(server.url, server.accessToken, 0, 2, 'createdAt', 'oot')
392 const users = res.body.data as User[]
393
394 expect(res.body.total).to.equal(1)
395 expect(users.length).to.equal(1)
370 396
371 it('Should be able to change the NSFW display attribute', async function () { 397 expect(users[ 0 ].username).to.equal('root')
372 await updateMyUser({
373 url: server.url,
374 accessToken: accessTokenUser,
375 nsfwPolicy: 'do_not_list'
376 }) 398 })
377 399
378 const res = await getMyUserInformation(server.url, accessTokenUser) 400 it('Should search user by email', async function () {
379 const user = res.body 401 {
402 const res = await getUsersListPaginationAndSort(server.url, server.accessToken, 0, 2, 'createdAt', 'r_1@exam')
403 const users = res.body.data as User[]
380 404
381 expect(user.username).to.equal('user_1') 405 expect(res.body.total).to.equal(1)
382 expect(user.email).to.equal('user_1@example.com') 406 expect(users.length).to.equal(1)
383 expect(user.nsfwPolicy).to.equal('do_not_list')
384 expect(user.videoQuota).to.equal(2 * 1024 * 1024)
385 expect(user.id).to.be.a('number')
386 expect(user.account.displayName).to.equal('user_1')
387 expect(user.account.description).to.be.null
388 })
389 407
390 it('Should be able to change the autoPlayVideo attribute', async function () { 408 expect(users[ 0 ].username).to.equal('user_1')
391 await updateMyUser({ 409 expect(users[ 0 ].email).to.equal('user_1@example.com')
392 url: server.url, 410 }
393 accessToken: accessTokenUser,
394 autoPlayVideo: false
395 })
396 411
397 const res = await getMyUserInformation(server.url, accessTokenUser) 412 {
398 const user = res.body 413 const res = await getUsersListPaginationAndSort(server.url, server.accessToken, 0, 2, 'createdAt', 'example')
414 const users = res.body.data as User[]
399 415
400 expect(user.autoPlayVideo).to.be.false 416 expect(res.body.total).to.equal(2)
417 expect(users.length).to.equal(2)
418
419 expect(users[ 0 ].username).to.equal('root')
420 expect(users[ 1 ].username).to.equal('user_1')
421 }
422 })
401 }) 423 })
402 424
403 it('Should be able to change the email display attribute', async function () { 425 describe('Update my account', function () {
404 await updateMyUser({ 426 it('Should update my password', async function () {
405 url: server.url, 427 await updateMyUser({
406 accessToken: accessTokenUser, 428 url: server.url,
407 email: 'updated@example.com' 429 accessToken: accessTokenUser,
430 currentPassword: 'super password',
431 newPassword: 'new password'
432 })
433 user.password = 'new password'
434
435 await userLogin(server, user, 200)
408 }) 436 })
409 437
410 const res = await getMyUserInformation(server.url, accessTokenUser) 438 it('Should be able to change the NSFW display attribute', async function () {
411 const user = res.body 439 await updateMyUser({
440 url: server.url,
441 accessToken: accessTokenUser,
442 nsfwPolicy: 'do_not_list'
443 })
444
445 const res = await getMyUserInformation(server.url, accessTokenUser)
446 const user = res.body
447
448 expect(user.username).to.equal('user_1')
449 expect(user.email).to.equal('user_1@example.com')
450 expect(user.nsfwPolicy).to.equal('do_not_list')
451 expect(user.videoQuota).to.equal(2 * 1024 * 1024)
452 expect(user.id).to.be.a('number')
453 expect(user.account.displayName).to.equal('user_1')
454 expect(user.account.description).to.be.null
455 })
412 456
413 expect(user.username).to.equal('user_1') 457 it('Should be able to change the autoPlayVideo attribute', async function () {
414 expect(user.email).to.equal('updated@example.com') 458 await updateMyUser({
415 expect(user.nsfwPolicy).to.equal('do_not_list') 459 url: server.url,
416 expect(user.videoQuota).to.equal(2 * 1024 * 1024) 460 accessToken: accessTokenUser,
417 expect(user.id).to.be.a('number') 461 autoPlayVideo: false
418 expect(user.account.displayName).to.equal('user_1') 462 })
419 expect(user.account.description).to.be.null 463
420 }) 464 const res = await getMyUserInformation(server.url, accessTokenUser)
465 const user = res.body
421 466
422 it('Should be able to update my avatar', async function () { 467 expect(user.autoPlayVideo).to.be.false
423 const fixture = 'avatar.png' 468 })
424 469
425 await updateMyAvatar({ 470 it('Should be able to change the email display attribute', async function () {
426 url: server.url, 471 await updateMyUser({
427 accessToken: accessTokenUser, 472 url: server.url,
428 fixture 473 accessToken: accessTokenUser,
474 email: 'updated@example.com'
475 })
476
477 const res = await getMyUserInformation(server.url, accessTokenUser)
478 const user = res.body
479
480 expect(user.username).to.equal('user_1')
481 expect(user.email).to.equal('updated@example.com')
482 expect(user.nsfwPolicy).to.equal('do_not_list')
483 expect(user.videoQuota).to.equal(2 * 1024 * 1024)
484 expect(user.id).to.be.a('number')
485 expect(user.account.displayName).to.equal('user_1')
486 expect(user.account.description).to.be.null
429 }) 487 })
430 488
431 const res = await getMyUserInformation(server.url, accessTokenUser) 489 it('Should be able to update my avatar', async function () {
432 const user = res.body 490 const fixture = 'avatar.png'
433 491
434 await testImage(server.url, 'avatar-resized', user.account.avatar.path, '.png') 492 await updateMyAvatar({
435 }) 493 url: server.url,
494 accessToken: accessTokenUser,
495 fixture
496 })
497
498 const res = await getMyUserInformation(server.url, accessTokenUser)
499 const user = res.body
436 500
437 it('Should be able to update my display name', async function () { 501 await testImage(server.url, 'avatar-resized', user.account.avatar.path, '.png')
438 await updateMyUser({
439 url: server.url,
440 accessToken: accessTokenUser,
441 displayName: 'new display name'
442 }) 502 })
443 503
444 const res = await getMyUserInformation(server.url, accessTokenUser) 504 it('Should be able to update my display name', async function () {
445 const user = res.body 505 await updateMyUser({
506 url: server.url,
507 accessToken: accessTokenUser,
508 displayName: 'new display name'
509 })
510
511 const res = await getMyUserInformation(server.url, accessTokenUser)
512 const user = res.body
513
514 expect(user.username).to.equal('user_1')
515 expect(user.email).to.equal('updated@example.com')
516 expect(user.nsfwPolicy).to.equal('do_not_list')
517 expect(user.videoQuota).to.equal(2 * 1024 * 1024)
518 expect(user.id).to.be.a('number')
519 expect(user.account.displayName).to.equal('new display name')
520 expect(user.account.description).to.be.null
521 })
446 522
447 expect(user.username).to.equal('user_1') 523 it('Should be able to update my description', async function () {
448 expect(user.email).to.equal('updated@example.com') 524 await updateMyUser({
449 expect(user.nsfwPolicy).to.equal('do_not_list') 525 url: server.url,
450 expect(user.videoQuota).to.equal(2 * 1024 * 1024) 526 accessToken: accessTokenUser,
451 expect(user.id).to.be.a('number') 527 description: 'my super description updated'
452 expect(user.account.displayName).to.equal('new display name') 528 })
453 expect(user.account.description).to.be.null 529
530 const res = await getMyUserInformation(server.url, accessTokenUser)
531 const user = res.body
532
533 expect(user.username).to.equal('user_1')
534 expect(user.email).to.equal('updated@example.com')
535 expect(user.nsfwPolicy).to.equal('do_not_list')
536 expect(user.videoQuota).to.equal(2 * 1024 * 1024)
537 expect(user.id).to.be.a('number')
538 expect(user.account.displayName).to.equal('new display name')
539 expect(user.account.description).to.equal('my super description updated')
540 })
454 }) 541 })
455 542
456 it('Should be able to update my description', async function () { 543 describe('Updating another user', function () {
457 await updateMyUser({ 544
458 url: server.url, 545 it('Should be able to update another user', async function () {
459 accessToken: accessTokenUser, 546 await updateUser({
460 description: 'my super description updated' 547 url: server.url,
548 userId,
549 accessToken,
550 email: 'updated2@example.com',
551 emailVerified: true,
552 videoQuota: 42,
553 role: UserRole.MODERATOR,
554 adminFlags: UserAdminFlag.NONE
555 })
556
557 const res = await getUserInformation(server.url, accessToken, userId)
558 const user = res.body
559
560 expect(user.username).to.equal('user_1')
561 expect(user.email).to.equal('updated2@example.com')
562 expect(user.emailVerified).to.be.true
563 expect(user.nsfwPolicy).to.equal('do_not_list')
564 expect(user.videoQuota).to.equal(42)
565 expect(user.roleLabel).to.equal('Moderator')
566 expect(user.id).to.be.a('number')
567 expect(user.adminFlags).to.equal(UserAdminFlag.NONE)
461 }) 568 })
462 569
463 const res = await getMyUserInformation(server.url, accessTokenUser) 570 it('Should have removed the user token', async function () {
464 const user = res.body 571 await getMyUserVideoQuotaUsed(server.url, accessTokenUser, 401)
465 572
466 expect(user.username).to.equal('user_1') 573 accessTokenUser = await userLogin(server, user)
467 expect(user.email).to.equal('updated@example.com') 574 })
468 expect(user.nsfwPolicy).to.equal('do_not_list')
469 expect(user.videoQuota).to.equal(2 * 1024 * 1024)
470 expect(user.id).to.be.a('number')
471 expect(user.account.displayName).to.equal('new display name')
472 expect(user.account.description).to.equal('my super description updated')
473 })
474 575
475 it('Should be able to update another user', async function () { 576 it('Should be able to update another user password', async function () {
476 await updateUser({ 577 await updateUser({
477 url: server.url, 578 url: server.url,
478 userId, 579 userId,
479 accessToken, 580 accessToken,
480 email: 'updated2@example.com', 581 password: 'password updated'
481 emailVerified: true, 582 })
482 videoQuota: 42,
483 role: UserRole.MODERATOR
484 })
485
486 const res = await getUserInformation(server.url, accessToken, userId)
487 const user = res.body
488
489 expect(user.username).to.equal('user_1')
490 expect(user.email).to.equal('updated2@example.com')
491 expect(user.emailVerified).to.be.true
492 expect(user.nsfwPolicy).to.equal('do_not_list')
493 expect(user.videoQuota).to.equal(42)
494 expect(user.roleLabel).to.equal('Moderator')
495 expect(user.id).to.be.a('number')
496 })
497 583
498 it('Should have removed the user token', async function () { 584 await getMyUserVideoQuotaUsed(server.url, accessTokenUser, 401)
499 await getMyUserVideoQuotaUsed(server.url, accessTokenUser, 401)
500 585
501 accessTokenUser = await userLogin(server, user) 586 await userLogin(server, user, 400)
502 })
503 587
504 it('Should be able to list video blacklist by a moderator', async function () { 588 user.password = 'password updated'
505 await getBlacklistedVideosList(server.url, accessTokenUser) 589 accessTokenUser = await userLogin(server, user)
590 })
506 }) 591 })
507 592
508 it('Should be able to remove this user', async function () { 593 describe('Video blacklists', function () {
509 await removeUser(server.url, userId, accessToken) 594 it('Should be able to list video blacklist by a moderator', async function () {
595 await getBlacklistedVideosList({ url: server.url, token: accessTokenUser })
596 })
510 }) 597 })
511 598
512 it('Should not be able to login with this user', async function () { 599 describe('Remove a user', function () {
513 await userLogin(server, user, 400) 600 it('Should be able to remove this user', async function () {
514 }) 601 await removeUser(server.url, userId, accessToken)
602 })
515 603
516 it('Should not have videos of this user', async function () { 604 it('Should not be able to login with this user', async function () {
517 const res = await getVideosList(server.url) 605 await userLogin(server, user, 400)
606 })
518 607
519 expect(res.body.total).to.equal(1) 608 it('Should not have videos of this user', async function () {
609 const res = await getVideosList(server.url)
520 610
521 const video = res.body.data[ 0 ] 611 expect(res.body.total).to.equal(1)
522 expect(video.account.name).to.equal('root')
523 })
524 612
525 it('Should register a new user', async function () { 613 const video = res.body.data[ 0 ]
526 await registerUser(server.url, 'user_15', 'my super password') 614 expect(video.account.name).to.equal('root')
615 })
527 }) 616 })
528 617
529 it('Should be able to login with this registered user', async function () { 618 describe('Registering a new user', function () {
530 const user15 = { 619 it('Should register a new user', async function () {
531 username: 'user_15', 620 await registerUser(server.url, 'user_15', 'my super password')
532 password: 'my super password' 621 })
533 }
534 622
535 accessToken = await userLogin(server, user15) 623 it('Should be able to login with this registered user', async function () {
536 }) 624 const user15 = {
625 username: 'user_15',
626 password: 'my super password'
627 }
537 628
538 it('Should have the correct video quota', async function () { 629 accessToken = await userLogin(server, user15)
539 const res = await getMyUserInformation(server.url, accessToken) 630 })
540 const user = res.body
541 631
542 expect(user.videoQuota).to.equal(5 * 1024 * 1024) 632 it('Should have the correct video quota', async function () {
543 }) 633 const res = await getMyUserInformation(server.url, accessToken)
634 const user = res.body
544 635
545 it('Should remove me', async function () { 636 expect(user.videoQuota).to.equal(5 * 1024 * 1024)
546 { 637 })
547 const res = await getUsersList(server.url, server.accessToken)
548 expect(res.body.data.find(u => u.username === 'user_15')).to.not.be.undefined
549 }
550 638
551 await deleteMe(server.url, accessToken) 639 it('Should remove me', async function () {
640 {
641 const res = await getUsersList(server.url, server.accessToken)
642 expect(res.body.data.find(u => u.username === 'user_15')).to.not.be.undefined
643 }
552 644
553 { 645 await deleteMe(server.url, accessToken)
554 const res = await getUsersList(server.url, server.accessToken) 646
555 expect(res.body.data.find(u => u.username === 'user_15')).to.be.undefined 647 {
556 } 648 const res = await getUsersList(server.url, server.accessToken)
649 expect(res.body.data.find(u => u.username === 'user_15')).to.be.undefined
650 }
651 })
557 }) 652 })
558 653
559 it('Should block and unblock a user', async function () { 654 describe('User blocking', function () {
560 const user16 = { 655 it('Should block and unblock a user', async function () {
561 username: 'user_16', 656 const user16 = {
562 password: 'my super password' 657 username: 'user_16',
563 } 658 password: 'my super password'
564 const resUser = await createUser(server.url, server.accessToken, user16.username, user16.password) 659 }
565 const user16Id = resUser.body.user.id 660 const resUser = await createUser({
661 url: server.url,
662 accessToken: server.accessToken,
663 username: user16.username,
664 password: user16.password
665 })
666 const user16Id = resUser.body.user.id
566 667
567 accessToken = await userLogin(server, user16) 668 accessToken = await userLogin(server, user16)
568 669
569 await getMyUserInformation(server.url, accessToken, 200) 670 await getMyUserInformation(server.url, accessToken, 200)
570 await blockUser(server.url, user16Id, server.accessToken) 671 await blockUser(server.url, user16Id, server.accessToken)
571 672
572 await getMyUserInformation(server.url, accessToken, 401) 673 await getMyUserInformation(server.url, accessToken, 401)
573 await userLogin(server, user16, 400) 674 await userLogin(server, user16, 400)
574 675
575 await unblockUser(server.url, user16Id, server.accessToken) 676 await unblockUser(server.url, user16Id, server.accessToken)
576 accessToken = await userLogin(server, user16) 677 accessToken = await userLogin(server, user16)
577 await getMyUserInformation(server.url, accessToken, 200) 678 await getMyUserInformation(server.url, accessToken, 200)
679 })
578 }) 680 })
579 681
580 after(async function () { 682 after(async function () {
581 killallServers([ server ]) 683 await cleanupTests([ server ])
582
583 // Keep the logs if the test failed
584 if (this[ 'ok' ]) {
585 await flushTests()
586 }
587 }) 684 })
588}) 685})
diff --git a/server/tests/api/videos/index.ts b/server/tests/api/videos/index.ts
index 97f467aae..93e1f3e98 100644
--- a/server/tests/api/videos/index.ts
+++ b/server/tests/api/videos/index.ts
@@ -8,11 +8,14 @@ import './video-change-ownership'
8import './video-channels' 8import './video-channels'
9import './video-comments' 9import './video-comments'
10import './video-description' 10import './video-description'
11import './video-hls'
11import './video-imports' 12import './video-imports'
12import './video-nsfw' 13import './video-nsfw'
14import './video-playlists'
13import './video-privacy' 15import './video-privacy'
14import './video-schedule-update' 16import './video-schedule-update'
15import './video-transcoder' 17import './video-transcoder'
16import './videos-filter' 18import './videos-filter'
17import './videos-history' 19import './videos-history'
18import './videos-overview' 20import './videos-overview'
21import './videos-views-cleaner'
diff --git a/server/tests/api/videos/multiple-servers.ts b/server/tests/api/videos/multiple-servers.ts
index 6c281e49e..68c1e9a8d 100644
--- a/server/tests/api/videos/multiple-servers.ts
+++ b/server/tests/api/videos/multiple-servers.ts
@@ -9,7 +9,7 @@ import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/
9import { 9import {
10 addVideoChannel, 10 addVideoChannel,
11 checkTmpIsEmpty, 11 checkTmpIsEmpty,
12 checkVideoFilesWereRemoved, 12 checkVideoFilesWereRemoved, cleanupTests,
13 completeVideoCheck, 13 completeVideoCheck,
14 createUser, 14 createUser,
15 dateIsValid, 15 dateIsValid,
@@ -32,15 +32,15 @@ import {
32 viewVideo, 32 viewVideo,
33 wait, 33 wait,
34 webtorrentAdd 34 webtorrentAdd
35} from '../../../../shared/utils' 35} from '../../../../shared/extra-utils'
36import { 36import {
37 addVideoCommentReply, 37 addVideoCommentReply,
38 addVideoCommentThread, 38 addVideoCommentThread,
39 deleteVideoComment, 39 deleteVideoComment,
40 getVideoCommentThreads, 40 getVideoCommentThreads,
41 getVideoThreadComments 41 getVideoThreadComments
42} from '../../../../shared/utils/videos/video-comments' 42} from '../../../../shared/extra-utils/videos/video-comments'
43import { waitJobs } from '../../../../shared/utils/server/jobs' 43import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
44 44
45const expect = chai.expect 45const expect = chai.expect
46 46
@@ -98,6 +98,7 @@ describe('Test multiple servers', function () {
98 nsfw: true, 98 nsfw: true,
99 description: 'my super description for server 1', 99 description: 'my super description for server 1',
100 support: 'my super support text for server 1', 100 support: 'my super support text for server 1',
101 originallyPublishedAt: '2019-02-10T13:38:14.449Z',
101 tags: [ 'tag1p1', 'tag2p1' ], 102 tags: [ 'tag1p1', 'tag2p1' ],
102 channelId: videoChannelId, 103 channelId: videoChannelId,
103 fixture: 'video_short1.webm' 104 fixture: 'video_short1.webm'
@@ -118,6 +119,7 @@ describe('Test multiple servers', function () {
118 nsfw: true, 119 nsfw: true,
119 description: 'my super description for server 1', 120 description: 'my super description for server 1',
120 support: 'my super support text for server 1', 121 support: 'my super support text for server 1',
122 originallyPublishedAt: '2019-02-10T13:38:14.449Z',
121 account: { 123 account: {
122 name: 'root', 124 name: 'root',
123 host: 'localhost:9001' 125 host: 'localhost:9001'
@@ -128,6 +130,7 @@ describe('Test multiple servers', function () {
128 tags: [ 'tag1p1', 'tag2p1' ], 130 tags: [ 'tag1p1', 'tag2p1' ],
129 privacy: VideoPrivacy.PUBLIC, 131 privacy: VideoPrivacy.PUBLIC,
130 commentsEnabled: true, 132 commentsEnabled: true,
133 downloadEnabled: true,
131 channel: { 134 channel: {
132 displayName: 'my channel', 135 displayName: 'my channel',
133 name: 'super_channel_name', 136 name: 'super_channel_name',
@@ -161,7 +164,7 @@ describe('Test multiple servers', function () {
161 username: 'user1', 164 username: 'user1',
162 password: 'super_password' 165 password: 'super_password'
163 } 166 }
164 await createUser(servers[1].url, servers[1].accessToken, user.username, user.password) 167 await createUser({ url: servers[ 1 ].url, accessToken: servers[ 1 ].accessToken, username: user.username, password: user.password })
165 const userAccessToken = await userLogin(servers[1], user) 168 const userAccessToken = await userLogin(servers[1], user)
166 169
167 const videoAttributes = { 170 const videoAttributes = {
@@ -199,6 +202,7 @@ describe('Test multiple servers', function () {
199 }, 202 },
200 isLocal, 203 isLocal,
201 commentsEnabled: true, 204 commentsEnabled: true,
205 downloadEnabled: true,
202 duration: 5, 206 duration: 5,
203 tags: [ 'tag1p2', 'tag2p2', 'tag3p2' ], 207 tags: [ 'tag1p2', 'tag2p2', 'tag3p2' ],
204 privacy: VideoPrivacy.PUBLIC, 208 privacy: VideoPrivacy.PUBLIC,
@@ -307,6 +311,7 @@ describe('Test multiple servers', function () {
307 isLocal, 311 isLocal,
308 duration: 5, 312 duration: 5,
309 commentsEnabled: true, 313 commentsEnabled: true,
314 downloadEnabled: true,
310 tags: [ 'tag1p3' ], 315 tags: [ 'tag1p3' ],
311 privacy: VideoPrivacy.PUBLIC, 316 privacy: VideoPrivacy.PUBLIC,
312 channel: { 317 channel: {
@@ -338,6 +343,7 @@ describe('Test multiple servers', function () {
338 host: 'localhost:9003' 343 host: 'localhost:9003'
339 }, 344 },
340 commentsEnabled: true, 345 commentsEnabled: true,
346 downloadEnabled: true,
341 isLocal, 347 isLocal,
342 duration: 5, 348 duration: 5,
343 tags: [ 'tag2p3', 'tag3p3', 'tag4p3' ], 349 tags: [ 'tag2p3', 'tag3p3', 'tag4p3' ],
@@ -573,15 +579,15 @@ describe('Test multiple servers', function () {
573 this.timeout(20000) 579 this.timeout(20000)
574 580
575 await rateVideo(servers[0].url, servers[0].accessToken, remoteVideosServer1[0], 'like') 581 await rateVideo(servers[0].url, servers[0].accessToken, remoteVideosServer1[0], 'like')
576 await wait(200) 582 await wait(500)
577 await rateVideo(servers[0].url, servers[0].accessToken, remoteVideosServer1[0], 'dislike') 583 await rateVideo(servers[0].url, servers[0].accessToken, remoteVideosServer1[0], 'dislike')
578 await wait(200) 584 await wait(500)
579 await rateVideo(servers[0].url, servers[0].accessToken, remoteVideosServer1[0], 'like') 585 await rateVideo(servers[0].url, servers[0].accessToken, remoteVideosServer1[0], 'like')
580 await rateVideo(servers[2].url, servers[2].accessToken, localVideosServer3[1], 'like') 586 await rateVideo(servers[2].url, servers[2].accessToken, localVideosServer3[1], 'like')
581 await wait(200) 587 await wait(500)
582 await rateVideo(servers[2].url, servers[2].accessToken, localVideosServer3[1], 'dislike') 588 await rateVideo(servers[2].url, servers[2].accessToken, localVideosServer3[1], 'dislike')
583 await rateVideo(servers[2].url, servers[2].accessToken, remoteVideosServer3[1], 'dislike') 589 await rateVideo(servers[2].url, servers[2].accessToken, remoteVideosServer3[1], 'dislike')
584 await wait(200) 590 await wait(500)
585 await rateVideo(servers[2].url, servers[2].accessToken, remoteVideosServer3[0], 'like') 591 await rateVideo(servers[2].url, servers[2].accessToken, remoteVideosServer3[0], 'like')
586 592
587 await waitJobs(servers) 593 await waitJobs(servers)
@@ -621,6 +627,7 @@ describe('Test multiple servers', function () {
621 support: 'my super support text updated', 627 support: 'my super support text updated',
622 tags: [ 'tag_up_1', 'tag_up_2' ], 628 tags: [ 'tag_up_1', 'tag_up_2' ],
623 thumbnailfile: 'thumbnail.jpg', 629 thumbnailfile: 'thumbnail.jpg',
630 originallyPublishedAt: '2019-02-11T13:38:14.449Z',
624 previewfile: 'preview.jpg' 631 previewfile: 'preview.jpg'
625 } 632 }
626 633
@@ -648,6 +655,7 @@ describe('Test multiple servers', function () {
648 nsfw: true, 655 nsfw: true,
649 description: 'my super description updated', 656 description: 'my super description updated',
650 support: 'my super support text updated', 657 support: 'my super support text updated',
658 originallyPublishedAt: '2019-02-11T13:38:14.449Z',
651 account: { 659 account: {
652 name: 'root', 660 name: 'root',
653 host: 'localhost:9003' 661 host: 'localhost:9003'
@@ -655,6 +663,7 @@ describe('Test multiple servers', function () {
655 isLocal, 663 isLocal,
656 duration: 5, 664 duration: 5,
657 commentsEnabled: true, 665 commentsEnabled: true,
666 downloadEnabled: true,
658 tags: [ 'tag_up_1', 'tag_up_2' ], 667 tags: [ 'tag_up_1', 'tag_up_2' ],
659 privacy: VideoPrivacy.PUBLIC, 668 privacy: VideoPrivacy.PUBLIC,
660 channel: { 669 channel: {
@@ -914,11 +923,12 @@ describe('Test multiple servers', function () {
914 } 923 }
915 }) 924 })
916 925
917 it('Should disable comments', async function () { 926 it('Should disable comments and download', async function () {
918 this.timeout(20000) 927 this.timeout(20000)
919 928
920 const attributes = { 929 const attributes = {
921 commentsEnabled: false 930 commentsEnabled: false,
931 downloadEnabled: false
922 } 932 }
923 933
924 await updateVideo(servers[0].url, servers[0].accessToken, videoUUID, attributes) 934 await updateVideo(servers[0].url, servers[0].accessToken, videoUUID, attributes)
@@ -928,6 +938,7 @@ describe('Test multiple servers', function () {
928 for (const server of servers) { 938 for (const server of servers) {
929 const res = await getVideo(server.url, videoUUID) 939 const res = await getVideo(server.url, videoUUID)
930 expect(res.body.commentsEnabled).to.be.false 940 expect(res.body.commentsEnabled).to.be.false
941 expect(res.body.downloadEnabled).to.be.false
931 942
932 const text = 'my super forbidden comment' 943 const text = 'my super forbidden comment'
933 await addVideoCommentThread(server.url, server.accessToken, videoUUID, text, 409) 944 await addVideoCommentThread(server.url, server.accessToken, videoUUID, text, 409)
@@ -976,6 +987,7 @@ describe('Test multiple servers', function () {
976 isLocal, 987 isLocal,
977 duration: 5, 988 duration: 5,
978 commentsEnabled: false, 989 commentsEnabled: false,
990 downloadEnabled: true,
979 tags: [ ], 991 tags: [ ],
980 privacy: VideoPrivacy.PUBLIC, 992 privacy: VideoPrivacy.PUBLIC,
981 channel: { 993 channel: {
@@ -1018,11 +1030,6 @@ describe('Test multiple servers', function () {
1018 }) 1030 })
1019 1031
1020 after(async function () { 1032 after(async function () {
1021 killallServers(servers) 1033 await cleanupTests(servers)
1022
1023 // Keep the logs if the test failed
1024 if (this['ok']) {
1025 await flushTests()
1026 }
1027 }) 1034 })
1028}) 1035})
diff --git a/server/tests/api/videos/services.ts b/server/tests/api/videos/services.ts
index 2da86964f..e9ad947b2 100644
--- a/server/tests/api/videos/services.ts
+++ b/server/tests/api/videos/services.ts
@@ -2,16 +2,8 @@
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { 5import { getOEmbed, getVideosList, ServerInfo, setAccessTokensToServers, uploadVideo } from '../../../../shared/extra-utils/index'
6 flushTests, 6import { cleanupTests, flushAndRunServer } from '../../../../shared/extra-utils/server/servers'
7 getOEmbed,
8 getVideosList,
9 killallServers,
10 ServerInfo,
11 setAccessTokensToServers,
12 uploadVideo
13} from '../../../../shared/utils/index'
14import { runServer } from '../../../../shared/utils/server/servers'
15 7
16const expect = chai.expect 8const expect = chai.expect
17 9
@@ -21,9 +13,7 @@ describe('Test services', function () {
21 before(async function () { 13 before(async function () {
22 this.timeout(30000) 14 this.timeout(30000)
23 15
24 await flushTests() 16 server = await flushAndRunServer(1)
25
26 server = await runServer(1)
27 17
28 await setAccessTokensToServers([ server ]) 18 await setAccessTokensToServers([ server ])
29 19
@@ -77,6 +67,6 @@ describe('Test services', function () {
77 }) 67 })
78 68
79 after(async function () { 69 after(async function () {
80 killallServers([ server ]) 70 await cleanupTests([ server ])
81 }) 71 })
82}) 72})
diff --git a/server/tests/api/videos/single-server.ts b/server/tests/api/videos/single-server.ts
index 069dec67c..1f366b642 100644
--- a/server/tests/api/videos/single-server.ts
+++ b/server/tests/api/videos/single-server.ts
@@ -6,8 +6,9 @@ import 'mocha'
6import { VideoPrivacy } from '../../../../shared/models/videos' 6import { VideoPrivacy } from '../../../../shared/models/videos'
7import { 7import {
8 checkVideoFilesWereRemoved, 8 checkVideoFilesWereRemoved,
9 cleanupTests,
9 completeVideoCheck, 10 completeVideoCheck,
10 flushTests, 11 flushAndRunServer,
11 getVideo, 12 getVideo,
12 getVideoCategories, 13 getVideoCategories,
13 getVideoLanguages, 14 getVideoLanguages,
@@ -17,10 +18,8 @@ import {
17 getVideosListPagination, 18 getVideosListPagination,
18 getVideosListSort, 19 getVideosListSort,
19 getVideosWithFilters, 20 getVideosWithFilters,
20 killallServers,
21 rateVideo, 21 rateVideo,
22 removeVideo, 22 removeVideo,
23 runServer,
24 ServerInfo, 23 ServerInfo,
25 setAccessTokensToServers, 24 setAccessTokensToServers,
26 testImage, 25 testImage,
@@ -28,7 +27,7 @@ import {
28 uploadVideo, 27 uploadVideo,
29 viewVideo, 28 viewVideo,
30 wait 29 wait
31} from '../../../../shared/utils' 30} from '../../../../shared/extra-utils'
32 31
33const expect = chai.expect 32const expect = chai.expect
34 33
@@ -55,6 +54,7 @@ describe('Test a single server', function () {
55 tags: [ 'tag1', 'tag2', 'tag3' ], 54 tags: [ 'tag1', 'tag2', 'tag3' ],
56 privacy: VideoPrivacy.PUBLIC, 55 privacy: VideoPrivacy.PUBLIC,
57 commentsEnabled: true, 56 commentsEnabled: true,
57 downloadEnabled: true,
58 channel: { 58 channel: {
59 displayName: 'Main root channel', 59 displayName: 'Main root channel',
60 name: 'root_channel', 60 name: 'root_channel',
@@ -87,6 +87,7 @@ describe('Test a single server', function () {
87 privacy: VideoPrivacy.PUBLIC, 87 privacy: VideoPrivacy.PUBLIC,
88 duration: 5, 88 duration: 5,
89 commentsEnabled: false, 89 commentsEnabled: false,
90 downloadEnabled: false,
90 channel: { 91 channel: {
91 name: 'root_channel', 92 name: 'root_channel',
92 displayName: 'Main root channel', 93 displayName: 'Main root channel',
@@ -105,9 +106,7 @@ describe('Test a single server', function () {
105 before(async function () { 106 before(async function () {
106 this.timeout(30000) 107 this.timeout(30000)
107 108
108 await flushTests() 109 server = await flushAndRunServer(1)
109
110 server = await runServer(1)
111 110
112 await setAccessTokensToServers([ server ]) 111 await setAccessTokensToServers([ server ])
113 }) 112 })
@@ -356,6 +355,7 @@ describe('Test a single server', function () {
356 nsfw: false, 355 nsfw: false,
357 description: 'my super description updated', 356 description: 'my super description updated',
358 commentsEnabled: false, 357 commentsEnabled: false,
358 downloadEnabled: false,
359 tags: [ 'tagup1', 'tagup2' ] 359 tags: [ 'tagup1', 'tagup2' ]
360 } 360 }
361 await updateVideo(server.url, server.accessToken, videoId, attributes) 361 await updateVideo(server.url, server.accessToken, videoId, attributes)
@@ -424,11 +424,6 @@ describe('Test a single server', function () {
424 }) 424 })
425 425
426 after(async function () { 426 after(async function () {
427 killallServers([ server ]) 427 await cleanupTests([ server ])
428
429 // Keep the logs if the test failed
430 if (this['ok']) {
431 await flushTests()
432 }
433 }) 428 })
434}) 429})
diff --git a/server/tests/api/videos/video-abuse.ts b/server/tests/api/videos/video-abuse.ts
index 3a7b623da..7318497d5 100644
--- a/server/tests/api/videos/video-abuse.ts
+++ b/server/tests/api/videos/video-abuse.ts
@@ -4,6 +4,7 @@ import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { VideoAbuse, VideoAbuseState } from '../../../../shared/models/videos' 5import { VideoAbuse, VideoAbuseState } from '../../../../shared/models/videos'
6import { 6import {
7 cleanupTests,
7 deleteVideoAbuse, 8 deleteVideoAbuse,
8 flushAndRunMultipleServers, 9 flushAndRunMultipleServers,
9 getVideoAbusesList, 10 getVideoAbusesList,
@@ -14,9 +15,9 @@ import {
14 setAccessTokensToServers, 15 setAccessTokensToServers,
15 updateVideoAbuse, 16 updateVideoAbuse,
16 uploadVideo 17 uploadVideo
17} from '../../../../shared/utils/index' 18} from '../../../../shared/extra-utils/index'
18import { doubleFollow } from '../../../../shared/utils/server/follows' 19import { doubleFollow } from '../../../../shared/extra-utils/server/follows'
19import { waitJobs } from '../../../../shared/utils/server/jobs' 20import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
20 21
21const expect = chai.expect 22const expect = chai.expect
22 23
@@ -173,6 +174,6 @@ describe('Test video abuses', function () {
173 }) 174 })
174 175
175 after(async function () { 176 after(async function () {
176 killallServers(servers) 177 await cleanupTests(servers)
177 }) 178 })
178}) 179})
diff --git a/server/tests/api/videos/video-blacklist.ts b/server/tests/api/videos/video-blacklist.ts
index d39ad63b4..e907bbdc0 100644
--- a/server/tests/api/videos/video-blacklist.ts
+++ b/server/tests/api/videos/video-blacklist.ts
@@ -4,29 +4,32 @@ import * as chai from 'chai'
4import { orderBy } from 'lodash' 4import { orderBy } from 'lodash'
5import 'mocha' 5import 'mocha'
6import { 6import {
7 addVideoToBlacklist, 7 addVideoToBlacklist, cleanupTests,
8 createUser,
8 flushAndRunMultipleServers, 9 flushAndRunMultipleServers,
9 getBlacklistedVideosList, 10 getBlacklistedVideosList,
10 getMyVideos, 11 getMyVideos,
11 getSortedBlacklistedVideosList,
12 getVideosList, 12 getVideosList,
13 killallServers, 13 killallServers,
14 removeVideoFromBlacklist, 14 removeVideoFromBlacklist,
15 reRunServer,
15 searchVideo, 16 searchVideo,
16 ServerInfo, 17 ServerInfo,
17 setAccessTokensToServers, 18 setAccessTokensToServers,
18 updateVideo, 19 updateVideo,
19 updateVideoBlacklist, 20 updateVideoBlacklist,
20 uploadVideo, 21 uploadVideo,
21 viewVideo 22 userLogin
22} from '../../../../shared/utils/index' 23} from '../../../../shared/extra-utils/index'
23import { doubleFollow } from '../../../../shared/utils/server/follows' 24import { doubleFollow } from '../../../../shared/extra-utils/server/follows'
24import { waitJobs } from '../../../../shared/utils/server/jobs' 25import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
25import { VideoBlacklist } from '../../../../shared/models/videos' 26import { VideoBlacklist, VideoBlacklistType } from '../../../../shared/models/videos'
27import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
28import { UserRole } from '../../../../shared/models/users'
26 29
27const expect = chai.expect 30const expect = chai.expect
28 31
29describe('Test video blacklist management', function () { 32describe('Test video blacklist', function () {
30 let servers: ServerInfo[] = [] 33 let servers: ServerInfo[] = []
31 let videoId: number 34 let videoId: number
32 35
@@ -101,9 +104,9 @@ describe('Test video blacklist management', function () {
101 }) 104 })
102 }) 105 })
103 106
104 describe('When listing blacklisted videos', function () { 107 describe('When listing manually blacklisted videos', function () {
105 it('Should display all the blacklisted videos', async function () { 108 it('Should display all the blacklisted videos', async function () {
106 const res = await getBlacklistedVideosList(servers[0].url, servers[0].accessToken) 109 const res = await getBlacklistedVideosList({ url: servers[0].url, token: servers[0].accessToken })
107 110
108 expect(res.body.total).to.equal(2) 111 expect(res.body.total).to.equal(2)
109 112
@@ -117,8 +120,36 @@ describe('Test video blacklist management', function () {
117 } 120 }
118 }) 121 })
119 122
123 it('Should display all the blacklisted videos when applying manual type filter', async function () {
124 const res = await getBlacklistedVideosList({
125 url: servers[ 0 ].url,
126 token: servers[ 0 ].accessToken,
127 type: VideoBlacklistType.MANUAL
128 })
129
130 expect(res.body.total).to.equal(2)
131
132 const blacklistedVideos = res.body.data
133 expect(blacklistedVideos).to.be.an('array')
134 expect(blacklistedVideos.length).to.equal(2)
135 })
136
137 it('Should display nothing when applying automatic type filter', async function () {
138 const res = await getBlacklistedVideosList({
139 url: servers[ 0 ].url,
140 token: servers[ 0 ].accessToken,
141 type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED
142 })
143
144 expect(res.body.total).to.equal(0)
145
146 const blacklistedVideos = res.body.data
147 expect(blacklistedVideos).to.be.an('array')
148 expect(blacklistedVideos.length).to.equal(0)
149 })
150
120 it('Should get the correct sort when sorting by descending id', async function () { 151 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') 152 const res = await getBlacklistedVideosList({ url: servers[ 0 ].url, token: servers[ 0 ].accessToken, sort: '-id' })
122 expect(res.body.total).to.equal(2) 153 expect(res.body.total).to.equal(2)
123 154
124 const blacklistedVideos = res.body.data 155 const blacklistedVideos = res.body.data
@@ -131,7 +162,7 @@ describe('Test video blacklist management', function () {
131 }) 162 })
132 163
133 it('Should get the correct sort when sorting by descending video name', async function () { 164 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') 165 const res = await getBlacklistedVideosList({ url: servers[ 0 ].url, token: servers[ 0 ].accessToken, sort: '-name' })
135 expect(res.body.total).to.equal(2) 166 expect(res.body.total).to.equal(2)
136 167
137 const blacklistedVideos = res.body.data 168 const blacklistedVideos = res.body.data
@@ -144,7 +175,7 @@ describe('Test video blacklist management', function () {
144 }) 175 })
145 176
146 it('Should get the correct sort when sorting by ascending creation date', async function () { 177 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') 178 const res = await getBlacklistedVideosList({ url: servers[ 0 ].url, token: servers[ 0 ].accessToken, sort: 'createdAt' })
148 expect(res.body.total).to.equal(2) 179 expect(res.body.total).to.equal(2)
149 180
150 const blacklistedVideos = res.body.data 181 const blacklistedVideos = res.body.data
@@ -161,7 +192,7 @@ describe('Test video blacklist management', function () {
161 it('Should change the reason', async function () { 192 it('Should change the reason', async function () {
162 await updateVideoBlacklist(servers[0].url, servers[0].accessToken, videoId, 'my super reason updated') 193 await updateVideoBlacklist(servers[0].url, servers[0].accessToken, videoId, 'my super reason updated')
163 194
164 const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, '-name') 195 const res = await getBlacklistedVideosList({ url: servers[ 0 ].url, token: servers[ 0 ].accessToken, sort: '-name' })
165 const video = res.body.data.find(b => b.video.id === videoId) 196 const video = res.body.data.find(b => b.video.id === videoId)
166 197
167 expect(video.reason).to.equal('my super reason updated') 198 expect(video.reason).to.equal('my super reason updated')
@@ -197,7 +228,7 @@ describe('Test video blacklist management', function () {
197 228
198 it('Should remove a video from the blacklist on server 1', async function () { 229 it('Should remove a video from the blacklist on server 1', async function () {
199 // Get one video in the blacklist 230 // Get one video in the blacklist
200 const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, '-name') 231 const res = await getBlacklistedVideosList({ url: servers[ 0 ].url, token: servers[ 0 ].accessToken, sort: '-name' })
201 videoToRemove = res.body.data[0] 232 videoToRemove = res.body.data[0]
202 blacklist = res.body.data.slice(1) 233 blacklist = res.body.data.slice(1)
203 234
@@ -218,7 +249,7 @@ describe('Test video blacklist management', function () {
218 }) 249 })
219 250
220 it('Should not have the ex-blacklisted video in videos blacklist list on server 1', async function () { 251 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') 252 const res = await getBlacklistedVideosList({ url: servers[ 0 ].url, token: servers[ 0 ].accessToken, sort: '-name' })
222 expect(res.body.total).to.equal(1) 253 expect(res.body.total).to.equal(1)
223 254
224 const videos = res.body.data 255 const videos = res.body.data
@@ -292,7 +323,7 @@ describe('Test video blacklist management', function () {
292 }) 323 })
293 324
294 it('Should have the correct video blacklist unfederate attribute', async function () { 325 it('Should have the correct video blacklist unfederate attribute', async function () {
295 const res = await getSortedBlacklistedVideosList(servers[0].url, servers[0].accessToken, 'createdAt') 326 const res = await getBlacklistedVideosList({ url: servers[ 0 ].url, token: servers[ 0 ].accessToken, sort: 'createdAt' })
296 327
297 const blacklistedVideos: VideoBlacklist[] = res.body.data 328 const blacklistedVideos: VideoBlacklist[] = res.body.data
298 const video3Blacklisted = blacklistedVideos.find(b => b.video.uuid === video3UUID) 329 const video3Blacklisted = blacklistedVideos.find(b => b.video.uuid === video3UUID)
@@ -317,7 +348,84 @@ describe('Test video blacklist management', function () {
317 348
318 }) 349 })
319 350
351 describe('When auto blacklist videos', function () {
352 let userWithoutFlag: string
353 let userWithFlag: string
354
355 before(async function () {
356 this.timeout(20000)
357
358 killallServers([ servers[0] ])
359
360 const config = {
361 'auto_blacklist': {
362 videos: {
363 'of_users': {
364 enabled: true
365 }
366 }
367 }
368 }
369 await reRunServer(servers[0], config)
370
371 {
372 const user = { username: 'user_without_flag', password: 'password' }
373 await createUser({
374 url: servers[ 0 ].url,
375 accessToken: servers[ 0 ].accessToken,
376 username: user.username,
377 adminFlags: UserAdminFlag.NONE,
378 password: user.password,
379 role: UserRole.USER
380 })
381
382 userWithoutFlag = await userLogin(servers[0], user)
383 }
384
385 {
386 const user = { username: 'user_with_flag', password: 'password' }
387 await createUser({
388 url: servers[ 0 ].url,
389 accessToken: servers[ 0 ].accessToken,
390 username: user.username,
391 adminFlags: UserAdminFlag.BY_PASS_VIDEO_AUTO_BLACKLIST,
392 password: user.password,
393 role: UserRole.USER
394 })
395
396 userWithFlag = await userLogin(servers[0], user)
397 }
398
399 await waitJobs(servers)
400 })
401
402 it('Should auto blacklist a video', async function () {
403 await uploadVideo(servers[0].url, userWithoutFlag, { name: 'blacklisted' })
404
405 const res = await getBlacklistedVideosList({
406 url: servers[ 0 ].url,
407 token: servers[ 0 ].accessToken,
408 type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED
409 })
410
411 expect(res.body.total).to.equal(1)
412 expect(res.body.data[0].video.name).to.equal('blacklisted')
413 })
414
415 it('Should not auto blacklist a video', async function () {
416 await uploadVideo(servers[0].url, userWithFlag, { name: 'not blacklisted' })
417
418 const res = await getBlacklistedVideosList({
419 url: servers[ 0 ].url,
420 token: servers[ 0 ].accessToken,
421 type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED
422 })
423
424 expect(res.body.total).to.equal(1)
425 })
426 })
427
320 after(async function () { 428 after(async function () {
321 killallServers(servers) 429 await cleanupTests(servers)
322 }) 430 })
323}) 431})
diff --git a/server/tests/api/videos/video-captions.ts b/server/tests/api/videos/video-captions.ts
index 57bee713f..5e13f5949 100644
--- a/server/tests/api/videos/video-captions.ts
+++ b/server/tests/api/videos/video-captions.ts
@@ -3,16 +3,21 @@
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { 5import {
6 checkVideoFilesWereRemoved, 6 checkVideoFilesWereRemoved, cleanupTests,
7 doubleFollow, 7 doubleFollow,
8 flushAndRunMultipleServers, 8 flushAndRunMultipleServers,
9 removeVideo, 9 removeVideo,
10 uploadVideo, 10 uploadVideo,
11 wait 11 wait
12} from '../../../../shared/utils' 12} from '../../../../shared/extra-utils'
13import { flushTests, killallServers, ServerInfo, setAccessTokensToServers } from '../../../../shared/utils/index' 13import { flushTests, killallServers, ServerInfo, setAccessTokensToServers } from '../../../../shared/extra-utils/index'
14import { waitJobs } from '../../../../shared/utils/server/jobs' 14import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
15import { createVideoCaption, deleteVideoCaption, listVideoCaptions, testCaptionFile } from '../../../../shared/utils/videos/video-captions' 15import {
16 createVideoCaption,
17 deleteVideoCaption,
18 listVideoCaptions,
19 testCaptionFile
20} from '../../../../shared/extra-utils/videos/video-captions'
16import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model' 21import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model'
17 22
18const expect = chai.expect 23const expect = chai.expect
@@ -24,8 +29,6 @@ describe('Test video captions', function () {
24 before(async function () { 29 before(async function () {
25 this.timeout(30000) 30 this.timeout(30000)
26 31
27 await flushTests()
28
29 servers = await flushAndRunMultipleServers(2) 32 servers = await flushAndRunMultipleServers(2)
30 33
31 await setAccessTokensToServers(servers) 34 await setAccessTokensToServers(servers)
@@ -193,6 +196,6 @@ describe('Test video captions', function () {
193 }) 196 })
194 197
195 after(async function () { 198 after(async function () {
196 killallServers(servers) 199 await cleanupTests(servers)
197 }) 200 })
198}) 201})
diff --git a/server/tests/api/videos/video-change-ownership.ts b/server/tests/api/videos/video-change-ownership.ts
index 25675a966..1c0327d40 100644
--- a/server/tests/api/videos/video-change-ownership.ts
+++ b/server/tests/api/videos/video-change-ownership.ts
@@ -4,22 +4,23 @@ import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { 5import {
6 acceptChangeOwnership, 6 acceptChangeOwnership,
7 changeVideoOwnership, 7 changeVideoOwnership, cleanupTests,
8 createUser, doubleFollow, flushAndRunMultipleServers, 8 createUser,
9 flushTests, 9 doubleFollow,
10 flushAndRunMultipleServers,
11 flushAndRunServer,
10 getMyUserInformation, 12 getMyUserInformation,
13 getVideo,
11 getVideoChangeOwnershipList, 14 getVideoChangeOwnershipList,
12 getVideosList, 15 getVideosList,
13 killallServers, 16 killallServers,
14 refuseChangeOwnership, 17 refuseChangeOwnership,
15 runServer,
16 ServerInfo, 18 ServerInfo,
17 setAccessTokensToServers, 19 setAccessTokensToServers,
18 uploadVideo, 20 uploadVideo,
19 userLogin, 21 userLogin
20 getVideo 22} from '../../../../shared/extra-utils'
21} from '../../../../shared/utils' 23import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
22import { waitJobs } from '../../../../shared/utils/server/jobs'
23import { User } from '../../../../shared/models/users' 24import { User } from '../../../../shared/models/users'
24import { VideoDetails } from '../../../../shared/models/videos' 25import { VideoDetails } from '../../../../shared/models/videos'
25 26
@@ -46,8 +47,20 @@ describe('Test video change ownership - nominal', function () {
46 await setAccessTokensToServers(servers) 47 await setAccessTokensToServers(servers)
47 48
48 const videoQuota = 42000000 49 const videoQuota = 42000000
49 await createUser(servers[0].url, servers[0].accessToken, firstUser.username, firstUser.password, videoQuota) 50 await createUser({
50 await createUser(servers[0].url, servers[0].accessToken, secondUser.username, secondUser.password, videoQuota) 51 url: servers[ 0 ].url,
52 accessToken: servers[ 0 ].accessToken,
53 username: firstUser.username,
54 password: firstUser.password,
55 videoQuota: videoQuota
56 })
57 await createUser({
58 url: servers[ 0 ].url,
59 accessToken: servers[ 0 ].accessToken,
60 username: secondUser.username,
61 password: secondUser.password,
62 videoQuota: videoQuota
63 })
51 64
52 firstUserAccessToken = await userLogin(servers[0], firstUser) 65 firstUserAccessToken = await userLogin(servers[0], firstUser)
53 secondUserAccessToken = await userLogin(servers[0], secondUser) 66 secondUserAccessToken = await userLogin(servers[0], secondUser)
@@ -190,7 +203,7 @@ describe('Test video change ownership - nominal', function () {
190 } 203 }
191 }) 204 })
192 205
193 after(async function () { 206 after(function () {
194 killallServers(servers) 207 killallServers(servers)
195 }) 208 })
196}) 209})
@@ -213,14 +226,25 @@ describe('Test video change ownership - quota too small', function () {
213 this.timeout(50000) 226 this.timeout(50000)
214 227
215 // Run one server 228 // Run one server
216 await flushTests() 229 server = await flushAndRunServer(1)
217 server = await runServer(1)
218 await setAccessTokensToServers([server]) 230 await setAccessTokensToServers([server])
219 231
220 const videoQuota = 42000000 232 const videoQuota = 42000000
221 const limitedVideoQuota = 10 233 const limitedVideoQuota = 10
222 await createUser(server.url, server.accessToken, firstUser.username, firstUser.password, videoQuota) 234 await createUser({
223 await createUser(server.url, server.accessToken, secondUser.username, secondUser.password, limitedVideoQuota) 235 url: server.url,
236 accessToken: server.accessToken,
237 username: firstUser.username,
238 password: firstUser.password,
239 videoQuota: videoQuota
240 })
241 await createUser({
242 url: server.url,
243 accessToken: server.accessToken,
244 username: secondUser.username,
245 password: secondUser.password,
246 videoQuota: limitedVideoQuota
247 })
224 248
225 firstUserAccessToken = await userLogin(server, firstUser) 249 firstUserAccessToken = await userLogin(server, firstUser)
226 secondUserAccessToken = await userLogin(server, secondUser) 250 secondUserAccessToken = await userLogin(server, secondUser)
@@ -274,6 +298,6 @@ describe('Test video change ownership - quota too small', function () {
274 }) 298 })
275 299
276 after(async function () { 300 after(async function () {
277 killallServers([server]) 301 await cleanupTests([ server ])
278 }) 302 })
279}) 303})
diff --git a/server/tests/api/videos/video-channels.ts b/server/tests/api/videos/video-channels.ts
index 63514d69c..345e96f43 100644
--- a/server/tests/api/videos/video-channels.ts
+++ b/server/tests/api/videos/video-channels.ts
@@ -4,6 +4,7 @@ import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { User, Video } from '../../../../shared/index' 5import { User, Video } from '../../../../shared/index'
6import { 6import {
7 cleanupTests,
7 createUser, 8 createUser,
8 doubleFollow, 9 doubleFollow,
9 flushAndRunMultipleServers, 10 flushAndRunMultipleServers,
@@ -13,7 +14,7 @@ import {
13 updateVideoChannelAvatar, 14 updateVideoChannelAvatar,
14 uploadVideo, 15 uploadVideo,
15 userLogin 16 userLogin
16} from '../../../../shared/utils' 17} from '../../../../shared/extra-utils'
17import { 18import {
18 addVideoChannel, 19 addVideoChannel,
19 deleteVideoChannel, 20 deleteVideoChannel,
@@ -26,8 +27,8 @@ import {
26 ServerInfo, 27 ServerInfo,
27 setAccessTokensToServers, 28 setAccessTokensToServers,
28 updateVideoChannel 29 updateVideoChannel
29} from '../../../../shared/utils/index' 30} from '../../../../shared/extra-utils/index'
30import { waitJobs } from '../../../../shared/utils/server/jobs' 31import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
31 32
32const expect = chai.expect 33const expect = chai.expect
33 34
@@ -42,8 +43,6 @@ describe('Test video channels', function () {
42 before(async function () { 43 before(async function () {
43 this.timeout(30000) 44 this.timeout(30000)
44 45
45 await flushTests()
46
47 servers = await flushAndRunMultipleServers(2) 46 servers = await flushAndRunMultipleServers(2)
48 47
49 await setAccessTokensToServers(servers) 48 await setAccessTokensToServers(servers)
@@ -270,7 +269,7 @@ describe('Test video channels', function () {
270 } 269 }
271 270
272 { 271 {
273 await createUser(servers[ 0 ].url, servers[ 0 ].accessToken, 'toto', 'password') 272 await createUser({ url: servers[ 0 ].url, accessToken: servers[ 0 ].accessToken, username: 'toto', password: 'password' })
274 const accessToken = await userLogin(servers[ 0 ], { username: 'toto', password: 'password' }) 273 const accessToken = await userLogin(servers[ 0 ], { username: 'toto', password: 'password' })
275 274
276 const res = await getMyUserInformation(servers[ 0 ].url, accessToken) 275 const res = await getMyUserInformation(servers[ 0 ].url, accessToken)
@@ -280,6 +279,6 @@ describe('Test video channels', function () {
280 }) 279 })
281 280
282 after(async function () { 281 after(async function () {
283 killallServers(servers) 282 await cleanupTests(servers)
284 }) 283 })
285}) 284})
diff --git a/server/tests/api/videos/video-comments.ts b/server/tests/api/videos/video-comments.ts
index ce1b17e35..22fd8c058 100644
--- a/server/tests/api/videos/video-comments.ts
+++ b/server/tests/api/videos/video-comments.ts
@@ -3,24 +3,22 @@
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 '../../../../shared/utils' 6import { cleanupTests, testImage } from '../../../../shared/extra-utils'
7import { 7import {
8 dateIsValid, 8 dateIsValid,
9 flushTests, 9 flushAndRunServer,
10 killallServers,
11 runServer,
12 ServerInfo, 10 ServerInfo,
13 setAccessTokensToServers, 11 setAccessTokensToServers,
14 updateMyAvatar, 12 updateMyAvatar,
15 uploadVideo 13 uploadVideo
16} from '../../../../shared/utils/index' 14} from '../../../../shared/extra-utils/index'
17import { 15import {
18 addVideoCommentReply, 16 addVideoCommentReply,
19 addVideoCommentThread, 17 addVideoCommentThread,
20 deleteVideoComment, 18 deleteVideoComment,
21 getVideoCommentThreads, 19 getVideoCommentThreads,
22 getVideoThreadComments 20 getVideoThreadComments
23} from '../../../../shared/utils/videos/video-comments' 21} from '../../../../shared/extra-utils/videos/video-comments'
24 22
25const expect = chai.expect 23const expect = chai.expect
26 24
@@ -34,9 +32,7 @@ describe('Test video comments', function () {
34 before(async function () { 32 before(async function () {
35 this.timeout(30000) 33 this.timeout(30000)
36 34
37 await flushTests() 35 server = await flushAndRunServer(1)
38
39 server = await runServer(1)
40 36
41 await setAccessTokensToServers([ server ]) 37 await setAccessTokensToServers([ server ])
42 38
@@ -202,6 +198,6 @@ describe('Test video comments', function () {
202 }) 198 })
203 199
204 after(async function () { 200 after(async function () {
205 killallServers([ server ]) 201 await cleanupTests([ server ])
206 }) 202 })
207}) 203})
diff --git a/server/tests/api/videos/video-description.ts b/server/tests/api/videos/video-description.ts
index cbda0b9a6..db4d278bf 100644
--- a/server/tests/api/videos/video-description.ts
+++ b/server/tests/api/videos/video-description.ts
@@ -3,6 +3,7 @@
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { 5import {
6 cleanupTests,
6 flushAndRunMultipleServers, 7 flushAndRunMultipleServers,
7 getVideo, 8 getVideo,
8 getVideoDescription, 9 getVideoDescription,
@@ -12,9 +13,9 @@ import {
12 setAccessTokensToServers, 13 setAccessTokensToServers,
13 updateVideo, 14 updateVideo,
14 uploadVideo 15 uploadVideo
15} from '../../../../shared/utils/index' 16} from '../../../../shared/extra-utils/index'
16import { doubleFollow } from '../../../../shared/utils/server/follows' 17import { doubleFollow } from '../../../../shared/extra-utils/server/follows'
17import { waitJobs } from '../../../../shared/utils/server/jobs' 18import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
18 19
19const expect = chai.expect 20const expect = chai.expect
20 21
@@ -100,6 +101,6 @@ describe('Test video description', function () {
100 }) 101 })
101 102
102 after(async function () { 103 after(async function () {
103 killallServers(servers) 104 await cleanupTests(servers)
104 }) 105 })
105}) 106})
diff --git a/server/tests/api/videos/video-hls.ts b/server/tests/api/videos/video-hls.ts
new file mode 100644
index 000000000..22031c18b
--- /dev/null
+++ b/server/tests/api/videos/video-hls.ts
@@ -0,0 +1,134 @@
1/* tslint:disable:no-unused-expression */
2
3import * as chai from 'chai'
4import 'mocha'
5import {
6 checkDirectoryIsEmpty,
7 checkSegmentHash,
8 checkTmpIsEmpty, cleanupTests,
9 doubleFollow,
10 flushAndRunMultipleServers,
11 flushTests,
12 getPlaylist,
13 getVideo,
14 killallServers,
15 removeVideo,
16 ServerInfo,
17 setAccessTokensToServers,
18 updateVideo,
19 uploadVideo,
20 waitJobs
21} from '../../../../shared/extra-utils'
22import { VideoDetails } from '../../../../shared/models/videos'
23import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type'
24import { join } from 'path'
25
26const expect = chai.expect
27
28async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string) {
29 const resolutions = [ 240, 360, 480, 720 ]
30
31 for (const server of servers) {
32 const res = await getVideo(server.url, videoUUID)
33 const videoDetails: VideoDetails = res.body
34
35 expect(videoDetails.streamingPlaylists).to.have.lengthOf(1)
36
37 const hlsPlaylist = videoDetails.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
38 expect(hlsPlaylist).to.not.be.undefined
39
40 {
41 const res2 = await getPlaylist(hlsPlaylist.playlistUrl)
42
43 const masterPlaylist = res2.text
44
45 expect(masterPlaylist).to.contain('#EXT-X-STREAM-INF:BANDWIDTH=55472,RESOLUTION=640x360,FRAME-RATE=25')
46
47 for (const resolution of resolutions) {
48 expect(masterPlaylist).to.contain(`${resolution}.m3u8`)
49 }
50 }
51
52 {
53 for (const resolution of resolutions) {
54 const res2 = await getPlaylist(`http://localhost:9001/static/streaming-playlists/hls/${videoUUID}/${resolution}.m3u8`)
55
56 const subPlaylist = res2.text
57 expect(subPlaylist).to.contain(`${videoUUID}-${resolution}-fragmented.mp4`)
58 }
59 }
60
61 {
62 const baseUrl = 'http://localhost:9001/static/streaming-playlists/hls'
63
64 for (const resolution of resolutions) {
65 await checkSegmentHash(baseUrl, baseUrl, videoUUID, resolution, hlsPlaylist)
66 }
67 }
68 }
69}
70
71describe('Test HLS videos', function () {
72 let servers: ServerInfo[] = []
73 let videoUUID = ''
74
75 before(async function () {
76 this.timeout(120000)
77
78 servers = await flushAndRunMultipleServers(2, { transcoding: { enabled: true, hls: { enabled: true } } })
79
80 // Get the access tokens
81 await setAccessTokensToServers(servers)
82
83 // Server 1 and server 2 follow each other
84 await doubleFollow(servers[0], servers[1])
85 })
86
87 it('Should upload a video and transcode it to HLS', async function () {
88 this.timeout(120000)
89
90 {
91 const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'video 1', fixture: 'video_short.webm' })
92 videoUUID = res.body.video.uuid
93 }
94
95 await waitJobs(servers)
96
97 await checkHlsPlaylist(servers, videoUUID)
98 })
99
100 it('Should update the video', async function () {
101 await updateVideo(servers[0].url, servers[0].accessToken, videoUUID, { name: 'video 1 updated' })
102
103 await waitJobs(servers)
104
105 await checkHlsPlaylist(servers, videoUUID)
106 })
107
108 it('Should delete the video', async function () {
109 await removeVideo(servers[0].url, servers[0].accessToken, videoUUID)
110
111 await waitJobs(servers)
112
113 for (const server of servers) {
114 await getVideo(server.url, videoUUID, 404)
115 }
116 })
117
118 it('Should have the playlists/segment deleted from the disk', async function () {
119 for (const server of servers) {
120 await checkDirectoryIsEmpty(server, 'videos')
121 await checkDirectoryIsEmpty(server, join('streaming-playlists', 'hls'))
122 }
123 })
124
125 it('Should have an empty tmp directory', async function () {
126 for (const server of servers) {
127 await checkTmpIsEmpty(server)
128 }
129 })
130
131 after(async function () {
132 await cleanupTests(servers)
133 })
134})
diff --git a/server/tests/api/videos/video-imports.ts b/server/tests/api/videos/video-imports.ts
index cd4988553..1233ed6eb 100644
--- a/server/tests/api/videos/video-imports.ts
+++ b/server/tests/api/videos/video-imports.ts
@@ -4,6 +4,7 @@ import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { VideoDetails, VideoImport, VideoPrivacy } from '../../../../shared/models/videos' 5import { VideoDetails, VideoImport, VideoPrivacy } from '../../../../shared/models/videos'
6import { 6import {
7 cleanupTests,
7 doubleFollow, 8 doubleFollow,
8 flushAndRunMultipleServers, 9 flushAndRunMultipleServers,
9 getMyUserInformation, 10 getMyUserInformation,
@@ -14,9 +15,9 @@ import {
14 killallServers, 15 killallServers,
15 ServerInfo, 16 ServerInfo,
16 setAccessTokensToServers 17 setAccessTokensToServers
17} from '../../../../shared/utils' 18} from '../../../../shared/extra-utils'
18import { waitJobs } from '../../../../shared/utils/server/jobs' 19import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
19import { getMagnetURI, getYoutubeVideoUrl, importVideo, getMyVideoImports } from '../../../../shared/utils/videos/video-imports' 20import { getMagnetURI, getYoutubeVideoUrl, importVideo, getMyVideoImports } from '../../../../shared/extra-utils/videos/video-imports'
20 21
21const expect = chai.expect 22const expect = chai.expect
22 23
@@ -38,6 +39,11 @@ describe('Test video imports', function () {
38 expect(videoHttp.tags).to.deep.equal([ 'tag1', 'tag2' ]) 39 expect(videoHttp.tags).to.deep.equal([ 'tag1', 'tag2' ])
39 expect(videoHttp.files).to.have.lengthOf(1) 40 expect(videoHttp.files).to.have.lengthOf(1)
40 41
42 const originallyPublishedAt = new Date(videoHttp.originallyPublishedAt)
43 expect(originallyPublishedAt.getDate()).to.equal(14)
44 expect(originallyPublishedAt.getMonth()).to.equal(0)
45 expect(originallyPublishedAt.getFullYear()).to.equal(2019)
46
41 const resMagnet = await getVideo(url, idMagnet) 47 const resMagnet = await getVideo(url, idMagnet)
42 const videoMagnet: VideoDetails = resMagnet.body 48 const videoMagnet: VideoDetails = resMagnet.body
43 const resTorrent = await getVideo(url, idTorrent) 49 const resTorrent = await getVideo(url, idTorrent)
@@ -237,6 +243,6 @@ describe('Test video imports', function () {
237 }) 243 })
238 244
239 after(async function () { 245 after(async function () {
240 killallServers(servers) 246 await cleanupTests(servers)
241 }) 247 })
242}) 248})
diff --git a/server/tests/api/videos/video-nsfw.ts b/server/tests/api/videos/video-nsfw.ts
index df1ee2eb9..ad6a4b43f 100644
--- a/server/tests/api/videos/video-nsfw.ts
+++ b/server/tests/api/videos/video-nsfw.ts
@@ -2,30 +2,23 @@
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { cleanupTests, getVideosList, ServerInfo, setAccessTokensToServers, uploadVideo } from '../../../../shared/extra-utils/index'
6import { userLogin } from '../../../../shared/extra-utils/users/login'
7import { createUser } from '../../../../shared/extra-utils/users/users'
8import { getMyVideos } from '../../../../shared/extra-utils/videos/videos'
5import { 9import {
6 flushTests, 10 flushAndRunServer,
7 getVideosList,
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'
16import {
17 getAccountVideos, 11 getAccountVideos,
18 getConfig, 12 getConfig,
19 getCustomConfig, 13 getCustomConfig,
20 getMyUserInformation, 14 getMyUserInformation,
21 getVideoChannelVideos, 15 getVideoChannelVideos,
22 getVideosListWithToken, 16 getVideosListWithToken,
23 runServer,
24 searchVideo, 17 searchVideo,
25 searchVideoWithToken, 18 searchVideoWithToken,
26 updateCustomConfig, 19 updateCustomConfig,
27 updateMyUser 20 updateMyUser
28} from '../../../../shared/utils' 21} from '../../../../shared/extra-utils'
29import { ServerConfig } from '../../../../shared/models' 22import { ServerConfig } from '../../../../shared/models'
30import { CustomConfig } from '../../../../shared/models/server/custom-config.model' 23import { CustomConfig } from '../../../../shared/models/server/custom-config.model'
31import { User } from '../../../../shared/models/users' 24import { User } from '../../../../shared/models/users'
@@ -64,9 +57,7 @@ describe('Test video NSFW policy', function () {
64 57
65 before(async function () { 58 before(async function () {
66 this.timeout(50000) 59 this.timeout(50000)
67 60 server = await flushAndRunServer(1)
68 await flushTests()
69 server = await runServer(1)
70 61
71 // Get the access tokens 62 // Get the access tokens
72 await setAccessTokensToServers([ server ]) 63 await setAccessTokensToServers([ server ])
@@ -144,7 +135,7 @@ describe('Test video NSFW policy', function () {
144 it('Should create a user having the default nsfw policy', async function () { 135 it('Should create a user having the default nsfw policy', async function () {
145 const username = 'user1' 136 const username = 'user1'
146 const password = 'my super password' 137 const password = 'my super password'
147 await createUser(server.url, server.accessToken, username, password) 138 await createUser({ url: server.url, accessToken: server.accessToken, username: username, password: password })
148 139
149 userAccessToken = await userLogin(server, { username, password }) 140 userAccessToken = await userLogin(server, { username, password })
150 141
@@ -244,6 +235,6 @@ describe('Test video NSFW policy', function () {
244 }) 235 })
245 236
246 after(async function () { 237 after(async function () {
247 killallServers([ server ]) 238 await cleanupTests([ server ])
248 }) 239 })
249}) 240})
diff --git a/server/tests/api/videos/video-playlists.ts b/server/tests/api/videos/video-playlists.ts
new file mode 100644
index 000000000..e4d817ff8
--- /dev/null
+++ b/server/tests/api/videos/video-playlists.ts
@@ -0,0 +1,867 @@
1/* tslint:disable:no-unused-expression */
2
3import * as chai from 'chai'
4import 'mocha'
5import {
6 addVideoChannel,
7 addVideoInPlaylist,
8 checkPlaylistFilesWereRemoved,
9 cleanupTests,
10 createUser,
11 createVideoPlaylist,
12 deleteVideoChannel,
13 deleteVideoPlaylist,
14 doubleFollow,
15 doVideosExistInMyPlaylist,
16 flushAndRunMultipleServers,
17 getAccountPlaylistsList,
18 getAccountPlaylistsListWithToken,
19 getMyUserInformation,
20 getPlaylistVideos,
21 getVideoChannelPlaylistsList,
22 getVideoPlaylist,
23 getVideoPlaylistPrivacies,
24 getVideoPlaylistsList,
25 getVideoPlaylistWithToken,
26 removeUser,
27 removeVideoFromPlaylist,
28 reorderVideosPlaylist,
29 ServerInfo,
30 setAccessTokensToServers,
31 setDefaultVideoChannel,
32 testImage,
33 unfollow,
34 updateVideoPlaylist,
35 updateVideoPlaylistElement,
36 uploadVideo,
37 uploadVideoAndGetId,
38 userLogin,
39 waitJobs
40} from '../../../../shared/extra-utils'
41import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
42import { VideoPlaylist } from '../../../../shared/models/videos/playlist/video-playlist.model'
43import { Video } from '../../../../shared/models/videos'
44import { VideoPlaylistType } from '../../../../shared/models/videos/playlist/video-playlist-type.model'
45import { VideoExistInPlaylist } from '../../../../shared/models/videos/playlist/video-exist-in-playlist.model'
46import { User } from '../../../../shared/models/users'
47
48const expect = chai.expect
49
50describe('Test video playlists', function () {
51 let servers: ServerInfo[] = []
52
53 let playlistServer2Id1: number
54 let playlistServer2Id2: number
55 let playlistServer2UUID2: number
56
57 let playlistServer1Id: number
58 let playlistServer1UUID: string
59
60 let nsfwVideoServer1: number
61
62 before(async function () {
63 this.timeout(120000)
64
65 servers = await flushAndRunMultipleServers(3, { transcoding: { enabled: false } })
66
67 // Get the access tokens
68 await setAccessTokensToServers(servers)
69 await setDefaultVideoChannel(servers)
70
71 // Server 1 and server 2 follow each other
72 await doubleFollow(servers[0], servers[1])
73 // Server 1 and server 3 follow each other
74 await doubleFollow(servers[0], servers[2])
75
76 {
77 const serverPromises: Promise<any>[][] = []
78
79 for (const server of servers) {
80 const videoPromises: Promise<any>[] = []
81
82 for (let i = 0; i < 7; i++) {
83 videoPromises.push(
84 uploadVideo(server.url, server.accessToken, { name: `video ${i} server ${server.serverNumber}`, nsfw: false })
85 .then(res => res.body.video)
86 )
87 }
88
89 serverPromises.push(videoPromises)
90 }
91
92 servers[0].videos = await Promise.all(serverPromises[0])
93 servers[1].videos = await Promise.all(serverPromises[1])
94 servers[2].videos = await Promise.all(serverPromises[2])
95 }
96
97 nsfwVideoServer1 = (await uploadVideoAndGetId({ server: servers[ 0 ], videoName: 'NSFW video', nsfw: true })).id
98
99 await waitJobs(servers)
100 })
101
102 it('Should list video playlist privacies', async function () {
103 const res = await getVideoPlaylistPrivacies(servers[0].url)
104
105 const privacies = res.body
106 expect(Object.keys(privacies)).to.have.length.at.least(3)
107
108 expect(privacies[3]).to.equal('Private')
109 })
110
111 it('Should list watch later playlist', async function () {
112 const url = servers[ 0 ].url
113 const accessToken = servers[ 0 ].accessToken
114
115 {
116 const res = await getAccountPlaylistsListWithToken(url, accessToken, 'root', 0, 5, VideoPlaylistType.WATCH_LATER)
117
118 expect(res.body.total).to.equal(1)
119 expect(res.body.data).to.have.lengthOf(1)
120
121 const playlist: VideoPlaylist = res.body.data[ 0 ]
122 expect(playlist.displayName).to.equal('Watch later')
123 expect(playlist.type.id).to.equal(VideoPlaylistType.WATCH_LATER)
124 expect(playlist.type.label).to.equal('Watch later')
125 }
126
127 {
128 const res = await getAccountPlaylistsListWithToken(url, accessToken, 'root', 0, 5, VideoPlaylistType.REGULAR)
129
130 expect(res.body.total).to.equal(0)
131 expect(res.body.data).to.have.lengthOf(0)
132 }
133
134 {
135 const res = await getAccountPlaylistsList(url, 'root', 0, 5)
136 expect(res.body.total).to.equal(0)
137 expect(res.body.data).to.have.lengthOf(0)
138 }
139 })
140
141 it('Should create a playlist on server 1 and have the playlist on server 2 and 3', async function () {
142 this.timeout(30000)
143
144 await createVideoPlaylist({
145 url: servers[0].url,
146 token: servers[0].accessToken,
147 playlistAttrs: {
148 displayName: 'my super playlist',
149 privacy: VideoPlaylistPrivacy.PUBLIC,
150 description: 'my super description',
151 thumbnailfile: 'thumbnail.jpg',
152 videoChannelId: servers[0].videoChannel.id
153 }
154 })
155
156 await waitJobs(servers)
157
158 for (const server of servers) {
159 const res = await getVideoPlaylistsList(server.url, 0, 5)
160 expect(res.body.total).to.equal(1)
161 expect(res.body.data).to.have.lengthOf(1)
162
163 const playlistFromList = res.body.data[0] as VideoPlaylist
164
165 const res2 = await getVideoPlaylist(server.url, playlistFromList.uuid)
166 const playlistFromGet = res2.body
167
168 for (const playlist of [ playlistFromGet, playlistFromList ]) {
169 expect(playlist.id).to.be.a('number')
170 expect(playlist.uuid).to.be.a('string')
171
172 expect(playlist.isLocal).to.equal(server.serverNumber === 1)
173
174 expect(playlist.displayName).to.equal('my super playlist')
175 expect(playlist.description).to.equal('my super description')
176 expect(playlist.privacy.id).to.equal(VideoPlaylistPrivacy.PUBLIC)
177 expect(playlist.privacy.label).to.equal('Public')
178 expect(playlist.type.id).to.equal(VideoPlaylistType.REGULAR)
179 expect(playlist.type.label).to.equal('Regular')
180
181 expect(playlist.videosLength).to.equal(0)
182
183 expect(playlist.ownerAccount.name).to.equal('root')
184 expect(playlist.ownerAccount.displayName).to.equal('root')
185 expect(playlist.videoChannel.name).to.equal('root_channel')
186 expect(playlist.videoChannel.displayName).to.equal('Main root channel')
187 }
188 }
189 })
190
191 it('Should create a playlist on server 2 and have the playlist on server 1 but not on server 3', async function () {
192 this.timeout(30000)
193
194 {
195 const res = await createVideoPlaylist({
196 url: servers[1].url,
197 token: servers[1].accessToken,
198 playlistAttrs: {
199 displayName: 'playlist 2',
200 privacy: VideoPlaylistPrivacy.PUBLIC,
201 videoChannelId: servers[1].videoChannel.id
202 }
203 })
204 playlistServer2Id1 = res.body.videoPlaylist.id
205 }
206
207 {
208 const res = await createVideoPlaylist({
209 url: servers[ 1 ].url,
210 token: servers[ 1 ].accessToken,
211 playlistAttrs: {
212 displayName: 'playlist 3',
213 privacy: VideoPlaylistPrivacy.PUBLIC,
214 thumbnailfile: 'thumbnail.jpg',
215 videoChannelId: servers[1].videoChannel.id
216 }
217 })
218
219 playlistServer2Id2 = res.body.videoPlaylist.id
220 playlistServer2UUID2 = res.body.videoPlaylist.uuid
221 }
222
223 for (let id of [ playlistServer2Id1, playlistServer2Id2 ]) {
224 await addVideoInPlaylist({
225 url: servers[ 1 ].url,
226 token: servers[ 1 ].accessToken,
227 playlistId: id,
228 elementAttrs: { videoId: servers[ 1 ].videos[ 0 ].id, startTimestamp: 1, stopTimestamp: 2 }
229 })
230 await addVideoInPlaylist({
231 url: servers[ 1 ].url,
232 token: servers[ 1 ].accessToken,
233 playlistId: id,
234 elementAttrs: { videoId: servers[ 1 ].videos[ 1 ].id }
235 })
236 }
237
238 await waitJobs(servers)
239
240 for (const server of [ servers[0], servers[1] ]) {
241 const res = await getVideoPlaylistsList(server.url, 0, 5)
242
243 const playlist2 = res.body.data.find(p => p.displayName === 'playlist 2')
244 expect(playlist2).to.not.be.undefined
245 await testImage(server.url, 'thumbnail-playlist', playlist2.thumbnailPath)
246
247 const playlist3 = res.body.data.find(p => p.displayName === 'playlist 3')
248 expect(playlist3).to.not.be.undefined
249 await testImage(server.url, 'thumbnail', playlist3.thumbnailPath)
250 }
251
252 const res = await getVideoPlaylistsList(servers[2].url, 0, 5)
253 expect(res.body.data.find(p => p.displayName === 'playlist 2')).to.be.undefined
254 expect(res.body.data.find(p => p.displayName === 'playlist 3')).to.be.undefined
255 })
256
257 it('Should have the playlist on server 3 after a new follow', async function () {
258 this.timeout(30000)
259
260 // Server 2 and server 3 follow each other
261 await doubleFollow(servers[1], servers[2])
262
263 const res = await getVideoPlaylistsList(servers[2].url, 0, 5)
264
265 const playlist2 = res.body.data.find(p => p.displayName === 'playlist 2')
266 expect(playlist2).to.not.be.undefined
267 await testImage(servers[2].url, 'thumbnail-playlist', playlist2.thumbnailPath)
268
269 expect(res.body.data.find(p => p.displayName === 'playlist 3')).to.not.be.undefined
270 })
271
272 it('Should correctly list the playlists', async function () {
273 this.timeout(30000)
274
275 {
276 const res = await getVideoPlaylistsList(servers[ 2 ].url, 1, 2, 'createdAt')
277
278 expect(res.body.total).to.equal(3)
279
280 const data: VideoPlaylist[] = res.body.data
281 expect(data).to.have.lengthOf(2)
282 expect(data[ 0 ].displayName).to.equal('playlist 2')
283 expect(data[ 1 ].displayName).to.equal('playlist 3')
284 }
285
286 {
287 const res = await getVideoPlaylistsList(servers[ 2 ].url, 1, 2, '-createdAt')
288
289 expect(res.body.total).to.equal(3)
290
291 const data: VideoPlaylist[] = res.body.data
292 expect(data).to.have.lengthOf(2)
293 expect(data[ 0 ].displayName).to.equal('playlist 2')
294 expect(data[ 1 ].displayName).to.equal('my super playlist')
295 }
296 })
297
298 it('Should list video channel playlists', async function () {
299 this.timeout(30000)
300
301 {
302 const res = await getVideoChannelPlaylistsList(servers[ 0 ].url, 'root_channel', 0, 2, '-createdAt')
303
304 expect(res.body.total).to.equal(1)
305
306 const data: VideoPlaylist[] = res.body.data
307 expect(data).to.have.lengthOf(1)
308 expect(data[ 0 ].displayName).to.equal('my super playlist')
309 }
310 })
311
312 it('Should list account playlists', async function () {
313 this.timeout(30000)
314
315 {
316 const res = await getAccountPlaylistsList(servers[ 1 ].url, 'root', 1, 2, '-createdAt')
317
318 expect(res.body.total).to.equal(2)
319
320 const data: VideoPlaylist[] = res.body.data
321 expect(data).to.have.lengthOf(1)
322 expect(data[ 0 ].displayName).to.equal('playlist 2')
323 }
324
325 {
326 const res = await getAccountPlaylistsList(servers[ 1 ].url, 'root', 1, 2, 'createdAt')
327
328 expect(res.body.total).to.equal(2)
329
330 const data: VideoPlaylist[] = res.body.data
331 expect(data).to.have.lengthOf(1)
332 expect(data[ 0 ].displayName).to.equal('playlist 3')
333 }
334 })
335
336 it('Should not list unlisted or private playlists', async function () {
337 this.timeout(30000)
338
339 await createVideoPlaylist({
340 url: servers[ 1 ].url,
341 token: servers[ 1 ].accessToken,
342 playlistAttrs: {
343 displayName: 'playlist unlisted',
344 privacy: VideoPlaylistPrivacy.UNLISTED
345 }
346 })
347
348 await createVideoPlaylist({
349 url: servers[ 1 ].url,
350 token: servers[ 1 ].accessToken,
351 playlistAttrs: {
352 displayName: 'playlist private',
353 privacy: VideoPlaylistPrivacy.PRIVATE
354 }
355 })
356
357 await waitJobs(servers)
358
359 for (const server of servers) {
360 const results = [
361 await getAccountPlaylistsList(server.url, 'root@localhost:9002', 0, 5, '-createdAt'),
362 await getVideoPlaylistsList(server.url, 0, 2, '-createdAt')
363 ]
364
365 expect(results[0].body.total).to.equal(2)
366 expect(results[1].body.total).to.equal(3)
367
368 for (const res of results) {
369 const data: VideoPlaylist[] = res.body.data
370 expect(data).to.have.lengthOf(2)
371 expect(data[ 0 ].displayName).to.equal('playlist 3')
372 expect(data[ 1 ].displayName).to.equal('playlist 2')
373 }
374 }
375 })
376
377 it('Should update a playlist', async function () {
378 this.timeout(30000)
379
380 await updateVideoPlaylist({
381 url: servers[1].url,
382 token: servers[1].accessToken,
383 playlistAttrs: {
384 displayName: 'playlist 3 updated',
385 description: 'description updated',
386 privacy: VideoPlaylistPrivacy.UNLISTED,
387 thumbnailfile: 'thumbnail.jpg',
388 videoChannelId: servers[1].videoChannel.id
389 },
390 playlistId: playlistServer2Id2
391 })
392
393 await waitJobs(servers)
394
395 for (const server of servers) {
396 const res = await getVideoPlaylist(server.url, playlistServer2UUID2)
397 const playlist: VideoPlaylist = res.body
398
399 expect(playlist.displayName).to.equal('playlist 3 updated')
400 expect(playlist.description).to.equal('description updated')
401
402 expect(playlist.privacy.id).to.equal(VideoPlaylistPrivacy.UNLISTED)
403 expect(playlist.privacy.label).to.equal('Unlisted')
404
405 expect(playlist.type.id).to.equal(VideoPlaylistType.REGULAR)
406 expect(playlist.type.label).to.equal('Regular')
407
408 expect(playlist.videosLength).to.equal(2)
409
410 expect(playlist.ownerAccount.name).to.equal('root')
411 expect(playlist.ownerAccount.displayName).to.equal('root')
412 expect(playlist.videoChannel.name).to.equal('root_channel')
413 expect(playlist.videoChannel.displayName).to.equal('Main root channel')
414 }
415 })
416
417 it('Should create a playlist containing different startTimestamp/endTimestamp videos', async function () {
418 this.timeout(30000)
419
420 const addVideo = (elementAttrs: any) => {
421 return addVideoInPlaylist({ url: servers[0].url, token: servers[0].accessToken, playlistId: playlistServer1Id, elementAttrs })
422 }
423
424 const res = await createVideoPlaylist({
425 url: servers[ 0 ].url,
426 token: servers[ 0 ].accessToken,
427 playlistAttrs: {
428 displayName: 'playlist 4',
429 privacy: VideoPlaylistPrivacy.PUBLIC,
430 videoChannelId: servers[0].videoChannel.id
431 }
432 })
433
434 playlistServer1Id = res.body.videoPlaylist.id
435 playlistServer1UUID = res.body.videoPlaylist.uuid
436
437 await addVideo({ videoId: servers[0].videos[0].uuid, startTimestamp: 15, stopTimestamp: 28 })
438 await addVideo({ videoId: servers[2].videos[1].uuid, startTimestamp: 35 })
439 await addVideo({ videoId: servers[2].videos[2].uuid })
440 await addVideo({ videoId: servers[0].videos[3].uuid, stopTimestamp: 35 })
441 await addVideo({ videoId: servers[0].videos[4].uuid, startTimestamp: 45, stopTimestamp: 60 })
442 await addVideo({ videoId: nsfwVideoServer1, startTimestamp: 5 })
443
444 await waitJobs(servers)
445 })
446
447 it('Should correctly list playlist videos', async function () {
448 this.timeout(30000)
449
450 for (const server of servers) {
451 const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10)
452
453 expect(res.body.total).to.equal(6)
454
455 const videos: Video[] = res.body.data
456 expect(videos).to.have.lengthOf(6)
457
458 expect(videos[0].name).to.equal('video 0 server 1')
459 expect(videos[0].playlistElement.position).to.equal(1)
460 expect(videos[0].playlistElement.startTimestamp).to.equal(15)
461 expect(videos[0].playlistElement.stopTimestamp).to.equal(28)
462
463 expect(videos[1].name).to.equal('video 1 server 3')
464 expect(videos[1].playlistElement.position).to.equal(2)
465 expect(videos[1].playlistElement.startTimestamp).to.equal(35)
466 expect(videos[1].playlistElement.stopTimestamp).to.be.null
467
468 expect(videos[2].name).to.equal('video 2 server 3')
469 expect(videos[2].playlistElement.position).to.equal(3)
470 expect(videos[2].playlistElement.startTimestamp).to.be.null
471 expect(videos[2].playlistElement.stopTimestamp).to.be.null
472
473 expect(videos[3].name).to.equal('video 3 server 1')
474 expect(videos[3].playlistElement.position).to.equal(4)
475 expect(videos[3].playlistElement.startTimestamp).to.be.null
476 expect(videos[3].playlistElement.stopTimestamp).to.equal(35)
477
478 expect(videos[4].name).to.equal('video 4 server 1')
479 expect(videos[4].playlistElement.position).to.equal(5)
480 expect(videos[4].playlistElement.startTimestamp).to.equal(45)
481 expect(videos[4].playlistElement.stopTimestamp).to.equal(60)
482
483 expect(videos[5].name).to.equal('NSFW video')
484 expect(videos[5].playlistElement.position).to.equal(6)
485 expect(videos[5].playlistElement.startTimestamp).to.equal(5)
486 expect(videos[5].playlistElement.stopTimestamp).to.be.null
487
488 const res2 = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10, { nsfw: false })
489 expect(res2.body.total).to.equal(5)
490 expect(res2.body.data.find(v => v.name === 'NSFW video')).to.be.undefined
491
492 const res3 = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 2)
493 expect(res3.body.data).to.have.lengthOf(2)
494 }
495 })
496
497 it('Should reorder the playlist', async function () {
498 this.timeout(30000)
499
500 {
501 await reorderVideosPlaylist({
502 url: servers[ 0 ].url,
503 token: servers[ 0 ].accessToken,
504 playlistId: playlistServer1Id,
505 elementAttrs: {
506 startPosition: 2,
507 insertAfterPosition: 3
508 }
509 })
510
511 await waitJobs(servers)
512
513 for (const server of servers) {
514 const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10)
515 const names = res.body.data.map(v => v.name)
516
517 expect(names).to.deep.equal([
518 'video 0 server 1',
519 'video 2 server 3',
520 'video 1 server 3',
521 'video 3 server 1',
522 'video 4 server 1',
523 'NSFW video'
524 ])
525 }
526 }
527
528 {
529 await reorderVideosPlaylist({
530 url: servers[0].url,
531 token: servers[0].accessToken,
532 playlistId: playlistServer1Id,
533 elementAttrs: {
534 startPosition: 1,
535 reorderLength: 3,
536 insertAfterPosition: 4
537 }
538 })
539
540 await waitJobs(servers)
541
542 for (const server of servers) {
543 const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10)
544 const names = res.body.data.map(v => v.name)
545
546 expect(names).to.deep.equal([
547 'video 3 server 1',
548 'video 0 server 1',
549 'video 2 server 3',
550 'video 1 server 3',
551 'video 4 server 1',
552 'NSFW video'
553 ])
554 }
555 }
556
557 {
558 await reorderVideosPlaylist({
559 url: servers[0].url,
560 token: servers[0].accessToken,
561 playlistId: playlistServer1Id,
562 elementAttrs: {
563 startPosition: 6,
564 insertAfterPosition: 3
565 }
566 })
567
568 await waitJobs(servers)
569
570 for (const server of servers) {
571 const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10)
572 const videos: Video[] = res.body.data
573
574 const names = videos.map(v => v.name)
575
576 expect(names).to.deep.equal([
577 'video 3 server 1',
578 'video 0 server 1',
579 'video 2 server 3',
580 'NSFW video',
581 'video 1 server 3',
582 'video 4 server 1'
583 ])
584
585 for (let i = 1; i <= videos.length; i++) {
586 expect(videos[i - 1].playlistElement.position).to.equal(i)
587 }
588 }
589 }
590 })
591
592 it('Should update startTimestamp/endTimestamp of some elements', async function () {
593 this.timeout(30000)
594
595 await updateVideoPlaylistElement({
596 url: servers[0].url,
597 token: servers[0].accessToken,
598 playlistId: playlistServer1Id,
599 videoId: servers[0].videos[3].uuid,
600 elementAttrs: {
601 startTimestamp: 1
602 }
603 })
604
605 await updateVideoPlaylistElement({
606 url: servers[0].url,
607 token: servers[0].accessToken,
608 playlistId: playlistServer1Id,
609 videoId: servers[0].videos[4].uuid,
610 elementAttrs: {
611 stopTimestamp: null
612 }
613 })
614
615 await waitJobs(servers)
616
617 for (const server of servers) {
618 const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10)
619 const videos: Video[] = res.body.data
620
621 expect(videos[0].name).to.equal('video 3 server 1')
622 expect(videos[0].playlistElement.position).to.equal(1)
623 expect(videos[0].playlistElement.startTimestamp).to.equal(1)
624 expect(videos[0].playlistElement.stopTimestamp).to.equal(35)
625
626 expect(videos[5].name).to.equal('video 4 server 1')
627 expect(videos[5].playlistElement.position).to.equal(6)
628 expect(videos[5].playlistElement.startTimestamp).to.equal(45)
629 expect(videos[5].playlistElement.stopTimestamp).to.be.null
630 }
631 })
632
633 it('Should check videos existence in my playlist', async function () {
634 const videoIds = [
635 servers[0].videos[0].id,
636 42000,
637 servers[0].videos[3].id,
638 43000,
639 servers[0].videos[4].id
640 ]
641 const res = await doVideosExistInMyPlaylist(servers[ 0 ].url, servers[ 0 ].accessToken, videoIds)
642 const obj = res.body as VideoExistInPlaylist
643
644 {
645 const elem = obj[servers[0].videos[0].id]
646 expect(elem).to.have.lengthOf(1)
647 expect(elem[ 0 ].playlistId).to.equal(playlistServer1Id)
648 expect(elem[ 0 ].startTimestamp).to.equal(15)
649 expect(elem[ 0 ].stopTimestamp).to.equal(28)
650 }
651
652 {
653 const elem = obj[servers[0].videos[3].id]
654 expect(elem).to.have.lengthOf(1)
655 expect(elem[ 0 ].playlistId).to.equal(playlistServer1Id)
656 expect(elem[ 0 ].startTimestamp).to.equal(1)
657 expect(elem[ 0 ].stopTimestamp).to.equal(35)
658 }
659
660 {
661 const elem = obj[servers[0].videos[4].id]
662 expect(elem).to.have.lengthOf(1)
663 expect(elem[ 0 ].playlistId).to.equal(playlistServer1Id)
664 expect(elem[ 0 ].startTimestamp).to.equal(45)
665 expect(elem[ 0 ].stopTimestamp).to.equal(null)
666 }
667
668 expect(obj[42000]).to.have.lengthOf(0)
669 expect(obj[43000]).to.have.lengthOf(0)
670 })
671
672 it('Should automatically update updatedAt field of playlists', async function () {
673 const server = servers[1]
674 const videoId = servers[1].videos[5].id
675
676 async function getPlaylistNames () {
677 const res = await getAccountPlaylistsListWithToken(server.url, server.accessToken, 'root', 0, 5, undefined, '-updatedAt')
678
679 return (res.body.data as VideoPlaylist[]).map(p => p.displayName)
680 }
681
682 const elementAttrs = { videoId }
683 await addVideoInPlaylist({ url: server.url, token: server.accessToken, playlistId: playlistServer2Id1, elementAttrs })
684 await addVideoInPlaylist({ url: server.url, token: server.accessToken, playlistId: playlistServer2Id2, elementAttrs })
685
686 const names1 = await getPlaylistNames()
687 expect(names1[0]).to.equal('playlist 3 updated')
688 expect(names1[1]).to.equal('playlist 2')
689
690 await removeVideoFromPlaylist({ url: server.url, token: server.accessToken, playlistId: playlistServer2Id1, videoId })
691
692 const names2 = await getPlaylistNames()
693 expect(names2[0]).to.equal('playlist 2')
694 expect(names2[1]).to.equal('playlist 3 updated')
695
696 await removeVideoFromPlaylist({ url: server.url, token: server.accessToken, playlistId: playlistServer2Id2, videoId })
697
698 const names3 = await getPlaylistNames()
699 expect(names3[0]).to.equal('playlist 3 updated')
700 expect(names3[1]).to.equal('playlist 2')
701 })
702
703 it('Should delete some elements', async function () {
704 this.timeout(30000)
705
706 await removeVideoFromPlaylist({
707 url: servers[0].url,
708 token: servers[0].accessToken,
709 playlistId: playlistServer1Id,
710 videoId: servers[0].videos[3].uuid
711 })
712
713 await removeVideoFromPlaylist({
714 url: servers[0].url,
715 token: servers[0].accessToken,
716 playlistId: playlistServer1Id,
717 videoId: nsfwVideoServer1
718 })
719
720 await waitJobs(servers)
721
722 for (const server of servers) {
723 const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10)
724
725 expect(res.body.total).to.equal(4)
726
727 const videos: Video[] = res.body.data
728 expect(videos).to.have.lengthOf(4)
729
730 expect(videos[ 0 ].name).to.equal('video 0 server 1')
731 expect(videos[ 0 ].playlistElement.position).to.equal(1)
732
733 expect(videos[ 1 ].name).to.equal('video 2 server 3')
734 expect(videos[ 1 ].playlistElement.position).to.equal(2)
735
736 expect(videos[ 2 ].name).to.equal('video 1 server 3')
737 expect(videos[ 2 ].playlistElement.position).to.equal(3)
738
739 expect(videos[ 3 ].name).to.equal('video 4 server 1')
740 expect(videos[ 3 ].playlistElement.position).to.equal(4)
741 }
742 })
743
744 it('Should delete the playlist on server 1 and delete on server 2 and 3', async function () {
745 this.timeout(30000)
746
747 await deleteVideoPlaylist(servers[0].url, servers[0].accessToken, playlistServer1Id)
748
749 await waitJobs(servers)
750
751 for (const server of servers) {
752 await getVideoPlaylist(server.url, playlistServer1UUID, 404)
753 }
754 })
755
756 it('Should have deleted the thumbnail on server 1, 2 and 3', async function () {
757 this.timeout(30000)
758
759 for (const server of servers) {
760 await checkPlaylistFilesWereRemoved(playlistServer1UUID, server.serverNumber)
761 }
762 })
763
764 it('Should unfollow servers 1 and 2 and hide their playlists', async function () {
765 this.timeout(30000)
766
767 const finder = data => data.find(p => p.displayName === 'my super playlist')
768
769 {
770 const res = await getVideoPlaylistsList(servers[ 2 ].url, 0, 5)
771 expect(res.body.total).to.equal(2)
772 expect(finder(res.body.data)).to.not.be.undefined
773 }
774
775 await unfollow(servers[2].url, servers[2].accessToken, servers[0])
776
777 {
778 const res = await getVideoPlaylistsList(servers[ 2 ].url, 0, 5)
779 expect(res.body.total).to.equal(1)
780
781 expect(finder(res.body.data)).to.be.undefined
782 }
783 })
784
785 it('Should delete a channel and put the associated playlist in private mode', async function () {
786 this.timeout(30000)
787
788 const res = await addVideoChannel(servers[0].url, servers[0].accessToken, { name: 'super_channel', displayName: 'super channel' })
789 const videoChannelId = res.body.videoChannel.id
790
791 const res2 = await createVideoPlaylist({
792 url: servers[0].url,
793 token: servers[0].accessToken,
794 playlistAttrs: {
795 displayName: 'channel playlist',
796 privacy: VideoPlaylistPrivacy.PUBLIC,
797 videoChannelId
798 }
799 })
800 const videoPlaylistUUID = res2.body.videoPlaylist.uuid
801
802 await waitJobs(servers)
803
804 await deleteVideoChannel(servers[0].url, servers[0].accessToken, 'super_channel')
805
806 await waitJobs(servers)
807
808 const res3 = await getVideoPlaylistWithToken(servers[0].url, servers[0].accessToken, videoPlaylistUUID)
809 expect(res3.body.displayName).to.equal('channel playlist')
810 expect(res3.body.privacy.id).to.equal(VideoPlaylistPrivacy.PRIVATE)
811
812 await getVideoPlaylist(servers[1].url, videoPlaylistUUID, 404)
813 })
814
815 it('Should delete an account and delete its playlists', async function () {
816 this.timeout(30000)
817
818 const user = { username: 'user_1', password: 'password' }
819 const res = await createUser({
820 url: servers[ 0 ].url,
821 accessToken: servers[ 0 ].accessToken,
822 username: user.username,
823 password: user.password
824 })
825
826 const userId = res.body.user.id
827 const userAccessToken = await userLogin(servers[0], user)
828
829 const resChannel = await getMyUserInformation(servers[0].url, userAccessToken)
830 const userChannel = (resChannel.body as User).videoChannels[0]
831
832 await createVideoPlaylist({
833 url: servers[0].url,
834 token: userAccessToken,
835 playlistAttrs: {
836 displayName: 'playlist to be deleted',
837 privacy: VideoPlaylistPrivacy.PUBLIC,
838 videoChannelId: userChannel.id
839 }
840 })
841
842 await waitJobs(servers)
843
844 const finder = data => data.find(p => p.displayName === 'playlist to be deleted')
845
846 {
847 for (const server of [ servers[0], servers[1] ]) {
848 const res = await getVideoPlaylistsList(server.url, 0, 15)
849 expect(finder(res.body.data)).to.not.be.undefined
850 }
851 }
852
853 await removeUser(servers[0].url, userId, servers[0].accessToken)
854 await waitJobs(servers)
855
856 {
857 for (const server of [ servers[0], servers[1] ]) {
858 const res = await getVideoPlaylistsList(server.url, 0, 15)
859 expect(finder(res.body.data)).to.be.undefined
860 }
861 }
862 })
863
864 after(async function () {
865 await cleanupTests(servers)
866 })
867})
diff --git a/server/tests/api/videos/video-privacy.ts b/server/tests/api/videos/video-privacy.ts
index 0b4e66369..ef1cf0f07 100644
--- a/server/tests/api/videos/video-privacy.ts
+++ b/server/tests/api/videos/video-privacy.ts
@@ -4,18 +4,19 @@ import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum' 5import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum'
6import { 6import {
7 cleanupTests,
7 flushAndRunMultipleServers, 8 flushAndRunMultipleServers,
8 getVideosList, 9 getVideosList,
9 killallServers, 10 killallServers,
10 ServerInfo, 11 ServerInfo,
11 setAccessTokensToServers, 12 setAccessTokensToServers,
12 uploadVideo 13 uploadVideo
13} from '../../../../shared/utils/index' 14} from '../../../../shared/extra-utils/index'
14import { doubleFollow } from '../../../../shared/utils/server/follows' 15import { doubleFollow } from '../../../../shared/extra-utils/server/follows'
15import { userLogin } from '../../../../shared/utils/users/login' 16import { userLogin } from '../../../../shared/extra-utils/users/login'
16import { createUser } from '../../../../shared/utils/users/users' 17import { createUser } from '../../../../shared/extra-utils/users/users'
17import { getMyVideos, getVideo, getVideoWithToken, updateVideo } from '../../../../shared/utils/videos/videos' 18import { getMyVideos, getVideo, getVideoWithToken, updateVideo } from '../../../../shared/extra-utils/videos/videos'
18import { waitJobs } from '../../../../shared/utils/server/jobs' 19import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
19 20
20const expect = chai.expect 21const expect = chai.expect
21 22
@@ -78,7 +79,7 @@ describe('Test video privacy', function () {
78 username: 'hello', 79 username: 'hello',
79 password: 'super password' 80 password: 'super password'
80 } 81 }
81 await createUser(servers[0].url, servers[0].accessToken, user.username, user.password) 82 await createUser({ url: servers[ 0 ].url, accessToken: servers[ 0 ].accessToken, username: user.username, password: user.password })
82 83
83 const token = await userLogin(servers[0], user) 84 const token = await userLogin(servers[0], user)
84 await getVideoWithToken(servers[0].url, token, privateVideoUUID, 403) 85 await getVideoWithToken(servers[0].url, token, privateVideoUUID, 403)
@@ -153,6 +154,6 @@ describe('Test video privacy', function () {
153 }) 154 })
154 155
155 after(async function () { 156 after(async function () {
156 killallServers(servers) 157 await cleanupTests(servers)
157 }) 158 })
158}) 159})
diff --git a/server/tests/api/videos/video-schedule-update.ts b/server/tests/api/videos/video-schedule-update.ts
index 632c4244c..64f657780 100644
--- a/server/tests/api/videos/video-schedule-update.ts
+++ b/server/tests/api/videos/video-schedule-update.ts
@@ -4,6 +4,7 @@ import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { VideoPrivacy } from '../../../../shared/models/videos' 5import { VideoPrivacy } from '../../../../shared/models/videos'
6import { 6import {
7 cleanupTests,
7 doubleFollow, 8 doubleFollow,
8 flushAndRunMultipleServers, 9 flushAndRunMultipleServers,
9 getMyVideos, 10 getMyVideos,
@@ -15,8 +16,8 @@ import {
15 updateVideo, 16 updateVideo,
16 uploadVideo, 17 uploadVideo,
17 wait 18 wait
18} from '../../../../shared/utils' 19} from '../../../../shared/extra-utils'
19import { waitJobs } from '../../../../shared/utils/server/jobs' 20import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
20 21
21const expect = chai.expect 22const expect = chai.expect
22 23
@@ -166,6 +167,6 @@ describe('Test video update scheduler', function () {
166 }) 167 })
167 168
168 after(async function () { 169 after(async function () {
169 killallServers(servers) 170 await cleanupTests(servers)
170 }) 171 })
171}) 172})
diff --git a/server/tests/api/videos/video-transcoder.ts b/server/tests/api/videos/video-transcoder.ts
index eefd32ef8..3cd43e99b 100644
--- a/server/tests/api/videos/video-transcoder.ts
+++ b/server/tests/api/videos/video-transcoder.ts
@@ -6,7 +6,7 @@ import { omit } from 'lodash'
6import { getMaxBitrate, VideoDetails, VideoResolution, VideoState } from '../../../../shared/models/videos' 6import { getMaxBitrate, VideoDetails, VideoResolution, VideoState } from '../../../../shared/models/videos'
7import { audio, getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' 7import { audio, getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
8import { 8import {
9 buildAbsoluteFixturePath, 9 buildAbsoluteFixturePath, cleanupTests,
10 doubleFollow, 10 doubleFollow,
11 flushAndRunMultipleServers, 11 flushAndRunMultipleServers,
12 generateHighBitrateVideo, 12 generateHighBitrateVideo,
@@ -19,9 +19,9 @@ import {
19 setAccessTokensToServers, 19 setAccessTokensToServers,
20 uploadVideo, 20 uploadVideo,
21 webtorrentAdd 21 webtorrentAdd
22} from '../../../../shared/utils' 22} from '../../../../shared/extra-utils'
23import { extname, join } from 'path' 23import { extname, join } from 'path'
24import { waitJobs } from '../../../../shared/utils/server/jobs' 24import { waitJobs } from '../../../../shared/extra-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
@@ -350,6 +350,6 @@ describe('Test video transcoding', function () {
350 }) 350 })
351 351
352 after(async function () { 352 after(async function () {
353 killallServers(servers) 353 await cleanupTests(servers)
354 }) 354 })
355}) 355})
diff --git a/server/tests/api/videos/videos-filter.ts b/server/tests/api/videos/videos-filter.ts
index 59e37ad86..e1e65260f 100644
--- a/server/tests/api/videos/videos-filter.ts
+++ b/server/tests/api/videos/videos-filter.ts
@@ -3,6 +3,7 @@
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { 5import {
6 cleanupTests,
6 createUser, 7 createUser,
7 doubleFollow, 8 doubleFollow,
8 flushAndRunMultipleServers, 9 flushAndRunMultipleServers,
@@ -13,7 +14,7 @@ import {
13 setAccessTokensToServers, 14 setAccessTokensToServers,
14 uploadVideo, 15 uploadVideo,
15 userLogin 16 userLogin
16} from '../../../../shared/utils' 17} from '../../../../shared/extra-utils'
17import { Video, VideoPrivacy } from '../../../../shared/models/videos' 18import { Video, VideoPrivacy } from '../../../../shared/models/videos'
18import { UserRole } from '../../../../shared/models/users' 19import { UserRole } from '../../../../shared/models/users'
19 20
@@ -55,8 +56,6 @@ describe('Test videos filter validator', function () {
55 before(async function () { 56 before(async function () {
56 this.timeout(120000) 57 this.timeout(120000)
57 58
58 await flushTests()
59
60 servers = await flushAndRunMultipleServers(2) 59 servers = await flushAndRunMultipleServers(2)
61 60
62 await setAccessTokensToServers(servers) 61 await setAccessTokensToServers(servers)
@@ -64,13 +63,15 @@ describe('Test videos filter validator', function () {
64 for (const server of servers) { 63 for (const server of servers) {
65 const moderator = { username: 'moderator', password: 'my super password' } 64 const moderator = { username: 'moderator', password: 'my super password' }
66 await createUser( 65 await createUser(
67 server.url, 66 {
68 server.accessToken, 67 url: server.url,
69 moderator.username, 68 accessToken: server.accessToken,
70 moderator.password, 69 username: moderator.username,
71 undefined, 70 password: moderator.password,
72 undefined, 71 videoQuota: undefined,
73 UserRole.MODERATOR 72 videoQuotaDaily: undefined,
73 role: UserRole.MODERATOR
74 }
74 ) 75 )
75 server['moderatorAccessToken'] = await userLogin(server, moderator) 76 server['moderatorAccessToken'] = await userLogin(server, moderator)
76 77
@@ -120,11 +121,6 @@ describe('Test videos filter validator', function () {
120 }) 121 })
121 122
122 after(async function () { 123 after(async function () {
123 killallServers(servers) 124 await cleanupTests(servers)
124
125 // Keep the logs if the test failed
126 if (this['ok']) {
127 await flushTests()
128 }
129 }) 125 })
130}) 126})
diff --git a/server/tests/api/videos/videos-history.ts b/server/tests/api/videos/videos-history.ts
index f654a422b..c7e55c1ab 100644
--- a/server/tests/api/videos/videos-history.ts
+++ b/server/tests/api/videos/videos-history.ts
@@ -3,21 +3,23 @@
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { 5import {
6 cleanupTests,
6 createUser, 7 createUser,
7 flushTests, 8 flushAndRunServer,
8 getVideosListWithToken, 9 getVideosListWithToken,
9 getVideoWithToken, 10 getVideoWithToken,
10 killallServers, 11 killallServers,
11 runServer, 12 reRunServer,
12 searchVideoWithToken, 13 searchVideoWithToken,
13 ServerInfo, 14 ServerInfo,
14 setAccessTokensToServers, 15 setAccessTokensToServers,
15 updateMyUser, 16 updateMyUser,
16 uploadVideo, 17 uploadVideo,
17 userLogin 18 userLogin,
18} from '../../../../shared/utils' 19 wait
20} from '../../../../shared/extra-utils'
19import { Video, VideoDetails } from '../../../../shared/models/videos' 21import { Video, VideoDetails } from '../../../../shared/models/videos'
20import { listMyVideosHistory, removeMyVideosHistory, userWatchVideo } from '../../../../shared/utils/videos/video-history' 22import { listMyVideosHistory, removeMyVideosHistory, userWatchVideo } from '../../../../shared/extra-utils/videos/video-history'
21 23
22const expect = chai.expect 24const expect = chai.expect
23 25
@@ -32,9 +34,7 @@ describe('Test videos history', function () {
32 before(async function () { 34 before(async function () {
33 this.timeout(30000) 35 this.timeout(30000)
34 36
35 await flushTests() 37 server = await flushAndRunServer(1)
36
37 server = await runServer(1)
38 38
39 await setAccessTokensToServers([ server ]) 39 await setAccessTokensToServers([ server ])
40 40
@@ -57,7 +57,7 @@ describe('Test videos history', function () {
57 username: 'user_1', 57 username: 'user_1',
58 password: 'super password' 58 password: 'super password'
59 } 59 }
60 await createUser(server.url, server.accessToken, user.username, user.password) 60 await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
61 userAccessToken = await userLogin(server, user) 61 userAccessToken = await userLogin(server, user)
62 }) 62 })
63 63
@@ -192,12 +192,36 @@ describe('Test videos history', function () {
192 expect(videos[1].name).to.equal('video 3') 192 expect(videos[1].name).to.equal('video 3')
193 }) 193 })
194 194
195 after(async function () { 195 it('Should not clean old history', async function () {
196 this.timeout(50000)
197
196 killallServers([ server ]) 198 killallServers([ server ])
197 199
198 // Keep the logs if the test failed 200 await reRunServer(server, { history: { videos: { max_age: '10 days' } } })
199 if (this['ok']) { 201
200 await flushTests() 202 await wait(6000)
201 } 203
204 // Should still have history
205
206 const res = await listMyVideosHistory(server.url, server.accessToken)
207
208 expect(res.body.total).to.equal(2)
209 })
210
211 it('Should clean old history', async function () {
212 this.timeout(50000)
213
214 killallServers([ server ])
215
216 await reRunServer(server, { history: { videos: { max_age: '5 seconds' } } })
217
218 await wait(6000)
219
220 const res = await listMyVideosHistory(server.url, server.accessToken)
221 expect(res.body.total).to.equal(0)
222 })
223
224 after(async function () {
225 await cleanupTests([ server ])
202 }) 226 })
203}) 227})
diff --git a/server/tests/api/videos/videos-overview.ts b/server/tests/api/videos/videos-overview.ts
index 7221bcae6..975a5c87a 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 '../../../../shared/utils' 5import { cleanupTests, flushAndRunServer, ServerInfo, setAccessTokensToServers, uploadVideo } from '../../../../shared/extra-utils'
6import { getVideosOverview } from '../../../../shared/utils/overviews/overviews' 6import { getVideosOverview } from '../../../../shared/extra-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
@@ -14,9 +14,7 @@ describe('Test a videos overview', function () {
14 before(async function () { 14 before(async function () {
15 this.timeout(30000) 15 this.timeout(30000)
16 16
17 await flushTests() 17 server = await flushAndRunServer(1)
18
19 server = await runServer(1)
20 18
21 await setAccessTokensToServers([ server ]) 19 await setAccessTokensToServers([ server ])
22 }) 20 })
@@ -90,11 +88,6 @@ describe('Test a videos overview', function () {
90 }) 88 })
91 89
92 after(async function () { 90 after(async function () {
93 killallServers([ server ]) 91 await cleanupTests([ server ])
94
95 // Keep the logs if the test failed
96 if (this['ok']) {
97 await flushTests()
98 }
99 }) 92 })
100}) 93})
diff --git a/server/tests/api/videos/videos-views-cleaner.ts b/server/tests/api/videos/videos-views-cleaner.ts
new file mode 100644
index 000000000..c21d46d56
--- /dev/null
+++ b/server/tests/api/videos/videos-views-cleaner.ts
@@ -0,0 +1,106 @@
1/* tslint:disable:no-unused-expression */
2
3import * as chai from 'chai'
4import 'mocha'
5import {
6 flushAndRunMultipleServers,
7 flushTests,
8 killallServers,
9 reRunServer,
10 flushAndRunServer,
11 ServerInfo,
12 setAccessTokensToServers,
13 uploadVideo, uploadVideoAndGetId, viewVideo, wait, countVideoViewsOf, doubleFollow, waitJobs, cleanupTests
14} from '../../../../shared/extra-utils'
15import { getVideosOverview } from '../../../../shared/extra-utils/overviews/overviews'
16import { VideosOverview } from '../../../../shared/models/overviews'
17import { listMyVideosHistory } from '../../../../shared/extra-utils/videos/video-history'
18
19const expect = chai.expect
20
21describe('Test video views cleaner', function () {
22 let servers: ServerInfo[]
23
24 let videoIdServer1: string
25 let videoIdServer2: string
26
27 before(async function () {
28 this.timeout(50000)
29
30 servers = await flushAndRunMultipleServers(2)
31 await setAccessTokensToServers(servers)
32
33 await doubleFollow(servers[0], servers[1])
34
35 videoIdServer1 = (await uploadVideoAndGetId({ server: servers[0], videoName: 'video server 1' })).uuid
36 videoIdServer2 = (await uploadVideoAndGetId({ server: servers[1], videoName: 'video server 2' })).uuid
37
38 await waitJobs(servers)
39
40 await viewVideo(servers[0].url, videoIdServer1)
41 await viewVideo(servers[1].url, videoIdServer1)
42 await viewVideo(servers[0].url, videoIdServer2)
43 await viewVideo(servers[1].url, videoIdServer2)
44
45 await waitJobs(servers)
46 })
47
48 it('Should not clean old video views', async function () {
49 this.timeout(50000)
50
51 killallServers([ servers[0] ])
52
53 await reRunServer(servers[0], { views: { videos: { remote: { max_age: '10 days' } } } })
54
55 await wait(6000)
56
57 // Should still have views
58
59 {
60 for (const server of servers) {
61 const total = await countVideoViewsOf(server.serverNumber, videoIdServer1)
62 expect(total).to.equal(2)
63 }
64 }
65
66 {
67 for (const server of servers) {
68 const total = await countVideoViewsOf(server.serverNumber, videoIdServer2)
69 expect(total).to.equal(2)
70 }
71 }
72 })
73
74 it('Should clean old video views', async function () {
75 this.timeout(50000)
76
77 this.timeout(50000)
78
79 killallServers([ servers[0] ])
80
81 await reRunServer(servers[0], { views: { videos: { remote: { max_age: '5 seconds' } } } })
82
83 await wait(6000)
84
85 // Should still have views
86
87 {
88 for (const server of servers) {
89 const total = await countVideoViewsOf(server.serverNumber, videoIdServer1)
90 expect(total).to.equal(2)
91 }
92 }
93
94 {
95 const totalServer1 = await countVideoViewsOf(servers[0].serverNumber, videoIdServer2)
96 expect(totalServer1).to.equal(0)
97
98 const totalServer2 = await countVideoViewsOf(servers[1].serverNumber, videoIdServer2)
99 expect(totalServer2).to.equal(2)
100 }
101 })
102
103 after(async function () {
104 await cleanupTests(servers)
105 })
106})
diff --git a/server/tests/cli/create-import-video-file-job.ts b/server/tests/cli/create-import-video-file-job.ts
index 4acda47b1..0d378c1aa 100644
--- a/server/tests/cli/create-import-video-file-job.ts
+++ b/server/tests/cli/create-import-video-file-job.ts
@@ -4,6 +4,7 @@ import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { VideoDetails, VideoFile } from '../../../shared/models/videos' 5import { VideoDetails, VideoFile } from '../../../shared/models/videos'
6import { 6import {
7 cleanupTests,
7 doubleFollow, 8 doubleFollow,
8 execCLI, 9 execCLI,
9 flushAndRunMultipleServers, 10 flushAndRunMultipleServers,
@@ -15,8 +16,8 @@ import {
15 ServerInfo, 16 ServerInfo,
16 setAccessTokensToServers, 17 setAccessTokensToServers,
17 uploadVideo 18 uploadVideo
18} from '../../../shared/utils' 19} from '../../../shared/extra-utils'
19import { waitJobs } from '../../../shared/utils/server/jobs' 20import { waitJobs } from '../../../shared/extra-utils/server/jobs'
20 21
21const expect = chai.expect 22const expect = chai.expect
22 23
@@ -39,7 +40,6 @@ describe('Test create import video jobs', function () {
39 40
40 before(async function () { 41 before(async function () {
41 this.timeout(90000) 42 this.timeout(90000)
42 await flushTests()
43 43
44 // Run server 2 to have transcoding enabled 44 // Run server 2 to have transcoding enabled
45 servers = await flushAndRunMultipleServers(2) 45 servers = await flushAndRunMultipleServers(2)
@@ -132,6 +132,6 @@ describe('Test create import video jobs', function () {
132 }) 132 })
133 133
134 after(async function () { 134 after(async function () {
135 killallServers(servers) 135 await cleanupTests(servers)
136 }) 136 })
137}) 137})
diff --git a/server/tests/cli/create-transcoding-job.ts b/server/tests/cli/create-transcoding-job.ts
index 50be5fa19..1c0e10066 100644
--- a/server/tests/cli/create-transcoding-job.ts
+++ b/server/tests/cli/create-transcoding-job.ts
@@ -4,6 +4,7 @@ import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { VideoDetails } from '../../../shared/models/videos' 5import { VideoDetails } from '../../../shared/models/videos'
6import { 6import {
7 cleanupTests,
7 doubleFollow, 8 doubleFollow,
8 execCLI, 9 execCLI,
9 flushAndRunMultipleServers, 10 flushAndRunMultipleServers,
@@ -15,8 +16,8 @@ import {
15 ServerInfo, 16 ServerInfo,
16 setAccessTokensToServers, 17 setAccessTokensToServers,
17 uploadVideo, wait 18 uploadVideo, wait
18} from '../../../shared/utils' 19} from '../../../shared/extra-utils'
19import { waitJobs } from '../../../shared/utils/server/jobs' 20import { waitJobs } from '../../../shared/extra-utils/server/jobs'
20 21
21const expect = chai.expect 22const expect = chai.expect
22 23
@@ -28,8 +29,6 @@ describe('Test create transcoding jobs', function () {
28 before(async function () { 29 before(async function () {
29 this.timeout(60000) 30 this.timeout(60000)
30 31
31 await flushTests()
32
33 // Run server 2 to have transcoding enabled 32 // Run server 2 to have transcoding enabled
34 servers = await flushAndRunMultipleServers(2) 33 servers = await flushAndRunMultipleServers(2)
35 await setAccessTokensToServers(servers) 34 await setAccessTokensToServers(servers)
@@ -127,6 +126,6 @@ describe('Test create transcoding jobs', function () {
127 }) 126 })
128 127
129 after(async function () { 128 after(async function () {
130 killallServers(servers) 129 await cleanupTests(servers)
131 }) 130 })
132}) 131})
diff --git a/server/tests/cli/optimize-old-videos.ts b/server/tests/cli/optimize-old-videos.ts
index 6f6bc25a6..5e12c0089 100644
--- a/server/tests/cli/optimize-old-videos.ts
+++ b/server/tests/cli/optimize-old-videos.ts
@@ -4,6 +4,7 @@ import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { getMaxBitrate, Video, VideoDetails, VideoResolution } from '../../../shared/models/videos' 5import { getMaxBitrate, Video, VideoDetails, VideoResolution } from '../../../shared/models/videos'
6import { 6import {
7 cleanupTests,
7 doubleFollow, 8 doubleFollow,
8 execCLI, 9 execCLI,
9 flushAndRunMultipleServers, 10 flushAndRunMultipleServers,
@@ -15,10 +16,10 @@ import {
15 ServerInfo, 16 ServerInfo,
16 setAccessTokensToServers, 17 setAccessTokensToServers,
17 uploadVideo, viewVideo, wait 18 uploadVideo, viewVideo, wait
18} from '../../../shared/utils' 19} from '../../../shared/extra-utils'
19import { waitJobs } from '../../../shared/utils/server/jobs' 20import { waitJobs } from '../../../shared/extra-utils/server/jobs'
20import { getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../../helpers/ffmpeg-utils' 21import { getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../../helpers/ffmpeg-utils'
21import { VIDEO_TRANSCODING_FPS } from '../../initializers' 22import { VIDEO_TRANSCODING_FPS } from '../../initializers/constants'
22import { join } from 'path' 23import { join } from 'path'
23 24
24const expect = chai.expect 25const expect = chai.expect
@@ -31,8 +32,6 @@ describe('Test optimize old videos', function () {
31 before(async function () { 32 before(async function () {
32 this.timeout(200000) 33 this.timeout(200000)
33 34
34 await flushTests()
35
36 // Run server 2 to have transcoding enabled 35 // Run server 2 to have transcoding enabled
37 servers = await flushAndRunMultipleServers(2) 36 servers = await flushAndRunMultipleServers(2)
38 await setAccessTokensToServers(servers) 37 await setAccessTokensToServers(servers)
@@ -115,6 +114,6 @@ describe('Test optimize old videos', function () {
115 }) 114 })
116 115
117 after(async function () { 116 after(async function () {
118 killallServers(servers) 117 await cleanupTests(servers)
119 }) 118 })
120}) 119})
diff --git a/server/tests/cli/peertube.ts b/server/tests/cli/peertube.ts
index e2836d0c3..80bbc98d5 100644
--- a/server/tests/cli/peertube.ts
+++ b/server/tests/cli/peertube.ts
@@ -8,10 +8,10 @@ import {
8 flushTests, 8 flushTests,
9 getEnvCli, 9 getEnvCli,
10 killallServers, 10 killallServers,
11 runServer, 11 flushAndRunServer,
12 ServerInfo, 12 ServerInfo,
13 setAccessTokensToServers 13 setAccessTokensToServers, cleanupTests
14} from '../../../shared/utils' 14} from '../../../shared/extra-utils'
15 15
16describe('Test CLI wrapper', function () { 16describe('Test CLI wrapper', function () {
17 let server: ServerInfo 17 let server: ServerInfo
@@ -19,12 +19,10 @@ describe('Test CLI wrapper', function () {
19 19
20 before(async function () { 20 before(async function () {
21 this.timeout(30000) 21 this.timeout(30000)
22 22 server = await flushAndRunServer(1)
23 await flushTests()
24 server = await runServer(1)
25 await setAccessTokensToServers([ server ]) 23 await setAccessTokensToServers([ server ])
26 24
27 await createUser(server.url, server.accessToken, 'user_1', 'super password') 25 await createUser({ url: server.url, accessToken: server.accessToken, username: 'user_1', password: 'super password' })
28 }) 26 })
29 27
30 it('Should display no selected instance', async function () { 28 it('Should display no selected instance', async function () {
@@ -48,6 +46,6 @@ describe('Test CLI wrapper', function () {
48 46
49 await execCLI(cmd + ` auth del ${server.url}`) 47 await execCLI(cmd + ` auth del ${server.url}`)
50 48
51 killallServers([ server ]) 49 await cleanupTests([ server ])
52 }) 50 })
53}) 51})
diff --git a/server/tests/cli/reset-password.ts b/server/tests/cli/reset-password.ts
index 1b65f7e39..6abb6738f 100644
--- a/server/tests/cli/reset-password.ts
+++ b/server/tests/cli/reset-password.ts
@@ -1,28 +1,25 @@
1import 'mocha' 1import 'mocha'
2 2
3import { 3import {
4 cleanupTests,
4 createUser, 5 createUser,
5 execCLI, 6 execCLI,
6 flushTests, 7 flushAndRunServer,
7 getEnvCli, 8 getEnvCli,
8 killallServers,
9 login, 9 login,
10 runServer,
11 ServerInfo, 10 ServerInfo,
12 setAccessTokensToServers 11 setAccessTokensToServers
13} from '../../../shared/utils' 12} from '../../../shared/extra-utils'
14 13
15describe('Test reset password scripts', function () { 14describe('Test reset password scripts', function () {
16 let server: ServerInfo 15 let server: ServerInfo
17 16
18 before(async function () { 17 before(async function () {
19 this.timeout(30000) 18 this.timeout(30000)
20 19 server = await flushAndRunServer(1)
21 await flushTests()
22 server = await runServer(1)
23 await setAccessTokensToServers([ server ]) 20 await setAccessTokensToServers([ server ])
24 21
25 await createUser(server.url, server.accessToken, 'user_1', 'super password') 22 await createUser({ url: server.url, accessToken: server.accessToken, username: 'user_1', password: 'super password' })
26 }) 23 })
27 24
28 it('Should change the user password from CLI', async function () { 25 it('Should change the user password from CLI', async function () {
@@ -35,6 +32,6 @@ describe('Test reset password scripts', function () {
35 }) 32 })
36 33
37 after(async function () { 34 after(async function () {
38 killallServers([ server ]) 35 await cleanupTests([ server ])
39 }) 36 })
40}) 37})
diff --git a/server/tests/cli/update-host.ts b/server/tests/cli/update-host.ts
index 811ea6a9f..55c43b32f 100644
--- a/server/tests/cli/update-host.ts
+++ b/server/tests/cli/update-host.ts
@@ -3,26 +3,26 @@
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 '../../../shared/utils/server/jobs' 6import { waitJobs } from '../../../shared/extra-utils/server/jobs'
7import { addVideoCommentThread } from '../../../shared/utils/videos/video-comments' 7import { addVideoCommentThread } from '../../../shared/extra-utils/videos/video-comments'
8import { 8import {
9 addVideoChannel, 9 addVideoChannel,
10 cleanupTests,
10 createUser, 11 createUser,
11 execCLI, 12 execCLI,
12 flushTests, 13 flushAndRunServer,
13 getEnvCli, 14 getEnvCli,
14 getVideo, 15 getVideo,
15 getVideoChannelsList, 16 getVideoChannelsList,
16 getVideosList, 17 getVideosList,
17 killallServers, 18 killallServers,
18 makeActivityPubGetRequest, 19 makeActivityPubGetRequest,
19 parseTorrentVideo, 20 parseTorrentVideo, reRunServer,
20 runServer,
21 ServerInfo, 21 ServerInfo,
22 setAccessTokensToServers, 22 setAccessTokensToServers,
23 uploadVideo 23 uploadVideo
24} from '../../../shared/utils' 24} from '../../../shared/extra-utils'
25import { getAccountsList } from '../../../shared/utils/users/accounts' 25import { getAccountsList } from '../../../shared/extra-utils/users/accounts'
26 26
27const expect = chai.expect 27const expect = chai.expect
28 28
@@ -32,15 +32,13 @@ describe('Test update host scripts', function () {
32 before(async function () { 32 before(async function () {
33 this.timeout(60000) 33 this.timeout(60000)
34 34
35 await flushTests()
36
37 const overrideConfig = { 35 const overrideConfig = {
38 webserver: { 36 webserver: {
39 port: 9256 37 port: 9256
40 } 38 }
41 } 39 }
42 // Run server 2 to have transcoding enabled 40 // Run server 2 to have transcoding enabled
43 server = await runServer(2, overrideConfig) 41 server = await flushAndRunServer(2, overrideConfig)
44 await setAccessTokensToServers([ server ]) 42 await setAccessTokensToServers([ server ])
45 43
46 // Upload two videos for our needs 44 // Upload two videos for our needs
@@ -50,7 +48,7 @@ describe('Test update host scripts', function () {
50 await uploadVideo(server.url, server.accessToken, videoAttributes) 48 await uploadVideo(server.url, server.accessToken, videoAttributes)
51 49
52 // Create a user 50 // Create a user
53 await createUser(server.url, server.accessToken, 'toto', 'coucou') 51 await createUser({ url: server.url, accessToken: server.accessToken, username: 'toto', password: 'coucou' })
54 52
55 // Create channel 53 // Create channel
56 const videoChannel = { 54 const videoChannel = {
@@ -72,7 +70,7 @@ describe('Test update host scripts', function () {
72 70
73 killallServers([ server ]) 71 killallServers([ server ])
74 // Run server with standard configuration 72 // Run server with standard configuration
75 server = await runServer(2) 73 await reRunServer(server)
76 74
77 const env = getEnvCli(server) 75 const env = getEnvCli(server)
78 await execCLI(`${env} npm run update-host`) 76 await execCLI(`${env} npm run update-host`)
@@ -86,6 +84,13 @@ describe('Test update host scripts', function () {
86 const { body } = await makeActivityPubGetRequest(server.url, '/videos/watch/' + video.uuid) 84 const { body } = await makeActivityPubGetRequest(server.url, '/videos/watch/' + video.uuid)
87 85
88 expect(body.id).to.equal('http://localhost:9002/videos/watch/' + video.uuid) 86 expect(body.id).to.equal('http://localhost:9002/videos/watch/' + video.uuid)
87
88 const res = await getVideo(server.url, video.uuid)
89 const videoDetails: VideoDetails = res.body
90
91 expect(videoDetails.trackerUrls[0]).to.include(server.host)
92 expect(videoDetails.streamingPlaylists[0].playlistUrl).to.include(server.host)
93 expect(videoDetails.streamingPlaylists[0].segmentsSha256Url).to.include(server.host)
89 } 94 }
90 }) 95 })
91 96
@@ -100,7 +105,7 @@ describe('Test update host scripts', function () {
100 } 105 }
101 }) 106 })
102 107
103 it('Should have update accounts url', async function () { 108 it('Should have updated accounts url', async function () {
104 const res = await getAccountsList(server.url) 109 const res = await getAccountsList(server.url)
105 expect(res.body.total).to.equal(3) 110 expect(res.body.total).to.equal(3)
106 111
@@ -112,7 +117,7 @@ describe('Test update host scripts', function () {
112 } 117 }
113 }) 118 })
114 119
115 it('Should update torrent hosts', async function () { 120 it('Should have updated torrent hosts', async function () {
116 this.timeout(30000) 121 this.timeout(30000)
117 122
118 const res = await getVideosList(server.url) 123 const res = await getVideosList(server.url)
@@ -142,6 +147,6 @@ describe('Test update host scripts', function () {
142 }) 147 })
143 148
144 after(async function () { 149 after(async function () {
145 killallServers([ server ]) 150 await cleanupTests([ server ])
146 }) 151 })
147}) 152})
diff --git a/server/tests/client.ts b/server/tests/client.ts
index 06b4a9c5a..778dcd08e 100644
--- a/server/tests/client.ts
+++ b/server/tests/client.ts
@@ -4,18 +4,17 @@ import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import * as request from 'supertest' 5import * as request from 'supertest'
6import { 6import {
7 flushTests, 7 cleanupTests,
8 flushAndRunServer,
8 getCustomConfig, 9 getCustomConfig,
9 getVideosList, 10 getVideosList,
10 killallServers,
11 makeHTMLRequest, 11 makeHTMLRequest,
12 runServer,
13 ServerInfo, 12 ServerInfo,
14 serverLogin, 13 serverLogin,
15 updateCustomConfig, 14 updateCustomConfig,
16 updateCustomSubConfig, 15 updateCustomSubConfig,
17 uploadVideo 16 uploadVideo
18} from '../../shared/utils' 17} from '../../shared/extra-utils'
19 18
20const expect = chai.expect 19const expect = chai.expect
21 20
@@ -31,9 +30,7 @@ describe('Test a client controllers', function () {
31 before(async function () { 30 before(async function () {
32 this.timeout(120000) 31 this.timeout(120000)
33 32
34 await flushTests() 33 server = await flushAndRunServer(1)
35
36 server = await runServer(1)
37 server.accessToken = await serverLogin(server) 34 server.accessToken = await serverLogin(server)
38 35
39 const videoAttributes = { 36 const videoAttributes = {
@@ -148,6 +145,6 @@ describe('Test a client controllers', function () {
148 }) 145 })
149 146
150 after(async function () { 147 after(async function () {
151 killallServers([ server ]) 148 await cleanupTests([ server ])
152 }) 149 })
153}) 150})
diff --git a/server/tests/feeds/feeds.ts b/server/tests/feeds/feeds.ts
index a771474bc..0dcdf09cf 100644
--- a/server/tests/feeds/feeds.ts
+++ b/server/tests/feeds/feeds.ts
@@ -3,6 +3,7 @@
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { 5import {
6 cleanupTests,
6 createUser, 7 createUser,
7 doubleFollow, 8 doubleFollow,
8 flushAndRunMultipleServers, 9 flushAndRunMultipleServers,
@@ -13,10 +14,10 @@ import {
13 ServerInfo, 14 ServerInfo,
14 setAccessTokensToServers, 15 setAccessTokensToServers,
15 uploadVideo, userLogin 16 uploadVideo, userLogin
16} from '../../../shared/utils' 17} from '../../../shared/extra-utils'
17import * as libxmljs from 'libxmljs' 18import * as libxmljs from 'libxmljs'
18import { addVideoCommentThread } from '../../../shared/utils/videos/video-comments' 19import { addVideoCommentThread } from '../../../shared/extra-utils/videos/video-comments'
19import { waitJobs } from '../../../shared/utils/server/jobs' 20import { waitJobs } from '../../../shared/extra-utils/server/jobs'
20import { User } from '../../../shared/models/users' 21import { User } from '../../../shared/models/users'
21 22
22chai.use(require('chai-xml')) 23chai.use(require('chai-xml'))
@@ -50,7 +51,7 @@ describe('Test syndication feeds', () => {
50 51
51 { 52 {
52 const attr = { username: 'john', password: 'password' } 53 const attr = { username: 'john', password: 'password' }
53 await createUser(servers[0].url, servers[0].accessToken, attr.username, attr.password) 54 await createUser({ url: servers[ 0 ].url, accessToken: servers[ 0 ].accessToken, username: attr.username, password: attr.password })
54 userAccessToken = await userLogin(servers[0], attr) 55 userAccessToken = await userLogin(servers[0], attr)
55 56
56 const res = await getMyUserInformation(servers[0].url, userAccessToken) 57 const res = await getMyUserInformation(servers[0].url, userAccessToken)
@@ -208,11 +209,6 @@ describe('Test syndication feeds', () => {
208 }) 209 })
209 210
210 after(async function () { 211 after(async function () {
211 killallServers(servers) 212 await cleanupTests(servers)
212
213 // Keep the logs if the test failed
214 if (this['ok']) {
215 await flushTests()
216 }
217 }) 213 })
218}) 214})
diff --git a/server/tests/fixtures/thumbnail-playlist.jpg b/server/tests/fixtures/thumbnail-playlist.jpg
new file mode 100644
index 000000000..19db4f18c
--- /dev/null
+++ b/server/tests/fixtures/thumbnail-playlist.jpg
Binary files differ
diff --git a/server/tests/helpers/index.ts b/server/tests/helpers/index.ts
index 551208245..03b971770 100644
--- a/server/tests/helpers/index.ts
+++ b/server/tests/helpers/index.ts
@@ -1,2 +1,3 @@
1import './core-utils' 1import './core-utils'
2import './comment-model' 2import './comment-model'
3import './request'
diff --git a/server/tests/helpers/request.ts b/server/tests/helpers/request.ts
new file mode 100644
index 000000000..a754bc6e2
--- /dev/null
+++ b/server/tests/helpers/request.ts
@@ -0,0 +1,48 @@
1/* tslint:disable:no-unused-expression */
2
3import 'mocha'
4import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
5import { get4KFileUrl, root, wait } from '../../../shared/extra-utils'
6import { join } from 'path'
7import { pathExists, remove } from 'fs-extra'
8import { expect } from 'chai'
9
10describe('Request helpers', function () {
11 const destPath1 = join(root(), 'test-output-1.txt')
12 const destPath2 = join(root(), 'test-output-2.txt')
13
14 it('Should throw an error when the bytes limit is exceeded for request', async function () {
15 try {
16 await doRequest({ uri: get4KFileUrl() }, 3)
17 } catch {
18 return
19 }
20
21 throw new Error('No error thrown by do request')
22 })
23
24 it('Should throw an error when the bytes limit is exceeded for request and save file', async function () {
25 try {
26 await doRequestAndSaveToFile({ uri: get4KFileUrl() }, destPath1, 3)
27 } catch {
28
29 await wait(500)
30 expect(await pathExists(destPath1)).to.be.false
31 return
32 }
33
34 throw new Error('No error thrown by do request and save to file')
35 })
36
37 it('Should succeed if the file is below the limit', async function () {
38 await doRequest({ uri: get4KFileUrl() }, 5)
39 await doRequestAndSaveToFile({ uri: get4KFileUrl() }, destPath2, 5)
40
41 expect(await pathExists(destPath2)).to.be.true
42 })
43
44 after(async function () {
45 await remove(destPath1)
46 await remove(destPath2)
47 })
48})
diff --git a/server/tests/misc-endpoints.ts b/server/tests/misc-endpoints.ts
index 5f82719da..ed406e1bc 100644
--- a/server/tests/misc-endpoints.ts
+++ b/server/tests/misc-endpoints.ts
@@ -4,15 +4,14 @@ import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { 5import {
6 addVideoChannel, 6 addVideoChannel,
7 cleanupTests,
7 createUser, 8 createUser,
8 flushTests, 9 flushAndRunServer,
9 killallServers,
10 makeGetRequest, 10 makeGetRequest,
11 runServer,
12 ServerInfo, 11 ServerInfo,
13 setAccessTokensToServers, 12 setAccessTokensToServers,
14 uploadVideo 13 uploadVideo
15} from '../../shared/utils' 14} from '../../shared/extra-utils'
16import { VideoPrivacy } from '../../shared/models/videos' 15import { VideoPrivacy } from '../../shared/models/videos'
17 16
18const expect = chai.expect 17const expect = chai.expect
@@ -23,9 +22,7 @@ describe('Test misc endpoints', function () {
23 before(async function () { 22 before(async function () {
24 this.timeout(120000) 23 this.timeout(120000)
25 24
26 await flushTests() 25 server = await flushAndRunServer(1)
27
28 server = await runServer(1)
29 await setAccessTokensToServers([ server ]) 26 await setAccessTokensToServers([ server ])
30 }) 27 })
31 28
@@ -149,8 +146,8 @@ describe('Test misc endpoints', function () {
149 await addVideoChannel(server.url, server.accessToken, { name: 'channel1', displayName: 'channel 1' }) 146 await addVideoChannel(server.url, server.accessToken, { name: 'channel1', displayName: 'channel 1' })
150 await addVideoChannel(server.url, server.accessToken, { name: 'channel2', displayName: 'channel 2' }) 147 await addVideoChannel(server.url, server.accessToken, { name: 'channel2', displayName: 'channel 2' })
151 148
152 await createUser(server.url, server.accessToken, 'user1', 'password') 149 await createUser({ url: server.url, accessToken: server.accessToken, username: 'user1', password: 'password' })
153 await createUser(server.url, server.accessToken, 'user2', 'password') 150 await createUser({ url: server.url, accessToken: server.accessToken, username: 'user2', password: 'password' })
154 151
155 const res = await makeGetRequest({ 152 const res = await makeGetRequest({
156 url: server.url, 153 url: server.url,
@@ -174,6 +171,6 @@ describe('Test misc endpoints', function () {
174 }) 171 })
175 172
176 after(async function () { 173 after(async function () {
177 killallServers([ server ]) 174 await cleanupTests([ server ])
178 }) 175 })
179}) 176})
diff --git a/server/tests/real-world/populate-database.ts b/server/tests/real-world/populate-database.ts
index 016503498..b1c1688e7 100644
--- a/server/tests/real-world/populate-database.ts
+++ b/server/tests/real-world/populate-database.ts
@@ -6,11 +6,11 @@ import {
6 getVideosList, 6 getVideosList,
7 killallServers, 7 killallServers,
8 rateVideo, 8 rateVideo,
9 runServer, 9 flushAndRunServer,
10 ServerInfo, 10 ServerInfo,
11 setAccessTokensToServers, 11 setAccessTokensToServers,
12 uploadVideo 12 uploadVideo
13} from '../../../shared/utils' 13} from '../../../shared/extra-utils'
14import * as Bluebird from 'bluebird' 14import * as Bluebird from 'bluebird'
15 15
16start() 16start()
@@ -19,11 +19,10 @@ start()
19// ---------------------------------------------------------------------------- 19// ----------------------------------------------------------------------------
20 20
21async function start () { 21async function start () {
22 await flushTests()
23 22
24 console.log('Flushed tests.') 23 console.log('Flushed tests.')
25 24
26 const server = await runServer(6) 25 const server = await flushAndRunServer(6)
27 26
28 process.on('exit', async () => { 27 process.on('exit', async () => {
29 killallServers([ server ]) 28 killallServers([ server ])
@@ -78,7 +77,7 @@ function createUserCustom (server: ServerInfo) {
78 const username = Date.now().toString() + getRandomInt(0, 100000) 77 const username = Date.now().toString() + getRandomInt(0, 100000)
79 console.log('Creating user %s.', username) 78 console.log('Creating user %s.', username)
80 79
81 return createUser(server.url, server.accessToken, username, 'coucou') 80 return createUser({ url: server.url, accessToken: server.accessToken, username: username, password: 'coucou' })
82} 81}
83 82
84function uploadCustom (server: ServerInfo) { 83function uploadCustom (server: ServerInfo) {
diff --git a/server/tests/real-world/real-world.ts b/server/tests/real-world/real-world.ts
index ac3baaf9a..8b070004d 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 '../../../shared/utils' 19} from '../../../shared/extra-utils'
20import { getJobsListPaginationAndSort } from '../../../shared/utils/server/jobs' 20import { getJobsListPaginationAndSort } from '../../../shared/extra-utils/server/jobs'
21 21
22interface ServerInfo extends DefaultServerInfo { 22interface ServerInfo extends DefaultServerInfo {
23 requestsNumber: number 23 requestsNumber: number
diff --git a/server/tools/cli.ts b/server/tools/cli.ts
index 108c44218..59e9fcfc4 100644
--- a/server/tools/cli.ts
+++ b/server/tools/cli.ts
@@ -3,51 +3,83 @@ const netrc = require('netrc-parser').default
3 3
4const version = require('../../../package.json').version 4const version = require('../../../package.json').version
5 5
6let settings = {
7 remotes: [],
8 default: 0
9}
10
11interface Settings { 6interface Settings {
12 remotes: any[], 7 remotes: any[],
13 default: number 8 default: number
14} 9}
15 10
16async function getSettings () { 11function getSettings () {
17 return new Promise<Settings>((res, rej) => { 12 return new Promise<Settings>((res, rej) => {
18 let settings = { 13 const defaultSettings = {
19 remotes: [], 14 remotes: [],
20 default: 0 15 default: 0
21 } as Settings 16 }
17
22 config.read((err, data) => { 18 config.read((err, data) => {
23 if (err) { 19 if (err) return rej(err)
24 return rej(err) 20
25 } 21 return res(Object.keys(data).length === 0 ? defaultSettings : data)
26 return res(Object.keys(data).length === 0 ? settings : data)
27 }) 22 })
28 }) 23 })
29} 24}
30 25
31async function writeSettings (settings) { 26async function getNetrc () {
27 await netrc.load()
28
29 return netrc
30}
31
32function writeSettings (settings) {
32 return new Promise((res, rej) => { 33 return new Promise((res, rej) => {
33 config.write(settings, function (err) { 34 config.write(settings, function (err) {
34 if (err) { 35 if (err) return rej(err)
35 return rej(err) 36
36 }
37 return res() 37 return res()
38 }) 38 })
39 }) 39 })
40} 40}
41 41
42netrc.loadSync() 42function getRemoteObjectOrDie (program: any, settings: Settings) {
43 if (!program['url'] || !program['username'] || !program['password']) {
44 // No remote and we don't have program parameters: throw
45 if (settings.remotes.length === 0) {
46 if (!program[ 'url' ]) console.error('--url field is required.')
47 if (!program[ 'username' ]) console.error('--username field is required.')
48 if (!program[ 'password' ]) console.error('--password field is required.')
49
50 return process.exit(-1)
51 }
52
53 let url: string = program['url']
54 let username: string = program['username']
55 let password: string = program['password']
56
57 if (!url) {
58 url = settings.default !== -1
59 ? settings.remotes[settings.default]
60 : settings.remotes[0]
61 }
62
63 if (!username) username = netrc.machines[url].login
64 if (!password) password = netrc.machines[url].password
65
66 return { url, username, password }
67 }
68
69 return {
70 url: program[ 'url' ],
71 username: program[ 'username' ],
72 password: program[ 'password' ]
73 }
74}
43 75
44// --------------------------------------------------------------------------- 76// ---------------------------------------------------------------------------
45 77
46export { 78export {
47 version, 79 version,
48 config, 80 config,
49 settings,
50 getSettings, 81 getSettings,
51 writeSettings, 82 getNetrc,
52 netrc 83 getRemoteObjectOrDie,
84 writeSettings
53} 85}
diff --git a/server/tools/peertube-auth.ts b/server/tools/peertube-auth.ts
index a962944a4..8bc3d332c 100644
--- a/server/tools/peertube-auth.ts
+++ b/server/tools/peertube-auth.ts
@@ -1,22 +1,25 @@
1import * as program from 'commander' 1import * as program from 'commander'
2import * as prompt from 'prompt' 2import * as prompt from 'prompt'
3const Table = require('cli-table') 3import { getSettings, writeSettings, getNetrc } from './cli'
4import { getSettings, writeSettings, netrc } from './cli'
5import { isHostValid } from '../helpers/custom-validators/servers' 4import { isHostValid } from '../helpers/custom-validators/servers'
6import { isUserUsernameValid } from '../helpers/custom-validators/users' 5import { isUserUsernameValid } from '../helpers/custom-validators/users'
7 6
7const Table = require('cli-table')
8
8async function delInstance (url: string) { 9async function delInstance (url: string) {
9 const settings = await getSettings() 10 const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ])
10 11
11 settings.remotes.splice(settings.remotes.indexOf(url)) 12 settings.remotes.splice(settings.remotes.indexOf(url))
12 await writeSettings(settings) 13 await writeSettings(settings)
13 14
14 delete netrc.machines[url] 15 delete netrc.machines[url]
16
15 await netrc.save() 17 await netrc.save()
16} 18}
17 19
18async function setInstance (url: string, username: string, password: string) { 20async function setInstance (url: string, username: string, password: string) {
19 const settings = await getSettings() 21 const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ])
22
20 if (settings.remotes.indexOf(url) === -1) { 23 if (settings.remotes.indexOf(url) === -1) {
21 settings.remotes.push(url) 24 settings.remotes.push(url)
22 } 25 }
@@ -82,12 +85,13 @@ program
82 .command('list') 85 .command('list')
83 .description('lists registered remote instances') 86 .description('lists registered remote instances')
84 .action(async () => { 87 .action(async () => {
85 const settings = await getSettings() 88 const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ])
89
86 const table = new Table({ 90 const table = new Table({
87 head: ['instance', 'login'], 91 head: ['instance', 'login'],
88 colWidths: [30, 30] 92 colWidths: [30, 30]
89 }) 93 })
90 netrc.loadSync() 94
91 settings.remotes.forEach(element => { 95 settings.remotes.forEach(element => {
92 table.push([ 96 table.push([
93 element, 97 element,
diff --git a/server/tools/peertube-get-access-token.ts b/server/tools/peertube-get-access-token.ts
index a68665f5b..85660de2c 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 '../../shared/utils' 9} from '../../shared/extra-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 f50aafc35..9a366dbbd 100644
--- a/server/tools/peertube-import-videos.ts
+++ b/server/tools/peertube-import-videos.ts
@@ -5,14 +5,14 @@ import * as program from 'commander'
5import { join } from 'path' 5import { 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/constants'
9import { getClient, getVideoCategories, login, searchVideoWithSort, uploadVideo } from '../../shared/utils/index' 9import { getClient, getVideoCategories, login, searchVideoWithSort, uploadVideo } from '../../shared/extra-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'
13import { sha256 } from '../helpers/core-utils' 13import { sha256 } from '../helpers/core-utils'
14import { safeGetYoutubeDL } from '../helpers/youtube-dl' 14import { buildOriginallyPublishedAt, safeGetYoutubeDL } from '../helpers/youtube-dl'
15import { getSettings, netrc } from './cli' 15import { getNetrc, getRemoteObjectOrDie, getSettings } from './cli'
16 16
17let accessToken: string 17let accessToken: string
18let client: { id: string, secret: string } 18let client: { id: string, secret: string }
@@ -32,58 +32,30 @@ program
32 .option('-v, --verbose', 'Verbose mode') 32 .option('-v, --verbose', 'Verbose mode')
33 .parse(process.argv) 33 .parse(process.argv)
34 34
35getSettings() 35Promise.all([ getSettings(), getNetrc() ])
36.then(settings => { 36 .then(([ settings, netrc ]) => {
37 if ( 37 const { url, username, password } = getRemoteObjectOrDie(program, settings)
38 (!program['url'] ||
39 !program['username'] ||
40 !program['password']) &&
41 (settings.remotes.length === 0)
42 ) {
43 if (!program['url']) console.error('--url field is required.')
44 if (!program['username']) console.error('--username field is required.')
45 if (!program['password']) console.error('--password field is required.')
46 if (!program['targetUrl']) console.error('--targetUrl field is required.')
47 process.exit(-1)
48 }
49
50 if (
51 (!program['url'] ||
52 !program['username'] ||
53 !program['password']) &&
54 (settings.remotes.length > 0)
55 ) {
56 if (!program['url']) {
57 program['url'] = (settings.default !== -1) ?
58 settings.remotes[settings.default] :
59 settings.remotes[0]
60 }
61 38
62 if (!program['username']) program['username'] = netrc.machines[program['url']].login 39 if (!program[ 'targetUrl' ]) {
63 if (!program['password']) program['password'] = netrc.machines[program['url']].password 40 console.error('--targetUrl field is required.')
64 }
65 41
66 if ( 42 process.exit(-1)
67 !program['targetUrl'] 43 }
68 ) {
69 if (!program['targetUrl']) console.error('--targetUrl field is required.')
70 process.exit(-1)
71 }
72 44
73 removeEndSlashes(program['url']) 45 removeEndSlashes(url)
74 removeEndSlashes(program['targetUrl']) 46 removeEndSlashes(program[ 'targetUrl' ])
75 47
76 const user = { 48 const user = {
77 username: program['username'], 49 username: username,
78 password: program['password'] 50 password: password
79 } 51 }
80 52
81 run(user, program['url']) 53 run(user, url)
82 .catch(err => { 54 .catch(err => {
83 console.error(err) 55 console.error(err)
84 process.exit(-1) 56 process.exit(-1)
85 }) 57 })
86}) 58 })
87 59
88async function promptPassword () { 60async function promptPassword () {
89 return new Promise((res, rej) => { 61 return new Promise((res, rej) => {
@@ -126,7 +98,7 @@ async function run (user, url: string) {
126 const youtubeDL = await safeGetYoutubeDL() 98 const youtubeDL = await safeGetYoutubeDL()
127 99
128 const options = [ '-j', '--flat-playlist', '--playlist-reverse' ] 100 const options = [ '-j', '--flat-playlist', '--playlist-reverse' ]
129 youtubeDL.getInfo(program['targetUrl'], options, processOptions, async (err, info) => { 101 youtubeDL.getInfo(program[ 'targetUrl' ], options, processOptions, async (err, info) => {
130 if (err) { 102 if (err) {
131 console.log(err.message) 103 console.log(err.message)
132 process.exit(1) 104 process.exit(1)
@@ -143,20 +115,20 @@ async function run (user, url: string) {
143 console.log('Will download and upload %d videos.\n', infoArray.length) 115 console.log('Will download and upload %d videos.\n', infoArray.length)
144 116
145 for (const info of infoArray) { 117 for (const info of infoArray) {
146 await processVideo(info, program['language'], processOptions.cwd, url, user) 118 await processVideo(info, program[ 'language' ], processOptions.cwd, url, user)
147 } 119 }
148 120
149 console.log('Video/s for user %s imported: %s', program['username'], program['targetUrl']) 121 console.log('Video/s for user %s imported: %s', program[ 'username' ], program[ 'targetUrl' ])
150 process.exit(0) 122 process.exit(0)
151 }) 123 })
152} 124}
153 125
154function processVideo (info: any, languageCode: string, cwd: string, url: string, user) { 126function processVideo (info: any, languageCode: string, cwd: string, url: string, user) {
155 return new Promise(async res => { 127 return new Promise(async res => {
156 if (program['verbose']) console.log('Fetching object.', info) 128 if (program[ 'verbose' ]) console.log('Fetching object.', info)
157 129
158 const videoInfo = await fetchObject(info) 130 const videoInfo = await fetchObject(info)
159 if (program['verbose']) console.log('Fetched object.', videoInfo) 131 if (program[ 'verbose' ]) console.log('Fetched object.', videoInfo)
160 132
161 const result = await searchVideoWithSort(url, videoInfo.title, '-match') 133 const result = await searchVideoWithSort(url, videoInfo.title, '-match')
162 134
@@ -197,9 +169,9 @@ async function uploadVideoOnPeerTube (videoInfo: any, videoPath: string, cwd: st
197 let tags = [] 169 let tags = []
198 if (Array.isArray(videoInfo.tags)) { 170 if (Array.isArray(videoInfo.tags)) {
199 tags = videoInfo.tags 171 tags = videoInfo.tags
200 .filter(t => t.length < CONSTRAINTS_FIELDS.VIDEOS.TAG.max && t.length > CONSTRAINTS_FIELDS.VIDEOS.TAG.min) 172 .filter(t => t.length < CONSTRAINTS_FIELDS.VIDEOS.TAG.max && t.length > CONSTRAINTS_FIELDS.VIDEOS.TAG.min)
201 .map(t => t.normalize()) 173 .map(t => t.normalize())
202 .slice(0, 5) 174 .slice(0, 5)
203 } 175 }
204 176
205 let thumbnailfile 177 let thumbnailfile
@@ -212,6 +184,8 @@ async function uploadVideoOnPeerTube (videoInfo: any, videoPath: string, cwd: st
212 }, thumbnailfile) 184 }, thumbnailfile)
213 } 185 }
214 186
187 const originallyPublishedAt = buildOriginallyPublishedAt(videoInfo)
188
215 const videoAttributes = { 189 const videoAttributes = {
216 name: truncate(videoInfo.title, { 190 name: truncate(videoInfo.title, {
217 'length': CONSTRAINTS_FIELDS.VIDEOS.NAME.max, 191 'length': CONSTRAINTS_FIELDS.VIDEOS.NAME.max,
@@ -224,13 +198,15 @@ async function uploadVideoOnPeerTube (videoInfo: any, videoPath: string, cwd: st
224 nsfw: isNSFW(videoInfo), 198 nsfw: isNSFW(videoInfo),
225 waitTranscoding: true, 199 waitTranscoding: true,
226 commentsEnabled: true, 200 commentsEnabled: true,
201 downloadEnabled: true,
227 description: videoInfo.description || undefined, 202 description: videoInfo.description || undefined,
228 support: undefined, 203 support: undefined,
229 tags, 204 tags,
230 privacy: VideoPrivacy.PUBLIC, 205 privacy: VideoPrivacy.PUBLIC,
231 fixture: videoPath, 206 fixture: videoPath,
232 thumbnailfile, 207 thumbnailfile,
233 previewfile: thumbnailfile 208 previewfile: thumbnailfile,
209 originallyPublishedAt: originallyPublishedAt ? originallyPublishedAt.toISOString() : null
234 } 210 }
235 211
236 console.log('\nUploading on PeerTube video "%s".', videoAttributes.name) 212 console.log('\nUploading on PeerTube video "%s".', videoAttributes.name)
@@ -259,7 +235,7 @@ async function uploadVideoOnPeerTube (videoInfo: any, videoPath: string, cwd: st
259async function getCategory (categories: string[], url: string) { 235async function getCategory (categories: string[], url: string) {
260 if (!categories) return undefined 236 if (!categories) return undefined
261 237
262 const categoryString = categories[0] 238 const categoryString = categories[ 0 ]
263 239
264 if (categoryString === 'News & Politics') return 11 240 if (categoryString === 'News & Politics') return 11
265 241
@@ -267,7 +243,7 @@ async function getCategory (categories: string[], url: string) {
267 const categoriesServer = res.body 243 const categoriesServer = res.body
268 244
269 for (const key of Object.keys(categoriesServer)) { 245 for (const key of Object.keys(categoriesServer)) {
270 const categoryServer = categoriesServer[key] 246 const categoryServer = categoriesServer[ key ]
271 if (categoryString.toLowerCase() === categoryServer.toLowerCase()) return parseInt(key, 10) 247 if (categoryString.toLowerCase() === categoryServer.toLowerCase()) return parseInt(key, 10)
272 } 248 }
273 249
@@ -291,12 +267,12 @@ function normalizeObject (obj: any) {
291 // Deprecated key 267 // Deprecated key
292 if (key === 'resolution') continue 268 if (key === 'resolution') continue
293 269
294 const value = obj[key] 270 const value = obj[ key ]
295 271
296 if (typeof value === 'string') { 272 if (typeof value === 'string') {
297 newObj[key] = value.normalize() 273 newObj[ key ] = value.normalize()
298 } else { 274 } else {
299 newObj[key] = value 275 newObj[ key ] = value
300 } 276 }
301 } 277 }
302 278
diff --git a/server/tools/peertube-upload.ts b/server/tools/peertube-upload.ts
index cc7bd9b4c..687f2e60b 100644
--- a/server/tools/peertube-upload.ts
+++ b/server/tools/peertube-upload.ts
@@ -1,10 +1,10 @@
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 '../../shared/utils' 4import { getClient, login } from '../../shared/extra-utils'
5import { uploadVideo } from '../../shared/utils/' 5import { uploadVideo } from '../../shared/extra-utils/'
6import { VideoPrivacy } from '../../shared/models/videos' 6import { VideoPrivacy } from '../../shared/models/videos'
7import { netrc, getSettings } from './cli' 7import { getRemoteObjectOrDie, getSettings } from './cli'
8 8
9program 9program
10 .name('upload') 10 .name('upload')
@@ -26,48 +26,15 @@ program
26 .option('-f, --file <file>', 'Video absolute file path') 26 .option('-f, --file <file>', 'Video absolute file path')
27 .parse(process.argv) 27 .parse(process.argv)
28 28
29if (!program['tags']) program['tags'] = []
30if (!program['nsfw']) program['nsfw'] = false
31if (!program['privacy']) program['privacy'] = VideoPrivacy.PUBLIC
32if (!program['commentsEnabled']) program['commentsEnabled'] = false
33
34getSettings() 29getSettings()
35 .then(settings => { 30 .then(settings => {
36 if ( 31 const { url, username, password } = getRemoteObjectOrDie(program, settings)
37 (!program['url'] ||
38 !program['username'] ||
39 !program['password']) &&
40 (settings.remotes.length === 0)
41 ) {
42 if (!program['url']) console.error('--url field is required.')
43 if (!program['username']) console.error('--username field is required.')
44 if (!program['password']) console.error('--password field is required.')
45 if (!program['videoName']) console.error('--video-name field is required.')
46 if (!program['file']) console.error('--file field is required.')
47 process.exit(-1)
48 }
49 32
50 if ( 33 if (!program['videoName'] || !program['file'] || !program['channelId']) {
51 (!program['url'] || 34 if (!program['videoName']) console.error('--video-name is required.')
52 !program['username'] || 35 if (!program['file']) console.error('--file is required.')
53 !program['password']) && 36 if (!program['channelId']) console.error('--channel-id is required.')
54 (settings.remotes.length > 0)
55 ) {
56 if (!program['url']) {
57 program['url'] = (settings.default !== -1) ?
58 settings.remotes[settings.default] :
59 settings.remotes[0]
60 }
61 if (!program['username']) program['username'] = netrc.machines[program['url']].login
62 if (!program['password']) program['password'] = netrc.machines[program['url']].password
63 }
64 37
65 if (
66 !program['videoName'] ||
67 !program['file']
68 ) {
69 if (!program['videoName']) console.error('--video-name field is required.')
70 if (!program['file']) console.error('--file field is required.')
71 process.exit(-1) 38 process.exit(-1)
72 } 39 }
73 40
@@ -76,28 +43,25 @@ getSettings()
76 process.exit(-1) 43 process.exit(-1)
77 } 44 }
78 45
79 run().catch(err => { 46 run(url, username, password).catch(err => {
80 console.error(err) 47 console.error(err)
81 process.exit(-1) 48 process.exit(-1)
82 }) 49 })
83 }) 50 })
84 51
85async function run () { 52async function run (url: string, username: string, password: string) {
86 const res = await getClient(program[ 'url' ]) 53 const resClient = await getClient(program[ 'url' ])
87 const client = { 54 const client = {
88 id: res.body.client_id, 55 id: resClient.body.client_id,
89 secret: res.body.client_secret 56 secret: resClient.body.client_secret
90 } 57 }
91 58
92 const user = { 59 const user = { username, password }
93 username: program[ 'username' ],
94 password: program[ 'password' ]
95 }
96 60
97 let accessToken: string 61 let accessToken: string
98 try { 62 try {
99 const res2 = await login(program[ 'url' ], client, user) 63 const res = await login(url, client, user)
100 accessToken = res2.body.access_token 64 accessToken = res.body.access_token
101 } catch (err) { 65 } catch (err) {
102 throw new Error('Cannot authenticate. Please check your username/password.') 66 throw new Error('Cannot authenticate. Please check your username/password.')
103 } 67 }
@@ -108,26 +72,32 @@ async function run () {
108 72
109 const videoAttributes = { 73 const videoAttributes = {
110 name: program['videoName'], 74 name: program['videoName'],
111 category: program['category'], 75 category: program['category'] || undefined,
112 channelId: program['channelId'], 76 channelId: program['channelId'],
113 licence: program['licence'], 77 licence: program['licence'] || undefined,
114 language: program['language'], 78 language: program['language'] || undefined,
115 nsfw: program['nsfw'], 79 nsfw: program['nsfw'] !== undefined ? program['nsfw'] : false,
116 description: program['videoDescription'], 80 description: program['videoDescription'] || '',
117 tags: program['tags'], 81 tags: program['tags'] || [],
118 commentsEnabled: program['commentsEnabled'], 82 commentsEnabled: program['commentsEnabled'] !== undefined ? program['commentsEnabled'] : true,
83 downloadEnabled: program['downloadEnabled'] !== undefined ? program['downloadEnabled'] : true,
119 fixture: program['file'], 84 fixture: program['file'],
120 thumbnailfile: program['thumbnail'], 85 thumbnailfile: program['thumbnail'],
121 previewfile: program['preview'], 86 previewfile: program['preview'],
122 waitTranscoding: true, 87 waitTranscoding: true,
123 privacy: program['privacy'], 88 privacy: program['privacy'] || VideoPrivacy.PUBLIC,
124 support: undefined 89 support: undefined
125 } 90 }
126 91
127 await uploadVideo(program[ 'url' ], accessToken, videoAttributes) 92 try {
128 93 await uploadVideo(url, accessToken, videoAttributes)
129 console.log(`Video ${program['videoName']} uploaded.`) 94 console.log(`Video ${program['videoName']} uploaded.`)
130 process.exit(0) 95 process.exit(0)
96 } catch (err) {
97 console.log('coucou')
98 console.error(require('util').inspect(err))
99 process.exit(-1)
100 }
131} 101}
132 102
133// ---------------------------------------------------------------------------- 103// ----------------------------------------------------------------------------
diff --git a/server/typings/express.ts b/server/typings/express.ts
new file mode 100644
index 000000000..324d78662
--- /dev/null
+++ b/server/typings/express.ts
@@ -0,0 +1,82 @@
1import { VideoChannelModel } from '../models/video/video-channel'
2import { VideoPlaylistModel } from '../models/video/video-playlist'
3import { VideoPlaylistElementModel } from '../models/video/video-playlist-element'
4import { UserModel } from '../models/account/user'
5import { VideoModel } from '../models/video/video'
6import { AccountModel } from '../models/account/account'
7import { VideoChangeOwnershipModel } from '../models/video/video-change-ownership'
8import { ActorModel } from '../models/activitypub/actor'
9import { VideoCommentModel } from '../models/video/video-comment'
10import { VideoShareModel } from '../models/video/video-share'
11import { AccountVideoRateModel } from '../models/account/account-video-rate'
12import { ActorFollowModel } from '../models/activitypub/actor-follow'
13import { ServerModel } from '../models/server/server'
14import { VideoFileModel } from '../models/video/video-file'
15import { VideoRedundancyModel } from '../models/redundancy/video-redundancy'
16import { ServerBlocklistModel } from '../models/server/server-blocklist'
17import { AccountBlocklistModel } from '../models/account/account-blocklist'
18import { VideoImportModel } from '../models/video/video-import'
19import { VideoAbuseModel } from '../models/video/video-abuse'
20import { VideoBlacklistModel } from '../models/video/video-blacklist'
21import { VideoCaptionModel } from '../models/video/video-caption'
22import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
23
24declare module 'express' {
25
26 interface Response {
27 locals: {
28 video?: VideoModel
29 videoShare?: VideoShareModel
30 videoFile?: VideoFileModel
31
32 videoImport?: VideoImportModel
33
34 videoBlacklist?: VideoBlacklistModel
35
36 videoCaption?: VideoCaptionModel
37
38 videoAbuse?: VideoAbuseModel
39
40 videoStreamingPlaylist?: VideoStreamingPlaylistModel
41
42 videoChannel?: VideoChannelModel
43
44 videoPlaylist?: VideoPlaylistModel
45 videoPlaylistElement?: VideoPlaylistElementModel
46
47 accountVideoRate?: AccountVideoRateModel
48
49 videoComment?: VideoCommentModel
50 videoCommentThread?: VideoCommentModel
51
52 follow?: ActorFollowModel
53 subscription?: ActorFollowModel
54
55 nextOwner?: AccountModel
56 videoChangeOwnership?: VideoChangeOwnershipModel
57 account?: AccountModel
58 actor?: ActorModel
59 user?: UserModel
60
61 server?: ServerModel
62
63 videoRedundancy?: VideoRedundancyModel
64
65 accountBlock?: AccountBlocklistModel
66 serverBlock?: ServerBlocklistModel
67
68 oauth?: {
69 token: {
70 User: UserModel
71 user: UserModel
72 }
73 }
74
75 signature?: {
76 actor: ActorModel
77 }
78
79 authenticated?: boolean
80 }
81 }
82}
diff --git a/server/typings/sequelize.ts b/server/typings/sequelize.ts
new file mode 100644
index 000000000..9cd83612d
--- /dev/null
+++ b/server/typings/sequelize.ts
@@ -0,0 +1,18 @@
1import { Model } from 'sequelize-typescript'
2
3// Thanks to sequelize-typescript: https://github.com/RobinBuschmann/sequelize-typescript
4
5export type Diff<T extends string | symbol | number, U extends string | symbol | number> =
6 ({ [P in T]: P } & { [P in U]: never } & { [ x: string ]: never })[T]
7
8export type Omit<T, K extends keyof T> = { [P in Diff<keyof T, K>]: T[P] }
9
10export type RecursivePartial<T> = { [P in keyof T]?: RecursivePartial<T[P]> }
11
12export type FilteredModelAttributes<T extends Model<T>> = RecursivePartial<Omit<T, keyof Model<any>>> & {
13 id?: number | any
14 createdAt?: Date | any
15 updatedAt?: Date | any
16 deletedAt?: Date | any
17 version?: number | any
18}