aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/activitypub/client.ts57
-rw-r--r--server/controllers/activitypub/inbox.ts10
-rw-r--r--server/controllers/api/accounts.ts19
-rw-r--r--server/controllers/api/config.ts91
-rw-r--r--server/controllers/api/index.ts4
-rw-r--r--server/controllers/api/jobs.ts2
-rw-r--r--server/controllers/api/overviews.ts72
-rw-r--r--server/controllers/api/plugins.ts2
-rw-r--r--server/controllers/api/search.ts8
-rw-r--r--server/controllers/api/server/debug.ts6
-rw-r--r--server/controllers/api/server/follows.ts8
-rw-r--r--server/controllers/api/server/logs.ts10
-rw-r--r--server/controllers/api/server/redundancy.ts84
-rw-r--r--server/controllers/api/server/server-blocklist.ts19
-rw-r--r--server/controllers/api/server/stats.ts19
-rw-r--r--server/controllers/api/users/index.ts43
-rw-r--r--server/controllers/api/users/me.ts4
-rw-r--r--server/controllers/api/users/my-blocklist.ts16
-rw-r--r--server/controllers/api/users/my-history.ts2
-rw-r--r--server/controllers/api/users/my-subscriptions.ts7
-rw-r--r--server/controllers/api/users/token.ts37
-rw-r--r--server/controllers/api/video-channel.ts8
-rw-r--r--server/controllers/api/video-playlist.ts12
-rw-r--r--server/controllers/api/videos/abuse.ts37
-rw-r--r--server/controllers/api/videos/blacklist.ts68
-rw-r--r--server/controllers/api/videos/captions.ts4
-rw-r--r--server/controllers/api/videos/comment.ts2
-rw-r--r--server/controllers/api/videos/import.ts107
-rw-r--r--server/controllers/api/videos/index.ts55
-rw-r--r--server/controllers/api/videos/ownership.ts4
-rw-r--r--server/controllers/api/videos/rate.ts2
-rw-r--r--server/controllers/bots.ts14
-rw-r--r--server/controllers/client.ts4
-rw-r--r--server/controllers/feeds.ts4
-rw-r--r--server/controllers/plugins.ts59
-rw-r--r--server/controllers/static.ts33
-rw-r--r--server/controllers/tracker.ts15
-rw-r--r--server/controllers/webfinger.ts7
-rw-r--r--server/helpers/activitypub.ts80
-rw-r--r--server/helpers/audit-logger.ts26
-rw-r--r--server/helpers/core-utils.ts68
-rw-r--r--server/helpers/custom-jsonld-signature.ts90
-rw-r--r--server/helpers/custom-validators/activitypub/actor.ts8
-rw-r--r--server/helpers/custom-validators/activitypub/cache-file.ts2
-rw-r--r--server/helpers/custom-validators/activitypub/video-comments.ts2
-rw-r--r--server/helpers/custom-validators/activitypub/videos.ts47
-rw-r--r--server/helpers/custom-validators/feeds.ts2
-rw-r--r--server/helpers/custom-validators/logs.ts2
-rw-r--r--server/helpers/custom-validators/misc.ts4
-rw-r--r--server/helpers/custom-validators/plugins.ts6
-rw-r--r--server/helpers/custom-validators/user-notifications.ts3
-rw-r--r--server/helpers/custom-validators/users.ts11
-rw-r--r--server/helpers/custom-validators/video-abuses.ts14
-rw-r--r--server/helpers/custom-validators/video-captions.ts8
-rw-r--r--server/helpers/custom-validators/video-imports.ts8
-rw-r--r--server/helpers/custom-validators/video-playlists.ts6
-rw-r--r--server/helpers/custom-validators/video-redundancies.ts12
-rw-r--r--server/helpers/custom-validators/videos.ts10
-rw-r--r--server/helpers/express-utils.ts16
-rw-r--r--server/helpers/ffmpeg-utils.ts228
-rw-r--r--server/helpers/logger.ts66
-rw-r--r--server/helpers/middlewares/video-abuses.ts12
-rw-r--r--server/helpers/middlewares/videos.ts31
-rw-r--r--server/helpers/peertube-crypto.ts8
-rw-r--r--server/helpers/regexp.ts2
-rw-r--r--server/helpers/register-ts-paths.ts2
-rw-r--r--server/helpers/signup.ts2
-rw-r--r--server/helpers/utils.ts29
-rw-r--r--server/helpers/video.ts63
-rw-r--r--server/helpers/webtorrent.ts26
-rw-r--r--server/helpers/youtube-dl.ts129
-rw-r--r--server/initializers/checker-after-init.ts24
-rw-r--r--server/initializers/checker-before-init.ts9
-rw-r--r--server/initializers/config.ts23
-rw-r--r--server/initializers/constants.ts163
-rw-r--r--server/initializers/database.ts2
-rw-r--r--server/initializers/index.ts3
-rw-r--r--server/initializers/migrations/0005-email-pod.ts4
-rw-r--r--server/initializers/migrations/0010-email-user.ts4
-rw-r--r--server/initializers/migrations/0015-video-views.ts4
-rw-r--r--server/initializers/migrations/0020-video-likes.ts4
-rw-r--r--server/initializers/migrations/0025-video-dislikes.ts4
-rw-r--r--server/initializers/migrations/0030-video-category.ts4
-rw-r--r--server/initializers/migrations/0035-video-licence.ts4
-rw-r--r--server/initializers/migrations/0040-video-nsfw.ts4
-rw-r--r--server/initializers/migrations/0045-user-display-nsfw.ts4
-rw-r--r--server/initializers/migrations/0050-video-language.ts4
-rw-r--r--server/initializers/migrations/0055-video-uuid.ts4
-rw-r--r--server/initializers/migrations/0060-video-file.ts6
-rw-r--r--server/initializers/migrations/0065-video-file-size.ts6
-rw-r--r--server/initializers/migrations/0070-user-video-quota.ts6
-rw-r--r--server/initializers/migrations/0075-video-resolutions.ts6
-rw-r--r--server/initializers/migrations/0080-video-channels.ts8
-rw-r--r--server/initializers/migrations/0085-user-role.ts6
-rw-r--r--server/initializers/migrations/0090-videos-description.ts6
-rw-r--r--server/initializers/migrations/0095-videos-privacy.ts6
-rw-r--r--server/initializers/migrations/0100-activitypub.ts6
-rw-r--r--server/initializers/migrations/0105-server-mail.ts6
-rw-r--r--server/initializers/migrations/0110-server-key.ts6
-rw-r--r--server/initializers/migrations/0115-account-avatar.ts6
-rw-r--r--server/initializers/migrations/0120-video-null.ts6
-rw-r--r--server/initializers/migrations/0125-table-lowercase.ts4
-rw-r--r--server/initializers/migrations/0130-user-autoplay-video.ts4
-rw-r--r--server/initializers/migrations/0135-video-channel-actor.ts44
-rw-r--r--server/initializers/migrations/0140-actor-url.ts4
-rw-r--r--server/initializers/migrations/0145-delete-author.ts4
-rw-r--r--server/initializers/migrations/0150-avatar-cascade.ts4
-rw-r--r--server/initializers/migrations/0155-video-comments-enabled.ts4
-rw-r--r--server/initializers/migrations/0160-account-route.ts4
-rw-r--r--server/initializers/migrations/0165-video-route.ts4
-rw-r--r--server/initializers/migrations/0170-actor-follow-score.ts4
-rw-r--r--server/initializers/migrations/0175-actor-follow-counts.ts4
-rw-r--r--server/initializers/migrations/0180-job-table-delete.ts4
-rw-r--r--server/initializers/migrations/0185-video-share-url.ts4
-rw-r--r--server/initializers/migrations/0190-video-comment-unique-url.ts4
-rw-r--r--server/initializers/migrations/0195-support.ts4
-rw-r--r--server/initializers/migrations/0200-video-published-at.ts4
-rw-r--r--server/initializers/migrations/0205-user-nsfw-policy.ts4
-rw-r--r--server/initializers/migrations/0210-video-language.ts4
-rw-r--r--server/initializers/migrations/0215-video-support-length.ts4
-rw-r--r--server/initializers/migrations/0255-video-blacklist-reason.ts1
-rw-r--r--server/initializers/migrations/0285-description-support.ts6
-rw-r--r--server/initializers/migrations/0290-account-video-rate-url.ts6
-rw-r--r--server/initializers/migrations/0295-video-file-extname.ts6
-rw-r--r--server/initializers/migrations/0300-user-videos-history-enabled.ts6
-rw-r--r--server/initializers/migrations/0305-fix-unfederated-videos.ts6
-rw-r--r--server/initializers/migrations/0310-drop-unused-video-indexes.ts6
-rw-r--r--server/initializers/migrations/0315-user-notifications.ts4
-rw-r--r--server/initializers/migrations/0320-blacklist-unfederate.ts4
-rw-r--r--server/initializers/migrations/0325-video-abuse-fields.ts4
-rw-r--r--server/initializers/migrations/0330-video-streaming-playlist.ts4
-rw-r--r--server/initializers/migrations/0335-video-downloading-enabled.ts4
-rw-r--r--server/initializers/migrations/0340-add-originally-published-at.ts4
-rw-r--r--server/initializers/migrations/0345-video-playlists.ts6
-rw-r--r--server/initializers/migrations/0350-video-blacklist-type.ts6
-rw-r--r--server/initializers/migrations/0355-p2p-peer-version.ts6
-rw-r--r--server/initializers/migrations/0360-notification-instance-follower.ts6
-rw-r--r--server/initializers/migrations/0365-user-admin-flags.ts6
-rw-r--r--server/initializers/migrations/0370-thumbnail.ts6
-rw-r--r--server/initializers/migrations/0375-account-description.ts6
-rw-r--r--server/initializers/migrations/0380-cleanup-timestamps.ts6
-rw-r--r--server/initializers/migrations/0385-remove-actor-uuid.ts6
-rw-r--r--server/initializers/migrations/0390-user-pending-email.ts6
-rw-r--r--server/initializers/migrations/0395-user-video-languages.ts6
-rw-r--r--server/initializers/migrations/0400-user-theme.ts6
-rw-r--r--server/initializers/migrations/0405-plugin.ts6
-rw-r--r--server/initializers/migrations/0410-video-playlist-element.ts6
-rw-r--r--server/initializers/migrations/0415-thumbnail-auto-generated.ts6
-rw-r--r--server/initializers/migrations/0420-avatar-lazy.ts6
-rw-r--r--server/initializers/migrations/0425-nullable-actor-fields.ts6
-rw-r--r--server/initializers/migrations/0430-auto-follow-notification-setting.ts6
-rw-r--r--server/initializers/migrations/0435-user-modals.ts6
-rw-r--r--server/initializers/migrations/0440-user-auto-play-next-video.ts6
-rw-r--r--server/initializers/migrations/0445-shared-inbox-optional.ts6
-rw-r--r--server/initializers/migrations/0450-streaming-playlist-files.ts16
-rw-r--r--server/initializers/migrations/0455-soft-delete-video-comments.ts6
-rw-r--r--server/initializers/migrations/0460-user-playlist-autoplay.ts6
-rw-r--r--server/initializers/migrations/0465-thumbnail-file-url-length.ts6
-rw-r--r--server/initializers/migrations/0470-cleaup-indexes.ts6
-rw-r--r--server/initializers/migrations/0475-redundancy-expires-on.ts27
-rw-r--r--server/initializers/migrations/0480-caption-file-url.ts27
-rw-r--r--server/initializers/migrations/0490-abuse-video.ts26
-rw-r--r--server/initializers/migrations/0495-plugin-auth.ts42
-rw-r--r--server/initializers/migrations/0500-playlist-description-length.ts26
-rw-r--r--server/initializers/migrations/0505-user-last-login-date.ts26
-rw-r--r--server/initializers/migrator.ts4
-rw-r--r--server/lib/activitypub/actor.ts61
-rw-r--r--server/lib/activitypub/audience.ts10
-rw-r--r--server/lib/activitypub/cache-file.ts4
-rw-r--r--server/lib/activitypub/crawl.ts6
-rw-r--r--server/lib/activitypub/follow.ts3
-rw-r--r--server/lib/activitypub/index.ts9
-rw-r--r--server/lib/activitypub/playlist.ts6
-rw-r--r--server/lib/activitypub/process/process-announce.ts2
-rw-r--r--server/lib/activitypub/process/process-create.ts5
-rw-r--r--server/lib/activitypub/process/process-delete.ts2
-rw-r--r--server/lib/activitypub/process/process-dislike.ts2
-rw-r--r--server/lib/activitypub/process/process-flag.ts19
-rw-r--r--server/lib/activitypub/process/process-follow.ts4
-rw-r--r--server/lib/activitypub/process/process-like.ts2
-rw-r--r--server/lib/activitypub/process/process-reject.ts2
-rw-r--r--server/lib/activitypub/process/process-undo.ts2
-rw-r--r--server/lib/activitypub/process/process-update.ts5
-rw-r--r--server/lib/activitypub/process/process-view.ts3
-rw-r--r--server/lib/activitypub/send/send-accept.ts2
-rw-r--r--server/lib/activitypub/send/send-announce.ts2
-rw-r--r--server/lib/activitypub/send/send-create.ts18
-rw-r--r--server/lib/activitypub/send/send-delete.ts2
-rw-r--r--server/lib/activitypub/send/send-dislike.ts2
-rw-r--r--server/lib/activitypub/send/send-flag.ts2
-rw-r--r--server/lib/activitypub/send/send-like.ts2
-rw-r--r--server/lib/activitypub/send/send-reject.ts2
-rw-r--r--server/lib/activitypub/send/send-undo.ts10
-rw-r--r--server/lib/activitypub/send/send-update.ts7
-rw-r--r--server/lib/activitypub/send/send-view.ts6
-rw-r--r--server/lib/activitypub/send/utils.ts48
-rw-r--r--server/lib/activitypub/share.ts4
-rw-r--r--server/lib/activitypub/video-comments.ts16
-rw-r--r--server/lib/activitypub/video-rates.ts4
-rw-r--r--server/lib/activitypub/videos.ts220
-rw-r--r--server/lib/auth.ts286
-rw-r--r--server/lib/avatar.ts4
-rw-r--r--server/lib/blocklist.ts4
-rw-r--r--server/lib/client-html.ts26
-rw-r--r--server/lib/emailer.ts477
-rw-r--r--server/lib/emails/common/base.pug267
-rw-r--r--server/lib/emails/common/greetings.pug11
-rw-r--r--server/lib/emails/common/html.pug4
-rw-r--r--server/lib/emails/common/mixins.pug3
-rw-r--r--server/lib/emails/contact-form/html.pug9
-rw-r--r--server/lib/emails/follower-on-channel/html.pug9
-rw-r--r--server/lib/emails/password-create/html.pug10
-rw-r--r--server/lib/emails/password-reset/html.pug12
-rw-r--r--server/lib/emails/user-registered/html.pug10
-rw-r--r--server/lib/emails/verify-email/html.pug14
-rw-r--r--server/lib/emails/video-abuse-new/html.pug18
-rw-r--r--server/lib/emails/video-auto-blacklist-new/html.pug17
-rw-r--r--server/lib/emails/video-comment-mention/html.pug11
-rw-r--r--server/lib/emails/video-comment-new/html.pug11
-rw-r--r--server/lib/files-cache/videos-caption-cache.ts7
-rw-r--r--server/lib/files-cache/videos-preview-cache.ts13
-rw-r--r--server/lib/job-queue/handlers/activitypub-follow.ts13
-rw-r--r--server/lib/job-queue/handlers/activitypub-http-broadcast.ts7
-rw-r--r--server/lib/job-queue/handlers/activitypub-http-fetcher.ts13
-rw-r--r--server/lib/job-queue/handlers/activitypub-http-unicast.ts7
-rw-r--r--server/lib/job-queue/handlers/activitypub-refresher.ts10
-rw-r--r--server/lib/job-queue/handlers/email.ts5
-rw-r--r--server/lib/job-queue/handlers/utils/activitypub-http-utils.ts22
-rw-r--r--server/lib/job-queue/handlers/video-file-import.ts6
-rw-r--r--server/lib/job-queue/handlers/video-import.ts58
-rw-r--r--server/lib/job-queue/handlers/video-redundancy.ts17
-rw-r--r--server/lib/job-queue/handlers/video-transcoding.ts52
-rw-r--r--server/lib/job-queue/handlers/video-views.ts6
-rw-r--r--server/lib/job-queue/job-queue.ts82
-rw-r--r--server/lib/moderation.ts22
-rw-r--r--server/lib/notifier.ts69
-rw-r--r--server/lib/oauth-model.ts145
-rw-r--r--server/lib/plugins/hooks.ts2
-rw-r--r--server/lib/plugins/plugin-helpers.ts133
-rw-r--r--server/lib/plugins/plugin-index.ts8
-rw-r--r--server/lib/plugins/plugin-manager.ts307
-rw-r--r--server/lib/plugins/register-helpers-store.ts355
-rw-r--r--server/lib/redis.ts50
-rw-r--r--server/lib/redundancy.ts37
-rw-r--r--server/lib/schedulers/auto-follow-index-instances.ts11
-rw-r--r--server/lib/schedulers/plugins-check-scheduler.ts2
-rw-r--r--server/lib/schedulers/remove-old-views-scheduler.ts2
-rw-r--r--server/lib/schedulers/update-videos-scheduler.ts3
-rw-r--r--server/lib/schedulers/videos-redundancy-scheduler.ts67
-rw-r--r--server/lib/thumbnail.ts18
-rw-r--r--server/lib/user.ts21
-rw-r--r--server/lib/video-blacklist.ts100
-rw-r--r--server/lib/video-channel.ts9
-rw-r--r--server/lib/video-comment.ts8
-rw-r--r--server/lib/video-paths.ts2
-rw-r--r--server/lib/video-playlist.ts2
-rw-r--r--server/lib/video-transcoding.ts6
-rw-r--r--server/lib/videos.ts11
-rw-r--r--server/middlewares/activitypub.ts18
-rw-r--r--server/middlewares/csp.ts30
-rw-r--r--server/middlewares/dnt.ts3
-rw-r--r--server/middlewares/oauth.ts30
-rw-r--r--server/middlewares/sort.ts25
-rw-r--r--server/middlewares/validators/activitypub/activity.ts2
-rw-r--r--server/middlewares/validators/avatar.ts4
-rw-r--r--server/middlewares/validators/blocklist.ts9
-rw-r--r--server/middlewares/validators/config.ts6
-rw-r--r--server/middlewares/validators/feeds.ts8
-rw-r--r--server/middlewares/validators/follows.ts2
-rw-r--r--server/middlewares/validators/plugins.ts71
-rw-r--r--server/middlewares/validators/redundancy.ts74
-rw-r--r--server/middlewares/validators/server.ts5
-rw-r--r--server/middlewares/validators/sort.ts3
-rw-r--r--server/middlewares/validators/themes.ts2
-rw-r--r--server/middlewares/validators/user-subscriptions.ts1
-rw-r--r--server/middlewares/validators/users.ts19
-rw-r--r--server/middlewares/validators/videos/video-abuses.ts51
-rw-r--r--server/middlewares/validators/videos/video-blacklist.ts4
-rw-r--r--server/middlewares/validators/videos/video-captions.ts10
-rw-r--r--server/middlewares/validators/videos/video-channels.ts14
-rw-r--r--server/middlewares/validators/videos/video-comments.ts25
-rw-r--r--server/middlewares/validators/videos/video-imports.ts9
-rw-r--r--server/middlewares/validators/videos/video-playlists.ts9
-rw-r--r--server/middlewares/validators/videos/video-rates.ts6
-rw-r--r--server/middlewares/validators/videos/videos.ts79
-rw-r--r--server/middlewares/validators/webfinger.ts5
-rw-r--r--server/models/account/account-blocklist.ts34
-rw-r--r--server/models/account/account-video-rate.ts12
-rw-r--r--server/models/account/account.ts69
-rw-r--r--server/models/account/user-notification.ts4
-rw-r--r--server/models/account/user-video-history.ts2
-rw-r--r--server/models/account/user.ts293
-rw-r--r--server/models/activitypub/actor-follow.ts68
-rw-r--r--server/models/activitypub/actor.ts115
-rw-r--r--server/models/application/application.ts11
-rw-r--r--server/models/model-cache.ts91
-rw-r--r--server/models/oauth/oauth-token.ts60
-rw-r--r--server/models/redundancy/video-redundancy.ts222
-rw-r--r--server/models/server/plugin.ts82
-rw-r--r--server/models/server/server-blocklist.ts17
-rw-r--r--server/models/server/server.ts7
-rw-r--r--server/models/utils.ts75
-rw-r--r--server/models/video/thumbnail.ts15
-rw-r--r--server/models/video/video-abuse.ts339
-rw-r--r--server/models/video/video-blacklist.ts13
-rw-r--r--server/models/video/video-caption.ts27
-rw-r--r--server/models/video/video-channel.ts176
-rw-r--r--server/models/video/video-comment.ts41
-rw-r--r--server/models/video/video-file.ts81
-rw-r--r--server/models/video/video-format-utils.ts53
-rw-r--r--server/models/video/video-import.ts1
-rw-r--r--server/models/video/video-playlist-element.ts13
-rw-r--r--server/models/video/video-playlist.ts42
-rw-r--r--server/models/video/video-query-builder.ts503
-rw-r--r--server/models/video/video-share.ts89
-rw-r--r--server/models/video/video.ts976
-rw-r--r--server/tests/api/activitypub/client.ts4
-rw-r--r--server/tests/api/activitypub/fetch.ts4
-rw-r--r--server/tests/api/activitypub/helpers.ts2
-rw-r--r--server/tests/api/activitypub/refresher.ts70
-rw-r--r--server/tests/api/activitypub/security.ts16
-rw-r--r--server/tests/api/check-params/accounts.ts2
-rw-r--r--server/tests/api/check-params/blocklist.ts14
-rw-r--r--server/tests/api/check-params/config.ts2
-rw-r--r--server/tests/api/check-params/contact-form.ts18
-rw-r--r--server/tests/api/check-params/debug.ts7
-rw-r--r--server/tests/api/check-params/follows.ts2
-rw-r--r--server/tests/api/check-params/index.ts1
-rw-r--r--server/tests/api/check-params/jobs.ts8
-rw-r--r--server/tests/api/check-params/logs.ts8
-rw-r--r--server/tests/api/check-params/plugins.ts15
-rw-r--r--server/tests/api/check-params/redundancy.ts145
-rw-r--r--server/tests/api/check-params/search.ts2
-rw-r--r--server/tests/api/check-params/services.ts2
-rw-r--r--server/tests/api/check-params/user-notifications.ts2
-rw-r--r--server/tests/api/check-params/user-subscriptions.ts2
-rw-r--r--server/tests/api/check-params/users.ts61
-rw-r--r--server/tests/api/check-params/video-abuses.ts20
-rw-r--r--server/tests/api/check-params/video-blacklist.ts15
-rw-r--r--server/tests/api/check-params/video-captions.ts4
-rw-r--r--server/tests/api/check-params/video-channels.ts12
-rw-r--r--server/tests/api/check-params/video-comments.ts52
-rw-r--r--server/tests/api/check-params/video-imports.ts17
-rw-r--r--server/tests/api/check-params/video-playlists.ts7
-rw-r--r--server/tests/api/check-params/videos-filter.ts6
-rw-r--r--server/tests/api/check-params/videos-history.ts9
-rw-r--r--server/tests/api/check-params/videos-overviews.ts33
-rw-r--r--server/tests/api/check-params/videos.ts38
-rw-r--r--server/tests/api/notifications/user-notifications.ts129
-rw-r--r--server/tests/api/redundancy/index.ts2
-rw-r--r--server/tests/api/redundancy/manage-redundancy.ts373
-rw-r--r--server/tests/api/redundancy/redundancy-constraints.ts200
-rw-r--r--server/tests/api/redundancy/redundancy.ts182
-rw-r--r--server/tests/api/search/search-activitypub-video-channels.ts44
-rw-r--r--server/tests/api/search/search-activitypub-videos.ts8
-rw-r--r--server/tests/api/search/search-videos.ts14
-rw-r--r--server/tests/api/server/auto-follows.ts21
-rw-r--r--server/tests/api/server/config.ts14
-rw-r--r--server/tests/api/server/contact-form.ts14
-rw-r--r--server/tests/api/server/email.ts78
-rw-r--r--server/tests/api/server/follow-constraints.ts8
-rw-r--r--server/tests/api/server/follows-moderation.ts18
-rw-r--r--server/tests/api/server/follows.ts88
-rw-r--r--server/tests/api/server/handle-down.ts36
-rw-r--r--server/tests/api/server/jobs.ts16
-rw-r--r--server/tests/api/server/logs.ts2
-rw-r--r--server/tests/api/server/no-client.ts2
-rw-r--r--server/tests/api/server/plugins.ts34
-rw-r--r--server/tests/api/server/reverse-proxy.ts2
-rw-r--r--server/tests/api/server/stats.ts54
-rw-r--r--server/tests/api/server/tracker.ts16
-rw-r--r--server/tests/api/users/blocklist.ts259
-rw-r--r--server/tests/api/users/user-subscriptions.ts15
-rw-r--r--server/tests/api/users/users-multiple-servers.ts11
-rw-r--r--server/tests/api/users/users-verification.ts2
-rw-r--r--server/tests/api/users/users.ts247
-rw-r--r--server/tests/api/videos/audio-only.ts27
-rw-r--r--server/tests/api/videos/multiple-servers.ts32
-rw-r--r--server/tests/api/videos/services.ts10
-rw-r--r--server/tests/api/videos/single-server.ts51
-rw-r--r--server/tests/api/videos/video-abuse.ts150
-rw-r--r--server/tests/api/videos/video-blacklist.ts86
-rw-r--r--server/tests/api/videos/video-captions.ts9
-rw-r--r--server/tests/api/videos/video-change-ownership.ts18
-rw-r--r--server/tests/api/videos/video-channels.ts79
-rw-r--r--server/tests/api/videos/video-comments.ts6
-rw-r--r--server/tests/api/videos/video-description.ts7
-rw-r--r--server/tests/api/videos/video-hls.ts29
-rw-r--r--server/tests/api/videos/video-imports.ts66
-rw-r--r--server/tests/api/videos/video-nsfw.ts72
-rw-r--r--server/tests/api/videos/video-playlist-thumbnails.ts53
-rw-r--r--server/tests/api/videos/video-playlists.ts392
-rw-r--r--server/tests/api/videos/video-privacy.ts11
-rw-r--r--server/tests/api/videos/video-schedule-update.ts3
-rw-r--r--server/tests/api/videos/video-transcoder.ts155
-rw-r--r--server/tests/api/videos/videos-filter.ts12
-rw-r--r--server/tests/api/videos/videos-history.ts2
-rw-r--r--server/tests/api/videos/videos-overview.ts87
-rw-r--r--server/tests/api/videos/videos-views-cleaner.ts16
-rw-r--r--server/tests/cli/create-import-video-file-job.ts6
-rw-r--r--server/tests/cli/create-transcoding-job.ts15
-rw-r--r--server/tests/cli/optimize-old-videos.ts4
-rw-r--r--server/tests/cli/peertube.ts84
-rw-r--r--server/tests/cli/plugins.ts2
-rw-r--r--server/tests/cli/prune-storage.ts21
-rw-r--r--server/tests/cli/update-host.ts2
-rw-r--r--server/tests/client.ts5
-rw-r--r--server/tests/external-plugins/auth-ldap.ts108
-rw-r--r--server/tests/external-plugins/auto-mute.ts243
-rw-r--r--server/tests/external-plugins/index.ts2
-rw-r--r--server/tests/feeds/feeds.ts82
-rw-r--r--server/tests/fixtures/peertube-plugin-test-external-auth-one/main.js75
-rw-r--r--server/tests/fixtures/peertube-plugin-test-external-auth-one/package.json20
-rw-r--r--server/tests/fixtures/peertube-plugin-test-external-auth-two/main.js31
-rw-r--r--server/tests/fixtures/peertube-plugin-test-external-auth-two/package.json20
-rw-r--r--server/tests/fixtures/peertube-plugin-test-five/main.js21
-rw-r--r--server/tests/fixtures/peertube-plugin-test-five/package.json20
-rw-r--r--server/tests/fixtures/peertube-plugin-test-four/main.js114
-rw-r--r--server/tests/fixtures/peertube-plugin-test-four/package.json20
-rw-r--r--server/tests/fixtures/peertube-plugin-test-id-pass-auth-one/main.js69
-rw-r--r--server/tests/fixtures/peertube-plugin-test-id-pass-auth-one/package.json20
-rw-r--r--server/tests/fixtures/peertube-plugin-test-id-pass-auth-three/main.js106
-rw-r--r--server/tests/fixtures/peertube-plugin-test-id-pass-auth-three/package.json20
-rw-r--r--server/tests/fixtures/peertube-plugin-test-id-pass-auth-two/main.js54
-rw-r--r--server/tests/fixtures/peertube-plugin-test-id-pass-auth-two/package.json20
-rw-r--r--server/tests/fixtures/peertube-plugin-test-six/main.js25
-rw-r--r--server/tests/fixtures/peertube-plugin-test-six/package.json20
-rw-r--r--server/tests/fixtures/peertube-plugin-test-three/main.js7
-rw-r--r--server/tests/fixtures/video_import_preview.jpgbin0 -> 37360 bytes
-rw-r--r--server/tests/fixtures/video_import_thumbnail.jpgbin0 -> 5885 bytes
-rw-r--r--server/tests/helpers/comment-model.ts4
-rw-r--r--server/tests/helpers/core-utils.ts2
-rw-r--r--server/tests/helpers/request.ts2
-rw-r--r--server/tests/index.ts2
-rw-r--r--server/tests/misc-endpoints.ts2
-rw-r--r--server/tests/plugins/action-hooks.ts17
-rw-r--r--server/tests/plugins/external-auth.ts331
-rw-r--r--server/tests/plugins/filter-hooks.ts41
-rw-r--r--server/tests/plugins/id-and-pass-auth.ts245
-rw-r--r--server/tests/plugins/index.ts5
-rw-r--r--server/tests/plugins/plugin-helpers.ts210
-rw-r--r--server/tests/plugins/plugin-router.ts91
-rw-r--r--server/tests/plugins/plugin-storage.ts30
-rw-r--r--server/tests/plugins/translations.ts35
-rw-r--r--server/tests/plugins/video-constants.ts111
-rw-r--r--server/tests/real-world/populate-database.ts122
-rw-r--r--server/tests/real-world/real-world.ts375
-rw-r--r--server/tools/cli.ts112
-rw-r--r--server/tools/package.json7
-rw-r--r--server/tools/peertube-auth.ts17
-rw-r--r--server/tools/peertube-import-videos.ts111
-rw-r--r--server/tools/peertube-plugins.ts34
-rw-r--r--server/tools/peertube-redundancy.ts197
-rw-r--r--server/tools/peertube-repl.ts45
-rw-r--r--server/tools/peertube-upload.ts21
-rw-r--r--server/tools/peertube-watch.ts17
-rw-r--r--server/tools/peertube.ts24
-rw-r--r--server/tools/yarn.lock587
-rw-r--r--server/typings/express.ts25
-rw-r--r--server/typings/models/account/account-blocklist.ts6
-rw-r--r--server/typings/models/account/account.ts42
-rw-r--r--server/typings/models/account/actor-follow.ts24
-rw-r--r--server/typings/models/account/actor.ts54
-rw-r--r--server/typings/models/account/avatar.ts3
-rw-r--r--server/typings/models/oauth/oauth-token.ts3
-rw-r--r--server/typings/models/server/plugin.ts3
-rw-r--r--server/typings/models/server/server-blocklist.ts6
-rw-r--r--server/typings/models/server/server.ts6
-rw-r--r--server/typings/models/user/user-notification.ts42
-rw-r--r--server/typings/models/user/user.ts33
-rw-r--r--server/typings/models/video/schedule-video-update.ts3
-rw-r--r--server/typings/models/video/video-abuse.ts16
-rw-r--r--server/typings/models/video/video-blacklist.ts9
-rw-r--r--server/typings/models/video/video-caption.ts7
-rw-r--r--server/typings/models/video/video-change-ownership.ts6
-rw-r--r--server/typings/models/video/video-channels.ts57
-rw-r--r--server/typings/models/video/video-comment.ts27
-rw-r--r--server/typings/models/video/video-file.ts21
-rw-r--r--server/typings/models/video/video-import.ts12
-rw-r--r--server/typings/models/video/video-playlist-element.ts12
-rw-r--r--server/typings/models/video/video-playlist.ts36
-rw-r--r--server/typings/models/video/video-rate.ts9
-rw-r--r--server/typings/models/video/video-redundancy.ts15
-rw-r--r--server/typings/models/video/video-share.ts6
-rw-r--r--server/typings/models/video/video-streaming-playlist.ts22
-rw-r--r--server/typings/models/video/video.ts94
-rw-r--r--server/typings/plugins/register-server-option.model.ts68
-rw-r--r--server/typings/utils.ts2
488 files changed, 13134 insertions, 5737 deletions
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts
index 62412cd62..e44f1c6ab 100644
--- a/server/controllers/activitypub/client.ts
+++ b/server/controllers/activitypub/client.ts
@@ -1,5 +1,5 @@
1// Intercept ActivityPub client requests
2import * as express from 'express' 1import * as express from 'express'
2import * as cors from 'cors'
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 { ROUTE_CACHE_LIFETIME, WEBSERVER } from '../../initializers/constants' 5import { ROUTE_CACHE_LIFETIME, WEBSERVER } from '../../initializers/constants'
@@ -14,7 +14,7 @@ import {
14 videosCustomGetValidator, 14 videosCustomGetValidator,
15 videosShareValidator 15 videosShareValidator
16} from '../../middlewares' 16} from '../../middlewares'
17import { getAccountVideoRateValidator, videoCommentGetValidator } from '../../middlewares/validators' 17import { getAccountVideoRateValidatorFactory, videoCommentGetValidator } from '../../middlewares/validators'
18import { AccountModel } from '../../models/account/account' 18import { AccountModel } from '../../models/account/account'
19import { ActorFollowModel } from '../../models/activitypub/actor-follow' 19import { ActorFollowModel } from '../../models/activitypub/actor-follow'
20import { VideoModel } from '../../models/video/video' 20import { VideoModel } from '../../models/video/video'
@@ -24,22 +24,25 @@ import { cacheRoute } from '../../middlewares/cache'
24import { activityPubResponse } from './utils' 24import { activityPubResponse } from './utils'
25import { AccountVideoRateModel } from '../../models/account/account-video-rate' 25import { AccountVideoRateModel } from '../../models/account/account-video-rate'
26import { 26import {
27 getRateUrl,
28 getVideoCommentsActivityPubUrl, 27 getVideoCommentsActivityPubUrl,
29 getVideoDislikesActivityPubUrl, 28 getVideoDislikesActivityPubUrl,
30 getVideoLikesActivityPubUrl, 29 getVideoLikesActivityPubUrl,
31 getVideoSharesActivityPubUrl 30 getVideoSharesActivityPubUrl
32} from '../../lib/activitypub' 31} from '../../lib/activitypub/url'
33import { VideoCaptionModel } from '../../models/video/video-caption' 32import { VideoCaptionModel } from '../../models/video/video-caption'
34import { videoFileRedundancyGetValidator, videoPlaylistRedundancyGetValidator } from '../../middlewares/validators/redundancy' 33import { videoFileRedundancyGetValidator, videoPlaylistRedundancyGetValidator } from '../../middlewares/validators/redundancy'
35import { getServerActor } from '../../helpers/utils'
36import { buildDislikeActivity } from '../../lib/activitypub/send/send-dislike' 34import { buildDislikeActivity } from '../../lib/activitypub/send/send-dislike'
37import { videoPlaylistElementAPGetValidator, videoPlaylistsGetValidator } from '../../middlewares/validators/videos/video-playlists' 35import { videoPlaylistElementAPGetValidator, videoPlaylistsGetValidator } from '../../middlewares/validators/videos/video-playlists'
38import { VideoPlaylistModel } from '../../models/video/video-playlist' 36import { VideoPlaylistModel } from '../../models/video/video-playlist'
39import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' 37import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
40import { MAccountId, MActorId, MVideo, MVideoAPWithoutCaption } from '@server/typings/models' 38import { MAccountId, MActorId, MVideoAPWithoutCaption, MVideoId } from '@server/typings/models'
39import { getServerActor } from '@server/models/application/application'
40import { getRateUrl } from '@server/lib/activitypub/video-rates'
41 41
42const activityPubClientRouter = express.Router() 42const activityPubClientRouter = express.Router()
43activityPubClientRouter.use(cors())
44
45// Intercept ActivityPub client requests
43 46
44activityPubClientRouter.get('/accounts?/:name', 47activityPubClientRouter.get('/accounts?/:name',
45 executeIfActivityPub, 48 executeIfActivityPub,
@@ -63,13 +66,13 @@ activityPubClientRouter.get('/accounts?/:name/playlists',
63) 66)
64activityPubClientRouter.get('/accounts?/:name/likes/:videoId', 67activityPubClientRouter.get('/accounts?/:name/likes/:videoId',
65 executeIfActivityPub, 68 executeIfActivityPub,
66 asyncMiddleware(getAccountVideoRateValidator('like')), 69 asyncMiddleware(getAccountVideoRateValidatorFactory('like')),
67 getAccountVideoRate('like') 70 getAccountVideoRateFactory('like')
68) 71)
69activityPubClientRouter.get('/accounts?/:name/dislikes/:videoId', 72activityPubClientRouter.get('/accounts?/:name/dislikes/:videoId',
70 executeIfActivityPub, 73 executeIfActivityPub,
71 asyncMiddleware(getAccountVideoRateValidator('dislike')), 74 asyncMiddleware(getAccountVideoRateValidatorFactory('dislike')),
72 getAccountVideoRate('dislike') 75 getAccountVideoRateFactory('dislike')
73) 76)
74 77
75activityPubClientRouter.get('/videos/watch/:id', 78activityPubClientRouter.get('/videos/watch/:id',
@@ -85,7 +88,7 @@ activityPubClientRouter.get('/videos/watch/:id/activity',
85) 88)
86activityPubClientRouter.get('/videos/watch/:id/announces', 89activityPubClientRouter.get('/videos/watch/:id/announces',
87 executeIfActivityPub, 90 executeIfActivityPub,
88 asyncMiddleware(videosCustomGetValidator('only-video')), 91 asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')),
89 asyncMiddleware(videoAnnouncesController) 92 asyncMiddleware(videoAnnouncesController)
90) 93)
91activityPubClientRouter.get('/videos/watch/:id/announces/:actorId', 94activityPubClientRouter.get('/videos/watch/:id/announces/:actorId',
@@ -95,17 +98,17 @@ activityPubClientRouter.get('/videos/watch/:id/announces/:actorId',
95) 98)
96activityPubClientRouter.get('/videos/watch/:id/likes', 99activityPubClientRouter.get('/videos/watch/:id/likes',
97 executeIfActivityPub, 100 executeIfActivityPub,
98 asyncMiddleware(videosCustomGetValidator('only-video')), 101 asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')),
99 asyncMiddleware(videoLikesController) 102 asyncMiddleware(videoLikesController)
100) 103)
101activityPubClientRouter.get('/videos/watch/:id/dislikes', 104activityPubClientRouter.get('/videos/watch/:id/dislikes',
102 executeIfActivityPub, 105 executeIfActivityPub,
103 asyncMiddleware(videosCustomGetValidator('only-video')), 106 asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')),
104 asyncMiddleware(videoDislikesController) 107 asyncMiddleware(videoDislikesController)
105) 108)
106activityPubClientRouter.get('/videos/watch/:id/comments', 109activityPubClientRouter.get('/videos/watch/:id/comments',
107 executeIfActivityPub, 110 executeIfActivityPub,
108 asyncMiddleware(videosCustomGetValidator('only-video')), 111 asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')),
109 asyncMiddleware(videoCommentsController) 112 asyncMiddleware(videoCommentsController)
110) 113)
111activityPubClientRouter.get('/videos/watch/:videoId/comments/:commentId', 114activityPubClientRouter.get('/videos/watch/:videoId/comments/:commentId',
@@ -122,7 +125,7 @@ activityPubClientRouter.get('/videos/watch/:videoId/comments/:commentId/activity
122activityPubClientRouter.get('/video-channels/:name', 125activityPubClientRouter.get('/video-channels/:name',
123 executeIfActivityPub, 126 executeIfActivityPub,
124 asyncMiddleware(localVideoChannelValidator), 127 asyncMiddleware(localVideoChannelValidator),
125 asyncMiddleware(videoChannelController) 128 videoChannelController
126) 129)
127activityPubClientRouter.get('/video-channels/:name/followers', 130activityPubClientRouter.get('/video-channels/:name/followers',
128 executeIfActivityPub, 131 executeIfActivityPub,
@@ -154,7 +157,7 @@ activityPubClientRouter.get('/video-playlists/:playlistId',
154activityPubClientRouter.get('/video-playlists/:playlistId/:videoId', 157activityPubClientRouter.get('/video-playlists/:playlistId/:videoId',
155 executeIfActivityPub, 158 executeIfActivityPub,
156 asyncMiddleware(videoPlaylistElementAPGetValidator), 159 asyncMiddleware(videoPlaylistElementAPGetValidator),
157 asyncMiddleware(videoPlaylistElementController) 160 videoPlaylistElementController
158) 161)
159 162
160// --------------------------------------------------------------------------- 163// ---------------------------------------------------------------------------
@@ -192,7 +195,7 @@ async function accountPlaylistsController (req: express.Request, res: express.Re
192 return activityPubResponse(activityPubContextify(activityPubResult), res) 195 return activityPubResponse(activityPubContextify(activityPubResult), res)
193} 196}
194 197
195function getAccountVideoRate (rateType: VideoRateType) { 198function getAccountVideoRateFactory (rateType: VideoRateType) {
196 return (req: express.Request, res: express.Response) => { 199 return (req: express.Request, res: express.Response) => {
197 const accountVideoRate = res.locals.accountVideoRate 200 const accountVideoRate = res.locals.accountVideoRate
198 201
@@ -234,11 +237,11 @@ async function videoAnnounceController (req: express.Request, res: express.Respo
234 237
235 const { activity } = await buildAnnounceWithVideoAudience(share.Actor, share, res.locals.videoAll, undefined) 238 const { activity } = await buildAnnounceWithVideoAudience(share.Actor, share, res.locals.videoAll, undefined)
236 239
237 return activityPubResponse(activityPubContextify(activity), res) 240 return activityPubResponse(activityPubContextify(activity, 'Announce'), res)
238} 241}
239 242
240async function videoAnnouncesController (req: express.Request, res: express.Response) { 243async function videoAnnouncesController (req: express.Request, res: express.Response) {
241 const video = res.locals.onlyVideo 244 const video = res.locals.onlyImmutableVideo
242 245
243 const handler = async (start: number, count: number) => { 246 const handler = async (start: number, count: number) => {
244 const result = await VideoShareModel.listAndCountByVideoId(video.id, start, count) 247 const result = await VideoShareModel.listAndCountByVideoId(video.id, start, count)
@@ -253,21 +256,21 @@ async function videoAnnouncesController (req: express.Request, res: express.Resp
253} 256}
254 257
255async function videoLikesController (req: express.Request, res: express.Response) { 258async function videoLikesController (req: express.Request, res: express.Response) {
256 const video = res.locals.onlyVideo 259 const video = res.locals.onlyImmutableVideo
257 const json = await videoRates(req, 'like', video, getVideoLikesActivityPubUrl(video)) 260 const json = await videoRates(req, 'like', video, getVideoLikesActivityPubUrl(video))
258 261
259 return activityPubResponse(activityPubContextify(json), res) 262 return activityPubResponse(activityPubContextify(json), res)
260} 263}
261 264
262async function videoDislikesController (req: express.Request, res: express.Response) { 265async function videoDislikesController (req: express.Request, res: express.Response) {
263 const video = res.locals.onlyVideo 266 const video = res.locals.onlyImmutableVideo
264 const json = await videoRates(req, 'dislike', video, getVideoDislikesActivityPubUrl(video)) 267 const json = await videoRates(req, 'dislike', video, getVideoDislikesActivityPubUrl(video))
265 268
266 return activityPubResponse(activityPubContextify(json), res) 269 return activityPubResponse(activityPubContextify(json), res)
267} 270}
268 271
269async function videoCommentsController (req: express.Request, res: express.Response) { 272async function videoCommentsController (req: express.Request, res: express.Response) {
270 const video = res.locals.onlyVideo 273 const video = res.locals.onlyImmutableVideo
271 274
272 const handler = async (start: number, count: number) => { 275 const handler = async (start: number, count: number) => {
273 const result = await VideoCommentModel.listAndCountByVideoId(video.id, start, count) 276 const result = await VideoCommentModel.listAndCountByVideoId(video.id, start, count)
@@ -281,7 +284,7 @@ async function videoCommentsController (req: express.Request, res: express.Respo
281 return activityPubResponse(activityPubContextify(json), res) 284 return activityPubResponse(activityPubContextify(json), res)
282} 285}
283 286
284async function videoChannelController (req: express.Request, res: express.Response) { 287function videoChannelController (req: express.Request, res: express.Response) {
285 const videoChannel = res.locals.videoChannel 288 const videoChannel = res.locals.videoChannel
286 289
287 return activityPubResponse(activityPubContextify(videoChannel.toActivityPubObject()), res) 290 return activityPubResponse(activityPubContextify(videoChannel.toActivityPubObject()), res)
@@ -334,10 +337,10 @@ async function videoRedundancyController (req: express.Request, res: express.Res
334 337
335 if (req.path.endsWith('/activity')) { 338 if (req.path.endsWith('/activity')) {
336 const data = buildCreateActivity(videoRedundancy.url, serverActor, object, audience) 339 const data = buildCreateActivity(videoRedundancy.url, serverActor, object, audience)
337 return activityPubResponse(activityPubContextify(data), res) 340 return activityPubResponse(activityPubContextify(data, 'CacheFile'), res)
338 } 341 }
339 342
340 return activityPubResponse(activityPubContextify(object), res) 343 return activityPubResponse(activityPubContextify(object, 'CacheFile'), res)
341} 344}
342 345
343async function videoPlaylistController (req: express.Request, res: express.Response) { 346async function videoPlaylistController (req: express.Request, res: express.Response) {
@@ -353,7 +356,7 @@ async function videoPlaylistController (req: express.Request, res: express.Respo
353 return activityPubResponse(activityPubContextify(object), res) 356 return activityPubResponse(activityPubContextify(object), res)
354} 357}
355 358
356async function videoPlaylistElementController (req: express.Request, res: express.Response) { 359function videoPlaylistElementController (req: express.Request, res: express.Response) {
357 const videoPlaylistElement = res.locals.videoPlaylistElementAP 360 const videoPlaylistElement = res.locals.videoPlaylistElementAP
358 361
359 const json = videoPlaylistElement.toActivityPubObject() 362 const json = videoPlaylistElement.toActivityPubObject()
@@ -386,7 +389,7 @@ async function actorPlaylists (req: express.Request, account: MAccountId) {
386 return activityPubCollectionPagination(WEBSERVER.URL + req.path, handler, req.query.page) 389 return activityPubCollectionPagination(WEBSERVER.URL + req.path, handler, req.query.page)
387} 390}
388 391
389function videoRates (req: express.Request, rateType: VideoRateType, video: MVideo, url: string) { 392function videoRates (req: express.Request, rateType: VideoRateType, video: MVideoId, url: string) {
390 const handler = async (start: number, count: number) => { 393 const handler = async (start: number, count: number) => {
391 const result = await AccountVideoRateModel.listAndCountAccountUrlsByVideoId(rateType, video.id, start, count) 394 const result = await AccountVideoRateModel.listAndCountAccountUrlsByVideoId(rateType, video.id, start, count)
392 return { 395 return {
diff --git a/server/controllers/activitypub/inbox.ts b/server/controllers/activitypub/inbox.ts
index ca42106b8..c5edf86b7 100644
--- a/server/controllers/activitypub/inbox.ts
+++ b/server/controllers/activitypub/inbox.ts
@@ -46,15 +46,19 @@ const inboxQueue = queue<QueueParam, Error>((task, cb) => {
46 46
47 processActivities(task.activities, options) 47 processActivities(task.activities, options)
48 .then(() => cb()) 48 .then(() => cb())
49 .catch(err => {
50 logger.error('Error in process activities.', { err })
51 cb()
52 })
49}) 53})
50 54
51function inboxController (req: express.Request, res: express.Response) { 55function inboxController (req: express.Request, res: express.Response) {
52 const rootActivity: RootActivity = req.body 56 const rootActivity: RootActivity = req.body
53 let activities: Activity[] = [] 57 let activities: Activity[]
54 58
55 if ([ 'Collection', 'CollectionPage' ].indexOf(rootActivity.type) !== -1) { 59 if ([ 'Collection', 'CollectionPage' ].includes(rootActivity.type)) {
56 activities = (rootActivity as ActivityPubCollection).items 60 activities = (rootActivity as ActivityPubCollection).items
57 } else if ([ 'OrderedCollection', 'OrderedCollectionPage' ].indexOf(rootActivity.type) !== -1) { 61 } else if ([ 'OrderedCollection', 'OrderedCollectionPage' ].includes(rootActivity.type)) {
58 activities = (rootActivity as ActivityPubOrderedCollection<Activity>).orderedItems 62 activities = (rootActivity as ActivityPubOrderedCollection<Activity>).orderedItems
59 } else { 63 } else {
60 activities = [ rootActivity as Activity ] 64 activities = [ rootActivity as Activity ]
diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts
index 05740318e..ccdc610a2 100644
--- a/server/controllers/api/accounts.ts
+++ b/server/controllers/api/accounts.ts
@@ -1,5 +1,5 @@
1import * as express from 'express' 1import * as express from 'express'
2import { getFormattedObjects, getServerActor } from '../../helpers/utils' 2import { getFormattedObjects } from '../../helpers/utils'
3import { 3import {
4 asyncMiddleware, 4 asyncMiddleware,
5 authenticate, 5 authenticate,
@@ -16,21 +16,19 @@ import {
16 accountNameWithHostGetValidator, 16 accountNameWithHostGetValidator,
17 accountsSortValidator, 17 accountsSortValidator,
18 ensureAuthUserOwnsAccountValidator, 18 ensureAuthUserOwnsAccountValidator,
19 videoChannelsSortValidator,
19 videosSortValidator, 20 videosSortValidator,
20 videoChannelsSortValidator 21 videoChannelStatsValidator
21} from '../../middlewares/validators' 22} from '../../middlewares/validators'
22import { AccountModel } from '../../models/account/account' 23import { AccountModel } from '../../models/account/account'
23import { AccountVideoRateModel } from '../../models/account/account-video-rate' 24import { AccountVideoRateModel } from '../../models/account/account-video-rate'
24import { VideoModel } from '../../models/video/video' 25import { VideoModel } from '../../models/video/video'
25import { buildNSFWFilter, isUserAbleToSearchRemoteURI, getCountVideos } from '../../helpers/express-utils' 26import { buildNSFWFilter, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
26import { VideoChannelModel } from '../../models/video/video-channel' 27import { VideoChannelModel } from '../../models/video/video-channel'
27import { JobQueue } from '../../lib/job-queue' 28import { JobQueue } from '../../lib/job-queue'
28import { logger } from '../../helpers/logger'
29import { VideoPlaylistModel } from '../../models/video/video-playlist' 29import { VideoPlaylistModel } from '../../models/video/video-playlist'
30import { 30import { commonVideoPlaylistFiltersValidator, videoPlaylistsSearchValidator } from '../../middlewares/validators/videos/video-playlists'
31 commonVideoPlaylistFiltersValidator, 31import { getServerActor } from '@server/models/application/application'
32 videoPlaylistsSearchValidator
33} from '../../middlewares/validators/videos/video-playlists'
34 32
35const accountsRouter = express.Router() 33const accountsRouter = express.Router()
36 34
@@ -60,6 +58,7 @@ accountsRouter.get('/:accountName/videos',
60 58
61accountsRouter.get('/:accountName/video-channels', 59accountsRouter.get('/:accountName/video-channels',
62 asyncMiddleware(accountNameWithHostGetValidator), 60 asyncMiddleware(accountNameWithHostGetValidator),
61 videoChannelStatsValidator,
63 paginationValidator, 62 paginationValidator,
64 videoChannelsSortValidator, 63 videoChannelsSortValidator,
65 setDefaultSort, 64 setDefaultSort,
@@ -104,7 +103,6 @@ function getAccount (req: express.Request, res: express.Response) {
104 103
105 if (account.isOutdated()) { 104 if (account.isOutdated()) {
106 JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'actor', url: account.Actor.url } }) 105 JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'actor', url: account.Actor.url } })
107 .catch(err => logger.error('Cannot create AP refresher job for actor %s.', account.Actor.url, { err }))
108 } 106 }
109 107
110 return res.json(account.toFormattedJSON()) 108 return res.json(account.toFormattedJSON())
@@ -121,7 +119,8 @@ async function listAccountChannels (req: express.Request, res: express.Response)
121 accountId: res.locals.account.id, 119 accountId: res.locals.account.id,
122 start: req.query.start, 120 start: req.query.start,
123 count: req.query.count, 121 count: req.query.count,
124 sort: req.query.sort 122 sort: req.query.sort,
123 withStats: req.query.withStats
125 } 124 }
126 125
127 const resultList = await VideoChannelModel.listByAccount(options) 126 const resultList = await VideoChannelModel.listByAccount(options)
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts
index ae4e00248..edcb0b99e 100644
--- a/server/controllers/api/config.ts
+++ b/server/controllers/api/config.ts
@@ -1,23 +1,22 @@
1import { Hooks } from '@server/lib/plugins/hooks'
1import * as express from 'express' 2import * as express from 'express'
3import { remove, writeJSON } from 'fs-extra'
2import { snakeCase } from 'lodash' 4import { snakeCase } from 'lodash'
3import { ServerConfig, UserRight } from '../../../shared' 5import validator from 'validator'
6import { RegisteredExternalAuthConfig, RegisteredIdAndPassAuthConfig, ServerConfig, UserRight } from '../../../shared'
4import { About } from '../../../shared/models/server/about.model' 7import { About } from '../../../shared/models/server/about.model'
5import { CustomConfig } from '../../../shared/models/server/custom-config.model' 8import { CustomConfig } from '../../../shared/models/server/custom-config.model'
9import { auditLoggerFactory, CustomConfigAuditView, getAuditIdFromRes } from '../../helpers/audit-logger'
10import { objectConverter } from '../../helpers/core-utils'
6import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup' 11import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup'
12import { getServerCommit } from '../../helpers/utils'
13import { CONFIG, isEmailEnabled, reloadConfig } from '../../initializers/config'
7import { CONSTRAINTS_FIELDS, DEFAULT_THEME_NAME, PEERTUBE_VERSION } from '../../initializers/constants' 14import { CONSTRAINTS_FIELDS, DEFAULT_THEME_NAME, PEERTUBE_VERSION } from '../../initializers/constants'
8import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares'
9import { customConfigUpdateValidator } from '../../middlewares/validators/config'
10import { ClientHtml } from '../../lib/client-html' 15import { ClientHtml } from '../../lib/client-html'
11import { auditLoggerFactory, CustomConfigAuditView, getAuditIdFromRes } from '../../helpers/audit-logger'
12import { remove, writeJSON } from 'fs-extra'
13import { getServerCommit } from '../../helpers/utils'
14import { Emailer } from '../../lib/emailer'
15import validator from 'validator'
16import { objectConverter } from '../../helpers/core-utils'
17import { CONFIG, reloadConfig } from '../../initializers/config'
18import { PluginManager } from '../../lib/plugins/plugin-manager' 16import { PluginManager } from '../../lib/plugins/plugin-manager'
19import { getThemeOrDefault } from '../../lib/plugins/theme-utils' 17import { getThemeOrDefault } from '../../lib/plugins/theme-utils'
20import { Hooks } from '@server/lib/plugins/hooks' 18import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../middlewares'
19import { customConfigUpdateValidator } from '../../middlewares/validators/config'
21 20
22const configRouter = express.Router() 21const configRouter = express.Router()
23 22
@@ -31,12 +30,12 @@ configRouter.get('/',
31configRouter.get('/custom', 30configRouter.get('/custom',
32 authenticate, 31 authenticate,
33 ensureUserHasRight(UserRight.MANAGE_CONFIGURATION), 32 ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
34 asyncMiddleware(getCustomConfig) 33 getCustomConfig
35) 34)
36configRouter.put('/custom', 35configRouter.put('/custom',
37 authenticate, 36 authenticate,
38 ensureUserHasRight(UserRight.MANAGE_CONFIGURATION), 37 ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
39 asyncMiddleware(customConfigUpdateValidator), 38 customConfigUpdateValidator,
40 asyncMiddleware(updateCustomConfig) 39 asyncMiddleware(updateCustomConfig)
41) 40)
42configRouter.delete('/custom', 41configRouter.delete('/custom',
@@ -73,15 +72,23 @@ async function getConfig (req: express.Request, res: express.Response) {
73 css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS 72 css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS
74 } 73 }
75 }, 74 },
75 search: {
76 remoteUri: {
77 users: CONFIG.SEARCH.REMOTE_URI.USERS,
78 anonymous: CONFIG.SEARCH.REMOTE_URI.ANONYMOUS
79 }
80 },
76 plugin: { 81 plugin: {
77 registered: getRegisteredPlugins() 82 registered: getRegisteredPlugins(),
83 registeredExternalAuths: getExternalAuthsPlugins(),
84 registeredIdAndPassAuths: getIdAndPassAuthPlugins()
78 }, 85 },
79 theme: { 86 theme: {
80 registered: getRegisteredThemes(), 87 registered: getRegisteredThemes(),
81 default: defaultTheme 88 default: defaultTheme
82 }, 89 },
83 email: { 90 email: {
84 enabled: Emailer.isEnabled() 91 enabled: isEmailEnabled()
85 }, 92 },
86 contactForm: { 93 contactForm: {
87 enabled: CONFIG.CONTACT_FORM.ENABLED 94 enabled: CONFIG.CONTACT_FORM.ENABLED
@@ -196,7 +203,7 @@ function getAbout (req: express.Request, res: express.Response) {
196 return res.json(about).end() 203 return res.json(about).end()
197} 204}
198 205
199async function getCustomConfig (req: express.Request, res: express.Response) { 206function getCustomConfig (req: express.Request, res: express.Response) {
200 const data = customConfig() 207 const data = customConfig()
201 208
202 return res.json(data).end() 209 return res.json(data).end()
@@ -250,7 +257,7 @@ function getRegisteredThemes () {
250 257
251function getEnabledResolutions () { 258function getEnabledResolutions () {
252 return Object.keys(CONFIG.TRANSCODING.RESOLUTIONS) 259 return Object.keys(CONFIG.TRANSCODING.RESOLUTIONS)
253 .filter(key => CONFIG.TRANSCODING.ENABLED && CONFIG.TRANSCODING.RESOLUTIONS[ key ] === true) 260 .filter(key => CONFIG.TRANSCODING.ENABLED && CONFIG.TRANSCODING.RESOLUTIONS[key] === true)
254 .map(r => parseInt(r, 10)) 261 .map(r => parseInt(r, 10))
255} 262}
256 263
@@ -264,6 +271,42 @@ function getRegisteredPlugins () {
264 })) 271 }))
265} 272}
266 273
274function getIdAndPassAuthPlugins () {
275 const result: RegisteredIdAndPassAuthConfig[] = []
276
277 for (const p of PluginManager.Instance.getIdAndPassAuths()) {
278 for (const auth of p.idAndPassAuths) {
279 result.push({
280 npmName: p.npmName,
281 name: p.name,
282 version: p.version,
283 authName: auth.authName,
284 weight: auth.getWeight()
285 })
286 }
287 }
288
289 return result
290}
291
292function getExternalAuthsPlugins () {
293 const result: RegisteredExternalAuthConfig[] = []
294
295 for (const p of PluginManager.Instance.getExternalAuths()) {
296 for (const auth of p.externalAuths) {
297 result.push({
298 npmName: p.npmName,
299 name: p.name,
300 version: p.version,
301 authName: auth.authName,
302 authDisplayName: auth.authDisplayName()
303 })
304 }
305 }
306
307 return result
308}
309
267// --------------------------------------------------------------------------- 310// ---------------------------------------------------------------------------
268 311
269export { 312export {
@@ -340,13 +383,13 @@ function customConfig (): CustomConfig {
340 allowAudioFiles: CONFIG.TRANSCODING.ALLOW_AUDIO_FILES, 383 allowAudioFiles: CONFIG.TRANSCODING.ALLOW_AUDIO_FILES,
341 threads: CONFIG.TRANSCODING.THREADS, 384 threads: CONFIG.TRANSCODING.THREADS,
342 resolutions: { 385 resolutions: {
343 '0p': CONFIG.TRANSCODING.RESOLUTIONS[ '0p' ], 386 '0p': CONFIG.TRANSCODING.RESOLUTIONS['0p'],
344 '240p': CONFIG.TRANSCODING.RESOLUTIONS[ '240p' ], 387 '240p': CONFIG.TRANSCODING.RESOLUTIONS['240p'],
345 '360p': CONFIG.TRANSCODING.RESOLUTIONS[ '360p' ], 388 '360p': CONFIG.TRANSCODING.RESOLUTIONS['360p'],
346 '480p': CONFIG.TRANSCODING.RESOLUTIONS[ '480p' ], 389 '480p': CONFIG.TRANSCODING.RESOLUTIONS['480p'],
347 '720p': CONFIG.TRANSCODING.RESOLUTIONS[ '720p' ], 390 '720p': CONFIG.TRANSCODING.RESOLUTIONS['720p'],
348 '1080p': CONFIG.TRANSCODING.RESOLUTIONS[ '1080p' ], 391 '1080p': CONFIG.TRANSCODING.RESOLUTIONS['1080p'],
349 '2160p': CONFIG.TRANSCODING.RESOLUTIONS[ '2160p' ] 392 '2160p': CONFIG.TRANSCODING.RESOLUTIONS['2160p']
350 }, 393 },
351 webtorrent: { 394 webtorrent: {
352 enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED 395 enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED
diff --git a/server/controllers/api/index.ts b/server/controllers/api/index.ts
index 6138a32de..7bec6c527 100644
--- a/server/controllers/api/index.ts
+++ b/server/controllers/api/index.ts
@@ -1,5 +1,4 @@
1import * as express from 'express' 1import * as express from 'express'
2import * as RateLimit from 'express-rate-limit'
3import { configRouter } from './config' 2import { configRouter } from './config'
4import { jobsRouter } from './jobs' 3import { jobsRouter } from './jobs'
5import { oauthClientsRouter } from './oauth-clients' 4import { oauthClientsRouter } from './oauth-clients'
@@ -15,6 +14,7 @@ import { overviewsRouter } from './overviews'
15import { videoPlaylistRouter } from './video-playlist' 14import { videoPlaylistRouter } from './video-playlist'
16import { CONFIG } from '../../initializers/config' 15import { CONFIG } from '../../initializers/config'
17import { pluginRouter } from './plugins' 16import { pluginRouter } from './plugins'
17import * as RateLimit from 'express-rate-limit'
18 18
19const apiRouter = express.Router() 19const apiRouter = express.Router()
20 20
@@ -24,8 +24,6 @@ apiRouter.use(cors({
24 credentials: true 24 credentials: true
25})) 25}))
26 26
27// FIXME: https://github.com/nfriedly/express-rate-limit/issues/138
28// @ts-ignore
29const apiRateLimiter = RateLimit({ 27const apiRateLimiter = RateLimit({
30 windowMs: CONFIG.RATES_LIMIT.API.WINDOW_MS, 28 windowMs: CONFIG.RATES_LIMIT.API.WINDOW_MS,
31 max: CONFIG.RATES_LIMIT.API.MAX 29 max: CONFIG.RATES_LIMIT.API.MAX
diff --git a/server/controllers/api/jobs.ts b/server/controllers/api/jobs.ts
index 05320311e..13fc04d18 100644
--- a/server/controllers/api/jobs.ts
+++ b/server/controllers/api/jobs.ts
@@ -50,7 +50,7 @@ async function listJobs (req: express.Request, res: express.Response) {
50 }) 50 })
51 const total = await JobQueue.Instance.count(state) 51 const total = await JobQueue.Instance.count(state)
52 52
53 const result: ResultList<any> = { 53 const result: ResultList<Job> = {
54 total, 54 total,
55 data: jobs.map(j => formatJob(j, state)) 55 data: jobs.map(j => formatJob(j, state))
56 } 56 }
diff --git a/server/controllers/api/overviews.ts b/server/controllers/api/overviews.ts
index 46e76ac6b..fb31932aa 100644
--- a/server/controllers/api/overviews.ts
+++ b/server/controllers/api/overviews.ts
@@ -1,17 +1,18 @@
1import * as express from 'express' 1import * as express from 'express'
2import { buildNSFWFilter } from '../../helpers/express-utils' 2import { buildNSFWFilter } from '../../helpers/express-utils'
3import { VideoModel } from '../../models/video/video' 3import { VideoModel } from '../../models/video/video'
4import { asyncMiddleware } from '../../middlewares' 4import { asyncMiddleware, optionalAuthenticate, videosOverviewValidator } from '../../middlewares'
5import { TagModel } from '../../models/video/tag' 5import { TagModel } from '../../models/video/tag'
6import { VideosOverview } from '../../../shared/models/overviews' 6import { CategoryOverview, ChannelOverview, TagOverview, VideosOverview } from '../../../shared/models/overviews'
7import { MEMOIZE_TTL, OVERVIEWS, ROUTE_CACHE_LIFETIME } from '../../initializers/constants' 7import { MEMOIZE_TTL, OVERVIEWS } from '../../initializers/constants'
8import { cacheRoute } from '../../middlewares/cache'
9import * as memoizee from 'memoizee' 8import * as memoizee from 'memoizee'
9import { logger } from '@server/helpers/logger'
10 10
11const overviewsRouter = express.Router() 11const overviewsRouter = express.Router()
12 12
13overviewsRouter.get('/videos', 13overviewsRouter.get('/videos',
14 asyncMiddleware(cacheRoute()(ROUTE_CACHE_LIFETIME.OVERVIEWS.VIDEOS)), 14 videosOverviewValidator,
15 optionalAuthenticate,
15 asyncMiddleware(getVideosOverview) 16 asyncMiddleware(getVideosOverview)
16) 17)
17 18
@@ -24,21 +25,32 @@ export { overviewsRouter }
24const buildSamples = memoizee(async function () { 25const buildSamples = memoizee(async function () {
25 const [ categories, channels, tags ] = await Promise.all([ 26 const [ categories, channels, tags ] = await Promise.all([
26 VideoModel.getRandomFieldSamples('category', OVERVIEWS.VIDEOS.SAMPLE_THRESHOLD, OVERVIEWS.VIDEOS.SAMPLES_COUNT), 27 VideoModel.getRandomFieldSamples('category', OVERVIEWS.VIDEOS.SAMPLE_THRESHOLD, OVERVIEWS.VIDEOS.SAMPLES_COUNT),
27 VideoModel.getRandomFieldSamples('channelId', OVERVIEWS.VIDEOS.SAMPLE_THRESHOLD ,OVERVIEWS.VIDEOS.SAMPLES_COUNT), 28 VideoModel.getRandomFieldSamples('channelId', OVERVIEWS.VIDEOS.SAMPLE_THRESHOLD, OVERVIEWS.VIDEOS.SAMPLES_COUNT),
28 TagModel.getRandomSamples(OVERVIEWS.VIDEOS.SAMPLE_THRESHOLD, OVERVIEWS.VIDEOS.SAMPLES_COUNT) 29 TagModel.getRandomSamples(OVERVIEWS.VIDEOS.SAMPLE_THRESHOLD, OVERVIEWS.VIDEOS.SAMPLES_COUNT)
29 ]) 30 ])
30 31
31 return { categories, channels, tags } 32 const result = { categories, channels, tags }
33
34 logger.debug('Building samples for overview endpoint.', { result })
35
36 return result
32}, { maxAge: MEMOIZE_TTL.OVERVIEWS_SAMPLE }) 37}, { maxAge: MEMOIZE_TTL.OVERVIEWS_SAMPLE })
33 38
34// This endpoint could be quite long, but we cache it 39// This endpoint could be quite long, but we cache it
35async function getVideosOverview (req: express.Request, res: express.Response) { 40async function getVideosOverview (req: express.Request, res: express.Response) {
36 const attributes = await buildSamples() 41 const attributes = await buildSamples()
37 42
38 const [ categories, channels, tags ] = await Promise.all([ 43 const page = req.query.page || 1
39 Promise.all(attributes.categories.map(c => getVideosByCategory(c, res))), 44 const index = page - 1
40 Promise.all(attributes.channels.map(c => getVideosByChannel(c, res))), 45
41 Promise.all(attributes.tags.map(t => getVideosByTag(t, res))) 46 const categories: CategoryOverview[] = []
47 const channels: ChannelOverview[] = []
48 const tags: TagOverview[] = []
49
50 await Promise.all([
51 getVideosByCategory(attributes.categories, index, res, categories),
52 getVideosByChannel(attributes.channels, index, res, channels),
53 getVideosByTag(attributes.tags, index, res, tags)
42 ]) 54 ])
43 55
44 const result: VideosOverview = { 56 const result: VideosOverview = {
@@ -47,45 +59,49 @@ async function getVideosOverview (req: express.Request, res: express.Response) {
47 tags 59 tags
48 } 60 }
49 61
50 // Cleanup our object
51 for (const key of Object.keys(result)) {
52 result[key] = result[key].filter(v => v !== undefined)
53 }
54
55 return res.json(result) 62 return res.json(result)
56} 63}
57 64
58async function getVideosByTag (tag: string, res: express.Response) { 65async function getVideosByTag (tagsSample: string[], index: number, res: express.Response, acc: TagOverview[]) {
66 if (tagsSample.length <= index) return
67
68 const tag = tagsSample[index]
59 const videos = await getVideos(res, { tagsOneOf: [ tag ] }) 69 const videos = await getVideos(res, { tagsOneOf: [ tag ] })
60 70
61 if (videos.length === 0) return undefined 71 if (videos.length === 0) return
62 72
63 return { 73 acc.push({
64 tag, 74 tag,
65 videos 75 videos
66 } 76 })
67} 77}
68 78
69async function getVideosByCategory (category: number, res: express.Response) { 79async function getVideosByCategory (categoriesSample: number[], index: number, res: express.Response, acc: CategoryOverview[]) {
80 if (categoriesSample.length <= index) return
81
82 const category = categoriesSample[index]
70 const videos = await getVideos(res, { categoryOneOf: [ category ] }) 83 const videos = await getVideos(res, { categoryOneOf: [ category ] })
71 84
72 if (videos.length === 0) return undefined 85 if (videos.length === 0) return
73 86
74 return { 87 acc.push({
75 category: videos[0].category, 88 category: videos[0].category,
76 videos 89 videos
77 } 90 })
78} 91}
79 92
80async function getVideosByChannel (channelId: number, res: express.Response) { 93async function getVideosByChannel (channelsSample: number[], index: number, res: express.Response, acc: ChannelOverview[]) {
94 if (channelsSample.length <= index) return
95
96 const channelId = channelsSample[index]
81 const videos = await getVideos(res, { videoChannelId: channelId }) 97 const videos = await getVideos(res, { videoChannelId: channelId })
82 98
83 if (videos.length === 0) return undefined 99 if (videos.length === 0) return
84 100
85 return { 101 acc.push({
86 channel: videos[0].channel, 102 channel: videos[0].channel,
87 videos 103 videos
88 } 104 })
89} 105}
90 106
91async function getVideos ( 107async function getVideos (
diff --git a/server/controllers/api/plugins.ts b/server/controllers/api/plugins.ts
index 6b7562fd3..f8a0d19ca 100644
--- a/server/controllers/api/plugins.ts
+++ b/server/controllers/api/plugins.ts
@@ -191,6 +191,8 @@ async function updatePluginSettings (req: express.Request, res: express.Response
191 plugin.settings = req.body.settings 191 plugin.settings = req.body.settings
192 await plugin.save() 192 await plugin.save()
193 193
194 await PluginManager.Instance.onSettingsChanged(plugin.name, plugin.settings)
195
194 return res.sendStatus(204) 196 return res.sendStatus(204)
195} 197}
196 198
diff --git a/server/controllers/api/search.ts b/server/controllers/api/search.ts
index 349650aca..35d94d747 100644
--- a/server/controllers/api/search.ts
+++ b/server/controllers/api/search.ts
@@ -1,6 +1,6 @@
1import * as express from 'express' 1import * as express from 'express'
2import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' 2import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
3import { getFormattedObjects, getServerActor } from '../../helpers/utils' 3import { getFormattedObjects } from '../../helpers/utils'
4import { VideoModel } from '../../models/video/video' 4import { VideoModel } from '../../models/video/video'
5import { 5import {
6 asyncMiddleware, 6 asyncMiddleware,
@@ -15,11 +15,13 @@ import {
15 videosSearchValidator 15 videosSearchValidator
16} from '../../middlewares' 16} from '../../middlewares'
17import { VideoChannelsSearchQuery, VideosSearchQuery } from '../../../shared/models/search' 17import { VideoChannelsSearchQuery, VideosSearchQuery } from '../../../shared/models/search'
18import { getOrCreateActorAndServerAndModel, getOrCreateVideoAndAccountAndChannel } from '../../lib/activitypub' 18import { getOrCreateActorAndServerAndModel } from '../../lib/activitypub/actor'
19import { logger } from '../../helpers/logger' 19import { logger } from '../../helpers/logger'
20import { VideoChannelModel } from '../../models/video/video-channel' 20import { VideoChannelModel } from '../../models/video/video-channel'
21import { loadActorUrlOrGetFromWebfinger } from '../../helpers/webfinger' 21import { loadActorUrlOrGetFromWebfinger } from '../../helpers/webfinger'
22import { MChannelAccountDefault, MVideoAccountLightBlacklistAllFiles } from '../../typings/models' 22import { MChannelAccountDefault, MVideoAccountLightBlacklistAllFiles } from '../../typings/models'
23import { getServerActor } from '@server/models/application/application'
24import { getOrCreateVideoAndAccountAndChannel } from '@server/lib/activitypub/videos'
23 25
24const searchRouter = express.Router() 26const searchRouter = express.Router()
25 27
@@ -60,7 +62,7 @@ function searchVideoChannels (req: express.Request, res: express.Response) {
60 62
61 // Handle strings like @toto@example.com 63 // Handle strings like @toto@example.com
62 if (parts.length === 3 && parts[0].length === 0) parts.shift() 64 if (parts.length === 3 && parts[0].length === 0) parts.shift()
63 const isWebfingerSearch = parts.length === 2 && parts.every(p => p && p.indexOf(' ') === -1) 65 const isWebfingerSearch = parts.length === 2 && parts.every(p => p && !p.includes(' '))
64 66
65 if (isURISearch || isWebfingerSearch) return searchVideoChannelURI(search, isWebfingerSearch, res) 67 if (isURISearch || isWebfingerSearch) return searchVideoChannelURI(search, isWebfingerSearch, res)
66 68
diff --git a/server/controllers/api/server/debug.ts b/server/controllers/api/server/debug.ts
index 4450038f6..e12fc1dd4 100644
--- a/server/controllers/api/server/debug.ts
+++ b/server/controllers/api/server/debug.ts
@@ -1,13 +1,13 @@
1import * as express from 'express' 1import * as express from 'express'
2import { UserRight } from '../../../../shared/models/users' 2import { UserRight } from '../../../../shared/models/users'
3import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../../middlewares' 3import { authenticate, ensureUserHasRight } from '../../../middlewares'
4 4
5const debugRouter = express.Router() 5const debugRouter = express.Router()
6 6
7debugRouter.get('/debug', 7debugRouter.get('/debug',
8 authenticate, 8 authenticate,
9 ensureUserHasRight(UserRight.MANAGE_DEBUG), 9 ensureUserHasRight(UserRight.MANAGE_DEBUG),
10 asyncMiddleware(getDebug) 10 getDebug
11) 11)
12 12
13// --------------------------------------------------------------------------- 13// ---------------------------------------------------------------------------
@@ -18,7 +18,7 @@ export {
18 18
19// --------------------------------------------------------------------------- 19// ---------------------------------------------------------------------------
20 20
21async function getDebug (req: express.Request, res: express.Response) { 21function getDebug (req: express.Request, res: express.Response) {
22 return res.json({ 22 return res.json({
23 ip: req.ip 23 ip: req.ip
24 }).end() 24 }).end()
diff --git a/server/controllers/api/server/follows.ts b/server/controllers/api/server/follows.ts
index 29a403a43..23823c9fb 100644
--- a/server/controllers/api/server/follows.ts
+++ b/server/controllers/api/server/follows.ts
@@ -1,7 +1,7 @@
1import * as express from 'express' 1import * 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 } from '../../../helpers/utils'
5import { SERVER_ACTOR_NAME } from '../../../initializers/constants' 5import { SERVER_ACTOR_NAME } from '../../../initializers/constants'
6import { sendAccept, sendReject, sendUndoFollow } from '../../../lib/activitypub/send' 6import { sendAccept, sendReject, sendUndoFollow } from '../../../lib/activitypub/send'
7import { 7import {
@@ -24,9 +24,10 @@ import {
24} from '../../../middlewares/validators' 24} from '../../../middlewares/validators'
25import { ActorFollowModel } from '../../../models/activitypub/actor-follow' 25import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
26import { JobQueue } from '../../../lib/job-queue' 26import { JobQueue } from '../../../lib/job-queue'
27import { removeRedundancyOf } from '../../../lib/redundancy' 27import { removeRedundanciesOfServer } from '../../../lib/redundancy'
28import { sequelizeTypescript } from '../../../initializers/database' 28import { sequelizeTypescript } from '../../../initializers/database'
29import { autoFollowBackIfNeeded } from '../../../lib/activitypub/follow' 29import { autoFollowBackIfNeeded } from '../../../lib/activitypub/follow'
30import { getServerActor } from '@server/models/application/application'
30 31
31const serverFollowsRouter = express.Router() 32const serverFollowsRouter = express.Router()
32serverFollowsRouter.get('/following', 33serverFollowsRouter.get('/following',
@@ -135,7 +136,6 @@ async function followInstance (req: express.Request, res: express.Response) {
135 } 136 }
136 137
137 JobQueue.Instance.createJob({ type: 'activitypub-follow', payload }) 138 JobQueue.Instance.createJob({ type: 'activitypub-follow', payload })
138 .catch(err => logger.error('Cannot create follow job for %s.', host, err))
139 } 139 }
140 140
141 return res.status(204).end() 141 return res.status(204).end()
@@ -153,7 +153,7 @@ async function removeFollowing (req: express.Request, res: express.Response) {
153 await server.save({ transaction: t }) 153 await server.save({ transaction: t })
154 154
155 // Async, could be long 155 // Async, could be long
156 removeRedundancyOf(server.id) 156 removeRedundanciesOfServer(server.id)
157 .catch(err => logger.error('Cannot remove redundancy of %s.', server.host, err)) 157 .catch(err => logger.error('Cannot remove redundancy of %s.', server.host, err))
158 158
159 await follow.destroy({ transaction: t }) 159 await follow.destroy({ transaction: t })
diff --git a/server/controllers/api/server/logs.ts b/server/controllers/api/server/logs.ts
index cd1e0f5bf..4b543d686 100644
--- a/server/controllers/api/server/logs.ts
+++ b/server/controllers/api/server/logs.ts
@@ -59,9 +59,9 @@ async function getLogs (req: express.Request, res: express.Response) {
59} 59}
60 60
61async function generateOutput (options: { 61async function generateOutput (options: {
62 startDateQuery: string, 62 startDateQuery: string
63 endDateQuery?: string, 63 endDateQuery?: string
64 level: LogLevel, 64 level: LogLevel
65 nameFilter: RegExp 65 nameFilter: RegExp
66}) { 66}) {
67 const { startDateQuery, level, nameFilter } = options 67 const { startDateQuery, level, nameFilter } = options
@@ -111,7 +111,7 @@ async function getOutputFromFile (path: string, startDate: Date, endDate: Date,
111 const output: any[] = [] 111 const output: any[] = []
112 112
113 for (let i = lines.length - 1; i >= 0; i--) { 113 for (let i = lines.length - 1; i >= 0; i--) {
114 const line = lines[ i ] 114 const line = lines[i]
115 let log: any 115 let log: any
116 116
117 try { 117 try {
@@ -122,7 +122,7 @@ async function getOutputFromFile (path: string, startDate: Date, endDate: Date,
122 } 122 }
123 123
124 logTime = new Date(log.timestamp).getTime() 124 logTime = new Date(log.timestamp).getTime()
125 if (logTime >= startTime && logTime <= endTime && logsLevel[ log.level ] >= logsLevel[ level ]) { 125 if (logTime >= startTime && logTime <= endTime && logsLevel[log.level] >= logsLevel[level]) {
126 output.push(log) 126 output.push(log)
127 127
128 currentSize += line.length 128 currentSize += line.length
diff --git a/server/controllers/api/server/redundancy.ts b/server/controllers/api/server/redundancy.ts
index 4ea6164a3..1ced0759e 100644
--- a/server/controllers/api/server/redundancy.ts
+++ b/server/controllers/api/server/redundancy.ts
@@ -1,9 +1,24 @@
1import * as express from 'express' 1import * as express from 'express'
2import { UserRight } from '../../../../shared/models/users' 2import { UserRight } from '../../../../shared/models/users'
3import { asyncMiddleware, authenticate, ensureUserHasRight } from '../../../middlewares' 3import {
4import { updateServerRedundancyValidator } from '../../../middlewares/validators/redundancy' 4 asyncMiddleware,
5import { removeRedundancyOf } from '../../../lib/redundancy' 5 authenticate,
6 ensureUserHasRight,
7 paginationValidator,
8 setDefaultPagination,
9 setDefaultVideoRedundanciesSort,
10 videoRedundanciesSortValidator
11} from '../../../middlewares'
12import {
13 listVideoRedundanciesValidator,
14 updateServerRedundancyValidator,
15 addVideoRedundancyValidator,
16 removeVideoRedundancyValidator
17} from '../../../middlewares/validators/redundancy'
18import { removeRedundanciesOfServer, removeVideoRedundancy } from '../../../lib/redundancy'
6import { logger } from '../../../helpers/logger' 19import { logger } from '../../../helpers/logger'
20import { VideoRedundancyModel } from '@server/models/redundancy/video-redundancy'
21import { JobQueue } from '@server/lib/job-queue'
7 22
8const serverRedundancyRouter = express.Router() 23const serverRedundancyRouter = express.Router()
9 24
@@ -14,6 +29,31 @@ serverRedundancyRouter.put('/redundancy/:host',
14 asyncMiddleware(updateRedundancy) 29 asyncMiddleware(updateRedundancy)
15) 30)
16 31
32serverRedundancyRouter.get('/redundancy/videos',
33 authenticate,
34 ensureUserHasRight(UserRight.MANAGE_VIDEOS_REDUNDANCIES),
35 listVideoRedundanciesValidator,
36 paginationValidator,
37 videoRedundanciesSortValidator,
38 setDefaultVideoRedundanciesSort,
39 setDefaultPagination,
40 asyncMiddleware(listVideoRedundancies)
41)
42
43serverRedundancyRouter.post('/redundancy/videos',
44 authenticate,
45 ensureUserHasRight(UserRight.MANAGE_VIDEOS_REDUNDANCIES),
46 addVideoRedundancyValidator,
47 asyncMiddleware(addVideoRedundancy)
48)
49
50serverRedundancyRouter.delete('/redundancy/videos/:redundancyId',
51 authenticate,
52 ensureUserHasRight(UserRight.MANAGE_VIDEOS_REDUNDANCIES),
53 removeVideoRedundancyValidator,
54 asyncMiddleware(removeVideoRedundancyController)
55)
56
17// --------------------------------------------------------------------------- 57// ---------------------------------------------------------------------------
18 58
19export { 59export {
@@ -22,6 +62,42 @@ export {
22 62
23// --------------------------------------------------------------------------- 63// ---------------------------------------------------------------------------
24 64
65async function listVideoRedundancies (req: express.Request, res: express.Response) {
66 const resultList = await VideoRedundancyModel.listForApi({
67 start: req.query.start,
68 count: req.query.count,
69 sort: req.query.sort,
70 target: req.query.target,
71 strategy: req.query.strategy
72 })
73
74 const result = {
75 total: resultList.total,
76 data: resultList.data.map(r => VideoRedundancyModel.toFormattedJSONStatic(r))
77 }
78
79 return res.json(result)
80}
81
82async function addVideoRedundancy (req: express.Request, res: express.Response) {
83 const payload = {
84 videoId: res.locals.onlyVideo.id
85 }
86
87 await JobQueue.Instance.createJobWithPromise({
88 type: 'video-redundancy',
89 payload
90 })
91
92 return res.sendStatus(204)
93}
94
95async function removeVideoRedundancyController (req: express.Request, res: express.Response) {
96 await removeVideoRedundancy(res.locals.videoRedundancy)
97
98 return res.sendStatus(204)
99}
100
25async function updateRedundancy (req: express.Request, res: express.Response) { 101async function updateRedundancy (req: express.Request, res: express.Response) {
26 const server = res.locals.server 102 const server = res.locals.server
27 103
@@ -30,7 +106,7 @@ async function updateRedundancy (req: express.Request, res: express.Response) {
30 await server.save() 106 await server.save()
31 107
32 // Async, could be long 108 // Async, could be long
33 removeRedundancyOf(server.id) 109 removeRedundanciesOfServer(server.id)
34 .catch(err => logger.error('Cannot remove redundancy of %s.', server.host, { err })) 110 .catch(err => logger.error('Cannot remove redundancy of %s.', server.host, { err }))
35 111
36 return res.sendStatus(204) 112 return res.sendStatus(204)
diff --git a/server/controllers/api/server/server-blocklist.ts b/server/controllers/api/server/server-blocklist.ts
index d165db191..f849b15c7 100644
--- a/server/controllers/api/server/server-blocklist.ts
+++ b/server/controllers/api/server/server-blocklist.ts
@@ -1,6 +1,6 @@
1import * as express from 'express' 1import * as express from 'express'
2import 'multer' 2import 'multer'
3import { getFormattedObjects, getServerActor } from '../../../helpers/utils' 3import { getFormattedObjects } from '../../../helpers/utils'
4import { 4import {
5 asyncMiddleware, 5 asyncMiddleware,
6 asyncRetryTransactionMiddleware, 6 asyncRetryTransactionMiddleware,
@@ -22,6 +22,7 @@ import { AccountBlocklistModel } from '../../../models/account/account-blocklist
22import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist' 22import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist'
23import { ServerBlocklistModel } from '../../../models/server/server-blocklist' 23import { ServerBlocklistModel } from '../../../models/server/server-blocklist'
24import { UserRight } from '../../../../shared/models/users' 24import { UserRight } from '../../../../shared/models/users'
25import { getServerActor } from '@server/models/application/application'
25 26
26const serverBlocklistRouter = express.Router() 27const serverBlocklistRouter = express.Router()
27 28
@@ -82,7 +83,13 @@ export {
82async function listBlockedAccounts (req: express.Request, res: express.Response) { 83async function listBlockedAccounts (req: express.Request, res: express.Response) {
83 const serverActor = await getServerActor() 84 const serverActor = await getServerActor()
84 85
85 const resultList = await AccountBlocklistModel.listForApi(serverActor.Account.id, req.query.start, req.query.count, req.query.sort) 86 const resultList = await AccountBlocklistModel.listForApi({
87 start: req.query.start,
88 count: req.query.count,
89 sort: req.query.sort,
90 search: req.query.search,
91 accountId: serverActor.Account.id
92 })
86 93
87 return res.json(getFormattedObjects(resultList.data, resultList.total)) 94 return res.json(getFormattedObjects(resultList.data, resultList.total))
88} 95}
@@ -107,7 +114,13 @@ async function unblockAccount (req: express.Request, res: express.Response) {
107async function listBlockedServers (req: express.Request, res: express.Response) { 114async function listBlockedServers (req: express.Request, res: express.Response) {
108 const serverActor = await getServerActor() 115 const serverActor = await getServerActor()
109 116
110 const resultList = await ServerBlocklistModel.listForApi(serverActor.Account.id, req.query.start, req.query.count, req.query.sort) 117 const resultList = await ServerBlocklistModel.listForApi({
118 start: req.query.start,
119 count: req.query.count,
120 sort: req.query.sort,
121 search: req.query.search,
122 accountId: serverActor.Account.id
123 })
111 124
112 return res.json(getFormattedObjects(resultList.data, resultList.total)) 125 return res.json(getFormattedObjects(resultList.data, resultList.total))
113} 126}
diff --git a/server/controllers/api/server/stats.ts b/server/controllers/api/server/stats.ts
index 3616c074d..f07301a04 100644
--- a/server/controllers/api/server/stats.ts
+++ b/server/controllers/api/server/stats.ts
@@ -10,6 +10,7 @@ import { 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' 12import { CONFIG } from '../../../initializers/config'
13import { VideoRedundancyStrategyWithManual } from '@shared/models'
13 14
14const statsRouter = express.Router() 15const statsRouter = express.Router()
15 16
@@ -21,12 +22,20 @@ statsRouter.get('/stats',
21async function getStats (req: express.Request, res: express.Response) { 22async function getStats (req: express.Request, res: express.Response) {
22 const { totalLocalVideos, totalLocalVideoViews, totalVideos } = await VideoModel.getStats() 23 const { totalLocalVideos, totalLocalVideoViews, totalVideos } = await VideoModel.getStats()
23 const { totalLocalVideoComments, totalVideoComments } = await VideoCommentModel.getStats() 24 const { totalLocalVideoComments, totalVideoComments } = await VideoCommentModel.getStats()
24 const { totalUsers } = await UserModel.getStats() 25 const { totalUsers, totalDailyActiveUsers, totalWeeklyActiveUsers, totalMonthlyActiveUsers } = await UserModel.getStats()
25 const { totalInstanceFollowers, totalInstanceFollowing } = await ActorFollowModel.getStats() 26 const { totalInstanceFollowers, totalInstanceFollowing } = await ActorFollowModel.getStats()
26 const { totalLocalVideoFilesSize } = await VideoFileModel.getStats() 27 const { totalLocalVideoFilesSize } = await VideoFileModel.getStats()
27 28
29 const strategies = CONFIG.REDUNDANCY.VIDEOS.STRATEGIES
30 .map(r => ({
31 strategy: r.strategy as VideoRedundancyStrategyWithManual,
32 size: r.size
33 }))
34
35 strategies.push({ strategy: 'manual', size: null })
36
28 const videosRedundancyStats = await Promise.all( 37 const videosRedundancyStats = await Promise.all(
29 CONFIG.REDUNDANCY.VIDEOS.STRATEGIES.map(r => { 38 strategies.map(r => {
30 return VideoRedundancyModel.getStats(r.strategy) 39 return VideoRedundancyModel.getStats(r.strategy)
31 .then(stats => Object.assign(stats, { strategy: r.strategy, totalSize: r.size })) 40 .then(stats => Object.assign(stats, { strategy: r.strategy, totalSize: r.size }))
32 }) 41 })
@@ -39,9 +48,15 @@ async function getStats (req: express.Request, res: express.Response) {
39 totalLocalVideoComments, 48 totalLocalVideoComments,
40 totalVideos, 49 totalVideos,
41 totalVideoComments, 50 totalVideoComments,
51
42 totalUsers, 52 totalUsers,
53 totalDailyActiveUsers,
54 totalWeeklyActiveUsers,
55 totalMonthlyActiveUsers,
56
43 totalInstanceFollowers, 57 totalInstanceFollowers,
44 totalInstanceFollowing, 58 totalInstanceFollowing,
59
45 videosRedundancy: videosRedundancyStats 60 videosRedundancy: videosRedundancyStats
46 } 61 }
47 62
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts
index b960e80c1..c488f720b 100644
--- a/server/controllers/api/users/index.ts
+++ b/server/controllers/api/users/index.ts
@@ -2,7 +2,7 @@ import * as express from 'express'
2import * as RateLimit from 'express-rate-limit' 2import * 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 { generateRandomString, getFormattedObjects } from '../../../helpers/utils'
6import { WEBSERVER } from '../../../initializers/constants' 6import { WEBSERVER } from '../../../initializers/constants'
7import { Emailer } from '../../../lib/emailer' 7import { Emailer } from '../../../lib/emailer'
8import { Redis } from '../../../lib/redis' 8import { Redis } from '../../../lib/redis'
@@ -17,7 +17,6 @@ import {
17 paginationValidator, 17 paginationValidator,
18 setDefaultPagination, 18 setDefaultPagination,
19 setDefaultSort, 19 setDefaultSort,
20 token,
21 userAutocompleteValidator, 20 userAutocompleteValidator,
22 usersAddValidator, 21 usersAddValidator,
23 usersGetValidator, 22 usersGetValidator,
@@ -27,12 +26,12 @@ import {
27 usersUpdateValidator 26 usersUpdateValidator
28} from '../../../middlewares' 27} from '../../../middlewares'
29import { 28import {
29 ensureCanManageUser,
30 usersAskResetPasswordValidator, 30 usersAskResetPasswordValidator,
31 usersAskSendVerifyEmailValidator, 31 usersAskSendVerifyEmailValidator,
32 usersBlockingValidator, 32 usersBlockingValidator,
33 usersResetPasswordValidator, 33 usersResetPasswordValidator,
34 usersVerifyEmailValidator, 34 usersVerifyEmailValidator
35 ensureCanManageUser
36} from '../../../middlewares/validators' 35} from '../../../middlewares/validators'
37import { UserModel } from '../../../models/account/user' 36import { UserModel } from '../../../models/account/user'
38import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger' 37import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger'
@@ -50,16 +49,10 @@ import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
50import { UserRegister } from '../../../../shared/models/users/user-register.model' 49import { UserRegister } from '../../../../shared/models/users/user-register.model'
51import { MUser, MUserAccountDefault } from '@server/typings/models' 50import { MUser, MUserAccountDefault } from '@server/typings/models'
52import { Hooks } from '@server/lib/plugins/hooks' 51import { Hooks } from '@server/lib/plugins/hooks'
52import { tokensRouter } from '@server/controllers/api/users/token'
53 53
54const auditLogger = auditLoggerFactory('users') 54const auditLogger = auditLoggerFactory('users')
55 55
56// FIXME: https://github.com/nfriedly/express-rate-limit/issues/138
57// @ts-ignore
58const loginRateLimiter = RateLimit({
59 windowMs: CONFIG.RATES_LIMIT.LOGIN.WINDOW_MS,
60 max: CONFIG.RATES_LIMIT.LOGIN.MAX
61})
62
63// @ts-ignore 56// @ts-ignore
64const signupRateLimiter = RateLimit({ 57const signupRateLimiter = RateLimit({
65 windowMs: CONFIG.RATES_LIMIT.SIGNUP.WINDOW_MS, 58 windowMs: CONFIG.RATES_LIMIT.SIGNUP.WINDOW_MS,
@@ -74,6 +67,7 @@ const askSendEmailLimiter = new RateLimit({
74}) 67})
75 68
76const usersRouter = express.Router() 69const usersRouter = express.Router()
70usersRouter.use('/', tokensRouter)
77usersRouter.use('/', myNotificationsRouter) 71usersRouter.use('/', myNotificationsRouter)
78usersRouter.use('/', mySubscriptionsRouter) 72usersRouter.use('/', mySubscriptionsRouter)
79usersRouter.use('/', myBlocklistRouter) 73usersRouter.use('/', myBlocklistRouter)
@@ -170,13 +164,6 @@ usersRouter.post('/:id/verify-email',
170 asyncMiddleware(verifyUserEmail) 164 asyncMiddleware(verifyUserEmail)
171) 165)
172 166
173usersRouter.post('/token',
174 loginRateLimiter,
175 token,
176 tokenSuccess
177)
178// TODO: Once https://github.com/oauthjs/node-oauth2-server/pull/289 is merged, implement revoke token route
179
180// --------------------------------------------------------------------------- 167// ---------------------------------------------------------------------------
181 168
182export { 169export {
@@ -199,11 +186,25 @@ async function createUser (req: express.Request, res: express.Response) {
199 adminFlags: body.adminFlags || UserAdminFlag.NONE 186 adminFlags: body.adminFlags || UserAdminFlag.NONE
200 }) as MUser 187 }) as MUser
201 188
189 // NB: due to the validator usersAddValidator, password==='' can only be true if we can send the mail.
190 const createPassword = userToCreate.password === ''
191 if (createPassword) {
192 userToCreate.password = await generateRandomString(20)
193 }
194
202 const { user, account, videoChannel } = await createUserAccountAndChannelAndPlaylist({ userToCreate: userToCreate }) 195 const { user, account, videoChannel } = await createUserAccountAndChannelAndPlaylist({ userToCreate: userToCreate })
203 196
204 auditLogger.create(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON())) 197 auditLogger.create(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()))
205 logger.info('User %s with its channel and account created.', body.username) 198 logger.info('User %s with its channel and account created.', body.username)
206 199
200 if (createPassword) {
201 // this will send an email for newly created users, so then can set their first password.
202 logger.info('Sending to user %s a create password email', body.username)
203 const verificationString = await Redis.Instance.setCreatePasswordVerificationString(user.id)
204 const url = WEBSERVER.URL + '/reset-password?userId=' + user.id + '&verificationString=' + verificationString
205 await Emailer.Instance.addPasswordCreateEmailJob(userToCreate.username, user.email, url)
206 }
207
207 Hooks.runAction('action:api.user.created', { body, user, account, videoChannel }) 208 Hooks.runAction('action:api.user.created', { body, user, account, videoChannel })
208 209
209 return res.json({ 210 return res.json({
@@ -369,12 +370,6 @@ async function verifyUserEmail (req: express.Request, res: express.Response) {
369 return res.status(204).end() 370 return res.status(204).end()
370} 371}
371 372
372function tokenSuccess (req: express.Request) {
373 const username = req.body.username
374
375 Hooks.runAction('action:api.user.oauth2-got-token', { username, ip: req.ip })
376}
377
378async function changeUserBlock (res: express.Response, user: MUserAccountDefault, block: boolean, reason?: string) { 373async function changeUserBlock (res: express.Response, user: MUserAccountDefault, block: boolean, reason?: string) {
379 const oldUserAuditView = new UserAuditView(user.toFormattedJSON()) 374 const oldUserAuditView = new UserAuditView(user.toFormattedJSON())
380 375
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts
index ac7c62aab..23890e20c 100644
--- a/server/controllers/api/users/me.ts
+++ b/server/controllers/api/users/me.ts
@@ -39,7 +39,7 @@ meRouter.get('/me',
39) 39)
40meRouter.delete('/me', 40meRouter.delete('/me',
41 authenticate, 41 authenticate,
42 asyncMiddleware(deleteMeValidator), 42 deleteMeValidator,
43 asyncMiddleware(deleteMe) 43 asyncMiddleware(deleteMe)
44) 44)
45 45
@@ -214,7 +214,7 @@ async function updateMe (req: express.Request, res: express.Response) {
214} 214}
215 215
216async function updateMyAvatar (req: express.Request, res: express.Response) { 216async function updateMyAvatar (req: express.Request, res: express.Response) {
217 const avatarPhysicalFile = req.files[ 'avatarfile' ][ 0 ] 217 const avatarPhysicalFile = req.files['avatarfile'][0]
218 const user = res.locals.oauth.token.user 218 const user = res.locals.oauth.token.user
219 219
220 const userAccount = await AccountModel.load(user.Account.id) 220 const userAccount = await AccountModel.load(user.Account.id)
diff --git a/server/controllers/api/users/my-blocklist.ts b/server/controllers/api/users/my-blocklist.ts
index 713c16022..3a44376f2 100644
--- a/server/controllers/api/users/my-blocklist.ts
+++ b/server/controllers/api/users/my-blocklist.ts
@@ -74,7 +74,13 @@ export {
74async function listBlockedAccounts (req: express.Request, res: express.Response) { 74async function listBlockedAccounts (req: express.Request, res: express.Response) {
75 const user = res.locals.oauth.token.User 75 const user = res.locals.oauth.token.User
76 76
77 const resultList = await AccountBlocklistModel.listForApi(user.Account.id, req.query.start, req.query.count, req.query.sort) 77 const resultList = await AccountBlocklistModel.listForApi({
78 start: req.query.start,
79 count: req.query.count,
80 sort: req.query.sort,
81 search: req.query.search,
82 accountId: user.Account.id
83 })
78 84
79 return res.json(getFormattedObjects(resultList.data, resultList.total)) 85 return res.json(getFormattedObjects(resultList.data, resultList.total))
80} 86}
@@ -99,7 +105,13 @@ async function unblockAccount (req: express.Request, res: express.Response) {
99async function listBlockedServers (req: express.Request, res: express.Response) { 105async function listBlockedServers (req: express.Request, res: express.Response) {
100 const user = res.locals.oauth.token.User 106 const user = res.locals.oauth.token.User
101 107
102 const resultList = await ServerBlocklistModel.listForApi(user.Account.id, req.query.start, req.query.count, req.query.sort) 108 const resultList = await ServerBlocklistModel.listForApi({
109 start: req.query.start,
110 count: req.query.count,
111 sort: req.query.sort,
112 search: req.query.search,
113 accountId: user.Account.id
114 })
103 115
104 return res.json(getFormattedObjects(resultList.data, resultList.total)) 116 return res.json(getFormattedObjects(resultList.data, resultList.total))
105} 117}
diff --git a/server/controllers/api/users/my-history.ts b/server/controllers/api/users/my-history.ts
index 4da1f3496..77a15e5fc 100644
--- a/server/controllers/api/users/my-history.ts
+++ b/server/controllers/api/users/my-history.ts
@@ -9,7 +9,7 @@ import {
9} from '../../../middlewares' 9} from '../../../middlewares'
10import { getFormattedObjects } from '../../../helpers/utils' 10import { getFormattedObjects } from '../../../helpers/utils'
11import { UserVideoHistoryModel } from '../../../models/account/user-video-history' 11import { UserVideoHistoryModel } from '../../../models/account/user-video-history'
12import { sequelizeTypescript } from '../../../initializers' 12import { sequelizeTypescript } from '../../../initializers/database'
13 13
14const myVideosHistoryRouter = express.Router() 14const myVideosHistoryRouter = express.Router()
15 15
diff --git a/server/controllers/api/users/my-subscriptions.ts b/server/controllers/api/users/my-subscriptions.ts
index 43c4c37d8..efe1b9bc3 100644
--- a/server/controllers/api/users/my-subscriptions.ts
+++ b/server/controllers/api/users/my-subscriptions.ts
@@ -19,7 +19,6 @@ import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils'
19import { VideoFilter } from '../../../../shared/models/videos/video-query.type' 19import { VideoFilter } from '../../../../shared/models/videos/video-query.type'
20import { ActorFollowModel } from '../../../models/activitypub/actor-follow' 20import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
21import { JobQueue } from '../../../lib/job-queue' 21import { JobQueue } from '../../../lib/job-queue'
22import { logger } from '../../../helpers/logger'
23import { sequelizeTypescript } from '../../../initializers/database' 22import { sequelizeTypescript } from '../../../initializers/database'
24 23
25const mySubscriptionsRouter = express.Router() 24const mySubscriptionsRouter = express.Router()
@@ -52,7 +51,7 @@ mySubscriptionsRouter.get('/me/subscriptions',
52mySubscriptionsRouter.post('/me/subscriptions', 51mySubscriptionsRouter.post('/me/subscriptions',
53 authenticate, 52 authenticate,
54 userSubscriptionAddValidator, 53 userSubscriptionAddValidator,
55 asyncMiddleware(addUserSubscription) 54 addUserSubscription
56) 55)
57 56
58mySubscriptionsRouter.get('/me/subscriptions/:uri', 57mySubscriptionsRouter.get('/me/subscriptions/:uri',
@@ -106,18 +105,18 @@ async function areSubscriptionsExist (req: express.Request, res: express.Respons
106 return res.json(existObject) 105 return res.json(existObject)
107} 106}
108 107
109async function addUserSubscription (req: express.Request, res: express.Response) { 108function addUserSubscription (req: express.Request, res: express.Response) {
110 const user = res.locals.oauth.token.User 109 const user = res.locals.oauth.token.User
111 const [ name, host ] = req.body.uri.split('@') 110 const [ name, host ] = req.body.uri.split('@')
112 111
113 const payload = { 112 const payload = {
114 name, 113 name,
115 host, 114 host,
115 assertIsChannel: true,
116 followerActorId: user.Account.Actor.id 116 followerActorId: user.Account.Actor.id
117 } 117 }
118 118
119 JobQueue.Instance.createJob({ type: 'activitypub-follow', payload }) 119 JobQueue.Instance.createJob({ type: 'activitypub-follow', payload })
120 .catch(err => logger.error('Cannot create follow job for subscription %s.', req.body.uri, err))
121 120
122 return res.status(204).end() 121 return res.status(204).end()
123} 122}
diff --git a/server/controllers/api/users/token.ts b/server/controllers/api/users/token.ts
new file mode 100644
index 000000000..41aa26769
--- /dev/null
+++ b/server/controllers/api/users/token.ts
@@ -0,0 +1,37 @@
1import { handleLogin, handleTokenRevocation } from '@server/lib/auth'
2import * as RateLimit from 'express-rate-limit'
3import { CONFIG } from '@server/initializers/config'
4import * as express from 'express'
5import { Hooks } from '@server/lib/plugins/hooks'
6import { asyncMiddleware, authenticate } from '@server/middlewares'
7
8const tokensRouter = express.Router()
9
10const loginRateLimiter = RateLimit({
11 windowMs: CONFIG.RATES_LIMIT.LOGIN.WINDOW_MS,
12 max: CONFIG.RATES_LIMIT.LOGIN.MAX
13})
14
15tokensRouter.post('/token',
16 loginRateLimiter,
17 handleLogin,
18 tokenSuccess
19)
20
21tokensRouter.post('/revoke-token',
22 authenticate,
23 asyncMiddleware(handleTokenRevocation)
24)
25
26// ---------------------------------------------------------------------------
27
28export {
29 tokensRouter
30}
31// ---------------------------------------------------------------------------
32
33function tokenSuccess (req: express.Request) {
34 const username = req.body.username
35
36 Hooks.runAction('action:api.user.oauth2-got-token', { username, ip: req.ip })
37}
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts
index e1f37a8fb..d779f1aab 100644
--- a/server/controllers/api/video-channel.ts
+++ b/server/controllers/api/video-channel.ts
@@ -1,5 +1,5 @@
1import * as express from 'express' 1import * as express from 'express'
2import { getFormattedObjects, getServerActor } from '../../helpers/utils' 2import { getFormattedObjects } from '../../helpers/utils'
3import { 3import {
4 asyncMiddleware, 4 asyncMiddleware,
5 asyncRetryTransactionMiddleware, 5 asyncRetryTransactionMiddleware,
@@ -21,7 +21,7 @@ import { sendUpdateActor } from '../../lib/activitypub/send'
21import { VideoChannelCreate, VideoChannelUpdate } from '../../../shared' 21import { VideoChannelCreate, VideoChannelUpdate } from '../../../shared'
22import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel' 22import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel'
23import { buildNSFWFilter, createReqFiles, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' 23import { buildNSFWFilter, createReqFiles, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
24import { setAsyncActorKeys } from '../../lib/activitypub' 24import { setAsyncActorKeys } from '../../lib/activitypub/actor'
25import { AccountModel } from '../../models/account/account' 25import { AccountModel } from '../../models/account/account'
26import { MIMETYPES } from '../../initializers/constants' 26import { MIMETYPES } from '../../initializers/constants'
27import { logger } from '../../helpers/logger' 27import { logger } from '../../helpers/logger'
@@ -36,6 +36,7 @@ import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validator
36import { CONFIG } from '../../initializers/config' 36import { CONFIG } from '../../initializers/config'
37import { sequelizeTypescript } from '../../initializers/database' 37import { sequelizeTypescript } from '../../initializers/database'
38import { MChannelAccountDefault } from '@server/typings/models' 38import { MChannelAccountDefault } from '@server/typings/models'
39import { getServerActor } from '@server/models/application/application'
39 40
40const auditLogger = auditLoggerFactory('channels') 41const auditLogger = auditLoggerFactory('channels')
41const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR }) 42const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR })
@@ -119,7 +120,7 @@ async function listVideoChannels (req: express.Request, res: express.Response) {
119} 120}
120 121
121async function updateVideoChannelAvatar (req: express.Request, res: express.Response) { 122async function updateVideoChannelAvatar (req: express.Request, res: express.Response) {
122 const avatarPhysicalFile = req.files[ 'avatarfile' ][ 0 ] 123 const avatarPhysicalFile = req.files['avatarfile'][0]
123 const videoChannel = res.locals.videoChannel 124 const videoChannel = res.locals.videoChannel
124 const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON()) 125 const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON())
125 126
@@ -232,7 +233,6 @@ async function getVideoChannel (req: express.Request, res: express.Response) {
232 233
233 if (videoChannelWithVideos.isOutdated()) { 234 if (videoChannelWithVideos.isOutdated()) {
234 JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'actor', url: videoChannelWithVideos.Actor.url } }) 235 JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'actor', url: videoChannelWithVideos.Actor.url } })
235 .catch(err => logger.error('Cannot create AP refresher job for actor %s.', videoChannelWithVideos.Actor.url, { err }))
236 } 236 }
237 237
238 return res.json(videoChannelWithVideos.toFormattedJSON()) 238 return res.json(videoChannelWithVideos.toFormattedJSON())
diff --git a/server/controllers/api/video-playlist.ts b/server/controllers/api/video-playlist.ts
index d9f0ff925..375d711fd 100644
--- a/server/controllers/api/video-playlist.ts
+++ b/server/controllers/api/video-playlist.ts
@@ -1,5 +1,5 @@
1import * as express from 'express' 1import * as express from 'express'
2import { getFormattedObjects, getServerActor } from '../../helpers/utils' 2import { getFormattedObjects } from '../../helpers/utils'
3import { 3import {
4 asyncMiddleware, 4 asyncMiddleware,
5 asyncRetryTransactionMiddleware, 5 asyncRetryTransactionMiddleware,
@@ -41,6 +41,7 @@ import { CONFIG } from '../../initializers/config'
41import { sequelizeTypescript } from '../../initializers/database' 41import { sequelizeTypescript } from '../../initializers/database'
42import { createPlaylistMiniatureFromExisting } from '../../lib/thumbnail' 42import { createPlaylistMiniatureFromExisting } from '../../lib/thumbnail'
43import { MVideoPlaylistFull, MVideoPlaylistThumbnail, MVideoThumbnail } from '@server/typings/models' 43import { MVideoPlaylistFull, MVideoPlaylistThumbnail, MVideoThumbnail } from '@server/typings/models'
44import { getServerActor } from '@server/models/application/application'
44 45
45const reqThumbnailFile = createReqFiles([ 'thumbnailfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { thumbnailfile: CONFIG.STORAGE.TMP_DIR }) 46const reqThumbnailFile = createReqFiles([ 'thumbnailfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { thumbnailfile: CONFIG.STORAGE.TMP_DIR })
46 47
@@ -144,7 +145,6 @@ function getVideoPlaylist (req: express.Request, res: express.Response) {
144 145
145 if (videoPlaylist.isOutdated()) { 146 if (videoPlaylist.isOutdated()) {
146 JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video-playlist', url: videoPlaylist.url } }) 147 JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video-playlist', url: videoPlaylist.url } })
147 .catch(err => logger.error('Cannot create AP refresher job for playlist %s.', videoPlaylist.url, { err }))
148 } 148 }
149 149
150 return res.json(videoPlaylist.toFormattedJSON()) 150 return res.json(videoPlaylist.toFormattedJSON())
@@ -464,7 +464,13 @@ async function regeneratePlaylistThumbnail (videoPlaylist: MVideoPlaylistThumbna
464async function generateThumbnailForPlaylist (videoPlaylist: MVideoPlaylistThumbnail, video: MVideoThumbnail) { 464async function generateThumbnailForPlaylist (videoPlaylist: MVideoPlaylistThumbnail, video: MVideoThumbnail) {
465 logger.info('Generating default thumbnail to playlist %s.', videoPlaylist.url) 465 logger.info('Generating default thumbnail to playlist %s.', videoPlaylist.url)
466 466
467 const inputPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getMiniature().filename) 467 const videoMiniature = video.getMiniature()
468 if (!videoMiniature) {
469 logger.info('Cannot generate thumbnail for playlist %s because video %s does not have any.', videoPlaylist.url, video.url)
470 return
471 }
472
473 const inputPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, videoMiniature.filename)
468 const thumbnailModel = await createPlaylistMiniatureFromExisting(inputPath, videoPlaylist, true, true) 474 const thumbnailModel = await createPlaylistMiniatureFromExisting(inputPath, videoPlaylist, true, true)
469 475
470 thumbnailModel.videoPlaylistId = videoPlaylist.id 476 thumbnailModel.videoPlaylistId = videoPlaylist.id
diff --git a/server/controllers/api/videos/abuse.ts b/server/controllers/api/videos/abuse.ts
index 4ae899b7e..2af7b3864 100644
--- a/server/controllers/api/videos/abuse.ts
+++ b/server/controllers/api/videos/abuse.ts
@@ -1,8 +1,8 @@
1import * as express from 'express' 1import * as express from 'express'
2import { UserRight, VideoAbuseCreate, VideoAbuseState } from '../../../../shared' 2import { UserRight, VideoAbuseCreate, VideoAbuseState, VideoAbuse } from '../../../../shared'
3import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
4import { getFormattedObjects, getServerActor } from '../../../helpers/utils' 4import { getFormattedObjects } from '../../../helpers/utils'
5import { sequelizeTypescript } from '../../../initializers' 5import { sequelizeTypescript } from '../../../initializers/database'
6import { 6import {
7 asyncMiddleware, 7 asyncMiddleware,
8 asyncRetryTransactionMiddleware, 8 asyncRetryTransactionMiddleware,
@@ -14,7 +14,8 @@ import {
14 videoAbuseGetValidator, 14 videoAbuseGetValidator,
15 videoAbuseReportValidator, 15 videoAbuseReportValidator,
16 videoAbusesSortValidator, 16 videoAbusesSortValidator,
17 videoAbuseUpdateValidator 17 videoAbuseUpdateValidator,
18 videoAbuseListValidator
18} from '../../../middlewares' 19} from '../../../middlewares'
19import { AccountModel } from '../../../models/account/account' 20import { AccountModel } from '../../../models/account/account'
20import { VideoAbuseModel } from '../../../models/video/video-abuse' 21import { VideoAbuseModel } from '../../../models/video/video-abuse'
@@ -22,6 +23,8 @@ import { auditLoggerFactory, VideoAbuseAuditView } from '../../../helpers/audit-
22import { Notifier } from '../../../lib/notifier' 23import { Notifier } from '../../../lib/notifier'
23import { sendVideoAbuse } from '../../../lib/activitypub/send/send-flag' 24import { sendVideoAbuse } from '../../../lib/activitypub/send/send-flag'
24import { MVideoAbuseAccountVideo } from '../../../typings/models/video' 25import { MVideoAbuseAccountVideo } from '../../../typings/models/video'
26import { getServerActor } from '@server/models/application/application'
27import { MAccountDefault } from '@server/typings/models'
25 28
26const auditLogger = auditLoggerFactory('abuse') 29const auditLogger = auditLoggerFactory('abuse')
27const abuseVideoRouter = express.Router() 30const abuseVideoRouter = express.Router()
@@ -33,6 +36,7 @@ abuseVideoRouter.get('/abuse',
33 videoAbusesSortValidator, 36 videoAbusesSortValidator,
34 setDefaultSort, 37 setDefaultSort,
35 setDefaultPagination, 38 setDefaultPagination,
39 videoAbuseListValidator,
36 asyncMiddleware(listVideoAbuses) 40 asyncMiddleware(listVideoAbuses)
37) 41)
38abuseVideoRouter.put('/:videoId/abuse/:id', 42abuseVideoRouter.put('/:videoId/abuse/:id',
@@ -69,6 +73,14 @@ async function listVideoAbuses (req: express.Request, res: express.Response) {
69 start: req.query.start, 73 start: req.query.start,
70 count: req.query.count, 74 count: req.query.count,
71 sort: req.query.sort, 75 sort: req.query.sort,
76 id: req.query.id,
77 search: req.query.search,
78 state: req.query.state,
79 videoIs: req.query.videoIs,
80 searchReporter: req.query.searchReporter,
81 searchReportee: req.query.searchReportee,
82 searchVideo: req.query.searchVideo,
83 searchVideoChannel: req.query.searchVideoChannel,
72 serverAccountId: serverActor.Account.id, 84 serverAccountId: serverActor.Account.id,
73 user 85 user
74 }) 86 })
@@ -106,9 +118,11 @@ async function deleteVideoAbuse (req: express.Request, res: express.Response) {
106async function reportVideoAbuse (req: express.Request, res: express.Response) { 118async function reportVideoAbuse (req: express.Request, res: express.Response) {
107 const videoInstance = res.locals.videoAll 119 const videoInstance = res.locals.videoAll
108 const body: VideoAbuseCreate = req.body 120 const body: VideoAbuseCreate = req.body
121 let reporterAccount: MAccountDefault
122 let videoAbuseJSON: VideoAbuse
109 123
110 const videoAbuse = await sequelizeTypescript.transaction(async t => { 124 const videoAbuseInstance = await sequelizeTypescript.transaction(async t => {
111 const reporterAccount = await AccountModel.load(res.locals.oauth.token.User.Account.id, t) 125 reporterAccount = await AccountModel.load(res.locals.oauth.token.User.Account.id, t)
112 126
113 const abuseToCreate = { 127 const abuseToCreate = {
114 reporterAccountId: reporterAccount.id, 128 reporterAccountId: reporterAccount.id,
@@ -126,14 +140,19 @@ async function reportVideoAbuse (req: express.Request, res: express.Response) {
126 await sendVideoAbuse(reporterAccount.Actor, videoAbuseInstance, videoInstance, t) 140 await sendVideoAbuse(reporterAccount.Actor, videoAbuseInstance, videoInstance, t)
127 } 141 }
128 142
129 auditLogger.create(reporterAccount.Actor.getIdentifier(), new VideoAbuseAuditView(videoAbuseInstance.toFormattedJSON())) 143 videoAbuseJSON = videoAbuseInstance.toFormattedJSON()
144 auditLogger.create(reporterAccount.Actor.getIdentifier(), new VideoAbuseAuditView(videoAbuseJSON))
130 145
131 return videoAbuseInstance 146 return videoAbuseInstance
132 }) 147 })
133 148
134 Notifier.Instance.notifyOnNewVideoAbuse(videoAbuse) 149 Notifier.Instance.notifyOnNewVideoAbuse({
150 videoAbuse: videoAbuseJSON,
151 videoAbuseInstance,
152 reporter: reporterAccount.Actor.getIdentifier()
153 })
135 154
136 logger.info('Abuse report for video %s created.', videoInstance.name) 155 logger.info('Abuse report for video %s created.', videoInstance.name)
137 156
138 return res.json({ videoAbuse: videoAbuse.toFormattedJSON() }).end() 157 return res.json({ videoAbuse: videoAbuseJSON }).end()
139} 158}
diff --git a/server/controllers/api/videos/blacklist.ts b/server/controllers/api/videos/blacklist.ts
index 2a667480d..3b25ceea2 100644
--- a/server/controllers/api/videos/blacklist.ts
+++ b/server/controllers/api/videos/blacklist.ts
@@ -1,7 +1,9 @@
1import * as express from 'express' 1import * as express from 'express'
2import { UserRight, VideoBlacklistCreate, VideoBlacklistType } from '../../../../shared' 2import { blacklistVideo, unblacklistVideo } from '@server/lib/video-blacklist'
3import { UserRight, VideoBlacklistCreate } from '../../../../shared'
3import { logger } from '../../../helpers/logger' 4import { logger } from '../../../helpers/logger'
4import { getFormattedObjects } from '../../../helpers/utils' 5import { getFormattedObjects } from '../../../helpers/utils'
6import { sequelizeTypescript } from '../../../initializers/database'
5import { 7import {
6 asyncMiddleware, 8 asyncMiddleware,
7 authenticate, 9 authenticate,
@@ -16,11 +18,6 @@ import {
16 videosBlacklistUpdateValidator 18 videosBlacklistUpdateValidator
17} from '../../../middlewares' 19} from '../../../middlewares'
18import { VideoBlacklistModel } from '../../../models/video/video-blacklist' 20import { VideoBlacklistModel } from '../../../models/video/video-blacklist'
19import { sequelizeTypescript } from '../../../initializers'
20import { Notifier } from '../../../lib/notifier'
21import { sendDeleteVideo } from '../../../lib/activitypub/send'
22import { federateVideoIfNeeded } from '../../../lib/activitypub'
23import { MVideoBlacklistVideo } from '@server/typings/models'
24 21
25const blacklistRouter = express.Router() 22const blacklistRouter = express.Router()
26 23
@@ -28,7 +25,7 @@ blacklistRouter.post('/:videoId/blacklist',
28 authenticate, 25 authenticate,
29 ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST), 26 ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST),
30 asyncMiddleware(videosBlacklistAddValidator), 27 asyncMiddleware(videosBlacklistAddValidator),
31 asyncMiddleware(addVideoToBlacklist) 28 asyncMiddleware(addVideoToBlacklistController)
32) 29)
33 30
34blacklistRouter.get('/blacklist', 31blacklistRouter.get('/blacklist',
@@ -64,29 +61,15 @@ export {
64 61
65// --------------------------------------------------------------------------- 62// ---------------------------------------------------------------------------
66 63
67async function addVideoToBlacklist (req: express.Request, res: express.Response) { 64async function addVideoToBlacklistController (req: express.Request, res: express.Response) {
68 const videoInstance = res.locals.videoAll 65 const videoInstance = res.locals.videoAll
69 const body: VideoBlacklistCreate = req.body 66 const body: VideoBlacklistCreate = req.body
70 67
71 const toCreate = { 68 await blacklistVideo(videoInstance, body)
72 videoId: videoInstance.id,
73 unfederated: body.unfederate === true,
74 reason: body.reason,
75 type: VideoBlacklistType.MANUAL
76 }
77
78 const blacklist: MVideoBlacklistVideo = await VideoBlacklistModel.create(toCreate)
79 blacklist.Video = videoInstance
80
81 if (body.unfederate === true) {
82 await sendDeleteVideo(videoInstance, undefined)
83 }
84
85 Notifier.Instance.notifyOnVideoBlacklist(blacklist)
86 69
87 logger.info('Video %s blacklisted.', videoInstance.uuid) 70 logger.info('Video %s blacklisted.', videoInstance.uuid)
88 71
89 return res.type('json').status(204).end() 72 return res.type('json').sendStatus(204)
90} 73}
91 74
92async function updateVideoBlacklistController (req: express.Request, res: express.Response) { 75async function updateVideoBlacklistController (req: express.Request, res: express.Response) {
@@ -98,11 +81,17 @@ async function updateVideoBlacklistController (req: express.Request, res: expres
98 return videoBlacklist.save({ transaction: t }) 81 return videoBlacklist.save({ transaction: t })
99 }) 82 })
100 83
101 return res.type('json').status(204).end() 84 return res.type('json').sendStatus(204)
102} 85}
103 86
104async function listBlacklist (req: express.Request, res: express.Response) { 87async function listBlacklist (req: express.Request, res: express.Response) {
105 const resultList = await VideoBlacklistModel.listForApi(req.query.start, req.query.count, req.query.sort, req.query.type) 88 const resultList = await VideoBlacklistModel.listForApi({
89 start: req.query.start,
90 count: req.query.count,
91 sort: req.query.sort,
92 search: req.query.search,
93 type: req.query.type
94 })
106 95
107 return res.json(getFormattedObjects(resultList.data, resultList.total)) 96 return res.json(getFormattedObjects(resultList.data, resultList.total))
108} 97}
@@ -111,32 +100,9 @@ async function removeVideoFromBlacklistController (req: express.Request, res: ex
111 const videoBlacklist = res.locals.videoBlacklist 100 const videoBlacklist = res.locals.videoBlacklist
112 const video = res.locals.videoAll 101 const video = res.locals.videoAll
113 102
114 const videoBlacklistType = await sequelizeTypescript.transaction(async t => { 103 await unblacklistVideo(videoBlacklist, video)
115 const unfederated = videoBlacklist.unfederated
116 const videoBlacklistType = videoBlacklist.type
117
118 await videoBlacklist.destroy({ transaction: t })
119 video.VideoBlacklist = undefined
120
121 // Re federate the video
122 if (unfederated === true) {
123 await federateVideoIfNeeded(video, true, t)
124 }
125
126 return videoBlacklistType
127 })
128
129 Notifier.Instance.notifyOnVideoUnblacklist(video)
130
131 if (videoBlacklistType === VideoBlacklistType.AUTO_BEFORE_PUBLISHED) {
132 Notifier.Instance.notifyOnVideoPublishedAfterRemovedFromAutoBlacklist(video)
133
134 // Delete on object so new video notifications will send
135 delete video.VideoBlacklist
136 Notifier.Instance.notifyOnNewVideoIfNeeded(video)
137 }
138 104
139 logger.info('Video %s removed from blacklist.', video.uuid) 105 logger.info('Video %s removed from blacklist.', video.uuid)
140 106
141 return res.type('json').status(204).end() 107 return res.type('json').sendStatus(204)
142} 108}
diff --git a/server/controllers/api/videos/captions.ts b/server/controllers/api/videos/captions.ts
index 37481d12f..8c1d12ca8 100644
--- a/server/controllers/api/videos/captions.ts
+++ b/server/controllers/api/videos/captions.ts
@@ -6,7 +6,7 @@ import { 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 { logger } from '../../../helpers/logger' 8import { logger } from '../../../helpers/logger'
9import { federateVideoIfNeeded } from '../../../lib/activitypub' 9import { federateVideoIfNeeded } from '../../../lib/activitypub/videos'
10import { moveAndProcessCaptionFile } from '../../../helpers/captions-utils' 10import { moveAndProcessCaptionFile } from '../../../helpers/captions-utils'
11import { CONFIG } from '../../../initializers/config' 11import { CONFIG } from '../../../initializers/config'
12import { sequelizeTypescript } from '../../../initializers/database' 12import { sequelizeTypescript } from '../../../initializers/database'
@@ -66,7 +66,7 @@ async function addVideoCaption (req: express.Request, res: express.Response) {
66 await moveAndProcessCaptionFile(videoCaptionPhysicalFile, videoCaption) 66 await moveAndProcessCaptionFile(videoCaptionPhysicalFile, videoCaption)
67 67
68 await sequelizeTypescript.transaction(async t => { 68 await sequelizeTypescript.transaction(async t => {
69 await VideoCaptionModel.insertOrReplaceLanguage(video.id, req.params.captionLanguage, t) 69 await VideoCaptionModel.insertOrReplaceLanguage(video.id, req.params.captionLanguage, null, t)
70 70
71 // Update video update 71 // Update video update
72 await federateVideoIfNeeded(video, false, t) 72 await federateVideoIfNeeded(video, false, t)
diff --git a/server/controllers/api/videos/comment.ts b/server/controllers/api/videos/comment.ts
index 5f3fed5c0..5070bb3c0 100644
--- a/server/controllers/api/videos/comment.ts
+++ b/server/controllers/api/videos/comment.ts
@@ -4,7 +4,7 @@ import { ResultList } from '../../../../shared/models'
4import { VideoCommentCreate } from '../../../../shared/models/videos/video-comment.model' 4import { VideoCommentCreate } from '../../../../shared/models/videos/video-comment.model'
5import { logger } from '../../../helpers/logger' 5import { logger } from '../../../helpers/logger'
6import { getFormattedObjects } from '../../../helpers/utils' 6import { getFormattedObjects } from '../../../helpers/utils'
7import { sequelizeTypescript } from '../../../initializers' 7import { sequelizeTypescript } from '../../../initializers/database'
8import { buildFormattedCommentTree, createVideoComment, markCommentAsDeleted } from '../../../lib/video-comment' 8import { buildFormattedCommentTree, createVideoComment, markCommentAsDeleted } from '../../../lib/video-comment'
9import { 9import {
10 asyncMiddleware, 10 asyncMiddleware,
diff --git a/server/controllers/api/videos/import.ts b/server/controllers/api/videos/import.ts
index 28ced5836..b4f70a086 100644
--- a/server/controllers/api/videos/import.ts
+++ b/server/controllers/api/videos/import.ts
@@ -3,12 +3,14 @@ import * as magnetUtil from 'magnet-uri'
3import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger' 3import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger'
4import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoImportAddValidator } from '../../../middlewares' 4import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoImportAddValidator } from '../../../middlewares'
5import { MIMETYPES } from '../../../initializers/constants' 5import { MIMETYPES } from '../../../initializers/constants'
6import { getYoutubeDLInfo, YoutubeDLInfo } from '../../../helpers/youtube-dl' 6import { getYoutubeDLInfo, YoutubeDLInfo, getYoutubeDLSubs } from '../../../helpers/youtube-dl'
7import { createReqFiles } from '../../../helpers/express-utils' 7import { createReqFiles } from '../../../helpers/express-utils'
8import { logger } from '../../../helpers/logger' 8import { logger } from '../../../helpers/logger'
9import { VideoImportCreate, VideoImportState, VideoPrivacy, VideoState } from '../../../../shared' 9import { VideoImportCreate, VideoImportState, VideoPrivacy, VideoState } from '../../../../shared'
10import { VideoModel } from '../../../models/video/video' 10import { VideoModel } from '../../../models/video/video'
11import { getVideoActivityPubUrl } from '../../../lib/activitypub' 11import { VideoCaptionModel } from '../../../models/video/video-caption'
12import { moveAndProcessCaptionFile } from '../../../helpers/captions-utils'
13import { getVideoActivityPubUrl } from '../../../lib/activitypub/url'
12import { TagModel } from '../../../models/video/tag' 14import { TagModel } from '../../../models/video/tag'
13import { VideoImportModel } from '../../../models/video/video-import' 15import { VideoImportModel } from '../../../models/video/video-import'
14import { JobQueue } from '../../../lib/job-queue/job-queue' 16import { JobQueue } from '../../../lib/job-queue/job-queue'
@@ -21,13 +23,14 @@ import { move, readFile } from 'fs-extra'
21import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' 23import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
22import { CONFIG } from '../../../initializers/config' 24import { CONFIG } from '../../../initializers/config'
23import { sequelizeTypescript } from '../../../initializers/database' 25import { sequelizeTypescript } from '../../../initializers/database'
24import { createVideoMiniatureFromExisting } from '../../../lib/thumbnail' 26import { createVideoMiniatureFromExisting, createVideoMiniatureFromUrl } from '../../../lib/thumbnail'
25import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' 27import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
26import { 28import {
27 MChannelAccountDefault, 29 MChannelAccountDefault,
28 MThumbnail, 30 MThumbnail,
29 MUser, 31 MUser,
30 MVideoAccountDefault, 32 MVideoAccountDefault,
33 MVideoCaptionVideo,
31 MVideoTag, 34 MVideoTag,
32 MVideoThumbnailAccountDefault, 35 MVideoThumbnailAccountDefault,
33 MVideoWithBlacklistLight 36 MVideoWithBlacklistLight
@@ -88,12 +91,12 @@ async function addTorrentImport (req: express.Request, res: express.Response, to
88 const buf = await readFile(torrentfile.path) 91 const buf = await readFile(torrentfile.path)
89 const parsedTorrent = parseTorrent(buf) 92 const parsedTorrent = parseTorrent(buf)
90 93
91 videoName = isArray(parsedTorrent.name) ? parsedTorrent.name[ 0 ] : parsedTorrent.name as string 94 videoName = isArray(parsedTorrent.name) ? parsedTorrent.name[0] : parsedTorrent.name as string
92 } else { 95 } else {
93 magnetUri = body.magnetUri 96 magnetUri = body.magnetUri
94 97
95 const parsed = magnetUtil.decode(magnetUri) 98 const parsed = magnetUtil.decode(magnetUri)
96 videoName = isArray(parsed.name) ? parsed.name[ 0 ] : parsed.name as string 99 videoName = isArray(parsed.name) ? parsed.name[0] : parsed.name as string
97 } 100 }
98 101
99 const video = buildVideo(res.locals.videoChannel.id, body, { name: videoName }) 102 const video = buildVideo(res.locals.videoChannel.id, body, { name: videoName })
@@ -124,7 +127,7 @@ async function addTorrentImport (req: express.Request, res: express.Response, to
124 videoImportId: videoImport.id, 127 videoImportId: videoImport.id,
125 magnetUri 128 magnetUri
126 } 129 }
127 await JobQueue.Instance.createJob({ type: 'video-import', payload }) 130 await JobQueue.Instance.createJobWithPromise({ type: 'video-import', payload })
128 131
129 auditLogger.create(getAuditIdFromRes(res), new VideoImportAuditView(videoImport.toFormattedJSON())) 132 auditLogger.create(getAuditIdFromRes(res), new VideoImportAuditView(videoImport.toFormattedJSON()))
130 133
@@ -136,6 +139,7 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response)
136 const targetUrl = body.targetUrl 139 const targetUrl = body.targetUrl
137 const user = res.locals.oauth.token.User 140 const user = res.locals.oauth.token.User
138 141
142 // Get video infos
139 let youtubeDLInfo: YoutubeDLInfo 143 let youtubeDLInfo: YoutubeDLInfo
140 try { 144 try {
141 youtubeDLInfo = await getYoutubeDLInfo(targetUrl) 145 youtubeDLInfo = await getYoutubeDLInfo(targetUrl)
@@ -149,8 +153,25 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response)
149 153
150 const video = buildVideo(res.locals.videoChannel.id, body, youtubeDLInfo) 154 const video = buildVideo(res.locals.videoChannel.id, body, youtubeDLInfo)
151 155
152 const thumbnailModel = await processThumbnail(req, video) 156 let thumbnailModel: MThumbnail
153 const previewModel = await processPreview(req, video) 157
158 // Process video thumbnail from request.files
159 thumbnailModel = await processThumbnail(req, video)
160
161 // Process video thumbnail from url if processing from request.files failed
162 if (!thumbnailModel && youtubeDLInfo.thumbnailUrl) {
163 thumbnailModel = await processThumbnailFromUrl(youtubeDLInfo.thumbnailUrl, video)
164 }
165
166 let previewModel: MThumbnail
167
168 // Process video preview from request.files
169 previewModel = await processPreview(req, video)
170
171 // Process video preview from url if processing from request.files failed
172 if (!previewModel && youtubeDLInfo.thumbnailUrl) {
173 previewModel = await processPreviewFromUrl(youtubeDLInfo.thumbnailUrl, video)
174 }
154 175
155 const tags = body.tags || youtubeDLInfo.tags 176 const tags = body.tags || youtubeDLInfo.tags
156 const videoImportAttributes = { 177 const videoImportAttributes = {
@@ -168,15 +189,41 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response)
168 user 189 user
169 }) 190 })
170 191
192 // Get video subtitles
193 try {
194 const subtitles = await getYoutubeDLSubs(targetUrl)
195
196 logger.info('Will create %s subtitles from youtube import %s.', subtitles.length, targetUrl)
197
198 for (const subtitle of subtitles) {
199 const videoCaption = new VideoCaptionModel({
200 videoId: video.id,
201 language: subtitle.language
202 }) as MVideoCaptionVideo
203 videoCaption.Video = video
204
205 // Move physical file
206 await moveAndProcessCaptionFile(subtitle, videoCaption)
207
208 await sequelizeTypescript.transaction(async t => {
209 await VideoCaptionModel.insertOrReplaceLanguage(video.id, subtitle.language, null, t)
210 })
211 }
212 } catch (err) {
213 logger.warn('Cannot get video subtitles.', { err })
214 }
215
171 // Create job to import the video 216 // Create job to import the video
172 const payload = { 217 const payload = {
173 type: 'youtube-dl' as 'youtube-dl', 218 type: 'youtube-dl' as 'youtube-dl',
174 videoImportId: videoImport.id, 219 videoImportId: videoImport.id,
175 thumbnailUrl: youtubeDLInfo.thumbnailUrl, 220 generateThumbnail: !thumbnailModel,
176 downloadThumbnail: !thumbnailModel, 221 generatePreview: !previewModel,
177 downloadPreview: !previewModel 222 fileExt: youtubeDLInfo.fileExt
223 ? `.${youtubeDLInfo.fileExt}`
224 : '.mp4'
178 } 225 }
179 await JobQueue.Instance.createJob({ type: 'video-import', payload }) 226 await JobQueue.Instance.createJobWithPromise({ type: 'video-import', payload })
180 227
181 auditLogger.create(getAuditIdFromRes(res), new VideoImportAuditView(videoImport.toFormattedJSON())) 228 auditLogger.create(getAuditIdFromRes(res), new VideoImportAuditView(videoImport.toFormattedJSON()))
182 229
@@ -189,7 +236,7 @@ function buildVideo (channelId: number, body: VideoImportCreate, importData: You
189 remote: false, 236 remote: false,
190 category: body.category || importData.category, 237 category: body.category || importData.category,
191 licence: body.licence || importData.licence, 238 licence: body.licence || importData.licence,
192 language: body.language || undefined, 239 language: body.language || importData.language,
193 commentsEnabled: body.commentsEnabled !== false, // If the value is not "false", the default is "true" 240 commentsEnabled: body.commentsEnabled !== false, // If the value is not "false", the default is "true"
194 downloadEnabled: body.downloadEnabled !== false, 241 downloadEnabled: body.downloadEnabled !== false,
195 waitTranscoding: body.waitTranscoding || false, 242 waitTranscoding: body.waitTranscoding || false,
@@ -200,7 +247,7 @@ function buildVideo (channelId: number, body: VideoImportCreate, importData: You
200 privacy: body.privacy || VideoPrivacy.PRIVATE, 247 privacy: body.privacy || VideoPrivacy.PRIVATE,
201 duration: 0, // duration will be set by the import job 248 duration: 0, // duration will be set by the import job
202 channelId: channelId, 249 channelId: channelId,
203 originallyPublishedAt: importData.originallyPublishedAt 250 originallyPublishedAt: body.originallyPublishedAt || importData.originallyPublishedAt
204 } 251 }
205 const video = new VideoModel(videoData) 252 const video = new VideoModel(videoData)
206 video.url = getVideoActivityPubUrl(video) 253 video.url = getVideoActivityPubUrl(video)
@@ -211,7 +258,7 @@ function buildVideo (channelId: number, body: VideoImportCreate, importData: You
211async function processThumbnail (req: express.Request, video: VideoModel) { 258async function processThumbnail (req: express.Request, video: VideoModel) {
212 const thumbnailField = req.files ? req.files['thumbnailfile'] : undefined 259 const thumbnailField = req.files ? req.files['thumbnailfile'] : undefined
213 if (thumbnailField) { 260 if (thumbnailField) {
214 const thumbnailPhysicalFile = thumbnailField[ 0 ] 261 const thumbnailPhysicalFile = thumbnailField[0]
215 262
216 return createVideoMiniatureFromExisting(thumbnailPhysicalFile.path, video, ThumbnailType.MINIATURE, false) 263 return createVideoMiniatureFromExisting(thumbnailPhysicalFile.path, video, ThumbnailType.MINIATURE, false)
217 } 264 }
@@ -230,13 +277,31 @@ async function processPreview (req: express.Request, video: VideoModel) {
230 return undefined 277 return undefined
231} 278}
232 279
280async function processThumbnailFromUrl (url: string, video: VideoModel) {
281 try {
282 return createVideoMiniatureFromUrl(url, video, ThumbnailType.MINIATURE)
283 } catch (err) {
284 logger.warn('Cannot generate video thumbnail %s for %s.', url, video.url, { err })
285 return undefined
286 }
287}
288
289async function processPreviewFromUrl (url: string, video: VideoModel) {
290 try {
291 return createVideoMiniatureFromUrl(url, video, ThumbnailType.PREVIEW)
292 } catch (err) {
293 logger.warn('Cannot generate video preview %s for %s.', url, video.url, { err })
294 return undefined
295 }
296}
297
233function insertIntoDB (parameters: { 298function insertIntoDB (parameters: {
234 video: MVideoThumbnailAccountDefault, 299 video: MVideoThumbnailAccountDefault
235 thumbnailModel: MThumbnail, 300 thumbnailModel: MThumbnail
236 previewModel: MThumbnail, 301 previewModel: MThumbnail
237 videoChannel: MChannelAccountDefault, 302 videoChannel: MChannelAccountDefault
238 tags: string[], 303 tags: string[]
239 videoImportAttributes: Partial<MVideoImport>, 304 videoImportAttributes: Partial<MVideoImport>
240 user: MUser 305 user: MUser
241}): Bluebird<MVideoImportFormattable> { 306}): Bluebird<MVideoImportFormattable> {
242 const { video, thumbnailModel, previewModel, videoChannel, tags, videoImportAttributes, user } = parameters 307 const { video, thumbnailModel, previewModel, videoChannel, tags, videoImportAttributes, user } = parameters
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index 8d4ff07eb..8048c568c 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -1,10 +1,10 @@
1import * as express from 'express' 1import * as express from 'express'
2import { extname } from 'path' 2import { extname } from 'path'
3import { VideoCreate, VideoPrivacy, VideoState, VideoUpdate } from '../../../../shared' 3import { VideoCreate, VideoPrivacy, VideoState, VideoUpdate } from '../../../../shared'
4import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' 4import { getMetadataFromFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
5import { logger } from '../../../helpers/logger' 5import { logger } from '../../../helpers/logger'
6import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' 6import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
7import { getFormattedObjects, getServerActor } from '../../../helpers/utils' 7import { getFormattedObjects } from '../../../helpers/utils'
8import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' 8import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
9import { 9import {
10 DEFAULT_AUDIO_RESOLUTION, 10 DEFAULT_AUDIO_RESOLUTION,
@@ -14,12 +14,7 @@ import {
14 VIDEO_LICENCES, 14 VIDEO_LICENCES,
15 VIDEO_PRIVACIES 15 VIDEO_PRIVACIES
16} from '../../../initializers/constants' 16} from '../../../initializers/constants'
17import { 17import { federateVideoIfNeeded, fetchRemoteVideoDescription } from '../../../lib/activitypub/videos'
18 changeVideoChannelShare,
19 federateVideoIfNeeded,
20 fetchRemoteVideoDescription,
21 getVideoActivityPubUrl
22} from '../../../lib/activitypub'
23import { JobQueue } from '../../../lib/job-queue' 18import { JobQueue } from '../../../lib/job-queue'
24import { Redis } from '../../../lib/redis' 19import { Redis } from '../../../lib/redis'
25import { 20import {
@@ -32,6 +27,7 @@ import {
32 paginationValidator, 27 paginationValidator,
33 setDefaultPagination, 28 setDefaultPagination,
34 setDefaultSort, 29 setDefaultSort,
30 videoFileMetadataGetValidator,
35 videosAddValidator, 31 videosAddValidator,
36 videosCustomGetValidator, 32 videosCustomGetValidator,
37 videosGetValidator, 33 videosGetValidator,
@@ -61,11 +57,15 @@ import { CONFIG } from '../../../initializers/config'
61import { sequelizeTypescript } from '../../../initializers/database' 57import { sequelizeTypescript } from '../../../initializers/database'
62import { createVideoMiniatureFromExisting, generateVideoMiniature } from '../../../lib/thumbnail' 58import { createVideoMiniatureFromExisting, generateVideoMiniature } from '../../../lib/thumbnail'
63import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' 59import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
64import { VideoTranscodingPayload } from '../../../lib/job-queue/handlers/video-transcoding'
65import { Hooks } from '../../../lib/plugins/hooks' 60import { Hooks } from '../../../lib/plugins/hooks'
66import { MVideoDetails, MVideoFullLight } from '@server/typings/models' 61import { MVideoDetails, MVideoFullLight } from '@server/typings/models'
67import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' 62import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
68import { getVideoFilePath } from '@server/lib/video-paths' 63import { getVideoFilePath } from '@server/lib/video-paths'
64import toInt from 'validator/lib/toInt'
65import { addOptimizeOrMergeAudioJob } from '@server/helpers/video'
66import { getServerActor } from '@server/models/application/application'
67import { changeVideoChannelShare } from '@server/lib/activitypub/share'
68import { getVideoActivityPubUrl } from '@server/lib/activitypub/url'
69 69
70const auditLogger = auditLoggerFactory('videos') 70const auditLogger = auditLoggerFactory('videos')
71const videosRouter = express.Router() 71const videosRouter = express.Router()
@@ -128,6 +128,10 @@ videosRouter.get('/:id/description',
128 asyncMiddleware(videosGetValidator), 128 asyncMiddleware(videosGetValidator),
129 asyncMiddleware(getVideoDescription) 129 asyncMiddleware(getVideoDescription)
130) 130)
131videosRouter.get('/:id/metadata/:videoFileId',
132 asyncMiddleware(videoFileMetadataGetValidator),
133 asyncMiddleware(getVideoFileMetadata)
134)
131videosRouter.get('/:id', 135videosRouter.get('/:id',
132 optionalAuthenticate, 136 optionalAuthenticate,
133 asyncMiddleware(videosCustomGetValidator('only-video-with-rights')), 137 asyncMiddleware(videosCustomGetValidator('only-video-with-rights')),
@@ -135,7 +139,7 @@ videosRouter.get('/:id',
135 asyncMiddleware(getVideo) 139 asyncMiddleware(getVideo)
136) 140)
137videosRouter.post('/:id/views', 141videosRouter.post('/:id/views',
138 asyncMiddleware(videosGetValidator), 142 asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')),
139 asyncMiddleware(viewVideo) 143 asyncMiddleware(viewVideo)
140) 144)
141 145
@@ -206,7 +210,8 @@ async function addVideo (req: express.Request, res: express.Response) {
206 const videoFile = new VideoFileModel({ 210 const videoFile = new VideoFileModel({
207 extname: extname(videoPhysicalFile.filename), 211 extname: extname(videoPhysicalFile.filename),
208 size: videoPhysicalFile.size, 212 size: videoPhysicalFile.size,
209 videoStreamingPlaylistId: null 213 videoStreamingPlaylistId: null,
214 metadata: await getMetadataFromFile<any>(videoPhysicalFile.path)
210 }) 215 })
211 216
212 if (videoFile.isAudio()) { 217 if (videoFile.isAudio()) {
@@ -289,25 +294,7 @@ async function addVideo (req: express.Request, res: express.Response) {
289 Notifier.Instance.notifyOnNewVideoIfNeeded(videoCreated) 294 Notifier.Instance.notifyOnNewVideoIfNeeded(videoCreated)
290 295
291 if (video.state === VideoState.TO_TRANSCODE) { 296 if (video.state === VideoState.TO_TRANSCODE) {
292 // Put uuid because we don't have id auto incremented for now 297 await addOptimizeOrMergeAudioJob(videoCreated, videoFile)
293 let dataInput: VideoTranscodingPayload
294
295 if (videoFile.isAudio()) {
296 dataInput = {
297 type: 'merge-audio' as 'merge-audio',
298 resolution: DEFAULT_AUDIO_RESOLUTION,
299 videoUUID: videoCreated.uuid,
300 isNewVideo: true
301 }
302 } else {
303 dataInput = {
304 type: 'optimize' as 'optimize',
305 videoUUID: videoCreated.uuid,
306 isNewVideo: true
307 }
308 }
309
310 await JobQueue.Instance.createJob({ type: 'video-transcoding', payload: dataInput })
311 } 298 }
312 299
313 Hooks.runAction('action:api.video.uploaded', { video: videoCreated }) 300 Hooks.runAction('action:api.video.uploaded', { video: videoCreated })
@@ -452,14 +439,13 @@ async function getVideo (req: express.Request, res: express.Response) {
452 439
453 if (video.isOutdated()) { 440 if (video.isOutdated()) {
454 JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', url: video.url } }) 441 JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', url: video.url } })
455 .catch(err => logger.error('Cannot create AP refresher job for video %s.', video.url, { err }))
456 } 442 }
457 443
458 return res.json(video.toFormattedDetailsJSON()) 444 return res.json(video.toFormattedDetailsJSON())
459} 445}
460 446
461async function viewVideo (req: express.Request, res: express.Response) { 447async function viewVideo (req: express.Request, res: express.Response) {
462 const videoInstance = res.locals.videoAll 448 const videoInstance = res.locals.onlyImmutableVideo
463 449
464 const ip = req.ip 450 const ip = req.ip
465 const exists = await Redis.Instance.doesVideoIPViewExist(ip, videoInstance.uuid) 451 const exists = await Redis.Instance.doesVideoIPViewExist(ip, videoInstance.uuid)
@@ -494,6 +480,11 @@ async function getVideoDescription (req: express.Request, res: express.Response)
494 return res.json({ description }) 480 return res.json({ description })
495} 481}
496 482
483async function getVideoFileMetadata (req: express.Request, res: express.Response) {
484 const videoFile = await VideoFileModel.loadWithMetadata(toInt(req.params.videoFileId))
485 return res.json(videoFile.metadata)
486}
487
497async function listVideos (req: express.Request, res: express.Response) { 488async function listVideos (req: express.Request, res: express.Response) {
498 const countVideos = getCountVideos(req) 489 const countVideos = getCountVideos(req)
499 490
diff --git a/server/controllers/api/videos/ownership.ts b/server/controllers/api/videos/ownership.ts
index 41d7cdc43..540a49010 100644
--- a/server/controllers/api/videos/ownership.ts
+++ b/server/controllers/api/videos/ownership.ts
@@ -1,6 +1,6 @@
1import * as express from 'express' 1import * as express from 'express'
2import { logger } from '../../../helpers/logger' 2import { logger } from '../../../helpers/logger'
3import { sequelizeTypescript } from '../../../initializers' 3import { sequelizeTypescript } from '../../../initializers/database'
4import { 4import {
5 asyncMiddleware, 5 asyncMiddleware,
6 asyncRetryTransactionMiddleware, 6 asyncRetryTransactionMiddleware,
@@ -15,7 +15,7 @@ import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ow
15import { VideoChangeOwnershipStatus, VideoState } from '../../../../shared/models/videos' 15import { VideoChangeOwnershipStatus, VideoState } from '../../../../shared/models/videos'
16import { VideoChannelModel } from '../../../models/video/video-channel' 16import { VideoChannelModel } from '../../../models/video/video-channel'
17import { getFormattedObjects } from '../../../helpers/utils' 17import { getFormattedObjects } from '../../../helpers/utils'
18import { changeVideoChannelShare } from '../../../lib/activitypub' 18import { changeVideoChannelShare } from '../../../lib/activitypub/share'
19import { sendUpdateVideo } from '../../../lib/activitypub/send' 19import { sendUpdateVideo } from '../../../lib/activitypub/send'
20import { VideoModel } from '../../../models/video/video' 20import { VideoModel } from '../../../models/video/video'
21import { MVideoFullLight } from '@server/typings/models' 21import { MVideoFullLight } from '@server/typings/models'
diff --git a/server/controllers/api/videos/rate.ts b/server/controllers/api/videos/rate.ts
index 3d2f3d728..3ee365289 100644
--- a/server/controllers/api/videos/rate.ts
+++ b/server/controllers/api/videos/rate.ts
@@ -2,7 +2,7 @@ import * as express from 'express'
2import { UserVideoRateUpdate } from '../../../../shared' 2import { UserVideoRateUpdate } from '../../../../shared'
3import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
4import { VIDEO_RATE_TYPES } from '../../../initializers/constants' 4import { VIDEO_RATE_TYPES } from '../../../initializers/constants'
5import { getRateUrl, sendVideoRateChange } from '../../../lib/activitypub' 5import { getRateUrl, sendVideoRateChange } from '../../../lib/activitypub/video-rates'
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'
diff --git a/server/controllers/bots.ts b/server/controllers/bots.ts
index 63280dabb..8d1fa72f3 100644
--- a/server/controllers/bots.ts
+++ b/server/controllers/bots.ts
@@ -1,7 +1,7 @@
1import * as express from 'express' 1import * as express from 'express'
2import { asyncMiddleware } from '../middlewares' 2import { asyncMiddleware } from '../middlewares'
3import { ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants' 3import { ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants'
4import * as sitemapModule from 'sitemap' 4import { SitemapStream, streamToPromise } from 'sitemap'
5import { VideoModel } from '../models/video/video' 5import { VideoModel } from '../models/video/video'
6import { VideoChannelModel } from '../models/video/video-channel' 6import { VideoChannelModel } from '../models/video/video-channel'
7import { AccountModel } from '../models/account/account' 7import { AccountModel } from '../models/account/account'
@@ -33,12 +33,14 @@ async function getSitemap (req: express.Request, res: express.Response) {
33 urls = urls.concat(await getSitemapVideoChannelUrls()) 33 urls = urls.concat(await getSitemapVideoChannelUrls())
34 urls = urls.concat(await getSitemapAccountUrls()) 34 urls = urls.concat(await getSitemapAccountUrls())
35 35
36 const sitemap = sitemapModule.createSitemap({ 36 const sitemapStream = new SitemapStream({ hostname: WEBSERVER.URL })
37 hostname: WEBSERVER.URL, 37
38 urls: urls 38 for (const urlObj of urls) {
39 }) 39 sitemapStream.write(urlObj)
40 }
41 sitemapStream.end()
40 42
41 const xml = sitemap.toXML() 43 const xml = await streamToPromise(sitemapStream)
42 44
43 res.header('Content-Type', 'application/xml') 45 res.header('Content-Type', 'application/xml')
44 res.send(xml) 46 res.send(xml)
diff --git a/server/controllers/client.ts b/server/controllers/client.ts
index 56685f102..08c0f1fa6 100644
--- a/server/controllers/client.ts
+++ b/server/controllers/client.ts
@@ -72,11 +72,11 @@ export {
72 72
73// --------------------------------------------------------------------------- 73// ---------------------------------------------------------------------------
74 74
75async function serveServerTranslations (req: express.Request, res: express.Response) { 75function serveServerTranslations (req: express.Request, res: express.Response) {
76 const locale = req.params.locale 76 const locale = req.params.locale
77 const file = req.params.file 77 const file = req.params.file
78 78
79 if (is18nLocale(locale) && LOCALE_FILES.indexOf(file) !== -1) { 79 if (is18nLocale(locale) && LOCALE_FILES.includes(file)) {
80 const completeLocale = getCompleteLocale(locale) 80 const completeLocale = getCompleteLocale(locale)
81 const completeFileLocale = buildFileLocale(completeLocale) 81 const completeFileLocale = buildFileLocale(completeLocale)
82 82
diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts
index 72628dffb..cb82bfc6d 100644
--- a/server/controllers/feeds.ts
+++ b/server/controllers/feeds.ts
@@ -67,7 +67,7 @@ async function generateVideoCommentsFeed (req: express.Request, res: express.Res
67 const feed = initFeed(name, description) 67 const feed = initFeed(name, description)
68 68
69 // Adding video items to the feed, one at a time 69 // Adding video items to the feed, one at a time
70 comments.forEach(comment => { 70 for (const comment of comments) {
71 const link = WEBSERVER.URL + comment.getCommentStaticPath() 71 const link = WEBSERVER.URL + comment.getCommentStaticPath()
72 72
73 let title = comment.Video.name 73 let title = comment.Video.name
@@ -89,7 +89,7 @@ async function generateVideoCommentsFeed (req: express.Request, res: express.Res
89 author, 89 author,
90 date: comment.createdAt 90 date: comment.createdAt
91 }) 91 })
92 }) 92 }
93 93
94 // Now the feed generation is done, let's send it! 94 // Now the feed generation is done, let's send it!
95 return sendFeed(feed, req, res) 95 return sendFeed(feed, req, res)
diff --git a/server/controllers/plugins.ts b/server/controllers/plugins.ts
index 1caee9a29..f88a1632d 100644
--- a/server/controllers/plugins.ts
+++ b/server/controllers/plugins.ts
@@ -2,11 +2,12 @@ import * as express from 'express'
2import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants' 2import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants'
3import { join } from 'path' 3import { join } from 'path'
4import { PluginManager, RegisteredPlugin } from '../lib/plugins/plugin-manager' 4import { PluginManager, RegisteredPlugin } from '../lib/plugins/plugin-manager'
5import { servePluginStaticDirectoryValidator } from '../middlewares/validators/plugins' 5import { getPluginValidator, pluginStaticDirectoryValidator, getExternalAuthValidator } from '../middlewares/validators/plugins'
6import { serveThemeCSSValidator } from '../middlewares/validators/themes' 6import { serveThemeCSSValidator } from '../middlewares/validators/themes'
7import { PluginType } from '../../shared/models/plugins/plugin.type' 7import { PluginType } from '../../shared/models/plugins/plugin.type'
8import { isTestInstance } from '../helpers/core-utils' 8import { isTestInstance } from '../helpers/core-utils'
9import { getCompleteLocale, is18nLocale } from '../../shared/models/i18n' 9import { getCompleteLocale, is18nLocale } from '../../shared/models/i18n'
10import { logger } from '@server/helpers/logger'
10 11
11const sendFileOptions = { 12const sendFileOptions = {
12 maxAge: '30 days', 13 maxAge: '30 days',
@@ -23,23 +24,43 @@ pluginsRouter.get('/plugins/translations/:locale.json',
23 getPluginTranslations 24 getPluginTranslations
24) 25)
25 26
27pluginsRouter.get('/plugins/:pluginName/:pluginVersion/auth/:authName',
28 getPluginValidator(PluginType.PLUGIN),
29 getExternalAuthValidator,
30 handleAuthInPlugin
31)
32
26pluginsRouter.get('/plugins/:pluginName/:pluginVersion/static/:staticEndpoint(*)', 33pluginsRouter.get('/plugins/:pluginName/:pluginVersion/static/:staticEndpoint(*)',
27 servePluginStaticDirectoryValidator(PluginType.PLUGIN), 34 getPluginValidator(PluginType.PLUGIN),
35 pluginStaticDirectoryValidator,
28 servePluginStaticDirectory 36 servePluginStaticDirectory
29) 37)
30 38
31pluginsRouter.get('/plugins/:pluginName/:pluginVersion/client-scripts/:staticEndpoint(*)', 39pluginsRouter.get('/plugins/:pluginName/:pluginVersion/client-scripts/:staticEndpoint(*)',
32 servePluginStaticDirectoryValidator(PluginType.PLUGIN), 40 getPluginValidator(PluginType.PLUGIN),
41 pluginStaticDirectoryValidator,
33 servePluginClientScripts 42 servePluginClientScripts
34) 43)
35 44
45pluginsRouter.use('/plugins/:pluginName/router',
46 getPluginValidator(PluginType.PLUGIN, false),
47 servePluginCustomRoutes
48)
49
50pluginsRouter.use('/plugins/:pluginName/:pluginVersion/router',
51 getPluginValidator(PluginType.PLUGIN),
52 servePluginCustomRoutes
53)
54
36pluginsRouter.get('/themes/:pluginName/:pluginVersion/static/:staticEndpoint(*)', 55pluginsRouter.get('/themes/:pluginName/:pluginVersion/static/:staticEndpoint(*)',
37 servePluginStaticDirectoryValidator(PluginType.THEME), 56 getPluginValidator(PluginType.THEME),
57 pluginStaticDirectoryValidator,
38 servePluginStaticDirectory 58 servePluginStaticDirectory
39) 59)
40 60
41pluginsRouter.get('/themes/:pluginName/:pluginVersion/client-scripts/:staticEndpoint(*)', 61pluginsRouter.get('/themes/:pluginName/:pluginVersion/client-scripts/:staticEndpoint(*)',
42 servePluginStaticDirectoryValidator(PluginType.THEME), 62 getPluginValidator(PluginType.THEME),
63 pluginStaticDirectoryValidator,
43 servePluginClientScripts 64 servePluginClientScripts
44) 65)
45 66
@@ -85,22 +106,27 @@ function servePluginStaticDirectory (req: express.Request, res: express.Response
85 const [ directory, ...file ] = staticEndpoint.split('/') 106 const [ directory, ...file ] = staticEndpoint.split('/')
86 107
87 const staticPath = plugin.staticDirs[directory] 108 const staticPath = plugin.staticDirs[directory]
88 if (!staticPath) { 109 if (!staticPath) return res.sendStatus(404)
89 return res.sendStatus(404)
90 }
91 110
92 const filepath = file.join('/') 111 const filepath = file.join('/')
93 return res.sendFile(join(plugin.path, staticPath, filepath), sendFileOptions) 112 return res.sendFile(join(plugin.path, staticPath, filepath), sendFileOptions)
94} 113}
95 114
115function servePluginCustomRoutes (req: express.Request, res: express.Response, next: express.NextFunction) {
116 const plugin: RegisteredPlugin = res.locals.registeredPlugin
117 const router = PluginManager.Instance.getRouter(plugin.npmName)
118
119 if (!router) return res.sendStatus(404)
120
121 return router(req, res, next)
122}
123
96function servePluginClientScripts (req: express.Request, res: express.Response) { 124function servePluginClientScripts (req: express.Request, res: express.Response) {
97 const plugin: RegisteredPlugin = res.locals.registeredPlugin 125 const plugin: RegisteredPlugin = res.locals.registeredPlugin
98 const staticEndpoint = req.params.staticEndpoint 126 const staticEndpoint = req.params.staticEndpoint
99 127
100 const file = plugin.clientScripts[staticEndpoint] 128 const file = plugin.clientScripts[staticEndpoint]
101 if (!file) { 129 if (!file) return res.sendStatus(404)
102 return res.sendStatus(404)
103 }
104 130
105 return res.sendFile(join(plugin.path, staticEndpoint), sendFileOptions) 131 return res.sendFile(join(plugin.path, staticEndpoint), sendFileOptions)
106} 132}
@@ -115,3 +141,14 @@ function serveThemeCSSDirectory (req: express.Request, res: express.Response) {
115 141
116 return res.sendFile(join(plugin.path, staticEndpoint), sendFileOptions) 142 return res.sendFile(join(plugin.path, staticEndpoint), sendFileOptions)
117} 143}
144
145function handleAuthInPlugin (req: express.Request, res: express.Response) {
146 const authOptions = res.locals.externalAuth
147
148 try {
149 logger.debug('Forwarding auth plugin request in %s of plugin %s.', authOptions.authName, res.locals.registeredPlugin.npmName)
150 authOptions.onAuthRequest(req, res)
151 } catch (err) {
152 logger.error('Forward request error in auth %s of plugin %s.', authOptions.authName, res.locals.registeredPlugin.npmName, { err })
153 }
154}
diff --git a/server/controllers/static.ts b/server/controllers/static.ts
index a4bb3a4d9..271b788f6 100644
--- a/server/controllers/static.ts
+++ b/server/controllers/static.ts
@@ -1,15 +1,15 @@
1import * as cors from 'cors' 1import * as cors from 'cors'
2import * as express from 'express' 2import * as express from 'express'
3import { 3import {
4 CONSTRAINTS_FIELDS,
5 DEFAULT_THEME_NAME,
4 HLS_STREAMING_PLAYLIST_DIRECTORY, 6 HLS_STREAMING_PLAYLIST_DIRECTORY,
5 PEERTUBE_VERSION, 7 PEERTUBE_VERSION,
6 ROUTE_CACHE_LIFETIME, 8 ROUTE_CACHE_LIFETIME,
7 STATIC_DOWNLOAD_PATHS, 9 STATIC_DOWNLOAD_PATHS,
8 STATIC_MAX_AGE, 10 STATIC_MAX_AGE,
9 STATIC_PATHS, 11 STATIC_PATHS,
10 WEBSERVER, 12 WEBSERVER
11 CONSTRAINTS_FIELDS,
12 DEFAULT_THEME_NAME
13} from '../initializers/constants' 13} from '../initializers/constants'
14import { cacheRoute } from '../middlewares/cache' 14import { cacheRoute } from '../middlewares/cache'
15import { asyncMiddleware, videosDownloadValidator } from '../middlewares' 15import { asyncMiddleware, videosDownloadValidator } from '../middlewares'
@@ -19,8 +19,7 @@ import { VideoCommentModel } from '../models/video/video-comment'
19import { HttpNodeinfoDiasporaSoftwareNsSchema20 } from '../../shared/models/nodeinfo' 19import { HttpNodeinfoDiasporaSoftwareNsSchema20 } from '../../shared/models/nodeinfo'
20import { join } from 'path' 20import { join } from 'path'
21import { root } from '../helpers/core-utils' 21import { root } from '../helpers/core-utils'
22import { CONFIG } from '../initializers/config' 22import { CONFIG, isEmailEnabled } from '../initializers/config'
23import { Emailer } from '../lib/emailer'
24import { getPreview, getVideoCaption } from './lazy-static' 23import { getPreview, getVideoCaption } from './lazy-static'
25import { VideoStreamingPlaylistType } from '@shared/models/videos/video-streaming-playlist.type' 24import { VideoStreamingPlaylistType } from '@shared/models/videos/video-streaming-playlist.type'
26import { MVideoFile, MVideoFullLight } from '@server/typings/models' 25import { MVideoFile, MVideoFullLight } from '@server/typings/models'
@@ -45,12 +44,12 @@ staticRouter.use(
45staticRouter.use( 44staticRouter.use(
46 STATIC_DOWNLOAD_PATHS.TORRENTS + ':id-:resolution([0-9]+).torrent', 45 STATIC_DOWNLOAD_PATHS.TORRENTS + ':id-:resolution([0-9]+).torrent',
47 asyncMiddleware(videosDownloadValidator), 46 asyncMiddleware(videosDownloadValidator),
48 asyncMiddleware(downloadTorrent) 47 downloadTorrent
49) 48)
50staticRouter.use( 49staticRouter.use(
51 STATIC_DOWNLOAD_PATHS.TORRENTS + ':id-:resolution([0-9]+)-hls.torrent', 50 STATIC_DOWNLOAD_PATHS.TORRENTS + ':id-:resolution([0-9]+)-hls.torrent',
52 asyncMiddleware(videosDownloadValidator), 51 asyncMiddleware(videosDownloadValidator),
53 asyncMiddleware(downloadHLSVideoFileTorrent) 52 downloadHLSVideoFileTorrent
54) 53)
55 54
56// Videos path for webseeding 55// Videos path for webseeding
@@ -68,13 +67,13 @@ staticRouter.use(
68staticRouter.use( 67staticRouter.use(
69 STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension', 68 STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension',
70 asyncMiddleware(videosDownloadValidator), 69 asyncMiddleware(videosDownloadValidator),
71 asyncMiddleware(downloadVideoFile) 70 downloadVideoFile
72) 71)
73 72
74staticRouter.use( 73staticRouter.use(
75 STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + ':id-:resolution([0-9]+)-fragmented.:extension', 74 STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + ':id-:resolution([0-9]+)-fragmented.:extension',
76 asyncMiddleware(videosDownloadValidator), 75 asyncMiddleware(videosDownloadValidator),
77 asyncMiddleware(downloadHLSVideoFile) 76 downloadHLSVideoFile
78) 77)
79 78
80// HLS 79// HLS
@@ -235,6 +234,12 @@ async function generateNodeinfo (req: express.Request, res: express.Response) {
235 nodeName: CONFIG.INSTANCE.NAME, 234 nodeName: CONFIG.INSTANCE.NAME,
236 nodeDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION, 235 nodeDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
237 nodeConfig: { 236 nodeConfig: {
237 search: {
238 remoteUri: {
239 users: CONFIG.SEARCH.REMOTE_URI.USERS,
240 anonymous: CONFIG.SEARCH.REMOTE_URI.ANONYMOUS
241 }
242 },
238 plugin: { 243 plugin: {
239 registered: getRegisteredPlugins() 244 registered: getRegisteredPlugins()
240 }, 245 },
@@ -243,7 +248,7 @@ async function generateNodeinfo (req: express.Request, res: express.Response) {
243 default: getThemeOrDefault(CONFIG.THEME.DEFAULT, DEFAULT_THEME_NAME) 248 default: getThemeOrDefault(CONFIG.THEME.DEFAULT, DEFAULT_THEME_NAME)
244 }, 249 },
245 email: { 250 email: {
246 enabled: Emailer.isEnabled() 251 enabled: isEmailEnabled()
247 }, 252 },
248 contactForm: { 253 contactForm: {
249 enabled: CONFIG.CONTACT_FORM.ENABLED 254 enabled: CONFIG.CONTACT_FORM.ENABLED
@@ -325,7 +330,7 @@ async function generateNodeinfo (req: express.Request, res: express.Response) {
325 return res.send(json).end() 330 return res.send(json).end()
326} 331}
327 332
328async function downloadTorrent (req: express.Request, res: express.Response) { 333function downloadTorrent (req: express.Request, res: express.Response) {
329 const video = res.locals.videoAll 334 const video = res.locals.videoAll
330 335
331 const videoFile = getVideoFile(req, video.VideoFiles) 336 const videoFile = getVideoFile(req, video.VideoFiles)
@@ -334,7 +339,7 @@ async function downloadTorrent (req: express.Request, res: express.Response) {
334 return res.download(getTorrentFilePath(video, videoFile), `${video.name}-${videoFile.resolution}p.torrent`) 339 return res.download(getTorrentFilePath(video, videoFile), `${video.name}-${videoFile.resolution}p.torrent`)
335} 340}
336 341
337async function downloadHLSVideoFileTorrent (req: express.Request, res: express.Response) { 342function downloadHLSVideoFileTorrent (req: express.Request, res: express.Response) {
338 const video = res.locals.videoAll 343 const video = res.locals.videoAll
339 344
340 const playlist = getHLSPlaylist(video) 345 const playlist = getHLSPlaylist(video)
@@ -346,7 +351,7 @@ async function downloadHLSVideoFileTorrent (req: express.Request, res: express.R
346 return res.download(getTorrentFilePath(playlist, videoFile), `${video.name}-${videoFile.resolution}p-hls.torrent`) 351 return res.download(getTorrentFilePath(playlist, videoFile), `${video.name}-${videoFile.resolution}p-hls.torrent`)
347} 352}
348 353
349async function downloadVideoFile (req: express.Request, res: express.Response) { 354function downloadVideoFile (req: express.Request, res: express.Response) {
350 const video = res.locals.videoAll 355 const video = res.locals.videoAll
351 356
352 const videoFile = getVideoFile(req, video.VideoFiles) 357 const videoFile = getVideoFile(req, video.VideoFiles)
@@ -355,7 +360,7 @@ async function downloadVideoFile (req: express.Request, res: express.Response) {
355 return res.download(getVideoFilePath(video, videoFile), `${video.name}-${videoFile.resolution}p${videoFile.extname}`) 360 return res.download(getVideoFilePath(video, videoFile), `${video.name}-${videoFile.resolution}p${videoFile.extname}`)
356} 361}
357 362
358async function downloadHLSVideoFile (req: express.Request, res: express.Response) { 363function downloadHLSVideoFile (req: express.Request, res: express.Response) {
359 const video = res.locals.videoAll 364 const video = res.locals.videoAll
360 const playlist = getHLSPlaylist(video) 365 const playlist = getHLSPlaylist(video)
361 if (!playlist) return res.status(404).end 366 if (!playlist) return res.status(404).end
diff --git a/server/controllers/tracker.ts b/server/controllers/tracker.ts
index 2ae1cf86c..4f756fc0a 100644
--- a/server/controllers/tracker.ts
+++ b/server/controllers/tracker.ts
@@ -6,7 +6,6 @@ import * as proxyAddr from 'proxy-addr'
6import { Server as WebSocketServer } from 'ws' 6import { Server as WebSocketServer } from 'ws'
7import { 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'
10import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' 9import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
11import { CONFIG } from '../initializers/config' 10import { CONFIG } from '../initializers/config'
12 11
@@ -38,11 +37,11 @@ const trackerServer = new TrackerServer({
38 37
39 const key = ip + '-' + infoHash 38 const key = ip + '-' + infoHash
40 39
41 peersIps[ ip ] = peersIps[ ip ] ? peersIps[ ip ] + 1 : 1 40 peersIps[ip] = peersIps[ip] ? peersIps[ip] + 1 : 1
42 peersIpInfoHash[ key ] = peersIpInfoHash[ key ] ? peersIpInfoHash[ key ] + 1 : 1 41 peersIpInfoHash[key] = peersIpInfoHash[key] ? peersIpInfoHash[key] + 1 : 1
43 42
44 if (CONFIG.TRACKER.REJECT_TOO_MANY_ANNOUNCES && peersIpInfoHash[ key ] > TRACKER_RATE_LIMITS.ANNOUNCES_PER_IP_PER_INFOHASH) { 43 if (CONFIG.TRACKER.REJECT_TOO_MANY_ANNOUNCES && peersIpInfoHash[key] > TRACKER_RATE_LIMITS.ANNOUNCES_PER_IP_PER_INFOHASH) {
45 return cb(new Error(`Too many requests (${peersIpInfoHash[ key ]} of ip ${ip} for torrent ${infoHash}`)) 44 return cb(new Error(`Too many requests (${peersIpInfoHash[key]} of ip ${ip} for torrent ${infoHash}`))
46 } 45 }
47 46
48 try { 47 try {
@@ -87,10 +86,8 @@ function createWebsocketTrackerServer (app: express.Application) {
87 trackerServer.onWebSocketConnection(ws) 86 trackerServer.onWebSocketConnection(ws)
88 }) 87 })
89 88
90 server.on('upgrade', (request, socket, head) => { 89 server.on('upgrade', (request: express.Request, socket, head) => {
91 const pathname = parse(request.url).pathname 90 if (request.url === '/tracker/socket') {
92
93 if (pathname === '/tracker/socket') {
94 wss.handleUpgrade(request, socket, head, ws => wss.emit('connection', ws, request)) 91 wss.handleUpgrade(request, socket, head, ws => wss.emit('connection', ws, request))
95 } 92 }
96 93
diff --git a/server/controllers/webfinger.ts b/server/controllers/webfinger.ts
index fc9575160..5c308d9ad 100644
--- a/server/controllers/webfinger.ts
+++ b/server/controllers/webfinger.ts
@@ -1,9 +1,12 @@
1import * as cors from 'cors'
1import * as express from 'express' 2import * as express from 'express'
2import { asyncMiddleware } from '../middlewares' 3import { asyncMiddleware } from '../middlewares'
3import { webfingerValidator } from '../middlewares/validators' 4import { webfingerValidator } from '../middlewares/validators'
4 5
5const webfingerRouter = express.Router() 6const webfingerRouter = express.Router()
6 7
8webfingerRouter.use(cors())
9
7webfingerRouter.get('/.well-known/webfinger', 10webfingerRouter.get('/.well-known/webfinger',
8 asyncMiddleware(webfingerValidator), 11 asyncMiddleware(webfingerValidator),
9 webfingerController 12 webfingerController
@@ -18,7 +21,7 @@ export {
18// --------------------------------------------------------------------------- 21// ---------------------------------------------------------------------------
19 22
20function webfingerController (req: express.Request, res: express.Response) { 23function webfingerController (req: express.Request, res: express.Response) {
21 const actor = res.locals.actorFull 24 const actor = res.locals.actorUrl
22 25
23 const json = { 26 const json = {
24 subject: req.query.resource, 27 subject: req.query.resource,
@@ -32,5 +35,5 @@ function webfingerController (req: express.Request, res: express.Response) {
32 ] 35 ]
33 } 36 }
34 37
35 return res.json(json).end() 38 return res.json(json)
36} 39}
diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts
index 239d8291d..aeb8fde01 100644
--- a/server/helpers/activitypub.ts
+++ b/server/helpers/activitypub.ts
@@ -2,21 +2,35 @@ import * as Bluebird from 'bluebird'
2import validator from 'validator' 2import 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/constants' 5import { ACTIVITY_PUB, REMOTE_SCHEME } from '../initializers/constants'
6import { signJsonLDObject } from './peertube-crypto' 6import { signJsonLDObject } from './peertube-crypto'
7import { pageToStartAndCount } from './core-utils' 7import { pageToStartAndCount } from './core-utils'
8import { parse } from 'url' 8import { URL } from 'url'
9import { MActor } from '../typings/models' 9import { MActor, MVideoAccountLight } from '../typings/models'
10 10import { ContextType } from '@shared/models/activitypub/context'
11function activityPubContextify <T> (data: T) { 11
12 return Object.assign(data, { 12function getContextData (type: ContextType) {
13 '@context': [ 13 const context: any[] = [
14 'https://www.w3.org/ns/activitystreams', 14 'https://www.w3.org/ns/activitystreams',
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 }
19 sc: 'http://schema.org#', 19 ]
20
21 if (type !== 'View' && type !== 'Announce') {
22 const additional = {
23 pt: 'https://joinpeertube.org/ns#',
24 sc: 'http://schema.org#'
25 }
26
27 if (type === 'CacheFile') {
28 Object.assign(additional, {
29 expires: 'sc:expires',
30 CacheFile: 'pt:CacheFile'
31 })
32 } else {
33 Object.assign(additional, {
20 Hashtag: 'as:Hashtag', 34 Hashtag: 'as:Hashtag',
21 uuid: 'sc:identifier', 35 uuid: 'sc:identifier',
22 category: 'sc:category', 36 category: 'sc:category',
@@ -24,8 +38,7 @@ function activityPubContextify <T> (data: T) {
24 subtitleLanguage: 'sc:subtitleLanguage', 38 subtitleLanguage: 'sc:subtitleLanguage',
25 sensitive: 'as:sensitive', 39 sensitive: 'as:sensitive',
26 language: 'sc:inLanguage', 40 language: 'sc:inLanguage',
27 expires: 'sc:expires', 41
28 CacheFile: 'pt:CacheFile',
29 Infohash: 'pt:Infohash', 42 Infohash: 'pt:Infohash',
30 originallyPublishedAt: 'sc:datePublished', 43 originallyPublishedAt: 'sc:datePublished',
31 views: { 44 views: {
@@ -71,9 +84,7 @@ function activityPubContextify <T> (data: T) {
71 support: { 84 support: {
72 '@type': 'sc:Text', 85 '@type': 'sc:Text',
73 '@id': 'pt:support' 86 '@id': 'pt:support'
74 } 87 },
75 },
76 {
77 likes: { 88 likes: {
78 '@id': 'as:likes', 89 '@id': 'as:likes',
79 '@type': '@id' 90 '@type': '@id'
@@ -94,9 +105,19 @@ function activityPubContextify <T> (data: T) {
94 '@id': 'as:comments', 105 '@id': 'as:comments',
95 '@type': '@id' 106 '@type': '@id'
96 } 107 }
97 } 108 })
98 ] 109 }
99 }) 110
111 context.push(additional)
112 }
113
114 return {
115 '@context': context
116 }
117}
118
119function activityPubContextify <T> (data: T, type: ContextType = 'All') {
120 return Object.assign({}, data, getContextData(type))
100} 121}
101 122
102type ActivityPubCollectionPaginationHandler = (start: number, count: number) => Bluebird<ResultList<any>> | Promise<ResultList<any>> 123type ActivityPubCollectionPaginationHandler = (start: number, count: number) => Bluebird<ResultList<any>> | Promise<ResultList<any>>
@@ -148,8 +169,8 @@ async function activityPubCollectionPagination (
148 169
149} 170}
150 171
151function buildSignedActivity (byActor: MActor, data: Object) { 172function buildSignedActivity (byActor: MActor, data: Object, contextType?: ContextType) {
152 const activity = activityPubContextify(data) 173 const activity = activityPubContextify(data, contextType)
153 174
154 return signJsonLDObject(byActor, activity) as Promise<Activity> 175 return signJsonLDObject(byActor, activity) as Promise<Activity>
155} 176}
@@ -161,12 +182,18 @@ function getAPId (activity: string | { id: string }) {
161} 182}
162 183
163function checkUrlsSameHost (url1: string, url2: string) { 184function checkUrlsSameHost (url1: string, url2: string) {
164 const idHost = parse(url1).host 185 const idHost = new URL(url1).host
165 const actorHost = parse(url2).host 186 const actorHost = new URL(url2).host
166 187
167 return idHost && actorHost && idHost.toLowerCase() === actorHost.toLowerCase() 188 return idHost && actorHost && idHost.toLowerCase() === actorHost.toLowerCase()
168} 189}
169 190
191function buildRemoteVideoBaseUrl (video: MVideoAccountLight, path: string) {
192 const host = video.VideoChannel.Account.Actor.Server.host
193
194 return REMOTE_SCHEME.HTTP + '://' + host + path
195}
196
170// --------------------------------------------------------------------------- 197// ---------------------------------------------------------------------------
171 198
172export { 199export {
@@ -174,5 +201,6 @@ export {
174 getAPId, 201 getAPId,
175 activityPubContextify, 202 activityPubContextify,
176 activityPubCollectionPagination, 203 activityPubCollectionPagination,
177 buildSignedActivity 204 buildSignedActivity,
205 buildRemoteVideoBaseUrl
178} 206}
diff --git a/server/helpers/audit-logger.ts b/server/helpers/audit-logger.ts
index 9b258dc3a..0bbfbc753 100644
--- a/server/helpers/audit-logger.ts
+++ b/server/helpers/audit-logger.ts
@@ -36,7 +36,7 @@ const auditLogger = winston.createLogger({
36 maxFiles: 5, 36 maxFiles: 5,
37 format: winston.format.combine( 37 format: winston.format.combine(
38 winston.format.timestamp(), 38 winston.format.timestamp(),
39 labelFormatter, 39 labelFormatter(),
40 winston.format.splat(), 40 winston.format.splat(),
41 jsonLoggerFormat 41 jsonLoggerFormat
42 ) 42 )
@@ -81,7 +81,8 @@ function auditLoggerFactory (domain: string) {
81} 81}
82 82
83abstract class EntityAuditView { 83abstract class EntityAuditView {
84 constructor (private keysToKeep: Array<string>, private prefix: string, private entityInfos: object) { } 84 constructor (private readonly keysToKeep: string[], private readonly prefix: string, private readonly entityInfos: object) { }
85
85 toLogKeys (): object { 86 toLogKeys (): object {
86 return chain(flatten(this.entityInfos, { delimiter: '-', safe: true })) 87 return chain(flatten(this.entityInfos, { delimiter: '-', safe: true }))
87 .pick(this.keysToKeep) 88 .pick(this.keysToKeep)
@@ -121,7 +122,7 @@ const videoKeysToKeep = [
121 'downloadEnabled' 122 'downloadEnabled'
122] 123]
123class VideoAuditView extends EntityAuditView { 124class VideoAuditView extends EntityAuditView {
124 constructor (private video: VideoDetails) { 125 constructor (private readonly video: VideoDetails) {
125 super(videoKeysToKeep, 'video', video) 126 super(videoKeysToKeep, 'video', video)
126 } 127 }
127} 128}
@@ -132,7 +133,7 @@ const videoImportKeysToKeep = [
132 'video-name' 133 'video-name'
133] 134]
134class VideoImportAuditView extends EntityAuditView { 135class VideoImportAuditView extends EntityAuditView {
135 constructor (private videoImport: VideoImport) { 136 constructor (private readonly videoImport: VideoImport) {
136 super(videoImportKeysToKeep, 'video-import', videoImport) 137 super(videoImportKeysToKeep, 'video-import', videoImport)
137 } 138 }
138} 139}
@@ -151,7 +152,7 @@ const commentKeysToKeep = [
151 'account-name' 152 'account-name'
152] 153]
153class CommentAuditView extends EntityAuditView { 154class CommentAuditView extends EntityAuditView {
154 constructor (private comment: VideoComment) { 155 constructor (private readonly comment: VideoComment) {
155 super(commentKeysToKeep, 'comment', comment) 156 super(commentKeysToKeep, 'comment', comment)
156 } 157 }
157} 158}
@@ -180,7 +181,7 @@ const userKeysToKeep = [
180 'videoChannels' 181 'videoChannels'
181] 182]
182class UserAuditView extends EntityAuditView { 183class UserAuditView extends EntityAuditView {
183 constructor (private user: User) { 184 constructor (private readonly user: User) {
184 super(userKeysToKeep, 'user', user) 185 super(userKeysToKeep, 'user', user)
185 } 186 }
186} 187}
@@ -206,7 +207,7 @@ const channelKeysToKeep = [
206 'ownerAccount-displayedName' 207 'ownerAccount-displayedName'
207] 208]
208class VideoChannelAuditView extends EntityAuditView { 209class VideoChannelAuditView extends EntityAuditView {
209 constructor (private channel: VideoChannel) { 210 constructor (private readonly channel: VideoChannel) {
210 super(channelKeysToKeep, 'channel', channel) 211 super(channelKeysToKeep, 'channel', channel)
211 } 212 }
212} 213}
@@ -221,7 +222,7 @@ const videoAbuseKeysToKeep = [
221 'createdAt' 222 'createdAt'
222] 223]
223class VideoAbuseAuditView extends EntityAuditView { 224class VideoAbuseAuditView extends EntityAuditView {
224 constructor (private videoAbuse: VideoAbuse) { 225 constructor (private readonly videoAbuse: VideoAbuse) {
225 super(videoAbuseKeysToKeep, 'abuse', videoAbuse) 226 super(videoAbuseKeysToKeep, 'abuse', videoAbuse)
226 } 227 }
227} 228}
@@ -253,9 +254,12 @@ class CustomConfigAuditView extends EntityAuditView {
253 const infos: any = customConfig 254 const infos: any = customConfig
254 const resolutionsDict = infos.transcoding.resolutions 255 const resolutionsDict = infos.transcoding.resolutions
255 const resolutionsArray = [] 256 const resolutionsArray = []
256 Object.entries(resolutionsDict).forEach(([resolution, isEnabled]) => { 257
257 if (isEnabled) resolutionsArray.push(resolution) 258 Object.entries(resolutionsDict)
258 }) 259 .forEach(([ resolution, isEnabled ]) => {
260 if (isEnabled) resolutionsArray.push(resolution)
261 })
262
259 Object.assign({}, infos, { transcoding: { resolutions: resolutionsArray } }) 263 Object.assign({}, infos, { transcoding: { resolutions: resolutionsArray } })
260 super(customConfigKeysToKeep, 'config', infos) 264 super(customConfigKeysToKeep, 'config', infos)
261 } 265 }
diff --git a/server/helpers/core-utils.ts b/server/helpers/core-utils.ts
index 7e8252aa4..b1f5d9610 100644
--- a/server/helpers/core-utils.ts
+++ b/server/helpers/core-utils.ts
@@ -1,9 +1,11 @@
1/* eslint-disable no-useless-call */
2
1/* 3/*
2 Different from 'utils' because we don't not import other PeerTube modules. 4 Different from 'utils' because we don't import other PeerTube modules.
3 Useful to avoid circular dependencies. 5 Useful to avoid circular dependencies.
4*/ 6*/
5 7
6import { createHash, HexBase64Latin1Encoding, pseudoRandomBytes } from 'crypto' 8import { createHash, HexBase64Latin1Encoding, randomBytes } from 'crypto'
7import { basename, isAbsolute, join, resolve } from 'path' 9import { basename, isAbsolute, join, resolve } from 'path'
8import * as pem from 'pem' 10import * as pem from 'pem'
9import { URL } from 'url' 11import { URL } from 'url'
@@ -22,31 +24,31 @@ const objectConverter = (oldObject: any, keyConverter: (e: string) => string, va
22 const newObject = {} 24 const newObject = {}
23 Object.keys(oldObject).forEach(oldKey => { 25 Object.keys(oldObject).forEach(oldKey => {
24 const newKey = keyConverter(oldKey) 26 const newKey = keyConverter(oldKey)
25 newObject[ newKey ] = objectConverter(oldObject[ oldKey ], keyConverter, valueConverter) 27 newObject[newKey] = objectConverter(oldObject[oldKey], keyConverter, valueConverter)
26 }) 28 })
27 29
28 return newObject 30 return newObject
29} 31}
30 32
31const timeTable = { 33const timeTable = {
32 ms: 1, 34 ms: 1,
33 second: 1000, 35 second: 1000,
34 minute: 60000, 36 minute: 60000,
35 hour: 3600000, 37 hour: 3600000,
36 day: 3600000 * 24, 38 day: 3600000 * 24,
37 week: 3600000 * 24 * 7, 39 week: 3600000 * 24 * 7,
38 month: 3600000 * 24 * 30 40 month: 3600000 * 24 * 30
39} 41}
40 42
41export function parseDurationToMs (duration: number | string): number { 43export function parseDurationToMs (duration: number | string): number {
42 if (typeof duration === 'number') return duration 44 if (typeof duration === 'number') return duration
43 45
44 if (typeof duration === 'string') { 46 if (typeof duration === 'string') {
45 const split = duration.match(/^([\d\.,]+)\s?(\w+)$/) 47 const split = duration.match(/^([\d.,]+)\s?(\w+)$/)
46 48
47 if (split.length === 3) { 49 if (split.length === 3) {
48 const len = parseFloat(split[1]) 50 const len = parseFloat(split[1])
49 let unit = split[2].replace(/s$/i,'').toLowerCase() 51 let unit = split[2].replace(/s$/i, '').toLowerCase()
50 if (unit === 'm') { 52 if (unit === 'm') {
51 unit = 'ms' 53 unit = 'ms'
52 } 54 }
@@ -73,21 +75,21 @@ export function parseBytes (value: string | number): number {
73 75
74 if (value.match(tgm)) { 76 if (value.match(tgm)) {
75 match = value.match(tgm) 77 match = value.match(tgm)
76 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 78 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 +
77 + parseInt(match[2], 10) * 1024 * 1024 * 1024 79 parseInt(match[2], 10) * 1024 * 1024 * 1024 +
78 + parseInt(match[3], 10) * 1024 * 1024 80 parseInt(match[3], 10) * 1024 * 1024
79 } else if (value.match(tg)) { 81 } else if (value.match(tg)) {
80 match = value.match(tg) 82 match = value.match(tg)
81 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 83 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 +
82 + parseInt(match[2], 10) * 1024 * 1024 * 1024 84 parseInt(match[2], 10) * 1024 * 1024 * 1024
83 } else if (value.match(tm)) { 85 } else if (value.match(tm)) {
84 match = value.match(tm) 86 match = value.match(tm)
85 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 87 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 +
86 + parseInt(match[2], 10) * 1024 * 1024 88 parseInt(match[2], 10) * 1024 * 1024
87 } else if (value.match(gm)) { 89 } else if (value.match(gm)) {
88 match = value.match(gm) 90 match = value.match(gm)
89 return parseInt(match[1], 10) * 1024 * 1024 * 1024 91 return parseInt(match[1], 10) * 1024 * 1024 * 1024 +
90 + parseInt(match[2], 10) * 1024 * 1024 92 parseInt(match[2], 10) * 1024 * 1024
91 } else if (value.match(t)) { 93 } else if (value.match(t)) {
92 match = value.match(t) 94 match = value.match(t)
93 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 95 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024
@@ -137,6 +139,7 @@ function getAppNumber () {
137} 139}
138 140
139let rootPath: string 141let rootPath: string
142
140function root () { 143function root () {
141 if (rootPath) return rootPath 144 if (rootPath) return rootPath
142 145
@@ -163,7 +166,7 @@ function escapeHTML (stringParam) {
163 '=': '&#x3D;' 166 '=': '&#x3D;'
164 } 167 }
165 168
166 return String(stringParam).replace(/[&<>"'`=\/]/g, s => entityMap[s]) 169 return String(stringParam).replace(/[&<>"'`=/]/g, s => entityMap[s])
167} 170}
168 171
169function pageToStartAndCount (page: number, itemsPerPage: number) { 172function pageToStartAndCount (page: number, itemsPerPage: number) {
@@ -202,6 +205,7 @@ function sha1 (str: string | Buffer, encoding: HexBase64Latin1Encoding = 'hex')
202function execShell (command: string, options?: ExecOptions) { 205function execShell (command: string, options?: ExecOptions) {
203 return new Promise<{ err?: Error, stdout: string, stderr: string }>((res, rej) => { 206 return new Promise<{ err?: Error, stdout: string, stderr: string }>((res, rej) => {
204 exec(command, options, (err, stdout, stderr) => { 207 exec(command, options, (err, stdout, stderr) => {
208 // eslint-disable-next-line prefer-promise-reject-errors
205 if (err) return rej({ err, stdout, stderr }) 209 if (err) return rej({ err, stdout, stderr })
206 210
207 return res({ stdout, stderr }) 211 return res({ stdout, stderr })
@@ -226,14 +230,6 @@ function promisify1<T, A> (func: (arg: T, cb: (err: any, result: A) => void) =>
226 } 230 }
227} 231}
228 232
229function promisify1WithVoid<T> (func: (arg: T, cb: (err: any) => void) => void): (arg: T) => Promise<void> {
230 return function promisified (arg: T): Promise<void> {
231 return new Promise<void>((resolve: () => void, reject: (err: any) => void) => {
232 func.apply(null, [ arg, (err: any) => err ? reject(err) : resolve() ])
233 })
234 }
235}
236
237function promisify2<T, U, A> (func: (arg1: T, arg2: U, cb: (err: any, result: A) => void) => void): (arg1: T, arg2: U) => Promise<A> { 233function promisify2<T, U, A> (func: (arg1: T, arg2: U, cb: (err: any, result: A) => void) => void): (arg1: T, arg2: U) => Promise<A> {
238 return function promisified (arg1: T, arg2: U): Promise<A> { 234 return function promisified (arg1: T, arg2: U): Promise<A> {
239 return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => { 235 return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
@@ -242,15 +238,7 @@ function promisify2<T, U, A> (func: (arg1: T, arg2: U, cb: (err: any, result: A)
242 } 238 }
243} 239}
244 240
245function promisify2WithVoid<T, U> (func: (arg1: T, arg2: U, cb: (err: any) => void) => void): (arg1: T, arg2: U) => Promise<void> { 241const randomBytesPromise = promisify1<number, Buffer>(randomBytes)
246 return function promisified (arg1: T, arg2: U): Promise<void> {
247 return new Promise<void>((resolve: () => void, reject: (err: any) => void) => {
248 func.apply(null, [ arg1, arg2, (err: any) => err ? reject(err) : resolve() ])
249 })
250 }
251}
252
253const pseudoRandomBytesPromise = promisify1<number, Buffer>(pseudoRandomBytes)
254const createPrivateKey = promisify1<number, { key: string }>(pem.createPrivateKey) 242const createPrivateKey = promisify1<number, { key: string }>(pem.createPrivateKey)
255const getPublicKey = promisify1<string, { publicKey: string }>(pem.getPublicKey) 243const getPublicKey = promisify1<string, { publicKey: string }>(pem.getPublicKey)
256const execPromise2 = promisify2<string, any, string>(exec) 244const execPromise2 = promisify2<string, any, string>(exec)
@@ -280,7 +268,7 @@ export {
280 promisify1, 268 promisify1,
281 promisify2, 269 promisify2,
282 270
283 pseudoRandomBytesPromise, 271 randomBytesPromise,
284 createPrivateKey, 272 createPrivateKey,
285 getPublicKey, 273 getPublicKey,
286 execPromise2, 274 execPromise2,
diff --git a/server/helpers/custom-jsonld-signature.ts b/server/helpers/custom-jsonld-signature.ts
index a407a9fec..749c50cb3 100644
--- a/server/helpers/custom-jsonld-signature.ts
+++ b/server/helpers/custom-jsonld-signature.ts
@@ -5,52 +5,52 @@ import { logger } from './logger'
5const CACHE = { 5const CACHE = {
6 'https://w3id.org/security/v1': { 6 'https://w3id.org/security/v1': {
7 '@context': { 7 '@context': {
8 'id': '@id', 8 id: '@id',
9 'type': '@type', 9 type: '@type',
10 10
11 'dc': 'http://purl.org/dc/terms/', 11 dc: 'http://purl.org/dc/terms/',
12 'sec': 'https://w3id.org/security#', 12 sec: 'https://w3id.org/security#',
13 'xsd': 'http://www.w3.org/2001/XMLSchema#', 13 xsd: 'http://www.w3.org/2001/XMLSchema#',
14 14
15 'EcdsaKoblitzSignature2016': 'sec:EcdsaKoblitzSignature2016', 15 EcdsaKoblitzSignature2016: 'sec:EcdsaKoblitzSignature2016',
16 'Ed25519Signature2018': 'sec:Ed25519Signature2018', 16 Ed25519Signature2018: 'sec:Ed25519Signature2018',
17 'EncryptedMessage': 'sec:EncryptedMessage', 17 EncryptedMessage: 'sec:EncryptedMessage',
18 'GraphSignature2012': 'sec:GraphSignature2012', 18 GraphSignature2012: 'sec:GraphSignature2012',
19 'LinkedDataSignature2015': 'sec:LinkedDataSignature2015', 19 LinkedDataSignature2015: 'sec:LinkedDataSignature2015',
20 'LinkedDataSignature2016': 'sec:LinkedDataSignature2016', 20 LinkedDataSignature2016: 'sec:LinkedDataSignature2016',
21 'CryptographicKey': 'sec:Key', 21 CryptographicKey: 'sec:Key',
22 22
23 'authenticationTag': 'sec:authenticationTag', 23 authenticationTag: 'sec:authenticationTag',
24 'canonicalizationAlgorithm': 'sec:canonicalizationAlgorithm', 24 canonicalizationAlgorithm: 'sec:canonicalizationAlgorithm',
25 'cipherAlgorithm': 'sec:cipherAlgorithm', 25 cipherAlgorithm: 'sec:cipherAlgorithm',
26 'cipherData': 'sec:cipherData', 26 cipherData: 'sec:cipherData',
27 'cipherKey': 'sec:cipherKey', 27 cipherKey: 'sec:cipherKey',
28 'created': { '@id': 'dc:created', '@type': 'xsd:dateTime' }, 28 created: { '@id': 'dc:created', '@type': 'xsd:dateTime' },
29 'creator': { '@id': 'dc:creator', '@type': '@id' }, 29 creator: { '@id': 'dc:creator', '@type': '@id' },
30 'digestAlgorithm': 'sec:digestAlgorithm', 30 digestAlgorithm: 'sec:digestAlgorithm',
31 'digestValue': 'sec:digestValue', 31 digestValue: 'sec:digestValue',
32 'domain': 'sec:domain', 32 domain: 'sec:domain',
33 'encryptionKey': 'sec:encryptionKey', 33 encryptionKey: 'sec:encryptionKey',
34 'expiration': { '@id': 'sec:expiration', '@type': 'xsd:dateTime' }, 34 expiration: { '@id': 'sec:expiration', '@type': 'xsd:dateTime' },
35 'expires': { '@id': 'sec:expiration', '@type': 'xsd:dateTime' }, 35 expires: { '@id': 'sec:expiration', '@type': 'xsd:dateTime' },
36 'initializationVector': 'sec:initializationVector', 36 initializationVector: 'sec:initializationVector',
37 'iterationCount': 'sec:iterationCount', 37 iterationCount: 'sec:iterationCount',
38 'nonce': 'sec:nonce', 38 nonce: 'sec:nonce',
39 'normalizationAlgorithm': 'sec:normalizationAlgorithm', 39 normalizationAlgorithm: 'sec:normalizationAlgorithm',
40 'owner': { '@id': 'sec:owner', '@type': '@id' }, 40 owner: { '@id': 'sec:owner', '@type': '@id' },
41 'password': 'sec:password', 41 password: 'sec:password',
42 'privateKey': { '@id': 'sec:privateKey', '@type': '@id' }, 42 privateKey: { '@id': 'sec:privateKey', '@type': '@id' },
43 'privateKeyPem': 'sec:privateKeyPem', 43 privateKeyPem: 'sec:privateKeyPem',
44 'publicKey': { '@id': 'sec:publicKey', '@type': '@id' }, 44 publicKey: { '@id': 'sec:publicKey', '@type': '@id' },
45 'publicKeyBase58': 'sec:publicKeyBase58', 45 publicKeyBase58: 'sec:publicKeyBase58',
46 'publicKeyPem': 'sec:publicKeyPem', 46 publicKeyPem: 'sec:publicKeyPem',
47 'publicKeyWif': 'sec:publicKeyWif', 47 publicKeyWif: 'sec:publicKeyWif',
48 'publicKeyService': { '@id': 'sec:publicKeyService', '@type': '@id' }, 48 publicKeyService: { '@id': 'sec:publicKeyService', '@type': '@id' },
49 'revoked': { '@id': 'sec:revoked', '@type': 'xsd:dateTime' }, 49 revoked: { '@id': 'sec:revoked', '@type': 'xsd:dateTime' },
50 'salt': 'sec:salt', 50 salt: 'sec:salt',
51 'signature': 'sec:signature', 51 signature: 'sec:signature',
52 'signatureAlgorithm': 'sec:signingAlgorithm', 52 signatureAlgorithm: 'sec:signingAlgorithm',
53 'signatureValue': 'sec:signatureValue' 53 signatureValue: 'sec:signatureValue'
54 } 54 }
55 } 55 }
56} 56}
@@ -60,12 +60,12 @@ const nodeDocumentLoader = jsonld.documentLoaders.node()
60const lru = new AsyncLRU({ 60const lru = new AsyncLRU({
61 max: 10, 61 max: 10,
62 load: (url, cb) => { 62 load: (url, cb) => {
63 if (CACHE[ url ] !== undefined) { 63 if (CACHE[url] !== undefined) {
64 logger.debug('Using cache for JSON-LD %s.', url) 64 logger.debug('Using cache for JSON-LD %s.', url)
65 65
66 return cb(null, { 66 return cb(null, {
67 contextUrl: null, 67 contextUrl: null,
68 document: CACHE[ url ], 68 document: CACHE[url],
69 documentUrl: url 69 documentUrl: url
70 }) 70 })
71 } 71 }
diff --git a/server/helpers/custom-validators/activitypub/actor.ts b/server/helpers/custom-validators/activitypub/actor.ts
index fa58e163f..2f44522a5 100644
--- a/server/helpers/custom-validators/activitypub/actor.ts
+++ b/server/helpers/custom-validators/activitypub/actor.ts
@@ -6,7 +6,7 @@ import { isHostValid } from '../servers'
6import { peertubeTruncate } from '@server/helpers/core-utils' 6import { peertubeTruncate } from '@server/helpers/core-utils'
7 7
8function isActorEndpointsObjectValid (endpointObject: any) { 8function isActorEndpointsObjectValid (endpointObject: any) {
9 if (endpointObject && endpointObject.sharedInbox) { 9 if (endpointObject?.sharedInbox) {
10 return isActivityPubUrlValid(endpointObject.sharedInbox) 10 return isActivityPubUrlValid(endpointObject.sharedInbox)
11 } 11 }
12 12
@@ -28,7 +28,7 @@ function isActorPublicKeyValid (publicKey: string) {
28 return exists(publicKey) && 28 return exists(publicKey) &&
29 typeof publicKey === 'string' && 29 typeof publicKey === 'string' &&
30 publicKey.startsWith('-----BEGIN PUBLIC KEY-----') && 30 publicKey.startsWith('-----BEGIN PUBLIC KEY-----') &&
31 publicKey.indexOf('-----END PUBLIC KEY-----') !== -1 && 31 publicKey.includes('-----END PUBLIC KEY-----') &&
32 validator.isLength(publicKey, CONSTRAINTS_FIELDS.ACTORS.PUBLIC_KEY) 32 validator.isLength(publicKey, CONSTRAINTS_FIELDS.ACTORS.PUBLIC_KEY)
33} 33}
34 34
@@ -43,7 +43,7 @@ function isActorPrivateKeyValid (privateKey: string) {
43 typeof privateKey === 'string' && 43 typeof privateKey === 'string' &&
44 privateKey.startsWith('-----BEGIN RSA PRIVATE KEY-----') && 44 privateKey.startsWith('-----BEGIN RSA PRIVATE KEY-----') &&
45 // Sometimes there is a \n at the end, so just assert the string contains the end mark 45 // Sometimes there is a \n at the end, so just assert the string contains the end mark
46 privateKey.indexOf('-----END RSA PRIVATE KEY-----') !== -1 && 46 privateKey.includes('-----END RSA PRIVATE KEY-----') &&
47 validator.isLength(privateKey, CONSTRAINTS_FIELDS.ACTORS.PRIVATE_KEY) 47 validator.isLength(privateKey, CONSTRAINTS_FIELDS.ACTORS.PRIVATE_KEY)
48} 48}
49 49
@@ -101,8 +101,6 @@ function normalizeActor (actor: any) {
101 actor.summary = null 101 actor.summary = null
102 } 102 }
103 } 103 }
104
105 return
106} 104}
107 105
108function isValidActorHandle (handle: string) { 106function isValidActorHandle (handle: string) {
diff --git a/server/helpers/custom-validators/activitypub/cache-file.ts b/server/helpers/custom-validators/activitypub/cache-file.ts
index 21d5c53ca..c5b3b4d9f 100644
--- a/server/helpers/custom-validators/activitypub/cache-file.ts
+++ b/server/helpers/custom-validators/activitypub/cache-file.ts
@@ -6,7 +6,7 @@ import { CacheFileObject } from '../../../../shared/models/activitypub/objects'
6function isCacheFileObjectValid (object: CacheFileObject) { 6function isCacheFileObjectValid (object: CacheFileObject) {
7 return exists(object) && 7 return exists(object) &&
8 object.type === 'CacheFile' && 8 object.type === 'CacheFile' &&
9 isDateValid(object.expires) && 9 (object.expires === null || isDateValid(object.expires)) &&
10 isActivityPubUrlValid(object.object) && 10 isActivityPubUrlValid(object.object) &&
11 (isRemoteVideoUrlValid(object.url) || isPlaylistRedundancyUrlValid(object.url)) 11 (isRemoteVideoUrlValid(object.url) || isPlaylistRedundancyUrlValid(object.url))
12} 12}
diff --git a/server/helpers/custom-validators/activitypub/video-comments.ts b/server/helpers/custom-validators/activitypub/video-comments.ts
index aa3c246b5..ea852c491 100644
--- a/server/helpers/custom-validators/activitypub/video-comments.ts
+++ b/server/helpers/custom-validators/activitypub/video-comments.ts
@@ -48,8 +48,6 @@ function normalizeComment (comment: any) {
48 if (typeof comment.url === 'object') comment.url = comment.url.href || comment.url.url 48 if (typeof comment.url === 'object') comment.url = comment.url.href || comment.url.url
49 else comment.url = comment.id 49 else comment.url = comment.id
50 } 50 }
51
52 return
53} 51}
54 52
55function isCommentTypeValid (comment: any): boolean { 53function isCommentTypeValid (comment: any): boolean {
diff --git a/server/helpers/custom-validators/activitypub/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts
index fe94bd58a..876cc7f50 100644
--- a/server/helpers/custom-validators/activitypub/videos.ts
+++ b/server/helpers/custom-validators/activitypub/videos.ts
@@ -13,6 +13,7 @@ import {
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 { logger } from '@server/helpers/logger' 15import { logger } from '@server/helpers/logger'
16import { ActivityVideoFileMetadataObject } from '@shared/models'
16 17
17function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) { 18function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) {
18 return isBaseActivityValid(activity, 'Update') && 19 return isBaseActivityValid(activity, 'Update') &&
@@ -51,11 +52,16 @@ function sanitizeAndCheckVideoTorrentObject (video: any) {
51 logger.debug('Video has invalid captions', { video }) 52 logger.debug('Video has invalid captions', { video })
52 return false 53 return false
53 } 54 }
55 if (!setValidRemoteIcon(video)) {
56 logger.debug('Video has invalid icons', { video })
57 return false
58 }
54 59
55 // Default attributes 60 // Default attributes
56 if (!isVideoStateValid(video.state)) video.state = VideoState.PUBLISHED 61 if (!isVideoStateValid(video.state)) video.state = VideoState.PUBLISHED
57 if (!isBooleanValid(video.waitTranscoding)) video.waitTranscoding = false 62 if (!isBooleanValid(video.waitTranscoding)) video.waitTranscoding = false
58 if (!isBooleanValid(video.downloadEnabled)) video.downloadEnabled = true 63 if (!isBooleanValid(video.downloadEnabled)) video.downloadEnabled = true
64 if (!isBooleanValid(video.commentsEnabled)) video.commentsEnabled = false
59 65
60 return isActivityPubUrlValid(video.id) && 66 return isActivityPubUrlValid(video.id) &&
61 isVideoNameValid(video.name) && 67 isVideoNameValid(video.name) &&
@@ -72,7 +78,6 @@ function sanitizeAndCheckVideoTorrentObject (video: any) {
72 isDateValid(video.updated) && 78 isDateValid(video.updated) &&
73 (!video.originallyPublishedAt || isDateValid(video.originallyPublishedAt)) && 79 (!video.originallyPublishedAt || isDateValid(video.originallyPublishedAt)) &&
74 (!video.content || isRemoteVideoContentValid(video.mediaType, video.content)) && 80 (!video.content || isRemoteVideoContentValid(video.mediaType, video.content)) &&
75 isRemoteVideoIconValid(video.icon) &&
76 video.url.length !== 0 && 81 video.url.length !== 0 &&
77 video.attributedTo.length !== 0 82 video.attributedTo.length !== 0
78} 83}
@@ -80,19 +85,19 @@ function sanitizeAndCheckVideoTorrentObject (video: any) {
80function isRemoteVideoUrlValid (url: any) { 85function isRemoteVideoUrlValid (url: any) {
81 return url.type === 'Link' && 86 return url.type === 'Link' &&
82 ( 87 (
83 ACTIVITY_PUB.URL_MIME_TYPES.VIDEO.indexOf(url.mediaType) !== -1 && 88 ACTIVITY_PUB.URL_MIME_TYPES.VIDEO.includes(url.mediaType) &&
84 isActivityPubUrlValid(url.href) && 89 isActivityPubUrlValid(url.href) &&
85 validator.isInt(url.height + '', { min: 0 }) && 90 validator.isInt(url.height + '', { min: 0 }) &&
86 validator.isInt(url.size + '', { min: 0 }) && 91 validator.isInt(url.size + '', { min: 0 }) &&
87 (!url.fps || validator.isInt(url.fps + '', { min: -1 })) 92 (!url.fps || validator.isInt(url.fps + '', { min: -1 }))
88 ) || 93 ) ||
89 ( 94 (
90 ACTIVITY_PUB.URL_MIME_TYPES.TORRENT.indexOf(url.mediaType) !== -1 && 95 ACTIVITY_PUB.URL_MIME_TYPES.TORRENT.includes(url.mediaType) &&
91 isActivityPubUrlValid(url.href) && 96 isActivityPubUrlValid(url.href) &&
92 validator.isInt(url.height + '', { min: 0 }) 97 validator.isInt(url.height + '', { min: 0 })
93 ) || 98 ) ||
94 ( 99 (
95 ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.indexOf(url.mediaType) !== -1 && 100 ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.includes(url.mediaType) &&
96 validator.isLength(url.href, { min: 5 }) && 101 validator.isLength(url.href, { min: 5 }) &&
97 validator.isInt(url.height + '', { min: 0 }) 102 validator.isInt(url.height + '', { min: 0 })
98 ) || 103 ) ||
@@ -100,7 +105,15 @@ function isRemoteVideoUrlValid (url: any) {
100 (url.mediaType || url.mimeType) === 'application/x-mpegURL' && 105 (url.mediaType || url.mimeType) === 'application/x-mpegURL' &&
101 isActivityPubUrlValid(url.href) && 106 isActivityPubUrlValid(url.href) &&
102 isArray(url.tag) 107 isArray(url.tag)
103 ) 108 ) ||
109 isAPVideoFileMetadataObject(url)
110}
111
112function isAPVideoFileMetadataObject (url: any): url is ActivityVideoFileMetadataObject {
113 return url &&
114 url.type === 'Link' &&
115 url.mediaType === 'application/json' &&
116 isArray(url.rel) && url.rel.includes('metadata')
104} 117}
105 118
106// --------------------------------------------------------------------------- 119// ---------------------------------------------------------------------------
@@ -109,7 +122,8 @@ export {
109 sanitizeAndCheckVideoTorrentUpdateActivity, 122 sanitizeAndCheckVideoTorrentUpdateActivity,
110 isRemoteStringIdentifierValid, 123 isRemoteStringIdentifierValid,
111 sanitizeAndCheckVideoTorrentObject, 124 sanitizeAndCheckVideoTorrentObject,
112 isRemoteVideoUrlValid 125 isRemoteVideoUrlValid,
126 isAPVideoFileMetadataObject
113} 127}
114 128
115// --------------------------------------------------------------------------- 129// ---------------------------------------------------------------------------
@@ -131,6 +145,8 @@ function setValidRemoteCaptions (video: any) {
131 if (Array.isArray(video.subtitleLanguage) === false) return false 145 if (Array.isArray(video.subtitleLanguage) === false) return false
132 146
133 video.subtitleLanguage = video.subtitleLanguage.filter(caption => { 147 video.subtitleLanguage = video.subtitleLanguage.filter(caption => {
148 if (!isActivityPubUrlValid(caption.url)) caption.url = null
149
134 return isRemoteStringIdentifierValid(caption) 150 return isRemoteStringIdentifierValid(caption)
135 }) 151 })
136 152
@@ -149,12 +165,19 @@ function isRemoteVideoContentValid (mediaType: string, content: string) {
149 return mediaType === 'text/markdown' && isVideoTruncatedDescriptionValid(content) 165 return mediaType === 'text/markdown' && isVideoTruncatedDescriptionValid(content)
150} 166}
151 167
152function isRemoteVideoIconValid (icon: any) { 168function setValidRemoteIcon (video: any) {
153 return icon.type === 'Image' && 169 if (video.icon && !isArray(video.icon)) video.icon = [ video.icon ]
154 isActivityPubUrlValid(icon.url) && 170 if (!video.icon) video.icon = []
155 icon.mediaType === 'image/jpeg' && 171
156 validator.isInt(icon.width + '', { min: 0 }) && 172 video.icon = video.icon.filter(icon => {
157 validator.isInt(icon.height + '', { min: 0 }) 173 return icon.type === 'Image' &&
174 isActivityPubUrlValid(icon.url) &&
175 icon.mediaType === 'image/jpeg' &&
176 validator.isInt(icon.width + '', { min: 0 }) &&
177 validator.isInt(icon.height + '', { min: 0 })
178 })
179
180 return video.icon.length !== 0
158} 181}
159 182
160function setValidRemoteVideoUrls (video: any) { 183function setValidRemoteVideoUrls (video: any) {
diff --git a/server/helpers/custom-validators/feeds.ts b/server/helpers/custom-validators/feeds.ts
index 638e814f0..fa35a7da6 100644
--- a/server/helpers/custom-validators/feeds.ts
+++ b/server/helpers/custom-validators/feeds.ts
@@ -13,7 +13,7 @@ function isValidRSSFeed (value: string) {
13 'atom1' 13 'atom1'
14 ] 14 ]
15 15
16 return feedExtensions.indexOf(value) !== -1 16 return feedExtensions.includes(value)
17} 17}
18 18
19// --------------------------------------------------------------------------- 19// ---------------------------------------------------------------------------
diff --git a/server/helpers/custom-validators/logs.ts b/server/helpers/custom-validators/logs.ts
index 30d0ce262..0f266ed3b 100644
--- a/server/helpers/custom-validators/logs.ts
+++ b/server/helpers/custom-validators/logs.ts
@@ -4,7 +4,7 @@ import { LogLevel } from '../../../shared/models/server/log-level.type'
4const logLevels: LogLevel[] = [ 'debug', 'info', 'warn', 'error' ] 4const logLevels: LogLevel[] = [ 'debug', 'info', 'warn', 'error' ]
5 5
6function isValidLogLevel (value: any) { 6function isValidLogLevel (value: any) {
7 return exists(value) && logLevels.indexOf(value) !== -1 7 return exists(value) && logLevels.includes(value)
8} 8}
9 9
10// --------------------------------------------------------------------------- 10// ---------------------------------------------------------------------------
diff --git a/server/helpers/custom-validators/misc.ts b/server/helpers/custom-validators/misc.ts
index 89149b3e0..cf32201c4 100644
--- a/server/helpers/custom-validators/misc.ts
+++ b/server/helpers/custom-validators/misc.ts
@@ -94,13 +94,13 @@ function isFileValid (
94 if (isArray(files)) return optional 94 if (isArray(files)) return optional
95 95
96 // Should have a file 96 // Should have a file
97 const fileArray = files[ field ] 97 const fileArray = files[field]
98 if (!fileArray || fileArray.length === 0) { 98 if (!fileArray || fileArray.length === 0) {
99 return optional 99 return optional
100 } 100 }
101 101
102 // The file should exist 102 // The file should exist
103 const file = fileArray[ 0 ] 103 const file = fileArray[0]
104 if (!file || !file.originalname) return false 104 if (!file || !file.originalname) return false
105 105
106 // Check size 106 // Check size
diff --git a/server/helpers/custom-validators/plugins.ts b/server/helpers/custom-validators/plugins.ts
index 3af72547b..d2fc03936 100644
--- a/server/helpers/custom-validators/plugins.ts
+++ b/server/helpers/custom-validators/plugins.ts
@@ -14,7 +14,7 @@ function isPluginTypeValid (value: any) {
14function isPluginNameValid (value: string) { 14function isPluginNameValid (value: string) {
15 return exists(value) && 15 return exists(value) &&
16 validator.isLength(value, PLUGINS_CONSTRAINTS_FIELDS.NAME) && 16 validator.isLength(value, PLUGINS_CONSTRAINTS_FIELDS.NAME) &&
17 validator.matches(value, /^[a-z\-]+$/) 17 validator.matches(value, /^[a-z-0-9]+$/)
18} 18}
19 19
20function isNpmPluginNameValid (value: string) { 20function isNpmPluginNameValid (value: string) {
@@ -146,8 +146,8 @@ function isPackageJSONValid (packageJSON: PluginPackageJson, pluginType: PluginT
146} 146}
147 147
148function isLibraryCodeValid (library: any) { 148function isLibraryCodeValid (library: any) {
149 return typeof library.register === 'function' 149 return typeof library.register === 'function' &&
150 && typeof library.unregister === 'function' 150 typeof library.unregister === 'function'
151} 151}
152 152
153export { 153export {
diff --git a/server/helpers/custom-validators/user-notifications.ts b/server/helpers/custom-validators/user-notifications.ts
index 5a4d10504..8a33b895b 100644
--- a/server/helpers/custom-validators/user-notifications.ts
+++ b/server/helpers/custom-validators/user-notifications.ts
@@ -9,7 +9,8 @@ function isUserNotificationTypeValid (value: any) {
9 9
10function isUserNotificationSettingValid (value: any) { 10function isUserNotificationSettingValid (value: any) {
11 return exists(value) && 11 return exists(value) &&
12 validator.isInt('' + value) && ( 12 validator.isInt('' + value) &&
13 (
13 value === UserNotificationSettingValue.NONE || 14 value === UserNotificationSettingValue.NONE ||
14 value === UserNotificationSettingValue.WEB || 15 value === UserNotificationSettingValue.WEB ||
15 value === UserNotificationSettingValue.EMAIL || 16 value === UserNotificationSettingValue.EMAIL ||
diff --git a/server/helpers/custom-validators/users.ts b/server/helpers/custom-validators/users.ts
index b4d5751e7..d6e91ad35 100644
--- a/server/helpers/custom-validators/users.ts
+++ b/server/helpers/custom-validators/users.ts
@@ -3,6 +3,7 @@ import { UserRole } from '../../../shared'
3import { CONSTRAINTS_FIELDS, NSFW_POLICY_TYPES } from '../../initializers/constants' 3import { CONSTRAINTS_FIELDS, NSFW_POLICY_TYPES } from '../../initializers/constants'
4import { exists, isArray, isBooleanValid, isFileValid } from './misc' 4import { exists, isArray, isBooleanValid, isFileValid } from './misc'
5import { values } from 'lodash' 5import { values } from 'lodash'
6import { isEmailEnabled } from '../../initializers/config'
6 7
7const USERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USERS 8const USERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USERS
8 9
@@ -10,6 +11,13 @@ function isUserPasswordValid (value: string) {
10 return validator.isLength(value, USERS_CONSTRAINTS_FIELDS.PASSWORD) 11 return validator.isLength(value, USERS_CONSTRAINTS_FIELDS.PASSWORD)
11} 12}
12 13
14function isUserPasswordValidOrEmpty (value: string) {
15 // Empty password is only possible if emailing is enabled.
16 if (value === '') return isEmailEnabled()
17
18 return isUserPasswordValid(value)
19}
20
13function isUserVideoQuotaValid (value: string) { 21function isUserVideoQuotaValid (value: string) {
14 return exists(value) && validator.isInt(value + '', USERS_CONSTRAINTS_FIELDS.VIDEO_QUOTA) 22 return exists(value) && validator.isInt(value + '', USERS_CONSTRAINTS_FIELDS.VIDEO_QUOTA)
15} 23}
@@ -38,7 +46,7 @@ function isUserEmailVerifiedValid (value: any) {
38 46
39const nsfwPolicies = values(NSFW_POLICY_TYPES) 47const nsfwPolicies = values(NSFW_POLICY_TYPES)
40function isUserNSFWPolicyValid (value: any) { 48function isUserNSFWPolicyValid (value: any) {
41 return exists(value) && nsfwPolicies.indexOf(value) !== -1 49 return exists(value) && nsfwPolicies.includes(value)
42} 50}
43 51
44function isUserWebTorrentEnabledValid (value: any) { 52function isUserWebTorrentEnabledValid (value: any) {
@@ -103,6 +111,7 @@ export {
103 isUserVideosHistoryEnabledValid, 111 isUserVideosHistoryEnabledValid,
104 isUserBlockedValid, 112 isUserBlockedValid,
105 isUserPasswordValid, 113 isUserPasswordValid,
114 isUserPasswordValidOrEmpty,
106 isUserVideoLanguages, 115 isUserVideoLanguages,
107 isUserBlockedReasonValid, 116 isUserBlockedReasonValid,
108 isUserRoleValid, 117 isUserRoleValid,
diff --git a/server/helpers/custom-validators/video-abuses.ts b/server/helpers/custom-validators/video-abuses.ts
index a9478c76a..05e11b1c6 100644
--- a/server/helpers/custom-validators/video-abuses.ts
+++ b/server/helpers/custom-validators/video-abuses.ts
@@ -1,8 +1,8 @@
1import { Response } from 'express'
2import validator from 'validator' 1import validator from 'validator'
2
3import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants' 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 { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type'
6 6
7const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES 7const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES
8 8
@@ -15,7 +15,14 @@ function isVideoAbuseModerationCommentValid (value: string) {
15} 15}
16 16
17function isVideoAbuseStateValid (value: string) { 17function isVideoAbuseStateValid (value: string) {
18 return exists(value) && VIDEO_ABUSE_STATES[ value ] !== undefined 18 return exists(value) && VIDEO_ABUSE_STATES[value] !== undefined
19}
20
21function isAbuseVideoIsValid (value: VideoAbuseVideoIs) {
22 return exists(value) && (
23 value === 'deleted' ||
24 value === 'blacklisted'
25 )
19} 26}
20 27
21// --------------------------------------------------------------------------- 28// ---------------------------------------------------------------------------
@@ -23,5 +30,6 @@ function isVideoAbuseStateValid (value: string) {
23export { 30export {
24 isVideoAbuseStateValid, 31 isVideoAbuseStateValid,
25 isVideoAbuseReasonValid, 32 isVideoAbuseReasonValid,
33 isAbuseVideoIsValid,
26 isVideoAbuseModerationCommentValid 34 isVideoAbuseModerationCommentValid
27} 35}
diff --git a/server/helpers/custom-validators/video-captions.ts b/server/helpers/custom-validators/video-captions.ts
index d06eb3695..528edf60c 100644
--- a/server/helpers/custom-validators/video-captions.ts
+++ b/server/helpers/custom-validators/video-captions.ts
@@ -2,13 +2,13 @@ import { CONSTRAINTS_FIELDS, MIMETYPES, VIDEO_LANGUAGES } from '../../initialize
2import { exists, isFileValid } from './misc' 2import { exists, isFileValid } from './misc'
3 3
4function isVideoCaptionLanguageValid (value: any) { 4function isVideoCaptionLanguageValid (value: any) {
5 return exists(value) && VIDEO_LANGUAGES[ value ] !== undefined 5 return exists(value) && VIDEO_LANGUAGES[value] !== undefined
6} 6}
7 7
8const videoCaptionTypes = Object.keys(MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT) 8const videoCaptionTypesRegex = Object.keys(MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT)
9 .concat([ 'application/octet-stream' ]) // MacOS sends application/octet-stream >< 9 .concat([ 'application/octet-stream' ]) // MacOS sends application/octet-stream
10 .map(m => `(${m})`) 10 .map(m => `(${m})`)
11const videoCaptionTypesRegex = videoCaptionTypes.join('|') 11 .join('|')
12function isVideoCaptionFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[], field: string) { 12function isVideoCaptionFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[], field: string) {
13 return isFileValid(files, videoCaptionTypesRegex, field, CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.FILE_SIZE.max) 13 return isFileValid(files, videoCaptionTypesRegex, field, CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.FILE_SIZE.max)
14} 14}
diff --git a/server/helpers/custom-validators/video-imports.ts b/server/helpers/custom-validators/video-imports.ts
index ffad482b4..33a1fa8ab 100644
--- a/server/helpers/custom-validators/video-imports.ts
+++ b/server/helpers/custom-validators/video-imports.ts
@@ -20,11 +20,13 @@ function isVideoImportTargetUrlValid (url: string) {
20} 20}
21 21
22function isVideoImportStateValid (value: any) { 22function isVideoImportStateValid (value: any) {
23 return exists(value) && VIDEO_IMPORT_STATES[ value ] !== undefined 23 return exists(value) && VIDEO_IMPORT_STATES[value] !== undefined
24} 24}
25 25
26const videoTorrentImportTypes = Object.keys(MIMETYPES.TORRENT.MIMETYPE_EXT).map(m => `(${m})`) 26const videoTorrentImportRegex = Object.keys(MIMETYPES.TORRENT.MIMETYPE_EXT)
27const videoTorrentImportRegex = videoTorrentImportTypes.join('|') 27 .concat([ 'application/octet-stream' ]) // MacOS sends application/octet-stream
28 .map(m => `(${m})`)
29 .join('|')
28function isVideoImportTorrentFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) { 30function isVideoImportTorrentFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) {
29 return isFileValid(files, videoTorrentImportRegex, 'torrentfile', CONSTRAINTS_FIELDS.VIDEO_IMPORTS.TORRENT_FILE.FILE_SIZE.max, true) 31 return isFileValid(files, videoTorrentImportRegex, 'torrentfile', CONSTRAINTS_FIELDS.VIDEO_IMPORTS.TORRENT_FILE.FILE_SIZE.max, true)
30} 32}
diff --git a/server/helpers/custom-validators/video-playlists.ts b/server/helpers/custom-validators/video-playlists.ts
index 4bb8384ab..180018fc5 100644
--- a/server/helpers/custom-validators/video-playlists.ts
+++ b/server/helpers/custom-validators/video-playlists.ts
@@ -1,8 +1,6 @@
1import { exists } from './misc' 1import { exists } from './misc'
2import validator from 'validator' 2import validator from 'validator'
3import { CONSTRAINTS_FIELDS, VIDEO_PLAYLIST_PRIVACIES, VIDEO_PLAYLIST_TYPES } from '../../initializers/constants' 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 4
7const PLAYLISTS_CONSTRAINT_FIELDS = CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS 5const PLAYLISTS_CONSTRAINT_FIELDS = CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS
8 6
@@ -15,7 +13,7 @@ function isVideoPlaylistDescriptionValid (value: any) {
15} 13}
16 14
17function isVideoPlaylistPrivacyValid (value: number) { 15function isVideoPlaylistPrivacyValid (value: number) {
18 return validator.isInt(value + '') && VIDEO_PLAYLIST_PRIVACIES[ value ] !== undefined 16 return validator.isInt(value + '') && VIDEO_PLAYLIST_PRIVACIES[value] !== undefined
19} 17}
20 18
21function isVideoPlaylistTimestampValid (value: any) { 19function isVideoPlaylistTimestampValid (value: any) {
@@ -23,7 +21,7 @@ function isVideoPlaylistTimestampValid (value: any) {
23} 21}
24 22
25function isVideoPlaylistTypeValid (value: any) { 23function isVideoPlaylistTypeValid (value: any) {
26 return exists(value) && VIDEO_PLAYLIST_TYPES[ value ] !== undefined 24 return exists(value) && VIDEO_PLAYLIST_TYPES[value] !== undefined
27} 25}
28 26
29// --------------------------------------------------------------------------- 27// ---------------------------------------------------------------------------
diff --git a/server/helpers/custom-validators/video-redundancies.ts b/server/helpers/custom-validators/video-redundancies.ts
new file mode 100644
index 000000000..50a559c4f
--- /dev/null
+++ b/server/helpers/custom-validators/video-redundancies.ts
@@ -0,0 +1,12 @@
1import { exists } from './misc'
2
3function isVideoRedundancyTarget (value: any) {
4 return exists(value) &&
5 (value === 'my-videos' || value === 'remote-videos')
6}
7
8// ---------------------------------------------------------------------------
9
10export {
11 isVideoRedundancyTarget
12}
diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts
index a9e859e54..60e8075f6 100644
--- a/server/helpers/custom-validators/videos.ts
+++ b/server/helpers/custom-validators/videos.ts
@@ -20,15 +20,15 @@ function isVideoFilterValid (filter: VideoFilter) {
20} 20}
21 21
22function isVideoCategoryValid (value: any) { 22function isVideoCategoryValid (value: any) {
23 return value === null || VIDEO_CATEGORIES[ value ] !== undefined 23 return value === null || VIDEO_CATEGORIES[value] !== undefined
24} 24}
25 25
26function isVideoStateValid (value: any) { 26function isVideoStateValid (value: any) {
27 return exists(value) && VIDEO_STATES[ value ] !== undefined 27 return exists(value) && VIDEO_STATES[value] !== undefined
28} 28}
29 29
30function isVideoLicenceValid (value: any) { 30function isVideoLicenceValid (value: any) {
31 return value === null || VIDEO_LICENCES[ value ] !== undefined 31 return value === null || VIDEO_LICENCES[value] !== undefined
32} 32}
33 33
34function isVideoLanguageValid (value: any) { 34function isVideoLanguageValid (value: any) {
@@ -73,7 +73,7 @@ function isVideoViewsValid (value: string) {
73} 73}
74 74
75function isVideoRatingTypeValid (value: string) { 75function isVideoRatingTypeValid (value: string) {
76 return value === 'none' || values(VIDEO_RATE_TYPES).indexOf(value as VideoRateType) !== -1 76 return value === 'none' || values(VIDEO_RATE_TYPES).includes(value as VideoRateType)
77} 77}
78 78
79function isVideoFileExtnameValid (value: string) { 79function isVideoFileExtnameValid (value: string) {
@@ -98,7 +98,7 @@ function isVideoImage (files: { [ fieldname: string ]: Express.Multer.File[] } |
98} 98}
99 99
100function isVideoPrivacyValid (value: number) { 100function isVideoPrivacyValid (value: number) {
101 return VIDEO_PRIVACIES[ value ] !== undefined 101 return VIDEO_PRIVACIES[value] !== undefined
102} 102}
103 103
104function isScheduleVideoUpdatePrivacyValid (value: number) { 104function isScheduleVideoUpdatePrivacyValid (value: number) {
diff --git a/server/helpers/express-utils.ts b/server/helpers/express-utils.ts
index 9bf6d85a8..f46812977 100644
--- a/server/helpers/express-utils.ts
+++ b/server/helpers/express-utils.ts
@@ -12,7 +12,7 @@ function buildNSFWFilter (res?: express.Response, paramNSFW?: string) {
12 if (paramNSFW === 'false') return false 12 if (paramNSFW === 'false') return false
13 if (paramNSFW === 'both') return undefined 13 if (paramNSFW === 'both') return undefined
14 14
15 if (res && res.locals.oauth) { 15 if (res?.locals.oauth) {
16 const user = 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
@@ -28,7 +28,7 @@ function buildNSFWFilter (res?: express.Response, paramNSFW?: string) {
28 return null 28 return null
29} 29}
30 30
31function cleanUpReqFiles (req: { files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[] }) { 31function cleanUpReqFiles (req: { files: { [fieldname: string]: Express.Multer.File[] } | Express.Multer.File[] }) {
32 const files = req.files 32 const files = req.files
33 33
34 if (!files) return 34 if (!files) return
@@ -39,7 +39,7 @@ function cleanUpReqFiles (req: { files: { [ fieldname: string ]: Express.Multer.
39 } 39 }
40 40
41 for (const key of Object.keys(files)) { 41 for (const key of Object.keys(files)) {
42 const file = files[ key ] 42 const file = files[key]
43 43
44 if (isArray(file)) file.forEach(f => deleteFileAsync(f.path)) 44 if (isArray(file)) file.forEach(f => deleteFileAsync(f.path))
45 else deleteFileAsync(file.path) 45 else deleteFileAsync(file.path)
@@ -65,18 +65,18 @@ function badRequest (req: express.Request, res: express.Response) {
65 65
66function createReqFiles ( 66function createReqFiles (
67 fieldNames: string[], 67 fieldNames: string[],
68 mimeTypes: { [ id: string ]: string }, 68 mimeTypes: { [id: string]: string },
69 destinations: { [ fieldName: string ]: string } 69 destinations: { [fieldName: string]: string }
70) { 70) {
71 const storage = multer.diskStorage({ 71 const storage = multer.diskStorage({
72 destination: (req, file, cb) => { 72 destination: (req, file, cb) => {
73 cb(null, destinations[ file.fieldname ]) 73 cb(null, destinations[file.fieldname])
74 }, 74 },
75 75
76 filename: async (req, file, cb) => { 76 filename: async (req, file, cb) => {
77 let extension: string 77 let extension: string
78 const fileExtension = extname(file.originalname) 78 const fileExtension = extname(file.originalname)
79 const extensionFromMimetype = mimeTypes[ file.mimetype ] 79 const extensionFromMimetype = mimeTypes[file.mimetype]
80 80
81 // Take the file extension if we don't understand the mime type 81 // Take the file extension if we don't understand the mime type
82 // We have the OGG/OGV exception too because firefox sends a bad mime type when sending an OGG file 82 // We have the OGG/OGV exception too because firefox sends a bad mime type when sending an OGG file
@@ -99,7 +99,7 @@ function createReqFiles (
99 } 99 }
100 }) 100 })
101 101
102 let fields: { name: string, maxCount: number }[] = [] 102 const fields: { name: string, maxCount: number }[] = []
103 for (const fieldName of fieldNames) { 103 for (const fieldName of fieldNames) {
104 fields.push({ 104 fields.push({
105 name: fieldName, 105 name: fieldName,
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts
index 00c32e99a..557fb5e3a 100644
--- a/server/helpers/ffmpeg-utils.ts
+++ b/server/helpers/ffmpeg-utils.ts
@@ -1,12 +1,78 @@
1import * as ffmpeg from 'fluent-ffmpeg' 1import * as ffmpeg from 'fluent-ffmpeg'
2import { dirname, join } from 'path' 2import { dirname, join } from 'path'
3import { getTargetBitrate, getMaxBitrate, VideoResolution } from '../../shared/models/videos' 3import { getMaxBitrate, getTargetBitrate, VideoResolution } from '../../shared/models/videos'
4import { 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 { readFile, remove, writeFile } from 'fs-extra' 8import { readFile, remove, writeFile } from 'fs-extra'
9import { CONFIG } from '../initializers/config' 9import { CONFIG } from '../initializers/config'
10import { VideoFileMetadata } from '@shared/models/videos/video-file-metadata'
11
12/**
13 * A toolbox to play with audio
14 */
15namespace audio {
16 export const get = (videoPath: string) => {
17 // without position, ffprobe considers the last input only
18 // we make it consider the first input only
19 // if you pass a file path to pos, then ffprobe acts on that file directly
20 return new Promise<{ absolutePath: string, audioStream?: any }>((res, rej) => {
21
22 function parseFfprobe (err: any, data: ffmpeg.FfprobeData) {
23 if (err) return rej(err)
24
25 if ('streams' in data) {
26 const audioStream = data.streams.find(stream => stream['codec_type'] === 'audio')
27 if (audioStream) {
28 return res({
29 absolutePath: data.format.filename,
30 audioStream
31 })
32 }
33 }
34
35 return res({ absolutePath: data.format.filename })
36 }
37
38 return ffmpeg.ffprobe(videoPath, parseFfprobe)
39 })
40 }
41
42 export namespace bitrate {
43 const baseKbitrate = 384
44
45 const toBits = (kbits: number) => kbits * 8000
46
47 export const aac = (bitrate: number): number => {
48 switch (true) {
49 case bitrate > toBits(baseKbitrate):
50 return baseKbitrate
51
52 default:
53 return -1 // we interpret it as a signal to copy the audio stream as is
54 }
55 }
56
57 export const mp3 = (bitrate: number): number => {
58 /*
59 a 192kbit/sec mp3 doesn't hold as much information as a 192kbit/sec aac.
60 That's why, when using aac, we can go to lower kbit/sec. The equivalences
61 made here are not made to be accurate, especially with good mp3 encoders.
62 */
63 switch (true) {
64 case bitrate <= toBits(192):
65 return 128
66
67 case bitrate <= toBits(384):
68 return 256
69
70 default:
71 return baseKbitrate
72 }
73 }
74 }
75}
10 76
11function computeResolutionsToTranscode (videoFileHeight: number) { 77function computeResolutionsToTranscode (videoFileHeight: number) {
12 const resolutionsEnabled: number[] = [] 78 const resolutionsEnabled: number[] = []
@@ -24,7 +90,7 @@ function computeResolutionsToTranscode (videoFileHeight: number) {
24 ] 90 ]
25 91
26 for (const resolution of resolutions) { 92 for (const resolution of resolutions) {
27 if (configResolutions[ resolution + 'p' ] === true && videoFileHeight > resolution) { 93 if (configResolutions[resolution + 'p'] === true && videoFileHeight > resolution) {
28 resolutionsEnabled.push(resolution) 94 resolutionsEnabled.push(resolution)
29 } 95 }
30 } 96 }
@@ -48,9 +114,9 @@ async function getVideoStreamCodec (path: string) {
48 const videoCodec = videoStream.codec_tag_string 114 const videoCodec = videoStream.codec_tag_string
49 115
50 const baseProfileMatrix = { 116 const baseProfileMatrix = {
51 'High': '6400', 117 High: '6400',
52 'Main': '4D40', 118 Main: '4D40',
53 'Baseline': '42E0' 119 Baseline: '42E0'
54 } 120 }
55 121
56 let baseProfile = baseProfileMatrix[videoStream.profile] 122 let baseProfile = baseProfileMatrix[videoStream.profile]
@@ -59,7 +125,8 @@ async function getVideoStreamCodec (path: string) {
59 baseProfile = baseProfileMatrix['High'] // Fallback 125 baseProfile = baseProfileMatrix['High'] // Fallback
60 } 126 }
61 127
62 const level = videoStream.level.toString(16) 128 let level = videoStream.level.toString(16)
129 if (level.length === 1) level = `0${level}`
63 130
64 return `${videoCodec}.${baseProfile}${level}` 131 return `${videoCodec}.${baseProfile}${level}`
65} 132}
@@ -91,7 +158,7 @@ async function getVideoFileFPS (path: string) {
91 if (videoStream === null) return 0 158 if (videoStream === null) return 0
92 159
93 for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) { 160 for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) {
94 const valuesText: string = videoStream[ key ] 161 const valuesText: string = videoStream[key]
95 if (!valuesText) continue 162 if (!valuesText) continue
96 163
97 const [ frames, seconds ] = valuesText.split('/') 164 const [ frames, seconds ] = valuesText.split('/')
@@ -104,24 +171,26 @@ async function getVideoFileFPS (path: string) {
104 return 0 171 return 0
105} 172}
106 173
107async function getVideoFileBitrate (path: string) { 174async function getMetadataFromFile <T> (path: string, cb = metadata => metadata) {
108 return new Promise<number>((res, rej) => { 175 return new Promise<T>((res, rej) => {
109 ffmpeg.ffprobe(path, (err, metadata) => { 176 ffmpeg.ffprobe(path, (err, metadata) => {
110 if (err) return rej(err) 177 if (err) return rej(err)
111 178
112 return res(metadata.format.bit_rate) 179 return res(cb(new VideoFileMetadata(metadata)))
113 }) 180 })
114 }) 181 })
115} 182}
116 183
184async function getVideoFileBitrate (path: string) {
185 return getMetadataFromFile<number>(path, metadata => metadata.format.bit_rate)
186}
187
117function getDurationFromVideoFile (path: string) { 188function getDurationFromVideoFile (path: string) {
118 return new Promise<number>((res, rej) => { 189 return getMetadataFromFile<number>(path, metadata => Math.floor(metadata.format.duration))
119 ffmpeg.ffprobe(path, (err, metadata) => { 190}
120 if (err) return rej(err)
121 191
122 return res(Math.floor(metadata.format.duration)) 192function getVideoStreamFromFile (path: string) {
123 }) 193 return getMetadataFromFile<any>(path, metadata => metadata.streams.find(s => s.codec_type === 'video') || null)
124 })
125} 194}
126 195
127async function generateImageFromVideoFile (fromPath: string, folder: string, imageName: string, size: { width: number, height: number }) { 196async function generateImageFromVideoFile (fromPath: string, folder: string, imageName: string, size: { width: number, height: number }) {
@@ -191,7 +260,8 @@ interface OnlyAudioTranscodeOptions extends BaseTranscodeOptions {
191 type: 'only-audio' 260 type: 'only-audio'
192} 261}
193 262
194type TranscodeOptions = HLSTranscodeOptions 263type TranscodeOptions =
264 HLSTranscodeOptions
195 | VideoTranscodeOptions 265 | VideoTranscodeOptions
196 | MergeAudioTranscodeOptions 266 | MergeAudioTranscodeOptions
197 | OnlyAudioTranscodeOptions 267 | OnlyAudioTranscodeOptions
@@ -204,13 +274,13 @@ function transcode (options: TranscodeOptions) {
204 .output(options.outputPath) 274 .output(options.outputPath)
205 275
206 if (options.type === 'quick-transcode') { 276 if (options.type === 'quick-transcode') {
207 command = await buildQuickTranscodeCommand(command) 277 command = buildQuickTranscodeCommand(command)
208 } else if (options.type === 'hls') { 278 } else if (options.type === 'hls') {
209 command = await buildHLSCommand(command, options) 279 command = await buildHLSCommand(command, options)
210 } else if (options.type === 'merge-audio') { 280 } else if (options.type === 'merge-audio') {
211 command = await buildAudioMergeCommand(command, options) 281 command = await buildAudioMergeCommand(command, options)
212 } else if (options.type === 'only-audio') { 282 } else if (options.type === 'only-audio') {
213 command = await buildOnlyAudioCommand(command, options) 283 command = buildOnlyAudioCommand(command, options)
214 } else { 284 } else {
215 command = await buildx264Command(command, options) 285 command = await buildx264Command(command, options)
216 } 286 }
@@ -247,22 +317,27 @@ async function canDoQuickTranscode (path: string): Promise<boolean> {
247 317
248 // check video params 318 // check video params
249 if (videoStream == null) return false 319 if (videoStream == null) return false
250 if (videoStream[ 'codec_name' ] !== 'h264') return false 320 if (videoStream['codec_name'] !== 'h264') return false
251 if (videoStream[ 'pix_fmt' ] !== 'yuv420p') return false 321 if (videoStream['pix_fmt'] !== 'yuv420p') return false
252 if (fps < VIDEO_TRANSCODING_FPS.MIN || fps > VIDEO_TRANSCODING_FPS.MAX) return false 322 if (fps < VIDEO_TRANSCODING_FPS.MIN || fps > VIDEO_TRANSCODING_FPS.MAX) return false
253 if (bitRate > getMaxBitrate(resolution.videoFileResolution, fps, VIDEO_TRANSCODING_FPS)) return false 323 if (bitRate > getMaxBitrate(resolution.videoFileResolution, fps, VIDEO_TRANSCODING_FPS)) return false
254 324
255 // check audio params (if audio stream exists) 325 // check audio params (if audio stream exists)
256 if (parsedAudio.audioStream) { 326 if (parsedAudio.audioStream) {
257 if (parsedAudio.audioStream[ 'codec_name' ] !== 'aac') return false 327 if (parsedAudio.audioStream['codec_name'] !== 'aac') return false
258 328
259 const maxAudioBitrate = audio.bitrate[ 'aac' ](parsedAudio.audioStream[ 'bit_rate' ]) 329 const maxAudioBitrate = audio.bitrate['aac'](parsedAudio.audioStream['bit_rate'])
260 if (maxAudioBitrate !== -1 && parsedAudio.audioStream[ 'bit_rate' ] > maxAudioBitrate) return false 330 if (maxAudioBitrate !== -1 && parsedAudio.audioStream['bit_rate'] > maxAudioBitrate) return false
261 } 331 }
262 332
263 return true 333 return true
264} 334}
265 335
336function getClosestFramerateStandard (fps: number, type: 'HD_STANDARD' | 'STANDARD'): number {
337 return VIDEO_TRANSCODING_FPS[type].slice(0)
338 .sort((a, b) => fps % a - fps % b)[0]
339}
340
266// --------------------------------------------------------------------------- 341// ---------------------------------------------------------------------------
267 342
268export { 343export {
@@ -270,6 +345,7 @@ export {
270 getAudioStreamCodec, 345 getAudioStreamCodec,
271 getVideoStreamSize, 346 getVideoStreamSize,
272 getVideoFileResolution, 347 getVideoFileResolution,
348 getMetadataFromFile,
273 getDurationFromVideoFile, 349 getDurationFromVideoFile,
274 generateImageFromVideoFile, 350 generateImageFromVideoFile,
275 TranscodeOptions, 351 TranscodeOptions,
@@ -286,13 +362,14 @@ export {
286 362
287async function buildx264Command (command: ffmpeg.FfmpegCommand, options: TranscodeOptions) { 363async function buildx264Command (command: ffmpeg.FfmpegCommand, options: TranscodeOptions) {
288 let fps = await getVideoFileFPS(options.inputPath) 364 let fps = await getVideoFileFPS(options.inputPath)
289 // On small/medium resolutions, limit FPS
290 if ( 365 if (
366 // On small/medium resolutions, limit FPS
291 options.resolution !== undefined && 367 options.resolution !== undefined &&
292 options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN && 368 options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN &&
293 fps > VIDEO_TRANSCODING_FPS.AVERAGE 369 fps > VIDEO_TRANSCODING_FPS.AVERAGE
294 ) { 370 ) {
295 fps = VIDEO_TRANSCODING_FPS.AVERAGE 371 // Get closest standard framerate by modulo: downsampling has to be done to a divisor of the nominal fps value
372 fps = getClosestFramerateStandard(fps, 'STANDARD')
296 } 373 }
297 374
298 command = await presetH264(command, options.inputPath, options.resolution, fps) 375 command = await presetH264(command, options.inputPath, options.resolution, fps)
@@ -305,7 +382,7 @@ async function buildx264Command (command: ffmpeg.FfmpegCommand, options: Transco
305 382
306 if (fps) { 383 if (fps) {
307 // Hard FPS limits 384 // Hard FPS limits
308 if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = VIDEO_TRANSCODING_FPS.MAX 385 if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = getClosestFramerateStandard(fps, 'HD_STANDARD')
309 else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN 386 else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN
310 387
311 command = command.withFPS(fps) 388 command = command.withFPS(fps)
@@ -327,14 +404,14 @@ async function buildAudioMergeCommand (command: ffmpeg.FfmpegCommand, options: M
327 return command 404 return command
328} 405}
329 406
330async function buildOnlyAudioCommand (command: ffmpeg.FfmpegCommand, options: OnlyAudioTranscodeOptions) { 407function buildOnlyAudioCommand (command: ffmpeg.FfmpegCommand, options: OnlyAudioTranscodeOptions) {
331 command = await presetOnlyAudio(command) 408 command = presetOnlyAudio(command)
332 409
333 return command 410 return command
334} 411}
335 412
336async function buildQuickTranscodeCommand (command: ffmpeg.FfmpegCommand) { 413function buildQuickTranscodeCommand (command: ffmpeg.FfmpegCommand) {
337 command = await presetCopy(command) 414 command = presetCopy(command)
338 415
339 command = command.outputOption('-map_metadata -1') // strip all metadata 416 command = command.outputOption('-map_metadata -1') // strip all metadata
340 .outputOption('-movflags faststart') 417 .outputOption('-movflags faststart')
@@ -345,7 +422,8 @@ async function buildQuickTranscodeCommand (command: ffmpeg.FfmpegCommand) {
345async function buildHLSCommand (command: ffmpeg.FfmpegCommand, options: HLSTranscodeOptions) { 422async function buildHLSCommand (command: ffmpeg.FfmpegCommand, options: HLSTranscodeOptions) {
346 const videoPath = getHLSVideoPath(options) 423 const videoPath = getHLSVideoPath(options)
347 424
348 if (options.copyCodecs) command = await presetCopy(command) 425 if (options.copyCodecs) command = presetCopy(command)
426 else if (options.resolution === VideoResolution.H_NOVIDEO) command = presetOnlyAudio(command)
349 else command = await buildx264Command(command, options) 427 else command = await buildx264Command(command, options)
350 428
351 command = command.outputOption('-hls_time 4') 429 command = command.outputOption('-hls_time 4')
@@ -378,17 +456,6 @@ async function fixHLSPlaylistIfNeeded (options: TranscodeOptions) {
378 await writeFile(options.outputPath, newContent) 456 await writeFile(options.outputPath, newContent)
379} 457}
380 458
381function getVideoStreamFromFile (path: string) {
382 return new Promise<any>((res, rej) => {
383 ffmpeg.ffprobe(path, (err, metadata) => {
384 if (err) return rej(err)
385
386 const videoStream = metadata.streams.find(s => s.codec_type === 'video')
387 return res(videoStream || null)
388 })
389 })
390}
391
392/** 459/**
393 * A slightly customised version of the 'veryfast' x264 preset 460 * A slightly customised version of the 'veryfast' x264 preset
394 * 461 *
@@ -413,71 +480,6 @@ async function presetH264VeryFast (command: ffmpeg.FfmpegCommand, input: string,
413} 480}
414 481
415/** 482/**
416 * A toolbox to play with audio
417 */
418namespace audio {
419 export const get = (videoPath: string) => {
420 // without position, ffprobe considers the last input only
421 // we make it consider the first input only
422 // if you pass a file path to pos, then ffprobe acts on that file directly
423 return new Promise<{ absolutePath: string, audioStream?: any }>((res, rej) => {
424
425 function parseFfprobe (err: any, data: ffmpeg.FfprobeData) {
426 if (err) return rej(err)
427
428 if ('streams' in data) {
429 const audioStream = data.streams.find(stream => stream[ 'codec_type' ] === 'audio')
430 if (audioStream) {
431 return res({
432 absolutePath: data.format.filename,
433 audioStream
434 })
435 }
436 }
437
438 return res({ absolutePath: data.format.filename })
439 }
440
441 return ffmpeg.ffprobe(videoPath, parseFfprobe)
442 })
443 }
444
445 export namespace bitrate {
446 const baseKbitrate = 384
447
448 const toBits = (kbits: number) => kbits * 8000
449
450 export const aac = (bitrate: number): number => {
451 switch (true) {
452 case bitrate > toBits(baseKbitrate):
453 return baseKbitrate
454
455 default:
456 return -1 // we interpret it as a signal to copy the audio stream as is
457 }
458 }
459
460 export const mp3 = (bitrate: number): number => {
461 /*
462 a 192kbit/sec mp3 doesn't hold as much information as a 192kbit/sec aac.
463 That's why, when using aac, we can go to lower kbit/sec. The equivalences
464 made here are not made to be accurate, especially with good mp3 encoders.
465 */
466 switch (true) {
467 case bitrate <= toBits(192):
468 return 128
469
470 case bitrate <= toBits(384):
471 return 256
472
473 default:
474 return baseKbitrate
475 }
476 }
477 }
478}
479
480/**
481 * Standard profile, with variable bitrate audio and faststart. 483 * Standard profile, with variable bitrate audio and faststart.
482 * 484 *
483 * As for the audio, quality '5' is the highest and ensures 96-112kbps/channel 485 * As for the audio, quality '5' is the highest and ensures 96-112kbps/channel
@@ -507,10 +509,10 @@ async function presetH264 (command: ffmpeg.FfmpegCommand, input: string, resolut
507 // of course this is far from perfect, but it might save some space in the end 509 // of course this is far from perfect, but it might save some space in the end
508 localCommand = localCommand.audioCodec('aac') 510 localCommand = localCommand.audioCodec('aac')
509 511
510 const audioCodecName = parsedAudio.audioStream[ 'codec_name' ] 512 const audioCodecName = parsedAudio.audioStream['codec_name']
511 513
512 if (audio.bitrate[ audioCodecName ]) { 514 if (audio.bitrate[audioCodecName]) {
513 const bitrate = audio.bitrate[ audioCodecName ](parsedAudio.audioStream[ 'bit_rate' ]) 515 const bitrate = audio.bitrate[audioCodecName](parsedAudio.audioStream['bit_rate'])
514 if (bitrate !== undefined && bitrate !== -1) localCommand = localCommand.audioBitrate(bitrate) 516 if (bitrate !== undefined && bitrate !== -1) localCommand = localCommand.audioBitrate(bitrate)
515 } 517 }
516 } 518 }
@@ -531,14 +533,14 @@ async function presetH264 (command: ffmpeg.FfmpegCommand, input: string, resolut
531 return localCommand 533 return localCommand
532} 534}
533 535
534async function presetCopy (command: ffmpeg.FfmpegCommand): Promise<ffmpeg.FfmpegCommand> { 536function presetCopy (command: ffmpeg.FfmpegCommand): ffmpeg.FfmpegCommand {
535 return command 537 return command
536 .format('mp4') 538 .format('mp4')
537 .videoCodec('copy') 539 .videoCodec('copy')
538 .audioCodec('copy') 540 .audioCodec('copy')
539} 541}
540 542
541async function presetOnlyAudio (command: ffmpeg.FfmpegCommand): Promise<ffmpeg.FfmpegCommand> { 543function presetOnlyAudio (command: ffmpeg.FfmpegCommand): ffmpeg.FfmpegCommand {
542 return command 544 return command
543 .format('mp4') 545 .format('mp4')
544 .audioCodec('copy') 546 .audioCodec('copy')
diff --git a/server/helpers/logger.ts b/server/helpers/logger.ts
index 395417612..9553f70e8 100644
--- a/server/helpers/logger.ts
+++ b/server/helpers/logger.ts
@@ -5,7 +5,7 @@ import * as winston from 'winston'
5import { FileTransportOptions } from 'winston/lib/winston/transports' 5import { FileTransportOptions } from 'winston/lib/winston/transports'
6import { CONFIG } from '../initializers/config' 6import { CONFIG } from '../initializers/config'
7import { omit } from 'lodash' 7import { omit } from 'lodash'
8import { LOG_FILENAME } from '@server/initializers/constants' 8import { LOG_FILENAME } from '../initializers/constants'
9 9
10const label = CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT 10const label = CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
11 11
@@ -27,7 +27,7 @@ function getLoggerReplacer () {
27 if (value instanceof Error) { 27 if (value instanceof Error) {
28 const error = {} 28 const error = {}
29 29
30 Object.getOwnPropertyNames(value).forEach(key => error[ key ] = value[ key ]) 30 Object.getOwnPropertyNames(value).forEach(key => { error[key] = value[key] })
31 31
32 return error 32 return error
33 } 33 }
@@ -54,9 +54,11 @@ const jsonLoggerFormat = winston.format.printf(info => {
54const timestampFormatter = winston.format.timestamp({ 54const timestampFormatter = winston.format.timestamp({
55 format: 'YYYY-MM-DD HH:mm:ss.SSS' 55 format: 'YYYY-MM-DD HH:mm:ss.SSS'
56}) 56})
57const labelFormatter = winston.format.label({ 57const labelFormatter = (suffix?: string) => {
58 label 58 return winston.format.label({
59}) 59 label: suffix ? `${label} ${suffix}` : label
60 })
61}
60 62
61const fileLoggerOptions: FileTransportOptions = { 63const fileLoggerOptions: FileTransportOptions = {
62 filename: path.join(CONFIG.STORAGE.LOG_DIR, LOG_FILENAME), 64 filename: path.join(CONFIG.STORAGE.LOG_DIR, LOG_FILENAME),
@@ -72,25 +74,29 @@ if (CONFIG.LOG.ROTATION.ENABLED) {
72 fileLoggerOptions.maxFiles = CONFIG.LOG.ROTATION.MAX_FILES 74 fileLoggerOptions.maxFiles = CONFIG.LOG.ROTATION.MAX_FILES
73} 75}
74 76
75const logger = winston.createLogger({ 77const logger = buildLogger()
76 level: CONFIG.LOG.LEVEL, 78
77 format: winston.format.combine( 79function buildLogger (labelSuffix?: string) {
78 labelFormatter, 80 return winston.createLogger({
79 winston.format.splat() 81 level: CONFIG.LOG.LEVEL,
80 ), 82 format: winston.format.combine(
81 transports: [ 83 labelFormatter(labelSuffix),
82 new winston.transports.File(fileLoggerOptions), 84 winston.format.splat()
83 new winston.transports.Console({ 85 ),
84 handleExceptions: true, 86 transports: [
85 format: winston.format.combine( 87 new winston.transports.File(fileLoggerOptions),
86 timestampFormatter, 88 new winston.transports.Console({
87 winston.format.colorize(), 89 handleExceptions: true,
88 consoleLoggerFormat 90 format: winston.format.combine(
89 ) 91 timestampFormatter,
90 }) 92 winston.format.colorize(),
91 ], 93 consoleLoggerFormat
92 exitOnError: true 94 )
93}) 95 })
96 ],
97 exitOnError: true
98 })
99}
94 100
95function bunyanLogFactory (level: string) { 101function bunyanLogFactory (level: string) {
96 return function () { 102 return function () {
@@ -98,19 +104,20 @@ function bunyanLogFactory (level: string) {
98 let args: any[] = [] 104 let args: any[] = []
99 args.concat(arguments) 105 args.concat(arguments)
100 106
101 if (arguments[ 0 ] instanceof Error) { 107 if (arguments[0] instanceof Error) {
102 meta = arguments[ 0 ].toString() 108 meta = arguments[0].toString()
103 args = Array.prototype.slice.call(arguments, 1) 109 args = Array.prototype.slice.call(arguments, 1)
104 args.push(meta) 110 args.push(meta)
105 } else if (typeof (args[ 0 ]) !== 'string') { 111 } else if (typeof (args[0]) !== 'string') {
106 meta = arguments[ 0 ] 112 meta = arguments[0]
107 args = Array.prototype.slice.call(arguments, 1) 113 args = Array.prototype.slice.call(arguments, 1)
108 args.push(meta) 114 args.push(meta)
109 } 115 }
110 116
111 logger[ level ].apply(logger, args) 117 logger[level].apply(logger, args)
112 } 118 }
113} 119}
120
114const bunyanLogger = { 121const bunyanLogger = {
115 trace: bunyanLogFactory('debug'), 122 trace: bunyanLogFactory('debug'),
116 debug: bunyanLogFactory('debug'), 123 debug: bunyanLogFactory('debug'),
@@ -122,6 +129,7 @@ const bunyanLogger = {
122// --------------------------------------------------------------------------- 129// ---------------------------------------------------------------------------
123 130
124export { 131export {
132 buildLogger,
125 timestampFormatter, 133 timestampFormatter,
126 labelFormatter, 134 labelFormatter,
127 consoleLoggerFormat, 135 consoleLoggerFormat,
diff --git a/server/helpers/middlewares/video-abuses.ts b/server/helpers/middlewares/video-abuses.ts
index 8a1d3d618..97a5724b6 100644
--- a/server/helpers/middlewares/video-abuses.ts
+++ b/server/helpers/middlewares/video-abuses.ts
@@ -1,9 +1,17 @@
1import { Response } from 'express' 1import { Response } from 'express'
2import { VideoAbuseModel } from '../../models/video/video-abuse' 2import { VideoAbuseModel } from '../../models/video/video-abuse'
3import { fetchVideo } from '../video'
3 4
4async function doesVideoAbuseExist (abuseIdArg: number | string, videoId: number, res: Response) { 5async function doesVideoAbuseExist (abuseIdArg: number | string, videoUUID: string, res: Response) {
5 const abuseId = parseInt(abuseIdArg + '', 10) 6 const abuseId = parseInt(abuseIdArg + '', 10)
6 const videoAbuse = await VideoAbuseModel.loadByIdAndVideoId(abuseId, videoId) 7 let videoAbuse = await VideoAbuseModel.loadByIdAndVideoId(abuseId, null, videoUUID)
8
9 if (!videoAbuse) {
10 const userId = res.locals.oauth?.token.User.id
11 const video = await fetchVideo(videoUUID, 'all', userId)
12
13 if (video) videoAbuse = await VideoAbuseModel.loadByIdAndVideoId(abuseId, video.id)
14 }
7 15
8 if (videoAbuse === null) { 16 if (videoAbuse === null) {
9 res.status(404) 17 res.status(404)
diff --git a/server/helpers/middlewares/videos.ts b/server/helpers/middlewares/videos.ts
index 74f529804..a0bbcdb21 100644
--- a/server/helpers/middlewares/videos.ts
+++ b/server/helpers/middlewares/videos.ts
@@ -2,7 +2,17 @@ import { Response } from 'express'
2import { fetchVideo, VideoFetchType } from '../video' 2import { fetchVideo, VideoFetchType } from '../video'
3import { UserRight } from '../../../shared/models/users' 3import { UserRight } from '../../../shared/models/users'
4import { VideoChannelModel } from '../../models/video/video-channel' 4import { VideoChannelModel } from '../../models/video/video-channel'
5import { MUser, MUserAccountId, MVideoAccountLight, MVideoFullLight, MVideoThumbnail, MVideoWithRights } from '@server/typings/models' 5import {
6 MUser,
7 MUserAccountId,
8 MVideoAccountLight,
9 MVideoFullLight,
10 MVideoIdThumbnail,
11 MVideoImmutable,
12 MVideoThumbnail,
13 MVideoWithRights
14} from '@server/typings/models'
15import { VideoFileModel } from '@server/models/video/video-file'
6 16
7async function doesVideoExist (id: number | string, res: Response, fetchType: VideoFetchType = 'all') { 17async function doesVideoExist (id: number | string, res: Response, fetchType: VideoFetchType = 'all') {
8 const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined 18 const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined
@@ -22,8 +32,12 @@ async function doesVideoExist (id: number | string, res: Response, fetchType: Vi
22 res.locals.videoAll = video as MVideoFullLight 32 res.locals.videoAll = video as MVideoFullLight
23 break 33 break
24 34
35 case 'only-immutable-attributes':
36 res.locals.onlyImmutableVideo = video as MVideoImmutable
37 break
38
25 case 'id': 39 case 'id':
26 res.locals.videoId = video 40 res.locals.videoId = video as MVideoIdThumbnail
27 break 41 break
28 42
29 case 'only-video': 43 case 'only-video':
@@ -38,6 +52,18 @@ async function doesVideoExist (id: number | string, res: Response, fetchType: Vi
38 return true 52 return true
39} 53}
40 54
55async function doesVideoFileOfVideoExist (id: number, videoIdOrUUID: number | string, res: Response) {
56 if (!await VideoFileModel.doesVideoExistForVideoFile(id, videoIdOrUUID)) {
57 res.status(404)
58 .json({ error: 'VideoFile matching Video not found' })
59 .end()
60
61 return false
62 }
63
64 return true
65}
66
41async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAccountId, res: Response) { 67async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAccountId, res: Response) {
42 if (user.hasRight(UserRight.UPDATE_ANY_VIDEO) === true) { 68 if (user.hasRight(UserRight.UPDATE_ANY_VIDEO) === true) {
43 const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId) 69 const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId)
@@ -94,5 +120,6 @@ function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right:
94export { 120export {
95 doesVideoChannelOfAccountExist, 121 doesVideoChannelOfAccountExist,
96 doesVideoExist, 122 doesVideoExist,
123 doesVideoFileOfVideoExist,
97 checkUserCanManageVideo 124 checkUserCanManageVideo
98} 125}
diff --git a/server/helpers/peertube-crypto.ts b/server/helpers/peertube-crypto.ts
index 89c0ab151..394e97fd5 100644
--- a/server/helpers/peertube-crypto.ts
+++ b/server/helpers/peertube-crypto.ts
@@ -5,7 +5,6 @@ import { jsonld } from './custom-jsonld-signature'
5import { logger } from './logger' 5import { logger } from './logger'
6import { cloneDeep } from 'lodash' 6import { cloneDeep } from 'lodash'
7import { createSign, createVerify } from 'crypto' 7import { createSign, createVerify } from 'crypto'
8import { buildDigest } from '../lib/job-queue/handlers/utils/activitypub-http-utils'
9import * as bcrypt from 'bcrypt' 8import * as bcrypt from 'bcrypt'
10import { MActor } from '../typings/models' 9import { MActor } from '../typings/models'
11 10
@@ -104,12 +103,19 @@ async function signJsonLDObject (byActor: MActor, data: any) {
104 return Object.assign(data, { signature }) 103 return Object.assign(data, { signature })
105} 104}
106 105
106function buildDigest (body: any) {
107 const rawBody = typeof body === 'string' ? body : JSON.stringify(body)
108
109 return 'SHA-256=' + sha256(rawBody, 'base64')
110}
111
107// --------------------------------------------------------------------------- 112// ---------------------------------------------------------------------------
108 113
109export { 114export {
110 isHTTPSignatureDigestValid, 115 isHTTPSignatureDigestValid,
111 parseHTTPSignature, 116 parseHTTPSignature,
112 isHTTPSignatureVerified, 117 isHTTPSignatureVerified,
118 buildDigest,
113 isJsonLDSignatureVerified, 119 isJsonLDSignatureVerified,
114 comparePassword, 120 comparePassword,
115 createPrivateAndPublicKeys, 121 createPrivateAndPublicKeys,
diff --git a/server/helpers/regexp.ts b/server/helpers/regexp.ts
index 2336654b0..cfc2be488 100644
--- a/server/helpers/regexp.ts
+++ b/server/helpers/regexp.ts
@@ -1,8 +1,8 @@
1// Thanks to https://regex101.com 1// Thanks to https://regex101.com
2function regexpCapture (str: string, regex: RegExp, maxIterations = 100) { 2function regexpCapture (str: string, regex: RegExp, maxIterations = 100) {
3 const result: RegExpExecArray[] = []
3 let m: RegExpExecArray 4 let m: RegExpExecArray
4 let i = 0 5 let i = 0
5 let result: RegExpExecArray[] = []
6 6
7 // tslint:disable:no-conditional-assignment 7 // tslint:disable:no-conditional-assignment
8 while ((m = regex.exec(str)) !== null && i < maxIterations) { 8 while ((m = regex.exec(str)) !== null && i < maxIterations) {
diff --git a/server/helpers/register-ts-paths.ts b/server/helpers/register-ts-paths.ts
index e8db369e3..eec7fed3e 100644
--- a/server/helpers/register-ts-paths.ts
+++ b/server/helpers/register-ts-paths.ts
@@ -1,5 +1,5 @@
1import { resolve } from 'path' 1import { resolve } from 'path'
2const tsConfigPaths = require('tsconfig-paths') 2import tsConfigPaths = require('tsconfig-paths')
3 3
4const tsConfig = require('../../tsconfig.json') 4const tsConfig = require('../../tsconfig.json')
5 5
diff --git a/server/helpers/signup.ts b/server/helpers/signup.ts
index 7c73f7c5c..d34ff2db5 100644
--- a/server/helpers/signup.ts
+++ b/server/helpers/signup.ts
@@ -21,7 +21,7 @@ async function isSignupAllowed (): Promise<{ allowed: boolean, errorMessage?: st
21 21
22function isSignupAllowedForCurrentIP (ip: string) { 22function isSignupAllowedForCurrentIP (ip: string) {
23 const addr = ipaddr.parse(ip) 23 const addr = ipaddr.parse(ip)
24 let excludeList = [ 'blacklist' ] 24 const excludeList = [ 'blacklist' ]
25 let matched = '' 25 let matched = ''
26 26
27 // if there is a valid, non-empty whitelist, we exclude all unknown adresses too 27 // if there is a valid, non-empty whitelist, we exclude all unknown adresses too
diff --git a/server/helpers/utils.ts b/server/helpers/utils.ts
index 4c6f200f8..ad3b7949e 100644
--- a/server/helpers/utils.ts
+++ b/server/helpers/utils.ts
@@ -1,12 +1,11 @@
1import { ResultList } from '../../shared' 1import { ResultList } from '../../shared'
2import { ApplicationModel } from '../models/application/application' 2import { execPromise, execPromise2, randomBytesPromise, sha256 } from './core-utils'
3import { execPromise, execPromise2, pseudoRandomBytesPromise, sha256 } from './core-utils'
4import { logger } from './logger' 3import { logger } from './logger'
5import { join } from 'path' 4import { join } from 'path'
6import { Instance as ParseTorrent } from 'parse-torrent' 5import { Instance as ParseTorrent } from 'parse-torrent'
7import { remove } from 'fs-extra' 6import { remove } from 'fs-extra'
8import * as memoizee from 'memoizee'
9import { CONFIG } from '../initializers/config' 7import { CONFIG } from '../initializers/config'
8import { isVideoFileExtnameValid } from './custom-validators/videos'
10 9
11function deleteFileAsync (path: string) { 10function deleteFileAsync (path: string) {
12 remove(path) 11 remove(path)
@@ -14,7 +13,7 @@ function deleteFileAsync (path: string) {
14} 13}
15 14
16async function generateRandomString (size: number) { 15async function generateRandomString (size: number) {
17 const raw = await pseudoRandomBytesPromise(size) 16 const raw = await randomBytesPromise(size)
18 17
19 return raw.toString('hex') 18 return raw.toString('hex')
20} 19}
@@ -32,21 +31,18 @@ function getFormattedObjects<U, V, T extends FormattableToJSON<U, V>> (objects:
32 } as ResultList<V> 31 } as ResultList<V>
33} 32}
34 33
35const getServerActor = memoizee(async function () { 34function generateVideoImportTmpPath (target: string | ParseTorrent, extensionArg?: string) {
36 const application = await ApplicationModel.load() 35 const id = typeof target === 'string'
37 if (!application) throw Error('Could not load Application from database.') 36 ? target
37 : target.infoHash
38 38
39 const actor = application.Account.Actor 39 let extension = '.mp4'
40 actor.Account = application.Account 40 if (extensionArg && isVideoFileExtnameValid(extensionArg)) {
41 41 extension = extensionArg
42 return actor 42 }
43}, { promise: true })
44
45function generateVideoImportTmpPath (target: string | ParseTorrent) {
46 const id = typeof target === 'string' ? target : target.infoHash
47 43
48 const hash = sha256(id) 44 const hash = sha256(id)
49 return join(CONFIG.STORAGE.TMP_DIR, hash + '-import.mp4') 45 return join(CONFIG.STORAGE.TMP_DIR, `${hash}-import${extension}`)
50} 46}
51 47
52function getSecureTorrentName (originalName: string) { 48function getSecureTorrentName (originalName: string) {
@@ -97,7 +93,6 @@ export {
97 generateRandomString, 93 generateRandomString,
98 getFormattedObjects, 94 getFormattedObjects,
99 getSecureTorrentName, 95 getSecureTorrentName,
100 getServerActor,
101 getServerCommit, 96 getServerCommit,
102 generateVideoImportTmpPath, 97 generateVideoImportTmpPath,
103 getUUIDFromFilename 98 getUUIDFromFilename
diff --git a/server/helpers/video.ts b/server/helpers/video.ts
index 5b9c026b1..6f76cbdfc 100644
--- a/server/helpers/video.ts
+++ b/server/helpers/video.ts
@@ -1,17 +1,26 @@
1import { VideoModel } from '../models/video/video' 1import { VideoModel } from '../models/video/video'
2import * as Bluebird from 'bluebird' 2import * as Bluebird from 'bluebird'
3import { 3import {
4 isStreamingPlaylist,
5 MStreamingPlaylistVideo,
6 MVideo,
4 MVideoAccountLightBlacklistAllFiles, 7 MVideoAccountLightBlacklistAllFiles,
8 MVideoFile,
5 MVideoFullLight, 9 MVideoFullLight,
6 MVideoIdThumbnail, 10 MVideoIdThumbnail,
11 MVideoImmutable,
7 MVideoThumbnail, 12 MVideoThumbnail,
8 MVideoWithRights 13 MVideoWithRights
9} from '@server/typings/models' 14} from '@server/typings/models'
10import { Response } from 'express' 15import { Response } from 'express'
16import { DEFAULT_AUDIO_RESOLUTION } from '@server/initializers/constants'
17import { JobQueue } from '@server/lib/job-queue'
18import { VideoTranscodingPayload } from '@shared/models'
11 19
12type VideoFetchType = 'all' | 'only-video' | 'only-video-with-rights' | 'id' | 'none' 20type VideoFetchType = 'all' | 'only-video' | 'only-video-with-rights' | 'id' | 'none' | 'only-immutable-attributes'
13 21
14function fetchVideo (id: number | string, fetchType: 'all', userId?: number): Bluebird<MVideoFullLight> 22function fetchVideo (id: number | string, fetchType: 'all', userId?: number): Bluebird<MVideoFullLight>
23function fetchVideo (id: number | string, fetchType: 'only-immutable-attributes'): Bluebird<MVideoImmutable>
15function fetchVideo (id: number | string, fetchType: 'only-video', userId?: number): Bluebird<MVideoThumbnail> 24function fetchVideo (id: number | string, fetchType: 'only-video', userId?: number): Bluebird<MVideoThumbnail>
16function fetchVideo (id: number | string, fetchType: 'only-video-with-rights', userId?: number): Bluebird<MVideoWithRights> 25function fetchVideo (id: number | string, fetchType: 'only-video-with-rights', userId?: number): Bluebird<MVideoWithRights>
17function fetchVideo (id: number | string, fetchType: 'id' | 'none', userId?: number): Bluebird<MVideoIdThumbnail> 26function fetchVideo (id: number | string, fetchType: 'id' | 'none', userId?: number): Bluebird<MVideoIdThumbnail>
@@ -19,14 +28,16 @@ function fetchVideo (
19 id: number | string, 28 id: number | string,
20 fetchType: VideoFetchType, 29 fetchType: VideoFetchType,
21 userId?: number 30 userId?: number
22): Bluebird<MVideoFullLight | MVideoThumbnail | MVideoWithRights | MVideoIdThumbnail> 31): Bluebird<MVideoFullLight | MVideoThumbnail | MVideoWithRights | MVideoIdThumbnail | MVideoImmutable>
23function fetchVideo ( 32function fetchVideo (
24 id: number | string, 33 id: number | string,
25 fetchType: VideoFetchType, 34 fetchType: VideoFetchType,
26 userId?: number 35 userId?: number
27): Bluebird<MVideoFullLight | MVideoThumbnail | MVideoWithRights | MVideoIdThumbnail> { 36): Bluebird<MVideoFullLight | MVideoThumbnail | MVideoWithRights | MVideoIdThumbnail | MVideoImmutable> {
28 if (fetchType === 'all') return VideoModel.loadAndPopulateAccountAndServerAndTags(id, undefined, userId) 37 if (fetchType === 'all') return VideoModel.loadAndPopulateAccountAndServerAndTags(id, undefined, userId)
29 38
39 if (fetchType === 'only-immutable-attributes') return VideoModel.loadImmutableAttributes(id)
40
30 if (fetchType === 'only-video-with-rights') return VideoModel.loadWithRights(id) 41 if (fetchType === 'only-video-with-rights') return VideoModel.loadWithRights(id)
31 42
32 if (fetchType === 'only-video') return VideoModel.load(id) 43 if (fetchType === 'only-video') return VideoModel.load(id)
@@ -34,14 +45,23 @@ function fetchVideo (
34 if (fetchType === 'id' || fetchType === 'none') return VideoModel.loadOnlyId(id) 45 if (fetchType === 'id' || fetchType === 'none') return VideoModel.loadOnlyId(id)
35} 46}
36 47
37type VideoFetchByUrlType = 'all' | 'only-video' 48type VideoFetchByUrlType = 'all' | 'only-video' | 'only-immutable-attributes'
38 49
39function fetchVideoByUrl (url: string, fetchType: 'all'): Bluebird<MVideoAccountLightBlacklistAllFiles> 50function fetchVideoByUrl (url: string, fetchType: 'all'): Bluebird<MVideoAccountLightBlacklistAllFiles>
51function fetchVideoByUrl (url: string, fetchType: 'only-immutable-attributes'): Bluebird<MVideoImmutable>
40function fetchVideoByUrl (url: string, fetchType: 'only-video'): Bluebird<MVideoThumbnail> 52function fetchVideoByUrl (url: string, fetchType: 'only-video'): Bluebird<MVideoThumbnail>
41function fetchVideoByUrl (url: string, fetchType: VideoFetchByUrlType): Bluebird<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail> 53function fetchVideoByUrl (
42function fetchVideoByUrl (url: string, fetchType: VideoFetchByUrlType): Bluebird<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail> { 54 url: string,
55 fetchType: VideoFetchByUrlType
56): Bluebird<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail | MVideoImmutable>
57function fetchVideoByUrl (
58 url: string,
59 fetchType: VideoFetchByUrlType
60): Bluebird<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail | MVideoImmutable> {
43 if (fetchType === 'all') return VideoModel.loadByUrlAndPopulateAccount(url) 61 if (fetchType === 'all') return VideoModel.loadByUrlAndPopulateAccount(url)
44 62
63 if (fetchType === 'only-immutable-attributes') return VideoModel.loadByUrlImmutableAttributes(url)
64
45 if (fetchType === 'only-video') return VideoModel.loadByUrl(url) 65 if (fetchType === 'only-video') return VideoModel.loadByUrl(url)
46} 66}
47 67
@@ -49,10 +69,39 @@ function getVideoWithAttributes (res: Response) {
49 return res.locals.videoAll || res.locals.onlyVideo || res.locals.onlyVideoWithRights 69 return res.locals.videoAll || res.locals.onlyVideo || res.locals.onlyVideoWithRights
50} 70}
51 71
72function addOptimizeOrMergeAudioJob (video: MVideo, videoFile: MVideoFile) {
73 let dataInput: VideoTranscodingPayload
74
75 if (videoFile.isAudio()) {
76 dataInput = {
77 type: 'merge-audio' as 'merge-audio',
78 resolution: DEFAULT_AUDIO_RESOLUTION,
79 videoUUID: video.uuid,
80 isNewVideo: true
81 }
82 } else {
83 dataInput = {
84 type: 'optimize' as 'optimize',
85 videoUUID: video.uuid,
86 isNewVideo: true
87 }
88 }
89
90 return JobQueue.Instance.createJobWithPromise({ type: 'video-transcoding', payload: dataInput })
91}
92
93function extractVideo (videoOrPlaylist: MVideo | MStreamingPlaylistVideo) {
94 return isStreamingPlaylist(videoOrPlaylist)
95 ? videoOrPlaylist.Video
96 : videoOrPlaylist
97}
98
52export { 99export {
53 VideoFetchType, 100 VideoFetchType,
54 VideoFetchByUrlType, 101 VideoFetchByUrlType,
55 fetchVideo, 102 fetchVideo,
56 getVideoWithAttributes, 103 getVideoWithAttributes,
57 fetchVideoByUrl 104 fetchVideoByUrl,
105 addOptimizeOrMergeAudioJob,
106 extractVideo
58} 107}
diff --git a/server/helpers/webtorrent.ts b/server/helpers/webtorrent.ts
index 3a99518c6..7cd76d708 100644
--- a/server/helpers/webtorrent.ts
+++ b/server/helpers/webtorrent.ts
@@ -9,12 +9,12 @@ import { promisify2 } from './core-utils'
9import { MVideo } from '@server/typings/models/video/video' 9import { MVideo } from '@server/typings/models/video/video'
10import { MVideoFile, MVideoFileRedundanciesOpt } from '@server/typings/models/video/video-file' 10import { MVideoFile, MVideoFileRedundanciesOpt } from '@server/typings/models/video/video-file'
11import { isStreamingPlaylist, MStreamingPlaylistVideo } from '@server/typings/models/video/video-streaming-playlist' 11import { isStreamingPlaylist, MStreamingPlaylistVideo } from '@server/typings/models/video/video-streaming-playlist'
12import { STATIC_PATHS, WEBSERVER } from '@server/initializers/constants' 12import { WEBSERVER } from '@server/initializers/constants'
13import * as parseTorrent from 'parse-torrent' 13import * as parseTorrent from 'parse-torrent'
14import * as magnetUtil from 'magnet-uri' 14import * as magnetUtil from 'magnet-uri'
15import { isArray } from '@server/helpers/custom-validators/misc' 15import { isArray } from '@server/helpers/custom-validators/misc'
16import { extractVideo } from '@server/lib/videos' 16import { getTorrentFileName, getVideoFilePath } from '@server/lib/video-paths'
17import { getTorrentFileName, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths' 17import { extractVideo } from '@server/helpers/video'
18 18
19const createTorrentPromise = promisify2<string, any, any>(createTorrent) 19const createTorrentPromise = promisify2<string, any, any>(createTorrent)
20 20
@@ -39,7 +39,7 @@ async function downloadWebTorrentVideo (target: { magnetUri: string, torrentName
39 if (torrent.files.length !== 1) { 39 if (torrent.files.length !== 1) {
40 if (timer) clearTimeout(timer) 40 if (timer) clearTimeout(timer)
41 41
42 for (let file of torrent.files) { 42 for (const file of torrent.files) {
43 deleteDownloadedFile({ directoryPath, filepath: file.path }) 43 deleteDownloadedFile({ directoryPath, filepath: file.path })
44 } 44 }
45 45
@@ -47,15 +47,16 @@ async function downloadWebTorrentVideo (target: { magnetUri: string, torrentName
47 .then(() => rej(new Error('Cannot import torrent ' + torrentId + ': there are multiple files in it'))) 47 .then(() => rej(new Error('Cannot import torrent ' + torrentId + ': there are multiple files in it')))
48 } 48 }
49 49
50 file = torrent.files[ 0 ] 50 file = torrent.files[0]
51 51
52 // FIXME: avoid creating another stream when https://github.com/webtorrent/webtorrent/issues/1517 is fixed 52 // FIXME: avoid creating another stream when https://github.com/webtorrent/webtorrent/issues/1517 is fixed
53 const writeStream = createWriteStream(path) 53 const writeStream = createWriteStream(path)
54 writeStream.on('finish', () => { 54 writeStream.on('finish', () => {
55 if (timer) clearTimeout(timer) 55 if (timer) clearTimeout(timer)
56 56
57 return safeWebtorrentDestroy(webtorrent, torrentId, { directoryPath, filepath: file.path }, target.torrentName) 57 safeWebtorrentDestroy(webtorrent, torrentId, { directoryPath, filepath: file.path }, target.torrentName)
58 .then(() => res(path)) 58 .then(() => res(path))
59 .catch(err => logger.error('Cannot destroy webtorrent.', { err }))
59 }) 60 })
60 61
61 file.createReadStream().pipe(writeStream) 62 file.createReadStream().pipe(writeStream)
@@ -63,9 +64,16 @@ async function downloadWebTorrentVideo (target: { magnetUri: string, torrentName
63 64
64 torrent.on('error', err => rej(err)) 65 torrent.on('error', err => rej(err))
65 66
66 timer = setTimeout(async () => { 67 timer = setTimeout(() => {
67 return safeWebtorrentDestroy(webtorrent, torrentId, file ? { directoryPath, filepath: file.path } : undefined, target.torrentName) 68 const err = new Error('Webtorrent download timeout.')
68 .then(() => rej(new Error('Webtorrent download timeout.'))) 69
70 safeWebtorrentDestroy(webtorrent, torrentId, file ? { directoryPath, filepath: file.path } : undefined, target.torrentName)
71 .then(() => rej(err))
72 .catch(destroyErr => {
73 logger.error('Cannot destroy webtorrent.', { err: destroyErr })
74 rej(err)
75 })
76
69 }, timeout) 77 }, timeout)
70 }) 78 })
71} 79}
diff --git a/server/helpers/youtube-dl.ts b/server/helpers/youtube-dl.ts
index 577a59dbf..f0944b94f 100644
--- a/server/helpers/youtube-dl.ts
+++ b/server/helpers/youtube-dl.ts
@@ -1,4 +1,4 @@
1import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES } from '../initializers/constants' 1import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../initializers/constants'
2import { logger } from './logger' 2import { logger } from './logger'
3import { generateVideoImportTmpPath } from './utils' 3import { generateVideoImportTmpPath } from './utils'
4import { join } from 'path' 4import { join } from 'path'
@@ -12,40 +12,85 @@ export type YoutubeDLInfo = {
12 name?: string 12 name?: string
13 description?: string 13 description?: string
14 category?: number 14 category?: number
15 language?: string
15 licence?: number 16 licence?: number
16 nsfw?: boolean 17 nsfw?: boolean
17 tags?: string[] 18 tags?: string[]
18 thumbnailUrl?: string 19 thumbnailUrl?: string
20 fileExt?: string
19 originallyPublishedAt?: Date 21 originallyPublishedAt?: Date
20} 22}
21 23
24export type YoutubeDLSubs = {
25 language: string
26 filename: string
27 path: string
28}[]
29
22const processOptions = { 30const processOptions = {
23 maxBuffer: 1024 * 1024 * 10 // 10MB 31 maxBuffer: 1024 * 1024 * 10 // 10MB
24} 32}
25 33
26function getYoutubeDLInfo (url: string, opts?: string[]): Promise<YoutubeDLInfo> { 34function getYoutubeDLInfo (url: string, opts?: string[]): Promise<YoutubeDLInfo> {
27 return new Promise<YoutubeDLInfo>(async (res, rej) => { 35 return new Promise<YoutubeDLInfo>((res, rej) => {
28 let args = opts || [ '-j', '--flat-playlist' ] 36 let args = opts || [ '-j', '--flat-playlist' ]
29 args = wrapWithProxyOptions(args) 37 args = wrapWithProxyOptions(args)
30 38
31 const youtubeDL = await safeGetYoutubeDL() 39 safeGetYoutubeDL()
32 youtubeDL.getInfo(url, args, processOptions, (err, info) => { 40 .then(youtubeDL => {
33 if (err) return rej(err) 41 youtubeDL.getInfo(url, args, processOptions, (err, info) => {
34 if (info.is_live === true) return rej(new Error('Cannot download a live streaming.')) 42 if (err) return rej(err)
43 if (info.is_live === true) return rej(new Error('Cannot download a live streaming.'))
35 44
36 const obj = buildVideoInfo(normalizeObject(info)) 45 const obj = buildVideoInfo(normalizeObject(info))
37 if (obj.name && obj.name.length < CONSTRAINTS_FIELDS.VIDEOS.NAME.min) obj.name += ' video' 46 if (obj.name && obj.name.length < CONSTRAINTS_FIELDS.VIDEOS.NAME.min) obj.name += ' video'
38 47
39 return res(obj) 48 return res(obj)
40 }) 49 })
50 })
51 .catch(err => rej(err))
52 })
53}
54
55function getYoutubeDLSubs (url: string, opts?: object): Promise<YoutubeDLSubs> {
56 return new Promise<YoutubeDLSubs>((res, rej) => {
57 const cwd = CONFIG.STORAGE.TMP_DIR
58 const options = opts || { all: true, format: 'vtt', cwd }
59
60 safeGetYoutubeDL()
61 .then(youtubeDL => {
62 youtubeDL.getSubs(url, options, (err, files) => {
63 if (err) return rej(err)
64
65 logger.debug('Get subtitles from youtube dl.', { url, files })
66
67 const subtitles = files.reduce((acc, filename) => {
68 const matched = filename.match(/\.([a-z]{2})\.(vtt|ttml)/i)
69
70 if (matched[1]) {
71 return [
72 ...acc,
73 {
74 language: matched[1],
75 path: join(cwd, filename),
76 filename
77 }
78 ]
79 }
80 }, [])
81
82 return res(subtitles)
83 })
84 })
85 .catch(err => rej(err))
41 }) 86 })
42} 87}
43 88
44function downloadYoutubeDLVideo (url: string, timeout: number) { 89function downloadYoutubeDLVideo (url: string, extension: string, timeout: number) {
45 const path = generateVideoImportTmpPath(url) 90 const path = generateVideoImportTmpPath(url, extension)
46 let timer 91 let timer
47 92
48 logger.info('Importing youtubeDL video %s', url) 93 logger.info('Importing youtubeDL video %s to %s', url, path)
49 94
50 let options = [ '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best', '-o', path ] 95 let options = [ '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best', '-o', path ]
51 options = wrapWithProxyOptions(options) 96 options = wrapWithProxyOptions(options)
@@ -54,26 +99,34 @@ function downloadYoutubeDLVideo (url: string, timeout: number) {
54 options = options.concat([ '--ffmpeg-location', process.env.FFMPEG_PATH ]) 99 options = options.concat([ '--ffmpeg-location', process.env.FFMPEG_PATH ])
55 } 100 }
56 101
57 return new Promise<string>(async (res, rej) => { 102 return new Promise<string>((res, rej) => {
58 const youtubeDL = await safeGetYoutubeDL() 103 safeGetYoutubeDL()
59 youtubeDL.exec(url, options, processOptions, err => { 104 .then(youtubeDL => {
60 clearTimeout(timer) 105 youtubeDL.exec(url, options, processOptions, err => {
106 clearTimeout(timer)
61 107
62 if (err) { 108 if (err) {
63 remove(path) 109 remove(path)
64 .catch(err => logger.error('Cannot delete path on YoutubeDL error.', { err })) 110 .catch(err => logger.error('Cannot delete path on YoutubeDL error.', { err }))
65 111
66 return rej(err) 112 return rej(err)
67 } 113 }
68 114
69 return res(path) 115 return res(path)
70 }) 116 })
71 117
72 timer = setTimeout(async () => { 118 timer = setTimeout(() => {
73 await remove(path) 119 const err = new Error('YoutubeDL download timeout.')
74 120
75 return rej(new Error('YoutubeDL download timeout.')) 121 remove(path)
76 }, timeout) 122 .finally(() => rej(err))
123 .catch(err => {
124 logger.error('Cannot remove %s in youtubeDL timeout.', path, { err })
125 return rej(err)
126 })
127 }, timeout)
128 })
129 .catch(err => rej(err))
77 }) 130 })
78} 131}
79 132
@@ -103,7 +156,7 @@ async function updateYoutubeDLBinary () {
103 156
104 const url = result.headers.location 157 const url = result.headers.location
105 const downloadFile = request.get(url) 158 const downloadFile = request.get(url)
106 const newVersion = /yt-dl\.org\/downloads\/(\d{4}\.\d\d\.\d\d(\.\d)?)\/youtube-dl/.exec(url)[ 1 ] 159 const newVersion = /yt-dl\.org\/downloads\/(\d{4}\.\d\d\.\d\d(\.\d)?)\/youtube-dl/.exec(url)[1]
107 160
108 downloadFile.on('response', result => { 161 downloadFile.on('response', result => {
109 if (result.statusCode !== 200) { 162 if (result.statusCode !== 200) {
@@ -173,6 +226,7 @@ function buildOriginallyPublishedAt (obj: any) {
173export { 226export {
174 updateYoutubeDLBinary, 227 updateYoutubeDLBinary,
175 downloadYoutubeDLVideo, 228 downloadYoutubeDLVideo,
229 getYoutubeDLSubs,
176 getYoutubeDLInfo, 230 getYoutubeDLInfo,
177 safeGetYoutubeDL, 231 safeGetYoutubeDL,
178 buildOriginallyPublishedAt 232 buildOriginallyPublishedAt
@@ -199,16 +253,18 @@ function normalizeObject (obj: any) {
199 return newObj 253 return newObj
200} 254}
201 255
202function buildVideoInfo (obj: any) { 256function buildVideoInfo (obj: any): YoutubeDLInfo {
203 return { 257 return {
204 name: titleTruncation(obj.title), 258 name: titleTruncation(obj.title),
205 description: descriptionTruncation(obj.description), 259 description: descriptionTruncation(obj.description),
206 category: getCategory(obj.categories), 260 category: getCategory(obj.categories),
207 licence: getLicence(obj.license), 261 licence: getLicence(obj.license),
262 language: getLanguage(obj.language),
208 nsfw: isNSFW(obj), 263 nsfw: isNSFW(obj),
209 tags: getTags(obj.tags), 264 tags: getTags(obj.tags),
210 thumbnailUrl: obj.thumbnail || undefined, 265 thumbnailUrl: obj.thumbnail || undefined,
211 originallyPublishedAt: buildOriginallyPublishedAt(obj) 266 originallyPublishedAt: buildOriginallyPublishedAt(obj),
267 fileExt: obj.ext
212 } 268 }
213} 269}
214 270
@@ -246,7 +302,12 @@ function getTags (tags: any) {
246function getLicence (licence: string) { 302function getLicence (licence: string) {
247 if (!licence) return undefined 303 if (!licence) return undefined
248 304
249 if (licence.indexOf('Creative Commons Attribution') !== -1) return 1 305 if (licence.includes('Creative Commons Attribution')) return 1
306
307 for (const key of Object.keys(VIDEO_LICENCES)) {
308 const peertubeLicence = VIDEO_LICENCES[key]
309 if (peertubeLicence.toLowerCase() === licence.toLowerCase()) return parseInt(key, 10)
310 }
250 311
251 return undefined 312 return undefined
252} 313}
@@ -267,6 +328,10 @@ function getCategory (categories: string[]) {
267 return undefined 328 return undefined
268} 329}
269 330
331function getLanguage (language: string) {
332 return VIDEO_LANGUAGES[language] ? language : undefined
333}
334
270function wrapWithProxyOptions (options: string[]) { 335function wrapWithProxyOptions (options: string[]) {
271 if (CONFIG.IMPORT.VIDEOS.HTTP.PROXY.ENABLED) { 336 if (CONFIG.IMPORT.VIDEOS.HTTP.PROXY.ENABLED) {
272 logger.debug('Using proxy for YoutubeDL') 337 logger.debug('Using proxy for YoutubeDL')
diff --git a/server/initializers/checker-after-init.ts b/server/initializers/checker-after-init.ts
index 44efd346c..f111be2ae 100644
--- a/server/initializers/checker-after-init.ts
+++ b/server/initializers/checker-after-init.ts
@@ -1,22 +1,21 @@
1import * as config from 'config' 1import * as config from 'config'
2import { isProdInstance, isTestInstance } from '../helpers/core-utils' 2import { isProdInstance, isTestInstance } from '../helpers/core-utils'
3import { UserModel } from '../models/account/user' 3import { UserModel } from '../models/account/user'
4import { ApplicationModel } from '../models/application/application' 4import { getServerActor, ApplicationModel } from '../models/application/application'
5import { OAuthClientModel } from '../models/oauth/oauth-client' 5import { OAuthClientModel } from '../models/oauth/oauth-client'
6import { parse } from 'url' 6import { URL } from 'url'
7import { CONFIG } from './config' 7import { CONFIG, isEmailEnabled } from './config'
8import { logger } from '../helpers/logger' 8import { logger } from '../helpers/logger'
9import { getServerActor } from '../helpers/utils'
10import { RecentlyAddedStrategy } from '../../shared/models/redundancy' 9import { RecentlyAddedStrategy } from '../../shared/models/redundancy'
11import { isArray } from '../helpers/custom-validators/misc' 10import { isArray } from '../helpers/custom-validators/misc'
12import { uniq } from 'lodash' 11import { uniq } from 'lodash'
13import { Emailer } from '../lib/emailer'
14import { WEBSERVER } from './constants' 12import { WEBSERVER } from './constants'
13import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type'
15 14
16async function checkActivityPubUrls () { 15async function checkActivityPubUrls () {
17 const actor = await getServerActor() 16 const actor = await getServerActor()
18 17
19 const parsed = parse(actor.url) 18 const parsed = new URL(actor.url)
20 if (WEBSERVER.HOST !== parsed.host) { 19 if (WEBSERVER.HOST !== parsed.host) {
21 const NODE_ENV = config.util.getEnv('NODE_ENV') 20 const NODE_ENV = config.util.getEnv('NODE_ENV')
22 const NODE_CONFIG_DIR = config.util.getEnv('NODE_CONFIG_DIR') 21 const NODE_CONFIG_DIR = config.util.getEnv('NODE_CONFIG_DIR')
@@ -41,7 +40,7 @@ function checkConfig () {
41 } 40 }
42 41
43 // Email verification 42 // Email verification
44 if (!Emailer.isEnabled()) { 43 if (!isEmailEnabled()) {
45 if (CONFIG.SIGNUP.ENABLED && CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) { 44 if (CONFIG.SIGNUP.ENABLED && CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) {
46 return 'Emailer is disabled but you require signup email verification.' 45 return 'Emailer is disabled but you require signup email verification.'
47 } 46 }
@@ -55,7 +54,7 @@ function checkConfig () {
55 const defaultNSFWPolicy = CONFIG.INSTANCE.DEFAULT_NSFW_POLICY 54 const defaultNSFWPolicy = CONFIG.INSTANCE.DEFAULT_NSFW_POLICY
56 { 55 {
57 const available = [ 'do_not_list', 'blur', 'display' ] 56 const available = [ 'do_not_list', 'blur', 'display' ]
58 if (available.indexOf(defaultNSFWPolicy) === -1) { 57 if (available.includes(defaultNSFWPolicy) === false) {
59 return 'NSFW policy setting should be ' + available.join(' or ') + ' instead of ' + defaultNSFWPolicy 58 return 'NSFW policy setting should be ' + available.join(' or ') + ' instead of ' + defaultNSFWPolicy
60 } 59 }
61 } 60 }
@@ -65,7 +64,7 @@ function checkConfig () {
65 if (isArray(redundancyVideos)) { 64 if (isArray(redundancyVideos)) {
66 const available = [ 'most-views', 'trending', 'recently-added' ] 65 const available = [ 'most-views', 'trending', 'recently-added' ]
67 for (const r of redundancyVideos) { 66 for (const r of redundancyVideos) {
68 if (available.indexOf(r.strategy) === -1) { 67 if (available.includes(r.strategy) === false) {
69 return 'Videos redundancy should have ' + available.join(' or ') + ' strategy instead of ' + r.strategy 68 return 'Videos redundancy should have ' + available.join(' or ') + ' strategy instead of ' + r.strategy
70 } 69 }
71 70
@@ -88,6 +87,13 @@ function checkConfig () {
88 return 'Videos redundancy should be an array (you must uncomment lines containing - too)' 87 return 'Videos redundancy should be an array (you must uncomment lines containing - too)'
89 } 88 }
90 89
90 // Remote redundancies
91 const acceptFrom = CONFIG.REMOTE_REDUNDANCY.VIDEOS.ACCEPT_FROM
92 const acceptFromValues = new Set<VideoRedundancyConfigFilter>([ 'nobody', 'anybody', 'followings' ])
93 if (acceptFromValues.has(acceptFrom) === false) {
94 return 'remote_redundancy.videos.accept_from has an incorrect value'
95 }
96
91 // Check storage directory locations 97 // Check storage directory locations
92 if (isProdInstance()) { 98 if (isProdInstance()) {
93 const configStorage = config.get('storage') 99 const configStorage = config.get('storage')
diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts
index 9731a0af9..56f8156c6 100644
--- a/server/initializers/checker-before-init.ts
+++ b/server/initializers/checker-before-init.ts
@@ -22,6 +22,8 @@ function checkMissedConfig () {
22 'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist', 22 'signup.filters.cidr.whitelist', 'signup.filters.cidr.blacklist',
23 'redundancy.videos.strategies', 'redundancy.videos.check_interval', 23 'redundancy.videos.strategies', 'redundancy.videos.check_interval',
24 'transcoding.enabled', 'transcoding.threads', 'transcoding.allow_additional_extensions', 'transcoding.hls.enabled', 24 'transcoding.enabled', 'transcoding.threads', 'transcoding.allow_additional_extensions', 'transcoding.hls.enabled',
25 'transcoding.resolutions.0p', 'transcoding.resolutions.240p', 'transcoding.resolutions.360p', 'transcoding.resolutions.480p',
26 'transcoding.resolutions.720p', 'transcoding.resolutions.1080p', 'transcoding.resolutions.2160p',
25 'import.videos.http.enabled', 'import.videos.torrent.enabled', 'auto_blacklist.videos.of_users.enabled', 27 'import.videos.http.enabled', 'import.videos.torrent.enabled', 'auto_blacklist.videos.of_users.enabled',
26 'trending.videos.interval_days', 28 'trending.videos.interval_days',
27 'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route', 29 'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route',
@@ -31,12 +33,13 @@ function checkMissedConfig () {
31 'tracker.enabled', 'tracker.private', 'tracker.reject_too_many_announces', 33 'tracker.enabled', 'tracker.private', 'tracker.reject_too_many_announces',
32 'history.videos.max_age', 'views.videos.remote.max_age', 34 'history.videos.max_age', 'views.videos.remote.max_age',
33 'rates_limit.login.window', 'rates_limit.login.max', 'rates_limit.ask_send_email.window', 'rates_limit.ask_send_email.max', 35 'rates_limit.login.window', 'rates_limit.login.max', 'rates_limit.ask_send_email.window', 'rates_limit.ask_send_email.max',
34 'theme.default' 36 'theme.default',
37 'remote_redundancy.videos.accept_from'
35 ] 38 ]
36 const requiredAlternatives = [ 39 const requiredAlternatives = [
37 [ // set 40 [ // set
38 ['redis.hostname', 'redis.port'], // alternative 41 [ 'redis.hostname', 'redis.port' ], // alternative
39 ['redis.socket'] 42 [ 'redis.socket' ]
40 ] 43 ]
41 ] 44 ]
42 const miss: string[] = [] 45 const miss: string[] = []
diff --git a/server/initializers/config.ts b/server/initializers/config.ts
index 7fd77f3e8..6932b41e1 100644
--- a/server/initializers/config.ts
+++ b/server/initializers/config.ts
@@ -1,10 +1,11 @@
1import { IConfig } from 'config' 1import { IConfig } from 'config'
2import { dirname, join } from 'path' 2import { dirname, join } from 'path'
3import { VideosRedundancy } from '../../shared/models' 3import { VideosRedundancyStrategy } from '../../shared/models'
4// Do not use barrels, remain constants as independent as possible 4// Do not use barrels, remain constants as independent as possible
5import { buildPath, parseBytes, parseDurationToMs, root } from '../helpers/core-utils' 5import { buildPath, parseBytes, parseDurationToMs, root } from '../helpers/core-utils'
6import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' 6import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type'
7import * as bytes from 'bytes' 7import * as bytes from 'bytes'
8import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type'
8 9
9// Use a variable to reload the configuration if we need 10// Use a variable to reload the configuration if we need
10let config: IConfig = require('config') 11let config: IConfig = require('config')
@@ -35,6 +36,8 @@ const CONFIG = {
35 DB: config.has('redis.db') ? config.get<number>('redis.db') : null 36 DB: config.has('redis.db') ? config.get<number>('redis.db') : null
36 }, 37 },
37 SMTP: { 38 SMTP: {
39 TRANSPORT: config.has('smtp.transport') ? config.get<string>('smtp.transport') : 'smtp',
40 SENDMAIL: config.has('smtp.sendmail') ? config.get<string>('smtp.sendmail') : null,
38 HOSTNAME: config.get<string>('smtp.hostname'), 41 HOSTNAME: config.get<string>('smtp.hostname'),
39 PORT: config.get<number>('smtp.port'), 42 PORT: config.get<number>('smtp.port'),
40 USERNAME: config.get<string>('smtp.username'), 43 USERNAME: config.get<string>('smtp.username'),
@@ -117,6 +120,11 @@ const CONFIG = {
117 STRATEGIES: buildVideosRedundancy(config.get<any[]>('redundancy.videos.strategies')) 120 STRATEGIES: buildVideosRedundancy(config.get<any[]>('redundancy.videos.strategies'))
118 } 121 }
119 }, 122 },
123 REMOTE_REDUNDANCY: {
124 VIDEOS: {
125 ACCEPT_FROM: config.get<VideoRedundancyConfigFilter>('remote_redundancy.videos.accept_from')
126 }
127 },
120 CSP: { 128 CSP: {
121 ENABLED: config.get<boolean>('csp.enabled'), 129 ENABLED: config.get<boolean>('csp.enabled'),
122 REPORT_ONLY: config.get<boolean>('csp.report_only'), 130 REPORT_ONLY: config.get<boolean>('csp.report_only'),
@@ -284,11 +292,16 @@ function registerConfigChangedHandler (fun: Function) {
284 configChangedHandlers.push(fun) 292 configChangedHandlers.push(fun)
285} 293}
286 294
295function isEmailEnabled () {
296 return !!CONFIG.SMTP.HOSTNAME && !!CONFIG.SMTP.PORT
297}
298
287// --------------------------------------------------------------------------- 299// ---------------------------------------------------------------------------
288 300
289export { 301export {
290 CONFIG, 302 CONFIG,
291 registerConfigChangedHandler 303 registerConfigChangedHandler,
304 isEmailEnabled
292} 305}
293 306
294// --------------------------------------------------------------------------- 307// ---------------------------------------------------------------------------
@@ -301,10 +314,10 @@ function getLocalConfigFilePath () {
301 if (process.env.NODE_ENV) filename += `-${process.env.NODE_ENV}` 314 if (process.env.NODE_ENV) filename += `-${process.env.NODE_ENV}`
302 if (process.env.NODE_APP_INSTANCE) filename += `-${process.env.NODE_APP_INSTANCE}` 315 if (process.env.NODE_APP_INSTANCE) filename += `-${process.env.NODE_APP_INSTANCE}`
303 316
304 return join(dirname(configSources[ 0 ].name), filename + '.json') 317 return join(dirname(configSources[0].name), filename + '.json')
305} 318}
306 319
307function buildVideosRedundancy (objs: any[]): VideosRedundancy[] { 320function buildVideosRedundancy (objs: any[]): VideosRedundancyStrategy[] {
308 if (!objs) return [] 321 if (!objs) return []
309 322
310 if (!Array.isArray(objs)) return objs 323 if (!Array.isArray(objs)) return objs
@@ -330,7 +343,7 @@ export function reloadConfig () {
330 343
331 function purge () { 344 function purge () {
332 for (const fileName in require.cache) { 345 for (const fileName in require.cache) {
333 if (-1 === fileName.indexOf(directory())) { 346 if (fileName.includes(directory()) === false) {
334 continue 347 continue
335 } 348 }
336 349
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 13e32b6d2..134560717 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -4,7 +4,7 @@ import { ActivityPubActorType } from '../../shared/models/activitypub'
4import { FollowState } from '../../shared/models/actors' 4import { FollowState } from '../../shared/models/actors'
5import { VideoAbuseState, VideoImportState, VideoPrivacy, VideoTranscodingFPS } from '../../shared/models/videos' 5import { VideoAbuseState, VideoImportState, VideoPrivacy, VideoTranscodingFPS } from '../../shared/models/videos'
6// Do not use barrels, remain constants as independent as possible 6// Do not use barrels, remain constants as independent as possible
7import { isTestInstance, sanitizeHost, sanitizeUrl, root, parseDurationToMs } from '../helpers/core-utils' 7import { isTestInstance, sanitizeHost, sanitizeUrl, root } from '../helpers/core-utils'
8import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' 8import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type'
9import { invert } from 'lodash' 9import { invert } from 'lodash'
10import { CronRepeatOptions, EveryRepeatOptions } from 'bull' 10import { CronRepeatOptions, EveryRepeatOptions } from 'bull'
@@ -14,7 +14,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
14 14
15// --------------------------------------------------------------------------- 15// ---------------------------------------------------------------------------
16 16
17const LAST_MIGRATION_VERSION = 470 17const LAST_MIGRATION_VERSION = 505
18 18
19// --------------------------------------------------------------------------- 19// ---------------------------------------------------------------------------
20 20
@@ -59,9 +59,9 @@ const SORTABLE_COLUMNS = {
59 FOLLOWERS: [ 'createdAt', 'state', 'score' ], 59 FOLLOWERS: [ 'createdAt', 'state', 'score' ],
60 FOLLOWING: [ 'createdAt', 'redundancyAllowed', 'state' ], 60 FOLLOWING: [ 'createdAt', 'redundancyAllowed', 'state' ],
61 61
62 VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes', 'trending' ], 62 VIDEOS: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'trending' ],
63 63
64 VIDEOS_SEARCH: [ 'name', 'duration', 'createdAt', 'publishedAt', 'views', 'likes', 'match' ], 64 VIDEOS_SEARCH: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'match' ],
65 VIDEO_CHANNELS_SEARCH: [ 'match', 'displayName', 'createdAt' ], 65 VIDEO_CHANNELS_SEARCH: [ 'match', 'displayName', 'createdAt' ],
66 66
67 ACCOUNTS_BLOCKLIST: [ 'createdAt' ], 67 ACCOUNTS_BLOCKLIST: [ 'createdAt' ],
@@ -73,7 +73,9 @@ const SORTABLE_COLUMNS = {
73 73
74 PLUGINS: [ 'name', 'createdAt', 'updatedAt' ], 74 PLUGINS: [ 'name', 'createdAt', 'updatedAt' ],
75 75
76 AVAILABLE_PLUGINS: [ 'npmName', 'popularity' ] 76 AVAILABLE_PLUGINS: [ 'npmName', 'popularity' ],
77
78 VIDEO_REDUNDANCIES: [ 'name' ]
77} 79}
78 80
79const OAUTH_LIFETIME = { 81const OAUTH_LIFETIME = {
@@ -88,9 +90,6 @@ const ROUTE_CACHE_LIFETIME = {
88 SECURITYTXT: '2 hours', 90 SECURITYTXT: '2 hours',
89 NODEINFO: '10 minutes', 91 NODEINFO: '10 minutes',
90 DNT_POLICY: '1 week', 92 DNT_POLICY: '1 week',
91 OVERVIEWS: {
92 VIDEOS: '1 hour'
93 },
94 ACTIVITY_PUB: { 93 ACTIVITY_PUB: {
95 VIDEOS: '1 second' // 1 second, cache concurrent requests after a broadcast for example 94 VIDEOS: '1 second' // 1 second, cache concurrent requests after a broadcast for example
96 }, 95 },
@@ -117,45 +116,44 @@ const REMOTE_SCHEME = {
117 WS: 'wss' 116 WS: 'wss'
118} 117}
119 118
120// TODO: remove 'video-file' 119const JOB_ATTEMPTS: { [id in JobType]: number } = {
121const JOB_ATTEMPTS: { [id in (JobType | 'video-file')]: number } = {
122 'activitypub-http-broadcast': 5, 120 'activitypub-http-broadcast': 5,
123 'activitypub-http-unicast': 5, 121 'activitypub-http-unicast': 5,
124 'activitypub-http-fetcher': 5, 122 'activitypub-http-fetcher': 5,
125 'activitypub-follow': 5, 123 'activitypub-follow': 5,
126 'video-file-import': 1, 124 'video-file-import': 1,
127 'video-transcoding': 1, 125 'video-transcoding': 1,
128 'video-file': 1,
129 'video-import': 1, 126 'video-import': 1,
130 'email': 5, 127 'email': 5,
131 'videos-views': 1, 128 'videos-views': 1,
132 'activitypub-refresher': 1 129 'activitypub-refresher': 1,
130 'video-redundancy': 1
133} 131}
134const JOB_CONCURRENCY: { [id in (JobType | 'video-file')]: number } = { 132const JOB_CONCURRENCY: { [id in JobType]: number } = {
135 'activitypub-http-broadcast': 1, 133 'activitypub-http-broadcast': 1,
136 'activitypub-http-unicast': 5, 134 'activitypub-http-unicast': 5,
137 'activitypub-http-fetcher': 1, 135 'activitypub-http-fetcher': 1,
138 'activitypub-follow': 1, 136 'activitypub-follow': 1,
139 'video-file-import': 1, 137 'video-file-import': 1,
140 'video-transcoding': 1, 138 'video-transcoding': 1,
141 'video-file': 1,
142 'video-import': 1, 139 'video-import': 1,
143 'email': 5, 140 'email': 5,
144 'videos-views': 1, 141 'videos-views': 1,
145 'activitypub-refresher': 1 142 'activitypub-refresher': 1,
143 'video-redundancy': 1
146} 144}
147const JOB_TTL: { [id in (JobType | 'video-file')]: number } = { 145const JOB_TTL: { [id in JobType]: number } = {
148 'activitypub-http-broadcast': 60000 * 10, // 10 minutes 146 'activitypub-http-broadcast': 60000 * 10, // 10 minutes
149 'activitypub-http-unicast': 60000 * 10, // 10 minutes 147 'activitypub-http-unicast': 60000 * 10, // 10 minutes
150 'activitypub-http-fetcher': 60000 * 10, // 10 minutes 148 'activitypub-http-fetcher': 1000 * 3600 * 10, // 10 hours
151 'activitypub-follow': 60000 * 10, // 10 minutes 149 'activitypub-follow': 60000 * 10, // 10 minutes
152 'video-file-import': 1000 * 3600, // 1 hour 150 'video-file-import': 1000 * 3600, // 1 hour
153 'video-transcoding': 1000 * 3600 * 48, // 2 days, transcoding could be long 151 'video-transcoding': 1000 * 3600 * 48, // 2 days, transcoding could be long
154 'video-file': 1000 * 3600 * 48, // 2 days, transcoding could be long 152 'video-import': 1000 * 3600 * 2, // 2 hours
155 'video-import': 1000 * 3600 * 2, // hours
156 'email': 60000 * 10, // 10 minutes 153 'email': 60000 * 10, // 10 minutes
157 'videos-views': undefined, // Unlimited 154 'videos-views': undefined, // Unlimited
158 'activitypub-refresher': 60000 * 10 // 10 minutes 155 'activitypub-refresher': 60000 * 10, // 10 minutes
156 'video-redundancy': 1000 * 3600 * 3 // 3 hours
159} 157}
160const REPEAT_JOBS: { [ id: string ]: EveryRepeatOptions | CronRepeatOptions } = { 158const REPEAT_JOBS: { [ id: string ]: EveryRepeatOptions | CronRepeatOptions } = {
161 'videos-views': { 159 'videos-views': {
@@ -285,7 +283,7 @@ const CONSTRAINTS_FIELDS = {
285 COUNT: { min: 0 } 283 COUNT: { min: 0 }
286 }, 284 },
287 VIDEO_COMMENTS: { 285 VIDEO_COMMENTS: {
288 TEXT: { min: 1, max: 3000 }, // Length 286 TEXT: { min: 1, max: 10000 }, // Length
289 URL: { min: 3, max: 2000 } // Length 287 URL: { min: 3, max: 2000 } // Length
290 }, 288 },
291 VIDEO_SHARE: { 289 VIDEO_SHARE: {
@@ -309,6 +307,8 @@ let CONTACT_FORM_LIFETIME = 60000 * 60 // 1 hour
309 307
310const VIDEO_TRANSCODING_FPS: VideoTranscodingFPS = { 308const VIDEO_TRANSCODING_FPS: VideoTranscodingFPS = {
311 MIN: 10, 309 MIN: 10,
310 STANDARD: [ 24, 25, 30 ],
311 HD_STANDARD: [ 50, 60 ],
312 AVERAGE: 30, 312 AVERAGE: 30,
313 MAX: 60, 313 MAX: 60,
314 KEEP_ORIGIN_FPS_RESOLUTION_MIN: 720 // We keep the original FPS on high resolutions (720 minimum) 314 KEEP_ORIGIN_FPS_RESOLUTION_MIN: 720 // We keep the original FPS on high resolutions (720 minimum)
@@ -358,42 +358,42 @@ const VIDEO_LICENCES = {
358 7: 'Public Domain Dedication' 358 7: 'Public Domain Dedication'
359} 359}
360 360
361let VIDEO_LANGUAGES: { [id: string]: string } = {} 361const VIDEO_LANGUAGES: { [id: string]: string } = {}
362 362
363const VIDEO_PRIVACIES = { 363const VIDEO_PRIVACIES = {
364 [ VideoPrivacy.PUBLIC ]: 'Public', 364 [VideoPrivacy.PUBLIC]: 'Public',
365 [ VideoPrivacy.UNLISTED ]: 'Unlisted', 365 [VideoPrivacy.UNLISTED]: 'Unlisted',
366 [ VideoPrivacy.PRIVATE ]: 'Private', 366 [VideoPrivacy.PRIVATE]: 'Private',
367 [ VideoPrivacy.INTERNAL ]: 'Internal' 367 [VideoPrivacy.INTERNAL]: 'Internal'
368} 368}
369 369
370const VIDEO_STATES = { 370const VIDEO_STATES = {
371 [ VideoState.PUBLISHED ]: 'Published', 371 [VideoState.PUBLISHED]: 'Published',
372 [ VideoState.TO_TRANSCODE ]: 'To transcode', 372 [VideoState.TO_TRANSCODE]: 'To transcode',
373 [ VideoState.TO_IMPORT ]: 'To import' 373 [VideoState.TO_IMPORT]: 'To import'
374} 374}
375 375
376const VIDEO_IMPORT_STATES = { 376const VIDEO_IMPORT_STATES = {
377 [ VideoImportState.FAILED ]: 'Failed', 377 [VideoImportState.FAILED]: 'Failed',
378 [ VideoImportState.PENDING ]: 'Pending', 378 [VideoImportState.PENDING]: 'Pending',
379 [ VideoImportState.SUCCESS ]: 'Success' 379 [VideoImportState.SUCCESS]: 'Success'
380} 380}
381 381
382const VIDEO_ABUSE_STATES = { 382const VIDEO_ABUSE_STATES = {
383 [ VideoAbuseState.PENDING ]: 'Pending', 383 [VideoAbuseState.PENDING]: 'Pending',
384 [ VideoAbuseState.REJECTED ]: 'Rejected', 384 [VideoAbuseState.REJECTED]: 'Rejected',
385 [ VideoAbuseState.ACCEPTED ]: 'Accepted' 385 [VideoAbuseState.ACCEPTED]: 'Accepted'
386} 386}
387 387
388const VIDEO_PLAYLIST_PRIVACIES = { 388const VIDEO_PLAYLIST_PRIVACIES = {
389 [ VideoPlaylistPrivacy.PUBLIC ]: 'Public', 389 [VideoPlaylistPrivacy.PUBLIC]: 'Public',
390 [ VideoPlaylistPrivacy.UNLISTED ]: 'Unlisted', 390 [VideoPlaylistPrivacy.UNLISTED]: 'Unlisted',
391 [ VideoPlaylistPrivacy.PRIVATE ]: 'Private' 391 [VideoPlaylistPrivacy.PRIVATE]: 'Private'
392} 392}
393 393
394const VIDEO_PLAYLIST_TYPES = { 394const VIDEO_PLAYLIST_TYPES = {
395 [ VideoPlaylistType.REGULAR ]: 'Regular', 395 [VideoPlaylistType.REGULAR]: 'Regular',
396 [ VideoPlaylistType.WATCH_LATER ]: 'Watch later' 396 [VideoPlaylistType.WATCH_LATER]: 'Watch later'
397} 397}
398 398
399const MIMETYPES = { 399const MIMETYPES = {
@@ -419,7 +419,8 @@ const MIMETYPES = {
419 'image/png': '.png', 419 'image/png': '.png',
420 'image/jpg': '.jpg', 420 'image/jpg': '.jpg',
421 'image/jpeg': '.jpg' 421 'image/jpeg': '.jpg'
422 } 422 },
423 EXT_MIMETYPE: null as { [ id: string ]: string }
423 }, 424 },
424 VIDEO_CAPTIONS: { 425 VIDEO_CAPTIONS: {
425 MIMETYPE_EXT: { 426 MIMETYPE_EXT: {
@@ -435,13 +436,14 @@ const MIMETYPES = {
435 } 436 }
436} 437}
437MIMETYPES.AUDIO.EXT_MIMETYPE = invert(MIMETYPES.AUDIO.MIMETYPE_EXT) 438MIMETYPES.AUDIO.EXT_MIMETYPE = invert(MIMETYPES.AUDIO.MIMETYPE_EXT)
439MIMETYPES.IMAGE.EXT_MIMETYPE = invert(MIMETYPES.IMAGE.MIMETYPE_EXT)
438 440
439// --------------------------------------------------------------------------- 441// ---------------------------------------------------------------------------
440 442
441const OVERVIEWS = { 443const OVERVIEWS = {
442 VIDEOS: { 444 VIDEOS: {
443 SAMPLE_THRESHOLD: 6, 445 SAMPLE_THRESHOLD: 6,
444 SAMPLES_COUNT: 2 446 SAMPLES_COUNT: 20
445 } 447 }
446} 448}
447 449
@@ -462,7 +464,7 @@ const ACTIVITY_PUB = {
462 ACCEPT_HEADER: 'application/activity+json, application/ld+json', 464 ACCEPT_HEADER: 'application/activity+json, application/ld+json',
463 PUBLIC: 'https://www.w3.org/ns/activitystreams#Public', 465 PUBLIC: 'https://www.w3.org/ns/activitystreams#Public',
464 COLLECTION_ITEMS_PER_PAGE: 10, 466 COLLECTION_ITEMS_PER_PAGE: 10,
465 FETCH_PAGE_LIMIT: 100, 467 FETCH_PAGE_LIMIT: 2000,
466 URL_MIME_TYPES: { 468 URL_MIME_TYPES: {
467 VIDEO: [] as string[], 469 VIDEO: [] as string[],
468 TORRENT: [ 'application/x-bittorrent' ], 470 TORRENT: [ 'application/x-bittorrent' ],
@@ -497,6 +499,7 @@ let PRIVATE_RSA_KEY_SIZE = 2048
497const BCRYPT_SALT_SIZE = 10 499const BCRYPT_SALT_SIZE = 10
498 500
499const USER_PASSWORD_RESET_LIFETIME = 60000 * 60 // 60 minutes 501const USER_PASSWORD_RESET_LIFETIME = 60000 * 60 // 60 minutes
502const USER_PASSWORD_CREATE_LIFETIME = 60000 * 60 * 24 * 7 // 7 days
500 503
501const USER_EMAIL_VERIFY_LIFETIME = 60000 * 60 // 60 minutes 504const USER_EMAIL_VERIFY_LIFETIME = 60000 * 60 // 60 minutes
502 505
@@ -533,7 +536,7 @@ const LAZY_STATIC_PATHS = {
533} 536}
534 537
535// Cache control 538// Cache control
536let STATIC_MAX_AGE = { 539const STATIC_MAX_AGE = {
537 SERVER: '2h', 540 SERVER: '2h',
538 CLIENT: '30d' 541 CLIENT: '30d'
539} 542}
@@ -541,11 +544,13 @@ let STATIC_MAX_AGE = {
541// Videos thumbnail size 544// Videos thumbnail size
542const THUMBNAILS_SIZE = { 545const THUMBNAILS_SIZE = {
543 width: 223, 546 width: 223,
544 height: 122 547 height: 122,
548 minWidth: 150
545} 549}
546const PREVIEWS_SIZE = { 550const PREVIEWS_SIZE = {
547 width: 850, 551 width: 850,
548 height: 480 552 height: 480,
553 minWidth: 400
549} 554}
550const AVATARS_SIZE = { 555const AVATARS_SIZE = {
551 width: 120, 556 width: 120,
@@ -640,6 +645,8 @@ const P2P_MEDIA_LOADER_PEER_VERSION = 2
640const PLUGIN_GLOBAL_CSS_FILE_NAME = 'plugins-global.css' 645const PLUGIN_GLOBAL_CSS_FILE_NAME = 'plugins-global.css'
641const PLUGIN_GLOBAL_CSS_PATH = join(CONFIG.STORAGE.TMP_DIR, PLUGIN_GLOBAL_CSS_FILE_NAME) 646const PLUGIN_GLOBAL_CSS_PATH = join(CONFIG.STORAGE.TMP_DIR, PLUGIN_GLOBAL_CSS_FILE_NAME)
642 647
648let PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME = 1000 * 60 * 5 // 5 minutes
649
643const DEFAULT_THEME_NAME = 'default' 650const DEFAULT_THEME_NAME = 'default'
644const DEFAULT_USER_THEME_NAME = 'instance-default' 651const DEFAULT_USER_THEME_NAME = 'instance-default'
645 652
@@ -669,18 +676,20 @@ if (isTestInstance() === true) {
669 SCHEDULER_INTERVALS_MS.removeOldViews = 5000 676 SCHEDULER_INTERVALS_MS.removeOldViews = 5000
670 SCHEDULER_INTERVALS_MS.updateVideos = 5000 677 SCHEDULER_INTERVALS_MS.updateVideos = 5000
671 SCHEDULER_INTERVALS_MS.autoFollowIndexInstances = 5000 678 SCHEDULER_INTERVALS_MS.autoFollowIndexInstances = 5000
672 REPEAT_JOBS[ 'videos-views' ] = { every: 5000 } 679 REPEAT_JOBS['videos-views'] = { every: 5000 }
673 680
674 REDUNDANCY.VIDEOS.RANDOMIZED_FACTOR = 1 681 REDUNDANCY.VIDEOS.RANDOMIZED_FACTOR = 1
675 682
676 VIDEO_VIEW_LIFETIME = 1000 // 1 second 683 VIDEO_VIEW_LIFETIME = 1000 // 1 second
677 CONTACT_FORM_LIFETIME = 1000 // 1 second 684 CONTACT_FORM_LIFETIME = 1000 // 1 second
678 685
679 JOB_ATTEMPTS[ 'email' ] = 1 686 JOB_ATTEMPTS['email'] = 1
680 687
681 FILES_CACHE.VIDEO_CAPTIONS.MAX_AGE = 3000 688 FILES_CACHE.VIDEO_CAPTIONS.MAX_AGE = 3000
682 MEMOIZE_TTL.OVERVIEWS_SAMPLE = 1 689 MEMOIZE_TTL.OVERVIEWS_SAMPLE = 3000
683 ROUTE_CACHE_LIFETIME.OVERVIEWS.VIDEOS = '0ms' 690 OVERVIEWS.VIDEOS.SAMPLE_THRESHOLD = 2
691
692 PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME = 5000
684} 693}
685 694
686updateWebserverUrls() 695updateWebserverUrls()
@@ -757,6 +766,7 @@ export {
757 LRU_CACHE, 766 LRU_CACHE,
758 JOB_REQUEST_TIMEOUT, 767 JOB_REQUEST_TIMEOUT,
759 USER_PASSWORD_RESET_LIFETIME, 768 USER_PASSWORD_RESET_LIFETIME,
769 USER_PASSWORD_CREATE_LIFETIME,
760 MEMOIZE_TTL, 770 MEMOIZE_TTL,
761 USER_EMAIL_VERIFY_LIFETIME, 771 USER_EMAIL_VERIFY_LIFETIME,
762 OVERVIEWS, 772 OVERVIEWS,
@@ -772,6 +782,7 @@ export {
772 VIDEO_VIEW_LIFETIME, 782 VIDEO_VIEW_LIFETIME,
773 CONTACT_FORM_LIFETIME, 783 CONTACT_FORM_LIFETIME,
774 VIDEO_PLAYLIST_PRIVACIES, 784 VIDEO_PLAYLIST_PRIVACIES,
785 PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME,
775 ASSETS_PATH, 786 ASSETS_PATH,
776 loadLanguages, 787 loadLanguages,
777 buildLanguages 788 buildLanguages
@@ -837,42 +848,42 @@ function loadLanguages () {
837function buildLanguages () { 848function buildLanguages () {
838 const iso639 = require('iso-639-3') 849 const iso639 = require('iso-639-3')
839 850
840 const languages: { [ id: string ]: string } = {} 851 const languages: { [id: string]: string } = {}
841 852
842 const additionalLanguages = { 853 const additionalLanguages = {
843 'sgn': true, // Sign languages (macro language) 854 sgn: true, // Sign languages (macro language)
844 'ase': true, // American sign language 855 ase: true, // American sign language
845 'sdl': true, // Arabian sign language 856 sdl: true, // Arabian sign language
846 'bfi': true, // British sign language 857 bfi: true, // British sign language
847 'bzs': true, // Brazilian sign language 858 bzs: true, // Brazilian sign language
848 'csl': true, // Chinese sign language 859 csl: true, // Chinese sign language
849 'cse': true, // Czech sign language 860 cse: true, // Czech sign language
850 'dsl': true, // Danish sign language 861 dsl: true, // Danish sign language
851 'fsl': true, // French sign language 862 fsl: true, // French sign language
852 'gsg': true, // German sign language 863 gsg: true, // German sign language
853 'pks': true, // Pakistan sign language 864 pks: true, // Pakistan sign language
854 'jsl': true, // Japanese sign language 865 jsl: true, // Japanese sign language
855 'sfs': true, // South African sign language 866 sfs: true, // South African sign language
856 'swl': true, // Swedish sign language 867 swl: true, // Swedish sign language
857 'rsl': true, // Russian sign language: true 868 rsl: true, // Russian sign language: true
858 869
859 'epo': true, // Esperanto 870 epo: true, // Esperanto
860 'tlh': true, // Klingon 871 tlh: true, // Klingon
861 'jbo': true, // Lojban 872 jbo: true, // Lojban
862 'avk': true // Kotava 873 avk: true // Kotava
863 } 874 }
864 875
865 // Only add ISO639-1 languages and some sign languages (ISO639-3) 876 // Only add ISO639-1 languages and some sign languages (ISO639-3)
866 iso639 877 iso639
867 .filter(l => { 878 .filter(l => {
868 return (l.iso6391 !== null && l.type === 'living') || 879 return (l.iso6391 !== undefined && l.type === 'living') ||
869 additionalLanguages[ l.iso6393 ] === true 880 additionalLanguages[l.iso6393] === true
870 }) 881 })
871 .forEach(l => languages[ l.iso6391 || l.iso6393 ] = l.name) 882 .forEach(l => { languages[l.iso6391 || l.iso6393] = l.name })
872 883
873 // Override Occitan label 884 // Override Occitan label
874 languages[ 'oc' ] = 'Occitan' 885 languages['oc'] = 'Occitan'
875 languages[ 'el' ] = 'Greek' 886 languages['el'] = 'Greek'
876 887
877 return languages 888 return languages
878} 889}
diff --git a/server/initializers/database.ts b/server/initializers/database.ts
index 9ec146ab1..eedaf3c4e 100644
--- a/server/initializers/database.ts
+++ b/server/initializers/database.ts
@@ -119,8 +119,6 @@ async function initDatabaseModels (silent: boolean) {
119 await createFunctions() 119 await createFunctions()
120 120
121 if (!silent) logger.info('Database %s is ready.', dbname) 121 if (!silent) logger.info('Database %s is ready.', dbname)
122
123 return
124} 122}
125 123
126// --------------------------------------------------------------------------- 124// ---------------------------------------------------------------------------
diff --git a/server/initializers/index.ts b/server/initializers/index.ts
deleted file mode 100644
index 0fc1a7363..000000000
--- a/server/initializers/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
1export * from './database'
2export * from './installer'
3export * from './migrator'
diff --git a/server/initializers/migrations/0005-email-pod.ts b/server/initializers/migrations/0005-email-pod.ts
index c34a12255..417c33b1f 100644
--- a/server/initializers/migrations/0005-email-pod.ts
+++ b/server/initializers/migrations/0005-email-pod.ts
@@ -3,8 +3,8 @@ import * as Promise from 'bluebird'
3import { Migration } from '../../models/migrations' 3import { Migration } from '../../models/migrations'
4 4
5function up (utils: { 5function up (utils: {
6 transaction: Sequelize.Transaction, 6 transaction: Sequelize.Transaction
7 queryInterface: Sequelize.QueryInterface, 7 queryInterface: Sequelize.QueryInterface
8 sequelize: Sequelize.Sequelize 8 sequelize: Sequelize.Sequelize
9}): Promise<void> { 9}): Promise<void> {
10 const q = utils.queryInterface 10 const q = utils.queryInterface
diff --git a/server/initializers/migrations/0010-email-user.ts b/server/initializers/migrations/0010-email-user.ts
index 37a7b0bb3..f7d01f6d6 100644
--- a/server/initializers/migrations/0010-email-user.ts
+++ b/server/initializers/migrations/0010-email-user.ts
@@ -3,8 +3,8 @@ import * as Promise from 'bluebird'
3import { Migration } from '../../models/migrations' 3import { Migration } from '../../models/migrations'
4 4
5function up (utils: { 5function up (utils: {
6 transaction: Sequelize.Transaction, 6 transaction: Sequelize.Transaction
7 queryInterface: Sequelize.QueryInterface, 7 queryInterface: Sequelize.QueryInterface
8 sequelize: Sequelize.Sequelize 8 sequelize: Sequelize.Sequelize
9}): Promise<void> { 9}): Promise<void> {
10 const q = utils.queryInterface 10 const q = utils.queryInterface
diff --git a/server/initializers/migrations/0015-video-views.ts b/server/initializers/migrations/0015-video-views.ts
index 25164ff4d..47dd4069b 100644
--- a/server/initializers/migrations/0015-video-views.ts
+++ b/server/initializers/migrations/0015-video-views.ts
@@ -2,8 +2,8 @@ import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird' 2import * as Promise from 'bluebird'
3 3
4function up (utils: { 4function 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 q = utils.queryInterface 9 const q = utils.queryInterface
diff --git a/server/initializers/migrations/0020-video-likes.ts b/server/initializers/migrations/0020-video-likes.ts
index 945be5a98..44333f3b0 100644
--- a/server/initializers/migrations/0020-video-likes.ts
+++ b/server/initializers/migrations/0020-video-likes.ts
@@ -2,8 +2,8 @@ import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird' 2import * as Promise from 'bluebird'
3 3
4function up (utils: { 4function 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 q = utils.queryInterface 9 const q = utils.queryInterface
diff --git a/server/initializers/migrations/0025-video-dislikes.ts b/server/initializers/migrations/0025-video-dislikes.ts
index 27144c437..2aa22e2d7 100644
--- a/server/initializers/migrations/0025-video-dislikes.ts
+++ b/server/initializers/migrations/0025-video-dislikes.ts
@@ -2,8 +2,8 @@ import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird' 2import * as Promise from 'bluebird'
3 3
4function up (utils: { 4function 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 q = utils.queryInterface 9 const q = utils.queryInterface
diff --git a/server/initializers/migrations/0030-video-category.ts b/server/initializers/migrations/0030-video-category.ts
index f784f820d..00cd2d8cf 100644
--- a/server/initializers/migrations/0030-video-category.ts
+++ b/server/initializers/migrations/0030-video-category.ts
@@ -3,8 +3,8 @@ import * as Promise from 'bluebird'
3import { Migration } from '../../models/migrations' 3import { Migration } from '../../models/migrations'
4 4
5function up (utils: { 5function up (utils: {
6 transaction: Sequelize.Transaction, 6 transaction: Sequelize.Transaction
7 queryInterface: Sequelize.QueryInterface, 7 queryInterface: Sequelize.QueryInterface
8 sequelize: Sequelize.Sequelize 8 sequelize: Sequelize.Sequelize
9}): Promise<void> { 9}): Promise<void> {
10 const q = utils.queryInterface 10 const q = utils.queryInterface
diff --git a/server/initializers/migrations/0035-video-licence.ts b/server/initializers/migrations/0035-video-licence.ts
index 3d0b0bac9..61d666c5e 100644
--- a/server/initializers/migrations/0035-video-licence.ts
+++ b/server/initializers/migrations/0035-video-licence.ts
@@ -3,8 +3,8 @@ import * as Promise from 'bluebird'
3import { Migration } from '../../models/migrations' 3import { Migration } from '../../models/migrations'
4 4
5function up (utils: { 5function up (utils: {
6 transaction: Sequelize.Transaction, 6 transaction: Sequelize.Transaction
7 queryInterface: Sequelize.QueryInterface, 7 queryInterface: Sequelize.QueryInterface
8 sequelize: Sequelize.Sequelize 8 sequelize: Sequelize.Sequelize
9}): Promise<void> { 9}): Promise<void> {
10 const q = utils.queryInterface 10 const q = utils.queryInterface
diff --git a/server/initializers/migrations/0040-video-nsfw.ts b/server/initializers/migrations/0040-video-nsfw.ts
index f7f70d3c4..44aec8a6c 100644
--- a/server/initializers/migrations/0040-video-nsfw.ts
+++ b/server/initializers/migrations/0040-video-nsfw.ts
@@ -3,8 +3,8 @@ import * as Promise from 'bluebird'
3import { Migration } from '../../models/migrations' 3import { Migration } from '../../models/migrations'
4 4
5function up (utils: { 5function up (utils: {
6 transaction: Sequelize.Transaction, 6 transaction: Sequelize.Transaction
7 queryInterface: Sequelize.QueryInterface, 7 queryInterface: Sequelize.QueryInterface
8 sequelize: Sequelize.Sequelize 8 sequelize: Sequelize.Sequelize
9}): Promise<void> { 9}): Promise<void> {
10 const q = utils.queryInterface 10 const q = utils.queryInterface
diff --git a/server/initializers/migrations/0045-user-display-nsfw.ts b/server/initializers/migrations/0045-user-display-nsfw.ts
index aef420f0e..07795bd75 100644
--- a/server/initializers/migrations/0045-user-display-nsfw.ts
+++ b/server/initializers/migrations/0045-user-display-nsfw.ts
@@ -2,8 +2,8 @@ import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird' 2import * as Promise from 'bluebird'
3 3
4function up (utils: { 4function 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 q = utils.queryInterface 9 const q = utils.queryInterface
diff --git a/server/initializers/migrations/0050-video-language.ts b/server/initializers/migrations/0050-video-language.ts
index 796fa5f95..6f90abb44 100644
--- a/server/initializers/migrations/0050-video-language.ts
+++ b/server/initializers/migrations/0050-video-language.ts
@@ -2,8 +2,8 @@ import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird' 2import * as Promise from 'bluebird'
3 3
4function up (utils: { 4function 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 q = utils.queryInterface 9 const q = utils.queryInterface
diff --git a/server/initializers/migrations/0055-video-uuid.ts b/server/initializers/migrations/0055-video-uuid.ts
index e0f904080..8a58aebb8 100644
--- a/server/initializers/migrations/0055-video-uuid.ts
+++ b/server/initializers/migrations/0055-video-uuid.ts
@@ -3,8 +3,8 @@ import * as Promise from 'bluebird'
3import { Migration } from '../../models/migrations' 3import { Migration } from '../../models/migrations'
4 4
5function up (utils: { 5function up (utils: {
6 transaction: Sequelize.Transaction, 6 transaction: Sequelize.Transaction
7 queryInterface: Sequelize.QueryInterface, 7 queryInterface: Sequelize.QueryInterface
8 sequelize: Sequelize.Sequelize 8 sequelize: Sequelize.Sequelize
9}): Promise<void> { 9}): Promise<void> {
10 const q = utils.queryInterface 10 const q = utils.queryInterface
diff --git a/server/initializers/migrations/0060-video-file.ts b/server/initializers/migrations/0060-video-file.ts
index c362cf71a..00647e60e 100644
--- a/server/initializers/migrations/0060-video-file.ts
+++ b/server/initializers/migrations/0060-video-file.ts
@@ -2,9 +2,9 @@ import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird' 2import * as Promise from 'bluebird'
3 3
4function up (utils: { 4function 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 db: any 8 db: any
9}): Promise<void> { 9}): Promise<void> {
10 const q = utils.queryInterface 10 const q = utils.queryInterface
diff --git a/server/initializers/migrations/0065-video-file-size.ts b/server/initializers/migrations/0065-video-file-size.ts
index e9ce77e50..0bdc675c2 100644
--- a/server/initializers/migrations/0065-video-file-size.ts
+++ b/server/initializers/migrations/0065-video-file-size.ts
@@ -5,9 +5,9 @@ import { VideoModel } from '../../models/video/video'
5import { getVideoFilePath } from '@server/lib/video-paths' 5import { getVideoFilePath } from '@server/lib/video-paths'
6 6
7function up (utils: { 7function up (utils: {
8 transaction: Sequelize.Transaction, 8 transaction: Sequelize.Transaction
9 queryInterface: Sequelize.QueryInterface, 9 queryInterface: Sequelize.QueryInterface
10 sequelize: Sequelize.Sequelize, 10 sequelize: Sequelize.Sequelize
11 db: any 11 db: any
12}): Promise<void> { 12}): Promise<void> {
13 return utils.db.Video.listOwnedAndPopulateAuthorAndTags() 13 return utils.db.Video.listOwnedAndPopulateAuthorAndTags()
diff --git a/server/initializers/migrations/0070-user-video-quota.ts b/server/initializers/migrations/0070-user-video-quota.ts
index 37683432f..1d073f244 100644
--- a/server/initializers/migrations/0070-user-video-quota.ts
+++ b/server/initializers/migrations/0070-user-video-quota.ts
@@ -3,9 +3,9 @@ import * as Promise from 'bluebird'
3import { Migration } from '../../models/migrations' 3import { Migration } from '../../models/migrations'
4 4
5function up (utils: { 5function up (utils: {
6 transaction: Sequelize.Transaction, 6 transaction: Sequelize.Transaction
7 queryInterface: Sequelize.QueryInterface, 7 queryInterface: Sequelize.QueryInterface
8 sequelize: Sequelize.Sequelize, 8 sequelize: Sequelize.Sequelize
9 db: any 9 db: any
10}): Promise<void> { 10}): Promise<void> {
11 const q = utils.queryInterface 11 const q = utils.queryInterface
diff --git a/server/initializers/migrations/0075-video-resolutions.ts b/server/initializers/migrations/0075-video-resolutions.ts
index e4f26cb77..f56c1b2c3 100644
--- a/server/initializers/migrations/0075-video-resolutions.ts
+++ b/server/initializers/migrations/0075-video-resolutions.ts
@@ -5,9 +5,9 @@ import { getVideoFileResolution } from '../../helpers/ffmpeg-utils'
5import { readdir, rename } from 'fs-extra' 5import { readdir, rename } from 'fs-extra'
6 6
7function up (utils: { 7function up (utils: {
8 transaction: Sequelize.Transaction, 8 transaction: Sequelize.Transaction
9 queryInterface: Sequelize.QueryInterface, 9 queryInterface: Sequelize.QueryInterface
10 sequelize: Sequelize.Sequelize, 10 sequelize: Sequelize.Sequelize
11 db: any 11 db: any
12}): Promise<void> { 12}): Promise<void> {
13 const torrentDir = CONFIG.STORAGE.TORRENTS_DIR 13 const torrentDir = CONFIG.STORAGE.TORRENTS_DIR
diff --git a/server/initializers/migrations/0080-video-channels.ts b/server/initializers/migrations/0080-video-channels.ts
index 5512bdcf1..883224cb0 100644
--- a/server/initializers/migrations/0080-video-channels.ts
+++ b/server/initializers/migrations/0080-video-channels.ts
@@ -1,10 +1,10 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import * as uuidv4 from 'uuid/v4' 2import { v4 as uuidv4 } from 'uuid'
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 db: any 8 db: any
9}): Promise<void> { 9}): Promise<void> {
10 const q = utils.queryInterface 10 const q = utils.queryInterface
diff --git a/server/initializers/migrations/0085-user-role.ts b/server/initializers/migrations/0085-user-role.ts
index de75faec2..ec7428fd5 100644
--- a/server/initializers/migrations/0085-user-role.ts
+++ b/server/initializers/migrations/0085-user-role.ts
@@ -1,9 +1,9 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize, 6 sequelize: Sequelize.Sequelize
7 db: any 7 db: any
8}): Promise<void> { 8}): Promise<void> {
9 const q = utils.queryInterface 9 const q = utils.queryInterface
diff --git a/server/initializers/migrations/0090-videos-description.ts b/server/initializers/migrations/0090-videos-description.ts
index 6f98dcade..32e518d75 100644
--- a/server/initializers/migrations/0090-videos-description.ts
+++ b/server/initializers/migrations/0090-videos-description.ts
@@ -1,9 +1,9 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize, 6 sequelize: Sequelize.Sequelize
7 db: any 7 db: any
8}): Promise<void> { 8}): Promise<void> {
9 const q = utils.queryInterface 9 const q = utils.queryInterface
diff --git a/server/initializers/migrations/0095-videos-privacy.ts b/server/initializers/migrations/0095-videos-privacy.ts
index 4c2bf91d0..c732d6f6a 100644
--- a/server/initializers/migrations/0095-videos-privacy.ts
+++ b/server/initializers/migrations/0095-videos-privacy.ts
@@ -1,9 +1,9 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize, 6 sequelize: Sequelize.Sequelize
7 db: any 7 db: any
8}): Promise<void> { 8}): Promise<void> {
9 const q = utils.queryInterface 9 const q = utils.queryInterface
diff --git a/server/initializers/migrations/0100-activitypub.ts b/server/initializers/migrations/0100-activitypub.ts
index 96d44a7ce..05fd37406 100644
--- a/server/initializers/migrations/0100-activitypub.ts
+++ b/server/initializers/migrations/0100-activitypub.ts
@@ -7,9 +7,9 @@ import { ApplicationModel } from '../../models/application/application'
7import { SERVER_ACTOR_NAME } from '../constants' 7import { SERVER_ACTOR_NAME } from '../constants'
8 8
9async function up (utils: { 9async function up (utils: {
10 transaction: Sequelize.Transaction, 10 transaction: Sequelize.Transaction
11 queryInterface: Sequelize.QueryInterface, 11 queryInterface: Sequelize.QueryInterface
12 sequelize: Sequelize.Sequelize, 12 sequelize: Sequelize.Sequelize
13 db: any 13 db: any
14}): Promise<void> { 14}): Promise<void> {
15 const q = utils.queryInterface 15 const q = utils.queryInterface
diff --git a/server/initializers/migrations/0105-server-mail.ts b/server/initializers/migrations/0105-server-mail.ts
index 4b9600e91..5ee37c418 100644
--- a/server/initializers/migrations/0105-server-mail.ts
+++ b/server/initializers/migrations/0105-server-mail.ts
@@ -1,9 +1,9 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize, 6 sequelize: Sequelize.Sequelize
7 db: any 7 db: any
8}): Promise<void> { 8}): Promise<void> {
9 await utils.queryInterface.removeColumn('Servers', 'email') 9 await utils.queryInterface.removeColumn('Servers', 'email')
diff --git a/server/initializers/migrations/0110-server-key.ts b/server/initializers/migrations/0110-server-key.ts
index 5ff6daf69..354cd7e76 100644
--- a/server/initializers/migrations/0110-server-key.ts
+++ b/server/initializers/migrations/0110-server-key.ts
@@ -1,9 +1,9 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize, 6 sequelize: Sequelize.Sequelize
7 db: any 7 db: any
8}): Promise<void> { 8}): Promise<void> {
9 await utils.queryInterface.removeColumn('Servers', 'publicKey') 9 await utils.queryInterface.removeColumn('Servers', 'publicKey')
diff --git a/server/initializers/migrations/0115-account-avatar.ts b/server/initializers/migrations/0115-account-avatar.ts
index b318e8163..604b6394a 100644
--- a/server/initializers/migrations/0115-account-avatar.ts
+++ b/server/initializers/migrations/0115-account-avatar.ts
@@ -1,9 +1,9 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize, 6 sequelize: Sequelize.Sequelize
7 db: any 7 db: any
8}): Promise<void> { 8}): Promise<void> {
9 await utils.db.Avatar.sync() 9 await utils.db.Avatar.sync()
diff --git a/server/initializers/migrations/0120-video-null.ts b/server/initializers/migrations/0120-video-null.ts
index 6d253f04f..1b407b270 100644
--- a/server/initializers/migrations/0120-video-null.ts
+++ b/server/initializers/migrations/0120-video-null.ts
@@ -1,9 +1,9 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize, 6 sequelize: Sequelize.Sequelize
7 db: any 7 db: any
8}): Promise<void> { 8}): Promise<void> {
9 9
diff --git a/server/initializers/migrations/0125-table-lowercase.ts b/server/initializers/migrations/0125-table-lowercase.ts
index 78041ccb0..f75a56791 100644
--- a/server/initializers/migrations/0125-table-lowercase.ts
+++ b/server/initializers/migrations/0125-table-lowercase.ts
@@ -1,8 +1,8 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize 6 sequelize: Sequelize.Sequelize
7}): Promise<void> { 7}): Promise<void> {
8 await utils.queryInterface.renameTable('Applications', 'application') 8 await utils.queryInterface.renameTable('Applications', 'application')
diff --git a/server/initializers/migrations/0130-user-autoplay-video.ts b/server/initializers/migrations/0130-user-autoplay-video.ts
index 9f6878e39..d57934588 100644
--- a/server/initializers/migrations/0130-user-autoplay-video.ts
+++ b/server/initializers/migrations/0130-user-autoplay-video.ts
@@ -2,8 +2,8 @@ import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird' 2import * as Promise from 'bluebird'
3 3
4function up (utils: { 4function 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 q = utils.queryInterface 9 const q = utils.queryInterface
diff --git a/server/initializers/migrations/0135-video-channel-actor.ts b/server/initializers/migrations/0135-video-channel-actor.ts
index 5ace0f4d2..3f620dfa3 100644
--- a/server/initializers/migrations/0135-video-channel-actor.ts
+++ b/server/initializers/migrations/0135-video-channel-actor.ts
@@ -3,8 +3,8 @@ import { DataType } from 'sequelize-typescript'
3import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto' 3import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto'
4 4
5async function up (utils: { 5async function up (utils: {
6 transaction: Sequelize.Transaction, 6 transaction: Sequelize.Transaction
7 queryInterface: Sequelize.QueryInterface, 7 queryInterface: Sequelize.QueryInterface
8 sequelize: Sequelize.Sequelize 8 sequelize: Sequelize.Sequelize
9}): Promise<void> { 9}): Promise<void> {
10 // Create actor table 10 // Create actor table
@@ -64,10 +64,10 @@ async function up (utils: {
64 type, uuid, "preferredUsername", url, "publicKey", "privateKey", "followersCount", "followingCount", "inboxUrl", "outboxUrl", 64 type, uuid, "preferredUsername", url, "publicKey", "privateKey", "followersCount", "followingCount", "inboxUrl", "outboxUrl",
65 "sharedInboxUrl", "followersUrl", "followingUrl", "avatarId", "serverId", "createdAt", "updatedAt" 65 "sharedInboxUrl", "followersUrl", "followingUrl", "avatarId", "serverId", "createdAt", "updatedAt"
66 ) 66 )
67 SELECT 67 SELECT
68 'Application', uuid, name, url, "publicKey", "privateKey", "followersCount", "followingCount", "inboxUrl", "outboxUrl", 68 'Application', uuid, name, url, "publicKey", "privateKey", "followersCount", "followingCount", "inboxUrl", "outboxUrl",
69 "sharedInboxUrl", "followersUrl", "followingUrl", "avatarId", "serverId", "createdAt", "updatedAt" 69 "sharedInboxUrl", "followersUrl", "followingUrl", "avatarId", "serverId", "createdAt", "updatedAt"
70 FROM account 70 FROM account
71 WHERE "applicationId" IS NOT NULL 71 WHERE "applicationId" IS NOT NULL
72 ` 72 `
73 await utils.sequelize.query(query1) 73 await utils.sequelize.query(query1)
@@ -79,10 +79,10 @@ async function up (utils: {
79 type, uuid, "preferredUsername", url, "publicKey", "privateKey", "followersCount", "followingCount", "inboxUrl", "outboxUrl", 79 type, uuid, "preferredUsername", url, "publicKey", "privateKey", "followersCount", "followingCount", "inboxUrl", "outboxUrl",
80 "sharedInboxUrl", "followersUrl", "followingUrl", "avatarId", "serverId", "createdAt", "updatedAt" 80 "sharedInboxUrl", "followersUrl", "followingUrl", "avatarId", "serverId", "createdAt", "updatedAt"
81 ) 81 )
82 SELECT 82 SELECT
83 'Person', uuid, name, url, "publicKey", "privateKey", "followersCount", "followingCount", "inboxUrl", "outboxUrl", 83 'Person', uuid, name, url, "publicKey", "privateKey", "followersCount", "followingCount", "inboxUrl", "outboxUrl",
84 "sharedInboxUrl", "followersUrl", "followingUrl", "avatarId", "serverId", "createdAt", "updatedAt" 84 "sharedInboxUrl", "followersUrl", "followingUrl", "avatarId", "serverId", "createdAt", "updatedAt"
85 FROM account 85 FROM account
86 WHERE "applicationId" IS NULL 86 WHERE "applicationId" IS NULL
87 ` 87 `
88 await utils.sequelize.query(query2) 88 await utils.sequelize.query(query2)
@@ -108,17 +108,17 @@ async function up (utils: {
108 } 108 }
109 109
110 { 110 {
111 const query = ` 111 const query = `
112 INSERT INTO actor 112 INSERT INTO actor
113 ( 113 (
114 type, uuid, "preferredUsername", url, "publicKey", "privateKey", "followersCount", "followingCount", "inboxUrl", "outboxUrl", 114 type, uuid, "preferredUsername", url, "publicKey", "privateKey", "followersCount", "followingCount", "inboxUrl", "outboxUrl",
115 "sharedInboxUrl", "followersUrl", "followingUrl", "avatarId", "serverId", "createdAt", "updatedAt" 115 "sharedInboxUrl", "followersUrl", "followingUrl", "avatarId", "serverId", "createdAt", "updatedAt"
116 ) 116 )
117 SELECT 117 SELECT
118 'Group', "videoChannel".uuid, "videoChannel".uuid, "videoChannel".url, null, null, 0, 0, "videoChannel".url || '/inbox', 118 'Group', "videoChannel".uuid, "videoChannel".uuid, "videoChannel".url, null, null, 0, 0, "videoChannel".url || '/inbox',
119 "videoChannel".url || '/outbox', "videoChannel".url || '/inbox', "videoChannel".url || '/followers', "videoChannel".url || '/following', 119 "videoChannel".url || '/outbox', "videoChannel".url || '/inbox', "videoChannel".url || '/followers', "videoChannel".url || '/following',
120 null, account."serverId", "videoChannel"."createdAt", "videoChannel"."updatedAt" 120 null, account."serverId", "videoChannel"."createdAt", "videoChannel"."updatedAt"
121 FROM "videoChannel" 121 FROM "videoChannel"
122 INNER JOIN "account" on "videoChannel"."accountId" = "account".id 122 INNER JOIN "account" on "videoChannel"."accountId" = "account".id
123 ` 123 `
124 await utils.sequelize.query(query) 124 await utils.sequelize.query(query)
@@ -157,13 +157,13 @@ async function up (utils: {
157 } 157 }
158 158
159 { 159 {
160 const query1 = `UPDATE "actorFollow" 160 const query1 = `UPDATE "actorFollow"
161 SET "actorId" = 161 SET "actorId" =
162 (SELECT "account"."actorId" FROM account WHERE "account"."id" = "actorFollow"."actorId")` 162 (SELECT "account"."actorId" FROM account WHERE "account"."id" = "actorFollow"."actorId")`
163 await utils.sequelize.query(query1) 163 await utils.sequelize.query(query1)
164 164
165 const query2 = `UPDATE "actorFollow" 165 const query2 = `UPDATE "actorFollow"
166 SET "targetActorId" = 166 SET "targetActorId" =
167 (SELECT "account"."actorId" FROM account WHERE "account"."id" = "actorFollow"."targetActorId")` 167 (SELECT "account"."actorId" FROM account WHERE "account"."id" = "actorFollow"."targetActorId")`
168 168
169 await utils.sequelize.query(query2) 169 await utils.sequelize.query(query2)
@@ -189,8 +189,8 @@ async function up (utils: {
189 await utils.queryInterface.removeConstraint('videoShare', 'videoShare_accountId_fkey') 189 await utils.queryInterface.removeConstraint('videoShare', 'videoShare_accountId_fkey')
190 } 190 }
191 191
192 const query = `UPDATE "videoShare" 192 const query = `UPDATE "videoShare"
193 SET "actorId" = 193 SET "actorId" =
194 (SELECT "actorId" FROM account WHERE id = "videoShare"."actorId")` 194 (SELECT "actorId" FROM account WHERE id = "videoShare"."actorId")`
195 await utils.sequelize.query(query) 195 await utils.sequelize.query(query)
196 196
@@ -240,7 +240,7 @@ async function up (utils: {
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 options = { type: Sequelize.QueryTypes.SELECT as Sequelize.QueryTypes.SELECT } 242 const options = { type: Sequelize.QueryTypes.SELECT as Sequelize.QueryTypes.SELECT }
243 const [ res ] = await utils.sequelize.query(query, options) 243 const [ res ] = await utils.sequelize.query<any>(query, options)
244 244
245 for (const actor of res) { 245 for (const actor of res) {
246 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 020499391..d790988ad 100644
--- a/server/initializers/migrations/0140-actor-url.ts
+++ b/server/initializers/migrations/0140-actor-url.ts
@@ -2,8 +2,8 @@ import * as Sequelize from 'sequelize'
2import { WEBSERVER } 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 = WEBSERVER.HOSTNAME + ':443' 9 const toReplace = WEBSERVER.HOSTNAME + ':443'
diff --git a/server/initializers/migrations/0145-delete-author.ts b/server/initializers/migrations/0145-delete-author.ts
index cb23d1cc2..6c9427997 100644
--- a/server/initializers/migrations/0145-delete-author.ts
+++ b/server/initializers/migrations/0145-delete-author.ts
@@ -1,8 +1,8 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize 6 sequelize: Sequelize.Sequelize
7}): Promise<void> { 7}): Promise<void> {
8 await utils.queryInterface.dropTable('Authors') 8 await utils.queryInterface.dropTable('Authors')
diff --git a/server/initializers/migrations/0150-avatar-cascade.ts b/server/initializers/migrations/0150-avatar-cascade.ts
index 821696717..fb3b25773 100644
--- a/server/initializers/migrations/0150-avatar-cascade.ts
+++ b/server/initializers/migrations/0150-avatar-cascade.ts
@@ -1,8 +1,8 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize 6 sequelize: Sequelize.Sequelize
7}): Promise<void> { 7}): Promise<void> {
8 await utils.queryInterface.removeConstraint('actor', 'actor_avatarId_fkey') 8 await utils.queryInterface.removeConstraint('actor', 'actor_avatarId_fkey')
diff --git a/server/initializers/migrations/0155-video-comments-enabled.ts b/server/initializers/migrations/0155-video-comments-enabled.ts
index 6949d3a0c..691640b35 100644
--- a/server/initializers/migrations/0155-video-comments-enabled.ts
+++ b/server/initializers/migrations/0155-video-comments-enabled.ts
@@ -2,8 +2,8 @@ import * as Sequelize from 'sequelize'
2import { Migration } from '../../models/migrations' 2import { Migration } from '../../models/migrations'
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 data = { 9 const data = {
diff --git a/server/initializers/migrations/0160-account-route.ts b/server/initializers/migrations/0160-account-route.ts
index cab4c72f1..97469948b 100644
--- a/server/initializers/migrations/0160-account-route.ts
+++ b/server/initializers/migrations/0160-account-route.ts
@@ -1,8 +1,8 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize 6 sequelize: Sequelize.Sequelize
7}): Promise<void> { 7}): Promise<void> {
8 { 8 {
diff --git a/server/initializers/migrations/0165-video-route.ts b/server/initializers/migrations/0165-video-route.ts
index 56d98bc69..aa7c75128 100644
--- a/server/initializers/migrations/0165-video-route.ts
+++ b/server/initializers/migrations/0165-video-route.ts
@@ -1,8 +1,8 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize 6 sequelize: Sequelize.Sequelize
7}): Promise<void> { 7}): Promise<void> {
8 { 8 {
diff --git a/server/initializers/migrations/0170-actor-follow-score.ts b/server/initializers/migrations/0170-actor-follow-score.ts
index a12b35da9..901a3c799 100644
--- a/server/initializers/migrations/0170-actor-follow-score.ts
+++ b/server/initializers/migrations/0170-actor-follow-score.ts
@@ -2,8 +2,8 @@ import * as Sequelize from 'sequelize'
2import { ACTOR_FOLLOW_SCORE } from '../constants' 2import { ACTOR_FOLLOW_SCORE } 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 await utils.queryInterface.removeColumn('server', 'score') 9 await utils.queryInterface.removeColumn('server', 'score')
diff --git a/server/initializers/migrations/0175-actor-follow-counts.ts b/server/initializers/migrations/0175-actor-follow-counts.ts
index 4fb524181..d7853f8dc 100644
--- a/server/initializers/migrations/0175-actor-follow-counts.ts
+++ b/server/initializers/migrations/0175-actor-follow-counts.ts
@@ -1,8 +1,8 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize 6 sequelize: Sequelize.Sequelize
7}): Promise<void> { 7}): Promise<void> {
8 const query = 'UPDATE "actor" SET ' + 8 const query = 'UPDATE "actor" SET ' +
diff --git a/server/initializers/migrations/0180-job-table-delete.ts b/server/initializers/migrations/0180-job-table-delete.ts
index df29145d0..fb48a0c9d 100644
--- a/server/initializers/migrations/0180-job-table-delete.ts
+++ b/server/initializers/migrations/0180-job-table-delete.ts
@@ -1,8 +1,8 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize 6 sequelize: Sequelize.Sequelize
7}): Promise<void> { 7}): Promise<void> {
8 await utils.queryInterface.dropTable('job') 8 await utils.queryInterface.dropTable('job')
diff --git a/server/initializers/migrations/0185-video-share-url.ts b/server/initializers/migrations/0185-video-share-url.ts
index f7eeb0878..f59931e0f 100644
--- a/server/initializers/migrations/0185-video-share-url.ts
+++ b/server/initializers/migrations/0185-video-share-url.ts
@@ -1,8 +1,8 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize 6 sequelize: Sequelize.Sequelize
7}): Promise<void> { 7}): Promise<void> {
8 { 8 {
diff --git a/server/initializers/migrations/0190-video-comment-unique-url.ts b/server/initializers/migrations/0190-video-comment-unique-url.ts
index b196c9352..a8769ed41 100644
--- a/server/initializers/migrations/0190-video-comment-unique-url.ts
+++ b/server/initializers/migrations/0190-video-comment-unique-url.ts
@@ -1,8 +1,8 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize 6 sequelize: Sequelize.Sequelize
7}): Promise<void> { 7}): Promise<void> {
8 { 8 {
diff --git a/server/initializers/migrations/0195-support.ts b/server/initializers/migrations/0195-support.ts
index 3b9eabe79..3f7c75dce 100644
--- a/server/initializers/migrations/0195-support.ts
+++ b/server/initializers/migrations/0195-support.ts
@@ -1,8 +1,8 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize 6 sequelize: Sequelize.Sequelize
7}): Promise<void> { 7}): Promise<void> {
8 { 8 {
diff --git a/server/initializers/migrations/0200-video-published-at.ts b/server/initializers/migrations/0200-video-published-at.ts
index 1701ea07a..d8c7b42a7 100644
--- a/server/initializers/migrations/0200-video-published-at.ts
+++ b/server/initializers/migrations/0200-video-published-at.ts
@@ -1,8 +1,8 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize 6 sequelize: Sequelize.Sequelize
7}): Promise<void> { 7}): Promise<void> {
8 8
diff --git a/server/initializers/migrations/0205-user-nsfw-policy.ts b/server/initializers/migrations/0205-user-nsfw-policy.ts
index d0f6e8962..9c2786f12 100644
--- a/server/initializers/migrations/0205-user-nsfw-policy.ts
+++ b/server/initializers/migrations/0205-user-nsfw-policy.ts
@@ -1,8 +1,8 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize 6 sequelize: Sequelize.Sequelize
7}): Promise<void> { 7}): Promise<void> {
8 8
diff --git a/server/initializers/migrations/0210-video-language.ts b/server/initializers/migrations/0210-video-language.ts
index ca95c7527..ee4ce9266 100644
--- a/server/initializers/migrations/0210-video-language.ts
+++ b/server/initializers/migrations/0210-video-language.ts
@@ -2,8 +2,8 @@ import * as Sequelize from 'sequelize'
2import { CONSTRAINTS_FIELDS } from '../constants' 2import { CONSTRAINTS_FIELDS } 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 9
diff --git a/server/initializers/migrations/0215-video-support-length.ts b/server/initializers/migrations/0215-video-support-length.ts
index ba395050f..26c0ca700 100644
--- a/server/initializers/migrations/0215-video-support-length.ts
+++ b/server/initializers/migrations/0215-video-support-length.ts
@@ -1,8 +1,8 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize 6 sequelize: Sequelize.Sequelize
7}): Promise<void> { 7}): Promise<void> {
8 { 8 {
diff --git a/server/initializers/migrations/0255-video-blacklist-reason.ts b/server/initializers/migrations/0255-video-blacklist-reason.ts
index 69d6efb9e..7de982f93 100644
--- a/server/initializers/migrations/0255-video-blacklist-reason.ts
+++ b/server/initializers/migrations/0255-video-blacklist-reason.ts
@@ -1,5 +1,4 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import { VideoAbuseState } from '../../../shared/models/videos'
3 2
4async function up (utils: { 3async function up (utils: {
5 transaction: Sequelize.Transaction 4 transaction: Sequelize.Transaction
diff --git a/server/initializers/migrations/0285-description-support.ts b/server/initializers/migrations/0285-description-support.ts
index 85ef4ef39..aab3a938f 100644
--- a/server/initializers/migrations/0285-description-support.ts
+++ b/server/initializers/migrations/0285-description-support.ts
@@ -1,9 +1,9 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize, 6 sequelize: Sequelize.Sequelize
7 db: any 7 db: any
8}): Promise<void> { 8}): Promise<void> {
9 { 9 {
diff --git a/server/initializers/migrations/0290-account-video-rate-url.ts b/server/initializers/migrations/0290-account-video-rate-url.ts
index bdabf2929..b974b1a81 100644
--- a/server/initializers/migrations/0290-account-video-rate-url.ts
+++ b/server/initializers/migrations/0290-account-video-rate-url.ts
@@ -1,9 +1,9 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize, 6 sequelize: Sequelize.Sequelize
7 db: any 7 db: any
8}): Promise<void> { 8}): Promise<void> {
9 { 9 {
diff --git a/server/initializers/migrations/0295-video-file-extname.ts b/server/initializers/migrations/0295-video-file-extname.ts
index dbf249f66..e1999b072 100644
--- a/server/initializers/migrations/0295-video-file-extname.ts
+++ b/server/initializers/migrations/0295-video-file-extname.ts
@@ -1,9 +1,9 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize, 6 sequelize: Sequelize.Sequelize
7 db: any 7 db: any
8}): Promise<void> { 8}): Promise<void> {
9 { 9 {
diff --git a/server/initializers/migrations/0300-user-videos-history-enabled.ts b/server/initializers/migrations/0300-user-videos-history-enabled.ts
index aa5fc21fb..5e35e14ba 100644
--- a/server/initializers/migrations/0300-user-videos-history-enabled.ts
+++ b/server/initializers/migrations/0300-user-videos-history-enabled.ts
@@ -1,9 +1,9 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize, 6 sequelize: Sequelize.Sequelize
7 db: any 7 db: any
8}): Promise<void> { 8}): Promise<void> {
9 { 9 {
diff --git a/server/initializers/migrations/0305-fix-unfederated-videos.ts b/server/initializers/migrations/0305-fix-unfederated-videos.ts
index be206601f..9c5d56b7b 100644
--- a/server/initializers/migrations/0305-fix-unfederated-videos.ts
+++ b/server/initializers/migrations/0305-fix-unfederated-videos.ts
@@ -1,9 +1,9 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize, 6 sequelize: Sequelize.Sequelize
7 db: any 7 db: any
8}): Promise<void> { 8}): Promise<void> {
9 { 9 {
diff --git a/server/initializers/migrations/0310-drop-unused-video-indexes.ts b/server/initializers/migrations/0310-drop-unused-video-indexes.ts
index d51f430c0..181858d3d 100644
--- a/server/initializers/migrations/0310-drop-unused-video-indexes.ts
+++ b/server/initializers/migrations/0310-drop-unused-video-indexes.ts
@@ -1,9 +1,9 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize, 6 sequelize: Sequelize.Sequelize
7 db: any 7 db: any
8}): Promise<void> { 8}): Promise<void> {
9 const indexNames = [ 9 const indexNames = [
diff --git a/server/initializers/migrations/0315-user-notifications.ts b/server/initializers/migrations/0315-user-notifications.ts
index 8284c58a0..0e3f4fbef 100644
--- a/server/initializers/migrations/0315-user-notifications.ts
+++ b/server/initializers/migrations/0315-user-notifications.ts
@@ -1,8 +1,8 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize 6 sequelize: Sequelize.Sequelize
7}): Promise<void> { 7}): Promise<void> {
8 8
diff --git a/server/initializers/migrations/0320-blacklist-unfederate.ts b/server/initializers/migrations/0320-blacklist-unfederate.ts
index 6fb7bbb90..41de41c55 100644
--- a/server/initializers/migrations/0320-blacklist-unfederate.ts
+++ b/server/initializers/migrations/0320-blacklist-unfederate.ts
@@ -1,8 +1,8 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize 6 sequelize: Sequelize.Sequelize
7}): Promise<void> { 7}): Promise<void> {
8 8
diff --git a/server/initializers/migrations/0325-video-abuse-fields.ts b/server/initializers/migrations/0325-video-abuse-fields.ts
index fca6d666f..d88724a20 100644
--- a/server/initializers/migrations/0325-video-abuse-fields.ts
+++ b/server/initializers/migrations/0325-video-abuse-fields.ts
@@ -1,8 +1,8 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize 6 sequelize: Sequelize.Sequelize
7}): Promise<void> { 7}): Promise<void> {
8 8
diff --git a/server/initializers/migrations/0330-video-streaming-playlist.ts b/server/initializers/migrations/0330-video-streaming-playlist.ts
index c85a762ab..f75541a7f 100644
--- a/server/initializers/migrations/0330-video-streaming-playlist.ts
+++ b/server/initializers/migrations/0330-video-streaming-playlist.ts
@@ -1,8 +1,8 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize 6 sequelize: Sequelize.Sequelize
7}): Promise<void> { 7}): Promise<void> {
8 8
diff --git a/server/initializers/migrations/0335-video-downloading-enabled.ts b/server/initializers/migrations/0335-video-downloading-enabled.ts
index e79466447..c745f1f02 100644
--- a/server/initializers/migrations/0335-video-downloading-enabled.ts
+++ b/server/initializers/migrations/0335-video-downloading-enabled.ts
@@ -2,8 +2,8 @@ import * as Sequelize from 'sequelize'
2import { Migration } from '../../models/migrations' 2import { Migration } from '../../models/migrations'
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 data = { 9 const data = {
diff --git a/server/initializers/migrations/0340-add-originally-published-at.ts b/server/initializers/migrations/0340-add-originally-published-at.ts
index fe4f4a5f9..7cbc14ab5 100644
--- a/server/initializers/migrations/0340-add-originally-published-at.ts
+++ b/server/initializers/migrations/0340-add-originally-published-at.ts
@@ -1,8 +1,8 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize 6 sequelize: Sequelize.Sequelize
7}): Promise<void> { 7}): Promise<void> {
8 8
diff --git a/server/initializers/migrations/0345-video-playlists.ts b/server/initializers/migrations/0345-video-playlists.ts
index de69f5b9e..89a14a6ee 100644
--- a/server/initializers/migrations/0345-video-playlists.ts
+++ b/server/initializers/migrations/0345-video-playlists.ts
@@ -1,11 +1,11 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import { VideoPlaylistPrivacy, VideoPlaylistType } from '../../../shared/models/videos' 2import { VideoPlaylistPrivacy, VideoPlaylistType } from '../../../shared/models/videos'
3import * as uuidv4 from 'uuid/v4' 3import { v4 as uuidv4 } from 'uuid'
4import { WEBSERVER } from '../constants' 4import { WEBSERVER } from '../constants'
5 5
6async function up (utils: { 6async function up (utils: {
7 transaction: Sequelize.Transaction, 7 transaction: Sequelize.Transaction
8 queryInterface: Sequelize.QueryInterface, 8 queryInterface: Sequelize.QueryInterface
9 sequelize: Sequelize.Sequelize 9 sequelize: Sequelize.Sequelize
10}): Promise<void> { 10}): Promise<void> {
11 const transaction = utils.transaction 11 const transaction = utils.transaction
diff --git a/server/initializers/migrations/0350-video-blacklist-type.ts b/server/initializers/migrations/0350-video-blacklist-type.ts
index 4849020ef..f79ae5ec7 100644
--- a/server/initializers/migrations/0350-video-blacklist-type.ts
+++ b/server/initializers/migrations/0350-video-blacklist-type.ts
@@ -2,9 +2,9 @@ import * as Sequelize from 'sequelize'
2import { VideoBlacklistType } from '../../../shared/models/videos' 2import { VideoBlacklistType } from '../../../shared/models/videos'
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 db: any 8 db: any
9}): Promise<void> { 9}): Promise<void> {
10 { 10 {
diff --git a/server/initializers/migrations/0355-p2p-peer-version.ts b/server/initializers/migrations/0355-p2p-peer-version.ts
index 18f23d9b7..89af28d07 100644
--- a/server/initializers/migrations/0355-p2p-peer-version.ts
+++ b/server/initializers/migrations/0355-p2p-peer-version.ts
@@ -1,9 +1,9 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize, 6 sequelize: Sequelize.Sequelize
7 db: any 7 db: any
8}): Promise<void> { 8}): Promise<void> {
9 9
diff --git a/server/initializers/migrations/0360-notification-instance-follower.ts b/server/initializers/migrations/0360-notification-instance-follower.ts
index 05caf8e1d..6f9a01a9c 100644
--- a/server/initializers/migrations/0360-notification-instance-follower.ts
+++ b/server/initializers/migrations/0360-notification-instance-follower.ts
@@ -1,9 +1,9 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize, 6 sequelize: Sequelize.Sequelize
7 db: any 7 db: any
8}): Promise<void> { 8}): Promise<void> {
9 { 9 {
diff --git a/server/initializers/migrations/0365-user-admin-flags.ts b/server/initializers/migrations/0365-user-admin-flags.ts
index 20553100a..b705387da 100644
--- a/server/initializers/migrations/0365-user-admin-flags.ts
+++ b/server/initializers/migrations/0365-user-admin-flags.ts
@@ -1,9 +1,9 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize, 6 sequelize: Sequelize.Sequelize
7 db: any 7 db: any
8}): Promise<void> { 8}): Promise<void> {
9 { 9 {
diff --git a/server/initializers/migrations/0370-thumbnail.ts b/server/initializers/migrations/0370-thumbnail.ts
index 384ca1a15..07c25436a 100644
--- a/server/initializers/migrations/0370-thumbnail.ts
+++ b/server/initializers/migrations/0370-thumbnail.ts
@@ -1,9 +1,9 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize, 6 sequelize: Sequelize.Sequelize
7 db: any 7 db: any
8}): Promise<void> { 8}): Promise<void> {
9 { 9 {
diff --git a/server/initializers/migrations/0375-account-description.ts b/server/initializers/migrations/0375-account-description.ts
index 1258563fd..f9af942e0 100644
--- a/server/initializers/migrations/0375-account-description.ts
+++ b/server/initializers/migrations/0375-account-description.ts
@@ -1,9 +1,9 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize, 6 sequelize: Sequelize.Sequelize
7 db: any 7 db: any
8}): Promise<void> { 8}): Promise<void> {
9 const data = { 9 const data = {
diff --git a/server/initializers/migrations/0380-cleanup-timestamps.ts b/server/initializers/migrations/0380-cleanup-timestamps.ts
index 2a9fd6f02..18ecfb997 100644
--- a/server/initializers/migrations/0380-cleanup-timestamps.ts
+++ b/server/initializers/migrations/0380-cleanup-timestamps.ts
@@ -1,9 +1,9 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize, 6 sequelize: Sequelize.Sequelize
7 db: any 7 db: any
8}): Promise<void> { 8}): Promise<void> {
9 try { 9 try {
diff --git a/server/initializers/migrations/0385-remove-actor-uuid.ts b/server/initializers/migrations/0385-remove-actor-uuid.ts
index 032c0562b..eefbc386b 100644
--- a/server/initializers/migrations/0385-remove-actor-uuid.ts
+++ b/server/initializers/migrations/0385-remove-actor-uuid.ts
@@ -1,9 +1,9 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize, 6 sequelize: Sequelize.Sequelize
7 db: any 7 db: any
8}): Promise<void> { 8}): Promise<void> {
9 await utils.queryInterface.removeColumn('actor', 'uuid') 9 await utils.queryInterface.removeColumn('actor', 'uuid')
diff --git a/server/initializers/migrations/0390-user-pending-email.ts b/server/initializers/migrations/0390-user-pending-email.ts
index 5ca871746..9cf0affa5 100644
--- a/server/initializers/migrations/0390-user-pending-email.ts
+++ b/server/initializers/migrations/0390-user-pending-email.ts
@@ -1,9 +1,9 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize, 6 sequelize: Sequelize.Sequelize
7 db: any 7 db: any
8}): Promise<void> { 8}): Promise<void> {
9 const data = { 9 const data = {
diff --git a/server/initializers/migrations/0395-user-video-languages.ts b/server/initializers/migrations/0395-user-video-languages.ts
index 278698bf4..9c24fbc9a 100644
--- a/server/initializers/migrations/0395-user-video-languages.ts
+++ b/server/initializers/migrations/0395-user-video-languages.ts
@@ -1,9 +1,9 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize, 6 sequelize: Sequelize.Sequelize
7 db: any 7 db: any
8}): Promise<void> { 8}): Promise<void> {
9 const data = { 9 const data = {
diff --git a/server/initializers/migrations/0400-user-theme.ts b/server/initializers/migrations/0400-user-theme.ts
index f74d76115..7addb1bb3 100644
--- a/server/initializers/migrations/0400-user-theme.ts
+++ b/server/initializers/migrations/0400-user-theme.ts
@@ -1,9 +1,9 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize, 6 sequelize: Sequelize.Sequelize
7 db: any 7 db: any
8}): Promise<void> { 8}): Promise<void> {
9 const data = { 9 const data = {
diff --git a/server/initializers/migrations/0405-plugin.ts b/server/initializers/migrations/0405-plugin.ts
index c55b81960..5c290b986 100644
--- a/server/initializers/migrations/0405-plugin.ts
+++ b/server/initializers/migrations/0405-plugin.ts
@@ -1,9 +1,9 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize, 6 sequelize: Sequelize.Sequelize
7 db: any 7 db: any
8}): Promise<void> { 8}): Promise<void> {
9 { 9 {
diff --git a/server/initializers/migrations/0410-video-playlist-element.ts b/server/initializers/migrations/0410-video-playlist-element.ts
index f536632a2..1b4692357 100644
--- a/server/initializers/migrations/0410-video-playlist-element.ts
+++ b/server/initializers/migrations/0410-video-playlist-element.ts
@@ -1,9 +1,9 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize, 6 sequelize: Sequelize.Sequelize
7 db: any 7 db: any
8}): Promise<void> { 8}): Promise<void> {
9 { 9 {
diff --git a/server/initializers/migrations/0415-thumbnail-auto-generated.ts b/server/initializers/migrations/0415-thumbnail-auto-generated.ts
index f822a4c05..98d563c88 100644
--- a/server/initializers/migrations/0415-thumbnail-auto-generated.ts
+++ b/server/initializers/migrations/0415-thumbnail-auto-generated.ts
@@ -1,9 +1,9 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize, 6 sequelize: Sequelize.Sequelize
7 db: any 7 db: any
8}): Promise<void> { 8}): Promise<void> {
9 { 9 {
diff --git a/server/initializers/migrations/0420-avatar-lazy.ts b/server/initializers/migrations/0420-avatar-lazy.ts
index 5fc57aac2..5c74819d2 100644
--- a/server/initializers/migrations/0420-avatar-lazy.ts
+++ b/server/initializers/migrations/0420-avatar-lazy.ts
@@ -1,9 +1,9 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize, 6 sequelize: Sequelize.Sequelize
7 db: any 7 db: any
8}): Promise<void> { 8}): Promise<void> {
9 { 9 {
diff --git a/server/initializers/migrations/0425-nullable-actor-fields.ts b/server/initializers/migrations/0425-nullable-actor-fields.ts
index 4e5f9e6ab..720b99ccc 100644
--- a/server/initializers/migrations/0425-nullable-actor-fields.ts
+++ b/server/initializers/migrations/0425-nullable-actor-fields.ts
@@ -1,9 +1,9 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize, 6 sequelize: Sequelize.Sequelize
7 db: any 7 db: any
8}): Promise<void> { 8}): Promise<void> {
9 const data = { 9 const data = {
diff --git a/server/initializers/migrations/0430-auto-follow-notification-setting.ts b/server/initializers/migrations/0430-auto-follow-notification-setting.ts
index 034bdd46d..1734828a4 100644
--- a/server/initializers/migrations/0430-auto-follow-notification-setting.ts
+++ b/server/initializers/migrations/0430-auto-follow-notification-setting.ts
@@ -1,9 +1,9 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize, 6 sequelize: Sequelize.Sequelize
7 db: any 7 db: any
8}): Promise<void> { 8}): Promise<void> {
9 { 9 {
diff --git a/server/initializers/migrations/0435-user-modals.ts b/server/initializers/migrations/0435-user-modals.ts
index 5c2aa85b5..737440e9b 100644
--- a/server/initializers/migrations/0435-user-modals.ts
+++ b/server/initializers/migrations/0435-user-modals.ts
@@ -1,9 +1,9 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize, 6 sequelize: Sequelize.Sequelize
7 db: any 7 db: any
8}): Promise<void> { 8}): Promise<void> {
9 { 9 {
diff --git a/server/initializers/migrations/0440-user-auto-play-next-video.ts b/server/initializers/migrations/0440-user-auto-play-next-video.ts
index f0baafe7b..f3f663f59 100644
--- a/server/initializers/migrations/0440-user-auto-play-next-video.ts
+++ b/server/initializers/migrations/0440-user-auto-play-next-video.ts
@@ -1,9 +1,9 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize, 6 sequelize: Sequelize.Sequelize
7 db: any 7 db: any
8}): Promise<void> { 8}): Promise<void> {
9 { 9 {
diff --git a/server/initializers/migrations/0445-shared-inbox-optional.ts b/server/initializers/migrations/0445-shared-inbox-optional.ts
index dad2d6569..ade1a2a57 100644
--- a/server/initializers/migrations/0445-shared-inbox-optional.ts
+++ b/server/initializers/migrations/0445-shared-inbox-optional.ts
@@ -1,9 +1,9 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize, 6 sequelize: Sequelize.Sequelize
7 db: any 7 db: any
8}): Promise<void> { 8}): Promise<void> {
9 { 9 {
diff --git a/server/initializers/migrations/0450-streaming-playlist-files.ts b/server/initializers/migrations/0450-streaming-playlist-files.ts
index 460dac8be..08e2e3989 100644
--- a/server/initializers/migrations/0450-streaming-playlist-files.ts
+++ b/server/initializers/migrations/0450-streaming-playlist-files.ts
@@ -1,15 +1,9 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import { join } from 'path'
3import { HLS_STREAMING_PLAYLIST_DIRECTORY, WEBSERVER } from '@server/initializers/constants'
4import { CONFIG } from '@server/initializers/config'
5import { pathExists, stat, writeFile } from 'fs-extra'
6import * as parseTorrent from 'parse-torrent'
7import { createTorrentPromise } from '@server/helpers/webtorrent'
8 2
9async function up (utils: { 3async function up (utils: {
10 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
11 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
12 sequelize: Sequelize.Sequelize, 6 sequelize: Sequelize.Sequelize
13 db: any 7 db: any
14}): Promise<void> { 8}): Promise<void> {
15 { 9 {
@@ -42,8 +36,8 @@ async function up (utils: {
42 { 36 {
43 const query = 'insert into "videoFile" ' + 37 const query = 'insert into "videoFile" ' +
44 '(resolution, size, "infoHash", "videoId", "createdAt", "updatedAt", fps, extname, "videoStreamingPlaylistId")' + 38 '(resolution, size, "infoHash", "videoId", "createdAt", "updatedAt", fps, extname, "videoStreamingPlaylistId")' +
45 '(SELECT "videoFile".resolution, "videoFile".size, \'fake\', NULL, "videoFile"."createdAt", "videoFile"."updatedAt", "videoFile"."fps", ' + 39 '(SELECT "videoFile".resolution, "videoFile".size, \'fake\', NULL, "videoFile"."createdAt", "videoFile"."updatedAt", ' +
46 '"videoFile".extname, "videoStreamingPlaylist".id FROM "videoStreamingPlaylist" ' + 40 '"videoFile"."fps", "videoFile".extname, "videoStreamingPlaylist".id FROM "videoStreamingPlaylist" ' +
47 'inner join video ON video.id = "videoStreamingPlaylist"."videoId" inner join "videoFile" ON "videoFile"."videoId" = video.id)' 41 'inner join video ON video.id = "videoStreamingPlaylist"."videoId" inner join "videoFile" ON "videoFile"."videoId" = video.id)'
48 42
49 await utils.sequelize.query(query, { transaction: utils.transaction }) 43 await utils.sequelize.query(query, { transaction: utils.transaction })
diff --git a/server/initializers/migrations/0455-soft-delete-video-comments.ts b/server/initializers/migrations/0455-soft-delete-video-comments.ts
index bcfb97b56..00e56015f 100644
--- a/server/initializers/migrations/0455-soft-delete-video-comments.ts
+++ b/server/initializers/migrations/0455-soft-delete-video-comments.ts
@@ -1,9 +1,9 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize, 6 sequelize: Sequelize.Sequelize
7 db: any 7 db: any
8}): Promise<void> { 8}): Promise<void> {
9 { 9 {
diff --git a/server/initializers/migrations/0460-user-playlist-autoplay.ts b/server/initializers/migrations/0460-user-playlist-autoplay.ts
index 3067ac1a4..d6f5081ab 100644
--- a/server/initializers/migrations/0460-user-playlist-autoplay.ts
+++ b/server/initializers/migrations/0460-user-playlist-autoplay.ts
@@ -1,9 +1,9 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize, 6 sequelize: Sequelize.Sequelize
7 db: any 7 db: any
8}): Promise<void> { 8}): Promise<void> {
9 { 9 {
diff --git a/server/initializers/migrations/0465-thumbnail-file-url-length.ts b/server/initializers/migrations/0465-thumbnail-file-url-length.ts
index db8c85c29..84a4fa0ba 100644
--- a/server/initializers/migrations/0465-thumbnail-file-url-length.ts
+++ b/server/initializers/migrations/0465-thumbnail-file-url-length.ts
@@ -1,9 +1,9 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize, 6 sequelize: Sequelize.Sequelize
7 db: any 7 db: any
8}): Promise<void> { 8}): Promise<void> {
9 { 9 {
diff --git a/server/initializers/migrations/0470-cleaup-indexes.ts b/server/initializers/migrations/0470-cleaup-indexes.ts
index 53e401c2b..7365c30f8 100644
--- a/server/initializers/migrations/0470-cleaup-indexes.ts
+++ b/server/initializers/migrations/0470-cleaup-indexes.ts
@@ -1,9 +1,9 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2 2
3async function up (utils: { 3async function up (utils: {
4 transaction: Sequelize.Transaction, 4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface, 5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize, 6 sequelize: Sequelize.Sequelize
7 db: any 7 db: any
8}): Promise<void> { 8}): Promise<void> {
9 await utils.sequelize.query('DROP INDEX IF EXISTS video_share_account_id;') 9 await utils.sequelize.query('DROP INDEX IF EXISTS video_share_account_id;')
diff --git a/server/initializers/migrations/0475-redundancy-expires-on.ts b/server/initializers/migrations/0475-redundancy-expires-on.ts
new file mode 100644
index 000000000..edbddba37
--- /dev/null
+++ b/server/initializers/migrations/0475-redundancy-expires-on.ts
@@ -0,0 +1,27 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 {
10 const data = {
11 type: Sequelize.DATE,
12 allowNull: true,
13 defaultValue: null
14 }
15
16 await utils.queryInterface.changeColumn('videoRedundancy', 'expiresOn', data)
17 }
18}
19
20function down (options) {
21 throw new Error('Not implemented.')
22}
23
24export {
25 up,
26 down
27}
diff --git a/server/initializers/migrations/0480-caption-file-url.ts b/server/initializers/migrations/0480-caption-file-url.ts
new file mode 100644
index 000000000..1f88206d3
--- /dev/null
+++ b/server/initializers/migrations/0480-caption-file-url.ts
@@ -0,0 +1,27 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 {
10 const data = {
11 type: Sequelize.STRING,
12 allowNull: true,
13 defaultValue: null
14 }
15
16 await utils.queryInterface.addColumn('videoCaption', 'fileUrl', data)
17 }
18}
19
20function down (options) {
21 throw new Error('Not implemented.')
22}
23
24export {
25 up,
26 down
27}
diff --git a/server/initializers/migrations/0490-abuse-video.ts b/server/initializers/migrations/0490-abuse-video.ts
new file mode 100644
index 000000000..610307aa4
--- /dev/null
+++ b/server/initializers/migrations/0490-abuse-video.ts
@@ -0,0 +1,26 @@
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 deletedVideo = {
10 type: Sequelize.JSONB,
11 allowNull: true
12 }
13 await utils.queryInterface.addColumn('videoAbuse', 'deletedVideo', deletedVideo)
14 await utils.sequelize.query(`ALTER TABLE "videoAbuse" ALTER COLUMN "videoId" DROP NOT NULL;`)
15 await utils.sequelize.query(`ALTER TABLE "videoAbuse" DROP CONSTRAINT IF EXISTS "videoAbuse_videoId_fkey";`)
16
17}
18
19function down (options) {
20 throw new Error('Not implemented.')
21}
22
23export {
24 up,
25 down
26}
diff --git a/server/initializers/migrations/0495-plugin-auth.ts b/server/initializers/migrations/0495-plugin-auth.ts
new file mode 100644
index 000000000..ea636a4ad
--- /dev/null
+++ b/server/initializers/migrations/0495-plugin-auth.ts
@@ -0,0 +1,42 @@
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 password = {
11 type: Sequelize.STRING,
12 allowNull: true
13 }
14 await utils.queryInterface.changeColumn('user', 'password', password)
15 }
16
17 {
18 const pluginAuth = {
19 type: Sequelize.STRING,
20 allowNull: true
21 }
22 await utils.queryInterface.addColumn('user', 'pluginAuth', pluginAuth)
23 }
24
25 {
26 const authName = {
27 type: Sequelize.STRING,
28 allowNull: true
29 }
30 await utils.queryInterface.addColumn('oAuthToken', 'authName', authName)
31 }
32
33}
34
35function down (options) {
36 throw new Error('Not implemented.')
37}
38
39export {
40 up,
41 down
42}
diff --git a/server/initializers/migrations/0500-playlist-description-length.ts b/server/initializers/migrations/0500-playlist-description-length.ts
new file mode 100644
index 000000000..f47f3d96a
--- /dev/null
+++ b/server/initializers/migrations/0500-playlist-description-length.ts
@@ -0,0 +1,26 @@
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 description = {
11 type: Sequelize.STRING(1000),
12 allowNull: true
13 }
14 await utils.queryInterface.changeColumn('videoPlaylist', 'description', description)
15 }
16
17}
18
19function down (options) {
20 throw new Error('Not implemented.')
21}
22
23export {
24 up,
25 down
26}
diff --git a/server/initializers/migrations/0505-user-last-login-date.ts b/server/initializers/migrations/0505-user-last-login-date.ts
new file mode 100644
index 000000000..29d970802
--- /dev/null
+++ b/server/initializers/migrations/0505-user-last-login-date.ts
@@ -0,0 +1,26 @@
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 field = {
11 type: Sequelize.DATE,
12 allowNull: true
13 }
14 await utils.queryInterface.addColumn('user', 'lastLoginDate', field)
15 }
16
17}
18
19function down (options) {
20 throw new Error('Not implemented.')
21}
22
23export {
24 up,
25 down
26}
diff --git a/server/initializers/migrator.ts b/server/initializers/migrator.ts
index 1cb0116b7..77203ae24 100644
--- a/server/initializers/migrator.ts
+++ b/server/initializers/migrator.ts
@@ -20,7 +20,7 @@ async function migrate () {
20 } 20 }
21 21
22 const rows = await sequelizeTypescript.query<{ migrationVersion: number }>(query, options) 22 const rows = await sequelizeTypescript.query<{ migrationVersion: number }>(query, options)
23 if (rows && rows[0] && rows[0].migrationVersion) { 23 if (rows?.[0]?.migrationVersion) {
24 actualVersion = rows[0].migrationVersion 24 actualVersion = rows[0].migrationVersion
25 } 25 }
26 26
@@ -60,7 +60,7 @@ export {
60async function getMigrationScripts () { 60async function getMigrationScripts () {
61 const files = await readdir(path.join(__dirname, 'migrations')) 61 const files = await readdir(path.join(__dirname, 'migrations'))
62 const filesToMigrate: { 62 const filesToMigrate: {
63 version: string, 63 version: string
64 script: string 64 script: string
65 }[] = [] 65 }[] = []
66 66
diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts
index 0b21de0ca..c743dcf3f 100644
--- a/server/lib/activitypub/actor.ts
+++ b/server/lib/activitypub/actor.ts
@@ -1,8 +1,8 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { Transaction } from 'sequelize' 2import { Transaction } from 'sequelize'
3import * as url from 'url' 3import { URL } from 'url'
4import * as uuidv4 from 'uuid/v4' 4import { v4 as uuidv4 } from 'uuid'
5import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub' 5import { ActivityPubActor, ActivityPubActorType, ActivityPubOrderedCollection } from '../../../shared/models/activitypub'
6import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' 6import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
7import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' 7import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
8import { sanitizeAndCheckActorObject } from '../../helpers/custom-validators/activitypub/actor' 8import { sanitizeAndCheckActorObject } from '../../helpers/custom-validators/activitypub/actor'
@@ -19,7 +19,6 @@ import { AvatarModel } from '../../models/avatar/avatar'
19import { ServerModel } from '../../models/server/server' 19import { ServerModel } from '../../models/server/server'
20import { VideoChannelModel } from '../../models/video/video-channel' 20import { VideoChannelModel } from '../../models/video/video-channel'
21import { JobQueue } from '../job-queue' 21import { JobQueue } from '../job-queue'
22import { getServerActor } from '../../helpers/utils'
23import { ActorFetchByUrlType, fetchActorByUrl } from '../../helpers/actor' 22import { ActorFetchByUrlType, fetchActorByUrl } from '../../helpers/actor'
24import { sequelizeTypescript } from '../../initializers/database' 23import { sequelizeTypescript } from '../../initializers/database'
25import { 24import {
@@ -33,9 +32,10 @@ import {
33 MActorFull, 32 MActorFull,
34 MActorFullActor, 33 MActorFullActor,
35 MActorId, 34 MActorId,
36 MChannel, 35 MChannel
37 MChannelAccountDefault
38} from '../../typings/models' 36} from '../../typings/models'
37import { extname } from 'path'
38import { getServerActor } from '@server/models/application/application'
39 39
40// Set account keys, this could be long so process after the account creation and do not block the client 40// Set account keys, this could be long so process after the account creation and do not block the client
41function setAsyncActorKeys <T extends MActor> (actor: T) { 41function setAsyncActorKeys <T extends MActor> (actor: T) {
@@ -117,17 +117,17 @@ async function getOrCreateActorAndServerAndModel (
117 if (actor.VideoChannel) (actor as MActorAccountChannelIdActor).VideoChannel.Actor = actor 117 if (actor.VideoChannel) (actor as MActorAccountChannelIdActor).VideoChannel.Actor = actor
118 118
119 const { actor: actorRefreshed, refreshed } = await retryTransactionWrapper(refreshActorIfNeeded, actor, fetchType) 119 const { actor: actorRefreshed, refreshed } = await retryTransactionWrapper(refreshActorIfNeeded, actor, fetchType)
120 if (!actorRefreshed) throw new Error('Actor ' + actorRefreshed.url + ' does not exist anymore.') 120 if (!actorRefreshed) throw new Error('Actor ' + actor.url + ' does not exist anymore.')
121 121
122 if ((created === true || refreshed === true) && updateCollections === true) { 122 if ((created === true || refreshed === true) && updateCollections === true) {
123 const payload = { uri: actor.outboxUrl, type: 'activity' as 'activity' } 123 const payload = { uri: actor.outboxUrl, type: 'activity' as 'activity' }
124 await JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload }) 124 await JobQueue.Instance.createJobWithPromise({ type: 'activitypub-http-fetcher', payload })
125 } 125 }
126 126
127 // We created a new account: fetch the playlists 127 // We created a new account: fetch the playlists
128 if (created === true && actor.Account && accountPlaylistsUrl) { 128 if (created === true && actor.Account && accountPlaylistsUrl) {
129 const payload = { uri: accountPlaylistsUrl, accountId: actor.Account.id, type: 'account-playlists' as 'account-playlists' } 129 const payload = { uri: accountPlaylistsUrl, accountId: actor.Account.id, type: 'account-playlists' as 'account-playlists' }
130 await JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload }) 130 await JobQueue.Instance.createJobWithPromise({ type: 'activitypub-http-fetcher', payload })
131 } 131 }
132 132
133 return actorRefreshed 133 return actorRefreshed
@@ -207,7 +207,7 @@ async function fetchActorTotalItems (url: string) {
207 } 207 }
208 208
209 try { 209 try {
210 const { body } = await doRequest(options) 210 const { body } = await doRequest<ActivityPubOrderedCollection<unknown>>(options)
211 return body.totalItems ? body.totalItems : 0 211 return body.totalItems ? body.totalItems : 0
212 } catch (err) { 212 } catch (err) {
213 logger.warn('Cannot fetch remote actor count %s.', url, { err }) 213 logger.warn('Cannot fetch remote actor count %s.', url, { err })
@@ -215,20 +215,28 @@ async function fetchActorTotalItems (url: string) {
215 } 215 }
216} 216}
217 217
218async function getAvatarInfoIfExists (actorJSON: ActivityPubActor) { 218function getAvatarInfoIfExists (actorJSON: ActivityPubActor) {
219 if ( 219 const mimetypes = MIMETYPES.IMAGE
220 actorJSON.icon && actorJSON.icon.type === 'Image' && MIMETYPES.IMAGE.MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined && 220 const icon = actorJSON.icon
221 isActivityPubUrlValid(actorJSON.icon.url)
222 ) {
223 const extension = MIMETYPES.IMAGE.MIMETYPE_EXT[actorJSON.icon.mediaType]
224 221
225 return { 222 if (!icon || icon.type !== 'Image' || !isActivityPubUrlValid(icon.url)) return undefined
226 name: uuidv4() + extension, 223
227 fileUrl: actorJSON.icon.url 224 let extension: string
228 } 225
226 if (icon.mediaType) {
227 extension = mimetypes.MIMETYPE_EXT[icon.mediaType]
228 } else {
229 const tmp = extname(icon.url)
230
231 if (mimetypes.EXT_MIMETYPE[tmp] !== undefined) extension = tmp
229 } 232 }
230 233
231 return undefined 234 if (!extension) return undefined
235
236 return {
237 name: uuidv4() + extension,
238 fileUrl: icon.url
239 }
232} 240}
233 241
234async function addFetchOutboxJob (actor: Pick<ActorModel, 'id' | 'outboxUrl'>) { 242async function addFetchOutboxJob (actor: Pick<ActorModel, 'id' | 'outboxUrl'>) {
@@ -271,7 +279,10 @@ async function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannel
271 279
272 if (statusCode === 404) { 280 if (statusCode === 404) {
273 logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url) 281 logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url)
274 actor.Account ? actor.Account.destroy() : actor.VideoChannel.destroy() 282 actor.Account
283 ? await actor.Account.destroy()
284 : await actor.VideoChannel.destroy()
285
275 return { actor: undefined, refreshed: false } 286 return { actor: undefined, refreshed: false }
276 } 287 }
277 288
@@ -337,14 +348,14 @@ function saveActorAndServerAndModelIfNotExist (
337 ownerActor?: MActorFullActor, 348 ownerActor?: MActorFullActor,
338 t?: Transaction 349 t?: Transaction
339): Bluebird<MActorFullActor> | Promise<MActorFullActor> { 350): Bluebird<MActorFullActor> | Promise<MActorFullActor> {
340 let actor = result.actor 351 const actor = result.actor
341 352
342 if (t !== undefined) return save(t) 353 if (t !== undefined) return save(t)
343 354
344 return sequelizeTypescript.transaction(t => save(t)) 355 return sequelizeTypescript.transaction(t => save(t))
345 356
346 async function save (t: Transaction) { 357 async function save (t: Transaction) {
347 const actorHost = url.parse(actor.url).host 358 const actorHost = new URL(actor.url).host
348 359
349 const serverOptions = { 360 const serverOptions = {
350 where: { 361 where: {
@@ -402,7 +413,7 @@ type FetchRemoteActorResult = {
402 support?: string 413 support?: string
403 playlists?: string 414 playlists?: string
404 avatar?: { 415 avatar?: {
405 name: string, 416 name: string
406 fileUrl: string 417 fileUrl: string
407 } 418 }
408 attributedTo: ActivityPubAttributedTo[] 419 attributedTo: ActivityPubAttributedTo[]
diff --git a/server/lib/activitypub/audience.ts b/server/lib/activitypub/audience.ts
index f2ab54cf7..551d04ae3 100644
--- a/server/lib/activitypub/audience.ts
+++ b/server/lib/activitypub/audience.ts
@@ -4,11 +4,11 @@ import { 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 { VideoShareModel } from '../../models/video/video-share' 6import { VideoShareModel } from '../../models/video/video-share'
7import { MActorFollowersUrl, MActorLight, MCommentOwner, MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../../typings/models' 7import { MActorFollowersUrl, MActorLight, MActorUrl, MCommentOwner, MCommentOwnerVideo, MVideoId } from '../../typings/models'
8 8
9function getRemoteVideoAudience (video: MVideoAccountLight, actorsInvolvedInVideo: MActorFollowersUrl[]): ActivityAudience { 9function getRemoteVideoAudience (accountActor: MActorUrl, actorsInvolvedInVideo: MActorFollowersUrl[]): ActivityAudience {
10 return { 10 return {
11 to: [ video.VideoChannel.Account.Actor.url ], 11 to: [ accountActor.url ],
12 cc: actorsInvolvedInVideo.map(a => a.followersUrl) 12 cc: actorsInvolvedInVideo.map(a => a.followersUrl)
13 } 13 }
14} 14}
@@ -32,6 +32,8 @@ function getVideoCommentAudience (
32 32
33 // Send to actors we reply to 33 // Send to actors we reply to
34 for (const parentComment of threadParentComments) { 34 for (const parentComment of threadParentComments) {
35 if (parentComment.isDeleted()) continue
36
35 cc.push(parentComment.Account.Actor.url) 37 cc.push(parentComment.Account.Actor.url)
36 } 38 }
37 39
@@ -48,7 +50,7 @@ function getAudienceFromFollowersOf (actorsInvolvedInObject: MActorFollowersUrl[
48 } 50 }
49} 51}
50 52
51async function getActorsInvolvedInVideo (video: MVideo, t: Transaction) { 53async function getActorsInvolvedInVideo (video: MVideoId, t: Transaction) {
52 const actors: MActorLight[] = await VideoShareModel.loadActorsByShare(video.id, t) 54 const actors: MActorLight[] = await VideoShareModel.loadActorsByShare(video.id, t)
53 55
54 const videoAll = video as VideoModel 56 const videoAll = video as VideoModel
diff --git a/server/lib/activitypub/cache-file.ts b/server/lib/activitypub/cache-file.ts
index 65b2dcb49..8252e95e9 100644
--- a/server/lib/activitypub/cache-file.ts
+++ b/server/lib/activitypub/cache-file.ts
@@ -13,7 +13,7 @@ function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject
13 if (!playlist) throw new Error('Cannot find HLS playlist of video ' + video.url) 13 if (!playlist) throw new Error('Cannot find HLS playlist of video ' + video.url)
14 14
15 return { 15 return {
16 expiresOn: new Date(cacheFileObject.expires), 16 expiresOn: cacheFileObject.expires ? new Date(cacheFileObject.expires) : null,
17 url: cacheFileObject.id, 17 url: cacheFileObject.id,
18 fileUrl: url.href, 18 fileUrl: url.href,
19 strategy: null, 19 strategy: null,
@@ -30,7 +30,7 @@ function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject
30 if (!videoFile) throw new Error(`Cannot find video file ${url.height} ${url.fps} of video ${video.url}`) 30 if (!videoFile) throw new Error(`Cannot find video file ${url.height} ${url.fps} of video ${video.url}`)
31 31
32 return { 32 return {
33 expiresOn: new Date(cacheFileObject.expires), 33 expiresOn: cacheFileObject.expires ? new Date(cacheFileObject.expires) : null,
34 url: cacheFileObject.id, 34 url: cacheFileObject.id,
35 fileUrl: url.href, 35 fileUrl: url.href,
36 strategy: null, 36 strategy: null,
diff --git a/server/lib/activitypub/crawl.ts b/server/lib/activitypub/crawl.ts
index 9e469e3e6..eeafdf4ba 100644
--- a/server/lib/activitypub/crawl.ts
+++ b/server/lib/activitypub/crawl.ts
@@ -3,7 +3,7 @@ import { 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' 6import { URL } from 'url'
7 7
8type HandlerFunction<T> = (items: T[]) => (Promise<any> | Bluebird<any>) 8type HandlerFunction<T> = (items: T[]) => (Promise<any> | Bluebird<any>)
9type CleanerFunction = (startedDate: Date) => (Promise<any> | Bluebird<any>) 9type CleanerFunction = (startedDate: Date) => (Promise<any> | Bluebird<any>)
@@ -24,7 +24,7 @@ async function crawlCollectionPage <T> (uri: string, handler: HandlerFunction<T>
24 const response = await doRequest<ActivityPubOrderedCollection<T>>(options) 24 const response = await doRequest<ActivityPubOrderedCollection<T>>(options)
25 const firstBody = response.body 25 const firstBody = response.body
26 26
27 let limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT 27 const limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT
28 let i = 0 28 let i = 0
29 let nextLink = firstBody.first 29 let nextLink = firstBody.first
30 while (nextLink && i < limit) { 30 while (nextLink && i < limit) {
@@ -32,7 +32,7 @@ async function crawlCollectionPage <T> (uri: string, handler: HandlerFunction<T>
32 32
33 if (typeof nextLink === 'string') { 33 if (typeof nextLink === 'string') {
34 // Don't crawl ourselves 34 // Don't crawl ourselves
35 const remoteHost = parse(nextLink).host 35 const remoteHost = new URL(nextLink).host
36 if (remoteHost === WEBSERVER.HOST) continue 36 if (remoteHost === WEBSERVER.HOST) continue
37 37
38 options.uri = nextLink 38 options.uri = nextLink
diff --git a/server/lib/activitypub/follow.ts b/server/lib/activitypub/follow.ts
index 1abf43cd4..3b5ad47c9 100644
--- a/server/lib/activitypub/follow.ts
+++ b/server/lib/activitypub/follow.ts
@@ -3,8 +3,8 @@ import { CONFIG } from '../../initializers/config'
3import { SERVER_ACTOR_NAME } from '../../initializers/constants' 3import { SERVER_ACTOR_NAME } from '../../initializers/constants'
4import { JobQueue } from '../job-queue' 4import { JobQueue } from '../job-queue'
5import { logger } from '../../helpers/logger' 5import { logger } from '../../helpers/logger'
6import { getServerActor } from '../../helpers/utils'
7import { ServerModel } from '../../models/server/server' 6import { ServerModel } from '../../models/server/server'
7import { getServerActor } from '@server/models/application/application'
8 8
9async function autoFollowBackIfNeeded (actorFollow: MActorFollowActors) { 9async function autoFollowBackIfNeeded (actorFollow: MActorFollowActors) {
10 if (!CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_BACK.ENABLED) return 10 if (!CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_BACK.ENABLED) return
@@ -27,7 +27,6 @@ async function autoFollowBackIfNeeded (actorFollow: MActorFollowActors) {
27 } 27 }
28 28
29 JobQueue.Instance.createJob({ type: 'activitypub-follow', payload }) 29 JobQueue.Instance.createJob({ type: 'activitypub-follow', payload })
30 .catch(err => logger.error('Cannot create auto follow back job for %s.', host, err))
31 } 30 }
32} 31}
33 32
diff --git a/server/lib/activitypub/index.ts b/server/lib/activitypub/index.ts
deleted file mode 100644
index d8c7d83b7..000000000
--- a/server/lib/activitypub/index.ts
+++ /dev/null
@@ -1,9 +0,0 @@
1export * from './process'
2export * from './send'
3export * from './actor'
4export * from './share'
5export * from './playlist'
6export * from './videos'
7export * from './video-comments'
8export * from './video-rates'
9export * from './url'
diff --git a/server/lib/activitypub/playlist.ts b/server/lib/activitypub/playlist.ts
index c52b715ef..c1d932a68 100644
--- a/server/lib/activitypub/playlist.ts
+++ b/server/lib/activitypub/playlist.ts
@@ -20,7 +20,9 @@ import { MAccountDefault, MAccountId, MVideoId } from '../../typings/models'
20import { MVideoPlaylist, MVideoPlaylistId, MVideoPlaylistOwner } from '../../typings/models/video/video-playlist' 20import { MVideoPlaylist, MVideoPlaylistId, MVideoPlaylistOwner } from '../../typings/models/video/video-playlist'
21 21
22function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: MAccountId, to: string[]) { 22function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: MAccountId, to: string[]) {
23 const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPlaylistPrivacy.PUBLIC : VideoPlaylistPrivacy.UNLISTED 23 const privacy = to.includes(ACTIVITY_PUB.PUBLIC)
24 ? VideoPlaylistPrivacy.PUBLIC
25 : VideoPlaylistPrivacy.UNLISTED
24 26
25 return { 27 return {
26 name: playlistObject.name, 28 name: playlistObject.name,
@@ -205,7 +207,7 @@ async function fetchRemoteVideoPlaylist (playlistUrl: string): Promise<{ statusC
205 207
206 logger.info('Fetching remote playlist %s.', playlistUrl) 208 logger.info('Fetching remote playlist %s.', playlistUrl)
207 209
208 const { response, body } = await doRequest(options) 210 const { response, body } = await doRequest<any>(options)
209 211
210 if (isPlaylistObjectValid(body) === false || checkUrlsSameHost(body.id, playlistUrl) !== true) { 212 if (isPlaylistObjectValid(body) === false || checkUrlsSameHost(body.id, playlistUrl) !== true) {
211 logger.debug('Remote video playlist JSON is not valid.', { body }) 213 logger.debug('Remote video playlist JSON is not valid.', { body })
diff --git a/server/lib/activitypub/process/process-announce.ts b/server/lib/activitypub/process/process-announce.ts
index 7e22125d5..26427aaa1 100644
--- a/server/lib/activitypub/process/process-announce.ts
+++ b/server/lib/activitypub/process/process-announce.ts
@@ -1,6 +1,6 @@
1import { ActivityAnnounce } from '../../../../shared/models/activitypub' 1import { ActivityAnnounce } from '../../../../shared/models/activitypub'
2import { retryTransactionWrapper } from '../../../helpers/database-utils' 2import { retryTransactionWrapper } from '../../../helpers/database-utils'
3import { sequelizeTypescript } from '../../../initializers' 3import { sequelizeTypescript } from '../../../initializers/database'
4import { VideoShareModel } from '../../../models/video/video-share' 4import { VideoShareModel } from '../../../models/video/video-share'
5import { forwardVideoRelatedActivity } from '../send/utils' 5import { forwardVideoRelatedActivity } from '../send/utils'
6import { getOrCreateVideoAndAccountAndChannel } from '../videos' 6import { getOrCreateVideoAndAccountAndChannel } from '../videos'
diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts
index bee853721..566bf6992 100644
--- a/server/lib/activitypub/process/process-create.ts
+++ b/server/lib/activitypub/process/process-create.ts
@@ -2,7 +2,7 @@ import { ActivityCreate, CacheFileObject, VideoTorrentObject } from '../../../..
2import { VideoCommentObject } from '../../../../shared/models/activitypub/objects/video-comment-object' 2import { VideoCommentObject } from '../../../../shared/models/activitypub/objects/video-comment-object'
3import { retryTransactionWrapper } from '../../../helpers/database-utils' 3import { retryTransactionWrapper } from '../../../helpers/database-utils'
4import { logger } from '../../../helpers/logger' 4import { logger } from '../../../helpers/logger'
5import { sequelizeTypescript } from '../../../initializers' 5import { sequelizeTypescript } from '../../../initializers/database'
6import { resolveThread } from '../video-comments' 6import { resolveThread } from '../video-comments'
7import { getOrCreateVideoAndAccountAndChannel } from '../videos' 7import { getOrCreateVideoAndAccountAndChannel } from '../videos'
8import { forwardVideoRelatedActivity } from '../send/utils' 8import { forwardVideoRelatedActivity } from '../send/utils'
@@ -12,6 +12,7 @@ import { PlaylistObject } from '../../../../shared/models/activitypub/objects/pl
12import { createOrUpdateVideoPlaylist } from '../playlist' 12import { createOrUpdateVideoPlaylist } from '../playlist'
13import { APProcessorOptions } from '../../../typings/activitypub-processor.model' 13import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
14import { MActorSignature, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../../typings/models' 14import { MActorSignature, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../../typings/models'
15import { isRedundancyAccepted } from '@server/lib/redundancy'
15 16
16async function processCreateActivity (options: APProcessorOptions<ActivityCreate>) { 17async function processCreateActivity (options: APProcessorOptions<ActivityCreate>) {
17 const { activity, byActor } = options 18 const { activity, byActor } = options
@@ -60,6 +61,8 @@ async function processCreateVideo (activity: ActivityCreate, notify: boolean) {
60} 61}
61 62
62async function processCreateCacheFile (activity: ActivityCreate, byActor: MActorSignature) { 63async function processCreateCacheFile (activity: ActivityCreate, byActor: MActorSignature) {
64 if (await isRedundancyAccepted(activity, byActor) !== true) return
65
63 const cacheFile = activity.object as CacheFileObject 66 const cacheFile = activity.object as CacheFileObject
64 67
65 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: cacheFile.object }) 68 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: cacheFile.object })
diff --git a/server/lib/activitypub/process/process-delete.ts b/server/lib/activitypub/process/process-delete.ts
index e76132f91..7c8dc83e8 100644
--- a/server/lib/activitypub/process/process-delete.ts
+++ b/server/lib/activitypub/process/process-delete.ts
@@ -1,7 +1,7 @@
1import { ActivityDelete } from '../../../../shared/models/activitypub' 1import { ActivityDelete } from '../../../../shared/models/activitypub'
2import { retryTransactionWrapper } from '../../../helpers/database-utils' 2import { retryTransactionWrapper } from '../../../helpers/database-utils'
3import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
4import { sequelizeTypescript } from '../../../initializers' 4import { sequelizeTypescript } from '../../../initializers/database'
5import { ActorModel } from '../../../models/activitypub/actor' 5import { ActorModel } from '../../../models/activitypub/actor'
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'
diff --git a/server/lib/activitypub/process/process-dislike.ts b/server/lib/activitypub/process/process-dislike.ts
index debd8a67c..fcdd0b86e 100644
--- a/server/lib/activitypub/process/process-dislike.ts
+++ b/server/lib/activitypub/process/process-dislike.ts
@@ -1,7 +1,7 @@
1import { ActivityCreate, ActivityDislike } from '../../../../shared' 1import { ActivityCreate, ActivityDislike } from '../../../../shared'
2import { DislikeObject } from '../../../../shared/models/activitypub/objects' 2import { DislikeObject } from '../../../../shared/models/activitypub/objects'
3import { retryTransactionWrapper } from '../../../helpers/database-utils' 3import { retryTransactionWrapper } from '../../../helpers/database-utils'
4import { sequelizeTypescript } from '../../../initializers' 4import { sequelizeTypescript } from '../../../initializers/database'
5import { AccountVideoRateModel } from '../../../models/account/account-video-rate' 5import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
6import { getOrCreateVideoAndAccountAndChannel } from '../videos' 6import { getOrCreateVideoAndAccountAndChannel } from '../videos'
7import { forwardVideoRelatedActivity } from '../send/utils' 7import { forwardVideoRelatedActivity } from '../send/utils'
diff --git a/server/lib/activitypub/process/process-flag.ts b/server/lib/activitypub/process/process-flag.ts
index e6e9084de..7337f337c 100644
--- a/server/lib/activitypub/process/process-flag.ts
+++ b/server/lib/activitypub/process/process-flag.ts
@@ -2,13 +2,14 @@ import { ActivityCreate, ActivityFlag, VideoAbuseState } from '../../../../share
2import { VideoAbuseObject } from '../../../../shared/models/activitypub/objects' 2import { VideoAbuseObject } from '../../../../shared/models/activitypub/objects'
3import { retryTransactionWrapper } from '../../../helpers/database-utils' 3import { retryTransactionWrapper } from '../../../helpers/database-utils'
4import { logger } from '../../../helpers/logger' 4import { logger } from '../../../helpers/logger'
5import { sequelizeTypescript } from '../../../initializers' 5import { sequelizeTypescript } from '../../../initializers/database'
6import { VideoAbuseModel } from '../../../models/video/video-abuse' 6import { VideoAbuseModel } from '../../../models/video/video-abuse'
7import { getOrCreateVideoAndAccountAndChannel } from '../videos' 7import { getOrCreateVideoAndAccountAndChannel } from '../videos'
8import { Notifier } from '../../notifier' 8import { Notifier } from '../../notifier'
9import { getAPId } from '../../../helpers/activitypub' 9import { getAPId } from '../../../helpers/activitypub'
10import { APProcessorOptions } from '../../../typings/activitypub-processor.model' 10import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
11import { MActorSignature, MVideoAbuseVideo } from '../../../typings/models' 11import { MActorSignature, MVideoAbuseAccountVideo } from '../../../typings/models'
12import { AccountModel } from '@server/models/account/account'
12 13
13async function processFlagActivity (options: APProcessorOptions<ActivityCreate | ActivityFlag>) { 14async function processFlagActivity (options: APProcessorOptions<ActivityCreate | ActivityFlag>) {
14 const { activity, byActor } = options 15 const { activity, byActor } = options
@@ -36,8 +37,9 @@ async function processCreateVideoAbuse (activity: ActivityCreate | ActivityFlag,
36 logger.debug('Reporting remote abuse for video %s.', getAPId(object)) 37 logger.debug('Reporting remote abuse for video %s.', getAPId(object))
37 38
38 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: object }) 39 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: object })
40 const reporterAccount = await sequelizeTypescript.transaction(async t => AccountModel.load(account.id, t))
39 41
40 const videoAbuse = await sequelizeTypescript.transaction(async t => { 42 const videoAbuseInstance = await sequelizeTypescript.transaction(async t => {
41 const videoAbuseData = { 43 const videoAbuseData = {
42 reporterAccountId: account.id, 44 reporterAccountId: account.id,
43 reason: flag.content, 45 reason: flag.content,
@@ -45,15 +47,22 @@ async function processCreateVideoAbuse (activity: ActivityCreate | ActivityFlag,
45 state: VideoAbuseState.PENDING 47 state: VideoAbuseState.PENDING
46 } 48 }
47 49
48 const videoAbuseInstance = await VideoAbuseModel.create(videoAbuseData, { transaction: t }) as MVideoAbuseVideo 50 const videoAbuseInstance: MVideoAbuseAccountVideo = await VideoAbuseModel.create(videoAbuseData, { transaction: t })
49 videoAbuseInstance.Video = video 51 videoAbuseInstance.Video = video
52 videoAbuseInstance.Account = reporterAccount
50 53
51 logger.info('Remote abuse for video uuid %s created', flag.object) 54 logger.info('Remote abuse for video uuid %s created', flag.object)
52 55
53 return videoAbuseInstance 56 return videoAbuseInstance
54 }) 57 })
55 58
56 Notifier.Instance.notifyOnNewVideoAbuse(videoAbuse) 59 const videoAbuseJSON = videoAbuseInstance.toFormattedJSON()
60
61 Notifier.Instance.notifyOnNewVideoAbuse({
62 videoAbuse: videoAbuseJSON,
63 videoAbuseInstance,
64 reporter: reporterAccount.Actor.getIdentifier()
65 })
57 } catch (err) { 66 } catch (err) {
58 logger.debug('Cannot process report of %s. (Maybe not a video abuse).', getAPId(object), { err }) 67 logger.debug('Cannot process report of %s. (Maybe not a video abuse).', getAPId(object), { err })
59 } 68 }
diff --git a/server/lib/activitypub/process/process-follow.ts b/server/lib/activitypub/process/process-follow.ts
index db7fb8568..950d421dd 100644
--- a/server/lib/activitypub/process/process-follow.ts
+++ b/server/lib/activitypub/process/process-follow.ts
@@ -1,17 +1,17 @@
1import { ActivityFollow } from '../../../../shared/models/activitypub' 1import { ActivityFollow } from '../../../../shared/models/activitypub'
2import { retryTransactionWrapper } from '../../../helpers/database-utils' 2import { retryTransactionWrapper } from '../../../helpers/database-utils'
3import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
4import { sequelizeTypescript } from '../../../initializers' 4import { sequelizeTypescript } from '../../../initializers/database'
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, sendReject } 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' 10import { CONFIG } from '../../../initializers/config'
12import { APProcessorOptions } from '../../../typings/activitypub-processor.model' 11import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
13import { MActorFollowActors, MActorSignature } from '../../../typings/models' 12import { MActorFollowActors, MActorSignature } from '../../../typings/models'
14import { autoFollowBackIfNeeded } from '../follow' 13import { autoFollowBackIfNeeded } from '../follow'
14import { getServerActor } from '@server/models/application/application'
15 15
16async function processFollowActivity (options: APProcessorOptions<ActivityFollow>) { 16async function processFollowActivity (options: APProcessorOptions<ActivityFollow>) {
17 const { activity, byActor } = options 17 const { activity, byActor } = options
diff --git a/server/lib/activitypub/process/process-like.ts b/server/lib/activitypub/process/process-like.ts
index 62be0de42..fba3c76a4 100644
--- a/server/lib/activitypub/process/process-like.ts
+++ b/server/lib/activitypub/process/process-like.ts
@@ -1,6 +1,6 @@
1import { ActivityLike } from '../../../../shared/models/activitypub' 1import { ActivityLike } from '../../../../shared/models/activitypub'
2import { retryTransactionWrapper } from '../../../helpers/database-utils' 2import { retryTransactionWrapper } from '../../../helpers/database-utils'
3import { sequelizeTypescript } from '../../../initializers' 3import { sequelizeTypescript } from '../../../initializers/database'
4import { AccountVideoRateModel } from '../../../models/account/account-video-rate' 4import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
5import { forwardVideoRelatedActivity } from '../send/utils' 5import { forwardVideoRelatedActivity } from '../send/utils'
6import { getOrCreateVideoAndAccountAndChannel } from '../videos' 6import { getOrCreateVideoAndAccountAndChannel } from '../videos'
diff --git a/server/lib/activitypub/process/process-reject.ts b/server/lib/activitypub/process/process-reject.ts
index 00e9afa10..9804436a2 100644
--- a/server/lib/activitypub/process/process-reject.ts
+++ b/server/lib/activitypub/process/process-reject.ts
@@ -1,5 +1,5 @@
1import { ActivityReject } from '../../../../shared/models/activitypub/activity' 1import { ActivityReject } from '../../../../shared/models/activitypub/activity'
2import { sequelizeTypescript } from '../../../initializers' 2import { sequelizeTypescript } from '../../../initializers/database'
3import { ActorFollowModel } from '../../../models/activitypub/actor-follow' 3import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
4import { APProcessorOptions } from '../../../typings/activitypub-processor.model' 4import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
5import { MActor } from '../../../typings/models' 5import { MActor } from '../../../typings/models'
diff --git a/server/lib/activitypub/process/process-undo.ts b/server/lib/activitypub/process/process-undo.ts
index 10643b2e9..9ef6a8a97 100644
--- a/server/lib/activitypub/process/process-undo.ts
+++ b/server/lib/activitypub/process/process-undo.ts
@@ -2,7 +2,7 @@ import { ActivityAnnounce, ActivityFollow, ActivityLike, ActivityUndo, CacheFile
2import { DislikeObject } from '../../../../shared/models/activitypub/objects' 2import { DislikeObject } from '../../../../shared/models/activitypub/objects'
3import { retryTransactionWrapper } from '../../../helpers/database-utils' 3import { retryTransactionWrapper } from '../../../helpers/database-utils'
4import { logger } from '../../../helpers/logger' 4import { logger } from '../../../helpers/logger'
5import { sequelizeTypescript } from '../../../initializers' 5import { sequelizeTypescript } from '../../../initializers/database'
6import { AccountVideoRateModel } from '../../../models/account/account-video-rate' 6import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
7import { ActorModel } from '../../../models/activitypub/actor' 7import { ActorModel } from '../../../models/activitypub/actor'
8import { ActorFollowModel } from '../../../models/activitypub/actor-follow' 8import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts
index a47d605d8..98ab0f83d 100644
--- a/server/lib/activitypub/process/process-update.ts
+++ b/server/lib/activitypub/process/process-update.ts
@@ -2,7 +2,7 @@ import { ActivityUpdate, CacheFileObject, VideoTorrentObject } from '../../../..
2import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor' 2import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor'
3import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils' 3import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils'
4import { logger } from '../../../helpers/logger' 4import { logger } from '../../../helpers/logger'
5import { sequelizeTypescript } from '../../../initializers' 5import { sequelizeTypescript } from '../../../initializers/database'
6import { AccountModel } from '../../../models/account/account' 6import { AccountModel } from '../../../models/account/account'
7import { ActorModel } from '../../../models/activitypub/actor' 7import { ActorModel } from '../../../models/activitypub/actor'
8import { VideoChannelModel } from '../../../models/video/video-channel' 8import { VideoChannelModel } from '../../../models/video/video-channel'
@@ -16,6 +16,7 @@ import { PlaylistObject } from '../../../../shared/models/activitypub/objects/pl
16import { createOrUpdateVideoPlaylist } from '../playlist' 16import { createOrUpdateVideoPlaylist } from '../playlist'
17import { APProcessorOptions } from '../../../typings/activitypub-processor.model' 17import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
18import { MActorSignature, MAccountIdActor } from '../../../typings/models' 18import { MActorSignature, MAccountIdActor } from '../../../typings/models'
19import { isRedundancyAccepted } from '@server/lib/redundancy'
19 20
20async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) { 21async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) {
21 const { activity, byActor } = options 22 const { activity, byActor } = options
@@ -78,6 +79,8 @@ async function processUpdateVideo (actor: MActorSignature, activity: ActivityUpd
78} 79}
79 80
80async function processUpdateCacheFile (byActor: MActorSignature, activity: ActivityUpdate) { 81async function processUpdateCacheFile (byActor: MActorSignature, activity: ActivityUpdate) {
82 if (await isRedundancyAccepted(activity, byActor) !== true) return
83
81 const cacheFileObject = activity.object as CacheFileObject 84 const cacheFileObject = activity.object as CacheFileObject
82 85
83 if (!isCacheFileObjectValid(cacheFileObject)) { 86 if (!isCacheFileObjectValid(cacheFileObject)) {
diff --git a/server/lib/activitypub/process/process-view.ts b/server/lib/activitypub/process/process-view.ts
index df29ee968..b3b6c933d 100644
--- a/server/lib/activitypub/process/process-view.ts
+++ b/server/lib/activitypub/process/process-view.ts
@@ -23,7 +23,8 @@ async function processCreateView (activity: ActivityView | ActivityCreate, byAct
23 23
24 const options = { 24 const options = {
25 videoObject, 25 videoObject,
26 fetchType: 'only-video' as 'only-video' 26 fetchType: 'only-immutable-attributes' as 'only-immutable-attributes',
27 allowRefresh: false as false
27 } 28 }
28 const { video } = await getOrCreateVideoAndAccountAndChannel(options) 29 const { video } = await getOrCreateVideoAndAccountAndChannel(options)
29 30
diff --git a/server/lib/activitypub/send/send-accept.ts b/server/lib/activitypub/send/send-accept.ts
index 9f0225b64..c4c6b849b 100644
--- a/server/lib/activitypub/send/send-accept.ts
+++ b/server/lib/activitypub/send/send-accept.ts
@@ -5,7 +5,7 @@ import { buildFollowActivity } from './send-follow'
5import { logger } from '../../../helpers/logger' 5import { logger } from '../../../helpers/logger'
6import { MActor, MActorFollowActors } from '../../../typings/models' 6import { MActor, MActorFollowActors } from '../../../typings/models'
7 7
8async function sendAccept (actorFollow: MActorFollowActors) { 8function sendAccept (actorFollow: MActorFollowActors) {
9 const follower = actorFollow.ActorFollower 9 const follower = actorFollow.ActorFollower
10 const me = actorFollow.ActorFollowing 10 const me = actorFollow.ActorFollowing
11 11
diff --git a/server/lib/activitypub/send/send-announce.ts b/server/lib/activitypub/send/send-announce.ts
index a0f33852c..d03b358f1 100644
--- a/server/lib/activitypub/send/send-announce.ts
+++ b/server/lib/activitypub/send/send-announce.ts
@@ -28,7 +28,7 @@ async function sendVideoAnnounce (byActor: MActorLight, videoShare: MVideoShare,
28 logger.info('Creating job to send announce %s.', videoShare.url) 28 logger.info('Creating job to send announce %s.', videoShare.url)
29 29
30 const followersException = [ byActor ] 30 const followersException = [ byActor ]
31 return broadcastToFollowers(activity, byActor, actorsInvolvedInVideo, t, followersException) 31 return broadcastToFollowers(activity, byActor, actorsInvolvedInVideo, t, followersException, 'Announce')
32} 32}
33 33
34function buildAnnounceActivity (url: string, byActor: MActorLight, object: string, audience?: ActivityAudience): ActivityAnnounce { 34function buildAnnounceActivity (url: string, byActor: MActorLight, object: string, audience?: ActivityAudience): ActivityAnnounce {
diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts
index 1709d8348..e521cabbc 100644
--- a/server/lib/activitypub/send/send-create.ts
+++ b/server/lib/activitypub/send/send-create.ts
@@ -6,7 +6,6 @@ import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unic
6import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience' 6import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience'
7import { logger } from '../../../helpers/logger' 7import { logger } from '../../../helpers/logger'
8import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' 8import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
9import { getServerActor } from '../../../helpers/utils'
10import { 9import {
11 MActorLight, 10 MActorLight,
12 MCommentOwnerVideo, 11 MCommentOwnerVideo,
@@ -16,6 +15,8 @@ import {
16 MVideoRedundancyFileVideo, 15 MVideoRedundancyFileVideo,
17 MVideoRedundancyStreamingPlaylistVideo 16 MVideoRedundancyStreamingPlaylistVideo
18} from '../../../typings/models' 17} from '../../../typings/models'
18import { getServerActor } from '@server/models/application/application'
19import { ContextType } from '@shared/models/activitypub/context'
19 20
20async function sendCreateVideo (video: MVideoAP, t: Transaction) { 21async function sendCreateVideo (video: MVideoAP, t: Transaction) {
21 if (!video.hasPrivacyForFederation()) return undefined 22 if (!video.hasPrivacyForFederation()) return undefined
@@ -42,7 +43,8 @@ async function sendCreateCacheFile (
42 byActor, 43 byActor,
43 video, 44 video,
44 url: fileRedundancy.url, 45 url: fileRedundancy.url,
45 object: fileRedundancy.toActivityPubObject() 46 object: fileRedundancy.toActivityPubObject(),
47 contextType: 'CacheFile'
46 }) 48 })
47} 49}
48 50
@@ -78,7 +80,8 @@ async function sendCreateVideoComment (comment: MCommentOwnerVideo, t: Transacti
78 // Add the actor that commented too 80 // Add the actor that commented too
79 actorsInvolvedInComment.push(byActor) 81 actorsInvolvedInComment.push(byActor)
80 82
81 const parentsCommentActors = threadParentComments.map(c => c.Account.Actor) 83 const parentsCommentActors = threadParentComments.filter(c => !c.isDeleted())
84 .map(c => c.Account.Actor)
82 85
83 let audience: ActivityAudience 86 let audience: ActivityAudience
84 if (isOrigin) { 87 if (isOrigin) {
@@ -130,11 +133,12 @@ export {
130// --------------------------------------------------------------------------- 133// ---------------------------------------------------------------------------
131 134
132async function sendVideoRelatedCreateActivity (options: { 135async function sendVideoRelatedCreateActivity (options: {
133 byActor: MActorLight, 136 byActor: MActorLight
134 video: MVideoAccountLight, 137 video: MVideoAccountLight
135 url: string, 138 url: string
136 object: any, 139 object: any
137 transaction?: Transaction 140 transaction?: Transaction
141 contextType?: ContextType
138}) { 142}) {
139 const activityBuilder = (audience: ActivityAudience) => { 143 const activityBuilder = (audience: ActivityAudience) => {
140 return buildCreateActivity(options.url, options.byActor, options.object, audience) 144 return buildCreateActivity(options.url, options.byActor, options.object, audience)
diff --git a/server/lib/activitypub/send/send-delete.ts b/server/lib/activitypub/send/send-delete.ts
index 3225ebf32..fd3f06dec 100644
--- a/server/lib/activitypub/send/send-delete.ts
+++ b/server/lib/activitypub/send/send-delete.ts
@@ -7,9 +7,9 @@ import { getDeleteActivityPubUrl } from '../url'
7import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' 7import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils'
8import { audiencify, getActorsInvolvedInVideo, getVideoCommentAudience } from '../audience' 8import { audiencify, getActorsInvolvedInVideo, getVideoCommentAudience } from '../audience'
9import { logger } from '../../../helpers/logger' 9import { logger } from '../../../helpers/logger'
10import { getServerActor } from '../../../helpers/utils'
11import { MCommentOwnerVideoReply, MVideoAccountLight, MVideoPlaylistFullSummary } from '../../../typings/models/video' 10import { MCommentOwnerVideoReply, MVideoAccountLight, MVideoPlaylistFullSummary } from '../../../typings/models/video'
12import { MActorUrl } from '../../../typings/models' 11import { MActorUrl } from '../../../typings/models'
12import { getServerActor } from '@server/models/application/application'
13 13
14async function sendDeleteVideo (video: MVideoAccountLight, transaction: Transaction) { 14async function sendDeleteVideo (video: MVideoAccountLight, transaction: Transaction) {
15 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)
diff --git a/server/lib/activitypub/send/send-dislike.ts b/server/lib/activitypub/send/send-dislike.ts
index 6e41f241f..600469c71 100644
--- a/server/lib/activitypub/send/send-dislike.ts
+++ b/server/lib/activitypub/send/send-dislike.ts
@@ -6,7 +6,7 @@ import { sendVideoRelatedActivity } from './utils'
6import { audiencify, getAudience } from '../audience' 6import { audiencify, getAudience } from '../audience'
7import { MActor, MActorAudience, MVideoAccountLight, MVideoUrl } from '../../../typings/models' 7import { MActor, MActorAudience, MVideoAccountLight, MVideoUrl } from '../../../typings/models'
8 8
9async function sendDislike (byActor: MActor, video: MVideoAccountLight, t: Transaction) { 9function sendDislike (byActor: MActor, video: MVideoAccountLight, t: Transaction) {
10 logger.info('Creating job to dislike %s.', video.url) 10 logger.info('Creating job to dislike %s.', video.url)
11 11
12 const activityBuilder = (audience: ActivityAudience) => { 12 const activityBuilder = (audience: ActivityAudience) => {
diff --git a/server/lib/activitypub/send/send-flag.ts b/server/lib/activitypub/send/send-flag.ts
index da7638a7b..e4e523631 100644
--- a/server/lib/activitypub/send/send-flag.ts
+++ b/server/lib/activitypub/send/send-flag.ts
@@ -7,7 +7,7 @@ import { Transaction } from 'sequelize'
7import { MActor, MVideoFullLight } from '../../../typings/models' 7import { MActor, MVideoFullLight } from '../../../typings/models'
8import { MVideoAbuseVideo } from '../../../typings/models/video' 8import { MVideoAbuseVideo } from '../../../typings/models/video'
9 9
10async function sendVideoAbuse (byActor: MActor, videoAbuse: MVideoAbuseVideo, video: MVideoFullLight, t: Transaction) { 10function sendVideoAbuse (byActor: MActor, videoAbuse: MVideoAbuseVideo, video: MVideoFullLight, t: Transaction) {
11 if (!video.VideoChannel.Account.Actor.serverId) return // Local user 11 if (!video.VideoChannel.Account.Actor.serverId) return // Local user
12 12
13 const url = getVideoAbuseActivityPubUrl(videoAbuse) 13 const url = getVideoAbuseActivityPubUrl(videoAbuse)
diff --git a/server/lib/activitypub/send/send-like.ts b/server/lib/activitypub/send/send-like.ts
index e84a6f98b..5db252325 100644
--- a/server/lib/activitypub/send/send-like.ts
+++ b/server/lib/activitypub/send/send-like.ts
@@ -6,7 +6,7 @@ import { audiencify, getAudience } from '../audience'
6import { logger } from '../../../helpers/logger' 6import { logger } from '../../../helpers/logger'
7import { MActor, MActorAudience, MVideoAccountLight, MVideoUrl } from '../../../typings/models' 7import { MActor, MActorAudience, MVideoAccountLight, MVideoUrl } from '../../../typings/models'
8 8
9async function sendLike (byActor: MActor, video: MVideoAccountLight, t: Transaction) { 9function sendLike (byActor: MActor, video: MVideoAccountLight, t: Transaction) {
10 logger.info('Creating job to like %s.', video.url) 10 logger.info('Creating job to like %s.', video.url)
11 11
12 const activityBuilder = (audience: ActivityAudience) => { 12 const activityBuilder = (audience: ActivityAudience) => {
diff --git a/server/lib/activitypub/send/send-reject.ts b/server/lib/activitypub/send/send-reject.ts
index 4258a3c36..643c468a9 100644
--- a/server/lib/activitypub/send/send-reject.ts
+++ b/server/lib/activitypub/send/send-reject.ts
@@ -5,7 +5,7 @@ import { buildFollowActivity } from './send-follow'
5import { logger } from '../../../helpers/logger' 5import { logger } from '../../../helpers/logger'
6import { MActor } from '../../../typings/models' 6import { MActor } from '../../../typings/models'
7 7
8async function sendReject (follower: MActor, following: MActor) { 8function sendReject (follower: MActor, following: MActor) {
9 if (!follower.serverId) { // This should never happen 9 if (!follower.serverId) { // This should never happen
10 logger.warn('Do not sending reject to local follower.') 10 logger.warn('Do not sending reject to local follower.')
11 return 11 return
diff --git a/server/lib/activitypub/send/send-undo.ts b/server/lib/activitypub/send/send-undo.ts
index e9ab5b3c5..33f1d4921 100644
--- a/server/lib/activitypub/send/send-undo.ts
+++ b/server/lib/activitypub/send/send-undo.ts
@@ -28,7 +28,7 @@ import {
28 MVideoShare 28 MVideoShare
29} from '../../../typings/models' 29} from '../../../typings/models'
30 30
31async function sendUndoFollow (actorFollow: MActorFollowActors, t: Transaction) { 31function sendUndoFollow (actorFollow: MActorFollowActors, t: Transaction) {
32 const me = actorFollow.ActorFollower 32 const me = actorFollow.ActorFollower
33 const following = actorFollow.ActorFollowing 33 const following = actorFollow.ActorFollowing
34 34
@@ -118,10 +118,10 @@ function undoActivityData (
118} 118}
119 119
120async function sendUndoVideoRelatedActivity (options: { 120async function sendUndoVideoRelatedActivity (options: {
121 byActor: MActor, 121 byActor: MActor
122 video: MVideoAccountLight, 122 video: MVideoAccountLight
123 url: string, 123 url: string
124 activity: ActivityFollow | ActivityLike | ActivityDislike | ActivityCreate | ActivityAnnounce, 124 activity: ActivityFollow | ActivityLike | ActivityDislike | ActivityCreate | ActivityAnnounce
125 transaction: Transaction 125 transaction: Transaction
126}) { 126}) {
127 const activityBuilder = (audience: ActivityAudience) => { 127 const activityBuilder = (audience: ActivityAudience) => {
diff --git a/server/lib/activitypub/send/send-update.ts b/server/lib/activitypub/send/send-update.ts
index 9c76671b5..7a4cf3f56 100644
--- a/server/lib/activitypub/send/send-update.ts
+++ b/server/lib/activitypub/send/send-update.ts
@@ -8,9 +8,7 @@ import { getUpdateActivityPubUrl } from '../url'
8import { broadcastToFollowers, sendVideoRelatedActivity } from './utils' 8import { broadcastToFollowers, sendVideoRelatedActivity } from './utils'
9import { audiencify, getActorsInvolvedInVideo, getAudience } from '../audience' 9import { audiencify, getActorsInvolvedInVideo, getAudience } from '../audience'
10import { logger } from '../../../helpers/logger' 10import { logger } from '../../../helpers/logger'
11import { VideoCaptionModel } from '../../../models/video/video-caption'
12import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' 11import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
13import { getServerActor } from '../../../helpers/utils'
14import { 12import {
15 MAccountDefault, 13 MAccountDefault,
16 MActor, 14 MActor,
@@ -21,6 +19,7 @@ import {
21 MVideoPlaylistFull, 19 MVideoPlaylistFull,
22 MVideoRedundancyVideo 20 MVideoRedundancyVideo
23} from '../../../typings/models' 21} from '../../../typings/models'
22import { getServerActor } from '@server/models/application/application'
24 23
25async function sendUpdateVideo (videoArg: MVideoAPWithoutCaption, t: Transaction, overrodeByActor?: MActor) { 24async function sendUpdateVideo (videoArg: MVideoAPWithoutCaption, t: Transaction, overrodeByActor?: MActor) {
26 const video = videoArg as MVideoAP 25 const video = videoArg as MVideoAP
@@ -29,7 +28,7 @@ async function sendUpdateVideo (videoArg: MVideoAPWithoutCaption, t: Transaction
29 28
30 logger.info('Creating job to update video %s.', video.url) 29 logger.info('Creating job to update video %s.', video.url)
31 30
32 const byActor = overrodeByActor ? overrodeByActor : video.VideoChannel.Account.Actor 31 const byActor = overrodeByActor || video.VideoChannel.Account.Actor
33 32
34 const url = getUpdateActivityPubUrl(video.url, video.updatedAt.toISOString()) 33 const url = getUpdateActivityPubUrl(video.url, video.updatedAt.toISOString())
35 34
@@ -85,7 +84,7 @@ async function sendUpdateCacheFile (byActor: MActorLight, redundancyModel: MVide
85 return buildUpdateActivity(url, byActor, redundancyObject, audience) 84 return buildUpdateActivity(url, byActor, redundancyObject, audience)
86 } 85 }
87 86
88 return sendVideoRelatedActivity(activityBuilder, { byActor, video }) 87 return sendVideoRelatedActivity(activityBuilder, { byActor, video, contextType: 'CacheFile' })
89} 88}
90 89
91async function sendUpdateVideoPlaylist (videoPlaylist: MVideoPlaylistFull, t: Transaction) { 90async function sendUpdateVideoPlaylist (videoPlaylist: MVideoPlaylistFull, t: Transaction) {
diff --git a/server/lib/activitypub/send/send-view.ts b/server/lib/activitypub/send/send-view.ts
index 8809417f9..1f864ea52 100644
--- a/server/lib/activitypub/send/send-view.ts
+++ b/server/lib/activitypub/send/send-view.ts
@@ -5,9 +5,9 @@ import { getVideoLikeActivityPubUrl } from '../url'
5import { sendVideoRelatedActivity } from './utils' 5import { sendVideoRelatedActivity } from './utils'
6import { audiencify, getAudience } from '../audience' 6import { audiencify, getAudience } from '../audience'
7import { logger } from '../../../helpers/logger' 7import { logger } from '../../../helpers/logger'
8import { MActorAudience, MVideoAccountLight, MVideoUrl } from '@server/typings/models' 8import { MActorAudience, MVideoImmutable, MVideoUrl } from '@server/typings/models'
9 9
10async function sendView (byActor: ActorModel, video: MVideoAccountLight, t: Transaction) { 10async function sendView (byActor: ActorModel, video: MVideoImmutable, t: Transaction) {
11 logger.info('Creating job to send view of %s.', video.url) 11 logger.info('Creating job to send view of %s.', video.url)
12 12
13 const activityBuilder = (audience: ActivityAudience) => { 13 const activityBuilder = (audience: ActivityAudience) => {
@@ -16,7 +16,7 @@ async function sendView (byActor: ActorModel, video: MVideoAccountLight, t: Tran
16 return buildViewActivity(url, byActor, video, audience) 16 return buildViewActivity(url, byActor, video, audience)
17 } 17 }
18 18
19 return sendVideoRelatedActivity(activityBuilder, { byActor, video, transaction: t }) 19 return sendVideoRelatedActivity(activityBuilder, { byActor, video, transaction: t, contextType: 'View' })
20} 20}
21 21
22function buildViewActivity (url: string, byActor: MActorAudience, video: MVideoUrl, audience?: ActivityAudience): ActivityView { 22function buildViewActivity (url: string, byActor: MActorAudience, video: MVideoUrl, audience?: ActivityAudience): ActivityView {
diff --git a/server/lib/activitypub/send/utils.ts b/server/lib/activitypub/send/utils.ts
index 77b723479..44a8926e5 100644
--- a/server/lib/activitypub/send/utils.ts
+++ b/server/lib/activitypub/send/utils.ts
@@ -5,26 +5,30 @@ import { ActorModel } from '../../../models/activitypub/actor'
5import { ActorFollowModel } from '../../../models/activitypub/actor-follow' 5import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
6import { JobQueue } from '../../job-queue' 6import { JobQueue } from '../../job-queue'
7import { getActorsInvolvedInVideo, getAudienceFromFollowersOf, getRemoteVideoAudience } from '../audience' 7import { getActorsInvolvedInVideo, getAudienceFromFollowersOf, getRemoteVideoAudience } from '../audience'
8import { getServerActor } from '../../../helpers/utils'
9import { afterCommitIfTransaction } from '../../../helpers/database-utils' 8import { afterCommitIfTransaction } from '../../../helpers/database-utils'
10import { MActorWithInboxes, MActor, MActorId, MActorLight, MVideo, MVideoAccountLight } from '../../../typings/models' 9import { MActor, MActorId, MActorLight, MActorWithInboxes, MVideoAccountLight, MVideoId, MVideoImmutable } from '../../../typings/models'
10import { getServerActor } from '@server/models/application/application'
11import { ContextType } from '@shared/models/activitypub/context'
11 12
12async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAudience) => Activity, options: { 13async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAudience) => Activity, options: {
13 byActor: MActorLight, 14 byActor: MActorLight
14 video: MVideoAccountLight, 15 video: MVideoImmutable | MVideoAccountLight
15 transaction?: Transaction 16 transaction?: Transaction
17 contextType?: ContextType
16}) { 18}) {
17 const { byActor, video, transaction } = options 19 const { byActor, video, transaction, contextType } = options
18 20
19 const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, transaction) 21 const actorsInvolvedInVideo = await getActorsInvolvedInVideo(video, transaction)
20 22
21 // Send to origin 23 // Send to origin
22 if (video.isOwned() === false) { 24 if (video.isOwned() === false) {
23 const audience = getRemoteVideoAudience(video, actorsInvolvedInVideo) 25 const accountActor = (video as MVideoAccountLight).VideoChannel?.Account?.Actor || await ActorModel.loadAccountActorByVideoId(video.id)
26
27 const audience = getRemoteVideoAudience(accountActor, actorsInvolvedInVideo)
24 const activity = activityBuilder(audience) 28 const activity = activityBuilder(audience)
25 29
26 return afterCommitIfTransaction(transaction, () => { 30 return afterCommitIfTransaction(transaction, () => {
27 return unicastTo(activity, byActor, video.VideoChannel.Account.Actor.getSharedInbox()) 31 return unicastTo(activity, byActor, accountActor.getSharedInbox(), contextType)
28 }) 32 })
29 } 33 }
30 34
@@ -34,14 +38,14 @@ async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAud
34 38
35 const actorsException = [ byActor ] 39 const actorsException = [ byActor ]
36 40
37 return broadcastToFollowers(activity, byActor, actorsInvolvedInVideo, transaction, actorsException) 41 return broadcastToFollowers(activity, byActor, actorsInvolvedInVideo, transaction, actorsException, contextType)
38} 42}
39 43
40async function forwardVideoRelatedActivity ( 44async function forwardVideoRelatedActivity (
41 activity: Activity, 45 activity: Activity,
42 t: Transaction, 46 t: Transaction,
43 followersException: MActorWithInboxes[] = [], 47 followersException: MActorWithInboxes[],
44 video: MVideo 48 video: MVideoId
45) { 49) {
46 // Mastodon does not add our announces in audience, so we forward to them manually 50 // Mastodon does not add our announces in audience, so we forward to them manually
47 const additionalActors = await getActorsInvolvedInVideo(video, t) 51 const additionalActors = await getActorsInvolvedInVideo(video, t)
@@ -90,11 +94,12 @@ async function broadcastToFollowers (
90 byActor: MActorId, 94 byActor: MActorId,
91 toFollowersOf: MActorId[], 95 toFollowersOf: MActorId[],
92 t: Transaction, 96 t: Transaction,
93 actorsException: MActorWithInboxes[] = [] 97 actorsException: MActorWithInboxes[] = [],
98 contextType?: ContextType
94) { 99) {
95 const uris = await computeFollowerUris(toFollowersOf, actorsException, t) 100 const uris = await computeFollowerUris(toFollowersOf, actorsException, t)
96 101
97 return afterCommitIfTransaction(t, () => broadcastTo(uris, data, byActor)) 102 return afterCommitIfTransaction(t, () => broadcastTo(uris, data, byActor, contextType))
98} 103}
99 104
100async function broadcastToActors ( 105async function broadcastToActors (
@@ -102,13 +107,14 @@ async function broadcastToActors (
102 byActor: MActorId, 107 byActor: MActorId,
103 toActors: MActor[], 108 toActors: MActor[],
104 t?: Transaction, 109 t?: Transaction,
105 actorsException: MActorWithInboxes[] = [] 110 actorsException: MActorWithInboxes[] = [],
111 contextType?: ContextType
106) { 112) {
107 const uris = await computeUris(toActors, actorsException) 113 const uris = await computeUris(toActors, actorsException)
108 return afterCommitIfTransaction(t, () => broadcastTo(uris, data, byActor)) 114 return afterCommitIfTransaction(t, () => broadcastTo(uris, data, byActor, contextType))
109} 115}
110 116
111function broadcastTo (uris: string[], data: any, byActor: MActorId) { 117function broadcastTo (uris: string[], data: any, byActor: MActorId, contextType?: ContextType) {
112 if (uris.length === 0) return undefined 118 if (uris.length === 0) return undefined
113 119
114 logger.debug('Creating broadcast job.', { uris }) 120 logger.debug('Creating broadcast job.', { uris })
@@ -116,19 +122,21 @@ function broadcastTo (uris: string[], data: any, byActor: MActorId) {
116 const payload = { 122 const payload = {
117 uris, 123 uris,
118 signatureActorId: byActor.id, 124 signatureActorId: byActor.id,
119 body: data 125 body: data,
126 contextType
120 } 127 }
121 128
122 return JobQueue.Instance.createJob({ type: 'activitypub-http-broadcast', payload }) 129 return JobQueue.Instance.createJob({ type: 'activitypub-http-broadcast', payload })
123} 130}
124 131
125function unicastTo (data: any, byActor: MActorId, toActorUrl: string) { 132function unicastTo (data: any, byActor: MActorId, toActorUrl: string, contextType?: ContextType) {
126 logger.debug('Creating unicast job.', { uri: toActorUrl }) 133 logger.debug('Creating unicast job.', { uri: toActorUrl })
127 134
128 const payload = { 135 const payload = {
129 uri: toActorUrl, 136 uri: toActorUrl,
130 signatureActorId: byActor.id, 137 signatureActorId: byActor.id,
131 body: data 138 body: data,
139 contextType
132 } 140 }
133 141
134 JobQueue.Instance.createJob({ type: 'activitypub-http-unicast', payload }) 142 JobQueue.Instance.createJob({ type: 'activitypub-http-unicast', payload })
@@ -153,7 +161,7 @@ async function computeFollowerUris (toFollowersOf: MActorId[], actorsException:
153 const result = await ActorFollowModel.listAcceptedFollowerSharedInboxUrls(toActorFollowerIds, t) 161 const result = await ActorFollowModel.listAcceptedFollowerSharedInboxUrls(toActorFollowerIds, t)
154 const sharedInboxesException = await buildSharedInboxesException(actorsException) 162 const sharedInboxesException = await buildSharedInboxesException(actorsException)
155 163
156 return result.data.filter(sharedInbox => sharedInboxesException.indexOf(sharedInbox) === -1) 164 return result.data.filter(sharedInbox => sharedInboxesException.includes(sharedInbox) === false)
157} 165}
158 166
159async function computeUris (toActors: MActor[], actorsException: MActorWithInboxes[] = []) { 167async function computeUris (toActors: MActor[], actorsException: MActorWithInboxes[] = []) {
@@ -166,7 +174,7 @@ async function computeUris (toActors: MActor[], actorsException: MActorWithInbox
166 174
167 const sharedInboxesException = await buildSharedInboxesException(actorsException) 175 const sharedInboxesException = await buildSharedInboxesException(actorsException)
168 return Array.from(toActorSharedInboxesSet) 176 return Array.from(toActorSharedInboxesSet)
169 .filter(sharedInbox => sharedInboxesException.indexOf(sharedInbox) === -1) 177 .filter(sharedInbox => sharedInboxesException.includes(sharedInbox) === false)
170} 178}
171 179
172async function buildSharedInboxesException (actorsException: MActorWithInboxes[]) { 180async function buildSharedInboxesException (actorsException: MActorWithInboxes[]) {
diff --git a/server/lib/activitypub/share.ts b/server/lib/activitypub/share.ts
index e847c4b7d..d2cbc59a8 100644
--- a/server/lib/activitypub/share.ts
+++ b/server/lib/activitypub/share.ts
@@ -1,5 +1,4 @@
1import { Transaction } from 'sequelize' 1import { Transaction } from 'sequelize'
2import { getServerActor } from '../../helpers/utils'
3import { VideoShareModel } from '../../models/video/video-share' 2import { VideoShareModel } from '../../models/video/video-share'
4import { sendUndoAnnounce, sendVideoAnnounce } from './send' 3import { sendUndoAnnounce, sendVideoAnnounce } from './send'
5import { getVideoAnnounceActivityPubUrl } from './url' 4import { getVideoAnnounceActivityPubUrl } from './url'
@@ -10,6 +9,7 @@ import { logger } from '../../helpers/logger'
10import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' 9import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
11import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' 10import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
12import { MChannelActorLight, MVideo, MVideoAccountLight, MVideoId } from '../../typings/models/video' 11import { MChannelActorLight, MVideo, MVideoAccountLight, MVideoId } from '../../typings/models/video'
12import { getServerActor } from '@server/models/application/application'
13 13
14async function shareVideoByServerAndChannel (video: MVideoAccountLight, t: Transaction) { 14async function shareVideoByServerAndChannel (video: MVideoAccountLight, t: Transaction) {
15 if (!video.hasPrivacyForFederation()) return undefined 15 if (!video.hasPrivacyForFederation()) return undefined
@@ -36,7 +36,7 @@ async function addVideoShares (shareUrls: string[], video: MVideoId) {
36 await Bluebird.map(shareUrls, async shareUrl => { 36 await Bluebird.map(shareUrls, async shareUrl => {
37 try { 37 try {
38 // Fetch url 38 // Fetch url
39 const { body } = await doRequest({ 39 const { body } = await doRequest<any>({
40 uri: shareUrl, 40 uri: shareUrl,
41 json: true, 41 json: true,
42 activityPub: true 42 activityPub: true
diff --git a/server/lib/activitypub/video-comments.ts b/server/lib/activitypub/video-comments.ts
index d5c078a29..3aee6799e 100644
--- a/server/lib/activitypub/video-comments.ts
+++ b/server/lib/activitypub/video-comments.ts
@@ -10,9 +10,9 @@ import { checkUrlsSameHost } from '../../helpers/activitypub'
10import { MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../typings/models/video' 10import { MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../typings/models/video'
11 11
12type ResolveThreadParams = { 12type ResolveThreadParams = {
13 url: string, 13 url: string
14 comments?: MCommentOwner[], 14 comments?: MCommentOwner[]
15 isVideo?: boolean, 15 isVideo?: boolean
16 commentCreated?: boolean 16 commentCreated?: boolean
17} 17}
18type ResolveThreadResult = Promise<{ video: MVideoAccountLightBlacklistAllFiles, comment: MCommentOwnerVideo, commentCreated: boolean }> 18type ResolveThreadResult = Promise<{ video: MVideoAccountLightBlacklistAllFiles, comment: MCommentOwnerVideo, commentCreated: boolean }>
@@ -28,7 +28,7 @@ async function resolveThread (params: ResolveThreadParams): ResolveThreadResult
28 if (params.commentCreated === undefined) params.commentCreated = false 28 if (params.commentCreated === undefined) params.commentCreated = false
29 if (params.comments === undefined) params.comments = [] 29 if (params.comments === undefined) params.comments = []
30 30
31 // Already have this comment? 31 // Already have this comment?
32 if (isVideo !== true) { 32 if (isVideo !== true) {
33 const result = await resolveCommentFromDB(params) 33 const result = await resolveCommentFromDB(params)
34 if (result) return result 34 if (result) return result
@@ -87,7 +87,7 @@ async function tryResolveThreadFromVideo (params: ResolveThreadParams) {
87 87
88 let resultComment: MCommentOwnerVideo 88 let resultComment: MCommentOwnerVideo
89 if (comments.length !== 0) { 89 if (comments.length !== 0) {
90 const firstReply = comments[ comments.length - 1 ] as MCommentOwnerVideo 90 const firstReply = comments[comments.length - 1] as MCommentOwnerVideo
91 firstReply.inReplyToCommentId = null 91 firstReply.inReplyToCommentId = null
92 firstReply.originCommentId = null 92 firstReply.originCommentId = null
93 firstReply.videoId = video.id 93 firstReply.videoId = video.id
@@ -97,9 +97,9 @@ async function tryResolveThreadFromVideo (params: ResolveThreadParams) {
97 comments[comments.length - 1] = await firstReply.save() 97 comments[comments.length - 1] = await firstReply.save()
98 98
99 for (let i = comments.length - 2; i >= 0; i--) { 99 for (let i = comments.length - 2; i >= 0; i--) {
100 const comment = comments[ i ] as MCommentOwnerVideo 100 const comment = comments[i] as MCommentOwnerVideo
101 comment.originCommentId = firstReply.id 101 comment.originCommentId = firstReply.id
102 comment.inReplyToCommentId = comments[ i + 1 ].id 102 comment.inReplyToCommentId = comments[i + 1].id
103 comment.videoId = video.id 103 comment.videoId = video.id
104 comment.changed('updatedAt', true) 104 comment.changed('updatedAt', true)
105 comment.Video = video 105 comment.Video = video
@@ -120,7 +120,7 @@ async function resolveParentComment (params: ResolveThreadParams) {
120 throw new Error('Recursion limit reached when resolving a thread') 120 throw new Error('Recursion limit reached when resolving a thread')
121 } 121 }
122 122
123 const { body } = await doRequest({ 123 const { body } = await doRequest<any>({
124 uri: url, 124 uri: url,
125 json: true, 125 json: true,
126 activityPub: true 126 activityPub: true
diff --git a/server/lib/activitypub/video-rates.ts b/server/lib/activitypub/video-rates.ts
index 6bd46bb58..202368c8f 100644
--- a/server/lib/activitypub/video-rates.ts
+++ b/server/lib/activitypub/video-rates.ts
@@ -18,7 +18,7 @@ async function createRates (ratesUrl: string[], video: MVideo, rate: VideoRateTy
18 await Bluebird.map(ratesUrl, async rateUrl => { 18 await Bluebird.map(ratesUrl, async rateUrl => {
19 try { 19 try {
20 // Fetch url 20 // Fetch url
21 const { body } = await doRequest({ 21 const { body } = await doRequest<any>({
22 uri: rateUrl, 22 uri: rateUrl,
23 json: true, 23 json: true,
24 activityPub: true 24 activityPub: true
@@ -58,8 +58,6 @@ async function createRates (ratesUrl: string[], video: MVideo, rate: VideoRateTy
58 const field = rate === 'like' ? 'likes' : 'dislikes' 58 const field = rate === 'like' ? 'likes' : 'dislikes'
59 await video.increment(field, { by: rateCounts }) 59 await video.increment(field, { by: rateCounts })
60 } 60 }
61
62 return
63} 61}
64 62
65async function sendVideoRateChange ( 63async function sendVideoRateChange (
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index ade93150f..7d16bd390 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -6,25 +6,27 @@ import {
6 ActivityHashTagObject, 6 ActivityHashTagObject,
7 ActivityMagnetUrlObject, 7 ActivityMagnetUrlObject,
8 ActivityPlaylistSegmentHashesObject, 8 ActivityPlaylistSegmentHashesObject,
9 ActivityPlaylistUrlObject, ActivityTagObject, 9 ActivityPlaylistUrlObject, ActivitypubHttpFetcherPayload,
10 ActivityTagObject,
10 ActivityUrlObject, 11 ActivityUrlObject,
11 ActivityVideoUrlObject, 12 ActivityVideoUrlObject,
12 VideoState 13 VideoState
13} from '../../../shared/index' 14} from '../../../shared/index'
14import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' 15import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
15import { VideoPrivacy } from '../../../shared/models/videos' 16import { VideoPrivacy } from '../../../shared/models/videos'
16import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos' 17import { isAPVideoFileMetadataObject, sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos'
17import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' 18import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
18import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' 19import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils'
19import { logger } from '../../helpers/logger' 20import { logger } from '../../helpers/logger'
20import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests' 21import { doRequest } from '../../helpers/requests'
21import { 22import {
22 ACTIVITY_PUB, 23 ACTIVITY_PUB,
23 MIMETYPES, 24 MIMETYPES,
24 P2P_MEDIA_LOADER_PEER_VERSION, 25 P2P_MEDIA_LOADER_PEER_VERSION,
25 PREVIEWS_SIZE, 26 PREVIEWS_SIZE,
26 REMOTE_SCHEME, 27 REMOTE_SCHEME,
27 STATIC_PATHS 28 STATIC_PATHS,
29 THUMBNAILS_SIZE
28} from '../../initializers/constants' 30} from '../../initializers/constants'
29import { TagModel } from '../../models/video/tag' 31import { TagModel } from '../../models/video/tag'
30import { VideoModel } from '../../models/video/video' 32import { VideoModel } from '../../models/video/video'
@@ -36,11 +38,10 @@ import { sendCreateVideo, sendUpdateVideo } from './send'
36import { isArray } from '../../helpers/custom-validators/misc' 38import { isArray } from '../../helpers/custom-validators/misc'
37import { VideoCaptionModel } from '../../models/video/video-caption' 39import { VideoCaptionModel } from '../../models/video/video-caption'
38import { JobQueue } from '../job-queue' 40import { JobQueue } from '../job-queue'
39import { ActivitypubHttpFetcherPayload } from '../job-queue/handlers/activitypub-http-fetcher'
40import { createRates } from './video-rates' 41import { createRates } from './video-rates'
41import { addVideoShares, shareVideoByServerAndChannel } from './share' 42import { addVideoShares, shareVideoByServerAndChannel } from './share'
42import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video' 43import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video'
43import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' 44import { buildRemoteVideoBaseUrl, checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
44import { Notifier } from '../notifier' 45import { Notifier } from '../notifier'
45import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist' 46import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist'
46import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' 47import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
@@ -68,9 +69,11 @@ import {
68 MVideoFile, 69 MVideoFile,
69 MVideoFullLight, 70 MVideoFullLight,
70 MVideoId, 71 MVideoId,
72 MVideoImmutable,
71 MVideoThumbnail 73 MVideoThumbnail
72} from '../../typings/models' 74} from '../../typings/models'
73import { MThumbnail } from '../../typings/models/video/thumbnail' 75import { MThumbnail } from '../../typings/models/video/thumbnail'
76import { maxBy, minBy } from 'lodash'
74 77
75async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVideo: boolean, transaction?: sequelize.Transaction) { 78async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVideo: boolean, transaction?: sequelize.Transaction) {
76 const video = videoArg as MVideoAP 79 const video = videoArg as MVideoAP
@@ -109,7 +112,7 @@ async function fetchRemoteVideo (videoUrl: string): Promise<{ response: request.
109 112
110 logger.info('Fetching remote video %s.', videoUrl) 113 logger.info('Fetching remote video %s.', videoUrl)
111 114
112 const { response, body } = await doRequest(options) 115 const { response, body } = await doRequest<any>(options)
113 116
114 if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) { 117 if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) {
115 logger.debug('Remote video JSON is not valid.', { body }) 118 logger.debug('Remote video JSON is not valid.', { body })
@@ -127,23 +130,10 @@ async function fetchRemoteVideoDescription (video: MVideoAccountLight) {
127 json: true 130 json: true
128 } 131 }
129 132
130 const { body } = await doRequest(options) 133 const { body } = await doRequest<any>(options)
131 return body.description ? body.description : '' 134 return body.description ? body.description : ''
132} 135}
133 136
134function fetchRemoteVideoStaticFile (video: MVideoAccountLight, path: string, destPath: string) {
135 const url = buildRemoteBaseUrl(video, path)
136
137 // We need to provide a callback, if no we could have an uncaught exception
138 return doRequestAndSaveToFile({ uri: url }, destPath)
139}
140
141function buildRemoteBaseUrl (video: MVideoAccountLight, path: string) {
142 const host = video.VideoChannel.Account.Actor.Server.host
143
144 return REMOTE_SCHEME.HTTP + '://' + host + path
145}
146
147function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) { 137function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) {
148 const channel = videoObject.attributedTo.find(a => a.type === 'Group') 138 const channel = videoObject.attributedTo.find(a => a.type === 'Group')
149 if (!channel) throw new Error('Cannot find associated video channel to video ' + videoObject.url) 139 if (!channel) throw new Error('Cannot find associated video channel to video ' + videoObject.url)
@@ -173,7 +163,7 @@ async function syncVideoExternalAttributes (video: MVideo, fetchedVideo: VideoTo
173 const cleaner = crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'like' as 'like', crawlStartDate) 163 const cleaner = crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'like' as 'like', crawlStartDate)
174 164
175 await crawlCollectionPage<string>(fetchedVideo.likes, handler, cleaner) 165 await crawlCollectionPage<string>(fetchedVideo.likes, handler, cleaner)
176 .catch(err => logger.error('Cannot add likes of video %s.', video.uuid, { err })) 166 .catch(err => logger.error('Cannot add likes of video %s.', video.uuid, { err, rootUrl: fetchedVideo.likes }))
177 } else { 167 } else {
178 jobPayloads.push({ uri: fetchedVideo.likes, videoId: video.id, type: 'video-likes' as 'video-likes' }) 168 jobPayloads.push({ uri: fetchedVideo.likes, videoId: video.id, type: 'video-likes' as 'video-likes' })
179 } 169 }
@@ -183,7 +173,7 @@ async function syncVideoExternalAttributes (video: MVideo, fetchedVideo: VideoTo
183 const cleaner = crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'dislike' as 'dislike', crawlStartDate) 173 const cleaner = crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'dislike' as 'dislike', crawlStartDate)
184 174
185 await crawlCollectionPage<string>(fetchedVideo.dislikes, handler, cleaner) 175 await crawlCollectionPage<string>(fetchedVideo.dislikes, handler, cleaner)
186 .catch(err => logger.error('Cannot add dislikes of video %s.', video.uuid, { err })) 176 .catch(err => logger.error('Cannot add dislikes of video %s.', video.uuid, { err, rootUrl: fetchedVideo.dislikes }))
187 } else { 177 } else {
188 jobPayloads.push({ uri: fetchedVideo.dislikes, videoId: video.id, type: 'video-dislikes' as 'video-dislikes' }) 178 jobPayloads.push({ uri: fetchedVideo.dislikes, videoId: video.id, type: 'video-dislikes' as 'video-dislikes' })
189 } 179 }
@@ -193,7 +183,7 @@ async function syncVideoExternalAttributes (video: MVideo, fetchedVideo: VideoTo
193 const cleaner = crawlStartDate => VideoShareModel.cleanOldSharesOf(video.id, crawlStartDate) 183 const cleaner = crawlStartDate => VideoShareModel.cleanOldSharesOf(video.id, crawlStartDate)
194 184
195 await crawlCollectionPage<string>(fetchedVideo.shares, handler, cleaner) 185 await crawlCollectionPage<string>(fetchedVideo.shares, handler, cleaner)
196 .catch(err => logger.error('Cannot add shares of video %s.', video.uuid, { err })) 186 .catch(err => logger.error('Cannot add shares of video %s.', video.uuid, { err, rootUrl: fetchedVideo.shares }))
197 } else { 187 } else {
198 jobPayloads.push({ uri: fetchedVideo.shares, videoId: video.id, type: 'video-shares' as 'video-shares' }) 188 jobPayloads.push({ uri: fetchedVideo.shares, videoId: video.id, type: 'video-shares' as 'video-shares' })
199 } 189 }
@@ -203,32 +193,49 @@ async function syncVideoExternalAttributes (video: MVideo, fetchedVideo: VideoTo
203 const cleaner = crawlStartDate => VideoCommentModel.cleanOldCommentsOf(video.id, crawlStartDate) 193 const cleaner = crawlStartDate => VideoCommentModel.cleanOldCommentsOf(video.id, crawlStartDate)
204 194
205 await crawlCollectionPage<string>(fetchedVideo.comments, handler, cleaner) 195 await crawlCollectionPage<string>(fetchedVideo.comments, handler, cleaner)
206 .catch(err => logger.error('Cannot add comments of video %s.', video.uuid, { err })) 196 .catch(err => logger.error('Cannot add comments of video %s.', video.uuid, { err, rootUrl: fetchedVideo.comments }))
207 } else { 197 } else {
208 jobPayloads.push({ uri: fetchedVideo.comments, videoId: video.id, type: 'video-comments' as 'video-comments' }) 198 jobPayloads.push({ uri: fetchedVideo.comments, videoId: video.id, type: 'video-comments' as 'video-comments' })
209 } 199 }
210 200
211 await Bluebird.map(jobPayloads, payload => JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })) 201 await Bluebird.map(jobPayloads, payload => JobQueue.Instance.createJobWithPromise({ type: 'activitypub-http-fetcher', payload }))
212} 202}
213 203
214function getOrCreateVideoAndAccountAndChannel (options: { 204type GetVideoResult <T> = Promise<{
215 videoObject: { id: string } | string, 205 video: T
216 syncParam?: SyncParam, 206 created: boolean
217 fetchType?: 'all', 207 autoBlacklisted?: boolean
208}>
209
210type GetVideoParamAll = {
211 videoObject: { id: string } | string
212 syncParam?: SyncParam
213 fetchType?: 'all'
218 allowRefresh?: boolean 214 allowRefresh?: boolean
219}): Promise<{ video: MVideoAccountLightBlacklistAllFiles, created: boolean, autoBlacklisted?: boolean }> 215}
220function getOrCreateVideoAndAccountAndChannel (options: { 216
221 videoObject: { id: string } | string, 217type GetVideoParamImmutable = {
222 syncParam?: SyncParam, 218 videoObject: { id: string } | string
223 fetchType?: VideoFetchByUrlType, 219 syncParam?: SyncParam
220 fetchType: 'only-immutable-attributes'
221 allowRefresh: false
222}
223
224type GetVideoParamOther = {
225 videoObject: { id: string } | string
226 syncParam?: SyncParam
227 fetchType?: 'all' | 'only-video'
224 allowRefresh?: boolean 228 allowRefresh?: boolean
225}): Promise<{ video: MVideoAccountLightBlacklistAllFiles | MVideoThumbnail, created: boolean, autoBlacklisted?: boolean }> 229}
226async function getOrCreateVideoAndAccountAndChannel (options: { 230
227 videoObject: { id: string } | string, 231function getOrCreateVideoAndAccountAndChannel (options: GetVideoParamAll): GetVideoResult<MVideoAccountLightBlacklistAllFiles>
228 syncParam?: SyncParam, 232function getOrCreateVideoAndAccountAndChannel (options: GetVideoParamImmutable): GetVideoResult<MVideoImmutable>
229 fetchType?: VideoFetchByUrlType, 233function getOrCreateVideoAndAccountAndChannel (
230 allowRefresh?: boolean // true by default 234 options: GetVideoParamOther
231}): Promise<{ video: MVideoAccountLightBlacklistAllFiles | MVideoThumbnail, created: boolean, autoBlacklisted?: boolean }> { 235): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail>
236async function getOrCreateVideoAndAccountAndChannel (
237 options: GetVideoParamAll | GetVideoParamImmutable | GetVideoParamOther
238): GetVideoResult<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail | MVideoImmutable> {
232 // Default params 239 // Default params
233 const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false } 240 const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false }
234 const fetchType = options.fetchType || 'all' 241 const fetchType = options.fetchType || 'all'
@@ -236,18 +243,25 @@ async function getOrCreateVideoAndAccountAndChannel (options: {
236 243
237 // Get video url 244 // Get video url
238 const videoUrl = getAPId(options.videoObject) 245 const videoUrl = getAPId(options.videoObject)
239
240 let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType) 246 let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType)
247
241 if (videoFromDatabase) { 248 if (videoFromDatabase) {
242 if (videoFromDatabase.isOutdated() && allowRefresh === true) { 249 // If allowRefresh is true, we could not call this function using 'only-immutable-attributes' fetch type
250 if (allowRefresh === true && (videoFromDatabase as MVideoThumbnail).isOutdated()) {
243 const refreshOptions = { 251 const refreshOptions = {
244 video: videoFromDatabase, 252 video: videoFromDatabase as MVideoThumbnail,
245 fetchedType: fetchType, 253 fetchedType: fetchType,
246 syncParam 254 syncParam
247 } 255 }
248 256
249 if (syncParam.refreshVideo === true) videoFromDatabase = await refreshVideoIfNeeded(refreshOptions) 257 if (syncParam.refreshVideo === true) {
250 else await JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', url: videoFromDatabase.url } }) 258 videoFromDatabase = await refreshVideoIfNeeded(refreshOptions)
259 } else {
260 await JobQueue.Instance.createJobWithPromise({
261 type: 'activitypub-refresher',
262 payload: { type: 'video', url: videoFromDatabase.url }
263 })
264 }
251 } 265 }
252 266
253 return { video: videoFromDatabase, created: false } 267 return { video: videoFromDatabase, created: false }
@@ -266,10 +280,10 @@ async function getOrCreateVideoAndAccountAndChannel (options: {
266} 280}
267 281
268async function updateVideoFromAP (options: { 282async function updateVideoFromAP (options: {
269 video: MVideoAccountLightBlacklistAllFiles, 283 video: MVideoAccountLightBlacklistAllFiles
270 videoObject: VideoTorrentObject, 284 videoObject: VideoTorrentObject
271 account: MAccountIdActor, 285 account: MAccountIdActor
272 channel: MChannelDefault, 286 channel: MChannelDefault
273 overrideTo?: string[] 287 overrideTo?: string[]
274}) { 288}) {
275 const { video, videoObject, account, channel, overrideTo } = options 289 const { video, videoObject, account, channel, overrideTo } = options
@@ -284,7 +298,7 @@ async function updateVideoFromAP (options: {
284 let thumbnailModel: MThumbnail 298 let thumbnailModel: MThumbnail
285 299
286 try { 300 try {
287 thumbnailModel = await createVideoMiniatureFromUrl(videoObject.icon.url, video, ThumbnailType.MINIATURE) 301 thumbnailModel = await createVideoMiniatureFromUrl(getThumbnailFromIcons(videoObject).url, video, ThumbnailType.MINIATURE)
288 } catch (err) { 302 } catch (err) {
289 logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err }) 303 logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err })
290 } 304 }
@@ -300,7 +314,7 @@ async function updateVideoFromAP (options: {
300 throw new Error('Account ' + account.Actor.url + ' does not own video channel ' + videoChannel.Actor.url) 314 throw new Error('Account ' + account.Actor.url + ' does not own video channel ' + videoChannel.Actor.url)
301 } 315 }
302 316
303 const to = overrideTo ? overrideTo : videoObject.to 317 const to = overrideTo || videoObject.to
304 const videoData = await videoActivityObjectToDBAttributes(channel, videoObject, to) 318 const videoData = await videoActivityObjectToDBAttributes(channel, videoObject, to)
305 video.name = videoData.name 319 video.name = videoData.name
306 video.uuid = videoData.uuid 320 video.uuid = videoData.uuid
@@ -327,10 +341,11 @@ async function updateVideoFromAP (options: {
327 341
328 if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel, t) 342 if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel, t)
329 343
330 // FIXME: use icon URL instead 344 if (videoUpdated.getPreview()) {
331 const previewUrl = buildRemoteBaseUrl(videoUpdated, join(STATIC_PATHS.PREVIEWS, videoUpdated.getPreview().filename)) 345 const previewUrl = videoUpdated.getPreview().getFileUrl(videoUpdated)
332 const previewModel = createPlaceholderThumbnail(previewUrl, video, ThumbnailType.PREVIEW, PREVIEWS_SIZE) 346 const previewModel = createPlaceholderThumbnail(previewUrl, video, ThumbnailType.PREVIEW, PREVIEWS_SIZE)
333 await videoUpdated.addAndSaveThumbnail(previewModel, t) 347 await videoUpdated.addAndSaveThumbnail(previewModel, t)
348 }
334 349
335 { 350 {
336 const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoUpdated, videoObject.url) 351 const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoUpdated, videoObject.url)
@@ -391,7 +406,7 @@ async function updateVideoFromAP (options: {
391 await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(videoUpdated.id, t) 406 await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(videoUpdated.id, t)
392 407
393 const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => { 408 const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => {
394 return VideoCaptionModel.insertOrReplaceLanguage(videoUpdated.id, c.identifier, t) 409 return VideoCaptionModel.insertOrReplaceLanguage(videoUpdated.id, c.identifier, c.url, t)
395 }) 410 })
396 await Promise.all(videoCaptionsPromises) 411 await Promise.all(videoCaptionsPromises)
397 } 412 }
@@ -424,8 +439,8 @@ async function updateVideoFromAP (options: {
424} 439}
425 440
426async function refreshVideoIfNeeded (options: { 441async function refreshVideoIfNeeded (options: {
427 video: MVideoThumbnail, 442 video: MVideoThumbnail
428 fetchedType: VideoFetchByUrlType, 443 fetchedType: VideoFetchByUrlType
429 syncParam: SyncParam 444 syncParam: SyncParam
430}): Promise<MVideoThumbnail> { 445}): Promise<MVideoThumbnail> {
431 if (!options.video.isOutdated()) return options.video 446 if (!options.video.isOutdated()) return options.video
@@ -483,7 +498,6 @@ export {
483 federateVideoIfNeeded, 498 federateVideoIfNeeded,
484 fetchRemoteVideo, 499 fetchRemoteVideo,
485 getOrCreateVideoAndAccountAndChannel, 500 getOrCreateVideoAndAccountAndChannel,
486 fetchRemoteVideoStaticFile,
487 fetchRemoteVideoDescription, 501 fetchRemoteVideoDescription,
488 getOrCreateVideoChannelFromVideoObject 502 getOrCreateVideoChannelFromVideoObject
489} 503}
@@ -494,7 +508,7 @@ function isAPVideoUrlObject (url: any): url is ActivityVideoUrlObject {
494 const mimeTypes = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT) 508 const mimeTypes = Object.keys(MIMETYPES.VIDEO.MIMETYPE_EXT)
495 509
496 const urlMediaType = url.mediaType 510 const urlMediaType = url.mediaType
497 return mimeTypes.indexOf(urlMediaType) !== -1 && urlMediaType.startsWith('video/') 511 return mimeTypes.includes(urlMediaType) && urlMediaType.startsWith('video/')
498} 512}
499 513
500function isAPStreamingPlaylistUrlObject (url: ActivityUrlObject): url is ActivityPlaylistUrlObject { 514function isAPStreamingPlaylistUrlObject (url: ActivityUrlObject): url is ActivityPlaylistUrlObject {
@@ -519,7 +533,11 @@ async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAc
519 const videoData = await videoActivityObjectToDBAttributes(channel, videoObject, videoObject.to) 533 const videoData = await videoActivityObjectToDBAttributes(channel, videoObject, videoObject.to)
520 const video = VideoModel.build(videoData) as MVideoThumbnail 534 const video = VideoModel.build(videoData) as MVideoThumbnail
521 535
522 const promiseThumbnail = createVideoMiniatureFromUrl(videoObject.icon.url, video, ThumbnailType.MINIATURE) 536 const promiseThumbnail = createVideoMiniatureFromUrl(getThumbnailFromIcons(videoObject).url, video, ThumbnailType.MINIATURE)
537 .catch(err => {
538 logger.error('Cannot create miniature from url.', { err })
539 return undefined
540 })
523 541
524 let thumbnailModel: MThumbnail 542 let thumbnailModel: MThumbnail
525 if (waitThumbnail === true) { 543 if (waitThumbnail === true) {
@@ -534,9 +552,12 @@ async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAc
534 552
535 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t) 553 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
536 554
537 // FIXME: use icon URL instead 555 const previewIcon = getPreviewFromIcons(videoObject)
538 const previewUrl = buildRemoteBaseUrl(videoCreated, join(STATIC_PATHS.PREVIEWS, video.generatePreviewName())) 556 const previewUrl = previewIcon
539 const previewModel = createPlaceholderThumbnail(previewUrl, video, ThumbnailType.PREVIEW, PREVIEWS_SIZE) 557 ? previewIcon.url
558 : buildRemoteVideoBaseUrl(videoCreated, join(STATIC_PATHS.PREVIEWS, video.generatePreviewName()))
559 const previewModel = createPlaceholderThumbnail(previewUrl, videoCreated, ThumbnailType.PREVIEW, PREVIEWS_SIZE)
560
540 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(previewModel, t) 561 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(previewModel, t)
541 562
542 // Process files 563 // Process files
@@ -567,7 +588,7 @@ async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAc
567 588
568 // Process captions 589 // Process captions
569 const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => { 590 const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => {
570 return VideoCaptionModel.insertOrReplaceLanguage(videoCreated.id, c.identifier, t) 591 return VideoCaptionModel.insertOrReplaceLanguage(videoCreated.id, c.identifier, c.url, t)
571 }) 592 })
572 await Promise.all(videoCaptionsPromises) 593 await Promise.all(videoCaptionsPromises)
573 594
@@ -588,7 +609,11 @@ async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAc
588 }) 609 })
589 610
590 if (waitThumbnail === false) { 611 if (waitThumbnail === false) {
612 // Error is already caught above
613 // eslint-disable-next-line @typescript-eslint/no-floating-promises
591 promiseThumbnail.then(thumbnailModel => { 614 promiseThumbnail.then(thumbnailModel => {
615 if (!thumbnailModel) return
616
592 thumbnailModel = videoCreated.id 617 thumbnailModel = videoCreated.id
593 618
594 return thumbnailModel.save() 619 return thumbnailModel.save()
@@ -598,24 +623,21 @@ async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAc
598 return { autoBlacklisted, videoCreated } 623 return { autoBlacklisted, videoCreated }
599} 624}
600 625
601async function videoActivityObjectToDBAttributes (videoChannel: MChannelId, videoObject: VideoTorrentObject, to: string[] = []) { 626function videoActivityObjectToDBAttributes (videoChannel: MChannelId, videoObject: VideoTorrentObject, to: string[] = []) {
602 const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPrivacy.PUBLIC : VideoPrivacy.UNLISTED 627 const privacy = to.includes(ACTIVITY_PUB.PUBLIC)
603 const duration = videoObject.duration.replace(/[^\d]+/, '') 628 ? VideoPrivacy.PUBLIC
629 : VideoPrivacy.UNLISTED
604 630
605 let language: string | undefined 631 const duration = videoObject.duration.replace(/[^\d]+/, '')
606 if (videoObject.language) { 632 const language = videoObject.language?.identifier
607 language = videoObject.language.identifier
608 }
609 633
610 let category: number | undefined 634 const category = videoObject.category
611 if (videoObject.category) { 635 ? parseInt(videoObject.category.identifier, 10)
612 category = parseInt(videoObject.category.identifier, 10) 636 : undefined
613 }
614 637
615 let licence: number | undefined 638 const licence = videoObject.licence
616 if (videoObject.licence) { 639 ? parseInt(videoObject.licence.identifier, 10)
617 licence = parseInt(videoObject.licence.identifier, 10) 640 : undefined
618 }
619 641
620 const description = videoObject.content || null 642 const description = videoObject.content || null
621 const support = videoObject.support || null 643 const support = videoObject.support || null
@@ -638,8 +660,11 @@ async function videoActivityObjectToDBAttributes (videoChannel: MChannelId, vide
638 duration: parseInt(duration, 10), 660 duration: parseInt(duration, 10),
639 createdAt: new Date(videoObject.published), 661 createdAt: new Date(videoObject.published),
640 publishedAt: new Date(videoObject.published), 662 publishedAt: new Date(videoObject.published),
641 originallyPublishedAt: videoObject.originallyPublishedAt ? new Date(videoObject.originallyPublishedAt) : null, 663
642 // FIXME: updatedAt does not seems to be considered by Sequelize 664 originallyPublishedAt: videoObject.originallyPublishedAt
665 ? new Date(videoObject.originallyPublishedAt)
666 : null,
667
643 updatedAt: new Date(videoObject.updated), 668 updatedAt: new Date(videoObject.updated),
644 views: videoObject.views, 669 views: videoObject.views,
645 likes: 0, 670 likes: 0,
@@ -670,13 +695,22 @@ function videoFileActivityUrlToDBAttributes (
670 throw new Error('Cannot parse magnet URI ' + magnet.href) 695 throw new Error('Cannot parse magnet URI ' + magnet.href)
671 } 696 }
672 697
698 // Fetch associated metadata url, if any
699 const metadata = urls.filter(isAPVideoFileMetadataObject)
700 .find(u => {
701 return u.height === fileUrl.height &&
702 u.fps === fileUrl.fps &&
703 u.rel.includes(fileUrl.mediaType)
704 })
705
673 const mediaType = fileUrl.mediaType 706 const mediaType = fileUrl.mediaType
674 const attribute = { 707 const attribute = {
675 extname: MIMETYPES.VIDEO.MIMETYPE_EXT[ mediaType ], 708 extname: MIMETYPES.VIDEO.MIMETYPE_EXT[mediaType],
676 infoHash: parsed.infoHash, 709 infoHash: parsed.infoHash,
677 resolution: fileUrl.height, 710 resolution: fileUrl.height,
678 size: fileUrl.size, 711 size: fileUrl.size,
679 fps: fileUrl.fps || -1, 712 fps: fileUrl.fps || -1,
713 metadataUrl: metadata?.href,
680 714
681 // This is a video file owned by a video or by a streaming playlist 715 // This is a video file owned by a video or by a streaming playlist
682 videoId: (videoOrPlaylist as MStreamingPlaylist).playlistUrl ? null : videoOrPlaylist.id, 716 videoId: (videoOrPlaylist as MStreamingPlaylist).playlistUrl ? null : videoOrPlaylist.id,
@@ -722,3 +756,19 @@ function streamingPlaylistActivityUrlToDBAttributes (video: MVideoId, videoObjec
722 756
723 return attributes 757 return attributes
724} 758}
759
760function getThumbnailFromIcons (videoObject: VideoTorrentObject) {
761 let validIcons = videoObject.icon.filter(i => i.width > THUMBNAILS_SIZE.minWidth)
762 // Fallback if there are not valid icons
763 if (validIcons.length === 0) validIcons = videoObject.icon
764
765 return minBy(validIcons, 'width')
766}
767
768function getPreviewFromIcons (videoObject: VideoTorrentObject) {
769 const validIcons = videoObject.icon.filter(i => i.width > PREVIEWS_SIZE.minWidth)
770
771 // FIXME: don't put a fallback here for compatibility with PeerTube <2.2
772
773 return maxBy(validIcons, 'width')
774}
diff --git a/server/lib/auth.ts b/server/lib/auth.ts
new file mode 100644
index 000000000..8579bdbb4
--- /dev/null
+++ b/server/lib/auth.ts
@@ -0,0 +1,286 @@
1import { isUserDisplayNameValid, isUserRoleValid, isUserUsernameValid } from '@server/helpers/custom-validators/users'
2import { logger } from '@server/helpers/logger'
3import { generateRandomString } from '@server/helpers/utils'
4import { OAUTH_LIFETIME, PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } from '@server/initializers/constants'
5import { revokeToken } from '@server/lib/oauth-model'
6import { PluginManager } from '@server/lib/plugins/plugin-manager'
7import { OAuthTokenModel } from '@server/models/oauth/oauth-token'
8import { UserRole } from '@shared/models'
9import {
10 RegisterServerAuthenticatedResult,
11 RegisterServerAuthPassOptions,
12 RegisterServerExternalAuthenticatedResult
13} from '@shared/models/plugins/register-server-auth.model'
14import * as express from 'express'
15import * as OAuthServer from 'express-oauth-server'
16
17const oAuthServer = new OAuthServer({
18 useErrorHandler: true,
19 accessTokenLifetime: OAUTH_LIFETIME.ACCESS_TOKEN,
20 refreshTokenLifetime: OAUTH_LIFETIME.REFRESH_TOKEN,
21 continueMiddleware: true,
22 model: require('./oauth-model')
23})
24
25// Token is the key, expiration date is the value
26const authBypassTokens = new Map<string, {
27 expires: Date
28 user: {
29 username: string
30 email: string
31 displayName: string
32 role: UserRole
33 }
34 authName: string
35 npmName: string
36}>()
37
38async function handleLogin (req: express.Request, res: express.Response, next: express.NextFunction) {
39 const grantType = req.body.grant_type
40
41 if (grantType === 'password') {
42 if (req.body.externalAuthToken) proxifyExternalAuthBypass(req, res)
43 else await proxifyPasswordGrant(req, res)
44 } else if (grantType === 'refresh_token') {
45 await proxifyRefreshGrant(req, res)
46 }
47
48 return forwardTokenReq(req, res, next)
49}
50
51async function handleTokenRevocation (req: express.Request, res: express.Response) {
52 const token = res.locals.oauth.token
53
54 res.locals.explicitLogout = true
55 await revokeToken(token)
56
57 // FIXME: uncomment when https://github.com/oauthjs/node-oauth2-server/pull/289 is released
58 // oAuthServer.revoke(req, res, err => {
59 // if (err) {
60 // logger.warn('Error in revoke token handler.', { err })
61 //
62 // return res.status(err.status)
63 // .json({
64 // error: err.message,
65 // code: err.name
66 // })
67 // .end()
68 // }
69 // })
70
71 return res.json()
72}
73
74async function onExternalUserAuthenticated (options: {
75 npmName: string
76 authName: string
77 authResult: RegisterServerExternalAuthenticatedResult
78}) {
79 const { npmName, authName, authResult } = options
80
81 if (!authResult.req || !authResult.res) {
82 logger.error('Cannot authenticate external user for auth %s of plugin %s: no req or res are provided.', authName, npmName)
83 return
84 }
85
86 const { res } = authResult
87
88 if (!isAuthResultValid(npmName, authName, authResult)) {
89 res.redirect('/login?externalAuthError=true')
90 return
91 }
92
93 logger.info('Generating auth bypass token for %s in auth %s of plugin %s.', authResult.username, authName, npmName)
94
95 const bypassToken = await generateRandomString(32)
96
97 const expires = new Date()
98 expires.setTime(expires.getTime() + PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME)
99
100 const user = buildUserResult(authResult)
101 authBypassTokens.set(bypassToken, {
102 expires,
103 user,
104 npmName,
105 authName
106 })
107
108 // Cleanup
109 const now = new Date()
110 for (const [ key, value ] of authBypassTokens) {
111 if (value.expires.getTime() < now.getTime()) {
112 authBypassTokens.delete(key)
113 }
114 }
115
116 res.redirect(`/login?externalAuthToken=${bypassToken}&username=${user.username}`)
117}
118
119// ---------------------------------------------------------------------------
120
121export { oAuthServer, handleLogin, onExternalUserAuthenticated, handleTokenRevocation }
122
123// ---------------------------------------------------------------------------
124
125function forwardTokenReq (req: express.Request, res: express.Response, next?: express.NextFunction) {
126 return oAuthServer.token()(req, res, err => {
127 if (err) {
128 logger.warn('Login error.', { err })
129
130 return res.status(err.status)
131 .json({
132 error: err.message,
133 code: err.name
134 })
135 }
136
137 if (next) return next()
138 })
139}
140
141async function proxifyRefreshGrant (req: express.Request, res: express.Response) {
142 const refreshToken = req.body.refresh_token
143 if (!refreshToken) return
144
145 const tokenModel = await OAuthTokenModel.loadByRefreshToken(refreshToken)
146 if (tokenModel?.authName) res.locals.refreshTokenAuthName = tokenModel.authName
147}
148
149async function proxifyPasswordGrant (req: express.Request, res: express.Response) {
150 const plugins = PluginManager.Instance.getIdAndPassAuths()
151 const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = []
152
153 for (const plugin of plugins) {
154 const auths = plugin.idAndPassAuths
155
156 for (const auth of auths) {
157 pluginAuths.push({
158 npmName: plugin.npmName,
159 registerAuthOptions: auth
160 })
161 }
162 }
163
164 pluginAuths.sort((a, b) => {
165 const aWeight = a.registerAuthOptions.getWeight()
166 const bWeight = b.registerAuthOptions.getWeight()
167
168 // DESC weight order
169 if (aWeight === bWeight) return 0
170 if (aWeight < bWeight) return 1
171 return -1
172 })
173
174 const loginOptions = {
175 id: req.body.username,
176 password: req.body.password
177 }
178
179 for (const pluginAuth of pluginAuths) {
180 const authOptions = pluginAuth.registerAuthOptions
181 const authName = authOptions.authName
182 const npmName = pluginAuth.npmName
183
184 logger.debug(
185 'Using auth method %s of plugin %s to login %s with weight %d.',
186 authName, npmName, loginOptions.id, authOptions.getWeight()
187 )
188
189 try {
190 const loginResult = await authOptions.login(loginOptions)
191
192 if (!loginResult) continue
193 if (!isAuthResultValid(pluginAuth.npmName, authOptions.authName, loginResult)) continue
194
195 logger.info(
196 'Login success with auth method %s of plugin %s for %s.',
197 authName, npmName, loginOptions.id
198 )
199
200 res.locals.bypassLogin = {
201 bypass: true,
202 pluginName: pluginAuth.npmName,
203 authName: authOptions.authName,
204 user: buildUserResult(loginResult)
205 }
206
207 return
208 } catch (err) {
209 logger.error('Error in auth method %s of plugin %s', authOptions.authName, pluginAuth.npmName, { err })
210 }
211 }
212}
213
214function proxifyExternalAuthBypass (req: express.Request, res: express.Response) {
215 const obj = authBypassTokens.get(req.body.externalAuthToken)
216 if (!obj) {
217 logger.error('Cannot authenticate user with unknown bypass token')
218 return res.sendStatus(400)
219 }
220
221 const { expires, user, authName, npmName } = obj
222
223 const now = new Date()
224 if (now.getTime() > expires.getTime()) {
225 logger.error('Cannot authenticate user with an expired external auth token')
226 return res.sendStatus(400)
227 }
228
229 if (user.username !== req.body.username) {
230 logger.error('Cannot authenticate user %s with invalid username %s.', req.body.username)
231 return res.sendStatus(400)
232 }
233
234 // Bypass oauth library validation
235 req.body.password = 'fake'
236
237 logger.info(
238 'Auth success with external auth method %s of plugin %s for %s.',
239 authName, npmName, user.email
240 )
241
242 res.locals.bypassLogin = {
243 bypass: true,
244 pluginName: npmName,
245 authName: authName,
246 user
247 }
248}
249
250function isAuthResultValid (npmName: string, authName: string, result: RegisterServerAuthenticatedResult) {
251 if (!isUserUsernameValid(result.username)) {
252 logger.error('Auth method %s of plugin %s did not provide a valid username.', authName, npmName, { username: result.username })
253 return false
254 }
255
256 if (!result.email) {
257 logger.error('Auth method %s of plugin %s did not provide a valid email.', authName, npmName, { email: result.email })
258 return false
259 }
260
261 // role is optional
262 if (result.role && !isUserRoleValid(result.role)) {
263 logger.error('Auth method %s of plugin %s did not provide a valid role.', authName, npmName, { role: result.role })
264 return false
265 }
266
267 // display name is optional
268 if (result.displayName && !isUserDisplayNameValid(result.displayName)) {
269 logger.error(
270 'Auth method %s of plugin %s did not provide a valid display name.',
271 authName, npmName, { displayName: result.displayName }
272 )
273 return false
274 }
275
276 return true
277}
278
279function buildUserResult (pluginResult: RegisterServerAuthenticatedResult) {
280 return {
281 username: pluginResult.username,
282 email: pluginResult.email,
283 role: pluginResult.role ?? UserRole.USER,
284 displayName: pluginResult.displayName || pluginResult.username
285 }
286}
diff --git a/server/lib/avatar.ts b/server/lib/avatar.ts
index ad4cdd3ab..282d834a2 100644
--- a/server/lib/avatar.ts
+++ b/server/lib/avatar.ts
@@ -1,11 +1,11 @@
1import 'multer' 1import 'multer'
2import { sendUpdateActor } from './activitypub/send' 2import { sendUpdateActor } from './activitypub/send'
3import { AVATARS_SIZE, LRU_CACHE, QUEUE_CONCURRENCY } from '../initializers/constants' 3import { AVATARS_SIZE, LRU_CACHE, QUEUE_CONCURRENCY } from '../initializers/constants'
4import { updateActorAvatarInstance } from './activitypub' 4import { updateActorAvatarInstance } from './activitypub/actor'
5import { processImage } from '../helpers/image-utils' 5import { processImage } from '../helpers/image-utils'
6import { extname, join } from 'path' 6import { extname, join } from 'path'
7import { retryTransactionWrapper } from '../helpers/database-utils' 7import { retryTransactionWrapper } from '../helpers/database-utils'
8import * as uuidv4 from 'uuid/v4' 8import { v4 as uuidv4 } from 'uuid'
9import { CONFIG } from '../initializers/config' 9import { CONFIG } from '../initializers/config'
10import { sequelizeTypescript } from '../initializers/database' 10import { sequelizeTypescript } from '../initializers/database'
11import * as LRUCache from 'lru-cache' 11import * as LRUCache from 'lru-cache'
diff --git a/server/lib/blocklist.ts b/server/lib/blocklist.ts
index 28c69b46e..842eecb5b 100644
--- a/server/lib/blocklist.ts
+++ b/server/lib/blocklist.ts
@@ -1,7 +1,7 @@
1import { sequelizeTypescript } from '../initializers' 1import { sequelizeTypescript } from '@server/initializers/database'
2import { MAccountBlocklist, MServerBlocklist } from '@server/typings/models'
2import { AccountBlocklistModel } from '../models/account/account-blocklist' 3import { AccountBlocklistModel } from '../models/account/account-blocklist'
3import { ServerBlocklistModel } from '../models/server/server-blocklist' 4import { ServerBlocklistModel } from '../models/server/server-blocklist'
4import { MAccountBlocklist, MServerBlocklist } from '@server/typings/models'
5 5
6function addAccountInBlocklist (byAccountId: number, targetAccountId: number) { 6function addAccountInBlocklist (byAccountId: number, targetAccountId: number) {
7 return sequelizeTypescript.transaction(async t => { 7 return sequelizeTypescript.transaction(async t => {
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts
index 1d8a08ed0..4a4b0d12f 100644
--- a/server/lib/client-html.ts
+++ b/server/lib/client-html.ts
@@ -17,7 +17,7 @@ import { MAccountActor, MChannelActor, MVideo } from '../typings/models'
17 17
18export class ClientHtml { 18export class ClientHtml {
19 19
20 private static htmlCache: { [ path: string ]: string } = {} 20 private static htmlCache: { [path: string]: string } = {}
21 21
22 static invalidCache () { 22 static invalidCache () {
23 logger.info('Cleaning HTML cache.') 23 logger.info('Cleaning HTML cache.')
@@ -94,7 +94,7 @@ export class ClientHtml {
94 94
95 private static async getIndexHTML (req: express.Request, res: express.Response, paramLang?: string) { 95 private static async getIndexHTML (req: express.Request, res: express.Response, paramLang?: string) {
96 const path = ClientHtml.getIndexPath(req, res, paramLang) 96 const path = ClientHtml.getIndexPath(req, res, paramLang)
97 if (ClientHtml.htmlCache[ path ]) return ClientHtml.htmlCache[ path ] 97 if (ClientHtml.htmlCache[path]) return ClientHtml.htmlCache[path]
98 98
99 const buffer = await readFile(path) 99 const buffer = await readFile(path)
100 100
@@ -104,7 +104,7 @@ export class ClientHtml {
104 html = ClientHtml.addCustomCSS(html) 104 html = ClientHtml.addCustomCSS(html)
105 html = await ClientHtml.addAsyncPluginCSS(html) 105 html = await ClientHtml.addAsyncPluginCSS(html)
106 106
107 ClientHtml.htmlCache[ path ] = html 107 ClientHtml.htmlCache[path] = html
108 108
109 return html 109 return html
110 } 110 }
@@ -119,7 +119,7 @@ export class ClientHtml {
119 // Save locale in cookies 119 // Save locale in cookies
120 res.cookie('clientLanguage', lang, { 120 res.cookie('clientLanguage', lang, {
121 secure: WEBSERVER.SCHEME === 'https', 121 secure: WEBSERVER.SCHEME === 'https',
122 sameSite: true, 122 sameSite: 'none',
123 maxAge: 1000 * 3600 * 24 * 90 // 3 months 123 maxAge: 1000 * 3600 * 24 * 90 // 3 months
124 }) 124 })
125 125
@@ -214,21 +214,21 @@ export class ClientHtml {
214 const schemaTags = { 214 const schemaTags = {
215 '@context': 'http://schema.org', 215 '@context': 'http://schema.org',
216 '@type': 'VideoObject', 216 '@type': 'VideoObject',
217 name: videoNameEscaped, 217 'name': videoNameEscaped,
218 description: videoDescriptionEscaped, 218 'description': videoDescriptionEscaped,
219 thumbnailUrl: previewUrl, 219 'thumbnailUrl': previewUrl,
220 uploadDate: video.createdAt.toISOString(), 220 'uploadDate': video.createdAt.toISOString(),
221 duration: getActivityStreamDuration(video.duration), 221 'duration': getActivityStreamDuration(video.duration),
222 contentUrl: videoUrl, 222 'contentUrl': videoUrl,
223 embedUrl: embedUrl, 223 'embedUrl': embedUrl,
224 interactionCount: video.views 224 'interactionCount': video.views
225 } 225 }
226 226
227 let tagsString = '' 227 let tagsString = ''
228 228
229 // Opengraph 229 // Opengraph
230 Object.keys(openGraphMetaTags).forEach(tagName => { 230 Object.keys(openGraphMetaTags).forEach(tagName => {
231 const tagValue = openGraphMetaTags[ tagName ] 231 const tagValue = openGraphMetaTags[tagName]
232 232
233 tagsString += `<meta property="${tagName}" content="${tagValue}" />` 233 tagsString += `<meta property="${tagName}" content="${tagValue}" />`
234 }) 234 })
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts
index 7484524a4..935c9e882 100644
--- a/server/lib/emailer.ts
+++ b/server/lib/emailer.ts
@@ -1,9 +1,8 @@
1import { createTransport, Transporter } from 'nodemailer' 1import { createTransport, Transporter } from 'nodemailer'
2import { isTestInstance } from '../helpers/core-utils' 2import { isTestInstance, root } from '../helpers/core-utils'
3import { bunyanLogger, logger } from '../helpers/logger' 3import { bunyanLogger, logger } from '../helpers/logger'
4import { CONFIG } from '../initializers/config' 4import { CONFIG, isEmailEnabled } from '../initializers/config'
5import { JobQueue } from './job-queue' 5import { JobQueue } from './job-queue'
6import { EmailPayload } from './job-queue/handlers/email'
7import { readFileSync } from 'fs-extra' 6import { readFileSync } from 'fs-extra'
8import { WEBSERVER } from '../initializers/constants' 7import { WEBSERVER } from '../initializers/constants'
9import { 8import {
@@ -16,15 +15,13 @@ import {
16} from '../typings/models/video' 15} from '../typings/models/video'
17import { MActorFollowActors, MActorFollowFull, MUser } from '../typings/models' 16import { MActorFollowActors, MActorFollowFull, MUser } from '../typings/models'
18import { MVideoImport, MVideoImportVideo } from '@server/typings/models/video/video-import' 17import { MVideoImport, MVideoImportVideo } from '@server/typings/models/video/video-import'
19 18import { EmailPayload } from '@shared/models'
20type SendEmailOptions = { 19import { join } from 'path'
21 to: string[] 20import { VideoAbuse } from '../../shared/models/videos'
22 subject: string 21import { SendEmailOptions } from '../../shared/models/server/emailer.model'
23 text: string 22import { merge } from 'lodash'
24 23import { VideoChannelModel } from '@server/models/video/video-channel'
25 fromDisplayName?: string 24const Email = require('email-templates')
26 replyTo?: string
27}
28 25
29class Emailer { 26class Emailer {
30 27
@@ -32,41 +29,52 @@ class Emailer {
32 private initialized = false 29 private initialized = false
33 private transporter: Transporter 30 private transporter: Transporter
34 31
35 private constructor () {} 32 private constructor () {
33 }
36 34
37 init () { 35 init () {
38 // Already initialized 36 // Already initialized
39 if (this.initialized === true) return 37 if (this.initialized === true) return
40 this.initialized = true 38 this.initialized = true
41 39
42 if (Emailer.isEnabled()) { 40 if (isEmailEnabled()) {
43 logger.info('Using %s:%s as SMTP server.', CONFIG.SMTP.HOSTNAME, CONFIG.SMTP.PORT) 41 if (CONFIG.SMTP.TRANSPORT === 'smtp') {
42 logger.info('Using %s:%s as SMTP server.', CONFIG.SMTP.HOSTNAME, CONFIG.SMTP.PORT)
44 43
45 let tls 44 let tls
46 if (CONFIG.SMTP.CA_FILE) { 45 if (CONFIG.SMTP.CA_FILE) {
47 tls = { 46 tls = {
48 ca: [ readFileSync(CONFIG.SMTP.CA_FILE) ] 47 ca: [ readFileSync(CONFIG.SMTP.CA_FILE) ]
48 }
49 } 49 }
50 }
51 50
52 let auth 51 let auth
53 if (CONFIG.SMTP.USERNAME && CONFIG.SMTP.PASSWORD) { 52 if (CONFIG.SMTP.USERNAME && CONFIG.SMTP.PASSWORD) {
54 auth = { 53 auth = {
55 user: CONFIG.SMTP.USERNAME, 54 user: CONFIG.SMTP.USERNAME,
56 pass: CONFIG.SMTP.PASSWORD 55 pass: CONFIG.SMTP.PASSWORD
56 }
57 } 57 }
58 }
59 58
60 this.transporter = createTransport({ 59 this.transporter = createTransport({
61 host: CONFIG.SMTP.HOSTNAME, 60 host: CONFIG.SMTP.HOSTNAME,
62 port: CONFIG.SMTP.PORT, 61 port: CONFIG.SMTP.PORT,
63 secure: CONFIG.SMTP.TLS, 62 secure: CONFIG.SMTP.TLS,
64 debug: CONFIG.LOG.LEVEL === 'debug', 63 debug: CONFIG.LOG.LEVEL === 'debug',
65 logger: bunyanLogger as any, 64 logger: bunyanLogger as any,
66 ignoreTLS: CONFIG.SMTP.DISABLE_STARTTLS, 65 ignoreTLS: CONFIG.SMTP.DISABLE_STARTTLS,
67 tls, 66 tls,
68 auth 67 auth
69 }) 68 })
69 } else { // sendmail
70 logger.info('Using sendmail to send emails')
71
72 this.transporter = createTransport({
73 sendmail: true,
74 newline: 'unix',
75 path: CONFIG.SMTP.SENDMAIL
76 })
77 }
70 } else { 78 } else {
71 if (!isTestInstance()) { 79 if (!isTestInstance()) {
72 logger.error('Cannot use SMTP server because of lack of configuration. PeerTube will not be able to send mails!') 80 logger.error('Cannot use SMTP server because of lack of configuration. PeerTube will not be able to send mails!')
@@ -75,11 +83,17 @@ class Emailer {
75 } 83 }
76 84
77 static isEnabled () { 85 static isEnabled () {
78 return !!CONFIG.SMTP.HOSTNAME && !!CONFIG.SMTP.PORT 86 if (CONFIG.SMTP.TRANSPORT === 'sendmail') {
87 return !!CONFIG.SMTP.SENDMAIL
88 } else if (CONFIG.SMTP.TRANSPORT === 'smtp') {
89 return !!CONFIG.SMTP.HOSTNAME && !!CONFIG.SMTP.PORT
90 } else {
91 return false
92 }
79 } 93 }
80 94
81 async checkConnectionOrDie () { 95 async checkConnectionOrDie () {
82 if (!this.transporter) return 96 if (!this.transporter || CONFIG.SMTP.TRANSPORT !== 'smtp') return
83 97
84 logger.info('Testing SMTP server...') 98 logger.info('Testing SMTP server...')
85 99
@@ -97,37 +111,36 @@ class Emailer {
97 const channelName = video.VideoChannel.getDisplayName() 111 const channelName = video.VideoChannel.getDisplayName()
98 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() 112 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
99 113
100 const text = `Hi dear user,\n\n` +
101 `Your subscription ${channelName} just published a new video: ${video.name}` +
102 `\n\n` +
103 `You can view it on ${videoUrl} ` +
104 `\n\n` +
105 `Cheers,\n` +
106 `${CONFIG.EMAIL.BODY.SIGNATURE}`
107
108 const emailPayload: EmailPayload = { 114 const emailPayload: EmailPayload = {
109 to, 115 to,
110 subject: CONFIG.EMAIL.SUBJECT.PREFIX + channelName + ' just published a new video', 116 subject: channelName + ' just published a new video',
111 text 117 text: `Your subscription ${channelName} just published a new video: "${video.name}".`,
118 locals: {
119 title: 'New content ',
120 action: {
121 text: 'View video',
122 url: videoUrl
123 }
124 }
112 } 125 }
113 126
114 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 127 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
115 } 128 }
116 129
117 addNewFollowNotification (to: string[], actorFollow: MActorFollowFull, followType: 'account' | 'channel') { 130 addNewFollowNotification (to: string[], actorFollow: MActorFollowFull, followType: 'account' | 'channel') {
118 const followerName = actorFollow.ActorFollower.Account.getDisplayName()
119 const followingName = (actorFollow.ActorFollowing.VideoChannel || actorFollow.ActorFollowing.Account).getDisplayName() 131 const followingName = (actorFollow.ActorFollowing.VideoChannel || actorFollow.ActorFollowing.Account).getDisplayName()
120 132
121 const text = `Hi dear user,\n\n` +
122 `Your ${followType} ${followingName} has a new subscriber: ${followerName}` +
123 `\n\n` +
124 `Cheers,\n` +
125 `${CONFIG.EMAIL.BODY.SIGNATURE}`
126
127 const emailPayload: EmailPayload = { 133 const emailPayload: EmailPayload = {
134 template: 'follower-on-channel',
128 to, 135 to,
129 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'New follower on your channel ' + followingName, 136 subject: `New follower on your channel ${followingName}`,
130 text 137 locals: {
138 followerName: actorFollow.ActorFollower.Account.getDisplayName(),
139 followerUrl: actorFollow.ActorFollower.url,
140 followingName,
141 followingUrl: actorFollow.ActorFollowing.url,
142 followType
143 }
131 } 144 }
132 145
133 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 146 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -136,32 +149,28 @@ class Emailer {
136 addNewInstanceFollowerNotification (to: string[], actorFollow: MActorFollowActors) { 149 addNewInstanceFollowerNotification (to: string[], actorFollow: MActorFollowActors) {
137 const awaitingApproval = actorFollow.state === 'pending' ? ' awaiting manual approval.' : '' 150 const awaitingApproval = actorFollow.state === 'pending' ? ' awaiting manual approval.' : ''
138 151
139 const text = `Hi dear admin,\n\n` +
140 `Your instance has a new follower: ${actorFollow.ActorFollower.url}${awaitingApproval}` +
141 `\n\n` +
142 `Cheers,\n` +
143 `${CONFIG.EMAIL.BODY.SIGNATURE}`
144
145 const emailPayload: EmailPayload = { 152 const emailPayload: EmailPayload = {
146 to, 153 to,
147 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'New instance follower', 154 subject: 'New instance follower',
148 text 155 text: `Your instance has a new follower: ${actorFollow.ActorFollower.url}${awaitingApproval}.`,
156 locals: {
157 title: 'New instance follower',
158 action: {
159 text: 'Review followers',
160 url: WEBSERVER.URL + '/admin/follows/followers-list'
161 }
162 }
149 } 163 }
150 164
151 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 165 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
152 } 166 }
153 167
154 addAutoInstanceFollowingNotification (to: string[], actorFollow: MActorFollowActors) { 168 addAutoInstanceFollowingNotification (to: string[], actorFollow: MActorFollowActors) {
155 const text = `Hi dear admin,\n\n` + 169 const instanceUrl = actorFollow.ActorFollowing.url
156 `Your instance automatically followed a new instance: ${actorFollow.ActorFollowing.url}` +
157 `\n\n` +
158 `Cheers,\n` +
159 `${CONFIG.EMAIL.BODY.SIGNATURE}`
160
161 const emailPayload: EmailPayload = { 170 const emailPayload: EmailPayload = {
162 to, 171 to,
163 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Auto instance following', 172 subject: 'Auto instance following',
164 text 173 text: `Your instance automatically followed a new instance: <a href="${instanceUrl}">${instanceUrl}</a>.`
165 } 174 }
166 175
167 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 176 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -170,18 +179,17 @@ class Emailer {
170 myVideoPublishedNotification (to: string[], video: MVideo) { 179 myVideoPublishedNotification (to: string[], video: MVideo) {
171 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() 180 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
172 181
173 const text = `Hi dear user,\n\n` +
174 `Your video ${video.name} has been published.` +
175 `\n\n` +
176 `You can view it on ${videoUrl} ` +
177 `\n\n` +
178 `Cheers,\n` +
179 `${CONFIG.EMAIL.BODY.SIGNATURE}`
180
181 const emailPayload: EmailPayload = { 182 const emailPayload: EmailPayload = {
182 to, 183 to,
183 subject: CONFIG.EMAIL.SUBJECT.PREFIX + `Your video ${video.name} is published`, 184 subject: `Your video ${video.name} has been published`,
184 text 185 text: `Your video "${video.name}" has been published.`,
186 locals: {
187 title: 'You video is live',
188 action: {
189 text: 'View video',
190 url: videoUrl
191 }
192 }
185 } 193 }
186 194
187 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 195 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -190,18 +198,17 @@ class Emailer {
190 myVideoImportSuccessNotification (to: string[], videoImport: MVideoImportVideo) { 198 myVideoImportSuccessNotification (to: string[], videoImport: MVideoImportVideo) {
191 const videoUrl = WEBSERVER.URL + videoImport.Video.getWatchStaticPath() 199 const videoUrl = WEBSERVER.URL + videoImport.Video.getWatchStaticPath()
192 200
193 const text = `Hi dear user,\n\n` +
194 `Your video import ${videoImport.getTargetIdentifier()} is finished.` +
195 `\n\n` +
196 `You can view the imported video on ${videoUrl} ` +
197 `\n\n` +
198 `Cheers,\n` +
199 `${CONFIG.EMAIL.BODY.SIGNATURE}`
200
201 const emailPayload: EmailPayload = { 201 const emailPayload: EmailPayload = {
202 to, 202 to,
203 subject: CONFIG.EMAIL.SUBJECT.PREFIX + `Your video import ${videoImport.getTargetIdentifier()} is finished`, 203 subject: `Your video import ${videoImport.getTargetIdentifier()} is complete`,
204 text 204 text: `Your video "${videoImport.getTargetIdentifier()}" just finished importing.`,
205 locals: {
206 title: 'Import complete',
207 action: {
208 text: 'View video',
209 url: videoUrl
210 }
211 }
205 } 212 }
206 213
207 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 214 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -210,40 +217,47 @@ class Emailer {
210 myVideoImportErrorNotification (to: string[], videoImport: MVideoImport) { 217 myVideoImportErrorNotification (to: string[], videoImport: MVideoImport) {
211 const importUrl = WEBSERVER.URL + '/my-account/video-imports' 218 const importUrl = WEBSERVER.URL + '/my-account/video-imports'
212 219
213 const text = `Hi dear user,\n\n` + 220 const text =
214 `Your video import ${videoImport.getTargetIdentifier()} encountered an error.` + 221 `Your video import "${videoImport.getTargetIdentifier()}" encountered an error.` +
215 `\n\n` + 222 '\n\n' +
216 `See your videos import dashboard for more information: ${importUrl}` + 223 `See your videos import dashboard for more information: <a href="${importUrl}">${importUrl}</a>.`
217 `\n\n` +
218 `Cheers,\n` +
219 `${CONFIG.EMAIL.BODY.SIGNATURE}`
220 224
221 const emailPayload: EmailPayload = { 225 const emailPayload: EmailPayload = {
222 to, 226 to,
223 subject: CONFIG.EMAIL.SUBJECT.PREFIX + `Your video import ${videoImport.getTargetIdentifier()} encountered an error`, 227 subject: `Your video import "${videoImport.getTargetIdentifier()}" encountered an error`,
224 text 228 text,
229 locals: {
230 title: 'Import failed',
231 action: {
232 text: 'Review imports',
233 url: importUrl
234 }
235 }
225 } 236 }
226 237
227 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 238 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
228 } 239 }
229 240
230 addNewCommentOnMyVideoNotification (to: string[], comment: MCommentOwnerVideo) { 241 addNewCommentOnMyVideoNotification (to: string[], comment: MCommentOwnerVideo) {
231 const accountName = comment.Account.getDisplayName()
232 const video = comment.Video 242 const video = comment.Video
243 const videoUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath()
233 const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath() 244 const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath()
234 245
235 const text = `Hi dear user,\n\n` +
236 `A new comment has been posted by ${accountName} on your video ${video.name}` +
237 `\n\n` +
238 `You can view it on ${commentUrl} ` +
239 `\n\n` +
240 `Cheers,\n` +
241 `${CONFIG.EMAIL.BODY.SIGNATURE}`
242
243 const emailPayload: EmailPayload = { 246 const emailPayload: EmailPayload = {
247 template: 'video-comment-new',
244 to, 248 to,
245 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'New comment on your video ' + video.name, 249 subject: 'New comment on your video ' + video.name,
246 text 250 locals: {
251 accountName: comment.Account.getDisplayName(),
252 accountUrl: comment.Account.Actor.url,
253 comment,
254 video,
255 videoUrl,
256 action: {
257 text: 'View comment',
258 url: commentUrl
259 }
260 }
247 } 261 }
248 262
249 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 263 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -252,75 +266,88 @@ class Emailer {
252 addNewCommentMentionNotification (to: string[], comment: MCommentOwnerVideo) { 266 addNewCommentMentionNotification (to: string[], comment: MCommentOwnerVideo) {
253 const accountName = comment.Account.getDisplayName() 267 const accountName = comment.Account.getDisplayName()
254 const video = comment.Video 268 const video = comment.Video
269 const videoUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath()
255 const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath() 270 const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath()
256 271
257 const text = `Hi dear user,\n\n` +
258 `${accountName} mentioned you on video ${video.name}` +
259 `\n\n` +
260 `You can view the comment on ${commentUrl} ` +
261 `\n\n` +
262 `Cheers,\n` +
263 `${CONFIG.EMAIL.BODY.SIGNATURE}`
264
265 const emailPayload: EmailPayload = { 272 const emailPayload: EmailPayload = {
273 template: 'video-comment-mention',
266 to, 274 to,
267 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Mention on video ' + video.name, 275 subject: 'Mention on video ' + video.name,
268 text 276 locals: {
277 comment,
278 video,
279 videoUrl,
280 accountName,
281 action: {
282 text: 'View comment',
283 url: commentUrl
284 }
285 }
269 } 286 }
270 287
271 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 288 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
272 } 289 }
273 290
274 addVideoAbuseModeratorsNotification (to: string[], videoAbuse: MVideoAbuseVideo) { 291 addVideoAbuseModeratorsNotification (to: string[], parameters: {
275 const videoUrl = WEBSERVER.URL + videoAbuse.Video.getWatchStaticPath() 292 videoAbuse: VideoAbuse
276 293 videoAbuseInstance: MVideoAbuseVideo
277 const text = `Hi,\n\n` + 294 reporter: string
278 `${WEBSERVER.HOST} received an abuse for the following video ${videoUrl}\n\n` + 295 }) {
279 `Cheers,\n` + 296 const videoAbuseUrl = WEBSERVER.URL + '/admin/moderation/video-abuses/list?search=%23' + parameters.videoAbuse.id
280 `${CONFIG.EMAIL.BODY.SIGNATURE}` 297 const videoUrl = WEBSERVER.URL + parameters.videoAbuseInstance.Video.getWatchStaticPath()
281 298
282 const emailPayload: EmailPayload = { 299 const emailPayload: EmailPayload = {
300 template: 'video-abuse-new',
283 to, 301 to,
284 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Received a video abuse', 302 subject: `New video abuse report from ${parameters.reporter}`,
285 text 303 locals: {
304 videoUrl,
305 videoAbuseUrl,
306 videoCreatedAt: new Date(parameters.videoAbuseInstance.Video.createdAt).toLocaleString(),
307 videoPublishedAt: new Date(parameters.videoAbuseInstance.Video.publishedAt).toLocaleString(),
308 videoAbuse: parameters.videoAbuse,
309 reporter: parameters.reporter,
310 action: {
311 text: 'View report #' + parameters.videoAbuse.id,
312 url: videoAbuseUrl
313 }
314 }
286 } 315 }
287 316
288 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 317 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
289 } 318 }
290 319
291 addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) { 320 async addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) {
292 const VIDEO_AUTO_BLACKLIST_URL = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list' 321 const VIDEO_AUTO_BLACKLIST_URL = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list'
293 const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath() 322 const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath()
294 323 const channel = (await VideoChannelModel.loadByIdAndPopulateAccount(videoBlacklist.Video.channelId)).toFormattedSummaryJSON()
295 const text = `Hi,\n\n` +
296 `A recently added video was auto-blacklisted and requires moderator review before publishing.` +
297 `\n\n` +
298 `You can view it and take appropriate action on ${videoUrl}` +
299 `\n\n` +
300 `A full list of auto-blacklisted videos can be reviewed here: ${VIDEO_AUTO_BLACKLIST_URL}` +
301 `\n\n` +
302 `Cheers,\n` +
303 `${CONFIG.EMAIL.BODY.SIGNATURE}`
304 324
305 const emailPayload: EmailPayload = { 325 const emailPayload: EmailPayload = {
326 template: 'video-auto-blacklist-new',
306 to, 327 to,
307 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'An auto-blacklisted video is awaiting review', 328 subject: 'A new video is pending moderation',
308 text 329 locals: {
330 channel,
331 videoUrl,
332 videoName: videoBlacklist.Video.name,
333 action: {
334 text: 'Review autoblacklist',
335 url: VIDEO_AUTO_BLACKLIST_URL
336 }
337 }
309 } 338 }
310 339
311 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 340 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
312 } 341 }
313 342
314 addNewUserRegistrationNotification (to: string[], user: MUser) { 343 addNewUserRegistrationNotification (to: string[], user: MUser) {
315 const text = `Hi,\n\n` +
316 `User ${user.username} just registered on ${WEBSERVER.HOST} PeerTube instance.\n\n` +
317 `Cheers,\n` +
318 `${CONFIG.EMAIL.BODY.SIGNATURE}`
319
320 const emailPayload: EmailPayload = { 344 const emailPayload: EmailPayload = {
345 template: 'user-registered',
321 to, 346 to,
322 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'New user registration on ' + WEBSERVER.HOST, 347 subject: `a new user registered on ${WEBSERVER.HOST}: ${user.username}`,
323 text 348 locals: {
349 user
350 }
324 } 351 }
325 352
326 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 353 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -333,16 +360,13 @@ class Emailer {
333 const reasonString = videoBlacklist.reason ? ` for the following reason: ${videoBlacklist.reason}` : '' 360 const reasonString = videoBlacklist.reason ? ` for the following reason: ${videoBlacklist.reason}` : ''
334 const blockedString = `Your video ${videoName} (${videoUrl} on ${WEBSERVER.HOST} has been blacklisted${reasonString}.` 361 const blockedString = `Your video ${videoName} (${videoUrl} on ${WEBSERVER.HOST} has been blacklisted${reasonString}.`
335 362
336 const text = 'Hi,\n\n' +
337 blockedString +
338 '\n\n' +
339 'Cheers,\n' +
340 `${CONFIG.EMAIL.BODY.SIGNATURE}`
341
342 const emailPayload: EmailPayload = { 363 const emailPayload: EmailPayload = {
343 to, 364 to,
344 subject: CONFIG.EMAIL.SUBJECT.PREFIX + `Video ${videoName} blacklisted`, 365 subject: `Video ${videoName} blacklisted`,
345 text 366 text: blockedString,
367 locals: {
368 title: 'Your video was blacklisted'
369 }
346 } 370 }
347 371
348 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 372 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -351,50 +375,53 @@ class Emailer {
351 addVideoUnblacklistNotification (to: string[], video: MVideo) { 375 addVideoUnblacklistNotification (to: string[], video: MVideo) {
352 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() 376 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
353 377
354 const text = 'Hi,\n\n' +
355 `Your video ${video.name} (${videoUrl}) on ${WEBSERVER.HOST} has been unblacklisted.` +
356 '\n\n' +
357 'Cheers,\n' +
358 `${CONFIG.EMAIL.BODY.SIGNATURE}`
359
360 const emailPayload: EmailPayload = { 378 const emailPayload: EmailPayload = {
361 to, 379 to,
362 subject: CONFIG.EMAIL.SUBJECT.PREFIX + `Video ${video.name} unblacklisted`, 380 subject: `Video ${video.name} unblacklisted`,
363 text 381 text: `Your video "${video.name}" (${videoUrl}) on ${WEBSERVER.HOST} has been unblacklisted.`,
382 locals: {
383 title: 'Your video was unblacklisted'
384 }
364 } 385 }
365 386
366 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 387 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
367 } 388 }
368 389
369 addPasswordResetEmailJob (to: string, resetPasswordUrl: string) { 390 addPasswordResetEmailJob (to: string, resetPasswordUrl: string) {
370 const text = `Hi dear user,\n\n` + 391 const emailPayload: EmailPayload = {
371 `A reset password procedure for your account ${to} has been requested on ${WEBSERVER.HOST} ` + 392 template: 'password-reset',
372 `Please follow this link to reset it: ${resetPasswordUrl} (the link will expire within 1 hour)\n\n` + 393 to: [ to ],
373 `If you are not the person who initiated this request, please ignore this email.\n\n` + 394 subject: 'Reset your account password',
374 `Cheers,\n` + 395 locals: {
375 `${CONFIG.EMAIL.BODY.SIGNATURE}` 396 resetPasswordUrl
397 }
398 }
376 399
400 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
401 }
402
403 addPasswordCreateEmailJob (username: string, to: string, createPasswordUrl: string) {
377 const emailPayload: EmailPayload = { 404 const emailPayload: EmailPayload = {
405 template: 'password-create',
378 to: [ to ], 406 to: [ to ],
379 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Reset your password', 407 subject: 'Create your account password',
380 text 408 locals: {
409 username,
410 createPasswordUrl
411 }
381 } 412 }
382 413
383 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 414 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
384 } 415 }
385 416
386 addVerifyEmailJob (to: string, verifyEmailUrl: string) { 417 addVerifyEmailJob (to: string, verifyEmailUrl: string) {
387 const text = `Welcome to PeerTube,\n\n` +
388 `To start using PeerTube on ${WEBSERVER.HOST} you must verify your email! ` +
389 `Please follow this link to verify this email belongs to you: ${verifyEmailUrl}\n\n` +
390 `If you are not the person who initiated this request, please ignore this email.\n\n` +
391 `Cheers,\n` +
392 `${CONFIG.EMAIL.BODY.SIGNATURE}`
393
394 const emailPayload: EmailPayload = { 418 const emailPayload: EmailPayload = {
419 template: 'verify-email',
395 to: [ to ], 420 to: [ to ],
396 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Verify your email', 421 subject: `Verify your email on ${WEBSERVER.HOST}`,
397 text 422 locals: {
423 verifyEmailUrl
424 }
398 } 425 }
399 426
400 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 427 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
@@ -403,61 +430,76 @@ class Emailer {
403 addUserBlockJob (user: MUser, blocked: boolean, reason?: string) { 430 addUserBlockJob (user: MUser, blocked: boolean, reason?: string) {
404 const reasonString = reason ? ` for the following reason: ${reason}` : '' 431 const reasonString = reason ? ` for the following reason: ${reason}` : ''
405 const blockedWord = blocked ? 'blocked' : 'unblocked' 432 const blockedWord = blocked ? 'blocked' : 'unblocked'
406 const blockedString = `Your account ${user.username} on ${WEBSERVER.HOST} has been ${blockedWord}${reasonString}.`
407
408 const text = 'Hi,\n\n' +
409 blockedString +
410 '\n\n' +
411 'Cheers,\n' +
412 `${CONFIG.EMAIL.BODY.SIGNATURE}`
413 433
414 const to = user.email 434 const to = user.email
415 const emailPayload: EmailPayload = { 435 const emailPayload: EmailPayload = {
416 to: [ to ], 436 to: [ to ],
417 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Account ' + blockedWord, 437 subject: 'Account ' + blockedWord,
418 text 438 text: `Your account ${user.username} on ${WEBSERVER.HOST} has been ${blockedWord}${reasonString}.`
419 } 439 }
420 440
421 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 441 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
422 } 442 }
423 443
424 addContactFormJob (fromEmail: string, fromName: string, subject: string, body: string) { 444 addContactFormJob (fromEmail: string, fromName: string, subject: string, body: string) {
425 const text = 'Hello dear admin,\n\n' +
426 fromName + ' sent you a message' +
427 '\n\n---------------------------------------\n\n' +
428 body +
429 '\n\n---------------------------------------\n\n' +
430 'Cheers,\n' +
431 'PeerTube.'
432
433 const emailPayload: EmailPayload = { 445 const emailPayload: EmailPayload = {
434 fromDisplayName: fromEmail, 446 template: 'contact-form',
435 replyTo: fromEmail,
436 to: [ CONFIG.ADMIN.EMAIL ], 447 to: [ CONFIG.ADMIN.EMAIL ],
437 subject: CONFIG.EMAIL.SUBJECT.PREFIX + subject, 448 replyTo: `"${fromName}" <${fromEmail}>`,
438 text 449 subject: `(contact form) ${subject}`,
450 locals: {
451 fromName,
452 fromEmail,
453 body
454 }
439 } 455 }
440 456
441 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 457 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
442 } 458 }
443 459
444 async sendMail (options: EmailPayload) { 460 async sendMail (options: EmailPayload) {
445 if (!Emailer.isEnabled()) { 461 if (!isEmailEnabled()) {
446 throw new Error('Cannot send mail because SMTP is not configured.') 462 throw new Error('Cannot send mail because SMTP is not configured.')
447 } 463 }
448 464
449 const fromDisplayName = options.fromDisplayName 465 const fromDisplayName = options.from
450 ? options.fromDisplayName 466 ? options.from
451 : WEBSERVER.HOST 467 : WEBSERVER.HOST
452 468
469 const email = new Email({
470 send: true,
471 message: {
472 from: `"${fromDisplayName}" <${CONFIG.SMTP.FROM_ADDRESS}>`
473 },
474 transport: this.transporter,
475 views: {
476 root: join(root(), 'server', 'lib', 'emails')
477 },
478 subjectPrefix: CONFIG.EMAIL.SUBJECT.PREFIX
479 })
480
453 for (const to of options.to) { 481 for (const to of options.to) {
454 await this.transporter.sendMail({ 482 await email
455 from: `"${fromDisplayName}" <${CONFIG.SMTP.FROM_ADDRESS}>`, 483 .send(merge(
456 replyTo: options.replyTo, 484 {
457 to, 485 template: 'common',
458 subject: options.subject, 486 message: {
459 text: options.text 487 to,
460 }) 488 from: options.from,
489 subject: options.subject,
490 replyTo: options.replyTo
491 },
492 locals: { // default variables available in all templates
493 WEBSERVER,
494 EMAIL: CONFIG.EMAIL,
495 text: options.text,
496 subject: options.subject
497 }
498 },
499 options // overriden/new variables given for a specific template in the payload
500 ) as SendEmailOptions)
501 .then(logger.info)
502 .catch(logger.error)
461 } 503 }
462 } 504 }
463 505
@@ -474,6 +516,5 @@ class Emailer {
474// --------------------------------------------------------------------------- 516// ---------------------------------------------------------------------------
475 517
476export { 518export {
477 Emailer, 519 Emailer
478 SendEmailOptions
479} 520}
diff --git a/server/lib/emails/common/base.pug b/server/lib/emails/common/base.pug
new file mode 100644
index 000000000..9a1894cab
--- /dev/null
+++ b/server/lib/emails/common/base.pug
@@ -0,0 +1,267 @@
1//-
2 The email background color is defined in three places:
3 1. body tag: for most email clients
4 2. center tag: for Gmail and Inbox mobile apps and web versions of Gmail, GSuite, Inbox, Yahoo, AOL, Libero, Comcast, freenet, Mail.ru, Orange.fr
5 3. mso conditional: For Windows 10 Mail
6- var backgroundColor = "#fff";
7- var mainColor = "#f2690d";
8doctype html
9head
10 // This template is heavily adapted from the Cerberus Fluid template. Kudos to them!
11 meta(charset='utf-8')
12 //- utf-8 works for most cases
13 meta(name='viewport' content='width=device-width')
14 //- Forcing initial-scale shouldn't be necessary
15 meta(http-equiv='X-UA-Compatible' content='IE=edge')
16 //- Use the latest (edge) version of IE rendering engine
17 meta(name='x-apple-disable-message-reformatting')
18 //- Disable auto-scale in iOS 10 Mail entirely
19 meta(name='format-detection' content='telephone=no,address=no,email=no,date=no,url=no')
20 //- Tell iOS not to automatically link certain text strings.
21 meta(name='color-scheme' content='light')
22 meta(name='supported-color-schemes' content='light')
23 //- The title tag shows in email notifications, like Android 4.4.
24 title #{subject}
25 //- What it does: Makes background images in 72ppi Outlook render at correct size.
26 //if gte mso 9
27 xml
28 o:officedocumentsettings
29 o:allowpng
30 o:pixelsperinch 96
31 //- CSS Reset : BEGIN
32 style.
33 /* What it does: Tells the email client that only light styles are provided but the client can transform them to dark. A duplicate of meta color-scheme meta tag above. */
34 :root {
35 color-scheme: light;
36 supported-color-schemes: light;
37 }
38 /* What it does: Remove spaces around the email design added by some email clients. */
39 /* Beware: It can remove the padding / margin and add a background color to the compose a reply window. */
40 html,
41 body {
42 margin: 0 auto !important;
43 padding: 0 !important;
44 height: 100% !important;
45 width: 100% !important;
46 }
47 /* What it does: Stops email clients resizing small text. */
48 * {
49 -ms-text-size-adjust: 100%;
50 -webkit-text-size-adjust: 100%;
51 }
52 /* What it does: Centers email on Android 4.4 */
53 div[style*="margin: 16px 0"] {
54 margin: 0 !important;
55 }
56 /* What it does: forces Samsung Android mail clients to use the entire viewport */
57 #MessageViewBody, #MessageWebViewDiv{
58 width: 100% !important;
59 }
60 /* What it does: Stops Outlook from adding extra spacing to tables. */
61 table,
62 td {
63 mso-table-lspace: 0pt !important;
64 mso-table-rspace: 0pt !important;
65 }
66 /* What it does: Fixes webkit padding issue. */
67 table {
68 border-spacing: 0 !important;
69 border-collapse: collapse !important;
70 table-layout: fixed !important;
71 margin: 0 auto !important;
72 }
73 /* What it does: Uses a better rendering method when resizing images in IE. */
74 img {
75 -ms-interpolation-mode:bicubic;
76 }
77 /* What it does: Prevents Windows 10 Mail from underlining links despite inline CSS. Styles for underlined links should be inline. */
78 a {
79 text-decoration: none;
80 }
81 a:not(.nocolor) {
82 color: #{mainColor};
83 }
84 a.nocolor {
85 color: inherit !important;
86 }
87 /* What it does: A work-around for email clients meddling in triggered links. */
88 a[x-apple-data-detectors], /* iOS */
89 .unstyle-auto-detected-links a,
90 .aBn {
91 border-bottom: 0 !important;
92 cursor: default !important;
93 color: inherit !important;
94 text-decoration: none !important;
95 font-size: inherit !important;
96 font-family: inherit !important;
97 font-weight: inherit !important;
98 line-height: inherit !important;
99 }
100 /* What it does: Prevents Gmail from displaying a download button on large, non-linked images. */
101 .a6S {
102 display: none !important;
103 opacity: 0.01 !important;
104 }
105 /* What it does: Prevents Gmail from changing the text color in conversation threads. */
106 .im {
107 color: inherit !important;
108 }
109 /* If the above doesn't work, add a .g-img class to any image in question. */
110 img.g-img + div {
111 display: none !important;
112 }
113 /* What it does: Removes right gutter in Gmail iOS app: https://github.com/TedGoas/Cerberus/issues/89 */
114 /* Create one of these media queries for each additional viewport size you'd like to fix */
115 /* iPhone 4, 4S, 5, 5S, 5C, and 5SE */
116 @media only screen and (min-device-width: 320px) and (max-device-width: 374px) {
117 u ~ div .email-container {
118 min-width: 320px !important;
119 }
120 }
121 /* iPhone 6, 6S, 7, 8, and X */
122 @media only screen and (min-device-width: 375px) and (max-device-width: 413px) {
123 u ~ div .email-container {
124 min-width: 375px !important;
125 }
126 }
127 /* iPhone 6+, 7+, and 8+ */
128 @media only screen and (min-device-width: 414px) {
129 u ~ div .email-container {
130 min-width: 414px !important;
131 }
132 }
133 //- CSS Reset : END
134 //- CSS for PeerTube : START
135 style.
136 blockquote {
137 margin-left: 0;
138 padding-left: 20px;
139 border-left: 2px solid #f2690d;
140 }
141 //- CSS for PeerTube : END
142 //- Progressive Enhancements : BEGIN
143 style.
144 /* What it does: Hover styles for buttons */
145 .button-td,
146 .button-a {
147 transition: all 100ms ease-in;
148 }
149 .button-td-primary:hover,
150 .button-a-primary:hover {
151 background: #555555 !important;
152 border-color: #555555 !important;
153 }
154 /* Media Queries */
155 @media screen and (max-width: 600px) {
156 /* What it does: Adjust typography on small screens to improve readability */
157 .email-container p {
158 font-size: 17px !important;
159 }
160 }
161 //- Progressive Enhancements : END
162
163body(width="100%" style="margin: 0; padding: 0 !important; mso-line-height-rule: exactly; background-color: #{backgroundColor};")
164 center(role='article' aria-roledescription='email' lang='en' style='width: 100%; background-color: #{backgroundColor};')
165 //if mso | IE
166 table(role='presentation' border='0' cellpadding='0' cellspacing='0' width='100%' style='background-color: #fff;')
167 tr
168 td
169 //- Visually Hidden Preheader Text : BEGIN
170 div(style='max-height:0; overflow:hidden; mso-hide:all;' aria-hidden='true')
171 block preheader
172 //- Visually Hidden Preheader Text : END
173
174 //- Create white space after the desired preview text so email clients don’t pull other distracting text into the inbox preview. Extend as necessary.
175 //- Preview Text Spacing Hack : BEGIN
176 div(style='display: none; font-size: 1px; line-height: 1px; max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden; mso-hide: all; font-family: sans-serif;')
177 | &zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;
178 //- Preview Text Spacing Hack : END
179
180 //-
181 Set the email width. Defined in two places:
182 1. max-width for all clients except Desktop Windows Outlook, allowing the email to squish on narrow but never go wider than 600px.
183 2. MSO tags for Desktop Windows Outlook enforce a 600px width.
184 .email-container(style='max-width: 600px; margin: 0 auto;')
185 //if mso
186 table(align='center' role='presentation' cellspacing='0' cellpadding='0' border='0' width='600')
187 tr
188 td
189 //- Email Body : BEGIN
190 table(align='center' role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%' style='margin: auto;')
191 //- 1 Column Text + Button : BEGIN
192 tr
193 td(style='background-color: #ffffff;')
194 table(role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%')
195 tr
196 td(style='padding: 20px; font-family: sans-serif; font-size: 15px; line-height: 20px; color: #555555;')
197 table(role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%")
198 tr
199 td(width="40px")
200 img(src=`${WEBSERVER.URL}/client/assets/images/icons/icon-192x192.png` width="auto" height="30px" alt="icon" border="0" style="height: 30px; background: #ffffff; font-family: sans-serif; font-size: 15px; line-height: 15px; color: #555555;")
201 td
202 h1(style='margin: 10px 0 10px 0; font-family: sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;')
203 block title
204 if title
205 | #{title}
206 else
207 | Something requires your attention
208 p(style='margin: 0;')
209 block body
210 if action
211 tr
212 td(style='padding: 0 20px;')
213 //- Button : BEGIN
214 table(align='center' role='presentation' cellspacing='0' cellpadding='0' border='0' style='margin: auto;')
215 tr
216 td.button-td.button-td-primary(style='border-radius: 4px; background: #222222;')
217 a.button-a.button-a-primary(href=action.url style='background: #222222; border: 1px solid #000000; font-family: sans-serif; font-size: 15px; line-height: 15px; text-decoration: none; padding: 13px 17px; color: #ffffff; display: block; border-radius: 4px;') #{action.text}
218 //- Button : END
219 //- 1 Column Text + Button : END
220 //- Clear Spacer : BEGIN
221 tr
222 td(aria-hidden='true' height='20' style='font-size: 0px; line-height: 0px;')
223 br
224 //- Clear Spacer : END
225 //- 1 Column Text : BEGIN
226 if username
227 tr
228 td(style='background-color: #cccccc;')
229 table(role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%')
230 tr
231 td(style='padding: 20px; font-family: sans-serif; font-size: 15px; line-height: 20px; color: #555555;')
232 p(style='margin: 0;')
233 | You are receiving this email as part of your notification settings on #{WEBSERVER.HOST} for your account #{username}.
234 //- 1 Column Text : END
235 //- Email Body : END
236 //- Email Footer : BEGIN
237 table(align='center' role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%' style='margin: auto;')
238 tr
239 td(style='padding: 20px; padding-bottom: 0px; font-family: sans-serif; font-size: 12px; line-height: 15px; text-align: center; color: #888888;')
240 webversion
241 a.nocolor(href=`${WEBSERVER.URL}/my-account/notifications` style='color: #cccccc; font-weight: bold;') View in your notifications
242 br
243 tr
244 td(style='padding: 20px; padding-top: 10px; font-family: sans-serif; font-size: 12px; line-height: 15px; text-align: center; color: #888888;')
245 unsubscribe
246 a.nocolor(href=`${WEBSERVER.URL}/my-account/settings#notifications` style='color: #888888;') Manage your notification preferences in your profile
247 br
248 //- Email Footer : END
249 //if mso
250 //- Full Bleed Background Section : BEGIN
251 table(role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%' style=`background-color: ${mainColor};`)
252 tr
253 td
254 .email-container(align='center' style='max-width: 600px; margin: auto;')
255 //if mso
256 table(role='presentation' cellspacing='0' cellpadding='0' border='0' width='600' align='center')
257 tr
258 td
259 table(role='presentation' cellspacing='0' cellpadding='0' border='0' width='100%')
260 tr
261 td(style='padding: 20px; text-align: left; font-family: sans-serif; font-size: 12px; line-height: 20px; color: #ffffff;')
262 table(role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%")
263 tr
264 td(valign="top") #[a(href="https://github.com/Chocobozzz/PeerTube" style="color: white !important") PeerTube © 2015-#{new Date().getFullYear()}] #[a(href="https://github.com/Chocobozzz/PeerTube/blob/master/CREDITS.md" style="color: white !important") PeerTube Contributors]
265 //if mso
266 //- Full Bleed Background Section : END
267 //if mso | IE
diff --git a/server/lib/emails/common/greetings.pug b/server/lib/emails/common/greetings.pug
new file mode 100644
index 000000000..5efe29dfb
--- /dev/null
+++ b/server/lib/emails/common/greetings.pug
@@ -0,0 +1,11 @@
1extends base
2
3block body
4 if username
5 p Hi #{username},
6 else
7 p Hi,
8 block content
9 p
10 | Cheers,#[br]
11 | #{EMAIL.BODY.SIGNATURE} \ No newline at end of file
diff --git a/server/lib/emails/common/html.pug b/server/lib/emails/common/html.pug
new file mode 100644
index 000000000..d76168b85
--- /dev/null
+++ b/server/lib/emails/common/html.pug
@@ -0,0 +1,4 @@
1extends greetings
2
3block content
4 p !{text} \ No newline at end of file
diff --git a/server/lib/emails/common/mixins.pug b/server/lib/emails/common/mixins.pug
new file mode 100644
index 000000000..76b805a24
--- /dev/null
+++ b/server/lib/emails/common/mixins.pug
@@ -0,0 +1,3 @@
1mixin channel(channel)
2 - var handle = `${channel.name}@${channel.host}`
3 | #[a(href=`${WEBSERVER.URL}/video-channels/${handle}` title=handle) #{channel.displayName}] \ No newline at end of file
diff --git a/server/lib/emails/contact-form/html.pug b/server/lib/emails/contact-form/html.pug
new file mode 100644
index 000000000..0073ff78e
--- /dev/null
+++ b/server/lib/emails/contact-form/html.pug
@@ -0,0 +1,9 @@
1extends ../common/greetings
2
3block title
4 | Someone just used the contact form
5
6block content
7 p #{fromName} sent you a message via the contact form on #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}]:
8 blockquote(style='white-space: pre-wrap') #{body}
9 p You can contact them at #[a(href=`mailto:${fromEmail}`) #{fromEmail}], or simply reply to this email to get in touch. \ No newline at end of file
diff --git a/server/lib/emails/follower-on-channel/html.pug b/server/lib/emails/follower-on-channel/html.pug
new file mode 100644
index 000000000..8a352e90f
--- /dev/null
+++ b/server/lib/emails/follower-on-channel/html.pug
@@ -0,0 +1,9 @@
1extends ../common/greetings
2
3block title
4 | New follower on your channel
5
6block content
7 p.
8 Your #{followType} #[a(href=followingUrl) #{followingName}] has a new subscriber:
9 #[a(href=followerUrl) #{followerName}]. \ No newline at end of file
diff --git a/server/lib/emails/password-create/html.pug b/server/lib/emails/password-create/html.pug
new file mode 100644
index 000000000..45ff3078a
--- /dev/null
+++ b/server/lib/emails/password-create/html.pug
@@ -0,0 +1,10 @@
1extends ../common/greetings
2
3block title
4 | Password creation for your account
5
6block content
7 p.
8 Welcome to #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}], your PeerTube instance. Your username is: #{username}.
9 Please set your password by following #[a(href=createPasswordUrl) this link]: #[a(href=createPasswordUrl) #{createPasswordUrl}]
10 (this link will expire within seven days). \ No newline at end of file
diff --git a/server/lib/emails/password-reset/html.pug b/server/lib/emails/password-reset/html.pug
new file mode 100644
index 000000000..bb6a9d16b
--- /dev/null
+++ b/server/lib/emails/password-reset/html.pug
@@ -0,0 +1,12 @@
1extends ../common/greetings
2
3block title
4 | Password reset for your account
5
6block content
7 p.
8 A reset password procedure for your account ${to} has been requested on #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}].
9 Please follow #[a(href=resetPasswordUrl) this link] to reset it: #[a(href=resetPasswordUrl) #{resetPasswordUrl}]
10 (the link will expire within 1 hour)
11 p.
12 If you are not the person who initiated this request, please ignore this email. \ No newline at end of file
diff --git a/server/lib/emails/user-registered/html.pug b/server/lib/emails/user-registered/html.pug
new file mode 100644
index 000000000..20f62125e
--- /dev/null
+++ b/server/lib/emails/user-registered/html.pug
@@ -0,0 +1,10 @@
1extends ../common/greetings
2
3block title
4 | A new user registered
5
6block content
7 - var mail = user.email || user.pendingEmail;
8 p
9 | User #[a(href=`${WEBSERVER.URL}/accounts/${user.username}`) #{user.username}] just registered.
10 | You might want to contact them at #[a(href=`mailto:${mail}`) #{mail}]. \ No newline at end of file
diff --git a/server/lib/emails/verify-email/html.pug b/server/lib/emails/verify-email/html.pug
new file mode 100644
index 000000000..8a4a77703
--- /dev/null
+++ b/server/lib/emails/verify-email/html.pug
@@ -0,0 +1,14 @@
1extends ../common/greetings
2
3block title
4 | Account verification
5
6block content
7 p Welcome to PeerTube!
8 p.
9 You just created an account #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}], your new PeerTube instance.
10 Your username there is: #{username}.
11 p.
12 To start using PeerTube on #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] you must verify your email first!
13 Please follow #[a(href=verifyEmailUrl) this link] to verify this email belongs to you: #[a(href=verifyEmailUrl) #{verifyEmailUrl}]
14 If you are not the person who initiated this request, please ignore this email. \ No newline at end of file
diff --git a/server/lib/emails/video-abuse-new/html.pug b/server/lib/emails/video-abuse-new/html.pug
new file mode 100644
index 000000000..999c89d26
--- /dev/null
+++ b/server/lib/emails/video-abuse-new/html.pug
@@ -0,0 +1,18 @@
1extends ../common/greetings
2include ../common/mixins.pug
3
4block title
5 | A video is pending moderation
6
7block content
8 p
9 | #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] received an abuse report for the #{videoAbuse.video.channel.isLocal ? '' : 'remote '}video "
10 a(href=videoUrl) #{videoAbuse.video.name}
11 | " by #[+channel(videoAbuse.video.channel)]
12 if videoPublishedAt
13 | , published the #{videoPublishedAt}.
14 else
15 | , uploaded the #{videoCreatedAt} but not yet published.
16 p The reporter, #{reporter}, cited the following reason(s):
17 blockquote #{videoAbuse.reason}
18 br(style="display: none;")
diff --git a/server/lib/emails/video-auto-blacklist-new/html.pug b/server/lib/emails/video-auto-blacklist-new/html.pug
new file mode 100644
index 000000000..07c8dfd16
--- /dev/null
+++ b/server/lib/emails/video-auto-blacklist-new/html.pug
@@ -0,0 +1,17 @@
1extends ../common/greetings
2include ../common/mixins
3
4block title
5 | A video is pending moderation
6
7block content
8 p
9 | A recently added video was auto-blacklisted and requires moderator review before going public:
10 |
11 a(href=videoUrl) #{videoName}
12 |
13 | by #[+channel(channel)].
14 p.
15 Apart from the publisher and the moderation team, no one will be able to see the video until you
16 unblacklist it. If you trust the publisher, any admin can whitelist the user for later videos so
17 that they don't require approval before going public.
diff --git a/server/lib/emails/video-comment-mention/html.pug b/server/lib/emails/video-comment-mention/html.pug
new file mode 100644
index 000000000..9e9ced62d
--- /dev/null
+++ b/server/lib/emails/video-comment-mention/html.pug
@@ -0,0 +1,11 @@
1extends ../common/greetings
2
3block title
4 | Someone mentioned you
5
6block content
7 p.
8 #[a(href=accountUrl title=handle) #{accountName}] mentioned you in a comment on video
9 "#[a(href=videoUrl) #{video.name}]":
10 blockquote #{comment.text}
11 br(style="display: none;") \ No newline at end of file
diff --git a/server/lib/emails/video-comment-new/html.pug b/server/lib/emails/video-comment-new/html.pug
new file mode 100644
index 000000000..075af5717
--- /dev/null
+++ b/server/lib/emails/video-comment-new/html.pug
@@ -0,0 +1,11 @@
1extends ../common/greetings
2
3block title
4 | Someone commented your video
5
6block content
7 p.
8 #[a(href=accountUrl title=handle) #{accountName}] added a comment on your video
9 "#[a(href=videoUrl) #{video.name}]":
10 blockquote #{comment.text}
11 br(style="display: none;") \ No newline at end of file
diff --git a/server/lib/files-cache/videos-caption-cache.ts b/server/lib/files-cache/videos-caption-cache.ts
index 440c3fde8..26ab3bd0d 100644
--- a/server/lib/files-cache/videos-caption-cache.ts
+++ b/server/lib/files-cache/videos-caption-cache.ts
@@ -5,7 +5,7 @@ import { 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' 6import { CONFIG } from '../../initializers/config'
7import { logger } from '../../helpers/logger' 7import { logger } from '../../helpers/logger'
8import { fetchRemoteVideoStaticFile } from '../activitypub' 8import { doRequestAndSaveToFile } from '@server/helpers/requests'
9 9
10type GetPathParam = { videoId: string, language: string } 10type GetPathParam = { videoId: string, language: string }
11 11
@@ -46,11 +46,10 @@ class VideosCaptionCache extends AbstractVideoStaticFileCache <GetPathParam> {
46 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId) 46 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId)
47 if (!video) return undefined 47 if (!video) return undefined
48 48
49 // FIXME: use URL 49 const remoteUrl = videoCaption.getFileUrl(video)
50 const remoteStaticPath = videoCaption.getCaptionStaticPath()
51 const destPath = join(FILES_CACHE.VIDEO_CAPTIONS.DIRECTORY, videoCaption.getCaptionName()) 50 const destPath = join(FILES_CACHE.VIDEO_CAPTIONS.DIRECTORY, videoCaption.getCaptionName())
52 51
53 await fetchRemoteVideoStaticFile(video, remoteStaticPath, destPath) 52 await doRequestAndSaveToFile({ uri: remoteUrl }, destPath)
54 53
55 return { isOwned: false, path: destPath } 54 return { isOwned: false, path: destPath }
56 } 55 }
diff --git a/server/lib/files-cache/videos-preview-cache.ts b/server/lib/files-cache/videos-preview-cache.ts
index 3da6bb138..d0d4fc5b5 100644
--- a/server/lib/files-cache/videos-preview-cache.ts
+++ b/server/lib/files-cache/videos-preview-cache.ts
@@ -1,9 +1,8 @@
1import { join } from 'path' 1import { join } from 'path'
2import { FILES_CACHE, STATIC_PATHS } from '../../initializers/constants' 2import { FILES_CACHE } 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' 5import { doRequestAndSaveToFile } from '@server/helpers/requests'
6import { fetchRemoteVideoStaticFile } from '../activitypub'
7 6
8class VideosPreviewCache extends AbstractVideoStaticFileCache <string> { 7class VideosPreviewCache extends AbstractVideoStaticFileCache <string> {
9 8
@@ -32,11 +31,11 @@ class VideosPreviewCache extends AbstractVideoStaticFileCache <string> {
32 31
33 if (video.isOwned()) throw new Error('Cannot load remote preview of owned video.') 32 if (video.isOwned()) throw new Error('Cannot load remote preview of owned video.')
34 33
35 // FIXME: use URL 34 const preview = video.getPreview()
36 const remoteStaticPath = join(STATIC_PATHS.PREVIEWS, video.getPreview().filename) 35 const destPath = join(FILES_CACHE.PREVIEWS.DIRECTORY, preview.filename)
37 const destPath = join(FILES_CACHE.PREVIEWS.DIRECTORY, video.getPreview().filename)
38 36
39 await fetchRemoteVideoStaticFile(video, remoteStaticPath, destPath) 37 const remoteUrl = preview.getFileUrl(video)
38 await doRequestAndSaveToFile({ uri: remoteUrl }, destPath)
40 39
41 return { isOwned: false, path: destPath } 40 return { isOwned: false, path: destPath }
42 } 41 }
diff --git a/server/lib/job-queue/handlers/activitypub-follow.ts b/server/lib/job-queue/handlers/activitypub-follow.ts
index 4a7cda0a2..7034c10d0 100644
--- a/server/lib/job-queue/handlers/activitypub-follow.ts
+++ b/server/lib/job-queue/handlers/activitypub-follow.ts
@@ -11,13 +11,7 @@ import { ActorModel } from '../../../models/activitypub/actor'
11import { Notifier } from '../../notifier' 11import { Notifier } from '../../notifier'
12import { sequelizeTypescript } from '../../../initializers/database' 12import { sequelizeTypescript } from '../../../initializers/database'
13import { MActor, MActorFollowActors, MActorFull } from '../../../typings/models' 13import { MActor, MActorFollowActors, MActorFull } from '../../../typings/models'
14 14import { ActivitypubFollowPayload } from '@shared/models'
15export type ActivitypubFollowPayload = {
16 followerActorId: number
17 name: string
18 host: string
19 isAutoFollow?: boolean
20}
21 15
22async function processActivityPubFollow (job: Bull.Job) { 16async function processActivityPubFollow (job: Bull.Job) {
23 const payload = job.data as ActivitypubFollowPayload 17 const payload = job.data as ActivitypubFollowPayload
@@ -34,6 +28,11 @@ async function processActivityPubFollow (job: Bull.Job) {
34 targetActor = await getOrCreateActorAndServerAndModel(actorUrl, 'all') 28 targetActor = await getOrCreateActorAndServerAndModel(actorUrl, 'all')
35 } 29 }
36 30
31 if (payload.assertIsChannel && !targetActor.VideoChannel) {
32 logger.warn('Do not follow %s@%s because it is not a channel.', payload.name, host)
33 return
34 }
35
37 const fromActor = await ActorModel.load(payload.followerActorId) 36 const fromActor = await ActorModel.load(payload.followerActorId)
38 37
39 return retryTransactionWrapper(follow, fromActor, targetActor, payload.isAutoFollow) 38 return retryTransactionWrapper(follow, fromActor, targetActor, payload.isAutoFollow)
diff --git a/server/lib/job-queue/handlers/activitypub-http-broadcast.ts b/server/lib/job-queue/handlers/activitypub-http-broadcast.ts
index 0ff7b44a0..e4d3dbbff 100644
--- a/server/lib/job-queue/handlers/activitypub-http-broadcast.ts
+++ b/server/lib/job-queue/handlers/activitypub-http-broadcast.ts
@@ -5,12 +5,7 @@ import { doRequest } from '../../../helpers/requests'
5import { buildGlobalHeaders, buildSignedRequestOptions, computeBody } from './utils/activitypub-http-utils' 5import { buildGlobalHeaders, buildSignedRequestOptions, computeBody } from './utils/activitypub-http-utils'
6import { BROADCAST_CONCURRENCY, JOB_REQUEST_TIMEOUT } from '../../../initializers/constants' 6import { BROADCAST_CONCURRENCY, JOB_REQUEST_TIMEOUT } from '../../../initializers/constants'
7import { ActorFollowScoreCache } from '../../files-cache' 7import { ActorFollowScoreCache } from '../../files-cache'
8 8import { ActivitypubHttpBroadcastPayload } from '@shared/models'
9export type ActivitypubHttpBroadcastPayload = {
10 uris: string[]
11 signatureActorId?: number
12 body: any
13}
14 9
15async function processActivityPubHttpBroadcast (job: Bull.Job) { 10async function processActivityPubHttpBroadcast (job: Bull.Job) {
16 logger.info('Processing ActivityPub broadcast in job %d.', job.id) 11 logger.info('Processing ActivityPub broadcast in job %d.', job.id)
diff --git a/server/lib/job-queue/handlers/activitypub-http-fetcher.ts b/server/lib/job-queue/handlers/activitypub-http-fetcher.ts
index 0182c5169..524aadc27 100644
--- a/server/lib/job-queue/handlers/activitypub-http-fetcher.ts
+++ b/server/lib/job-queue/handlers/activitypub-http-fetcher.ts
@@ -5,22 +5,15 @@ import { processActivities } from '../../activitypub/process'
5import { addVideoComments } from '../../activitypub/video-comments' 5import { addVideoComments } from '../../activitypub/video-comments'
6import { crawlCollectionPage } from '../../activitypub/crawl' 6import { crawlCollectionPage } from '../../activitypub/crawl'
7import { VideoModel } from '../../../models/video/video' 7import { VideoModel } from '../../../models/video/video'
8import { addVideoShares, createRates } from '../../activitypub' 8import { addVideoShares } from '../../activitypub/share'
9import { createRates } from '../../activitypub/video-rates'
9import { createAccountPlaylists } from '../../activitypub/playlist' 10import { createAccountPlaylists } from '../../activitypub/playlist'
10import { AccountModel } from '../../../models/account/account' 11import { AccountModel } from '../../../models/account/account'
11import { AccountVideoRateModel } from '../../../models/account/account-video-rate' 12import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
12import { VideoShareModel } from '../../../models/video/video-share' 13import { VideoShareModel } from '../../../models/video/video-share'
13import { VideoCommentModel } from '../../../models/video/video-comment' 14import { VideoCommentModel } from '../../../models/video/video-comment'
14import { MAccountDefault, MVideoFullLight } from '../../../typings/models' 15import { MAccountDefault, MVideoFullLight } from '../../../typings/models'
15 16import { ActivitypubHttpFetcherPayload, FetchType } from '@shared/models'
16type FetchType = 'activity' | 'video-likes' | 'video-dislikes' | 'video-shares' | 'video-comments' | 'account-playlists'
17
18export type ActivitypubHttpFetcherPayload = {
19 uri: string
20 type: FetchType
21 videoId?: number
22 accountId?: number
23}
24 17
25async function processActivityPubHttpFetcher (job: Bull.Job) { 18async function processActivityPubHttpFetcher (job: Bull.Job) {
26 logger.info('Processing ActivityPub fetcher in job %d.', job.id) 19 logger.info('Processing ActivityPub fetcher in job %d.', job.id)
diff --git a/server/lib/job-queue/handlers/activitypub-http-unicast.ts b/server/lib/job-queue/handlers/activitypub-http-unicast.ts
index c70ce3be9..b65eeb677 100644
--- a/server/lib/job-queue/handlers/activitypub-http-unicast.ts
+++ b/server/lib/job-queue/handlers/activitypub-http-unicast.ts
@@ -4,12 +4,7 @@ import { 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/constants' 5import { JOB_REQUEST_TIMEOUT } from '../../../initializers/constants'
6import { ActorFollowScoreCache } from '../../files-cache' 6import { ActorFollowScoreCache } from '../../files-cache'
7 7import { ActivitypubHttpUnicastPayload } from '@shared/models'
8export type ActivitypubHttpUnicastPayload = {
9 uri: string
10 signatureActorId?: number
11 body: any
12}
13 8
14async function processActivityPubHttpUnicast (job: Bull.Job) { 9async function processActivityPubHttpUnicast (job: Bull.Job) {
15 logger.info('Processing ActivityPub unicast in job %d.', job.id) 10 logger.info('Processing ActivityPub unicast in job %d.', job.id)
diff --git a/server/lib/job-queue/handlers/activitypub-refresher.ts b/server/lib/job-queue/handlers/activitypub-refresher.ts
index 4d6c38cfa..666e56868 100644
--- a/server/lib/job-queue/handlers/activitypub-refresher.ts
+++ b/server/lib/job-queue/handlers/activitypub-refresher.ts
@@ -1,14 +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 { refreshActorIfNeeded, refreshVideoIfNeeded, refreshVideoPlaylistIfNeeded } from '../../activitypub' 4import { refreshActorIfNeeded } from '../../activitypub/actor'
5import { refreshVideoIfNeeded } from '../../activitypub/videos'
5import { ActorModel } from '../../../models/activitypub/actor' 6import { ActorModel } from '../../../models/activitypub/actor'
6import { VideoPlaylistModel } from '../../../models/video/video-playlist' 7import { VideoPlaylistModel } from '../../../models/video/video-playlist'
7 8import { RefreshPayload } from '@shared/models'
8export type RefreshPayload = { 9import { refreshVideoPlaylistIfNeeded } from '@server/lib/activitypub/playlist'
9 type: 'video' | 'video-playlist' | 'actor'
10 url: string
11}
12 10
13async function refreshAPObject (job: Bull.Job) { 11async function refreshAPObject (job: Bull.Job) {
14 const payload = job.data as RefreshPayload 12 const payload = job.data as RefreshPayload
diff --git a/server/lib/job-queue/handlers/email.ts b/server/lib/job-queue/handlers/email.ts
index 62701222c..3157731e2 100644
--- a/server/lib/job-queue/handlers/email.ts
+++ b/server/lib/job-queue/handlers/email.ts
@@ -1,8 +1,7 @@
1import * as Bull from 'bull' 1import * as Bull from 'bull'
2import { logger } from '../../../helpers/logger' 2import { logger } from '../../../helpers/logger'
3import { Emailer, SendEmailOptions } from '../../emailer' 3import { Emailer } from '../../emailer'
4 4import { EmailPayload } from '@shared/models'
5export type EmailPayload = SendEmailOptions
6 5
7async function processEmail (job: Bull.Job) { 6async function processEmail (job: Bull.Job) {
8 const payload = job.data as EmailPayload 7 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 d3bde6e6a..bcb49a731 100644
--- a/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts
+++ b/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts
@@ -1,11 +1,12 @@
1import { buildSignedActivity } from '../../../../helpers/activitypub' 1import { buildSignedActivity } from '../../../../helpers/activitypub'
2import { getServerActor } from '../../../../helpers/utils'
3import { ActorModel } from '../../../../models/activitypub/actor' 2import { ActorModel } from '../../../../models/activitypub/actor'
4import { sha256 } from '../../../../helpers/core-utils' 3import { ACTIVITY_PUB, HTTP_SIGNATURE } from '../../../../initializers/constants'
5import { HTTP_SIGNATURE } from '../../../../initializers/constants'
6import { MActor } from '../../../../typings/models' 4import { MActor } from '../../../../typings/models'
5import { getServerActor } from '@server/models/application/application'
6import { buildDigest } from '@server/helpers/peertube-crypto'
7import { ContextType } from '@shared/models/activitypub/context'
7 8
8type Payload = { body: any, signatureActorId?: number } 9type Payload = { body: any, contextType?: ContextType, signatureActorId?: number }
9 10
10async function computeBody (payload: Payload) { 11async function computeBody (payload: Payload) {
11 let body = payload.body 12 let body = payload.body
@@ -13,7 +14,7 @@ async function computeBody (payload: Payload) {
13 if (payload.signatureActorId) { 14 if (payload.signatureActorId) {
14 const actorSignature = await ActorModel.load(payload.signatureActorId) 15 const actorSignature = await ActorModel.load(payload.signatureActorId)
15 if (!actorSignature) throw new Error('Unknown signature actor id.') 16 if (!actorSignature) throw new Error('Unknown signature actor id.')
16 body = await buildSignedActivity(actorSignature, payload.body) 17 body = await buildSignedActivity(actorSignature, payload.body, payload.contextType)
17 } 18 }
18 19
19 return body 20 return body
@@ -42,18 +43,13 @@ async function buildSignedRequestOptions (payload: Payload) {
42 43
43function buildGlobalHeaders (body: any) { 44function buildGlobalHeaders (body: any) {
44 return { 45 return {
45 'Digest': buildDigest(body) 46 'Digest': buildDigest(body),
47 'Content-Type': 'application/activity+json',
48 'Accept': ACTIVITY_PUB.ACCEPT_HEADER
46 } 49 }
47} 50}
48 51
49function buildDigest (body: any) {
50 const rawBody = typeof body === 'string' ? body : JSON.stringify(body)
51
52 return 'SHA-256=' + sha256(rawBody, 'base64')
53}
54
55export { 52export {
56 buildDigest,
57 buildGlobalHeaders, 53 buildGlobalHeaders,
58 computeBody, 54 computeBody,
59 buildSignedRequestOptions 55 buildSignedRequestOptions
diff --git a/server/lib/job-queue/handlers/video-file-import.ts b/server/lib/job-queue/handlers/video-file-import.ts
index 99c991e72..ae11f1de3 100644
--- a/server/lib/job-queue/handlers/video-file-import.ts
+++ b/server/lib/job-queue/handlers/video-file-import.ts
@@ -9,11 +9,7 @@ import { extname } from 'path'
9import { MVideoFile, MVideoWithFile } from '@server/typings/models' 9import { MVideoFile, MVideoWithFile } from '@server/typings/models'
10import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' 10import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
11import { getVideoFilePath } from '@server/lib/video-paths' 11import { getVideoFilePath } from '@server/lib/video-paths'
12 12import { VideoFileImportPayload } from '@shared/models'
13export type VideoFileImportPayload = {
14 videoUUID: string,
15 filePath: string
16}
17 13
18async function processVideoFileImport (job: Bull.Job) { 14async function processVideoFileImport (job: Bull.Job) {
19 const payload = job.data as VideoFileImportPayload 15 const payload = job.data as VideoFileImportPayload
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts
index 1fca17584..ad549c6fc 100644
--- a/server/lib/job-queue/handlers/video-import.ts
+++ b/server/lib/job-queue/handlers/video-import.ts
@@ -7,9 +7,8 @@ import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } fro
7import { extname } from 'path' 7import { extname } from 'path'
8import { VideoFileModel } from '../../../models/video/video-file' 8import { VideoFileModel } from '../../../models/video/video-file'
9import { VIDEO_IMPORT_TIMEOUT } from '../../../initializers/constants' 9import { VIDEO_IMPORT_TIMEOUT } from '../../../initializers/constants'
10import { VideoState } from '../../../../shared' 10import { VideoImportPayload, VideoImportTorrentPayload, VideoImportYoutubeDLPayload, VideoState } from '../../../../shared'
11import { JobQueue } from '../index' 11import { federateVideoIfNeeded } from '../../activitypub/videos'
12import { federateVideoIfNeeded } from '../../activitypub'
13import { VideoModel } from '../../../models/video/video' 12import { VideoModel } from '../../../models/video/video'
14import { createTorrentAndSetInfoHash, downloadWebTorrentVideo } from '../../../helpers/webtorrent' 13import { createTorrentAndSetInfoHash, downloadWebTorrentVideo } from '../../../helpers/webtorrent'
15import { getSecureTorrentName } from '../../../helpers/utils' 14import { getSecureTorrentName } from '../../../helpers/utils'
@@ -17,27 +16,12 @@ import { move, remove, stat } from 'fs-extra'
17import { Notifier } from '../../notifier' 16import { Notifier } from '../../notifier'
18import { CONFIG } from '../../../initializers/config' 17import { CONFIG } from '../../../initializers/config'
19import { sequelizeTypescript } from '../../../initializers/database' 18import { sequelizeTypescript } from '../../../initializers/database'
20import { createVideoMiniatureFromUrl, generateVideoMiniature } from '../../thumbnail' 19import { generateVideoMiniature } from '../../thumbnail'
21import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' 20import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
22import { MThumbnail } from '../../../typings/models/video/thumbnail' 21import { MThumbnail } from '../../../typings/models/video/thumbnail'
23import { MVideoImportDefault, MVideoImportDefaultFiles, MVideoImportVideo } from '@server/typings/models/video/video-import' 22import { MVideoImportDefault, MVideoImportDefaultFiles, MVideoImportVideo } from '@server/typings/models/video/video-import'
24import { getVideoFilePath } from '@server/lib/video-paths' 23import { getVideoFilePath } from '@server/lib/video-paths'
25 24import { addOptimizeOrMergeAudioJob } from '@server/helpers/video'
26type VideoImportYoutubeDLPayload = {
27 type: 'youtube-dl'
28 videoImportId: number
29
30 thumbnailUrl: string
31 downloadThumbnail: boolean
32 downloadPreview: boolean
33}
34
35type VideoImportTorrentPayload = {
36 type: 'magnet-uri' | 'torrent-file'
37 videoImportId: number
38}
39
40export type VideoImportPayload = VideoImportYoutubeDLPayload | VideoImportTorrentPayload
41 25
42async function processVideoImport (job: Bull.Job) { 26async function processVideoImport (job: Bull.Job) {
43 const payload = job.data as VideoImportPayload 27 const payload = job.data as VideoImportPayload
@@ -62,9 +46,6 @@ async function processTorrentImport (job: Bull.Job, payload: VideoImportTorrentP
62 const options = { 46 const options = {
63 videoImportId: payload.videoImportId, 47 videoImportId: payload.videoImportId,
64 48
65 downloadThumbnail: false,
66 downloadPreview: false,
67
68 generateThumbnail: true, 49 generateThumbnail: true,
69 generatePreview: true 50 generatePreview: true
70 } 51 }
@@ -82,15 +63,11 @@ async function processYoutubeDLImport (job: Bull.Job, payload: VideoImportYoutub
82 const options = { 63 const options = {
83 videoImportId: videoImport.id, 64 videoImportId: videoImport.id,
84 65
85 downloadThumbnail: payload.downloadThumbnail, 66 generateThumbnail: payload.generateThumbnail,
86 downloadPreview: payload.downloadPreview, 67 generatePreview: payload.generatePreview
87 thumbnailUrl: payload.thumbnailUrl,
88
89 generateThumbnail: false,
90 generatePreview: false
91 } 68 }
92 69
93 return processFile(() => downloadYoutubeDLVideo(videoImport.targetUrl, VIDEO_IMPORT_TIMEOUT), videoImport, options) 70 return processFile(() => downloadYoutubeDLVideo(videoImport.targetUrl, payload.fileExt, VIDEO_IMPORT_TIMEOUT), videoImport, options)
94} 71}
95 72
96async function getVideoImportOrDie (videoImportId: number) { 73async function getVideoImportOrDie (videoImportId: number) {
@@ -105,10 +82,6 @@ async function getVideoImportOrDie (videoImportId: number) {
105type ProcessFileOptions = { 82type ProcessFileOptions = {
106 videoImportId: number 83 videoImportId: number
107 84
108 downloadThumbnail: boolean
109 downloadPreview: boolean
110 thumbnailUrl?: string
111
112 generateThumbnail: boolean 85 generateThumbnail: boolean
113 generatePreview: boolean 86 generatePreview: boolean
114} 87}
@@ -153,17 +126,13 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
153 126
154 // Process thumbnail 127 // Process thumbnail
155 let thumbnailModel: MThumbnail 128 let thumbnailModel: MThumbnail
156 if (options.downloadThumbnail && options.thumbnailUrl) { 129 if (options.generateThumbnail) {
157 thumbnailModel = await createVideoMiniatureFromUrl(options.thumbnailUrl, videoImportWithFiles.Video, ThumbnailType.MINIATURE)
158 } else if (options.generateThumbnail || options.downloadThumbnail) {
159 thumbnailModel = await generateVideoMiniature(videoImportWithFiles.Video, videoFile, ThumbnailType.MINIATURE) 130 thumbnailModel = await generateVideoMiniature(videoImportWithFiles.Video, videoFile, ThumbnailType.MINIATURE)
160 } 131 }
161 132
162 // Process preview 133 // Process preview
163 let previewModel: MThumbnail 134 let previewModel: MThumbnail
164 if (options.downloadPreview && options.thumbnailUrl) { 135 if (options.generatePreview) {
165 previewModel = await createVideoMiniatureFromUrl(options.thumbnailUrl, videoImportWithFiles.Video, ThumbnailType.PREVIEW)
166 } else if (options.generatePreview || options.downloadPreview) {
167 previewModel = await generateVideoMiniature(videoImportWithFiles.Video, videoFile, ThumbnailType.PREVIEW) 136 previewModel = await generateVideoMiniature(videoImportWithFiles.Video, videoFile, ThumbnailType.PREVIEW)
168 } 137 }
169 138
@@ -214,14 +183,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
214 183
215 // Create transcoding jobs? 184 // Create transcoding jobs?
216 if (video.state === VideoState.TO_TRANSCODE) { 185 if (video.state === VideoState.TO_TRANSCODE) {
217 // Put uuid because we don't have id auto incremented for now 186 await addOptimizeOrMergeAudioJob(videoImportUpdated.Video, videoFile)
218 const dataInput = {
219 type: 'optimize' as 'optimize',
220 videoUUID: videoImportUpdated.Video.uuid,
221 isNewVideo: true
222 }
223
224 await JobQueue.Instance.createJob({ type: 'video-transcoding', payload: dataInput })
225 } 187 }
226 188
227 } catch (err) { 189 } catch (err) {
diff --git a/server/lib/job-queue/handlers/video-redundancy.ts b/server/lib/job-queue/handlers/video-redundancy.ts
new file mode 100644
index 000000000..6296dab05
--- /dev/null
+++ b/server/lib/job-queue/handlers/video-redundancy.ts
@@ -0,0 +1,17 @@
1import * as Bull from 'bull'
2import { logger } from '../../../helpers/logger'
3import { VideosRedundancyScheduler } from '@server/lib/schedulers/videos-redundancy-scheduler'
4import { VideoRedundancyPayload } from '@shared/models'
5
6async function processVideoRedundancy (job: Bull.Job) {
7 const payload = job.data as VideoRedundancyPayload
8 logger.info('Processing video redundancy in job %d.', job.id)
9
10 return VideosRedundancyScheduler.Instance.createManualRedundancy(payload.videoId)
11}
12
13// ---------------------------------------------------------------------------
14
15export {
16 processVideoRedundancy
17}
diff --git a/server/lib/job-queue/handlers/video-transcoding.ts b/server/lib/job-queue/handlers/video-transcoding.ts
index 39b9fac98..46d52e1cf 100644
--- a/server/lib/job-queue/handlers/video-transcoding.ts
+++ b/server/lib/job-queue/handlers/video-transcoding.ts
@@ -1,48 +1,22 @@
1import * as Bull from 'bull' 1import * as Bull from 'bull'
2import { VideoResolution } from '../../../../shared' 2import {
3 MergeAudioTranscodingPayload,
4 NewResolutionTranscodingPayload,
5 OptimizeTranscodingPayload,
6 VideoTranscodingPayload
7} from '../../../../shared'
3import { logger } from '../../../helpers/logger' 8import { logger } from '../../../helpers/logger'
4import { VideoModel } from '../../../models/video/video' 9import { VideoModel } from '../../../models/video/video'
5import { JobQueue } from '../job-queue' 10import { JobQueue } from '../job-queue'
6import { federateVideoIfNeeded } from '../../activitypub' 11import { federateVideoIfNeeded } from '../../activitypub/videos'
7import { retryTransactionWrapper } from '../../../helpers/database-utils' 12import { retryTransactionWrapper } from '../../../helpers/database-utils'
8import { sequelizeTypescript } from '../../../initializers' 13import { sequelizeTypescript } from '../../../initializers/database'
9import * as Bluebird from 'bluebird'
10import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils' 14import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils'
11import { generateHlsPlaylist, mergeAudioVideofile, optimizeOriginalVideofile, transcodeNewResolution } from '../../video-transcoding' 15import { generateHlsPlaylist, mergeAudioVideofile, optimizeOriginalVideofile, transcodeNewResolution } from '../../video-transcoding'
12import { Notifier } from '../../notifier' 16import { Notifier } from '../../notifier'
13import { CONFIG } from '../../../initializers/config' 17import { CONFIG } from '../../../initializers/config'
14import { MVideoFullLight, MVideoUUID, MVideoWithFile } from '@server/typings/models' 18import { MVideoFullLight, MVideoUUID, MVideoWithFile } from '@server/typings/models'
15 19
16interface BaseTranscodingPayload {
17 videoUUID: string
18 isNewVideo?: boolean
19}
20
21interface HLSTranscodingPayload extends BaseTranscodingPayload {
22 type: 'hls'
23 isPortraitMode?: boolean
24 resolution: VideoResolution
25 copyCodecs: boolean
26}
27
28interface NewResolutionTranscodingPayload extends BaseTranscodingPayload {
29 type: 'new-resolution'
30 isPortraitMode?: boolean
31 resolution: VideoResolution
32}
33
34interface MergeAudioTranscodingPayload extends BaseTranscodingPayload {
35 type: 'merge-audio'
36 resolution: VideoResolution
37}
38
39interface OptimizeTranscodingPayload extends BaseTranscodingPayload {
40 type: 'optimize'
41}
42
43export type VideoTranscodingPayload = HLSTranscodingPayload | NewResolutionTranscodingPayload
44 | OptimizeTranscodingPayload | MergeAudioTranscodingPayload
45
46async function processVideoTranscoding (job: Bull.Job) { 20async function processVideoTranscoding (job: Bull.Job) {
47 const payload = job.data as VideoTranscodingPayload 21 const payload = job.data as VideoTranscodingPayload
48 logger.info('Processing video file in job %d.', job.id) 22 logger.info('Processing video file in job %d.', job.id)
@@ -105,7 +79,7 @@ async function onVideoFileOptimizerSuccess (videoArg: MVideoWithFile, payload: O
105 79
106 const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => { 80 const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => {
107 // Maybe the video changed in database, refresh it 81 // Maybe the video changed in database, refresh it
108 let videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoArg.uuid, t) 82 const videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoArg.uuid, t)
109 // Video does not exist anymore 83 // Video does not exist anymore
110 if (!videoDatabase) return undefined 84 if (!videoDatabase) return undefined
111 85
@@ -118,12 +92,11 @@ async function onVideoFileOptimizerSuccess (videoArg: MVideoWithFile, payload: O
118 92
119 let videoPublished = false 93 let videoPublished = false
120 94
95 // Generate HLS version of the max quality file
121 const hlsPayload = Object.assign({}, payload, { resolution: videoDatabase.getMaxQualityFile().resolution }) 96 const hlsPayload = Object.assign({}, payload, { resolution: videoDatabase.getMaxQualityFile().resolution })
122 await createHlsJobIfEnabled(hlsPayload) 97 await createHlsJobIfEnabled(hlsPayload)
123 98
124 if (resolutionsEnabled.length !== 0) { 99 if (resolutionsEnabled.length !== 0) {
125 const tasks: (Bluebird<Bull.Job<any>> | Promise<Bull.Job<any>>)[] = []
126
127 for (const resolution of resolutionsEnabled) { 100 for (const resolution of resolutionsEnabled) {
128 let dataInput: VideoTranscodingPayload 101 let dataInput: VideoTranscodingPayload
129 102
@@ -143,12 +116,9 @@ async function onVideoFileOptimizerSuccess (videoArg: MVideoWithFile, payload: O
143 } 116 }
144 } 117 }
145 118
146 const p = JobQueue.Instance.createJob({ type: 'video-transcoding', payload: dataInput }) 119 JobQueue.Instance.createJob({ type: 'video-transcoding', payload: dataInput })
147 tasks.push(p)
148 } 120 }
149 121
150 await Promise.all(tasks)
151
152 logger.info('Transcoding jobs created for uuid %s.', videoDatabase.uuid, { resolutionsEnabled }) 122 logger.info('Transcoding jobs created for uuid %s.', videoDatabase.uuid, { resolutionsEnabled })
153 } else { 123 } else {
154 // No transcoding to do, it's now published 124 // No transcoding to do, it's now published
diff --git a/server/lib/job-queue/handlers/video-views.ts b/server/lib/job-queue/handlers/video-views.ts
index 73fa5ed04..7211df237 100644
--- a/server/lib/job-queue/handlers/video-views.ts
+++ b/server/lib/job-queue/handlers/video-views.ts
@@ -3,7 +3,7 @@ import { logger } from '../../../helpers/logger'
3import { VideoModel } from '../../../models/video/video' 3import { VideoModel } from '../../../models/video/video'
4import { VideoViewModel } from '../../../models/video/video-views' 4import { VideoViewModel } from '../../../models/video/video-views'
5import { isTestInstance } from '../../../helpers/core-utils' 5import { isTestInstance } from '../../../helpers/core-utils'
6import { federateVideoIfNeeded } from '../../activitypub' 6import { federateVideoIfNeeded } from '../../activitypub/videos'
7 7
8async function processVideosViews () { 8async function processVideosViews () {
9 const lastHour = new Date() 9 const lastHour = new Date()
@@ -23,6 +23,8 @@ async function processVideosViews () {
23 for (const videoId of videoIds) { 23 for (const videoId of videoIds) {
24 try { 24 try {
25 const views = await Redis.Instance.getVideoViews(videoId, hour) 25 const views = await Redis.Instance.getVideoViews(videoId, hour)
26 await Redis.Instance.deleteVideoViews(videoId, hour)
27
26 if (views) { 28 if (views) {
27 logger.debug('Adding %d views to video %d in hour %d.', views, videoId, hour) 29 logger.debug('Adding %d views to video %d in hour %d.', views, videoId, hour)
28 30
@@ -52,8 +54,6 @@ async function processVideosViews () {
52 logger.error('Cannot create video views for video %d in hour %d.', videoId, hour, { err }) 54 logger.error('Cannot create video views for video %d in hour %d.', videoId, hour, { err })
53 } 55 }
54 } 56 }
55
56 await Redis.Instance.deleteVideoViews(videoId, hour)
57 } catch (err) { 57 } catch (err) {
58 logger.error('Cannot update video views of video %d in hour %d.', videoId, hour, { err }) 58 logger.error('Cannot update video views of video %d in hour %d.', videoId, hour, { err })
59 } 59 }
diff --git a/server/lib/job-queue/job-queue.ts b/server/lib/job-queue/job-queue.ts
index ec601e9ea..14e181835 100644
--- a/server/lib/job-queue/job-queue.ts
+++ b/server/lib/job-queue/job-queue.ts
@@ -1,18 +1,32 @@
1import * as Bull from 'bull' 1import * as Bull from 'bull'
2import { JobState, JobType } from '../../../shared/models' 2import {
3 ActivitypubFollowPayload,
4 ActivitypubHttpBroadcastPayload,
5 ActivitypubHttpFetcherPayload,
6 ActivitypubHttpUnicastPayload,
7 EmailPayload,
8 JobState,
9 JobType,
10 RefreshPayload,
11 VideoFileImportPayload,
12 VideoImportPayload,
13 VideoRedundancyPayload,
14 VideoTranscodingPayload
15} from '../../../shared/models'
3import { logger } from '../../helpers/logger' 16import { logger } from '../../helpers/logger'
4import { Redis } from '../redis' 17import { Redis } from '../redis'
5import { JOB_ATTEMPTS, JOB_COMPLETED_LIFETIME, JOB_CONCURRENCY, JOB_TTL, REPEAT_JOBS, WEBSERVER } from '../../initializers/constants' 18import { JOB_ATTEMPTS, JOB_COMPLETED_LIFETIME, JOB_CONCURRENCY, JOB_TTL, REPEAT_JOBS, WEBSERVER } from '../../initializers/constants'
6import { ActivitypubHttpBroadcastPayload, processActivityPubHttpBroadcast } from './handlers/activitypub-http-broadcast' 19import { processActivityPubHttpBroadcast } from './handlers/activitypub-http-broadcast'
7import { ActivitypubHttpFetcherPayload, processActivityPubHttpFetcher } from './handlers/activitypub-http-fetcher' 20import { processActivityPubHttpFetcher } from './handlers/activitypub-http-fetcher'
8import { ActivitypubHttpUnicastPayload, processActivityPubHttpUnicast } from './handlers/activitypub-http-unicast' 21import { processActivityPubHttpUnicast } from './handlers/activitypub-http-unicast'
9import { EmailPayload, processEmail } from './handlers/email' 22import { processEmail } from './handlers/email'
10import { processVideoTranscoding, VideoTranscodingPayload } from './handlers/video-transcoding' 23import { processVideoTranscoding } from './handlers/video-transcoding'
11import { ActivitypubFollowPayload, processActivityPubFollow } from './handlers/activitypub-follow' 24import { processActivityPubFollow } from './handlers/activitypub-follow'
12import { processVideoImport, VideoImportPayload } from './handlers/video-import' 25import { processVideoImport } from './handlers/video-import'
13import { processVideosViews } from './handlers/video-views' 26import { processVideosViews } from './handlers/video-views'
14import { refreshAPObject, RefreshPayload } from './handlers/activitypub-refresher' 27import { refreshAPObject } from './handlers/activitypub-refresher'
15import { processVideoFileImport, VideoFileImportPayload } from './handlers/video-file-import' 28import { processVideoFileImport } from './handlers/video-file-import'
29import { processVideoRedundancy } from '@server/lib/job-queue/handlers/video-redundancy'
16 30
17type CreateJobArgument = 31type CreateJobArgument =
18 { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } | 32 { type: 'activitypub-http-broadcast', payload: ActivitypubHttpBroadcastPayload } |
@@ -24,20 +38,21 @@ type CreateJobArgument =
24 { type: 'email', payload: EmailPayload } | 38 { type: 'email', payload: EmailPayload } |
25 { type: 'video-import', payload: VideoImportPayload } | 39 { type: 'video-import', payload: VideoImportPayload } |
26 { type: 'activitypub-refresher', payload: RefreshPayload } | 40 { type: 'activitypub-refresher', payload: RefreshPayload } |
27 { type: 'videos-views', payload: {} } 41 { type: 'videos-views', payload: {} } |
42 { type: 'video-redundancy', payload: VideoRedundancyPayload }
28 43
29const handlers: { [ id in (JobType | 'video-file') ]: (job: Bull.Job) => Promise<any>} = { 44const handlers: { [id in JobType]: (job: Bull.Job) => Promise<any> } = {
30 'activitypub-http-broadcast': processActivityPubHttpBroadcast, 45 'activitypub-http-broadcast': processActivityPubHttpBroadcast,
31 'activitypub-http-unicast': processActivityPubHttpUnicast, 46 'activitypub-http-unicast': processActivityPubHttpUnicast,
32 'activitypub-http-fetcher': processActivityPubHttpFetcher, 47 'activitypub-http-fetcher': processActivityPubHttpFetcher,
33 'activitypub-follow': processActivityPubFollow, 48 'activitypub-follow': processActivityPubFollow,
34 'video-file-import': processVideoFileImport, 49 'video-file-import': processVideoFileImport,
35 'video-transcoding': processVideoTranscoding, 50 'video-transcoding': processVideoTranscoding,
36 'video-file': processVideoTranscoding, // TODO: remove it (changed in 1.3)
37 'email': processEmail, 51 'email': processEmail,
38 'video-import': processVideoImport, 52 'video-import': processVideoImport,
39 'videos-views': processVideosViews, 53 'videos-views': processVideosViews,
40 'activitypub-refresher': refreshAPObject 54 'activitypub-refresher': refreshAPObject,
55 'video-redundancy': processVideoRedundancy
41} 56}
42 57
43const jobTypes: JobType[] = [ 58const jobTypes: JobType[] = [
@@ -50,20 +65,22 @@ const jobTypes: JobType[] = [
50 'video-file-import', 65 'video-file-import',
51 'video-import', 66 'video-import',
52 'videos-views', 67 'videos-views',
53 'activitypub-refresher' 68 'activitypub-refresher',
69 'video-redundancy'
54] 70]
55 71
56class JobQueue { 72class JobQueue {
57 73
58 private static instance: JobQueue 74 private static instance: JobQueue
59 75
60 private queues: { [ id in JobType ]?: Bull.Queue } = {} 76 private queues: { [id in JobType]?: Bull.Queue } = {}
61 private initialized = false 77 private initialized = false
62 private jobRedisPrefix: string 78 private jobRedisPrefix: string
63 79
64 private constructor () {} 80 private constructor () {
81 }
65 82
66 async init () { 83 init () {
67 // Already initialized 84 // Already initialized
68 if (this.initialized === true) return 85 if (this.initialized === true) return
69 this.initialized = true 86 this.initialized = true
@@ -105,11 +122,16 @@ class JobQueue {
105 } 122 }
106 } 123 }
107 124
108 createJob (obj: CreateJobArgument) { 125 createJob (obj: CreateJobArgument): void {
126 this.createJobWithPromise(obj)
127 .catch(err => logger.error('Cannot create job.', { err, obj }))
128 }
129
130 createJobWithPromise (obj: CreateJobArgument) {
109 const queue = this.queues[obj.type] 131 const queue = this.queues[obj.type]
110 if (queue === undefined) { 132 if (queue === undefined) {
111 logger.error('Unknown queue %s: cannot create job.', obj.type) 133 logger.error('Unknown queue %s: cannot create job.', obj.type)
112 throw Error('Unknown queue, cannot create job') 134 return
113 } 135 }
114 136
115 const jobArgs: Bull.JobOptions = { 137 const jobArgs: Bull.JobOptions = {
@@ -122,10 +144,10 @@ class JobQueue {
122 } 144 }
123 145
124 async listForApi (options: { 146 async listForApi (options: {
125 state: JobState, 147 state: JobState
126 start: number, 148 start: number
127 count: number, 149 count: number
128 asc?: boolean, 150 asc?: boolean
129 jobType: JobType 151 jobType: JobType
130 }): Promise<Bull.Job[]> { 152 }): Promise<Bull.Job[]> {
131 const { state, start, count, asc, jobType } = options 153 const { state, start, count, asc, jobType } = options
@@ -133,16 +155,14 @@ class JobQueue {
133 155
134 const filteredJobTypes = this.filterJobTypes(jobType) 156 const filteredJobTypes = this.filterJobTypes(jobType)
135 157
136 // TODO: optimize
137 for (const jobType of filteredJobTypes) { 158 for (const jobType of filteredJobTypes) {
138 const queue = this.queues[ jobType ] 159 const queue = this.queues[jobType]
139 if (queue === undefined) { 160 if (queue === undefined) {
140 logger.error('Unknown queue %s to list jobs.', jobType) 161 logger.error('Unknown queue %s to list jobs.', jobType)
141 continue 162 continue
142 } 163 }
143 164
144 // FIXME: Bull queue typings does not have getJobs method 165 const jobs = await queue.getJobs([ state ], 0, start + count, asc)
145 const jobs = await (queue as any).getJobs(state, 0, start + count, asc)
146 results = results.concat(jobs) 166 results = results.concat(jobs)
147 } 167 }
148 168
@@ -164,7 +184,7 @@ class JobQueue {
164 const filteredJobTypes = this.filterJobTypes(jobType) 184 const filteredJobTypes = this.filterJobTypes(jobType)
165 185
166 for (const type of filteredJobTypes) { 186 for (const type of filteredJobTypes) {
167 const queue = this.queues[ type ] 187 const queue = this.queues[type]
168 if (queue === undefined) { 188 if (queue === undefined) {
169 logger.error('Unknown queue %s to count jobs.', type) 189 logger.error('Unknown queue %s to count jobs.', type)
170 continue 190 continue
@@ -172,7 +192,7 @@ class JobQueue {
172 192
173 const counts = await queue.getJobCounts() 193 const counts = await queue.getJobCounts()
174 194
175 total += counts[ state ] 195 total += counts[state]
176 } 196 }
177 197
178 return total 198 return total
@@ -188,7 +208,7 @@ class JobQueue {
188 private addRepeatableJobs () { 208 private addRepeatableJobs () {
189 this.queues['videos-views'].add({}, { 209 this.queues['videos-views'].add({}, {
190 repeat: REPEAT_JOBS['videos-views'] 210 repeat: REPEAT_JOBS['videos-views']
191 }) 211 }).catch(err => logger.error('Cannot add repeatable job.', { err }))
192 } 212 }
193 213
194 private filterJobTypes (jobType?: JobType) { 214 private filterJobTypes (jobType?: JobType) {
diff --git a/server/lib/moderation.ts b/server/lib/moderation.ts
index b609f4585..55f7a985d 100644
--- a/server/lib/moderation.ts
+++ b/server/lib/moderation.ts
@@ -15,41 +15,41 @@ export type AcceptResult = {
15 15
16// Can be filtered by plugins 16// Can be filtered by plugins
17function isLocalVideoAccepted (object: { 17function isLocalVideoAccepted (object: {
18 videoBody: VideoCreate, 18 videoBody: VideoCreate
19 videoFile: Express.Multer.File & { duration?: number }, 19 videoFile: Express.Multer.File & { duration?: number }
20 user: UserModel 20 user: UserModel
21}): AcceptResult { 21}): AcceptResult {
22 return { accepted: true } 22 return { accepted: true }
23} 23}
24 24
25function isLocalVideoThreadAccepted (_object: { 25function isLocalVideoThreadAccepted (_object: {
26 commentBody: VideoCommentCreate, 26 commentBody: VideoCommentCreate
27 video: VideoModel, 27 video: VideoModel
28 user: UserModel 28 user: UserModel
29}): AcceptResult { 29}): AcceptResult {
30 return { accepted: true } 30 return { accepted: true }
31} 31}
32 32
33function isLocalVideoCommentReplyAccepted (_object: { 33function isLocalVideoCommentReplyAccepted (_object: {
34 commentBody: VideoCommentCreate, 34 commentBody: VideoCommentCreate
35 parentComment: VideoCommentModel, 35 parentComment: VideoCommentModel
36 video: VideoModel, 36 video: VideoModel
37 user: UserModel 37 user: UserModel
38}): AcceptResult { 38}): AcceptResult {
39 return { accepted: true } 39 return { accepted: true }
40} 40}
41 41
42function isRemoteVideoAccepted (_object: { 42function isRemoteVideoAccepted (_object: {
43 activity: ActivityCreate, 43 activity: ActivityCreate
44 videoAP: VideoTorrentObject, 44 videoAP: VideoTorrentObject
45 byActor: ActorModel 45 byActor: ActorModel
46}): AcceptResult { 46}): AcceptResult {
47 return { accepted: true } 47 return { accepted: true }
48} 48}
49 49
50function isRemoteVideoCommentAccepted (_object: { 50function isRemoteVideoCommentAccepted (_object: {
51 activity: ActivityCreate, 51 activity: ActivityCreate
52 commentAP: VideoCommentObject, 52 commentAP: VideoCommentObject
53 byActor: ActorModel 53 byActor: ActorModel
54}): AcceptResult { 54}): AcceptResult {
55 return { accepted: true } 55 return { accepted: true }
diff --git a/server/lib/notifier.ts b/server/lib/notifier.ts
index 679b9bcf6..017739523 100644
--- a/server/lib/notifier.ts
+++ b/server/lib/notifier.ts
@@ -5,8 +5,7 @@ import { UserNotificationModel } from '../models/account/user-notification'
5import { UserModel } from '../models/account/user' 5import { UserModel } from '../models/account/user'
6import { PeerTubeSocket } from './peertube-socket' 6import { PeerTubeSocket } from './peertube-socket'
7import { CONFIG } from '../initializers/config' 7import { CONFIG } from '../initializers/config'
8import { VideoPrivacy, VideoState } from '../../shared/models/videos' 8import { VideoPrivacy, VideoState, VideoAbuse } from '../../shared/models/videos'
9import * as Bluebird from 'bluebird'
10import { AccountBlocklistModel } from '../models/account/account-blocklist' 9import { AccountBlocklistModel } from '../models/account/account-blocklist'
11import { 10import {
12 MCommentOwnerVideo, 11 MCommentOwnerVideo,
@@ -17,7 +16,8 @@ import {
17 MVideoFullLight 16 MVideoFullLight
18} from '../typings/models/video' 17} from '../typings/models/video'
19import { 18import {
20 MUser, MUserAccount, 19 MUser,
20 MUserAccount,
21 MUserDefault, 21 MUserDefault,
22 MUserNotifSettingAccount, 22 MUserNotifSettingAccount,
23 MUserWithNotificationSetting, 23 MUserWithNotificationSetting,
@@ -26,20 +26,21 @@ import {
26import { MAccountDefault, MActorFollowFull } from '../typings/models' 26import { MAccountDefault, MActorFollowFull } from '../typings/models'
27import { MVideoImportVideo } from '@server/typings/models/video/video-import' 27import { MVideoImportVideo } from '@server/typings/models/video/video-import'
28import { ServerBlocklistModel } from '@server/models/server/server-blocklist' 28import { ServerBlocklistModel } from '@server/models/server/server-blocklist'
29import { getServerActor } from '@server/helpers/utils' 29import { getServerActor } from '@server/models/application/application'
30 30
31class Notifier { 31class Notifier {
32 32
33 private static instance: Notifier 33 private static instance: Notifier
34 34
35 private constructor () {} 35 private constructor () {
36 }
36 37
37 notifyOnNewVideoIfNeeded (video: MVideoAccountLight): void { 38 notifyOnNewVideoIfNeeded (video: MVideoAccountLight): void {
38 // Only notify on public and published videos which are not blacklisted 39 // Only notify on public and published videos which are not blacklisted
39 if (video.privacy !== VideoPrivacy.PUBLIC || video.state !== VideoState.PUBLISHED || video.isBlacklisted()) return 40 if (video.privacy !== VideoPrivacy.PUBLIC || video.state !== VideoState.PUBLISHED || video.isBlacklisted()) return
40 41
41 this.notifySubscribersOfNewVideo(video) 42 this.notifySubscribersOfNewVideo(video)
42 .catch(err => logger.error('Cannot notify subscribers of new video %s.', video.url, { err })) 43 .catch(err => logger.error('Cannot notify subscribers of new video %s.', video.url, { err }))
43 } 44 }
44 45
45 notifyOnVideoPublishedAfterTranscoding (video: MVideoFullLight): void { 46 notifyOnVideoPublishedAfterTranscoding (video: MVideoFullLight): void {
@@ -63,7 +64,9 @@ class Notifier {
63 if (video.ScheduleVideoUpdate || (video.waitTranscoding && video.state !== VideoState.PUBLISHED)) return 64 if (video.ScheduleVideoUpdate || (video.waitTranscoding && video.state !== VideoState.PUBLISHED)) return
64 65
65 this.notifyOwnedVideoHasBeenPublished(video) 66 this.notifyOwnedVideoHasBeenPublished(video)
66 .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 67 .catch(err => {
68 logger.error('Cannot notify owner that its video %s has been published after removed from auto-blacklist.', video.url, { err })
69 })
67 } 70 }
68 71
69 notifyOnNewComment (comment: MCommentOwnerVideo): void { 72 notifyOnNewComment (comment: MCommentOwnerVideo): void {
@@ -74,19 +77,19 @@ class Notifier {
74 .catch(err => logger.error('Cannot notify mentions of comment %s.', comment.url, { err })) 77 .catch(err => logger.error('Cannot notify mentions of comment %s.', comment.url, { err }))
75 } 78 }
76 79
77 notifyOnNewVideoAbuse (videoAbuse: MVideoAbuseVideo): void { 80 notifyOnNewVideoAbuse (parameters: { videoAbuse: VideoAbuse, videoAbuseInstance: MVideoAbuseVideo, reporter: string }): void {
78 this.notifyModeratorsOfNewVideoAbuse(videoAbuse) 81 this.notifyModeratorsOfNewVideoAbuse(parameters)
79 .catch(err => logger.error('Cannot notify of new video abuse of video %s.', videoAbuse.Video.url, { err })) 82 .catch(err => logger.error('Cannot notify of new video abuse of video %s.', parameters.videoAbuseInstance.Video.url, { err }))
80 } 83 }
81 84
82 notifyOnVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo): void { 85 notifyOnVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo): void {
83 this.notifyModeratorsOfVideoAutoBlacklist(videoBlacklist) 86 this.notifyModeratorsOfVideoAutoBlacklist(videoBlacklist)
84 .catch(err => logger.error('Cannot notify of auto-blacklist of video %s.', videoBlacklist.Video.url, { err })) 87 .catch(err => logger.error('Cannot notify of auto-blacklist of video %s.', videoBlacklist.Video.url, { err }))
85 } 88 }
86 89
87 notifyOnVideoBlacklist (videoBlacklist: MVideoBlacklistVideo): void { 90 notifyOnVideoBlacklist (videoBlacklist: MVideoBlacklistVideo): void {
88 this.notifyVideoOwnerOfBlacklist(videoBlacklist) 91 this.notifyVideoOwnerOfBlacklist(videoBlacklist)
89 .catch(err => logger.error('Cannot notify video owner of new video blacklist of %s.', videoBlacklist.Video.url, { err })) 92 .catch(err => logger.error('Cannot notify video owner of new video blacklist of %s.', videoBlacklist.Video.url, { err }))
90 } 93 }
91 94
92 notifyOnVideoUnblacklist (video: MVideoFullLight): void { 95 notifyOnVideoUnblacklist (video: MVideoFullLight): void {
@@ -96,7 +99,7 @@ class Notifier {
96 99
97 notifyOnFinishedVideoImport (videoImport: MVideoImportVideo, success: boolean): void { 100 notifyOnFinishedVideoImport (videoImport: MVideoImportVideo, success: boolean): void {
98 this.notifyOwnerVideoImportIsFinished(videoImport, success) 101 this.notifyOwnerVideoImportIsFinished(videoImport, success)
99 .catch(err => logger.error('Cannot notify owner that its video import %s is finished.', videoImport.getTargetIdentifier(), { err })) 102 .catch(err => logger.error('Cannot notify owner that its video import %s is finished.', videoImport.getTargetIdentifier(), { err }))
100 } 103 }
101 104
102 notifyOnNewUserRegistration (user: MUserDefault): void { 105 notifyOnNewUserRegistration (user: MUserDefault): void {
@@ -106,14 +109,14 @@ class Notifier {
106 109
107 notifyOfNewUserFollow (actorFollow: MActorFollowFull): void { 110 notifyOfNewUserFollow (actorFollow: MActorFollowFull): void {
108 this.notifyUserOfNewActorFollow(actorFollow) 111 this.notifyUserOfNewActorFollow(actorFollow)
109 .catch(err => { 112 .catch(err => {
110 logger.error( 113 logger.error(
111 'Cannot notify owner of channel %s of a new follow by %s.', 114 'Cannot notify owner of channel %s of a new follow by %s.',
112 actorFollow.ActorFollowing.VideoChannel.getDisplayName(), 115 actorFollow.ActorFollowing.VideoChannel.getDisplayName(),
113 actorFollow.ActorFollower.Account.getDisplayName(), 116 actorFollow.ActorFollower.Account.getDisplayName(),
114 { err } 117 { err }
115 ) 118 )
116 }) 119 })
117 } 120 }
118 121
119 notifyOfNewInstanceFollow (actorFollow: MActorFollowFull): void { 122 notifyOfNewInstanceFollow (actorFollow: MActorFollowFull): void {
@@ -347,11 +350,15 @@ class Notifier {
347 return this.notify({ users: admins, settingGetter, notificationCreator, emailSender }) 350 return this.notify({ users: admins, settingGetter, notificationCreator, emailSender })
348 } 351 }
349 352
350 private async notifyModeratorsOfNewVideoAbuse (videoAbuse: MVideoAbuseVideo) { 353 private async notifyModeratorsOfNewVideoAbuse (parameters: {
354 videoAbuse: VideoAbuse
355 videoAbuseInstance: MVideoAbuseVideo
356 reporter: string
357 }) {
351 const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_ABUSES) 358 const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_ABUSES)
352 if (moderators.length === 0) return 359 if (moderators.length === 0) return
353 360
354 logger.info('Notifying %s user/moderators of new video abuse %s.', moderators.length, videoAbuse.Video.url) 361 logger.info('Notifying %s user/moderators of new video abuse %s.', moderators.length, parameters.videoAbuseInstance.Video.url)
355 362
356 function settingGetter (user: MUserWithNotificationSetting) { 363 function settingGetter (user: MUserWithNotificationSetting) {
357 return user.NotificationSetting.videoAbuseAsModerator 364 return user.NotificationSetting.videoAbuseAsModerator
@@ -361,15 +368,15 @@ class Notifier {
361 const notification: UserNotificationModelForApi = await UserNotificationModel.create<UserNotificationModelForApi>({ 368 const notification: UserNotificationModelForApi = await UserNotificationModel.create<UserNotificationModelForApi>({
362 type: UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS, 369 type: UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS,
363 userId: user.id, 370 userId: user.id,
364 videoAbuseId: videoAbuse.id 371 videoAbuseId: parameters.videoAbuse.id
365 }) 372 })
366 notification.VideoAbuse = videoAbuse 373 notification.VideoAbuse = parameters.videoAbuseInstance
367 374
368 return notification 375 return notification
369 } 376 }
370 377
371 function emailSender (emails: string[]) { 378 function emailSender (emails: string[]) {
372 return Emailer.Instance.addVideoAbuseModeratorsNotification(emails, videoAbuse) 379 return Emailer.Instance.addVideoAbuseModeratorsNotification(emails, parameters)
373 } 380 }
374 381
375 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) 382 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
@@ -548,10 +555,10 @@ class Notifier {
548 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) 555 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
549 } 556 }
550 557
551 private async notify <T extends MUserWithNotificationSetting> (options: { 558 private async notify<T extends MUserWithNotificationSetting> (options: {
552 users: T[], 559 users: T[]
553 notificationCreator: (user: T) => Promise<UserNotificationModelForApi>, 560 notificationCreator: (user: T) => Promise<UserNotificationModelForApi>
554 emailSender: (emails: string[]) => Promise<any> | Bluebird<any>, 561 emailSender: (emails: string[]) => void
555 settingGetter: (user: T) => UserNotificationSettingValue 562 settingGetter: (user: T) => UserNotificationSettingValue
556 }) { 563 }) {
557 const emails: string[] = [] 564 const emails: string[] = []
@@ -569,7 +576,7 @@ class Notifier {
569 } 576 }
570 577
571 if (emails.length !== 0) { 578 if (emails.length !== 0) {
572 await options.emailSender(emails) 579 options.emailSender(emails)
573 } 580 }
574 } 581 }
575 582
diff --git a/server/lib/oauth-model.ts b/server/lib/oauth-model.ts
index 086856f41..dbcba897a 100644
--- a/server/lib/oauth-model.ts
+++ b/server/lib/oauth-model.ts
@@ -1,4 +1,4 @@
1import * as Bluebird from 'bluebird' 1import * as express from 'express'
2import { AccessDeniedError } from 'oauth2-server' 2import { AccessDeniedError } from 'oauth2-server'
3import { logger } from '../helpers/logger' 3import { logger } from '../helpers/logger'
4import { UserModel } from '../models/account/user' 4import { UserModel } from '../models/account/user'
@@ -9,6 +9,11 @@ import { Transaction } from 'sequelize'
9import { CONFIG } from '../initializers/config' 9import { CONFIG } from '../initializers/config'
10import * as LRUCache from 'lru-cache' 10import * as LRUCache from 'lru-cache'
11import { MOAuthTokenUser } from '@server/typings/models/oauth/oauth-token' 11import { MOAuthTokenUser } from '@server/typings/models/oauth/oauth-token'
12import { MUser } from '@server/typings/models/user/user'
13import { UserAdminFlag } from '@shared/models/users/user-flag.model'
14import { createUserAccountAndChannelAndPlaylist } from './user'
15import { UserRole } from '@shared/models/users/user-role'
16import { PluginManager } from '@server/lib/plugins/plugin-manager'
12 17
13type TokenInfo = { accessToken: string, refreshToken: string, accessTokenExpiresAt: Date, refreshTokenExpiresAt: Date } 18type TokenInfo = { accessToken: string, refreshToken: string, accessTokenExpiresAt: Date, refreshTokenExpiresAt: Date }
14 19
@@ -41,22 +46,33 @@ function clearCacheByToken (token: string) {
41 } 46 }
42} 47}
43 48
44function getAccessToken (bearerToken: string) { 49async function getAccessToken (bearerToken: string) {
45 logger.debug('Getting access token (bearerToken: ' + bearerToken + ').') 50 logger.debug('Getting access token (bearerToken: ' + bearerToken + ').')
46 51
47 if (!bearerToken) return Bluebird.resolve(undefined) 52 if (!bearerToken) return undefined
48 53
49 if (accessTokenCache.has(bearerToken)) return Bluebird.resolve(accessTokenCache.get(bearerToken)) 54 let tokenModel: MOAuthTokenUser
50 55
51 return OAuthTokenModel.getByTokenAndPopulateUser(bearerToken) 56 if (accessTokenCache.has(bearerToken)) {
52 .then(tokenModel => { 57 tokenModel = accessTokenCache.get(bearerToken)
53 if (tokenModel) { 58 } else {
54 accessTokenCache.set(bearerToken, tokenModel) 59 tokenModel = await OAuthTokenModel.getByTokenAndPopulateUser(bearerToken)
55 userHavingToken.set(tokenModel.userId, tokenModel.accessToken)
56 }
57 60
58 return tokenModel 61 if (tokenModel) {
59 }) 62 accessTokenCache.set(bearerToken, tokenModel)
63 userHavingToken.set(tokenModel.userId, tokenModel.accessToken)
64 }
65 }
66
67 if (!tokenModel) return undefined
68
69 if (tokenModel.User.pluginAuth) {
70 const valid = await PluginManager.Instance.isTokenValid(tokenModel, 'access')
71
72 if (valid !== true) return undefined
73 }
74
75 return tokenModel
60} 76}
61 77
62function getClient (clientId: string, clientSecret: string) { 78function getClient (clientId: string, clientSecret: string) {
@@ -65,20 +81,52 @@ function getClient (clientId: string, clientSecret: string) {
65 return OAuthClientModel.getByIdAndSecret(clientId, clientSecret) 81 return OAuthClientModel.getByIdAndSecret(clientId, clientSecret)
66} 82}
67 83
68function getRefreshToken (refreshToken: string) { 84async function getRefreshToken (refreshToken: string) {
69 logger.debug('Getting RefreshToken (refreshToken: ' + refreshToken + ').') 85 logger.debug('Getting RefreshToken (refreshToken: ' + refreshToken + ').')
70 86
71 return OAuthTokenModel.getByRefreshTokenAndPopulateClient(refreshToken) 87 const tokenInfo = await OAuthTokenModel.getByRefreshTokenAndPopulateClient(refreshToken)
88 if (!tokenInfo) return undefined
89
90 const tokenModel = tokenInfo.token
91
92 if (tokenModel.User.pluginAuth) {
93 const valid = await PluginManager.Instance.isTokenValid(tokenModel, 'refresh')
94
95 if (valid !== true) return undefined
96 }
97
98 return tokenInfo
72} 99}
73 100
74async function getUser (usernameOrEmail: string, password: string) { 101async function getUser (usernameOrEmail?: string, password?: string) {
102 const res: express.Response = this.request.res
103
104 // Special treatment coming from a plugin
105 if (res.locals.bypassLogin && res.locals.bypassLogin.bypass === true) {
106 const obj = res.locals.bypassLogin
107 logger.info('Bypassing oauth login by plugin %s.', obj.pluginName)
108
109 let user = await UserModel.loadByEmail(obj.user.email)
110 if (!user) user = await createUserFromExternal(obj.pluginName, obj.user)
111
112 // If the user does not belongs to a plugin, it was created before its installation
113 // Then we just go through a regular login process
114 if (user.pluginAuth !== null) {
115 // This user does not belong to this plugin, skip it
116 if (user.pluginAuth !== obj.pluginName) return null
117
118 return user
119 }
120 }
121
75 logger.debug('Getting User (username/email: ' + usernameOrEmail + ', password: ******).') 122 logger.debug('Getting User (username/email: ' + usernameOrEmail + ', password: ******).')
76 123
77 const user = await UserModel.loadByUsernameOrEmail(usernameOrEmail) 124 const user = await UserModel.loadByUsernameOrEmail(usernameOrEmail)
78 if (!user) return null 125 // If we don't find the user, or if the user belongs to a plugin
126 if (!user || user.pluginAuth !== null) return null
79 127
80 const passwordMatch = await user.isPasswordMatch(password) 128 const passwordMatch = await user.isPasswordMatch(password)
81 if (passwordMatch === false) return null 129 if (passwordMatch !== true) return null
82 130
83 if (user.blocked) throw new AccessDeniedError('User is blocked.') 131 if (user.blocked) throw new AccessDeniedError('User is blocked.')
84 132
@@ -89,29 +137,36 @@ async function getUser (usernameOrEmail: string, password: string) {
89 return user 137 return user
90} 138}
91 139
92async function revokeToken (tokenInfo: TokenInfo) { 140async function revokeToken (tokenInfo: { refreshToken: string }) {
141 const res: express.Response = this.request.res
93 const token = await OAuthTokenModel.getByRefreshTokenAndPopulateUser(tokenInfo.refreshToken) 142 const token = await OAuthTokenModel.getByRefreshTokenAndPopulateUser(tokenInfo.refreshToken)
143
94 if (token) { 144 if (token) {
145 if (res.locals.explicitLogout === true && token.User.pluginAuth && token.authName) {
146 PluginManager.Instance.onLogout(token.User.pluginAuth, token.authName, token.User)
147 }
148
95 clearCacheByToken(token.accessToken) 149 clearCacheByToken(token.accessToken)
96 150
97 token.destroy() 151 token.destroy()
98 .catch(err => logger.error('Cannot destroy token when revoking token.', { err })) 152 .catch(err => logger.error('Cannot destroy token when revoking token.', { err }))
153
154 return true
99 } 155 }
100 156
101 /* 157 return false
102 * Thanks to https://github.com/manjeshpv/node-oauth2-server-implementation/blob/master/components/oauth/mongo-models.js
103 * "As per the discussion we need set older date
104 * revokeToken will expected return a boolean in future version
105 * https://github.com/oauthjs/node-oauth2-server/pull/274
106 * https://github.com/oauthjs/node-oauth2-server/issues/290"
107 */
108 const expiredToken = token
109 expiredToken.refreshTokenExpiresAt = new Date('2015-05-28T06:59:53.000Z')
110
111 return expiredToken
112} 158}
113 159
114async function saveToken (token: TokenInfo, client: OAuthClientModel, user: UserModel) { 160async function saveToken (token: TokenInfo, client: OAuthClientModel, user: UserModel) {
161 const res: express.Response = this.request.res
162
163 let authName: string = null
164 if (res.locals.bypassLogin?.bypass === true) {
165 authName = res.locals.bypassLogin.authName
166 } else if (res.locals.refreshTokenAuthName) {
167 authName = res.locals.refreshTokenAuthName
168 }
169
115 logger.debug('Saving token ' + token.accessToken + ' for client ' + client.id + ' and user ' + user.id + '.') 170 logger.debug('Saving token ' + token.accessToken + ' for client ' + client.id + ' and user ' + user.id + '.')
116 171
117 const tokenToCreate = { 172 const tokenToCreate = {
@@ -119,11 +174,16 @@ async function saveToken (token: TokenInfo, client: OAuthClientModel, user: User
119 accessTokenExpiresAt: token.accessTokenExpiresAt, 174 accessTokenExpiresAt: token.accessTokenExpiresAt,
120 refreshToken: token.refreshToken, 175 refreshToken: token.refreshToken,
121 refreshTokenExpiresAt: token.refreshTokenExpiresAt, 176 refreshTokenExpiresAt: token.refreshTokenExpiresAt,
177 authName,
122 oAuthClientId: client.id, 178 oAuthClientId: client.id,
123 userId: user.id 179 userId: user.id
124 } 180 }
125 181
126 const tokenCreated = await OAuthTokenModel.create(tokenToCreate) 182 const tokenCreated = await OAuthTokenModel.create(tokenToCreate)
183
184 user.lastLoginDate = new Date()
185 await user.save()
186
127 return Object.assign(tokenCreated, { client, user }) 187 return Object.assign(tokenCreated, { client, user })
128} 188}
129 189
@@ -141,3 +201,30 @@ export {
141 revokeToken, 201 revokeToken,
142 saveToken 202 saveToken
143} 203}
204
205async function createUserFromExternal (pluginAuth: string, options: {
206 username: string
207 email: string
208 role: UserRole
209 displayName: string
210}) {
211 const userToCreate = new UserModel({
212 username: options.username,
213 password: null,
214 email: options.email,
215 nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
216 autoPlayVideo: true,
217 role: options.role,
218 videoQuota: CONFIG.USER.VIDEO_QUOTA,
219 videoQuotaDaily: CONFIG.USER.VIDEO_QUOTA_DAILY,
220 adminFlags: UserAdminFlag.NONE,
221 pluginAuth
222 }) as MUser
223
224 const { user } = await createUserAccountAndChannelAndPlaylist({
225 userToCreate,
226 userDisplayName: options.displayName
227 })
228
229 return user
230}
diff --git a/server/lib/plugins/hooks.ts b/server/lib/plugins/hooks.ts
index bcc8c674e..aa92f03cc 100644
--- a/server/lib/plugins/hooks.ts
+++ b/server/lib/plugins/hooks.ts
@@ -25,7 +25,7 @@ const Hooks = {
25 }, 25 },
26 26
27 runAction: <T, U extends ServerActionHookName>(hookName: U, params?: T) => { 27 runAction: <T, U extends ServerActionHookName>(hookName: U, params?: T) => {
28 PluginManager.Instance.runHook(hookName, params) 28 PluginManager.Instance.runHook(hookName, undefined, params)
29 .catch(err => logger.error('Fatal hook error.', { err })) 29 .catch(err => logger.error('Fatal hook error.', { err }))
30 } 30 }
31} 31}
diff --git a/server/lib/plugins/plugin-helpers.ts b/server/lib/plugins/plugin-helpers.ts
new file mode 100644
index 000000000..de82b4918
--- /dev/null
+++ b/server/lib/plugins/plugin-helpers.ts
@@ -0,0 +1,133 @@
1import { PeerTubeHelpers } from '@server/typings/plugins'
2import { sequelizeTypescript } from '@server/initializers/database'
3import { buildLogger } from '@server/helpers/logger'
4import { VideoModel } from '@server/models/video/video'
5import { WEBSERVER } from '@server/initializers/constants'
6import { ServerModel } from '@server/models/server/server'
7import { getServerActor } from '@server/models/application/application'
8import { addServerInBlocklist, removeServerFromBlocklist, addAccountInBlocklist, removeAccountFromBlocklist } from '../blocklist'
9import { ServerBlocklistModel } from '@server/models/server/server-blocklist'
10import { AccountModel } from '@server/models/account/account'
11import { VideoBlacklistCreate } from '@shared/models'
12import { blacklistVideo, unblacklistVideo } from '../video-blacklist'
13import { VideoBlacklistModel } from '@server/models/video/video-blacklist'
14import { AccountBlocklistModel } from '@server/models/account/account-blocklist'
15
16function buildPluginHelpers (npmName: string): PeerTubeHelpers {
17 const logger = buildPluginLogger(npmName)
18
19 const database = buildDatabaseHelpers()
20 const videos = buildVideosHelpers()
21
22 const config = buildConfigHelpers()
23
24 const server = buildServerHelpers()
25
26 const moderation = buildModerationHelpers()
27
28 return {
29 logger,
30 database,
31 videos,
32 config,
33 moderation,
34 server
35 }
36}
37
38export {
39 buildPluginHelpers
40}
41
42// ---------------------------------------------------------------------------
43
44function buildPluginLogger (npmName: string) {
45 return buildLogger(npmName)
46}
47
48function buildDatabaseHelpers () {
49 return {
50 query: sequelizeTypescript.query.bind(sequelizeTypescript)
51 }
52}
53
54function buildServerHelpers () {
55 return {
56 getServerActor: () => getServerActor()
57 }
58}
59
60function buildVideosHelpers () {
61 return {
62 loadByUrl: (url: string) => {
63 return VideoModel.loadByUrl(url)
64 },
65
66 removeVideo: (id: number) => {
67 return sequelizeTypescript.transaction(async t => {
68 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(id, t)
69
70 await video.destroy({ transaction: t })
71 })
72 }
73 }
74}
75
76function buildModerationHelpers () {
77 return {
78 blockServer: async (options: { byAccountId: number, hostToBlock: string }) => {
79 const serverToBlock = await ServerModel.loadOrCreateByHost(options.hostToBlock)
80
81 await addServerInBlocklist(options.byAccountId, serverToBlock.id)
82 },
83
84 unblockServer: async (options: { byAccountId: number, hostToUnblock: string }) => {
85 const serverBlock = await ServerBlocklistModel.loadByAccountAndHost(options.byAccountId, options.hostToUnblock)
86 if (!serverBlock) return
87
88 await removeServerFromBlocklist(serverBlock)
89 },
90
91 blockAccount: async (options: { byAccountId: number, handleToBlock: string }) => {
92 const accountToBlock = await AccountModel.loadByNameWithHost(options.handleToBlock)
93 if (!accountToBlock) return
94
95 await addAccountInBlocklist(options.byAccountId, accountToBlock.id)
96 },
97
98 unblockAccount: async (options: { byAccountId: number, handleToUnblock: string }) => {
99 const targetAccount = await AccountModel.loadByNameWithHost(options.handleToUnblock)
100 if (!targetAccount) return
101
102 const accountBlock = await AccountBlocklistModel.loadByAccountAndTarget(options.byAccountId, targetAccount.id)
103 if (!accountBlock) return
104
105 await removeAccountFromBlocklist(accountBlock)
106 },
107
108 blacklistVideo: async (options: { videoIdOrUUID: number | string, createOptions: VideoBlacklistCreate }) => {
109 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(options.videoIdOrUUID)
110 if (!video) return
111
112 await blacklistVideo(video, options.createOptions)
113 },
114
115 unblacklistVideo: async (options: { videoIdOrUUID: number | string }) => {
116 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(options.videoIdOrUUID)
117 if (!video) return
118
119 const videoBlacklist = await VideoBlacklistModel.loadByVideoId(video.id)
120 if (!videoBlacklist) return
121
122 await unblacklistVideo(videoBlacklist, video)
123 }
124 }
125}
126
127function buildConfigHelpers () {
128 return {
129 getWebserverUrl () {
130 return WEBSERVER.URL
131 }
132 }
133}
diff --git a/server/lib/plugins/plugin-index.ts b/server/lib/plugins/plugin-index.ts
index 25b4f3c61..170f0c7e2 100644
--- a/server/lib/plugins/plugin-index.ts
+++ b/server/lib/plugins/plugin-index.ts
@@ -27,11 +27,11 @@ async function listAvailablePluginsFromIndex (options: PeertubePluginIndexList)
27 const uri = CONFIG.PLUGINS.INDEX.URL + '/api/v1/plugins' 27 const uri = CONFIG.PLUGINS.INDEX.URL + '/api/v1/plugins'
28 28
29 try { 29 try {
30 const { body } = await doRequest({ uri, qs, json: true }) 30 const { body } = await doRequest<any>({ uri, qs, json: true })
31 31
32 logger.debug('Got result from PeerTube index.', { body }) 32 logger.debug('Got result from PeerTube index.', { body })
33 33
34 await addInstanceInformation(body) 34 addInstanceInformation(body)
35 35
36 return body as ResultList<PeerTubePluginIndex> 36 return body as ResultList<PeerTubePluginIndex>
37 } catch (err) { 37 } catch (err) {
@@ -40,7 +40,7 @@ async function listAvailablePluginsFromIndex (options: PeertubePluginIndexList)
40 } 40 }
41} 41}
42 42
43async function addInstanceInformation (result: ResultList<PeerTubePluginIndex>) { 43function addInstanceInformation (result: ResultList<PeerTubePluginIndex>) {
44 for (const d of result.data) { 44 for (const d of result.data) {
45 d.installed = PluginManager.Instance.isRegistered(d.npmName) 45 d.installed = PluginManager.Instance.isRegistered(d.npmName)
46 d.name = PluginModel.normalizePluginName(d.npmName) 46 d.name = PluginModel.normalizePluginName(d.npmName)
@@ -57,7 +57,7 @@ async function getLatestPluginsVersion (npmNames: string[]): Promise<PeertubePlu
57 57
58 const uri = CONFIG.PLUGINS.INDEX.URL + '/api/v1/plugins/latest-version' 58 const uri = CONFIG.PLUGINS.INDEX.URL + '/api/v1/plugins/latest-version'
59 59
60 const { body } = await doRequest({ uri, body: bodyRequest, json: true, method: 'POST' }) 60 const { body } = await doRequest<any>({ uri, body: bodyRequest, json: true, method: 'POST' })
61 61
62 return body 62 return body
63} 63}
diff --git a/server/lib/plugins/plugin-manager.ts b/server/lib/plugins/plugin-manager.ts
index 7ebdabd34..950acf7ad 100644
--- a/server/lib/plugins/plugin-manager.ts
+++ b/server/lib/plugins/plugin-manager.ts
@@ -9,23 +9,20 @@ import {
9 PluginTranslationPaths as PackagePluginTranslations 9 PluginTranslationPaths as PackagePluginTranslations
10} from '../../../shared/models/plugins/plugin-package-json.model' 10} from '../../../shared/models/plugins/plugin-package-json.model'
11import { createReadStream, createWriteStream } from 'fs' 11import { createReadStream, createWriteStream } from 'fs'
12import { PLUGIN_GLOBAL_CSS_PATH, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../../initializers/constants' 12import { PLUGIN_GLOBAL_CSS_PATH } from '../../initializers/constants'
13import { PluginType } from '../../../shared/models/plugins/plugin.type' 13import { PluginType } from '../../../shared/models/plugins/plugin.type'
14import { installNpmPlugin, installNpmPluginFromDisk, removeNpmPlugin } from './yarn' 14import { installNpmPlugin, installNpmPluginFromDisk, removeNpmPlugin } from './yarn'
15import { outputFile, readJSON } from 'fs-extra' 15import { outputFile, readJSON } from 'fs-extra'
16import { PluginSettingsManager } from '../../../shared/models/plugins/plugin-settings-manager.model' 16import { ServerHook, ServerHookName } from '../../../shared/models/plugins/server-hook.model'
17import { PluginStorageManager } from '../../../shared/models/plugins/plugin-storage-manager.model'
18import { ServerHook, ServerHookName, serverHookObject } from '../../../shared/models/plugins/server-hook.model'
19import { getHookType, internalRunHook } from '../../../shared/core-utils/plugins/hooks' 17import { getHookType, internalRunHook } from '../../../shared/core-utils/plugins/hooks'
20import { RegisterServerOptions } from '../../typings/plugins/register-server-option.model' 18import { RegisterServerOptions } from '../../typings/plugins/register-server-option.model'
21import { PluginLibrary } from '../../typings/plugins' 19import { PluginLibrary } from '../../typings/plugins'
22import { ClientHtml } from '../client-html' 20import { ClientHtml } from '../client-html'
23import { RegisterServerHookOptions } from '../../../shared/models/plugins/register-server-hook.model'
24import { RegisterServerSettingOptions } from '../../../shared/models/plugins/register-server-setting.model'
25import { PluginVideoLanguageManager } from '../../../shared/models/plugins/plugin-video-language-manager.model'
26import { PluginVideoCategoryManager } from '../../../shared/models/plugins/plugin-video-category-manager.model'
27import { PluginVideoLicenceManager } from '../../../shared/models/plugins/plugin-video-licence-manager.model'
28import { PluginTranslation } from '../../../shared/models/plugins/plugin-translation.model' 21import { PluginTranslation } from '../../../shared/models/plugins/plugin-translation.model'
22import { RegisterHelpersStore } from './register-helpers-store'
23import { RegisterServerHookOptions } from '@shared/models/plugins/register-server-hook.model'
24import { MOAuthTokenUser, MUser } from '@server/typings/models'
25import { RegisterServerAuthPassOptions, RegisterServerAuthExternalOptions } from '@shared/models/plugins/register-server-auth.model'
29 26
30export interface RegisteredPlugin { 27export interface RegisteredPlugin {
31 npmName: string 28 npmName: string
@@ -44,6 +41,7 @@ export interface RegisteredPlugin {
44 css: string[] 41 css: string[]
45 42
46 // Only if this is a plugin 43 // Only if this is a plugin
44 registerHelpersStore?: RegisterHelpersStore
47 unregister?: Function 45 unregister?: Function
48} 46}
49 47
@@ -54,35 +52,18 @@ export interface HookInformationValue {
54 priority: number 52 priority: number
55} 53}
56 54
57type AlterableVideoConstant = 'language' | 'licence' | 'category'
58type VideoConstant = { [ key in number | string ]: string }
59type UpdatedVideoConstant = {
60 [ name in AlterableVideoConstant ]: {
61 [ npmName: string ]: {
62 added: { key: number | string, label: string }[],
63 deleted: { key: number | string, label: string }[]
64 }
65 }
66}
67
68type PluginLocalesTranslations = { 55type PluginLocalesTranslations = {
69 [ locale: string ]: PluginTranslation 56 [locale: string]: PluginTranslation
70} 57}
71 58
72export class PluginManager implements ServerHook { 59export class PluginManager implements ServerHook {
73 60
74 private static instance: PluginManager 61 private static instance: PluginManager
75 62
76 private registeredPlugins: { [ name: string ]: RegisteredPlugin } = {} 63 private registeredPlugins: { [name: string]: RegisteredPlugin } = {}
77 private settings: { [ name: string ]: RegisterServerSettingOptions[] } = {}
78 private hooks: { [ name: string ]: HookInformationValue[] } = {}
79 private translations: PluginLocalesTranslations = {}
80 64
81 private updatedVideoConstants: UpdatedVideoConstant = { 65 private hooks: { [name: string]: HookInformationValue[] } = {}
82 language: {}, 66 private translations: PluginLocalesTranslations = {}
83 licence: {},
84 category: {}
85 }
86 67
87 private constructor () { 68 private constructor () {
88 } 69 }
@@ -97,7 +78,7 @@ export class PluginManager implements ServerHook {
97 return this.registeredPlugins[npmName] 78 return this.registeredPlugins[npmName]
98 } 79 }
99 80
100 getRegisteredPlugin (name: string) { 81 getRegisteredPluginByShortName (name: string) {
101 const npmName = PluginModel.buildNpmName(name, PluginType.PLUGIN) 82 const npmName = PluginModel.buildNpmName(name, PluginType.PLUGIN)
102 const registered = this.getRegisteredPluginOrTheme(npmName) 83 const registered = this.getRegisteredPluginOrTheme(npmName)
103 84
@@ -106,7 +87,7 @@ export class PluginManager implements ServerHook {
106 return registered 87 return registered
107 } 88 }
108 89
109 getRegisteredTheme (name: string) { 90 getRegisteredThemeByShortName (name: string) {
110 const npmName = PluginModel.buildNpmName(name, PluginType.THEME) 91 const npmName = PluginModel.buildNpmName(name, PluginType.THEME)
111 const registered = this.getRegisteredPluginOrTheme(npmName) 92 const registered = this.getRegisteredPluginOrTheme(npmName)
112 93
@@ -123,17 +104,102 @@ export class PluginManager implements ServerHook {
123 return this.getRegisteredPluginsOrThemes(PluginType.THEME) 104 return this.getRegisteredPluginsOrThemes(PluginType.THEME)
124 } 105 }
125 106
107 getIdAndPassAuths () {
108 return this.getRegisteredPlugins()
109 .map(p => ({
110 npmName: p.npmName,
111 name: p.name,
112 version: p.version,
113 idAndPassAuths: p.registerHelpersStore.getIdAndPassAuths()
114 }))
115 .filter(v => v.idAndPassAuths.length !== 0)
116 }
117
118 getExternalAuths () {
119 return this.getRegisteredPlugins()
120 .map(p => ({
121 npmName: p.npmName,
122 name: p.name,
123 version: p.version,
124 externalAuths: p.registerHelpersStore.getExternalAuths()
125 }))
126 .filter(v => v.externalAuths.length !== 0)
127 }
128
126 getRegisteredSettings (npmName: string) { 129 getRegisteredSettings (npmName: string) {
127 return this.settings[npmName] || [] 130 const result = this.getRegisteredPluginOrTheme(npmName)
131 if (!result || result.type !== PluginType.PLUGIN) return []
132
133 return result.registerHelpersStore.getSettings()
134 }
135
136 getRouter (npmName: string) {
137 const result = this.getRegisteredPluginOrTheme(npmName)
138 if (!result || result.type !== PluginType.PLUGIN) return null
139
140 return result.registerHelpersStore.getRouter()
128 } 141 }
129 142
130 getTranslations (locale: string) { 143 getTranslations (locale: string) {
131 return this.translations[locale] || {} 144 return this.translations[locale] || {}
132 } 145 }
133 146
147 async isTokenValid (token: MOAuthTokenUser, type: 'access' | 'refresh') {
148 const auth = this.getAuth(token.User.pluginAuth, token.authName)
149 if (!auth) return true
150
151 if (auth.hookTokenValidity) {
152 try {
153 const { valid } = await auth.hookTokenValidity({ token, type })
154
155 if (valid === false) {
156 logger.info('Rejecting %s token validity from auth %s of plugin %s', type, token.authName, token.User.pluginAuth)
157 }
158
159 return valid
160 } catch (err) {
161 logger.warn('Cannot run check token validity from auth %s of plugin %s.', token.authName, token.User.pluginAuth, { err })
162 return true
163 }
164 }
165
166 return true
167 }
168
169 // ###################### External events ######################
170
171 onLogout (npmName: string, authName: string, user: MUser) {
172 const auth = this.getAuth(npmName, authName)
173
174 if (auth?.onLogout) {
175 logger.info('Running onLogout function from auth %s of plugin %s', authName, npmName)
176
177 try {
178 auth.onLogout(user)
179 } catch (err) {
180 logger.warn('Cannot run onLogout function from auth %s of plugin %s.', authName, npmName, { err })
181 }
182 }
183 }
184
185 onSettingsChanged (name: string, settings: any) {
186 const registered = this.getRegisteredPluginByShortName(name)
187 if (!registered) {
188 logger.error('Cannot find plugin %s to call on settings changed.', name)
189 }
190
191 for (const cb of registered.registerHelpersStore.getOnSettingsChangedCallbacks()) {
192 try {
193 cb(settings)
194 } catch (err) {
195 logger.error('Cannot run on settings changed callback for %s.', registered.npmName, { err })
196 }
197 }
198 }
199
134 // ###################### Hooks ###################### 200 // ###################### Hooks ######################
135 201
136 async runHook <T> (hookName: ServerHookName, result?: T, params?: any): Promise<T> { 202 async runHook<T> (hookName: ServerHookName, result?: T, params?: any): Promise<T> {
137 if (!this.hooks[hookName]) return Promise.resolve(result) 203 if (!this.hooks[hookName]) return Promise.resolve(result)
138 204
139 const hookType = getHookType(hookName) 205 const hookType = getHookType(hookName)
@@ -185,7 +251,6 @@ export class PluginManager implements ServerHook {
185 } 251 }
186 252
187 delete this.registeredPlugins[plugin.npmName] 253 delete this.registeredPlugins[plugin.npmName]
188 delete this.settings[plugin.npmName]
189 254
190 this.deleteTranslations(plugin.npmName) 255 this.deleteTranslations(plugin.npmName)
191 256
@@ -197,7 +262,8 @@ export class PluginManager implements ServerHook {
197 this.hooks[key] = this.hooks[key].filter(h => h.npmName !== npmName) 262 this.hooks[key] = this.hooks[key].filter(h => h.npmName !== npmName)
198 } 263 }
199 264
200 this.reinitVideoConstants(plugin.npmName) 265 const store = plugin.registerHelpersStore
266 store.reinitVideoConstants(plugin.npmName)
201 267
202 logger.info('Regenerating registered plugin CSS to global file.') 268 logger.info('Regenerating registered plugin CSS to global file.')
203 await this.regeneratePluginGlobalCSS() 269 await this.regeneratePluginGlobalCSS()
@@ -303,8 +369,11 @@ export class PluginManager implements ServerHook {
303 this.sanitizeAndCheckPackageJSONOrThrow(packageJSON, plugin.type) 369 this.sanitizeAndCheckPackageJSONOrThrow(packageJSON, plugin.type)
304 370
305 let library: PluginLibrary 371 let library: PluginLibrary
372 let registerHelpersStore: RegisterHelpersStore
306 if (plugin.type === PluginType.PLUGIN) { 373 if (plugin.type === PluginType.PLUGIN) {
307 library = await this.registerPlugin(plugin, pluginPath, packageJSON) 374 const result = await this.registerPlugin(plugin, pluginPath, packageJSON)
375 library = result.library
376 registerHelpersStore = result.registerStore
308 } 377 }
309 378
310 const clientScripts: { [id: string]: ClientScript } = {} 379 const clientScripts: { [id: string]: ClientScript } = {}
@@ -312,7 +381,7 @@ export class PluginManager implements ServerHook {
312 clientScripts[c.script] = c 381 clientScripts[c.script] = c
313 } 382 }
314 383
315 this.registeredPlugins[ npmName ] = { 384 this.registeredPlugins[npmName] = {
316 npmName, 385 npmName,
317 name: plugin.name, 386 name: plugin.name,
318 type: plugin.type, 387 type: plugin.type,
@@ -323,6 +392,7 @@ export class PluginManager implements ServerHook {
323 staticDirs: packageJSON.staticDirs, 392 staticDirs: packageJSON.staticDirs,
324 clientScripts, 393 clientScripts,
325 css: packageJSON.css, 394 css: packageJSON.css,
395 registerHelpersStore: registerHelpersStore || undefined,
326 unregister: library ? library.unregister : undefined 396 unregister: library ? library.unregister : undefined
327 } 397 }
328 398
@@ -341,15 +411,15 @@ export class PluginManager implements ServerHook {
341 throw new Error('Library code is not valid (miss register or unregister function)') 411 throw new Error('Library code is not valid (miss register or unregister function)')
342 } 412 }
343 413
344 const registerHelpers = this.getRegisterHelpers(npmName, plugin) 414 const { registerOptions, registerStore } = this.getRegisterHelpers(npmName, plugin)
345 library.register(registerHelpers) 415 library.register(registerOptions)
346 .catch(err => logger.error('Cannot register plugin %s.', npmName, { err })) 416 .catch(err => logger.error('Cannot register plugin %s.', npmName, { err }))
347 417
348 logger.info('Add plugin %s CSS to global file.', npmName) 418 logger.info('Add plugin %s CSS to global file.', npmName)
349 419
350 await this.addCSSToGlobalFile(pluginPath, packageJSON.css) 420 await this.addCSSToGlobalFile(pluginPath, packageJSON.css)
351 421
352 return library 422 return { library, registerStore }
353 } 423 }
354 424
355 // ###################### Translations ###################### 425 // ###################### Translations ######################
@@ -432,13 +502,23 @@ export class PluginManager implements ServerHook {
432 return join(CONFIG.STORAGE.PLUGINS_DIR, 'node_modules', npmName) 502 return join(CONFIG.STORAGE.PLUGINS_DIR, 'node_modules', npmName)
433 } 503 }
434 504
505 private getAuth (npmName: string, authName: string) {
506 const plugin = this.getRegisteredPluginOrTheme(npmName)
507 if (!plugin || plugin.type !== PluginType.PLUGIN) return null
508
509 let auths: (RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions)[] = plugin.registerHelpersStore.getIdAndPassAuths()
510 auths = auths.concat(plugin.registerHelpersStore.getExternalAuths())
511
512 return auths.find(a => a.authName === authName)
513 }
514
435 // ###################### Private getters ###################### 515 // ###################### Private getters ######################
436 516
437 private getRegisteredPluginsOrThemes (type: PluginType) { 517 private getRegisteredPluginsOrThemes (type: PluginType) {
438 const plugins: RegisteredPlugin[] = [] 518 const plugins: RegisteredPlugin[] = []
439 519
440 for (const npmName of Object.keys(this.registeredPlugins)) { 520 for (const npmName of Object.keys(this.registeredPlugins)) {
441 const plugin = this.registeredPlugins[ npmName ] 521 const plugin = this.registeredPlugins[npmName]
442 if (plugin.type !== type) continue 522 if (plugin.type !== type) continue
443 523
444 plugins.push(plugin) 524 plugins.push(plugin)
@@ -449,149 +529,26 @@ export class PluginManager implements ServerHook {
449 529
450 // ###################### Generate register helpers ###################### 530 // ###################### Generate register helpers ######################
451 531
452 private getRegisterHelpers (npmName: string, plugin: PluginModel): RegisterServerOptions { 532 private getRegisterHelpers (
453 const registerHook = (options: RegisterServerHookOptions) => { 533 npmName: string,
454 if (serverHookObject[options.target] !== true) { 534 plugin: PluginModel
455 logger.warn('Unknown hook %s of plugin %s. Skipping.', options.target, npmName) 535 ): { registerStore: RegisterHelpersStore, registerOptions: RegisterServerOptions } {
456 return 536 const onHookAdded = (options: RegisterServerHookOptions) => {
457 }
458
459 if (!this.hooks[options.target]) this.hooks[options.target] = [] 537 if (!this.hooks[options.target]) this.hooks[options.target] = []
460 538
461 this.hooks[options.target].push({ 539 this.hooks[options.target].push({
462 npmName, 540 npmName: npmName,
463 pluginName: plugin.name, 541 pluginName: plugin.name,
464 handler: options.handler, 542 handler: options.handler,
465 priority: options.priority || 0 543 priority: options.priority || 0
466 }) 544 })
467 } 545 }
468 546
469 const registerSetting = (options: RegisterServerSettingOptions) => { 547 const registerHelpersStore = new RegisterHelpersStore(npmName, plugin, onHookAdded.bind(this))
470 if (!this.settings[npmName]) this.settings[npmName] = []
471
472 this.settings[npmName].push(options)
473 }
474
475 const settingsManager: PluginSettingsManager = {
476 getSetting: (name: string) => PluginModel.getSetting(plugin.name, plugin.type, name),
477
478 setSetting: (name: string, value: string) => PluginModel.setSetting(plugin.name, plugin.type, name, value)
479 }
480
481 const storageManager: PluginStorageManager = {
482 getData: (key: string) => PluginModel.getData(plugin.name, plugin.type, key),
483
484 storeData: (key: string, data: any) => PluginModel.storeData(plugin.name, plugin.type, key, data)
485 }
486
487 const videoLanguageManager: PluginVideoLanguageManager = {
488 addLanguage: (key: string, label: string) => this.addConstant({ npmName, type: 'language', obj: VIDEO_LANGUAGES, key, label }),
489
490 deleteLanguage: (key: string) => this.deleteConstant({ npmName, type: 'language', obj: VIDEO_LANGUAGES, key })
491 }
492
493 const videoCategoryManager: PluginVideoCategoryManager = {
494 addCategory: (key: number, label: string) => this.addConstant({ npmName, type: 'category', obj: VIDEO_CATEGORIES, key, label }),
495
496 deleteCategory: (key: number) => this.deleteConstant({ npmName, type: 'category', obj: VIDEO_CATEGORIES, key })
497 }
498
499 const videoLicenceManager: PluginVideoLicenceManager = {
500 addLicence: (key: number, label: string) => this.addConstant({ npmName, type: 'licence', obj: VIDEO_LICENCES, key, label }),
501
502 deleteLicence: (key: number) => this.deleteConstant({ npmName, type: 'licence', obj: VIDEO_LICENCES, key })
503 }
504
505 const peertubeHelpers = {
506 logger
507 }
508 548
509 return { 549 return {
510 registerHook, 550 registerStore: registerHelpersStore,
511 registerSetting, 551 registerOptions: registerHelpersStore.buildRegisterHelpers()
512 settingsManager,
513 storageManager,
514 videoLanguageManager,
515 videoCategoryManager,
516 videoLicenceManager,
517 peertubeHelpers
518 }
519 }
520
521 private addConstant <T extends string | number> (parameters: {
522 npmName: string,
523 type: AlterableVideoConstant,
524 obj: VideoConstant,
525 key: T,
526 label: string
527 }) {
528 const { npmName, type, obj, key, label } = parameters
529
530 if (obj[key]) {
531 logger.warn('Cannot add %s %s by plugin %s: key already exists.', type, npmName, key)
532 return false
533 }
534
535 if (!this.updatedVideoConstants[type][npmName]) {
536 this.updatedVideoConstants[type][npmName] = {
537 added: [],
538 deleted: []
539 }
540 }
541
542 this.updatedVideoConstants[type][npmName].added.push({ key, label })
543 obj[key] = label
544
545 return true
546 }
547
548 private deleteConstant <T extends string | number> (parameters: {
549 npmName: string,
550 type: AlterableVideoConstant,
551 obj: VideoConstant,
552 key: T
553 }) {
554 const { npmName, type, obj, key } = parameters
555
556 if (!obj[key]) {
557 logger.warn('Cannot delete %s %s by plugin %s: key does not exist.', type, npmName, key)
558 return false
559 }
560
561 if (!this.updatedVideoConstants[type][npmName]) {
562 this.updatedVideoConstants[type][npmName] = {
563 added: [],
564 deleted: []
565 }
566 }
567
568 this.updatedVideoConstants[type][npmName].deleted.push({ key, label: obj[key] })
569 delete obj[key]
570
571 return true
572 }
573
574 private reinitVideoConstants (npmName: string) {
575 const hash = {
576 language: VIDEO_LANGUAGES,
577 licence: VIDEO_LICENCES,
578 category: VIDEO_CATEGORIES
579 }
580 const types: AlterableVideoConstant[] = [ 'language', 'licence', 'category' ]
581
582 for (const type of types) {
583 const updatedConstants = this.updatedVideoConstants[type][npmName]
584 if (!updatedConstants) continue
585
586 for (const added of updatedConstants.added) {
587 delete hash[type][added.key]
588 }
589
590 for (const deleted of updatedConstants.deleted) {
591 hash[type][deleted.key] = deleted.label
592 }
593
594 delete this.updatedVideoConstants[type][npmName]
595 } 552 }
596 } 553 }
597 554
@@ -604,7 +561,7 @@ export class PluginManager implements ServerHook {
604 const { result: packageJSONValid, badFields } = isPackageJSONValid(packageJSON, pluginType) 561 const { result: packageJSONValid, badFields } = isPackageJSONValid(packageJSON, pluginType)
605 if (!packageJSONValid) { 562 if (!packageJSONValid) {
606 const formattedFields = badFields.map(f => `"${f}"`) 563 const formattedFields = badFields.map(f => `"${f}"`)
607 .join(', ') 564 .join(', ')
608 565
609 throw new Error(`PackageJSON is invalid (invalid fields: ${formattedFields}).`) 566 throw new Error(`PackageJSON is invalid (invalid fields: ${formattedFields}).`)
610 } 567 }
diff --git a/server/lib/plugins/register-helpers-store.ts b/server/lib/plugins/register-helpers-store.ts
new file mode 100644
index 000000000..e337b1cb0
--- /dev/null
+++ b/server/lib/plugins/register-helpers-store.ts
@@ -0,0 +1,355 @@
1import * as express from 'express'
2import { logger } from '@server/helpers/logger'
3import {
4 VIDEO_CATEGORIES,
5 VIDEO_LANGUAGES,
6 VIDEO_LICENCES,
7 VIDEO_PLAYLIST_PRIVACIES,
8 VIDEO_PRIVACIES
9} from '@server/initializers/constants'
10import { onExternalUserAuthenticated } from '@server/lib/auth'
11import { PluginModel } from '@server/models/server/plugin'
12import { RegisterServerOptions } from '@server/typings/plugins'
13import { PluginPlaylistPrivacyManager } from '@shared/models/plugins/plugin-playlist-privacy-manager.model'
14import { PluginSettingsManager } from '@shared/models/plugins/plugin-settings-manager.model'
15import { PluginStorageManager } from '@shared/models/plugins/plugin-storage-manager.model'
16import { PluginVideoCategoryManager } from '@shared/models/plugins/plugin-video-category-manager.model'
17import { PluginVideoLanguageManager } from '@shared/models/plugins/plugin-video-language-manager.model'
18import { PluginVideoLicenceManager } from '@shared/models/plugins/plugin-video-licence-manager.model'
19import { PluginVideoPrivacyManager } from '@shared/models/plugins/plugin-video-privacy-manager.model'
20import {
21 RegisterServerAuthExternalOptions,
22 RegisterServerAuthExternalResult,
23 RegisterServerAuthPassOptions,
24 RegisterServerExternalAuthenticatedResult
25} from '@shared/models/plugins/register-server-auth.model'
26import { RegisterServerHookOptions } from '@shared/models/plugins/register-server-hook.model'
27import { RegisterServerSettingOptions } from '@shared/models/plugins/register-server-setting.model'
28import { serverHookObject } from '@shared/models/plugins/server-hook.model'
29import { buildPluginHelpers } from './plugin-helpers'
30
31type AlterableVideoConstant = 'language' | 'licence' | 'category' | 'privacy' | 'playlistPrivacy'
32type VideoConstant = { [key in number | string]: string }
33
34type UpdatedVideoConstant = {
35 [name in AlterableVideoConstant]: {
36 added: { key: number | string, label: string }[]
37 deleted: { key: number | string, label: string }[]
38 }
39}
40
41export class RegisterHelpersStore {
42 private readonly updatedVideoConstants: UpdatedVideoConstant = {
43 playlistPrivacy: { added: [], deleted: [] },
44 privacy: { added: [], deleted: [] },
45 language: { added: [], deleted: [] },
46 licence: { added: [], deleted: [] },
47 category: { added: [], deleted: [] }
48 }
49
50 private readonly settings: RegisterServerSettingOptions[] = []
51
52 private idAndPassAuths: RegisterServerAuthPassOptions[] = []
53 private externalAuths: RegisterServerAuthExternalOptions[] = []
54
55 private readonly onSettingsChangeCallbacks: ((settings: any) => void)[] = []
56
57 private readonly router: express.Router
58
59 constructor (
60 private readonly npmName: string,
61 private readonly plugin: PluginModel,
62 private readonly onHookAdded: (options: RegisterServerHookOptions) => void
63 ) {
64 this.router = express.Router()
65 }
66
67 buildRegisterHelpers (): RegisterServerOptions {
68 const registerHook = this.buildRegisterHook()
69 const registerSetting = this.buildRegisterSetting()
70
71 const getRouter = this.buildGetRouter()
72
73 const settingsManager = this.buildSettingsManager()
74 const storageManager = this.buildStorageManager()
75
76 const videoLanguageManager = this.buildVideoLanguageManager()
77
78 const videoLicenceManager = this.buildVideoLicenceManager()
79 const videoCategoryManager = this.buildVideoCategoryManager()
80
81 const videoPrivacyManager = this.buildVideoPrivacyManager()
82 const playlistPrivacyManager = this.buildPlaylistPrivacyManager()
83
84 const registerIdAndPassAuth = this.buildRegisterIdAndPassAuth()
85 const registerExternalAuth = this.buildRegisterExternalAuth()
86 const unregisterIdAndPassAuth = this.buildUnregisterIdAndPassAuth()
87 const unregisterExternalAuth = this.buildUnregisterExternalAuth()
88
89 const peertubeHelpers = buildPluginHelpers(this.npmName)
90
91 return {
92 registerHook,
93 registerSetting,
94
95 getRouter,
96
97 settingsManager,
98 storageManager,
99
100 videoLanguageManager,
101 videoCategoryManager,
102 videoLicenceManager,
103
104 videoPrivacyManager,
105 playlistPrivacyManager,
106
107 registerIdAndPassAuth,
108 registerExternalAuth,
109 unregisterIdAndPassAuth,
110 unregisterExternalAuth,
111
112 peertubeHelpers
113 }
114 }
115
116 reinitVideoConstants (npmName: string) {
117 const hash = {
118 language: VIDEO_LANGUAGES,
119 licence: VIDEO_LICENCES,
120 category: VIDEO_CATEGORIES,
121 privacy: VIDEO_PRIVACIES,
122 playlistPrivacy: VIDEO_PLAYLIST_PRIVACIES
123 }
124 const types: AlterableVideoConstant[] = [ 'language', 'licence', 'category', 'privacy', 'playlistPrivacy' ]
125
126 for (const type of types) {
127 const updatedConstants = this.updatedVideoConstants[type][npmName]
128 if (!updatedConstants) continue
129
130 for (const added of updatedConstants.added) {
131 delete hash[type][added.key]
132 }
133
134 for (const deleted of updatedConstants.deleted) {
135 hash[type][deleted.key] = deleted.label
136 }
137
138 delete this.updatedVideoConstants[type][npmName]
139 }
140 }
141
142 getSettings () {
143 return this.settings
144 }
145
146 getRouter () {
147 return this.router
148 }
149
150 getIdAndPassAuths () {
151 return this.idAndPassAuths
152 }
153
154 getExternalAuths () {
155 return this.externalAuths
156 }
157
158 getOnSettingsChangedCallbacks () {
159 return this.onSettingsChangeCallbacks
160 }
161
162 private buildGetRouter () {
163 return () => this.router
164 }
165
166 private buildRegisterSetting () {
167 return (options: RegisterServerSettingOptions) => {
168 this.settings.push(options)
169 }
170 }
171
172 private buildRegisterHook () {
173 return (options: RegisterServerHookOptions) => {
174 if (serverHookObject[options.target] !== true) {
175 logger.warn('Unknown hook %s of plugin %s. Skipping.', options.target, this.npmName)
176 return
177 }
178
179 return this.onHookAdded(options)
180 }
181 }
182
183 private buildRegisterIdAndPassAuth () {
184 return (options: RegisterServerAuthPassOptions) => {
185 if (!options.authName || typeof options.getWeight !== 'function' || typeof options.login !== 'function') {
186 logger.error('Cannot register auth plugin %s: authName, getWeight or login are not valid.', this.npmName, { options })
187 return
188 }
189
190 this.idAndPassAuths.push(options)
191 }
192 }
193
194 private buildRegisterExternalAuth () {
195 const self = this
196
197 return (options: RegisterServerAuthExternalOptions) => {
198 if (!options.authName || typeof options.authDisplayName !== 'function' || typeof options.onAuthRequest !== 'function') {
199 logger.error('Cannot register auth plugin %s: authName, authDisplayName or onAuthRequest are not valid.', this.npmName, { options })
200 return
201 }
202
203 this.externalAuths.push(options)
204
205 return {
206 userAuthenticated (result: RegisterServerExternalAuthenticatedResult): void {
207 onExternalUserAuthenticated({
208 npmName: self.npmName,
209 authName: options.authName,
210 authResult: result
211 }).catch(err => {
212 logger.error('Cannot execute onExternalUserAuthenticated.', { npmName: self.npmName, authName: options.authName, err })
213 })
214 }
215 } as RegisterServerAuthExternalResult
216 }
217 }
218
219 private buildUnregisterExternalAuth () {
220 return (authName: string) => {
221 this.externalAuths = this.externalAuths.filter(a => a.authName !== authName)
222 }
223 }
224
225 private buildUnregisterIdAndPassAuth () {
226 return (authName: string) => {
227 this.idAndPassAuths = this.idAndPassAuths.filter(a => a.authName !== authName)
228 }
229 }
230
231 private buildSettingsManager (): PluginSettingsManager {
232 return {
233 getSetting: (name: string) => PluginModel.getSetting(this.plugin.name, this.plugin.type, name, this.settings),
234
235 getSettings: (names: string[]) => PluginModel.getSettings(this.plugin.name, this.plugin.type, names, this.settings),
236
237 setSetting: (name: string, value: string) => PluginModel.setSetting(this.plugin.name, this.plugin.type, name, value),
238
239 onSettingsChange: (cb: (settings: any) => void) => this.onSettingsChangeCallbacks.push(cb)
240 }
241 }
242
243 private buildStorageManager (): PluginStorageManager {
244 return {
245 getData: (key: string) => PluginModel.getData(this.plugin.name, this.plugin.type, key),
246
247 storeData: (key: string, data: any) => PluginModel.storeData(this.plugin.name, this.plugin.type, key, data)
248 }
249 }
250
251 private buildVideoLanguageManager (): PluginVideoLanguageManager {
252 return {
253 addLanguage: (key: string, label: string) => {
254 return this.addConstant({ npmName: this.npmName, type: 'language', obj: VIDEO_LANGUAGES, key, label })
255 },
256
257 deleteLanguage: (key: string) => {
258 return this.deleteConstant({ npmName: this.npmName, type: 'language', obj: VIDEO_LANGUAGES, key })
259 }
260 }
261 }
262
263 private buildVideoCategoryManager (): PluginVideoCategoryManager {
264 return {
265 addCategory: (key: number, label: string) => {
266 return this.addConstant({ npmName: this.npmName, type: 'category', obj: VIDEO_CATEGORIES, key, label })
267 },
268
269 deleteCategory: (key: number) => {
270 return this.deleteConstant({ npmName: this.npmName, type: 'category', obj: VIDEO_CATEGORIES, key })
271 }
272 }
273 }
274
275 private buildVideoPrivacyManager (): PluginVideoPrivacyManager {
276 return {
277 deletePrivacy: (key: number) => {
278 return this.deleteConstant({ npmName: this.npmName, type: 'privacy', obj: VIDEO_PRIVACIES, key })
279 }
280 }
281 }
282
283 private buildPlaylistPrivacyManager (): PluginPlaylistPrivacyManager {
284 return {
285 deletePlaylistPrivacy: (key: number) => {
286 return this.deleteConstant({ npmName: this.npmName, type: 'playlistPrivacy', obj: VIDEO_PLAYLIST_PRIVACIES, key })
287 }
288 }
289 }
290
291 private buildVideoLicenceManager (): PluginVideoLicenceManager {
292 return {
293 addLicence: (key: number, label: string) => {
294 return this.addConstant({ npmName: this.npmName, type: 'licence', obj: VIDEO_LICENCES, key, label })
295 },
296
297 deleteLicence: (key: number) => {
298 return this.deleteConstant({ npmName: this.npmName, type: 'licence', obj: VIDEO_LICENCES, key })
299 }
300 }
301 }
302
303 private addConstant<T extends string | number> (parameters: {
304 npmName: string
305 type: AlterableVideoConstant
306 obj: VideoConstant
307 key: T
308 label: string
309 }) {
310 const { npmName, type, obj, key, label } = parameters
311
312 if (obj[key]) {
313 logger.warn('Cannot add %s %s by plugin %s: key already exists.', type, npmName, key)
314 return false
315 }
316
317 if (!this.updatedVideoConstants[type][npmName]) {
318 this.updatedVideoConstants[type][npmName] = {
319 added: [],
320 deleted: []
321 }
322 }
323
324 this.updatedVideoConstants[type][npmName].added.push({ key, label })
325 obj[key] = label
326
327 return true
328 }
329
330 private deleteConstant<T extends string | number> (parameters: {
331 npmName: string
332 type: AlterableVideoConstant
333 obj: VideoConstant
334 key: T
335 }) {
336 const { npmName, type, obj, key } = parameters
337
338 if (!obj[key]) {
339 logger.warn('Cannot delete %s %s by plugin %s: key does not exist.', type, npmName, key)
340 return false
341 }
342
343 if (!this.updatedVideoConstants[type][npmName]) {
344 this.updatedVideoConstants[type][npmName] = {
345 added: [],
346 deleted: []
347 }
348 }
349
350 this.updatedVideoConstants[type][npmName].deleted.push({ key, label: obj[key] })
351 delete obj[key]
352
353 return true
354 }
355}
diff --git a/server/lib/redis.ts b/server/lib/redis.ts
index f77d0b62c..b4cd6f8e7 100644
--- a/server/lib/redis.ts
+++ b/server/lib/redis.ts
@@ -6,13 +6,14 @@ import {
6 CONTACT_FORM_LIFETIME, 6 CONTACT_FORM_LIFETIME,
7 USER_EMAIL_VERIFY_LIFETIME, 7 USER_EMAIL_VERIFY_LIFETIME,
8 USER_PASSWORD_RESET_LIFETIME, 8 USER_PASSWORD_RESET_LIFETIME,
9 USER_PASSWORD_CREATE_LIFETIME,
9 VIDEO_VIEW_LIFETIME, 10 VIDEO_VIEW_LIFETIME,
10 WEBSERVER 11 WEBSERVER
11} from '../initializers/constants' 12} from '../initializers/constants'
12import { CONFIG } from '../initializers/config' 13import { CONFIG } from '../initializers/config'
13 14
14type CachedRoute = { 15type CachedRoute = {
15 body: string, 16 body: string
16 contentType?: string 17 contentType?: string
17 statusCode?: string 18 statusCode?: string
18} 19}
@@ -24,7 +25,8 @@ class Redis {
24 private client: RedisClient 25 private client: RedisClient
25 private prefix: string 26 private prefix: string
26 27
27 private constructor () {} 28 private constructor () {
29 }
28 30
29 init () { 31 init () {
30 // Already initialized 32 // Already initialized
@@ -49,9 +51,9 @@ class Redis {
49 return Object.assign({}, 51 return Object.assign({},
50 (CONFIG.REDIS.AUTH && CONFIG.REDIS.AUTH != null) ? { password: CONFIG.REDIS.AUTH } : {}, 52 (CONFIG.REDIS.AUTH && CONFIG.REDIS.AUTH != null) ? { password: CONFIG.REDIS.AUTH } : {},
51 (CONFIG.REDIS.DB) ? { db: CONFIG.REDIS.DB } : {}, 53 (CONFIG.REDIS.DB) ? { db: CONFIG.REDIS.DB } : {},
52 (CONFIG.REDIS.HOSTNAME && CONFIG.REDIS.PORT) ? 54 (CONFIG.REDIS.HOSTNAME && CONFIG.REDIS.PORT)
53 { host: CONFIG.REDIS.HOSTNAME, port: CONFIG.REDIS.PORT } : 55 ? { host: CONFIG.REDIS.HOSTNAME, port: CONFIG.REDIS.PORT }
54 { path: CONFIG.REDIS.SOCKET } 56 : { path: CONFIG.REDIS.SOCKET }
55 ) 57 )
56 } 58 }
57 59
@@ -63,7 +65,7 @@ class Redis {
63 return this.prefix 65 return this.prefix
64 } 66 }
65 67
66 /************* Forgot password *************/ 68 /* ************ Forgot password ************ */
67 69
68 async setResetPasswordVerificationString (userId: number) { 70 async setResetPasswordVerificationString (userId: number) {
69 const generatedString = await generateRandomString(32) 71 const generatedString = await generateRandomString(32)
@@ -73,11 +75,19 @@ class Redis {
73 return generatedString 75 return generatedString
74 } 76 }
75 77
78 async setCreatePasswordVerificationString (userId: number) {
79 const generatedString = await generateRandomString(32)
80
81 await this.setValue(this.generateResetPasswordKey(userId), generatedString, USER_PASSWORD_CREATE_LIFETIME)
82
83 return generatedString
84 }
85
76 async getResetPasswordLink (userId: number) { 86 async getResetPasswordLink (userId: number) {
77 return this.getValue(this.generateResetPasswordKey(userId)) 87 return this.getValue(this.generateResetPasswordKey(userId))
78 } 88 }
79 89
80 /************* Email verification *************/ 90 /* ************ Email verification ************ */
81 91
82 async setVerifyEmailVerificationString (userId: number) { 92 async setVerifyEmailVerificationString (userId: number) {
83 const generatedString = await generateRandomString(32) 93 const generatedString = await generateRandomString(32)
@@ -91,7 +101,7 @@ class Redis {
91 return this.getValue(this.generateVerifyEmailKey(userId)) 101 return this.getValue(this.generateVerifyEmailKey(userId))
92 } 102 }
93 103
94 /************* Contact form per IP *************/ 104 /* ************ Contact form per IP ************ */
95 105
96 async setContactFormIp (ip: string) { 106 async setContactFormIp (ip: string) {
97 return this.setValue(this.generateContactFormKey(ip), '1', CONTACT_FORM_LIFETIME) 107 return this.setValue(this.generateContactFormKey(ip), '1', CONTACT_FORM_LIFETIME)
@@ -101,7 +111,7 @@ class Redis {
101 return this.exists(this.generateContactFormKey(ip)) 111 return this.exists(this.generateContactFormKey(ip))
102 } 112 }
103 113
104 /************* Views per IP *************/ 114 /* ************ Views per IP ************ */
105 115
106 setIPVideoView (ip: string, videoUUID: string) { 116 setIPVideoView (ip: string, videoUUID: string) {
107 return this.setValue(this.generateViewKey(ip, videoUUID), '1', VIDEO_VIEW_LIFETIME) 117 return this.setValue(this.generateViewKey(ip, videoUUID), '1', VIDEO_VIEW_LIFETIME)
@@ -111,7 +121,7 @@ class Redis {
111 return this.exists(this.generateViewKey(ip, videoUUID)) 121 return this.exists(this.generateViewKey(ip, videoUUID))
112 } 122 }
113 123
114 /************* API cache *************/ 124 /* ************ API cache ************ */
115 125
116 async getCachedRoute (req: express.Request) { 126 async getCachedRoute (req: express.Request) {
117 const cached = await this.getObject(this.generateCachedRouteKey(req)) 127 const cached = await this.getObject(this.generateCachedRouteKey(req))
@@ -120,17 +130,17 @@ class Redis {
120 } 130 }
121 131
122 setCachedRoute (req: express.Request, body: any, lifetime: number, contentType?: string, statusCode?: number) { 132 setCachedRoute (req: express.Request, body: any, lifetime: number, contentType?: string, statusCode?: number) {
123 const cached: CachedRoute = Object.assign({}, { 133 const cached: CachedRoute = Object.assign(
124 body: body.toString() 134 {},
125 }, 135 { body: body.toString() },
126 (contentType) ? { contentType } : null, 136 (contentType) ? { contentType } : null,
127 (statusCode) ? { statusCode: statusCode.toString() } : null 137 (statusCode) ? { statusCode: statusCode.toString() } : null
128 ) 138 )
129 139
130 return this.setObject(this.generateCachedRouteKey(req), cached, lifetime) 140 return this.setObject(this.generateCachedRouteKey(req), cached, lifetime)
131 } 141 }
132 142
133 /************* Video views *************/ 143 /* ************ Video views ************ */
134 144
135 addVideoView (videoId: number) { 145 addVideoView (videoId: number) {
136 const keyIncr = this.generateVideoViewKey(videoId) 146 const keyIncr = this.generateVideoViewKey(videoId)
@@ -173,7 +183,7 @@ class Redis {
173 ]) 183 ])
174 } 184 }
175 185
176 /************* Keys generation *************/ 186 /* ************ Keys generation ************ */
177 187
178 generateCachedRouteKey (req: express.Request) { 188 generateCachedRouteKey (req: express.Request) {
179 return req.method + '-' + req.originalUrl 189 return req.method + '-' + req.originalUrl
@@ -207,7 +217,7 @@ class Redis {
207 return 'contact-form-' + ip 217 return 'contact-form-' + ip
208 } 218 }
209 219
210 /************* Redis helpers *************/ 220 /* ************ Redis helpers ************ */
211 221
212 private getValue (key: string) { 222 private getValue (key: string) {
213 return new Promise<string>((res, rej) => { 223 return new Promise<string>((res, rej) => {
@@ -265,7 +275,7 @@ class Redis {
265 }) 275 })
266 } 276 }
267 277
268 private setObject (key: string, obj: { [ id: string ]: string }, expirationMilliseconds: number) { 278 private setObject (key: string, obj: { [id: string]: string }, expirationMilliseconds: number) {
269 return new Promise<void>((res, rej) => { 279 return new Promise<void>((res, rej) => {
270 this.client.hmset(this.prefix + key, obj, (err, ok) => { 280 this.client.hmset(this.prefix + key, obj, (err, ok) => {
271 if (err) return rej(err) 281 if (err) return rej(err)
@@ -282,7 +292,7 @@ class Redis {
282 } 292 }
283 293
284 private getObject (key: string) { 294 private getObject (key: string) {
285 return new Promise<{ [ id: string ]: string }>((res, rej) => { 295 return new Promise<{ [id: string]: string }>((res, rej) => {
286 this.client.hgetall(this.prefix + key, (err, value) => { 296 this.client.hgetall(this.prefix + key, (err, value) => {
287 if (err) return rej(err) 297 if (err) return rej(err)
288 298
diff --git a/server/lib/redundancy.ts b/server/lib/redundancy.ts
index 1b4ecd7c0..361b401a5 100644
--- a/server/lib/redundancy.ts
+++ b/server/lib/redundancy.ts
@@ -1,8 +1,12 @@
1import { VideoRedundancyModel } from '../models/redundancy/video-redundancy' 1import { VideoRedundancyModel } from '../models/redundancy/video-redundancy'
2import { sendUndoCacheFile } from './activitypub/send' 2import { sendUndoCacheFile } from './activitypub/send'
3import { Transaction } from 'sequelize' 3import { Transaction } from 'sequelize'
4import { getServerActor } from '../helpers/utils' 4import { MActorSignature, MVideoRedundancyVideo } from '@server/typings/models'
5import { MVideoRedundancyVideo } from '@server/typings/models' 5import { CONFIG } from '@server/initializers/config'
6import { logger } from '@server/helpers/logger'
7import { ActorFollowModel } from '@server/models/activitypub/actor-follow'
8import { Activity } from '@shared/models'
9import { getServerActor } from '@server/models/application/application'
6 10
7async function removeVideoRedundancy (videoRedundancy: MVideoRedundancyVideo, t?: Transaction) { 11async function removeVideoRedundancy (videoRedundancy: MVideoRedundancyVideo, t?: Transaction) {
8 const serverActor = await getServerActor() 12 const serverActor = await getServerActor()
@@ -13,17 +17,38 @@ async function removeVideoRedundancy (videoRedundancy: MVideoRedundancyVideo, t?
13 await videoRedundancy.destroy({ transaction: t }) 17 await videoRedundancy.destroy({ transaction: t })
14} 18}
15 19
16async function removeRedundancyOf (serverId: number) { 20async function removeRedundanciesOfServer (serverId: number) {
17 const videosRedundancy = await VideoRedundancyModel.listLocalOfServer(serverId) 21 const redundancies = await VideoRedundancyModel.listLocalOfServer(serverId)
18 22
19 for (const redundancy of videosRedundancy) { 23 for (const redundancy of redundancies) {
20 await removeVideoRedundancy(redundancy) 24 await removeVideoRedundancy(redundancy)
21 } 25 }
22} 26}
23 27
28async function isRedundancyAccepted (activity: Activity, byActor: MActorSignature) {
29 const configAcceptFrom = CONFIG.REMOTE_REDUNDANCY.VIDEOS.ACCEPT_FROM
30 if (configAcceptFrom === 'nobody') {
31 logger.info('Do not accept remote redundancy %s due instance accept policy.', activity.id)
32 return false
33 }
34
35 if (configAcceptFrom === 'followings') {
36 const serverActor = await getServerActor()
37 const allowed = await ActorFollowModel.isFollowedBy(byActor.id, serverActor.id)
38
39 if (allowed !== true) {
40 logger.info('Do not accept remote redundancy %s because actor %s is not followed by our instance.', activity.id, byActor.url)
41 return false
42 }
43 }
44
45 return true
46}
47
24// --------------------------------------------------------------------------- 48// ---------------------------------------------------------------------------
25 49
26export { 50export {
27 removeRedundancyOf, 51 isRedundancyAccepted,
52 removeRedundanciesOfServer,
28 removeVideoRedundancy 53 removeVideoRedundancy
29} 54}
diff --git a/server/lib/schedulers/auto-follow-index-instances.ts b/server/lib/schedulers/auto-follow-index-instances.ts
index dd326bc1e..a57436a45 100644
--- a/server/lib/schedulers/auto-follow-index-instances.ts
+++ b/server/lib/schedulers/auto-follow-index-instances.ts
@@ -6,7 +6,7 @@ import { chunk } from 'lodash'
6import { doRequest } from '@server/helpers/requests' 6import { doRequest } from '@server/helpers/requests'
7import { ActorFollowModel } from '@server/models/activitypub/actor-follow' 7import { ActorFollowModel } from '@server/models/activitypub/actor-follow'
8import { JobQueue } from '@server/lib/job-queue' 8import { JobQueue } from '@server/lib/job-queue'
9import { getServerActor } from '@server/helpers/utils' 9import { getServerActor } from '@server/models/application/application'
10 10
11export class AutoFollowIndexInstances extends AbstractScheduler { 11export class AutoFollowIndexInstances extends AbstractScheduler {
12 12
@@ -41,7 +41,11 @@ export class AutoFollowIndexInstances extends AbstractScheduler {
41 41
42 this.lastCheck = new Date() 42 this.lastCheck = new Date()
43 43
44 const { body } = await doRequest({ uri, qs, json: true }) 44 const { body } = await doRequest<any>({ uri, qs, json: true })
45 if (!body.data || Array.isArray(body.data) === false) {
46 logger.error('Cannot auto follow instances of index %s: bad URL format. Please check the auto follow URL.', indexUrl)
47 return
48 }
45 49
46 const hosts: string[] = body.data.map(o => o.host) 50 const hosts: string[] = body.data.map(o => o.host)
47 const chunks = chunk(hosts, 20) 51 const chunks = chunk(hosts, 20)
@@ -57,8 +61,7 @@ export class AutoFollowIndexInstances extends AbstractScheduler {
57 isAutoFollow: true 61 isAutoFollow: true
58 } 62 }
59 63
60 await JobQueue.Instance.createJob({ type: 'activitypub-follow', payload }) 64 JobQueue.Instance.createJob({ type: 'activitypub-follow', payload })
61 .catch(err => logger.error('Cannot create follow job for %s.', unfollowedHost, err))
62 } 65 }
63 } 66 }
64 67
diff --git a/server/lib/schedulers/plugins-check-scheduler.ts b/server/lib/schedulers/plugins-check-scheduler.ts
index 7ff41e639..014993e94 100644
--- a/server/lib/schedulers/plugins-check-scheduler.ts
+++ b/server/lib/schedulers/plugins-check-scheduler.ts
@@ -43,7 +43,7 @@ export class PluginsCheckScheduler extends AbstractScheduler {
43 const results = await getLatestPluginsVersion(npmNames) 43 const results = await getLatestPluginsVersion(npmNames)
44 44
45 for (const result of results) { 45 for (const result of results) {
46 const plugin = pluginIndex[ result.npmName ] 46 const plugin = pluginIndex[result.npmName]
47 if (!result.latestVersion) continue 47 if (!result.latestVersion) continue
48 48
49 if ( 49 if (
diff --git a/server/lib/schedulers/remove-old-views-scheduler.ts b/server/lib/schedulers/remove-old-views-scheduler.ts
index 39fbb9163..5ae87fe50 100644
--- a/server/lib/schedulers/remove-old-views-scheduler.ts
+++ b/server/lib/schedulers/remove-old-views-scheduler.ts
@@ -1,9 +1,7 @@
1import { logger } from '../../helpers/logger' 1import { logger } from '../../helpers/logger'
2import { AbstractScheduler } from './abstract-scheduler' 2import { AbstractScheduler } from './abstract-scheduler'
3import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants' 3import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants'
4import { UserVideoHistoryModel } from '../../models/account/user-video-history'
5import { CONFIG } from '../../initializers/config' 4import { CONFIG } from '../../initializers/config'
6import { isTestInstance } from '../../helpers/core-utils'
7import { VideoViewModel } from '../../models/video/video-views' 5import { VideoViewModel } from '../../models/video/video-views'
8 6
9export class RemoveOldViewsScheduler extends AbstractScheduler { 7export class RemoveOldViewsScheduler extends AbstractScheduler {
diff --git a/server/lib/schedulers/update-videos-scheduler.ts b/server/lib/schedulers/update-videos-scheduler.ts
index 350a335d3..d32c1c068 100644
--- a/server/lib/schedulers/update-videos-scheduler.ts
+++ b/server/lib/schedulers/update-videos-scheduler.ts
@@ -2,9 +2,8 @@ import { logger } from '../../helpers/logger'
2import { AbstractScheduler } from './abstract-scheduler' 2import { 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/videos'
6import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants' 6import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants'
7import { VideoPrivacy } from '../../../shared/models/videos'
8import { Notifier } from '../notifier' 7import { Notifier } from '../notifier'
9import { sequelizeTypescript } from '../../initializers/database' 8import { sequelizeTypescript } from '../../initializers/database'
10import { MVideoFullLight } from '@server/typings/models' 9import { MVideoFullLight } from '@server/typings/models'
diff --git a/server/lib/schedulers/videos-redundancy-scheduler.ts b/server/lib/schedulers/videos-redundancy-scheduler.ts
index c1c91b656..8da9d52b5 100644
--- a/server/lib/schedulers/videos-redundancy-scheduler.ts
+++ b/server/lib/schedulers/videos-redundancy-scheduler.ts
@@ -1,16 +1,15 @@
1import { AbstractScheduler } from './abstract-scheduler' 1import { AbstractScheduler } from './abstract-scheduler'
2import { HLS_REDUNDANCY_DIRECTORY, REDUNDANCY, VIDEO_IMPORT_TIMEOUT, WEBSERVER } from '../../initializers/constants' 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 { VideosRedundancyStrategy } from '../../../shared/models/redundancy'
5import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' 5import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy'
6import { downloadWebTorrentVideo, generateMagnetUri } from '../../helpers/webtorrent' 6import { downloadWebTorrentVideo, generateMagnetUri } from '../../helpers/webtorrent'
7import { join } from 'path' 7import { join } from 'path'
8import { move } from 'fs-extra' 8import { move } from 'fs-extra'
9import { getServerActor } from '../../helpers/utils'
10import { sendCreateCacheFile, sendUpdateCacheFile } from '../activitypub/send' 9import { sendCreateCacheFile, sendUpdateCacheFile } from '../activitypub/send'
11import { getVideoCacheFileActivityPubUrl, getVideoCacheStreamingPlaylistActivityPubUrl } from '../activitypub/url' 10import { getVideoCacheFileActivityPubUrl, getVideoCacheStreamingPlaylistActivityPubUrl } from '../activitypub/url'
12import { removeVideoRedundancy } from '../redundancy' 11import { removeVideoRedundancy } from '../redundancy'
13import { getOrCreateVideoAndAccountAndChannel } from '../activitypub' 12import { getOrCreateVideoAndAccountAndChannel } from '../activitypub/videos'
14import { downloadPlaylistSegments } from '../hls' 13import { downloadPlaylistSegments } from '../hls'
15import { CONFIG } from '../../initializers/config' 14import { CONFIG } from '../../initializers/config'
16import { 15import {
@@ -25,11 +24,13 @@ import {
25 MVideoWithAllFiles 24 MVideoWithAllFiles
26} from '@server/typings/models' 25} from '@server/typings/models'
27import { getVideoFilename } from '../video-paths' 26import { getVideoFilename } from '../video-paths'
27import { VideoModel } from '@server/models/video/video'
28import { getServerActor } from '@server/models/application/application'
28 29
29type CandidateToDuplicate = { 30type CandidateToDuplicate = {
30 redundancy: VideosRedundancy, 31 redundancy: VideosRedundancyStrategy
31 video: MVideoWithAllFiles, 32 video: MVideoWithAllFiles
32 files: MVideoFile[], 33 files: MVideoFile[]
33 streamingPlaylists: MStreamingPlaylistFiles[] 34 streamingPlaylists: MStreamingPlaylistFiles[]
34} 35}
35 36
@@ -41,7 +42,7 @@ function isMVideoRedundancyFileVideo (
41 42
42export class VideosRedundancyScheduler extends AbstractScheduler { 43export class VideosRedundancyScheduler extends AbstractScheduler {
43 44
44 private static instance: AbstractScheduler 45 private static instance: VideosRedundancyScheduler
45 46
46 protected schedulerIntervalMs = CONFIG.REDUNDANCY.VIDEOS.CHECK_INTERVAL 47 protected schedulerIntervalMs = CONFIG.REDUNDANCY.VIDEOS.CHECK_INTERVAL
47 48
@@ -49,6 +50,22 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
49 super() 50 super()
50 } 51 }
51 52
53 async createManualRedundancy (videoId: number) {
54 const videoToDuplicate = await VideoModel.loadWithFiles(videoId)
55
56 if (!videoToDuplicate) {
57 logger.warn('Video to manually duplicate %d does not exist anymore.', videoId)
58 return
59 }
60
61 return this.createVideoRedundancies({
62 video: videoToDuplicate,
63 redundancy: null,
64 files: videoToDuplicate.VideoFiles,
65 streamingPlaylists: videoToDuplicate.VideoStreamingPlaylists
66 })
67 }
68
52 protected async internalExecute () { 69 protected async internalExecute () {
53 for (const redundancyConfig of CONFIG.REDUNDANCY.VIDEOS.STRATEGIES) { 70 for (const redundancyConfig of CONFIG.REDUNDANCY.VIDEOS.STRATEGIES) {
54 logger.info('Running redundancy scheduler for strategy %s.', redundancyConfig.strategy) 71 logger.info('Running redundancy scheduler for strategy %s.', redundancyConfig.strategy)
@@ -94,7 +111,7 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
94 for (const redundancyModel of expired) { 111 for (const redundancyModel of expired) {
95 try { 112 try {
96 const redundancyConfig = CONFIG.REDUNDANCY.VIDEOS.STRATEGIES.find(s => s.strategy === redundancyModel.strategy) 113 const redundancyConfig = CONFIG.REDUNDANCY.VIDEOS.STRATEGIES.find(s => s.strategy === redundancyModel.strategy)
97 const candidate = { 114 const candidate: CandidateToDuplicate = {
98 redundancy: redundancyConfig, 115 redundancy: redundancyConfig,
99 video: null, 116 video: null,
100 files: [], 117 files: [],
@@ -140,7 +157,7 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
140 } 157 }
141 } 158 }
142 159
143 private findVideoToDuplicate (cache: VideosRedundancy) { 160 private findVideoToDuplicate (cache: VideosRedundancyStrategy) {
144 if (cache.strategy === 'most-views') { 161 if (cache.strategy === 'most-views') {
145 return VideoRedundancyModel.findMostViewToDuplicate(REDUNDANCY.VIDEOS.RANDOMIZED_FACTOR) 162 return VideoRedundancyModel.findMostViewToDuplicate(REDUNDANCY.VIDEOS.RANDOMIZED_FACTOR)
146 } 163 }
@@ -187,13 +204,21 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
187 } 204 }
188 } 205 }
189 206
190 private async createVideoFileRedundancy (redundancy: VideosRedundancy, video: MVideoAccountLight, fileArg: MVideoFile) { 207 private async createVideoFileRedundancy (redundancy: VideosRedundancyStrategy | null, video: MVideoAccountLight, fileArg: MVideoFile) {
208 let strategy = 'manual'
209 let expiresOn: Date = null
210
211 if (redundancy) {
212 strategy = redundancy.strategy
213 expiresOn = this.buildNewExpiration(redundancy.minLifetime)
214 }
215
191 const file = fileArg as MVideoFileVideo 216 const file = fileArg as MVideoFileVideo
192 file.Video = video 217 file.Video = video
193 218
194 const serverActor = await getServerActor() 219 const serverActor = await getServerActor()
195 220
196 logger.info('Duplicating %s - %d in videos redundancy with "%s" strategy.', video.url, file.resolution, redundancy.strategy) 221 logger.info('Duplicating %s - %d in videos redundancy with "%s" strategy.', video.url, file.resolution, strategy)
197 222
198 const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() 223 const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
199 const magnetUri = generateMagnetUri(video, file, baseUrlHttp, baseUrlWs) 224 const magnetUri = generateMagnetUri(video, file, baseUrlHttp, baseUrlWs)
@@ -204,10 +229,10 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
204 await move(tmpPath, destPath, { overwrite: true }) 229 await move(tmpPath, destPath, { overwrite: true })
205 230
206 const createdModel: MVideoRedundancyFileVideo = await VideoRedundancyModel.create({ 231 const createdModel: MVideoRedundancyFileVideo = await VideoRedundancyModel.create({
207 expiresOn: this.buildNewExpiration(redundancy.minLifetime), 232 expiresOn,
208 url: getVideoCacheFileActivityPubUrl(file), 233 url: getVideoCacheFileActivityPubUrl(file),
209 fileUrl: video.getVideoRedundancyUrl(file, WEBSERVER.URL), 234 fileUrl: video.getVideoRedundancyUrl(file, WEBSERVER.URL),
210 strategy: redundancy.strategy, 235 strategy,
211 videoFileId: file.id, 236 videoFileId: file.id,
212 actorId: serverActor.id 237 actorId: serverActor.id
213 }) 238 })
@@ -220,25 +245,33 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
220 } 245 }
221 246
222 private async createStreamingPlaylistRedundancy ( 247 private async createStreamingPlaylistRedundancy (
223 redundancy: VideosRedundancy, 248 redundancy: VideosRedundancyStrategy,
224 video: MVideoAccountLight, 249 video: MVideoAccountLight,
225 playlistArg: MStreamingPlaylist 250 playlistArg: MStreamingPlaylist
226 ) { 251 ) {
252 let strategy = 'manual'
253 let expiresOn: Date = null
254
255 if (redundancy) {
256 strategy = redundancy.strategy
257 expiresOn = this.buildNewExpiration(redundancy.minLifetime)
258 }
259
227 const playlist = playlistArg as MStreamingPlaylistVideo 260 const playlist = playlistArg as MStreamingPlaylistVideo
228 playlist.Video = video 261 playlist.Video = video
229 262
230 const serverActor = await getServerActor() 263 const serverActor = await getServerActor()
231 264
232 logger.info('Duplicating %s streaming playlist in videos redundancy with "%s" strategy.', video.url, redundancy.strategy) 265 logger.info('Duplicating %s streaming playlist in videos redundancy with "%s" strategy.', video.url, strategy)
233 266
234 const destDirectory = join(HLS_REDUNDANCY_DIRECTORY, video.uuid) 267 const destDirectory = join(HLS_REDUNDANCY_DIRECTORY, video.uuid)
235 await downloadPlaylistSegments(playlist.playlistUrl, destDirectory, VIDEO_IMPORT_TIMEOUT) 268 await downloadPlaylistSegments(playlist.playlistUrl, destDirectory, VIDEO_IMPORT_TIMEOUT)
236 269
237 const createdModel: MVideoRedundancyStreamingPlaylistVideo = await VideoRedundancyModel.create({ 270 const createdModel: MVideoRedundancyStreamingPlaylistVideo = await VideoRedundancyModel.create({
238 expiresOn: this.buildNewExpiration(redundancy.minLifetime), 271 expiresOn,
239 url: getVideoCacheStreamingPlaylistActivityPubUrl(video, playlist), 272 url: getVideoCacheStreamingPlaylistActivityPubUrl(video, playlist),
240 fileUrl: playlist.getVideoRedundancyUrl(WEBSERVER.URL), 273 fileUrl: playlist.getVideoRedundancyUrl(WEBSERVER.URL),
241 strategy: redundancy.strategy, 274 strategy,
242 videoStreamingPlaylistId: playlist.id, 275 videoStreamingPlaylistId: playlist.id,
243 actorId: serverActor.id 276 actorId: serverActor.id
244 }) 277 })
diff --git a/server/lib/thumbnail.ts b/server/lib/thumbnail.ts
index a99f71629..8dbd41771 100644
--- a/server/lib/thumbnail.ts
+++ b/server/lib/thumbnail.ts
@@ -69,7 +69,7 @@ function generateVideoMiniature (video: MVideoThumbnail, videoFile: MVideoFile,
69function createPlaceholderThumbnail (fileUrl: string, video: MVideoThumbnail, type: ThumbnailType, size: ImageSize) { 69function createPlaceholderThumbnail (fileUrl: string, video: MVideoThumbnail, type: ThumbnailType, size: ImageSize) {
70 const { filename, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) 70 const { filename, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
71 71
72 const thumbnail = existingThumbnail ? existingThumbnail : new ThumbnailModel() 72 const thumbnail = existingThumbnail || new ThumbnailModel()
73 73
74 thumbnail.filename = filename 74 thumbnail.filename = filename
75 thumbnail.height = height 75 thumbnail.height = height
@@ -142,18 +142,18 @@ function buildMetadataFromVideo (video: MVideoThumbnail, type: ThumbnailType, si
142} 142}
143 143
144async function createThumbnailFromFunction (parameters: { 144async function createThumbnailFromFunction (parameters: {
145 thumbnailCreator: () => Promise<any>, 145 thumbnailCreator: () => Promise<any>
146 filename: string, 146 filename: string
147 height: number, 147 height: number
148 width: number, 148 width: number
149 type: ThumbnailType, 149 type: ThumbnailType
150 automaticallyGenerated?: boolean, 150 automaticallyGenerated?: boolean
151 fileUrl?: string, 151 fileUrl?: string
152 existingThumbnail?: MThumbnail 152 existingThumbnail?: MThumbnail
153}) { 153}) {
154 const { thumbnailCreator, filename, width, height, type, existingThumbnail, automaticallyGenerated = null, fileUrl = null } = parameters 154 const { thumbnailCreator, filename, width, height, type, existingThumbnail, automaticallyGenerated = null, fileUrl = null } = parameters
155 155
156 const thumbnail = existingThumbnail ? existingThumbnail : new ThumbnailModel() 156 const thumbnail = existingThumbnail || new ThumbnailModel()
157 157
158 thumbnail.filename = filename 158 thumbnail.filename = filename
159 thumbnail.height = height 159 thumbnail.height = height
diff --git a/server/lib/user.ts b/server/lib/user.ts
index c45438d95..8b447583e 100644
--- a/server/lib/user.ts
+++ b/server/lib/user.ts
@@ -1,8 +1,8 @@
1import * as uuidv4 from 'uuid/v4' 1import { v4 as uuidv4 } from 'uuid'
2import { ActivityPubActorType } from '../../shared/models/activitypub' 2import { ActivityPubActorType } from '../../shared/models/activitypub'
3import { SERVER_ACTOR_NAME, WEBSERVER } from '../initializers/constants' 3import { SERVER_ACTOR_NAME, WEBSERVER } from '../initializers/constants'
4import { AccountModel } from '../models/account/account' 4import { AccountModel } from '../models/account/account'
5import { buildActorInstance, getAccountActivityPubUrl, setAsyncActorKeys } from './activitypub' 5import { buildActorInstance, setAsyncActorKeys } from './activitypub/actor'
6import { createLocalVideoChannel } from './video-channel' 6import { createLocalVideoChannel } from './video-channel'
7import { ActorModel } from '../models/activitypub/actor' 7import { ActorModel } from '../models/activitypub/actor'
8import { UserNotificationSettingModel } from '../models/account/user-notification-setting' 8import { UserNotificationSettingModel } from '../models/account/user-notification-setting'
@@ -14,13 +14,14 @@ import { Redis } from './redis'
14import { Emailer } from './emailer' 14import { Emailer } from './emailer'
15import { MAccountDefault, MActorDefault, MChannelActor } from '../typings/models' 15import { MAccountDefault, MActorDefault, MChannelActor } from '../typings/models'
16import { MUser, MUserDefault, MUserId } from '../typings/models/user' 16import { MUser, MUserDefault, MUserId } from '../typings/models/user'
17import { getAccountActivityPubUrl } from './activitypub/url'
17 18
18type ChannelNames = { name: string, displayName: string } 19type ChannelNames = { name: string, displayName: string }
19 20
20async function createUserAccountAndChannelAndPlaylist (parameters: { 21async function createUserAccountAndChannelAndPlaylist (parameters: {
21 userToCreate: MUser, 22 userToCreate: MUser
22 userDisplayName?: string, 23 userDisplayName?: string
23 channelNames?: ChannelNames, 24 channelNames?: ChannelNames
24 validateUser?: boolean 25 validateUser?: boolean
25}): Promise<{ user: MUserDefault, account: MAccountDefault, videoChannel: MChannelActor }> { 26}): Promise<{ user: MUserDefault, account: MAccountDefault, videoChannel: MChannelActor }> {
26 const { userToCreate, userDisplayName, channelNames, validateUser = true } = parameters 27 const { userToCreate, userDisplayName, channelNames, validateUser = true } = parameters
@@ -63,11 +64,11 @@ async function createUserAccountAndChannelAndPlaylist (parameters: {
63} 64}
64 65
65async function createLocalAccountWithoutKeys (parameters: { 66async function createLocalAccountWithoutKeys (parameters: {
66 name: string, 67 name: string
67 displayName?: string, 68 displayName?: string
68 userId: number | null, 69 userId: number | null
69 applicationId: number | null, 70 applicationId: number | null
70 t: Transaction | undefined, 71 t: Transaction | undefined
71 type?: ActivityPubActorType 72 type?: ActivityPubActorType
72}) { 73}) {
73 const { name, displayName, userId, applicationId, t, type = 'Person' } = parameters 74 const { name, displayName, userId, applicationId, t, type = 'Person' } = parameters
diff --git a/server/lib/video-blacklist.ts b/server/lib/video-blacklist.ts
index 1dd45b76d..bd60c6201 100644
--- a/server/lib/video-blacklist.ts
+++ b/server/lib/video-blacklist.ts
@@ -1,23 +1,33 @@
1import { Transaction } from 'sequelize' 1import { Transaction } from 'sequelize'
2import { sequelizeTypescript } from '@server/initializers/database'
3import {
4 MUser,
5 MVideoAccountLight,
6 MVideoBlacklist,
7 MVideoBlacklistVideo,
8 MVideoFullLight,
9 MVideoWithBlacklistLight
10} from '@server/typings/models'
11import { UserRight, VideoBlacklistCreate, VideoBlacklistType } from '../../shared/models'
12import { UserAdminFlag } from '../../shared/models/users/user-flag.model'
13import { logger } from '../helpers/logger'
2import { CONFIG } from '../initializers/config' 14import { CONFIG } from '../initializers/config'
3import { UserRight, VideoBlacklistType } from '../../shared/models'
4import { VideoBlacklistModel } from '../models/video/video-blacklist' 15import { VideoBlacklistModel } from '../models/video/video-blacklist'
5import { logger } from '../helpers/logger' 16import { sendDeleteVideo } from './activitypub/send'
6import { UserAdminFlag } from '../../shared/models/users/user-flag.model' 17import { federateVideoIfNeeded } from './activitypub/videos'
7import { Hooks } from './plugins/hooks'
8import { Notifier } from './notifier' 18import { Notifier } from './notifier'
9import { MUser, MVideoBlacklistVideo, MVideoWithBlacklistLight } from '@server/typings/models' 19import { Hooks } from './plugins/hooks'
10 20
11async function autoBlacklistVideoIfNeeded (parameters: { 21async function autoBlacklistVideoIfNeeded (parameters: {
12 video: MVideoWithBlacklistLight, 22 video: MVideoWithBlacklistLight
13 user?: MUser, 23 user?: MUser
14 isRemote: boolean, 24 isRemote: boolean
15 isNew: boolean, 25 isNew: boolean
16 notify?: boolean, 26 notify?: boolean
17 transaction?: Transaction 27 transaction?: Transaction
18}) { 28}) {
19 const { video, user, isRemote, isNew, notify = true, transaction } = parameters 29 const { video, user, isRemote, isNew, notify = true, transaction } = parameters
20 const doAutoBlacklist = await Hooks.wrapPromiseFun( 30 const doAutoBlacklist = await Hooks.wrapFun(
21 autoBlacklistNeeded, 31 autoBlacklistNeeded,
22 { video, user, isRemote, isNew }, 32 { video, user, isRemote, isNew },
23 'filter:video.auto-blacklist.result' 33 'filter:video.auto-blacklist.result'
@@ -49,10 +59,64 @@ async function autoBlacklistVideoIfNeeded (parameters: {
49 return true 59 return true
50} 60}
51 61
52async function autoBlacklistNeeded (parameters: { 62async function blacklistVideo (videoInstance: MVideoAccountLight, options: VideoBlacklistCreate) {
53 video: MVideoWithBlacklistLight, 63 const blacklist: MVideoBlacklistVideo = await VideoBlacklistModel.create({
54 isRemote: boolean, 64 videoId: videoInstance.id,
55 isNew: boolean, 65 unfederated: options.unfederate === true,
66 reason: options.reason,
67 type: VideoBlacklistType.MANUAL
68 }
69 )
70 blacklist.Video = videoInstance
71
72 if (options.unfederate === true) {
73 await sendDeleteVideo(videoInstance, undefined)
74 }
75
76 Notifier.Instance.notifyOnVideoBlacklist(blacklist)
77}
78
79async function unblacklistVideo (videoBlacklist: MVideoBlacklist, video: MVideoFullLight) {
80 const videoBlacklistType = await sequelizeTypescript.transaction(async t => {
81 const unfederated = videoBlacklist.unfederated
82 const videoBlacklistType = videoBlacklist.type
83
84 await videoBlacklist.destroy({ transaction: t })
85 video.VideoBlacklist = undefined
86
87 // Re federate the video
88 if (unfederated === true) {
89 await federateVideoIfNeeded(video, true, t)
90 }
91
92 return videoBlacklistType
93 })
94
95 Notifier.Instance.notifyOnVideoUnblacklist(video)
96
97 if (videoBlacklistType === VideoBlacklistType.AUTO_BEFORE_PUBLISHED) {
98 Notifier.Instance.notifyOnVideoPublishedAfterRemovedFromAutoBlacklist(video)
99
100 // Delete on object so new video notifications will send
101 delete video.VideoBlacklist
102 Notifier.Instance.notifyOnNewVideoIfNeeded(video)
103 }
104}
105
106// ---------------------------------------------------------------------------
107
108export {
109 autoBlacklistVideoIfNeeded,
110 blacklistVideo,
111 unblacklistVideo
112}
113
114// ---------------------------------------------------------------------------
115
116function autoBlacklistNeeded (parameters: {
117 video: MVideoWithBlacklistLight
118 isRemote: boolean
119 isNew: boolean
56 user?: MUser 120 user?: MUser
57}) { 121}) {
58 const { user, video, isRemote, isNew } = parameters 122 const { user, video, isRemote, isNew } = parameters
@@ -66,9 +130,3 @@ async function autoBlacklistNeeded (parameters: {
66 130
67 return true 131 return true
68} 132}
69
70// ---------------------------------------------------------------------------
71
72export {
73 autoBlacklistVideoIfNeeded
74}
diff --git a/server/lib/video-channel.ts b/server/lib/video-channel.ts
index 41eab456b..102c1088d 100644
--- a/server/lib/video-channel.ts
+++ b/server/lib/video-channel.ts
@@ -1,13 +1,14 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import * as uuidv4 from 'uuid/v4' 2import { v4 as uuidv4 } from 'uuid'
3import { VideoChannelCreate } from '../../shared/models' 3import { VideoChannelCreate } from '../../shared/models'
4import { VideoChannelModel } from '../models/video/video-channel' 4import { VideoChannelModel } from '../models/video/video-channel'
5import { buildActorInstance, federateVideoIfNeeded, getVideoChannelActivityPubUrl } from './activitypub' 5import { buildActorInstance } from './activitypub/actor'
6import { VideoModel } from '../models/video/video' 6import { VideoModel } from '../models/video/video'
7import { MAccountId, MChannelDefault, MChannelId } from '../typings/models' 7import { MAccountId, MChannelDefault, MChannelId } from '../typings/models'
8import { getVideoChannelActivityPubUrl } from './activitypub/url'
9import { federateVideoIfNeeded } from './activitypub/videos'
8 10
9type CustomVideoChannelModelAccount <T extends MAccountId> = MChannelDefault & 11type CustomVideoChannelModelAccount <T extends MAccountId> = MChannelDefault & { Account?: T }
10 { Account?: T }
11 12
12async function createLocalVideoChannel <T extends MAccountId> ( 13async function createLocalVideoChannel <T extends MAccountId> (
13 videoChannelInfo: VideoChannelCreate, 14 videoChannelInfo: VideoChannelCreate,
diff --git a/server/lib/video-comment.ts b/server/lib/video-comment.ts
index b8074e6d2..516c912a9 100644
--- a/server/lib/video-comment.ts
+++ b/server/lib/video-comment.ts
@@ -2,14 +2,14 @@ import * as Sequelize from 'sequelize'
2import { ResultList } from '../../shared/models' 2import { ResultList } from '../../shared/models'
3import { VideoCommentThreadTree } from '../../shared/models/videos/video-comment.model' 3import { VideoCommentThreadTree } from '../../shared/models/videos/video-comment.model'
4import { VideoCommentModel } from '../models/video/video-comment' 4import { VideoCommentModel } from '../models/video/video-comment'
5import { getVideoCommentActivityPubUrl } from './activitypub' 5import { getVideoCommentActivityPubUrl } from './activitypub/url'
6import { sendCreateVideoComment } from './activitypub/send' 6import { sendCreateVideoComment } from './activitypub/send'
7import { MAccountDefault, MComment, MCommentOwnerVideoReply, MVideoFullLight } from '../typings/models' 7import { MAccountDefault, MComment, MCommentOwnerVideoReply, MVideoFullLight } from '../typings/models'
8 8
9async function createVideoComment (obj: { 9async function createVideoComment (obj: {
10 text: string, 10 text: string
11 inReplyToComment: MComment | null, 11 inReplyToComment: MComment | null
12 video: MVideoFullLight, 12 video: MVideoFullLight
13 account: MAccountDefault 13 account: MAccountDefault
14}, t: Sequelize.Transaction) { 14}, t: Sequelize.Transaction) {
15 let originCommentId: number | null = null 15 let originCommentId: number | null = null
diff --git a/server/lib/video-paths.ts b/server/lib/video-paths.ts
index fe0a004e4..05aaca8af 100644
--- a/server/lib/video-paths.ts
+++ b/server/lib/video-paths.ts
@@ -1,8 +1,8 @@
1import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoUUID } from '@server/typings/models' 1import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoUUID } from '@server/typings/models'
2import { extractVideo } from './videos'
3import { join } from 'path' 2import { join } from 'path'
4import { CONFIG } from '@server/initializers/config' 3import { CONFIG } from '@server/initializers/config'
5import { HLS_REDUNDANCY_DIRECTORY, HLS_STREAMING_PLAYLIST_DIRECTORY } from '@server/initializers/constants' 4import { HLS_REDUNDANCY_DIRECTORY, HLS_STREAMING_PLAYLIST_DIRECTORY } from '@server/initializers/constants'
5import { extractVideo } from '@server/helpers/video'
6 6
7// ################## Video file name ################## 7// ################## Video file name ##################
8 8
diff --git a/server/lib/video-playlist.ts b/server/lib/video-playlist.ts
index 29b70cfda..75fbd6896 100644
--- a/server/lib/video-playlist.ts
+++ b/server/lib/video-playlist.ts
@@ -1,7 +1,7 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import { VideoPlaylistModel } from '../models/video/video-playlist' 2import { VideoPlaylistModel } from '../models/video/video-playlist'
3import { VideoPlaylistPrivacy } from '../../shared/models/videos/playlist/video-playlist-privacy.model' 3import { VideoPlaylistPrivacy } from '../../shared/models/videos/playlist/video-playlist-privacy.model'
4import { getVideoPlaylistActivityPubUrl } from './activitypub' 4import { getVideoPlaylistActivityPubUrl } from './activitypub/url'
5import { VideoPlaylistType } from '../../shared/models/videos/playlist/video-playlist-type.model' 5import { VideoPlaylistType } from '../../shared/models/videos/playlist/video-playlist-type.model'
6import { MAccount } from '../typings/models' 6import { MAccount } from '../typings/models'
7import { MVideoPlaylistOwner } from '../typings/models/video/video-playlist' 7import { MVideoPlaylistOwner } from '../typings/models/video/video-playlist'
diff --git a/server/lib/video-transcoding.ts b/server/lib/video-transcoding.ts
index 0d5b3ae39..dcda82e0a 100644
--- a/server/lib/video-transcoding.ts
+++ b/server/lib/video-transcoding.ts
@@ -3,6 +3,7 @@ import { basename, extname as extnameUtil, join } from 'path'
3import { 3import {
4 canDoQuickTranscode, 4 canDoQuickTranscode,
5 getDurationFromVideoFile, 5 getDurationFromVideoFile,
6 getMetadataFromFile,
6 getVideoFileFPS, 7 getVideoFileFPS,
7 transcode, 8 transcode,
8 TranscodeOptions, 9 TranscodeOptions,
@@ -202,10 +203,11 @@ async function generateHlsPlaylist (video: MVideoWithFile, resolution: VideoReso
202 203
203 newVideoFile.size = stats.size 204 newVideoFile.size = stats.size
204 newVideoFile.fps = await getVideoFileFPS(videoFilePath) 205 newVideoFile.fps = await getVideoFileFPS(videoFilePath)
206 newVideoFile.metadata = await getMetadataFromFile(videoFilePath)
205 207
206 await createTorrentAndSetInfoHash(videoStreamingPlaylist, newVideoFile) 208 await createTorrentAndSetInfoHash(videoStreamingPlaylist, newVideoFile)
207 209
208 await newVideoFile.save() 210 await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined)
209 videoStreamingPlaylist.VideoFiles = await videoStreamingPlaylist.$get('VideoFiles') 211 videoStreamingPlaylist.VideoFiles = await videoStreamingPlaylist.$get('VideoFiles')
210 212
211 video.setHLSPlaylist(videoStreamingPlaylist) 213 video.setHLSPlaylist(videoStreamingPlaylist)
@@ -230,11 +232,13 @@ export {
230async function onVideoFileTranscoding (video: MVideoWithFile, videoFile: MVideoFile, transcodingPath: string, outputPath: string) { 232async function onVideoFileTranscoding (video: MVideoWithFile, videoFile: MVideoFile, transcodingPath: string, outputPath: string) {
231 const stats = await stat(transcodingPath) 233 const stats = await stat(transcodingPath)
232 const fps = await getVideoFileFPS(transcodingPath) 234 const fps = await getVideoFileFPS(transcodingPath)
235 const metadata = await getMetadataFromFile(transcodingPath)
233 236
234 await move(transcodingPath, outputPath) 237 await move(transcodingPath, outputPath)
235 238
236 videoFile.size = stats.size 239 videoFile.size = stats.size
237 videoFile.fps = fps 240 videoFile.fps = fps
241 videoFile.metadata = metadata
238 242
239 await createTorrentAndSetInfoHash(video, videoFile) 243 await createTorrentAndSetInfoHash(video, videoFile)
240 244
diff --git a/server/lib/videos.ts b/server/lib/videos.ts
deleted file mode 100644
index 22e9afbf9..000000000
--- a/server/lib/videos.ts
+++ /dev/null
@@ -1,11 +0,0 @@
1import { isStreamingPlaylist, MStreamingPlaylistVideo, MVideo } from '@server/typings/models'
2
3function extractVideo (videoOrPlaylist: MVideo | MStreamingPlaylistVideo) {
4 return isStreamingPlaylist(videoOrPlaylist)
5 ? videoOrPlaylist.Video
6 : videoOrPlaylist
7}
8
9export {
10 extractVideo
11}
diff --git a/server/middlewares/activitypub.ts b/server/middlewares/activitypub.ts
index c6d8466ac..580606a68 100644
--- a/server/middlewares/activitypub.ts
+++ b/server/middlewares/activitypub.ts
@@ -1,10 +1,12 @@
1import { NextFunction, Request, Response } from 'express' 1import { NextFunction, Request, Response } from 'express'
2import { ActivityPubSignature } from '../../shared' 2import { ActivityDelete, ActivityPubSignature } from '../../shared'
3import { logger } from '../helpers/logger' 3import { logger } from '../helpers/logger'
4import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../helpers/peertube-crypto' 4import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../helpers/peertube-crypto'
5import { ACCEPT_HEADERS, ACTIVITY_PUB, HTTP_SIGNATURE } from '../initializers/constants' 5import { ACCEPT_HEADERS, ACTIVITY_PUB, HTTP_SIGNATURE } from '../initializers/constants'
6import { getOrCreateActorAndServerAndModel } from '../lib/activitypub' 6import { getOrCreateActorAndServerAndModel } from '../lib/activitypub/actor'
7import { loadActorUrlOrGetFromWebfinger } from '../helpers/webfinger' 7import { loadActorUrlOrGetFromWebfinger } from '../helpers/webfinger'
8import { isActorDeleteActivityValid } from '@server/helpers/custom-validators/activitypub/actor'
9import { getAPId } from '@server/helpers/activitypub'
8 10
9async function checkSignature (req: Request, res: Response, next: NextFunction) { 11async function checkSignature (req: Request, res: Response, next: NextFunction) {
10 try { 12 try {
@@ -15,7 +17,7 @@ async function checkSignature (req: Request, res: Response, next: NextFunction)
15 17
16 // Forwarded activity 18 // Forwarded activity
17 const bodyActor = req.body.actor 19 const bodyActor = req.body.actor
18 const bodyActorId = bodyActor && bodyActor.id ? bodyActor.id : bodyActor 20 const bodyActorId = getAPId(bodyActor)
19 if (bodyActorId && bodyActorId !== actor.url) { 21 if (bodyActorId && bodyActorId !== actor.url) {
20 const jsonLDSignatureChecked = await checkJsonLDSignature(req, res) 22 const jsonLDSignatureChecked = await checkJsonLDSignature(req, res)
21 if (jsonLDSignatureChecked !== true) return 23 if (jsonLDSignatureChecked !== true) return
@@ -23,14 +25,20 @@ async function checkSignature (req: Request, res: Response, next: NextFunction)
23 25
24 return next() 26 return next()
25 } catch (err) { 27 } catch (err) {
26 logger.error('Error in ActivityPub signature checker.', err) 28 const activity: ActivityDelete = req.body
29 if (isActorDeleteActivityValid(activity) && activity.object === activity.actor) {
30 logger.debug('Handling signature error on actor delete activity', { err })
31 return res.sendStatus(204)
32 }
33
34 logger.warn('Error in ActivityPub signature checker.', { err })
27 return res.sendStatus(403) 35 return res.sendStatus(403)
28 } 36 }
29} 37}
30 38
31function executeIfActivityPub (req: Request, res: Response, next: NextFunction) { 39function executeIfActivityPub (req: Request, res: Response, next: NextFunction) {
32 const accepted = req.accepts(ACCEPT_HEADERS) 40 const accepted = req.accepts(ACCEPT_HEADERS)
33 if (accepted === false || ACTIVITY_PUB.POTENTIAL_ACCEPT_HEADERS.indexOf(accepted) === -1) { 41 if (accepted === false || ACTIVITY_PUB.POTENTIAL_ACCEPT_HEADERS.includes(accepted) === false) {
34 // Bypass this route 42 // Bypass this route
35 return next('route') 43 return next('route')
36 } 44 }
diff --git a/server/middlewares/csp.ts b/server/middlewares/csp.ts
index d11d70790..f5de69603 100644
--- a/server/middlewares/csp.ts
+++ b/server/middlewares/csp.ts
@@ -3,20 +3,20 @@ import { CONFIG } from '../initializers/config'
3 3
4const baseDirectives = Object.assign({}, 4const baseDirectives = Object.assign({},
5 { 5 {
6 defaultSrc: ["'none'"], // by default, not specifying default-src = '*' 6 defaultSrc: [ '\'none\'' ], // by default, not specifying default-src = '*'
7 connectSrc: ['*', 'data:'], 7 connectSrc: [ '*', 'data:' ],
8 mediaSrc: ["'self'", 'https:', 'blob:'], 8 mediaSrc: [ '\'self\'', 'https:', 'blob:' ],
9 fontSrc: ["'self'", 'data:'], 9 fontSrc: [ '\'self\'', 'data:' ],
10 imgSrc: ["'self'", 'data:', 'blob:'], 10 imgSrc: [ '\'self\'', 'data:', 'blob:' ],
11 scriptSrc: ["'self' 'unsafe-inline' 'unsafe-eval'", 'blob:'], 11 scriptSrc: [ '\'self\' \'unsafe-inline\' \'unsafe-eval\'', 'blob:' ],
12 styleSrc: ["'self' 'unsafe-inline'"], 12 styleSrc: [ '\'self\' \'unsafe-inline\'' ],
13 objectSrc: ["'none'"], // only define to allow plugins, else let defaultSrc 'none' block it 13 objectSrc: [ '\'none\'' ], // only define to allow plugins, else let defaultSrc 'none' block it
14 formAction: ["'self'"], 14 formAction: [ '\'self\'' ],
15 frameAncestors: ["'none'"], 15 frameAncestors: [ '\'none\'' ],
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'", 'blob:'] // instead of deprecated child-src 19 workerSrc: [ '\'self\'', 'blob:' ] // instead of deprecated child-src
20 }, 20 },
21 CONFIG.CSP.REPORT_URI ? { reportUri: CONFIG.CSP.REPORT_URI } : {}, 21 CONFIG.CSP.REPORT_URI ? { reportUri: CONFIG.CSP.REPORT_URI } : {},
22 CONFIG.WEBSERVER.SCHEME === 'https' ? { upgradeInsecureRequests: true } : {} 22 CONFIG.WEBSERVER.SCHEME === 'https' ? { upgradeInsecureRequests: true } : {}
@@ -29,7 +29,7 @@ const baseCSP = helmet.contentSecurityPolicy({
29}) 29})
30 30
31const embedCSP = helmet.contentSecurityPolicy({ 31const embedCSP = helmet.contentSecurityPolicy({
32 directives: Object.assign({}, baseDirectives, { frameAncestors: ['*'] }), 32 directives: Object.assign({}, baseDirectives, { frameAncestors: [ '*' ] }),
33 browserSniff: false, // assumes a modern browser, but allows CDN in front 33 browserSniff: false, // assumes a modern browser, but allows CDN in front
34 reportOnly: CONFIG.CSP.REPORT_ONLY 34 reportOnly: CONFIG.CSP.REPORT_ONLY
35}) 35})
diff --git a/server/middlewares/dnt.ts b/server/middlewares/dnt.ts
index 607def855..dd88005dd 100644
--- a/server/middlewares/dnt.ts
+++ b/server/middlewares/dnt.ts
@@ -1,6 +1,3 @@
1import * as ipaddr from 'ipaddr.js'
2import { format } from 'util'
3
4const advertiseDoNotTrack = (_, res, next) => { 1const advertiseDoNotTrack = (_, res, next) => {
5 res.setHeader('Tk', 'N') 2 res.setHeader('Tk', 'N')
6 return next() 3 return next()
diff --git a/server/middlewares/oauth.ts b/server/middlewares/oauth.ts
index 749f5cccd..9d0eaa51f 100644
--- a/server/middlewares/oauth.ts
+++ b/server/middlewares/oauth.ts
@@ -1,17 +1,8 @@
1import * as express from 'express' 1import * as express from 'express'
2import * as OAuthServer from 'express-oauth-server'
3import { OAUTH_LIFETIME } from '../initializers/constants'
4import { logger } from '../helpers/logger' 2import { logger } from '../helpers/logger'
5import { Socket } from 'socket.io' 3import { Socket } from 'socket.io'
6import { getAccessToken } from '../lib/oauth-model' 4import { getAccessToken } from '../lib/oauth-model'
7 5import { oAuthServer } from '@server/lib/auth'
8const oAuthServer = new OAuthServer({
9 useErrorHandler: true,
10 accessTokenLifetime: OAUTH_LIFETIME.ACCESS_TOKEN,
11 refreshTokenLifetime: OAUTH_LIFETIME.REFRESH_TOKEN,
12 continueMiddleware: true,
13 model: require('../lib/oauth-model')
14})
15 6
16function authenticate (req: express.Request, res: express.Response, next: express.NextFunction, authenticateInQuery = false) { 7function authenticate (req: express.Request, res: express.Response, next: express.NextFunction, authenticateInQuery = false) {
17 const options = authenticateInQuery ? { allowBearerTokensInQueryString: true } : {} 8 const options = authenticateInQuery ? { allowBearerTokensInQueryString: true } : {}
@@ -51,6 +42,7 @@ function authenticateSocket (socket: Socket, next: (err?: any) => void) {
51 42
52 return next() 43 return next()
53 }) 44 })
45 .catch(err => logger.error('Cannot get access token.', { err }))
54} 46}
55 47
56function authenticatePromiseIfNeeded (req: express.Request, res: express.Response, authenticateInQuery = false) { 48function authenticatePromiseIfNeeded (req: express.Request, res: express.Response, authenticateInQuery = false) {
@@ -72,27 +64,11 @@ function optionalAuthenticate (req: express.Request, res: express.Response, next
72 return next() 64 return next()
73} 65}
74 66
75function token (req: express.Request, res: express.Response, next: express.NextFunction) {
76 return oAuthServer.token()(req, res, err => {
77 if (err) {
78 return res.status(err.status)
79 .json({
80 error: err.message,
81 code: err.name
82 })
83 .end()
84 }
85
86 return next()
87 })
88}
89
90// --------------------------------------------------------------------------- 67// ---------------------------------------------------------------------------
91 68
92export { 69export {
93 authenticate, 70 authenticate,
94 authenticateSocket, 71 authenticateSocket,
95 authenticatePromiseIfNeeded, 72 authenticatePromiseIfNeeded,
96 optionalAuthenticate, 73 optionalAuthenticate
97 token
98} 74}
diff --git a/server/middlewares/sort.ts b/server/middlewares/sort.ts
index 8c27e8237..fcbb2902c 100644
--- a/server/middlewares/sort.ts
+++ b/server/middlewares/sort.ts
@@ -1,20 +1,14 @@
1import * as express from 'express' 1import * as express from 'express'
2import { SortType } from '../models/utils' 2import { SortType } from '../models/utils'
3 3
4function setDefaultSort (req: express.Request, res: express.Response, next: express.NextFunction) { 4const setDefaultSort = setDefaultSortFactory('-createdAt')
5 if (!req.query.sort) req.query.sort = '-createdAt'
6
7 return next()
8}
9 5
10function setDefaultSearchSort (req: express.Request, res: express.Response, next: express.NextFunction) { 6const setDefaultVideoRedundanciesSort = setDefaultSortFactory('name')
11 if (!req.query.sort) req.query.sort = '-match'
12 7
13 return next() 8const setDefaultSearchSort = setDefaultSortFactory('-match')
14}
15 9
16function setBlacklistSort (req: express.Request, res: express.Response, next: express.NextFunction) { 10function setBlacklistSort (req: express.Request, res: express.Response, next: express.NextFunction) {
17 let newSort: SortType = { sortModel: undefined, sortValue: '' } 11 const newSort: SortType = { sortModel: undefined, sortValue: '' }
18 12
19 if (!req.query.sort) req.query.sort = '-createdAt' 13 if (!req.query.sort) req.query.sort = '-createdAt'
20 14
@@ -39,5 +33,16 @@ function setBlacklistSort (req: express.Request, res: express.Response, next: ex
39export { 33export {
40 setDefaultSort, 34 setDefaultSort,
41 setDefaultSearchSort, 35 setDefaultSearchSort,
36 setDefaultVideoRedundanciesSort,
42 setBlacklistSort 37 setBlacklistSort
43} 38}
39
40// ---------------------------------------------------------------------------
41
42function setDefaultSortFactory (sort: string) {
43 return (req: express.Request, res: express.Response, next: express.NextFunction) => {
44 if (!req.query.sort) req.query.sort = sort
45
46 return next()
47 }
48}
diff --git a/server/middlewares/validators/activitypub/activity.ts b/server/middlewares/validators/activitypub/activity.ts
index 7582f65e7..7350be5d5 100644
--- a/server/middlewares/validators/activitypub/activity.ts
+++ b/server/middlewares/validators/activitypub/activity.ts
@@ -1,7 +1,7 @@
1import * as express from 'express' 1import * 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 '@server/models/application/application'
5 5
6async function activityPubValidator (req: express.Request, res: express.Response, next: express.NextFunction) { 6async function activityPubValidator (req: express.Request, res: express.Response, next: express.NextFunction) {
7 logger.debug('Checking activity pub parameters') 7 logger.debug('Checking activity pub parameters')
diff --git a/server/middlewares/validators/avatar.ts b/server/middlewares/validators/avatar.ts
index 8623d07e8..2acb97483 100644
--- a/server/middlewares/validators/avatar.ts
+++ b/server/middlewares/validators/avatar.ts
@@ -8,8 +8,8 @@ import { cleanUpReqFiles } from '../../helpers/express-utils'
8 8
9const updateAvatarValidator = [ 9const updateAvatarValidator = [
10 body('avatarfile').custom((value, { req }) => isAvatarFile(req.files)).withMessage( 10 body('avatarfile').custom((value, { req }) => isAvatarFile(req.files)).withMessage(
11 'This file is not supported or too large. Please, make sure it is of the following type : ' 11 'This file is not supported or too large. Please, make sure it is of the following type : ' +
12 + CONSTRAINTS_FIELDS.ACTORS.AVATAR.EXTNAME.join(', ') 12 CONSTRAINTS_FIELDS.ACTORS.AVATAR.EXTNAME.join(', ')
13 ), 13 ),
14 14
15 (req: express.Request, res: express.Response, next: express.NextFunction) => { 15 (req: express.Request, res: express.Response, next: express.NextFunction) => {
diff --git a/server/middlewares/validators/blocklist.ts b/server/middlewares/validators/blocklist.ts
index 47a0b1a1c..27224ff9b 100644
--- a/server/middlewares/validators/blocklist.ts
+++ b/server/middlewares/validators/blocklist.ts
@@ -6,9 +6,9 @@ import { AccountBlocklistModel } from '../../models/account/account-blocklist'
6import { isHostValid } from '../../helpers/custom-validators/servers' 6import { isHostValid } from '../../helpers/custom-validators/servers'
7import { ServerBlocklistModel } from '../../models/server/server-blocklist' 7import { ServerBlocklistModel } from '../../models/server/server-blocklist'
8import { ServerModel } from '../../models/server/server' 8import { ServerModel } from '../../models/server/server'
9import { getServerActor } from '../../helpers/utils'
10import { WEBSERVER } from '../../initializers/constants' 9import { WEBSERVER } from '../../initializers/constants'
11import { doesAccountNameWithHostExist } from '../../helpers/middlewares' 10import { doesAccountNameWithHostExist } from '../../helpers/middlewares'
11import { getServerActor } from '@server/models/application/application'
12 12
13const blockAccountValidator = [ 13const blockAccountValidator = [
14 body('accountName').exists().withMessage('Should have an account name with host'), 14 body('accountName').exists().withMessage('Should have an account name with host'),
@@ -84,12 +84,7 @@ const blockServerValidator = [
84 .end() 84 .end()
85 } 85 }
86 86
87 const server = await ServerModel.loadByHost(host) 87 const server = await ServerModel.loadOrCreateByHost(host)
88 if (!server) {
89 return res.status(404)
90 .send({ error: 'Server host not found.' })
91 .end()
92 }
93 88
94 res.locals.server = server 89 res.locals.server = server
95 90
diff --git a/server/middlewares/validators/config.ts b/server/middlewares/validators/config.ts
index 2d1f61947..dfa549e76 100644
--- a/server/middlewares/validators/config.ts
+++ b/server/middlewares/validators/config.ts
@@ -3,10 +3,10 @@ import { body } from 'express-validator'
3import { isUserNSFWPolicyValid, isUserVideoQuotaDailyValid, isUserVideoQuotaValid } from '../../helpers/custom-validators/users' 3import { isUserNSFWPolicyValid, isUserVideoQuotaDailyValid, isUserVideoQuotaValid } from '../../helpers/custom-validators/users'
4import { logger } from '../../helpers/logger' 4import { logger } from '../../helpers/logger'
5import { CustomConfig } from '../../../shared/models/server/custom-config.model' 5import { CustomConfig } from '../../../shared/models/server/custom-config.model'
6import { Emailer } from '../../lib/emailer'
7import { areValidationErrors } from './utils' 6import { areValidationErrors } from './utils'
8import { isThemeNameValid } from '../../helpers/custom-validators/plugins' 7import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
9import { isThemeRegistered } from '../../lib/plugins/theme-utils' 8import { isThemeRegistered } from '../../lib/plugins/theme-utils'
9import { isEmailEnabled } from '@server/initializers/config'
10 10
11const customConfigUpdateValidator = [ 11const customConfigUpdateValidator = [
12 body('instance.name').exists().withMessage('Should have a valid instance name'), 12 body('instance.name').exists().withMessage('Should have a valid instance name'),
@@ -55,7 +55,7 @@ const customConfigUpdateValidator = [
55 55
56 body('theme.default').custom(v => isThemeNameValid(v) && isThemeRegistered(v)).withMessage('Should have a valid theme'), 56 body('theme.default').custom(v => isThemeNameValid(v) && isThemeRegistered(v)).withMessage('Should have a valid theme'),
57 57
58 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 58 (req: express.Request, res: express.Response, next: express.NextFunction) => {
59 logger.debug('Checking customConfigUpdateValidator parameters', { parameters: req.body }) 59 logger.debug('Checking customConfigUpdateValidator parameters', { parameters: req.body })
60 60
61 if (areValidationErrors(req, res)) return 61 if (areValidationErrors(req, res)) return
@@ -73,7 +73,7 @@ export {
73} 73}
74 74
75function checkInvalidConfigIfEmailDisabled (customConfig: CustomConfig, res: express.Response) { 75function checkInvalidConfigIfEmailDisabled (customConfig: CustomConfig, res: express.Response) {
76 if (Emailer.isEnabled()) return true 76 if (isEmailEnabled()) return true
77 77
78 if (customConfig.signup.requiresEmailVerification === true) { 78 if (customConfig.signup.requiresEmailVerification === true) {
79 res.status(400) 79 res.status(400)
diff --git a/server/middlewares/validators/feeds.ts b/server/middlewares/validators/feeds.ts
index 29f6c87be..f34c2b174 100644
--- a/server/middlewares/validators/feeds.ts
+++ b/server/middlewares/validators/feeds.ts
@@ -22,13 +22,13 @@ function setFeedFormatContentType (req: express.Request, res: express.Response,
22 22
23 let acceptableContentTypes: string[] 23 let acceptableContentTypes: string[]
24 if (format === 'atom' || format === 'atom1') { 24 if (format === 'atom' || format === 'atom1') {
25 acceptableContentTypes = ['application/atom+xml', 'application/xml', 'text/xml'] 25 acceptableContentTypes = [ 'application/atom+xml', 'application/xml', 'text/xml' ]
26 } else if (format === 'json' || format === 'json1') { 26 } else if (format === 'json' || format === 'json1') {
27 acceptableContentTypes = ['application/json'] 27 acceptableContentTypes = [ 'application/json' ]
28 } else if (format === 'rss' || format === 'rss2') { 28 } else if (format === 'rss' || format === 'rss2') {
29 acceptableContentTypes = ['application/rss+xml', 'application/xml', 'text/xml'] 29 acceptableContentTypes = [ 'application/rss+xml', 'application/xml', 'text/xml' ]
30 } else { 30 } else {
31 acceptableContentTypes = ['application/xml', 'text/xml'] 31 acceptableContentTypes = [ 'application/xml', 'text/xml' ]
32 } 32 }
33 33
34 if (req.accepts(acceptableContentTypes)) { 34 if (req.accepts(acceptableContentTypes)) {
diff --git a/server/middlewares/validators/follows.ts b/server/middlewares/validators/follows.ts
index a98d32d86..7808135f7 100644
--- a/server/middlewares/validators/follows.ts
+++ b/server/middlewares/validators/follows.ts
@@ -3,7 +3,6 @@ import { body, param, query } from 'express-validator'
3import { isTestInstance } from '../../helpers/core-utils' 3import { 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'
7import { SERVER_ACTOR_NAME, WEBSERVER } from '../../initializers/constants' 6import { SERVER_ACTOR_NAME, WEBSERVER } from '../../initializers/constants'
8import { ActorFollowModel } from '../../models/activitypub/actor-follow' 7import { ActorFollowModel } from '../../models/activitypub/actor-follow'
9import { areValidationErrors } from './utils' 8import { areValidationErrors } from './utils'
@@ -12,6 +11,7 @@ import { loadActorUrlOrGetFromWebfinger } from '../../helpers/webfinger'
12import { isActorTypeValid, isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor' 11import { isActorTypeValid, isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor'
13import { MActorFollowActorsDefault } from '@server/typings/models' 12import { MActorFollowActorsDefault } from '@server/typings/models'
14import { isFollowStateValid } from '@server/helpers/custom-validators/follows' 13import { isFollowStateValid } from '@server/helpers/custom-validators/follows'
14import { getServerActor } from '@server/models/application/application'
15 15
16const listFollowsValidator = [ 16const listFollowsValidator = [
17 query('state') 17 query('state')
diff --git a/server/middlewares/validators/plugins.ts b/server/middlewares/validators/plugins.ts
index 910d03c29..2cb49ec43 100644
--- a/server/middlewares/validators/plugins.ts
+++ b/server/middlewares/validators/plugins.ts
@@ -1,33 +1,72 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body, param, query } from 'express-validator' 2import { body, param, query, ValidationChain } from 'express-validator'
3import { logger } from '../../helpers/logger' 3import { logger } from '../../helpers/logger'
4import { areValidationErrors } from './utils' 4import { areValidationErrors } from './utils'
5import { isNpmPluginNameValid, isPluginNameValid, isPluginTypeValid, isPluginVersionValid } from '../../helpers/custom-validators/plugins' 5import { isNpmPluginNameValid, isPluginNameValid, isPluginTypeValid, isPluginVersionValid } from '../../helpers/custom-validators/plugins'
6import { PluginManager } from '../../lib/plugins/plugin-manager' 6import { PluginManager } from '../../lib/plugins/plugin-manager'
7import { isBooleanValid, isSafePath, toBooleanOrNull } from '../../helpers/custom-validators/misc' 7import { isBooleanValid, isSafePath, toBooleanOrNull, exists } from '../../helpers/custom-validators/misc'
8import { PluginModel } from '../../models/server/plugin' 8import { PluginModel } from '../../models/server/plugin'
9import { InstallOrUpdatePlugin } from '../../../shared/models/plugins/install-plugin.model' 9import { InstallOrUpdatePlugin } from '../../../shared/models/plugins/install-plugin.model'
10import { PluginType } from '../../../shared/models/plugins/plugin.type' 10import { PluginType } from '../../../shared/models/plugins/plugin.type'
11import { CONFIG } from '../../initializers/config' 11import { CONFIG } from '../../initializers/config'
12 12
13const servePluginStaticDirectoryValidator = (pluginType: PluginType) => [ 13const getPluginValidator = (pluginType: PluginType, withVersion = true) => {
14 param('pluginName').custom(isPluginNameValid).withMessage('Should have a valid plugin name'), 14 const validators: (ValidationChain | express.Handler)[] = [
15 param('pluginVersion').custom(isPluginVersionValid).withMessage('Should have a valid plugin version'), 15 param('pluginName').custom(isPluginNameValid).withMessage('Should have a valid plugin name')
16 param('staticEndpoint').custom(isSafePath).withMessage('Should have a valid static endpoint'), 16 ]
17
18 if (withVersion) {
19 validators.push(
20 param('pluginVersion').custom(isPluginVersionValid).withMessage('Should have a valid plugin version')
21 )
22 }
23
24 return validators.concat([
25 (req: express.Request, res: express.Response, next: express.NextFunction) => {
26 logger.debug('Checking getPluginValidator parameters', { parameters: req.params })
27
28 if (areValidationErrors(req, res)) return
29
30 const npmName = PluginModel.buildNpmName(req.params.pluginName, pluginType)
31 const plugin = PluginManager.Instance.getRegisteredPluginOrTheme(npmName)
32
33 if (!plugin) return res.sendStatus(404)
34 if (withVersion && plugin.version !== req.params.pluginVersion) return res.sendStatus(404)
35
36 res.locals.registeredPlugin = plugin
37
38 return next()
39 }
40 ])
41}
42
43const getExternalAuthValidator = [
44 param('authName').custom(exists).withMessage('Should have a valid auth name'),
17 45
18 (req: express.Request, res: express.Response, next: express.NextFunction) => { 46 (req: express.Request, res: express.Response, next: express.NextFunction) => {
19 logger.debug('Checking servePluginStaticDirectory parameters', { parameters: req.params }) 47 logger.debug('Checking getExternalAuthValidator parameters', { parameters: req.params })
20 48
21 if (areValidationErrors(req, res)) return 49 if (areValidationErrors(req, res)) return
22 50
23 const npmName = PluginModel.buildNpmName(req.params.pluginName, pluginType) 51 const plugin = res.locals.registeredPlugin
24 const plugin = PluginManager.Instance.getRegisteredPluginOrTheme(npmName) 52 if (!plugin.registerHelpersStore) return res.sendStatus(404)
25 53
26 if (!plugin || plugin.version !== req.params.pluginVersion) { 54 const externalAuth = plugin.registerHelpersStore.getExternalAuths().find(a => a.authName === req.params.authName)
27 return res.sendStatus(404) 55 if (!externalAuth) return res.sendStatus(404)
28 } 56
57 res.locals.externalAuth = externalAuth
58
59 return next()
60 }
61]
29 62
30 res.locals.registeredPlugin = plugin 63const pluginStaticDirectoryValidator = [
64 param('staticEndpoint').custom(isSafePath).withMessage('Should have a valid static endpoint'),
65
66 (req: express.Request, res: express.Response, next: express.NextFunction) => {
67 logger.debug('Checking pluginStaticDirectoryValidator parameters', { parameters: req.params })
68
69 if (areValidationErrors(req, res)) return
31 70
32 return next() 71 return next()
33 } 72 }
@@ -149,11 +188,13 @@ const listAvailablePluginsValidator = [
149// --------------------------------------------------------------------------- 188// ---------------------------------------------------------------------------
150 189
151export { 190export {
152 servePluginStaticDirectoryValidator, 191 pluginStaticDirectoryValidator,
192 getPluginValidator,
153 updatePluginSettingsValidator, 193 updatePluginSettingsValidator,
154 uninstallPluginValidator, 194 uninstallPluginValidator,
155 listAvailablePluginsValidator, 195 listAvailablePluginsValidator,
156 existingPluginValidator, 196 existingPluginValidator,
157 installOrUpdatePluginValidator, 197 installOrUpdatePluginValidator,
158 listPluginsValidator 198 listPluginsValidator,
199 getExternalAuthValidator
159} 200}
diff --git a/server/middlewares/validators/redundancy.ts b/server/middlewares/validators/redundancy.ts
index 8098e3a44..8cd3bc33d 100644
--- a/server/middlewares/validators/redundancy.ts
+++ b/server/middlewares/validators/redundancy.ts
@@ -1,12 +1,13 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body, param } from 'express-validator' 2import { body, param, query } from 'express-validator'
3import { exists, isBooleanValid, isIdOrUUIDValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc' 3import { exists, isBooleanValid, isIdOrUUIDValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
4import { logger } from '../../helpers/logger' 4import { logger } from '../../helpers/logger'
5import { areValidationErrors } from './utils' 5import { areValidationErrors } from './utils'
6import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' 6import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy'
7import { isHostValid } from '../../helpers/custom-validators/servers' 7import { isHostValid } from '../../helpers/custom-validators/servers'
8import { ServerModel } from '../../models/server/server' 8import { ServerModel } from '../../models/server/server'
9import { doesVideoExist } from '../../helpers/middlewares' 9import { doesVideoExist } from '../../helpers/middlewares'
10import { isVideoRedundancyTarget } from '@server/helpers/custom-validators/video-redundancies'
10 11
11const videoFileRedundancyGetValidator = [ 12const videoFileRedundancyGetValidator = [
12 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'),
@@ -101,10 +102,77 @@ const updateServerRedundancyValidator = [
101 } 102 }
102] 103]
103 104
105const listVideoRedundanciesValidator = [
106 query('target')
107 .custom(isVideoRedundancyTarget).withMessage('Should have a valid video redundancies target'),
108
109 (req: express.Request, res: express.Response, next: express.NextFunction) => {
110 logger.debug('Checking listVideoRedundanciesValidator parameters', { parameters: req.query })
111
112 if (areValidationErrors(req, res)) return
113
114 return next()
115 }
116]
117
118const addVideoRedundancyValidator = [
119 body('videoId')
120 .custom(isIdValid)
121 .withMessage('Should have a valid video id'),
122
123 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
124 logger.debug('Checking addVideoRedundancyValidator parameters', { parameters: req.query })
125
126 if (areValidationErrors(req, res)) return
127
128 if (!await doesVideoExist(req.body.videoId, res, 'only-video')) return
129
130 if (res.locals.onlyVideo.remote === false) {
131 return res.status(400)
132 .json({ error: 'Cannot create a redundancy on a local video' })
133 .end()
134 }
135
136 const alreadyExists = await VideoRedundancyModel.isLocalByVideoUUIDExists(res.locals.onlyVideo.uuid)
137 if (alreadyExists) {
138 return res.status(409)
139 .json({ error: 'This video is already duplicated by your instance.' })
140 }
141
142 return next()
143 }
144]
145
146const removeVideoRedundancyValidator = [
147 param('redundancyId')
148 .custom(isIdValid)
149 .withMessage('Should have a valid redundancy id'),
150
151 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
152 logger.debug('Checking removeVideoRedundancyValidator parameters', { parameters: req.query })
153
154 if (areValidationErrors(req, res)) return
155
156 const redundancy = await VideoRedundancyModel.loadByIdWithVideo(parseInt(req.params.redundancyId, 10))
157 if (!redundancy) {
158 return res.status(404)
159 .json({ error: 'Video redundancy not found' })
160 .end()
161 }
162
163 res.locals.videoRedundancy = redundancy
164
165 return next()
166 }
167]
168
104// --------------------------------------------------------------------------- 169// ---------------------------------------------------------------------------
105 170
106export { 171export {
107 videoFileRedundancyGetValidator, 172 videoFileRedundancyGetValidator,
108 videoPlaylistRedundancyGetValidator, 173 videoPlaylistRedundancyGetValidator,
109 updateServerRedundancyValidator 174 updateServerRedundancyValidator,
175 listVideoRedundanciesValidator,
176 addVideoRedundancyValidator,
177 removeVideoRedundancyValidator
110} 178}
diff --git a/server/middlewares/validators/server.ts b/server/middlewares/validators/server.ts
index f6812647b..6158c3363 100644
--- a/server/middlewares/validators/server.ts
+++ b/server/middlewares/validators/server.ts
@@ -5,9 +5,8 @@ import { isHostValid, isValidContactBody } from '../../helpers/custom-validators
5import { ServerModel } from '../../models/server/server' 5import { ServerModel } from '../../models/server/server'
6import { body } from 'express-validator' 6import { body } from 'express-validator'
7import { isUserDisplayNameValid } from '../../helpers/custom-validators/users' 7import { isUserDisplayNameValid } from '../../helpers/custom-validators/users'
8import { Emailer } from '../../lib/emailer'
9import { Redis } from '../../lib/redis' 8import { Redis } from '../../lib/redis'
10import { CONFIG } from '../../initializers/config' 9import { CONFIG, isEmailEnabled } from '../../initializers/config'
11 10
12const serverGetValidator = [ 11const serverGetValidator = [
13 body('host').custom(isHostValid).withMessage('Should have a valid host'), 12 body('host').custom(isHostValid).withMessage('Should have a valid host'),
@@ -50,7 +49,7 @@ const contactAdministratorValidator = [
50 .end() 49 .end()
51 } 50 }
52 51
53 if (Emailer.isEnabled() === false) { 52 if (isEmailEnabled() === false) {
54 return res 53 return res
55 .status(409) 54 .status(409)
56 .send({ error: 'Emailer is not enabled on this instance.' }) 55 .send({ error: 'Emailer is not enabled on this instance.' })
diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts
index c75e701d6..b76dab722 100644
--- a/server/middlewares/validators/sort.ts
+++ b/server/middlewares/validators/sort.ts
@@ -23,6 +23,7 @@ const SORTABLE_USER_NOTIFICATIONS_COLUMNS = createSortableColumns(SORTABLE_COLUM
23const SORTABLE_VIDEO_PLAYLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_PLAYLISTS) 23const SORTABLE_VIDEO_PLAYLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_PLAYLISTS)
24const SORTABLE_PLUGINS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.PLUGINS) 24const SORTABLE_PLUGINS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.PLUGINS)
25const SORTABLE_AVAILABLE_PLUGINS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.AVAILABLE_PLUGINS) 25const SORTABLE_AVAILABLE_PLUGINS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.AVAILABLE_PLUGINS)
26const SORTABLE_VIDEO_REDUNDANCIES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_REDUNDANCIES)
26 27
27const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) 28const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS)
28const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS) 29const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS)
@@ -45,6 +46,7 @@ const userNotificationsSortValidator = checkSort(SORTABLE_USER_NOTIFICATIONS_COL
45const videoPlaylistsSortValidator = checkSort(SORTABLE_VIDEO_PLAYLISTS_COLUMNS) 46const videoPlaylistsSortValidator = checkSort(SORTABLE_VIDEO_PLAYLISTS_COLUMNS)
46const pluginsSortValidator = checkSort(SORTABLE_PLUGINS_COLUMNS) 47const pluginsSortValidator = checkSort(SORTABLE_PLUGINS_COLUMNS)
47const availablePluginsSortValidator = checkSort(SORTABLE_AVAILABLE_PLUGINS_COLUMNS) 48const availablePluginsSortValidator = checkSort(SORTABLE_AVAILABLE_PLUGINS_COLUMNS)
49const videoRedundanciesSortValidator = checkSort(SORTABLE_VIDEO_REDUNDANCIES_COLUMNS)
48 50
49// --------------------------------------------------------------------------- 51// ---------------------------------------------------------------------------
50 52
@@ -69,5 +71,6 @@ export {
69 serversBlocklistSortValidator, 71 serversBlocklistSortValidator,
70 userNotificationsSortValidator, 72 userNotificationsSortValidator,
71 videoPlaylistsSortValidator, 73 videoPlaylistsSortValidator,
74 videoRedundanciesSortValidator,
72 pluginsSortValidator 75 pluginsSortValidator
73} 76}
diff --git a/server/middlewares/validators/themes.ts b/server/middlewares/validators/themes.ts
index 24a9673f7..82794656d 100644
--- a/server/middlewares/validators/themes.ts
+++ b/server/middlewares/validators/themes.ts
@@ -16,7 +16,7 @@ const serveThemeCSSValidator = [
16 16
17 if (areValidationErrors(req, res)) return 17 if (areValidationErrors(req, res)) return
18 18
19 const theme = PluginManager.Instance.getRegisteredTheme(req.params.themeName) 19 const theme = PluginManager.Instance.getRegisteredThemeByShortName(req.params.themeName)
20 20
21 if (!theme || theme.version !== req.params.themeVersion) { 21 if (!theme || theme.version !== req.params.themeVersion) {
22 return res.sendStatus(404) 22 return res.sendStatus(404)
diff --git a/server/middlewares/validators/user-subscriptions.ts b/server/middlewares/validators/user-subscriptions.ts
index 9bc8c87e7..5d4cc94c5 100644
--- a/server/middlewares/validators/user-subscriptions.ts
+++ b/server/middlewares/validators/user-subscriptions.ts
@@ -53,7 +53,6 @@ const userSubscriptionGetValidator = [
53 .json({ 53 .json({
54 error: `Subscription ${req.params.uri} not found.` 54 error: `Subscription ${req.params.uri} not found.`
55 }) 55 })
56 .end()
57 } 56 }
58 57
59 res.locals.subscription = subscription 58 res.locals.subscription = subscription
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts
index c78c67a8c..840b9fc74 100644
--- a/server/middlewares/validators/users.ts
+++ b/server/middlewares/validators/users.ts
@@ -1,6 +1,6 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import * as express from 'express' 2import * as express from 'express'
3import { body, param } from 'express-validator' 3import { body, param, query } from 'express-validator'
4import { omit } from 'lodash' 4import { omit } from 'lodash'
5import { isIdOrUUIDValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc' 5import { isIdOrUUIDValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
6import { 6import {
@@ -14,6 +14,7 @@ import {
14 isUserDisplayNameValid, 14 isUserDisplayNameValid,
15 isUserNSFWPolicyValid, 15 isUserNSFWPolicyValid,
16 isUserPasswordValid, 16 isUserPasswordValid,
17 isUserPasswordValidOrEmpty,
17 isUserRoleValid, 18 isUserRoleValid,
18 isUserUsernameValid, 19 isUserUsernameValid,
19 isUserVideoLanguages, 20 isUserVideoLanguages,
@@ -36,11 +37,10 @@ import { doesVideoExist } from '../../helpers/middlewares'
36import { UserRole } from '../../../shared/models/users' 37import { UserRole } from '../../../shared/models/users'
37import { MUserDefault } from '@server/typings/models' 38import { MUserDefault } from '@server/typings/models'
38import { Hooks } from '@server/lib/plugins/hooks' 39import { Hooks } from '@server/lib/plugins/hooks'
39import { isLocalVideoAccepted } from '@server/lib/moderation'
40 40
41const usersAddValidator = [ 41const usersAddValidator = [
42 body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'), 42 body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'),
43 body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'), 43 body('password').custom(isUserPasswordValidOrEmpty).withMessage('Should have a valid password'),
44 body('email').isEmail().withMessage('Should have a valid email'), 44 body('email').isEmail().withMessage('Should have a valid email'),
45 body('videoQuota').custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'), 45 body('videoQuota').custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'),
46 body('videoQuotaDaily').custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'), 46 body('videoQuotaDaily').custom(isUserVideoQuotaDailyValid).withMessage('Should have a valid daily user quota'),
@@ -149,7 +149,7 @@ const usersBlockingValidator = [
149] 149]
150 150
151const deleteMeValidator = [ 151const deleteMeValidator = [
152 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 152 (req: express.Request, res: express.Response, next: express.NextFunction) => {
153 const user = res.locals.oauth.token.User 153 const user = res.locals.oauth.token.User
154 if (user.username === 'root') { 154 if (user.username === 'root') {
155 return res.status(400) 155 return res.status(400)
@@ -256,12 +256,13 @@ const usersUpdateMeValidator = [
256 256
257const usersGetValidator = [ 257const usersGetValidator = [
258 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'), 258 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
259 query('withStats').optional().isBoolean().withMessage('Should have a valid stats flag'),
259 260
260 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 261 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
261 logger.debug('Checking usersGet parameters', { parameters: req.params }) 262 logger.debug('Checking usersGet parameters', { parameters: req.params })
262 263
263 if (areValidationErrors(req, res)) return 264 if (areValidationErrors(req, res)) return
264 if (!await checkUserIdExist(req.params.id, res)) return 265 if (!await checkUserIdExist(req.params.id, res, req.query.withStats)) return
265 266
266 return next() 267 return next()
267 } 268 }
@@ -303,7 +304,7 @@ const ensureUserRegistrationAllowed = [
303] 304]
304 305
305const ensureUserRegistrationAllowedForIP = [ 306const ensureUserRegistrationAllowedForIP = [
306 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 307 (req: express.Request, res: express.Response, next: express.NextFunction) => {
307 const allowed = isSignupAllowedForCurrentIP(req.ip) 308 const allowed = isSignupAllowedForCurrentIP(req.ip)
308 309
309 if (allowed === false) { 310 if (allowed === false) {
@@ -410,7 +411,7 @@ const userAutocompleteValidator = [
410] 411]
411 412
412const ensureAuthUserOwnsAccountValidator = [ 413const ensureAuthUserOwnsAccountValidator = [
413 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 414 (req: express.Request, res: express.Response, next: express.NextFunction) => {
414 const user = res.locals.oauth.token.User 415 const user = res.locals.oauth.token.User
415 416
416 if (res.locals.account.id !== user.Account.id) { 417 if (res.locals.account.id !== user.Account.id) {
@@ -460,9 +461,9 @@ export {
460 461
461// --------------------------------------------------------------------------- 462// ---------------------------------------------------------------------------
462 463
463function checkUserIdExist (idArg: number | string, res: express.Response) { 464function checkUserIdExist (idArg: number | string, res: express.Response, withStats = false) {
464 const id = parseInt(idArg + '', 10) 465 const id = parseInt(idArg + '', 10)
465 return checkUserExist(() => UserModel.loadById(id), res) 466 return checkUserExist(() => UserModel.loadById(id, withStats), res)
466} 467}
467 468
468function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) { 469function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) {
diff --git a/server/middlewares/validators/videos/video-abuses.ts b/server/middlewares/validators/videos/video-abuses.ts
index a4aef4024..901997bcb 100644
--- a/server/middlewares/validators/videos/video-abuses.ts
+++ b/server/middlewares/validators/videos/video-abuses.ts
@@ -1,14 +1,15 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body, param } from 'express-validator' 2import { body, param, query } from 'express-validator'
3import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc' 3import { exists, isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc'
4import { logger } from '../../../helpers/logger'
5import { areValidationErrors } from '../utils'
6import { 4import {
5 isAbuseVideoIsValid,
7 isVideoAbuseModerationCommentValid, 6 isVideoAbuseModerationCommentValid,
8 isVideoAbuseReasonValid, 7 isVideoAbuseReasonValid,
9 isVideoAbuseStateValid 8 isVideoAbuseStateValid
10} from '../../../helpers/custom-validators/video-abuses' 9} from '../../../helpers/custom-validators/video-abuses'
10import { logger } from '../../../helpers/logger'
11import { doesVideoAbuseExist, doesVideoExist } from '../../../helpers/middlewares' 11import { doesVideoAbuseExist, doesVideoExist } from '../../../helpers/middlewares'
12import { areValidationErrors } from '../utils'
12 13
13const videoAbuseReportValidator = [ 14const videoAbuseReportValidator = [
14 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), 15 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
@@ -32,8 +33,7 @@ const videoAbuseGetValidator = [
32 logger.debug('Checking videoAbuseGetValidator parameters', { parameters: req.body }) 33 logger.debug('Checking videoAbuseGetValidator parameters', { parameters: req.body })
33 34
34 if (areValidationErrors(req, res)) return 35 if (areValidationErrors(req, res)) return
35 if (!await doesVideoExist(req.params.videoId, res)) return 36 if (!await doesVideoAbuseExist(req.params.id, req.params.videoId, res)) return
36 if (!await doesVideoAbuseExist(req.params.id, res.locals.videoAll.id, res)) return
37 37
38 return next() 38 return next()
39 } 39 }
@@ -53,8 +53,42 @@ const videoAbuseUpdateValidator = [
53 logger.debug('Checking videoAbuseUpdateValidator parameters', { parameters: req.body }) 53 logger.debug('Checking videoAbuseUpdateValidator parameters', { parameters: req.body })
54 54
55 if (areValidationErrors(req, res)) return 55 if (areValidationErrors(req, res)) return
56 if (!await doesVideoExist(req.params.videoId, res)) return 56 if (!await doesVideoAbuseExist(req.params.id, req.params.videoId, res)) return
57 if (!await doesVideoAbuseExist(req.params.id, res.locals.videoAll.id, res)) return 57
58 return next()
59 }
60]
61
62const videoAbuseListValidator = [
63 query('id')
64 .optional()
65 .custom(isIdValid).withMessage('Should have a valid id'),
66 query('search')
67 .optional()
68 .custom(exists).withMessage('Should have a valid search'),
69 query('state')
70 .optional()
71 .custom(isVideoAbuseStateValid).withMessage('Should have a valid video abuse state'),
72 query('videoIs')
73 .optional()
74 .custom(isAbuseVideoIsValid).withMessage('Should have a valid "video is" attribute'),
75 query('searchReporter')
76 .optional()
77 .custom(exists).withMessage('Should have a valid reporter search'),
78 query('searchReportee')
79 .optional()
80 .custom(exists).withMessage('Should have a valid reportee search'),
81 query('searchVideo')
82 .optional()
83 .custom(exists).withMessage('Should have a valid video search'),
84 query('searchVideoChannel')
85 .optional()
86 .custom(exists).withMessage('Should have a valid video channel search'),
87
88 (req: express.Request, res: express.Response, next: express.NextFunction) => {
89 logger.debug('Checking videoAbuseListValidator parameters', { parameters: req.body })
90
91 if (areValidationErrors(req, res)) return
58 92
59 return next() 93 return next()
60 } 94 }
@@ -63,6 +97,7 @@ const videoAbuseUpdateValidator = [
63// --------------------------------------------------------------------------- 97// ---------------------------------------------------------------------------
64 98
65export { 99export {
100 videoAbuseListValidator,
66 videoAbuseReportValidator, 101 videoAbuseReportValidator,
67 videoAbuseGetValidator, 102 videoAbuseGetValidator,
68 videoAbuseUpdateValidator 103 videoAbuseUpdateValidator
diff --git a/server/middlewares/validators/videos/video-blacklist.ts b/server/middlewares/validators/videos/video-blacklist.ts
index 5440e57e7..4bd6a8333 100644
--- a/server/middlewares/validators/videos/video-blacklist.ts
+++ b/server/middlewares/validators/videos/video-blacklist.ts
@@ -69,6 +69,10 @@ const videosBlacklistFiltersValidator = [
69 query('type') 69 query('type')
70 .optional() 70 .optional()
71 .custom(isVideoBlacklistTypeValid).withMessage('Should have a valid video blacklist type attribute'), 71 .custom(isVideoBlacklistTypeValid).withMessage('Should have a valid video blacklist type attribute'),
72 query('search')
73 .optional()
74 .not()
75 .isEmpty().withMessage('Should have a valid search'),
72 76
73 (req: express.Request, res: express.Response, next: express.NextFunction) => { 77 (req: express.Request, res: express.Response, next: express.NextFunction) => {
74 logger.debug('Checking videos blacklist filters query', { parameters: req.query }) 78 logger.debug('Checking videos blacklist filters query', { parameters: req.query })
diff --git a/server/middlewares/validators/videos/video-captions.ts b/server/middlewares/validators/videos/video-captions.ts
index 7b0cd6f66..872d9c2ab 100644
--- a/server/middlewares/validators/videos/video-captions.ts
+++ b/server/middlewares/validators/videos/video-captions.ts
@@ -13,10 +13,12 @@ const addVideoCaptionValidator = [
13 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'),
14 param('captionLanguage').custom(isVideoCaptionLanguageValid).not().isEmpty().withMessage('Should have a valid caption language'), 14 param('captionLanguage').custom(isVideoCaptionLanguageValid).not().isEmpty().withMessage('Should have a valid caption language'),
15 body('captionfile') 15 body('captionfile')
16 .custom((_, { req }) => isVideoCaptionFile(req.files, 'captionfile')).withMessage( 16 .custom((_, { req }) => isVideoCaptionFile(req.files, 'captionfile'))
17 `This caption file is not supported or too large. Please, make sure it is under ${CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.FILE_SIZE} and one of the following mimetypes: ` 17 .withMessage(
18 + Object.keys(MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT).map(key => `${key} (${MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT[key]})`).join(', ') 18 'This caption file is not supported or too large. ' +
19 ), 19 `Please, make sure it is under ${CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.FILE_SIZE} and one of the following mimetypes: ` +
20 Object.keys(MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT).map(key => `${key} (${MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT[key]})`).join(', ')
21 ),
20 22
21 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 23 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
22 logger.debug('Checking addVideoCaption parameters', { parameters: req.body }) 24 logger.debug('Checking addVideoCaption parameters', { parameters: req.body })
diff --git a/server/middlewares/validators/videos/video-channels.ts b/server/middlewares/validators/videos/video-channels.ts
index ebce14714..882fb2b84 100644
--- a/server/middlewares/validators/videos/video-channels.ts
+++ b/server/middlewares/validators/videos/video-channels.ts
@@ -1,5 +1,5 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body, param } from 'express-validator' 2import { body, param, query } from 'express-validator'
3import { UserRight } from '../../../../shared' 3import { UserRight } from '../../../../shared'
4import { 4import {
5 isVideoChannelDescriptionValid, 5 isVideoChannelDescriptionValid,
@@ -128,6 +128,15 @@ const localVideoChannelValidator = [
128 } 128 }
129] 129]
130 130
131const videoChannelStatsValidator = [
132 query('withStats').optional().isBoolean().withMessage('Should have a valid stats flag'),
133
134 (req: express.Request, res: express.Response, next: express.NextFunction) => {
135 if (areValidationErrors(req, res)) return
136 return next()
137 }
138]
139
131// --------------------------------------------------------------------------- 140// ---------------------------------------------------------------------------
132 141
133export { 142export {
@@ -135,7 +144,8 @@ export {
135 videoChannelsUpdateValidator, 144 videoChannelsUpdateValidator,
136 videoChannelsRemoveValidator, 145 videoChannelsRemoveValidator,
137 videoChannelsNameWithHostValidator, 146 videoChannelsNameWithHostValidator,
138 localVideoChannelValidator 147 localVideoChannelValidator,
148 videoChannelStatsValidator
139} 149}
140 150
141// --------------------------------------------------------------------------- 151// ---------------------------------------------------------------------------
diff --git a/server/middlewares/validators/videos/video-comments.ts b/server/middlewares/validators/videos/video-comments.ts
index 77c5f940d..4846a5e9e 100644
--- a/server/middlewares/validators/videos/video-comments.ts
+++ b/server/middlewares/validators/videos/video-comments.ts
@@ -1,16 +1,16 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body, param } from 'express-validator' 2import { body, param } from 'express-validator'
3import { MUserAccountUrl } from '@server/typings/models'
3import { UserRight } from '../../../../shared' 4import { UserRight } from '../../../../shared'
4import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc' 5import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc'
5import { isValidVideoCommentText } from '../../../helpers/custom-validators/video-comments' 6import { isValidVideoCommentText } from '../../../helpers/custom-validators/video-comments'
6import { logger } from '../../../helpers/logger' 7import { logger } from '../../../helpers/logger'
8import { doesVideoExist } from '../../../helpers/middlewares'
9import { AcceptResult, isLocalVideoCommentReplyAccepted, isLocalVideoThreadAccepted } from '../../../lib/moderation'
10import { Hooks } from '../../../lib/plugins/hooks'
7import { VideoCommentModel } from '../../../models/video/video-comment' 11import { VideoCommentModel } from '../../../models/video/video-comment'
12import { MCommentOwnerVideoReply, MVideo, MVideoFullLight, MVideoId } from '../../../typings/models/video'
8import { areValidationErrors } from '../utils' 13import { areValidationErrors } from '../utils'
9import { Hooks } from '../../../lib/plugins/hooks'
10import { AcceptResult, isLocalVideoCommentReplyAccepted, isLocalVideoThreadAccepted } from '../../../lib/moderation'
11import { doesVideoExist } from '../../../helpers/middlewares'
12import { MCommentOwner, MVideo, MVideoFullLight, MVideoId } from '../../../typings/models/video'
13import { MUser } from '@server/typings/models'
14 14
15const listVideoCommentThreadsValidator = [ 15const listVideoCommentThreadsValidator = [
16 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), 16 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
@@ -50,7 +50,7 @@ const addVideoCommentThreadValidator = [
50 if (areValidationErrors(req, res)) return 50 if (areValidationErrors(req, res)) return
51 if (!await doesVideoExist(req.params.videoId, res)) return 51 if (!await doesVideoExist(req.params.videoId, res)) return
52 if (!isVideoCommentsEnabled(res.locals.videoAll, res)) return 52 if (!isVideoCommentsEnabled(res.locals.videoAll, res)) return
53 if (!await isVideoCommentAccepted(req, res, res.locals.videoAll,false)) return 53 if (!await isVideoCommentAccepted(req, res, res.locals.videoAll, false)) return
54 54
55 return next() 55 return next()
56 } 56 }
@@ -188,7 +188,7 @@ function isVideoCommentsEnabled (video: MVideo, res: express.Response) {
188 return true 188 return true
189} 189}
190 190
191function checkUserCanDeleteVideoComment (user: MUser, videoComment: MCommentOwner, res: express.Response) { 191function checkUserCanDeleteVideoComment (user: MUserAccountUrl, videoComment: MCommentOwnerVideoReply, res: express.Response) {
192 if (videoComment.isDeleted()) { 192 if (videoComment.isDeleted()) {
193 res.status(409) 193 res.status(409)
194 .json({ error: 'This comment is already deleted' }) 194 .json({ error: 'This comment is already deleted' })
@@ -196,11 +196,16 @@ function checkUserCanDeleteVideoComment (user: MUser, videoComment: MCommentOwne
196 return false 196 return false
197 } 197 }
198 198
199 const account = videoComment.Account 199 const userAccount = user.Account
200 if (user.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT) === false && account.userId !== user.id) { 200
201 if (
202 user.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT) === false && // Not a moderator
203 videoComment.accountId !== userAccount.id && // Not the comment owner
204 videoComment.Video.VideoChannel.accountId !== userAccount.id // Not the video owner
205 ) {
201 res.status(403) 206 res.status(403)
202 .json({ error: 'Cannot remove video comment of another user' }) 207 .json({ error: 'Cannot remove video comment of another user' })
203 .end() 208
204 return false 209 return false
205 } 210 }
206 211
diff --git a/server/middlewares/validators/videos/video-imports.ts b/server/middlewares/validators/videos/video-imports.ts
index 318dad100..5dc5db533 100644
--- a/server/middlewares/validators/videos/video-imports.ts
+++ b/server/middlewares/validators/videos/video-imports.ts
@@ -22,10 +22,11 @@ const videoImportAddValidator = getCommonVideoEditAttributes().concat([
22 .optional() 22 .optional()
23 .custom(isVideoMagnetUriValid).withMessage('Should have a valid video magnet URI'), 23 .custom(isVideoMagnetUriValid).withMessage('Should have a valid video magnet URI'),
24 body('torrentfile') 24 body('torrentfile')
25 .custom((value, { req }) => isVideoImportTorrentFile(req.files)).withMessage( 25 .custom((value, { req }) => isVideoImportTorrentFile(req.files))
26 'This torrent file is not supported or too large. Please, make sure it is of the following type: ' 26 .withMessage(
27 + CONSTRAINTS_FIELDS.VIDEO_IMPORTS.TORRENT_FILE.EXTNAME.join(', ') 27 'This torrent file is not supported or too large. Please, make sure it is of the following type: ' +
28 ), 28 CONSTRAINTS_FIELDS.VIDEO_IMPORTS.TORRENT_FILE.EXTNAME.join(', ')
29 ),
29 body('name') 30 body('name')
30 .optional() 31 .optional()
31 .custom(isVideoNameValid).withMessage('Should have a valid name'), 32 .custom(isVideoNameValid).withMessage('Should have a valid name'),
diff --git a/server/middlewares/validators/videos/video-playlists.ts b/server/middlewares/validators/videos/video-playlists.ts
index 1d67e8666..6b15c5464 100644
--- a/server/middlewares/validators/videos/video-playlists.ts
+++ b/server/middlewares/validators/videos/video-playlists.ts
@@ -384,10 +384,11 @@ export {
384function getCommonPlaylistEditAttributes () { 384function getCommonPlaylistEditAttributes () {
385 return [ 385 return [
386 body('thumbnailfile') 386 body('thumbnailfile')
387 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage( 387 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile'))
388 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' 388 .withMessage(
389 + CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS.IMAGE.EXTNAME.join(', ') 389 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' +
390 ), 390 CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS.IMAGE.EXTNAME.join(', ')
391 ),
391 392
392 body('description') 393 body('description')
393 .optional() 394 .optional()
diff --git a/server/middlewares/validators/videos/video-rates.ts b/server/middlewares/validators/videos/video-rates.ts
index 4021cfecc..cbc144f69 100644
--- a/server/middlewares/validators/videos/video-rates.ts
+++ b/server/middlewares/validators/videos/video-rates.ts
@@ -24,7 +24,7 @@ const videoUpdateRateValidator = [
24 } 24 }
25] 25]
26 26
27const getAccountVideoRateValidator = function (rateType: VideoRateType) { 27const getAccountVideoRateValidatorFactory = function (rateType: VideoRateType) {
28 return [ 28 return [
29 param('name').custom(isAccountNameValid).withMessage('Should have a valid account name'), 29 param('name').custom(isAccountNameValid).withMessage('Should have a valid account name'),
30 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), 30 param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
@@ -51,7 +51,7 @@ const getAccountVideoRateValidator = function (rateType: VideoRateType) {
51const videoRatingValidator = [ 51const videoRatingValidator = [
52 query('rating').optional().custom(isRatingValid).withMessage('Value must be one of "like" or "dislike"'), 52 query('rating').optional().custom(isRatingValid).withMessage('Value must be one of "like" or "dislike"'),
53 53
54 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 54 (req: express.Request, res: express.Response, next: express.NextFunction) => {
55 logger.debug('Checking rating parameter', { parameters: req.params }) 55 logger.debug('Checking rating parameter', { parameters: req.params })
56 56
57 if (areValidationErrors(req, res)) return 57 if (areValidationErrors(req, res)) return
@@ -64,6 +64,6 @@ const videoRatingValidator = [
64 64
65export { 65export {
66 videoUpdateRateValidator, 66 videoUpdateRateValidator,
67 getAccountVideoRateValidator, 67 getAccountVideoRateValidatorFactory,
68 videoRatingValidator 68 videoRatingValidator
69} 69}
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts
index 6733d9dec..867c05fc1 100644
--- a/server/middlewares/validators/videos/videos.ts
+++ b/server/middlewares/validators/videos/videos.ts
@@ -29,7 +29,7 @@ import {
29} from '../../../helpers/custom-validators/videos' 29} from '../../../helpers/custom-validators/videos'
30import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils' 30import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils'
31import { logger } from '../../../helpers/logger' 31import { logger } from '../../../helpers/logger'
32import { CONSTRAINTS_FIELDS } from '../../../initializers/constants' 32import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants'
33import { authenticatePromiseIfNeeded } from '../../oauth' 33import { authenticatePromiseIfNeeded } from '../../oauth'
34import { areValidationErrors } from '../utils' 34import { areValidationErrors } from '../utils'
35import { cleanUpReqFiles } from '../../../helpers/express-utils' 35import { cleanUpReqFiles } from '../../../helpers/express-utils'
@@ -38,19 +38,24 @@ import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } f
38import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model' 38import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model'
39import { AccountModel } from '../../../models/account/account' 39import { AccountModel } from '../../../models/account/account'
40import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search' 40import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
41import { getServerActor } from '../../../helpers/utils'
42import { CONFIG } from '../../../initializers/config' 41import { CONFIG } from '../../../initializers/config'
43import { isLocalVideoAccepted } from '../../../lib/moderation' 42import { isLocalVideoAccepted } from '../../../lib/moderation'
44import { Hooks } from '../../../lib/plugins/hooks' 43import { Hooks } from '../../../lib/plugins/hooks'
45import { checkUserCanManageVideo, doesVideoChannelOfAccountExist, doesVideoExist } from '../../../helpers/middlewares' 44import {
45 checkUserCanManageVideo,
46 doesVideoChannelOfAccountExist,
47 doesVideoExist,
48 doesVideoFileOfVideoExist
49} from '../../../helpers/middlewares'
46import { MVideoFullLight } from '@server/typings/models' 50import { MVideoFullLight } from '@server/typings/models'
47import { getVideoWithAttributes } from '../../../helpers/video' 51import { getVideoWithAttributes } from '../../../helpers/video'
52import { getServerActor } from '@server/models/application/application'
48 53
49const videosAddValidator = getCommonVideoEditAttributes().concat([ 54const videosAddValidator = getCommonVideoEditAttributes().concat([
50 body('videofile') 55 body('videofile')
51 .custom((value, { req }) => isVideoFile(req.files)).withMessage( 56 .custom((value, { req }) => isVideoFile(req.files)).withMessage(
52 'This file is not supported or too large. Please, make sure it is of the following type: ' 57 'This file is not supported or too large. Please, make sure it is of the following type: ' +
53 + CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ') 58 CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
54 ), 59 ),
55 body('name').custom(isVideoNameValid).withMessage('Should have a valid name'), 60 body('name').custom(isVideoNameValid).withMessage('Should have a valid name'),
56 body('channelId') 61 body('channelId')
@@ -147,7 +152,10 @@ async function checkVideoFollowConstraints (req: express.Request, res: express.R
147 }) 152 })
148} 153}
149 154
150const videosCustomGetValidator = (fetchType: 'all' | 'only-video' | 'only-video-with-rights', authenticateInQuery = false) => { 155const videosCustomGetValidator = (
156 fetchType: 'all' | 'only-video' | 'only-video-with-rights' | 'only-immutable-attributes',
157 authenticateInQuery = false
158) => {
151 return [ 159 return [
152 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), 160 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
153 161
@@ -157,6 +165,9 @@ const videosCustomGetValidator = (fetchType: 'all' | 'only-video' | 'only-video-
157 if (areValidationErrors(req, res)) return 165 if (areValidationErrors(req, res)) return
158 if (!await doesVideoExist(req.params.id, res, fetchType)) return 166 if (!await doesVideoExist(req.params.id, res, fetchType)) return
159 167
168 // Controllers does not need to check video rights
169 if (fetchType === 'only-immutable-attributes') return next()
170
160 const video = getVideoWithAttributes(res) 171 const video = getVideoWithAttributes(res)
161 const videoAll = video as MVideoFullLight 172 const videoAll = video as MVideoFullLight
162 173
@@ -192,6 +203,20 @@ const videosCustomGetValidator = (fetchType: 'all' | 'only-video' | 'only-video-
192const videosGetValidator = videosCustomGetValidator('all') 203const videosGetValidator = videosCustomGetValidator('all')
193const videosDownloadValidator = videosCustomGetValidator('all', true) 204const videosDownloadValidator = videosCustomGetValidator('all', true)
194 205
206const videoFileMetadataGetValidator = getCommonVideoEditAttributes().concat([
207 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
208 param('videoFileId').custom(isIdValid).not().isEmpty().withMessage('Should have a valid videoFileId'),
209
210 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
211 logger.debug('Checking videoFileMetadataGet parameters', { parameters: req.params })
212
213 if (areValidationErrors(req, res)) return
214 if (!await doesVideoFileOfVideoExist(+req.params.videoFileId, req.params.id, res)) return
215
216 return next()
217 }
218])
219
195const videosRemoveValidator = [ 220const videosRemoveValidator = [
196 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), 221 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
197 222
@@ -245,19 +270,15 @@ const videosTerminateChangeOwnershipValidator = [
245 // Check if the user who did the request is able to change the ownership of the video 270 // Check if the user who did the request is able to change the ownership of the video
246 if (!checkUserCanTerminateOwnershipChange(res.locals.oauth.token.User, res.locals.videoChangeOwnership, res)) return 271 if (!checkUserCanTerminateOwnershipChange(res.locals.oauth.token.User, res.locals.videoChangeOwnership, res)) return
247 272
248 return next()
249 },
250 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
251 const videoChangeOwnership = res.locals.videoChangeOwnership 273 const videoChangeOwnership = res.locals.videoChangeOwnership
252 274
253 if (videoChangeOwnership.status === VideoChangeOwnershipStatus.WAITING) { 275 if (videoChangeOwnership.status !== VideoChangeOwnershipStatus.WAITING) {
254 return next()
255 } else {
256 res.status(403) 276 res.status(403)
257 .json({ error: 'Ownership already accepted or refused' }) 277 .json({ error: 'Ownership already accepted or refused' })
258
259 return 278 return
260 } 279 }
280
281 return next()
261 } 282 }
262] 283]
263 284
@@ -280,18 +301,31 @@ const videosAcceptChangeOwnershipValidator = [
280 } 301 }
281] 302]
282 303
304const videosOverviewValidator = [
305 query('page')
306 .optional()
307 .isInt({ min: 1, max: OVERVIEWS.VIDEOS.SAMPLES_COUNT })
308 .withMessage('Should have a valid pagination'),
309
310 (req: express.Request, res: express.Response, next: express.NextFunction) => {
311 if (areValidationErrors(req, res)) return
312
313 return next()
314 }
315]
316
283function getCommonVideoEditAttributes () { 317function getCommonVideoEditAttributes () {
284 return [ 318 return [
285 body('thumbnailfile') 319 body('thumbnailfile')
286 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage( 320 .custom((value, { req }) => isVideoImage(req.files, 'thumbnailfile')).withMessage(
287 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' 321 'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' +
288 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ') 322 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
289 ), 323 ),
290 body('previewfile') 324 body('previewfile')
291 .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage( 325 .custom((value, { req }) => isVideoImage(req.files, 'previewfile')).withMessage(
292 'This preview file is not supported or too large. Please, make sure it is of the following type: ' 326 'This preview file is not supported or too large. Please, make sure it is of the following type: ' +
293 + CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ') 327 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
294 ), 328 ),
295 329
296 body('category') 330 body('category')
297 .optional() 331 .optional()
@@ -409,6 +443,7 @@ export {
409 videosAddValidator, 443 videosAddValidator,
410 videosUpdateValidator, 444 videosUpdateValidator,
411 videosGetValidator, 445 videosGetValidator,
446 videoFileMetadataGetValidator,
412 videosDownloadValidator, 447 videosDownloadValidator,
413 checkVideoFollowConstraints, 448 checkVideoFollowConstraints,
414 videosCustomGetValidator, 449 videosCustomGetValidator,
@@ -420,7 +455,9 @@ export {
420 455
421 getCommonVideoEditAttributes, 456 getCommonVideoEditAttributes,
422 457
423 commonVideosFiltersValidator 458 commonVideosFiltersValidator,
459
460 videosOverviewValidator
424} 461}
425 462
426// --------------------------------------------------------------------------- 463// ---------------------------------------------------------------------------
diff --git a/server/middlewares/validators/webfinger.ts b/server/middlewares/validators/webfinger.ts
index d50e6527f..5fe864f8b 100644
--- a/server/middlewares/validators/webfinger.ts
+++ b/server/middlewares/validators/webfinger.ts
@@ -18,15 +18,14 @@ const webfingerValidator = [
18 const nameWithHost = getHostWithPort(req.query.resource.substr(5)) 18 const nameWithHost = getHostWithPort(req.query.resource.substr(5))
19 const [ name ] = nameWithHost.split('@') 19 const [ name ] = nameWithHost.split('@')
20 20
21 // FIXME: we don't need the full actor 21 const actor = await ActorModel.loadLocalUrlByName(name)
22 const actor = await ActorModel.loadLocalByName(name)
23 if (!actor) { 22 if (!actor) {
24 return res.status(404) 23 return res.status(404)
25 .send({ error: 'Actor not found' }) 24 .send({ error: 'Actor not found' })
26 .end() 25 .end()
27 } 26 }
28 27
29 res.locals.actorFull = actor 28 res.locals.actorUrl = actor
30 return next() 29 return next()
31 } 30 }
32] 31]
diff --git a/server/models/account/account-blocklist.ts b/server/models/account/account-blocklist.ts
index 6ebe32556..d8a7ce4b4 100644
--- a/server/models/account/account-blocklist.ts
+++ b/server/models/account/account-blocklist.ts
@@ -1,6 +1,6 @@
1import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' 1import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
2import { AccountModel } from './account' 2import { AccountModel } from './account'
3import { getSort } from '../utils' 3import { getSort, searchAttribute } from '../utils'
4import { AccountBlock } from '../../../shared/models/blocklist' 4import { AccountBlock } from '../../../shared/models/blocklist'
5import { Op } from 'sequelize' 5import { Op } from 'sequelize'
6import * as Bluebird from 'bluebird' 6import * as Bluebird from 'bluebird'
@@ -80,7 +80,7 @@ export class AccountBlocklistModel extends Model<AccountBlocklistModel> {
80 attributes: [ 'accountId', 'id' ], 80 attributes: [ 'accountId', 'id' ],
81 where: { 81 where: {
82 accountId: { 82 accountId: {
83 [Op.in]: accountIds // FIXME: sequelize ANY seems broken 83 [Op.in]: accountIds
84 }, 84 },
85 targetAccountId 85 targetAccountId
86 }, 86 },
@@ -111,16 +111,36 @@ export class AccountBlocklistModel extends Model<AccountBlocklistModel> {
111 return AccountBlocklistModel.findOne(query) 111 return AccountBlocklistModel.findOne(query)
112 } 112 }
113 113
114 static listForApi (accountId: number, start: number, count: number, sort: string) { 114 static listForApi (parameters: {
115 start: number
116 count: number
117 sort: string
118 search?: string
119 accountId: number
120 }) {
121 const { start, count, sort, search, accountId } = parameters
122
115 const query = { 123 const query = {
116 offset: start, 124 offset: start,
117 limit: count, 125 limit: count,
118 order: getSort(sort), 126 order: getSort(sort)
119 where: { 127 }
120 accountId 128
121 } 129 const where = {
130 accountId
122 } 131 }
123 132
133 if (search) {
134 Object.assign(where, {
135 [Op.or]: [
136 searchAttribute(search, '$BlockedAccount.name$'),
137 searchAttribute(search, '$BlockedAccount.Actor.url$')
138 ]
139 })
140 }
141
142 Object.assign(query, { where })
143
124 return AccountBlocklistModel 144 return AccountBlocklistModel
125 .scope([ ScopeNames.WITH_ACCOUNTS ]) 145 .scope([ ScopeNames.WITH_ACCOUNTS ])
126 .findAndCountAll<MAccountBlocklistAccounts>(query) 146 .findAndCountAll<MAccountBlocklistAccounts>(query)
diff --git a/server/models/account/account-video-rate.ts b/server/models/account/account-video-rate.ts
index c593595b2..8aeb486d1 100644
--- a/server/models/account/account-video-rate.ts
+++ b/server/models/account/account-video-rate.ts
@@ -99,7 +99,7 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
99 static loadByAccountAndVideoOrUrl (accountId: number, videoId: number, url: string, t?: Transaction): Bluebird<MAccountVideoRate> { 99 static loadByAccountAndVideoOrUrl (accountId: number, videoId: number, url: string, t?: Transaction): Bluebird<MAccountVideoRate> {
100 const options: FindOptions = { 100 const options: FindOptions = {
101 where: { 101 where: {
102 [ Op.or]: [ 102 [Op.or]: [
103 { 103 {
104 accountId, 104 accountId,
105 videoId 105 videoId
@@ -116,10 +116,10 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
116 } 116 }
117 117
118 static listByAccountForApi (options: { 118 static listByAccountForApi (options: {
119 start: number, 119 start: number
120 count: number, 120 count: number
121 sort: string, 121 sort: string
122 type?: string, 122 type?: string
123 accountId: number 123 accountId: number
124 }) { 124 }) {
125 const query: FindOptions = { 125 const query: FindOptions = {
@@ -135,7 +135,7 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
135 required: true, 135 required: true,
136 include: [ 136 include: [
137 { 137 {
138 model: VideoChannelModel.scope({ method: [VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }), 138 model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }),
139 required: true 139 required: true
140 } 140 }
141 ] 141 ]
diff --git a/server/models/account/account.ts b/server/models/account/account.ts
index 8a0ffeb63..a0081f259 100644
--- a/server/models/account/account.ts
+++ b/server/models/account/account.ts
@@ -32,8 +32,9 @@ import { FindOptions, IncludeOptions, Op, Transaction, WhereOptions } from 'sequ
32import { AccountBlocklistModel } from './account-blocklist' 32import { AccountBlocklistModel } from './account-blocklist'
33import { ServerBlocklistModel } from '../server/server-blocklist' 33import { ServerBlocklistModel } from '../server/server-blocklist'
34import { ActorFollowModel } from '../activitypub/actor-follow' 34import { ActorFollowModel } from '../activitypub/actor-follow'
35import { MAccountActor, MAccountDefault, MAccountSummaryFormattable, MAccountFormattable, MAccountAP } from '../../typings/models' 35import { MAccountActor, MAccountAP, MAccountDefault, MAccountFormattable, MAccountSummaryFormattable } from '../../typings/models'
36import * as Bluebird from 'bluebird' 36import * as Bluebird from 'bluebird'
37import { ModelCache } from '@server/models/model-cache'
37 38
38export enum ScopeNames { 39export enum ScopeNames {
39 SUMMARY = 'SUMMARY' 40 SUMMARY = 'SUMMARY'
@@ -53,7 +54,7 @@ export type SummaryOptions = {
53 ] 54 ]
54})) 55}))
55@Scopes(() => ({ 56@Scopes(() => ({
56 [ ScopeNames.SUMMARY ]: (options: SummaryOptions = {}) => { 57 [ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => {
57 const whereActor = options.whereActor || undefined 58 const whereActor = options.whereActor || undefined
58 59
59 const serverInclude: IncludeOptions = { 60 const serverInclude: IncludeOptions = {
@@ -218,8 +219,6 @@ export class AccountModel extends Model<AccountModel> {
218 }) 219 })
219 BlockedAccounts: AccountBlocklistModel[] 220 BlockedAccounts: AccountBlocklistModel[]
220 221
221 private static cache: { [ id: string ]: any } = {}
222
223 @BeforeDestroy 222 @BeforeDestroy
224 static async sendDeleteIfOwned (instance: AccountModel, options) { 223 static async sendDeleteIfOwned (instance: AccountModel, options) {
225 if (!instance.Actor) { 224 if (!instance.Actor) {
@@ -247,45 +246,43 @@ export class AccountModel extends Model<AccountModel> {
247 } 246 }
248 247
249 static loadLocalByName (name: string): Bluebird<MAccountDefault> { 248 static loadLocalByName (name: string): Bluebird<MAccountDefault> {
250 // The server actor never change, so we can easily cache it 249 const fun = () => {
251 if (name === SERVER_ACTOR_NAME && AccountModel.cache[name]) { 250 const query = {
252 return Bluebird.resolve(AccountModel.cache[name]) 251 where: {
253 } 252 [Op.or]: [
254 253 {
255 const query = { 254 userId: {
256 where: { 255 [Op.ne]: null
257 [ Op.or ]: [ 256 }
258 { 257 },
259 userId: { 258 {
260 [ Op.ne ]: null 259 applicationId: {
260 [Op.ne]: null
261 }
261 } 262 }
262 }, 263 ]
264 },
265 include: [
263 { 266 {
264 applicationId: { 267 model: ActorModel,
265 [ Op.ne ]: null 268 required: true,
269 where: {
270 preferredUsername: name
266 } 271 }
267 } 272 }
268 ] 273 ]
269 }, 274 }
270 include: [
271 {
272 model: ActorModel,
273 required: true,
274 where: {
275 preferredUsername: name
276 }
277 }
278 ]
279 }
280 275
281 return AccountModel.findOne(query) 276 return AccountModel.findOne(query)
282 .then(account => { 277 }
283 if (name === SERVER_ACTOR_NAME) {
284 AccountModel.cache[name] = account
285 }
286 278
287 return account 279 return ModelCache.Instance.doCache({
288 }) 280 cacheType: 'local-account-name',
281 key: name,
282 fun,
283 // The server actor never change, so we can easily cache it
284 whitelist: () => name === SERVER_ACTOR_NAME
285 })
289 } 286 }
290 287
291 static loadByNameAndHost (name: string, host: string): Bluebird<MAccountDefault> { 288 static loadByNameAndHost (name: string, host: string): Bluebird<MAccountDefault> {
diff --git a/server/models/account/user-notification.ts b/server/models/account/user-notification.ts
index a05f30175..5a725187a 100644
--- a/server/models/account/user-notification.ts
+++ b/server/models/account/user-notification.ts
@@ -363,7 +363,7 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
363 where: { 363 where: {
364 userId, 364 userId,
365 id: { 365 id: {
366 [Op.in]: notificationIds // FIXME: sequelize ANY seems broken 366 [Op.in]: notificationIds
367 } 367 }
368 } 368 }
369 } 369 }
@@ -379,7 +379,7 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
379 379
380 toFormattedJSON (this: UserNotificationModelForApi): UserNotification { 380 toFormattedJSON (this: UserNotificationModelForApi): UserNotification {
381 const video = this.Video 381 const video = this.Video
382 ? Object.assign(this.formatVideo(this.Video),{ channel: this.formatActor(this.Video.VideoChannel) }) 382 ? Object.assign(this.formatVideo(this.Video), { channel: this.formatActor(this.Video.VideoChannel) })
383 : undefined 383 : undefined
384 384
385 const videoImport = this.VideoImport ? { 385 const videoImport = this.VideoImport ? {
diff --git a/server/models/account/user-video-history.ts b/server/models/account/user-video-history.ts
index 3fe4c8db1..522eebeaf 100644
--- a/server/models/account/user-video-history.ts
+++ b/server/models/account/user-video-history.ts
@@ -59,7 +59,7 @@ export class UserVideoHistoryModel extends Model<UserVideoHistoryModel> {
59 return VideoModel.listForApi({ 59 return VideoModel.listForApi({
60 start, 60 start,
61 count, 61 count,
62 sort: '-UserVideoHistories.updatedAt', 62 sort: '-"userVideoHistory"."updatedAt"',
63 nsfw: null, // All 63 nsfw: null, // All
64 includeLocalVideos: true, 64 includeLocalVideos: true,
65 withFiles: false, 65 withFiles: false,
diff --git a/server/models/account/user.ts b/server/models/account/user.ts
index 4c2c5e278..fbd3080c6 100644
--- a/server/models/account/user.ts
+++ b/server/models/account/user.ts
@@ -1,4 +1,4 @@
1import { FindOptions, literal, Op, QueryTypes, where, fn, col } from 'sequelize' 1import { col, FindOptions, fn, literal, Op, QueryTypes, where, WhereOptions } from 'sequelize'
2import { 2import {
3 AfterDestroy, 3 AfterDestroy,
4 AfterUpdate, 4 AfterUpdate,
@@ -19,7 +19,7 @@ import {
19 Table, 19 Table,
20 UpdatedAt 20 UpdatedAt
21} from 'sequelize-typescript' 21} from 'sequelize-typescript'
22import { hasUserRight, MyUser, USER_ROLE_LABELS, UserRight, VideoPlaylistType, VideoPrivacy } from '../../../shared' 22import { hasUserRight, MyUser, USER_ROLE_LABELS, UserRight, VideoAbuseState, VideoPlaylistType, VideoPrivacy } from '../../../shared'
23import { User, UserRole } from '../../../shared/models/users' 23import { User, UserRole } from '../../../shared/models/users'
24import { 24import {
25 isNoInstanceConfigWarningModal, 25 isNoInstanceConfigWarningModal,
@@ -49,7 +49,7 @@ import { VideoPlaylistModel } from '../video/video-playlist'
49import { AccountModel } from './account' 49import { AccountModel } from './account'
50import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type' 50import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type'
51import { values } from 'lodash' 51import { values } from 'lodash'
52import { DEFAULT_THEME_NAME, DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants' 52import { DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants'
53import { clearCacheByUserId } from '../../lib/oauth-model' 53import { clearCacheByUserId } from '../../lib/oauth-model'
54import { UserNotificationSettingModel } from './user-notification-setting' 54import { UserNotificationSettingModel } from './user-notification-setting'
55import { VideoModel } from '../video/video' 55import { VideoModel } from '../video/video'
@@ -71,7 +71,9 @@ import {
71} from '@server/typings/models' 71} from '@server/typings/models'
72 72
73enum ScopeNames { 73enum ScopeNames {
74 FOR_ME_API = 'FOR_ME_API' 74 FOR_ME_API = 'FOR_ME_API',
75 WITH_VIDEOCHANNELS = 'WITH_VIDEOCHANNELS',
76 WITH_STATS = 'WITH_STATS'
75} 77}
76 78
77@DefaultScope(() => ({ 79@DefaultScope(() => ({
@@ -101,7 +103,7 @@ enum ScopeNames {
101 required: true, 103 required: true,
102 where: { 104 where: {
103 type: { 105 type: {
104 [ Op.ne ]: VideoPlaylistType.REGULAR 106 [Op.ne]: VideoPlaylistType.REGULAR
105 } 107 }
106 } 108 }
107 } 109 }
@@ -112,6 +114,96 @@ enum ScopeNames {
112 required: true 114 required: true
113 } 115 }
114 ] 116 ]
117 },
118 [ScopeNames.WITH_VIDEOCHANNELS]: {
119 include: [
120 {
121 model: AccountModel,
122 include: [
123 {
124 model: VideoChannelModel
125 },
126 {
127 attributes: [ 'id', 'name', 'type' ],
128 model: VideoPlaylistModel.unscoped(),
129 required: true,
130 where: {
131 type: {
132 [Op.ne]: VideoPlaylistType.REGULAR
133 }
134 }
135 }
136 ]
137 }
138 ]
139 },
140 [ScopeNames.WITH_STATS]: {
141 attributes: {
142 include: [
143 [
144 literal(
145 '(' +
146 UserModel.generateUserQuotaBaseSQL({
147 withSelect: false,
148 whereUserId: '"UserModel"."id"'
149 }) +
150 ')'
151 ),
152 'videoQuotaUsed'
153 ],
154 [
155 literal(
156 '(' +
157 'SELECT COUNT("video"."id") ' +
158 'FROM "video" ' +
159 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
160 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' +
161 'WHERE "account"."userId" = "UserModel"."id"' +
162 ')'
163 ),
164 'videosCount'
165 ],
166 [
167 literal(
168 '(' +
169 `SELECT concat_ws(':', "abuses", "acceptedAbuses") ` +
170 'FROM (' +
171 'SELECT COUNT("videoAbuse"."id") AS "abuses", ' +
172 `COUNT("videoAbuse"."id") FILTER (WHERE "videoAbuse"."state" = ${VideoAbuseState.ACCEPTED}) AS "acceptedAbuses" ` +
173 'FROM "videoAbuse" ' +
174 'INNER JOIN "video" ON "videoAbuse"."videoId" = "video"."id" ' +
175 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
176 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' +
177 'WHERE "account"."userId" = "UserModel"."id"' +
178 ') t' +
179 ')'
180 ),
181 'videoAbusesCount'
182 ],
183 [
184 literal(
185 '(' +
186 'SELECT COUNT("videoAbuse"."id") ' +
187 'FROM "videoAbuse" ' +
188 'INNER JOIN "account" ON "account"."id" = "videoAbuse"."reporterAccountId" ' +
189 'WHERE "account"."userId" = "UserModel"."id"' +
190 ')'
191 ),
192 'videoAbusesCreatedCount'
193 ],
194 [
195 literal(
196 '(' +
197 'SELECT COUNT("videoComment"."id") ' +
198 'FROM "videoComment" ' +
199 'INNER JOIN "account" ON "account"."id" = "videoComment"."accountId" ' +
200 'WHERE "account"."userId" = "UserModel"."id"' +
201 ')'
202 ),
203 'videoCommentsCount'
204 ]
205 ]
206 }
115 } 207 }
116})) 208}))
117@Table({ 209@Table({
@@ -129,13 +221,13 @@ enum ScopeNames {
129}) 221})
130export class UserModel extends Model<UserModel> { 222export class UserModel extends Model<UserModel> {
131 223
132 @AllowNull(false) 224 @AllowNull(true)
133 @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password')) 225 @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password', true))
134 @Column 226 @Column
135 password: string 227 password: string
136 228
137 @AllowNull(false) 229 @AllowNull(false)
138 @Is('UserPassword', value => throwIfNotValid(value, isUserUsernameValid, 'user name')) 230 @Is('UserUsername', value => throwIfNotValid(value, isUserUsernameValid, 'user name'))
139 @Column 231 @Column
140 username: string 232 username: string
141 233
@@ -186,7 +278,10 @@ export class UserModel extends Model<UserModel> {
186 278
187 @AllowNull(false) 279 @AllowNull(false)
188 @Default(true) 280 @Default(true)
189 @Is('UserAutoPlayNextVideoPlaylist', value => throwIfNotValid(value, isUserAutoPlayNextVideoPlaylistValid, 'auto play next video for playlists boolean')) 281 @Is(
282 'UserAutoPlayNextVideoPlaylist',
283 value => throwIfNotValid(value, isUserAutoPlayNextVideoPlaylistValid, 'auto play next video for playlists boolean')
284 )
190 @Column 285 @Column
191 autoPlayNextVideoPlaylist: boolean 286 autoPlayNextVideoPlaylist: boolean
192 287
@@ -230,7 +325,7 @@ export class UserModel extends Model<UserModel> {
230 videoQuotaDaily: number 325 videoQuotaDaily: number
231 326
232 @AllowNull(false) 327 @AllowNull(false)
233 @Default(DEFAULT_THEME_NAME) 328 @Default(DEFAULT_USER_THEME_NAME)
234 @Is('UserTheme', value => throwIfNotValid(value, isThemeNameValid, 'theme')) 329 @Is('UserTheme', value => throwIfNotValid(value, isThemeNameValid, 'theme'))
235 @Column 330 @Column
236 theme: string 331 theme: string
@@ -253,6 +348,16 @@ export class UserModel extends Model<UserModel> {
253 @Column 348 @Column
254 noWelcomeModal: boolean 349 noWelcomeModal: boolean
255 350
351 @AllowNull(true)
352 @Default(null)
353 @Column
354 pluginAuth: string
355
356 @AllowNull(true)
357 @Default(null)
358 @Column
359 lastLoginDate: Date
360
256 @CreatedAt 361 @CreatedAt
257 createdAt: Date 362 createdAt: Date
258 363
@@ -288,7 +393,7 @@ export class UserModel extends Model<UserModel> {
288 @BeforeCreate 393 @BeforeCreate
289 @BeforeUpdate 394 @BeforeUpdate
290 static cryptPasswordIfNeeded (instance: UserModel) { 395 static cryptPasswordIfNeeded (instance: UserModel) {
291 if (instance.changed('password')) { 396 if (instance.changed('password') && instance.password) {
292 return cryptPassword(instance.password) 397 return cryptPassword(instance.password)
293 .then(hash => { 398 .then(hash => {
294 instance.password = hash 399 instance.password = hash
@@ -308,7 +413,8 @@ export class UserModel extends Model<UserModel> {
308 } 413 }
309 414
310 static listForApi (start: number, count: number, sort: string, search?: string) { 415 static listForApi (start: number, count: number, sort: string, search?: string) {
311 let where = undefined 416 let where: WhereOptions
417
312 if (search) { 418 if (search) {
313 where = { 419 where = {
314 [Op.or]: [ 420 [Op.or]: [
@@ -319,7 +425,7 @@ export class UserModel extends Model<UserModel> {
319 }, 425 },
320 { 426 {
321 username: { 427 username: {
322 [ Op.iLike ]: '%' + search + '%' 428 [Op.iLike]: '%' + search + '%'
323 } 429 }
324 } 430 }
325 ] 431 ]
@@ -332,18 +438,14 @@ export class UserModel extends Model<UserModel> {
332 [ 438 [
333 literal( 439 literal(
334 '(' + 440 '(' +
335 'SELECT COALESCE(SUM("size"), 0) ' + 441 UserModel.generateUserQuotaBaseSQL({
336 'FROM (' + 442 withSelect: false,
337 'SELECT MAX("videoFile"."size") AS "size" FROM "videoFile" ' + 443 whereUserId: '"UserModel"."id"'
338 'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' + 444 }) +
339 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
340 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
341 'WHERE "account"."userId" = "UserModel"."id" GROUP BY "video"."id"' +
342 ') t' +
343 ')' 445 ')'
344 ), 446 ),
345 'videoQuotaUsed' 447 'videoQuotaUsed'
346 ] 448 ] as any // FIXME: typings
347 ] 449 ]
348 }, 450 },
349 offset: start, 451 offset: start,
@@ -353,18 +455,18 @@ export class UserModel extends Model<UserModel> {
353 } 455 }
354 456
355 return UserModel.findAndCountAll(query) 457 return UserModel.findAndCountAll(query)
356 .then(({ rows, count }) => { 458 .then(({ rows, count }) => {
357 return { 459 return {
358 data: rows, 460 data: rows,
359 total: count 461 total: count
360 } 462 }
361 }) 463 })
362 } 464 }
363 465
364 static listWithRight (right: UserRight): Bluebird<MUserDefault[]> { 466 static listWithRight (right: UserRight): Bluebird<MUserDefault[]> {
365 const roles = Object.keys(USER_ROLE_LABELS) 467 const roles = Object.keys(USER_ROLE_LABELS)
366 .map(k => parseInt(k, 10) as UserRole) 468 .map(k => parseInt(k, 10) as UserRole)
367 .filter(role => hasUserRight(role, right)) 469 .filter(role => hasUserRight(role, right))
368 470
369 const query = { 471 const query = {
370 where: { 472 where: {
@@ -390,7 +492,7 @@ export class UserModel extends Model<UserModel> {
390 required: true, 492 required: true,
391 include: [ 493 include: [
392 { 494 {
393 attributes: [ ], 495 attributes: [],
394 model: ActorModel.unscoped(), 496 model: ActorModel.unscoped(),
395 required: true, 497 required: true,
396 where: { 498 where: {
@@ -398,7 +500,7 @@ export class UserModel extends Model<UserModel> {
398 }, 500 },
399 include: [ 501 include: [
400 { 502 {
401 attributes: [ ], 503 attributes: [],
402 as: 'ActorFollowings', 504 as: 'ActorFollowings',
403 model: ActorFollowModel.unscoped(), 505 model: ActorFollowModel.unscoped(),
404 required: true, 506 required: true,
@@ -426,14 +528,20 @@ export class UserModel extends Model<UserModel> {
426 return UserModel.findAll(query) 528 return UserModel.findAll(query)
427 } 529 }
428 530
429 static loadById (id: number): Bluebird<MUserDefault> { 531 static loadById (id: number, withStats = false): Bluebird<MUserDefault> {
430 return UserModel.findByPk(id) 532 const scopes = [
533 ScopeNames.WITH_VIDEOCHANNELS
534 ]
535
536 if (withStats) scopes.push(ScopeNames.WITH_STATS)
537
538 return UserModel.scope(scopes).findByPk(id)
431 } 539 }
432 540
433 static loadByUsername (username: string): Bluebird<MUserDefault> { 541 static loadByUsername (username: string): Bluebird<MUserDefault> {
434 const query = { 542 const query = {
435 where: { 543 where: {
436 username: { [ Op.iLike ]: username } 544 username: { [Op.iLike]: username }
437 } 545 }
438 } 546 }
439 547
@@ -443,7 +551,7 @@ export class UserModel extends Model<UserModel> {
443 static loadForMeAPI (username: string): Bluebird<MUserNotifSettingChannelDefault> { 551 static loadForMeAPI (username: string): Bluebird<MUserNotifSettingChannelDefault> {
444 const query = { 552 const query = {
445 where: { 553 where: {
446 username: { [ Op.iLike ]: username } 554 username: { [Op.iLike]: username }
447 } 555 }
448 } 556 }
449 557
@@ -465,7 +573,7 @@ export class UserModel extends Model<UserModel> {
465 573
466 const query = { 574 const query = {
467 where: { 575 where: {
468 [ Op.or ]: [ 576 [Op.or]: [
469 where(fn('lower', col('username')), fn('lower', username)), 577 where(fn('lower', col('username')), fn('lower', username)),
470 578
471 { email } 579 { email }
@@ -567,7 +675,10 @@ export class UserModel extends Model<UserModel> {
567 675
568 static getOriginalVideoFileTotalFromUser (user: MUserId) { 676 static getOriginalVideoFileTotalFromUser (user: MUserId) {
569 // Don't use sequelize because we need to use a sub query 677 // Don't use sequelize because we need to use a sub query
570 const query = UserModel.generateUserQuotaBaseSQL() 678 const query = UserModel.generateUserQuotaBaseSQL({
679 withSelect: true,
680 whereUserId: '$userId'
681 })
571 682
572 return UserModel.getTotalRawQuery(query, user.id) 683 return UserModel.getTotalRawQuery(query, user.id)
573 } 684 }
@@ -575,16 +686,38 @@ export class UserModel extends Model<UserModel> {
575 // Returns cumulative size of all video files uploaded in the last 24 hours. 686 // Returns cumulative size of all video files uploaded in the last 24 hours.
576 static getOriginalVideoFileTotalDailyFromUser (user: MUserId) { 687 static getOriginalVideoFileTotalDailyFromUser (user: MUserId) {
577 // Don't use sequelize because we need to use a sub query 688 // Don't use sequelize because we need to use a sub query
578 const query = UserModel.generateUserQuotaBaseSQL('"video"."createdAt" > now() - interval \'24 hours\'') 689 const query = UserModel.generateUserQuotaBaseSQL({
690 withSelect: true,
691 whereUserId: '$userId',
692 where: '"video"."createdAt" > now() - interval \'24 hours\''
693 })
579 694
580 return UserModel.getTotalRawQuery(query, user.id) 695 return UserModel.getTotalRawQuery(query, user.id)
581 } 696 }
582 697
583 static async getStats () { 698 static async getStats () {
699 function getActiveUsers (days: number) {
700 const query = {
701 where: {
702 [Op.and]: [
703 literal(`"lastLoginDate" > NOW() - INTERVAL '${days}d'`)
704 ]
705 }
706 }
707
708 return UserModel.count(query)
709 }
710
584 const totalUsers = await UserModel.count() 711 const totalUsers = await UserModel.count()
712 const totalDailyActiveUsers = await getActiveUsers(1)
713 const totalWeeklyActiveUsers = await getActiveUsers(7)
714 const totalMonthlyActiveUsers = await getActiveUsers(30)
585 715
586 return { 716 return {
587 totalUsers 717 totalUsers,
718 totalDailyActiveUsers,
719 totalWeeklyActiveUsers,
720 totalMonthlyActiveUsers
588 } 721 }
589 } 722 }
590 723
@@ -592,7 +725,7 @@ export class UserModel extends Model<UserModel> {
592 const query = { 725 const query = {
593 where: { 726 where: {
594 username: { 727 username: {
595 [ Op.like ]: `%${search}%` 728 [Op.like]: `%${search}%`
596 } 729 }
597 }, 730 },
598 limit: 10 731 limit: 10
@@ -633,6 +766,10 @@ export class UserModel extends Model<UserModel> {
633 toFormattedJSON (this: MUserFormattable, parameters: { withAdminFlags?: boolean } = {}): User { 766 toFormattedJSON (this: MUserFormattable, parameters: { withAdminFlags?: boolean } = {}): User {
634 const videoQuotaUsed = this.get('videoQuotaUsed') 767 const videoQuotaUsed = this.get('videoQuotaUsed')
635 const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily') 768 const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily')
769 const videosCount = this.get('videosCount')
770 const [ videoAbusesCount, videoAbusesAcceptedCount ] = (this.get('videoAbusesCount') as string || ':').split(':')
771 const videoAbusesCreatedCount = this.get('videoAbusesCreatedCount')
772 const videoCommentsCount = this.get('videoCommentsCount')
636 773
637 const json: User = { 774 const json: User = {
638 id: this.id, 775 id: this.id,
@@ -652,7 +789,7 @@ export class UserModel extends Model<UserModel> {
652 videoLanguages: this.videoLanguages, 789 videoLanguages: this.videoLanguages,
653 790
654 role: this.role, 791 role: this.role,
655 roleLabel: USER_ROLE_LABELS[ this.role ], 792 roleLabel: USER_ROLE_LABELS[this.role],
656 793
657 videoQuota: this.videoQuota, 794 videoQuota: this.videoQuota,
658 videoQuotaDaily: this.videoQuotaDaily, 795 videoQuotaDaily: this.videoQuotaDaily,
@@ -662,6 +799,21 @@ export class UserModel extends Model<UserModel> {
662 videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined 799 videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined
663 ? parseInt(videoQuotaUsedDaily + '', 10) 800 ? parseInt(videoQuotaUsedDaily + '', 10)
664 : undefined, 801 : undefined,
802 videosCount: videosCount !== undefined
803 ? parseInt(videosCount + '', 10)
804 : undefined,
805 videoAbusesCount: videoAbusesCount
806 ? parseInt(videoAbusesCount, 10)
807 : undefined,
808 videoAbusesAcceptedCount: videoAbusesAcceptedCount
809 ? parseInt(videoAbusesAcceptedCount, 10)
810 : undefined,
811 videoAbusesCreatedCount: videoAbusesCreatedCount !== undefined
812 ? parseInt(videoAbusesCreatedCount + '', 10)
813 : undefined,
814 videoCommentsCount: videoCommentsCount !== undefined
815 ? parseInt(videoCommentsCount + '', 10)
816 : undefined,
665 817
666 noInstanceConfigWarningModal: this.noInstanceConfigWarningModal, 818 noInstanceConfigWarningModal: this.noInstanceConfigWarningModal,
667 noWelcomeModal: this.noWelcomeModal, 819 noWelcomeModal: this.noWelcomeModal,
@@ -677,7 +829,11 @@ export class UserModel extends Model<UserModel> {
677 829
678 videoChannels: [], 830 videoChannels: [],
679 831
680 createdAt: this.createdAt 832 createdAt: this.createdAt,
833
834 pluginAuth: this.pluginAuth,
835
836 lastLoginDate: this.lastLoginDate
681 } 837 }
682 838
683 if (parameters.withAdminFlags) { 839 if (parameters.withAdminFlags) {
@@ -686,13 +842,13 @@ export class UserModel extends Model<UserModel> {
686 842
687 if (Array.isArray(this.Account.VideoChannels) === true) { 843 if (Array.isArray(this.Account.VideoChannels) === true) {
688 json.videoChannels = this.Account.VideoChannels 844 json.videoChannels = this.Account.VideoChannels
689 .map(c => c.toFormattedJSON()) 845 .map(c => c.toFormattedJSON())
690 .sort((v1, v2) => { 846 .sort((v1, v2) => {
691 if (v1.createdAt < v2.createdAt) return -1 847 if (v1.createdAt < v2.createdAt) return -1
692 if (v1.createdAt === v2.createdAt) return 0 848 if (v1.createdAt === v2.createdAt) return 0
693 849
694 return 1 850 return 1
695 }) 851 })
696 } 852 }
697 853
698 return json 854 return json
@@ -702,7 +858,7 @@ export class UserModel extends Model<UserModel> {
702 const formatted = this.toFormattedJSON() 858 const formatted = this.toFormattedJSON()
703 859
704 const specialPlaylists = this.Account.VideoPlaylists 860 const specialPlaylists = this.Account.VideoPlaylists
705 .map(p => ({ id: p.id, name: p.name, type: p.type })) 861 .map(p => ({ id: p.id, name: p.name, type: p.type }))
706 862
707 return Object.assign(formatted, { specialPlaylists }) 863 return Object.assign(formatted, { specialPlaylists })
708 } 864 }
@@ -724,18 +880,33 @@ export class UserModel extends Model<UserModel> {
724 return uploadedTotal < this.videoQuota && uploadedDaily < this.videoQuotaDaily 880 return uploadedTotal < this.videoQuota && uploadedDaily < this.videoQuotaDaily
725 } 881 }
726 882
727 private static generateUserQuotaBaseSQL (where?: string) { 883 private static generateUserQuotaBaseSQL (options: {
728 const andWhere = where ? 'AND ' + where : '' 884 whereUserId: '$userId' | '"UserModel"."id"'
729 885 withSelect: boolean
730 return 'SELECT SUM("size") AS "total" ' + 886 where?: string
887 }) {
888 const andWhere = options.where
889 ? 'AND ' + options.where
890 : ''
891
892 const videoChannelJoin = 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
893 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
894 `WHERE "account"."userId" = ${options.whereUserId} ${andWhere}`
895
896 const webtorrentFiles = 'SELECT "videoFile"."size" AS "size", "video"."id" AS "videoId" FROM "videoFile" ' +
897 'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' +
898 videoChannelJoin
899
900 const hlsFiles = 'SELECT "videoFile"."size" AS "size", "video"."id" AS "videoId" FROM "videoFile" ' +
901 'INNER JOIN "videoStreamingPlaylist" ON "videoFile"."videoStreamingPlaylistId" = "videoStreamingPlaylist".id ' +
902 'INNER JOIN "video" ON "videoStreamingPlaylist"."videoId" = "video"."id" ' +
903 videoChannelJoin
904
905 return 'SELECT COALESCE(SUM("size"), 0) AS "total" ' +
731 'FROM (' + 906 'FROM (' +
732 'SELECT MAX("videoFile"."size") AS "size" FROM "videoFile" ' + 907 `SELECT MAX("t1"."size") AS "size" FROM (${webtorrentFiles} UNION ${hlsFiles}) t1 ` +
733 'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' + 908 'GROUP BY "t1"."videoId"' +
734 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + 909 ') t2'
735 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
736 'WHERE "account"."userId" = $userId ' + andWhere +
737 'GROUP BY "video"."id"' +
738 ') t'
739 } 910 }
740 911
741 private static getTotalRawQuery (query: string, userId: number) { 912 private static getTotalRawQuery (query: string, userId: number) {
diff --git a/server/models/activitypub/actor-follow.ts b/server/models/activitypub/actor-follow.ts
index f21d2b8a2..85a371026 100644
--- a/server/models/activitypub/actor-follow.ts
+++ b/server/models/activitypub/actor-follow.ts
@@ -1,5 +1,5 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { values, difference } from 'lodash' 2import { difference, values } from 'lodash'
3import { 3import {
4 AfterCreate, 4 AfterCreate,
5 AfterDestroy, 5 AfterDestroy,
@@ -20,10 +20,9 @@ import {
20import { FollowState } from '../../../shared/models/actors' 20import { FollowState } from '../../../shared/models/actors'
21import { ActorFollow } from '../../../shared/models/actors/follow.model' 21import { ActorFollow } from '../../../shared/models/actors/follow.model'
22import { logger } from '../../helpers/logger' 22import { logger } from '../../helpers/logger'
23import { getServerActor } from '../../helpers/utils'
24import { ACTOR_FOLLOW_SCORE, FOLLOW_STATES, SERVER_ACTOR_NAME } from '../../initializers/constants' 23import { ACTOR_FOLLOW_SCORE, FOLLOW_STATES, SERVER_ACTOR_NAME } from '../../initializers/constants'
25import { ServerModel } from '../server/server' 24import { ServerModel } from '../server/server'
26import { createSafeIn, getSort, getFollowsSort } from '../utils' 25import { createSafeIn, getFollowsSort, getSort } from '../utils'
27import { ActorModel, unusedActorAttributesForAPI } from './actor' 26import { ActorModel, unusedActorAttributesForAPI } from './actor'
28import { VideoChannelModel } from '../video/video-channel' 27import { VideoChannelModel } from '../video/video-channel'
29import { AccountModel } from '../account/account' 28import { AccountModel } from '../account/account'
@@ -36,7 +35,8 @@ import {
36 MActorFollowSubscriptions 35 MActorFollowSubscriptions
37} from '@server/typings/models' 36} from '@server/typings/models'
38import { ActivityPubActorType } from '@shared/models' 37import { ActivityPubActorType } from '@shared/models'
39import { afterCommitIfTransaction } from '@server/helpers/database-utils' 38import { VideoModel } from '@server/models/video/video'
39import { getServerActor } from '@server/models/application/application'
40 40
41@Table({ 41@Table({
42 tableName: 'actorFollow', 42 tableName: 'actorFollow',
@@ -152,6 +152,18 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
152 if (numberOfActorFollowsRemoved) logger.info('Removed bad %d actor follows.', numberOfActorFollowsRemoved) 152 if (numberOfActorFollowsRemoved) logger.info('Removed bad %d actor follows.', numberOfActorFollowsRemoved)
153 } 153 }
154 154
155 static isFollowedBy (actorId: number, followerActorId: number) {
156 const query = 'SELECT 1 FROM "actorFollow" WHERE "actorId" = $followerActorId AND "targetActorId" = $actorId LIMIT 1'
157 const options = {
158 type: QueryTypes.SELECT as QueryTypes.SELECT,
159 bind: { actorId, followerActorId },
160 raw: true
161 }
162
163 return VideoModel.sequelize.query(query, options)
164 .then(results => results.length === 1)
165 }
166
155 static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Transaction): Bluebird<MActorFollowActorsDefault> { 167 static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Transaction): Bluebird<MActorFollowActorsDefault> {
156 const query = { 168 const query = {
157 where: { 169 where: {
@@ -226,7 +238,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
226 238
227 return ActorFollowModel.findOne(query) 239 return ActorFollowModel.findOne(query)
228 .then(result => { 240 .then(result => {
229 if (result && result.ActorFollowing.VideoChannel) { 241 if (result?.ActorFollowing.VideoChannel) {
230 result.ActorFollowing.VideoChannel.Actor = result.ActorFollowing 242 result.ActorFollowing.VideoChannel.Actor = result.ActorFollowing
231 } 243 }
232 244
@@ -239,24 +251,24 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
239 .map(t => { 251 .map(t => {
240 if (t.host) { 252 if (t.host) {
241 return { 253 return {
242 [ Op.and ]: [ 254 [Op.and]: [
243 { 255 {
244 '$preferredUsername$': t.name 256 $preferredUsername$: t.name
245 }, 257 },
246 { 258 {
247 '$host$': t.host 259 $host$: t.host
248 } 260 }
249 ] 261 ]
250 } 262 }
251 } 263 }
252 264
253 return { 265 return {
254 [ Op.and ]: [ 266 [Op.and]: [
255 { 267 {
256 '$preferredUsername$': t.name 268 $preferredUsername$: t.name
257 }, 269 },
258 { 270 {
259 '$serverId$': null 271 $serverId$: null
260 } 272 }
261 ] 273 ]
262 } 274 }
@@ -265,9 +277,9 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
265 const query = { 277 const query = {
266 attributes: [], 278 attributes: [],
267 where: { 279 where: {
268 [ Op.and ]: [ 280 [Op.and]: [
269 { 281 {
270 [ Op.or ]: whereTab 282 [Op.or]: whereTab
271 }, 283 },
272 { 284 {
273 actorId 285 actorId
@@ -295,12 +307,12 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
295 } 307 }
296 308
297 static listFollowingForApi (options: { 309 static listFollowingForApi (options: {
298 id: number, 310 id: number
299 start: number, 311 start: number
300 count: number, 312 count: number
301 sort: string, 313 sort: string
302 state?: FollowState, 314 state?: FollowState
303 actorType?: ActivityPubActorType, 315 actorType?: ActivityPubActorType
304 search?: string 316 search?: string
305 }) { 317 }) {
306 const { id, start, count, sort, search, state, actorType } = options 318 const { id, start, count, sort, search, state, actorType } = options
@@ -312,7 +324,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
312 if (search) { 324 if (search) {
313 Object.assign(followingServerWhere, { 325 Object.assign(followingServerWhere, {
314 host: { 326 host: {
315 [ Op.iLike ]: '%' + search + '%' 327 [Op.iLike]: '%' + search + '%'
316 } 328 }
317 }) 329 })
318 } 330 }
@@ -362,12 +374,12 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
362 } 374 }
363 375
364 static listFollowersForApi (options: { 376 static listFollowersForApi (options: {
365 actorId: number, 377 actorId: number
366 start: number, 378 start: number
367 count: number, 379 count: number
368 sort: string, 380 sort: string
369 state?: FollowState, 381 state?: FollowState
370 actorType?: ActivityPubActorType, 382 actorType?: ActivityPubActorType
371 search?: string 383 search?: string
372 }) { 384 }) {
373 const { actorId, start, count, sort, search, state, actorType } = options 385 const { actorId, start, count, sort, search, state, actorType } = options
@@ -379,7 +391,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
379 if (search) { 391 if (search) {
380 Object.assign(followerServerWhere, { 392 Object.assign(followerServerWhere, {
381 host: { 393 host: {
382 [ Op.iLike ]: '%' + search + '%' 394 [Op.iLike]: '%' + search + '%'
383 } 395 }
384 }) 396 })
385 } 397 }
@@ -631,7 +643,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
631 643
632 const tasks: Bluebird<any>[] = [] 644 const tasks: Bluebird<any>[] = []
633 645
634 for (let selection of selections) { 646 for (const selection of selections) {
635 let query = 'SELECT ' + selection + ' FROM "actor" ' + 647 let query = 'SELECT ' + selection + ' FROM "actor" ' +
636 'INNER JOIN "actorFollow" ON "actorFollow"."' + firstJoin + '" = "actor"."id" ' + 648 'INNER JOIN "actorFollow" ON "actorFollow"."' + firstJoin + '" = "actor"."id" ' +
637 'INNER JOIN "actor" AS "Follows" ON "actorFollow"."' + secondJoin + '" = "Follows"."id" ' + 649 'INNER JOIN "actor" AS "Follows" ON "actorFollow"."' + secondJoin + '" = "Follows"."id" ' +
diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts
index 007647ced..34bc91706 100644
--- a/server/models/activitypub/actor.ts
+++ b/server/models/activitypub/actor.ts
@@ -16,7 +16,7 @@ import {
16 Table, 16 Table,
17 UpdatedAt 17 UpdatedAt
18} from 'sequelize-typescript' 18} from 'sequelize-typescript'
19import { ActivityPubActorType } from '../../../shared/models/activitypub' 19import { ActivityIconObject, ActivityPubActorType } from '../../../shared/models/activitypub'
20import { Avatar } from '../../../shared/models/avatars/avatar.model' 20import { Avatar } from '../../../shared/models/avatars/avatar.model'
21import { activityPubContextify } from '../../helpers/activitypub' 21import { activityPubContextify } from '../../helpers/activitypub'
22import { 22import {
@@ -43,11 +43,12 @@ import {
43 MActorFull, 43 MActorFull,
44 MActorHost, 44 MActorHost,
45 MActorServer, 45 MActorServer,
46 MActorSummaryFormattable, 46 MActorSummaryFormattable, MActorUrl,
47 MActorWithInboxes 47 MActorWithInboxes
48} from '../../typings/models' 48} from '../../typings/models'
49import * as Bluebird from 'bluebird' 49import * as Bluebird from 'bluebird'
50import { Op, Transaction, literal } from 'sequelize' 50import { Op, Transaction, literal } from 'sequelize'
51import { ModelCache } from '@server/models/model-cache'
51 52
52enum ScopeNames { 53enum ScopeNames {
53 FULL = 'FULL' 54 FULL = 'FULL'
@@ -122,13 +123,13 @@ export const unusedActorAttributesForAPI = [
122 } 123 }
123 } 124 }
124 }, 125 },
125 // { 126 {
126 // fields: [ 'preferredUsername' ], 127 fields: [ 'preferredUsername' ],
127 // unique: true, 128 unique: true,
128 // where: { 129 where: {
129 // serverId: null 130 serverId: null
130 // } 131 }
131 // }, 132 },
132 { 133 {
133 fields: [ 'inboxUrl', 'sharedInboxUrl' ] 134 fields: [ 'inboxUrl', 'sharedInboxUrl' ]
134 }, 135 },
@@ -276,8 +277,6 @@ export class ActorModel extends Model<ActorModel> {
276 }) 277 })
277 VideoChannel: VideoChannelModel 278 VideoChannel: VideoChannelModel
278 279
279 private static cache: { [ id: string ]: any } = {}
280
281 static load (id: number): Bluebird<MActor> { 280 static load (id: number): Bluebird<MActor> {
282 return ActorModel.unscoped().findByPk(id) 281 return ActorModel.unscoped().findByPk(id)
283 } 282 }
@@ -334,7 +333,7 @@ export class ActorModel extends Model<ActorModel> {
334 const query = { 333 const query = {
335 where: { 334 where: {
336 followersUrl: { 335 followersUrl: {
337 [ Op.in ]: followersUrls 336 [Op.in]: followersUrls
338 } 337 }
339 }, 338 },
340 transaction 339 transaction
@@ -344,28 +343,50 @@ export class ActorModel extends Model<ActorModel> {
344 } 343 }
345 344
346 static loadLocalByName (preferredUsername: string, transaction?: Transaction): Bluebird<MActorFull> { 345 static loadLocalByName (preferredUsername: string, transaction?: Transaction): Bluebird<MActorFull> {
347 // The server actor never change, so we can easily cache it 346 const fun = () => {
348 if (preferredUsername === SERVER_ACTOR_NAME && ActorModel.cache[preferredUsername]) { 347 const query = {
349 return Bluebird.resolve(ActorModel.cache[preferredUsername]) 348 where: {
350 } 349 preferredUsername,
350 serverId: null
351 },
352 transaction
353 }
351 354
352 const query = { 355 return ActorModel.scope(ScopeNames.FULL)
353 where: { 356 .findOne(query)
354 preferredUsername,
355 serverId: null
356 },
357 transaction
358 } 357 }
359 358
360 return ActorModel.scope(ScopeNames.FULL) 359 return ModelCache.Instance.doCache({
361 .findOne(query) 360 cacheType: 'local-actor-name',
362 .then(actor => { 361 key: preferredUsername,
363 if (preferredUsername === SERVER_ACTOR_NAME) { 362 // The server actor never change, so we can easily cache it
364 ActorModel.cache[ preferredUsername ] = actor 363 whitelist: () => preferredUsername === SERVER_ACTOR_NAME,
365 } 364 fun
365 })
366 }
367
368 static loadLocalUrlByName (preferredUsername: string, transaction?: Transaction): Bluebird<MActorUrl> {
369 const fun = () => {
370 const query = {
371 attributes: [ 'url' ],
372 where: {
373 preferredUsername,
374 serverId: null
375 },
376 transaction
377 }
366 378
367 return actor 379 return ActorModel.unscoped()
368 }) 380 .findOne(query)
381 }
382
383 return ModelCache.Instance.doCache({
384 cacheType: 'local-actor-name',
385 key: preferredUsername,
386 // The server actor never change, so we can easily cache it
387 whitelist: () => preferredUsername === SERVER_ACTOR_NAME,
388 fun
389 })
369 } 390 }
370 391
371 static loadByNameAndHost (preferredUsername: string, host: string): Bluebird<MActorFull> { 392 static loadByNameAndHost (preferredUsername: string, host: string): Bluebird<MActorFull> {
@@ -441,6 +462,36 @@ export class ActorModel extends Model<ActorModel> {
441 }, { where, transaction }) 462 }, { where, transaction })
442 } 463 }
443 464
465 static loadAccountActorByVideoId (videoId: number): Bluebird<MActor> {
466 const query = {
467 include: [
468 {
469 attributes: [ 'id' ],
470 model: AccountModel.unscoped(),
471 required: true,
472 include: [
473 {
474 attributes: [ 'id', 'accountId' ],
475 model: VideoChannelModel.unscoped(),
476 required: true,
477 include: [
478 {
479 attributes: [ 'id', 'channelId' ],
480 model: VideoModel.unscoped(),
481 where: {
482 id: videoId
483 }
484 }
485 ]
486 }
487 ]
488 }
489 ]
490 }
491
492 return ActorModel.unscoped().findOne(query)
493 }
494
444 getSharedInbox (this: MActorWithInboxes) { 495 getSharedInbox (this: MActorWithInboxes) {
445 return this.sharedInboxUrl || this.inboxUrl 496 return this.sharedInboxUrl || this.inboxUrl
446 } 497 }
@@ -473,9 +524,11 @@ export class ActorModel extends Model<ActorModel> {
473 } 524 }
474 525
475 toActivityPubObject (this: MActorAP, name: string) { 526 toActivityPubObject (this: MActorAP, name: string) {
476 let icon = undefined 527 let icon: ActivityIconObject
528
477 if (this.avatarId) { 529 if (this.avatarId) {
478 const extension = extname(this.Avatar.filename) 530 const extension = extname(this.Avatar.filename)
531
479 icon = { 532 icon = {
480 type: 'Image', 533 type: 'Image',
481 mediaType: extension === '.png' ? 'image/png' : 'image/jpeg', 534 mediaType: extension === '.png' ? 'image/png' : 'image/jpeg',
diff --git a/server/models/application/application.ts b/server/models/application/application.ts
index 81320b9af..3bba2c70e 100644
--- a/server/models/application/application.ts
+++ b/server/models/application/application.ts
@@ -1,5 +1,16 @@
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'
3import * as memoizee from 'memoizee'
4
5export const getServerActor = memoizee(async function () {
6 const application = await ApplicationModel.load()
7 if (!application) throw Error('Could not load Application from database.')
8
9 const actor = application.Account.Actor
10 actor.Account = application.Account
11
12 return actor
13}, { promise: true })
3 14
4@DefaultScope(() => ({ 15@DefaultScope(() => ({
5 include: [ 16 include: [
diff --git a/server/models/model-cache.ts b/server/models/model-cache.ts
new file mode 100644
index 000000000..a87f99aa2
--- /dev/null
+++ b/server/models/model-cache.ts
@@ -0,0 +1,91 @@
1import { Model } from 'sequelize-typescript'
2import * as Bluebird from 'bluebird'
3import { logger } from '@server/helpers/logger'
4
5type ModelCacheType =
6 'local-account-name'
7 | 'local-actor-name'
8 | 'local-actor-url'
9 | 'load-video-immutable-id'
10 | 'load-video-immutable-url'
11
12type DeleteKey =
13 'video'
14
15class ModelCache {
16
17 private static instance: ModelCache
18
19 private readonly localCache: { [id in ModelCacheType]: Map<string, any> } = {
20 'local-account-name': new Map(),
21 'local-actor-name': new Map(),
22 'local-actor-url': new Map(),
23 'load-video-immutable-id': new Map(),
24 'load-video-immutable-url': new Map()
25 }
26
27 private readonly deleteIds: {
28 [deleteKey in DeleteKey]: Map<number, { cacheType: ModelCacheType, key: string }[]>
29 } = {
30 video: new Map()
31 }
32
33 private constructor () {
34 }
35
36 static get Instance () {
37 return this.instance || (this.instance = new this())
38 }
39
40 doCache<T extends Model> (options: {
41 cacheType: ModelCacheType
42 key: string
43 fun: () => Bluebird<T>
44 whitelist?: () => boolean
45 deleteKey?: DeleteKey
46 }) {
47 const { cacheType, key, fun, whitelist, deleteKey } = options
48
49 if (whitelist && whitelist() !== true) return fun()
50
51 const cache = this.localCache[cacheType]
52
53 if (cache.has(key)) {
54 logger.debug('Model cache hit for %s -> %s.', cacheType, key)
55 return Bluebird.resolve<T>(cache.get(key))
56 }
57
58 return fun().then(m => {
59 if (!m) return m
60
61 if (!whitelist || whitelist()) cache.set(key, m)
62
63 if (deleteKey) {
64 const map = this.deleteIds[deleteKey]
65 if (!map.has(m.id)) map.set(m.id, [])
66
67 const a = map.get(m.id)
68 a.push({ cacheType, key })
69 }
70
71 return m
72 })
73 }
74
75 invalidateCache (deleteKey: DeleteKey, modelId: number) {
76 const map = this.deleteIds[deleteKey]
77
78 if (!map.has(modelId)) return
79
80 for (const toDelete of map.get(modelId)) {
81 logger.debug('Removing %s -> %d of model cache %s -> %s.', deleteKey, modelId, toDelete.cacheType, toDelete.key)
82 this.localCache[toDelete.cacheType].delete(toDelete.key)
83 }
84
85 map.delete(modelId)
86 }
87}
88
89export {
90 ModelCache
91}
diff --git a/server/models/oauth/oauth-token.ts b/server/models/oauth/oauth-token.ts
index b680be237..38953e8ad 100644
--- a/server/models/oauth/oauth-token.ts
+++ b/server/models/oauth/oauth-token.ts
@@ -23,13 +23,14 @@ import { MOAuthTokenUser } from '@server/typings/models/oauth/oauth-token'
23 23
24export type OAuthTokenInfo = { 24export type OAuthTokenInfo = {
25 refreshToken: string 25 refreshToken: string
26 refreshTokenExpiresAt: Date, 26 refreshTokenExpiresAt: Date
27 client: { 27 client: {
28 id: number 28 id: number
29 }, 29 }
30 user: { 30 user: {
31 id: number 31 id: number
32 } 32 }
33 token: MOAuthTokenUser
33} 34}
34 35
35enum ScopeNames { 36enum ScopeNames {
@@ -97,6 +98,9 @@ export class OAuthTokenModel extends Model<OAuthTokenModel> {
97 @Column 98 @Column
98 refreshTokenExpiresAt: Date 99 refreshTokenExpiresAt: Date
99 100
101 @Column
102 authName: string
103
100 @CreatedAt 104 @CreatedAt
101 createdAt: Date 105 createdAt: Date
102 106
@@ -133,33 +137,41 @@ export class OAuthTokenModel extends Model<OAuthTokenModel> {
133 return clearCacheByToken(token.accessToken) 137 return clearCacheByToken(token.accessToken)
134 } 138 }
135 139
140 static loadByRefreshToken (refreshToken: string) {
141 const query = {
142 where: { refreshToken }
143 }
144
145 return OAuthTokenModel.findOne(query)
146 }
147
136 static getByRefreshTokenAndPopulateClient (refreshToken: string) { 148 static getByRefreshTokenAndPopulateClient (refreshToken: string) {
137 const query = { 149 const query = {
138 where: { 150 where: {
139 refreshToken: refreshToken 151 refreshToken
140 }, 152 },
141 include: [ OAuthClientModel ] 153 include: [ OAuthClientModel ]
142 } 154 }
143 155
144 return OAuthTokenModel.findOne(query) 156 return OAuthTokenModel.scope(ScopeNames.WITH_USER)
145 .then(token => { 157 .findOne(query)
146 if (!token) return null 158 .then(token => {
147 159 if (!token) return null
148 return { 160
149 refreshToken: token.refreshToken, 161 return {
150 refreshTokenExpiresAt: token.refreshTokenExpiresAt, 162 refreshToken: token.refreshToken,
151 client: { 163 refreshTokenExpiresAt: token.refreshTokenExpiresAt,
152 id: token.oAuthClientId 164 client: {
153 }, 165 id: token.oAuthClientId
154 user: { 166 },
155 id: token.userId 167 user: token.User,
156 } 168 token
157 } as OAuthTokenInfo 169 } as OAuthTokenInfo
158 }) 170 })
159 .catch(err => { 171 .catch(err => {
160 logger.error('getRefreshToken error.', { err }) 172 logger.error('getRefreshToken error.', { err })
161 throw err 173 throw err
162 }) 174 })
163 } 175 }
164 176
165 static getByTokenAndPopulateUser (bearerToken: string): Bluebird<MOAuthTokenUser> { 177 static getByTokenAndPopulateUser (bearerToken: string): Bluebird<MOAuthTokenUser> {
@@ -181,14 +193,14 @@ export class OAuthTokenModel extends Model<OAuthTokenModel> {
181 static getByRefreshTokenAndPopulateUser (refreshToken: string): Bluebird<MOAuthTokenUser> { 193 static getByRefreshTokenAndPopulateUser (refreshToken: string): Bluebird<MOAuthTokenUser> {
182 const query = { 194 const query = {
183 where: { 195 where: {
184 refreshToken: refreshToken 196 refreshToken
185 } 197 }
186 } 198 }
187 199
188 return OAuthTokenModel.scope(ScopeNames.WITH_USER) 200 return OAuthTokenModel.scope(ScopeNames.WITH_USER)
189 .findOne(query) 201 .findOne(query)
190 .then(token => { 202 .then(token => {
191 if (!token) return new OAuthTokenModel() 203 if (!token) return undefined
192 204
193 return Object.assign(token, { user: token.User }) 205 return Object.assign(token, { user: token.User })
194 }) 206 })
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts
index 8c9a7eabf..6021408bf 100644
--- a/server/models/redundancy/video-redundancy.ts
+++ b/server/models/redundancy/video-redundancy.ts
@@ -13,13 +13,12 @@ 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, parseAggregateResult, throwIfNotValid } from '../utils' 16import { getSort, getVideoSort, parseAggregateResult, throwIfNotValid } from '../utils'
17import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc' 17import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc'
18import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers/constants' 18import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers/constants'
19import { VideoFileModel } from '../video/video-file' 19import { VideoFileModel } from '../video/video-file'
20import { getServerActor } from '../../helpers/utils'
21import { VideoModel } from '../video/video' 20import { VideoModel } from '../video/video'
22import { VideoRedundancyStrategy } from '../../../shared/models/redundancy' 21import { VideoRedundancyStrategy, VideoRedundancyStrategyWithManual } from '../../../shared/models/redundancy'
23import { logger } from '../../helpers/logger' 22import { logger } from '../../helpers/logger'
24import { CacheFileObject, VideoPrivacy } from '../../../shared' 23import { CacheFileObject, VideoPrivacy } from '../../../shared'
25import { VideoChannelModel } from '../video/video-channel' 24import { VideoChannelModel } from '../video/video-channel'
@@ -27,17 +26,24 @@ import { ServerModel } from '../server/server'
27import { sample } from 'lodash' 26import { sample } from 'lodash'
28import { isTestInstance } from '../../helpers/core-utils' 27import { isTestInstance } from '../../helpers/core-utils'
29import * as Bluebird from 'bluebird' 28import * as Bluebird from 'bluebird'
30import { col, FindOptions, fn, literal, Op, Transaction } from 'sequelize' 29import { col, FindOptions, fn, literal, Op, Transaction, WhereOptions } from 'sequelize'
31import { VideoStreamingPlaylistModel } from '../video/video-streaming-playlist' 30import { VideoStreamingPlaylistModel } from '../video/video-streaming-playlist'
32import { CONFIG } from '../../initializers/config' 31import { CONFIG } from '../../initializers/config'
33import { MVideoRedundancy, MVideoRedundancyAP, MVideoRedundancyVideo } from '@server/typings/models' 32import { MVideoForRedundancyAPI, MVideoRedundancy, MVideoRedundancyAP, MVideoRedundancyVideo } from '@server/typings/models'
33import { VideoRedundanciesTarget } from '@shared/models/redundancy/video-redundancies-filters.model'
34import {
35 FileRedundancyInformation,
36 StreamingPlaylistRedundancyInformation,
37 VideoRedundancy
38} from '@shared/models/redundancy/video-redundancy.model'
39import { getServerActor } from '@server/models/application/application'
34 40
35export enum ScopeNames { 41export enum ScopeNames {
36 WITH_VIDEO = 'WITH_VIDEO' 42 WITH_VIDEO = 'WITH_VIDEO'
37} 43}
38 44
39@Scopes(() => ({ 45@Scopes(() => ({
40 [ ScopeNames.WITH_VIDEO ]: { 46 [ScopeNames.WITH_VIDEO]: {
41 include: [ 47 include: [
42 { 48 {
43 model: VideoFileModel, 49 model: VideoFileModel,
@@ -86,7 +92,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
86 @UpdatedAt 92 @UpdatedAt
87 updatedAt: Date 93 updatedAt: Date
88 94
89 @AllowNull(false) 95 @AllowNull(true)
90 @Column 96 @Column
91 expiresOn: Date 97 expiresOn: Date
92 98
@@ -161,7 +167,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
161 logger.info('Removing duplicated video streaming playlist %s.', videoUUID) 167 logger.info('Removing duplicated video streaming playlist %s.', videoUUID)
162 168
163 videoStreamingPlaylist.Video.removeStreamingPlaylistFiles(videoStreamingPlaylist, true) 169 videoStreamingPlaylist.Video.removeStreamingPlaylistFiles(videoStreamingPlaylist, true)
164 .catch(err => logger.error('Cannot delete video streaming playlist files of %s.', videoUUID, { err })) 170 .catch(err => logger.error('Cannot delete video streaming playlist files of %s.', videoUUID, { err }))
165 } 171 }
166 172
167 return undefined 173 return undefined
@@ -193,6 +199,15 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
193 return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query) 199 return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query)
194 } 200 }
195 201
202 static loadByIdWithVideo (id: number, transaction?: Transaction): Bluebird<MVideoRedundancyVideo> {
203 const query = {
204 where: { id },
205 transaction
206 }
207
208 return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query)
209 }
210
196 static loadByUrl (url: string, transaction?: Transaction): Bluebird<MVideoRedundancy> { 211 static loadByUrl (url: string, transaction?: Transaction): Bluebird<MVideoRedundancy> {
197 const query = { 212 const query = {
198 where: { 213 where: {
@@ -215,12 +230,12 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
215 }, 230 },
216 include: [ 231 include: [
217 { 232 {
218 attributes: [ ], 233 attributes: [],
219 model: VideoFileModel, 234 model: VideoFileModel,
220 required: true, 235 required: true,
221 include: [ 236 include: [
222 { 237 {
223 attributes: [ ], 238 attributes: [],
224 model: VideoModel, 239 model: VideoModel,
225 required: true, 240 required: true,
226 where: { 241 where: {
@@ -233,7 +248,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
233 } 248 }
234 249
235 return VideoRedundancyModel.findOne(query) 250 return VideoRedundancyModel.findOne(query)
236 .then(r => !!r) 251 .then(r => !!r)
237 } 252 }
238 253
239 static async getVideoSample (p: Bluebird<VideoModel[]>) { 254 static async getVideoSample (p: Bluebird<VideoModel[]>) {
@@ -295,7 +310,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
295 where: { 310 where: {
296 privacy: VideoPrivacy.PUBLIC, 311 privacy: VideoPrivacy.PUBLIC,
297 views: { 312 views: {
298 [ Op.gte ]: minViews 313 [Op.gte]: minViews
299 } 314 }
300 }, 315 },
301 include: [ 316 include: [
@@ -318,7 +333,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
318 actorId: actor.id, 333 actorId: actor.id,
319 strategy, 334 strategy,
320 createdAt: { 335 createdAt: {
321 [ Op.lt ]: expiredDate 336 [Op.lt]: expiredDate
322 } 337 }
323 } 338 }
324 } 339 }
@@ -377,7 +392,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
377 where: { 392 where: {
378 actorId: actor.id, 393 actorId: actor.id,
379 expiresOn: { 394 expiresOn: {
380 [ Op.lt ]: new Date() 395 [Op.lt]: new Date()
381 } 396 }
382 } 397 }
383 } 398 }
@@ -394,7 +409,8 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
394 [Op.ne]: actor.id 409 [Op.ne]: actor.id
395 }, 410 },
396 expiresOn: { 411 expiresOn: {
397 [ Op.lt ]: new Date() 412 [Op.lt]: new Date(),
413 [Op.ne]: null
398 } 414 }
399 } 415 }
400 } 416 }
@@ -447,7 +463,112 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
447 return VideoRedundancyModel.findAll(query) 463 return VideoRedundancyModel.findAll(query)
448 } 464 }
449 465
450 static async getStats (strategy: VideoRedundancyStrategy) { 466 static listForApi (options: {
467 start: number
468 count: number
469 sort: string
470 target: VideoRedundanciesTarget
471 strategy?: string
472 }) {
473 const { start, count, sort, target, strategy } = options
474 const redundancyWhere: WhereOptions = {}
475 const videosWhere: WhereOptions = {}
476 let redundancySqlSuffix = ''
477
478 if (target === 'my-videos') {
479 Object.assign(videosWhere, { remote: false })
480 } else if (target === 'remote-videos') {
481 Object.assign(videosWhere, { remote: true })
482 Object.assign(redundancyWhere, { strategy: { [Op.ne]: null } })
483 redundancySqlSuffix = ' AND "videoRedundancy"."strategy" IS NOT NULL'
484 }
485
486 if (strategy) {
487 Object.assign(redundancyWhere, { strategy: strategy })
488 }
489
490 const videoFilterWhere = {
491 [Op.and]: [
492 {
493 [Op.or]: [
494 {
495 id: {
496 [Op.in]: literal(
497 '(' +
498 'SELECT "videoId" FROM "videoFile" ' +
499 'INNER JOIN "videoRedundancy" ON "videoRedundancy"."videoFileId" = "videoFile".id' +
500 redundancySqlSuffix +
501 ')'
502 )
503 }
504 },
505 {
506 id: {
507 [Op.in]: literal(
508 '(' +
509 'select "videoId" FROM "videoStreamingPlaylist" ' +
510 'INNER JOIN "videoRedundancy" ON "videoRedundancy"."videoStreamingPlaylistId" = "videoStreamingPlaylist".id' +
511 redundancySqlSuffix +
512 ')'
513 )
514 }
515 }
516 ]
517 },
518
519 videosWhere
520 ]
521 }
522
523 // /!\ On video model /!\
524 const findOptions = {
525 offset: start,
526 limit: count,
527 order: getSort(sort),
528 include: [
529 {
530 required: false,
531 model: VideoFileModel,
532 include: [
533 {
534 model: VideoRedundancyModel.unscoped(),
535 required: false,
536 where: redundancyWhere
537 }
538 ]
539 },
540 {
541 required: false,
542 model: VideoStreamingPlaylistModel.unscoped(),
543 include: [
544 {
545 model: VideoRedundancyModel.unscoped(),
546 required: false,
547 where: redundancyWhere
548 },
549 {
550 model: VideoFileModel,
551 required: false
552 }
553 ]
554 }
555 ],
556 where: videoFilterWhere
557 }
558
559 // /!\ On video model /!\
560 const countOptions = {
561 where: videoFilterWhere
562 }
563
564 return Promise.all([
565 VideoModel.findAll(findOptions),
566
567 VideoModel.count(countOptions)
568 ]).then(([ data, total ]) => ({ total, data }))
569 }
570
571 static async getStats (strategy: VideoRedundancyStrategyWithManual) {
451 const actor = await getServerActor() 572 const actor = await getServerActor()
452 573
453 const query: FindOptions = { 574 const query: FindOptions = {
@@ -471,11 +592,58 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
471 } 592 }
472 593
473 return VideoRedundancyModel.findOne(query) 594 return VideoRedundancyModel.findOne(query)
474 .then((r: any) => ({ 595 .then((r: any) => ({
475 totalUsed: parseAggregateResult(r.totalUsed), 596 totalUsed: parseAggregateResult(r.totalUsed),
476 totalVideos: r.totalVideos, 597 totalVideos: r.totalVideos,
477 totalVideoFiles: r.totalVideoFiles 598 totalVideoFiles: r.totalVideoFiles
478 })) 599 }))
600 }
601
602 static toFormattedJSONStatic (video: MVideoForRedundancyAPI): VideoRedundancy {
603 const filesRedundancies: FileRedundancyInformation[] = []
604 const streamingPlaylistsRedundancies: StreamingPlaylistRedundancyInformation[] = []
605
606 for (const file of video.VideoFiles) {
607 for (const redundancy of file.RedundancyVideos) {
608 filesRedundancies.push({
609 id: redundancy.id,
610 fileUrl: redundancy.fileUrl,
611 strategy: redundancy.strategy,
612 createdAt: redundancy.createdAt,
613 updatedAt: redundancy.updatedAt,
614 expiresOn: redundancy.expiresOn,
615 size: file.size
616 })
617 }
618 }
619
620 for (const playlist of video.VideoStreamingPlaylists) {
621 const size = playlist.VideoFiles.reduce((a, b) => a + b.size, 0)
622
623 for (const redundancy of playlist.RedundancyVideos) {
624 streamingPlaylistsRedundancies.push({
625 id: redundancy.id,
626 fileUrl: redundancy.fileUrl,
627 strategy: redundancy.strategy,
628 createdAt: redundancy.createdAt,
629 updatedAt: redundancy.updatedAt,
630 expiresOn: redundancy.expiresOn,
631 size
632 })
633 }
634 }
635
636 return {
637 id: video.id,
638 name: video.name,
639 url: video.url,
640 uuid: video.uuid,
641
642 redundancies: {
643 files: filesRedundancies,
644 streamingPlaylists: streamingPlaylistsRedundancies
645 }
646 }
479 } 647 }
480 648
481 getVideo () { 649 getVideo () {
@@ -494,7 +662,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
494 id: this.url, 662 id: this.url,
495 type: 'CacheFile' as 'CacheFile', 663 type: 'CacheFile' as 'CacheFile',
496 object: this.VideoStreamingPlaylist.Video.url, 664 object: this.VideoStreamingPlaylist.Video.url,
497 expires: this.expiresOn.toISOString(), 665 expires: this.expiresOn ? this.expiresOn.toISOString() : null,
498 url: { 666 url: {
499 type: 'Link', 667 type: 'Link',
500 mediaType: 'application/x-mpegURL', 668 mediaType: 'application/x-mpegURL',
@@ -507,10 +675,10 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
507 id: this.url, 675 id: this.url,
508 type: 'CacheFile' as 'CacheFile', 676 type: 'CacheFile' as 'CacheFile',
509 object: this.VideoFile.Video.url, 677 object: this.VideoFile.Video.url,
510 expires: this.expiresOn.toISOString(), 678 expires: this.expiresOn ? this.expiresOn.toISOString() : null,
511 url: { 679 url: {
512 type: 'Link', 680 type: 'Link',
513 mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[ this.VideoFile.extname ] as any, 681 mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[this.VideoFile.extname] as any,
514 href: this.fileUrl, 682 href: this.fileUrl,
515 height: this.VideoFile.resolution, 683 height: this.VideoFile.resolution,
516 size: this.VideoFile.size, 684 size: this.VideoFile.size,
@@ -525,17 +693,17 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
525 693
526 const notIn = literal( 694 const notIn = literal(
527 '(' + 695 '(' +
528 `SELECT "videoFileId" FROM "videoRedundancy" WHERE "actorId" = ${actor.id} AND "videoFileId" IS NOT NULL` + 696 `SELECT "videoFileId" FROM "videoRedundancy" WHERE "actorId" = ${actor.id} AND "videoFileId" IS NOT NULL` +
529 ')' 697 ')'
530 ) 698 )
531 699
532 return { 700 return {
533 attributes: [], 701 attributes: [],
534 model: VideoFileModel.unscoped(), 702 model: VideoFileModel,
535 required: true, 703 required: true,
536 where: { 704 where: {
537 id: { 705 id: {
538 [ Op.notIn ]: notIn 706 [Op.notIn]: notIn
539 } 707 }
540 } 708 }
541 } 709 }
diff --git a/server/models/server/plugin.ts b/server/models/server/plugin.ts
index d094da1f5..53b6227d7 100644
--- a/server/models/server/plugin.ts
+++ b/server/models/server/plugin.ts
@@ -1,5 +1,10 @@
1import * as Bluebird from 'bluebird'
2import { FindAndCountOptions, json, QueryTypes } from 'sequelize'
1import { AllowNull, Column, CreatedAt, DataType, DefaultScope, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' 3import { AllowNull, Column, CreatedAt, DataType, DefaultScope, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
2import { getSort, throwIfNotValid } from '../utils' 4import { MPlugin, MPluginFormattable } from '@server/typings/models'
5import { PeerTubePlugin } from '../../../shared/models/plugins/peertube-plugin.model'
6import { PluginType } from '../../../shared/models/plugins/plugin.type'
7import { RegisterServerSettingOptions } from '../../../shared/models/plugins/register-server-setting.model'
3import { 8import {
4 isPluginDescriptionValid, 9 isPluginDescriptionValid,
5 isPluginHomepage, 10 isPluginHomepage,
@@ -7,12 +12,7 @@ import {
7 isPluginTypeValid, 12 isPluginTypeValid,
8 isPluginVersionValid 13 isPluginVersionValid
9} from '../../helpers/custom-validators/plugins' 14} from '../../helpers/custom-validators/plugins'
10import { PluginType } from '../../../shared/models/plugins/plugin.type' 15import { getSort, throwIfNotValid } from '../utils'
11import { PeerTubePlugin } from '../../../shared/models/plugins/peertube-plugin.model'
12import { FindAndCountOptions, json } from 'sequelize'
13import { RegisterServerSettingOptions } from '../../../shared/models/plugins/register-server-setting.model'
14import * as Bluebird from 'bluebird'
15import { MPlugin, MPluginFormattable } from '@server/typings/models'
16 16
17@DefaultScope(() => ({ 17@DefaultScope(() => ({
18 attributes: { 18 attributes: {
@@ -112,7 +112,7 @@ export class PluginModel extends Model<PluginModel> {
112 return PluginModel.findOne(query) 112 return PluginModel.findOne(query)
113 } 113 }
114 114
115 static getSetting (pluginName: string, pluginType: PluginType, settingName: string) { 115 static getSetting (pluginName: string, pluginType: PluginType, settingName: string, registeredSettings: RegisterServerSettingOptions[]) {
116 const query = { 116 const query = {
117 attributes: [ 'settings' ], 117 attributes: [ 'settings' ],
118 where: { 118 where: {
@@ -123,12 +123,51 @@ export class PluginModel extends Model<PluginModel> {
123 123
124 return PluginModel.findOne(query) 124 return PluginModel.findOne(query)
125 .then(p => { 125 .then(p => {
126 if (!p || !p.settings) return undefined 126 if (!p || !p.settings || p.settings === undefined) {
127 const registered = registeredSettings.find(s => s.name === settingName)
128 if (!registered || registered.default === undefined) return undefined
129
130 return registered.default
131 }
127 132
128 return p.settings[settingName] 133 return p.settings[settingName]
129 }) 134 })
130 } 135 }
131 136
137 static getSettings (
138 pluginName: string,
139 pluginType: PluginType,
140 settingNames: string[],
141 registeredSettings: RegisterServerSettingOptions[]
142 ) {
143 const query = {
144 attributes: [ 'settings' ],
145 where: {
146 name: pluginName,
147 type: pluginType
148 }
149 }
150
151 return PluginModel.findOne(query)
152 .then(p => {
153 const result: { [settingName: string ]: string | boolean } = {}
154
155 for (const name of settingNames) {
156 if (!p || !p.settings || p.settings[name] === undefined) {
157 const registered = registeredSettings.find(s => s.name === name)
158
159 if (registered?.default !== undefined) {
160 result[name] = registered.default
161 }
162 } else {
163 result[name] = p.settings[name]
164 }
165 }
166
167 return result
168 })
169 }
170
132 static setSetting (pluginName: string, pluginType: PluginType, settingName: string, settingValue: string) { 171 static setSetting (pluginName: string, pluginType: PluginType, settingName: string, settingValue: string) {
133 const query = { 172 const query = {
134 where: { 173 where: {
@@ -173,26 +212,25 @@ export class PluginModel extends Model<PluginModel> {
173 } 212 }
174 213
175 static storeData (pluginName: string, pluginType: PluginType, key: string, data: any) { 214 static storeData (pluginName: string, pluginType: PluginType, key: string, data: any) {
176 const query = { 215 const query = 'UPDATE "plugin" SET "storage" = jsonb_set(coalesce("storage", \'{}\'), :key, :data::jsonb) ' +
177 where: { 216 'WHERE "name" = :pluginName AND "type" = :pluginType'
178 name: pluginName,
179 type: pluginType
180 }
181 }
182 217
183 const toSave = { 218 const jsonPath = '{' + key + '}'
184 [`storage.${key}`]: data 219
220 const options = {
221 replacements: { pluginName, pluginType, key: jsonPath, data: JSON.stringify(data) },
222 type: QueryTypes.UPDATE
185 } 223 }
186 224
187 return PluginModel.update(toSave, query) 225 return PluginModel.sequelize.query(query, options)
188 .then(() => undefined) 226 .then(() => undefined)
189 } 227 }
190 228
191 static listForApi (options: { 229 static listForApi (options: {
192 pluginType?: PluginType, 230 pluginType?: PluginType
193 uninstalled?: boolean, 231 uninstalled?: boolean
194 start: number, 232 start: number
195 count: number, 233 count: number
196 sort: string 234 sort: string
197 }) { 235 }) {
198 const { uninstalled = false } = options 236 const { uninstalled = false } = options
diff --git a/server/models/server/server-blocklist.ts b/server/models/server/server-blocklist.ts
index b88df4fd5..892024c04 100644
--- a/server/models/server/server-blocklist.ts
+++ b/server/models/server/server-blocklist.ts
@@ -2,7 +2,7 @@ import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, Updated
2import { AccountModel } from '../account/account' 2import { AccountModel } from '../account/account'
3import { ServerModel } from './server' 3import { ServerModel } from './server'
4import { ServerBlock } from '../../../shared/models/blocklist' 4import { ServerBlock } from '../../../shared/models/blocklist'
5import { getSort } from '../utils' 5import { getSort, searchAttribute } from '../utils'
6import * as Bluebird from 'bluebird' 6import * as Bluebird from 'bluebird'
7import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/typings/models' 7import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/typings/models'
8import { Op } from 'sequelize' 8import { Op } from 'sequelize'
@@ -81,7 +81,7 @@ export class ServerBlocklistModel extends Model<ServerBlocklistModel> {
81 attributes: [ 'accountId', 'id' ], 81 attributes: [ 'accountId', 'id' ],
82 where: { 82 where: {
83 accountId: { 83 accountId: {
84 [Op.in]: accountIds // FIXME: sequelize ANY seems broken 84 [Op.in]: accountIds
85 }, 85 },
86 targetServerId 86 targetServerId
87 }, 87 },
@@ -120,13 +120,22 @@ export class ServerBlocklistModel extends Model<ServerBlocklistModel> {
120 return ServerBlocklistModel.findOne(query) 120 return ServerBlocklistModel.findOne(query)
121 } 121 }
122 122
123 static listForApi (accountId: number, start: number, count: number, sort: string) { 123 static listForApi (parameters: {
124 start: number
125 count: number
126 sort: string
127 search?: string
128 accountId: number
129 }) {
130 const { start, count, sort, search, accountId } = parameters
131
124 const query = { 132 const query = {
125 offset: start, 133 offset: start,
126 limit: count, 134 limit: count,
127 order: getSort(sort), 135 order: getSort(sort),
128 where: { 136 where: {
129 accountId 137 accountId,
138 ...searchAttribute(search, '$BlockedServer.host$')
130 } 139 }
131 } 140 }
132 141
diff --git a/server/models/server/server.ts b/server/models/server/server.ts
index 8b07115f1..5131257ec 100644
--- a/server/models/server/server.ts
+++ b/server/models/server/server.ts
@@ -71,6 +71,13 @@ export class ServerModel extends Model<ServerModel> {
71 return ServerModel.findOne(query) 71 return ServerModel.findOne(query)
72 } 72 }
73 73
74 static async loadOrCreateByHost (host: string) {
75 let server = await ServerModel.loadByHost(host)
76 if (!server) server = await ServerModel.create({ host })
77
78 return server
79 }
80
74 isBlocked () { 81 isBlocked () {
75 return this.BlockedByAccounts && this.BlockedByAccounts.length !== 0 82 return this.BlockedByAccounts && this.BlockedByAccounts.length !== 0
76 } 83 }
diff --git a/server/models/utils.ts b/server/models/utils.ts
index f89b80011..b2573cd35 100644
--- a/server/models/utils.ts
+++ b/server/models/utils.ts
@@ -1,7 +1,24 @@
1import { Model, Sequelize } from 'sequelize-typescript' 1import { Model, Sequelize } from 'sequelize-typescript'
2import validator from 'validator' 2import validator from 'validator'
3import { Col } from 'sequelize/types/lib/utils' 3import { Col } from 'sequelize/types/lib/utils'
4import { literal, OrderItem } from 'sequelize' 4import { literal, OrderItem, Op } from 'sequelize'
5
6type Primitive = string | Function | number | boolean | Symbol | undefined | null
7type DeepOmitHelper<T, K extends keyof T> = {
8 [P in K]: // extra level of indirection needed to trigger homomorhic behavior
9 T[P] extends infer TP // distribute over unions
10 ? TP extends Primitive
11 ? TP // leave primitives and functions alone
12 : TP extends any[]
13 ? DeepOmitArray<TP, K> // Array special handling
14 : DeepOmit<TP, K>
15 : never
16}
17type DeepOmit<T, K> = T extends Primitive ? T : DeepOmitHelper<T, Exclude<keyof T, K>>
18
19type DeepOmitArray<T extends any[], K> = {
20 [P in keyof T]: DeepOmit<T[P], K>
21}
5 22
6type SortType = { sortModel: string, sortValue: string } 23type SortType = { sortModel: string, sortValue: string }
7 24
@@ -67,7 +84,7 @@ function getVideoSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): Or
67function getBlacklistSort (model: any, value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { 84function getBlacklistSort (model: any, value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] {
68 const [ firstSort ] = getSort(value) 85 const [ firstSort ] = getSort(value)
69 86
70 if (model) return [ [ literal(`"${model}.${firstSort[ 0 ]}" ${firstSort[ 1 ]}`) ], lastSort ] as any[] // FIXME: typings 87 if (model) return [ [ literal(`"${model}.${firstSort[0]}" ${firstSort[1]}`) ], lastSort ] as any[] // FIXME: typings
71 return [ firstSort, lastSort ] 88 return [ firstSort, lastSort ]
72} 89}
73 90
@@ -139,7 +156,7 @@ function buildServerIdsFollowedBy (actorId: any) {
139 'SELECT "actor"."serverId" FROM "actorFollow" ' + 156 'SELECT "actor"."serverId" FROM "actorFollow" ' +
140 'INNER JOIN "actor" ON actor.id = "actorFollow"."targetActorId" ' + 157 'INNER JOIN "actor" ON actor.id = "actorFollow"."targetActorId" ' +
141 'WHERE "actorFollow"."actorId" = ' + actorIdNumber + 158 'WHERE "actorFollow"."actorId" = ' + actorIdNumber +
142 ')' 159 ')'
143} 160}
144 161
145function buildWhereIdOrUUID (id: number | string) { 162function buildWhereIdOrUUID (id: number | string) {
@@ -156,8 +173,11 @@ function parseAggregateResult (result: any) {
156} 173}
157 174
158const createSafeIn = (model: typeof Model, stringArr: (string | number)[]) => { 175const createSafeIn = (model: typeof Model, stringArr: (string | number)[]) => {
159 return stringArr.map(t => model.sequelize.escape('' + t)) 176 return stringArr.map(t => {
160 .join(', ') 177 return t === null
178 ? null
179 : model.sequelize.escape('' + t)
180 }).join(', ')
161} 181}
162 182
163function buildLocalAccountIdsIn () { 183function buildLocalAccountIdsIn () {
@@ -172,9 +192,35 @@ function buildLocalActorIdsIn () {
172 ) 192 )
173} 193}
174 194
195function buildDirectionAndField (value: string) {
196 let field: string
197 let direction: 'ASC' | 'DESC'
198
199 if (value.substring(0, 1) === '-') {
200 direction = 'DESC'
201 field = value.substring(1)
202 } else {
203 direction = 'ASC'
204 field = value
205 }
206
207 return { direction, field }
208}
209
210function searchAttribute (sourceField?: string, targetField?: string) {
211 if (!sourceField) return {}
212
213 return {
214 [targetField]: {
215 [Op.iLike]: `%${sourceField}%`
216 }
217 }
218}
219
175// --------------------------------------------------------------------------- 220// ---------------------------------------------------------------------------
176 221
177export { 222export {
223 DeepOmit,
178 buildBlockedAccountSQL, 224 buildBlockedAccountSQL,
179 buildLocalActorIdsIn, 225 buildLocalActorIdsIn,
180 SortType, 226 SortType,
@@ -191,7 +237,9 @@ export {
191 isOutdated, 237 isOutdated,
192 parseAggregateResult, 238 parseAggregateResult,
193 getFollowsSort, 239 getFollowsSort,
194 createSafeIn 240 buildDirectionAndField,
241 createSafeIn,
242 searchAttribute
195} 243}
196 244
197// --------------------------------------------------------------------------- 245// ---------------------------------------------------------------------------
@@ -203,18 +251,3 @@ function searchTrigramNormalizeValue (value: string) {
203function searchTrigramNormalizeCol (col: string) { 251function searchTrigramNormalizeCol (col: string) {
204 return Sequelize.fn('lower', Sequelize.fn('immutable_unaccent', Sequelize.col(col))) 252 return Sequelize.fn('lower', Sequelize.fn('immutable_unaccent', Sequelize.col(col)))
205} 253}
206
207function buildDirectionAndField (value: string) {
208 let field: string
209 let direction: 'ASC' | 'DESC'
210
211 if (value.substring(0, 1) === '-') {
212 direction = 'DESC'
213 field = value.substring(1)
214 } else {
215 direction = 'ASC'
216 field = value
217 }
218
219 return { direction, field }
220}
diff --git a/server/models/video/thumbnail.ts b/server/models/video/thumbnail.ts
index 3b011b1d2..e396784d2 100644
--- a/server/models/video/thumbnail.ts
+++ b/server/models/video/thumbnail.ts
@@ -19,6 +19,8 @@ import { CONFIG } from '../../initializers/config'
19import { VideoModel } from './video' 19import { VideoModel } from './video'
20import { VideoPlaylistModel } from './video-playlist' 20import { VideoPlaylistModel } from './video-playlist'
21import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' 21import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
22import { MVideoAccountLight } from '@server/typings/models'
23import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub'
22 24
23@Table({ 25@Table({
24 tableName: 'thumbnail', 26 tableName: 'thumbnail',
@@ -90,7 +92,7 @@ export class ThumbnailModel extends Model<ThumbnailModel> {
90 @UpdatedAt 92 @UpdatedAt
91 updatedAt: Date 93 updatedAt: Date
92 94
93 private static types: { [ id in ThumbnailType ]: { label: string, directory: string, staticPath: string } } = { 95 private static readonly types: { [ id in ThumbnailType ]: { label: string, directory: string, staticPath: string } } = {
94 [ThumbnailType.MINIATURE]: { 96 [ThumbnailType.MINIATURE]: {
95 label: 'miniature', 97 label: 'miniature',
96 directory: CONFIG.STORAGE.THUMBNAILS_DIR, 98 directory: CONFIG.STORAGE.THUMBNAILS_DIR,
@@ -126,11 +128,14 @@ export class ThumbnailModel extends Model<ThumbnailModel> {
126 return videoUUID + '.jpg' 128 return videoUUID + '.jpg'
127 } 129 }
128 130
129 getFileUrl (isLocal: boolean) { 131 getFileUrl (video: MVideoAccountLight) {
130 if (isLocal === false) return this.fileUrl 132 const staticPath = ThumbnailModel.types[this.type].staticPath + this.filename
131 133
132 const staticPath = ThumbnailModel.types[this.type].staticPath 134 if (video.isOwned()) return WEBSERVER.URL + staticPath
133 return WEBSERVER.URL + staticPath + this.filename 135 if (this.fileUrl) return this.fileUrl
136
137 // Fallback if we don't have a file URL
138 return buildRemoteVideoBaseUrl(video, staticPath)
134 } 139 }
135 140
136 getPath () { 141 getPath () {
diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts
index 3636db18d..0844f702d 100644
--- a/server/models/video/video-abuse.ts
+++ b/server/models/video/video-abuse.ts
@@ -1,4 +1,21 @@
1import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' 1import * as Bluebird from 'bluebird'
2import { literal, Op } from 'sequelize'
3import {
4 AllowNull,
5 BelongsTo,
6 Column,
7 CreatedAt,
8 DataType,
9 Default,
10 ForeignKey,
11 Is,
12 Model,
13 Scopes,
14 Table,
15 UpdatedAt
16} from 'sequelize-typescript'
17import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type'
18import { VideoAbuseState, VideoDetails } from '../../../shared'
2import { VideoAbuseObject } from '../../../shared/models/activitypub/objects' 19import { VideoAbuseObject } from '../../../shared/models/activitypub/objects'
3import { VideoAbuse } from '../../../shared/models/videos' 20import { VideoAbuse } from '../../../shared/models/videos'
4import { 21import {
@@ -6,15 +23,205 @@ import {
6 isVideoAbuseReasonValid, 23 isVideoAbuseReasonValid,
7 isVideoAbuseStateValid 24 isVideoAbuseStateValid
8} from '../../helpers/custom-validators/video-abuses' 25} from '../../helpers/custom-validators/video-abuses'
9import { AccountModel } from '../account/account'
10import { buildBlockedAccountSQL, getSort, throwIfNotValid } from '../utils'
11import { VideoModel } from './video'
12import { VideoAbuseState } from '../../../shared'
13import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants' 26import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants'
14import { MUserAccountId, MVideoAbuse, MVideoAbuseFormattable, MVideoAbuseVideo } from '../../typings/models' 27import { MUserAccountId, MVideoAbuse, MVideoAbuseFormattable, MVideoAbuseVideo } from '../../typings/models'
15import * as Bluebird from 'bluebird' 28import { AccountModel } from '../account/account'
16import { literal, Op } from 'sequelize' 29import { buildBlockedAccountSQL, getSort, searchAttribute, throwIfNotValid } from '../utils'
30import { ThumbnailModel } from './thumbnail'
31import { VideoModel } from './video'
32import { VideoBlacklistModel } from './video-blacklist'
33import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel'
34
35export enum ScopeNames {
36 FOR_API = 'FOR_API'
37}
38
39@Scopes(() => ({
40 [ScopeNames.FOR_API]: (options: {
41 // search
42 search?: string
43 searchReporter?: string
44 searchReportee?: string
45 searchVideo?: string
46 searchVideoChannel?: string
47
48 // filters
49 id?: number
50
51 state?: VideoAbuseState
52 videoIs?: VideoAbuseVideoIs
53
54 // accountIds
55 serverAccountId: number
56 userAccountId: number
57 }) => {
58 const where = {
59 reporterAccountId: {
60 [Op.notIn]: literal('(' + buildBlockedAccountSQL(options.serverAccountId, options.userAccountId) + ')')
61 }
62 }
63
64 if (options.search) {
65 Object.assign(where, {
66 [Op.or]: [
67 {
68 [Op.and]: [
69 { videoId: { [Op.not]: null } },
70 searchAttribute(options.search, '$Video.name$')
71 ]
72 },
73 {
74 [Op.and]: [
75 { videoId: { [Op.not]: null } },
76 searchAttribute(options.search, '$Video.VideoChannel.name$')
77 ]
78 },
79 {
80 [Op.and]: [
81 { deletedVideo: { [Op.not]: null } },
82 { deletedVideo: searchAttribute(options.search, 'name') }
83 ]
84 },
85 {
86 [Op.and]: [
87 { deletedVideo: { [Op.not]: null } },
88 { deletedVideo: { channel: searchAttribute(options.search, 'displayName') } }
89 ]
90 },
91 searchAttribute(options.search, '$Account.name$')
92 ]
93 })
94 }
17 95
96 if (options.id) Object.assign(where, { id: options.id })
97 if (options.state) Object.assign(where, { state: options.state })
98
99 if (options.videoIs === 'deleted') {
100 Object.assign(where, {
101 deletedVideo: {
102 [Op.not]: null
103 }
104 })
105 }
106
107 const onlyBlacklisted = options.videoIs === 'blacklisted'
108
109 return {
110 attributes: {
111 include: [
112 [
113 // we don't care about this count for deleted videos, so there are not included
114 literal(
115 '(' +
116 'SELECT count(*) ' +
117 'FROM "videoAbuse" ' +
118 'WHERE "videoId" = "VideoAbuseModel"."videoId" ' +
119 ')'
120 ),
121 'countReportsForVideo'
122 ],
123 [
124 // we don't care about this count for deleted videos, so there are not included
125 literal(
126 '(' +
127 'SELECT t.nth ' +
128 'FROM ( ' +
129 'SELECT id, ' +
130 'row_number() OVER (PARTITION BY "videoId" ORDER BY "createdAt") AS nth ' +
131 'FROM "videoAbuse" ' +
132 ') t ' +
133 'WHERE t.id = "VideoAbuseModel".id ' +
134 ')'
135 ),
136 'nthReportForVideo'
137 ],
138 [
139 literal(
140 '(' +
141 'SELECT count("videoAbuse"."id") ' +
142 'FROM "videoAbuse" ' +
143 'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' +
144 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
145 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
146 'WHERE "account"."id" = "VideoAbuseModel"."reporterAccountId" ' +
147 ')'
148 ),
149 'countReportsForReporter__video'
150 ],
151 [
152 literal(
153 '(' +
154 'SELECT count(DISTINCT "videoAbuse"."id") ' +
155 'FROM "videoAbuse" ' +
156 `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "VideoAbuseModel"."reporterAccountId" ` +
157 ')'
158 ),
159 'countReportsForReporter__deletedVideo'
160 ],
161 [
162 literal(
163 '(' +
164 'SELECT count(DISTINCT "videoAbuse"."id") ' +
165 'FROM "videoAbuse" ' +
166 'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' +
167 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
168 'INNER JOIN "account" ON ' +
169 '"videoChannel"."accountId" = "Video->VideoChannel"."accountId" ' +
170 `OR "videoChannel"."accountId" = CAST("VideoAbuseModel"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` +
171 ')'
172 ),
173 'countReportsForReportee__video'
174 ],
175 [
176 literal(
177 '(' +
178 'SELECT count(DISTINCT "videoAbuse"."id") ' +
179 'FROM "videoAbuse" ' +
180 `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "Video->VideoChannel"."accountId" ` +
181 `OR CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = ` +
182 `CAST("VideoAbuseModel"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` +
183 ')'
184 ),
185 'countReportsForReportee__deletedVideo'
186 ]
187 ]
188 },
189 include: [
190 {
191 model: AccountModel,
192 required: true,
193 where: searchAttribute(options.searchReporter, 'name')
194 },
195 {
196 model: VideoModel,
197 required: !!(onlyBlacklisted || options.searchVideo || options.searchReportee || options.searchVideoChannel),
198 where: searchAttribute(options.searchVideo, 'name'),
199 include: [
200 {
201 model: ThumbnailModel
202 },
203 {
204 model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }),
205 where: searchAttribute(options.searchVideoChannel, 'name'),
206 include: [
207 {
208 model: AccountModel,
209 where: searchAttribute(options.searchReportee, 'name')
210 }
211 ]
212 },
213 {
214 attributes: [ 'id', 'reason', 'unfederated' ],
215 model: VideoBlacklistModel,
216 required: onlyBlacklisted
217 }
218 ]
219 }
220 ],
221 where
222 }
223 }
224}))
18@Table({ 225@Table({
19 tableName: 'videoAbuse', 226 tableName: 'videoAbuse',
20 indexes: [ 227 indexes: [
@@ -46,6 +253,11 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
46 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.MODERATION_COMMENT.max)) 253 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.MODERATION_COMMENT.max))
47 moderationComment: string 254 moderationComment: string
48 255
256 @AllowNull(true)
257 @Default(null)
258 @Column(DataType.JSONB)
259 deletedVideo: VideoDetails
260
49 @CreatedAt 261 @CreatedAt
50 createdAt: Date 262 createdAt: Date
51 263
@@ -58,9 +270,9 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
58 270
59 @BelongsTo(() => AccountModel, { 271 @BelongsTo(() => AccountModel, {
60 foreignKey: { 272 foreignKey: {
61 allowNull: false 273 allowNull: true
62 }, 274 },
63 onDelete: 'cascade' 275 onDelete: 'set null'
64 }) 276 })
65 Account: AccountModel 277 Account: AccountModel
66 278
@@ -70,60 +282,103 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
70 282
71 @BelongsTo(() => VideoModel, { 283 @BelongsTo(() => VideoModel, {
72 foreignKey: { 284 foreignKey: {
73 allowNull: false 285 allowNull: true
74 }, 286 },
75 onDelete: 'cascade' 287 onDelete: 'set null'
76 }) 288 })
77 Video: VideoModel 289 Video: VideoModel
78 290
79 static loadByIdAndVideoId (id: number, videoId: number): Bluebird<MVideoAbuse> { 291 static loadByIdAndVideoId (id: number, videoId?: number, uuid?: string): Bluebird<MVideoAbuse> {
292 const videoAttributes = {}
293 if (videoId) videoAttributes['videoId'] = videoId
294 if (uuid) videoAttributes['deletedVideo'] = { uuid }
295
80 const query = { 296 const query = {
81 where: { 297 where: {
82 id, 298 id,
83 videoId 299 ...videoAttributes
84 } 300 }
85 } 301 }
86 return VideoAbuseModel.findOne(query) 302 return VideoAbuseModel.findOne(query)
87 } 303 }
88 304
89 static listForApi (parameters: { 305 static listForApi (parameters: {
90 start: number, 306 start: number
91 count: number, 307 count: number
92 sort: string, 308 sort: string
309
93 serverAccountId: number 310 serverAccountId: number
94 user?: MUserAccountId 311 user?: MUserAccountId
312
313 id?: number
314 state?: VideoAbuseState
315 videoIs?: VideoAbuseVideoIs
316
317 search?: string
318 searchReporter?: string
319 searchReportee?: string
320 searchVideo?: string
321 searchVideoChannel?: string
95 }) { 322 }) {
96 const { start, count, sort, user, serverAccountId } = parameters 323 const {
324 start,
325 count,
326 sort,
327 search,
328 user,
329 serverAccountId,
330 state,
331 videoIs,
332 searchReportee,
333 searchVideo,
334 searchVideoChannel,
335 searchReporter,
336 id
337 } = parameters
338
97 const userAccountId = user ? user.Account.id : undefined 339 const userAccountId = user ? user.Account.id : undefined
98 340
99 const query = { 341 const query = {
100 offset: start, 342 offset: start,
101 limit: count, 343 limit: count,
102 order: getSort(sort), 344 order: getSort(sort),
103 where: { 345 col: 'VideoAbuseModel.id',
104 reporterAccountId: { 346 distinct: true
105 [Op.notIn]: literal('(' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')')
106 }
107 },
108 include: [
109 {
110 model: AccountModel,
111 required: true
112 },
113 {
114 model: VideoModel,
115 required: true
116 }
117 ]
118 } 347 }
119 348
120 return VideoAbuseModel.findAndCountAll(query) 349 const filters = {
350 id,
351 search,
352 state,
353 videoIs,
354 searchReportee,
355 searchVideo,
356 searchVideoChannel,
357 searchReporter,
358 serverAccountId,
359 userAccountId
360 }
361
362 return VideoAbuseModel
363 .scope({ method: [ ScopeNames.FOR_API, filters ] })
364 .findAndCountAll(query)
121 .then(({ rows, count }) => { 365 .then(({ rows, count }) => {
122 return { total: count, data: rows } 366 return { total: count, data: rows }
123 }) 367 })
124 } 368 }
125 369
126 toFormattedJSON (this: MVideoAbuseFormattable): VideoAbuse { 370 toFormattedJSON (this: MVideoAbuseFormattable): VideoAbuse {
371 const countReportsForVideo = this.get('countReportsForVideo') as number
372 const nthReportForVideo = this.get('nthReportForVideo') as number
373 const countReportsForReporterVideo = this.get('countReportsForReporter__video') as number
374 const countReportsForReporterDeletedVideo = this.get('countReportsForReporter__deletedVideo') as number
375 const countReportsForReporteeVideo = this.get('countReportsForReportee__video') as number
376 const countReportsForReporteeDeletedVideo = this.get('countReportsForReportee__deletedVideo') as number
377
378 const video = this.Video
379 ? this.Video
380 : this.deletedVideo
381
127 return { 382 return {
128 id: this.id, 383 id: this.id,
129 reason: this.reason, 384 reason: this.reason,
@@ -134,11 +389,21 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
134 }, 389 },
135 moderationComment: this.moderationComment, 390 moderationComment: this.moderationComment,
136 video: { 391 video: {
137 id: this.Video.id, 392 id: video.id,
138 uuid: this.Video.uuid, 393 uuid: video.uuid,
139 name: this.Video.name 394 name: video.name,
395 nsfw: video.nsfw,
396 deleted: !this.Video,
397 blacklisted: this.Video && this.Video.isBlacklisted(),
398 thumbnailPath: this.Video?.getMiniatureStaticPath(),
399 channel: this.Video?.VideoChannel.toFormattedJSON() || this.deletedVideo?.channel
140 }, 400 },
141 createdAt: this.createdAt 401 createdAt: this.createdAt,
402 updatedAt: this.updatedAt,
403 count: countReportsForVideo || 0,
404 nth: nthReportForVideo || 0,
405 countReportsForReporter: (countReportsForReporterVideo || 0) + (countReportsForReporterDeletedVideo || 0),
406 countReportsForReportee: (countReportsForReporteeVideo || 0) + (countReportsForReporteeDeletedVideo || 0)
142 } 407 }
143 } 408 }
144 409
diff --git a/server/models/video/video-blacklist.ts b/server/models/video/video-blacklist.ts
index 694983cb3..8cbfe362e 100644
--- a/server/models/video/video-blacklist.ts
+++ b/server/models/video/video-blacklist.ts
@@ -1,5 +1,5 @@
1import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' 1import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
2import { getBlacklistSort, SortType, throwIfNotValid } from '../utils' 2import { getBlacklistSort, SortType, throwIfNotValid, searchAttribute } from '../utils'
3import { VideoModel } from './video' 3import { VideoModel } from './video'
4import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel' 4import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel'
5import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../helpers/custom-validators/video-blacklist' 5import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../helpers/custom-validators/video-blacklist'
@@ -54,7 +54,15 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> {
54 }) 54 })
55 Video: VideoModel 55 Video: VideoModel
56 56
57 static listForApi (start: number, count: number, sort: SortType, type?: VideoBlacklistType) { 57 static listForApi (parameters: {
58 start: number
59 count: number
60 sort: SortType
61 search?: string
62 type?: VideoBlacklistType
63 }) {
64 const { start, count, sort, search, type } = parameters
65
58 function buildBaseQuery (): FindOptions { 66 function buildBaseQuery (): FindOptions {
59 return { 67 return {
60 offset: start, 68 offset: start,
@@ -70,6 +78,7 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> {
70 { 78 {
71 model: VideoModel, 79 model: VideoModel,
72 required: true, 80 required: true,
81 where: searchAttribute(search, 'name'),
73 include: [ 82 include: [
74 { 83 {
75 model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }), 84 model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }),
diff --git a/server/models/video/video-caption.ts b/server/models/video/video-caption.ts
index eeb2a4afd..59d3e1050 100644
--- a/server/models/video/video-caption.ts
+++ b/server/models/video/video-caption.ts
@@ -5,6 +5,7 @@ import {
5 BelongsTo, 5 BelongsTo,
6 Column, 6 Column,
7 CreatedAt, 7 CreatedAt,
8 DataType,
8 ForeignKey, 9 ForeignKey,
9 Is, 10 Is,
10 Model, 11 Model,
@@ -16,13 +17,14 @@ import { buildWhereIdOrUUID, throwIfNotValid } from '../utils'
16import { VideoModel } from './video' 17import { VideoModel } from './video'
17import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions' 18import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions'
18import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model' 19import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model'
19import { LAZY_STATIC_PATHS, VIDEO_LANGUAGES } from '../../initializers/constants' 20import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, VIDEO_LANGUAGES, WEBSERVER } from '../../initializers/constants'
20import { join } from 'path' 21import { join } from 'path'
21import { logger } from '../../helpers/logger' 22import { logger } from '../../helpers/logger'
22import { remove } from 'fs-extra' 23import { remove } from 'fs-extra'
23import { CONFIG } from '../../initializers/config' 24import { CONFIG } from '../../initializers/config'
24import * as Bluebird from 'bluebird' 25import * as Bluebird from 'bluebird'
25import { MVideoCaptionFormattable, MVideoCaptionVideo } from '@server/typings/models' 26import { MVideoAccountLight, MVideoCaptionFormattable, MVideoCaptionVideo } from '@server/typings/models'
27import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub'
26 28
27export enum ScopeNames { 29export enum ScopeNames {
28 WITH_VIDEO_UUID_AND_REMOTE = 'WITH_VIDEO_UUID_AND_REMOTE' 30 WITH_VIDEO_UUID_AND_REMOTE = 'WITH_VIDEO_UUID_AND_REMOTE'
@@ -64,6 +66,10 @@ export class VideoCaptionModel extends Model<VideoCaptionModel> {
64 @Column 66 @Column
65 language: string 67 language: string
66 68
69 @AllowNull(true)
70 @Column(DataType.STRING(CONSTRAINTS_FIELDS.COMMONS.URL.max))
71 fileUrl: string
72
67 @ForeignKey(() => VideoModel) 73 @ForeignKey(() => VideoModel)
68 @Column 74 @Column
69 videoId: number 75 videoId: number
@@ -114,13 +120,14 @@ export class VideoCaptionModel extends Model<VideoCaptionModel> {
114 return VideoCaptionModel.findOne(query) 120 return VideoCaptionModel.findOne(query)
115 } 121 }
116 122
117 static insertOrReplaceLanguage (videoId: number, language: string, transaction: Transaction) { 123 static insertOrReplaceLanguage (videoId: number, language: string, fileUrl: string, transaction: Transaction) {
118 const values = { 124 const values = {
119 videoId, 125 videoId,
120 language 126 language,
127 fileUrl
121 } 128 }
122 129
123 return (VideoCaptionModel.upsert<VideoCaptionModel>(values, { transaction, returning: true }) as any) // FIXME: typings 130 return VideoCaptionModel.upsert(values, { transaction, returning: true })
124 .then(([ caption ]) => caption) 131 .then(([ caption ]) => caption)
125 } 132 }
126 133
@@ -175,4 +182,14 @@ export class VideoCaptionModel extends Model<VideoCaptionModel> {
175 removeCaptionFile (this: MVideoCaptionFormattable) { 182 removeCaptionFile (this: MVideoCaptionFormattable) {
176 return remove(CONFIG.STORAGE.CAPTIONS_DIR + this.getCaptionName()) 183 return remove(CONFIG.STORAGE.CAPTIONS_DIR + this.getCaptionName())
177 } 184 }
185
186 getFileUrl (video: MVideoAccountLight) {
187 if (!this.Video) this.Video = video as VideoModel
188
189 if (video.isOwned()) return WEBSERVER.URL + this.getCaptionStaticPath()
190 if (this.fileUrl) return this.fileUrl
191
192 // Fallback if we don't have a file URL
193 return buildRemoteVideoBaseUrl(video, this.getCaptionStaticPath())
194 }
178} 195}
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts
index e10adcb3a..642e129ff 100644
--- a/server/models/video/video-channel.ts
+++ b/server/models/video/video-channel.ts
@@ -30,7 +30,7 @@ import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttr
30import { VideoModel } from './video' 30import { VideoModel } from './video'
31import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' 31import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
32import { ServerModel } from '../server/server' 32import { ServerModel } from '../server/server'
33import { FindOptions, ModelIndexesOptions, Op } from 'sequelize' 33import { FindOptions, Op, literal, ScopeOptions } from 'sequelize'
34import { AvatarModel } from '../avatar/avatar' 34import { AvatarModel } from '../avatar/avatar'
35import { VideoPlaylistModel } from './video-playlist' 35import { VideoPlaylistModel } from './video-playlist'
36import * as Bluebird from 'bluebird' 36import * as Bluebird from 'bluebird'
@@ -43,30 +43,23 @@ import {
43 MChannelSummaryFormattable 43 MChannelSummaryFormattable
44} from '../../typings/models/video' 44} from '../../typings/models/video'
45 45
46// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
47const indexes: ModelIndexesOptions[] = [
48 buildTrigramSearchIndex('video_channel_name_trigram', 'name'),
49
50 {
51 fields: [ 'accountId' ]
52 },
53 {
54 fields: [ 'actorId' ]
55 }
56]
57
58export enum ScopeNames { 46export enum ScopeNames {
59 FOR_API = 'FOR_API', 47 FOR_API = 'FOR_API',
48 SUMMARY = 'SUMMARY',
60 WITH_ACCOUNT = 'WITH_ACCOUNT', 49 WITH_ACCOUNT = 'WITH_ACCOUNT',
61 WITH_ACTOR = 'WITH_ACTOR', 50 WITH_ACTOR = 'WITH_ACTOR',
62 WITH_VIDEOS = 'WITH_VIDEOS', 51 WITH_VIDEOS = 'WITH_VIDEOS',
63 SUMMARY = 'SUMMARY' 52 WITH_STATS = 'WITH_STATS'
64} 53}
65 54
66type AvailableForListOptions = { 55type AvailableForListOptions = {
67 actorId: number 56 actorId: number
68} 57}
69 58
59type AvailableWithStatsOptions = {
60 daysPrior: number
61}
62
70export type SummaryOptions = { 63export type SummaryOptions = {
71 withAccount?: boolean // Default: false 64 withAccount?: boolean // Default: false
72 withAccountBlockerIds?: number[] 65 withAccountBlockerIds?: number[]
@@ -81,40 +74,6 @@ export type SummaryOptions = {
81 ] 74 ]
82})) 75}))
83@Scopes(() => ({ 76@Scopes(() => ({
84 [ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => {
85 const base: FindOptions = {
86 attributes: [ 'id', 'name', 'description', 'actorId' ],
87 include: [
88 {
89 attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ],
90 model: ActorModel.unscoped(),
91 required: true,
92 include: [
93 {
94 attributes: [ 'host' ],
95 model: ServerModel.unscoped(),
96 required: false
97 },
98 {
99 model: AvatarModel.unscoped(),
100 required: false
101 }
102 ]
103 }
104 ]
105 }
106
107 if (options.withAccount === true) {
108 base.include.push({
109 model: AccountModel.scope({
110 method: [ AccountModelScopeNames.SUMMARY, { withAccountBlockerIds: options.withAccountBlockerIds } as AccountSummaryOptions ]
111 }),
112 required: true
113 })
114 }
115
116 return base
117 },
118 [ScopeNames.FOR_API]: (options: AvailableForListOptions) => { 77 [ScopeNames.FOR_API]: (options: AvailableForListOptions) => {
119 // Only list local channels OR channels that are on an instance followed by actorId 78 // Only list local channels OR channels that are on an instance followed by actorId
120 const inQueryInstanceFollow = buildServerIdsFollowedBy(options.actorId) 79 const inQueryInstanceFollow = buildServerIdsFollowedBy(options.actorId)
@@ -133,7 +92,7 @@ export type SummaryOptions = {
133 }, 92 },
134 { 93 {
135 serverId: { 94 serverId: {
136 [ Op.in ]: Sequelize.literal(inQueryInstanceFollow) 95 [Op.in]: Sequelize.literal(inQueryInstanceFollow)
137 } 96 }
138 } 97 }
139 ] 98 ]
@@ -155,6 +114,40 @@ export type SummaryOptions = {
155 ] 114 ]
156 } 115 }
157 }, 116 },
117 [ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => {
118 const base: FindOptions = {
119 attributes: [ 'id', 'name', 'description', 'actorId' ],
120 include: [
121 {
122 attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ],
123 model: ActorModel.unscoped(),
124 required: true,
125 include: [
126 {
127 attributes: [ 'host' ],
128 model: ServerModel.unscoped(),
129 required: false
130 },
131 {
132 model: AvatarModel.unscoped(),
133 required: false
134 }
135 ]
136 }
137 ]
138 }
139
140 if (options.withAccount === true) {
141 base.include.push({
142 model: AccountModel.scope({
143 method: [ AccountModelScopeNames.SUMMARY, { withAccountBlockerIds: options.withAccountBlockerIds } as AccountSummaryOptions ]
144 }),
145 required: true
146 })
147 }
148
149 return base
150 },
158 [ScopeNames.WITH_ACCOUNT]: { 151 [ScopeNames.WITH_ACCOUNT]: {
159 include: [ 152 include: [
160 { 153 {
@@ -163,20 +156,66 @@ export type SummaryOptions = {
163 } 156 }
164 ] 157 ]
165 }, 158 },
166 [ScopeNames.WITH_VIDEOS]: { 159 [ScopeNames.WITH_ACTOR]: {
167 include: [ 160 include: [
168 VideoModel 161 ActorModel
169 ] 162 ]
170 }, 163 },
171 [ScopeNames.WITH_ACTOR]: { 164 [ScopeNames.WITH_VIDEOS]: {
172 include: [ 165 include: [
173 ActorModel 166 VideoModel
174 ] 167 ]
168 },
169 [ScopeNames.WITH_STATS]: (options: AvailableWithStatsOptions = { daysPrior: 30 }) => {
170 const daysPrior = parseInt(options.daysPrior + '', 10)
171
172 return {
173 attributes: {
174 include: [
175 [
176 literal(
177 '(' +
178 `SELECT string_agg(concat_ws('|', t.day, t.views), ',') ` +
179 'FROM ( ' +
180 'WITH ' +
181 'days AS ( ' +
182 `SELECT generate_series(date_trunc('day', now()) - '${daysPrior} day'::interval, ` +
183 `date_trunc('day', now()), '1 day'::interval) AS day ` +
184 '), ' +
185 'views AS ( ' +
186 'SELECT v.* ' +
187 'FROM "videoView" AS v ' +
188 'INNER JOIN "video" ON "video"."id" = v."videoId" ' +
189 'WHERE "video"."channelId" = "VideoChannelModel"."id" ' +
190 ') ' +
191 'SELECT days.day AS day, ' +
192 'COALESCE(SUM(views.views), 0) AS views ' +
193 'FROM days ' +
194 `LEFT JOIN views ON date_trunc('day', "views"."startDate") = date_trunc('day', days.day) ` +
195 'GROUP BY day ' +
196 'ORDER BY day ' +
197 ') t' +
198 ')'
199 ),
200 'viewsPerDay'
201 ]
202 ]
203 }
204 }
175 } 205 }
176})) 206}))
177@Table({ 207@Table({
178 tableName: 'videoChannel', 208 tableName: 'videoChannel',
179 indexes 209 indexes: [
210 buildTrigramSearchIndex('video_channel_name_trigram', 'name'),
211
212 {
213 fields: [ 'accountId' ]
214 },
215 {
216 fields: [ 'actorId' ]
217 }
218 ]
180}) 219})
181export class VideoChannelModel extends Model<VideoChannelModel> { 220export class VideoChannelModel extends Model<VideoChannelModel> {
182 221
@@ -351,10 +390,11 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
351 } 390 }
352 391
353 static listByAccount (options: { 392 static listByAccount (options: {
354 accountId: number, 393 accountId: number
355 start: number, 394 start: number
356 count: number, 395 count: number
357 sort: string 396 sort: string
397 withStats?: boolean
358 }) { 398 }) {
359 const query = { 399 const query = {
360 offset: options.start, 400 offset: options.start,
@@ -371,7 +411,16 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
371 ] 411 ]
372 } 412 }
373 413
414 const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR ]
415
416 if (options.withStats) {
417 scopes.push({
418 method: [ ScopeNames.WITH_STATS, { daysPrior: 30 } as AvailableWithStatsOptions ]
419 })
420 }
421
374 return VideoChannelModel 422 return VideoChannelModel
423 .scope(scopes)
375 .findAndCountAll(query) 424 .findAndCountAll(query)
376 .then(({ rows, count }) => { 425 .then(({ rows, count }) => {
377 return { total: count, data: rows } 426 return { total: count, data: rows }
@@ -499,6 +548,8 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
499 } 548 }
500 549
501 toFormattedJSON (this: MChannelFormattable): VideoChannel { 550 toFormattedJSON (this: MChannelFormattable): VideoChannel {
551 const viewsPerDay = this.get('viewsPerDay') as string
552
502 const actor = this.Actor.toFormattedJSON() 553 const actor = this.Actor.toFormattedJSON()
503 const videoChannel = { 554 const videoChannel = {
504 id: this.id, 555 id: this.id,
@@ -508,7 +559,16 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
508 isLocal: this.Actor.isOwned(), 559 isLocal: this.Actor.isOwned(),
509 createdAt: this.createdAt, 560 createdAt: this.createdAt,
510 updatedAt: this.updatedAt, 561 updatedAt: this.updatedAt,
511 ownerAccount: undefined 562 ownerAccount: undefined,
563 viewsPerDay: viewsPerDay !== undefined
564 ? viewsPerDay.split(',').map(v => {
565 const o = v.split('|')
566 return {
567 date: new Date(o[0]),
568 views: +o[1]
569 }
570 })
571 : undefined
512 } 572 }
513 573
514 if (this.Account) videoChannel.ownerAccount = this.Account.toFormattedJSON() 574 if (this.Account) videoChannel.ownerAccount = this.Account.toFormattedJSON()
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts
index fb4d16b4d..6d60271e6 100644
--- a/server/models/video/video-comment.ts
+++ b/server/models/video/video-comment.ts
@@ -9,7 +9,6 @@ import { ActorModel } from '../activitypub/actor'
9import { buildBlockedAccountSQL, buildLocalAccountIdsIn, getCommentSort, throwIfNotValid } from '../utils' 9import { buildBlockedAccountSQL, buildLocalAccountIdsIn, getCommentSort, throwIfNotValid } from '../utils'
10import { VideoModel } from './video' 10import { VideoModel } from './video'
11import { VideoChannelModel } from './video-channel' 11import { VideoChannelModel } from './video-channel'
12import { getServerActor } from '../../helpers/utils'
13import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor' 12import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor'
14import { regexpCapture } from '../../helpers/regexp' 13import { regexpCapture } from '../../helpers/regexp'
15import { uniq } from 'lodash' 14import { uniq } from 'lodash'
@@ -27,6 +26,8 @@ import {
27 MCommentOwnerVideoReply 26 MCommentOwnerVideoReply
28} from '../../typings/models/video' 27} from '../../typings/models/video'
29import { MUserAccountId } from '@server/typings/models' 28import { MUserAccountId } from '@server/typings/models'
29import { VideoPrivacy } from '@shared/models'
30import { getServerActor } from '@server/models/application/application'
30 31
31enum ScopeNames { 32enum ScopeNames {
32 WITH_ACCOUNT = 'WITH_ACCOUNT', 33 WITH_ACCOUNT = 'WITH_ACCOUNT',
@@ -257,10 +258,10 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
257 } 258 }
258 259
259 static async listThreadsForApi (parameters: { 260 static async listThreadsForApi (parameters: {
260 videoId: number, 261 videoId: number
261 start: number, 262 start: number
262 count: number, 263 count: number
263 sort: string, 264 sort: string
264 user?: MUserAccountId 265 user?: MUserAccountId
265 }) { 266 }) {
266 const { videoId, start, count, sort, user } = parameters 267 const { videoId, start, count, sort, user } = parameters
@@ -300,8 +301,8 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
300 } 301 }
301 302
302 static async listThreadCommentsForApi (parameters: { 303 static async listThreadCommentsForApi (parameters: {
303 videoId: number, 304 videoId: number
304 threadId: number, 305 threadId: number
305 user?: MUserAccountId 306 user?: MUserAccountId
306 }) { 307 }) {
307 const { videoId, threadId, user } = parameters 308 const { videoId, threadId, user } = parameters
@@ -314,7 +315,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
314 order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ] as Order, 315 order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ] as Order,
315 where: { 316 where: {
316 videoId, 317 videoId,
317 [ Op.or ]: [ 318 [Op.or]: [
318 { id: threadId }, 319 { id: threadId },
319 { originCommentId: threadId } 320 { originCommentId: threadId }
320 ], 321 ],
@@ -346,7 +347,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
346 order: [ [ 'createdAt', order ] ] as Order, 347 order: [ [ 'createdAt', order ] ] as Order,
347 where: { 348 where: {
348 id: { 349 id: {
349 [ Op.in ]: Sequelize.literal('(' + 350 [Op.in]: Sequelize.literal('(' +
350 'WITH RECURSIVE children (id, "inReplyToCommentId") AS ( ' + 351 'WITH RECURSIVE children (id, "inReplyToCommentId") AS ( ' +
351 `SELECT id, "inReplyToCommentId" FROM "videoComment" WHERE id = ${comment.id} ` + 352 `SELECT id, "inReplyToCommentId" FROM "videoComment" WHERE id = ${comment.id} ` +
352 'UNION ' + 353 'UNION ' +
@@ -355,7 +356,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
355 ') ' + 356 ') ' +
356 'SELECT id FROM children' + 357 'SELECT id FROM children' +
357 ')'), 358 ')'),
358 [ Op.ne ]: comment.id 359 [Op.ne]: comment.id
359 } 360 }
360 }, 361 },
361 transaction: t 362 transaction: t
@@ -380,17 +381,29 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
380 return VideoCommentModel.findAndCountAll<MComment>(query) 381 return VideoCommentModel.findAndCountAll<MComment>(query)
381 } 382 }
382 383
383 static listForFeed (start: number, count: number, videoId?: number): Bluebird<MCommentOwnerVideoFeed[]> { 384 static async listForFeed (start: number, count: number, videoId?: number): Promise<MCommentOwnerVideoFeed[]> {
385 const serverActor = await getServerActor()
386
384 const query = { 387 const query = {
385 order: [ [ 'createdAt', 'DESC' ] ] as Order, 388 order: [ [ 'createdAt', 'DESC' ] ] as Order,
386 offset: start, 389 offset: start,
387 limit: count, 390 limit: count,
388 where: {}, 391 where: {
392 deletedAt: null,
393 accountId: {
394 [Op.notIn]: Sequelize.literal(
395 '(' + buildBlockedAccountSQL(serverActor.Account.id) + ')'
396 )
397 }
398 },
389 include: [ 399 include: [
390 { 400 {
391 attributes: [ 'name', 'uuid' ], 401 attributes: [ 'name', 'uuid' ],
392 model: VideoModel.unscoped(), 402 model: VideoModel.unscoped(),
393 required: true 403 required: true,
404 where: {
405 privacy: VideoPrivacy.PUBLIC
406 }
394 } 407 }
395 ] 408 ]
396 } 409 }
@@ -461,7 +474,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
461 } 474 }
462 475
463 isDeleted () { 476 isDeleted () {
464 return null !== this.deletedAt 477 return this.deletedAt !== null
465 } 478 }
466 479
467 extractMentions () { 480 extractMentions () {
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts
index e08999385..201f0c0f1 100644
--- a/server/models/video/video-file.ts
+++ b/server/models/video/video-file.ts
@@ -10,7 +10,9 @@ import {
10 Is, 10 Is,
11 Model, 11 Model,
12 Table, 12 Table,
13 UpdatedAt 13 UpdatedAt,
14 Scopes,
15 DefaultScope
14} from 'sequelize-typescript' 16} from 'sequelize-typescript'
15import { 17import {
16 isVideoFileExtnameValid, 18 isVideoFileExtnameValid,
@@ -28,7 +30,33 @@ import { MIMETYPES, MEMOIZE_LENGTH, MEMOIZE_TTL } from '../../initializers/const
28import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../typings/models/video/video-file' 30import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../typings/models/video/video-file'
29import { MStreamingPlaylistVideo, MVideo } from '@server/typings/models' 31import { MStreamingPlaylistVideo, MVideo } from '@server/typings/models'
30import * as memoizee from 'memoizee' 32import * as memoizee from 'memoizee'
33import validator from 'validator'
31 34
35export enum ScopeNames {
36 WITH_VIDEO = 'WITH_VIDEO',
37 WITH_METADATA = 'WITH_METADATA'
38}
39
40@DefaultScope(() => ({
41 attributes: {
42 exclude: [ 'metadata' ]
43 }
44}))
45@Scopes(() => ({
46 [ScopeNames.WITH_VIDEO]: {
47 include: [
48 {
49 model: VideoModel.unscoped(),
50 required: true
51 }
52 ]
53 },
54 [ScopeNames.WITH_METADATA]: {
55 attributes: {
56 include: [ 'metadata' ]
57 }
58 }
59}))
32@Table({ 60@Table({
33 tableName: 'videoFile', 61 tableName: 'videoFile',
34 indexes: [ 62 indexes: [
@@ -106,6 +134,14 @@ export class VideoFileModel extends Model<VideoFileModel> {
106 @Column 134 @Column
107 fps: number 135 fps: number
108 136
137 @AllowNull(true)
138 @Column(DataType.JSONB)
139 metadata: any
140
141 @AllowNull(true)
142 @Column
143 metadataUrl: string
144
109 @ForeignKey(() => VideoModel) 145 @ForeignKey(() => VideoModel)
110 @Column 146 @Column
111 videoId: number 147 videoId: number
@@ -157,17 +193,56 @@ export class VideoFileModel extends Model<VideoFileModel> {
157 .then(results => results.length === 1) 193 .then(results => results.length === 1)
158 } 194 }
159 195
196 static async doesVideoExistForVideoFile (id: number, videoIdOrUUID: number | string) {
197 const videoFile = await VideoFileModel.loadWithVideoOrPlaylist(id, videoIdOrUUID)
198
199 return !!videoFile
200 }
201
202 static loadWithMetadata (id: number) {
203 return VideoFileModel.scope(ScopeNames.WITH_METADATA).findByPk(id)
204 }
205
160 static loadWithVideo (id: number) { 206 static loadWithVideo (id: number) {
207 return VideoFileModel.scope(ScopeNames.WITH_VIDEO).findByPk(id)
208 }
209
210 static loadWithVideoOrPlaylist (id: number, videoIdOrUUID: number | string) {
211 const whereVideo = validator.isUUID(videoIdOrUUID + '')
212 ? { uuid: videoIdOrUUID }
213 : { id: videoIdOrUUID }
214
161 const options = { 215 const options = {
216 where: {
217 id
218 },
162 include: [ 219 include: [
163 { 220 {
164 model: VideoModel.unscoped(), 221 model: VideoModel.unscoped(),
165 required: true 222 required: false,
223 where: whereVideo
224 },
225 {
226 model: VideoStreamingPlaylistModel.unscoped(),
227 required: false,
228 include: [
229 {
230 model: VideoModel.unscoped(),
231 required: true,
232 where: whereVideo
233 }
234 ]
166 } 235 }
167 ] 236 ]
168 } 237 }
169 238
170 return VideoFileModel.findByPk(id, options) 239 return VideoFileModel.findOne(options)
240 .then(file => {
241 // We used `required: false` so check we have at least a video or a streaming playlist
242 if (!file.Video && !file.VideoStreamingPlaylist) return null
243
244 return file
245 })
171 } 246 }
172 247
173 static listByStreamingPlaylist (streamingPlaylistId: number, transaction: Transaction) { 248 static listByStreamingPlaylist (streamingPlaylistId: number, transaction: Transaction) {
diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts
index 67395e5c0..d71a3a5db 100644
--- a/server/models/video/video-format-utils.ts
+++ b/server/models/video/video-format-utils.ts
@@ -8,7 +8,7 @@ import {
8 getVideoDislikesActivityPubUrl, 8 getVideoDislikesActivityPubUrl,
9 getVideoLikesActivityPubUrl, 9 getVideoLikesActivityPubUrl,
10 getVideoSharesActivityPubUrl 10 getVideoSharesActivityPubUrl
11} from '../../lib/activitypub' 11} from '../../lib/activitypub/url'
12import { isArray } from '../../helpers/custom-validators/misc' 12import { isArray } from '../../helpers/custom-validators/misc'
13import { VideoStreamingPlaylist } from '../../../shared/models/videos/video-streaming-playlist.model' 13import { VideoStreamingPlaylist } from '../../../shared/models/videos/video-streaming-playlist.model'
14import { 14import {
@@ -23,16 +23,18 @@ import {
23import { MVideoFileRedundanciesOpt } from '../../typings/models/video/video-file' 23import { MVideoFileRedundanciesOpt } from '../../typings/models/video/video-file'
24import { VideoFile } from '@shared/models/videos/video-file.model' 24import { VideoFile } from '@shared/models/videos/video-file.model'
25import { generateMagnetUri } from '@server/helpers/webtorrent' 25import { generateMagnetUri } from '@server/helpers/webtorrent'
26import { extractVideo } from '@server/helpers/video'
26 27
27export type VideoFormattingJSONOptions = { 28export type VideoFormattingJSONOptions = {
28 completeDescription?: boolean 29 completeDescription?: boolean
29 additionalAttributes: { 30 additionalAttributes: {
30 state?: boolean, 31 state?: boolean
31 waitTranscoding?: boolean, 32 waitTranscoding?: boolean
32 scheduledUpdate?: boolean, 33 scheduledUpdate?: boolean
33 blacklistInfo?: boolean 34 blacklistInfo?: boolean
34 } 35 }
35} 36}
37
36function videoModelToFormattedJSON (video: MVideoFormattable, options?: VideoFormattingJSONOptions): Video { 38function videoModelToFormattedJSON (video: MVideoFormattable, options?: VideoFormattingJSONOptions): Video {
37 const userHistory = isArray(video.UserVideoHistories) ? video.UserVideoHistories[0] : undefined 39 const userHistory = isArray(video.UserVideoHistories) ? video.UserVideoHistories[0] : undefined
38 40
@@ -179,14 +181,14 @@ function videoFilesModelToFormattedJSON (
179 baseUrlWs: string, 181 baseUrlWs: string,
180 videoFiles: MVideoFileRedundanciesOpt[] 182 videoFiles: MVideoFileRedundanciesOpt[]
181): VideoFile[] { 183): VideoFile[] {
184 const video = extractVideo(model)
185
182 return videoFiles 186 return videoFiles
183 .map(videoFile => { 187 .map(videoFile => {
184 let resolutionLabel = videoFile.resolution + 'p'
185
186 return { 188 return {
187 resolution: { 189 resolution: {
188 id: videoFile.resolution, 190 id: videoFile.resolution,
189 label: resolutionLabel 191 label: videoFile.resolution + 'p'
190 }, 192 },
191 magnetUri: generateMagnetUri(model, videoFile, baseUrlHttp, baseUrlWs), 193 magnetUri: generateMagnetUri(model, videoFile, baseUrlHttp, baseUrlWs),
192 size: videoFile.size, 194 size: videoFile.size,
@@ -194,7 +196,8 @@ function videoFilesModelToFormattedJSON (
194 torrentUrl: model.getTorrentUrl(videoFile, baseUrlHttp), 196 torrentUrl: model.getTorrentUrl(videoFile, baseUrlHttp),
195 torrentDownloadUrl: model.getTorrentDownloadUrl(videoFile, baseUrlHttp), 197 torrentDownloadUrl: model.getTorrentDownloadUrl(videoFile, baseUrlHttp),
196 fileUrl: model.getVideoFileUrl(videoFile, baseUrlHttp), 198 fileUrl: model.getVideoFileUrl(videoFile, baseUrlHttp),
197 fileDownloadUrl: model.getVideoFileDownloadUrl(videoFile, baseUrlHttp) 199 fileDownloadUrl: model.getVideoFileDownloadUrl(videoFile, baseUrlHttp),
200 metadataUrl: video.getVideoFileMetadataUrl(videoFile, baseUrlHttp)
198 } as VideoFile 201 } as VideoFile
199 }) 202 })
200 .sort((a, b) => { 203 .sort((a, b) => {
@@ -214,7 +217,7 @@ function addVideoFilesInAPAcc (
214 for (const file of files) { 217 for (const file of files) {
215 acc.push({ 218 acc.push({
216 type: 'Link', 219 type: 'Link',
217 mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[ file.extname ] as any, 220 mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[file.extname] as any,
218 href: model.getVideoFileUrl(file, baseUrlHttp), 221 href: model.getVideoFileUrl(file, baseUrlHttp),
219 height: file.resolution, 222 height: file.resolution,
220 size: file.size, 223 size: file.size,
@@ -223,6 +226,15 @@ function addVideoFilesInAPAcc (
223 226
224 acc.push({ 227 acc.push({
225 type: 'Link', 228 type: 'Link',
229 rel: [ 'metadata', MIMETYPES.VIDEO.EXT_MIMETYPE[file.extname] ],
230 mediaType: 'application/json' as 'application/json',
231 href: extractVideo(model).getVideoFileMetadataUrl(file, baseUrlHttp),
232 height: file.resolution,
233 fps: file.fps
234 })
235
236 acc.push({
237 type: 'Link',
226 mediaType: 'application/x-bittorrent' as 'application/x-bittorrent', 238 mediaType: 'application/x-bittorrent' as 'application/x-bittorrent',
227 href: model.getTorrentUrl(file, baseUrlHttp), 239 href: model.getTorrentUrl(file, baseUrlHttp),
228 height: file.resolution 240 height: file.resolution
@@ -282,10 +294,8 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoTorrentObject {
282 addVideoFilesInAPAcc(url, video, baseUrlHttp, baseUrlWs, video.VideoFiles || []) 294 addVideoFilesInAPAcc(url, video, baseUrlHttp, baseUrlWs, video.VideoFiles || [])
283 295
284 for (const playlist of (video.VideoStreamingPlaylists || [])) { 296 for (const playlist of (video.VideoStreamingPlaylists || [])) {
285 let tag: ActivityTagObject[] 297 const tag = playlist.p2pMediaLoaderInfohashes
286 298 .map(i => ({ type: 'Infohash' as 'Infohash', name: i })) as ActivityTagObject[]
287 tag = playlist.p2pMediaLoaderInfohashes
288 .map(i => ({ type: 'Infohash' as 'Infohash', name: i }))
289 tag.push({ 299 tag.push({
290 type: 'Link', 300 type: 'Link',
291 name: 'sha256', 301 name: 'sha256',
@@ -308,10 +318,14 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoTorrentObject {
308 for (const caption of video.VideoCaptions) { 318 for (const caption of video.VideoCaptions) {
309 subtitleLanguage.push({ 319 subtitleLanguage.push({
310 identifier: caption.language, 320 identifier: caption.language,
311 name: VideoCaptionModel.getLanguageLabel(caption.language) 321 name: VideoCaptionModel.getLanguageLabel(caption.language),
322 url: caption.getFileUrl(video)
312 }) 323 })
313 } 324 }
314 325
326 // FIXME: remove and uncomment in PT 2.3
327 // Breaks compatibility with PT <= 2.1
328 // const icons = [ video.getMiniature(), video.getPreview() ]
315 const miniature = video.getMiniature() 329 const miniature = video.getMiniature()
316 330
317 return { 331 return {
@@ -339,11 +353,18 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoTorrentObject {
339 subtitleLanguage, 353 subtitleLanguage,
340 icon: { 354 icon: {
341 type: 'Image', 355 type: 'Image',
342 url: miniature.getFileUrl(video.isOwned()), 356 url: miniature.getFileUrl(video),
343 mediaType: 'image/jpeg', 357 mediaType: 'image/jpeg',
344 width: miniature.width, 358 width: miniature.width,
345 height: miniature.height 359 height: miniature.height
346 }, 360 } as any,
361 // icon: icons.map(i => ({
362 // type: 'Image',
363 // url: i.getFileUrl(video),
364 // mediaType: 'image/jpeg',
365 // width: i.width,
366 // height: i.height
367 // })),
347 url, 368 url,
348 likes: getVideoLikesActivityPubUrl(video), 369 likes: getVideoLikesActivityPubUrl(video),
349 dislikes: getVideoDislikesActivityPubUrl(video), 370 dislikes: getVideoDislikesActivityPubUrl(video),
diff --git a/server/models/video/video-import.ts b/server/models/video/video-import.ts
index af5314ce9..fbe0ee0a7 100644
--- a/server/models/video/video-import.ts
+++ b/server/models/video/video-import.ts
@@ -129,6 +129,7 @@ export class VideoImportModel extends Model<VideoImportModel> {
129 distinct: true, 129 distinct: true,
130 include: [ 130 include: [
131 { 131 {
132 attributes: [ 'id' ],
132 model: UserModel.unscoped(), // FIXME: Without this, sequelize try to COUNT(DISTINCT(*)) which is an invalid SQL query 133 model: UserModel.unscoped(), // FIXME: Without this, sequelize try to COUNT(DISTINCT(*)) which is an invalid SQL query
133 required: true 134 required: true
134 } 135 }
diff --git a/server/models/video/video-playlist-element.ts b/server/models/video/video-playlist-element.ts
index f2d71357f..9ea73e82e 100644
--- a/server/models/video/video-playlist-element.ts
+++ b/server/models/video/video-playlist-element.ts
@@ -120,10 +120,10 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel>
120 } 120 }
121 121
122 static listForApi (options: { 122 static listForApi (options: {
123 start: number, 123 start: number
124 count: number, 124 count: number
125 videoPlaylistId: number, 125 videoPlaylistId: number
126 serverAccount: AccountModel, 126 serverAccount: AccountModel
127 user?: MUserAccountId 127 user?: MUserAccountId
128 }) { 128 }) {
129 const accountIds = [ options.serverAccount.id ] 129 const accountIds = [ options.serverAccount.id ]
@@ -309,7 +309,10 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel>
309 // Owned video, don't filter it 309 // Owned video, don't filter it
310 if (accountId && video.VideoChannel.Account.id === accountId) return VideoPlaylistElementType.REGULAR 310 if (accountId && video.VideoChannel.Account.id === accountId) return VideoPlaylistElementType.REGULAR
311 311
312 if (video.privacy === VideoPrivacy.PRIVATE) return VideoPlaylistElementType.PRIVATE 312 // Internal video?
313 if (video.privacy === VideoPrivacy.INTERNAL && accountId) return VideoPlaylistElementType.REGULAR
314
315 if (video.privacy === VideoPrivacy.PRIVATE || video.privacy === VideoPrivacy.INTERNAL) return VideoPlaylistElementType.PRIVATE
313 316
314 if (video.isBlacklisted() || video.isBlocked()) return VideoPlaylistElementType.UNAVAILABLE 317 if (video.isBlacklisted() || video.isBlocked()) return VideoPlaylistElementType.UNAVAILABLE
315 if (video.nsfw === true && displayNSFW === false) return VideoPlaylistElementType.UNAVAILABLE 318 if (video.nsfw === true && displayNSFW === false) return VideoPlaylistElementType.UNAVAILABLE
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts
index bcdda36e5..b9b95e067 100644
--- a/server/models/video/video-playlist.ts
+++ b/server/models/video/video-playlist.ts
@@ -68,12 +68,12 @@ type AvailableForListOptions = {
68 type?: VideoPlaylistType 68 type?: VideoPlaylistType
69 accountId?: number 69 accountId?: number
70 videoChannelId?: number 70 videoChannelId?: number
71 listMyPlaylists?: boolean, 71 listMyPlaylists?: boolean
72 search?: string 72 search?: string
73} 73}
74 74
75@Scopes(() => ({ 75@Scopes(() => ({
76 [ ScopeNames.WITH_THUMBNAIL ]: { 76 [ScopeNames.WITH_THUMBNAIL]: {
77 include: [ 77 include: [
78 { 78 {
79 model: ThumbnailModel, 79 model: ThumbnailModel,
@@ -81,7 +81,7 @@ type AvailableForListOptions = {
81 } 81 }
82 ] 82 ]
83 }, 83 },
84 [ ScopeNames.WITH_VIDEOS_LENGTH ]: { 84 [ScopeNames.WITH_VIDEOS_LENGTH]: {
85 attributes: { 85 attributes: {
86 include: [ 86 include: [
87 [ 87 [
@@ -91,7 +91,7 @@ type AvailableForListOptions = {
91 ] 91 ]
92 } 92 }
93 } as FindOptions, 93 } as FindOptions,
94 [ ScopeNames.WITH_ACCOUNT ]: { 94 [ScopeNames.WITH_ACCOUNT]: {
95 include: [ 95 include: [
96 { 96 {
97 model: AccountModel, 97 model: AccountModel,
@@ -99,7 +99,7 @@ type AvailableForListOptions = {
99 } 99 }
100 ] 100 ]
101 }, 101 },
102 [ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY ]: { 102 [ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY]: {
103 include: [ 103 include: [
104 { 104 {
105 model: AccountModel.scope(AccountScopeNames.SUMMARY), 105 model: AccountModel.scope(AccountScopeNames.SUMMARY),
@@ -111,7 +111,7 @@ type AvailableForListOptions = {
111 } 111 }
112 ] 112 ]
113 }, 113 },
114 [ ScopeNames.WITH_ACCOUNT_AND_CHANNEL ]: { 114 [ScopeNames.WITH_ACCOUNT_AND_CHANNEL]: {
115 include: [ 115 include: [
116 { 116 {
117 model: AccountModel, 117 model: AccountModel,
@@ -123,7 +123,7 @@ type AvailableForListOptions = {
123 } 123 }
124 ] 124 ]
125 }, 125 },
126 [ ScopeNames.AVAILABLE_FOR_LIST ]: (options: AvailableForListOptions) => { 126 [ScopeNames.AVAILABLE_FOR_LIST]: (options: AvailableForListOptions) => {
127 127
128 let whereActor: WhereOptions = {} 128 let whereActor: WhereOptions = {}
129 129
@@ -138,13 +138,13 @@ type AvailableForListOptions = {
138 const inQueryInstanceFollow = buildServerIdsFollowedBy(options.followerActorId) 138 const inQueryInstanceFollow = buildServerIdsFollowedBy(options.followerActorId)
139 139
140 whereActor = { 140 whereActor = {
141 [ Op.or ]: [ 141 [Op.or]: [
142 { 142 {
143 serverId: null 143 serverId: null
144 }, 144 },
145 { 145 {
146 serverId: { 146 serverId: {
147 [ Op.in ]: literal(inQueryInstanceFollow) 147 [Op.in]: literal(inQueryInstanceFollow)
148 } 148 }
149 } 149 }
150 ] 150 ]
@@ -172,7 +172,7 @@ type AvailableForListOptions = {
172 if (options.search) { 172 if (options.search) {
173 whereAnd.push({ 173 whereAnd.push({
174 name: { 174 name: {
175 [ Op.iLike ]: '%' + options.search + '%' 175 [Op.iLike]: '%' + options.search + '%'
176 } 176 }
177 }) 177 })
178 } 178 }
@@ -230,7 +230,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
230 230
231 @AllowNull(true) 231 @AllowNull(true)
232 @Is('VideoPlaylistDescription', value => throwIfNotValid(value, isVideoPlaylistDescriptionValid, 'description', true)) 232 @Is('VideoPlaylistDescription', value => throwIfNotValid(value, isVideoPlaylistDescriptionValid, 'description', true))
233 @Column 233 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS.DESCRIPTION.max))
234 description: string 234 description: string
235 235
236 @AllowNull(false) 236 @AllowNull(false)
@@ -299,13 +299,13 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
299 299
300 static listForApi (options: { 300 static listForApi (options: {
301 followerActorId: number 301 followerActorId: number
302 start: number, 302 start: number
303 count: number, 303 count: number
304 sort: string, 304 sort: string
305 type?: VideoPlaylistType, 305 type?: VideoPlaylistType
306 accountId?: number, 306 accountId?: number
307 videoChannelId?: number, 307 videoChannelId?: number
308 listMyPlaylists?: boolean, 308 listMyPlaylists?: boolean
309 search?: string 309 search?: string
310 }) { 310 }) {
311 const query = { 311 const query = {
@@ -369,7 +369,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
369 model: VideoPlaylistElementModel.unscoped(), 369 model: VideoPlaylistElementModel.unscoped(),
370 where: { 370 where: {
371 videoId: { 371 videoId: {
372 [Op.in]: videoIds // FIXME: sequelize ANY seems broken 372 [Op.in]: videoIds
373 } 373 }
374 }, 374 },
375 required: true 375 required: true
@@ -522,7 +522,9 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
522 updatedAt: this.updatedAt, 522 updatedAt: this.updatedAt,
523 523
524 ownerAccount: this.OwnerAccount.toFormattedSummaryJSON(), 524 ownerAccount: this.OwnerAccount.toFormattedSummaryJSON(),
525 videoChannel: this.VideoChannel ? this.VideoChannel.toFormattedSummaryJSON() : null 525 videoChannel: this.VideoChannel
526 ? this.VideoChannel.toFormattedSummaryJSON()
527 : null
526 } 528 }
527 } 529 }
528 530
diff --git a/server/models/video/video-query-builder.ts b/server/models/video/video-query-builder.ts
new file mode 100644
index 000000000..455f9f30f
--- /dev/null
+++ b/server/models/video/video-query-builder.ts
@@ -0,0 +1,503 @@
1import { VideoFilter, VideoPrivacy, VideoState } from '@shared/models'
2import { buildDirectionAndField, createSafeIn } from '@server/models/utils'
3import { Model } from 'sequelize-typescript'
4import { MUserAccountId, MUserId } from '@server/typings/models'
5import validator from 'validator'
6import { exists } from '@server/helpers/custom-validators/misc'
7
8export type BuildVideosQueryOptions = {
9 attributes?: string[]
10
11 serverAccountId: number
12 followerActorId: number
13 includeLocalVideos: boolean
14
15 count: number
16 start: number
17 sort: string
18
19 filter?: VideoFilter
20 categoryOneOf?: number[]
21 nsfw?: boolean
22 licenceOneOf?: number[]
23 languageOneOf?: string[]
24 tagsOneOf?: string[]
25 tagsAllOf?: string[]
26
27 withFiles?: boolean
28
29 accountId?: number
30 videoChannelId?: number
31
32 videoPlaylistId?: number
33
34 trendingDays?: number
35 user?: MUserAccountId
36 historyOfUser?: MUserId
37
38 startDate?: string // ISO 8601
39 endDate?: string // ISO 8601
40 originallyPublishedStartDate?: string
41 originallyPublishedEndDate?: string
42
43 durationMin?: number // seconds
44 durationMax?: number // seconds
45
46 search?: string
47
48 isCount?: boolean
49
50 group?: string
51 having?: string
52}
53
54function buildListQuery (model: typeof Model, options: BuildVideosQueryOptions) {
55 const and: string[] = []
56 const joins: string[] = []
57 const replacements: any = {}
58 const cte: string[] = []
59
60 let attributes: string[] = options.attributes || [ '"video"."id"' ]
61 let group = options.group || ''
62 const having = options.having || ''
63
64 joins.push(
65 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId"' +
66 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId"' +
67 'INNER JOIN "actor" "accountActor" ON "account"."actorId" = "accountActor"."id"'
68 )
69
70 and.push('"video"."id" NOT IN (SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")')
71
72 if (options.serverAccountId) {
73 const blockerIds = [ options.serverAccountId ]
74 if (options.user) blockerIds.push(options.user.Account.id)
75
76 const inClause = createSafeIn(model, blockerIds)
77
78 and.push(
79 'NOT EXISTS (' +
80 ' SELECT 1 FROM "accountBlocklist" ' +
81 ' WHERE "accountBlocklist"."accountId" IN (' + inClause + ') ' +
82 ' AND "accountBlocklist"."targetAccountId" = "account"."id" ' +
83 ')' +
84 'AND NOT EXISTS (' +
85 ' SELECT 1 FROM "serverBlocklist" WHERE "serverBlocklist"."accountId" IN (' + inClause + ') ' +
86 ' AND "serverBlocklist"."targetServerId" = "accountActor"."serverId"' +
87 ')'
88 )
89 }
90
91 // Only list public/published videos
92 if (!options.filter || options.filter !== 'all-local') {
93 and.push(
94 `("video"."state" = ${VideoState.PUBLISHED} OR ` +
95 `("video"."state" = ${VideoState.TO_TRANSCODE} AND "video"."waitTranscoding" IS false))`
96 )
97
98 if (options.user) {
99 and.push(
100 `("video"."privacy" = ${VideoPrivacy.PUBLIC} OR "video"."privacy" = ${VideoPrivacy.INTERNAL})`
101 )
102 } else { // Or only public videos
103 and.push(
104 `"video"."privacy" = ${VideoPrivacy.PUBLIC}`
105 )
106 }
107 }
108
109 if (options.videoPlaylistId) {
110 joins.push(
111 'INNER JOIN "videoPlaylistElement" "video"."id" = "videoPlaylistElement"."videoId" ' +
112 'AND "videoPlaylistElement"."videoPlaylistId" = :videoPlaylistId'
113 )
114
115 replacements.videoPlaylistId = options.videoPlaylistId
116 }
117
118 if (options.filter && (options.filter === 'local' || options.filter === 'all-local')) {
119 and.push('"video"."remote" IS FALSE')
120 }
121
122 if (options.accountId) {
123 and.push('"account"."id" = :accountId')
124 replacements.accountId = options.accountId
125 }
126
127 if (options.videoChannelId) {
128 and.push('"videoChannel"."id" = :videoChannelId')
129 replacements.videoChannelId = options.videoChannelId
130 }
131
132 if (options.followerActorId) {
133 let query =
134 '(' +
135 ' EXISTS (' +
136 ' SELECT 1 FROM "videoShare" ' +
137 ' INNER JOIN "actorFollow" "actorFollowShare" ON "actorFollowShare"."targetActorId" = "videoShare"."actorId" ' +
138 ' AND "actorFollowShare"."actorId" = :followerActorId WHERE "videoShare"."videoId" = "video"."id"' +
139 ' )' +
140 ' OR' +
141 ' EXISTS (' +
142 ' SELECT 1 from "actorFollow" ' +
143 ' WHERE "actorFollow"."targetActorId" = "videoChannel"."actorId" AND "actorFollow"."actorId" = :followerActorId' +
144 ' )'
145
146 if (options.includeLocalVideos) {
147 query += ' OR "video"."remote" IS FALSE'
148 }
149
150 query += ')'
151
152 and.push(query)
153 replacements.followerActorId = options.followerActorId
154 }
155
156 if (options.withFiles === true) {
157 and.push('EXISTS (SELECT 1 FROM "videoFile" WHERE "videoFile"."videoId" = "video"."id")')
158 }
159
160 if (options.tagsOneOf) {
161 const tagsOneOfLower = options.tagsOneOf.map(t => t.toLowerCase())
162
163 and.push(
164 'EXISTS (' +
165 ' SELECT 1 FROM "videoTag" ' +
166 ' INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' +
167 ' WHERE lower("tag"."name") IN (' + createSafeIn(model, tagsOneOfLower) + ') ' +
168 ' AND "video"."id" = "videoTag"."videoId"' +
169 ')'
170 )
171 }
172
173 if (options.tagsAllOf) {
174 const tagsAllOfLower = options.tagsAllOf.map(t => t.toLowerCase())
175
176 and.push(
177 'EXISTS (' +
178 ' SELECT 1 FROM "videoTag" ' +
179 ' INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' +
180 ' WHERE lower("tag"."name") IN (' + createSafeIn(model, tagsAllOfLower) + ') ' +
181 ' AND "video"."id" = "videoTag"."videoId" ' +
182 ' GROUP BY "videoTag"."videoId" HAVING COUNT(*) = ' + tagsAllOfLower.length +
183 ')'
184 )
185 }
186
187 if (options.nsfw === true) {
188 and.push('"video"."nsfw" IS TRUE')
189 }
190
191 if (options.nsfw === false) {
192 and.push('"video"."nsfw" IS FALSE')
193 }
194
195 if (options.categoryOneOf) {
196 and.push('"video"."category" IN (:categoryOneOf)')
197 replacements.categoryOneOf = options.categoryOneOf
198 }
199
200 if (options.licenceOneOf) {
201 and.push('"video"."licence" IN (:licenceOneOf)')
202 replacements.licenceOneOf = options.licenceOneOf
203 }
204
205 if (options.languageOneOf) {
206 const languages = options.languageOneOf.filter(l => l && l !== '_unknown')
207 const languagesQueryParts: string[] = []
208
209 if (languages.length !== 0) {
210 languagesQueryParts.push('"video"."language" IN (:languageOneOf)')
211 replacements.languageOneOf = languages
212
213 languagesQueryParts.push(
214 'EXISTS (' +
215 ' SELECT 1 FROM "videoCaption" WHERE "videoCaption"."language" ' +
216 ' IN (' + createSafeIn(model, languages) + ') AND ' +
217 ' "videoCaption"."videoId" = "video"."id"' +
218 ')'
219 )
220 }
221
222 if (options.languageOneOf.includes('_unknown')) {
223 languagesQueryParts.push('"video"."language" IS NULL')
224 }
225
226 if (languagesQueryParts.length !== 0) {
227 and.push('(' + languagesQueryParts.join(' OR ') + ')')
228 }
229 }
230
231 // We don't exclude results in this if so if we do a count we don't need to add this complex clauses
232 if (options.trendingDays && options.isCount !== true) {
233 const viewsGteDate = new Date(new Date().getTime() - (24 * 3600 * 1000) * options.trendingDays)
234
235 joins.push('LEFT JOIN "videoView" ON "video"."id" = "videoView"."videoId" AND "videoView"."startDate" >= :viewsGteDate')
236 replacements.viewsGteDate = viewsGteDate
237
238 attributes.push('COALESCE(SUM("videoView"."views"), 0) AS "videoViewsSum"')
239
240 group = 'GROUP BY "video"."id"'
241 }
242
243 if (options.historyOfUser) {
244 joins.push('INNER JOIN "userVideoHistory" on "video"."id" = "userVideoHistory"."videoId"')
245
246 and.push('"userVideoHistory"."userId" = :historyOfUser')
247 replacements.historyOfUser = options.historyOfUser.id
248 }
249
250 if (options.startDate) {
251 and.push('"video"."publishedAt" >= :startDate')
252 replacements.startDate = options.startDate
253 }
254
255 if (options.endDate) {
256 and.push('"video"."publishedAt" <= :endDate')
257 replacements.endDate = options.endDate
258 }
259
260 if (options.originallyPublishedStartDate) {
261 and.push('"video"."originallyPublishedAt" >= :originallyPublishedStartDate')
262 replacements.originallyPublishedStartDate = options.originallyPublishedStartDate
263 }
264
265 if (options.originallyPublishedEndDate) {
266 and.push('"video"."originallyPublishedAt" <= :originallyPublishedEndDate')
267 replacements.originallyPublishedEndDate = options.originallyPublishedEndDate
268 }
269
270 if (options.durationMin) {
271 and.push('"video"."duration" >= :durationMin')
272 replacements.durationMin = options.durationMin
273 }
274
275 if (options.durationMax) {
276 and.push('"video"."duration" <= :durationMax')
277 replacements.durationMax = options.durationMax
278 }
279
280 if (options.search) {
281 const escapedSearch = model.sequelize.escape(options.search)
282 const escapedLikeSearch = model.sequelize.escape('%' + options.search + '%')
283
284 cte.push(
285 '"trigramSearch" AS (' +
286 ' SELECT "video"."id", ' +
287 ` similarity(lower(immutable_unaccent("video"."name")), lower(immutable_unaccent(${escapedSearch}))) as similarity ` +
288 ' FROM "video" ' +
289 ' WHERE lower(immutable_unaccent("video"."name")) % lower(immutable_unaccent(' + escapedSearch + ')) OR ' +
290 ' lower(immutable_unaccent("video"."name")) LIKE lower(immutable_unaccent(' + escapedLikeSearch + '))' +
291 ')'
292 )
293
294 joins.push('LEFT JOIN "trigramSearch" ON "video"."id" = "trigramSearch"."id"')
295
296 let base = '(' +
297 ' "trigramSearch"."id" IS NOT NULL OR ' +
298 ' EXISTS (' +
299 ' SELECT 1 FROM "videoTag" ' +
300 ' INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' +
301 ` WHERE lower("tag"."name") = ${escapedSearch} ` +
302 ' AND "video"."id" = "videoTag"."videoId"' +
303 ' )'
304
305 if (validator.isUUID(options.search)) {
306 base += ` OR "video"."uuid" = ${escapedSearch}`
307 }
308
309 base += ')'
310 and.push(base)
311
312 attributes.push(`COALESCE("trigramSearch"."similarity", 0) as similarity`)
313 } else {
314 attributes.push('0 as similarity')
315 }
316
317 if (options.isCount === true) attributes = [ 'COUNT(*) as "total"' ]
318
319 let suffix = ''
320 let order = ''
321 if (options.isCount !== true) {
322
323 if (exists(options.sort)) {
324 if (options.sort === '-originallyPublishedAt' || options.sort === 'originallyPublishedAt') {
325 attributes.push('COALESCE("video"."originallyPublishedAt", "video"."publishedAt") AS "publishedAtForOrder"')
326 }
327
328 order = buildOrder(model, options.sort)
329 suffix += `${order} `
330 }
331
332 if (exists(options.count)) {
333 const count = parseInt(options.count + '', 10)
334 suffix += `LIMIT ${count} `
335 }
336
337 if (exists(options.start)) {
338 const start = parseInt(options.start + '', 10)
339 suffix += `OFFSET ${start} `
340 }
341 }
342
343 const cteString = cte.length !== 0
344 ? `WITH ${cte.join(', ')} `
345 : ''
346
347 const query = cteString +
348 'SELECT ' + attributes.join(', ') + ' ' +
349 'FROM "video" ' + joins.join(' ') + ' ' +
350 'WHERE ' + and.join(' AND ') + ' ' +
351 group + ' ' +
352 having + ' ' +
353 suffix
354
355 return { query, replacements, order }
356}
357
358function buildOrder (model: typeof Model, value: string) {
359 const { direction, field } = buildDirectionAndField(value)
360 if (field.match(/^[a-zA-Z."]+$/) === null) throw new Error('Invalid sort column ' + field)
361
362 if (field.toLowerCase() === 'random') return 'ORDER BY RANDOM()'
363
364 if (field.toLowerCase() === 'trending') { // Sort by aggregation
365 return `ORDER BY "videoViewsSum" ${direction}, "video"."views" ${direction}`
366 }
367
368 let firstSort: string
369
370 if (field.toLowerCase() === 'match') { // Search
371 firstSort = '"similarity"'
372 } else if (field === 'originallyPublishedAt') {
373 firstSort = '"publishedAtForOrder"'
374 } else if (field.includes('.')) {
375 firstSort = field
376 } else {
377 firstSort = `"video"."${field}"`
378 }
379
380 return `ORDER BY ${firstSort} ${direction}, "video"."id" ASC`
381}
382
383function wrapForAPIResults (baseQuery: string, replacements: any, options: BuildVideosQueryOptions, order: string) {
384 const attributes = {
385 '"video".*': '',
386 '"VideoChannel"."id"': '"VideoChannel.id"',
387 '"VideoChannel"."name"': '"VideoChannel.name"',
388 '"VideoChannel"."description"': '"VideoChannel.description"',
389 '"VideoChannel"."actorId"': '"VideoChannel.actorId"',
390 '"VideoChannel->Actor"."id"': '"VideoChannel.Actor.id"',
391 '"VideoChannel->Actor"."preferredUsername"': '"VideoChannel.Actor.preferredUsername"',
392 '"VideoChannel->Actor"."url"': '"VideoChannel.Actor.url"',
393 '"VideoChannel->Actor"."serverId"': '"VideoChannel.Actor.serverId"',
394 '"VideoChannel->Actor"."avatarId"': '"VideoChannel.Actor.avatarId"',
395 '"VideoChannel->Account"."id"': '"VideoChannel.Account.id"',
396 '"VideoChannel->Account"."name"': '"VideoChannel.Account.name"',
397 '"VideoChannel->Account->Actor"."id"': '"VideoChannel.Account.Actor.id"',
398 '"VideoChannel->Account->Actor"."preferredUsername"': '"VideoChannel.Account.Actor.preferredUsername"',
399 '"VideoChannel->Account->Actor"."url"': '"VideoChannel.Account.Actor.url"',
400 '"VideoChannel->Account->Actor"."serverId"': '"VideoChannel.Account.Actor.serverId"',
401 '"VideoChannel->Account->Actor"."avatarId"': '"VideoChannel.Account.Actor.avatarId"',
402 '"VideoChannel->Actor->Server"."id"': '"VideoChannel.Actor.Server.id"',
403 '"VideoChannel->Actor->Server"."host"': '"VideoChannel.Actor.Server.host"',
404 '"VideoChannel->Actor->Avatar"."id"': '"VideoChannel.Actor.Avatar.id"',
405 '"VideoChannel->Actor->Avatar"."filename"': '"VideoChannel.Actor.Avatar.filename"',
406 '"VideoChannel->Actor->Avatar"."fileUrl"': '"VideoChannel.Actor.Avatar.fileUrl"',
407 '"VideoChannel->Actor->Avatar"."onDisk"': '"VideoChannel.Actor.Avatar.onDisk"',
408 '"VideoChannel->Actor->Avatar"."createdAt"': '"VideoChannel.Actor.Avatar.createdAt"',
409 '"VideoChannel->Actor->Avatar"."updatedAt"': '"VideoChannel.Actor.Avatar.updatedAt"',
410 '"VideoChannel->Account->Actor->Server"."id"': '"VideoChannel.Account.Actor.Server.id"',
411 '"VideoChannel->Account->Actor->Server"."host"': '"VideoChannel.Account.Actor.Server.host"',
412 '"VideoChannel->Account->Actor->Avatar"."id"': '"VideoChannel.Account.Actor.Avatar.id"',
413 '"VideoChannel->Account->Actor->Avatar"."filename"': '"VideoChannel.Account.Actor.Avatar.filename"',
414 '"VideoChannel->Account->Actor->Avatar"."fileUrl"': '"VideoChannel.Account.Actor.Avatar.fileUrl"',
415 '"VideoChannel->Account->Actor->Avatar"."onDisk"': '"VideoChannel.Account.Actor.Avatar.onDisk"',
416 '"VideoChannel->Account->Actor->Avatar"."createdAt"': '"VideoChannel.Account.Actor.Avatar.createdAt"',
417 '"VideoChannel->Account->Actor->Avatar"."updatedAt"': '"VideoChannel.Account.Actor.Avatar.updatedAt"',
418 '"Thumbnails"."id"': '"Thumbnails.id"',
419 '"Thumbnails"."type"': '"Thumbnails.type"',
420 '"Thumbnails"."filename"': '"Thumbnails.filename"'
421 }
422
423 const joins = [
424 'INNER JOIN "video" ON "tmp"."id" = "video"."id"',
425
426 'INNER JOIN "videoChannel" AS "VideoChannel" ON "video"."channelId" = "VideoChannel"."id"',
427 'INNER JOIN "actor" AS "VideoChannel->Actor" ON "VideoChannel"."actorId" = "VideoChannel->Actor"."id"',
428 'INNER JOIN "account" AS "VideoChannel->Account" ON "VideoChannel"."accountId" = "VideoChannel->Account"."id"',
429 'INNER JOIN "actor" AS "VideoChannel->Account->Actor" ON "VideoChannel->Account"."actorId" = "VideoChannel->Account->Actor"."id"',
430
431 'LEFT OUTER JOIN "server" AS "VideoChannel->Actor->Server" ON "VideoChannel->Actor"."serverId" = "VideoChannel->Actor->Server"."id"',
432 'LEFT OUTER JOIN "avatar" AS "VideoChannel->Actor->Avatar" ON "VideoChannel->Actor"."avatarId" = "VideoChannel->Actor->Avatar"."id"',
433
434 'LEFT OUTER JOIN "server" AS "VideoChannel->Account->Actor->Server" ' +
435 'ON "VideoChannel->Account->Actor"."serverId" = "VideoChannel->Account->Actor->Server"."id"',
436
437 'LEFT OUTER JOIN "avatar" AS "VideoChannel->Account->Actor->Avatar" ' +
438 'ON "VideoChannel->Account->Actor"."avatarId" = "VideoChannel->Account->Actor->Avatar"."id"',
439
440 'LEFT OUTER JOIN "thumbnail" AS "Thumbnails" ON "video"."id" = "Thumbnails"."videoId"'
441 ]
442
443 if (options.withFiles) {
444 joins.push('INNER JOIN "videoFile" AS "VideoFiles" ON "VideoFiles"."videoId" = "video"."id"')
445
446 Object.assign(attributes, {
447 '"VideoFiles"."id"': '"VideoFiles.id"',
448 '"VideoFiles"."createdAt"': '"VideoFiles.createdAt"',
449 '"VideoFiles"."updatedAt"': '"VideoFiles.updatedAt"',
450 '"VideoFiles"."resolution"': '"VideoFiles.resolution"',
451 '"VideoFiles"."size"': '"VideoFiles.size"',
452 '"VideoFiles"."extname"': '"VideoFiles.extname"',
453 '"VideoFiles"."infoHash"': '"VideoFiles.infoHash"',
454 '"VideoFiles"."fps"': '"VideoFiles.fps"',
455 '"VideoFiles"."videoId"': '"VideoFiles.videoId"'
456 })
457 }
458
459 if (options.user) {
460 joins.push(
461 'LEFT OUTER JOIN "userVideoHistory" ' +
462 'ON "video"."id" = "userVideoHistory"."videoId" AND "userVideoHistory"."userId" = :userVideoHistoryId'
463 )
464 replacements.userVideoHistoryId = options.user.id
465
466 Object.assign(attributes, {
467 '"userVideoHistory"."id"': '"userVideoHistory.id"',
468 '"userVideoHistory"."currentTime"': '"userVideoHistory.currentTime"'
469 })
470 }
471
472 if (options.videoPlaylistId) {
473 joins.push(
474 'INNER JOIN "videoPlaylistElement" as "VideoPlaylistElement" ON "videoPlaylistElement"."videoId" = "video"."id" ' +
475 'AND "VideoPlaylistElement"."videoPlaylistId" = :videoPlaylistId'
476 )
477 replacements.videoPlaylistId = options.videoPlaylistId
478
479 Object.assign(attributes, {
480 '"VideoPlaylistElement"."createdAt"': '"VideoPlaylistElement.createdAt"',
481 '"VideoPlaylistElement"."updatedAt"': '"VideoPlaylistElement.updatedAt"',
482 '"VideoPlaylistElement"."url"': '"VideoPlaylistElement.url"',
483 '"VideoPlaylistElement"."position"': '"VideoPlaylistElement.position"',
484 '"VideoPlaylistElement"."startTimestamp"': '"VideoPlaylistElement.startTimestamp"',
485 '"VideoPlaylistElement"."stopTimestamp"': '"VideoPlaylistElement.stopTimestamp"',
486 '"VideoPlaylistElement"."videoPlaylistId"': '"VideoPlaylistElement.videoPlaylistId"'
487 })
488 }
489
490 const select = 'SELECT ' + Object.keys(attributes).map(key => {
491 const value = attributes[key]
492 if (value) return `${key} AS ${value}`
493
494 return key
495 }).join(', ')
496
497 return `${select} FROM (${baseQuery}) AS "tmp" ${joins.join(' ')} ${order}`
498}
499
500export {
501 buildListQuery,
502 wrapForAPIResults
503}
diff --git a/server/models/video/video-share.ts b/server/models/video/video-share.ts
index 50525b4c2..4bbef75e6 100644
--- a/server/models/video/video-share.ts
+++ b/server/models/video/video-share.ts
@@ -2,12 +2,10 @@ import * as Bluebird from 'bluebird'
2import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' 2import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
3import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 3import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
4import { CONSTRAINTS_FIELDS } from '../../initializers/constants' 4import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
5import { AccountModel } from '../account/account'
6import { ActorModel } from '../activitypub/actor' 5import { ActorModel } from '../activitypub/actor'
7import { buildLocalActorIdsIn, throwIfNotValid } from '../utils' 6import { buildLocalActorIdsIn, throwIfNotValid } from '../utils'
8import { VideoModel } from './video' 7import { VideoModel } from './video'
9import { VideoChannelModel } from './video-channel' 8import { literal, Op, Transaction } from 'sequelize'
10import { Op, Transaction } from 'sequelize'
11import { MVideoShareActor, MVideoShareFull } from '../../typings/models/video' 9import { MVideoShareActor, MVideoShareFull } from '../../typings/models/video'
12import { MActorDefault } from '../../typings/models' 10import { MActorDefault } from '../../typings/models'
13 11
@@ -124,70 +122,55 @@ export class VideoShareModel extends Model<VideoShareModel> {
124 } 122 }
125 123
126 return VideoShareModel.scope(ScopeNames.FULL).findAll(query) 124 return VideoShareModel.scope(ScopeNames.FULL).findAll(query)
127 .then((res: MVideoShareFull[]) => res.map(r => r.Actor)) 125 .then((res: MVideoShareFull[]) => res.map(r => r.Actor))
128 } 126 }
129 127
130 static loadActorsWhoSharedVideosOf (actorOwnerId: number, t: Transaction): Bluebird<MActorDefault[]> { 128 static loadActorsWhoSharedVideosOf (actorOwnerId: number, t: Transaction): Bluebird<MActorDefault[]> {
129 const safeOwnerId = parseInt(actorOwnerId + '', 10)
130
131 // /!\ On actor model
131 const query = { 132 const query = {
132 attributes: [], 133 where: {
133 include: [ 134 [Op.and]: [
134 { 135 literal(
135 model: ActorModel, 136 `EXISTS (` +
136 required: true 137 ` SELECT 1 FROM "videoShare" ` +
137 }, 138 ` INNER JOIN "video" ON "videoShare"."videoId" = "video"."id" ` +
138 { 139 ` INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ` +
139 attributes: [], 140 ` INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ` +
140 model: VideoModel, 141 ` WHERE "videoShare"."actorId" = "ActorModel"."id" AND "account"."actorId" = ${safeOwnerId} ` +
141 required: true, 142 ` LIMIT 1` +
142 include: [ 143 `)`
143 { 144 )
144 attributes: [], 145 ]
145 model: VideoChannelModel.unscoped(), 146 },
146 required: true,
147 include: [
148 {
149 attributes: [],
150 model: AccountModel.unscoped(),
151 required: true,
152 where: {
153 actorId: actorOwnerId
154 }
155 }
156 ]
157 }
158 ]
159 }
160 ],
161 transaction: t 147 transaction: t
162 } 148 }
163 149
164 return VideoShareModel.scope(ScopeNames.FULL).findAll(query) 150 return ActorModel.findAll(query)
165 .then(res => res.map(r => r.Actor))
166 } 151 }
167 152
168 static loadActorsByVideoChannel (videoChannelId: number, t: Transaction): Bluebird<MActorDefault[]> { 153 static loadActorsByVideoChannel (videoChannelId: number, t: Transaction): Bluebird<MActorDefault[]> {
154 const safeChannelId = parseInt(videoChannelId + '', 10)
155
156 // /!\ On actor model
169 const query = { 157 const query = {
170 attributes: [], 158 where: {
171 include: [ 159 [Op.and]: [
172 { 160 literal(
173 model: ActorModel, 161 `EXISTS (` +
174 required: true 162 ` SELECT 1 FROM "videoShare" ` +
175 }, 163 ` INNER JOIN "video" ON "videoShare"."videoId" = "video"."id" ` +
176 { 164 ` WHERE "videoShare"."actorId" = "ActorModel"."id" AND "video"."channelId" = ${safeChannelId} ` +
177 attributes: [], 165 ` LIMIT 1` +
178 model: VideoModel, 166 `)`
179 required: true, 167 )
180 where: { 168 ]
181 channelId: videoChannelId 169 },
182 }
183 }
184 ],
185 transaction: t 170 transaction: t
186 } 171 }
187 172
188 return VideoShareModel.scope(ScopeNames.FULL) 173 return ActorModel.findAll(query)
189 .findAll(query)
190 .then(res => res.map(r => r.Actor))
191 } 174 }
192 175
193 static listAndCountByVideoId (videoId: number, start: number, count: number, t?: Transaction) { 176 static listAndCountByVideoId (videoId: number, start: number, count: number, t?: Transaction) {
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index a91a7663d..f5194e259 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -1,18 +1,7 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { maxBy, minBy } from 'lodash' 2import { maxBy, minBy, pick } from 'lodash'
3import { join } from 'path' 3import { join } from 'path'
4import { 4import { FindOptions, IncludeOptions, Op, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize'
5 CountOptions,
6 FindOptions,
7 IncludeOptions,
8 ModelIndexesOptions,
9 Op,
10 QueryTypes,
11 ScopeOptions,
12 Sequelize,
13 Transaction,
14 WhereOptions
15} from 'sequelize'
16import { 5import {
17 AllowNull, 6 AllowNull,
18 BeforeDestroy, 7 BeforeDestroy,
@@ -54,7 +43,6 @@ import {
54} from '../../helpers/custom-validators/videos' 43} from '../../helpers/custom-validators/videos'
55import { getVideoFileResolution } from '../../helpers/ffmpeg-utils' 44import { getVideoFileResolution } from '../../helpers/ffmpeg-utils'
56import { logger } from '../../helpers/logger' 45import { logger } from '../../helpers/logger'
57import { getServerActor } from '../../helpers/utils'
58import { 46import {
59 ACTIVITY_PUB, 47 ACTIVITY_PUB,
60 API_VERSION, 48 API_VERSION,
@@ -76,16 +64,7 @@ import { AccountVideoRateModel } from '../account/account-video-rate'
76import { ActorModel } from '../activitypub/actor' 64import { ActorModel } from '../activitypub/actor'
77import { AvatarModel } from '../avatar/avatar' 65import { AvatarModel } from '../avatar/avatar'
78import { ServerModel } from '../server/server' 66import { ServerModel } from '../server/server'
79import { 67import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils'
80 buildBlockedAccountSQL,
81 buildTrigramSearchIndex,
82 buildWhereIdOrUUID,
83 createSafeIn,
84 createSimilarityAttribute,
85 getVideoSort,
86 isOutdated,
87 throwIfNotValid
88} from '../utils'
89import { TagModel } from './tag' 68import { TagModel } from './tag'
90import { VideoAbuseModel } from './video-abuse' 69import { VideoAbuseModel } from './video-abuse'
91import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel' 70import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel'
@@ -132,6 +111,7 @@ import {
132 MVideoForUser, 111 MVideoForUser,
133 MVideoFullLight, 112 MVideoFullLight,
134 MVideoIdThumbnail, 113 MVideoIdThumbnail,
114 MVideoImmutable,
135 MVideoThumbnail, 115 MVideoThumbnail,
136 MVideoThumbnailBlacklist, 116 MVideoThumbnailBlacklist,
137 MVideoWithAllFiles, 117 MVideoWithAllFiles,
@@ -142,75 +122,10 @@ import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../typings/mode
142import { MThumbnail } from '../../typings/models/video/thumbnail' 122import { MThumbnail } from '../../typings/models/video/thumbnail'
143import { VideoFile } from '@shared/models/videos/video-file.model' 123import { VideoFile } from '@shared/models/videos/video-file.model'
144import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths' 124import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
145import validator from 'validator' 125import { ModelCache } from '@server/models/model-cache'
146 126import { buildListQuery, BuildVideosQueryOptions, wrapForAPIResults } from './video-query-builder'
147// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation 127import { buildNSFWFilter } from '@server/helpers/express-utils'
148const indexes: (ModelIndexesOptions & { where?: WhereOptions })[] = [ 128import { getServerActor } from '@server/models/application/application'
149 buildTrigramSearchIndex('video_name_trigram', 'name'),
150
151 { fields: [ 'createdAt' ] },
152 {
153 fields: [
154 { name: 'publishedAt', order: 'DESC' },
155 { name: 'id', order: 'ASC' }
156 ]
157 },
158 { fields: [ 'duration' ] },
159 { fields: [ 'views' ] },
160 { fields: [ 'channelId' ] },
161 {
162 fields: [ 'originallyPublishedAt' ],
163 where: {
164 originallyPublishedAt: {
165 [Op.ne]: null
166 }
167 }
168 },
169 {
170 fields: [ 'category' ], // We don't care videos with an unknown category
171 where: {
172 category: {
173 [Op.ne]: null
174 }
175 }
176 },
177 {
178 fields: [ 'licence' ], // We don't care videos with an unknown licence
179 where: {
180 licence: {
181 [Op.ne]: null
182 }
183 }
184 },
185 {
186 fields: [ 'language' ], // We don't care videos with an unknown language
187 where: {
188 language: {
189 [Op.ne]: null
190 }
191 }
192 },
193 {
194 fields: [ 'nsfw' ], // Most of the videos are not NSFW
195 where: {
196 nsfw: true
197 }
198 },
199 {
200 fields: [ 'remote' ], // Only index local videos
201 where: {
202 remote: false
203 }
204 },
205 {
206 fields: [ 'uuid' ],
207 unique: true
208 },
209 {
210 fields: [ 'url' ],
211 unique: true
212 }
213]
214 129
215export enum ScopeNames { 130export enum ScopeNames {
216 AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS', 131 AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS',
@@ -223,6 +138,7 @@ export enum ScopeNames {
223 WITH_USER_HISTORY = 'WITH_USER_HISTORY', 138 WITH_USER_HISTORY = 'WITH_USER_HISTORY',
224 WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS', 139 WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS',
225 WITH_USER_ID = 'WITH_USER_ID', 140 WITH_USER_ID = 'WITH_USER_ID',
141 WITH_IMMUTABLE_ATTRIBUTES = 'WITH_IMMUTABLE_ATTRIBUTES',
226 WITH_THUMBNAILS = 'WITH_THUMBNAILS' 142 WITH_THUMBNAILS = 'WITH_THUMBNAILS'
227} 143}
228 144
@@ -266,7 +182,10 @@ export type AvailableForListIDsOptions = {
266} 182}
267 183
268@Scopes(() => ({ 184@Scopes(() => ({
269 [ ScopeNames.FOR_API ]: (options: ForAPIOptions) => { 185 [ScopeNames.WITH_IMMUTABLE_ATTRIBUTES]: {
186 attributes: [ 'id', 'url', 'uuid', 'remote' ]
187 },
188 [ScopeNames.FOR_API]: (options: ForAPIOptions) => {
270 const query: FindOptions = { 189 const query: FindOptions = {
271 include: [ 190 include: [
272 { 191 {
@@ -291,14 +210,14 @@ export type AvailableForListIDsOptions = {
291 if (options.ids) { 210 if (options.ids) {
292 query.where = { 211 query.where = {
293 id: { 212 id: {
294 [ Op.in ]: options.ids // FIXME: sequelize ANY seems broken 213 [Op.in]: options.ids
295 } 214 }
296 } 215 }
297 } 216 }
298 217
299 if (options.withFiles === true) { 218 if (options.withFiles === true) {
300 query.include.push({ 219 query.include.push({
301 model: VideoFileModel.unscoped(), 220 model: VideoFileModel,
302 required: true 221 required: true
303 }) 222 })
304 } 223 }
@@ -315,276 +234,7 @@ export type AvailableForListIDsOptions = {
315 234
316 return query 235 return query
317 }, 236 },
318 [ ScopeNames.AVAILABLE_FOR_LIST_IDS ]: (options: AvailableForListIDsOptions) => { 237 [ScopeNames.WITH_THUMBNAILS]: {
319 const whereAnd = options.baseWhere ? [].concat(options.baseWhere) : []
320
321 const query: FindOptions = {
322 raw: true,
323 include: []
324 }
325
326 const attributesType = options.attributesType || 'id'
327
328 if (attributesType === 'id') query.attributes = [ 'id' ]
329 else if (attributesType === 'none') query.attributes = [ ]
330
331 whereAnd.push({
332 id: {
333 [ Op.notIn ]: Sequelize.literal(
334 '(SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")'
335 )
336 }
337 })
338
339 if (options.serverAccountId) {
340 whereAnd.push({
341 channelId: {
342 [ Op.notIn ]: Sequelize.literal(
343 '(' +
344 'SELECT id FROM "videoChannel" WHERE "accountId" IN (' +
345 buildBlockedAccountSQL(options.serverAccountId, options.user ? options.user.Account.id : undefined) +
346 ')' +
347 ')'
348 )
349 }
350 })
351 }
352
353 // Only list public/published videos
354 if (!options.filter || options.filter !== 'all-local') {
355
356 const publishWhere = {
357 // Always list published videos, or videos that are being transcoded but on which we don't want to wait for transcoding
358 [ Op.or ]: [
359 {
360 state: VideoState.PUBLISHED
361 },
362 {
363 [ Op.and ]: {
364 state: VideoState.TO_TRANSCODE,
365 waitTranscoding: false
366 }
367 }
368 ]
369 }
370 whereAnd.push(publishWhere)
371
372 // List internal videos if the user is logged in
373 if (options.user) {
374 const privacyWhere = {
375 [Op.or]: [
376 {
377 privacy: VideoPrivacy.INTERNAL
378 },
379 {
380 privacy: VideoPrivacy.PUBLIC
381 }
382 ]
383 }
384
385 whereAnd.push(privacyWhere)
386 } else { // Or only public videos
387 const privacyWhere = { privacy: VideoPrivacy.PUBLIC }
388 whereAnd.push(privacyWhere)
389 }
390 }
391
392 if (options.videoPlaylistId) {
393 query.include.push({
394 attributes: [],
395 model: VideoPlaylistElementModel.unscoped(),
396 required: true,
397 where: {
398 videoPlaylistId: options.videoPlaylistId
399 }
400 })
401
402 query.subQuery = false
403 }
404
405 if (options.filter && (options.filter === 'local' || options.filter === 'all-local')) {
406 whereAnd.push({
407 remote: false
408 })
409 }
410
411 if (options.accountId || options.videoChannelId) {
412 const videoChannelInclude: IncludeOptions = {
413 attributes: [],
414 model: VideoChannelModel.unscoped(),
415 required: true
416 }
417
418 if (options.videoChannelId) {
419 videoChannelInclude.where = {
420 id: options.videoChannelId
421 }
422 }
423
424 if (options.accountId) {
425 const accountInclude: IncludeOptions = {
426 attributes: [],
427 model: AccountModel.unscoped(),
428 required: true
429 }
430
431 accountInclude.where = { id: options.accountId }
432 videoChannelInclude.include = [ accountInclude ]
433 }
434
435 query.include.push(videoChannelInclude)
436 }
437
438 if (options.followerActorId) {
439 let localVideosReq = ''
440 if (options.includeLocalVideos === true) {
441 localVideosReq = ' UNION ALL SELECT "video"."id" FROM "video" WHERE remote IS FALSE'
442 }
443
444 // Force actorId to be a number to avoid SQL injections
445 const actorIdNumber = parseInt(options.followerActorId.toString(), 10)
446 whereAnd.push({
447 id: {
448 [Op.in]: Sequelize.literal(
449 '(' +
450 'SELECT "videoShare"."videoId" AS "id" FROM "videoShare" ' +
451 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' +
452 'WHERE "actorFollow"."actorId" = ' + actorIdNumber +
453 ' UNION ALL ' +
454 'SELECT "video"."id" AS "id" FROM "video" ' +
455 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
456 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' +
457 'INNER JOIN "actor" ON "account"."actorId" = "actor"."id" ' +
458 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "actor"."id" ' +
459 'WHERE "actorFollow"."actorId" = ' + actorIdNumber +
460 localVideosReq +
461 ')'
462 )
463 }
464 })
465 }
466
467 if (options.withFiles === true) {
468 whereAnd.push({
469 id: {
470 [ Op.in ]: Sequelize.literal(
471 '(SELECT "videoId" FROM "videoFile")'
472 )
473 }
474 })
475 }
476
477 // FIXME: issues with sequelize count when making a join on n:m relation, so we just make a IN()
478 if (options.tagsAllOf || options.tagsOneOf) {
479 if (options.tagsOneOf) {
480 const tagsOneOfLower = options.tagsOneOf.map(t => t.toLowerCase())
481
482 whereAnd.push({
483 id: {
484 [ Op.in ]: Sequelize.literal(
485 '(' +
486 'SELECT "videoId" FROM "videoTag" ' +
487 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' +
488 'WHERE lower("tag"."name") IN (' + createSafeIn(VideoModel, tagsOneOfLower) + ')' +
489 ')'
490 )
491 }
492 })
493 }
494
495 if (options.tagsAllOf) {
496 const tagsAllOfLower = options.tagsAllOf.map(t => t.toLowerCase())
497
498 whereAnd.push({
499 id: {
500 [ Op.in ]: Sequelize.literal(
501 '(' +
502 'SELECT "videoId" FROM "videoTag" ' +
503 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' +
504 'WHERE lower("tag"."name") IN (' + createSafeIn(VideoModel, tagsAllOfLower) + ')' +
505 'GROUP BY "videoTag"."videoId" HAVING COUNT(*) = ' + tagsAllOfLower.length +
506 ')'
507 )
508 }
509 })
510 }
511 }
512
513 if (options.nsfw === true || options.nsfw === false) {
514 whereAnd.push({ nsfw: options.nsfw })
515 }
516
517 if (options.categoryOneOf) {
518 whereAnd.push({
519 category: {
520 [ Op.or ]: options.categoryOneOf
521 }
522 })
523 }
524
525 if (options.licenceOneOf) {
526 whereAnd.push({
527 licence: {
528 [ Op.or ]: options.licenceOneOf
529 }
530 })
531 }
532
533 if (options.languageOneOf) {
534 let videoLanguages = options.languageOneOf
535 if (options.languageOneOf.find(l => l === '_unknown')) {
536 videoLanguages = videoLanguages.concat([ null ])
537 }
538
539 whereAnd.push({
540 [Op.or]: [
541 {
542 language: {
543 [ Op.or ]: videoLanguages
544 }
545 },
546 {
547 id: {
548 [ Op.in ]: Sequelize.literal(
549 '(' +
550 'SELECT "videoId" FROM "videoCaption" ' +
551 'WHERE "language" IN (' + createSafeIn(VideoModel, options.languageOneOf) + ') ' +
552 ')'
553 )
554 }
555 }
556 ]
557 })
558 }
559
560 if (options.trendingDays) {
561 query.include.push(VideoModel.buildTrendingQuery(options.trendingDays))
562
563 query.subQuery = false
564 }
565
566 if (options.historyOfUser) {
567 query.include.push({
568 model: UserVideoHistoryModel,
569 required: true,
570 where: {
571 userId: options.historyOfUser.id
572 }
573 })
574
575 // Even if the relation is n:m, we know that a user only have 0..1 video history
576 // So we won't have multiple rows for the same video
577 // Without this, we would not be able to sort on "updatedAt" column of UserVideoHistoryModel
578 query.subQuery = false
579 }
580
581 query.where = {
582 [ Op.and ]: whereAnd
583 }
584
585 return query
586 },
587 [ ScopeNames.WITH_THUMBNAILS ]: {
588 include: [ 238 include: [
589 { 239 {
590 model: ThumbnailModel, 240 model: ThumbnailModel,
@@ -592,7 +242,7 @@ export type AvailableForListIDsOptions = {
592 } 242 }
593 ] 243 ]
594 }, 244 },
595 [ ScopeNames.WITH_USER_ID ]: { 245 [ScopeNames.WITH_USER_ID]: {
596 include: [ 246 include: [
597 { 247 {
598 attributes: [ 'accountId' ], 248 attributes: [ 'accountId' ],
@@ -608,7 +258,7 @@ export type AvailableForListIDsOptions = {
608 } 258 }
609 ] 259 ]
610 }, 260 },
611 [ ScopeNames.WITH_ACCOUNT_DETAILS ]: { 261 [ScopeNames.WITH_ACCOUNT_DETAILS]: {
612 include: [ 262 include: [
613 { 263 {
614 model: VideoChannelModel.unscoped(), 264 model: VideoChannelModel.unscoped(),
@@ -660,10 +310,10 @@ export type AvailableForListIDsOptions = {
660 } 310 }
661 ] 311 ]
662 }, 312 },
663 [ ScopeNames.WITH_TAGS ]: { 313 [ScopeNames.WITH_TAGS]: {
664 include: [ TagModel ] 314 include: [ TagModel ]
665 }, 315 },
666 [ ScopeNames.WITH_BLACKLISTED ]: { 316 [ScopeNames.WITH_BLACKLISTED]: {
667 include: [ 317 include: [
668 { 318 {
669 attributes: [ 'id', 'reason', 'unfederated' ], 319 attributes: [ 'id', 'reason', 'unfederated' ],
@@ -672,7 +322,7 @@ export type AvailableForListIDsOptions = {
672 } 322 }
673 ] 323 ]
674 }, 324 },
675 [ ScopeNames.WITH_WEBTORRENT_FILES ]: (withRedundancies = false) => { 325 [ScopeNames.WITH_WEBTORRENT_FILES]: (withRedundancies = false) => {
676 let subInclude: any[] = [] 326 let subInclude: any[] = []
677 327
678 if (withRedundancies === true) { 328 if (withRedundancies === true) {
@@ -688,7 +338,7 @@ export type AvailableForListIDsOptions = {
688 return { 338 return {
689 include: [ 339 include: [
690 { 340 {
691 model: VideoFileModel.unscoped(), 341 model: VideoFileModel,
692 separate: true, // We may have multiple files, having multiple redundancies so let's separate this join 342 separate: true, // We may have multiple files, having multiple redundancies so let's separate this join
693 required: false, 343 required: false,
694 include: subInclude 344 include: subInclude
@@ -696,10 +346,10 @@ export type AvailableForListIDsOptions = {
696 ] 346 ]
697 } 347 }
698 }, 348 },
699 [ ScopeNames.WITH_STREAMING_PLAYLISTS ]: (withRedundancies = false) => { 349 [ScopeNames.WITH_STREAMING_PLAYLISTS]: (withRedundancies = false) => {
700 const subInclude: IncludeOptions[] = [ 350 const subInclude: IncludeOptions[] = [
701 { 351 {
702 model: VideoFileModel.unscoped(), 352 model: VideoFileModel,
703 required: false 353 required: false
704 } 354 }
705 ] 355 ]
@@ -723,7 +373,7 @@ export type AvailableForListIDsOptions = {
723 ] 373 ]
724 } 374 }
725 }, 375 },
726 [ ScopeNames.WITH_SCHEDULED_UPDATE ]: { 376 [ScopeNames.WITH_SCHEDULED_UPDATE]: {
727 include: [ 377 include: [
728 { 378 {
729 model: ScheduleVideoUpdateModel.unscoped(), 379 model: ScheduleVideoUpdateModel.unscoped(),
@@ -731,7 +381,7 @@ export type AvailableForListIDsOptions = {
731 } 381 }
732 ] 382 ]
733 }, 383 },
734 [ ScopeNames.WITH_USER_HISTORY ]: (userId: number) => { 384 [ScopeNames.WITH_USER_HISTORY]: (userId: number) => {
735 return { 385 return {
736 include: [ 386 include: [
737 { 387 {
@@ -748,7 +398,72 @@ export type AvailableForListIDsOptions = {
748})) 398}))
749@Table({ 399@Table({
750 tableName: 'video', 400 tableName: 'video',
751 indexes 401 indexes: [
402 buildTrigramSearchIndex('video_name_trigram', 'name'),
403
404 { fields: [ 'createdAt' ] },
405 {
406 fields: [
407 { name: 'publishedAt', order: 'DESC' },
408 { name: 'id', order: 'ASC' }
409 ]
410 },
411 { fields: [ 'duration' ] },
412 { fields: [ 'views' ] },
413 { fields: [ 'channelId' ] },
414 {
415 fields: [ 'originallyPublishedAt' ],
416 where: {
417 originallyPublishedAt: {
418 [Op.ne]: null
419 }
420 }
421 },
422 {
423 fields: [ 'category' ], // We don't care videos with an unknown category
424 where: {
425 category: {
426 [Op.ne]: null
427 }
428 }
429 },
430 {
431 fields: [ 'licence' ], // We don't care videos with an unknown licence
432 where: {
433 licence: {
434 [Op.ne]: null
435 }
436 }
437 },
438 {
439 fields: [ 'language' ], // We don't care videos with an unknown language
440 where: {
441 language: {
442 [Op.ne]: null
443 }
444 }
445 },
446 {
447 fields: [ 'nsfw' ], // Most of the videos are not NSFW
448 where: {
449 nsfw: true
450 }
451 },
452 {
453 fields: [ 'remote' ], // Only index local videos
454 where: {
455 remote: false
456 }
457 },
458 {
459 fields: [ 'uuid' ],
460 unique: true
461 },
462 {
463 fields: [ 'url' ],
464 unique: true
465 }
466 ]
752}) 467})
753export class VideoModel extends Model<VideoModel> { 468export class VideoModel extends Model<VideoModel> {
754 469
@@ -913,9 +628,9 @@ export class VideoModel extends Model<VideoModel> {
913 @HasMany(() => VideoAbuseModel, { 628 @HasMany(() => VideoAbuseModel, {
914 foreignKey: { 629 foreignKey: {
915 name: 'videoId', 630 name: 'videoId',
916 allowNull: false 631 allowNull: true
917 }, 632 },
918 onDelete: 'cascade' 633 onDelete: 'set null'
919 }) 634 })
920 VideoAbuses: VideoAbuseModel[] 635 VideoAbuses: VideoAbuseModel[]
921 636
@@ -1019,7 +734,7 @@ export class VideoModel extends Model<VideoModel> {
1019 }, 734 },
1020 onDelete: 'cascade', 735 onDelete: 'cascade',
1021 hooks: true, 736 hooks: true,
1022 [ 'separate' as any ]: true 737 ['separate' as any]: true
1023 }) 738 })
1024 VideoCaptions: VideoCaptionModel[] 739 VideoCaptions: VideoCaptionModel[]
1025 740
@@ -1078,6 +793,38 @@ export class VideoModel extends Model<VideoModel> {
1078 return undefined 793 return undefined
1079 } 794 }
1080 795
796 @BeforeDestroy
797 static invalidateCache (instance: VideoModel) {
798 ModelCache.Instance.invalidateCache('video', instance.id)
799 }
800
801 @BeforeDestroy
802 static async saveEssentialDataToAbuses (instance: VideoModel, options) {
803 const tasks: Promise<any>[] = []
804
805 logger.info('Saving video abuses details of video %s.', instance.url)
806
807 if (!Array.isArray(instance.VideoAbuses)) {
808 instance.VideoAbuses = await instance.$get('VideoAbuses')
809
810 if (instance.VideoAbuses.length === 0) return undefined
811 }
812
813 const details = instance.toFormattedDetailsJSON()
814
815 for (const abuse of instance.VideoAbuses) {
816 abuse.deletedVideo = details
817 tasks.push(abuse.save({ transaction: options.transaction }))
818 }
819
820 Promise.all(tasks)
821 .catch(err => {
822 logger.error('Some errors when saving details of video %s in its abuses before destroy hook.', instance.uuid, { err })
823 })
824
825 return undefined
826 }
827
1081 static listLocal (): Bluebird<MVideoWithAllFiles[]> { 828 static listLocal (): Bluebird<MVideoWithAllFiles[]> {
1082 const query = { 829 const query = {
1083 where: { 830 where: {
@@ -1112,19 +859,19 @@ export class VideoModel extends Model<VideoModel> {
1112 distinct: true, 859 distinct: true,
1113 offset: start, 860 offset: start,
1114 limit: count, 861 limit: count,
1115 order: getVideoSort('createdAt', [ 'Tags', 'name', 'ASC' ] as any), // FIXME: sequelize typings 862 order: getVideoSort('-createdAt', [ 'Tags', 'name', 'ASC' ] as any), // FIXME: sequelize typings
1116 where: { 863 where: {
1117 id: { 864 id: {
1118 [ Op.in ]: Sequelize.literal('(' + rawQuery + ')') 865 [Op.in]: Sequelize.literal('(' + rawQuery + ')')
1119 }, 866 },
1120 [ Op.or ]: [ 867 [Op.or]: [
1121 { privacy: VideoPrivacy.PUBLIC }, 868 { privacy: VideoPrivacy.PUBLIC },
1122 { privacy: VideoPrivacy.UNLISTED } 869 { privacy: VideoPrivacy.UNLISTED }
1123 ] 870 ]
1124 }, 871 },
1125 include: [ 872 include: [
1126 { 873 {
1127 attributes: [ 'language' ], 874 attributes: [ 'language', 'fileUrl' ],
1128 model: VideoCaptionModel.unscoped(), 875 model: VideoCaptionModel.unscoped(),
1129 required: false 876 required: false
1130 }, 877 },
@@ -1134,10 +881,10 @@ export class VideoModel extends Model<VideoModel> {
1134 required: false, 881 required: false,
1135 // We only want videos shared by this actor 882 // We only want videos shared by this actor
1136 where: { 883 where: {
1137 [ Op.and ]: [ 884 [Op.and]: [
1138 { 885 {
1139 id: { 886 id: {
1140 [ Op.not ]: null 887 [Op.not]: null
1141 } 888 }
1142 }, 889 },
1143 { 890 {
@@ -1187,8 +934,8 @@ export class VideoModel extends Model<VideoModel> {
1187 // totals: totalVideos + totalVideoShares 934 // totals: totalVideos + totalVideoShares
1188 let totalVideos = 0 935 let totalVideos = 0
1189 let totalVideoShares = 0 936 let totalVideoShares = 0
1190 if (totals[ 0 ]) totalVideos = parseInt(totals[ 0 ].total, 10) 937 if (totals[0]) totalVideos = parseInt(totals[0].total, 10)
1191 if (totals[ 1 ]) totalVideoShares = parseInt(totals[ 1 ].total, 10) 938 if (totals[1]) totalVideoShares = parseInt(totals[1].total, 10)
1192 939
1193 const total = totalVideos + totalVideoShares 940 const total = totalVideos + totalVideoShares
1194 return { 941 return {
@@ -1231,7 +978,7 @@ export class VideoModel extends Model<VideoModel> {
1231 baseQuery = Object.assign(baseQuery, { 978 baseQuery = Object.assign(baseQuery, {
1232 where: { 979 where: {
1233 name: { 980 name: {
1234 [ Op.iLike ]: '%' + search + '%' 981 [Op.iLike]: '%' + search + '%'
1235 } 982 }
1236 } 983 }
1237 }) 984 })
@@ -1261,50 +1008,46 @@ export class VideoModel extends Model<VideoModel> {
1261 } 1008 }
1262 1009
1263 static async listForApi (options: { 1010 static async listForApi (options: {
1264 start: number, 1011 start: number
1265 count: number, 1012 count: number
1266 sort: string, 1013 sort: string
1267 nsfw: boolean, 1014 nsfw: boolean
1268 includeLocalVideos: boolean, 1015 includeLocalVideos: boolean
1269 withFiles: boolean, 1016 withFiles: boolean
1270 categoryOneOf?: number[], 1017 categoryOneOf?: number[]
1271 licenceOneOf?: number[], 1018 licenceOneOf?: number[]
1272 languageOneOf?: string[], 1019 languageOneOf?: string[]
1273 tagsOneOf?: string[], 1020 tagsOneOf?: string[]
1274 tagsAllOf?: string[], 1021 tagsAllOf?: string[]
1275 filter?: VideoFilter, 1022 filter?: VideoFilter
1276 accountId?: number, 1023 accountId?: number
1277 videoChannelId?: number, 1024 videoChannelId?: number
1278 followerActorId?: number 1025 followerActorId?: number
1279 videoPlaylistId?: number, 1026 videoPlaylistId?: number
1280 trendingDays?: number, 1027 trendingDays?: number
1281 user?: MUserAccountId, 1028 user?: MUserAccountId
1282 historyOfUser?: MUserId, 1029 historyOfUser?: MUserId
1283 countVideos?: boolean 1030 countVideos?: boolean
1284 }) { 1031 }) {
1285 if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) { 1032 if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) {
1286 throw new Error('Try to filter all-local but no user has not the see all videos right') 1033 throw new Error('Try to filter all-local but no user has not the see all videos right')
1287 } 1034 }
1288 1035
1289 const query: FindOptions & { where?: null } = { 1036 const trendingDays = options.sort.endsWith('trending')
1290 offset: options.start, 1037 ? CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS
1291 limit: options.count, 1038 : undefined
1292 order: getVideoSort(options.sort)
1293 }
1294
1295 let trendingDays: number
1296 if (options.sort.endsWith('trending')) {
1297 trendingDays = CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS
1298
1299 query.group = 'VideoModel.id'
1300 }
1301 1039
1302 const serverActor = await getServerActor() 1040 const serverActor = await getServerActor()
1303 1041
1304 // followerActorId === null has a meaning, so just check undefined 1042 // followerActorId === null has a meaning, so just check undefined
1305 const followerActorId = options.followerActorId !== undefined ? options.followerActorId : serverActor.id 1043 const followerActorId = options.followerActorId !== undefined
1044 ? options.followerActorId
1045 : serverActor.id
1306 1046
1307 const queryOptions = { 1047 const queryOptions = {
1048 start: options.start,
1049 count: options.count,
1050 sort: options.sort,
1308 followerActorId, 1051 followerActorId,
1309 serverAccountId: serverActor.Account.id, 1052 serverAccountId: serverActor.Account.id,
1310 nsfw: options.nsfw, 1053 nsfw: options.nsfw,
@@ -1324,7 +1067,7 @@ export class VideoModel extends Model<VideoModel> {
1324 trendingDays 1067 trendingDays
1325 } 1068 }
1326 1069
1327 return VideoModel.getAvailableForApi(query, queryOptions, options.countVideos) 1070 return VideoModel.getAvailableForApi(queryOptions, options.countVideos)
1328 } 1071 }
1329 1072
1330 static async searchAndPopulateAccountAndServer (options: { 1073 static async searchAndPopulateAccountAndServer (options: {
@@ -1345,91 +1088,9 @@ export class VideoModel extends Model<VideoModel> {
1345 tagsAllOf?: string[] 1088 tagsAllOf?: string[]
1346 durationMin?: number // seconds 1089 durationMin?: number // seconds
1347 durationMax?: number // seconds 1090 durationMax?: number // seconds
1348 user?: MUserAccountId, 1091 user?: MUserAccountId
1349 filter?: VideoFilter 1092 filter?: VideoFilter
1350 }) { 1093 }) {
1351 const whereAnd = []
1352
1353 if (options.startDate || options.endDate) {
1354 const publishedAtRange = {}
1355
1356 if (options.startDate) publishedAtRange[ Op.gte ] = options.startDate
1357 if (options.endDate) publishedAtRange[ Op.lte ] = options.endDate
1358
1359 whereAnd.push({ publishedAt: publishedAtRange })
1360 }
1361
1362 if (options.originallyPublishedStartDate || options.originallyPublishedEndDate) {
1363 const originallyPublishedAtRange = {}
1364
1365 if (options.originallyPublishedStartDate) originallyPublishedAtRange[ Op.gte ] = options.originallyPublishedStartDate
1366 if (options.originallyPublishedEndDate) originallyPublishedAtRange[ Op.lte ] = options.originallyPublishedEndDate
1367
1368 whereAnd.push({ originallyPublishedAt: originallyPublishedAtRange })
1369 }
1370
1371 if (options.durationMin || options.durationMax) {
1372 const durationRange = {}
1373
1374 if (options.durationMin) durationRange[ Op.gte ] = options.durationMin
1375 if (options.durationMax) durationRange[ Op.lte ] = options.durationMax
1376
1377 whereAnd.push({ duration: durationRange })
1378 }
1379
1380 const attributesInclude = []
1381 const escapedSearch = VideoModel.sequelize.escape(options.search)
1382 const escapedLikeSearch = VideoModel.sequelize.escape('%' + options.search + '%')
1383 if (options.search) {
1384 const trigramSearch = {
1385 id: {
1386 [ Op.in ]: Sequelize.literal(
1387 '(' +
1388 'SELECT "video"."id" FROM "video" ' +
1389 'WHERE ' +
1390 'lower(immutable_unaccent("video"."name")) % lower(immutable_unaccent(' + escapedSearch + ')) OR ' +
1391 'lower(immutable_unaccent("video"."name")) LIKE lower(immutable_unaccent(' + escapedLikeSearch + '))' +
1392 'UNION ALL ' +
1393 'SELECT "video"."id" FROM "video" LEFT JOIN "videoTag" ON "videoTag"."videoId" = "video"."id" ' +
1394 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' +
1395 'WHERE lower("tag"."name") = lower(' + escapedSearch + ')' +
1396 ')'
1397 )
1398 }
1399 }
1400
1401 if (validator.isUUID(options.search)) {
1402 whereAnd.push({
1403 [Op.or]: [
1404 trigramSearch,
1405 {
1406 uuid: options.search
1407 }
1408 ]
1409 })
1410 } else {
1411 whereAnd.push(trigramSearch)
1412 }
1413
1414 attributesInclude.push(createSimilarityAttribute('VideoModel.name', options.search))
1415 }
1416
1417 // Cannot search on similarity if we don't have a search
1418 if (!options.search) {
1419 attributesInclude.push(
1420 Sequelize.literal('0 as similarity')
1421 )
1422 }
1423
1424 const query = {
1425 attributes: {
1426 include: attributesInclude
1427 },
1428 offset: options.start,
1429 limit: options.count,
1430 order: getVideoSort(options.sort)
1431 }
1432
1433 const serverActor = await getServerActor() 1094 const serverActor = await getServerActor()
1434 const queryOptions = { 1095 const queryOptions = {
1435 followerActorId: serverActor.id, 1096 followerActorId: serverActor.id,
@@ -1443,10 +1104,21 @@ export class VideoModel extends Model<VideoModel> {
1443 tagsAllOf: options.tagsAllOf, 1104 tagsAllOf: options.tagsAllOf,
1444 user: options.user, 1105 user: options.user,
1445 filter: options.filter, 1106 filter: options.filter,
1446 baseWhere: whereAnd 1107 start: options.start,
1108 count: options.count,
1109 sort: options.sort,
1110 startDate: options.startDate,
1111 endDate: options.endDate,
1112 originallyPublishedStartDate: options.originallyPublishedStartDate,
1113 originallyPublishedEndDate: options.originallyPublishedEndDate,
1114
1115 durationMin: options.durationMin,
1116 durationMax: options.durationMax,
1117
1118 search: options.search
1447 } 1119 }
1448 1120
1449 return VideoModel.getAvailableForApi(query, queryOptions) 1121 return VideoModel.getAvailableForApi(queryOptions)
1450 } 1122 }
1451 1123
1452 static load (id: number | string, t?: Transaction): Bluebird<MVideoThumbnail> { 1124 static load (id: number | string, t?: Transaction): Bluebird<MVideoThumbnail> {
@@ -1472,6 +1144,24 @@ export class VideoModel extends Model<VideoModel> {
1472 ]).findOne(options) 1144 ]).findOne(options)
1473 } 1145 }
1474 1146
1147 static loadImmutableAttributes (id: number | string, t?: Transaction): Bluebird<MVideoImmutable> {
1148 const fun = () => {
1149 const query = {
1150 where: buildWhereIdOrUUID(id),
1151 transaction: t
1152 }
1153
1154 return VideoModel.scope(ScopeNames.WITH_IMMUTABLE_ATTRIBUTES).findOne(query)
1155 }
1156
1157 return ModelCache.Instance.doCache({
1158 cacheType: 'load-video-immutable-id',
1159 key: '' + id,
1160 deleteKey: 'video',
1161 fun
1162 })
1163 }
1164
1475 static loadWithRights (id: number | string, t?: Transaction): Bluebird<MVideoWithRights> { 1165 static loadWithRights (id: number | string, t?: Transaction): Bluebird<MVideoWithRights> {
1476 const where = buildWhereIdOrUUID(id) 1166 const where = buildWhereIdOrUUID(id)
1477 const options = { 1167 const options = {
@@ -1535,6 +1225,26 @@ export class VideoModel extends Model<VideoModel> {
1535 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(query) 1225 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(query)
1536 } 1226 }
1537 1227
1228 static loadByUrlImmutableAttributes (url: string, transaction?: Transaction): Bluebird<MVideoImmutable> {
1229 const fun = () => {
1230 const query: FindOptions = {
1231 where: {
1232 url
1233 },
1234 transaction
1235 }
1236
1237 return VideoModel.scope(ScopeNames.WITH_IMMUTABLE_ATTRIBUTES).findOne(query)
1238 }
1239
1240 return ModelCache.Instance.doCache({
1241 cacheType: 'load-video-immutable-url',
1242 key: url,
1243 deleteKey: 'video',
1244 fun
1245 })
1246 }
1247
1538 static loadByUrlAndPopulateAccount (url: string, transaction?: Transaction): Bluebird<MVideoAccountLightBlacklistAllFiles> { 1248 static loadByUrlAndPopulateAccount (url: string, transaction?: Transaction): Bluebird<MVideoAccountLightBlacklistAllFiles> {
1539 const query: FindOptions = { 1249 const query: FindOptions = {
1540 where: { 1250 where: {
@@ -1581,8 +1291,8 @@ export class VideoModel extends Model<VideoModel> {
1581 } 1291 }
1582 1292
1583 static loadForGetAPI (parameters: { 1293 static loadForGetAPI (parameters: {
1584 id: number | string, 1294 id: number | string
1585 t?: Transaction, 1295 t?: Transaction
1586 userId?: number 1296 userId?: number
1587 }): Bluebird<MVideoDetails> { 1297 }): Bluebird<MVideoDetails> {
1588 const { id, t, userId } = parameters 1298 const { id, t, userId } = parameters
@@ -1619,16 +1329,25 @@ export class VideoModel extends Model<VideoModel> {
1619 remote: false 1329 remote: false
1620 } 1330 }
1621 }) 1331 })
1622 const totalVideos = await VideoModel.count()
1623 1332
1624 let totalLocalVideoViews = await VideoModel.sum('views', { 1333 let totalLocalVideoViews = await VideoModel.sum('views', {
1625 where: { 1334 where: {
1626 remote: false 1335 remote: false
1627 } 1336 }
1628 }) 1337 })
1338
1629 // Sequelize could return null... 1339 // Sequelize could return null...
1630 if (!totalLocalVideoViews) totalLocalVideoViews = 0 1340 if (!totalLocalVideoViews) totalLocalVideoViews = 0
1631 1341
1342 const { total: totalVideos } = await VideoModel.listForApi({
1343 start: 0,
1344 count: 0,
1345 sort: '-publishedAt',
1346 nsfw: buildNSFWFilter(),
1347 includeLocalVideos: true,
1348 withFiles: false
1349 })
1350
1632 return { 1351 return {
1633 totalLocalVideos, 1352 totalLocalVideos,
1634 totalLocalVideoViews, 1353 totalLocalVideoViews,
@@ -1648,9 +1367,9 @@ export class VideoModel extends Model<VideoModel> {
1648 static checkVideoHasInstanceFollow (videoId: number, followerActorId: number) { 1367 static checkVideoHasInstanceFollow (videoId: number, followerActorId: number) {
1649 // Instances only share videos 1368 // Instances only share videos
1650 const query = 'SELECT 1 FROM "videoShare" ' + 1369 const query = 'SELECT 1 FROM "videoShare" ' +
1651 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' + 1370 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' +
1652 'WHERE "actorFollow"."actorId" = $followerActorId AND "videoShare"."videoId" = $videoId ' + 1371 'WHERE "actorFollow"."actorId" = $followerActorId AND "videoShare"."videoId" = $videoId ' +
1653 'LIMIT 1' 1372 'LIMIT 1'
1654 1373
1655 const options = { 1374 const options = {
1656 type: QueryTypes.SELECT as QueryTypes.SELECT, 1375 type: QueryTypes.SELECT as QueryTypes.SELECT,
@@ -1682,7 +1401,7 @@ export class VideoModel extends Model<VideoModel> {
1682 } 1401 }
1683 1402
1684 return VideoModel.findAll(query) 1403 return VideoModel.findAll(query)
1685 .then(videos => videos.map(v => v.id)) 1404 .then(videos => videos.map(v => v.id))
1686 } 1405 }
1687 1406
1688 // threshold corresponds to how many video the field should have to be returned 1407 // threshold corresponds to how many video the field should have to be returned
@@ -1690,26 +1409,22 @@ export class VideoModel extends Model<VideoModel> {
1690 const serverActor = await getServerActor() 1409 const serverActor = await getServerActor()
1691 const followerActorId = serverActor.id 1410 const followerActorId = serverActor.id
1692 1411
1693 const scopeOptions: AvailableForListIDsOptions = { 1412 const queryOptions: BuildVideosQueryOptions = {
1413 attributes: [ `"${field}"` ],
1414 group: `GROUP BY "${field}"`,
1415 having: `HAVING COUNT("${field}") >= ${threshold}`,
1416 start: 0,
1417 sort: 'random',
1418 count,
1694 serverAccountId: serverActor.Account.id, 1419 serverAccountId: serverActor.Account.id,
1695 followerActorId, 1420 followerActorId,
1696 includeLocalVideos: true, 1421 includeLocalVideos: true
1697 attributesType: 'none' // Don't break aggregation
1698 } 1422 }
1699 1423
1700 const query: FindOptions = { 1424 const { query, replacements } = buildListQuery(VideoModel, queryOptions)
1701 attributes: [ field ],
1702 limit: count,
1703 group: field,
1704 having: Sequelize.where(
1705 Sequelize.fn('COUNT', Sequelize.col(field)), { [ Op.gte ]: threshold }
1706 ),
1707 order: [ (this.sequelize as any).random() ]
1708 }
1709 1425
1710 return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST_IDS, scopeOptions ] }) 1426 return this.sequelize.query<any>(query, { replacements, type: QueryTypes.SELECT })
1711 .findAll(query) 1427 .then(rows => rows.map(r => r[field]))
1712 .then(rows => rows.map(r => r[ field ]))
1713 } 1428 }
1714 1429
1715 static buildTrendingQuery (trendingDays: number) { 1430 static buildTrendingQuery (trendingDays: number) {
@@ -1720,42 +1435,37 @@ export class VideoModel extends Model<VideoModel> {
1720 required: false, 1435 required: false,
1721 where: { 1436 where: {
1722 startDate: { 1437 startDate: {
1723 [ Op.gte ]: new Date(new Date().getTime() - (24 * 3600 * 1000) * trendingDays) 1438 [Op.gte]: new Date(new Date().getTime() - (24 * 3600 * 1000) * trendingDays)
1724 } 1439 }
1725 } 1440 }
1726 } 1441 }
1727 } 1442 }
1728 1443
1729 private static async getAvailableForApi ( 1444 private static async getAvailableForApi (
1730 query: FindOptions & { where?: null }, // Forbid where field in query 1445 options: BuildVideosQueryOptions,
1731 options: AvailableForListIDsOptions,
1732 countVideos = true 1446 countVideos = true
1733 ) { 1447 ) {
1734 const idsScope: ScopeOptions = { 1448 function getCount () {
1735 method: [ 1449 if (countVideos !== true) return Promise.resolve(undefined)
1736 ScopeNames.AVAILABLE_FOR_LIST_IDS, options
1737 ]
1738 }
1739 1450
1740 // Remove trending sort on count, because it uses a group by 1451 const countOptions = Object.assign({}, options, { isCount: true })
1741 const countOptions = Object.assign({}, options, { trendingDays: undefined }) 1452 const { query: queryCount, replacements: replacementsCount } = buildListQuery(VideoModel, countOptions)
1742 const countQuery: CountOptions = Object.assign({}, query, { attributes: undefined, group: undefined }) 1453
1743 const countScope: ScopeOptions = { 1454 return VideoModel.sequelize.query<any>(queryCount, { replacements: replacementsCount, type: QueryTypes.SELECT })
1744 method: [ 1455 .then(rows => rows.length !== 0 ? rows[0].total : 0)
1745 ScopeNames.AVAILABLE_FOR_LIST_IDS, countOptions
1746 ]
1747 } 1456 }
1748 1457
1749 const [ count, rows ] = await Promise.all([ 1458 function getModels () {
1750 countVideos 1459 if (options.count === 0) return Promise.resolve([])
1751 ? VideoModel.scope(countScope).count(countQuery) 1460
1752 : Promise.resolve<number>(undefined), 1461 const { query, replacements, order } = buildListQuery(VideoModel, options)
1462 const queryModels = wrapForAPIResults(query, replacements, options, order)
1753 1463
1754 VideoModel.scope(idsScope) 1464 return VideoModel.sequelize.query<any>(queryModels, { replacements, type: QueryTypes.SELECT, nest: true })
1755 .findAll(Object.assign({}, query, { raw: true })) 1465 .then(rows => VideoModel.buildAPIResult(rows))
1756 .then(rows => rows.map(r => r.id)) 1466 }
1757 .then(ids => VideoModel.loadCompleteVideosForApi(ids, query, options)) 1467
1758 ]) 1468 const [ count, rows ] = await Promise.all([ getCount(), getModels() ])
1759 1469
1760 return { 1470 return {
1761 data: rows, 1471 data: rows,
@@ -1763,37 +1473,113 @@ export class VideoModel extends Model<VideoModel> {
1763 } 1473 }
1764 } 1474 }
1765 1475
1766 private static loadCompleteVideosForApi (ids: number[], query: FindOptions, options: AvailableForListIDsOptions) { 1476 private static buildAPIResult (rows: any[]) {
1767 if (ids.length === 0) return [] 1477 const memo: { [ id: number ]: VideoModel } = {}
1478
1479 const thumbnailsDone = new Set<number>()
1480 const historyDone = new Set<number>()
1481 const videoFilesDone = new Set<number>()
1482
1483 const videos: VideoModel[] = []
1484
1485 const avatarKeys = [ 'id', 'filename', 'fileUrl', 'onDisk', 'createdAt', 'updatedAt' ]
1486 const actorKeys = [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ]
1487 const serverKeys = [ 'id', 'host' ]
1488 const videoFileKeys = [ 'id', 'createdAt', 'updatedAt', 'resolution', 'size', 'extname', 'infoHash', 'fps', 'videoId' ]
1489 const videoKeys = [
1490 'id',
1491 'uuid',
1492 'name',
1493 'category',
1494 'licence',
1495 'language',
1496 'privacy',
1497 'nsfw',
1498 'description',
1499 'support',
1500 'duration',
1501 'views',
1502 'likes',
1503 'dislikes',
1504 'remote',
1505 'url',
1506 'commentsEnabled',
1507 'downloadEnabled',
1508 'waitTranscoding',
1509 'state',
1510 'publishedAt',
1511 'originallyPublishedAt',
1512 'channelId',
1513 'createdAt',
1514 'updatedAt'
1515 ]
1768 1516
1769 const secondQuery: FindOptions = { 1517 function buildActor (rowActor: any) {
1770 offset: 0, 1518 const avatarModel = rowActor.Avatar.id !== null
1771 limit: query.limit, 1519 ? new AvatarModel(pick(rowActor.Avatar, avatarKeys))
1772 attributes: query.attributes, 1520 : null
1773 order: [ // Keep original order 1521
1774 Sequelize.literal( 1522 const serverModel = rowActor.Server.id !== null
1775 ids.map(id => `"VideoModel".id = ${id} DESC`).join(', ') 1523 ? new ServerModel(pick(rowActor.Server, serverKeys))
1776 ) 1524 : null
1777 ]
1778 }
1779 1525
1780 const apiScope: (string | ScopeOptions)[] = [] 1526 const actorModel = new ActorModel(pick(rowActor, actorKeys))
1527 actorModel.Avatar = avatarModel
1528 actorModel.Server = serverModel
1781 1529
1782 if (options.user) { 1530 return actorModel
1783 apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.user.id ] })
1784 } 1531 }
1785 1532
1786 apiScope.push({ 1533 for (const row of rows) {
1787 method: [ 1534 if (!memo[row.id]) {
1788 ScopeNames.FOR_API, { 1535 // Build Channel
1789 ids, 1536 const channel = row.VideoChannel
1790 withFiles: options.withFiles, 1537 const channelModel = new VideoChannelModel(pick(channel, [ 'id', 'name', 'description', 'actorId' ]))
1791 videoPlaylistId: options.videoPlaylistId 1538 channelModel.Actor = buildActor(channel.Actor)
1792 } as ForAPIOptions 1539
1793 ] 1540 const account = row.VideoChannel.Account
1794 }) 1541 const accountModel = new AccountModel(pick(account, [ 'id', 'name' ]))
1542 accountModel.Actor = buildActor(account.Actor)
1543
1544 channelModel.Account = accountModel
1545
1546 const videoModel = new VideoModel(pick(row, videoKeys))
1547 videoModel.VideoChannel = channelModel
1795 1548
1796 return VideoModel.scope(apiScope).findAll(secondQuery) 1549 videoModel.UserVideoHistories = []
1550 videoModel.Thumbnails = []
1551 videoModel.VideoFiles = []
1552
1553 memo[row.id] = videoModel
1554 // Don't take object value to have a sorted array
1555 videos.push(videoModel)
1556 }
1557
1558 const videoModel = memo[row.id]
1559
1560 if (row.userVideoHistory?.id && !historyDone.has(row.userVideoHistory.id)) {
1561 const historyModel = new UserVideoHistoryModel(pick(row.userVideoHistory, [ 'id', 'currentTime' ]))
1562 videoModel.UserVideoHistories.push(historyModel)
1563
1564 historyDone.add(row.userVideoHistory.id)
1565 }
1566
1567 if (row.Thumbnails?.id && !thumbnailsDone.has(row.Thumbnails.id)) {
1568 const thumbnailModel = new ThumbnailModel(pick(row.Thumbnails, [ 'id', 'type', 'filename' ]))
1569 videoModel.Thumbnails.push(thumbnailModel)
1570
1571 thumbnailsDone.add(row.Thumbnails.id)
1572 }
1573
1574 if (row.VideoFiles?.id && !videoFilesDone.has(row.VideoFiles.id)) {
1575 const videoFileModel = new VideoFileModel(pick(row.VideoFiles, videoFileKeys))
1576 videoModel.VideoFiles.push(videoFileModel)
1577
1578 videoFilesDone.add(row.VideoFiles.id)
1579 }
1580 }
1581
1582 return videos
1797 } 1583 }
1798 1584
1799 private static isPrivacyForFederation (privacy: VideoPrivacy) { 1585 private static isPrivacyForFederation (privacy: VideoPrivacy) {
@@ -1803,23 +1589,23 @@ export class VideoModel extends Model<VideoModel> {
1803 } 1589 }
1804 1590
1805 static getCategoryLabel (id: number) { 1591 static getCategoryLabel (id: number) {
1806 return VIDEO_CATEGORIES[ id ] || 'Misc' 1592 return VIDEO_CATEGORIES[id] || 'Misc'
1807 } 1593 }
1808 1594
1809 static getLicenceLabel (id: number) { 1595 static getLicenceLabel (id: number) {
1810 return VIDEO_LICENCES[ id ] || 'Unknown' 1596 return VIDEO_LICENCES[id] || 'Unknown'
1811 } 1597 }
1812 1598
1813 static getLanguageLabel (id: string) { 1599 static getLanguageLabel (id: string) {
1814 return VIDEO_LANGUAGES[ id ] || 'Unknown' 1600 return VIDEO_LANGUAGES[id] || 'Unknown'
1815 } 1601 }
1816 1602
1817 static getPrivacyLabel (id: number) { 1603 static getPrivacyLabel (id: number) {
1818 return VIDEO_PRIVACIES[ id ] || 'Unknown' 1604 return VIDEO_PRIVACIES[id] || 'Unknown'
1819 } 1605 }
1820 1606
1821 static getStateLabel (id: number) { 1607 static getStateLabel (id: number) {
1822 return VIDEO_STATES[ id ] || 'Unknown' 1608 return VIDEO_STATES[id] || 'Unknown'
1823 } 1609 }
1824 1610
1825 isBlacklisted () { 1611 isBlacklisted () {
@@ -1831,7 +1617,7 @@ export class VideoModel extends Model<VideoModel> {
1831 this.VideoChannel.Account.isBlocked() 1617 this.VideoChannel.Account.isBlocked()
1832 } 1618 }
1833 1619
1834 getQualityFileBy <T extends MVideoWithFile> (this: T, fun: (files: MVideoFile[], it: (file: MVideoFile) => number) => MVideoFile) { 1620 getQualityFileBy<T extends MVideoWithFile> (this: T, fun: (files: MVideoFile[], it: (file: MVideoFile) => number) => MVideoFile) {
1835 if (Array.isArray(this.VideoFiles) && this.VideoFiles.length !== 0) { 1621 if (Array.isArray(this.VideoFiles) && this.VideoFiles.length !== 0) {
1836 const file = fun(this.VideoFiles, file => file.resolution) 1622 const file = fun(this.VideoFiles, file => file.resolution)
1837 1623
@@ -1849,15 +1635,15 @@ export class VideoModel extends Model<VideoModel> {
1849 return undefined 1635 return undefined
1850 } 1636 }
1851 1637
1852 getMaxQualityFile <T extends MVideoWithFile> (this: T): MVideoFileVideo | MVideoFileStreamingPlaylistVideo { 1638 getMaxQualityFile<T extends MVideoWithFile> (this: T): MVideoFileVideo | MVideoFileStreamingPlaylistVideo {
1853 return this.getQualityFileBy(maxBy) 1639 return this.getQualityFileBy(maxBy)
1854 } 1640 }
1855 1641
1856 getMinQualityFile <T extends MVideoWithFile> (this: T): MVideoFileVideo | MVideoFileStreamingPlaylistVideo { 1642 getMinQualityFile<T extends MVideoWithFile> (this: T): MVideoFileVideo | MVideoFileStreamingPlaylistVideo {
1857 return this.getQualityFileBy(minBy) 1643 return this.getQualityFileBy(minBy)
1858 } 1644 }
1859 1645
1860 getWebTorrentFile <T extends MVideoWithFile> (this: T, resolution: number): MVideoFileVideo { 1646 getWebTorrentFile<T extends MVideoWithFile> (this: T, resolution: number): MVideoFileVideo {
1861 if (Array.isArray(this.VideoFiles) === false) return undefined 1647 if (Array.isArray(this.VideoFiles) === false) return undefined
1862 1648
1863 const file = this.VideoFiles.find(f => f.resolution === resolution) 1649 const file = this.VideoFiles.find(f => f.resolution === resolution)
@@ -1893,6 +1679,10 @@ export class VideoModel extends Model<VideoModel> {
1893 return this.uuid + '.jpg' 1679 return this.uuid + '.jpg'
1894 } 1680 }
1895 1681
1682 hasPreview () {
1683 return !!this.getPreview()
1684 }
1685
1896 getPreview () { 1686 getPreview () {
1897 if (Array.isArray(this.Thumbnails) === false) return undefined 1687 if (Array.isArray(this.Thumbnails) === false) return undefined
1898 1688
@@ -1980,8 +1770,8 @@ export class VideoModel extends Model<VideoModel> {
1980 } 1770 }
1981 1771
1982 this.VideoStreamingPlaylists = this.VideoStreamingPlaylists 1772 this.VideoStreamingPlaylists = this.VideoStreamingPlaylists
1983 .filter(s => s.type !== VideoStreamingPlaylistType.HLS) 1773 .filter(s => s.type !== VideoStreamingPlaylistType.HLS)
1984 .concat(toAdd) 1774 .concat(toAdd)
1985 } 1775 }
1986 1776
1987 removeFile (videoFile: MVideoFile, isRedundancy = false) { 1777 removeFile (videoFile: MVideoFile, isRedundancy = false) {
@@ -2002,7 +1792,7 @@ export class VideoModel extends Model<VideoModel> {
2002 await remove(directoryPath) 1792 await remove(directoryPath)
2003 1793
2004 if (isRedundancy !== true) { 1794 if (isRedundancy !== true) {
2005 let streamingPlaylistWithFiles = streamingPlaylist as MStreamingPlaylistFilesVideo 1795 const streamingPlaylistWithFiles = streamingPlaylist as MStreamingPlaylistFilesVideo
2006 streamingPlaylistWithFiles.Video = this 1796 streamingPlaylistWithFiles.Video = this
2007 1797
2008 if (!Array.isArray(streamingPlaylistWithFiles.VideoFiles)) { 1798 if (!Array.isArray(streamingPlaylistWithFiles.VideoFiles)) {
@@ -2096,6 +1886,14 @@ export class VideoModel extends Model<VideoModel> {
2096 return baseUrlHttp + STATIC_PATHS.WEBSEED + getVideoFilename(this, videoFile) 1886 return baseUrlHttp + STATIC_PATHS.WEBSEED + getVideoFilename(this, videoFile)
2097 } 1887 }
2098 1888
1889 getVideoFileMetadataUrl (videoFile: MVideoFile, baseUrlHttp: string) {
1890 const path = '/api/v1/videos/'
1891
1892 return this.isOwned()
1893 ? baseUrlHttp + path + this.uuid + '/metadata/' + videoFile.id
1894 : videoFile.metadataUrl
1895 }
1896
2099 getVideoRedundancyUrl (videoFile: MVideoFile, baseUrlHttp: string) { 1897 getVideoRedundancyUrl (videoFile: MVideoFile, baseUrlHttp: string) {
2100 return baseUrlHttp + STATIC_PATHS.REDUNDANCY + getVideoFilename(this, videoFile) 1898 return baseUrlHttp + STATIC_PATHS.REDUNDANCY + getVideoFilename(this, videoFile)
2101 } 1899 }
diff --git a/server/tests/api/activitypub/client.ts b/server/tests/api/activitypub/client.ts
index 34c6be49b..d16f05108 100644
--- a/server/tests/api/activitypub/client.ts
+++ b/server/tests/api/activitypub/client.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
@@ -6,8 +6,6 @@ import {
6 cleanupTests, 6 cleanupTests,
7 doubleFollow, 7 doubleFollow,
8 flushAndRunMultipleServers, 8 flushAndRunMultipleServers,
9 flushTests,
10 killallServers,
11 makeActivityPubGetRequest, 9 makeActivityPubGetRequest,
12 ServerInfo, 10 ServerInfo,
13 setAccessTokensToServers, 11 setAccessTokensToServers,
diff --git a/server/tests/api/activitypub/fetch.ts b/server/tests/api/activitypub/fetch.ts
index 3d54c2042..35fd94eed 100644
--- a/server/tests/api/activitypub/fetch.ts
+++ b/server/tests/api/activitypub/fetch.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4 4
@@ -8,9 +8,7 @@ import {
8 createUser, 8 createUser,
9 doubleFollow, 9 doubleFollow,
10 flushAndRunMultipleServers, 10 flushAndRunMultipleServers,
11 flushTests,
12 getVideosListSort, 11 getVideosListSort,
13 killallServers,
14 ServerInfo, 12 ServerInfo,
15 setAccessTokensToServers, 13 setAccessTokensToServers,
16 setActorField, 14 setActorField,
diff --git a/server/tests/api/activitypub/helpers.ts b/server/tests/api/activitypub/helpers.ts
index 8c00ba3d6..60d95b823 100644
--- a/server/tests/api/activitypub/helpers.ts
+++ b/server/tests/api/activitypub/helpers.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4import { expect } from 'chai' 4import { expect } from 'chai'
diff --git a/server/tests/api/activitypub/refresher.ts b/server/tests/api/activitypub/refresher.ts
index aa4bc6c0f..232c5d823 100644
--- a/server/tests/api/activitypub/refresher.ts
+++ b/server/tests/api/activitypub/refresher.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4import { 4import {
@@ -43,32 +43,32 @@ describe('Test AP refresher', function () {
43 await setDefaultVideoChannel(servers) 43 await setDefaultVideoChannel(servers)
44 44
45 { 45 {
46 videoUUID1 = (await uploadVideoAndGetId({ server: servers[ 1 ], videoName: 'video1' })).uuid 46 videoUUID1 = (await uploadVideoAndGetId({ server: servers[1], videoName: 'video1' })).uuid
47 videoUUID2 = (await uploadVideoAndGetId({ server: servers[ 1 ], videoName: 'video2' })).uuid 47 videoUUID2 = (await uploadVideoAndGetId({ server: servers[1], videoName: 'video2' })).uuid
48 videoUUID3 = (await uploadVideoAndGetId({ server: servers[ 1 ], videoName: 'video3' })).uuid 48 videoUUID3 = (await uploadVideoAndGetId({ server: servers[1], videoName: 'video3' })).uuid
49 } 49 }
50 50
51 { 51 {
52 const a1 = await generateUserAccessToken(servers[ 1 ], 'user1') 52 const a1 = await generateUserAccessToken(servers[1], 'user1')
53 await uploadVideo(servers[ 1 ].url, a1, { name: 'video4' }) 53 await uploadVideo(servers[1].url, a1, { name: 'video4' })
54 54
55 const a2 = await generateUserAccessToken(servers[ 1 ], 'user2') 55 const a2 = await generateUserAccessToken(servers[1], 'user2')
56 await uploadVideo(servers[ 1 ].url, a2, { name: 'video5' }) 56 await uploadVideo(servers[1].url, a2, { name: 'video5' })
57 } 57 }
58 58
59 { 59 {
60 const playlistAttrs = { displayName: 'playlist1', privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: servers[ 1 ].videoChannel.id } 60 const playlistAttrs = { displayName: 'playlist1', privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: servers[1].videoChannel.id }
61 const res = await createVideoPlaylist({ url: servers[ 1 ].url, token: servers[ 1 ].accessToken, playlistAttrs }) 61 const res = await createVideoPlaylist({ url: servers[1].url, token: servers[1].accessToken, playlistAttrs })
62 playlistUUID1 = res.body.videoPlaylist.uuid 62 playlistUUID1 = res.body.videoPlaylist.uuid
63 } 63 }
64 64
65 { 65 {
66 const playlistAttrs = { displayName: 'playlist2', privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: servers[ 1 ].videoChannel.id } 66 const playlistAttrs = { displayName: 'playlist2', privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: servers[1].videoChannel.id }
67 const res = await createVideoPlaylist({ url: servers[ 1 ].url, token: servers[ 1 ].accessToken, playlistAttrs }) 67 const res = await createVideoPlaylist({ url: servers[1].url, token: servers[1].accessToken, playlistAttrs })
68 playlistUUID2 = res.body.videoPlaylist.uuid 68 playlistUUID2 = res.body.videoPlaylist.uuid
69 } 69 }
70 70
71 await doubleFollow(servers[ 0 ], servers[ 1 ]) 71 await doubleFollow(servers[0], servers[1])
72 }) 72 })
73 73
74 describe('Videos refresher', function () { 74 describe('Videos refresher', function () {
@@ -79,34 +79,34 @@ describe('Test AP refresher', function () {
79 await wait(10000) 79 await wait(10000)
80 80
81 // Change UUID so the remote server returns a 404 81 // Change UUID so the remote server returns a 404
82 await setVideoField(servers[ 1 ].internalServerNumber, videoUUID1, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b174f') 82 await setVideoField(servers[1].internalServerNumber, videoUUID1, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b174f')
83 83
84 await getVideo(servers[ 0 ].url, videoUUID1) 84 await getVideo(servers[0].url, videoUUID1)
85 await getVideo(servers[ 0 ].url, videoUUID2) 85 await getVideo(servers[0].url, videoUUID2)
86 86
87 await waitJobs(servers) 87 await waitJobs(servers)
88 88
89 await getVideo(servers[ 0 ].url, videoUUID1, 404) 89 await getVideo(servers[0].url, videoUUID1, 404)
90 await getVideo(servers[ 0 ].url, videoUUID2, 200) 90 await getVideo(servers[0].url, videoUUID2, 200)
91 }) 91 })
92 92
93 it('Should not update a remote video if the remote instance is down', async function () { 93 it('Should not update a remote video if the remote instance is down', async function () {
94 this.timeout(70000) 94 this.timeout(70000)
95 95
96 killallServers([ servers[ 1 ] ]) 96 killallServers([ servers[1] ])
97 97
98 await setVideoField(servers[ 1 ].internalServerNumber, videoUUID3, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b174e') 98 await setVideoField(servers[1].internalServerNumber, videoUUID3, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b174e')
99 99
100 // Video will need a refresh 100 // Video will need a refresh
101 await wait(10000) 101 await wait(10000)
102 102
103 await getVideo(servers[ 0 ].url, videoUUID3) 103 await getVideo(servers[0].url, videoUUID3)
104 // The refresh should fail 104 // The refresh should fail
105 await waitJobs([ servers[ 0 ] ]) 105 await waitJobs([ servers[0] ])
106 106
107 await reRunServer(servers[ 1 ]) 107 await reRunServer(servers[1])
108 108
109 await getVideo(servers[ 0 ].url, videoUUID3, 200) 109 await getVideo(servers[0].url, videoUUID3, 200)
110 }) 110 })
111 }) 111 })
112 112
@@ -118,16 +118,16 @@ describe('Test AP refresher', function () {
118 await wait(10000) 118 await wait(10000)
119 119
120 // Change actor name so the remote server returns a 404 120 // Change actor name so the remote server returns a 404
121 const to = 'http://localhost:' + servers[ 1 ].port + '/accounts/user2' 121 const to = 'http://localhost:' + servers[1].port + '/accounts/user2'
122 await setActorField(servers[ 1 ].internalServerNumber, to, 'preferredUsername', 'toto') 122 await setActorField(servers[1].internalServerNumber, to, 'preferredUsername', 'toto')
123 123
124 await getAccount(servers[ 0 ].url, 'user1@localhost:' + servers[ 1 ].port) 124 await getAccount(servers[0].url, 'user1@localhost:' + servers[1].port)
125 await getAccount(servers[ 0 ].url, 'user2@localhost:' + servers[ 1 ].port) 125 await getAccount(servers[0].url, 'user2@localhost:' + servers[1].port)
126 126
127 await waitJobs(servers) 127 await waitJobs(servers)
128 128
129 await getAccount(servers[ 0 ].url, 'user1@localhost:' + servers[ 1 ].port, 200) 129 await getAccount(servers[0].url, 'user1@localhost:' + servers[1].port, 200)
130 await getAccount(servers[ 0 ].url, 'user2@localhost:' + servers[ 1 ].port, 404) 130 await getAccount(servers[0].url, 'user2@localhost:' + servers[1].port, 404)
131 }) 131 })
132 }) 132 })
133 133
@@ -139,15 +139,15 @@ describe('Test AP refresher', function () {
139 await wait(10000) 139 await wait(10000)
140 140
141 // Change UUID so the remote server returns a 404 141 // Change UUID so the remote server returns a 404
142 await setPlaylistField(servers[ 1 ].internalServerNumber, playlistUUID2, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b178e') 142 await setPlaylistField(servers[1].internalServerNumber, playlistUUID2, 'uuid', '304afe4f-39f9-4d49-8ed7-ac57b86b178e')
143 143
144 await getVideoPlaylist(servers[ 0 ].url, playlistUUID1) 144 await getVideoPlaylist(servers[0].url, playlistUUID1)
145 await getVideoPlaylist(servers[ 0 ].url, playlistUUID2) 145 await getVideoPlaylist(servers[0].url, playlistUUID2)
146 146
147 await waitJobs(servers) 147 await waitJobs(servers)
148 148
149 await getVideoPlaylist(servers[ 0 ].url, playlistUUID1, 200) 149 await getVideoPlaylist(servers[0].url, playlistUUID1, 200)
150 await getVideoPlaylist(servers[ 0 ].url, playlistUUID2, 404) 150 await getVideoPlaylist(servers[0].url, playlistUUID2, 404)
151 }) 151 })
152 }) 152 })
153 153
diff --git a/server/tests/api/activitypub/security.ts b/server/tests/api/activitypub/security.ts
index dc960c5c3..ac4bc7c6a 100644
--- a/server/tests/api/activitypub/security.ts
+++ b/server/tests/api/activitypub/security.ts
@@ -1,20 +1,14 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4 4
5import { 5import { cleanupTests, closeAllSequelize, flushAndRunMultipleServers, ServerInfo, setActorField } from '../../../../shared/extra-utils'
6 cleanupTests,
7 closeAllSequelize,
8 flushAndRunMultipleServers,
9 killallServers,
10 ServerInfo,
11 setActorField
12} from '../../../../shared/extra-utils'
13import { HTTP_SIGNATURE } from '../../../initializers/constants' 6import { HTTP_SIGNATURE } from '../../../initializers/constants'
14import { buildDigest, buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils' 7import { buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils'
15import * as chai from 'chai' 8import * as chai from 'chai'
16import { activityPubContextify, buildSignedActivity } from '../../../helpers/activitypub' 9import { activityPubContextify, buildSignedActivity } from '../../../helpers/activitypub'
17import { makeFollowRequest, makePOSTAPRequest } from '../../../../shared/extra-utils/requests/activitypub' 10import { makeFollowRequest, makePOSTAPRequest } from '../../../../shared/extra-utils/requests/activitypub'
11import { buildDigest } from '@server/helpers/peertube-crypto'
18 12
19const expect = chai.expect 13const expect = chai.expect
20 14
@@ -33,7 +27,7 @@ function getAnnounceWithoutContext (server2: ServerInfo) {
33 if (Array.isArray(json[key])) { 27 if (Array.isArray(json[key])) {
34 result[key] = json[key].map(v => v.replace(':9002', `:${server2.port}`)) 28 result[key] = json[key].map(v => v.replace(':9002', `:${server2.port}`))
35 } else { 29 } else {
36 result[ key ] = json[ key ].replace(':9002', `:${server2.port}`) 30 result[key] = json[key].replace(':9002', `:${server2.port}`)
37 } 31 }
38 } 32 }
39 33
diff --git a/server/tests/api/check-params/accounts.ts b/server/tests/api/check-params/accounts.ts
index 4f79685bd..c29af7cd7 100644
--- a/server/tests/api/check-params/accounts.ts
+++ b/server/tests/api/check-params/accounts.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4 4
diff --git a/server/tests/api/check-params/blocklist.ts b/server/tests/api/check-params/blocklist.ts
index 0661676ce..1219ec9bd 100644
--- a/server/tests/api/check-params/blocklist.ts
+++ b/server/tests/api/check-params/blocklist.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4 4
@@ -175,13 +175,13 @@ describe('Test blocklist API validators', function () {
175 }) 175 })
176 }) 176 })
177 177
178 it('Should fail with an unknown server', async function () { 178 it('Should succeed with an unknown server', async function () {
179 await makePostBodyRequest({ 179 await makePostBodyRequest({
180 url: server.url, 180 url: server.url,
181 token: server.accessToken, 181 token: server.accessToken,
182 path, 182 path,
183 fields: { host: 'localhost:9003' }, 183 fields: { host: 'localhost:9003' },
184 statusCodeExpected: 404 184 statusCodeExpected: 204
185 }) 185 })
186 }) 186 })
187 187
@@ -218,7 +218,7 @@ describe('Test blocklist API validators', function () {
218 it('Should fail with an unknown server block', async function () { 218 it('Should fail with an unknown server block', async function () {
219 await makeDeleteRequest({ 219 await makeDeleteRequest({
220 url: server.url, 220 url: server.url,
221 path: path + '/localhost:9003', 221 path: path + '/localhost:9004',
222 token: server.accessToken, 222 token: server.accessToken,
223 statusCodeExpected: 404 223 statusCodeExpected: 404
224 }) 224 })
@@ -415,13 +415,13 @@ describe('Test blocklist API validators', function () {
415 }) 415 })
416 }) 416 })
417 417
418 it('Should fail with an unknown server', async function () { 418 it('Should succeed with an unknown server', async function () {
419 await makePostBodyRequest({ 419 await makePostBodyRequest({
420 url: server.url, 420 url: server.url,
421 token: server.accessToken, 421 token: server.accessToken,
422 path, 422 path,
423 fields: { host: 'localhost:9003' }, 423 fields: { host: 'localhost:9003' },
424 statusCodeExpected: 404 424 statusCodeExpected: 204
425 }) 425 })
426 }) 426 })
427 427
@@ -467,7 +467,7 @@ describe('Test blocklist API validators', function () {
467 it('Should fail with an unknown server block', async function () { 467 it('Should fail with an unknown server block', async function () {
468 await makeDeleteRequest({ 468 await makeDeleteRequest({
469 url: server.url, 469 url: server.url,
470 path: path + '/localhost:9003', 470 path: path + '/localhost:9004',
471 token: server.accessToken, 471 token: server.accessToken,
472 statusCodeExpected: 404 472 statusCodeExpected: 404
473 }) 473 })
diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts
index 443fbcb60..f1a79806b 100644
--- a/server/tests/api/check-params/config.ts
+++ b/server/tests/api/check-params/config.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import { omit } from 'lodash' 3import { omit } from 'lodash'
4import 'mocha' 4import 'mocha'
diff --git a/server/tests/api/check-params/contact-form.ts b/server/tests/api/check-params/contact-form.ts
index b3051945e..b2126b9b0 100644
--- a/server/tests/api/check-params/contact-form.ts
+++ b/server/tests/api/check-params/contact-form.ts
@@ -1,22 +1,8 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4 4
5import { 5import { cleanupTests, flushAndRunServer, immutableAssign, killallServers, reRunServer, ServerInfo } from '../../../../shared/extra-utils'
6 flushTests,
7 immutableAssign,
8 killallServers,
9 reRunServer,
10 flushAndRunServer,
11 ServerInfo,
12 setAccessTokensToServers, cleanupTests
13} from '../../../../shared/extra-utils'
14import {
15 checkBadCountPagination,
16 checkBadSortPagination,
17 checkBadStartPagination
18} from '../../../../shared/extra-utils/requests/check-api-params'
19import { getAccount } from '../../../../shared/extra-utils/users/accounts'
20import { sendContactForm } from '../../../../shared/extra-utils/server/contact-form' 6import { sendContactForm } from '../../../../shared/extra-utils/server/contact-form'
21import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email' 7import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email'
22 8
diff --git a/server/tests/api/check-params/debug.ts b/server/tests/api/check-params/debug.ts
index 8dad26723..5fac73485 100644
--- a/server/tests/api/check-params/debug.ts
+++ b/server/tests/api/check-params/debug.ts
@@ -1,15 +1,14 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4 4
5import { 5import {
6 cleanupTests,
6 createUser, 7 createUser,
7 flushTests,
8 killallServers,
9 flushAndRunServer, 8 flushAndRunServer,
10 ServerInfo, 9 ServerInfo,
11 setAccessTokensToServers, 10 setAccessTokensToServers,
12 userLogin, cleanupTests 11 userLogin
13} from '../../../../shared/extra-utils' 12} from '../../../../shared/extra-utils'
14import { makeGetRequest } from '../../../../shared/extra-utils/requests/requests' 13import { makeGetRequest } from '../../../../shared/extra-utils/requests/requests'
15 14
diff --git a/server/tests/api/check-params/follows.ts b/server/tests/api/check-params/follows.ts
index be2a603a3..2c2224a45 100644
--- a/server/tests/api/check-params/follows.ts
+++ b/server/tests/api/check-params/follows.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4 4
diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts
index 924c0df76..ef152f55c 100644
--- a/server/tests/api/check-params/index.ts
+++ b/server/tests/api/check-params/index.ts
@@ -23,3 +23,4 @@ import './video-playlists'
23import './videos' 23import './videos'
24import './videos-filter' 24import './videos-filter'
25import './videos-history' 25import './videos-history'
26import './videos-overviews'
diff --git a/server/tests/api/check-params/jobs.ts b/server/tests/api/check-params/jobs.ts
index 22e237964..8f4af8d16 100644
--- a/server/tests/api/check-params/jobs.ts
+++ b/server/tests/api/check-params/jobs.ts
@@ -1,16 +1,14 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4 4
5import { 5import {
6 cleanupTests,
6 createUser, 7 createUser,
7 flushTests,
8 killallServers,
9 flushAndRunServer, 8 flushAndRunServer,
10 ServerInfo, 9 ServerInfo,
11 setAccessTokensToServers, 10 setAccessTokensToServers,
12 userLogin, 11 userLogin
13 cleanupTests
14} from '../../../../shared/extra-utils' 12} from '../../../../shared/extra-utils'
15import { 13import {
16 checkBadCountPagination, 14 checkBadCountPagination,
diff --git a/server/tests/api/check-params/logs.ts b/server/tests/api/check-params/logs.ts
index f9d96bcc0..719da54e6 100644
--- a/server/tests/api/check-params/logs.ts
+++ b/server/tests/api/check-params/logs.ts
@@ -1,16 +1,14 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4 4
5import { 5import {
6 cleanupTests,
6 createUser, 7 createUser,
7 flushTests,
8 killallServers,
9 flushAndRunServer, 8 flushAndRunServer,
10 ServerInfo, 9 ServerInfo,
11 setAccessTokensToServers, 10 setAccessTokensToServers,
12 userLogin, 11 userLogin
13 cleanupTests
14} from '../../../../shared/extra-utils' 12} from '../../../../shared/extra-utils'
15import { makeGetRequest } from '../../../../shared/extra-utils/requests/requests' 13import { makeGetRequest } from '../../../../shared/extra-utils/requests/requests'
16 14
diff --git a/server/tests/api/check-params/plugins.ts b/server/tests/api/check-params/plugins.ts
index 9553bce17..07ded26ee 100644
--- a/server/tests/api/check-params/plugins.ts
+++ b/server/tests/api/check-params/plugins.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4 4
@@ -64,6 +64,7 @@ describe('Test server plugins API validators', function () {
64 describe('With static plugin routes', function () { 64 describe('With static plugin routes', function () {
65 it('Should fail with an unknown plugin name/plugin version', async function () { 65 it('Should fail with an unknown plugin name/plugin version', async function () {
66 const paths = [ 66 const paths = [
67 '/plugins/' + pluginName + '/0.0.1/auth/fake-auth',
67 '/plugins/' + pluginName + '/0.0.1/static/images/chocobo.png', 68 '/plugins/' + pluginName + '/0.0.1/static/images/chocobo.png',
68 '/plugins/' + pluginName + '/0.0.1/client-scripts/client/common-client-plugin.js', 69 '/plugins/' + pluginName + '/0.0.1/client-scripts/client/common-client-plugin.js',
69 '/themes/' + themeName + '/0.0.1/static/images/chocobo.png', 70 '/themes/' + themeName + '/0.0.1/static/images/chocobo.png',
@@ -86,6 +87,7 @@ describe('Test server plugins API validators', function () {
86 87
87 it('Should fail with invalid versions', async function () { 88 it('Should fail with invalid versions', async function () {
88 const paths = [ 89 const paths = [
90 '/plugins/' + pluginName + '/0.0.1.1/auth/fake-auth',
89 '/plugins/' + pluginName + '/0.0.1.1/static/images/chocobo.png', 91 '/plugins/' + pluginName + '/0.0.1.1/static/images/chocobo.png',
90 '/plugins/' + pluginName + '/0.1/client-scripts/client/common-client-plugin.js', 92 '/plugins/' + pluginName + '/0.1/client-scripts/client/common-client-plugin.js',
91 '/themes/' + themeName + '/1/static/images/chocobo.png', 93 '/themes/' + themeName + '/1/static/images/chocobo.png',
@@ -112,6 +114,12 @@ describe('Test server plugins API validators', function () {
112 } 114 }
113 }) 115 })
114 116
117 it('Should fail with an unknown auth name', async function () {
118 const path = '/plugins/' + pluginName + '/' + npmVersion + '/auth/bad-auth'
119
120 await makeGetRequest({ url: server.url, path, statusCodeExpected: 404 })
121 })
122
115 it('Should fail with an unknown static file', async function () { 123 it('Should fail with an unknown static file', async function () {
116 const paths = [ 124 const paths = [
117 '/plugins/' + pluginName + '/' + npmVersion + '/static/fake/chocobo.png', 125 '/plugins/' + pluginName + '/' + npmVersion + '/static/fake/chocobo.png',
@@ -145,6 +153,9 @@ describe('Test server plugins API validators', function () {
145 for (const p of paths) { 153 for (const p of paths) {
146 await makeGetRequest({ url: server.url, path: p, statusCodeExpected: 200 }) 154 await makeGetRequest({ url: server.url, path: p, statusCodeExpected: 200 })
147 } 155 }
156
157 const authPath = '/plugins/' + pluginName + '/' + npmVersion + '/auth/fake-auth'
158 await makeGetRequest({ url: server.url, path: authPath, statusCodeExpected: 302 })
148 }) 159 })
149 }) 160 })
150 161
@@ -462,6 +473,8 @@ describe('Test server plugins API validators', function () {
462 }) 473 })
463 474
464 it('Should succeed with the correct parameters', async function () { 475 it('Should succeed with the correct parameters', async function () {
476 this.timeout(10000)
477
465 const it = [ 478 const it = [
466 { suffix: 'install', status: 200 }, 479 { suffix: 'install', status: 200 },
467 { suffix: 'update', status: 200 }, 480 { suffix: 'update', status: 200 },
diff --git a/server/tests/api/check-params/redundancy.ts b/server/tests/api/check-params/redundancy.ts
index 6471da840..b2370a094 100644
--- a/server/tests/api/check-params/redundancy.ts
+++ b/server/tests/api/check-params/redundancy.ts
@@ -1,23 +1,27 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4 4
5import { 5import {
6 checkBadCountPagination,
7 checkBadSortPagination,
8 checkBadStartPagination,
6 cleanupTests, 9 cleanupTests,
7 createUser, 10 createUser,
8 doubleFollow, 11 doubleFollow,
9 flushAndRunMultipleServers, 12 flushAndRunMultipleServers, makeDeleteRequest,
10 flushTests, 13 makeGetRequest, makePostBodyRequest,
11 killallServers,
12 makePutBodyRequest, 14 makePutBodyRequest,
13 ServerInfo, 15 ServerInfo,
14 setAccessTokensToServers, 16 setAccessTokensToServers, uploadVideoAndGetId,
15 userLogin 17 userLogin, waitJobs
16} from '../../../../shared/extra-utils' 18} from '../../../../shared/extra-utils'
17 19
18describe('Test server redundancy API validators', function () { 20describe('Test server redundancy API validators', function () {
19 let servers: ServerInfo[] 21 let servers: ServerInfo[]
20 let userAccessToken = null 22 let userAccessToken = null
23 let videoIdLocal: number
24 let videoIdRemote: number
21 25
22 // --------------------------------------------------------------- 26 // ---------------------------------------------------------------
23 27
@@ -34,11 +38,136 @@ describe('Test server redundancy API validators', function () {
34 password: 'password' 38 password: 'password'
35 } 39 }
36 40
37 await createUser({ url: servers[ 0 ].url, accessToken: servers[ 0 ].accessToken, username: user.username, password: user.password }) 41 await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: user.username, password: user.password })
38 userAccessToken = await userLogin(servers[0], user) 42 userAccessToken = await userLogin(servers[0], user)
43
44 videoIdLocal = (await uploadVideoAndGetId({ server: servers[0], videoName: 'video' })).id
45 videoIdRemote = (await uploadVideoAndGetId({ server: servers[1], videoName: 'video' })).id
46
47 await waitJobs(servers)
48 })
49
50 describe('When listing redundancies', function () {
51 const path = '/api/v1/server/redundancy/videos'
52
53 let url: string
54 let token: string
55
56 before(function () {
57 url = servers[0].url
58 token = servers[0].accessToken
59 })
60
61 it('Should fail with an invalid token', async function () {
62 await makeGetRequest({ url, path, token: 'fake_token', statusCodeExpected: 401 })
63 })
64
65 it('Should fail if the user is not an administrator', async function () {
66 await makeGetRequest({ url, path, token: userAccessToken, statusCodeExpected: 403 })
67 })
68
69 it('Should fail with a bad start pagination', async function () {
70 await checkBadStartPagination(url, path, servers[0].accessToken)
71 })
72
73 it('Should fail with a bad count pagination', async function () {
74 await checkBadCountPagination(url, path, servers[0].accessToken)
75 })
76
77 it('Should fail with an incorrect sort', async function () {
78 await checkBadSortPagination(url, path, servers[0].accessToken)
79 })
80
81 it('Should fail with a bad target', async function () {
82 await makeGetRequest({ url, path, token, query: { target: 'bad target' } })
83 })
84
85 it('Should fail without target', async function () {
86 await makeGetRequest({ url, path, token })
87 })
88
89 it('Should succeed with the correct params', async function () {
90 await makeGetRequest({ url, path, token, query: { target: 'my-videos' }, statusCodeExpected: 200 })
91 })
92 })
93
94 describe('When manually adding a redundancy', function () {
95 const path = '/api/v1/server/redundancy/videos'
96
97 let url: string
98 let token: string
99
100 before(function () {
101 url = servers[0].url
102 token = servers[0].accessToken
103 })
104
105 it('Should fail with an invalid token', async function () {
106 await makePostBodyRequest({ url, path, token: 'fake_token', statusCodeExpected: 401 })
107 })
108
109 it('Should fail if the user is not an administrator', async function () {
110 await makePostBodyRequest({ url, path, token: userAccessToken, statusCodeExpected: 403 })
111 })
112
113 it('Should fail without a video id', async function () {
114 await makePostBodyRequest({ url, path, token })
115 })
116
117 it('Should fail with an incorrect video id', async function () {
118 await makePostBodyRequest({ url, path, token, fields: { videoId: 'peertube' } })
119 })
120
121 it('Should fail with a not found video id', async function () {
122 await makePostBodyRequest({ url, path, token, fields: { videoId: 6565 }, statusCodeExpected: 404 })
123 })
124
125 it('Should fail with a local a video id', async function () {
126 await makePostBodyRequest({ url, path, token, fields: { videoId: videoIdLocal } })
127 })
128
129 it('Should succeed with the correct params', async function () {
130 await makePostBodyRequest({ url, path, token, fields: { videoId: videoIdRemote }, statusCodeExpected: 204 })
131 })
132
133 it('Should fail if the video is already duplicated', async function () {
134 this.timeout(30000)
135
136 await waitJobs(servers)
137
138 await makePostBodyRequest({ url, path, token, fields: { videoId: videoIdRemote }, statusCodeExpected: 409 })
139 })
140 })
141
142 describe('When manually removing a redundancy', function () {
143 const path = '/api/v1/server/redundancy/videos/'
144
145 let url: string
146 let token: string
147
148 before(function () {
149 url = servers[0].url
150 token = servers[0].accessToken
151 })
152
153 it('Should fail with an invalid token', async function () {
154 await makeDeleteRequest({ url, path: path + '1', token: 'fake_token', statusCodeExpected: 401 })
155 })
156
157 it('Should fail if the user is not an administrator', async function () {
158 await makeDeleteRequest({ url, path: path + '1', token: userAccessToken, statusCodeExpected: 403 })
159 })
160
161 it('Should fail with an incorrect video id', async function () {
162 await makeDeleteRequest({ url, path: path + 'toto', token })
163 })
164
165 it('Should fail with a not found video redundancy', async function () {
166 await makeDeleteRequest({ url, path: path + '454545', token, statusCodeExpected: 404 })
167 })
39 }) 168 })
40 169
41 describe('When updating redundancy', function () { 170 describe('When updating server redundancy', function () {
42 const path = '/api/v1/server/redundancy' 171 const path = '/api/v1/server/redundancy'
43 172
44 it('Should fail with an invalid token', async function () { 173 it('Should fail with an invalid token', async function () {
diff --git a/server/tests/api/check-params/search.ts b/server/tests/api/check-params/search.ts
index 8ad9d98bf..f8d0cd4ec 100644
--- a/server/tests/api/check-params/search.ts
+++ b/server/tests/api/check-params/search.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4 4
diff --git a/server/tests/api/check-params/services.ts b/server/tests/api/check-params/services.ts
index d15753aed..457adfaab 100644
--- a/server/tests/api/check-params/services.ts
+++ b/server/tests/api/check-params/services.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4 4
diff --git a/server/tests/api/check-params/user-notifications.ts b/server/tests/api/check-params/user-notifications.ts
index 3b06be7ef..2048fa667 100644
--- a/server/tests/api/check-params/user-notifications.ts
+++ b/server/tests/api/check-params/user-notifications.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4import * as io from 'socket.io-client' 4import * as io from 'socket.io-client'
diff --git a/server/tests/api/check-params/user-subscriptions.ts b/server/tests/api/check-params/user-subscriptions.ts
index fa36c4078..1edba4d64 100644
--- a/server/tests/api/check-params/user-subscriptions.ts
+++ b/server/tests/api/check-params/user-subscriptions.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4 4
diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts
index 5d5af284c..4d597f0a3 100644
--- a/server/tests/api/check-params/users.ts
+++ b/server/tests/api/check-params/users.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import { omit } from 'lodash' 3import { omit } from 'lodash'
4import 'mocha' 4import 'mocha'
@@ -16,12 +16,14 @@ import {
16 getMyUserVideoRating, 16 getMyUserVideoRating,
17 getUsersList, 17 getUsersList,
18 immutableAssign, 18 immutableAssign,
19 killallServers,
19 makeGetRequest, 20 makeGetRequest,
20 makePostBodyRequest, 21 makePostBodyRequest,
21 makePutBodyRequest, 22 makePutBodyRequest,
22 makeUploadRequest, 23 makeUploadRequest,
23 registerUser, 24 registerUser,
24 removeUser, 25 removeUser,
26 reRunServer,
25 ServerInfo, 27 ServerInfo,
26 setAccessTokensToServers, 28 setAccessTokensToServers,
27 unblockUser, 29 unblockUser,
@@ -39,6 +41,7 @@ import { VideoPrivacy } from '../../../../shared/models/videos'
39import { waitJobs } from '../../../../shared/extra-utils/server/jobs' 41import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
40import { expect } from 'chai' 42import { expect } from 'chai'
41import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' 43import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
44import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email'
42 45
43describe('Test users API validators', function () { 46describe('Test users API validators', function () {
44 const path = '/api/v1/users/' 47 const path = '/api/v1/users/'
@@ -50,6 +53,9 @@ describe('Test users API validators', function () {
50 let serverWithRegistrationDisabled: ServerInfo 53 let serverWithRegistrationDisabled: ServerInfo
51 let userAccessToken = '' 54 let userAccessToken = ''
52 let moderatorAccessToken = '' 55 let moderatorAccessToken = ''
56 let emailPort: number
57 let overrideConfig: Object
58 // eslint-disable-next-line @typescript-eslint/no-unused-vars
53 let channelId: number 59 let channelId: number
54 60
55 // --------------------------------------------------------------- 61 // ---------------------------------------------------------------
@@ -57,9 +63,14 @@ describe('Test users API validators', function () {
57 before(async function () { 63 before(async function () {
58 this.timeout(30000) 64 this.timeout(30000)
59 65
66 const emails: object[] = []
67 emailPort = await MockSmtpServer.Instance.collectEmails(emails)
68
69 overrideConfig = { signup: { limit: 8 } }
70
60 { 71 {
61 const res = await Promise.all([ 72 const res = await Promise.all([
62 flushAndRunServer(1, { signup: { limit: 7 } }), 73 flushAndRunServer(1, overrideConfig),
63 flushAndRunServer(2) 74 flushAndRunServer(2)
64 ]) 75 ])
65 76
@@ -120,7 +131,7 @@ describe('Test users API validators', function () {
120 131
121 { 132 {
122 const res = await getMyUserInformation(server.url, server.accessToken) 133 const res = await getMyUserInformation(server.url, server.accessToken)
123 channelId = res.body.videoChannels[ 0 ].id 134 channelId = res.body.videoChannels[0].id
124 } 135 }
125 136
126 { 137 {
@@ -228,6 +239,40 @@ describe('Test users API validators', function () {
228 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) 239 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
229 }) 240 })
230 241
242 it('Should fail with empty password and no smtp configured', async function () {
243 const fields = immutableAssign(baseCorrectParams, { password: '' })
244
245 await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
246 })
247
248 it('Should succeed with no password on a server with smtp enabled', async function () {
249 this.timeout(10000)
250
251 killallServers([ server ])
252
253 const config = immutableAssign(overrideConfig, {
254 smtp: {
255 hostname: 'localhost',
256 port: emailPort
257 }
258 })
259 await reRunServer(server, config)
260
261 const fields = immutableAssign(baseCorrectParams, {
262 password: '',
263 username: 'create_password',
264 email: 'create_password@example.com'
265 })
266
267 await makePostBodyRequest({
268 url: server.url,
269 path: path,
270 token: server.accessToken,
271 fields,
272 statusCodeExpected: 200
273 })
274 })
275
231 it('Should fail with invalid admin flags', async function () { 276 it('Should fail with invalid admin flags', async function () {
232 const fields = immutableAssign(baseCorrectParams, { adminFlags: 'toto' }) 277 const fields = immutableAssign(baseCorrectParams, { adminFlags: 'toto' })
233 278
@@ -529,7 +574,7 @@ describe('Test users API validators', function () {
529 it('Should fail without an incorrect input file', async function () { 574 it('Should fail without an incorrect input file', async function () {
530 const fields = {} 575 const fields = {}
531 const attaches = { 576 const attaches = {
532 'avatarfile': join(__dirname, '..', '..', 'fixtures', 'video_short.mp4') 577 avatarfile: join(__dirname, '..', '..', 'fixtures', 'video_short.mp4')
533 } 578 }
534 await makeUploadRequest({ url: server.url, path: path + '/me/avatar/pick', token: server.accessToken, fields, attaches }) 579 await makeUploadRequest({ url: server.url, path: path + '/me/avatar/pick', token: server.accessToken, fields, attaches })
535 }) 580 })
@@ -537,7 +582,7 @@ describe('Test users API validators', function () {
537 it('Should fail with a big file', async function () { 582 it('Should fail with a big file', async function () {
538 const fields = {} 583 const fields = {}
539 const attaches = { 584 const attaches = {
540 'avatarfile': join(__dirname, '..', '..', 'fixtures', 'avatar-big.png') 585 avatarfile: join(__dirname, '..', '..', 'fixtures', 'avatar-big.png')
541 } 586 }
542 await makeUploadRequest({ url: server.url, path: path + '/me/avatar/pick', token: server.accessToken, fields, attaches }) 587 await makeUploadRequest({ url: server.url, path: path + '/me/avatar/pick', token: server.accessToken, fields, attaches })
543 }) 588 })
@@ -545,7 +590,7 @@ describe('Test users API validators', function () {
545 it('Should fail with an unauthenticated user', async function () { 590 it('Should fail with an unauthenticated user', async function () {
546 const fields = {} 591 const fields = {}
547 const attaches = { 592 const attaches = {
548 'avatarfile': join(__dirname, '..', '..', 'fixtures', 'avatar.png') 593 avatarfile: join(__dirname, '..', '..', 'fixtures', 'avatar.png')
549 } 594 }
550 await makeUploadRequest({ 595 await makeUploadRequest({
551 url: server.url, 596 url: server.url,
@@ -559,7 +604,7 @@ describe('Test users API validators', function () {
559 it('Should succeed with the correct params', async function () { 604 it('Should succeed with the correct params', async function () {
560 const fields = {} 605 const fields = {}
561 const attaches = { 606 const attaches = {
562 'avatarfile': join(__dirname, '..', '..', 'fixtures', 'avatar.png') 607 avatarfile: join(__dirname, '..', '..', 'fixtures', 'avatar.png')
563 } 608 }
564 await makeUploadRequest({ 609 await makeUploadRequest({
565 url: server.url, 610 url: server.url,
@@ -1101,6 +1146,8 @@ describe('Test users API validators', function () {
1101 }) 1146 })
1102 1147
1103 after(async function () { 1148 after(async function () {
1149 MockSmtpServer.Instance.kill()
1150
1104 await cleanupTests([ server, serverWithRegistrationDisabled ]) 1151 await cleanupTests([ server, serverWithRegistrationDisabled ])
1105 }) 1152 })
1106}) 1153})
diff --git a/server/tests/api/check-params/video-abuses.ts b/server/tests/api/check-params/video-abuses.ts
index bf29f8d4d..e643cb95e 100644
--- a/server/tests/api/check-params/video-abuses.ts
+++ b/server/tests/api/check-params/video-abuses.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4 4
@@ -76,6 +76,22 @@ describe('Test video abuses API validators', function () {
76 statusCodeExpected: 403 76 statusCodeExpected: 403
77 }) 77 })
78 }) 78 })
79
80 it('Should fail with a bad id filter', async function () {
81 await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { id: 'toto' } })
82 })
83
84 it('Should fail with a bad state filter', async function () {
85 await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { state: 'toto' } })
86 })
87
88 it('Should fail with a bad videoIs filter', async function () {
89 await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { videoIs: 'toto' } })
90 })
91
92 it('Should succeed with the correct params', async function () {
93 await makeGetRequest({ url: server.url, path, token: server.accessToken, query: { id: 13 }, statusCodeExpected: 200 })
94 })
79 }) 95 })
80 96
81 describe('When reporting a video abuse', function () { 97 describe('When reporting a video abuse', function () {
@@ -126,6 +142,7 @@ describe('Test video abuses API validators', function () {
126 142
127 describe('When updating a video abuse', function () { 143 describe('When updating a video abuse', function () {
128 const basePath = '/api/v1/videos/' 144 const basePath = '/api/v1/videos/'
145 // eslint-disable-next-line @typescript-eslint/no-unused-vars
129 let path: string 146 let path: string
130 147
131 before(() => { 148 before(() => {
@@ -163,6 +180,7 @@ describe('Test video abuses API validators', function () {
163 180
164 describe('When deleting a video abuse', function () { 181 describe('When deleting a video abuse', function () {
165 const basePath = '/api/v1/videos/' 182 const basePath = '/api/v1/videos/'
183 // eslint-disable-next-line @typescript-eslint/no-unused-vars
166 let path: string 184 let path: string
167 185
168 before(() => { 186 before(() => {
diff --git a/server/tests/api/check-params/video-blacklist.ts b/server/tests/api/check-params/video-blacklist.ts
index 6466888fb..145f43980 100644
--- a/server/tests/api/check-params/video-blacklist.ts
+++ b/server/tests/api/check-params/video-blacklist.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4 4
@@ -7,25 +7,24 @@ import {
7 createUser, 7 createUser,
8 doubleFollow, 8 doubleFollow,
9 flushAndRunMultipleServers, 9 flushAndRunMultipleServers,
10 flushTests,
11 getBlacklistedVideosList, 10 getBlacklistedVideosList,
12 getVideo, 11 getVideo,
13 getVideoWithToken, 12 getVideoWithToken,
14 killallServers,
15 makePostBodyRequest, 13 makePostBodyRequest,
16 makePutBodyRequest, 14 makePutBodyRequest,
17 removeVideoFromBlacklist, 15 removeVideoFromBlacklist,
18 ServerInfo, 16 ServerInfo,
19 setAccessTokensToServers, 17 setAccessTokensToServers,
20 uploadVideo, 18 uploadVideo,
21 userLogin, waitJobs 19 userLogin,
20 waitJobs
22} from '../../../../shared/extra-utils' 21} from '../../../../shared/extra-utils'
23import { 22import {
24 checkBadCountPagination, 23 checkBadCountPagination,
25 checkBadSortPagination, 24 checkBadSortPagination,
26 checkBadStartPagination 25 checkBadStartPagination
27} from '../../../../shared/extra-utils/requests/check-api-params' 26} from '../../../../shared/extra-utils/requests/check-api-params'
28import { VideoDetails, VideoBlacklistType } from '../../../../shared/models/videos' 27import { VideoBlacklistType, VideoDetails } from '../../../../shared/models/videos'
29import { expect } from 'chai' 28import { expect } from 'chai'
30 29
31describe('Test video blacklist API validators', function () { 30describe('Test video blacklist API validators', function () {
@@ -48,14 +47,14 @@ describe('Test video blacklist API validators', function () {
48 { 47 {
49 const username = 'user1' 48 const username = 'user1'
50 const password = 'my super password' 49 const password = 'my super password'
51 await createUser({ url: servers[ 0 ].url, accessToken: servers[ 0 ].accessToken, username: username, password: password }) 50 await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: username, password: password })
52 userAccessToken1 = await userLogin(servers[0], { username, password }) 51 userAccessToken1 = await userLogin(servers[0], { username, password })
53 } 52 }
54 53
55 { 54 {
56 const username = 'user2' 55 const username = 'user2'
57 const password = 'my super password' 56 const password = 'my super password'
58 await createUser({ url: servers[ 0 ].url, accessToken: servers[ 0 ].accessToken, username: username, password: password }) 57 await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: username, password: password })
59 userAccessToken2 = await userLogin(servers[0], { username, password }) 58 userAccessToken2 = await userLogin(servers[0], { username, password })
60 } 59 }
61 60
@@ -120,7 +119,7 @@ describe('Test video blacklist API validators', function () {
120 119
121 it('Should succeed with the correct params', async function () { 120 it('Should succeed with the correct params', async function () {
122 const path = basePath + servers[0].video.uuid + '/blacklist' 121 const path = basePath + servers[0].video.uuid + '/blacklist'
123 const fields = { } 122 const fields = {}
124 123
125 await makePostBodyRequest({ url: servers[0].url, path, token: servers[0].accessToken, fields, statusCodeExpected: 204 }) 124 await makePostBodyRequest({ url: servers[0].url, path, token: servers[0].accessToken, fields, statusCodeExpected: 204 })
126 }) 125 })
diff --git a/server/tests/api/check-params/video-captions.ts b/server/tests/api/check-params/video-captions.ts
index 6ddc20d69..a5f5c3322 100644
--- a/server/tests/api/check-params/video-captions.ts
+++ b/server/tests/api/check-params/video-captions.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4import { 4import {
@@ -50,7 +50,7 @@ describe('Test video captions API validator', function () {
50 describe('When adding video caption', function () { 50 describe('When adding video caption', function () {
51 const fields = { } 51 const fields = { }
52 const attaches = { 52 const attaches = {
53 'captionfile': join(__dirname, '..', '..', 'fixtures', 'subtitle-good1.vtt') 53 captionfile: join(__dirname, '..', '..', 'fixtures', 'subtitle-good1.vtt')
54 } 54 }
55 55
56 it('Should fail without a valid uuid', async function () { 56 it('Should fail without a valid uuid', async function () {
diff --git a/server/tests/api/check-params/video-channels.ts b/server/tests/api/check-params/video-channels.ts
index de88298d1..2795ad7d5 100644
--- a/server/tests/api/check-params/video-channels.ts
+++ b/server/tests/api/check-params/video-channels.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import { omit } from 'lodash' 4import { omit } from 'lodash'
@@ -243,7 +243,7 @@ describe('Test video channels API validator', function () {
243 it('Should fail with an incorrect input file', async function () { 243 it('Should fail with an incorrect input file', async function () {
244 const fields = {} 244 const fields = {}
245 const attaches = { 245 const attaches = {
246 'avatarfile': join(__dirname, '..', '..', 'fixtures', 'video_short.mp4') 246 avatarfile: join(__dirname, '..', '..', 'fixtures', 'video_short.mp4')
247 } 247 }
248 await makeUploadRequest({ url: server.url, path: path + '/avatar/pick', token: server.accessToken, fields, attaches }) 248 await makeUploadRequest({ url: server.url, path: path + '/avatar/pick', token: server.accessToken, fields, attaches })
249 }) 249 })
@@ -251,7 +251,7 @@ describe('Test video channels API validator', function () {
251 it('Should fail with a big file', async function () { 251 it('Should fail with a big file', async function () {
252 const fields = {} 252 const fields = {}
253 const attaches = { 253 const attaches = {
254 'avatarfile': join(__dirname, '..', '..', 'fixtures', 'avatar-big.png') 254 avatarfile: join(__dirname, '..', '..', 'fixtures', 'avatar-big.png')
255 } 255 }
256 await makeUploadRequest({ url: server.url, path: path + '/avatar/pick', token: server.accessToken, fields, attaches }) 256 await makeUploadRequest({ url: server.url, path: path + '/avatar/pick', token: server.accessToken, fields, attaches })
257 }) 257 })
@@ -259,7 +259,7 @@ describe('Test video channels API validator', function () {
259 it('Should fail with an unauthenticated user', async function () { 259 it('Should fail with an unauthenticated user', async function () {
260 const fields = {} 260 const fields = {}
261 const attaches = { 261 const attaches = {
262 'avatarfile': join(__dirname, '..', '..', 'fixtures', 'avatar.png') 262 avatarfile: join(__dirname, '..', '..', 'fixtures', 'avatar.png')
263 } 263 }
264 await makeUploadRequest({ 264 await makeUploadRequest({
265 url: server.url, 265 url: server.url,
@@ -273,7 +273,7 @@ describe('Test video channels API validator', function () {
273 it('Should succeed with the correct params', async function () { 273 it('Should succeed with the correct params', async function () {
274 const fields = {} 274 const fields = {}
275 const attaches = { 275 const attaches = {
276 'avatarfile': join(__dirname, '..', '..', 'fixtures', 'avatar.png') 276 avatarfile: join(__dirname, '..', '..', 'fixtures', 'avatar.png')
277 } 277 }
278 await makeUploadRequest({ 278 await makeUploadRequest({
279 url: server.url, 279 url: server.url,
@@ -324,7 +324,7 @@ describe('Test video channels API validator', function () {
324 }) 324 })
325 325
326 it('Should fail with an unknown video channel id', async function () { 326 it('Should fail with an unknown video channel id', async function () {
327 await deleteVideoChannel(server.url, server.accessToken,'super_channel2', 404) 327 await deleteVideoChannel(server.url, server.accessToken, 'super_channel2', 404)
328 }) 328 })
329 329
330 it('Should succeed with the correct parameters', async function () { 330 it('Should succeed with the correct parameters', async function () {
diff --git a/server/tests/api/check-params/video-comments.ts b/server/tests/api/check-params/video-comments.ts
index 5cf90bacc..181282ce1 100644
--- a/server/tests/api/check-params/video-comments.ts
+++ b/server/tests/api/check-params/video-comments.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
@@ -29,6 +29,7 @@ describe('Test video comments API validator', function () {
29 let server: ServerInfo 29 let server: ServerInfo
30 let videoUUID: string 30 let videoUUID: string
31 let userAccessToken: string 31 let userAccessToken: string
32 let userAccessToken2: string
32 let commentId: number 33 let commentId: number
33 34
34 // --------------------------------------------------------------- 35 // ---------------------------------------------------------------
@@ -53,13 +54,16 @@ describe('Test video comments API validator', function () {
53 } 54 }
54 55
55 { 56 {
56 const user = { 57 const user = { username: 'user1', password: 'my super password' }
57 username: 'user1',
58 password: 'my super password'
59 }
60 await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password }) 58 await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
61 userAccessToken = await userLogin(server, user) 59 userAccessToken = await userLogin(server, user)
62 } 60 }
61
62 {
63 const user = { username: 'user2', password: 'my super password' }
64 await createUser({ url: server.url, accessToken: server.accessToken, username: user.username, password: user.password })
65 userAccessToken2 = await userLogin(server, user)
66 }
63 }) 67 })
64 68
65 describe('When listing video comment threads', function () { 69 describe('When listing video comment threads', function () {
@@ -133,7 +137,7 @@ describe('Test video comments API validator', function () {
133 137
134 it('Should fail with a long comment', async function () { 138 it('Should fail with a long comment', async function () {
135 const fields = { 139 const fields = {
136 text: 'h'.repeat(3001) 140 text: 'h'.repeat(10001)
137 } 141 }
138 await makePostBodyRequest({ url: server.url, path: pathThread, token: server.accessToken, fields }) 142 await makePostBodyRequest({ url: server.url, path: pathThread, token: server.accessToken, fields })
139 }) 143 })
@@ -176,7 +180,7 @@ describe('Test video comments API validator', function () {
176 180
177 it('Should fail with a long comment', async function () { 181 it('Should fail with a long comment', async function () {
178 const fields = { 182 const fields = {
179 text: 'h'.repeat(3001) 183 text: 'h'.repeat(10001)
180 } 184 }
181 await makePostBodyRequest({ url: server.url, path: pathComment, token: server.accessToken, fields }) 185 await makePostBodyRequest({ url: server.url, path: pathComment, token: server.accessToken, fields })
182 }) 186 })
@@ -224,6 +228,40 @@ describe('Test video comments API validator', function () {
224 await makeDeleteRequest({ url: server.url, path, token: server.accessToken, statusCodeExpected: 404 }) 228 await makeDeleteRequest({ url: server.url, path, token: server.accessToken, statusCodeExpected: 404 })
225 }) 229 })
226 230
231 it('Should succeed with the same user', async function () {
232 let commentToDelete: number
233
234 {
235 const res = await addVideoCommentThread(server.url, userAccessToken, videoUUID, 'hello')
236 commentToDelete = res.body.comment.id
237 }
238
239 const path = '/api/v1/videos/' + videoUUID + '/comments/' + commentToDelete
240
241 await makeDeleteRequest({ url: server.url, path, token: userAccessToken2, statusCodeExpected: 403 })
242 await makeDeleteRequest({ url: server.url, path, token: userAccessToken, statusCodeExpected: 204 })
243 })
244
245 it('Should succeed with the owner of the video', async function () {
246 let commentToDelete: number
247 let anotherVideoUUID: string
248
249 {
250 const res = await uploadVideo(server.url, userAccessToken, { name: 'video' })
251 anotherVideoUUID = res.body.video.uuid
252 }
253
254 {
255 const res = await addVideoCommentThread(server.url, server.accessToken, anotherVideoUUID, 'hello')
256 commentToDelete = res.body.comment.id
257 }
258
259 const path = '/api/v1/videos/' + anotherVideoUUID + '/comments/' + commentToDelete
260
261 await makeDeleteRequest({ url: server.url, path, token: userAccessToken2, statusCodeExpected: 403 })
262 await makeDeleteRequest({ url: server.url, path, token: userAccessToken, statusCodeExpected: 204 })
263 })
264
227 it('Should succeed with the correct parameters', async function () { 265 it('Should succeed with the correct parameters', async function () {
228 await makeDeleteRequest({ url: server.url, path: pathComment, token: server.accessToken, statusCodeExpected: 204 }) 266 await makeDeleteRequest({ url: server.url, path: pathComment, token: server.accessToken, statusCodeExpected: 204 })
229 }) 267 })
diff --git a/server/tests/api/check-params/video-imports.ts b/server/tests/api/check-params/video-imports.ts
index 231d5cc85..dbea39c48 100644
--- a/server/tests/api/check-params/video-imports.ts
+++ b/server/tests/api/check-params/video-imports.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import { omit } from 'lodash' 3import { omit } from 'lodash'
4import 'mocha' 4import 'mocha'
@@ -29,6 +29,7 @@ describe('Test video imports API validator', function () {
29 const path = '/api/v1/videos/imports' 29 const path = '/api/v1/videos/imports'
30 let server: ServerInfo 30 let server: ServerInfo
31 let userAccessToken = '' 31 let userAccessToken = ''
32 // eslint-disable-next-line @typescript-eslint/no-unused-vars
32 let accountName: string 33 let accountName: string
33 let channelId: number 34 let channelId: number
34 35
@@ -48,7 +49,7 @@ describe('Test video imports API validator', function () {
48 49
49 { 50 {
50 const res = await getMyUserInformation(server.url, server.accessToken) 51 const res = await getMyUserInformation(server.url, server.accessToken)
51 channelId = res.body.videoChannels[ 0 ].id 52 channelId = res.body.videoChannels[0].id
52 accountName = res.body.account.name + '@' + res.body.account.host 53 accountName = res.body.account.name + '@' + res.body.account.host
53 } 54 }
54 }) 55 })
@@ -196,7 +197,7 @@ describe('Test video imports API validator', function () {
196 it('Should fail with an incorrect thumbnail file', async function () { 197 it('Should fail with an incorrect thumbnail file', async function () {
197 const fields = baseCorrectParams 198 const fields = baseCorrectParams
198 const attaches = { 199 const attaches = {
199 'thumbnailfile': join(__dirname, '..', '..', 'fixtures', 'avatar.png') 200 thumbnailfile: join(__dirname, '..', '..', 'fixtures', 'avatar.png')
200 } 201 }
201 202
202 await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) 203 await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches })
@@ -205,7 +206,7 @@ describe('Test video imports API validator', function () {
205 it('Should fail with a big thumbnail file', async function () { 206 it('Should fail with a big thumbnail file', async function () {
206 const fields = baseCorrectParams 207 const fields = baseCorrectParams
207 const attaches = { 208 const attaches = {
208 'thumbnailfile': join(__dirname, '..', '..', 'fixtures', 'avatar-big.png') 209 thumbnailfile: join(__dirname, '..', '..', 'fixtures', 'avatar-big.png')
209 } 210 }
210 211
211 await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) 212 await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches })
@@ -214,7 +215,7 @@ describe('Test video imports API validator', function () {
214 it('Should fail with an incorrect preview file', async function () { 215 it('Should fail with an incorrect preview file', async function () {
215 const fields = baseCorrectParams 216 const fields = baseCorrectParams
216 const attaches = { 217 const attaches = {
217 'previewfile': join(__dirname, '..', '..', 'fixtures', 'avatar.png') 218 previewfile: join(__dirname, '..', '..', 'fixtures', 'avatar.png')
218 } 219 }
219 220
220 await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) 221 await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches })
@@ -223,7 +224,7 @@ describe('Test video imports API validator', function () {
223 it('Should fail with a big preview file', async function () { 224 it('Should fail with a big preview file', async function () {
224 const fields = baseCorrectParams 225 const fields = baseCorrectParams
225 const attaches = { 226 const attaches = {
226 'previewfile': join(__dirname, '..', '..', 'fixtures', 'avatar-big.png') 227 previewfile: join(__dirname, '..', '..', 'fixtures', 'avatar-big.png')
227 } 228 }
228 229
229 await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) 230 await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches })
@@ -232,7 +233,7 @@ describe('Test video imports API validator', function () {
232 it('Should fail with an invalid torrent file', async function () { 233 it('Should fail with an invalid torrent file', async function () {
233 const fields = omit(baseCorrectParams, 'targetUrl') 234 const fields = omit(baseCorrectParams, 'targetUrl')
234 const attaches = { 235 const attaches = {
235 'torrentfile': join(__dirname, '..', '..', 'fixtures', 'avatar-big.png') 236 torrentfile: join(__dirname, '..', '..', 'fixtures', 'avatar-big.png')
236 } 237 }
237 238
238 await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches }) 239 await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches })
@@ -303,7 +304,7 @@ describe('Test video imports API validator', function () {
303 304
304 fields = omit(fields, 'magnetUri') 305 fields = omit(fields, 'magnetUri')
305 const attaches = { 306 const attaches = {
306 'torrentfile': join(__dirname, '..', '..', 'fixtures', 'video-720p.torrent') 307 torrentfile: join(__dirname, '..', '..', 'fixtures', 'video-720p.torrent')
307 } 308 }
308 309
309 await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches, statusCodeExpected: 409 }) 310 await makeUploadRequest({ url: server.url, path, token: server.accessToken, fields, attaches, statusCodeExpected: 409 })
diff --git a/server/tests/api/check-params/video-playlists.ts b/server/tests/api/check-params/video-playlists.ts
index df158f3b1..0410e737a 100644
--- a/server/tests/api/check-params/video-playlists.ts
+++ b/server/tests/api/check-params/video-playlists.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4import { 4import {
@@ -36,6 +36,7 @@ describe('Test video playlists API validator', function () {
36 let privatePlaylistUUID: string 36 let privatePlaylistUUID: string
37 let watchLaterPlaylistId: number 37 let watchLaterPlaylistId: number
38 let videoId: number 38 let videoId: number
39 // eslint-disable-next-line @typescript-eslint/no-unused-vars
39 let videoId2: number 40 let videoId2: number
40 let playlistElementId: number 41 let playlistElementId: number
41 42
@@ -449,7 +450,7 @@ describe('Test video playlists API validator', function () {
449 videoId3 = (await uploadVideoAndGetId({ server, videoName: 'video 3' })).id 450 videoId3 = (await uploadVideoAndGetId({ server, videoName: 'video 3' })).id
450 videoId4 = (await uploadVideoAndGetId({ server, videoName: 'video 4' })).id 451 videoId4 = (await uploadVideoAndGetId({ server, videoName: 'video 4' })).id
451 452
452 for (let id of [ videoId3, videoId4 ]) { 453 for (const id of [ videoId3, videoId4 ]) {
453 await addVideoInPlaylist({ 454 await addVideoInPlaylist({
454 url: server.url, 455 url: server.url,
455 token: server.accessToken, 456 token: server.accessToken,
@@ -476,7 +477,7 @@ describe('Test video playlists API validator', function () {
476 } 477 }
477 478
478 { 479 {
479 const params = getBase({}, { playlistId: 42, expectedStatus: 404 }) 480 const params = getBase({}, { playlistId: 42, expectedStatus: 404 })
480 await reorderVideosPlaylist(params) 481 await reorderVideosPlaylist(params)
481 } 482 }
482 }) 483 })
diff --git a/server/tests/api/check-params/videos-filter.ts b/server/tests/api/check-params/videos-filter.ts
index 811756745..ec8654db2 100644
--- a/server/tests/api/check-params/videos-filter.ts
+++ b/server/tests/api/check-params/videos-filter.ts
@@ -1,10 +1,9 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4import { 4import {
5 cleanupTests, 5 cleanupTests,
6 createUser, 6 createUser,
7 createVideoPlaylist,
8 flushAndRunServer, 7 flushAndRunServer,
9 makeGetRequest, 8 makeGetRequest,
10 ServerInfo, 9 ServerInfo,
@@ -13,7 +12,6 @@ import {
13 userLogin 12 userLogin
14} from '../../../../shared/extra-utils' 13} from '../../../../shared/extra-utils'
15import { UserRole } from '../../../../shared/models/users' 14import { UserRole } from '../../../../shared/models/users'
16import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
17 15
18async function testEndpoints (server: ServerInfo, token: string, filter: string, statusCodeExpected: number) { 16async function testEndpoints (server: ServerInfo, token: string, filter: string, statusCodeExpected: number) {
19 const paths = [ 17 const paths = [
@@ -77,7 +75,7 @@ describe('Test videos filters', function () {
77 }) 75 })
78 76
79 it('Should succeed with a good filter', async function () { 77 it('Should succeed with a good filter', async function () {
80 await testEndpoints(server, server.accessToken,'local', 200) 78 await testEndpoints(server, server.accessToken, 'local', 200)
81 }) 79 })
82 80
83 it('Should fail to list all-local with a simple user', async function () { 81 it('Should fail to list all-local with a simple user', async function () {
diff --git a/server/tests/api/check-params/videos-history.ts b/server/tests/api/check-params/videos-history.ts
index 3739e3fad..941f62654 100644
--- a/server/tests/api/check-params/videos-history.ts
+++ b/server/tests/api/check-params/videos-history.ts
@@ -1,6 +1,5 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai'
4import 'mocha' 3import 'mocha'
5import { 4import {
6 checkBadCountPagination, 5 checkBadCountPagination,
@@ -15,12 +14,10 @@ import {
15 uploadVideo 14 uploadVideo
16} from '../../../../shared/extra-utils' 15} from '../../../../shared/extra-utils'
17 16
18const expect = chai.expect
19
20describe('Test videos history API validator', function () { 17describe('Test videos history API validator', function () {
18 const myHistoryPath = '/api/v1/users/me/history/videos'
19 const myHistoryRemove = myHistoryPath + '/remove'
21 let watchingPath: string 20 let watchingPath: string
22 let myHistoryPath = '/api/v1/users/me/history/videos'
23 let myHistoryRemove = myHistoryPath + '/remove'
24 let server: ServerInfo 21 let server: ServerInfo
25 22
26 // --------------------------------------------------------------- 23 // ---------------------------------------------------------------
diff --git a/server/tests/api/check-params/videos-overviews.ts b/server/tests/api/check-params/videos-overviews.ts
new file mode 100644
index 000000000..69d7fc471
--- /dev/null
+++ b/server/tests/api/check-params/videos-overviews.ts
@@ -0,0 +1,33 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4import { cleanupTests, flushAndRunServer, ServerInfo } from '../../../../shared/extra-utils'
5import { getVideosOverview } from '@shared/extra-utils/overviews/overviews'
6
7describe('Test videos overview', function () {
8 let server: ServerInfo
9
10 // ---------------------------------------------------------------
11
12 before(async function () {
13 this.timeout(30000)
14
15 server = await flushAndRunServer(1)
16 })
17
18 describe('When getting videos overview', function () {
19
20 it('Should fail with a bad pagination', async function () {
21 await getVideosOverview(server.url, 0, 400)
22 await getVideosOverview(server.url, 100, 400)
23 })
24
25 it('Should succeed with a good pagination', async function () {
26 await getVideosOverview(server.url, 1)
27 })
28 })
29
30 after(async function () {
31 await cleanupTests([ server ])
32 })
33})
diff --git a/server/tests/api/check-params/videos.ts b/server/tests/api/check-params/videos.ts
index 16ef1c505..0d4665954 100644
--- a/server/tests/api/check-params/videos.ts
+++ b/server/tests/api/check-params/videos.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import { omit } from 'lodash' 4import { omit } from 'lodash'
@@ -56,8 +56,8 @@ describe('Test videos API validator', function () {
56 56
57 { 57 {
58 const res = await getMyUserInformation(server.url, server.accessToken) 58 const res = await getMyUserInformation(server.url, server.accessToken)
59 channelId = res.body.videoChannels[ 0 ].id 59 channelId = res.body.videoChannels[0].id
60 channelName = res.body.videoChannels[ 0 ].name 60 channelName = res.body.videoChannels[0].name
61 accountName = res.body.account.name + '@' + res.body.account.host 61 accountName = res.body.account.name + '@' + res.body.account.host
62 } 62 }
63 }) 63 })
@@ -182,7 +182,7 @@ describe('Test videos API validator', function () {
182 describe('When adding a video', function () { 182 describe('When adding a video', function () {
183 let baseCorrectParams 183 let baseCorrectParams
184 const baseCorrectAttaches = { 184 const baseCorrectAttaches = {
185 'videofile': join(root(), 'server', 'tests', 'fixtures', 'video_short.webm') 185 videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short.webm')
186 } 186 }
187 187
188 before(function () { 188 before(function () {
@@ -330,7 +330,7 @@ describe('Test videos API validator', function () {
330 }) 330 })
331 331
332 it('Should fail with a bad originally published at attribute', async function () { 332 it('Should fail with a bad originally published at attribute', async function () {
333 const fields = immutableAssign(baseCorrectParams, { 'originallyPublishedAt': 'toto' }) 333 const fields = immutableAssign(baseCorrectParams, { originallyPublishedAt: 'toto' })
334 const attaches = baseCorrectAttaches 334 const attaches = baseCorrectAttaches
335 335
336 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) 336 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
@@ -345,12 +345,12 @@ describe('Test videos API validator', function () {
345 it('Should fail with an incorrect input file', async function () { 345 it('Should fail with an incorrect input file', async function () {
346 const fields = baseCorrectParams 346 const fields = baseCorrectParams
347 let attaches = { 347 let attaches = {
348 'videofile': join(root(), 'server', 'tests', 'fixtures', 'video_short_fake.webm') 348 videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short_fake.webm')
349 } 349 }
350 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) 350 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
351 351
352 attaches = { 352 attaches = {
353 'videofile': join(root(), 'server', 'tests', 'fixtures', 'video_short.mkv') 353 videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short.mkv')
354 } 354 }
355 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) 355 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
356 }) 356 })
@@ -358,8 +358,8 @@ describe('Test videos API validator', function () {
358 it('Should fail with an incorrect thumbnail file', async function () { 358 it('Should fail with an incorrect thumbnail file', async function () {
359 const fields = baseCorrectParams 359 const fields = baseCorrectParams
360 const attaches = { 360 const attaches = {
361 'thumbnailfile': join(root(), 'server', 'tests', 'fixtures', 'avatar.png'), 361 thumbnailfile: join(root(), 'server', 'tests', 'fixtures', 'avatar.png'),
362 'videofile': join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4') 362 videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4')
363 } 363 }
364 364
365 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) 365 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
@@ -368,8 +368,8 @@ describe('Test videos API validator', function () {
368 it('Should fail with a big thumbnail file', async function () { 368 it('Should fail with a big thumbnail file', async function () {
369 const fields = baseCorrectParams 369 const fields = baseCorrectParams
370 const attaches = { 370 const attaches = {
371 'thumbnailfile': join(root(), 'server', 'tests', 'fixtures', 'avatar-big.png'), 371 thumbnailfile: join(root(), 'server', 'tests', 'fixtures', 'avatar-big.png'),
372 'videofile': join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4') 372 videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4')
373 } 373 }
374 374
375 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) 375 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
@@ -378,8 +378,8 @@ describe('Test videos API validator', function () {
378 it('Should fail with an incorrect preview file', async function () { 378 it('Should fail with an incorrect preview file', async function () {
379 const fields = baseCorrectParams 379 const fields = baseCorrectParams
380 const attaches = { 380 const attaches = {
381 'previewfile': join(root(), 'server', 'tests', 'fixtures', 'avatar.png'), 381 previewfile: join(root(), 'server', 'tests', 'fixtures', 'avatar.png'),
382 'videofile': join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4') 382 videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4')
383 } 383 }
384 384
385 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) 385 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
@@ -388,8 +388,8 @@ describe('Test videos API validator', function () {
388 it('Should fail with a big preview file', async function () { 388 it('Should fail with a big preview file', async function () {
389 const fields = baseCorrectParams 389 const fields = baseCorrectParams
390 const attaches = { 390 const attaches = {
391 'previewfile': join(root(), 'server', 'tests', 'fixtures', 'avatar-big.png'), 391 previewfile: join(root(), 'server', 'tests', 'fixtures', 'avatar-big.png'),
392 'videofile': join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4') 392 videofile: join(root(), 'server', 'tests', 'fixtures', 'video_short.mp4')
393 } 393 }
394 394
395 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches }) 395 await makeUploadRequest({ url: server.url, path: path + '/upload', token: server.accessToken, fields, attaches })
@@ -566,7 +566,7 @@ describe('Test videos API validator', function () {
566 it('Should fail with an incorrect thumbnail file', async function () { 566 it('Should fail with an incorrect thumbnail file', async function () {
567 const fields = baseCorrectParams 567 const fields = baseCorrectParams
568 const attaches = { 568 const attaches = {
569 'thumbnailfile': join(root(), 'server', 'tests', 'fixtures', 'avatar.png') 569 thumbnailfile: join(root(), 'server', 'tests', 'fixtures', 'avatar.png')
570 } 570 }
571 571
572 await makeUploadRequest({ 572 await makeUploadRequest({
@@ -582,7 +582,7 @@ describe('Test videos API validator', function () {
582 it('Should fail with a big thumbnail file', async function () { 582 it('Should fail with a big thumbnail file', async function () {
583 const fields = baseCorrectParams 583 const fields = baseCorrectParams
584 const attaches = { 584 const attaches = {
585 'thumbnailfile': join(root(), 'server', 'tests', 'fixtures', 'avatar-big.png') 585 thumbnailfile: join(root(), 'server', 'tests', 'fixtures', 'avatar-big.png')
586 } 586 }
587 587
588 await makeUploadRequest({ 588 await makeUploadRequest({
@@ -598,7 +598,7 @@ describe('Test videos API validator', function () {
598 it('Should fail with an incorrect preview file', async function () { 598 it('Should fail with an incorrect preview file', async function () {
599 const fields = baseCorrectParams 599 const fields = baseCorrectParams
600 const attaches = { 600 const attaches = {
601 'previewfile': join(root(), 'server', 'tests', 'fixtures', 'avatar.png') 601 previewfile: join(root(), 'server', 'tests', 'fixtures', 'avatar.png')
602 } 602 }
603 603
604 await makeUploadRequest({ 604 await makeUploadRequest({
@@ -614,7 +614,7 @@ describe('Test videos API validator', function () {
614 it('Should fail with a big preview file', async function () { 614 it('Should fail with a big preview file', async function () {
615 const fields = baseCorrectParams 615 const fields = baseCorrectParams
616 const attaches = { 616 const attaches = {
617 'previewfile': join(root(), 'server', 'tests', 'fixtures', 'avatar-big.png') 617 previewfile: join(root(), 'server', 'tests', 'fixtures', 'avatar-big.png')
618 } 618 }
619 619
620 await makeUploadRequest({ 620 await makeUploadRequest({
diff --git a/server/tests/api/notifications/user-notifications.ts b/server/tests/api/notifications/user-notifications.ts
index 15a34f5aa..dfa2234da 100644
--- a/server/tests/api/notifications/user-notifications.ts
+++ b/server/tests/api/notifications/user-notifications.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
@@ -63,7 +63,7 @@ import { addUserSubscription, removeUserSubscription } from '../../../../shared/
63import { VideoPrivacy } from '../../../../shared/models/videos' 63import { VideoPrivacy } from '../../../../shared/models/videos'
64import { getBadVideoUrl, getYoutubeVideoUrl, importVideo } from '../../../../shared/extra-utils/videos/video-imports' 64import { getBadVideoUrl, getYoutubeVideoUrl, importVideo } from '../../../../shared/extra-utils/videos/video-imports'
65import { addVideoCommentReply, addVideoCommentThread } from '../../../../shared/extra-utils/videos/video-comments' 65import { addVideoCommentReply, addVideoCommentThread } from '../../../../shared/extra-utils/videos/video-comments'
66import * as uuidv4 from 'uuid/v4' 66import { v4 as uuidv4 } from 'uuid'
67import { addAccountToAccountBlocklist, removeAccountFromAccountBlocklist } from '../../../../shared/extra-utils/users/blocklist' 67import { addAccountToAccountBlocklist, removeAccountFromAccountBlocklist } from '../../../../shared/extra-utils/users/blocklist'
68import { CustomConfig } from '../../../../shared/models/server' 68import { CustomConfig } from '../../../../shared/models/server'
69import { VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model' 69import { VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model'
@@ -74,7 +74,7 @@ async function uploadVideoByRemoteAccount (servers: ServerInfo[], additionalPara
74 const name = 'remote video ' + uuidv4() 74 const name = 'remote video ' + uuidv4()
75 75
76 const data = Object.assign({ name }, additionalParams) 76 const data = Object.assign({ name }, additionalParams)
77 const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, data) 77 const res = await uploadVideo(servers[1].url, servers[1].accessToken, data)
78 78
79 await waitJobs(servers) 79 await waitJobs(servers)
80 80
@@ -85,7 +85,7 @@ async function uploadVideoByLocalAccount (servers: ServerInfo[], additionalParam
85 const name = 'local video ' + uuidv4() 85 const name = 'local video ' + uuidv4()
86 86
87 const data = Object.assign({ name }, additionalParams) 87 const data = Object.assign({ name }, additionalParams)
88 const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, data) 88 const res = await uploadVideo(servers[0].url, servers[0].accessToken, data)
89 89
90 await waitJobs(servers) 90 await waitJobs(servers)
91 91
@@ -95,9 +95,9 @@ async function uploadVideoByLocalAccount (servers: ServerInfo[], additionalParam
95describe('Test users notifications', function () { 95describe('Test users notifications', function () {
96 let servers: ServerInfo[] = [] 96 let servers: ServerInfo[] = []
97 let userAccessToken: string 97 let userAccessToken: string
98 let userNotifications: UserNotification[] = [] 98 const userNotifications: UserNotification[] = []
99 let adminNotifications: UserNotification[] = [] 99 const adminNotifications: UserNotification[] = []
100 let adminNotificationsServer2: UserNotification[] = [] 100 const adminNotificationsServer2: UserNotification[] = []
101 const emails: object[] = [] 101 const emails: object[] = []
102 let channelId: number 102 let channelId: number
103 103
@@ -142,8 +142,8 @@ describe('Test users notifications', function () {
142 password: 'super password' 142 password: 'super password'
143 } 143 }
144 await createUser({ 144 await createUser({
145 url: servers[ 0 ].url, 145 url: servers[0].url,
146 accessToken: servers[ 0 ].accessToken, 146 accessToken: servers[0].accessToken,
147 username: user.username, 147 username: user.username,
148 password: user.password, 148 password: user.password,
149 videoQuota: 10 * 1000 * 1000 149 videoQuota: 10 * 1000 * 1000
@@ -155,15 +155,15 @@ describe('Test users notifications', function () {
155 await updateMyNotificationSettings(servers[1].url, servers[1].accessToken, allNotificationSettings) 155 await updateMyNotificationSettings(servers[1].url, servers[1].accessToken, allNotificationSettings)
156 156
157 { 157 {
158 const socket = getUserNotificationSocket(servers[ 0 ].url, userAccessToken) 158 const socket = getUserNotificationSocket(servers[0].url, userAccessToken)
159 socket.on('new-notification', n => userNotifications.push(n)) 159 socket.on('new-notification', n => userNotifications.push(n))
160 } 160 }
161 { 161 {
162 const socket = getUserNotificationSocket(servers[ 0 ].url, servers[0].accessToken) 162 const socket = getUserNotificationSocket(servers[0].url, servers[0].accessToken)
163 socket.on('new-notification', n => adminNotifications.push(n)) 163 socket.on('new-notification', n => adminNotifications.push(n))
164 } 164 }
165 { 165 {
166 const socket = getUserNotificationSocket(servers[ 1 ].url, servers[1].accessToken) 166 const socket = getUserNotificationSocket(servers[1].url, servers[1].accessToken)
167 socket.on('new-notification', n => adminNotificationsServer2.push(n)) 167 socket.on('new-notification', n => adminNotificationsServer2.push(n))
168 } 168 }
169 169
@@ -190,7 +190,7 @@ describe('Test users notifications', function () {
190 190
191 await uploadVideoByLocalAccount(servers) 191 await uploadVideoByLocalAccount(servers)
192 192
193 const notification = await getLastNotification(servers[ 0 ].url, userAccessToken) 193 const notification = await getLastNotification(servers[0].url, userAccessToken)
194 expect(notification).to.be.undefined 194 expect(notification).to.be.undefined
195 195
196 expect(emails).to.have.lengthOf(0) 196 expect(emails).to.have.lengthOf(0)
@@ -221,7 +221,7 @@ describe('Test users notifications', function () {
221 this.timeout(20000) 221 this.timeout(20000)
222 222
223 // In 2 seconds 223 // In 2 seconds
224 let updateAt = new Date(new Date().getTime() + 2000) 224 const updateAt = new Date(new Date().getTime() + 2000)
225 225
226 const data = { 226 const data = {
227 privacy: VideoPrivacy.PRIVATE, 227 privacy: VideoPrivacy.PRIVATE,
@@ -240,7 +240,7 @@ describe('Test users notifications', function () {
240 this.timeout(50000) 240 this.timeout(50000)
241 241
242 // In 2 seconds 242 // In 2 seconds
243 let updateAt = new Date(new Date().getTime() + 2000) 243 const updateAt = new Date(new Date().getTime() + 2000)
244 244
245 const data = { 245 const data = {
246 privacy: VideoPrivacy.PRIVATE, 246 privacy: VideoPrivacy.PRIVATE,
@@ -259,7 +259,7 @@ describe('Test users notifications', function () {
259 it('Should not send a notification before the video is published', async function () { 259 it('Should not send a notification before the video is published', async function () {
260 this.timeout(20000) 260 this.timeout(20000)
261 261
262 let updateAt = new Date(new Date().getTime() + 1000000) 262 const updateAt = new Date(new Date().getTime() + 1000000)
263 263
264 const data = { 264 const data = {
265 privacy: VideoPrivacy.PRIVATE, 265 privacy: VideoPrivacy.PRIVATE,
@@ -386,7 +386,7 @@ describe('Test users notifications', function () {
386 it('Should not send a new comment notification if the account is muted', async function () { 386 it('Should not send a new comment notification if the account is muted', async function () {
387 this.timeout(10000) 387 this.timeout(10000)
388 388
389 await addAccountToAccountBlocklist(servers[ 0 ].url, userAccessToken, 'root') 389 await addAccountToAccountBlocklist(servers[0].url, userAccessToken, 'root')
390 390
391 const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: 'super video' }) 391 const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: 'super video' })
392 const uuid = resVideo.body.video.uuid 392 const uuid = resVideo.body.video.uuid
@@ -397,7 +397,7 @@ describe('Test users notifications', function () {
397 await wait(500) 397 await wait(500)
398 await checkNewCommentOnMyVideo(baseParams, uuid, commentId, commentId, 'absence') 398 await checkNewCommentOnMyVideo(baseParams, uuid, commentId, commentId, 'absence')
399 399
400 await removeAccountFromAccountBlocklist(servers[ 0 ].url, userAccessToken, 'root') 400 await removeAccountFromAccountBlocklist(servers[0].url, userAccessToken, 'root')
401 }) 401 })
402 402
403 it('Should send a new comment notification after a local comment on my video', async function () { 403 it('Should send a new comment notification after a local comment on my video', async function () {
@@ -456,9 +456,9 @@ describe('Test users notifications', function () {
456 await waitJobs(servers) 456 await waitJobs(servers)
457 457
458 { 458 {
459 const resThread = await addVideoCommentThread(servers[ 1 ].url, servers[ 1 ].accessToken, uuid, 'comment') 459 const resThread = await addVideoCommentThread(servers[1].url, servers[1].accessToken, uuid, 'comment')
460 const threadId = resThread.body.comment.id 460 const threadId = resThread.body.comment.id
461 await addVideoCommentReply(servers[ 1 ].url, servers[ 1 ].accessToken, uuid, threadId, 'reply') 461 await addVideoCommentReply(servers[1].url, servers[1].accessToken, uuid, threadId, 'reply')
462 } 462 }
463 463
464 await waitJobs(servers) 464 await waitJobs(servers)
@@ -530,7 +530,7 @@ describe('Test users notifications', function () {
530 it('Should not send a new mention notification if the account is muted', async function () { 530 it('Should not send a new mention notification if the account is muted', async function () {
531 this.timeout(10000) 531 this.timeout(10000)
532 532
533 await addAccountToAccountBlocklist(servers[ 0 ].url, userAccessToken, 'root') 533 await addAccountToAccountBlocklist(servers[0].url, userAccessToken, 'root')
534 534
535 const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'super video' }) 535 const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'super video' })
536 const uuid = resVideo.body.video.uuid 536 const uuid = resVideo.body.video.uuid
@@ -541,7 +541,7 @@ describe('Test users notifications', function () {
541 await wait(500) 541 await wait(500)
542 await checkCommentMention(baseParams, uuid, commentId, commentId, 'super root name', 'absence') 542 await checkCommentMention(baseParams, uuid, commentId, commentId, 'super root name', 'absence')
543 543
544 await removeAccountFromAccountBlocklist(servers[ 0 ].url, userAccessToken, 'root') 544 await removeAccountFromAccountBlocklist(servers[0].url, userAccessToken, 'root')
545 }) 545 })
546 546
547 it('Should not send a new mention notification if the remote account mention a local account', async function () { 547 it('Should not send a new mention notification if the remote account mention a local account', async function () {
@@ -585,7 +585,7 @@ describe('Test users notifications', function () {
585 585
586 await waitJobs(servers) 586 await waitJobs(servers)
587 587
588 const text1 = `hello @user_1@localhost:${servers[ 0 ].port} 1` 588 const text1 = `hello @user_1@localhost:${servers[0].port} 1`
589 const resThread = await addVideoCommentThread(servers[1].url, servers[1].accessToken, uuid, text1) 589 const resThread = await addVideoCommentThread(servers[1].url, servers[1].accessToken, uuid, text1)
590 const server2ThreadId = resThread.body.comment.id 590 const server2ThreadId = resThread.body.comment.id
591 591
@@ -596,7 +596,7 @@ describe('Test users notifications', function () {
596 const server1ThreadId = resThread2.body.data[0].id 596 const server1ThreadId = resThread2.body.data[0].id
597 await checkCommentMention(baseParams, uuid, server1ThreadId, server1ThreadId, 'super root 2 name', 'presence') 597 await checkCommentMention(baseParams, uuid, server1ThreadId, server1ThreadId, 'super root 2 name', 'presence')
598 598
599 const text2 = `@user_1@localhost:${servers[ 0 ].port} hello 2 @root@localhost:${servers[ 0 ].port}` 599 const text2 = `@user_1@localhost:${servers[0].port} hello 2 @root@localhost:${servers[0].port}`
600 await addVideoCommentReply(servers[1].url, servers[1].accessToken, uuid, server2ThreadId, text2) 600 await addVideoCommentReply(servers[1].url, servers[1].accessToken, uuid, server2ThreadId, text2)
601 601
602 await waitJobs(servers) 602 await waitJobs(servers)
@@ -611,7 +611,7 @@ describe('Test users notifications', function () {
611 }) 611 })
612 }) 612 })
613 613
614 describe('Video abuse for moderators notification' , function () { 614 describe('Video abuse for moderators notification', function () {
615 let baseParams: CheckerBaseParams 615 let baseParams: CheckerBaseParams
616 616
617 before(() => { 617 before(() => {
@@ -722,7 +722,7 @@ describe('Test users notifications', function () {
722 await uploadVideoByRemoteAccount(servers, { waitTranscoding: false }) 722 await uploadVideoByRemoteAccount(servers, { waitTranscoding: false })
723 await waitJobs(servers) 723 await waitJobs(servers)
724 724
725 const notification = await getLastNotification(servers[ 0 ].url, userAccessToken) 725 const notification = await getLastNotification(servers[0].url, userAccessToken)
726 if (notification) { 726 if (notification) {
727 expect(notification.type).to.not.equal(UserNotificationType.MY_VIDEO_PUBLISHED) 727 expect(notification.type).to.not.equal(UserNotificationType.MY_VIDEO_PUBLISHED)
728 } 728 }
@@ -769,7 +769,7 @@ describe('Test users notifications', function () {
769 this.timeout(70000) 769 this.timeout(70000)
770 770
771 // In 2 seconds 771 // In 2 seconds
772 let updateAt = new Date(new Date().getTime() + 2000) 772 const updateAt = new Date(new Date().getTime() + 2000)
773 773
774 const data = { 774 const data = {
775 privacy: VideoPrivacy.PRIVATE, 775 privacy: VideoPrivacy.PRIVATE,
@@ -787,7 +787,7 @@ describe('Test users notifications', function () {
787 it('Should not send a notification before the video is published', async function () { 787 it('Should not send a notification before the video is published', async function () {
788 this.timeout(20000) 788 this.timeout(20000)
789 789
790 let updateAt = new Date(new Date().getTime() + 1000000) 790 const updateAt = new Date(new Date().getTime() + 1000000)
791 791
792 const data = { 792 const data = {
793 privacy: VideoPrivacy.PRIVATE, 793 privacy: VideoPrivacy.PRIVATE,
@@ -970,8 +970,8 @@ describe('Test users notifications', function () {
970 970
971 describe('New actor follow', function () { 971 describe('New actor follow', function () {
972 let baseParams: CheckerBaseParams 972 let baseParams: CheckerBaseParams
973 let myChannelName = 'super channel name' 973 const myChannelName = 'super channel name'
974 let myUserName = 'super user name' 974 const myUserName = 'super user name'
975 975
976 before(async () => { 976 before(async () => {
977 baseParams = { 977 baseParams = {
@@ -1024,25 +1024,26 @@ describe('Test users notifications', function () {
1024 await removeUserSubscription(servers[1].url, servers[1].accessToken, 'user_1_channel@localhost:' + servers[0].port) 1024 await removeUserSubscription(servers[1].url, servers[1].accessToken, 'user_1_channel@localhost:' + servers[0].port)
1025 }) 1025 })
1026 1026
1027 it('Should notify when a local account is following one of our channel', async function () { 1027 // PeerTube does not support accout -> account follows
1028 this.timeout(10000) 1028 // it('Should notify when a local account is following one of our channel', async function () {
1029 1029 // this.timeout(10000)
1030 await addUserSubscription(servers[0].url, servers[0].accessToken, 'user_1@localhost:' + servers[0].port) 1030 //
1031 1031 // await addUserSubscription(servers[0].url, servers[0].accessToken, 'user_1@localhost:' + servers[0].port)
1032 await waitJobs(servers) 1032 //
1033 1033 // await waitJobs(servers)
1034 await checkNewActorFollow(baseParams, 'account', 'root', 'super root name', myUserName, 'presence') 1034 //
1035 }) 1035 // await checkNewActorFollow(baseParams, 'account', 'root', 'super root name', myUserName, 'presence')
1036 1036 // })
1037 it('Should notify when a remote account is following one of our channel', async function () { 1037
1038 this.timeout(10000) 1038 // it('Should notify when a remote account is following one of our channel', async function () {
1039 1039 // this.timeout(10000)
1040 await addUserSubscription(servers[1].url, servers[1].accessToken, 'user_1@localhost:' + servers[0].port) 1040 //
1041 1041 // await addUserSubscription(servers[1].url, servers[1].accessToken, 'user_1@localhost:' + servers[0].port)
1042 await waitJobs(servers) 1042 //
1043 1043 // await waitJobs(servers)
1044 await checkNewActorFollow(baseParams, 'account', 'root', 'super root 2 name', myUserName, 'presence') 1044 //
1045 }) 1045 // await checkNewActorFollow(baseParams, 'account', 'root', 'super root 2 name', myUserName, 'presence')
1046 // })
1046 }) 1047 })
1047 1048
1048 describe('Video-related notifications when video auto-blacklist is enabled', function () { 1049 describe('Video-related notifications when video auto-blacklist is enabled', function () {
@@ -1143,7 +1144,7 @@ describe('Test users notifications', function () {
1143 it('Should send unblacklist but not published/subscription notes after unblacklisted if scheduled update pending', async function () { 1144 it('Should send unblacklist but not published/subscription notes after unblacklisted if scheduled update pending', async function () {
1144 this.timeout(20000) 1145 this.timeout(20000)
1145 1146
1146 let updateAt = new Date(new Date().getTime() + 1000000) 1147 const updateAt = new Date(new Date().getTime() + 1000000)
1147 1148
1148 const name = 'video with auto-blacklist and future schedule ' + uuidv4() 1149 const name = 'video with auto-blacklist and future schedule ' + uuidv4()
1149 1150
@@ -1176,7 +1177,7 @@ describe('Test users notifications', function () {
1176 this.timeout(20000) 1177 this.timeout(20000)
1177 1178
1178 // In 2 seconds 1179 // In 2 seconds
1179 let updateAt = new Date(new Date().getTime() + 2000) 1180 const updateAt = new Date(new Date().getTime() + 2000)
1180 1181
1181 const name = 'video with schedule done and still auto-blacklisted ' + uuidv4() 1182 const name = 'video with schedule done and still auto-blacklisted ' + uuidv4()
1182 1183
@@ -1221,26 +1222,26 @@ describe('Test users notifications', function () {
1221 1222
1222 describe('Mark as read', function () { 1223 describe('Mark as read', function () {
1223 it('Should mark as read some notifications', async function () { 1224 it('Should mark as read some notifications', async function () {
1224 const res = await getUserNotifications(servers[ 0 ].url, userAccessToken, 2, 3) 1225 const res = await getUserNotifications(servers[0].url, userAccessToken, 2, 3)
1225 const ids = res.body.data.map(n => n.id) 1226 const ids = res.body.data.map(n => n.id)
1226 1227
1227 await markAsReadNotifications(servers[ 0 ].url, userAccessToken, ids) 1228 await markAsReadNotifications(servers[0].url, userAccessToken, ids)
1228 }) 1229 })
1229 1230
1230 it('Should have the notifications marked as read', async function () { 1231 it('Should have the notifications marked as read', async function () {
1231 const res = await getUserNotifications(servers[ 0 ].url, userAccessToken, 0, 10) 1232 const res = await getUserNotifications(servers[0].url, userAccessToken, 0, 10)
1232 1233
1233 const notifications = res.body.data as UserNotification[] 1234 const notifications = res.body.data as UserNotification[]
1234 expect(notifications[ 0 ].read).to.be.false 1235 expect(notifications[0].read).to.be.false
1235 expect(notifications[ 1 ].read).to.be.false 1236 expect(notifications[1].read).to.be.false
1236 expect(notifications[ 2 ].read).to.be.true 1237 expect(notifications[2].read).to.be.true
1237 expect(notifications[ 3 ].read).to.be.true 1238 expect(notifications[3].read).to.be.true
1238 expect(notifications[ 4 ].read).to.be.true 1239 expect(notifications[4].read).to.be.true
1239 expect(notifications[ 5 ].read).to.be.false 1240 expect(notifications[5].read).to.be.false
1240 }) 1241 })
1241 1242
1242 it('Should only list read notifications', async function () { 1243 it('Should only list read notifications', async function () {
1243 const res = await getUserNotifications(servers[ 0 ].url, userAccessToken, 0, 10, false) 1244 const res = await getUserNotifications(servers[0].url, userAccessToken, 0, 10, false)
1244 1245
1245 const notifications = res.body.data as UserNotification[] 1246 const notifications = res.body.data as UserNotification[]
1246 for (const notification of notifications) { 1247 for (const notification of notifications) {
@@ -1249,7 +1250,7 @@ describe('Test users notifications', function () {
1249 }) 1250 })
1250 1251
1251 it('Should only list unread notifications', async function () { 1252 it('Should only list unread notifications', async function () {
1252 const res = await getUserNotifications(servers[ 0 ].url, userAccessToken, 0, 10, true) 1253 const res = await getUserNotifications(servers[0].url, userAccessToken, 0, 10, true)
1253 1254
1254 const notifications = res.body.data as UserNotification[] 1255 const notifications = res.body.data as UserNotification[]
1255 for (const notification of notifications) { 1256 for (const notification of notifications) {
@@ -1258,9 +1259,9 @@ describe('Test users notifications', function () {
1258 }) 1259 })
1259 1260
1260 it('Should mark as read all notifications', async function () { 1261 it('Should mark as read all notifications', async function () {
1261 await markAsReadAllNotifications(servers[ 0 ].url, userAccessToken) 1262 await markAsReadAllNotifications(servers[0].url, userAccessToken)
1262 1263
1263 const res = await getUserNotifications(servers[ 0 ].url, userAccessToken, 0, 10, true) 1264 const res = await getUserNotifications(servers[0].url, userAccessToken, 0, 10, true)
1264 1265
1265 expect(res.body.total).to.equal(0) 1266 expect(res.body.total).to.equal(0)
1266 expect(res.body.data).to.have.lengthOf(0) 1267 expect(res.body.data).to.have.lengthOf(0)
diff --git a/server/tests/api/redundancy/index.ts b/server/tests/api/redundancy/index.ts
index 8e69b95a6..37dc3f88c 100644
--- a/server/tests/api/redundancy/index.ts
+++ b/server/tests/api/redundancy/index.ts
@@ -1 +1,3 @@
1import './redundancy-constraints'
1import './redundancy' 2import './redundancy'
3import './manage-redundancy'
diff --git a/server/tests/api/redundancy/manage-redundancy.ts b/server/tests/api/redundancy/manage-redundancy.ts
new file mode 100644
index 000000000..4253124c8
--- /dev/null
+++ b/server/tests/api/redundancy/manage-redundancy.ts
@@ -0,0 +1,373 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import * as chai from 'chai'
4import 'mocha'
5import {
6 cleanupTests,
7 doubleFollow,
8 flushAndRunMultipleServers,
9 getLocalIdByUUID,
10 ServerInfo,
11 setAccessTokensToServers,
12 uploadVideo,
13 uploadVideoAndGetId,
14 waitUntilLog
15} from '../../../../shared/extra-utils'
16import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
17import { addVideoRedundancy, listVideoRedundancies, removeVideoRedundancy, updateRedundancy } from '@shared/extra-utils/server/redundancy'
18import { VideoPrivacy, VideoRedundanciesTarget, VideoRedundancy } from '@shared/models'
19
20const expect = chai.expect
21
22describe('Test manage videos redundancy', function () {
23 const targets: VideoRedundanciesTarget[] = [ 'my-videos', 'remote-videos' ]
24
25 let servers: ServerInfo[]
26 let video1Server2UUID: string
27 let video2Server2UUID: string
28 let redundanciesToRemove: number[] = []
29
30 before(async function () {
31 this.timeout(120000)
32
33 const config = {
34 transcoding: {
35 hls: {
36 enabled: true
37 }
38 },
39 redundancy: {
40 videos: {
41 check_interval: '1 second',
42 strategies: [
43 {
44 strategy: 'recently-added',
45 min_lifetime: '1 hour',
46 size: '10MB',
47 min_views: 0
48 }
49 ]
50 }
51 }
52 }
53 servers = await flushAndRunMultipleServers(3, config)
54
55 // Get the access tokens
56 await setAccessTokensToServers(servers)
57
58 {
59 const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video 1 server 2' })
60 video1Server2UUID = res.body.video.uuid
61 }
62
63 {
64 const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video 2 server 2' })
65 video2Server2UUID = res.body.video.uuid
66 }
67
68 await waitJobs(servers)
69
70 // Server 1 and server 2 follow each other
71 await doubleFollow(servers[0], servers[1])
72 await updateRedundancy(servers[0].url, servers[0].accessToken, servers[1].host, true)
73
74 await waitJobs(servers)
75 })
76
77 it('Should not have redundancies on server 3', async function () {
78 for (const target of targets) {
79 const res = await listVideoRedundancies({
80 url: servers[2].url,
81 accessToken: servers[2].accessToken,
82 target
83 })
84
85 expect(res.body.total).to.equal(0)
86 expect(res.body.data).to.have.lengthOf(0)
87 }
88 })
89
90 it('Should not have "remote-videos" redundancies on server 2', async function () {
91 this.timeout(120000)
92
93 await waitJobs(servers)
94 await waitUntilLog(servers[0], 'Duplicated ', 10)
95 await waitJobs(servers)
96
97 const res = await listVideoRedundancies({
98 url: servers[1].url,
99 accessToken: servers[1].accessToken,
100 target: 'remote-videos'
101 })
102
103 expect(res.body.total).to.equal(0)
104 expect(res.body.data).to.have.lengthOf(0)
105 })
106
107 it('Should have "my-videos" redundancies on server 2', async function () {
108 this.timeout(120000)
109
110 const res = await listVideoRedundancies({
111 url: servers[1].url,
112 accessToken: servers[1].accessToken,
113 target: 'my-videos'
114 })
115
116 expect(res.body.total).to.equal(2)
117
118 const videos = res.body.data as VideoRedundancy[]
119 expect(videos).to.have.lengthOf(2)
120
121 const videos1 = videos.find(v => v.uuid === video1Server2UUID)
122 const videos2 = videos.find(v => v.uuid === video2Server2UUID)
123
124 expect(videos1.name).to.equal('video 1 server 2')
125 expect(videos2.name).to.equal('video 2 server 2')
126
127 expect(videos1.redundancies.files).to.have.lengthOf(4)
128 expect(videos1.redundancies.streamingPlaylists).to.have.lengthOf(1)
129
130 const redundancies = videos1.redundancies.files.concat(videos1.redundancies.streamingPlaylists)
131
132 for (const r of redundancies) {
133 expect(r.strategy).to.be.null
134 expect(r.fileUrl).to.exist
135 expect(r.createdAt).to.exist
136 expect(r.updatedAt).to.exist
137 expect(r.expiresOn).to.exist
138 }
139 })
140
141 it('Should not have "my-videos" redundancies on server 1', async function () {
142 const res = await listVideoRedundancies({
143 url: servers[0].url,
144 accessToken: servers[0].accessToken,
145 target: 'my-videos'
146 })
147
148 expect(res.body.total).to.equal(0)
149 expect(res.body.data).to.have.lengthOf(0)
150 })
151
152 it('Should have "remote-videos" redundancies on server 1', async function () {
153 this.timeout(120000)
154
155 const res = await listVideoRedundancies({
156 url: servers[0].url,
157 accessToken: servers[0].accessToken,
158 target: 'remote-videos'
159 })
160
161 expect(res.body.total).to.equal(2)
162
163 const videos = res.body.data as VideoRedundancy[]
164 expect(videos).to.have.lengthOf(2)
165
166 const videos1 = videos.find(v => v.uuid === video1Server2UUID)
167 const videos2 = videos.find(v => v.uuid === video2Server2UUID)
168
169 expect(videos1.name).to.equal('video 1 server 2')
170 expect(videos2.name).to.equal('video 2 server 2')
171
172 expect(videos1.redundancies.files).to.have.lengthOf(4)
173 expect(videos1.redundancies.streamingPlaylists).to.have.lengthOf(1)
174
175 const redundancies = videos1.redundancies.files.concat(videos1.redundancies.streamingPlaylists)
176
177 for (const r of redundancies) {
178 expect(r.strategy).to.equal('recently-added')
179 expect(r.fileUrl).to.exist
180 expect(r.createdAt).to.exist
181 expect(r.updatedAt).to.exist
182 expect(r.expiresOn).to.exist
183 }
184 })
185
186 it('Should correctly paginate and sort results', async function () {
187 {
188 const res = await listVideoRedundancies({
189 url: servers[0].url,
190 accessToken: servers[0].accessToken,
191 target: 'remote-videos',
192 sort: 'name',
193 start: 0,
194 count: 2
195 })
196
197 const videos = res.body.data
198 expect(videos[0].name).to.equal('video 1 server 2')
199 expect(videos[1].name).to.equal('video 2 server 2')
200 }
201
202 {
203 const res = await listVideoRedundancies({
204 url: servers[0].url,
205 accessToken: servers[0].accessToken,
206 target: 'remote-videos',
207 sort: '-name',
208 start: 0,
209 count: 2
210 })
211
212 const videos = res.body.data
213 expect(videos[0].name).to.equal('video 2 server 2')
214 expect(videos[1].name).to.equal('video 1 server 2')
215 }
216
217 {
218 const res = await listVideoRedundancies({
219 url: servers[0].url,
220 accessToken: servers[0].accessToken,
221 target: 'remote-videos',
222 sort: '-name',
223 start: 1,
224 count: 1
225 })
226
227 const videos = res.body.data
228 expect(videos[0].name).to.equal('video 1 server 2')
229 }
230 })
231
232 it('Should manually add a redundancy and list it', async function () {
233 this.timeout(120000)
234
235 const uuid = (await uploadVideoAndGetId({ server: servers[1], videoName: 'video 3 server 2', privacy: VideoPrivacy.UNLISTED })).uuid
236 await waitJobs(servers)
237 const videoId = await getLocalIdByUUID(servers[0].url, uuid)
238
239 await addVideoRedundancy({
240 url: servers[0].url,
241 accessToken: servers[0].accessToken,
242 videoId
243 })
244
245 await waitJobs(servers)
246 await waitUntilLog(servers[0], 'Duplicated ', 15)
247 await waitJobs(servers)
248
249 {
250 const res = await listVideoRedundancies({
251 url: servers[0].url,
252 accessToken: servers[0].accessToken,
253 target: 'remote-videos',
254 sort: '-name',
255 start: 0,
256 count: 5
257 })
258
259 const videos = res.body.data
260 expect(videos[0].name).to.equal('video 3 server 2')
261
262 const video = videos[0]
263 expect(video.redundancies.files).to.have.lengthOf(4)
264 expect(video.redundancies.streamingPlaylists).to.have.lengthOf(1)
265
266 const redundancies = video.redundancies.files.concat(video.redundancies.streamingPlaylists)
267
268 for (const r of redundancies) {
269 redundanciesToRemove.push(r.id)
270
271 expect(r.strategy).to.equal('manual')
272 expect(r.fileUrl).to.exist
273 expect(r.createdAt).to.exist
274 expect(r.updatedAt).to.exist
275 expect(r.expiresOn).to.be.null
276 }
277 }
278
279 const res = await listVideoRedundancies({
280 url: servers[1].url,
281 accessToken: servers[1].accessToken,
282 target: 'my-videos',
283 sort: '-name',
284 start: 0,
285 count: 5
286 })
287
288 const videos = res.body.data
289 expect(videos[0].name).to.equal('video 3 server 2')
290
291 const video = videos[0]
292 expect(video.redundancies.files).to.have.lengthOf(4)
293 expect(video.redundancies.streamingPlaylists).to.have.lengthOf(1)
294
295 const redundancies = video.redundancies.files.concat(video.redundancies.streamingPlaylists)
296
297 for (const r of redundancies) {
298 expect(r.strategy).to.be.null
299 expect(r.fileUrl).to.exist
300 expect(r.createdAt).to.exist
301 expect(r.updatedAt).to.exist
302 expect(r.expiresOn).to.be.null
303 }
304 })
305
306 it('Should manually remove a redundancy and remove it from the list', async function () {
307 this.timeout(120000)
308
309 for (const redundancyId of redundanciesToRemove) {
310 await removeVideoRedundancy({
311 url: servers[0].url,
312 accessToken: servers[0].accessToken,
313 redundancyId
314 })
315 }
316
317 {
318 const res = await listVideoRedundancies({
319 url: servers[0].url,
320 accessToken: servers[0].accessToken,
321 target: 'remote-videos',
322 sort: '-name',
323 start: 0,
324 count: 5
325 })
326
327 const videos = res.body.data
328 expect(videos).to.have.lengthOf(2)
329
330 expect(videos[0].name).to.equal('video 2 server 2')
331
332 redundanciesToRemove = []
333 const video = videos[0]
334 expect(video.redundancies.files).to.have.lengthOf(4)
335 expect(video.redundancies.streamingPlaylists).to.have.lengthOf(1)
336
337 const redundancies = video.redundancies.files.concat(video.redundancies.streamingPlaylists)
338
339 for (const r of redundancies) {
340 redundanciesToRemove.push(r.id)
341 }
342 }
343 })
344
345 it('Should remove another (auto) redundancy', async function () {
346 {
347 for (const redundancyId of redundanciesToRemove) {
348 await removeVideoRedundancy({
349 url: servers[0].url,
350 accessToken: servers[0].accessToken,
351 redundancyId
352 })
353 }
354
355 const res = await listVideoRedundancies({
356 url: servers[0].url,
357 accessToken: servers[0].accessToken,
358 target: 'remote-videos',
359 sort: '-name',
360 start: 0,
361 count: 5
362 })
363
364 const videos = res.body.data
365 expect(videos[0].name).to.equal('video 1 server 2')
366 expect(videos).to.have.lengthOf(1)
367 }
368 })
369
370 after(async function () {
371 await cleanupTests(servers)
372 })
373})
diff --git a/server/tests/api/redundancy/redundancy-constraints.ts b/server/tests/api/redundancy/redundancy-constraints.ts
new file mode 100644
index 000000000..4fd8f065c
--- /dev/null
+++ b/server/tests/api/redundancy/redundancy-constraints.ts
@@ -0,0 +1,200 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import * as chai from 'chai'
4import 'mocha'
5import {
6 cleanupTests,
7 flushAndRunServer,
8 follow,
9 killallServers,
10 reRunServer,
11 ServerInfo,
12 setAccessTokensToServers,
13 uploadVideo,
14 waitUntilLog
15} from '../../../../shared/extra-utils'
16import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
17import { listVideoRedundancies, updateRedundancy } from '@shared/extra-utils/server/redundancy'
18
19const expect = chai.expect
20
21describe('Test redundancy constraints', function () {
22 let remoteServer: ServerInfo
23 let localServer: ServerInfo
24 let servers: ServerInfo[]
25
26 async function getTotalRedundanciesLocalServer () {
27 const res = await listVideoRedundancies({
28 url: localServer.url,
29 accessToken: localServer.accessToken,
30 target: 'my-videos'
31 })
32
33 return res.body.total
34 }
35
36 async function getTotalRedundanciesRemoteServer () {
37 const res = await listVideoRedundancies({
38 url: remoteServer.url,
39 accessToken: remoteServer.accessToken,
40 target: 'remote-videos'
41 })
42
43 return res.body.total
44 }
45
46 before(async function () {
47 this.timeout(120000)
48
49 {
50 const config = {
51 redundancy: {
52 videos: {
53 check_interval: '1 second',
54 strategies: [
55 {
56 strategy: 'recently-added',
57 min_lifetime: '1 hour',
58 size: '100MB',
59 min_views: 0
60 }
61 ]
62 }
63 }
64 }
65 remoteServer = await flushAndRunServer(1, config)
66 }
67
68 {
69 const config = {
70 remote_redundancy: {
71 videos: {
72 accept_from: 'nobody'
73 }
74 }
75 }
76 localServer = await flushAndRunServer(2, config)
77 }
78
79 servers = [ remoteServer, localServer ]
80
81 // Get the access tokens
82 await setAccessTokensToServers(servers)
83
84 await uploadVideo(localServer.url, localServer.accessToken, { name: 'video 1 server 2' })
85
86 await waitJobs(servers)
87
88 // Server 1 and server 2 follow each other
89 await follow(remoteServer.url, [ localServer.url ], remoteServer.accessToken)
90 await waitJobs(servers)
91 await updateRedundancy(remoteServer.url, remoteServer.accessToken, localServer.host, true)
92
93 await waitJobs(servers)
94 })
95
96 it('Should have redundancy on server 1 but not on server 2 with a nobody filter', async function () {
97 this.timeout(120000)
98
99 await waitJobs(servers)
100 await waitUntilLog(remoteServer, 'Duplicated ', 5)
101 await waitJobs(servers)
102
103 {
104 const total = await getTotalRedundanciesRemoteServer()
105 expect(total).to.equal(1)
106 }
107
108 {
109 const total = await getTotalRedundanciesLocalServer()
110 expect(total).to.equal(0)
111 }
112 })
113
114 it('Should have redundancy on server 1 and on server 2 with an anybody filter', async function () {
115 this.timeout(120000)
116
117 const config = {
118 remote_redundancy: {
119 videos: {
120 accept_from: 'anybody'
121 }
122 }
123 }
124 await killallServers([ localServer ])
125 await reRunServer(localServer, config)
126
127 await uploadVideo(localServer.url, localServer.accessToken, { name: 'video 2 server 2' })
128
129 await waitJobs(servers)
130 await waitUntilLog(remoteServer, 'Duplicated ', 10)
131 await waitJobs(servers)
132
133 {
134 const total = await getTotalRedundanciesRemoteServer()
135 expect(total).to.equal(2)
136 }
137
138 {
139 const total = await getTotalRedundanciesLocalServer()
140 expect(total).to.equal(1)
141 }
142 })
143
144 it('Should have redundancy on server 1 but not on server 2 with a followings filter', async function () {
145 this.timeout(120000)
146
147 const config = {
148 remote_redundancy: {
149 videos: {
150 accept_from: 'followings'
151 }
152 }
153 }
154 await killallServers([ localServer ])
155 await reRunServer(localServer, config)
156
157 await uploadVideo(localServer.url, localServer.accessToken, { name: 'video 3 server 2' })
158
159 await waitJobs(servers)
160 await waitUntilLog(remoteServer, 'Duplicated ', 15)
161 await waitJobs(servers)
162
163 {
164 const total = await getTotalRedundanciesRemoteServer()
165 expect(total).to.equal(3)
166 }
167
168 {
169 const total = await getTotalRedundanciesLocalServer()
170 expect(total).to.equal(1)
171 }
172 })
173
174 it('Should have redundancy on server 1 and on server 2 with followings filter now server 2 follows server 1', async function () {
175 this.timeout(120000)
176
177 await follow(localServer.url, [ remoteServer.url ], localServer.accessToken)
178 await waitJobs(servers)
179
180 await uploadVideo(localServer.url, localServer.accessToken, { name: 'video 4 server 2' })
181
182 await waitJobs(servers)
183 await waitUntilLog(remoteServer, 'Duplicated ', 20)
184 await waitJobs(servers)
185
186 {
187 const total = await getTotalRedundanciesRemoteServer()
188 expect(total).to.equal(4)
189 }
190
191 {
192 const total = await getTotalRedundanciesLocalServer()
193 expect(total).to.equal(2)
194 }
195 })
196
197 after(async function () {
198 await cleanupTests(servers)
199 })
200})
diff --git a/server/tests/api/redundancy/redundancy.ts b/server/tests/api/redundancy/redundancy.ts
index 1cdf93aa1..c5037a541 100644
--- a/server/tests/api/redundancy/redundancy.ts
+++ b/server/tests/api/redundancy/redundancy.ts
@@ -1,11 +1,12 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { VideoDetails } from '../../../../shared/models/videos' 5import { VideoDetails } from '../../../../shared/models/videos'
6import { 6import {
7 checkSegmentHash, 7 checkSegmentHash,
8 checkVideoFilesWereRemoved, cleanupTests, 8 checkVideoFilesWereRemoved,
9 cleanupTests,
9 doubleFollow, 10 doubleFollow,
10 flushAndRunMultipleServers, 11 flushAndRunMultipleServers,
11 getFollowingListPaginationAndSort, 12 getFollowingListPaginationAndSort,
@@ -28,11 +29,16 @@ import {
28import { waitJobs } from '../../../../shared/extra-utils/server/jobs' 29import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
29 30
30import * as magnetUtil from 'magnet-uri' 31import * as magnetUtil from 'magnet-uri'
31import { updateRedundancy } from '../../../../shared/extra-utils/server/redundancy' 32import {
33 addVideoRedundancy,
34 listVideoRedundancies,
35 removeVideoRedundancy,
36 updateRedundancy
37} from '../../../../shared/extra-utils/server/redundancy'
32import { ActorFollow } from '../../../../shared/models/actors' 38import { ActorFollow } from '../../../../shared/models/actors'
33import { readdir } from 'fs-extra' 39import { readdir } from 'fs-extra'
34import { join } from 'path' 40import { join } from 'path'
35import { VideoRedundancyStrategy } from '../../../../shared/models/redundancy' 41import { VideoRedundancy, VideoRedundancyStrategy, VideoRedundancyStrategyWithManual } from '../../../../shared/models/redundancy'
36import { getStats } from '../../../../shared/extra-utils/server/stats' 42import { getStats } from '../../../../shared/extra-utils/server/stats'
37import { ServerStats } from '../../../../shared/models/server/server-stats.model' 43import { ServerStats } from '../../../../shared/models/server/server-stats.model'
38 44
@@ -40,6 +46,7 @@ const expect = chai.expect
40 46
41let servers: ServerInfo[] = [] 47let servers: ServerInfo[] = []
42let video1Server2UUID: string 48let video1Server2UUID: string
49let video1Server2Id: number
43 50
44function checkMagnetWebseeds (file: { magnetUri: string, resolution: { id: number } }, baseWebseeds: string[], server: ServerInfo) { 51function checkMagnetWebseeds (file: { magnetUri: string, resolution: { id: number } }, baseWebseeds: string[], server: ServerInfo) {
45 const parsed = magnetUtil.decode(file.magnetUri) 52 const parsed = magnetUtil.decode(file.magnetUri)
@@ -52,7 +59,19 @@ function checkMagnetWebseeds (file: { magnetUri: string, resolution: { id: numbe
52 expect(parsed.urlList).to.have.lengthOf(baseWebseeds.length) 59 expect(parsed.urlList).to.have.lengthOf(baseWebseeds.length)
53} 60}
54 61
55async function flushAndRunServers (strategy: VideoRedundancyStrategy, additionalParams: any = {}) { 62async function flushAndRunServers (strategy: VideoRedundancyStrategy | null, additionalParams: any = {}) {
63 const strategies: any[] = []
64
65 if (strategy !== null) {
66 strategies.push(
67 immutableAssign({
68 min_lifetime: '1 hour',
69 strategy: strategy,
70 size: '400KB'
71 }, additionalParams)
72 )
73 }
74
56 const config = { 75 const config = {
57 transcoding: { 76 transcoding: {
58 hls: { 77 hls: {
@@ -62,36 +81,32 @@ async function flushAndRunServers (strategy: VideoRedundancyStrategy, additional
62 redundancy: { 81 redundancy: {
63 videos: { 82 videos: {
64 check_interval: '5 seconds', 83 check_interval: '5 seconds',
65 strategies: [ 84 strategies
66 immutableAssign({
67 min_lifetime: '1 hour',
68 strategy: strategy,
69 size: '400KB'
70 }, additionalParams)
71 ]
72 } 85 }
73 } 86 }
74 } 87 }
88
75 servers = await flushAndRunMultipleServers(3, config) 89 servers = await flushAndRunMultipleServers(3, config)
76 90
77 // Get the access tokens 91 // Get the access tokens
78 await setAccessTokensToServers(servers) 92 await setAccessTokensToServers(servers)
79 93
80 { 94 {
81 const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, { name: 'video 1 server 2' }) 95 const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video 1 server 2' })
82 video1Server2UUID = res.body.video.uuid 96 video1Server2UUID = res.body.video.uuid
97 video1Server2Id = res.body.video.id
83 98
84 await viewVideo(servers[ 1 ].url, video1Server2UUID) 99 await viewVideo(servers[1].url, video1Server2UUID)
85 } 100 }
86 101
87 await waitJobs(servers) 102 await waitJobs(servers)
88 103
89 // Server 1 and server 2 follow each other 104 // Server 1 and server 2 follow each other
90 await doubleFollow(servers[ 0 ], servers[ 1 ]) 105 await doubleFollow(servers[0], servers[1])
91 // Server 1 and server 3 follow each other 106 // Server 1 and server 3 follow each other
92 await doubleFollow(servers[ 0 ], servers[ 2 ]) 107 await doubleFollow(servers[0], servers[2])
93 // Server 2 and server 3 follow each other 108 // Server 2 and server 3 follow each other
94 await doubleFollow(servers[ 1 ], servers[ 2 ]) 109 await doubleFollow(servers[1], servers[2])
95 110
96 await waitJobs(servers) 111 await waitJobs(servers)
97} 112}
@@ -100,7 +115,7 @@ async function check1WebSeed (videoUUID?: string) {
100 if (!videoUUID) videoUUID = video1Server2UUID 115 if (!videoUUID) videoUUID = video1Server2UUID
101 116
102 const webseeds = [ 117 const webseeds = [
103 `http://localhost:${servers[ 1 ].port}/static/webseed/${videoUUID}` 118 `http://localhost:${servers[1].port}/static/webseed/${videoUUID}`
104 ] 119 ]
105 120
106 for (const server of servers) { 121 for (const server of servers) {
@@ -118,8 +133,8 @@ async function check2Webseeds (videoUUID?: string) {
118 if (!videoUUID) videoUUID = video1Server2UUID 133 if (!videoUUID) videoUUID = video1Server2UUID
119 134
120 const webseeds = [ 135 const webseeds = [
121 `http://localhost:${servers[ 0 ].port}/static/redundancy/${videoUUID}`, 136 `http://localhost:${servers[0].port}/static/redundancy/${videoUUID}`,
122 `http://localhost:${servers[ 1 ].port}/static/webseed/${videoUUID}` 137 `http://localhost:${servers[1].port}/static/webseed/${videoUUID}`
123 ] 138 ]
124 139
125 for (const server of servers) { 140 for (const server of servers) {
@@ -216,41 +231,50 @@ async function check1PlaylistRedundancies (videoUUID?: string) {
216 } 231 }
217} 232}
218 233
219async function checkStatsWith2Webseed (strategy: VideoRedundancyStrategy) { 234async function checkStatsGlobal (strategy: VideoRedundancyStrategyWithManual) {
235 let totalSize: number = null
236 let statsLength = 1
237
238 if (strategy !== 'manual') {
239 totalSize = 409600
240 statsLength = 2
241 }
242
220 const res = await getStats(servers[0].url) 243 const res = await getStats(servers[0].url)
221 const data: ServerStats = res.body 244 const data: ServerStats = res.body
222 245
223 expect(data.videosRedundancy).to.have.lengthOf(1) 246 expect(data.videosRedundancy).to.have.lengthOf(statsLength)
224 const stat = data.videosRedundancy[0]
225 247
248 const stat = data.videosRedundancy[0]
226 expect(stat.strategy).to.equal(strategy) 249 expect(stat.strategy).to.equal(strategy)
227 expect(stat.totalSize).to.equal(409600) 250 expect(stat.totalSize).to.equal(totalSize)
251
252 return stat
253}
254
255async function checkStatsWith2Webseed (strategy: VideoRedundancyStrategyWithManual) {
256 const stat = await checkStatsGlobal(strategy)
257
228 expect(stat.totalUsed).to.be.at.least(1).and.below(409601) 258 expect(stat.totalUsed).to.be.at.least(1).and.below(409601)
229 expect(stat.totalVideoFiles).to.equal(4) 259 expect(stat.totalVideoFiles).to.equal(4)
230 expect(stat.totalVideos).to.equal(1) 260 expect(stat.totalVideos).to.equal(1)
231} 261}
232 262
233async function checkStatsWith1Webseed (strategy: VideoRedundancyStrategy) { 263async function checkStatsWith1Webseed (strategy: VideoRedundancyStrategyWithManual) {
234 const res = await getStats(servers[0].url) 264 const stat = await checkStatsGlobal(strategy)
235 const data: ServerStats = res.body
236 265
237 expect(data.videosRedundancy).to.have.lengthOf(1)
238
239 const stat = data.videosRedundancy[0]
240 expect(stat.strategy).to.equal(strategy)
241 expect(stat.totalSize).to.equal(409600)
242 expect(stat.totalUsed).to.equal(0) 266 expect(stat.totalUsed).to.equal(0)
243 expect(stat.totalVideoFiles).to.equal(0) 267 expect(stat.totalVideoFiles).to.equal(0)
244 expect(stat.totalVideos).to.equal(0) 268 expect(stat.totalVideos).to.equal(0)
245} 269}
246 270
247async function enableRedundancyOnServer1 () { 271async function enableRedundancyOnServer1 () {
248 await updateRedundancy(servers[ 0 ].url, servers[ 0 ].accessToken, servers[ 1 ].host, true) 272 await updateRedundancy(servers[0].url, servers[0].accessToken, servers[1].host, true)
249 273
250 const res = await getFollowingListPaginationAndSort({ url: servers[ 0 ].url, start: 0, count: 5, sort: '-createdAt' }) 274 const res = await getFollowingListPaginationAndSort({ url: servers[0].url, start: 0, count: 5, sort: '-createdAt' })
251 const follows: ActorFollow[] = res.body.data 275 const follows: ActorFollow[] = res.body.data
252 const server2 = follows.find(f => f.following.host === `localhost:${servers[ 1 ].port}`) 276 const server2 = follows.find(f => f.following.host === `localhost:${servers[1].port}`)
253 const server3 = follows.find(f => f.following.host === `localhost:${servers[ 2 ].port}`) 277 const server3 = follows.find(f => f.following.host === `localhost:${servers[2].port}`)
254 278
255 expect(server3).to.not.be.undefined 279 expect(server3).to.not.be.undefined
256 expect(server3.following.hostRedundancyAllowed).to.be.false 280 expect(server3.following.hostRedundancyAllowed).to.be.false
@@ -260,12 +284,12 @@ async function enableRedundancyOnServer1 () {
260} 284}
261 285
262async function disableRedundancyOnServer1 () { 286async function disableRedundancyOnServer1 () {
263 await updateRedundancy(servers[ 0 ].url, servers[ 0 ].accessToken, servers[ 1 ].host, false) 287 await updateRedundancy(servers[0].url, servers[0].accessToken, servers[1].host, false)
264 288
265 const res = await getFollowingListPaginationAndSort({ url: servers[ 0 ].url, start: 0, count: 5, sort: '-createdAt' }) 289 const res = await getFollowingListPaginationAndSort({ url: servers[0].url, start: 0, count: 5, sort: '-createdAt' })
266 const follows: ActorFollow[] = res.body.data 290 const follows: ActorFollow[] = res.body.data
267 const server2 = follows.find(f => f.following.host === `localhost:${servers[ 1 ].port}`) 291 const server2 = follows.find(f => f.following.host === `localhost:${servers[1].port}`)
268 const server3 = follows.find(f => f.following.host === `localhost:${servers[ 2 ].port}`) 292 const server3 = follows.find(f => f.following.host === `localhost:${servers[2].port}`)
269 293
270 expect(server3).to.not.be.undefined 294 expect(server3).to.not.be.undefined
271 expect(server3.following.hostRedundancyAllowed).to.be.false 295 expect(server3.following.hostRedundancyAllowed).to.be.false
@@ -410,8 +434,8 @@ describe('Test videos redundancy', function () {
410 it('Should view 2 times the first video to have > min_views config', async function () { 434 it('Should view 2 times the first video to have > min_views config', async function () {
411 this.timeout(80000) 435 this.timeout(80000)
412 436
413 await viewVideo(servers[ 0 ].url, video1Server2UUID) 437 await viewVideo(servers[0].url, video1Server2UUID)
414 await viewVideo(servers[ 2 ].url, video1Server2UUID) 438 await viewVideo(servers[2].url, video1Server2UUID)
415 439
416 await wait(10000) 440 await wait(10000)
417 await waitJobs(servers) 441 await waitJobs(servers)
@@ -446,6 +470,74 @@ describe('Test videos redundancy', function () {
446 }) 470 })
447 }) 471 })
448 472
473 describe('With manual strategy', function () {
474 before(function () {
475 this.timeout(120000)
476
477 return flushAndRunServers(null)
478 })
479
480 it('Should have 1 webseed on the first video', async function () {
481 await check1WebSeed()
482 await check0PlaylistRedundancies()
483 await checkStatsWith1Webseed('manual')
484 })
485
486 it('Should create a redundancy on first video', async function () {
487 await addVideoRedundancy({
488 url: servers[0].url,
489 accessToken: servers[0].accessToken,
490 videoId: video1Server2Id
491 })
492 })
493
494 it('Should have 2 webseeds on the first video', async function () {
495 this.timeout(80000)
496
497 await waitJobs(servers)
498 await waitUntilLog(servers[0], 'Duplicated ', 5)
499 await waitJobs(servers)
500
501 await check2Webseeds()
502 await check1PlaylistRedundancies()
503 await checkStatsWith2Webseed('manual')
504 })
505
506 it('Should manually remove redundancies on server 1 and remove duplicated videos', async function () {
507 this.timeout(80000)
508
509 const res = await listVideoRedundancies({
510 url: servers[0].url,
511 accessToken: servers[0].accessToken,
512 target: 'remote-videos'
513 })
514
515 const videos = res.body.data as VideoRedundancy[]
516 expect(videos).to.have.lengthOf(1)
517
518 const video = videos[0]
519 for (const r of video.redundancies.files.concat(video.redundancies.streamingPlaylists)) {
520 await removeVideoRedundancy({
521 url: servers[0].url,
522 accessToken: servers[0].accessToken,
523 redundancyId: r.id
524 })
525 }
526
527 await waitJobs(servers)
528 await wait(5000)
529
530 await check1WebSeed()
531 await check0PlaylistRedundancies()
532
533 await checkVideoFilesWereRemoved(video1Server2UUID, servers[0].serverNumber, [ 'videos' ])
534 })
535
536 after(async function () {
537 await cleanupTests(servers)
538 })
539 })
540
449 describe('Test expiration', function () { 541 describe('Test expiration', function () {
450 const strategy = 'recently-added' 542 const strategy = 'recently-added'
451 543
@@ -528,7 +620,7 @@ describe('Test videos redundancy', function () {
528 await check1PlaylistRedundancies() 620 await check1PlaylistRedundancies()
529 await checkStatsWith2Webseed(strategy) 621 await checkStatsWith2Webseed(strategy)
530 622
531 const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, { name: 'video 2 server 2' }) 623 const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video 2 server 2' })
532 video2Server2UUID = res.body.video.uuid 624 video2Server2UUID = res.body.video.uuid
533 }) 625 })
534 626
@@ -560,8 +652,8 @@ describe('Test videos redundancy', function () {
560 652
561 await waitJobs(servers) 653 await waitJobs(servers)
562 654
563 killallServers([ servers[ 0 ] ]) 655 killallServers([ servers[0] ])
564 await reRunServer(servers[ 0 ], { 656 await reRunServer(servers[0], {
565 redundancy: { 657 redundancy: {
566 videos: { 658 videos: {
567 check_interval: '1 second', 659 check_interval: '1 second',
diff --git a/server/tests/api/search/search-activitypub-video-channels.ts b/server/tests/api/search/search-activitypub-video-channels.ts
index d5f0a5457..d7e3ed5be 100644
--- a/server/tests/api/search/search-activitypub-video-channels.ts
+++ b/server/tests/api/search/search-activitypub-video-channels.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
@@ -39,7 +39,7 @@ describe('Test ActivityPub video channels search', function () {
39 await setAccessTokensToServers(servers) 39 await setAccessTokensToServers(servers)
40 40
41 { 41 {
42 await createUser({ url: servers[ 0 ].url, accessToken: servers[ 0 ].accessToken, username: 'user1_server1', password: 'password' }) 42 await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: 'user1_server1', password: 'password' })
43 const channel = { 43 const channel = {
44 name: 'channel1_server1', 44 name: 'channel1_server1',
45 displayName: 'Channel 1 server 1' 45 displayName: 'Channel 1 server 1'
@@ -49,7 +49,7 @@ describe('Test ActivityPub video channels search', function () {
49 49
50 { 50 {
51 const user = { username: 'user1_server2', password: 'password' } 51 const user = { username: 'user1_server2', password: 'password' }
52 await createUser({ url: servers[ 1 ].url, accessToken: servers[ 1 ].accessToken, username: user.username, password: user.password }) 52 await createUser({ url: servers[1].url, accessToken: servers[1].accessToken, username: user.username, password: user.password })
53 userServer2Token = await userLogin(servers[1], user) 53 userServer2Token = await userLogin(servers[1], user)
54 54
55 const channel = { 55 const channel = {
@@ -70,8 +70,8 @@ describe('Test ActivityPub video channels search', function () {
70 this.timeout(15000) 70 this.timeout(15000)
71 71
72 { 72 {
73 const search = 'http://localhost:' + servers[ 1 ].port + '/video-channels/channel1_server3' 73 const search = 'http://localhost:' + servers[1].port + '/video-channels/channel1_server3'
74 const res = await searchVideoChannel(servers[ 0 ].url, search, servers[ 0 ].accessToken) 74 const res = await searchVideoChannel(servers[0].url, search, servers[0].accessToken)
75 75
76 expect(res.body.total).to.equal(0) 76 expect(res.body.total).to.equal(0)
77 expect(res.body.data).to.be.an('array') 77 expect(res.body.data).to.be.an('array')
@@ -80,7 +80,7 @@ describe('Test ActivityPub video channels search', function () {
80 80
81 { 81 {
82 // Without token 82 // Without token
83 const search = 'http://localhost:' + servers[ 1 ].port + '/video-channels/channel1_server2' 83 const search = 'http://localhost:' + servers[1].port + '/video-channels/channel1_server2'
84 const res = await searchVideoChannel(servers[0].url, search) 84 const res = await searchVideoChannel(servers[0].url, search)
85 85
86 expect(res.body.total).to.equal(0) 86 expect(res.body.total).to.equal(0)
@@ -91,35 +91,35 @@ describe('Test ActivityPub video channels search', function () {
91 91
92 it('Should search a local video channel', async function () { 92 it('Should search a local video channel', async function () {
93 const searches = [ 93 const searches = [
94 'http://localhost:' + servers[ 0 ].port + '/video-channels/channel1_server1', 94 'http://localhost:' + servers[0].port + '/video-channels/channel1_server1',
95 'channel1_server1@localhost:' + servers[ 0 ].port 95 'channel1_server1@localhost:' + servers[0].port
96 ] 96 ]
97 97
98 for (const search of searches) { 98 for (const search of searches) {
99 const res = await searchVideoChannel(servers[ 0 ].url, search) 99 const res = await searchVideoChannel(servers[0].url, search)
100 100
101 expect(res.body.total).to.equal(1) 101 expect(res.body.total).to.equal(1)
102 expect(res.body.data).to.be.an('array') 102 expect(res.body.data).to.be.an('array')
103 expect(res.body.data).to.have.lengthOf(1) 103 expect(res.body.data).to.have.lengthOf(1)
104 expect(res.body.data[ 0 ].name).to.equal('channel1_server1') 104 expect(res.body.data[0].name).to.equal('channel1_server1')
105 expect(res.body.data[ 0 ].displayName).to.equal('Channel 1 server 1') 105 expect(res.body.data[0].displayName).to.equal('Channel 1 server 1')
106 } 106 }
107 }) 107 })
108 108
109 it('Should search a remote video channel with URL or handle', async function () { 109 it('Should search a remote video channel with URL or handle', async function () {
110 const searches = [ 110 const searches = [
111 'http://localhost:' + servers[ 1 ].port + '/video-channels/channel1_server2', 111 'http://localhost:' + servers[1].port + '/video-channels/channel1_server2',
112 'channel1_server2@localhost:' + servers[ 1 ].port 112 'channel1_server2@localhost:' + servers[1].port
113 ] 113 ]
114 114
115 for (const search of searches) { 115 for (const search of searches) {
116 const res = await searchVideoChannel(servers[ 0 ].url, search, servers[ 0 ].accessToken) 116 const res = await searchVideoChannel(servers[0].url, search, servers[0].accessToken)
117 117
118 expect(res.body.total).to.equal(1) 118 expect(res.body.total).to.equal(1)
119 expect(res.body.data).to.be.an('array') 119 expect(res.body.data).to.be.an('array')
120 expect(res.body.data).to.have.lengthOf(1) 120 expect(res.body.data).to.have.lengthOf(1)
121 expect(res.body.data[ 0 ].name).to.equal('channel1_server2') 121 expect(res.body.data[0].name).to.equal('channel1_server2')
122 expect(res.body.data[ 0 ].displayName).to.equal('Channel 1 server 2') 122 expect(res.body.data[0].displayName).to.equal('Channel 1 server 2')
123 } 123 }
124 }) 124 })
125 125
@@ -137,13 +137,13 @@ describe('Test ActivityPub video channels search', function () {
137 137
138 await waitJobs(servers) 138 await waitJobs(servers)
139 139
140 const res = await getVideoChannelVideos(servers[0].url, null, 'channel1_server2@localhost:' + servers[ 1 ].port, 0, 5) 140 const res = await getVideoChannelVideos(servers[0].url, null, 'channel1_server2@localhost:' + servers[1].port, 0, 5)
141 expect(res.body.total).to.equal(0) 141 expect(res.body.total).to.equal(0)
142 expect(res.body.data).to.have.lengthOf(0) 142 expect(res.body.data).to.have.lengthOf(0)
143 }) 143 })
144 144
145 it('Should list video channel videos of server 2 with token', async function () { 145 it('Should list video channel videos of server 2 with token', async function () {
146 const res = await getVideoChannelVideos(servers[0].url, servers[0].accessToken, 'channel1_server2@localhost:' + servers[ 1 ].port, 0, 5) 146 const res = await getVideoChannelVideos(servers[0].url, servers[0].accessToken, 'channel1_server2@localhost:' + servers[1].port, 0, 5)
147 147
148 expect(res.body.total).to.equal(1) 148 expect(res.body.total).to.equal(1)
149 expect(res.body.data[0].name).to.equal('video 1 server 2') 149 expect(res.body.data[0].name).to.equal('video 1 server 2')
@@ -159,7 +159,7 @@ describe('Test ActivityPub video channels search', function () {
159 // Expire video channel 159 // Expire video channel
160 await wait(10000) 160 await wait(10000)
161 161
162 const search = 'http://localhost:' + servers[ 1 ].port + '/video-channels/channel1_server2' 162 const search = 'http://localhost:' + servers[1].port + '/video-channels/channel1_server2'
163 const res = await searchVideoChannel(servers[0].url, search, servers[0].accessToken) 163 const res = await searchVideoChannel(servers[0].url, search, servers[0].accessToken)
164 expect(res.body.total).to.equal(1) 164 expect(res.body.total).to.equal(1)
165 expect(res.body.data).to.have.lengthOf(1) 165 expect(res.body.data).to.have.lengthOf(1)
@@ -182,12 +182,12 @@ describe('Test ActivityPub video channels search', function () {
182 // Expire video channel 182 // Expire video channel
183 await wait(10000) 183 await wait(10000)
184 184
185 const search = 'http://localhost:' + servers[ 1 ].port + '/video-channels/channel1_server2' 185 const search = 'http://localhost:' + servers[1].port + '/video-channels/channel1_server2'
186 await searchVideoChannel(servers[0].url, search, servers[0].accessToken) 186 await searchVideoChannel(servers[0].url, search, servers[0].accessToken)
187 187
188 await waitJobs(servers) 188 await waitJobs(servers)
189 189
190 const videoChannelName = 'channel1_server2@localhost:' + servers[ 1 ].port 190 const videoChannelName = 'channel1_server2@localhost:' + servers[1].port
191 const res = await getVideoChannelVideos(servers[0].url, servers[0].accessToken, videoChannelName, 0, 5, '-createdAt') 191 const res = await getVideoChannelVideos(servers[0].url, servers[0].accessToken, videoChannelName, 0, 5, '-createdAt')
192 192
193 expect(res.body.total).to.equal(2) 193 expect(res.body.total).to.equal(2)
@@ -204,7 +204,7 @@ describe('Test ActivityPub video channels search', function () {
204 // Expire video 204 // Expire video
205 await wait(10000) 205 await wait(10000)
206 206
207 const search = 'http://localhost:' + servers[ 1 ].port + '/video-channels/channel1_server2' 207 const search = 'http://localhost:' + servers[1].port + '/video-channels/channel1_server2'
208 const res = await searchVideoChannel(servers[0].url, search, servers[0].accessToken) 208 const res = await searchVideoChannel(servers[0].url, search, servers[0].accessToken)
209 expect(res.body.total).to.equal(0) 209 expect(res.body.total).to.equal(0)
210 expect(res.body.data).to.have.lengthOf(0) 210 expect(res.body.data).to.have.lengthOf(0)
diff --git a/server/tests/api/search/search-activitypub-videos.ts b/server/tests/api/search/search-activitypub-videos.ts
index dbfefadda..c62dfca0d 100644
--- a/server/tests/api/search/search-activitypub-videos.ts
+++ b/server/tests/api/search/search-activitypub-videos.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
@@ -34,12 +34,12 @@ describe('Test ActivityPub videos search', function () {
34 await setAccessTokensToServers(servers) 34 await setAccessTokensToServers(servers)
35 35
36 { 36 {
37 const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'video 1 on server 1' }) 37 const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video 1 on server 1' })
38 videoServer1UUID = res.body.video.uuid 38 videoServer1UUID = res.body.video.uuid
39 } 39 }
40 40
41 { 41 {
42 const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, { name: 'video 1 on server 2' }) 42 const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video 1 on server 2' })
43 videoServer2UUID = res.body.video.uuid 43 videoServer2UUID = res.body.video.uuid
44 } 44 }
45 45
@@ -49,7 +49,7 @@ describe('Test ActivityPub videos search', function () {
49 it('Should not find a remote video', async function () { 49 it('Should not find a remote video', async function () {
50 { 50 {
51 const search = 'http://localhost:' + servers[1].port + '/videos/watch/43' 51 const search = 'http://localhost:' + servers[1].port + '/videos/watch/43'
52 const res = await searchVideoWithToken(servers[ 0 ].url, search, servers[ 0 ].accessToken) 52 const res = await searchVideoWithToken(servers[0].url, search, servers[0].accessToken)
53 53
54 expect(res.body.total).to.equal(0) 54 expect(res.body.total).to.equal(0)
55 expect(res.body.data).to.be.an('array') 55 expect(res.body.data).to.be.an('array')
diff --git a/server/tests/api/search/search-videos.ts b/server/tests/api/search/search-videos.ts
index 7882d9373..4801fe04a 100644
--- a/server/tests/api/search/search-videos.ts
+++ b/server/tests/api/search/search-videos.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
@@ -78,7 +78,7 @@ describe('Test videos search', function () {
78 const attributes5 = immutableAssign(attributes1, { name: attributes1.name + ' - 5', licence: 2, language: undefined }) 78 const attributes5 = immutableAssign(attributes1, { name: attributes1.name + ' - 5', licence: 2, language: undefined })
79 await uploadVideo(server.url, server.accessToken, attributes5) 79 await uploadVideo(server.url, server.accessToken, attributes5)
80 80
81 const attributes6 = immutableAssign(attributes1, { name: attributes1.name + ' - 6', tags: [ 't1', 't2 '] }) 81 const attributes6 = immutableAssign(attributes1, { name: attributes1.name + ' - 6', tags: [ 't1', 't2' ] })
82 await uploadVideo(server.url, server.accessToken, attributes6) 82 await uploadVideo(server.url, server.accessToken, attributes6)
83 83
84 const attributes7 = immutableAssign(attributes1, { 84 const attributes7 = immutableAssign(attributes1, {
@@ -269,16 +269,16 @@ describe('Test videos search', function () {
269 { 269 {
270 const res = await advancedVideosSearch(server.url, query) 270 const res = await advancedVideosSearch(server.url, query)
271 expect(res.body.total).to.equal(2) 271 expect(res.body.total).to.equal(2)
272 expect(res.body.data[ 0 ].name).to.equal('1111 2222 3333 - 3') 272 expect(res.body.data[0].name).to.equal('1111 2222 3333 - 3')
273 expect(res.body.data[ 1 ].name).to.equal('1111 2222 3333 - 4') 273 expect(res.body.data[1].name).to.equal('1111 2222 3333 - 4')
274 } 274 }
275 275
276 { 276 {
277 const res = await advancedVideosSearch(server.url, immutableAssign(query, { languageOneOf: [ 'pl', 'en', '_unknown' ] })) 277 const res = await advancedVideosSearch(server.url, immutableAssign(query, { languageOneOf: [ 'pl', 'en', '_unknown' ] }))
278 expect(res.body.total).to.equal(3) 278 expect(res.body.total).to.equal(3)
279 expect(res.body.data[ 0 ].name).to.equal('1111 2222 3333 - 3') 279 expect(res.body.data[0].name).to.equal('1111 2222 3333 - 3')
280 expect(res.body.data[ 1 ].name).to.equal('1111 2222 3333 - 4') 280 expect(res.body.data[1].name).to.equal('1111 2222 3333 - 4')
281 expect(res.body.data[ 2 ].name).to.equal('1111 2222 3333 - 5') 281 expect(res.body.data[2].name).to.equal('1111 2222 3333 - 5')
282 } 282 }
283 283
284 { 284 {
diff --git a/server/tests/api/server/auto-follows.ts b/server/tests/api/server/auto-follows.ts
index a06f578fc..7efccc3e2 100644
--- a/server/tests/api/server/auto-follows.ts
+++ b/server/tests/api/server/auto-follows.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
@@ -59,9 +59,10 @@ async function server1Follows2 (servers: ServerInfo[]) {
59 59
60async function resetFollows (servers: ServerInfo[]) { 60async function resetFollows (servers: ServerInfo[]) {
61 try { 61 try {
62 await unfollow(servers[ 0 ].url, servers[ 0 ].accessToken, servers[ 1 ]) 62 await unfollow(servers[0].url, servers[0].accessToken, servers[1])
63 await unfollow(servers[ 1 ].url, servers[ 1 ].accessToken, servers[ 0 ]) 63 await unfollow(servers[1].url, servers[1].accessToken, servers[0])
64 } catch { /* empty */ } 64 } catch { /* empty */
65 }
65 66
66 await waitJobs(servers) 67 await waitJobs(servers)
67 68
@@ -163,8 +164,8 @@ describe('Test auto follows', function () {
163 await wait(5000) 164 await wait(5000)
164 await waitJobs(servers) 165 await waitJobs(servers)
165 166
166 await checkFollow(servers[ 0 ], servers[ 1 ], false) 167 await checkFollow(servers[0], servers[1], false)
167 await checkFollow(servers[ 1 ], servers[ 0 ], false) 168 await checkFollow(servers[1], servers[0], false)
168 }) 169 })
169 170
170 it('Should auto follow the index', async function () { 171 it('Should auto follow the index', async function () {
@@ -176,7 +177,7 @@ describe('Test auto follows', function () {
176 followings: { 177 followings: {
177 instance: { 178 instance: {
178 autoFollowIndex: { 179 autoFollowIndex: {
179 indexUrl: 'http://localhost:42100', 180 indexUrl: 'http://localhost:42100/api/v1/instances/hosts',
180 enabled: true 181 enabled: true
181 } 182 }
182 } 183 }
@@ -187,7 +188,7 @@ describe('Test auto follows', function () {
187 await wait(5000) 188 await wait(5000)
188 await waitJobs(servers) 189 await waitJobs(servers)
189 190
190 await checkFollow(servers[ 0 ], servers[ 1 ], true) 191 await checkFollow(servers[0], servers[1], true)
191 192
192 await resetFollows(servers) 193 await resetFollows(servers)
193 }) 194 })
@@ -200,8 +201,8 @@ describe('Test auto follows', function () {
200 await wait(5000) 201 await wait(5000)
201 await waitJobs(servers) 202 await waitJobs(servers)
202 203
203 await checkFollow(servers[ 0 ], servers[ 1 ], false) 204 await checkFollow(servers[0], servers[1], false)
204 await checkFollow(servers[ 0 ], servers[ 2 ], true) 205 await checkFollow(servers[0], servers[2], true)
205 }) 206 })
206 }) 207 })
207 208
diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts
index cf99e5c0a..1d81e1dce 100644
--- a/server/tests/api/server/config.ts
+++ b/server/tests/api/server/config.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
@@ -11,11 +11,14 @@ import {
11 getAbout, 11 getAbout,
12 getConfig, 12 getConfig,
13 getCustomConfig, 13 getCustomConfig,
14 killallServers, parallelTests, 14 killallServers,
15 parallelTests,
15 registerUser, 16 registerUser,
16 reRunServer, ServerInfo, 17 reRunServer,
18 ServerInfo,
17 setAccessTokensToServers, 19 setAccessTokensToServers,
18 updateCustomConfig, uploadVideo 20 updateCustomConfig,
21 uploadVideo
19} from '../../../../shared/extra-utils' 22} from '../../../../shared/extra-utils'
20import { ServerConfig } from '../../../../shared/models' 23import { ServerConfig } from '../../../../shared/models'
21 24
@@ -24,8 +27,7 @@ const expect = chai.expect
24function checkInitialConfig (server: ServerInfo, data: CustomConfig) { 27function checkInitialConfig (server: ServerInfo, data: CustomConfig) {
25 expect(data.instance.name).to.equal('PeerTube') 28 expect(data.instance.name).to.equal('PeerTube')
26 expect(data.instance.shortDescription).to.equal( 29 expect(data.instance.shortDescription).to.equal(
27 'PeerTube, a federated (ActivityPub) video streaming platform using P2P (BitTorrent) directly in the web browser ' + 30 'PeerTube, an ActivityPub-federated video streaming platform using P2P directly in your web browser.'
28 'with WebTorrent and Angular.'
29 ) 31 )
30 expect(data.instance.description).to.equal('Welcome to this PeerTube instance!') 32 expect(data.instance.description).to.equal('Welcome to this PeerTube instance!')
31 33
diff --git a/server/tests/api/server/contact-form.ts b/server/tests/api/server/contact-form.ts
index e4e895acb..8d1270358 100644
--- a/server/tests/api/server/contact-form.ts
+++ b/server/tests/api/server/contact-form.ts
@@ -1,16 +1,8 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { 5import { cleanupTests, flushAndRunServer, ServerInfo, setAccessTokensToServers, wait } from '../../../../shared/extra-utils'
6 flushTests,
7 killallServers,
8 flushAndRunServer,
9 ServerInfo,
10 setAccessTokensToServers,
11 wait,
12 cleanupTests
13} from '../../../../shared/extra-utils'
14import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email' 6import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email'
15import { waitJobs } from '../../../../shared/extra-utils/server/jobs' 7import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
16import { sendContactForm } from '../../../../shared/extra-utils/server/contact-form' 8import { sendContactForm } from '../../../../shared/extra-utils/server/contact-form'
@@ -54,7 +46,7 @@ describe('Test contact form', function () {
54 const email = emails[0] 46 const email = emails[0]
55 47
56 expect(email['from'][0]['address']).equal('test-admin@localhost') 48 expect(email['from'][0]['address']).equal('test-admin@localhost')
57 expect(email['from'][0]['name']).equal('toto@example.com') 49 expect(email['replyTo'][0]['address']).equal('toto@example.com')
58 expect(email['to'][0]['address']).equal('admin' + server.internalServerNumber + '@example.com') 50 expect(email['to'][0]['address']).equal('admin' + server.internalServerNumber + '@example.com')
59 expect(email['subject']).contains('my subject') 51 expect(email['subject']).contains('my subject')
60 expect(email['text']).contains('my super message') 52 expect(email['text']).contains('my super message')
diff --git a/server/tests/api/server/email.ts b/server/tests/api/server/email.ts
index c55a221f2..95b64a459 100644
--- a/server/tests/api/server/email.ts
+++ b/server/tests/api/server/email.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
@@ -28,10 +28,12 @@ const expect = chai.expect
28describe('Test emails', function () { 28describe('Test emails', function () {
29 let server: ServerInfo 29 let server: ServerInfo
30 let userId: number 30 let userId: number
31 let userId2: number
31 let userAccessToken: string 32 let userAccessToken: string
32 let videoUUID: string 33 let videoUUID: string
33 let videoUserUUID: string 34 let videoUserUUID: string
34 let verificationString: string 35 let verificationString: string
36 let verificationString2: string
35 const emails: object[] = [] 37 const emails: object[] = []
36 const user = { 38 const user = {
37 username: 'user_1', 39 username: 'user_1',
@@ -122,6 +124,56 @@ describe('Test emails', function () {
122 }) 124 })
123 }) 125 })
124 126
127 describe('When creating a user without password', function () {
128 it('Should send a create password email', async function () {
129 this.timeout(10000)
130
131 await createUser({
132 url: server.url,
133 accessToken: server.accessToken,
134 username: 'create_password',
135 password: ''
136 })
137
138 await waitJobs(server)
139 expect(emails).to.have.lengthOf(2)
140
141 const email = emails[1]
142
143 expect(email['from'][0]['name']).equal('localhost:' + server.port)
144 expect(email['from'][0]['address']).equal('test-admin@localhost')
145 expect(email['to'][0]['address']).equal('create_password@example.com')
146 expect(email['subject']).contains('account')
147 expect(email['subject']).contains('password')
148
149 const verificationStringMatches = /verificationString=([a-z0-9]+)/.exec(email['text'])
150 expect(verificationStringMatches).not.to.be.null
151
152 verificationString2 = verificationStringMatches[1]
153 expect(verificationString2).to.have.length.above(2)
154
155 const userIdMatches = /userId=([0-9]+)/.exec(email['text'])
156 expect(userIdMatches).not.to.be.null
157
158 userId2 = parseInt(userIdMatches[1], 10)
159 })
160
161 it('Should not reset the password with an invalid verification string', async function () {
162 await resetPassword(server.url, userId2, verificationString2 + 'c', 'newly_created_password', 403)
163 })
164
165 it('Should reset the password', async function () {
166 await resetPassword(server.url, userId2, verificationString2, 'newly_created_password')
167 })
168
169 it('Should login with this new password', async function () {
170 await userLogin(server, {
171 username: 'create_password',
172 password: 'newly_created_password'
173 })
174 })
175 })
176
125 describe('When creating a video abuse', function () { 177 describe('When creating a video abuse', function () {
126 it('Should send the notification email', async function () { 178 it('Should send the notification email', async function () {
127 this.timeout(10000) 179 this.timeout(10000)
@@ -130,9 +182,9 @@ describe('Test emails', function () {
130 await reportVideoAbuse(server.url, server.accessToken, videoUUID, reason) 182 await reportVideoAbuse(server.url, server.accessToken, videoUUID, reason)
131 183
132 await waitJobs(server) 184 await waitJobs(server)
133 expect(emails).to.have.lengthOf(2) 185 expect(emails).to.have.lengthOf(3)
134 186
135 const email = emails[1] 187 const email = emails[2]
136 188
137 expect(email['from'][0]['name']).equal('localhost:' + server.port) 189 expect(email['from'][0]['name']).equal('localhost:' + server.port)
138 expect(email['from'][0]['address']).equal('test-admin@localhost') 190 expect(email['from'][0]['address']).equal('test-admin@localhost')
@@ -151,9 +203,9 @@ describe('Test emails', function () {
151 await blockUser(server.url, userId, server.accessToken, 204, reason) 203 await blockUser(server.url, userId, server.accessToken, 204, reason)
152 204
153 await waitJobs(server) 205 await waitJobs(server)
154 expect(emails).to.have.lengthOf(3) 206 expect(emails).to.have.lengthOf(4)
155 207
156 const email = emails[2] 208 const email = emails[3]
157 209
158 expect(email['from'][0]['name']).equal('localhost:' + server.port) 210 expect(email['from'][0]['name']).equal('localhost:' + server.port)
159 expect(email['from'][0]['address']).equal('test-admin@localhost') 211 expect(email['from'][0]['address']).equal('test-admin@localhost')
@@ -169,9 +221,9 @@ describe('Test emails', function () {
169 await unblockUser(server.url, userId, server.accessToken, 204) 221 await unblockUser(server.url, userId, server.accessToken, 204)
170 222
171 await waitJobs(server) 223 await waitJobs(server)
172 expect(emails).to.have.lengthOf(4) 224 expect(emails).to.have.lengthOf(5)
173 225
174 const email = emails[3] 226 const email = emails[4]
175 227
176 expect(email['from'][0]['name']).equal('localhost:' + server.port) 228 expect(email['from'][0]['name']).equal('localhost:' + server.port)
177 expect(email['from'][0]['address']).equal('test-admin@localhost') 229 expect(email['from'][0]['address']).equal('test-admin@localhost')
@@ -189,9 +241,9 @@ describe('Test emails', function () {
189 await addVideoToBlacklist(server.url, server.accessToken, videoUserUUID, reason) 241 await addVideoToBlacklist(server.url, server.accessToken, videoUserUUID, reason)
190 242
191 await waitJobs(server) 243 await waitJobs(server)
192 expect(emails).to.have.lengthOf(5) 244 expect(emails).to.have.lengthOf(6)
193 245
194 const email = emails[4] 246 const email = emails[5]
195 247
196 expect(email['from'][0]['name']).equal('localhost:' + server.port) 248 expect(email['from'][0]['name']).equal('localhost:' + server.port)
197 expect(email['from'][0]['address']).equal('test-admin@localhost') 249 expect(email['from'][0]['address']).equal('test-admin@localhost')
@@ -207,9 +259,9 @@ describe('Test emails', function () {
207 await removeVideoFromBlacklist(server.url, server.accessToken, videoUserUUID) 259 await removeVideoFromBlacklist(server.url, server.accessToken, videoUserUUID)
208 260
209 await waitJobs(server) 261 await waitJobs(server)
210 expect(emails).to.have.lengthOf(6) 262 expect(emails).to.have.lengthOf(7)
211 263
212 const email = emails[5] 264 const email = emails[6]
213 265
214 expect(email['from'][0]['name']).equal('localhost:' + server.port) 266 expect(email['from'][0]['name']).equal('localhost:' + server.port)
215 expect(email['from'][0]['address']).equal('test-admin@localhost') 267 expect(email['from'][0]['address']).equal('test-admin@localhost')
@@ -227,9 +279,9 @@ describe('Test emails', function () {
227 await askSendVerifyEmail(server.url, 'user_1@example.com') 279 await askSendVerifyEmail(server.url, 'user_1@example.com')
228 280
229 await waitJobs(server) 281 await waitJobs(server)
230 expect(emails).to.have.lengthOf(7) 282 expect(emails).to.have.lengthOf(8)
231 283
232 const email = emails[6] 284 const email = emails[7]
233 285
234 expect(email['from'][0]['name']).equal('localhost:' + server.port) 286 expect(email['from'][0]['name']).equal('localhost:' + server.port)
235 expect(email['from'][0]['address']).equal('test-admin@localhost') 287 expect(email['from'][0]['address']).equal('test-admin@localhost')
diff --git a/server/tests/api/server/follow-constraints.ts b/server/tests/api/server/follow-constraints.ts
index 46663bf7c..a73440286 100644
--- a/server/tests/api/server/follow-constraints.ts
+++ b/server/tests/api/server/follow-constraints.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
@@ -35,11 +35,11 @@ describe('Test follow constraints', function () {
35 await setAccessTokensToServers(servers) 35 await setAccessTokensToServers(servers)
36 36
37 { 37 {
38 const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'video server 1' }) 38 const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video server 1' })
39 video1UUID = res.body.video.uuid 39 video1UUID = res.body.video.uuid
40 } 40 }
41 { 41 {
42 const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, { name: 'video server 2' }) 42 const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video server 2' })
43 video2UUID = res.body.video.uuid 43 video2UUID = res.body.video.uuid
44 } 44 }
45 45
@@ -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({ url: servers[ 0 ].url, accessToken: servers[ 0 ].accessToken, username: user.username, password: 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])
diff --git a/server/tests/api/server/follows-moderation.ts b/server/tests/api/server/follows-moderation.ts
index 1984c9eb1..cee85cc4b 100644
--- a/server/tests/api/server/follows-moderation.ts
+++ b/server/tests/api/server/follows-moderation.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
@@ -24,7 +24,7 @@ const expect = chai.expect
24 24
25async function checkServer1And2HasFollowers (servers: ServerInfo[], state = 'accepted') { 25async function checkServer1And2HasFollowers (servers: ServerInfo[], state = 'accepted') {
26 { 26 {
27 const res = await getFollowingListPaginationAndSort({ url: servers[ 0 ].url, start: 0, count: 5, sort: 'createdAt' }) 27 const res = await getFollowingListPaginationAndSort({ url: servers[0].url, start: 0, count: 5, sort: 'createdAt' })
28 expect(res.body.total).to.equal(1) 28 expect(res.body.total).to.equal(1)
29 29
30 const follow = res.body.data[0] as ActorFollow 30 const follow = res.body.data[0] as ActorFollow
@@ -34,7 +34,7 @@ async function checkServer1And2HasFollowers (servers: ServerInfo[], state = 'acc
34 } 34 }
35 35
36 { 36 {
37 const res = await getFollowersListPaginationAndSort({ url: servers[ 1 ].url, start: 0, count: 5, sort: 'createdAt' }) 37 const res = await getFollowersListPaginationAndSort({ url: servers[1].url, start: 0, count: 5, sort: 'createdAt' })
38 expect(res.body.total).to.equal(1) 38 expect(res.body.total).to.equal(1)
39 39
40 const follow = res.body.data[0] as ActorFollow 40 const follow = res.body.data[0] as ActorFollow
@@ -46,12 +46,12 @@ async function checkServer1And2HasFollowers (servers: ServerInfo[], state = 'acc
46 46
47async function checkNoFollowers (servers: ServerInfo[]) { 47async function checkNoFollowers (servers: ServerInfo[]) {
48 { 48 {
49 const res = await getFollowingListPaginationAndSort({ url: servers[ 0 ].url, start: 0, count: 5, sort: 'createdAt' }) 49 const res = await getFollowingListPaginationAndSort({ url: servers[0].url, start: 0, count: 5, sort: 'createdAt' })
50 expect(res.body.total).to.equal(0) 50 expect(res.body.total).to.equal(0)
51 } 51 }
52 52
53 { 53 {
54 const res = await getFollowersListPaginationAndSort({ url: servers[ 1 ].url, start: 0, count: 5, sort: 'createdAt' }) 54 const res = await getFollowersListPaginationAndSort({ url: servers[1].url, start: 0, count: 5, sort: 'createdAt' })
55 expect(res.body.total).to.equal(0) 55 expect(res.body.total).to.equal(0)
56 } 56 }
57} 57}
@@ -164,17 +164,17 @@ describe('Test follows moderation', function () {
164 await waitJobs(servers) 164 await waitJobs(servers)
165 165
166 { 166 {
167 const res = await getFollowingListPaginationAndSort({ url: servers[ 0 ].url, start: 0, count: 5, sort: 'createdAt' }) 167 const res = await getFollowingListPaginationAndSort({ url: servers[0].url, start: 0, count: 5, sort: 'createdAt' })
168 expect(res.body.total).to.equal(2) 168 expect(res.body.total).to.equal(2)
169 } 169 }
170 170
171 { 171 {
172 const res = await getFollowersListPaginationAndSort({ url: servers[ 1 ].url, start: 0, count: 5, sort: 'createdAt' }) 172 const res = await getFollowersListPaginationAndSort({ url: servers[1].url, start: 0, count: 5, sort: 'createdAt' })
173 expect(res.body.total).to.equal(1) 173 expect(res.body.total).to.equal(1)
174 } 174 }
175 175
176 { 176 {
177 const res = await getFollowersListPaginationAndSort({ url: servers[ 2 ].url, start: 0, count: 5, sort: 'createdAt' }) 177 const res = await getFollowersListPaginationAndSort({ url: servers[2].url, start: 0, count: 5, sort: 'createdAt' })
178 expect(res.body.total).to.equal(1) 178 expect(res.body.total).to.equal(1)
179 } 179 }
180 180
@@ -184,7 +184,7 @@ describe('Test follows moderation', function () {
184 await checkServer1And2HasFollowers(servers) 184 await checkServer1And2HasFollowers(servers)
185 185
186 { 186 {
187 const res = await getFollowersListPaginationAndSort({ url: servers[ 2 ].url, start: 0, count: 5, sort: 'createdAt' }) 187 const res = await getFollowersListPaginationAndSort({ url: servers[2].url, start: 0, count: 5, sort: 'createdAt' })
188 expect(res.body.total).to.equal(0) 188 expect(res.body.total).to.equal(0)
189 } 189 }
190 }) 190 })
diff --git a/server/tests/api/server/follows.ts b/server/tests/api/server/follows.ts
index 4ffa9e791..b686af4e4 100644
--- a/server/tests/api/server/follows.ts
+++ b/server/tests/api/server/follows.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
@@ -78,14 +78,14 @@ describe('Test follows', function () {
78 }) 78 })
79 79
80 it('Should have 2 followings on server 1', async function () { 80 it('Should have 2 followings on server 1', async function () {
81 let res = await getFollowingListPaginationAndSort({ url: servers[ 0 ].url, start: 0, count: 1, sort: 'createdAt' }) 81 let res = await getFollowingListPaginationAndSort({ url: servers[0].url, start: 0, count: 1, sort: 'createdAt' })
82 let follows = res.body.data 82 let follows = res.body.data
83 83
84 expect(res.body.total).to.equal(2) 84 expect(res.body.total).to.equal(2)
85 expect(follows).to.be.an('array') 85 expect(follows).to.be.an('array')
86 expect(follows.length).to.equal(1) 86 expect(follows.length).to.equal(1)
87 87
88 res = await getFollowingListPaginationAndSort({ url: servers[ 0 ].url, start: 1, count: 1, sort: 'createdAt' }) 88 res = await getFollowingListPaginationAndSort({ url: servers[0].url, start: 1, count: 1, sort: 'createdAt' })
89 follows = follows.concat(res.body.data) 89 follows = follows.concat(res.body.data)
90 90
91 const server2Follow = follows.find(f => f.following.host === 'localhost:' + servers[1].port) 91 const server2Follow = follows.find(f => f.following.host === 'localhost:' + servers[1].port)
@@ -101,7 +101,7 @@ describe('Test follows', function () {
101 const sort = 'createdAt' 101 const sort = 'createdAt'
102 const start = 0 102 const start = 0
103 const count = 1 103 const count = 1
104 const url = servers[ 0 ].url 104 const url = servers[0].url
105 105
106 { 106 {
107 const search = ':' + servers[1].port 107 const search = ':' + servers[1].port
@@ -112,7 +112,7 @@ describe('Test follows', function () {
112 112
113 expect(res.body.total).to.equal(1) 113 expect(res.body.total).to.equal(1)
114 expect(follows.length).to.equal(1) 114 expect(follows.length).to.equal(1)
115 expect(follows[ 0 ].following.host).to.equal('localhost:' + servers[ 1 ].port) 115 expect(follows[0].following.host).to.equal('localhost:' + servers[1].port)
116 } 116 }
117 117
118 { 118 {
@@ -170,9 +170,9 @@ describe('Test follows', function () {
170 170
171 it('Should have 1 followers on server 2 and 3', async function () { 171 it('Should have 1 followers on server 2 and 3', async function () {
172 for (const server of [ servers[1], servers[2] ]) { 172 for (const server of [ servers[1], servers[2] ]) {
173 let res = await getFollowersListPaginationAndSort({ url: server.url, start: 0, count: 1, sort: 'createdAt' }) 173 const res = await getFollowersListPaginationAndSort({ url: server.url, start: 0, count: 1, sort: 'createdAt' })
174 174
175 let follows = res.body.data 175 const follows = res.body.data
176 expect(res.body.total).to.equal(1) 176 expect(res.body.total).to.equal(1)
177 expect(follows).to.be.an('array') 177 expect(follows).to.be.an('array')
178 expect(follows.length).to.equal(1) 178 expect(follows.length).to.equal(1)
@@ -181,7 +181,7 @@ describe('Test follows', function () {
181 }) 181 })
182 182
183 it('Should search/filter followers on server 2', async function () { 183 it('Should search/filter followers on server 2', async function () {
184 const url = servers[ 2 ].url 184 const url = servers[2].url
185 const start = 0 185 const start = 0
186 const count = 5 186 const count = 5
187 const sort = 'createdAt' 187 const sort = 'createdAt'
@@ -195,7 +195,7 @@ describe('Test follows', function () {
195 195
196 expect(res.body.total).to.equal(1) 196 expect(res.body.total).to.equal(1)
197 expect(follows.length).to.equal(1) 197 expect(follows.length).to.equal(1)
198 expect(follows[ 0 ].following.host).to.equal('localhost:' + servers[ 2 ].port) 198 expect(follows[0].following.host).to.equal('localhost:' + servers[2].port)
199 } 199 }
200 200
201 { 201 {
@@ -241,7 +241,7 @@ describe('Test follows', function () {
241 }) 241 })
242 242
243 it('Should have 0 followers on server 1', async function () { 243 it('Should have 0 followers on server 1', async function () {
244 const res = await getFollowersListPaginationAndSort({ url: servers[ 0 ].url, start: 0, count: 5, sort: 'createdAt' }) 244 const res = await getFollowersListPaginationAndSort({ url: servers[0].url, start: 0, count: 5, sort: 'createdAt' })
245 const follows = res.body.data 245 const follows = res.body.data
246 246
247 expect(res.body.total).to.equal(0) 247 expect(res.body.total).to.equal(0)
@@ -271,8 +271,8 @@ describe('Test follows', function () {
271 }) 271 })
272 272
273 it('Should not follow server 3 on server 1 anymore', async function () { 273 it('Should not follow server 3 on server 1 anymore', async function () {
274 const res = await getFollowingListPaginationAndSort({ url: servers[ 0 ].url, start: 0, count: 2, sort: 'createdAt' }) 274 const res = await getFollowingListPaginationAndSort({ url: servers[0].url, start: 0, count: 2, sort: 'createdAt' })
275 let follows = res.body.data 275 const follows = res.body.data
276 276
277 expect(res.body.total).to.equal(1) 277 expect(res.body.total).to.equal(1)
278 expect(follows).to.be.an('array') 278 expect(follows).to.be.an('array')
@@ -282,9 +282,9 @@ describe('Test follows', function () {
282 }) 282 })
283 283
284 it('Should not have server 1 as follower on server 3 anymore', async function () { 284 it('Should not have server 1 as follower on server 3 anymore', async function () {
285 const res = await getFollowersListPaginationAndSort({ url: servers[ 2 ].url, start: 0, count: 1, sort: 'createdAt' }) 285 const res = await getFollowersListPaginationAndSort({ url: servers[2].url, start: 0, count: 1, sort: 'createdAt' })
286 286
287 let follows = res.body.data 287 const follows = res.body.data
288 expect(res.body.total).to.equal(0) 288 expect(res.body.total).to.equal(0)
289 expect(follows).to.be.an('array') 289 expect(follows).to.be.an('array')
290 expect(follows.length).to.equal(0) 290 expect(follows.length).to.equal(0)
@@ -336,59 +336,59 @@ describe('Test follows', function () {
336 tags: [ 'tag1', 'tag2', 'tag3' ] 336 tags: [ 'tag1', 'tag2', 'tag3' ]
337 } 337 }
338 338
339 await uploadVideo(servers[ 2 ].url, servers[ 2 ].accessToken, { name: 'server3-2' }) 339 await uploadVideo(servers[2].url, servers[2].accessToken, { name: 'server3-2' })
340 await uploadVideo(servers[ 2 ].url, servers[ 2 ].accessToken, { name: 'server3-3' }) 340 await uploadVideo(servers[2].url, servers[2].accessToken, { name: 'server3-3' })
341 await uploadVideo(servers[ 2 ].url, servers[ 2 ].accessToken, video4Attributes) 341 await uploadVideo(servers[2].url, servers[2].accessToken, video4Attributes)
342 await uploadVideo(servers[ 2 ].url, servers[ 2 ].accessToken, { name: 'server3-5' }) 342 await uploadVideo(servers[2].url, servers[2].accessToken, { name: 'server3-5' })
343 await uploadVideo(servers[ 2 ].url, servers[ 2 ].accessToken, { name: 'server3-6' }) 343 await uploadVideo(servers[2].url, servers[2].accessToken, { name: 'server3-6' })
344 344
345 { 345 {
346 const user = { username: 'captain', password: 'password' } 346 const user = { username: 'captain', password: 'password' }
347 await createUser({ url: servers[ 2 ].url, accessToken: servers[ 2 ].accessToken, username: user.username, password: user.password }) 347 await createUser({ url: servers[2].url, accessToken: servers[2].accessToken, username: user.username, password: user.password })
348 const userAccessToken = await userLogin(servers[ 2 ], user) 348 const userAccessToken = await userLogin(servers[2], user)
349 349
350 const resVideos = await getVideosList(servers[ 2 ].url) 350 const resVideos = await getVideosList(servers[2].url)
351 video4 = resVideos.body.data.find(v => v.name === 'server3-4') 351 video4 = resVideos.body.data.find(v => v.name === 'server3-4')
352 352
353 { 353 {
354 await rateVideo(servers[ 2 ].url, servers[ 2 ].accessToken, video4.id, 'like') 354 await rateVideo(servers[2].url, servers[2].accessToken, video4.id, 'like')
355 await rateVideo(servers[ 2 ].url, userAccessToken, video4.id, 'dislike') 355 await rateVideo(servers[2].url, userAccessToken, video4.id, 'dislike')
356 } 356 }
357 357
358 { 358 {
359 { 359 {
360 const text = 'my super first comment' 360 const text = 'my super first comment'
361 const res = await addVideoCommentThread(servers[ 2 ].url, servers[ 2 ].accessToken, video4.id, text) 361 const res = await addVideoCommentThread(servers[2].url, servers[2].accessToken, video4.id, text)
362 const threadId = res.body.comment.id 362 const threadId = res.body.comment.id
363 363
364 const text1 = 'my super answer to thread 1' 364 const text1 = 'my super answer to thread 1'
365 const childCommentRes = await addVideoCommentReply(servers[ 2 ].url, servers[ 2 ].accessToken, video4.id, threadId, text1) 365 const childCommentRes = await addVideoCommentReply(servers[2].url, servers[2].accessToken, video4.id, threadId, text1)
366 const childCommentId = childCommentRes.body.comment.id 366 const childCommentId = childCommentRes.body.comment.id
367 367
368 const text2 = 'my super answer to answer of thread 1' 368 const text2 = 'my super answer to answer of thread 1'
369 await addVideoCommentReply(servers[ 2 ].url, servers[ 2 ].accessToken, video4.id, childCommentId, text2) 369 await addVideoCommentReply(servers[2].url, servers[2].accessToken, video4.id, childCommentId, text2)
370 370
371 const text3 = 'my second answer to thread 1' 371 const text3 = 'my second answer to thread 1'
372 await addVideoCommentReply(servers[ 2 ].url, servers[ 2 ].accessToken, video4.id, threadId, text3) 372 await addVideoCommentReply(servers[2].url, servers[2].accessToken, video4.id, threadId, text3)
373 } 373 }
374 374
375 { 375 {
376 const text = 'will be deleted' 376 const text = 'will be deleted'
377 const res = await addVideoCommentThread(servers[ 2 ].url, servers[ 2 ].accessToken, video4.id, text) 377 const res = await addVideoCommentThread(servers[2].url, servers[2].accessToken, video4.id, text)
378 const threadId = res.body.comment.id 378 const threadId = res.body.comment.id
379 379
380 const text1 = 'answer to deleted' 380 const text1 = 'answer to deleted'
381 await addVideoCommentReply(servers[ 2 ].url, servers[ 2 ].accessToken, video4.id, threadId, text1) 381 await addVideoCommentReply(servers[2].url, servers[2].accessToken, video4.id, threadId, text1)
382 382
383 const text2 = 'will also be deleted' 383 const text2 = 'will also be deleted'
384 const childCommentRes = await addVideoCommentReply(servers[ 2 ].url, servers[ 2 ].accessToken, video4.id, threadId, text2) 384 const childCommentRes = await addVideoCommentReply(servers[2].url, servers[2].accessToken, video4.id, threadId, text2)
385 const childCommentId = childCommentRes.body.comment.id 385 const childCommentId = childCommentRes.body.comment.id
386 386
387 const text3 = 'my second answer to deleted' 387 const text3 = 'my second answer to deleted'
388 await addVideoCommentReply(servers[ 2 ].url, servers[ 2 ].accessToken, video4.id, childCommentId, text3) 388 await addVideoCommentReply(servers[2].url, servers[2].accessToken, video4.id, childCommentId, text3)
389 389
390 await deleteVideoComment(servers[ 2 ].url, servers[ 2 ].accessToken, video4.id, threadId) 390 await deleteVideoComment(servers[2].url, servers[2].accessToken, video4.id, threadId)
391 await deleteVideoComment(servers[ 2 ].url, servers[ 2 ].accessToken, video4.id, childCommentId) 391 await deleteVideoComment(servers[2].url, servers[2].accessToken, video4.id, childCommentId)
392 } 392 }
393 } 393 }
394 394
@@ -406,7 +406,7 @@ describe('Test follows', function () {
406 await waitJobs(servers) 406 await waitJobs(servers)
407 407
408 // Server 1 follows server 3 408 // Server 1 follows server 3
409 await follow(servers[ 0 ].url, [ servers[ 2 ].url ], servers[ 0 ].accessToken) 409 await follow(servers[0].url, [ servers[2].url ], servers[0].accessToken)
410 410
411 await waitJobs(servers) 411 await waitJobs(servers)
412 }) 412 })
@@ -424,7 +424,7 @@ describe('Test follows', function () {
424 }) 424 })
425 425
426 it('Should have propagated videos', async function () { 426 it('Should have propagated videos', async function () {
427 const res = await getVideosList(servers[ 0 ].url) 427 const res = await getVideosList(servers[0].url)
428 expect(res.body.total).to.equal(7) 428 expect(res.body.total).to.equal(7)
429 429
430 const video2 = res.body.data.find(v => v.name === 'server3-2') 430 const video2 = res.body.data.find(v => v.name === 'server3-2')
@@ -470,7 +470,7 @@ describe('Test follows', function () {
470 } 470 }
471 ] 471 ]
472 } 472 }
473 await completeVideoCheck(servers[ 0 ].url, video4, checkAttributes) 473 await completeVideoCheck(servers[0].url, video4, checkAttributes)
474 }) 474 })
475 475
476 it('Should have propagated comments', async function () { 476 it('Should have propagated comments', async function () {
@@ -481,34 +481,34 @@ describe('Test follows', function () {
481 expect(res1.body.data).to.have.lengthOf(2) 481 expect(res1.body.data).to.have.lengthOf(2)
482 482
483 { 483 {
484 const comment: VideoComment = res1.body.data[ 0 ] 484 const comment: VideoComment = res1.body.data[0]
485 expect(comment.inReplyToCommentId).to.be.null 485 expect(comment.inReplyToCommentId).to.be.null
486 expect(comment.text).equal('my super first comment') 486 expect(comment.text).equal('my super first comment')
487 expect(comment.videoId).to.equal(video4.id) 487 expect(comment.videoId).to.equal(video4.id)
488 expect(comment.id).to.equal(comment.threadId) 488 expect(comment.id).to.equal(comment.threadId)
489 expect(comment.account.name).to.equal('root') 489 expect(comment.account.name).to.equal('root')
490 expect(comment.account.host).to.equal('localhost:' + servers[ 2 ].port) 490 expect(comment.account.host).to.equal('localhost:' + servers[2].port)
491 expect(comment.totalReplies).to.equal(3) 491 expect(comment.totalReplies).to.equal(3)
492 expect(dateIsValid(comment.createdAt as string)).to.be.true 492 expect(dateIsValid(comment.createdAt as string)).to.be.true
493 expect(dateIsValid(comment.updatedAt as string)).to.be.true 493 expect(dateIsValid(comment.updatedAt as string)).to.be.true
494 494
495 const threadId = comment.threadId 495 const threadId = comment.threadId
496 496
497 const res2 = await getVideoThreadComments(servers[ 0 ].url, video4.id, threadId) 497 const res2 = await getVideoThreadComments(servers[0].url, video4.id, threadId)
498 498
499 const tree: VideoCommentThreadTree = res2.body 499 const tree: VideoCommentThreadTree = res2.body
500 expect(tree.comment.text).equal('my super first comment') 500 expect(tree.comment.text).equal('my super first comment')
501 expect(tree.children).to.have.lengthOf(2) 501 expect(tree.children).to.have.lengthOf(2)
502 502
503 const firstChild = tree.children[ 0 ] 503 const firstChild = tree.children[0]
504 expect(firstChild.comment.text).to.equal('my super answer to thread 1') 504 expect(firstChild.comment.text).to.equal('my super answer to thread 1')
505 expect(firstChild.children).to.have.lengthOf(1) 505 expect(firstChild.children).to.have.lengthOf(1)
506 506
507 const childOfFirstChild = firstChild.children[ 0 ] 507 const childOfFirstChild = firstChild.children[0]
508 expect(childOfFirstChild.comment.text).to.equal('my super answer to answer of thread 1') 508 expect(childOfFirstChild.comment.text).to.equal('my super answer to answer of thread 1')
509 expect(childOfFirstChild.children).to.have.lengthOf(0) 509 expect(childOfFirstChild.children).to.have.lengthOf(0)
510 510
511 const secondChild = tree.children[ 1 ] 511 const secondChild = tree.children[1]
512 expect(secondChild.comment.text).to.equal('my second answer to thread 1') 512 expect(secondChild.comment.text).to.equal('my second answer to thread 1')
513 expect(secondChild.children).to.have.lengthOf(0) 513 expect(secondChild.children).to.have.lengthOf(0)
514 } 514 }
@@ -569,7 +569,7 @@ describe('Test follows', function () {
569 569
570 await waitJobs(servers) 570 await waitJobs(servers)
571 571
572 let res = await getVideosList(servers[ 0 ].url) 572 const res = await getVideosList(servers[0].url)
573 expect(res.body.total).to.equal(1) 573 expect(res.body.total).to.equal(1)
574 }) 574 })
575 575
diff --git a/server/tests/api/server/handle-down.ts b/server/tests/api/server/handle-down.ts
index 7e36067f1..2cf6e15ad 100644
--- a/server/tests/api/server/handle-down.ts
+++ b/server/tests/api/server/handle-down.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
@@ -8,6 +8,7 @@ import { VideoCommentThreadTree } from '../../../../shared/models/videos/video-c
8 8
9import { 9import {
10 cleanupTests, 10 cleanupTests,
11 closeAllSequelize,
11 completeVideoCheck, 12 completeVideoCheck,
12 flushAndRunMultipleServers, 13 flushAndRunMultipleServers,
13 getVideo, 14 getVideo,
@@ -17,11 +18,12 @@ import {
17 reRunServer, 18 reRunServer,
18 ServerInfo, 19 ServerInfo,
19 setAccessTokensToServers, 20 setAccessTokensToServers,
21 setActorFollowScores,
20 unfollow, 22 unfollow,
21 updateVideo, 23 updateVideo,
22 uploadVideo, uploadVideoAndGetId, 24 uploadVideo,
23 wait, 25 uploadVideoAndGetId,
24 setActorFollowScores, closeAllSequelize 26 wait
25} from '../../../../shared/extra-utils' 27} from '../../../../shared/extra-utils'
26import { follow, getFollowersListPaginationAndSort } from '../../../../shared/extra-utils/server/follows' 28import { follow, getFollowersListPaginationAndSort } from '../../../../shared/extra-utils/server/follows'
27import { getJobsListPaginationAndSort, waitJobs } from '../../../../shared/extra-utils/server/jobs' 29import { getJobsListPaginationAndSort, waitJobs } from '../../../../shared/extra-utils/server/jobs'
@@ -44,7 +46,7 @@ describe('Test handle downs', function () {
44 let missedVideo2: Video 46 let missedVideo2: Video
45 let unlistedVideo: Video 47 let unlistedVideo: Video
46 48
47 let videoIdsServer1: number[] = [] 49 const videoIdsServer1: number[] = []
48 50
49 const videoAttributes = { 51 const videoAttributes = {
50 name: 'my super name for server 1', 52 name: 'my super name for server 1',
@@ -137,7 +139,7 @@ describe('Test handle downs', function () {
137 139
138 // Remove server 2 follower 140 // Remove server 2 follower
139 for (let i = 0; i < 10; i++) { 141 for (let i = 0; i < 10; i++) {
140 await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, videoAttributes) 142 await uploadVideo(servers[0].url, servers[0].accessToken, videoAttributes)
141 } 143 }
142 144
143 await waitJobs(servers[0]) 145 await waitJobs(servers[0])
@@ -145,14 +147,14 @@ describe('Test handle downs', function () {
145 // Kill server 3 147 // Kill server 3
146 killallServers([ servers[2] ]) 148 killallServers([ servers[2] ])
147 149
148 const resLastVideo1 = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, videoAttributes) 150 const resLastVideo1 = await uploadVideo(servers[0].url, servers[0].accessToken, videoAttributes)
149 missedVideo1 = resLastVideo1.body.video 151 missedVideo1 = resLastVideo1.body.video
150 152
151 const resLastVideo2 = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, videoAttributes) 153 const resLastVideo2 = await uploadVideo(servers[0].url, servers[0].accessToken, videoAttributes)
152 missedVideo2 = resLastVideo2.body.video 154 missedVideo2 = resLastVideo2.body.video
153 155
154 // Unlisted video 156 // Unlisted video
155 let resVideo = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, unlistedVideoAttributes) 157 const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, unlistedVideoAttributes)
156 unlistedVideo = resVideo.body.video 158 unlistedVideo = resVideo.body.video
157 159
158 // Add comments to video 2 160 // Add comments to video 2
@@ -174,7 +176,7 @@ describe('Test handle downs', function () {
174 await wait(11000) 176 await wait(11000)
175 177
176 // Only server 3 is still a follower of server 1 178 // Only server 3 is still a follower of server 1
177 const res = await getFollowersListPaginationAndSort({ url: servers[ 0 ].url, start: 0, count: 2, sort: 'createdAt' }) 179 const res = await getFollowersListPaginationAndSort({ url: servers[0].url, start: 0, count: 2, sort: 'createdAt' })
178 expect(res.body.data).to.be.an('array') 180 expect(res.body.data).to.be.an('array')
179 expect(res.body.data).to.have.lengthOf(1) 181 expect(res.body.data).to.have.lengthOf(1)
180 expect(res.body.data[0].follower.host).to.equal('localhost:' + servers[2].port) 182 expect(res.body.data[0].follower.host).to.equal('localhost:' + servers[2].port)
@@ -185,8 +187,8 @@ describe('Test handle downs', function () {
185 187
186 for (const state of states) { 188 for (const state of states) {
187 const res = await getJobsListPaginationAndSort({ 189 const res = await getJobsListPaginationAndSort({
188 url: servers[ 0 ].url, 190 url: servers[0].url,
189 accessToken: servers[ 0 ].accessToken, 191 accessToken: servers[0].accessToken,
190 state: state, 192 state: state,
191 start: 0, 193 start: 0,
192 count: 50, 194 count: 50,
@@ -209,7 +211,7 @@ describe('Test handle downs', function () {
209 211
210 await waitJobs(servers) 212 await waitJobs(servers)
211 213
212 const res = await getFollowersListPaginationAndSort({ url: servers[ 0 ].url, start: 0, count: 2, sort: 'createdAt' }) 214 const res = await getFollowersListPaginationAndSort({ url: servers[0].url, start: 0, count: 2, sort: 'createdAt' })
213 expect(res.body.data).to.be.an('array') 215 expect(res.body.data).to.be.an('array')
214 expect(res.body.data).to.have.lengthOf(2) 216 expect(res.body.data).to.have.lengthOf(2)
215 }) 217 })
@@ -221,8 +223,8 @@ describe('Test handle downs', function () {
221 expect(res1.body.data).to.be.an('array') 223 expect(res1.body.data).to.be.an('array')
222 expect(res1.body.data).to.have.lengthOf(11) 224 expect(res1.body.data).to.have.lengthOf(11)
223 225
224 await updateVideo(servers[0].url, servers[0].accessToken, missedVideo1.uuid, { }) 226 await updateVideo(servers[0].url, servers[0].accessToken, missedVideo1.uuid, {})
225 await updateVideo(servers[0].url, servers[0].accessToken, unlistedVideo.uuid, { }) 227 await updateVideo(servers[0].url, servers[0].accessToken, unlistedVideo.uuid, {})
226 228
227 await waitJobs(servers) 229 await waitJobs(servers)
228 230
@@ -313,14 +315,14 @@ describe('Test handle downs', function () {
313 this.timeout(120000) 315 this.timeout(120000)
314 316
315 for (let i = 0; i < 10; i++) { 317 for (let i = 0; i < 10; i++) {
316 const uuid = (await uploadVideoAndGetId({ server: servers[ 0 ], videoName: 'video ' + i })).uuid 318 const uuid = (await uploadVideoAndGetId({ server: servers[0], videoName: 'video ' + i })).uuid
317 videoIdsServer1.push(uuid) 319 videoIdsServer1.push(uuid)
318 } 320 }
319 321
320 await waitJobs(servers) 322 await waitJobs(servers)
321 323
322 for (const id of videoIdsServer1) { 324 for (const id of videoIdsServer1) {
323 await getVideo(servers[ 1 ].url, id) 325 await getVideo(servers[1].url, id)
324 } 326 }
325 327
326 await waitJobs(servers) 328 await waitJobs(servers)
diff --git a/server/tests/api/server/jobs.ts b/server/tests/api/server/jobs.ts
index 58d8c8c10..19c8836b5 100644
--- a/server/tests/api/server/jobs.ts
+++ b/server/tests/api/server/jobs.ts
@@ -1,8 +1,8 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { cleanupTests, killallServers, ServerInfo, setAccessTokensToServers } from '../../../../shared/extra-utils/index' 5import { cleanupTests, ServerInfo, setAccessTokensToServers } from '../../../../shared/extra-utils/index'
6import { doubleFollow } from '../../../../shared/extra-utils/server/follows' 6import { doubleFollow } from '../../../../shared/extra-utils/server/follows'
7import { getJobsList, getJobsListPaginationAndSort, waitJobs } from '../../../../shared/extra-utils/server/jobs' 7import { getJobsList, getJobsListPaginationAndSort, waitJobs } from '../../../../shared/extra-utils/server/jobs'
8import { flushAndRunMultipleServers } from '../../../../shared/extra-utils/server/servers' 8import { flushAndRunMultipleServers } from '../../../../shared/extra-utils/server/servers'
@@ -44,8 +44,8 @@ describe('Test jobs', function () {
44 it('Should list jobs with sort, pagination and job type', async function () { 44 it('Should list jobs with sort, pagination and job type', async function () {
45 { 45 {
46 const res = await getJobsListPaginationAndSort({ 46 const res = await getJobsListPaginationAndSort({
47 url: servers[ 1 ].url, 47 url: servers[1].url,
48 accessToken: servers[ 1 ].accessToken, 48 accessToken: servers[1].accessToken,
49 state: 'completed', 49 state: 'completed',
50 start: 1, 50 start: 1,
51 count: 2, 51 count: 2,
@@ -54,9 +54,9 @@ describe('Test jobs', function () {
54 expect(res.body.total).to.be.above(2) 54 expect(res.body.total).to.be.above(2)
55 expect(res.body.data).to.have.lengthOf(2) 55 expect(res.body.data).to.have.lengthOf(2)
56 56
57 let job: Job = res.body.data[ 0 ] 57 let job: Job = res.body.data[0]
58 // Skip repeat jobs 58 // Skip repeat jobs
59 if (job.type === 'videos-views') job = res.body.data[ 1 ] 59 if (job.type === 'videos-views') job = res.body.data[1]
60 60
61 expect(job.state).to.equal('completed') 61 expect(job.state).to.equal('completed')
62 expect(job.type.startsWith('activitypub-')).to.be.true 62 expect(job.type.startsWith('activitypub-')).to.be.true
@@ -67,8 +67,8 @@ describe('Test jobs', function () {
67 67
68 { 68 {
69 const res = await getJobsListPaginationAndSort({ 69 const res = await getJobsListPaginationAndSort({
70 url: servers[ 1 ].url, 70 url: servers[1].url,
71 accessToken: servers[ 1 ].accessToken, 71 accessToken: servers[1].accessToken,
72 state: 'completed', 72 state: 'completed',
73 start: 0, 73 start: 0,
74 count: 100, 74 count: 100,
diff --git a/server/tests/api/server/logs.ts b/server/tests/api/server/logs.ts
index d3c877408..b8714c7a1 100644
--- a/server/tests/api/server/logs.ts
+++ b/server/tests/api/server/logs.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
diff --git a/server/tests/api/server/no-client.ts b/server/tests/api/server/no-client.ts
index 86edeb289..d0450aba0 100644
--- a/server/tests/api/server/no-client.ts
+++ b/server/tests/api/server/no-client.ts
@@ -9,7 +9,7 @@ describe('Start and stop server without web client routes', function () {
9 before(async function () { 9 before(async function () {
10 this.timeout(30000) 10 this.timeout(30000)
11 11
12 server = await flushAndRunServer(1, {}, ['--no-client']) 12 server = await flushAndRunServer(1, {}, [ '--no-client' ])
13 }) 13 })
14 14
15 it('Should fail getting the client', function () { 15 it('Should fail getting the client', function () {
diff --git a/server/tests/api/server/plugins.ts b/server/tests/api/server/plugins.ts
index b8a8a2fee..9885be4e8 100644
--- a/server/tests/api/server/plugins.ts
+++ b/server/tests/api/server/plugins.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
@@ -6,19 +6,29 @@ import {
6 cleanupTests, 6 cleanupTests,
7 closeAllSequelize, 7 closeAllSequelize,
8 flushAndRunServer, 8 flushAndRunServer,
9 getConfig, getMyUserInformation, getPluginPackageJSON, 9 getConfig,
10 getMyUserInformation,
10 getPlugin, 11 getPlugin,
12 getPluginPackageJSON,
11 getPluginRegisteredSettings, 13 getPluginRegisteredSettings,
12 getPluginsCSS, 14 getPluginsCSS,
13 installPlugin, killallServers, 15 getPublicSettings,
16 installPlugin,
17 killallServers,
14 listAvailablePlugins, 18 listAvailablePlugins,
15 listPlugins, reRunServer, 19 listPlugins,
20 reRunServer,
16 ServerInfo, 21 ServerInfo,
17 setAccessTokensToServers, 22 setAccessTokensToServers,
18 setPluginVersion, uninstallPlugin, 23 setPluginVersion,
19 updateCustomSubConfig, updateMyUser, updatePluginPackageJSON, updatePlugin, 24 uninstallPlugin,
25 updateCustomSubConfig,
26 updateMyUser,
27 updatePlugin,
28 updatePluginPackageJSON,
20 updatePluginSettings, 29 updatePluginSettings,
21 wait, getPublicSettings 30 wait,
31 waitUntilLog
22} from '../../../../shared/extra-utils' 32} from '../../../../shared/extra-utils'
23import { PluginType } from '../../../../shared/models/plugins/plugin.type' 33import { PluginType } from '../../../../shared/models/plugins/plugin.type'
24import { PeerTubePluginIndex } from '../../../../shared/models/plugins/peertube-plugin-index.model' 34import { PeerTubePluginIndex } from '../../../../shared/models/plugins/peertube-plugin-index.model'
@@ -88,7 +98,7 @@ describe('Test plugins', function () {
88 expect(res2.body.total).to.be.at.least(2) 98 expect(res2.body.total).to.be.at.least(2)
89 expect(data2).to.have.lengthOf(2) 99 expect(data2).to.have.lengthOf(2)
90 100
91 expect(data1[0].npmName).to.not.equal(data2[ 0 ].npmName) 101 expect(data1[0].npmName).to.not.equal(data2[0].npmName)
92 } 102 }
93 103
94 { 104 {
@@ -133,7 +143,7 @@ describe('Test plugins', function () {
133 it('Should have the correct global css', async function () { 143 it('Should have the correct global css', async function () {
134 const res = await getPluginsCSS(server.url) 144 const res = await getPluginsCSS(server.url)
135 145
136 expect(res.text).to.contain('--mainBackgroundColor') 146 expect(res.text).to.contain('background-color: red')
137 }) 147 })
138 148
139 it('Should have the plugin loaded in the configuration', async function () { 149 it('Should have the plugin loaded in the configuration', async function () {
@@ -249,6 +259,12 @@ describe('Test plugins', function () {
249 }) 259 })
250 }) 260 })
251 261
262 it('Should have watched settings changes', async function () {
263 this.timeout(10000)
264
265 await waitUntilLog(server, 'Settings changed!')
266 })
267
252 it('Should get a plugin and a theme', async function () { 268 it('Should get a plugin and a theme', async function () {
253 { 269 {
254 const res = await getPlugin({ 270 const res = await getPlugin({
diff --git a/server/tests/api/server/reverse-proxy.ts b/server/tests/api/server/reverse-proxy.ts
index b6b33a884..d0d79c4f6 100644
--- a/server/tests/api/server/reverse-proxy.ts
+++ b/server/tests/api/server/reverse-proxy.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
diff --git a/server/tests/api/server/stats.ts b/server/tests/api/server/stats.ts
index a01cd4b38..637525ff8 100644
--- a/server/tests/api/server/stats.ts
+++ b/server/tests/api/server/stats.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
@@ -9,13 +9,13 @@ import {
9 doubleFollow, 9 doubleFollow,
10 flushAndRunMultipleServers, 10 flushAndRunMultipleServers,
11 follow, 11 follow,
12 killallServers, 12 ServerInfo, unfollow,
13 ServerInfo,
14 uploadVideo, 13 uploadVideo,
15 viewVideo, 14 viewVideo,
16 wait 15 wait,
16 userLogin
17} from '../../../../shared/extra-utils' 17} from '../../../../shared/extra-utils'
18import { flushTests, setAccessTokensToServers } from '../../../../shared/extra-utils/index' 18import { setAccessTokensToServers } from '../../../../shared/extra-utils/index'
19import { getStats } from '../../../../shared/extra-utils/server/stats' 19import { getStats } from '../../../../shared/extra-utils/server/stats'
20import { addVideoCommentThread } from '../../../../shared/extra-utils/videos/video-comments' 20import { addVideoCommentThread } from '../../../../shared/extra-utils/videos/video-comments'
21import { waitJobs } from '../../../../shared/extra-utils/server/jobs' 21import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
@@ -24,6 +24,10 @@ const expect = chai.expect
24 24
25describe('Test stats (excluding redundancy)', function () { 25describe('Test stats (excluding redundancy)', function () {
26 let servers: ServerInfo[] = [] 26 let servers: ServerInfo[] = []
27 const user = {
28 username: 'user1',
29 password: 'super_password'
30 }
27 31
28 before(async function () { 32 before(async function () {
29 this.timeout(60000) 33 this.timeout(60000)
@@ -32,11 +36,7 @@ describe('Test stats (excluding redundancy)', function () {
32 36
33 await doubleFollow(servers[0], servers[1]) 37 await doubleFollow(servers[0], servers[1])
34 38
35 const user = { 39 await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: user.username, password: user.password })
36 username: 'user1',
37 password: 'super_password'
38 }
39 await createUser({ url: servers[ 0 ].url, accessToken: servers[ 0 ].accessToken, username: user.username, password: user.password })
40 40
41 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' })
42 const videoUUID = resVideo.body.video.uuid 42 const videoUUID = resVideo.body.video.uuid
@@ -96,6 +96,40 @@ describe('Test stats (excluding redundancy)', function () {
96 expect(data.totalInstanceFollowers).to.equal(0) 96 expect(data.totalInstanceFollowers).to.equal(0)
97 }) 97 })
98 98
99 it('Should have the correct total videos stats after an unfollow', async function () {
100 this.timeout(15000)
101
102 await unfollow(servers[2].url, servers[2].accessToken, servers[0])
103 await waitJobs(servers)
104
105 const res = await getStats(servers[2].url)
106 const data: ServerStats = res.body
107
108 expect(data.totalVideos).to.equal(0)
109 })
110
111 it('Should have the correct active users stats', async function () {
112 const server = servers[0]
113
114 {
115 const res = await getStats(server.url)
116 const data: ServerStats = res.body
117 expect(data.totalDailyActiveUsers).to.equal(1)
118 expect(data.totalWeeklyActiveUsers).to.equal(1)
119 expect(data.totalMonthlyActiveUsers).to.equal(1)
120 }
121
122 {
123 await userLogin(server, user)
124
125 const res = await getStats(server.url)
126 const data: ServerStats = res.body
127 expect(data.totalDailyActiveUsers).to.equal(2)
128 expect(data.totalWeeklyActiveUsers).to.equal(2)
129 expect(data.totalMonthlyActiveUsers).to.equal(2)
130 }
131 })
132
99 after(async function () { 133 after(async function () {
100 await cleanupTests(servers) 134 await cleanupTests(servers)
101 }) 135 })
diff --git a/server/tests/api/server/tracker.ts b/server/tests/api/server/tracker.ts
index 9d7eec8ca..5b56a83bb 100644
--- a/server/tests/api/server/tracker.ts
+++ b/server/tests/api/server/tracker.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await,@typescript-eslint/no-floating-promises */
2 2
3import * as magnetUtil from 'magnet-uri' 3import * as magnetUtil from 'magnet-uri'
4import 'mocha' 4import 'mocha'
@@ -49,7 +49,7 @@ describe('Test tracker', function () {
49 torrent.on('error', done) 49 torrent.on('error', done)
50 torrent.on('warning', warn => { 50 torrent.on('warning', warn => {
51 const message = typeof warn === 'string' ? warn : warn.message 51 const message = typeof warn === 'string' ? warn : warn.message
52 if (message.indexOf('Unknown infoHash ') !== -1) return done() 52 if (message.includes('Unknown infoHash ')) return done()
53 }) 53 })
54 54
55 torrent.on('done', () => done(new Error('No error on infohash'))) 55 torrent.on('done', () => done(new Error('No error on infohash')))
@@ -64,7 +64,7 @@ describe('Test tracker', function () {
64 torrent.on('error', done) 64 torrent.on('error', done)
65 torrent.on('warning', warn => { 65 torrent.on('warning', warn => {
66 const message = typeof warn === 'string' ? warn : warn.message 66 const message = typeof warn === 'string' ? warn : warn.message
67 if (message.indexOf('Unknown infoHash ') !== -1) return done(new Error('Error on infohash')) 67 if (message.includes('Unknown infoHash ')) return done(new Error('Error on infohash'))
68 }) 68 })
69 69
70 torrent.on('done', done) 70 torrent.on('done', done)
@@ -73,6 +73,8 @@ describe('Test tracker', function () {
73 it('Should disable the tracker', function (done) { 73 it('Should disable the tracker', function (done) {
74 this.timeout(20000) 74 this.timeout(20000)
75 75
76 const errCb = () => done(new Error('Tracker is enabled'))
77
76 killallServers([ server ]) 78 killallServers([ server ])
77 reRunServer(server, { tracker: { enabled: false } }) 79 reRunServer(server, { tracker: { enabled: false } })
78 .then(() => { 80 .then(() => {
@@ -83,10 +85,14 @@ describe('Test tracker', function () {
83 torrent.on('error', done) 85 torrent.on('error', done)
84 torrent.on('warning', warn => { 86 torrent.on('warning', warn => {
85 const message = typeof warn === 'string' ? warn : warn.message 87 const message = typeof warn === 'string' ? warn : warn.message
86 if (message.indexOf('disabled ') !== -1) return done() 88 if (message.includes('disabled ')) {
89 torrent.off('done', errCb)
90
91 return done()
92 }
87 }) 93 })
88 94
89 torrent.on('done', () => done(new Error('Tracker is enabled'))) 95 torrent.on('done', errCb)
90 }) 96 })
91 }) 97 })
92 98
diff --git a/server/tests/api/users/blocklist.ts b/server/tests/api/users/blocklist.ts
index 05e58017a..21b9ae4f8 100644
--- a/server/tests/api/users/blocklist.ts
+++ b/server/tests/api/users/blocklist.ts
@@ -1,21 +1,20 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { AccountBlock, ServerBlock, UserNotificationType, Video } from '../../../../shared/index' 5import { AccountBlock, ServerBlock, Video } from '../../../../shared/index'
6import { 6import {
7 cleanupTests, 7 cleanupTests,
8 createUser, deleteVideoComment, 8 createUser,
9 deleteVideoComment,
9 doubleFollow, 10 doubleFollow,
10 flushAndRunMultipleServers, 11 flushAndRunMultipleServers,
11 flushTests,
12 killallServers,
13 ServerInfo, 12 ServerInfo,
14 uploadVideo, 13 uploadVideo,
15 userLogin 14 userLogin
16} from '../../../../shared/extra-utils/index' 15} from '../../../../shared/extra-utils/index'
17import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login' 16import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login'
18import { getVideosListWithToken, getVideosList } from '../../../../shared/extra-utils/videos/videos' 17import { getVideosList, getVideosListWithToken } from '../../../../shared/extra-utils/videos/videos'
19import { 18import {
20 addVideoCommentReply, 19 addVideoCommentReply,
21 addVideoCommentThread, 20 addVideoCommentThread,
@@ -79,7 +78,7 @@ async function checkCommentNotification (
79 const resComment = await addVideoCommentThread(comment.server.url, comment.token, comment.videoUUID, comment.text) 78 const resComment = await addVideoCommentThread(comment.server.url, comment.token, comment.videoUUID, comment.text)
80 const threadId = resComment.body.comment.id 79 const threadId = resComment.body.comment.id
81 80
82 await waitJobs([ mainServer, comment.server]) 81 await waitJobs([ mainServer, comment.server ])
83 82
84 const res = await getUserNotifications(mainServer.url, mainServer.accessToken, 0, 30) 83 const res = await getUserNotifications(mainServer.url, mainServer.accessToken, 0, 30)
85 const commentNotifications = res.body.data 84 const commentNotifications = res.body.data
@@ -90,7 +89,7 @@ async function checkCommentNotification (
90 89
91 await deleteVideoComment(comment.server.url, comment.token, comment.videoUUID, threadId) 90 await deleteVideoComment(comment.server.url, comment.token, comment.videoUUID, threadId)
92 91
93 await waitJobs([ mainServer, comment.server]) 92 await waitJobs([ mainServer, comment.server ])
94} 93}
95 94
96describe('Test blocklist', function () { 95describe('Test blocklist', function () {
@@ -109,7 +108,7 @@ describe('Test blocklist', function () {
109 108
110 { 109 {
111 const user = { username: 'user1', password: 'password' } 110 const user = { username: 'user1', password: 'password' }
112 await createUser({ url: servers[ 0 ].url, accessToken: servers[ 0 ].accessToken, username: user.username, password: user.password }) 111 await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: user.username, password: user.password })
113 112
114 userToken1 = await userLogin(servers[0], user) 113 userToken1 = await userLogin(servers[0], user)
115 await uploadVideo(servers[0].url, userToken1, { name: 'video user 1' }) 114 await uploadVideo(servers[0].url, userToken1, { name: 'video user 1' })
@@ -117,14 +116,14 @@ describe('Test blocklist', function () {
117 116
118 { 117 {
119 const user = { username: 'moderator', password: 'password' } 118 const user = { username: 'moderator', password: 'password' }
120 await createUser({ url: servers[ 0 ].url, accessToken: servers[ 0 ].accessToken, username: user.username, password: user.password }) 119 await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: user.username, password: user.password })
121 120
122 userModeratorToken = await userLogin(servers[0], user) 121 userModeratorToken = await userLogin(servers[0], user)
123 } 122 }
124 123
125 { 124 {
126 const user = { username: 'user2', password: 'password' } 125 const user = { username: 'user2', password: 'password' }
127 await createUser({ url: servers[ 1 ].url, accessToken: servers[ 1 ].accessToken, username: user.username, password: user.password }) 126 await createUser({ url: servers[1].url, accessToken: servers[1].accessToken, username: user.username, password: user.password })
128 127
129 userToken2 = await userLogin(servers[1], user) 128 userToken2 = await userLogin(servers[1], user)
130 await uploadVideo(servers[1].url, userToken2, { name: 'video user 2' }) 129 await uploadVideo(servers[1].url, userToken2, { name: 'video user 2' })
@@ -143,14 +142,14 @@ describe('Test blocklist', function () {
143 await doubleFollow(servers[0], servers[1]) 142 await doubleFollow(servers[0], servers[1])
144 143
145 { 144 {
146 const resComment = await addVideoCommentThread(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID1, 'comment root 1') 145 const resComment = await addVideoCommentThread(servers[0].url, servers[0].accessToken, videoUUID1, 'comment root 1')
147 const resReply = await addVideoCommentReply(servers[ 0 ].url, userToken1, videoUUID1, resComment.body.comment.id, 'comment user 1') 146 const resReply = await addVideoCommentReply(servers[0].url, userToken1, videoUUID1, resComment.body.comment.id, 'comment user 1')
148 await addVideoCommentReply(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID1, resReply.body.comment.id, 'comment root 1') 147 await addVideoCommentReply(servers[0].url, servers[0].accessToken, videoUUID1, resReply.body.comment.id, 'comment root 1')
149 } 148 }
150 149
151 { 150 {
152 const resComment = await addVideoCommentThread(servers[ 0 ].url, userToken1, videoUUID1, 'comment user 1') 151 const resComment = await addVideoCommentThread(servers[0].url, userToken1, videoUUID1, 'comment user 1')
153 await addVideoCommentReply(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID1, resComment.body.comment.id, 'comment root 1') 152 await addVideoCommentReply(servers[0].url, servers[0].accessToken, videoUUID1, resComment.body.comment.id, 'comment root 1')
154 } 153 }
155 154
156 await waitJobs(servers) 155 await waitJobs(servers)
@@ -160,19 +159,19 @@ describe('Test blocklist', function () {
160 159
161 describe('When managing account blocklist', function () { 160 describe('When managing account blocklist', function () {
162 it('Should list all videos', function () { 161 it('Should list all videos', function () {
163 return checkAllVideos(servers[ 0 ].url, servers[ 0 ].accessToken) 162 return checkAllVideos(servers[0].url, servers[0].accessToken)
164 }) 163 })
165 164
166 it('Should list the comments', function () { 165 it('Should list the comments', function () {
167 return checkAllComments(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID1) 166 return checkAllComments(servers[0].url, servers[0].accessToken, videoUUID1)
168 }) 167 })
169 168
170 it('Should block a remote account', async function () { 169 it('Should block a remote account', async function () {
171 await addAccountToAccountBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'user2@localhost:' + servers[1].port) 170 await addAccountToAccountBlocklist(servers[0].url, servers[0].accessToken, 'user2@localhost:' + servers[1].port)
172 }) 171 })
173 172
174 it('Should hide its videos', async function () { 173 it('Should hide its videos', async function () {
175 const res = await getVideosListWithToken(servers[ 0 ].url, servers[ 0 ].accessToken) 174 const res = await getVideosListWithToken(servers[0].url, servers[0].accessToken)
176 175
177 const videos: Video[] = res.body.data 176 const videos: Video[] = res.body.data
178 expect(videos).to.have.lengthOf(3) 177 expect(videos).to.have.lengthOf(3)
@@ -182,11 +181,11 @@ describe('Test blocklist', function () {
182 }) 181 })
183 182
184 it('Should block a local account', async function () { 183 it('Should block a local account', async function () {
185 await addAccountToAccountBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'user1') 184 await addAccountToAccountBlocklist(servers[0].url, servers[0].accessToken, 'user1')
186 }) 185 })
187 186
188 it('Should hide its videos', async function () { 187 it('Should hide its videos', async function () {
189 const res = await getVideosListWithToken(servers[ 0 ].url, servers[ 0 ].accessToken) 188 const res = await getVideosListWithToken(servers[0].url, servers[0].accessToken)
190 189
191 const videos: Video[] = res.body.data 190 const videos: Video[] = res.body.data
192 expect(videos).to.have.lengthOf(2) 191 expect(videos).to.have.lengthOf(2)
@@ -196,17 +195,17 @@ describe('Test blocklist', function () {
196 }) 195 })
197 196
198 it('Should hide its comments', async function () { 197 it('Should hide its comments', async function () {
199 const resThreads = await getVideoCommentThreads(servers[ 0 ].url, videoUUID1, 0, 5, '-createdAt', servers[ 0 ].accessToken) 198 const resThreads = await getVideoCommentThreads(servers[0].url, videoUUID1, 0, 5, '-createdAt', servers[0].accessToken)
200 199
201 const threads: VideoComment[] = resThreads.body.data 200 const threads: VideoComment[] = resThreads.body.data
202 expect(threads).to.have.lengthOf(1) 201 expect(threads).to.have.lengthOf(1)
203 expect(threads[ 0 ].totalReplies).to.equal(0) 202 expect(threads[0].totalReplies).to.equal(0)
204 203
205 const t = threads.find(t => t.text === 'comment user 1') 204 const t = threads.find(t => t.text === 'comment user 1')
206 expect(t).to.be.undefined 205 expect(t).to.be.undefined
207 206
208 for (const thread of threads) { 207 for (const thread of threads) {
209 const res = await getVideoThreadComments(servers[ 0 ].url, videoUUID1, thread.id, servers[ 0 ].accessToken) 208 const res = await getVideoThreadComments(servers[0].url, videoUUID1, thread.id, servers[0].accessToken)
210 209
211 const tree: VideoCommentThreadTree = res.body 210 const tree: VideoCommentThreadTree = res.body
212 expect(tree.children).to.have.lengthOf(0) 211 expect(tree.children).to.have.lengthOf(0)
@@ -217,37 +216,37 @@ describe('Test blocklist', function () {
217 this.timeout(20000) 216 this.timeout(20000)
218 217
219 { 218 {
220 const comment = { server: servers[ 0 ], token: userToken1, videoUUID: videoUUID1, text: 'hidden comment' } 219 const comment = { server: servers[0], token: userToken1, videoUUID: videoUUID1, text: 'hidden comment' }
221 await checkCommentNotification(servers[ 0 ], comment, 'absence') 220 await checkCommentNotification(servers[0], comment, 'absence')
222 } 221 }
223 222
224 { 223 {
225 const comment = { 224 const comment = {
226 server: servers[ 0 ], 225 server: servers[0],
227 token: userToken1, 226 token: userToken1,
228 videoUUID: videoUUID2, 227 videoUUID: videoUUID2,
229 text: 'hello @root@localhost:' + servers[ 0 ].port 228 text: 'hello @root@localhost:' + servers[0].port
230 } 229 }
231 await checkCommentNotification(servers[ 0 ], comment, 'absence') 230 await checkCommentNotification(servers[0], comment, 'absence')
232 } 231 }
233 }) 232 })
234 233
235 it('Should list all the videos with another user', async function () { 234 it('Should list all the videos with another user', async function () {
236 return checkAllVideos(servers[ 0 ].url, userToken1) 235 return checkAllVideos(servers[0].url, userToken1)
237 }) 236 })
238 237
239 it('Should list all the comments with another user', async function () { 238 it('Should list all the comments with another user', async function () {
240 return checkAllComments(servers[ 0 ].url, userToken1, videoUUID1) 239 return checkAllComments(servers[0].url, userToken1, videoUUID1)
241 }) 240 })
242 241
243 it('Should list blocked accounts', async function () { 242 it('Should list blocked accounts', async function () {
244 { 243 {
245 const res = await getAccountBlocklistByAccount(servers[ 0 ].url, servers[ 0 ].accessToken, 0, 1, 'createdAt') 244 const res = await getAccountBlocklistByAccount(servers[0].url, servers[0].accessToken, 0, 1, 'createdAt')
246 const blocks: AccountBlock[] = res.body.data 245 const blocks: AccountBlock[] = res.body.data
247 246
248 expect(res.body.total).to.equal(2) 247 expect(res.body.total).to.equal(2)
249 248
250 const block = blocks[ 0 ] 249 const block = blocks[0]
251 expect(block.byAccount.displayName).to.equal('root') 250 expect(block.byAccount.displayName).to.equal('root')
252 expect(block.byAccount.name).to.equal('root') 251 expect(block.byAccount.name).to.equal('root')
253 expect(block.blockedAccount.displayName).to.equal('user2') 252 expect(block.blockedAccount.displayName).to.equal('user2')
@@ -256,12 +255,12 @@ describe('Test blocklist', function () {
256 } 255 }
257 256
258 { 257 {
259 const res = await getAccountBlocklistByAccount(servers[ 0 ].url, servers[ 0 ].accessToken, 1, 2, 'createdAt') 258 const res = await getAccountBlocklistByAccount(servers[0].url, servers[0].accessToken, 1, 2, 'createdAt')
260 const blocks: AccountBlock[] = res.body.data 259 const blocks: AccountBlock[] = res.body.data
261 260
262 expect(res.body.total).to.equal(2) 261 expect(res.body.total).to.equal(2)
263 262
264 const block = blocks[ 0 ] 263 const block = blocks[0]
265 expect(block.byAccount.displayName).to.equal('root') 264 expect(block.byAccount.displayName).to.equal('root')
266 expect(block.byAccount.name).to.equal('root') 265 expect(block.byAccount.name).to.equal('root')
267 expect(block.blockedAccount.displayName).to.equal('user1') 266 expect(block.blockedAccount.displayName).to.equal('user1')
@@ -271,11 +270,11 @@ describe('Test blocklist', function () {
271 }) 270 })
272 271
273 it('Should unblock the remote account', async function () { 272 it('Should unblock the remote account', async function () {
274 await removeAccountFromAccountBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'user2@localhost:' + servers[1].port) 273 await removeAccountFromAccountBlocklist(servers[0].url, servers[0].accessToken, 'user2@localhost:' + servers[1].port)
275 }) 274 })
276 275
277 it('Should display its videos', async function () { 276 it('Should display its videos', async function () {
278 const res = await getVideosListWithToken(servers[ 0 ].url, servers[ 0 ].accessToken) 277 const res = await getVideosListWithToken(servers[0].url, servers[0].accessToken)
279 278
280 const videos: Video[] = res.body.data 279 const videos: Video[] = res.body.data
281 expect(videos).to.have.lengthOf(3) 280 expect(videos).to.have.lengthOf(3)
@@ -285,48 +284,48 @@ describe('Test blocklist', function () {
285 }) 284 })
286 285
287 it('Should unblock the local account', async function () { 286 it('Should unblock the local account', async function () {
288 await removeAccountFromAccountBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'user1') 287 await removeAccountFromAccountBlocklist(servers[0].url, servers[0].accessToken, 'user1')
289 }) 288 })
290 289
291 it('Should display its comments', function () { 290 it('Should display its comments', function () {
292 return checkAllComments(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID1) 291 return checkAllComments(servers[0].url, servers[0].accessToken, videoUUID1)
293 }) 292 })
294 293
295 it('Should have a notification from a non blocked account', async function () { 294 it('Should have a notification from a non blocked account', async function () {
296 this.timeout(20000) 295 this.timeout(20000)
297 296
298 { 297 {
299 const comment = { server: servers[ 1 ], token: userToken2, videoUUID: videoUUID1, text: 'displayed comment' } 298 const comment = { server: servers[1], token: userToken2, videoUUID: videoUUID1, text: 'displayed comment' }
300 await checkCommentNotification(servers[ 0 ], comment, 'presence') 299 await checkCommentNotification(servers[0], comment, 'presence')
301 } 300 }
302 301
303 { 302 {
304 const comment = { 303 const comment = {
305 server: servers[ 0 ], 304 server: servers[0],
306 token: userToken1, 305 token: userToken1,
307 videoUUID: videoUUID2, 306 videoUUID: videoUUID2,
308 text: 'hello @root@localhost:' + servers[ 0 ].port 307 text: 'hello @root@localhost:' + servers[0].port
309 } 308 }
310 await checkCommentNotification(servers[ 0 ], comment, 'presence') 309 await checkCommentNotification(servers[0], comment, 'presence')
311 } 310 }
312 }) 311 })
313 }) 312 })
314 313
315 describe('When managing server blocklist', function () { 314 describe('When managing server blocklist', function () {
316 it('Should list all videos', function () { 315 it('Should list all videos', function () {
317 return checkAllVideos(servers[ 0 ].url, servers[ 0 ].accessToken) 316 return checkAllVideos(servers[0].url, servers[0].accessToken)
318 }) 317 })
319 318
320 it('Should list the comments', function () { 319 it('Should list the comments', function () {
321 return checkAllComments(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID1) 320 return checkAllComments(servers[0].url, servers[0].accessToken, videoUUID1)
322 }) 321 })
323 322
324 it('Should block a remote server', async function () { 323 it('Should block a remote server', async function () {
325 await addServerToAccountBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'localhost:' + servers[1].port) 324 await addServerToAccountBlocklist(servers[0].url, servers[0].accessToken, 'localhost:' + servers[1].port)
326 }) 325 })
327 326
328 it('Should hide its videos', async function () { 327 it('Should hide its videos', async function () {
329 const res = await getVideosListWithToken(servers[ 0 ].url, servers[ 0 ].accessToken) 328 const res = await getVideosListWithToken(servers[0].url, servers[0].accessToken)
330 329
331 const videos: Video[] = res.body.data 330 const videos: Video[] = res.body.data
332 expect(videos).to.have.lengthOf(2) 331 expect(videos).to.have.lengthOf(2)
@@ -339,81 +338,81 @@ describe('Test blocklist', function () {
339 }) 338 })
340 339
341 it('Should list all the videos with another user', async function () { 340 it('Should list all the videos with another user', async function () {
342 return checkAllVideos(servers[ 0 ].url, userToken1) 341 return checkAllVideos(servers[0].url, userToken1)
343 }) 342 })
344 343
345 it('Should hide its comments', async function () { 344 it('Should hide its comments', async function () {
346 this.timeout(10000) 345 this.timeout(10000)
347 346
348 const resThreads = await addVideoCommentThread(servers[ 1 ].url, userToken2, videoUUID1, 'hidden comment 2') 347 const resThreads = await addVideoCommentThread(servers[1].url, userToken2, videoUUID1, 'hidden comment 2')
349 const threadId = resThreads.body.comment.id 348 const threadId = resThreads.body.comment.id
350 349
351 await waitJobs(servers) 350 await waitJobs(servers)
352 351
353 await checkAllComments(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID1) 352 await checkAllComments(servers[0].url, servers[0].accessToken, videoUUID1)
354 353
355 await deleteVideoComment(servers[ 1 ].url, userToken2, videoUUID1, threadId) 354 await deleteVideoComment(servers[1].url, userToken2, videoUUID1, threadId)
356 }) 355 })
357 356
358 it('Should not have notifications from blocked server', async function () { 357 it('Should not have notifications from blocked server', async function () {
359 this.timeout(20000) 358 this.timeout(20000)
360 359
361 { 360 {
362 const comment = { server: servers[ 1 ], token: userToken2, videoUUID: videoUUID1, text: 'hidden comment' } 361 const comment = { server: servers[1], token: userToken2, videoUUID: videoUUID1, text: 'hidden comment' }
363 await checkCommentNotification(servers[ 0 ], comment, 'absence') 362 await checkCommentNotification(servers[0], comment, 'absence')
364 } 363 }
365 364
366 { 365 {
367 const comment = { 366 const comment = {
368 server: servers[ 1 ], 367 server: servers[1],
369 token: userToken2, 368 token: userToken2,
370 videoUUID: videoUUID1, 369 videoUUID: videoUUID1,
371 text: 'hello @root@localhost:' + servers[ 0 ].port 370 text: 'hello @root@localhost:' + servers[0].port
372 } 371 }
373 await checkCommentNotification(servers[ 0 ], comment, 'absence') 372 await checkCommentNotification(servers[0], comment, 'absence')
374 } 373 }
375 }) 374 })
376 375
377 it('Should list blocked servers', async function () { 376 it('Should list blocked servers', async function () {
378 const res = await getServerBlocklistByAccount(servers[ 0 ].url, servers[ 0 ].accessToken, 0, 1, 'createdAt') 377 const res = await getServerBlocklistByAccount(servers[0].url, servers[0].accessToken, 0, 1, 'createdAt')
379 const blocks: ServerBlock[] = res.body.data 378 const blocks: ServerBlock[] = res.body.data
380 379
381 expect(res.body.total).to.equal(1) 380 expect(res.body.total).to.equal(1)
382 381
383 const block = blocks[ 0 ] 382 const block = blocks[0]
384 expect(block.byAccount.displayName).to.equal('root') 383 expect(block.byAccount.displayName).to.equal('root')
385 expect(block.byAccount.name).to.equal('root') 384 expect(block.byAccount.name).to.equal('root')
386 expect(block.blockedServer.host).to.equal('localhost:' + servers[1].port) 385 expect(block.blockedServer.host).to.equal('localhost:' + servers[1].port)
387 }) 386 })
388 387
389 it('Should unblock the remote server', async function () { 388 it('Should unblock the remote server', async function () {
390 await removeServerFromAccountBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'localhost:' + servers[1].port) 389 await removeServerFromAccountBlocklist(servers[0].url, servers[0].accessToken, 'localhost:' + servers[1].port)
391 }) 390 })
392 391
393 it('Should display its videos', function () { 392 it('Should display its videos', function () {
394 return checkAllVideos(servers[ 0 ].url, servers[ 0 ].accessToken) 393 return checkAllVideos(servers[0].url, servers[0].accessToken)
395 }) 394 })
396 395
397 it('Should display its comments', function () { 396 it('Should display its comments', function () {
398 return checkAllComments(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID1) 397 return checkAllComments(servers[0].url, servers[0].accessToken, videoUUID1)
399 }) 398 })
400 399
401 it('Should have notification from unblocked server', async function () { 400 it('Should have notification from unblocked server', async function () {
402 this.timeout(20000) 401 this.timeout(20000)
403 402
404 { 403 {
405 const comment = { server: servers[ 1 ], token: userToken2, videoUUID: videoUUID1, text: 'displayed comment' } 404 const comment = { server: servers[1], token: userToken2, videoUUID: videoUUID1, text: 'displayed comment' }
406 await checkCommentNotification(servers[ 0 ], comment, 'presence') 405 await checkCommentNotification(servers[0], comment, 'presence')
407 } 406 }
408 407
409 { 408 {
410 const comment = { 409 const comment = {
411 server: servers[ 1 ], 410 server: servers[1],
412 token: userToken2, 411 token: userToken2,
413 videoUUID: videoUUID1, 412 videoUUID: videoUUID1,
414 text: 'hello @root@localhost:' + servers[ 0 ].port 413 text: 'hello @root@localhost:' + servers[0].port
415 } 414 }
416 await checkCommentNotification(servers[ 0 ], comment, 'presence') 415 await checkCommentNotification(servers[0], comment, 'presence')
417 } 416 }
418 }) 417 })
419 }) 418 })
@@ -423,24 +422,24 @@ describe('Test blocklist', function () {
423 422
424 describe('When managing account blocklist', function () { 423 describe('When managing account blocklist', function () {
425 it('Should list all videos', async function () { 424 it('Should list all videos', async function () {
426 for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) { 425 for (const token of [ userModeratorToken, servers[0].accessToken ]) {
427 await checkAllVideos(servers[ 0 ].url, token) 426 await checkAllVideos(servers[0].url, token)
428 } 427 }
429 }) 428 })
430 429
431 it('Should list the comments', async function () { 430 it('Should list the comments', async function () {
432 for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) { 431 for (const token of [ userModeratorToken, servers[0].accessToken ]) {
433 await checkAllComments(servers[ 0 ].url, token, videoUUID1) 432 await checkAllComments(servers[0].url, token, videoUUID1)
434 } 433 }
435 }) 434 })
436 435
437 it('Should block a remote account', async function () { 436 it('Should block a remote account', async function () {
438 await addAccountToServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'user2@localhost:' + servers[1].port) 437 await addAccountToServerBlocklist(servers[0].url, servers[0].accessToken, 'user2@localhost:' + servers[1].port)
439 }) 438 })
440 439
441 it('Should hide its videos', async function () { 440 it('Should hide its videos', async function () {
442 for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) { 441 for (const token of [ userModeratorToken, servers[0].accessToken ]) {
443 const res = await getVideosListWithToken(servers[ 0 ].url, token) 442 const res = await getVideosListWithToken(servers[0].url, token)
444 443
445 const videos: Video[] = res.body.data 444 const videos: Video[] = res.body.data
446 expect(videos).to.have.lengthOf(3) 445 expect(videos).to.have.lengthOf(3)
@@ -451,12 +450,12 @@ describe('Test blocklist', function () {
451 }) 450 })
452 451
453 it('Should block a local account', async function () { 452 it('Should block a local account', async function () {
454 await addAccountToServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'user1') 453 await addAccountToServerBlocklist(servers[0].url, servers[0].accessToken, 'user1')
455 }) 454 })
456 455
457 it('Should hide its videos', async function () { 456 it('Should hide its videos', async function () {
458 for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) { 457 for (const token of [ userModeratorToken, servers[0].accessToken ]) {
459 const res = await getVideosListWithToken(servers[ 0 ].url, token) 458 const res = await getVideosListWithToken(servers[0].url, token)
460 459
461 const videos: Video[] = res.body.data 460 const videos: Video[] = res.body.data
462 expect(videos).to.have.lengthOf(2) 461 expect(videos).to.have.lengthOf(2)
@@ -467,18 +466,18 @@ describe('Test blocklist', function () {
467 }) 466 })
468 467
469 it('Should hide its comments', async function () { 468 it('Should hide its comments', async function () {
470 for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) { 469 for (const token of [ userModeratorToken, servers[0].accessToken ]) {
471 const resThreads = await getVideoCommentThreads(servers[ 0 ].url, videoUUID1, 0, 5, '-createdAt', token) 470 const resThreads = await getVideoCommentThreads(servers[0].url, videoUUID1, 0, 5, '-createdAt', token)
472 471
473 const threads: VideoComment[] = resThreads.body.data 472 const threads: VideoComment[] = resThreads.body.data
474 expect(threads).to.have.lengthOf(1) 473 expect(threads).to.have.lengthOf(1)
475 expect(threads[ 0 ].totalReplies).to.equal(0) 474 expect(threads[0].totalReplies).to.equal(0)
476 475
477 const t = threads.find(t => t.text === 'comment user 1') 476 const t = threads.find(t => t.text === 'comment user 1')
478 expect(t).to.be.undefined 477 expect(t).to.be.undefined
479 478
480 for (const thread of threads) { 479 for (const thread of threads) {
481 const res = await getVideoThreadComments(servers[ 0 ].url, videoUUID1, thread.id, token) 480 const res = await getVideoThreadComments(servers[0].url, videoUUID1, thread.id, token)
482 481
483 const tree: VideoCommentThreadTree = res.body 482 const tree: VideoCommentThreadTree = res.body
484 expect(tree.children).to.have.lengthOf(0) 483 expect(tree.children).to.have.lengthOf(0)
@@ -490,29 +489,29 @@ describe('Test blocklist', function () {
490 this.timeout(20000) 489 this.timeout(20000)
491 490
492 { 491 {
493 const comment = { server: servers[ 0 ], token: userToken1, videoUUID: videoUUID1, text: 'hidden comment' } 492 const comment = { server: servers[0], token: userToken1, videoUUID: videoUUID1, text: 'hidden comment' }
494 await checkCommentNotification(servers[ 0 ], comment, 'absence') 493 await checkCommentNotification(servers[0], comment, 'absence')
495 } 494 }
496 495
497 { 496 {
498 const comment = { 497 const comment = {
499 server: servers[ 1 ], 498 server: servers[1],
500 token: userToken2, 499 token: userToken2,
501 videoUUID: videoUUID1, 500 videoUUID: videoUUID1,
502 text: 'hello @root@localhost:' + servers[ 0 ].port 501 text: 'hello @root@localhost:' + servers[0].port
503 } 502 }
504 await checkCommentNotification(servers[ 0 ], comment, 'absence') 503 await checkCommentNotification(servers[0], comment, 'absence')
505 } 504 }
506 }) 505 })
507 506
508 it('Should list blocked accounts', async function () { 507 it('Should list blocked accounts', async function () {
509 { 508 {
510 const res = await getAccountBlocklistByServer(servers[ 0 ].url, servers[ 0 ].accessToken, 0, 1, 'createdAt') 509 const res = await getAccountBlocklistByServer(servers[0].url, servers[0].accessToken, 0, 1, 'createdAt')
511 const blocks: AccountBlock[] = res.body.data 510 const blocks: AccountBlock[] = res.body.data
512 511
513 expect(res.body.total).to.equal(2) 512 expect(res.body.total).to.equal(2)
514 513
515 const block = blocks[ 0 ] 514 const block = blocks[0]
516 expect(block.byAccount.displayName).to.equal('peertube') 515 expect(block.byAccount.displayName).to.equal('peertube')
517 expect(block.byAccount.name).to.equal('peertube') 516 expect(block.byAccount.name).to.equal('peertube')
518 expect(block.blockedAccount.displayName).to.equal('user2') 517 expect(block.blockedAccount.displayName).to.equal('user2')
@@ -521,12 +520,12 @@ describe('Test blocklist', function () {
521 } 520 }
522 521
523 { 522 {
524 const res = await getAccountBlocklistByServer(servers[ 0 ].url, servers[ 0 ].accessToken, 1, 2, 'createdAt') 523 const res = await getAccountBlocklistByServer(servers[0].url, servers[0].accessToken, 1, 2, 'createdAt')
525 const blocks: AccountBlock[] = res.body.data 524 const blocks: AccountBlock[] = res.body.data
526 525
527 expect(res.body.total).to.equal(2) 526 expect(res.body.total).to.equal(2)
528 527
529 const block = blocks[ 0 ] 528 const block = blocks[0]
530 expect(block.byAccount.displayName).to.equal('peertube') 529 expect(block.byAccount.displayName).to.equal('peertube')
531 expect(block.byAccount.name).to.equal('peertube') 530 expect(block.byAccount.name).to.equal('peertube')
532 expect(block.blockedAccount.displayName).to.equal('user1') 531 expect(block.blockedAccount.displayName).to.equal('user1')
@@ -536,12 +535,12 @@ describe('Test blocklist', function () {
536 }) 535 })
537 536
538 it('Should unblock the remote account', async function () { 537 it('Should unblock the remote account', async function () {
539 await removeAccountFromServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'user2@localhost:' + servers[1].port) 538 await removeAccountFromServerBlocklist(servers[0].url, servers[0].accessToken, 'user2@localhost:' + servers[1].port)
540 }) 539 })
541 540
542 it('Should display its videos', async function () { 541 it('Should display its videos', async function () {
543 for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) { 542 for (const token of [ userModeratorToken, servers[0].accessToken ]) {
544 const res = await getVideosListWithToken(servers[ 0 ].url, token) 543 const res = await getVideosListWithToken(servers[0].url, token)
545 544
546 const videos: Video[] = res.body.data 545 const videos: Video[] = res.body.data
547 expect(videos).to.have.lengthOf(3) 546 expect(videos).to.have.lengthOf(3)
@@ -552,12 +551,12 @@ describe('Test blocklist', function () {
552 }) 551 })
553 552
554 it('Should unblock the local account', async function () { 553 it('Should unblock the local account', async function () {
555 await removeAccountFromServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'user1') 554 await removeAccountFromServerBlocklist(servers[0].url, servers[0].accessToken, 'user1')
556 }) 555 })
557 556
558 it('Should display its comments', async function () { 557 it('Should display its comments', async function () {
559 for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) { 558 for (const token of [ userModeratorToken, servers[0].accessToken ]) {
560 await checkAllComments(servers[ 0 ].url, token, videoUUID1) 559 await checkAllComments(servers[0].url, token, videoUUID1)
561 } 560 }
562 }) 561 })
563 562
@@ -565,43 +564,43 @@ describe('Test blocklist', function () {
565 this.timeout(20000) 564 this.timeout(20000)
566 565
567 { 566 {
568 const comment = { server: servers[ 0 ], token: userToken1, videoUUID: videoUUID1, text: 'displayed comment' } 567 const comment = { server: servers[0], token: userToken1, videoUUID: videoUUID1, text: 'displayed comment' }
569 await checkCommentNotification(servers[ 0 ], comment, 'presence') 568 await checkCommentNotification(servers[0], comment, 'presence')
570 } 569 }
571 570
572 { 571 {
573 const comment = { 572 const comment = {
574 server: servers[ 1 ], 573 server: servers[1],
575 token: userToken2, 574 token: userToken2,
576 videoUUID: videoUUID1, 575 videoUUID: videoUUID1,
577 text: 'hello @root@localhost:' + servers[ 0 ].port 576 text: 'hello @root@localhost:' + servers[0].port
578 } 577 }
579 await checkCommentNotification(servers[ 0 ], comment, 'presence') 578 await checkCommentNotification(servers[0], comment, 'presence')
580 } 579 }
581 }) 580 })
582 }) 581 })
583 582
584 describe('When managing server blocklist', function () { 583 describe('When managing server blocklist', function () {
585 it('Should list all videos', async function () { 584 it('Should list all videos', async function () {
586 for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) { 585 for (const token of [ userModeratorToken, servers[0].accessToken ]) {
587 await checkAllVideos(servers[ 0 ].url, token) 586 await checkAllVideos(servers[0].url, token)
588 } 587 }
589 }) 588 })
590 589
591 it('Should list the comments', async function () { 590 it('Should list the comments', async function () {
592 for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) { 591 for (const token of [ userModeratorToken, servers[0].accessToken ]) {
593 await checkAllComments(servers[ 0 ].url, token, videoUUID1) 592 await checkAllComments(servers[0].url, token, videoUUID1)
594 } 593 }
595 }) 594 })
596 595
597 it('Should block a remote server', async function () { 596 it('Should block a remote server', async function () {
598 await addServerToServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'localhost:' + servers[1].port) 597 await addServerToServerBlocklist(servers[0].url, servers[0].accessToken, 'localhost:' + servers[1].port)
599 }) 598 })
600 599
601 it('Should hide its videos', async function () { 600 it('Should hide its videos', async function () {
602 for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) { 601 for (const token of [ userModeratorToken, servers[0].accessToken ]) {
603 const res1 = await getVideosList(servers[ 0 ].url) 602 const res1 = await getVideosList(servers[0].url)
604 const res2 = await getVideosListWithToken(servers[ 0 ].url, token) 603 const res2 = await getVideosListWithToken(servers[0].url, token)
605 604
606 for (const res of [ res1, res2 ]) { 605 for (const res of [ res1, res2 ]) {
607 const videos: Video[] = res.body.data 606 const videos: Video[] = res.body.data
@@ -619,60 +618,60 @@ describe('Test blocklist', function () {
619 it('Should hide its comments', async function () { 618 it('Should hide its comments', async function () {
620 this.timeout(10000) 619 this.timeout(10000)
621 620
622 const resThreads = await addVideoCommentThread(servers[ 1 ].url, userToken2, videoUUID1, 'hidden comment 2') 621 const resThreads = await addVideoCommentThread(servers[1].url, userToken2, videoUUID1, 'hidden comment 2')
623 const threadId = resThreads.body.comment.id 622 const threadId = resThreads.body.comment.id
624 623
625 await waitJobs(servers) 624 await waitJobs(servers)
626 625
627 await checkAllComments(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID1) 626 await checkAllComments(servers[0].url, servers[0].accessToken, videoUUID1)
628 627
629 await deleteVideoComment(servers[ 1 ].url, userToken2, videoUUID1, threadId) 628 await deleteVideoComment(servers[1].url, userToken2, videoUUID1, threadId)
630 }) 629 })
631 630
632 it('Should not have notification from blocked instances by instance', async function () { 631 it('Should not have notification from blocked instances by instance', async function () {
633 this.timeout(20000) 632 this.timeout(20000)
634 633
635 { 634 {
636 const comment = { server: servers[ 1 ], token: userToken2, videoUUID: videoUUID1, text: 'hidden comment' } 635 const comment = { server: servers[1], token: userToken2, videoUUID: videoUUID1, text: 'hidden comment' }
637 await checkCommentNotification(servers[ 0 ], comment, 'absence') 636 await checkCommentNotification(servers[0], comment, 'absence')
638 } 637 }
639 638
640 { 639 {
641 const comment = { 640 const comment = {
642 server: servers[ 1 ], 641 server: servers[1],
643 token: userToken2, 642 token: userToken2,
644 videoUUID: videoUUID1, 643 videoUUID: videoUUID1,
645 text: 'hello @root@localhost:' + servers[ 0 ].port 644 text: 'hello @root@localhost:' + servers[0].port
646 } 645 }
647 await checkCommentNotification(servers[ 0 ], comment, 'absence') 646 await checkCommentNotification(servers[0], comment, 'absence')
648 } 647 }
649 }) 648 })
650 649
651 it('Should list blocked servers', async function () { 650 it('Should list blocked servers', async function () {
652 const res = await getServerBlocklistByServer(servers[ 0 ].url, servers[ 0 ].accessToken, 0, 1, 'createdAt') 651 const res = await getServerBlocklistByServer(servers[0].url, servers[0].accessToken, 0, 1, 'createdAt')
653 const blocks: ServerBlock[] = res.body.data 652 const blocks: ServerBlock[] = res.body.data
654 653
655 expect(res.body.total).to.equal(1) 654 expect(res.body.total).to.equal(1)
656 655
657 const block = blocks[ 0 ] 656 const block = blocks[0]
658 expect(block.byAccount.displayName).to.equal('peertube') 657 expect(block.byAccount.displayName).to.equal('peertube')
659 expect(block.byAccount.name).to.equal('peertube') 658 expect(block.byAccount.name).to.equal('peertube')
660 expect(block.blockedServer.host).to.equal('localhost:' + servers[1].port) 659 expect(block.blockedServer.host).to.equal('localhost:' + servers[1].port)
661 }) 660 })
662 661
663 it('Should unblock the remote server', async function () { 662 it('Should unblock the remote server', async function () {
664 await removeServerFromServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'localhost:' + servers[1].port) 663 await removeServerFromServerBlocklist(servers[0].url, servers[0].accessToken, 'localhost:' + servers[1].port)
665 }) 664 })
666 665
667 it('Should list all videos', async function () { 666 it('Should list all videos', async function () {
668 for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) { 667 for (const token of [ userModeratorToken, servers[0].accessToken ]) {
669 await checkAllVideos(servers[ 0 ].url, token) 668 await checkAllVideos(servers[0].url, token)
670 } 669 }
671 }) 670 })
672 671
673 it('Should list the comments', async function () { 672 it('Should list the comments', async function () {
674 for (const token of [ userModeratorToken, servers[ 0 ].accessToken ]) { 673 for (const token of [ userModeratorToken, servers[0].accessToken ]) {
675 await checkAllComments(servers[ 0 ].url, token, videoUUID1) 674 await checkAllComments(servers[0].url, token, videoUUID1)
676 } 675 }
677 }) 676 })
678 677
@@ -680,18 +679,18 @@ describe('Test blocklist', function () {
680 this.timeout(20000) 679 this.timeout(20000)
681 680
682 { 681 {
683 const comment = { server: servers[ 1 ], token: userToken2, videoUUID: videoUUID1, text: 'displayed comment' } 682 const comment = { server: servers[1], token: userToken2, videoUUID: videoUUID1, text: 'displayed comment' }
684 await checkCommentNotification(servers[ 0 ], comment, 'presence') 683 await checkCommentNotification(servers[0], comment, 'presence')
685 } 684 }
686 685
687 { 686 {
688 const comment = { 687 const comment = {
689 server: servers[ 1 ], 688 server: servers[1],
690 token: userToken2, 689 token: userToken2,
691 videoUUID: videoUUID1, 690 videoUUID: videoUUID1,
692 text: 'hello @root@localhost:' + servers[ 0 ].port 691 text: 'hello @root@localhost:' + servers[0].port
693 } 692 }
694 await checkCommentNotification(servers[ 0 ], comment, 'presence') 693 await checkCommentNotification(servers[0], comment, 'presence')
695 } 694 }
696 }) 695 })
697 }) 696 })
diff --git a/server/tests/api/users/user-subscriptions.ts b/server/tests/api/users/user-subscriptions.ts
index 08017f89c..7d6b0c6a9 100644
--- a/server/tests/api/users/user-subscriptions.ts
+++ b/server/tests/api/users/user-subscriptions.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
@@ -13,16 +13,17 @@ import {
13 updateVideo, 13 updateVideo,
14 userLogin 14 userLogin
15} from '../../../../shared/extra-utils' 15} from '../../../../shared/extra-utils'
16import { killallServers, ServerInfo, uploadVideo } from '../../../../shared/extra-utils/index' 16import { ServerInfo, uploadVideo } from '../../../../shared/extra-utils/index'
17import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login' 17import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login'
18import { Video, VideoChannel } from '../../../../shared/models/videos' 18import { Video, VideoChannel } from '../../../../shared/models/videos'
19import { waitJobs } from '../../../../shared/extra-utils/server/jobs' 19import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
20import { 20import {
21 addUserSubscription, 21 addUserSubscription,
22 areSubscriptionsExist,
23 getUserSubscription,
22 listUserSubscriptions, 24 listUserSubscriptions,
23 listUserSubscriptionVideos, 25 listUserSubscriptionVideos,
24 removeUserSubscription, 26 removeUserSubscription
25 getUserSubscription, areSubscriptionsExist
26} from '../../../../shared/extra-utils/users/user-subscriptions' 27} from '../../../../shared/extra-utils/users/user-subscriptions'
27 28
28const expect = chai.expect 29const expect = chai.expect
@@ -116,7 +117,7 @@ describe('Test users subscriptions', function () {
116 117
117 it('Should get subscription', async function () { 118 it('Should get subscription', async function () {
118 { 119 {
119 const res = await getUserSubscription(servers[ 0 ].url, users[ 0 ].accessToken, 'user3_channel@localhost:' + servers[2].port) 120 const res = await getUserSubscription(servers[0].url, users[0].accessToken, 'user3_channel@localhost:' + servers[2].port)
120 const videoChannel: VideoChannel = res.body 121 const videoChannel: VideoChannel = res.body
121 122
122 expect(videoChannel.name).to.equal('user3_channel') 123 expect(videoChannel.name).to.equal('user3_channel')
@@ -127,7 +128,7 @@ describe('Test users subscriptions', function () {
127 } 128 }
128 129
129 { 130 {
130 const res = await getUserSubscription(servers[ 0 ].url, users[ 0 ].accessToken, 'root_channel@localhost:' + servers[0].port) 131 const res = await getUserSubscription(servers[0].url, users[0].accessToken, 'root_channel@localhost:' + servers[0].port)
131 const videoChannel: VideoChannel = res.body 132 const videoChannel: VideoChannel = res.body
132 133
133 expect(videoChannel.name).to.equal('root_channel') 134 expect(videoChannel.name).to.equal('root_channel')
@@ -146,7 +147,7 @@ describe('Test users subscriptions', function () {
146 'user3_channel@localhost:' + servers[0].port 147 'user3_channel@localhost:' + servers[0].port
147 ] 148 ]
148 149
149 const res = await areSubscriptionsExist(servers[ 0 ].url, users[ 0 ].accessToken, uris) 150 const res = await areSubscriptionsExist(servers[0].url, users[0].accessToken, uris)
150 const body = res.body 151 const body = res.body
151 152
152 expect(body['user3_channel@localhost:' + servers[2].port]).to.be.true 153 expect(body['user3_channel@localhost:' + servers[2].port]).to.be.true
diff --git a/server/tests/api/users/users-multiple-servers.ts b/server/tests/api/users/users-multiple-servers.ts
index 791418318..591ce4959 100644
--- a/server/tests/api/users/users-multiple-servers.ts
+++ b/server/tests/api/users/users-multiple-servers.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
@@ -57,17 +57,17 @@ describe('Test users with multiple servers', function () {
57 password: 'password' 57 password: 'password'
58 } 58 }
59 const res = await createUser({ 59 const res = await createUser({
60 url: servers[ 0 ].url, 60 url: servers[0].url,
61 accessToken: servers[ 0 ].accessToken, 61 accessToken: servers[0].accessToken,
62 username: user.username, 62 username: user.username,
63 password: user.password 63 password: user.password
64 }) 64 })
65 userId = res.body.user.id 65 userId = res.body.user.id
66 userAccessToken = await userLogin(servers[ 0 ], user) 66 userAccessToken = await userLogin(servers[0], user)
67 } 67 }
68 68
69 { 69 {
70 const resVideo = await uploadVideo(servers[ 0 ].url, userAccessToken, {}) 70 const resVideo = await uploadVideo(servers[0].url, userAccessToken, {})
71 videoUUID = resVideo.body.video.uuid 71 videoUUID = resVideo.body.video.uuid
72 } 72 }
73 73
@@ -86,7 +86,6 @@ describe('Test users with multiple servers', function () {
86 const res = await getMyUserInformation(servers[0].url, servers[0].accessToken) 86 const res = await getMyUserInformation(servers[0].url, servers[0].accessToken)
87 user = res.body 87 user = res.body
88 88
89 const account: Account = user.account
90 expect(user.account.displayName).to.equal('my super display name') 89 expect(user.account.displayName).to.equal('my super display name')
91 90
92 await waitJobs(servers) 91 await waitJobs(servers)
diff --git a/server/tests/api/users/users-verification.ts b/server/tests/api/users/users-verification.ts
index 7cd61f539..675ebf690 100644
--- a/server/tests/api/users/users-verification.ts
+++ b/server/tests/api/users/users-verification.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts
index 24203a731..c0cbce360 100644
--- a/server/tests/api/users/users.ts
+++ b/server/tests/api/users/users.ts
@@ -1,9 +1,10 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { User, UserRole, Video, MyUser, VideoPlaylistType } from '../../../../shared/index' 5import { MyUser, User, UserRole, Video, VideoAbuseState, VideoAbuseUpdate, VideoPlaylistType } from '../../../../shared/index'
6import { 6import {
7 addVideoCommentThread,
7 blockUser, 8 blockUser,
8 cleanupTests, 9 cleanupTests,
9 createUser, 10 createUser,
@@ -11,33 +12,41 @@ import {
11 flushAndRunServer, 12 flushAndRunServer,
12 getAccountRatings, 13 getAccountRatings,
13 getBlacklistedVideosList, 14 getBlacklistedVideosList,
15 getCustomConfig,
14 getMyUserInformation, 16 getMyUserInformation,
15 getMyUserVideoQuotaUsed, 17 getMyUserVideoQuotaUsed,
16 getMyUserVideoRating, 18 getMyUserVideoRating,
17 getUserInformation, 19 getUserInformation,
18 getUsersList, 20 getUsersList,
19 getUsersListPaginationAndSort, 21 getUsersListPaginationAndSort,
22 getVideoAbusesList,
20 getVideoChannel, 23 getVideoChannel,
21 getVideosList, installPlugin, 24 getVideosList,
25 installPlugin,
22 login, 26 login,
23 makePutBodyRequest, 27 makePutBodyRequest,
24 rateVideo, 28 rateVideo,
25 registerUserWithChannel, 29 registerUserWithChannel,
26 removeUser, 30 removeUser,
27 removeVideo, 31 removeVideo,
32 reportVideoAbuse,
28 ServerInfo, 33 ServerInfo,
29 testImage, 34 testImage,
30 unblockUser, 35 unblockUser,
36 updateCustomSubConfig,
31 updateMyAvatar, 37 updateMyAvatar,
32 updateMyUser, 38 updateMyUser,
33 updateUser, 39 updateUser,
40 updateVideoAbuse,
34 uploadVideo, 41 uploadVideo,
35 userLogin 42 userLogin,
43 waitJobs
36} from '../../../../shared/extra-utils' 44} from '../../../../shared/extra-utils'
37import { follow } from '../../../../shared/extra-utils/server/follows' 45import { follow } from '../../../../shared/extra-utils/server/follows'
38import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login' 46import { logout, serverLogin, setAccessTokensToServers } from '../../../../shared/extra-utils/users/login'
39import { getMyVideos } from '../../../../shared/extra-utils/videos/videos' 47import { getMyVideos } from '../../../../shared/extra-utils/videos/videos'
40import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' 48import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
49import { CustomConfig } from '@shared/models/server'
41 50
42const expect = chai.expect 51const expect = chai.expect
43 52
@@ -54,7 +63,14 @@ describe('Test users', function () {
54 63
55 before(async function () { 64 before(async function () {
56 this.timeout(30000) 65 this.timeout(30000)
57 server = await flushAndRunServer(1) 66
67 server = await flushAndRunServer(1, {
68 rates_limit: {
69 login: {
70 max: 30
71 }
72 }
73 })
58 74
59 await setAccessTokensToServers([ server ]) 75 await setAccessTokensToServers([ server ])
60 76
@@ -121,13 +137,13 @@ describe('Test users', function () {
121 137
122 it('Should be able to login with an insensitive username', async function () { 138 it('Should be able to login with an insensitive username', async function () {
123 const user = { username: 'RoOt', password: server.user.password } 139 const user = { username: 'RoOt', password: server.user.password }
124 const res = await login(server.url, server.client, user, 200) 140 await login(server.url, server.client, user, 200)
125 141
126 const user2 = { username: 'rOoT', password: server.user.password } 142 const user2 = { username: 'rOoT', password: server.user.password }
127 const res2 = await login(server.url, server.client, user2, 200) 143 await login(server.url, server.client, user2, 200)
128 144
129 const user3 = { username: 'ROOt', password: server.user.password } 145 const user3 = { username: 'ROOt', password: server.user.password }
130 const res3 = await login(server.url, server.client, user3, 200) 146 await login(server.url, server.client, user3, 200)
131 }) 147 })
132 }) 148 })
133 149
@@ -137,7 +153,7 @@ describe('Test users', function () {
137 const videoAttributes = {} 153 const videoAttributes = {}
138 await uploadVideo(server.url, accessToken, videoAttributes) 154 await uploadVideo(server.url, accessToken, videoAttributes)
139 const res = await getVideosList(server.url) 155 const res = await getVideosList(server.url)
140 const video = res.body.data[ 0 ] 156 const video = res.body.data[0]
141 157
142 expect(video.account.name).to.equal('root') 158 expect(video.account.name).to.equal('root')
143 videoId = video.id 159 videoId = video.id
@@ -167,8 +183,8 @@ describe('Test users', function () {
167 const ratings = res.body 183 const ratings = res.body
168 184
169 expect(ratings.total).to.equal(1) 185 expect(ratings.total).to.equal(1)
170 expect(ratings.data[ 0 ].video.id).to.equal(videoId) 186 expect(ratings.data[0].video.id).to.equal(videoId)
171 expect(ratings.data[ 0 ].rating).to.equal('like') 187 expect(ratings.data[0].rating).to.equal('like')
172 }) 188 })
173 189
174 it('Should retrieve ratings list by rating type', async function () { 190 it('Should retrieve ratings list by rating type', async function () {
@@ -199,13 +215,17 @@ describe('Test users', function () {
199 }) 215 })
200 216
201 describe('Logout', function () { 217 describe('Logout', function () {
202 it('Should logout (revoke token)') 218 it('Should logout (revoke token)', async function () {
203 219 await logout(server.url, server.accessToken)
204 it('Should not be able to get the user information') 220 })
205 221
206 it('Should not be able to upload a video') 222 it('Should not be able to get the user information', async function () {
223 await getMyUserInformation(server.url, server.accessToken, 401)
224 })
207 225
208 it('Should not be able to remove a video') 226 it('Should not be able to upload a video', async function () {
227 await uploadVideo(server.url, server.accessToken, { name: 'video' }, 401)
228 })
209 229
210 it('Should not be able to rate a video', async function () { 230 it('Should not be able to rate a video', async function () {
211 const path = '/api/v1/videos/' 231 const path = '/api/v1/videos/'
@@ -223,13 +243,17 @@ describe('Test users', function () {
223 await makePutBodyRequest(options) 243 await makePutBodyRequest(options)
224 }) 244 })
225 245
226 it('Should be able to login again') 246 it('Should be able to login again', async function () {
247 server.accessToken = await serverLogin(server)
248 })
227 249
228 it('Should have an expired access token') 250 it('Should have an expired access token')
229 251
230 it('Should refresh the token') 252 it('Should refresh the token')
231 253
232 it('Should be able to upload a video again') 254 it('Should be able to get my user information again', async function () {
255 await getMyUserInformation(server.url, server.accessToken)
256 })
233 }) 257 })
234 258
235 describe('Creating a user', function () { 259 describe('Creating a user', function () {
@@ -253,7 +277,7 @@ describe('Test users', function () {
253 const res1 = await getMyUserInformation(server.url, accessTokenUser) 277 const res1 = await getMyUserInformation(server.url, accessTokenUser)
254 const userMe: MyUser = res1.body 278 const userMe: MyUser = res1.body
255 279
256 const res2 = await getUserInformation(server.url, server.accessToken, userMe.id) 280 const res2 = await getUserInformation(server.url, server.accessToken, userMe.id, true)
257 const userGet: User = res2.body 281 const userGet: User = res2.body
258 282
259 for (const user of [ userMe, userGet ]) { 283 for (const user of [ userMe, userGet ]) {
@@ -272,13 +296,23 @@ describe('Test users', function () {
272 296
273 expect(userMe.specialPlaylists).to.have.lengthOf(1) 297 expect(userMe.specialPlaylists).to.have.lengthOf(1)
274 expect(userMe.specialPlaylists[0].type).to.equal(VideoPlaylistType.WATCH_LATER) 298 expect(userMe.specialPlaylists[0].type).to.equal(VideoPlaylistType.WATCH_LATER)
299
300 // Check stats are included with withStats
301 expect(userGet.videosCount).to.be.a('number')
302 expect(userGet.videosCount).to.equal(0)
303 expect(userGet.videoCommentsCount).to.be.a('number')
304 expect(userGet.videoCommentsCount).to.equal(0)
305 expect(userGet.videoAbusesCount).to.be.a('number')
306 expect(userGet.videoAbusesCount).to.equal(0)
307 expect(userGet.videoAbusesAcceptedCount).to.be.a('number')
308 expect(userGet.videoAbusesAcceptedCount).to.equal(0)
275 }) 309 })
276 }) 310 })
277 311
278 describe('My videos & quotas', function () { 312 describe('My videos & quotas', function () {
279 313
280 it('Should be able to upload a video with this user', async function () { 314 it('Should be able to upload a video with this user', async function () {
281 this.timeout(5000) 315 this.timeout(10000)
282 316
283 const videoAttributes = { 317 const videoAttributes = {
284 name: 'super user video', 318 name: 'super user video',
@@ -307,7 +341,7 @@ describe('Test users', function () {
307 const videos = res.body.data 341 const videos = res.body.data
308 expect(videos).to.have.lengthOf(1) 342 expect(videos).to.have.lengthOf(1)
309 343
310 const video: Video = videos[ 0 ] 344 const video: Video = videos[0]
311 expect(video.name).to.equal('super user video') 345 expect(video.name).to.equal('super user video')
312 expect(video.thumbnailPath).to.not.be.null 346 expect(video.thumbnailPath).to.not.be.null
313 expect(video.previewPath).to.not.be.null 347 expect(video.previewPath).to.not.be.null
@@ -330,6 +364,36 @@ describe('Test users', function () {
330 expect(videos).to.have.lengthOf(0) 364 expect(videos).to.have.lengthOf(0)
331 } 365 }
332 }) 366 })
367
368 it('Should disable webtorrent, enable HLS, and update my quota', async function () {
369 this.timeout(60000)
370
371 {
372 const res = await getCustomConfig(server.url, server.accessToken)
373 const config = res.body as CustomConfig
374 config.transcoding.webtorrent.enabled = false
375 config.transcoding.hls.enabled = true
376 config.transcoding.enabled = true
377 await updateCustomSubConfig(server.url, server.accessToken, config)
378 }
379
380 {
381 const videoAttributes = {
382 name: 'super user video 2',
383 fixture: 'video_short.webm'
384 }
385 await uploadVideo(server.url, accessTokenUser, videoAttributes)
386
387 await waitJobs([ server ])
388 }
389
390 {
391 const res = await getMyUserVideoQuotaUsed(server.url, accessTokenUser)
392 const data = res.body
393
394 expect(data.videoQuotaUsed).to.be.greaterThan(220000)
395 }
396 })
333 }) 397 })
334 398
335 describe('Users listing', function () { 399 describe('Users listing', function () {
@@ -344,16 +408,19 @@ describe('Test users', function () {
344 expect(users).to.be.an('array') 408 expect(users).to.be.an('array')
345 expect(users.length).to.equal(2) 409 expect(users.length).to.equal(2)
346 410
347 const user = users[ 0 ] 411 const user = users[0]
348 expect(user.username).to.equal('user_1') 412 expect(user.username).to.equal('user_1')
349 expect(user.email).to.equal('user_1@example.com') 413 expect(user.email).to.equal('user_1@example.com')
350 expect(user.nsfwPolicy).to.equal('display') 414 expect(user.nsfwPolicy).to.equal('display')
351 415
352 const rootUser = users[ 1 ] 416 const rootUser = users[1]
353 expect(rootUser.username).to.equal('root') 417 expect(rootUser.username).to.equal('root')
354 expect(rootUser.email).to.equal('admin' + server.internalServerNumber + '@example.com') 418 expect(rootUser.email).to.equal('admin' + server.internalServerNumber + '@example.com')
355 expect(user.nsfwPolicy).to.equal('display') 419 expect(user.nsfwPolicy).to.equal('display')
356 420
421 expect(rootUser.lastLoginDate).to.exist
422 expect(user.lastLoginDate).to.exist
423
357 userId = user.id 424 userId = user.id
358 }) 425 })
359 426
@@ -367,7 +434,7 @@ describe('Test users', function () {
367 expect(total).to.equal(2) 434 expect(total).to.equal(2)
368 expect(users.length).to.equal(1) 435 expect(users.length).to.equal(1)
369 436
370 const user = users[ 0 ] 437 const user = users[0]
371 expect(user.username).to.equal('root') 438 expect(user.username).to.equal('root')
372 expect(user.email).to.equal('admin' + server.internalServerNumber + '@example.com') 439 expect(user.email).to.equal('admin' + server.internalServerNumber + '@example.com')
373 expect(user.roleLabel).to.equal('Administrator') 440 expect(user.roleLabel).to.equal('Administrator')
@@ -383,7 +450,7 @@ describe('Test users', function () {
383 expect(total).to.equal(2) 450 expect(total).to.equal(2)
384 expect(users.length).to.equal(1) 451 expect(users.length).to.equal(1)
385 452
386 const user = users[ 0 ] 453 const user = users[0]
387 expect(user.username).to.equal('user_1') 454 expect(user.username).to.equal('user_1')
388 expect(user.email).to.equal('user_1@example.com') 455 expect(user.email).to.equal('user_1@example.com')
389 expect(user.nsfwPolicy).to.equal('display') 456 expect(user.nsfwPolicy).to.equal('display')
@@ -398,7 +465,7 @@ describe('Test users', function () {
398 expect(total).to.equal(2) 465 expect(total).to.equal(2)
399 expect(users.length).to.equal(1) 466 expect(users.length).to.equal(1)
400 467
401 const user = users[ 0 ] 468 const user = users[0]
402 expect(user.username).to.equal('user_1') 469 expect(user.username).to.equal('user_1')
403 expect(user.email).to.equal('user_1@example.com') 470 expect(user.email).to.equal('user_1@example.com')
404 expect(user.nsfwPolicy).to.equal('display') 471 expect(user.nsfwPolicy).to.equal('display')
@@ -413,13 +480,13 @@ describe('Test users', function () {
413 expect(total).to.equal(2) 480 expect(total).to.equal(2)
414 expect(users.length).to.equal(2) 481 expect(users.length).to.equal(2)
415 482
416 expect(users[ 0 ].username).to.equal('root') 483 expect(users[0].username).to.equal('root')
417 expect(users[ 0 ].email).to.equal('admin' + server.internalServerNumber + '@example.com') 484 expect(users[0].email).to.equal('admin' + server.internalServerNumber + '@example.com')
418 expect(users[ 0 ].nsfwPolicy).to.equal('display') 485 expect(users[0].nsfwPolicy).to.equal('display')
419 486
420 expect(users[ 1 ].username).to.equal('user_1') 487 expect(users[1].username).to.equal('user_1')
421 expect(users[ 1 ].email).to.equal('user_1@example.com') 488 expect(users[1].email).to.equal('user_1@example.com')
422 expect(users[ 1 ].nsfwPolicy).to.equal('display') 489 expect(users[1].nsfwPolicy).to.equal('display')
423 }) 490 })
424 491
425 it('Should search user by username', async function () { 492 it('Should search user by username', async function () {
@@ -429,7 +496,7 @@ describe('Test users', function () {
429 expect(res.body.total).to.equal(1) 496 expect(res.body.total).to.equal(1)
430 expect(users.length).to.equal(1) 497 expect(users.length).to.equal(1)
431 498
432 expect(users[ 0 ].username).to.equal('root') 499 expect(users[0].username).to.equal('root')
433 }) 500 })
434 501
435 it('Should search user by email', async function () { 502 it('Should search user by email', async function () {
@@ -440,8 +507,8 @@ describe('Test users', function () {
440 expect(res.body.total).to.equal(1) 507 expect(res.body.total).to.equal(1)
441 expect(users.length).to.equal(1) 508 expect(users.length).to.equal(1)
442 509
443 expect(users[ 0 ].username).to.equal('user_1') 510 expect(users[0].username).to.equal('user_1')
444 expect(users[ 0 ].email).to.equal('user_1@example.com') 511 expect(users[0].email).to.equal('user_1@example.com')
445 } 512 }
446 513
447 { 514 {
@@ -451,8 +518,8 @@ describe('Test users', function () {
451 expect(res.body.total).to.equal(2) 518 expect(res.body.total).to.equal(2)
452 expect(users.length).to.equal(2) 519 expect(users.length).to.equal(2)
453 520
454 expect(users[ 0 ].username).to.equal('root') 521 expect(users[0].username).to.equal('root')
455 expect(users[ 1 ].username).to.equal('user_1') 522 expect(users[1].username).to.equal('user_1')
456 } 523 }
457 }) 524 })
458 }) 525 })
@@ -622,7 +689,6 @@ describe('Test users', function () {
622 }) 689 })
623 690
624 describe('Updating another user', function () { 691 describe('Updating another user', function () {
625
626 it('Should be able to update another user', async function () { 692 it('Should be able to update another user', async function () {
627 await updateUser({ 693 await updateUser({
628 url: server.url, 694 url: server.url,
@@ -691,12 +757,14 @@ describe('Test users', function () {
691 757
692 expect(res.body.total).to.equal(1) 758 expect(res.body.total).to.equal(1)
693 759
694 const video = res.body.data[ 0 ] 760 const video = res.body.data[0]
695 expect(video.account.name).to.equal('root') 761 expect(video.account.name).to.equal('root')
696 }) 762 })
697 }) 763 })
698 764
699 describe('Registering a new user', function () { 765 describe('Registering a new user', function () {
766 let user15AccessToken
767
700 it('Should register a new user', async function () { 768 it('Should register a new user', async function () {
701 const user = { displayName: 'super user 15', username: 'user_15', password: 'my super password' } 769 const user = { displayName: 'super user 15', username: 'user_15', password: 'my super password' }
702 const channel = { name: 'my_user_15_channel', displayName: 'my channel rocks' } 770 const channel = { name: 'my_user_15_channel', displayName: 'my channel rocks' }
@@ -710,18 +778,18 @@ describe('Test users', function () {
710 password: 'my super password' 778 password: 'my super password'
711 } 779 }
712 780
713 accessToken = await userLogin(server, user15) 781 user15AccessToken = await userLogin(server, user15)
714 }) 782 })
715 783
716 it('Should have the correct display name', async function () { 784 it('Should have the correct display name', async function () {
717 const res = await getMyUserInformation(server.url, accessToken) 785 const res = await getMyUserInformation(server.url, user15AccessToken)
718 const user: User = res.body 786 const user: User = res.body
719 787
720 expect(user.account.displayName).to.equal('super user 15') 788 expect(user.account.displayName).to.equal('super user 15')
721 }) 789 })
722 790
723 it('Should have the correct video quota', async function () { 791 it('Should have the correct video quota', async function () {
724 const res = await getMyUserInformation(server.url, accessToken) 792 const res = await getMyUserInformation(server.url, user15AccessToken)
725 const user = res.body 793 const user = res.body
726 794
727 expect(user.videoQuota).to.equal(5 * 1024 * 1024) 795 expect(user.videoQuota).to.equal(5 * 1024 * 1024)
@@ -739,7 +807,7 @@ describe('Test users', function () {
739 expect(res.body.data.find(u => u.username === 'user_15')).to.not.be.undefined 807 expect(res.body.data.find(u => u.username === 'user_15')).to.not.be.undefined
740 } 808 }
741 809
742 await deleteMe(server.url, accessToken) 810 await deleteMe(server.url, user15AccessToken)
743 811
744 { 812 {
745 const res = await getUsersList(server.url, server.accessToken) 813 const res = await getUsersList(server.url, server.accessToken)
@@ -749,6 +817,9 @@ describe('Test users', function () {
749 }) 817 })
750 818
751 describe('User blocking', function () { 819 describe('User blocking', function () {
820 let user16Id
821 let user16AccessToken
822
752 it('Should block and unblock a user', async function () { 823 it('Should block and unblock a user', async function () {
753 const user16 = { 824 const user16 = {
754 username: 'user_16', 825 username: 'user_16',
@@ -760,19 +831,95 @@ describe('Test users', function () {
760 username: user16.username, 831 username: user16.username,
761 password: user16.password 832 password: user16.password
762 }) 833 })
763 const user16Id = resUser.body.user.id 834 user16Id = resUser.body.user.id
764 835
765 accessToken = await userLogin(server, user16) 836 user16AccessToken = await userLogin(server, user16)
766 837
767 await getMyUserInformation(server.url, accessToken, 200) 838 await getMyUserInformation(server.url, user16AccessToken, 200)
768 await blockUser(server.url, user16Id, server.accessToken) 839 await blockUser(server.url, user16Id, server.accessToken)
769 840
770 await getMyUserInformation(server.url, accessToken, 401) 841 await getMyUserInformation(server.url, user16AccessToken, 401)
771 await userLogin(server, user16, 400) 842 await userLogin(server, user16, 400)
772 843
773 await unblockUser(server.url, user16Id, server.accessToken) 844 await unblockUser(server.url, user16Id, server.accessToken)
774 accessToken = await userLogin(server, user16) 845 user16AccessToken = await userLogin(server, user16)
775 await getMyUserInformation(server.url, accessToken, 200) 846 await getMyUserInformation(server.url, user16AccessToken, 200)
847 })
848 })
849
850 describe('User stats', function () {
851 let user17Id
852 let user17AccessToken
853
854 it('Should report correct initial statistics about a user', async function () {
855 const user17 = {
856 username: 'user_17',
857 password: 'my super password'
858 }
859 const resUser = await createUser({
860 url: server.url,
861 accessToken: server.accessToken,
862 username: user17.username,
863 password: user17.password
864 })
865
866 user17Id = resUser.body.user.id
867 user17AccessToken = await userLogin(server, user17)
868
869 const res = await getUserInformation(server.url, server.accessToken, user17Id, true)
870 const user: User = res.body
871
872 expect(user.videosCount).to.equal(0)
873 expect(user.videoCommentsCount).to.equal(0)
874 expect(user.videoAbusesCount).to.equal(0)
875 expect(user.videoAbusesCreatedCount).to.equal(0)
876 expect(user.videoAbusesAcceptedCount).to.equal(0)
877 })
878
879 it('Should report correct videos count', async function () {
880 const videoAttributes = {
881 name: 'video to test user stats'
882 }
883 await uploadVideo(server.url, user17AccessToken, videoAttributes)
884 const res1 = await getVideosList(server.url)
885 videoId = res1.body.data.find(video => video.name === videoAttributes.name).id
886
887 const res2 = await getUserInformation(server.url, server.accessToken, user17Id, true)
888 const user: User = res2.body
889
890 expect(user.videosCount).to.equal(1)
891 })
892
893 it('Should report correct video comments for user', async function () {
894 const text = 'super comment'
895 await addVideoCommentThread(server.url, user17AccessToken, videoId, text)
896
897 const res = await getUserInformation(server.url, server.accessToken, user17Id, true)
898 const user: User = res.body
899
900 expect(user.videoCommentsCount).to.equal(1)
901 })
902
903 it('Should report correct video abuses counts', async function () {
904 const reason = 'my super bad reason'
905 await reportVideoAbuse(server.url, user17AccessToken, videoId, reason)
906
907 const res1 = await getVideoAbusesList({ url: server.url, token: server.accessToken })
908 const abuseId = res1.body.data[0].id
909
910 const res2 = await getUserInformation(server.url, server.accessToken, user17Id, true)
911 const user2: User = res2.body
912
913 expect(user2.videoAbusesCount).to.equal(1) // number of incriminations
914 expect(user2.videoAbusesCreatedCount).to.equal(1) // number of reports created
915
916 const body: VideoAbuseUpdate = { state: VideoAbuseState.ACCEPTED }
917 await updateVideoAbuse(server.url, server.accessToken, videoId, abuseId, body)
918
919 const res3 = await getUserInformation(server.url, server.accessToken, user17Id, true)
920 const user3: User = res3.body
921
922 expect(user3.videoAbusesAcceptedCount).to.equal(1) // number of reports created accepted
776 }) 923 })
777 }) 924 })
778 925
diff --git a/server/tests/api/videos/audio-only.ts b/server/tests/api/videos/audio-only.ts
index f12d730cc..ac7a0b89c 100644
--- a/server/tests/api/videos/audio-only.ts
+++ b/server/tests/api/videos/audio-only.ts
@@ -1,28 +1,21 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { 5import {
6 checkDirectoryIsEmpty,
7 checkSegmentHash,
8 checkTmpIsEmpty,
9 cleanupTests, 6 cleanupTests,
10 doubleFollow, 7 doubleFollow,
11 flushAndRunMultipleServers, 8 flushAndRunMultipleServers,
12 getPlaylist, 9 getVideo,
13 getVideo, makeGetRequest, makeRawRequest, 10 root,
14 removeVideo, root,
15 ServerInfo, 11 ServerInfo,
16 setAccessTokensToServers, updateCustomSubConfig, 12 setAccessTokensToServers,
17 updateVideo,
18 uploadVideo, 13 uploadVideo,
19 waitJobs, webtorrentAdd 14 waitJobs
20} from '../../../../shared/extra-utils' 15} from '../../../../shared/extra-utils'
21import { VideoDetails } from '../../../../shared/models/videos' 16import { VideoDetails } from '../../../../shared/models/videos'
22import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type'
23import { join } from 'path' 17import { join } from 'path'
24import { DEFAULT_AUDIO_RESOLUTION } from '../../../initializers/constants' 18import { audio, getVideoStreamSize } from '@server/helpers/ffmpeg-utils'
25import { getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution, audio, getVideoStreamSize } from '@server/helpers/ffmpeg-utils'
26 19
27const expect = chai.expect 20const expect = chai.expect
28 21
@@ -87,14 +80,14 @@ describe('Test audio only video transcoding', function () {
87 80
88 it('0p transcoded video should not have video', async function () { 81 it('0p transcoded video should not have video', async function () {
89 const paths = [ 82 const paths = [
90 join(root(), 'test' + servers[ 0 ].internalServerNumber, 'videos', videoUUID + '-0.mp4'), 83 join(root(), 'test' + servers[0].internalServerNumber, 'videos', videoUUID + '-0.mp4'),
91 join(root(), 'test' + servers[ 0 ].internalServerNumber, 'streaming-playlists', 'hls', videoUUID, videoUUID + '-0-fragmented.mp4') 84 join(root(), 'test' + servers[0].internalServerNumber, 'streaming-playlists', 'hls', videoUUID, videoUUID + '-0-fragmented.mp4')
92 ] 85 ]
93 86
94 for (const path of paths) { 87 for (const path of paths) {
95 const { audioStream } = await audio.get(path) 88 const { audioStream } = await audio.get(path)
96 expect(audioStream[ 'codec_name' ]).to.be.equal('aac') 89 expect(audioStream['codec_name']).to.be.equal('aac')
97 expect(audioStream[ 'bit_rate' ]).to.be.at.most(384 * 8000) 90 expect(audioStream['bit_rate']).to.be.at.most(384 * 8000)
98 91
99 const size = await getVideoStreamSize(path) 92 const size = await getVideoStreamSize(path)
100 expect(size.height).to.equal(0) 93 expect(size.height).to.equal(0)
diff --git a/server/tests/api/videos/multiple-servers.ts b/server/tests/api/videos/multiple-servers.ts
index fa3e250ec..e3029f1ae 100644
--- a/server/tests/api/videos/multiple-servers.ts
+++ b/server/tests/api/videos/multiple-servers.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
@@ -63,9 +63,9 @@ describe('Test multiple servers', function () {
63 displayName: 'my channel', 63 displayName: 'my channel',
64 description: 'super channel' 64 description: 'super channel'
65 } 65 }
66 await addVideoChannel(servers[ 0 ].url, servers[ 0 ].accessToken, videoChannel) 66 await addVideoChannel(servers[0].url, servers[0].accessToken, videoChannel)
67 const channelRes = await getVideoChannelsList(servers[ 0 ].url, 0, 1) 67 const channelRes = await getVideoChannelsList(servers[0].url, 0, 1)
68 videoChannelId = channelRes.body.data[ 0 ].id 68 videoChannelId = channelRes.body.data[0].id
69 } 69 }
70 70
71 // Server 1 and server 2 follow each other 71 // Server 1 and server 2 follow each other
@@ -163,7 +163,7 @@ describe('Test multiple servers', function () {
163 username: 'user1', 163 username: 'user1',
164 password: 'super_password' 164 password: 'super_password'
165 } 165 }
166 await createUser({ url: servers[ 1 ].url, accessToken: servers[ 1 ].accessToken, username: user.username, password: user.password }) 166 await createUser({ url: servers[1].url, accessToken: servers[1].accessToken, username: user.username, password: user.password })
167 const userAccessToken = await userLogin(servers[1], user) 167 const userAccessToken = await userLogin(servers[1], user)
168 168
169 const videoAttributes = { 169 const videoAttributes = {
@@ -762,12 +762,12 @@ describe('Test multiple servers', function () {
762 762
763 { 763 {
764 const text = 'my super first comment' 764 const text = 'my super first comment'
765 await addVideoCommentThread(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID, text) 765 await addVideoCommentThread(servers[0].url, servers[0].accessToken, videoUUID, text)
766 } 766 }
767 767
768 { 768 {
769 const text = 'my super second comment' 769 const text = 'my super second comment'
770 await addVideoCommentThread(servers[ 2 ].url, servers[ 2 ].accessToken, videoUUID, text) 770 await addVideoCommentThread(servers[2].url, servers[2].accessToken, videoUUID, text)
771 } 771 }
772 772
773 await waitJobs(servers) 773 await waitJobs(servers)
@@ -777,7 +777,7 @@ describe('Test multiple servers', function () {
777 const threadId = res.body.data.find(c => c.text === 'my super first comment').id 777 const threadId = res.body.data.find(c => c.text === 'my super first comment').id
778 778
779 const text = 'my super answer to thread 1' 779 const text = 'my super answer to thread 1'
780 await addVideoCommentReply(servers[ 1 ].url, servers[ 1 ].accessToken, videoUUID, threadId, text) 780 await addVideoCommentReply(servers[1].url, servers[1].accessToken, videoUUID, threadId, text)
781 } 781 }
782 782
783 await waitJobs(servers) 783 await waitJobs(servers)
@@ -790,10 +790,10 @@ describe('Test multiple servers', function () {
790 const childCommentId = res2.body.children[0].comment.id 790 const childCommentId = res2.body.children[0].comment.id
791 791
792 const text3 = 'my second answer to thread 1' 792 const text3 = 'my second answer to thread 1'
793 await addVideoCommentReply(servers[ 2 ].url, servers[ 2 ].accessToken, videoUUID, threadId, text3) 793 await addVideoCommentReply(servers[2].url, servers[2].accessToken, videoUUID, threadId, text3)
794 794
795 const text2 = 'my super answer to answer of thread 1' 795 const text2 = 'my super answer to answer of thread 1'
796 await addVideoCommentReply(servers[ 2 ].url, servers[ 2 ].accessToken, videoUUID, childCommentId, text2) 796 await addVideoCommentReply(servers[2].url, servers[2].accessToken, videoUUID, childCommentId, text2)
797 } 797 }
798 798
799 await waitJobs(servers) 799 await waitJobs(servers)
@@ -900,9 +900,9 @@ describe('Test multiple servers', function () {
900 it('Should delete the thread comments', async function () { 900 it('Should delete the thread comments', async function () {
901 this.timeout(10000) 901 this.timeout(10000)
902 902
903 const res = await getVideoCommentThreads(servers[ 0 ].url, videoUUID, 0, 5) 903 const res = await getVideoCommentThreads(servers[0].url, videoUUID, 0, 5)
904 const threadId = res.body.data.find(c => c.text === 'my super first comment').id 904 const threadId = res.body.data.find(c => c.text === 'my super first comment').id
905 await deleteVideoComment(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID, threadId) 905 await deleteVideoComment(servers[0].url, servers[0].accessToken, videoUUID, threadId)
906 906
907 await waitJobs(servers) 907 await waitJobs(servers)
908 }) 908 })
@@ -945,9 +945,9 @@ describe('Test multiple servers', function () {
945 it('Should delete a remote thread by the origin server', async function () { 945 it('Should delete a remote thread by the origin server', async function () {
946 this.timeout(5000) 946 this.timeout(5000)
947 947
948 const res = await getVideoCommentThreads(servers[ 0 ].url, videoUUID, 0, 5) 948 const res = await getVideoCommentThreads(servers[0].url, videoUUID, 0, 5)
949 const threadId = res.body.data.find(c => c.text === 'my super second comment').id 949 const threadId = res.body.data.find(c => c.text === 'my super second comment').id
950 await deleteVideoComment(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID, threadId) 950 await deleteVideoComment(servers[0].url, servers[0].accessToken, videoUUID, threadId)
951 951
952 await waitJobs(servers) 952 await waitJobs(servers)
953 }) 953 })
@@ -1021,7 +1021,7 @@ describe('Test multiple servers', function () {
1021 const filePath = join(__dirname, '..', '..', 'fixtures', 'video_short.webm') 1021 const filePath = join(__dirname, '..', '..', 'fixtures', 'video_short.webm')
1022 1022
1023 await req.attach('videofile', filePath) 1023 await req.attach('videofile', filePath)
1024 .expect(200) 1024 .expect(200)
1025 1025
1026 await waitJobs(servers) 1026 await waitJobs(servers)
1027 1027
@@ -1046,7 +1046,7 @@ describe('Test multiple servers', function () {
1046 duration: 5, 1046 duration: 5,
1047 commentsEnabled: true, 1047 commentsEnabled: true,
1048 downloadEnabled: true, 1048 downloadEnabled: true,
1049 tags: [ ], 1049 tags: [],
1050 privacy: VideoPrivacy.PUBLIC, 1050 privacy: VideoPrivacy.PUBLIC,
1051 channel: { 1051 channel: {
1052 displayName: 'Main root channel', 1052 displayName: 'Main root channel',
diff --git a/server/tests/api/videos/services.ts b/server/tests/api/videos/services.ts
index 17172331f..5505a845a 100644
--- a/server/tests/api/videos/services.ts
+++ b/server/tests/api/videos/services.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
@@ -31,8 +31,8 @@ describe('Test services', function () {
31 31
32 const res = await getOEmbed(server.url, oembedUrl) 32 const res = await getOEmbed(server.url, oembedUrl)
33 const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts" ' + 33 const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts" ' +
34 `src="http://localhost:${server.port}/videos/embed/${server.video.uuid}" ` + 34 `src="http://localhost:${server.port}/videos/embed/${server.video.uuid}" ` +
35 'frameborder="0" allowfullscreen></iframe>' 35 'frameborder="0" allowfullscreen></iframe>'
36 const expectedThumbnailUrl = 'http://localhost:' + server.port + '/static/previews/' + server.video.uuid + '.jpg' 36 const expectedThumbnailUrl = 'http://localhost:' + server.port + '/static/previews/' + server.video.uuid + '.jpg'
37 37
38 expect(res.body.html).to.equal(expectedHtml) 38 expect(res.body.html).to.equal(expectedHtml)
@@ -53,8 +53,8 @@ describe('Test services', function () {
53 53
54 const res = await getOEmbed(server.url, oembedUrl, format, maxHeight, maxWidth) 54 const res = await getOEmbed(server.url, oembedUrl, format, maxHeight, maxWidth)
55 const expectedHtml = '<iframe width="50" height="50" sandbox="allow-same-origin allow-scripts" ' + 55 const expectedHtml = '<iframe width="50" height="50" sandbox="allow-same-origin allow-scripts" ' +
56 `src="http://localhost:${server.port}/videos/embed/${server.video.uuid}" ` + 56 `src="http://localhost:${server.port}/videos/embed/${server.video.uuid}" ` +
57 'frameborder="0" allowfullscreen></iframe>' 57 'frameborder="0" allowfullscreen></iframe>'
58 58
59 expect(res.body.html).to.equal(expectedHtml) 59 expect(res.body.html).to.equal(expectedHtml)
60 expect(res.body.title).to.equal(server.video.name) 60 expect(res.body.title).to.equal(server.video.name)
diff --git a/server/tests/api/videos/single-server.ts b/server/tests/api/videos/single-server.ts
index 362d6b78f..0ae405950 100644
--- a/server/tests/api/videos/single-server.ts
+++ b/server/tests/api/videos/single-server.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import { keyBy } from 'lodash' 4import { keyBy } from 'lodash'
@@ -34,6 +34,7 @@ const expect = chai.expect
34describe('Test a single server', function () { 34describe('Test a single server', function () {
35 let server: ServerInfo = null 35 let server: ServerInfo = null
36 let videoId = -1 36 let videoId = -1
37 let videoId2 = -1
37 let videoUUID = '' 38 let videoUUID = ''
38 let videosListBase: any[] = null 39 let videosListBase: any[] = null
39 40
@@ -237,12 +238,11 @@ describe('Test a single server', function () {
237 it('Should upload 6 videos', async function () { 238 it('Should upload 6 videos', async function () {
238 this.timeout(25000) 239 this.timeout(25000)
239 240
240 const videos = [ 241 const videos = new Set([
241 'video_short.mp4', 'video_short.ogv', 'video_short.webm', 242 'video_short.mp4', 'video_short.ogv', 'video_short.webm',
242 'video_short1.webm', 'video_short2.webm', 'video_short3.webm' 243 'video_short1.webm', 'video_short2.webm', 'video_short3.webm'
243 ] 244 ])
244 245
245 const tasks: Promise<any>[] = []
246 for (const video of videos) { 246 for (const video of videos) {
247 const videoAttributes = { 247 const videoAttributes = {
248 name: video + ' name', 248 name: video + ' name',
@@ -255,11 +255,8 @@ describe('Test a single server', function () {
255 fixture: video 255 fixture: video
256 } 256 }
257 257
258 const p = uploadVideo(server.url, server.accessToken, videoAttributes) 258 await uploadVideo(server.url, server.accessToken, videoAttributes)
259 tasks.push(p)
260 } 259 }
261
262 await Promise.all(tasks)
263 }) 260 })
264 261
265 it('Should have the correct durations', async function () { 262 it('Should have the correct durations', async function () {
@@ -345,6 +342,7 @@ describe('Test a single server', function () {
345 expect(videos[5].name).to.equal('video_short1.webm name') 342 expect(videos[5].name).to.equal('video_short1.webm name')
346 343
347 videoId = videos[3].uuid 344 videoId = videos[3].uuid
345 videoId2 = videos[5].uuid
348 }) 346 })
349 347
350 it('Should list and sort by trending in descending order', async function () { 348 it('Should list and sort by trending in descending order', async function () {
@@ -433,6 +431,43 @@ describe('Test a single server', function () {
433 expect(video.dislikes).to.equal(1) 431 expect(video.dislikes).to.equal(1)
434 }) 432 })
435 433
434 it('Should sort by originallyPublishedAt', async function () {
435 {
436
437 {
438 const now = new Date()
439 const attributes = { originallyPublishedAt: now.toISOString() }
440 await updateVideo(server.url, server.accessToken, videoId, attributes)
441
442 const res = await getVideosListSort(server.url, '-originallyPublishedAt')
443 const names = res.body.data.map(v => v.name)
444
445 expect(names[0]).to.equal('my super video updated')
446 expect(names[1]).to.equal('video_short2.webm name')
447 expect(names[2]).to.equal('video_short1.webm name')
448 expect(names[3]).to.equal('video_short.webm name')
449 expect(names[4]).to.equal('video_short.ogv name')
450 expect(names[5]).to.equal('video_short.mp4 name')
451 }
452
453 {
454 const now = new Date()
455 const attributes = { originallyPublishedAt: now.toISOString() }
456 await updateVideo(server.url, server.accessToken, videoId2, attributes)
457
458 const res = await getVideosListSort(server.url, '-originallyPublishedAt')
459 const names = res.body.data.map(v => v.name)
460
461 expect(names[0]).to.equal('video_short1.webm name')
462 expect(names[1]).to.equal('my super video updated')
463 expect(names[2]).to.equal('video_short2.webm name')
464 expect(names[3]).to.equal('video_short.webm name')
465 expect(names[4]).to.equal('video_short.ogv name')
466 expect(names[5]).to.equal('video_short.mp4 name')
467 }
468 }
469 })
470
436 after(async function () { 471 after(async function () {
437 await cleanupTests([ server ]) 472 await cleanupTests([ server ])
438 }) 473 })
diff --git a/server/tests/api/videos/video-abuse.ts b/server/tests/api/videos/video-abuse.ts
index 0cd6f22c7..a96be97f6 100644
--- a/server/tests/api/videos/video-abuse.ts
+++ b/server/tests/api/videos/video-abuse.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
@@ -13,7 +13,10 @@ import {
13 ServerInfo, 13 ServerInfo,
14 setAccessTokensToServers, 14 setAccessTokensToServers,
15 updateVideoAbuse, 15 updateVideoAbuse,
16 uploadVideo 16 uploadVideo,
17 removeVideo,
18 createUser,
19 userLogin
17} from '../../../../shared/extra-utils/index' 20} from '../../../../shared/extra-utils/index'
18import { doubleFollow } from '../../../../shared/extra-utils/server/follows' 21import { doubleFollow } from '../../../../shared/extra-utils/server/follows'
19import { waitJobs } from '../../../../shared/extra-utils/server/jobs' 22import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
@@ -68,7 +71,7 @@ describe('Test video abuses', function () {
68 }) 71 })
69 72
70 it('Should not have video abuses', async function () { 73 it('Should not have video abuses', async function () {
71 const res = await getVideoAbusesList(servers[0].url, servers[0].accessToken) 74 const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
72 75
73 expect(res.body.total).to.equal(0) 76 expect(res.body.total).to.equal(0)
74 expect(res.body.data).to.be.an('array') 77 expect(res.body.data).to.be.an('array')
@@ -86,7 +89,7 @@ describe('Test video abuses', function () {
86 }) 89 })
87 90
88 it('Should have 1 video abuses on server 1 and 0 on server 2', async function () { 91 it('Should have 1 video abuses on server 1 and 0 on server 2', async function () {
89 const res1 = await getVideoAbusesList(servers[0].url, servers[0].accessToken) 92 const res1 = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
90 93
91 expect(res1.body.total).to.equal(1) 94 expect(res1.body.total).to.equal(1)
92 expect(res1.body.data).to.be.an('array') 95 expect(res1.body.data).to.be.an('array')
@@ -97,8 +100,13 @@ describe('Test video abuses', function () {
97 expect(abuse.reporterAccount.name).to.equal('root') 100 expect(abuse.reporterAccount.name).to.equal('root')
98 expect(abuse.reporterAccount.host).to.equal('localhost:' + servers[0].port) 101 expect(abuse.reporterAccount.host).to.equal('localhost:' + servers[0].port)
99 expect(abuse.video.id).to.equal(servers[0].video.id) 102 expect(abuse.video.id).to.equal(servers[0].video.id)
103 expect(abuse.video.channel).to.exist
104 expect(abuse.count).to.equal(1)
105 expect(abuse.nth).to.equal(1)
106 expect(abuse.countReportsForReporter).to.equal(1)
107 expect(abuse.countReportsForReportee).to.equal(1)
100 108
101 const res2 = await getVideoAbusesList(servers[1].url, servers[1].accessToken) 109 const res2 = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken })
102 expect(res2.body.total).to.equal(0) 110 expect(res2.body.total).to.equal(0)
103 expect(res2.body.data).to.be.an('array') 111 expect(res2.body.data).to.be.an('array')
104 expect(res2.body.data.length).to.equal(0) 112 expect(res2.body.data.length).to.equal(0)
@@ -115,7 +123,7 @@ describe('Test video abuses', function () {
115 }) 123 })
116 124
117 it('Should have 2 video abuses on server 1 and 1 on server 2', async function () { 125 it('Should have 2 video abuses on server 1 and 1 on server 2', async function () {
118 const res1 = await getVideoAbusesList(servers[0].url, servers[0].accessToken) 126 const res1 = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
119 expect(res1.body.total).to.equal(2) 127 expect(res1.body.total).to.equal(2)
120 expect(res1.body.data).to.be.an('array') 128 expect(res1.body.data).to.be.an('array')
121 expect(res1.body.data.length).to.equal(2) 129 expect(res1.body.data.length).to.equal(2)
@@ -128,6 +136,8 @@ describe('Test video abuses', function () {
128 expect(abuse1.state.id).to.equal(VideoAbuseState.PENDING) 136 expect(abuse1.state.id).to.equal(VideoAbuseState.PENDING)
129 expect(abuse1.state.label).to.equal('Pending') 137 expect(abuse1.state.label).to.equal('Pending')
130 expect(abuse1.moderationComment).to.be.null 138 expect(abuse1.moderationComment).to.be.null
139 expect(abuse1.count).to.equal(1)
140 expect(abuse1.nth).to.equal(1)
131 141
132 const abuse2: VideoAbuse = res1.body.data[1] 142 const abuse2: VideoAbuse = res1.body.data[1]
133 expect(abuse2.reason).to.equal('my super bad reason 2') 143 expect(abuse2.reason).to.equal('my super bad reason 2')
@@ -138,7 +148,7 @@ describe('Test video abuses', function () {
138 expect(abuse2.state.label).to.equal('Pending') 148 expect(abuse2.state.label).to.equal('Pending')
139 expect(abuse2.moderationComment).to.be.null 149 expect(abuse2.moderationComment).to.be.null
140 150
141 const res2 = await getVideoAbusesList(servers[1].url, servers[1].accessToken) 151 const res2 = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken })
142 expect(res2.body.total).to.equal(1) 152 expect(res2.body.total).to.equal(1)
143 expect(res2.body.data).to.be.an('array') 153 expect(res2.body.data).to.be.an('array')
144 expect(res2.body.data.length).to.equal(1) 154 expect(res2.body.data.length).to.equal(1)
@@ -156,7 +166,7 @@ describe('Test video abuses', function () {
156 const body = { state: VideoAbuseState.REJECTED } 166 const body = { state: VideoAbuseState.REJECTED }
157 await updateVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id, body) 167 await updateVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id, body)
158 168
159 const res = await getVideoAbusesList(servers[1].url, servers[1].accessToken) 169 const res = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken })
160 expect(res.body.data[0].state.id).to.equal(VideoAbuseState.REJECTED) 170 expect(res.body.data[0].state.id).to.equal(VideoAbuseState.REJECTED)
161 }) 171 })
162 172
@@ -164,7 +174,7 @@ describe('Test video abuses', function () {
164 const body = { state: VideoAbuseState.ACCEPTED, moderationComment: 'It is valid' } 174 const body = { state: VideoAbuseState.ACCEPTED, moderationComment: 'It is valid' }
165 await updateVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id, body) 175 await updateVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id, body)
166 176
167 const res = await getVideoAbusesList(servers[1].url, servers[1].accessToken) 177 const res = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken })
168 expect(res.body.data[0].state.id).to.equal(VideoAbuseState.ACCEPTED) 178 expect(res.body.data[0].state.id).to.equal(VideoAbuseState.ACCEPTED)
169 expect(res.body.data[0].moderationComment).to.equal('It is valid') 179 expect(res.body.data[0].moderationComment).to.equal('It is valid')
170 }) 180 })
@@ -176,16 +186,16 @@ describe('Test video abuses', function () {
176 await reportVideoAbuse(servers[1].url, servers[1].accessToken, servers[0].video.uuid, 'will mute this') 186 await reportVideoAbuse(servers[1].url, servers[1].accessToken, servers[0].video.uuid, 'will mute this')
177 await waitJobs(servers) 187 await waitJobs(servers)
178 188
179 const res = await getVideoAbusesList(servers[0].url, servers[0].accessToken) 189 const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
180 expect(res.body.total).to.equal(3) 190 expect(res.body.total).to.equal(3)
181 } 191 }
182 192
183 const accountToBlock = 'root@localhost:' + servers[1].port 193 const accountToBlock = 'root@localhost:' + servers[1].port
184 194
185 { 195 {
186 await addAccountToServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, accountToBlock) 196 await addAccountToServerBlocklist(servers[0].url, servers[0].accessToken, accountToBlock)
187 197
188 const res = await getVideoAbusesList(servers[ 0 ].url, servers[ 0 ].accessToken) 198 const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
189 expect(res.body.total).to.equal(2) 199 expect(res.body.total).to.equal(2)
190 200
191 const abuse = res.body.data.find(a => a.reason === 'will mute this') 201 const abuse = res.body.data.find(a => a.reason === 'will mute this')
@@ -193,9 +203,9 @@ describe('Test video abuses', function () {
193 } 203 }
194 204
195 { 205 {
196 await removeAccountFromServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, accountToBlock) 206 await removeAccountFromServerBlocklist(servers[0].url, servers[0].accessToken, accountToBlock)
197 207
198 const res = await getVideoAbusesList(servers[ 0 ].url, servers[ 0 ].accessToken) 208 const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
199 expect(res.body.total).to.equal(3) 209 expect(res.body.total).to.equal(3)
200 } 210 }
201 }) 211 })
@@ -204,9 +214,9 @@ describe('Test video abuses', function () {
204 const serverToBlock = servers[1].host 214 const serverToBlock = servers[1].host
205 215
206 { 216 {
207 await addServerToServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, servers[1].host) 217 await addServerToServerBlocklist(servers[0].url, servers[0].accessToken, servers[1].host)
208 218
209 const res = await getVideoAbusesList(servers[ 0 ].url, servers[ 0 ].accessToken) 219 const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
210 expect(res.body.total).to.equal(2) 220 expect(res.body.total).to.equal(2)
211 221
212 const abuse = res.body.data.find(a => a.reason === 'will mute this') 222 const abuse = res.body.data.find(a => a.reason === 'will mute this')
@@ -214,13 +224,73 @@ describe('Test video abuses', function () {
214 } 224 }
215 225
216 { 226 {
217 await removeServerFromServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, serverToBlock) 227 await removeServerFromServerBlocklist(servers[0].url, servers[0].accessToken, serverToBlock)
218 228
219 const res = await getVideoAbusesList(servers[ 0 ].url, servers[ 0 ].accessToken) 229 const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
220 expect(res.body.total).to.equal(3) 230 expect(res.body.total).to.equal(3)
221 } 231 }
222 }) 232 })
223 233
234 it('Should keep the video abuse when deleting the video', async function () {
235 this.timeout(10000)
236
237 await removeVideo(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid)
238
239 await waitJobs(servers)
240
241 const res = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken })
242 expect(res.body.total).to.equal(2, "wrong number of videos returned")
243 expect(res.body.data.length).to.equal(2, "wrong number of videos returned")
244 expect(res.body.data[0].id).to.equal(abuseServer2.id, "wrong origin server id for first video")
245
246 const abuse: VideoAbuse = res.body.data[0]
247 expect(abuse.video.id).to.equal(abuseServer2.video.id, "wrong video id")
248 expect(abuse.video.channel).to.exist
249 expect(abuse.video.deleted).to.be.true
250 })
251
252 it('Should include counts of reports from reporter and reportee', async function () {
253 this.timeout(10000)
254
255 // register a second user to have two reporters/reportees
256 const user = { username: 'user2', password: 'password' }
257 await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, ...user })
258 const userAccessToken = await userLogin(servers[0], user)
259
260 // upload a third video via this user
261 const video3Attributes = {
262 name: 'my second super name for server 1',
263 description: 'my second super description for server 1'
264 }
265 await uploadVideo(servers[0].url, userAccessToken, video3Attributes)
266
267 const res1 = await getVideosList(servers[0].url)
268 const videos = res1.body.data
269 const video3 = videos.find(video => video.name === 'my second super name for server 1')
270
271 // resume with the test
272 const reason3 = 'my super bad reason 3'
273 await reportVideoAbuse(servers[0].url, servers[0].accessToken, video3.id, reason3)
274 const reason4 = 'my super bad reason 4'
275 await reportVideoAbuse(servers[0].url, userAccessToken, servers[0].video.id, reason4)
276
277 const res2 = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
278
279 {
280 for (const abuse of res2.body.data as VideoAbuse[]) {
281 if (abuse.video.id === video3.id) {
282 expect(abuse.count).to.equal(1, "wrong reports count for video 3")
283 expect(abuse.nth).to.equal(1, "wrong report position in report list for video 3")
284 expect(abuse.countReportsForReportee).to.equal(1, "wrong reports count for reporter on video 3 abuse")
285 expect(abuse.countReportsForReporter).to.equal(3, "wrong reports count for reportee on video 3 abuse")
286 }
287 if (abuse.video.id === servers[0].video.id) {
288 expect(abuse.countReportsForReportee).to.equal(3, "wrong reports count for reporter on video 1 abuse")
289 }
290 }
291 }
292 })
293
224 it('Should delete the video abuse', async function () { 294 it('Should delete the video abuse', async function () {
225 this.timeout(10000) 295 this.timeout(10000)
226 296
@@ -229,16 +299,54 @@ describe('Test video abuses', function () {
229 await waitJobs(servers) 299 await waitJobs(servers)
230 300
231 { 301 {
232 const res = await getVideoAbusesList(servers[1].url, servers[1].accessToken) 302 const res = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken })
233 expect(res.body.total).to.equal(1) 303 expect(res.body.total).to.equal(1)
234 expect(res.body.data.length).to.equal(1) 304 expect(res.body.data.length).to.equal(1)
235 expect(res.body.data[0].id).to.not.equal(abuseServer2.id) 305 expect(res.body.data[0].id).to.not.equal(abuseServer2.id)
236 } 306 }
237 307
238 { 308 {
239 const res = await getVideoAbusesList(servers[0].url, servers[0].accessToken) 309 const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
240 expect(res.body.total).to.equal(3) 310 expect(res.body.total).to.equal(5)
311 }
312 })
313
314 it('Should list and filter video abuses', async function () {
315 async function list (query: Omit<Parameters<typeof getVideoAbusesList>[0], 'url' | 'token'>) {
316 const options = {
317 url: servers[0].url,
318 token: servers[0].accessToken
319 }
320
321 Object.assign(options, query)
322
323 const res = await getVideoAbusesList(options)
324
325 return res.body.data as VideoAbuse[]
241 } 326 }
327
328 expect(await list({ id: 56 })).to.have.lengthOf(0)
329 expect(await list({ id: 1 })).to.have.lengthOf(1)
330
331 expect(await list({ search: 'my super name for server 1' })).to.have.lengthOf(3)
332 expect(await list({ search: 'aaaaaaaaaaaaaaaaaaaaaaaaaa' })).to.have.lengthOf(0)
333
334 expect(await list({ searchVideo: 'my second super name for server 1' })).to.have.lengthOf(1)
335
336 expect(await list({ searchVideoChannel: 'root' })).to.have.lengthOf(3)
337 expect(await list({ searchVideoChannel: 'aaaa' })).to.have.lengthOf(0)
338
339 expect(await list({ searchReporter: 'user2' })).to.have.lengthOf(1)
340 expect(await list({ searchReporter: 'root' })).to.have.lengthOf(4)
341
342 expect(await list({ searchReportee: 'root' })).to.have.lengthOf(3)
343 expect(await list({ searchReportee: 'aaaa' })).to.have.lengthOf(0)
344
345 expect(await list({ videoIs: 'deleted' })).to.have.lengthOf(1)
346 expect(await list({ videoIs: 'blacklisted' })).to.have.lengthOf(0)
347
348 expect(await list({ state: VideoAbuseState.ACCEPTED })).to.have.lengthOf(0)
349 expect(await list({ state: VideoAbuseState.PENDING })).to.have.lengthOf(5)
242 }) 350 })
243 351
244 after(async function () { 352 after(async function () {
diff --git a/server/tests/api/videos/video-blacklist.ts b/server/tests/api/videos/video-blacklist.ts
index 854b2f0cb..67bc0114c 100644
--- a/server/tests/api/videos/video-blacklist.ts
+++ b/server/tests/api/videos/video-blacklist.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import { orderBy } from 'lodash' 4import { orderBy } from 'lodash'
@@ -8,7 +8,8 @@ import {
8 cleanupTests, 8 cleanupTests,
9 createUser, 9 createUser,
10 flushAndRunMultipleServers, 10 flushAndRunMultipleServers,
11 getBlacklistedVideosList, getMyUserInformation, 11 getBlacklistedVideosList,
12 getMyUserInformation,
12 getMyVideos, 13 getMyVideos,
13 getVideosList, 14 getVideosList,
14 killallServers, 15 killallServers,
@@ -17,7 +18,6 @@ import {
17 searchVideo, 18 searchVideo,
18 ServerInfo, 19 ServerInfo,
19 setAccessTokensToServers, 20 setAccessTokensToServers,
20 setDefaultVideoChannel,
21 updateVideo, 21 updateVideo,
22 updateVideoBlacklist, 22 updateVideoBlacklist,
23 uploadVideo, 23 uploadVideo,
@@ -27,7 +27,7 @@ import { doubleFollow } from '../../../../shared/extra-utils/server/follows'
27import { waitJobs } from '../../../../shared/extra-utils/server/jobs' 27import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
28import { VideoBlacklist, VideoBlacklistType } from '../../../../shared/models/videos' 28import { VideoBlacklist, VideoBlacklistType } from '../../../../shared/models/videos'
29import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' 29import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
30import { User, UserRole, UserUpdateMe } from '../../../../shared/models/users' 30import { User, UserRole } from '../../../../shared/models/users'
31import { getMagnetURI, getYoutubeVideoUrl, importVideo } from '../../../../shared/extra-utils/videos/video-imports' 31import { getMagnetURI, getYoutubeVideoUrl, importVideo } from '../../../../shared/extra-utils/videos/video-imports'
32 32
33const expect = chai.expect 33const expect = chai.expect
@@ -40,7 +40,7 @@ describe('Test video blacklist', function () {
40 const res = await getVideosList(server.url) 40 const res = await getVideosList(server.url)
41 41
42 const videos = res.body.data 42 const videos = res.body.data
43 for (let video of videos) { 43 for (const video of videos) {
44 await addVideoToBlacklist(server.url, server.accessToken, video.id, 'super reason') 44 await addVideoToBlacklist(server.url, server.accessToken, video.id, 'super reason')
45 } 45 }
46 } 46 }
@@ -72,7 +72,7 @@ describe('Test video blacklist', function () {
72 72
73 it('Should not have the video blacklisted in videos list/search on server 1', async function () { 73 it('Should not have the video blacklisted in videos list/search on server 1', async function () {
74 { 74 {
75 const res = await getVideosList(servers[ 0 ].url) 75 const res = await getVideosList(servers[0].url)
76 76
77 expect(res.body.total).to.equal(0) 77 expect(res.body.total).to.equal(0)
78 expect(res.body.data).to.be.an('array') 78 expect(res.body.data).to.be.an('array')
@@ -80,7 +80,7 @@ describe('Test video blacklist', function () {
80 } 80 }
81 81
82 { 82 {
83 const res = await searchVideo(servers[ 0 ].url, 'name') 83 const res = await searchVideo(servers[0].url, 'name')
84 84
85 expect(res.body.total).to.equal(0) 85 expect(res.body.total).to.equal(0)
86 expect(res.body.data).to.be.an('array') 86 expect(res.body.data).to.be.an('array')
@@ -90,7 +90,7 @@ describe('Test video blacklist', function () {
90 90
91 it('Should have the blacklisted video in videos list/search on server 2', async function () { 91 it('Should have the blacklisted video in videos list/search on server 2', async function () {
92 { 92 {
93 const res = await getVideosList(servers[ 1 ].url) 93 const res = await getVideosList(servers[1].url)
94 94
95 expect(res.body.total).to.equal(2) 95 expect(res.body.total).to.equal(2)
96 expect(res.body.data).to.be.an('array') 96 expect(res.body.data).to.be.an('array')
@@ -98,7 +98,7 @@ describe('Test video blacklist', function () {
98 } 98 }
99 99
100 { 100 {
101 const res = await searchVideo(servers[ 1 ].url, 'video') 101 const res = await searchVideo(servers[1].url, 'video')
102 102
103 expect(res.body.total).to.equal(2) 103 expect(res.body.total).to.equal(2)
104 expect(res.body.data).to.be.an('array') 104 expect(res.body.data).to.be.an('array')
@@ -125,8 +125,8 @@ describe('Test video blacklist', function () {
125 125
126 it('Should display all the blacklisted videos when applying manual type filter', async function () { 126 it('Should display all the blacklisted videos when applying manual type filter', async function () {
127 const res = await getBlacklistedVideosList({ 127 const res = await getBlacklistedVideosList({
128 url: servers[ 0 ].url, 128 url: servers[0].url,
129 token: servers[ 0 ].accessToken, 129 token: servers[0].accessToken,
130 type: VideoBlacklistType.MANUAL 130 type: VideoBlacklistType.MANUAL
131 }) 131 })
132 132
@@ -139,8 +139,8 @@ describe('Test video blacklist', function () {
139 139
140 it('Should display nothing when applying automatic type filter', async function () { 140 it('Should display nothing when applying automatic type filter', async function () {
141 const res = await getBlacklistedVideosList({ 141 const res = await getBlacklistedVideosList({
142 url: servers[ 0 ].url, 142 url: servers[0].url,
143 token: servers[ 0 ].accessToken, 143 token: servers[0].accessToken,
144 type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED 144 type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED
145 }) 145 })
146 146
@@ -152,7 +152,7 @@ describe('Test video blacklist', function () {
152 }) 152 })
153 153
154 it('Should get the correct sort when sorting by descending id', async function () { 154 it('Should get the correct sort when sorting by descending id', async function () {
155 const res = await getBlacklistedVideosList({ url: servers[ 0 ].url, token: servers[ 0 ].accessToken, sort: '-id' }) 155 const res = await getBlacklistedVideosList({ url: servers[0].url, token: servers[0].accessToken, sort: '-id' })
156 expect(res.body.total).to.equal(2) 156 expect(res.body.total).to.equal(2)
157 157
158 const blacklistedVideos = res.body.data 158 const blacklistedVideos = res.body.data
@@ -165,7 +165,7 @@ describe('Test video blacklist', function () {
165 }) 165 })
166 166
167 it('Should get the correct sort when sorting by descending video name', async function () { 167 it('Should get the correct sort when sorting by descending video name', async function () {
168 const res = await getBlacklistedVideosList({ url: servers[ 0 ].url, token: servers[ 0 ].accessToken, sort: '-name' }) 168 const res = await getBlacklistedVideosList({ url: servers[0].url, token: servers[0].accessToken, sort: '-name' })
169 expect(res.body.total).to.equal(2) 169 expect(res.body.total).to.equal(2)
170 170
171 const blacklistedVideos = res.body.data 171 const blacklistedVideos = res.body.data
@@ -178,7 +178,7 @@ describe('Test video blacklist', function () {
178 }) 178 })
179 179
180 it('Should get the correct sort when sorting by ascending creation date', async function () { 180 it('Should get the correct sort when sorting by ascending creation date', async function () {
181 const res = await getBlacklistedVideosList({ url: servers[ 0 ].url, token: servers[ 0 ].accessToken, sort: 'createdAt' }) 181 const res = await getBlacklistedVideosList({ url: servers[0].url, token: servers[0].accessToken, sort: 'createdAt' })
182 expect(res.body.total).to.equal(2) 182 expect(res.body.total).to.equal(2)
183 183
184 const blacklistedVideos = res.body.data 184 const blacklistedVideos = res.body.data
@@ -195,7 +195,7 @@ describe('Test video blacklist', function () {
195 it('Should change the reason', async function () { 195 it('Should change the reason', async function () {
196 await updateVideoBlacklist(servers[0].url, servers[0].accessToken, videoId, 'my super reason updated') 196 await updateVideoBlacklist(servers[0].url, servers[0].accessToken, videoId, 'my super reason updated')
197 197
198 const res = await getBlacklistedVideosList({ url: servers[ 0 ].url, token: servers[ 0 ].accessToken, sort: '-name' }) 198 const res = await getBlacklistedVideosList({ url: servers[0].url, token: servers[0].accessToken, sort: '-name' })
199 const video = res.body.data.find(b => b.video.id === videoId) 199 const video = res.body.data.find(b => b.video.id === videoId)
200 200
201 expect(video.reason).to.equal('my super reason updated') 201 expect(video.reason).to.equal('my super reason updated')
@@ -231,7 +231,7 @@ describe('Test video blacklist', function () {
231 231
232 it('Should remove a video from the blacklist on server 1', async function () { 232 it('Should remove a video from the blacklist on server 1', async function () {
233 // Get one video in the blacklist 233 // Get one video in the blacklist
234 const res = await getBlacklistedVideosList({ url: servers[ 0 ].url, token: servers[ 0 ].accessToken, sort: '-name' }) 234 const res = await getBlacklistedVideosList({ url: servers[0].url, token: servers[0].accessToken, sort: '-name' })
235 videoToRemove = res.body.data[0] 235 videoToRemove = res.body.data[0]
236 blacklist = res.body.data.slice(1) 236 blacklist = res.body.data.slice(1)
237 237
@@ -252,7 +252,7 @@ describe('Test video blacklist', function () {
252 }) 252 })
253 253
254 it('Should not have the ex-blacklisted video in videos blacklist list on server 1', async function () { 254 it('Should not have the ex-blacklisted video in videos blacklist list on server 1', async function () {
255 const res = await getBlacklistedVideosList({ url: servers[ 0 ].url, token: servers[ 0 ].accessToken, sort: '-name' }) 255 const res = await getBlacklistedVideosList({ url: servers[0].url, token: servers[0].accessToken, sort: '-name' })
256 expect(res.body.total).to.equal(1) 256 expect(res.body.total).to.equal(1)
257 257
258 const videos = res.body.data 258 const videos = res.body.data
@@ -274,7 +274,7 @@ describe('Test video blacklist', function () {
274 video3UUID = res.body.video.uuid 274 video3UUID = res.body.video.uuid
275 } 275 }
276 { 276 {
277 const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'Video 4' }) 277 const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'Video 4' })
278 video4UUID = res.body.video.uuid 278 video4UUID = res.body.video.uuid
279 } 279 }
280 280
@@ -284,17 +284,17 @@ describe('Test video blacklist', function () {
284 it('Should blacklist video 3 and keep it federated', async function () { 284 it('Should blacklist video 3 and keep it federated', async function () {
285 this.timeout(10000) 285 this.timeout(10000)
286 286
287 await addVideoToBlacklist(servers[ 0 ].url, servers[ 0 ].accessToken, video3UUID, 'super reason', false) 287 await addVideoToBlacklist(servers[0].url, servers[0].accessToken, video3UUID, 'super reason', false)
288 288
289 await waitJobs(servers) 289 await waitJobs(servers)
290 290
291 { 291 {
292 const res = await getVideosList(servers[ 0 ].url) 292 const res = await getVideosList(servers[0].url)
293 expect(res.body.data.find(v => v.uuid === video3UUID)).to.be.undefined 293 expect(res.body.data.find(v => v.uuid === video3UUID)).to.be.undefined
294 } 294 }
295 295
296 { 296 {
297 const res = await getVideosList(servers[ 1 ].url) 297 const res = await getVideosList(servers[1].url)
298 expect(res.body.data.find(v => v.uuid === video3UUID)).to.not.be.undefined 298 expect(res.body.data.find(v => v.uuid === video3UUID)).to.not.be.undefined
299 } 299 }
300 }) 300 })
@@ -302,7 +302,7 @@ describe('Test video blacklist', function () {
302 it('Should unfederate the video', async function () { 302 it('Should unfederate the video', async function () {
303 this.timeout(10000) 303 this.timeout(10000)
304 304
305 await addVideoToBlacklist(servers[ 0 ].url, servers[ 0 ].accessToken, video4UUID, 'super reason', true) 305 await addVideoToBlacklist(servers[0].url, servers[0].accessToken, video4UUID, 'super reason', true)
306 306
307 await waitJobs(servers) 307 await waitJobs(servers)
308 308
@@ -315,7 +315,7 @@ describe('Test video blacklist', function () {
315 it('Should have the video unfederated even after an Update AP message', async function () { 315 it('Should have the video unfederated even after an Update AP message', async function () {
316 this.timeout(10000) 316 this.timeout(10000)
317 317
318 await updateVideo(servers[ 0 ].url, servers[ 0 ].accessToken, video4UUID, { description: 'super description' }) 318 await updateVideo(servers[0].url, servers[0].accessToken, video4UUID, { description: 'super description' })
319 319
320 await waitJobs(servers) 320 await waitJobs(servers)
321 321
@@ -326,7 +326,7 @@ describe('Test video blacklist', function () {
326 }) 326 })
327 327
328 it('Should have the correct video blacklist unfederate attribute', async function () { 328 it('Should have the correct video blacklist unfederate attribute', async function () {
329 const res = await getBlacklistedVideosList({ url: servers[ 0 ].url, token: servers[ 0 ].accessToken, sort: 'createdAt' }) 329 const res = await getBlacklistedVideosList({ url: servers[0].url, token: servers[0].accessToken, sort: 'createdAt' })
330 330
331 const blacklistedVideos: VideoBlacklist[] = res.body.data 331 const blacklistedVideos: VideoBlacklist[] = res.body.data
332 const video3Blacklisted = blacklistedVideos.find(b => b.video.uuid === video3UUID) 332 const video3Blacklisted = blacklistedVideos.find(b => b.video.uuid === video3UUID)
@@ -339,7 +339,7 @@ describe('Test video blacklist', function () {
339 it('Should remove the video from blacklist and refederate the video', async function () { 339 it('Should remove the video from blacklist and refederate the video', async function () {
340 this.timeout(10000) 340 this.timeout(10000)
341 341
342 await removeVideoFromBlacklist(servers[ 0 ].url, servers[ 0 ].accessToken, video4UUID) 342 await removeVideoFromBlacklist(servers[0].url, servers[0].accessToken, video4UUID)
343 343
344 await waitJobs(servers) 344 await waitJobs(servers)
345 345
@@ -362,9 +362,9 @@ describe('Test video blacklist', function () {
362 killallServers([ servers[0] ]) 362 killallServers([ servers[0] ])
363 363
364 const config = { 364 const config = {
365 'auto_blacklist': { 365 auto_blacklist: {
366 videos: { 366 videos: {
367 'of_users': { 367 of_users: {
368 enabled: true 368 enabled: true
369 } 369 }
370 } 370 }
@@ -375,8 +375,8 @@ describe('Test video blacklist', function () {
375 { 375 {
376 const user = { username: 'user_without_flag', password: 'password' } 376 const user = { username: 'user_without_flag', password: 'password' }
377 await createUser({ 377 await createUser({
378 url: servers[ 0 ].url, 378 url: servers[0].url,
379 accessToken: servers[ 0 ].accessToken, 379 accessToken: servers[0].accessToken,
380 username: user.username, 380 username: user.username,
381 adminFlags: UserAdminFlag.NONE, 381 adminFlags: UserAdminFlag.NONE,
382 password: user.password, 382 password: user.password,
@@ -393,8 +393,8 @@ describe('Test video blacklist', function () {
393 { 393 {
394 const user = { username: 'user_with_flag', password: 'password' } 394 const user = { username: 'user_with_flag', password: 'password' }
395 await createUser({ 395 await createUser({
396 url: servers[ 0 ].url, 396 url: servers[0].url,
397 accessToken: servers[ 0 ].accessToken, 397 accessToken: servers[0].accessToken,
398 username: user.username, 398 username: user.username,
399 adminFlags: UserAdminFlag.BY_PASS_VIDEO_AUTO_BLACKLIST, 399 adminFlags: UserAdminFlag.BY_PASS_VIDEO_AUTO_BLACKLIST,
400 password: user.password, 400 password: user.password,
@@ -411,8 +411,8 @@ describe('Test video blacklist', function () {
411 await uploadVideo(servers[0].url, userWithoutFlag, { name: 'blacklisted' }) 411 await uploadVideo(servers[0].url, userWithoutFlag, { name: 'blacklisted' })
412 412
413 const res = await getBlacklistedVideosList({ 413 const res = await getBlacklistedVideosList({
414 url: servers[ 0 ].url, 414 url: servers[0].url,
415 token: servers[ 0 ].accessToken, 415 token: servers[0].accessToken,
416 type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED 416 type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED
417 }) 417 })
418 418
@@ -428,11 +428,11 @@ describe('Test video blacklist', function () {
428 name: 'URL import', 428 name: 'URL import',
429 channelId: channelOfUserWithoutFlag 429 channelId: channelOfUserWithoutFlag
430 } 430 }
431 await importVideo(servers[ 0 ].url, userWithoutFlag, attributes) 431 await importVideo(servers[0].url, userWithoutFlag, attributes)
432 432
433 const res = await getBlacklistedVideosList({ 433 const res = await getBlacklistedVideosList({
434 url: servers[ 0 ].url, 434 url: servers[0].url,
435 token: servers[ 0 ].accessToken, 435 token: servers[0].accessToken,
436 sort: 'createdAt', 436 sort: 'createdAt',
437 type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED 437 type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED
438 }) 438 })
@@ -447,11 +447,11 @@ describe('Test video blacklist', function () {
447 name: 'Torrent import', 447 name: 'Torrent import',
448 channelId: channelOfUserWithoutFlag 448 channelId: channelOfUserWithoutFlag
449 } 449 }
450 await importVideo(servers[ 0 ].url, userWithoutFlag, attributes) 450 await importVideo(servers[0].url, userWithoutFlag, attributes)
451 451
452 const res = await getBlacklistedVideosList({ 452 const res = await getBlacklistedVideosList({
453 url: servers[ 0 ].url, 453 url: servers[0].url,
454 token: servers[ 0 ].accessToken, 454 token: servers[0].accessToken,
455 sort: 'createdAt', 455 sort: 'createdAt',
456 type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED 456 type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED
457 }) 457 })
@@ -464,8 +464,8 @@ describe('Test video blacklist', function () {
464 await uploadVideo(servers[0].url, userWithFlag, { name: 'not blacklisted' }) 464 await uploadVideo(servers[0].url, userWithFlag, { name: 'not blacklisted' })
465 465
466 const res = await getBlacklistedVideosList({ 466 const res = await getBlacklistedVideosList({
467 url: servers[ 0 ].url, 467 url: servers[0].url,
468 token: servers[ 0 ].accessToken, 468 token: servers[0].accessToken,
469 type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED 469 type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED
470 }) 470 })
471 471
diff --git a/server/tests/api/videos/video-captions.ts b/server/tests/api/videos/video-captions.ts
index 5e13f5949..b4ecb39f4 100644
--- a/server/tests/api/videos/video-captions.ts
+++ b/server/tests/api/videos/video-captions.ts
@@ -1,16 +1,17 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { 5import {
6 checkVideoFilesWereRemoved, cleanupTests, 6 checkVideoFilesWereRemoved,
7 cleanupTests,
7 doubleFollow, 8 doubleFollow,
8 flushAndRunMultipleServers, 9 flushAndRunMultipleServers,
9 removeVideo, 10 removeVideo,
10 uploadVideo, 11 uploadVideo,
11 wait 12 wait
12} from '../../../../shared/extra-utils' 13} from '../../../../shared/extra-utils'
13import { flushTests, killallServers, ServerInfo, setAccessTokensToServers } from '../../../../shared/extra-utils/index' 14import { ServerInfo, setAccessTokensToServers } from '../../../../shared/extra-utils/index'
14import { waitJobs } from '../../../../shared/extra-utils/server/jobs' 15import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
15import { 16import {
16 createVideoCaption, 17 createVideoCaption,
@@ -36,7 +37,7 @@ describe('Test video captions', function () {
36 37
37 await waitJobs(servers) 38 await waitJobs(servers)
38 39
39 const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'my video name' }) 40 const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'my video name' })
40 videoUUID = res.body.video.uuid 41 videoUUID = res.body.video.uuid
41 42
42 await waitJobs(servers) 43 await waitJobs(servers)
diff --git a/server/tests/api/videos/video-change-ownership.ts b/server/tests/api/videos/video-change-ownership.ts
index 64ee2355a..dee6575b9 100644
--- a/server/tests/api/videos/video-change-ownership.ts
+++ b/server/tests/api/videos/video-change-ownership.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
@@ -38,7 +38,7 @@ describe('Test video change ownership - nominal', function () {
38 } 38 }
39 let firstUserAccessToken = '' 39 let firstUserAccessToken = ''
40 let secondUserAccessToken = '' 40 let secondUserAccessToken = ''
41 let lastRequestChangeOwnershipId = undefined 41 let lastRequestChangeOwnershipId = ''
42 42
43 before(async function () { 43 before(async function () {
44 this.timeout(50000) 44 this.timeout(50000)
@@ -48,15 +48,15 @@ describe('Test video change ownership - nominal', function () {
48 48
49 const videoQuota = 42000000 49 const videoQuota = 42000000
50 await createUser({ 50 await createUser({
51 url: servers[ 0 ].url, 51 url: servers[0].url,
52 accessToken: servers[ 0 ].accessToken, 52 accessToken: servers[0].accessToken,
53 username: firstUser.username, 53 username: firstUser.username,
54 password: firstUser.password, 54 password: firstUser.password,
55 videoQuota: videoQuota 55 videoQuota: videoQuota
56 }) 56 })
57 await createUser({ 57 await createUser({
58 url: servers[ 0 ].url, 58 url: servers[0].url,
59 accessToken: servers[ 0 ].accessToken, 59 accessToken: servers[0].accessToken,
60 username: secondUser.username, 60 username: secondUser.username,
61 password: secondUser.password, 61 password: secondUser.password,
62 videoQuota: videoQuota 62 videoQuota: videoQuota
@@ -209,7 +209,7 @@ describe('Test video change ownership - nominal', function () {
209}) 209})
210 210
211describe('Test video change ownership - quota too small', function () { 211describe('Test video change ownership - quota too small', function () {
212 let server: ServerInfo = undefined 212 let server: ServerInfo
213 const firstUser = { 213 const firstUser = {
214 username: 'first', 214 username: 'first',
215 password: 'My great password' 215 password: 'My great password'
@@ -220,14 +220,14 @@ describe('Test video change ownership - quota too small', function () {
220 } 220 }
221 let firstUserAccessToken = '' 221 let firstUserAccessToken = ''
222 let secondUserAccessToken = '' 222 let secondUserAccessToken = ''
223 let lastRequestChangeOwnershipId = undefined 223 let lastRequestChangeOwnershipId = ''
224 224
225 before(async function () { 225 before(async function () {
226 this.timeout(50000) 226 this.timeout(50000)
227 227
228 // Run one server 228 // Run one server
229 server = await flushAndRunServer(1) 229 server = await flushAndRunServer(1)
230 await setAccessTokensToServers([server]) 230 await setAccessTokensToServers([ server ])
231 231
232 const videoQuota = 42000000 232 const videoQuota = 42000000
233 const limitedVideoQuota = 10 233 const limitedVideoQuota = 10
diff --git a/server/tests/api/videos/video-channels.ts b/server/tests/api/videos/video-channels.ts
index 4f600cae8..876a6ab66 100644
--- a/server/tests/api/videos/video-channels.ts
+++ b/server/tests/api/videos/video-channels.ts
@@ -1,19 +1,21 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { User, Video, VideoChannel, VideoDetails } from '../../../../shared/index' 5import { User, Video, VideoChannel, ViewsPerDate, VideoDetails } from '../../../../shared/index'
6import { 6import {
7 cleanupTests, 7 cleanupTests,
8 createUser, 8 createUser,
9 doubleFollow, 9 doubleFollow,
10 flushAndRunMultipleServers, getVideo, 10 flushAndRunMultipleServers,
11 getVideo,
11 getVideoChannelVideos, 12 getVideoChannelVideos,
12 testImage, 13 testImage,
13 updateVideo, 14 updateVideo,
14 updateVideoChannelAvatar, 15 updateVideoChannelAvatar,
15 uploadVideo, 16 uploadVideo,
16 userLogin 17 userLogin,
18 wait
17} from '../../../../shared/extra-utils' 19} from '../../../../shared/extra-utils'
18import { 20import {
19 addVideoChannel, 21 addVideoChannel,
@@ -24,7 +26,8 @@ import {
24 getVideoChannelsList, 26 getVideoChannelsList,
25 ServerInfo, 27 ServerInfo,
26 setAccessTokensToServers, 28 setAccessTokensToServers,
27 updateVideoChannel 29 updateVideoChannel,
30 viewVideo
28} from '../../../../shared/extra-utils/index' 31} from '../../../../shared/extra-utils/index'
29import { waitJobs } from '../../../../shared/extra-utils/server/jobs' 32import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
30 33
@@ -73,14 +76,14 @@ describe('Test video channels', function () {
73 description: 'super video channel description', 76 description: 'super video channel description',
74 support: 'super video channel support text' 77 support: 'super video channel support text'
75 } 78 }
76 const res = await addVideoChannel(servers[ 0 ].url, servers[ 0 ].accessToken, videoChannel) 79 const res = await addVideoChannel(servers[0].url, servers[0].accessToken, videoChannel)
77 secondVideoChannelId = res.body.videoChannel.id 80 secondVideoChannelId = res.body.videoChannel.id
78 } 81 }
79 82
80 // The channel is 1 is propagated to servers 2 83 // The channel is 1 is propagated to servers 2
81 { 84 {
82 const videoAttributesArg = { name: 'my video name', channelId: secondVideoChannelId, support: 'video support field' } 85 const videoAttributesArg = { name: 'my video name', channelId: secondVideoChannelId, support: 'video support field' }
83 const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, videoAttributesArg) 86 const res = await uploadVideo(servers[0].url, servers[0].accessToken, videoAttributesArg)
84 videoUUID = res.body.video.uuid 87 videoUUID = res.body.video.uuid
85 } 88 }
86 89
@@ -106,7 +109,7 @@ describe('Test video channels', function () {
106 109
107 it('Should have two video channels when getting account channels on server 1', async function () { 110 it('Should have two video channels when getting account channels on server 1', async function () {
108 const res = await getAccountVideoChannelsList({ 111 const res = await getAccountVideoChannelsList({
109 url: servers[ 0 ].url, 112 url: servers[0].url,
110 accountName: userInfo.account.name + '@' + userInfo.account.host 113 accountName: userInfo.account.name + '@' + userInfo.account.host
111 }) 114 })
112 115
@@ -127,7 +130,7 @@ describe('Test video channels', function () {
127 it('Should paginate and sort account channels', async function () { 130 it('Should paginate and sort account channels', async function () {
128 { 131 {
129 const res = await getAccountVideoChannelsList({ 132 const res = await getAccountVideoChannelsList({
130 url: servers[ 0 ].url, 133 url: servers[0].url,
131 accountName: userInfo.account.name + '@' + userInfo.account.host, 134 accountName: userInfo.account.name + '@' + userInfo.account.host,
132 start: 0, 135 start: 0,
133 count: 1, 136 count: 1,
@@ -137,13 +140,13 @@ describe('Test video channels', function () {
137 expect(res.body.total).to.equal(2) 140 expect(res.body.total).to.equal(2)
138 expect(res.body.data).to.have.lengthOf(1) 141 expect(res.body.data).to.have.lengthOf(1)
139 142
140 const videoChannel: VideoChannel = res.body.data[ 0 ] 143 const videoChannel: VideoChannel = res.body.data[0]
141 expect(videoChannel.name).to.equal('root_channel') 144 expect(videoChannel.name).to.equal('root_channel')
142 } 145 }
143 146
144 { 147 {
145 const res = await getAccountVideoChannelsList({ 148 const res = await getAccountVideoChannelsList({
146 url: servers[ 0 ].url, 149 url: servers[0].url,
147 accountName: userInfo.account.name + '@' + userInfo.account.host, 150 accountName: userInfo.account.name + '@' + userInfo.account.host,
148 start: 0, 151 start: 0,
149 count: 1, 152 count: 1,
@@ -153,13 +156,13 @@ describe('Test video channels', function () {
153 expect(res.body.total).to.equal(2) 156 expect(res.body.total).to.equal(2)
154 expect(res.body.data).to.have.lengthOf(1) 157 expect(res.body.data).to.have.lengthOf(1)
155 158
156 const videoChannel: VideoChannel = res.body.data[ 0 ] 159 const videoChannel: VideoChannel = res.body.data[0]
157 expect(videoChannel.name).to.equal('second_video_channel') 160 expect(videoChannel.name).to.equal('second_video_channel')
158 } 161 }
159 162
160 { 163 {
161 const res = await getAccountVideoChannelsList({ 164 const res = await getAccountVideoChannelsList({
162 url: servers[ 0 ].url, 165 url: servers[0].url,
163 accountName: userInfo.account.name + '@' + userInfo.account.host, 166 accountName: userInfo.account.name + '@' + userInfo.account.host,
164 start: 1, 167 start: 1,
165 count: 1, 168 count: 1,
@@ -169,14 +172,14 @@ describe('Test video channels', function () {
169 expect(res.body.total).to.equal(2) 172 expect(res.body.total).to.equal(2)
170 expect(res.body.data).to.have.lengthOf(1) 173 expect(res.body.data).to.have.lengthOf(1)
171 174
172 const videoChannel: VideoChannel = res.body.data[ 0 ] 175 const videoChannel: VideoChannel = res.body.data[0]
173 expect(videoChannel.name).to.equal('root_channel') 176 expect(videoChannel.name).to.equal('root_channel')
174 } 177 }
175 }) 178 })
176 179
177 it('Should have one video channel when getting account channels on server 2', async function () { 180 it('Should have one video channel when getting account channels on server 2', async function () {
178 const res = await getAccountVideoChannelsList({ 181 const res = await getAccountVideoChannelsList({
179 url: servers[ 1 ].url, 182 url: servers[1].url,
180 accountName: userInfo.account.name + '@' + userInfo.account.host 183 accountName: userInfo.account.name + '@' + userInfo.account.host
181 }) 184 })
182 185
@@ -349,19 +352,55 @@ describe('Test video channels', function () {
349 it('Should create the main channel with an uuid if there is a conflict', async function () { 352 it('Should create the main channel with an uuid if there is a conflict', async function () {
350 { 353 {
351 const videoChannel = { name: 'toto_channel', displayName: 'My toto channel' } 354 const videoChannel = { name: 'toto_channel', displayName: 'My toto channel' }
352 await addVideoChannel(servers[ 0 ].url, servers[ 0 ].accessToken, videoChannel) 355 await addVideoChannel(servers[0].url, servers[0].accessToken, videoChannel)
353 } 356 }
354 357
355 { 358 {
356 await createUser({ url: servers[ 0 ].url, accessToken: servers[ 0 ].accessToken, username: 'toto', password: 'password' }) 359 await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: 'toto', password: 'password' })
357 const accessToken = await userLogin(servers[ 0 ], { username: 'toto', password: 'password' }) 360 const accessToken = await userLogin(servers[0], { username: 'toto', password: 'password' })
358 361
359 const res = await getMyUserInformation(servers[ 0 ].url, accessToken) 362 const res = await getMyUserInformation(servers[0].url, accessToken)
360 const videoChannel = res.body.videoChannels[ 0 ] 363 const videoChannel = res.body.videoChannels[0]
361 expect(videoChannel.name).to.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/) 364 expect(videoChannel.name).to.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/)
362 } 365 }
363 }) 366 })
364 367
368 it('Should report correct channel statistics', async function () {
369
370 {
371 const res = await getAccountVideoChannelsList({
372 url: servers[0].url,
373 accountName: userInfo.account.name + '@' + userInfo.account.host,
374 withStats: true
375 })
376 res.body.data.forEach((channel: VideoChannel) => {
377 expect(channel).to.haveOwnProperty('viewsPerDay')
378 expect(channel.viewsPerDay).to.have.length(30 + 1) // daysPrior + today
379 channel.viewsPerDay.forEach((v: ViewsPerDate) => {
380 expect(v.date).to.be.an('string')
381 expect(v.views).to.equal(0)
382 })
383 })
384 }
385
386 {
387 // video has been posted on channel firstVideoChannelId since last update
388 await viewVideo(servers[0].url, videoUUID, 204, '0.0.0.1,127.0.0.1')
389 await viewVideo(servers[0].url, videoUUID, 204, '0.0.0.2,127.0.0.1')
390
391 // Wait the repeatable job
392 await wait(8000)
393
394 const res = await getAccountVideoChannelsList({
395 url: servers[0].url,
396 accountName: userInfo.account.name + '@' + userInfo.account.host,
397 withStats: true
398 })
399 const channelWithView = res.body.data.find((channel: VideoChannel) => channel.id === firstVideoChannelId)
400 expect(channelWithView.viewsPerDay.slice(-1)[0].views).to.equal(2)
401 }
402 })
403
365 after(async function () { 404 after(async function () {
366 await cleanupTests(servers) 405 await cleanupTests(servers)
367 }) 406 })
diff --git a/server/tests/api/videos/video-comments.ts b/server/tests/api/videos/video-comments.ts
index 06e4ff9c8..afb58e95a 100644
--- a/server/tests/api/videos/video-comments.ts
+++ b/server/tests/api/videos/video-comments.ts
@@ -1,17 +1,17 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
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 { cleanupTests, testImage } from '../../../../shared/extra-utils' 6import { cleanupTests, testImage } from '../../../../shared/extra-utils'
7import { 7import {
8 createUser,
8 dateIsValid, 9 dateIsValid,
9 flushAndRunServer, 10 flushAndRunServer,
11 getAccessToken,
10 ServerInfo, 12 ServerInfo,
11 setAccessTokensToServers, 13 setAccessTokensToServers,
12 updateMyAvatar, 14 updateMyAvatar,
13 getAccessToken,
14 createUser,
15 uploadVideo 15 uploadVideo
16} from '../../../../shared/extra-utils/index' 16} from '../../../../shared/extra-utils/index'
17import { 17import {
diff --git a/server/tests/api/videos/video-description.ts b/server/tests/api/videos/video-description.ts
index db4d278bf..b8e98e45f 100644
--- a/server/tests/api/videos/video-description.ts
+++ b/server/tests/api/videos/video-description.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
@@ -8,7 +8,6 @@ import {
8 getVideo, 8 getVideo,
9 getVideoDescription, 9 getVideoDescription,
10 getVideosList, 10 getVideosList,
11 killallServers,
12 ServerInfo, 11 ServerInfo,
13 setAccessTokensToServers, 12 setAccessTokensToServers,
14 updateVideo, 13 updateVideo,
@@ -23,7 +22,7 @@ describe('Test video description', function () {
23 let servers: ServerInfo[] = [] 22 let servers: ServerInfo[] = []
24 let videoUUID = '' 23 let videoUUID = ''
25 let videoId: number 24 let videoId: number
26 let longDescription = 'my super description for server 1'.repeat(50) 25 const longDescription = 'my super description for server 1'.repeat(50)
27 26
28 before(async function () { 27 before(async function () {
29 this.timeout(40000) 28 this.timeout(40000)
@@ -61,7 +60,7 @@ describe('Test video description', function () {
61 60
62 // 30 characters * 6 -> 240 characters 61 // 30 characters * 6 -> 240 characters
63 const truncatedDescription = 'my super description for server 1'.repeat(7) + 62 const truncatedDescription = 'my super description for server 1'.repeat(7) +
64 'my super descrip...' 63 'my super descrip...'
65 64
66 expect(video.description).to.equal(truncatedDescription) 65 expect(video.description).to.equal(truncatedDescription)
67 } 66 }
diff --git a/server/tests/api/videos/video-hls.ts b/server/tests/api/videos/video-hls.ts
index bde3b5656..6555bc8b6 100644
--- a/server/tests/api/videos/video-hls.ts
+++ b/server/tests/api/videos/video-hls.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
@@ -10,13 +10,16 @@ import {
10 doubleFollow, 10 doubleFollow,
11 flushAndRunMultipleServers, 11 flushAndRunMultipleServers,
12 getPlaylist, 12 getPlaylist,
13 getVideo, makeGetRequest, makeRawRequest, 13 getVideo,
14 makeRawRequest,
14 removeVideo, 15 removeVideo,
15 ServerInfo, 16 ServerInfo,
16 setAccessTokensToServers, updateCustomSubConfig, 17 setAccessTokensToServers,
18 updateCustomSubConfig,
17 updateVideo, 19 updateVideo,
18 uploadVideo, 20 uploadVideo,
19 waitJobs, webtorrentAdd 21 waitJobs,
22 webtorrentAdd
20} from '../../../../shared/extra-utils' 23} from '../../../../shared/extra-utils'
21import { VideoDetails } from '../../../../shared/models/videos' 24import { VideoDetails } from '../../../../shared/models/videos'
22import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type' 25import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type'
@@ -48,7 +51,9 @@ async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string, hlsOn
48 51
49 expect(file.magnetUri).to.have.lengthOf.above(2) 52 expect(file.magnetUri).to.have.lengthOf.above(2)
50 expect(file.torrentUrl).to.equal(`${baseUrl}/static/torrents/${videoDetails.uuid}-${file.resolution.id}-hls.torrent`) 53 expect(file.torrentUrl).to.equal(`${baseUrl}/static/torrents/${videoDetails.uuid}-${file.resolution.id}-hls.torrent`)
51 expect(file.fileUrl).to.equal(`${baseUrl}/static/streaming-playlists/hls/${videoDetails.uuid}/${videoDetails.uuid}-${file.resolution.id}-fragmented.mp4`) 54 expect(file.fileUrl).to.equal(
55 `${baseUrl}/static/streaming-playlists/hls/${videoDetails.uuid}/${videoDetails.uuid}-${file.resolution.id}-fragmented.mp4`
56 )
52 expect(file.resolution.label).to.equal(resolution + 'p') 57 expect(file.resolution.label).to.equal(resolution + 'p')
53 58
54 await makeRawRequest(file.torrentUrl, 200) 59 await makeRawRequest(file.torrentUrl, 200)
@@ -66,7 +71,9 @@ async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string, hlsOn
66 const masterPlaylist = res.text 71 const masterPlaylist = res.text
67 72
68 for (const resolution of resolutions) { 73 for (const resolution of resolutions) {
69 const reg = new RegExp('#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution + ',FRAME-RATE=\\d+,CODECS="avc1.64001f,mp4a.40.2"') 74 const reg = new RegExp(
75 '#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution + ',FRAME-RATE=\\d+,CODECS="avc1.64001f,mp4a.40.2"'
76 )
70 77
71 expect(masterPlaylist).to.match(reg) 78 expect(masterPlaylist).to.match(reg)
72 expect(masterPlaylist).to.contain(`${resolution}.m3u8`) 79 expect(masterPlaylist).to.contain(`${resolution}.m3u8`)
@@ -102,7 +109,7 @@ describe('Test HLS videos', function () {
102 it('Should upload a video and transcode it to HLS', async function () { 109 it('Should upload a video and transcode it to HLS', async function () {
103 this.timeout(120000) 110 this.timeout(120000)
104 111
105 const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'video 1', fixture: 'video_short.webm' }) 112 const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video 1', fixture: 'video_short.webm' })
106 videoUUID = res.body.video.uuid 113 videoUUID = res.body.video.uuid
107 114
108 await waitJobs(servers) 115 await waitJobs(servers)
@@ -113,7 +120,7 @@ describe('Test HLS videos', function () {
113 it('Should upload an audio file and transcode it to HLS', async function () { 120 it('Should upload an audio file and transcode it to HLS', async function () {
114 this.timeout(120000) 121 this.timeout(120000)
115 122
116 const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'video audio', fixture: 'sample.ogg' }) 123 const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video audio', fixture: 'sample.ogg' })
117 videoAudioUUID = res.body.video.uuid 124 videoAudioUUID = res.body.video.uuid
118 125
119 await waitJobs(servers) 126 await waitJobs(servers)
@@ -124,7 +131,7 @@ describe('Test HLS videos', function () {
124 it('Should update the video', async function () { 131 it('Should update the video', async function () {
125 this.timeout(10000) 132 this.timeout(10000)
126 133
127 await updateVideo(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID, { name: 'video 1 updated' }) 134 await updateVideo(servers[0].url, servers[0].accessToken, videoUUID, { name: 'video 1 updated' })
128 135
129 await waitJobs(servers) 136 await waitJobs(servers)
130 137
@@ -134,8 +141,8 @@ describe('Test HLS videos', function () {
134 it('Should delete videos', async function () { 141 it('Should delete videos', async function () {
135 this.timeout(10000) 142 this.timeout(10000)
136 143
137 await removeVideo(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID) 144 await removeVideo(servers[0].url, servers[0].accessToken, videoUUID)
138 await removeVideo(servers[ 0 ].url, servers[ 0 ].accessToken, videoAudioUUID) 145 await removeVideo(servers[0].url, servers[0].accessToken, videoAudioUUID)
139 146
140 await waitJobs(servers) 147 await waitJobs(servers)
141 148
diff --git a/server/tests/api/videos/video-imports.ts b/server/tests/api/videos/video-imports.ts
index 1233ed6eb..4d5989f43 100644
--- a/server/tests/api/videos/video-imports.ts
+++ b/server/tests/api/videos/video-imports.ts
@@ -1,8 +1,8 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { VideoDetails, VideoImport, VideoPrivacy } from '../../../../shared/models/videos' 5import { VideoDetails, VideoImport, VideoPrivacy, VideoCaption } from '../../../../shared/models/videos'
6import { 6import {
7 cleanupTests, 7 cleanupTests,
8 doubleFollow, 8 doubleFollow,
@@ -11,13 +11,15 @@ import {
11 getMyVideos, 11 getMyVideos,
12 getVideo, 12 getVideo,
13 getVideosList, 13 getVideosList,
14 listVideoCaptions,
15 testCaptionFile,
14 immutableAssign, 16 immutableAssign,
15 killallServers,
16 ServerInfo, 17 ServerInfo,
17 setAccessTokensToServers 18 setAccessTokensToServers
18} from '../../../../shared/extra-utils' 19} from '../../../../shared/extra-utils'
19import { waitJobs } from '../../../../shared/extra-utils/server/jobs' 20import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
20import { getMagnetURI, getYoutubeVideoUrl, importVideo, getMyVideoImports } from '../../../../shared/extra-utils/videos/video-imports' 21import { getMagnetURI, getMyVideoImports, getYoutubeVideoUrl, importVideo } from '../../../../shared/extra-utils/videos/video-imports'
22import { testImage } from '../../../../shared/extra-utils/miscs/miscs'
21 23
22const expect = chai.expect 24const expect = chai.expect
23 25
@@ -61,11 +63,14 @@ describe('Test video imports', function () {
61 63
62 expect(videoTorrent.name).to.contain('你好 世界 720p.mp4') 64 expect(videoTorrent.name).to.contain('你好 世界 720p.mp4')
63 expect(videoMagnet.name).to.contain('super peertube2 video') 65 expect(videoMagnet.name).to.contain('super peertube2 video')
66
67 const resCaptions = await listVideoCaptions(url, idHttp)
68 expect(resCaptions.body.total).to.equal(2)
64 } 69 }
65 70
66 async function checkVideoServer2 (url: string, id: number | string) { 71 async function checkVideoServer2 (url: string, id: number | string) {
67 const res = await getVideo(url, id) 72 const res = await getVideo(url, id)
68 const video = res.body 73 const video: VideoDetails = res.body
69 74
70 expect(video.name).to.equal('my super name') 75 expect(video.name).to.equal('my super name')
71 expect(video.category.label).to.equal('Entertainment') 76 expect(video.category.label).to.equal('Entertainment')
@@ -76,6 +81,9 @@ describe('Test video imports', function () {
76 expect(video.tags).to.deep.equal([ 'supertag1', 'supertag2' ]) 81 expect(video.tags).to.deep.equal([ 'supertag1', 'supertag2' ])
77 82
78 expect(video.files).to.have.lengthOf(1) 83 expect(video.files).to.have.lengthOf(1)
84
85 const resCaptions = await listVideoCaptions(url, id)
86 expect(resCaptions.body.total).to.equal(2)
79 } 87 }
80 88
81 before(async function () { 89 before(async function () {
@@ -88,12 +96,12 @@ describe('Test video imports', function () {
88 96
89 { 97 {
90 const res = await getMyUserInformation(servers[0].url, servers[0].accessToken) 98 const res = await getMyUserInformation(servers[0].url, servers[0].accessToken)
91 channelIdServer1 = res.body.videoChannels[ 0 ].id 99 channelIdServer1 = res.body.videoChannels[0].id
92 } 100 }
93 101
94 { 102 {
95 const res = await getMyUserInformation(servers[1].url, servers[1].accessToken) 103 const res = await getMyUserInformation(servers[1].url, servers[1].accessToken)
96 channelIdServer2 = res.body.videoChannels[ 0 ].id 104 channelIdServer2 = res.body.videoChannels[0].id
97 } 105 }
98 106
99 await doubleFollow(servers[0], servers[1]) 107 await doubleFollow(servers[0], servers[1])
@@ -111,6 +119,48 @@ describe('Test video imports', function () {
111 const attributes = immutableAssign(baseAttributes, { targetUrl: getYoutubeVideoUrl() }) 119 const attributes = immutableAssign(baseAttributes, { targetUrl: getYoutubeVideoUrl() })
112 const res = await importVideo(servers[0].url, servers[0].accessToken, attributes) 120 const res = await importVideo(servers[0].url, servers[0].accessToken, attributes)
113 expect(res.body.video.name).to.equal('small video - youtube') 121 expect(res.body.video.name).to.equal('small video - youtube')
122 expect(res.body.video.thumbnailPath).to.equal(`/static/thumbnails/${res.body.video.uuid}.jpg`)
123 expect(res.body.video.previewPath).to.equal(`/static/previews/${res.body.video.uuid}.jpg`)
124 await testImage(servers[0].url, 'video_import_thumbnail', res.body.video.thumbnailPath)
125 await testImage(servers[0].url, 'video_import_preview', res.body.video.previewPath)
126
127 const resCaptions = await listVideoCaptions(servers[0].url, res.body.video.id)
128 const videoCaptions: VideoCaption[] = resCaptions.body.data
129 expect(videoCaptions).to.have.lengthOf(2)
130
131 const enCaption = videoCaptions.find(caption => caption.language.id === 'en')
132 expect(enCaption).to.exist
133 expect(enCaption.language.label).to.equal('English')
134 expect(enCaption.captionPath).to.equal(`/static/video-captions/${res.body.video.uuid}-en.vtt`)
135 await testCaptionFile(servers[0].url, enCaption.captionPath, `WEBVTT
136Kind: captions
137Language: en
138
13900:00:01.600 --> 00:00:04.200
140English (US)
141
14200:00:05.900 --> 00:00:07.999
143This is a subtitle in American English
144
14500:00:10.000 --> 00:00:14.000
146Adding subtitles is very easy to do`)
147
148 const frCaption = videoCaptions.find(caption => caption.language.id === 'fr')
149 expect(frCaption).to.exist
150 expect(frCaption.language.label).to.equal('French')
151 expect(frCaption.captionPath).to.equal(`/static/video-captions/${res.body.video.uuid}-fr.vtt`)
152 await testCaptionFile(servers[0].url, frCaption.captionPath, `WEBVTT
153Kind: captions
154Language: fr
155
15600:00:01.600 --> 00:00:04.200
157Français (FR)
158
15900:00:05.900 --> 00:00:07.999
160C'est un sous-titre français
161
16200:00:10.000 --> 00:00:14.000
163Ajouter un sous-titre est vraiment facile`)
114 } 164 }
115 165
116 { 166 {
@@ -214,7 +264,7 @@ describe('Test video imports', function () {
214 264
215 await checkVideoServer2(server.url, res.body.data[0].uuid) 265 await checkVideoServer2(server.url, res.body.data[0].uuid)
216 266
217 const [ ,videoHttp, videoMagnet, videoTorrent ] = res.body.data 267 const [ , videoHttp, videoMagnet, videoTorrent ] = res.body.data
218 await checkVideosServer1(server.url, videoHttp.uuid, videoMagnet.uuid, videoTorrent.uuid) 268 await checkVideosServer1(server.url, videoHttp.uuid, videoMagnet.uuid, videoTorrent.uuid)
219 } 269 }
220 }) 270 })
diff --git a/server/tests/api/videos/video-nsfw.ts b/server/tests/api/videos/video-nsfw.ts
index ad6a4b43f..b16b484b9 100644
--- a/server/tests/api/videos/video-nsfw.ts
+++ b/server/tests/api/videos/video-nsfw.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
@@ -19,12 +19,20 @@ import {
19 updateCustomConfig, 19 updateCustomConfig,
20 updateMyUser 20 updateMyUser
21} from '../../../../shared/extra-utils' 21} from '../../../../shared/extra-utils'
22import { ServerConfig } from '../../../../shared/models' 22import { ServerConfig, VideosOverview } from '../../../../shared/models'
23import { CustomConfig } from '../../../../shared/models/server/custom-config.model' 23import { CustomConfig } from '../../../../shared/models/server/custom-config.model'
24import { User } from '../../../../shared/models/users' 24import { User } from '../../../../shared/models/users'
25import { getVideosOverview, getVideosOverviewWithToken } from '@shared/extra-utils/overviews/overviews'
25 26
26const expect = chai.expect 27const expect = chai.expect
27 28
29function createOverviewRes (res: any) {
30 const overview = res.body as VideosOverview
31
32 const videos = overview.categories[0].videos
33 return { body: { data: videos, total: videos.length } }
34}
35
28describe('Test video NSFW policy', function () { 36describe('Test video NSFW policy', function () {
29 let server: ServerInfo 37 let server: ServerInfo
30 let userAccessToken: string 38 let userAccessToken: string
@@ -36,22 +44,38 @@ describe('Test video NSFW policy', function () {
36 const user: User = res.body 44 const user: User = res.body
37 const videoChannelName = user.videoChannels[0].name 45 const videoChannelName = user.videoChannels[0].name
38 const accountName = user.account.name + '@' + user.account.host 46 const accountName = user.account.name + '@' + user.account.host
47 const hasQuery = Object.keys(query).length !== 0
48 let promises: Promise<any>[]
39 49
40 if (token) { 50 if (token) {
41 return Promise.all([ 51 promises = [
42 getVideosListWithToken(server.url, token, query), 52 getVideosListWithToken(server.url, token, query),
43 searchVideoWithToken(server.url, 'n', token, query), 53 searchVideoWithToken(server.url, 'n', token, query),
44 getAccountVideos(server.url, token, accountName, 0, 5, undefined, query), 54 getAccountVideos(server.url, token, accountName, 0, 5, undefined, query),
45 getVideoChannelVideos(server.url, token, videoChannelName, 0, 5, undefined, query) 55 getVideoChannelVideos(server.url, token, videoChannelName, 0, 5, undefined, query)
46 ]) 56 ]
57
58 // Overviews do not support video filters
59 if (!hasQuery) {
60 promises.push(getVideosOverviewWithToken(server.url, 1, token).then(res => createOverviewRes(res)))
61 }
62
63 return Promise.all(promises)
47 } 64 }
48 65
49 return Promise.all([ 66 promises = [
50 getVideosList(server.url), 67 getVideosList(server.url),
51 searchVideo(server.url, 'n'), 68 searchVideo(server.url, 'n'),
52 getAccountVideos(server.url, undefined, accountName, 0, 5), 69 getAccountVideos(server.url, undefined, accountName, 0, 5),
53 getVideoChannelVideos(server.url, undefined, videoChannelName, 0, 5) 70 getVideoChannelVideos(server.url, undefined, videoChannelName, 0, 5)
54 ]) 71 ]
72
73 // Overviews do not support video filters
74 if (!hasQuery) {
75 promises.push(getVideosOverview(server.url, 1).then(res => createOverviewRes(res)))
76 }
77
78 return Promise.all(promises)
55 }) 79 })
56 } 80 }
57 81
@@ -63,12 +87,12 @@ describe('Test video NSFW policy', function () {
63 await setAccessTokensToServers([ server ]) 87 await setAccessTokensToServers([ server ])
64 88
65 { 89 {
66 const attributes = { name: 'nsfw', nsfw: true } 90 const attributes = { name: 'nsfw', nsfw: true, category: 1 }
67 await uploadVideo(server.url, server.accessToken, attributes) 91 await uploadVideo(server.url, server.accessToken, attributes)
68 } 92 }
69 93
70 { 94 {
71 const attributes = { name: 'normal', nsfw: false } 95 const attributes = { name: 'normal', nsfw: false, category: 1 }
72 await uploadVideo(server.url, server.accessToken, attributes) 96 await uploadVideo(server.url, server.accessToken, attributes)
73 } 97 }
74 98
@@ -89,8 +113,8 @@ describe('Test video NSFW policy', function () {
89 113
90 const videos = res.body.data 114 const videos = res.body.data
91 expect(videos).to.have.lengthOf(2) 115 expect(videos).to.have.lengthOf(2)
92 expect(videos[ 0 ].name).to.equal('normal') 116 expect(videos[0].name).to.equal('normal')
93 expect(videos[ 1 ].name).to.equal('nsfw') 117 expect(videos[1].name).to.equal('nsfw')
94 } 118 }
95 }) 119 })
96 120
@@ -107,7 +131,7 @@ describe('Test video NSFW policy', function () {
107 131
108 const videos = res.body.data 132 const videos = res.body.data
109 expect(videos).to.have.lengthOf(1) 133 expect(videos).to.have.lengthOf(1)
110 expect(videos[ 0 ].name).to.equal('normal') 134 expect(videos[0].name).to.equal('normal')
111 } 135 }
112 }) 136 })
113 137
@@ -124,8 +148,8 @@ describe('Test video NSFW policy', function () {
124 148
125 const videos = res.body.data 149 const videos = res.body.data
126 expect(videos).to.have.lengthOf(2) 150 expect(videos).to.have.lengthOf(2)
127 expect(videos[ 0 ].name).to.equal('normal') 151 expect(videos[0].name).to.equal('normal')
128 expect(videos[ 1 ].name).to.equal('nsfw') 152 expect(videos[1].name).to.equal('nsfw')
129 } 153 }
130 }) 154 })
131 }) 155 })
@@ -154,8 +178,8 @@ describe('Test video NSFW policy', function () {
154 178
155 const videos = res.body.data 179 const videos = res.body.data
156 expect(videos).to.have.lengthOf(2) 180 expect(videos).to.have.lengthOf(2)
157 expect(videos[ 0 ].name).to.equal('normal') 181 expect(videos[0].name).to.equal('normal')
158 expect(videos[ 1 ].name).to.equal('nsfw') 182 expect(videos[1].name).to.equal('nsfw')
159 } 183 }
160 }) 184 })
161 185
@@ -171,8 +195,8 @@ describe('Test video NSFW policy', function () {
171 195
172 const videos = res.body.data 196 const videos = res.body.data
173 expect(videos).to.have.lengthOf(2) 197 expect(videos).to.have.lengthOf(2)
174 expect(videos[ 0 ].name).to.equal('normal') 198 expect(videos[0].name).to.equal('normal')
175 expect(videos[ 1 ].name).to.equal('nsfw') 199 expect(videos[1].name).to.equal('nsfw')
176 } 200 }
177 }) 201 })
178 202
@@ -188,7 +212,7 @@ describe('Test video NSFW policy', function () {
188 212
189 const videos = res.body.data 213 const videos = res.body.data
190 expect(videos).to.have.lengthOf(1) 214 expect(videos).to.have.lengthOf(1)
191 expect(videos[ 0 ].name).to.equal('normal') 215 expect(videos[0].name).to.equal('normal')
192 } 216 }
193 }) 217 })
194 218
@@ -198,8 +222,8 @@ describe('Test video NSFW policy', function () {
198 222
199 const videos = res.body.data 223 const videos = res.body.data
200 expect(videos).to.have.lengthOf(2) 224 expect(videos).to.have.lengthOf(2)
201 expect(videos[ 0 ].name).to.equal('normal') 225 expect(videos[0].name).to.equal('normal')
202 expect(videos[ 1 ].name).to.equal('nsfw') 226 expect(videos[1].name).to.equal('nsfw')
203 }) 227 })
204 228
205 it('Should display NSFW videos when the nsfw param === true', async function () { 229 it('Should display NSFW videos when the nsfw param === true', async function () {
@@ -208,7 +232,7 @@ describe('Test video NSFW policy', function () {
208 232
209 const videos = res.body.data 233 const videos = res.body.data
210 expect(videos).to.have.lengthOf(1) 234 expect(videos).to.have.lengthOf(1)
211 expect(videos[ 0 ].name).to.equal('nsfw') 235 expect(videos[0].name).to.equal('nsfw')
212 } 236 }
213 }) 237 })
214 238
@@ -218,7 +242,7 @@ describe('Test video NSFW policy', function () {
218 242
219 const videos = res.body.data 243 const videos = res.body.data
220 expect(videos).to.have.lengthOf(1) 244 expect(videos).to.have.lengthOf(1)
221 expect(videos[ 0 ].name).to.equal('normal') 245 expect(videos[0].name).to.equal('normal')
222 } 246 }
223 }) 247 })
224 248
@@ -228,8 +252,8 @@ describe('Test video NSFW policy', function () {
228 252
229 const videos = res.body.data 253 const videos = res.body.data
230 expect(videos).to.have.lengthOf(2) 254 expect(videos).to.have.lengthOf(2)
231 expect(videos[ 0 ].name).to.equal('normal') 255 expect(videos[0].name).to.equal('normal')
232 expect(videos[ 1 ].name).to.equal('nsfw') 256 expect(videos[1].name).to.equal('nsfw')
233 } 257 }
234 }) 258 })
235 }) 259 })
diff --git a/server/tests/api/videos/video-playlist-thumbnails.ts b/server/tests/api/videos/video-playlist-thumbnails.ts
index 73ab02c17..a93a0b7de 100644
--- a/server/tests/api/videos/video-playlist-thumbnails.ts
+++ b/server/tests/api/videos/video-playlist-thumbnails.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
@@ -8,14 +8,15 @@ import {
8 createVideoPlaylist, 8 createVideoPlaylist,
9 doubleFollow, 9 doubleFollow,
10 flushAndRunMultipleServers, 10 flushAndRunMultipleServers,
11 getVideoPlaylistsList, removeVideoFromPlaylist, 11 getVideoPlaylistsList,
12 removeVideoFromPlaylist,
13 reorderVideosPlaylist,
12 ServerInfo, 14 ServerInfo,
13 setAccessTokensToServers, 15 setAccessTokensToServers,
14 setDefaultVideoChannel, 16 setDefaultVideoChannel,
15 testImage, 17 testImage,
16 uploadVideoAndGetId, 18 uploadVideoAndGetId,
17 waitJobs, 19 waitJobs
18 reorderVideosPlaylist
19} from '../../../../shared/extra-utils' 20} from '../../../../shared/extra-utils'
20import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' 21import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
21 22
@@ -69,19 +70,19 @@ describe('Playlist thumbnail', function () {
69 this.timeout(30000) 70 this.timeout(30000)
70 71
71 const res = await createVideoPlaylist({ 72 const res = await createVideoPlaylist({
72 url: servers[ 1 ].url, 73 url: servers[1].url,
73 token: servers[ 1 ].accessToken, 74 token: servers[1].accessToken,
74 playlistAttrs: { 75 playlistAttrs: {
75 displayName: 'playlist without thumbnail', 76 displayName: 'playlist without thumbnail',
76 privacy: VideoPlaylistPrivacy.PUBLIC, 77 privacy: VideoPlaylistPrivacy.PUBLIC,
77 videoChannelId: servers[ 1 ].videoChannel.id 78 videoChannelId: servers[1].videoChannel.id
78 } 79 }
79 }) 80 })
80 playlistWithoutThumbnail = res.body.videoPlaylist.id 81 playlistWithoutThumbnail = res.body.videoPlaylist.id
81 82
82 const res2 = await addVideoInPlaylist({ 83 const res2 = await addVideoInPlaylist({
83 url: servers[ 1 ].url, 84 url: servers[1].url,
84 token: servers[ 1 ].accessToken, 85 token: servers[1].accessToken,
85 playlistId: playlistWithoutThumbnail, 86 playlistId: playlistWithoutThumbnail,
86 elementAttrs: { videoId: video1 } 87 elementAttrs: { videoId: video1 }
87 }) 88 })
@@ -99,20 +100,20 @@ describe('Playlist thumbnail', function () {
99 this.timeout(30000) 100 this.timeout(30000)
100 101
101 const res = await createVideoPlaylist({ 102 const res = await createVideoPlaylist({
102 url: servers[ 1 ].url, 103 url: servers[1].url,
103 token: servers[ 1 ].accessToken, 104 token: servers[1].accessToken,
104 playlistAttrs: { 105 playlistAttrs: {
105 displayName: 'playlist with thumbnail', 106 displayName: 'playlist with thumbnail',
106 privacy: VideoPlaylistPrivacy.PUBLIC, 107 privacy: VideoPlaylistPrivacy.PUBLIC,
107 videoChannelId: servers[ 1 ].videoChannel.id, 108 videoChannelId: servers[1].videoChannel.id,
108 thumbnailfile: 'thumbnail.jpg' 109 thumbnailfile: 'thumbnail.jpg'
109 } 110 }
110 }) 111 })
111 playlistWithThumbnail = res.body.videoPlaylist.id 112 playlistWithThumbnail = res.body.videoPlaylist.id
112 113
113 const res2 = await addVideoInPlaylist({ 114 const res2 = await addVideoInPlaylist({
114 url: servers[ 1 ].url, 115 url: servers[1].url,
115 token: servers[ 1 ].accessToken, 116 token: servers[1].accessToken,
116 playlistId: playlistWithThumbnail, 117 playlistId: playlistWithThumbnail,
117 elementAttrs: { videoId: video1 } 118 elementAttrs: { videoId: video1 }
118 }) 119 })
@@ -130,8 +131,8 @@ describe('Playlist thumbnail', function () {
130 this.timeout(30000) 131 this.timeout(30000)
131 132
132 const res = await addVideoInPlaylist({ 133 const res = await addVideoInPlaylist({
133 url: servers[ 1 ].url, 134 url: servers[1].url,
134 token: servers[ 1 ].accessToken, 135 token: servers[1].accessToken,
135 playlistId: playlistWithoutThumbnail, 136 playlistId: playlistWithoutThumbnail,
136 elementAttrs: { videoId: video2 } 137 elementAttrs: { videoId: video2 }
137 }) 138 })
@@ -159,8 +160,8 @@ describe('Playlist thumbnail', function () {
159 this.timeout(30000) 160 this.timeout(30000)
160 161
161 const res = await addVideoInPlaylist({ 162 const res = await addVideoInPlaylist({
162 url: servers[ 1 ].url, 163 url: servers[1].url,
163 token: servers[ 1 ].accessToken, 164 token: servers[1].accessToken,
164 playlistId: playlistWithThumbnail, 165 playlistId: playlistWithThumbnail,
165 elementAttrs: { videoId: video2 } 166 elementAttrs: { videoId: video2 }
166 }) 167 })
@@ -188,8 +189,8 @@ describe('Playlist thumbnail', function () {
188 this.timeout(30000) 189 this.timeout(30000)
189 190
190 await removeVideoFromPlaylist({ 191 await removeVideoFromPlaylist({
191 url: servers[ 1 ].url, 192 url: servers[1].url,
192 token: servers[ 1 ].accessToken, 193 token: servers[1].accessToken,
193 playlistId: playlistWithoutThumbnail, 194 playlistId: playlistWithoutThumbnail,
194 playlistElementId: withoutThumbnailE1 195 playlistElementId: withoutThumbnailE1
195 }) 196 })
@@ -206,8 +207,8 @@ describe('Playlist thumbnail', function () {
206 this.timeout(30000) 207 this.timeout(30000)
207 208
208 await removeVideoFromPlaylist({ 209 await removeVideoFromPlaylist({
209 url: servers[ 1 ].url, 210 url: servers[1].url,
210 token: servers[ 1 ].accessToken, 211 token: servers[1].accessToken,
211 playlistId: playlistWithThumbnail, 212 playlistId: playlistWithThumbnail,
212 playlistElementId: withThumbnailE1 213 playlistElementId: withThumbnailE1
213 }) 214 })
@@ -224,8 +225,8 @@ describe('Playlist thumbnail', function () {
224 this.timeout(30000) 225 this.timeout(30000)
225 226
226 await removeVideoFromPlaylist({ 227 await removeVideoFromPlaylist({
227 url: servers[ 1 ].url, 228 url: servers[1].url,
228 token: servers[ 1 ].accessToken, 229 token: servers[1].accessToken,
229 playlistId: playlistWithoutThumbnail, 230 playlistId: playlistWithoutThumbnail,
230 playlistElementId: withoutThumbnailE2 231 playlistElementId: withoutThumbnailE2
231 }) 232 })
@@ -242,8 +243,8 @@ describe('Playlist thumbnail', function () {
242 this.timeout(30000) 243 this.timeout(30000)
243 244
244 await removeVideoFromPlaylist({ 245 await removeVideoFromPlaylist({
245 url: servers[ 1 ].url, 246 url: servers[1].url,
246 token: servers[ 1 ].accessToken, 247 token: servers[1].accessToken,
247 playlistId: playlistWithThumbnail, 248 playlistId: playlistWithThumbnail,
248 playlistElementId: withThumbnailE2 249 playlistElementId: withThumbnailE2
249 }) 250 })
diff --git a/server/tests/api/videos/video-playlists.ts b/server/tests/api/videos/video-playlists.ts
index 9fd48ac7c..2bb97d7a8 100644
--- a/server/tests/api/videos/video-playlists.ts
+++ b/server/tests/api/videos/video-playlists.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
@@ -141,12 +141,12 @@ describe('Test video playlists', function () {
141 servers[2].videos = await Promise.all(serverPromises[2]) 141 servers[2].videos = await Promise.all(serverPromises[2])
142 } 142 }
143 143
144 nsfwVideoServer1 = (await uploadVideoAndGetId({ server: servers[ 0 ], videoName: 'NSFW video', nsfw: true })).id 144 nsfwVideoServer1 = (await uploadVideoAndGetId({ server: servers[0], videoName: 'NSFW video', nsfw: true })).id
145 145
146 { 146 {
147 await createUser({ 147 await createUser({
148 url: servers[ 0 ].url, 148 url: servers[0].url,
149 accessToken: servers[ 0 ].accessToken, 149 accessToken: servers[0].accessToken,
150 username: 'user1', 150 username: 'user1',
151 password: 'password' 151 password: 'password'
152 }) 152 })
@@ -158,17 +158,17 @@ describe('Test video playlists', function () {
158 158
159 describe('Get default playlists', function () { 159 describe('Get default playlists', function () {
160 it('Should list video playlist privacies', async function () { 160 it('Should list video playlist privacies', async function () {
161 const res = await getVideoPlaylistPrivacies(servers[ 0 ].url) 161 const res = await getVideoPlaylistPrivacies(servers[0].url)
162 162
163 const privacies = res.body 163 const privacies = res.body
164 expect(Object.keys(privacies)).to.have.length.at.least(3) 164 expect(Object.keys(privacies)).to.have.length.at.least(3)
165 165
166 expect(privacies[ 3 ]).to.equal('Private') 166 expect(privacies[3]).to.equal('Private')
167 }) 167 })
168 168
169 it('Should list watch later playlist', async function () { 169 it('Should list watch later playlist', async function () {
170 const url = servers[ 0 ].url 170 const url = servers[0].url
171 const accessToken = servers[ 0 ].accessToken 171 const accessToken = servers[0].accessToken
172 172
173 { 173 {
174 const res = await getAccountPlaylistsListWithToken(url, accessToken, 'root', 0, 5, VideoPlaylistType.WATCH_LATER) 174 const res = await getAccountPlaylistsListWithToken(url, accessToken, 'root', 0, 5, VideoPlaylistType.WATCH_LATER)
@@ -176,7 +176,7 @@ describe('Test video playlists', function () {
176 expect(res.body.total).to.equal(1) 176 expect(res.body.total).to.equal(1)
177 expect(res.body.data).to.have.lengthOf(1) 177 expect(res.body.data).to.have.lengthOf(1)
178 178
179 const playlist: VideoPlaylist = res.body.data[ 0 ] 179 const playlist: VideoPlaylist = res.body.data[0]
180 expect(playlist.displayName).to.equal('Watch later') 180 expect(playlist.displayName).to.equal('Watch later')
181 expect(playlist.type.id).to.equal(VideoPlaylistType.WATCH_LATER) 181 expect(playlist.type.id).to.equal(VideoPlaylistType.WATCH_LATER)
182 expect(playlist.type.label).to.equal('Watch later') 182 expect(playlist.type.label).to.equal('Watch later')
@@ -197,15 +197,15 @@ describe('Test video playlists', function () {
197 }) 197 })
198 198
199 it('Should get private playlist for a classic user', async function () { 199 it('Should get private playlist for a classic user', async function () {
200 const token = await generateUserAccessToken(servers[ 0 ], 'toto') 200 const token = await generateUserAccessToken(servers[0], 'toto')
201 201
202 const res = await getAccountPlaylistsListWithToken(servers[ 0 ].url, token, 'toto', 0, 5) 202 const res = await getAccountPlaylistsListWithToken(servers[0].url, token, 'toto', 0, 5)
203 203
204 expect(res.body.total).to.equal(1) 204 expect(res.body.total).to.equal(1)
205 expect(res.body.data).to.have.lengthOf(1) 205 expect(res.body.data).to.have.lengthOf(1)
206 206
207 const playlistId = res.body.data[ 0 ].id 207 const playlistId = res.body.data[0].id
208 await getPlaylistVideos(servers[ 0 ].url, token, playlistId, 0, 5) 208 await getPlaylistVideos(servers[0].url, token, playlistId, 0, 5)
209 }) 209 })
210 }) 210 })
211 211
@@ -215,14 +215,14 @@ describe('Test video playlists', function () {
215 this.timeout(30000) 215 this.timeout(30000)
216 216
217 await createVideoPlaylist({ 217 await createVideoPlaylist({
218 url: servers[ 0 ].url, 218 url: servers[0].url,
219 token: servers[ 0 ].accessToken, 219 token: servers[0].accessToken,
220 playlistAttrs: { 220 playlistAttrs: {
221 displayName: 'my super playlist', 221 displayName: 'my super playlist',
222 privacy: VideoPlaylistPrivacy.PUBLIC, 222 privacy: VideoPlaylistPrivacy.PUBLIC,
223 description: 'my super description', 223 description: 'my super description',
224 thumbnailfile: 'thumbnail.jpg', 224 thumbnailfile: 'thumbnail.jpg',
225 videoChannelId: servers[ 0 ].videoChannel.id 225 videoChannelId: servers[0].videoChannel.id
226 } 226 }
227 }) 227 })
228 228
@@ -233,7 +233,7 @@ describe('Test video playlists', function () {
233 expect(res.body.total).to.equal(1) 233 expect(res.body.total).to.equal(1)
234 expect(res.body.data).to.have.lengthOf(1) 234 expect(res.body.data).to.have.lengthOf(1)
235 235
236 const playlistFromList = res.body.data[ 0 ] as VideoPlaylist 236 const playlistFromList = res.body.data[0] as VideoPlaylist
237 237
238 const res2 = await getVideoPlaylist(server.url, playlistFromList.uuid) 238 const res2 = await getVideoPlaylist(server.url, playlistFromList.uuid)
239 const playlistFromGet = res2.body 239 const playlistFromGet = res2.body
@@ -266,12 +266,12 @@ describe('Test video playlists', function () {
266 266
267 { 267 {
268 const res = await createVideoPlaylist({ 268 const res = await createVideoPlaylist({
269 url: servers[ 1 ].url, 269 url: servers[1].url,
270 token: servers[ 1 ].accessToken, 270 token: servers[1].accessToken,
271 playlistAttrs: { 271 playlistAttrs: {
272 displayName: 'playlist 2', 272 displayName: 'playlist 2',
273 privacy: VideoPlaylistPrivacy.PUBLIC, 273 privacy: VideoPlaylistPrivacy.PUBLIC,
274 videoChannelId: servers[ 1 ].videoChannel.id 274 videoChannelId: servers[1].videoChannel.id
275 } 275 }
276 }) 276 })
277 playlistServer2Id1 = res.body.videoPlaylist.id 277 playlistServer2Id1 = res.body.videoPlaylist.id
@@ -279,13 +279,13 @@ describe('Test video playlists', function () {
279 279
280 { 280 {
281 const res = await createVideoPlaylist({ 281 const res = await createVideoPlaylist({
282 url: servers[ 1 ].url, 282 url: servers[1].url,
283 token: servers[ 1 ].accessToken, 283 token: servers[1].accessToken,
284 playlistAttrs: { 284 playlistAttrs: {
285 displayName: 'playlist 3', 285 displayName: 'playlist 3',
286 privacy: VideoPlaylistPrivacy.PUBLIC, 286 privacy: VideoPlaylistPrivacy.PUBLIC,
287 thumbnailfile: 'thumbnail.jpg', 287 thumbnailfile: 'thumbnail.jpg',
288 videoChannelId: servers[ 1 ].videoChannel.id 288 videoChannelId: servers[1].videoChannel.id
289 } 289 }
290 }) 290 })
291 291
@@ -293,24 +293,24 @@ describe('Test video playlists', function () {
293 playlistServer2UUID2 = res.body.videoPlaylist.uuid 293 playlistServer2UUID2 = res.body.videoPlaylist.uuid
294 } 294 }
295 295
296 for (let id of [ playlistServer2Id1, playlistServer2Id2 ]) { 296 for (const id of [ playlistServer2Id1, playlistServer2Id2 ]) {
297 await addVideoInPlaylist({ 297 await addVideoInPlaylist({
298 url: servers[ 1 ].url, 298 url: servers[1].url,
299 token: servers[ 1 ].accessToken, 299 token: servers[1].accessToken,
300 playlistId: id, 300 playlistId: id,
301 elementAttrs: { videoId: servers[ 1 ].videos[ 0 ].id, startTimestamp: 1, stopTimestamp: 2 } 301 elementAttrs: { videoId: servers[1].videos[0].id, startTimestamp: 1, stopTimestamp: 2 }
302 }) 302 })
303 await addVideoInPlaylist({ 303 await addVideoInPlaylist({
304 url: servers[ 1 ].url, 304 url: servers[1].url,
305 token: servers[ 1 ].accessToken, 305 token: servers[1].accessToken,
306 playlistId: id, 306 playlistId: id,
307 elementAttrs: { videoId: servers[ 1 ].videos[ 1 ].id } 307 elementAttrs: { videoId: servers[1].videos[1].id }
308 }) 308 })
309 } 309 }
310 310
311 await waitJobs(servers) 311 await waitJobs(servers)
312 312
313 for (const server of [ servers[ 0 ], servers[ 1 ] ]) { 313 for (const server of [ servers[0], servers[1] ]) {
314 const res = await getVideoPlaylistsList(server.url, 0, 5) 314 const res = await getVideoPlaylistsList(server.url, 0, 5)
315 315
316 const playlist2 = res.body.data.find(p => p.displayName === 'playlist 2') 316 const playlist2 = res.body.data.find(p => p.displayName === 'playlist 2')
@@ -322,7 +322,7 @@ describe('Test video playlists', function () {
322 await testImage(server.url, 'thumbnail', playlist3.thumbnailPath) 322 await testImage(server.url, 'thumbnail', playlist3.thumbnailPath)
323 } 323 }
324 324
325 const res = await getVideoPlaylistsList(servers[ 2 ].url, 0, 5) 325 const res = await getVideoPlaylistsList(servers[2].url, 0, 5)
326 expect(res.body.data.find(p => p.displayName === 'playlist 2')).to.be.undefined 326 expect(res.body.data.find(p => p.displayName === 'playlist 2')).to.be.undefined
327 expect(res.body.data.find(p => p.displayName === 'playlist 3')).to.be.undefined 327 expect(res.body.data.find(p => p.displayName === 'playlist 3')).to.be.undefined
328 }) 328 })
@@ -331,13 +331,13 @@ describe('Test video playlists', function () {
331 this.timeout(30000) 331 this.timeout(30000)
332 332
333 // Server 2 and server 3 follow each other 333 // Server 2 and server 3 follow each other
334 await doubleFollow(servers[ 1 ], servers[ 2 ]) 334 await doubleFollow(servers[1], servers[2])
335 335
336 const res = await getVideoPlaylistsList(servers[ 2 ].url, 0, 5) 336 const res = await getVideoPlaylistsList(servers[2].url, 0, 5)
337 337
338 const playlist2 = res.body.data.find(p => p.displayName === 'playlist 2') 338 const playlist2 = res.body.data.find(p => p.displayName === 'playlist 2')
339 expect(playlist2).to.not.be.undefined 339 expect(playlist2).to.not.be.undefined
340 await testImage(servers[ 2 ].url, 'thumbnail-playlist', playlist2.thumbnailPath) 340 await testImage(servers[2].url, 'thumbnail-playlist', playlist2.thumbnailPath)
341 341
342 expect(res.body.data.find(p => p.displayName === 'playlist 3')).to.not.be.undefined 342 expect(res.body.data.find(p => p.displayName === 'playlist 3')).to.not.be.undefined
343 }) 343 })
@@ -349,25 +349,25 @@ describe('Test video playlists', function () {
349 this.timeout(30000) 349 this.timeout(30000)
350 350
351 { 351 {
352 const res = await getVideoPlaylistsList(servers[ 2 ].url, 1, 2, 'createdAt') 352 const res = await getVideoPlaylistsList(servers[2].url, 1, 2, 'createdAt')
353 353
354 expect(res.body.total).to.equal(3) 354 expect(res.body.total).to.equal(3)
355 355
356 const data: VideoPlaylist[] = res.body.data 356 const data: VideoPlaylist[] = res.body.data
357 expect(data).to.have.lengthOf(2) 357 expect(data).to.have.lengthOf(2)
358 expect(data[ 0 ].displayName).to.equal('playlist 2') 358 expect(data[0].displayName).to.equal('playlist 2')
359 expect(data[ 1 ].displayName).to.equal('playlist 3') 359 expect(data[1].displayName).to.equal('playlist 3')
360 } 360 }
361 361
362 { 362 {
363 const res = await getVideoPlaylistsList(servers[ 2 ].url, 1, 2, '-createdAt') 363 const res = await getVideoPlaylistsList(servers[2].url, 1, 2, '-createdAt')
364 364
365 expect(res.body.total).to.equal(3) 365 expect(res.body.total).to.equal(3)
366 366
367 const data: VideoPlaylist[] = res.body.data 367 const data: VideoPlaylist[] = res.body.data
368 expect(data).to.have.lengthOf(2) 368 expect(data).to.have.lengthOf(2)
369 expect(data[ 0 ].displayName).to.equal('playlist 2') 369 expect(data[0].displayName).to.equal('playlist 2')
370 expect(data[ 1 ].displayName).to.equal('my super playlist') 370 expect(data[1].displayName).to.equal('my super playlist')
371 } 371 }
372 }) 372 })
373 373
@@ -375,13 +375,13 @@ describe('Test video playlists', function () {
375 this.timeout(30000) 375 this.timeout(30000)
376 376
377 { 377 {
378 const res = await getVideoChannelPlaylistsList(servers[ 0 ].url, 'root_channel', 0, 2, '-createdAt') 378 const res = await getVideoChannelPlaylistsList(servers[0].url, 'root_channel', 0, 2, '-createdAt')
379 379
380 expect(res.body.total).to.equal(1) 380 expect(res.body.total).to.equal(1)
381 381
382 const data: VideoPlaylist[] = res.body.data 382 const data: VideoPlaylist[] = res.body.data
383 expect(data).to.have.lengthOf(1) 383 expect(data).to.have.lengthOf(1)
384 expect(data[ 0 ].displayName).to.equal('my super playlist') 384 expect(data[0].displayName).to.equal('my super playlist')
385 } 385 }
386 }) 386 })
387 387
@@ -389,37 +389,37 @@ describe('Test video playlists', function () {
389 this.timeout(30000) 389 this.timeout(30000)
390 390
391 { 391 {
392 const res = await getAccountPlaylistsList(servers[ 1 ].url, 'root', 1, 2, '-createdAt') 392 const res = await getAccountPlaylistsList(servers[1].url, 'root', 1, 2, '-createdAt')
393 393
394 expect(res.body.total).to.equal(2) 394 expect(res.body.total).to.equal(2)
395 395
396 const data: VideoPlaylist[] = res.body.data 396 const data: VideoPlaylist[] = res.body.data
397 expect(data).to.have.lengthOf(1) 397 expect(data).to.have.lengthOf(1)
398 expect(data[ 0 ].displayName).to.equal('playlist 2') 398 expect(data[0].displayName).to.equal('playlist 2')
399 } 399 }
400 400
401 { 401 {
402 const res = await getAccountPlaylistsList(servers[ 1 ].url, 'root', 1, 2, 'createdAt') 402 const res = await getAccountPlaylistsList(servers[1].url, 'root', 1, 2, 'createdAt')
403 403
404 expect(res.body.total).to.equal(2) 404 expect(res.body.total).to.equal(2)
405 405
406 const data: VideoPlaylist[] = res.body.data 406 const data: VideoPlaylist[] = res.body.data
407 expect(data).to.have.lengthOf(1) 407 expect(data).to.have.lengthOf(1)
408 expect(data[ 0 ].displayName).to.equal('playlist 3') 408 expect(data[0].displayName).to.equal('playlist 3')
409 } 409 }
410 410
411 { 411 {
412 const res = await getAccountPlaylistsList(servers[ 1 ].url, 'root', 0, 10, 'createdAt', '3') 412 const res = await getAccountPlaylistsList(servers[1].url, 'root', 0, 10, 'createdAt', '3')
413 413
414 expect(res.body.total).to.equal(1) 414 expect(res.body.total).to.equal(1)
415 415
416 const data: VideoPlaylist[] = res.body.data 416 const data: VideoPlaylist[] = res.body.data
417 expect(data).to.have.lengthOf(1) 417 expect(data).to.have.lengthOf(1)
418 expect(data[ 0 ].displayName).to.equal('playlist 3') 418 expect(data[0].displayName).to.equal('playlist 3')
419 } 419 }
420 420
421 { 421 {
422 const res = await getAccountPlaylistsList(servers[ 1 ].url, 'root', 0, 10, 'createdAt', '4') 422 const res = await getAccountPlaylistsList(servers[1].url, 'root', 0, 10, 'createdAt', '4')
423 423
424 expect(res.body.total).to.equal(0) 424 expect(res.body.total).to.equal(0)
425 425
@@ -432,8 +432,8 @@ describe('Test video playlists', function () {
432 this.timeout(30000) 432 this.timeout(30000)
433 433
434 await createVideoPlaylist({ 434 await createVideoPlaylist({
435 url: servers[ 1 ].url, 435 url: servers[1].url,
436 token: servers[ 1 ].accessToken, 436 token: servers[1].accessToken,
437 playlistAttrs: { 437 playlistAttrs: {
438 displayName: 'playlist unlisted', 438 displayName: 'playlist unlisted',
439 privacy: VideoPlaylistPrivacy.UNLISTED 439 privacy: VideoPlaylistPrivacy.UNLISTED
@@ -441,8 +441,8 @@ describe('Test video playlists', function () {
441 }) 441 })
442 442
443 await createVideoPlaylist({ 443 await createVideoPlaylist({
444 url: servers[ 1 ].url, 444 url: servers[1].url,
445 token: servers[ 1 ].accessToken, 445 token: servers[1].accessToken,
446 playlistAttrs: { 446 playlistAttrs: {
447 displayName: 'playlist private', 447 displayName: 'playlist private',
448 privacy: VideoPlaylistPrivacy.PRIVATE 448 privacy: VideoPlaylistPrivacy.PRIVATE
@@ -453,18 +453,18 @@ describe('Test video playlists', function () {
453 453
454 for (const server of servers) { 454 for (const server of servers) {
455 const results = [ 455 const results = [
456 await getAccountPlaylistsList(server.url, 'root@localhost:' + servers[ 1 ].port, 0, 5, '-createdAt'), 456 await getAccountPlaylistsList(server.url, 'root@localhost:' + servers[1].port, 0, 5, '-createdAt'),
457 await getVideoPlaylistsList(server.url, 0, 2, '-createdAt') 457 await getVideoPlaylistsList(server.url, 0, 2, '-createdAt')
458 ] 458 ]
459 459
460 expect(results[ 0 ].body.total).to.equal(2) 460 expect(results[0].body.total).to.equal(2)
461 expect(results[ 1 ].body.total).to.equal(3) 461 expect(results[1].body.total).to.equal(3)
462 462
463 for (const res of results) { 463 for (const res of results) {
464 const data: VideoPlaylist[] = res.body.data 464 const data: VideoPlaylist[] = res.body.data
465 expect(data).to.have.lengthOf(2) 465 expect(data).to.have.lengthOf(2)
466 expect(data[ 0 ].displayName).to.equal('playlist 3') 466 expect(data[0].displayName).to.equal('playlist 3')
467 expect(data[ 1 ].displayName).to.equal('playlist 2') 467 expect(data[1].displayName).to.equal('playlist 2')
468 } 468 }
469 } 469 }
470 }) 470 })
@@ -519,32 +519,32 @@ describe('Test video playlists', function () {
519 this.timeout(30000) 519 this.timeout(30000)
520 520
521 const addVideo = (elementAttrs: any) => { 521 const addVideo = (elementAttrs: any) => {
522 return addVideoInPlaylist({ url: servers[ 0 ].url, token: servers[ 0 ].accessToken, playlistId: playlistServer1Id, elementAttrs }) 522 return addVideoInPlaylist({ url: servers[0].url, token: servers[0].accessToken, playlistId: playlistServer1Id, elementAttrs })
523 } 523 }
524 524
525 const res = await createVideoPlaylist({ 525 const res = await createVideoPlaylist({
526 url: servers[ 0 ].url, 526 url: servers[0].url,
527 token: servers[ 0 ].accessToken, 527 token: servers[0].accessToken,
528 playlistAttrs: { 528 playlistAttrs: {
529 displayName: 'playlist 4', 529 displayName: 'playlist 4',
530 privacy: VideoPlaylistPrivacy.PUBLIC, 530 privacy: VideoPlaylistPrivacy.PUBLIC,
531 videoChannelId: servers[ 0 ].videoChannel.id 531 videoChannelId: servers[0].videoChannel.id
532 } 532 }
533 }) 533 })
534 534
535 playlistServer1Id = res.body.videoPlaylist.id 535 playlistServer1Id = res.body.videoPlaylist.id
536 playlistServer1UUID = res.body.videoPlaylist.uuid 536 playlistServer1UUID = res.body.videoPlaylist.uuid
537 537
538 await addVideo({ videoId: servers[ 0 ].videos[ 0 ].uuid, startTimestamp: 15, stopTimestamp: 28 }) 538 await addVideo({ videoId: servers[0].videos[0].uuid, startTimestamp: 15, stopTimestamp: 28 })
539 await addVideo({ videoId: servers[ 2 ].videos[ 1 ].uuid, startTimestamp: 35 }) 539 await addVideo({ videoId: servers[2].videos[1].uuid, startTimestamp: 35 })
540 await addVideo({ videoId: servers[ 2 ].videos[ 2 ].uuid }) 540 await addVideo({ videoId: servers[2].videos[2].uuid })
541 { 541 {
542 const res = await addVideo({ videoId: servers[ 0 ].videos[ 3 ].uuid, stopTimestamp: 35 }) 542 const res = await addVideo({ videoId: servers[0].videos[3].uuid, stopTimestamp: 35 })
543 playlistElementServer1Video4 = res.body.videoPlaylistElement.id 543 playlistElementServer1Video4 = res.body.videoPlaylistElement.id
544 } 544 }
545 545
546 { 546 {
547 const res = await addVideo({ videoId: servers[ 0 ].videos[ 4 ].uuid, startTimestamp: 45, stopTimestamp: 60 }) 547 const res = await addVideo({ videoId: servers[0].videos[4].uuid, startTimestamp: 45, stopTimestamp: 60 })
548 playlistElementServer1Video5 = res.body.videoPlaylistElement.id 548 playlistElementServer1Video5 = res.body.videoPlaylistElement.id
549 } 549 }
550 550
@@ -567,35 +567,35 @@ describe('Test video playlists', function () {
567 const videoElements: VideoPlaylistElement[] = res.body.data 567 const videoElements: VideoPlaylistElement[] = res.body.data
568 expect(videoElements).to.have.lengthOf(6) 568 expect(videoElements).to.have.lengthOf(6)
569 569
570 expect(videoElements[ 0 ].video.name).to.equal('video 0 server 1') 570 expect(videoElements[0].video.name).to.equal('video 0 server 1')
571 expect(videoElements[ 0 ].position).to.equal(1) 571 expect(videoElements[0].position).to.equal(1)
572 expect(videoElements[ 0 ].startTimestamp).to.equal(15) 572 expect(videoElements[0].startTimestamp).to.equal(15)
573 expect(videoElements[ 0 ].stopTimestamp).to.equal(28) 573 expect(videoElements[0].stopTimestamp).to.equal(28)
574 574
575 expect(videoElements[ 1 ].video.name).to.equal('video 1 server 3') 575 expect(videoElements[1].video.name).to.equal('video 1 server 3')
576 expect(videoElements[ 1 ].position).to.equal(2) 576 expect(videoElements[1].position).to.equal(2)
577 expect(videoElements[ 1 ].startTimestamp).to.equal(35) 577 expect(videoElements[1].startTimestamp).to.equal(35)
578 expect(videoElements[ 1 ].stopTimestamp).to.be.null 578 expect(videoElements[1].stopTimestamp).to.be.null
579 579
580 expect(videoElements[ 2 ].video.name).to.equal('video 2 server 3') 580 expect(videoElements[2].video.name).to.equal('video 2 server 3')
581 expect(videoElements[ 2 ].position).to.equal(3) 581 expect(videoElements[2].position).to.equal(3)
582 expect(videoElements[ 2 ].startTimestamp).to.be.null 582 expect(videoElements[2].startTimestamp).to.be.null
583 expect(videoElements[ 2 ].stopTimestamp).to.be.null 583 expect(videoElements[2].stopTimestamp).to.be.null
584 584
585 expect(videoElements[ 3 ].video.name).to.equal('video 3 server 1') 585 expect(videoElements[3].video.name).to.equal('video 3 server 1')
586 expect(videoElements[ 3 ].position).to.equal(4) 586 expect(videoElements[3].position).to.equal(4)
587 expect(videoElements[ 3 ].startTimestamp).to.be.null 587 expect(videoElements[3].startTimestamp).to.be.null
588 expect(videoElements[ 3 ].stopTimestamp).to.equal(35) 588 expect(videoElements[3].stopTimestamp).to.equal(35)
589 589
590 expect(videoElements[ 4 ].video.name).to.equal('video 4 server 1') 590 expect(videoElements[4].video.name).to.equal('video 4 server 1')
591 expect(videoElements[ 4 ].position).to.equal(5) 591 expect(videoElements[4].position).to.equal(5)
592 expect(videoElements[ 4 ].startTimestamp).to.equal(45) 592 expect(videoElements[4].startTimestamp).to.equal(45)
593 expect(videoElements[ 4 ].stopTimestamp).to.equal(60) 593 expect(videoElements[4].stopTimestamp).to.equal(60)
594 594
595 expect(videoElements[ 5 ].video.name).to.equal('NSFW video') 595 expect(videoElements[5].video.name).to.equal('NSFW video')
596 expect(videoElements[ 5 ].position).to.equal(6) 596 expect(videoElements[5].position).to.equal(6)
597 expect(videoElements[ 5 ].startTimestamp).to.equal(5) 597 expect(videoElements[5].startTimestamp).to.equal(5)
598 expect(videoElements[ 5 ].stopTimestamp).to.be.null 598 expect(videoElements[5].stopTimestamp).to.be.null
599 599
600 const res3 = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 2) 600 const res3 = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 2)
601 expect(res3.body.data).to.have.lengthOf(2) 601 expect(res3.body.data).to.have.lengthOf(2)
@@ -616,18 +616,18 @@ describe('Test video playlists', function () {
616 before(async function () { 616 before(async function () {
617 this.timeout(30000) 617 this.timeout(30000)
618 618
619 groupUser1 = [ Object.assign({}, servers[ 0 ], { accessToken: userAccessTokenServer1 }) ] 619 groupUser1 = [ Object.assign({}, servers[0], { accessToken: userAccessTokenServer1 }) ]
620 groupWithoutToken1 = [ Object.assign({}, servers[ 0 ], { accessToken: undefined }) ] 620 groupWithoutToken1 = [ Object.assign({}, servers[0], { accessToken: undefined }) ]
621 group1 = [ servers[ 0 ] ] 621 group1 = [ servers[0] ]
622 group2 = [ servers[ 1 ], servers[ 2 ] ] 622 group2 = [ servers[1], servers[2] ]
623 623
624 const res = await createVideoPlaylist({ 624 const res = await createVideoPlaylist({
625 url: servers[ 0 ].url, 625 url: servers[0].url,
626 token: userAccessTokenServer1, 626 token: userAccessTokenServer1,
627 playlistAttrs: { 627 playlistAttrs: {
628 displayName: 'playlist 56', 628 displayName: 'playlist 56',
629 privacy: VideoPlaylistPrivacy.PUBLIC, 629 privacy: VideoPlaylistPrivacy.PUBLIC,
630 videoChannelId: servers[ 0 ].videoChannel.id 630 videoChannelId: servers[0].videoChannel.id
631 } 631 }
632 }) 632 })
633 633
@@ -635,7 +635,7 @@ describe('Test video playlists', function () {
635 playlistServer1UUID2 = res.body.videoPlaylist.uuid 635 playlistServer1UUID2 = res.body.videoPlaylist.uuid
636 636
637 const addVideo = (elementAttrs: any) => { 637 const addVideo = (elementAttrs: any) => {
638 return addVideoInPlaylist({ url: servers[ 0 ].url, token: userAccessTokenServer1, playlistId: playlistServer1Id2, elementAttrs }) 638 return addVideoInPlaylist({ url: servers[0].url, token: userAccessTokenServer1, playlistId: playlistServer1Id2, elementAttrs })
639 } 639 }
640 640
641 video1 = (await uploadVideoAndGetId({ server: servers[0], videoName: 'video 89', token: userAccessTokenServer1 })).uuid 641 video1 = (await uploadVideoAndGetId({ server: servers[0], videoName: 'video 89', token: userAccessTokenServer1 })).uuid
@@ -656,7 +656,7 @@ describe('Test video playlists', function () {
656 const position = 1 656 const position = 1
657 657
658 { 658 {
659 await updateVideo(servers[ 0 ].url, servers[ 0 ].accessToken, video1, { privacy: VideoPrivacy.PRIVATE }) 659 await updateVideo(servers[0].url, servers[0].accessToken, video1, { privacy: VideoPrivacy.PRIVATE })
660 await waitJobs(servers) 660 await waitJobs(servers)
661 661
662 await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) 662 await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
@@ -666,7 +666,7 @@ describe('Test video playlists', function () {
666 } 666 }
667 667
668 { 668 {
669 await updateVideo(servers[ 0 ].url, servers[ 0 ].accessToken, video1, { privacy: VideoPrivacy.PUBLIC }) 669 await updateVideo(servers[0].url, servers[0].accessToken, video1, { privacy: VideoPrivacy.PUBLIC })
670 await waitJobs(servers) 670 await waitJobs(servers)
671 671
672 await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) 672 await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
@@ -684,7 +684,7 @@ describe('Test video playlists', function () {
684 const position = 1 684 const position = 1
685 685
686 { 686 {
687 await addVideoToBlacklist(servers[ 0 ].url, servers[ 0 ].accessToken, video1, 'reason', true) 687 await addVideoToBlacklist(servers[0].url, servers[0].accessToken, video1, 'reason', true)
688 await waitJobs(servers) 688 await waitJobs(servers)
689 689
690 await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) 690 await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
@@ -694,7 +694,7 @@ describe('Test video playlists', function () {
694 } 694 }
695 695
696 { 696 {
697 await removeVideoFromBlacklist(servers[ 0 ].url, servers[ 0 ].accessToken, video1) 697 await removeVideoFromBlacklist(servers[0].url, servers[0].accessToken, video1)
698 await waitJobs(servers) 698 await waitJobs(servers)
699 699
700 await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) 700 await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
@@ -712,52 +712,52 @@ describe('Test video playlists', function () {
712 const position = 2 712 const position = 2
713 713
714 { 714 {
715 await addAccountToAccountBlocklist(servers[ 0 ].url, userAccessTokenServer1, 'root@localhost:' + servers[1].port) 715 await addAccountToAccountBlocklist(servers[0].url, userAccessTokenServer1, 'root@localhost:' + servers[1].port)
716 await waitJobs(servers) 716 await waitJobs(servers)
717 717
718 await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3) 718 await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3)
719 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) 719 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
720 720
721 await removeAccountFromAccountBlocklist(servers[ 0 ].url, userAccessTokenServer1, 'root@localhost:' + servers[1].port) 721 await removeAccountFromAccountBlocklist(servers[0].url, userAccessTokenServer1, 'root@localhost:' + servers[1].port)
722 await waitJobs(servers) 722 await waitJobs(servers)
723 723
724 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) 724 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
725 } 725 }
726 726
727 { 727 {
728 await addServerToAccountBlocklist(servers[ 0 ].url, userAccessTokenServer1, 'localhost:' + servers[1].port) 728 await addServerToAccountBlocklist(servers[0].url, userAccessTokenServer1, 'localhost:' + servers[1].port)
729 await waitJobs(servers) 729 await waitJobs(servers)
730 730
731 await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3) 731 await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3)
732 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) 732 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
733 733
734 await removeServerFromAccountBlocklist(servers[ 0 ].url, userAccessTokenServer1, 'localhost:' + servers[1].port) 734 await removeServerFromAccountBlocklist(servers[0].url, userAccessTokenServer1, 'localhost:' + servers[1].port)
735 await waitJobs(servers) 735 await waitJobs(servers)
736 736
737 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) 737 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
738 } 738 }
739 739
740 { 740 {
741 await addAccountToServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'root@localhost:' + servers[1].port) 741 await addAccountToServerBlocklist(servers[0].url, servers[0].accessToken, 'root@localhost:' + servers[1].port)
742 await waitJobs(servers) 742 await waitJobs(servers)
743 743
744 await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3) 744 await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3)
745 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) 745 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
746 746
747 await removeAccountFromServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'root@localhost:' + servers[1].port) 747 await removeAccountFromServerBlocklist(servers[0].url, servers[0].accessToken, 'root@localhost:' + servers[1].port)
748 await waitJobs(servers) 748 await waitJobs(servers)
749 749
750 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) 750 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
751 } 751 }
752 752
753 { 753 {
754 await addServerToServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'localhost:' + servers[1].port) 754 await addServerToServerBlocklist(servers[0].url, servers[0].accessToken, 'localhost:' + servers[1].port)
755 await waitJobs(servers) 755 await waitJobs(servers)
756 756
757 await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3) 757 await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3)
758 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) 758 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
759 759
760 await removeServerFromServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'localhost:' + servers[1].port) 760 await removeServerFromServerBlocklist(servers[0].url, servers[0].accessToken, 'localhost:' + servers[1].port)
761 await waitJobs(servers) 761 await waitJobs(servers)
762 762
763 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) 763 await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3)
@@ -785,8 +785,8 @@ describe('Test video playlists', function () {
785 785
786 { 786 {
787 await reorderVideosPlaylist({ 787 await reorderVideosPlaylist({
788 url: servers[ 0 ].url, 788 url: servers[0].url,
789 token: servers[ 0 ].accessToken, 789 token: servers[0].accessToken,
790 playlistId: playlistServer1Id, 790 playlistId: playlistServer1Id,
791 elementAttrs: { 791 elementAttrs: {
792 startPosition: 2, 792 startPosition: 2,
@@ -813,8 +813,8 @@ describe('Test video playlists', function () {
813 813
814 { 814 {
815 await reorderVideosPlaylist({ 815 await reorderVideosPlaylist({
816 url: servers[ 0 ].url, 816 url: servers[0].url,
817 token: servers[ 0 ].accessToken, 817 token: servers[0].accessToken,
818 playlistId: playlistServer1Id, 818 playlistId: playlistServer1Id,
819 elementAttrs: { 819 elementAttrs: {
820 startPosition: 1, 820 startPosition: 1,
@@ -842,8 +842,8 @@ describe('Test video playlists', function () {
842 842
843 { 843 {
844 await reorderVideosPlaylist({ 844 await reorderVideosPlaylist({
845 url: servers[ 0 ].url, 845 url: servers[0].url,
846 token: servers[ 0 ].accessToken, 846 token: servers[0].accessToken,
847 playlistId: playlistServer1Id, 847 playlistId: playlistServer1Id,
848 elementAttrs: { 848 elementAttrs: {
849 startPosition: 6, 849 startPosition: 6,
@@ -868,7 +868,7 @@ describe('Test video playlists', function () {
868 ]) 868 ])
869 869
870 for (let i = 1; i <= elements.length; i++) { 870 for (let i = 1; i <= elements.length; i++) {
871 expect(elements[ i - 1 ].position).to.equal(i) 871 expect(elements[i - 1].position).to.equal(i)
872 } 872 }
873 } 873 }
874 } 874 }
@@ -878,8 +878,8 @@ describe('Test video playlists', function () {
878 this.timeout(30000) 878 this.timeout(30000)
879 879
880 await updateVideoPlaylistElement({ 880 await updateVideoPlaylistElement({
881 url: servers[ 0 ].url, 881 url: servers[0].url,
882 token: servers[ 0 ].accessToken, 882 token: servers[0].accessToken,
883 playlistId: playlistServer1Id, 883 playlistId: playlistServer1Id,
884 playlistElementId: playlistElementServer1Video4, 884 playlistElementId: playlistElementServer1Video4,
885 elementAttrs: { 885 elementAttrs: {
@@ -888,8 +888,8 @@ describe('Test video playlists', function () {
888 }) 888 })
889 889
890 await updateVideoPlaylistElement({ 890 await updateVideoPlaylistElement({
891 url: servers[ 0 ].url, 891 url: servers[0].url,
892 token: servers[ 0 ].accessToken, 892 token: servers[0].accessToken,
893 playlistId: playlistServer1Id, 893 playlistId: playlistServer1Id,
894 playlistElementId: playlistElementServer1Video5, 894 playlistElementId: playlistElementServer1Video5,
895 elementAttrs: { 895 elementAttrs: {
@@ -903,62 +903,62 @@ describe('Test video playlists', function () {
903 const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10) 903 const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10)
904 const elements: VideoPlaylistElement[] = res.body.data 904 const elements: VideoPlaylistElement[] = res.body.data
905 905
906 expect(elements[ 0 ].video.name).to.equal('video 3 server 1') 906 expect(elements[0].video.name).to.equal('video 3 server 1')
907 expect(elements[ 0 ].position).to.equal(1) 907 expect(elements[0].position).to.equal(1)
908 expect(elements[ 0 ].startTimestamp).to.equal(1) 908 expect(elements[0].startTimestamp).to.equal(1)
909 expect(elements[ 0 ].stopTimestamp).to.equal(35) 909 expect(elements[0].stopTimestamp).to.equal(35)
910 910
911 expect(elements[ 5 ].video.name).to.equal('video 4 server 1') 911 expect(elements[5].video.name).to.equal('video 4 server 1')
912 expect(elements[ 5 ].position).to.equal(6) 912 expect(elements[5].position).to.equal(6)
913 expect(elements[ 5 ].startTimestamp).to.equal(45) 913 expect(elements[5].startTimestamp).to.equal(45)
914 expect(elements[ 5 ].stopTimestamp).to.be.null 914 expect(elements[5].stopTimestamp).to.be.null
915 } 915 }
916 }) 916 })
917 917
918 it('Should check videos existence in my playlist', async function () { 918 it('Should check videos existence in my playlist', async function () {
919 const videoIds = [ 919 const videoIds = [
920 servers[ 0 ].videos[ 0 ].id, 920 servers[0].videos[0].id,
921 42000, 921 42000,
922 servers[ 0 ].videos[ 3 ].id, 922 servers[0].videos[3].id,
923 43000, 923 43000,
924 servers[ 0 ].videos[ 4 ].id 924 servers[0].videos[4].id
925 ] 925 ]
926 const res = await doVideosExistInMyPlaylist(servers[ 0 ].url, servers[ 0 ].accessToken, videoIds) 926 const res = await doVideosExistInMyPlaylist(servers[0].url, servers[0].accessToken, videoIds)
927 const obj = res.body as VideoExistInPlaylist 927 const obj = res.body as VideoExistInPlaylist
928 928
929 { 929 {
930 const elem = obj[ servers[ 0 ].videos[ 0 ].id ] 930 const elem = obj[servers[0].videos[0].id]
931 expect(elem).to.have.lengthOf(1) 931 expect(elem).to.have.lengthOf(1)
932 expect(elem[ 0 ].playlistElementId).to.exist 932 expect(elem[0].playlistElementId).to.exist
933 expect(elem[ 0 ].playlistId).to.equal(playlistServer1Id) 933 expect(elem[0].playlistId).to.equal(playlistServer1Id)
934 expect(elem[ 0 ].startTimestamp).to.equal(15) 934 expect(elem[0].startTimestamp).to.equal(15)
935 expect(elem[ 0 ].stopTimestamp).to.equal(28) 935 expect(elem[0].stopTimestamp).to.equal(28)
936 } 936 }
937 937
938 { 938 {
939 const elem = obj[ servers[ 0 ].videos[ 3 ].id ] 939 const elem = obj[servers[0].videos[3].id]
940 expect(elem).to.have.lengthOf(1) 940 expect(elem).to.have.lengthOf(1)
941 expect(elem[ 0 ].playlistElementId).to.equal(playlistElementServer1Video4) 941 expect(elem[0].playlistElementId).to.equal(playlistElementServer1Video4)
942 expect(elem[ 0 ].playlistId).to.equal(playlistServer1Id) 942 expect(elem[0].playlistId).to.equal(playlistServer1Id)
943 expect(elem[ 0 ].startTimestamp).to.equal(1) 943 expect(elem[0].startTimestamp).to.equal(1)
944 expect(elem[ 0 ].stopTimestamp).to.equal(35) 944 expect(elem[0].stopTimestamp).to.equal(35)
945 } 945 }
946 946
947 { 947 {
948 const elem = obj[ servers[ 0 ].videos[ 4 ].id ] 948 const elem = obj[servers[0].videos[4].id]
949 expect(elem).to.have.lengthOf(1) 949 expect(elem).to.have.lengthOf(1)
950 expect(elem[ 0 ].playlistId).to.equal(playlistServer1Id) 950 expect(elem[0].playlistId).to.equal(playlistServer1Id)
951 expect(elem[ 0 ].startTimestamp).to.equal(45) 951 expect(elem[0].startTimestamp).to.equal(45)
952 expect(elem[ 0 ].stopTimestamp).to.equal(null) 952 expect(elem[0].stopTimestamp).to.equal(null)
953 } 953 }
954 954
955 expect(obj[ 42000 ]).to.have.lengthOf(0) 955 expect(obj[42000]).to.have.lengthOf(0)
956 expect(obj[ 43000 ]).to.have.lengthOf(0) 956 expect(obj[43000]).to.have.lengthOf(0)
957 }) 957 })
958 958
959 it('Should automatically update updatedAt field of playlists', async function () { 959 it('Should automatically update updatedAt field of playlists', async function () {
960 const server = servers[ 1 ] 960 const server = servers[1]
961 const videoId = servers[ 1 ].videos[ 5 ].id 961 const videoId = servers[1].videos[5].id
962 962
963 async function getPlaylistNames () { 963 async function getPlaylistNames () {
964 const res = await getAccountPlaylistsListWithToken(server.url, server.accessToken, 'root', 0, 5, undefined, '-updatedAt') 964 const res = await getAccountPlaylistsListWithToken(server.url, server.accessToken, 'root', 0, 5, undefined, '-updatedAt')
@@ -974,8 +974,8 @@ describe('Test video playlists', function () {
974 const element2 = res2.body.videoPlaylistElement.id 974 const element2 = res2.body.videoPlaylistElement.id
975 975
976 const names1 = await getPlaylistNames() 976 const names1 = await getPlaylistNames()
977 expect(names1[ 0 ]).to.equal('playlist 3 updated') 977 expect(names1[0]).to.equal('playlist 3 updated')
978 expect(names1[ 1 ]).to.equal('playlist 2') 978 expect(names1[1]).to.equal('playlist 2')
979 979
980 await removeVideoFromPlaylist({ 980 await removeVideoFromPlaylist({
981 url: server.url, 981 url: server.url,
@@ -985,8 +985,8 @@ describe('Test video playlists', function () {
985 }) 985 })
986 986
987 const names2 = await getPlaylistNames() 987 const names2 = await getPlaylistNames()
988 expect(names2[ 0 ]).to.equal('playlist 2') 988 expect(names2[0]).to.equal('playlist 2')
989 expect(names2[ 1 ]).to.equal('playlist 3 updated') 989 expect(names2[1]).to.equal('playlist 3 updated')
990 990
991 await removeVideoFromPlaylist({ 991 await removeVideoFromPlaylist({
992 url: server.url, 992 url: server.url,
@@ -996,23 +996,23 @@ describe('Test video playlists', function () {
996 }) 996 })
997 997
998 const names3 = await getPlaylistNames() 998 const names3 = await getPlaylistNames()
999 expect(names3[ 0 ]).to.equal('playlist 3 updated') 999 expect(names3[0]).to.equal('playlist 3 updated')
1000 expect(names3[ 1 ]).to.equal('playlist 2') 1000 expect(names3[1]).to.equal('playlist 2')
1001 }) 1001 })
1002 1002
1003 it('Should delete some elements', async function () { 1003 it('Should delete some elements', async function () {
1004 this.timeout(30000) 1004 this.timeout(30000)
1005 1005
1006 await removeVideoFromPlaylist({ 1006 await removeVideoFromPlaylist({
1007 url: servers[ 0 ].url, 1007 url: servers[0].url,
1008 token: servers[ 0 ].accessToken, 1008 token: servers[0].accessToken,
1009 playlistId: playlistServer1Id, 1009 playlistId: playlistServer1Id,
1010 playlistElementId: playlistElementServer1Video4 1010 playlistElementId: playlistElementServer1Video4
1011 }) 1011 })
1012 1012
1013 await removeVideoFromPlaylist({ 1013 await removeVideoFromPlaylist({
1014 url: servers[ 0 ].url, 1014 url: servers[0].url,
1015 token: servers[ 0 ].accessToken, 1015 token: servers[0].accessToken,
1016 playlistId: playlistServer1Id, 1016 playlistId: playlistServer1Id,
1017 playlistElementId: playlistElementNSFW 1017 playlistElementId: playlistElementNSFW
1018 }) 1018 })
@@ -1027,17 +1027,17 @@ describe('Test video playlists', function () {
1027 const elements: VideoPlaylistElement[] = res.body.data 1027 const elements: VideoPlaylistElement[] = res.body.data
1028 expect(elements).to.have.lengthOf(4) 1028 expect(elements).to.have.lengthOf(4)
1029 1029
1030 expect(elements[ 0 ].video.name).to.equal('video 0 server 1') 1030 expect(elements[0].video.name).to.equal('video 0 server 1')
1031 expect(elements[ 0 ].position).to.equal(1) 1031 expect(elements[0].position).to.equal(1)
1032 1032
1033 expect(elements[ 1 ].video.name).to.equal('video 2 server 3') 1033 expect(elements[1].video.name).to.equal('video 2 server 3')
1034 expect(elements[ 1 ].position).to.equal(2) 1034 expect(elements[1].position).to.equal(2)
1035 1035
1036 expect(elements[ 2 ].video.name).to.equal('video 1 server 3') 1036 expect(elements[2].video.name).to.equal('video 1 server 3')
1037 expect(elements[ 2 ].position).to.equal(3) 1037 expect(elements[2].position).to.equal(3)
1038 1038
1039 expect(elements[ 3 ].video.name).to.equal('video 4 server 1') 1039 expect(elements[3].video.name).to.equal('video 4 server 1')
1040 expect(elements[ 3 ].position).to.equal(4) 1040 expect(elements[3].position).to.equal(4)
1041 } 1041 }
1042 }) 1042 })
1043 1043
@@ -1045,12 +1045,12 @@ describe('Test video playlists', function () {
1045 this.timeout(30000) 1045 this.timeout(30000)
1046 1046
1047 const res = await createVideoPlaylist({ 1047 const res = await createVideoPlaylist({
1048 url: servers[ 0 ].url, 1048 url: servers[0].url,
1049 token: servers[ 0 ].accessToken, 1049 token: servers[0].accessToken,
1050 playlistAttrs: { 1050 playlistAttrs: {
1051 displayName: 'my super public playlist', 1051 displayName: 'my super public playlist',
1052 privacy: VideoPlaylistPrivacy.PUBLIC, 1052 privacy: VideoPlaylistPrivacy.PUBLIC,
1053 videoChannelId: servers[ 0 ].videoChannel.id 1053 videoChannelId: servers[0].videoChannel.id
1054 } 1054 }
1055 }) 1055 })
1056 const videoPlaylistIds = res.body.videoPlaylist 1056 const videoPlaylistIds = res.body.videoPlaylist
@@ -1062,16 +1062,16 @@ describe('Test video playlists', function () {
1062 } 1062 }
1063 1063
1064 const playlistAttrs = { privacy: VideoPlaylistPrivacy.PRIVATE } 1064 const playlistAttrs = { privacy: VideoPlaylistPrivacy.PRIVATE }
1065 await updateVideoPlaylist({ url: servers[ 0 ].url, token: servers[ 0 ].accessToken, playlistId: videoPlaylistIds.id, playlistAttrs }) 1065 await updateVideoPlaylist({ url: servers[0].url, token: servers[0].accessToken, playlistId: videoPlaylistIds.id, playlistAttrs })
1066 1066
1067 await waitJobs(servers) 1067 await waitJobs(servers)
1068 1068
1069 for (const server of [ servers[ 1 ], servers[ 2 ] ]) { 1069 for (const server of [ servers[1], servers[2] ]) {
1070 await getVideoPlaylist(server.url, videoPlaylistIds.uuid, 404) 1070 await getVideoPlaylist(server.url, videoPlaylistIds.uuid, 404)
1071 } 1071 }
1072 await getVideoPlaylist(servers[ 0 ].url, videoPlaylistIds.uuid, 401) 1072 await getVideoPlaylist(servers[0].url, videoPlaylistIds.uuid, 401)
1073 1073
1074 await getVideoPlaylistWithToken(servers[ 0 ].url, servers[ 0 ].accessToken, videoPlaylistIds.uuid, 200) 1074 await getVideoPlaylistWithToken(servers[0].url, servers[0].accessToken, videoPlaylistIds.uuid, 200)
1075 }) 1075 })
1076 }) 1076 })
1077 1077
@@ -1080,7 +1080,7 @@ describe('Test video playlists', function () {
1080 it('Should delete the playlist on server 1 and delete on server 2 and 3', async function () { 1080 it('Should delete the playlist on server 1 and delete on server 2 and 3', async function () {
1081 this.timeout(30000) 1081 this.timeout(30000)
1082 1082
1083 await deleteVideoPlaylist(servers[ 0 ].url, servers[ 0 ].accessToken, playlistServer1Id) 1083 await deleteVideoPlaylist(servers[0].url, servers[0].accessToken, playlistServer1Id)
1084 1084
1085 await waitJobs(servers) 1085 await waitJobs(servers)
1086 1086
@@ -1103,15 +1103,15 @@ describe('Test video playlists', function () {
1103 const finder = data => data.find(p => p.displayName === 'my super playlist') 1103 const finder = data => data.find(p => p.displayName === 'my super playlist')
1104 1104
1105 { 1105 {
1106 const res = await getVideoPlaylistsList(servers[ 2 ].url, 0, 5) 1106 const res = await getVideoPlaylistsList(servers[2].url, 0, 5)
1107 expect(res.body.total).to.equal(3) 1107 expect(res.body.total).to.equal(3)
1108 expect(finder(res.body.data)).to.not.be.undefined 1108 expect(finder(res.body.data)).to.not.be.undefined
1109 } 1109 }
1110 1110
1111 await unfollow(servers[ 2 ].url, servers[ 2 ].accessToken, servers[ 0 ]) 1111 await unfollow(servers[2].url, servers[2].accessToken, servers[0])
1112 1112
1113 { 1113 {
1114 const res = await getVideoPlaylistsList(servers[ 2 ].url, 0, 5) 1114 const res = await getVideoPlaylistsList(servers[2].url, 0, 5)
1115 expect(res.body.total).to.equal(1) 1115 expect(res.body.total).to.equal(1)
1116 1116
1117 expect(finder(res.body.data)).to.be.undefined 1117 expect(finder(res.body.data)).to.be.undefined
@@ -1121,12 +1121,12 @@ describe('Test video playlists', function () {
1121 it('Should delete a channel and put the associated playlist in private mode', async function () { 1121 it('Should delete a channel and put the associated playlist in private mode', async function () {
1122 this.timeout(30000) 1122 this.timeout(30000)
1123 1123
1124 const res = await addVideoChannel(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'super_channel', displayName: 'super channel' }) 1124 const res = await addVideoChannel(servers[0].url, servers[0].accessToken, { name: 'super_channel', displayName: 'super channel' })
1125 const videoChannelId = res.body.videoChannel.id 1125 const videoChannelId = res.body.videoChannel.id
1126 1126
1127 const res2 = await createVideoPlaylist({ 1127 const res2 = await createVideoPlaylist({
1128 url: servers[ 0 ].url, 1128 url: servers[0].url,
1129 token: servers[ 0 ].accessToken, 1129 token: servers[0].accessToken,
1130 playlistAttrs: { 1130 playlistAttrs: {
1131 displayName: 'channel playlist', 1131 displayName: 'channel playlist',
1132 privacy: VideoPlaylistPrivacy.PUBLIC, 1132 privacy: VideoPlaylistPrivacy.PUBLIC,
@@ -1137,15 +1137,15 @@ describe('Test video playlists', function () {
1137 1137
1138 await waitJobs(servers) 1138 await waitJobs(servers)
1139 1139
1140 await deleteVideoChannel(servers[ 0 ].url, servers[ 0 ].accessToken, 'super_channel') 1140 await deleteVideoChannel(servers[0].url, servers[0].accessToken, 'super_channel')
1141 1141
1142 await waitJobs(servers) 1142 await waitJobs(servers)
1143 1143
1144 const res3 = await getVideoPlaylistWithToken(servers[ 0 ].url, servers[ 0 ].accessToken, videoPlaylistUUID) 1144 const res3 = await getVideoPlaylistWithToken(servers[0].url, servers[0].accessToken, videoPlaylistUUID)
1145 expect(res3.body.displayName).to.equal('channel playlist') 1145 expect(res3.body.displayName).to.equal('channel playlist')
1146 expect(res3.body.privacy.id).to.equal(VideoPlaylistPrivacy.PRIVATE) 1146 expect(res3.body.privacy.id).to.equal(VideoPlaylistPrivacy.PRIVATE)
1147 1147
1148 await getVideoPlaylist(servers[ 1 ].url, videoPlaylistUUID, 404) 1148 await getVideoPlaylist(servers[1].url, videoPlaylistUUID, 404)
1149 }) 1149 })
1150 1150
1151 it('Should delete an account and delete its playlists', async function () { 1151 it('Should delete an account and delete its playlists', async function () {
@@ -1153,20 +1153,20 @@ describe('Test video playlists', function () {
1153 1153
1154 const user = { username: 'user_1', password: 'password' } 1154 const user = { username: 'user_1', password: 'password' }
1155 const res = await createUser({ 1155 const res = await createUser({
1156 url: servers[ 0 ].url, 1156 url: servers[0].url,
1157 accessToken: servers[ 0 ].accessToken, 1157 accessToken: servers[0].accessToken,
1158 username: user.username, 1158 username: user.username,
1159 password: user.password 1159 password: user.password
1160 }) 1160 })
1161 1161
1162 const userId = res.body.user.id 1162 const userId = res.body.user.id
1163 const userAccessToken = await userLogin(servers[ 0 ], user) 1163 const userAccessToken = await userLogin(servers[0], user)
1164 1164
1165 const resChannel = await getMyUserInformation(servers[ 0 ].url, userAccessToken) 1165 const resChannel = await getMyUserInformation(servers[0].url, userAccessToken)
1166 const userChannel = (resChannel.body as User).videoChannels[ 0 ] 1166 const userChannel = (resChannel.body as User).videoChannels[0]
1167 1167
1168 await createVideoPlaylist({ 1168 await createVideoPlaylist({
1169 url: servers[ 0 ].url, 1169 url: servers[0].url,
1170 token: userAccessToken, 1170 token: userAccessToken,
1171 playlistAttrs: { 1171 playlistAttrs: {
1172 displayName: 'playlist to be deleted', 1172 displayName: 'playlist to be deleted',
@@ -1180,17 +1180,17 @@ describe('Test video playlists', function () {
1180 const finder = data => data.find(p => p.displayName === 'playlist to be deleted') 1180 const finder = data => data.find(p => p.displayName === 'playlist to be deleted')
1181 1181
1182 { 1182 {
1183 for (const server of [ servers[ 0 ], servers[ 1 ] ]) { 1183 for (const server of [ servers[0], servers[1] ]) {
1184 const res = await getVideoPlaylistsList(server.url, 0, 15) 1184 const res = await getVideoPlaylistsList(server.url, 0, 15)
1185 expect(finder(res.body.data)).to.not.be.undefined 1185 expect(finder(res.body.data)).to.not.be.undefined
1186 } 1186 }
1187 } 1187 }
1188 1188
1189 await removeUser(servers[ 0 ].url, userId, servers[ 0 ].accessToken) 1189 await removeUser(servers[0].url, userId, servers[0].accessToken)
1190 await waitJobs(servers) 1190 await waitJobs(servers)
1191 1191
1192 { 1192 {
1193 for (const server of [ servers[ 0 ], servers[ 1 ] ]) { 1193 for (const server of [ servers[0], servers[1] ]) {
1194 const res = await getVideoPlaylistsList(server.url, 0, 15) 1194 const res = await getVideoPlaylistsList(server.url, 0, 15)
1195 expect(finder(res.body.data)).to.be.undefined 1195 expect(finder(res.body.data)).to.be.undefined
1196 } 1196 }
diff --git a/server/tests/api/videos/video-privacy.ts b/server/tests/api/videos/video-privacy.ts
index e630ca84a..4bbbb90f3 100644
--- a/server/tests/api/videos/video-privacy.ts
+++ b/server/tests/api/videos/video-privacy.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
@@ -6,7 +6,8 @@ import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enu
6import { 6import {
7 cleanupTests, 7 cleanupTests,
8 flushAndRunMultipleServers, 8 flushAndRunMultipleServers,
9 getVideosList, getVideosListWithToken, 9 getVideosList,
10 getVideosListWithToken,
10 ServerInfo, 11 ServerInfo,
11 setAccessTokensToServers, 12 setAccessTokensToServers,
12 uploadVideo 13 uploadVideo
@@ -110,7 +111,7 @@ describe('Test video privacy', function () {
110 username: 'hello', 111 username: 'hello',
111 password: 'super password' 112 password: 'super password'
112 } 113 }
113 await createUser({ url: servers[ 0 ].url, accessToken: servers[ 0 ].accessToken, username: user.username, password: user.password }) 114 await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: user.username, password: user.password })
114 115
115 anotherUserToken = await userLogin(servers[0], user) 116 anotherUserToken = await userLogin(servers[0], user)
116 await getVideoWithToken(servers[0].url, anotherUserToken, privateVideoUUID, 403) 117 await getVideoWithToken(servers[0].url, anotherUserToken, privateVideoUUID, 403)
@@ -174,7 +175,7 @@ describe('Test video privacy', function () {
174 privacy: VideoPrivacy.PUBLIC 175 privacy: VideoPrivacy.PUBLIC
175 } 176 }
176 177
177 await updateVideo(servers[ 0 ].url, servers[ 0 ].accessToken, privateVideoId, attribute) 178 await updateVideo(servers[0].url, servers[0].accessToken, privateVideoId, attribute)
178 } 179 }
179 180
180 { 181 {
@@ -182,7 +183,7 @@ describe('Test video privacy', function () {
182 name: 'internal video becomes public', 183 name: 'internal video becomes public',
183 privacy: VideoPrivacy.PUBLIC 184 privacy: VideoPrivacy.PUBLIC
184 } 185 }
185 await updateVideo(servers[ 0 ].url, servers[ 0 ].accessToken, internalVideoId, attribute) 186 await updateVideo(servers[0].url, servers[0].accessToken, internalVideoId, attribute)
186 } 187 }
187 188
188 await waitJobs(servers) 189 await waitJobs(servers)
diff --git a/server/tests/api/videos/video-schedule-update.ts b/server/tests/api/videos/video-schedule-update.ts
index 65a8eafb8..204f43611 100644
--- a/server/tests/api/videos/video-schedule-update.ts
+++ b/server/tests/api/videos/video-schedule-update.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
@@ -10,7 +10,6 @@ import {
10 getMyVideos, 10 getMyVideos,
11 getVideosList, 11 getVideosList,
12 getVideoWithToken, 12 getVideoWithToken,
13 killallServers,
14 ServerInfo, 13 ServerInfo,
15 setAccessTokensToServers, 14 setAccessTokensToServers,
16 updateVideo, 15 updateVideo,
diff --git a/server/tests/api/videos/video-transcoder.ts b/server/tests/api/videos/video-transcoder.ts
index 4be74901a..13b3530b1 100644
--- a/server/tests/api/videos/video-transcoder.ts
+++ b/server/tests/api/videos/video-transcoder.ts
@@ -1,29 +1,40 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { omit } from 'lodash' 5import { omit } from 'lodash'
6import { getMaxBitrate, VideoDetails, VideoResolution, VideoState } from '../../../../shared/models/videos' 6import { getMaxBitrate, VideoDetails, VideoResolution, VideoState } from '../../../../shared/models/videos'
7import { audio, canDoQuickTranscode, getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' 7import {
8 audio,
9 canDoQuickTranscode,
10 getVideoFileBitrate,
11 getVideoFileFPS,
12 getVideoFileResolution,
13 getMetadataFromFile
14} from '../../../helpers/ffmpeg-utils'
8import { 15import {
9 buildAbsoluteFixturePath, 16 buildAbsoluteFixturePath,
10 cleanupTests, 17 cleanupTests,
11 doubleFollow, 18 doubleFollow,
12 flushAndRunMultipleServers, 19 flushAndRunMultipleServers,
13 generateHighBitrateVideo, 20 generateHighBitrateVideo,
21 generateVideoWithFramerate,
14 getMyVideos, 22 getMyVideos,
15 getVideo, 23 getVideo,
24 getVideoFileMetadataUrl,
16 getVideosList, 25 getVideosList,
17 makeGetRequest, 26 makeGetRequest,
18 root, 27 root,
19 ServerInfo, 28 ServerInfo,
20 setAccessTokensToServers, 29 setAccessTokensToServers,
21 uploadVideo, 30 uploadVideo, uploadVideoAndGetId,
22 waitJobs, 31 waitJobs,
23 webtorrentAdd 32 webtorrentAdd
24} from '../../../../shared/extra-utils' 33} from '../../../../shared/extra-utils'
25import { join } from 'path' 34import { join } from 'path'
26import { VIDEO_TRANSCODING_FPS } from '../../../../server/initializers/constants' 35import { VIDEO_TRANSCODING_FPS } from '../../../../server/initializers/constants'
36import { FfprobeData } from 'fluent-ffmpeg'
37import { VideoFileMetadata } from '@shared/models/videos/video-file-metadata'
27 38
28const expect = chai.expect 39const expect = chai.expect
29 40
@@ -55,19 +66,19 @@ describe('Test video transcoding', function () {
55 66
56 for (const server of servers) { 67 for (const server of servers) {
57 const res = await getVideosList(server.url) 68 const res = await getVideosList(server.url)
58 const video = res.body.data[ 0 ] 69 const video = res.body.data[0]
59 70
60 const res2 = await getVideo(server.url, video.id) 71 const res2 = await getVideo(server.url, video.id)
61 const videoDetails = res2.body 72 const videoDetails = res2.body
62 expect(videoDetails.files).to.have.lengthOf(1) 73 expect(videoDetails.files).to.have.lengthOf(1)
63 74
64 const magnetUri = videoDetails.files[ 0 ].magnetUri 75 const magnetUri = videoDetails.files[0].magnetUri
65 expect(magnetUri).to.match(/\.webm/) 76 expect(magnetUri).to.match(/\.webm/)
66 77
67 const torrent = await webtorrentAdd(magnetUri, true) 78 const torrent = await webtorrentAdd(magnetUri, true)
68 expect(torrent.files).to.be.an('array') 79 expect(torrent.files).to.be.an('array')
69 expect(torrent.files.length).to.equal(1) 80 expect(torrent.files.length).to.equal(1)
70 expect(torrent.files[ 0 ].path).match(/\.webm$/) 81 expect(torrent.files[0].path).match(/\.webm$/)
71 } 82 }
72 }) 83 })
73 84
@@ -92,13 +103,13 @@ describe('Test video transcoding', function () {
92 103
93 expect(videoDetails.files).to.have.lengthOf(4) 104 expect(videoDetails.files).to.have.lengthOf(4)
94 105
95 const magnetUri = videoDetails.files[ 0 ].magnetUri 106 const magnetUri = videoDetails.files[0].magnetUri
96 expect(magnetUri).to.match(/\.mp4/) 107 expect(magnetUri).to.match(/\.mp4/)
97 108
98 const torrent = await webtorrentAdd(magnetUri, true) 109 const torrent = await webtorrentAdd(magnetUri, true)
99 expect(torrent.files).to.be.an('array') 110 expect(torrent.files).to.be.an('array')
100 expect(torrent.files.length).to.equal(1) 111 expect(torrent.files.length).to.equal(1)
101 expect(torrent.files[ 0 ].path).match(/\.mp4$/) 112 expect(torrent.files[0].path).match(/\.mp4$/)
102 } 113 }
103 }) 114 })
104 115
@@ -126,8 +137,8 @@ describe('Test video transcoding', function () {
126 const probe = await audio.get(path) 137 const probe = await audio.get(path)
127 138
128 if (probe.audioStream) { 139 if (probe.audioStream) {
129 expect(probe.audioStream[ 'codec_name' ]).to.be.equal('aac') 140 expect(probe.audioStream['codec_name']).to.be.equal('aac')
130 expect(probe.audioStream[ 'bit_rate' ]).to.be.at.most(384 * 8000) 141 expect(probe.audioStream['bit_rate']).to.be.at.most(384 * 8000)
131 } else { 142 } else {
132 this.fail('Could not retrieve the audio stream on ' + probe.absolutePath) 143 this.fail('Could not retrieve the audio stream on ' + probe.absolutePath)
133 } 144 }
@@ -211,10 +222,10 @@ describe('Test video transcoding', function () {
211 const videoDetails: VideoDetails = res2.body 222 const videoDetails: VideoDetails = res2.body
212 223
213 expect(videoDetails.files).to.have.lengthOf(4) 224 expect(videoDetails.files).to.have.lengthOf(4)
214 expect(videoDetails.files[ 0 ].fps).to.be.above(58).and.below(62) 225 expect(videoDetails.files[0].fps).to.be.above(58).and.below(62)
215 expect(videoDetails.files[ 1 ].fps).to.be.below(31) 226 expect(videoDetails.files[1].fps).to.be.below(31)
216 expect(videoDetails.files[ 2 ].fps).to.be.below(31) 227 expect(videoDetails.files[2].fps).to.be.below(31)
217 expect(videoDetails.files[ 3 ].fps).to.be.below(31) 228 expect(videoDetails.files[3].fps).to.be.below(31)
218 229
219 for (const resolution of [ '240', '360', '480' ]) { 230 for (const resolution of [ '240', '360', '480' ]) {
220 const path = join(root(), 'test' + servers[1].internalServerNumber, 'videos', video.uuid + '-' + resolution + '.mp4') 231 const path = join(root(), 'test' + servers[1].internalServerNumber, 'videos', video.uuid + '-' + resolution + '.mp4')
@@ -240,11 +251,11 @@ describe('Test video transcoding', function () {
240 fixture: 'video_short1.webm', 251 fixture: 'video_short1.webm',
241 waitTranscoding: true 252 waitTranscoding: true
242 } 253 }
243 const resVideo = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, videoAttributes) 254 const resVideo = await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
244 const videoId = resVideo.body.video.uuid 255 const videoId = resVideo.body.video.uuid
245 256
246 // Should be in transcode state 257 // Should be in transcode state
247 const { body } = await getVideo(servers[ 1 ].url, videoId) 258 const { body } = await getVideo(servers[1].url, videoId)
248 expect(body.name).to.equal('waiting video') 259 expect(body.name).to.equal('waiting video')
249 expect(body.state.id).to.equal(VideoState.TO_TRANSCODE) 260 expect(body.state.id).to.equal(VideoState.TO_TRANSCODE)
250 expect(body.state.label).to.equal('To transcode') 261 expect(body.state.label).to.equal('To transcode')
@@ -310,7 +321,7 @@ describe('Test video transcoding', function () {
310 321
311 const video = res.body.data.find(v => v.name === videoAttributes.name) 322 const video = res.body.data.find(v => v.name === videoAttributes.name)
312 323
313 for (const resolution of ['240', '360', '480', '720', '1080']) { 324 for (const resolution of [ '240', '360', '480', '720', '1080' ]) {
314 const path = join(root(), 'test' + servers[1].internalServerNumber, 'videos', video.uuid + '-' + resolution + '.mp4') 325 const path = join(root(), 'test' + servers[1].internalServerNumber, 'videos', video.uuid + '-' + resolution + '.mp4')
315 const bitrate = await getVideoFileBitrate(path) 326 const bitrate = await getVideoFileBitrate(path)
316 const fps = await getVideoFileFPS(path) 327 const fps = await getVideoFileFPS(path)
@@ -340,7 +351,7 @@ describe('Test video transcoding', function () {
340 fixture 351 fixture
341 } 352 }
342 353
343 await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, videoAttributes) 354 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
344 355
345 await waitJobs(servers) 356 await waitJobs(servers)
346 357
@@ -353,7 +364,7 @@ describe('Test video transcoding', function () {
353 364
354 expect(videoDetails.files).to.have.lengthOf(4) 365 expect(videoDetails.files).to.have.lengthOf(4)
355 366
356 const magnetUri = videoDetails.files[ 0 ].magnetUri 367 const magnetUri = videoDetails.files[0].magnetUri
357 expect(magnetUri).to.contain('.mp4') 368 expect(magnetUri).to.contain('.mp4')
358 } 369 }
359 } 370 }
@@ -370,7 +381,7 @@ describe('Test video transcoding', function () {
370 this.timeout(60000) 381 this.timeout(60000)
371 382
372 const videoAttributesArg = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' } 383 const videoAttributesArg = { name: 'audio_with_preview', previewfile: 'preview.jpg', fixture: 'sample.ogg' }
373 await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, videoAttributesArg) 384 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributesArg)
374 385
375 await waitJobs(servers) 386 await waitJobs(servers)
376 387
@@ -386,7 +397,7 @@ describe('Test video transcoding', function () {
386 await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, statusCodeExpected: 200 }) 397 await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, statusCodeExpected: 200 })
387 await makeGetRequest({ url: server.url, path: videoDetails.previewPath, statusCodeExpected: 200 }) 398 await makeGetRequest({ url: server.url, path: videoDetails.previewPath, statusCodeExpected: 200 })
388 399
389 const magnetUri = videoDetails.files[ 0 ].magnetUri 400 const magnetUri = videoDetails.files[0].magnetUri
390 expect(magnetUri).to.contain('.mp4') 401 expect(magnetUri).to.contain('.mp4')
391 } 402 }
392 }) 403 })
@@ -395,7 +406,7 @@ describe('Test video transcoding', function () {
395 this.timeout(60000) 406 this.timeout(60000)
396 407
397 const videoAttributesArg = { name: 'audio_without_preview', fixture: 'sample.ogg' } 408 const videoAttributesArg = { name: 'audio_without_preview', fixture: 'sample.ogg' }
398 await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, videoAttributesArg) 409 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributesArg)
399 410
400 await waitJobs(servers) 411 await waitJobs(servers)
401 412
@@ -411,11 +422,109 @@ describe('Test video transcoding', function () {
411 await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, statusCodeExpected: 200 }) 422 await makeGetRequest({ url: server.url, path: videoDetails.thumbnailPath, statusCodeExpected: 200 })
412 await makeGetRequest({ url: server.url, path: videoDetails.previewPath, statusCodeExpected: 200 }) 423 await makeGetRequest({ url: server.url, path: videoDetails.previewPath, statusCodeExpected: 200 })
413 424
414 const magnetUri = videoDetails.files[ 0 ].magnetUri 425 const magnetUri = videoDetails.files[0].magnetUri
415 expect(magnetUri).to.contain('.mp4') 426 expect(magnetUri).to.contain('.mp4')
416 } 427 }
417 }) 428 })
418 429
430 it('Should downscale to the closest divisor standard framerate', async function () {
431 this.timeout(160000)
432
433 let tempFixturePath: string
434
435 {
436 tempFixturePath = await generateVideoWithFramerate(59)
437
438 const fps = await getVideoFileFPS(tempFixturePath)
439 expect(fps).to.be.equal(59)
440 }
441
442 const videoAttributes = {
443 name: '59fps video',
444 description: '59fps video',
445 fixture: tempFixturePath
446 }
447
448 await uploadVideo(servers[1].url, servers[1].accessToken, videoAttributes)
449
450 await waitJobs(servers)
451
452 for (const server of servers) {
453 const res = await getVideosList(server.url)
454
455 const video = res.body.data.find(v => v.name === videoAttributes.name)
456
457 {
458 const path = join(root(), 'test' + servers[1].internalServerNumber, 'videos', video.uuid + '-240.mp4')
459 const fps = await getVideoFileFPS(path)
460 expect(fps).to.be.equal(25)
461 }
462
463 {
464 const path = join(root(), 'test' + servers[1].internalServerNumber, 'videos', video.uuid + '-720.mp4')
465 const fps = await getVideoFileFPS(path)
466 expect(fps).to.be.equal(59)
467 }
468 }
469 })
470
471 it('Should provide valid ffprobe data', async function () {
472 this.timeout(160000)
473
474 const videoUUID = (await uploadVideoAndGetId({ server: servers[1], videoName: 'ffprobe data' })).uuid
475 await waitJobs(servers)
476
477 {
478 const path = join(root(), 'test' + servers[1].internalServerNumber, 'videos', videoUUID + '-240.mp4')
479 const metadata = await getMetadataFromFile<VideoFileMetadata>(path)
480
481 // expected format properties
482 for (const p of [
483 'tags.encoder',
484 'format_long_name',
485 'size',
486 'bit_rate'
487 ]) {
488 expect(metadata.format).to.have.nested.property(p)
489 }
490
491 // expected stream properties
492 for (const p of [
493 'codec_long_name',
494 'profile',
495 'width',
496 'height',
497 'display_aspect_ratio',
498 'avg_frame_rate',
499 'pix_fmt'
500 ]) {
501 expect(metadata.streams[0]).to.have.nested.property(p)
502 }
503
504 expect(metadata).to.not.have.nested.property('format.filename')
505 }
506
507 for (const server of servers) {
508 const res2 = await getVideo(server.url, videoUUID)
509 const videoDetails: VideoDetails = res2.body
510
511 const videoFiles = videoDetails.files
512 .concat(videoDetails.streamingPlaylists[0].files)
513 expect(videoFiles).to.have.lengthOf(8)
514
515 for (const file of videoFiles) {
516 expect(file.metadata).to.be.undefined
517 expect(file.metadataUrl).to.exist
518 expect(file.metadataUrl).to.contain(servers[1].url)
519 expect(file.metadataUrl).to.contain(videoUUID)
520
521 const res3 = await getVideoFileMetadataUrl(file.metadataUrl)
522 const metadata: FfprobeData = res3.body
523 expect(metadata).to.have.nested.property('format.size')
524 }
525 }
526 })
527
419 after(async function () { 528 after(async function () {
420 await cleanupTests(servers) 529 await cleanupTests(servers)
421 }) 530 })
diff --git a/server/tests/api/videos/videos-filter.ts b/server/tests/api/videos/videos-filter.ts
index e1e65260f..95e12e43c 100644
--- a/server/tests/api/videos/videos-filter.ts
+++ b/server/tests/api/videos/videos-filter.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
@@ -7,8 +7,6 @@ import {
7 createUser, 7 createUser,
8 doubleFollow, 8 doubleFollow,
9 flushAndRunMultipleServers, 9 flushAndRunMultipleServers,
10 flushTests,
11 killallServers,
12 makeGetRequest, 10 makeGetRequest,
13 ServerInfo, 11 ServerInfo,
14 setAccessTokensToServers, 12 setAccessTokensToServers,
@@ -98,7 +96,7 @@ describe('Test videos filter validator', function () {
98 const namesResults = await getVideosNames(server, server.accessToken, 'local') 96 const namesResults = await getVideosNames(server, server.accessToken, 'local')
99 for (const names of namesResults) { 97 for (const names of namesResults) {
100 expect(names).to.have.lengthOf(1) 98 expect(names).to.have.lengthOf(1)
101 expect(names[ 0 ]).to.equal('public ' + server.serverNumber) 99 expect(names[0]).to.equal('public ' + server.serverNumber)
102 } 100 }
103 } 101 }
104 }) 102 })
@@ -111,9 +109,9 @@ describe('Test videos filter validator', function () {
111 for (const names of namesResults) { 109 for (const names of namesResults) {
112 expect(names).to.have.lengthOf(3) 110 expect(names).to.have.lengthOf(3)
113 111
114 expect(names[ 0 ]).to.equal('public ' + server.serverNumber) 112 expect(names[0]).to.equal('public ' + server.serverNumber)
115 expect(names[ 1 ]).to.equal('unlisted ' + server.serverNumber) 113 expect(names[1]).to.equal('unlisted ' + server.serverNumber)
116 expect(names[ 2 ]).to.equal('private ' + server.serverNumber) 114 expect(names[2]).to.equal('private ' + server.serverNumber)
117 } 115 }
118 } 116 }
119 } 117 }
diff --git a/server/tests/api/videos/videos-history.ts b/server/tests/api/videos/videos-history.ts
index c7e55c1ab..6f90e9a57 100644
--- a/server/tests/api/videos/videos-history.ts
+++ b/server/tests/api/videos/videos-history.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
diff --git a/server/tests/api/videos/videos-overview.ts b/server/tests/api/videos/videos-overview.ts
index 975a5c87a..d38bcb6eb 100644
--- a/server/tests/api/videos/videos-overview.ts
+++ b/server/tests/api/videos/videos-overview.ts
@@ -1,8 +1,8 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { cleanupTests, flushAndRunServer, ServerInfo, setAccessTokensToServers, uploadVideo } from '../../../../shared/extra-utils' 5import { cleanupTests, flushAndRunServer, ServerInfo, setAccessTokensToServers, uploadVideo, wait } from '../../../../shared/extra-utils'
6import { getVideosOverview } from '../../../../shared/extra-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
@@ -20,7 +20,7 @@ describe('Test a videos overview', function () {
20 }) 20 })
21 21
22 it('Should send empty overview', async function () { 22 it('Should send empty overview', async function () {
23 const res = await getVideosOverview(server.url) 23 const res = await getVideosOverview(server.url, 1)
24 24
25 const overview: VideosOverview = res.body 25 const overview: VideosOverview = res.body
26 expect(overview.tags).to.have.lengthOf(0) 26 expect(overview.tags).to.have.lengthOf(0)
@@ -31,15 +31,15 @@ describe('Test a videos overview', function () {
31 it('Should upload 5 videos in a specific category, tag and channel but not include them in overview', async function () { 31 it('Should upload 5 videos in a specific category, tag and channel but not include them in overview', async function () {
32 this.timeout(15000) 32 this.timeout(15000)
33 33
34 for (let i = 0; i < 5; i++) { 34 await wait(3000)
35 await uploadVideo(server.url, server.accessToken, { 35
36 name: 'video ' + i, 36 await uploadVideo(server.url, server.accessToken, {
37 category: 3, 37 name: 'video 0',
38 tags: [ 'coucou1', 'coucou2' ] 38 category: 3,
39 }) 39 tags: [ 'coucou1', 'coucou2' ]
40 } 40 })
41 41
42 const res = await getVideosOverview(server.url) 42 const res = await getVideosOverview(server.url, 1)
43 43
44 const overview: VideosOverview = res.body 44 const overview: VideosOverview = res.body
45 expect(overview.tags).to.have.lengthOf(0) 45 expect(overview.tags).to.have.lengthOf(0)
@@ -48,27 +48,55 @@ describe('Test a videos overview', function () {
48 }) 48 })
49 49
50 it('Should upload another video and include all videos in the overview', async function () { 50 it('Should upload another video and include all videos in the overview', async function () {
51 await uploadVideo(server.url, server.accessToken, { 51 this.timeout(15000)
52 name: 'video 5',
53 category: 3,
54 tags: [ 'coucou1', 'coucou2' ]
55 })
56 52
57 const res = await getVideosOverview(server.url) 53 for (let i = 1; i < 6; i++) {
54 await uploadVideo(server.url, server.accessToken, {
55 name: 'video ' + i,
56 category: 3,
57 tags: [ 'coucou1', 'coucou2' ]
58 })
59 }
58 60
59 const overview: VideosOverview = res.body 61 await wait(3000)
60 expect(overview.tags).to.have.lengthOf(2) 62
61 expect(overview.categories).to.have.lengthOf(1) 63 {
62 expect(overview.channels).to.have.lengthOf(1) 64 const res = await getVideosOverview(server.url, 1)
65
66 const overview: VideosOverview = res.body
67 expect(overview.tags).to.have.lengthOf(1)
68 expect(overview.categories).to.have.lengthOf(1)
69 expect(overview.channels).to.have.lengthOf(1)
70 }
71
72 {
73 const res = await getVideosOverview(server.url, 2)
74
75 const overview: VideosOverview = res.body
76 expect(overview.tags).to.have.lengthOf(1)
77 expect(overview.categories).to.have.lengthOf(0)
78 expect(overview.channels).to.have.lengthOf(0)
79 }
63 }) 80 })
64 81
65 it('Should have the correct overview', async function () { 82 it('Should have the correct overview', async function () {
66 const res = await getVideosOverview(server.url) 83 const res1 = await getVideosOverview(server.url, 1)
84 const res2 = await getVideosOverview(server.url, 2)
67 85
68 const overview: VideosOverview = res.body 86 const overview1: VideosOverview = res1.body
87 const overview2: VideosOverview = res2.body
88
89 const tmp = [
90 overview1.tags,
91 overview1.categories,
92 overview1.channels,
93 overview2.tags
94 ]
95
96 for (const arr of tmp) {
97 expect(arr).to.have.lengthOf(1)
69 98
70 for (const attr of [ 'tags', 'categories', 'channels' ]) { 99 const obj = arr[0]
71 const obj = overview[attr][0]
72 100
73 expect(obj.videos).to.have.lengthOf(6) 101 expect(obj.videos).to.have.lengthOf(6)
74 expect(obj.videos[0].name).to.equal('video 5') 102 expect(obj.videos[0].name).to.equal('video 5')
@@ -79,12 +107,13 @@ describe('Test a videos overview', function () {
79 expect(obj.videos[5].name).to.equal('video 0') 107 expect(obj.videos[5].name).to.equal('video 0')
80 } 108 }
81 109
82 expect(overview.tags.find(t => t.tag === 'coucou1')).to.not.be.undefined 110 const tags = [ overview1.tags[0].tag, overview2.tags[0].tag ]
83 expect(overview.tags.find(t => t.tag === 'coucou2')).to.not.be.undefined 111 expect(tags.find(t => t === 'coucou1')).to.not.be.undefined
112 expect(tags.find(t => t === 'coucou2')).to.not.be.undefined
84 113
85 expect(overview.categories[0].category.id).to.equal(3) 114 expect(overview1.categories[0].category.id).to.equal(3)
86 115
87 expect(overview.channels[0].channel.name).to.equal('root_channel') 116 expect(overview1.channels[0].channel.name).to.equal('root_channel')
88 }) 117 })
89 118
90 after(async function () { 119 after(async function () {
diff --git a/server/tests/api/videos/videos-views-cleaner.ts b/server/tests/api/videos/videos-views-cleaner.ts
index fbddd40f4..d063d7973 100644
--- a/server/tests/api/videos/videos-views-cleaner.ts
+++ b/server/tests/api/videos/videos-views-cleaner.ts
@@ -1,20 +1,22 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { 5import {
6 cleanupTests,
7 closeAllSequelize,
8 countVideoViewsOf,
9 doubleFollow,
6 flushAndRunMultipleServers, 10 flushAndRunMultipleServers,
7 flushTests,
8 killallServers, 11 killallServers,
9 reRunServer, 12 reRunServer,
10 flushAndRunServer,
11 ServerInfo, 13 ServerInfo,
12 setAccessTokensToServers, 14 setAccessTokensToServers,
13 uploadVideo, uploadVideoAndGetId, viewVideo, wait, countVideoViewsOf, doubleFollow, waitJobs, cleanupTests, closeAllSequelize 15 uploadVideoAndGetId,
16 viewVideo,
17 wait,
18 waitJobs
14} from '../../../../shared/extra-utils' 19} 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 20
19const expect = chai.expect 21const expect = chai.expect
20 22
diff --git a/server/tests/cli/create-import-video-file-job.ts b/server/tests/cli/create-import-video-file-job.ts
index aca3216bb..dac049fe4 100644
--- a/server/tests/cli/create-import-video-file-job.ts
+++ b/server/tests/cli/create-import-video-file-job.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
@@ -71,7 +71,7 @@ describe('Test create import video jobs', function () {
71 const videoDetail: VideoDetails = (await getVideo(server.url, video.uuid)).body 71 const videoDetail: VideoDetails = (await getVideo(server.url, video.uuid)).body
72 72
73 expect(videoDetail.files).to.have.lengthOf(2) 73 expect(videoDetail.files).to.have.lengthOf(2)
74 const [originalVideo, transcodedVideo] = videoDetail.files 74 const [ originalVideo, transcodedVideo ] = videoDetail.files
75 assertVideoProperties(originalVideo, 720, 'webm', 218910) 75 assertVideoProperties(originalVideo, 720, 'webm', 218910)
76 assertVideoProperties(transcodedVideo, 480, 'webm', 69217) 76 assertVideoProperties(transcodedVideo, 480, 'webm', 69217)
77 77
@@ -95,7 +95,7 @@ describe('Test create import video jobs', function () {
95 const videoDetail: VideoDetails = (await getVideo(server.url, video.uuid)).body 95 const videoDetail: VideoDetails = (await getVideo(server.url, video.uuid)).body
96 96
97 expect(videoDetail.files).to.have.lengthOf(4) 97 expect(videoDetail.files).to.have.lengthOf(4)
98 const [originalVideo, transcodedVideo420, transcodedVideo320, transcodedVideo240] = videoDetail.files 98 const [ originalVideo, transcodedVideo420, transcodedVideo320, transcodedVideo240 ] = videoDetail.files
99 assertVideoProperties(originalVideo, 720, 'ogv', 140849) 99 assertVideoProperties(originalVideo, 720, 'ogv', 140849)
100 assertVideoProperties(transcodedVideo420, 480, 'mp4') 100 assertVideoProperties(transcodedVideo420, 480, 'mp4')
101 assertVideoProperties(transcodedVideo320, 360, 'mp4') 101 assertVideoProperties(transcodedVideo320, 360, 'mp4')
diff --git a/server/tests/cli/create-transcoding-job.ts b/server/tests/cli/create-transcoding-job.ts
index 7897ff1b3..997a9a1fd 100644
--- a/server/tests/cli/create-transcoding-job.ts
+++ b/server/tests/cli/create-transcoding-job.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
@@ -8,14 +8,13 @@ import {
8 doubleFollow, 8 doubleFollow,
9 execCLI, 9 execCLI,
10 flushAndRunMultipleServers, 10 flushAndRunMultipleServers,
11 flushTests,
12 getEnvCli, 11 getEnvCli,
13 getVideo, 12 getVideo,
14 getVideosList, 13 getVideosList,
15 killallServers,
16 ServerInfo, 14 ServerInfo,
17 setAccessTokensToServers, updateCustomSubConfig, 15 setAccessTokensToServers,
18 uploadVideo, wait 16 updateCustomSubConfig,
17 uploadVideo
19} from '../../../shared/extra-utils' 18} from '../../../shared/extra-utils'
20import { waitJobs } from '../../../shared/extra-utils/server/jobs' 19import { waitJobs } from '../../../shared/extra-utils/server/jobs'
21 20
@@ -23,7 +22,7 @@ const expect = chai.expect
23 22
24describe('Test create transcoding jobs', function () { 23describe('Test create transcoding jobs', function () {
25 let servers: ServerInfo[] = [] 24 let servers: ServerInfo[] = []
26 let videosUUID: string[] = [] 25 const videosUUID: string[] = []
27 26
28 const config = { 27 const config = {
29 transcoding: { 28 transcoding: {
@@ -54,7 +53,7 @@ describe('Test create transcoding jobs', function () {
54 await doubleFollow(servers[0], servers[1]) 53 await doubleFollow(servers[0], servers[1])
55 54
56 for (let i = 1; i <= 5; i++) { 55 for (let i = 1; i <= 5; i++) {
57 const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'video' + i }) 56 const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video' + i })
58 videosUUID.push(res.body.video.uuid) 57 videosUUID.push(res.body.video.uuid)
59 } 58 }
60 59
@@ -90,7 +89,7 @@ describe('Test create transcoding jobs', function () {
90 const res = await getVideosList(server.url) 89 const res = await getVideosList(server.url)
91 const videos = res.body.data 90 const videos = res.body.data
92 91
93 let infoHashes: { [ id: number ]: string } 92 let infoHashes: { [id: number]: string }
94 93
95 for (const video of videos) { 94 for (const video of videos) {
96 const res2 = await getVideo(server.url, video.uuid) 95 const res2 = await getVideo(server.url, video.uuid)
diff --git a/server/tests/cli/optimize-old-videos.ts b/server/tests/cli/optimize-old-videos.ts
index de5d672f5..e2e13598f 100644
--- a/server/tests/cli/optimize-old-videos.ts
+++ b/server/tests/cli/optimize-old-videos.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
@@ -28,7 +28,9 @@ const expect = chai.expect
28 28
29describe('Test optimize old videos', function () { 29describe('Test optimize old videos', function () {
30 let servers: ServerInfo[] = [] 30 let servers: ServerInfo[] = []
31 // eslint-disable-next-line @typescript-eslint/no-unused-vars
31 let video1UUID: string 32 let video1UUID: string
33 // eslint-disable-next-line @typescript-eslint/no-unused-vars
32 let video2UUID: string 34 let video2UUID: string
33 35
34 before(async function () { 36 before(async function () {
diff --git a/server/tests/cli/peertube.ts b/server/tests/cli/peertube.ts
index b8c0b1f79..27fbde02d 100644
--- a/server/tests/cli/peertube.ts
+++ b/server/tests/cli/peertube.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4import { expect } from 'chai' 4import { expect } from 'chai'
@@ -7,14 +7,17 @@ import {
7 buildAbsoluteFixturePath, 7 buildAbsoluteFixturePath,
8 cleanupTests, 8 cleanupTests,
9 createUser, 9 createUser,
10 doubleFollow,
10 execCLI, 11 execCLI,
11 flushAndRunServer, 12 flushAndRunServer,
12 getEnvCli, 13 getEnvCli,
14 getLocalIdByUUID,
13 getVideo, 15 getVideo,
14 getVideosList, 16 getVideosList,
15 getVideosListWithToken, removeVideo, 17 removeVideo,
16 ServerInfo, 18 ServerInfo,
17 setAccessTokensToServers, 19 setAccessTokensToServers,
20 uploadVideoAndGetId,
18 userLogin, 21 userLogin,
19 waitJobs 22 waitJobs
20} from '../../../shared/extra-utils' 23} from '../../../shared/extra-utils'
@@ -101,7 +104,7 @@ describe('Test CLI wrapper', function () {
101 104
102 const videos: Video[] = res.body.data 105 const videos: Video[] = res.body.data
103 106
104 const video: VideoDetails = (await getVideo(server.url, videos[ 0 ].uuid)).body 107 const video: VideoDetails = (await getVideo(server.url, videos[0].uuid)).body
105 108
106 expect(video.name).to.equal('test upload') 109 expect(video.name).to.equal('test upload')
107 expect(video.support).to.equal('support_text') 110 expect(video.support).to.equal('support_text')
@@ -210,6 +213,81 @@ describe('Test CLI wrapper', function () {
210 }) 213 })
211 }) 214 })
212 215
216 describe('Manage video redundancies', function () {
217 let anotherServer: ServerInfo
218 let video1Server2: number
219 let servers: ServerInfo[]
220
221 before(async function () {
222 this.timeout(120000)
223
224 anotherServer = await flushAndRunServer(2)
225 await setAccessTokensToServers([ anotherServer ])
226
227 await doubleFollow(server, anotherServer)
228
229 servers = [ server, anotherServer ]
230 await waitJobs(servers)
231
232 const uuid = (await uploadVideoAndGetId({ server: anotherServer, videoName: 'super video' })).uuid
233 await waitJobs(servers)
234
235 video1Server2 = await getLocalIdByUUID(server.url, uuid)
236 })
237
238 it('Should add a redundancy', async function () {
239 this.timeout(60000)
240
241 const env = getEnvCli(server)
242
243 const params = `add --video ${video1Server2}`
244
245 await execCLI(`${env} ${cmd} redundancy ${params}`)
246
247 await waitJobs(servers)
248 })
249
250 it('Should list redundancies', async function () {
251 this.timeout(60000)
252
253 {
254 const env = getEnvCli(server)
255
256 const params = 'list-my-redundancies'
257 const stdout = await execCLI(`${env} ${cmd} redundancy ${params}`)
258
259 expect(stdout).to.contain('super video')
260 expect(stdout).to.contain(`localhost:${server.port}`)
261 }
262 })
263
264 it('Should remove a redundancy', async function () {
265 this.timeout(60000)
266
267 const env = getEnvCli(server)
268
269 const params = `remove --video ${video1Server2}`
270
271 await execCLI(`${env} ${cmd} redundancy ${params}`)
272
273 await waitJobs(servers)
274
275 {
276 const env = getEnvCli(server)
277 const params = 'list-my-redundancies'
278 const stdout = await execCLI(`${env} ${cmd} redundancy ${params}`)
279
280 expect(stdout).to.not.contain('super video')
281 }
282 })
283
284 after(async function () {
285 this.timeout(10000)
286
287 await cleanupTests([ anotherServer ])
288 })
289 })
290
213 after(async function () { 291 after(async function () {
214 this.timeout(10000) 292 this.timeout(10000)
215 293
diff --git a/server/tests/cli/plugins.ts b/server/tests/cli/plugins.ts
index a5257d671..7f19f14b7 100644
--- a/server/tests/cli/plugins.ts
+++ b/server/tests/cli/plugins.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4import { 4import {
diff --git a/server/tests/cli/prune-storage.ts b/server/tests/cli/prune-storage.ts
index 144e67c44..6cda80070 100644
--- a/server/tests/cli/prune-storage.ts
+++ b/server/tests/cli/prune-storage.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
@@ -11,18 +11,19 @@ import {
11 execCLI, 11 execCLI,
12 flushAndRunMultipleServers, 12 flushAndRunMultipleServers,
13 getAccount, 13 getAccount,
14 getEnvCli, makeGetRequest, makeRawRequest, 14 getEnvCli,
15 makeGetRequest,
15 ServerInfo, 16 ServerInfo,
16 setAccessTokensToServers, setDefaultVideoChannel, 17 setAccessTokensToServers,
18 setDefaultVideoChannel,
17 updateMyAvatar, 19 updateMyAvatar,
18 uploadVideo, 20 uploadVideo,
19 wait 21 wait
20} from '../../../shared/extra-utils' 22} from '../../../shared/extra-utils'
21import { Account, VideoPlaylistPrivacy } from '../../../shared/models' 23import { Account, VideoPlaylistPrivacy } from '../../../shared/models'
22import { createFile, readdir } from 'fs-extra' 24import { createFile, readdir } from 'fs-extra'
23import * as uuidv4 from 'uuid/v4' 25import { v4 as uuidv4 } from 'uuid'
24import { join } from 'path' 26import { join } from 'path'
25import * as request from 'supertest'
26 27
27const expect = chai.expect 28const expect = chai.expect
28 29
@@ -61,7 +62,7 @@ async function assertCountAreOkay (servers: ServerInfo[]) {
61 62
62describe('Test prune storage scripts', function () { 63describe('Test prune storage scripts', function () {
63 let servers: ServerInfo[] 64 let servers: ServerInfo[]
64 const badNames: { [ directory: string ]: string[] } = {} 65 const badNames: { [directory: string]: string[] } = {}
65 66
66 before(async function () { 67 before(async function () {
67 this.timeout(120000) 68 this.timeout(120000)
@@ -92,20 +93,20 @@ describe('Test prune storage scripts', function () {
92 93
93 // Lazy load the remote avatar 94 // Lazy load the remote avatar
94 { 95 {
95 const res = await getAccount(servers[ 0 ].url, 'root@localhost:' + servers[ 1 ].port) 96 const res = await getAccount(servers[0].url, 'root@localhost:' + servers[1].port)
96 const account: Account = res.body 97 const account: Account = res.body
97 await makeGetRequest({ 98 await makeGetRequest({
98 url: servers[ 0 ].url, 99 url: servers[0].url,
99 path: account.avatar.path, 100 path: account.avatar.path,
100 statusCodeExpected: 200 101 statusCodeExpected: 200
101 }) 102 })
102 } 103 }
103 104
104 { 105 {
105 const res = await getAccount(servers[ 1 ].url, 'root@localhost:' + servers[ 0 ].port) 106 const res = await getAccount(servers[1].url, 'root@localhost:' + servers[0].port)
106 const account: Account = res.body 107 const account: Account = res.body
107 await makeGetRequest({ 108 await makeGetRequest({
108 url: servers[ 1 ].url, 109 url: servers[1].url,
109 path: account.avatar.path, 110 path: account.avatar.path,
110 statusCodeExpected: 200 111 statusCodeExpected: 200
111 }) 112 })
diff --git a/server/tests/cli/update-host.ts b/server/tests/cli/update-host.ts
index 55c43b32f..2070f16f5 100644
--- a/server/tests/cli/update-host.ts
+++ b/server/tests/cli/update-host.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
diff --git a/server/tests/client.ts b/server/tests/client.ts
index 778dcd08e..670bc6701 100644
--- a/server/tests/client.ts
+++ b/server/tests/client.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
@@ -112,8 +112,7 @@ describe('Test a client controllers', function () {
112 it('Should have valid index html tags (title, description...)', async function () { 112 it('Should have valid index html tags (title, description...)', async function () {
113 const res = await makeHTMLRequest(server.url, '/videos/trending') 113 const res = await makeHTMLRequest(server.url, '/videos/trending')
114 114
115 const description = 'PeerTube, a federated (ActivityPub) video streaming platform using P2P (BitTorrent) directly in the web browser ' + 115 const description = 'PeerTube, an ActivityPub-federated video streaming platform using P2P directly in your web browser.'
116 'with WebTorrent and Angular.'
117 checkIndexTags(res.text, 'PeerTube', description, '') 116 checkIndexTags(res.text, 'PeerTube', description, '')
118 }) 117 })
119 118
diff --git a/server/tests/external-plugins/auth-ldap.ts b/server/tests/external-plugins/auth-ldap.ts
new file mode 100644
index 000000000..0f0a08532
--- /dev/null
+++ b/server/tests/external-plugins/auth-ldap.ts
@@ -0,0 +1,108 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4import { expect } from 'chai'
5import { User } from '@shared/models/users/user.model'
6import {
7 getMyUserInformation,
8 installPlugin,
9 setAccessTokensToServers,
10 uninstallPlugin,
11 updatePluginSettings,
12 uploadVideo,
13 userLogin
14} from '../../../shared/extra-utils'
15import { cleanupTests, flushAndRunServer, ServerInfo } from '../../../shared/extra-utils/server/servers'
16
17describe('Official plugin auth-ldap', function () {
18 let server: ServerInfo
19 let accessToken: string
20
21 before(async function () {
22 this.timeout(30000)
23
24 server = await flushAndRunServer(1)
25 await setAccessTokensToServers([ server ])
26
27 await installPlugin({
28 url: server.url,
29 accessToken: server.accessToken,
30 npmName: 'peertube-plugin-auth-ldap'
31 })
32 })
33
34 it('Should not login with without LDAP settings', async function () {
35 await userLogin(server, { username: 'fry', password: 'fry' }, 400)
36 })
37
38 it('Should not login with bad LDAP settings', async function () {
39 await updatePluginSettings({
40 url: server.url,
41 accessToken: server.accessToken,
42 npmName: 'peertube-plugin-auth-ldap',
43 settings: {
44 'bind-credentials': 'GoodNewsEveryone',
45 'bind-dn': 'cn=admin,dc=planetexpress,dc=com',
46 'insecure-tls': false,
47 'mail-property': 'mail',
48 'search-base': 'ou=people,dc=planetexpress,dc=com',
49 'search-filter': '(|(mail={{username}})(uid={{username}}))',
50 'url': 'ldap://ldap:390',
51 'username-property': 'uid'
52 }
53 })
54
55 await userLogin(server, { username: 'fry', password: 'fry' }, 400)
56 })
57
58 it('Should not login with good LDAP settings but wrong username/password', async function () {
59 await updatePluginSettings({
60 url: server.url,
61 accessToken: server.accessToken,
62 npmName: 'peertube-plugin-auth-ldap',
63 settings: {
64 'bind-credentials': 'GoodNewsEveryone',
65 'bind-dn': 'cn=admin,dc=planetexpress,dc=com',
66 'insecure-tls': false,
67 'mail-property': 'mail',
68 'search-base': 'ou=people,dc=planetexpress,dc=com',
69 'search-filter': '(|(mail={{username}})(uid={{username}}))',
70 'url': 'ldap://ldap:389',
71 'username-property': 'uid'
72 }
73 })
74
75 await userLogin(server, { username: 'fry', password: 'bad password' }, 400)
76 await userLogin(server, { username: 'fryr', password: 'fry' }, 400)
77 })
78
79 it('Should login with the appropriate username/password', async function () {
80 accessToken = await userLogin(server, { username: 'fry', password: 'fry' })
81 })
82
83 it('Should login with the appropriate email/password', async function () {
84 accessToken = await userLogin(server, { username: 'fry@planetexpress.com', password: 'fry' })
85 })
86
87 it('Should login get my profile', async function () {
88 const res = await getMyUserInformation(server.url, accessToken)
89 const body: User = res.body
90
91 expect(body.username).to.equal('fry')
92 expect(body.email).to.equal('fry@planetexpress.com')
93 })
94
95 it('Should upload a video', async function () {
96 await uploadVideo(server.url, accessToken, { name: 'my super video' })
97 })
98
99 it('Should not login if the plugin is uninstalled', async function () {
100 await uninstallPlugin({ url: server.url, accessToken: server.accessToken, npmName: 'peertube-plugin-auth-ldap' })
101
102 await userLogin(server, { username: 'fry@planetexpress.com', password: 'fry' }, 400)
103 })
104
105 after(async function () {
106 await cleanupTests([ server ])
107 })
108})
diff --git a/server/tests/external-plugins/auto-mute.ts b/server/tests/external-plugins/auto-mute.ts
new file mode 100644
index 000000000..bfdbee80a
--- /dev/null
+++ b/server/tests/external-plugins/auto-mute.ts
@@ -0,0 +1,243 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4import { expect } from 'chai'
5import {
6 addAccountToServerBlocklist,
7 addServerToAccountBlocklist,
8 removeAccountFromServerBlocklist
9} from '@shared/extra-utils/users/blocklist'
10import {
11 doubleFollow,
12 getVideosList,
13 installPlugin,
14 makeGetRequest,
15 MockBlocklist,
16 setAccessTokensToServers,
17 updatePluginSettings,
18 uploadVideoAndGetId,
19 wait
20} from '../../../shared/extra-utils'
21import {
22 cleanupTests,
23 flushAndRunMultipleServers,
24 killallServers,
25 reRunServer,
26 ServerInfo
27} from '../../../shared/extra-utils/server/servers'
28
29describe('Official plugin auto-mute', function () {
30 const autoMuteListPath = '/plugins/auto-mute/router/api/v1/mute-list'
31 let servers: ServerInfo[]
32 let blocklistServer: MockBlocklist
33
34 before(async function () {
35 this.timeout(30000)
36
37 servers = await flushAndRunMultipleServers(2)
38 await setAccessTokensToServers(servers)
39
40 for (const server of servers) {
41 await installPlugin({
42 url: server.url,
43 accessToken: server.accessToken,
44 npmName: 'peertube-plugin-auto-mute'
45 })
46 }
47
48 blocklistServer = new MockBlocklist()
49 await blocklistServer.initialize()
50
51 await uploadVideoAndGetId({ server: servers[0], videoName: 'video server 1' })
52 await uploadVideoAndGetId({ server: servers[1], videoName: 'video server 2' })
53
54 await doubleFollow(servers[0], servers[1])
55 })
56
57 it('Should update plugin settings', async function () {
58 await updatePluginSettings({
59 url: servers[0].url,
60 accessToken: servers[0].accessToken,
61 npmName: 'peertube-plugin-auto-mute',
62 settings: {
63 'blocklist-urls': 'http://localhost:42100/blocklist',
64 'check-seconds-interval': 1
65 }
66 })
67 })
68
69 it('Should add a server blocklist', async function () {
70 this.timeout(10000)
71
72 blocklistServer.replace({
73 data: [
74 {
75 value: 'localhost:' + servers[1].port
76 }
77 ]
78 })
79
80 await wait(2000)
81
82 const res = await getVideosList(servers[0].url)
83 expect(res.body.total).to.equal(1)
84 })
85
86 it('Should remove a server blocklist', async function () {
87 this.timeout(10000)
88
89 blocklistServer.replace({
90 data: [
91 {
92 value: 'localhost:' + servers[1].port,
93 action: 'remove'
94 }
95 ]
96 })
97
98 await wait(2000)
99
100 const res = await getVideosList(servers[0].url)
101 expect(res.body.total).to.equal(2)
102 })
103
104 it('Should add an account blocklist', async function () {
105 this.timeout(10000)
106
107 blocklistServer.replace({
108 data: [
109 {
110 value: 'root@localhost:' + servers[1].port
111 }
112 ]
113 })
114
115 await wait(2000)
116
117 const res = await getVideosList(servers[0].url)
118 expect(res.body.total).to.equal(1)
119 })
120
121 it('Should remove an account blocklist', async function () {
122 this.timeout(10000)
123
124 blocklistServer.replace({
125 data: [
126 {
127 value: 'root@localhost:' + servers[1].port,
128 action: 'remove'
129 }
130 ]
131 })
132
133 await wait(2000)
134
135 const res = await getVideosList(servers[0].url)
136 expect(res.body.total).to.equal(2)
137 })
138
139 it('Should auto mute an account, manually unmute it and do not remute it automatically', async function () {
140 this.timeout(20000)
141
142 const account = 'root@localhost:' + servers[1].port
143
144 blocklistServer.replace({
145 data: [
146 {
147 value: account,
148 updatedAt: new Date().toISOString()
149 }
150 ]
151 })
152
153 await wait(2000)
154
155 {
156 const res = await getVideosList(servers[0].url)
157 expect(res.body.total).to.equal(1)
158 }
159
160 await removeAccountFromServerBlocklist(servers[0].url, servers[0].accessToken, account)
161
162 {
163 const res = await getVideosList(servers[0].url)
164 expect(res.body.total).to.equal(2)
165 }
166
167 killallServers([ servers[0] ])
168 await reRunServer(servers[0])
169 await wait(2000)
170
171 {
172 const res = await getVideosList(servers[0].url)
173 expect(res.body.total).to.equal(2)
174 }
175 })
176
177 it('Should not expose the auto mute list', async function () {
178 await makeGetRequest({
179 url: servers[0].url,
180 path: '/plugins/auto-mute/router/api/v1/mute-list',
181 statusCodeExpected: 403
182 })
183 })
184
185 it('Should enable auto mute list', async function () {
186 await updatePluginSettings({
187 url: servers[0].url,
188 accessToken: servers[0].accessToken,
189 npmName: 'peertube-plugin-auto-mute',
190 settings: {
191 'blocklist-urls': '',
192 'check-seconds-interval': 1,
193 'expose-mute-list': true
194 }
195 })
196
197 await makeGetRequest({
198 url: servers[0].url,
199 path: '/plugins/auto-mute/router/api/v1/mute-list',
200 statusCodeExpected: 200
201 })
202 })
203
204 it('Should mute an account on server 1, and server 2 auto mutes it', async function () {
205 this.timeout(20000)
206
207 await updatePluginSettings({
208 url: servers[1].url,
209 accessToken: servers[1].accessToken,
210 npmName: 'peertube-plugin-auto-mute',
211 settings: {
212 'blocklist-urls': 'http://localhost:' + servers[0].port + autoMuteListPath,
213 'check-seconds-interval': 1,
214 'expose-mute-list': false
215 }
216 })
217
218 await addAccountToServerBlocklist(servers[0].url, servers[0].accessToken, 'root@localhost:' + servers[1].port)
219 await addServerToAccountBlocklist(servers[0].url, servers[0].accessToken, 'localhost:' + servers[1].port)
220
221 const res = await makeGetRequest({
222 url: servers[0].url,
223 path: '/plugins/auto-mute/router/api/v1/mute-list',
224 statusCodeExpected: 200
225 })
226
227 const data = res.body.data
228 expect(data).to.have.lengthOf(1)
229 expect(data[0].updatedAt).to.exist
230 expect(data[0].value).to.equal('root@localhost:' + servers[1].port)
231
232 await wait(2000)
233
234 for (const server of servers) {
235 const res = await getVideosList(server.url)
236 expect(res.body.total).to.equal(1)
237 }
238 })
239
240 after(async function () {
241 await cleanupTests(servers)
242 })
243})
diff --git a/server/tests/external-plugins/index.ts b/server/tests/external-plugins/index.ts
new file mode 100644
index 000000000..d17894c15
--- /dev/null
+++ b/server/tests/external-plugins/index.ts
@@ -0,0 +1,2 @@
1import './auth-ldap'
2import './auto-mute'
diff --git a/server/tests/feeds/feeds.ts b/server/tests/feeds/feeds.ts
index 437470327..7fac921a3 100644
--- a/server/tests/feeds/feeds.ts
+++ b/server/tests/feeds/feeds.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
@@ -19,6 +19,8 @@ import * as libxmljs from 'libxmljs'
19import { addVideoCommentThread } from '../../../shared/extra-utils/videos/video-comments' 19import { addVideoCommentThread } from '../../../shared/extra-utils/videos/video-comments'
20import { waitJobs } from '../../../shared/extra-utils/server/jobs' 20import { waitJobs } from '../../../shared/extra-utils/server/jobs'
21import { User } from '../../../shared/models/users' 21import { User } from '../../../shared/models/users'
22import { VideoPrivacy } from '@shared/models'
23import { addAccountToServerBlocklist } from '@shared/extra-utils/users/blocklist'
22 24
23chai.use(require('chai-xml')) 25chai.use(require('chai-xml'))
24chai.use(require('chai-json-schema')) 26chai.use(require('chai-json-schema'))
@@ -51,7 +53,7 @@ describe('Test syndication feeds', () => {
51 53
52 { 54 {
53 const attr = { username: 'john', password: 'password' } 55 const attr = { username: 'john', password: 'password' }
54 await createUser({ url: servers[ 0 ].url, accessToken: servers[ 0 ].accessToken, username: attr.username, password: attr.password }) 56 await createUser({ url: servers[0].url, accessToken: servers[0].accessToken, username: attr.username, password: attr.password })
55 userAccessToken = await userLogin(servers[0], attr) 57 userAccessToken = await userLogin(servers[0], attr)
56 58
57 const res = await getMyUserInformation(servers[0].url, userAccessToken) 59 const res = await getMyUserInformation(servers[0].url, userAccessToken)
@@ -61,7 +63,7 @@ describe('Test syndication feeds', () => {
61 } 63 }
62 64
63 { 65 {
64 await uploadVideo(servers[ 0 ].url, userAccessToken, { name: 'user video' }) 66 await uploadVideo(servers[0].url, userAccessToken, { name: 'user video' })
65 } 67 }
66 68
67 { 69 {
@@ -70,11 +72,19 @@ describe('Test syndication feeds', () => {
70 description: 'my super description for server 1', 72 description: 'my super description for server 1',
71 fixture: 'video_short.webm' 73 fixture: 'video_short.webm'
72 } 74 }
73 const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, videoAttributes) 75 const res = await uploadVideo(servers[0].url, servers[0].accessToken, videoAttributes)
74 const videoId = res.body.video.id 76 const videoId = res.body.video.id
75 77
76 await addVideoCommentThread(servers[ 0 ].url, servers[ 0 ].accessToken, videoId, 'super comment 1') 78 await addVideoCommentThread(servers[0].url, servers[0].accessToken, videoId, 'super comment 1')
77 await addVideoCommentThread(servers[ 0 ].url, servers[ 0 ].accessToken, videoId, 'super comment 2') 79 await addVideoCommentThread(servers[0].url, servers[0].accessToken, videoId, 'super comment 2')
80 }
81
82 {
83 const videoAttributes = { name: 'unlisted video', privacy: VideoPrivacy.UNLISTED }
84 const res = await uploadVideo(servers[0].url, servers[0].accessToken, videoAttributes)
85 const videoId = res.body.video.id
86
87 await addVideoCommentThread(servers[0].url, servers[0].accessToken, videoId, 'comment on unlisted video')
78 } 88 }
79 89
80 await waitJobs(servers) 90 await waitJobs(servers)
@@ -84,18 +94,18 @@ describe('Test syndication feeds', () => {
84 94
85 it('Should be well formed XML (covers RSS 2.0 and ATOM 1.0 endpoints)', async function () { 95 it('Should be well formed XML (covers RSS 2.0 and ATOM 1.0 endpoints)', async function () {
86 for (const feed of [ 'video-comments' as 'video-comments', 'videos' as 'videos' ]) { 96 for (const feed of [ 'video-comments' as 'video-comments', 'videos' as 'videos' ]) {
87 const rss = await getXMLfeed(servers[ 0 ].url, feed) 97 const rss = await getXMLfeed(servers[0].url, feed)
88 expect(rss.text).xml.to.be.valid() 98 expect(rss.text).xml.to.be.valid()
89 99
90 const atom = await getXMLfeed(servers[ 0 ].url, feed, 'atom') 100 const atom = await getXMLfeed(servers[0].url, feed, 'atom')
91 expect(atom.text).xml.to.be.valid() 101 expect(atom.text).xml.to.be.valid()
92 } 102 }
93 }) 103 })
94 104
95 it('Should be well formed JSON (covers JSON feed 1.0 endpoint)', async function () { 105 it('Should be well formed JSON (covers JSON feed 1.0 endpoint)', async function () {
96 for (const feed of [ 'video-comments' as 'video-comments', 'videos' as 'videos' ]) { 106 for (const feed of [ 'video-comments' as 'video-comments', 'videos' as 'videos' ]) {
97 const json = await getJSONfeed(servers[ 0 ].url, feed) 107 const json = await getJSONfeed(servers[0].url, feed)
98 expect(JSON.parse(json.text)).to.be.jsonSchema({ 'type': 'object' }) 108 expect(JSON.parse(json.text)).to.be.jsonSchema({ type: 'object' })
99 } 109 }
100 }) 110 })
101 }) 111 })
@@ -118,11 +128,11 @@ describe('Test syndication feeds', () => {
118 const json = await getJSONfeed(server.url, 'videos') 128 const json = await getJSONfeed(server.url, 'videos')
119 const jsonObj = JSON.parse(json.text) 129 const jsonObj = JSON.parse(json.text)
120 expect(jsonObj.items.length).to.be.equal(2) 130 expect(jsonObj.items.length).to.be.equal(2)
121 expect(jsonObj.items[ 0 ].attachments).to.exist 131 expect(jsonObj.items[0].attachments).to.exist
122 expect(jsonObj.items[ 0 ].attachments.length).to.be.eq(1) 132 expect(jsonObj.items[0].attachments.length).to.be.eq(1)
123 expect(jsonObj.items[ 0 ].attachments[ 0 ].mime_type).to.be.eq('application/x-bittorrent') 133 expect(jsonObj.items[0].attachments[0].mime_type).to.be.eq('application/x-bittorrent')
124 expect(jsonObj.items[ 0 ].attachments[ 0 ].size_in_bytes).to.be.eq(218910) 134 expect(jsonObj.items[0].attachments[0].size_in_bytes).to.be.eq(218910)
125 expect(jsonObj.items[ 0 ].attachments[ 0 ].url).to.contain('720.torrent') 135 expect(jsonObj.items[0].attachments[0].url).to.contain('720.torrent')
126 } 136 }
127 }) 137 })
128 138
@@ -131,16 +141,16 @@ describe('Test syndication feeds', () => {
131 const json = await getJSONfeed(servers[0].url, 'videos', { accountId: rootAccountId }) 141 const json = await getJSONfeed(servers[0].url, 'videos', { accountId: rootAccountId })
132 const jsonObj = JSON.parse(json.text) 142 const jsonObj = JSON.parse(json.text)
133 expect(jsonObj.items.length).to.be.equal(1) 143 expect(jsonObj.items.length).to.be.equal(1)
134 expect(jsonObj.items[ 0 ].title).to.equal('my super name for server 1') 144 expect(jsonObj.items[0].title).to.equal('my super name for server 1')
135 expect(jsonObj.items[ 0 ].author.name).to.equal('root') 145 expect(jsonObj.items[0].author.name).to.equal('root')
136 } 146 }
137 147
138 { 148 {
139 const json = await getJSONfeed(servers[0].url, 'videos', { accountId: userAccountId }) 149 const json = await getJSONfeed(servers[0].url, 'videos', { accountId: userAccountId })
140 const jsonObj = JSON.parse(json.text) 150 const jsonObj = JSON.parse(json.text)
141 expect(jsonObj.items.length).to.be.equal(1) 151 expect(jsonObj.items.length).to.be.equal(1)
142 expect(jsonObj.items[ 0 ].title).to.equal('user video') 152 expect(jsonObj.items[0].title).to.equal('user video')
143 expect(jsonObj.items[ 0 ].author.name).to.equal('john') 153 expect(jsonObj.items[0].author.name).to.equal('john')
144 } 154 }
145 155
146 for (const server of servers) { 156 for (const server of servers) {
@@ -148,14 +158,14 @@ describe('Test syndication feeds', () => {
148 const json = await getJSONfeed(server.url, 'videos', { accountName: 'root@localhost:' + servers[0].port }) 158 const json = await getJSONfeed(server.url, 'videos', { accountName: 'root@localhost:' + servers[0].port })
149 const jsonObj = JSON.parse(json.text) 159 const jsonObj = JSON.parse(json.text)
150 expect(jsonObj.items.length).to.be.equal(1) 160 expect(jsonObj.items.length).to.be.equal(1)
151 expect(jsonObj.items[ 0 ].title).to.equal('my super name for server 1') 161 expect(jsonObj.items[0].title).to.equal('my super name for server 1')
152 } 162 }
153 163
154 { 164 {
155 const json = await getJSONfeed(server.url, 'videos', { accountName: 'john@localhost:' + servers[0].port }) 165 const json = await getJSONfeed(server.url, 'videos', { accountName: 'john@localhost:' + servers[0].port })
156 const jsonObj = JSON.parse(json.text) 166 const jsonObj = JSON.parse(json.text)
157 expect(jsonObj.items.length).to.be.equal(1) 167 expect(jsonObj.items.length).to.be.equal(1)
158 expect(jsonObj.items[ 0 ].title).to.equal('user video') 168 expect(jsonObj.items[0].title).to.equal('user video')
159 } 169 }
160 } 170 }
161 }) 171 })
@@ -165,16 +175,16 @@ describe('Test syndication feeds', () => {
165 const json = await getJSONfeed(servers[0].url, 'videos', { videoChannelId: rootChannelId }) 175 const json = await getJSONfeed(servers[0].url, 'videos', { videoChannelId: rootChannelId })
166 const jsonObj = JSON.parse(json.text) 176 const jsonObj = JSON.parse(json.text)
167 expect(jsonObj.items.length).to.be.equal(1) 177 expect(jsonObj.items.length).to.be.equal(1)
168 expect(jsonObj.items[ 0 ].title).to.equal('my super name for server 1') 178 expect(jsonObj.items[0].title).to.equal('my super name for server 1')
169 expect(jsonObj.items[ 0 ].author.name).to.equal('root') 179 expect(jsonObj.items[0].author.name).to.equal('root')
170 } 180 }
171 181
172 { 182 {
173 const json = await getJSONfeed(servers[0].url, 'videos', { videoChannelId: userChannelId }) 183 const json = await getJSONfeed(servers[0].url, 'videos', { videoChannelId: userChannelId })
174 const jsonObj = JSON.parse(json.text) 184 const jsonObj = JSON.parse(json.text)
175 expect(jsonObj.items.length).to.be.equal(1) 185 expect(jsonObj.items.length).to.be.equal(1)
176 expect(jsonObj.items[ 0 ].title).to.equal('user video') 186 expect(jsonObj.items[0].title).to.equal('user video')
177 expect(jsonObj.items[ 0 ].author.name).to.equal('john') 187 expect(jsonObj.items[0].author.name).to.equal('john')
178 } 188 }
179 189
180 for (const server of servers) { 190 for (const server of servers) {
@@ -182,30 +192,42 @@ describe('Test syndication feeds', () => {
182 const json = await getJSONfeed(server.url, 'videos', { videoChannelName: 'root_channel@localhost:' + servers[0].port }) 192 const json = await getJSONfeed(server.url, 'videos', { videoChannelName: 'root_channel@localhost:' + servers[0].port })
183 const jsonObj = JSON.parse(json.text) 193 const jsonObj = JSON.parse(json.text)
184 expect(jsonObj.items.length).to.be.equal(1) 194 expect(jsonObj.items.length).to.be.equal(1)
185 expect(jsonObj.items[ 0 ].title).to.equal('my super name for server 1') 195 expect(jsonObj.items[0].title).to.equal('my super name for server 1')
186 } 196 }
187 197
188 { 198 {
189 const json = await getJSONfeed(server.url, 'videos', { videoChannelName: 'john_channel@localhost:' + servers[0].port }) 199 const json = await getJSONfeed(server.url, 'videos', { videoChannelName: 'john_channel@localhost:' + servers[0].port })
190 const jsonObj = JSON.parse(json.text) 200 const jsonObj = JSON.parse(json.text)
191 expect(jsonObj.items.length).to.be.equal(1) 201 expect(jsonObj.items.length).to.be.equal(1)
192 expect(jsonObj.items[ 0 ].title).to.equal('user video') 202 expect(jsonObj.items[0].title).to.equal('user video')
193 } 203 }
194 } 204 }
195 }) 205 })
196 }) 206 })
197 207
198 describe('Video comments feed', function () { 208 describe('Video comments feed', function () {
199 it('Should contain valid comments (covers JSON feed 1.0 endpoint)', async function () { 209
210 it('Should contain valid comments (covers JSON feed 1.0 endpoint) and not from unlisted videos', async function () {
200 for (const server of servers) { 211 for (const server of servers) {
201 const json = await getJSONfeed(server.url, 'video-comments') 212 const json = await getJSONfeed(server.url, 'video-comments')
202 213
203 const jsonObj = JSON.parse(json.text) 214 const jsonObj = JSON.parse(json.text)
204 expect(jsonObj.items.length).to.be.equal(2) 215 expect(jsonObj.items.length).to.be.equal(2)
205 expect(jsonObj.items[ 0 ].html_content).to.equal('super comment 2') 216 expect(jsonObj.items[0].html_content).to.equal('super comment 2')
206 expect(jsonObj.items[ 1 ].html_content).to.equal('super comment 1') 217 expect(jsonObj.items[1].html_content).to.equal('super comment 1')
207 } 218 }
208 }) 219 })
220
221 it('Should not list comments from muted accounts or instances', async function () {
222 await addAccountToServerBlocklist(servers[1].url, servers[1].accessToken, 'root@localhost:' + servers[0].port)
223
224 {
225 const json = await getJSONfeed(servers[1].url, 'video-comments', { version: 2 })
226 const jsonObj = JSON.parse(json.text)
227 expect(jsonObj.items.length).to.be.equal(0)
228 }
229
230 })
209 }) 231 })
210 232
211 after(async function () { 233 after(async function () {
diff --git a/server/tests/fixtures/peertube-plugin-test-external-auth-one/main.js b/server/tests/fixtures/peertube-plugin-test-external-auth-one/main.js
new file mode 100644
index 000000000..c65b8d3a8
--- /dev/null
+++ b/server/tests/fixtures/peertube-plugin-test-external-auth-one/main.js
@@ -0,0 +1,75 @@
1async function register ({
2 registerExternalAuth,
3 peertubeHelpers,
4 settingsManager,
5 unregisterExternalAuth
6}) {
7 {
8 const result = registerExternalAuth({
9 authName: 'external-auth-1',
10 authDisplayName: () => 'External Auth 1',
11 onLogout: user => peertubeHelpers.logger.info('On logout %s', user.username),
12 onAuthRequest: (req, res) => {
13 const username = req.query.username
14
15 result.userAuthenticated({
16 req,
17 res,
18 username,
19 email: username + '@example.com'
20 })
21 }
22 })
23 }
24
25 {
26 const result = registerExternalAuth({
27 authName: 'external-auth-2',
28 authDisplayName: () => 'External Auth 2',
29 onAuthRequest: (req, res) => {
30 result.userAuthenticated({
31 req,
32 res,
33 username: 'kefka',
34 email: 'kefka@example.com',
35 role: 0,
36 displayName: 'Kefka Palazzo'
37 })
38 },
39 hookTokenValidity: (options) => {
40 if (options.type === 'refresh') {
41 return { valid: false }
42 }
43
44 if (options.type === 'access') {
45 const token = options.token
46 const now = new Date()
47 now.setTime(now.getTime() - 5000)
48
49 const createdAt = new Date(token.createdAt)
50
51 return { valid: createdAt.getTime() >= now.getTime() }
52 }
53
54 return { valid: true }
55 }
56 })
57 }
58
59 settingsManager.onSettingsChange(settings => {
60 if (settings.disableKefka) {
61 unregisterExternalAuth('external-auth-2')
62 }
63 })
64}
65
66async function unregister () {
67 return
68}
69
70module.exports = {
71 register,
72 unregister
73}
74
75// ###########################################################################
diff --git a/server/tests/fixtures/peertube-plugin-test-external-auth-one/package.json b/server/tests/fixtures/peertube-plugin-test-external-auth-one/package.json
new file mode 100644
index 000000000..22814b047
--- /dev/null
+++ b/server/tests/fixtures/peertube-plugin-test-external-auth-one/package.json
@@ -0,0 +1,20 @@
1{
2 "name": "peertube-plugin-test-external-auth-one",
3 "version": "0.0.1",
4 "description": "External auth one",
5 "engine": {
6 "peertube": ">=1.3.0"
7 },
8 "keywords": [
9 "peertube",
10 "plugin"
11 ],
12 "homepage": "https://github.com/Chocobozzz/PeerTube",
13 "author": "Chocobozzz",
14 "bugs": "https://github.com/Chocobozzz/PeerTube/issues",
15 "library": "./main.js",
16 "staticDirs": {},
17 "css": [],
18 "clientScripts": [],
19 "translations": {}
20}
diff --git a/server/tests/fixtures/peertube-plugin-test-external-auth-two/main.js b/server/tests/fixtures/peertube-plugin-test-external-auth-two/main.js
new file mode 100644
index 000000000..126905ffc
--- /dev/null
+++ b/server/tests/fixtures/peertube-plugin-test-external-auth-two/main.js
@@ -0,0 +1,31 @@
1async function register ({
2 registerExternalAuth,
3 peertubeHelpers
4}) {
5 {
6 const result = registerExternalAuth({
7 authName: 'external-auth-3',
8 authDisplayName: () => 'External Auth 3',
9 onAuthRequest: (req, res) => {
10 result.userAuthenticated({
11 req,
12 res,
13 username: 'cid',
14 email: 'cid@example.com',
15 displayName: 'Cid Marquez'
16 })
17 }
18 })
19 }
20}
21
22async function unregister () {
23 return
24}
25
26module.exports = {
27 register,
28 unregister
29}
30
31// ###########################################################################
diff --git a/server/tests/fixtures/peertube-plugin-test-external-auth-two/package.json b/server/tests/fixtures/peertube-plugin-test-external-auth-two/package.json
new file mode 100644
index 000000000..a5ca4d07a
--- /dev/null
+++ b/server/tests/fixtures/peertube-plugin-test-external-auth-two/package.json
@@ -0,0 +1,20 @@
1{
2 "name": "peertube-plugin-test-external-auth-two",
3 "version": "0.0.1",
4 "description": "External auth two",
5 "engine": {
6 "peertube": ">=1.3.0"
7 },
8 "keywords": [
9 "peertube",
10 "plugin"
11 ],
12 "homepage": "https://github.com/Chocobozzz/PeerTube",
13 "author": "Chocobozzz",
14 "bugs": "https://github.com/Chocobozzz/PeerTube/issues",
15 "library": "./main.js",
16 "staticDirs": {},
17 "css": [],
18 "clientScripts": [],
19 "translations": {}
20}
diff --git a/server/tests/fixtures/peertube-plugin-test-five/main.js b/server/tests/fixtures/peertube-plugin-test-five/main.js
new file mode 100644
index 000000000..c1435b928
--- /dev/null
+++ b/server/tests/fixtures/peertube-plugin-test-five/main.js
@@ -0,0 +1,21 @@
1async function register ({
2 getRouter
3}) {
4 const router = getRouter()
5 router.get('/ping', (req, res) => res.json({ message: 'pong' }))
6
7 router.post('/form/post/mirror', (req, res) => {
8 res.json(req.body)
9 })
10}
11
12async function unregister () {
13 return
14}
15
16module.exports = {
17 register,
18 unregister
19}
20
21// ###########################################################################
diff --git a/server/tests/fixtures/peertube-plugin-test-five/package.json b/server/tests/fixtures/peertube-plugin-test-five/package.json
new file mode 100644
index 000000000..1f5d65d9d
--- /dev/null
+++ b/server/tests/fixtures/peertube-plugin-test-five/package.json
@@ -0,0 +1,20 @@
1{
2 "name": "peertube-plugin-test-five",
3 "version": "0.0.1",
4 "description": "Plugin test 5",
5 "engine": {
6 "peertube": ">=1.3.0"
7 },
8 "keywords": [
9 "peertube",
10 "plugin"
11 ],
12 "homepage": "https://github.com/Chocobozzz/PeerTube",
13 "author": "Chocobozzz",
14 "bugs": "https://github.com/Chocobozzz/PeerTube/issues",
15 "library": "./main.js",
16 "staticDirs": {},
17 "css": [],
18 "clientScripts": [],
19 "translations": {}
20}
diff --git a/server/tests/fixtures/peertube-plugin-test-four/main.js b/server/tests/fixtures/peertube-plugin-test-four/main.js
new file mode 100644
index 000000000..067c3fe15
--- /dev/null
+++ b/server/tests/fixtures/peertube-plugin-test-four/main.js
@@ -0,0 +1,114 @@
1async function register ({
2 peertubeHelpers,
3 registerHook,
4 getRouter
5}) {
6 const logger = peertubeHelpers.logger
7
8 logger.info('Hello world from plugin four')
9
10 {
11 const username = 'root'
12 const results = await peertubeHelpers.database.query(
13 'SELECT "email" from "user" WHERE "username" = $username',
14 {
15 type: 'SELECT',
16 bind: { username }
17 }
18 )
19
20 logger.info('root email is ' + results[0]['email'])
21 }
22
23 {
24 registerHook({
25 target: 'action:api.video.viewed',
26 handler: async ({ video }) => {
27 const videoFromDB = await peertubeHelpers.videos.loadByUrl(video.url)
28 logger.info('video from DB uuid is %s.', videoFromDB.uuid)
29
30 await peertubeHelpers.videos.removeVideo(video.id)
31
32 logger.info('Video deleted by plugin four.')
33 }
34 })
35 }
36
37 {
38 const serverActor = await peertubeHelpers.server.getServerActor()
39 logger.info('server actor name is %s', serverActor.preferredUsername)
40 }
41
42 {
43 logger.info('server url is %s', peertubeHelpers.config.getWebserverUrl())
44 }
45
46 {
47 const actions = {
48 blockServer,
49 unblockServer,
50 blockAccount,
51 unblockAccount,
52 blacklist,
53 unblacklist
54 }
55
56 const router = getRouter()
57 router.post('/commander', async (req, res) => {
58 try {
59 await actions[req.body.command](peertubeHelpers, req.body)
60
61 res.sendStatus(204)
62 } catch (err) {
63 logger.error('Error in commander.', { err })
64 res.sendStatus(500)
65 }
66 })
67 }
68}
69
70async function unregister () {
71 return
72}
73
74module.exports = {
75 register,
76 unregister
77}
78
79// ###########################################################################
80
81async function blockServer (peertubeHelpers, body) {
82 const serverActor = await peertubeHelpers.server.getServerActor()
83
84 await peertubeHelpers.moderation.blockServer({ byAccountId: serverActor.Account.id, hostToBlock: body.hostToBlock })
85}
86
87async function unblockServer (peertubeHelpers, body) {
88 const serverActor = await peertubeHelpers.server.getServerActor()
89
90 await peertubeHelpers.moderation.unblockServer({ byAccountId: serverActor.Account.id, hostToUnblock: body.hostToUnblock })
91}
92
93async function blockAccount (peertubeHelpers, body) {
94 const serverActor = await peertubeHelpers.server.getServerActor()
95
96 await peertubeHelpers.moderation.blockAccount({ byAccountId: serverActor.Account.id, handleToBlock: body.handleToBlock })
97}
98
99async function unblockAccount (peertubeHelpers, body) {
100 const serverActor = await peertubeHelpers.server.getServerActor()
101
102 await peertubeHelpers.moderation.unblockAccount({ byAccountId: serverActor.Account.id, handleToUnblock: body.handleToUnblock })
103}
104
105async function blacklist (peertubeHelpers, body) {
106 await peertubeHelpers.moderation.blacklistVideo({
107 videoIdOrUUID: body.videoUUID,
108 createOptions: body
109 })
110}
111
112async function unblacklist (peertubeHelpers, body) {
113 await peertubeHelpers.moderation.unblacklistVideo({ videoIdOrUUID: body.videoUUID })
114}
diff --git a/server/tests/fixtures/peertube-plugin-test-four/package.json b/server/tests/fixtures/peertube-plugin-test-four/package.json
new file mode 100644
index 000000000..dda3c7f37
--- /dev/null
+++ b/server/tests/fixtures/peertube-plugin-test-four/package.json
@@ -0,0 +1,20 @@
1{
2 "name": "peertube-plugin-test-four",
3 "version": "0.0.1",
4 "description": "Plugin test 4",
5 "engine": {
6 "peertube": ">=1.3.0"
7 },
8 "keywords": [
9 "peertube",
10 "plugin"
11 ],
12 "homepage": "https://github.com/Chocobozzz/PeerTube",
13 "author": "Chocobozzz",
14 "bugs": "https://github.com/Chocobozzz/PeerTube/issues",
15 "library": "./main.js",
16 "staticDirs": {},
17 "css": [],
18 "clientScripts": [],
19 "translations": {}
20}
diff --git a/server/tests/fixtures/peertube-plugin-test-id-pass-auth-one/main.js b/server/tests/fixtures/peertube-plugin-test-id-pass-auth-one/main.js
new file mode 100644
index 000000000..f58faa847
--- /dev/null
+++ b/server/tests/fixtures/peertube-plugin-test-id-pass-auth-one/main.js
@@ -0,0 +1,69 @@
1async function register ({
2 registerIdAndPassAuth,
3 peertubeHelpers,
4 settingsManager,
5 unregisterIdAndPassAuth
6}) {
7 registerIdAndPassAuth({
8 authName: 'spyro-auth',
9
10 onLogout: () => {
11 peertubeHelpers.logger.info('On logout for auth 1 - 1')
12 },
13
14 getWeight: () => 15,
15
16 login (body) {
17 if (body.id === 'spyro' && body.password === 'spyro password') {
18 return Promise.resolve({
19 username: 'spyro',
20 email: 'spyro@example.com',
21 role: 2,
22 displayName: 'Spyro the Dragon'
23 })
24 }
25
26 return null
27 }
28 })
29
30 registerIdAndPassAuth({
31 authName: 'crash-auth',
32
33 onLogout: () => {
34 peertubeHelpers.logger.info('On logout for auth 1 - 2')
35 },
36
37 getWeight: () => 50,
38
39 login (body) {
40 if (body.id === 'crash' && body.password === 'crash password') {
41 return Promise.resolve({
42 username: 'crash',
43 email: 'crash@example.com',
44 role: 1,
45 displayName: 'Crash Bandicoot'
46 })
47 }
48
49 return null
50 }
51 })
52
53 settingsManager.onSettingsChange(settings => {
54 if (settings.disableSpyro) {
55 unregisterIdAndPassAuth('spyro-auth')
56 }
57 })
58}
59
60async function unregister () {
61 return
62}
63
64module.exports = {
65 register,
66 unregister
67}
68
69// ###########################################################################
diff --git a/server/tests/fixtures/peertube-plugin-test-id-pass-auth-one/package.json b/server/tests/fixtures/peertube-plugin-test-id-pass-auth-one/package.json
new file mode 100644
index 000000000..f8ad18a90
--- /dev/null
+++ b/server/tests/fixtures/peertube-plugin-test-id-pass-auth-one/package.json
@@ -0,0 +1,20 @@
1{
2 "name": "peertube-plugin-test-id-pass-auth-one",
3 "version": "0.0.1",
4 "description": "Id and pass auth one",
5 "engine": {
6 "peertube": ">=1.3.0"
7 },
8 "keywords": [
9 "peertube",
10 "plugin"
11 ],
12 "homepage": "https://github.com/Chocobozzz/PeerTube",
13 "author": "Chocobozzz",
14 "bugs": "https://github.com/Chocobozzz/PeerTube/issues",
15 "library": "./main.js",
16 "staticDirs": {},
17 "css": [],
18 "clientScripts": [],
19 "translations": {}
20}
diff --git a/server/tests/fixtures/peertube-plugin-test-id-pass-auth-three/main.js b/server/tests/fixtures/peertube-plugin-test-id-pass-auth-three/main.js
new file mode 100644
index 000000000..caa6a7ccd
--- /dev/null
+++ b/server/tests/fixtures/peertube-plugin-test-id-pass-auth-three/main.js
@@ -0,0 +1,106 @@
1async function register ({
2 registerIdAndPassAuth,
3 peertubeHelpers
4}) {
5 registerIdAndPassAuth({
6 authName: 'laguna-bad-auth',
7
8 onLogout: () => {
9 peertubeHelpers.logger.info('On logout for auth 3 - 1')
10 },
11
12 getWeight: () => 5,
13
14 login (body) {
15 if (body.id === 'laguna' && body.password === 'laguna password') {
16 return Promise.resolve({
17 username: 'laguna',
18 email: 'laguna@example.com',
19 displayName: 'Laguna Loire'
20 })
21 }
22
23 return null
24 }
25 })
26
27 registerIdAndPassAuth({
28 authName: 'ward-auth',
29
30 getWeight: () => 5,
31
32 login (body) {
33 if (body.id === 'ward') {
34 return Promise.resolve({
35 username: 'ward-42',
36 email: 'ward@example.com'
37 })
38 }
39
40 return null
41 }
42 })
43
44 registerIdAndPassAuth({
45 authName: 'kiros-auth',
46
47 getWeight: () => 5,
48
49 login (body) {
50 if (body.id === 'kiros') {
51 return Promise.resolve({
52 username: 'kiros',
53 email: 'kiros@example.com',
54 displayName: 'a'.repeat(5000)
55 })
56 }
57
58 return null
59 }
60 })
61
62 registerIdAndPassAuth({
63 authName: 'raine-auth',
64
65 getWeight: () => 5,
66
67 login (body) {
68 if (body.id === 'raine') {
69 return Promise.resolve({
70 username: 'raine',
71 email: 'raine@example.com',
72 role: 42
73 })
74 }
75
76 return null
77 }
78 })
79
80 registerIdAndPassAuth({
81 authName: 'ellone-auth',
82
83 getWeight: () => 5,
84
85 login (body) {
86 if (body.id === 'ellone') {
87 return Promise.resolve({
88 username: 'ellone'
89 })
90 }
91
92 return null
93 }
94 })
95}
96
97async function unregister () {
98 return
99}
100
101module.exports = {
102 register,
103 unregister
104}
105
106// ###########################################################################
diff --git a/server/tests/fixtures/peertube-plugin-test-id-pass-auth-three/package.json b/server/tests/fixtures/peertube-plugin-test-id-pass-auth-three/package.json
new file mode 100644
index 000000000..f9f107b1a
--- /dev/null
+++ b/server/tests/fixtures/peertube-plugin-test-id-pass-auth-three/package.json
@@ -0,0 +1,20 @@
1{
2 "name": "peertube-plugin-test-id-pass-auth-three",
3 "version": "0.0.1",
4 "description": "Id and pass auth three",
5 "engine": {
6 "peertube": ">=1.3.0"
7 },
8 "keywords": [
9 "peertube",
10 "plugin"
11 ],
12 "homepage": "https://github.com/Chocobozzz/PeerTube",
13 "author": "Chocobozzz",
14 "bugs": "https://github.com/Chocobozzz/PeerTube/issues",
15 "library": "./main.js",
16 "staticDirs": {},
17 "css": [],
18 "clientScripts": [],
19 "translations": {}
20}
diff --git a/server/tests/fixtures/peertube-plugin-test-id-pass-auth-two/main.js b/server/tests/fixtures/peertube-plugin-test-id-pass-auth-two/main.js
new file mode 100644
index 000000000..ceab7b60d
--- /dev/null
+++ b/server/tests/fixtures/peertube-plugin-test-id-pass-auth-two/main.js
@@ -0,0 +1,54 @@
1async function register ({
2 registerIdAndPassAuth,
3 peertubeHelpers
4}) {
5 registerIdAndPassAuth({
6 authName: 'laguna-auth',
7
8 onLogout: () => {
9 peertubeHelpers.logger.info('On logout for auth 2 - 1')
10 },
11
12 getWeight: () => 30,
13
14 hookTokenValidity: (options) => {
15 if (options.type === 'refresh') {
16 return { valid: false }
17 }
18
19 if (options.type === 'access') {
20 const token = options.token
21 const now = new Date()
22 now.setTime(now.getTime() - 5000)
23
24 const createdAt = new Date(token.createdAt)
25
26 return { valid: createdAt.getTime() >= now.getTime() }
27 }
28
29 return { valid: true }
30 },
31
32 login (body) {
33 if (body.id === 'laguna' && body.password === 'laguna password') {
34 return Promise.resolve({
35 username: 'laguna',
36 email: 'laguna@example.com'
37 })
38 }
39
40 return null
41 }
42 })
43}
44
45async function unregister () {
46 return
47}
48
49module.exports = {
50 register,
51 unregister
52}
53
54// ###########################################################################
diff --git a/server/tests/fixtures/peertube-plugin-test-id-pass-auth-two/package.json b/server/tests/fixtures/peertube-plugin-test-id-pass-auth-two/package.json
new file mode 100644
index 000000000..5df15fac1
--- /dev/null
+++ b/server/tests/fixtures/peertube-plugin-test-id-pass-auth-two/package.json
@@ -0,0 +1,20 @@
1{
2 "name": "peertube-plugin-test-id-pass-auth-two",
3 "version": "0.0.1",
4 "description": "Id and pass auth two",
5 "engine": {
6 "peertube": ">=1.3.0"
7 },
8 "keywords": [
9 "peertube",
10 "plugin"
11 ],
12 "homepage": "https://github.com/Chocobozzz/PeerTube",
13 "author": "Chocobozzz",
14 "bugs": "https://github.com/Chocobozzz/PeerTube/issues",
15 "library": "./main.js",
16 "staticDirs": {},
17 "css": [],
18 "clientScripts": [],
19 "translations": {}
20}
diff --git a/server/tests/fixtures/peertube-plugin-test-six/main.js b/server/tests/fixtures/peertube-plugin-test-six/main.js
new file mode 100644
index 000000000..bb9aaffa7
--- /dev/null
+++ b/server/tests/fixtures/peertube-plugin-test-six/main.js
@@ -0,0 +1,25 @@
1async function register ({
2 storageManager,
3 peertubeHelpers
4}) {
5 const { logger } = peertubeHelpers
6
7 {
8 await storageManager.storeData('superkey', { value: 'toto' })
9 await storageManager.storeData('anotherkey', { value: 'toto2' })
10
11 const result = await storageManager.getData('superkey')
12 logger.info('superkey stored value is %s', result.value)
13 }
14}
15
16async function unregister () {
17 return
18}
19
20module.exports = {
21 register,
22 unregister
23}
24
25// ###########################################################################
diff --git a/server/tests/fixtures/peertube-plugin-test-six/package.json b/server/tests/fixtures/peertube-plugin-test-six/package.json
new file mode 100644
index 000000000..8c97826b0
--- /dev/null
+++ b/server/tests/fixtures/peertube-plugin-test-six/package.json
@@ -0,0 +1,20 @@
1{
2 "name": "peertube-plugin-test-six",
3 "version": "0.0.1",
4 "description": "Plugin test 6",
5 "engine": {
6 "peertube": ">=1.3.0"
7 },
8 "keywords": [
9 "peertube",
10 "plugin"
11 ],
12 "homepage": "https://github.com/Chocobozzz/PeerTube",
13 "author": "Chocobozzz",
14 "bugs": "https://github.com/Chocobozzz/PeerTube/issues",
15 "library": "./main.js",
16 "staticDirs": {},
17 "css": [],
18 "clientScripts": [],
19 "translations": {}
20}
diff --git a/server/tests/fixtures/peertube-plugin-test-three/main.js b/server/tests/fixtures/peertube-plugin-test-three/main.js
index 4945feb55..f2b89bcf0 100644
--- a/server/tests/fixtures/peertube-plugin-test-three/main.js
+++ b/server/tests/fixtures/peertube-plugin-test-three/main.js
@@ -5,7 +5,9 @@ async function register ({
5 storageManager, 5 storageManager,
6 videoCategoryManager, 6 videoCategoryManager,
7 videoLicenceManager, 7 videoLicenceManager,
8 videoLanguageManager 8 videoLanguageManager,
9 videoPrivacyManager,
10 playlistPrivacyManager
9}) { 11}) {
10 videoLanguageManager.addLanguage('al_bhed', 'Al Bhed') 12 videoLanguageManager.addLanguage('al_bhed', 'Al Bhed')
11 videoLanguageManager.addLanguage('al_bhed2', 'Al Bhed 2') 13 videoLanguageManager.addLanguage('al_bhed2', 'Al Bhed 2')
@@ -21,6 +23,9 @@ async function register ({
21 videoLicenceManager.addLicence(43, 'High best licence') 23 videoLicenceManager.addLicence(43, 'High best licence')
22 videoLicenceManager.deleteLicence(1) // Attribution 24 videoLicenceManager.deleteLicence(1) // Attribution
23 videoLicenceManager.deleteLicence(7) // Public domain 25 videoLicenceManager.deleteLicence(7) // Public domain
26
27 videoPrivacyManager.deletePrivacy(2)
28 playlistPrivacyManager.deletePlaylistPrivacy(3)
24} 29}
25 30
26async function unregister () { 31async function unregister () {
diff --git a/server/tests/fixtures/video_import_preview.jpg b/server/tests/fixtures/video_import_preview.jpg
new file mode 100644
index 000000000..1f8d1d91d
--- /dev/null
+++ b/server/tests/fixtures/video_import_preview.jpg
Binary files differ
diff --git a/server/tests/fixtures/video_import_thumbnail.jpg b/server/tests/fixtures/video_import_thumbnail.jpg
new file mode 100644
index 000000000..fcc50b75f
--- /dev/null
+++ b/server/tests/fixtures/video_import_thumbnail.jpg
Binary files differ
diff --git a/server/tests/helpers/comment-model.ts b/server/tests/helpers/comment-model.ts
index ebfd779e1..4c51b7000 100644
--- a/server/tests/helpers/comment-model.ts
+++ b/server/tests/helpers/comment-model.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
@@ -20,7 +20,7 @@ describe('Comment model', function () {
20 20
21 comment.text = '@florian @jean@localhost:9000 @flo @another@localhost:9000 @flo2@jean.com hello ' + 21 comment.text = '@florian @jean@localhost:9000 @flo @another@localhost:9000 @flo2@jean.com hello ' +
22 'email@localhost:9000 coucou.com no? @chocobozzz @chocobozzz @end' 22 'email@localhost:9000 coucou.com no? @chocobozzz @chocobozzz @end'
23 const result = comment.extractMentions().sort() 23 const result = comment.extractMentions().sort((a, b) => a.localeCompare(b))
24 24
25 expect(result).to.deep.equal([ 'another', 'chocobozzz', 'end', 'flo', 'florian', 'jean' ]) 25 expect(result).to.deep.equal([ 'another', 'chocobozzz', 'end', 'flo', 'florian', 'jean' ])
26 }) 26 })
diff --git a/server/tests/helpers/core-utils.ts b/server/tests/helpers/core-utils.ts
index 31fc6dd7c..c028b316d 100644
--- a/server/tests/helpers/core-utils.ts
+++ b/server/tests/helpers/core-utils.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
diff --git a/server/tests/helpers/request.ts b/server/tests/helpers/request.ts
index a754bc6e2..f8b2d599b 100644
--- a/server/tests/helpers/request.ts
+++ b/server/tests/helpers/request.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests' 4import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
diff --git a/server/tests/index.ts b/server/tests/index.ts
index 8bddcfc7c..3fbd0ebbd 100644
--- a/server/tests/index.ts
+++ b/server/tests/index.ts
@@ -1,6 +1,8 @@
1// Order of the tests we want to execute 1// Order of the tests we want to execute
2import './client' 2import './client'
3import './misc-endpoints'
3import './feeds/' 4import './feeds/'
4import './cli/' 5import './cli/'
5import './api/' 6import './api/'
6import './plugins/' 7import './plugins/'
8import './helpers/'
diff --git a/server/tests/misc-endpoints.ts b/server/tests/misc-endpoints.ts
index ab2dd3a0f..32b035c9e 100644
--- a/server/tests/misc-endpoints.ts
+++ b/server/tests/misc-endpoints.ts
@@ -1,4 +1,4 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
diff --git a/server/tests/plugins/action-hooks.ts b/server/tests/plugins/action-hooks.ts
index 510ec3151..ca57a4b51 100644
--- a/server/tests/plugins/action-hooks.ts
+++ b/server/tests/plugins/action-hooks.ts
@@ -1,6 +1,5 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai'
4import 'mocha' 3import 'mocha'
5import { 4import {
6 cleanupTests, 5 cleanupTests,
@@ -17,18 +16,18 @@ import {
17 createUser, 16 createUser,
18 deleteVideoComment, 17 deleteVideoComment,
19 getPluginTestPath, 18 getPluginTestPath,
20 installPlugin, login, 19 installPlugin,
21 registerUser, removeUser, 20 registerUser,
21 removeUser,
22 setAccessTokensToServers, 22 setAccessTokensToServers,
23 unblockUser, updateUser, 23 unblockUser,
24 updateUser,
24 updateVideo, 25 updateVideo,
25 uploadVideo, 26 uploadVideo,
26 viewVideo, 27 userLogin,
27 userLogin 28 viewVideo
28} from '../../../shared/extra-utils' 29} from '../../../shared/extra-utils'
29 30
30const expect = chai.expect
31
32describe('Test plugin action hooks', function () { 31describe('Test plugin action hooks', function () {
33 let servers: ServerInfo[] 32 let servers: ServerInfo[]
34 let videoUUID: string 33 let videoUUID: string
diff --git a/server/tests/plugins/external-auth.ts b/server/tests/plugins/external-auth.ts
new file mode 100644
index 000000000..312561538
--- /dev/null
+++ b/server/tests/plugins/external-auth.ts
@@ -0,0 +1,331 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4import { expect } from 'chai'
5import { ServerConfig, User, UserRole } from '@shared/models'
6import {
7 decodeQueryString,
8 getConfig,
9 getExternalAuth,
10 getMyUserInformation,
11 getPluginTestPath,
12 installPlugin,
13 loginUsingExternalToken,
14 logout,
15 refreshToken,
16 setAccessTokensToServers,
17 uninstallPlugin,
18 updateMyUser,
19 wait,
20 userLogin,
21 updatePluginSettings
22} from '../../../shared/extra-utils'
23import { cleanupTests, flushAndRunServer, ServerInfo, waitUntilLog } from '../../../shared/extra-utils/server/servers'
24
25async function loginExternal (options: {
26 server: ServerInfo
27 npmName: string
28 authName: string
29 username: string
30 query?: any
31 statusCodeExpected?: number
32}) {
33 const res = await getExternalAuth({
34 url: options.server.url,
35 npmName: options.npmName,
36 npmVersion: '0.0.1',
37 authName: options.authName,
38 query: options.query,
39 statusCodeExpected: options.statusCodeExpected || 302
40 })
41
42 if (res.status !== 302) return
43
44 const location = res.header.location
45 const { externalAuthToken } = decodeQueryString(location)
46
47 const resLogin = await loginUsingExternalToken(
48 options.server,
49 options.username,
50 externalAuthToken as string
51 )
52
53 return resLogin.body
54}
55
56describe('Test external auth plugins', function () {
57 let server: ServerInfo
58
59 let cyanAccessToken: string
60 let cyanRefreshToken: string
61
62 let kefkaAccessToken: string
63 let kefkaRefreshToken: string
64
65 let externalAuthToken: string
66
67 before(async function () {
68 this.timeout(30000)
69
70 server = await flushAndRunServer(1)
71 await setAccessTokensToServers([ server ])
72
73 for (const suffix of [ 'one', 'two' ]) {
74 await installPlugin({
75 url: server.url,
76 accessToken: server.accessToken,
77 path: getPluginTestPath('-external-auth-' + suffix)
78 })
79 }
80 })
81
82 it('Should display the correct configuration', async function () {
83 const res = await getConfig(server.url)
84
85 const config: ServerConfig = res.body
86
87 const auths = config.plugin.registeredExternalAuths
88 expect(auths).to.have.lengthOf(3)
89
90 const auth2 = auths.find((a) => a.authName === 'external-auth-2')
91 expect(auth2).to.exist
92 expect(auth2.authDisplayName).to.equal('External Auth 2')
93 expect(auth2.npmName).to.equal('peertube-plugin-test-external-auth-one')
94 })
95
96 it('Should redirect for a Cyan login', async function () {
97 const res = await getExternalAuth({
98 url: server.url,
99 npmName: 'test-external-auth-one',
100 npmVersion: '0.0.1',
101 authName: 'external-auth-1',
102 query: {
103 username: 'cyan'
104 },
105 statusCodeExpected: 302
106 })
107
108 const location = res.header.location
109 expect(location.startsWith('/login?')).to.be.true
110
111 const searchParams = decodeQueryString(location)
112
113 expect(searchParams.externalAuthToken).to.exist
114 expect(searchParams.username).to.equal('cyan')
115
116 externalAuthToken = searchParams.externalAuthToken as string
117 })
118
119 it('Should reject auto external login with a missing or invalid token', async function () {
120 await loginUsingExternalToken(server, 'cyan', '', 400)
121 await loginUsingExternalToken(server, 'cyan', 'blabla', 400)
122 })
123
124 it('Should reject auto external login with a missing or invalid username', async function () {
125 await loginUsingExternalToken(server, '', externalAuthToken, 400)
126 await loginUsingExternalToken(server, '', externalAuthToken, 400)
127 })
128
129 it('Should reject auto external login with an expired token', async function () {
130 this.timeout(15000)
131
132 await wait(5000)
133
134 await loginUsingExternalToken(server, 'cyan', externalAuthToken, 400)
135
136 await waitUntilLog(server, 'expired external auth token')
137 })
138
139 it('Should auto login Cyan, create the user and use the token', async function () {
140 {
141 const res = await loginExternal({
142 server,
143 npmName: 'test-external-auth-one',
144 authName: 'external-auth-1',
145 query: {
146 username: 'cyan'
147 },
148 username: 'cyan'
149 })
150
151 cyanAccessToken = res.access_token
152 cyanRefreshToken = res.refresh_token
153 }
154
155 {
156 const res = await getMyUserInformation(server.url, cyanAccessToken)
157
158 const body: User = res.body
159 expect(body.username).to.equal('cyan')
160 expect(body.account.displayName).to.equal('cyan')
161 expect(body.email).to.equal('cyan@example.com')
162 expect(body.role).to.equal(UserRole.USER)
163 }
164 })
165
166 it('Should auto login Kefka, create the user and use the token', async function () {
167 {
168 const res = await loginExternal({
169 server,
170 npmName: 'test-external-auth-one',
171 authName: 'external-auth-2',
172 username: 'kefka'
173 })
174
175 kefkaAccessToken = res.access_token
176 kefkaRefreshToken = res.refresh_token
177 }
178
179 {
180 const res = await getMyUserInformation(server.url, kefkaAccessToken)
181
182 const body: User = res.body
183 expect(body.username).to.equal('kefka')
184 expect(body.account.displayName).to.equal('Kefka Palazzo')
185 expect(body.email).to.equal('kefka@example.com')
186 expect(body.role).to.equal(UserRole.ADMINISTRATOR)
187 }
188 })
189
190 it('Should refresh Cyan token, but not Kefka token', async function () {
191 {
192 const resRefresh = await refreshToken(server, cyanRefreshToken)
193 cyanAccessToken = resRefresh.body.access_token
194 cyanRefreshToken = resRefresh.body.refresh_token
195
196 const res = await getMyUserInformation(server.url, cyanAccessToken)
197 const user: User = res.body
198 expect(user.username).to.equal('cyan')
199 }
200
201 {
202 await refreshToken(server, kefkaRefreshToken, 400)
203 }
204 })
205
206 it('Should update Cyan profile', async function () {
207 await updateMyUser({
208 url: server.url,
209 accessToken: cyanAccessToken,
210 displayName: 'Cyan Garamonde',
211 description: 'Retainer to the king of Doma'
212 })
213
214 const res = await getMyUserInformation(server.url, cyanAccessToken)
215
216 const body: User = res.body
217 expect(body.account.displayName).to.equal('Cyan Garamonde')
218 expect(body.account.description).to.equal('Retainer to the king of Doma')
219 })
220
221 it('Should logout Cyan', async function () {
222 await logout(server.url, cyanAccessToken)
223 })
224
225 it('Should have logged out Cyan', async function () {
226 await waitUntilLog(server, 'On logout cyan')
227
228 await getMyUserInformation(server.url, cyanAccessToken, 401)
229 })
230
231 it('Should login Cyan and keep the old existing profile', async function () {
232 {
233 const res = await loginExternal({
234 server,
235 npmName: 'test-external-auth-one',
236 authName: 'external-auth-1',
237 query: {
238 username: 'cyan'
239 },
240 username: 'cyan'
241 })
242
243 cyanAccessToken = res.access_token
244 }
245
246 const res = await getMyUserInformation(server.url, cyanAccessToken)
247
248 const body: User = res.body
249 expect(body.username).to.equal('cyan')
250 expect(body.account.displayName).to.equal('Cyan Garamonde')
251 expect(body.account.description).to.equal('Retainer to the king of Doma')
252 expect(body.role).to.equal(UserRole.USER)
253 })
254
255 it('Should reject token of Kefka by the plugin hook', async function () {
256 this.timeout(10000)
257
258 await wait(5000)
259
260 await getMyUserInformation(server.url, kefkaAccessToken, 401)
261 })
262
263 it('Should unregister external-auth-2 and do not login existing Kefka', async function () {
264 await updatePluginSettings({
265 url: server.url,
266 accessToken: server.accessToken,
267 npmName: 'peertube-plugin-test-external-auth-one',
268 settings: { disableKefka: true }
269 })
270
271 await userLogin(server, { username: 'kefka', password: 'fake' }, 400)
272
273 await loginExternal({
274 server,
275 npmName: 'test-external-auth-one',
276 authName: 'external-auth-2',
277 query: {
278 username: 'kefka'
279 },
280 username: 'kefka',
281 statusCodeExpected: 404
282 })
283 })
284
285 it('Should have disabled this auth', async function () {
286 const res = await getConfig(server.url)
287
288 const config: ServerConfig = res.body
289
290 const auths = config.plugin.registeredExternalAuths
291 expect(auths).to.have.lengthOf(2)
292
293 const auth1 = auths.find(a => a.authName === 'external-auth-2')
294 expect(auth1).to.not.exist
295 })
296
297 it('Should uninstall the plugin one and do not login Cyan', async function () {
298 await uninstallPlugin({
299 url: server.url,
300 accessToken: server.accessToken,
301 npmName: 'peertube-plugin-test-external-auth-one'
302 })
303
304 await loginExternal({
305 server,
306 npmName: 'test-external-auth-one',
307 authName: 'external-auth-1',
308 query: {
309 username: 'cyan'
310 },
311 username: 'cyan',
312 statusCodeExpected: 404
313 })
314 })
315
316 it('Should display the correct configuration', async function () {
317 const res = await getConfig(server.url)
318
319 const config: ServerConfig = res.body
320
321 const auths = config.plugin.registeredExternalAuths
322 expect(auths).to.have.lengthOf(1)
323
324 const auth2 = auths.find((a) => a.authName === 'external-auth-2')
325 expect(auth2).to.not.exist
326 })
327
328 after(async function () {
329 await cleanupTests([ server ])
330 })
331})
diff --git a/server/tests/plugins/filter-hooks.ts b/server/tests/plugins/filter-hooks.ts
index 6a5ea4641..6c1fd40ba 100644
--- a/server/tests/plugins/filter-hooks.ts
+++ b/server/tests/plugins/filter-hooks.ts
@@ -1,34 +1,27 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { 5import { cleanupTests, flushAndRunMultipleServers, ServerInfo } from '../../../shared/extra-utils/server/servers'
6 cleanupTests,
7 flushAndRunMultipleServers,
8 flushAndRunServer, killallServers, reRunServer,
9 ServerInfo,
10 waitUntilLog
11} from '../../../shared/extra-utils/server/servers'
12import { 6import {
13 addVideoCommentReply, 7 addVideoCommentReply,
14 addVideoCommentThread, 8 addVideoCommentThread,
15 deleteVideoComment, 9 doubleFollow,
10 getConfig,
16 getPluginTestPath, 11 getPluginTestPath,
17 getVideosList,
18 installPlugin,
19 removeVideo,
20 setAccessTokensToServers,
21 updateVideo,
22 uploadVideo,
23 viewVideo,
24 getVideosListPagination,
25 getVideo, 12 getVideo,
26 getVideoCommentThreads, 13 getVideoCommentThreads,
14 getVideosList,
15 getVideosListPagination,
27 getVideoThreadComments, 16 getVideoThreadComments,
28 getVideoWithToken, 17 getVideoWithToken,
18 installPlugin,
19 registerUser,
20 setAccessTokensToServers,
29 setDefaultVideoChannel, 21 setDefaultVideoChannel,
30 waitJobs, 22 updateVideo,
31 doubleFollow, getConfig, registerUser 23 uploadVideo,
24 waitJobs
32} from '../../../shared/extra-utils' 25} from '../../../shared/extra-utils'
33import { VideoCommentThreadTree } from '../../../shared/models/videos/video-comment.model' 26import { VideoCommentThreadTree } from '../../../shared/models/videos/video-comment.model'
34import { VideoDetails } from '../../../shared/models/videos' 27import { VideoDetails } from '../../../shared/models/videos'
@@ -140,7 +133,7 @@ describe('Test plugin filter hooks', function () {
140 } 133 }
141 134
142 it('Should blacklist on upload', async function () { 135 it('Should blacklist on upload', async function () {
143 const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'video please blacklist me' }) 136 const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video please blacklist me' })
144 await checkIsBlacklisted(res, true) 137 await checkIsBlacklisted(res, true)
145 }) 138 })
146 139
@@ -157,18 +150,18 @@ describe('Test plugin filter hooks', function () {
157 }) 150 })
158 151
159 it('Should blacklist on update', async function () { 152 it('Should blacklist on update', async function () {
160 const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'video' }) 153 const res = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'video' })
161 const videoId = res.body.video.uuid 154 const videoId = res.body.video.uuid
162 await checkIsBlacklisted(res, false) 155 await checkIsBlacklisted(res, false)
163 156
164 await updateVideo(servers[ 0 ].url, servers[ 0 ].accessToken, videoId, { name: 'please blacklist me' }) 157 await updateVideo(servers[0].url, servers[0].accessToken, videoId, { name: 'please blacklist me' })
165 await checkIsBlacklisted(res, true) 158 await checkIsBlacklisted(res, true)
166 }) 159 })
167 160
168 it('Should blacklist on remote upload', async function () { 161 it('Should blacklist on remote upload', async function () {
169 this.timeout(45000) 162 this.timeout(45000)
170 163
171 const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, { name: 'remote please blacklist me' }) 164 const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'remote please blacklist me' })
172 await waitJobs(servers) 165 await waitJobs(servers)
173 166
174 await checkIsBlacklisted(res, true) 167 await checkIsBlacklisted(res, true)
@@ -177,7 +170,7 @@ describe('Test plugin filter hooks', function () {
177 it('Should blacklist on remote update', async function () { 170 it('Should blacklist on remote update', async function () {
178 this.timeout(45000) 171 this.timeout(45000)
179 172
180 const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, { name: 'video' }) 173 const res = await uploadVideo(servers[1].url, servers[1].accessToken, { name: 'video' })
181 await waitJobs(servers) 174 await waitJobs(servers)
182 175
183 const videoId = res.body.video.uuid 176 const videoId = res.body.video.uuid
diff --git a/server/tests/plugins/id-and-pass-auth.ts b/server/tests/plugins/id-and-pass-auth.ts
new file mode 100644
index 000000000..cbba638c2
--- /dev/null
+++ b/server/tests/plugins/id-and-pass-auth.ts
@@ -0,0 +1,245 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4import { cleanupTests, flushAndRunServer, ServerInfo, waitUntilLog } from '../../../shared/extra-utils/server/servers'
5import {
6 getMyUserInformation,
7 getPluginTestPath,
8 installPlugin,
9 logout,
10 setAccessTokensToServers,
11 uninstallPlugin,
12 updateMyUser,
13 userLogin,
14 wait,
15 login, refreshToken, getConfig, updatePluginSettings, getUsersList
16} from '../../../shared/extra-utils'
17import { User, UserRole, ServerConfig } from '@shared/models'
18import { expect } from 'chai'
19
20describe('Test id and pass auth plugins', function () {
21 let server: ServerInfo
22
23 let crashAccessToken: string
24 let crashRefreshToken: string
25
26 let lagunaAccessToken: string
27 let lagunaRefreshToken: string
28
29 before(async function () {
30 this.timeout(30000)
31
32 server = await flushAndRunServer(1)
33 await setAccessTokensToServers([ server ])
34
35 for (const suffix of [ 'one', 'two', 'three' ]) {
36 await installPlugin({
37 url: server.url,
38 accessToken: server.accessToken,
39 path: getPluginTestPath('-id-pass-auth-' + suffix)
40 })
41 }
42 })
43
44 it('Should display the correct configuration', async function () {
45 const res = await getConfig(server.url)
46
47 const config: ServerConfig = res.body
48
49 const auths = config.plugin.registeredIdAndPassAuths
50 expect(auths).to.have.lengthOf(8)
51
52 const crashAuth = auths.find(a => a.authName === 'crash-auth')
53 expect(crashAuth).to.exist
54 expect(crashAuth.npmName).to.equal('peertube-plugin-test-id-pass-auth-one')
55 expect(crashAuth.weight).to.equal(50)
56 })
57
58 it('Should not login', async function () {
59 await userLogin(server, { username: 'toto', password: 'password' }, 400)
60 })
61
62 it('Should login Spyro, create the user and use the token', async function () {
63 const accessToken = await userLogin(server, { username: 'spyro', password: 'spyro password' })
64
65 const res = await getMyUserInformation(server.url, accessToken)
66
67 const body: User = res.body
68 expect(body.username).to.equal('spyro')
69 expect(body.account.displayName).to.equal('Spyro the Dragon')
70 expect(body.role).to.equal(UserRole.USER)
71 })
72
73 it('Should login Crash, create the user and use the token', async function () {
74 {
75 const res = await login(server.url, server.client, { username: 'crash', password: 'crash password' })
76 crashAccessToken = res.body.access_token
77 crashRefreshToken = res.body.refresh_token
78 }
79
80 {
81 const res = await getMyUserInformation(server.url, crashAccessToken)
82
83 const body: User = res.body
84 expect(body.username).to.equal('crash')
85 expect(body.account.displayName).to.equal('Crash Bandicoot')
86 expect(body.role).to.equal(UserRole.MODERATOR)
87 }
88 })
89
90 it('Should login the first Laguna, create the user and use the token', async function () {
91 {
92 const res = await login(server.url, server.client, { username: 'laguna', password: 'laguna password' })
93 lagunaAccessToken = res.body.access_token
94 lagunaRefreshToken = res.body.refresh_token
95 }
96
97 {
98 const res = await getMyUserInformation(server.url, lagunaAccessToken)
99
100 const body: User = res.body
101 expect(body.username).to.equal('laguna')
102 expect(body.account.displayName).to.equal('laguna')
103 expect(body.role).to.equal(UserRole.USER)
104 }
105 })
106
107 it('Should refresh crash token, but not laguna token', async function () {
108 {
109 const resRefresh = await refreshToken(server, crashRefreshToken)
110 crashAccessToken = resRefresh.body.access_token
111 crashRefreshToken = resRefresh.body.refresh_token
112
113 const res = await getMyUserInformation(server.url, crashAccessToken)
114 const user: User = res.body
115 expect(user.username).to.equal('crash')
116 }
117
118 {
119 await refreshToken(server, lagunaRefreshToken, 400)
120 }
121 })
122
123 it('Should update Crash profile', async function () {
124 await updateMyUser({
125 url: server.url,
126 accessToken: crashAccessToken,
127 displayName: 'Beautiful Crash',
128 description: 'Mutant eastern barred bandicoot'
129 })
130
131 const res = await getMyUserInformation(server.url, crashAccessToken)
132
133 const body: User = res.body
134 expect(body.account.displayName).to.equal('Beautiful Crash')
135 expect(body.account.description).to.equal('Mutant eastern barred bandicoot')
136 })
137
138 it('Should logout Crash', async function () {
139 await logout(server.url, crashAccessToken)
140 })
141
142 it('Should have logged out Crash', async function () {
143 await waitUntilLog(server, 'On logout for auth 1 - 2')
144
145 await getMyUserInformation(server.url, crashAccessToken, 401)
146 })
147
148 it('Should login Crash and keep the old existing profile', async function () {
149 crashAccessToken = await userLogin(server, { username: 'crash', password: 'crash password' })
150
151 const res = await getMyUserInformation(server.url, crashAccessToken)
152
153 const body: User = res.body
154 expect(body.username).to.equal('crash')
155 expect(body.account.displayName).to.equal('Beautiful Crash')
156 expect(body.account.description).to.equal('Mutant eastern barred bandicoot')
157 expect(body.role).to.equal(UserRole.MODERATOR)
158 })
159
160 it('Should reject token of laguna by the plugin hook', async function () {
161 this.timeout(10000)
162
163 await wait(5000)
164
165 await getMyUserInformation(server.url, lagunaAccessToken, 401)
166 })
167
168 it('Should reject an invalid username, email, role or display name', async function () {
169 await userLogin(server, { username: 'ward', password: 'ward password' }, 400)
170 await waitUntilLog(server, 'valid username')
171
172 await userLogin(server, { username: 'kiros', password: 'kiros password' }, 400)
173 await waitUntilLog(server, 'valid display name')
174
175 await userLogin(server, { username: 'raine', password: 'raine password' }, 400)
176 await waitUntilLog(server, 'valid role')
177
178 await userLogin(server, { username: 'ellone', password: 'elonne password' }, 400)
179 await waitUntilLog(server, 'valid email')
180 })
181
182 it('Should unregister spyro-auth and do not login existing Spyro', async function () {
183 await updatePluginSettings({
184 url: server.url,
185 accessToken: server.accessToken,
186 npmName: 'peertube-plugin-test-id-pass-auth-one',
187 settings: { disableSpyro: true }
188 })
189
190 await userLogin(server, { username: 'spyro', password: 'spyro password' }, 400)
191 await userLogin(server, { username: 'spyro', password: 'fake' }, 400)
192 })
193
194 it('Should have disabled this auth', async function () {
195 const res = await getConfig(server.url)
196
197 const config: ServerConfig = res.body
198
199 const auths = config.plugin.registeredIdAndPassAuths
200 expect(auths).to.have.lengthOf(7)
201
202 const spyroAuth = auths.find(a => a.authName === 'spyro-auth')
203 expect(spyroAuth).to.not.exist
204 })
205
206 it('Should uninstall the plugin one and do not login existing Crash', async function () {
207 await uninstallPlugin({
208 url: server.url,
209 accessToken: server.accessToken,
210 npmName: 'peertube-plugin-test-id-pass-auth-one'
211 })
212
213 await userLogin(server, { username: 'crash', password: 'crash password' }, 400)
214 })
215
216 it('Should display the correct configuration', async function () {
217 const res = await getConfig(server.url)
218
219 const config: ServerConfig = res.body
220
221 const auths = config.plugin.registeredIdAndPassAuths
222 expect(auths).to.have.lengthOf(6)
223
224 const crashAuth = auths.find(a => a.authName === 'crash-auth')
225 expect(crashAuth).to.not.exist
226 })
227
228 it('Should display plugin auth information in users list', async function () {
229 const res = await getUsersList(server.url, server.accessToken)
230
231 const users: User[] = res.body.data
232
233 const root = users.find(u => u.username === 'root')
234 const crash = users.find(u => u.username === 'crash')
235 const laguna = users.find(u => u.username === 'laguna')
236
237 expect(root.pluginAuth).to.be.null
238 expect(crash.pluginAuth).to.equal('peertube-plugin-test-id-pass-auth-one')
239 expect(laguna.pluginAuth).to.equal('peertube-plugin-test-id-pass-auth-two')
240 })
241
242 after(async function () {
243 await cleanupTests([ server ])
244 })
245})
diff --git a/server/tests/plugins/index.ts b/server/tests/plugins/index.ts
index f41708055..39c4c958a 100644
--- a/server/tests/plugins/index.ts
+++ b/server/tests/plugins/index.ts
@@ -1,4 +1,9 @@
1import './action-hooks' 1import './action-hooks'
2import './id-and-pass-auth'
3import './external-auth'
2import './filter-hooks' 4import './filter-hooks'
3import './translations' 5import './translations'
4import './video-constants' 6import './video-constants'
7import './plugin-helpers'
8import './plugin-router'
9import './plugin-storage'
diff --git a/server/tests/plugins/plugin-helpers.ts b/server/tests/plugins/plugin-helpers.ts
new file mode 100644
index 000000000..0915603d0
--- /dev/null
+++ b/server/tests/plugins/plugin-helpers.ts
@@ -0,0 +1,210 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4import {
5 checkVideoFilesWereRemoved,
6 doubleFollow,
7 getPluginTestPath,
8 getVideo,
9 installPlugin,
10 makePostBodyRequest,
11 setAccessTokensToServers,
12 uploadVideoAndGetId,
13 viewVideo,
14 getVideosList,
15 waitJobs
16} from '../../../shared/extra-utils'
17import { cleanupTests, flushAndRunMultipleServers, ServerInfo, waitUntilLog } from '../../../shared/extra-utils/server/servers'
18import { expect } from 'chai'
19
20function postCommand (server: ServerInfo, command: string, bodyArg?: object) {
21 const body = { command }
22 if (bodyArg) Object.assign(body, bodyArg)
23
24 return makePostBodyRequest({
25 url: server.url,
26 path: '/plugins/test-four/router/commander',
27 fields: body,
28 statusCodeExpected: 204
29 })
30}
31
32describe('Test plugin helpers', function () {
33 let servers: ServerInfo[]
34
35 before(async function () {
36 this.timeout(60000)
37
38 servers = await flushAndRunMultipleServers(2)
39 await setAccessTokensToServers(servers)
40
41 await doubleFollow(servers[0], servers[1])
42
43 await installPlugin({
44 url: servers[0].url,
45 accessToken: servers[0].accessToken,
46 path: getPluginTestPath('-four')
47 })
48 })
49
50 describe('Logger', function () {
51
52 it('Should have logged things', async function () {
53 await waitUntilLog(servers[0], 'localhost:' + servers[0].port + ' peertube-plugin-test-four', 1, false)
54 await waitUntilLog(servers[0], 'Hello world from plugin four', 1)
55 })
56 })
57
58 describe('Database', function () {
59
60 it('Should have made a query', async function () {
61 await waitUntilLog(servers[0], `root email is admin${servers[0].internalServerNumber}@example.com`)
62 })
63 })
64
65 describe('Config', function () {
66
67 it('Should have the correct webserver url', async function () {
68 await waitUntilLog(servers[0], `server url is http://localhost:${servers[0].port}`)
69 })
70 })
71
72 describe('Server', function () {
73
74 it('Should get the server actor', async function () {
75 await waitUntilLog(servers[0], 'server actor name is peertube')
76 })
77 })
78
79 describe('Moderation', function () {
80 let videoUUIDServer1: string
81
82 before(async function () {
83 this.timeout(15000)
84
85 {
86 const res = await uploadVideoAndGetId({ server: servers[0], videoName: 'video server 1' })
87 videoUUIDServer1 = res.uuid
88 }
89
90 {
91 await uploadVideoAndGetId({ server: servers[1], videoName: 'video server 2' })
92 }
93
94 await waitJobs(servers)
95
96 const res = await getVideosList(servers[0].url)
97 const videos = res.body.data
98
99 expect(videos).to.have.lengthOf(2)
100 })
101
102 it('Should mute server 2', async function () {
103 this.timeout(10000)
104 await postCommand(servers[0], 'blockServer', { hostToBlock: `localhost:${servers[1].port}` })
105
106 const res = await getVideosList(servers[0].url)
107 const videos = res.body.data
108
109 expect(videos).to.have.lengthOf(1)
110 expect(videos[0].name).to.equal('video server 1')
111 })
112
113 it('Should unmute server 2', async function () {
114 await postCommand(servers[0], 'unblockServer', { hostToUnblock: `localhost:${servers[1].port}` })
115
116 const res = await getVideosList(servers[0].url)
117 const videos = res.body.data
118
119 expect(videos).to.have.lengthOf(2)
120 })
121
122 it('Should mute account of server 2', async function () {
123 await postCommand(servers[0], 'blockAccount', { handleToBlock: `root@localhost:${servers[1].port}` })
124
125 const res = await getVideosList(servers[0].url)
126 const videos = res.body.data
127
128 expect(videos).to.have.lengthOf(1)
129 expect(videos[0].name).to.equal('video server 1')
130 })
131
132 it('Should unmute account of server 2', async function () {
133 await postCommand(servers[0], 'unblockAccount', { handleToUnblock: `root@localhost:${servers[1].port}` })
134
135 const res = await getVideosList(servers[0].url)
136 const videos = res.body.data
137
138 expect(videos).to.have.lengthOf(2)
139 })
140
141 it('Should blacklist video', async function () {
142 this.timeout(10000)
143
144 await postCommand(servers[0], 'blacklist', { videoUUID: videoUUIDServer1, unfederate: true })
145
146 await waitJobs(servers)
147
148 for (const server of servers) {
149 const res = await getVideosList(server.url)
150 const videos = res.body.data
151
152 expect(videos).to.have.lengthOf(1)
153 expect(videos[0].name).to.equal('video server 2')
154 }
155 })
156
157 it('Should unblacklist video', async function () {
158 this.timeout(10000)
159
160 await postCommand(servers[0], 'unblacklist', { videoUUID: videoUUIDServer1 })
161
162 await waitJobs(servers)
163
164 for (const server of servers) {
165 const res = await getVideosList(server.url)
166 const videos = res.body.data
167
168 expect(videos).to.have.lengthOf(2)
169 }
170 })
171 })
172
173 describe('Videos', function () {
174 let videoUUID: string
175
176 before(async () => {
177 const res = await uploadVideoAndGetId({ server: servers[0], videoName: 'video1' })
178 videoUUID = res.uuid
179 })
180
181 it('Should remove a video after a view', async function () {
182 this.timeout(20000)
183
184 // Should not throw -> video exists
185 await getVideo(servers[0].url, videoUUID)
186 // Should delete the video
187 await viewVideo(servers[0].url, videoUUID)
188
189 await waitUntilLog(servers[0], 'Video deleted by plugin four.')
190
191 try {
192 // Should throw because the video should have been deleted
193 await getVideo(servers[0].url, videoUUID)
194 throw new Error('Video exists')
195 } catch (err) {
196 if (err.message.includes('exists')) throw err
197 }
198
199 await checkVideoFilesWereRemoved(videoUUID, servers[0].internalServerNumber)
200 })
201
202 it('Should have fetched the video by URL', async function () {
203 await waitUntilLog(servers[0], `video from DB uuid is ${videoUUID}`)
204 })
205 })
206
207 after(async function () {
208 await cleanupTests(servers)
209 })
210})
diff --git a/server/tests/plugins/plugin-router.ts b/server/tests/plugins/plugin-router.ts
new file mode 100644
index 000000000..cf4130f4b
--- /dev/null
+++ b/server/tests/plugins/plugin-router.ts
@@ -0,0 +1,91 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4import { cleanupTests, flushAndRunServer, ServerInfo } from '../../../shared/extra-utils/server/servers'
5import {
6 getPluginTestPath,
7 installPlugin,
8 makeGetRequest,
9 makePostBodyRequest,
10 setAccessTokensToServers, uninstallPlugin
11} from '../../../shared/extra-utils'
12import { expect } from 'chai'
13
14describe('Test plugin helpers', function () {
15 let server: ServerInfo
16 const basePaths = [
17 '/plugins/test-five/router/',
18 '/plugins/test-five/0.0.1/router/'
19 ]
20
21 before(async function () {
22 this.timeout(30000)
23
24 server = await flushAndRunServer(1)
25 await setAccessTokensToServers([ server ])
26
27 await installPlugin({
28 url: server.url,
29 accessToken: server.accessToken,
30 path: getPluginTestPath('-five')
31 })
32 })
33
34 it('Should answer "pong"', async function () {
35 for (const path of basePaths) {
36 const res = await makeGetRequest({
37 url: server.url,
38 path: path + 'ping',
39 statusCodeExpected: 200
40 })
41
42 expect(res.body.message).to.equal('pong')
43 }
44 })
45
46 it('Should mirror post body', async function () {
47 const body = {
48 hello: 'world',
49 riri: 'fifi',
50 loulou: 'picsou'
51 }
52
53 for (const path of basePaths) {
54 const res = await makePostBodyRequest({
55 url: server.url,
56 path: path + 'form/post/mirror',
57 fields: body,
58 statusCodeExpected: 200
59 })
60
61 expect(res.body).to.deep.equal(body)
62 }
63 })
64
65 it('Should remove the plugin and remove the routes', async function () {
66 await uninstallPlugin({
67 url: server.url,
68 accessToken: server.accessToken,
69 npmName: 'peertube-plugin-test-five'
70 })
71
72 for (const path of basePaths) {
73 await makeGetRequest({
74 url: server.url,
75 path: path + 'ping',
76 statusCodeExpected: 404
77 })
78
79 await makePostBodyRequest({
80 url: server.url,
81 path: path + 'ping',
82 fields: {},
83 statusCodeExpected: 404
84 })
85 }
86 })
87
88 after(async function () {
89 await cleanupTests([ server ])
90 })
91})
diff --git a/server/tests/plugins/plugin-storage.ts b/server/tests/plugins/plugin-storage.ts
new file mode 100644
index 000000000..356692eb9
--- /dev/null
+++ b/server/tests/plugins/plugin-storage.ts
@@ -0,0 +1,30 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4import { getPluginTestPath, installPlugin, setAccessTokensToServers } from '../../../shared/extra-utils'
5import { cleanupTests, flushAndRunServer, ServerInfo, waitUntilLog } from '../../../shared/extra-utils/server/servers'
6
7describe('Test plugin storage', function () {
8 let server: ServerInfo
9
10 before(async function () {
11 this.timeout(30000)
12
13 server = await flushAndRunServer(1)
14 await setAccessTokensToServers([ server ])
15
16 await installPlugin({
17 url: server.url,
18 accessToken: server.accessToken,
19 path: getPluginTestPath('-six')
20 })
21 })
22
23 it('Should correctly store a subkey', async function () {
24 await waitUntilLog(server, 'superkey stored value is toto')
25 })
26
27 after(async function () {
28 await cleanupTests([ server ])
29 })
30})
diff --git a/server/tests/plugins/translations.ts b/server/tests/plugins/translations.ts
index 88d91a033..8dc2043b8 100644
--- a/server/tests/plugins/translations.ts
+++ b/server/tests/plugins/translations.ts
@@ -1,38 +1,15 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { cleanupTests, flushAndRunServer, ServerInfo } from '../../../shared/extra-utils/server/servers'
5import { 6import {
6 cleanupTests,
7 flushAndRunMultipleServers,
8 flushAndRunServer, killallServers, reRunServer,
9 ServerInfo,
10 waitUntilLog
11} from '../../../shared/extra-utils/server/servers'
12import {
13 addVideoCommentReply,
14 addVideoCommentThread,
15 deleteVideoComment,
16 getPluginTestPath, 7 getPluginTestPath,
17 getVideosList, 8 getPluginTranslations,
18 installPlugin, 9 installPlugin,
19 removeVideo,
20 setAccessTokensToServers, 10 setAccessTokensToServers,
21 updateVideo, 11 uninstallPlugin
22 uploadVideo,
23 viewVideo,
24 getVideosListPagination,
25 getVideo,
26 getVideoCommentThreads,
27 getVideoThreadComments,
28 getVideoWithToken,
29 setDefaultVideoChannel,
30 waitJobs,
31 doubleFollow, getVideoLanguages, getVideoLicences, getVideoCategories, uninstallPlugin, getPluginTranslations
32} from '../../../shared/extra-utils' 12} from '../../../shared/extra-utils'
33import { VideoCommentThreadTree } from '../../../shared/models/videos/video-comment.model'
34import { VideoDetails } from '../../../shared/models/videos'
35import { getYoutubeVideoUrl, importVideo } from '../../../shared/extra-utils/videos/video-imports'
36 13
37const expect = chai.expect 14const expect = chai.expect
38 15
@@ -69,7 +46,7 @@ describe('Test plugin translations', function () {
69 46
70 expect(res.body).to.deep.equal({ 47 expect(res.body).to.deep.equal({
71 'peertube-plugin-test': { 48 'peertube-plugin-test': {
72 'Hi': 'Coucou' 49 Hi: 'Coucou'
73 }, 50 },
74 'peertube-plugin-test-two': { 51 'peertube-plugin-test-two': {
75 'Hello world': 'Bonjour le monde' 52 'Hello world': 'Bonjour le monde'
@@ -95,7 +72,7 @@ describe('Test plugin translations', function () {
95 72
96 expect(res.body).to.deep.equal({ 73 expect(res.body).to.deep.equal({
97 'peertube-plugin-test': { 74 'peertube-plugin-test': {
98 'Hi': 'Coucou' 75 Hi: 'Coucou'
99 } 76 }
100 }) 77 })
101 } 78 }
diff --git a/server/tests/plugins/video-constants.ts b/server/tests/plugins/video-constants.ts
index 6562e2b45..fec9196e2 100644
--- a/server/tests/plugins/video-constants.ts
+++ b/server/tests/plugins/video-constants.ts
@@ -1,38 +1,21 @@
1/* tslint:disable:no-unused-expression */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { cleanupTests, flushAndRunServer, ServerInfo } from '../../../shared/extra-utils/server/servers'
5import { 6import {
6 cleanupTests, 7 createVideoPlaylist,
7 flushAndRunMultipleServers,
8 flushAndRunServer, killallServers, reRunServer,
9 ServerInfo,
10 waitUntilLog
11} from '../../../shared/extra-utils/server/servers'
12import {
13 addVideoCommentReply,
14 addVideoCommentThread,
15 deleteVideoComment,
16 getPluginTestPath, 8 getPluginTestPath,
17 getVideosList, 9 getVideo,
10 getVideoCategories,
11 getVideoLanguages,
12 getVideoLicences, getVideoPlaylistPrivacies, getVideoPrivacies,
18 installPlugin, 13 installPlugin,
19 removeVideo,
20 setAccessTokensToServers, 14 setAccessTokensToServers,
21 updateVideo, 15 uninstallPlugin,
22 uploadVideo, 16 uploadVideo
23 viewVideo,
24 getVideosListPagination,
25 getVideo,
26 getVideoCommentThreads,
27 getVideoThreadComments,
28 getVideoWithToken,
29 setDefaultVideoChannel,
30 waitJobs,
31 doubleFollow, getVideoLanguages, getVideoLicences, getVideoCategories, uninstallPlugin
32} from '../../../shared/extra-utils' 17} from '../../../shared/extra-utils'
33import { VideoCommentThreadTree } from '../../../shared/models/videos/video-comment.model' 18import { VideoDetails, VideoPlaylistPrivacy } from '../../../shared/models/videos'
34import { VideoDetails } from '../../../shared/models/videos'
35import { getYoutubeVideoUrl, importVideo } from '../../../shared/extra-utils/videos/video-imports'
36 19
37const expect = chai.expect 20const expect = chai.expect
38 21
@@ -85,6 +68,35 @@ describe('Test plugin altering video constants', function () {
85 expect(licences[43]).to.equal('High best licence') 68 expect(licences[43]).to.equal('High best licence')
86 }) 69 })
87 70
71 it('Should have updated video privacies', async function () {
72 const res = await getVideoPrivacies(server.url)
73 const privacies = res.body
74
75 expect(privacies[1]).to.exist
76 expect(privacies[2]).to.not.exist
77 expect(privacies[3]).to.exist
78 expect(privacies[4]).to.exist
79 })
80
81 it('Should have updated playlist privacies', async function () {
82 const res = await getVideoPlaylistPrivacies(server.url)
83 const playlistPrivacies = res.body
84
85 expect(playlistPrivacies[1]).to.exist
86 expect(playlistPrivacies[2]).to.exist
87 expect(playlistPrivacies[3]).to.not.exist
88 })
89
90 it('Should not be able to create a video with this privacy', async function () {
91 const attrs = { name: 'video', privacy: 2 }
92 await uploadVideo(server.url, server.accessToken, attrs, 400)
93 })
94
95 it('Should not be able to create a video with this privacy', async function () {
96 const attrs = { displayName: 'video playlist', privacy: VideoPlaylistPrivacy.PRIVATE }
97 await createVideoPlaylist({ url: server.url, token: server.accessToken, playlistAttrs: attrs, expectedStatus: 400 })
98 })
99
88 it('Should be able to upload a video with these values', async function () { 100 it('Should be able to upload a video with these values', async function () {
89 const attrs = { name: 'video', category: 42, licence: 42, language: 'al_bhed2' } 101 const attrs = { name: 'video', category: 42, licence: 42, language: 'al_bhed2' }
90 const resUpload = await uploadVideo(server.url, server.accessToken, attrs) 102 const resUpload = await uploadVideo(server.url, server.accessToken, attrs)
@@ -97,40 +109,59 @@ describe('Test plugin altering video constants', function () {
97 expect(video.category.label).to.equal('Best category') 109 expect(video.category.label).to.equal('Best category')
98 }) 110 })
99 111
100 it('Should uninstall the plugin and reset languages, categories and licences', async function () { 112 it('Should uninstall the plugin and reset languages, categories, licences and privacies', async function () {
101 await uninstallPlugin({ url: server.url, accessToken: server.accessToken, npmName: 'peertube-plugin-test-three' }) 113 await uninstallPlugin({ url: server.url, accessToken: server.accessToken, npmName: 'peertube-plugin-test-three' })
102 114
103 { 115 {
104 const res = await getVideoLanguages(server.url) 116 const res = await getVideoLanguages(server.url)
105 const languages = res.body 117 const languages = res.body
106 118
107 expect(languages[ 'en' ]).to.equal('English') 119 expect(languages['en']).to.equal('English')
108 expect(languages[ 'fr' ]).to.equal('French') 120 expect(languages['fr']).to.equal('French')
109 121
110 expect(languages[ 'al_bhed' ]).to.not.exist 122 expect(languages['al_bhed']).to.not.exist
111 expect(languages[ 'al_bhed2' ]).to.not.exist 123 expect(languages['al_bhed2']).to.not.exist
112 } 124 }
113 125
114 { 126 {
115 const res = await getVideoCategories(server.url) 127 const res = await getVideoCategories(server.url)
116 const categories = res.body 128 const categories = res.body
117 129
118 expect(categories[ 1 ]).to.equal('Music') 130 expect(categories[1]).to.equal('Music')
119 expect(categories[ 2 ]).to.equal('Films') 131 expect(categories[2]).to.equal('Films')
120 132
121 expect(categories[ 42 ]).to.not.exist 133 expect(categories[42]).to.not.exist
122 expect(categories[ 43 ]).to.not.exist 134 expect(categories[43]).to.not.exist
123 } 135 }
124 136
125 { 137 {
126 const res = await getVideoLicences(server.url) 138 const res = await getVideoLicences(server.url)
127 const licences = res.body 139 const licences = res.body
128 140
129 expect(licences[ 1 ]).to.equal('Attribution') 141 expect(licences[1]).to.equal('Attribution')
130 expect(licences[ 7 ]).to.equal('Public Domain Dedication') 142 expect(licences[7]).to.equal('Public Domain Dedication')
143
144 expect(licences[42]).to.not.exist
145 expect(licences[43]).to.not.exist
146 }
147
148 {
149 const res = await getVideoPrivacies(server.url)
150 const privacies = res.body
151
152 expect(privacies[1]).to.exist
153 expect(privacies[2]).to.exist
154 expect(privacies[3]).to.exist
155 expect(privacies[4]).to.exist
156 }
157
158 {
159 const res = await getVideoPlaylistPrivacies(server.url)
160 const playlistPrivacies = res.body
131 161
132 expect(licences[ 42 ]).to.not.exist 162 expect(playlistPrivacies[1]).to.exist
133 expect(licences[ 43 ]).to.not.exist 163 expect(playlistPrivacies[2]).to.exist
164 expect(playlistPrivacies[3]).to.exist
134 } 165 }
135 }) 166 })
136 167
diff --git a/server/tests/real-world/populate-database.ts b/server/tests/real-world/populate-database.ts
deleted file mode 100644
index b1c1688e7..000000000
--- a/server/tests/real-world/populate-database.ts
+++ /dev/null
@@ -1,122 +0,0 @@
1import { VideoRateType } from '../../../shared'
2import {
3 addVideoChannel,
4 createUser,
5 flushTests,
6 getVideosList,
7 killallServers,
8 rateVideo,
9 flushAndRunServer,
10 ServerInfo,
11 setAccessTokensToServers,
12 uploadVideo
13} from '../../../shared/extra-utils'
14import * as Bluebird from 'bluebird'
15
16start()
17 .catch(err => console.error(err))
18
19// ----------------------------------------------------------------------------
20
21async function start () {
22
23 console.log('Flushed tests.')
24
25 const server = await flushAndRunServer(6)
26
27 process.on('exit', async () => {
28 killallServers([ server ])
29 return
30 })
31 process.on('SIGINT', goodbye)
32 process.on('SIGTERM', goodbye)
33
34 await setAccessTokensToServers([ server ])
35
36 console.log('Servers ran.')
37
38 // Forever
39 const fakeTab = Array.from(Array(1000000).keys())
40 const funs = [
41 uploadCustom
42 // uploadCustom,
43 // uploadCustom,
44 // uploadCustom,
45 // likeCustom,
46 // createUserCustom,
47 // createCustomChannel
48 ]
49 const promises = []
50
51 for (const fun of funs) {
52 promises.push(
53 Bluebird.map(fakeTab, () => {
54 return fun(server).catch(err => console.error(err))
55 }, { concurrency: 3 })
56 )
57 }
58
59 await Promise.all(promises)
60}
61
62function getRandomInt (min, max) {
63 return Math.floor(Math.random() * (max - min)) + min
64}
65
66function createCustomChannel (server: ServerInfo) {
67 const videoChannel = {
68 name: Date.now().toString(),
69 displayName: Date.now().toString(),
70 description: Date.now().toString()
71 }
72
73 return addVideoChannel(server.url, server.accessToken, videoChannel)
74}
75
76function createUserCustom (server: ServerInfo) {
77 const username = Date.now().toString() + getRandomInt(0, 100000)
78 console.log('Creating user %s.', username)
79
80 return createUser({ url: server.url, accessToken: server.accessToken, username: username, password: 'coucou' })
81}
82
83function uploadCustom (server: ServerInfo) {
84 console.log('Uploading video.')
85
86 const videoAttributes = {
87 name: Date.now() + ' name',
88 category: 4,
89 nsfw: false,
90 licence: 2,
91 language: 'en',
92 description: Date.now() + ' description',
93 tags: [ Date.now().toString().substring(0, 5) + 't1', Date.now().toString().substring(0, 5) + 't2' ],
94 fixture: 'video_short.mp4'
95 }
96
97 return uploadVideo(server.url, server.accessToken, videoAttributes)
98}
99
100function likeCustom (server: ServerInfo) {
101 return rateCustom(server, 'like')
102}
103
104function dislikeCustom (server: ServerInfo) {
105 return rateCustom(server, 'dislike')
106}
107
108async function rateCustom (server: ServerInfo, rating: VideoRateType) {
109 const res = await getVideosList(server.url)
110
111 const videos = res.body.data
112 if (videos.length === 0) return undefined
113
114 const videoToRate = videos[getRandomInt(0, videos.length)]
115
116 console.log('Rating (%s) video.', rating)
117 return rateVideo(server.url, server.accessToken, videoToRate.id, rating)
118}
119
120function goodbye () {
121 return process.exit(-1)
122}
diff --git a/server/tests/real-world/real-world.ts b/server/tests/real-world/real-world.ts
deleted file mode 100644
index cba5ac311..000000000
--- a/server/tests/real-world/real-world.ts
+++ /dev/null
@@ -1,375 +0,0 @@
1// /!\ Before imports /!\
2process.env.NODE_ENV = 'test'
3
4import * as program from 'commander'
5import { Video, VideoFile, VideoRateType } from '../../../shared'
6import { JobState } from '../../../shared/models'
7import {
8 flushAndRunMultipleServers,
9 flushTests, follow,
10 getVideo,
11 getVideosList, getVideosListPagination,
12 killallServers,
13 removeVideo,
14 ServerInfo as DefaultServerInfo,
15 setAccessTokensToServers,
16 updateVideo,
17 uploadVideo, viewVideo,
18 wait
19} from '../../../shared/extra-utils'
20import { getJobsListPaginationAndSort } from '../../../shared/extra-utils/server/jobs'
21
22interface ServerInfo extends DefaultServerInfo {
23 requestsNumber: number
24}
25
26program
27 .option('-c, --create [weight]', 'Weight for creating videos')
28 .option('-r, --remove [weight]', 'Weight for removing videos')
29 .option('-u, --update [weight]', 'Weight for updating videos')
30 .option('-v, --view [weight]', 'Weight for viewing videos')
31 .option('-l, --like [weight]', 'Weight for liking videos')
32 .option('-s, --dislike [weight]', 'Weight for disliking videos')
33 .option('-p, --servers [n]', 'Number of servers to run (3 or 6)', /^3|6$/, 3)
34 .option('-i, --interval-action [interval]', 'Interval in ms for an action')
35 .option('-I, --interval-integrity [interval]', 'Interval in ms for an integrity check')
36 .option('-f, --flush', 'Flush data on exit')
37 .option('-d, --difference', 'Display difference if integrity is not okay')
38 .parse(process.argv)
39
40const createWeight = program['create'] !== undefined ? parseInt(program['create'], 10) : 5
41const removeWeight = program['remove'] !== undefined ? parseInt(program['remove'], 10) : 4
42const updateWeight = program['update'] !== undefined ? parseInt(program['update'], 10) : 4
43const viewWeight = program['view'] !== undefined ? parseInt(program['view'], 10) : 4
44const likeWeight = program['like'] !== undefined ? parseInt(program['like'], 10) : 4
45const dislikeWeight = program['dislike'] !== undefined ? parseInt(program['dislike'], 10) : 4
46const flushAtExit = program['flush'] || false
47const actionInterval = program['intervalAction'] !== undefined ? parseInt(program['intervalAction'], 10) : 500
48const integrityInterval = program['intervalIntegrity'] !== undefined ? parseInt(program['intervalIntegrity'], 10) : 60000
49const displayDiffOnFail = program['difference'] || false
50
51const numberOfServers = 6
52
53console.log(
54 'Create weight: %d, update weight: %d, remove weight: %d, view weight: %d, like weight: %d, dislike weight: %d.',
55 createWeight, updateWeight, removeWeight, viewWeight, likeWeight, dislikeWeight
56)
57
58if (flushAtExit) {
59 console.log('Program will flush data on exit.')
60} else {
61 console.log('Program will not flush data on exit.')
62}
63if (displayDiffOnFail) {
64 console.log('Program will display diff on failure.')
65} else {
66 console.log('Program will not display diff on failure')
67}
68console.log('Interval in ms for each action: %d.', actionInterval)
69console.log('Interval in ms for each integrity check: %d.', integrityInterval)
70
71console.log('Run servers...')
72
73start()
74
75// ----------------------------------------------------------------------------
76
77async function start () {
78 const servers = await runServers(numberOfServers)
79
80 process.on('exit', async () => {
81 await exitServers(servers, flushAtExit)
82
83 return
84 })
85 process.on('SIGINT', goodbye)
86 process.on('SIGTERM', goodbye)
87
88 console.log('Servers ran')
89 initializeRequestsPerServer(servers)
90
91 let checking = false
92
93 setInterval(async () => {
94 if (checking === true) return
95
96 const rand = getRandomInt(0, createWeight + updateWeight + removeWeight + viewWeight + likeWeight + dislikeWeight)
97
98 const numServer = getRandomNumServer(servers)
99 servers[numServer].requestsNumber++
100
101 if (rand < createWeight) {
102 await upload(servers, numServer)
103 } else if (rand < createWeight + updateWeight) {
104 await update(servers, numServer)
105 } else if (rand < createWeight + updateWeight + removeWeight) {
106 await remove(servers, numServer)
107 } else if (rand < createWeight + updateWeight + removeWeight + viewWeight) {
108 await view(servers, numServer)
109 } else if (rand < createWeight + updateWeight + removeWeight + viewWeight + likeWeight) {
110 await like(servers, numServer)
111 } else {
112 await dislike(servers, numServer)
113 }
114 }, actionInterval)
115
116 // The function will check the consistency between servers (should have the same videos with same attributes...)
117 setInterval(function () {
118 if (checking === true) return
119
120 console.log('Checking integrity...')
121 checking = true
122
123 const waitingInterval = setInterval(async () => {
124 const pendingRequests = await isTherePendingRequests(servers)
125 if (pendingRequests === true) {
126 console.log('A server has pending requests, waiting...')
127 return
128 }
129
130 // Even if there are no pending request, wait some potential processes
131 await wait(2000)
132 await checkIntegrity(servers)
133
134 initializeRequestsPerServer(servers)
135 checking = false
136 clearInterval(waitingInterval)
137 }, 10000)
138 }, integrityInterval)
139}
140
141function initializeRequestsPerServer (servers: ServerInfo[]) {
142 servers.forEach(server => server.requestsNumber = 0)
143}
144
145function getRandomInt (min, max) {
146 return Math.floor(Math.random() * (max - min)) + min
147}
148
149function getRandomNumServer (servers) {
150 return getRandomInt(0, servers.length)
151}
152
153async function runServers (numberOfServers: number) {
154 const servers: ServerInfo[] = (await flushAndRunMultipleServers(numberOfServers))
155 .map(s => Object.assign({ requestsNumber: 0 }, s))
156
157 // Get the access tokens
158 await setAccessTokensToServers(servers)
159
160 for (let i = 0; i < numberOfServers; i++) {
161 for (let j = 0; j < numberOfServers; j++) {
162 if (i === j) continue
163
164 await follow(servers[i].url, [ servers[j].url ], servers[i].accessToken)
165 }
166 }
167
168 return servers
169}
170
171async function exitServers (servers: ServerInfo[], flushAtExit: boolean) {
172 killallServers(servers)
173
174 if (flushAtExit) await flushTests()
175}
176
177function upload (servers: ServerInfo[], numServer: number) {
178 console.log('Uploading video to server ' + numServer)
179
180 const videoAttributes = {
181 name: Date.now() + ' name',
182 category: 4,
183 nsfw: false,
184 licence: 2,
185 language: 'en',
186 description: Date.now() + ' description',
187 tags: [ Date.now().toString().substring(0, 5) + 't1', Date.now().toString().substring(0, 5) + 't2' ],
188 fixture: 'video_short1.webm'
189 }
190 return uploadVideo(servers[numServer].url, servers[numServer].accessToken, videoAttributes)
191}
192
193async function update (servers: ServerInfo[], numServer: number) {
194 const res = await getVideosList(servers[numServer].url)
195
196 const videos = res.body.data.filter(video => video.isLocal === true)
197 if (videos.length === 0) return undefined
198
199 const toUpdate = videos[getRandomInt(0, videos.length)].id
200 const attributes = {
201 name: Date.now() + ' name',
202 description: Date.now() + ' description',
203 tags: [ Date.now().toString().substring(0, 5) + 't1', Date.now().toString().substring(0, 5) + 't2' ]
204 }
205
206 console.log('Updating video of server ' + numServer)
207
208 return updateVideo(servers[numServer].url, servers[numServer].accessToken, toUpdate, attributes)
209}
210
211async function remove (servers: ServerInfo[], numServer: number) {
212 const res = await getVideosList(servers[numServer].url)
213 const videos = res.body.data.filter(video => video.isLocal === true)
214 if (videos.length === 0) return undefined
215
216 const toRemove = videos[getRandomInt(0, videos.length)].id
217
218 console.log('Removing video from server ' + numServer)
219 return removeVideo(servers[numServer].url, servers[numServer].accessToken, toRemove)
220}
221
222async function view (servers: ServerInfo[], numServer: number) {
223 const res = await getVideosList(servers[numServer].url)
224
225 const videos = res.body.data
226 if (videos.length === 0) return undefined
227
228 const toView = videos[getRandomInt(0, videos.length)].id
229
230 console.log('Viewing video from server ' + numServer)
231 return viewVideo(servers[numServer].url, toView)
232}
233
234function like (servers: ServerInfo[], numServer: number) {
235 return rate(servers, numServer, 'like')
236}
237
238function dislike (servers: ServerInfo[], numServer: number) {
239 return rate(servers, numServer, 'dislike')
240}
241
242async function rate (servers: ServerInfo[], numServer: number, rating: VideoRateType) {
243 const res = await getVideosList(servers[numServer].url)
244
245 const videos = res.body.data
246 if (videos.length === 0) return undefined
247
248 const toRate = videos[getRandomInt(0, videos.length)].id
249
250 console.log('Rating (%s) video from server %d', rating, numServer)
251 return getVideo(servers[numServer].url, toRate)
252}
253
254async function checkIntegrity (servers: ServerInfo[]) {
255 const videos: Video[][] = []
256 const tasks: Promise<any>[] = []
257
258 // Fetch all videos and remove some fields that can differ between servers
259 for (const server of servers) {
260 const p = getVideosListPagination(server.url, 0, 1000000, '-createdAt')
261 .then(res => videos.push(res.body.data))
262 tasks.push(p)
263 }
264
265 await Promise.all(tasks)
266
267 let i = 0
268 for (const video of videos) {
269 const differences = areDifferences(video, videos[0])
270 if (differences !== undefined) {
271 console.error('Integrity not ok with server %d!', i + 1)
272
273 if (displayDiffOnFail) {
274 console.log(differences)
275 }
276
277 process.exit(-1)
278 }
279
280 i++
281 }
282
283 console.log('Integrity ok.')
284}
285
286function areDifferences (videos1: Video[], videos2: Video[]) {
287 // Remove some keys we don't want to compare
288 videos1.concat(videos2).forEach(video => {
289 delete video.id
290 delete video.isLocal
291 delete video.thumbnailPath
292 delete video.updatedAt
293 delete video.views
294 })
295
296 if (videos1.length !== videos2.length) {
297 return `Videos length are different (${videos1.length}/${videos2.length}).`
298 }
299
300 for (const video1 of videos1) {
301 const video2 = videos2.find(video => video.uuid === video1.uuid)
302
303 if (!video2) return 'Video ' + video1.uuid + ' is missing.'
304
305 for (const videoKey of Object.keys(video1)) {
306 const attribute1 = video1[videoKey]
307 const attribute2 = video2[videoKey]
308
309 if (videoKey === 'tags') {
310 if (attribute1.length !== attribute2.length) {
311 return 'Tags are different.'
312 }
313
314 attribute1.forEach(tag1 => {
315 if (attribute2.indexOf(tag1) === -1) {
316 return 'Tag ' + tag1 + ' is missing.'
317 }
318 })
319 } else if (videoKey === 'files') {
320 if (attribute1.length !== attribute2.length) {
321 return 'Video files are different.'
322 }
323
324 attribute1.forEach((videoFile1: VideoFile) => {
325 const videoFile2: VideoFile = attribute2.find(videoFile => videoFile.magnetUri === videoFile1.magnetUri)
326 if (!videoFile2) {
327 return `Video ${video1.uuid} has missing video file ${videoFile1.magnetUri}.`
328 }
329
330 if (videoFile1.size !== videoFile2.size || videoFile1.resolution.label !== videoFile2.resolution.label) {
331 return `Video ${video1.uuid} has different video file ${videoFile1.magnetUri}.`
332 }
333 })
334 } else {
335 if (attribute1 !== attribute2) {
336 return `Video ${video1.uuid} has different value for attribute ${videoKey}.`
337 }
338 }
339 }
340 }
341
342 return undefined
343}
344
345function goodbye () {
346 return process.exit(-1)
347}
348
349async function isTherePendingRequests (servers: ServerInfo[]) {
350 const states: JobState[] = [ 'waiting', 'active', 'delayed' ]
351 const tasks: Promise<any>[] = []
352 let pendingRequests = false
353
354 // Check if each server has pending request
355 for (const server of servers) {
356 for (const state of states) {
357 const p = getJobsListPaginationAndSort({
358 url: server.url,
359 accessToken: server.accessToken,
360 state: state,
361 start: 0,
362 count: 10,
363 sort: '-createdAt'
364 })
365 .then(res => {
366 if (res.body.total > 0) pendingRequests = true
367 })
368 tasks.push(p)
369 }
370 }
371
372 await Promise.all(tasks)
373
374 return pendingRequests
375}
diff --git a/server/tools/cli.ts b/server/tools/cli.ts
index 58e2445ac..d5416fc38 100644
--- a/server/tools/cli.ts
+++ b/server/tools/cli.ts
@@ -3,9 +3,12 @@ import { getAppNumber, isTestInstance } from '../helpers/core-utils'
3import { join } from 'path' 3import { join } from 'path'
4import { root } from '../../shared/extra-utils/miscs/miscs' 4import { root } from '../../shared/extra-utils/miscs/miscs'
5import { getVideoChannel } from '../../shared/extra-utils/videos/video-channels' 5import { getVideoChannel } from '../../shared/extra-utils/videos/video-channels'
6import { Command } from 'commander' 6import { CommanderStatic } from 'commander'
7import { VideoChannel, VideoPrivacy } from '../../shared/models/videos' 7import { VideoChannel, VideoPrivacy } from '../../shared/models/videos'
8import { createLogger, format, transports } from 'winston' 8import { createLogger, format, transports } from 'winston'
9import { getMyUserInformation } from '@shared/extra-utils/users/users'
10import { User, UserRole } from '@shared/models'
11import { getAccessToken } from '@shared/extra-utils/users/login'
9 12
10let configName = 'PeerTube/CLI' 13let configName = 'PeerTube/CLI'
11if (isTestInstance()) configName += `-${getAppNumber()}` 14if (isTestInstance()) configName += `-${getAppNumber()}`
@@ -14,24 +17,35 @@ const config = require('application-config')(configName)
14 17
15const version = require('../../../package.json').version 18const version = require('../../../package.json').version
16 19
20async function getAdminTokenOrDie (url: string, username: string, password: string) {
21 const accessToken = await getAccessToken(url, username, password)
22 const resMe = await getMyUserInformation(url, accessToken)
23 const me: User = resMe.body
24
25 if (me.role !== UserRole.ADMINISTRATOR) {
26 console.error('You must be an administrator.')
27 process.exit(-1)
28 }
29
30 return accessToken
31}
32
17interface Settings { 33interface Settings {
18 remotes: any[], 34 remotes: any[]
19 default: number 35 default: number
20} 36}
21 37
22function getSettings () { 38async function getSettings (): Promise<Settings> {
23 return new Promise<Settings>((res, rej) => { 39 const defaultSettings = {
24 const defaultSettings = { 40 remotes: [],
25 remotes: [], 41 default: -1
26 default: -1 42 }
27 }
28 43
29 config.read((err, data) => { 44 const data = await config.read()
30 if (err) return rej(err)
31 45
32 return res(Object.keys(data).length === 0 ? defaultSettings : data) 46 return Object.keys(data).length === 0
33 }) 47 ? defaultSettings
34 }) 48 : data
35} 49}
36 50
37async function getNetrc () { 51async function getNetrc () {
@@ -46,24 +60,12 @@ async function getNetrc () {
46 return netrc 60 return netrc
47} 61}
48 62
49function writeSettings (settings) { 63function writeSettings (settings: Settings) {
50 return new Promise((res, rej) => { 64 return config.write(settings)
51 config.write(settings, err => {
52 if (err) return rej(err)
53
54 return res()
55 })
56 })
57} 65}
58 66
59function deleteSettings () { 67function deleteSettings () {
60 return new Promise((res, rej) => { 68 return config.trash()
61 config.trash((err) => {
62 if (err) return rej(err)
63
64 return res()
65 })
66 })
67} 69}
68 70
69function getRemoteObjectOrDie ( 71function getRemoteObjectOrDie (
@@ -74,9 +76,9 @@ function getRemoteObjectOrDie (
74 if (!program['url'] || !program['username'] || !program['password']) { 76 if (!program['url'] || !program['username'] || !program['password']) {
75 // No remote and we don't have program parameters: quit 77 // No remote and we don't have program parameters: quit
76 if (settings.remotes.length === 0 || Object.keys(netrc.machines).length === 0) { 78 if (settings.remotes.length === 0 || Object.keys(netrc.machines).length === 0) {
77 if (!program[ 'url' ]) console.error('--url field is required.') 79 if (!program['url']) console.error('--url field is required.')
78 if (!program[ 'username' ]) console.error('--username field is required.') 80 if (!program['username']) console.error('--username field is required.')
79 if (!program[ 'password' ]) console.error('--password field is required.') 81 if (!program['password']) console.error('--password field is required.')
80 82
81 return process.exit(-1) 83 return process.exit(-1)
82 } 84 }
@@ -96,13 +98,13 @@ function getRemoteObjectOrDie (
96 } 98 }
97 99
98 return { 100 return {
99 url: program[ 'url' ], 101 url: program['url'],
100 username: program[ 'username' ], 102 username: program['username'],
101 password: program[ 'password' ] 103 password: program['password']
102 } 104 }
103} 105}
104 106
105function buildCommonVideoOptions (command: Command) { 107function buildCommonVideoOptions (command: CommanderStatic) {
106 function list (val) { 108 function list (val) {
107 return val.split(',') 109 return val.split(',')
108 } 110 }
@@ -117,13 +119,14 @@ function buildCommonVideoOptions (command: Command) {
117 .option('-d, --video-description <description>', 'Video description') 119 .option('-d, --video-description <description>', 'Video description')
118 .option('-P, --privacy <privacy_number>', 'Privacy') 120 .option('-P, --privacy <privacy_number>', 'Privacy')
119 .option('-C, --channel-name <channel_name>', 'Channel name') 121 .option('-C, --channel-name <channel_name>', 'Channel name')
120 .option('-m, --comments-enabled', 'Enable comments') 122 .option('--no-comments-enabled', 'Disable video comments')
121 .option('-s, --support <support>', 'Video support text') 123 .option('-s, --support <support>', 'Video support text')
122 .option('-w, --wait-transcoding', 'Wait transcoding before publishing the video') 124 .option('--no-wait-transcoding', 'Do not wait transcoding before publishing the video')
125 .option('--no-download-enabled', 'Disable video download')
123 .option('-v, --verbose <verbose>', 'Verbosity, from 0/\'error\' to 4/\'debug\'', 'info') 126 .option('-v, --verbose <verbose>', 'Verbosity, from 0/\'error\' to 4/\'debug\'', 'info')
124} 127}
125 128
126async function buildVideoAttributesFromCommander (url: string, command: Command, defaultAttributes: any = {}) { 129async function buildVideoAttributesFromCommander (url: string, command: CommanderStatic, defaultAttributes: any = {}) {
127 const defaultBooleanAttributes = { 130 const defaultBooleanAttributes = {
128 nsfw: false, 131 nsfw: false,
129 commentsEnabled: true, 132 commentsEnabled: true,
@@ -134,8 +137,8 @@ async function buildVideoAttributesFromCommander (url: string, command: Command,
134 const booleanAttributes: { [id in keyof typeof defaultBooleanAttributes]: boolean } | {} = {} 137 const booleanAttributes: { [id in keyof typeof defaultBooleanAttributes]: boolean } | {} = {}
135 138
136 for (const key of Object.keys(defaultBooleanAttributes)) { 139 for (const key of Object.keys(defaultBooleanAttributes)) {
137 if (command[ key ] !== undefined) { 140 if (command[key] !== undefined) {
138 booleanAttributes[key] = command[ key ] 141 booleanAttributes[key] = command[key]
139 } else if (defaultAttributes[key] !== undefined) { 142 } else if (defaultAttributes[key] !== undefined) {
140 booleanAttributes[key] = defaultAttributes[key] 143 booleanAttributes[key] = defaultAttributes[key]
141 } else { 144 } else {
@@ -144,19 +147,19 @@ async function buildVideoAttributesFromCommander (url: string, command: Command,
144 } 147 }
145 148
146 const videoAttributes = { 149 const videoAttributes = {
147 name: command[ 'videoName' ] || defaultAttributes.name, 150 name: command['videoName'] || defaultAttributes.name,
148 category: command[ 'category' ] || defaultAttributes.category || undefined, 151 category: command['category'] || defaultAttributes.category || undefined,
149 licence: command[ 'licence' ] || defaultAttributes.licence || undefined, 152 licence: command['licence'] || defaultAttributes.licence || undefined,
150 language: command[ 'language' ] || defaultAttributes.language || undefined, 153 language: command['language'] || defaultAttributes.language || undefined,
151 privacy: command[ 'privacy' ] || defaultAttributes.privacy || VideoPrivacy.PUBLIC, 154 privacy: command['privacy'] || defaultAttributes.privacy || VideoPrivacy.PUBLIC,
152 support: command[ 'support' ] || defaultAttributes.support || undefined, 155 support: command['support'] || defaultAttributes.support || undefined,
153 description: command[ 'videoDescription' ] || defaultAttributes.description || undefined, 156 description: command['videoDescription'] || defaultAttributes.description || undefined,
154 tags: command[ 'tags' ] || defaultAttributes.tags || undefined 157 tags: command['tags'] || defaultAttributes.tags || undefined
155 } 158 }
156 159
157 Object.assign(videoAttributes, booleanAttributes) 160 Object.assign(videoAttributes, booleanAttributes)
158 161
159 if (command[ 'channelName' ]) { 162 if (command['channelName']) {
160 const res = await getVideoChannel(url, command['channelName']) 163 const res = await getVideoChannel(url, command['channelName'])
161 const videoChannel: VideoChannel = res.body 164 const videoChannel: VideoChannel = res.body
162 165
@@ -172,9 +175,9 @@ async function buildVideoAttributesFromCommander (url: string, command: Command,
172 175
173function getServerCredentials (program: any) { 176function getServerCredentials (program: any) {
174 return Promise.all([ getSettings(), getNetrc() ]) 177 return Promise.all([ getSettings(), getNetrc() ])
175 .then(([ settings, netrc ]) => { 178 .then(([ settings, netrc ]) => {
176 return getRemoteObjectOrDie(program, settings, netrc) 179 return getRemoteObjectOrDie(program, settings, netrc)
177 }) 180 })
178} 181}
179 182
180function getLogger (logLevel = 'info') { 183function getLogger (logLevel = 'info') {
@@ -211,7 +214,6 @@ function getLogger (logLevel = 'info') {
211 214
212export { 215export {
213 version, 216 version,
214 config,
215 getLogger, 217 getLogger,
216 getSettings, 218 getSettings,
217 getNetrc, 219 getNetrc,
@@ -222,5 +224,7 @@ export {
222 getServerCredentials, 224 getServerCredentials,
223 225
224 buildCommonVideoOptions, 226 buildCommonVideoOptions,
225 buildVideoAttributesFromCommander 227 buildVideoAttributesFromCommander,
228
229 getAdminTokenOrDie
226} 230}
diff --git a/server/tools/package.json b/server/tools/package.json
index 40959d76e..3821850c0 100644
--- a/server/tools/package.json
+++ b/server/tools/package.json
@@ -3,12 +3,13 @@
3 "version": "1.0.0", 3 "version": "1.0.0",
4 "private": true, 4 "private": true,
5 "dependencies": { 5 "dependencies": {
6 "application-config": "^1.0.1", 6 "application-config": "^2.0.0",
7 "cli-table": "^0.3.1", 7 "cli-table3": "^0.6.0",
8 "netrc-parser": "^3.1.6", 8 "netrc-parser": "^3.1.6",
9 "webtorrent-hybrid": "^4.0.1" 9 "webtorrent-hybrid": "^4.0.1"
10 }, 10 },
11 "summon": { 11 "summon": {
12 "silent": true 12 "silent": true
13 } 13 },
14 "devDependencies": {}
14} 15}
diff --git a/server/tools/peertube-auth.ts b/server/tools/peertube-auth.ts
index 6597a5c36..c1a804f83 100644
--- a/server/tools/peertube-auth.ts
+++ b/server/tools/peertube-auth.ts
@@ -1,3 +1,5 @@
1// eslint-disable @typescript-eslint/no-unnecessary-type-assertion
2
1import { registerTSPaths } from '../helpers/register-ts-paths' 3import { registerTSPaths } from '../helpers/register-ts-paths'
2registerTSPaths() 4registerTSPaths()
3 5
@@ -5,9 +7,8 @@ import * as program from 'commander'
5import * as prompt from 'prompt' 7import * as prompt from 'prompt'
6import { getNetrc, getSettings, writeSettings } from './cli' 8import { getNetrc, getSettings, writeSettings } from './cli'
7import { isUserUsernameValid } from '../helpers/custom-validators/users' 9import { isUserUsernameValid } from '../helpers/custom-validators/users'
8import { getAccessToken, login } from '../../shared/extra-utils' 10import { getAccessToken } from '../../shared/extra-utils'
9 11import * as CliTable3 from 'cli-table3'
10const Table = require('cli-table')
11 12
12async function delInstance (url: string) { 13async function delInstance (url: string) {
13 const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ]) 14 const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ])
@@ -108,10 +109,10 @@ program
108 .action(async () => { 109 .action(async () => {
109 const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ]) 110 const [ settings, netrc ] = await Promise.all([ getSettings(), getNetrc() ])
110 111
111 const table = new Table({ 112 const table = new CliTable3({
112 head: ['instance', 'login'], 113 head: [ 'instance', 'login' ],
113 colWidths: [30, 30] 114 colWidths: [ 30, 30 ]
114 }) 115 }) as any
115 116
116 settings.remotes.forEach(element => { 117 settings.remotes.forEach(element => {
117 if (!netrc.machines[element]) return 118 if (!netrc.machines[element]) return
@@ -132,7 +133,7 @@ program
132 .description('set an existing entry as default') 133 .description('set an existing entry as default')
133 .action(async url => { 134 .action(async url => {
134 const settings = await getSettings() 135 const settings = await getSettings()
135 const instanceExists = settings.remotes.indexOf(url) !== -1 136 const instanceExists = settings.remotes.includes(url)
136 137
137 if (instanceExists) { 138 if (instanceExists) {
138 settings.default = settings.remotes.indexOf(url) 139 settings.default = settings.remotes.indexOf(url)
diff --git a/server/tools/peertube-import-videos.ts b/server/tools/peertube-import-videos.ts
index eaa792763..2c9eabe98 100644
--- a/server/tools/peertube-import-videos.ts
+++ b/server/tools/peertube-import-videos.ts
@@ -1,10 +1,6 @@
1import { registerTSPaths } from '../helpers/register-ts-paths' 1import { registerTSPaths } from '../helpers/register-ts-paths'
2
3registerTSPaths() 2registerTSPaths()
4 3
5// FIXME: https://github.com/nodejs/node/pull/16853
6require('tls').DEFAULT_ECDH_CURVE = 'auto'
7
8import * as program from 'commander' 4import * as program from 'commander'
9import { join } from 'path' 5import { join } from 'path'
10import { doRequestAndSaveToFile } from '../helpers/requests' 6import { doRequestAndSaveToFile } from '../helpers/requests'
@@ -16,7 +12,7 @@ import { accessSync, constants } from 'fs'
16import { remove } from 'fs-extra' 12import { remove } from 'fs-extra'
17import { sha256 } from '../helpers/core-utils' 13import { sha256 } from '../helpers/core-utils'
18import { buildOriginallyPublishedAt, safeGetYoutubeDL } from '../helpers/youtube-dl' 14import { buildOriginallyPublishedAt, safeGetYoutubeDL } from '../helpers/youtube-dl'
19import { buildCommonVideoOptions, buildVideoAttributesFromCommander, getServerCredentials, getLogger } from './cli' 15import { buildCommonVideoOptions, buildVideoAttributesFromCommander, getLogger, getServerCredentials } from './cli'
20 16
21type UserInfo = { 17type UserInfo = {
22 username: string 18 username: string
@@ -42,32 +38,32 @@ command
42 .option('--first <first>', 'Process first n elements of returned playlist') 38 .option('--first <first>', 'Process first n elements of returned playlist')
43 .option('--last <last>', 'Process last n elements of returned playlist') 39 .option('--last <last>', 'Process last n elements of returned playlist')
44 .option('-T, --tmpdir <tmpdir>', 'Working directory', __dirname) 40 .option('-T, --tmpdir <tmpdir>', 'Working directory', __dirname)
41 .usage("[global options] [ -- youtube-dl options]")
45 .parse(process.argv) 42 .parse(process.argv)
46 43
47let log = getLogger(program[ 'verbose' ]) 44const log = getLogger(program['verbose'])
48 45
49getServerCredentials(command) 46getServerCredentials(command)
50 .then(({ url, username, password }) => { 47 .then(({ url, username, password }) => {
51 if (!program[ 'targetUrl' ]) { 48 if (!program['targetUrl']) {
52 exitError('--target-url field is required.') 49 exitError('--target-url field is required.')
53 } 50 }
54 51
55 try { 52 try {
56 accessSync(program[ 'tmpdir' ], constants.R_OK | constants.W_OK) 53 accessSync(program['tmpdir'], constants.R_OK | constants.W_OK)
57 } catch (e) { 54 } catch (e) {
58 exitError('--tmpdir %s: directory does not exist or is not accessible', program[ 'tmpdir' ]) 55 exitError('--tmpdir %s: directory does not exist or is not accessible', program['tmpdir'])
59 } 56 }
60 57
61 url = normalizeTargetUrl(url) 58 url = normalizeTargetUrl(url)
62 program[ 'targetUrl' ] = normalizeTargetUrl(program[ 'targetUrl' ]) 59 program['targetUrl'] = normalizeTargetUrl(program['targetUrl'])
63 60
64 const user = { username, password } 61 const user = { username, password }
65 62
66 run(url, user) 63 run(url, user)
67 .catch(err => { 64 .catch(err => exitError(err))
68 exitError(err)
69 })
70 }) 65 })
66 .catch(err => console.error(err))
71 67
72async function run (url: string, user: UserInfo) { 68async function run (url: string, user: UserInfo) {
73 if (!user.password) { 69 if (!user.password) {
@@ -76,43 +72,48 @@ async function run (url: string, user: UserInfo) {
76 72
77 const youtubeDL = await safeGetYoutubeDL() 73 const youtubeDL = await safeGetYoutubeDL()
78 74
79 const options = [ '-j', '--flat-playlist', '--playlist-reverse' ] 75 const options = [ '-j', '--flat-playlist', '--playlist-reverse', ...command.args ]
80 youtubeDL.getInfo(program[ 'targetUrl' ], options, processOptions, async (err, info) => { 76
77 youtubeDL.getInfo(program['targetUrl'], options, processOptions, async (err, info) => {
81 if (err) { 78 if (err) {
82 exitError(err.message) 79 exitError(err.stderr + ' ' + err.message)
83 } 80 }
84 81
85 let infoArray: any[] 82 let infoArray: any[]
86 83
87 // Normalize utf8 fields 84 // Normalize utf8 fields
88 infoArray = [].concat(info) 85 infoArray = [].concat(info)
89 if (program[ 'first' ]) { 86 if (program['first']) {
90 infoArray = infoArray.slice(0, program[ 'first' ]) 87 infoArray = infoArray.slice(0, program['first'])
91 } else if (program[ 'last' ]) { 88 } else if (program['last']) {
92 infoArray = infoArray.slice(-program[ 'last' ]) 89 infoArray = infoArray.slice(-program['last'])
93 } 90 }
94 infoArray = infoArray.map(i => normalizeObject(i)) 91 infoArray = infoArray.map(i => normalizeObject(i))
95 92
96 log.info('Will download and upload %d videos.\n', infoArray.length) 93 log.info('Will download and upload %d videos.\n', infoArray.length)
97 94
98 for (const info of infoArray) { 95 for (const info of infoArray) {
99 await processVideo({ 96 try {
100 cwd: program[ 'tmpdir' ], 97 await processVideo({
101 url, 98 cwd: program['tmpdir'],
102 user, 99 url,
103 youtubeInfo: info 100 user,
104 }) 101 youtubeInfo: info
102 })
103 } catch (err) {
104 console.error('Cannot process video.', { info, url })
105 }
105 } 106 }
106 107
107 log.info('Video/s for user %s imported: %s', user.username, program[ 'targetUrl' ]) 108 log.info('Video/s for user %s imported: %s', user.username, program['targetUrl'])
108 process.exit(0) 109 process.exit(0)
109 }) 110 })
110} 111}
111 112
112function processVideo (parameters: { 113function processVideo (parameters: {
113 cwd: string, 114 cwd: string
114 url: string, 115 url: string
115 user: { username: string, password: string }, 116 user: { username: string, password: string }
116 youtubeInfo: any 117 youtubeInfo: any
117}) { 118}) {
118 const { youtubeInfo, cwd, url, user } = parameters 119 const { youtubeInfo, cwd, url, user } = parameters
@@ -123,17 +124,17 @@ function processVideo (parameters: {
123 const videoInfo = await fetchObject(youtubeInfo) 124 const videoInfo = await fetchObject(youtubeInfo)
124 log.debug('Fetched object.', videoInfo) 125 log.debug('Fetched object.', videoInfo)
125 126
126 if (program[ 'since' ]) { 127 if (program['since']) {
127 if (buildOriginallyPublishedAt(videoInfo).getTime() < program[ 'since' ].getTime()) { 128 if (buildOriginallyPublishedAt(videoInfo).getTime() < program['since'].getTime()) {
128 log.info('Video "%s" has been published before "%s", don\'t upload it.\n', 129 log.info('Video "%s" has been published before "%s", don\'t upload it.\n',
129 videoInfo.title, formatDate(program[ 'since' ])) 130 videoInfo.title, formatDate(program['since']))
130 return res() 131 return res()
131 } 132 }
132 } 133 }
133 if (program[ 'until' ]) { 134 if (program['until']) {
134 if (buildOriginallyPublishedAt(videoInfo).getTime() > program[ 'until' ].getTime()) { 135 if (buildOriginallyPublishedAt(videoInfo).getTime() > program['until'].getTime()) {
135 log.info('Video "%s" has been published after "%s", don\'t upload it.\n', 136 log.info('Video "%s" has been published after "%s", don\'t upload it.\n',
136 videoInfo.title, formatDate(program[ 'until' ])) 137 videoInfo.title, formatDate(program['until']))
137 return res() 138 return res()
138 } 139 }
139 } 140 }
@@ -151,7 +152,7 @@ function processVideo (parameters: {
151 152
152 log.info('Downloading video "%s"...', videoInfo.title) 153 log.info('Downloading video "%s"...', videoInfo.title)
153 154
154 const options = [ '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best', '-o', path ] 155 const options = [ '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best', ...command.args, '-o', path ]
155 try { 156 try {
156 const youtubeDL = await safeGetYoutubeDL() 157 const youtubeDL = await safeGetYoutubeDL()
157 youtubeDL.exec(videoInfo.url, options, processOptions, async (err, output) => { 158 youtubeDL.exec(videoInfo.url, options, processOptions, async (err, output) => {
@@ -178,11 +179,11 @@ function processVideo (parameters: {
178} 179}
179 180
180async function uploadVideoOnPeerTube (parameters: { 181async function uploadVideoOnPeerTube (parameters: {
181 videoInfo: any, 182 videoInfo: any
182 videoPath: string, 183 videoPath: string
183 cwd: string, 184 cwd: string
184 url: string, 185 url: string
185 user: { username: string; password: string } 186 user: { username: string, password: string }
186}) { 187}) {
187 const { videoInfo, videoPath, cwd, url, user } = parameters 188 const { videoInfo, videoPath, cwd, url, user } = parameters
188 189
@@ -210,9 +211,9 @@ async function uploadVideoOnPeerTube (parameters: {
210 211
211 const defaultAttributes = { 212 const defaultAttributes = {
212 name: truncate(videoInfo.title, { 213 name: truncate(videoInfo.title, {
213 'length': CONSTRAINTS_FIELDS.VIDEOS.NAME.max, 214 length: CONSTRAINTS_FIELDS.VIDEOS.NAME.max,
214 'separator': /,? +/, 215 separator: /,? +/,
215 'omission': ' […]' 216 omission: ' […]'
216 }), 217 }),
217 category, 218 category,
218 licence, 219 licence,
@@ -259,7 +260,7 @@ async function uploadVideoOnPeerTube (parameters: {
259async function getCategory (categories: string[], url: string) { 260async function getCategory (categories: string[], url: string) {
260 if (!categories) return undefined 261 if (!categories) return undefined
261 262
262 const categoryString = categories[ 0 ] 263 const categoryString = categories[0]
263 264
264 if (categoryString === 'News & Politics') return 11 265 if (categoryString === 'News & Politics') return 11
265 266
@@ -267,7 +268,7 @@ async function getCategory (categories: string[], url: string) {
267 const categoriesServer = res.body 268 const categoriesServer = res.body
268 269
269 for (const key of Object.keys(categoriesServer)) { 270 for (const key of Object.keys(categoriesServer)) {
270 const categoryServer = categoriesServer[ key ] 271 const categoryServer = categoriesServer[key]
271 if (categoryString.toLowerCase() === categoryServer.toLowerCase()) return parseInt(key, 10) 272 if (categoryString.toLowerCase() === categoryServer.toLowerCase()) return parseInt(key, 10)
272 } 273 }
273 274
@@ -277,7 +278,7 @@ async function getCategory (categories: string[], url: string) {
277function getLicence (licence: string) { 278function getLicence (licence: string) {
278 if (!licence) return undefined 279 if (!licence) return undefined
279 280
280 if (licence.indexOf('Creative Commons Attribution licence') !== -1) return 1 281 if (licence.includes('Creative Commons Attribution licence')) return 1
281 282
282 return undefined 283 return undefined
283} 284}
@@ -289,12 +290,12 @@ function normalizeObject (obj: any) {
289 // Deprecated key 290 // Deprecated key
290 if (key === 'resolution') continue 291 if (key === 'resolution') continue
291 292
292 const value = obj[ key ] 293 const value = obj[key]
293 294
294 if (typeof value === 'string') { 295 if (typeof value === 'string') {
295 newObj[ key ] = value.normalize() 296 newObj[key] = value.normalize()
296 } else { 297 } else {
297 newObj[ key ] = value 298 newObj[key] = value
298 } 299 }
299 } 300 }
300 301
@@ -306,7 +307,7 @@ function fetchObject (info: any) {
306 307
307 return new Promise<any>(async (res, rej) => { 308 return new Promise<any>(async (res, rej) => {
308 const youtubeDL = await safeGetYoutubeDL() 309 const youtubeDL = await safeGetYoutubeDL()
309 youtubeDL.getInfo(url, undefined, processOptions, async (err, videoInfo) => { 310 youtubeDL.getInfo(url, undefined, processOptions, (err, videoInfo) => {
310 if (err) return rej(err) 311 if (err) return rej(err)
311 312
312 const videoInfoWithUrl = Object.assign(videoInfo, { url }) 313 const videoInfoWithUrl = Object.assign(videoInfo, { url })
@@ -317,10 +318,10 @@ function fetchObject (info: any) {
317 318
318function buildUrl (info: any) { 319function buildUrl (info: any) {
319 const webpageUrl = info.webpage_url as string 320 const webpageUrl = info.webpage_url as string
320 if (webpageUrl && webpageUrl.match(/^https?:\/\//)) return webpageUrl 321 if (webpageUrl?.match(/^https?:\/\//)) return webpageUrl
321 322
322 const url = info.url as string 323 const url = info.url as string
323 if (url && url.match(/^https?:\/\//)) return url 324 if (url?.match(/^https?:\/\//)) return url
324 325
325 // It seems youtube-dl does not return the video url 326 // It seems youtube-dl does not return the video url
326 return 'https://www.youtube.com/watch?v=' + info.id 327 return 'https://www.youtube.com/watch?v=' + info.id
@@ -388,7 +389,7 @@ function parseDate (dateAsStr: string): Date {
388} 389}
389 390
390function formatDate (date: Date): string { 391function formatDate (date: Date): string {
391 return date.toISOString().split('T')[ 0 ] 392 return date.toISOString().split('T')[0]
392} 393}
393 394
394function exitError (message: string, ...meta: any[]) { 395function exitError (message: string, ...meta: any[]) {
diff --git a/server/tools/peertube-plugins.ts b/server/tools/peertube-plugins.ts
index e40606107..05b75fab2 100644
--- a/server/tools/peertube-plugins.ts
+++ b/server/tools/peertube-plugins.ts
@@ -1,17 +1,15 @@
1// eslint-disable @typescript-eslint/no-unnecessary-type-assertion
2
1import { registerTSPaths } from '../helpers/register-ts-paths' 3import { registerTSPaths } from '../helpers/register-ts-paths'
2registerTSPaths() 4registerTSPaths()
3 5
4import * as program from 'commander' 6import * as program from 'commander'
5import { PluginType } from '../../shared/models/plugins/plugin.type' 7import { PluginType } from '../../shared/models/plugins/plugin.type'
6import { getAccessToken } from '../../shared/extra-utils/users/login'
7import { getMyUserInformation } from '../../shared/extra-utils/users/users'
8import { installPlugin, listPlugins, uninstallPlugin, updatePlugin } from '../../shared/extra-utils/server/plugins' 8import { installPlugin, listPlugins, uninstallPlugin, updatePlugin } from '../../shared/extra-utils/server/plugins'
9import { getServerCredentials } from './cli' 9import { getAdminTokenOrDie, getServerCredentials } from './cli'
10import { User, UserRole } from '../../shared/models/users'
11import { PeerTubePlugin } from '../../shared/models/plugins/peertube-plugin.model' 10import { PeerTubePlugin } from '../../shared/models/plugins/peertube-plugin.model'
12import { isAbsolute } from 'path' 11import { isAbsolute } from 'path'
13 12import * as CliTable3 from 'cli-table3'
14const Table = require('cli-table')
15 13
16program 14program
17 .name('plugins') 15 .name('plugins')
@@ -82,10 +80,10 @@ async function pluginsListCLI () {
82 }) 80 })
83 const plugins: PeerTubePlugin[] = res.body.data 81 const plugins: PeerTubePlugin[] = res.body.data
84 82
85 const table = new Table({ 83 const table = new CliTable3({
86 head: ['name', 'version', 'homepage'], 84 head: [ 'name', 'version', 'homepage' ],
87 colWidths: [ 50, 10, 50 ] 85 colWidths: [ 50, 10, 50 ]
88 }) 86 }) as any
89 87
90 for (const plugin of plugins) { 88 for (const plugin of plugins) {
91 const npmName = plugin.type === PluginType.PLUGIN 89 const npmName = plugin.type === PluginType.PLUGIN
@@ -128,7 +126,6 @@ async function installPluginCLI (options: any) {
128 } catch (err) { 126 } catch (err) {
129 console.error('Cannot install plugin.', err) 127 console.error('Cannot install plugin.', err)
130 process.exit(-1) 128 process.exit(-1)
131 return
132 } 129 }
133 130
134 console.log('Plugin installed.') 131 console.log('Plugin installed.')
@@ -160,7 +157,6 @@ async function updatePluginCLI (options: any) {
160 } catch (err) { 157 } catch (err) {
161 console.error('Cannot update plugin.', err) 158 console.error('Cannot update plugin.', err)
162 process.exit(-1) 159 process.exit(-1)
163 return
164 } 160 }
165 161
166 console.log('Plugin updated.') 162 console.log('Plugin updated.')
@@ -181,27 +177,13 @@ async function uninstallPluginCLI (options: any) {
181 await uninstallPlugin({ 177 await uninstallPlugin({
182 url, 178 url,
183 accessToken, 179 accessToken,
184 npmName: options[ 'npmName' ] 180 npmName: options['npmName']
185 }) 181 })
186 } catch (err) { 182 } catch (err) {
187 console.error('Cannot uninstall plugin.', err) 183 console.error('Cannot uninstall plugin.', err)
188 process.exit(-1) 184 process.exit(-1)
189 return
190 } 185 }
191 186
192 console.log('Plugin uninstalled.') 187 console.log('Plugin uninstalled.')
193 process.exit(0) 188 process.exit(0)
194} 189}
195
196async function getAdminTokenOrDie (url: string, username: string, password: string) {
197 const accessToken = await getAccessToken(url, username, password)
198 const resMe = await getMyUserInformation(url, accessToken)
199 const me: User = resMe.body
200
201 if (me.role !== UserRole.ADMINISTRATOR) {
202 console.error('Cannot list plugins if you are not administrator.')
203 process.exit(-1)
204 }
205
206 return accessToken
207}
diff --git a/server/tools/peertube-redundancy.ts b/server/tools/peertube-redundancy.ts
new file mode 100644
index 000000000..1ab58a438
--- /dev/null
+++ b/server/tools/peertube-redundancy.ts
@@ -0,0 +1,197 @@
1// eslint-disable @typescript-eslint/no-unnecessary-type-assertion
2
3import { registerTSPaths } from '../helpers/register-ts-paths'
4registerTSPaths()
5
6import * as program from 'commander'
7import { getAdminTokenOrDie, getServerCredentials } from './cli'
8import { VideoRedundanciesTarget, VideoRedundancy } from '@shared/models'
9import { addVideoRedundancy, listVideoRedundancies, removeVideoRedundancy } from '@shared/extra-utils/server/redundancy'
10import validator from 'validator'
11import * as CliTable3 from 'cli-table3'
12import { URL } from 'url'
13import { uniq } from 'lodash'
14
15import bytes = require('bytes')
16
17program
18 .name('plugins')
19 .usage('[command] [options]')
20
21program
22 .command('list-remote-redundancies')
23 .description('List remote redundancies on your videos')
24 .option('-u, --url <url>', 'Server url')
25 .option('-U, --username <username>', 'Username')
26 .option('-p, --password <token>', 'Password')
27 .action(() => listRedundanciesCLI('my-videos'))
28
29program
30 .command('list-my-redundancies')
31 .description('List your redundancies of remote videos')
32 .option('-u, --url <url>', 'Server url')
33 .option('-U, --username <username>', 'Username')
34 .option('-p, --password <token>', 'Password')
35 .action(() => listRedundanciesCLI('remote-videos'))
36
37program
38 .command('add')
39 .description('Duplicate a video in your redundancy system')
40 .option('-u, --url <url>', 'Server url')
41 .option('-U, --username <username>', 'Username')
42 .option('-p, --password <token>', 'Password')
43 .option('-v, --video <videoId>', 'Video id to duplicate')
44 .action((options) => addRedundancyCLI(options))
45
46program
47 .command('remove')
48 .description('Remove a video from your redundancies')
49 .option('-u, --url <url>', 'Server url')
50 .option('-U, --username <username>', 'Username')
51 .option('-p, --password <token>', 'Password')
52 .option('-v, --video <videoId>', 'Video id to remove from redundancies')
53 .action((options) => removeRedundancyCLI(options))
54
55if (!process.argv.slice(2).length) {
56 program.outputHelp()
57}
58
59program.parse(process.argv)
60
61// ----------------------------------------------------------------------------
62
63async function listRedundanciesCLI (target: VideoRedundanciesTarget) {
64 const { url, username, password } = await getServerCredentials(program)
65 const accessToken = await getAdminTokenOrDie(url, username, password)
66
67 const redundancies = await listVideoRedundanciesData(url, accessToken, target)
68
69 const table = new CliTable3({
70 head: [ 'video id', 'video name', 'video url', 'files', 'playlists', 'by instances', 'total size' ]
71 }) as any
72
73 for (const redundancy of redundancies) {
74 const webtorrentFiles = redundancy.redundancies.files
75 const streamingPlaylists = redundancy.redundancies.streamingPlaylists
76
77 let totalSize = ''
78 if (target === 'remote-videos') {
79 const tmp = webtorrentFiles.concat(streamingPlaylists)
80 .reduce((a, b) => a + b.size, 0)
81
82 totalSize = bytes(tmp)
83 }
84
85 const instances = uniq(
86 webtorrentFiles.concat(streamingPlaylists)
87 .map(r => r.fileUrl)
88 .map(u => new URL(u).host)
89 )
90
91 table.push([
92 redundancy.id.toString(),
93 redundancy.name,
94 redundancy.url,
95 webtorrentFiles.length,
96 streamingPlaylists.length,
97 instances.join('\n'),
98 totalSize
99 ])
100 }
101
102 console.log(table.toString())
103 process.exit(0)
104}
105
106async function addRedundancyCLI (options: { videoId: number }) {
107 const { url, username, password } = await getServerCredentials(program)
108 const accessToken = await getAdminTokenOrDie(url, username, password)
109
110 if (!options['video'] || validator.isInt('' + options['video']) === false) {
111 console.error('You need to specify the video id to duplicate and it should be a number.\n')
112 program.outputHelp()
113 process.exit(-1)
114 }
115
116 try {
117 await addVideoRedundancy({
118 url,
119 accessToken,
120 videoId: options['video']
121 })
122
123 console.log('Video will be duplicated by your instance!')
124
125 process.exit(0)
126 } catch (err) {
127 if (err.message.includes(409)) {
128 console.error('This video is already duplicated by your instance.')
129 } else if (err.message.includes(404)) {
130 console.error('This video id does not exist.')
131 } else {
132 console.error(err)
133 }
134
135 process.exit(-1)
136 }
137}
138
139async function removeRedundancyCLI (options: { videoId: number }) {
140 const { url, username, password } = await getServerCredentials(program)
141 const accessToken = await getAdminTokenOrDie(url, username, password)
142
143 if (!options['video'] || validator.isInt('' + options['video']) === false) {
144 console.error('You need to specify the video id to remove from your redundancies.\n')
145 program.outputHelp()
146 process.exit(-1)
147 }
148
149 const videoId = parseInt(options['video'] + '', 10)
150
151 let redundancies = await listVideoRedundanciesData(url, accessToken, 'my-videos')
152 let videoRedundancy = redundancies.find(r => videoId === r.id)
153
154 if (!videoRedundancy) {
155 redundancies = await listVideoRedundanciesData(url, accessToken, 'remote-videos')
156 videoRedundancy = redundancies.find(r => videoId === r.id)
157 }
158
159 if (!videoRedundancy) {
160 console.error('Video redundancy not found.')
161 process.exit(-1)
162 }
163
164 try {
165 const ids = videoRedundancy.redundancies.files
166 .concat(videoRedundancy.redundancies.streamingPlaylists)
167 .map(r => r.id)
168
169 for (const id of ids) {
170 await removeVideoRedundancy({
171 url,
172 accessToken,
173 redundancyId: id
174 })
175 }
176
177 console.log('Video redundancy removed!')
178
179 process.exit(0)
180 } catch (err) {
181 console.error(err)
182 process.exit(-1)
183 }
184}
185
186async function listVideoRedundanciesData (url: string, accessToken: string, target: VideoRedundanciesTarget) {
187 const res = await listVideoRedundancies({
188 url,
189 accessToken,
190 start: 0,
191 count: 100,
192 sort: 'name',
193 target
194 })
195
196 return res.body.data as VideoRedundancy[]
197}
diff --git a/server/tools/peertube-repl.ts b/server/tools/peertube-repl.ts
index ab6e215d9..a5c35e9ea 100644
--- a/server/tools/peertube-repl.ts
+++ b/server/tools/peertube-repl.ts
@@ -4,14 +4,10 @@ registerTSPaths()
4import * as repl from 'repl' 4import * as repl from 'repl'
5import * as path from 'path' 5import * as path from 'path'
6import * as _ from 'lodash' 6import * as _ from 'lodash'
7import * as uuidv1 from 'uuid/v1' 7import { uuidv1, uuidv3, uuidv4, uuidv5 } from 'uuid'
8import * as uuidv3 from 'uuid/v3'
9import * as uuidv4 from 'uuid/v4'
10import * as uuidv5 from 'uuid/v5'
11import * as Sequelize from 'sequelize' 8import * as Sequelize from 'sequelize'
12import * as YoutubeDL from 'youtube-dl' 9import * as YoutubeDL from 'youtube-dl'
13 10import { initDatabaseModels, sequelizeTypescript } from '../initializers/database'
14import { initDatabaseModels, sequelizeTypescript } from '../initializers'
15import * as cli from '../tools/cli' 11import * as cli from '../tools/cli'
16import { logger } from '../helpers/logger' 12import { logger } from '../helpers/logger'
17import * as constants from '../initializers/constants' 13import * as constants from '../initializers/constants'
@@ -31,22 +27,39 @@ const start = async () => {
31 const initContext = (replServer) => { 27 const initContext = (replServer) => {
32 return (context) => { 28 return (context) => {
33 const properties = { 29 const properties = {
34 context, repl: replServer, env: process.env, 30 context,
35 lodash: _, path, 31 repl: replServer,
36 uuidv1, uuidv3, uuidv4, uuidv5, 32 env: process.env,
37 cli, logger, constants, 33 lodash: _,
38 Sequelize, sequelizeTypescript, modelsUtils, 34 path,
39 models: sequelizeTypescript.models, transaction: sequelizeTypescript.transaction, 35 uuidv1,
40 query: sequelizeTypescript.query, queryInterface: sequelizeTypescript.getQueryInterface(), 36 uuidv3,
37 uuidv4,
38 uuidv5,
39 cli,
40 logger,
41 constants,
42 Sequelize,
43 sequelizeTypescript,
44 modelsUtils,
45 models: sequelizeTypescript.models,
46 transaction: sequelizeTypescript.transaction,
47 query: sequelizeTypescript.query,
48 queryInterface: sequelizeTypescript.getQueryInterface(),
41 YoutubeDL, 49 YoutubeDL,
42 coreUtils, ffmpegUtils, peertubeCryptoUtils, signupUtils, utils, YoutubeDLUtils 50 coreUtils,
51 ffmpegUtils,
52 peertubeCryptoUtils,
53 signupUtils,
54 utils,
55 YoutubeDLUtils
43 } 56 }
44 57
45 for (let prop in properties) { 58 for (const prop in properties) {
46 Object.defineProperty(context, prop, { 59 Object.defineProperty(context, prop, {
47 configurable: false, 60 configurable: false,
48 enumerable: true, 61 enumerable: true,
49 value: properties[ prop ] 62 value: properties[prop]
50 }) 63 })
51 } 64 }
52 } 65 }
diff --git a/server/tools/peertube-upload.ts b/server/tools/peertube-upload.ts
index f604c9bee..8de952e7b 100644
--- a/server/tools/peertube-upload.ts
+++ b/server/tools/peertube-upload.ts
@@ -24,14 +24,14 @@ command
24 24
25getServerCredentials(command) 25getServerCredentials(command)
26 .then(({ url, username, password }) => { 26 .then(({ url, username, password }) => {
27 if (!program[ 'videoName' ] || !program[ 'file' ]) { 27 if (!program['videoName'] || !program['file']) {
28 if (!program[ 'videoName' ]) console.error('--video-name is required.') 28 if (!program['videoName']) console.error('--video-name is required.')
29 if (!program[ 'file' ]) console.error('--file is required.') 29 if (!program['file']) console.error('--file is required.')
30 30
31 process.exit(-1) 31 process.exit(-1)
32 } 32 }
33 33
34 if (isAbsolute(program[ 'file' ]) === false) { 34 if (isAbsolute(program['file']) === false) {
35 console.error('File path should be absolute.') 35 console.error('File path should be absolute.')
36 process.exit(-1) 36 process.exit(-1)
37 } 37 }
@@ -41,25 +41,26 @@ getServerCredentials(command)
41 process.exit(-1) 41 process.exit(-1)
42 }) 42 })
43 }) 43 })
44 .catch(err => console.error(err))
44 45
45async function run (url: string, username: string, password: string) { 46async function run (url: string, username: string, password: string) {
46 const accessToken = await getAccessToken(url, username, password) 47 const accessToken = await getAccessToken(url, username, password)
47 48
48 await access(program[ 'file' ], constants.F_OK) 49 await access(program['file'], constants.F_OK)
49 50
50 console.log('Uploading %s video...', program[ 'videoName' ]) 51 console.log('Uploading %s video...', program['videoName'])
51 52
52 const videoAttributes = await buildVideoAttributesFromCommander(url, program) 53 const videoAttributes = await buildVideoAttributesFromCommander(url, program)
53 54
54 Object.assign(videoAttributes, { 55 Object.assign(videoAttributes, {
55 fixture: program[ 'file' ], 56 fixture: program['file'],
56 thumbnailfile: program[ 'thumbnail' ], 57 thumbnailfile: program['thumbnail'],
57 previewfile: program[ 'preview' ] 58 previewfile: program['preview']
58 }) 59 })
59 60
60 try { 61 try {
61 await uploadVideo(url, accessToken, videoAttributes) 62 await uploadVideo(url, accessToken, videoAttributes)
62 console.log(`Video ${program[ 'videoName' ]} uploaded.`) 63 console.log(`Video ${program['videoName']} uploaded.`)
63 process.exit(0) 64 process.exit(0)
64 } catch (err) { 65 } catch (err) {
65 console.error(require('util').inspect(err)) 66 console.error(require('util').inspect(err))
diff --git a/server/tools/peertube-watch.ts b/server/tools/peertube-watch.ts
index 9ac1d05f9..b8e750a37 100644
--- a/server/tools/peertube-watch.ts
+++ b/server/tools/peertube-watch.ts
@@ -29,16 +29,10 @@ program
29 console.log(' $ peertube watch https://peertube.cpy.re/videos/watch/e8a1af4e-414a-4d58-bfe6-2146eed06d10') 29 console.log(' $ peertube watch https://peertube.cpy.re/videos/watch/e8a1af4e-414a-4d58-bfe6-2146eed06d10')
30 console.log() 30 console.log()
31 }) 31 })
32 .action((url, cmd) => { 32 .action((url, cmd) => run(url, cmd))
33 run(url, cmd)
34 .catch(err => {
35 console.error(err)
36 process.exit(-1)
37 })
38 })
39 .parse(process.argv) 33 .parse(process.argv)
40 34
41async function run (url: string, program: any) { 35function run (url: string, program: any) {
42 if (!url) { 36 if (!url) {
43 console.error('<url> positional argument is required.') 37 console.error('<url> positional argument is required.')
44 process.exit(-1) 38 process.exit(-1)
@@ -49,5 +43,10 @@ async function run (url: string, program: any) {
49 url.replace('videos/watch', 'download/torrents') + 43 url.replace('videos/watch', 'download/torrents') +
50 `-${program.resolution}.torrent` 44 `-${program.resolution}.torrent`
51 45
52 execSync(cmd + args) 46 try {
47 execSync(cmd + args)
48 } catch (err) {
49 console.error('Cannto exec command.', err)
50 process.exit(-1)
51 }
53} 52}
diff --git a/server/tools/peertube.ts b/server/tools/peertube.ts
index fc85c4210..88dd5f7f6 100644
--- a/server/tools/peertube.ts
+++ b/server/tools/peertube.ts
@@ -1,13 +1,12 @@
1#!/usr/bin/env node 1#!/usr/bin/env node
2 2
3/* eslint-disable no-useless-escape */
4
3import { registerTSPaths } from '../helpers/register-ts-paths' 5import { registerTSPaths } from '../helpers/register-ts-paths'
4registerTSPaths() 6registerTSPaths()
5 7
6import * as program from 'commander' 8import * as program from 'commander'
7import { 9import { getSettings, version } from './cli'
8 version,
9 getSettings
10} from './cli'
11 10
12program 11program
13 .version(version, '-v, --version') 12 .version(version, '-v, --version')
@@ -22,17 +21,19 @@ program
22 .command('watch', 'watch a video in the terminal ✩°。⋆').alias('w') 21 .command('watch', 'watch a video in the terminal ✩°。⋆').alias('w')
23 .command('repl', 'initiate a REPL to access internals') 22 .command('repl', 'initiate a REPL to access internals')
24 .command('plugins [action]', 'manage instance plugins/themes').alias('p') 23 .command('plugins [action]', 'manage instance plugins/themes').alias('p')
24 .command('redundancy [action]', 'manage instance redundancies').alias('r')
25 25
26/* Not Yet Implemented */ 26/* Not Yet Implemented */
27program 27program
28 .command('diagnostic [action]', 28 .command(
29 'like couple therapy, but for your instance', 29 'diagnostic [action]',
30 { noHelp: true } as program.CommandOptions 30 'like couple therapy, but for your instance',
31 ).alias('d') 31 { noHelp: true } as program.CommandOptions
32 ).alias('d')
32 .command('admin', 33 .command('admin',
33 'manage an instance where you have elevated rights', 34 'manage an instance where you have elevated rights',
34 { noHelp: true } as program.CommandOptions 35 { noHelp: true } as program.CommandOptions
35 ).alias('a') 36 ).alias('a')
36 37
37// help on no command 38// help on no command
38if (!process.argv.slice(2).length) { 39if (!process.argv.slice(2).length) {
@@ -81,3 +82,4 @@ getSettings()
81 }) 82 })
82 .parse(process.argv) 83 .parse(process.argv)
83 }) 84 })
85 .catch(err => console.error(err))
diff --git a/server/tools/yarn.lock b/server/tools/yarn.lock
index 28756cbc2..7faa26eaa 100644
--- a/server/tools/yarn.lock
+++ b/server/tools/yarn.lock
@@ -2,6 +2,27 @@
2# yarn lockfile v1 2# yarn lockfile v1
3 3
4 4
5"@babel/code-frame@^7.0.0":
6 version "7.8.3"
7 resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e"
8 integrity sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==
9 dependencies:
10 "@babel/highlight" "^7.8.3"
11
12"@babel/helper-validator-identifier@^7.9.0":
13 version "7.9.0"
14 resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.0.tgz#ad53562a7fc29b3b9a91bbf7d10397fd146346ed"
15 integrity sha512-6G8bQKjOh+of4PV/ThDm/rRqlU7+IGoJuofpagU5GlEl29Vv0RGqqt86ZGRV8ZuSOY3o+8yXl5y782SMcG7SHw==
16
17"@babel/highlight@^7.8.3":
18 version "7.9.0"
19 resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.9.0.tgz#4e9b45ccb82b79607271b2979ad82c7b68163079"
20 integrity sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==
21 dependencies:
22 "@babel/helper-validator-identifier" "^7.9.0"
23 chalk "^2.0.0"
24 js-tokens "^4.0.0"
25
5"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": 26"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
6 version "1.1.2" 27 version "1.1.2"
7 resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" 28 resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"
@@ -56,14 +77,14 @@
56 integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= 77 integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=
57 78
58"@types/long@^4.0.0": 79"@types/long@^4.0.0":
59 version "4.0.0" 80 version "4.0.1"
60 resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.0.tgz#719551d2352d301ac8b81db732acb6bdc28dbdef" 81 resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9"
61 integrity sha512-1w52Nyx4Gq47uuu0EVcsHBxZFJgurQ+rTKS3qMHxR1GY2T8c2AJYd6vZoZ9q1rupaDjU0yT+Jc2XTyXkjeMA+Q== 82 integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==
62 83
63"@types/node@^10.1.0": 84"@types/node@^10.1.0":
64 version "10.14.22" 85 version "10.17.18"
65 resolved "https://registry.yarnpkg.com/@types/node/-/node-10.14.22.tgz#34bcdf6b6cb5fc0db33d24816ad9d3ece22feea4" 86 resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.18.tgz#ae364d97382aacdebf583fa4e7132af2dfe56a0c"
66 integrity sha512-9taxKC944BqoTVjE+UT3pQH0nHZlTvITwfsOZqyc+R3sfJuxaTtxWjfn1K2UlxyPcKHf0rnaXcVFrS9F9vf0bw== 87 integrity sha512-DQ2hl/Jl3g33KuAUOcMrcAOtsbzb+y/ufakzAdeK9z/H/xsvkpbETZZbPNMIiQuk24f5ZRMCcZIViAwyFIiKmg==
67 88
68abbrev@1: 89abbrev@1:
69 version "1.1.1" 90 version "1.1.1"
@@ -93,18 +114,31 @@ ansi-regex@^3.0.0:
93 resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" 114 resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
94 integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= 115 integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=
95 116
117ansi-regex@^5.0.0:
118 version "5.0.0"
119 resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75"
120 integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==
121
122ansi-styles@^3.2.1:
123 version "3.2.1"
124 resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
125 integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
126 dependencies:
127 color-convert "^1.9.0"
128
96application-config-path@^0.1.0: 129application-config-path@^0.1.0:
97 version "0.1.0" 130 version "0.1.0"
98 resolved "https://registry.yarnpkg.com/application-config-path/-/application-config-path-0.1.0.tgz#193c5f0a86541a4c66fba1e2dc38583362ea5e8f" 131 resolved "https://registry.yarnpkg.com/application-config-path/-/application-config-path-0.1.0.tgz#193c5f0a86541a4c66fba1e2dc38583362ea5e8f"
99 integrity sha1-GTxfCoZUGkxm+6Hi3DhYM2LqXo8= 132 integrity sha1-GTxfCoZUGkxm+6Hi3DhYM2LqXo8=
100 133
101application-config@^1.0.1: 134application-config@^2.0.0:
102 version "1.0.1" 135 version "2.0.0"
103 resolved "https://registry.yarnpkg.com/application-config/-/application-config-1.0.1.tgz#5aa2e2a5ed6abd2e5d1d473d3596f574044fe9e7" 136 resolved "https://registry.yarnpkg.com/application-config/-/application-config-2.0.0.tgz#15b4d54d61c0c082f9802227e3e85de876b47747"
104 integrity sha1-WqLipe1qvS5dHUc9NZb1dARP6ec= 137 integrity sha512-NC5/0guSZK3/UgUDfCk/riByXzqz0owL1L3r63JPSBzYk5QALrp3bLxbsR7qeSfvYfFmAhnp3dbqYsW3U9MpZQ==
105 dependencies: 138 dependencies:
106 application-config-path "^0.1.0" 139 application-config-path "^0.1.0"
107 mkdirp "^0.5.1" 140 load-json-file "^6.2.0"
141 write-json-file "^4.2.0"
108 142
109aproba@^1.0.3: 143aproba@^1.0.3:
110 version "1.2.0" 144 version "1.2.0"
@@ -119,11 +153,6 @@ are-we-there-yet@~1.1.2:
119 delegates "^1.0.0" 153 delegates "^1.0.0"
120 readable-stream "^2.0.6" 154 readable-stream "^2.0.6"
121 155
122async-limiter@^1.0.0:
123 version "1.0.1"
124 resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
125 integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
126
127balanced-match@^1.0.0: 156balanced-match@^1.0.0:
128 version "1.0.0" 157 version "1.0.0"
129 resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" 158 resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
@@ -181,9 +210,9 @@ bittorrent-protocol@^3.0.0:
181 unordered-array-remove "^1.0.2" 210 unordered-array-remove "^1.0.2"
182 211
183bittorrent-tracker@^9.0.0: 212bittorrent-tracker@^9.0.0:
184 version "9.14.4" 213 version "9.14.5"
185 resolved "https://registry.yarnpkg.com/bittorrent-tracker/-/bittorrent-tracker-9.14.4.tgz#0d9661560e6fec37689dfc5045142772eac05536" 214 resolved "https://registry.yarnpkg.com/bittorrent-tracker/-/bittorrent-tracker-9.14.5.tgz#aa5573ba91c003581cb337c2889226137f65f32a"
186 integrity sha512-2Y/MNRjYhysD6t4r38z7l1WTT7g23IAqRWZRsj7xnnpciFn4xE4qiKmyFwA4gtbFGAZ14K3DdaqZbiQsC3PEfQ== 215 integrity sha512-Y1ng5r2qGCgDldjd9eYL8Mv1DjCo6eljqC+T6IMcwmYx0h20KNPKTxJkyNT5gaeJkAhM+p+jmhlV7/ty535Txg==
187 dependencies: 216 dependencies:
188 bencode "^2.0.0" 217 bencode "^2.0.0"
189 bittorrent-peerid "^1.0.2" 218 bittorrent-peerid "^1.0.2"
@@ -203,7 +232,6 @@ bittorrent-tracker@^9.0.0:
203 simple-peer "^9.0.0" 232 simple-peer "^9.0.0"
204 simple-websocket "^8.0.0" 233 simple-websocket "^8.0.0"
205 string2compact "^1.1.1" 234 string2compact "^1.1.1"
206 uniq "^1.0.1"
207 unordered-array-remove "^1.0.2" 235 unordered-array-remove "^1.0.2"
208 ws "^7.0.0" 236 ws "^7.0.0"
209 optionalDependencies: 237 optionalDependencies:
@@ -223,9 +251,9 @@ block-stream2@^2.0.0:
223 readable-stream "^3.4.0" 251 readable-stream "^3.4.0"
224 252
225bn.js@^5.0.0: 253bn.js@^5.0.0:
226 version "5.0.0" 254 version "5.1.1"
227 resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.0.0.tgz#5c3d398021b3ddb548c1296a16f857e908f35c70" 255 resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.1.tgz#48efc4031a9c4041b9c99c6941d903463ab62eb5"
228 integrity sha512-bVwDX8AF+72fIUNuARelKAlQUNtPOfG2fRxorbVvFk4zpHbqLrPdOGfVg5vrKwVzLLePqPBiATaOZNELQzmS0A== 256 integrity sha512-IUTD/REb78Z2eodka1QZyyEk66pciRcP6Sroka0aI3tG/iwIdYLrBD62RsubR7vqdt3WyX8p4jxeatzmRSphtA==
229 257
230brace-expansion@^1.1.7: 258brace-expansion@^1.1.7:
231 version "1.1.11" 259 version "1.1.11"
@@ -291,15 +319,24 @@ castv2@~0.1.4:
291 debug "^4.1.1" 319 debug "^4.1.1"
292 protobufjs "^6.8.8" 320 protobufjs "^6.8.8"
293 321
322chalk@^2.0.0:
323 version "2.4.2"
324 resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
325 integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
326 dependencies:
327 ansi-styles "^3.2.1"
328 escape-string-regexp "^1.0.5"
329 supports-color "^5.3.0"
330
294charset@^1.0.1: 331charset@^1.0.1:
295 version "1.0.1" 332 version "1.0.1"
296 resolved "https://registry.yarnpkg.com/charset/-/charset-1.0.1.tgz#8d59546c355be61049a8fa9164747793319852bd" 333 resolved "https://registry.yarnpkg.com/charset/-/charset-1.0.1.tgz#8d59546c355be61049a8fa9164747793319852bd"
297 integrity sha512-6dVyOOYjpfFcL1Y4qChrAoQLRHvj2ziyhcm0QJlhOcAhykL/k1kTUPbeo+87MNRTRdk2OIIsIXbuF3x2wi5EXg== 334 integrity sha512-6dVyOOYjpfFcL1Y4qChrAoQLRHvj2ziyhcm0QJlhOcAhykL/k1kTUPbeo+87MNRTRdk2OIIsIXbuF3x2wi5EXg==
298 335
299chownr@^1.1.1: 336chownr@^1.1.1:
300 version "1.1.3" 337 version "1.1.4"
301 resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.3.tgz#42d837d5239688d55f303003a508230fa6727142" 338 resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
302 integrity sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw== 339 integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
303 340
304chrome-dgram@^3.0.2: 341chrome-dgram@^3.0.2:
305 version "3.0.4" 342 version "3.0.4"
@@ -347,12 +384,15 @@ chunk-store-stream@^4.0.0:
347 block-stream2 "^2.0.0" 384 block-stream2 "^2.0.0"
348 readable-stream "^3.4.0" 385 readable-stream "^3.4.0"
349 386
350cli-table@^0.3.1: 387cli-table3@^0.6.0:
351 version "0.3.1" 388 version "0.6.0"
352 resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23" 389 resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.0.tgz#b7b1bc65ca8e7b5cef9124e13dc2b21e2ce4faee"
353 integrity sha1-9TsFJmqLGguTSz0IIebi3FkUriM= 390 integrity sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ==
354 dependencies: 391 dependencies:
355 colors "1.0.3" 392 object-assign "^4.1.0"
393 string-width "^4.2.0"
394 optionalDependencies:
395 colors "^1.1.2"
356 396
357clivas@^0.2.0: 397clivas@^0.2.0:
358 version "0.2.0" 398 version "0.2.0"
@@ -364,10 +404,22 @@ code-point-at@^1.0.0:
364 resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" 404 resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
365 integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= 405 integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
366 406
367colors@1.0.3: 407color-convert@^1.9.0:
368 version "1.0.3" 408 version "1.9.3"
369 resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" 409 resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
370 integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs= 410 integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
411 dependencies:
412 color-name "1.1.3"
413
414color-name@1.1.3:
415 version "1.1.3"
416 resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
417 integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
418
419colors@^1.1.2:
420 version "1.4.0"
421 resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
422 integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
371 423
372common-tags@^1.8.0: 424common-tags@^1.8.0:
373 version "1.8.0" 425 version "1.8.0"
@@ -475,18 +527,16 @@ deep-extend@^0.6.0:
475 resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" 527 resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
476 integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== 528 integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
477 529
478define-properties@^1.1.2, define-properties@^1.1.3:
479 version "1.1.3"
480 resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
481 integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
482 dependencies:
483 object-keys "^1.0.12"
484
485delegates@^1.0.0: 530delegates@^1.0.0:
486 version "1.0.0" 531 version "1.0.0"
487 resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" 532 resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
488 integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= 533 integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
489 534
535detect-indent@^6.0.0:
536 version "6.0.0"
537 resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.0.0.tgz#0abd0f549f69fc6659a254fe96786186b6f528fd"
538 integrity sha512-oSyFlqaTHCItVRGK5RmrmjB+CmaMOW7IaNA/kdxqhoa6d17j/5ce9O9eWXmV/KEdRwqpQA+Vqe8a8Bsybu4YnA==
539
490detect-libc@^1.0.2: 540detect-libc@^1.0.2:
491 version "1.0.3" 541 version "1.0.3"
492 resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" 542 resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
@@ -529,9 +579,9 @@ domexception@^1.0.1:
529 webidl-conversions "^4.0.2" 579 webidl-conversions "^4.0.2"
530 580
531ecstatic@^4.0.0: 581ecstatic@^4.0.0:
532 version "4.1.2" 582 version "4.1.4"
533 resolved "https://registry.yarnpkg.com/ecstatic/-/ecstatic-4.1.2.tgz#3afbe29849b32bc2a1f8a90f67e01dc048c7ad40" 583 resolved "https://registry.yarnpkg.com/ecstatic/-/ecstatic-4.1.4.tgz#86bf340dabe56c4d0c93d406ac36c040f68e1d79"
534 integrity sha512-lnrAOpU2f7Ra8dm1pW0D1ucyUxQIEk8RjFrvROg1YqCV0ueVu9hzgiSEbSyROqXDDiHREdqC4w3AwOTb23P4UQ== 584 integrity sha512-8E4ZLK4uRuB9pwywGpy/B9vcz4gCp6IY7u4cMbeCINr/fjb1v+0wf0Ae2XlfSnG8xZYnE4uaJBjFkYI0bqcIdw==
535 dependencies: 585 dependencies:
536 charset "^1.0.1" 586 charset "^1.0.1"
537 he "^1.1.1" 587 he "^1.1.1"
@@ -552,6 +602,18 @@ elementtree@^0.1.6, elementtree@~0.1.6:
552 dependencies: 602 dependencies:
553 sax "1.1.4" 603 sax "1.1.4"
554 604
605emoji-regex@^8.0.0:
606 version "8.0.0"
607 resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
608 integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
609
610end-of-stream@1.4.1:
611 version "1.4.1"
612 resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43"
613 integrity sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==
614 dependencies:
615 once "^1.4.0"
616
555end-of-stream@^1.1.0: 617end-of-stream@^1.1.0:
556 version "1.4.4" 618 version "1.4.4"
557 resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" 619 resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
@@ -559,36 +621,23 @@ end-of-stream@^1.1.0:
559 dependencies: 621 dependencies:
560 once "^1.4.0" 622 once "^1.4.0"
561 623
562es-abstract@^1.5.1: 624error-ex@^1.3.1:
563 version "1.16.0" 625 version "1.3.2"
564 resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.16.0.tgz#d3a26dc9c3283ac9750dca569586e976d9dcc06d" 626 resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
565 integrity sha512-xdQnfykZ9JMEiasTAJZJdMWCQ1Vm00NBw79/AWi7ELfZuuPCSOMDZbT9mkOfSctVtfhb+sAAzrm+j//GjjLHLg== 627 integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
566 dependencies:
567 es-to-primitive "^1.2.0"
568 function-bind "^1.1.1"
569 has "^1.0.3"
570 has-symbols "^1.0.0"
571 is-callable "^1.1.4"
572 is-regex "^1.0.4"
573 object-inspect "^1.6.0"
574 object-keys "^1.1.1"
575 string.prototype.trimleft "^2.1.0"
576 string.prototype.trimright "^2.1.0"
577
578es-to-primitive@^1.2.0:
579 version "1.2.0"
580 resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377"
581 integrity sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==
582 dependencies: 628 dependencies:
583 is-callable "^1.1.4" 629 is-arrayish "^0.2.1"
584 is-date-object "^1.0.1"
585 is-symbol "^1.0.2"
586 630
587escape-html@^1.0.3: 631escape-html@^1.0.3:
588 version "1.0.3" 632 version "1.0.3"
589 resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 633 resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
590 integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= 634 integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
591 635
636escape-string-regexp@^1.0.5:
637 version "1.0.5"
638 resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
639 integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
640
592execa@^0.10.0: 641execa@^0.10.0:
593 version "0.10.0" 642 version "0.10.0"
594 resolved "https://registry.yarnpkg.com/execa/-/execa-0.10.0.tgz#ff456a8f53f90f8eccc71a96d11bdfc7f082cb50" 643 resolved "https://registry.yarnpkg.com/execa/-/execa-0.10.0.tgz#ff456a8f53f90f8eccc71a96d11bdfc7f082cb50"
@@ -645,11 +694,6 @@ fs.realpath@^1.0.0:
645 resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 694 resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
646 integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= 695 integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
647 696
648function-bind@^1.1.1:
649 version "1.1.1"
650 resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
651 integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
652
653gauge@~2.7.3: 697gauge@~2.7.3:
654 version "2.7.4" 698 version "2.7.4"
655 resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" 699 resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
@@ -680,9 +724,9 @@ get-stream@^3.0.0:
680 integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= 724 integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=
681 725
682glob@^7.1.3: 726glob@^7.1.3:
683 version "7.1.4" 727 version "7.1.6"
684 resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" 728 resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
685 integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A== 729 integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
686 dependencies: 730 dependencies:
687 fs.realpath "^1.0.0" 731 fs.realpath "^1.0.0"
688 inflight "^1.0.4" 732 inflight "^1.0.4"
@@ -691,23 +735,21 @@ glob@^7.1.3:
691 once "^1.3.0" 735 once "^1.3.0"
692 path-is-absolute "^1.0.0" 736 path-is-absolute "^1.0.0"
693 737
694has-symbols@^1.0.0: 738graceful-fs@^4.1.15:
695 version "1.0.0" 739 version "4.2.3"
696 resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" 740 resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423"
697 integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q= 741 integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==
742
743has-flag@^3.0.0:
744 version "3.0.0"
745 resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
746 integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
698 747
699has-unicode@^2.0.0: 748has-unicode@^2.0.0:
700 version "2.0.1" 749 version "2.0.1"
701 resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" 750 resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
702 integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= 751 integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=
703 752
704has@^1.0.1, has@^1.0.3:
705 version "1.0.3"
706 resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
707 integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
708 dependencies:
709 function-bind "^1.1.1"
710
711he@^1.1.1: 753he@^1.1.1:
712 version "1.2.0" 754 version "1.2.0"
713 resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" 755 resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
@@ -747,6 +789,11 @@ immediate-chunk-store@^2.0.0:
747 dependencies: 789 dependencies:
748 queue-microtask "^1.1.2" 790 queue-microtask "^1.1.2"
749 791
792imurmurhash@^0.1.4:
793 version "0.1.4"
794 resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
795 integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
796
750inflight@^1.0.4: 797inflight@^1.0.4:
751 version "1.0.6" 798 version "1.0.6"
752 resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 799 resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
@@ -782,20 +829,20 @@ ip@^1.0.1, ip@^1.1.0, ip@^1.1.3:
782 resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" 829 resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
783 integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== 830 integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
784 831
832is-arrayish@^0.2.1:
833 version "0.2.1"
834 resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
835 integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=
836
785is-ascii@^1.0.0: 837is-ascii@^1.0.0:
786 version "1.0.0" 838 version "1.0.0"
787 resolved "https://registry.yarnpkg.com/is-ascii/-/is-ascii-1.0.0.tgz#f02ad0259a0921cd199ff21ce1b09e0f6b4e3929" 839 resolved "https://registry.yarnpkg.com/is-ascii/-/is-ascii-1.0.0.tgz#f02ad0259a0921cd199ff21ce1b09e0f6b4e3929"
788 integrity sha1-8CrQJZoJIc0Zn/Ic4bCeD2tOOSk= 840 integrity sha1-8CrQJZoJIc0Zn/Ic4bCeD2tOOSk=
789 841
790is-callable@^1.1.4: 842is-docker@^2.0.0:
791 version "1.1.4" 843 version "2.0.0"
792 resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" 844 resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.0.0.tgz#2cb0df0e75e2d064fe1864c37cdeacb7b2dcf25b"
793 integrity sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA== 845 integrity sha512-pJEdRugimx4fBMra5z2/5iRdZ63OhYV0vr0Dwm5+xtW4D1FvRkB8hamMIhnWfyJeDdyr/aa7BDyNbtG38VxgoQ==
794
795is-date-object@^1.0.1:
796 version "1.0.1"
797 resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16"
798 integrity sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=
799 846
800is-file@^1.0.0: 847is-file@^1.0.0:
801 version "1.0.0" 848 version "1.0.0"
@@ -814,31 +861,27 @@ is-fullwidth-code-point@^2.0.0:
814 resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" 861 resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
815 integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= 862 integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=
816 863
817is-regex@^1.0.4: 864is-fullwidth-code-point@^3.0.0:
818 version "1.0.4" 865 version "3.0.0"
819 resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" 866 resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
820 integrity sha1-VRdIm1RwkbCTDglWVM7SXul+lJE= 867 integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
821 dependencies: 868
822 has "^1.0.1" 869is-plain-obj@^2.0.0:
870 version "2.1.0"
871 resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
872 integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==
823 873
824is-stream@^1.1.0: 874is-stream@^1.1.0:
825 version "1.1.0" 875 version "1.1.0"
826 resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" 876 resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
827 integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= 877 integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
828 878
829is-symbol@^1.0.2:
830 version "1.0.2"
831 resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.2.tgz#a055f6ae57192caee329e7a860118b497a950f38"
832 integrity sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==
833 dependencies:
834 has-symbols "^1.0.0"
835
836is-typedarray@^1.0.0: 879is-typedarray@^1.0.0:
837 version "1.0.0" 880 version "1.0.0"
838 resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" 881 resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
839 integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= 882 integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
840 883
841is-wsl@^2.1.0: 884is-wsl@^2.1.1:
842 version "2.1.1" 885 version "2.1.1"
843 resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.1.1.tgz#4a1c152d429df3d441669498e2486d3596ebaf1d" 886 resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.1.1.tgz#4a1c152d429df3d441669498e2486d3596ebaf1d"
844 integrity sha512-umZHcSrwlDHo2TGMXv0DZ8dIUGunZ2Iv68YZnrmCiBPkZ4aaOhtv7pXJKeki9k3qJ3RJr0cDyitcl5wEH3AYog== 887 integrity sha512-umZHcSrwlDHo2TGMXv0DZ8dIUGunZ2Iv68YZnrmCiBPkZ4aaOhtv7pXJKeki9k3qJ3RJr0cDyitcl5wEH3AYog==
@@ -853,6 +896,16 @@ isexe@^2.0.0:
853 resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" 896 resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
854 integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= 897 integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
855 898
899js-tokens@^4.0.0:
900 version "4.0.0"
901 resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
902 integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
903
904json-parse-better-errors@^1.0.1:
905 version "1.0.2"
906 resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
907 integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
908
856junk@^3.1.0: 909junk@^3.1.0:
857 version "3.1.0" 910 version "3.1.0"
858 resolved "https://registry.yarnpkg.com/junk/-/junk-3.1.0.tgz#31499098d902b7e98c5d9b9c80f43457a88abfa1" 911 resolved "https://registry.yarnpkg.com/junk/-/junk-3.1.0.tgz#31499098d902b7e98c5d9b9c80f43457a88abfa1"
@@ -889,6 +942,11 @@ last-one-wins@^1.0.4:
889 resolved "https://registry.yarnpkg.com/last-one-wins/-/last-one-wins-1.0.4.tgz#c1bfd0cbcb46790ec9156b8d1aee8fcb86cda22a" 942 resolved "https://registry.yarnpkg.com/last-one-wins/-/last-one-wins-1.0.4.tgz#c1bfd0cbcb46790ec9156b8d1aee8fcb86cda22a"
890 integrity sha1-wb/Qy8tGeQ7JFWuNGu6Py4bNoio= 943 integrity sha1-wb/Qy8tGeQ7JFWuNGu6Py4bNoio=
891 944
945lines-and-columns@^1.1.6:
946 version "1.1.6"
947 resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
948 integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
949
892load-ip-set@^2.1.0: 950load-ip-set@^2.1.0:
893 version "2.1.0" 951 version "2.1.0"
894 resolved "https://registry.yarnpkg.com/load-ip-set/-/load-ip-set-2.1.0.tgz#2d50b737cae41de4e413d213991d4083a3e1784b" 952 resolved "https://registry.yarnpkg.com/load-ip-set/-/load-ip-set-2.1.0.tgz#2d50b737cae41de4e413d213991d4083a3e1784b"
@@ -900,6 +958,16 @@ load-ip-set@^2.1.0:
900 simple-get "^3.0.0" 958 simple-get "^3.0.0"
901 split "^1.0.0" 959 split "^1.0.0"
902 960
961load-json-file@^6.2.0:
962 version "6.2.0"
963 resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-6.2.0.tgz#5c7770b42cafa97074ca2848707c61662f4251a1"
964 integrity sha512-gUD/epcRms75Cw8RT1pUdHugZYM5ce64ucs2GEISABwkRsOQr0q2wm/MV2TKThycIe5e0ytRweW2RZxclogCdQ==
965 dependencies:
966 graceful-fs "^4.1.15"
967 parse-json "^5.0.0"
968 strip-bom "^4.0.0"
969 type-fest "^0.6.0"
970
903long@^4.0.0: 971long@^4.0.0:
904 version "4.0.0" 972 version "4.0.0"
905 resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" 973 resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28"
@@ -920,6 +988,13 @@ magnet-uri@^5.1.3:
920 thirty-two "^1.0.1" 988 thirty-two "^1.0.1"
921 uniq "^1.0.1" 989 uniq "^1.0.1"
922 990
991make-dir@^3.0.0:
992 version "3.0.2"
993 resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.0.2.tgz#04a1acbf22221e1d6ef43559f43e05a90dbb4392"
994 integrity sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w==
995 dependencies:
996 semver "^6.0.0"
997
923mdns-js-packet@~0.2.0: 998mdns-js-packet@~0.2.0:
924 version "0.2.0" 999 version "0.2.0"
925 resolved "https://registry.yarnpkg.com/mdns-js-packet/-/mdns-js-packet-0.2.0.tgz#642409e8183c7561cc60615bbd1420ec2fad7616" 1000 resolved "https://registry.yarnpkg.com/mdns-js-packet/-/mdns-js-packet-0.2.0.tgz#642409e8183c7561cc60615bbd1420ec2fad7616"
@@ -967,9 +1042,9 @@ mimic-response@^1.0.0:
967 integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== 1042 integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==
968 1043
969mimic-response@^2.0.0: 1044mimic-response@^2.0.0:
970 version "2.0.0" 1045 version "2.1.0"
971 resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.0.0.tgz#996a51c60adf12cb8a87d7fb8ef24c2f3d5ebb46" 1046 resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43"
972 integrity sha512-8ilDoEapqA4uQ3TwS0jakGONKXVJqpy+RpM+3b7pLdOjghCrEiGp9SRkFbUHAmZW9vdnrENWHjaweIoTIJExSQ== 1047 integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==
973 1048
974minimatch@^3.0.4: 1049minimatch@^3.0.4:
975 version "3.0.4" 1050 version "3.0.4"
@@ -978,15 +1053,10 @@ minimatch@^3.0.4:
978 dependencies: 1053 dependencies:
979 brace-expansion "^1.1.7" 1054 brace-expansion "^1.1.7"
980 1055
981minimist@0.0.8: 1056minimist@^1.1.0, minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5:
982 version "0.0.8" 1057 version "1.2.5"
983 resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" 1058 resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
984 integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= 1059 integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
985
986minimist@^1.1.0, minimist@^1.1.1, minimist@^1.2.0:
987 version "1.2.0"
988 resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
989 integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
990 1060
991minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0: 1061minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0:
992 version "2.9.0" 1062 version "2.9.0"
@@ -1003,12 +1073,17 @@ minizlib@^1.2.1:
1003 dependencies: 1073 dependencies:
1004 minipass "^2.9.0" 1074 minipass "^2.9.0"
1005 1075
1076mkdirp-classic@^0.5.2:
1077 version "0.5.2"
1078 resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.2.tgz#54c441ce4c96cd7790e10b41a87aa51068ecab2b"
1079 integrity sha512-ejdnDQcR75gwknmMw/tx02AuRs8jCtqFoFqDZMjiNxsu85sRIJVXDKHuLYvUUPRBUtV2FpSZa9bL1BUa3BdR2g==
1080
1006mkdirp@^0.5.0, mkdirp@^0.5.1: 1081mkdirp@^0.5.0, mkdirp@^0.5.1:
1007 version "0.5.1" 1082 version "0.5.4"
1008 resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" 1083 resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.4.tgz#fd01504a6797ec5c9be81ff43d204961ed64a512"
1009 integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= 1084 integrity sha512-iG9AK/dJLtJ0XNgTuDbSyNS3zECqDlAhnQW4CsNxBG3LQJBbHmRX1egw39DmtOdCAqY+dKXV+sgPgilNWUKMVw==
1010 dependencies: 1085 dependencies:
1011 minimist "0.0.8" 1086 minimist "^1.2.5"
1012 1087
1013moment@^2.12.0: 1088moment@^2.12.0:
1014 version "2.24.0" 1089 version "2.24.0"
@@ -1057,9 +1132,9 @@ multistream@^4.0.0:
1057 readable-stream "^3.4.0" 1132 readable-stream "^3.4.0"
1058 1133
1059needle@^2.2.1: 1134needle@^2.2.1:
1060 version "2.4.0" 1135 version "2.3.3"
1061 resolved "https://registry.yarnpkg.com/needle/-/needle-2.4.0.tgz#6833e74975c444642590e15a750288c5f939b57c" 1136 resolved "https://registry.yarnpkg.com/needle/-/needle-2.3.3.tgz#a041ad1d04a871b0ebb666f40baaf1fb47867117"
1062 integrity sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg== 1137 integrity sha512-EkY0GeSq87rWp1hoq/sH/wnTWgFVhYlnIkbJ0YJFfRgEFlz2RraCjBpFQ+vrEgEdp0ThfyHADmkChEhcb7PKyw==
1063 dependencies: 1138 dependencies:
1064 debug "^3.2.6" 1139 debug "^3.2.6"
1065 iconv-lite "^0.4.4" 1140 iconv-lite "^0.4.4"
@@ -1130,25 +1205,33 @@ nodebmc@0.0.7:
1130 mdns-js "0.5.0" 1205 mdns-js "0.5.0"
1131 1206
1132nopt@^4.0.1: 1207nopt@^4.0.1:
1133 version "4.0.1" 1208 version "4.0.3"
1134 resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" 1209 resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.3.tgz#a375cad9d02fd921278d954c2254d5aa57e15e48"
1135 integrity sha1-0NRoWv1UFRk8jHUFYC0NF81kR00= 1210 integrity sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==
1136 dependencies: 1211 dependencies:
1137 abbrev "1" 1212 abbrev "1"
1138 osenv "^0.1.4" 1213 osenv "^0.1.4"
1139 1214
1140npm-bundled@^1.0.1: 1215npm-bundled@^1.0.1:
1141 version "1.0.6" 1216 version "1.1.1"
1142 resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.6.tgz#e7ba9aadcef962bb61248f91721cd932b3fe6bdd" 1217 resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.1.1.tgz#1edd570865a94cdb1bc8220775e29466c9fb234b"
1143 integrity sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g== 1218 integrity sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==
1219 dependencies:
1220 npm-normalize-package-bin "^1.0.1"
1221
1222npm-normalize-package-bin@^1.0.1:
1223 version "1.0.1"
1224 resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz#6e79a41f23fd235c0623218228da7d9c23b8f6e2"
1225 integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==
1144 1226
1145npm-packlist@^1.1.6: 1227npm-packlist@^1.1.6:
1146 version "1.4.6" 1228 version "1.4.8"
1147 resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.6.tgz#53ba3ed11f8523079f1457376dd379ee4ea42ff4" 1229 resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.8.tgz#56ee6cc135b9f98ad3d51c1c95da22bbb9b2ef3e"
1148 integrity sha512-u65uQdb+qwtGvEJh/DgQgW1Xg7sqeNbmxYyrvlNznaVTjV3E5P6F/EFjM+BVHXl7JJlsdG8A64M0XI8FI/IOlg== 1230 integrity sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==
1149 dependencies: 1231 dependencies:
1150 ignore-walk "^3.0.1" 1232 ignore-walk "^3.0.1"
1151 npm-bundled "^1.0.1" 1233 npm-bundled "^1.0.1"
1234 npm-normalize-package-bin "^1.0.1"
1152 1235
1153npm-run-path@^2.0.0: 1236npm-run-path@^2.0.0:
1154 version "2.0.2" 1237 version "2.0.2"
@@ -1177,24 +1260,6 @@ object-assign@^4.1.0:
1177 resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" 1260 resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
1178 integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= 1261 integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
1179 1262
1180object-inspect@^1.6.0:
1181 version "1.6.0"
1182 resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.6.0.tgz#c70b6cbf72f274aab4c34c0c82f5167bf82cf15b"
1183 integrity sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ==
1184
1185object-keys@^1.0.12, object-keys@^1.1.1:
1186 version "1.1.1"
1187 resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
1188 integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
1189
1190object.getownpropertydescriptors@^2.0.3:
1191 version "2.0.3"
1192 resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16"
1193 integrity sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=
1194 dependencies:
1195 define-properties "^1.1.2"
1196 es-abstract "^1.5.1"
1197
1198on-finished@^2.3.0: 1263on-finished@^2.3.0:
1199 version "2.3.0" 1264 version "2.3.0"
1200 resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" 1265 resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
@@ -1210,11 +1275,12 @@ once@^1.3.0, once@^1.3.1, once@^1.3.3, once@^1.4.0:
1210 wrappy "1" 1275 wrappy "1"
1211 1276
1212open@^7.0.0: 1277open@^7.0.0:
1213 version "7.0.0" 1278 version "7.0.3"
1214 resolved "https://registry.yarnpkg.com/open/-/open-7.0.0.tgz#7e52999b14eb73f90f0f0807fe93897c4ae73ec9" 1279 resolved "https://registry.yarnpkg.com/open/-/open-7.0.3.tgz#db551a1af9c7ab4c7af664139930826138531c48"
1215 integrity sha512-K6EKzYqnwQzk+/dzJAQSBORub3xlBTxMz+ntpZpH/LyCa1o6KjXhuN+2npAaI9jaSmU3R1Q8NWf4KUWcyytGsQ== 1280 integrity sha512-sP2ru2v0P290WFfv49Ap8MF6PkzGNnGlAwHweB4WR4mr5d2d0woiCluUeJ218w7/+PmoBy9JmYgD5A4mLcWOFA==
1216 dependencies: 1281 dependencies:
1217 is-wsl "^2.1.0" 1282 is-docker "^2.0.0"
1283 is-wsl "^2.1.1"
1218 1284
1219os-homedir@^1.0.0: 1285os-homedir@^1.0.0:
1220 version "1.0.2" 1286 version "1.0.2"
@@ -1246,15 +1312,25 @@ package-json-versionify@^1.0.2:
1246 dependencies: 1312 dependencies:
1247 browserify-package-json "^1.0.0" 1313 browserify-package-json "^1.0.0"
1248 1314
1315parse-json@^5.0.0:
1316 version "5.0.0"
1317 resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.0.0.tgz#73e5114c986d143efa3712d4ea24db9a4266f60f"
1318 integrity sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==
1319 dependencies:
1320 "@babel/code-frame" "^7.0.0"
1321 error-ex "^1.3.1"
1322 json-parse-better-errors "^1.0.1"
1323 lines-and-columns "^1.1.6"
1324
1249parse-numeric-range@^0.0.2: 1325parse-numeric-range@^0.0.2:
1250 version "0.0.2" 1326 version "0.0.2"
1251 resolved "https://registry.yarnpkg.com/parse-numeric-range/-/parse-numeric-range-0.0.2.tgz#b4f09d413c7adbcd987f6e9233c7b4b210c938e4" 1327 resolved "https://registry.yarnpkg.com/parse-numeric-range/-/parse-numeric-range-0.0.2.tgz#b4f09d413c7adbcd987f6e9233c7b4b210c938e4"
1252 integrity sha1-tPCdQTx6282Yf26SM8e0shDJOOQ= 1328 integrity sha1-tPCdQTx6282Yf26SM8e0shDJOOQ=
1253 1329
1254parse-torrent@^7.0.0: 1330parse-torrent@^7.0.0:
1255 version "7.0.1" 1331 version "7.1.2"
1256 resolved "https://registry.yarnpkg.com/parse-torrent/-/parse-torrent-7.0.1.tgz#669c51a95363550055c7de0957741d6a05575daf" 1332 resolved "https://registry.yarnpkg.com/parse-torrent/-/parse-torrent-7.1.2.tgz#4ecde4b3be2729ba2b6f336040910d6fe4649d19"
1257 integrity sha512-FdF1kBImRLt+ICV4NTz8L+sI2hFlPXAq1tXuw21gKz8EuThyVUFJ/wPfBEyYQrvnBpmGf7cM/LVSOhMRe8MrKw== 1333 integrity sha512-1boHRA+aV7aeZBIg0rMBYhtfizAd/BXCXOCh/klYrgVnSpUAuJUIzQrIGkCsb93U1KOVN6C3NZOgpNy8htmqgw==
1258 dependencies: 1334 dependencies:
1259 bencode "^2.0.0" 1335 bencode "^2.0.0"
1260 blob-to-buffer "^1.2.6" 1336 blob-to-buffer "^1.2.6"
@@ -1262,7 +1338,6 @@ parse-torrent@^7.0.0:
1262 magnet-uri "^5.1.3" 1338 magnet-uri "^5.1.3"
1263 simple-get "^3.0.1" 1339 simple-get "^3.0.1"
1264 simple-sha1 "^3.0.0" 1340 simple-sha1 "^3.0.0"
1265 uniq "^1.0.1"
1266 1341
1267path-is-absolute@^1.0.0: 1342path-is-absolute@^1.0.0:
1268 version "1.0.1" 1343 version "1.0.1"
@@ -1303,9 +1378,9 @@ process-nextick-args@~2.0.0:
1303 integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== 1378 integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
1304 1379
1305protobufjs@^6.8.8: 1380protobufjs@^6.8.8:
1306 version "6.8.8" 1381 version "6.8.9"
1307 resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.8.8.tgz#c8b4f1282fd7a90e6f5b109ed11c84af82908e7c" 1382 resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.8.9.tgz#0b1adbcdaa983d369c3d9108a97c814edc030754"
1308 integrity sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw== 1383 integrity sha512-j2JlRdUeL/f4Z6x4aU4gj9I2LECglC+5qR2TrWb193Tla1qfdaNQTZ8I27Pt7K0Ajmvjjpft7O3KWTGciz4gpw==
1309 dependencies: 1384 dependencies:
1310 "@protobufjs/aspromise" "^1.1.2" 1385 "@protobufjs/aspromise" "^1.1.2"
1311 "@protobufjs/base64" "^1.1.2" 1386 "@protobufjs/base64" "^1.1.2"
@@ -1340,17 +1415,17 @@ queue-microtask@^1.1.0, queue-microtask@^1.1.2:
1340 integrity sha512-F9wwNePtXrzZenAB3ax0Y8TSKGvuB7Qw16J30hspEUTbfUM+H827XyN3rlpwhVmtm5wuZtbKIHjOnwDn7MUxWQ== 1415 integrity sha512-F9wwNePtXrzZenAB3ax0Y8TSKGvuB7Qw16J30hspEUTbfUM+H827XyN3rlpwhVmtm5wuZtbKIHjOnwDn7MUxWQ==
1341 1416
1342random-access-file@^2.0.1: 1417random-access-file@^2.0.1:
1343 version "2.1.3" 1418 version "2.1.4"
1344 resolved "https://registry.yarnpkg.com/random-access-file/-/random-access-file-2.1.3.tgz#642c4b29e39c7dd91609a2e912f174d70fd4f82a" 1419 resolved "https://registry.yarnpkg.com/random-access-file/-/random-access-file-2.1.4.tgz#d783e9082d08094c08c6f3dd481f37b2079709dc"
1345 integrity sha512-AE0Z1ywR5gIkzACMC1lCsR6LP8g4ynNm7oYWYdKPSSU6Y3H+mGDJxBqfcV9B9KstfHNemhfX3nYmx99ZC9f/yg== 1420 integrity sha512-WAcBP5iLhg1pbjZA40WyMenjK7c5gJUY6Pi5HJ3fLJCeVFNSZv3juf20yFMKxBdvcX5GKbX/HZSfFzlLBdGTdQ==
1346 dependencies: 1421 dependencies:
1347 mkdirp "^0.5.1" 1422 mkdirp-classic "^0.5.2"
1348 random-access-storage "^1.1.1" 1423 random-access-storage "^1.1.1"
1349 1424
1350random-access-storage@^1.1.1: 1425random-access-storage@^1.1.1:
1351 version "1.4.0" 1426 version "1.4.1"
1352 resolved "https://registry.yarnpkg.com/random-access-storage/-/random-access-storage-1.4.0.tgz#cbe5b5ccfb38680aac7b78a050d9f0a5ef36841f" 1427 resolved "https://registry.yarnpkg.com/random-access-storage/-/random-access-storage-1.4.1.tgz#39a524dd428ade9161ce61a8ae677766e6117ffb"
1353 integrity sha512-7oszloM/+PdqWp/oFGyL6SeI14liqo8AAisHAZQGkWdHISyAnngKjNPL0JYIazeLxbHPY6oed2yUffowdq/o6A== 1428 integrity sha512-DbCc2TIzOxPaHF6KCbr8zLtiYOJQQQCBHUVNHV/SckUQobCBB2YkDtbLdxGnPwPNpJfEyMWxDAm36A2xkbxxtw==
1354 dependencies: 1429 dependencies:
1355 inherits "^2.0.3" 1430 inherits "^2.0.3"
1356 1431
@@ -1389,9 +1464,9 @@ rc@^1.2.7:
1389 strip-json-comments "~2.0.1" 1464 strip-json-comments "~2.0.1"
1390 1465
1391readable-stream@^2.0.6, readable-stream@^2.2.2: 1466readable-stream@^2.0.6, readable-stream@^2.2.2:
1392 version "2.3.6" 1467 version "2.3.7"
1393 resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" 1468 resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
1394 integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== 1469 integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
1395 dependencies: 1470 dependencies:
1396 core-util-is "~1.0.0" 1471 core-util-is "~1.0.0"
1397 inherits "~2.0.3" 1472 inherits "~2.0.3"
@@ -1402,9 +1477,9 @@ readable-stream@^2.0.6, readable-stream@^2.2.2:
1402 util-deprecate "~1.0.1" 1477 util-deprecate "~1.0.1"
1403 1478
1404readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0: 1479readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0:
1405 version "3.4.0" 1480 version "3.6.0"
1406 resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.4.0.tgz#a51c26754658e0a3c21dbf59163bd45ba6f447fc" 1481 resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
1407 integrity sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ== 1482 integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
1408 dependencies: 1483 dependencies:
1409 inherits "^2.0.3" 1484 inherits "^2.0.3"
1410 string_decoder "^1.1.1" 1485 string_decoder "^1.1.1"
@@ -1434,9 +1509,9 @@ rimraf@^2.6.1:
1434 glob "^7.1.3" 1509 glob "^7.1.3"
1435 1510
1436rimraf@^3.0.0: 1511rimraf@^3.0.0:
1437 version "3.0.0" 1512 version "3.0.2"
1438 resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.0.tgz#614176d4b3010b75e5c390eb0ee96f6dc0cebb9b" 1513 resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
1439 integrity sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg== 1514 integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
1440 dependencies: 1515 dependencies:
1441 glob "^7.1.3" 1516 glob "^7.1.3"
1442 1517
@@ -1490,6 +1565,11 @@ semver@^5.3.0, semver@^5.5.0:
1490 resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" 1565 resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
1491 integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== 1566 integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
1492 1567
1568semver@^6.0.0:
1569 version "6.3.0"
1570 resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
1571 integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
1572
1493semver@~5.1.0: 1573semver@~5.1.0:
1494 version "5.1.1" 1574 version "5.1.1"
1495 resolved "https://registry.yarnpkg.com/semver/-/semver-5.1.1.tgz#a3292a373e6f3e0798da0b20641b9a9c5bc47e19" 1575 resolved "https://registry.yarnpkg.com/semver/-/semver-5.1.1.tgz#a3292a373e6f3e0798da0b20641b9a9c5bc47e19"
@@ -1512,10 +1592,10 @@ shebang-regex@^1.0.0:
1512 resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" 1592 resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
1513 integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= 1593 integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
1514 1594
1515signal-exit@^3.0.0: 1595signal-exit@^3.0.0, signal-exit@^3.0.2:
1516 version "3.0.2" 1596 version "3.0.3"
1517 resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" 1597 resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
1518 integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= 1598 integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
1519 1599
1520simple-concat@^1.0.0: 1600simple-concat@^1.0.0:
1521 version "1.0.0" 1601 version "1.0.0"
@@ -1541,9 +1621,9 @@ simple-get@^3.0.0, simple-get@^3.0.1:
1541 simple-concat "^1.0.0" 1621 simple-concat "^1.0.0"
1542 1622
1543simple-peer@^9.0.0: 1623simple-peer@^9.0.0:
1544 version "9.6.0" 1624 version "9.6.2"
1545 resolved "https://registry.yarnpkg.com/simple-peer/-/simple-peer-9.6.0.tgz#1560653c2f5360c122f7912cfdb32e8124f5e2c4" 1625 resolved "https://registry.yarnpkg.com/simple-peer/-/simple-peer-9.6.2.tgz#42418e77cf8f9184e4fa22ef1017b195c2bf84d7"
1546 integrity sha512-NYqSKPu75xhkZYKGJhCbLCG5kfBtDHf8U9ddk4EKFfYNU7XgIisov+V8wMbVVgyMCfn8pm8uOqQQmE50FPDFWA== 1626 integrity sha512-EOKoImCaqtNvXIntxT1CBBK/3pVi7tMAoJ3shdyd9qk3zLm3QPiRLb/sPC1G2xvKJkJc5fkQjCXqRZ0AknwTig==
1547 dependencies: 1627 dependencies:
1548 debug "^4.0.1" 1628 debug "^4.0.1"
1549 get-browser-rtc "^1.0.0" 1629 get-browser-rtc "^1.0.0"
@@ -1560,15 +1640,23 @@ simple-sha1@^3.0.0, simple-sha1@^3.0.1:
1560 rusha "^0.8.1" 1640 rusha "^0.8.1"
1561 1641
1562simple-websocket@^8.0.0: 1642simple-websocket@^8.0.0:
1563 version "8.0.1" 1643 version "8.1.1"
1564 resolved "https://registry.yarnpkg.com/simple-websocket/-/simple-websocket-8.0.1.tgz#c28af779034b329d0cf1448a45fdd311d21fa289" 1644 resolved "https://registry.yarnpkg.com/simple-websocket/-/simple-websocket-8.1.1.tgz#4fd68cb1301c1253b2607cfe0950a8be37e6116a"
1565 integrity sha512-2QKSRjf+tqFXLVmOQjf95gHeKhuyx2k1ouDjtnE0uKCYw84HfN85HsXo+GmPH+2PIh5BQql++g2AIbHgGAZU4w== 1645 integrity sha512-06I3cwOD5Q3LdVd6qfyDGp1U9eau9x9qniSL3b/aDgM5bsJX4nZfCuii2UCFcTfrDq0jCXF4NQ/38qeC8CJZTg==
1566 dependencies: 1646 dependencies:
1567 debug "^4.1.1" 1647 debug "^4.1.1"
1648 queue-microtask "^1.1.0"
1568 randombytes "^2.0.3" 1649 randombytes "^2.0.3"
1569 readable-stream "^3.1.1" 1650 readable-stream "^3.1.1"
1570 ws "^7.0.0" 1651 ws "^7.0.0"
1571 1652
1653sort-keys@^4.0.0:
1654 version "4.0.0"
1655 resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-4.0.0.tgz#56dc5e256637bfe3fec8db0dc57c08b1a2be22d6"
1656 integrity sha512-hlJLzrn/VN49uyNkZ8+9b+0q9DjmmYcYOnbMQtpkLrYpPwRApDPZfmqbUfJnAA3sb/nRib+nDot7Zi/1ER1fuA==
1657 dependencies:
1658 is-plain-obj "^2.0.0"
1659
1572speedometer@^1.0.0: 1660speedometer@^1.0.0:
1573 version "1.1.0" 1661 version "1.1.0"
1574 resolved "https://registry.yarnpkg.com/speedometer/-/speedometer-1.1.0.tgz#a30b13abda45687a1a76977012c060f2ac8a7934" 1662 resolved "https://registry.yarnpkg.com/speedometer/-/speedometer-1.1.0.tgz#a30b13abda45687a1a76977012c060f2ac8a7934"
@@ -1617,21 +1705,14 @@ string-width@^1.0.1:
1617 is-fullwidth-code-point "^2.0.0" 1705 is-fullwidth-code-point "^2.0.0"
1618 strip-ansi "^4.0.0" 1706 strip-ansi "^4.0.0"
1619 1707
1620string.prototype.trimleft@^2.1.0: 1708string-width@^4.2.0:
1621 version "2.1.0" 1709 version "4.2.0"
1622 resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz#6cc47f0d7eb8d62b0f3701611715a3954591d634" 1710 resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5"
1623 integrity sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw== 1711 integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==
1624 dependencies:
1625 define-properties "^1.1.3"
1626 function-bind "^1.1.1"
1627
1628string.prototype.trimright@^2.1.0:
1629 version "2.1.0"
1630 resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz#669d164be9df9b6f7559fa8e89945b168a5a6c58"
1631 integrity sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg==
1632 dependencies: 1712 dependencies:
1633 define-properties "^1.1.3" 1713 emoji-regex "^8.0.0"
1634 function-bind "^1.1.1" 1714 is-fullwidth-code-point "^3.0.0"
1715 strip-ansi "^6.0.0"
1635 1716
1636string2compact@^1.1.1, string2compact@^1.2.5: 1717string2compact@^1.1.1, string2compact@^1.2.5:
1637 version "1.3.0" 1718 version "1.3.0"
@@ -1669,6 +1750,18 @@ strip-ansi@^4.0.0:
1669 dependencies: 1750 dependencies:
1670 ansi-regex "^3.0.0" 1751 ansi-regex "^3.0.0"
1671 1752
1753strip-ansi@^6.0.0:
1754 version "6.0.0"
1755 resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532"
1756 integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==
1757 dependencies:
1758 ansi-regex "^5.0.0"
1759
1760strip-bom@^4.0.0:
1761 version "4.0.0"
1762 resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878"
1763 integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==
1764
1672strip-eof@^1.0.0: 1765strip-eof@^1.0.0:
1673 version "1.0.0" 1766 version "1.0.0"
1674 resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" 1767 resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
@@ -1679,6 +1772,13 @@ strip-json-comments@~2.0.1:
1679 resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" 1772 resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
1680 integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= 1773 integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
1681 1774
1775supports-color@^5.3.0:
1776 version "5.5.0"
1777 resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
1778 integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
1779 dependencies:
1780 has-flag "^3.0.0"
1781
1682tar@^4: 1782tar@^4:
1683 version "4.4.13" 1783 version "4.4.13"
1684 resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525" 1784 resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525"
@@ -1732,7 +1832,12 @@ torrent-piece@^2.0.0:
1732 resolved "https://registry.yarnpkg.com/torrent-piece/-/torrent-piece-2.0.0.tgz#6598ae67d93699e887f178db267ba16d89d7ec9b" 1832 resolved "https://registry.yarnpkg.com/torrent-piece/-/torrent-piece-2.0.0.tgz#6598ae67d93699e887f178db267ba16d89d7ec9b"
1733 integrity sha512-H/Z/yCuvZJj1vl1IQHI8dvF2QrUuXRJoptT5DW5967/dsLpXlCg+uyhFR5lfNj5mNaYePUbKtnL+qKWZGXv4Nw== 1833 integrity sha512-H/Z/yCuvZJj1vl1IQHI8dvF2QrUuXRJoptT5DW5967/dsLpXlCg+uyhFR5lfNj5mNaYePUbKtnL+qKWZGXv4Nw==
1734 1834
1735typedarray-to-buffer@^3.0.0: 1835type-fest@^0.6.0:
1836 version "0.6.0"
1837 resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b"
1838 integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==
1839
1840typedarray-to-buffer@^3.0.0, typedarray-to-buffer@^3.1.5:
1736 version "3.1.5" 1841 version "3.1.5"
1737 resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" 1842 resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
1738 integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== 1843 integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==
@@ -1816,14 +1921,6 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1:
1816 resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" 1921 resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
1817 integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= 1922 integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
1818 1923
1819util.promisify@~1.0.0:
1820 version "1.0.0"
1821 resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030"
1822 integrity sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==
1823 dependencies:
1824 define-properties "^1.1.2"
1825 object.getownpropertydescriptors "^2.0.3"
1826
1827videostream@^3.2.0: 1924videostream@^3.2.0:
1828 version "3.2.1" 1925 version "3.2.1"
1829 resolved "https://registry.yarnpkg.com/videostream/-/videostream-3.2.1.tgz#643688ad4bfbf37570d421e3196b7e0ad38eeebc" 1926 resolved "https://registry.yarnpkg.com/videostream/-/videostream-3.2.1.tgz#643688ad4bfbf37570d421e3196b7e0ad38eeebc"
@@ -1886,9 +1983,9 @@ webtorrent-hybrid@^4.0.1:
1886 wrtc "^0.4.1" 1983 wrtc "^0.4.1"
1887 1984
1888webtorrent@>=0.107.6: 1985webtorrent@>=0.107.6:
1889 version "0.107.16" 1986 version "0.108.1"
1890 resolved "https://registry.yarnpkg.com/webtorrent/-/webtorrent-0.107.16.tgz#cf2231f87b3f4334f8eb56ba5aae80df8cd8521c" 1987 resolved "https://registry.yarnpkg.com/webtorrent/-/webtorrent-0.108.1.tgz#c5d8ebc538eff85a86deec327b74508ebcf4a371"
1891 integrity sha512-5fdPZFiZPxwbigAHtMVQ7ZCXbZSQlxgB6JPD77itpc9DdKYPpliFwCLsNiQpj1jmpo91HlHUJk+Xp3ks1fLUQg== 1988 integrity sha512-+w6JaqGKZBZHVrYLmG2VDuRLZlUhQrkLXw0/nw3VKV4aloICWGwBKzjLclXmexUhnqeVzZjCRIQgSZ8+YmgJUQ==
1892 dependencies: 1989 dependencies:
1893 addr-to-ip-port "^1.4.2" 1990 addr-to-ip-port "^1.4.2"
1894 bitfield "^3.0.0" 1991 bitfield "^3.0.0"
@@ -1898,7 +1995,7 @@ webtorrent@>=0.107.6:
1898 chunk-store-stream "^4.0.0" 1995 chunk-store-stream "^4.0.0"
1899 create-torrent "^4.0.0" 1996 create-torrent "^4.0.0"
1900 debug "^4.1.0" 1997 debug "^4.1.0"
1901 end-of-stream "^1.1.0" 1998 end-of-stream "1.4.1"
1902 escape-html "^1.0.3" 1999 escape-html "^1.0.3"
1903 fs-chunk-store "^2.0.0" 2000 fs-chunk-store "^2.0.0"
1904 http-node "github:feross/http-node#webtorrent" 2001 http-node "github:feross/http-node#webtorrent"
@@ -1928,7 +2025,6 @@ webtorrent@>=0.107.6:
1928 stream-with-known-length-to-buffer "^1.0.0" 2025 stream-with-known-length-to-buffer "^1.0.0"
1929 torrent-discovery "^9.1.1" 2026 torrent-discovery "^9.1.1"
1930 torrent-piece "^2.0.0" 2027 torrent-piece "^2.0.0"
1931 uniq "^1.0.1"
1932 unordered-array-remove "^1.0.2" 2028 unordered-array-remove "^1.0.2"
1933 ut_metadata "^3.3.0" 2029 ut_metadata "^3.3.0"
1934 ut_pex "^2.0.0" 2030 ut_pex "^2.0.0"
@@ -1957,29 +2053,48 @@ wrappy@1:
1957 resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 2053 resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
1958 integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= 2054 integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
1959 2055
2056write-file-atomic@^3.0.0:
2057 version "3.0.3"
2058 resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8"
2059 integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==
2060 dependencies:
2061 imurmurhash "^0.1.4"
2062 is-typedarray "^1.0.0"
2063 signal-exit "^3.0.2"
2064 typedarray-to-buffer "^3.1.5"
2065
2066write-json-file@^4.2.0:
2067 version "4.3.0"
2068 resolved "https://registry.yarnpkg.com/write-json-file/-/write-json-file-4.3.0.tgz#908493d6fd23225344af324016e4ca8f702dd12d"
2069 integrity sha512-PxiShnxf0IlnQuMYOPPhPkhExoCQuTUNPOa/2JWCYTmBquU9njyyDuwRKN26IZBlp4yn1nt+Agh2HOOBl+55HQ==
2070 dependencies:
2071 detect-indent "^6.0.0"
2072 graceful-fs "^4.1.15"
2073 is-plain-obj "^2.0.0"
2074 make-dir "^3.0.0"
2075 sort-keys "^4.0.0"
2076 write-file-atomic "^3.0.0"
2077
1960wrtc@^0.4.1: 2078wrtc@^0.4.1:
1961 version "0.4.2" 2079 version "0.4.4"
1962 resolved "https://registry.yarnpkg.com/wrtc/-/wrtc-0.4.2.tgz#feeb829709dac17139b49f7a18f57f4e98300a6f" 2080 resolved "https://registry.yarnpkg.com/wrtc/-/wrtc-0.4.4.tgz#523bcec18ea91de5d8ee1ef11e5cbe4fcf7daf3d"
1963 integrity sha512-IaXogllhkd3dHKrZxzQKXmKjqsR35X9XIjp1ElimieZuaPgDCPEbIg/DOwzGTT4bdKqmB8FjPgau2RNMFTGvHQ== 2081 integrity sha512-ithsvEKqS6pIbzPiXgJXU4SjQHR7fSszDgGMOREW8j2S4N+ay05r4aYpUZJnsa1fr6o5efeQ/ikFiDXDl5YqeQ==
1964 dependencies: 2082 dependencies:
1965 node-pre-gyp "^0.13.0" 2083 node-pre-gyp "^0.13.0"
1966 optionalDependencies: 2084 optionalDependencies:
1967 domexception "^1.0.1" 2085 domexception "^1.0.1"
1968 2086
1969ws@^7.0.0: 2087ws@^7.0.0:
1970 version "7.2.0" 2088 version "7.2.3"
1971 resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.0.tgz#422eda8c02a4b5dba7744ba66eebbd84bcef0ec7" 2089 resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.3.tgz#a5411e1fb04d5ed0efee76d26d5c46d830c39b46"
1972 integrity sha512-+SqNqFbwTm/0DC18KYzIsMTnEWpLwJsiasW/O17la4iDRRIO9uaHbvKiAS3AHgTiuuWerK/brj4O6MYZkei9xg== 2090 integrity sha512-HTDl9G9hbkNDk98naoR/cHDws7+EyYMOdL1BmjsZXRUjf7d+MficC4B7HLUPlSiho0vg+CWKrGIt/VJBd1xunQ==
1973 dependencies:
1974 async-limiter "^1.0.0"
1975 2091
1976xml2js@^0.4.8: 2092xml2js@^0.4.8:
1977 version "0.4.22" 2093 version "0.4.23"
1978 resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.22.tgz#4fa2d846ec803237de86f30aa9b5f70b6600de02" 2094 resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66"
1979 integrity sha512-MWTbxAQqclRSTnehWWe5nMKzI3VmJ8ltiJEco8akcC6j3miOhjjfzKum5sId+CWhfxdOs/1xauYr8/ZDBtQiRw== 2095 integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==
1980 dependencies: 2096 dependencies:
1981 sax ">=0.6.0" 2097 sax ">=0.6.0"
1982 util.promisify "~1.0.0"
1983 xmlbuilder "~11.0.0" 2098 xmlbuilder "~11.0.0"
1984 2099
1985xmlbuilder@0.4.x: 2100xmlbuilder@0.4.x:
@@ -1993,9 +2108,9 @@ xmlbuilder@~11.0.0:
1993 integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== 2108 integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==
1994 2109
1995xmldom@0.1.x: 2110xmldom@0.1.x:
1996 version "0.1.27" 2111 version "0.1.31"
1997 resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9" 2112 resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.31.tgz#b76c9a1bd9f0a9737e5a72dc37231cf38375e2ff"
1998 integrity sha1-1QH5ezvbQDr4757MIFcxh6rawOk= 2113 integrity sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ==
1999 2114
2000yallist@^3.0.0, yallist@^3.0.3: 2115yallist@^3.0.0, yallist@^3.0.3:
2001 version "3.1.1" 2116 version "3.1.1"
diff --git a/server/typings/express.ts b/server/typings/express.ts
index 3cc7c7632..5973496f1 100644
--- a/server/typings/express.ts
+++ b/server/typings/express.ts
@@ -21,20 +21,38 @@ import {
21} from './models' 21} from './models'
22import { MVideoPlaylistFull, MVideoPlaylistFullSummary } from './models/video/video-playlist' 22import { MVideoPlaylistFull, MVideoPlaylistFullSummary } from './models/video/video-playlist'
23import { MVideoImportDefault } from '@server/typings/models/video/video-import' 23import { MVideoImportDefault } from '@server/typings/models/video/video-import'
24import { MAccountBlocklist, MStreamingPlaylist, MVideoFile } from '@server/typings/models' 24import { MAccountBlocklist, MActorUrl, MStreamingPlaylist, MVideoFile, MVideoImmutable } from '@server/typings/models'
25import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/typings/models/video/video-playlist-element' 25import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/typings/models/video/video-playlist-element'
26import { MAccountVideoRateAccountVideo } from '@server/typings/models/video/video-rate' 26import { MAccountVideoRateAccountVideo } from '@server/typings/models/video/video-rate'
27import { MVideoChangeOwnershipFull } from './models/video/video-change-ownership' 27import { MVideoChangeOwnershipFull } from './models/video/video-change-ownership'
28import { MPlugin, MServer } from '@server/typings/models/server' 28import { MPlugin, MServer } from '@server/typings/models/server'
29import { MServerBlocklist } from './models/server/server-blocklist' 29import { MServerBlocklist } from './models/server/server-blocklist'
30import { MOAuthTokenUser } from '@server/typings/models/oauth/oauth-token' 30import { MOAuthTokenUser } from '@server/typings/models/oauth/oauth-token'
31import { UserRole } from '@shared/models'
32import { RegisterServerAuthExternalOptions } from '@shared/models/plugins/register-server-auth.model'
31 33
32declare module 'express' { 34declare module 'express' {
33
34 interface Response { 35 interface Response {
35 36
36 locals: { 37 locals: {
38 bypassLogin?: {
39 bypass: boolean
40 pluginName: string
41 authName?: string
42 user: {
43 username: string
44 email: string
45 displayName: string
46 role: UserRole
47 }
48 }
49
50 refreshTokenAuthName?: string
51
52 explicitLogout: boolean
53
37 videoAll?: MVideoFullLight 54 videoAll?: MVideoFullLight
55 onlyImmutableVideo?: MVideoImmutable
38 onlyVideo?: MVideoThumbnail 56 onlyVideo?: MVideoThumbnail
39 onlyVideoWithRights?: MVideoWithRights 57 onlyVideoWithRights?: MVideoWithRights
40 videoId?: MVideoIdThumbnail 58 videoId?: MVideoIdThumbnail
@@ -74,6 +92,7 @@ declare module 'express' {
74 92
75 account?: MAccountDefault 93 account?: MAccountDefault
76 94
95 actorUrl?: MActorUrl
77 actorFull?: MActorFull 96 actorFull?: MActorFull
78 97
79 user?: MUserDefault 98 user?: MUserDefault
@@ -97,6 +116,8 @@ declare module 'express' {
97 116
98 registeredPlugin?: RegisteredPlugin 117 registeredPlugin?: RegisteredPlugin
99 118
119 externalAuth?: RegisterServerAuthExternalOptions
120
100 plugin?: MPlugin 121 plugin?: MPlugin
101 } 122 }
102 } 123 }
diff --git a/server/typings/models/account/account-blocklist.ts b/server/typings/models/account/account-blocklist.ts
index c9cb55332..0d8bf11bd 100644
--- a/server/typings/models/account/account-blocklist.ts
+++ b/server/typings/models/account/account-blocklist.ts
@@ -12,7 +12,8 @@ export type MAccountBlocklist = Omit<AccountBlocklistModel, 'ByAccount' | 'Block
12 12
13export type MAccountBlocklistId = Pick<AccountBlocklistModel, 'id'> 13export type MAccountBlocklistId = Pick<AccountBlocklistModel, 'id'>
14 14
15export type MAccountBlocklistAccounts = MAccountBlocklist & 15export type MAccountBlocklistAccounts =
16 MAccountBlocklist &
16 Use<'ByAccount', MAccountDefault> & 17 Use<'ByAccount', MAccountDefault> &
17 Use<'BlockedAccount', MAccountDefault> 18 Use<'BlockedAccount', MAccountDefault>
18 19
@@ -20,6 +21,7 @@ export type MAccountBlocklistAccounts = MAccountBlocklist &
20 21
21// Format for API or AP object 22// Format for API or AP object
22 23
23export type MAccountBlocklistFormattable = Pick<MAccountBlocklist, 'createdAt'> & 24export type MAccountBlocklistFormattable =
25 Pick<MAccountBlocklist, 'createdAt'> &
24 Use<'ByAccount', MAccountFormattable> & 26 Use<'ByAccount', MAccountFormattable> &
25 Use<'BlockedAccount', MAccountFormattable> 27 Use<'BlockedAccount', MAccountFormattable>
diff --git a/server/typings/models/account/account.ts b/server/typings/models/account/account.ts
index adb1f3689..7b826ee04 100644
--- a/server/typings/models/account/account.ts
+++ b/server/typings/models/account/account.ts
@@ -21,7 +21,8 @@ type Use<K extends keyof AccountModel, M> = PickWith<AccountModel, K, M>
21 21
22// ############################################################################ 22// ############################################################################
23 23
24export type MAccount = Omit<AccountModel, 'Actor' | 'User' | 'Application' | 'VideoChannels' | 'VideoPlaylists' | 24export type MAccount =
25 Omit<AccountModel, 'Actor' | 'User' | 'Application' | 'VideoChannels' | 'VideoPlaylists' |
25 'VideoComments' | 'BlockedAccounts'> 26 'VideoComments' | 'BlockedAccounts'>
26 27
27// ############################################################################ 28// ############################################################################
@@ -34,62 +35,75 @@ export type MAccountUserId = Pick<MAccount, 'userId'>
34export type MAccountUrl = Use<'Actor', MActorUrl> 35export type MAccountUrl = Use<'Actor', MActorUrl>
35export type MAccountAudience = Use<'Actor', MActorAudience> 36export type MAccountAudience = Use<'Actor', MActorAudience>
36 37
37export type MAccountIdActor = MAccountId & 38export type MAccountIdActor =
39 MAccountId &
38 Use<'Actor', MActor> 40 Use<'Actor', MActor>
39 41
40export type MAccountIdActorId = MAccountId & 42export type MAccountIdActorId =
43 MAccountId &
41 Use<'Actor', MActorId> 44 Use<'Actor', MActorId>
42 45
43// ############################################################################ 46// ############################################################################
44 47
45// Default scope 48// Default scope
46export type MAccountDefault = MAccount & 49export type MAccountDefault =
50 MAccount &
47 Use<'Actor', MActorDefault> 51 Use<'Actor', MActorDefault>
48 52
49// Default with default association scopes 53// Default with default association scopes
50export type MAccountDefaultChannelDefault = MAccount & 54export type MAccountDefaultChannelDefault =
55 MAccount &
51 Use<'Actor', MActorDefault> & 56 Use<'Actor', MActorDefault> &
52 Use<'VideoChannels', MChannelDefault[]> 57 Use<'VideoChannels', MChannelDefault[]>
53 58
54// We don't need some actors attributes 59// We don't need some actors attributes
55export type MAccountLight = MAccount & 60export type MAccountLight =
61 MAccount &
56 Use<'Actor', MActorDefaultLight> 62 Use<'Actor', MActorDefaultLight>
57 63
58// ############################################################################ 64// ############################################################################
59 65
60// Full actor 66// Full actor
61export type MAccountActor = MAccount & 67export type MAccountActor =
68 MAccount &
62 Use<'Actor', MActor> 69 Use<'Actor', MActor>
63 70
64// Full actor with server 71// Full actor with server
65export type MAccountServer = MAccount & 72export type MAccountServer =
73 MAccount &
66 Use<'Actor', MActorServer> 74 Use<'Actor', MActorServer>
67 75
68// ############################################################################ 76// ############################################################################
69 77
70// For API 78// For API
71 79
72export type MAccountSummary = FunctionProperties<MAccount> & 80export type MAccountSummary =
81 FunctionProperties<MAccount> &
73 Pick<MAccount, 'id' | 'name'> & 82 Pick<MAccount, 'id' | 'name'> &
74 Use<'Actor', MActorSummary> 83 Use<'Actor', MActorSummary>
75 84
76export type MAccountSummaryBlocks = MAccountSummary & 85export type MAccountSummaryBlocks =
86 MAccountSummary &
77 Use<'BlockedAccounts', MAccountBlocklistId[]> 87 Use<'BlockedAccounts', MAccountBlocklistId[]>
78 88
79export type MAccountAPI = MAccount & 89export type MAccountAPI =
90 MAccount &
80 Use<'Actor', MActorAPI> 91 Use<'Actor', MActorAPI>
81 92
82// ############################################################################ 93// ############################################################################
83 94
84// Format for API or AP object 95// Format for API or AP object
85 96
86export type MAccountSummaryFormattable = FunctionProperties<MAccount> & 97export type MAccountSummaryFormattable =
98 FunctionProperties<MAccount> &
87 Pick<MAccount, 'id' | 'name'> & 99 Pick<MAccount, 'id' | 'name'> &
88 Use<'Actor', MActorSummaryFormattable> 100 Use<'Actor', MActorSummaryFormattable>
89 101
90export type MAccountFormattable = FunctionProperties<MAccount> & 102export type MAccountFormattable =
103 FunctionProperties<MAccount> &
91 Pick<MAccount, 'id' | 'name' | 'description' | 'createdAt' | 'updatedAt' | 'userId'> & 104 Pick<MAccount, 'id' | 'name' | 'description' | 'createdAt' | 'updatedAt' | 'userId'> &
92 Use<'Actor', MActorFormattable> 105 Use<'Actor', MActorFormattable>
93 106
94export type MAccountAP = Pick<MAccount, 'name' | 'description'> & 107export type MAccountAP =
108 Pick<MAccount, 'name' | 'description'> &
95 Use<'Actor', MActorAP> 109 Use<'Actor', MActorAP>
diff --git a/server/typings/models/account/actor-follow.ts b/server/typings/models/account/actor-follow.ts
index f44157eba..5d0c03c8d 100644
--- a/server/typings/models/account/actor-follow.ts
+++ b/server/typings/models/account/actor-follow.ts
@@ -20,22 +20,26 @@ export type MActorFollow = Omit<ActorFollowModel, 'ActorFollower' | 'ActorFollow
20 20
21// ############################################################################ 21// ############################################################################
22 22
23export type MActorFollowFollowingHost = MActorFollow & 23export type MActorFollowFollowingHost =
24 MActorFollow &
24 Use<'ActorFollowing', MActorUsername & MActorHost> 25 Use<'ActorFollowing', MActorUsername & MActorHost>
25 26
26// ############################################################################ 27// ############################################################################
27 28
28// With actors or actors default 29// With actors or actors default
29 30
30export type MActorFollowActors = MActorFollow & 31export type MActorFollowActors =
32 MActorFollow &
31 Use<'ActorFollower', MActor> & 33 Use<'ActorFollower', MActor> &
32 Use<'ActorFollowing', MActor> 34 Use<'ActorFollowing', MActor>
33 35
34export type MActorFollowActorsDefault = MActorFollow & 36export type MActorFollowActorsDefault =
37 MActorFollow &
35 Use<'ActorFollower', MActorDefault> & 38 Use<'ActorFollower', MActorDefault> &
36 Use<'ActorFollowing', MActorDefault> 39 Use<'ActorFollowing', MActorDefault>
37 40
38export type MActorFollowFull = MActorFollow & 41export type MActorFollowFull =
42 MActorFollow &
39 Use<'ActorFollower', MActorDefaultAccountChannel> & 43 Use<'ActorFollower', MActorDefaultAccountChannel> &
40 Use<'ActorFollowing', MActorDefaultAccountChannel> 44 Use<'ActorFollowing', MActorDefaultAccountChannel>
41 45
@@ -43,20 +47,24 @@ export type MActorFollowFull = MActorFollow &
43 47
44// For subscriptions 48// For subscriptions
45 49
46type SubscriptionFollowing = MActorDefault & 50type SubscriptionFollowing =
51 MActorDefault &
47 PickWith<ActorModel, 'VideoChannel', MChannelDefault> 52 PickWith<ActorModel, 'VideoChannel', MChannelDefault>
48 53
49export type MActorFollowActorsDefaultSubscription = MActorFollow & 54export type MActorFollowActorsDefaultSubscription =
55 MActorFollow &
50 Use<'ActorFollower', MActorDefault> & 56 Use<'ActorFollower', MActorDefault> &
51 Use<'ActorFollowing', SubscriptionFollowing> 57 Use<'ActorFollowing', SubscriptionFollowing>
52 58
53export type MActorFollowSubscriptions = MActorFollow & 59export type MActorFollowSubscriptions =
60 MActorFollow &
54 Use<'ActorFollowing', MActorChannelAccountActor> 61 Use<'ActorFollowing', MActorChannelAccountActor>
55 62
56// ############################################################################ 63// ############################################################################
57 64
58// Format for API or AP object 65// Format for API or AP object
59 66
60export type MActorFollowFormattable = Pick<MActorFollow, 'id' | 'score' | 'state' | 'createdAt' | 'updatedAt'> & 67export type MActorFollowFormattable =
68 Pick<MActorFollow, 'id' | 'score' | 'state' | 'createdAt' | 'updatedAt'> &
61 Use<'ActorFollower', MActorFormattable> & 69 Use<'ActorFollower', MActorFormattable> &
62 Use<'ActorFollowing', MActorFormattable> 70 Use<'ActorFollowing', MActorFormattable>
diff --git a/server/typings/models/account/actor.ts b/server/typings/models/account/actor.ts
index ee4ece755..1160e84cb 100644
--- a/server/typings/models/account/actor.ts
+++ b/server/typings/models/account/actor.ts
@@ -31,18 +31,23 @@ export type MActorLight = Omit<MActor, 'privateKey' | 'privateKey'>
31export type MActorHost = Use<'Server', MServerHost> 31export type MActorHost = Use<'Server', MServerHost>
32export type MActorRedundancyAllowedOpt = PickWithOpt<ActorModel, 'Server', MServerRedundancyAllowed> 32export type MActorRedundancyAllowedOpt = PickWithOpt<ActorModel, 'Server', MServerRedundancyAllowed>
33 33
34export type MActorDefaultLight = MActorLight & 34export type MActorDefaultLight =
35 MActorLight &
35 Use<'Server', MServerHost> & 36 Use<'Server', MServerHost> &
36 Use<'Avatar', MAvatar> 37 Use<'Avatar', MAvatar>
37 38
38export type MActorAccountId = MActor & 39export type MActorAccountId =
40 MActor &
39 Use<'Account', MAccountId> 41 Use<'Account', MAccountId>
40export type MActorAccountIdActor = MActor & 42export type MActorAccountIdActor =
43 MActor &
41 Use<'Account', MAccountIdActor> 44 Use<'Account', MAccountIdActor>
42 45
43export type MActorChannelId = MActor & 46export type MActorChannelId =
47 MActor &
44 Use<'VideoChannel', MChannelId> 48 Use<'VideoChannel', MChannelId>
45export type MActorChannelIdActor = MActor & 49export type MActorChannelIdActor =
50 MActor &
46 Use<'VideoChannel', MChannelIdActor> 51 Use<'VideoChannel', MChannelIdActor>
47 52
48export type MActorAccountChannelId = MActorAccountId & MActorChannelId 53export type MActorAccountChannelId = MActorAccountId & MActorChannelId
@@ -52,38 +57,45 @@ export type MActorAccountChannelIdActor = MActorAccountIdActor & MActorChannelId
52 57
53// Include raw account/channel/server 58// Include raw account/channel/server
54 59
55export type MActorAccount = MActor & 60export type MActorAccount =
61 MActor &
56 Use<'Account', MAccount> 62 Use<'Account', MAccount>
57 63
58export type MActorChannel = MActor & 64export type MActorChannel =
65 MActor &
59 Use<'VideoChannel', MChannel> 66 Use<'VideoChannel', MChannel>
60 67
61export type MActorDefaultAccountChannel = MActorDefault & MActorAccount & MActorChannel 68export type MActorDefaultAccountChannel = MActorDefault & MActorAccount & MActorChannel
62 69
63export type MActorServer = MActor & 70export type MActorServer =
71 MActor &
64 Use<'Server', MServer> 72 Use<'Server', MServer>
65 73
66// ############################################################################ 74// ############################################################################
67 75
68// Complex actor associations 76// Complex actor associations
69 77
70export type MActorDefault = MActor & 78export type MActorDefault =
79 MActor &
71 Use<'Server', MServer> & 80 Use<'Server', MServer> &
72 Use<'Avatar', MAvatar> 81 Use<'Avatar', MAvatar>
73 82
74// Actor with channel that is associated to an account and its actor 83// Actor with channel that is associated to an account and its actor
75// Actor -> VideoChannel -> Account -> Actor 84// Actor -> VideoChannel -> Account -> Actor
76export type MActorChannelAccountActor = MActor & 85export type MActorChannelAccountActor =
86 MActor &
77 Use<'VideoChannel', MChannelAccountActor> 87 Use<'VideoChannel', MChannelAccountActor>
78 88
79export type MActorFull = MActor & 89export type MActorFull =
90 MActor &
80 Use<'Server', MServer> & 91 Use<'Server', MServer> &
81 Use<'Avatar', MAvatar> & 92 Use<'Avatar', MAvatar> &
82 Use<'Account', MAccount> & 93 Use<'Account', MAccount> &
83 Use<'VideoChannel', MChannelAccountActor> 94 Use<'VideoChannel', MChannelAccountActor>
84 95
85// Same than ActorFull, but the account and the channel have their actor 96// Same than ActorFull, but the account and the channel have their actor
86export type MActorFullActor = MActor & 97export type MActorFullActor =
98 MActor &
87 Use<'Server', MServer> & 99 Use<'Server', MServer> &
88 Use<'Avatar', MAvatar> & 100 Use<'Avatar', MAvatar> &
89 Use<'Account', MAccountDefault> & 101 Use<'Account', MAccountDefault> &
@@ -93,29 +105,35 @@ export type MActorFullActor = MActor &
93 105
94// API 106// API
95 107
96export type MActorSummary = FunctionProperties<MActor> & 108export type MActorSummary =
109 FunctionProperties<MActor> &
97 Pick<MActor, 'id' | 'preferredUsername' | 'url' | 'serverId' | 'avatarId'> & 110 Pick<MActor, 'id' | 'preferredUsername' | 'url' | 'serverId' | 'avatarId'> &
98 Use<'Server', MServerHost> & 111 Use<'Server', MServerHost> &
99 Use<'Avatar', MAvatar> 112 Use<'Avatar', MAvatar>
100 113
101export type MActorSummaryBlocks = MActorSummary & 114export type MActorSummaryBlocks =
115 MActorSummary &
102 Use<'Server', MServerHostBlocks> 116 Use<'Server', MServerHostBlocks>
103 117
104export type MActorAPI = Omit<MActorDefault, 'publicKey' | 'privateKey' | 'inboxUrl' | 'outboxUrl' | 'sharedInboxUrl' | 118export type MActorAPI =
119 Omit<MActorDefault, 'publicKey' | 'privateKey' | 'inboxUrl' | 'outboxUrl' | 'sharedInboxUrl' |
105 'followersUrl' | 'followingUrl' | 'url' | 'createdAt' | 'updatedAt'> 120 'followersUrl' | 'followingUrl' | 'url' | 'createdAt' | 'updatedAt'>
106 121
107// ############################################################################ 122// ############################################################################
108 123
109// Format for API or AP object 124// Format for API or AP object
110 125
111export type MActorSummaryFormattable = FunctionProperties<MActor> & 126export type MActorSummaryFormattable =
127 FunctionProperties<MActor> &
112 Pick<MActor, 'url' | 'preferredUsername'> & 128 Pick<MActor, 'url' | 'preferredUsername'> &
113 Use<'Server', MServerHost> & 129 Use<'Server', MServerHost> &
114 Use<'Avatar', MAvatarFormattable> 130 Use<'Avatar', MAvatarFormattable>
115 131
116export type MActorFormattable = MActorSummaryFormattable & 132export type MActorFormattable =
133 MActorSummaryFormattable &
117 Pick<MActor, 'id' | 'followingCount' | 'followersCount' | 'createdAt' | 'updatedAt'> & 134 Pick<MActor, 'id' | 'followingCount' | 'followersCount' | 'createdAt' | 'updatedAt'> &
118 Use<'Server', MServerHost & Partial<Pick<MServer, 'redundancyAllowed'>>> 135 Use<'Server', MServerHost & Partial<Pick<MServer, 'redundancyAllowed'>>>
119 136
120export type MActorAP = MActor & 137export type MActorAP =
138 MActor &
121 Use<'Avatar', MAvatar> 139 Use<'Avatar', MAvatar>
diff --git a/server/typings/models/account/avatar.ts b/server/typings/models/account/avatar.ts
index 8af6cc787..21b47180f 100644
--- a/server/typings/models/account/avatar.ts
+++ b/server/typings/models/account/avatar.ts
@@ -7,5 +7,6 @@ export type MAvatar = AvatarModel
7 7
8// Format for API or AP object 8// Format for API or AP object
9 9
10export type MAvatarFormattable = FunctionProperties<MAvatar> & 10export type MAvatarFormattable =
11 FunctionProperties<MAvatar> &
11 Pick<MAvatar, 'filename' | 'createdAt' | 'updatedAt'> 12 Pick<MAvatar, 'filename' | 'createdAt' | 'updatedAt'>
diff --git a/server/typings/models/oauth/oauth-token.ts b/server/typings/models/oauth/oauth-token.ts
index 8ef042d4e..b24a95fd8 100644
--- a/server/typings/models/oauth/oauth-token.ts
+++ b/server/typings/models/oauth/oauth-token.ts
@@ -8,6 +8,7 @@ type Use<K extends keyof OAuthTokenModel, M> = PickWith<OAuthTokenModel, K, M>
8 8
9export type MOAuthToken = Omit<OAuthTokenModel, 'User' | 'OAuthClients'> 9export type MOAuthToken = Omit<OAuthTokenModel, 'User' | 'OAuthClients'>
10 10
11export type MOAuthTokenUser = MOAuthToken & 11export type MOAuthTokenUser =
12 MOAuthToken &
12 Use<'User', MUserAccountUrl> & 13 Use<'User', MUserAccountUrl> &
13 { user?: MUserAccountUrl } 14 { user?: MUserAccountUrl }
diff --git a/server/typings/models/server/plugin.ts b/server/typings/models/server/plugin.ts
index 94674c318..83eb83794 100644
--- a/server/typings/models/server/plugin.ts
+++ b/server/typings/models/server/plugin.ts
@@ -6,5 +6,6 @@ export type MPlugin = PluginModel
6 6
7// Format for API or AP object 7// Format for API or AP object
8 8
9export type MPluginFormattable = Pick<MPlugin, 'name' | 'type' | 'version' | 'latestVersion' | 'enabled' | 'uninstalled' 9export type MPluginFormattable =
10 Pick<MPlugin, 'name' | 'type' | 'version' | 'latestVersion' | 'enabled' | 'uninstalled'
10 | 'peertubeEngine' | 'description' | 'homepage' | 'settings' | 'createdAt' | 'updatedAt'> 11 | 'peertubeEngine' | 'description' | 'homepage' | 'settings' | 'createdAt' | 'updatedAt'>
diff --git a/server/typings/models/server/server-blocklist.ts b/server/typings/models/server/server-blocklist.ts
index c3e6230f2..ff6f49176 100644
--- a/server/typings/models/server/server-blocklist.ts
+++ b/server/typings/models/server/server-blocklist.ts
@@ -11,7 +11,8 @@ export type MServerBlocklist = Omit<ServerBlocklistModel, 'ByAccount' | 'Blocked
11 11
12// ############################################################################ 12// ############################################################################
13 13
14export type MServerBlocklistAccountServer = MServerBlocklist & 14export type MServerBlocklistAccountServer =
15 MServerBlocklist &
15 Use<'ByAccount', MAccountDefault> & 16 Use<'ByAccount', MAccountDefault> &
16 Use<'BlockedServer', MServer> 17 Use<'BlockedServer', MServer>
17 18
@@ -19,6 +20,7 @@ export type MServerBlocklistAccountServer = MServerBlocklist &
19 20
20// Format for API or AP object 21// Format for API or AP object
21 22
22export type MServerBlocklistFormattable = Pick<MServerBlocklist, 'createdAt'> & 23export type MServerBlocklistFormattable =
24 Pick<MServerBlocklist, 'createdAt'> &
23 Use<'ByAccount', MAccountFormattable> & 25 Use<'ByAccount', MAccountFormattable> &
24 Use<'BlockedServer', MServerFormattable> 26 Use<'BlockedServer', MServerFormattable>
diff --git a/server/typings/models/server/server.ts b/server/typings/models/server/server.ts
index 190cc0c28..b35e55aeb 100644
--- a/server/typings/models/server/server.ts
+++ b/server/typings/models/server/server.ts
@@ -13,12 +13,14 @@ export type MServer = Omit<ServerModel, 'Actors' | 'BlockedByAccounts'>
13export type MServerHost = Pick<MServer, 'host'> 13export type MServerHost = Pick<MServer, 'host'>
14export type MServerRedundancyAllowed = Pick<MServer, 'redundancyAllowed'> 14export type MServerRedundancyAllowed = Pick<MServer, 'redundancyAllowed'>
15 15
16export type MServerHostBlocks = MServerHost & 16export type MServerHostBlocks =
17 MServerHost &
17 Use<'BlockedByAccounts', MAccountBlocklistId[]> 18 Use<'BlockedByAccounts', MAccountBlocklistId[]>
18 19
19// ############################################################################ 20// ############################################################################
20 21
21// Format for API or AP object 22// Format for API or AP object
22 23
23export type MServerFormattable = FunctionProperties<MServer> & 24export type MServerFormattable =
25 FunctionProperties<MServer> &
24 Pick<MServer, 'host'> 26 Pick<MServer, 'host'>
diff --git a/server/typings/models/user/user-notification.ts b/server/typings/models/user/user-notification.ts
index 1cdc691b0..2080360e1 100644
--- a/server/typings/models/user/user-notification.ts
+++ b/server/typings/models/user/user-notification.ts
@@ -16,59 +16,73 @@ type Use<K extends keyof UserNotificationModel, M> = PickWith<UserNotificationMo
16 16
17// ############################################################################ 17// ############################################################################
18 18
19export namespace UserNotificationIncludes { 19export module UserNotificationIncludes {
20
20 export type VideoInclude = Pick<VideoModel, 'id' | 'uuid' | 'name'> 21 export type VideoInclude = Pick<VideoModel, 'id' | 'uuid' | 'name'>
21 export type VideoIncludeChannel = VideoInclude & 22 export type VideoIncludeChannel =
23 VideoInclude &
22 PickWith<VideoModel, 'VideoChannel', VideoChannelIncludeActor> 24 PickWith<VideoModel, 'VideoChannel', VideoChannelIncludeActor>
23 25
24 export type ActorInclude = Pick<ActorModel, 'preferredUsername' | 'getHost'> & 26 export type ActorInclude =
27 Pick<ActorModel, 'preferredUsername' | 'getHost'> &
25 PickWith<ActorModel, 'Avatar', Pick<AvatarModel, 'filename' | 'getStaticPath'>> & 28 PickWith<ActorModel, 'Avatar', Pick<AvatarModel, 'filename' | 'getStaticPath'>> &
26 PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>> 29 PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>>
27 30
28 export type VideoChannelInclude = Pick<VideoChannelModel, 'id' | 'name' | 'getDisplayName'> 31 export type VideoChannelInclude = Pick<VideoChannelModel, 'id' | 'name' | 'getDisplayName'>
29 export type VideoChannelIncludeActor = VideoChannelInclude & 32 export type VideoChannelIncludeActor =
33 VideoChannelInclude &
30 PickWith<VideoChannelModel, 'Actor', ActorInclude> 34 PickWith<VideoChannelModel, 'Actor', ActorInclude>
31 35
32 export type AccountInclude = Pick<AccountModel, 'id' | 'name' | 'getDisplayName'> 36 export type AccountInclude = Pick<AccountModel, 'id' | 'name' | 'getDisplayName'>
33 export type AccountIncludeActor = AccountInclude & 37 export type AccountIncludeActor =
38 AccountInclude &
34 PickWith<AccountModel, 'Actor', ActorInclude> 39 PickWith<AccountModel, 'Actor', ActorInclude>
35 40
36 export type VideoCommentInclude = Pick<VideoCommentModel, 'id' | 'originCommentId' | 'getThreadId'> & 41 export type VideoCommentInclude =
42 Pick<VideoCommentModel, 'id' | 'originCommentId' | 'getThreadId'> &
37 PickWith<VideoCommentModel, 'Account', AccountIncludeActor> & 43 PickWith<VideoCommentModel, 'Account', AccountIncludeActor> &
38 PickWith<VideoCommentModel, 'Video', VideoInclude> 44 PickWith<VideoCommentModel, 'Video', VideoInclude>
39 45
40 export type VideoAbuseInclude = Pick<VideoAbuseModel, 'id'> & 46 export type VideoAbuseInclude =
47 Pick<VideoAbuseModel, 'id'> &
41 PickWith<VideoAbuseModel, 'Video', VideoInclude> 48 PickWith<VideoAbuseModel, 'Video', VideoInclude>
42 49
43 export type VideoBlacklistInclude = Pick<VideoBlacklistModel, 'id'> & 50 export type VideoBlacklistInclude =
51 Pick<VideoBlacklistModel, 'id'> &
44 PickWith<VideoAbuseModel, 'Video', VideoInclude> 52 PickWith<VideoAbuseModel, 'Video', VideoInclude>
45 53
46 export type VideoImportInclude = Pick<VideoImportModel, 'id' | 'magnetUri' | 'targetUrl' | 'torrentName'> & 54 export type VideoImportInclude =
55 Pick<VideoImportModel, 'id' | 'magnetUri' | 'targetUrl' | 'torrentName'> &
47 PickWith<VideoImportModel, 'Video', VideoInclude> 56 PickWith<VideoImportModel, 'Video', VideoInclude>
48 57
49 export type ActorFollower = Pick<ActorModel, 'preferredUsername' | 'getHost'> & 58 export type ActorFollower =
59 Pick<ActorModel, 'preferredUsername' | 'getHost'> &
50 PickWith<ActorModel, 'Account', AccountInclude> & 60 PickWith<ActorModel, 'Account', AccountInclude> &
51 PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>> & 61 PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>> &
52 PickWithOpt<ActorModel, 'Avatar', Pick<AvatarModel, 'filename' | 'getStaticPath'>> 62 PickWithOpt<ActorModel, 'Avatar', Pick<AvatarModel, 'filename' | 'getStaticPath'>>
53 63
54 export type ActorFollowing = Pick<ActorModel, 'preferredUsername' | 'type' | 'getHost'> & 64 export type ActorFollowing =
65 Pick<ActorModel, 'preferredUsername' | 'type' | 'getHost'> &
55 PickWith<ActorModel, 'VideoChannel', VideoChannelInclude> & 66 PickWith<ActorModel, 'VideoChannel', VideoChannelInclude> &
56 PickWith<ActorModel, 'Account', AccountInclude> & 67 PickWith<ActorModel, 'Account', AccountInclude> &
57 PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>> 68 PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>>
58 69
59 export type ActorFollowInclude = Pick<ActorFollowModel, 'id' | 'state'> & 70 export type ActorFollowInclude =
71 Pick<ActorFollowModel, 'id' | 'state'> &
60 PickWith<ActorFollowModel, 'ActorFollower', ActorFollower> & 72 PickWith<ActorFollowModel, 'ActorFollower', ActorFollower> &
61 PickWith<ActorFollowModel, 'ActorFollowing', ActorFollowing> 73 PickWith<ActorFollowModel, 'ActorFollowing', ActorFollowing>
62} 74}
63 75
64// ############################################################################ 76// ############################################################################
65 77
66export type MUserNotification = Omit<UserNotificationModel, 'User' | 'Video' | 'Comment' | 'VideoAbuse' | 'VideoBlacklist' | 78export type MUserNotification =
79 Omit<UserNotificationModel, 'User' | 'Video' | 'Comment' | 'VideoAbuse' | 'VideoBlacklist' |
67 'VideoImport' | 'Account' | 'ActorFollow'> 80 'VideoImport' | 'Account' | 'ActorFollow'>
68 81
69// ############################################################################ 82// ############################################################################
70 83
71export type UserNotificationModelForApi = MUserNotification & 84export type UserNotificationModelForApi =
85 MUserNotification &
72 Use<'Video', UserNotificationIncludes.VideoIncludeChannel> & 86 Use<'Video', UserNotificationIncludes.VideoIncludeChannel> &
73 Use<'Comment', UserNotificationIncludes.VideoCommentInclude> & 87 Use<'Comment', UserNotificationIncludes.VideoCommentInclude> &
74 Use<'VideoAbuse', UserNotificationIncludes.VideoAbuseInclude> & 88 Use<'VideoAbuse', UserNotificationIncludes.VideoAbuseInclude> &
diff --git a/server/typings/models/user/user.ts b/server/typings/models/user/user.ts
index 6ac19c20b..31cf075ef 100644
--- a/server/typings/models/user/user.ts
+++ b/server/typings/models/user/user.ts
@@ -29,36 +29,44 @@ export type MUserId = Pick<UserModel, 'id'>
29 29
30// With account 30// With account
31 31
32export type MUserAccountId = MUser & 32export type MUserAccountId =
33 MUser &
33 Use<'Account', MAccountId> 34 Use<'Account', MAccountId>
34 35
35export type MUserAccountUrl = MUser & 36export type MUserAccountUrl =
37 MUser &
36 Use<'Account', MAccountUrl & MAccountIdActorId> 38 Use<'Account', MAccountUrl & MAccountIdActorId>
37 39
38export type MUserAccount = MUser & 40export type MUserAccount =
41 MUser &
39 Use<'Account', MAccount> 42 Use<'Account', MAccount>
40 43
41export type MUserAccountDefault = MUser & 44export type MUserAccountDefault =
45 MUser &
42 Use<'Account', MAccountDefault> 46 Use<'Account', MAccountDefault>
43 47
44// With channel 48// With channel
45 49
46export type MUserNotifSettingChannelDefault = MUser & 50export type MUserNotifSettingChannelDefault =
51 MUser &
47 Use<'NotificationSetting', MNotificationSetting> & 52 Use<'NotificationSetting', MNotificationSetting> &
48 Use<'Account', MAccountDefaultChannelDefault> 53 Use<'Account', MAccountDefaultChannelDefault>
49 54
50// With notification settings 55// With notification settings
51 56
52export type MUserWithNotificationSetting = MUser & 57export type MUserWithNotificationSetting =
58 MUser &
53 Use<'NotificationSetting', MNotificationSetting> 59 Use<'NotificationSetting', MNotificationSetting>
54 60
55export type MUserNotifSettingAccount = MUser & 61export type MUserNotifSettingAccount =
62 MUser &
56 Use<'NotificationSetting', MNotificationSetting> & 63 Use<'NotificationSetting', MNotificationSetting> &
57 Use<'Account', MAccount> 64 Use<'Account', MAccount>
58 65
59// Default scope 66// Default scope
60 67
61export type MUserDefault = MUser & 68export type MUserDefault =
69 MUser &
62 Use<'NotificationSetting', MNotificationSetting> & 70 Use<'NotificationSetting', MNotificationSetting> &
63 Use<'Account', MAccountDefault> 71 Use<'Account', MAccountDefault>
64 72
@@ -67,12 +75,15 @@ export type MUserDefault = MUser &
67// Format for API or AP object 75// Format for API or AP object
68 76
69type MAccountWithChannels = MAccountFormattable & PickWithOpt<AccountModel, 'VideoChannels', MChannelFormattable[]> 77type MAccountWithChannels = MAccountFormattable & PickWithOpt<AccountModel, 'VideoChannels', MChannelFormattable[]>
70type MAccountWithChannelsAndSpecialPlaylists = MAccountWithChannels & 78type MAccountWithChannelsAndSpecialPlaylists =
79 MAccountWithChannels &
71 PickWithOpt<AccountModel, 'VideoPlaylists', MVideoPlaylist[]> 80 PickWithOpt<AccountModel, 'VideoPlaylists', MVideoPlaylist[]>
72 81
73export type MUserFormattable = MUserQuotaUsed & 82export type MUserFormattable =
83 MUserQuotaUsed &
74 Use<'Account', MAccountWithChannels> & 84 Use<'Account', MAccountWithChannels> &
75 PickWithOpt<UserModel, 'NotificationSetting', MNotificationSettingFormattable> 85 PickWithOpt<UserModel, 'NotificationSetting', MNotificationSettingFormattable>
76 86
77export type MMyUserFormattable = MUserFormattable & 87export type MMyUserFormattable =
88 MUserFormattable &
78 Use<'Account', MAccountWithChannelsAndSpecialPlaylists> 89 Use<'Account', MAccountWithChannelsAndSpecialPlaylists>
diff --git a/server/typings/models/video/schedule-video-update.ts b/server/typings/models/video/schedule-video-update.ts
index e6f478cdf..95a53d139 100644
--- a/server/typings/models/video/schedule-video-update.ts
+++ b/server/typings/models/video/schedule-video-update.ts
@@ -10,7 +10,8 @@ export type MScheduleVideoUpdate = Omit<ScheduleVideoUpdateModel, 'Video'>
10 10
11// ############################################################################ 11// ############################################################################
12 12
13export type MScheduleVideoUpdateVideoAll = MScheduleVideoUpdate & 13export type MScheduleVideoUpdateVideoAll =
14 MScheduleVideoUpdate &
14 Use<'Video', MVideoAPWithoutCaption & MVideoWithBlacklistLight> 15 Use<'Video', MVideoAPWithoutCaption & MVideoWithBlacklistLight>
15 16
16// Format for API or AP object 17// Format for API or AP object
diff --git a/server/typings/models/video/video-abuse.ts b/server/typings/models/video/video-abuse.ts
index e38c3f586..d60f05e4c 100644
--- a/server/typings/models/video/video-abuse.ts
+++ b/server/typings/models/video/video-abuse.ts
@@ -1,6 +1,6 @@
1import { VideoAbuseModel } from '../../../models/video/video-abuse' 1import { VideoAbuseModel } from '../../../models/video/video-abuse'
2import { PickWith } from '../../utils' 2import { PickWith } from '../../utils'
3import { MVideo } from './video' 3import { MVideoAccountLightBlacklistAllFiles, MVideo } from './video'
4import { MAccountDefault, MAccountFormattable } from '../account' 4import { MAccountDefault, MAccountFormattable } from '../account'
5 5
6type Use<K extends keyof VideoAbuseModel, M> = PickWith<VideoAbuseModel, K, M> 6type Use<K extends keyof VideoAbuseModel, M> = PickWith<VideoAbuseModel, K, M>
@@ -13,19 +13,23 @@ export type MVideoAbuse = Omit<VideoAbuseModel, 'Account' | 'Video' | 'toActivit
13 13
14export type MVideoAbuseId = Pick<VideoAbuseModel, 'id'> 14export type MVideoAbuseId = Pick<VideoAbuseModel, 'id'>
15 15
16export type MVideoAbuseVideo = MVideoAbuse & 16export type MVideoAbuseVideo =
17 MVideoAbuse &
17 Pick<VideoAbuseModel, 'toActivityPubObject'> & 18 Pick<VideoAbuseModel, 'toActivityPubObject'> &
18 Use<'Video', MVideo> 19 Use<'Video', MVideo>
19 20
20export type MVideoAbuseAccountVideo = MVideoAbuse & 21export type MVideoAbuseAccountVideo =
22 MVideoAbuse &
21 Pick<VideoAbuseModel, 'toActivityPubObject'> & 23 Pick<VideoAbuseModel, 'toActivityPubObject'> &
22 Use<'Video', MVideo> & 24 Use<'Video', MVideoAccountLightBlacklistAllFiles> &
23 Use<'Account', MAccountDefault> 25 Use<'Account', MAccountDefault>
24 26
25// ############################################################################ 27// ############################################################################
26 28
27// Format for API or AP object 29// Format for API or AP object
28 30
29export type MVideoAbuseFormattable = MVideoAbuse & 31export type MVideoAbuseFormattable =
32 MVideoAbuse &
30 Use<'Account', MAccountFormattable> & 33 Use<'Account', MAccountFormattable> &
31 Use<'Video', Pick<MVideo, 'id' | 'uuid' | 'name'>> 34 Use<'Video', Pick<MVideoAccountLightBlacklistAllFiles,
35 'id' | 'uuid' | 'name' | 'nsfw' | 'getMiniatureStaticPath' | 'isBlacklisted' | 'VideoChannel'>>
diff --git a/server/typings/models/video/video-blacklist.ts b/server/typings/models/video/video-blacklist.ts
index 7122a9dc0..ddb4db832 100644
--- a/server/typings/models/video/video-blacklist.ts
+++ b/server/typings/models/video/video-blacklist.ts
@@ -13,15 +13,18 @@ export type MVideoBlacklistUnfederated = Pick<MVideoBlacklist, 'unfederated'>
13 13
14// ############################################################################ 14// ############################################################################
15 15
16export type MVideoBlacklistLightVideo = MVideoBlacklistLight & 16export type MVideoBlacklistLightVideo =
17 MVideoBlacklistLight &
17 Use<'Video', MVideo> 18 Use<'Video', MVideo>
18 19
19export type MVideoBlacklistVideo = MVideoBlacklist & 20export type MVideoBlacklistVideo =
21 MVideoBlacklist &
20 Use<'Video', MVideo> 22 Use<'Video', MVideo>
21 23
22// ############################################################################ 24// ############################################################################
23 25
24// Format for API or AP object 26// Format for API or AP object
25 27
26export type MVideoBlacklistFormattable = MVideoBlacklist & 28export type MVideoBlacklistFormattable =
29 MVideoBlacklist &
27 Use<'Video', MVideoFormattable> 30 Use<'Video', MVideoFormattable>
diff --git a/server/typings/models/video/video-caption.ts b/server/typings/models/video/video-caption.ts
index ffa56f544..e7aff6956 100644
--- a/server/typings/models/video/video-caption.ts
+++ b/server/typings/models/video/video-caption.ts
@@ -11,14 +11,17 @@ export type MVideoCaption = Omit<VideoCaptionModel, 'Video'>
11// ############################################################################ 11// ############################################################################
12 12
13export type MVideoCaptionLanguage = Pick<MVideoCaption, 'language'> 13export type MVideoCaptionLanguage = Pick<MVideoCaption, 'language'>
14export type MVideoCaptionLanguageUrl = Pick<MVideoCaption, 'language' | 'fileUrl' | 'getFileUrl'>
14 15
15export type MVideoCaptionVideo = MVideoCaption & 16export type MVideoCaptionVideo =
17 MVideoCaption &
16 Use<'Video', Pick<MVideo, 'id' | 'remote' | 'uuid'>> 18 Use<'Video', Pick<MVideo, 'id' | 'remote' | 'uuid'>>
17 19
18// ############################################################################ 20// ############################################################################
19 21
20// Format for API or AP object 22// Format for API or AP object
21 23
22export type MVideoCaptionFormattable = FunctionProperties<MVideoCaption> & 24export type MVideoCaptionFormattable =
25 FunctionProperties<MVideoCaption> &
23 Pick<MVideoCaption, 'language'> & 26 Pick<MVideoCaption, 'language'> &
24 Use<'Video', MVideoUUID> 27 Use<'Video', MVideoUUID>
diff --git a/server/typings/models/video/video-change-ownership.ts b/server/typings/models/video/video-change-ownership.ts
index e5b5bbc1d..971dc3db5 100644
--- a/server/typings/models/video/video-change-ownership.ts
+++ b/server/typings/models/video/video-change-ownership.ts
@@ -9,7 +9,8 @@ type Use<K extends keyof VideoChangeOwnershipModel, M> = PickWith<VideoChangeOwn
9 9
10export type MVideoChangeOwnership = Omit<VideoChangeOwnershipModel, 'Initiator' | 'NextOwner' | 'Video'> 10export type MVideoChangeOwnership = Omit<VideoChangeOwnershipModel, 'Initiator' | 'NextOwner' | 'Video'>
11 11
12export type MVideoChangeOwnershipFull = MVideoChangeOwnership & 12export type MVideoChangeOwnershipFull =
13 MVideoChangeOwnership &
13 Use<'Initiator', MAccountDefault> & 14 Use<'Initiator', MAccountDefault> &
14 Use<'NextOwner', MAccountDefault> & 15 Use<'NextOwner', MAccountDefault> &
15 Use<'Video', MVideoWithAllFiles> 16 Use<'Video', MVideoWithAllFiles>
@@ -18,7 +19,8 @@ export type MVideoChangeOwnershipFull = MVideoChangeOwnership &
18 19
19// Format for API or AP object 20// Format for API or AP object
20 21
21export type MVideoChangeOwnershipFormattable = Pick<MVideoChangeOwnership, 'id' | 'status' | 'createdAt'> & 22export type MVideoChangeOwnershipFormattable =
23 Pick<MVideoChangeOwnership, 'id' | 'status' | 'createdAt'> &
22 Use<'Initiator', MAccountFormattable> & 24 Use<'Initiator', MAccountFormattable> &
23 Use<'NextOwner', MAccountFormattable> & 25 Use<'NextOwner', MAccountFormattable> &
24 Use<'Video', Pick<MVideo, 'id' | 'uuid' | 'url' | 'name'>> 26 Use<'Video', Pick<MVideo, 'id' | 'uuid' | 'url' | 'name'>>
diff --git a/server/typings/models/video/video-channels.ts b/server/typings/models/video/video-channels.ts
index 292d0ac95..50f7c2d8a 100644
--- a/server/typings/models/video/video-channels.ts
+++ b/server/typings/models/video/video-channels.ts
@@ -35,32 +35,39 @@ export type MChannelId = Pick<MChannel, 'id'>
35 35
36// ############################################################################ 36// ############################################################################
37 37
38export type MChannelIdActor = MChannelId & 38export type MChannelIdActor =
39 MChannelId &
39 Use<'Actor', MActorAccountChannelId> 40 Use<'Actor', MActorAccountChannelId>
40 41
41export type MChannelUserId = Pick<MChannel, 'accountId'> & 42export type MChannelUserId =
43 Pick<MChannel, 'accountId'> &
42 Use<'Account', MAccountUserId> 44 Use<'Account', MAccountUserId>
43 45
44export type MChannelActor = MChannel & 46export type MChannelActor =
47 MChannel &
45 Use<'Actor', MActor> 48 Use<'Actor', MActor>
46 49
47export type MChannelUrl = Use<'Actor', MActorUrl> 50export type MChannelUrl = Use<'Actor', MActorUrl>
48 51
49// Default scope 52// Default scope
50export type MChannelDefault = MChannel & 53export type MChannelDefault =
54 MChannel &
51 Use<'Actor', MActorDefault> 55 Use<'Actor', MActorDefault>
52 56
53// ############################################################################ 57// ############################################################################
54 58
55// Not all association attributes 59// Not all association attributes
56 60
57export type MChannelLight = MChannel & 61export type MChannelLight =
62 MChannel &
58 Use<'Actor', MActorDefaultLight> 63 Use<'Actor', MActorDefaultLight>
59 64
60export type MChannelActorLight = MChannel & 65export type MChannelActorLight =
66 MChannel &
61 Use<'Actor', MActorLight> 67 Use<'Actor', MActorLight>
62 68
63export type MChannelAccountLight = MChannel & 69export type MChannelAccountLight =
70 MChannel &
64 Use<'Actor', MActorDefaultLight> & 71 Use<'Actor', MActorDefaultLight> &
65 Use<'Account', MAccountLight> 72 Use<'Account', MAccountLight>
66 73
@@ -68,24 +75,29 @@ export type MChannelAccountLight = MChannel &
68 75
69// Account associations 76// Account associations
70 77
71export type MChannelAccountActor = MChannel & 78export type MChannelAccountActor =
79 MChannel &
72 Use<'Account', MAccountActor> 80 Use<'Account', MAccountActor>
73 81
74export type MChannelAccountDefault = MChannel & 82export type MChannelAccountDefault =
83 MChannel &
75 Use<'Actor', MActorDefault> & 84 Use<'Actor', MActorDefault> &
76 Use<'Account', MAccountDefault> 85 Use<'Account', MAccountDefault>
77 86
78export type MChannelActorAccountActor = MChannel & 87export type MChannelActorAccountActor =
88 MChannel &
79 Use<'Account', MAccountActor> & 89 Use<'Account', MAccountActor> &
80 Use<'Actor', MActor> 90 Use<'Actor', MActor>
81 91
82// ############################################################################ 92// ############################################################################
83 93
84// Videos associations 94// Videos associations
85export type MChannelVideos = MChannel & 95export type MChannelVideos =
96 MChannel &
86 Use<'Videos', MVideo[]> 97 Use<'Videos', MVideo[]>
87 98
88export type MChannelActorAccountDefaultVideos = MChannel & 99export type MChannelActorAccountDefaultVideos =
100 MChannel &
89 Use<'Actor', MActorDefault> & 101 Use<'Actor', MActorDefault> &
90 Use<'Account', MAccountDefault> & 102 Use<'Account', MAccountDefault> &
91 Use<'Videos', MVideo[]> 103 Use<'Videos', MVideo[]>
@@ -94,14 +106,17 @@ export type MChannelActorAccountDefaultVideos = MChannel &
94 106
95// For API 107// For API
96 108
97export type MChannelSummary = FunctionProperties<MChannel> & 109export type MChannelSummary =
110 FunctionProperties<MChannel> &
98 Pick<MChannel, 'id' | 'name' | 'description' | 'actorId'> & 111 Pick<MChannel, 'id' | 'name' | 'description' | 'actorId'> &
99 Use<'Actor', MActorSummary> 112 Use<'Actor', MActorSummary>
100 113
101export type MChannelSummaryAccount = MChannelSummary & 114export type MChannelSummaryAccount =
115 MChannelSummary &
102 Use<'Account', MAccountSummaryBlocks> 116 Use<'Account', MAccountSummaryBlocks>
103 117
104export type MChannelAPI = MChannel & 118export type MChannelAPI =
119 MChannel &
105 Use<'Actor', MActorAPI> & 120 Use<'Actor', MActorAPI> &
106 Use<'Account', MAccountAPI> 121 Use<'Account', MAccountAPI>
107 122
@@ -109,18 +124,22 @@ export type MChannelAPI = MChannel &
109 124
110// Format for API or AP object 125// Format for API or AP object
111 126
112export type MChannelSummaryFormattable = FunctionProperties<MChannel> & 127export type MChannelSummaryFormattable =
128 FunctionProperties<MChannel> &
113 Pick<MChannel, 'id' | 'name'> & 129 Pick<MChannel, 'id' | 'name'> &
114 Use<'Actor', MActorSummaryFormattable> 130 Use<'Actor', MActorSummaryFormattable>
115 131
116export type MChannelAccountSummaryFormattable = MChannelSummaryFormattable & 132export type MChannelAccountSummaryFormattable =
133 MChannelSummaryFormattable &
117 Use<'Account', MAccountSummaryFormattable> 134 Use<'Account', MAccountSummaryFormattable>
118 135
119export type MChannelFormattable = FunctionProperties<MChannel> & 136export type MChannelFormattable =
137 FunctionProperties<MChannel> &
120 Pick<MChannel, 'id' | 'name' | 'description' | 'createdAt' | 'updatedAt' | 'support'> & 138 Pick<MChannel, 'id' | 'name' | 'description' | 'createdAt' | 'updatedAt' | 'support'> &
121 Use<'Actor', MActorFormattable> & 139 Use<'Actor', MActorFormattable> &
122 PickWithOpt<VideoChannelModel, 'Account', MAccountFormattable> 140 PickWithOpt<VideoChannelModel, 'Account', MAccountFormattable>
123 141
124export type MChannelAP = Pick<MChannel, 'name' | 'description' | 'support'> & 142export type MChannelAP =
143 Pick<MChannel, 'name' | 'description' | 'support'> &
125 Use<'Actor', MActorAP> & 144 Use<'Actor', MActorAP> &
126 Use<'Account', MAccountUrl> 145 Use<'Account', MAccountUrl>
diff --git a/server/typings/models/video/video-comment.ts b/server/typings/models/video/video-comment.ts
index d693f9186..d6e0b66f5 100644
--- a/server/typings/models/video/video-comment.ts
+++ b/server/typings/models/video/video-comment.ts
@@ -14,30 +14,37 @@ export type MCommentUrl = Pick<MComment, 'url'>
14 14
15// ############################################################################ 15// ############################################################################
16 16
17export type MCommentOwner = MComment & 17export type MCommentOwner =
18 MComment &
18 Use<'Account', MAccountDefault> 19 Use<'Account', MAccountDefault>
19 20
20export type MCommentVideo = MComment & 21export type MCommentVideo =
22 MComment &
21 Use<'Video', MVideoAccountLight> 23 Use<'Video', MVideoAccountLight>
22 24
23export type MCommentReply = MComment & 25export type MCommentReply =
26 MComment &
24 Use<'InReplyToVideoComment', MComment> 27 Use<'InReplyToVideoComment', MComment>
25 28
26export type MCommentOwnerVideo = MComment & 29export type MCommentOwnerVideo =
30 MComment &
27 Use<'Account', MAccountDefault> & 31 Use<'Account', MAccountDefault> &
28 Use<'Video', MVideoAccountLight> 32 Use<'Video', MVideoAccountLight>
29 33
30export type MCommentOwnerVideoReply = MComment & 34export type MCommentOwnerVideoReply =
35 MComment &
31 Use<'Account', MAccountDefault> & 36 Use<'Account', MAccountDefault> &
32 Use<'Video', MVideoAccountLight> & 37 Use<'Video', MVideoAccountLight> &
33 Use<'InReplyToVideoComment', MComment> 38 Use<'InReplyToVideoComment', MComment>
34 39
35export type MCommentOwnerReplyVideoLight = MComment & 40export type MCommentOwnerReplyVideoLight =
41 MComment &
36 Use<'Account', MAccountDefault> & 42 Use<'Account', MAccountDefault> &
37 Use<'InReplyToVideoComment', MComment> & 43 Use<'InReplyToVideoComment', MComment> &
38 Use<'Video', MVideoIdUrl> 44 Use<'Video', MVideoIdUrl>
39 45
40export type MCommentOwnerVideoFeed = MCommentOwner & 46export type MCommentOwnerVideoFeed =
47 MCommentOwner &
41 Use<'Video', MVideoFeed> 48 Use<'Video', MVideoFeed>
42 49
43// ############################################################################ 50// ############################################################################
@@ -48,10 +55,12 @@ export type MCommentAPI = MComment & { totalReplies: number }
48 55
49// Format for API or AP object 56// Format for API or AP object
50 57
51export type MCommentFormattable = MCommentTotalReplies & 58export type MCommentFormattable =
59 MCommentTotalReplies &
52 Use<'Account', MAccountFormattable> 60 Use<'Account', MAccountFormattable>
53 61
54export type MCommentAP = MComment & 62export type MCommentAP =
63 MComment &
55 Use<'Account', MAccountUrl> & 64 Use<'Account', MAccountUrl> &
56 PickWithOpt<VideoCommentModel, 'Video', MVideoUrl> & 65 PickWithOpt<VideoCommentModel, 'Video', MVideoUrl> &
57 PickWithOpt<VideoCommentModel, 'InReplyToVideoComment', MCommentUrl> 66 PickWithOpt<VideoCommentModel, 'InReplyToVideoComment', MCommentUrl>
diff --git a/server/typings/models/video/video-file.ts b/server/typings/models/video/video-file.ts
index 352fe3d32..3fcaca78f 100644
--- a/server/typings/models/video/video-file.ts
+++ b/server/typings/models/video/video-file.ts
@@ -1,7 +1,7 @@
1import { VideoFileModel } from '../../../models/video/video-file' 1import { VideoFileModel } from '../../../models/video/video-file'
2import { PickWith, PickWithOpt } from '../../utils' 2import { PickWith, PickWithOpt } from '../../utils'
3import { MVideo, MVideoUUID } from './video' 3import { MVideo, MVideoUUID } from './video'
4import { MVideoRedundancyFileUrl } from './video-redundancy' 4import { MVideoRedundancy, MVideoRedundancyFileUrl } from './video-redundancy'
5import { MStreamingPlaylistVideo, MStreamingPlaylist } from './video-streaming-playlist' 5import { MStreamingPlaylistVideo, MStreamingPlaylist } from './video-streaming-playlist'
6 6
7type Use<K extends keyof VideoFileModel, M> = PickWith<VideoFileModel, K, M> 7type Use<K extends keyof VideoFileModel, M> = PickWith<VideoFileModel, K, M>
@@ -10,19 +10,28 @@ type Use<K extends keyof VideoFileModel, M> = PickWith<VideoFileModel, K, M>
10 10
11export type MVideoFile = Omit<VideoFileModel, 'Video' | 'RedundancyVideos' | 'VideoStreamingPlaylist'> 11export type MVideoFile = Omit<VideoFileModel, 'Video' | 'RedundancyVideos' | 'VideoStreamingPlaylist'>
12 12
13export type MVideoFileVideo = MVideoFile & 13export type MVideoFileVideo =
14 MVideoFile &
14 Use<'Video', MVideo> 15 Use<'Video', MVideo>
15 16
16export type MVideoFileStreamingPlaylist = MVideoFile & 17export type MVideoFileStreamingPlaylist =
18 MVideoFile &
17 Use<'VideoStreamingPlaylist', MStreamingPlaylist> 19 Use<'VideoStreamingPlaylist', MStreamingPlaylist>
18 20
19export type MVideoFileStreamingPlaylistVideo = MVideoFile & 21export type MVideoFileStreamingPlaylistVideo =
22 MVideoFile &
20 Use<'VideoStreamingPlaylist', MStreamingPlaylistVideo> 23 Use<'VideoStreamingPlaylist', MStreamingPlaylistVideo>
21 24
22export type MVideoFileVideoUUID = MVideoFile & 25export type MVideoFileVideoUUID =
26 MVideoFile &
23 Use<'Video', MVideoUUID> 27 Use<'Video', MVideoUUID>
24 28
25export type MVideoFileRedundanciesOpt = MVideoFile & 29export type MVideoFileRedundanciesAll =
30 MVideoFile &
31 PickWithOpt<VideoFileModel, 'RedundancyVideos', MVideoRedundancy[]>
32
33export type MVideoFileRedundanciesOpt =
34 MVideoFile &
26 PickWithOpt<VideoFileModel, 'RedundancyVideos', MVideoRedundancyFileUrl[]> 35 PickWithOpt<VideoFileModel, 'RedundancyVideos', MVideoRedundancyFileUrl[]>
27 36
28export function isStreamingPlaylistFile (file: any): file is MVideoFileStreamingPlaylist { 37export function isStreamingPlaylistFile (file: any): file is MVideoFileStreamingPlaylist {
diff --git a/server/typings/models/video/video-import.ts b/server/typings/models/video/video-import.ts
index e119f17f9..4e5c2e4f0 100644
--- a/server/typings/models/video/video-import.ts
+++ b/server/typings/models/video/video-import.ts
@@ -9,18 +9,21 @@ type Use<K extends keyof VideoImportModel, M> = PickWith<VideoImportModel, K, M>
9 9
10export type MVideoImport = Omit<VideoImportModel, 'User' | 'Video'> 10export type MVideoImport = Omit<VideoImportModel, 'User' | 'Video'>
11 11
12export type MVideoImportVideo = MVideoImport & 12export type MVideoImportVideo =
13 MVideoImport &
13 Use<'Video', MVideo> 14 Use<'Video', MVideo>
14 15
15// ############################################################################ 16// ############################################################################
16 17
17type VideoAssociation = MVideoTag & MVideoAccountLight & MVideoThumbnail 18type VideoAssociation = MVideoTag & MVideoAccountLight & MVideoThumbnail
18 19
19export type MVideoImportDefault = MVideoImport & 20export type MVideoImportDefault =
21 MVideoImport &
20 Use<'User', MUser> & 22 Use<'User', MUser> &
21 Use<'Video', VideoAssociation> 23 Use<'Video', VideoAssociation>
22 24
23export type MVideoImportDefaultFiles = MVideoImport & 25export type MVideoImportDefaultFiles =
26 MVideoImport &
24 Use<'User', MUser> & 27 Use<'User', MUser> &
25 Use<'Video', VideoAssociation & MVideoWithFile> 28 Use<'Video', VideoAssociation & MVideoWithFile>
26 29
@@ -28,5 +31,6 @@ export type MVideoImportDefaultFiles = MVideoImport &
28 31
29// Format for API or AP object 32// Format for API or AP object
30 33
31export type MVideoImportFormattable = MVideoImport & 34export type MVideoImportFormattable =
35 MVideoImport &
32 PickWithOpt<VideoImportModel, 'Video', MVideoFormattable & MVideoTag> 36 PickWithOpt<VideoImportModel, 'Video', MVideoFormattable & MVideoTag>
diff --git a/server/typings/models/video/video-playlist-element.ts b/server/typings/models/video/video-playlist-element.ts
index 1aeff78d8..f33c76594 100644
--- a/server/typings/models/video/video-playlist-element.ts
+++ b/server/typings/models/video/video-playlist-element.ts
@@ -17,10 +17,12 @@ export type MVideoPlaylistElementLight = Pick<MVideoPlaylistElement, 'id' | 'vid
17 17
18// ############################################################################ 18// ############################################################################
19 19
20export type MVideoPlaylistVideoThumbnail = MVideoPlaylistElement & 20export type MVideoPlaylistVideoThumbnail =
21 MVideoPlaylistElement &
21 Use<'Video', MVideoThumbnail> 22 Use<'Video', MVideoThumbnail>
22 23
23export type MVideoPlaylistElementVideoUrlPlaylistPrivacy = MVideoPlaylistElement & 24export type MVideoPlaylistElementVideoUrlPlaylistPrivacy =
25 MVideoPlaylistElement &
24 Use<'Video', MVideoUrl> & 26 Use<'Video', MVideoUrl> &
25 Use<'VideoPlaylist', MVideoPlaylistPrivacy> 27 Use<'VideoPlaylist', MVideoPlaylistPrivacy>
26 28
@@ -28,8 +30,10 @@ export type MVideoPlaylistElementVideoUrlPlaylistPrivacy = MVideoPlaylistElement
28 30
29// Format for API or AP object 31// Format for API or AP object
30 32
31export type MVideoPlaylistElementFormattable = MVideoPlaylistElement & 33export type MVideoPlaylistElementFormattable =
34 MVideoPlaylistElement &
32 Use<'Video', MVideoFormattable> 35 Use<'Video', MVideoFormattable>
33 36
34export type MVideoPlaylistElementAP = MVideoPlaylistElement & 37export type MVideoPlaylistElementAP =
38 MVideoPlaylistElement &
35 Use<'Video', MVideoUrl> 39 Use<'Video', MVideoUrl>
diff --git a/server/typings/models/video/video-playlist.ts b/server/typings/models/video/video-playlist.ts
index a40c7aca9..49c27f4a7 100644
--- a/server/typings/models/video/video-playlist.ts
+++ b/server/typings/models/video/video-playlist.ts
@@ -22,30 +22,36 @@ export type MVideoPlaylistVideosLength = MVideoPlaylist & { videosLength?: numbe
22 22
23// With elements 23// With elements
24 24
25export type MVideoPlaylistWithElements = MVideoPlaylist & 25export type MVideoPlaylistWithElements =
26 MVideoPlaylist &
26 Use<'VideoPlaylistElements', MVideoPlaylistElementLight[]> 27 Use<'VideoPlaylistElements', MVideoPlaylistElementLight[]>
27 28
28export type MVideoPlaylistIdWithElements = MVideoPlaylistId & 29export type MVideoPlaylistIdWithElements =
30 MVideoPlaylistId &
29 Use<'VideoPlaylistElements', MVideoPlaylistElementLight[]> 31 Use<'VideoPlaylistElements', MVideoPlaylistElementLight[]>
30 32
31// ############################################################################ 33// ############################################################################
32 34
33// With account 35// With account
34 36
35export type MVideoPlaylistOwner = MVideoPlaylist & 37export type MVideoPlaylistOwner =
38 MVideoPlaylist &
36 Use<'OwnerAccount', MAccount> 39 Use<'OwnerAccount', MAccount>
37 40
38export type MVideoPlaylistOwnerDefault = MVideoPlaylist & 41export type MVideoPlaylistOwnerDefault =
42 MVideoPlaylist &
39 Use<'OwnerAccount', MAccountDefault> 43 Use<'OwnerAccount', MAccountDefault>
40 44
41// ############################################################################ 45// ############################################################################
42 46
43// With thumbnail 47// With thumbnail
44 48
45export type MVideoPlaylistThumbnail = MVideoPlaylist & 49export type MVideoPlaylistThumbnail =
50 MVideoPlaylist &
46 Use<'Thumbnail', MThumbnail> 51 Use<'Thumbnail', MThumbnail>
47 52
48export type MVideoPlaylistAccountThumbnail = MVideoPlaylist & 53export type MVideoPlaylistAccountThumbnail =
54 MVideoPlaylist &
49 Use<'OwnerAccount', MAccountDefault> & 55 Use<'OwnerAccount', MAccountDefault> &
50 Use<'Thumbnail', MThumbnail> 56 Use<'Thumbnail', MThumbnail>
51 57
@@ -53,7 +59,8 @@ export type MVideoPlaylistAccountThumbnail = MVideoPlaylist &
53 59
54// With channel 60// With channel
55 61
56export type MVideoPlaylistAccountChannelDefault = MVideoPlaylist & 62export type MVideoPlaylistAccountChannelDefault =
63 MVideoPlaylist &
57 Use<'OwnerAccount', MAccountDefault> & 64 Use<'OwnerAccount', MAccountDefault> &
58 Use<'VideoChannel', MChannelDefault> 65 Use<'VideoChannel', MChannelDefault>
59 66
@@ -61,7 +68,8 @@ export type MVideoPlaylistAccountChannelDefault = MVideoPlaylist &
61 68
62// With all associations 69// With all associations
63 70
64export type MVideoPlaylistFull = MVideoPlaylist & 71export type MVideoPlaylistFull =
72 MVideoPlaylist &
65 Use<'OwnerAccount', MAccountDefault> & 73 Use<'OwnerAccount', MAccountDefault> &
66 Use<'VideoChannel', MChannelDefault> & 74 Use<'VideoChannel', MChannelDefault> &
67 Use<'Thumbnail', MThumbnail> 75 Use<'Thumbnail', MThumbnail>
@@ -70,11 +78,13 @@ export type MVideoPlaylistFull = MVideoPlaylist &
70 78
71// For API 79// For API
72 80
73export type MVideoPlaylistAccountChannelSummary = MVideoPlaylist & 81export type MVideoPlaylistAccountChannelSummary =
82 MVideoPlaylist &
74 Use<'OwnerAccount', MAccountSummary> & 83 Use<'OwnerAccount', MAccountSummary> &
75 Use<'VideoChannel', MChannelSummary> 84 Use<'VideoChannel', MChannelSummary>
76 85
77export type MVideoPlaylistFullSummary = MVideoPlaylist & 86export type MVideoPlaylistFullSummary =
87 MVideoPlaylist &
78 Use<'Thumbnail', MThumbnail> & 88 Use<'Thumbnail', MThumbnail> &
79 Use<'OwnerAccount', MAccountSummary> & 89 Use<'OwnerAccount', MAccountSummary> &
80 Use<'VideoChannel', MChannelSummary> 90 Use<'VideoChannel', MChannelSummary>
@@ -83,10 +93,12 @@ export type MVideoPlaylistFullSummary = MVideoPlaylist &
83 93
84// Format for API or AP object 94// Format for API or AP object
85 95
86export type MVideoPlaylistFormattable = MVideoPlaylistVideosLength & 96export type MVideoPlaylistFormattable =
97 MVideoPlaylistVideosLength &
87 Use<'OwnerAccount', MAccountSummaryFormattable> & 98 Use<'OwnerAccount', MAccountSummaryFormattable> &
88 Use<'VideoChannel', MChannelSummaryFormattable> 99 Use<'VideoChannel', MChannelSummaryFormattable>
89 100
90export type MVideoPlaylistAP = MVideoPlaylist & 101export type MVideoPlaylistAP =
102 MVideoPlaylist &
91 Use<'Thumbnail', MThumbnail> & 103 Use<'Thumbnail', MThumbnail> &
92 Use<'VideoChannel', MChannelUrl> 104 Use<'VideoChannel', MChannelUrl>
diff --git a/server/typings/models/video/video-rate.ts b/server/typings/models/video/video-rate.ts
index f6bb527fc..64ce4965b 100644
--- a/server/typings/models/video/video-rate.ts
+++ b/server/typings/models/video/video-rate.ts
@@ -9,10 +9,12 @@ type Use<K extends keyof AccountVideoRateModel, M> = PickWith<AccountVideoRateMo
9 9
10export type MAccountVideoRate = Omit<AccountVideoRateModel, 'Video' | 'Account'> 10export type MAccountVideoRate = Omit<AccountVideoRateModel, 'Video' | 'Account'>
11 11
12export type MAccountVideoRateAccountUrl = MAccountVideoRate & 12export type MAccountVideoRateAccountUrl =
13 MAccountVideoRate &
13 Use<'Account', MAccountUrl> 14 Use<'Account', MAccountUrl>
14 15
15export type MAccountVideoRateAccountVideo = MAccountVideoRate & 16export type MAccountVideoRateAccountVideo =
17 MAccountVideoRate &
16 Use<'Account', MAccountAudience> & 18 Use<'Account', MAccountAudience> &
17 Use<'Video', MVideo> 19 Use<'Video', MVideo>
18 20
@@ -20,5 +22,6 @@ export type MAccountVideoRateAccountVideo = MAccountVideoRate &
20 22
21// Format for API or AP object 23// Format for API or AP object
22 24
23export type MAccountVideoRateFormattable = Pick<MAccountVideoRate, 'type'> & 25export type MAccountVideoRateFormattable =
26 Pick<MAccountVideoRate, 'type'> &
24 Use<'Video', MVideoFormattable> 27 Use<'Video', MVideoFormattable>
diff --git a/server/typings/models/video/video-redundancy.ts b/server/typings/models/video/video-redundancy.ts
index 25bdac057..5107aa7f4 100644
--- a/server/typings/models/video/video-redundancy.ts
+++ b/server/typings/models/video/video-redundancy.ts
@@ -16,16 +16,20 @@ export type MVideoRedundancyFileUrl = Pick<MVideoRedundancy, 'fileUrl'>
16 16
17// ############################################################################ 17// ############################################################################
18 18
19export type MVideoRedundancyFile = MVideoRedundancy & 19export type MVideoRedundancyFile =
20 MVideoRedundancy &
20 Use<'VideoFile', MVideoFile> 21 Use<'VideoFile', MVideoFile>
21 22
22export type MVideoRedundancyFileVideo = MVideoRedundancy & 23export type MVideoRedundancyFileVideo =
24 MVideoRedundancy &
23 Use<'VideoFile', MVideoFileVideo> 25 Use<'VideoFile', MVideoFileVideo>
24 26
25export type MVideoRedundancyStreamingPlaylistVideo = MVideoRedundancy & 27export type MVideoRedundancyStreamingPlaylistVideo =
28 MVideoRedundancy &
26 Use<'VideoStreamingPlaylist', MStreamingPlaylistVideo> 29 Use<'VideoStreamingPlaylist', MStreamingPlaylistVideo>
27 30
28export type MVideoRedundancyVideo = MVideoRedundancy & 31export type MVideoRedundancyVideo =
32 MVideoRedundancy &
29 Use<'VideoFile', MVideoFileVideo> & 33 Use<'VideoFile', MVideoFileVideo> &
30 Use<'VideoStreamingPlaylist', MStreamingPlaylistVideo> 34 Use<'VideoStreamingPlaylist', MStreamingPlaylistVideo>
31 35
@@ -33,6 +37,7 @@ export type MVideoRedundancyVideo = MVideoRedundancy &
33 37
34// Format for API or AP object 38// Format for API or AP object
35 39
36export type MVideoRedundancyAP = MVideoRedundancy & 40export type MVideoRedundancyAP =
41 MVideoRedundancy &
37 PickWithOpt<VideoRedundancyModel, 'VideoFile', MVideoFile & PickWith<VideoFileModel, 'Video', MVideoUrl>> & 42 PickWithOpt<VideoRedundancyModel, 'VideoFile', MVideoFile & PickWith<VideoFileModel, 'Video', MVideoUrl>> &
38 PickWithOpt<VideoRedundancyModel, 'VideoStreamingPlaylist', PickWith<VideoStreamingPlaylistModel, 'Video', MVideoUrl>> 43 PickWithOpt<VideoRedundancyModel, 'VideoStreamingPlaylist', PickWith<VideoStreamingPlaylistModel, 'Video', MVideoUrl>>
diff --git a/server/typings/models/video/video-share.ts b/server/typings/models/video/video-share.ts
index a7a90beeb..50ca75d26 100644
--- a/server/typings/models/video/video-share.ts
+++ b/server/typings/models/video/video-share.ts
@@ -9,9 +9,11 @@ type Use<K extends keyof VideoShareModel, M> = PickWith<VideoShareModel, K, M>
9 9
10export type MVideoShare = Omit<VideoShareModel, 'Actor' | 'Video'> 10export type MVideoShare = Omit<VideoShareModel, 'Actor' | 'Video'>
11 11
12export type MVideoShareActor = MVideoShare & 12export type MVideoShareActor =
13 MVideoShare &
13 Use<'Actor', MActorDefault> 14 Use<'Actor', MActorDefault>
14 15
15export type MVideoShareFull = MVideoShare & 16export type MVideoShareFull =
17 MVideoShare &
16 Use<'Actor', MActorDefault> & 18 Use<'Actor', MActorDefault> &
17 Use<'Video', MVideo> 19 Use<'Video', MVideo>
diff --git a/server/typings/models/video/video-streaming-playlist.ts b/server/typings/models/video/video-streaming-playlist.ts
index 436c0c072..3f54aa560 100644
--- a/server/typings/models/video/video-streaming-playlist.ts
+++ b/server/typings/models/video/video-streaming-playlist.ts
@@ -1,6 +1,6 @@
1import { VideoStreamingPlaylistModel } from '../../../models/video/video-streaming-playlist' 1import { VideoStreamingPlaylistModel } from '../../../models/video/video-streaming-playlist'
2import { PickWith, PickWithOpt } from '../../utils' 2import { PickWith, PickWithOpt } from '../../utils'
3import { MVideoRedundancyFileUrl } from './video-redundancy' 3import { MVideoRedundancyFileUrl, MVideoRedundancy } from './video-redundancy'
4import { MVideo } from './video' 4import { MVideo } from './video'
5import { MVideoFile } from './video-file' 5import { MVideoFile } from './video-file'
6 6
@@ -10,21 +10,31 @@ type Use<K extends keyof VideoStreamingPlaylistModel, M> = PickWith<VideoStreami
10 10
11export type MStreamingPlaylist = Omit<VideoStreamingPlaylistModel, 'Video' | 'RedundancyVideos' | 'VideoFiles'> 11export type MStreamingPlaylist = Omit<VideoStreamingPlaylistModel, 'Video' | 'RedundancyVideos' | 'VideoFiles'>
12 12
13export type MStreamingPlaylistFiles = MStreamingPlaylist & 13export type MStreamingPlaylistFiles =
14 MStreamingPlaylist &
14 Use<'VideoFiles', MVideoFile[]> 15 Use<'VideoFiles', MVideoFile[]>
15 16
16export type MStreamingPlaylistVideo = MStreamingPlaylist & 17export type MStreamingPlaylistVideo =
18 MStreamingPlaylist &
17 Use<'Video', MVideo> 19 Use<'Video', MVideo>
18 20
19export type MStreamingPlaylistFilesVideo = MStreamingPlaylist & 21export type MStreamingPlaylistFilesVideo =
22 MStreamingPlaylist &
20 Use<'VideoFiles', MVideoFile[]> & 23 Use<'VideoFiles', MVideoFile[]> &
21 Use<'Video', MVideo> 24 Use<'Video', MVideo>
22 25
23export type MStreamingPlaylistRedundancies = MStreamingPlaylist & 26export type MStreamingPlaylistRedundanciesAll =
27 MStreamingPlaylist &
28 Use<'VideoFiles', MVideoFile[]> &
29 Use<'RedundancyVideos', MVideoRedundancy[]>
30
31export type MStreamingPlaylistRedundancies =
32 MStreamingPlaylist &
24 Use<'VideoFiles', MVideoFile[]> & 33 Use<'VideoFiles', MVideoFile[]> &
25 Use<'RedundancyVideos', MVideoRedundancyFileUrl[]> 34 Use<'RedundancyVideos', MVideoRedundancyFileUrl[]>
26 35
27export type MStreamingPlaylistRedundanciesOpt = MStreamingPlaylist & 36export type MStreamingPlaylistRedundanciesOpt =
37 MStreamingPlaylist &
28 Use<'VideoFiles', MVideoFile[]> & 38 Use<'VideoFiles', MVideoFile[]> &
29 PickWithOpt<VideoStreamingPlaylistModel, 'RedundancyVideos', MVideoRedundancyFileUrl[]> 39 PickWithOpt<VideoStreamingPlaylistModel, 'RedundancyVideos', MVideoRedundancyFileUrl[]>
30 40
diff --git a/server/typings/models/video/video.ts b/server/typings/models/video/video.ts
index 7f69a91de..022a9566d 100644
--- a/server/typings/models/video/video.ts
+++ b/server/typings/models/video/video.ts
@@ -9,9 +9,14 @@ import {
9 MChannelUserId 9 MChannelUserId
10} from './video-channels' 10} from './video-channels'
11import { MTag } from './tag' 11import { MTag } from './tag'
12import { MVideoCaptionLanguage } from './video-caption' 12import { MVideoCaptionLanguage, MVideoCaptionLanguageUrl } from './video-caption'
13import { MStreamingPlaylistFiles, MStreamingPlaylistRedundancies, MStreamingPlaylistRedundanciesOpt } from './video-streaming-playlist' 13import {
14import { MVideoFile, MVideoFileRedundanciesOpt } from './video-file' 14 MStreamingPlaylistFiles,
15 MStreamingPlaylistRedundancies,
16 MStreamingPlaylistRedundanciesAll,
17 MStreamingPlaylistRedundanciesOpt
18} from './video-streaming-playlist'
19import { MVideoFile, MVideoFileRedundanciesAll, MVideoFileRedundanciesOpt } from './video-file'
15import { MThumbnail } from './thumbnail' 20import { MThumbnail } from './thumbnail'
16import { MVideoBlacklist, MVideoBlacklistLight, MVideoBlacklistUnfederated } from './video-blacklist' 21import { MVideoBlacklist, MVideoBlacklistLight, MVideoBlacklistUnfederated } from './video-blacklist'
17import { MScheduleVideoUpdate } from './schedule-video-update' 22import { MScheduleVideoUpdate } from './schedule-video-update'
@@ -21,7 +26,8 @@ type Use<K extends keyof VideoModel, M> = PickWith<VideoModel, K, M>
21 26
22// ############################################################################ 27// ############################################################################
23 28
24export type MVideo = Omit<VideoModel, 'VideoChannel' | 'Tags' | 'Thumbnails' | 'VideoPlaylistElements' | 'VideoAbuses' | 29export type MVideo =
30 Omit<VideoModel, 'VideoChannel' | 'Tags' | 'Thumbnails' | 'VideoPlaylistElements' | 'VideoAbuses' |
25 'VideoFiles' | 'VideoStreamingPlaylists' | 'VideoShares' | 'AccountVideoRates' | 'VideoComments' | 'VideoViews' | 'UserVideoHistories' | 31 'VideoFiles' | 'VideoStreamingPlaylists' | 'VideoShares' | 'AccountVideoRates' | 'VideoComments' | 'VideoViews' | 'UserVideoHistories' |
26 'ScheduleVideoUpdate' | 'VideoBlacklist' | 'VideoImport' | 'VideoCaptions'> 32 'ScheduleVideoUpdate' | 'VideoBlacklist' | 'VideoImport' | 'VideoCaptions'>
27 33
@@ -31,6 +37,7 @@ export type MVideoId = Pick<MVideo, 'id'>
31export type MVideoUrl = Pick<MVideo, 'url'> 37export type MVideoUrl = Pick<MVideo, 'url'>
32export type MVideoUUID = Pick<MVideo, 'uuid'> 38export type MVideoUUID = Pick<MVideo, 'uuid'>
33 39
40export type MVideoImmutable = Pick<MVideo, 'id' | 'url' | 'uuid' | 'remote' | 'isOwned'>
34export type MVideoIdUrl = MVideoId & MVideoUrl 41export type MVideoIdUrl = MVideoId & MVideoUrl
35export type MVideoFeed = Pick<MVideo, 'name' | 'uuid'> 42export type MVideoFeed = Pick<MVideo, 'name' | 'uuid'>
36 43
@@ -39,50 +46,63 @@ export type MVideoFeed = Pick<MVideo, 'name' | 'uuid'>
39// Video raw associations: schedules, video files, tags, thumbnails, captions, streaming playlists 46// Video raw associations: schedules, video files, tags, thumbnails, captions, streaming playlists
40 47
41// "With" to not confuse with the VideoFile model 48// "With" to not confuse with the VideoFile model
42export type MVideoWithFile = MVideo & 49export type MVideoWithFile =
50 MVideo &
43 Use<'VideoFiles', MVideoFile[]> & 51 Use<'VideoFiles', MVideoFile[]> &
44 Use<'VideoStreamingPlaylists', MStreamingPlaylistFiles[]> 52 Use<'VideoStreamingPlaylists', MStreamingPlaylistFiles[]>
45 53
46export type MVideoThumbnail = MVideo & 54export type MVideoThumbnail =
55 MVideo &
47 Use<'Thumbnails', MThumbnail[]> 56 Use<'Thumbnails', MThumbnail[]>
48 57
49export type MVideoIdThumbnail = MVideoId & 58export type MVideoIdThumbnail =
59 MVideoId &
50 Use<'Thumbnails', MThumbnail[]> 60 Use<'Thumbnails', MThumbnail[]>
51 61
52export type MVideoWithFileThumbnail = MVideo & 62export type MVideoWithFileThumbnail =
63 MVideo &
53 Use<'VideoFiles', MVideoFile[]> & 64 Use<'VideoFiles', MVideoFile[]> &
54 Use<'Thumbnails', MThumbnail[]> 65 Use<'Thumbnails', MThumbnail[]>
55 66
56export type MVideoThumbnailBlacklist = MVideo & 67export type MVideoThumbnailBlacklist =
68 MVideo &
57 Use<'Thumbnails', MThumbnail[]> & 69 Use<'Thumbnails', MThumbnail[]> &
58 Use<'VideoBlacklist', MVideoBlacklistLight> 70 Use<'VideoBlacklist', MVideoBlacklistLight>
59 71
60export type MVideoTag = MVideo & 72export type MVideoTag =
73 MVideo &
61 Use<'Tags', MTag[]> 74 Use<'Tags', MTag[]>
62 75
63export type MVideoWithSchedule = MVideo & 76export type MVideoWithSchedule =
77 MVideo &
64 PickWithOpt<VideoModel, 'ScheduleVideoUpdate', MScheduleVideoUpdate> 78 PickWithOpt<VideoModel, 'ScheduleVideoUpdate', MScheduleVideoUpdate>
65 79
66export type MVideoWithCaptions = MVideo & 80export type MVideoWithCaptions =
81 MVideo &
67 Use<'VideoCaptions', MVideoCaptionLanguage[]> 82 Use<'VideoCaptions', MVideoCaptionLanguage[]>
68 83
69export type MVideoWithStreamingPlaylist = MVideo & 84export type MVideoWithStreamingPlaylist =
85 MVideo &
70 Use<'VideoStreamingPlaylists', MStreamingPlaylistFiles[]> 86 Use<'VideoStreamingPlaylists', MStreamingPlaylistFiles[]>
71 87
72// ############################################################################ 88// ############################################################################
73 89
74// Associations with not all their attributes 90// Associations with not all their attributes
75 91
76export type MVideoUserHistory = MVideo & 92export type MVideoUserHistory =
93 MVideo &
77 Use<'UserVideoHistories', MUserVideoHistoryTime[]> 94 Use<'UserVideoHistories', MUserVideoHistoryTime[]>
78 95
79export type MVideoWithBlacklistLight = MVideo & 96export type MVideoWithBlacklistLight =
97 MVideo &
80 Use<'VideoBlacklist', MVideoBlacklistLight> 98 Use<'VideoBlacklist', MVideoBlacklistLight>
81 99
82export type MVideoAccountLight = MVideo & 100export type MVideoAccountLight =
101 MVideo &
83 Use<'VideoChannel', MChannelAccountLight> 102 Use<'VideoChannel', MChannelAccountLight>
84 103
85export type MVideoWithRights = MVideo & 104export type MVideoWithRights =
105 MVideo &
86 Use<'VideoBlacklist', MVideoBlacklistLight> & 106 Use<'VideoBlacklist', MVideoBlacklistLight> &
87 Use<'Thumbnails', MThumbnail[]> & 107 Use<'Thumbnails', MThumbnail[]> &
88 Use<'VideoChannel', MChannelUserId> 108 Use<'VideoChannel', MChannelUserId>
@@ -91,12 +111,14 @@ export type MVideoWithRights = MVideo &
91 111
92// All files with some additional associations 112// All files with some additional associations
93 113
94export type MVideoWithAllFiles = MVideo & 114export type MVideoWithAllFiles =
115 MVideo &
95 Use<'VideoFiles', MVideoFile[]> & 116 Use<'VideoFiles', MVideoFile[]> &
96 Use<'Thumbnails', MThumbnail[]> & 117 Use<'Thumbnails', MThumbnail[]> &
97 Use<'VideoStreamingPlaylists', MStreamingPlaylistFiles[]> 118 Use<'VideoStreamingPlaylists', MStreamingPlaylistFiles[]>
98 119
99export type MVideoAccountLightBlacklistAllFiles = MVideo & 120export type MVideoAccountLightBlacklistAllFiles =
121 MVideo &
100 Use<'VideoFiles', MVideoFile[]> & 122 Use<'VideoFiles', MVideoFile[]> &
101 Use<'Thumbnails', MThumbnail[]> & 123 Use<'Thumbnails', MThumbnail[]> &
102 Use<'VideoStreamingPlaylists', MStreamingPlaylistFiles[]> & 124 Use<'VideoStreamingPlaylists', MStreamingPlaylistFiles[]> &
@@ -107,17 +129,21 @@ export type MVideoAccountLightBlacklistAllFiles = MVideo &
107 129
108// With account 130// With account
109 131
110export type MVideoAccountDefault = MVideo & 132export type MVideoAccountDefault =
133 MVideo &
111 Use<'VideoChannel', MChannelAccountDefault> 134 Use<'VideoChannel', MChannelAccountDefault>
112 135
113export type MVideoThumbnailAccountDefault = MVideo & 136export type MVideoThumbnailAccountDefault =
137 MVideo &
114 Use<'Thumbnails', MThumbnail[]> & 138 Use<'Thumbnails', MThumbnail[]> &
115 Use<'VideoChannel', MChannelAccountDefault> 139 Use<'VideoChannel', MChannelAccountDefault>
116 140
117export type MVideoWithChannelActor = MVideo & 141export type MVideoWithChannelActor =
142 MVideo &
118 Use<'VideoChannel', MChannelActor> 143 Use<'VideoChannel', MChannelActor>
119 144
120export type MVideoFullLight = MVideo & 145export type MVideoFullLight =
146 MVideo &
121 Use<'Thumbnails', MThumbnail[]> & 147 Use<'Thumbnails', MThumbnail[]> &
122 Use<'VideoBlacklist', MVideoBlacklistLight> & 148 Use<'VideoBlacklist', MVideoBlacklistLight> &
123 Use<'Tags', MTag[]> & 149 Use<'Tags', MTag[]> &
@@ -131,18 +157,20 @@ export type MVideoFullLight = MVideo &
131 157
132// API 158// API
133 159
134export type MVideoAP = MVideo & 160export type MVideoAP =
161 MVideo &
135 Use<'Tags', MTag[]> & 162 Use<'Tags', MTag[]> &
136 Use<'VideoChannel', MChannelAccountLight> & 163 Use<'VideoChannel', MChannelAccountLight> &
137 Use<'VideoStreamingPlaylists', MStreamingPlaylistFiles[]> & 164 Use<'VideoStreamingPlaylists', MStreamingPlaylistFiles[]> &
138 Use<'VideoCaptions', MVideoCaptionLanguage[]> & 165 Use<'VideoCaptions', MVideoCaptionLanguageUrl[]> &
139 Use<'VideoBlacklist', MVideoBlacklistUnfederated> & 166 Use<'VideoBlacklist', MVideoBlacklistUnfederated> &
140 Use<'VideoFiles', MVideoFileRedundanciesOpt[]> & 167 Use<'VideoFiles', MVideoFileRedundanciesOpt[]> &
141 Use<'Thumbnails', MThumbnail[]> 168 Use<'Thumbnails', MThumbnail[]>
142 169
143export type MVideoAPWithoutCaption = Omit<MVideoAP, 'VideoCaptions'> 170export type MVideoAPWithoutCaption = Omit<MVideoAP, 'VideoCaptions'>
144 171
145export type MVideoDetails = MVideo & 172export type MVideoDetails =
173 MVideo &
146 Use<'VideoBlacklist', MVideoBlacklistLight> & 174 Use<'VideoBlacklist', MVideoBlacklistLight> &
147 Use<'Tags', MTag[]> & 175 Use<'Tags', MTag[]> &
148 Use<'VideoChannel', MChannelAccountLight> & 176 Use<'VideoChannel', MChannelAccountLight> &
@@ -152,23 +180,31 @@ export type MVideoDetails = MVideo &
152 Use<'VideoStreamingPlaylists', MStreamingPlaylistRedundancies[]> & 180 Use<'VideoStreamingPlaylists', MStreamingPlaylistRedundancies[]> &
153 Use<'VideoFiles', MVideoFileRedundanciesOpt[]> 181 Use<'VideoFiles', MVideoFileRedundanciesOpt[]>
154 182
155export type MVideoForUser = MVideo & 183export type MVideoForUser =
184 MVideo &
156 Use<'VideoChannel', MChannelAccountDefault> & 185 Use<'VideoChannel', MChannelAccountDefault> &
157 Use<'ScheduleVideoUpdate', MScheduleVideoUpdate> & 186 Use<'ScheduleVideoUpdate', MScheduleVideoUpdate> &
158 Use<'VideoBlacklist', MVideoBlacklistLight> & 187 Use<'VideoBlacklist', MVideoBlacklistLight> &
159 Use<'Thumbnails', MThumbnail[]> 188 Use<'Thumbnails', MThumbnail[]>
160 189
190export type MVideoForRedundancyAPI =
191 MVideo &
192 Use<'VideoFiles', MVideoFileRedundanciesAll[]> &
193 Use<'VideoStreamingPlaylists', MStreamingPlaylistRedundanciesAll[]>
194
161// ############################################################################ 195// ############################################################################
162 196
163// Format for API or AP object 197// Format for API or AP object
164 198
165export type MVideoFormattable = MVideo & 199export type MVideoFormattable =
200 MVideo &
166 PickWithOpt<VideoModel, 'UserVideoHistories', MUserVideoHistoryTime[]> & 201 PickWithOpt<VideoModel, 'UserVideoHistories', MUserVideoHistoryTime[]> &
167 Use<'VideoChannel', MChannelAccountSummaryFormattable> & 202 Use<'VideoChannel', MChannelAccountSummaryFormattable> &
168 PickWithOpt<VideoModel, 'ScheduleVideoUpdate', Pick<MScheduleVideoUpdate, 'updateAt' | 'privacy'>> & 203 PickWithOpt<VideoModel, 'ScheduleVideoUpdate', Pick<MScheduleVideoUpdate, 'updateAt' | 'privacy'>> &
169 PickWithOpt<VideoModel, 'VideoBlacklist', Pick<MVideoBlacklist, 'reason'>> 204 PickWithOpt<VideoModel, 'VideoBlacklist', Pick<MVideoBlacklist, 'reason'>>
170 205
171export type MVideoFormattableDetails = MVideoFormattable & 206export type MVideoFormattableDetails =
207 MVideoFormattable &
172 Use<'VideoChannel', MChannelFormattable> & 208 Use<'VideoChannel', MChannelFormattable> &
173 Use<'Tags', MTag[]> & 209 Use<'Tags', MTag[]> &
174 Use<'VideoStreamingPlaylists', MStreamingPlaylistRedundanciesOpt[]> & 210 Use<'VideoStreamingPlaylists', MStreamingPlaylistRedundanciesOpt[]> &
diff --git a/server/typings/plugins/register-server-option.model.ts b/server/typings/plugins/register-server-option.model.ts
index 54753cc01..8f1d66007 100644
--- a/server/typings/plugins/register-server-option.model.ts
+++ b/server/typings/plugins/register-server-option.model.ts
@@ -1,11 +1,55 @@
1import { logger } from '../../helpers/logger' 1import * as Bluebird from 'bluebird'
2import { Router } from 'express'
3import { Logger } from 'winston'
4import { ActorModel } from '@server/models/activitypub/actor'
5import { VideoBlacklistCreate } from '@shared/models'
6import { PluginPlaylistPrivacyManager } from '@shared/models/plugins/plugin-playlist-privacy-manager.model'
7import { PluginVideoPrivacyManager } from '@shared/models/plugins/plugin-video-privacy-manager.model'
8import {
9 RegisterServerAuthExternalOptions,
10 RegisterServerAuthExternalResult,
11 RegisterServerAuthPassOptions
12} from '@shared/models/plugins/register-server-auth.model'
2import { PluginSettingsManager } from '../../../shared/models/plugins/plugin-settings-manager.model' 13import { PluginSettingsManager } from '../../../shared/models/plugins/plugin-settings-manager.model'
3import { PluginStorageManager } from '../../../shared/models/plugins/plugin-storage-manager.model' 14import { PluginStorageManager } from '../../../shared/models/plugins/plugin-storage-manager.model'
4import { RegisterServerHookOptions } from '../../../shared/models/plugins/register-server-hook.model'
5import { RegisterServerSettingOptions } from '../../../shared/models/plugins/register-server-setting.model'
6import { PluginVideoCategoryManager } from '../../../shared/models/plugins/plugin-video-category-manager.model' 15import { PluginVideoCategoryManager } from '../../../shared/models/plugins/plugin-video-category-manager.model'
7import { PluginVideoLanguageManager } from '../../../shared/models/plugins/plugin-video-language-manager.model' 16import { PluginVideoLanguageManager } from '../../../shared/models/plugins/plugin-video-language-manager.model'
8import { PluginVideoLicenceManager } from '../../../shared/models/plugins/plugin-video-licence-manager.model' 17import { PluginVideoLicenceManager } from '../../../shared/models/plugins/plugin-video-licence-manager.model'
18import { RegisterServerHookOptions } from '../../../shared/models/plugins/register-server-hook.model'
19import { RegisterServerSettingOptions } from '../../../shared/models/plugins/register-server-setting.model'
20import { MVideoThumbnail } from '../models'
21
22export type PeerTubeHelpers = {
23 logger: Logger
24
25 database: {
26 query: Function
27 }
28
29 videos: {
30 loadByUrl: (url: string) => Bluebird<MVideoThumbnail>
31
32 removeVideo: (videoId: number) => Promise<void>
33 }
34
35 config: {
36 getWebserverUrl: () => string
37 }
38
39 moderation: {
40 blockServer: (options: { byAccountId: number, hostToBlock: string }) => Promise<void>
41 unblockServer: (options: { byAccountId: number, hostToUnblock: string }) => Promise<void>
42 blockAccount: (options: { byAccountId: number, handleToBlock: string }) => Promise<void>
43 unblockAccount: (options: { byAccountId: number, handleToUnblock: string }) => Promise<void>
44
45 blacklistVideo: (options: { videoIdOrUUID: number | string, createOptions: VideoBlacklistCreate }) => Promise<void>
46 unblacklistVideo: (options: { videoIdOrUUID: number | string }) => Promise<void>
47 }
48
49 server: {
50 getServerActor: () => Promise<ActorModel>
51 }
52}
9 53
10export type RegisterServerOptions = { 54export type RegisterServerOptions = {
11 registerHook: (options: RegisterServerHookOptions) => void 55 registerHook: (options: RegisterServerHookOptions) => void
@@ -20,7 +64,19 @@ export type RegisterServerOptions = {
20 videoLanguageManager: PluginVideoLanguageManager 64 videoLanguageManager: PluginVideoLanguageManager
21 videoLicenceManager: PluginVideoLicenceManager 65 videoLicenceManager: PluginVideoLicenceManager
22 66
23 peertubeHelpers: { 67 videoPrivacyManager: PluginVideoPrivacyManager
24 logger: typeof logger 68 playlistPrivacyManager: PluginPlaylistPrivacyManager
25 } 69
70 registerIdAndPassAuth: (options: RegisterServerAuthPassOptions) => void
71 registerExternalAuth: (options: RegisterServerAuthExternalOptions) => RegisterServerAuthExternalResult
72 unregisterIdAndPassAuth: (authName: string) => void
73 unregisterExternalAuth: (authName: string) => void
74
75 // Get plugin router to create custom routes
76 // Base routes of this router are
77 // * /plugins/:pluginName/:pluginVersion/router/...
78 // * /plugins/:pluginName/router/...
79 getRouter(): Router
80
81 peertubeHelpers: PeerTubeHelpers
26} 82}
diff --git a/server/typings/utils.ts b/server/typings/utils.ts
index 24d43b258..55500d8c4 100644
--- a/server/typings/utils.ts
+++ b/server/typings/utils.ts
@@ -1,3 +1,5 @@
1/* eslint-disable @typescript-eslint/array-type */
2
1export type FunctionPropertyNames<T> = { 3export type FunctionPropertyNames<T> = {
2 [K in keyof T]: T[K] extends Function ? K : never 4 [K in keyof T]: T[K] extends Function ? K : never
3}[keyof T] 5}[keyof T]