aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--CREDITS.md34
-rw-r--r--README.md4
-rw-r--r--client/package.json2
-rw-r--r--client/src/app/+about/about-instance/about-instance.component.html83
-rw-r--r--client/src/app/+about/about-instance/about-instance.component.scss35
-rw-r--r--client/src/app/+about/about-instance/about-instance.component.ts49
-rw-r--r--client/src/app/+about/about-peertube/about-peertube-contributors.component.html13
-rw-r--r--client/src/app/+about/about-peertube/about-peertube-contributors.component.scss15
-rw-r--r--client/src/app/+about/about-peertube/about-peertube-contributors.component.ts19
-rw-r--r--client/src/app/+about/about-peertube/about-peertube.component.html163
-rw-r--r--client/src/app/+about/about-peertube/about-peertube.component.scss43
-rw-r--r--client/src/app/+about/about.module.ts3
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html337
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss13
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts83
-rw-r--r--client/src/app/+admin/system/debug/debug.component.html2
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts6
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.html21
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.ts47
-rw-r--r--client/src/app/+my-account/my-account.module.ts4
-rw-r--r--client/src/app/+signup/+register/register-step-user.component.html15
-rw-r--r--client/src/app/+signup/+register/register-step-user.component.ts16
-rw-r--r--client/src/app/+signup/+register/register.component.html63
-rw-r--r--client/src/app/+signup/+register/register.component.scss37
-rw-r--r--client/src/app/+signup/+register/register.component.ts40
-rw-r--r--client/src/app/+signup/+register/register.module.ts4
-rw-r--r--client/src/app/app.component.html5
-rw-r--r--client/src/app/app.component.ts49
-rw-r--r--client/src/app/app.module.ts7
-rw-r--r--client/src/app/login/login.component.html9
-rw-r--r--client/src/app/modal/instance-config-warning-modal.component.html45
-rw-r--r--client/src/app/modal/instance-config-warning-modal.component.scss22
-rw-r--r--client/src/app/modal/instance-config-warning-modal.component.ts47
-rw-r--r--client/src/app/modal/welcome-modal.component.html67
-rw-r--r--client/src/app/modal/welcome-modal.component.scss56
-rw-r--r--client/src/app/modal/welcome-modal.component.ts38
-rw-r--r--client/src/app/shared/angular/peertube-template.directive.ts4
-rw-r--r--client/src/app/shared/forms/form-validators/custom-config-validators.service.ts9
-rw-r--r--client/src/app/shared/forms/peertube-checkbox.component.html11
-rw-r--r--client/src/app/shared/forms/peertube-checkbox.component.scss2
-rw-r--r--client/src/app/shared/forms/peertube-checkbox.component.ts24
-rw-r--r--client/src/app/shared/instance/feature-boolean.component.html3
-rw-r--r--client/src/app/shared/instance/feature-boolean.component.scss10
-rw-r--r--client/src/app/shared/instance/feature-boolean.component.ts10
-rw-r--r--client/src/app/shared/instance/instance-features-table.component.html71
-rw-r--r--client/src/app/shared/instance/instance-features-table.component.scss26
-rw-r--r--client/src/app/shared/instance/instance-features-table.component.ts36
-rw-r--r--client/src/app/shared/instance/instance.service.ts46
-rw-r--r--client/src/app/shared/misc/help.component.html28
-rw-r--r--client/src/app/shared/misc/help.component.ts36
-rw-r--r--client/src/app/shared/renderer/markdown.service.ts19
-rw-r--r--client/src/app/shared/shared.module.ts9
-rw-r--r--client/src/app/shared/user-subscription/remote-subscribe.component.html22
-rw-r--r--client/src/app/shared/users/user-notification.model.ts12
-rw-r--r--client/src/app/shared/users/user-notifications.component.html12
-rw-r--r--client/src/app/shared/users/user.model.ts33
-rw-r--r--client/src/app/shared/video/videos-selection.component.ts2
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.component.html54
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html12
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html14
-rw-r--r--client/src/assets/images/framasoft.pngbin0 -> 5232 bytes
-rw-r--r--client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts2
-rw-r--r--client/src/assets/player/p2p-media-loader/redundancy-url-manager.ts17
-rw-r--r--client/src/sass/include/_mixins.scss2
-rw-r--r--client/src/sass/player/peertube-skin.scss5
-rw-r--r--client/src/standalone/videos/embed.ts2
-rw-r--r--client/yarn.lock104
-rw-r--r--config/default.yaml74
-rw-r--r--config/production.yaml.example85
-rw-r--r--package.json7
-rwxr-xr-xscripts/client-report.sh4
-rwxr-xr-xscripts/generate-code-contributors.ts4
-rw-r--r--server.ts4
-rw-r--r--server/controllers/activitypub/client.ts39
-rw-r--r--server/controllers/activitypub/inbox.ts5
-rw-r--r--server/controllers/activitypub/outbox.ts10
-rw-r--r--server/controllers/api/config.ts38
-rw-r--r--server/controllers/api/search.ts5
-rw-r--r--server/controllers/api/server/follows.ts3
-rw-r--r--server/controllers/api/users/index.ts5
-rw-r--r--server/controllers/api/users/me.ts29
-rw-r--r--server/controllers/api/users/my-history.ts1
-rw-r--r--server/controllers/api/users/my-notifications.ts3
-rw-r--r--server/controllers/api/video-channel.ts9
-rw-r--r--server/controllers/api/video-playlist.ts36
-rw-r--r--server/controllers/api/videos/abuse.ts20
-rw-r--r--server/controllers/api/videos/blacklist.ts17
-rw-r--r--server/controllers/api/videos/captions.ts9
-rw-r--r--server/controllers/api/videos/comment.ts17
-rw-r--r--server/controllers/api/videos/import.ts38
-rw-r--r--server/controllers/api/videos/index.ts19
-rw-r--r--server/controllers/api/videos/ownership.ts5
-rw-r--r--server/controllers/api/videos/rate.ts2
-rw-r--r--server/controllers/api/videos/watching.ts2
-rw-r--r--server/controllers/feeds.ts2
-rw-r--r--server/controllers/services.ts2
-rw-r--r--server/controllers/static.ts6
-rw-r--r--server/controllers/webfinger.ts2
-rw-r--r--server/helpers/activitypub.ts3
-rw-r--r--server/helpers/actor.ts9
-rw-r--r--server/helpers/captions-utils.ts6
-rw-r--r--server/helpers/custom-jsonld-signature.ts5
-rw-r--r--server/helpers/custom-validators/activitypub/actor.ts15
-rw-r--r--server/helpers/custom-validators/users.ts10
-rw-r--r--server/helpers/custom-validators/video-ownership.ts22
-rw-r--r--server/helpers/middlewares/accounts.ts7
-rw-r--r--server/helpers/middlewares/video-abuses.ts44
-rw-r--r--server/helpers/middlewares/video-captions.ts4
-rw-r--r--server/helpers/middlewares/video-channels.ts45
-rw-r--r--server/helpers/middlewares/video-playlists.ts35
-rw-r--r--server/helpers/middlewares/videos.ts26
-rw-r--r--server/helpers/peertube-crypto.ts126
-rw-r--r--server/helpers/utils.ts5
-rw-r--r--server/helpers/video.ts40
-rw-r--r--server/helpers/webfinger.ts3
-rw-r--r--server/initializers/config.ts30
-rw-r--r--server/initializers/constants.ts9
-rw-r--r--server/initializers/migrations/0425-nullable-actor-fields.ts26
-rw-r--r--server/initializers/migrations/0430-auto-follow-notification-setting.ts40
-rw-r--r--server/initializers/migrations/0435-user-modals.ts40
-rw-r--r--server/lib/activitypub/actor.ts82
-rw-r--r--server/lib/activitypub/audience.ts27
-rw-r--r--server/lib/activitypub/cache-file.ts14
-rw-r--r--server/lib/activitypub/follow.ts36
-rw-r--r--server/lib/activitypub/playlist.ts19
-rw-r--r--server/lib/activitypub/process/process-accept.ts7
-rw-r--r--server/lib/activitypub/process/process-announce.ts7
-rw-r--r--server/lib/activitypub/process/process-create.ts14
-rw-r--r--server/lib/activitypub/process/process-delete.ts26
-rw-r--r--server/lib/activitypub/process/process-dislike.ts4
-rw-r--r--server/lib/activitypub/process/process-flag.ts44
-rw-r--r--server/lib/activitypub/process/process-follow.ts31
-rw-r--r--server/lib/activitypub/process/process-like.ts4
-rw-r--r--server/lib/activitypub/process/process-reject.ts4
-rw-r--r--server/lib/activitypub/process/process-undo.ts12
-rw-r--r--server/lib/activitypub/process/process-update.ts15
-rw-r--r--server/lib/activitypub/process/process-view.ts6
-rw-r--r--server/lib/activitypub/process/process.ts11
-rw-r--r--server/lib/activitypub/send/send-accept.ts7
-rw-r--r--server/lib/activitypub/send/send-announce.ts15
-rw-r--r--server/lib/activitypub/send/send-create.ts34
-rw-r--r--server/lib/activitypub/send/send-delete.ts12
-rw-r--r--server/lib/activitypub/send/send-dislike.ts7
-rw-r--r--server/lib/activitypub/send/send-flag.ts9
-rw-r--r--server/lib/activitypub/send/send-follow.ts7
-rw-r--r--server/lib/activitypub/send/send-like.ts7
-rw-r--r--server/lib/activitypub/send/send-reject.ts7
-rw-r--r--server/lib/activitypub/send/send-undo.ts34
-rw-r--r--server/lib/activitypub/send/send-update.ts31
-rw-r--r--server/lib/activitypub/send/send-view.ts6
-rw-r--r--server/lib/activitypub/send/utils.ts35
-rw-r--r--server/lib/activitypub/share.ts21
-rw-r--r--server/lib/activitypub/url.ts60
-rw-r--r--server/lib/activitypub/video-comments.ts16
-rw-r--r--server/lib/activitypub/video-rates.ts24
-rw-r--r--server/lib/activitypub/videos.ts141
-rw-r--r--server/lib/avatar.ts8
-rw-r--r--server/lib/blocklist.ts5
-rw-r--r--server/lib/client-html.ts11
-rw-r--r--server/lib/emailer.ts63
-rw-r--r--server/lib/hls.ts6
-rw-r--r--server/lib/job-queue/handlers/activitypub-follow.ts24
-rw-r--r--server/lib/job-queue/handlers/activitypub-http-fetcher.ts5
-rw-r--r--server/lib/job-queue/handlers/utils/activitypub-http-utils.ts4
-rw-r--r--server/lib/job-queue/handlers/video-file-import.ts11
-rw-r--r--server/lib/job-queue/handlers/video-import.ts55
-rw-r--r--server/lib/job-queue/handlers/video-transcoding.ts7
-rw-r--r--server/lib/notifier.ts207
-rw-r--r--server/lib/oauth-model.ts3
-rw-r--r--server/lib/peertube-socket.ts7
-rw-r--r--server/lib/redundancy.ts3
-rw-r--r--server/lib/schedulers/auto-follow-index-instances.ts72
-rw-r--r--server/lib/schedulers/videos-redundancy-scheduler.ts57
-rw-r--r--server/lib/thumbnail.ts26
-rw-r--r--server/lib/user.ts36
-rw-r--r--server/lib/video-blacklist.ts17
-rw-r--r--server/lib/video-channel.ts19
-rw-r--r--server/lib/video-comment.ts11
-rw-r--r--server/lib/video-playlist.ts7
-rw-r--r--server/lib/video-transcoding.ts16
-rw-r--r--server/middlewares/activitypub.ts2
-rw-r--r--server/middlewares/validators/follows.ts3
-rw-r--r--server/middlewares/validators/redundancy.ts4
-rw-r--r--server/middlewares/validators/user-notifications.ts2
-rw-r--r--server/middlewares/validators/users.ts10
-rw-r--r--server/middlewares/validators/videos/video-abuses.ts4
-rw-r--r--server/middlewares/validators/videos/video-blacklist.ts6
-rw-r--r--server/middlewares/validators/videos/video-captions.ts6
-rw-r--r--server/middlewares/validators/videos/video-channels.ts4
-rw-r--r--server/middlewares/validators/videos/video-comments.ts40
-rw-r--r--server/middlewares/validators/videos/video-playlists.ts86
-rw-r--r--server/middlewares/validators/videos/video-shares.ts2
-rw-r--r--server/middlewares/validators/videos/videos.ts20
-rw-r--r--server/middlewares/validators/webfinger.ts3
-rw-r--r--server/models/account/account-blocklist.ts8
-rw-r--r--server/models/account/account-video-rate.ts28
-rw-r--r--server/models/account/account.ts27
-rw-r--r--server/models/account/user-notification-setting.ts15
-rw-r--r--server/models/account/user-notification.ts29
-rw-r--r--server/models/account/user-video-history.ts7
-rw-r--r--server/models/account/user.ts94
-rw-r--r--server/models/activitypub/actor-follow.ts71
-rw-r--r--server/models/activitypub/actor.ts71
-rw-r--r--server/models/avatar/avatar.ts3
-rw-r--r--server/models/oauth/oauth-token.ts19
-rw-r--r--server/models/redundancy/video-redundancy.ts11
-rw-r--r--server/models/server/plugin.ts12
-rw-r--r--server/models/server/server-blocklist.ts8
-rw-r--r--server/models/server/server.ts17
-rw-r--r--server/models/video/schedule-video-update.ts3
-rw-r--r--server/models/video/tag.ts15
-rw-r--r--server/models/video/video-abuse.ts27
-rw-r--r--server/models/video/video-blacklist.ts12
-rw-r--r--server/models/video/video-caption.ts16
-rw-r--r--server/models/video/video-change-ownership.ts8
-rw-r--r--server/models/video/video-channel.ts69
-rw-r--r--server/models/video/video-comment.ts87
-rw-r--r--server/models/video/video-file.ts3
-rw-r--r--server/models/video/video-format-utils.ts17
-rw-r--r--server/models/video/video-import.ts14
-rw-r--r--server/models/video/video-playlist-element.ts34
-rw-r--r--server/models/video/video-playlist.ts23
-rw-r--r--server/models/video/video-share.ts14
-rw-r--r--server/models/video/video-streaming-playlist.ts12
-rw-r--r--server/models/video/video.ts149
-rw-r--r--server/tests/api/activitypub/helpers.ts13
-rw-r--r--server/tests/api/check-params/config.ts35
-rw-r--r--server/tests/api/check-params/user-notifications.ts3
-rw-r--r--server/tests/api/check-params/users.ts20
-rw-r--r--server/tests/api/notifications/user-notifications.ts73
-rw-r--r--server/tests/api/search/search-videos.ts8
-rw-r--r--server/tests/api/server/auto-follows.ts211
-rw-r--r--server/tests/api/server/config.ts66
-rw-r--r--server/tests/api/server/index.ts1
-rw-r--r--server/tests/api/users/users.ts21
-rw-r--r--server/tests/api/videos/video-abuse.ts77
-rw-r--r--server/tests/api/videos/video-change-ownership.ts2
-rw-r--r--server/tools/cli.ts33
-rw-r--r--server/tools/peertube-import-videos.ts85
-rw-r--r--server/typings/activitypub-processor.model.ts7
-rw-r--r--server/typings/express.ts124
-rw-r--r--server/typings/models/account/account-blocklist.ts25
-rw-r--r--server/typings/models/account/account.ts95
-rw-r--r--server/typings/models/account/actor-follow.ts63
-rw-r--r--server/typings/models/account/actor.ts121
-rw-r--r--server/typings/models/account/avatar.ts11
-rw-r--r--server/typings/models/account/index.d.ts5
-rw-r--r--server/typings/models/actor-follow.ts8
-rw-r--r--server/typings/models/actor.ts22
-rw-r--r--server/typings/models/index.d.ts6
-rw-r--r--server/typings/models/oauth/index.d.ts2
-rw-r--r--server/typings/models/oauth/oauth-client.ts3
-rw-r--r--server/typings/models/oauth/oauth-token.ts13
-rw-r--r--server/typings/models/server/index.d.ts3
-rw-r--r--server/typings/models/server/plugin.ts10
-rw-r--r--server/typings/models/server/server-blocklist.ts23
-rw-r--r--server/typings/models/server/server.ts24
-rw-r--r--server/typings/models/user/index.d.ts4
-rw-r--r--server/typings/models/user/user-notification-setting.ts9
-rw-r--r--server/typings/models/user/user-notification.ts78
-rw-r--r--server/typings/models/user/user-video-history.ts5
-rw-r--r--server/typings/models/user/user.ts70
-rw-r--r--server/typings/models/video-share.ts3
-rw-r--r--server/typings/models/video/index.d.ts18
-rw-r--r--server/typings/models/video/schedule-video-update.ts9
-rw-r--r--server/typings/models/video/tag.ts3
-rw-r--r--server/typings/models/video/thumbnail.ts3
-rw-r--r--server/typings/models/video/video-abuse.ts31
-rw-r--r--server/typings/models/video/video-blacklist.ts27
-rw-r--r--server/typings/models/video/video-caption.ts24
-rw-r--r--server/typings/models/video/video-change-ownership.ts23
-rw-r--r--server/typings/models/video/video-channels.ts126
-rw-r--r--server/typings/models/video/video-comment.ts57
-rw-r--r--server/typings/models/video/video-file.ts19
-rw-r--r--server/typings/models/video/video-import.ts31
-rw-r--r--server/typings/models/video/video-playlist-element.ts34
-rw-r--r--server/typings/models/video/video-playlist.ts92
-rw-r--r--server/typings/models/video/video-rate.ts23
-rw-r--r--server/typings/models/video/video-redundancy.ts38
-rw-r--r--server/typings/models/video/video-share.ts17
-rw-r--r--server/typings/models/video/video-streaming-playlist.ts19
-rw-r--r--server/typings/models/video/video.ts173
-rw-r--r--server/typings/utils.ts21
-rw-r--r--shared/extra-utils/index.ts1
-rw-r--r--shared/extra-utils/instances-index/mock-instances-index.ts38
-rw-r--r--shared/extra-utils/server/config.ts29
-rw-r--r--shared/extra-utils/users/user-notifications.ts41
-rw-r--r--shared/extra-utils/users/users.ts29
-rw-r--r--shared/models/activitypub/activity.ts2
-rw-r--r--shared/models/activitypub/objects/video-abuse-object.ts2
-rw-r--r--shared/models/i18n/i18n.ts4
-rw-r--r--shared/models/server/about.model.ts12
-rw-r--r--shared/models/server/custom-config.model.ts24
-rw-r--r--shared/models/users/user-notification-setting.model.ts1
-rw-r--r--shared/models/users/user-notification.model.ts8
-rw-r--r--shared/models/users/user-update-me.model.ts3
-rw-r--r--shared/models/users/user.model.ts10
-rw-r--r--support/doc/api/openapi.yaml346
-rw-r--r--support/doc/tools.md26
-rw-r--r--tsconfig.json8
-rw-r--r--yarn.lock129
301 files changed, 6573 insertions, 2126 deletions
diff --git a/CREDITS.md b/CREDITS.md
index 1b8ef7370..09719974d 100644
--- a/CREDITS.md
+++ b/CREDITS.md
@@ -1,4 +1,4 @@
1# Code 1# Code contributors
2 2
3 * [Chocobozzz](https://github.com/Chocobozzz) 3 * [Chocobozzz](https://github.com/Chocobozzz)
4 * [rigelk](https://github.com/rigelk) 4 * [rigelk](https://github.com/rigelk)
@@ -8,10 +8,12 @@
8 * [Jorropo](https://github.com/Jorropo) 8 * [Jorropo](https://github.com/Jorropo)
9 * [buoyantair](https://github.com/buoyantair) 9 * [buoyantair](https://github.com/buoyantair)
10 * [bnjbvr](https://github.com/bnjbvr) 10 * [bnjbvr](https://github.com/bnjbvr)
11 * [frankstrater](https://github.com/frankstrater)
11 * [jankeromnes](https://github.com/jankeromnes) 12 * [jankeromnes](https://github.com/jankeromnes)
12 * [lucas-dclrcq](https://github.com/lucas-dclrcq) 13 * [lucas-dclrcq](https://github.com/lucas-dclrcq)
13 * [DavidLibeau](https://github.com/DavidLibeau)
14 * [JohnXLivingston](https://github.com/JohnXLivingston) 14 * [JohnXLivingston](https://github.com/JohnXLivingston)
15 * [DavidLibeau](https://github.com/DavidLibeau)
16 * [fflorent](https://github.com/fflorent)
15 * [kaiyou](https://github.com/kaiyou) 17 * [kaiyou](https://github.com/kaiyou)
16 * [ldidry](https://github.com/ldidry) 18 * [ldidry](https://github.com/ldidry)
17 * [McFlat](https://github.com/McFlat) 19 * [McFlat](https://github.com/McFlat)
@@ -21,25 +23,25 @@
21 * [NassimBounouas](https://github.com/NassimBounouas) 23 * [NassimBounouas](https://github.com/NassimBounouas)
22 * [thomaskuntzz](https://github.com/thomaskuntzz) 24 * [thomaskuntzz](https://github.com/thomaskuntzz)
23 * [rezonant](https://github.com/rezonant) 25 * [rezonant](https://github.com/rezonant)
26 * [Wirebrass](https://github.com/Wirebrass)
24 * [clementbrizard](https://github.com/clementbrizard) 27 * [clementbrizard](https://github.com/clementbrizard)
25 * [LecygneNoir](https://github.com/LecygneNoir) 28 * [LecygneNoir](https://github.com/LecygneNoir)
26 * [okhin](https://github.com/okhin) 29 * [okhin](https://github.com/okhin)
27 * [daftaupe](https://github.com/daftaupe) 30 * [daftaupe](https://github.com/daftaupe)
28 * [tcitworld](https://github.com/tcitworld) 31 * [tcitworld](https://github.com/tcitworld)
29 * [fflorent](https://github.com/fflorent)
30 * [dedesite](https://github.com/dedesite) 32 * [dedesite](https://github.com/dedesite)
31 * [Nautigsam](https://github.com/Nautigsam) 33 * [Nautigsam](https://github.com/Nautigsam)
32 * [scanlime](https://github.com/scanlime) 34 * [scanlime](https://github.com/scanlime)
33 * [am97](https://github.com/am97) 35 * [am97](https://github.com/am97)
34 * [dadall](https://github.com/dadall) 36 * [dadall](https://github.com/dadall)
35 * [jonathanraes](https://github.com/jonathanraes) 37 * [jonathanraes](https://github.com/jonathanraes)
36 * [Wirebrass](https://github.com/Wirebrass)
37 * [yohanboniface](https://github.com/yohanboniface) 38 * [yohanboniface](https://github.com/yohanboniface)
38 * [anoadragon453](https://github.com/anoadragon453) 39 * [anoadragon453](https://github.com/anoadragon453)
39 * [auberanger](https://github.com/auberanger) 40 * [auberanger](https://github.com/auberanger)
40 * [darnuria](https://github.com/darnuria) 41 * [darnuria](https://github.com/darnuria)
41 * [rhaamo](https://github.com/rhaamo) 42 * [rhaamo](https://github.com/rhaamo)
42 * [mrflos](https://github.com/mrflos) 43 * [mrflos](https://github.com/mrflos)
44 * [Yetangitu](https://github.com/Yetangitu)
43 * [jocelynj](https://github.com/jocelynj) 45 * [jocelynj](https://github.com/jocelynj)
44 * [lucaspontoexe](https://github.com/lucaspontoexe) 46 * [lucaspontoexe](https://github.com/lucaspontoexe)
45 * [flyingrub](https://github.com/flyingrub) 47 * [flyingrub](https://github.com/flyingrub)
@@ -57,6 +59,7 @@
57 * [Anton-Latukha](https://github.com/Anton-Latukha) 59 * [Anton-Latukha](https://github.com/Anton-Latukha)
58 * [noplanman](https://github.com/noplanman) 60 * [noplanman](https://github.com/noplanman)
59 * [austinheap](https://github.com/austinheap) 61 * [austinheap](https://github.com/austinheap)
62 * [BO41](https://github.com/BO41)
60 * [benabbottnz](https://github.com/benabbottnz) 63 * [benabbottnz](https://github.com/benabbottnz)
61 * [ewft](https://github.com/ewft) 64 * [ewft](https://github.com/ewft)
62 * [bradsk88](https://github.com/bradsk88) 65 * [bradsk88](https://github.com/bradsk88)
@@ -67,7 +70,6 @@
67 * [ebrehault](https://github.com/ebrehault) 70 * [ebrehault](https://github.com/ebrehault)
68 * [DatBewar](https://github.com/DatBewar) 71 * [DatBewar](https://github.com/DatBewar)
69 * [ReK2Fernandez](https://github.com/ReK2Fernandez) 72 * [ReK2Fernandez](https://github.com/ReK2Fernandez)
70 * [Yetangitu](https://github.com/Yetangitu)
71 * [grizio](https://github.com/grizio) 73 * [grizio](https://github.com/grizio)
72 * [Glandos](https://github.com/Glandos) 74 * [Glandos](https://github.com/Glandos)
73 * [lanodan](https://github.com/lanodan) 75 * [lanodan](https://github.com/lanodan)
@@ -80,6 +82,7 @@
80 * [pichouk](https://github.com/pichouk) 82 * [pichouk](https://github.com/pichouk)
81 * [LeoMouyna](https://github.com/LeoMouyna) 83 * [LeoMouyna](https://github.com/LeoMouyna)
82 * [LiPeK](https://github.com/LiPeK) 84 * [LiPeK](https://github.com/LiPeK)
85 * [Findus23](https://github.com/Findus23)
83 * [zapashcanon](https://github.com/zapashcanon) 86 * [zapashcanon](https://github.com/zapashcanon)
84 * [mart-e](https://github.com/mart-e) 87 * [mart-e](https://github.com/mart-e)
85 * [0mp](https://github.com/0mp) 88 * [0mp](https://github.com/0mp)
@@ -95,6 +98,7 @@
95 * [quentinDupont](https://github.com/quentinDupont) 98 * [quentinDupont](https://github.com/quentinDupont)
96 * [Quenty31](https://github.com/Quenty31) 99 * [Quenty31](https://github.com/Quenty31)
97 * [sundowndev](https://github.com/sundowndev) 100 * [sundowndev](https://github.com/sundowndev)
101 * [robinkooli](https://github.com/robinkooli)
98 * [sesn](https://github.com/sesn) 102 * [sesn](https://github.com/sesn)
99 * [ALSai](https://github.com/ALSai) 103 * [ALSai](https://github.com/ALSai)
100 * [Simounet](https://github.com/Simounet) 104 * [Simounet](https://github.com/Simounet)
@@ -103,14 +107,13 @@
103 * [FrozenDroid](https://github.com/FrozenDroid) 107 * [FrozenDroid](https://github.com/FrozenDroid)
104 * [fallen](https://github.com/fallen) 108 * [fallen](https://github.com/fallen)
105 * [melongbob](https://github.com/melongbob) 109 * [melongbob](https://github.com/melongbob)
106 * [Zig-03](https://github.com/Zig-03)
107 * [anmol26s](https://github.com/anmol26s) 110 * [anmol26s](https://github.com/anmol26s)
108 * [imbsky](https://github.com/imbsky) 111 * [imbsky](https://github.com/imbsky)
109 * [ctlaltdefeat](https://github.com/ctlaltdefeat) 112 * [ctlaltdefeat](https://github.com/ctlaltdefeat)
110 * [jomo](https://github.com/jomo) 113 * [jomo](https://github.com/jomo)
111 * [libertysoft3](https://github.com/libertysoft3) 114 * [libertysoft3](https://github.com/libertysoft3)
112 * [lsde](https://github.com/lsde) 115 * [lsde](https://github.com/lsde)
113 * [memoryboxes](https://github.com/memoryboxes) 116 * [brain-zhang](https://github.com/brain-zhang)
114 * [norrist](https://github.com/norrist) 117 * [norrist](https://github.com/norrist)
115 * [osauzet](https://github.com/osauzet) 118 * [osauzet](https://github.com/osauzet)
116 * [SansPseudoFix](https://github.com/SansPseudoFix) 119 * [SansPseudoFix](https://github.com/SansPseudoFix)
@@ -121,7 +124,7 @@
121 * [ewasion](https://github.com/ewasion) 124 * [ewasion](https://github.com/ewasion)
122 125
123 126
124# Translations 127# Translation contributors
125 128
126 * [abdhessuk](https://trad.framasoft.org/zanata/profile/view/abdhessuk) 129 * [abdhessuk](https://trad.framasoft.org/zanata/profile/view/abdhessuk)
127 * [abidin24](https://trad.framasoft.org/zanata/profile/view/abidin24) 130 * [abidin24](https://trad.framasoft.org/zanata/profile/view/abidin24)
@@ -196,6 +199,7 @@
196 * [abidin24](https://trad.framasoft.org/zanata/profile/view/abidin24) 199 * [abidin24](https://trad.framasoft.org/zanata/profile/view/abidin24)
197 * [aditoo](https://trad.framasoft.org/zanata/profile/view/aditoo) 200 * [aditoo](https://trad.framasoft.org/zanata/profile/view/aditoo)
198 * [alidemirtas](https://trad.framasoft.org/zanata/profile/view/alidemirtas) 201 * [alidemirtas](https://trad.framasoft.org/zanata/profile/view/alidemirtas)
202 * [anastasia](https://trad.framasoft.org/zanata/profile/view/anastasia)
199 * [ariasuni](https://trad.framasoft.org/zanata/profile/view/ariasuni) 203 * [ariasuni](https://trad.framasoft.org/zanata/profile/view/ariasuni)
200 * [autom](https://trad.framasoft.org/zanata/profile/view/autom) 204 * [autom](https://trad.framasoft.org/zanata/profile/view/autom)
201 * [balaji](https://trad.framasoft.org/zanata/profile/view/balaji) 205 * [balaji](https://trad.framasoft.org/zanata/profile/view/balaji)
@@ -203,13 +207,16 @@
203 * [bristow](https://trad.framasoft.org/zanata/profile/view/bristow) 207 * [bristow](https://trad.framasoft.org/zanata/profile/view/bristow)
204 * [butterflyoffire](https://trad.framasoft.org/zanata/profile/view/butterflyoffire) 208 * [butterflyoffire](https://trad.framasoft.org/zanata/profile/view/butterflyoffire)
205 * [c0dr](https://trad.framasoft.org/zanata/profile/view/c0dr) 209 * [c0dr](https://trad.framasoft.org/zanata/profile/view/c0dr)
210 * [canony](https://trad.framasoft.org/zanata/profile/view/canony)
206 * [cat](https://trad.framasoft.org/zanata/profile/view/cat) 211 * [cat](https://trad.framasoft.org/zanata/profile/view/cat)
207 * [chocobozzz](https://trad.framasoft.org/zanata/profile/view/chocobozzz) 212 * [chocobozzz](https://trad.framasoft.org/zanata/profile/view/chocobozzz)
208 * [clerie](https://trad.framasoft.org/zanata/profile/view/clerie) 213 * [clerie](https://trad.framasoft.org/zanata/profile/view/clerie)
209 * [curupira](https://trad.framasoft.org/zanata/profile/view/curupira) 214 * [curupira](https://trad.framasoft.org/zanata/profile/view/curupira)
210 * [dhsets](https://trad.framasoft.org/zanata/profile/view/dhsets) 215 * [dhsets](https://trad.framasoft.org/zanata/profile/view/dhsets)
216 * [dibek](https://trad.framasoft.org/zanata/profile/view/dibek)
211 * [digitalkiller](https://trad.framasoft.org/zanata/profile/view/digitalkiller) 217 * [digitalkiller](https://trad.framasoft.org/zanata/profile/view/digitalkiller)
212 * [dwsage](https://trad.framasoft.org/zanata/profile/view/dwsage) 218 * [dwsage](https://trad.framasoft.org/zanata/profile/view/dwsage)
219 * [fkohrt](https://trad.framasoft.org/zanata/profile/view/fkohrt)
213 * [flauta](https://trad.framasoft.org/zanata/profile/view/flauta) 220 * [flauta](https://trad.framasoft.org/zanata/profile/view/flauta)
214 * [frankstrater](https://trad.framasoft.org/zanata/profile/view/frankstrater) 221 * [frankstrater](https://trad.framasoft.org/zanata/profile/view/frankstrater)
215 * [gillux](https://trad.framasoft.org/zanata/profile/view/gillux) 222 * [gillux](https://trad.framasoft.org/zanata/profile/view/gillux)
@@ -220,17 +227,25 @@
220 * [jhertel](https://trad.framasoft.org/zanata/profile/view/jhertel) 227 * [jhertel](https://trad.framasoft.org/zanata/profile/view/jhertel)
221 * [joss2lyon](https://trad.framasoft.org/zanata/profile/view/joss2lyon) 228 * [joss2lyon](https://trad.framasoft.org/zanata/profile/view/joss2lyon)
222 * [kekkotranslates](https://trad.framasoft.org/zanata/profile/view/kekkotranslates) 229 * [kekkotranslates](https://trad.framasoft.org/zanata/profile/view/kekkotranslates)
230 * [kingu](https://trad.framasoft.org/zanata/profile/view/kingu)
223 * [kittybecca](https://trad.framasoft.org/zanata/profile/view/kittybecca) 231 * [kittybecca](https://trad.framasoft.org/zanata/profile/view/kittybecca)
232 * [kousha](https://trad.framasoft.org/zanata/profile/view/kousha)
224 * [krkk](https://trad.framasoft.org/zanata/profile/view/krkk) 233 * [krkk](https://trad.framasoft.org/zanata/profile/view/krkk)
234 * [lapor](https://trad.framasoft.org/zanata/profile/view/lapor)
225 * [laufor](https://trad.framasoft.org/zanata/profile/view/laufor) 235 * [laufor](https://trad.framasoft.org/zanata/profile/view/laufor)
226 * [leeroyepold48](https://trad.framasoft.org/zanata/profile/view/leeroyepold48) 236 * [leeroyepold48](https://trad.framasoft.org/zanata/profile/view/leeroyepold48)
227 * [lstamellos](https://trad.framasoft.org/zanata/profile/view/lstamellos) 237 * [lstamellos](https://trad.framasoft.org/zanata/profile/view/lstamellos)
228 * [mablr](https://trad.framasoft.org/zanata/profile/view/mablr) 238 * [mablr](https://trad.framasoft.org/zanata/profile/view/mablr)
229 * [marcinmalecki](https://trad.framasoft.org/zanata/profile/view/marcinmalecki) 239 * [marcinmalecki](https://trad.framasoft.org/zanata/profile/view/marcinmalecki)
230 * [matograine](https://trad.framasoft.org/zanata/profile/view/matograine) 240 * [matograine](https://trad.framasoft.org/zanata/profile/view/matograine)
241 * [mayana](https://trad.framasoft.org/zanata/profile/view/mayana)
231 * [mikeorlov](https://trad.framasoft.org/zanata/profile/view/mikeorlov) 242 * [mikeorlov](https://trad.framasoft.org/zanata/profile/view/mikeorlov)
232 * [nin](https://trad.framasoft.org/zanata/profile/view/nin) 243 * [nin](https://trad.framasoft.org/zanata/profile/view/nin)
244 * [noncommutativegeo](https://trad.framasoft.org/zanata/profile/view/noncommutativegeo)
233 * [norbipeti](https://trad.framasoft.org/zanata/profile/view/norbipeti) 245 * [norbipeti](https://trad.framasoft.org/zanata/profile/view/norbipeti)
246 * [nvivant](https://trad.framasoft.org/zanata/profile/view/nvivant)
247 * [osoitz](https://trad.framasoft.org/zanata/profile/view/osoitz)
248 * [ppnplus](https://trad.framasoft.org/zanata/profile/view/ppnplus)
234 * [predatorix](https://trad.framasoft.org/zanata/profile/view/predatorix) 249 * [predatorix](https://trad.framasoft.org/zanata/profile/view/predatorix)
235 * [quentin](https://trad.framasoft.org/zanata/profile/view/quentin) 250 * [quentin](https://trad.framasoft.org/zanata/profile/view/quentin)
236 * [quentind](https://trad.framasoft.org/zanata/profile/view/quentind) 251 * [quentind](https://trad.framasoft.org/zanata/profile/view/quentind)
@@ -238,11 +253,14 @@
238 * [robin](https://trad.framasoft.org/zanata/profile/view/robin) 253 * [robin](https://trad.framasoft.org/zanata/profile/view/robin)
239 * [rond](https://trad.framasoft.org/zanata/profile/view/rond) 254 * [rond](https://trad.framasoft.org/zanata/profile/view/rond)
240 * [s8321414](https://trad.framasoft.org/zanata/profile/view/s8321414) 255 * [s8321414](https://trad.framasoft.org/zanata/profile/view/s8321414)
256 * [sato_ss](https://trad.framasoft.org/zanata/profile/view/sato_ss)
241 * [secreet](https://trad.framasoft.org/zanata/profile/view/secreet) 257 * [secreet](https://trad.framasoft.org/zanata/profile/view/secreet)
258 * [sercom_kc](https://trad.framasoft.org/zanata/profile/view/sercom_kc)
242 * [severo](https://trad.framasoft.org/zanata/profile/view/severo) 259 * [severo](https://trad.framasoft.org/zanata/profile/view/severo)
243 * [silkevicious](https://trad.framasoft.org/zanata/profile/view/silkevicious) 260 * [silkevicious](https://trad.framasoft.org/zanata/profile/view/silkevicious)
244 * [sporiff](https://trad.framasoft.org/zanata/profile/view/sporiff) 261 * [sporiff](https://trad.framasoft.org/zanata/profile/view/sporiff)
245 * [tekuteku](https://trad.framasoft.org/zanata/profile/view/tekuteku) 262 * [tekuteku](https://trad.framasoft.org/zanata/profile/view/tekuteku)
263 * [thecatjustmeow](https://trad.framasoft.org/zanata/profile/view/thecatjustmeow)
246 * [tirifto](https://trad.framasoft.org/zanata/profile/view/tirifto) 264 * [tirifto](https://trad.framasoft.org/zanata/profile/view/tirifto)
247 * [tmota](https://trad.framasoft.org/zanata/profile/view/tmota) 265 * [tmota](https://trad.framasoft.org/zanata/profile/view/tmota)
248 * [tuxayo](https://trad.framasoft.org/zanata/profile/view/tuxayo) 266 * [tuxayo](https://trad.framasoft.org/zanata/profile/view/tuxayo)
diff --git a/README.md b/README.md
index 29478b085..5ed7d5b4c 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@ Be part of a network of multiple small federated, interoperable video hosting pr
22 22
23<p align="center"> 23<p align="center">
24 <a href="https://framasoft.org"> 24 <a href="https://framasoft.org">
25 <img width="150px" src="http://lutim.cpy.re/Prd3ci7G.png" alt="Framasoft logo"/> 25 <img width="150px" src="https://lutim.cpy.re/FeRgHH8r.png" alt="Framasoft logo"/>
26 </a> 26 </a>
27</p> 27</p>
28 28
@@ -182,7 +182,7 @@ See the [architecture blueprint](https://docs.joinpeertube.org/#/contribute-arch
182 182
183See our REST API documentation: 183See our REST API documentation:
184 * OpenAPI 3.0.0 schema: [/support/doc/api/openapi.yaml](/support/doc/api/openapi.yaml) 184 * OpenAPI 3.0.0 schema: [/support/doc/api/openapi.yaml](/support/doc/api/openapi.yaml)
185 * Spec explorer: [docs.joinpeertube.org/#/api-rest-reference.html](https://docs.joinpeertube.org/#/api-rest-reference.html) 185 * Spec explorer: [docs.joinpeertube.org/api-rest-reference.html](https://docs.joinpeertube.org/api-rest-reference.html)
186 186
187See our [ActivityPub documentation](https://docs.joinpeertube.org/#/api-activitypub). 187See our [ActivityPub documentation](https://docs.joinpeertube.org/#/api-activitypub).
188 188
diff --git a/client/package.json b/client/package.json
index ac881d64c..ddb3eb06a 100644
--- a/client/package.json
+++ b/client/package.json
@@ -100,7 +100,7 @@
100 "ngx-pipes": "^2.1.7", 100 "ngx-pipes": "^2.1.7",
101 "node-sass": "^4.9.3", 101 "node-sass": "^4.9.3",
102 "npm-font-source-sans-pro": "^1.0.2", 102 "npm-font-source-sans-pro": "^1.0.2",
103 "p2p-media-loader-hlsjs": "^0.6.1", 103 "p2p-media-loader-hlsjs": "^0.6.2",
104 "path-browserify": "^1.0.0", 104 "path-browserify": "^1.0.0",
105 "primeng": "^8.0.2", 105 "primeng": "^8.0.2",
106 "process": "^0.11.10", 106 "process": "^0.11.10",
diff --git a/client/src/app/+about/about-instance/about-instance.component.html b/client/src/app/+about/about-instance/about-instance.component.html
index 7c27ec760..25d416740 100644
--- a/client/src/app/+about/about-instance/about-instance.component.html
+++ b/client/src/app/+about/about-instance/about-instance.component.html
@@ -1,32 +1,97 @@
1<div class="row"> 1<div class="row">
2 <div class="col-md-12 col-xl-6"> 2 <div class="col-md-12 col-xl-6">
3
3 <div class="about-instance-title"> 4 <div class="about-instance-title">
4 <div i18n>About {{ instanceName }} instance</div> 5 <div i18n class="title">About {{ instanceName }} instance</div>
5 6
6 <div *ngIf="isContactFormEnabled" (click)="openContactModal()" i18n role="button" class="contact-admin">Contact administrator</div> 7 <div i18n *ngIf="isContactFormEnabled" (click)="openContactModal()" role="button" class="contact-admin">Contact administrator</div>
8 </div>
9
10 <div class="block instance-badges">
11 <span *ngFor="let category of categories" class="badge badge-primary category">{{ category }}</span>
12
13 <span *ngFor="let language of languages" class="badge badge-secondary language">{{ language }}</span>
7 </div> 14 </div>
8 15
9 <div class="short-description"> 16 <div class="short-description">
10 <div>{{ shortDescription }}</div> 17 <div class="block short-description">{{ shortDescription }}</div>
18
19 <div i18n *ngIf="isNSFW" class="block dedicated-to-nsfw">This instance is dedicated to sensitive/NSFW content.</div>
20 </div>
21
22 <div i18n class="middle-title" *ngIf="html.administrator || maintenanceLifetime || businessModel">
23 Administrators & sustainability
24 </div>
25
26 <div class="block administrator" *ngIf="html.administrator">
27 <div i18n class="section-title">Who we are</div>
11 28
12 <div *ngIf="isNSFW" class="dedicated-to-nsfw">This instance is dedicated to sensitive/NSFW content.</div> 29 <div [innerHTML]="html.administrator"></div>
13 </div> 30 </div>
14 31
15 <div class="description"> 32 <div class="block creation-reason" *ngIf="creationReason">
33 <div i18n class="section-title">Why we created this instance</div>
34
35 <p>{{ creationReason }}</p>
36 </div>
37
38 <div class="block maintenance-lifetime" *ngIf="maintenanceLifetime">
39 <div i18n class="section-title">How long we plan to maintain this instance</div>
40
41 <p>{{ maintenanceLifetime }}</p>
42 </div>
43
44 <div class="block business-model" *ngIf="businessModel">
45 <div i18n class="section-title">How we will pay this instance</div>
46
47 <p>{{ businessModel }}</p>
48 </div>
49
50 <div i18n class="middle-title" *ngIf="html.description">
51 Information
52 </div>
53
54 <div class="block description">
16 <div i18n class="section-title">Description</div> 55 <div i18n class="section-title">Description</div>
17 56
18 <div [innerHTML]="descriptionHTML"></div> 57 <div [innerHTML]="html.description"></div>
58 </div>
59
60 <div i18n class="middle-title" *ngIf="html.moderationInformation || html.codeOfConduct || html.terms">
61 Moderation
62 </div>
63
64 <div class="block moderation-information" *ngIf="html.moderationInformation">
65 <div i18n class="section-title">Moderation information</div>
66
67 <div [innerHTML]="html.moderationInformation"></div>
19 </div> 68 </div>
20 69
21 <div class="terms" id="terms-section"> 70 <div class="block code-of-conduct" *ngIf="html.codeOfConduct">
71 <div i18n class="section-title">Code of conduct</div>
72
73 <div [innerHTML]="html.codeOfConduct"></div>
74 </div>
75
76 <div class="block terms">
22 <div i18n class="section-title">Terms</div> 77 <div i18n class="section-title">Terms</div>
23 78
24 <div [innerHTML]="termsHTML"></div> 79 <div [innerHTML]="html.terms"></div>
80 </div>
81
82 <div i18n class="middle-title" *ngIf="html.hardwareInformation">
83 Other information
84 </div>
85
86 <div class="block hardware-information">
87 <div i18n class="section-title">Hardware information</div>
88
89 <div [innerHTML]="html.hardwareInformation"></div>
25 </div> 90 </div>
26 </div> 91 </div>
27 92
28 <div class="col-md-12 col-xl-6"> 93 <div class="col-md-12 col-xl-6">
29 <label>Features found on this instance</label> 94 <label i18n>Features found on this instance</label>
30 <my-instance-features-table></my-instance-features-table> 95 <my-instance-features-table></my-instance-features-table>
31 </div> 96 </div>
32</div> 97</div>
diff --git a/client/src/app/+about/about-instance/about-instance.component.scss b/client/src/app/+about/about-instance/about-instance.component.scss
index 0296ae8e9..909ae5c21 100644
--- a/client/src/app/+about/about-instance/about-instance.component.scss
+++ b/client/src/app/+about/about-instance/about-instance.component.scss
@@ -5,13 +5,12 @@
5 display: flex; 5 display: flex;
6 justify-content: space-between; 6 justify-content: space-between;
7 7
8 & > div { 8 .title {
9 font-size: 20px; 9 font-size: 20px;
10 font-weight: bold; 10 font-weight: $font-semibold;
11 margin-bottom: 15px;
12 } 11 }
13 12
14 & > .contact-admin { 13 .contact-admin {
15 @include peertube-button; 14 @include peertube-button;
16 @include orange-button; 15 @include orange-button;
17 16
@@ -19,14 +18,38 @@
19 } 18 }
20} 19}
21 20
21.instance-badges {
22 font-size: 16px;
23
24 .badge {
25 font-size: 12px;
26 font-weight: $font-semibold;
27 margin-right: 5px;
28
29 &.category {
30 background-color: var(--mainColor);
31 }
32 }
33}
34
22.section-title { 35.section-title {
23 font-weight: $font-semibold; 36 font-weight: $font-semibold;
24 font-size: 20px; 37 font-size: 16px;
25 margin-bottom: 5px; 38 margin-bottom: 5px;
39 display: flex;
40 align-items: center;
41}
42
43.middle-title {
44 @include in-content-small-title;
45
46 margin-top: 45px;
47 margin-bottom: 25px;
26} 48}
27 49
28.short-description, .description, .terms, .signup { 50.block {
29 margin-bottom: 30px; 51 margin-bottom: 30px;
52 font-size: 15px;
30} 53}
31 54
32.short-description .dedicated-to-nsfw { 55.short-description .dedicated-to-nsfw {
diff --git a/client/src/app/+about/about-instance/about-instance.component.ts b/client/src/app/+about/about-instance/about-instance.component.ts
index a5204de27..16ccae2e2 100644
--- a/client/src/app/+about/about-instance/about-instance.component.ts
+++ b/client/src/app/+about/about-instance/about-instance.component.ts
@@ -4,6 +4,8 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
4import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component' 4import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component'
5import { InstanceService } from '@app/shared/instance/instance.service' 5import { InstanceService } from '@app/shared/instance/instance.service'
6import { MarkdownService } from '@app/shared/renderer' 6import { MarkdownService } from '@app/shared/renderer'
7import { forkJoin } from 'rxjs'
8import { first } from 'rxjs/operators'
7 9
8@Component({ 10@Component({
9 selector: 'my-about-instance', 11 selector: 'my-about-instance',
@@ -14,8 +16,22 @@ export class AboutInstanceComponent implements OnInit {
14 @ViewChild('contactAdminModal', { static: true }) contactAdminModal: ContactAdminModalComponent 16 @ViewChild('contactAdminModal', { static: true }) contactAdminModal: ContactAdminModalComponent
15 17
16 shortDescription = '' 18 shortDescription = ''
17 descriptionHTML = '' 19
18 termsHTML = '' 20 html = {
21 description: '',
22 terms: '',
23 codeOfConduct: '',
24 moderationInformation: '',
25 administrator: '',
26 hardwareInformation: ''
27 }
28
29 creationReason = ''
30 maintenanceLifetime = ''
31 businessModel = ''
32
33 languages: string[] = []
34 categories: string[] = []
19 35
20 constructor ( 36 constructor (
21 private notifier: Notifier, 37 private notifier: Notifier,
@@ -38,21 +54,30 @@ export class AboutInstanceComponent implements OnInit {
38 } 54 }
39 55
40 ngOnInit () { 56 ngOnInit () {
41 this.instanceService.getAbout() 57 forkJoin([
42 .subscribe( 58 this.instanceService.getAbout(),
43 async res => { 59 this.serverService.localeObservable.pipe(first()),
44 this.shortDescription = res.instance.shortDescription 60 this.serverService.videoLanguagesLoaded.pipe(first()),
61 this.serverService.videoCategoriesLoaded.pipe(first())
62 ]).subscribe(
63 async ([ about, translations ]) => {
64 this.shortDescription = about.instance.shortDescription
45 65
46 this.descriptionHTML = await this.markdownService.textMarkdownToHTML(res.instance.description) 66 this.creationReason = about.instance.creationReason
47 this.termsHTML = await this.markdownService.textMarkdownToHTML(res.instance.terms) 67 this.maintenanceLifetime = about.instance.maintenanceLifetime
48 }, 68 this.businessModel = about.instance.businessModel
49 69
50 () => this.notifier.error(this.i18n('Cannot get about information from server')) 70 this.html = await this.instanceService.buildHtml(about)
51 ) 71
72 this.languages = this.instanceService.buildTranslatedLanguages(about, translations)
73 this.categories = this.instanceService.buildTranslatedCategories(about, translations)
74 },
75
76 () => this.notifier.error(this.i18n('Cannot get about information from server'))
77 )
52 } 78 }
53 79
54 openContactModal () { 80 openContactModal () {
55 return this.contactAdminModal.show() 81 return this.contactAdminModal.show()
56 } 82 }
57
58} 83}
diff --git a/client/src/app/+about/about-peertube/about-peertube-contributors.component.html b/client/src/app/+about/about-peertube/about-peertube-contributors.component.html
new file mode 100644
index 000000000..997a6a3e1
--- /dev/null
+++ b/client/src/app/+about/about-peertube/about-peertube-contributors.component.html
@@ -0,0 +1,13 @@
1<h3 i18n class="section-title">Who made this software?</h3>
2
3<p align="center">
4 <strong>Developed with &#10084; by <a target="_blank" rel="noopener noreferrer" href="https://framasoft.org">Framasoft</a></strong>
5</p>
6
7<p align="center">
8 <a target="_blank" rel="noopener noreferrer" href="https://framasoft.org">
9 <img width="150px" src="/client/assets/images/framasoft.png" alt="Framasoft logo"/>
10 </a>
11</p>
12
13<div [innerHTML]="creditsHtml"></div>
diff --git a/client/src/app/+about/about-peertube/about-peertube-contributors.component.scss b/client/src/app/+about/about-peertube/about-peertube-contributors.component.scss
new file mode 100644
index 000000000..9c3b0a46b
--- /dev/null
+++ b/client/src/app/+about/about-peertube/about-peertube-contributors.component.scss
@@ -0,0 +1,15 @@
1@import '_variables';
2@import '_mixins';
3
4/deep/ h1 {
5 font-size: 1rem;
6}
7
8/deep/ ul {
9 padding: 0;
10
11 li {
12 display: inline-block;
13 margin-right: 10px;
14 }
15}
diff --git a/client/src/app/+about/about-peertube/about-peertube-contributors.component.ts b/client/src/app/+about/about-peertube/about-peertube-contributors.component.ts
new file mode 100644
index 000000000..fa2c0daa0
--- /dev/null
+++ b/client/src/app/+about/about-peertube/about-peertube-contributors.component.ts
@@ -0,0 +1,19 @@
1import { Component, OnInit } from '@angular/core'
2import { MarkdownService } from '@app/shared/renderer'
3
4@Component({
5 selector: 'my-about-peertube-contributors',
6 templateUrl: './about-peertube-contributors.component.html',
7 styleUrls: [ './about-peertube-contributors.component.scss' ]
8})
9export class AboutPeertubeContributorsComponent implements OnInit {
10 creditsHtml: string
11
12 private markdown = require('raw-loader!../../../../../CREDITS.md')
13
14 constructor (private markdownService: MarkdownService) { }
15
16 async ngOnInit () {
17 this.creditsHtml = await this.markdownService.completeMarkdownToHTML(this.markdown)
18 }
19}
diff --git a/client/src/app/+about/about-peertube/about-peertube.component.html b/client/src/app/+about/about-peertube/about-peertube.component.html
index d3fc9a828..423f7bce7 100644
--- a/client/src/app/+about/about-peertube/about-peertube.component.html
+++ b/client/src/app/+about/about-peertube/about-peertube.component.html
@@ -14,84 +14,89 @@
14 </p> 14 </p>
15</div> 15</div>
16 16
17<div id="p2p-privacy"> 17<div class="privacy-contributors">
18 <h3 i18n class="section-title">P2P & Privacy</h3> 18 <my-about-peertube-contributors></my-about-peertube-contributors>
19
20 <div class="p2p-privacy">
21 <h3 i18n class="section-title">P2P & Privacy</h3>
22
23 <p i18n>
24 PeerTube uses the BitTorrent protocol to share bandwidth between users.
25 This implies that your IP address is stored in the instance's BitTorrent tracker as long as you download or watch the video.
26 </p>
27
28 <h6 i18n class="p2p-privacy-title">What are the consequences?</h6>
29
30 <p i18n>
31 In theory, someone with enough technical skills could create a script that tracks which IP is downloading which video.
32 In practice, this is much more difficult because:
33 </p>
34
35 <ul>
36 <li i18n>
37 An HTTP request has to be sent on each tracker for each video to spy.
38 If we want to spy all PeerTube's videos, we have to send as many requests as there are videos (so potentially a lot)
39 </li>
40
41 <li i18n>
42 For each request sent, the tracker returns random peers at a limited number.
43 For instance, if there are 1000 peers in the swarm and the tracker sends only 20 peers for each request, there must be at least 50 requests sent to know every peers in the swarm
44 </li>
45
46 <li i18n>
47 Those requests have to be sent regularly to know who starts/stops watching a video. It is easy to detect that kind of behaviour
48 </li>
49
50 <li i18n>
51 If an IP address is stored in the tracker, it doesn't mean that the person behind the IP (if this person exists) has watched the video
52 </li>
53
54 <li i18n>
55 The IP address is a vague information : usually, it regularly changes and can represent many persons or entities
56 </li>
57
58 <li i18n>
59 Web peers are not publicly accessible: because we use WebRTC inside the web browser (<a href="https://webtorrent.io/">with the WebTorrent library</a>), the protocol is different from classic BitTorrent.
60 When you are in a web browser, you send a signal containing your IP address to the tracker that will randomly choose other peers to forward the information to.
61 See <a href="https://github.com/yciabaud/webtorrent/blob/beps/bep_webrtc.rst">this document</a> for more information
62 </li>
63 </ul>
64
65 <p i18n>
66 The worst-case scenario of an average person spying on their friends is quite unlikely.
67 There are much more effective ways to get that kind of information.
68 </p>
69
70 <h6 i18n class="p2p-privacy-title">How does PeerTube compare with YouTube?</h6>
71
72 <p i18n>
73 The threats to privacy in YouTube are different from PeerTube's.
74 In YouTube's case, the platform gathers a huge amount of your personal information (not only your IP) to analyze them and track you.
75 Moreover, YouTube is owned by Google/Alphabet, a company that tracks you across many websites (via AdSense or Google Analytics).
76 </p>
77
78 <h6 i18n class="p2p-privacy-title">What can I do to limit the exposure of my IP address?</h6>
79
80 <p i18n>
81 Your IP address is public so every time you consult a website, there is a number of actors (in addition to the final website) seeing your IP in their connection logs: ISP/routers/trackers/CDN and more.
82 PeerTube is transparent about it: we warn you that if you want to keep your IP private, you must use a VPN or Tor Browser.
83 Thinking that removing P2P from PeerTube will give you back anonymity doesn't make sense.
84 </p>
85
86 <h6 i18n class="p2p-privacy-title">What will be done to mitigate this problem?</h6>
87
88 <p i18n>
89 PeerTube is in its early stages, and want to deliver the best countermeasures possible by the time the stable is released.
90 In the meantime, we want to test different ideas related to this issue:
91 </p>
92
93 <ul>
94 <li i18n>Set a limit to the number of peers sent by the tracker</li>
95 <li i18n>Set a limit on the request frequency received by the tracker (being tested)</li>
96 <li i18n>Ring a bell if there are unusual requests (being tested)</li>
97 <li i18n>Disable P2P from the administration interface</li>
98 <li i18n>An automatic video redundancy program: we wouldn't know if the IP downloaded the video on purpose or if it was the automatized program</li>
99 </ul>
100 </div>
19 101
20 <p i18n>
21 PeerTube uses the BitTorrent protocol to share bandwidth between users.
22 This implies that your IP address is stored in the instance's BitTorrent tracker as long as you download or watch the video.
23 </p>
24
25 <h6 i18n class="p2p-privacy-title">What are the consequences?</h6>
26
27 <p i18n>
28 In theory, someone with enough technical skills could create a script that tracks which IP is downloading which video.
29 In practice, this is much more difficult because:
30 </p>
31
32 <ul>
33 <li i18n>
34 An HTTP request has to be sent on each tracker for each video to spy.
35 If we want to spy all PeerTube's videos, we have to send as many requests as there are videos (so potentially a lot)
36 </li>
37
38 <li i18n>
39 For each request sent, the tracker returns random peers at a limited number.
40 For instance, if there are 1000 peers in the swarm and the tracker sends only 20 peers for each request, there must be at least 50 requests sent to know every peers in the swarm
41 </li>
42
43 <li i18n>
44 Those requests have to be sent regularly to know who starts/stops watching a video. It is easy to detect that kind of behaviour
45 </li>
46
47 <li i18n>
48 If an IP address is stored in the tracker, it doesn't mean that the person behind the IP (if this person exists) has watched the video
49 </li>
50
51 <li i18n>
52 The IP address is a vague information : usually, it regularly changes and can represent many persons or entities
53 </li>
54
55 <li i18n>
56 Web peers are not publicly accessible: because we use WebRTC inside the web browser (<a href="https://webtorrent.io/">with the WebTorrent library</a>), the protocol is different from classic BitTorrent.
57 When you are in a web browser, you send a signal containing your IP address to the tracker that will randomly choose other peers to forward the information to.
58 See <a href="https://github.com/yciabaud/webtorrent/blob/beps/bep_webrtc.rst">this document</a> for more information
59 </li>
60 </ul>
61
62 <p i18n>
63 The worst-case scenario of an average person spying on their friends is quite unlikely.
64 There are much more effective ways to get that kind of information.
65 </p>
66
67 <h6 i18n class="p2p-privacy-title">How does PeerTube compare with YouTube?</h6>
68
69 <p i18n>
70 The threats to privacy in YouTube are different from PeerTube's.
71 In YouTube's case, the platform gathers a huge amount of your personal information (not only your IP) to analyze them and track you.
72 Moreover, YouTube is owned by Google/Alphabet, a company that tracks you across many websites (via AdSense or Google Analytics).
73 </p>
74
75 <h6 i18n class="p2p-privacy-title">What can I do to limit the exposure of my IP address?</h6>
76
77 <p i18n>
78 Your IP address is public so every time you consult a website, there is a number of actors (in addition to the final website) seeing your IP in their connection logs: ISP/routers/trackers/CDN and more.
79 PeerTube is transparent about it: we warn you that if you want to keep your IP private, you must use a VPN or Tor Browser.
80 Thinking that removing P2P from PeerTube will give you back anonymity doesn't make sense.
81 </p>
82
83 <h6 i18n class="p2p-privacy-title">What will be done to mitigate this problem?</h6>
84
85 <p i18n>
86 PeerTube is in its early stages, and want to deliver the best countermeasures possible by the time the stable is released.
87 In the meantime, we want to test different ideas related to this issue:
88 </p>
89
90 <ul>
91 <li i18n>Set a limit to the number of peers sent by the tracker</li>
92 <li i18n>Set a limit on the request frequency received by the tracker (being tested)</li>
93 <li i18n>Ring a bell if there are unusual requests (being tested)</li>
94 <li i18n>Disable P2P from the administration interface</li>
95 <li i18n>An automatic video redundancy program: we wouldn't know if the IP downloaded the video on purpose or if it was the automatized program</li>
96 </ul>
97</div> 102</div>
diff --git a/client/src/app/+about/about-peertube/about-peertube.component.scss b/client/src/app/+about/about-peertube/about-peertube.component.scss
index 0d2e2bb68..8fca53e90 100644
--- a/client/src/app/+about/about-peertube/about-peertube.component.scss
+++ b/client/src/app/+about/about-peertube/about-peertube.component.scss
@@ -2,12 +2,12 @@
2@import '_mixins'; 2@import '_mixins';
3 3
4.about-peertube-title { 4.about-peertube-title {
5 font-size: 25px; 5 font-size: 20px;
6 font-weight: bold; 6 font-weight: $font-semibold;
7 margin-bottom: 15px; 7 margin-bottom: 15px;
8} 8}
9 9
10.section-title { 10/deep/ .section-title {
11 font-weight: $font-semibold; 11 font-weight: $font-semibold;
12 font-size: 20px; 12 font-size: 20px;
13 margin-bottom: 5px; 13 margin-bottom: 5px;
@@ -17,6 +17,41 @@
17 margin-bottom: 30px; 17 margin-bottom: 30px;
18} 18}
19 19
20.description,
21.p2p-privacy,
22my-about-peertube-contributors {
23 /deep/ {
24 p, li {
25 font-size: 15px;
26 }
27 }
28}
29
20.p2p-privacy-title { 30.p2p-privacy-title {
21 margin-top: 15px; 31 margin-top: 15px;
22} \ No newline at end of file 32}
33
34.privacy-contributors {
35 display: flex;
36 flex-direction: row;
37
38 > div,
39 > my-about-peertube-contributors {
40 flex-basis: 100%;
41 display: block;
42 }
43
44 .p2p-privacy {
45 h6 {
46 font-size: 20px;
47 }
48 }
49
50 my-about-peertube-contributors {
51 margin: 0 40px 40px 0;
52 }
53
54 @media screen and (max-width: $small-view) {
55 flex-direction: column;
56 }
57}
diff --git a/client/src/app/+about/about.module.ts b/client/src/app/+about/about.module.ts
index 49a7a52f8..14bf76e55 100644
--- a/client/src/app/+about/about.module.ts
+++ b/client/src/app/+about/about.module.ts
@@ -1,5 +1,4 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2
3import { AboutRoutingModule } from './about-routing.module' 2import { AboutRoutingModule } from './about-routing.module'
4import { AboutComponent } from './about.component' 3import { AboutComponent } from './about.component'
5import { SharedModule } from '../shared' 4import { SharedModule } from '../shared'
@@ -7,6 +6,7 @@ import { AboutInstanceComponent } from '@app/+about/about-instance/about-instanc
7import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component' 6import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component'
8import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component' 7import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component'
9import { AboutFollowsComponent } from '@app/+about/about-follows/about-follows.component' 8import { AboutFollowsComponent } from '@app/+about/about-follows/about-follows.component'
9import { AboutPeertubeContributorsComponent } from '@app/+about/about-peertube/about-peertube-contributors.component'
10 10
11@NgModule({ 11@NgModule({
12 imports: [ 12 imports: [
@@ -19,6 +19,7 @@ import { AboutFollowsComponent } from '@app/+about/about-follows/about-follows.c
19 AboutInstanceComponent, 19 AboutInstanceComponent,
20 AboutPeertubeComponent, 20 AboutPeertubeComponent,
21 AboutFollowsComponent, 21 AboutFollowsComponent,
22 AboutPeertubeContributorsComponent,
22 ContactAdminModalComponent 23 ContactAdminModalComponent
23 ], 24 ],
24 25
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
index fe9d856d0..54115055a 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
@@ -2,12 +2,13 @@
2 2
3 <ngb-tabset class="root-tabset bootstrap"> 3 <ngb-tabset class="root-tabset bootstrap">
4 4
5 <ngb-tab i18n-title title="Basic configuration"> 5 <ngb-tab i18n-title title="Instance information">
6 <ng-template ngbTabContent> 6 <ng-template ngbTabContent>
7 7
8 <div i18n class="inner-form-title">Instance</div>
9
10 <ng-container formGroupName="instance"> 8 <ng-container formGroupName="instance">
9
10 <div i18n class="inner-form-title">Instance</div>
11
11 <div class="form-group"> 12 <div class="form-group">
12 <label i18n for="instanceName">Name</label> 13 <label i18n for="instanceName">Name</label>
13 <input 14 <input
@@ -20,7 +21,7 @@
20 <div class="form-group"> 21 <div class="form-group">
21 <label i18n for="instanceShortDescription">Short description</label> 22 <label i18n for="instanceShortDescription">Short description</label>
22 <textarea 23 <textarea
23 id="instanceShortDescription" formControlName="shortDescription" 24 id="instanceShortDescription" formControlName="shortDescription" class="small"
24 [ngClass]="{ 'input-error': formErrors['instance.shortDescription'] }" 25 [ngClass]="{ 'input-error': formErrors['instance.shortDescription'] }"
25 ></textarea> 26 ></textarea>
26 <div *ngIf="formErrors.instance.shortDescription" class="form-error">{{ formErrors.instance.shortDescription }}</div> 27 <div *ngIf="formErrors.instance.shortDescription" class="form-error">{{ formErrors.instance.shortDescription }}</div>
@@ -36,42 +37,56 @@
36 </div> 37 </div>
37 38
38 <div class="form-group"> 39 <div class="form-group">
39 <label i18n for="instanceTerms">Terms</label><my-help helpType="markdownText"></my-help> 40 <label i18n for="instanceCategories">Main instance categories</label>
40 <my-markdown-textarea 41
41 id="instanceTerms" formControlName="terms" textareaWidth="500px" [previewColumn]="true" 42 <div>
42 [ngClass]="{ 'input-error': formErrors['instance.terms'] }" 43 <p-multiSelect
43 ></my-markdown-textarea> 44 inputId="instanceCategories" [options]="categoryItems" formControlName="categories" showToggleAll="false"
44 <div *ngIf="formErrors.instance.terms" class="form-error">{{ formErrors.instance.terms }}</div> 45 [defaultLabel]="getDefaultCategoryLabel()" [selectedItemsLabel]="getSelectedCategoryLabel()"
46 emptyFilterMessage="No results found" i18n-emptyFilterMessage
47 ></p-multiSelect>
48 </div>
45 </div> 49 </div>
46 50
47 <div class="form-group"> 51 <div class="form-group">
48 <my-peertube-checkbox 52 <label i18n for="instanceLanguages">Main languages you/your moderators speak</label>
49 inputName="instanceIsNSFW" formControlName="isNSFW" 53
50 i18n-labelText labelText="Dedicated to sensitive or NSFW content" 54 <div>
51 i18n-helpHtml helpHtml="Enabling it will allow other administrators to know that you are mainly federating sensitive content.<br /><br /> 55 <p-multiSelect
52 Moreover, the NSFW checkbox on video upload will be automatically checked by default." 56 inputId="instanceLanguages" [options]="languageItems" formControlName="languages" showToggleAll="false"
53 ></my-peertube-checkbox> 57 [defaultLabel]="getDefaultLanguageLabel()" [selectedItemsLabel]="getSelectedLanguageLabel()"
58 emptyFilterMessage="No results found" i18n-emptyFilterMessage
59 ></p-multiSelect>
60 </div>
54 </div> 61 </div>
55 62
63 <div i18n class="inner-form-title">Moderation & NSFW</div>
64
56 <div class="form-group"> 65 <div class="form-group">
57 <label i18n for="instanceDefaultClientRoute">Default client route</label> 66 <my-peertube-checkbox inputName="instanceIsNSFW" formControlName="isNSFW">
58 <div class="peertube-select-container"> 67 <ng-template ptTemplate="label">
59 <select id="instanceDefaultClientRoute" formControlName="defaultClientRoute"> 68 <ng-container i18n>This instance is dedicated to sensitive or NSFW content</ng-container>
60 <option i18n value="/videos/overview">Videos Overview</option> 69 </ng-template>
61 <option i18n value="/videos/trending">Videos Trending</option> 70
62 <option i18n value="/videos/recently-added">Videos Recently Added</option> 71 <ng-template ptTemplate="help">
63 <option i18n value="/videos/local">Local videos</option> 72 <ng-container i18n>
64 </select> 73 Enabling it will allow other administrators to know that you are mainly federating sensitive content.<br /><br />
65 </div> 74 Moreover, the NSFW checkbox on video upload will be automatically checked by default.
66 <div *ngIf="formErrors.instance.defaultClientRoute" class="form-error">{{ formErrors.instance.defaultClientRoute }}</div> 75 </ng-container>
76 </ng-template>
77 </my-peertube-checkbox>
67 </div> 78 </div>
68 79
69 <div class="form-group"> 80 <div class="form-group">
70 <label i18n for="instanceDefaultNSFWPolicy">Policy on videos containing sensitive content</label> 81 <label i18n for="instanceDefaultNSFWPolicy">Policy on videos containing sensitive content</label>
71 <my-help 82
72 helpType="custom" i18n-customHtml 83 <my-help>
73 customHtml="With <strong>Do not list</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video." 84 <ng-template ptTemplate="customHtml">
74 ></my-help> 85 <ng-container i18n>
86 With <strong>Do not list</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video.
87 </ng-container>
88 </ng-template>
89 </my-help>
75 90
76 <div class="peertube-select-container"> 91 <div class="peertube-select-container">
77 <select id="instanceDefaultNSFWPolicy" formControlName="defaultNSFWPolicy"> 92 <select id="instanceDefaultNSFWPolicy" formControlName="defaultNSFWPolicy">
@@ -82,10 +97,105 @@
82 </div> 97 </div>
83 <div *ngIf="formErrors.instance.defaultNSFWPolicy" class="form-error">{{ formErrors.instance.defaultNSFWPolicy }}</div> 98 <div *ngIf="formErrors.instance.defaultNSFWPolicy" class="form-error">{{ formErrors.instance.defaultNSFWPolicy }}</div>
84 </div> 99 </div>
100
101 <div class="form-group">
102 <label i18n for="instanceTerms">Terms</label><my-help helpType="markdownText"></my-help>
103 <my-markdown-textarea
104 id="instanceTerms" formControlName="terms" textareaWidth="500px" [previewColumn]="true"
105 [ngClass]="{ 'input-error': formErrors['instance.terms'] }"
106 ></my-markdown-textarea>
107 <div *ngIf="formErrors.instance.terms" class="form-error">{{ formErrors.instance.terms }}</div>
108 </div>
109
110 <div class="form-group">
111 <label i18n for="instanceCodeOfConduct">Code of conduct</label><my-help helpType="markdownText"></my-help>
112 <my-markdown-textarea
113 id="instanceCodeOfConduct" formControlName="codeOfConduct" textareaWidth="500px" [previewColumn]="true"
114 [ngClass]="{ 'input-error': formErrors['instance.codeOfConduct'] }"
115 ></my-markdown-textarea>
116 <div *ngIf="formErrors.instance.codeOfConduct" class="form-error">{{ formErrors.instance.codeOfConduct }}</div>
117 </div>
118
119 <div class="form-group">
120 <label i18n for="instanceModerationInformation">Moderation information</label><my-help helpType="markdownText"></my-help>
121 <div class="label-small-info">Who moderates the instance? What is the policy regarding NSFW videos? Political videos? etc</div>
122
123 <my-markdown-textarea
124 id="instanceModerationInformation" formControlName="moderationInformation" textareaWidth="500px" [previewColumn]="true"
125 [ngClass]="{ 'input-error': formErrors['instance.moderationInformation'] }"
126 ></my-markdown-textarea>
127 <div *ngIf="formErrors.instance.moderationInformation" class="form-error">{{ formErrors.instance.moderationInformation }}</div>
128 </div>
129
130 <div i18n class="inner-form-title">You and your instance</div>
131
132 <div class="form-group">
133 <label i18n for="instanceAdministrator">Who is behind the instance?</label>
134 <div class="label-small-info">A single person? A non profit? A company?</div>
135
136 <my-markdown-textarea
137 id="instanceAdministrator" formControlName="administrator" textareaWidth="500px" textareaHeight="75px" [previewColumn]="true"
138 [classes]="{ 'input-error': formErrors['instance.administrator'] }"
139 ></my-markdown-textarea>
140
141 <div *ngIf="formErrors.instance.administrator" class="form-error">{{ formErrors.instance.administrator }}</div>
142 </div>
143
144 <div class="form-group">
145 <label i18n for="instanceCreationReason">Why did you create this instance?</label>
146 <div class="label-small-info">To share your personal videos? To open registrations and allow people to upload what they want?</div>
147
148 <textarea
149 id="instanceCreationReason" formControlName="creationReason" class="small"
150 [ngClass]="{ 'input-error': formErrors['instance.creationReason'] }"
151 ></textarea>
152 <div *ngIf="formErrors.instance.creationReason" class="form-error">{{ formErrors.instance.creationReason }}</div>
153 </div>
154
155 <div class="form-group">
156 <label i18n for="instanceMaintenanceLifetime">How long do you plan to maintain this instance?</label>
157 <div class="label-small-info">It's important to know for users who want to register on your instance</div>
158
159 <textarea
160 id="instanceMaintenanceLifetime" formControlName="maintenanceLifetime" class="small"
161 [ngClass]="{ 'input-error': formErrors['instance.maintenanceLifetime'] }"
162 ></textarea>
163 <div *ngIf="formErrors.instance.maintenanceLifetime" class="form-error">{{ formErrors.instance.maintenanceLifetime }}</div>
164 </div>
165
166 <div class="form-group">
167 <label i18n for="instanceBusinessModel">How will you pay the PeerTube instance server?</label>
168 <div class="label-small-info">With you own funds? With users donations? Advertising?</div>
169
170 <textarea
171 id="instanceBusinessModel" formControlName="businessModel" class="small"
172 [ngClass]="{ 'input-error': formErrors['instance.businessModel'] }"
173 ></textarea>
174 <div *ngIf="formErrors.instance.businessModel" class="form-error">{{ formErrors.instance.businessModel }}</div>
175 </div>
176
177 <div i18n class="inner-form-title">Other information</div>
178
179 <div class="form-group">
180 <label i18n for="instanceHardwareInformation">On what server/hardware the instance runs?</label>
181 <div class="label-small-info">2vCore 2GB RAM/or directly the link to the server you rent etc</div>
182
183 <my-markdown-textarea
184 id="instanceHardwareInformation" formControlName="hardwareInformation" textareaWidth="500px" textareaHeight="75px" [previewColumn]="true"
185 [classes]="{ 'input-error': formErrors['instance.hardwareInformation'] }"
186 ></my-markdown-textarea>
187
188 <div *ngIf="formErrors.instance.hardwareInformation" class="form-error">{{ formErrors.instance.hardwareInformation }}</div>
189 </div>
190
85 </ng-container> 191 </ng-container>
192 </ng-template>
193 </ngb-tab>
86 194
195 <ngb-tab i18n-title title="Basic configuration">
196 <ng-template ngbTabContent>
87 197
88 <div i18n class="inner-form-title">Theme</div> 198 <div i18n class="inner-form-title">Theme & Default route</div>
89 199
90 <ng-container formGroupName="theme"> 200 <ng-container formGroupName="theme">
91 <div class="form-group"> 201 <div class="form-group">
@@ -102,6 +212,19 @@
102 </ng-container> 212 </ng-container>
103 213
104 214
215 <div class="form-group" formGroupName="instance">
216 <label i18n for="instanceDefaultClientRoute">Default client route</label>
217 <div class="peertube-select-container">
218 <select id="instanceDefaultClientRoute" formControlName="defaultClientRoute">
219 <option i18n value="/videos/overview">Videos Discover</option>
220 <option i18n value="/videos/trending">Videos Trending</option>
221 <option i18n value="/videos/recently-added">Videos Recently Added</option>
222 <option i18n value="/videos/local">Local videos</option>
223 </select>
224 </div>
225 <div *ngIf="formErrors.instance.defaultClientRoute" class="form-error">{{ formErrors.instance.defaultClientRoute }}</div>
226 </div>
227
105 <div i18n class="inner-form-title">Signup</div> 228 <div i18n class="inner-form-title">Signup</div>
106 229
107 <ng-container formGroupName="signup"> 230 <ng-container formGroupName="signup">
@@ -221,6 +344,41 @@
221 </ng-container> 344 </ng-container>
222 </ng-container> 345 </ng-container>
223 346
347 <div i18n class="inner-form-title">Instance followings</div>
348
349 <ng-container formGroupName="followings">
350 <ng-container formGroupName="instance">
351
352 <ng-container formGroupName="autoFollowBack">
353 <div class="form-group">
354 <my-peertube-checkbox
355 inputName="followingsInstanceAutoFollowBackEnabled" formControlName="enabled"
356 i18n-labelText labelText="Automatically follow other instances that follow you"
357 ></my-peertube-checkbox>
358 </div>
359 </ng-container>
360
361 <ng-container formGroupName="autoFollowIndex">
362 <div class="form-group">
363 <my-peertube-checkbox
364 inputName="followingsInstanceAutoFollowIndexEnabled" formControlName="enabled"
365 i18n-labelText labelText="Automatically follow instance of the public index (below)"
366 ></my-peertube-checkbox>
367 </div>
368
369 <div class="form-group">
370 <label i18n for="followingsInstanceAutoFollowIndexUrl">Index URL</label>
371 <input
372 type="text" id="followingsInstanceAutoFollowIndexUrl"
373 formControlName="indexUrl" [ngClass]="{ 'input-error': formErrors['followings.instance.autoFollowIndex.indexUrl'] }"
374 >
375 <div *ngIf="formErrors.followings.instance.autoFollowIndex.indexUrl" class="form-error">{{ formErrors.followings.instance.autoFollowIndex.indexUrl }}</div>
376 </div>
377
378 </ng-container>
379 </ng-container>
380 </ng-container>
381
224 382
225 <div i18n class="inner-form-title">Administrator</div> 383 <div i18n class="inner-form-title">Administrator</div>
226 384
@@ -252,10 +410,13 @@
252 410
253 <div class="form-group"> 411 <div class="form-group">
254 <label i18n for="signupLimit">Your Twitter username</label> 412 <label i18n for="signupLimit">Your Twitter username</label>
255 <my-help 413
256 helpType="custom" i18n-customHtml 414 <my-help>
257 customHtml="Indicates the Twitter account for the website or platform on which the content was published." 415 <ng-template ptTemplate="customHtml">
258 ></my-help> 416 <ng-container i18n>Indicates the Twitter account for the website or platform on which the content was published.</ng-container>
417 </ng-template>
418 </my-help>
419
259 <input 420 <input
260 type="text" id="servicesTwitterUsername" 421 type="text" id="servicesTwitterUsername"
261 formControlName="username" [ngClass]="{ 'input-error': formErrors['services.twitter.username'] }" 422 formControlName="username" [ngClass]="{ 'input-error': formErrors['services.twitter.username'] }"
@@ -264,13 +425,21 @@
264 </div> 425 </div>
265 426
266 <div class="form-group"> 427 <div class="form-group">
267 <my-peertube-checkbox 428 <my-peertube-checkbox inputName="servicesTwitterWhitelisted" formControlName="whitelisted">
268 inputName="servicesTwitterWhitelisted" formControlName="whitelisted" 429 <ng-template ptTemplate="label">
269 i18n-labelText labelText="Instance whitelisted by Twitter" 430 <ng-container i18n>Instance whitelisted by Twitter</ng-container>
270 i18n-helpHtml helpHtml="If your instance is whitelisted by Twitter, a video player will be embedded in the Twitter feed on PeerTube video share.<br /> 431 </ng-template>
271 If the instance is not whitelisted, we use an image link card that will redirect on your PeerTube instance.<br /><br /> 432
272 Check this checkbox, save the configuration and test with a video URL of your instance (https://example.com/videos/watch/blabla) on <a target='_blank' rel='noopener noreferrer' href='https://cards-dev.twitter.com/validator'>https://cards-dev.twitter.com/validator</a> to see if you instance is whitelisted." 433 <ng-template ptTemplate="help">
273 ></my-peertube-checkbox> 434 <ng-container i18n>
435 If your instance is whitelisted by Twitter, a video player will be embedded in the Twitter feed on PeerTube video share.<br />
436 If the instance is not whitelisted, we use an image link card that will redirect on your PeerTube instance.<br /><br />
437 Check this checkbox, save the configuration and test with a video URL of your instance (https://example.com/videos/watch/blabla) on
438 <a target='_blank' rel='noopener noreferrer' href='https://cards-dev.twitter.com/validator'>https://cards-dev.twitter.com/validator</a>
439 to see if you instance is whitelisted.
440 </ng-container>
441 </ng-template>
442 </my-peertube-checkbox>
274 </div> 443 </div>
275 444
276 </ng-container> 445 </ng-container>
@@ -286,11 +455,15 @@
286 455
287 <ng-container formGroupName="transcoding"> 456 <ng-container formGroupName="transcoding">
288 <div class="form-group"> 457 <div class="form-group">
289 <my-peertube-checkbox 458 <my-peertube-checkbox inputName="transcodingEnabled" formControlName="enabled">
290 inputName="transcodingEnabled" formControlName="enabled" 459 <ng-template ptTemplate="label">
291 i18n-labelText labelText="Transcoding enabled" 460 <ng-container i18n>Transcoding enabled</ng-container>
292 i18n-helpHtml helpHtml="If you disable transcoding, many videos from your users will not work!" 461 </ng-template>
293 ></my-peertube-checkbox> 462
463 <ng-template ptTemplate="help">
464 <ng-container i18n>If you disable transcoding, many videos from your users will not work!</ng-container>
465 </ng-template>
466 </my-peertube-checkbox>
294 </div> 467 </div>
295 468
296 <ng-container *ngIf="isTranscodingEnabled()"> 469 <ng-container *ngIf="isTranscodingEnabled()">
@@ -299,16 +472,22 @@
299 <my-peertube-checkbox 472 <my-peertube-checkbox
300 inputName="transcodingAllowAdditionalExtensions" formControlName="allowAdditionalExtensions" 473 inputName="transcodingAllowAdditionalExtensions" formControlName="allowAdditionalExtensions"
301 i18n-labelText labelText="Allow additional extensions" 474 i18n-labelText labelText="Allow additional extensions"
302 i18n-helpHtml helpHtml="Allow your users to upload .mkv, .mov, .avi, .flv videos" 475 >
303 ></my-peertube-checkbox> 476 <ng-template ptTemplate="help">
477 <ng-container i18n>Allow your users to upload .mkv, .mov, .avi, .flv videos</ng-container>
478 </ng-template>
479 </my-peertube-checkbox>
304 </div> 480 </div>
305 481
306 <div class="form-group"> 482 <div class="form-group">
307 <my-peertube-checkbox 483 <my-peertube-checkbox
308 inputName="transcodingAllowAudioFiles" formControlName="allowAudioFiles" 484 inputName="transcodingAllowAudioFiles" formControlName="allowAudioFiles"
309 i18n-labelText labelText="Allow audio files upload" 485 i18n-labelText labelText="Allow audio files upload"
310 i18n-helpHtml helpHtml="Allow your users to upload audio files that will be merged with the preview file on upload" 486 >
311 ></my-peertube-checkbox> 487 <ng-template ptTemplate="help">
488 <ng-container i18n>Allow your users to upload audio files that will be merged with the preview file on upload</ng-container>
489 </ng-template>
490 </my-peertube-checkbox>
312 </div> 491 </div>
313 492
314 <div class="form-group"> 493 <div class="form-group">
@@ -338,10 +517,11 @@
338 <div i18n class="inner-form-title"> 517 <div i18n class="inner-form-title">
339 Cache 518 Cache
340 519
341 <my-help 520 <my-help>
342 helpType="custom" i18n-customHtml 521 <ng-template ptTemplate="customHtml">
343 customHtml="Some files are not federated (previews, captions). We fetch them directly from the origin instance and cache them." 522 <ng-container i18n>Some files are not federated (previews, captions). We fetch them directly from the origin instance and cache them.</ng-container>
344 ></my-help> 523 </ng-template>
524 </my-help>
345 </div> 525 </div>
346 526
347 <ng-container formGroupName="cache"> 527 <ng-container formGroupName="cache">
@@ -370,38 +550,45 @@
370 <ng-container formGroupName="customizations"> 550 <ng-container formGroupName="customizations">
371 <div class="form-group"> 551 <div class="form-group">
372 <label i18n for="customizationJavascript">JavaScript</label> 552 <label i18n for="customizationJavascript">JavaScript</label>
373 <my-help 553 <my-help>
374 helpType="custom" i18n-customHtml 554 <ng-template ptTemplate="customHtml">
375 customHtml="Write directly JavaScript code.<br />Example: <pre>console.log('my instance is amazing');</pre>" 555 <ng-container i18n>
376 ></my-help> 556 Write directly JavaScript code.<br />Example: <pre>console.log('my instance is amazing');</pre>
557 </ng-container>
558 </ng-template>
559 </my-help>
560
377 <textarea 561 <textarea
378 id="customizationJavascript" formControlName="javascript" 562 id="customizationJavascript" formControlName="javascript"
379 [ngClass]="{ 'input-error': formErrors['instance.customizations.javascript'] }" 563 [ngClass]="{ 'input-error': formErrors['instance.customizations.javascript'] }"
380 ></textarea> 564 ></textarea>
565
381 <div *ngIf="formErrors.instance.customizations.javascript" class="form-error">{{ formErrors.instance.customizations.javascript }}</div> 566 <div *ngIf="formErrors.instance.customizations.javascript" class="form-error">{{ formErrors.instance.customizations.javascript }}</div>
382 </div> 567 </div>
383 568
384 <div class="form-group"> 569 <div class="form-group">
385 <label for="customizationCSS">CSS</label> 570 <label for="customizationCSS">CSS</label>
386 <my-help 571
387 helpType="custom" 572 <my-help>
388 i18n-customHtml 573 <ng-template ptTemplate="customHtml">
389 customHtml=" 574 <ng-container i18n>
390 Write directly CSS code. Example:<br /><br /> 575 Write directly CSS code. Example:<br /><br />
391 <pre> 576<pre>
392 #custom-css {{ '{' }} 577#custom-css {{ '{' }}
393 color: red; 578 color: red;
394 {{ '}' }} 579{{ '}' }}
395 </pre> 580</pre>
396 581
397 Prepend with <em>#custom-css</em> to override styles. Example:<br /><br /> 582 Prepend with <em>#custom-css</em> to override styles. Example:<br /><br />
398 <pre> 583<pre>
399 #custom-css .logged-in-email {{ '{' }} 584#custom-css .logged-in-email {{ '{' }}
400 color: red; 585 color: red;
401 {{ '}' }} 586{{ '}' }}
402 </pre> 587</pre>
403 " 588 </ng-container>
404 ></my-help> 589 </ng-template>
590 </my-help>
591
405 <textarea 592 <textarea
406 id="customizationCSS" formControlName="css" 593 id="customizationCSS" formControlName="css"
407 [ngClass]="{ 'input-error': formErrors['instance.customizations.css'] }" 594 [ngClass]="{ 'input-error': formErrors['instance.customizations.css'] }"
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss
index c90bd5141..2b4d0da2c 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss
@@ -1,6 +1,10 @@
1@import '_variables'; 1@import '_variables';
2@import '_mixins'; 2@import '_mixins';
3 3
4.form-group {
5 margin-bottom: 25px;
6}
7
4input[type=text] { 8input[type=text] {
5 @include peertube-input-text(340px); 9 @include peertube-input-text(340px);
6 display: block; 10 display: block;
@@ -40,7 +44,12 @@ textarea {
40 44
41 display: block; 45 display: block;
42 46
43 &#instanceShortDescription { 47 &.small {
44 height: 100px; 48 height: 75px;
45 } 49 }
46} 50}
51
52.label-small-info {
53 font-style: italic;
54 margin-bottom: 10px;
55}
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
index 8bd7f7cf6..0a69f3481 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
@@ -6,6 +6,9 @@ import { Notifier } from '@app/core'
6import { CustomConfig } from '../../../../../../shared/models/server/custom-config.model' 6import { CustomConfig } from '../../../../../../shared/models/server/custom-config.model'
7import { I18n } from '@ngx-translate/i18n-polyfill' 7import { I18n } from '@ngx-translate/i18n-polyfill'
8import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 8import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
9import { SelectItem } from 'primeng/api'
10import { forkJoin } from 'rxjs'
11import { first } from 'rxjs/operators'
9 12
10@Component({ 13@Component({
11 selector: 'my-edit-custom-config', 14 selector: 'my-edit-custom-config',
@@ -18,6 +21,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
18 resolutions: { id: string, label: string }[] = [] 21 resolutions: { id: string, label: string }[] = []
19 transcodingThreadOptions: { label: string, value: number }[] = [] 22 transcodingThreadOptions: { label: string, value: number }[] = []
20 23
24 languageItems: SelectItem[] = []
25 categoryItems: SelectItem[] = []
26
21 constructor ( 27 constructor (
22 protected formValidatorService: FormValidatorService, 28 protected formValidatorService: FormValidatorService,
23 private customConfigValidatorsService: CustomConfigValidatorsService, 29 private customConfigValidatorsService: CustomConfigValidatorsService,
@@ -88,10 +94,26 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
88 name: this.customConfigValidatorsService.INSTANCE_NAME, 94 name: this.customConfigValidatorsService.INSTANCE_NAME,
89 shortDescription: this.customConfigValidatorsService.INSTANCE_SHORT_DESCRIPTION, 95 shortDescription: this.customConfigValidatorsService.INSTANCE_SHORT_DESCRIPTION,
90 description: null, 96 description: null,
91 terms: null, 97
92 defaultClientRoute: null,
93 isNSFW: false, 98 isNSFW: false,
94 defaultNSFWPolicy: null, 99 defaultNSFWPolicy: null,
100
101 terms: null,
102 codeOfConduct: null,
103
104 creationReason: null,
105 moderationInformation: null,
106 administrator: null,
107 maintenanceLifetime: null,
108 businessModel: null,
109
110 hardwareInformation: null,
111
112 categories: null,
113 languages: null,
114
115 defaultClientRoute: null,
116
95 customizations: { 117 customizations: {
96 javascript: null, 118 javascript: null,
97 css: null 119 css: null
@@ -158,6 +180,17 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
158 enabled: null, 180 enabled: null,
159 manualApproval: null 181 manualApproval: null
160 } 182 }
183 },
184 followings: {
185 instance: {
186 autoFollowBack: {
187 enabled: null
188 },
189 autoFollowIndex: {
190 enabled: null,
191 indexUrl: this.customConfigValidatorsService.INDEX_URL
192 }
193 }
161 } 194 }
162 } 195 }
163 196
@@ -173,18 +206,27 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
173 206
174 this.buildForm(formGroupData) 207 this.buildForm(formGroupData)
175 208
176 this.configService.getCustomConfig() 209 forkJoin([
177 .subscribe( 210 this.configService.getCustomConfig(),
178 res => { 211 this.serverService.videoLanguagesLoaded.pipe(first()), // First so the observable completes
179 this.customConfig = res 212 this.serverService.videoCategoriesLoaded.pipe(first())
213 ]).subscribe(
214 ([ config ]) => {
215 this.customConfig = config
180 216
181 this.updateForm() 217 const languages = this.serverService.getVideoLanguages()
182 // Force form validation 218 this.languageItems = languages.map(l => ({ label: l.label, value: l.id }))
183 this.forceCheck()
184 },
185 219
186 err => this.notifier.error(err.message) 220 const categories = this.serverService.getVideoCategories()
187 ) 221 this.categoryItems = categories.map(l => ({ label: l.label, value: l.id }))
222
223 this.updateForm()
224 // Force form validation
225 this.forceCheck()
226 },
227
228 err => this.notifier.error(err.message)
229 )
188 } 230 }
189 231
190 isTranscodingEnabled () { 232 isTranscodingEnabled () {
@@ -213,8 +255,23 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
213 ) 255 )
214 } 256 }
215 257
258 getSelectedLanguageLabel () {
259 return this.i18n('{{\'{0} languages selected')
260 }
261
262 getDefaultLanguageLabel () {
263 return this.i18n('No language')
264 }
265
266 getSelectedCategoryLabel () {
267 return this.i18n('{{\'{0} categories selected')
268 }
269
270 getDefaultCategoryLabel () {
271 return this.i18n('No category')
272 }
273
216 private updateForm () { 274 private updateForm () {
217 this.form.patchValue(this.customConfig) 275 this.form.patchValue(this.customConfig)
218 } 276 }
219
220} 277}
diff --git a/client/src/app/+admin/system/debug/debug.component.html b/client/src/app/+admin/system/debug/debug.component.html
index f35414b37..75f3df601 100644
--- a/client/src/app/+admin/system/debug/debug.component.html
+++ b/client/src/app/+admin/system/debug/debug.component.html
@@ -1,7 +1,7 @@
1<div class="root"> 1<div class="root">
2 <h4>IP</h4> 2 <h4>IP</h4>
3 3
4 <p>PeerTube thinks your public IP is <strong>{{ debug?.ip }}</strong>.</p> 4 <p>PeerTube thinks your web browser public IP is <strong>{{ debug?.ip }}</strong>.</p>
5 5
6 <p>If this is not your correct public IP, please consider fixing it because:</p> 6 <p>If this is not your correct public IP, please consider fixing it because:</p>
7 <ul> 7 <ul>
diff --git a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts
index 34febc457..76fabb19d 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts
+++ b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts
@@ -43,7 +43,8 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
43 newUserRegistration: this.i18n('A new user registered on your instance'), 43 newUserRegistration: this.i18n('A new user registered on your instance'),
44 newFollow: this.i18n('You or your channel(s) has a new follower'), 44 newFollow: this.i18n('You or your channel(s) has a new follower'),
45 commentMention: this.i18n('Someone mentioned you in video comments'), 45 commentMention: this.i18n('Someone mentioned you in video comments'),
46 newInstanceFollower: this.i18n('Your instance has a new follower') 46 newInstanceFollower: this.i18n('Your instance has a new follower'),
47 autoInstanceFollowing: this.i18n('Your instance auto followed another instance')
47 } 48 }
48 this.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[] 49 this.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[]
49 50
@@ -51,7 +52,8 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
51 videoAbuseAsModerator: UserRight.MANAGE_VIDEO_ABUSES, 52 videoAbuseAsModerator: UserRight.MANAGE_VIDEO_ABUSES,
52 videoAutoBlacklistAsModerator: UserRight.MANAGE_VIDEO_BLACKLIST, 53 videoAutoBlacklistAsModerator: UserRight.MANAGE_VIDEO_BLACKLIST,
53 newUserRegistration: UserRight.MANAGE_USERS, 54 newUserRegistration: UserRight.MANAGE_USERS,
54 newInstanceFollower: UserRight.MANAGE_SERVER_FOLLOW 55 newInstanceFollower: UserRight.MANAGE_SERVER_FOLLOW,
56 autoInstanceFollowing: UserRight.MANAGE_CONFIGURATION
55 } 57 }
56 58
57 this.emailEnabled = this.serverService.getConfig().email.enabled 59 this.emailEnabled = this.serverService.getConfig().email.enabled
diff --git a/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.html b/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.html
index 2796dd2db..a11238925 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.html
+++ b/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.html
@@ -1,10 +1,13 @@
1<form role="form" (ngSubmit)="updateDetails()" [formGroup]="form"> 1<form role="form" (ngSubmit)="updateDetails()" [formGroup]="form">
2 <div class="form-group"> 2 <div class="form-group">
3 <label i18n for="nsfwPolicy">Default policy on videos containing sensitive content</label> 3 <label i18n for="nsfwPolicy">Default policy on videos containing sensitive content</label>
4 <my-help 4 <my-help>
5 helpType="custom" i18n-customHtml 5 <ng-template ptTemplate="customHtml">
6 customHtml="With <strong>Do not list</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video." 6 <ng-container i18n>
7 ></my-help> 7 With <strong>Do not list</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video.
8 </ng-container>
9 </ng-template>
10 </my-help>
8 11
9 <div class="peertube-select-container"> 12 <div class="peertube-select-container">
10 <select id="nsfwPolicy" formControlName="nsfwPolicy"> 13 <select id="nsfwPolicy" formControlName="nsfwPolicy">
@@ -17,13 +20,15 @@
17 20
18 <div class="form-group"> 21 <div class="form-group">
19 <label i18n for="videoLanguages">Only display videos in the following languages</label> 22 <label i18n for="videoLanguages">Only display videos in the following languages</label>
20 <my-help i18n-customHtml 23 <my-help>
21 customHtml="In Recently added, Trending, Local and Search pages" 24 <ng-template ptTemplate="customHtml">
22 ></my-help> 25 <ng-container i18n>In Recently added, Trending, Local and Search pages</ng-container>
26 </ng-template>
27 </my-help>
23 28
24 <div> 29 <div>
25 <p-multiSelect 30 <p-multiSelect
26 [options]="languageItems" formControlName="videoLanguages" showToggleAll="true" 31 inputId="videoLanguages" [options]="languageItems" formControlName="videoLanguages" showToggleAll="true"
27 [defaultLabel]="getDefaultVideoLanguageLabel()" [selectedItemsLabel]="getSelectedVideoLanguageLabel()" 32 [defaultLabel]="getDefaultVideoLanguageLabel()" [selectedItemsLabel]="getSelectedVideoLanguageLabel()"
28 emptyFilterMessage="No results found" i18n-emptyFilterMessage 33 emptyFilterMessage="No results found" i18n-emptyFilterMessage
29 ></p-multiSelect> 34 ></p-multiSelect>
diff --git a/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.ts b/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.ts
index 77febf179..4fb828082 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.ts
+++ b/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.ts
@@ -5,9 +5,9 @@ import { AuthService } from '../../../core'
5import { FormReactive, User, UserService } from '../../../shared' 5import { FormReactive, User, UserService } from '../../../shared'
6import { I18n } from '@ngx-translate/i18n-polyfill' 6import { I18n } from '@ngx-translate/i18n-polyfill'
7import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 7import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
8import { Subject } from 'rxjs' 8import { forkJoin, Subject } from 'rxjs'
9import { SelectItem } from 'primeng/api' 9import { SelectItem } from 'primeng/api'
10import { switchMap } from 'rxjs/operators' 10import { first } from 'rxjs/operators'
11 11
12@Component({ 12@Component({
13 selector: 'my-account-video-settings', 13 selector: 'my-account-video-settings',
@@ -39,30 +39,31 @@ export class MyAccountVideoSettingsComponent extends FormReactive implements OnI
39 videoLanguages: null 39 videoLanguages: null
40 }) 40 })
41 41
42 this.serverService.videoLanguagesLoaded 42 forkJoin([
43 .pipe(switchMap(() => this.userInformationLoaded)) 43 this.serverService.videoLanguagesLoaded.pipe(first()),
44 .subscribe(() => { 44 this.userInformationLoaded.pipe(first())
45 const languages = this.serverService.getVideoLanguages() 45 ]).subscribe(() => {
46 46 const languages = this.serverService.getVideoLanguages()
47 this.languageItems = [ { label: this.i18n('Unknown language'), value: '_unknown' } ] 47
48 this.languageItems = this.languageItems 48 this.languageItems = [ { label: this.i18n('Unknown language'), value: '_unknown' } ]
49 .concat(languages.map(l => ({ label: l.label, value: l.id }))) 49 this.languageItems = this.languageItems
50 50 .concat(languages.map(l => ({ label: l.label, value: l.id })))
51 const videoLanguages = this.user.videoLanguages 51
52 ? this.user.videoLanguages 52 const videoLanguages = this.user.videoLanguages
53 : this.languageItems.map(l => l.value) 53 ? this.user.videoLanguages
54 54 : this.languageItems.map(l => l.value)
55 this.form.patchValue({ 55
56 nsfwPolicy: this.user.nsfwPolicy, 56 this.form.patchValue({
57 webTorrentEnabled: this.user.webTorrentEnabled, 57 nsfwPolicy: this.user.nsfwPolicy,
58 autoPlayVideo: this.user.autoPlayVideo === true, 58 webTorrentEnabled: this.user.webTorrentEnabled,
59 videoLanguages 59 autoPlayVideo: this.user.autoPlayVideo === true,
60 }) 60 videoLanguages
61 }) 61 })
62 })
62 } 63 }
63 64
64 updateDetails () { 65 updateDetails () {
65 const nsfwPolicy = this.form.value['nsfwPolicy'] 66 const nsfwPolicy = this.form.value[ 'nsfwPolicy' ]
66 const webTorrentEnabled = this.form.value['webTorrentEnabled'] 67 const webTorrentEnabled = this.form.value['webTorrentEnabled']
67 const autoPlayVideo = this.form.value['autoPlayVideo'] 68 const autoPlayVideo = this.form.value['autoPlayVideo']
68 69
diff --git a/client/src/app/+my-account/my-account.module.ts b/client/src/app/+my-account/my-account.module.ts
index 571f46de9..6cf1499d3 100644
--- a/client/src/app/+my-account/my-account.module.ts
+++ b/client/src/app/+my-account/my-account.module.ts
@@ -37,7 +37,6 @@ import {
37} from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component' 37} from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component'
38import { DragDropModule } from '@angular/cdk/drag-drop' 38import { DragDropModule } from '@angular/cdk/drag-drop'
39import { MyAccountChangeEmailComponent } from '@app/+my-account/my-account-settings/my-account-change-email' 39import { MyAccountChangeEmailComponent } from '@app/+my-account/my-account-settings/my-account-change-email'
40import { MultiSelectModule } from 'primeng/multiselect'
41import { MyAccountInterfaceSettingsComponent } from '@app/+my-account/my-account-settings/my-account-interface' 40import { MyAccountInterfaceSettingsComponent } from '@app/+my-account/my-account-settings/my-account-interface'
42 41
43@NgModule({ 42@NgModule({
@@ -48,8 +47,7 @@ import { MyAccountInterfaceSettingsComponent } from '@app/+my-account/my-account
48 SharedModule, 47 SharedModule,
49 TableModule, 48 TableModule,
50 InputSwitchModule, 49 InputSwitchModule,
51 DragDropModule, 50 DragDropModule
52 MultiSelectModule
53 ], 51 ],
54 52
55 declarations: [ 53 declarations: [
diff --git a/client/src/app/+signup/+register/register-step-user.component.html b/client/src/app/+signup/+register/register-step-user.component.html
index 47b3be8cc..4381702ae 100644
--- a/client/src/app/+signup/+register/register-step-user.component.html
+++ b/client/src/app/+signup/+register/register-step-user.component.html
@@ -60,11 +60,16 @@
60 </div> 60 </div>
61 61
62 <div class="form-group form-group-terms"> 62 <div class="form-group form-group-terms">
63 <my-peertube-checkbox 63 <my-peertube-checkbox inputName="terms" formControlName="terms">
64 inputName="terms" formControlName="terms" 64 <ng-template ptTemplate="label">
65 i18n-labelHtml 65 <ng-container i18n>
66 labelHtml="I am at least 16 years old and agree to the <a href='/about/instance#terms-section' target='_blank'rel='noopener noreferrer'>Terms</a> of this instance" 66 I am at least 16 years old and agree
67 ></my-peertube-checkbox> 67 to the <a (click)="onTermsClick($event)" href='#'>Terms</a>
68 <ng-container *ngIf="hasCodeOfConduct"> and to the <a (click)="onCodeOfConductClick($event)" href='#'>Code of Conduct</a></ng-container>
69 of this instance
70 </ng-container>
71 </ng-template>
72 </my-peertube-checkbox>
68 73
69 <div *ngIf="formErrors.terms" class="form-error"> 74 <div *ngIf="formErrors.terms" class="form-error">
70 {{ formErrors.terms }} 75 {{ formErrors.terms }}
diff --git a/client/src/app/+signup/+register/register-step-user.component.ts b/client/src/app/+signup/+register/register-step-user.component.ts
index 3b71fd3c4..6c96f20b4 100644
--- a/client/src/app/+signup/+register/register-step-user.component.ts
+++ b/client/src/app/+signup/+register/register-step-user.component.ts
@@ -1,4 +1,4 @@
1import { Component, EventEmitter, OnInit, Output } from '@angular/core' 1import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
2import { AuthService } from '@app/core' 2import { AuthService } from '@app/core'
3import { FormReactive, UserService, UserValidatorsService } from '@app/shared' 3import { FormReactive, UserService, UserValidatorsService } from '@app/shared'
4import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 4import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
@@ -12,7 +12,11 @@ import { concat, of } from 'rxjs'
12 styleUrls: [ './register.component.scss' ] 12 styleUrls: [ './register.component.scss' ]
13}) 13})
14export class RegisterStepUserComponent extends FormReactive implements OnInit { 14export class RegisterStepUserComponent extends FormReactive implements OnInit {
15 @Input() hasCodeOfConduct = false
16
15 @Output() formBuilt = new EventEmitter<FormGroup>() 17 @Output() formBuilt = new EventEmitter<FormGroup>()
18 @Output() termsClick = new EventEmitter<void>()
19 @Output() codeOfConductClick = new EventEmitter<void>()
16 20
17 constructor ( 21 constructor (
18 protected formValidatorService: FormValidatorService, 22 protected formValidatorService: FormValidatorService,
@@ -45,6 +49,16 @@ export class RegisterStepUserComponent extends FormReactive implements OnInit {
45 .subscribe(([ oldValue, newValue ]) => this.onDisplayNameChange(oldValue, newValue)) 49 .subscribe(([ oldValue, newValue ]) => this.onDisplayNameChange(oldValue, newValue))
46 } 50 }
47 51
52 onTermsClick (event: Event) {
53 event.preventDefault()
54 this.termsClick.emit()
55 }
56
57 onCodeOfConductClick (event: Event) {
58 event.preventDefault()
59 this.codeOfConductClick.emit()
60 }
61
48 private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) { 62 private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) {
49 const username = this.form.value['username'] || '' 63 const username = this.form.value['username'] || ''
50 64
diff --git a/client/src/app/+signup/+register/register.component.html b/client/src/app/+signup/+register/register.component.html
index e4d647fef..906e29aed 100644
--- a/client/src/app/+signup/+register/register.component.html
+++ b/client/src/app/+signup/+register/register.component.html
@@ -7,11 +7,15 @@
7 <my-signup-success *ngIf="signupDone" [message]="success"></my-signup-success> 7 <my-signup-success *ngIf="signupDone" [message]="success"></my-signup-success>
8 <div *ngIf="info" class="alert alert-info">{{ info }}</div> 8 <div *ngIf="info" class="alert alert-info">{{ info }}</div>
9 9
10 <div class="wrapper" *ngIf="!signupDone"> 10 <div class="wrapper" [hidden]="signupDone">
11 <div> 11 <div class="register-form">
12 <my-custom-stepper linear *ngIf="!signupDone"> 12 <my-custom-stepper linear *ngIf="!signupDone">
13 <cdk-step [stepControl]="formStepUser" i18n-label label="User"> 13 <cdk-step [stepControl]="formStepUser" i18n-label label="User">
14 <my-register-step-user (formBuilt)="onUserFormBuilt($event)"></my-register-step-user> 14 <my-register-step-user
15 [hasCodeOfConduct]="!!aboutHtml.codeOfConduct"
16 (formBuilt)="onUserFormBuilt($event)" (termsClick)="onTermsClick()" (codeOfConductClick)="onCodeOfConductClick()"
17 >
18 </my-register-step-user>
15 19
16 <button i18n cdkStepperNext [disabled]="!formStepUser || !formStepUser.valid">Next</button> 20 <button i18n cdkStepperNext [disabled]="!formStepUser || !formStepUser.valid">Next</button>
17 </cdk-step> 21 </cdk-step>
@@ -38,9 +42,56 @@
38 </my-custom-stepper> 42 </my-custom-stepper>
39 </div> 43 </div>
40 44
41 <div> 45 <div class="instance-information">
42 <label i18n>Features found on this instance</label> 46 <ngb-accordion [closeOthers]="true" #accordion="ngbAccordion">
43 <my-instance-features-table></my-instance-features-table> 47 <ngb-panel id="instance-features" i18n-title title="Features found on this instance">
48 <ng-template ngbPanelContent>
49 <my-instance-features-table></my-instance-features-table>
50 </ng-template>
51 </ngb-panel>
52
53 <ng-container *ngIf="about">
54 <ngb-panel
55 *ngIf="aboutHtml.administrator || about.instance.maintenanceLifetime || about.instance.businessModel"
56 id="admin-sustainability" i18n-title title="Administrators & Sustainability"
57 >
58 <ng-template ngbPanelContent>
59 <div class="block">
60 <strong i18n>Who are we?</strong>
61 <div [innerHTML]="aboutHtml.administrator"></div>
62 </div>
63
64 <div class="block">
65 <strong i18n>How long do we plan to maintain this instance?</strong>
66 <div [innerHTML]="about.instance.maintenanceLifetime"></div>
67 </div>
68
69 <div class="block">
70 <strong i18n>How will we pay this instance?</strong>
71 <div [innerHTML]="about.instance.businessModel"></div>
72 </div>
73 </ng-template>
74 </ngb-panel>
75
76 <ngb-panel *ngIf="aboutHtml.moderationInformation" id="moderation-information" i18n-title title="Moderation information">
77 <ng-template ngbPanelContent>
78 <div class="block" [innerHTML]="aboutHtml.moderationInformation"></div>
79 </ng-template>
80 </ngb-panel>
81
82 <ngb-panel *ngIf="aboutHtml.codeOfConduct" id="code-of-conduct" i18n-title title="Code of conduct">
83 <ng-template ngbPanelContent>
84 <div class="block" [innerHTML]="aboutHtml.codeOfConduct"></div>
85 </ng-template>
86 </ngb-panel>
87
88 <ngb-panel *ngIf="aboutHtml.terms" id="terms" i18n-title title="Terms">
89 <ng-template ngbPanelContent>
90 <div class="block" [innerHTML]="aboutHtml.terms"></div>
91 </ng-template>
92 </ngb-panel>
93 </ng-container>
94 </ngb-accordion>
44 </div> 95 </div>
45 </div> 96 </div>
46 97
diff --git a/client/src/app/+signup/+register/register.component.scss b/client/src/app/+signup/+register/register.component.scss
index 9405b5293..2f62dd59d 100644
--- a/client/src/app/+signup/+register/register.component.scss
+++ b/client/src/app/+signup/+register/register.component.scss
@@ -1,5 +1,9 @@
1@import '_variables'; 1@import '_variables';
2@import '_mixins'; 2@import '_mixins';
3@import "./_bootstrap-variables";
4
5@import '~bootstrap/scss/functions';
6@import '~bootstrap/scss/variables';
3 7
4.alert { 8.alert {
5 font-size: 15px; 9 font-size: 15px;
@@ -13,7 +17,32 @@
13 17
14 & > div { 18 & > div {
15 margin-bottom: 40px; 19 margin-bottom: 40px;
16 width: 450px; 20
21 &.register-form {
22 width: 450px;
23 }
24
25 &.instance-information {
26 width: 600px;
27 margin-bottom: 40px;
28
29 .block {
30 font-size: 15px;
31 margin-bottom: 15px;
32 padding: 0 $btn-padding-x;
33 }
34
35 @media screen and (max-width: 1500px) {
36 width: 450px;
37 }
38
39 ngb-accordion ::ng-deep {
40 .btn {
41 font-weight: $font-semibold !important;
42 color: var(--mainForegroundColor) !important;
43 }
44 }
45 }
17 46
18 @media screen and (max-width: 500px) { 47 @media screen and (max-width: 500px) {
19 width: auto; 48 width: auto;
@@ -21,12 +50,6 @@
21 } 50 }
22} 51}
23 52
24my-instance-features-table {
25 display: block;
26
27 margin-bottom: 40px;
28}
29
30.form-group-terms { 53.form-group-terms {
31 margin: 30px 0; 54 margin: 30px 0;
32} 55}
diff --git a/client/src/app/+signup/+register/register.component.ts b/client/src/app/+signup/+register/register.component.ts
index cd6059728..d470ef4dc 100644
--- a/client/src/app/+signup/+register/register.component.ts
+++ b/client/src/app/+signup/+register/register.component.ts
@@ -1,21 +1,35 @@
1import { Component } from '@angular/core' 1import { Component, OnInit, ViewChild } from '@angular/core'
2import { AuthService, Notifier, RedirectService, ServerService } from '@app/core' 2import { AuthService, Notifier, RedirectService, ServerService } from '@app/core'
3import { UserService, UserValidatorsService } from '@app/shared' 3import { UserService, UserValidatorsService } from '@app/shared'
4import { I18n } from '@ngx-translate/i18n-polyfill' 4import { I18n } from '@ngx-translate/i18n-polyfill'
5import { UserRegister } from '@shared/models/users/user-register.model' 5import { UserRegister } from '@shared/models/users/user-register.model'
6import { FormGroup } from '@angular/forms' 6import { FormGroup } from '@angular/forms'
7import { About } from '@shared/models/server'
8import { InstanceService } from '@app/shared/instance/instance.service'
9import { NgbAccordion } from '@ng-bootstrap/ng-bootstrap'
7 10
8@Component({ 11@Component({
9 selector: 'my-register', 12 selector: 'my-register',
10 templateUrl: './register.component.html', 13 templateUrl: './register.component.html',
11 styleUrls: [ './register.component.scss' ] 14 styleUrls: [ './register.component.scss' ]
12}) 15})
13export class RegisterComponent { 16export class RegisterComponent implements OnInit {
17 @ViewChild('accordion', { static: true }) accordion: NgbAccordion
18
14 info: string = null 19 info: string = null
15 error: string = null 20 error: string = null
16 success: string = null 21 success: string = null
17 signupDone = false 22 signupDone = false
18 23
24 about: About
25 aboutHtml = {
26 description: '',
27 terms: '',
28 codeOfConduct: '',
29 moderationInformation: '',
30 administrator: ''
31 }
32
19 formStepUser: FormGroup 33 formStepUser: FormGroup
20 formStepChannel: FormGroup 34 formStepChannel: FormGroup
21 35
@@ -26,6 +40,7 @@ export class RegisterComponent {
26 private userService: UserService, 40 private userService: UserService,
27 private serverService: ServerService, 41 private serverService: ServerService,
28 private redirectService: RedirectService, 42 private redirectService: RedirectService,
43 private instanceService: InstanceService,
29 private i18n: I18n 44 private i18n: I18n
30 ) { 45 ) {
31 } 46 }
@@ -34,6 +49,19 @@ export class RegisterComponent {
34 return this.serverService.getConfig().signup.requiresEmailVerification 49 return this.serverService.getConfig().signup.requiresEmailVerification
35 } 50 }
36 51
52 ngOnInit (): void {
53 this.instanceService.getAbout()
54 .subscribe(
55 async about => {
56 this.about = about
57
58 this.aboutHtml = await this.instanceService.buildHtml(about)
59 },
60
61 err => this.notifier.error(err.message)
62 )
63 }
64
37 hasSameChannelAndAccountNames () { 65 hasSameChannelAndAccountNames () {
38 return this.getUsername() === this.getChannelName() 66 return this.getUsername() === this.getChannelName()
39 } 67 }
@@ -58,6 +86,14 @@ export class RegisterComponent {
58 this.formStepChannel = form 86 this.formStepChannel = form
59 } 87 }
60 88
89 onTermsClick () {
90 if (this.accordion) this.accordion.toggle('terms')
91 }
92
93 onCodeOfConductClick () {
94 if (this.accordion) this.accordion.toggle('code-of-conduct')
95 }
96
61 signup () { 97 signup () {
62 this.error = null 98 this.error = null
63 99
diff --git a/client/src/app/+signup/+register/register.module.ts b/client/src/app/+signup/+register/register.module.ts
index 46336cbd0..e55f83990 100644
--- a/client/src/app/+signup/+register/register.module.ts
+++ b/client/src/app/+signup/+register/register.module.ts
@@ -7,13 +7,15 @@ import { RegisterStepChannelComponent } from './register-step-channel.component'
7import { RegisterStepUserComponent } from './register-step-user.component' 7import { RegisterStepUserComponent } from './register-step-user.component'
8import { CustomStepperComponent } from './custom-stepper.component' 8import { CustomStepperComponent } from './custom-stepper.component'
9import { SignupSharedModule } from '@app/+signup/shared/signup-shared.module' 9import { SignupSharedModule } from '@app/+signup/shared/signup-shared.module'
10import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'
10 11
11@NgModule({ 12@NgModule({
12 imports: [ 13 imports: [
13 RegisterRoutingModule, 14 RegisterRoutingModule,
14 SharedModule, 15 SharedModule,
15 CdkStepperModule, 16 CdkStepperModule,
16 SignupSharedModule 17 SignupSharedModule,
18 NgbAccordionModule
17 ], 19 ],
18 20
19 declarations: [ 21 declarations: [
diff --git a/client/src/app/app.component.html b/client/src/app/app.component.html
index 07a576083..81b4351c5 100644
--- a/client/src/app/app.component.html
+++ b/client/src/app/app.component.html
@@ -54,3 +54,8 @@
54 </div> 54 </div>
55 </ng-template> 55 </ng-template>
56</p-toast> 56</p-toast>
57
58<ng-template [ngIf]="isUserLoggedIn()">
59 <my-welcome-modal #welcomeModal></my-welcome-modal>
60 <my-instance-config-warning-modal #instanceConfigWarningModal></my-instance-config-warning-modal>
61</ng-template>
diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts
index 64bfb9671..6b18e5feb 100644
--- a/client/src/app/app.component.ts
+++ b/client/src/app/app.component.ts
@@ -1,10 +1,10 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit, ViewChild } from '@angular/core'
2import { DomSanitizer, SafeHtml } from '@angular/platform-browser' 2import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
3import { Event, GuardsCheckStart, NavigationEnd, Router, Scroll } from '@angular/router' 3import { Event, GuardsCheckStart, NavigationEnd, Router, Scroll } from '@angular/router'
4import { AuthService, RedirectService, ServerService, ThemeService } from '@app/core' 4import { AuthService, RedirectService, ServerService, ThemeService } from '@app/core'
5import { is18nPath } from '../../../shared/models/i18n' 5import { is18nPath } from '../../../shared/models/i18n'
6import { ScreenService } from '@app/shared/misc/screen.service' 6import { ScreenService } from '@app/shared/misc/screen.service'
7import { debounceTime, filter, map, pairwise, skip } from 'rxjs/operators' 7import { debounceTime, filter, map, pairwise, skip, switchMap } from 'rxjs/operators'
8import { Hotkey, HotkeysService } from 'angular2-hotkeys' 8import { Hotkey, HotkeysService } from 'angular2-hotkeys'
9import { I18n } from '@ngx-translate/i18n-polyfill' 9import { I18n } from '@ngx-translate/i18n-polyfill'
10import { fromEvent } from 'rxjs' 10import { fromEvent } from 'rxjs'
@@ -13,6 +13,11 @@ import { PluginService } from '@app/core/plugins/plugin.service'
13import { HooksService } from '@app/core/plugins/hooks.service' 13import { HooksService } from '@app/core/plugins/hooks.service'
14import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 14import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
15import { POP_STATE_MODAL_DISMISS } from '@app/shared/misc/constants' 15import { POP_STATE_MODAL_DISMISS } from '@app/shared/misc/constants'
16import { WelcomeModalComponent } from '@app/modal/welcome-modal.component'
17import { InstanceConfigWarningModalComponent } from '@app/modal/instance-config-warning-modal.component'
18import { UserRole } from '@shared/models'
19import { User } from '@app/shared'
20import { InstanceService } from '@app/shared/instance/instance.service'
16 21
17@Component({ 22@Component({
18 selector: 'my-app', 23 selector: 'my-app',
@@ -20,6 +25,9 @@ import { POP_STATE_MODAL_DISMISS } from '@app/shared/misc/constants'
20 styleUrls: [ './app.component.scss' ] 25 styleUrls: [ './app.component.scss' ]
21}) 26})
22export class AppComponent implements OnInit { 27export class AppComponent implements OnInit {
28 @ViewChild('welcomeModal', { static: false }) welcomeModal: WelcomeModalComponent
29 @ViewChild('instanceConfigWarningModal', { static: false }) instanceConfigWarningModal: InstanceConfigWarningModalComponent
30
23 isMenuDisplayed = true 31 isMenuDisplayed = true
24 isMenuChangedByUser = false 32 isMenuChangedByUser = false
25 33
@@ -32,6 +40,7 @@ export class AppComponent implements OnInit {
32 private authService: AuthService, 40 private authService: AuthService,
33 private serverService: ServerService, 41 private serverService: ServerService,
34 private pluginService: PluginService, 42 private pluginService: PluginService,
43 private instanceService: InstanceService,
35 private domSanitizer: DomSanitizer, 44 private domSanitizer: DomSanitizer,
36 private redirectService: RedirectService, 45 private redirectService: RedirectService,
37 private screenService: ScreenService, 46 private screenService: ScreenService,
@@ -96,6 +105,8 @@ export class AppComponent implements OnInit {
96 .subscribe(() => this.onResize()) 105 .subscribe(() => this.onResize())
97 106
98 this.location.onPopState(() => this.modalService.dismissAll(POP_STATE_MODAL_DISMISS)) 107 this.location.onPopState(() => this.modalService.dismissAll(POP_STATE_MODAL_DISMISS))
108
109 this.openModalsIfNeeded()
99 } 110 }
100 111
101 isUserLoggedIn () { 112 isUserLoggedIn () {
@@ -220,32 +231,66 @@ export class AppComponent implements OnInit {
220 this.hooks.runAction('action:application.init', 'common') 231 this.hooks.runAction('action:application.init', 'common')
221 } 232 }
222 233
234 private async openModalsIfNeeded () {
235 this.serverService.configLoaded
236 .pipe(
237 switchMap(() => this.authService.userInformationLoaded),
238 map(() => this.authService.getUser()),
239 filter(user => user.role === UserRole.ADMINISTRATOR)
240 ).subscribe(user => setTimeout(() => this.openAdminModals(user))) // setTimeout because of ngIf in template
241 }
242
243 private async openAdminModals (user: User) {
244 if (user.noWelcomeModal !== true) return this.welcomeModal.show()
245
246 const config = this.serverService.getConfig()
247 if (user.noInstanceConfigWarningModal === true || !config.signup.allowed) return
248
249 this.instanceService.getAbout()
250 .subscribe(about => {
251 if (
252 config.instance.name.toLowerCase() === 'peertube' ||
253 !about.instance.terms ||
254 !about.instance.administrator ||
255 !about.instance.maintenanceLifetime
256 ) {
257 this.instanceConfigWarningModal.show(about)
258 }
259 })
260 }
261
223 private initHotkeys () { 262 private initHotkeys () {
224 this.hotkeysService.add([ 263 this.hotkeysService.add([
225 new Hotkey(['/', 's'], (event: KeyboardEvent): boolean => { 264 new Hotkey(['/', 's'], (event: KeyboardEvent): boolean => {
226 document.getElementById('search-video').focus() 265 document.getElementById('search-video').focus()
227 return false 266 return false
228 }, undefined, this.i18n('Focus the search bar')), 267 }, undefined, this.i18n('Focus the search bar')),
268
229 new Hotkey('b', (event: KeyboardEvent): boolean => { 269 new Hotkey('b', (event: KeyboardEvent): boolean => {
230 this.toggleMenu() 270 this.toggleMenu()
231 return false 271 return false
232 }, undefined, this.i18n('Toggle the left menu')), 272 }, undefined, this.i18n('Toggle the left menu')),
273
233 new Hotkey('g o', (event: KeyboardEvent): boolean => { 274 new Hotkey('g o', (event: KeyboardEvent): boolean => {
234 this.router.navigate([ '/videos/overview' ]) 275 this.router.navigate([ '/videos/overview' ])
235 return false 276 return false
236 }, undefined, this.i18n('Go to the discover videos page')), 277 }, undefined, this.i18n('Go to the discover videos page')),
278
237 new Hotkey('g t', (event: KeyboardEvent): boolean => { 279 new Hotkey('g t', (event: KeyboardEvent): boolean => {
238 this.router.navigate([ '/videos/trending' ]) 280 this.router.navigate([ '/videos/trending' ])
239 return false 281 return false
240 }, undefined, this.i18n('Go to the trending videos page')), 282 }, undefined, this.i18n('Go to the trending videos page')),
283
241 new Hotkey('g r', (event: KeyboardEvent): boolean => { 284 new Hotkey('g r', (event: KeyboardEvent): boolean => {
242 this.router.navigate([ '/videos/recently-added' ]) 285 this.router.navigate([ '/videos/recently-added' ])
243 return false 286 return false
244 }, undefined, this.i18n('Go to the recently added videos page')), 287 }, undefined, this.i18n('Go to the recently added videos page')),
288
245 new Hotkey('g l', (event: KeyboardEvent): boolean => { 289 new Hotkey('g l', (event: KeyboardEvent): boolean => {
246 this.router.navigate([ '/videos/local' ]) 290 this.router.navigate([ '/videos/local' ])
247 return false 291 return false
248 }, undefined, this.i18n('Go to the local videos page')), 292 }, undefined, this.i18n('Go to the local videos page')),
293
249 new Hotkey('g u', (event: KeyboardEvent): boolean => { 294 new Hotkey('g u', (event: KeyboardEvent): boolean => {
250 this.router.navigate([ '/videos/upload' ]) 295 this.router.navigate([ '/videos/upload' ])
251 return false 296 return false
diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts
index 1e2936a37..a3ea33ca9 100644
--- a/client/src/app/app.module.ts
+++ b/client/src/app/app.module.ts
@@ -18,6 +18,8 @@ import { VideosModule } from './videos'
18import { buildFileLocale, getCompleteLocale, isDefaultLocale } from '../../../shared/models/i18n' 18import { buildFileLocale, getCompleteLocale, isDefaultLocale } from '../../../shared/models/i18n'
19import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' 19import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils'
20import { SearchModule } from '@app/search' 20import { SearchModule } from '@app/search'
21import { WelcomeModalComponent } from '@app/modal/welcome-modal.component'
22import { InstanceConfigWarningModalComponent } from '@app/modal/instance-config-warning-modal.component'
21 23
22export function metaFactory (serverService: ServerService): MetaLoader { 24export function metaFactory (serverService: ServerService): MetaLoader {
23 return new MetaStaticLoader({ 25 return new MetaStaticLoader({
@@ -39,7 +41,10 @@ export function metaFactory (serverService: ServerService): MetaLoader {
39 MenuComponent, 41 MenuComponent,
40 LanguageChooserComponent, 42 LanguageChooserComponent,
41 AvatarNotificationComponent, 43 AvatarNotificationComponent,
42 HeaderComponent 44 HeaderComponent,
45
46 WelcomeModalComponent,
47 InstanceConfigWarningModalComponent
43 ], 48 ],
44 imports: [ 49 imports: [
45 BrowserModule, 50 BrowserModule,
diff --git a/client/src/app/login/login.component.html b/client/src/app/login/login.component.html
index 4efe3fb22..683355960 100644
--- a/client/src/app/login/login.component.html
+++ b/client/src/app/login/login.component.html
@@ -23,10 +23,11 @@
23 or create an account on another instance 23 or create an account on another instance
24 </a> 24 </a>
25 25
26 <my-help 26 <my-help *ngIf="signupAllowed === false">
27 *ngIf="signupAllowed === false" helpType="custom" i18n-customHtml 27 <ng-template ptTemplate="customHtml">
28 customHtml="User registration is not allowed on this instance, but you can register on many others!" 28 <ng-container i18n>User registration is not allowed on this instance, but you can register on many others!</ng-container>
29 ></my-help> 29 </ng-template>
30 </my-help>
30 </div> 31 </div>
31 32
32 <div *ngIf="formErrors.username" class="form-error"> 33 <div *ngIf="formErrors.username" class="form-error">
diff --git a/client/src/app/modal/instance-config-warning-modal.component.html b/client/src/app/modal/instance-config-warning-modal.component.html
new file mode 100644
index 000000000..64f14e69b
--- /dev/null
+++ b/client/src/app/modal/instance-config-warning-modal.component.html
@@ -0,0 +1,45 @@
1<ng-template #modal let-hide="close">
2 <div class="modal-header">
3 <h4 i18n class="modal-title">Configuration warning!</h4>
4 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
5 </div>
6
7 <div class="modal-body">
8
9 <p i18n>Hello dear administrator. You enabled user registration on your instance but you did not configure the following fields:</p>
10
11 <ul>
12 <li i18n *ngIf="about.instance.name.toLowerCase() === 'peertube'">Instance name</li>
13 <li i18n *ngIf="isDefaultShortDescription(about.instance.shortDescription)">Instance short description</li>
14
15 <li i18n *ngIf="!about.instance.administrator">Who you are</li>
16 <li i18n *ngIf="!about.instance.maintenanceLifetime">How long you plan to maintain your instance</li>
17 <li i18n *ngIf="!about.instance.businessModel">How you plan to pay your instance</li>
18
19 <li i18n *ngIf="!about.instance.moderationInformation">How you will moderate your instance</li>
20 <li i18n *ngIf="!about.instance.terms">Instance terms</li>
21 </ul>
22
23 <p>
24 Please consider to configure these fields to help people to choose <strong>the appropriate instance</strong>.
25 Without them, your instance may not be referenced on <a target="_blank" rel="noopener noreferrer" href="https://joinpeertube.org">JoinPeerTube website</a>.
26 </p>
27
28 <div class="configure-instance">
29 <a i18n href="/admin/config/edit-custom" target="_blank" rel="noopener noreferrer">Configure these fields</a>
30 </div>
31
32 </div>
33
34 <div class="modal-footer inputs">
35 <my-peertube-checkbox
36 inputName="stopDisplayModal" [(ngModel)]="stopDisplayModal"
37 i18n-labelText labelText="Don't show me this warning anymore"
38 >
39
40 </my-peertube-checkbox>
41
42 <span i18n class="action-button action-button-cancel" (click)="hide()">Close</span>
43 </div>
44
45</ng-template>
diff --git a/client/src/app/modal/instance-config-warning-modal.component.scss b/client/src/app/modal/instance-config-warning-modal.component.scss
new file mode 100644
index 000000000..ff62a1b9c
--- /dev/null
+++ b/client/src/app/modal/instance-config-warning-modal.component.scss
@@ -0,0 +1,22 @@
1@import '_mixins';
2@import '_variables';
3
4.action-button-cancel {
5 margin-right: 0 !important;
6}
7
8.modal-body {
9 font-size: 15px;
10}
11
12li {
13 margin-bottom: 10px;
14}
15
16.configure-instance {
17 text-align: center;
18 font-weight: 600;
19 font-size: 18px;
20 margin-top: 40px;
21 margin-bottom: 10px;
22}
diff --git a/client/src/app/modal/instance-config-warning-modal.component.ts b/client/src/app/modal/instance-config-warning-modal.component.ts
new file mode 100644
index 000000000..742a7dd41
--- /dev/null
+++ b/client/src/app/modal/instance-config-warning-modal.component.ts
@@ -0,0 +1,47 @@
1import { Component, ElementRef, ViewChild } from '@angular/core'
2import { Notifier } from '@app/core'
3import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
4import { About } from '@shared/models/server'
5import { UserService } from '@app/shared'
6
7@Component({
8 selector: 'my-instance-config-warning-modal',
9 templateUrl: './instance-config-warning-modal.component.html',
10 styleUrls: [ './instance-config-warning-modal.component.scss' ]
11})
12export class InstanceConfigWarningModalComponent {
13 @ViewChild('modal', { static: true }) modal: ElementRef
14
15 stopDisplayModal = false
16 about: About
17
18 constructor (
19 private userService: UserService,
20 private modalService: NgbModal,
21 private notifier: Notifier
22 ) { }
23
24 show (about: About) {
25 this.about = about
26
27 const ref = this.modalService.open(this.modal)
28
29 ref.result.finally(() => {
30 if (this.stopDisplayModal === true) this.doNotOpenAgain()
31 })
32 }
33
34 isDefaultShortDescription (description: string) {
35 return description === 'PeerTube, a federated (ActivityPub) video streaming platform using P2P (BitTorrent) directly ' +
36 'in the web browser with WebTorrent and Angular.'
37 }
38
39 private doNotOpenAgain () {
40 this.userService.updateMyProfile({ noInstanceConfigWarningModal: true })
41 .subscribe(
42 () => console.log('We will not open the instance config warning modal again.'),
43
44 err => this.notifier.error(err.message)
45 )
46 }
47}
diff --git a/client/src/app/modal/welcome-modal.component.html b/client/src/app/modal/welcome-modal.component.html
new file mode 100644
index 000000000..09ff2163b
--- /dev/null
+++ b/client/src/app/modal/welcome-modal.component.html
@@ -0,0 +1,67 @@
1<ng-template #modal let-hide="close">
2 <div class="modal-header">
3 <h4 i18n class="modal-title">Welcome on PeerTube dear administrator!</h4>
4 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
5 </div>
6
7 <div class="modal-body">
8
9 <div class="block-documentation">
10 <div i18n class="subtitle">Documentation</div>
11
12 <div class="columns">
13 <a class="link-block" href="https://docs.joinpeertube.org/#/maintain-tools" target="_blank" rel="noopener noreferrer">
14 <a class="link-title" href="https://docs.joinpeertube.org/#/maintain-tools" target="_blank" rel="noopener noreferrer">CLI</a>
15
16 <div>Upload or import videos, parse logs, prune storage directories, reset user password...</div>
17 </a>
18
19 <a class="link-block" href="https://docs.joinpeertube.org/#/admin-following-instances" target="_blank" rel="noopener noreferrer">
20 <a class="link-title" href="https://docs.joinpeertube.org/#/admin-following-instances" target="_blank" rel="noopener noreferrer">Administer</a>
21
22 <div>Managing users, following other instances, dealing with spammers...</div>
23 </a>
24
25 <a class="link-block" href="https://docs.joinpeertube.org/#/use-setup-account" target="_blank" rel="noopener noreferrer">
26 <a class="link-title" href="https://docs.joinpeertube.org/#/use-setup-account" target="_blank" rel="noopener noreferrer">Use</a>
27
28 <div>Setup your account, managing video playlists, discover third-party applications...</div>
29 </a>
30 </div>
31 </div>
32
33 <div class="block-configuration">
34 <div i18n class="subtitle">It's time to configure your instance!</div>
35
36 <p i18n>
37 Choosing your <strong>instance name</strong>, <strong>setting up a description</strong>, specifying <strong>who you are</strong>,
38 why <strong>you created your instance</strong> and <strong>how long</strong> you plan to <strong>maintain your it</strong>
39 is very important for visitors to understand on what type of instance they are.
40 </p>
41
42 <p i18n>
43 If you want to open registrations, please decide what are <strong>your moderation rules</strong>, fill your <strong>instance terms</strong>
44 and specify the categories and languages you speak. This way, you will help users to register on <strong>the appropriate</strong> PeerTube instance.
45 </p>
46
47 <div class="configure-instance">
48 <a i18n href="/admin/config/edit-custom" target="_blank" rel="noopener noreferrer">Configure your instance</a>
49 </div>
50 </div>
51
52 <div class="block-links">
53 <div i18n class="subtitle">Useful links</div>
54
55 <ul>
56 <li>Official PeerTube website (news, support, contribute...): <a href="https://joinpeertube.org" target="_blank" rel="noopener noreferrer">https://joinpeertube.org</a></li>
57
58 <li>Put your instance on the public PeerTube index: <a href="https://instances.joinpeertube.org/instances">https://instances.joinpeertube.org/instances</a></li>
59 </ul>
60 </div>
61 </div>
62
63 <div class="modal-footer inputs">
64 <span i18n class="action-button action-button-submit" (click)="hide()">Understood!</span>
65 </div>
66
67</ng-template>
diff --git a/client/src/app/modal/welcome-modal.component.scss b/client/src/app/modal/welcome-modal.component.scss
new file mode 100644
index 000000000..8bb6973f4
--- /dev/null
+++ b/client/src/app/modal/welcome-modal.component.scss
@@ -0,0 +1,56 @@
1@import '_mixins';
2@import '_variables';
3
4.modal-body {
5 font-size: 15px;
6}
7
8.subtitle {
9 font-weight: $font-semibold;
10 margin-bottom: 10px;
11 font-size: 16px;
12}
13
14.block-documentation .subtitle {
15 margin-bottom: 20px;
16}
17
18.block-configuration,
19.block-instance {
20 margin-top: 30px;
21}
22
23li {
24 margin-bottom: 10px;
25}
26
27.configure-instance {
28 text-align: center;
29 font-weight: 600;
30 font-size: 18px;
31 margin: 20px 0 40px 0;
32}
33
34.columns {
35 display: flex;
36
37 .link-block {
38 @include disable-default-a-behaviour;
39
40 color: var(--mainForegroundColor);
41 padding: 10px;
42 transition: background-color 0.2s ease-in;
43
44 &:hover {
45 background-color: rgba(0, 0, 0, 0.05);
46 }
47
48 .link-title {
49 font-size: 16px;
50 font-weight: $font-semibold;
51 display: flex;
52 justify-content: center;
53 margin-bottom: 5px;
54 }
55 }
56}
diff --git a/client/src/app/modal/welcome-modal.component.ts b/client/src/app/modal/welcome-modal.component.ts
new file mode 100644
index 000000000..05412a4cd
--- /dev/null
+++ b/client/src/app/modal/welcome-modal.component.ts
@@ -0,0 +1,38 @@
1import { Component, ElementRef, ViewChild } from '@angular/core'
2import { Notifier } from '@app/core'
3import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
4import { UserService } from '@app/shared'
5
6@Component({
7 selector: 'my-welcome-modal',
8 templateUrl: './welcome-modal.component.html',
9 styleUrls: [ './welcome-modal.component.scss' ]
10})
11export class WelcomeModalComponent {
12 @ViewChild('modal', { static: true }) modal: ElementRef
13
14 constructor (
15 private userService: UserService,
16 private modalService: NgbModal,
17 private notifier: Notifier
18 ) { }
19
20 show () {
21 const ref = this.modalService.open(this.modal,{
22 backdrop: 'static',
23 keyboard: false,
24 size: 'lg'
25 })
26
27 ref.result.finally(() => this.doNotOpenAgain())
28 }
29
30 private doNotOpenAgain () {
31 this.userService.updateMyProfile({ noWelcomeModal: true })
32 .subscribe(
33 () => console.log('We will not open the welcome modal again.'),
34
35 err => this.notifier.error(err.message)
36 )
37 }
38}
diff --git a/client/src/app/shared/angular/peertube-template.directive.ts b/client/src/app/shared/angular/peertube-template.directive.ts
index a514b6057..e04c25d9a 100644
--- a/client/src/app/shared/angular/peertube-template.directive.ts
+++ b/client/src/app/shared/angular/peertube-template.directive.ts
@@ -3,8 +3,8 @@ import { Directive, Input, TemplateRef } from '@angular/core'
3@Directive({ 3@Directive({
4 selector: '[ptTemplate]' 4 selector: '[ptTemplate]'
5}) 5})
6export class PeerTubeTemplateDirective { 6export class PeerTubeTemplateDirective <T extends string> {
7 @Input('ptTemplate') name: string 7 @Input('ptTemplate') name: T
8 8
9 constructor (public template: TemplateRef<any>) { 9 constructor (public template: TemplateRef<any>) {
10 // empty 10 // empty
diff --git a/client/src/app/shared/forms/form-validators/custom-config-validators.service.ts b/client/src/app/shared/forms/form-validators/custom-config-validators.service.ts
index 882e39453..767e3f026 100644
--- a/client/src/app/shared/forms/form-validators/custom-config-validators.service.ts
+++ b/client/src/app/shared/forms/form-validators/custom-config-validators.service.ts
@@ -13,6 +13,7 @@ export class CustomConfigValidatorsService {
13 readonly SIGNUP_LIMIT: BuildFormValidator 13 readonly SIGNUP_LIMIT: BuildFormValidator
14 readonly ADMIN_EMAIL: BuildFormValidator 14 readonly ADMIN_EMAIL: BuildFormValidator
15 readonly TRANSCODING_THREADS: BuildFormValidator 15 readonly TRANSCODING_THREADS: BuildFormValidator
16 readonly INDEX_URL: BuildFormValidator
16 17
17 constructor (private i18n: I18n) { 18 constructor (private i18n: I18n) {
18 this.INSTANCE_NAME = { 19 this.INSTANCE_NAME = {
@@ -78,5 +79,13 @@ export class CustomConfigValidatorsService {
78 'min': this.i18n('Transcoding threads must be greater or equal to 0.') 79 'min': this.i18n('Transcoding threads must be greater or equal to 0.')
79 } 80 }
80 } 81 }
82
83 this.INDEX_URL = {
84 VALIDATORS: [ Validators.required, Validators.pattern(/^https:\/\//) ],
85 MESSAGES: {
86 'required': this.i18n('Index URL is required.'),
87 'pattern': this.i18n('Index URL should be a URL')
88 }
89 }
81 } 90 }
82} 91}
diff --git a/client/src/app/shared/forms/peertube-checkbox.component.html b/client/src/app/shared/forms/peertube-checkbox.component.html
index 571a1a673..f1e3bf0bf 100644
--- a/client/src/app/shared/forms/peertube-checkbox.component.html
+++ b/client/src/app/shared/forms/peertube-checkbox.component.html
@@ -3,8 +3,15 @@
3 <input type="checkbox" [(ngModel)]="checked" (ngModelChange)="onModelChange()" [id]="inputName" [disabled]="disabled" /> 3 <input type="checkbox" [(ngModel)]="checked" (ngModelChange)="onModelChange()" [id]="inputName" [disabled]="disabled" />
4 <span role="checkbox" [attr.aria-checked]="checked"></span> 4 <span role="checkbox" [attr.aria-checked]="checked"></span>
5 <span *ngIf="labelText">{{ labelText }}</span> 5 <span *ngIf="labelText">{{ labelText }}</span>
6 <span *ngIf="labelHtml" [innerHTML]="labelHtml"></span> 6
7 <span *ngIf="labelTemplate">
8 <ng-container *ngTemplateOutlet="labelTemplate"></ng-container>
9 </span>
7 </label> 10 </label>
8 11
9 <my-help *ngIf="helpHtml" [tooltipPlacement]="helpPlacement" helpType="custom" i18n-customHtml [customHtml]="helpHtml"></my-help> 12 <my-help *ngIf="helpTemplate" [tooltipPlacement]="helpPlacement" helpType="custom">
13 <ng-template ptTemplate="customHtml">
14 <ng-template *ngTemplateOutlet="helpTemplate"></ng-template>
15 </ng-template>
16 </my-help>
10</div> 17</div>
diff --git a/client/src/app/shared/forms/peertube-checkbox.component.scss b/client/src/app/shared/forms/peertube-checkbox.component.scss
index 84ea788af..51f98b0bc 100644
--- a/client/src/app/shared/forms/peertube-checkbox.component.scss
+++ b/client/src/app/shared/forms/peertube-checkbox.component.scss
@@ -7,7 +7,7 @@
7 .form-group-checkbox { 7 .form-group-checkbox {
8 display: flex; 8 display: flex;
9 9
10 span { 10 .label-text {
11 font-weight: $font-regular; 11 font-weight: $font-regular;
12 margin: 0; 12 margin: 0;
13 } 13 }
diff --git a/client/src/app/shared/forms/peertube-checkbox.component.ts b/client/src/app/shared/forms/peertube-checkbox.component.ts
index a4b72aa37..3b8f39ed0 100644
--- a/client/src/app/shared/forms/peertube-checkbox.component.ts
+++ b/client/src/app/shared/forms/peertube-checkbox.component.ts
@@ -1,5 +1,6 @@
1import { ChangeDetectorRef, Component, forwardRef, Input, OnChanges, SimpleChanges } from '@angular/core' 1import { AfterContentInit, ChangeDetectorRef, Component, ContentChildren, forwardRef, Input, QueryList, TemplateRef } from '@angular/core'
2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' 2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
3import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template.directive'
3 4
4@Component({ 5@Component({
5 selector: 'my-peertube-checkbox', 6 selector: 'my-peertube-checkbox',
@@ -13,20 +14,35 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
13 } 14 }
14 ] 15 ]
15}) 16})
16export class PeertubeCheckboxComponent implements ControlValueAccessor { 17export class PeertubeCheckboxComponent implements ControlValueAccessor, AfterContentInit {
17 @Input() checked = false 18 @Input() checked = false
18 @Input() inputName: string 19 @Input() inputName: string
19 @Input() labelText: string 20 @Input() labelText: string
20 @Input() labelHtml: string
21 @Input() helpHtml: string
22 @Input() helpPlacement = 'top' 21 @Input() helpPlacement = 'top'
23 @Input() disabled = false 22 @Input() disabled = false
24 23
24 @ContentChildren(PeerTubeTemplateDirective) templates: QueryList<PeerTubeTemplateDirective<'label' | 'help'>>
25
25 // FIXME: https://github.com/angular/angular/issues/10816#issuecomment-307567836 26 // FIXME: https://github.com/angular/angular/issues/10816#issuecomment-307567836
26 @Input() onPushWorkaround = false 27 @Input() onPushWorkaround = false
27 28
29 labelTemplate: TemplateRef<any>
30 helpTemplate: TemplateRef<any>
31
28 constructor (private cdr: ChangeDetectorRef) { } 32 constructor (private cdr: ChangeDetectorRef) { }
29 33
34 ngAfterContentInit () {
35 {
36 const t = this.templates.find(t => t.name === 'label')
37 if (t) this.labelTemplate = t.template
38 }
39
40 {
41 const t = this.templates.find(t => t.name === 'help')
42 if (t) this.helpTemplate = t.template
43 }
44 }
45
30 propagateChange = (_: any) => { /* empty */ } 46 propagateChange = (_: any) => { /* empty */ }
31 47
32 writeValue (checked: boolean) { 48 writeValue (checked: boolean) {
diff --git a/client/src/app/shared/instance/feature-boolean.component.html b/client/src/app/shared/instance/feature-boolean.component.html
new file mode 100644
index 000000000..ac208fc13
--- /dev/null
+++ b/client/src/app/shared/instance/feature-boolean.component.html
@@ -0,0 +1,3 @@
1<span *ngIf="value === true" class="glyphicon glyphicon-ok"></span>
2<span *ngIf="value === false" class="glyphicon glyphicon-remove"></span>
3
diff --git a/client/src/app/shared/instance/feature-boolean.component.scss b/client/src/app/shared/instance/feature-boolean.component.scss
new file mode 100644
index 000000000..56d08af06
--- /dev/null
+++ b/client/src/app/shared/instance/feature-boolean.component.scss
@@ -0,0 +1,10 @@
1@import '_variables';
2@import '_mixins';
3
4.glyphicon-ok {
5 color: $green;
6}
7
8.glyphicon-remove {
9 color: $red;
10}
diff --git a/client/src/app/shared/instance/feature-boolean.component.ts b/client/src/app/shared/instance/feature-boolean.component.ts
new file mode 100644
index 000000000..d02d513d6
--- /dev/null
+++ b/client/src/app/shared/instance/feature-boolean.component.ts
@@ -0,0 +1,10 @@
1import { Component, Input } from '@angular/core'
2
3@Component({
4 selector: 'my-feature-boolean',
5 templateUrl: './feature-boolean.component.html',
6 styleUrls: [ './feature-boolean.component.scss' ]
7})
8export class FeatureBooleanComponent {
9 @Input() value: boolean
10}
diff --git a/client/src/app/shared/instance/instance-features-table.component.html b/client/src/app/shared/instance/instance-features-table.component.html
index 2987bd00e..d1cb8fcbe 100644
--- a/client/src/app/shared/instance/instance-features-table.component.html
+++ b/client/src/app/shared/instance/instance-features-table.component.html
@@ -1,28 +1,53 @@
1<div class="feature-table"> 1<div class="feature-table">
2 2
3 <table class="table"> 3 <table class="table" *ngIf="config">
4 <tr> 4 <tr>
5 <td i18n class="label">Default NSFW/sensitive videos policy (can be redefined by the users)</td> 5 <td i18n class="label">
6 <div>Default NSFW/sensitive videos policy</div>
7 <div class="more-info">can be redefined by the users</div>
8 </td>
6 9
7 <td class="value">{{ buildNSFWLabel() }}</td> 10 <td class="value">{{ buildNSFWLabel() }}</td>
8 </tr> 11 </tr>
9 12
10 <tr *ngFor="let feature of features"> 13 <tr>
11 <td class="label">{{ feature.label }}</td> 14 <td i18n class="label">User registration allowed</td>
12 <td> 15 <td>
13 <span *ngIf="feature.value === true" class="glyphicon glyphicon-ok"></span> 16 <my-feature-boolean [value]="config.signup.allowed"></my-feature-boolean>
14 <span *ngIf="feature.value === false" class="glyphicon glyphicon-remove"></span>
15 </td> 17 </td>
16 </tr> 18 </tr>
17 19
18 <tr> 20 <tr>
19 <td i18n class="label">Video quota</td> 21 <td i18n class="label" colspan="2">Video uploads</td>
22 </tr>
23
24 <tr>
25 <td i18n class="sub-label">Transcoding in multiple resolutions</td>
26 <td>
27 <my-feature-boolean [value]="config.transcoding.enabledResolutions.length !== 0"></my-feature-boolean>
28 </td>
29 </tr>
30
31 <tr>
32 <td i18n class="sub-label">Video uploads</td>
33 <td>
34 <span *ngIf="config.autoBlacklist.videos.ofUsers.enabled">Requires manual validation by moderators</span>
35 <span *ngIf="!config.autoBlacklist.videos.ofUsers.enabled">Automatically published</span>
36 </td>
37 </tr>
38
39 <tr>
40 <td i18n class="sub-label">Video quota</td>
20 41
21 <td class="value"> 42 <td class="value">
22 <ng-container *ngIf="initialUserVideoQuota !== -1"> 43 <ng-container *ngIf="initialUserVideoQuota !== -1">
23 {{ initialUserVideoQuota | bytes: 0 }} <ng-container *ngIf="dailyUserVideoQuota !== -1">({{ dailyUserVideoQuota | bytes: 0 }} per day)</ng-container> 44 {{ initialUserVideoQuota | bytes: 0 }} <ng-container *ngIf="dailyUserVideoQuota !== -1">({{ dailyUserVideoQuota | bytes: 0 }} per day)</ng-container>
24 45
25 <my-help tooltipPlacement="auto" helpType="custom" [customHtml]="quotaHelpIndication"></my-help> 46 <my-help tooltipPlacement="auto" helpType="custom">
47 <ng-template ptTemplate="customHtml">
48 <div [innerHTML]="quotaHelpIndication"></div>
49 </ng-template>
50 </my-help>
26 </ng-container> 51 </ng-container>
27 52
28 <ng-container i18n *ngIf="initialUserVideoQuota === -1"> 53 <ng-container i18n *ngIf="initialUserVideoQuota === -1">
@@ -30,5 +55,35 @@
30 </ng-container> 55 </ng-container>
31 </td> 56 </td>
32 </tr> 57 </tr>
58
59 <tr>
60 <td i18n class="label" colspan="2">Import</td>
61 </tr>
62
63 <tr>
64 <td i18n class="sub-label">HTTP import (YouTube, Vimeo, direct URL...)</td>
65 <td>
66 <my-feature-boolean [value]="config.import.videos.http.enabled"></my-feature-boolean>
67 </td>
68 </tr>
69
70 <tr>
71 <td i18n class="sub-label">Torrent import</td>
72 <td>
73 <my-feature-boolean [value]="config.import.videos.torrent.enabled"></my-feature-boolean>
74 </td>
75 </tr>
76
77
78 <tr>
79 <td i18n class="label" colspan="2">Player</td>
80 </tr>
81
82 <tr>
83 <td i18n class="sub-label">P2P enabled</td>
84 <td>
85 <my-feature-boolean [value]="config.tracker.enabled"></my-feature-boolean>
86 </td>
87 </tr>
33 </table> 88 </table>
34</div> 89</div>
diff --git a/client/src/app/shared/instance/instance-features-table.component.scss b/client/src/app/shared/instance/instance-features-table.component.scss
index f9bec038d..67f2b6c84 100644
--- a/client/src/app/shared/instance/instance-features-table.component.scss
+++ b/client/src/app/shared/instance/instance-features-table.component.scss
@@ -5,16 +5,28 @@ table {
5 font-size: 14px; 5 font-size: 14px;
6 color: var(--mainForegroundColor); 6 color: var(--mainForegroundColor);
7 7
8 .label { 8 .label,
9 font-weight: $font-semibold; 9 .sub-label {
10 min-width: 330px; 10 min-width: 330px;
11 }
12 11
13 .glyphicon-ok { 12 &.label {
14 color: $green; 13 font-weight: $font-semibold;
14 }
15
16 &.sub-label {
17 padding-left: 30px;
18 }
19
20 .more-info {
21 font-style: italic;
22 font-weight: initial;
23 font-size: 14px
24 }
15 } 25 }
16 26
17 .glyphicon-remove { 27 td {
18 color: $red; 28 vertical-align: middle;
19 } 29 }
20} 30}
31
32
diff --git a/client/src/app/shared/instance/instance-features-table.component.ts b/client/src/app/shared/instance/instance-features-table.component.ts
index a53082a93..46df4d0b2 100644
--- a/client/src/app/shared/instance/instance-features-table.component.ts
+++ b/client/src/app/shared/instance/instance-features-table.component.ts
@@ -1,6 +1,7 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { ServerService } from '@app/core' 2import { ServerService } from '@app/core'
3import { I18n } from '@ngx-translate/i18n-polyfill' 3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { ServerConfig } from '@shared/models'
4 5
5@Component({ 6@Component({
6 selector: 'my-instance-features-table', 7 selector: 'my-instance-features-table',
@@ -8,8 +9,8 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
8 styleUrls: [ './instance-features-table.component.scss' ] 9 styleUrls: [ './instance-features-table.component.scss' ]
9}) 10})
10export class InstanceFeaturesTableComponent implements OnInit { 11export class InstanceFeaturesTableComponent implements OnInit {
11 features: { label: string, value?: boolean }[] = []
12 quotaHelpIndication = '' 12 quotaHelpIndication = ''
13 config: ServerConfig
13 14
14 constructor ( 15 constructor (
15 private i18n: I18n, 16 private i18n: I18n,
@@ -28,7 +29,7 @@ export class InstanceFeaturesTableComponent implements OnInit {
28 ngOnInit () { 29 ngOnInit () {
29 this.serverService.configLoaded 30 this.serverService.configLoaded
30 .subscribe(() => { 31 .subscribe(() => {
31 this.buildFeatures() 32 this.config = this.serverService.getConfig()
32 this.buildQuotaHelpIndication() 33 this.buildQuotaHelpIndication()
33 }) 34 })
34 } 35 }
@@ -41,37 +42,6 @@ export class InstanceFeaturesTableComponent implements OnInit {
41 if (policy === 'display') return this.i18n('Displayed') 42 if (policy === 'display') return this.i18n('Displayed')
42 } 43 }
43 44
44 private buildFeatures () {
45 const config = this.serverService.getConfig()
46
47 this.features = [
48 {
49 label: this.i18n('User registration allowed'),
50 value: config.signup.allowed
51 },
52 {
53 label: this.i18n('Video uploads require manual validation by moderators'),
54 value: config.autoBlacklist.videos.ofUsers.enabled
55 },
56 {
57 label: this.i18n('Transcode your videos in multiple resolutions'),
58 value: config.transcoding.enabledResolutions.length !== 0
59 },
60 {
61 label: this.i18n('HTTP import (YouTube, Vimeo, direct URL...)'),
62 value: config.import.videos.http.enabled
63 },
64 {
65 label: this.i18n('Torrent import'),
66 value: config.import.videos.torrent.enabled
67 },
68 {
69 label: this.i18n('P2P enabled'),
70 value: config.tracker.enabled
71 }
72 ]
73 }
74
75 private getApproximateTime (seconds: number) { 45 private getApproximateTime (seconds: number) {
76 const hours = Math.floor(seconds / 3600) 46 const hours = Math.floor(seconds / 3600)
77 let pluralSuffix = '' 47 let pluralSuffix = ''
diff --git a/client/src/app/shared/instance/instance.service.ts b/client/src/app/shared/instance/instance.service.ts
index d0c96941d..44b413fa4 100644
--- a/client/src/app/shared/instance/instance.service.ts
+++ b/client/src/app/shared/instance/instance.service.ts
@@ -4,6 +4,9 @@ import { Injectable } from '@angular/core'
4import { environment } from '../../../environments/environment' 4import { environment } from '../../../environments/environment'
5import { RestExtractor, RestService } from '../rest' 5import { RestExtractor, RestService } from '../rest'
6import { About } from '../../../../../shared/models/server' 6import { About } from '../../../../../shared/models/server'
7import { MarkdownService } from '@app/shared/renderer'
8import { peertubeTranslate } from '@shared/models'
9import { ServerService } from '@app/core'
7 10
8@Injectable() 11@Injectable()
9export class InstanceService { 12export class InstanceService {
@@ -13,7 +16,9 @@ export class InstanceService {
13 constructor ( 16 constructor (
14 private authHttp: HttpClient, 17 private authHttp: HttpClient,
15 private restService: RestService, 18 private restService: RestService,
16 private restExtractor: RestExtractor 19 private restExtractor: RestExtractor,
20 private markdownService: MarkdownService,
21 private serverService: ServerService
17 ) { 22 ) {
18 } 23 }
19 24
@@ -34,4 +39,43 @@ export class InstanceService {
34 .pipe(catchError(res => this.restExtractor.handleError(res))) 39 .pipe(catchError(res => this.restExtractor.handleError(res)))
35 40
36 } 41 }
42
43 async buildHtml (about: About) {
44 const html = {
45 description: '',
46 terms: '',
47 codeOfConduct: '',
48 moderationInformation: '',
49 administrator: '',
50 hardwareInformation: ''
51 }
52
53 for (const key of Object.keys(html)) {
54 html[ key ] = await this.markdownService.textMarkdownToHTML(about.instance[ key ])
55 }
56
57 return html
58 }
59
60 buildTranslatedLanguages (about: About, translations: any) {
61 const languagesArray = this.serverService.getVideoLanguages()
62
63 return about.instance.languages
64 .map(l => {
65 const languageObj = languagesArray.find(la => la.id === l)
66
67 return peertubeTranslate(languageObj.label, translations)
68 })
69 }
70
71 buildTranslatedCategories (about: About, translations: any) {
72 const categoriesArray = this.serverService.getVideoCategories()
73
74 return about.instance.categories
75 .map(c => {
76 const categoryObj = categoriesArray.find(ca => ca.id === c)
77
78 return peertubeTranslate(categoryObj.label, translations)
79 })
80 }
37} 81}
diff --git a/client/src/app/shared/misc/help.component.html b/client/src/app/shared/misc/help.component.html
index e31eef06a..9a6d3e48e 100644
--- a/client/src/app/shared/misc/help.component.html
+++ b/client/src/app/shared/misc/help.component.html
@@ -1,15 +1,25 @@
1<ng-template #tooltipTemplate> 1<ng-template #tooltipTemplate>
2 <ng-template [ngIf]="preHtml"> 2 <p *ngIf="preHtmlTemplate">
3 <p [innerHTML]="preHtml"></p> 3 <ng-template *ngTemplateOutlet="preHtmlTemplate"></ng-template>
4 <br /> 4 </p>
5 </ng-template>
6 5
7 <p [innerHTML]="mainHtml"></p> 6 <ng-container *ngIf="preHtmlTemplate && (customHtmlTemplate || mainHtml || postHtmlTemplate)">
7 <br /><br />
8 </ng-container>
8 9
9 <ng-template [ngIf]="postHtml"> 10 <p *ngIf="customHtmlTemplate">
10 <br /> 11 <ng-template *ngTemplateOutlet="customHtmlTemplate"></ng-template>
11 <p [innerHTML]="postHtml"></p> 12 </p>
12 </ng-template> 13
14 <p *ngIf="mainHtml" [innerHTML]="mainHtml"></p>
15
16 <ng-container *ngIf="(customHtmlTemplate || mainHtml) && postHtmlTemplate">
17 <br /><br />
18 </ng-container>
19
20 <p *ngIf="postHtmlTemplate">
21 <ng-template *ngTemplateOutlet="postHtmlTemplate"></ng-template>
22 </p>
13</ng-template> 23</ng-template>
14 24
15<span 25<span
diff --git a/client/src/app/shared/misc/help.component.ts b/client/src/app/shared/misc/help.component.ts
index f3426f70f..18ba8ad5e 100644
--- a/client/src/app/shared/misc/help.component.ts
+++ b/client/src/app/shared/misc/help.component.ts
@@ -1,6 +1,7 @@
1import { Component, Input, OnChanges, OnInit } from '@angular/core' 1import { AfterContentInit, Component, ContentChildren, Input, OnChanges, OnInit, QueryList, TemplateRef } from '@angular/core'
2import { I18n } from '@ngx-translate/i18n-polyfill' 2import { I18n } from '@ngx-translate/i18n-polyfill'
3import { MarkdownService } from '@app/shared/renderer' 3import { MarkdownService } from '@app/shared/renderer'
4import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template.directive'
4 5
5@Component({ 6@Component({
6 selector: 'my-help', 7 selector: 'my-help',
@@ -8,22 +9,42 @@ import { MarkdownService } from '@app/shared/renderer'
8 templateUrl: './help.component.html' 9 templateUrl: './help.component.html'
9}) 10})
10 11
11export class HelpComponent implements OnInit, OnChanges { 12export class HelpComponent implements OnInit, OnChanges, AfterContentInit {
12 @Input() preHtml = ''
13 @Input() postHtml = ''
14 @Input() customHtml = ''
15 @Input() helpType: 'custom' | 'markdownText' | 'markdownEnhanced' = 'custom' 13 @Input() helpType: 'custom' | 'markdownText' | 'markdownEnhanced' = 'custom'
16 @Input() tooltipPlacement = 'right' 14 @Input() tooltipPlacement = 'right'
17 15
16 @ContentChildren(PeerTubeTemplateDirective) templates: QueryList<PeerTubeTemplateDirective<'preHtml' | 'customHtml' | 'postHtml'>>
17
18 isPopoverOpened = false 18 isPopoverOpened = false
19 mainHtml = '' 19 mainHtml = ''
20 20
21 preHtmlTemplate: TemplateRef<any>
22 customHtmlTemplate: TemplateRef<any>
23 postHtmlTemplate: TemplateRef<any>
24
21 constructor (private i18n: I18n) { } 25 constructor (private i18n: I18n) { }
22 26
23 ngOnInit () { 27 ngOnInit () {
24 this.init() 28 this.init()
25 } 29 }
26 30
31 ngAfterContentInit () {
32 {
33 const t = this.templates.find(t => t.name === 'preHtml')
34 if (t) this.preHtmlTemplate = t.template
35 }
36
37 {
38 const t = this.templates.find(t => t.name === 'customHtml')
39 if (t) this.customHtmlTemplate = t.template
40 }
41
42 {
43 const t = this.templates.find(t => t.name === 'postHtml')
44 if (t) this.postHtmlTemplate = t.template
45 }
46 }
47
27 ngOnChanges () { 48 ngOnChanges () {
28 this.init() 49 this.init()
29 } 50 }
@@ -37,11 +58,6 @@ export class HelpComponent implements OnInit, OnChanges {
37 } 58 }
38 59
39 private init () { 60 private init () {
40 if (this.helpType === 'custom') {
41 this.mainHtml = this.customHtml
42 return
43 }
44
45 if (this.helpType === 'markdownText') { 61 if (this.helpType === 'markdownText') {
46 this.mainHtml = this.formatMarkdownSupport(MarkdownService.TEXT_RULES) 62 this.mainHtml = this.formatMarkdownSupport(MarkdownService.TEXT_RULES)
47 return 63 return
diff --git a/client/src/app/shared/renderer/markdown.service.ts b/client/src/app/shared/renderer/markdown.service.ts
index 9a9066351..0e24f3085 100644
--- a/client/src/app/shared/renderer/markdown.service.ts
+++ b/client/src/app/shared/renderer/markdown.service.ts
@@ -13,9 +13,11 @@ export class MarkdownService {
13 'list' 13 'list'
14 ] 14 ]
15 static ENHANCED_RULES = MarkdownService.TEXT_RULES.concat([ 'image' ]) 15 static ENHANCED_RULES = MarkdownService.TEXT_RULES.concat([ 'image' ])
16 static COMPLETE_RULES = MarkdownService.ENHANCED_RULES.concat([ 'block', 'inline', 'heading', 'html_inline', 'html_block', 'paragraph' ])
16 17
17 private textMarkdownIt: MarkdownIt 18 private textMarkdownIt: MarkdownIt
18 private enhancedMarkdownIt: MarkdownIt 19 private enhancedMarkdownIt: MarkdownIt
20 private completeMarkdownIt: MarkdownIt
19 21
20 async textMarkdownToHTML (markdown: string) { 22 async textMarkdownToHTML (markdown: string) {
21 if (!markdown) return '' 23 if (!markdown) return ''
@@ -39,11 +41,22 @@ export class MarkdownService {
39 return this.avoidTruncatedTags(html) 41 return this.avoidTruncatedTags(html)
40 } 42 }
41 43
42 private async createMarkdownIt (rules: string[]) { 44 async completeMarkdownToHTML (markdown: string) {
43 // FIXME: import('..') returns a struct module, containing a "default" field corresponding to our sanitizeHtml function 45 if (!markdown) return ''
46
47 if (!this.completeMarkdownIt) {
48 this.completeMarkdownIt = await this.createMarkdownIt(MarkdownService.COMPLETE_RULES, true)
49 }
50
51 const html = this.completeMarkdownIt.render(markdown)
52 return this.avoidTruncatedTags(html)
53 }
54
55 private async createMarkdownIt (rules: string[], html = false) {
56 // FIXME: import('...') returns a struct module, containing a "default" field corresponding to our sanitizeHtml function
44 const MarkdownItClass: typeof import ('markdown-it') = (await import('markdown-it') as any).default 57 const MarkdownItClass: typeof import ('markdown-it') = (await import('markdown-it') as any).default
45 58
46 const markdownIt = new MarkdownItClass('zero', { linkify: true, breaks: true }) 59 const markdownIt = new MarkdownItClass('zero', { linkify: true, breaks: true, html })
47 60
48 for (const rule of rules) { 61 for (const rule of rules) {
49 markdownIt.enable(rule) 62 markdownIt.enable(rule)
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts
index eb57a2fff..65e0f21a4 100644
--- a/client/src/app/shared/shared.module.ts
+++ b/client/src/app/shared/shared.module.ts
@@ -6,10 +6,8 @@ import { RouterModule } from '@angular/router'
6import { MarkdownTextareaComponent } from '@app/shared/forms/markdown-textarea.component' 6import { MarkdownTextareaComponent } from '@app/shared/forms/markdown-textarea.component'
7import { HelpComponent } from '@app/shared/misc/help.component' 7import { HelpComponent } from '@app/shared/misc/help.component'
8import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive' 8import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive'
9
10import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes' 9import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes'
11import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared' 10import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared'
12
13import { AUTH_INTERCEPTOR_PROVIDER } from './auth' 11import { AUTH_INTERCEPTOR_PROVIDER } from './auth'
14import { ButtonComponent } from './buttons/button.component' 12import { ButtonComponent } from './buttons/button.component'
15import { DeleteButtonComponent } from './buttons/delete-button.component' 13import { DeleteButtonComponent } from './buttons/delete-button.component'
@@ -93,6 +91,8 @@ import { VideoDownloadComponent } from '@app/shared/video/modals/video-download.
93import { VideoReportComponent } from '@app/shared/video/modals/video-report.component' 91import { VideoReportComponent } from '@app/shared/video/modals/video-report.component'
94import { ClipboardModule } from 'ngx-clipboard' 92import { ClipboardModule } from 'ngx-clipboard'
95import { FollowService } from '@app/shared/instance/follow.service' 93import { FollowService } from '@app/shared/instance/follow.service'
94import { MultiSelectModule } from 'primeng/multiselect'
95import { FeatureBooleanComponent } from '@app/shared/instance/feature-boolean.component'
96 96
97@NgModule({ 97@NgModule({
98 imports: [ 98 imports: [
@@ -113,7 +113,8 @@ import { FollowService } from '@app/shared/instance/follow.service'
113 113
114 PrimeSharedModule, 114 PrimeSharedModule,
115 InputMaskModule, 115 InputMaskModule,
116 NgPipesModule 116 NgPipesModule,
117 MultiSelectModule
117 ], 118 ],
118 119
119 declarations: [ 120 declarations: [
@@ -156,6 +157,7 @@ import { FollowService } from '@app/shared/instance/follow.service'
156 SubscribeButtonComponent, 157 SubscribeButtonComponent,
157 RemoteSubscribeComponent, 158 RemoteSubscribeComponent,
158 InstanceFeaturesTableComponent, 159 InstanceFeaturesTableComponent,
160 FeatureBooleanComponent,
159 UserBanModalComponent, 161 UserBanModalComponent,
160 UserModerationDropdownComponent, 162 UserModerationDropdownComponent,
161 TopMenuDropdownComponent, 163 TopMenuDropdownComponent,
@@ -186,6 +188,7 @@ import { FollowService } from '@app/shared/instance/follow.service'
186 InputMaskModule, 188 InputMaskModule,
187 BytesPipe, 189 BytesPipe,
188 KeysPipe, 190 KeysPipe,
191 MultiSelectModule,
189 192
190 LoaderComponent, 193 LoaderComponent,
191 SmallLoaderComponent, 194 SmallLoaderComponent,
diff --git a/client/src/app/shared/user-subscription/remote-subscribe.component.html b/client/src/app/shared/user-subscription/remote-subscribe.component.html
index ec3636b3e..59ee1cb04 100644
--- a/client/src/app/shared/user-subscription/remote-subscribe.component.html
+++ b/client/src/app/shared/user-subscription/remote-subscribe.component.html
@@ -12,13 +12,21 @@
12 <span *ngIf="interact">Remote interact</span> 12 <span *ngIf="interact">Remote interact</span>
13 </button> 13 </button>
14 14
15 <my-help *ngIf="!interact && showHelp" 15 <my-help *ngIf="!interact && showHelp">
16 helpType="custom" 16 <ng-template ptTemplate="customHtml">
17 i18n-customHtml customHtml="You can subscribe to the channel via any ActivityPub-capable fediverse instance. For instance with Mastodon or Pleroma you can type the channel URL in the search box and subscribe there."> 17 <ng-container i18n>
18 You can subscribe to the channel via any ActivityPub-capable fediverse instance.<br /><br />
19 For instance with Mastodon or Pleroma you can type the channel URL in the search box and subscribe there.
20 </ng-container>
21 </ng-template>
18 </my-help> 22 </my-help>
19 23
20 <my-help *ngIf="showHelp && interact" 24 <my-help *ngIf="showHelp && interact">
21 helpType="custom" 25 <ng-template ptTemplate="customHtml">
22 i18n-customHtml customHtml="You can interact with this via any ActivityPub-capable fediverse instance. For instance with Mastodon or Pleroma you can type the current URL in the search box and interact with it there."> 26 <ng-container i18n>
27 You can interact with this via any ActivityPub-capable fediverse instance.<br /><br />
28 For instance with Mastodon or Pleroma you can type the current URL in the search box and interact with it there.
29 </ng-container>
30 </ng-template>
23 </my-help> 31 </my-help>
24</form> \ No newline at end of file 32</form>
diff --git a/client/src/app/shared/users/user-notification.model.ts b/client/src/app/shared/users/user-notification.model.ts
index 06eace71c..c3f4bf429 100644
--- a/client/src/app/shared/users/user-notification.model.ts
+++ b/client/src/app/shared/users/user-notification.model.ts
@@ -42,9 +42,10 @@ export class UserNotification implements UserNotificationServer {
42 state: FollowState 42 state: FollowState
43 follower: ActorInfo & { avatarUrl?: string } 43 follower: ActorInfo & { avatarUrl?: string }
44 following: { 44 following: {
45 type: 'account' | 'channel' 45 type: 'account' | 'channel' | 'instance'
46 name: string 46 name: string
47 displayName: string 47 displayName: string
48 host: string
48 } 49 }
49 } 50 }
50 51
@@ -112,7 +113,10 @@ export class UserNotification implements UserNotificationServer {
112 113
113 case UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS: 114 case UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS:
114 this.videoAutoBlacklistUrl = '/admin/moderation/video-auto-blacklist/list' 115 this.videoAutoBlacklistUrl = '/admin/moderation/video-auto-blacklist/list'
115 this.videoUrl = this.buildVideoUrl(this.video) 116 // Backward compatibility where we did not assign videoBlacklist to this type of notification before
117 if (!this.videoBlacklist) this.videoBlacklist = { id: null, video: this.video }
118
119 this.videoUrl = this.buildVideoUrl(this.videoBlacklist.video)
116 break 120 break
117 121
118 case UserNotificationType.BLACKLIST_ON_MY_VIDEO: 122 case UserNotificationType.BLACKLIST_ON_MY_VIDEO:
@@ -146,6 +150,10 @@ export class UserNotification implements UserNotificationServer {
146 case UserNotificationType.NEW_INSTANCE_FOLLOWER: 150 case UserNotificationType.NEW_INSTANCE_FOLLOWER:
147 this.instanceFollowUrl = '/admin/follows/followers-list' 151 this.instanceFollowUrl = '/admin/follows/followers-list'
148 break 152 break
153
154 case UserNotificationType.AUTO_INSTANCE_FOLLOWING:
155 this.instanceFollowUrl = '/admin/follows/following-list'
156 break
149 } 157 }
150 } catch (err) { 158 } catch (err) {
151 this.type = null 159 this.type = null
diff --git a/client/src/app/shared/users/user-notifications.component.html b/client/src/app/shared/users/user-notifications.component.html
index 292813426..a0f8e6df5 100644
--- a/client/src/app/shared/users/user-notifications.component.html
+++ b/client/src/app/shared/users/user-notifications.component.html
@@ -8,7 +8,7 @@
8 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.video.channel.avatarUrl" /> 8 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.video.channel.avatarUrl" />
9 9
10 <div class="message"> 10 <div class="message">
11 {{ notification.video.channel.displayName }} published a <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">new video</a> 11 {{ notification.video.channel.displayName }} published a new video: <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.video.name }}</a>
12 </div> 12 </div>
13 </ng-container> 13 </ng-container>
14 14
@@ -40,7 +40,7 @@
40 <my-global-icon iconName="no"></my-global-icon> 40 <my-global-icon iconName="no"></my-global-icon>
41 41
42 <div class="message"> 42 <div class="message">
43 The recently added video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.video.name }}</a> has been <a (click)="markAsRead(notification)" [routerLink]="notification.videoAutoBlacklistUrl">auto-blacklisted</a> 43 The recently added video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.videoBlacklist.video.name }}</a> has been <a (click)="markAsRead(notification)" [routerLink]="notification.videoAutoBlacklistUrl">auto-blacklisted</a>
44 </div> 44 </div>
45 </ng-container> 45 </ng-container>
46 46
@@ -111,6 +111,14 @@
111 <ng-container *ngIf="notification.actorFollow.state === 'pending'"> awaiting your approval</ng-container> 111 <ng-container *ngIf="notification.actorFollow.state === 'pending'"> awaiting your approval</ng-container>
112 </div> 112 </div>
113 </ng-container> 113 </ng-container>
114
115 <ng-container i18n *ngSwitchCase="UserNotificationType.AUTO_INSTANCE_FOLLOWING">
116 <my-global-icon iconName="users"></my-global-icon>
117
118 <div class="message">
119 Your instance automatically followed <a (click)="markAsRead(notification)" [routerLink]="notification.instanceFollowUrl">{{ notification.actorFollow.following.host }}</a>
120 </div>
121 </ng-container>
114 </ng-container> 122 </ng-container>
115 123
116 <div class="from-date">{{ notification.createdAt | myFromNow }}</div> 124 <div class="from-date">{{ notification.createdAt | myFromNow }}</div>
diff --git a/client/src/app/shared/users/user.model.ts b/client/src/app/shared/users/user.model.ts
index 53809f82c..656b73dd2 100644
--- a/client/src/app/shared/users/user.model.ts
+++ b/client/src/app/shared/users/user.model.ts
@@ -9,31 +9,38 @@ export class User implements UserServerModel {
9 username: string 9 username: string
10 email: string 10 email: string
11 pendingEmail: string | null 11 pendingEmail: string | null
12
12 emailVerified: boolean 13 emailVerified: boolean
13 nsfwPolicy: NSFWPolicyType 14 nsfwPolicy: NSFWPolicyType
14 15
15 role: UserRole 16 adminFlags?: UserAdminFlag
16 roleLabel: string
17 17
18 webTorrentEnabled: boolean
19 autoPlayVideo: boolean 18 autoPlayVideo: boolean
19 webTorrentEnabled: boolean
20 videosHistoryEnabled: boolean 20 videosHistoryEnabled: boolean
21 videoLanguages: string[] 21 videoLanguages: string[]
22 22
23 role: UserRole
24 roleLabel: string
25
23 videoQuota: number 26 videoQuota: number
24 videoQuotaDaily: number 27 videoQuotaDaily: number
25 account: Account 28 videoQuotaUsed?: number
26 videoChannels: VideoChannel[] 29 videoQuotaUsedDaily?: number
27 createdAt: Date
28 30
29 theme: string 31 theme: string
30 32
31 adminFlags?: UserAdminFlag 33 account: Account
34 notificationSettings?: UserNotificationSetting
35 videoChannels?: VideoChannel[]
32 36
33 blocked: boolean 37 blocked: boolean
34 blockedReason?: string 38 blockedReason?: string
35 39
36 notificationSettings?: UserNotificationSetting 40 noInstanceConfigWarningModal: boolean
41 noWelcomeModal: boolean
42
43 createdAt: Date
37 44
38 constructor (hash: Partial<UserServerModel>) { 45 constructor (hash: Partial<UserServerModel>) {
39 this.id = hash.id 46 this.id = hash.id
@@ -43,13 +50,16 @@ export class User implements UserServerModel {
43 this.role = hash.role 50 this.role = hash.role
44 51
45 this.videoChannels = hash.videoChannels 52 this.videoChannels = hash.videoChannels
53
46 this.videoQuota = hash.videoQuota 54 this.videoQuota = hash.videoQuota
47 this.videoQuotaDaily = hash.videoQuotaDaily 55 this.videoQuotaDaily = hash.videoQuotaDaily
56 this.videoQuotaUsed = hash.videoQuotaUsed
57 this.videoQuotaUsedDaily = hash.videoQuotaUsedDaily
58
48 this.nsfwPolicy = hash.nsfwPolicy 59 this.nsfwPolicy = hash.nsfwPolicy
49 this.webTorrentEnabled = hash.webTorrentEnabled 60 this.webTorrentEnabled = hash.webTorrentEnabled
50 this.videosHistoryEnabled = hash.videosHistoryEnabled 61 this.videosHistoryEnabled = hash.videosHistoryEnabled
51 this.autoPlayVideo = hash.autoPlayVideo 62 this.autoPlayVideo = hash.autoPlayVideo
52 this.createdAt = hash.createdAt
53 63
54 this.theme = hash.theme 64 this.theme = hash.theme
55 65
@@ -58,8 +68,13 @@ export class User implements UserServerModel {
58 this.blocked = hash.blocked 68 this.blocked = hash.blocked
59 this.blockedReason = hash.blockedReason 69 this.blockedReason = hash.blockedReason
60 70
71 this.noInstanceConfigWarningModal = hash.noInstanceConfigWarningModal
72 this.noWelcomeModal = hash.noWelcomeModal
73
61 this.notificationSettings = hash.notificationSettings 74 this.notificationSettings = hash.notificationSettings
62 75
76 this.createdAt = hash.createdAt
77
63 if (hash.account !== undefined) { 78 if (hash.account !== undefined) {
64 this.account = new Account(hash.account) 79 this.account = new Account(hash.account)
65 } 80 }
diff --git a/client/src/app/shared/video/videos-selection.component.ts b/client/src/app/shared/video/videos-selection.component.ts
index 994e0fa1e..064420056 100644
--- a/client/src/app/shared/video/videos-selection.component.ts
+++ b/client/src/app/shared/video/videos-selection.component.ts
@@ -35,7 +35,7 @@ export class VideosSelectionComponent extends AbstractVideoList implements OnIni
35 @Input() titlePage: string 35 @Input() titlePage: string
36 @Input() miniatureDisplayOptions: MiniatureDisplayOptions 36 @Input() miniatureDisplayOptions: MiniatureDisplayOptions
37 @Input() getVideosObservableFunction: (page: number, sort?: VideoSortField) => Observable<ResultList<Video>> 37 @Input() getVideosObservableFunction: (page: number, sort?: VideoSortField) => Observable<ResultList<Video>>
38 @ContentChildren(PeerTubeTemplateDirective) templates: QueryList<PeerTubeTemplateDirective> 38 @ContentChildren(PeerTubeTemplateDirective) templates: QueryList<PeerTubeTemplateDirective<'rowButtons' | 'globalButtons'>>
39 39
40 @Output() selectionChange = new EventEmitter<SelectionType>() 40 @Output() selectionChange = new EventEmitter<SelectionType>()
41 @Output() videosModelChange = new EventEmitter<Video[]>() 41 @Output() videosModelChange = new EventEmitter<Video[]>()
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.html b/client/src/app/videos/+video-edit/shared/video-edit.component.html
index 217cadc66..245ae42b6 100644
--- a/client/src/app/videos/+video-edit/shared/video-edit.component.html
+++ b/client/src/app/videos/+video-edit/shared/video-edit.component.html
@@ -15,7 +15,16 @@
15 15
16 <div class="form-group"> 16 <div class="form-group">
17 <label i18n class="label-tags">Tags</label> 17 <label i18n class="label-tags">Tags</label>
18 <my-help i18n-preHtml preHtml="Tags could be used to suggest relevant recommendations.</br>Press Enter to add a new tag."></my-help> 18
19 <my-help>
20 <ng-template ptTemplate="customHtml">
21 <ng-container i18n>
22 Tags could be used to suggest relevant recommendations. <br />
23 Press Enter to add a new tag.
24 </ng-container>
25 </ng-template>
26 </my-help>
27
19 <tag-input 28 <tag-input
20 [validators]="tagValidators" [errorMessages]="tagValidatorsMessages" 29 [validators]="tagValidators" [errorMessages]="tagValidatorsMessages"
21 i18n-placeholder placeholder="+ Tag" i18n-secondaryPlaceholder secondaryPlaceholder="Enter a new tag" 30 i18n-placeholder placeholder="+ Tag" i18n-secondaryPlaceholder secondaryPlaceholder="Enter a new tag"
@@ -25,7 +34,15 @@
25 34
26 <div class="form-group"> 35 <div class="form-group">
27 <label i18n for="description">Description</label> 36 <label i18n for="description">Description</label>
28 <my-help helpType="markdownText" i18n-preHtml preHtml="Video descriptions are truncated by default and require manual action to expand them."></my-help> 37
38 <my-help helpType="markdownText">
39 <ng-template ptTemplate="preHtml">
40 <ng-container i18n>
41 Video descriptions are truncated by default and require manual action to expand them.
42 </ng-container>
43 </ng-template>
44 </my-help>
45
29 <my-markdown-textarea truncate="250" formControlName="description"></my-markdown-textarea> 46 <my-markdown-textarea truncate="250" formControlName="description"></my-markdown-textarea>
30 47
31 <div *ngIf="formErrors.description" class="form-error"> 48 <div *ngIf="formErrors.description" class="form-error">
@@ -114,20 +131,25 @@
114 </div> 131 </div>
115 </div> 132 </div>
116 133
117 <my-peertube-checkbox 134 <my-peertube-checkbox inputName="nsfw" formControlName="nsfw" helpPlacement="bottom-right">
118 inputName="nsfw" formControlName="nsfw" 135 <ng-template ptTemplate="label">
119 i18n-labelText labelText="This video contains mature or explicit content" 136 <ng-container i18n>This video contains mature or explicit content</ng-container>
120 i18n-helpHtml helpHtml="Some instances do not list videos containing mature or explicit content by default." 137 </ng-template>
121 helpPlacement="bottom-right" 138
122 ></my-peertube-checkbox> 139 <ng-template ptTemplate="help">
123 140 <ng-container i18n>Some instances do not list videos containing mature or explicit content by default.</ng-container>
124 <my-peertube-checkbox 141 </ng-template>
125 *ngIf="waitTranscodingEnabled" 142 </my-peertube-checkbox>
126 inputName="waitTranscoding" formControlName="waitTranscoding" 143
127 i18n-labelText labelText="Wait transcoding before publishing the video" 144 <my-peertube-checkbox *ngIf="waitTranscodingEnabled" inputName="waitTranscoding" formControlName="waitTranscoding" helpPlacement="bottom-right">
128 i18n-helpHtml helpHtml="If you decide not to wait for transcoding before publishing the video, it could be unplayable until transcoding ends." 145 <ng-template ptTemplate="label">
129 helpPlacement="bottom-right" 146 <ng-container i18n>Wait transcoding before publishing the video</ng-container>
130 ></my-peertube-checkbox> 147 </ng-template>
148
149 <ng-template ptTemplate="help">
150 <ng-container i18n>If you decide not to wait for transcoding before publishing the video, it could be unplayable until transcoding ends.</ng-container>
151 </ng-template>
152 </my-peertube-checkbox>
131 153
132 </div> 154 </div>
133 </div> 155 </div>
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html
index 7a495fea5..c290fd4b1 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html
+++ b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html
@@ -12,10 +12,14 @@
12 12
13 <div class="form-group form-group-magnet-uri"> 13 <div class="form-group form-group-magnet-uri">
14 <label i18n for="magnetUri">Paste magnet URI</label> 14 <label i18n for="magnetUri">Paste magnet URI</label>
15 <my-help 15 <my-help>
16 helpType="custom" i18n-customHtml 16 <ng-template ptTemplate="customHtml">
17 customHtml="You can import any torrent file that points to a mp4 file. You should make sure you have diffusion rights over the content it points to, otherwise it could cause legal trouble to yourself and your instance." 17 <ng-container i18n>
18 ></my-help> 18 You can import any torrent file that points to a mp4 file.
19 You should make sure you have diffusion rights over the content it points to, otherwise it could cause legal trouble to yourself and your instance.
20 </ng-container>
21 </ng-template>
22 </my-help>
19 23
20 <input type="text" id="magnetUri" [(ngModel)]="magnetUri" /> 24 <input type="text" id="magnetUri" [(ngModel)]="magnetUri" />
21 </div> 25 </div>
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html
index e4f19faa8..09d0b8272 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html
+++ b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html
@@ -4,10 +4,16 @@
4 4
5 <div class="form-group"> 5 <div class="form-group">
6 <label i18n for="targetUrl">URL</label> 6 <label i18n for="targetUrl">URL</label>
7 <my-help 7
8 helpType="custom" i18n-customHtml 8 <my-help>
9 customHtml="You can import any URL <a href='https://rg3.github.io/youtube-dl/supportedsites.html' target='_blank' rel='noopener noreferrer'>supported by youtube-dl</a> or URL that points to a raw MP4 file. You should make sure you have diffusion rights over the content it points to, otherwise it could cause legal trouble to yourself and your instance." 9 <ng-template ptTemplate="customHtml">
10 ></my-help> 10 <ng-container i18n>
11 You can import any URL <a href='https://rg3.github.io/youtube-dl/supportedsites.html' target='_blank' rel='noopener noreferrer'>supported by youtube-dl</a>
12 or URL that points to a raw MP4 file.
13 You should make sure you have diffusion rights over the content it points to, otherwise it could cause legal trouble to yourself and your instance.
14 </ng-container>
15 </ng-template>
16 </my-help>
11 17
12 <input type="text" id="targetUrl" [(ngModel)]="targetUrl" /> 18 <input type="text" id="targetUrl" [(ngModel)]="targetUrl" />
13 </div> 19 </div>
diff --git a/client/src/assets/images/framasoft.png b/client/src/assets/images/framasoft.png
new file mode 100644
index 000000000..57be8c219
--- /dev/null
+++ b/client/src/assets/images/framasoft.png
Binary files differ
diff --git a/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts b/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts
index 0c8c612ee..c44c184d5 100644
--- a/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts
+++ b/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts
@@ -92,7 +92,7 @@ class P2pMediaLoaderPlugin extends Plugin {
92 this.p2pEngine.on(Events.SegmentError, (segment: Segment, err) => { 92 this.p2pEngine.on(Events.SegmentError, (segment: Segment, err) => {
93 console.error('Segment error.', segment, err) 93 console.error('Segment error.', segment, err)
94 94
95 this.options.redundancyUrlManager.removeByOriginUrl(segment.url) 95 this.options.redundancyUrlManager.removeBySegmentUrl(segment.requestUrl)
96 }) 96 })
97 97
98 this.statsP2PBytes.numPeers = 1 + this.options.redundancyUrlManager.countBaseUrls() 98 this.statsP2PBytes.numPeers = 1 + this.options.redundancyUrlManager.countBaseUrls()
diff --git a/client/src/assets/player/p2p-media-loader/redundancy-url-manager.ts b/client/src/assets/player/p2p-media-loader/redundancy-url-manager.ts
index 7fc2b6ab1..abab8aa99 100644
--- a/client/src/assets/player/p2p-media-loader/redundancy-url-manager.ts
+++ b/client/src/assets/player/p2p-media-loader/redundancy-url-manager.ts
@@ -2,9 +2,6 @@ import { basename, dirname } from 'path'
2 2
3class RedundancyUrlManager { 3class RedundancyUrlManager {
4 4
5 // Remember by what new URL we replaced an origin URL
6 private replacedSegmentUrls: { [originUrl: string]: string } = {}
7
8 constructor (private baseUrls: string[] = []) { 5 constructor (private baseUrls: string[] = []) {
9 // empty 6 // empty
10 } 7 }
@@ -17,16 +14,7 @@ class RedundancyUrlManager {
17 this.baseUrls = this.baseUrls.filter(u => u !== baseUrl && u !== baseUrl + '/') 14 this.baseUrls = this.baseUrls.filter(u => u !== baseUrl && u !== baseUrl + '/')
18 } 15 }
19 16
20 removeByOriginUrl (originUrl: string) {
21 const replaced = this.replacedSegmentUrls[originUrl]
22 if (!replaced) return
23
24 return this.removeBySegmentUrl(replaced)
25 }
26
27 buildUrl (url: string) { 17 buildUrl (url: string) {
28 delete this.replacedSegmentUrls[url]
29
30 const max = this.baseUrls.length + 1 18 const max = this.baseUrls.length + 1
31 const i = this.getRandomInt(max) 19 const i = this.getRandomInt(max)
32 20
@@ -35,10 +23,7 @@ class RedundancyUrlManager {
35 const newBaseUrl = this.baseUrls[i] 23 const newBaseUrl = this.baseUrls[i]
36 const slashPart = newBaseUrl.endsWith('/') ? '' : '/' 24 const slashPart = newBaseUrl.endsWith('/') ? '' : '/'
37 25
38 const newUrl = newBaseUrl + slashPart + basename(url) 26 return newBaseUrl + slashPart + basename(url)
39 this.replacedSegmentUrls[url] = newUrl
40
41 return newUrl
42 } 27 }
43 28
44 countBaseUrls () { 29 countBaseUrls () {
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss
index abbc137b2..26ba490c7 100644
--- a/client/src/sass/include/_mixins.scss
+++ b/client/src/sass/include/_mixins.scss
@@ -343,6 +343,7 @@
343 & + span { 343 & + span {
344 position: relative; 344 position: relative;
345 width: 18px; 345 width: 18px;
346 min-width: 18px;
346 height: 18px; 347 height: 18px;
347 border: $border-width solid var(--mainForegroundColor); 348 border: $border-width solid var(--mainForegroundColor);
348 border-radius: 3px; 349 border-radius: 3px;
@@ -395,6 +396,7 @@
395 border-radius: 50%; 396 border-radius: 50%;
396 width: $size; 397 width: $size;
397 height: $size; 398 height: $size;
399 min-width: $size;
398} 400}
399 401
400@mixin chevron ($size, $border-width) { 402@mixin chevron ($size, $border-width) {
diff --git a/client/src/sass/player/peertube-skin.scss b/client/src/sass/player/peertube-skin.scss
index 1a5144b11..4bf48a570 100644
--- a/client/src/sass/player/peertube-skin.scss
+++ b/client/src/sass/player/peertube-skin.scss
@@ -26,11 +26,6 @@ body {
26 .vjs-dock-description { 26 .vjs-dock-description {
27 font-size: 11px; 27 font-size: 11px;
28 28
29 .text::before, .text::after {
30 display: inline-block;
31 content: '\1F308';
32 }
33
34 .text::before { 29 .text::before {
35 margin-right: 4px; 30 margin-right: 4px;
36 } 31 }
diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts
index 6ff3efef1..19d2a1d02 100644
--- a/client/src/standalone/videos/embed.ts
+++ b/client/src/standalone/videos/embed.ts
@@ -239,7 +239,7 @@ export class PeerTubeEmbed {
239 239
240 const config: ServerConfig = await configResponse.json() 240 const config: ServerConfig = await configResponse.json()
241 const description = config.tracker.enabled && this.warningTitle 241 const description = config.tracker.enabled && this.warningTitle
242 ? '<span class="text">' + this.player.localize('Uses P2P, others may know your IP is downloading this video.') + '</span>' 242 ? '<span class="text">' + this.player.localize('Watching this video may reveal your IP address to others.') + '</span>'
243 : undefined 243 : undefined
244 244
245 this.player.dock({ 245 this.player.dock({
diff --git a/client/yarn.lock b/client/yarn.lock
index a67ffe6d1..6755d7e64 100644
--- a/client/yarn.lock
+++ b/client/yarn.lock
@@ -1139,6 +1139,11 @@ async-foreach@^0.1.3:
1139 resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542" 1139 resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542"
1140 integrity sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI= 1140 integrity sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=
1141 1141
1142async-limiter@^1.0.0:
1143 version "1.0.1"
1144 resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
1145 integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
1146
1142async-limiter@~1.0.0: 1147async-limiter@~1.0.0:
1143 version "1.0.0" 1148 version "1.0.0"
1144 resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" 1149 resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8"
@@ -1433,7 +1438,7 @@ bittorrent-protocol@^3.0.0:
1433 speedometer "^1.0.0" 1438 speedometer "^1.0.0"
1434 unordered-array-remove "^1.0.2" 1439 unordered-array-remove "^1.0.2"
1435 1440
1436bittorrent-tracker@^9.0.0, bittorrent-tracker@^9.11.0: 1441bittorrent-tracker@^9.0.0:
1437 version "9.11.0" 1442 version "9.11.0"
1438 resolved "https://registry.yarnpkg.com/bittorrent-tracker/-/bittorrent-tracker-9.11.0.tgz#9911f9c14e5a29f84990a0c31b3d83dd16eb2876" 1443 resolved "https://registry.yarnpkg.com/bittorrent-tracker/-/bittorrent-tracker-9.11.0.tgz#9911f9c14e5a29f84990a0c31b3d83dd16eb2876"
1439 integrity sha512-T1zvW/kSeEnWT4I3JE+6c7aZbO5jtleZyQe911SyzIxFF9DvtUNWXud3p5ZUkXaoI2xXwfpvlks5VFj5SKEB+A== 1444 integrity sha512-T1zvW/kSeEnWT4I3JE+6c7aZbO5jtleZyQe911SyzIxFF9DvtUNWXud3p5ZUkXaoI2xXwfpvlks5VFj5SKEB+A==
@@ -1463,6 +1468,36 @@ bittorrent-tracker@^9.0.0, bittorrent-tracker@^9.11.0:
1463 bufferutil "^4.0.0" 1468 bufferutil "^4.0.0"
1464 utf-8-validate "^5.0.1" 1469 utf-8-validate "^5.0.1"
1465 1470
1471bittorrent-tracker@^9.14.4:
1472 version "9.14.4"
1473 resolved "https://registry.yarnpkg.com/bittorrent-tracker/-/bittorrent-tracker-9.14.4.tgz#0d9661560e6fec37689dfc5045142772eac05536"
1474 integrity sha512-2Y/MNRjYhysD6t4r38z7l1WTT7g23IAqRWZRsj7xnnpciFn4xE4qiKmyFwA4gtbFGAZ14K3DdaqZbiQsC3PEfQ==
1475 dependencies:
1476 bencode "^2.0.0"
1477 bittorrent-peerid "^1.0.2"
1478 bn.js "^5.0.0"
1479 chrome-dgram "^3.0.2"
1480 compact2string "^1.2.0"
1481 debug "^4.0.1"
1482 ip "^1.0.1"
1483 lru "^3.0.0"
1484 minimist "^1.1.1"
1485 once "^1.3.0"
1486 random-iterate "^1.0.1"
1487 randombytes "^2.0.3"
1488 run-parallel "^1.1.2"
1489 run-series "^1.0.2"
1490 simple-get "^3.0.0"
1491 simple-peer "^9.0.0"
1492 simple-websocket "^8.0.0"
1493 string2compact "^1.1.1"
1494 uniq "^1.0.1"
1495 unordered-array-remove "^1.0.2"
1496 ws "^7.0.0"
1497 optionalDependencies:
1498 bufferutil "^4.0.0"
1499 utf-8-validate "^5.0.1"
1500
1466blob-to-buffer@^1.2.6: 1501blob-to-buffer@^1.2.6:
1467 version "1.2.8" 1502 version "1.2.8"
1468 resolved "https://registry.yarnpkg.com/blob-to-buffer/-/blob-to-buffer-1.2.8.tgz#78eeeb332f1280ed0ca6fb2b60693a8c6d36903a" 1503 resolved "https://registry.yarnpkg.com/blob-to-buffer/-/blob-to-buffer-1.2.8.tgz#78eeeb332f1280ed0ca6fb2b60693a8c6d36903a"
@@ -1506,6 +1541,11 @@ bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
1506 resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" 1541 resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
1507 integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA== 1542 integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==
1508 1543
1544bn.js@^5.0.0:
1545 version "5.0.0"
1546 resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.0.0.tgz#5c3d398021b3ddb548c1296a16f857e908f35c70"
1547 integrity sha512-bVwDX8AF+72fIUNuARelKAlQUNtPOfG2fRxorbVvFk4zpHbqLrPdOGfVg5vrKwVzLLePqPBiATaOZNELQzmS0A==
1548
1509body-parser@1.19.0, body-parser@^1.16.1: 1549body-parser@1.19.0, body-parser@^1.16.1:
1510 version "1.19.0" 1550 version "1.19.0"
1511 resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" 1551 resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
@@ -1959,6 +1999,14 @@ chownr@^1.1.1:
1959 resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.2.tgz#a18f1e0b269c8a6a5d3c86eb298beb14c3dd7bf6" 1999 resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.2.tgz#a18f1e0b269c8a6a5d3c86eb298beb14c3dd7bf6"
1960 integrity sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A== 2000 integrity sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A==
1961 2001
2002chrome-dgram@^3.0.2:
2003 version "3.0.2"
2004 resolved "https://registry.yarnpkg.com/chrome-dgram/-/chrome-dgram-3.0.2.tgz#7e0e00084b57971714214372368ad18a7785ad52"
2005 integrity sha512-Ay741EHF/Ib18un+LUtBNK43NrabD6GOuwVaka7uUbV0gFRLEPULm2Q05YSzRNBtSrbaO4eErmDdniiy/u8Lig==
2006 dependencies:
2007 inherits "^2.0.1"
2008 run-series "^1.1.2"
2009
1962chrome-trace-event@^1.0.0: 2010chrome-trace-event@^1.0.0:
1963 version "1.0.2" 2011 version "1.0.2"
1964 resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4" 2012 resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4"
@@ -6488,26 +6536,26 @@ p-try@^2.0.0:
6488 resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" 6536 resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
6489 integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== 6537 integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
6490 6538
6491p2p-media-loader-core@^0.6.1: 6539p2p-media-loader-core@^0.6.2:
6492 version "0.6.1" 6540 version "0.6.2"
6493 resolved "https://registry.yarnpkg.com/p2p-media-loader-core/-/p2p-media-loader-core-0.6.1.tgz#90cc05460cb5207897953e92059b32930f06a56f" 6541 resolved "https://registry.yarnpkg.com/p2p-media-loader-core/-/p2p-media-loader-core-0.6.2.tgz#7e46cf8fc4357596f389e106bee850908cc974ef"
6494 integrity sha512-bTyOdTVxbjzr1GCt6bOIxXlw7U6gPvYXOGo07EU0wufabKscn/TNyuTH4fDhVtw6NGMISn18G06td3V049tOBw== 6542 integrity sha512-yspgCOrVVYitVNece5CA6W/kcVA0UybvbD4kyBE5ooyhCAXQK5/q6JsIpXiVQ3VkQw8Qs4mfZjU39Vt6vEk6aw==
6495 dependencies: 6543 dependencies:
6496 bittorrent-tracker "^9.11.0" 6544 bittorrent-tracker "^9.14.4"
6497 debug "^4.1.1" 6545 debug "^4.1.1"
6498 events "^3.0.0" 6546 events "^3.0.0"
6499 get-browser-rtc "^1.0.2" 6547 get-browser-rtc "^1.0.2"
6500 sha.js "^2.4.11" 6548 sha.js "^2.4.11"
6501 simple-peer "^9.4.0" 6549 simple-peer "^9.5.0"
6502 6550
6503p2p-media-loader-hlsjs@^0.6.1: 6551p2p-media-loader-hlsjs@^0.6.2:
6504 version "0.6.1" 6552 version "0.6.2"
6505 resolved "https://registry.yarnpkg.com/p2p-media-loader-hlsjs/-/p2p-media-loader-hlsjs-0.6.1.tgz#558e1737241f3c17810cddafde0e992c20656886" 6553 resolved "https://registry.yarnpkg.com/p2p-media-loader-hlsjs/-/p2p-media-loader-hlsjs-0.6.2.tgz#b66f977a5d28986c8f6e62d2ffa297aec3c05186"
6506 integrity sha512-JadTwrxNNKXyO4MyiK7i5zT1zOSFmaiIOlE4Gr6NjxDg8v3+Q8q09YHJPXumXexUWDNpw5vw8eHTpBdQClJ9lQ== 6554 integrity sha512-5LgqWPDsgyST9rxoHGDpExZU1rIDZIT0qft2wAnlg8Cb8aVeaBxUsmF4Sj692Qb5/GBDsi8vLE03LW8gpvlh1g==
6507 dependencies: 6555 dependencies:
6508 events "^3.0.0" 6556 events "^3.0.0"
6509 m3u8-parser "^4.4.0" 6557 m3u8-parser "^4.4.0"
6510 p2p-media-loader-core "^0.6.1" 6558 p2p-media-loader-core "^0.6.2"
6511 6559
6512package-json-versionify@^1.0.2: 6560package-json-versionify@^1.0.2:
6513 version "1.0.4" 6561 version "1.0.4"
@@ -7624,7 +7672,7 @@ run-queue@^1.0.0, run-queue@^1.0.3:
7624 dependencies: 7672 dependencies:
7625 aproba "^1.1.1" 7673 aproba "^1.1.1"
7626 7674
7627run-series@^1.0.2: 7675run-series@^1.0.2, run-series@^1.1.2:
7628 version "1.1.8" 7676 version "1.1.8"
7629 resolved "https://registry.yarnpkg.com/run-series/-/run-series-1.1.8.tgz#2c4558f49221e01cd6371ff4e0a1e203e460fc36" 7677 resolved "https://registry.yarnpkg.com/run-series/-/run-series-1.1.8.tgz#2c4558f49221e01cd6371ff4e0a1e203e460fc36"
7630 integrity sha512-+GztYEPRpIsQoCSraWHDBs9WVy4eVME16zhOtDB4H9J4xN0XRhknnmLOl+4gRgZtu8dpp9N/utSPjKH/xmDzXg== 7678 integrity sha512-+GztYEPRpIsQoCSraWHDBs9WVy4eVME16zhOtDB4H9J4xN0XRhknnmLOl+4gRgZtu8dpp9N/utSPjKH/xmDzXg==
@@ -7985,7 +8033,7 @@ simple-get@^2.8.1, simple-get@^3.0.0, simple-get@^3.0.1:
7985 once "^1.3.1" 8033 once "^1.3.1"
7986 simple-concat "^1.0.0" 8034 simple-concat "^1.0.0"
7987 8035
7988simple-peer@^9.0.0, simple-peer@^9.4.0: 8036simple-peer@^9.0.0:
7989 version "9.4.0" 8037 version "9.4.0"
7990 resolved "https://registry.yarnpkg.com/simple-peer/-/simple-peer-9.4.0.tgz#eb82ef1181e10ec0c014a94953e2eb278f3d9025" 8038 resolved "https://registry.yarnpkg.com/simple-peer/-/simple-peer-9.4.0.tgz#eb82ef1181e10ec0c014a94953e2eb278f3d9025"
7991 integrity sha512-8qF32uq6SSSVXoBq9g31uGqZYupwRD3Ta/QK9fV04U/IbnIS6mictLb8/kjFyLVa3JrD7QYyKrw3nvJJ+lNFDw== 8039 integrity sha512-8qF32uq6SSSVXoBq9g31uGqZYupwRD3Ta/QK9fV04U/IbnIS6mictLb8/kjFyLVa3JrD7QYyKrw3nvJJ+lNFDw==
@@ -7996,6 +8044,17 @@ simple-peer@^9.0.0, simple-peer@^9.4.0:
7996 randombytes "^2.0.3" 8044 randombytes "^2.0.3"
7997 readable-stream "^2.3.4" 8045 readable-stream "^2.3.4"
7998 8046
8047simple-peer@^9.5.0:
8048 version "9.5.0"
8049 resolved "https://registry.yarnpkg.com/simple-peer/-/simple-peer-9.5.0.tgz#67ba8bd4b54efc3acf19aceafdc118b27e24fcbc"
8050 integrity sha512-3tROq3nBo/CIZI8PWlXGbAxQIlQF6KQ/zcd4lQ2pAC4+rPiV7E721hI22nTO54uw/nzb2HKbvmDtZ4Wr173+vA==
8051 dependencies:
8052 debug "^4.0.1"
8053 get-browser-rtc "^1.0.0"
8054 inherits "^2.0.1"
8055 randombytes "^2.0.3"
8056 readable-stream "^3.4.0"
8057
7999simple-sha1@^2.0.0, simple-sha1@^2.0.8, simple-sha1@^2.1.0: 8058simple-sha1@^2.0.0, simple-sha1@^2.0.8, simple-sha1@^2.1.0:
8000 version "2.1.2" 8059 version "2.1.2"
8001 resolved "https://registry.yarnpkg.com/simple-sha1/-/simple-sha1-2.1.2.tgz#de40cbd5aae278fde8e3bb3250a35d74c67326b1" 8060 resolved "https://registry.yarnpkg.com/simple-sha1/-/simple-sha1-2.1.2.tgz#de40cbd5aae278fde8e3bb3250a35d74c67326b1"
@@ -8014,6 +8073,16 @@ simple-websocket@^7.0.1:
8014 readable-stream "^2.0.5" 8073 readable-stream "^2.0.5"
8015 ws "^6.0.0" 8074 ws "^6.0.0"
8016 8075
8076simple-websocket@^8.0.0:
8077 version "8.0.1"
8078 resolved "https://registry.yarnpkg.com/simple-websocket/-/simple-websocket-8.0.1.tgz#c28af779034b329d0cf1448a45fdd311d21fa289"
8079 integrity sha512-2QKSRjf+tqFXLVmOQjf95gHeKhuyx2k1ouDjtnE0uKCYw84HfN85HsXo+GmPH+2PIh5BQql++g2AIbHgGAZU4w==
8080 dependencies:
8081 debug "^4.1.1"
8082 randombytes "^2.0.3"
8083 readable-stream "^3.1.1"
8084 ws "^7.0.0"
8085
8017slash@^1.0.0: 8086slash@^1.0.0:
8018 version "1.0.0" 8087 version "1.0.0"
8019 resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" 8088 resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
@@ -9710,6 +9779,13 @@ ws@^6.0.0:
9710 dependencies: 9779 dependencies:
9711 async-limiter "~1.0.0" 9780 async-limiter "~1.0.0"
9712 9781
9782ws@^7.0.0:
9783 version "7.1.2"
9784 resolved "https://registry.yarnpkg.com/ws/-/ws-7.1.2.tgz#c672d1629de8bb27a9699eb599be47aeeedd8f73"
9785 integrity sha512-gftXq3XI81cJCgkUiAVixA0raD9IVmXqsylCrjRygw4+UOOGzPoxnQ6r/CnVL9i+mDncJo94tSkyrtuuQVBmrg==
9786 dependencies:
9787 async-limiter "^1.0.0"
9788
9713ws@~3.3.1: 9789ws@~3.3.1:
9714 version "3.3.3" 9790 version "3.3.3"
9715 resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2" 9791 resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2"
diff --git a/config/default.yaml b/config/default.yaml
index dfba23f59..5ebfdeddb 100644
--- a/config/default.yaml
+++ b/config/default.yaml
@@ -69,7 +69,7 @@ email:
69 69
70# From the project root directory 70# From the project root directory
71storage: 71storage:
72 tmp: 'storage/tmp/' # Used to download data (imports etc), store uploaded files before processing... 72 tmp: 'storage/tmp/' # Use to download data (imports etc), store uploaded files before processing...
73 avatars: 'storage/avatars/' 73 avatars: 'storage/avatars/'
74 videos: 'storage/videos/' 74 videos: 'storage/videos/'
75 streaming_playlists: 'storage/streaming-playlists/' 75 streaming_playlists: 'storage/streaming-playlists/'
@@ -85,7 +85,7 @@ storage:
85log: 85log:
86 level: 'info' # debug/info/warning/error 86 level: 'info' # debug/info/warning/error
87 rotation: 87 rotation:
88 enabled : true # Enabled by default, if disabled make sure that 'storage.logs' is pointing to a folder handled by logrotate 88 enabled : true
89 89
90search: 90search:
91 # Add ability to fetch remote videos/actors by their URI, that may not be federated with your instance 91 # Add ability to fetch remote videos/actors by their URI, that may not be federated with your instance
@@ -238,7 +238,60 @@ instance:
238 short_description: 'PeerTube, a federated (ActivityPub) video streaming platform using P2P (BitTorrent) directly in the web browser with WebTorrent and Angular.' 238 short_description: 'PeerTube, a federated (ActivityPub) video streaming platform using P2P (BitTorrent) directly in the web browser with WebTorrent and Angular.'
239 description: 'Welcome to this PeerTube instance!' # Support markdown 239 description: 'Welcome to this PeerTube instance!' # Support markdown
240 terms: 'No terms for now.' # Support markdown 240 terms: 'No terms for now.' # Support markdown
241 code_of_conduct: '' # Supports markdown
242
243 # Who moderates the instance? What is the policy regarding NSFW videos? Political videos? etc
244 moderation_information: '' # Supports markdown
245
246 # Why did you create this instance?
247 creation_reason: ''
248
249 # Who is behind the instance? A single person? A non profit?
250 administrator: ''
251
252 # How long do you plan to maintain this instance?
253 maintenance_lifetime: ''
254
255 # How will you pay the PeerTube instance server? With you own funds? With users donations? Advertising?
256 business_model: ''
257
258 # If you want to explain on what type of hardware your PeerTube instance runs
259 # Example: "2 vCore, 2GB RAM..."
260 hardware_information: '' # Supports Markdown
261
262 # What are the main languages of your instance? To interact with your users for example
263 # Uncomment or add the languages you want
264 # List of supported languages: https://peertube.cpy.re/api/v1/videos/languages
265 languages:
266# - en
267# - es
268# - fr
269
270 # You can specify the main categories of your instance (dedicated to music, gaming or politics etc)
271 # Uncomment or add the category ids you want
272 # List of supported categories: https://peertube.cpy.re/api/v1/videos/categories
273 categories:
274# - 1 # Music
275# - 2 # Films
276# - 3 # Vehicles
277# - 4 # Art
278# - 5 # Sports
279# - 6 # Travels
280# - 7 # Gaming
281# - 8 # People
282# - 9 # Comedy
283# - 10 # Entertainment
284# - 11 # News & Politics
285# - 12 # How To
286# - 13 # Education
287# - 14 # Activism
288# - 15 # Science & Technology
289# - 16 # Animals
290# - 17 # Kids
291# - 18 # Food
292
241 default_client_route: '/videos/trending' 293 default_client_route: '/videos/trending'
294
242 # Whether or not the instance is dedicated to NSFW content 295 # Whether or not the instance is dedicated to NSFW content
243 # Enabling it will allow other administrators to know that you are mainly federating sensitive content 296 # Enabling it will allow other administrators to know that you are mainly federating sensitive content
244 # Moreover, the NSFW checkbox on video upload will be automatically checked by default 297 # Moreover, the NSFW checkbox on video upload will be automatically checked by default
@@ -246,6 +299,7 @@ instance:
246 # By default, "do_not_list" or "blur" or "display" NSFW videos 299 # By default, "do_not_list" or "blur" or "display" NSFW videos
247 # Could be overridden per user with a setting 300 # Could be overridden per user with a setting
248 default_nsfw_policy: 'do_not_list' 301 default_nsfw_policy: 'do_not_list'
302
249 customizations: 303 customizations:
250 javascript: '' # Directly your JavaScript code (without <script> tags). Will be eval at runtime 304 javascript: '' # Directly your JavaScript code (without <script> tags). Will be eval at runtime
251 css: '' # Directly your CSS code (without <style> tags). Will be injected at runtime 305 css: '' # Directly your CSS code (without <style> tags). Will be injected at runtime
@@ -273,5 +327,21 @@ followers:
273 # Whether or not an administrator must manually validate a new follower 327 # Whether or not an administrator must manually validate a new follower
274 manual_approval: false 328 manual_approval: false
275 329
330followings:
331 instance:
332 # If you want to automatically follow back new instance followers
333 # Only follows accepted followers (in case you enabled manual followers approbation)
334 # If this option is enabled, use the mute feature instead of deleting followings
335 # /!\ Don't enable this if you don't have a reactive moderation team /!\
336 auto_follow_back:
337 enabled: false
338
339 # If you want to automatically follow instances of the public index
340 # If this option is enabled, use the mute feature instead of deleting followings
341 # /!\ Don't enable this if you don't have a reactive moderation team /!\
342 auto_follow_index:
343 enabled: false
344 index_url: 'https://instances.joinpeertube.org'
345
276theme: 346theme:
277 default: 'default' 347 default: 'default'
diff --git a/config/production.yaml.example b/config/production.yaml.example
index 267186e08..96d676a35 100644
--- a/config/production.yaml.example
+++ b/config/production.yaml.example
@@ -70,11 +70,11 @@ email:
70 70
71# From the project root directory 71# From the project root directory
72storage: 72storage:
73 tmp: '/var/www/peertube/storage/tmp/' # Used to download data (imports etc), store uploaded files before processing... 73 tmp: '/var/www/peertube/storage/tmp/' # Use to download data (imports etc), store uploaded files before processing...
74 avatars: '/var/www/peertube/storage/avatars/' 74 avatars: '/var/www/peertube/storage/avatars/'
75 videos: '/var/www/peertube/storage/videos/' 75 videos: '/var/www/peertube/storage/videos/'
76 streaming_playlists: '/var/www/peertube/storage/streaming-playlists/' 76 streaming_playlists: '/var/www/peertube/storage/streaming-playlists/'
77 redundancy: '/var/www/peertube/storage/videos/' 77 redundancy: '/var/www/peertube/storage/redundancy/'
78 logs: '/var/www/peertube/storage/logs/' 78 logs: '/var/www/peertube/storage/logs/'
79 previews: '/var/www/peertube/storage/previews/' 79 previews: '/var/www/peertube/storage/previews/'
80 thumbnails: '/var/www/peertube/storage/thumbnails/' 80 thumbnails: '/var/www/peertube/storage/thumbnails/'
@@ -86,7 +86,7 @@ storage:
86log: 86log:
87 level: 'info' # debug/info/warning/error 87 level: 'info' # debug/info/warning/error
88 rotation: 88 rotation:
89 enabled : true 89 enabled : true # Enabled by default, if disabled make sure that 'storage.logs' is pointing to a folder handled by logrotate
90 90
91search: 91search:
92 # Add ability to fetch remote videos/actors by their URI, that may not be federated with your instance 92 # Add ability to fetch remote videos/actors by their URI, that may not be federated with your instance
@@ -157,8 +157,8 @@ views:
157 max_age: -1 157 max_age: -1
158 158
159plugins: 159plugins:
160 # The website PeerTube will ask for available PeerTube plugins 160 # The website PeerTube will ask for available PeerTube plugins and themes
161 # This is an unmoderated plugin index, so only install plugins you trust 161 # This is an unmoderated plugin index, so only install plugins/themes you trust
162 index: 162 index:
163 enabled: true 163 enabled: true
164 check_latest_versions_interval: '12 hours' # How often you want to check new plugins/themes versions 164 check_latest_versions_interval: '12 hours' # How often you want to check new plugins/themes versions
@@ -251,9 +251,62 @@ auto_blacklist:
251instance: 251instance:
252 name: 'PeerTube' 252 name: 'PeerTube'
253 short_description: 'PeerTube, a federated (ActivityPub) video streaming platform using P2P (BitTorrent) directly in the web browser with WebTorrent and Angular.' 253 short_description: 'PeerTube, a federated (ActivityPub) video streaming platform using P2P (BitTorrent) directly in the web browser with WebTorrent and Angular.'
254 description: '' # Support markdown 254 description: 'Welcome to this PeerTube instance!' # Support markdown
255 terms: '' # Support markdown 255 terms: 'No terms for now.' # Support markdown
256 code_of_conduct: '' # Supports markdown
257
258 # Who moderates the instance? What is the policy regarding NSFW videos? Political videos? etc
259 moderation_information: '' # Supports markdown
260
261 # Why did you create this instance?
262 creation_reason: ''
263
264 # Who is behind the instance? A single person? A non profit?
265 administrator: ''
266
267 # How long do you plan to maintain this instance?
268 maintenance_lifetime: ''
269
270 # How will you pay the PeerTube instance server? With you own funds? With users donations? Advertising?
271 business_model: ''
272
273 # If you want to explain on what type of hardware your PeerTube instance runs
274 # Example: "2 vCore, 2GB RAM..."
275 hardware_information: '' # Supports Markdown
276
277 # What are the main languages of your instance? To interact with your users for example
278 # Uncomment or add the languages you want
279 # List of supported languages: https://peertube.cpy.re/api/v1/videos/languages
280 languages:
281# - en
282# - es
283# - fr
284
285 # You can specify the main categories of your instance (dedicated to music, gaming or politics etc)
286 # Uncomment or add the category ids you want
287 # List of supported categories: https://peertube.cpy.re/api/v1/videos/categories
288 categories:
289# - 1 # Music
290# - 2 # Films
291# - 3 # Vehicles
292# - 4 # Art
293# - 5 # Sports
294# - 6 # Travels
295# - 7 # Gaming
296# - 8 # People
297# - 9 # Comedy
298# - 10 # Entertainment
299# - 11 # News & Politics
300# - 12 # How To
301# - 13 # Education
302# - 14 # Activism
303# - 15 # Science & Technology
304# - 16 # Animals
305# - 17 # Kids
306# - 18 # Food
307
256 default_client_route: '/videos/trending' 308 default_client_route: '/videos/trending'
309
257 # Whether or not the instance is dedicated to NSFW content 310 # Whether or not the instance is dedicated to NSFW content
258 # Enabling it will allow other administrators to know that you are mainly federating sensitive content 311 # Enabling it will allow other administrators to know that you are mainly federating sensitive content
259 # Moreover, the NSFW checkbox on video upload will be automatically checked by default 312 # Moreover, the NSFW checkbox on video upload will be automatically checked by default
@@ -261,6 +314,7 @@ instance:
261 # By default, "do_not_list" or "blur" or "display" NSFW videos 314 # By default, "do_not_list" or "blur" or "display" NSFW videos
262 # Could be overridden per user with a setting 315 # Could be overridden per user with a setting
263 default_nsfw_policy: 'do_not_list' 316 default_nsfw_policy: 'do_not_list'
317
264 customizations: 318 customizations:
265 javascript: '' # Directly your JavaScript code (without <script> tags). Will be eval at runtime 319 javascript: '' # Directly your JavaScript code (without <script> tags). Will be eval at runtime
266 css: '' # Directly your CSS code (without <style> tags). Will be injected at runtime 320 css: '' # Directly your CSS code (without <style> tags). Will be injected at runtime
@@ -278,7 +332,7 @@ services:
278 username: '@Chocobozzz' # Indicates the Twitter account for the website or platform on which the content was published 332 username: '@Chocobozzz' # Indicates the Twitter account for the website or platform on which the content was published
279 # If true, a video player will be embedded in the Twitter feed on PeerTube video share 333 # If true, a video player will be embedded in the Twitter feed on PeerTube video share
280 # If false, we use an image link card that will redirect on your PeerTube instance 334 # If false, we use an image link card that will redirect on your PeerTube instance
281 # Test on https://cards-dev.twitter.com/validator to see if you are whitelisted 335 # Change it to "true", and then test on https://cards-dev.twitter.com/validator to see if you are whitelisted
282 whitelisted: false 336 whitelisted: false
283 337
284followers: 338followers:
@@ -288,5 +342,20 @@ followers:
288 # Whether or not an administrator must manually validate a new follower 342 # Whether or not an administrator must manually validate a new follower
289 manual_approval: false 343 manual_approval: false
290 344
345followings:
346 instance:
347 # If you want to automatically follow back new instance followers
348 # If this option is enabled, use the mute feature instead of deleting followings
349 # /!\ Don't enable this if you don't have a reactive moderation team /!\
350 auto_follow_back:
351 enabled: false
352
353 # If you want to automatically follow instances of the public index
354 # If this option is enabled, use the mute feature instead of deleting followings
355 # /!\ Don't enable this if you don't have a reactive moderation team /!\
356 auto_follow_index:
357 enabled: false
358 index_url: 'https://instances.joinpeertube.org'
359
291theme: 360theme:
292 default: 'default' 361 default: 'default'
diff --git a/package.json b/package.json
index ce689a4b3..38c40bcf0 100644
--- a/package.json
+++ b/package.json
@@ -128,11 +128,11 @@
128 "iso-639-3": "^1.0.1", 128 "iso-639-3": "^1.0.1",
129 "js-yaml": "^3.5.4", 129 "js-yaml": "^3.5.4",
130 "jsonld": "~1.1.0", 130 "jsonld": "~1.1.0",
131 "jsonld-signatures": "https://github.com/Chocobozzz/jsonld-signatures#rsa2017",
132 "lodash": "^4.17.10", 131 "lodash": "^4.17.10",
133 "lru-cache": "^5.1.1", 132 "lru-cache": "^5.1.1",
134 "magnet-uri": "^5.1.4", 133 "magnet-uri": "^5.1.4",
135 "memoizee": "^0.4.14", 134 "memoizee": "^0.4.14",
135 "module-alias": "^2.2.1",
136 "morgan": "^1.5.3", 136 "morgan": "^1.5.3",
137 "multer": "^1.1.0", 137 "multer": "^1.1.0",
138 "nodemailer": "^6.0.0", 138 "nodemailer": "^6.0.0",
@@ -225,5 +225,8 @@
225 "scripty": { 225 "scripty": {
226 "silent": true 226 "silent": true
227 }, 227 },
228 "sasslintConfig": "client/.sass-lint.yml" 228 "sasslintConfig": "client/.sass-lint.yml",
229 "_moduleAliases": {
230 "@server": "dist/server"
231 }
229} 232}
diff --git a/scripts/client-report.sh b/scripts/client-report.sh
index df7ccda27..a758a211c 100755
--- a/scripts/client-report.sh
+++ b/scripts/client-report.sh
@@ -5,5 +5,5 @@ set -eu
5gawk -i inplace 'BEGIN { found=0 } { if (found || $0 ~ /^{/) { found=1; print }}' ./client/dist/embed-stats.json 5gawk -i inplace 'BEGIN { found=0 } { if (found || $0 ~ /^{/) { found=1; print }}' ./client/dist/embed-stats.json
6 6
7npm run concurrently -- -k \ 7npm run concurrently -- -k \
8 "cd client && npm run webpack-bundle-analyzer -- -p 8888 ./dist/en_US/stats.json" \ 8 "cd client && npm run webpack-bundle-analyzer -- -p 8888 ./dist/en_US/stats-es2015.json" \
9 "cd client && npm run webpack-bundle-analyzer -- -p 8889 ./dist/embed-stats.json" \ No newline at end of file 9 "cd client && npm run webpack-bundle-analyzer -- -p 8889 ./dist/embed-stats.json"
diff --git a/scripts/generate-code-contributors.ts b/scripts/generate-code-contributors.ts
index 0d6266056..c745b1cb2 100755
--- a/scripts/generate-code-contributors.ts
+++ b/scripts/generate-code-contributors.ts
@@ -13,7 +13,7 @@ async function run () {
13 { 13 {
14 const contributors = await fetchGithub('https://api.github.com/repos/chocobozzz/peertube/contributors') 14 const contributors = await fetchGithub('https://api.github.com/repos/chocobozzz/peertube/contributors')
15 15
16 console.log('# Code\n') 16 console.log('# Code contributors\n')
17 for (const contributor of contributors) { 17 for (const contributor of contributors) {
18 const contributorUrl = contributor.url.replace('api.github.com/users', 'github.com') 18 const contributorUrl = contributor.url.replace('api.github.com/users', 'github.com')
19 console.log(` * [${contributor.login}](${contributorUrl})`) 19 console.log(` * [${contributor.login}](${contributorUrl})`)
@@ -27,7 +27,7 @@ async function run () {
27 27
28 const translators = await fetchZanata(zanataUsername, zanataToken) 28 const translators = await fetchZanata(zanataUsername, zanataToken)
29 29
30 console.log('\n\n# Translations\n') 30 console.log('\n\n# Translation contributors\n')
31 for (const translator of translators) { 31 for (const translator of translators) {
32 console.log(` * [${translator.username}](https://trad.framasoft.org/zanata/profile/view/${translator.username})`) 32 console.log(` * [${translator.username}](https://trad.framasoft.org/zanata/profile/view/${translator.username})`)
33 } 33 }
diff --git a/server.ts b/server.ts
index 50511a906..5cfa09445 100644
--- a/server.ts
+++ b/server.ts
@@ -1,3 +1,5 @@
1require('module-alias/register')
2
1// FIXME: https://github.com/nodejs/node/pull/16853 3// FIXME: https://github.com/nodejs/node/pull/16853
2import { PluginManager } from './server/lib/plugins/plugin-manager' 4import { PluginManager } from './server/lib/plugins/plugin-manager'
3 5
@@ -113,6 +115,7 @@ import { UpdateVideosScheduler } from './server/lib/schedulers/update-videos-sch
113import { YoutubeDlUpdateScheduler } from './server/lib/schedulers/youtube-dl-update-scheduler' 115import { YoutubeDlUpdateScheduler } from './server/lib/schedulers/youtube-dl-update-scheduler'
114import { VideosRedundancyScheduler } from './server/lib/schedulers/videos-redundancy-scheduler' 116import { VideosRedundancyScheduler } from './server/lib/schedulers/videos-redundancy-scheduler'
115import { RemoveOldHistoryScheduler } from './server/lib/schedulers/remove-old-history-scheduler' 117import { RemoveOldHistoryScheduler } from './server/lib/schedulers/remove-old-history-scheduler'
118import { AutoFollowIndexInstances } from './server/lib/schedulers/auto-follow-index-instances'
116import { isHTTPSignatureDigestValid } from './server/helpers/peertube-crypto' 119import { isHTTPSignatureDigestValid } from './server/helpers/peertube-crypto'
117import { PeerTubeSocket } from './server/lib/peertube-socket' 120import { PeerTubeSocket } from './server/lib/peertube-socket'
118import { updateStreamingPlaylistsInfohashesIfNeeded } from './server/lib/hls' 121import { updateStreamingPlaylistsInfohashesIfNeeded } from './server/lib/hls'
@@ -258,6 +261,7 @@ async function startApplication () {
258 RemoveOldHistoryScheduler.Instance.enable() 261 RemoveOldHistoryScheduler.Instance.enable()
259 RemoveOldViewsScheduler.Instance.enable() 262 RemoveOldViewsScheduler.Instance.enable()
260 PluginsCheckScheduler.Instance.enable() 263 PluginsCheckScheduler.Instance.enable()
264 AutoFollowIndexInstances.Instance.enable()
261 265
262 // Redis initialization 266 // Redis initialization
263 Redis.Instance.init() 267 Redis.Instance.init()
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts
index 11504b354..453ced8bf 100644
--- a/server/controllers/activitypub/client.ts
+++ b/server/controllers/activitypub/client.ts
@@ -16,7 +16,6 @@ import {
16} from '../../middlewares' 16} from '../../middlewares'
17import { getAccountVideoRateValidator, videoCommentGetValidator } from '../../middlewares/validators' 17import { getAccountVideoRateValidator, videoCommentGetValidator } from '../../middlewares/validators'
18import { AccountModel } from '../../models/account/account' 18import { AccountModel } from '../../models/account/account'
19import { ActorModel } from '../../models/activitypub/actor'
20import { ActorFollowModel } from '../../models/activitypub/actor-follow' 19import { ActorFollowModel } from '../../models/activitypub/actor-follow'
21import { VideoModel } from '../../models/video/video' 20import { VideoModel } from '../../models/video/video'
22import { VideoCommentModel } from '../../models/video/video-comment' 21import { VideoCommentModel } from '../../models/video/video-comment'
@@ -38,6 +37,7 @@ import { buildDislikeActivity } from '../../lib/activitypub/send/send-dislike'
38import { videoPlaylistElementAPGetValidator, videoPlaylistsGetValidator } from '../../middlewares/validators/videos/video-playlists' 37import { videoPlaylistElementAPGetValidator, videoPlaylistsGetValidator } from '../../middlewares/validators/videos/video-playlists'
39import { VideoPlaylistModel } from '../../models/video/video-playlist' 38import { VideoPlaylistModel } from '../../models/video/video-playlist'
40import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' 39import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
40import { MAccountId, MActorId, MVideo, MVideoAPWithoutCaption } from '@server/typings/models'
41 41
42const activityPubClientRouter = express.Router() 42const activityPubClientRouter = express.Router()
43 43
@@ -148,7 +148,7 @@ activityPubClientRouter.get('/redundancy/streaming-playlists/:streamingPlaylistT
148 148
149activityPubClientRouter.get('/video-playlists/:playlistId', 149activityPubClientRouter.get('/video-playlists/:playlistId',
150 executeIfActivityPub, 150 executeIfActivityPub,
151 asyncMiddleware(videoPlaylistsGetValidator), 151 asyncMiddleware(videoPlaylistsGetValidator('all')),
152 asyncMiddleware(videoPlaylistController) 152 asyncMiddleware(videoPlaylistController)
153) 153)
154activityPubClientRouter.get('/video-playlists/:playlistId/:videoId', 154activityPubClientRouter.get('/video-playlists/:playlistId/:videoId',
@@ -208,18 +208,19 @@ function getAccountVideoRate (rateType: VideoRateType) {
208 208
209async function videoController (req: express.Request, res: express.Response) { 209async function videoController (req: express.Request, res: express.Response) {
210 // We need more attributes 210 // We need more attributes
211 const video = await VideoModel.loadForGetAPI({ id: res.locals.video.id }) 211 const video = await VideoModel.loadForGetAPI({ id: res.locals.onlyVideoWithRights.id }) as MVideoAPWithoutCaption
212 212
213 if (video.url.startsWith(WEBSERVER.URL) === false) return res.redirect(video.url) 213 if (video.url.startsWith(WEBSERVER.URL) === false) return res.redirect(video.url)
214 214
215 // We need captions to render AP object 215 // We need captions to render AP object
216 video.VideoCaptions = await VideoCaptionModel.listVideoCaptions(video.id) 216 const captions = await VideoCaptionModel.listVideoCaptions(video.id)
217 const videoWithCaptions = Object.assign(video, { VideoCaptions: captions })
217 218
218 const audience = getAudience(video.VideoChannel.Account.Actor, video.privacy === VideoPrivacy.PUBLIC) 219 const audience = getAudience(videoWithCaptions.VideoChannel.Account.Actor, videoWithCaptions.privacy === VideoPrivacy.PUBLIC)
219 const videoObject = audiencify(video.toActivityPubObject(), audience) 220 const videoObject = audiencify(videoWithCaptions.toActivityPubObject(), audience)
220 221
221 if (req.path.endsWith('/activity')) { 222 if (req.path.endsWith('/activity')) {
222 const data = buildCreateActivity(video.url, video.VideoChannel.Account.Actor, videoObject, audience) 223 const data = buildCreateActivity(videoWithCaptions.url, video.VideoChannel.Account.Actor, videoObject, audience)
223 return activityPubResponse(activityPubContextify(data), res) 224 return activityPubResponse(activityPubContextify(data), res)
224 } 225 }
225 226
@@ -231,13 +232,13 @@ async function videoAnnounceController (req: express.Request, res: express.Respo
231 232
232 if (share.url.startsWith(WEBSERVER.URL) === false) return res.redirect(share.url) 233 if (share.url.startsWith(WEBSERVER.URL) === false) return res.redirect(share.url)
233 234
234 const { activity } = await buildAnnounceWithVideoAudience(share.Actor, share, res.locals.video, undefined) 235 const { activity } = await buildAnnounceWithVideoAudience(share.Actor, share, res.locals.videoAll, undefined)
235 236
236 return activityPubResponse(activityPubContextify(activity), res) 237 return activityPubResponse(activityPubContextify(activity), res)
237} 238}
238 239
239async function videoAnnouncesController (req: express.Request, res: express.Response) { 240async function videoAnnouncesController (req: express.Request, res: express.Response) {
240 const video = res.locals.video 241 const video = res.locals.onlyVideo
241 242
242 const handler = async (start: number, count: number) => { 243 const handler = async (start: number, count: number) => {
243 const result = await VideoShareModel.listAndCountByVideoId(video.id, start, count) 244 const result = await VideoShareModel.listAndCountByVideoId(video.id, start, count)
@@ -252,21 +253,21 @@ async function videoAnnouncesController (req: express.Request, res: express.Resp
252} 253}
253 254
254async function videoLikesController (req: express.Request, res: express.Response) { 255async function videoLikesController (req: express.Request, res: express.Response) {
255 const video = res.locals.video 256 const video = res.locals.onlyVideo
256 const json = await videoRates(req, 'like', video, getVideoLikesActivityPubUrl(video)) 257 const json = await videoRates(req, 'like', video, getVideoLikesActivityPubUrl(video))
257 258
258 return activityPubResponse(activityPubContextify(json), res) 259 return activityPubResponse(activityPubContextify(json), res)
259} 260}
260 261
261async function videoDislikesController (req: express.Request, res: express.Response) { 262async function videoDislikesController (req: express.Request, res: express.Response) {
262 const video = res.locals.video 263 const video = res.locals.onlyVideo
263 const json = await videoRates(req, 'dislike', video, getVideoDislikesActivityPubUrl(video)) 264 const json = await videoRates(req, 'dislike', video, getVideoDislikesActivityPubUrl(video))
264 265
265 return activityPubResponse(activityPubContextify(json), res) 266 return activityPubResponse(activityPubContextify(json), res)
266} 267}
267 268
268async function videoCommentsController (req: express.Request, res: express.Response) { 269async function videoCommentsController (req: express.Request, res: express.Response) {
269 const video = res.locals.video 270 const video = res.locals.onlyVideo
270 271
271 const handler = async (start: number, count: number) => { 272 const handler = async (start: number, count: number) => {
272 const result = await VideoCommentModel.listAndCountByVideoId(video.id, start, count) 273 const result = await VideoCommentModel.listAndCountByVideoId(video.id, start, count)
@@ -301,7 +302,7 @@ async function videoChannelFollowingController (req: express.Request, res: expre
301} 302}
302 303
303async function videoCommentController (req: express.Request, res: express.Response) { 304async function videoCommentController (req: express.Request, res: express.Response) {
304 const videoComment = res.locals.videoComment 305 const videoComment = res.locals.videoCommentFull
305 306
306 if (videoComment.url.startsWith(WEBSERVER.URL) === false) return res.redirect(videoComment.url) 307 if (videoComment.url.startsWith(WEBSERVER.URL) === false) return res.redirect(videoComment.url)
307 308
@@ -337,7 +338,7 @@ async function videoRedundancyController (req: express.Request, res: express.Res
337} 338}
338 339
339async function videoPlaylistController (req: express.Request, res: express.Response) { 340async function videoPlaylistController (req: express.Request, res: express.Response) {
340 const playlist = res.locals.videoPlaylist 341 const playlist = res.locals.videoPlaylistFull
341 342
342 // We need more attributes 343 // We need more attributes
343 playlist.OwnerAccount = await AccountModel.load(playlist.ownerAccountId) 344 playlist.OwnerAccount = await AccountModel.load(playlist.ownerAccountId)
@@ -350,7 +351,7 @@ async function videoPlaylistController (req: express.Request, res: express.Respo
350} 351}
351 352
352async function videoPlaylistElementController (req: express.Request, res: express.Response) { 353async function videoPlaylistElementController (req: express.Request, res: express.Response) {
353 const videoPlaylistElement = res.locals.videoPlaylistElement 354 const videoPlaylistElement = res.locals.videoPlaylistElementAP
354 355
355 const json = videoPlaylistElement.toActivityPubObject() 356 const json = videoPlaylistElement.toActivityPubObject()
356 return activityPubResponse(activityPubContextify(json), res) 357 return activityPubResponse(activityPubContextify(json), res)
@@ -358,7 +359,7 @@ async function videoPlaylistElementController (req: express.Request, res: expres
358 359
359// --------------------------------------------------------------------------- 360// ---------------------------------------------------------------------------
360 361
361async function actorFollowing (req: express.Request, actor: ActorModel) { 362async function actorFollowing (req: express.Request, actor: MActorId) {
362 const handler = (start: number, count: number) => { 363 const handler = (start: number, count: number) => {
363 return ActorFollowModel.listAcceptedFollowingUrlsForApi([ actor.id ], undefined, start, count) 364 return ActorFollowModel.listAcceptedFollowingUrlsForApi([ actor.id ], undefined, start, count)
364 } 365 }
@@ -366,7 +367,7 @@ async function actorFollowing (req: express.Request, actor: ActorModel) {
366 return activityPubCollectionPagination(WEBSERVER.URL + req.path, handler, req.query.page) 367 return activityPubCollectionPagination(WEBSERVER.URL + req.path, handler, req.query.page)
367} 368}
368 369
369async function actorFollowers (req: express.Request, actor: ActorModel) { 370async function actorFollowers (req: express.Request, actor: MActorId) {
370 const handler = (start: number, count: number) => { 371 const handler = (start: number, count: number) => {
371 return ActorFollowModel.listAcceptedFollowerUrlsForAP([ actor.id ], undefined, start, count) 372 return ActorFollowModel.listAcceptedFollowerUrlsForAP([ actor.id ], undefined, start, count)
372 } 373 }
@@ -374,7 +375,7 @@ async function actorFollowers (req: express.Request, actor: ActorModel) {
374 return activityPubCollectionPagination(WEBSERVER.URL + req.path, handler, req.query.page) 375 return activityPubCollectionPagination(WEBSERVER.URL + req.path, handler, req.query.page)
375} 376}
376 377
377async function actorPlaylists (req: express.Request, account: AccountModel) { 378async function actorPlaylists (req: express.Request, account: MAccountId) {
378 const handler = (start: number, count: number) => { 379 const handler = (start: number, count: number) => {
379 return VideoPlaylistModel.listPublicUrlsOfForAP(account.id, start, count) 380 return VideoPlaylistModel.listPublicUrlsOfForAP(account.id, start, count)
380 } 381 }
@@ -382,7 +383,7 @@ async function actorPlaylists (req: express.Request, account: AccountModel) {
382 return activityPubCollectionPagination(WEBSERVER.URL + req.path, handler, req.query.page) 383 return activityPubCollectionPagination(WEBSERVER.URL + req.path, handler, req.query.page)
383} 384}
384 385
385function videoRates (req: express.Request, rateType: VideoRateType, video: VideoModel, url: string) { 386function videoRates (req: express.Request, rateType: VideoRateType, video: MVideo, url: string) {
386 const handler = async (start: number, count: number) => { 387 const handler = async (start: number, count: number) => {
387 const result = await AccountVideoRateModel.listAndCountAccountUrlsByVideoId(rateType, video.id, start, count) 388 const result = await AccountVideoRateModel.listAndCountAccountUrlsByVideoId(rateType, video.id, start, count)
388 return { 389 return {
diff --git a/server/controllers/activitypub/inbox.ts b/server/controllers/activitypub/inbox.ts
index 2d3eef222..d9df253aa 100644
--- a/server/controllers/activitypub/inbox.ts
+++ b/server/controllers/activitypub/inbox.ts
@@ -7,7 +7,7 @@ import { asyncMiddleware, checkSignature, localAccountValidator, localVideoChann
7import { activityPubValidator } from '../../middlewares/validators/activitypub/activity' 7import { activityPubValidator } from '../../middlewares/validators/activitypub/activity'
8import { queue } from 'async' 8import { queue } from 'async'
9import { ActorModel } from '../../models/activitypub/actor' 9import { ActorModel } from '../../models/activitypub/actor'
10import { SignatureActorModel } from '../../typings/models' 10import { MActorDefault, MActorSignature } from '../../typings/models'
11 11
12const inboxRouter = express.Router() 12const inboxRouter = express.Router()
13 13
@@ -41,7 +41,8 @@ export {
41 41
42// --------------------------------------------------------------------------- 42// ---------------------------------------------------------------------------
43 43
44const inboxQueue = queue<{ activities: Activity[], signatureActor?: SignatureActorModel, inboxActor?: ActorModel }, Error>((task, cb) => { 44type QueueParam = { activities: Activity[], signatureActor?: MActorSignature, inboxActor?: MActorDefault }
45const inboxQueue = queue<QueueParam, Error>((task, cb) => {
45 const options = { signatureActor: task.signatureActor, inboxActor: task.inboxActor } 46 const options = { signatureActor: task.signatureActor, inboxActor: task.inboxActor }
46 47
47 processActivities(task.activities, options) 48 processActivities(task.activities, options)
diff --git a/server/controllers/activitypub/outbox.ts b/server/controllers/activitypub/outbox.ts
index 38b6ec976..f3dd2ad7d 100644
--- a/server/controllers/activitypub/outbox.ts
+++ b/server/controllers/activitypub/outbox.ts
@@ -6,11 +6,9 @@ import { logger } from '../../helpers/logger'
6import { buildAnnounceActivity, buildCreateActivity } from '../../lib/activitypub/send' 6import { buildAnnounceActivity, buildCreateActivity } from '../../lib/activitypub/send'
7import { buildAudience } from '../../lib/activitypub/audience' 7import { buildAudience } from '../../lib/activitypub/audience'
8import { asyncMiddleware, localAccountValidator, localVideoChannelValidator } from '../../middlewares' 8import { asyncMiddleware, localAccountValidator, localVideoChannelValidator } from '../../middlewares'
9import { AccountModel } from '../../models/account/account'
10import { ActorModel } from '../../models/activitypub/actor'
11import { VideoModel } from '../../models/video/video' 9import { VideoModel } from '../../models/video/video'
12import { activityPubResponse } from './utils' 10import { activityPubResponse } from './utils'
13import { VideoChannelModel } from '../../models/video/video-channel' 11import { MActorLight } from '@server/typings/models'
14 12
15const outboxRouter = express.Router() 13const outboxRouter = express.Router()
16 14
@@ -45,14 +43,10 @@ async function outboxController (req: express.Request, res: express.Response) {
45 return activityPubResponse(activityPubContextify(json), res) 43 return activityPubResponse(activityPubContextify(json), res)
46} 44}
47 45
48async function buildActivities (actor: ActorModel, start: number, count: number) { 46async function buildActivities (actor: MActorLight, start: number, count: number) {
49 const data = await VideoModel.listAllAndSharedByActorForOutbox(actor.id, start, count) 47 const data = await VideoModel.listAllAndSharedByActorForOutbox(actor.id, start, count)
50 const activities: Activity[] = [] 48 const activities: Activity[] = []
51 49
52 // Avoid too many SQL requests
53 const actors = data.data.map(v => v.VideoChannel.Account.Actor)
54 actors.push(actor)
55
56 for (const video of data.data) { 50 for (const video of data.data) {
57 const byActor = video.VideoChannel.Account.Actor 51 const byActor = video.VideoChannel.Account.Actor
58 const createActivityAudience = buildAudience([ byActor.followersUrl ], video.privacy === VideoPrivacy.PUBLIC) 52 const createActivityAudience = buildAudience([ byActor.followersUrl ], video.privacy === VideoPrivacy.PUBLIC)
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts
index 21fa85a08..39a124fc5 100644
--- a/server/controllers/api/config.ts
+++ b/server/controllers/api/config.ts
@@ -158,7 +158,19 @@ function getAbout (req: express.Request, res: express.Response) {
158 name: CONFIG.INSTANCE.NAME, 158 name: CONFIG.INSTANCE.NAME,
159 shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION, 159 shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
160 description: CONFIG.INSTANCE.DESCRIPTION, 160 description: CONFIG.INSTANCE.DESCRIPTION,
161 terms: CONFIG.INSTANCE.TERMS 161 terms: CONFIG.INSTANCE.TERMS,
162 codeOfConduct: CONFIG.INSTANCE.CODE_OF_CONDUCT,
163
164 hardwareInformation: CONFIG.INSTANCE.HARDWARE_INFORMATION,
165
166 creationReason: CONFIG.INSTANCE.CREATION_REASON,
167 moderationInformation: CONFIG.INSTANCE.MODERATION_INFORMATION,
168 administrator: CONFIG.INSTANCE.ADMINISTRATOR,
169 maintenanceLifetime: CONFIG.INSTANCE.MAINTENANCE_LIFETIME,
170 businessModel: CONFIG.INSTANCE.BUSINESS_MODEL,
171
172 languages: CONFIG.INSTANCE.LANGUAGES,
173 categories: CONFIG.INSTANCE.CATEGORIES
162 } 174 }
163 } 175 }
164 176
@@ -221,6 +233,18 @@ function customConfig (): CustomConfig {
221 shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION, 233 shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
222 description: CONFIG.INSTANCE.DESCRIPTION, 234 description: CONFIG.INSTANCE.DESCRIPTION,
223 terms: CONFIG.INSTANCE.TERMS, 235 terms: CONFIG.INSTANCE.TERMS,
236 codeOfConduct: CONFIG.INSTANCE.CODE_OF_CONDUCT,
237
238 creationReason: CONFIG.INSTANCE.CREATION_REASON,
239 moderationInformation: CONFIG.INSTANCE.MODERATION_INFORMATION,
240 administrator: CONFIG.INSTANCE.ADMINISTRATOR,
241 maintenanceLifetime: CONFIG.INSTANCE.MAINTENANCE_LIFETIME,
242 businessModel: CONFIG.INSTANCE.BUSINESS_MODEL,
243 hardwareInformation: CONFIG.INSTANCE.HARDWARE_INFORMATION,
244
245 languages: CONFIG.INSTANCE.LANGUAGES,
246 categories: CONFIG.INSTANCE.CATEGORIES,
247
224 isNSFW: CONFIG.INSTANCE.IS_NSFW, 248 isNSFW: CONFIG.INSTANCE.IS_NSFW,
225 defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE, 249 defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE,
226 defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY, 250 defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
@@ -300,6 +324,18 @@ function customConfig (): CustomConfig {
300 enabled: CONFIG.FOLLOWERS.INSTANCE.ENABLED, 324 enabled: CONFIG.FOLLOWERS.INSTANCE.ENABLED,
301 manualApproval: CONFIG.FOLLOWERS.INSTANCE.MANUAL_APPROVAL 325 manualApproval: CONFIG.FOLLOWERS.INSTANCE.MANUAL_APPROVAL
302 } 326 }
327 },
328 followings: {
329 instance: {
330 autoFollowBack: {
331 enabled: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_BACK.ENABLED
332 },
333
334 autoFollowIndex: {
335 enabled: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.ENABLED,
336 indexUrl: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.INDEX_URL
337 }
338 }
303 } 339 }
304 } 340 }
305} 341}
diff --git a/server/controllers/api/search.ts b/server/controllers/api/search.ts
index 9a1e30b83..349650aca 100644
--- a/server/controllers/api/search.ts
+++ b/server/controllers/api/search.ts
@@ -19,6 +19,7 @@ import { getOrCreateActorAndServerAndModel, getOrCreateVideoAndAccountAndChannel
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'
22 23
23const searchRouter = express.Router() 24const searchRouter = express.Router()
24 25
@@ -84,7 +85,7 @@ async function searchVideoChannelsDB (query: VideoChannelsSearchQuery, res: expr
84} 85}
85 86
86async function searchVideoChannelURI (search: string, isWebfingerSearch: boolean, res: express.Response) { 87async function searchVideoChannelURI (search: string, isWebfingerSearch: boolean, res: express.Response) {
87 let videoChannel: VideoChannelModel 88 let videoChannel: MChannelAccountDefault
88 let uri = search 89 let uri = search
89 90
90 if (isWebfingerSearch) { 91 if (isWebfingerSearch) {
@@ -137,7 +138,7 @@ async function searchVideosDB (query: VideosSearchQuery, res: express.Response)
137} 138}
138 139
139async function searchVideoURI (url: string, res: express.Response) { 140async function searchVideoURI (url: string, res: express.Response) {
140 let video: VideoModel 141 let video: MVideoAccountLightBlacklistAllFiles
141 142
142 // Check if we can fetch a remote video with the URL 143 // Check if we can fetch a remote video with the URL
143 if (isUserAbleToSearchRemoteURI(res)) { 144 if (isUserAbleToSearchRemoteURI(res)) {
diff --git a/server/controllers/api/server/follows.ts b/server/controllers/api/server/follows.ts
index d38ce91de..37647622b 100644
--- a/server/controllers/api/server/follows.ts
+++ b/server/controllers/api/server/follows.ts
@@ -25,6 +25,7 @@ import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
25import { JobQueue } from '../../../lib/job-queue' 25import { JobQueue } from '../../../lib/job-queue'
26import { removeRedundancyOf } from '../../../lib/redundancy' 26import { removeRedundancyOf } from '../../../lib/redundancy'
27import { sequelizeTypescript } from '../../../initializers/database' 27import { sequelizeTypescript } from '../../../initializers/database'
28import { autoFollowBackIfNeeded } from '../../../lib/activitypub/follow'
28 29
29const serverFollowsRouter = express.Router() 30const serverFollowsRouter = express.Router()
30serverFollowsRouter.get('/following', 31serverFollowsRouter.get('/following',
@@ -172,5 +173,7 @@ async function acceptFollower (req: express.Request, res: express.Response) {
172 follow.state = 'accepted' 173 follow.state = 'accepted'
173 await follow.save() 174 await follow.save()
174 175
176 await autoFollowBackIfNeeded(follow)
177
175 return res.status(204).end() 178 return res.status(204).end()
176} 179}
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts
index ae40e86f8..27351c1a9 100644
--- a/server/controllers/api/users/index.ts
+++ b/server/controllers/api/users/index.ts
@@ -48,6 +48,7 @@ import { CONFIG } from '../../../initializers/config'
48import { sequelizeTypescript } from '../../../initializers/database' 48import { sequelizeTypescript } from '../../../initializers/database'
49import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' 49import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
50import { UserRegister } from '../../../../shared/models/users/user-register.model' 50import { UserRegister } from '../../../../shared/models/users/user-register.model'
51import { MUser, MUserAccountDefault } from '@server/typings/models'
51 52
52const auditLogger = auditLoggerFactory('users') 53const auditLogger = auditLoggerFactory('users')
53 54
@@ -195,7 +196,7 @@ async function createUser (req: express.Request, res: express.Response) {
195 videoQuota: body.videoQuota, 196 videoQuota: body.videoQuota,
196 videoQuotaDaily: body.videoQuotaDaily, 197 videoQuotaDaily: body.videoQuotaDaily,
197 adminFlags: body.adminFlags || UserAdminFlag.NONE 198 adminFlags: body.adminFlags || UserAdminFlag.NONE
198 }) 199 }) as MUser
199 200
200 const { user, account } = await createUserAccountAndChannelAndPlaylist({ userToCreate: userToCreate }) 201 const { user, account } = await createUserAccountAndChannelAndPlaylist({ userToCreate: userToCreate })
201 202
@@ -359,7 +360,7 @@ function success (req: express.Request, res: express.Response) {
359 res.end() 360 res.end()
360} 361}
361 362
362async function changeUserBlock (res: express.Response, user: UserModel, block: boolean, reason?: string) { 363async function changeUserBlock (res: express.Response, user: MUserAccountDefault, block: boolean, reason?: string) {
363 const oldUserAuditView = new UserAuditView(user.toFormattedJSON()) 364 const oldUserAuditView = new UserAuditView(user.toFormattedJSON())
364 365
365 user.blocked = block 366 user.blocked = block
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts
index e7ed3de64..bf872ca52 100644
--- a/server/controllers/api/users/me.ts
+++ b/server/controllers/api/users/me.ts
@@ -23,15 +23,12 @@ import { createReqFiles } from '../../../helpers/express-utils'
23import { UserVideoQuota } from '../../../../shared/models/users/user-video-quota.model' 23import { UserVideoQuota } from '../../../../shared/models/users/user-video-quota.model'
24import { updateAvatarValidator } from '../../../middlewares/validators/avatar' 24import { updateAvatarValidator } from '../../../middlewares/validators/avatar'
25import { updateActorAvatarFile } from '../../../lib/avatar' 25import { updateActorAvatarFile } from '../../../lib/avatar'
26import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger'
27import { VideoImportModel } from '../../../models/video/video-import' 26import { VideoImportModel } from '../../../models/video/video-import'
28import { AccountModel } from '../../../models/account/account' 27import { AccountModel } from '../../../models/account/account'
29import { CONFIG } from '../../../initializers/config' 28import { CONFIG } from '../../../initializers/config'
30import { sequelizeTypescript } from '../../../initializers/database' 29import { sequelizeTypescript } from '../../../initializers/database'
31import { sendVerifyUserEmail } from '../../../lib/user' 30import { sendVerifyUserEmail } from '../../../lib/user'
32 31
33const auditLogger = auditLoggerFactory('users-me')
34
35const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR }) 32const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR })
36 33
37const meRouter = express.Router() 34const meRouter = express.Router()
@@ -130,7 +127,7 @@ async function getUserInformation (req: express.Request, res: express.Response)
130 // We did not load channels in res.locals.user 127 // We did not load channels in res.locals.user
131 const user = await UserModel.loadByUsernameAndPopulateChannels(res.locals.oauth.token.user.username) 128 const user = await UserModel.loadByUsernameAndPopulateChannels(res.locals.oauth.token.user.username)
132 129
133 return res.json(user.toFormattedJSON({})) 130 return res.json(user.toFormattedJSON())
134} 131}
135 132
136async function getUserVideoQuotaUsed (req: express.Request, res: express.Response) { 133async function getUserVideoQuotaUsed (req: express.Request, res: express.Response) {
@@ -147,7 +144,7 @@ async function getUserVideoQuotaUsed (req: express.Request, res: express.Respons
147} 144}
148 145
149async function getUserVideoRating (req: express.Request, res: express.Response) { 146async function getUserVideoRating (req: express.Request, res: express.Response) {
150 const videoId = res.locals.video.id 147 const videoId = res.locals.videoId.id
151 const accountId = +res.locals.oauth.token.User.Account.id 148 const accountId = +res.locals.oauth.token.User.Account.id
152 149
153 const ratingObj = await AccountVideoRateModel.load(accountId, videoId, null) 150 const ratingObj = await AccountVideoRateModel.load(accountId, videoId, null)
@@ -165,8 +162,6 @@ async function deleteMe (req: express.Request, res: express.Response) {
165 162
166 await user.destroy() 163 await user.destroy()
167 164
168 auditLogger.delete(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON({})))
169
170 return res.sendStatus(204) 165 return res.sendStatus(204)
171} 166}
172 167
@@ -175,7 +170,6 @@ async function updateMe (req: express.Request, res: express.Response) {
175 let sendVerificationEmail = false 170 let sendVerificationEmail = false
176 171
177 const user = res.locals.oauth.token.user 172 const user = res.locals.oauth.token.user
178 const oldUserAuditView = new UserAuditView(user.toFormattedJSON({}))
179 173
180 if (body.password !== undefined) user.password = body.password 174 if (body.password !== undefined) user.password = body.password
181 if (body.nsfwPolicy !== undefined) user.nsfwPolicy = body.nsfwPolicy 175 if (body.nsfwPolicy !== undefined) user.nsfwPolicy = body.nsfwPolicy
@@ -184,6 +178,8 @@ async function updateMe (req: express.Request, res: express.Response) {
184 if (body.videosHistoryEnabled !== undefined) user.videosHistoryEnabled = body.videosHistoryEnabled 178 if (body.videosHistoryEnabled !== undefined) user.videosHistoryEnabled = body.videosHistoryEnabled
185 if (body.videoLanguages !== undefined) user.videoLanguages = body.videoLanguages 179 if (body.videoLanguages !== undefined) user.videoLanguages = body.videoLanguages
186 if (body.theme !== undefined) user.theme = body.theme 180 if (body.theme !== undefined) user.theme = body.theme
181 if (body.noInstanceConfigWarningModal !== undefined) user.noInstanceConfigWarningModal = body.noInstanceConfigWarningModal
182 if (body.noWelcomeModal !== undefined) user.noWelcomeModal = body.noWelcomeModal
187 183
188 if (body.email !== undefined) { 184 if (body.email !== undefined) {
189 if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) { 185 if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) {
@@ -195,17 +191,17 @@ async function updateMe (req: express.Request, res: express.Response) {
195 } 191 }
196 192
197 await sequelizeTypescript.transaction(async t => { 193 await sequelizeTypescript.transaction(async t => {
198 const userAccount = await AccountModel.load(user.Account.id)
199
200 await user.save({ transaction: t }) 194 await user.save({ transaction: t })
201 195
202 if (body.displayName !== undefined) userAccount.name = body.displayName 196 if (body.displayName !== undefined || body.description !== undefined) {
203 if (body.description !== undefined) userAccount.description = body.description 197 const userAccount = await AccountModel.load(user.Account.id, t)
204 await userAccount.save({ transaction: t })
205 198
206 await sendUpdateActor(userAccount, t) 199 if (body.displayName !== undefined) userAccount.name = body.displayName
200 if (body.description !== undefined) userAccount.description = body.description
201 await userAccount.save({ transaction: t })
207 202
208 auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON({})), oldUserAuditView) 203 await sendUpdateActor(userAccount, t)
204 }
209 }) 205 })
210 206
211 if (sendVerificationEmail === true) { 207 if (sendVerificationEmail === true) {
@@ -218,13 +214,10 @@ async function updateMe (req: express.Request, res: express.Response) {
218async function updateMyAvatar (req: express.Request, res: express.Response) { 214async function updateMyAvatar (req: express.Request, res: express.Response) {
219 const avatarPhysicalFile = req.files[ 'avatarfile' ][ 0 ] 215 const avatarPhysicalFile = req.files[ 'avatarfile' ][ 0 ]
220 const user = res.locals.oauth.token.user 216 const user = res.locals.oauth.token.user
221 const oldUserAuditView = new UserAuditView(user.toFormattedJSON({}))
222 217
223 const userAccount = await AccountModel.load(user.Account.id) 218 const userAccount = await AccountModel.load(user.Account.id)
224 219
225 const avatar = await updateActorAvatarFile(avatarPhysicalFile, userAccount) 220 const avatar = await updateActorAvatarFile(avatarPhysicalFile, userAccount)
226 221
227 auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON({})), oldUserAuditView)
228
229 return res.json({ avatar: avatar.toFormattedJSON() }) 222 return res.json({ avatar: avatar.toFormattedJSON() })
230} 223}
diff --git a/server/controllers/api/users/my-history.ts b/server/controllers/api/users/my-history.ts
index 7025c0ff1..4da1f3496 100644
--- a/server/controllers/api/users/my-history.ts
+++ b/server/controllers/api/users/my-history.ts
@@ -7,7 +7,6 @@ import {
7 setDefaultPagination, 7 setDefaultPagination,
8 userHistoryRemoveValidator 8 userHistoryRemoveValidator
9} from '../../../middlewares' 9} from '../../../middlewares'
10import { UserModel } from '../../../models/account/user'
11import { getFormattedObjects } from '../../../helpers/utils' 10import { getFormattedObjects } from '../../../helpers/utils'
12import { UserVideoHistoryModel } from '../../../models/account/user-video-history' 11import { UserVideoHistoryModel } from '../../../models/account/user-video-history'
13import { sequelizeTypescript } from '../../../initializers' 12import { sequelizeTypescript } from '../../../initializers'
diff --git a/server/controllers/api/users/my-notifications.ts b/server/controllers/api/users/my-notifications.ts
index f146284e4..017f5219e 100644
--- a/server/controllers/api/users/my-notifications.ts
+++ b/server/controllers/api/users/my-notifications.ts
@@ -76,7 +76,8 @@ async function updateNotificationSettings (req: express.Request, res: express.Re
76 newFollow: body.newFollow, 76 newFollow: body.newFollow,
77 newUserRegistration: body.newUserRegistration, 77 newUserRegistration: body.newUserRegistration,
78 commentMention: body.commentMention, 78 commentMention: body.commentMention,
79 newInstanceFollower: body.newInstanceFollower 79 newInstanceFollower: body.newInstanceFollower,
80 autoInstanceFollowing: body.autoInstanceFollowing
80 } 81 }
81 82
82 await UserNotificationSettingModel.update(values, query) 83 await UserNotificationSettingModel.update(values, query)
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts
index 81a03a62b..acc5b2987 100644
--- a/server/controllers/api/video-channel.ts
+++ b/server/controllers/api/video-channel.ts
@@ -19,7 +19,7 @@ import { VideoChannelModel } from '../../models/video/video-channel'
19import { videoChannelsNameWithHostValidator, videosSortValidator } from '../../middlewares/validators' 19import { videoChannelsNameWithHostValidator, videosSortValidator } from '../../middlewares/validators'
20import { sendUpdateActor } from '../../lib/activitypub/send' 20import { sendUpdateActor } from '../../lib/activitypub/send'
21import { VideoChannelCreate, VideoChannelUpdate } from '../../../shared' 21import { VideoChannelCreate, VideoChannelUpdate } from '../../../shared'
22import { createVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel' 22import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel'
23import { buildNSFWFilter, createReqFiles, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' 23import { buildNSFWFilter, createReqFiles, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
24import { setAsyncActorKeys } from '../../lib/activitypub' 24import { setAsyncActorKeys } from '../../lib/activitypub'
25import { AccountModel } from '../../models/account/account' 25import { AccountModel } from '../../models/account/account'
@@ -35,6 +35,7 @@ import { VideoPlaylistModel } from '../../models/video/video-playlist'
35import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists' 35import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists'
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'
38 39
39const auditLogger = auditLoggerFactory('channels') 40const auditLogger = auditLoggerFactory('channels')
40const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR }) 41const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR })
@@ -136,10 +137,10 @@ async function updateVideoChannelAvatar (req: express.Request, res: express.Resp
136async function addVideoChannel (req: express.Request, res: express.Response) { 137async function addVideoChannel (req: express.Request, res: express.Response) {
137 const videoChannelInfo: VideoChannelCreate = req.body 138 const videoChannelInfo: VideoChannelCreate = req.body
138 139
139 const videoChannelCreated: VideoChannelModel = await sequelizeTypescript.transaction(async t => { 140 const videoChannelCreated = await sequelizeTypescript.transaction(async t => {
140 const account = await AccountModel.load(res.locals.oauth.token.User.Account.id, t) 141 const account = await AccountModel.load(res.locals.oauth.token.User.Account.id, t)
141 142
142 return createVideoChannel(videoChannelInfo, account, t) 143 return createLocalVideoChannel(videoChannelInfo, account, t)
143 }) 144 })
144 145
145 setAsyncActorKeys(videoChannelCreated.Actor) 146 setAsyncActorKeys(videoChannelCreated.Actor)
@@ -181,7 +182,7 @@ async function updateVideoChannel (req: express.Request, res: express.Response)
181 } 182 }
182 } 183 }
183 184
184 const videoChannelInstanceUpdated = await videoChannelInstance.save(sequelizeOptions) 185 const videoChannelInstanceUpdated = await videoChannelInstance.save(sequelizeOptions) as MChannelAccountDefault
185 await sendUpdateActor(videoChannelInstanceUpdated, t) 186 await sendUpdateActor(videoChannelInstanceUpdated, t)
186 187
187 auditLogger.update( 188 auditLogger.update(
diff --git a/server/controllers/api/video-playlist.ts b/server/controllers/api/video-playlist.ts
index bd454f553..d9f0ff925 100644
--- a/server/controllers/api/video-playlist.ts
+++ b/server/controllers/api/video-playlist.ts
@@ -40,7 +40,7 @@ import { JobQueue } from '../../lib/job-queue'
40import { CONFIG } from '../../initializers/config' 40import { 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 { VideoModel } from '../../models/video/video' 43import { MVideoPlaylistFull, MVideoPlaylistThumbnail, MVideoThumbnail } from '@server/typings/models'
44 44
45const reqThumbnailFile = createReqFiles([ 'thumbnailfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { thumbnailfile: CONFIG.STORAGE.TMP_DIR }) 45const reqThumbnailFile = createReqFiles([ 'thumbnailfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { thumbnailfile: CONFIG.STORAGE.TMP_DIR })
46 46
@@ -58,7 +58,7 @@ videoPlaylistRouter.get('/',
58) 58)
59 59
60videoPlaylistRouter.get('/:playlistId', 60videoPlaylistRouter.get('/:playlistId',
61 asyncMiddleware(videoPlaylistsGetValidator), 61 asyncMiddleware(videoPlaylistsGetValidator('summary')),
62 getVideoPlaylist 62 getVideoPlaylist
63) 63)
64 64
@@ -83,7 +83,7 @@ videoPlaylistRouter.delete('/:playlistId',
83) 83)
84 84
85videoPlaylistRouter.get('/:playlistId/videos', 85videoPlaylistRouter.get('/:playlistId/videos',
86 asyncMiddleware(videoPlaylistsGetValidator), 86 asyncMiddleware(videoPlaylistsGetValidator('summary')),
87 paginationValidator, 87 paginationValidator,
88 setDefaultPagination, 88 setDefaultPagination,
89 optionalAuthenticate, 89 optionalAuthenticate,
@@ -140,7 +140,7 @@ async function listVideoPlaylists (req: express.Request, res: express.Response)
140} 140}
141 141
142function getVideoPlaylist (req: express.Request, res: express.Response) { 142function getVideoPlaylist (req: express.Request, res: express.Response) {
143 const videoPlaylist = res.locals.videoPlaylist 143 const videoPlaylist = res.locals.videoPlaylistSummary
144 144
145 if (videoPlaylist.isOutdated()) { 145 if (videoPlaylist.isOutdated()) {
146 JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video-playlist', url: videoPlaylist.url } }) 146 JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video-playlist', url: videoPlaylist.url } })
@@ -159,7 +159,7 @@ async function addVideoPlaylist (req: express.Request, res: express.Response) {
159 description: videoPlaylistInfo.description, 159 description: videoPlaylistInfo.description,
160 privacy: videoPlaylistInfo.privacy || VideoPlaylistPrivacy.PRIVATE, 160 privacy: videoPlaylistInfo.privacy || VideoPlaylistPrivacy.PRIVATE,
161 ownerAccountId: user.Account.id 161 ownerAccountId: user.Account.id
162 }) 162 }) as MVideoPlaylistFull
163 163
164 videoPlaylist.url = getVideoPlaylistActivityPubUrl(videoPlaylist) // We use the UUID, so set the URL after building the object 164 videoPlaylist.url = getVideoPlaylistActivityPubUrl(videoPlaylist) // We use the UUID, so set the URL after building the object
165 165
@@ -175,8 +175,8 @@ async function addVideoPlaylist (req: express.Request, res: express.Response) {
175 ? await createPlaylistMiniatureFromExisting(thumbnailField[0].path, videoPlaylist, false) 175 ? await createPlaylistMiniatureFromExisting(thumbnailField[0].path, videoPlaylist, false)
176 : undefined 176 : undefined
177 177
178 const videoPlaylistCreated: VideoPlaylistModel = await sequelizeTypescript.transaction(async t => { 178 const videoPlaylistCreated = await sequelizeTypescript.transaction(async t => {
179 const videoPlaylistCreated = await videoPlaylist.save({ transaction: t }) 179 const videoPlaylistCreated = await videoPlaylist.save({ transaction: t }) as MVideoPlaylistFull
180 180
181 if (thumbnailModel) { 181 if (thumbnailModel) {
182 thumbnailModel.automaticallyGenerated = false 182 thumbnailModel.automaticallyGenerated = false
@@ -201,7 +201,7 @@ async function addVideoPlaylist (req: express.Request, res: express.Response) {
201} 201}
202 202
203async function updateVideoPlaylist (req: express.Request, res: express.Response) { 203async function updateVideoPlaylist (req: express.Request, res: express.Response) {
204 const videoPlaylistInstance = res.locals.videoPlaylist 204 const videoPlaylistInstance = res.locals.videoPlaylistFull
205 const videoPlaylistFieldsSave = videoPlaylistInstance.toJSON() 205 const videoPlaylistFieldsSave = videoPlaylistInstance.toJSON()
206 const videoPlaylistInfoToUpdate = req.body as VideoPlaylistUpdate 206 const videoPlaylistInfoToUpdate = req.body as VideoPlaylistUpdate
207 207
@@ -275,7 +275,7 @@ async function updateVideoPlaylist (req: express.Request, res: express.Response)
275} 275}
276 276
277async function removeVideoPlaylist (req: express.Request, res: express.Response) { 277async function removeVideoPlaylist (req: express.Request, res: express.Response) {
278 const videoPlaylistInstance = res.locals.videoPlaylist 278 const videoPlaylistInstance = res.locals.videoPlaylistSummary
279 279
280 await sequelizeTypescript.transaction(async t => { 280 await sequelizeTypescript.transaction(async t => {
281 await videoPlaylistInstance.destroy({ transaction: t }) 281 await videoPlaylistInstance.destroy({ transaction: t })
@@ -290,10 +290,10 @@ async function removeVideoPlaylist (req: express.Request, res: express.Response)
290 290
291async function addVideoInPlaylist (req: express.Request, res: express.Response) { 291async function addVideoInPlaylist (req: express.Request, res: express.Response) {
292 const body: VideoPlaylistElementCreate = req.body 292 const body: VideoPlaylistElementCreate = req.body
293 const videoPlaylist = res.locals.videoPlaylist 293 const videoPlaylist = res.locals.videoPlaylistFull
294 const video = res.locals.video 294 const video = res.locals.onlyVideo
295 295
296 const playlistElement: VideoPlaylistElementModel = await sequelizeTypescript.transaction(async t => { 296 const playlistElement = await sequelizeTypescript.transaction(async t => {
297 const position = await VideoPlaylistElementModel.getNextPositionOf(videoPlaylist.id, t) 297 const position = await VideoPlaylistElementModel.getNextPositionOf(videoPlaylist.id, t)
298 298
299 const playlistElement = await VideoPlaylistElementModel.create({ 299 const playlistElement = await VideoPlaylistElementModel.create({
@@ -330,7 +330,7 @@ async function addVideoInPlaylist (req: express.Request, res: express.Response)
330 330
331async function updateVideoPlaylistElement (req: express.Request, res: express.Response) { 331async function updateVideoPlaylistElement (req: express.Request, res: express.Response) {
332 const body: VideoPlaylistElementUpdate = req.body 332 const body: VideoPlaylistElementUpdate = req.body
333 const videoPlaylist = res.locals.videoPlaylist 333 const videoPlaylist = res.locals.videoPlaylistFull
334 const videoPlaylistElement = res.locals.videoPlaylistElement 334 const videoPlaylistElement = res.locals.videoPlaylistElement
335 335
336 const playlistElement: VideoPlaylistElementModel = await sequelizeTypescript.transaction(async t => { 336 const playlistElement: VideoPlaylistElementModel = await sequelizeTypescript.transaction(async t => {
@@ -354,7 +354,7 @@ async function updateVideoPlaylistElement (req: express.Request, res: express.Re
354 354
355async function removeVideoFromPlaylist (req: express.Request, res: express.Response) { 355async function removeVideoFromPlaylist (req: express.Request, res: express.Response) {
356 const videoPlaylistElement = res.locals.videoPlaylistElement 356 const videoPlaylistElement = res.locals.videoPlaylistElement
357 const videoPlaylist = res.locals.videoPlaylist 357 const videoPlaylist = res.locals.videoPlaylistFull
358 const positionToDelete = videoPlaylistElement.position 358 const positionToDelete = videoPlaylistElement.position
359 359
360 await sequelizeTypescript.transaction(async t => { 360 await sequelizeTypescript.transaction(async t => {
@@ -381,7 +381,7 @@ async function removeVideoFromPlaylist (req: express.Request, res: express.Respo
381} 381}
382 382
383async function reorderVideosPlaylist (req: express.Request, res: express.Response) { 383async function reorderVideosPlaylist (req: express.Request, res: express.Response) {
384 const videoPlaylist = res.locals.videoPlaylist 384 const videoPlaylist = res.locals.videoPlaylistFull
385 const body: VideoPlaylistReorder = req.body 385 const body: VideoPlaylistReorder = req.body
386 386
387 const start: number = body.startPosition 387 const start: number = body.startPosition
@@ -434,7 +434,7 @@ async function reorderVideosPlaylist (req: express.Request, res: express.Respons
434} 434}
435 435
436async function getVideoPlaylistVideos (req: express.Request, res: express.Response) { 436async function getVideoPlaylistVideos (req: express.Request, res: express.Response) {
437 const videoPlaylistInstance = res.locals.videoPlaylist 437 const videoPlaylistInstance = res.locals.videoPlaylistSummary
438 const user = res.locals.oauth ? res.locals.oauth.token.User : undefined 438 const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
439 const server = await getServerActor() 439 const server = await getServerActor()
440 440
@@ -453,7 +453,7 @@ async function getVideoPlaylistVideos (req: express.Request, res: express.Respon
453 return res.json(getFormattedObjects(resultList.data, resultList.total, options)) 453 return res.json(getFormattedObjects(resultList.data, resultList.total, options))
454} 454}
455 455
456async function regeneratePlaylistThumbnail (videoPlaylist: VideoPlaylistModel) { 456async function regeneratePlaylistThumbnail (videoPlaylist: MVideoPlaylistThumbnail) {
457 await videoPlaylist.Thumbnail.destroy() 457 await videoPlaylist.Thumbnail.destroy()
458 videoPlaylist.Thumbnail = null 458 videoPlaylist.Thumbnail = null
459 459
@@ -461,7 +461,7 @@ async function regeneratePlaylistThumbnail (videoPlaylist: VideoPlaylistModel) {
461 if (firstElement) await generateThumbnailForPlaylist(videoPlaylist, firstElement.Video) 461 if (firstElement) await generateThumbnailForPlaylist(videoPlaylist, firstElement.Video)
462} 462}
463 463
464async function generateThumbnailForPlaylist (videoPlaylist: VideoPlaylistModel, video: VideoModel) { 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 inputPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getMiniature().filename)
diff --git a/server/controllers/api/videos/abuse.ts b/server/controllers/api/videos/abuse.ts
index 77808466c..4ae899b7e 100644
--- a/server/controllers/api/videos/abuse.ts
+++ b/server/controllers/api/videos/abuse.ts
@@ -1,7 +1,7 @@
1import * as express from 'express' 1import * as express from 'express'
2import { UserRight, VideoAbuseCreate, VideoAbuseState } from '../../../../shared' 2import { UserRight, VideoAbuseCreate, VideoAbuseState } from '../../../../shared'
3import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
4import { getFormattedObjects } from '../../../helpers/utils' 4import { getFormattedObjects, getServerActor } from '../../../helpers/utils'
5import { sequelizeTypescript } from '../../../initializers' 5import { sequelizeTypescript } from '../../../initializers'
6import { 6import {
7 asyncMiddleware, 7 asyncMiddleware,
@@ -21,6 +21,7 @@ import { VideoAbuseModel } from '../../../models/video/video-abuse'
21import { auditLoggerFactory, VideoAbuseAuditView } from '../../../helpers/audit-logger' 21import { auditLoggerFactory, VideoAbuseAuditView } from '../../../helpers/audit-logger'
22import { Notifier } from '../../../lib/notifier' 22import { Notifier } from '../../../lib/notifier'
23import { sendVideoAbuse } from '../../../lib/activitypub/send/send-flag' 23import { sendVideoAbuse } from '../../../lib/activitypub/send/send-flag'
24import { MVideoAbuseAccountVideo } from '../../../typings/models/video'
24 25
25const auditLogger = auditLoggerFactory('abuse') 26const auditLogger = auditLoggerFactory('abuse')
26const abuseVideoRouter = express.Router() 27const abuseVideoRouter = express.Router()
@@ -61,7 +62,16 @@ export {
61// --------------------------------------------------------------------------- 62// ---------------------------------------------------------------------------
62 63
63async function listVideoAbuses (req: express.Request, res: express.Response) { 64async function listVideoAbuses (req: express.Request, res: express.Response) {
64 const resultList = await VideoAbuseModel.listForApi(req.query.start, req.query.count, req.query.sort) 65 const user = res.locals.oauth.token.user
66 const serverActor = await getServerActor()
67
68 const resultList = await VideoAbuseModel.listForApi({
69 start: req.query.start,
70 count: req.query.count,
71 sort: req.query.sort,
72 serverAccountId: serverActor.Account.id,
73 user
74 })
65 75
66 return res.json(getFormattedObjects(resultList.data, resultList.total)) 76 return res.json(getFormattedObjects(resultList.data, resultList.total))
67} 77}
@@ -94,10 +104,10 @@ async function deleteVideoAbuse (req: express.Request, res: express.Response) {
94} 104}
95 105
96async function reportVideoAbuse (req: express.Request, res: express.Response) { 106async function reportVideoAbuse (req: express.Request, res: express.Response) {
97 const videoInstance = res.locals.video 107 const videoInstance = res.locals.videoAll
98 const body: VideoAbuseCreate = req.body 108 const body: VideoAbuseCreate = req.body
99 109
100 const videoAbuse: VideoAbuseModel = await sequelizeTypescript.transaction(async t => { 110 const videoAbuse = await sequelizeTypescript.transaction(async t => {
101 const reporterAccount = await AccountModel.load(res.locals.oauth.token.User.Account.id, t) 111 const reporterAccount = await AccountModel.load(res.locals.oauth.token.User.Account.id, t)
102 112
103 const abuseToCreate = { 113 const abuseToCreate = {
@@ -107,7 +117,7 @@ async function reportVideoAbuse (req: express.Request, res: express.Response) {
107 state: VideoAbuseState.PENDING 117 state: VideoAbuseState.PENDING
108 } 118 }
109 119
110 const videoAbuseInstance = await VideoAbuseModel.create(abuseToCreate, { transaction: t }) 120 const videoAbuseInstance: MVideoAbuseAccountVideo = await VideoAbuseModel.create(abuseToCreate, { transaction: t })
111 videoAbuseInstance.Video = videoInstance 121 videoAbuseInstance.Video = videoInstance
112 videoAbuseInstance.Account = reporterAccount 122 videoAbuseInstance.Account = reporterAccount
113 123
diff --git a/server/controllers/api/videos/blacklist.ts b/server/controllers/api/videos/blacklist.ts
index 9ff494def..2a667480d 100644
--- a/server/controllers/api/videos/blacklist.ts
+++ b/server/controllers/api/videos/blacklist.ts
@@ -1,5 +1,5 @@
1import * as express from 'express' 1import * as express from 'express'
2import { VideoBlacklist, UserRight, VideoBlacklistCreate, VideoBlacklistType } from '../../../../shared' 2import { UserRight, VideoBlacklistCreate, VideoBlacklistType } from '../../../../shared'
3import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
4import { getFormattedObjects } from '../../../helpers/utils' 4import { getFormattedObjects } from '../../../helpers/utils'
5import { 5import {
@@ -11,15 +11,16 @@ import {
11 setBlacklistSort, 11 setBlacklistSort,
12 setDefaultPagination, 12 setDefaultPagination,
13 videosBlacklistAddValidator, 13 videosBlacklistAddValidator,
14 videosBlacklistFiltersValidator,
14 videosBlacklistRemoveValidator, 15 videosBlacklistRemoveValidator,
15 videosBlacklistUpdateValidator, 16 videosBlacklistUpdateValidator
16 videosBlacklistFiltersValidator
17} from '../../../middlewares' 17} from '../../../middlewares'
18import { VideoBlacklistModel } from '../../../models/video/video-blacklist' 18import { VideoBlacklistModel } from '../../../models/video/video-blacklist'
19import { sequelizeTypescript } from '../../../initializers' 19import { sequelizeTypescript } from '../../../initializers'
20import { Notifier } from '../../../lib/notifier' 20import { Notifier } from '../../../lib/notifier'
21import { sendDeleteVideo } from '../../../lib/activitypub/send' 21import { sendDeleteVideo } from '../../../lib/activitypub/send'
22import { federateVideoIfNeeded } from '../../../lib/activitypub' 22import { federateVideoIfNeeded } from '../../../lib/activitypub'
23import { MVideoBlacklistVideo } from '@server/typings/models'
23 24
24const blacklistRouter = express.Router() 25const blacklistRouter = express.Router()
25 26
@@ -64,7 +65,7 @@ export {
64// --------------------------------------------------------------------------- 65// ---------------------------------------------------------------------------
65 66
66async function addVideoToBlacklist (req: express.Request, res: express.Response) { 67async function addVideoToBlacklist (req: express.Request, res: express.Response) {
67 const videoInstance = res.locals.video 68 const videoInstance = res.locals.videoAll
68 const body: VideoBlacklistCreate = req.body 69 const body: VideoBlacklistCreate = req.body
69 70
70 const toCreate = { 71 const toCreate = {
@@ -74,7 +75,7 @@ async function addVideoToBlacklist (req: express.Request, res: express.Response)
74 type: VideoBlacklistType.MANUAL 75 type: VideoBlacklistType.MANUAL
75 } 76 }
76 77
77 const blacklist = await VideoBlacklistModel.create(toCreate) 78 const blacklist: MVideoBlacklistVideo = await VideoBlacklistModel.create(toCreate)
78 blacklist.Video = videoInstance 79 blacklist.Video = videoInstance
79 80
80 if (body.unfederate === true) { 81 if (body.unfederate === true) {
@@ -83,7 +84,7 @@ async function addVideoToBlacklist (req: express.Request, res: express.Response)
83 84
84 Notifier.Instance.notifyOnVideoBlacklist(blacklist) 85 Notifier.Instance.notifyOnVideoBlacklist(blacklist)
85 86
86 logger.info('Video %s blacklisted.', res.locals.video.uuid) 87 logger.info('Video %s blacklisted.', videoInstance.uuid)
87 88
88 return res.type('json').status(204).end() 89 return res.type('json').status(204).end()
89} 90}
@@ -108,7 +109,7 @@ async function listBlacklist (req: express.Request, res: express.Response) {
108 109
109async function removeVideoFromBlacklistController (req: express.Request, res: express.Response) { 110async function removeVideoFromBlacklistController (req: express.Request, res: express.Response) {
110 const videoBlacklist = res.locals.videoBlacklist 111 const videoBlacklist = res.locals.videoBlacklist
111 const video = res.locals.video 112 const video = res.locals.videoAll
112 113
113 const videoBlacklistType = await sequelizeTypescript.transaction(async t => { 114 const videoBlacklistType = await sequelizeTypescript.transaction(async t => {
114 const unfederated = videoBlacklist.unfederated 115 const unfederated = videoBlacklist.unfederated
@@ -135,7 +136,7 @@ async function removeVideoFromBlacklistController (req: express.Request, res: ex
135 Notifier.Instance.notifyOnNewVideoIfNeeded(video) 136 Notifier.Instance.notifyOnNewVideoIfNeeded(video)
136 } 137 }
137 138
138 logger.info('Video %s removed from blacklist.', res.locals.video.uuid) 139 logger.info('Video %s removed from blacklist.', video.uuid)
139 140
140 return res.type('json').status(204).end() 141 return res.type('json').status(204).end()
141} 142}
diff --git a/server/controllers/api/videos/captions.ts b/server/controllers/api/videos/captions.ts
index 44c255232..37481d12f 100644
--- a/server/controllers/api/videos/captions.ts
+++ b/server/controllers/api/videos/captions.ts
@@ -10,6 +10,7 @@ import { federateVideoIfNeeded } from '../../../lib/activitypub'
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'
13import { MVideoCaptionVideo } from '@server/typings/models'
13 14
14const reqVideoCaptionAdd = createReqFiles( 15const reqVideoCaptionAdd = createReqFiles(
15 [ 'captionfile' ], 16 [ 'captionfile' ],
@@ -46,19 +47,19 @@ export {
46// --------------------------------------------------------------------------- 47// ---------------------------------------------------------------------------
47 48
48async function listVideoCaptions (req: express.Request, res: express.Response) { 49async function listVideoCaptions (req: express.Request, res: express.Response) {
49 const data = await VideoCaptionModel.listVideoCaptions(res.locals.video.id) 50 const data = await VideoCaptionModel.listVideoCaptions(res.locals.videoId.id)
50 51
51 return res.json(getFormattedObjects(data, data.length)) 52 return res.json(getFormattedObjects(data, data.length))
52} 53}
53 54
54async function addVideoCaption (req: express.Request, res: express.Response) { 55async function addVideoCaption (req: express.Request, res: express.Response) {
55 const videoCaptionPhysicalFile = req.files['captionfile'][0] 56 const videoCaptionPhysicalFile = req.files['captionfile'][0]
56 const video = res.locals.video 57 const video = res.locals.videoAll
57 58
58 const videoCaption = new VideoCaptionModel({ 59 const videoCaption = new VideoCaptionModel({
59 videoId: video.id, 60 videoId: video.id,
60 language: req.params.captionLanguage 61 language: req.params.captionLanguage
61 }) 62 }) as MVideoCaptionVideo
62 videoCaption.Video = video 63 videoCaption.Video = video
63 64
64 // Move physical file 65 // Move physical file
@@ -75,7 +76,7 @@ async function addVideoCaption (req: express.Request, res: express.Response) {
75} 76}
76 77
77async function deleteVideoCaption (req: express.Request, res: express.Response) { 78async function deleteVideoCaption (req: express.Request, res: express.Response) {
78 const video = res.locals.video 79 const video = res.locals.videoAll
79 const videoCaption = res.locals.videoCaption 80 const videoCaption = res.locals.videoCaption
80 81
81 await sequelizeTypescript.transaction(async t => { 82 await sequelizeTypescript.transaction(async t => {
diff --git a/server/controllers/api/videos/comment.ts b/server/controllers/api/videos/comment.ts
index bc6d81a7c..b2b06b170 100644
--- a/server/controllers/api/videos/comment.ts
+++ b/server/controllers/api/videos/comment.ts
@@ -27,9 +27,6 @@ import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../.
27import { AccountModel } from '../../../models/account/account' 27import { AccountModel } from '../../../models/account/account'
28import { Notifier } from '../../../lib/notifier' 28import { Notifier } from '../../../lib/notifier'
29import { Hooks } from '../../../lib/plugins/hooks' 29import { Hooks } from '../../../lib/plugins/hooks'
30import { ActorModel } from '../../../models/activitypub/actor'
31import { VideoChannelModel } from '../../../models/video/video-channel'
32import { VideoModel } from '../../../models/video/video'
33import { sendDeleteVideoComment } from '../../../lib/activitypub/send' 30import { sendDeleteVideoComment } from '../../../lib/activitypub/send'
34 31
35const auditLogger = auditLoggerFactory('comments') 32const auditLogger = auditLoggerFactory('comments')
@@ -75,7 +72,7 @@ export {
75// --------------------------------------------------------------------------- 72// ---------------------------------------------------------------------------
76 73
77async function listVideoThreads (req: express.Request, res: express.Response) { 74async function listVideoThreads (req: express.Request, res: express.Response) {
78 const video = res.locals.video 75 const video = res.locals.onlyVideo
79 const user = res.locals.oauth ? res.locals.oauth.token.User : undefined 76 const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
80 77
81 let resultList: ResultList<VideoCommentModel> 78 let resultList: ResultList<VideoCommentModel>
@@ -86,7 +83,7 @@ async function listVideoThreads (req: express.Request, res: express.Response) {
86 start: req.query.start, 83 start: req.query.start,
87 count: req.query.count, 84 count: req.query.count,
88 sort: req.query.sort, 85 sort: req.query.sort,
89 user: user 86 user
90 }, 'filter:api.video-threads.list.params') 87 }, 'filter:api.video-threads.list.params')
91 88
92 resultList = await Hooks.wrapPromiseFun( 89 resultList = await Hooks.wrapPromiseFun(
@@ -105,7 +102,7 @@ async function listVideoThreads (req: express.Request, res: express.Response) {
105} 102}
106 103
107async function listVideoThreadComments (req: express.Request, res: express.Response) { 104async function listVideoThreadComments (req: express.Request, res: express.Response) {
108 const video = res.locals.video 105 const video = res.locals.onlyVideo
109 const user = res.locals.oauth ? res.locals.oauth.token.User : undefined 106 const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
110 107
111 let resultList: ResultList<VideoCommentModel> 108 let resultList: ResultList<VideoCommentModel>
@@ -141,7 +138,7 @@ async function addVideoCommentThread (req: express.Request, res: express.Respons
141 return createVideoComment({ 138 return createVideoComment({
142 text: videoCommentInfo.text, 139 text: videoCommentInfo.text,
143 inReplyToComment: null, 140 inReplyToComment: null,
144 video: res.locals.video, 141 video: res.locals.videoAll,
145 account 142 account
146 }, t) 143 }, t)
147 }) 144 })
@@ -164,8 +161,8 @@ async function addVideoCommentReply (req: express.Request, res: express.Response
164 161
165 return createVideoComment({ 162 return createVideoComment({
166 text: videoCommentInfo.text, 163 text: videoCommentInfo.text,
167 inReplyToComment: res.locals.videoComment, 164 inReplyToComment: res.locals.videoCommentFull,
168 video: res.locals.video, 165 video: res.locals.videoAll,
169 account 166 account
170 }, t) 167 }, t)
171 }) 168 })
@@ -179,7 +176,7 @@ async function addVideoCommentReply (req: express.Request, res: express.Response
179} 176}
180 177
181async function removeVideoComment (req: express.Request, res: express.Response) { 178async function removeVideoComment (req: express.Request, res: express.Response) {
182 const videoCommentInstance = res.locals.videoComment 179 const videoCommentInstance = res.locals.videoCommentFull
183 180
184 await sequelizeTypescript.transaction(async t => { 181 await sequelizeTypescript.transaction(async t => {
185 await videoCommentInstance.destroy({ transaction: t }) 182 await videoCommentInstance.destroy({ transaction: t })
diff --git a/server/controllers/api/videos/import.ts b/server/controllers/api/videos/import.ts
index 04c9b547b..28ced5836 100644
--- a/server/controllers/api/videos/import.ts
+++ b/server/controllers/api/videos/import.ts
@@ -1,6 +1,5 @@
1import * as express from 'express' 1import * as express from 'express'
2import * as magnetUtil from 'magnet-uri' 2import * as magnetUtil from 'magnet-uri'
3import 'multer'
4import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger' 3import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger'
5import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoImportAddValidator } from '../../../middlewares' 4import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoImportAddValidator } from '../../../middlewares'
6import { MIMETYPES } from '../../../initializers/constants' 5import { MIMETYPES } from '../../../initializers/constants'
@@ -15,7 +14,6 @@ import { VideoImportModel } from '../../../models/video/video-import'
15import { JobQueue } from '../../../lib/job-queue/job-queue' 14import { JobQueue } from '../../../lib/job-queue/job-queue'
16import { join } from 'path' 15import { join } from 'path'
17import { isArray } from '../../../helpers/custom-validators/misc' 16import { isArray } from '../../../helpers/custom-validators/misc'
18import { VideoChannelModel } from '../../../models/video/video-channel'
19import * as Bluebird from 'bluebird' 17import * as Bluebird from 'bluebird'
20import * as parseTorrent from 'parse-torrent' 18import * as parseTorrent from 'parse-torrent'
21import { getSecureTorrentName } from '../../../helpers/utils' 19import { getSecureTorrentName } from '../../../helpers/utils'
@@ -25,8 +23,16 @@ import { CONFIG } from '../../../initializers/config'
25import { sequelizeTypescript } from '../../../initializers/database' 23import { sequelizeTypescript } from '../../../initializers/database'
26import { createVideoMiniatureFromExisting } from '../../../lib/thumbnail' 24import { createVideoMiniatureFromExisting } from '../../../lib/thumbnail'
27import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' 25import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
28import { ThumbnailModel } from '../../../models/video/thumbnail' 26import {
29import { UserModel } from '../../../models/account/user' 27 MChannelAccountDefault,
28 MThumbnail,
29 MUser,
30 MVideoAccountDefault,
31 MVideoTag,
32 MVideoThumbnailAccountDefault,
33 MVideoWithBlacklistLight
34} from '@server/typings/models'
35import { MVideoImport, MVideoImportFormattable } from '@server/typings/models/video/video-import'
30 36
31const auditLogger = auditLoggerFactory('video-imports') 37const auditLogger = auditLoggerFactory('video-imports')
32const videoImportsRouter = express.Router() 38const videoImportsRouter = express.Router()
@@ -184,8 +190,8 @@ function buildVideo (channelId: number, body: VideoImportCreate, importData: You
184 category: body.category || importData.category, 190 category: body.category || importData.category,
185 licence: body.licence || importData.licence, 191 licence: body.licence || importData.licence,
186 language: body.language || undefined, 192 language: body.language || undefined,
187 commentsEnabled: body.commentsEnabled || true, 193 commentsEnabled: body.commentsEnabled !== false, // If the value is not "false", the default is "true"
188 downloadEnabled: body.downloadEnabled || true, 194 downloadEnabled: body.downloadEnabled !== false,
189 waitTranscoding: body.waitTranscoding || false, 195 waitTranscoding: body.waitTranscoding || false,
190 state: VideoState.TO_IMPORT, 196 state: VideoState.TO_IMPORT,
191 nsfw: body.nsfw || importData.nsfw || false, 197 nsfw: body.nsfw || importData.nsfw || false,
@@ -225,28 +231,28 @@ async function processPreview (req: express.Request, video: VideoModel) {
225} 231}
226 232
227function insertIntoDB (parameters: { 233function insertIntoDB (parameters: {
228 video: VideoModel, 234 video: MVideoThumbnailAccountDefault,
229 thumbnailModel: ThumbnailModel, 235 thumbnailModel: MThumbnail,
230 previewModel: ThumbnailModel, 236 previewModel: MThumbnail,
231 videoChannel: VideoChannelModel, 237 videoChannel: MChannelAccountDefault,
232 tags: string[], 238 tags: string[],
233 videoImportAttributes: Partial<VideoImportModel>, 239 videoImportAttributes: Partial<MVideoImport>,
234 user: UserModel 240 user: MUser
235}): Bluebird<VideoImportModel> { 241}): Bluebird<MVideoImportFormattable> {
236 const { video, thumbnailModel, previewModel, videoChannel, tags, videoImportAttributes, user } = parameters 242 const { video, thumbnailModel, previewModel, videoChannel, tags, videoImportAttributes, user } = parameters
237 243
238 return sequelizeTypescript.transaction(async t => { 244 return sequelizeTypescript.transaction(async t => {
239 const sequelizeOptions = { transaction: t } 245 const sequelizeOptions = { transaction: t }
240 246
241 // Save video object in database 247 // Save video object in database
242 const videoCreated = await video.save(sequelizeOptions) 248 const videoCreated = await video.save(sequelizeOptions) as (MVideoAccountDefault & MVideoWithBlacklistLight & MVideoTag)
243 videoCreated.VideoChannel = videoChannel 249 videoCreated.VideoChannel = videoChannel
244 250
245 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t) 251 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
246 if (previewModel) await videoCreated.addAndSaveThumbnail(previewModel, t) 252 if (previewModel) await videoCreated.addAndSaveThumbnail(previewModel, t)
247 253
248 await autoBlacklistVideoIfNeeded({ 254 await autoBlacklistVideoIfNeeded({
249 video, 255 video: videoCreated,
250 user, 256 user,
251 notify: false, 257 notify: false,
252 isRemote: false, 258 isRemote: false,
@@ -268,7 +274,7 @@ function insertIntoDB (parameters: {
268 const videoImport = await VideoImportModel.create( 274 const videoImport = await VideoImportModel.create(
269 Object.assign({ videoId: videoCreated.id }, videoImportAttributes), 275 Object.assign({ videoId: videoCreated.id }, videoImportAttributes),
270 sequelizeOptions 276 sequelizeOptions
271 ) 277 ) as MVideoImportFormattable
272 videoImport.Video = videoCreated 278 videoImport.Video = videoCreated
273 279
274 return videoImport 280 return videoImport
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index 155ca4678..19da504c7 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -63,6 +63,7 @@ import { createVideoMiniatureFromExisting, generateVideoMiniature } from '../../
63import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' 63import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
64import { VideoTranscodingPayload } from '../../../lib/job-queue/handlers/video-transcoding' 64import { VideoTranscodingPayload } from '../../../lib/job-queue/handlers/video-transcoding'
65import { Hooks } from '../../../lib/plugins/hooks' 65import { Hooks } from '../../../lib/plugins/hooks'
66import { MVideoDetails, MVideoFullLight } from '@server/typings/models'
66 67
67const auditLogger = auditLoggerFactory('videos') 68const auditLogger = auditLoggerFactory('videos')
68const videosRouter = express.Router() 69const videosRouter = express.Router()
@@ -185,7 +186,7 @@ async function addVideo (req: express.Request, res: express.Response) {
185 licence: videoInfo.licence, 186 licence: videoInfo.licence,
186 language: videoInfo.language, 187 language: videoInfo.language,
187 commentsEnabled: videoInfo.commentsEnabled || false, 188 commentsEnabled: videoInfo.commentsEnabled || false,
188 downloadEnabled: videoInfo.downloadEnabled || true, 189 downloadEnabled: videoInfo.downloadEnabled !== false, // If the value is not "false", the default is "true"
189 waitTranscoding: videoInfo.waitTranscoding || false, 190 waitTranscoding: videoInfo.waitTranscoding || false,
190 state: CONFIG.TRANSCODING.ENABLED ? VideoState.TO_TRANSCODE : VideoState.PUBLISHED, 191 state: CONFIG.TRANSCODING.ENABLED ? VideoState.TO_TRANSCODE : VideoState.PUBLISHED,
191 nsfw: videoInfo.nsfw || false, 192 nsfw: videoInfo.nsfw || false,
@@ -197,7 +198,7 @@ async function addVideo (req: express.Request, res: express.Response) {
197 originallyPublishedAt: videoInfo.originallyPublishedAt 198 originallyPublishedAt: videoInfo.originallyPublishedAt
198 } 199 }
199 200
200 const video = new VideoModel(videoData) 201 const video = new VideoModel(videoData) as MVideoDetails
201 video.url = getVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object 202 video.url = getVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object
202 203
203 const videoFile = new VideoFileModel({ 204 const videoFile = new VideoFileModel({
@@ -238,7 +239,7 @@ async function addVideo (req: express.Request, res: express.Response) {
238 const { videoCreated } = await sequelizeTypescript.transaction(async t => { 239 const { videoCreated } = await sequelizeTypescript.transaction(async t => {
239 const sequelizeOptions = { transaction: t } 240 const sequelizeOptions = { transaction: t }
240 241
241 const videoCreated = await video.save(sequelizeOptions) 242 const videoCreated = await video.save(sequelizeOptions) as MVideoFullLight
242 243
243 await videoCreated.addAndSaveThumbnail(thumbnailModel, t) 244 await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
244 await videoCreated.addAndSaveThumbnail(previewModel, t) 245 await videoCreated.addAndSaveThumbnail(previewModel, t)
@@ -318,7 +319,7 @@ async function addVideo (req: express.Request, res: express.Response) {
318} 319}
319 320
320async function updateVideo (req: express.Request, res: express.Response) { 321async function updateVideo (req: express.Request, res: express.Response) {
321 const videoInstance = res.locals.video 322 const videoInstance = res.locals.videoAll
322 const videoFieldsSave = videoInstance.toJSON() 323 const videoFieldsSave = videoInstance.toJSON()
323 const oldVideoAuditView = new VideoAuditView(videoInstance.toFormattedDetailsJSON()) 324 const oldVideoAuditView = new VideoAuditView(videoInstance.toFormattedDetailsJSON())
324 const videoInfoToUpdate: VideoUpdate = req.body 325 const videoInfoToUpdate: VideoUpdate = req.body
@@ -371,7 +372,7 @@ async function updateVideo (req: express.Request, res: express.Response) {
371 } 372 }
372 } 373 }
373 374
374 const videoInstanceUpdated = await videoInstance.save(sequelizeOptions) 375 const videoInstanceUpdated = await videoInstance.save(sequelizeOptions) as MVideoFullLight
375 376
376 if (thumbnailModel) await videoInstanceUpdated.addAndSaveThumbnail(thumbnailModel, t) 377 if (thumbnailModel) await videoInstanceUpdated.addAndSaveThumbnail(thumbnailModel, t)
377 if (previewModel) await videoInstanceUpdated.addAndSaveThumbnail(previewModel, t) 378 if (previewModel) await videoInstanceUpdated.addAndSaveThumbnail(previewModel, t)
@@ -447,7 +448,7 @@ async function getVideo (req: express.Request, res: express.Response) {
447 448
448 const video = await Hooks.wrapPromiseFun( 449 const video = await Hooks.wrapPromiseFun(
449 VideoModel.loadForGetAPI, 450 VideoModel.loadForGetAPI,
450 { id: res.locals.video.id, userId }, 451 { id: res.locals.onlyVideoWithRights.id, userId },
451 'filter:api.video.get.result' 452 'filter:api.video.get.result'
452 ) 453 )
453 454
@@ -460,7 +461,7 @@ async function getVideo (req: express.Request, res: express.Response) {
460} 461}
461 462
462async function viewVideo (req: express.Request, res: express.Response) { 463async function viewVideo (req: express.Request, res: express.Response) {
463 const videoInstance = res.locals.video 464 const videoInstance = res.locals.videoAll
464 465
465 const ip = req.ip 466 const ip = req.ip
466 const exists = await Redis.Instance.doesVideoIPViewExist(ip, videoInstance.uuid) 467 const exists = await Redis.Instance.doesVideoIPViewExist(ip, videoInstance.uuid)
@@ -483,7 +484,7 @@ async function viewVideo (req: express.Request, res: express.Response) {
483} 484}
484 485
485async function getVideoDescription (req: express.Request, res: express.Response) { 486async function getVideoDescription (req: express.Request, res: express.Response) {
486 const videoInstance = res.locals.video 487 const videoInstance = res.locals.videoAll
487 let description = '' 488 let description = ''
488 489
489 if (videoInstance.isOwned()) { 490 if (videoInstance.isOwned()) {
@@ -522,7 +523,7 @@ async function listVideos (req: express.Request, res: express.Response) {
522} 523}
523 524
524async function removeVideo (req: express.Request, res: express.Response) { 525async function removeVideo (req: express.Request, res: express.Response) {
525 const videoInstance = res.locals.video 526 const videoInstance = res.locals.videoAll
526 527
527 await sequelizeTypescript.transaction(async t => { 528 await sequelizeTypescript.transaction(async t => {
528 await videoInstance.destroy({ transaction: t }) 529 await videoInstance.destroy({ transaction: t })
diff --git a/server/controllers/api/videos/ownership.ts b/server/controllers/api/videos/ownership.ts
index 5272c1385..abb34082e 100644
--- a/server/controllers/api/videos/ownership.ts
+++ b/server/controllers/api/videos/ownership.ts
@@ -18,6 +18,7 @@ import { getFormattedObjects } from '../../../helpers/utils'
18import { changeVideoChannelShare } from '../../../lib/activitypub' 18import { changeVideoChannelShare } from '../../../lib/activitypub'
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'
21 22
22const ownershipVideoRouter = express.Router() 23const ownershipVideoRouter = express.Router()
23 24
@@ -56,7 +57,7 @@ export {
56// --------------------------------------------------------------------------- 57// ---------------------------------------------------------------------------
57 58
58async function giveVideoOwnership (req: express.Request, res: express.Response) { 59async function giveVideoOwnership (req: express.Request, res: express.Response) {
59 const videoInstance = res.locals.video 60 const videoInstance = res.locals.videoAll
60 const initiatorAccountId = res.locals.oauth.token.User.Account.id 61 const initiatorAccountId = res.locals.oauth.token.User.Account.id
61 const nextOwner = res.locals.nextOwner 62 const nextOwner = res.locals.nextOwner
62 63
@@ -107,7 +108,7 @@ async function acceptOwnership (req: express.Request, res: express.Response) {
107 108
108 targetVideo.channelId = channel.id 109 targetVideo.channelId = channel.id
109 110
110 const targetVideoUpdated = await targetVideo.save({ transaction: t }) 111 const targetVideoUpdated = await targetVideo.save({ transaction: t }) as MVideoFullLight
111 targetVideoUpdated.VideoChannel = channel 112 targetVideoUpdated.VideoChannel = channel
112 113
113 if (targetVideoUpdated.privacy !== VideoPrivacy.PRIVATE && targetVideoUpdated.state === VideoState.PUBLISHED) { 114 if (targetVideoUpdated.privacy !== VideoPrivacy.PRIVATE && targetVideoUpdated.state === VideoState.PUBLISHED) {
diff --git a/server/controllers/api/videos/rate.ts b/server/controllers/api/videos/rate.ts
index b65babedf..3d2f3d728 100644
--- a/server/controllers/api/videos/rate.ts
+++ b/server/controllers/api/videos/rate.ts
@@ -27,7 +27,7 @@ export {
27async function rateVideo (req: express.Request, res: express.Response) { 27async function rateVideo (req: express.Request, res: express.Response) {
28 const body: UserVideoRateUpdate = req.body 28 const body: UserVideoRateUpdate = req.body
29 const rateType = body.rating 29 const rateType = body.rating
30 const videoInstance = res.locals.video 30 const videoInstance = res.locals.videoAll
31 const userAccount = res.locals.oauth.token.User.Account 31 const userAccount = res.locals.oauth.token.User.Account
32 32
33 await sequelizeTypescript.transaction(async t => { 33 await sequelizeTypescript.transaction(async t => {
diff --git a/server/controllers/api/videos/watching.ts b/server/controllers/api/videos/watching.ts
index dcd1f070d..036e16f3a 100644
--- a/server/controllers/api/videos/watching.ts
+++ b/server/controllers/api/videos/watching.ts
@@ -23,7 +23,7 @@ async function userWatchVideo (req: express.Request, res: express.Response) {
23 const user = res.locals.oauth.token.User 23 const user = res.locals.oauth.token.User
24 24
25 const body: UserWatchingVideo = req.body 25 const body: UserWatchingVideo = req.body
26 const { id: videoId } = res.locals.video as { id: number } 26 const { id: videoId } = res.locals.videoId
27 27
28 await UserVideoHistoryModel.upsert({ 28 await UserVideoHistoryModel.upsert({
29 videoId, 29 videoId,
diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts
index d3f581615..468f7a668 100644
--- a/server/controllers/feeds.ts
+++ b/server/controllers/feeds.ts
@@ -43,7 +43,7 @@ export {
43async function generateVideoCommentsFeed (req: express.Request, res: express.Response) { 43async function generateVideoCommentsFeed (req: express.Request, res: express.Response) {
44 const start = 0 44 const start = 0
45 45
46 const video = res.locals.video 46 const video = res.locals.videoAll
47 const videoId: number = video ? video.id : undefined 47 const videoId: number = video ? video.id : undefined
48 48
49 const comments = await VideoCommentModel.listForFeed(start, FEEDS.COUNT, videoId) 49 const comments = await VideoCommentModel.listForFeed(start, FEEDS.COUNT, videoId)
diff --git a/server/controllers/services.ts b/server/controllers/services.ts
index c1c53c3fc..ec057235f 100644
--- a/server/controllers/services.ts
+++ b/server/controllers/services.ts
@@ -23,7 +23,7 @@ export {
23// --------------------------------------------------------------------------- 23// ---------------------------------------------------------------------------
24 24
25function generateOEmbed (req: express.Request, res: express.Response) { 25function generateOEmbed (req: express.Request, res: express.Response) {
26 const video = res.locals.video 26 const video = res.locals.videoAll
27 const webserverUrl = WEBSERVER.URL 27 const webserverUrl = WEBSERVER.URL
28 const maxHeight = parseInt(req.query.maxheight, 10) 28 const maxHeight = parseInt(req.query.maxheight, 10)
29 const maxWidth = parseInt(req.query.maxwidth, 10) 29 const maxWidth = parseInt(req.query.maxwidth, 10)
diff --git a/server/controllers/static.ts b/server/controllers/static.ts
index 8979ef5f3..0f4772310 100644
--- a/server/controllers/static.ts
+++ b/server/controllers/static.ts
@@ -226,14 +226,14 @@ async function generateNodeinfo (req: express.Request, res: express.Response) {
226 return res.send(json).end() 226 return res.send(json).end()
227} 227}
228 228
229async function downloadTorrent (req: express.Request, res: express.Response, next: express.NextFunction) { 229async function downloadTorrent (req: express.Request, res: express.Response) {
230 const { video, videoFile } = getVideoAndFile(req, res) 230 const { video, videoFile } = getVideoAndFile(req, res)
231 if (!videoFile) return res.status(404).end() 231 if (!videoFile) return res.status(404).end()
232 232
233 return res.download(video.getTorrentFilePath(videoFile), `${video.name}-${videoFile.resolution}p.torrent`) 233 return res.download(video.getTorrentFilePath(videoFile), `${video.name}-${videoFile.resolution}p.torrent`)
234} 234}
235 235
236async function downloadVideoFile (req: express.Request, res: express.Response, next: express.NextFunction) { 236async function downloadVideoFile (req: express.Request, res: express.Response) {
237 const { video, videoFile } = getVideoAndFile(req, res) 237 const { video, videoFile } = getVideoAndFile(req, res)
238 if (!videoFile) return res.status(404).end() 238 if (!videoFile) return res.status(404).end()
239 239
@@ -242,7 +242,7 @@ async function downloadVideoFile (req: express.Request, res: express.Response, n
242 242
243function getVideoAndFile (req: express.Request, res: express.Response) { 243function getVideoAndFile (req: express.Request, res: express.Response) {
244 const resolution = parseInt(req.params.resolution, 10) 244 const resolution = parseInt(req.params.resolution, 10)
245 const video = res.locals.video 245 const video = res.locals.videoAll
246 246
247 const videoFile = video.VideoFiles.find(f => f.resolution === resolution) 247 const videoFile = video.VideoFiles.find(f => f.resolution === resolution)
248 248
diff --git a/server/controllers/webfinger.ts b/server/controllers/webfinger.ts
index f2ba3c826..fc9575160 100644
--- a/server/controllers/webfinger.ts
+++ b/server/controllers/webfinger.ts
@@ -18,7 +18,7 @@ export {
18// --------------------------------------------------------------------------- 18// ---------------------------------------------------------------------------
19 19
20function webfingerController (req: express.Request, res: express.Response) { 20function webfingerController (req: express.Request, res: express.Response) {
21 const actor = res.locals.actor 21 const actor = res.locals.actorFull
22 22
23 const json = { 23 const json = {
24 subject: req.query.resource, 24 subject: req.query.resource,
diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts
index 951a25669..97c809a0c 100644
--- a/server/helpers/activitypub.ts
+++ b/server/helpers/activitypub.ts
@@ -7,6 +7,7 @@ import { ActorModel } from '../models/activitypub/actor'
7import { signJsonLDObject } from './peertube-crypto' 7import { signJsonLDObject } from './peertube-crypto'
8import { pageToStartAndCount } from './core-utils' 8import { pageToStartAndCount } from './core-utils'
9import { parse } from 'url' 9import { parse } from 'url'
10import { MActor } from '../typings/models'
10 11
11function activityPubContextify <T> (data: T) { 12function activityPubContextify <T> (data: T) {
12 return Object.assign(data, { 13 return Object.assign(data, {
@@ -143,7 +144,7 @@ async function activityPubCollectionPagination (baseUrl: string, handler: Activi
143 144
144} 145}
145 146
146function buildSignedActivity (byActor: ActorModel, data: Object) { 147function buildSignedActivity (byActor: MActor, data: Object) {
147 const activity = activityPubContextify(data) 148 const activity = activityPubContextify(data)
148 149
149 return signJsonLDObject(byActor, activity) as Promise<Activity> 150 return signJsonLDObject(byActor, activity) as Promise<Activity>
diff --git a/server/helpers/actor.ts b/server/helpers/actor.ts
index 12a7ace9f..117548a60 100644
--- a/server/helpers/actor.ts
+++ b/server/helpers/actor.ts
@@ -1,10 +1,13 @@
1import { ActorModel } from '../models/activitypub/actor' 1import { ActorModel } from '../models/activitypub/actor'
2import * as Bluebird from 'bluebird'
3import { MActorFull, MActorAccountChannelId } from '../typings/models'
2 4
3type ActorFetchByUrlType = 'all' | 'actor-and-association-ids' 5type ActorFetchByUrlType = 'all' | 'association-ids'
4function fetchActorByUrl (url: string, fetchType: ActorFetchByUrlType) { 6
7function fetchActorByUrl (url: string, fetchType: ActorFetchByUrlType): Bluebird<MActorFull | MActorAccountChannelId> {
5 if (fetchType === 'all') return ActorModel.loadByUrlAndPopulateAccountAndChannel(url) 8 if (fetchType === 'all') return ActorModel.loadByUrlAndPopulateAccountAndChannel(url)
6 9
7 if (fetchType === 'actor-and-association-ids') return ActorModel.loadByUrl(url) 10 if (fetchType === 'association-ids') return ActorModel.loadByUrl(url)
8} 11}
9 12
10export { 13export {
diff --git a/server/helpers/captions-utils.ts b/server/helpers/captions-utils.ts
index 7174d4654..2830ae017 100644
--- a/server/helpers/captions-utils.ts
+++ b/server/helpers/captions-utils.ts
@@ -1,10 +1,10 @@
1import { join } from 'path' 1import { join } from 'path'
2import { CONFIG } from '../initializers/config' 2import { CONFIG } from '../initializers/config'
3import { VideoCaptionModel } from '../models/video/video-caption'
4import * as srt2vtt from 'srt-to-vtt' 3import * as srt2vtt from 'srt-to-vtt'
5import { createReadStream, createWriteStream, remove, move } from 'fs-extra' 4import { createReadStream, createWriteStream, move, remove } from 'fs-extra'
5import { MVideoCaptionFormattable } from '@server/typings/models'
6 6
7async function moveAndProcessCaptionFile (physicalFile: { filename: string, path: string }, videoCaption: VideoCaptionModel) { 7async function moveAndProcessCaptionFile (physicalFile: { filename: string, path: string }, videoCaption: MVideoCaptionFormattable) {
8 const videoCaptionsDir = CONFIG.STORAGE.CAPTIONS_DIR 8 const videoCaptionsDir = CONFIG.STORAGE.CAPTIONS_DIR
9 const destination = join(videoCaptionsDir, videoCaption.getCaptionName()) 9 const destination = join(videoCaptionsDir, videoCaption.getCaptionName())
10 10
diff --git a/server/helpers/custom-jsonld-signature.ts b/server/helpers/custom-jsonld-signature.ts
index a3bceb047..cb07fa3b2 100644
--- a/server/helpers/custom-jsonld-signature.ts
+++ b/server/helpers/custom-jsonld-signature.ts
@@ -1,6 +1,5 @@
1import * as AsyncLRU from 'async-lru' 1import * as AsyncLRU from 'async-lru'
2import * as jsonld from 'jsonld' 2import * as jsonld from 'jsonld'
3import * as jsig from 'jsonld-signatures'
4import { logger } from './logger' 3import { logger } from './logger'
5 4
6const CACHE = { 5const CACHE = {
@@ -79,6 +78,4 @@ jsonld.documentLoader = (url, cb) => {
79 lru.get(url, cb) 78 lru.get(url, cb)
80} 79}
81 80
82jsig.use('jsonld', jsonld) 81export { jsonld }
83
84export { jsig, jsonld }
diff --git a/server/helpers/custom-validators/activitypub/actor.ts b/server/helpers/custom-validators/activitypub/actor.ts
index deb331abb..55bc8cc96 100644
--- a/server/helpers/custom-validators/activitypub/actor.ts
+++ b/server/helpers/custom-validators/activitypub/actor.ts
@@ -27,7 +27,7 @@ function isActorPublicKeyValid (publicKey: string) {
27 validator.isLength(publicKey, CONSTRAINTS_FIELDS.ACTORS.PUBLIC_KEY) 27 validator.isLength(publicKey, CONSTRAINTS_FIELDS.ACTORS.PUBLIC_KEY)
28} 28}
29 29
30const actorNameAlphabet = '[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\\-_.]' 30const actorNameAlphabet = '[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\\-_.:]'
31const actorNameRegExp = new RegExp(`^${actorNameAlphabet}+$`) 31const actorNameRegExp = new RegExp(`^${actorNameAlphabet}+$`)
32function isActorPreferredUsernameValid (preferredUsername: string) { 32function isActorPreferredUsernameValid (preferredUsername: string) {
33 return exists(preferredUsername) && validator.matches(preferredUsername, actorNameRegExp) 33 return exists(preferredUsername) && validator.matches(preferredUsername, actorNameRegExp)
@@ -46,19 +46,20 @@ function isActorObjectValid (actor: any) {
46 return exists(actor) && 46 return exists(actor) &&
47 isActivityPubUrlValid(actor.id) && 47 isActivityPubUrlValid(actor.id) &&
48 isActorTypeValid(actor.type) && 48 isActorTypeValid(actor.type) &&
49 isActivityPubUrlValid(actor.following) &&
50 isActivityPubUrlValid(actor.followers) &&
51 isActivityPubUrlValid(actor.inbox) && 49 isActivityPubUrlValid(actor.inbox) &&
52 isActivityPubUrlValid(actor.outbox) &&
53 isActorPreferredUsernameValid(actor.preferredUsername) && 50 isActorPreferredUsernameValid(actor.preferredUsername) &&
54 isActivityPubUrlValid(actor.url) && 51 isActivityPubUrlValid(actor.url) &&
55 isActorPublicKeyObjectValid(actor.publicKey) && 52 isActorPublicKeyObjectValid(actor.publicKey) &&
56 isActorEndpointsObjectValid(actor.endpoints) && 53 isActorEndpointsObjectValid(actor.endpoints) &&
57 setValidAttributedTo(actor) &&
58 54
59 // If this is not an account, it should be attributed to an account 55 (!actor.outbox || isActivityPubUrlValid(actor.outbox)) &&
56 (!actor.following || isActivityPubUrlValid(actor.following)) &&
57 (!actor.followers || isActivityPubUrlValid(actor.followers)) &&
58
59 setValidAttributedTo(actor) &&
60 // If this is a group (a channel), it should be attributed to an account
60 // In PeerTube we use this to attach a video channel to a specific account 61 // In PeerTube we use this to attach a video channel to a specific account
61 (actor.type === 'Person' || actor.attributedTo.length !== 0) 62 (actor.type !== 'Group' || actor.attributedTo.length !== 0)
62} 63}
63 64
64function isActorFollowingCountValid (value: string) { 65function isActorFollowingCountValid (value: string) {
diff --git a/server/helpers/custom-validators/users.ts b/server/helpers/custom-validators/users.ts
index c56ae14ef..68e84d9eb 100644
--- a/server/helpers/custom-validators/users.ts
+++ b/server/helpers/custom-validators/users.ts
@@ -65,6 +65,14 @@ function isUserBlockedValid (value: any) {
65 return isBooleanValid(value) 65 return isBooleanValid(value)
66} 66}
67 67
68function isNoInstanceConfigWarningModal (value: any) {
69 return isBooleanValid(value)
70}
71
72function isNoWelcomeModal (value: any) {
73 return isBooleanValid(value)
74}
75
68function isUserBlockedReasonValid (value: any) { 76function isUserBlockedReasonValid (value: any) {
69 return value === null || (exists(value) && validator.isLength(value, CONSTRAINTS_FIELDS.USERS.BLOCKED_REASON)) 77 return value === null || (exists(value) && validator.isLength(value, CONSTRAINTS_FIELDS.USERS.BLOCKED_REASON))
70} 78}
@@ -100,5 +108,7 @@ export {
100 isUserAutoPlayVideoValid, 108 isUserAutoPlayVideoValid,
101 isUserDisplayNameValid, 109 isUserDisplayNameValid,
102 isUserDescriptionValid, 110 isUserDescriptionValid,
111 isNoInstanceConfigWarningModal,
112 isNoWelcomeModal,
103 isAvatarFile 113 isAvatarFile
104} 114}
diff --git a/server/helpers/custom-validators/video-ownership.ts b/server/helpers/custom-validators/video-ownership.ts
index a7771e07b..9570b2799 100644
--- a/server/helpers/custom-validators/video-ownership.ts
+++ b/server/helpers/custom-validators/video-ownership.ts
@@ -1,10 +1,10 @@
1import { Response } from 'express' 1import { Response } from 'express'
2import * as validator from 'validator'
3import { VideoChangeOwnershipModel } from '../../models/video/video-change-ownership' 2import { VideoChangeOwnershipModel } from '../../models/video/video-change-ownership'
4import { UserModel } from '../../models/account/user' 3import { MVideoChangeOwnershipFull } from '@server/typings/models/video/video-change-ownership'
4import { MUserId } from '@server/typings/models'
5 5
6export async function doesChangeVideoOwnershipExist (id: string, res: Response): Promise<boolean> { 6export async function doesChangeVideoOwnershipExist (id: number, res: Response) {
7 const videoChangeOwnership = await loadVideoChangeOwnership(id) 7 const videoChangeOwnership = await VideoChangeOwnershipModel.load(id)
8 8
9 if (!videoChangeOwnership) { 9 if (!videoChangeOwnership) {
10 res.status(404) 10 res.status(404)
@@ -18,19 +18,7 @@ export async function doesChangeVideoOwnershipExist (id: string, res: Response):
18 return true 18 return true
19} 19}
20 20
21async function loadVideoChangeOwnership (id: string): Promise<VideoChangeOwnershipModel | undefined> { 21export function checkUserCanTerminateOwnershipChange (user: MUserId, videoChangeOwnership: MVideoChangeOwnershipFull, res: Response) {
22 if (validator.isInt(id)) {
23 return VideoChangeOwnershipModel.load(parseInt(id, 10))
24 }
25
26 return undefined
27}
28
29export function checkUserCanTerminateOwnershipChange (
30 user: UserModel,
31 videoChangeOwnership: VideoChangeOwnershipModel,
32 res: Response
33): boolean {
34 if (videoChangeOwnership.NextOwner.userId === user.id) { 22 if (videoChangeOwnership.NextOwner.userId === user.id) {
35 return true 23 return true
36 } 24 }
diff --git a/server/helpers/middlewares/accounts.ts b/server/helpers/middlewares/accounts.ts
index 791022b97..f5aa0bada 100644
--- a/server/helpers/middlewares/accounts.ts
+++ b/server/helpers/middlewares/accounts.ts
@@ -1,6 +1,7 @@
1import { Response } from 'express' 1import { Response } from 'express'
2import { AccountModel } from '../../models/account/account' 2import { AccountModel } from '../../models/account/account'
3import * as Bluebird from 'bluebird' 3import * as Bluebird from 'bluebird'
4import { MAccountDefault } from '../../typings/models'
4 5
5function doesAccountIdExist (id: number, res: Response, sendNotFound = true) { 6function doesAccountIdExist (id: number, res: Response, sendNotFound = true) {
6 const promise = AccountModel.load(id) 7 const promise = AccountModel.load(id)
@@ -15,10 +16,12 @@ function doesLocalAccountNameExist (name: string, res: Response, sendNotFound =
15} 16}
16 17
17function doesAccountNameWithHostExist (nameWithDomain: string, res: Response, sendNotFound = true) { 18function doesAccountNameWithHostExist (nameWithDomain: string, res: Response, sendNotFound = true) {
18 return doesAccountExist(AccountModel.loadByNameWithHost(nameWithDomain), res, sendNotFound) 19 const promise = AccountModel.loadByNameWithHost(nameWithDomain)
20
21 return doesAccountExist(promise, res, sendNotFound)
19} 22}
20 23
21async function doesAccountExist (p: Bluebird<AccountModel>, res: Response, sendNotFound: boolean) { 24async function doesAccountExist (p: Bluebird<MAccountDefault>, res: Response, sendNotFound: boolean) {
22 const account = await p 25 const account = await p
23 26
24 if (!account) { 27 if (!account) {
diff --git a/server/helpers/middlewares/video-abuses.ts b/server/helpers/middlewares/video-abuses.ts
index b23f1f021..1b573ca37 100644
--- a/server/helpers/middlewares/video-abuses.ts
+++ b/server/helpers/middlewares/video-abuses.ts
@@ -1,41 +1,23 @@
1import * as express from 'express' 1import { Response } from 'express'
2import { VideoChannelModel } from '../../models/video/video-channel' 2import { VideoAbuseModel } from '../../models/video/video-abuse'
3 3
4async function doesLocalVideoChannelNameExist (name: string, res: express.Response) { 4async function doesVideoAbuseExist (abuseId: number, videoId: number, res: Response) {
5 const videoChannel = await VideoChannelModel.loadLocalByNameAndPopulateAccount(name) 5 const videoAbuse = await VideoAbuseModel.loadByIdAndVideoId(abuseId, videoId)
6 6
7 return processVideoChannelExist(videoChannel, res) 7 if (videoAbuse === null) {
8}
9
10async function doesVideoChannelIdExist (id: number, res: express.Response) {
11 const videoChannel = await VideoChannelModel.loadAndPopulateAccount(+id)
12
13 return processVideoChannelExist(videoChannel, res)
14}
15
16async function doesVideoChannelNameWithHostExist (nameWithDomain: string, res: express.Response) {
17 const videoChannel = await VideoChannelModel.loadByNameWithHostAndPopulateAccount(nameWithDomain)
18
19 return processVideoChannelExist(videoChannel, res)
20}
21
22// ---------------------------------------------------------------------------
23
24export {
25 doesLocalVideoChannelNameExist,
26 doesVideoChannelIdExist,
27 doesVideoChannelNameWithHostExist
28}
29
30function processVideoChannelExist (videoChannel: VideoChannelModel, res: express.Response) {
31 if (!videoChannel) {
32 res.status(404) 8 res.status(404)
33 .json({ error: 'Video channel not found' }) 9 .json({ error: 'Video abuse not found' })
34 .end() 10 .end()
35 11
36 return false 12 return false
37 } 13 }
38 14
39 res.locals.videoChannel = videoChannel 15 res.locals.videoAbuse = videoAbuse
40 return true 16 return true
41} 17}
18
19// ---------------------------------------------------------------------------
20
21export {
22 doesVideoAbuseExist
23}
diff --git a/server/helpers/middlewares/video-captions.ts b/server/helpers/middlewares/video-captions.ts
index dc3d0144b..1b2513b60 100644
--- a/server/helpers/middlewares/video-captions.ts
+++ b/server/helpers/middlewares/video-captions.ts
@@ -1,8 +1,8 @@
1import { VideoModel } from '../../models/video/video'
2import { Response } from 'express' 1import { Response } from 'express'
3import { VideoCaptionModel } from '../../models/video/video-caption' 2import { VideoCaptionModel } from '../../models/video/video-caption'
3import { MVideoId } from '@server/typings/models'
4 4
5async function doesVideoCaptionExist (video: VideoModel, language: string, res: Response) { 5async function doesVideoCaptionExist (video: MVideoId, language: string, res: Response) {
6 const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(video.id, language) 6 const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(video.id, language)
7 7
8 if (!videoCaption) { 8 if (!videoCaption) {
diff --git a/server/helpers/middlewares/video-channels.ts b/server/helpers/middlewares/video-channels.ts
index 1b573ca37..1595ecd94 100644
--- a/server/helpers/middlewares/video-channels.ts
+++ b/server/helpers/middlewares/video-channels.ts
@@ -1,23 +1,42 @@
1import { Response } from 'express' 1import * as express from 'express'
2import { VideoAbuseModel } from '../../models/video/video-abuse' 2import { VideoChannelModel } from '../../models/video/video-channel'
3import { MChannelAccountDefault } from '@server/typings/models'
3 4
4async function doesVideoAbuseExist (abuseId: number, videoId: number, res: Response) { 5async function doesLocalVideoChannelNameExist (name: string, res: express.Response) {
5 const videoAbuse = await VideoAbuseModel.loadByIdAndVideoId(abuseId, videoId) 6 const videoChannel = await VideoChannelModel.loadLocalByNameAndPopulateAccount(name)
6 7
7 if (videoAbuse === null) { 8 return processVideoChannelExist(videoChannel, res)
8 res.status(404) 9}
9 .json({ error: 'Video abuse not found' })
10 .end()
11 10
12 return false 11async function doesVideoChannelIdExist (id: number, res: express.Response) {
13 } 12 const videoChannel = await VideoChannelModel.loadAndPopulateAccount(+id)
14 13
15 res.locals.videoAbuse = videoAbuse 14 return processVideoChannelExist(videoChannel, res)
16 return true 15}
16
17async function doesVideoChannelNameWithHostExist (nameWithDomain: string, res: express.Response) {
18 const videoChannel = await VideoChannelModel.loadByNameWithHostAndPopulateAccount(nameWithDomain)
19
20 return processVideoChannelExist(videoChannel, res)
17} 21}
18 22
19// --------------------------------------------------------------------------- 23// ---------------------------------------------------------------------------
20 24
21export { 25export {
22 doesVideoAbuseExist 26 doesLocalVideoChannelNameExist,
27 doesVideoChannelIdExist,
28 doesVideoChannelNameWithHostExist
29}
30
31function processVideoChannelExist (videoChannel: MChannelAccountDefault, res: express.Response) {
32 if (!videoChannel) {
33 res.status(404)
34 .json({ error: 'Video channel not found' })
35 .end()
36
37 return false
38 }
39
40 res.locals.videoChannel = videoChannel
41 return true
23} 42}
diff --git a/server/helpers/middlewares/video-playlists.ts b/server/helpers/middlewares/video-playlists.ts
index 735bf362f..8e7484483 100644
--- a/server/helpers/middlewares/video-playlists.ts
+++ b/server/helpers/middlewares/video-playlists.ts
@@ -1,11 +1,31 @@
1import * as express from 'express' 1import * as express from 'express'
2import { VideoPlaylistModel } from '../../models/video/video-playlist' 2import { VideoPlaylistModel } from '../../models/video/video-playlist'
3import { MVideoPlaylist } from '../../typings/models/video/video-playlist'
3 4
4async function doesVideoPlaylistExist (id: number | string, res: express.Response, fetchType: 'summary' | 'all' = 'summary') { 5export type VideoPlaylistFetchType = 'summary' | 'all'
5 const videoPlaylist = fetchType === 'summary' 6async function doesVideoPlaylistExist (id: number | string, res: express.Response, fetchType: VideoPlaylistFetchType = 'summary') {
6 ? await VideoPlaylistModel.loadWithAccountAndChannelSummary(id, undefined) 7 if (fetchType === 'summary') {
7 : await VideoPlaylistModel.loadWithAccountAndChannel(id, undefined) 8 const videoPlaylist = await VideoPlaylistModel.loadWithAccountAndChannelSummary(id, undefined)
9 res.locals.videoPlaylistSummary = videoPlaylist
8 10
11 return handleVideoPlaylist(videoPlaylist, res)
12 }
13
14 const videoPlaylist = await VideoPlaylistModel.loadWithAccountAndChannel(id, undefined)
15 res.locals.videoPlaylistFull = videoPlaylist
16
17 return handleVideoPlaylist(videoPlaylist, res)
18}
19
20// ---------------------------------------------------------------------------
21
22export {
23 doesVideoPlaylistExist
24}
25
26// ---------------------------------------------------------------------------
27
28function handleVideoPlaylist (videoPlaylist: MVideoPlaylist, res: express.Response) {
9 if (!videoPlaylist) { 29 if (!videoPlaylist) {
10 res.status(404) 30 res.status(404)
11 .json({ error: 'Video playlist not found' }) 31 .json({ error: 'Video playlist not found' })
@@ -14,12 +34,5 @@ async function doesVideoPlaylistExist (id: number | string, res: express.Respons
14 return false 34 return false
15 } 35 }
16 36
17 res.locals.videoPlaylist = videoPlaylist
18 return true 37 return true
19} 38}
20
21// ---------------------------------------------------------------------------
22
23export {
24 doesVideoPlaylistExist
25}
diff --git a/server/helpers/middlewares/videos.ts b/server/helpers/middlewares/videos.ts
index ceb1058ec..74f529804 100644
--- a/server/helpers/middlewares/videos.ts
+++ b/server/helpers/middlewares/videos.ts
@@ -1,9 +1,8 @@
1import { Response } from 'express' 1import { Response } from 'express'
2import { fetchVideo, VideoFetchType } from '../video' 2import { fetchVideo, VideoFetchType } from '../video'
3import { UserModel } from '../../models/account/user'
4import { UserRight } from '../../../shared/models/users' 3import { UserRight } from '../../../shared/models/users'
5import { VideoChannelModel } from '../../models/video/video-channel' 4import { VideoChannelModel } from '../../models/video/video-channel'
6import { VideoModel } from '../../models/video/video' 5import { MUser, MUserAccountId, MVideoAccountLight, MVideoFullLight, MVideoThumbnail, MVideoWithRights } from '@server/typings/models'
7 6
8async function doesVideoExist (id: number | string, res: Response, fetchType: VideoFetchType = 'all') { 7async function doesVideoExist (id: number | string, res: Response, fetchType: VideoFetchType = 'all') {
9 const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined 8 const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined
@@ -18,11 +17,28 @@ async function doesVideoExist (id: number | string, res: Response, fetchType: Vi
18 return false 17 return false
19 } 18 }
20 19
21 if (fetchType !== 'none') res.locals.video = video 20 switch (fetchType) {
21 case 'all':
22 res.locals.videoAll = video as MVideoFullLight
23 break
24
25 case 'id':
26 res.locals.videoId = video
27 break
28
29 case 'only-video':
30 res.locals.onlyVideo = video as MVideoThumbnail
31 break
32
33 case 'only-video-with-rights':
34 res.locals.onlyVideoWithRights = video as MVideoWithRights
35 break
36 }
37
22 return true 38 return true
23} 39}
24 40
25async function doesVideoChannelOfAccountExist (channelId: number, user: UserModel, res: Response) { 41async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAccountId, res: Response) {
26 if (user.hasRight(UserRight.UPDATE_ANY_VIDEO) === true) { 42 if (user.hasRight(UserRight.UPDATE_ANY_VIDEO) === true) {
27 const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId) 43 const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId)
28 if (videoChannel === null) { 44 if (videoChannel === null) {
@@ -50,7 +66,7 @@ async function doesVideoChannelOfAccountExist (channelId: number, user: UserMode
50 return true 66 return true
51} 67}
52 68
53function checkUserCanManageVideo (user: UserModel, video: VideoModel, right: UserRight, res: Response) { 69function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right: UserRight, res: Response) {
54 // Retrieve the user who did the request 70 // Retrieve the user who did the request
55 if (video.isOwned() === false) { 71 if (video.isOwned() === false) {
56 res.status(403) 72 res.status(403)
diff --git a/server/helpers/peertube-crypto.ts b/server/helpers/peertube-crypto.ts
index 1424949d0..9eb782302 100644
--- a/server/helpers/peertube-crypto.ts
+++ b/server/helpers/peertube-crypto.ts
@@ -1,13 +1,13 @@
1import { Request } from 'express' 1import { Request } from 'express'
2import { BCRYPT_SALT_SIZE, HTTP_SIGNATURE, PRIVATE_RSA_KEY_SIZE } from '../initializers/constants' 2import { BCRYPT_SALT_SIZE, HTTP_SIGNATURE, PRIVATE_RSA_KEY_SIZE } from '../initializers/constants'
3import { ActorModel } from '../models/activitypub/actor'
4import { createPrivateKey, getPublicKey, promisify1, promisify2, sha256 } from './core-utils' 3import { createPrivateKey, getPublicKey, promisify1, promisify2, sha256 } from './core-utils'
5import { jsig, jsonld } from './custom-jsonld-signature' 4import { jsonld } from './custom-jsonld-signature'
6import { logger } from './logger' 5import { logger } from './logger'
7import { cloneDeep } from 'lodash' 6import { cloneDeep } from 'lodash'
8import { createVerify } from 'crypto' 7import { createSign, createVerify } from 'crypto'
9import { buildDigest } from '../lib/job-queue/handlers/utils/activitypub-http-utils' 8import { buildDigest } from '../lib/job-queue/handlers/utils/activitypub-http-utils'
10import * as bcrypt from 'bcrypt' 9import * as bcrypt from 'bcrypt'
10import { MActor } from '../typings/models'
11 11
12const bcryptComparePromise = promisify2<any, string, boolean>(bcrypt.compare) 12const bcryptComparePromise = promisify2<any, string, boolean>(bcrypt.compare)
13const bcryptGenSaltPromise = promisify1<number, string>(bcrypt.genSalt) 13const bcryptGenSaltPromise = promisify1<number, string>(bcrypt.genSalt)
@@ -46,7 +46,7 @@ function isHTTPSignatureDigestValid (rawBody: Buffer, req: Request): boolean {
46 return true 46 return true
47} 47}
48 48
49function isHTTPSignatureVerified (httpSignatureParsed: any, actor: ActorModel): boolean { 49function isHTTPSignatureVerified (httpSignatureParsed: any, actor: MActor): boolean {
50 return httpSignature.verifySignature(httpSignatureParsed, actor.publicKey) === true 50 return httpSignature.verifySignature(httpSignatureParsed, actor.publicKey) === true
51} 51}
52 52
@@ -56,70 +56,21 @@ function parseHTTPSignature (req: Request, clockSkew?: number) {
56 56
57// JSONLD 57// JSONLD
58 58
59async function isJsonLDSignatureVerified (fromActor: ActorModel, signedDocument: any): Promise<boolean> { 59function isJsonLDSignatureVerified (fromActor: MActor, signedDocument: any): Promise<boolean> {
60 if (signedDocument.signature.type === 'RsaSignature2017') { 60 if (signedDocument.signature.type === 'RsaSignature2017') {
61 // Mastodon algorithm 61 return isJsonLDRSA2017Verified(fromActor, signedDocument)
62 const res = await isJsonLDRSA2017Verified(fromActor, signedDocument)
63 // Success? If no, try with our library
64 if (res === true) return true
65 } 62 }
66 63
67 const publicKeyObject = { 64 logger.warn('Unknown JSON LD signature %s.', signedDocument.signature.type, signedDocument)
68 '@context': jsig.SECURITY_CONTEXT_URL,
69 id: fromActor.url,
70 type: 'CryptographicKey',
71 owner: fromActor.url,
72 publicKeyPem: fromActor.publicKey
73 }
74
75 const publicKeyOwnerObject = {
76 '@context': jsig.SECURITY_CONTEXT_URL,
77 id: fromActor.url,
78 publicKey: [ publicKeyObject ]
79 }
80 65
81 const options = { 66 return Promise.resolve(false)
82 publicKey: publicKeyObject,
83 publicKeyOwner: publicKeyOwnerObject
84 }
85
86 return jsig.promises
87 .verify(signedDocument, options)
88 .then((result: { verified: boolean }) => result.verified)
89 .catch(err => {
90 logger.error('Cannot check signature.', { err })
91 return false
92 })
93} 67}
94 68
95// Backward compatibility with "other" implementations 69// Backward compatibility with "other" implementations
96async function isJsonLDRSA2017Verified (fromActor: ActorModel, signedDocument: any) { 70async function isJsonLDRSA2017Verified (fromActor: MActor, signedDocument: any) {
97 function hash (obj: any): Promise<any> {
98 return jsonld.promises
99 .normalize(obj, {
100 algorithm: 'URDNA2015',
101 format: 'application/n-quads'
102 })
103 .then(res => sha256(res))
104 }
105
106 const signatureCopy = cloneDeep(signedDocument.signature)
107 Object.assign(signatureCopy, {
108 '@context': [
109 'https://w3id.org/security/v1',
110 { RsaSignature2017: 'https://w3id.org/security#RsaSignature2017' }
111 ]
112 })
113 delete signatureCopy.type
114 delete signatureCopy.id
115 delete signatureCopy.signatureValue
116
117 const docWithoutSignature = cloneDeep(signedDocument)
118 delete docWithoutSignature.signature
119
120 const [ documentHash, optionsHash ] = await Promise.all([ 71 const [ documentHash, optionsHash ] = await Promise.all([
121 hash(docWithoutSignature), 72 createDocWithoutSignatureHash(signedDocument),
122 hash(signatureCopy) 73 createSignatureHash(signedDocument.signature)
123 ]) 74 ])
124 75
125 const toVerify = optionsHash + documentHash 76 const toVerify = optionsHash + documentHash
@@ -130,14 +81,27 @@ async function isJsonLDRSA2017Verified (fromActor: ActorModel, signedDocument: a
130 return verify.verify(fromActor.publicKey, signedDocument.signature.signatureValue, 'base64') 81 return verify.verify(fromActor.publicKey, signedDocument.signature.signatureValue, 'base64')
131} 82}
132 83
133function signJsonLDObject (byActor: ActorModel, data: any) { 84async function signJsonLDObject (byActor: MActor, data: any) {
134 const options = { 85 const signature = {
135 privateKeyPem: byActor.privateKey, 86 type: 'RsaSignature2017',
136 creator: byActor.url, 87 creator: byActor.url,
137 algorithm: 'RsaSignature2017' 88 created: new Date().toISOString()
138 } 89 }
139 90
140 return jsig.promises.sign(data, options) 91 const [ documentHash, optionsHash ] = await Promise.all([
92 createDocWithoutSignatureHash(data),
93 createSignatureHash(signature)
94 ])
95
96 const toSign = optionsHash + documentHash
97
98 const sign = createSign('RSA-SHA256')
99 sign.update(toSign, 'utf8')
100
101 const signatureValue = sign.sign(byActor.privateKey, 'base64')
102 Object.assign(signature, { signatureValue })
103
104 return Object.assign(data, { signature })
141} 105}
142 106
143// --------------------------------------------------------------------------- 107// ---------------------------------------------------------------------------
@@ -154,3 +118,35 @@ export {
154} 118}
155 119
156// --------------------------------------------------------------------------- 120// ---------------------------------------------------------------------------
121
122function hash (obj: any): Promise<any> {
123 return jsonld.promises
124 .normalize(obj, {
125 algorithm: 'URDNA2015',
126 format: 'application/n-quads'
127 })
128 .then(res => sha256(res))
129}
130
131function createSignatureHash (signature: any) {
132 const signatureCopy = cloneDeep(signature)
133 Object.assign(signatureCopy, {
134 '@context': [
135 'https://w3id.org/security/v1',
136 { RsaSignature2017: 'https://w3id.org/security#RsaSignature2017' }
137 ]
138 })
139
140 delete signatureCopy.type
141 delete signatureCopy.id
142 delete signatureCopy.signatureValue
143
144 return hash(signatureCopy)
145}
146
147function createDocWithoutSignatureHash (doc: any) {
148 const docWithoutSignature = cloneDeep(doc)
149 delete docWithoutSignature.signature
150
151 return hash(docWithoutSignature)
152}
diff --git a/server/helpers/utils.ts b/server/helpers/utils.ts
index 1464b1477..ba07eaaf3 100644
--- a/server/helpers/utils.ts
+++ b/server/helpers/utils.ts
@@ -19,7 +19,10 @@ async function generateRandomString (size: number) {
19 return raw.toString('hex') 19 return raw.toString('hex')
20} 20}
21 21
22interface FormattableToJSON<U, V> { toFormattedJSON (args?: U): V } 22interface FormattableToJSON<U, V> {
23 toFormattedJSON (args?: U): V
24}
25
23function getFormattedObjects<U, V, T extends FormattableToJSON<U, V>> (objects: T[], objectsTotal: number, formattedArg?: U) { 26function getFormattedObjects<U, V, T extends FormattableToJSON<U, V>> (objects: T[], objectsTotal: number, formattedArg?: U) {
24 const formattedObjects = objects.map(o => o.toFormattedJSON(formattedArg)) 27 const formattedObjects = objects.map(o => o.toFormattedJSON(formattedArg))
25 28
diff --git a/server/helpers/video.ts b/server/helpers/video.ts
index c90fe06c7..d066e2b1f 100644
--- a/server/helpers/video.ts
+++ b/server/helpers/video.ts
@@ -1,8 +1,30 @@
1import { VideoModel } from '../models/video/video' 1import { VideoModel } from '../models/video/video'
2import * as Bluebird from 'bluebird'
3import {
4 MVideoAccountLightBlacklistAllFiles,
5 MVideoFullLight,
6 MVideoIdThumbnail,
7 MVideoThumbnail,
8 MVideoWithRights
9} from '@server/typings/models'
10import { Response } from 'express'
2 11
3type VideoFetchType = 'all' | 'only-video' | 'only-video-with-rights' | 'id' | 'none' 12type VideoFetchType = 'all' | 'only-video' | 'only-video-with-rights' | 'id' | 'none'
4 13
5function fetchVideo (id: number | string, fetchType: VideoFetchType, userId?: number) { 14function fetchVideo (id: number | string, fetchType: 'all', userId?: number): Bluebird<MVideoFullLight>
15function 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>
17function fetchVideo (id: number | string, fetchType: 'id' | 'none', userId?: number): Bluebird<MVideoIdThumbnail>
18function fetchVideo (
19 id: number | string,
20 fetchType: VideoFetchType,
21 userId?: number
22): Bluebird<MVideoFullLight | MVideoThumbnail | MVideoWithRights | MVideoIdThumbnail>
23function fetchVideo (
24 id: number | string,
25 fetchType: VideoFetchType,
26 userId?: number
27): Bluebird<MVideoFullLight | MVideoThumbnail | MVideoWithRights | MVideoIdThumbnail> {
6 if (fetchType === 'all') return VideoModel.loadAndPopulateAccountAndServerAndTags(id, undefined, userId) 28 if (fetchType === 'all') return VideoModel.loadAndPopulateAccountAndServerAndTags(id, undefined, userId)
7 29
8 if (fetchType === 'only-video-with-rights') return VideoModel.loadWithRights(id) 30 if (fetchType === 'only-video-with-rights') return VideoModel.loadWithRights(id)
@@ -13,15 +35,29 @@ function fetchVideo (id: number | string, fetchType: VideoFetchType, userId?: nu
13} 35}
14 36
15type VideoFetchByUrlType = 'all' | 'only-video' 37type VideoFetchByUrlType = 'all' | 'only-video'
16function fetchVideoByUrl (url: string, fetchType: VideoFetchByUrlType) { 38
39function fetchVideoByUrl (url: string, fetchType: 'all'): Bluebird<MVideoAccountLightBlacklistAllFiles>
40function fetchVideoByUrl (url: string, fetchType: 'only-video'): Bluebird<MVideoThumbnail>
41function fetchVideoByUrl (url: string, fetchType: VideoFetchByUrlType): Bluebird<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail>
42function fetchVideoByUrl (url: string, fetchType: VideoFetchByUrlType): Bluebird<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail> {
17 if (fetchType === 'all') return VideoModel.loadByUrlAndPopulateAccount(url) 43 if (fetchType === 'all') return VideoModel.loadByUrlAndPopulateAccount(url)
18 44
19 if (fetchType === 'only-video') return VideoModel.loadByUrl(url) 45 if (fetchType === 'only-video') return VideoModel.loadByUrl(url)
20} 46}
21 47
48function getVideo (res: Response) {
49 return res.locals.videoAll || res.locals.onlyVideo || res.locals.onlyVideoWithRights || res.locals.videoId
50}
51
52function getVideoWithAttributes (res: Response) {
53 return res.locals.videoAll || res.locals.onlyVideo || res.locals.onlyVideoWithRights
54}
55
22export { 56export {
23 VideoFetchType, 57 VideoFetchType,
24 VideoFetchByUrlType, 58 VideoFetchByUrlType,
25 fetchVideo, 59 fetchVideo,
60 getVideo,
61 getVideoWithAttributes,
26 fetchVideoByUrl 62 fetchVideoByUrl
27} 63}
diff --git a/server/helpers/webfinger.ts b/server/helpers/webfinger.ts
index d1229e28f..5443a266b 100644
--- a/server/helpers/webfinger.ts
+++ b/server/helpers/webfinger.ts
@@ -4,6 +4,7 @@ import { ActorModel } from '../models/activitypub/actor'
4import { isTestInstance } from './core-utils' 4import { isTestInstance } from './core-utils'
5import { isActivityPubUrlValid } from './custom-validators/activitypub/misc' 5import { isActivityPubUrlValid } from './custom-validators/activitypub/misc'
6import { WEBSERVER } from '../initializers/constants' 6import { WEBSERVER } from '../initializers/constants'
7import { MActorFull } from '../typings/models'
7 8
8const webfinger = new WebFinger({ 9const webfinger = new WebFinger({
9 webfist_fallback: false, 10 webfist_fallback: false,
@@ -17,7 +18,7 @@ async function loadActorUrlOrGetFromWebfinger (uriArg: string) {
17 const uri = uriArg.startsWith('@') ? uriArg.slice(1) : uriArg 18 const uri = uriArg.startsWith('@') ? uriArg.slice(1) : uriArg
18 19
19 const [ name, host ] = uri.split('@') 20 const [ name, host ] = uri.split('@')
20 let actor: ActorModel 21 let actor: MActorFull
21 22
22 if (!host || host === WEBSERVER.HOST) { 23 if (!host || host === WEBSERVER.HOST) {
23 actor = await ActorModel.loadLocalByName(name) 24 actor = await ActorModel.loadLocalByName(name)
diff --git a/server/initializers/config.ts b/server/initializers/config.ts
index 510f7d64d..164d714d6 100644
--- a/server/initializers/config.ts
+++ b/server/initializers/config.ts
@@ -209,6 +209,19 @@ const CONFIG = {
209 get SHORT_DESCRIPTION () { return config.get<string>('instance.short_description') }, 209 get SHORT_DESCRIPTION () { return config.get<string>('instance.short_description') },
210 get DESCRIPTION () { return config.get<string>('instance.description') }, 210 get DESCRIPTION () { return config.get<string>('instance.description') },
211 get TERMS () { return config.get<string>('instance.terms') }, 211 get TERMS () { return config.get<string>('instance.terms') },
212 get CODE_OF_CONDUCT () { return config.get<string>('instance.code_of_conduct') },
213
214 get CREATION_REASON () { return config.get<string>('instance.creation_reason') },
215
216 get MODERATION_INFORMATION () { return config.get<string>('instance.moderation_information') },
217 get ADMINISTRATOR () { return config.get<string>('instance.administrator') },
218 get MAINTENANCE_LIFETIME () { return config.get<string>('instance.maintenance_lifetime') },
219 get BUSINESS_MODEL () { return config.get<string>('instance.business_model') },
220 get HARDWARE_INFORMATION () { return config.get<string>('instance.hardware_information') },
221
222 get LANGUAGES () { return config.get<string[]>('instance.languages') || [] },
223 get CATEGORIES () { return config.get<number[]>('instance.categories') || [] },
224
212 get IS_NSFW () { return config.get<boolean>('instance.is_nsfw') }, 225 get IS_NSFW () { return config.get<boolean>('instance.is_nsfw') },
213 get DEFAULT_CLIENT_ROUTE () { return config.get<string>('instance.default_client_route') }, 226 get DEFAULT_CLIENT_ROUTE () { return config.get<string>('instance.default_client_route') },
214 get DEFAULT_NSFW_POLICY () { return config.get<NSFWPolicyType>('instance.default_nsfw_policy') }, 227 get DEFAULT_NSFW_POLICY () { return config.get<NSFWPolicyType>('instance.default_nsfw_policy') },
@@ -232,6 +245,23 @@ const CONFIG = {
232 get MANUAL_APPROVAL () { return config.get<boolean>('followers.instance.manual_approval') } 245 get MANUAL_APPROVAL () { return config.get<boolean>('followers.instance.manual_approval') }
233 } 246 }
234 }, 247 },
248 FOLLOWINGS: {
249 INSTANCE: {
250 AUTO_FOLLOW_BACK: {
251 get ENABLED () {
252 return config.get<boolean>('followings.instance.auto_follow_back.enabled')
253 }
254 },
255 AUTO_FOLLOW_INDEX: {
256 get ENABLED () {
257 return config.get<boolean>('followings.instance.auto_follow_index.enabled')
258 },
259 get INDEX_URL () {
260 return config.get<string>('followings.instance.auto_follow_index.index_url')
261 }
262 }
263 }
264 },
235 THEME: { 265 THEME: {
236 get DEFAULT () { return config.get<string>('theme.default') } 266 get DEFAULT () { return config.get<string>('theme.default') }
237 } 267 }
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 3dc178b11..be4a66488 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -14,7 +14,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
14 14
15// --------------------------------------------------------------------------- 15// ---------------------------------------------------------------------------
16 16
17const LAST_MIGRATION_VERSION = 420 17const LAST_MIGRATION_VERSION = 435
18 18
19// --------------------------------------------------------------------------- 19// ---------------------------------------------------------------------------
20 20
@@ -168,10 +168,15 @@ const SCHEDULER_INTERVALS_MS = {
168 updateVideos: 60000, // 1 minute 168 updateVideos: 60000, // 1 minute
169 youtubeDLUpdate: 60000 * 60 * 24, // 1 day 169 youtubeDLUpdate: 60000 * 60 * 24, // 1 day
170 checkPlugins: CONFIG.PLUGINS.INDEX.CHECK_LATEST_VERSIONS_INTERVAL, 170 checkPlugins: CONFIG.PLUGINS.INDEX.CHECK_LATEST_VERSIONS_INTERVAL,
171 autoFollowIndexInstances: 60000 * 60 * 24, // 1 day
171 removeOldViews: 60000 * 60 * 24, // 1 day 172 removeOldViews: 60000 * 60 * 24, // 1 day
172 removeOldHistory: 60000 * 60 * 24 // 1 day 173 removeOldHistory: 60000 * 60 * 24 // 1 day
173} 174}
174 175
176const INSTANCES_INDEX = {
177 HOSTS_PATH: '/api/v1/instances/hosts'
178}
179
175// --------------------------------------------------------------------------- 180// ---------------------------------------------------------------------------
176 181
177const CONSTRAINTS_FIELDS = { 182const CONSTRAINTS_FIELDS = {
@@ -633,6 +638,7 @@ if (isTestInstance() === true) {
633 SCHEDULER_INTERVALS_MS.removeOldHistory = 5000 638 SCHEDULER_INTERVALS_MS.removeOldHistory = 5000
634 SCHEDULER_INTERVALS_MS.removeOldViews = 5000 639 SCHEDULER_INTERVALS_MS.removeOldViews = 5000
635 SCHEDULER_INTERVALS_MS.updateVideos = 5000 640 SCHEDULER_INTERVALS_MS.updateVideos = 5000
641 SCHEDULER_INTERVALS_MS.autoFollowIndexInstances = 5000
636 REPEAT_JOBS[ 'videos-views' ] = { every: 5000 } 642 REPEAT_JOBS[ 'videos-views' ] = { every: 5000 }
637 643
638 REDUNDANCY.VIDEOS.RANDOMIZED_FACTOR = 1 644 REDUNDANCY.VIDEOS.RANDOMIZED_FACTOR = 1
@@ -683,6 +689,7 @@ export {
683 PREVIEWS_SIZE, 689 PREVIEWS_SIZE,
684 REMOTE_SCHEME, 690 REMOTE_SCHEME,
685 FOLLOW_STATES, 691 FOLLOW_STATES,
692 INSTANCES_INDEX,
686 DEFAULT_USER_THEME_NAME, 693 DEFAULT_USER_THEME_NAME,
687 SERVER_ACTOR_NAME, 694 SERVER_ACTOR_NAME,
688 PLUGIN_GLOBAL_CSS_FILE_NAME, 695 PLUGIN_GLOBAL_CSS_FILE_NAME,
diff --git a/server/initializers/migrations/0425-nullable-actor-fields.ts b/server/initializers/migrations/0425-nullable-actor-fields.ts
new file mode 100644
index 000000000..4e5f9e6ab
--- /dev/null
+++ b/server/initializers/migrations/0425-nullable-actor-fields.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 db: any
8}): Promise<void> {
9 const data = {
10 type: Sequelize.STRING,
11 allowNull: true
12 }
13
14 await utils.queryInterface.changeColumn('actor', 'outboxUrl', data)
15 await utils.queryInterface.changeColumn('actor', 'followersUrl', data)
16 await utils.queryInterface.changeColumn('actor', 'followingUrl', data)
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/0430-auto-follow-notification-setting.ts b/server/initializers/migrations/0430-auto-follow-notification-setting.ts
new file mode 100644
index 000000000..034bdd46d
--- /dev/null
+++ b/server/initializers/migrations/0430-auto-follow-notification-setting.ts
@@ -0,0 +1,40 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction,
5 queryInterface: Sequelize.QueryInterface,
6 sequelize: Sequelize.Sequelize,
7 db: any
8}): Promise<void> {
9 {
10 const data = {
11 type: Sequelize.INTEGER,
12 defaultValue: null,
13 allowNull: true
14 }
15 await utils.queryInterface.addColumn('userNotificationSetting', 'autoInstanceFollowing', data)
16 }
17
18 {
19 const query = 'UPDATE "userNotificationSetting" SET "autoInstanceFollowing" = 1'
20 await utils.sequelize.query(query)
21 }
22
23 {
24 const data = {
25 type: Sequelize.INTEGER,
26 defaultValue: null,
27 allowNull: false
28 }
29 await utils.queryInterface.changeColumn('userNotificationSetting', 'autoInstanceFollowing', data)
30 }
31}
32
33function down (options) {
34 throw new Error('Not implemented.')
35}
36
37export {
38 up,
39 down
40}
diff --git a/server/initializers/migrations/0435-user-modals.ts b/server/initializers/migrations/0435-user-modals.ts
new file mode 100644
index 000000000..5c2aa85b5
--- /dev/null
+++ b/server/initializers/migrations/0435-user-modals.ts
@@ -0,0 +1,40 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction,
5 queryInterface: Sequelize.QueryInterface,
6 sequelize: Sequelize.Sequelize,
7 db: any
8}): Promise<void> {
9 {
10 const data = {
11 type: Sequelize.BOOLEAN,
12 allowNull: false,
13 defaultValue: false
14 }
15
16 await utils.queryInterface.addColumn('user', 'noInstanceConfigWarningModal', data)
17 }
18
19 {
20 const data = {
21 type: Sequelize.BOOLEAN,
22 allowNull: false,
23 defaultValue: true
24 }
25
26 await utils.queryInterface.addColumn('user', 'noWelcomeModal', data)
27 data.defaultValue = false
28
29 await utils.queryInterface.changeColumn('user', 'noWelcomeModal', data)
30 }
31}
32
33function down (options) {
34 throw new Error('Not implemented.')
35}
36
37export {
38 up,
39 down
40}
diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts
index 9f5d12eb4..13b73077e 100644
--- a/server/lib/activitypub/actor.ts
+++ b/server/lib/activitypub/actor.ts
@@ -22,13 +22,27 @@ import { JobQueue } from '../job-queue'
22import { getServerActor } from '../../helpers/utils' 22import { getServerActor } from '../../helpers/utils'
23import { ActorFetchByUrlType, fetchActorByUrl } from '../../helpers/actor' 23import { ActorFetchByUrlType, fetchActorByUrl } from '../../helpers/actor'
24import { sequelizeTypescript } from '../../initializers/database' 24import { sequelizeTypescript } from '../../initializers/database'
25import {
26 MAccount,
27 MAccountDefault,
28 MActor,
29 MActorAccountChannelId,
30 MActorAccountChannelIdActor,
31 MActorAccountId,
32 MActorDefault,
33 MActorFull,
34 MActorFullActor,
35 MActorId,
36 MChannel,
37 MChannelAccountDefault
38} from '../../typings/models'
25 39
26// 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
27function setAsyncActorKeys (actor: ActorModel) { 41function setAsyncActorKeys <T extends MActor> (actor: T) {
28 return createPrivateAndPublicKeys() 42 return createPrivateAndPublicKeys()
29 .then(({ publicKey, privateKey }) => { 43 .then(({ publicKey, privateKey }) => {
30 actor.set('publicKey', publicKey) 44 actor.publicKey = publicKey
31 actor.set('privateKey', privateKey) 45 actor.privateKey = privateKey
32 return actor.save() 46 return actor.save()
33 }) 47 })
34 .catch(err => { 48 .catch(err => {
@@ -37,12 +51,26 @@ function setAsyncActorKeys (actor: ActorModel) {
37 }) 51 })
38} 52}
39 53
54function getOrCreateActorAndServerAndModel (
55 activityActor: string | ActivityPubActor,
56 fetchType: 'all',
57 recurseIfNeeded?: boolean,
58 updateCollections?: boolean
59): Promise<MActorFullActor>
60
61function getOrCreateActorAndServerAndModel (
62 activityActor: string | ActivityPubActor,
63 fetchType?: 'association-ids',
64 recurseIfNeeded?: boolean,
65 updateCollections?: boolean
66): Promise<MActorAccountChannelId>
67
40async function getOrCreateActorAndServerAndModel ( 68async function getOrCreateActorAndServerAndModel (
41 activityActor: string | ActivityPubActor, 69 activityActor: string | ActivityPubActor,
42 fetchType: ActorFetchByUrlType = 'actor-and-association-ids', 70 fetchType: ActorFetchByUrlType = 'association-ids',
43 recurseIfNeeded = true, 71 recurseIfNeeded = true,
44 updateCollections = false 72 updateCollections = false
45) { 73): Promise<MActorFullActor | MActorAccountChannelId> {
46 const actorUrl = getAPId(activityActor) 74 const actorUrl = getAPId(activityActor)
47 let created = false 75 let created = false
48 let accountPlaylistsUrl: string 76 let accountPlaylistsUrl: string
@@ -61,7 +89,7 @@ async function getOrCreateActorAndServerAndModel (
61 89
62 // Create the attributed to actor 90 // Create the attributed to actor
63 // In PeerTube a video channel is owned by an account 91 // In PeerTube a video channel is owned by an account
64 let ownerActor: ActorModel = undefined 92 let ownerActor: MActorFullActor
65 if (recurseIfNeeded === true && result.actor.type === 'Group') { 93 if (recurseIfNeeded === true && result.actor.type === 'Group') {
66 const accountAttributedTo = result.attributedTo.find(a => a.type === 'Person') 94 const accountAttributedTo = result.attributedTo.find(a => a.type === 'Person')
67 if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actor.url) 95 if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actor.url)
@@ -85,8 +113,8 @@ async function getOrCreateActorAndServerAndModel (
85 accountPlaylistsUrl = result.playlists 113 accountPlaylistsUrl = result.playlists
86 } 114 }
87 115
88 if (actor.Account) actor.Account.Actor = actor 116 if (actor.Account) (actor as MActorAccountChannelIdActor).Account.Actor = actor
89 if (actor.VideoChannel) actor.VideoChannel.Actor = actor 117 if (actor.VideoChannel) (actor as MActorAccountChannelIdActor).VideoChannel.Actor = actor
90 118
91 const { actor: actorRefreshed, refreshed } = await retryTransactionWrapper(refreshActorIfNeeded, actor, fetchType) 119 const { actor: actorRefreshed, refreshed } = await retryTransactionWrapper(refreshActorIfNeeded, actor, fetchType)
92 if (!actorRefreshed) throw new Error('Actor ' + actorRefreshed.url + ' does not exist anymore.') 120 if (!actorRefreshed) throw new Error('Actor ' + actorRefreshed.url + ' does not exist anymore.')
@@ -120,7 +148,7 @@ function buildActorInstance (type: ActivityPubActorType, url: string, preferredU
120 sharedInboxUrl: WEBSERVER.URL + '/inbox', 148 sharedInboxUrl: WEBSERVER.URL + '/inbox',
121 followersUrl: url + '/followers', 149 followersUrl: url + '/followers',
122 followingUrl: url + '/following' 150 followingUrl: url + '/following'
123 }) 151 }) as MActor
124} 152}
125 153
126async function updateActorInstance (actorInstance: ActorModel, attributes: ActivityPubActor) { 154async function updateActorInstance (actorInstance: ActorModel, attributes: ActivityPubActor) {
@@ -140,7 +168,8 @@ async function updateActorInstance (actorInstance: ActorModel, attributes: Activ
140 actorInstance.followingUrl = attributes.following 168 actorInstance.followingUrl = attributes.following
141} 169}
142 170
143async function updateActorAvatarInstance (actor: ActorModel, info: { name: string, onDisk: boolean, fileUrl: string }, t: Transaction) { 171type AvatarInfo = { name: string, onDisk: boolean, fileUrl: string }
172async function updateActorAvatarInstance (actor: MActorDefault, info: AvatarInfo, t: Transaction) {
144 if (info.name !== undefined) { 173 if (info.name !== undefined) {
145 if (actor.avatarId) { 174 if (actor.avatarId) {
146 try { 175 try {
@@ -212,14 +241,16 @@ async function addFetchOutboxJob (actor: Pick<ActorModel, 'id' | 'outboxUrl'>) {
212 return JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload }) 241 return JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
213} 242}
214 243
215async function refreshActorIfNeeded ( 244async function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannelId> (
216 actorArg: ActorModel, 245 actorArg: T,
217 fetchedType: ActorFetchByUrlType 246 fetchedType: ActorFetchByUrlType
218): Promise<{ actor: ActorModel, refreshed: boolean }> { 247): Promise<{ actor: T | MActorFull, refreshed: boolean }> {
219 if (!actorArg.isOutdated()) return { actor: actorArg, refreshed: false } 248 if (!actorArg.isOutdated()) return { actor: actorArg, refreshed: false }
220 249
221 // We need more attributes 250 // We need more attributes
222 const actor = fetchedType === 'all' ? actorArg : await ActorModel.loadByUrlAndPopulateAccountAndChannel(actorArg.url) 251 const actor = fetchedType === 'all'
252 ? actorArg as MActorFull
253 : await ActorModel.loadByUrlAndPopulateAccountAndChannel(actorArg.url)
223 254
224 try { 255 try {
225 let actorUrl: string 256 let actorUrl: string
@@ -297,9 +328,9 @@ export {
297 328
298function saveActorAndServerAndModelIfNotExist ( 329function saveActorAndServerAndModelIfNotExist (
299 result: FetchRemoteActorResult, 330 result: FetchRemoteActorResult,
300 ownerActor?: ActorModel, 331 ownerActor?: MActorFullActor,
301 t?: Transaction 332 t?: Transaction
302): Bluebird<ActorModel> | Promise<ActorModel> { 333): Bluebird<MActorFullActor> | Promise<MActorFullActor> {
303 let actor = result.actor 334 let actor = result.actor
304 335
305 if (t !== undefined) return save(t) 336 if (t !== undefined) return save(t)
@@ -336,7 +367,7 @@ function saveActorAndServerAndModelIfNotExist (
336 367
337 // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists 368 // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists
338 // (which could be false in a retried query) 369 // (which could be false in a retried query)
339 const [ actorCreated ] = await ActorModel.findOrCreate({ 370 const [ actorCreated ] = await ActorModel.findOrCreate<MActorFullActor>({
340 defaults: actor.toJSON(), 371 defaults: actor.toJSON(),
341 where: { 372 where: {
342 url: actor.url 373 url: actor.url
@@ -345,12 +376,11 @@ function saveActorAndServerAndModelIfNotExist (
345 }) 376 })
346 377
347 if (actorCreated.type === 'Person' || actorCreated.type === 'Application') { 378 if (actorCreated.type === 'Person' || actorCreated.type === 'Application') {
348 actorCreated.Account = await saveAccount(actorCreated, result, t) 379 actorCreated.Account = await saveAccount(actorCreated, result, t) as MAccountDefault
349 actorCreated.Account.Actor = actorCreated 380 actorCreated.Account.Actor = actorCreated
350 } else if (actorCreated.type === 'Group') { // Video channel 381 } else if (actorCreated.type === 'Group') { // Video channel
351 actorCreated.VideoChannel = await saveVideoChannel(actorCreated, result, ownerActor, t) 382 const channel = await saveVideoChannel(actorCreated, result, ownerActor, t)
352 actorCreated.VideoChannel.Actor = actorCreated 383 actorCreated.VideoChannel = Object.assign(channel, { Actor: actorCreated, Account: ownerActor.Account })
353 actorCreated.VideoChannel.Account = ownerActor.Account
354 } 384 }
355 385
356 actorCreated.Server = server 386 actorCreated.Server = server
@@ -360,7 +390,7 @@ function saveActorAndServerAndModelIfNotExist (
360} 390}
361 391
362type FetchRemoteActorResult = { 392type FetchRemoteActorResult = {
363 actor: ActorModel 393 actor: MActor
364 name: string 394 name: string
365 summary: string 395 summary: string
366 support?: string 396 support?: string
@@ -429,7 +459,7 @@ async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: numbe
429 } 459 }
430} 460}
431 461
432async function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) { 462async function saveAccount (actor: MActorId, result: FetchRemoteActorResult, t: Transaction) {
433 const [ accountCreated ] = await AccountModel.findOrCreate({ 463 const [ accountCreated ] = await AccountModel.findOrCreate({
434 defaults: { 464 defaults: {
435 name: result.name, 465 name: result.name,
@@ -442,10 +472,10 @@ async function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t
442 transaction: t 472 transaction: t
443 }) 473 })
444 474
445 return accountCreated 475 return accountCreated as MAccount
446} 476}
447 477
448async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResult, ownerActor: ActorModel, t: Transaction) { 478async function saveVideoChannel (actor: MActorId, result: FetchRemoteActorResult, ownerActor: MActorAccountId, t: Transaction) {
449 const [ videoChannelCreated ] = await VideoChannelModel.findOrCreate({ 479 const [ videoChannelCreated ] = await VideoChannelModel.findOrCreate({
450 defaults: { 480 defaults: {
451 name: result.name, 481 name: result.name,
@@ -460,5 +490,5 @@ async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResu
460 transaction: t 490 transaction: t
461 }) 491 })
462 492
463 return videoChannelCreated 493 return videoChannelCreated as MChannel
464} 494}
diff --git a/server/lib/activitypub/audience.ts b/server/lib/activitypub/audience.ts
index 0e3d78590..f2ab54cf7 100644
--- a/server/lib/activitypub/audience.ts
+++ b/server/lib/activitypub/audience.ts
@@ -3,11 +3,10 @@ import { ActivityAudience } from '../../../shared/models/activitypub'
3import { ACTIVITY_PUB } from '../../initializers/constants' 3import { ACTIVITY_PUB } from '../../initializers/constants'
4import { ActorModel } from '../../models/activitypub/actor' 4import { ActorModel } from '../../models/activitypub/actor'
5import { VideoModel } from '../../models/video/video' 5import { VideoModel } from '../../models/video/video'
6import { VideoCommentModel } from '../../models/video/video-comment'
7import { VideoShareModel } from '../../models/video/video-share' 6import { VideoShareModel } from '../../models/video/video-share'
8import { ActorModelOnly } from '../../typings/models' 7import { MActorFollowersUrl, MActorLight, MCommentOwner, MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../../typings/models'
9 8
10function getRemoteVideoAudience (video: VideoModel, actorsInvolvedInVideo: ActorModel[]): ActivityAudience { 9function getRemoteVideoAudience (video: MVideoAccountLight, actorsInvolvedInVideo: MActorFollowersUrl[]): ActivityAudience {
11 return { 10 return {
12 to: [ video.VideoChannel.Account.Actor.url ], 11 to: [ video.VideoChannel.Account.Actor.url ],
13 cc: actorsInvolvedInVideo.map(a => a.followersUrl) 12 cc: actorsInvolvedInVideo.map(a => a.followersUrl)
@@ -15,9 +14,9 @@ function getRemoteVideoAudience (video: VideoModel, actorsInvolvedInVideo: Actor
15} 14}
16 15
17function getVideoCommentAudience ( 16function getVideoCommentAudience (
18 videoComment: VideoCommentModel, 17 videoComment: MCommentOwnerVideo,
19 threadParentComments: VideoCommentModel[], 18 threadParentComments: MCommentOwner[],
20 actorsInvolvedInVideo: ActorModel[], 19 actorsInvolvedInVideo: MActorFollowersUrl[],
21 isOrigin = false 20 isOrigin = false
22): ActivityAudience { 21): ActivityAudience {
23 const to = [ ACTIVITY_PUB.PUBLIC ] 22 const to = [ ACTIVITY_PUB.PUBLIC ]
@@ -42,26 +41,28 @@ function getVideoCommentAudience (
42 } 41 }
43} 42}
44 43
45function getAudienceFromFollowersOf (actorsInvolvedInObject: ActorModel[]): ActivityAudience { 44function getAudienceFromFollowersOf (actorsInvolvedInObject: MActorFollowersUrl[]): ActivityAudience {
46 return { 45 return {
47 to: [ ACTIVITY_PUB.PUBLIC ].concat(actorsInvolvedInObject.map(a => a.followersUrl)), 46 to: [ ACTIVITY_PUB.PUBLIC ].concat(actorsInvolvedInObject.map(a => a.followersUrl)),
48 cc: [] 47 cc: []
49 } 48 }
50} 49}
51 50
52async function getActorsInvolvedInVideo (video: VideoModel, t: Transaction) { 51async function getActorsInvolvedInVideo (video: MVideo, t: Transaction) {
53 const actors = await VideoShareModel.loadActorsByShare(video.id, t) 52 const actors: MActorLight[] = await VideoShareModel.loadActorsByShare(video.id, t)
54 53
55 const videoActor = video.VideoChannel && video.VideoChannel.Account 54 const videoAll = video as VideoModel
56 ? video.VideoChannel.Account.Actor 55
57 : await ActorModel.loadAccountActorByVideoId(video.id, t) 56 const videoActor = videoAll.VideoChannel && videoAll.VideoChannel.Account
57 ? videoAll.VideoChannel.Account.Actor
58 : await ActorModel.loadFromAccountByVideoId(video.id, t)
58 59
59 actors.push(videoActor) 60 actors.push(videoActor)
60 61
61 return actors 62 return actors
62} 63}
63 64
64function getAudience (actorSender: ActorModelOnly, isPublic = true) { 65function getAudience (actorSender: MActorFollowersUrl, isPublic = true) {
65 return buildAudience([ actorSender.followersUrl ], isPublic) 66 return buildAudience([ actorSender.followersUrl ], isPublic)
66} 67}
67 68
diff --git a/server/lib/activitypub/cache-file.ts b/server/lib/activitypub/cache-file.ts
index de5cc54ac..65b2dcb49 100644
--- a/server/lib/activitypub/cache-file.ts
+++ b/server/lib/activitypub/cache-file.ts
@@ -1,10 +1,10 @@
1import { CacheFileObject } from '../../../shared/index' 1import { CacheFileObject } from '../../../shared/index'
2import { VideoModel } from '../../models/video/video'
3import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' 2import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy'
4import { Transaction } from 'sequelize' 3import { Transaction } from 'sequelize'
5import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' 4import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
5import { MActorId, MVideoRedundancy, MVideoWithAllFiles } from '@server/typings/models'
6 6
7function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject, video: VideoModel, byActor: { id?: number }) { 7function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject, video: MVideoWithAllFiles, byActor: MActorId) {
8 8
9 if (cacheFileObject.url.mediaType === 'application/x-mpegURL') { 9 if (cacheFileObject.url.mediaType === 'application/x-mpegURL') {
10 const url = cacheFileObject.url 10 const url = cacheFileObject.url
@@ -39,7 +39,7 @@ function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject
39 } 39 }
40} 40}
41 41
42async function createOrUpdateCacheFile (cacheFileObject: CacheFileObject, video: VideoModel, byActor: { id?: number }, t: Transaction) { 42async function createOrUpdateCacheFile (cacheFileObject: CacheFileObject, video: MVideoWithAllFiles, byActor: MActorId, t: Transaction) {
43 const redundancyModel = await VideoRedundancyModel.loadByUrl(cacheFileObject.id, t) 43 const redundancyModel = await VideoRedundancyModel.loadByUrl(cacheFileObject.id, t)
44 44
45 if (!redundancyModel) { 45 if (!redundancyModel) {
@@ -49,7 +49,7 @@ async function createOrUpdateCacheFile (cacheFileObject: CacheFileObject, video:
49 } 49 }
50} 50}
51 51
52function createCacheFile (cacheFileObject: CacheFileObject, video: VideoModel, byActor: { id?: number }, t: Transaction) { 52function createCacheFile (cacheFileObject: CacheFileObject, video: MVideoWithAllFiles, byActor: MActorId, t: Transaction) {
53 const attributes = cacheFileActivityObjectToDBAttributes(cacheFileObject, video, byActor) 53 const attributes = cacheFileActivityObjectToDBAttributes(cacheFileObject, video, byActor)
54 54
55 return VideoRedundancyModel.create(attributes, { transaction: t }) 55 return VideoRedundancyModel.create(attributes, { transaction: t })
@@ -57,9 +57,9 @@ function createCacheFile (cacheFileObject: CacheFileObject, video: VideoModel, b
57 57
58function updateCacheFile ( 58function updateCacheFile (
59 cacheFileObject: CacheFileObject, 59 cacheFileObject: CacheFileObject,
60 redundancyModel: VideoRedundancyModel, 60 redundancyModel: MVideoRedundancy,
61 video: VideoModel, 61 video: MVideoWithAllFiles,
62 byActor: { id?: number }, 62 byActor: MActorId,
63 t: Transaction 63 t: Transaction
64) { 64) {
65 if (redundancyModel.actorId !== byActor.id) { 65 if (redundancyModel.actorId !== byActor.id) {
diff --git a/server/lib/activitypub/follow.ts b/server/lib/activitypub/follow.ts
new file mode 100644
index 000000000..1abf43cd4
--- /dev/null
+++ b/server/lib/activitypub/follow.ts
@@ -0,0 +1,36 @@
1import { MActorFollowActors } from '../../typings/models'
2import { CONFIG } from '../../initializers/config'
3import { SERVER_ACTOR_NAME } from '../../initializers/constants'
4import { JobQueue } from '../job-queue'
5import { logger } from '../../helpers/logger'
6import { getServerActor } from '../../helpers/utils'
7import { ServerModel } from '../../models/server/server'
8
9async function autoFollowBackIfNeeded (actorFollow: MActorFollowActors) {
10 if (!CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_BACK.ENABLED) return
11
12 const follower = actorFollow.ActorFollower
13
14 if (follower.type === 'Application' && follower.preferredUsername === SERVER_ACTOR_NAME) {
15 logger.info('Auto follow back %s.', follower.url)
16
17 const me = await getServerActor()
18
19 const server = await ServerModel.load(follower.serverId)
20 const host = server.host
21
22 const payload = {
23 host,
24 name: SERVER_ACTOR_NAME,
25 followerActorId: me.id,
26 isAutoFollow: true
27 }
28
29 JobQueue.Instance.createJob({ type: 'activitypub-follow', payload })
30 .catch(err => logger.error('Cannot create auto follow back job for %s.', host, err))
31 }
32}
33
34export {
35 autoFollowBackIfNeeded
36}
diff --git a/server/lib/activitypub/playlist.ts b/server/lib/activitypub/playlist.ts
index c2e2a3283..c52b715ef 100644
--- a/server/lib/activitypub/playlist.ts
+++ b/server/lib/activitypub/playlist.ts
@@ -1,7 +1,6 @@
1import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' 1import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object'
2import { crawlCollectionPage } from './crawl' 2import { crawlCollectionPage } from './crawl'
3import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' 3import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
4import { AccountModel } from '../../models/account/account'
5import { isArray } from '../../helpers/custom-validators/misc' 4import { isArray } from '../../helpers/custom-validators/misc'
6import { getOrCreateActorAndServerAndModel } from './actor' 5import { getOrCreateActorAndServerAndModel } from './actor'
7import { logger } from '../../helpers/logger' 6import { logger } from '../../helpers/logger'
@@ -13,14 +12,14 @@ import { PlaylistElementObject } from '../../../shared/models/activitypub/object
13import { getOrCreateVideoAndAccountAndChannel } from './videos' 12import { getOrCreateVideoAndAccountAndChannel } from './videos'
14import { isPlaylistElementObjectValid, isPlaylistObjectValid } from '../../helpers/custom-validators/activitypub/playlist' 13import { isPlaylistElementObjectValid, isPlaylistObjectValid } from '../../helpers/custom-validators/activitypub/playlist'
15import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element' 14import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element'
16import { VideoModel } from '../../models/video/video'
17import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' 15import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
18import { sequelizeTypescript } from '../../initializers/database' 16import { sequelizeTypescript } from '../../initializers/database'
19import { createPlaylistMiniatureFromUrl } from '../thumbnail' 17import { createPlaylistMiniatureFromUrl } from '../thumbnail'
20import { FilteredModelAttributes } from '../../typings/sequelize' 18import { FilteredModelAttributes } from '../../typings/sequelize'
21import { AccountModelId } from '../../typings/models' 19import { MAccountDefault, MAccountId, MVideoId } from '../../typings/models'
20import { MVideoPlaylist, MVideoPlaylistId, MVideoPlaylistOwner } from '../../typings/models/video/video-playlist'
22 21
23function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: AccountModelId, to: string[]) { 22function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: MAccountId, to: string[]) {
24 const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPlaylistPrivacy.PUBLIC : VideoPlaylistPrivacy.UNLISTED 23 const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPlaylistPrivacy.PUBLIC : VideoPlaylistPrivacy.UNLISTED
25 24
26 return { 25 return {
@@ -36,7 +35,7 @@ function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount
36 } 35 }
37} 36}
38 37
39function playlistElementObjectToDBAttributes (elementObject: PlaylistElementObject, videoPlaylist: VideoPlaylistModel, video: VideoModel) { 38function playlistElementObjectToDBAttributes (elementObject: PlaylistElementObject, videoPlaylist: MVideoPlaylistId, video: MVideoId) {
40 return { 39 return {
41 position: elementObject.position, 40 position: elementObject.position,
42 url: elementObject.id, 41 url: elementObject.id,
@@ -47,7 +46,7 @@ function playlistElementObjectToDBAttributes (elementObject: PlaylistElementObje
47 } 46 }
48} 47}
49 48
50async function createAccountPlaylists (playlistUrls: string[], account: AccountModel) { 49async function createAccountPlaylists (playlistUrls: string[], account: MAccountDefault) {
51 await Bluebird.map(playlistUrls, async playlistUrl => { 50 await Bluebird.map(playlistUrls, async playlistUrl => {
52 try { 51 try {
53 const exists = await VideoPlaylistModel.doesPlaylistExist(playlistUrl) 52 const exists = await VideoPlaylistModel.doesPlaylistExist(playlistUrl)
@@ -75,7 +74,7 @@ async function createAccountPlaylists (playlistUrls: string[], account: AccountM
75 }, { concurrency: CRAWL_REQUEST_CONCURRENCY }) 74 }, { concurrency: CRAWL_REQUEST_CONCURRENCY })
76} 75}
77 76
78async function createOrUpdateVideoPlaylist (playlistObject: PlaylistObject, byAccount: AccountModelId, to: string[]) { 77async function createOrUpdateVideoPlaylist (playlistObject: PlaylistObject, byAccount: MAccountId, to: string[]) {
79 const playlistAttributes = playlistObjectToDBAttributes(playlistObject, byAccount, to) 78 const playlistAttributes = playlistObjectToDBAttributes(playlistObject, byAccount, to)
80 79
81 if (isArray(playlistObject.attributedTo) && playlistObject.attributedTo.length === 1) { 80 if (isArray(playlistObject.attributedTo) && playlistObject.attributedTo.length === 1) {
@@ -88,7 +87,7 @@ async function createOrUpdateVideoPlaylist (playlistObject: PlaylistObject, byAc
88 } 87 }
89 } 88 }
90 89
91 const [ playlist ] = await VideoPlaylistModel.upsert<VideoPlaylistModel>(playlistAttributes, { returning: true }) 90 const [ playlist ] = await VideoPlaylistModel.upsert<MVideoPlaylist>(playlistAttributes, { returning: true })
92 91
93 let accItems: string[] = [] 92 let accItems: string[] = []
94 await crawlCollectionPage<string>(playlistObject.id, items => { 93 await crawlCollectionPage<string>(playlistObject.id, items => {
@@ -114,7 +113,7 @@ async function createOrUpdateVideoPlaylist (playlistObject: PlaylistObject, byAc
114 return resetVideoPlaylistElements(accItems, refreshedPlaylist) 113 return resetVideoPlaylistElements(accItems, refreshedPlaylist)
115} 114}
116 115
117async function refreshVideoPlaylistIfNeeded (videoPlaylist: VideoPlaylistModel): Promise<VideoPlaylistModel> { 116async function refreshVideoPlaylistIfNeeded (videoPlaylist: MVideoPlaylistOwner): Promise<MVideoPlaylistOwner> {
118 if (!videoPlaylist.isOutdated()) return videoPlaylist 117 if (!videoPlaylist.isOutdated()) return videoPlaylist
119 118
120 try { 119 try {
@@ -157,7 +156,7 @@ export {
157 156
158// --------------------------------------------------------------------------- 157// ---------------------------------------------------------------------------
159 158
160async function resetVideoPlaylistElements (elementUrls: string[], playlist: VideoPlaylistModel) { 159async function resetVideoPlaylistElements (elementUrls: string[], playlist: MVideoPlaylist) {
161 const elementsToCreate: FilteredModelAttributes<VideoPlaylistElementModel>[] = [] 160 const elementsToCreate: FilteredModelAttributes<VideoPlaylistElementModel>[] = []
162 161
163 await Bluebird.map(elementUrls, async elementUrl => { 162 await Bluebird.map(elementUrls, async elementUrl => {
diff --git a/server/lib/activitypub/process/process-accept.ts b/server/lib/activitypub/process/process-accept.ts
index cf27e6c32..dcfbb2c84 100644
--- a/server/lib/activitypub/process/process-accept.ts
+++ b/server/lib/activitypub/process/process-accept.ts
@@ -1,9 +1,8 @@
1import { ActivityAccept } from '../../../../shared/models/activitypub' 1import { ActivityAccept } from '../../../../shared/models/activitypub'
2import { ActorModel } from '../../../models/activitypub/actor'
3import { ActorFollowModel } from '../../../models/activitypub/actor-follow' 2import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
4import { addFetchOutboxJob } from '../actor' 3import { addFetchOutboxJob } from '../actor'
5import { APProcessorOptions } from '../../../typings/activitypub-processor.model' 4import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
6import { SignatureActorModel } from '../../../typings/models' 5import { MActorDefault, MActorSignature } from '../../../typings/models'
7 6
8async function processAcceptActivity (options: APProcessorOptions<ActivityAccept>) { 7async function processAcceptActivity (options: APProcessorOptions<ActivityAccept>) {
9 const { byActor: targetActor, inboxActor } = options 8 const { byActor: targetActor, inboxActor } = options
@@ -20,12 +19,12 @@ export {
20 19
21// --------------------------------------------------------------------------- 20// ---------------------------------------------------------------------------
22 21
23async function processAccept (actor: ActorModel, targetActor: SignatureActorModel) { 22async function processAccept (actor: MActorDefault, targetActor: MActorSignature) {
24 const follow = await ActorFollowModel.loadByActorAndTarget(actor.id, targetActor.id) 23 const follow = await ActorFollowModel.loadByActorAndTarget(actor.id, targetActor.id)
25 if (!follow) throw new Error('Cannot find associated follow.') 24 if (!follow) throw new Error('Cannot find associated follow.')
26 25
27 if (follow.state !== 'accepted') { 26 if (follow.state !== 'accepted') {
28 follow.set('state', 'accepted') 27 follow.state = 'accepted'
29 await follow.save() 28 await follow.save()
30 29
31 await addFetchOutboxJob(targetActor) 30 await addFetchOutboxJob(targetActor)
diff --git a/server/lib/activitypub/process/process-announce.ts b/server/lib/activitypub/process/process-announce.ts
index b3cdc4441..7e22125d5 100644
--- a/server/lib/activitypub/process/process-announce.ts
+++ b/server/lib/activitypub/process/process-announce.ts
@@ -5,10 +5,9 @@ import { VideoShareModel } from '../../../models/video/video-share'
5import { forwardVideoRelatedActivity } from '../send/utils' 5import { forwardVideoRelatedActivity } from '../send/utils'
6import { getOrCreateVideoAndAccountAndChannel } from '../videos' 6import { getOrCreateVideoAndAccountAndChannel } from '../videos'
7import { Notifier } from '../../notifier' 7import { Notifier } from '../../notifier'
8import { VideoModel } from '../../../models/video/video'
9import { logger } from '../../../helpers/logger' 8import { logger } from '../../../helpers/logger'
10import { APProcessorOptions } from '../../../typings/activitypub-processor.model' 9import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
11import { SignatureActorModel } from '../../../typings/models' 10import { MActorSignature, MVideoAccountLightBlacklistAllFiles } from '../../../typings/models'
12 11
13async function processAnnounceActivity (options: APProcessorOptions<ActivityAnnounce>) { 12async function processAnnounceActivity (options: APProcessorOptions<ActivityAnnounce>) {
14 const { activity, byActor: actorAnnouncer } = options 13 const { activity, byActor: actorAnnouncer } = options
@@ -26,10 +25,10 @@ export {
26 25
27// --------------------------------------------------------------------------- 26// ---------------------------------------------------------------------------
28 27
29async function processVideoShare (actorAnnouncer: SignatureActorModel, activity: ActivityAnnounce, notify: boolean) { 28async function processVideoShare (actorAnnouncer: MActorSignature, activity: ActivityAnnounce, notify: boolean) {
30 const objectUri = typeof activity.object === 'string' ? activity.object : activity.object.id 29 const objectUri = typeof activity.object === 'string' ? activity.object : activity.object.id
31 30
32 let video: VideoModel 31 let video: MVideoAccountLightBlacklistAllFiles
33 let videoCreated: boolean 32 let videoCreated: boolean
34 33
35 try { 34 try {
diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts
index 6815c6997..bee853721 100644
--- a/server/lib/activitypub/process/process-create.ts
+++ b/server/lib/activitypub/process/process-create.ts
@@ -10,10 +10,8 @@ import { createOrUpdateCacheFile } from '../cache-file'
10import { Notifier } from '../../notifier' 10import { Notifier } from '../../notifier'
11import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object' 11import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object'
12import { createOrUpdateVideoPlaylist } from '../playlist' 12import { createOrUpdateVideoPlaylist } from '../playlist'
13import { VideoModel } from '../../../models/video/video'
14import { APProcessorOptions } from '../../../typings/activitypub-processor.model' 13import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
15import { VideoCommentModel } from '../../../models/video/video-comment' 14import { MActorSignature, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../../typings/models'
16import { SignatureActorModel } from '../../../typings/models'
17 15
18async function processCreateActivity (options: APProcessorOptions<ActivityCreate>) { 16async function processCreateActivity (options: APProcessorOptions<ActivityCreate>) {
19 const { activity, byActor } = options 17 const { activity, byActor } = options
@@ -61,7 +59,7 @@ async function processCreateVideo (activity: ActivityCreate, notify: boolean) {
61 return video 59 return video
62} 60}
63 61
64async function processCreateCacheFile (activity: ActivityCreate, byActor: SignatureActorModel) { 62async function processCreateCacheFile (activity: ActivityCreate, byActor: MActorSignature) {
65 const cacheFile = activity.object as CacheFileObject 63 const cacheFile = activity.object as CacheFileObject
66 64
67 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: cacheFile.object }) 65 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: cacheFile.object })
@@ -77,15 +75,15 @@ async function processCreateCacheFile (activity: ActivityCreate, byActor: Signat
77 } 75 }
78} 76}
79 77
80async function processCreateVideoComment (activity: ActivityCreate, byActor: SignatureActorModel, notify: boolean) { 78async function processCreateVideoComment (activity: ActivityCreate, byActor: MActorSignature, notify: boolean) {
81 const commentObject = activity.object as VideoCommentObject 79 const commentObject = activity.object as VideoCommentObject
82 const byAccount = byActor.Account 80 const byAccount = byActor.Account
83 81
84 if (!byAccount) throw new Error('Cannot create video comment with the non account actor ' + byActor.url) 82 if (!byAccount) throw new Error('Cannot create video comment with the non account actor ' + byActor.url)
85 83
86 let video: VideoModel 84 let video: MVideoAccountLightBlacklistAllFiles
87 let created: boolean 85 let created: boolean
88 let comment: VideoCommentModel 86 let comment: MCommentOwnerVideo
89 try { 87 try {
90 const resolveThreadResult = await resolveThread({ url: commentObject.id, isVideo: false }) 88 const resolveThreadResult = await resolveThread({ url: commentObject.id, isVideo: false })
91 video = resolveThreadResult.video 89 video = resolveThreadResult.video
@@ -110,7 +108,7 @@ async function processCreateVideoComment (activity: ActivityCreate, byActor: Sig
110 if (created && notify) Notifier.Instance.notifyOnNewComment(comment) 108 if (created && notify) Notifier.Instance.notifyOnNewComment(comment)
111} 109}
112 110
113async function processCreatePlaylist (activity: ActivityCreate, byActor: SignatureActorModel) { 111async function processCreatePlaylist (activity: ActivityCreate, byActor: MActorSignature) {
114 const playlistObject = activity.object as PlaylistObject 112 const playlistObject = activity.object as PlaylistObject
115 const byAccount = byActor.Account 113 const byAccount = byActor.Account
116 114
diff --git a/server/lib/activitypub/process/process-delete.ts b/server/lib/activitypub/process/process-delete.ts
index 344d14322..79d0e0d79 100644
--- a/server/lib/activitypub/process/process-delete.ts
+++ b/server/lib/activitypub/process/process-delete.ts
@@ -2,15 +2,13 @@ import { 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'
5import { AccountModel } from '../../../models/account/account'
6import { ActorModel } from '../../../models/activitypub/actor' 5import { ActorModel } from '../../../models/activitypub/actor'
7import { VideoModel } from '../../../models/video/video' 6import { VideoModel } from '../../../models/video/video'
8import { VideoChannelModel } from '../../../models/video/video-channel'
9import { VideoCommentModel } from '../../../models/video/video-comment' 7import { VideoCommentModel } from '../../../models/video/video-comment'
10import { forwardVideoRelatedActivity } from '../send/utils' 8import { forwardVideoRelatedActivity } from '../send/utils'
11import { VideoPlaylistModel } from '../../../models/video/video-playlist' 9import { VideoPlaylistModel } from '../../../models/video/video-playlist'
12import { APProcessorOptions } from '../../../typings/activitypub-processor.model' 10import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
13import { SignatureActorModel } from '../../../typings/models' 11import { MAccountActor, MActor, MActorSignature, MChannelActor, MChannelActorAccountActor } from '../../../typings/models'
14 12
15async function processDeleteActivity (options: APProcessorOptions<ActivityDelete>) { 13async function processDeleteActivity (options: APProcessorOptions<ActivityDelete>) {
16 const { activity, byActor } = options 14 const { activity, byActor } = options
@@ -24,13 +22,17 @@ async function processDeleteActivity (options: APProcessorOptions<ActivityDelete
24 if (byActorFull.type === 'Person') { 22 if (byActorFull.type === 'Person') {
25 if (!byActorFull.Account) throw new Error('Actor ' + byActorFull.url + ' is a person but we cannot find it in database.') 23 if (!byActorFull.Account) throw new Error('Actor ' + byActorFull.url + ' is a person but we cannot find it in database.')
26 24
27 byActorFull.Account.Actor = await byActorFull.Account.$get('Actor') as ActorModel 25 const accountToDelete = byActorFull.Account as MAccountActor
28 return retryTransactionWrapper(processDeleteAccount, byActorFull.Account) 26 accountToDelete.Actor = byActorFull
27
28 return retryTransactionWrapper(processDeleteAccount, accountToDelete)
29 } else if (byActorFull.type === 'Group') { 29 } else if (byActorFull.type === 'Group') {
30 if (!byActorFull.VideoChannel) throw new Error('Actor ' + byActorFull.url + ' is a group but we cannot find it in database.') 30 if (!byActorFull.VideoChannel) throw new Error('Actor ' + byActorFull.url + ' is a group but we cannot find it in database.')
31 31
32 byActorFull.VideoChannel.Actor = await byActorFull.VideoChannel.$get('Actor') as ActorModel 32 const channelToDelete = byActorFull.VideoChannel as MChannelActorAccountActor
33 return retryTransactionWrapper(processDeleteVideoChannel, byActorFull.VideoChannel) 33 channelToDelete.Actor = byActorFull
34
35 return retryTransactionWrapper(processDeleteVideoChannel, channelToDelete)
34 } 36 }
35 } 37 }
36 38
@@ -70,7 +72,7 @@ export {
70 72
71// --------------------------------------------------------------------------- 73// ---------------------------------------------------------------------------
72 74
73async function processDeleteVideo (actor: ActorModel, videoToDelete: VideoModel) { 75async function processDeleteVideo (actor: MActor, videoToDelete: VideoModel) {
74 logger.debug('Removing remote video "%s".', videoToDelete.uuid) 76 logger.debug('Removing remote video "%s".', videoToDelete.uuid)
75 77
76 await sequelizeTypescript.transaction(async t => { 78 await sequelizeTypescript.transaction(async t => {
@@ -84,7 +86,7 @@ async function processDeleteVideo (actor: ActorModel, videoToDelete: VideoModel)
84 logger.info('Remote video with uuid %s removed.', videoToDelete.uuid) 86 logger.info('Remote video with uuid %s removed.', videoToDelete.uuid)
85} 87}
86 88
87async function processDeleteVideoPlaylist (actor: ActorModel, playlistToDelete: VideoPlaylistModel) { 89async function processDeleteVideoPlaylist (actor: MActor, playlistToDelete: VideoPlaylistModel) {
88 logger.debug('Removing remote video playlist "%s".', playlistToDelete.uuid) 90 logger.debug('Removing remote video playlist "%s".', playlistToDelete.uuid)
89 91
90 await sequelizeTypescript.transaction(async t => { 92 await sequelizeTypescript.transaction(async t => {
@@ -98,7 +100,7 @@ async function processDeleteVideoPlaylist (actor: ActorModel, playlistToDelete:
98 logger.info('Remote video playlist with uuid %s removed.', playlistToDelete.uuid) 100 logger.info('Remote video playlist with uuid %s removed.', playlistToDelete.uuid)
99} 101}
100 102
101async function processDeleteAccount (accountToRemove: AccountModel) { 103async function processDeleteAccount (accountToRemove: MAccountActor) {
102 logger.debug('Removing remote account "%s".', accountToRemove.Actor.url) 104 logger.debug('Removing remote account "%s".', accountToRemove.Actor.url)
103 105
104 await sequelizeTypescript.transaction(async t => { 106 await sequelizeTypescript.transaction(async t => {
@@ -108,7 +110,7 @@ async function processDeleteAccount (accountToRemove: AccountModel) {
108 logger.info('Remote account %s removed.', accountToRemove.Actor.url) 110 logger.info('Remote account %s removed.', accountToRemove.Actor.url)
109} 111}
110 112
111async function processDeleteVideoChannel (videoChannelToRemove: VideoChannelModel) { 113async function processDeleteVideoChannel (videoChannelToRemove: MChannelActor) {
112 logger.debug('Removing remote video channel "%s".', videoChannelToRemove.Actor.url) 114 logger.debug('Removing remote video channel "%s".', videoChannelToRemove.Actor.url)
113 115
114 await sequelizeTypescript.transaction(async t => { 116 await sequelizeTypescript.transaction(async t => {
@@ -118,7 +120,7 @@ async function processDeleteVideoChannel (videoChannelToRemove: VideoChannelMode
118 logger.info('Remote video channel %s removed.', videoChannelToRemove.Actor.url) 120 logger.info('Remote video channel %s removed.', videoChannelToRemove.Actor.url)
119} 121}
120 122
121function processDeleteVideoComment (byActor: SignatureActorModel, videoComment: VideoCommentModel, activity: ActivityDelete) { 123function processDeleteVideoComment (byActor: MActorSignature, videoComment: VideoCommentModel, activity: ActivityDelete) {
122 logger.debug('Removing remote video comment "%s".', videoComment.url) 124 logger.debug('Removing remote video comment "%s".', videoComment.url)
123 125
124 return sequelizeTypescript.transaction(async t => { 126 return sequelizeTypescript.transaction(async t => {
diff --git a/server/lib/activitypub/process/process-dislike.ts b/server/lib/activitypub/process/process-dislike.ts
index 727fcfee0..debd8a67c 100644
--- a/server/lib/activitypub/process/process-dislike.ts
+++ b/server/lib/activitypub/process/process-dislike.ts
@@ -7,7 +7,7 @@ import { getOrCreateVideoAndAccountAndChannel } from '../videos'
7import { forwardVideoRelatedActivity } from '../send/utils' 7import { forwardVideoRelatedActivity } from '../send/utils'
8import { getVideoDislikeActivityPubUrl } from '../url' 8import { getVideoDislikeActivityPubUrl } from '../url'
9import { APProcessorOptions } from '../../../typings/activitypub-processor.model' 9import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
10import { SignatureActorModel } from '../../../typings/models' 10import { MActorSignature } from '../../../typings/models'
11 11
12async function processDislikeActivity (options: APProcessorOptions<ActivityCreate | ActivityDislike>) { 12async function processDislikeActivity (options: APProcessorOptions<ActivityCreate | ActivityDislike>) {
13 const { activity, byActor } = options 13 const { activity, byActor } = options
@@ -22,7 +22,7 @@ export {
22 22
23// --------------------------------------------------------------------------- 23// ---------------------------------------------------------------------------
24 24
25async function processDislike (activity: ActivityCreate | ActivityDislike, byActor: SignatureActorModel) { 25async function processDislike (activity: ActivityCreate | ActivityDislike, byActor: MActorSignature) {
26 const dislikeObject = activity.type === 'Dislike' ? activity.object : (activity.object as DislikeObject).object 26 const dislikeObject = activity.type === 'Dislike' ? activity.object : (activity.object as DislikeObject).object
27 const byAccount = byActor.Account 27 const byAccount = byActor.Account
28 28
diff --git a/server/lib/activitypub/process/process-flag.ts b/server/lib/activitypub/process/process-flag.ts
index 1f8a80c14..e6e9084de 100644
--- a/server/lib/activitypub/process/process-flag.ts
+++ b/server/lib/activitypub/process/process-flag.ts
@@ -8,7 +8,7 @@ import { 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 { SignatureActorModel } from '../../../typings/models' 11import { MActorSignature, MVideoAbuseVideo } from '../../../typings/models'
12 12
13async function processFlagActivity (options: APProcessorOptions<ActivityCreate | ActivityFlag>) { 13async function processFlagActivity (options: APProcessorOptions<ActivityCreate | ActivityFlag>) {
14 const { activity, byActor } = options 14 const { activity, byActor } = options
@@ -23,31 +23,39 @@ export {
23 23
24// --------------------------------------------------------------------------- 24// ---------------------------------------------------------------------------
25 25
26async function processCreateVideoAbuse (activity: ActivityCreate | ActivityFlag, byActor: SignatureActorModel) { 26async function processCreateVideoAbuse (activity: ActivityCreate | ActivityFlag, byActor: MActorSignature) {
27 const flag = activity.type === 'Flag' ? activity : (activity.object as VideoAbuseObject) 27 const flag = activity.type === 'Flag' ? activity : (activity.object as VideoAbuseObject)
28 28
29 logger.debug('Reporting remote abuse for video %s.', getAPId(flag.object))
30
31 const account = byActor.Account 29 const account = byActor.Account
32 if (!account) throw new Error('Cannot create video abuse with the non account actor ' + byActor.url) 30 if (!account) throw new Error('Cannot create video abuse with the non account actor ' + byActor.url)
33 31
34 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: flag.object }) 32 const objects = Array.isArray(flag.object) ? flag.object : [ flag.object ]
35 33
36 const videoAbuse = await sequelizeTypescript.transaction(async t => { 34 for (const object of objects) {
37 const videoAbuseData = { 35 try {
38 reporterAccountId: account.id, 36 logger.debug('Reporting remote abuse for video %s.', getAPId(object))
39 reason: flag.content, 37
40 videoId: video.id, 38 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: object })
41 state: VideoAbuseState.PENDING
42 }
43 39
44 const videoAbuseInstance = await VideoAbuseModel.create(videoAbuseData, { transaction: t }) 40 const videoAbuse = await sequelizeTypescript.transaction(async t => {
45 videoAbuseInstance.Video = video 41 const videoAbuseData = {
42 reporterAccountId: account.id,
43 reason: flag.content,
44 videoId: video.id,
45 state: VideoAbuseState.PENDING
46 }
46 47
47 logger.info('Remote abuse for video uuid %s created', flag.object) 48 const videoAbuseInstance = await VideoAbuseModel.create(videoAbuseData, { transaction: t }) as MVideoAbuseVideo
49 videoAbuseInstance.Video = video
48 50
49 return videoAbuseInstance 51 logger.info('Remote abuse for video uuid %s created', flag.object)
50 })
51 52
52 Notifier.Instance.notifyOnNewVideoAbuse(videoAbuse) 53 return videoAbuseInstance
54 })
55
56 Notifier.Instance.notifyOnNewVideoAbuse(videoAbuse)
57 } catch (err) {
58 logger.debug('Cannot process report of %s. (Maybe not a video abuse).', getAPId(object), { err })
59 }
60 }
53} 61}
diff --git a/server/lib/activitypub/process/process-follow.ts b/server/lib/activitypub/process/process-follow.ts
index 240aa5799..85f22d654 100644
--- a/server/lib/activitypub/process/process-follow.ts
+++ b/server/lib/activitypub/process/process-follow.ts
@@ -10,8 +10,8 @@ import { getAPId } from '../../../helpers/activitypub'
10import { getServerActor } from '../../../helpers/utils' 10import { getServerActor } from '../../../helpers/utils'
11import { CONFIG } from '../../../initializers/config' 11import { CONFIG } from '../../../initializers/config'
12import { APProcessorOptions } from '../../../typings/activitypub-processor.model' 12import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
13import { SignatureActorModel } from '../../../typings/models' 13import { MActorFollowActors, MActorSignature } from '../../../typings/models'
14import { ActorFollowModelLight } from '../../../typings/models/actor-follow' 14import { autoFollowBackIfNeeded } from '../follow'
15 15
16async function processFollowActivity (options: APProcessorOptions<ActivityFollow>) { 16async function processFollowActivity (options: APProcessorOptions<ActivityFollow>) {
17 const { activity, byActor } = options 17 const { activity, byActor } = options
@@ -28,8 +28,8 @@ export {
28 28
29// --------------------------------------------------------------------------- 29// ---------------------------------------------------------------------------
30 30
31async function processFollow (byActor: SignatureActorModel, targetActorURL: string) { 31async function processFollow (byActor: MActorSignature, targetActorURL: string) {
32 const { actorFollow, created, isFollowingInstance } = await sequelizeTypescript.transaction(async t => { 32 const { actorFollow, created, isFollowingInstance, targetActor } = await sequelizeTypescript.transaction(async t => {
33 const targetActor = await ActorModel.loadByUrlAndPopulateAccountAndChannel(targetActorURL, t) 33 const targetActor = await ActorModel.loadByUrlAndPopulateAccountAndChannel(targetActorURL, t)
34 34
35 if (!targetActor) throw new Error('Unknown actor') 35 if (!targetActor) throw new Error('Unknown actor')
@@ -43,10 +43,10 @@ async function processFollow (byActor: SignatureActorModel, targetActorURL: stri
43 43
44 await sendReject(byActor, targetActor) 44 await sendReject(byActor, targetActor)
45 45
46 return { actorFollow: undefined } 46 return { actorFollow: undefined as MActorFollowActors }
47 } 47 }
48 48
49 const [ actorFollow, created ] = await ActorFollowModel.findOrCreate({ 49 const [ actorFollow, created ] = await ActorFollowModel.findOrCreate<MActorFollowActors>({
50 where: { 50 where: {
51 actorId: byActor.id, 51 actorId: byActor.id,
52 targetActorId: targetActor.id 52 targetActorId: targetActor.id
@@ -57,7 +57,7 @@ async function processFollow (byActor: SignatureActorModel, targetActorURL: stri
57 state: CONFIG.FOLLOWERS.INSTANCE.MANUAL_APPROVAL ? 'pending' : 'accepted' 57 state: CONFIG.FOLLOWERS.INSTANCE.MANUAL_APPROVAL ? 'pending' : 'accepted'
58 }, 58 },
59 transaction: t 59 transaction: t
60 }) as [ ActorFollowModelLight, boolean ] 60 })
61 61
62 if (actorFollow.state !== 'accepted' && CONFIG.FOLLOWERS.INSTANCE.MANUAL_APPROVAL === false) { 62 if (actorFollow.state !== 'accepted' && CONFIG.FOLLOWERS.INSTANCE.MANUAL_APPROVAL === false) {
63 actorFollow.state = 'accepted' 63 actorFollow.state = 'accepted'
@@ -68,17 +68,26 @@ async function processFollow (byActor: SignatureActorModel, targetActorURL: stri
68 actorFollow.ActorFollowing = targetActor 68 actorFollow.ActorFollowing = targetActor
69 69
70 // Target sends to actor he accepted the follow request 70 // Target sends to actor he accepted the follow request
71 if (actorFollow.state === 'accepted') await sendAccept(actorFollow) 71 if (actorFollow.state === 'accepted') {
72 await sendAccept(actorFollow)
73 await autoFollowBackIfNeeded(actorFollow)
74 }
72 75
73 return { actorFollow, created, isFollowingInstance } 76 return { actorFollow, created, isFollowingInstance, targetActor }
74 }) 77 })
75 78
76 // Rejected 79 // Rejected
77 if (!actorFollow) return 80 if (!actorFollow) return
78 81
79 if (created) { 82 if (created) {
80 if (isFollowingInstance) Notifier.Instance.notifyOfNewInstanceFollow(actorFollow) 83 const follower = await ActorModel.loadFull(byActor.id)
81 else Notifier.Instance.notifyOfNewUserFollow(actorFollow) 84 const actorFollowFull = Object.assign(actorFollow, { ActorFollowing: targetActor, ActorFollower: follower })
85
86 if (isFollowingInstance) {
87 Notifier.Instance.notifyOfNewInstanceFollow(actorFollowFull)
88 } else {
89 Notifier.Instance.notifyOfNewUserFollow(actorFollowFull)
90 }
82 } 91 }
83 92
84 logger.info('Actor %s is followed by actor %s.', targetActorURL, byActor.url) 93 logger.info('Actor %s is followed by actor %s.', targetActorURL, byActor.url)
diff --git a/server/lib/activitypub/process/process-like.ts b/server/lib/activitypub/process/process-like.ts
index cf559af72..62be0de42 100644
--- a/server/lib/activitypub/process/process-like.ts
+++ b/server/lib/activitypub/process/process-like.ts
@@ -7,7 +7,7 @@ import { getOrCreateVideoAndAccountAndChannel } from '../videos'
7import { getVideoLikeActivityPubUrl } from '../url' 7import { getVideoLikeActivityPubUrl } from '../url'
8import { getAPId } from '../../../helpers/activitypub' 8import { getAPId } from '../../../helpers/activitypub'
9import { APProcessorOptions } from '../../../typings/activitypub-processor.model' 9import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
10import { SignatureActorModel } from '../../../typings/models' 10import { MActorSignature } from '../../../typings/models'
11 11
12async function processLikeActivity (options: APProcessorOptions<ActivityLike>) { 12async function processLikeActivity (options: APProcessorOptions<ActivityLike>) {
13 const { activity, byActor } = options 13 const { activity, byActor } = options
@@ -22,7 +22,7 @@ export {
22 22
23// --------------------------------------------------------------------------- 23// ---------------------------------------------------------------------------
24 24
25async function processLikeVideo (byActor: SignatureActorModel, activity: ActivityLike) { 25async function processLikeVideo (byActor: MActorSignature, activity: ActivityLike) {
26 const videoUrl = getAPId(activity.object) 26 const videoUrl = getAPId(activity.object)
27 27
28 const byAccount = byActor.Account 28 const byAccount = byActor.Account
diff --git a/server/lib/activitypub/process/process-reject.ts b/server/lib/activitypub/process/process-reject.ts
index 22e311ceb..00e9afa10 100644
--- a/server/lib/activitypub/process/process-reject.ts
+++ b/server/lib/activitypub/process/process-reject.ts
@@ -2,7 +2,7 @@ import { ActivityReject } from '../../../../shared/models/activitypub/activity'
2import { sequelizeTypescript } from '../../../initializers' 2import { sequelizeTypescript } from '../../../initializers'
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 { ActorModelOnly } from '../../../typings/models' 5import { MActor } from '../../../typings/models'
6 6
7async function processRejectActivity (options: APProcessorOptions<ActivityReject>) { 7async function processRejectActivity (options: APProcessorOptions<ActivityReject>) {
8 const { byActor: targetActor, inboxActor } = options 8 const { byActor: targetActor, inboxActor } = options
@@ -19,7 +19,7 @@ export {
19 19
20// --------------------------------------------------------------------------- 20// ---------------------------------------------------------------------------
21 21
22async function processReject (follower: ActorModelOnly, targetActor: ActorModelOnly) { 22async function processReject (follower: MActor, targetActor: MActor) {
23 return sequelizeTypescript.transaction(async t => { 23 return sequelizeTypescript.transaction(async t => {
24 const actorFollow = await ActorFollowModel.loadByActorAndTarget(follower.id, targetActor.id, t) 24 const actorFollow = await ActorFollowModel.loadByActorAndTarget(follower.id, targetActor.id, t)
25 25
diff --git a/server/lib/activitypub/process/process-undo.ts b/server/lib/activitypub/process/process-undo.ts
index c37ee38bb..10643b2e9 100644
--- a/server/lib/activitypub/process/process-undo.ts
+++ b/server/lib/activitypub/process/process-undo.ts
@@ -11,7 +11,7 @@ import { getOrCreateVideoAndAccountAndChannel } from '../videos'
11import { VideoShareModel } from '../../../models/video/video-share' 11import { VideoShareModel } from '../../../models/video/video-share'
12import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy' 12import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy'
13import { APProcessorOptions } from '../../../typings/activitypub-processor.model' 13import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
14import { SignatureActorModel } from '../../../typings/models' 14import { MActorSignature } from '../../../typings/models'
15 15
16async function processUndoActivity (options: APProcessorOptions<ActivityUndo>) { 16async function processUndoActivity (options: APProcessorOptions<ActivityUndo>) {
17 const { activity, byActor } = options 17 const { activity, byActor } = options
@@ -54,7 +54,7 @@ export {
54 54
55// --------------------------------------------------------------------------- 55// ---------------------------------------------------------------------------
56 56
57async function processUndoLike (byActor: SignatureActorModel, activity: ActivityUndo) { 57async function processUndoLike (byActor: MActorSignature, activity: ActivityUndo) {
58 const likeActivity = activity.object as ActivityLike 58 const likeActivity = activity.object as ActivityLike
59 59
60 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: likeActivity.object }) 60 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: likeActivity.object })
@@ -77,7 +77,7 @@ async function processUndoLike (byActor: SignatureActorModel, activity: Activity
77 }) 77 })
78} 78}
79 79
80async function processUndoDislike (byActor: SignatureActorModel, activity: ActivityUndo) { 80async function processUndoDislike (byActor: MActorSignature, activity: ActivityUndo) {
81 const dislike = activity.object.type === 'Dislike' 81 const dislike = activity.object.type === 'Dislike'
82 ? activity.object 82 ? activity.object
83 : activity.object.object as DislikeObject 83 : activity.object.object as DislikeObject
@@ -102,7 +102,7 @@ async function processUndoDislike (byActor: SignatureActorModel, activity: Activ
102 }) 102 })
103} 103}
104 104
105async function processUndoCacheFile (byActor: SignatureActorModel, activity: ActivityUndo) { 105async function processUndoCacheFile (byActor: MActorSignature, activity: ActivityUndo) {
106 const cacheFileObject = activity.object.object as CacheFileObject 106 const cacheFileObject = activity.object.object as CacheFileObject
107 107
108 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: cacheFileObject.object }) 108 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: cacheFileObject.object })
@@ -127,7 +127,7 @@ async function processUndoCacheFile (byActor: SignatureActorModel, activity: Act
127 }) 127 })
128} 128}
129 129
130function processUndoFollow (follower: SignatureActorModel, followActivity: ActivityFollow) { 130function processUndoFollow (follower: MActorSignature, followActivity: ActivityFollow) {
131 return sequelizeTypescript.transaction(async t => { 131 return sequelizeTypescript.transaction(async t => {
132 const following = await ActorModel.loadByUrlAndPopulateAccountAndChannel(followActivity.object, t) 132 const following = await ActorModel.loadByUrlAndPopulateAccountAndChannel(followActivity.object, t)
133 const actorFollow = await ActorFollowModel.loadByActorAndTarget(follower.id, following.id, t) 133 const actorFollow = await ActorFollowModel.loadByActorAndTarget(follower.id, following.id, t)
@@ -140,7 +140,7 @@ function processUndoFollow (follower: SignatureActorModel, followActivity: Activ
140 }) 140 })
141} 141}
142 142
143function processUndoAnnounce (byActor: SignatureActorModel, announceActivity: ActivityAnnounce) { 143function processUndoAnnounce (byActor: MActorSignature, announceActivity: ActivityAnnounce) {
144 return sequelizeTypescript.transaction(async t => { 144 return sequelizeTypescript.transaction(async t => {
145 const share = await VideoShareModel.loadByUrl(announceActivity.id, t) 145 const share = await VideoShareModel.loadByUrl(announceActivity.id, t)
146 if (!share) throw new Error(`Unknown video share ${announceActivity.id}.`) 146 if (!share) throw new Error(`Unknown video share ${announceActivity.id}.`)
diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts
index 414f9e375..a47d605d8 100644
--- a/server/lib/activitypub/process/process-update.ts
+++ b/server/lib/activitypub/process/process-update.ts
@@ -15,7 +15,7 @@ import { forwardVideoRelatedActivity } from '../send/utils'
15import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object' 15import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object'
16import { createOrUpdateVideoPlaylist } from '../playlist' 16import { createOrUpdateVideoPlaylist } from '../playlist'
17import { APProcessorOptions } from '../../../typings/activitypub-processor.model' 17import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
18import { SignatureActorModel } from '../../../typings/models' 18import { MActorSignature, MAccountIdActor } from '../../../typings/models'
19 19
20async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) { 20async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) {
21 const { activity, byActor } = options 21 const { activity, byActor } = options
@@ -53,7 +53,7 @@ export {
53 53
54// --------------------------------------------------------------------------- 54// ---------------------------------------------------------------------------
55 55
56async function processUpdateVideo (actor: SignatureActorModel, activity: ActivityUpdate) { 56async function processUpdateVideo (actor: MActorSignature, activity: ActivityUpdate) {
57 const videoObject = activity.object as VideoTorrentObject 57 const videoObject = activity.object as VideoTorrentObject
58 58
59 if (sanitizeAndCheckVideoTorrentObject(videoObject) === false) { 59 if (sanitizeAndCheckVideoTorrentObject(videoObject) === false) {
@@ -61,20 +61,23 @@ async function processUpdateVideo (actor: SignatureActorModel, activity: Activit
61 return undefined 61 return undefined
62 } 62 }
63 63
64 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: videoObject.id, allowRefresh: false }) 64 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: videoObject.id, allowRefresh: false, fetchType: 'all' })
65 const channelActor = await getOrCreateVideoChannelFromVideoObject(videoObject) 65 const channelActor = await getOrCreateVideoChannelFromVideoObject(videoObject)
66 66
67 const account = actor.Account as MAccountIdActor
68 account.Actor = actor
69
67 const updateOptions = { 70 const updateOptions = {
68 video, 71 video,
69 videoObject, 72 videoObject,
70 account: actor.Account, 73 account,
71 channel: channelActor.VideoChannel, 74 channel: channelActor.VideoChannel,
72 overrideTo: activity.to 75 overrideTo: activity.to
73 } 76 }
74 return updateVideoFromAP(updateOptions) 77 return updateVideoFromAP(updateOptions)
75} 78}
76 79
77async function processUpdateCacheFile (byActor: SignatureActorModel, activity: ActivityUpdate) { 80async function processUpdateCacheFile (byActor: MActorSignature, activity: ActivityUpdate) {
78 const cacheFileObject = activity.object as CacheFileObject 81 const cacheFileObject = activity.object as CacheFileObject
79 82
80 if (!isCacheFileObjectValid(cacheFileObject)) { 83 if (!isCacheFileObjectValid(cacheFileObject)) {
@@ -150,7 +153,7 @@ async function processUpdateActor (actor: ActorModel, activity: ActivityUpdate)
150 } 153 }
151} 154}
152 155
153async function processUpdatePlaylist (byActor: SignatureActorModel, activity: ActivityUpdate) { 156async function processUpdatePlaylist (byActor: MActorSignature, activity: ActivityUpdate) {
154 const playlistObject = activity.object as PlaylistObject 157 const playlistObject = activity.object as PlaylistObject
155 const byAccount = byActor.Account 158 const byAccount = byActor.Account
156 159
diff --git a/server/lib/activitypub/process/process-view.ts b/server/lib/activitypub/process/process-view.ts
index e4997b828..df29ee968 100644
--- a/server/lib/activitypub/process/process-view.ts
+++ b/server/lib/activitypub/process/process-view.ts
@@ -3,7 +3,7 @@ import { forwardVideoRelatedActivity } from '../send/utils'
3import { Redis } from '../../redis' 3import { Redis } from '../../redis'
4import { ActivityCreate, ActivityView, ViewObject } from '../../../../shared/models/activitypub' 4import { ActivityCreate, ActivityView, ViewObject } from '../../../../shared/models/activitypub'
5import { APProcessorOptions } from '../../../typings/activitypub-processor.model' 5import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
6import { SignatureActorModel } from '../../../typings/models' 6import { MActorSignature } from '../../../typings/models'
7 7
8async function processViewActivity (options: APProcessorOptions<ActivityCreate | ActivityView>) { 8async function processViewActivity (options: APProcessorOptions<ActivityCreate | ActivityView>) {
9 const { activity, byActor } = options 9 const { activity, byActor } = options
@@ -18,11 +18,11 @@ export {
18 18
19// --------------------------------------------------------------------------- 19// ---------------------------------------------------------------------------
20 20
21async function processCreateView (activity: ActivityView | ActivityCreate, byActor: SignatureActorModel) { 21async function processCreateView (activity: ActivityView | ActivityCreate, byActor: MActorSignature) {
22 const videoObject = activity.type === 'View' ? activity.object : (activity.object as ViewObject).object 22 const videoObject = activity.type === 'View' ? activity.object : (activity.object as ViewObject).object
23 23
24 const options = { 24 const options = {
25 videoObject: videoObject, 25 videoObject,
26 fetchType: 'only-video' as 'only-video' 26 fetchType: 'only-video' as 'only-video'
27 } 27 }
28 const { video } = await getOrCreateVideoAndAccountAndChannel(options) 28 const { video } = await getOrCreateVideoAndAccountAndChannel(options)
diff --git a/server/lib/activitypub/process/process.ts b/server/lib/activitypub/process/process.ts
index d108fe321..c602bf218 100644
--- a/server/lib/activitypub/process/process.ts
+++ b/server/lib/activitypub/process/process.ts
@@ -1,7 +1,6 @@
1import { Activity, ActivityType } from '../../../../shared/models/activitypub' 1import { Activity, ActivityType } from '../../../../shared/models/activitypub'
2import { checkUrlsSameHost, getAPId } from '../../../helpers/activitypub' 2import { checkUrlsSameHost, getAPId } from '../../../helpers/activitypub'
3import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
4import { ActorModel } from '../../../models/activitypub/actor'
5import { processAcceptActivity } from './process-accept' 4import { processAcceptActivity } from './process-accept'
6import { processAnnounceActivity } from './process-announce' 5import { processAnnounceActivity } from './process-announce'
7import { processCreateActivity } from './process-create' 6import { processCreateActivity } from './process-create'
@@ -16,7 +15,7 @@ import { processDislikeActivity } from './process-dislike'
16import { processFlagActivity } from './process-flag' 15import { processFlagActivity } from './process-flag'
17import { processViewActivity } from './process-view' 16import { processViewActivity } from './process-view'
18import { APProcessorOptions } from '../../../typings/activitypub-processor.model' 17import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
19import { SignatureActorModel } from '../../../typings/models' 18import { MActorDefault, MActorSignature } from '../../../typings/models'
20 19
21const processActivity: { [ P in ActivityType ]: (options: APProcessorOptions<Activity>) => Promise<any> } = { 20const processActivity: { [ P in ActivityType ]: (options: APProcessorOptions<Activity>) => Promise<any> } = {
22 Create: processCreateActivity, 21 Create: processCreateActivity,
@@ -36,15 +35,15 @@ const processActivity: { [ P in ActivityType ]: (options: APProcessorOptions<Act
36async function processActivities ( 35async function processActivities (
37 activities: Activity[], 36 activities: Activity[],
38 options: { 37 options: {
39 signatureActor?: SignatureActorModel 38 signatureActor?: MActorSignature
40 inboxActor?: ActorModel 39 inboxActor?: MActorDefault
41 outboxUrl?: string 40 outboxUrl?: string
42 fromFetch?: boolean 41 fromFetch?: boolean
43 } = {} 42 } = {}
44) { 43) {
45 const { outboxUrl, signatureActor, inboxActor, fromFetch = false } = options 44 const { outboxUrl, signatureActor, inboxActor, fromFetch = false } = options
46 45
47 const actorsCache: { [ url: string ]: SignatureActorModel } = {} 46 const actorsCache: { [ url: string ]: MActorSignature } = {}
48 47
49 for (const activity of activities) { 48 for (const activity of activities) {
50 if (!signatureActor && [ 'Create', 'Announce', 'Like' ].includes(activity.type) === false) { 49 if (!signatureActor && [ 'Create', 'Announce', 'Like' ].includes(activity.type) === false) {
@@ -75,7 +74,7 @@ async function processActivities (
75 } 74 }
76 75
77 try { 76 try {
78 await activityProcessor({ activity, byActor, inboxActor: inboxActor, fromFetch }) 77 await activityProcessor({ activity, byActor, inboxActor, fromFetch })
79 } catch (err) { 78 } catch (err) {
80 logger.warn('Cannot process activity %s.', activity.type, { err }) 79 logger.warn('Cannot process activity %s.', activity.type, { err })
81 } 80 }
diff --git a/server/lib/activitypub/send/send-accept.ts b/server/lib/activitypub/send/send-accept.ts
index 813c42e15..9f0225b64 100644
--- a/server/lib/activitypub/send/send-accept.ts
+++ b/server/lib/activitypub/send/send-accept.ts
@@ -3,10 +3,9 @@ import { getActorFollowAcceptActivityPubUrl, getActorFollowActivityPubUrl } from
3import { unicastTo } from './utils' 3import { unicastTo } from './utils'
4import { buildFollowActivity } from './send-follow' 4import { buildFollowActivity } from './send-follow'
5import { logger } from '../../../helpers/logger' 5import { logger } from '../../../helpers/logger'
6import { ActorFollowModelLight } from '../../../typings/models/actor-follow' 6import { MActor, MActorFollowActors } from '../../../typings/models'
7import { ActorModelOnly } from '../../../typings/models'
8 7
9async function sendAccept (actorFollow: ActorFollowModelLight) { 8async function sendAccept (actorFollow: MActorFollowActors) {
10 const follower = actorFollow.ActorFollower 9 const follower = actorFollow.ActorFollower
11 const me = actorFollow.ActorFollowing 10 const me = actorFollow.ActorFollowing
12 11
@@ -34,7 +33,7 @@ export {
34 33
35// --------------------------------------------------------------------------- 34// ---------------------------------------------------------------------------
36 35
37function buildAcceptActivity (url: string, byActor: ActorModelOnly, followActivityData: ActivityFollow): ActivityAccept { 36function buildAcceptActivity (url: string, byActor: MActor, followActivityData: ActivityFollow): ActivityAccept {
38 return { 37 return {
39 type: 'Accept', 38 type: 'Accept',
40 id: url, 39 id: url,
diff --git a/server/lib/activitypub/send/send-announce.ts b/server/lib/activitypub/send/send-announce.ts
index 7fe4ca180..a0f33852c 100644
--- a/server/lib/activitypub/send/send-announce.ts
+++ b/server/lib/activitypub/send/send-announce.ts
@@ -1,16 +1,15 @@
1import { Transaction } from 'sequelize' 1import { Transaction } from 'sequelize'
2import { ActivityAnnounce, ActivityAudience } from '../../../../shared/models/activitypub' 2import { ActivityAnnounce, ActivityAudience } from '../../../../shared/models/activitypub'
3import { VideoModel } from '../../../models/video/video'
4import { broadcastToFollowers } from './utils' 3import { broadcastToFollowers } from './utils'
5import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf } from '../audience' 4import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf } from '../audience'
6import { logger } from '../../../helpers/logger' 5import { logger } from '../../../helpers/logger'
7import { ActorModelOnly } from '../../../typings/models' 6import { MActorLight, MVideo } from '../../../typings/models'
8import { VideoShareModelOnly } from '../../../typings/models/video-share' 7import { MVideoShare } from '../../../typings/models/video'
9 8
10async function buildAnnounceWithVideoAudience ( 9async function buildAnnounceWithVideoAudience (
11 byActor: ActorModelOnly, 10 byActor: MActorLight,
12 videoShare: VideoShareModelOnly, 11 videoShare: MVideoShare,
13 video: VideoModel, 12 video: MVideo,
14 t: Transaction 13 t: Transaction
15) { 14) {
16 const announcedObject = video.url 15 const announcedObject = video.url
@@ -23,7 +22,7 @@ async function buildAnnounceWithVideoAudience (
23 return { activity, actorsInvolvedInVideo } 22 return { activity, actorsInvolvedInVideo }
24} 23}
25 24
26async function sendVideoAnnounce (byActor: ActorModelOnly, videoShare: VideoShareModelOnly, video: VideoModel, t: Transaction) { 25async function sendVideoAnnounce (byActor: MActorLight, videoShare: MVideoShare, video: MVideo, t: Transaction) {
27 const { activity, actorsInvolvedInVideo } = await buildAnnounceWithVideoAudience(byActor, videoShare, video, t) 26 const { activity, actorsInvolvedInVideo } = await buildAnnounceWithVideoAudience(byActor, videoShare, video, t)
28 27
29 logger.info('Creating job to send announce %s.', videoShare.url) 28 logger.info('Creating job to send announce %s.', videoShare.url)
@@ -32,7 +31,7 @@ async function sendVideoAnnounce (byActor: ActorModelOnly, videoShare: VideoShar
32 return broadcastToFollowers(activity, byActor, actorsInvolvedInVideo, t, followersException) 31 return broadcastToFollowers(activity, byActor, actorsInvolvedInVideo, t, followersException)
33} 32}
34 33
35function buildAnnounceActivity (url: string, byActor: ActorModelOnly, object: string, audience?: ActivityAudience): ActivityAnnounce { 34function buildAnnounceActivity (url: string, byActor: MActorLight, object: string, audience?: ActivityAudience): ActivityAnnounce {
36 if (!audience) audience = getAudience(byActor) 35 if (!audience) audience = getAudience(byActor)
37 36
38 return audiencify({ 37 return audiencify({
diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts
index 9c21149f2..26ec3e948 100644
--- a/server/lib/activitypub/send/send-create.ts
+++ b/server/lib/activitypub/send/send-create.ts
@@ -1,19 +1,23 @@
1import { Transaction } from 'sequelize' 1import { Transaction } from 'sequelize'
2import { ActivityAudience, ActivityCreate } from '../../../../shared/models/activitypub' 2import { ActivityAudience, ActivityCreate } from '../../../../shared/models/activitypub'
3import { VideoPrivacy } from '../../../../shared/models/videos' 3import { VideoPrivacy } from '../../../../shared/models/videos'
4import { ActorModel } from '../../../models/activitypub/actor'
5import { VideoModel } from '../../../models/video/video'
6import { VideoCommentModel } from '../../../models/video/video-comment' 4import { VideoCommentModel } from '../../../models/video/video-comment'
7import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' 5import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils'
8import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience' 6import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience'
9import { logger } from '../../../helpers/logger' 7import { logger } from '../../../helpers/logger'
10import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy'
11import { VideoPlaylistModel } from '../../../models/video/video-playlist'
12import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' 8import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
13import { getServerActor } from '../../../helpers/utils' 9import { getServerActor } from '../../../helpers/utils'
14import * as Bluebird from 'bluebird' 10import {
15 11 MActorLight,
16async function sendCreateVideo (video: VideoModel, t: Transaction) { 12 MCommentOwnerVideo,
13 MVideoAccountLight,
14 MVideoAP,
15 MVideoPlaylistFull,
16 MVideoRedundancyFileVideo,
17 MVideoRedundancyStreamingPlaylistVideo
18} from '../../../typings/models'
19
20async function sendCreateVideo (video: MVideoAP, t: Transaction) {
17 if (video.privacy === VideoPrivacy.PRIVATE) return undefined 21 if (video.privacy === VideoPrivacy.PRIVATE) return undefined
18 22
19 logger.info('Creating job to send video creation of %s.', video.url) 23 logger.info('Creating job to send video creation of %s.', video.url)
@@ -27,7 +31,11 @@ async function sendCreateVideo (video: VideoModel, t: Transaction) {
27 return broadcastToFollowers(createActivity, byActor, [ byActor ], t) 31 return broadcastToFollowers(createActivity, byActor, [ byActor ], t)
28} 32}
29 33
30async function sendCreateCacheFile (byActor: ActorModel, video: VideoModel, fileRedundancy: VideoRedundancyModel) { 34async function sendCreateCacheFile (
35 byActor: MActorLight,
36 video: MVideoAccountLight,
37 fileRedundancy: MVideoRedundancyStreamingPlaylistVideo | MVideoRedundancyFileVideo
38) {
31 logger.info('Creating job to send file cache of %s.', fileRedundancy.url) 39 logger.info('Creating job to send file cache of %s.', fileRedundancy.url)
32 40
33 return sendVideoRelatedCreateActivity({ 41 return sendVideoRelatedCreateActivity({
@@ -38,7 +46,7 @@ async function sendCreateCacheFile (byActor: ActorModel, video: VideoModel, file
38 }) 46 })
39} 47}
40 48
41async function sendCreateVideoPlaylist (playlist: VideoPlaylistModel, t: Transaction) { 49async function sendCreateVideoPlaylist (playlist: MVideoPlaylistFull, t: Transaction) {
42 if (playlist.privacy === VideoPlaylistPrivacy.PRIVATE) return undefined 50 if (playlist.privacy === VideoPlaylistPrivacy.PRIVATE) return undefined
43 51
44 logger.info('Creating job to send create video playlist of %s.', playlist.url) 52 logger.info('Creating job to send create video playlist of %s.', playlist.url)
@@ -57,7 +65,7 @@ async function sendCreateVideoPlaylist (playlist: VideoPlaylistModel, t: Transac
57 return broadcastToFollowers(createActivity, byActor, toFollowersOf, t) 65 return broadcastToFollowers(createActivity, byActor, toFollowersOf, t)
58} 66}
59 67
60async function sendCreateVideoComment (comment: VideoCommentModel, t: Transaction) { 68async function sendCreateVideoComment (comment: MCommentOwnerVideo, t: Transaction) {
61 logger.info('Creating job to send comment %s.', comment.url) 69 logger.info('Creating job to send comment %s.', comment.url)
62 70
63 const isOrigin = comment.Video.isOwned() 71 const isOrigin = comment.Video.isOwned()
@@ -95,7 +103,7 @@ async function sendCreateVideoComment (comment: VideoCommentModel, t: Transactio
95 t.afterCommit(() => unicastTo(createActivity, byActor, comment.Video.VideoChannel.Account.Actor.sharedInboxUrl)) 103 t.afterCommit(() => unicastTo(createActivity, byActor, comment.Video.VideoChannel.Account.Actor.sharedInboxUrl))
96} 104}
97 105
98function buildCreateActivity (url: string, byActor: ActorModel, object: any, audience?: ActivityAudience): ActivityCreate { 106function buildCreateActivity (url: string, byActor: MActorLight, object: any, audience?: ActivityAudience): ActivityCreate {
99 if (!audience) audience = getAudience(byActor) 107 if (!audience) audience = getAudience(byActor)
100 108
101 return audiencify( 109 return audiencify(
@@ -122,8 +130,8 @@ export {
122// --------------------------------------------------------------------------- 130// ---------------------------------------------------------------------------
123 131
124async function sendVideoRelatedCreateActivity (options: { 132async function sendVideoRelatedCreateActivity (options: {
125 byActor: ActorModel, 133 byActor: MActorLight,
126 video: VideoModel, 134 video: MVideoAccountLight,
127 url: string, 135 url: string,
128 object: any, 136 object: any,
129 transaction?: Transaction 137 transaction?: Transaction
diff --git a/server/lib/activitypub/send/send-delete.ts b/server/lib/activitypub/send/send-delete.ts
index 6c7fb8449..4b1ff8dc5 100644
--- a/server/lib/activitypub/send/send-delete.ts
+++ b/server/lib/activitypub/send/send-delete.ts
@@ -1,17 +1,17 @@
1import { Transaction } from 'sequelize' 1import { Transaction } from 'sequelize'
2import { ActivityAudience, ActivityDelete } from '../../../../shared/models/activitypub' 2import { ActivityAudience, ActivityDelete } from '../../../../shared/models/activitypub'
3import { ActorModel } from '../../../models/activitypub/actor' 3import { ActorModel } from '../../../models/activitypub/actor'
4import { VideoModel } from '../../../models/video/video'
5import { VideoCommentModel } from '../../../models/video/video-comment' 4import { VideoCommentModel } from '../../../models/video/video-comment'
6import { VideoShareModel } from '../../../models/video/video-share' 5import { VideoShareModel } from '../../../models/video/video-share'
7import { getDeleteActivityPubUrl } from '../url' 6import { getDeleteActivityPubUrl } from '../url'
8import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' 7import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils'
9import { audiencify, getActorsInvolvedInVideo, getVideoCommentAudience } from '../audience' 8import { audiencify, getActorsInvolvedInVideo, getVideoCommentAudience } from '../audience'
10import { logger } from '../../../helpers/logger' 9import { logger } from '../../../helpers/logger'
11import { VideoPlaylistModel } from '../../../models/video/video-playlist'
12import { getServerActor } from '../../../helpers/utils' 10import { getServerActor } from '../../../helpers/utils'
11import { MCommentOwnerVideoReply, MVideoAccountLight, MVideoPlaylistFullSummary } from '../../../typings/models/video'
12import { MActorUrl } from '../../../typings/models'
13 13
14async function sendDeleteVideo (video: VideoModel, 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)
16 16
17 const byActor = video.VideoChannel.Account.Actor 17 const byActor = video.VideoChannel.Account.Actor
@@ -42,7 +42,7 @@ async function sendDeleteActor (byActor: ActorModel, t: Transaction) {
42 return broadcastToFollowers(activity, byActor, actorsInvolved, t) 42 return broadcastToFollowers(activity, byActor, actorsInvolved, t)
43} 43}
44 44
45async function sendDeleteVideoComment (videoComment: VideoCommentModel, t: Transaction) { 45async function sendDeleteVideoComment (videoComment: MCommentOwnerVideoReply, t: Transaction) {
46 logger.info('Creating job to send delete of comment %s.', videoComment.url) 46 logger.info('Creating job to send delete of comment %s.', videoComment.url)
47 47
48 const isVideoOrigin = videoComment.Video.isOwned() 48 const isVideoOrigin = videoComment.Video.isOwned()
@@ -74,7 +74,7 @@ async function sendDeleteVideoComment (videoComment: VideoCommentModel, t: Trans
74 t.afterCommit(() => unicastTo(activity, byActor, videoComment.Video.VideoChannel.Account.Actor.sharedInboxUrl)) 74 t.afterCommit(() => unicastTo(activity, byActor, videoComment.Video.VideoChannel.Account.Actor.sharedInboxUrl))
75} 75}
76 76
77async function sendDeleteVideoPlaylist (videoPlaylist: VideoPlaylistModel, t: Transaction) { 77async function sendDeleteVideoPlaylist (videoPlaylist: MVideoPlaylistFullSummary, t: Transaction) {
78 logger.info('Creating job to send delete of playlist %s.', videoPlaylist.url) 78 logger.info('Creating job to send delete of playlist %s.', videoPlaylist.url)
79 79
80 const byActor = videoPlaylist.OwnerAccount.Actor 80 const byActor = videoPlaylist.OwnerAccount.Actor
@@ -101,7 +101,7 @@ export {
101 101
102// --------------------------------------------------------------------------- 102// ---------------------------------------------------------------------------
103 103
104function buildDeleteActivity (url: string, object: string, byActor: ActorModel, audience?: ActivityAudience): ActivityDelete { 104function buildDeleteActivity (url: string, object: string, byActor: MActorUrl, audience?: ActivityAudience): ActivityDelete {
105 const activity = { 105 const activity = {
106 type: 'Delete' as 'Delete', 106 type: 'Delete' as 'Delete',
107 id: url, 107 id: url,
diff --git a/server/lib/activitypub/send/send-dislike.ts b/server/lib/activitypub/send/send-dislike.ts
index a88436f2c..6e41f241f 100644
--- a/server/lib/activitypub/send/send-dislike.ts
+++ b/server/lib/activitypub/send/send-dislike.ts
@@ -1,13 +1,12 @@
1import { Transaction } from 'sequelize' 1import { Transaction } from 'sequelize'
2import { ActorModel } from '../../../models/activitypub/actor'
3import { VideoModel } from '../../../models/video/video'
4import { getVideoDislikeActivityPubUrl } from '../url' 2import { getVideoDislikeActivityPubUrl } from '../url'
5import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
6import { ActivityAudience, ActivityDislike } from '../../../../shared/models/activitypub' 4import { ActivityAudience, ActivityDislike } from '../../../../shared/models/activitypub'
7import { sendVideoRelatedActivity } from './utils' 5import { sendVideoRelatedActivity } from './utils'
8import { audiencify, getAudience } from '../audience' 6import { audiencify, getAudience } from '../audience'
7import { MActor, MActorAudience, MVideoAccountLight, MVideoUrl } from '../../../typings/models'
9 8
10async function sendDislike (byActor: ActorModel, video: VideoModel, t: Transaction) { 9async function sendDislike (byActor: MActor, video: MVideoAccountLight, t: Transaction) {
11 logger.info('Creating job to dislike %s.', video.url) 10 logger.info('Creating job to dislike %s.', video.url)
12 11
13 const activityBuilder = (audience: ActivityAudience) => { 12 const activityBuilder = (audience: ActivityAudience) => {
@@ -19,7 +18,7 @@ async function sendDislike (byActor: ActorModel, video: VideoModel, t: Transacti
19 return sendVideoRelatedActivity(activityBuilder, { byActor, video, transaction: t }) 18 return sendVideoRelatedActivity(activityBuilder, { byActor, video, transaction: t })
20} 19}
21 20
22function buildDislikeActivity (url: string, byActor: ActorModel, video: VideoModel, audience?: ActivityAudience): ActivityDislike { 21function buildDislikeActivity (url: string, byActor: MActorAudience, video: MVideoUrl, audience?: ActivityAudience): ActivityDislike {
23 if (!audience) audience = getAudience(byActor) 22 if (!audience) audience = getAudience(byActor)
24 23
25 return audiencify( 24 return audiencify(
diff --git a/server/lib/activitypub/send/send-flag.ts b/server/lib/activitypub/send/send-flag.ts
index 61ee389a6..5ae1614ab 100644
--- a/server/lib/activitypub/send/send-flag.ts
+++ b/server/lib/activitypub/send/send-flag.ts
@@ -1,14 +1,13 @@
1import { ActorModel } from '../../../models/activitypub/actor'
2import { VideoModel } from '../../../models/video/video'
3import { VideoAbuseModel } from '../../../models/video/video-abuse'
4import { getVideoAbuseActivityPubUrl } from '../url' 1import { getVideoAbuseActivityPubUrl } from '../url'
5import { unicastTo } from './utils' 2import { unicastTo } from './utils'
6import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
7import { ActivityAudience, ActivityFlag } from '../../../../shared/models/activitypub' 4import { ActivityAudience, ActivityFlag } from '../../../../shared/models/activitypub'
8import { audiencify, getAudience } from '../audience' 5import { audiencify, getAudience } from '../audience'
9import { Transaction } from 'sequelize' 6import { Transaction } from 'sequelize'
7import { MActor, MVideoFullLight } from '../../../typings/models'
8import { MVideoAbuseVideo } from '../../../typings/models/video'
10 9
11async function sendVideoAbuse (byActor: ActorModel, videoAbuse: VideoAbuseModel, video: VideoModel, t: Transaction) { 10async function sendVideoAbuse (byActor: MActor, videoAbuse: MVideoAbuseVideo, video: MVideoFullLight, t: Transaction) {
12 if (!video.VideoChannel.Account.Actor.serverId) return // Local user 11 if (!video.VideoChannel.Account.Actor.serverId) return // Local user
13 12
14 const url = getVideoAbuseActivityPubUrl(videoAbuse) 13 const url = getVideoAbuseActivityPubUrl(videoAbuse)
@@ -22,7 +21,7 @@ async function sendVideoAbuse (byActor: ActorModel, videoAbuse: VideoAbuseModel,
22 t.afterCommit(() => unicastTo(flagActivity, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl)) 21 t.afterCommit(() => unicastTo(flagActivity, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl))
23} 22}
24 23
25function buildFlagActivity (url: string, byActor: ActorModel, videoAbuse: VideoAbuseModel, audience: ActivityAudience): ActivityFlag { 24function buildFlagActivity (url: string, byActor: MActor, videoAbuse: MVideoAbuseVideo, audience: ActivityAudience): ActivityFlag {
26 if (!audience) audience = getAudience(byActor) 25 if (!audience) audience = getAudience(byActor)
27 26
28 const activity = Object.assign( 27 const activity = Object.assign(
diff --git a/server/lib/activitypub/send/send-follow.ts b/server/lib/activitypub/send/send-follow.ts
index a59ed50cf..ce400d8ff 100644
--- a/server/lib/activitypub/send/send-follow.ts
+++ b/server/lib/activitypub/send/send-follow.ts
@@ -1,12 +1,11 @@
1import { ActivityFollow } from '../../../../shared/models/activitypub' 1import { ActivityFollow } from '../../../../shared/models/activitypub'
2import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
3import { getActorFollowActivityPubUrl } from '../url' 2import { getActorFollowActivityPubUrl } from '../url'
4import { unicastTo } from './utils' 3import { unicastTo } from './utils'
5import { logger } from '../../../helpers/logger' 4import { logger } from '../../../helpers/logger'
6import { Transaction } from 'sequelize' 5import { Transaction } from 'sequelize'
7import { ActorModelOnly } from '../../../typings/models' 6import { MActor, MActorFollowActors } from '../../../typings/models'
8 7
9function sendFollow (actorFollow: ActorFollowModel, t: Transaction) { 8function sendFollow (actorFollow: MActorFollowActors, t: Transaction) {
10 const me = actorFollow.ActorFollower 9 const me = actorFollow.ActorFollower
11 const following = actorFollow.ActorFollowing 10 const following = actorFollow.ActorFollowing
12 11
@@ -21,7 +20,7 @@ function sendFollow (actorFollow: ActorFollowModel, t: Transaction) {
21 t.afterCommit(() => unicastTo(data, me, following.inboxUrl)) 20 t.afterCommit(() => unicastTo(data, me, following.inboxUrl))
22} 21}
23 22
24function buildFollowActivity (url: string, byActor: ActorModelOnly, targetActor: ActorModelOnly): ActivityFollow { 23function buildFollowActivity (url: string, byActor: MActor, targetActor: MActor): ActivityFollow {
25 return { 24 return {
26 type: 'Follow', 25 type: 'Follow',
27 id: url, 26 id: url,
diff --git a/server/lib/activitypub/send/send-like.ts b/server/lib/activitypub/send/send-like.ts
index 35227887a..e84a6f98b 100644
--- a/server/lib/activitypub/send/send-like.ts
+++ b/server/lib/activitypub/send/send-like.ts
@@ -1,13 +1,12 @@
1import { Transaction } from 'sequelize' 1import { Transaction } from 'sequelize'
2import { ActivityAudience, ActivityLike } from '../../../../shared/models/activitypub' 2import { ActivityAudience, ActivityLike } from '../../../../shared/models/activitypub'
3import { ActorModel } from '../../../models/activitypub/actor'
4import { VideoModel } from '../../../models/video/video'
5import { getVideoLikeActivityPubUrl } from '../url' 3import { getVideoLikeActivityPubUrl } from '../url'
6import { sendVideoRelatedActivity } from './utils' 4import { sendVideoRelatedActivity } from './utils'
7import { audiencify, getAudience } from '../audience' 5import { audiencify, getAudience } from '../audience'
8import { logger } from '../../../helpers/logger' 6import { logger } from '../../../helpers/logger'
7import { MActor, MActorAudience, MVideoAccountLight, MVideoUrl } from '../../../typings/models'
9 8
10async function sendLike (byActor: ActorModel, video: VideoModel, t: Transaction) { 9async function sendLike (byActor: MActor, video: MVideoAccountLight, t: Transaction) {
11 logger.info('Creating job to like %s.', video.url) 10 logger.info('Creating job to like %s.', video.url)
12 11
13 const activityBuilder = (audience: ActivityAudience) => { 12 const activityBuilder = (audience: ActivityAudience) => {
@@ -19,7 +18,7 @@ async function sendLike (byActor: ActorModel, video: VideoModel, t: Transaction)
19 return sendVideoRelatedActivity(activityBuilder, { byActor, video, transaction: t }) 18 return sendVideoRelatedActivity(activityBuilder, { byActor, video, transaction: t })
20} 19}
21 20
22function buildLikeActivity (url: string, byActor: ActorModel, video: VideoModel, audience?: ActivityAudience): ActivityLike { 21function buildLikeActivity (url: string, byActor: MActorAudience, video: MVideoUrl, audience?: ActivityAudience): ActivityLike {
23 if (!audience) audience = getAudience(byActor) 22 if (!audience) audience = getAudience(byActor)
24 23
25 return audiencify( 24 return audiencify(
diff --git a/server/lib/activitypub/send/send-reject.ts b/server/lib/activitypub/send/send-reject.ts
index 63110b433..4258a3c36 100644
--- a/server/lib/activitypub/send/send-reject.ts
+++ b/server/lib/activitypub/send/send-reject.ts
@@ -1,12 +1,11 @@
1import { ActivityFollow, ActivityReject } from '../../../../shared/models/activitypub' 1import { ActivityFollow, ActivityReject } from '../../../../shared/models/activitypub'
2import { ActorModel } from '../../../models/activitypub/actor'
3import { getActorFollowActivityPubUrl, getActorFollowRejectActivityPubUrl } from '../url' 2import { getActorFollowActivityPubUrl, getActorFollowRejectActivityPubUrl } from '../url'
4import { unicastTo } from './utils' 3import { unicastTo } from './utils'
5import { buildFollowActivity } from './send-follow' 4import { buildFollowActivity } from './send-follow'
6import { logger } from '../../../helpers/logger' 5import { logger } from '../../../helpers/logger'
7import { SignatureActorModel } from '../../../typings/models' 6import { MActor } from '../../../typings/models'
8 7
9async function sendReject (follower: SignatureActorModel, following: ActorModel) { 8async function sendReject (follower: MActor, following: MActor) {
10 if (!follower.serverId) { // This should never happen 9 if (!follower.serverId) { // This should never happen
11 logger.warn('Do not sending reject to local follower.') 10 logger.warn('Do not sending reject to local follower.')
12 return 11 return
@@ -31,7 +30,7 @@ export {
31 30
32// --------------------------------------------------------------------------- 31// ---------------------------------------------------------------------------
33 32
34function buildRejectActivity (url: string, byActor: ActorModel, followActivityData: ActivityFollow): ActivityReject { 33function buildRejectActivity (url: string, byActor: MActor, followActivityData: ActivityFollow): ActivityReject {
35 return { 34 return {
36 type: 'Reject', 35 type: 'Reject',
37 id: url, 36 id: url,
diff --git a/server/lib/activitypub/send/send-undo.ts b/server/lib/activitypub/send/send-undo.ts
index 8fcbbac5c..e9ab5b3c5 100644
--- a/server/lib/activitypub/send/send-undo.ts
+++ b/server/lib/activitypub/send/send-undo.ts
@@ -2,13 +2,12 @@ import { Transaction } from 'sequelize'
2import { 2import {
3 ActivityAnnounce, 3 ActivityAnnounce,
4 ActivityAudience, 4 ActivityAudience,
5 ActivityCreate, ActivityDislike, 5 ActivityCreate,
6 ActivityDislike,
6 ActivityFollow, 7 ActivityFollow,
7 ActivityLike, 8 ActivityLike,
8 ActivityUndo 9 ActivityUndo
9} from '../../../../shared/models/activitypub' 10} from '../../../../shared/models/activitypub'
10import { ActorModel } from '../../../models/activitypub/actor'
11import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
12import { VideoModel } from '../../../models/video/video' 11import { VideoModel } from '../../../models/video/video'
13import { getActorFollowActivityPubUrl, getUndoActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from '../url' 12import { getActorFollowActivityPubUrl, getUndoActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from '../url'
14import { broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' 13import { broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils'
@@ -16,13 +15,20 @@ import { audiencify, getAudience } from '../audience'
16import { buildCreateActivity } from './send-create' 15import { buildCreateActivity } from './send-create'
17import { buildFollowActivity } from './send-follow' 16import { buildFollowActivity } from './send-follow'
18import { buildLikeActivity } from './send-like' 17import { buildLikeActivity } from './send-like'
19import { VideoShareModel } from '../../../models/video/video-share'
20import { buildAnnounceWithVideoAudience } from './send-announce' 18import { buildAnnounceWithVideoAudience } from './send-announce'
21import { logger } from '../../../helpers/logger' 19import { logger } from '../../../helpers/logger'
22import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy'
23import { buildDislikeActivity } from './send-dislike' 20import { buildDislikeActivity } from './send-dislike'
24 21import {
25async function sendUndoFollow (actorFollow: ActorFollowModel, t: Transaction) { 22 MActor, MActorAudience,
23 MActorFollowActors,
24 MActorLight,
25 MVideo,
26 MVideoAccountLight,
27 MVideoRedundancyVideo,
28 MVideoShare
29} from '../../../typings/models'
30
31async function sendUndoFollow (actorFollow: MActorFollowActors, t: Transaction) {
26 const me = actorFollow.ActorFollower 32 const me = actorFollow.ActorFollower
27 const following = actorFollow.ActorFollowing 33 const following = actorFollow.ActorFollowing
28 34
@@ -40,7 +46,7 @@ async function sendUndoFollow (actorFollow: ActorFollowModel, t: Transaction) {
40 t.afterCommit(() => unicastTo(undoActivity, me, following.inboxUrl)) 46 t.afterCommit(() => unicastTo(undoActivity, me, following.inboxUrl))
41} 47}
42 48
43async function sendUndoAnnounce (byActor: ActorModel, videoShare: VideoShareModel, video: VideoModel, t: Transaction) { 49async function sendUndoAnnounce (byActor: MActorLight, videoShare: MVideoShare, video: MVideo, t: Transaction) {
44 logger.info('Creating job to undo announce %s.', videoShare.url) 50 logger.info('Creating job to undo announce %s.', videoShare.url)
45 51
46 const undoUrl = getUndoActivityPubUrl(videoShare.url) 52 const undoUrl = getUndoActivityPubUrl(videoShare.url)
@@ -52,7 +58,7 @@ async function sendUndoAnnounce (byActor: ActorModel, videoShare: VideoShareMode
52 return broadcastToFollowers(undoActivity, byActor, actorsInvolvedInVideo, t, followersException) 58 return broadcastToFollowers(undoActivity, byActor, actorsInvolvedInVideo, t, followersException)
53} 59}
54 60
55async function sendUndoLike (byActor: ActorModel, video: VideoModel, t: Transaction) { 61async function sendUndoLike (byActor: MActor, video: MVideoAccountLight, t: Transaction) {
56 logger.info('Creating job to undo a like of video %s.', video.url) 62 logger.info('Creating job to undo a like of video %s.', video.url)
57 63
58 const likeUrl = getVideoLikeActivityPubUrl(byActor, video) 64 const likeUrl = getVideoLikeActivityPubUrl(byActor, video)
@@ -61,7 +67,7 @@ async function sendUndoLike (byActor: ActorModel, video: VideoModel, t: Transact
61 return sendUndoVideoRelatedActivity({ byActor, video, url: likeUrl, activity: likeActivity, transaction: t }) 67 return sendUndoVideoRelatedActivity({ byActor, video, url: likeUrl, activity: likeActivity, transaction: t })
62} 68}
63 69
64async function sendUndoDislike (byActor: ActorModel, video: VideoModel, t: Transaction) { 70async function sendUndoDislike (byActor: MActor, video: MVideoAccountLight, t: Transaction) {
65 logger.info('Creating job to undo a dislike of video %s.', video.url) 71 logger.info('Creating job to undo a dislike of video %s.', video.url)
66 72
67 const dislikeUrl = getVideoDislikeActivityPubUrl(byActor, video) 73 const dislikeUrl = getVideoDislikeActivityPubUrl(byActor, video)
@@ -70,7 +76,7 @@ async function sendUndoDislike (byActor: ActorModel, video: VideoModel, t: Trans
70 return sendUndoVideoRelatedActivity({ byActor, video, url: dislikeUrl, activity: dislikeActivity, transaction: t }) 76 return sendUndoVideoRelatedActivity({ byActor, video, url: dislikeUrl, activity: dislikeActivity, transaction: t })
71} 77}
72 78
73async function sendUndoCacheFile (byActor: ActorModel, redundancyModel: VideoRedundancyModel, t: Transaction) { 79async function sendUndoCacheFile (byActor: MActor, redundancyModel: MVideoRedundancyVideo, t: Transaction) {
74 logger.info('Creating job to undo cache file %s.', redundancyModel.url) 80 logger.info('Creating job to undo cache file %s.', redundancyModel.url)
75 81
76 const videoId = redundancyModel.getVideo().id 82 const videoId = redundancyModel.getVideo().id
@@ -94,7 +100,7 @@ export {
94 100
95function undoActivityData ( 101function undoActivityData (
96 url: string, 102 url: string,
97 byActor: ActorModel, 103 byActor: MActorAudience,
98 object: ActivityFollow | ActivityLike | ActivityDislike | ActivityCreate | ActivityAnnounce, 104 object: ActivityFollow | ActivityLike | ActivityDislike | ActivityCreate | ActivityAnnounce,
99 audience?: ActivityAudience 105 audience?: ActivityAudience
100): ActivityUndo { 106): ActivityUndo {
@@ -112,8 +118,8 @@ function undoActivityData (
112} 118}
113 119
114async function sendUndoVideoRelatedActivity (options: { 120async function sendUndoVideoRelatedActivity (options: {
115 byActor: ActorModel, 121 byActor: MActor,
116 video: VideoModel, 122 video: MVideoAccountLight,
117 url: string, 123 url: string,
118 activity: ActivityFollow | ActivityLike | ActivityDislike | ActivityCreate | ActivityAnnounce, 124 activity: ActivityFollow | ActivityLike | ActivityDislike | ActivityCreate | ActivityAnnounce,
119 transaction: Transaction 125 transaction: Transaction
diff --git a/server/lib/activitypub/send/send-update.ts b/server/lib/activitypub/send/send-update.ts
index 5bf092894..37517c2be 100644
--- a/server/lib/activitypub/send/send-update.ts
+++ b/server/lib/activitypub/send/send-update.ts
@@ -2,21 +2,29 @@ import { Transaction } from 'sequelize'
2import { ActivityAudience, ActivityUpdate } from '../../../../shared/models/activitypub' 2import { ActivityAudience, ActivityUpdate } from '../../../../shared/models/activitypub'
3import { VideoPrivacy } from '../../../../shared/models/videos' 3import { VideoPrivacy } from '../../../../shared/models/videos'
4import { AccountModel } from '../../../models/account/account' 4import { AccountModel } from '../../../models/account/account'
5import { ActorModel } from '../../../models/activitypub/actor'
6import { VideoModel } from '../../../models/video/video' 5import { VideoModel } from '../../../models/video/video'
7import { VideoChannelModel } from '../../../models/video/video-channel'
8import { VideoShareModel } from '../../../models/video/video-share' 6import { VideoShareModel } from '../../../models/video/video-share'
9import { getUpdateActivityPubUrl } from '../url' 7import { getUpdateActivityPubUrl } from '../url'
10import { broadcastToFollowers, sendVideoRelatedActivity } from './utils' 8import { broadcastToFollowers, sendVideoRelatedActivity } from './utils'
11import { audiencify, getActorsInvolvedInVideo, getAudience } from '../audience' 9import { audiencify, getActorsInvolvedInVideo, getAudience } from '../audience'
12import { logger } from '../../../helpers/logger' 10import { logger } from '../../../helpers/logger'
13import { VideoCaptionModel } from '../../../models/video/video-caption' 11import { VideoCaptionModel } from '../../../models/video/video-caption'
14import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy'
15import { VideoPlaylistModel } from '../../../models/video/video-playlist'
16import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' 12import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
17import { getServerActor } from '../../../helpers/utils' 13import { getServerActor } from '../../../helpers/utils'
14import {
15 MAccountDefault,
16 MActor,
17 MActorLight,
18 MChannelDefault,
19 MVideoAP,
20 MVideoAPWithoutCaption,
21 MVideoPlaylistFull,
22 MVideoRedundancyVideo
23} from '../../../typings/models'
24
25async function sendUpdateVideo (videoArg: MVideoAPWithoutCaption, t: Transaction, overrodeByActor?: MActor) {
26 const video = videoArg as MVideoAP
18 27
19async function sendUpdateVideo (video: VideoModel, t: Transaction, overrodeByActor?: ActorModel) {
20 if (video.privacy === VideoPrivacy.PRIVATE) return undefined 28 if (video.privacy === VideoPrivacy.PRIVATE) return undefined
21 29
22 logger.info('Creating job to update video %s.', video.url) 30 logger.info('Creating job to update video %s.', video.url)
@@ -41,7 +49,7 @@ async function sendUpdateVideo (video: VideoModel, t: Transaction, overrodeByAct
41 return broadcastToFollowers(updateActivity, byActor, actorsInvolved, t) 49 return broadcastToFollowers(updateActivity, byActor, actorsInvolved, t)
42} 50}
43 51
44async function sendUpdateActor (accountOrChannel: AccountModel | VideoChannelModel, t: Transaction) { 52async function sendUpdateActor (accountOrChannel: MChannelDefault | MAccountDefault, t: Transaction) {
45 const byActor = accountOrChannel.Actor 53 const byActor = accountOrChannel.Actor
46 54
47 logger.info('Creating job to update actor %s.', byActor.url) 55 logger.info('Creating job to update actor %s.', byActor.url)
@@ -51,7 +59,7 @@ async function sendUpdateActor (accountOrChannel: AccountModel | VideoChannelMod
51 const audience = getAudience(byActor) 59 const audience = getAudience(byActor)
52 const updateActivity = buildUpdateActivity(url, byActor, accountOrChannelObject, audience) 60 const updateActivity = buildUpdateActivity(url, byActor, accountOrChannelObject, audience)
53 61
54 let actorsInvolved: ActorModel[] 62 let actorsInvolved: MActor[]
55 if (accountOrChannel instanceof AccountModel) { 63 if (accountOrChannel instanceof AccountModel) {
56 // Actors that shared my videos are involved too 64 // Actors that shared my videos are involved too
57 actorsInvolved = await VideoShareModel.loadActorsWhoSharedVideosOf(byActor.id, t) 65 actorsInvolved = await VideoShareModel.loadActorsWhoSharedVideosOf(byActor.id, t)
@@ -65,7 +73,7 @@ async function sendUpdateActor (accountOrChannel: AccountModel | VideoChannelMod
65 return broadcastToFollowers(updateActivity, byActor, actorsInvolved, t) 73 return broadcastToFollowers(updateActivity, byActor, actorsInvolved, t)
66} 74}
67 75
68async function sendUpdateCacheFile (byActor: ActorModel, redundancyModel: VideoRedundancyModel) { 76async function sendUpdateCacheFile (byActor: MActorLight, redundancyModel: MVideoRedundancyVideo) {
69 logger.info('Creating job to update cache file %s.', redundancyModel.url) 77 logger.info('Creating job to update cache file %s.', redundancyModel.url)
70 78
71 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(redundancyModel.getVideo().id) 79 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(redundancyModel.getVideo().id)
@@ -80,7 +88,7 @@ async function sendUpdateCacheFile (byActor: ActorModel, redundancyModel: VideoR
80 return sendVideoRelatedActivity(activityBuilder, { byActor, video }) 88 return sendVideoRelatedActivity(activityBuilder, { byActor, video })
81} 89}
82 90
83async function sendUpdateVideoPlaylist (videoPlaylist: VideoPlaylistModel, t: Transaction) { 91async function sendUpdateVideoPlaylist (videoPlaylist: MVideoPlaylistFull, t: Transaction) {
84 if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) return undefined 92 if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) return undefined
85 93
86 const byActor = videoPlaylist.OwnerAccount.Actor 94 const byActor = videoPlaylist.OwnerAccount.Actor
@@ -113,7 +121,7 @@ export {
113 121
114// --------------------------------------------------------------------------- 122// ---------------------------------------------------------------------------
115 123
116function buildUpdateActivity (url: string, byActor: ActorModel, object: any, audience?: ActivityAudience): ActivityUpdate { 124function buildUpdateActivity (url: string, byActor: MActorLight, object: any, audience?: ActivityAudience): ActivityUpdate {
117 if (!audience) audience = getAudience(byActor) 125 if (!audience) audience = getAudience(byActor)
118 126
119 return audiencify( 127 return audiencify(
@@ -121,8 +129,7 @@ function buildUpdateActivity (url: string, byActor: ActorModel, object: any, aud
121 type: 'Update' as 'Update', 129 type: 'Update' as 'Update',
122 id: url, 130 id: url,
123 actor: byActor.url, 131 actor: byActor.url,
124 object: audiencify(object, audience 132 object: audiencify(object, audience)
125 )
126 }, 133 },
127 audience 134 audience
128 ) 135 )
diff --git a/server/lib/activitypub/send/send-view.ts b/server/lib/activitypub/send/send-view.ts
index 8ad126be0..8809417f9 100644
--- a/server/lib/activitypub/send/send-view.ts
+++ b/server/lib/activitypub/send/send-view.ts
@@ -1,13 +1,13 @@
1import { Transaction } from 'sequelize' 1import { Transaction } from 'sequelize'
2import { ActivityAudience, ActivityView } from '../../../../shared/models/activitypub' 2import { ActivityAudience, ActivityView } from '../../../../shared/models/activitypub'
3import { ActorModel } from '../../../models/activitypub/actor' 3import { ActorModel } from '../../../models/activitypub/actor'
4import { VideoModel } from '../../../models/video/video'
5import { getVideoLikeActivityPubUrl } from '../url' 4import { getVideoLikeActivityPubUrl } from '../url'
6import { sendVideoRelatedActivity } from './utils' 5import { sendVideoRelatedActivity } from './utils'
7import { audiencify, getAudience } from '../audience' 6import { audiencify, getAudience } from '../audience'
8import { logger } from '../../../helpers/logger' 7import { logger } from '../../../helpers/logger'
8import { MActorAudience, MVideoAccountLight, MVideoUrl } from '@server/typings/models'
9 9
10async function sendView (byActor: ActorModel, video: VideoModel, t: Transaction) { 10async function sendView (byActor: ActorModel, video: MVideoAccountLight, 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) => {
@@ -19,7 +19,7 @@ async function sendView (byActor: ActorModel, video: VideoModel, t: Transaction)
19 return sendVideoRelatedActivity(activityBuilder, { byActor, video, transaction: t }) 19 return sendVideoRelatedActivity(activityBuilder, { byActor, video, transaction: t })
20} 20}
21 21
22function buildViewActivity (url: string, byActor: ActorModel, video: VideoModel, audience?: ActivityAudience): ActivityView { 22function buildViewActivity (url: string, byActor: MActorAudience, video: MVideoUrl, audience?: ActivityAudience): ActivityView {
23 if (!audience) audience = getAudience(byActor) 23 if (!audience) audience = getAudience(byActor)
24 24
25 return audiencify( 25 return audiencify(
diff --git a/server/lib/activitypub/send/utils.ts b/server/lib/activitypub/send/utils.ts
index 4f69afb00..8129ab32a 100644
--- a/server/lib/activitypub/send/utils.ts
+++ b/server/lib/activitypub/send/utils.ts
@@ -4,15 +4,14 @@ import { logger } from '../../../helpers/logger'
4import { ActorModel } from '../../../models/activitypub/actor' 4import { 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 { VideoModel } from '../../../models/video/video'
8import { getActorsInvolvedInVideo, getAudienceFromFollowersOf, getRemoteVideoAudience } from '../audience' 7import { getActorsInvolvedInVideo, getAudienceFromFollowersOf, getRemoteVideoAudience } from '../audience'
9import { getServerActor } from '../../../helpers/utils' 8import { getServerActor } from '../../../helpers/utils'
10import { afterCommitIfTransaction } from '../../../helpers/database-utils' 9import { afterCommitIfTransaction } from '../../../helpers/database-utils'
11import { ActorFollowerException, ActorModelId, ActorModelOnly } from '../../../typings/models' 10import { MActorFollowerException, MActor, MActorId, MActorLight, MVideo, MVideoAccountLight } from '../../../typings/models'
12 11
13async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAudience) => Activity, options: { 12async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAudience) => Activity, options: {
14 byActor: ActorModelOnly, 13 byActor: MActorLight,
15 video: VideoModel, 14 video: MVideoAccountLight,
16 transaction?: Transaction 15 transaction?: Transaction
17}) { 16}) {
18 const { byActor, video, transaction } = options 17 const { byActor, video, transaction } = options
@@ -41,8 +40,8 @@ async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAud
41async function forwardVideoRelatedActivity ( 40async function forwardVideoRelatedActivity (
42 activity: Activity, 41 activity: Activity,
43 t: Transaction, 42 t: Transaction,
44 followersException: ActorFollowerException[] = [], 43 followersException: MActorFollowerException[] = [],
45 video: VideoModel 44 video: MVideo
46) { 45) {
47 // Mastodon does not add our announces in audience, so we forward to them manually 46 // Mastodon does not add our announces in audience, so we forward to them manually
48 const additionalActors = await getActorsInvolvedInVideo(video, t) 47 const additionalActors = await getActorsInvolvedInVideo(video, t)
@@ -54,7 +53,7 @@ async function forwardVideoRelatedActivity (
54async function forwardActivity ( 53async function forwardActivity (
55 activity: Activity, 54 activity: Activity,
56 t: Transaction, 55 t: Transaction,
57 followersException: ActorFollowerException[] = [], 56 followersException: MActorFollowerException[] = [],
58 additionalFollowerUrls: string[] = [] 57 additionalFollowerUrls: string[] = []
59) { 58) {
60 logger.info('Forwarding activity %s.', activity.id) 59 logger.info('Forwarding activity %s.', activity.id)
@@ -88,10 +87,10 @@ async function forwardActivity (
88 87
89async function broadcastToFollowers ( 88async function broadcastToFollowers (
90 data: any, 89 data: any,
91 byActor: ActorModelId, 90 byActor: MActorId,
92 toFollowersOf: ActorModelId[], 91 toFollowersOf: MActorId[],
93 t: Transaction, 92 t: Transaction,
94 actorsException: ActorFollowerException[] = [] 93 actorsException: MActorFollowerException[] = []
95) { 94) {
96 const uris = await computeFollowerUris(toFollowersOf, actorsException, t) 95 const uris = await computeFollowerUris(toFollowersOf, actorsException, t)
97 96
@@ -100,16 +99,16 @@ async function broadcastToFollowers (
100 99
101async function broadcastToActors ( 100async function broadcastToActors (
102 data: any, 101 data: any,
103 byActor: ActorModelId, 102 byActor: MActorId,
104 toActors: ActorModelOnly[], 103 toActors: MActor[],
105 t?: Transaction, 104 t?: Transaction,
106 actorsException: ActorFollowerException[] = [] 105 actorsException: MActorFollowerException[] = []
107) { 106) {
108 const uris = await computeUris(toActors, actorsException) 107 const uris = await computeUris(toActors, actorsException)
109 return afterCommitIfTransaction(t, () => broadcastTo(uris, data, byActor)) 108 return afterCommitIfTransaction(t, () => broadcastTo(uris, data, byActor))
110} 109}
111 110
112function broadcastTo (uris: string[], data: any, byActor: ActorModelId) { 111function broadcastTo (uris: string[], data: any, byActor: MActorId) {
113 if (uris.length === 0) return undefined 112 if (uris.length === 0) return undefined
114 113
115 logger.debug('Creating broadcast job.', { uris }) 114 logger.debug('Creating broadcast job.', { uris })
@@ -123,7 +122,7 @@ function broadcastTo (uris: string[], data: any, byActor: ActorModelId) {
123 return JobQueue.Instance.createJob({ type: 'activitypub-http-broadcast', payload }) 122 return JobQueue.Instance.createJob({ type: 'activitypub-http-broadcast', payload })
124} 123}
125 124
126function unicastTo (data: any, byActor: ActorModelId, toActorUrl: string) { 125function unicastTo (data: any, byActor: MActorId, toActorUrl: string) {
127 logger.debug('Creating unicast job.', { uri: toActorUrl }) 126 logger.debug('Creating unicast job.', { uri: toActorUrl })
128 127
129 const payload = { 128 const payload = {
@@ -148,7 +147,7 @@ export {
148 147
149// --------------------------------------------------------------------------- 148// ---------------------------------------------------------------------------
150 149
151async function computeFollowerUris (toFollowersOf: ActorModelId[], actorsException: ActorFollowerException[], t: Transaction) { 150async function computeFollowerUris (toFollowersOf: MActorId[], actorsException: MActorFollowerException[], t: Transaction) {
152 const toActorFollowerIds = toFollowersOf.map(a => a.id) 151 const toActorFollowerIds = toFollowersOf.map(a => a.id)
153 152
154 const result = await ActorFollowModel.listAcceptedFollowerSharedInboxUrls(toActorFollowerIds, t) 153 const result = await ActorFollowModel.listAcceptedFollowerSharedInboxUrls(toActorFollowerIds, t)
@@ -157,7 +156,7 @@ async function computeFollowerUris (toFollowersOf: ActorModelId[], actorsExcepti
157 return result.data.filter(sharedInbox => sharedInboxesException.indexOf(sharedInbox) === -1) 156 return result.data.filter(sharedInbox => sharedInboxesException.indexOf(sharedInbox) === -1)
158} 157}
159 158
160async function computeUris (toActors: ActorModelOnly[], actorsException: ActorFollowerException[] = []) { 159async function computeUris (toActors: MActor[], actorsException: MActorFollowerException[] = []) {
161 const serverActor = await getServerActor() 160 const serverActor = await getServerActor()
162 const targetUrls = toActors 161 const targetUrls = toActors
163 .filter(a => a.id !== serverActor.id) // Don't send to ourselves 162 .filter(a => a.id !== serverActor.id) // Don't send to ourselves
@@ -170,7 +169,7 @@ async function computeUris (toActors: ActorModelOnly[], actorsException: ActorFo
170 .filter(sharedInbox => sharedInboxesException.indexOf(sharedInbox) === -1) 169 .filter(sharedInbox => sharedInboxesException.indexOf(sharedInbox) === -1)
171} 170}
172 171
173async function buildSharedInboxesException (actorsException: ActorFollowerException[]) { 172async function buildSharedInboxesException (actorsException: MActorFollowerException[]) {
174 const serverActor = await getServerActor() 173 const serverActor = await getServerActor()
175 174
176 return actorsException 175 return actorsException
diff --git a/server/lib/activitypub/share.ts b/server/lib/activitypub/share.ts
index 7f38402b6..fdca9bed7 100644
--- a/server/lib/activitypub/share.ts
+++ b/server/lib/activitypub/share.ts
@@ -1,19 +1,18 @@
1import { Transaction } from 'sequelize' 1import { Transaction } from 'sequelize'
2import { VideoPrivacy } from '../../../shared/models/videos' 2import { VideoPrivacy } from '../../../shared/models/videos'
3import { getServerActor } from '../../helpers/utils' 3import { getServerActor } from '../../helpers/utils'
4import { VideoModel } from '../../models/video/video'
5import { VideoShareModel } from '../../models/video/video-share' 4import { VideoShareModel } from '../../models/video/video-share'
6import { sendUndoAnnounce, sendVideoAnnounce } from './send' 5import { sendUndoAnnounce, sendVideoAnnounce } from './send'
7import { getVideoAnnounceActivityPubUrl } from './url' 6import { getVideoAnnounceActivityPubUrl } from './url'
8import { VideoChannelModel } from '../../models/video/video-channel'
9import * as Bluebird from 'bluebird' 7import * as Bluebird from 'bluebird'
10import { doRequest } from '../../helpers/requests' 8import { doRequest } from '../../helpers/requests'
11import { getOrCreateActorAndServerAndModel } from './actor' 9import { getOrCreateActorAndServerAndModel } from './actor'
12import { logger } from '../../helpers/logger' 10import { logger } from '../../helpers/logger'
13import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' 11import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
14import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' 12import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
13import { MChannelActor, MChannelActorLight, MVideo, MVideoAccountLight, MVideoId } from '../../typings/models/video'
15 14
16async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction) { 15async function shareVideoByServerAndChannel (video: MVideoAccountLight, t: Transaction) {
17 if (video.privacy === VideoPrivacy.PRIVATE) return undefined 16 if (video.privacy === VideoPrivacy.PRIVATE) return undefined
18 17
19 return Promise.all([ 18 return Promise.all([
@@ -22,7 +21,11 @@ async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction)
22 ]) 21 ])
23} 22}
24 23
25async function changeVideoChannelShare (video: VideoModel, oldVideoChannel: VideoChannelModel, t: Transaction) { 24async function changeVideoChannelShare (
25 video: MVideoAccountLight,
26 oldVideoChannel: MChannelActorLight,
27 t: Transaction
28) {
26 logger.info('Updating video channel of video %s: %s -> %s.', video.uuid, oldVideoChannel.name, video.VideoChannel.name) 29 logger.info('Updating video channel of video %s: %s -> %s.', video.uuid, oldVideoChannel.name, video.VideoChannel.name)
27 30
28 await undoShareByVideoChannel(video, oldVideoChannel, t) 31 await undoShareByVideoChannel(video, oldVideoChannel, t)
@@ -30,7 +33,7 @@ async function changeVideoChannelShare (video: VideoModel, oldVideoChannel: Vide
30 await shareByVideoChannel(video, t) 33 await shareByVideoChannel(video, t)
31} 34}
32 35
33async function addVideoShares (shareUrls: string[], instance: VideoModel) { 36async function addVideoShares (shareUrls: string[], video: MVideoId) {
34 await Bluebird.map(shareUrls, async shareUrl => { 37 await Bluebird.map(shareUrls, async shareUrl => {
35 try { 38 try {
36 // Fetch url 39 // Fetch url
@@ -50,7 +53,7 @@ async function addVideoShares (shareUrls: string[], instance: VideoModel) {
50 53
51 const entry = { 54 const entry = {
52 actorId: actor.id, 55 actorId: actor.id,
53 videoId: instance.id, 56 videoId: video.id,
54 url: shareUrl 57 url: shareUrl
55 } 58 }
56 59
@@ -69,7 +72,7 @@ export {
69 72
70// --------------------------------------------------------------------------- 73// ---------------------------------------------------------------------------
71 74
72async function shareByServer (video: VideoModel, t: Transaction) { 75async function shareByServer (video: MVideo, t: Transaction) {
73 const serverActor = await getServerActor() 76 const serverActor = await getServerActor()
74 77
75 const serverShareUrl = getVideoAnnounceActivityPubUrl(serverActor, video) 78 const serverShareUrl = getVideoAnnounceActivityPubUrl(serverActor, video)
@@ -88,7 +91,7 @@ async function shareByServer (video: VideoModel, t: Transaction) {
88 return sendVideoAnnounce(serverActor, serverShare, video, t) 91 return sendVideoAnnounce(serverActor, serverShare, video, t)
89} 92}
90 93
91async function shareByVideoChannel (video: VideoModel, t: Transaction) { 94async function shareByVideoChannel (video: MVideoAccountLight, t: Transaction) {
92 const videoChannelShareUrl = getVideoAnnounceActivityPubUrl(video.VideoChannel.Actor, video) 95 const videoChannelShareUrl = getVideoAnnounceActivityPubUrl(video.VideoChannel.Actor, video)
93 const [ videoChannelShare ] = await VideoShareModel.findOrCreate({ 96 const [ videoChannelShare ] = await VideoShareModel.findOrCreate({
94 defaults: { 97 defaults: {
@@ -105,7 +108,7 @@ async function shareByVideoChannel (video: VideoModel, t: Transaction) {
105 return sendVideoAnnounce(video.VideoChannel.Actor, videoChannelShare, video, t) 108 return sendVideoAnnounce(video.VideoChannel.Actor, videoChannelShare, video, t)
106} 109}
107 110
108async function undoShareByVideoChannel (video: VideoModel, oldVideoChannel: VideoChannelModel, t: Transaction) { 111async function undoShareByVideoChannel (video: MVideo, oldVideoChannel: MChannelActorLight, t: Transaction) {
109 // Load old share 112 // Load old share
110 const oldShare = await VideoShareModel.load(oldVideoChannel.actorId, video.id, t) 113 const oldShare = await VideoShareModel.load(oldVideoChannel.actorId, video.id, t)
111 if (!oldShare) return new Error('Cannot find old video channel share ' + oldVideoChannel.actorId + ' for video ' + video.id) 114 if (!oldShare) return new Error('Cannot find old video channel share ' + oldVideoChannel.actorId + ' for video ' + video.id)
diff --git a/server/lib/activitypub/url.ts b/server/lib/activitypub/url.ts
index dfcb3c668..6290af34b 100644
--- a/server/lib/activitypub/url.ts
+++ b/server/lib/activitypub/url.ts
@@ -1,36 +1,42 @@
1import { WEBSERVER } from '../../initializers/constants' 1import { WEBSERVER } from '../../initializers/constants'
2import { VideoModel } from '../../models/video/video' 2import {
3import { VideoAbuseModel } from '../../models/video/video-abuse' 3 MActor,
4import { VideoCommentModel } from '../../models/video/video-comment' 4 MActorFollowActors,
5import { VideoFileModel } from '../../models/video/video-file' 5 MActorId,
6import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist' 6 MActorUrl,
7import { VideoPlaylistModel } from '../../models/video/video-playlist' 7 MCommentId,
8import { ActorModelOnly, ActorModelUrl } from '../../typings/models' 8 MVideoAbuseId,
9import { ActorFollowModelLight } from '../../typings/models/actor-follow' 9 MVideoId,
10 10 MVideoUrl,
11function getVideoActivityPubUrl (video: VideoModel) { 11 MVideoUUID
12} from '../../typings/models'
13import { MVideoPlaylist, MVideoPlaylistUUID } from '../../typings/models/video/video-playlist'
14import { MVideoFileVideoUUID } from '../../typings/models/video/video-file'
15import { MStreamingPlaylist } from '../../typings/models/video/video-streaming-playlist'
16
17function getVideoActivityPubUrl (video: MVideoUUID) {
12 return WEBSERVER.URL + '/videos/watch/' + video.uuid 18 return WEBSERVER.URL + '/videos/watch/' + video.uuid
13} 19}
14 20
15function getVideoPlaylistActivityPubUrl (videoPlaylist: VideoPlaylistModel) { 21function getVideoPlaylistActivityPubUrl (videoPlaylist: MVideoPlaylist) {
16 return WEBSERVER.URL + '/video-playlists/' + videoPlaylist.uuid 22 return WEBSERVER.URL + '/video-playlists/' + videoPlaylist.uuid
17} 23}
18 24
19function getVideoPlaylistElementActivityPubUrl (videoPlaylist: VideoPlaylistModel, video: VideoModel) { 25function getVideoPlaylistElementActivityPubUrl (videoPlaylist: MVideoPlaylistUUID, video: MVideoUUID) {
20 return WEBSERVER.URL + '/video-playlists/' + videoPlaylist.uuid + '/' + video.uuid 26 return WEBSERVER.URL + '/video-playlists/' + videoPlaylist.uuid + '/' + video.uuid
21} 27}
22 28
23function getVideoCacheFileActivityPubUrl (videoFile: VideoFileModel) { 29function getVideoCacheFileActivityPubUrl (videoFile: MVideoFileVideoUUID) {
24 const suffixFPS = videoFile.fps && videoFile.fps !== -1 ? '-' + videoFile.fps : '' 30 const suffixFPS = videoFile.fps && videoFile.fps !== -1 ? '-' + videoFile.fps : ''
25 31
26 return `${WEBSERVER.URL}/redundancy/videos/${videoFile.Video.uuid}/${videoFile.resolution}${suffixFPS}` 32 return `${WEBSERVER.URL}/redundancy/videos/${videoFile.Video.uuid}/${videoFile.resolution}${suffixFPS}`
27} 33}
28 34
29function getVideoCacheStreamingPlaylistActivityPubUrl (video: VideoModel, playlist: VideoStreamingPlaylistModel) { 35function getVideoCacheStreamingPlaylistActivityPubUrl (video: MVideoUUID, playlist: MStreamingPlaylist) {
30 return `${WEBSERVER.URL}/redundancy/streaming-playlists/${playlist.getStringType()}/${video.uuid}` 36 return `${WEBSERVER.URL}/redundancy/streaming-playlists/${playlist.getStringType()}/${video.uuid}`
31} 37}
32 38
33function getVideoCommentActivityPubUrl (video: VideoModel, videoComment: VideoCommentModel) { 39function getVideoCommentActivityPubUrl (video: MVideoUUID, videoComment: MCommentId) {
34 return WEBSERVER.URL + '/videos/watch/' + video.uuid + '/comments/' + videoComment.id 40 return WEBSERVER.URL + '/videos/watch/' + video.uuid + '/comments/' + videoComment.id
35} 41}
36 42
@@ -42,54 +48,54 @@ function getAccountActivityPubUrl (accountName: string) {
42 return WEBSERVER.URL + '/accounts/' + accountName 48 return WEBSERVER.URL + '/accounts/' + accountName
43} 49}
44 50
45function getVideoAbuseActivityPubUrl (videoAbuse: VideoAbuseModel) { 51function getVideoAbuseActivityPubUrl (videoAbuse: MVideoAbuseId) {
46 return WEBSERVER.URL + '/admin/video-abuses/' + videoAbuse.id 52 return WEBSERVER.URL + '/admin/video-abuses/' + videoAbuse.id
47} 53}
48 54
49function getVideoViewActivityPubUrl (byActor: ActorModelUrl, video: VideoModel) { 55function getVideoViewActivityPubUrl (byActor: MActorUrl, video: MVideoId) {
50 return byActor.url + '/views/videos/' + video.id + '/' + new Date().toISOString() 56 return byActor.url + '/views/videos/' + video.id + '/' + new Date().toISOString()
51} 57}
52 58
53function getVideoLikeActivityPubUrl (byActor: ActorModelUrl, video: VideoModel | { id: number }) { 59function getVideoLikeActivityPubUrl (byActor: MActorUrl, video: MVideoId) {
54 return byActor.url + '/likes/' + video.id 60 return byActor.url + '/likes/' + video.id
55} 61}
56 62
57function getVideoDislikeActivityPubUrl (byActor: ActorModelUrl, video: VideoModel | { id: number }) { 63function getVideoDislikeActivityPubUrl (byActor: MActorUrl, video: MVideoId) {
58 return byActor.url + '/dislikes/' + video.id 64 return byActor.url + '/dislikes/' + video.id
59} 65}
60 66
61function getVideoSharesActivityPubUrl (video: VideoModel) { 67function getVideoSharesActivityPubUrl (video: MVideoUrl) {
62 return video.url + '/announces' 68 return video.url + '/announces'
63} 69}
64 70
65function getVideoCommentsActivityPubUrl (video: VideoModel) { 71function getVideoCommentsActivityPubUrl (video: MVideoUrl) {
66 return video.url + '/comments' 72 return video.url + '/comments'
67} 73}
68 74
69function getVideoLikesActivityPubUrl (video: VideoModel) { 75function getVideoLikesActivityPubUrl (video: MVideoUrl) {
70 return video.url + '/likes' 76 return video.url + '/likes'
71} 77}
72 78
73function getVideoDislikesActivityPubUrl (video: VideoModel) { 79function getVideoDislikesActivityPubUrl (video: MVideoUrl) {
74 return video.url + '/dislikes' 80 return video.url + '/dislikes'
75} 81}
76 82
77function getActorFollowActivityPubUrl (follower: ActorModelOnly, following: ActorModelOnly) { 83function getActorFollowActivityPubUrl (follower: MActor, following: MActorId) {
78 return follower.url + '/follows/' + following.id 84 return follower.url + '/follows/' + following.id
79} 85}
80 86
81function getActorFollowAcceptActivityPubUrl (actorFollow: ActorFollowModelLight) { 87function getActorFollowAcceptActivityPubUrl (actorFollow: MActorFollowActors) {
82 const follower = actorFollow.ActorFollower 88 const follower = actorFollow.ActorFollower
83 const me = actorFollow.ActorFollowing 89 const me = actorFollow.ActorFollowing
84 90
85 return follower.url + '/accepts/follows/' + me.id 91 return follower.url + '/accepts/follows/' + me.id
86} 92}
87 93
88function getActorFollowRejectActivityPubUrl (follower: ActorModelOnly, following: ActorModelOnly) { 94function getActorFollowRejectActivityPubUrl (follower: MActorUrl, following: MActorId) {
89 return follower.url + '/rejects/follows/' + following.id 95 return follower.url + '/rejects/follows/' + following.id
90} 96}
91 97
92function getVideoAnnounceActivityPubUrl (byActor: ActorModelOnly, video: VideoModel) { 98function getVideoAnnounceActivityPubUrl (byActor: MActorId, video: MVideoUrl) {
93 return video.url + '/announces/' + byActor.id 99 return video.url + '/announces/' + byActor.id
94} 100}
95 101
diff --git a/server/lib/activitypub/video-comments.ts b/server/lib/activitypub/video-comments.ts
index 8d2c1ade3..3e8306fa4 100644
--- a/server/lib/activitypub/video-comments.ts
+++ b/server/lib/activitypub/video-comments.ts
@@ -2,20 +2,20 @@ import { sanitizeAndCheckVideoCommentObject } from '../../helpers/custom-validat
2import { logger } from '../../helpers/logger' 2import { logger } from '../../helpers/logger'
3import { doRequest } from '../../helpers/requests' 3import { doRequest } from '../../helpers/requests'
4import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' 4import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
5import { VideoModel } from '../../models/video/video'
6import { VideoCommentModel } from '../../models/video/video-comment' 5import { VideoCommentModel } from '../../models/video/video-comment'
7import { getOrCreateActorAndServerAndModel } from './actor' 6import { getOrCreateActorAndServerAndModel } from './actor'
8import { getOrCreateVideoAndAccountAndChannel } from './videos' 7import { getOrCreateVideoAndAccountAndChannel } from './videos'
9import * as Bluebird from 'bluebird' 8import * as Bluebird from 'bluebird'
10import { checkUrlsSameHost } from '../../helpers/activitypub' 9import { checkUrlsSameHost } from '../../helpers/activitypub'
10import { MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../typings/models/video'
11 11
12type ResolveThreadParams = { 12type ResolveThreadParams = {
13 url: string, 13 url: string,
14 comments?: VideoCommentModel[], 14 comments?: MCommentOwner[],
15 isVideo?: boolean, 15 isVideo?: boolean,
16 commentCreated?: boolean 16 commentCreated?: boolean
17} 17}
18type ResolveThreadResult = Promise<{ video: VideoModel, comment: VideoCommentModel, commentCreated: boolean }> 18type ResolveThreadResult = Promise<{ video: MVideoAccountLightBlacklistAllFiles, comment: MCommentOwnerVideo, commentCreated: boolean }>
19 19
20async function addVideoComments (commentUrls: string[]) { 20async function addVideoComments (commentUrls: string[]) {
21 return Bluebird.map(commentUrls, commentUrl => { 21 return Bluebird.map(commentUrls, commentUrl => {
@@ -85,9 +85,9 @@ async function tryResolveThreadFromVideo (params: ResolveThreadParams) {
85 const syncParam = { likes: true, dislikes: true, shares: true, comments: false, thumbnail: true, refreshVideo: false } 85 const syncParam = { likes: true, dislikes: true, shares: true, comments: false, thumbnail: true, refreshVideo: false }
86 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: url, syncParam }) 86 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: url, syncParam })
87 87
88 let resultComment: VideoCommentModel 88 let resultComment: MCommentOwnerVideo
89 if (comments.length !== 0) { 89 if (comments.length !== 0) {
90 const firstReply = comments[ comments.length - 1 ] 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,7 +97,7 @@ 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 ] 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
@@ -107,7 +107,7 @@ async function tryResolveThreadFromVideo (params: ResolveThreadParams) {
107 comments[i] = await comment.save() 107 comments[i] = await comment.save()
108 } 108 }
109 109
110 resultComment = comments[0] 110 resultComment = comments[0] as MCommentOwnerVideo
111 } 111 }
112 112
113 return { video, comment: resultComment, commentCreated } 113 return { video, comment: resultComment, commentCreated }
@@ -151,7 +151,7 @@ async function resolveParentComment (params: ResolveThreadParams) {
151 originCommentId: null, 151 originCommentId: null,
152 createdAt: new Date(body.published), 152 createdAt: new Date(body.published),
153 updatedAt: new Date(body.updated) 153 updatedAt: new Date(body.updated)
154 }) 154 }) as MCommentOwner
155 comment.Account = actor.Account 155 comment.Account = actor.Account
156 156
157 return resolveThread({ 157 return resolveThread({
diff --git a/server/lib/activitypub/video-rates.ts b/server/lib/activitypub/video-rates.ts
index cda5b2981..6bd46bb58 100644
--- a/server/lib/activitypub/video-rates.ts
+++ b/server/lib/activitypub/video-rates.ts
@@ -1,6 +1,4 @@
1import { Transaction } from 'sequelize' 1import { Transaction } from 'sequelize'
2import { AccountModel } from '../../models/account/account'
3import { VideoModel } from '../../models/video/video'
4import { sendLike, sendUndoDislike, sendUndoLike } from './send' 2import { sendLike, sendUndoDislike, sendUndoLike } from './send'
5import { VideoRateType } from '../../../shared/models/videos' 3import { VideoRateType } from '../../../shared/models/videos'
6import * as Bluebird from 'bluebird' 4import * as Bluebird from 'bluebird'
@@ -10,11 +8,11 @@ import { logger } from '../../helpers/logger'
10import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' 8import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
11import { doRequest } from '../../helpers/requests' 9import { doRequest } from '../../helpers/requests'
12import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' 10import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
13import { ActorModel } from '../../models/activitypub/actor'
14import { getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from './url' 11import { getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from './url'
15import { sendDislike } from './send/send-dislike' 12import { sendDislike } from './send/send-dislike'
13import { MAccountActor, MActorUrl, MVideo, MVideoAccountLight, MVideoId } from '../../typings/models'
16 14
17async function createRates (ratesUrl: string[], video: VideoModel, rate: VideoRateType) { 15async function createRates (ratesUrl: string[], video: MVideo, rate: VideoRateType) {
18 let rateCounts = 0 16 let rateCounts = 0
19 17
20 await Bluebird.map(ratesUrl, async rateUrl => { 18 await Bluebird.map(ratesUrl, async rateUrl => {
@@ -64,11 +62,13 @@ async function createRates (ratesUrl: string[], video: VideoModel, rate: VideoRa
64 return 62 return
65} 63}
66 64
67async function sendVideoRateChange (account: AccountModel, 65async function sendVideoRateChange (
68 video: VideoModel, 66 account: MAccountActor,
69 likes: number, 67 video: MVideoAccountLight,
70 dislikes: number, 68 likes: number,
71 t: Transaction) { 69 dislikes: number,
70 t: Transaction
71) {
72 const actor = account.Actor 72 const actor = account.Actor
73 73
74 // Keep the order: first we undo and then we create 74 // Keep the order: first we undo and then we create
@@ -84,8 +84,10 @@ async function sendVideoRateChange (account: AccountModel,
84 if (dislikes > 0) await sendDislike(actor, video, t) 84 if (dislikes > 0) await sendDislike(actor, video, t)
85} 85}
86 86
87function getRateUrl (rateType: VideoRateType, actor: ActorModel, video: VideoModel) { 87function getRateUrl (rateType: VideoRateType, actor: MActorUrl, video: MVideoId) {
88 return rateType === 'like' ? getVideoLikeActivityPubUrl(actor, video) : getVideoDislikeActivityPubUrl(actor, video) 88 return rateType === 'like'
89 ? getVideoLikeActivityPubUrl(actor, video)
90 : getVideoDislikeActivityPubUrl(actor, video)
89} 91}
90 92
91export { 93export {
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index 3a8451a32..c318978fd 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -24,7 +24,6 @@ import {
24 REMOTE_SCHEME, 24 REMOTE_SCHEME,
25 STATIC_PATHS 25 STATIC_PATHS
26} from '../../initializers/constants' 26} from '../../initializers/constants'
27import { ActorModel } from '../../models/activitypub/actor'
28import { TagModel } from '../../models/video/tag' 27import { TagModel } from '../../models/video/tag'
29import { VideoModel } from '../../models/video/video' 28import { VideoModel } from '../../models/video/video'
30import { VideoFileModel } from '../../models/video/video-file' 29import { VideoFileModel } from '../../models/video/video-file'
@@ -38,7 +37,6 @@ import { JobQueue } from '../job-queue'
38import { ActivitypubHttpFetcherPayload } from '../job-queue/handlers/activitypub-http-fetcher' 37import { ActivitypubHttpFetcherPayload } from '../job-queue/handlers/activitypub-http-fetcher'
39import { createRates } from './video-rates' 38import { createRates } from './video-rates'
40import { addVideoShares, shareVideoByServerAndChannel } from './share' 39import { addVideoShares, shareVideoByServerAndChannel } from './share'
41import { AccountModel } from '../../models/account/account'
42import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video' 40import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video'
43import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' 41import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
44import { Notifier } from '../notifier' 42import { Notifier } from '../notifier'
@@ -49,15 +47,31 @@ import { VideoShareModel } from '../../models/video/video-share'
49import { VideoCommentModel } from '../../models/video/video-comment' 47import { VideoCommentModel } from '../../models/video/video-comment'
50import { sequelizeTypescript } from '../../initializers/database' 48import { sequelizeTypescript } from '../../initializers/database'
51import { createPlaceholderThumbnail, createVideoMiniatureFromUrl } from '../thumbnail' 49import { createPlaceholderThumbnail, createVideoMiniatureFromUrl } from '../thumbnail'
52import { ThumbnailModel } from '../../models/video/thumbnail'
53import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' 50import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
54import { join } from 'path' 51import { join } from 'path'
55import { FilteredModelAttributes } from '../../typings/sequelize' 52import { FilteredModelAttributes } from '../../typings/sequelize'
56import { autoBlacklistVideoIfNeeded } from '../video-blacklist' 53import { autoBlacklistVideoIfNeeded } from '../video-blacklist'
57import { ActorFollowScoreCache } from '../files-cache' 54import { ActorFollowScoreCache } from '../files-cache'
58import { AccountModelIdActor, VideoChannelModelId, VideoChannelModelIdActor } from '../../typings/models' 55import {
56 MAccountIdActor,
57 MChannelAccountLight,
58 MChannelDefault,
59 MChannelId,
60 MVideo,
61 MVideoAccountLight,
62 MVideoAccountLightBlacklistAllFiles,
63 MVideoAP,
64 MVideoAPWithoutCaption,
65 MVideoFile,
66 MVideoFullLight,
67 MVideoId,
68 MVideoThumbnail
69} from '../../typings/models'
70import { MThumbnail } from '../../typings/models/video/thumbnail'
71
72async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVideo: boolean, transaction?: sequelize.Transaction) {
73 const video = videoArg as MVideoAP
59 74
60async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) {
61 if ( 75 if (
62 // Check this is not a blacklisted video, or unfederated blacklisted video 76 // Check this is not a blacklisted video, or unfederated blacklisted video
63 (video.isBlacklisted() === false || (isNewVideo === false && video.VideoBlacklist.unfederated === false)) && 77 (video.isBlacklisted() === false || (isNewVideo === false && video.VideoBlacklist.unfederated === false)) &&
@@ -102,7 +116,7 @@ async function fetchRemoteVideo (videoUrl: string): Promise<{ response: request.
102 return { response, videoObject: body } 116 return { response, videoObject: body }
103} 117}
104 118
105async function fetchRemoteVideoDescription (video: VideoModel) { 119async function fetchRemoteVideoDescription (video: MVideoAccountLight) {
106 const host = video.VideoChannel.Account.Actor.Server.host 120 const host = video.VideoChannel.Account.Actor.Server.host
107 const path = video.getDescriptionAPIPath() 121 const path = video.getDescriptionAPIPath()
108 const options = { 122 const options = {
@@ -114,14 +128,14 @@ async function fetchRemoteVideoDescription (video: VideoModel) {
114 return body.description ? body.description : '' 128 return body.description ? body.description : ''
115} 129}
116 130
117function fetchRemoteVideoStaticFile (video: VideoModel, path: string, destPath: string) { 131function fetchRemoteVideoStaticFile (video: MVideoAccountLight, path: string, destPath: string) {
118 const url = buildRemoteBaseUrl(video, path) 132 const url = buildRemoteBaseUrl(video, path)
119 133
120 // We need to provide a callback, if no we could have an uncaught exception 134 // We need to provide a callback, if no we could have an uncaught exception
121 return doRequestAndSaveToFile({ uri: url }, destPath) 135 return doRequestAndSaveToFile({ uri: url }, destPath)
122} 136}
123 137
124function buildRemoteBaseUrl (video: VideoModel, path: string) { 138function buildRemoteBaseUrl (video: MVideoAccountLight, path: string) {
125 const host = video.VideoChannel.Account.Actor.Server.host 139 const host = video.VideoChannel.Account.Actor.Server.host
126 140
127 return REMOTE_SCHEME.HTTP + '://' + host + path 141 return REMOTE_SCHEME.HTTP + '://' + host + path
@@ -146,7 +160,7 @@ type SyncParam = {
146 thumbnail: boolean 160 thumbnail: boolean
147 refreshVideo?: boolean 161 refreshVideo?: boolean
148} 162}
149async function syncVideoExternalAttributes (video: VideoModel, fetchedVideo: VideoTorrentObject, syncParam: SyncParam) { 163async function syncVideoExternalAttributes (video: MVideo, fetchedVideo: VideoTorrentObject, syncParam: SyncParam) {
150 logger.info('Adding likes/dislikes/shares/comments of video %s.', video.uuid) 164 logger.info('Adding likes/dislikes/shares/comments of video %s.', video.uuid)
151 165
152 const jobPayloads: ActivitypubHttpFetcherPayload[] = [] 166 const jobPayloads: ActivitypubHttpFetcherPayload[] = []
@@ -194,12 +208,24 @@ async function syncVideoExternalAttributes (video: VideoModel, fetchedVideo: Vid
194 await Bluebird.map(jobPayloads, payload => JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })) 208 await Bluebird.map(jobPayloads, payload => JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload }))
195} 209}
196 210
211function getOrCreateVideoAndAccountAndChannel (options: {
212 videoObject: { id: string } | string,
213 syncParam?: SyncParam,
214 fetchType?: 'all',
215 allowRefresh?: boolean
216}): Promise<{ video: MVideoAccountLightBlacklistAllFiles, created: boolean, autoBlacklisted?: boolean }>
217function getOrCreateVideoAndAccountAndChannel (options: {
218 videoObject: { id: string } | string,
219 syncParam?: SyncParam,
220 fetchType?: VideoFetchByUrlType,
221 allowRefresh?: boolean
222}): Promise<{ video: MVideoAccountLightBlacklistAllFiles | MVideoThumbnail, created: boolean, autoBlacklisted?: boolean }>
197async function getOrCreateVideoAndAccountAndChannel (options: { 223async function getOrCreateVideoAndAccountAndChannel (options: {
198 videoObject: { id: string } | string, 224 videoObject: { id: string } | string,
199 syncParam?: SyncParam, 225 syncParam?: SyncParam,
200 fetchType?: VideoFetchByUrlType, 226 fetchType?: VideoFetchByUrlType,
201 allowRefresh?: boolean // true by default 227 allowRefresh?: boolean // true by default
202}) { 228}): Promise<{ video: MVideoAccountLightBlacklistAllFiles | MVideoThumbnail, created: boolean, autoBlacklisted?: boolean }> {
203 // Default params 229 // Default params
204 const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false } 230 const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false }
205 const fetchType = options.fetchType || 'all' 231 const fetchType = options.fetchType || 'all'
@@ -227,8 +253,9 @@ async function getOrCreateVideoAndAccountAndChannel (options: {
227 const { videoObject: fetchedVideo } = await fetchRemoteVideo(videoUrl) 253 const { videoObject: fetchedVideo } = await fetchRemoteVideo(videoUrl)
228 if (!fetchedVideo) throw new Error('Cannot fetch remote video with url: ' + videoUrl) 254 if (!fetchedVideo) throw new Error('Cannot fetch remote video with url: ' + videoUrl)
229 255
230 const channelActor = await getOrCreateVideoChannelFromVideoObject(fetchedVideo) 256 const actor = await getOrCreateVideoChannelFromVideoObject(fetchedVideo)
231 const { autoBlacklisted, videoCreated } = await retryTransactionWrapper(createVideo, fetchedVideo, channelActor, syncParam.thumbnail) 257 const videoChannel = actor.VideoChannel
258 const { autoBlacklisted, videoCreated } = await retryTransactionWrapper(createVideo, fetchedVideo, videoChannel, syncParam.thumbnail)
232 259
233 await syncVideoExternalAttributes(videoCreated, fetchedVideo, syncParam) 260 await syncVideoExternalAttributes(videoCreated, fetchedVideo, syncParam)
234 261
@@ -236,22 +263,22 @@ async function getOrCreateVideoAndAccountAndChannel (options: {
236} 263}
237 264
238async function updateVideoFromAP (options: { 265async function updateVideoFromAP (options: {
239 video: VideoModel, 266 video: MVideoAccountLightBlacklistAllFiles,
240 videoObject: VideoTorrentObject, 267 videoObject: VideoTorrentObject,
241 account: AccountModelIdActor, 268 account: MAccountIdActor,
242 channel: VideoChannelModelIdActor, 269 channel: MChannelDefault,
243 overrideTo?: string[] 270 overrideTo?: string[]
244}) { 271}) {
245 const { video, videoObject, account, channel, overrideTo } = options 272 const { video, videoObject, account, channel, overrideTo } = options
246 273
247 logger.debug('Updating remote video "%s".', options.videoObject.uuid) 274 logger.debug('Updating remote video "%s".', options.videoObject.uuid, { account, channel })
248 275
249 let videoFieldsSave: any 276 let videoFieldsSave: any
250 const wasPrivateVideo = video.privacy === VideoPrivacy.PRIVATE 277 const wasPrivateVideo = video.privacy === VideoPrivacy.PRIVATE
251 const wasUnlistedVideo = video.privacy === VideoPrivacy.UNLISTED 278 const wasUnlistedVideo = video.privacy === VideoPrivacy.UNLISTED
252 279
253 try { 280 try {
254 let thumbnailModel: ThumbnailModel 281 let thumbnailModel: MThumbnail
255 282
256 try { 283 try {
257 thumbnailModel = await createVideoMiniatureFromUrl(videoObject.icon.url, video, ThumbnailType.MINIATURE) 284 thumbnailModel = await createVideoMiniatureFromUrl(videoObject.icon.url, video, ThumbnailType.MINIATURE)
@@ -259,7 +286,7 @@ async function updateVideoFromAP (options: {
259 logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err }) 286 logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err })
260 } 287 }
261 288
262 await sequelizeTypescript.transaction(async t => { 289 const videoUpdated = await sequelizeTypescript.transaction(async t => {
263 const sequelizeOptions = { transaction: t } 290 const sequelizeOptions = { transaction: t }
264 291
265 videoFieldsSave = video.toJSON() 292 videoFieldsSave = video.toJSON()
@@ -293,21 +320,21 @@ async function updateVideoFromAP (options: {
293 video.channelId = videoData.channelId 320 video.channelId = videoData.channelId
294 video.views = videoData.views 321 video.views = videoData.views
295 322
296 await video.save(sequelizeOptions) 323 const videoUpdated = await video.save(sequelizeOptions) as MVideoFullLight
297 324
298 if (thumbnailModel) await video.addAndSaveThumbnail(thumbnailModel, t) 325 if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel, t)
299 326
300 // FIXME: use icon URL instead 327 // FIXME: use icon URL instead
301 const previewUrl = buildRemoteBaseUrl(video, join(STATIC_PATHS.PREVIEWS, video.getPreview().filename)) 328 const previewUrl = buildRemoteBaseUrl(videoUpdated, join(STATIC_PATHS.PREVIEWS, videoUpdated.getPreview().filename))
302 const previewModel = createPlaceholderThumbnail(previewUrl, video, ThumbnailType.PREVIEW, PREVIEWS_SIZE) 329 const previewModel = createPlaceholderThumbnail(previewUrl, video, ThumbnailType.PREVIEW, PREVIEWS_SIZE)
303 await video.addAndSaveThumbnail(previewModel, t) 330 await videoUpdated.addAndSaveThumbnail(previewModel, t)
304 331
305 { 332 {
306 const videoFileAttributes = videoFileActivityUrlToDBAttributes(video, videoObject) 333 const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoUpdated, videoObject)
307 const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a)) 334 const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a))
308 335
309 // Remove video files that do not exist anymore 336 // Remove video files that do not exist anymore
310 const destroyTasks = video.VideoFiles 337 const destroyTasks = videoUpdated.VideoFiles
311 .filter(f => !newVideoFiles.find(newFile => newFile.hasSameUniqueKeysThan(f))) 338 .filter(f => !newVideoFiles.find(newFile => newFile.hasSameUniqueKeysThan(f)))
312 .map(f => f.destroy(sequelizeOptions)) 339 .map(f => f.destroy(sequelizeOptions))
313 await Promise.all(destroyTasks) 340 await Promise.all(destroyTasks)
@@ -318,15 +345,15 @@ async function updateVideoFromAP (options: {
318 .then(([ file ]) => file) 345 .then(([ file ]) => file)
319 }) 346 })
320 347
321 video.VideoFiles = await Promise.all(upsertTasks) 348 videoUpdated.VideoFiles = await Promise.all(upsertTasks)
322 } 349 }
323 350
324 { 351 {
325 const streamingPlaylistAttributes = streamingPlaylistActivityUrlToDBAttributes(video, videoObject, video.VideoFiles) 352 const streamingPlaylistAttributes = streamingPlaylistActivityUrlToDBAttributes(videoUpdated, videoObject, videoUpdated.VideoFiles)
326 const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a)) 353 const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a))
327 354
328 // Remove video files that do not exist anymore 355 // Remove video files that do not exist anymore
329 const destroyTasks = video.VideoStreamingPlaylists 356 const destroyTasks = videoUpdated.VideoStreamingPlaylists
330 .filter(f => !newStreamingPlaylists.find(newPlaylist => newPlaylist.hasSameUniqueKeysThan(f))) 357 .filter(f => !newStreamingPlaylists.find(newPlaylist => newPlaylist.hasSameUniqueKeysThan(f)))
331 .map(f => f.destroy(sequelizeOptions)) 358 .map(f => f.destroy(sequelizeOptions))
332 await Promise.all(destroyTasks) 359 await Promise.all(destroyTasks)
@@ -337,38 +364,42 @@ async function updateVideoFromAP (options: {
337 .then(([ streamingPlaylist ]) => streamingPlaylist) 364 .then(([ streamingPlaylist ]) => streamingPlaylist)
338 }) 365 })
339 366
340 video.VideoStreamingPlaylists = await Promise.all(upsertTasks) 367 videoUpdated.VideoStreamingPlaylists = await Promise.all(upsertTasks)
341 } 368 }
342 369
343 { 370 {
344 // Update Tags 371 // Update Tags
345 const tags = videoObject.tag.map(tag => tag.name) 372 const tags = videoObject.tag.map(tag => tag.name)
346 const tagInstances = await TagModel.findOrCreateTags(tags, t) 373 const tagInstances = await TagModel.findOrCreateTags(tags, t)
347 await video.$set('Tags', tagInstances, sequelizeOptions) 374 await videoUpdated.$set('Tags', tagInstances, sequelizeOptions)
348 } 375 }
349 376
350 { 377 {
351 // Update captions 378 // Update captions
352 await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(video.id, t) 379 await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(videoUpdated.id, t)
353 380
354 const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => { 381 const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => {
355 return VideoCaptionModel.insertOrReplaceLanguage(video.id, c.identifier, t) 382 return VideoCaptionModel.insertOrReplaceLanguage(videoUpdated.id, c.identifier, t)
356 }) 383 })
357 video.VideoCaptions = await Promise.all(videoCaptionsPromises) 384 await Promise.all(videoCaptionsPromises)
358 } 385 }
386
387 return videoUpdated
359 }) 388 })
360 389
361 await autoBlacklistVideoIfNeeded({ 390 await autoBlacklistVideoIfNeeded({
362 video, 391 video: videoUpdated,
363 user: undefined, 392 user: undefined,
364 isRemote: true, 393 isRemote: true,
365 isNew: false, 394 isNew: false,
366 transaction: undefined 395 transaction: undefined
367 }) 396 })
368 397
369 if (wasPrivateVideo || wasUnlistedVideo) Notifier.Instance.notifyOnNewVideoIfNeeded(video) // Notify our users? 398 if (wasPrivateVideo || wasUnlistedVideo) Notifier.Instance.notifyOnNewVideoIfNeeded(videoUpdated) // Notify our users?
370 399
371 logger.info('Remote video with uuid %s updated', videoObject.uuid) 400 logger.info('Remote video with uuid %s updated', videoObject.uuid)
401
402 return videoUpdated
372 } catch (err) { 403 } catch (err) {
373 if (video !== undefined && videoFieldsSave !== undefined) { 404 if (video !== undefined && videoFieldsSave !== undefined) {
374 resetSequelizeInstance(video, videoFieldsSave) 405 resetSequelizeInstance(video, videoFieldsSave)
@@ -381,15 +412,15 @@ async function updateVideoFromAP (options: {
381} 412}
382 413
383async function refreshVideoIfNeeded (options: { 414async function refreshVideoIfNeeded (options: {
384 video: VideoModel, 415 video: MVideoThumbnail,
385 fetchedType: VideoFetchByUrlType, 416 fetchedType: VideoFetchByUrlType,
386 syncParam: SyncParam 417 syncParam: SyncParam
387}): Promise<VideoModel> { 418}): Promise<MVideoThumbnail> {
388 if (!options.video.isOutdated()) return options.video 419 if (!options.video.isOutdated()) return options.video
389 420
390 // We need more attributes if the argument video was fetched with not enough joints 421 // We need more attributes if the argument video was fetched with not enough joints
391 const video = options.fetchedType === 'all' 422 const video = options.fetchedType === 'all'
392 ? options.video 423 ? options.video as MVideoAccountLightBlacklistAllFiles
393 : await VideoModel.loadByUrlAndPopulateAccount(options.video.url) 424 : await VideoModel.loadByUrlAndPopulateAccount(options.video.url)
394 425
395 try { 426 try {
@@ -410,12 +441,11 @@ async function refreshVideoIfNeeded (options: {
410 } 441 }
411 442
412 const channelActor = await getOrCreateVideoChannelFromVideoObject(videoObject) 443 const channelActor = await getOrCreateVideoChannelFromVideoObject(videoObject)
413 const account = await AccountModel.load(channelActor.VideoChannel.accountId)
414 444
415 const updateOptions = { 445 const updateOptions = {
416 video, 446 video,
417 videoObject, 447 videoObject,
418 account, 448 account: channelActor.VideoChannel.Account,
419 channel: channelActor.VideoChannel 449 channel: channelActor.VideoChannel
420 } 450 }
421 await retryTransactionWrapper(updateVideoFromAP, updateOptions) 451 await retryTransactionWrapper(updateVideoFromAP, updateOptions)
@@ -467,15 +497,15 @@ function isAPPlaylistSegmentHashesUrlObject (tag: any): tag is ActivityPlaylistS
467 return tag.name === 'sha256' && tag.type === 'Link' && urlMediaType === 'application/json' 497 return tag.name === 'sha256' && tag.type === 'Link' && urlMediaType === 'application/json'
468} 498}
469 499
470async function createVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) { 500async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAccountLight, waitThumbnail = false) {
471 logger.debug('Adding remote video %s.', videoObject.id) 501 logger.debug('Adding remote video %s.', videoObject.id)
472 502
473 const videoData = await videoActivityObjectToDBAttributes(channelActor.VideoChannel, videoObject, videoObject.to) 503 const videoData = await videoActivityObjectToDBAttributes(channel, videoObject, videoObject.to)
474 const video = VideoModel.build(videoData) 504 const video = VideoModel.build(videoData) as MVideoThumbnail
475 505
476 const promiseThumbnail = createVideoMiniatureFromUrl(videoObject.icon.url, video, ThumbnailType.MINIATURE) 506 const promiseThumbnail = createVideoMiniatureFromUrl(videoObject.icon.url, video, ThumbnailType.MINIATURE)
477 507
478 let thumbnailModel: ThumbnailModel 508 let thumbnailModel: MThumbnail
479 if (waitThumbnail === true) { 509 if (waitThumbnail === true) {
480 thumbnailModel = await promiseThumbnail 510 thumbnailModel = await promiseThumbnail
481 } 511 }
@@ -483,8 +513,8 @@ async function createVideo (videoObject: VideoTorrentObject, channelActor: Actor
483 const { autoBlacklisted, videoCreated } = await sequelizeTypescript.transaction(async t => { 513 const { autoBlacklisted, videoCreated } = await sequelizeTypescript.transaction(async t => {
484 const sequelizeOptions = { transaction: t } 514 const sequelizeOptions = { transaction: t }
485 515
486 const videoCreated = await video.save(sequelizeOptions) 516 const videoCreated = await video.save(sequelizeOptions) as MVideoFullLight
487 videoCreated.VideoChannel = channelActor.VideoChannel 517 videoCreated.VideoChannel = channel
488 518
489 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t) 519 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
490 520
@@ -517,15 +547,14 @@ async function createVideo (videoObject: VideoTorrentObject, channelActor: Actor
517 const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => { 547 const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => {
518 return VideoCaptionModel.insertOrReplaceLanguage(videoCreated.id, c.identifier, t) 548 return VideoCaptionModel.insertOrReplaceLanguage(videoCreated.id, c.identifier, t)
519 }) 549 })
520 const captions = await Promise.all(videoCaptionsPromises) 550 await Promise.all(videoCaptionsPromises)
521 551
522 video.VideoFiles = videoFiles 552 videoCreated.VideoFiles = videoFiles
523 video.VideoStreamingPlaylists = streamingPlaylists 553 videoCreated.VideoStreamingPlaylists = streamingPlaylists
524 video.Tags = tagInstances 554 videoCreated.Tags = tagInstances
525 video.VideoCaptions = captions
526 555
527 const autoBlacklisted = await autoBlacklistVideoIfNeeded({ 556 const autoBlacklisted = await autoBlacklistVideoIfNeeded({
528 video, 557 video: videoCreated,
529 user: undefined, 558 user: undefined,
530 isRemote: true, 559 isRemote: true,
531 isNew: true, 560 isNew: true,
@@ -548,11 +577,7 @@ async function createVideo (videoObject: VideoTorrentObject, channelActor: Actor
548 return { autoBlacklisted, videoCreated } 577 return { autoBlacklisted, videoCreated }
549} 578}
550 579
551async function videoActivityObjectToDBAttributes ( 580async function videoActivityObjectToDBAttributes (videoChannel: MChannelId, videoObject: VideoTorrentObject, to: string[] = []) {
552 videoChannel: VideoChannelModelId,
553 videoObject: VideoTorrentObject,
554 to: string[] = []
555) {
556 const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPrivacy.PUBLIC : VideoPrivacy.UNLISTED 581 const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPrivacy.PUBLIC : VideoPrivacy.UNLISTED
557 const duration = videoObject.duration.replace(/[^\d]+/, '') 582 const duration = videoObject.duration.replace(/[^\d]+/, '')
558 583
@@ -603,7 +628,7 @@ async function videoActivityObjectToDBAttributes (
603 } 628 }
604} 629}
605 630
606function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: VideoTorrentObject) { 631function videoFileActivityUrlToDBAttributes (video: MVideo, videoObject: VideoTorrentObject) {
607 const fileUrls = videoObject.url.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[] 632 const fileUrls = videoObject.url.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[]
608 633
609 if (fileUrls.length === 0) { 634 if (fileUrls.length === 0) {
@@ -641,7 +666,7 @@ function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: Vid
641 return attributes 666 return attributes
642} 667}
643 668
644function streamingPlaylistActivityUrlToDBAttributes (video: VideoModel, videoObject: VideoTorrentObject, videoFiles: VideoFileModel[]) { 669function streamingPlaylistActivityUrlToDBAttributes (video: MVideoId, videoObject: VideoTorrentObject, videoFiles: MVideoFile[]) {
645 const playlistUrls = videoObject.url.filter(u => isAPStreamingPlaylistUrlObject(u)) as ActivityPlaylistUrlObject[] 670 const playlistUrls = videoObject.url.filter(u => isAPStreamingPlaylistUrlObject(u)) as ActivityPlaylistUrlObject[]
646 if (playlistUrls.length === 0) return [] 671 if (playlistUrls.length === 0) return []
647 672
diff --git a/server/lib/avatar.ts b/server/lib/avatar.ts
index 1b38e6cb5..ad4cdd3ab 100644
--- a/server/lib/avatar.ts
+++ b/server/lib/avatar.ts
@@ -3,8 +3,6 @@ import { 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'
5import { processImage } from '../helpers/image-utils' 5import { processImage } from '../helpers/image-utils'
6import { AccountModel } from '../models/account/account'
7import { VideoChannelModel } from '../models/video/video-channel'
8import { extname, join } from 'path' 6import { extname, join } from 'path'
9import { retryTransactionWrapper } from '../helpers/database-utils' 7import { retryTransactionWrapper } from '../helpers/database-utils'
10import * as uuidv4 from 'uuid/v4' 8import * as uuidv4 from 'uuid/v4'
@@ -13,8 +11,12 @@ import { sequelizeTypescript } from '../initializers/database'
13import * as LRUCache from 'lru-cache' 11import * as LRUCache from 'lru-cache'
14import { queue } from 'async' 12import { queue } from 'async'
15import { downloadImage } from '../helpers/requests' 13import { downloadImage } from '../helpers/requests'
14import { MAccountDefault, MChannelDefault } from '../typings/models'
16 15
17async function updateActorAvatarFile (avatarPhysicalFile: Express.Multer.File, accountOrChannel: AccountModel | VideoChannelModel) { 16async function updateActorAvatarFile (
17 avatarPhysicalFile: Express.Multer.File,
18 accountOrChannel: MAccountDefault | MChannelDefault
19) {
18 const extension = extname(avatarPhysicalFile.filename) 20 const extension = extname(avatarPhysicalFile.filename)
19 const avatarName = uuidv4() + extension 21 const avatarName = uuidv4() + extension
20 const destination = join(CONFIG.STORAGE.AVATARS_DIR, avatarName) 22 const destination = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)
diff --git a/server/lib/blocklist.ts b/server/lib/blocklist.ts
index 1633e500c..28c69b46e 100644
--- a/server/lib/blocklist.ts
+++ b/server/lib/blocklist.ts
@@ -1,6 +1,7 @@
1import { sequelizeTypescript } from '../initializers' 1import { sequelizeTypescript } from '../initializers'
2import { AccountBlocklistModel } from '../models/account/account-blocklist' 2import { AccountBlocklistModel } from '../models/account/account-blocklist'
3import { ServerBlocklistModel } from '../models/server/server-blocklist' 3import { ServerBlocklistModel } from '../models/server/server-blocklist'
4import { MAccountBlocklist, MServerBlocklist } from '@server/typings/models'
4 5
5function addAccountInBlocklist (byAccountId: number, targetAccountId: number) { 6function addAccountInBlocklist (byAccountId: number, targetAccountId: number) {
6 return sequelizeTypescript.transaction(async t => { 7 return sequelizeTypescript.transaction(async t => {
@@ -20,13 +21,13 @@ function addServerInBlocklist (byAccountId: number, targetServerId: number) {
20 }) 21 })
21} 22}
22 23
23function removeAccountFromBlocklist (accountBlock: AccountBlocklistModel) { 24function removeAccountFromBlocklist (accountBlock: MAccountBlocklist) {
24 return sequelizeTypescript.transaction(async t => { 25 return sequelizeTypescript.transaction(async t => {
25 return accountBlock.destroy({ transaction: t }) 26 return accountBlock.destroy({ transaction: t })
26 }) 27 })
27} 28}
28 29
29function removeServerFromBlocklist (serverBlock: ServerBlocklistModel) { 30function removeServerFromBlocklist (serverBlock: MServerBlocklist) {
30 return sequelizeTypescript.transaction(async t => { 31 return sequelizeTypescript.transaction(async t => {
31 return serverBlock.destroy({ transaction: t }) 32 return serverBlock.destroy({ transaction: t })
32 }) 33 })
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts
index 8841dd2ac..a1f4ae858 100644
--- a/server/lib/client-html.ts
+++ b/server/lib/client-html.ts
@@ -13,6 +13,7 @@ import { VideoChannelModel } from '../models/video/video-channel'
13import * as Bluebird from 'bluebird' 13import * as Bluebird from 'bluebird'
14import { CONFIG } from '../initializers/config' 14import { CONFIG } from '../initializers/config'
15import { logger } from '../helpers/logger' 15import { logger } from '../helpers/logger'
16import { MAccountActor, MChannelActor, MVideo } from '../typings/models'
16 17
17export class ClientHtml { 18export class ClientHtml {
18 19
@@ -41,11 +42,11 @@ export class ClientHtml {
41 42
42 const [ html, video ] = await Promise.all([ 43 const [ html, video ] = await Promise.all([
43 ClientHtml.getIndexHTML(req, res), 44 ClientHtml.getIndexHTML(req, res),
44 VideoModel.load(videoId) 45 VideoModel.loadWithBlacklist(videoId)
45 ]) 46 ])
46 47
47 // Let Angular application handle errors 48 // Let Angular application handle errors
48 if (!video || video.privacy === VideoPrivacy.PRIVATE) { 49 if (!video || video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) {
49 return ClientHtml.getIndexHTML(req, res) 50 return ClientHtml.getIndexHTML(req, res)
50 } 51 }
51 52
@@ -65,7 +66,7 @@ export class ClientHtml {
65 } 66 }
66 67
67 private static async getAccountOrChannelHTMLPage ( 68 private static async getAccountOrChannelHTMLPage (
68 loader: () => Bluebird<AccountModel | VideoChannelModel>, 69 loader: () => Bluebird<MAccountActor | MChannelActor>,
69 req: express.Request, 70 req: express.Request,
70 res: express.Response 71 res: express.Response
71 ) { 72 ) {
@@ -157,7 +158,7 @@ export class ClientHtml {
157 return htmlStringPage.replace('</head>', linkTag + '</head>') 158 return htmlStringPage.replace('</head>', linkTag + '</head>')
158 } 159 }
159 160
160 private static addVideoOpenGraphAndOEmbedTags (htmlStringPage: string, video: VideoModel) { 161 private static addVideoOpenGraphAndOEmbedTags (htmlStringPage: string, video: MVideo) {
161 const previewUrl = WEBSERVER.URL + video.getPreviewStaticPath() 162 const previewUrl = WEBSERVER.URL + video.getPreviewStaticPath()
162 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() 163 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
163 164
@@ -236,7 +237,7 @@ export class ClientHtml {
236 return this.addOpenGraphAndOEmbedTags(htmlStringPage, tagsString) 237 return this.addOpenGraphAndOEmbedTags(htmlStringPage, tagsString)
237 } 238 }
238 239
239 private static addAccountOrChannelMetaTags (htmlStringPage: string, entity: AccountModel | VideoChannelModel) { 240 private static addAccountOrChannelMetaTags (htmlStringPage: string, entity: MAccountActor | MChannelActor) {
240 // SEO, use origin account or channel URL 241 // SEO, use origin account or channel URL
241 const metaTags = `<link rel="canonical" href="${entity.Actor.url}" />` 242 const metaTags = `<link rel="canonical" href="${entity.Actor.url}" />`
242 243
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts
index 10e7d0479..bd3d4f252 100644
--- a/server/lib/emailer.ts
+++ b/server/lib/emailer.ts
@@ -2,17 +2,20 @@ import { createTransport, Transporter } from 'nodemailer'
2import { isTestInstance } from '../helpers/core-utils' 2import { isTestInstance } from '../helpers/core-utils'
3import { bunyanLogger, logger } from '../helpers/logger' 3import { bunyanLogger, logger } from '../helpers/logger'
4import { CONFIG } from '../initializers/config' 4import { CONFIG } from '../initializers/config'
5import { UserModel } from '../models/account/user'
6import { VideoModel } from '../models/video/video'
7import { JobQueue } from './job-queue' 5import { JobQueue } from './job-queue'
8import { EmailPayload } from './job-queue/handlers/email' 6import { EmailPayload } from './job-queue/handlers/email'
9import { readFileSync } from 'fs-extra' 7import { readFileSync } from 'fs-extra'
10import { VideoCommentModel } from '../models/video/video-comment'
11import { VideoAbuseModel } from '../models/video/video-abuse'
12import { VideoBlacklistModel } from '../models/video/video-blacklist'
13import { VideoImportModel } from '../models/video/video-import'
14import { ActorFollowModel } from '../models/activitypub/actor-follow'
15import { WEBSERVER } from '../initializers/constants' 8import { WEBSERVER } from '../initializers/constants'
9import {
10 MCommentOwnerVideo,
11 MVideo,
12 MVideoAbuseVideo,
13 MVideoAccountLight,
14 MVideoBlacklistLightVideo,
15 MVideoBlacklistVideo
16} from '../typings/models/video'
17import { MActorFollowActors, MActorFollowFull, MUser } from '../typings/models'
18import { MVideoImport, MVideoImportVideo } from '@server/typings/models/video/video-import'
16 19
17type SendEmailOptions = { 20type SendEmailOptions = {
18 to: string[] 21 to: string[]
@@ -90,7 +93,7 @@ class Emailer {
90 } 93 }
91 } 94 }
92 95
93 addNewVideoFromSubscriberNotification (to: string[], video: VideoModel) { 96 addNewVideoFromSubscriberNotification (to: string[], video: MVideoAccountLight) {
94 const channelName = video.VideoChannel.getDisplayName() 97 const channelName = video.VideoChannel.getDisplayName()
95 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() 98 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
96 99
@@ -111,7 +114,7 @@ class Emailer {
111 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 114 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
112 } 115 }
113 116
114 addNewFollowNotification (to: string[], actorFollow: ActorFollowModel, followType: 'account' | 'channel') { 117 addNewFollowNotification (to: string[], actorFollow: MActorFollowFull, followType: 'account' | 'channel') {
115 const followerName = actorFollow.ActorFollower.Account.getDisplayName() 118 const followerName = actorFollow.ActorFollower.Account.getDisplayName()
116 const followingName = (actorFollow.ActorFollowing.VideoChannel || actorFollow.ActorFollowing.Account).getDisplayName() 119 const followingName = (actorFollow.ActorFollowing.VideoChannel || actorFollow.ActorFollowing.Account).getDisplayName()
117 120
@@ -130,7 +133,7 @@ class Emailer {
130 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 133 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
131 } 134 }
132 135
133 addNewInstanceFollowerNotification (to: string[], actorFollow: ActorFollowModel) { 136 addNewInstanceFollowerNotification (to: string[], actorFollow: MActorFollowActors) {
134 const awaitingApproval = actorFollow.state === 'pending' ? ' awaiting manual approval.' : '' 137 const awaitingApproval = actorFollow.state === 'pending' ? ' awaiting manual approval.' : ''
135 138
136 const text = `Hi dear admin,\n\n` + 139 const text = `Hi dear admin,\n\n` +
@@ -148,7 +151,23 @@ class Emailer {
148 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 151 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
149 } 152 }
150 153
151 myVideoPublishedNotification (to: string[], video: VideoModel) { 154 addAutoInstanceFollowingNotification (to: string[], actorFollow: MActorFollowActors) {
155 const text = `Hi dear admin,\n\n` +
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 = {
162 to,
163 subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Auto instance following',
164 text
165 }
166
167 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
168 }
169
170 myVideoPublishedNotification (to: string[], video: MVideo) {
152 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() 171 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
153 172
154 const text = `Hi dear user,\n\n` + 173 const text = `Hi dear user,\n\n` +
@@ -168,7 +187,7 @@ class Emailer {
168 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 187 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
169 } 188 }
170 189
171 myVideoImportSuccessNotification (to: string[], videoImport: VideoImportModel) { 190 myVideoImportSuccessNotification (to: string[], videoImport: MVideoImportVideo) {
172 const videoUrl = WEBSERVER.URL + videoImport.Video.getWatchStaticPath() 191 const videoUrl = WEBSERVER.URL + videoImport.Video.getWatchStaticPath()
173 192
174 const text = `Hi dear user,\n\n` + 193 const text = `Hi dear user,\n\n` +
@@ -188,7 +207,7 @@ class Emailer {
188 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 207 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
189 } 208 }
190 209
191 myVideoImportErrorNotification (to: string[], videoImport: VideoImportModel) { 210 myVideoImportErrorNotification (to: string[], videoImport: MVideoImport) {
192 const importUrl = WEBSERVER.URL + '/my-account/video-imports' 211 const importUrl = WEBSERVER.URL + '/my-account/video-imports'
193 212
194 const text = `Hi dear user,\n\n` + 213 const text = `Hi dear user,\n\n` +
@@ -208,7 +227,7 @@ class Emailer {
208 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 227 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
209 } 228 }
210 229
211 addNewCommentOnMyVideoNotification (to: string[], comment: VideoCommentModel) { 230 addNewCommentOnMyVideoNotification (to: string[], comment: MCommentOwnerVideo) {
212 const accountName = comment.Account.getDisplayName() 231 const accountName = comment.Account.getDisplayName()
213 const video = comment.Video 232 const video = comment.Video
214 const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath() 233 const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath()
@@ -230,7 +249,7 @@ class Emailer {
230 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 249 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
231 } 250 }
232 251
233 addNewCommentMentionNotification (to: string[], comment: VideoCommentModel) { 252 addNewCommentMentionNotification (to: string[], comment: MCommentOwnerVideo) {
234 const accountName = comment.Account.getDisplayName() 253 const accountName = comment.Account.getDisplayName()
235 const video = comment.Video 254 const video = comment.Video
236 const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath() 255 const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath()
@@ -252,7 +271,7 @@ class Emailer {
252 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 271 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
253 } 272 }
254 273
255 addVideoAbuseModeratorsNotification (to: string[], videoAbuse: VideoAbuseModel) { 274 addVideoAbuseModeratorsNotification (to: string[], videoAbuse: MVideoAbuseVideo) {
256 const videoUrl = WEBSERVER.URL + videoAbuse.Video.getWatchStaticPath() 275 const videoUrl = WEBSERVER.URL + videoAbuse.Video.getWatchStaticPath()
257 276
258 const text = `Hi,\n\n` + 277 const text = `Hi,\n\n` +
@@ -269,9 +288,9 @@ class Emailer {
269 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 288 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
270 } 289 }
271 290
272 addVideoAutoBlacklistModeratorsNotification (to: string[], video: VideoModel) { 291 addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) {
273 const VIDEO_AUTO_BLACKLIST_URL = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list' 292 const VIDEO_AUTO_BLACKLIST_URL = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list'
274 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() 293 const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath()
275 294
276 const text = `Hi,\n\n` + 295 const text = `Hi,\n\n` +
277 `A recently added video was auto-blacklisted and requires moderator review before publishing.` + 296 `A recently added video was auto-blacklisted and requires moderator review before publishing.` +
@@ -292,7 +311,7 @@ class Emailer {
292 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 311 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
293 } 312 }
294 313
295 addNewUserRegistrationNotification (to: string[], user: UserModel) { 314 addNewUserRegistrationNotification (to: string[], user: MUser) {
296 const text = `Hi,\n\n` + 315 const text = `Hi,\n\n` +
297 `User ${user.username} just registered on ${WEBSERVER.HOST} PeerTube instance.\n\n` + 316 `User ${user.username} just registered on ${WEBSERVER.HOST} PeerTube instance.\n\n` +
298 `Cheers,\n` + 317 `Cheers,\n` +
@@ -307,7 +326,7 @@ class Emailer {
307 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 326 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
308 } 327 }
309 328
310 addVideoBlacklistNotification (to: string[], videoBlacklist: VideoBlacklistModel) { 329 addVideoBlacklistNotification (to: string[], videoBlacklist: MVideoBlacklistVideo) {
311 const videoName = videoBlacklist.Video.name 330 const videoName = videoBlacklist.Video.name
312 const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath() 331 const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath()
313 332
@@ -329,7 +348,7 @@ class Emailer {
329 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 348 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
330 } 349 }
331 350
332 addVideoUnblacklistNotification (to: string[], video: VideoModel) { 351 addVideoUnblacklistNotification (to: string[], video: MVideo) {
333 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath() 352 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
334 353
335 const text = 'Hi,\n\n' + 354 const text = 'Hi,\n\n' +
@@ -381,7 +400,7 @@ class Emailer {
381 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 400 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
382 } 401 }
383 402
384 addUserBlockJob (user: UserModel, blocked: boolean, reason?: string) { 403 addUserBlockJob (user: MUser, blocked: boolean, reason?: string) {
385 const reasonString = reason ? ` for the following reason: ${reason}` : '' 404 const reasonString = reason ? ` for the following reason: ${reason}` : ''
386 const blockedWord = blocked ? 'blocked' : 'unblocked' 405 const blockedWord = blocked ? 'blocked' : 'unblocked'
387 const blockedString = `Your account ${user.username} on ${WEBSERVER.HOST} has been ${blockedWord}${reasonString}.` 406 const blockedString = `Your account ${user.username} on ${WEBSERVER.HOST} has been ${blockedWord}${reasonString}.`
diff --git a/server/lib/hls.ts b/server/lib/hls.ts
index 98da4dcd8..05136c21c 100644
--- a/server/lib/hls.ts
+++ b/server/lib/hls.ts
@@ -1,4 +1,3 @@
1import { VideoModel } from '../models/video/video'
2import { basename, dirname, join } from 'path' 1import { basename, dirname, join } from 'path'
3import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION } from '../initializers/constants' 2import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION } from '../initializers/constants'
4import { close, ensureDir, move, open, outputJSON, pathExists, read, readFile, remove, writeFile } from 'fs-extra' 3import { close, ensureDir, move, open, outputJSON, pathExists, read, readFile, remove, writeFile } from 'fs-extra'
@@ -12,6 +11,7 @@ import { flatten, uniq } from 'lodash'
12import { VideoFileModel } from '../models/video/video-file' 11import { VideoFileModel } from '../models/video/video-file'
13import { CONFIG } from '../initializers/config' 12import { CONFIG } from '../initializers/config'
14import { sequelizeTypescript } from '../initializers/database' 13import { sequelizeTypescript } from '../initializers/database'
14import { MVideoWithFile } from '@server/typings/models'
15 15
16async function updateStreamingPlaylistsInfohashesIfNeeded () { 16async function updateStreamingPlaylistsInfohashesIfNeeded () {
17 const playlistsToUpdate = await VideoStreamingPlaylistModel.listByIncorrectPeerVersion() 17 const playlistsToUpdate = await VideoStreamingPlaylistModel.listByIncorrectPeerVersion()
@@ -28,7 +28,7 @@ async function updateStreamingPlaylistsInfohashesIfNeeded () {
28 } 28 }
29} 29}
30 30
31async function updateMasterHLSPlaylist (video: VideoModel) { 31async function updateMasterHLSPlaylist (video: MVideoWithFile) {
32 const directory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid) 32 const directory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)
33 const masterPlaylists: string[] = [ '#EXTM3U', '#EXT-X-VERSION:3' ] 33 const masterPlaylists: string[] = [ '#EXTM3U', '#EXT-X-VERSION:3' ]
34 const masterPlaylistPath = join(directory, VideoStreamingPlaylistModel.getMasterHlsPlaylistFilename()) 34 const masterPlaylistPath = join(directory, VideoStreamingPlaylistModel.getMasterHlsPlaylistFilename())
@@ -55,7 +55,7 @@ async function updateMasterHLSPlaylist (video: VideoModel) {
55 await writeFile(masterPlaylistPath, masterPlaylists.join('\n') + '\n') 55 await writeFile(masterPlaylistPath, masterPlaylists.join('\n') + '\n')
56} 56}
57 57
58async function updateSha256Segments (video: VideoModel) { 58async function updateSha256Segments (video: MVideoWithFile) {
59 const json: { [filename: string]: { [range: string]: string } } = {} 59 const json: { [filename: string]: { [range: string]: string } } = {}
60 60
61 const playlistDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid) 61 const playlistDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)
diff --git a/server/lib/job-queue/handlers/activitypub-follow.ts b/server/lib/job-queue/handlers/activitypub-follow.ts
index 4ae66cd01..af7c8a838 100644
--- a/server/lib/job-queue/handlers/activitypub-follow.ts
+++ b/server/lib/job-queue/handlers/activitypub-follow.ts
@@ -10,11 +10,13 @@ import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
10import { ActorModel } from '../../../models/activitypub/actor' 10import { ActorModel } from '../../../models/activitypub/actor'
11import { Notifier } from '../../notifier' 11import { Notifier } from '../../notifier'
12import { sequelizeTypescript } from '../../../initializers/database' 12import { sequelizeTypescript } from '../../../initializers/database'
13import { MActor, MActorFollowActors, MActorFull } from '../../../typings/models'
13 14
14export type ActivitypubFollowPayload = { 15export type ActivitypubFollowPayload = {
15 followerActorId: number 16 followerActorId: number
16 name: string 17 name: string
17 host: string 18 host: string
19 isAutoFollow?: boolean
18} 20}
19 21
20async function processActivityPubFollow (job: Bull.Job) { 22async function processActivityPubFollow (job: Bull.Job) {
@@ -23,18 +25,18 @@ async function processActivityPubFollow (job: Bull.Job) {
23 25
24 logger.info('Processing ActivityPub follow in job %d.', job.id) 26 logger.info('Processing ActivityPub follow in job %d.', job.id)
25 27
26 let targetActor: ActorModel 28 let targetActor: MActorFull
27 if (!host || host === WEBSERVER.HOST) { 29 if (!host || host === WEBSERVER.HOST) {
28 targetActor = await ActorModel.loadLocalByName(payload.name) 30 targetActor = await ActorModel.loadLocalByName(payload.name)
29 } else { 31 } else {
30 const sanitizedHost = sanitizeHost(host, REMOTE_SCHEME.HTTP) 32 const sanitizedHost = sanitizeHost(host, REMOTE_SCHEME.HTTP)
31 const actorUrl = await loadActorUrlOrGetFromWebfinger(payload.name + '@' + sanitizedHost) 33 const actorUrl = await loadActorUrlOrGetFromWebfinger(payload.name + '@' + sanitizedHost)
32 targetActor = await getOrCreateActorAndServerAndModel(actorUrl) 34 targetActor = await getOrCreateActorAndServerAndModel(actorUrl, 'all')
33 } 35 }
34 36
35 const fromActor = await ActorModel.load(payload.followerActorId) 37 const fromActor = await ActorModel.load(payload.followerActorId)
36 38
37 return retryTransactionWrapper(follow, fromActor, targetActor) 39 return retryTransactionWrapper(follow, fromActor, targetActor, payload.isAutoFollow)
38} 40}
39// --------------------------------------------------------------------------- 41// ---------------------------------------------------------------------------
40 42
@@ -44,7 +46,7 @@ export {
44 46
45// --------------------------------------------------------------------------- 47// ---------------------------------------------------------------------------
46 48
47async function follow (fromActor: ActorModel, targetActor: ActorModel) { 49async function follow (fromActor: MActor, targetActor: MActorFull, isAutoFollow = false) {
48 if (fromActor.id === targetActor.id) { 50 if (fromActor.id === targetActor.id) {
49 throw new Error('Follower is the same than target actor.') 51 throw new Error('Follower is the same than target actor.')
50 } 52 }
@@ -53,7 +55,7 @@ async function follow (fromActor: ActorModel, targetActor: ActorModel) {
53 const state = !fromActor.serverId && !targetActor.serverId ? 'accepted' : 'pending' 55 const state = !fromActor.serverId && !targetActor.serverId ? 'accepted' : 'pending'
54 56
55 const actorFollow = await sequelizeTypescript.transaction(async t => { 57 const actorFollow = await sequelizeTypescript.transaction(async t => {
56 const [ actorFollow ] = await ActorFollowModel.findOrCreate({ 58 const [ actorFollow ] = await ActorFollowModel.findOrCreate<MActorFollowActors>({
57 where: { 59 where: {
58 actorId: fromActor.id, 60 actorId: fromActor.id,
59 targetActorId: targetActor.id 61 targetActorId: targetActor.id
@@ -74,5 +76,15 @@ async function follow (fromActor: ActorModel, targetActor: ActorModel) {
74 return actorFollow 76 return actorFollow
75 }) 77 })
76 78
77 if (actorFollow.state === 'accepted') Notifier.Instance.notifyOfNewUserFollow(actorFollow) 79 const followerFull = await ActorModel.loadFull(fromActor.id)
80
81 const actorFollowFull = Object.assign(actorFollow, {
82 ActorFollowing: targetActor,
83 ActorFollower: followerFull
84 })
85
86 if (actorFollow.state === 'accepted') Notifier.Instance.notifyOfNewUserFollow(actorFollowFull)
87 if (isAutoFollow === true) Notifier.Instance.notifyOfAutoInstanceFollowing(actorFollowFull)
88
89 return actorFollow
78} 90}
diff --git a/server/lib/job-queue/handlers/activitypub-http-fetcher.ts b/server/lib/job-queue/handlers/activitypub-http-fetcher.ts
index c3f59dc77..0182c5169 100644
--- a/server/lib/job-queue/handlers/activitypub-http-fetcher.ts
+++ b/server/lib/job-queue/handlers/activitypub-http-fetcher.ts
@@ -11,6 +11,7 @@ import { AccountModel } from '../../../models/account/account'
11import { AccountVideoRateModel } from '../../../models/account/account-video-rate' 11import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
12import { VideoShareModel } from '../../../models/video/video-share' 12import { VideoShareModel } from '../../../models/video/video-share'
13import { VideoCommentModel } from '../../../models/video/video-comment' 13import { VideoCommentModel } from '../../../models/video/video-comment'
14import { MAccountDefault, MVideoFullLight } from '../../../typings/models'
14 15
15type FetchType = 'activity' | 'video-likes' | 'video-dislikes' | 'video-shares' | 'video-comments' | 'account-playlists' 16type FetchType = 'activity' | 'video-likes' | 'video-dislikes' | 'video-shares' | 'video-comments' | 'account-playlists'
16 17
@@ -26,10 +27,10 @@ async function processActivityPubHttpFetcher (job: Bull.Job) {
26 27
27 const payload = job.data as ActivitypubHttpFetcherPayload 28 const payload = job.data as ActivitypubHttpFetcherPayload
28 29
29 let video: VideoModel 30 let video: MVideoFullLight
30 if (payload.videoId) video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoId) 31 if (payload.videoId) video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoId)
31 32
32 let account: AccountModel 33 let account: MAccountDefault
33 if (payload.accountId) account = await AccountModel.load(payload.accountId) 34 if (payload.accountId) account = await AccountModel.load(payload.accountId)
34 35
35 const fetcherType: { [ id in FetchType ]: (items: any[]) => Promise<any> } = { 36 const fetcherType: { [ id in FetchType ]: (items: any[]) => Promise<any> } = {
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 cdee1f6fd..d3bde6e6a 100644
--- a/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts
+++ b/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts
@@ -3,6 +3,7 @@ import { getServerActor } from '../../../../helpers/utils'
3import { ActorModel } from '../../../../models/activitypub/actor' 3import { ActorModel } from '../../../../models/activitypub/actor'
4import { sha256 } from '../../../../helpers/core-utils' 4import { sha256 } from '../../../../helpers/core-utils'
5import { HTTP_SIGNATURE } from '../../../../initializers/constants' 5import { HTTP_SIGNATURE } from '../../../../initializers/constants'
6import { MActor } from '../../../../typings/models'
6 7
7type Payload = { body: any, signatureActorId?: number } 8type Payload = { body: any, signatureActorId?: number }
8 9
@@ -19,7 +20,8 @@ async function computeBody (payload: Payload) {
19} 20}
20 21
21async function buildSignedRequestOptions (payload: Payload) { 22async function buildSignedRequestOptions (payload: Payload) {
22 let actor: ActorModel | null 23 let actor: MActor | null
24
23 if (payload.signatureActorId) { 25 if (payload.signatureActorId) {
24 actor = await ActorModel.load(payload.signatureActorId) 26 actor = await ActorModel.load(payload.signatureActorId)
25 if (!actor) throw new Error('Unknown signature actor id.') 27 if (!actor) throw new Error('Unknown signature actor id.')
diff --git a/server/lib/job-queue/handlers/video-file-import.ts b/server/lib/job-queue/handlers/video-file-import.ts
index 8cacb0ef3..5c5b7dccb 100644
--- a/server/lib/job-queue/handlers/video-file-import.ts
+++ b/server/lib/job-queue/handlers/video-file-import.ts
@@ -6,6 +6,7 @@ import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg
6import { copy, stat } from 'fs-extra' 6import { copy, stat } from 'fs-extra'
7import { VideoFileModel } from '../../../models/video/video-file' 7import { VideoFileModel } from '../../../models/video/video-file'
8import { extname } from 'path' 8import { extname } from 'path'
9import { MVideoFile, MVideoWithFile } from '@server/typings/models'
9 10
10export type VideoFileImportPayload = { 11export type VideoFileImportPayload = {
11 videoUUID: string, 12 videoUUID: string,
@@ -37,7 +38,7 @@ export {
37 38
38// --------------------------------------------------------------------------- 39// ---------------------------------------------------------------------------
39 40
40async function updateVideoFile (video: VideoModel, inputFilePath: string) { 41async function updateVideoFile (video: MVideoWithFile, inputFilePath: string) {
41 const { videoFileResolution } = await getVideoFileResolution(inputFilePath) 42 const { videoFileResolution } = await getVideoFileResolution(inputFilePath)
42 const { size } = await stat(inputFilePath) 43 const { size } = await stat(inputFilePath)
43 const fps = await getVideoFileFPS(inputFilePath) 44 const fps = await getVideoFileFPS(inputFilePath)
@@ -48,7 +49,7 @@ async function updateVideoFile (video: VideoModel, inputFilePath: string) {
48 size, 49 size,
49 fps, 50 fps,
50 videoId: video.id 51 videoId: video.id
51 }) 52 }) as MVideoFile
52 53
53 const currentVideoFile = video.VideoFiles.find(videoFile => videoFile.resolution === updatedVideoFile.resolution) 54 const currentVideoFile = video.VideoFiles.find(videoFile => videoFile.resolution === updatedVideoFile.resolution)
54 55
@@ -60,9 +61,9 @@ async function updateVideoFile (video: VideoModel, inputFilePath: string) {
60 video.VideoFiles = video.VideoFiles.filter(f => f !== currentVideoFile) 61 video.VideoFiles = video.VideoFiles.filter(f => f !== currentVideoFile)
61 62
62 // Update the database 63 // Update the database
63 currentVideoFile.set('extname', updatedVideoFile.extname) 64 currentVideoFile.extname = updatedVideoFile.extname
64 currentVideoFile.set('size', updatedVideoFile.size) 65 currentVideoFile.size = updatedVideoFile.size
65 currentVideoFile.set('fps', updatedVideoFile.fps) 66 currentVideoFile.fps = updatedVideoFile.fps
66 67
67 updatedVideoFile = currentVideoFile 68 updatedVideoFile = currentVideoFile
68 } 69 }
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts
index 13b741180..93a3e9d90 100644
--- a/server/lib/job-queue/handlers/video-import.ts
+++ b/server/lib/job-queue/handlers/video-import.ts
@@ -17,9 +17,11 @@ import { move, remove, stat } from 'fs-extra'
17import { Notifier } from '../../notifier' 17import { Notifier } from '../../notifier'
18import { CONFIG } from '../../../initializers/config' 18import { CONFIG } from '../../../initializers/config'
19import { sequelizeTypescript } from '../../../initializers/database' 19import { sequelizeTypescript } from '../../../initializers/database'
20import { ThumbnailModel } from '../../../models/video/thumbnail'
21import { createVideoMiniatureFromUrl, generateVideoMiniature } from '../../thumbnail' 20import { createVideoMiniatureFromUrl, generateVideoMiniature } from '../../thumbnail'
22import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' 21import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
22import { MThumbnail } from '../../../typings/models/video/thumbnail'
23import { MVideoImportDefault, MVideoImportDefaultFiles, MVideoImportVideo } from '@server/typings/models/video/video-import'
24import { MVideoBlacklistVideo, MVideoBlacklist } from '@server/typings/models'
23 25
24type VideoImportYoutubeDLPayload = { 26type VideoImportYoutubeDLPayload = {
25 type: 'youtube-dl' 27 type: 'youtube-dl'
@@ -110,7 +112,7 @@ type ProcessFileOptions = {
110 generateThumbnail: boolean 112 generateThumbnail: boolean
111 generatePreview: boolean 113 generatePreview: boolean
112} 114}
113async function processFile (downloader: () => Promise<string>, videoImport: VideoImportModel, options: ProcessFileOptions) { 115async function processFile (downloader: () => Promise<string>, videoImport: MVideoImportDefault, options: ProcessFileOptions) {
114 let tempVideoPath: string 116 let tempVideoPath: string
115 let videoDestFile: string 117 let videoDestFile: string
116 let videoFile: VideoFileModel 118 let videoFile: VideoFileModel
@@ -139,41 +141,44 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
139 videoId: videoImport.videoId 141 videoId: videoImport.videoId
140 } 142 }
141 videoFile = new VideoFileModel(videoFileData) 143 videoFile = new VideoFileModel(videoFileData)
144
145 const videoWithFiles = Object.assign(videoImport.Video, { VideoFiles: [ videoFile ] })
142 // To clean files if the import fails 146 // To clean files if the import fails
143 videoImport.Video.VideoFiles = [ videoFile ] 147 const videoImportWithFiles: MVideoImportDefaultFiles = Object.assign(videoImport, { Video: videoWithFiles })
144 148
145 // Move file 149 // Move file
146 videoDestFile = join(CONFIG.STORAGE.VIDEOS_DIR, videoImport.Video.getVideoFilename(videoFile)) 150 videoDestFile = join(CONFIG.STORAGE.VIDEOS_DIR, videoImportWithFiles.Video.getVideoFilename(videoFile))
147 await move(tempVideoPath, videoDestFile) 151 await move(tempVideoPath, videoDestFile)
148 tempVideoPath = null // This path is not used anymore 152 tempVideoPath = null // This path is not used anymore
149 153
150 // Process thumbnail 154 // Process thumbnail
151 let thumbnailModel: ThumbnailModel 155 let thumbnailModel: MThumbnail
152 if (options.downloadThumbnail && options.thumbnailUrl) { 156 if (options.downloadThumbnail && options.thumbnailUrl) {
153 thumbnailModel = await createVideoMiniatureFromUrl(options.thumbnailUrl, videoImport.Video, ThumbnailType.MINIATURE) 157 thumbnailModel = await createVideoMiniatureFromUrl(options.thumbnailUrl, videoImportWithFiles.Video, ThumbnailType.MINIATURE)
154 } else if (options.generateThumbnail || options.downloadThumbnail) { 158 } else if (options.generateThumbnail || options.downloadThumbnail) {
155 thumbnailModel = await generateVideoMiniature(videoImport.Video, videoFile, ThumbnailType.MINIATURE) 159 thumbnailModel = await generateVideoMiniature(videoImportWithFiles.Video, videoFile, ThumbnailType.MINIATURE)
156 } 160 }
157 161
158 // Process preview 162 // Process preview
159 let previewModel: ThumbnailModel 163 let previewModel: MThumbnail
160 if (options.downloadPreview && options.thumbnailUrl) { 164 if (options.downloadPreview && options.thumbnailUrl) {
161 previewModel = await createVideoMiniatureFromUrl(options.thumbnailUrl, videoImport.Video, ThumbnailType.PREVIEW) 165 previewModel = await createVideoMiniatureFromUrl(options.thumbnailUrl, videoImportWithFiles.Video, ThumbnailType.PREVIEW)
162 } else if (options.generatePreview || options.downloadPreview) { 166 } else if (options.generatePreview || options.downloadPreview) {
163 previewModel = await generateVideoMiniature(videoImport.Video, videoFile, ThumbnailType.PREVIEW) 167 previewModel = await generateVideoMiniature(videoImportWithFiles.Video, videoFile, ThumbnailType.PREVIEW)
164 } 168 }
165 169
166 // Create torrent 170 // Create torrent
167 await videoImport.Video.createTorrentAndSetInfoHash(videoFile) 171 await videoImportWithFiles.Video.createTorrentAndSetInfoHash(videoFile)
172
173 const { videoImportUpdated, video } = await sequelizeTypescript.transaction(async t => {
174 const videoImportToUpdate = videoImportWithFiles as MVideoImportVideo
168 175
169 const videoImportUpdated: VideoImportModel = await sequelizeTypescript.transaction(async t => {
170 // Refresh video 176 // Refresh video
171 const video = await VideoModel.load(videoImport.videoId, t) 177 const video = await VideoModel.load(videoImportToUpdate.videoId, t)
172 if (!video) throw new Error('Video linked to import ' + videoImport.videoId + ' does not exist anymore.') 178 if (!video) throw new Error('Video linked to import ' + videoImportToUpdate.videoId + ' does not exist anymore.')
173 videoImport.Video = video
174 179
175 const videoFileCreated = await videoFile.save({ transaction: t }) 180 const videoFileCreated = await videoFile.save({ transaction: t })
176 video.VideoFiles = [ videoFileCreated ] 181 videoImportToUpdate.Video = Object.assign(video, { VideoFiles: [ videoFileCreated ] })
177 182
178 // Update video DB object 183 // Update video DB object
179 video.duration = duration 184 video.duration = duration
@@ -188,25 +193,27 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
188 await federateVideoIfNeeded(videoForFederation, true, t) 193 await federateVideoIfNeeded(videoForFederation, true, t)
189 194
190 // Update video import object 195 // Update video import object
191 videoImport.state = VideoImportState.SUCCESS 196 videoImportToUpdate.state = VideoImportState.SUCCESS
192 const videoImportUpdated = await videoImport.save({ transaction: t }) 197 const videoImportUpdated = await videoImportToUpdate.save({ transaction: t }) as MVideoImportVideo
198 videoImportUpdated.Video = video
193 199
194 logger.info('Video %s imported.', video.uuid) 200 logger.info('Video %s imported.', video.uuid)
195 201
196 videoImportUpdated.Video = videoForFederation 202 return { videoImportUpdated, video: videoForFederation }
197 return videoImportUpdated
198 }) 203 })
199 204
200 Notifier.Instance.notifyOnFinishedVideoImport(videoImportUpdated, true) 205 Notifier.Instance.notifyOnFinishedVideoImport(videoImportUpdated, true)
201 206
202 if (videoImportUpdated.Video.isBlacklisted()) { 207 if (video.isBlacklisted()) {
203 Notifier.Instance.notifyOnVideoAutoBlacklist(videoImportUpdated.Video) 208 const videoBlacklist = Object.assign(video.VideoBlacklist, { Video: video })
209
210 Notifier.Instance.notifyOnVideoAutoBlacklist(videoBlacklist)
204 } else { 211 } else {
205 Notifier.Instance.notifyOnNewVideoIfNeeded(videoImportUpdated.Video) 212 Notifier.Instance.notifyOnNewVideoIfNeeded(video)
206 } 213 }
207 214
208 // Create transcoding jobs? 215 // Create transcoding jobs?
209 if (videoImportUpdated.Video.state === VideoState.TO_TRANSCODE) { 216 if (video.state === VideoState.TO_TRANSCODE) {
210 // Put uuid because we don't have id auto incremented for now 217 // Put uuid because we don't have id auto incremented for now
211 const dataInput = { 218 const dataInput = {
212 type: 'optimize' as 'optimize', 219 type: 'optimize' as 'optimize',
diff --git a/server/lib/job-queue/handlers/video-transcoding.ts b/server/lib/job-queue/handlers/video-transcoding.ts
index 981daf9a1..2ebe15bcb 100644
--- a/server/lib/job-queue/handlers/video-transcoding.ts
+++ b/server/lib/job-queue/handlers/video-transcoding.ts
@@ -11,6 +11,7 @@ import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils'
11import { generateHlsPlaylist, optimizeVideofile, transcodeOriginalVideofile, mergeAudioVideofile } from '../../video-transcoding' 11import { generateHlsPlaylist, optimizeVideofile, transcodeOriginalVideofile, mergeAudioVideofile } from '../../video-transcoding'
12import { Notifier } from '../../notifier' 12import { Notifier } from '../../notifier'
13import { CONFIG } from '../../../initializers/config' 13import { CONFIG } from '../../../initializers/config'
14import { MVideoUUID, MVideoWithFile } from '@server/typings/models'
14 15
15interface BaseTranscodingPayload { 16interface BaseTranscodingPayload {
16 videoUUID: string 17 videoUUID: string
@@ -73,7 +74,7 @@ async function processVideoTranscoding (job: Bull.Job) {
73 return video 74 return video
74} 75}
75 76
76async function onHlsPlaylistGenerationSuccess (video: VideoModel) { 77async function onHlsPlaylistGenerationSuccess (video: MVideoUUID) {
77 if (video === undefined) return undefined 78 if (video === undefined) return undefined
78 79
79 await sequelizeTypescript.transaction(async t => { 80 await sequelizeTypescript.transaction(async t => {
@@ -87,7 +88,7 @@ async function onHlsPlaylistGenerationSuccess (video: VideoModel) {
87 }) 88 })
88} 89}
89 90
90async function publishNewResolutionIfNeeded (video: VideoModel, payload?: NewResolutionTranscodingPayload | MergeAudioTranscodingPayload) { 91async function publishNewResolutionIfNeeded (video: MVideoUUID, payload?: NewResolutionTranscodingPayload | MergeAudioTranscodingPayload) {
91 const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => { 92 const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => {
92 // Maybe the video changed in database, refresh it 93 // Maybe the video changed in database, refresh it
93 let videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t) 94 let videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t)
@@ -119,7 +120,7 @@ async function publishNewResolutionIfNeeded (video: VideoModel, payload?: NewRes
119 await createHlsJobIfEnabled(payload) 120 await createHlsJobIfEnabled(payload)
120} 121}
121 122
122async function onVideoFileOptimizerSuccess (videoArg: VideoModel, payload: OptimizeTranscodingPayload) { 123async function onVideoFileOptimizerSuccess (videoArg: MVideoWithFile, payload: OptimizeTranscodingPayload) {
123 if (videoArg === undefined) return undefined 124 if (videoArg === undefined) return undefined
124 125
125 // Outside the transaction (IO on disk) 126 // Outside the transaction (IO on disk)
diff --git a/server/lib/notifier.ts b/server/lib/notifier.ts
index a7dfb0979..b7cc2607d 100644
--- a/server/lib/notifier.ts
+++ b/server/lib/notifier.ts
@@ -1,20 +1,30 @@
1import { UserNotificationSettingValue, UserNotificationType, UserRight } from '../../shared/models/users' 1import { UserNotificationSettingValue, UserNotificationType, UserRight } from '../../shared/models/users'
2import { logger } from '../helpers/logger' 2import { logger } from '../helpers/logger'
3import { VideoModel } from '../models/video/video'
4import { Emailer } from './emailer' 3import { Emailer } from './emailer'
5import { UserNotificationModel } from '../models/account/user-notification' 4import { UserNotificationModel } from '../models/account/user-notification'
6import { VideoCommentModel } from '../models/video/video-comment'
7import { UserModel } from '../models/account/user' 5import { UserModel } from '../models/account/user'
8import { PeerTubeSocket } from './peertube-socket' 6import { PeerTubeSocket } from './peertube-socket'
9import { CONFIG } from '../initializers/config' 7import { CONFIG } from '../initializers/config'
10import { VideoPrivacy, VideoState } from '../../shared/models/videos' 8import { VideoPrivacy, VideoState } from '../../shared/models/videos'
11import { VideoAbuseModel } from '../models/video/video-abuse'
12import { VideoBlacklistModel } from '../models/video/video-blacklist'
13import * as Bluebird from 'bluebird' 9import * as Bluebird from 'bluebird'
14import { VideoImportModel } from '../models/video/video-import'
15import { AccountBlocklistModel } from '../models/account/account-blocklist' 10import { AccountBlocklistModel } from '../models/account/account-blocklist'
16import { ActorFollowModel } from '../models/activitypub/actor-follow' 11import {
17import { AccountModel } from '../models/account/account' 12 MCommentOwnerVideo,
13 MVideoAbuseVideo,
14 MVideoAccountLight,
15 MVideoBlacklistLightVideo,
16 MVideoBlacklistVideo,
17 MVideoFullLight
18} from '../typings/models/video'
19import {
20 MUser,
21 MUserDefault,
22 MUserNotifSettingAccount,
23 MUserWithNotificationSetting,
24 UserNotificationModelForApi
25} from '@server/typings/models/user'
26import { MActorFollowFull } from '../typings/models'
27import { MVideoImportVideo } from '@server/typings/models/video/video-import'
18 28
19class Notifier { 29class Notifier {
20 30
@@ -22,7 +32,7 @@ class Notifier {
22 32
23 private constructor () {} 33 private constructor () {}
24 34
25 notifyOnNewVideoIfNeeded (video: VideoModel): void { 35 notifyOnNewVideoIfNeeded (video: MVideoAccountLight): void {
26 // Only notify on public and published videos which are not blacklisted 36 // Only notify on public and published videos which are not blacklisted
27 if (video.privacy !== VideoPrivacy.PUBLIC || video.state !== VideoState.PUBLISHED || video.isBlacklisted()) return 37 if (video.privacy !== VideoPrivacy.PUBLIC || video.state !== VideoState.PUBLISHED || video.isBlacklisted()) return
28 38
@@ -30,7 +40,7 @@ class Notifier {
30 .catch(err => logger.error('Cannot notify subscribers of new video %s.', video.url, { err })) 40 .catch(err => logger.error('Cannot notify subscribers of new video %s.', video.url, { err }))
31 } 41 }
32 42
33 notifyOnVideoPublishedAfterTranscoding (video: VideoModel): void { 43 notifyOnVideoPublishedAfterTranscoding (video: MVideoFullLight): void {
34 // don't notify if didn't wait for transcoding or video is still blacklisted/waiting for scheduled update 44 // don't notify if didn't wait for transcoding or video is still blacklisted/waiting for scheduled update
35 if (!video.waitTranscoding || video.VideoBlacklist || video.ScheduleVideoUpdate) return 45 if (!video.waitTranscoding || video.VideoBlacklist || video.ScheduleVideoUpdate) return
36 46
@@ -38,7 +48,7 @@ class Notifier {
38 .catch(err => logger.error('Cannot notify owner that its video %s has been published after transcoding.', video.url, { err })) 48 .catch(err => logger.error('Cannot notify owner that its video %s has been published after transcoding.', video.url, { err }))
39 } 49 }
40 50
41 notifyOnVideoPublishedAfterScheduledUpdate (video: VideoModel): void { 51 notifyOnVideoPublishedAfterScheduledUpdate (video: MVideoFullLight): void {
42 // don't notify if video is still blacklisted or waiting for transcoding 52 // don't notify if video is still blacklisted or waiting for transcoding
43 if (video.VideoBlacklist || (video.waitTranscoding && video.state !== VideoState.PUBLISHED)) return 53 if (video.VideoBlacklist || (video.waitTranscoding && video.state !== VideoState.PUBLISHED)) return
44 54
@@ -46,7 +56,7 @@ class Notifier {
46 .catch(err => logger.error('Cannot notify owner that its video %s has been published after scheduled update.', video.url, { err })) 56 .catch(err => logger.error('Cannot notify owner that its video %s has been published after scheduled update.', video.url, { err }))
47 } 57 }
48 58
49 notifyOnVideoPublishedAfterRemovedFromAutoBlacklist (video: VideoModel): void { 59 notifyOnVideoPublishedAfterRemovedFromAutoBlacklist (video: MVideoFullLight): void {
50 // don't notify if video is still waiting for transcoding or scheduled update 60 // don't notify if video is still waiting for transcoding or scheduled update
51 if (video.ScheduleVideoUpdate || (video.waitTranscoding && video.state !== VideoState.PUBLISHED)) return 61 if (video.ScheduleVideoUpdate || (video.waitTranscoding && video.state !== VideoState.PUBLISHED)) return
52 62
@@ -54,7 +64,7 @@ class Notifier {
54 .catch(err => logger.error('Cannot notify owner that its video %s has been published after removed from auto-blacklist.', video.url, { err })) // tslint:disable-line:max-line-length 64 .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
55 } 65 }
56 66
57 notifyOnNewComment (comment: VideoCommentModel): void { 67 notifyOnNewComment (comment: MCommentOwnerVideo): void {
58 this.notifyVideoOwnerOfNewComment(comment) 68 this.notifyVideoOwnerOfNewComment(comment)
59 .catch(err => logger.error('Cannot notify video owner of new comment %s.', comment.url, { err })) 69 .catch(err => logger.error('Cannot notify video owner of new comment %s.', comment.url, { err }))
60 70
@@ -62,37 +72,37 @@ class Notifier {
62 .catch(err => logger.error('Cannot notify mentions of comment %s.', comment.url, { err })) 72 .catch(err => logger.error('Cannot notify mentions of comment %s.', comment.url, { err }))
63 } 73 }
64 74
65 notifyOnNewVideoAbuse (videoAbuse: VideoAbuseModel): void { 75 notifyOnNewVideoAbuse (videoAbuse: MVideoAbuseVideo): void {
66 this.notifyModeratorsOfNewVideoAbuse(videoAbuse) 76 this.notifyModeratorsOfNewVideoAbuse(videoAbuse)
67 .catch(err => logger.error('Cannot notify of new video abuse of video %s.', videoAbuse.Video.url, { err })) 77 .catch(err => logger.error('Cannot notify of new video abuse of video %s.', videoAbuse.Video.url, { err }))
68 } 78 }
69 79
70 notifyOnVideoAutoBlacklist (video: VideoModel): void { 80 notifyOnVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo): void {
71 this.notifyModeratorsOfVideoAutoBlacklist(video) 81 this.notifyModeratorsOfVideoAutoBlacklist(videoBlacklist)
72 .catch(err => logger.error('Cannot notify of auto-blacklist of video %s.', video.url, { err })) 82 .catch(err => logger.error('Cannot notify of auto-blacklist of video %s.', videoBlacklist.Video.url, { err }))
73 } 83 }
74 84
75 notifyOnVideoBlacklist (videoBlacklist: VideoBlacklistModel): void { 85 notifyOnVideoBlacklist (videoBlacklist: MVideoBlacklistVideo): void {
76 this.notifyVideoOwnerOfBlacklist(videoBlacklist) 86 this.notifyVideoOwnerOfBlacklist(videoBlacklist)
77 .catch(err => logger.error('Cannot notify video owner of new video blacklist of %s.', videoBlacklist.Video.url, { err })) 87 .catch(err => logger.error('Cannot notify video owner of new video blacklist of %s.', videoBlacklist.Video.url, { err }))
78 } 88 }
79 89
80 notifyOnVideoUnblacklist (video: VideoModel): void { 90 notifyOnVideoUnblacklist (video: MVideoFullLight): void {
81 this.notifyVideoOwnerOfUnblacklist(video) 91 this.notifyVideoOwnerOfUnblacklist(video)
82 .catch(err => logger.error('Cannot notify video owner of unblacklist of %s.', video.url, { err })) 92 .catch(err => logger.error('Cannot notify video owner of unblacklist of %s.', video.url, { err }))
83 } 93 }
84 94
85 notifyOnFinishedVideoImport (videoImport: VideoImportModel, success: boolean): void { 95 notifyOnFinishedVideoImport (videoImport: MVideoImportVideo, success: boolean): void {
86 this.notifyOwnerVideoImportIsFinished(videoImport, success) 96 this.notifyOwnerVideoImportIsFinished(videoImport, success)
87 .catch(err => logger.error('Cannot notify owner that its video import %s is finished.', videoImport.getTargetIdentifier(), { err })) 97 .catch(err => logger.error('Cannot notify owner that its video import %s is finished.', videoImport.getTargetIdentifier(), { err }))
88 } 98 }
89 99
90 notifyOnNewUserRegistration (user: UserModel): void { 100 notifyOnNewUserRegistration (user: MUserDefault): void {
91 this.notifyModeratorsOfNewUserRegistration(user) 101 this.notifyModeratorsOfNewUserRegistration(user)
92 .catch(err => logger.error('Cannot notify moderators of new user registration (%s).', user.username, { err })) 102 .catch(err => logger.error('Cannot notify moderators of new user registration (%s).', user.username, { err }))
93 } 103 }
94 104
95 notifyOfNewUserFollow (actorFollow: ActorFollowModel): void { 105 notifyOfNewUserFollow (actorFollow: MActorFollowFull): void {
96 this.notifyUserOfNewActorFollow(actorFollow) 106 this.notifyUserOfNewActorFollow(actorFollow)
97 .catch(err => { 107 .catch(err => {
98 logger.error( 108 logger.error(
@@ -104,25 +114,32 @@ class Notifier {
104 }) 114 })
105 } 115 }
106 116
107 notifyOfNewInstanceFollow (actorFollow: ActorFollowModel): void { 117 notifyOfNewInstanceFollow (actorFollow: MActorFollowFull): void {
108 this.notifyAdminsOfNewInstanceFollow(actorFollow) 118 this.notifyAdminsOfNewInstanceFollow(actorFollow)
109 .catch(err => { 119 .catch(err => {
110 logger.error('Cannot notify administrators of new follower %s.', actorFollow.ActorFollower.url, { err }) 120 logger.error('Cannot notify administrators of new follower %s.', actorFollow.ActorFollower.url, { err })
111 }) 121 })
112 } 122 }
113 123
114 private async notifySubscribersOfNewVideo (video: VideoModel) { 124 notifyOfAutoInstanceFollowing (actorFollow: MActorFollowFull): void {
125 this.notifyAdminsOfAutoInstanceFollowing(actorFollow)
126 .catch(err => {
127 logger.error('Cannot notify administrators of auto instance following %s.', actorFollow.ActorFollowing.url, { err })
128 })
129 }
130
131 private async notifySubscribersOfNewVideo (video: MVideoAccountLight) {
115 // List all followers that are users 132 // List all followers that are users
116 const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId) 133 const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId)
117 134
118 logger.info('Notifying %d users of new video %s.', users.length, video.url) 135 logger.info('Notifying %d users of new video %s.', users.length, video.url)
119 136
120 function settingGetter (user: UserModel) { 137 function settingGetter (user: MUserWithNotificationSetting) {
121 return user.NotificationSetting.newVideoFromSubscription 138 return user.NotificationSetting.newVideoFromSubscription
122 } 139 }
123 140
124 async function notificationCreator (user: UserModel) { 141 async function notificationCreator (user: MUserWithNotificationSetting) {
125 const notification = await UserNotificationModel.create({ 142 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
126 type: UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION, 143 type: UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION,
127 userId: user.id, 144 userId: user.id,
128 videoId: video.id 145 videoId: video.id
@@ -139,7 +156,7 @@ class Notifier {
139 return this.notify({ users, settingGetter, notificationCreator, emailSender }) 156 return this.notify({ users, settingGetter, notificationCreator, emailSender })
140 } 157 }
141 158
142 private async notifyVideoOwnerOfNewComment (comment: VideoCommentModel) { 159 private async notifyVideoOwnerOfNewComment (comment: MCommentOwnerVideo) {
143 if (comment.Video.isOwned() === false) return 160 if (comment.Video.isOwned() === false) return
144 161
145 const user = await UserModel.loadByVideoId(comment.videoId) 162 const user = await UserModel.loadByVideoId(comment.videoId)
@@ -152,12 +169,12 @@ class Notifier {
152 169
153 logger.info('Notifying user %s of new comment %s.', user.username, comment.url) 170 logger.info('Notifying user %s of new comment %s.', user.username, comment.url)
154 171
155 function settingGetter (user: UserModel) { 172 function settingGetter (user: MUserWithNotificationSetting) {
156 return user.NotificationSetting.newCommentOnMyVideo 173 return user.NotificationSetting.newCommentOnMyVideo
157 } 174 }
158 175
159 async function notificationCreator (user: UserModel) { 176 async function notificationCreator (user: MUserWithNotificationSetting) {
160 const notification = await UserNotificationModel.create({ 177 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
161 type: UserNotificationType.NEW_COMMENT_ON_MY_VIDEO, 178 type: UserNotificationType.NEW_COMMENT_ON_MY_VIDEO,
162 userId: user.id, 179 userId: user.id,
163 commentId: comment.id 180 commentId: comment.id
@@ -174,7 +191,7 @@ class Notifier {
174 return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender }) 191 return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender })
175 } 192 }
176 193
177 private async notifyOfCommentMention (comment: VideoCommentModel) { 194 private async notifyOfCommentMention (comment: MCommentOwnerVideo) {
178 const extractedUsernames = comment.extractMentions() 195 const extractedUsernames = comment.extractMentions()
179 logger.debug( 196 logger.debug(
180 'Extracted %d username from comment %s.', extractedUsernames.length, comment.url, 197 'Extracted %d username from comment %s.', extractedUsernames.length, comment.url,
@@ -197,14 +214,14 @@ class Notifier {
197 214
198 logger.info('Notifying %d users of new comment %s.', users.length, comment.url) 215 logger.info('Notifying %d users of new comment %s.', users.length, comment.url)
199 216
200 function settingGetter (user: UserModel) { 217 function settingGetter (user: MUserNotifSettingAccount) {
201 if (accountMutedHash[user.Account.id] === true) return UserNotificationSettingValue.NONE 218 if (accountMutedHash[user.Account.id] === true) return UserNotificationSettingValue.NONE
202 219
203 return user.NotificationSetting.commentMention 220 return user.NotificationSetting.commentMention
204 } 221 }
205 222
206 async function notificationCreator (user: UserModel) { 223 async function notificationCreator (user: MUserNotifSettingAccount) {
207 const notification = await UserNotificationModel.create({ 224 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
208 type: UserNotificationType.COMMENT_MENTION, 225 type: UserNotificationType.COMMENT_MENTION,
209 userId: user.id, 226 userId: user.id,
210 commentId: comment.id 227 commentId: comment.id
@@ -221,7 +238,7 @@ class Notifier {
221 return this.notify({ users, settingGetter, notificationCreator, emailSender }) 238 return this.notify({ users, settingGetter, notificationCreator, emailSender })
222 } 239 }
223 240
224 private async notifyUserOfNewActorFollow (actorFollow: ActorFollowModel) { 241 private async notifyUserOfNewActorFollow (actorFollow: MActorFollowFull) {
225 if (actorFollow.ActorFollowing.isOwned() === false) return 242 if (actorFollow.ActorFollowing.isOwned() === false) return
226 243
227 // Account follows one of our account? 244 // Account follows one of our account?
@@ -236,9 +253,6 @@ class Notifier {
236 253
237 if (!user) return 254 if (!user) return
238 255
239 if (!actorFollow.ActorFollower.Account || !actorFollow.ActorFollower.Account.name) {
240 actorFollow.ActorFollower.Account = await actorFollow.ActorFollower.$get('Account') as AccountModel
241 }
242 const followerAccount = actorFollow.ActorFollower.Account 256 const followerAccount = actorFollow.ActorFollower.Account
243 257
244 const accountMuted = await AccountBlocklistModel.isAccountMutedBy(user.Account.id, followerAccount.id) 258 const accountMuted = await AccountBlocklistModel.isAccountMutedBy(user.Account.id, followerAccount.id)
@@ -246,12 +260,12 @@ class Notifier {
246 260
247 logger.info('Notifying user %s of new follower: %s.', user.username, followerAccount.getDisplayName()) 261 logger.info('Notifying user %s of new follower: %s.', user.username, followerAccount.getDisplayName())
248 262
249 function settingGetter (user: UserModel) { 263 function settingGetter (user: MUserWithNotificationSetting) {
250 return user.NotificationSetting.newFollow 264 return user.NotificationSetting.newFollow
251 } 265 }
252 266
253 async function notificationCreator (user: UserModel) { 267 async function notificationCreator (user: MUserWithNotificationSetting) {
254 const notification = await UserNotificationModel.create({ 268 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
255 type: UserNotificationType.NEW_FOLLOW, 269 type: UserNotificationType.NEW_FOLLOW,
256 userId: user.id, 270 userId: user.id,
257 actorFollowId: actorFollow.id 271 actorFollowId: actorFollow.id
@@ -268,17 +282,17 @@ class Notifier {
268 return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender }) 282 return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender })
269 } 283 }
270 284
271 private async notifyAdminsOfNewInstanceFollow (actorFollow: ActorFollowModel) { 285 private async notifyAdminsOfNewInstanceFollow (actorFollow: MActorFollowFull) {
272 const admins = await UserModel.listWithRight(UserRight.MANAGE_SERVER_FOLLOW) 286 const admins = await UserModel.listWithRight(UserRight.MANAGE_SERVER_FOLLOW)
273 287
274 logger.info('Notifying %d administrators of new instance follower: %s.', admins.length, actorFollow.ActorFollower.url) 288 logger.info('Notifying %d administrators of new instance follower: %s.', admins.length, actorFollow.ActorFollower.url)
275 289
276 function settingGetter (user: UserModel) { 290 function settingGetter (user: MUserWithNotificationSetting) {
277 return user.NotificationSetting.newInstanceFollower 291 return user.NotificationSetting.newInstanceFollower
278 } 292 }
279 293
280 async function notificationCreator (user: UserModel) { 294 async function notificationCreator (user: MUserWithNotificationSetting) {
281 const notification = await UserNotificationModel.create({ 295 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
282 type: UserNotificationType.NEW_INSTANCE_FOLLOWER, 296 type: UserNotificationType.NEW_INSTANCE_FOLLOWER,
283 userId: user.id, 297 userId: user.id,
284 actorFollowId: actorFollow.id 298 actorFollowId: actorFollow.id
@@ -295,18 +309,45 @@ class Notifier {
295 return this.notify({ users: admins, settingGetter, notificationCreator, emailSender }) 309 return this.notify({ users: admins, settingGetter, notificationCreator, emailSender })
296 } 310 }
297 311
298 private async notifyModeratorsOfNewVideoAbuse (videoAbuse: VideoAbuseModel) { 312 private async notifyAdminsOfAutoInstanceFollowing (actorFollow: MActorFollowFull) {
313 const admins = await UserModel.listWithRight(UserRight.MANAGE_SERVER_FOLLOW)
314
315 logger.info('Notifying %d administrators of auto instance following: %s.', admins.length, actorFollow.ActorFollowing.url)
316
317 function settingGetter (user: MUserWithNotificationSetting) {
318 return user.NotificationSetting.autoInstanceFollowing
319 }
320
321 async function notificationCreator (user: MUserWithNotificationSetting) {
322 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
323 type: UserNotificationType.AUTO_INSTANCE_FOLLOWING,
324 userId: user.id,
325 actorFollowId: actorFollow.id
326 })
327 notification.ActorFollow = actorFollow
328
329 return notification
330 }
331
332 function emailSender (emails: string[]) {
333 return Emailer.Instance.addAutoInstanceFollowingNotification(emails, actorFollow)
334 }
335
336 return this.notify({ users: admins, settingGetter, notificationCreator, emailSender })
337 }
338
339 private async notifyModeratorsOfNewVideoAbuse (videoAbuse: MVideoAbuseVideo) {
299 const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_ABUSES) 340 const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_ABUSES)
300 if (moderators.length === 0) return 341 if (moderators.length === 0) return
301 342
302 logger.info('Notifying %s user/moderators of new video abuse %s.', moderators.length, videoAbuse.Video.url) 343 logger.info('Notifying %s user/moderators of new video abuse %s.', moderators.length, videoAbuse.Video.url)
303 344
304 function settingGetter (user: UserModel) { 345 function settingGetter (user: MUserWithNotificationSetting) {
305 return user.NotificationSetting.videoAbuseAsModerator 346 return user.NotificationSetting.videoAbuseAsModerator
306 } 347 }
307 348
308 async function notificationCreator (user: UserModel) { 349 async function notificationCreator (user: MUserWithNotificationSetting) {
309 const notification = await UserNotificationModel.create({ 350 const notification: UserNotificationModelForApi = await UserNotificationModel.create<UserNotificationModelForApi>({
310 type: UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS, 351 type: UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS,
311 userId: user.id, 352 userId: user.id,
312 videoAbuseId: videoAbuse.id 353 videoAbuseId: videoAbuse.id
@@ -323,46 +364,46 @@ class Notifier {
323 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) 364 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
324 } 365 }
325 366
326 private async notifyModeratorsOfVideoAutoBlacklist (video: VideoModel) { 367 private async notifyModeratorsOfVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo) {
327 const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_BLACKLIST) 368 const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_BLACKLIST)
328 if (moderators.length === 0) return 369 if (moderators.length === 0) return
329 370
330 logger.info('Notifying %s moderators of video auto-blacklist %s.', moderators.length, video.url) 371 logger.info('Notifying %s moderators of video auto-blacklist %s.', moderators.length, videoBlacklist.Video.url)
331 372
332 function settingGetter (user: UserModel) { 373 function settingGetter (user: MUserWithNotificationSetting) {
333 return user.NotificationSetting.videoAutoBlacklistAsModerator 374 return user.NotificationSetting.videoAutoBlacklistAsModerator
334 } 375 }
335 async function notificationCreator (user: UserModel) {
336 376
337 const notification = await UserNotificationModel.create({ 377 async function notificationCreator (user: MUserWithNotificationSetting) {
378 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
338 type: UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS, 379 type: UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS,
339 userId: user.id, 380 userId: user.id,
340 videoId: video.id 381 videoBlacklistId: videoBlacklist.id
341 }) 382 })
342 notification.Video = video 383 notification.VideoBlacklist = videoBlacklist
343 384
344 return notification 385 return notification
345 } 386 }
346 387
347 function emailSender (emails: string[]) { 388 function emailSender (emails: string[]) {
348 return Emailer.Instance.addVideoAutoBlacklistModeratorsNotification(emails, video) 389 return Emailer.Instance.addVideoAutoBlacklistModeratorsNotification(emails, videoBlacklist)
349 } 390 }
350 391
351 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) 392 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
352 } 393 }
353 394
354 private async notifyVideoOwnerOfBlacklist (videoBlacklist: VideoBlacklistModel) { 395 private async notifyVideoOwnerOfBlacklist (videoBlacklist: MVideoBlacklistVideo) {
355 const user = await UserModel.loadByVideoId(videoBlacklist.videoId) 396 const user = await UserModel.loadByVideoId(videoBlacklist.videoId)
356 if (!user) return 397 if (!user) return
357 398
358 logger.info('Notifying user %s that its video %s has been blacklisted.', user.username, videoBlacklist.Video.url) 399 logger.info('Notifying user %s that its video %s has been blacklisted.', user.username, videoBlacklist.Video.url)
359 400
360 function settingGetter (user: UserModel) { 401 function settingGetter (user: MUserWithNotificationSetting) {
361 return user.NotificationSetting.blacklistOnMyVideo 402 return user.NotificationSetting.blacklistOnMyVideo
362 } 403 }
363 404
364 async function notificationCreator (user: UserModel) { 405 async function notificationCreator (user: MUserWithNotificationSetting) {
365 const notification = await UserNotificationModel.create({ 406 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
366 type: UserNotificationType.BLACKLIST_ON_MY_VIDEO, 407 type: UserNotificationType.BLACKLIST_ON_MY_VIDEO,
367 userId: user.id, 408 userId: user.id,
368 videoBlacklistId: videoBlacklist.id 409 videoBlacklistId: videoBlacklist.id
@@ -379,18 +420,18 @@ class Notifier {
379 return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender }) 420 return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender })
380 } 421 }
381 422
382 private async notifyVideoOwnerOfUnblacklist (video: VideoModel) { 423 private async notifyVideoOwnerOfUnblacklist (video: MVideoFullLight) {
383 const user = await UserModel.loadByVideoId(video.id) 424 const user = await UserModel.loadByVideoId(video.id)
384 if (!user) return 425 if (!user) return
385 426
386 logger.info('Notifying user %s that its video %s has been unblacklisted.', user.username, video.url) 427 logger.info('Notifying user %s that its video %s has been unblacklisted.', user.username, video.url)
387 428
388 function settingGetter (user: UserModel) { 429 function settingGetter (user: MUserWithNotificationSetting) {
389 return user.NotificationSetting.blacklistOnMyVideo 430 return user.NotificationSetting.blacklistOnMyVideo
390 } 431 }
391 432
392 async function notificationCreator (user: UserModel) { 433 async function notificationCreator (user: MUserWithNotificationSetting) {
393 const notification = await UserNotificationModel.create({ 434 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
394 type: UserNotificationType.UNBLACKLIST_ON_MY_VIDEO, 435 type: UserNotificationType.UNBLACKLIST_ON_MY_VIDEO,
395 userId: user.id, 436 userId: user.id,
396 videoId: video.id 437 videoId: video.id
@@ -407,18 +448,18 @@ class Notifier {
407 return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender }) 448 return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender })
408 } 449 }
409 450
410 private async notifyOwnedVideoHasBeenPublished (video: VideoModel) { 451 private async notifyOwnedVideoHasBeenPublished (video: MVideoFullLight) {
411 const user = await UserModel.loadByVideoId(video.id) 452 const user = await UserModel.loadByVideoId(video.id)
412 if (!user) return 453 if (!user) return
413 454
414 logger.info('Notifying user %s of the publication of its video %s.', user.username, video.url) 455 logger.info('Notifying user %s of the publication of its video %s.', user.username, video.url)
415 456
416 function settingGetter (user: UserModel) { 457 function settingGetter (user: MUserWithNotificationSetting) {
417 return user.NotificationSetting.myVideoPublished 458 return user.NotificationSetting.myVideoPublished
418 } 459 }
419 460
420 async function notificationCreator (user: UserModel) { 461 async function notificationCreator (user: MUserWithNotificationSetting) {
421 const notification = await UserNotificationModel.create({ 462 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
422 type: UserNotificationType.MY_VIDEO_PUBLISHED, 463 type: UserNotificationType.MY_VIDEO_PUBLISHED,
423 userId: user.id, 464 userId: user.id,
424 videoId: video.id 465 videoId: video.id
@@ -435,18 +476,18 @@ class Notifier {
435 return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender }) 476 return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender })
436 } 477 }
437 478
438 private async notifyOwnerVideoImportIsFinished (videoImport: VideoImportModel, success: boolean) { 479 private async notifyOwnerVideoImportIsFinished (videoImport: MVideoImportVideo, success: boolean) {
439 const user = await UserModel.loadByVideoImportId(videoImport.id) 480 const user = await UserModel.loadByVideoImportId(videoImport.id)
440 if (!user) return 481 if (!user) return
441 482
442 logger.info('Notifying user %s its video import %s is finished.', user.username, videoImport.getTargetIdentifier()) 483 logger.info('Notifying user %s its video import %s is finished.', user.username, videoImport.getTargetIdentifier())
443 484
444 function settingGetter (user: UserModel) { 485 function settingGetter (user: MUserWithNotificationSetting) {
445 return user.NotificationSetting.myVideoImportFinished 486 return user.NotificationSetting.myVideoImportFinished
446 } 487 }
447 488
448 async function notificationCreator (user: UserModel) { 489 async function notificationCreator (user: MUserWithNotificationSetting) {
449 const notification = await UserNotificationModel.create({ 490 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
450 type: success ? UserNotificationType.MY_VIDEO_IMPORT_SUCCESS : UserNotificationType.MY_VIDEO_IMPORT_ERROR, 491 type: success ? UserNotificationType.MY_VIDEO_IMPORT_SUCCESS : UserNotificationType.MY_VIDEO_IMPORT_ERROR,
451 userId: user.id, 492 userId: user.id,
452 videoImportId: videoImport.id 493 videoImportId: videoImport.id
@@ -465,21 +506,21 @@ class Notifier {
465 return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender }) 506 return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender })
466 } 507 }
467 508
468 private async notifyModeratorsOfNewUserRegistration (registeredUser: UserModel) { 509 private async notifyModeratorsOfNewUserRegistration (registeredUser: MUserDefault) {
469 const moderators = await UserModel.listWithRight(UserRight.MANAGE_USERS) 510 const moderators = await UserModel.listWithRight(UserRight.MANAGE_USERS)
470 if (moderators.length === 0) return 511 if (moderators.length === 0) return
471 512
472 logger.info( 513 logger.info(
473 'Notifying %s moderators of new user registration of %s.', 514 'Notifying %s moderators of new user registration of %s.',
474 moderators.length, registeredUser.Account.Actor.preferredUsername 515 moderators.length, registeredUser.username
475 ) 516 )
476 517
477 function settingGetter (user: UserModel) { 518 function settingGetter (user: MUserWithNotificationSetting) {
478 return user.NotificationSetting.newUserRegistration 519 return user.NotificationSetting.newUserRegistration
479 } 520 }
480 521
481 async function notificationCreator (user: UserModel) { 522 async function notificationCreator (user: MUserWithNotificationSetting) {
482 const notification = await UserNotificationModel.create({ 523 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
483 type: UserNotificationType.NEW_USER_REGISTRATION, 524 type: UserNotificationType.NEW_USER_REGISTRATION,
484 userId: user.id, 525 userId: user.id,
485 accountId: registeredUser.Account.id 526 accountId: registeredUser.Account.id
@@ -496,11 +537,11 @@ class Notifier {
496 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) 537 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
497 } 538 }
498 539
499 private async notify (options: { 540 private async notify <T extends MUserWithNotificationSetting> (options: {
500 users: UserModel[], 541 users: T[],
501 notificationCreator: (user: UserModel) => Promise<UserNotificationModel>, 542 notificationCreator: (user: T) => Promise<UserNotificationModelForApi>,
502 emailSender: (emails: string[]) => Promise<any> | Bluebird<any>, 543 emailSender: (emails: string[]) => Promise<any> | Bluebird<any>,
503 settingGetter: (user: UserModel) => UserNotificationSettingValue 544 settingGetter: (user: T) => UserNotificationSettingValue
504 }) { 545 }) {
505 const emails: string[] = [] 546 const emails: string[] = []
506 547
@@ -521,7 +562,7 @@ class Notifier {
521 } 562 }
522 } 563 }
523 564
524 private isEmailEnabled (user: UserModel, value: UserNotificationSettingValue) { 565 private isEmailEnabled (user: MUser, value: UserNotificationSettingValue) {
525 if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION === true && user.emailVerified === false) return false 566 if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION === true && user.emailVerified === false) return false
526 567
527 return value & UserNotificationSettingValue.EMAIL 568 return value & UserNotificationSettingValue.EMAIL
diff --git a/server/lib/oauth-model.ts b/server/lib/oauth-model.ts
index a1153e88a..086856f41 100644
--- a/server/lib/oauth-model.ts
+++ b/server/lib/oauth-model.ts
@@ -8,10 +8,11 @@ import { LRU_CACHE } from '../initializers/constants'
8import { Transaction } from 'sequelize' 8import { 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'
11 12
12type TokenInfo = { accessToken: string, refreshToken: string, accessTokenExpiresAt: Date, refreshTokenExpiresAt: Date } 13type TokenInfo = { accessToken: string, refreshToken: string, accessTokenExpiresAt: Date, refreshTokenExpiresAt: Date }
13 14
14const accessTokenCache = new LRUCache<string, OAuthTokenModel>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE }) 15const accessTokenCache = new LRUCache<string, MOAuthTokenUser>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE })
15const userHavingToken = new LRUCache<number, string>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE }) 16const userHavingToken = new LRUCache<number, string>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE })
16 17
17// --------------------------------------------------------------------------- 18// ---------------------------------------------------------------------------
diff --git a/server/lib/peertube-socket.ts b/server/lib/peertube-socket.ts
index 17748fd18..26ced351f 100644
--- a/server/lib/peertube-socket.ts
+++ b/server/lib/peertube-socket.ts
@@ -1,8 +1,8 @@
1import * as SocketIO from 'socket.io' 1import * as SocketIO from 'socket.io'
2import { authenticateSocket } from '../middlewares' 2import { authenticateSocket } from '../middlewares'
3import { UserNotificationModel } from '../models/account/user-notification'
4import { logger } from '../helpers/logger' 3import { logger } from '../helpers/logger'
5import { Server } from 'http' 4import { Server } from 'http'
5import { UserNotificationModelForApi } from '@server/typings/models/user'
6 6
7class PeerTubeSocket { 7class PeerTubeSocket {
8 8
@@ -34,13 +34,14 @@ class PeerTubeSocket {
34 }) 34 })
35 } 35 }
36 36
37 sendNotification (userId: number, notification: UserNotificationModel) { 37 sendNotification (userId: number, notification: UserNotificationModelForApi) {
38 const sockets = this.userNotificationSockets[userId] 38 const sockets = this.userNotificationSockets[userId]
39 39
40 if (!sockets) return 40 if (!sockets) return
41 41
42 const notificationMessage = notification.toFormattedJSON()
42 for (const socket of sockets) { 43 for (const socket of sockets) {
43 socket.emit('new-notification', notification.toFormattedJSON()) 44 socket.emit('new-notification', notificationMessage)
44 } 45 }
45 } 46 }
46 47
diff --git a/server/lib/redundancy.ts b/server/lib/redundancy.ts
index 04d3ded8f..1b4ecd7c0 100644
--- a/server/lib/redundancy.ts
+++ b/server/lib/redundancy.ts
@@ -2,8 +2,9 @@ import { 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 { getServerActor } from '../helpers/utils'
5import { MVideoRedundancyVideo } from '@server/typings/models'
5 6
6async function removeVideoRedundancy (videoRedundancy: VideoRedundancyModel, t?: Transaction) { 7async function removeVideoRedundancy (videoRedundancy: MVideoRedundancyVideo, t?: Transaction) {
7 const serverActor = await getServerActor() 8 const serverActor = await getServerActor()
8 9
9 // Local cache, send undo to remote instances 10 // Local cache, send undo to remote instances
diff --git a/server/lib/schedulers/auto-follow-index-instances.ts b/server/lib/schedulers/auto-follow-index-instances.ts
new file mode 100644
index 000000000..ef11fc87f
--- /dev/null
+++ b/server/lib/schedulers/auto-follow-index-instances.ts
@@ -0,0 +1,72 @@
1import { logger } from '../../helpers/logger'
2import { AbstractScheduler } from './abstract-scheduler'
3import { INSTANCES_INDEX, SCHEDULER_INTERVALS_MS, SERVER_ACTOR_NAME } from '../../initializers/constants'
4import { CONFIG } from '../../initializers/config'
5import { chunk } from 'lodash'
6import { doRequest } from '@server/helpers/requests'
7import { ActorFollowModel } from '@server/models/activitypub/actor-follow'
8import { JobQueue } from '@server/lib/job-queue'
9import { getServerActor } from '@server/helpers/utils'
10
11export class AutoFollowIndexInstances extends AbstractScheduler {
12
13 private static instance: AbstractScheduler
14
15 protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.autoFollowIndexInstances
16
17 private lastCheck: Date
18
19 private constructor () {
20 super()
21 }
22
23 protected async internalExecute () {
24 return this.autoFollow()
25 }
26
27 private async autoFollow () {
28 if (CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.ENABLED === false) return
29
30 const indexUrl = CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.INDEX_URL
31
32 logger.info('Auto follow instances of index %s.', indexUrl)
33
34 try {
35 const serverActor = await getServerActor()
36
37 const uri = indexUrl + INSTANCES_INDEX.HOSTS_PATH
38
39 const qs = this.lastCheck ? { since: this.lastCheck.toISOString() } : {}
40 this.lastCheck = new Date()
41
42 const { body } = await doRequest({ uri, qs, json: true })
43
44 const hosts: string[] = body.data.map(o => o.host)
45 const chunks = chunk(hosts, 20)
46
47 for (const chunk of chunks) {
48 const unfollowedHosts = await ActorFollowModel.keepUnfollowedInstance(chunk)
49
50 for (const unfollowedHost of unfollowedHosts) {
51 const payload = {
52 host: unfollowedHost,
53 name: SERVER_ACTOR_NAME,
54 followerActorId: serverActor.id,
55 isAutoFollow: true
56 }
57
58 await JobQueue.Instance.createJob({ type: 'activitypub-follow', payload })
59 .catch(err => logger.error('Cannot create follow job for %s.', unfollowedHost, err))
60 }
61 }
62
63 } catch (err) {
64 logger.error('Cannot auto follow hosts of index %s.', indexUrl, { err })
65 }
66
67 }
68
69 static get Instance () {
70 return this.instance || (this.instance = new this())
71 }
72}
diff --git a/server/lib/schedulers/videos-redundancy-scheduler.ts b/server/lib/schedulers/videos-redundancy-scheduler.ts
index 5f4aad66e..1e30f6ebc 100644
--- a/server/lib/schedulers/videos-redundancy-scheduler.ts
+++ b/server/lib/schedulers/videos-redundancy-scheduler.ts
@@ -3,7 +3,6 @@ import { HLS_REDUNDANCY_DIRECTORY, REDUNDANCY, VIDEO_IMPORT_TIMEOUT, WEBSERVER }
3import { logger } from '../../helpers/logger' 3import { logger } from '../../helpers/logger'
4import { VideosRedundancy } from '../../../shared/models/redundancy' 4import { VideosRedundancy } from '../../../shared/models/redundancy'
5import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy' 5import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy'
6import { VideoFileModel } from '../../models/video/video-file'
7import { downloadWebTorrentVideo } from '../../helpers/webtorrent' 6import { downloadWebTorrentVideo } from '../../helpers/webtorrent'
8import { join } from 'path' 7import { join } from 'path'
9import { move } from 'fs-extra' 8import { move } from 'fs-extra'
@@ -12,16 +11,31 @@ import { sendCreateCacheFile, sendUpdateCacheFile } from '../activitypub/send'
12import { getVideoCacheFileActivityPubUrl, getVideoCacheStreamingPlaylistActivityPubUrl } from '../activitypub/url' 11import { getVideoCacheFileActivityPubUrl, getVideoCacheStreamingPlaylistActivityPubUrl } from '../activitypub/url'
13import { removeVideoRedundancy } from '../redundancy' 12import { removeVideoRedundancy } from '../redundancy'
14import { getOrCreateVideoAndAccountAndChannel } from '../activitypub' 13import { getOrCreateVideoAndAccountAndChannel } from '../activitypub'
15import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist'
16import { VideoModel } from '../../models/video/video'
17import { downloadPlaylistSegments } from '../hls' 14import { downloadPlaylistSegments } from '../hls'
18import { CONFIG } from '../../initializers/config' 15import { CONFIG } from '../../initializers/config'
16import {
17 MStreamingPlaylist,
18 MStreamingPlaylistVideo,
19 MVideoAccountLight,
20 MVideoFile,
21 MVideoFileVideo,
22 MVideoRedundancyFileVideo,
23 MVideoRedundancyStreamingPlaylistVideo,
24 MVideoRedundancyVideo,
25 MVideoWithAllFiles
26} from '@server/typings/models'
19 27
20type CandidateToDuplicate = { 28type CandidateToDuplicate = {
21 redundancy: VideosRedundancy, 29 redundancy: VideosRedundancy,
22 video: VideoModel, 30 video: MVideoWithAllFiles,
23 files: VideoFileModel[], 31 files: MVideoFile[],
24 streamingPlaylists: VideoStreamingPlaylistModel[] 32 streamingPlaylists: MStreamingPlaylist[]
33}
34
35function isMVideoRedundancyFileVideo (
36 o: MVideoRedundancyFileVideo | MVideoRedundancyStreamingPlaylistVideo
37): o is MVideoRedundancyFileVideo {
38 return !!(o as MVideoRedundancyFileVideo).VideoFile
25} 39}
26 40
27export class VideosRedundancyScheduler extends AbstractScheduler { 41export class VideosRedundancyScheduler extends AbstractScheduler {
@@ -102,7 +116,7 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
102 } 116 }
103 } 117 }
104 118
105 private async extendsRedundancy (redundancyModel: VideoRedundancyModel) { 119 private async extendsRedundancy (redundancyModel: MVideoRedundancyVideo) {
106 const redundancy = CONFIG.REDUNDANCY.VIDEOS.STRATEGIES.find(s => s.strategy === redundancyModel.strategy) 120 const redundancy = CONFIG.REDUNDANCY.VIDEOS.STRATEGIES.find(s => s.strategy === redundancyModel.strategy)
107 // Redundancy strategy disabled, remove our redundancy instead of extending expiration 121 // Redundancy strategy disabled, remove our redundancy instead of extending expiration
108 if (!redundancy) { 122 if (!redundancy) {
@@ -172,7 +186,8 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
172 } 186 }
173 } 187 }
174 188
175 private async createVideoFileRedundancy (redundancy: VideosRedundancy, video: VideoModel, file: VideoFileModel) { 189 private async createVideoFileRedundancy (redundancy: VideosRedundancy, video: MVideoAccountLight, fileArg: MVideoFile) {
190 const file = fileArg as MVideoFileVideo
176 file.Video = video 191 file.Video = video
177 192
178 const serverActor = await getServerActor() 193 const serverActor = await getServerActor()
@@ -187,7 +202,7 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
187 const destPath = join(CONFIG.STORAGE.REDUNDANCY_DIR, video.getVideoFilename(file)) 202 const destPath = join(CONFIG.STORAGE.REDUNDANCY_DIR, video.getVideoFilename(file))
188 await move(tmpPath, destPath, { overwrite: true }) 203 await move(tmpPath, destPath, { overwrite: true })
189 204
190 const createdModel = await VideoRedundancyModel.create({ 205 const createdModel: MVideoRedundancyFileVideo = await VideoRedundancyModel.create({
191 expiresOn: this.buildNewExpiration(redundancy.minLifetime), 206 expiresOn: this.buildNewExpiration(redundancy.minLifetime),
192 url: getVideoCacheFileActivityPubUrl(file), 207 url: getVideoCacheFileActivityPubUrl(file),
193 fileUrl: video.getVideoRedundancyUrl(file, WEBSERVER.URL), 208 fileUrl: video.getVideoRedundancyUrl(file, WEBSERVER.URL),
@@ -203,7 +218,12 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
203 logger.info('Duplicated %s - %d -> %s.', video.url, file.resolution, createdModel.url) 218 logger.info('Duplicated %s - %d -> %s.', video.url, file.resolution, createdModel.url)
204 } 219 }
205 220
206 private async createStreamingPlaylistRedundancy (redundancy: VideosRedundancy, video: VideoModel, playlist: VideoStreamingPlaylistModel) { 221 private async createStreamingPlaylistRedundancy (
222 redundancy: VideosRedundancy,
223 video: MVideoAccountLight,
224 playlistArg: MStreamingPlaylist
225 ) {
226 const playlist = playlistArg as MStreamingPlaylistVideo
207 playlist.Video = video 227 playlist.Video = video
208 228
209 const serverActor = await getServerActor() 229 const serverActor = await getServerActor()
@@ -213,7 +233,7 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
213 const destDirectory = join(HLS_REDUNDANCY_DIRECTORY, video.uuid) 233 const destDirectory = join(HLS_REDUNDANCY_DIRECTORY, video.uuid)
214 await downloadPlaylistSegments(playlist.playlistUrl, destDirectory, VIDEO_IMPORT_TIMEOUT) 234 await downloadPlaylistSegments(playlist.playlistUrl, destDirectory, VIDEO_IMPORT_TIMEOUT)
215 235
216 const createdModel = await VideoRedundancyModel.create({ 236 const createdModel: MVideoRedundancyStreamingPlaylistVideo = await VideoRedundancyModel.create({
217 expiresOn: this.buildNewExpiration(redundancy.minLifetime), 237 expiresOn: this.buildNewExpiration(redundancy.minLifetime),
218 url: getVideoCacheStreamingPlaylistActivityPubUrl(video, playlist), 238 url: getVideoCacheStreamingPlaylistActivityPubUrl(video, playlist),
219 fileUrl: playlist.getVideoRedundancyUrl(WEBSERVER.URL), 239 fileUrl: playlist.getVideoRedundancyUrl(WEBSERVER.URL),
@@ -229,7 +249,7 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
229 logger.info('Duplicated playlist %s -> %s.', playlist.playlistUrl, createdModel.url) 249 logger.info('Duplicated playlist %s -> %s.', playlist.playlistUrl, createdModel.url)
230 } 250 }
231 251
232 private async extendsExpirationOf (redundancy: VideoRedundancyModel, expiresAfterMs: number) { 252 private async extendsExpirationOf (redundancy: MVideoRedundancyVideo, expiresAfterMs: number) {
233 logger.info('Extending expiration of %s.', redundancy.url) 253 logger.info('Extending expiration of %s.', redundancy.url)
234 254
235 const serverActor = await getServerActor() 255 const serverActor = await getServerActor()
@@ -243,7 +263,7 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
243 private async purgeCacheIfNeeded (candidateToDuplicate: CandidateToDuplicate) { 263 private async purgeCacheIfNeeded (candidateToDuplicate: CandidateToDuplicate) {
244 while (await this.isTooHeavy(candidateToDuplicate)) { 264 while (await this.isTooHeavy(candidateToDuplicate)) {
245 const redundancy = candidateToDuplicate.redundancy 265 const redundancy = candidateToDuplicate.redundancy
246 const toDelete = await VideoRedundancyModel.loadOldestLocalThatAlreadyExpired(redundancy.strategy, redundancy.minLifetime) 266 const toDelete = await VideoRedundancyModel.loadOldestLocalExpired(redundancy.strategy, redundancy.minLifetime)
247 if (!toDelete) return 267 if (!toDelete) return
248 268
249 await removeVideoRedundancy(toDelete) 269 await removeVideoRedundancy(toDelete)
@@ -263,19 +283,18 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
263 return new Date(Date.now() + expiresAfterMs) 283 return new Date(Date.now() + expiresAfterMs)
264 } 284 }
265 285
266 private buildEntryLogId (object: VideoRedundancyModel) { 286 private buildEntryLogId (object: MVideoRedundancyFileVideo | MVideoRedundancyStreamingPlaylistVideo) {
267 if (object.VideoFile) return `${object.VideoFile.Video.url}-${object.VideoFile.resolution}` 287 if (isMVideoRedundancyFileVideo(object)) return `${object.VideoFile.Video.url}-${object.VideoFile.resolution}`
268 288
269 return `${object.VideoStreamingPlaylist.playlistUrl}` 289 return `${object.VideoStreamingPlaylist.playlistUrl}`
270 } 290 }
271 291
272 private getTotalFileSizes (files: VideoFileModel[], playlists: VideoStreamingPlaylistModel[]) { 292 private getTotalFileSizes (files: MVideoFile[], playlists: MStreamingPlaylist[]) {
273 const fileReducer = (previous: number, current: VideoFileModel) => previous + current.size 293 const fileReducer = (previous: number, current: MVideoFile) => previous + current.size
274 294
275 const totalSize = files.reduce(fileReducer, 0) 295 const totalSize = files.reduce(fileReducer, 0)
276 if (playlists.length === 0) return totalSize
277 296
278 return totalSize * playlists.length 297 return totalSize + (totalSize * playlists.length)
279 } 298 }
280 299
281 private async loadAndRefreshVideo (videoUrl: string) { 300 private async loadAndRefreshVideo (videoUrl: string) {
diff --git a/server/lib/thumbnail.ts b/server/lib/thumbnail.ts
index a59773f5a..84791955e 100644
--- a/server/lib/thumbnail.ts
+++ b/server/lib/thumbnail.ts
@@ -1,20 +1,20 @@
1import { VideoFileModel } from '../models/video/video-file'
2import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils' 1import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils'
3import { CONFIG } from '../initializers/config' 2import { CONFIG } from '../initializers/config'
4import { PREVIEWS_SIZE, THUMBNAILS_SIZE, ASSETS_PATH } from '../initializers/constants' 3import { ASSETS_PATH, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants'
5import { VideoModel } from '../models/video/video'
6import { ThumbnailModel } from '../models/video/thumbnail' 4import { ThumbnailModel } from '../models/video/thumbnail'
7import { ThumbnailType } from '../../shared/models/videos/thumbnail.type' 5import { ThumbnailType } from '../../shared/models/videos/thumbnail.type'
8import { processImage } from '../helpers/image-utils' 6import { processImage } from '../helpers/image-utils'
9import { join } from 'path' 7import { join } from 'path'
10import { downloadImage } from '../helpers/requests' 8import { downloadImage } from '../helpers/requests'
11import { VideoPlaylistModel } from '../models/video/video-playlist' 9import { MVideoPlaylistThumbnail } from '../typings/models/video/video-playlist'
10import { MVideoFile, MVideoThumbnail } from '../typings/models'
11import { MThumbnail } from '../typings/models/video/thumbnail'
12 12
13type ImageSize = { height: number, width: number } 13type ImageSize = { height: number, width: number }
14 14
15function createPlaylistMiniatureFromExisting ( 15function createPlaylistMiniatureFromExisting (
16 inputPath: string, 16 inputPath: string,
17 playlist: VideoPlaylistModel, 17 playlist: MVideoPlaylistThumbnail,
18 automaticallyGenerated: boolean, 18 automaticallyGenerated: boolean,
19 keepOriginal = false, 19 keepOriginal = false,
20 size?: ImageSize 20 size?: ImageSize
@@ -26,7 +26,7 @@ function createPlaylistMiniatureFromExisting (
26 return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, automaticallyGenerated, existingThumbnail }) 26 return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, automaticallyGenerated, existingThumbnail })
27} 27}
28 28
29function createPlaylistMiniatureFromUrl (fileUrl: string, playlist: VideoPlaylistModel, size?: ImageSize) { 29function createPlaylistMiniatureFromUrl (fileUrl: string, playlist: MVideoPlaylistThumbnail, size?: ImageSize) {
30 const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromPlaylist(playlist, size) 30 const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromPlaylist(playlist, size)
31 const type = ThumbnailType.MINIATURE 31 const type = ThumbnailType.MINIATURE
32 32
@@ -34,7 +34,7 @@ function createPlaylistMiniatureFromUrl (fileUrl: string, playlist: VideoPlaylis
34 return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl }) 34 return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl })
35} 35}
36 36
37function createVideoMiniatureFromUrl (fileUrl: string, video: VideoModel, type: ThumbnailType, size?: ImageSize) { 37function createVideoMiniatureFromUrl (fileUrl: string, video: MVideoThumbnail, type: ThumbnailType, size?: ImageSize) {
38 const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) 38 const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
39 const thumbnailCreator = () => downloadImage(fileUrl, basePath, filename, { width, height }) 39 const thumbnailCreator = () => downloadImage(fileUrl, basePath, filename, { width, height })
40 40
@@ -43,7 +43,7 @@ function createVideoMiniatureFromUrl (fileUrl: string, video: VideoModel, type:
43 43
44function createVideoMiniatureFromExisting ( 44function createVideoMiniatureFromExisting (
45 inputPath: string, 45 inputPath: string,
46 video: VideoModel, 46 video: MVideoThumbnail,
47 type: ThumbnailType, 47 type: ThumbnailType,
48 automaticallyGenerated: boolean, 48 automaticallyGenerated: boolean,
49 size?: ImageSize 49 size?: ImageSize
@@ -54,7 +54,7 @@ function createVideoMiniatureFromExisting (
54 return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, automaticallyGenerated, existingThumbnail }) 54 return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, automaticallyGenerated, existingThumbnail })
55} 55}
56 56
57function generateVideoMiniature (video: VideoModel, videoFile: VideoFileModel, type: ThumbnailType) { 57function generateVideoMiniature (video: MVideoThumbnail, videoFile: MVideoFile, type: ThumbnailType) {
58 const input = video.getVideoFilePath(videoFile) 58 const input = video.getVideoFilePath(videoFile)
59 59
60 const { filename, basePath, height, width, existingThumbnail, outputPath } = buildMetadataFromVideo(video, type) 60 const { filename, basePath, height, width, existingThumbnail, outputPath } = buildMetadataFromVideo(video, type)
@@ -65,7 +65,7 @@ function generateVideoMiniature (video: VideoModel, videoFile: VideoFileModel, t
65 return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, automaticallyGenerated: true, existingThumbnail }) 65 return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, automaticallyGenerated: true, existingThumbnail })
66} 66}
67 67
68function createPlaceholderThumbnail (fileUrl: string, video: VideoModel, type: ThumbnailType, size: ImageSize) { 68function createPlaceholderThumbnail (fileUrl: string, video: MVideoThumbnail, type: ThumbnailType, size: ImageSize) {
69 const { filename, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) 69 const { filename, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
70 70
71 const thumbnail = existingThumbnail ? existingThumbnail : new ThumbnailModel() 71 const thumbnail = existingThumbnail ? existingThumbnail : new ThumbnailModel()
@@ -90,7 +90,7 @@ export {
90 createPlaylistMiniatureFromExisting 90 createPlaylistMiniatureFromExisting
91} 91}
92 92
93function buildMetadataFromPlaylist (playlist: VideoPlaylistModel, size: ImageSize) { 93function buildMetadataFromPlaylist (playlist: MVideoPlaylistThumbnail, size: ImageSize) {
94 const filename = playlist.generateThumbnailName() 94 const filename = playlist.generateThumbnailName()
95 const basePath = CONFIG.STORAGE.THUMBNAILS_DIR 95 const basePath = CONFIG.STORAGE.THUMBNAILS_DIR
96 96
@@ -104,7 +104,7 @@ function buildMetadataFromPlaylist (playlist: VideoPlaylistModel, size: ImageSiz
104 } 104 }
105} 105}
106 106
107function buildMetadataFromVideo (video: VideoModel, type: ThumbnailType, size?: ImageSize) { 107function buildMetadataFromVideo (video: MVideoThumbnail, type: ThumbnailType, size?: ImageSize) {
108 const existingThumbnail = Array.isArray(video.Thumbnails) 108 const existingThumbnail = Array.isArray(video.Thumbnails)
109 ? video.Thumbnails.find(t => t.type === type) 109 ? video.Thumbnails.find(t => t.type === type)
110 : undefined 110 : undefined
@@ -148,7 +148,7 @@ async function createThumbnailFromFunction (parameters: {
148 type: ThumbnailType, 148 type: ThumbnailType,
149 automaticallyGenerated?: boolean, 149 automaticallyGenerated?: boolean,
150 fileUrl?: string, 150 fileUrl?: string,
151 existingThumbnail?: ThumbnailModel 151 existingThumbnail?: MThumbnail
152}) { 152}) {
153 const { thumbnailCreator, filename, width, height, type, existingThumbnail, automaticallyGenerated = null, fileUrl = null } = parameters 153 const { thumbnailCreator, filename, width, height, type, existingThumbnail, automaticallyGenerated = null, fileUrl = null } = parameters
154 154
diff --git a/server/lib/user.ts b/server/lib/user.ts
index 0e4007770..c45438d95 100644
--- a/server/lib/user.ts
+++ b/server/lib/user.ts
@@ -2,10 +2,8 @@ import * as uuidv4 from 'uuid/v4'
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 { UserModel } from '../models/account/user'
6import { buildActorInstance, getAccountActivityPubUrl, setAsyncActorKeys } from './activitypub' 5import { buildActorInstance, getAccountActivityPubUrl, setAsyncActorKeys } from './activitypub'
7import { createVideoChannel } from './video-channel' 6import { createLocalVideoChannel } from './video-channel'
8import { VideoChannelModel } from '../models/video/video-channel'
9import { ActorModel } from '../models/activitypub/actor' 7import { ActorModel } from '../models/activitypub/actor'
10import { UserNotificationSettingModel } from '../models/account/user-notification-setting' 8import { UserNotificationSettingModel } from '../models/account/user-notification-setting'
11import { UserNotificationSetting, UserNotificationSettingValue } from '../../shared/models/users' 9import { UserNotificationSetting, UserNotificationSettingValue } from '../../shared/models/users'
@@ -14,14 +12,17 @@ import { sequelizeTypescript } from '../initializers/database'
14import { Transaction } from 'sequelize/types' 12import { Transaction } from 'sequelize/types'
15import { Redis } from './redis' 13import { Redis } from './redis'
16import { Emailer } from './emailer' 14import { Emailer } from './emailer'
15import { MAccountDefault, MActorDefault, MChannelActor } from '../typings/models'
16import { MUser, MUserDefault, MUserId } from '../typings/models/user'
17 17
18type ChannelNames = { name: string, displayName: string } 18type ChannelNames = { name: string, displayName: string }
19
19async function createUserAccountAndChannelAndPlaylist (parameters: { 20async function createUserAccountAndChannelAndPlaylist (parameters: {
20 userToCreate: UserModel, 21 userToCreate: MUser,
21 userDisplayName?: string, 22 userDisplayName?: string,
22 channelNames?: ChannelNames, 23 channelNames?: ChannelNames,
23 validateUser?: boolean 24 validateUser?: boolean
24}) { 25}): Promise<{ user: MUserDefault, account: MAccountDefault, videoChannel: MChannelActor }> {
25 const { userToCreate, userDisplayName, channelNames, validateUser = true } = parameters 26 const { userToCreate, userDisplayName, channelNames, validateUser = true } = parameters
26 27
27 const { user, account, videoChannel } = await sequelizeTypescript.transaction(async t => { 28 const { user, account, videoChannel } = await sequelizeTypescript.transaction(async t => {
@@ -30,7 +31,7 @@ async function createUserAccountAndChannelAndPlaylist (parameters: {
30 validate: validateUser 31 validate: validateUser
31 } 32 }
32 33
33 const userCreated = await userToCreate.save(userOptions) 34 const userCreated: MUserDefault = await userToCreate.save(userOptions)
34 userCreated.NotificationSetting = await createDefaultUserNotificationSettings(userCreated, t) 35 userCreated.NotificationSetting = await createDefaultUserNotificationSettings(userCreated, t)
35 36
36 const accountCreated = await createLocalAccountWithoutKeys({ 37 const accountCreated = await createLocalAccountWithoutKeys({
@@ -43,22 +44,22 @@ async function createUserAccountAndChannelAndPlaylist (parameters: {
43 userCreated.Account = accountCreated 44 userCreated.Account = accountCreated
44 45
45 const channelAttributes = await buildChannelAttributes(userCreated, channelNames) 46 const channelAttributes = await buildChannelAttributes(userCreated, channelNames)
46 const videoChannel = await createVideoChannel(channelAttributes, accountCreated, t) 47 const videoChannel = await createLocalVideoChannel(channelAttributes, accountCreated, t)
47 48
48 const videoPlaylist = await createWatchLaterPlaylist(accountCreated, t) 49 const videoPlaylist = await createWatchLaterPlaylist(accountCreated, t)
49 50
50 return { user: userCreated, account: accountCreated, videoChannel, videoPlaylist } 51 return { user: userCreated, account: accountCreated, videoChannel, videoPlaylist }
51 }) 52 })
52 53
53 const [ accountKeys, channelKeys ] = await Promise.all([ 54 const [ accountActorWithKeys, channelActorWithKeys ] = await Promise.all([
54 setAsyncActorKeys(account.Actor), 55 setAsyncActorKeys(account.Actor),
55 setAsyncActorKeys(videoChannel.Actor) 56 setAsyncActorKeys(videoChannel.Actor)
56 ]) 57 ])
57 58
58 account.Actor = accountKeys 59 account.Actor = accountActorWithKeys
59 videoChannel.Actor = channelKeys 60 videoChannel.Actor = channelActorWithKeys
60 61
61 return { user, account, videoChannel } as { user: UserModel, account: AccountModel, videoChannel: VideoChannelModel } 62 return { user, account, videoChannel }
62} 63}
63 64
64async function createLocalAccountWithoutKeys (parameters: { 65async function createLocalAccountWithoutKeys (parameters: {
@@ -73,7 +74,7 @@ async function createLocalAccountWithoutKeys (parameters: {
73 const url = getAccountActivityPubUrl(name) 74 const url = getAccountActivityPubUrl(name)
74 75
75 const actorInstance = buildActorInstance(type, url, name) 76 const actorInstance = buildActorInstance(type, url, name)
76 const actorInstanceCreated = await actorInstance.save({ transaction: t }) 77 const actorInstanceCreated: MActorDefault = await actorInstance.save({ transaction: t })
77 78
78 const accountInstance = new AccountModel({ 79 const accountInstance = new AccountModel({
79 name: displayName || name, 80 name: displayName || name,
@@ -82,7 +83,7 @@ async function createLocalAccountWithoutKeys (parameters: {
82 actorId: actorInstanceCreated.id 83 actorId: actorInstanceCreated.id
83 }) 84 })
84 85
85 const accountInstanceCreated = await accountInstance.save({ transaction: t }) 86 const accountInstanceCreated: MAccountDefault = await accountInstance.save({ transaction: t })
86 accountInstanceCreated.Actor = actorInstanceCreated 87 accountInstanceCreated.Actor = actorInstanceCreated
87 88
88 return accountInstanceCreated 89 return accountInstanceCreated
@@ -102,7 +103,7 @@ async function createApplicationActor (applicationId: number) {
102 return accountCreated 103 return accountCreated
103} 104}
104 105
105async function sendVerifyUserEmail (user: UserModel, isPendingEmail = false) { 106async function sendVerifyUserEmail (user: MUser, isPendingEmail = false) {
106 const verificationString = await Redis.Instance.setVerifyEmailVerificationString(user.id) 107 const verificationString = await Redis.Instance.setVerifyEmailVerificationString(user.id)
107 let url = WEBSERVER.URL + '/verify-account/email?userId=' + user.id + '&verificationString=' + verificationString 108 let url = WEBSERVER.URL + '/verify-account/email?userId=' + user.id + '&verificationString=' + verificationString
108 109
@@ -124,7 +125,7 @@ export {
124 125
125// --------------------------------------------------------------------------- 126// ---------------------------------------------------------------------------
126 127
127function createDefaultUserNotificationSettings (user: UserModel, t: Transaction | undefined) { 128function createDefaultUserNotificationSettings (user: MUserId, t: Transaction | undefined) {
128 const values: UserNotificationSetting & { userId: number } = { 129 const values: UserNotificationSetting & { userId: number } = {
129 userId: user.id, 130 userId: user.id,
130 newVideoFromSubscription: UserNotificationSettingValue.WEB, 131 newVideoFromSubscription: UserNotificationSettingValue.WEB,
@@ -137,13 +138,14 @@ function createDefaultUserNotificationSettings (user: UserModel, t: Transaction
137 newUserRegistration: UserNotificationSettingValue.WEB, 138 newUserRegistration: UserNotificationSettingValue.WEB,
138 commentMention: UserNotificationSettingValue.WEB, 139 commentMention: UserNotificationSettingValue.WEB,
139 newFollow: UserNotificationSettingValue.WEB, 140 newFollow: UserNotificationSettingValue.WEB,
140 newInstanceFollower: UserNotificationSettingValue.WEB 141 newInstanceFollower: UserNotificationSettingValue.WEB,
142 autoInstanceFollowing: UserNotificationSettingValue.WEB
141 } 143 }
142 144
143 return UserNotificationSettingModel.create(values, { transaction: t }) 145 return UserNotificationSettingModel.create(values, { transaction: t })
144} 146}
145 147
146async function buildChannelAttributes (user: UserModel, channelNames?: ChannelNames) { 148async function buildChannelAttributes (user: MUser, channelNames?: ChannelNames) {
147 if (channelNames) return channelNames 149 if (channelNames) return channelNames
148 150
149 let channelName = user.username + '_channel' 151 let channelName = user.username + '_channel'
diff --git a/server/lib/video-blacklist.ts b/server/lib/video-blacklist.ts
index bdaecd8e2..1dd45b76d 100644
--- a/server/lib/video-blacklist.ts
+++ b/server/lib/video-blacklist.ts
@@ -2,16 +2,15 @@ import { Transaction } from 'sequelize'
2import { CONFIG } from '../initializers/config' 2import { CONFIG } from '../initializers/config'
3import { UserRight, VideoBlacklistType } from '../../shared/models' 3import { UserRight, VideoBlacklistType } from '../../shared/models'
4import { VideoBlacklistModel } from '../models/video/video-blacklist' 4import { VideoBlacklistModel } from '../models/video/video-blacklist'
5import { UserModel } from '../models/account/user'
6import { VideoModel } from '../models/video/video'
7import { logger } from '../helpers/logger' 5import { logger } from '../helpers/logger'
8import { UserAdminFlag } from '../../shared/models/users/user-flag.model' 6import { UserAdminFlag } from '../../shared/models/users/user-flag.model'
9import { Hooks } from './plugins/hooks' 7import { Hooks } from './plugins/hooks'
10import { Notifier } from './notifier' 8import { Notifier } from './notifier'
9import { MUser, MVideoBlacklistVideo, MVideoWithBlacklistLight } from '@server/typings/models'
11 10
12async function autoBlacklistVideoIfNeeded (parameters: { 11async function autoBlacklistVideoIfNeeded (parameters: {
13 video: VideoModel, 12 video: MVideoWithBlacklistLight,
14 user?: UserModel, 13 user?: MUser,
15 isRemote: boolean, 14 isRemote: boolean,
16 isNew: boolean, 15 isNew: boolean,
17 notify?: boolean, 16 notify?: boolean,
@@ -32,7 +31,7 @@ async function autoBlacklistVideoIfNeeded (parameters: {
32 reason: 'Auto-blacklisted. Moderator review required.', 31 reason: 'Auto-blacklisted. Moderator review required.',
33 type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED 32 type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED
34 } 33 }
35 const [ videoBlacklist ] = await VideoBlacklistModel.findOrCreate({ 34 const [ videoBlacklist ] = await VideoBlacklistModel.findOrCreate<MVideoBlacklistVideo>({
36 where: { 35 where: {
37 videoId: video.id 36 videoId: video.id
38 }, 37 },
@@ -41,7 +40,9 @@ async function autoBlacklistVideoIfNeeded (parameters: {
41 }) 40 })
42 video.VideoBlacklist = videoBlacklist 41 video.VideoBlacklist = videoBlacklist
43 42
44 if (notify) Notifier.Instance.notifyOnVideoAutoBlacklist(video) 43 videoBlacklist.Video = video
44
45 if (notify) Notifier.Instance.notifyOnVideoAutoBlacklist(videoBlacklist)
45 46
46 logger.info('Video %s auto-blacklisted.', video.uuid) 47 logger.info('Video %s auto-blacklisted.', video.uuid)
47 48
@@ -49,10 +50,10 @@ async function autoBlacklistVideoIfNeeded (parameters: {
49} 50}
50 51
51async function autoBlacklistNeeded (parameters: { 52async function autoBlacklistNeeded (parameters: {
52 video: VideoModel, 53 video: MVideoWithBlacklistLight,
53 isRemote: boolean, 54 isRemote: boolean,
54 isNew: boolean, 55 isNew: boolean,
55 user?: UserModel 56 user?: MUser
56}) { 57}) {
57 const { user, video, isRemote, isNew } = parameters 58 const { user, video, isRemote, isNew } = parameters
58 59
diff --git a/server/lib/video-channel.ts b/server/lib/video-channel.ts
index ee0482c36..41eab456b 100644
--- a/server/lib/video-channel.ts
+++ b/server/lib/video-channel.ts
@@ -1,12 +1,19 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import * as uuidv4 from 'uuid/v4' 2import * as uuidv4 from 'uuid/v4'
3import { VideoChannelCreate } from '../../shared/models' 3import { VideoChannelCreate } from '../../shared/models'
4import { AccountModel } from '../models/account/account'
5import { VideoChannelModel } from '../models/video/video-channel' 4import { VideoChannelModel } from '../models/video/video-channel'
6import { buildActorInstance, federateVideoIfNeeded, getVideoChannelActivityPubUrl } from './activitypub' 5import { buildActorInstance, federateVideoIfNeeded, getVideoChannelActivityPubUrl } from './activitypub'
7import { VideoModel } from '../models/video/video' 6import { VideoModel } from '../models/video/video'
7import { MAccountId, MChannelDefault, MChannelId } from '../typings/models'
8 8
9async function createVideoChannel (videoChannelInfo: VideoChannelCreate, account: AccountModel, t: Sequelize.Transaction) { 9type CustomVideoChannelModelAccount <T extends MAccountId> = MChannelDefault &
10 { Account?: T }
11
12async function createLocalVideoChannel <T extends MAccountId> (
13 videoChannelInfo: VideoChannelCreate,
14 account: T,
15 t: Sequelize.Transaction
16): Promise<CustomVideoChannelModelAccount<T>> {
10 const uuid = uuidv4() 17 const uuid = uuidv4()
11 const url = getVideoChannelActivityPubUrl(videoChannelInfo.name) 18 const url = getVideoChannelActivityPubUrl(videoChannelInfo.name)
12 const actorInstance = buildActorInstance('Group', url, videoChannelInfo.name, uuid) 19 const actorInstance = buildActorInstance('Group', url, videoChannelInfo.name, uuid)
@@ -21,10 +28,10 @@ async function createVideoChannel (videoChannelInfo: VideoChannelCreate, account
21 actorId: actorInstanceCreated.id 28 actorId: actorInstanceCreated.id
22 } 29 }
23 30
24 const videoChannel = VideoChannelModel.build(videoChannelData) 31 const videoChannel = new VideoChannelModel(videoChannelData)
25 32
26 const options = { transaction: t } 33 const options = { transaction: t }
27 const videoChannelCreated = await videoChannel.save(options) 34 const videoChannelCreated: CustomVideoChannelModelAccount<T> = await videoChannel.save(options) as MChannelDefault
28 35
29 // Do not forget to add Account/Actor information to the created video channel 36 // Do not forget to add Account/Actor information to the created video channel
30 videoChannelCreated.Account = account 37 videoChannelCreated.Account = account
@@ -34,7 +41,7 @@ async function createVideoChannel (videoChannelInfo: VideoChannelCreate, account
34 return videoChannelCreated 41 return videoChannelCreated
35} 42}
36 43
37async function federateAllVideosOfChannel (videoChannel: VideoChannelModel) { 44async function federateAllVideosOfChannel (videoChannel: MChannelId) {
38 const videoIds = await VideoModel.getAllIdsFromChannel(videoChannel) 45 const videoIds = await VideoModel.getAllIdsFromChannel(videoChannel)
39 46
40 for (const videoId of videoIds) { 47 for (const videoId of videoIds) {
@@ -47,6 +54,6 @@ async function federateAllVideosOfChannel (videoChannel: VideoChannelModel) {
47// --------------------------------------------------------------------------- 54// ---------------------------------------------------------------------------
48 55
49export { 56export {
50 createVideoChannel, 57 createLocalVideoChannel,
51 federateAllVideosOfChannel 58 federateAllVideosOfChannel
52} 59}
diff --git a/server/lib/video-comment.ts b/server/lib/video-comment.ts
index 449aa74cb..bb811bd2c 100644
--- a/server/lib/video-comment.ts
+++ b/server/lib/video-comment.ts
@@ -1,17 +1,16 @@
1import * as Sequelize from 'sequelize' 1import * 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 { AccountModel } from '../models/account/account'
5import { VideoModel } from '../models/video/video'
6import { VideoCommentModel } from '../models/video/video-comment' 4import { VideoCommentModel } from '../models/video/video-comment'
7import { getVideoCommentActivityPubUrl } from './activitypub' 5import { getVideoCommentActivityPubUrl } from './activitypub'
8import { sendCreateVideoComment } from './activitypub/send' 6import { sendCreateVideoComment } from './activitypub/send'
7import { MAccountDefault, MComment, MCommentOwnerVideoReply, MVideoFullLight } from '../typings/models'
9 8
10async function createVideoComment (obj: { 9async function createVideoComment (obj: {
11 text: string, 10 text: string,
12 inReplyToComment: VideoCommentModel | null, 11 inReplyToComment: MComment | null,
13 video: VideoModel 12 video: MVideoFullLight,
14 account: AccountModel 13 account: MAccountDefault
15}, t: Sequelize.Transaction) { 14}, t: Sequelize.Transaction) {
16 let originCommentId: number | null = null 15 let originCommentId: number | null = null
17 let inReplyToCommentId: number | null = null 16 let inReplyToCommentId: number | null = null
@@ -32,7 +31,7 @@ async function createVideoComment (obj: {
32 31
33 comment.url = getVideoCommentActivityPubUrl(obj.video, comment) 32 comment.url = getVideoCommentActivityPubUrl(obj.video, comment)
34 33
35 const savedComment = await comment.save({ transaction: t }) 34 const savedComment: MCommentOwnerVideoReply = await comment.save({ transaction: t })
36 savedComment.InReplyToVideoComment = obj.inReplyToComment 35 savedComment.InReplyToVideoComment = obj.inReplyToComment
37 savedComment.Video = obj.video 36 savedComment.Video = obj.video
38 savedComment.Account = obj.account 37 savedComment.Account = obj.account
diff --git a/server/lib/video-playlist.ts b/server/lib/video-playlist.ts
index 6e214e60f..29b70cfda 100644
--- a/server/lib/video-playlist.ts
+++ b/server/lib/video-playlist.ts
@@ -1,12 +1,13 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import { AccountModel } from '../models/account/account'
3import { VideoPlaylistModel } from '../models/video/video-playlist' 2import { VideoPlaylistModel } from '../models/video/video-playlist'
4import { VideoPlaylistPrivacy } from '../../shared/models/videos/playlist/video-playlist-privacy.model' 3import { VideoPlaylistPrivacy } from '../../shared/models/videos/playlist/video-playlist-privacy.model'
5import { getVideoPlaylistActivityPubUrl } from './activitypub' 4import { getVideoPlaylistActivityPubUrl } from './activitypub'
6import { 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'
7import { MVideoPlaylistOwner } from '../typings/models/video/video-playlist'
7 8
8async function createWatchLaterPlaylist (account: AccountModel, t: Sequelize.Transaction) { 9async function createWatchLaterPlaylist (account: MAccount, t: Sequelize.Transaction) {
9 const videoPlaylist = new VideoPlaylistModel({ 10 const videoPlaylist: MVideoPlaylistOwner = new VideoPlaylistModel({
10 name: 'Watch later', 11 name: 'Watch later',
11 privacy: VideoPlaylistPrivacy.PRIVATE, 12 privacy: VideoPlaylistPrivacy.PRIVATE,
12 type: VideoPlaylistType.WATCH_LATER, 13 type: VideoPlaylistType.WATCH_LATER,
diff --git a/server/lib/video-transcoding.ts b/server/lib/video-transcoding.ts
index ba6b29163..a204c0c63 100644
--- a/server/lib/video-transcoding.ts
+++ b/server/lib/video-transcoding.ts
@@ -5,16 +5,16 @@ import { ensureDir, move, remove, stat } from 'fs-extra'
5import { logger } from '../helpers/logger' 5import { logger } from '../helpers/logger'
6import { VideoResolution } from '../../shared/models/videos' 6import { VideoResolution } from '../../shared/models/videos'
7import { VideoFileModel } from '../models/video/video-file' 7import { VideoFileModel } from '../models/video/video-file'
8import { VideoModel } from '../models/video/video'
9import { updateMasterHLSPlaylist, updateSha256Segments } from './hls' 8import { updateMasterHLSPlaylist, updateSha256Segments } from './hls'
10import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' 9import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
11import { VideoStreamingPlaylistType } from '../../shared/models/videos/video-streaming-playlist.type' 10import { VideoStreamingPlaylistType } from '../../shared/models/videos/video-streaming-playlist.type'
12import { CONFIG } from '../initializers/config' 11import { CONFIG } from '../initializers/config'
12import { MVideoFile, MVideoWithFile, MVideoWithFileThumbnail } from '@server/typings/models'
13 13
14/** 14/**
15 * Optimize the original video file and replace it. The resolution is not changed. 15 * Optimize the original video file and replace it. The resolution is not changed.
16 */ 16 */
17async function optimizeVideofile (video: VideoModel, inputVideoFileArg?: VideoFileModel) { 17async function optimizeVideofile (video: MVideoWithFile, inputVideoFileArg?: MVideoFile) {
18 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR 18 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
19 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR 19 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
20 const newExtname = '.mp4' 20 const newExtname = '.mp4'
@@ -57,7 +57,7 @@ async function optimizeVideofile (video: VideoModel, inputVideoFileArg?: VideoFi
57/** 57/**
58 * Transcode the original video file to a lower resolution. 58 * Transcode the original video file to a lower resolution.
59 */ 59 */
60async function transcodeOriginalVideofile (video: VideoModel, resolution: VideoResolution, isPortrait: boolean) { 60async function transcodeOriginalVideofile (video: MVideoWithFile, resolution: VideoResolution, isPortrait: boolean) {
61 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR 61 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
62 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR 62 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
63 const extname = '.mp4' 63 const extname = '.mp4'
@@ -87,7 +87,7 @@ async function transcodeOriginalVideofile (video: VideoModel, resolution: VideoR
87 return onVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath) 87 return onVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath)
88} 88}
89 89
90async function mergeAudioVideofile (video: VideoModel, resolution: VideoResolution) { 90async function mergeAudioVideofile (video: MVideoWithFileThumbnail, resolution: VideoResolution) {
91 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR 91 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
92 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR 92 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
93 const newExtname = '.mp4' 93 const newExtname = '.mp4'
@@ -117,7 +117,7 @@ async function mergeAudioVideofile (video: VideoModel, resolution: VideoResoluti
117 return onVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath) 117 return onVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
118} 118}
119 119
120async function generateHlsPlaylist (video: VideoModel, resolution: VideoResolution, isPortraitMode: boolean) { 120async function generateHlsPlaylist (video: MVideoWithFile, resolution: VideoResolution, isPortraitMode: boolean) {
121 const baseHlsDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid) 121 const baseHlsDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)
122 await ensureDir(join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)) 122 await ensureDir(join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid))
123 123
@@ -165,14 +165,14 @@ export {
165 165
166// --------------------------------------------------------------------------- 166// ---------------------------------------------------------------------------
167 167
168async function onVideoFileTranscoding (video: VideoModel, videoFile: VideoFileModel, transcodingPath: string, outputPath: string) { 168async function onVideoFileTranscoding (video: MVideoWithFile, videoFile: MVideoFile, transcodingPath: string, outputPath: string) {
169 const stats = await stat(transcodingPath) 169 const stats = await stat(transcodingPath)
170 const fps = await getVideoFileFPS(transcodingPath) 170 const fps = await getVideoFileFPS(transcodingPath)
171 171
172 await move(transcodingPath, outputPath) 172 await move(transcodingPath, outputPath)
173 173
174 videoFile.set('size', stats.size) 174 videoFile.size = stats.size
175 videoFile.set('fps', fps) 175 videoFile.fps = fps
176 176
177 await video.createTorrentAndSetInfoHash(videoFile) 177 await video.createTorrentAndSetInfoHash(videoFile)
178 178
diff --git a/server/middlewares/activitypub.ts b/server/middlewares/activitypub.ts
index b1e5b5236..bea213d27 100644
--- a/server/middlewares/activitypub.ts
+++ b/server/middlewares/activitypub.ts
@@ -101,6 +101,8 @@ async function checkJsonLDSignature (req: Request, res: Response) {
101 const verified = await isJsonLDSignatureVerified(actor, req.body) 101 const verified = await isJsonLDSignatureVerified(actor, req.body)
102 102
103 if (verified !== true) { 103 if (verified !== true) {
104 logger.warn('Signature not verified.', req.body)
105
104 res.sendStatus(403) 106 res.sendStatus(403)
105 return false 107 return false
106 } 108 }
diff --git a/server/middlewares/validators/follows.ts b/server/middlewares/validators/follows.ts
index c3d772297..788735663 100644
--- a/server/middlewares/validators/follows.ts
+++ b/server/middlewares/validators/follows.ts
@@ -10,6 +10,7 @@ import { areValidationErrors } from './utils'
10import { ActorModel } from '../../models/activitypub/actor' 10import { ActorModel } from '../../models/activitypub/actor'
11import { loadActorUrlOrGetFromWebfinger } from '../../helpers/webfinger' 11import { loadActorUrlOrGetFromWebfinger } from '../../helpers/webfinger'
12import { isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor' 12import { isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor'
13import { MActorFollowActorsDefault } from '@server/typings/models'
13 14
14const followValidator = [ 15const followValidator = [
15 body('hosts').custom(isEachUniqueHostValid).withMessage('Should have an array of unique hosts'), 16 body('hosts').custom(isEachUniqueHostValid).withMessage('Should have an array of unique hosts'),
@@ -65,7 +66,7 @@ const getFollowerValidator = [
65 66
66 if (areValidationErrors(req, res)) return 67 if (areValidationErrors(req, res)) return
67 68
68 let follow: ActorFollowModel 69 let follow: MActorFollowActorsDefault
69 try { 70 try {
70 const actorUrl = await loadActorUrlOrGetFromWebfinger(req.params.nameWithHost) 71 const actorUrl = await loadActorUrlOrGetFromWebfinger(req.params.nameWithHost)
71 const actor = await ActorModel.loadByUrl(actorUrl) 72 const actor = await ActorModel.loadByUrl(actorUrl)
diff --git a/server/middlewares/validators/redundancy.ts b/server/middlewares/validators/redundancy.ts
index 1fdac0e4e..e65d3b8d3 100644
--- a/server/middlewares/validators/redundancy.ts
+++ b/server/middlewares/validators/redundancy.ts
@@ -24,7 +24,7 @@ const videoFileRedundancyGetValidator = [
24 if (areValidationErrors(req, res)) return 24 if (areValidationErrors(req, res)) return
25 if (!await doesVideoExist(req.params.videoId, res)) return 25 if (!await doesVideoExist(req.params.videoId, res)) return
26 26
27 const video = res.locals.video 27 const video = res.locals.videoAll
28 const videoFile = video.VideoFiles.find(f => { 28 const videoFile = video.VideoFiles.find(f => {
29 return f.resolution === req.params.resolution && (!req.params.fps || f.fps === req.params.fps) 29 return f.resolution === req.params.resolution && (!req.params.fps || f.fps === req.params.fps)
30 }) 30 })
@@ -50,7 +50,7 @@ const videoPlaylistRedundancyGetValidator = [
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 52
53 const video = res.locals.video 53 const video = res.locals.videoAll
54 const videoStreamingPlaylist = video.VideoStreamingPlaylists.find(p => p === req.params.streamingPlaylistType) 54 const videoStreamingPlaylist = video.VideoStreamingPlaylists.find(p => p === req.params.streamingPlaylistType)
55 55
56 if (!videoStreamingPlaylist) return res.status(404).json({ error: 'Video playlist not found.' }) 56 if (!videoStreamingPlaylist) return res.status(404).json({ error: 'Video playlist not found.' })
diff --git a/server/middlewares/validators/user-notifications.ts b/server/middlewares/validators/user-notifications.ts
index 308b32655..fbfcb0a4c 100644
--- a/server/middlewares/validators/user-notifications.ts
+++ b/server/middlewares/validators/user-notifications.ts
@@ -43,6 +43,8 @@ const updateNotificationSettingsValidator = [
43 .custom(isUserNotificationSettingValid).withMessage('Should have a valid new user registration notification setting'), 43 .custom(isUserNotificationSettingValid).withMessage('Should have a valid new user registration notification setting'),
44 body('newInstanceFollower') 44 body('newInstanceFollower')
45 .custom(isUserNotificationSettingValid).withMessage('Should have a valid new instance follower notification setting'), 45 .custom(isUserNotificationSettingValid).withMessage('Should have a valid new instance follower notification setting'),
46 body('autoInstanceFollowing')
47 .custom(isUserNotificationSettingValid).withMessage('Should have a valid new instance following notification setting'),
46 48
47 (req: express.Request, res: express.Response, next: express.NextFunction) => { 49 (req: express.Request, res: express.Response, next: express.NextFunction) => {
48 logger.debug('Checking updateNotificationSettingsValidator parameters', { parameters: req.body }) 50 logger.debug('Checking updateNotificationSettingsValidator parameters', { parameters: req.body })
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts
index 8ee2ec1f5..544db76d7 100644
--- a/server/middlewares/validators/users.ts
+++ b/server/middlewares/validators/users.ts
@@ -4,6 +4,7 @@ import { body, param } 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 {
7 isNoInstanceConfigWarningModal, isNoWelcomeModal,
7 isUserAdminFlagsValid, 8 isUserAdminFlagsValid,
8 isUserAutoPlayVideoValid, 9 isUserAutoPlayVideoValid,
9 isUserBlockedReasonValid, 10 isUserBlockedReasonValid,
@@ -31,6 +32,7 @@ import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
31import { isThemeRegistered } from '../../lib/plugins/theme-utils' 32import { isThemeRegistered } from '../../lib/plugins/theme-utils'
32import { doesVideoExist } from '../../helpers/middlewares' 33import { doesVideoExist } from '../../helpers/middlewares'
33import { UserRole } from '../../../shared/models/users' 34import { UserRole } from '../../../shared/models/users'
35import { MUserDefault } from '@server/typings/models'
34 36
35const usersAddValidator = [ 37const usersAddValidator = [
36 body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'), 38 body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'),
@@ -215,6 +217,12 @@ const usersUpdateMeValidator = [
215 body('theme') 217 body('theme')
216 .optional() 218 .optional()
217 .custom(v => isThemeNameValid(v) && isThemeRegistered(v)).withMessage('Should have a valid theme'), 219 .custom(v => isThemeNameValid(v) && isThemeRegistered(v)).withMessage('Should have a valid theme'),
220 body('noInstanceConfigWarningModal')
221 .optional()
222 .custom(v => isNoInstanceConfigWarningModal(v)).withMessage('Should have a valid noInstanceConfigWarningModal boolean'),
223 body('noWelcomeModal')
224 .optional()
225 .custom(v => isNoWelcomeModal(v)).withMessage('Should have a valid noWelcomeModal boolean'),
218 226
219 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 227 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
220 logger.debug('Checking usersUpdateMe parameters', { parameters: omit(req.body, 'password') }) 228 logger.debug('Checking usersUpdateMe parameters', { parameters: omit(req.body, 'password') })
@@ -462,7 +470,7 @@ async function checkUserNameOrEmailDoesNotAlreadyExist (username: string, email:
462 return true 470 return true
463} 471}
464 472
465async function checkUserExist (finder: () => Bluebird<UserModel>, res: express.Response, abortResponse = true) { 473async function checkUserExist (finder: () => Bluebird<MUserDefault>, res: express.Response, abortResponse = true) {
466 const user = await finder() 474 const user = await finder()
467 475
468 if (!user) { 476 if (!user) {
diff --git a/server/middlewares/validators/videos/video-abuses.ts b/server/middlewares/validators/videos/video-abuses.ts
index e27d91bb1..a4aef4024 100644
--- a/server/middlewares/validators/videos/video-abuses.ts
+++ b/server/middlewares/validators/videos/video-abuses.ts
@@ -33,7 +33,7 @@ const videoAbuseGetValidator = [
33 33
34 if (areValidationErrors(req, res)) return 34 if (areValidationErrors(req, res)) return
35 if (!await doesVideoExist(req.params.videoId, res)) return 35 if (!await doesVideoExist(req.params.videoId, res)) return
36 if (!await doesVideoAbuseExist(req.params.id, res.locals.video.id, 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 }
@@ -54,7 +54,7 @@ const videoAbuseUpdateValidator = [
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 doesVideoExist(req.params.videoId, res)) return
57 if (!await doesVideoAbuseExist(req.params.id, res.locals.video.id, res)) return 57 if (!await doesVideoAbuseExist(req.params.id, res.locals.videoAll.id, res)) return
58 58
59 return next() 59 return next()
60 } 60 }
diff --git a/server/middlewares/validators/videos/video-blacklist.ts b/server/middlewares/validators/videos/video-blacklist.ts
index 3e8c5b30c..5440e57e7 100644
--- a/server/middlewares/validators/videos/video-blacklist.ts
+++ b/server/middlewares/validators/videos/video-blacklist.ts
@@ -14,7 +14,7 @@ const videosBlacklistRemoveValidator = [
14 14
15 if (areValidationErrors(req, res)) return 15 if (areValidationErrors(req, res)) return
16 if (!await doesVideoExist(req.params.videoId, res)) return 16 if (!await doesVideoExist(req.params.videoId, res)) return
17 if (!await doesVideoBlacklistExist(res.locals.video.id, res)) return 17 if (!await doesVideoBlacklistExist(res.locals.videoAll.id, res)) return
18 18
19 return next() 19 return next()
20 } 20 }
@@ -36,7 +36,7 @@ const videosBlacklistAddValidator = [
36 if (areValidationErrors(req, res)) return 36 if (areValidationErrors(req, res)) return
37 if (!await doesVideoExist(req.params.videoId, res)) return 37 if (!await doesVideoExist(req.params.videoId, res)) return
38 38
39 const video = res.locals.video 39 const video = res.locals.videoAll
40 if (req.body.unfederate === true && video.remote === true) { 40 if (req.body.unfederate === true && video.remote === true) {
41 return res 41 return res
42 .status(409) 42 .status(409)
@@ -59,7 +59,7 @@ const videosBlacklistUpdateValidator = [
59 59
60 if (areValidationErrors(req, res)) return 60 if (areValidationErrors(req, res)) return
61 if (!await doesVideoExist(req.params.videoId, res)) return 61 if (!await doesVideoExist(req.params.videoId, res)) return
62 if (!await doesVideoBlacklistExist(res.locals.video.id, res)) return 62 if (!await doesVideoBlacklistExist(res.locals.videoAll.id, res)) return
63 63
64 return next() 64 return next()
65 } 65 }
diff --git a/server/middlewares/validators/videos/video-captions.ts b/server/middlewares/validators/videos/video-captions.ts
index f5610222a..2fb1da5ce 100644
--- a/server/middlewares/validators/videos/video-captions.ts
+++ b/server/middlewares/validators/videos/video-captions.ts
@@ -26,7 +26,7 @@ const addVideoCaptionValidator = [
26 26
27 // Check if the user who did the request is able to update the video 27 // Check if the user who did the request is able to update the video
28 const user = res.locals.oauth.token.User 28 const user = res.locals.oauth.token.User
29 if (!checkUserCanManageVideo(user, res.locals.video, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req) 29 if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
30 30
31 return next() 31 return next()
32 } 32 }
@@ -41,11 +41,11 @@ const deleteVideoCaptionValidator = [
41 41
42 if (areValidationErrors(req, res)) return 42 if (areValidationErrors(req, res)) return
43 if (!await doesVideoExist(req.params.videoId, res)) return 43 if (!await doesVideoExist(req.params.videoId, res)) return
44 if (!await doesVideoCaptionExist(res.locals.video, req.params.captionLanguage, res)) return 44 if (!await doesVideoCaptionExist(res.locals.videoAll, req.params.captionLanguage, res)) return
45 45
46 // Check if the user who did the request is able to update the video 46 // Check if the user who did the request is able to update the video
47 const user = res.locals.oauth.token.User 47 const user = res.locals.oauth.token.User
48 if (!checkUserCanManageVideo(user, res.locals.video, UserRight.UPDATE_ANY_VIDEO, res)) return 48 if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return
49 49
50 return next() 50 return next()
51 } 51 }
diff --git a/server/middlewares/validators/videos/video-channels.ts b/server/middlewares/validators/videos/video-channels.ts
index 3ee5064fc..d21274527 100644
--- a/server/middlewares/validators/videos/video-channels.ts
+++ b/server/middlewares/validators/videos/video-channels.ts
@@ -7,13 +7,13 @@ import {
7 isVideoChannelSupportValid 7 isVideoChannelSupportValid
8} from '../../../helpers/custom-validators/video-channels' 8} from '../../../helpers/custom-validators/video-channels'
9import { logger } from '../../../helpers/logger' 9import { logger } from '../../../helpers/logger'
10import { UserModel } from '../../../models/account/user'
11import { VideoChannelModel } from '../../../models/video/video-channel' 10import { VideoChannelModel } from '../../../models/video/video-channel'
12import { areValidationErrors } from '../utils' 11import { areValidationErrors } from '../utils'
13import { isActorPreferredUsernameValid } from '../../../helpers/custom-validators/activitypub/actor' 12import { isActorPreferredUsernameValid } from '../../../helpers/custom-validators/activitypub/actor'
14import { ActorModel } from '../../../models/activitypub/actor' 13import { ActorModel } from '../../../models/activitypub/actor'
15import { isBooleanValid } from '../../../helpers/custom-validators/misc' 14import { isBooleanValid } from '../../../helpers/custom-validators/misc'
16import { doesLocalVideoChannelNameExist, doesVideoChannelNameWithHostExist } from '../../../helpers/middlewares' 15import { doesLocalVideoChannelNameExist, doesVideoChannelNameWithHostExist } from '../../../helpers/middlewares'
16import { MChannelAccountDefault, MUser } from '@server/typings/models'
17 17
18const videoChannelsAddValidator = [ 18const videoChannelsAddValidator = [
19 body('name').custom(isActorPreferredUsernameValid).withMessage('Should have a valid channel name'), 19 body('name').custom(isActorPreferredUsernameValid).withMessage('Should have a valid channel name'),
@@ -131,7 +131,7 @@ export {
131 131
132// --------------------------------------------------------------------------- 132// ---------------------------------------------------------------------------
133 133
134function checkUserCanDeleteVideoChannel (user: UserModel, videoChannel: VideoChannelModel, res: express.Response) { 134function checkUserCanDeleteVideoChannel (user: MUser, videoChannel: MChannelAccountDefault, res: express.Response) {
135 if (videoChannel.Actor.isOwned() === false) { 135 if (videoChannel.Actor.isOwned() === false) {
136 res.status(403) 136 res.status(403)
137 .json({ error: 'Cannot remove video channel of another server.' }) 137 .json({ error: 'Cannot remove video channel of another server.' })
diff --git a/server/middlewares/validators/videos/video-comments.ts b/server/middlewares/validators/videos/video-comments.ts
index 83a0c24b0..8adbb02ba 100644
--- a/server/middlewares/validators/videos/video-comments.ts
+++ b/server/middlewares/validators/videos/video-comments.ts
@@ -4,13 +4,13 @@ import { UserRight } from '../../../../shared'
4import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc' 4import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc'
5import { isValidVideoCommentText } from '../../../helpers/custom-validators/video-comments' 5import { isValidVideoCommentText } from '../../../helpers/custom-validators/video-comments'
6import { logger } from '../../../helpers/logger' 6import { logger } from '../../../helpers/logger'
7import { UserModel } from '../../../models/account/user'
8import { VideoModel } from '../../../models/video/video'
9import { VideoCommentModel } from '../../../models/video/video-comment' 7import { VideoCommentModel } from '../../../models/video/video-comment'
10import { areValidationErrors } from '../utils' 8import { areValidationErrors } from '../utils'
11import { Hooks } from '../../../lib/plugins/hooks' 9import { Hooks } from '../../../lib/plugins/hooks'
12import { isLocalVideoThreadAccepted, isLocalVideoCommentReplyAccepted, AcceptResult } from '../../../lib/moderation' 10import { AcceptResult, isLocalVideoCommentReplyAccepted, isLocalVideoThreadAccepted } from '../../../lib/moderation'
13import { doesVideoExist } from '../../../helpers/middlewares' 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'),
@@ -34,7 +34,7 @@ const listVideoThreadCommentsValidator = [
34 34
35 if (areValidationErrors(req, res)) return 35 if (areValidationErrors(req, res)) return
36 if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return 36 if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return
37 if (!await doesVideoCommentThreadExist(req.params.threadId, res.locals.video, res)) return 37 if (!await doesVideoCommentThreadExist(req.params.threadId, res.locals.onlyVideo, res)) return
38 38
39 return next() 39 return next()
40 } 40 }
@@ -49,8 +49,8 @@ const addVideoCommentThreadValidator = [
49 49
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.video, res)) return 52 if (!isVideoCommentsEnabled(res.locals.videoAll, res)) return
53 if (!await isVideoCommentAccepted(req, res, false)) return 53 if (!await isVideoCommentAccepted(req, res, res.locals.videoAll,false)) return
54 54
55 return next() 55 return next()
56 } 56 }
@@ -66,9 +66,9 @@ const addVideoCommentReplyValidator = [
66 66
67 if (areValidationErrors(req, res)) return 67 if (areValidationErrors(req, res)) return
68 if (!await doesVideoExist(req.params.videoId, res)) return 68 if (!await doesVideoExist(req.params.videoId, res)) return
69 if (!isVideoCommentsEnabled(res.locals.video, res)) return 69 if (!isVideoCommentsEnabled(res.locals.videoAll, res)) return
70 if (!await doesVideoCommentExist(req.params.commentId, res.locals.video, res)) return 70 if (!await doesVideoCommentExist(req.params.commentId, res.locals.videoAll, res)) return
71 if (!await isVideoCommentAccepted(req, res, true)) return 71 if (!await isVideoCommentAccepted(req, res, res.locals.videoAll, true)) return
72 72
73 return next() 73 return next()
74 } 74 }
@@ -83,7 +83,7 @@ const videoCommentGetValidator = [
83 83
84 if (areValidationErrors(req, res)) return 84 if (areValidationErrors(req, res)) return
85 if (!await doesVideoExist(req.params.videoId, res, 'id')) return 85 if (!await doesVideoExist(req.params.videoId, res, 'id')) return
86 if (!await doesVideoCommentExist(req.params.commentId, res.locals.video, res)) return 86 if (!await doesVideoCommentExist(req.params.commentId, res.locals.videoId, res)) return
87 87
88 return next() 88 return next()
89 } 89 }
@@ -98,10 +98,10 @@ const removeVideoCommentValidator = [
98 98
99 if (areValidationErrors(req, res)) return 99 if (areValidationErrors(req, res)) return
100 if (!await doesVideoExist(req.params.videoId, res)) return 100 if (!await doesVideoExist(req.params.videoId, res)) return
101 if (!await doesVideoCommentExist(req.params.commentId, res.locals.video, res)) return 101 if (!await doesVideoCommentExist(req.params.commentId, res.locals.videoAll, res)) return
102 102
103 // Check if the user who did the request is able to delete the video 103 // Check if the user who did the request is able to delete the video
104 if (!checkUserCanDeleteVideoComment(res.locals.oauth.token.User, res.locals.videoComment, res)) return 104 if (!checkUserCanDeleteVideoComment(res.locals.oauth.token.User, res.locals.videoCommentFull, res)) return
105 105
106 return next() 106 return next()
107 } 107 }
@@ -120,7 +120,7 @@ export {
120 120
121// --------------------------------------------------------------------------- 121// ---------------------------------------------------------------------------
122 122
123async function doesVideoCommentThreadExist (id: number, video: VideoModel, res: express.Response) { 123async function doesVideoCommentThreadExist (id: number, video: MVideoId, res: express.Response) {
124 const videoComment = await VideoCommentModel.loadById(id) 124 const videoComment = await VideoCommentModel.loadById(id)
125 125
126 if (!videoComment) { 126 if (!videoComment) {
@@ -151,7 +151,7 @@ async function doesVideoCommentThreadExist (id: number, video: VideoModel, res:
151 return true 151 return true
152} 152}
153 153
154async function doesVideoCommentExist (id: number, video: VideoModel, res: express.Response) { 154async function doesVideoCommentExist (id: number, video: MVideoId, res: express.Response) {
155 const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id) 155 const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id)
156 156
157 if (!videoComment) { 157 if (!videoComment) {
@@ -170,11 +170,11 @@ async function doesVideoCommentExist (id: number, video: VideoModel, res: expres
170 return false 170 return false
171 } 171 }
172 172
173 res.locals.videoComment = videoComment 173 res.locals.videoCommentFull = videoComment
174 return true 174 return true
175} 175}
176 176
177function isVideoCommentsEnabled (video: VideoModel, res: express.Response) { 177function isVideoCommentsEnabled (video: MVideo, res: express.Response) {
178 if (video.commentsEnabled !== true) { 178 if (video.commentsEnabled !== true) {
179 res.status(409) 179 res.status(409)
180 .json({ error: 'Video comments are disabled for this video.' }) 180 .json({ error: 'Video comments are disabled for this video.' })
@@ -186,7 +186,7 @@ function isVideoCommentsEnabled (video: VideoModel, res: express.Response) {
186 return true 186 return true
187} 187}
188 188
189function checkUserCanDeleteVideoComment (user: UserModel, videoComment: VideoCommentModel, res: express.Response) { 189function checkUserCanDeleteVideoComment (user: MUser, videoComment: MCommentOwner, res: express.Response) {
190 const account = videoComment.Account 190 const account = videoComment.Account
191 if (user.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT) === false && account.userId !== user.id) { 191 if (user.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT) === false && account.userId !== user.id) {
192 res.status(403) 192 res.status(403)
@@ -198,9 +198,9 @@ function checkUserCanDeleteVideoComment (user: UserModel, videoComment: VideoCom
198 return true 198 return true
199} 199}
200 200
201async function isVideoCommentAccepted (req: express.Request, res: express.Response, isReply: boolean) { 201async function isVideoCommentAccepted (req: express.Request, res: express.Response, video: MVideoFullLight, isReply: boolean) {
202 const acceptParameters = { 202 const acceptParameters = {
203 video: res.locals.video, 203 video,
204 commentBody: req.body, 204 commentBody: req.body,
205 user: res.locals.oauth.token.User 205 user: res.locals.oauth.token.User
206 } 206 }
@@ -208,7 +208,7 @@ async function isVideoCommentAccepted (req: express.Request, res: express.Respon
208 let acceptedResult: AcceptResult 208 let acceptedResult: AcceptResult
209 209
210 if (isReply) { 210 if (isReply) {
211 const acceptReplyParameters = Object.assign(acceptParameters, { parentComment: res.locals.videoComment }) 211 const acceptReplyParameters = Object.assign(acceptParameters, { parentComment: res.locals.videoCommentFull })
212 212
213 acceptedResult = await Hooks.wrapFun( 213 acceptedResult = await Hooks.wrapFun(
214 isLocalVideoCommentReplyAccepted, 214 isLocalVideoCommentReplyAccepted,
diff --git a/server/middlewares/validators/videos/video-playlists.ts b/server/middlewares/validators/videos/video-playlists.ts
index 5823795be..27ee62b1f 100644
--- a/server/middlewares/validators/videos/video-playlists.ts
+++ b/server/middlewares/validators/videos/video-playlists.ts
@@ -2,7 +2,6 @@ import * as express from 'express'
2import { body, param, query, ValidationChain } from 'express-validator' 2import { body, param, query, ValidationChain } from 'express-validator'
3import { UserRight, VideoPlaylistCreate, VideoPlaylistUpdate } from '../../../../shared' 3import { UserRight, VideoPlaylistCreate, VideoPlaylistUpdate } from '../../../../shared'
4import { logger } from '../../../helpers/logger' 4import { logger } from '../../../helpers/logger'
5import { UserModel } from '../../../models/account/user'
6import { areValidationErrors } from '../utils' 5import { areValidationErrors } from '../utils'
7import { isVideoImage } from '../../../helpers/custom-validators/videos' 6import { isVideoImage } from '../../../helpers/custom-validators/videos'
8import { CONSTRAINTS_FIELDS } from '../../../initializers/constants' 7import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
@@ -22,13 +21,14 @@ import {
22 isVideoPlaylistTimestampValid, 21 isVideoPlaylistTimestampValid,
23 isVideoPlaylistTypeValid 22 isVideoPlaylistTypeValid
24} from '../../../helpers/custom-validators/video-playlists' 23} from '../../../helpers/custom-validators/video-playlists'
25import { VideoPlaylistModel } from '../../../models/video/video-playlist'
26import { cleanUpReqFiles } from '../../../helpers/express-utils' 24import { cleanUpReqFiles } from '../../../helpers/express-utils'
27import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-element' 25import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-element'
28import { authenticatePromiseIfNeeded } from '../../oauth' 26import { authenticatePromiseIfNeeded } from '../../oauth'
29import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' 27import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
30import { VideoPlaylistType } from '../../../../shared/models/videos/playlist/video-playlist-type.model' 28import { VideoPlaylistType } from '../../../../shared/models/videos/playlist/video-playlist-type.model'
31import { doesVideoChannelIdExist, doesVideoExist, doesVideoPlaylistExist } from '../../../helpers/middlewares' 29import { doesVideoChannelIdExist, doesVideoExist, doesVideoPlaylistExist, VideoPlaylistFetchType } from '../../../helpers/middlewares'
30import { MVideoPlaylist } from '../../../typings/models/video/video-playlist'
31import { MUserAccountId } from '@server/typings/models'
32 32
33const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([ 33const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([
34 body('displayName') 34 body('displayName')
@@ -67,9 +67,9 @@ const videoPlaylistsUpdateValidator = getCommonPlaylistEditAttributes().concat([
67 67
68 if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return cleanUpReqFiles(req) 68 if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return cleanUpReqFiles(req)
69 69
70 const videoPlaylist = res.locals.videoPlaylist 70 const videoPlaylist = getPlaylist(res)
71 71
72 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, res.locals.videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) { 72 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) {
73 return cleanUpReqFiles(req) 73 return cleanUpReqFiles(req)
74 } 74 }
75 75
@@ -110,13 +110,13 @@ const videoPlaylistsDeleteValidator = [
110 110
111 if (!await doesVideoPlaylistExist(req.params.playlistId, res)) return 111 if (!await doesVideoPlaylistExist(req.params.playlistId, res)) return
112 112
113 const videoPlaylist = res.locals.videoPlaylist 113 const videoPlaylist = getPlaylist(res)
114 if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) { 114 if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) {
115 return res.status(400) 115 return res.status(400)
116 .json({ error: 'Cannot delete a watch later playlist.' }) 116 .json({ error: 'Cannot delete a watch later playlist.' })
117 } 117 }
118 118
119 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, res.locals.videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) { 119 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) {
120 return 120 return
121 } 121 }
122 122
@@ -124,45 +124,47 @@ const videoPlaylistsDeleteValidator = [
124 } 124 }
125] 125]
126 126
127const videoPlaylistsGetValidator = [ 127const videoPlaylistsGetValidator = (fetchType: VideoPlaylistFetchType) => {
128 param('playlistId') 128 return [
129 .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'), 129 param('playlistId')
130 .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'),
130 131
131 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 132 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
132 logger.debug('Checking videoPlaylistsGetValidator parameters', { parameters: req.params }) 133 logger.debug('Checking videoPlaylistsGetValidator parameters', { parameters: req.params })
133 134
134 if (areValidationErrors(req, res)) return 135 if (areValidationErrors(req, res)) return
135 136
136 if (!await doesVideoPlaylistExist(req.params.playlistId, res)) return 137 if (!await doesVideoPlaylistExist(req.params.playlistId, res, fetchType)) return
137 138
138 const videoPlaylist = res.locals.videoPlaylist 139 const videoPlaylist = res.locals.videoPlaylistFull || res.locals.videoPlaylistSummary
139 140
140 // Video is unlisted, check we used the uuid to fetch it 141 // Video is unlisted, check we used the uuid to fetch it
141 if (videoPlaylist.privacy === VideoPlaylistPrivacy.UNLISTED) { 142 if (videoPlaylist.privacy === VideoPlaylistPrivacy.UNLISTED) {
142 if (isUUIDValid(req.params.playlistId)) return next() 143 if (isUUIDValid(req.params.playlistId)) return next()
143 144
144 return res.status(404).end() 145 return res.status(404).end()
145 } 146 }
147
148 if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) {
149 await authenticatePromiseIfNeeded(req, res)
146 150
147 if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) { 151 const user = res.locals.oauth ? res.locals.oauth.token.User : null
148 await authenticatePromiseIfNeeded(req, res)
149 152
150 const user = res.locals.oauth ? res.locals.oauth.token.User : null 153 if (
154 !user ||
155 (videoPlaylist.OwnerAccount.id !== user.Account.id && !user.hasRight(UserRight.UPDATE_ANY_VIDEO_PLAYLIST))
156 ) {
157 return res.status(403)
158 .json({ error: 'Cannot get this private video playlist.' })
159 }
151 160
152 if ( 161 return next()
153 !user ||
154 (videoPlaylist.OwnerAccount.id !== user.Account.id && !user.hasRight(UserRight.UPDATE_ANY_VIDEO_PLAYLIST))
155 ) {
156 return res.status(403)
157 .json({ error: 'Cannot get this private video playlist.' })
158 } 162 }
159 163
160 return next() 164 return next()
161 } 165 }
162 166 ]
163 return next() 167}
164 }
165]
166 168
167const videoPlaylistsAddVideoValidator = [ 169const videoPlaylistsAddVideoValidator = [
168 param('playlistId') 170 param('playlistId')
@@ -184,8 +186,8 @@ const videoPlaylistsAddVideoValidator = [
184 if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return 186 if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return
185 if (!await doesVideoExist(req.body.videoId, res, 'only-video')) return 187 if (!await doesVideoExist(req.body.videoId, res, 'only-video')) return
186 188
187 const videoPlaylist = res.locals.videoPlaylist 189 const videoPlaylist = getPlaylist(res)
188 const video = res.locals.video 190 const video = res.locals.onlyVideo
189 191
190 const videoPlaylistElement = await VideoPlaylistElementModel.loadByPlaylistAndVideo(videoPlaylist.id, video.id) 192 const videoPlaylistElement = await VideoPlaylistElementModel.loadByPlaylistAndVideo(videoPlaylist.id, video.id)
191 if (videoPlaylistElement) { 193 if (videoPlaylistElement) {
@@ -196,7 +198,7 @@ const videoPlaylistsAddVideoValidator = [
196 return 198 return
197 } 199 }
198 200
199 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, res.locals.videoPlaylist, UserRight.UPDATE_ANY_VIDEO_PLAYLIST, res)) { 201 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.UPDATE_ANY_VIDEO_PLAYLIST, res)) {
200 return 202 return
201 } 203 }
202 204
@@ -223,7 +225,7 @@ const videoPlaylistsUpdateOrRemoveVideoValidator = [
223 225
224 if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return 226 if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return
225 227
226 const videoPlaylist = res.locals.videoPlaylist 228 const videoPlaylist = getPlaylist(res)
227 229
228 const videoPlaylistElement = await VideoPlaylistElementModel.loadById(req.params.playlistElementId) 230 const videoPlaylistElement = await VideoPlaylistElementModel.loadById(req.params.playlistElementId)
229 if (!videoPlaylistElement) { 231 if (!videoPlaylistElement) {
@@ -265,7 +267,7 @@ const videoPlaylistElementAPGetValidator = [
265 return res.status(403).end() 267 return res.status(403).end()
266 } 268 }
267 269
268 res.locals.videoPlaylistElement = videoPlaylistElement 270 res.locals.videoPlaylistElementAP = videoPlaylistElement
269 271
270 return next() 272 return next()
271 } 273 }
@@ -289,7 +291,7 @@ const videoPlaylistsReorderVideosValidator = [
289 291
290 if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return 292 if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return
291 293
292 const videoPlaylist = res.locals.videoPlaylist 294 const videoPlaylist = getPlaylist(res)
293 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.UPDATE_ANY_VIDEO_PLAYLIST, res)) return 295 if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.UPDATE_ANY_VIDEO_PLAYLIST, res)) return
294 296
295 const nextPosition = await VideoPlaylistElementModel.getNextPositionOf(videoPlaylist.id) 297 const nextPosition = await VideoPlaylistElementModel.getNextPositionOf(videoPlaylist.id)
@@ -388,7 +390,7 @@ function getCommonPlaylistEditAttributes () {
388 ] as (ValidationChain | express.Handler)[] 390 ] as (ValidationChain | express.Handler)[]
389} 391}
390 392
391function checkUserCanManageVideoPlaylist (user: UserModel, videoPlaylist: VideoPlaylistModel, right: UserRight, res: express.Response) { 393function checkUserCanManageVideoPlaylist (user: MUserAccountId, videoPlaylist: MVideoPlaylist, right: UserRight, res: express.Response) {
392 if (videoPlaylist.isOwned() === false) { 394 if (videoPlaylist.isOwned() === false) {
393 res.status(403) 395 res.status(403)
394 .json({ error: 'Cannot manage video playlist of another server.' }) 396 .json({ error: 'Cannot manage video playlist of another server.' })
@@ -410,3 +412,7 @@ function checkUserCanManageVideoPlaylist (user: UserModel, videoPlaylist: VideoP
410 412
411 return true 413 return true
412} 414}
415
416function getPlaylist (res: express.Response) {
417 return res.locals.videoPlaylistFull || res.locals.videoPlaylistSummary
418}
diff --git a/server/middlewares/validators/videos/video-shares.ts b/server/middlewares/validators/videos/video-shares.ts
index ace62be5c..20fc96243 100644
--- a/server/middlewares/validators/videos/video-shares.ts
+++ b/server/middlewares/validators/videos/video-shares.ts
@@ -16,7 +16,7 @@ const videosShareValidator = [
16 if (areValidationErrors(req, res)) return 16 if (areValidationErrors(req, res)) return
17 if (!await doesVideoExist(req.params.id, res)) return 17 if (!await doesVideoExist(req.params.id, res)) return
18 18
19 const video = res.locals.video 19 const video = res.locals.videoAll
20 20
21 const share = await VideoShareModel.load(req.params.actorId, video.id) 21 const share = await VideoShareModel.load(req.params.actorId, video.id)
22 if (!share) { 22 if (!share) {
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts
index af06f3c62..1449903b7 100644
--- a/server/middlewares/validators/videos/videos.ts
+++ b/server/middlewares/validators/videos/videos.ts
@@ -37,13 +37,14 @@ import { VideoModel } from '../../../models/video/video'
37import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership' 37import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership'
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 { VideoFetchType } from '../../../helpers/video'
41import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search' 40import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
42import { getServerActor } from '../../../helpers/utils' 41import { getServerActor } from '../../../helpers/utils'
43import { CONFIG } from '../../../initializers/config' 42import { CONFIG } from '../../../initializers/config'
44import { isLocalVideoAccepted } from '../../../lib/moderation' 43import { isLocalVideoAccepted } from '../../../lib/moderation'
45import { Hooks } from '../../../lib/plugins/hooks' 44import { Hooks } from '../../../lib/plugins/hooks'
46import { checkUserCanManageVideo, doesVideoChannelOfAccountExist, doesVideoExist } from '../../../helpers/middlewares' 45import { checkUserCanManageVideo, doesVideoChannelOfAccountExist, doesVideoExist } from '../../../helpers/middlewares'
46import { MVideoFullLight } from '@server/typings/models'
47import { getVideoWithAttributes } from '../../../helpers/video'
47 48
48const videosAddValidator = getCommonVideoEditAttributes().concat([ 49const videosAddValidator = getCommonVideoEditAttributes().concat([
49 body('videofile') 50 body('videofile')
@@ -113,7 +114,7 @@ const videosUpdateValidator = getCommonVideoEditAttributes().concat([
113 114
114 // Check if the user who did the request is able to update the video 115 // Check if the user who did the request is able to update the video
115 const user = res.locals.oauth.token.User 116 const user = res.locals.oauth.token.User
116 if (!checkUserCanManageVideo(user, res.locals.video, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req) 117 if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
117 118
118 if (req.body.channelId && !await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req) 119 if (req.body.channelId && !await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
119 120
@@ -122,7 +123,7 @@ const videosUpdateValidator = getCommonVideoEditAttributes().concat([
122]) 123])
123 124
124async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) { 125async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) {
125 const video = res.locals.video 126 const video = getVideoWithAttributes(res)
126 127
127 // Anybody can watch local videos 128 // Anybody can watch local videos
128 if (video.isOwned() === true) return next() 129 if (video.isOwned() === true) return next()
@@ -146,7 +147,7 @@ async function checkVideoFollowConstraints (req: express.Request, res: express.R
146 }) 147 })
147} 148}
148 149
149const videosCustomGetValidator = (fetchType: VideoFetchType) => { 150const videosCustomGetValidator = (fetchType: 'all' | 'only-video' | 'only-video-with-rights') => {
150 return [ 151 return [
151 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), 152 param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
152 153
@@ -156,10 +157,11 @@ const videosCustomGetValidator = (fetchType: VideoFetchType) => {
156 if (areValidationErrors(req, res)) return 157 if (areValidationErrors(req, res)) return
157 if (!await doesVideoExist(req.params.id, res, fetchType)) return 158 if (!await doesVideoExist(req.params.id, res, fetchType)) return
158 159
159 const video = res.locals.video 160 const video = getVideoWithAttributes(res)
161 const videoAll = video as MVideoFullLight
160 162
161 // Video private or blacklisted 163 // Video private or blacklisted
162 if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) { 164 if (video.privacy === VideoPrivacy.PRIVATE || videoAll.VideoBlacklist) {
163 await authenticatePromiseIfNeeded(req, res) 165 await authenticatePromiseIfNeeded(req, res)
164 166
165 const user = res.locals.oauth ? res.locals.oauth.token.User : null 167 const user = res.locals.oauth ? res.locals.oauth.token.User : null
@@ -167,7 +169,7 @@ const videosCustomGetValidator = (fetchType: VideoFetchType) => {
167 // Only the owner or a user that have blacklist rights can see the video 169 // Only the owner or a user that have blacklist rights can see the video
168 if ( 170 if (
169 !user || 171 !user ||
170 (video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) 172 (videoAll.VideoChannel && videoAll.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST))
171 ) { 173 ) {
172 return res.status(403) 174 return res.status(403)
173 .json({ error: 'Cannot get this private or blacklisted video.' }) 175 .json({ error: 'Cannot get this private or blacklisted video.' })
@@ -202,7 +204,7 @@ const videosRemoveValidator = [
202 if (!await doesVideoExist(req.params.id, res)) return 204 if (!await doesVideoExist(req.params.id, res)) return
203 205
204 // Check if the user who did the request is able to delete the video 206 // Check if the user who did the request is able to delete the video
205 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.REMOVE_ANY_VIDEO, res)) return 207 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.REMOVE_ANY_VIDEO, res)) return
206 208
207 return next() 209 return next()
208 } 210 }
@@ -218,7 +220,7 @@ const videosChangeOwnershipValidator = [
218 if (!await doesVideoExist(req.params.videoId, res)) return 220 if (!await doesVideoExist(req.params.videoId, res)) return
219 221
220 // Check if the user who did the request is able to change the ownership of the video 222 // Check if the user who did the request is able to change the ownership of the video
221 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return 223 if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return
222 224
223 const nextOwner = await AccountModel.loadLocalByName(req.body.username) 225 const nextOwner = await AccountModel.loadLocalByName(req.body.username)
224 if (!nextOwner) { 226 if (!nextOwner) {
diff --git a/server/middlewares/validators/webfinger.ts b/server/middlewares/validators/webfinger.ts
index d7cfe17f0..d50e6527f 100644
--- a/server/middlewares/validators/webfinger.ts
+++ b/server/middlewares/validators/webfinger.ts
@@ -18,6 +18,7 @@ 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.loadLocalByName(name) 22 const actor = await ActorModel.loadLocalByName(name)
22 if (!actor) { 23 if (!actor) {
23 return res.status(404) 24 return res.status(404)
@@ -25,7 +26,7 @@ const webfingerValidator = [
25 .end() 26 .end()
26 } 27 }
27 28
28 res.locals.actor = actor 29 res.locals.actorFull = actor
29 return next() 30 return next()
30 } 31 }
31] 32]
diff --git a/server/models/account/account-blocklist.ts b/server/models/account/account-blocklist.ts
index d5746ad76..8bcaca828 100644
--- a/server/models/account/account-blocklist.ts
+++ b/server/models/account/account-blocklist.ts
@@ -3,6 +3,8 @@ import { AccountModel } from './account'
3import { getSort } from '../utils' 3import { getSort } from '../utils'
4import { AccountBlock } from '../../../shared/models/blocklist' 4import { AccountBlock } from '../../../shared/models/blocklist'
5import { Op } from 'sequelize' 5import { Op } from 'sequelize'
6import * as Bluebird from 'bluebird'
7import { MAccountBlocklist, MAccountBlocklistAccounts, MAccountBlocklistFormattable } from '@server/typings/models'
6 8
7enum ScopeNames { 9enum ScopeNames {
8 WITH_ACCOUNTS = 'WITH_ACCOUNTS' 10 WITH_ACCOUNTS = 'WITH_ACCOUNTS'
@@ -103,7 +105,7 @@ export class AccountBlocklistModel extends Model<AccountBlocklistModel> {
103 }) 105 })
104 } 106 }
105 107
106 static loadByAccountAndTarget (accountId: number, targetAccountId: number) { 108 static loadByAccountAndTarget (accountId: number, targetAccountId: number): Bluebird<MAccountBlocklist> {
107 const query = { 109 const query = {
108 where: { 110 where: {
109 accountId, 111 accountId,
@@ -126,13 +128,13 @@ export class AccountBlocklistModel extends Model<AccountBlocklistModel> {
126 128
127 return AccountBlocklistModel 129 return AccountBlocklistModel
128 .scope([ ScopeNames.WITH_ACCOUNTS ]) 130 .scope([ ScopeNames.WITH_ACCOUNTS ])
129 .findAndCountAll(query) 131 .findAndCountAll<MAccountBlocklistAccounts>(query)
130 .then(({ rows, count }) => { 132 .then(({ rows, count }) => {
131 return { total: count, data: rows } 133 return { total: count, data: rows }
132 }) 134 })
133 } 135 }
134 136
135 toFormattedJSON (): AccountBlock { 137 toFormattedJSON (this: MAccountBlocklistFormattable): AccountBlock {
136 return { 138 return {
137 byAccount: this.ByAccount.toFormattedJSON(), 139 byAccount: this.ByAccount.toFormattedJSON(),
138 blockedAccount: this.BlockedAccount.toFormattedJSON(), 140 blockedAccount: this.BlockedAccount.toFormattedJSON(),
diff --git a/server/models/account/account-video-rate.ts b/server/models/account/account-video-rate.ts
index 4bd8114cf..a6edbeee8 100644
--- a/server/models/account/account-video-rate.ts
+++ b/server/models/account/account-video-rate.ts
@@ -10,6 +10,13 @@ import { buildLocalAccountIdsIn, getSort, throwIfNotValid } from '../utils'
10import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 10import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
11import { AccountVideoRate } from '../../../shared' 11import { AccountVideoRate } from '../../../shared'
12import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel' 12import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel'
13import * as Bluebird from 'bluebird'
14import {
15 MAccountVideoRate,
16 MAccountVideoRateAccountUrl,
17 MAccountVideoRateAccountVideo,
18 MAccountVideoRateFormattable
19} from '@server/typings/models/video/video-rate'
13 20
14/* 21/*
15 Account rates per video. 22 Account rates per video.
@@ -77,7 +84,7 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
77 }) 84 })
78 Account: AccountModel 85 Account: AccountModel
79 86
80 static load (accountId: number, videoId: number, transaction?: Transaction) { 87 static load (accountId: number, videoId: number, transaction?: Transaction): Bluebird<MAccountVideoRate> {
81 const options: FindOptions = { 88 const options: FindOptions = {
82 where: { 89 where: {
83 accountId, 90 accountId,
@@ -89,7 +96,7 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
89 return AccountVideoRateModel.findOne(options) 96 return AccountVideoRateModel.findOne(options)
90 } 97 }
91 98
92 static loadByAccountAndVideoOrUrl (accountId: number, videoId: number, url: string, transaction?: Transaction) { 99 static loadByAccountAndVideoOrUrl (accountId: number, videoId: number, url: string, t?: Transaction): Bluebird<MAccountVideoRate> {
93 const options: FindOptions = { 100 const options: FindOptions = {
94 where: { 101 where: {
95 [ Op.or]: [ 102 [ Op.or]: [
@@ -103,7 +110,7 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
103 ] 110 ]
104 } 111 }
105 } 112 }
106 if (transaction) options.transaction = transaction 113 if (t) options.transaction = t
107 114
108 return AccountVideoRateModel.findOne(options) 115 return AccountVideoRateModel.findOne(options)
109 } 116 }
@@ -140,7 +147,12 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
140 return AccountVideoRateModel.findAndCountAll(query) 147 return AccountVideoRateModel.findAndCountAll(query)
141 } 148 }
142 149
143 static loadLocalAndPopulateVideo (rateType: VideoRateType, accountName: string, videoId: number, transaction?: Transaction) { 150 static loadLocalAndPopulateVideo (
151 rateType: VideoRateType,
152 accountName: string,
153 videoId: number,
154 t?: Transaction
155 ): Bluebird<MAccountVideoRateAccountVideo> {
144 const options: FindOptions = { 156 const options: FindOptions = {
145 where: { 157 where: {
146 videoId, 158 videoId,
@@ -152,7 +164,7 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
152 required: true, 164 required: true,
153 include: [ 165 include: [
154 { 166 {
155 attributes: [ 'id', 'url', 'preferredUsername' ], 167 attributes: [ 'id', 'url', 'followersUrl', 'preferredUsername' ],
156 model: ActorModel.unscoped(), 168 model: ActorModel.unscoped(),
157 required: true, 169 required: true,
158 where: { 170 where: {
@@ -167,7 +179,7 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
167 } 179 }
168 ] 180 ]
169 } 181 }
170 if (transaction) options.transaction = transaction 182 if (t) options.transaction = t
171 183
172 return AccountVideoRateModel.findOne(options) 184 return AccountVideoRateModel.findOne(options)
173 } 185 }
@@ -208,7 +220,7 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
208 ] 220 ]
209 } 221 }
210 222
211 return AccountVideoRateModel.findAndCountAll(query) 223 return AccountVideoRateModel.findAndCountAll<MAccountVideoRateAccountUrl>(query)
212 } 224 }
213 225
214 static cleanOldRatesOf (videoId: number, type: VideoRateType, beforeUpdatedAt: Date) { 226 static cleanOldRatesOf (videoId: number, type: VideoRateType, beforeUpdatedAt: Date) {
@@ -241,7 +253,7 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
241 }) 253 })
242 } 254 }
243 255
244 toFormattedJSON (): AccountVideoRate { 256 toFormattedJSON (this: MAccountVideoRateFormattable): AccountVideoRate {
245 return { 257 return {
246 video: this.Video.toFormattedJSON(), 258 video: this.Video.toFormattedJSON(),
247 rating: this.type 259 rating: this.type
diff --git a/server/models/account/account.ts b/server/models/account/account.ts
index 4dc412301..ba1094536 100644
--- a/server/models/account/account.ts
+++ b/server/models/account/account.ts
@@ -3,7 +3,8 @@ import {
3 BeforeDestroy, 3 BeforeDestroy,
4 BelongsTo, 4 BelongsTo,
5 Column, 5 Column,
6 CreatedAt, DataType, 6 CreatedAt,
7 DataType,
7 Default, 8 Default,
8 DefaultScope, 9 DefaultScope,
9 ForeignKey, 10 ForeignKey,
@@ -31,6 +32,8 @@ import { FindOptions, IncludeOptions, Op, Transaction, WhereOptions } from 'sequ
31import { AccountBlocklistModel } from './account-blocklist' 32import { AccountBlocklistModel } from './account-blocklist'
32import { ServerBlocklistModel } from '../server/server-blocklist' 33import { ServerBlocklistModel } from '../server/server-blocklist'
33import { ActorFollowModel } from '../activitypub/actor-follow' 34import { ActorFollowModel } from '../activitypub/actor-follow'
35import { MAccountActor, MAccountDefault, MAccountSummaryFormattable, MAccountFormattable, MAccountAP } from '../../typings/models'
36import * as Bluebird from 'bluebird'
34 37
35export enum ScopeNames { 38export enum ScopeNames {
36 SUMMARY = 'SUMMARY' 39 SUMMARY = 'SUMMARY'
@@ -229,11 +232,11 @@ export class AccountModel extends Model<AccountModel> {
229 return undefined 232 return undefined
230 } 233 }
231 234
232 static load (id: number, transaction?: Transaction) { 235 static load (id: number, transaction?: Transaction): Bluebird<MAccountDefault> {
233 return AccountModel.findByPk(id, { transaction }) 236 return AccountModel.findByPk(id, { transaction })
234 } 237 }
235 238
236 static loadByNameWithHost (nameWithHost: string) { 239 static loadByNameWithHost (nameWithHost: string): Bluebird<MAccountDefault> {
237 const [ accountName, host ] = nameWithHost.split('@') 240 const [ accountName, host ] = nameWithHost.split('@')
238 241
239 if (!host || host === WEBSERVER.HOST) return AccountModel.loadLocalByName(accountName) 242 if (!host || host === WEBSERVER.HOST) return AccountModel.loadLocalByName(accountName)
@@ -241,7 +244,7 @@ export class AccountModel extends Model<AccountModel> {
241 return AccountModel.loadByNameAndHost(accountName, host) 244 return AccountModel.loadByNameAndHost(accountName, host)
242 } 245 }
243 246
244 static loadLocalByName (name: string) { 247 static loadLocalByName (name: string): Bluebird<MAccountDefault> {
245 const query = { 248 const query = {
246 where: { 249 where: {
247 [ Op.or ]: [ 250 [ Op.or ]: [
@@ -271,7 +274,7 @@ export class AccountModel extends Model<AccountModel> {
271 return AccountModel.findOne(query) 274 return AccountModel.findOne(query)
272 } 275 }
273 276
274 static loadByNameAndHost (name: string, host: string) { 277 static loadByNameAndHost (name: string, host: string): Bluebird<MAccountDefault> {
275 const query = { 278 const query = {
276 include: [ 279 include: [
277 { 280 {
@@ -296,7 +299,7 @@ export class AccountModel extends Model<AccountModel> {
296 return AccountModel.findOne(query) 299 return AccountModel.findOne(query)
297 } 300 }
298 301
299 static loadByUrl (url: string, transaction?: Transaction) { 302 static loadByUrl (url: string, transaction?: Transaction): Bluebird<MAccountDefault> {
300 const query = { 303 const query = {
301 include: [ 304 include: [
302 { 305 {
@@ -329,7 +332,7 @@ export class AccountModel extends Model<AccountModel> {
329 }) 332 })
330 } 333 }
331 334
332 static listLocalsForSitemap (sort: string) { 335 static listLocalsForSitemap (sort: string): Bluebird<MAccountActor[]> {
333 const query = { 336 const query = {
334 attributes: [ ], 337 attributes: [ ],
335 offset: 0, 338 offset: 0,
@@ -350,7 +353,7 @@ export class AccountModel extends Model<AccountModel> {
350 .findAll(query) 353 .findAll(query)
351 } 354 }
352 355
353 toFormattedJSON (): Account { 356 toFormattedJSON (this: MAccountFormattable): Account {
354 const actor = this.Actor.toFormattedJSON() 357 const actor = this.Actor.toFormattedJSON()
355 const account = { 358 const account = {
356 id: this.id, 359 id: this.id,
@@ -364,8 +367,8 @@ export class AccountModel extends Model<AccountModel> {
364 return Object.assign(actor, account) 367 return Object.assign(actor, account)
365 } 368 }
366 369
367 toFormattedSummaryJSON (): AccountSummary { 370 toFormattedSummaryJSON (this: MAccountSummaryFormattable): AccountSummary {
368 const actor = this.Actor.toFormattedJSON() 371 const actor = this.Actor.toFormattedSummaryJSON()
369 372
370 return { 373 return {
371 id: this.id, 374 id: this.id,
@@ -377,8 +380,8 @@ export class AccountModel extends Model<AccountModel> {
377 } 380 }
378 } 381 }
379 382
380 toActivityPubObject () { 383 toActivityPubObject (this: MAccountAP) {
381 const obj = this.Actor.toActivityPubObject(this.name, 'Account') 384 const obj = this.Actor.toActivityPubObject(this.name)
382 385
383 return Object.assign(obj, { 386 return Object.assign(obj, {
384 summary: this.description 387 summary: this.description
diff --git a/server/models/account/user-notification-setting.ts b/server/models/account/user-notification-setting.ts
index c2fbc6d23..dc69a17fd 100644
--- a/server/models/account/user-notification-setting.ts
+++ b/server/models/account/user-notification-setting.ts
@@ -17,6 +17,7 @@ import { UserModel } from './user'
17import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' 17import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications'
18import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' 18import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model'
19import { clearCacheByUserId } from '../../lib/oauth-model' 19import { clearCacheByUserId } from '../../lib/oauth-model'
20import { MNotificationSettingFormattable } from '@server/typings/models'
20 21
21@Table({ 22@Table({
22 tableName: 'userNotificationSetting', 23 tableName: 'userNotificationSetting',
@@ -113,6 +114,15 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM
113 @AllowNull(false) 114 @AllowNull(false)
114 @Default(null) 115 @Default(null)
115 @Is( 116 @Is(
117 'UserNotificationSettingNewInstanceFollower',
118 value => throwIfNotValid(value, isUserNotificationSettingValid, 'autoInstanceFollowing')
119 )
120 @Column
121 autoInstanceFollowing: UserNotificationSettingValue
122
123 @AllowNull(false)
124 @Default(null)
125 @Is(
116 'UserNotificationSettingNewFollow', 126 'UserNotificationSettingNewFollow',
117 value => throwIfNotValid(value, isUserNotificationSettingValid, 'newFollow') 127 value => throwIfNotValid(value, isUserNotificationSettingValid, 'newFollow')
118 ) 128 )
@@ -152,7 +162,7 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM
152 return clearCacheByUserId(instance.userId) 162 return clearCacheByUserId(instance.userId)
153 } 163 }
154 164
155 toFormattedJSON (): UserNotificationSetting { 165 toFormattedJSON (this: MNotificationSettingFormattable): UserNotificationSetting {
156 return { 166 return {
157 newCommentOnMyVideo: this.newCommentOnMyVideo, 167 newCommentOnMyVideo: this.newCommentOnMyVideo,
158 newVideoFromSubscription: this.newVideoFromSubscription, 168 newVideoFromSubscription: this.newVideoFromSubscription,
@@ -164,7 +174,8 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM
164 newUserRegistration: this.newUserRegistration, 174 newUserRegistration: this.newUserRegistration,
165 commentMention: this.commentMention, 175 commentMention: this.commentMention,
166 newFollow: this.newFollow, 176 newFollow: this.newFollow,
167 newInstanceFollower: this.newInstanceFollower 177 newInstanceFollower: this.newInstanceFollower,
178 autoInstanceFollowing: this.autoInstanceFollowing
168 } 179 }
169 } 180 }
170} 181}
diff --git a/server/models/account/user-notification.ts b/server/models/account/user-notification.ts
index f38cd7e78..ccb81b891 100644
--- a/server/models/account/user-notification.ts
+++ b/server/models/account/user-notification.ts
@@ -16,6 +16,7 @@ import { ActorModel } from '../activitypub/actor'
16import { ActorFollowModel } from '../activitypub/actor-follow' 16import { ActorFollowModel } from '../activitypub/actor-follow'
17import { AvatarModel } from '../avatar/avatar' 17import { AvatarModel } from '../avatar/avatar'
18import { ServerModel } from '../server/server' 18import { ServerModel } from '../server/server'
19import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/typings/models/user'
19 20
20enum ScopeNames { 21enum ScopeNames {
21 WITH_ALL = 'WITH_ALL' 22 WITH_ALL = 'WITH_ALL'
@@ -134,13 +135,18 @@ function buildAccountInclude (required: boolean, withActor = false) {
134 ] 135 ]
135 }, 136 },
136 { 137 {
137 attributes: [ 'preferredUsername' ], 138 attributes: [ 'preferredUsername', 'type' ],
138 model: ActorModel.unscoped(), 139 model: ActorModel.unscoped(),
139 required: true, 140 required: true,
140 as: 'ActorFollowing', 141 as: 'ActorFollowing',
141 include: [ 142 include: [
142 buildChannelInclude(false), 143 buildChannelInclude(false),
143 buildAccountInclude(false) 144 buildAccountInclude(false),
145 {
146 attributes: [ 'host' ],
147 model: ServerModel.unscoped(),
148 required: false
149 }
144 ] 150 ]
145 } 151 }
146 ] 152 ]
@@ -371,7 +377,7 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
371 return UserNotificationModel.update({ read: true }, query) 377 return UserNotificationModel.update({ read: true }, query)
372 } 378 }
373 379
374 toFormattedJSON (): UserNotification { 380 toFormattedJSON (this: UserNotificationModelForApi): UserNotification {
375 const video = this.Video 381 const video = this.Video
376 ? 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) })
377 : undefined 383 : undefined
@@ -403,6 +409,11 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
403 409
404 const account = this.Account ? this.formatActor(this.Account) : undefined 410 const account = this.Account ? this.formatActor(this.Account) : undefined
405 411
412 const actorFollowingType = {
413 Application: 'instance' as 'instance',
414 Group: 'channel' as 'channel',
415 Person: 'account' as 'account'
416 }
406 const actorFollow = this.ActorFollow ? { 417 const actorFollow = this.ActorFollow ? {
407 id: this.ActorFollow.id, 418 id: this.ActorFollow.id,
408 state: this.ActorFollow.state, 419 state: this.ActorFollow.state,
@@ -414,9 +425,10 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
414 host: this.ActorFollow.ActorFollower.getHost() 425 host: this.ActorFollow.ActorFollower.getHost()
415 }, 426 },
416 following: { 427 following: {
417 type: this.ActorFollow.ActorFollowing.VideoChannel ? 'channel' as 'channel' : 'account' as 'account', 428 type: actorFollowingType[this.ActorFollow.ActorFollowing.type],
418 displayName: (this.ActorFollow.ActorFollowing.VideoChannel || this.ActorFollow.ActorFollowing.Account).getDisplayName(), 429 displayName: (this.ActorFollow.ActorFollowing.VideoChannel || this.ActorFollow.ActorFollowing.Account).getDisplayName(),
419 name: this.ActorFollow.ActorFollowing.preferredUsername 430 name: this.ActorFollow.ActorFollowing.preferredUsername,
431 host: this.ActorFollow.ActorFollowing.getHost()
420 } 432 }
421 } : undefined 433 } : undefined
422 434
@@ -436,7 +448,7 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
436 } 448 }
437 } 449 }
438 450
439 private formatVideo (video: VideoModel) { 451 formatVideo (this: UserNotificationModelForApi, video: UserNotificationIncludes.VideoInclude) {
440 return { 452 return {
441 id: video.id, 453 id: video.id,
442 uuid: video.uuid, 454 uuid: video.uuid,
@@ -444,7 +456,10 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
444 } 456 }
445 } 457 }
446 458
447 private formatActor (accountOrChannel: AccountModel | VideoChannelModel) { 459 formatActor (
460 this: UserNotificationModelForApi,
461 accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor
462 ) {
448 const avatar = accountOrChannel.Actor.Avatar 463 const avatar = accountOrChannel.Actor.Avatar
449 ? { path: accountOrChannel.Actor.Avatar.getStaticPath() } 464 ? { path: accountOrChannel.Actor.Avatar.getStaticPath() }
450 : undefined 465 : undefined
diff --git a/server/models/account/user-video-history.ts b/server/models/account/user-video-history.ts
index a862fc45f..3fe4c8db1 100644
--- a/server/models/account/user-video-history.ts
+++ b/server/models/account/user-video-history.ts
@@ -1,7 +1,8 @@
1import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, IsInt, Model, Table, UpdatedAt } from 'sequelize-typescript' 1import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, IsInt, Model, Table, UpdatedAt } from 'sequelize-typescript'
2import { VideoModel } from '../video/video' 2import { VideoModel } from '../video/video'
3import { UserModel } from './user' 3import { UserModel } from './user'
4import { Transaction, Op, DestroyOptions } from 'sequelize' 4import { DestroyOptions, Op, Transaction } from 'sequelize'
5import { MUserAccountId, MUserId } from '@server/typings/models'
5 6
6@Table({ 7@Table({
7 tableName: 'userVideoHistory', 8 tableName: 'userVideoHistory',
@@ -54,7 +55,7 @@ export class UserVideoHistoryModel extends Model<UserVideoHistoryModel> {
54 }) 55 })
55 User: UserModel 56 User: UserModel
56 57
57 static listForApi (user: UserModel, start: number, count: number) { 58 static listForApi (user: MUserAccountId, start: number, count: number) {
58 return VideoModel.listForApi({ 59 return VideoModel.listForApi({
59 start, 60 start,
60 count, 61 count,
@@ -67,7 +68,7 @@ export class UserVideoHistoryModel extends Model<UserVideoHistoryModel> {
67 }) 68 })
68 } 69 }
69 70
70 static removeUserHistoryBefore (user: UserModel, beforeDate: string, t: Transaction) { 71 static removeUserHistoryBefore (user: MUserId, beforeDate: string, t: Transaction) {
71 const query: DestroyOptions = { 72 const query: DestroyOptions = {
72 where: { 73 where: {
73 userId: user.id 74 userId: user.id
diff --git a/server/models/account/user.ts b/server/models/account/user.ts
index 0041bf577..451e1fd6b 100644
--- a/server/models/account/user.ts
+++ b/server/models/account/user.ts
@@ -22,6 +22,7 @@ import {
22import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared' 22import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared'
23import { User, UserRole } from '../../../shared/models/users' 23import { User, UserRole } from '../../../shared/models/users'
24import { 24import {
25 isNoInstanceConfigWarningModal,
25 isUserAdminFlagsValid, 26 isUserAdminFlagsValid,
26 isUserAutoPlayVideoValid, 27 isUserAutoPlayVideoValid,
27 isUserBlockedReasonValid, 28 isUserBlockedReasonValid,
@@ -35,7 +36,8 @@ import {
35 isUserVideoQuotaDailyValid, 36 isUserVideoQuotaDailyValid,
36 isUserVideoQuotaValid, 37 isUserVideoQuotaValid,
37 isUserVideosHistoryEnabledValid, 38 isUserVideosHistoryEnabledValid,
38 isUserWebTorrentEnabledValid 39 isUserWebTorrentEnabledValid,
40 isNoWelcomeModal
39} from '../../helpers/custom-validators/users' 41} from '../../helpers/custom-validators/users'
40import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' 42import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto'
41import { OAuthTokenModel } from '../oauth/oauth-token' 43import { OAuthTokenModel } from '../oauth/oauth-token'
@@ -54,6 +56,14 @@ import { VideoImportModel } from '../video/video-import'
54import { UserAdminFlag } from '../../../shared/models/users/user-flag.model' 56import { UserAdminFlag } from '../../../shared/models/users/user-flag.model'
55import { isThemeNameValid } from '../../helpers/custom-validators/plugins' 57import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
56import { getThemeOrDefault } from '../../lib/plugins/theme-utils' 58import { getThemeOrDefault } from '../../lib/plugins/theme-utils'
59import * as Bluebird from 'bluebird'
60import {
61 MUserDefault,
62 MUserFormattable,
63 MUserId,
64 MUserNotifSettingChannelDefault,
65 MUserWithNotificationSetting
66} from '@server/typings/models'
57 67
58enum ScopeNames { 68enum ScopeNames {
59 WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL' 69 WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL'
@@ -195,6 +205,24 @@ export class UserModel extends Model<UserModel> {
195 @Column 205 @Column
196 theme: string 206 theme: string
197 207
208 @AllowNull(false)
209 @Default(false)
210 @Is(
211 'UserNoInstanceConfigWarningModal',
212 value => throwIfNotValid(value, isNoInstanceConfigWarningModal, 'no instance config warning modal')
213 )
214 @Column
215 noInstanceConfigWarningModal: boolean
216
217 @AllowNull(false)
218 @Default(false)
219 @Is(
220 'UserNoInstanceConfigWarningModal',
221 value => throwIfNotValid(value, isNoWelcomeModal, 'no welcome modal')
222 )
223 @Column
224 noWelcomeModal: boolean
225
198 @CreatedAt 226 @CreatedAt
199 createdAt: Date 227 createdAt: Date
200 228
@@ -303,7 +331,7 @@ export class UserModel extends Model<UserModel> {
303 }) 331 })
304 } 332 }
305 333
306 static listWithRight (right: UserRight) { 334 static listWithRight (right: UserRight): Bluebird<MUserDefault[]> {
307 const roles = Object.keys(USER_ROLE_LABELS) 335 const roles = Object.keys(USER_ROLE_LABELS)
308 .map(k => parseInt(k, 10) as UserRole) 336 .map(k => parseInt(k, 10) as UserRole)
309 .filter(role => hasUserRight(role, right)) 337 .filter(role => hasUserRight(role, right))
@@ -319,7 +347,7 @@ export class UserModel extends Model<UserModel> {
319 return UserModel.findAll(query) 347 return UserModel.findAll(query)
320 } 348 }
321 349
322 static listUserSubscribersOf (actorId: number) { 350 static listUserSubscribersOf (actorId: number): Bluebird<MUserWithNotificationSetting[]> {
323 const query = { 351 const query = {
324 include: [ 352 include: [
325 { 353 {
@@ -358,7 +386,7 @@ export class UserModel extends Model<UserModel> {
358 return UserModel.unscoped().findAll(query) 386 return UserModel.unscoped().findAll(query)
359 } 387 }
360 388
361 static listByUsernames (usernames: string[]) { 389 static listByUsernames (usernames: string[]): Bluebird<MUserDefault[]> {
362 const query = { 390 const query = {
363 where: { 391 where: {
364 username: usernames 392 username: usernames
@@ -368,11 +396,11 @@ export class UserModel extends Model<UserModel> {
368 return UserModel.findAll(query) 396 return UserModel.findAll(query)
369 } 397 }
370 398
371 static loadById (id: number) { 399 static loadById (id: number): Bluebird<MUserDefault> {
372 return UserModel.findByPk(id) 400 return UserModel.findByPk(id)
373 } 401 }
374 402
375 static loadByUsername (username: string) { 403 static loadByUsername (username: string): Bluebird<MUserDefault> {
376 const query = { 404 const query = {
377 where: { 405 where: {
378 username: { [ Op.iLike ]: username } 406 username: { [ Op.iLike ]: username }
@@ -382,7 +410,7 @@ export class UserModel extends Model<UserModel> {
382 return UserModel.findOne(query) 410 return UserModel.findOne(query)
383 } 411 }
384 412
385 static loadByUsernameAndPopulateChannels (username: string) { 413 static loadByUsernameAndPopulateChannels (username: string): Bluebird<MUserNotifSettingChannelDefault> {
386 const query = { 414 const query = {
387 where: { 415 where: {
388 username: { [ Op.iLike ]: username } 416 username: { [ Op.iLike ]: username }
@@ -392,7 +420,7 @@ export class UserModel extends Model<UserModel> {
392 return UserModel.scope(ScopeNames.WITH_VIDEO_CHANNEL).findOne(query) 420 return UserModel.scope(ScopeNames.WITH_VIDEO_CHANNEL).findOne(query)
393 } 421 }
394 422
395 static loadByEmail (email: string) { 423 static loadByEmail (email: string): Bluebird<MUserDefault> {
396 const query = { 424 const query = {
397 where: { 425 where: {
398 email 426 email
@@ -402,7 +430,7 @@ export class UserModel extends Model<UserModel> {
402 return UserModel.findOne(query) 430 return UserModel.findOne(query)
403 } 431 }
404 432
405 static loadByUsernameOrEmail (username: string, email?: string) { 433 static loadByUsernameOrEmail (username: string, email?: string): Bluebird<MUserDefault> {
406 if (!email) email = username 434 if (!email) email = username
407 435
408 const query = { 436 const query = {
@@ -414,7 +442,7 @@ export class UserModel extends Model<UserModel> {
414 return UserModel.findOne(query) 442 return UserModel.findOne(query)
415 } 443 }
416 444
417 static loadByVideoId (videoId: number) { 445 static loadByVideoId (videoId: number): Bluebird<MUserDefault> {
418 const query = { 446 const query = {
419 include: [ 447 include: [
420 { 448 {
@@ -445,7 +473,7 @@ export class UserModel extends Model<UserModel> {
445 return UserModel.findOne(query) 473 return UserModel.findOne(query)
446 } 474 }
447 475
448 static loadByVideoImportId (videoImportId: number) { 476 static loadByVideoImportId (videoImportId: number): Bluebird<MUserDefault> {
449 const query = { 477 const query = {
450 include: [ 478 include: [
451 { 479 {
@@ -462,7 +490,7 @@ export class UserModel extends Model<UserModel> {
462 return UserModel.findOne(query) 490 return UserModel.findOne(query)
463 } 491 }
464 492
465 static loadByChannelActorId (videoChannelActorId: number) { 493 static loadByChannelActorId (videoChannelActorId: number): Bluebird<MUserDefault> {
466 const query = { 494 const query = {
467 include: [ 495 include: [
468 { 496 {
@@ -486,7 +514,7 @@ export class UserModel extends Model<UserModel> {
486 return UserModel.findOne(query) 514 return UserModel.findOne(query)
487 } 515 }
488 516
489 static loadByAccountActorId (accountActorId: number) { 517 static loadByAccountActorId (accountActorId: number): Bluebird<MUserDefault> {
490 const query = { 518 const query = {
491 include: [ 519 include: [
492 { 520 {
@@ -503,7 +531,7 @@ export class UserModel extends Model<UserModel> {
503 return UserModel.findOne(query) 531 return UserModel.findOne(query)
504 } 532 }
505 533
506 static getOriginalVideoFileTotalFromUser (user: UserModel) { 534 static getOriginalVideoFileTotalFromUser (user: MUserId) {
507 // Don't use sequelize because we need to use a sub query 535 // Don't use sequelize because we need to use a sub query
508 const query = UserModel.generateUserQuotaBaseSQL() 536 const query = UserModel.generateUserQuotaBaseSQL()
509 537
@@ -511,7 +539,7 @@ export class UserModel extends Model<UserModel> {
511 } 539 }
512 540
513 // Returns cumulative size of all video files uploaded in the last 24 hours. 541 // Returns cumulative size of all video files uploaded in the last 24 hours.
514 static getOriginalVideoFileTotalDailyFromUser (user: UserModel) { 542 static getOriginalVideoFileTotalDailyFromUser (user: MUserId) {
515 // Don't use sequelize because we need to use a sub query 543 // Don't use sequelize because we need to use a sub query
516 const query = UserModel.generateUserQuotaBaseSQL('"video"."createdAt" > now() - interval \'24 hours\'') 544 const query = UserModel.generateUserQuotaBaseSQL('"video"."createdAt" > now() - interval \'24 hours\'')
517 545
@@ -552,38 +580,52 @@ export class UserModel extends Model<UserModel> {
552 return comparePassword(password, this.password) 580 return comparePassword(password, this.password)
553 } 581 }
554 582
555 toFormattedJSON (parameters: { withAdminFlags?: boolean } = {}): User { 583 toFormattedJSON (this: MUserFormattable, parameters: { withAdminFlags?: boolean } = {}): User {
556 const videoQuotaUsed = this.get('videoQuotaUsed') 584 const videoQuotaUsed = this.get('videoQuotaUsed')
557 const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily') 585 const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily')
558 586
559 const json = { 587 const json: User = {
560 id: this.id, 588 id: this.id,
561 username: this.username, 589 username: this.username,
562 email: this.email, 590 email: this.email,
591 theme: getThemeOrDefault(this.theme, DEFAULT_USER_THEME_NAME),
592
563 pendingEmail: this.pendingEmail, 593 pendingEmail: this.pendingEmail,
564 emailVerified: this.emailVerified, 594 emailVerified: this.emailVerified,
595
565 nsfwPolicy: this.nsfwPolicy, 596 nsfwPolicy: this.nsfwPolicy,
566 webTorrentEnabled: this.webTorrentEnabled, 597 webTorrentEnabled: this.webTorrentEnabled,
567 videosHistoryEnabled: this.videosHistoryEnabled, 598 videosHistoryEnabled: this.videosHistoryEnabled,
568 autoPlayVideo: this.autoPlayVideo, 599 autoPlayVideo: this.autoPlayVideo,
569 videoLanguages: this.videoLanguages, 600 videoLanguages: this.videoLanguages,
601
570 role: this.role, 602 role: this.role,
571 theme: getThemeOrDefault(this.theme, DEFAULT_USER_THEME_NAME),
572 roleLabel: USER_ROLE_LABELS[ this.role ], 603 roleLabel: USER_ROLE_LABELS[ this.role ],
604
573 videoQuota: this.videoQuota, 605 videoQuota: this.videoQuota,
574 videoQuotaDaily: this.videoQuotaDaily, 606 videoQuotaDaily: this.videoQuotaDaily,
575 createdAt: this.createdAt, 607 videoQuotaUsed: videoQuotaUsed !== undefined
608 ? parseInt(videoQuotaUsed + '', 10)
609 : undefined,
610 videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined
611 ? parseInt(videoQuotaUsedDaily + '', 10)
612 : undefined,
613
614 noInstanceConfigWarningModal: this.noInstanceConfigWarningModal,
615 noWelcomeModal: this.noWelcomeModal,
616
576 blocked: this.blocked, 617 blocked: this.blocked,
577 blockedReason: this.blockedReason, 618 blockedReason: this.blockedReason,
619
578 account: this.Account.toFormattedJSON(), 620 account: this.Account.toFormattedJSON(),
579 notificationSettings: this.NotificationSetting ? this.NotificationSetting.toFormattedJSON() : undefined, 621
622 notificationSettings: this.NotificationSetting
623 ? this.NotificationSetting.toFormattedJSON()
624 : undefined,
625
580 videoChannels: [], 626 videoChannels: [],
581 videoQuotaUsed: videoQuotaUsed !== undefined 627
582 ? parseInt(videoQuotaUsed + '', 10) 628 createdAt: this.createdAt
583 : undefined,
584 videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined
585 ? parseInt(videoQuotaUsedDaily + '', 10)
586 : undefined
587 } 629 }
588 630
589 if (parameters.withAdminFlags) { 631 if (parameters.withAdminFlags) {
diff --git a/server/models/activitypub/actor-follow.ts b/server/models/activitypub/actor-follow.ts
index 51b09e09b..8498692f0 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 } from 'lodash' 2import { values, difference } from 'lodash'
3import { 3import {
4 AfterCreate, 4 AfterCreate,
5 AfterDestroy, 5 AfterDestroy,
@@ -21,13 +21,20 @@ import { 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' 23import { getServerActor } from '../../helpers/utils'
24import { ACTOR_FOLLOW_SCORE, FOLLOW_STATES } from '../../initializers/constants' 24import { ACTOR_FOLLOW_SCORE, FOLLOW_STATES, SERVER_ACTOR_NAME } from '../../initializers/constants'
25import { ServerModel } from '../server/server' 25import { ServerModel } from '../server/server'
26import { createSafeIn, getSort } from '../utils' 26import { createSafeIn, getSort } from '../utils'
27import { ActorModel, unusedActorAttributesForAPI } from './actor' 27import { ActorModel, unusedActorAttributesForAPI } from './actor'
28import { VideoChannelModel } from '../video/video-channel' 28import { VideoChannelModel } from '../video/video-channel'
29import { AccountModel } from '../account/account' 29import { AccountModel } from '../account/account'
30import { IncludeOptions, Op, Transaction, QueryTypes } from 'sequelize' 30import { IncludeOptions, Op, QueryTypes, Transaction } from 'sequelize'
31import {
32 MActorFollowActorsDefault,
33 MActorFollowActorsDefaultSubscription,
34 MActorFollowFollowingHost,
35 MActorFollowFormattable,
36 MActorFollowSubscriptions
37} from '@server/typings/models'
31 38
32@Table({ 39@Table({
33 tableName: 'actorFollow', 40 tableName: 'actorFollow',
@@ -143,7 +150,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
143 if (numberOfActorFollowsRemoved) logger.info('Removed bad %d actor follows.', numberOfActorFollowsRemoved) 150 if (numberOfActorFollowsRemoved) logger.info('Removed bad %d actor follows.', numberOfActorFollowsRemoved)
144 } 151 }
145 152
146 static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Transaction) { 153 static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Transaction): Bluebird<MActorFollowActorsDefault> {
147 const query = { 154 const query = {
148 where: { 155 where: {
149 actorId, 156 actorId,
@@ -167,7 +174,12 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
167 return ActorFollowModel.findOne(query) 174 return ActorFollowModel.findOne(query)
168 } 175 }
169 176
170 static loadByActorAndTargetNameAndHostForAPI (actorId: number, targetName: string, targetHost: string, t?: Transaction) { 177 static loadByActorAndTargetNameAndHostForAPI (
178 actorId: number,
179 targetName: string,
180 targetHost: string,
181 t?: Transaction
182 ): Bluebird<MActorFollowActorsDefaultSubscription> {
171 const actorFollowingPartInclude: IncludeOptions = { 183 const actorFollowingPartInclude: IncludeOptions = {
172 model: ActorModel, 184 model: ActorModel,
173 required: true, 185 required: true,
@@ -220,7 +232,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
220 }) 232 })
221 } 233 }
222 234
223 static listSubscribedIn (actorId: number, targets: { name: string, host?: string }[]) { 235 static listSubscribedIn (actorId: number, targets: { name: string, host?: string }[]): Bluebird<MActorFollowFollowingHost[]> {
224 const whereTab = targets 236 const whereTab = targets
225 .map(t => { 237 .map(t => {
226 if (t.host) { 238 if (t.host) {
@@ -314,7 +326,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
314 ] 326 ]
315 } 327 }
316 328
317 return ActorFollowModel.findAndCountAll(query) 329 return ActorFollowModel.findAndCountAll<MActorFollowActorsDefault>(query)
318 .then(({ rows, count }) => { 330 .then(({ rows, count }) => {
319 return { 331 return {
320 data: rows, 332 data: rows,
@@ -357,7 +369,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
357 ] 369 ]
358 } 370 }
359 371
360 return ActorFollowModel.findAndCountAll(query) 372 return ActorFollowModel.findAndCountAll<MActorFollowActorsDefault>(query)
361 .then(({ rows, count }) => { 373 .then(({ rows, count }) => {
362 return { 374 return {
363 data: rows, 375 data: rows,
@@ -414,7 +426,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
414 ] 426 ]
415 } 427 }
416 428
417 return ActorFollowModel.findAndCountAll(query) 429 return ActorFollowModel.findAndCountAll<MActorFollowSubscriptions>(query)
418 .then(({ rows, count }) => { 430 .then(({ rows, count }) => {
419 return { 431 return {
420 data: rows.map(r => r.ActorFollowing.VideoChannel), 432 data: rows.map(r => r.ActorFollowing.VideoChannel),
@@ -423,6 +435,45 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
423 }) 435 })
424 } 436 }
425 437
438 static async keepUnfollowedInstance (hosts: string[]) {
439 const followerId = (await getServerActor()).id
440
441 const query = {
442 attributes: [ 'id' ],
443 where: {
444 actorId: followerId
445 },
446 include: [
447 {
448 attributes: [ 'id' ],
449 model: ActorModel.unscoped(),
450 required: true,
451 as: 'ActorFollowing',
452 where: {
453 preferredUsername: SERVER_ACTOR_NAME
454 },
455 include: [
456 {
457 attributes: [ 'host' ],
458 model: ServerModel.unscoped(),
459 required: true,
460 where: {
461 host: {
462 [Op.in]: hosts
463 }
464 }
465 }
466 ]
467 }
468 ]
469 }
470
471 const res = await ActorFollowModel.findAll(query)
472 const followedHosts = res.map(row => row.ActorFollowing.Server.host)
473
474 return difference(hosts, followedHosts)
475 }
476
426 static listAcceptedFollowerUrlsForAP (actorIds: number[], t: Transaction, start?: number, count?: number) { 477 static listAcceptedFollowerUrlsForAP (actorIds: number[], t: Transaction, start?: number, count?: number) {
427 return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count) 478 return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count)
428 } 479 }
@@ -569,7 +620,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
569 return ActorFollowModel.findAll(query) 620 return ActorFollowModel.findAll(query)
570 } 621 }
571 622
572 toFormattedJSON (): ActorFollow { 623 toFormattedJSON (this: MActorFollowFormattable): ActorFollow {
573 const follower = this.ActorFollower.toFormattedJSON() 624 const follower = this.ActorFollower.toFormattedJSON()
574 const following = this.ActorFollowing.toFormattedJSON() 625 const following = this.ActorFollowing.toFormattedJSON()
575 626
diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts
index 9cc53f78a..05de1905d 100644
--- a/server/models/activitypub/actor.ts
+++ b/server/models/activitypub/actor.ts
@@ -36,6 +36,17 @@ import { isOutdated, throwIfNotValid } from '../utils'
36import { VideoChannelModel } from '../video/video-channel' 36import { VideoChannelModel } from '../video/video-channel'
37import { ActorFollowModel } from './actor-follow' 37import { ActorFollowModel } from './actor-follow'
38import { VideoModel } from '../video/video' 38import { VideoModel } from '../video/video'
39import {
40 MActor,
41 MActorAccountChannelId,
42 MActorAP,
43 MActorFormattable,
44 MActorFull,
45 MActorHost,
46 MActorServer,
47 MActorSummaryFormattable
48} from '../../typings/models'
49import * as Bluebird from 'bluebird'
39 50
40enum ScopeNames { 51enum ScopeNames {
41 FULL = 'FULL' 52 FULL = 'FULL'
@@ -163,8 +174,8 @@ export class ActorModel extends Model<ActorModel> {
163 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max)) 174 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
164 inboxUrl: string 175 inboxUrl: string
165 176
166 @AllowNull(false) 177 @AllowNull(true)
167 @Is('ActorOutboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'outbox url')) 178 @Is('ActorOutboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'outbox url', true))
168 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max)) 179 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
169 outboxUrl: string 180 outboxUrl: string
170 181
@@ -173,13 +184,13 @@ export class ActorModel extends Model<ActorModel> {
173 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max)) 184 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
174 sharedInboxUrl: string 185 sharedInboxUrl: string
175 186
176 @AllowNull(false) 187 @AllowNull(true)
177 @Is('ActorFollowersUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'followers url')) 188 @Is('ActorFollowersUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'followers url', true))
178 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max)) 189 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
179 followersUrl: string 190 followersUrl: string
180 191
181 @AllowNull(false) 192 @AllowNull(true)
182 @Is('ActorFollowingUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'following url')) 193 @Is('ActorFollowingUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'following url', true))
183 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max)) 194 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
184 followingUrl: string 195 followingUrl: string
185 196
@@ -252,11 +263,15 @@ export class ActorModel extends Model<ActorModel> {
252 }) 263 })
253 VideoChannel: VideoChannelModel 264 VideoChannel: VideoChannelModel
254 265
255 static load (id: number) { 266 static load (id: number): Bluebird<MActor> {
256 return ActorModel.unscoped().findByPk(id) 267 return ActorModel.unscoped().findByPk(id)
257 } 268 }
258 269
259 static loadAccountActorByVideoId (videoId: number, transaction: Sequelize.Transaction) { 270 static loadFull (id: number): Bluebird<MActorFull> {
271 return ActorModel.scope(ScopeNames.FULL).findByPk(id)
272 }
273
274 static loadFromAccountByVideoId (videoId: number, transaction: Sequelize.Transaction): Bluebird<MActor> {
260 const query = { 275 const query = {
261 include: [ 276 include: [
262 { 277 {
@@ -300,7 +315,7 @@ export class ActorModel extends Model<ActorModel> {
300 .then(a => !!a) 315 .then(a => !!a)
301 } 316 }
302 317
303 static listByFollowersUrls (followersUrls: string[], transaction?: Sequelize.Transaction) { 318 static listByFollowersUrls (followersUrls: string[], transaction?: Sequelize.Transaction): Bluebird<MActorFull[]> {
304 const query = { 319 const query = {
305 where: { 320 where: {
306 followersUrl: { 321 followersUrl: {
@@ -313,7 +328,7 @@ export class ActorModel extends Model<ActorModel> {
313 return ActorModel.scope(ScopeNames.FULL).findAll(query) 328 return ActorModel.scope(ScopeNames.FULL).findAll(query)
314 } 329 }
315 330
316 static loadLocalByName (preferredUsername: string, transaction?: Sequelize.Transaction) { 331 static loadLocalByName (preferredUsername: string, transaction?: Sequelize.Transaction): Bluebird<MActorFull> {
317 const query = { 332 const query = {
318 where: { 333 where: {
319 preferredUsername, 334 preferredUsername,
@@ -325,7 +340,7 @@ export class ActorModel extends Model<ActorModel> {
325 return ActorModel.scope(ScopeNames.FULL).findOne(query) 340 return ActorModel.scope(ScopeNames.FULL).findOne(query)
326 } 341 }
327 342
328 static loadByNameAndHost (preferredUsername: string, host: string) { 343 static loadByNameAndHost (preferredUsername: string, host: string): Bluebird<MActorFull> {
329 const query = { 344 const query = {
330 where: { 345 where: {
331 preferredUsername 346 preferredUsername
@@ -344,7 +359,7 @@ export class ActorModel extends Model<ActorModel> {
344 return ActorModel.scope(ScopeNames.FULL).findOne(query) 359 return ActorModel.scope(ScopeNames.FULL).findOne(query)
345 } 360 }
346 361
347 static loadByUrl (url: string, transaction?: Sequelize.Transaction) { 362 static loadByUrl (url: string, transaction?: Sequelize.Transaction): Bluebird<MActorAccountChannelId> {
348 const query = { 363 const query = {
349 where: { 364 where: {
350 url 365 url
@@ -367,7 +382,7 @@ export class ActorModel extends Model<ActorModel> {
367 return ActorModel.unscoped().findOne(query) 382 return ActorModel.unscoped().findOne(query)
368 } 383 }
369 384
370 static loadByUrlAndPopulateAccountAndChannel (url: string, transaction?: Sequelize.Transaction) { 385 static loadByUrlAndPopulateAccountAndChannel (url: string, transaction?: Sequelize.Transaction): Bluebird<MActorFull> {
371 const query = { 386 const query = {
372 where: { 387 where: {
373 url 388 url
@@ -387,35 +402,35 @@ export class ActorModel extends Model<ActorModel> {
387 }) 402 })
388 } 403 }
389 404
390 toFormattedJSON () { 405 toFormattedSummaryJSON (this: MActorSummaryFormattable) {
391 let avatar: Avatar = null 406 let avatar: Avatar = null
392 if (this.Avatar) { 407 if (this.Avatar) {
393 avatar = this.Avatar.toFormattedJSON() 408 avatar = this.Avatar.toFormattedJSON()
394 } 409 }
395 410
396 return { 411 return {
397 id: this.id,
398 url: this.url, 412 url: this.url,
399 name: this.preferredUsername, 413 name: this.preferredUsername,
400 host: this.getHost(), 414 host: this.getHost(),
415 avatar
416 }
417 }
418
419 toFormattedJSON (this: MActorFormattable) {
420 const base = this.toFormattedSummaryJSON()
421
422 return Object.assign(base, {
423 id: this.id,
401 hostRedundancyAllowed: this.getRedundancyAllowed(), 424 hostRedundancyAllowed: this.getRedundancyAllowed(),
402 followingCount: this.followingCount, 425 followingCount: this.followingCount,
403 followersCount: this.followersCount, 426 followersCount: this.followersCount,
404 avatar,
405 createdAt: this.createdAt, 427 createdAt: this.createdAt,
406 updatedAt: this.updatedAt 428 updatedAt: this.updatedAt
407 } 429 })
408 } 430 }
409 431
410 toActivityPubObject (name: string, type: 'Account' | 'Application' | 'VideoChannel') { 432 toActivityPubObject (this: MActorAP, name: string) {
411 let activityPubType 433 let activityPubType
412 if (type === 'Account') {
413 activityPubType = 'Person' as 'Person'
414 } else if (type === 'Application') {
415 activityPubType = 'Application' as 'Application'
416 } else { // VideoChannel
417 activityPubType = 'Group' as 'Group'
418 }
419 434
420 let icon = undefined 435 let icon = undefined
421 if (this.avatarId) { 436 if (this.avatarId) {
@@ -428,7 +443,7 @@ export class ActorModel extends Model<ActorModel> {
428 } 443 }
429 444
430 const json = { 445 const json = {
431 type: activityPubType, 446 type: this.type,
432 id: this.url, 447 id: this.url,
433 following: this.getFollowingUrl(), 448 following: this.getFollowingUrl(),
434 followers: this.getFollowersUrl(), 449 followers: this.getFollowersUrl(),
@@ -494,7 +509,7 @@ export class ActorModel extends Model<ActorModel> {
494 return this.serverId === null 509 return this.serverId === null
495 } 510 }
496 511
497 getWebfingerUrl () { 512 getWebfingerUrl (this: MActorServer) {
498 return 'acct:' + this.preferredUsername + '@' + this.getHost() 513 return 'acct:' + this.preferredUsername + '@' + this.getHost()
499 } 514 }
500 515
@@ -502,7 +517,7 @@ export class ActorModel extends Model<ActorModel> {
502 return this.Server ? `${this.preferredUsername}@${this.Server.host}` : this.preferredUsername 517 return this.Server ? `${this.preferredUsername}@${this.Server.host}` : this.preferredUsername
503 } 518 }
504 519
505 getHost () { 520 getHost (this: MActorHost) {
506 return this.Server ? this.Server.host : WEBSERVER.HOST 521 return this.Server ? this.Server.host : WEBSERVER.HOST
507 } 522 }
508 523
diff --git a/server/models/avatar/avatar.ts b/server/models/avatar/avatar.ts
index b40144592..950e4b181 100644
--- a/server/models/avatar/avatar.ts
+++ b/server/models/avatar/avatar.ts
@@ -7,6 +7,7 @@ import { remove } from 'fs-extra'
7import { CONFIG } from '../../initializers/config' 7import { CONFIG } from '../../initializers/config'
8import { throwIfNotValid } from '../utils' 8import { throwIfNotValid } from '../utils'
9import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 9import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
10import { MAvatarFormattable } from '@server/typings/models'
10 11
11@Table({ 12@Table({
12 tableName: 'avatar', 13 tableName: 'avatar',
@@ -57,7 +58,7 @@ export class AvatarModel extends Model<AvatarModel> {
57 return AvatarModel.findOne(query) 58 return AvatarModel.findOne(query)
58 } 59 }
59 60
60 toFormattedJSON (): Avatar { 61 toFormattedJSON (this: MAvatarFormattable): Avatar {
61 return { 62 return {
62 path: this.getStaticPath(), 63 path: this.getStaticPath(),
63 createdAt: this.createdAt, 64 createdAt: this.createdAt,
diff --git a/server/models/oauth/oauth-token.ts b/server/models/oauth/oauth-token.ts
index 903d551df..b680be237 100644
--- a/server/models/oauth/oauth-token.ts
+++ b/server/models/oauth/oauth-token.ts
@@ -18,6 +18,8 @@ import { Transaction } from 'sequelize'
18import { AccountModel } from '../account/account' 18import { AccountModel } from '../account/account'
19import { ActorModel } from '../activitypub/actor' 19import { ActorModel } from '../activitypub/actor'
20import { clearCacheByToken } from '../../lib/oauth-model' 20import { clearCacheByToken } from '../../lib/oauth-model'
21import * as Bluebird from 'bluebird'
22import { MOAuthTokenUser } from '@server/typings/models/oauth/oauth-token'
21 23
22export type OAuthTokenInfo = { 24export type OAuthTokenInfo = {
23 refreshToken: string 25 refreshToken: string
@@ -160,7 +162,7 @@ export class OAuthTokenModel extends Model<OAuthTokenModel> {
160 }) 162 })
161 } 163 }
162 164
163 static getByTokenAndPopulateUser (bearerToken: string) { 165 static getByTokenAndPopulateUser (bearerToken: string): Bluebird<MOAuthTokenUser> {
164 const query = { 166 const query = {
165 where: { 167 where: {
166 accessToken: bearerToken 168 accessToken: bearerToken
@@ -170,13 +172,13 @@ export class OAuthTokenModel extends Model<OAuthTokenModel> {
170 return OAuthTokenModel.scope(ScopeNames.WITH_USER) 172 return OAuthTokenModel.scope(ScopeNames.WITH_USER)
171 .findOne(query) 173 .findOne(query)
172 .then(token => { 174 .then(token => {
173 if (token) token[ 'user' ] = token.User 175 if (!token) return null
174 176
175 return token 177 return Object.assign(token, { user: token.User })
176 }) 178 })
177 } 179 }
178 180
179 static getByRefreshTokenAndPopulateUser (refreshToken: string) { 181 static getByRefreshTokenAndPopulateUser (refreshToken: string): Bluebird<MOAuthTokenUser> {
180 const query = { 182 const query = {
181 where: { 183 where: {
182 refreshToken: refreshToken 184 refreshToken: refreshToken
@@ -186,12 +188,9 @@ export class OAuthTokenModel extends Model<OAuthTokenModel> {
186 return OAuthTokenModel.scope(ScopeNames.WITH_USER) 188 return OAuthTokenModel.scope(ScopeNames.WITH_USER)
187 .findOne(query) 189 .findOne(query)
188 .then(token => { 190 .then(token => {
189 if (token) { 191 if (!token) return new OAuthTokenModel()
190 token['user'] = token.User 192
191 return token 193 return Object.assign(token, { user: token.User })
192 } else {
193 return new OAuthTokenModel()
194 }
195 }) 194 })
196 } 195 }
197 196
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts
index 3df1c4f9c..61d9a5612 100644
--- a/server/models/redundancy/video-redundancy.ts
+++ b/server/models/redundancy/video-redundancy.ts
@@ -30,6 +30,7 @@ import * as Bluebird from 'bluebird'
30import { col, FindOptions, fn, literal, Op, Transaction } from 'sequelize' 30import { col, FindOptions, fn, literal, Op, Transaction } from 'sequelize'
31import { VideoStreamingPlaylistModel } from '../video/video-streaming-playlist' 31import { VideoStreamingPlaylistModel } from '../video/video-streaming-playlist'
32import { CONFIG } from '../../initializers/config' 32import { CONFIG } from '../../initializers/config'
33import { MVideoRedundancy, MVideoRedundancyAP, MVideoRedundancyVideo } from '@server/typings/models'
33 34
34export enum ScopeNames { 35export enum ScopeNames {
35 WITH_VIDEO = 'WITH_VIDEO' 36 WITH_VIDEO = 'WITH_VIDEO'
@@ -166,7 +167,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
166 return undefined 167 return undefined
167 } 168 }
168 169
169 static async loadLocalByFileId (videoFileId: number) { 170 static async loadLocalByFileId (videoFileId: number): Promise<MVideoRedundancyVideo> {
170 const actor = await getServerActor() 171 const actor = await getServerActor()
171 172
172 const query = { 173 const query = {
@@ -179,7 +180,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
179 return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query) 180 return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query)
180 } 181 }
181 182
182 static async loadLocalByStreamingPlaylistId (videoStreamingPlaylistId: number) { 183 static async loadLocalByStreamingPlaylistId (videoStreamingPlaylistId: number): Promise<MVideoRedundancyVideo> {
183 const actor = await getServerActor() 184 const actor = await getServerActor()
184 185
185 const query = { 186 const query = {
@@ -192,7 +193,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
192 return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query) 193 return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query)
193 } 194 }
194 195
195 static loadByUrl (url: string, transaction?: Transaction) { 196 static loadByUrl (url: string, transaction?: Transaction): Bluebird<MVideoRedundancy> {
196 const query = { 197 const query = {
197 where: { 198 where: {
198 url 199 url
@@ -306,7 +307,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
306 return VideoRedundancyModel.getVideoSample(VideoModel.unscoped().findAll(query)) 307 return VideoRedundancyModel.getVideoSample(VideoModel.unscoped().findAll(query))
307 } 308 }
308 309
309 static async loadOldestLocalThatAlreadyExpired (strategy: VideoRedundancyStrategy, expiresAfterMs: number) { 310 static async loadOldestLocalExpired (strategy: VideoRedundancyStrategy, expiresAfterMs: number): Promise<MVideoRedundancyVideo> {
310 const expiredDate = new Date() 311 const expiredDate = new Date()
311 expiredDate.setMilliseconds(expiredDate.getMilliseconds() - expiresAfterMs) 312 expiredDate.setMilliseconds(expiredDate.getMilliseconds() - expiresAfterMs)
312 313
@@ -487,7 +488,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
487 return !!this.strategy 488 return !!this.strategy
488 } 489 }
489 490
490 toActivityPubObject (): CacheFileObject { 491 toActivityPubObject (this: MVideoRedundancyAP): CacheFileObject {
491 if (this.VideoStreamingPlaylist) { 492 if (this.VideoStreamingPlaylist) {
492 return { 493 return {
493 id: this.url, 494 id: this.url,
diff --git a/server/models/server/plugin.ts b/server/models/server/plugin.ts
index a15f9a7e2..d094da1f5 100644
--- a/server/models/server/plugin.ts
+++ b/server/models/server/plugin.ts
@@ -11,6 +11,8 @@ import { PluginType } from '../../../shared/models/plugins/plugin.type'
11import { PeerTubePlugin } from '../../../shared/models/plugins/peertube-plugin.model' 11import { PeerTubePlugin } from '../../../shared/models/plugins/peertube-plugin.model'
12import { FindAndCountOptions, json } from 'sequelize' 12import { FindAndCountOptions, json } from 'sequelize'
13import { RegisterServerSettingOptions } from '../../../shared/models/plugins/register-server-setting.model' 13import { RegisterServerSettingOptions } from '../../../shared/models/plugins/register-server-setting.model'
14import * as Bluebird from 'bluebird'
15import { MPlugin, MPluginFormattable } from '@server/typings/models'
14 16
15@DefaultScope(() => ({ 17@DefaultScope(() => ({
16 attributes: { 18 attributes: {
@@ -85,7 +87,7 @@ export class PluginModel extends Model<PluginModel> {
85 @UpdatedAt 87 @UpdatedAt
86 updatedAt: Date 88 updatedAt: Date
87 89
88 static listEnabledPluginsAndThemes () { 90 static listEnabledPluginsAndThemes (): Bluebird<MPlugin[]> {
89 const query = { 91 const query = {
90 where: { 92 where: {
91 enabled: true, 93 enabled: true,
@@ -96,7 +98,7 @@ export class PluginModel extends Model<PluginModel> {
96 return PluginModel.findAll(query) 98 return PluginModel.findAll(query)
97 } 99 }
98 100
99 static loadByNpmName (npmName: string) { 101 static loadByNpmName (npmName: string): Bluebird<MPlugin> {
100 const name = this.normalizePluginName(npmName) 102 const name = this.normalizePluginName(npmName)
101 const type = this.getTypeFromNpmName(npmName) 103 const type = this.getTypeFromNpmName(npmName)
102 104
@@ -206,13 +208,13 @@ export class PluginModel extends Model<PluginModel> {
206 if (options.pluginType) query.where['type'] = options.pluginType 208 if (options.pluginType) query.where['type'] = options.pluginType
207 209
208 return PluginModel 210 return PluginModel
209 .findAndCountAll(query) 211 .findAndCountAll<MPlugin>(query)
210 .then(({ rows, count }) => { 212 .then(({ rows, count }) => {
211 return { total: count, data: rows } 213 return { total: count, data: rows }
212 }) 214 })
213 } 215 }
214 216
215 static listInstalled () { 217 static listInstalled (): Bluebird<MPlugin[]> {
216 const query = { 218 const query = {
217 where: { 219 where: {
218 uninstalled: false 220 uninstalled: false
@@ -251,7 +253,7 @@ export class PluginModel extends Model<PluginModel> {
251 return result 253 return result
252 } 254 }
253 255
254 toFormattedJSON (): PeerTubePlugin { 256 toFormattedJSON (this: MPluginFormattable): PeerTubePlugin {
255 return { 257 return {
256 name: this.name, 258 name: this.name,
257 type: this.type, 259 type: this.type,
diff --git a/server/models/server/server-blocklist.ts b/server/models/server/server-blocklist.ts
index 5138b0f76..3e9687191 100644
--- a/server/models/server/server-blocklist.ts
+++ b/server/models/server/server-blocklist.ts
@@ -3,6 +3,8 @@ import { 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 } from '../utils'
6import * as Bluebird from 'bluebird'
7import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/typings/models'
6 8
7enum ScopeNames { 9enum ScopeNames {
8 WITH_ACCOUNT = 'WITH_ACCOUNT', 10 WITH_ACCOUNT = 'WITH_ACCOUNT',
@@ -73,7 +75,7 @@ export class ServerBlocklistModel extends Model<ServerBlocklistModel> {
73 }) 75 })
74 BlockedServer: ServerModel 76 BlockedServer: ServerModel
75 77
76 static loadByAccountAndHost (accountId: number, host: string) { 78 static loadByAccountAndHost (accountId: number, host: string): Bluebird<MServerBlocklist> {
77 const query = { 79 const query = {
78 where: { 80 where: {
79 accountId 81 accountId
@@ -104,13 +106,13 @@ export class ServerBlocklistModel extends Model<ServerBlocklistModel> {
104 106
105 return ServerBlocklistModel 107 return ServerBlocklistModel
106 .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_SERVER ]) 108 .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_SERVER ])
107 .findAndCountAll(query) 109 .findAndCountAll<MServerBlocklistAccountServer>(query)
108 .then(({ rows, count }) => { 110 .then(({ rows, count }) => {
109 return { total: count, data: rows } 111 return { total: count, data: rows }
110 }) 112 })
111 } 113 }
112 114
113 toFormattedJSON (): ServerBlock { 115 toFormattedJSON (this: MServerBlocklistFormattable): ServerBlock {
114 return { 116 return {
115 byAccount: this.ByAccount.toFormattedJSON(), 117 byAccount: this.ByAccount.toFormattedJSON(),
116 blockedServer: this.BlockedServer.toFormattedJSON(), 118 blockedServer: this.BlockedServer.toFormattedJSON(),
diff --git a/server/models/server/server.ts b/server/models/server/server.ts
index 1d211f1e0..8b07115f1 100644
--- a/server/models/server/server.ts
+++ b/server/models/server/server.ts
@@ -2,8 +2,9 @@ import { AllowNull, Column, CreatedAt, Default, HasMany, Is, Model, Table, Updat
2import { isHostValid } from '../../helpers/custom-validators/servers' 2import { isHostValid } from '../../helpers/custom-validators/servers'
3import { ActorModel } from '../activitypub/actor' 3import { ActorModel } from '../activitypub/actor'
4import { throwIfNotValid } from '../utils' 4import { throwIfNotValid } from '../utils'
5import { AccountBlocklistModel } from '../account/account-blocklist'
6import { ServerBlocklistModel } from './server-blocklist' 5import { ServerBlocklistModel } from './server-blocklist'
6import * as Bluebird from 'bluebird'
7import { MServer, MServerFormattable } from '@server/typings/models/server'
7 8
8@Table({ 9@Table({
9 tableName: 'server', 10 tableName: 'server',
@@ -50,7 +51,17 @@ export class ServerModel extends Model<ServerModel> {
50 }) 51 })
51 BlockedByAccounts: ServerBlocklistModel[] 52 BlockedByAccounts: ServerBlocklistModel[]
52 53
53 static loadByHost (host: string) { 54 static load (id: number): Bluebird<MServer> {
55 const query = {
56 where: {
57 id
58 }
59 }
60
61 return ServerModel.findOne(query)
62 }
63
64 static loadByHost (host: string): Bluebird<MServer> {
54 const query = { 65 const query = {
55 where: { 66 where: {
56 host 67 host
@@ -64,7 +75,7 @@ export class ServerModel extends Model<ServerModel> {
64 return this.BlockedByAccounts && this.BlockedByAccounts.length !== 0 75 return this.BlockedByAccounts && this.BlockedByAccounts.length !== 0
65 } 76 }
66 77
67 toFormattedJSON () { 78 toFormattedJSON (this: MServerFormattable) {
68 return { 79 return {
69 host: this.host 80 host: this.host
70 } 81 }
diff --git a/server/models/video/schedule-video-update.ts b/server/models/video/schedule-video-update.ts
index 603d55692..fc2a424aa 100644
--- a/server/models/video/schedule-video-update.ts
+++ b/server/models/video/schedule-video-update.ts
@@ -2,6 +2,7 @@ import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Model, Ta
2import { ScopeNames as VideoScopeNames, VideoModel } from './video' 2import { ScopeNames as VideoScopeNames, VideoModel } from './video'
3import { VideoPrivacy } from '../../../shared/models/videos' 3import { VideoPrivacy } from '../../../shared/models/videos'
4import { Op, Transaction } from 'sequelize' 4import { Op, Transaction } from 'sequelize'
5import { MScheduleVideoUpdateFormattable } from '@server/typings/models'
5 6
6@Table({ 7@Table({
7 tableName: 'scheduleVideoUpdate', 8 tableName: 'scheduleVideoUpdate',
@@ -96,7 +97,7 @@ export class ScheduleVideoUpdateModel extends Model<ScheduleVideoUpdateModel> {
96 return ScheduleVideoUpdateModel.destroy(query) 97 return ScheduleVideoUpdateModel.destroy(query)
97 } 98 }
98 99
99 toFormattedJSON () { 100 toFormattedJSON (this: MScheduleVideoUpdateFormattable) {
100 return { 101 return {
101 updateAt: this.updateAt, 102 updateAt: this.updateAt,
102 privacy: this.privacy || undefined 103 privacy: this.privacy || undefined
diff --git a/server/models/video/tag.ts b/server/models/video/tag.ts
index 0fc3cfd4c..ed8df8b48 100644
--- a/server/models/video/tag.ts
+++ b/server/models/video/tag.ts
@@ -1,11 +1,12 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { QueryTypes, Transaction } from 'sequelize' 2import { fn, QueryTypes, Transaction, col } from 'sequelize'
3import { AllowNull, BelongsToMany, Column, CreatedAt, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' 3import { AllowNull, BelongsToMany, Column, CreatedAt, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
4import { isVideoTagValid } from '../../helpers/custom-validators/videos' 4import { isVideoTagValid } from '../../helpers/custom-validators/videos'
5import { throwIfNotValid } from '../utils' 5import { throwIfNotValid } from '../utils'
6import { VideoModel } from './video' 6import { VideoModel } from './video'
7import { VideoTagModel } from './video-tag' 7import { VideoTagModel } from './video-tag'
8import { VideoPrivacy, VideoState } from '../../../shared/models/videos' 8import { VideoPrivacy, VideoState } from '../../../shared/models/videos'
9import { MTag } from '@server/typings/models'
9 10
10@Table({ 11@Table({
11 tableName: 'tag', 12 tableName: 'tag',
@@ -14,6 +15,10 @@ import { VideoPrivacy, VideoState } from '../../../shared/models/videos'
14 { 15 {
15 fields: [ 'name' ], 16 fields: [ 'name' ],
16 unique: true 17 unique: true
18 },
19 {
20 name: 'tag_lower_name',
21 fields: [ fn('lower', col('name')) ] as any // FIXME: typings
17 } 22 }
18 ] 23 ]
19}) 24})
@@ -37,10 +42,10 @@ export class TagModel extends Model<TagModel> {
37 }) 42 })
38 Videos: VideoModel[] 43 Videos: VideoModel[]
39 44
40 static findOrCreateTags (tags: string[], transaction: Transaction) { 45 static findOrCreateTags (tags: string[], transaction: Transaction): Promise<MTag[]> {
41 if (tags === null) return [] 46 if (tags === null) return Promise.resolve([])
42 47
43 const tasks: Bluebird<TagModel>[] = [] 48 const tasks: Bluebird<MTag>[] = []
44 tags.forEach(tag => { 49 tags.forEach(tag => {
45 const query = { 50 const query = {
46 where: { 51 where: {
@@ -52,7 +57,7 @@ export class TagModel extends Model<TagModel> {
52 transaction 57 transaction
53 } 58 }
54 59
55 const promise = TagModel.findOrCreate(query) 60 const promise = TagModel.findOrCreate<MTag>(query)
56 .then(([ tagInstance ]) => tagInstance) 61 .then(([ tagInstance ]) => tagInstance)
57 tasks.push(promise) 62 tasks.push(promise)
58 }) 63 })
diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts
index 1ac7919b3..3636db18d 100644
--- a/server/models/video/video-abuse.ts
+++ b/server/models/video/video-abuse.ts
@@ -7,10 +7,13 @@ import {
7 isVideoAbuseStateValid 7 isVideoAbuseStateValid
8} from '../../helpers/custom-validators/video-abuses' 8} from '../../helpers/custom-validators/video-abuses'
9import { AccountModel } from '../account/account' 9import { AccountModel } from '../account/account'
10import { getSort, throwIfNotValid } from '../utils' 10import { buildBlockedAccountSQL, getSort, throwIfNotValid } from '../utils'
11import { VideoModel } from './video' 11import { VideoModel } from './video'
12import { VideoAbuseState } from '../../../shared' 12import { VideoAbuseState } from '../../../shared'
13import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants' 13import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants'
14import { MUserAccountId, MVideoAbuse, MVideoAbuseFormattable, MVideoAbuseVideo } from '../../typings/models'
15import * as Bluebird from 'bluebird'
16import { literal, Op } from 'sequelize'
14 17
15@Table({ 18@Table({
16 tableName: 'videoAbuse', 19 tableName: 'videoAbuse',
@@ -73,7 +76,7 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
73 }) 76 })
74 Video: VideoModel 77 Video: VideoModel
75 78
76 static loadByIdAndVideoId (id: number, videoId: number) { 79 static loadByIdAndVideoId (id: number, videoId: number): Bluebird<MVideoAbuse> {
77 const query = { 80 const query = {
78 where: { 81 where: {
79 id, 82 id,
@@ -83,11 +86,25 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
83 return VideoAbuseModel.findOne(query) 86 return VideoAbuseModel.findOne(query)
84 } 87 }
85 88
86 static listForApi (start: number, count: number, sort: string) { 89 static listForApi (parameters: {
90 start: number,
91 count: number,
92 sort: string,
93 serverAccountId: number
94 user?: MUserAccountId
95 }) {
96 const { start, count, sort, user, serverAccountId } = parameters
97 const userAccountId = user ? user.Account.id : undefined
98
87 const query = { 99 const query = {
88 offset: start, 100 offset: start,
89 limit: count, 101 limit: count,
90 order: getSort(sort), 102 order: getSort(sort),
103 where: {
104 reporterAccountId: {
105 [Op.notIn]: literal('(' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')')
106 }
107 },
91 include: [ 108 include: [
92 { 109 {
93 model: AccountModel, 110 model: AccountModel,
@@ -106,7 +123,7 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
106 }) 123 })
107 } 124 }
108 125
109 toFormattedJSON (): VideoAbuse { 126 toFormattedJSON (this: MVideoAbuseFormattable): VideoAbuse {
110 return { 127 return {
111 id: this.id, 128 id: this.id,
112 reason: this.reason, 129 reason: this.reason,
@@ -125,7 +142,7 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
125 } 142 }
126 } 143 }
127 144
128 toActivityPubObject (): VideoAbuseObject { 145 toActivityPubObject (this: MVideoAbuseVideo): VideoAbuseObject {
129 return { 146 return {
130 type: 'Flag' as 'Flag', 147 type: 'Flag' as 'Flag',
131 content: this.reason, 148 content: this.reason,
diff --git a/server/models/video/video-blacklist.ts b/server/models/video/video-blacklist.ts
index cdb725e7a..694983cb3 100644
--- a/server/models/video/video-blacklist.ts
+++ b/server/models/video/video-blacklist.ts
@@ -1,12 +1,14 @@
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, getSort, SortType, throwIfNotValid } from '../utils' 2import { getBlacklistSort, SortType, throwIfNotValid } from '../utils'
3import { ScopeNames as VideoModelScopeNames, 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'
6import { VideoBlacklist, VideoBlacklistType } from '../../../shared/models/videos' 6import { VideoBlacklist, VideoBlacklistType } from '../../../shared/models/videos'
7import { CONSTRAINTS_FIELDS } from '../../initializers/constants' 7import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
8import { FindOptions, literal } from 'sequelize' 8import { FindOptions } from 'sequelize'
9import { ThumbnailModel } from './thumbnail' 9import { ThumbnailModel } from './thumbnail'
10import * as Bluebird from 'bluebird'
11import { MVideoBlacklist, MVideoBlacklistFormattable } from '@server/typings/models'
10 12
11@Table({ 13@Table({
12 tableName: 'videoBlacklist', 14 tableName: 'videoBlacklist',
@@ -98,7 +100,7 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> {
98 }) 100 })
99 } 101 }
100 102
101 static loadByVideoId (id: number) { 103 static loadByVideoId (id: number): Bluebird<MVideoBlacklist> {
102 const query = { 104 const query = {
103 where: { 105 where: {
104 videoId: id 106 videoId: id
@@ -108,7 +110,7 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> {
108 return VideoBlacklistModel.findOne(query) 110 return VideoBlacklistModel.findOne(query)
109 } 111 }
110 112
111 toFormattedJSON (): VideoBlacklist { 113 toFormattedJSON (this: MVideoBlacklistFormattable): VideoBlacklist {
112 return { 114 return {
113 id: this.id, 115 id: this.id,
114 createdAt: this.createdAt, 116 createdAt: this.createdAt,
diff --git a/server/models/video/video-caption.ts b/server/models/video/video-caption.ts
index a01565851..ad5801768 100644
--- a/server/models/video/video-caption.ts
+++ b/server/models/video/video-caption.ts
@@ -21,6 +21,8 @@ import { join } from 'path'
21import { logger } from '../../helpers/logger' 21import { logger } from '../../helpers/logger'
22import { remove } from 'fs-extra' 22import { remove } from 'fs-extra'
23import { CONFIG } from '../../initializers/config' 23import { CONFIG } from '../../initializers/config'
24import * as Bluebird from 'bluebird'
25import { MVideoCaptionFormattable, MVideoCaptionVideo } from '@server/typings/models'
24 26
25export enum ScopeNames { 27export enum ScopeNames {
26 WITH_VIDEO_UUID_AND_REMOTE = 'WITH_VIDEO_UUID_AND_REMOTE' 28 WITH_VIDEO_UUID_AND_REMOTE = 'WITH_VIDEO_UUID_AND_REMOTE'
@@ -30,7 +32,7 @@ export enum ScopeNames {
30 [ScopeNames.WITH_VIDEO_UUID_AND_REMOTE]: { 32 [ScopeNames.WITH_VIDEO_UUID_AND_REMOTE]: {
31 include: [ 33 include: [
32 { 34 {
33 attributes: [ 'uuid', 'remote' ], 35 attributes: [ 'id', 'uuid', 'remote' ],
34 model: VideoModel.unscoped(), 36 model: VideoModel.unscoped(),
35 required: true 37 required: true
36 } 38 }
@@ -93,7 +95,7 @@ export class VideoCaptionModel extends Model<VideoCaptionModel> {
93 return undefined 95 return undefined
94 } 96 }
95 97
96 static loadByVideoIdAndLanguage (videoId: string | number, language: string) { 98 static loadByVideoIdAndLanguage (videoId: string | number, language: string): Bluebird<MVideoCaptionVideo> {
97 const videoInclude = { 99 const videoInclude = {
98 model: VideoModel.unscoped(), 100 model: VideoModel.unscoped(),
99 attributes: [ 'id', 'remote', 'uuid' ], 101 attributes: [ 'id', 'remote', 'uuid' ],
@@ -122,7 +124,7 @@ export class VideoCaptionModel extends Model<VideoCaptionModel> {
122 .then(([ caption ]) => caption) 124 .then(([ caption ]) => caption)
123 } 125 }
124 126
125 static listVideoCaptions (videoId: number) { 127 static listVideoCaptions (videoId: number): Bluebird<MVideoCaptionVideo[]> {
126 const query = { 128 const query = {
127 order: [ [ 'language', 'ASC' ] ] as OrderItem[], 129 order: [ [ 'language', 'ASC' ] ] as OrderItem[],
128 where: { 130 where: {
@@ -152,7 +154,7 @@ export class VideoCaptionModel extends Model<VideoCaptionModel> {
152 return this.Video.remote === false 154 return this.Video.remote === false
153 } 155 }
154 156
155 toFormattedJSON (): VideoCaption { 157 toFormattedJSON (this: MVideoCaptionFormattable): VideoCaption {
156 return { 158 return {
157 language: { 159 language: {
158 id: this.language, 160 id: this.language,
@@ -162,15 +164,15 @@ export class VideoCaptionModel extends Model<VideoCaptionModel> {
162 } 164 }
163 } 165 }
164 166
165 getCaptionStaticPath () { 167 getCaptionStaticPath (this: MVideoCaptionFormattable) {
166 return join(LAZY_STATIC_PATHS.VIDEO_CAPTIONS, this.getCaptionName()) 168 return join(LAZY_STATIC_PATHS.VIDEO_CAPTIONS, this.getCaptionName())
167 } 169 }
168 170
169 getCaptionName () { 171 getCaptionName (this: MVideoCaptionFormattable) {
170 return `${this.Video.uuid}-${this.language}.vtt` 172 return `${this.Video.uuid}-${this.language}.vtt`
171 } 173 }
172 174
173 removeCaptionFile () { 175 removeCaptionFile (this: MVideoCaptionFormattable) {
174 return remove(CONFIG.STORAGE.CAPTIONS_DIR + this.getCaptionName()) 176 return remove(CONFIG.STORAGE.CAPTIONS_DIR + this.getCaptionName())
175 } 177 }
176} 178}
diff --git a/server/models/video/video-change-ownership.ts b/server/models/video/video-change-ownership.ts
index b545a2f8c..f7a351329 100644
--- a/server/models/video/video-change-ownership.ts
+++ b/server/models/video/video-change-ownership.ts
@@ -3,6 +3,8 @@ import { AccountModel } from '../account/account'
3import { ScopeNames as VideoScopeNames, VideoModel } from './video' 3import { ScopeNames as VideoScopeNames, VideoModel } from './video'
4import { VideoChangeOwnership, VideoChangeOwnershipStatus } from '../../../shared/models/videos' 4import { VideoChangeOwnership, VideoChangeOwnershipStatus } from '../../../shared/models/videos'
5import { getSort } from '../utils' 5import { getSort } from '../utils'
6import { MVideoChangeOwnershipFormattable, MVideoChangeOwnershipFull } from '@server/typings/models/video/video-change-ownership'
7import * as Bluebird from 'bluebird'
6 8
7enum ScopeNames { 9enum ScopeNames {
8 WITH_ACCOUNTS = 'WITH_ACCOUNTS', 10 WITH_ACCOUNTS = 'WITH_ACCOUNTS',
@@ -108,16 +110,16 @@ export class VideoChangeOwnershipModel extends Model<VideoChangeOwnershipModel>
108 110
109 return Promise.all([ 111 return Promise.all([
110 VideoChangeOwnershipModel.scope(ScopeNames.WITH_ACCOUNTS).count(query), 112 VideoChangeOwnershipModel.scope(ScopeNames.WITH_ACCOUNTS).count(query),
111 VideoChangeOwnershipModel.scope([ ScopeNames.WITH_ACCOUNTS, ScopeNames.WITH_VIDEO ]).findAll(query) 113 VideoChangeOwnershipModel.scope([ ScopeNames.WITH_ACCOUNTS, ScopeNames.WITH_VIDEO ]).findAll<MVideoChangeOwnershipFull>(query)
112 ]).then(([ count, rows ]) => ({ total: count, data: rows })) 114 ]).then(([ count, rows ]) => ({ total: count, data: rows }))
113 } 115 }
114 116
115 static load (id: number) { 117 static load (id: number): Bluebird<MVideoChangeOwnershipFull> {
116 return VideoChangeOwnershipModel.scope([ ScopeNames.WITH_ACCOUNTS, ScopeNames.WITH_VIDEO ]) 118 return VideoChangeOwnershipModel.scope([ ScopeNames.WITH_ACCOUNTS, ScopeNames.WITH_VIDEO ])
117 .findByPk(id) 119 .findByPk(id)
118 } 120 }
119 121
120 toFormattedJSON (): VideoChangeOwnership { 122 toFormattedJSON (this: MVideoChangeOwnershipFormattable): VideoChangeOwnership {
121 return { 123 return {
122 id: this.id, 124 id: this.id,
123 status: this.status, 125 status: this.status,
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts
index 6241a75a3..05545bd9d 100644
--- a/server/models/video/video-channel.ts
+++ b/server/models/video/video-channel.ts
@@ -33,6 +33,15 @@ import { ServerModel } from '../server/server'
33import { FindOptions, ModelIndexesOptions, Op } from 'sequelize' 33import { FindOptions, ModelIndexesOptions, Op } 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'
37import {
38 MChannelAccountDefault,
39 MChannelActor,
40 MChannelActorAccountDefaultVideos,
41 MChannelAP,
42 MChannelFormattable,
43 MChannelSummaryFormattable
44} from '../../typings/models/video'
36 45
37// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation 46// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
38const indexes: ModelIndexesOptions[] = [ 47const indexes: ModelIndexesOptions[] = [
@@ -47,7 +56,7 @@ const indexes: ModelIndexesOptions[] = [
47] 56]
48 57
49export enum ScopeNames { 58export enum ScopeNames {
50 AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST', 59 FOR_API = 'FOR_API',
51 WITH_ACCOUNT = 'WITH_ACCOUNT', 60 WITH_ACCOUNT = 'WITH_ACCOUNT',
52 WITH_ACTOR = 'WITH_ACTOR', 61 WITH_ACTOR = 'WITH_ACTOR',
53 WITH_VIDEOS = 'WITH_VIDEOS', 62 WITH_VIDEOS = 'WITH_VIDEOS',
@@ -74,10 +83,10 @@ export type SummaryOptions = {
74@Scopes(() => ({ 83@Scopes(() => ({
75 [ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => { 84 [ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => {
76 const base: FindOptions = { 85 const base: FindOptions = {
77 attributes: [ 'name', 'description', 'id', 'actorId' ], 86 attributes: [ 'id', 'name', 'description', 'actorId' ],
78 include: [ 87 include: [
79 { 88 {
80 attributes: [ 'preferredUsername', 'url', 'serverId', 'avatarId' ], 89 attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ],
81 model: ActorModel.unscoped(), 90 model: ActorModel.unscoped(),
82 required: true, 91 required: true,
83 include: [ 92 include: [
@@ -106,7 +115,7 @@ export type SummaryOptions = {
106 115
107 return base 116 return base
108 }, 117 },
109 [ScopeNames.AVAILABLE_FOR_LIST]: (options: AvailableForListOptions) => { 118 [ScopeNames.FOR_API]: (options: AvailableForListOptions) => {
110 // Only list local channels OR channels that are on an instance followed by actorId 119 // Only list local channels OR channels that are on an instance followed by actorId
111 const inQueryInstanceFollow = buildServerIdsFollowedBy(options.actorId) 120 const inQueryInstanceFollow = buildServerIdsFollowedBy(options.actorId)
112 121
@@ -268,7 +277,7 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
268 } 277 }
269 278
270 const scopes = { 279 const scopes = {
271 method: [ ScopeNames.AVAILABLE_FOR_LIST, { actorId } as AvailableForListOptions ] 280 method: [ ScopeNames.FOR_API, { actorId } as AvailableForListOptions ]
272 } 281 }
273 return VideoChannelModel 282 return VideoChannelModel
274 .scope(scopes) 283 .scope(scopes)
@@ -278,7 +287,7 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
278 }) 287 })
279 } 288 }
280 289
281 static listLocalsForSitemap (sort: string) { 290 static listLocalsForSitemap (sort: string): Bluebird<MChannelActor[]> {
282 const query = { 291 const query = {
283 attributes: [ ], 292 attributes: [ ],
284 offset: 0, 293 offset: 0,
@@ -331,7 +340,7 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
331 } 340 }
332 341
333 const scopes = { 342 const scopes = {
334 method: [ ScopeNames.AVAILABLE_FOR_LIST, { actorId: options.actorId } as AvailableForListOptions ] 343 method: [ ScopeNames.FOR_API, { actorId: options.actorId } as AvailableForListOptions ]
335 } 344 }
336 return VideoChannelModel 345 return VideoChannelModel
337 .scope(scopes) 346 .scope(scopes)
@@ -369,13 +378,13 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
369 }) 378 })
370 } 379 }
371 380
372 static loadByIdAndPopulateAccount (id: number) { 381 static loadByIdAndPopulateAccount (id: number): Bluebird<MChannelAccountDefault> {
373 return VideoChannelModel.unscoped() 382 return VideoChannelModel.unscoped()
374 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) 383 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
375 .findByPk(id) 384 .findByPk(id)
376 } 385 }
377 386
378 static loadByIdAndAccount (id: number, accountId: number) { 387 static loadByIdAndAccount (id: number, accountId: number): Bluebird<MChannelAccountDefault> {
379 const query = { 388 const query = {
380 where: { 389 where: {
381 id, 390 id,
@@ -388,13 +397,13 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
388 .findOne(query) 397 .findOne(query)
389 } 398 }
390 399
391 static loadAndPopulateAccount (id: number) { 400 static loadAndPopulateAccount (id: number): Bluebird<MChannelAccountDefault> {
392 return VideoChannelModel.unscoped() 401 return VideoChannelModel.unscoped()
393 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) 402 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
394 .findByPk(id) 403 .findByPk(id)
395 } 404 }
396 405
397 static loadByUrlAndPopulateAccount (url: string) { 406 static loadByUrlAndPopulateAccount (url: string): Bluebird<MChannelAccountDefault> {
398 const query = { 407 const query = {
399 include: [ 408 include: [
400 { 409 {
@@ -420,7 +429,7 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
420 return VideoChannelModel.loadByNameAndHostAndPopulateAccount(name, host) 429 return VideoChannelModel.loadByNameAndHostAndPopulateAccount(name, host)
421 } 430 }
422 431
423 static loadLocalByNameAndPopulateAccount (name: string) { 432 static loadLocalByNameAndPopulateAccount (name: string): Bluebird<MChannelAccountDefault> {
424 const query = { 433 const query = {
425 include: [ 434 include: [
426 { 435 {
@@ -439,7 +448,7 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
439 .findOne(query) 448 .findOne(query)
440 } 449 }
441 450
442 static loadByNameAndHostAndPopulateAccount (name: string, host: string) { 451 static loadByNameAndHostAndPopulateAccount (name: string, host: string): Bluebird<MChannelAccountDefault> {
443 const query = { 452 const query = {
444 include: [ 453 include: [
445 { 454 {
@@ -464,7 +473,7 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
464 .findOne(query) 473 .findOne(query)
465 } 474 }
466 475
467 static loadAndPopulateAccountAndVideos (id: number) { 476 static loadAndPopulateAccountAndVideos (id: number): Bluebird<MChannelActorAccountDefaultVideos> {
468 const options = { 477 const options = {
469 include: [ 478 include: [
470 VideoModel 479 VideoModel
@@ -476,7 +485,20 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
476 .findByPk(id, options) 485 .findByPk(id, options)
477 } 486 }
478 487
479 toFormattedJSON (): VideoChannel { 488 toFormattedSummaryJSON (this: MChannelSummaryFormattable): VideoChannelSummary {
489 const actor = this.Actor.toFormattedSummaryJSON()
490
491 return {
492 id: this.id,
493 name: actor.name,
494 displayName: this.getDisplayName(),
495 url: actor.url,
496 host: actor.host,
497 avatar: actor.avatar
498 }
499 }
500
501 toFormattedJSON (this: MChannelFormattable): VideoChannel {
480 const actor = this.Actor.toFormattedJSON() 502 const actor = this.Actor.toFormattedJSON()
481 const videoChannel = { 503 const videoChannel = {
482 id: this.id, 504 id: this.id,
@@ -494,21 +516,8 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
494 return Object.assign(actor, videoChannel) 516 return Object.assign(actor, videoChannel)
495 } 517 }
496 518
497 toFormattedSummaryJSON (): VideoChannelSummary { 519 toActivityPubObject (this: MChannelAP): ActivityPubActor {
498 const actor = this.Actor.toFormattedJSON() 520 const obj = this.Actor.toActivityPubObject(this.name)
499
500 return {
501 id: this.id,
502 name: actor.name,
503 displayName: this.getDisplayName(),
504 url: actor.url,
505 host: actor.host,
506 avatar: actor.avatar
507 }
508 }
509
510 toActivityPubObject (): ActivityPubActor {
511 const obj = this.Actor.toActivityPubObject(this.name, 'VideoChannel')
512 521
513 return Object.assign(obj, { 522 return Object.assign(obj, {
514 summary: this.description, 523 summary: this.description,
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts
index 58b75510d..2e4220434 100644
--- a/server/models/video/video-comment.ts
+++ b/server/models/video/video-comment.ts
@@ -1,36 +1,32 @@
1import { 1import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
2 AllowNull,
3 BeforeDestroy,
4 BelongsTo,
5 Column,
6 CreatedAt,
7 DataType,
8 ForeignKey,
9 Is,
10 Model,
11 Scopes,
12 Table,
13 UpdatedAt
14} from 'sequelize-typescript'
15import { ActivityTagObject } from '../../../shared/models/activitypub/objects/common-objects' 2import { ActivityTagObject } from '../../../shared/models/activitypub/objects/common-objects'
16import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' 3import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object'
17import { VideoComment } from '../../../shared/models/videos/video-comment.model' 4import { VideoComment } from '../../../shared/models/videos/video-comment.model'
18import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 5import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
19import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' 6import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
20import { sendDeleteVideoComment } from '../../lib/activitypub/send'
21import { AccountModel } from '../account/account' 7import { AccountModel } from '../account/account'
22import { ActorModel } from '../activitypub/actor' 8import { ActorModel } from '../activitypub/actor'
23import { AvatarModel } from '../avatar/avatar'
24import { ServerModel } from '../server/server'
25import { buildBlockedAccountSQL, buildLocalAccountIdsIn, getSort, throwIfNotValid } from '../utils' 9import { buildBlockedAccountSQL, buildLocalAccountIdsIn, getSort, throwIfNotValid } from '../utils'
26import { VideoModel } from './video' 10import { VideoModel } from './video'
27import { VideoChannelModel } from './video-channel' 11import { VideoChannelModel } from './video-channel'
28import { getServerActor } from '../../helpers/utils' 12import { getServerActor } from '../../helpers/utils'
29import { UserModel } from '../account/user'
30import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor' 13import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor'
31import { regexpCapture } from '../../helpers/regexp' 14import { regexpCapture } from '../../helpers/regexp'
32import { uniq } from 'lodash' 15import { uniq } from 'lodash'
33import { FindOptions, literal, Op, Order, ScopeOptions, Sequelize, Transaction } from 'sequelize' 16import { FindOptions, Op, Order, ScopeOptions, Sequelize, Transaction } from 'sequelize'
17import * as Bluebird from 'bluebird'
18import {
19 MComment,
20 MCommentAP,
21 MCommentFormattable,
22 MCommentId,
23 MCommentOwner,
24 MCommentOwnerReplyVideoLight,
25 MCommentOwnerVideo,
26 MCommentOwnerVideoFeed,
27 MCommentOwnerVideoReply
28} from '../../typings/models/video'
29import { MUserAccountId } from '@server/typings/models'
34 30
35enum ScopeNames { 31enum ScopeNames {
36 WITH_ACCOUNT = 'WITH_ACCOUNT', 32 WITH_ACCOUNT = 'WITH_ACCOUNT',
@@ -68,22 +64,7 @@ enum ScopeNames {
68 [ScopeNames.WITH_ACCOUNT]: { 64 [ScopeNames.WITH_ACCOUNT]: {
69 include: [ 65 include: [
70 { 66 {
71 model: AccountModel, 67 model: AccountModel
72 include: [
73 {
74 model: ActorModel,
75 include: [
76 {
77 model: ServerModel,
78 required: false
79 },
80 {
81 model: AvatarModel,
82 required: false
83 }
84 ]
85 }
86 ]
87 } 68 }
88 ] 69 ]
89 }, 70 },
@@ -102,22 +83,12 @@ enum ScopeNames {
102 required: true, 83 required: true,
103 include: [ 84 include: [
104 { 85 {
105 model: VideoChannelModel.unscoped(), 86 model: VideoChannelModel,
106 required: true, 87 required: true,
107 include: [ 88 include: [
108 { 89 {
109 model: ActorModel,
110 required: true
111 },
112 {
113 model: AccountModel, 90 model: AccountModel,
114 required: true, 91 required: true
115 include: [
116 {
117 model: ActorModel,
118 required: true
119 }
120 ]
121 } 92 }
122 ] 93 ]
123 } 94 }
@@ -212,7 +183,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
212 }) 183 })
213 Account: AccountModel 184 Account: AccountModel
214 185
215 static loadById (id: number, t?: Transaction) { 186 static loadById (id: number, t?: Transaction): Bluebird<MComment> {
216 const query: FindOptions = { 187 const query: FindOptions = {
217 where: { 188 where: {
218 id 189 id
@@ -224,7 +195,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
224 return VideoCommentModel.findOne(query) 195 return VideoCommentModel.findOne(query)
225 } 196 }
226 197
227 static loadByIdAndPopulateVideoAndAccountAndReply (id: number, t?: Transaction) { 198 static loadByIdAndPopulateVideoAndAccountAndReply (id: number, t?: Transaction): Bluebird<MCommentOwnerVideoReply> {
228 const query: FindOptions = { 199 const query: FindOptions = {
229 where: { 200 where: {
230 id 201 id
@@ -238,7 +209,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
238 .findOne(query) 209 .findOne(query)
239 } 210 }
240 211
241 static loadByUrlAndPopulateAccountAndVideo (url: string, t?: Transaction) { 212 static loadByUrlAndPopulateAccountAndVideo (url: string, t?: Transaction): Bluebird<MCommentOwnerVideo> {
242 const query: FindOptions = { 213 const query: FindOptions = {
243 where: { 214 where: {
244 url 215 url
@@ -250,7 +221,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
250 return VideoCommentModel.scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_VIDEO ]).findOne(query) 221 return VideoCommentModel.scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_VIDEO ]).findOne(query)
251 } 222 }
252 223
253 static loadByUrlAndPopulateReplyAndVideoUrlAndAccount (url: string, t?: Transaction) { 224 static loadByUrlAndPopulateReplyAndVideoUrlAndAccount (url: string, t?: Transaction): Bluebird<MCommentOwnerReplyVideoLight> {
254 const query: FindOptions = { 225 const query: FindOptions = {
255 where: { 226 where: {
256 url 227 url
@@ -273,7 +244,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
273 start: number, 244 start: number,
274 count: number, 245 count: number,
275 sort: string, 246 sort: string,
276 user?: UserModel 247 user?: MUserAccountId
277 }) { 248 }) {
278 const { videoId, start, count, sort, user } = parameters 249 const { videoId, start, count, sort, user } = parameters
279 250
@@ -314,7 +285,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
314 static async listThreadCommentsForApi (parameters: { 285 static async listThreadCommentsForApi (parameters: {
315 videoId: number, 286 videoId: number,
316 threadId: number, 287 threadId: number,
317 user?: UserModel 288 user?: MUserAccountId
318 }) { 289 }) {
319 const { videoId, threadId, user } = parameters 290 const { videoId, threadId, user } = parameters
320 291
@@ -353,7 +324,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
353 }) 324 })
354 } 325 }
355 326
356 static listThreadParentComments (comment: VideoCommentModel, t: Transaction, order: 'ASC' | 'DESC' = 'ASC') { 327 static listThreadParentComments (comment: MCommentId, t: Transaction, order: 'ASC' | 'DESC' = 'ASC'): Bluebird<MCommentOwner[]> {
357 const query = { 328 const query = {
358 order: [ [ 'createdAt', order ] ] as Order, 329 order: [ [ 'createdAt', order ] ] as Order,
359 where: { 330 where: {
@@ -389,10 +360,10 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
389 transaction: t 360 transaction: t
390 } 361 }
391 362
392 return VideoCommentModel.findAndCountAll(query) 363 return VideoCommentModel.findAndCountAll<MComment>(query)
393 } 364 }
394 365
395 static listForFeed (start: number, count: number, videoId?: number) { 366 static listForFeed (start: number, count: number, videoId?: number): Bluebird<MCommentOwnerVideoFeed[]> {
396 const query = { 367 const query = {
397 order: [ [ 'createdAt', 'DESC' ] ] as Order, 368 order: [ [ 'createdAt', 'DESC' ] ] as Order,
398 offset: start, 369 offset: start,
@@ -506,7 +477,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
506 return uniq(result) 477 return uniq(result)
507 } 478 }
508 479
509 toFormattedJSON () { 480 toFormattedJSON (this: MCommentFormattable) {
510 return { 481 return {
511 id: this.id, 482 id: this.id,
512 url: this.url, 483 url: this.url,
@@ -521,7 +492,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
521 } as VideoComment 492 } as VideoComment
522 } 493 }
523 494
524 toActivityPubObject (threadParentComments: VideoCommentModel[]): VideoCommentObject { 495 toActivityPubObject (this: MCommentAP, threadParentComments: MCommentOwner[]): VideoCommentObject {
525 let inReplyTo: string 496 let inReplyTo: string
526 // New thread, so in AS we reply to the video 497 // New thread, so in AS we reply to the video
527 if (this.inReplyToCommentId === null) { 498 if (this.inReplyToCommentId === null) {
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts
index 05c490759..6304f741c 100644
--- a/server/models/video/video-file.ts
+++ b/server/models/video/video-file.ts
@@ -25,6 +25,7 @@ import { VideoRedundancyModel } from '../redundancy/video-redundancy'
25import { VideoStreamingPlaylistModel } from './video-streaming-playlist' 25import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
26import { FindOptions, QueryTypes, Transaction } from 'sequelize' 26import { FindOptions, QueryTypes, Transaction } from 'sequelize'
27import { MIMETYPES } from '../../initializers/constants' 27import { MIMETYPES } from '../../initializers/constants'
28import { MVideoFile } from '@server/typings/models'
28 29
29@Table({ 30@Table({
30 tableName: 'videoFile', 31 tableName: 'videoFile',
@@ -166,7 +167,7 @@ export class VideoFileModel extends Model<VideoFileModel> {
166 return !!MIMETYPES.AUDIO.EXT_MIMETYPE[this.extname] 167 return !!MIMETYPES.AUDIO.EXT_MIMETYPE[this.extname]
167 } 168 }
168 169
169 hasSameUniqueKeysThan (other: VideoFileModel) { 170 hasSameUniqueKeysThan (other: MVideoFile) {
170 return this.fps === other.fps && 171 return this.fps === other.fps &&
171 this.resolution === other.resolution && 172 this.resolution === other.resolution &&
172 this.videoId === other.videoId 173 this.videoId === other.videoId
diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts
index 284539def..2987aa780 100644
--- a/server/models/video/video-format-utils.ts
+++ b/server/models/video/video-format-utils.ts
@@ -1,6 +1,5 @@
1import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' 1import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos'
2import { VideoModel } from './video' 2import { VideoModel } from './video'
3import { VideoFileModel } from './video-file'
4import { 3import {
5 ActivityPlaylistInfohashesObject, 4 ActivityPlaylistInfohashesObject,
6 ActivityPlaylistSegmentHashesObject, 5 ActivityPlaylistSegmentHashesObject,
@@ -17,7 +16,9 @@ import {
17} from '../../lib/activitypub' 16} from '../../lib/activitypub'
18import { isArray } from '../../helpers/custom-validators/misc' 17import { isArray } from '../../helpers/custom-validators/misc'
19import { VideoStreamingPlaylist } from '../../../shared/models/videos/video-streaming-playlist.model' 18import { VideoStreamingPlaylist } from '../../../shared/models/videos/video-streaming-playlist.model'
20import { VideoStreamingPlaylistModel } from './video-streaming-playlist' 19import { MStreamingPlaylistRedundanciesOpt, MVideo, MVideoAP, MVideoFormattable, MVideoFormattableDetails } from '../../typings/models'
20import { MStreamingPlaylistRedundancies } from '../../typings/models/video/video-streaming-playlist'
21import { MVideoFileRedundanciesOpt } from '../../typings/models/video/video-file'
21 22
22export type VideoFormattingJSONOptions = { 23export type VideoFormattingJSONOptions = {
23 completeDescription?: boolean 24 completeDescription?: boolean
@@ -28,7 +29,7 @@ export type VideoFormattingJSONOptions = {
28 blacklistInfo?: boolean 29 blacklistInfo?: boolean
29 } 30 }
30} 31}
31function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormattingJSONOptions): Video { 32function videoModelToFormattedJSON (video: MVideoFormattable, options?: VideoFormattingJSONOptions): Video {
32 const userHistory = isArray(video.UserVideoHistories) ? video.UserVideoHistories[0] : undefined 33 const userHistory = isArray(video.UserVideoHistories) ? video.UserVideoHistories[0] : undefined
33 34
34 const videoObject: Video = { 35 const videoObject: Video = {
@@ -102,7 +103,7 @@ function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormatting
102 return videoObject 103 return videoObject
103} 104}
104 105
105function videoModelToFormattedDetailsJSON (video: VideoModel): VideoDetails { 106function videoModelToFormattedDetailsJSON (video: MVideoFormattableDetails): VideoDetails {
106 const formattedJson = video.toFormattedJSON({ 107 const formattedJson = video.toFormattedJSON({
107 additionalAttributes: { 108 additionalAttributes: {
108 scheduledUpdate: true, 109 scheduledUpdate: true,
@@ -114,7 +115,7 @@ function videoModelToFormattedDetailsJSON (video: VideoModel): VideoDetails {
114 115
115 const tags = video.Tags ? video.Tags.map(t => t.name) : [] 116 const tags = video.Tags ? video.Tags.map(t => t.name) : []
116 117
117 const streamingPlaylists = streamingPlaylistsModelToFormattedJSON(video, video.VideoStreamingPlaylists) 118 const streamingPlaylists = streamingPlaylistsModelToFormattedJSON(video.VideoStreamingPlaylists)
118 119
119 const detailsJson = { 120 const detailsJson = {
120 support: video.support, 121 support: video.support,
@@ -142,7 +143,7 @@ function videoModelToFormattedDetailsJSON (video: VideoModel): VideoDetails {
142 return Object.assign(formattedJson, detailsJson) 143 return Object.assign(formattedJson, detailsJson)
143} 144}
144 145
145function streamingPlaylistsModelToFormattedJSON (video: VideoModel, playlists: VideoStreamingPlaylistModel[]): VideoStreamingPlaylist[] { 146function streamingPlaylistsModelToFormattedJSON (playlists: MStreamingPlaylistRedundanciesOpt[]): VideoStreamingPlaylist[] {
146 if (isArray(playlists) === false) return [] 147 if (isArray(playlists) === false) return []
147 148
148 return playlists 149 return playlists
@@ -161,7 +162,7 @@ function streamingPlaylistsModelToFormattedJSON (video: VideoModel, playlists: V
161 }) 162 })
162} 163}
163 164
164function videoFilesModelToFormattedJSON (video: VideoModel, videoFiles: VideoFileModel[]): VideoFile[] { 165function videoFilesModelToFormattedJSON (video: MVideo, videoFiles: MVideoFileRedundanciesOpt[]): VideoFile[] {
165 const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() 166 const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
166 167
167 return videoFiles 168 return videoFiles
@@ -189,7 +190,7 @@ function videoFilesModelToFormattedJSON (video: VideoModel, videoFiles: VideoFil
189 }) 190 })
190} 191}
191 192
192function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject { 193function videoModelToActivityPubObject (video: MVideoAP): VideoTorrentObject {
193 const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() 194 const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
194 if (!video.Tags) video.Tags = [] 195 if (!video.Tags) video.Tags = []
195 196
diff --git a/server/models/video/video-import.ts b/server/models/video/video-import.ts
index 480a671c8..af5314ce9 100644
--- a/server/models/video/video-import.ts
+++ b/server/models/video/video-import.ts
@@ -20,6 +20,8 @@ import { isVideoImportStateValid, isVideoImportTargetUrlValid } from '../../help
20import { VideoImport, VideoImportState } from '../../../shared' 20import { VideoImport, VideoImportState } from '../../../shared'
21import { isVideoMagnetUriValid } from '../../helpers/custom-validators/videos' 21import { isVideoMagnetUriValid } from '../../helpers/custom-validators/videos'
22import { UserModel } from '../account/user' 22import { UserModel } from '../account/user'
23import * as Bluebird from 'bluebird'
24import { MVideoImportDefault, MVideoImportFormattable } from '@server/typings/models/video/video-import'
23 25
24@DefaultScope(() => ({ 26@DefaultScope(() => ({
25 include: [ 27 include: [
@@ -28,7 +30,11 @@ import { UserModel } from '../account/user'
28 required: true 30 required: true
29 }, 31 },
30 { 32 {
31 model: VideoModel.scope([ VideoModelScopeNames.WITH_ACCOUNT_DETAILS, VideoModelScopeNames.WITH_TAGS]), 33 model: VideoModel.scope([
34 VideoModelScopeNames.WITH_ACCOUNT_DETAILS,
35 VideoModelScopeNames.WITH_TAGS,
36 VideoModelScopeNames.WITH_THUMBNAILS
37 ]),
32 required: false 38 required: false
33 } 39 }
34 ] 40 ]
@@ -114,7 +120,7 @@ export class VideoImportModel extends Model<VideoImportModel> {
114 return undefined 120 return undefined
115 } 121 }
116 122
117 static loadAndPopulateVideo (id: number) { 123 static loadAndPopulateVideo (id: number): Bluebird<MVideoImportDefault> {
118 return VideoImportModel.findByPk(id) 124 return VideoImportModel.findByPk(id)
119 } 125 }
120 126
@@ -135,7 +141,7 @@ export class VideoImportModel extends Model<VideoImportModel> {
135 } 141 }
136 } 142 }
137 143
138 return VideoImportModel.findAndCountAll(query) 144 return VideoImportModel.findAndCountAll<MVideoImportDefault>(query)
139 .then(({ rows, count }) => { 145 .then(({ rows, count }) => {
140 return { 146 return {
141 data: rows, 147 data: rows,
@@ -148,7 +154,7 @@ export class VideoImportModel extends Model<VideoImportModel> {
148 return this.targetUrl || this.magnetUri || this.torrentName 154 return this.targetUrl || this.magnetUri || this.torrentName
149 } 155 }
150 156
151 toFormattedJSON (): VideoImport { 157 toFormattedJSON (this: MVideoImportFormattable): VideoImport {
152 const videoFormatOptions = { 158 const videoFormatOptions = {
153 completeDescription: true, 159 completeDescription: true,
154 additionalAttributes: { state: true, waitTranscoding: true, scheduledUpdate: true } 160 additionalAttributes: { state: true, waitTranscoding: true, scheduledUpdate: true }
diff --git a/server/models/video/video-playlist-element.ts b/server/models/video/video-playlist-element.ts
index dd7653533..a28021313 100644
--- a/server/models/video/video-playlist-element.ts
+++ b/server/models/video/video-playlist-element.ts
@@ -21,10 +21,18 @@ import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
21import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object' 21import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object'
22import * as validator from 'validator' 22import * as validator from 'validator'
23import { AggregateOptions, Op, ScopeOptions, Sequelize, Transaction } from 'sequelize' 23import { AggregateOptions, Op, ScopeOptions, Sequelize, Transaction } from 'sequelize'
24import { UserModel } from '../account/user'
25import { VideoPlaylistElement, VideoPlaylistElementType } from '../../../shared/models/videos/playlist/video-playlist-element.model' 24import { VideoPlaylistElement, VideoPlaylistElementType } from '../../../shared/models/videos/playlist/video-playlist-element.model'
26import { AccountModel } from '../account/account' 25import { AccountModel } from '../account/account'
27import { VideoPrivacy } from '../../../shared/models/videos' 26import { VideoPrivacy } from '../../../shared/models/videos'
27import * as Bluebird from 'bluebird'
28import {
29 MVideoPlaylistElement,
30 MVideoPlaylistElementAP,
31 MVideoPlaylistElementFormattable,
32 MVideoPlaylistElementVideoUrlPlaylistPrivacy,
33 MVideoPlaylistVideoThumbnail
34} from '@server/typings/models/video/video-playlist-element'
35import { MUserAccountId } from '@server/typings/models'
28 36
29@Table({ 37@Table({
30 tableName: 'videoPlaylistElement', 38 tableName: 'videoPlaylistElement',
@@ -116,7 +124,7 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel>
116 count: number, 124 count: number,
117 videoPlaylistId: number, 125 videoPlaylistId: number,
118 serverAccount: AccountModel, 126 serverAccount: AccountModel,
119 user?: UserModel 127 user?: MUserAccountId
120 }) { 128 }) {
121 const accountIds = [ options.serverAccount.id ] 129 const accountIds = [ options.serverAccount.id ]
122 const videoScope: (ScopeOptions | string)[] = [ 130 const videoScope: (ScopeOptions | string)[] = [
@@ -162,7 +170,7 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel>
162 ]).then(([ total, data ]) => ({ total, data })) 170 ]).then(([ total, data ]) => ({ total, data }))
163 } 171 }
164 172
165 static loadByPlaylistAndVideo (videoPlaylistId: number, videoId: number) { 173 static loadByPlaylistAndVideo (videoPlaylistId: number, videoId: number): Bluebird<MVideoPlaylistElement> {
166 const query = { 174 const query = {
167 where: { 175 where: {
168 videoPlaylistId, 176 videoPlaylistId,
@@ -173,11 +181,14 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel>
173 return VideoPlaylistElementModel.findOne(query) 181 return VideoPlaylistElementModel.findOne(query)
174 } 182 }
175 183
176 static loadById (playlistElementId: number) { 184 static loadById (playlistElementId: number): Bluebird<MVideoPlaylistElement> {
177 return VideoPlaylistElementModel.findByPk(playlistElementId) 185 return VideoPlaylistElementModel.findByPk(playlistElementId)
178 } 186 }
179 187
180 static loadByPlaylistAndVideoForAP (playlistId: number | string, videoId: number | string) { 188 static loadByPlaylistAndVideoForAP (
189 playlistId: number | string,
190 videoId: number | string
191 ): Bluebird<MVideoPlaylistElementVideoUrlPlaylistPrivacy> {
181 const playlistWhere = validator.isUUID('' + playlistId) ? { uuid: playlistId } : { id: playlistId } 192 const playlistWhere = validator.isUUID('' + playlistId) ? { uuid: playlistId } : { id: playlistId }
182 const videoWhere = validator.isUUID('' + videoId) ? { uuid: videoId } : { id: videoId } 193 const videoWhere = validator.isUUID('' + videoId) ? { uuid: videoId } : { id: videoId }
183 194
@@ -218,7 +229,7 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel>
218 }) 229 })
219 } 230 }
220 231
221 static loadFirstElementWithVideoThumbnail (videoPlaylistId: number) { 232 static loadFirstElementWithVideoThumbnail (videoPlaylistId: number): Bluebird<MVideoPlaylistVideoThumbnail> {
222 const query = { 233 const query = {
223 order: getSort('position'), 234 order: getSort('position'),
224 where: { 235 where: {
@@ -290,7 +301,7 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel>
290 return VideoPlaylistElementModel.increment({ position: by }, query) 301 return VideoPlaylistElementModel.increment({ position: by }, query)
291 } 302 }
292 303
293 getType (displayNSFW?: boolean, accountId?: number) { 304 getType (this: MVideoPlaylistElementFormattable, displayNSFW?: boolean, accountId?: number) {
294 const video = this.Video 305 const video = this.Video
295 306
296 if (!video) return VideoPlaylistElementType.DELETED 307 if (!video) return VideoPlaylistElementType.DELETED
@@ -306,14 +317,17 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel>
306 return VideoPlaylistElementType.REGULAR 317 return VideoPlaylistElementType.REGULAR
307 } 318 }
308 319
309 getVideoElement (displayNSFW?: boolean, accountId?: number) { 320 getVideoElement (this: MVideoPlaylistElementFormattable, displayNSFW?: boolean, accountId?: number) {
310 if (!this.Video) return null 321 if (!this.Video) return null
311 if (this.getType(displayNSFW, accountId) !== VideoPlaylistElementType.REGULAR) return null 322 if (this.getType(displayNSFW, accountId) !== VideoPlaylistElementType.REGULAR) return null
312 323
313 return this.Video.toFormattedJSON() 324 return this.Video.toFormattedJSON()
314 } 325 }
315 326
316 toFormattedJSON (options: { displayNSFW?: boolean, accountId?: number } = {}): VideoPlaylistElement { 327 toFormattedJSON (
328 this: MVideoPlaylistElementFormattable,
329 options: { displayNSFW?: boolean, accountId?: number } = {}
330 ): VideoPlaylistElement {
317 return { 331 return {
318 id: this.id, 332 id: this.id,
319 position: this.position, 333 position: this.position,
@@ -326,7 +340,7 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel>
326 } 340 }
327 } 341 }
328 342
329 toActivityPubObject (): PlaylistElementObject { 343 toActivityPubObject (this: MVideoPlaylistElementAP): PlaylistElementObject {
330 const base: PlaylistElementObject = { 344 const base: PlaylistElementObject = {
331 id: this.url, 345 id: this.url,
332 type: 'PlaylistElement', 346 type: 'PlaylistElement',
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts
index c8e97c491..278d80ac0 100644
--- a/server/models/video/video-playlist.ts
+++ b/server/models/video/video-playlist.ts
@@ -43,6 +43,15 @@ import { VideoPlaylistType } from '../../../shared/models/videos/playlist/video-
43import { ThumbnailModel } from './thumbnail' 43import { ThumbnailModel } from './thumbnail'
44import { ActivityIconObject } from '../../../shared/models/activitypub/objects' 44import { ActivityIconObject } from '../../../shared/models/activitypub/objects'
45import { FindOptions, literal, Op, ScopeOptions, Transaction, WhereOptions } from 'sequelize' 45import { FindOptions, literal, Op, ScopeOptions, Transaction, WhereOptions } from 'sequelize'
46import * as Bluebird from 'bluebird'
47import {
48 MVideoPlaylistAccountThumbnail, MVideoPlaylistAP,
49 MVideoPlaylistFormattable,
50 MVideoPlaylistFull,
51 MVideoPlaylistFullSummary,
52 MVideoPlaylistIdWithElements
53} from '../../typings/models/video/video-playlist'
54import { MThumbnail } from '../../typings/models/video/thumbnail'
46 55
47enum ScopeNames { 56enum ScopeNames {
48 AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST', 57 AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
@@ -332,7 +341,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
332 }) 341 })
333 } 342 }
334 343
335 static listPlaylistIdsOf (accountId: number, videoIds: number[]) { 344 static listPlaylistIdsOf (accountId: number, videoIds: number[]): Bluebird<MVideoPlaylistIdWithElements[]> {
336 const query = { 345 const query = {
337 attributes: [ 'id' ], 346 attributes: [ 'id' ],
338 where: { 347 where: {
@@ -368,7 +377,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
368 .then(e => !!e) 377 .then(e => !!e)
369 } 378 }
370 379
371 static loadWithAccountAndChannelSummary (id: number | string, transaction: Transaction) { 380 static loadWithAccountAndChannelSummary (id: number | string, transaction: Transaction): Bluebird<MVideoPlaylistFullSummary> {
372 const where = buildWhereIdOrUUID(id) 381 const where = buildWhereIdOrUUID(id)
373 382
374 const query = { 383 const query = {
@@ -381,7 +390,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
381 .findOne(query) 390 .findOne(query)
382 } 391 }
383 392
384 static loadWithAccountAndChannel (id: number | string, transaction: Transaction) { 393 static loadWithAccountAndChannel (id: number | string, transaction: Transaction): Bluebird<MVideoPlaylistFull> {
385 const where = buildWhereIdOrUUID(id) 394 const where = buildWhereIdOrUUID(id)
386 395
387 const query = { 396 const query = {
@@ -394,7 +403,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
394 .findOne(query) 403 .findOne(query)
395 } 404 }
396 405
397 static loadByUrlAndPopulateAccount (url: string) { 406 static loadByUrlAndPopulateAccount (url: string): Bluebird<MVideoPlaylistAccountThumbnail> {
398 const query = { 407 const query = {
399 where: { 408 where: {
400 url 409 url
@@ -423,7 +432,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
423 return VideoPlaylistModel.update({ privacy: VideoPlaylistPrivacy.PRIVATE, videoChannelId: null }, query) 432 return VideoPlaylistModel.update({ privacy: VideoPlaylistPrivacy.PRIVATE, videoChannelId: null }, query)
424 } 433 }
425 434
426 async setAndSaveThumbnail (thumbnail: ThumbnailModel, t: Transaction) { 435 async setAndSaveThumbnail (thumbnail: MThumbnail, t: Transaction) {
427 thumbnail.videoPlaylistId = this.id 436 thumbnail.videoPlaylistId = this.id
428 437
429 this.Thumbnail = await thumbnail.save({ transaction: t }) 438 this.Thumbnail = await thumbnail.save({ transaction: t })
@@ -471,7 +480,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
471 return isOutdated(this, ACTIVITY_PUB.VIDEO_PLAYLIST_REFRESH_INTERVAL) 480 return isOutdated(this, ACTIVITY_PUB.VIDEO_PLAYLIST_REFRESH_INTERVAL)
472 } 481 }
473 482
474 toFormattedJSON (): VideoPlaylist { 483 toFormattedJSON (this: MVideoPlaylistFormattable): VideoPlaylist {
475 return { 484 return {
476 id: this.id, 485 id: this.id,
477 uuid: this.uuid, 486 uuid: this.uuid,
@@ -501,7 +510,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
501 } 510 }
502 } 511 }
503 512
504 toActivityPubObject (page: number, t: Transaction): Promise<PlaylistObject> { 513 toActivityPubObject (this: MVideoPlaylistAP, page: number, t: Transaction): Promise<PlaylistObject> {
505 const handler = (start: number, count: number) => { 514 const handler = (start: number, count: number) => {
506 return VideoPlaylistElementModel.listUrlsOfForAP(this.id, start, count, t) 515 return VideoPlaylistElementModel.listUrlsOfForAP(this.id, start, count, t)
507 } 516 }
diff --git a/server/models/video/video-share.ts b/server/models/video/video-share.ts
index d8ed64557..9019b401a 100644
--- a/server/models/video/video-share.ts
+++ b/server/models/video/video-share.ts
@@ -8,6 +8,8 @@ import { buildLocalActorIdsIn, throwIfNotValid } from '../utils'
8import { VideoModel } from './video' 8import { VideoModel } from './video'
9import { VideoChannelModel } from './video-channel' 9import { VideoChannelModel } from './video-channel'
10import { Op, Transaction } from 'sequelize' 10import { Op, Transaction } from 'sequelize'
11import { MVideoShareActor, MVideoShareFull } from '../../typings/models/video'
12import { MActorDefault } from '../../typings/models'
11 13
12enum ScopeNames { 14enum ScopeNames {
13 FULL = 'FULL', 15 FULL = 'FULL',
@@ -88,7 +90,7 @@ export class VideoShareModel extends Model<VideoShareModel> {
88 }) 90 })
89 Video: VideoModel 91 Video: VideoModel
90 92
91 static load (actorId: number, videoId: number, t?: Transaction) { 93 static load (actorId: number, videoId: number, t?: Transaction): Bluebird<MVideoShareActor> {
92 return VideoShareModel.scope(ScopeNames.WITH_ACTOR).findOne({ 94 return VideoShareModel.scope(ScopeNames.WITH_ACTOR).findOne({
93 where: { 95 where: {
94 actorId, 96 actorId,
@@ -98,7 +100,7 @@ export class VideoShareModel extends Model<VideoShareModel> {
98 }) 100 })
99 } 101 }
100 102
101 static loadByUrl (url: string, t: Transaction) { 103 static loadByUrl (url: string, t: Transaction): Bluebird<MVideoShareFull> {
102 return VideoShareModel.scope(ScopeNames.FULL).findOne({ 104 return VideoShareModel.scope(ScopeNames.FULL).findOne({
103 where: { 105 where: {
104 url 106 url
@@ -107,7 +109,7 @@ export class VideoShareModel extends Model<VideoShareModel> {
107 }) 109 })
108 } 110 }
109 111
110 static loadActorsByShare (videoId: number, t: Transaction) { 112 static loadActorsByShare (videoId: number, t: Transaction): Bluebird<MActorDefault[]> {
111 const query = { 113 const query = {
112 where: { 114 where: {
113 videoId 115 videoId
@@ -122,10 +124,10 @@ export class VideoShareModel extends Model<VideoShareModel> {
122 } 124 }
123 125
124 return VideoShareModel.scope(ScopeNames.FULL).findAll(query) 126 return VideoShareModel.scope(ScopeNames.FULL).findAll(query)
125 .then(res => res.map(r => r.Actor)) 127 .then((res: MVideoShareFull[]) => res.map(r => r.Actor))
126 } 128 }
127 129
128 static loadActorsWhoSharedVideosOf (actorOwnerId: number, t: Transaction): Bluebird<ActorModel[]> { 130 static loadActorsWhoSharedVideosOf (actorOwnerId: number, t: Transaction): Bluebird<MActorDefault[]> {
129 const query = { 131 const query = {
130 attributes: [], 132 attributes: [],
131 include: [ 133 include: [
@@ -163,7 +165,7 @@ export class VideoShareModel extends Model<VideoShareModel> {
163 .then(res => res.map(r => r.Actor)) 165 .then(res => res.map(r => r.Actor))
164 } 166 }
165 167
166 static loadActorsByVideoChannel (videoChannelId: number, t: Transaction): Bluebird<ActorModel[]> { 168 static loadActorsByVideoChannel (videoChannelId: number, t: Transaction): Bluebird<MActorDefault[]> {
167 const query = { 169 const query = {
168 attributes: [], 170 attributes: [],
169 include: [ 171 include: [
diff --git a/server/models/video/video-streaming-playlist.ts b/server/models/video/video-streaming-playlist.ts
index 31dc82c54..0ea90d28c 100644
--- a/server/models/video/video-streaming-playlist.ts
+++ b/server/models/video/video-streaming-playlist.ts
@@ -1,16 +1,16 @@
1import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, HasMany, Is, Model, Table, UpdatedAt, DataType } from 'sequelize-typescript' 1import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
2import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' 2import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
3import { throwIfNotValid } from '../utils' 3import { throwIfNotValid } from '../utils'
4import { VideoModel } from './video' 4import { VideoModel } from './video'
5import { VideoRedundancyModel } from '../redundancy/video-redundancy' 5import { VideoRedundancyModel } from '../redundancy/video-redundancy'
6import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' 6import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
7import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 7import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
8import { CONSTRAINTS_FIELDS, STATIC_PATHS, P2P_MEDIA_LOADER_PEER_VERSION } from '../../initializers/constants' 8import { CONSTRAINTS_FIELDS, P2P_MEDIA_LOADER_PEER_VERSION, STATIC_PATHS } from '../../initializers/constants'
9import { VideoFileModel } from './video-file'
10import { join } from 'path' 9import { join } from 'path'
11import { sha1 } from '../../helpers/core-utils' 10import { sha1 } from '../../helpers/core-utils'
12import { isArrayOf } from '../../helpers/custom-validators/misc' 11import { isArrayOf } from '../../helpers/custom-validators/misc'
13import { QueryTypes, Op } from 'sequelize' 12import { Op, QueryTypes } from 'sequelize'
13import { MStreamingPlaylist, MVideoFile } from '@server/typings/models'
14 14
15@Table({ 15@Table({
16 tableName: 'videoStreamingPlaylist', 16 tableName: 'videoStreamingPlaylist',
@@ -91,7 +91,7 @@ export class VideoStreamingPlaylistModel extends Model<VideoStreamingPlaylistMod
91 .then(results => results.length === 1) 91 .then(results => results.length === 1)
92 } 92 }
93 93
94 static buildP2PMediaLoaderInfoHashes (playlistUrl: string, videoFiles: VideoFileModel[]) { 94 static buildP2PMediaLoaderInfoHashes (playlistUrl: string, videoFiles: MVideoFile[]) {
95 const hashes: string[] = [] 95 const hashes: string[] = []
96 96
97 // https://github.com/Novage/p2p-media-loader/blob/master/p2p-media-loader-core/lib/p2p-media-manager.ts#L115 97 // https://github.com/Novage/p2p-media-loader/blob/master/p2p-media-loader-core/lib/p2p-media-manager.ts#L115
@@ -165,7 +165,7 @@ export class VideoStreamingPlaylistModel extends Model<VideoStreamingPlaylistMod
165 return baseUrlHttp + STATIC_PATHS.REDUNDANCY + this.getStringType() + '/' + this.Video.uuid 165 return baseUrlHttp + STATIC_PATHS.REDUNDANCY + this.getStringType() + '/' + this.Video.uuid
166 } 166 }
167 167
168 hasSameUniqueKeysThan (other: VideoStreamingPlaylistModel) { 168 hasSameUniqueKeysThan (other: MStreamingPlaylist) {
169 return this.type === other.type && 169 return this.type === other.type &&
170 this.videoId === other.videoId 170 this.videoId === other.videoId
171 } 171 }
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index b59df397d..6856dcd9f 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -36,7 +36,7 @@ import {
36 Table, 36 Table,
37 UpdatedAt 37 UpdatedAt
38} from 'sequelize-typescript' 38} from 'sequelize-typescript'
39import { UserRight, VideoPrivacy, VideoResolution, VideoState } from '../../../shared' 39import { UserRight, VideoPrivacy, VideoState } from '../../../shared'
40import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' 40import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
41import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' 41import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos'
42import { VideoFilter } from '../../../shared/models/videos/video-query.type' 42import { VideoFilter } from '../../../shared/models/videos/video-query.type'
@@ -111,7 +111,6 @@ import {
111 videoModelToFormattedJSON 111 videoModelToFormattedJSON
112} from './video-format-utils' 112} from './video-format-utils'
113import { UserVideoHistoryModel } from '../account/user-video-history' 113import { UserVideoHistoryModel } from '../account/user-video-history'
114import { UserModel } from '../account/user'
115import { VideoImportModel } from './video-import' 114import { VideoImportModel } from './video-import'
116import { VideoStreamingPlaylistModel } from './video-streaming-playlist' 115import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
117import { VideoPlaylistElementModel } from './video-playlist-element' 116import { VideoPlaylistElementModel } from './video-playlist-element'
@@ -120,6 +119,29 @@ import { ThumbnailModel } from './thumbnail'
120import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' 119import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
121import { createTorrentPromise } from '../../helpers/webtorrent' 120import { createTorrentPromise } from '../../helpers/webtorrent'
122import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' 121import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
122import {
123 MChannel,
124 MChannelAccountDefault,
125 MChannelId,
126 MUserAccountId,
127 MUserId,
128 MVideoAccountLight,
129 MVideoAccountLightBlacklistAllFiles,
130 MVideoAP,
131 MVideoDetails,
132 MVideoFormattable,
133 MVideoFormattableDetails,
134 MVideoForUser,
135 MVideoFullLight,
136 MVideoIdThumbnail,
137 MVideoThumbnail,
138 MVideoThumbnailBlacklist,
139 MVideoWithAllFiles,
140 MVideoWithFile,
141 MVideoWithRights
142} from '../../typings/models'
143import { MVideoFile, MVideoFileRedundanciesOpt } from '../../typings/models/video/video-file'
144import { MThumbnail } from '../../typings/models/video/thumbnail'
123 145
124// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation 146// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
125const indexes: (ModelIndexesOptions & { where?: WhereOptions })[] = [ 147const indexes: (ModelIndexesOptions & { where?: WhereOptions })[] = [
@@ -232,8 +254,8 @@ export type AvailableForListIDsOptions = {
232 videoPlaylistId?: number 254 videoPlaylistId?: number
233 255
234 trendingDays?: number 256 trendingDays?: number
235 user?: UserModel, 257 user?: MUserAccountId
236 historyOfUser?: UserModel 258 historyOfUser?: MUserId
237 259
238 baseWhere?: WhereOptions[] 260 baseWhere?: WhereOptions[]
239} 261}
@@ -446,13 +468,15 @@ export type AvailableForListIDsOptions = {
446 // FIXME: issues with sequelize count when making a join on n:m relation, so we just make a IN() 468 // FIXME: issues with sequelize count when making a join on n:m relation, so we just make a IN()
447 if (options.tagsAllOf || options.tagsOneOf) { 469 if (options.tagsAllOf || options.tagsOneOf) {
448 if (options.tagsOneOf) { 470 if (options.tagsOneOf) {
471 const tagsOneOfLower = options.tagsOneOf.map(t => t.toLowerCase())
472
449 whereAnd.push({ 473 whereAnd.push({
450 id: { 474 id: {
451 [ Op.in ]: Sequelize.literal( 475 [ Op.in ]: Sequelize.literal(
452 '(' + 476 '(' +
453 'SELECT "videoId" FROM "videoTag" ' + 477 'SELECT "videoId" FROM "videoTag" ' +
454 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' + 478 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' +
455 'WHERE "tag"."name" IN (' + createSafeIn(VideoModel, options.tagsOneOf) + ')' + 479 'WHERE lower("tag"."name") IN (' + createSafeIn(VideoModel, tagsOneOfLower) + ')' +
456 ')' 480 ')'
457 ) 481 )
458 } 482 }
@@ -460,14 +484,16 @@ export type AvailableForListIDsOptions = {
460 } 484 }
461 485
462 if (options.tagsAllOf) { 486 if (options.tagsAllOf) {
487 const tagsAllOfLower = options.tagsAllOf.map(t => t.toLowerCase())
488
463 whereAnd.push({ 489 whereAnd.push({
464 id: { 490 id: {
465 [ Op.in ]: Sequelize.literal( 491 [ Op.in ]: Sequelize.literal(
466 '(' + 492 '(' +
467 'SELECT "videoId" FROM "videoTag" ' + 493 'SELECT "videoId" FROM "videoTag" ' +
468 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' + 494 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' +
469 'WHERE "tag"."name" IN (' + createSafeIn(VideoModel, options.tagsAllOf) + ')' + 495 'WHERE lower("tag"."name") IN (' + createSafeIn(VideoModel, tagsAllOfLower) + ')' +
470 'GROUP BY "videoTag"."videoId" HAVING COUNT(*) = ' + options.tagsAllOf.length + 496 'GROUP BY "videoTag"."videoId" HAVING COUNT(*) = ' + tagsAllOfLower.length +
471 ')' 497 ')'
472 ) 498 )
473 } 499 }
@@ -634,7 +660,7 @@ export type AvailableForListIDsOptions = {
634 [ ScopeNames.WITH_BLACKLISTED ]: { 660 [ ScopeNames.WITH_BLACKLISTED ]: {
635 include: [ 661 include: [
636 { 662 {
637 attributes: [ 'id', 'reason' ], 663 attributes: [ 'id', 'reason', 'unfederated' ],
638 model: VideoBlacklistModel, 664 model: VideoBlacklistModel,
639 required: false 665 required: false
640 } 666 }
@@ -989,18 +1015,16 @@ export class VideoModel extends Model<VideoModel> {
989 VideoCaptions: VideoCaptionModel[] 1015 VideoCaptions: VideoCaptionModel[]
990 1016
991 @BeforeDestroy 1017 @BeforeDestroy
992 static async sendDelete (instance: VideoModel, options) { 1018 static async sendDelete (instance: MVideoAccountLight, options) {
993 if (instance.isOwned()) { 1019 if (instance.isOwned()) {
994 if (!instance.VideoChannel) { 1020 if (!instance.VideoChannel) {
995 instance.VideoChannel = await instance.$get('VideoChannel', { 1021 instance.VideoChannel = await instance.$get('VideoChannel', {
996 include: [ 1022 include: [
997 { 1023 ActorModel,
998 model: AccountModel, 1024 AccountModel
999 include: [ ActorModel ]
1000 }
1001 ], 1025 ],
1002 transaction: options.transaction 1026 transaction: options.transaction
1003 }) as VideoChannelModel 1027 }) as MChannelAccountDefault
1004 } 1028 }
1005 1029
1006 return sendDeleteVideo(instance, options.transaction) 1030 return sendDeleteVideo(instance, options.transaction)
@@ -1039,7 +1063,7 @@ export class VideoModel extends Model<VideoModel> {
1039 return undefined 1063 return undefined
1040 } 1064 }
1041 1065
1042 static listLocal () { 1066 static listLocal (): Bluebird<MVideoWithAllFiles[]> {
1043 const query = { 1067 const query = {
1044 where: { 1068 where: {
1045 remote: false 1069 remote: false
@@ -1159,7 +1183,7 @@ export class VideoModel extends Model<VideoModel> {
1159 }) 1183 })
1160 } 1184 }
1161 1185
1162 static listUserVideosForApi (accountId: number, start: number, count: number, sort: string, withFiles = false) { 1186 static listUserVideosForApi (accountId: number, start: number, count: number, sort: string) {
1163 function buildBaseQuery (): FindOptions { 1187 function buildBaseQuery (): FindOptions {
1164 return { 1188 return {
1165 offset: start, 1189 offset: start,
@@ -1192,16 +1216,9 @@ export class VideoModel extends Model<VideoModel> {
1192 ScopeNames.WITH_THUMBNAILS 1216 ScopeNames.WITH_THUMBNAILS
1193 ] 1217 ]
1194 1218
1195 if (withFiles === true) {
1196 findQuery.include.push({
1197 model: VideoFileModel.unscoped(),
1198 required: true
1199 })
1200 }
1201
1202 return Promise.all([ 1219 return Promise.all([
1203 VideoModel.count(countQuery), 1220 VideoModel.count(countQuery),
1204 VideoModel.scope(findScopes).findAll(findQuery) 1221 VideoModel.scope(findScopes).findAll<MVideoForUser>(findQuery)
1205 ]).then(([ count, rows ]) => { 1222 ]).then(([ count, rows ]) => {
1206 return { 1223 return {
1207 data: rows, 1224 data: rows,
@@ -1228,8 +1245,8 @@ export class VideoModel extends Model<VideoModel> {
1228 followerActorId?: number 1245 followerActorId?: number
1229 videoPlaylistId?: number, 1246 videoPlaylistId?: number,
1230 trendingDays?: number, 1247 trendingDays?: number,
1231 user?: UserModel, 1248 user?: MUserAccountId,
1232 historyOfUser?: UserModel 1249 historyOfUser?: MUserId
1233 }, countVideos = true) { 1250 }, countVideos = true) {
1234 if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) { 1251 if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) {
1235 throw new Error('Try to filter all-local but no user has not the see all videos right') 1252 throw new Error('Try to filter all-local but no user has not the see all videos right')
@@ -1294,7 +1311,7 @@ export class VideoModel extends Model<VideoModel> {
1294 tagsAllOf?: string[] 1311 tagsAllOf?: string[]
1295 durationMin?: number // seconds 1312 durationMin?: number // seconds
1296 durationMax?: number // seconds 1313 durationMax?: number // seconds
1297 user?: UserModel, 1314 user?: MUserAccountId,
1298 filter?: VideoFilter 1315 filter?: VideoFilter
1299 }) { 1316 }) {
1300 const whereAnd = [] 1317 const whereAnd = []
@@ -1387,7 +1404,7 @@ export class VideoModel extends Model<VideoModel> {
1387 return VideoModel.getAvailableForApi(query, queryOptions) 1404 return VideoModel.getAvailableForApi(query, queryOptions)
1388 } 1405 }
1389 1406
1390 static load (id: number | string, t?: Transaction) { 1407 static load (id: number | string, t?: Transaction): Bluebird<MVideoThumbnail> {
1391 const where = buildWhereIdOrUUID(id) 1408 const where = buildWhereIdOrUUID(id)
1392 const options = { 1409 const options = {
1393 where, 1410 where,
@@ -1397,7 +1414,20 @@ export class VideoModel extends Model<VideoModel> {
1397 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options) 1414 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
1398 } 1415 }
1399 1416
1400 static loadWithRights (id: number | string, t?: Transaction) { 1417 static loadWithBlacklist (id: number | string, t?: Transaction): Bluebird<MVideoThumbnailBlacklist> {
1418 const where = buildWhereIdOrUUID(id)
1419 const options = {
1420 where,
1421 transaction: t
1422 }
1423
1424 return VideoModel.scope([
1425 ScopeNames.WITH_THUMBNAILS,
1426 ScopeNames.WITH_BLACKLISTED
1427 ]).findOne(options)
1428 }
1429
1430 static loadWithRights (id: number | string, t?: Transaction): Bluebird<MVideoWithRights> {
1401 const where = buildWhereIdOrUUID(id) 1431 const where = buildWhereIdOrUUID(id)
1402 const options = { 1432 const options = {
1403 where, 1433 where,
@@ -1411,7 +1441,7 @@ export class VideoModel extends Model<VideoModel> {
1411 ]).findOne(options) 1441 ]).findOne(options)
1412 } 1442 }
1413 1443
1414 static loadOnlyId (id: number | string, t?: Transaction) { 1444 static loadOnlyId (id: number | string, t?: Transaction): Bluebird<MVideoIdThumbnail> {
1415 const where = buildWhereIdOrUUID(id) 1445 const where = buildWhereIdOrUUID(id)
1416 1446
1417 const options = { 1447 const options = {
@@ -1423,7 +1453,7 @@ export class VideoModel extends Model<VideoModel> {
1423 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options) 1453 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
1424 } 1454 }
1425 1455
1426 static loadWithFiles (id: number | string, t?: Transaction, logging?: boolean) { 1456 static loadWithFiles (id: number | string, t?: Transaction, logging?: boolean): Bluebird<MVideoWithAllFiles> {
1427 const where = buildWhereIdOrUUID(id) 1457 const where = buildWhereIdOrUUID(id)
1428 1458
1429 const query = { 1459 const query = {
@@ -1439,7 +1469,7 @@ export class VideoModel extends Model<VideoModel> {
1439 ]).findOne(query) 1469 ]).findOne(query)
1440 } 1470 }
1441 1471
1442 static loadByUUID (uuid: string) { 1472 static loadByUUID (uuid: string): Bluebird<MVideoThumbnail> {
1443 const options = { 1473 const options = {
1444 where: { 1474 where: {
1445 uuid 1475 uuid
@@ -1449,7 +1479,7 @@ export class VideoModel extends Model<VideoModel> {
1449 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options) 1479 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
1450 } 1480 }
1451 1481
1452 static loadByUrl (url: string, transaction?: Transaction) { 1482 static loadByUrl (url: string, transaction?: Transaction): Bluebird<MVideoThumbnail> {
1453 const query: FindOptions = { 1483 const query: FindOptions = {
1454 where: { 1484 where: {
1455 url 1485 url
@@ -1460,7 +1490,7 @@ export class VideoModel extends Model<VideoModel> {
1460 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(query) 1490 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(query)
1461 } 1491 }
1462 1492
1463 static loadByUrlAndPopulateAccount (url: string, transaction?: Transaction) { 1493 static loadByUrlAndPopulateAccount (url: string, transaction?: Transaction): Bluebird<MVideoAccountLightBlacklistAllFiles> {
1464 const query: FindOptions = { 1494 const query: FindOptions = {
1465 where: { 1495 where: {
1466 url 1496 url
@@ -1472,11 +1502,12 @@ export class VideoModel extends Model<VideoModel> {
1472 ScopeNames.WITH_ACCOUNT_DETAILS, 1502 ScopeNames.WITH_ACCOUNT_DETAILS,
1473 ScopeNames.WITH_FILES, 1503 ScopeNames.WITH_FILES,
1474 ScopeNames.WITH_STREAMING_PLAYLISTS, 1504 ScopeNames.WITH_STREAMING_PLAYLISTS,
1475 ScopeNames.WITH_THUMBNAILS 1505 ScopeNames.WITH_THUMBNAILS,
1506 ScopeNames.WITH_BLACKLISTED
1476 ]).findOne(query) 1507 ]).findOne(query)
1477 } 1508 }
1478 1509
1479 static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Transaction, userId?: number) { 1510 static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Transaction, userId?: number): Bluebird<MVideoFullLight> {
1480 const where = buildWhereIdOrUUID(id) 1511 const where = buildWhereIdOrUUID(id)
1481 1512
1482 const options = { 1513 const options = {
@@ -1508,7 +1539,7 @@ export class VideoModel extends Model<VideoModel> {
1508 id: number | string, 1539 id: number | string,
1509 t?: Transaction, 1540 t?: Transaction,
1510 userId?: number 1541 userId?: number
1511 }) { 1542 }): Bluebird<MVideoDetails> {
1512 const { id, t, userId } = parameters 1543 const { id, t, userId } = parameters
1513 const where = buildWhereIdOrUUID(id) 1544 const where = buildWhereIdOrUUID(id)
1514 1545
@@ -1586,7 +1617,7 @@ export class VideoModel extends Model<VideoModel> {
1586 .then(results => results.length === 1) 1617 .then(results => results.length === 1)
1587 } 1618 }
1588 1619
1589 static bulkUpdateSupportField (videoChannel: VideoChannelModel, t: Transaction) { 1620 static bulkUpdateSupportField (videoChannel: MChannel, t: Transaction) {
1590 const options = { 1621 const options = {
1591 where: { 1622 where: {
1592 channelId: videoChannel.id 1623 channelId: videoChannel.id
@@ -1597,7 +1628,7 @@ export class VideoModel extends Model<VideoModel> {
1597 return VideoModel.update({ support: videoChannel.support }, options) 1628 return VideoModel.update({ support: videoChannel.support }, options)
1598 } 1629 }
1599 1630
1600 static getAllIdsFromChannel (videoChannel: VideoChannelModel) { 1631 static getAllIdsFromChannel (videoChannel: MChannelId): Bluebird<number[]> {
1601 const query = { 1632 const query = {
1602 attributes: [ 'id' ], 1633 attributes: [ 'id' ],
1603 where: { 1634 where: {
@@ -1756,20 +1787,20 @@ export class VideoModel extends Model<VideoModel> {
1756 this.VideoChannel.Account.isBlocked() 1787 this.VideoChannel.Account.isBlocked()
1757 } 1788 }
1758 1789
1759 getOriginalFile () { 1790 getOriginalFile <T extends MVideoWithFile> (this: T) {
1760 if (Array.isArray(this.VideoFiles) === false) return undefined 1791 if (Array.isArray(this.VideoFiles) === false) return undefined
1761 1792
1762 // The original file is the file that have the higher resolution 1793 // The original file is the file that have the higher resolution
1763 return maxBy(this.VideoFiles, file => file.resolution) 1794 return maxBy(this.VideoFiles, file => file.resolution)
1764 } 1795 }
1765 1796
1766 getFile (resolution: number) { 1797 getFile <T extends MVideoWithFile> (this: T, resolution: number) {
1767 if (Array.isArray(this.VideoFiles) === false) return undefined 1798 if (Array.isArray(this.VideoFiles) === false) return undefined
1768 1799
1769 return this.VideoFiles.find(f => f.resolution === resolution) 1800 return this.VideoFiles.find(f => f.resolution === resolution)
1770 } 1801 }
1771 1802
1772 async addAndSaveThumbnail (thumbnail: ThumbnailModel, transaction: Transaction) { 1803 async addAndSaveThumbnail (thumbnail: MThumbnail, transaction: Transaction) {
1773 thumbnail.videoId = this.id 1804 thumbnail.videoId = this.id
1774 1805
1775 const savedThumbnail = await thumbnail.save({ transaction }) 1806 const savedThumbnail = await thumbnail.save({ transaction })
@@ -1782,7 +1813,7 @@ export class VideoModel extends Model<VideoModel> {
1782 this.Thumbnails.push(savedThumbnail) 1813 this.Thumbnails.push(savedThumbnail)
1783 } 1814 }
1784 1815
1785 getVideoFilename (videoFile: VideoFileModel) { 1816 getVideoFilename (videoFile: MVideoFile) {
1786 return this.uuid + '-' + videoFile.resolution + videoFile.extname 1817 return this.uuid + '-' + videoFile.resolution + videoFile.extname
1787 } 1818 }
1788 1819
@@ -1806,7 +1837,7 @@ export class VideoModel extends Model<VideoModel> {
1806 return this.Thumbnails.find(t => t.type === ThumbnailType.PREVIEW) 1837 return this.Thumbnails.find(t => t.type === ThumbnailType.PREVIEW)
1807 } 1838 }
1808 1839
1809 getTorrentFileName (videoFile: VideoFileModel) { 1840 getTorrentFileName (videoFile: MVideoFile) {
1810 const extension = '.torrent' 1841 const extension = '.torrent'
1811 return this.uuid + '-' + videoFile.resolution + extension 1842 return this.uuid + '-' + videoFile.resolution + extension
1812 } 1843 }
@@ -1815,15 +1846,15 @@ export class VideoModel extends Model<VideoModel> {
1815 return this.remote === false 1846 return this.remote === false
1816 } 1847 }
1817 1848
1818 getTorrentFilePath (videoFile: VideoFileModel) { 1849 getTorrentFilePath (videoFile: MVideoFile) {
1819 return join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile)) 1850 return join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
1820 } 1851 }
1821 1852
1822 getVideoFilePath (videoFile: VideoFileModel) { 1853 getVideoFilePath (videoFile: MVideoFile) {
1823 return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile)) 1854 return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
1824 } 1855 }
1825 1856
1826 async createTorrentAndSetInfoHash (videoFile: VideoFileModel) { 1857 async createTorrentAndSetInfoHash (videoFile: MVideoFile) {
1827 const options = { 1858 const options = {
1828 // Keep the extname, it's used by the client to stream the file inside a web browser 1859 // Keep the extname, it's used by the client to stream the file inside a web browser
1829 name: `${this.name} ${videoFile.resolution}p${videoFile.extname}`, 1860 name: `${this.name} ${videoFile.resolution}p${videoFile.extname}`,
@@ -1869,11 +1900,11 @@ export class VideoModel extends Model<VideoModel> {
1869 return join(LAZY_STATIC_PATHS.PREVIEWS, preview.filename) 1900 return join(LAZY_STATIC_PATHS.PREVIEWS, preview.filename)
1870 } 1901 }
1871 1902
1872 toFormattedJSON (options?: VideoFormattingJSONOptions): Video { 1903 toFormattedJSON (this: MVideoFormattable, options?: VideoFormattingJSONOptions): Video {
1873 return videoModelToFormattedJSON(this, options) 1904 return videoModelToFormattedJSON(this, options)
1874 } 1905 }
1875 1906
1876 toFormattedDetailsJSON (): VideoDetails { 1907 toFormattedDetailsJSON (this: MVideoFormattableDetails): VideoDetails {
1877 return videoModelToFormattedDetailsJSON(this) 1908 return videoModelToFormattedDetailsJSON(this)
1878 } 1909 }
1879 1910
@@ -1881,7 +1912,7 @@ export class VideoModel extends Model<VideoModel> {
1881 return videoFilesModelToFormattedJSON(this, this.VideoFiles) 1912 return videoFilesModelToFormattedJSON(this, this.VideoFiles)
1882 } 1913 }
1883 1914
1884 toActivityPubObject (): VideoTorrentObject { 1915 toActivityPubObject (this: MVideoAP): VideoTorrentObject {
1885 return videoModelToActivityPubObject(this) 1916 return videoModelToActivityPubObject(this)
1886 } 1917 }
1887 1918
@@ -1908,7 +1939,7 @@ export class VideoModel extends Model<VideoModel> {
1908 return this.VideoStreamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) 1939 return this.VideoStreamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
1909 } 1940 }
1910 1941
1911 removeFile (videoFile: VideoFileModel, isRedundancy = false) { 1942 removeFile (videoFile: MVideoFile, isRedundancy = false) {
1912 const baseDir = isRedundancy ? CONFIG.STORAGE.REDUNDANCY_DIR : CONFIG.STORAGE.VIDEOS_DIR 1943 const baseDir = isRedundancy ? CONFIG.STORAGE.REDUNDANCY_DIR : CONFIG.STORAGE.VIDEOS_DIR
1913 1944
1914 const filePath = join(baseDir, this.getVideoFilename(videoFile)) 1945 const filePath = join(baseDir, this.getVideoFilename(videoFile))
@@ -1916,7 +1947,7 @@ export class VideoModel extends Model<VideoModel> {
1916 .catch(err => logger.warn('Cannot delete file %s.', filePath, { err })) 1947 .catch(err => logger.warn('Cannot delete file %s.', filePath, { err }))
1917 } 1948 }
1918 1949
1919 removeTorrent (videoFile: VideoFileModel) { 1950 removeTorrent (videoFile: MVideoFile) {
1920 const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile)) 1951 const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
1921 return remove(torrentPath) 1952 return remove(torrentPath)
1922 .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err })) 1953 .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err }))
@@ -1957,7 +1988,7 @@ export class VideoModel extends Model<VideoModel> {
1957 return { baseUrlHttp, baseUrlWs } 1988 return { baseUrlHttp, baseUrlWs }
1958 } 1989 }
1959 1990
1960 generateMagnetUri (videoFile: VideoFileModel, baseUrlHttp: string, baseUrlWs: string) { 1991 generateMagnetUri (videoFile: MVideoFileRedundanciesOpt, baseUrlHttp: string, baseUrlWs: string) {
1961 const xs = this.getTorrentUrl(videoFile, baseUrlHttp) 1992 const xs = this.getTorrentUrl(videoFile, baseUrlHttp)
1962 const announce = this.getTrackerUrls(baseUrlHttp, baseUrlWs) 1993 const announce = this.getTrackerUrls(baseUrlHttp, baseUrlWs)
1963 let urlList = [ this.getVideoFileUrl(videoFile, baseUrlHttp) ] 1994 let urlList = [ this.getVideoFileUrl(videoFile, baseUrlHttp) ]
@@ -1980,27 +2011,27 @@ export class VideoModel extends Model<VideoModel> {
1980 return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ] 2011 return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
1981 } 2012 }
1982 2013
1983 getTorrentUrl (videoFile: VideoFileModel, baseUrlHttp: string) { 2014 getTorrentUrl (videoFile: MVideoFile, baseUrlHttp: string) {
1984 return baseUrlHttp + STATIC_PATHS.TORRENTS + this.getTorrentFileName(videoFile) 2015 return baseUrlHttp + STATIC_PATHS.TORRENTS + this.getTorrentFileName(videoFile)
1985 } 2016 }
1986 2017
1987 getTorrentDownloadUrl (videoFile: VideoFileModel, baseUrlHttp: string) { 2018 getTorrentDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
1988 return baseUrlHttp + STATIC_DOWNLOAD_PATHS.TORRENTS + this.getTorrentFileName(videoFile) 2019 return baseUrlHttp + STATIC_DOWNLOAD_PATHS.TORRENTS + this.getTorrentFileName(videoFile)
1989 } 2020 }
1990 2021
1991 getVideoFileUrl (videoFile: VideoFileModel, baseUrlHttp: string) { 2022 getVideoFileUrl (videoFile: MVideoFile, baseUrlHttp: string) {
1992 return baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) 2023 return baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile)
1993 } 2024 }
1994 2025
1995 getVideoRedundancyUrl (videoFile: VideoFileModel, baseUrlHttp: string) { 2026 getVideoRedundancyUrl (videoFile: MVideoFile, baseUrlHttp: string) {
1996 return baseUrlHttp + STATIC_PATHS.REDUNDANCY + this.getVideoFilename(videoFile) 2027 return baseUrlHttp + STATIC_PATHS.REDUNDANCY + this.getVideoFilename(videoFile)
1997 } 2028 }
1998 2029
1999 getVideoFileDownloadUrl (videoFile: VideoFileModel, baseUrlHttp: string) { 2030 getVideoFileDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
2000 return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + this.getVideoFilename(videoFile) 2031 return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + this.getVideoFilename(videoFile)
2001 } 2032 }
2002 2033
2003 getBandwidthBits (videoFile: VideoFileModel) { 2034 getBandwidthBits (videoFile: MVideoFile) {
2004 return Math.ceil((videoFile.size * 8) / this.duration) 2035 return Math.ceil((videoFile.size * 8) / this.duration)
2005 } 2036 }
2006} 2037}
diff --git a/server/tests/api/activitypub/helpers.ts b/server/tests/api/activitypub/helpers.ts
index 365d0e1ae..0d1f154fe 100644
--- a/server/tests/api/activitypub/helpers.ts
+++ b/server/tests/api/activitypub/helpers.ts
@@ -53,19 +53,6 @@ describe('Test activity pub helpers', function () {
53 expect(result).to.be.false 53 expect(result).to.be.false
54 }) 54 })
55 55
56 it('Should fail with an invalid PeerTube URL', async function () {
57 const keys = require('./json/peertube/keys.json')
58 const body = require('./json/peertube/announce-without-context.json')
59
60 const actorSignature = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey }
61 const signedBody = await buildSignedActivity(actorSignature as any, body)
62
63 const fromActor = { publicKey: keys.publicKey, url: 'http://localhost:9003/accounts/peertube' }
64 const result = await isJsonLDSignatureVerified(fromActor as any, signedBody)
65
66 expect(result).to.be.false
67 })
68
69 it('Should succeed with a valid PeerTube signature', async function () { 56 it('Should succeed with a valid PeerTube signature', async function () {
70 const keys = require('./json/peertube/keys.json') 57 const keys = require('./json/peertube/keys.json')
71 const body = require('./json/peertube/announce-without-context.json') 58 const body = require('./json/peertube/announce-without-context.json')
diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts
index 7773ae1e7..9435bb1e8 100644
--- a/server/tests/api/check-params/config.ts
+++ b/server/tests/api/check-params/config.ts
@@ -5,8 +5,16 @@ import 'mocha'
5import { CustomConfig } from '../../../../shared/models/server/custom-config.model' 5import { CustomConfig } from '../../../../shared/models/server/custom-config.model'
6 6
7import { 7import {
8 createUser, flushTests, killallServers, makeDeleteRequest, makeGetRequest, makePutBodyRequest, flushAndRunServer, ServerInfo, 8 cleanupTests,
9 setAccessTokensToServers, userLogin, immutableAssign, cleanupTests 9 createUser,
10 flushAndRunServer,
11 immutableAssign,
12 makeDeleteRequest,
13 makeGetRequest,
14 makePutBodyRequest,
15 ServerInfo,
16 setAccessTokensToServers,
17 userLogin
10} from '../../../../shared/extra-utils' 18} from '../../../../shared/extra-utils'
11 19
12describe('Test config API validators', function () { 20describe('Test config API validators', function () {
@@ -19,6 +27,18 @@ describe('Test config API validators', function () {
19 shortDescription: 'my short description', 27 shortDescription: 'my short description',
20 description: 'my super description', 28 description: 'my super description',
21 terms: 'my super terms', 29 terms: 'my super terms',
30 codeOfConduct: 'my super coc',
31
32 creationReason: 'my super reason',
33 moderationInformation: 'my super moderation information',
34 administrator: 'Kuja',
35 maintenanceLifetime: 'forever',
36 businessModel: 'my super business model',
37 hardwareInformation: '2vCore 3GB RAM',
38
39 languages: [ 'en', 'es' ],
40 categories: [ 1, 2 ],
41
22 isNSFW: true, 42 isNSFW: true,
23 defaultClientRoute: '/videos/recently-added', 43 defaultClientRoute: '/videos/recently-added',
24 defaultNSFWPolicy: 'blur', 44 defaultNSFWPolicy: 'blur',
@@ -98,6 +118,17 @@ describe('Test config API validators', function () {
98 enabled: false, 118 enabled: false,
99 manualApproval: true 119 manualApproval: true
100 } 120 }
121 },
122 followings: {
123 instance: {
124 autoFollowBack: {
125 enabled: true
126 },
127 autoFollowIndex: {
128 enabled: true,
129 indexUrl: 'https://index.example.com'
130 }
131 }
101 } 132 }
102 } 133 }
103 134
diff --git a/server/tests/api/check-params/user-notifications.ts b/server/tests/api/check-params/user-notifications.ts
index 14ee20d45..3b06be7ef 100644
--- a/server/tests/api/check-params/user-notifications.ts
+++ b/server/tests/api/check-params/user-notifications.ts
@@ -172,7 +172,8 @@ describe('Test user notifications API validators', function () {
172 commentMention: UserNotificationSettingValue.WEB, 172 commentMention: UserNotificationSettingValue.WEB,
173 newFollow: UserNotificationSettingValue.WEB, 173 newFollow: UserNotificationSettingValue.WEB,
174 newUserRegistration: UserNotificationSettingValue.WEB, 174 newUserRegistration: UserNotificationSettingValue.WEB,
175 newInstanceFollower: UserNotificationSettingValue.WEB 175 newInstanceFollower: UserNotificationSettingValue.WEB,
176 autoInstanceFollowing: UserNotificationSettingValue.WEB
176 } 177 }
177 178
178 it('Should fail with missing fields', async function () { 179 it('Should fail with missing fields', async function () {
diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts
index 939b919ed..55094795c 100644
--- a/server/tests/api/check-params/users.ts
+++ b/server/tests/api/check-params/users.ts
@@ -476,6 +476,22 @@ describe('Test users API validators', function () {
476 await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields }) 476 await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
477 }) 477 })
478 478
479 it('Should fail with an invalid noInstanceConfigWarningModal attribute', async function () {
480 const fields = {
481 noInstanceConfigWarningModal: -1
482 }
483
484 await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
485 })
486
487 it('Should fail with an invalid noWelcomeModal attribute', async function () {
488 const fields = {
489 noWelcomeModal: -1
490 }
491
492 await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
493 })
494
479 it('Should succeed to change password with the correct params', async function () { 495 it('Should succeed to change password with the correct params', async function () {
480 const fields = { 496 const fields = {
481 currentPassword: 'my super password', 497 currentPassword: 'my super password',
@@ -483,7 +499,9 @@ describe('Test users API validators', function () {
483 nsfwPolicy: 'blur', 499 nsfwPolicy: 'blur',
484 autoPlayVideo: false, 500 autoPlayVideo: false,
485 email: 'super_email@example.com', 501 email: 'super_email@example.com',
486 theme: 'default' 502 theme: 'default',
503 noInstanceConfigWarningModal: true,
504 noWelcomeModal: true
487 } 505 }
488 506
489 await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields, statusCodeExpected: 204 }) 507 await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields, statusCodeExpected: 204 })
diff --git a/server/tests/api/notifications/user-notifications.ts b/server/tests/api/notifications/user-notifications.ts
index 6fa630562..15a34f5aa 100644
--- a/server/tests/api/notifications/user-notifications.ts
+++ b/server/tests/api/notifications/user-notifications.ts
@@ -14,10 +14,13 @@ import {
14 getVideoCommentThreads, 14 getVideoCommentThreads,
15 getVideoThreadComments, 15 getVideoThreadComments,
16 immutableAssign, 16 immutableAssign,
17 MockInstancesIndex,
17 registerUser, 18 registerUser,
18 removeVideoFromBlacklist, 19 removeVideoFromBlacklist,
19 reportVideoAbuse, 20 reportVideoAbuse,
21 unfollow,
20 updateCustomConfig, 22 updateCustomConfig,
23 updateCustomSubConfig,
21 updateMyUser, 24 updateMyUser,
22 updateVideo, 25 updateVideo,
23 updateVideoChannel, 26 updateVideoChannel,
@@ -29,6 +32,7 @@ import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/l
29import { waitJobs } from '../../../../shared/extra-utils/server/jobs' 32import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
30import { getUserNotificationSocket } from '../../../../shared/extra-utils/socket/socket-io' 33import { getUserNotificationSocket } from '../../../../shared/extra-utils/socket/socket-io'
31import { 34import {
35 checkAutoInstanceFollowing,
32 checkCommentMention, 36 checkCommentMention,
33 CheckerBaseParams, 37 CheckerBaseParams,
34 checkMyVideoImportIsFinished, 38 checkMyVideoImportIsFinished,
@@ -108,7 +112,8 @@ describe('Test users notifications', function () {
108 commentMention: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 112 commentMention: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
109 newFollow: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 113 newFollow: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
110 newUserRegistration: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 114 newUserRegistration: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
111 newInstanceFollower: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL 115 newInstanceFollower: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
116 autoInstanceFollowing: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
112 } 117 }
113 118
114 before(async function () { 119 before(async function () {
@@ -873,7 +878,18 @@ describe('Test users notifications', function () {
873 }) 878 })
874 }) 879 })
875 880
876 describe('New instance follower', function () { 881 describe('New instance follows', function () {
882 const instanceIndexServer = new MockInstancesIndex()
883 const config = {
884 followings: {
885 instance: {
886 autoFollowIndex: {
887 indexUrl: 'http://localhost:42100',
888 enabled: true
889 }
890 }
891 }
892 }
877 let baseParams: CheckerBaseParams 893 let baseParams: CheckerBaseParams
878 894
879 before(async () => { 895 before(async () => {
@@ -883,6 +899,9 @@ describe('Test users notifications', function () {
883 socketNotifications: adminNotifications, 899 socketNotifications: adminNotifications,
884 token: servers[0].accessToken 900 token: servers[0].accessToken
885 } 901 }
902
903 await instanceIndexServer.initialize()
904 instanceIndexServer.addInstance(servers[1].host)
886 }) 905 })
887 906
888 it('Should send a notification only to admin when there is a new instance follower', async function () { 907 it('Should send a notification only to admin when there is a new instance follower', async function () {
@@ -897,6 +916,56 @@ describe('Test users notifications', function () {
897 const userOverride = { socketNotifications: userNotifications, token: userAccessToken, check: { web: true, mail: false } } 916 const userOverride = { socketNotifications: userNotifications, token: userAccessToken, check: { web: true, mail: false } }
898 await checkNewInstanceFollower(immutableAssign(baseParams, userOverride), 'localhost:' + servers[2].port, 'absence') 917 await checkNewInstanceFollower(immutableAssign(baseParams, userOverride), 'localhost:' + servers[2].port, 'absence')
899 }) 918 })
919
920 it('Should send a notification on auto follow back', async function () {
921 this.timeout(40000)
922
923 await unfollow(servers[2].url, servers[2].accessToken, servers[0])
924 await waitJobs(servers)
925
926 const config = {
927 followings: {
928 instance: {
929 autoFollowBack: { enabled: true }
930 }
931 }
932 }
933 await updateCustomSubConfig(servers[0].url, servers[0].accessToken, config)
934
935 await follow(servers[2].url, [ servers[0].url ], servers[2].accessToken)
936
937 await waitJobs(servers)
938
939 const followerHost = servers[0].host
940 const followingHost = servers[2].host
941 await checkAutoInstanceFollowing(baseParams, followerHost, followingHost, 'presence')
942
943 const userOverride = { socketNotifications: userNotifications, token: userAccessToken, check: { web: true, mail: false } }
944 await checkAutoInstanceFollowing(immutableAssign(baseParams, userOverride), followerHost, followingHost, 'absence')
945
946 config.followings.instance.autoFollowBack.enabled = false
947 await updateCustomSubConfig(servers[0].url, servers[0].accessToken, config)
948 await unfollow(servers[0].url, servers[0].accessToken, servers[2])
949 await unfollow(servers[2].url, servers[2].accessToken, servers[0])
950 })
951
952 it('Should send a notification on auto instances index follow', async function () {
953 this.timeout(30000)
954 await unfollow(servers[0].url, servers[0].accessToken, servers[1])
955
956 await updateCustomSubConfig(servers[0].url, servers[0].accessToken, config)
957
958 await wait(5000)
959 await waitJobs(servers)
960
961 const followerHost = servers[0].host
962 const followingHost = servers[1].host
963 await checkAutoInstanceFollowing(baseParams, followerHost, followingHost, 'presence')
964
965 config.followings.instance.autoFollowIndex.enabled = false
966 await updateCustomSubConfig(servers[0].url, servers[0].accessToken, config)
967 await unfollow(servers[0].url, servers[0].accessToken, servers[1])
968 })
900 }) 969 })
901 970
902 describe('New actor follow', function () { 971 describe('New actor follow', function () {
diff --git a/server/tests/api/search/search-videos.ts b/server/tests/api/search/search-videos.ts
index c06200ffe..a3e05156b 100644
--- a/server/tests/api/search/search-videos.ts
+++ b/server/tests/api/search/search-videos.ts
@@ -206,7 +206,7 @@ describe('Test videos search', function () {
206 const query = { 206 const query = {
207 search: '9999', 207 search: '9999',
208 categoryOneOf: [ 1 ], 208 categoryOneOf: [ 1 ],
209 tagsOneOf: [ 'aaaa', 'ffff' ] 209 tagsOneOf: [ 'aAaa', 'ffff' ]
210 } 210 }
211 const res1 = await advancedVideosSearch(server.url, query) 211 const res1 = await advancedVideosSearch(server.url, query)
212 expect(res1.body.total).to.equal(2) 212 expect(res1.body.total).to.equal(2)
@@ -219,15 +219,15 @@ describe('Test videos search', function () {
219 const query = { 219 const query = {
220 search: '9999', 220 search: '9999',
221 categoryOneOf: [ 1 ], 221 categoryOneOf: [ 1 ],
222 tagsAllOf: [ 'cccc' ] 222 tagsAllOf: [ 'CCcc' ]
223 } 223 }
224 const res1 = await advancedVideosSearch(server.url, query) 224 const res1 = await advancedVideosSearch(server.url, query)
225 expect(res1.body.total).to.equal(2) 225 expect(res1.body.total).to.equal(2)
226 226
227 const res2 = await advancedVideosSearch(server.url, immutableAssign(query, { tagsAllOf: [ 'blabla' ] })) 227 const res2 = await advancedVideosSearch(server.url, immutableAssign(query, { tagsAllOf: [ 'blAbla' ] }))
228 expect(res2.body.total).to.equal(0) 228 expect(res2.body.total).to.equal(0)
229 229
230 const res3 = await advancedVideosSearch(server.url, immutableAssign(query, { tagsAllOf: [ 'bbbb', 'cccc' ] })) 230 const res3 = await advancedVideosSearch(server.url, immutableAssign(query, { tagsAllOf: [ 'bbbb', 'CCCC' ] }))
231 expect(res3.body.total).to.equal(1) 231 expect(res3.body.total).to.equal(1)
232 }) 232 })
233 233
diff --git a/server/tests/api/server/auto-follows.ts b/server/tests/api/server/auto-follows.ts
new file mode 100644
index 000000000..df468034c
--- /dev/null
+++ b/server/tests/api/server/auto-follows.ts
@@ -0,0 +1,211 @@
1/* tslint:disable:no-unused-expression */
2
3import * as chai from 'chai'
4import 'mocha'
5import {
6 acceptFollower,
7 cleanupTests,
8 flushAndRunMultipleServers,
9 MockInstancesIndex,
10 ServerInfo,
11 setAccessTokensToServers,
12 unfollow,
13 updateCustomSubConfig,
14 wait
15} from '../../../../shared/extra-utils/index'
16import { follow, getFollowersListPaginationAndSort, getFollowingListPaginationAndSort } from '../../../../shared/extra-utils/server/follows'
17import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
18import { ActorFollow } from '../../../../shared/models/actors'
19
20const expect = chai.expect
21
22async function checkFollow (follower: ServerInfo, following: ServerInfo, exists: boolean) {
23 {
24 const res = await getFollowersListPaginationAndSort(following.url, 0, 5, '-createdAt')
25 const follows = res.body.data as ActorFollow[]
26
27 const follow = follows.find(f => {
28 return f.follower.host === follower.host && f.state === 'accepted'
29 })
30
31 if (exists === true) {
32 expect(follow).to.exist
33 } else {
34 expect(follow).to.be.undefined
35 }
36 }
37
38 {
39 const res = await getFollowingListPaginationAndSort(follower.url, 0, 5, '-createdAt')
40 const follows = res.body.data as ActorFollow[]
41
42 const follow = follows.find(f => {
43 return f.following.host === following.host && f.state === 'accepted'
44 })
45
46 if (exists === true) {
47 expect(follow).to.exist
48 } else {
49 expect(follow).to.be.undefined
50 }
51 }
52}
53
54async function server1Follows2 (servers: ServerInfo[]) {
55 await follow(servers[0].url, [ servers[1].host ], servers[0].accessToken)
56
57 await waitJobs(servers)
58}
59
60async function resetFollows (servers: ServerInfo[]) {
61 try {
62 await unfollow(servers[ 0 ].url, servers[ 0 ].accessToken, servers[ 1 ])
63 await unfollow(servers[ 1 ].url, servers[ 1 ].accessToken, servers[ 0 ])
64 } catch { /* empty */ }
65
66 await waitJobs(servers)
67
68 await checkFollow(servers[0], servers[1], false)
69 await checkFollow(servers[1], servers[0], false)
70}
71
72describe('Test auto follows', function () {
73 let servers: ServerInfo[] = []
74
75 before(async function () {
76 this.timeout(30000)
77
78 servers = await flushAndRunMultipleServers(3)
79
80 // Get the access tokens
81 await setAccessTokensToServers(servers)
82 })
83
84 describe('Auto follow back', function () {
85
86 it('Should not auto follow back if the option is not enabled', async function () {
87 this.timeout(15000)
88
89 await server1Follows2(servers)
90
91 await checkFollow(servers[0], servers[1], true)
92 await checkFollow(servers[1], servers[0], false)
93
94 await resetFollows(servers)
95 })
96
97 it('Should auto follow back on auto accept if the option is enabled', async function () {
98 this.timeout(15000)
99
100 const config = {
101 followings: {
102 instance: {
103 autoFollowBack: { enabled: true }
104 }
105 }
106 }
107 await updateCustomSubConfig(servers[1].url, servers[1].accessToken, config)
108
109 await server1Follows2(servers)
110
111 await checkFollow(servers[0], servers[1], true)
112 await checkFollow(servers[1], servers[0], true)
113
114 await resetFollows(servers)
115 })
116
117 it('Should wait the acceptation before auto follow back', async function () {
118 this.timeout(30000)
119
120 const config = {
121 followings: {
122 instance: {
123 autoFollowBack: { enabled: true }
124 }
125 },
126 followers: {
127 instance: {
128 manualApproval: true
129 }
130 }
131 }
132 await updateCustomSubConfig(servers[1].url, servers[1].accessToken, config)
133
134 await server1Follows2(servers)
135
136 await checkFollow(servers[0], servers[1], false)
137 await checkFollow(servers[1], servers[0], false)
138
139 await acceptFollower(servers[1].url, servers[1].accessToken, 'peertube@' + servers[0].host)
140 await waitJobs(servers)
141
142 await checkFollow(servers[0], servers[1], true)
143 await checkFollow(servers[1], servers[0], true)
144
145 await resetFollows(servers)
146
147 config.followings.instance.autoFollowBack.enabled = false
148 config.followers.instance.manualApproval = false
149 await updateCustomSubConfig(servers[1].url, servers[1].accessToken, config)
150 })
151 })
152
153 describe('Auto follow index', function () {
154 const instanceIndexServer = new MockInstancesIndex()
155
156 before(async () => {
157 await instanceIndexServer.initialize()
158 })
159
160 it('Should not auto follow index if the option is not enabled', async function () {
161 this.timeout(30000)
162
163 await wait(5000)
164 await waitJobs(servers)
165
166 await checkFollow(servers[ 0 ], servers[ 1 ], false)
167 await checkFollow(servers[ 1 ], servers[ 0 ], false)
168 })
169
170 it('Should auto follow the index', async function () {
171 this.timeout(30000)
172
173 instanceIndexServer.addInstance(servers[1].host)
174
175 const config = {
176 followings: {
177 instance: {
178 autoFollowIndex: {
179 indexUrl: 'http://localhost:42100',
180 enabled: true
181 }
182 }
183 }
184 }
185 await updateCustomSubConfig(servers[0].url, servers[0].accessToken, config)
186
187 await wait(5000)
188 await waitJobs(servers)
189
190 await checkFollow(servers[ 0 ], servers[ 1 ], true)
191
192 await resetFollows(servers)
193 })
194
195 it('Should follow new added instances in the index but not old ones', async function () {
196 this.timeout(30000)
197
198 instanceIndexServer.addInstance(servers[2].host)
199
200 await wait(5000)
201 await waitJobs(servers)
202
203 await checkFollow(servers[ 0 ], servers[ 1 ], false)
204 await checkFollow(servers[ 0 ], servers[ 2 ], true)
205 })
206 })
207
208 after(async function () {
209 await cleanupTests(servers)
210 })
211})
diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts
index 78fdc9cc0..97cc99eea 100644
--- a/server/tests/api/server/config.ts
+++ b/server/tests/api/server/config.ts
@@ -28,7 +28,19 @@ function checkInitialConfig (server: ServerInfo, data: CustomConfig) {
28 'with WebTorrent and Angular.' 28 'with WebTorrent and Angular.'
29 ) 29 )
30 expect(data.instance.description).to.equal('Welcome to this PeerTube instance!') 30 expect(data.instance.description).to.equal('Welcome to this PeerTube instance!')
31
31 expect(data.instance.terms).to.equal('No terms for now.') 32 expect(data.instance.terms).to.equal('No terms for now.')
33 expect(data.instance.creationReason).to.be.empty
34 expect(data.instance.codeOfConduct).to.be.empty
35 expect(data.instance.moderationInformation).to.be.empty
36 expect(data.instance.administrator).to.be.empty
37 expect(data.instance.maintenanceLifetime).to.be.empty
38 expect(data.instance.businessModel).to.be.empty
39 expect(data.instance.hardwareInformation).to.be.empty
40
41 expect(data.instance.languages).to.have.lengthOf(0)
42 expect(data.instance.categories).to.have.lengthOf(0)
43
32 expect(data.instance.defaultClientRoute).to.equal('/videos/trending') 44 expect(data.instance.defaultClientRoute).to.equal('/videos/trending')
33 expect(data.instance.isNSFW).to.be.false 45 expect(data.instance.isNSFW).to.be.false
34 expect(data.instance.defaultNSFWPolicy).to.equal('display') 46 expect(data.instance.defaultNSFWPolicy).to.equal('display')
@@ -68,13 +80,29 @@ function checkInitialConfig (server: ServerInfo, data: CustomConfig) {
68 80
69 expect(data.followers.instance.enabled).to.be.true 81 expect(data.followers.instance.enabled).to.be.true
70 expect(data.followers.instance.manualApproval).to.be.false 82 expect(data.followers.instance.manualApproval).to.be.false
83
84 expect(data.followings.instance.autoFollowBack.enabled).to.be.false
85 expect(data.followings.instance.autoFollowIndex.enabled).to.be.false
86 expect(data.followings.instance.autoFollowIndex.indexUrl).to.equal('https://instances.joinpeertube.org')
71} 87}
72 88
73function checkUpdatedConfig (data: CustomConfig) { 89function checkUpdatedConfig (data: CustomConfig) {
74 expect(data.instance.name).to.equal('PeerTube updated') 90 expect(data.instance.name).to.equal('PeerTube updated')
75 expect(data.instance.shortDescription).to.equal('my short description') 91 expect(data.instance.shortDescription).to.equal('my short description')
76 expect(data.instance.description).to.equal('my super description') 92 expect(data.instance.description).to.equal('my super description')
93
77 expect(data.instance.terms).to.equal('my super terms') 94 expect(data.instance.terms).to.equal('my super terms')
95 expect(data.instance.creationReason).to.equal('my super creation reason')
96 expect(data.instance.codeOfConduct).to.equal('my super coc')
97 expect(data.instance.moderationInformation).to.equal('my super moderation information')
98 expect(data.instance.administrator).to.equal('Kuja')
99 expect(data.instance.maintenanceLifetime).to.equal('forever')
100 expect(data.instance.businessModel).to.equal('my super business model')
101 expect(data.instance.hardwareInformation).to.equal('2vCore 3GB RAM')
102
103 expect(data.instance.languages).to.deep.equal([ 'en', 'es' ])
104 expect(data.instance.categories).to.deep.equal([ 1, 2 ])
105
78 expect(data.instance.defaultClientRoute).to.equal('/videos/recently-added') 106 expect(data.instance.defaultClientRoute).to.equal('/videos/recently-added')
79 expect(data.instance.isNSFW).to.be.true 107 expect(data.instance.isNSFW).to.be.true
80 expect(data.instance.defaultNSFWPolicy).to.equal('blur') 108 expect(data.instance.defaultNSFWPolicy).to.equal('blur')
@@ -119,6 +147,10 @@ function checkUpdatedConfig (data: CustomConfig) {
119 147
120 expect(data.followers.instance.enabled).to.be.false 148 expect(data.followers.instance.enabled).to.be.false
121 expect(data.followers.instance.manualApproval).to.be.true 149 expect(data.followers.instance.manualApproval).to.be.true
150
151 expect(data.followings.instance.autoFollowBack.enabled).to.be.true
152 expect(data.followings.instance.autoFollowIndex.enabled).to.be.true
153 expect(data.followings.instance.autoFollowIndex.indexUrl).to.equal('https://updated.example.com')
122} 154}
123 155
124describe('Test config', function () { 156describe('Test config', function () {
@@ -182,6 +214,18 @@ describe('Test config', function () {
182 shortDescription: 'my short description', 214 shortDescription: 'my short description',
183 description: 'my super description', 215 description: 'my super description',
184 terms: 'my super terms', 216 terms: 'my super terms',
217 codeOfConduct: 'my super coc',
218
219 creationReason: 'my super creation reason',
220 moderationInformation: 'my super moderation information',
221 administrator: 'Kuja',
222 maintenanceLifetime: 'forever',
223 businessModel: 'my super business model',
224 hardwareInformation: '2vCore 3GB RAM',
225
226 languages: [ 'en', 'es' ],
227 categories: [ 1, 2 ],
228
185 defaultClientRoute: '/videos/recently-added', 229 defaultClientRoute: '/videos/recently-added',
186 isNSFW: true, 230 isNSFW: true,
187 defaultNSFWPolicy: 'blur' as 'blur', 231 defaultNSFWPolicy: 'blur' as 'blur',
@@ -261,6 +305,17 @@ describe('Test config', function () {
261 enabled: false, 305 enabled: false,
262 manualApproval: true 306 manualApproval: true
263 } 307 }
308 },
309 followings: {
310 instance: {
311 autoFollowBack: {
312 enabled: true
313 },
314 autoFollowIndex: {
315 enabled: true,
316 indexUrl: 'https://updated.example.com'
317 }
318 }
264 } 319 }
265 } 320 }
266 await updateCustomConfig(server.url, server.accessToken, newCustomConfig) 321 await updateCustomConfig(server.url, server.accessToken, newCustomConfig)
@@ -310,6 +365,17 @@ describe('Test config', function () {
310 expect(data.instance.shortDescription).to.equal('my short description') 365 expect(data.instance.shortDescription).to.equal('my short description')
311 expect(data.instance.description).to.equal('my super description') 366 expect(data.instance.description).to.equal('my super description')
312 expect(data.instance.terms).to.equal('my super terms') 367 expect(data.instance.terms).to.equal('my super terms')
368 expect(data.instance.codeOfConduct).to.equal('my super coc')
369
370 expect(data.instance.creationReason).to.equal('my super creation reason')
371 expect(data.instance.moderationInformation).to.equal('my super moderation information')
372 expect(data.instance.administrator).to.equal('Kuja')
373 expect(data.instance.maintenanceLifetime).to.equal('forever')
374 expect(data.instance.businessModel).to.equal('my super business model')
375 expect(data.instance.hardwareInformation).to.equal('2vCore 3GB RAM')
376
377 expect(data.instance.languages).to.deep.equal([ 'en', 'es' ])
378 expect(data.instance.categories).to.deep.equal([ 1, 2 ])
313 }) 379 })
314 380
315 it('Should remove the custom configuration', async function () { 381 it('Should remove the custom configuration', async function () {
diff --git a/server/tests/api/server/index.ts b/server/tests/api/server/index.ts
index 3daeeb49a..08205b2c8 100644
--- a/server/tests/api/server/index.ts
+++ b/server/tests/api/server/index.ts
@@ -1,3 +1,4 @@
1import './auto-follows'
1import './config' 2import './config'
2import './contact-form' 3import './contact-form'
3import './email' 4import './email'
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts
index 3a3fabb4c..95b1bb626 100644
--- a/server/tests/api/users/users.ts
+++ b/server/tests/api/users/users.ts
@@ -442,7 +442,7 @@ describe('Test users', function () {
442 url: server.url, 442 url: server.url,
443 accessToken: accessTokenUser, 443 accessToken: accessTokenUser,
444 currentPassword: 'super password', 444 currentPassword: 'super password',
445 newPassword: 'new password' 445 password: 'new password'
446 }) 446 })
447 user.password = 'new password' 447 user.password = 'new password'
448 448
@@ -543,7 +543,7 @@ describe('Test users', function () {
543 }) 543 })
544 544
545 const res = await getMyUserInformation(server.url, accessTokenUser) 545 const res = await getMyUserInformation(server.url, accessTokenUser)
546 const user = res.body 546 const user: User = res.body
547 547
548 expect(user.username).to.equal('user_1') 548 expect(user.username).to.equal('user_1')
549 expect(user.email).to.equal('updated@example.com') 549 expect(user.email).to.equal('updated@example.com')
@@ -552,6 +552,8 @@ describe('Test users', function () {
552 expect(user.id).to.be.a('number') 552 expect(user.id).to.be.a('number')
553 expect(user.account.displayName).to.equal('new display name') 553 expect(user.account.displayName).to.equal('new display name')
554 expect(user.account.description).to.equal('my super description updated') 554 expect(user.account.description).to.equal('my super description updated')
555 expect(user.noWelcomeModal).to.be.false
556 expect(user.noInstanceConfigWarningModal).to.be.false
555 }) 557 })
556 558
557 it('Should be able to update my theme', async function () { 559 it('Should be able to update my theme', async function () {
@@ -568,6 +570,21 @@ describe('Test users', function () {
568 expect(body.theme).to.equal(theme) 570 expect(body.theme).to.equal(theme)
569 } 571 }
570 }) 572 })
573
574 it('Should be able to update my modal preferences', async function () {
575 await updateMyUser({
576 url: server.url,
577 accessToken: accessTokenUser,
578 noInstanceConfigWarningModal: true,
579 noWelcomeModal: true
580 })
581
582 const res = await getMyUserInformation(server.url, accessTokenUser)
583 const user: User = res.body
584
585 expect(user.noWelcomeModal).to.be.true
586 expect(user.noInstanceConfigWarningModal).to.be.true
587 })
571 }) 588 })
572 589
573 describe('Updating another user', function () { 590 describe('Updating another user', function () {
diff --git a/server/tests/api/videos/video-abuse.ts b/server/tests/api/videos/video-abuse.ts
index a2f3ee161..0cd6f22c7 100644
--- a/server/tests/api/videos/video-abuse.ts
+++ b/server/tests/api/videos/video-abuse.ts
@@ -17,6 +17,12 @@ import {
17} from '../../../../shared/extra-utils/index' 17} from '../../../../shared/extra-utils/index'
18import { doubleFollow } from '../../../../shared/extra-utils/server/follows' 18import { doubleFollow } from '../../../../shared/extra-utils/server/follows'
19import { waitJobs } from '../../../../shared/extra-utils/server/jobs' 19import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
20import {
21 addAccountToServerBlocklist,
22 addServerToServerBlocklist,
23 removeAccountFromServerBlocklist,
24 removeServerFromServerBlocklist
25} from '../../../../shared/extra-utils/users/blocklist'
20 26
21const expect = chai.expect 27const expect = chai.expect
22 28
@@ -163,13 +169,76 @@ describe('Test video abuses', function () {
163 expect(res.body.data[0].moderationComment).to.equal('It is valid') 169 expect(res.body.data[0].moderationComment).to.equal('It is valid')
164 }) 170 })
165 171
172 it('Should hide video abuses from blocked accounts', async function () {
173 this.timeout(10000)
174
175 {
176 await reportVideoAbuse(servers[1].url, servers[1].accessToken, servers[0].video.uuid, 'will mute this')
177 await waitJobs(servers)
178
179 const res = await getVideoAbusesList(servers[0].url, servers[0].accessToken)
180 expect(res.body.total).to.equal(3)
181 }
182
183 const accountToBlock = 'root@localhost:' + servers[1].port
184
185 {
186 await addAccountToServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, accountToBlock)
187
188 const res = await getVideoAbusesList(servers[ 0 ].url, servers[ 0 ].accessToken)
189 expect(res.body.total).to.equal(2)
190
191 const abuse = res.body.data.find(a => a.reason === 'will mute this')
192 expect(abuse).to.be.undefined
193 }
194
195 {
196 await removeAccountFromServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, accountToBlock)
197
198 const res = await getVideoAbusesList(servers[ 0 ].url, servers[ 0 ].accessToken)
199 expect(res.body.total).to.equal(3)
200 }
201 })
202
203 it('Should hide video abuses from blocked servers', async function () {
204 const serverToBlock = servers[1].host
205
206 {
207 await addServerToServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, servers[1].host)
208
209 const res = await getVideoAbusesList(servers[ 0 ].url, servers[ 0 ].accessToken)
210 expect(res.body.total).to.equal(2)
211
212 const abuse = res.body.data.find(a => a.reason === 'will mute this')
213 expect(abuse).to.be.undefined
214 }
215
216 {
217 await removeServerFromServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, serverToBlock)
218
219 const res = await getVideoAbusesList(servers[ 0 ].url, servers[ 0 ].accessToken)
220 expect(res.body.total).to.equal(3)
221 }
222 })
223
166 it('Should delete the video abuse', async function () { 224 it('Should delete the video abuse', async function () {
225 this.timeout(10000)
226
167 await deleteVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id) 227 await deleteVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id)
168 228
169 const res = await getVideoAbusesList(servers[1].url, servers[1].accessToken) 229 await waitJobs(servers)
170 expect(res.body.total).to.equal(0) 230
171 expect(res.body.data).to.be.an('array') 231 {
172 expect(res.body.data.length).to.equal(0) 232 const res = await getVideoAbusesList(servers[1].url, servers[1].accessToken)
233 expect(res.body.total).to.equal(1)
234 expect(res.body.data.length).to.equal(1)
235 expect(res.body.data[0].id).to.not.equal(abuseServer2.id)
236 }
237
238 {
239 const res = await getVideoAbusesList(servers[0].url, servers[0].accessToken)
240 expect(res.body.total).to.equal(3)
241 }
173 }) 242 })
174 243
175 after(async function () { 244 after(async function () {
diff --git a/server/tests/api/videos/video-change-ownership.ts b/server/tests/api/videos/video-change-ownership.ts
index 3a3add71b..64ee2355a 100644
--- a/server/tests/api/videos/video-change-ownership.ts
+++ b/server/tests/api/videos/video-change-ownership.ts
@@ -191,7 +191,7 @@ describe('Test video change ownership - nominal', function () {
191 await waitJobs(servers) 191 await waitJobs(servers)
192 }) 192 })
193 193
194 it('Should have video channel updated', async function () { 194 it('Should have the channel of the video updated', async function () {
195 for (const server of servers) { 195 for (const server of servers) {
196 const res = await getVideo(server.url, servers[0].video.uuid) 196 const res = await getVideo(server.url, servers[0].video.uuid)
197 197
diff --git a/server/tools/cli.ts b/server/tools/cli.ts
index 8599a270f..58e2445ac 100644
--- a/server/tools/cli.ts
+++ b/server/tools/cli.ts
@@ -5,6 +5,7 @@ import { 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 { Command } from 'commander'
7import { VideoChannel, VideoPrivacy } from '../../shared/models/videos' 7import { VideoChannel, VideoPrivacy } from '../../shared/models/videos'
8import { createLogger, format, transports } from 'winston'
8 9
9let configName = 'PeerTube/CLI' 10let configName = 'PeerTube/CLI'
10if (isTestInstance()) configName += `-${getAppNumber()}` 11if (isTestInstance()) configName += `-${getAppNumber()}`
@@ -119,6 +120,7 @@ function buildCommonVideoOptions (command: Command) {
119 .option('-m, --comments-enabled', 'Enable comments') 120 .option('-m, --comments-enabled', 'Enable comments')
120 .option('-s, --support <support>', 'Video support text') 121 .option('-s, --support <support>', 'Video support text')
121 .option('-w, --wait-transcoding', 'Wait transcoding before publishing the video') 122 .option('-w, --wait-transcoding', 'Wait transcoding before publishing the video')
123 .option('-v, --verbose <verbose>', 'Verbosity, from 0/\'error\' to 4/\'debug\'', 'info')
122} 124}
123 125
124async function buildVideoAttributesFromCommander (url: string, command: Command, defaultAttributes: any = {}) { 126async function buildVideoAttributesFromCommander (url: string, command: Command, defaultAttributes: any = {}) {
@@ -175,11 +177,42 @@ function getServerCredentials (program: any) {
175 }) 177 })
176} 178}
177 179
180function getLogger (logLevel = 'info') {
181 const logLevels = {
182 0: 0,
183 error: 0,
184 1: 1,
185 warn: 1,
186 2: 2,
187 info: 2,
188 3: 3,
189 verbose: 3,
190 4: 4,
191 debug: 4
192 }
193
194 const logger = createLogger({
195 levels: logLevels,
196 format: format.combine(
197 format.splat(),
198 format.simple()
199 ),
200 transports: [
201 new (transports.Console)({
202 level: logLevel
203 })
204 ]
205 })
206
207 return logger
208}
209
178// --------------------------------------------------------------------------- 210// ---------------------------------------------------------------------------
179 211
180export { 212export {
181 version, 213 version,
182 config, 214 config,
215 getLogger,
183 getSettings, 216 getSettings,
184 getNetrc, 217 getNetrc,
185 getRemoteObjectOrDie, 218 getRemoteObjectOrDie,
diff --git a/server/tools/peertube-import-videos.ts b/server/tools/peertube-import-videos.ts
index 0ebfa7442..fcb90cca3 100644
--- a/server/tools/peertube-import-videos.ts
+++ b/server/tools/peertube-import-videos.ts
@@ -8,10 +8,11 @@ import { CONSTRAINTS_FIELDS } from '../initializers/constants'
8import { getClient, getVideoCategories, login, searchVideoWithSort, uploadVideo } from '../../shared/extra-utils/index' 8import { getClient, getVideoCategories, login, searchVideoWithSort, uploadVideo } from '../../shared/extra-utils/index'
9import { truncate } from 'lodash' 9import { truncate } from 'lodash'
10import * as prompt from 'prompt' 10import * as prompt from 'prompt'
11import { accessSync, constants } from 'fs'
11import { remove } from 'fs-extra' 12import { remove } from 'fs-extra'
12import { sha256 } from '../helpers/core-utils' 13import { sha256 } from '../helpers/core-utils'
13import { buildOriginallyPublishedAt, safeGetYoutubeDL } from '../helpers/youtube-dl' 14import { buildOriginallyPublishedAt, safeGetYoutubeDL } from '../helpers/youtube-dl'
14import { buildCommonVideoOptions, buildVideoAttributesFromCommander, getServerCredentials } from './cli' 15import { buildCommonVideoOptions, buildVideoAttributesFromCommander, getServerCredentials, getLogger } from './cli'
15 16
16type UserInfo = { 17type UserInfo = {
17 username: string 18 username: string
@@ -19,7 +20,6 @@ type UserInfo = {
19} 20}
20 21
21const processOptions = { 22const processOptions = {
22 cwd: __dirname,
23 maxBuffer: Infinity 23 maxBuffer: Infinity
24} 24}
25 25
@@ -35,15 +35,23 @@ command
35 .option('--target-url <targetUrl>', 'Video target URL') 35 .option('--target-url <targetUrl>', 'Video target URL')
36 .option('--since <since>', 'Publication date (inclusive) since which the videos can be imported (YYYY-MM-DD)', parseDate) 36 .option('--since <since>', 'Publication date (inclusive) since which the videos can be imported (YYYY-MM-DD)', parseDate)
37 .option('--until <until>', 'Publication date (inclusive) until which the videos can be imported (YYYY-MM-DD)', parseDate) 37 .option('--until <until>', 'Publication date (inclusive) until which the videos can be imported (YYYY-MM-DD)', parseDate)
38 .option('-v, --verbose', 'Verbose mode') 38 .option('--first <first>', 'Process first n elements of returned playlist')
39 .option('--last <last>', 'Process last n elements of returned playlist')
40 .option('-T, --tmpdir <tmpdir>', 'Working directory', __dirname)
39 .parse(process.argv) 41 .parse(process.argv)
40 42
43let log = getLogger(program[ 'verbose' ])
44
41getServerCredentials(command) 45getServerCredentials(command)
42 .then(({ url, username, password }) => { 46 .then(({ url, username, password }) => {
43 if (!program[ 'targetUrl' ]) { 47 if (!program[ 'targetUrl' ]) {
44 console.error('--targetUrl field is required.') 48 exitError('--target-url field is required.')
49 }
45 50
46 process.exit(-1) 51 try {
52 accessSync(program[ 'tmpdir' ], constants.R_OK | constants.W_OK)
53 } catch (e) {
54 exitError('--tmpdir %s: directory does not exist or is not accessible', program[ 'tmpdir' ])
47 } 55 }
48 56
49 removeEndSlashes(url) 57 removeEndSlashes(url)
@@ -53,8 +61,7 @@ getServerCredentials(command)
53 61
54 run(url, user) 62 run(url, user)
55 .catch(err => { 63 .catch(err => {
56 console.error(err) 64 exitError(err)
57 process.exit(-1)
58 }) 65 })
59 }) 66 })
60 67
@@ -68,30 +75,32 @@ async function run (url: string, user: UserInfo) {
68 const options = [ '-j', '--flat-playlist', '--playlist-reverse' ] 75 const options = [ '-j', '--flat-playlist', '--playlist-reverse' ]
69 youtubeDL.getInfo(program[ 'targetUrl' ], options, processOptions, async (err, info) => { 76 youtubeDL.getInfo(program[ 'targetUrl' ], options, processOptions, async (err, info) => {
70 if (err) { 77 if (err) {
71 console.log(err.message) 78 exitError(err.message)
72 process.exit(1)
73 } 79 }
74 80
75 let infoArray: any[] 81 let infoArray: any[]
76 82
77 // Normalize utf8 fields 83 // Normalize utf8 fields
78 if (Array.isArray(info) === true) { 84 infoArray = [].concat(info);
79 infoArray = info.map(i => normalizeObject(i)) 85 if (program[ 'first' ]) {
80 } else { 86 infoArray = infoArray.slice(0, program[ 'first' ])
81 infoArray = [ normalizeObject(info) ] 87 } else if (program[ 'last' ]) {
88 infoArray = infoArray.slice(- program[ 'last' ])
82 } 89 }
83 console.log('Will download and upload %d videos.\n', infoArray.length) 90 infoArray = infoArray.map(i => normalizeObject(i))
91
92 log.info('Will download and upload %d videos.\n', infoArray.length)
84 93
85 for (const info of infoArray) { 94 for (const info of infoArray) {
86 await processVideo({ 95 await processVideo({
87 cwd: processOptions.cwd, 96 cwd: program[ 'tmpdir' ],
88 url, 97 url,
89 user, 98 user,
90 youtubeInfo: info 99 youtubeInfo: info
91 }) 100 })
92 } 101 }
93 102
94 console.log('Video/s for user %s imported: %s', program[ 'username' ], program[ 'targetUrl' ]) 103 log.info('Video/s for user %s imported: %s', user.username, program[ 'targetUrl' ])
95 process.exit(0) 104 process.exit(0)
96 }) 105 })
97} 106}
@@ -105,21 +114,21 @@ function processVideo (parameters: {
105 const { youtubeInfo, cwd, url, user } = parameters 114 const { youtubeInfo, cwd, url, user } = parameters
106 115
107 return new Promise(async res => { 116 return new Promise(async res => {
108 if (program[ 'verbose' ]) console.log('Fetching object.', youtubeInfo) 117 log.debug('Fetching object.', youtubeInfo)
109 118
110 const videoInfo = await fetchObject(youtubeInfo) 119 const videoInfo = await fetchObject(youtubeInfo)
111 if (program[ 'verbose' ]) console.log('Fetched object.', videoInfo) 120 log.debug('Fetched object.', videoInfo)
112 121
113 if (program[ 'since' ]) { 122 if (program[ 'since' ]) {
114 if (buildOriginallyPublishedAt(videoInfo).getTime() < program[ 'since' ].getTime()) { 123 if (buildOriginallyPublishedAt(videoInfo).getTime() < program[ 'since' ].getTime()) {
115 console.log('Video "%s" has been published before "%s", don\'t upload it.\n', 124 log.info('Video "%s" has been published before "%s", don\'t upload it.\n',
116 videoInfo.title, formatDate(program[ 'since' ])); 125 videoInfo.title, formatDate(program[ 'since' ]));
117 return res(); 126 return res();
118 } 127 }
119 } 128 }
120 if (program[ 'until' ]) { 129 if (program[ 'until' ]) {
121 if (buildOriginallyPublishedAt(videoInfo).getTime() > program[ 'until' ].getTime()) { 130 if (buildOriginallyPublishedAt(videoInfo).getTime() > program[ 'until' ].getTime()) {
122 console.log('Video "%s" has been published after "%s", don\'t upload it.\n', 131 log.info('Video "%s" has been published after "%s", don\'t upload it.\n',
123 videoInfo.title, formatDate(program[ 'until' ])); 132 videoInfo.title, formatDate(program[ 'until' ]));
124 return res(); 133 return res();
125 } 134 }
@@ -127,27 +136,27 @@ function processVideo (parameters: {
127 136
128 const result = await searchVideoWithSort(url, videoInfo.title, '-match') 137 const result = await searchVideoWithSort(url, videoInfo.title, '-match')
129 138
130 console.log('############################################################\n') 139 log.info('############################################################\n')
131 140
132 if (result.body.data.find(v => v.name === videoInfo.title)) { 141 if (result.body.data.find(v => v.name === videoInfo.title)) {
133 console.log('Video "%s" already exists, don\'t reupload it.\n', videoInfo.title) 142 log.info('Video "%s" already exists, don\'t reupload it.\n', videoInfo.title)
134 return res() 143 return res()
135 } 144 }
136 145
137 const path = join(cwd, sha256(videoInfo.url) + '.mp4') 146 const path = join(cwd, sha256(videoInfo.url) + '.mp4')
138 147
139 console.log('Downloading video "%s"...', videoInfo.title) 148 log.info('Downloading video "%s"...', videoInfo.title)
140 149
141 const options = [ '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best', '-o', path ] 150 const options = [ '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best', '-o', path ]
142 try { 151 try {
143 const youtubeDL = await safeGetYoutubeDL() 152 const youtubeDL = await safeGetYoutubeDL()
144 youtubeDL.exec(videoInfo.url, options, processOptions, async (err, output) => { 153 youtubeDL.exec(videoInfo.url, options, processOptions, async (err, output) => {
145 if (err) { 154 if (err) {
146 console.error(err) 155 log.error(err)
147 return res() 156 return res()
148 } 157 }
149 158
150 console.log(output.join('\n')) 159 log.info(output.join('\n'))
151 await uploadVideoOnPeerTube({ 160 await uploadVideoOnPeerTube({
152 cwd, 161 cwd,
153 url, 162 url,
@@ -158,7 +167,7 @@ function processVideo (parameters: {
158 return res() 167 return res()
159 }) 168 })
160 } catch (err) { 169 } catch (err) {
161 console.log(err.message) 170 log.error(err.message)
162 return res() 171 return res()
163 } 172 }
164 }) 173 })
@@ -217,7 +226,7 @@ async function uploadVideoOnPeerTube (parameters: {
217 fixture: videoPath 226 fixture: videoPath
218 }) 227 })
219 228
220 console.log('\nUploading on PeerTube video "%s".', videoAttributes.name) 229 log.info('\nUploading on PeerTube video "%s".', videoAttributes.name)
221 230
222 let accessToken = await getAccessTokenOrDie(url, user) 231 let accessToken = await getAccessTokenOrDie(url, user)
223 232
@@ -225,21 +234,20 @@ async function uploadVideoOnPeerTube (parameters: {
225 await uploadVideo(url, accessToken, videoAttributes) 234 await uploadVideo(url, accessToken, videoAttributes)
226 } catch (err) { 235 } catch (err) {
227 if (err.message.indexOf('401') !== -1) { 236 if (err.message.indexOf('401') !== -1) {
228 console.log('Got 401 Unauthorized, token may have expired, renewing token and retry.') 237 log.info('Got 401 Unauthorized, token may have expired, renewing token and retry.')
229 238
230 accessToken = await getAccessTokenOrDie(url, user) 239 accessToken = await getAccessTokenOrDie(url, user)
231 240
232 await uploadVideo(url, accessToken, videoAttributes) 241 await uploadVideo(url, accessToken, videoAttributes)
233 } else { 242 } else {
234 console.log(err.message) 243 exitError(err.message)
235 process.exit(1)
236 } 244 }
237 } 245 }
238 246
239 await remove(videoPath) 247 await remove(videoPath)
240 if (thumbnailfile) await remove(thumbnailfile) 248 if (thumbnailfile) await remove(thumbnailfile)
241 249
242 console.log('Uploaded video "%s"!\n', videoAttributes.name) 250 log.warn('Uploaded video "%s"!\n', videoAttributes.name)
243} 251}
244 252
245/* ---------------------------------------------------------- */ 253/* ---------------------------------------------------------- */
@@ -355,20 +363,17 @@ async function getAccessTokenOrDie (url: string, user: UserInfo) {
355 const res = await login(url, client, user) 363 const res = await login(url, client, user)
356 return res.body.access_token 364 return res.body.access_token
357 } catch (err) { 365 } catch (err) {
358 console.error('Cannot authenticate. Please check your username/password.') 366 exitError('Cannot authenticate. Please check your username/password.')
359 process.exit(-1)
360 } 367 }
361} 368}
362 369
363function parseDate (dateAsStr: string): Date { 370function parseDate (dateAsStr: string): Date {
364 if (!/\d{4}-\d{2}-\d{2}/.test(dateAsStr)) { 371 if (!/\d{4}-\d{2}-\d{2}/.test(dateAsStr)) {
365 console.error(`Invalid date passed: ${dateAsStr}. Expected format: YYYY-MM-DD. See help for usage.`); 372 exitError(`Invalid date passed: ${dateAsStr}. Expected format: YYYY-MM-DD. See help for usage.`);
366 process.exit(-1);
367 } 373 }
368 const date = new Date(dateAsStr); 374 const date = new Date(dateAsStr);
369 if (isNaN(date.getTime())) { 375 if (isNaN(date.getTime())) {
370 console.error(`Invalid date passed: ${dateAsStr}. See help for usage.`); 376 exitError(`Invalid date passed: ${dateAsStr}. See help for usage.`);
371 process.exit(-1);
372 } 377 }
373 return date; 378 return date;
374} 379}
@@ -376,3 +381,9 @@ function parseDate (dateAsStr: string): Date {
376function formatDate (date: Date): string { 381function formatDate (date: Date): string {
377 return date.toISOString().split('T')[0]; 382 return date.toISOString().split('T')[0];
378} 383}
384
385function exitError (message:string, ...meta: any[]) {
386 // use console.error instead of log.error here
387 console.error(message, ...meta)
388 process.exit(-1)
389}
diff --git a/server/typings/activitypub-processor.model.ts b/server/typings/activitypub-processor.model.ts
index 37b2859de..7ed3a65b1 100644
--- a/server/typings/activitypub-processor.model.ts
+++ b/server/typings/activitypub-processor.model.ts
@@ -1,10 +1,9 @@
1import { Activity } from '../../shared/models/activitypub' 1import { Activity } from '../../shared/models/activitypub'
2import { ActorModel } from '../models/activitypub/actor' 2import { MActorDefault, MActorSignature } from './models'
3import { SignatureActorModel } from './models'
4 3
5export type APProcessorOptions<T extends Activity> = { 4export type APProcessorOptions<T extends Activity> = {
6 activity: T 5 activity: T
7 byActor: SignatureActorModel 6 byActor: MActorSignature
8 inboxActor?: ActorModel 7 inboxActor?: MActorDefault
9 fromFetch?: boolean 8 fromFetch?: boolean
10} 9}
diff --git a/server/typings/express.ts b/server/typings/express.ts
index f7da55ab0..3cc7c7632 100644
--- a/server/typings/express.ts
+++ b/server/typings/express.ts
@@ -1,89 +1,103 @@
1import { VideoChannelModel } from '../models/video/video-channel'
2import { VideoPlaylistModel } from '../models/video/video-playlist'
3import { VideoPlaylistElementModel } from '../models/video/video-playlist-element'
4import { UserModel } from '../models/account/user'
5import { VideoModel } from '../models/video/video'
6import { AccountModel } from '../models/account/account'
7import { VideoChangeOwnershipModel } from '../models/video/video-change-ownership'
8import { ActorModel } from '../models/activitypub/actor'
9import { VideoCommentModel } from '../models/video/video-comment'
10import { VideoShareModel } from '../models/video/video-share'
11import { AccountVideoRateModel } from '../models/account/account-video-rate'
12import { ActorFollowModel } from '../models/activitypub/actor-follow'
13import { ServerModel } from '../models/server/server'
14import { VideoFileModel } from '../models/video/video-file'
15import { VideoRedundancyModel } from '../models/redundancy/video-redundancy'
16import { ServerBlocklistModel } from '../models/server/server-blocklist'
17import { AccountBlocklistModel } from '../models/account/account-blocklist'
18import { VideoImportModel } from '../models/video/video-import'
19import { VideoAbuseModel } from '../models/video/video-abuse'
20import { VideoBlacklistModel } from '../models/video/video-blacklist'
21import { VideoCaptionModel } from '../models/video/video-caption'
22import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
23import { RegisteredPlugin } from '../lib/plugins/plugin-manager' 1import { RegisteredPlugin } from '../lib/plugins/plugin-manager'
24import { PluginModel } from '../models/server/plugin' 2import {
25import { SignatureActorModel } from './models' 3 MAccountDefault,
4 MActorAccountChannelId,
5 MActorFollowActorsDefault,
6 MActorFollowActorsDefaultSubscription,
7 MActorFull,
8 MChannelAccountDefault,
9 MComment,
10 MCommentOwnerVideoReply,
11 MUserDefault,
12 MVideoAbuse,
13 MVideoBlacklist,
14 MVideoCaptionVideo,
15 MVideoFullLight,
16 MVideoIdThumbnail,
17 MVideoRedundancyVideo,
18 MVideoShareActor,
19 MVideoThumbnail,
20 MVideoWithRights
21} from './models'
22import { MVideoPlaylistFull, MVideoPlaylistFullSummary } from './models/video/video-playlist'
23import { MVideoImportDefault } from '@server/typings/models/video/video-import'
24import { MAccountBlocklist, MStreamingPlaylist, MVideoFile } from '@server/typings/models'
25import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/typings/models/video/video-playlist-element'
26import { MAccountVideoRateAccountVideo } from '@server/typings/models/video/video-rate'
27import { MVideoChangeOwnershipFull } from './models/video/video-change-ownership'
28import { MPlugin, MServer } from '@server/typings/models/server'
29import { MServerBlocklist } from './models/server/server-blocklist'
30import { MOAuthTokenUser } from '@server/typings/models/oauth/oauth-token'
26 31
27declare module 'express' { 32declare module 'express' {
28 33
29 interface Response { 34 interface Response {
35
30 locals: { 36 locals: {
31 video?: VideoModel 37 videoAll?: MVideoFullLight
32 videoShare?: VideoShareModel 38 onlyVideo?: MVideoThumbnail
33 videoFile?: VideoFileModel 39 onlyVideoWithRights?: MVideoWithRights
40 videoId?: MVideoIdThumbnail
41
42 videoShare?: MVideoShareActor
43
44 videoFile?: MVideoFile
45
46 videoImport?: MVideoImportDefault
47
48 videoBlacklist?: MVideoBlacklist
49
50 videoCaption?: MVideoCaptionVideo
51
52 videoAbuse?: MVideoAbuse
34 53
35 videoImport?: VideoImportModel 54 videoStreamingPlaylist?: MStreamingPlaylist
36 55
37 videoBlacklist?: VideoBlacklistModel 56 videoChannel?: MChannelAccountDefault
38 57
39 videoCaption?: VideoCaptionModel 58 videoPlaylistFull?: MVideoPlaylistFull
59 videoPlaylistSummary?: MVideoPlaylistFullSummary
40 60
41 videoAbuse?: VideoAbuseModel 61 videoPlaylistElement?: MVideoPlaylistElement
62 videoPlaylistElementAP?: MVideoPlaylistElementVideoUrlPlaylistPrivacy
42 63
43 videoStreamingPlaylist?: VideoStreamingPlaylistModel 64 accountVideoRate?: MAccountVideoRateAccountVideo
44 65
45 videoChannel?: VideoChannelModel 66 videoCommentFull?: MCommentOwnerVideoReply
67 videoCommentThread?: MComment
46 68
47 videoPlaylist?: VideoPlaylistModel 69 follow?: MActorFollowActorsDefault
48 videoPlaylistElement?: VideoPlaylistElementModel 70 subscription?: MActorFollowActorsDefaultSubscription
49 71
50 accountVideoRate?: AccountVideoRateModel 72 nextOwner?: MAccountDefault
73 videoChangeOwnership?: MVideoChangeOwnershipFull
51 74
52 videoComment?: VideoCommentModel 75 account?: MAccountDefault
53 videoCommentThread?: VideoCommentModel
54 76
55 follow?: ActorFollowModel 77 actorFull?: MActorFull
56 subscription?: ActorFollowModel
57 78
58 nextOwner?: AccountModel 79 user?: MUserDefault
59 videoChangeOwnership?: VideoChangeOwnershipModel
60 account?: AccountModel
61 actor?: ActorModel
62 user?: UserModel
63 80
64 server?: ServerModel 81 server?: MServer
65 82
66 videoRedundancy?: VideoRedundancyModel 83 videoRedundancy?: MVideoRedundancyVideo
67 84
68 accountBlock?: AccountBlocklistModel 85 accountBlock?: MAccountBlocklist
69 serverBlock?: ServerBlocklistModel 86 serverBlock?: MServerBlocklist
70 87
71 oauth?: { 88 oauth?: {
72 token: { 89 token: MOAuthTokenUser
73 User: UserModel
74 user: UserModel
75 }
76 } 90 }
77 91
78 signature?: { 92 signature?: {
79 actor: SignatureActorModel 93 actor: MActorAccountChannelId
80 } 94 }
81 95
82 authenticated?: boolean 96 authenticated?: boolean
83 97
84 registeredPlugin?: RegisteredPlugin 98 registeredPlugin?: RegisteredPlugin
85 99
86 plugin?: PluginModel 100 plugin?: MPlugin
87 } 101 }
88 } 102 }
89} 103}
diff --git a/server/typings/models/account/account-blocklist.ts b/server/typings/models/account/account-blocklist.ts
new file mode 100644
index 000000000..c9cb55332
--- /dev/null
+++ b/server/typings/models/account/account-blocklist.ts
@@ -0,0 +1,25 @@
1import { AccountBlocklistModel } from '../../../models/account/account-blocklist'
2import { PickWith } from '../../utils'
3import { MAccountDefault, MAccountFormattable } from './account'
4
5type Use<K extends keyof AccountBlocklistModel, M> = PickWith<AccountBlocklistModel, K, M>
6
7// ############################################################################
8
9export type MAccountBlocklist = Omit<AccountBlocklistModel, 'ByAccount' | 'BlockedAccount'>
10
11// ############################################################################
12
13export type MAccountBlocklistId = Pick<AccountBlocklistModel, 'id'>
14
15export type MAccountBlocklistAccounts = MAccountBlocklist &
16 Use<'ByAccount', MAccountDefault> &
17 Use<'BlockedAccount', MAccountDefault>
18
19// ############################################################################
20
21// Format for API or AP object
22
23export type MAccountBlocklistFormattable = Pick<MAccountBlocklist, 'createdAt'> &
24 Use<'ByAccount', MAccountFormattable> &
25 Use<'BlockedAccount', MAccountFormattable>
diff --git a/server/typings/models/account/account.ts b/server/typings/models/account/account.ts
new file mode 100644
index 000000000..ec78fece8
--- /dev/null
+++ b/server/typings/models/account/account.ts
@@ -0,0 +1,95 @@
1import { AccountModel } from '../../../models/account/account'
2import {
3 MActor,
4 MActorAP,
5 MActorAPI,
6 MActorAudience,
7 MActorDefault,
8 MActorDefaultLight,
9 MActorFormattable,
10 MActorId,
11 MActorServer,
12 MActorSummary,
13 MActorSummaryFormattable,
14 MActorUrl
15} from './actor'
16import { FunctionProperties, PickWith } from '../../utils'
17import { MAccountBlocklistId } from './account-blocklist'
18import { MChannelDefault } from '@server/typings/models'
19
20type Use<K extends keyof AccountModel, M> = PickWith<AccountModel, K, M>
21
22// ############################################################################
23
24export type MAccount = Omit<AccountModel, 'Actor' | 'User' | 'Application' | 'VideoChannels' | 'VideoPlaylists' |
25 'VideoComments' | 'BlockedAccounts'>
26
27// ############################################################################
28
29// Only some attributes
30export type MAccountId = Pick<MAccount, 'id'>
31export type MAccountUserId = Pick<MAccount, 'userId'>
32
33// Only some Actor attributes
34export type MAccountUrl = Use<'Actor', MActorUrl>
35export type MAccountAudience = Use<'Actor', MActorAudience>
36
37export type MAccountIdActor = MAccountId &
38 Use<'Actor', MActor>
39
40export type MAccountIdActorId = MAccountId &
41 Use<'Actor', MActorId>
42
43// ############################################################################
44
45// Default scope
46export type MAccountDefault = MAccount &
47 Use<'Actor', MActorDefault>
48
49// Default with default association scopes
50export type MAccountDefaultChannelDefault = MAccount &
51 Use<'Actor', MActorDefault> &
52 Use<'VideoChannels', MChannelDefault[]>
53
54// We don't need some actors attributes
55export type MAccountLight = MAccount &
56 Use<'Actor', MActorDefaultLight>
57
58// ############################################################################
59
60// Full actor
61export type MAccountActor = MAccount &
62 Use<'Actor', MActor>
63
64// Full actor with server
65export type MAccountServer = MAccount &
66 Use<'Actor', MActorServer>
67
68// ############################################################################
69
70// For API
71
72export type MAccountSummary = FunctionProperties<MAccount> &
73 Pick<MAccount, 'id' | 'name'> &
74 Use<'Actor', MActorSummary>
75
76export type MAccountSummaryBlocks = MAccountSummary &
77 Use<'BlockedAccounts', MAccountBlocklistId[]>
78
79export type MAccountAPI = MAccount &
80 Use<'Actor', MActorAPI>
81
82// ############################################################################
83
84// Format for API or AP object
85
86export type MAccountSummaryFormattable = FunctionProperties<MAccount> &
87 Pick<MAccount, 'id' | 'name'> &
88 Use<'Actor', MActorSummaryFormattable>
89
90export type MAccountFormattable = FunctionProperties<MAccount> &
91 Pick<MAccount, 'id' | 'name' | 'description' | 'createdAt' | 'updatedAt' | 'userId'> &
92 Use<'Actor', MActorFormattable>
93
94export type MAccountAP = Pick<MAccount, 'name' | 'description'> &
95 Use<'Actor', MActorAP>
diff --git a/server/typings/models/account/actor-follow.ts b/server/typings/models/account/actor-follow.ts
new file mode 100644
index 000000000..1c66eb0a0
--- /dev/null
+++ b/server/typings/models/account/actor-follow.ts
@@ -0,0 +1,63 @@
1import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
2import {
3 MActor,
4 MActorAccount,
5 MActorDefaultAccountChannel,
6 MActorChannelAccountActor,
7 MActorDefault,
8 MActorFormattable,
9 MActorHost,
10 MActorUsername
11} from './actor'
12import { PickWith } from '../../utils'
13import { ActorModel } from '@server/models/activitypub/actor'
14import { MChannelDefault } from '@server/typings/models'
15
16type Use<K extends keyof ActorFollowModel, M> = PickWith<ActorFollowModel, K, M>
17
18// ############################################################################
19
20export type MActorFollow = Omit<ActorFollowModel, 'ActorFollower' | 'ActorFollowing'>
21
22// ############################################################################
23
24export type MActorFollowFollowingHost = MActorFollow &
25 Use<'ActorFollowing', MActorUsername & MActorHost>
26
27// ############################################################################
28
29// With actors or actors default
30
31export type MActorFollowActors = MActorFollow &
32 Use<'ActorFollower', MActor> &
33 Use<'ActorFollowing', MActor>
34
35export type MActorFollowActorsDefault = MActorFollow &
36 Use<'ActorFollower', MActorDefault> &
37 Use<'ActorFollowing', MActorDefault>
38
39export type MActorFollowFull = MActorFollow &
40 Use<'ActorFollower', MActorDefaultAccountChannel> &
41 Use<'ActorFollowing', MActorDefaultAccountChannel>
42
43// ############################################################################
44
45// For subscriptions
46
47type SubscriptionFollowing = MActorDefault &
48 PickWith<ActorModel, 'VideoChannel', MChannelDefault>
49
50export type MActorFollowActorsDefaultSubscription = MActorFollow &
51 Use<'ActorFollower', MActorDefault> &
52 Use<'ActorFollowing', SubscriptionFollowing>
53
54export type MActorFollowSubscriptions = MActorFollow &
55 Use<'ActorFollowing', MActorChannelAccountActor>
56
57// ############################################################################
58
59// Format for API or AP object
60
61export type MActorFollowFormattable = Pick<MActorFollow, 'id' | 'score' | 'state' | 'createdAt' | 'updatedAt'> &
62 Use<'ActorFollower', MActorFormattable> &
63 Use<'ActorFollowing', MActorFormattable>
diff --git a/server/typings/models/account/actor.ts b/server/typings/models/account/actor.ts
new file mode 100644
index 000000000..bcacb8351
--- /dev/null
+++ b/server/typings/models/account/actor.ts
@@ -0,0 +1,121 @@
1import { ActorModel } from '../../../models/activitypub/actor'
2import { FunctionProperties, PickWith, PickWithOpt } from '../../utils'
3import { MAccount, MAccountDefault, MAccountId, MAccountIdActor } from './account'
4import { MServer, MServerHost, MServerHostBlocks, MServerRedundancyAllowed } from '../server'
5import { MAvatar, MAvatarFormattable } from './avatar'
6import { MChannel, MChannelAccountActor, MChannelAccountDefault, MChannelId, MChannelIdActor } from '../video'
7
8type Use<K extends keyof ActorModel, M> = PickWith<ActorModel, K, M>
9
10// ############################################################################
11
12export type MActor = Omit<ActorModel, 'Account' | 'VideoChannel' | 'ActorFollowing' | 'Avatar' | 'ActorFollowers' | 'Server'>
13
14// ############################################################################
15
16export type MActorUrl = Pick<MActor, 'url'>
17export type MActorId = Pick<MActor, 'id'>
18export type MActorUsername = Pick<MActor, 'preferredUsername'>
19
20export type MActorFollowersUrl = Pick<MActor, 'followersUrl'>
21export type MActorAudience = MActorUrl & MActorFollowersUrl
22export type MActorFollowerException = Pick<ActorModel, 'sharedInboxUrl' | 'inboxUrl'>
23export type MActorSignature = MActorAccountChannelId
24
25export type MActorLight = Omit<MActor, 'privateKey' | 'privateKey'>
26
27// ############################################################################
28
29// Some association attributes
30
31export type MActorHost = Use<'Server', MServerHost>
32export type MActorRedundancyAllowedOpt = PickWithOpt<ActorModel, 'Server', MServerRedundancyAllowed>
33
34export type MActorDefaultLight = MActorLight &
35 Use<'Server', MServerHost> &
36 Use<'Avatar', MAvatar>
37
38export type MActorAccountId = MActor &
39 Use<'Account', MAccountId>
40export type MActorAccountIdActor = MActor &
41 Use<'Account', MAccountIdActor>
42
43export type MActorChannelId = MActor &
44 Use<'VideoChannel', MChannelId>
45export type MActorChannelIdActor = MActor &
46 Use<'VideoChannel', MChannelIdActor>
47
48export type MActorAccountChannelId = MActorAccountId & MActorChannelId
49export type MActorAccountChannelIdActor = MActorAccountIdActor & MActorChannelIdActor
50
51// ############################################################################
52
53// Include raw account/channel/server
54
55export type MActorAccount = MActor &
56 Use<'Account', MAccount>
57
58export type MActorChannel = MActor &
59 Use<'VideoChannel', MChannel>
60
61export type MActorDefaultAccountChannel = MActorDefault & MActorAccount & MActorChannel
62
63export type MActorServer = MActor &
64 Use<'Server', MServer>
65
66// ############################################################################
67
68// Complex actor associations
69
70export type MActorDefault = MActor &
71 Use<'Server', MServer> &
72 Use<'Avatar', MAvatar>
73
74// Actor with channel that is associated to an account and its actor
75// Actor -> VideoChannel -> Account -> Actor
76export type MActorChannelAccountActor = MActor &
77 Use<'VideoChannel', MChannelAccountActor>
78
79export type MActorFull = MActor &
80 Use<'Server', MServer> &
81 Use<'Avatar', MAvatar> &
82 Use<'Account', MAccount> &
83 Use<'VideoChannel', MChannelAccountActor>
84
85// Same than ActorFull, but the account and the channel have their actor
86export type MActorFullActor = MActor &
87 Use<'Server', MServer> &
88 Use<'Avatar', MAvatar> &
89 Use<'Account', MAccountDefault> &
90 Use<'VideoChannel', MChannelAccountDefault>
91
92// ############################################################################
93
94// API
95
96export type MActorSummary = FunctionProperties<MActor> &
97 Pick<MActor, 'id' | 'preferredUsername' | 'url' | 'serverId' | 'avatarId'> &
98 Use<'Server', MServerHost> &
99 Use<'Avatar', MAvatar>
100
101export type MActorSummaryBlocks = MActorSummary &
102 Use<'Server', MServerHostBlocks>
103
104export type MActorAPI = Omit<MActorDefault, 'publicKey' | 'privateKey' | 'inboxUrl' | 'outboxUrl' | 'sharedInboxUrl' |
105 'followersUrl' | 'followingUrl' | 'url' | 'createdAt' | 'updatedAt'>
106
107// ############################################################################
108
109// Format for API or AP object
110
111export type MActorSummaryFormattable = FunctionProperties<MActor> &
112 Pick<MActor, 'url' | 'preferredUsername'> &
113 Use<'Server', MServerHost> &
114 Use<'Avatar', MAvatarFormattable>
115
116export type MActorFormattable = MActorSummaryFormattable &
117 Pick<MActor, 'id' | 'followingCount' | 'followersCount' | 'createdAt' | 'updatedAt'> &
118 Use<'Server', MServerHost & Partial<Pick<MServer, 'redundancyAllowed'>>>
119
120export type MActorAP = MActor &
121 Use<'Avatar', MAvatar>
diff --git a/server/typings/models/account/avatar.ts b/server/typings/models/account/avatar.ts
new file mode 100644
index 000000000..8af6cc787
--- /dev/null
+++ b/server/typings/models/account/avatar.ts
@@ -0,0 +1,11 @@
1import { AvatarModel } from '../../../models/avatar/avatar'
2import { FunctionProperties } from '@server/typings/utils'
3
4export type MAvatar = AvatarModel
5
6// ############################################################################
7
8// Format for API or AP object
9
10export type MAvatarFormattable = FunctionProperties<MAvatar> &
11 Pick<MAvatar, 'filename' | 'createdAt' | 'updatedAt'>
diff --git a/server/typings/models/account/index.d.ts b/server/typings/models/account/index.d.ts
new file mode 100644
index 000000000..513c09c40
--- /dev/null
+++ b/server/typings/models/account/index.d.ts
@@ -0,0 +1,5 @@
1export * from './account'
2export * from './account-blocklist'
3export * from './actor'
4export * from './actor-follow'
5export * from './avatar'
diff --git a/server/typings/models/actor-follow.ts b/server/typings/models/actor-follow.ts
deleted file mode 100644
index 952ef877b..000000000
--- a/server/typings/models/actor-follow.ts
+++ /dev/null
@@ -1,8 +0,0 @@
1import { ActorFollowModel } from '../../models/activitypub/actor-follow'
2import { ActorModelOnly } from './actor'
3
4export type ActorFollowModelOnly = Omit<ActorFollowModel, 'ActorFollower' | 'ActorFollowing'>
5export type ActorFollowModelLight = ActorFollowModelOnly & {
6 ActorFollower: ActorModelOnly
7 ActorFollowing: ActorModelOnly
8}
diff --git a/server/typings/models/actor.ts b/server/typings/models/actor.ts
deleted file mode 100644
index 2656c7b66..000000000
--- a/server/typings/models/actor.ts
+++ /dev/null
@@ -1,22 +0,0 @@
1import { ActorModel } from '../../models/activitypub/actor'
2import { VideoChannelModel } from '../../models/video/video-channel'
3import { AccountModel } from '../../models/account/account'
4import { FunctionProperties } from '../utils'
5
6export type VideoChannelModelId = FunctionProperties<VideoChannelModel>
7export type AccountModelId = FunctionProperties<AccountModel> | Pick<AccountModel, 'id'>
8
9export type VideoChannelModelIdActor = VideoChannelModelId & Pick<VideoChannelModel, 'Actor'>
10export type AccountModelIdActor = AccountModelId & Pick<AccountModel, 'Actor'>
11
12export type ActorModelUrl = Pick<ActorModel, 'url'>
13export type ActorModelOnly = Omit<ActorModel, 'Account' | 'VideoChannel' | 'ActorFollowing' | 'Avatar' | 'ActorFollowers' | 'Server'>
14export type ActorModelId = Pick<ActorModelOnly, 'id'>
15
16export type SignatureActorModel = ActorModelOnly & {
17 VideoChannel: VideoChannelModelIdActor
18
19 Account: AccountModelIdActor
20}
21
22export type ActorFollowerException = Pick<ActorModel, 'sharedInboxUrl' | 'inboxUrl'>
diff --git a/server/typings/models/index.d.ts b/server/typings/models/index.d.ts
index c90656965..78b4948ce 100644
--- a/server/typings/models/index.d.ts
+++ b/server/typings/models/index.d.ts
@@ -1 +1,5 @@
1export * from './actor' 1export * from './account'
2export * from './oauth'
3export * from './server'
4export * from './user'
5export * from './video'
diff --git a/server/typings/models/oauth/index.d.ts b/server/typings/models/oauth/index.d.ts
new file mode 100644
index 000000000..36b7ea8ca
--- /dev/null
+++ b/server/typings/models/oauth/index.d.ts
@@ -0,0 +1,2 @@
1export * from './oauth-client'
2export * from './oauth-token'
diff --git a/server/typings/models/oauth/oauth-client.ts b/server/typings/models/oauth/oauth-client.ts
new file mode 100644
index 000000000..904a07863
--- /dev/null
+++ b/server/typings/models/oauth/oauth-client.ts
@@ -0,0 +1,3 @@
1import { OAuthClientModel } from '@server/models/oauth/oauth-client'
2
3export type MOAuthClient = Omit<OAuthClientModel, 'OAuthTokens'>
diff --git a/server/typings/models/oauth/oauth-token.ts b/server/typings/models/oauth/oauth-token.ts
new file mode 100644
index 000000000..af3412925
--- /dev/null
+++ b/server/typings/models/oauth/oauth-token.ts
@@ -0,0 +1,13 @@
1import { OAuthTokenModel } from '@server/models/oauth/oauth-token'
2import { PickWith } from '@server/typings/utils'
3import { MUserAccountUrl } from '@server/typings/models'
4
5type Use<K extends keyof OAuthTokenModel, M> = PickWith<OAuthTokenModel, K, M>
6
7// ############################################################################
8
9export type MOAuthToken = Omit<OAuthTokenModel, 'User' | 'OAuthClients'>
10
11export type MOAuthTokenUser = MOAuthToken &
12 Use<'User', MUserAccountUrl> &
13 { user?: MUserAccountUrl }
diff --git a/server/typings/models/server/index.d.ts b/server/typings/models/server/index.d.ts
new file mode 100644
index 000000000..c853795ad
--- /dev/null
+++ b/server/typings/models/server/index.d.ts
@@ -0,0 +1,3 @@
1export * from './plugin'
2export * from './server'
3export * from './server-blocklist'
diff --git a/server/typings/models/server/plugin.ts b/server/typings/models/server/plugin.ts
new file mode 100644
index 000000000..94674c318
--- /dev/null
+++ b/server/typings/models/server/plugin.ts
@@ -0,0 +1,10 @@
1import { PluginModel } from '@server/models/server/plugin'
2
3export type MPlugin = PluginModel
4
5// ############################################################################
6
7// Format for API or AP object
8
9export type MPluginFormattable = Pick<MPlugin, 'name' | 'type' | 'version' | 'latestVersion' | 'enabled' | 'uninstalled'
10 | 'peertubeEngine' | 'description' | 'homepage' | 'settings' | 'createdAt' | 'updatedAt'>
diff --git a/server/typings/models/server/server-blocklist.ts b/server/typings/models/server/server-blocklist.ts
new file mode 100644
index 000000000..c81f604f5
--- /dev/null
+++ b/server/typings/models/server/server-blocklist.ts
@@ -0,0 +1,23 @@
1import { ServerBlocklistModel } from '@server/models/server/server-blocklist'
2import { PickWith } from '@server/typings/utils'
3import { MAccountDefault, MAccountFormattable, MServer, MServerFormattable } from '@server/typings/models'
4
5type Use<K extends keyof ServerBlocklistModel, M> = PickWith<ServerBlocklistModel, K, M>
6
7// ############################################################################
8
9export type MServerBlocklist = Omit<ServerBlocklistModel, 'ByAccount' | 'BlockedServer'>
10
11// ############################################################################
12
13export type MServerBlocklistAccountServer = MServerBlocklist &
14 Use<'ByAccount', MAccountDefault> &
15 Use<'BlockedServer', MServer>
16
17// ############################################################################
18
19// Format for API or AP object
20
21export type MServerBlocklistFormattable = Pick<MServerBlocklist, 'createdAt'> &
22 Use<'ByAccount', MAccountFormattable> &
23 Use<'BlockedServer', MServerFormattable>
diff --git a/server/typings/models/server/server.ts b/server/typings/models/server/server.ts
new file mode 100644
index 000000000..190cc0c28
--- /dev/null
+++ b/server/typings/models/server/server.ts
@@ -0,0 +1,24 @@
1import { ServerModel } from '../../../models/server/server'
2import { FunctionProperties, PickWith } from '../../utils'
3import { MAccountBlocklistId } from '../account'
4
5type Use<K extends keyof ServerModel, M> = PickWith<ServerModel, K, M>
6
7// ############################################################################
8
9export type MServer = Omit<ServerModel, 'Actors' | 'BlockedByAccounts'>
10
11// ############################################################################
12
13export type MServerHost = Pick<MServer, 'host'>
14export type MServerRedundancyAllowed = Pick<MServer, 'redundancyAllowed'>
15
16export type MServerHostBlocks = MServerHost &
17 Use<'BlockedByAccounts', MAccountBlocklistId[]>
18
19// ############################################################################
20
21// Format for API or AP object
22
23export type MServerFormattable = FunctionProperties<MServer> &
24 Pick<MServer, 'host'>
diff --git a/server/typings/models/user/index.d.ts b/server/typings/models/user/index.d.ts
new file mode 100644
index 000000000..6657b2128
--- /dev/null
+++ b/server/typings/models/user/index.d.ts
@@ -0,0 +1,4 @@
1export * from './user'
2export * from './user-notification'
3export * from './user-notification-setting'
4export * from './user-video-history'
diff --git a/server/typings/models/user/user-notification-setting.ts b/server/typings/models/user/user-notification-setting.ts
new file mode 100644
index 000000000..c674add1b
--- /dev/null
+++ b/server/typings/models/user/user-notification-setting.ts
@@ -0,0 +1,9 @@
1import { UserNotificationSettingModel } from '@server/models/account/user-notification-setting'
2
3export type MNotificationSetting = Omit<UserNotificationSettingModel, 'User'>
4
5// ############################################################################
6
7// Format for API or AP object
8
9export type MNotificationSettingFormattable = MNotificationSetting
diff --git a/server/typings/models/user/user-notification.ts b/server/typings/models/user/user-notification.ts
new file mode 100644
index 000000000..1cdc691b0
--- /dev/null
+++ b/server/typings/models/user/user-notification.ts
@@ -0,0 +1,78 @@
1import { UserNotificationModel } from '../../../models/account/user-notification'
2import { PickWith, PickWithOpt } from '../../utils'
3import { VideoModel } from '../../../models/video/video'
4import { ActorModel } from '../../../models/activitypub/actor'
5import { ServerModel } from '../../../models/server/server'
6import { AvatarModel } from '../../../models/avatar/avatar'
7import { VideoChannelModel } from '../../../models/video/video-channel'
8import { AccountModel } from '../../../models/account/account'
9import { VideoCommentModel } from '../../../models/video/video-comment'
10import { VideoAbuseModel } from '../../../models/video/video-abuse'
11import { VideoBlacklistModel } from '../../../models/video/video-blacklist'
12import { VideoImportModel } from '../../../models/video/video-import'
13import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
14
15type Use<K extends keyof UserNotificationModel, M> = PickWith<UserNotificationModel, K, M>
16
17// ############################################################################
18
19export namespace UserNotificationIncludes {
20 export type VideoInclude = Pick<VideoModel, 'id' | 'uuid' | 'name'>
21 export type VideoIncludeChannel = VideoInclude &
22 PickWith<VideoModel, 'VideoChannel', VideoChannelIncludeActor>
23
24 export type ActorInclude = Pick<ActorModel, 'preferredUsername' | 'getHost'> &
25 PickWith<ActorModel, 'Avatar', Pick<AvatarModel, 'filename' | 'getStaticPath'>> &
26 PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>>
27
28 export type VideoChannelInclude = Pick<VideoChannelModel, 'id' | 'name' | 'getDisplayName'>
29 export type VideoChannelIncludeActor = VideoChannelInclude &
30 PickWith<VideoChannelModel, 'Actor', ActorInclude>
31
32 export type AccountInclude = Pick<AccountModel, 'id' | 'name' | 'getDisplayName'>
33 export type AccountIncludeActor = AccountInclude &
34 PickWith<AccountModel, 'Actor', ActorInclude>
35
36 export type VideoCommentInclude = Pick<VideoCommentModel, 'id' | 'originCommentId' | 'getThreadId'> &
37 PickWith<VideoCommentModel, 'Account', AccountIncludeActor> &
38 PickWith<VideoCommentModel, 'Video', VideoInclude>
39
40 export type VideoAbuseInclude = Pick<VideoAbuseModel, 'id'> &
41 PickWith<VideoAbuseModel, 'Video', VideoInclude>
42
43 export type VideoBlacklistInclude = Pick<VideoBlacklistModel, 'id'> &
44 PickWith<VideoAbuseModel, 'Video', VideoInclude>
45
46 export type VideoImportInclude = Pick<VideoImportModel, 'id' | 'magnetUri' | 'targetUrl' | 'torrentName'> &
47 PickWith<VideoImportModel, 'Video', VideoInclude>
48
49 export type ActorFollower = Pick<ActorModel, 'preferredUsername' | 'getHost'> &
50 PickWith<ActorModel, 'Account', AccountInclude> &
51 PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>> &
52 PickWithOpt<ActorModel, 'Avatar', Pick<AvatarModel, 'filename' | 'getStaticPath'>>
53
54 export type ActorFollowing = Pick<ActorModel, 'preferredUsername' | 'type' | 'getHost'> &
55 PickWith<ActorModel, 'VideoChannel', VideoChannelInclude> &
56 PickWith<ActorModel, 'Account', AccountInclude> &
57 PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>>
58
59 export type ActorFollowInclude = Pick<ActorFollowModel, 'id' | 'state'> &
60 PickWith<ActorFollowModel, 'ActorFollower', ActorFollower> &
61 PickWith<ActorFollowModel, 'ActorFollowing', ActorFollowing>
62}
63
64// ############################################################################
65
66export type MUserNotification = Omit<UserNotificationModel, 'User' | 'Video' | 'Comment' | 'VideoAbuse' | 'VideoBlacklist' |
67 'VideoImport' | 'Account' | 'ActorFollow'>
68
69// ############################################################################
70
71export type UserNotificationModelForApi = MUserNotification &
72 Use<'Video', UserNotificationIncludes.VideoIncludeChannel> &
73 Use<'Comment', UserNotificationIncludes.VideoCommentInclude> &
74 Use<'VideoAbuse', UserNotificationIncludes.VideoAbuseInclude> &
75 Use<'VideoBlacklist', UserNotificationIncludes.VideoBlacklistInclude> &
76 Use<'VideoImport', UserNotificationIncludes.VideoImportInclude> &
77 Use<'ActorFollow', UserNotificationIncludes.ActorFollowInclude> &
78 Use<'Account', UserNotificationIncludes.AccountIncludeActor>
diff --git a/server/typings/models/user/user-video-history.ts b/server/typings/models/user/user-video-history.ts
new file mode 100644
index 000000000..62673ab1b
--- /dev/null
+++ b/server/typings/models/user/user-video-history.ts
@@ -0,0 +1,5 @@
1import { UserVideoHistoryModel } from '../../../models/account/user-video-history'
2
3export type MUserVideoHistory = Omit<UserVideoHistoryModel, 'Video' | 'User'>
4
5export type MUserVideoHistoryTime = Pick<MUserVideoHistory, 'currentTime'>
diff --git a/server/typings/models/user/user.ts b/server/typings/models/user/user.ts
new file mode 100644
index 000000000..52d6d4a05
--- /dev/null
+++ b/server/typings/models/user/user.ts
@@ -0,0 +1,70 @@
1import { UserModel } from '../../../models/account/user'
2import { PickWith, PickWithOpt } from '../../utils'
3import {
4 MAccount,
5 MAccountDefault,
6 MAccountDefaultChannelDefault,
7 MAccountFormattable,
8 MAccountId,
9 MAccountIdActorId,
10 MAccountUrl
11} from '../account'
12import { MNotificationSetting, MNotificationSettingFormattable } from './user-notification-setting'
13import { AccountModel } from '@server/models/account/account'
14import { MChannelFormattable } from '@server/typings/models'
15
16type Use<K extends keyof UserModel, M> = PickWith<UserModel, K, M>
17
18// ############################################################################
19
20export type MUser = Omit<UserModel, 'Account' | 'NotificationSetting' | 'VideoImports' | 'OAuthTokens'>
21
22// ############################################################################
23
24export type MUserQuotaUsed = MUser & { videoQuotaUsed?: number, videoQuotaUsedDaily?: number }
25export type MUserId = Pick<UserModel, 'id'>
26
27// ############################################################################
28
29// With account
30
31export type MUserAccountId = MUser &
32 Use<'Account', MAccountId>
33
34export type MUserAccountUrl = MUser &
35 Use<'Account', MAccountUrl & MAccountIdActorId>
36
37export type MUserAccount = MUser &
38 Use<'Account', MAccount>
39
40export type MUserAccountDefault = MUser &
41 Use<'Account', MAccountDefault>
42
43// With channel
44
45export type MUserNotifSettingChannelDefault = MUser &
46 Use<'NotificationSetting', MNotificationSetting> &
47 Use<'Account', MAccountDefaultChannelDefault>
48
49// With notification settings
50
51export type MUserWithNotificationSetting = MUser &
52 Use<'NotificationSetting', MNotificationSetting>
53
54export type MUserNotifSettingAccount = MUser &
55 Use<'NotificationSetting', MNotificationSetting> &
56 Use<'Account', MAccount>
57
58// Default scope
59
60export type MUserDefault = MUser &
61 Use<'NotificationSetting', MNotificationSetting> &
62 Use<'Account', MAccountDefault>
63
64// ############################################################################
65
66// Format for API or AP object
67
68export type MUserFormattable = MUserQuotaUsed &
69 Use<'Account', MAccountFormattable & PickWithOpt<AccountModel, 'VideoChannels', MChannelFormattable[]>> &
70 PickWithOpt<UserModel, 'NotificationSetting', MNotificationSettingFormattable>
diff --git a/server/typings/models/video-share.ts b/server/typings/models/video-share.ts
deleted file mode 100644
index 1406749d2..000000000
--- a/server/typings/models/video-share.ts
+++ /dev/null
@@ -1,3 +0,0 @@
1import { VideoShareModel } from '../../models/video/video-share'
2
3export type VideoShareModelOnly = Omit<VideoShareModel, 'Actor' | 'Video'>
diff --git a/server/typings/models/video/index.d.ts b/server/typings/models/video/index.d.ts
new file mode 100644
index 000000000..bd69c8a4b
--- /dev/null
+++ b/server/typings/models/video/index.d.ts
@@ -0,0 +1,18 @@
1export * from './schedule-video-update'
2export * from './tag'
3export * from './thumbnail'
4export * from './video'
5export * from './video-abuse'
6export * from './video-blacklist'
7export * from './video-caption'
8export * from './video-change-ownership'
9export * from './video-channels'
10export * from './video-comment'
11export * from './video-file'
12export * from './video-import'
13export * from './video-playlist'
14export * from './video-playlist-element'
15export * from './video-rate'
16export * from './video-redundancy'
17export * from './video-share'
18export * from './video-streaming-playlist'
diff --git a/server/typings/models/video/schedule-video-update.ts b/server/typings/models/video/schedule-video-update.ts
new file mode 100644
index 000000000..ada9af06e
--- /dev/null
+++ b/server/typings/models/video/schedule-video-update.ts
@@ -0,0 +1,9 @@
1import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
2
3export type MScheduleVideoUpdate = Omit<ScheduleVideoUpdateModel, 'Video'>
4
5// ############################################################################
6
7// Format for API or AP object
8
9export type MScheduleVideoUpdateFormattable = Pick<MScheduleVideoUpdate, 'updateAt' | 'privacy'>
diff --git a/server/typings/models/video/tag.ts b/server/typings/models/video/tag.ts
new file mode 100644
index 000000000..64a68873e
--- /dev/null
+++ b/server/typings/models/video/tag.ts
@@ -0,0 +1,3 @@
1import { TagModel } from '../../../models/video/tag'
2
3export type MTag = Omit<TagModel, 'Videos'>
diff --git a/server/typings/models/video/thumbnail.ts b/server/typings/models/video/thumbnail.ts
new file mode 100644
index 000000000..c03ba55ac
--- /dev/null
+++ b/server/typings/models/video/thumbnail.ts
@@ -0,0 +1,3 @@
1import { ThumbnailModel } from '../../../models/video/thumbnail'
2
3export type MThumbnail = Omit<ThumbnailModel, 'Video' | 'VideoPlaylist'>
diff --git a/server/typings/models/video/video-abuse.ts b/server/typings/models/video/video-abuse.ts
new file mode 100644
index 000000000..e38c3f586
--- /dev/null
+++ b/server/typings/models/video/video-abuse.ts
@@ -0,0 +1,31 @@
1import { VideoAbuseModel } from '../../../models/video/video-abuse'
2import { PickWith } from '../../utils'
3import { MVideo } from './video'
4import { MAccountDefault, MAccountFormattable } from '../account'
5
6type Use<K extends keyof VideoAbuseModel, M> = PickWith<VideoAbuseModel, K, M>
7
8// ############################################################################
9
10export type MVideoAbuse = Omit<VideoAbuseModel, 'Account' | 'Video' | 'toActivityPubObject'>
11
12// ############################################################################
13
14export type MVideoAbuseId = Pick<VideoAbuseModel, 'id'>
15
16export type MVideoAbuseVideo = MVideoAbuse &
17 Pick<VideoAbuseModel, 'toActivityPubObject'> &
18 Use<'Video', MVideo>
19
20export type MVideoAbuseAccountVideo = MVideoAbuse &
21 Pick<VideoAbuseModel, 'toActivityPubObject'> &
22 Use<'Video', MVideo> &
23 Use<'Account', MAccountDefault>
24
25// ############################################################################
26
27// Format for API or AP object
28
29export type MVideoAbuseFormattable = MVideoAbuse &
30 Use<'Account', MAccountFormattable> &
31 Use<'Video', Pick<MVideo, 'id' | 'uuid' | 'name'>>
diff --git a/server/typings/models/video/video-blacklist.ts b/server/typings/models/video/video-blacklist.ts
new file mode 100644
index 000000000..e12880454
--- /dev/null
+++ b/server/typings/models/video/video-blacklist.ts
@@ -0,0 +1,27 @@
1import { VideoBlacklistModel } from '../../../models/video/video-blacklist'
2import { PickWith } from '@server/typings/utils'
3import { MVideo, MVideoFormattable } from '@server/typings/models'
4
5type Use<K extends keyof VideoBlacklistModel, M> = PickWith<VideoBlacklistModel, K, M>
6
7// ############################################################################
8
9export type MVideoBlacklist = Omit<VideoBlacklistModel, 'Video'>
10
11export type MVideoBlacklistLight = Pick<MVideoBlacklist, 'id' | 'reason' | 'unfederated'>
12export type MVideoBlacklistUnfederated = Pick<MVideoBlacklist, 'unfederated'>
13
14// ############################################################################
15
16export type MVideoBlacklistLightVideo = MVideoBlacklistLight &
17 Use<'Video', MVideo>
18
19export type MVideoBlacklistVideo = MVideoBlacklist &
20 Use<'Video', MVideo>
21
22// ############################################################################
23
24// Format for API or AP object
25
26export type MVideoBlacklistFormattable = MVideoBlacklist &
27 Use<'Video', MVideoFormattable>
diff --git a/server/typings/models/video/video-caption.ts b/server/typings/models/video/video-caption.ts
new file mode 100644
index 000000000..7cb2a2ad3
--- /dev/null
+++ b/server/typings/models/video/video-caption.ts
@@ -0,0 +1,24 @@
1import { VideoCaptionModel } from '../../../models/video/video-caption'
2import { FunctionProperties, PickWith } from '@server/typings/utils'
3import { MVideo, MVideoUUID } from '@server/typings/models'
4
5type Use<K extends keyof VideoCaptionModel, M> = PickWith<VideoCaptionModel, K, M>
6
7// ############################################################################
8
9export type MVideoCaption = Omit<VideoCaptionModel, 'Video'>
10
11// ############################################################################
12
13export type MVideoCaptionLanguage = Pick<MVideoCaption, 'language'>
14
15export type MVideoCaptionVideo = MVideoCaption &
16 Use<'Video', Pick<MVideo, 'id' | 'remote' | 'uuid'>>
17
18// ############################################################################
19
20// Format for API or AP object
21
22export type MVideoCaptionFormattable = FunctionProperties<MVideoCaption> &
23 Pick<MVideoCaption, 'language'> &
24 Use<'Video', MVideoUUID>
diff --git a/server/typings/models/video/video-change-ownership.ts b/server/typings/models/video/video-change-ownership.ts
new file mode 100644
index 000000000..72634cdb2
--- /dev/null
+++ b/server/typings/models/video/video-change-ownership.ts
@@ -0,0 +1,23 @@
1import { VideoChangeOwnershipModel } from '@server/models/video/video-change-ownership'
2import { PickWith } from '@server/typings/utils'
3import { MAccountDefault, MAccountFormattable, MVideo, MVideoWithFileThumbnail } from '@server/typings/models'
4
5type Use<K extends keyof VideoChangeOwnershipModel, M> = PickWith<VideoChangeOwnershipModel, K, M>
6
7// ############################################################################
8
9export type MVideoChangeOwnership = Omit<VideoChangeOwnershipModel, 'Initiator' | 'NextOwner' | 'Video'>
10
11export type MVideoChangeOwnershipFull = MVideoChangeOwnership &
12 Use<'Initiator', MAccountDefault> &
13 Use<'NextOwner', MAccountDefault> &
14 Use<'Video', MVideoWithFileThumbnail>
15
16// ############################################################################
17
18// Format for API or AP object
19
20export type MVideoChangeOwnershipFormattable = Pick<MVideoChangeOwnership, 'id' | 'status' | 'createdAt'> &
21 Use<'Initiator', MAccountFormattable> &
22 Use<'NextOwner', MAccountFormattable> &
23 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
new file mode 100644
index 000000000..292d0ac95
--- /dev/null
+++ b/server/typings/models/video/video-channels.ts
@@ -0,0 +1,126 @@
1import { FunctionProperties, PickWith, PickWithOpt } from '../../utils'
2import { VideoChannelModel } from '../../../models/video/video-channel'
3import {
4 MAccountActor,
5 MAccountAPI,
6 MAccountDefault,
7 MAccountFormattable,
8 MAccountLight,
9 MAccountSummaryBlocks,
10 MAccountSummaryFormattable,
11 MAccountUrl,
12 MAccountUserId,
13 MActor,
14 MActorAccountChannelId,
15 MActorAP,
16 MActorAPI,
17 MActorDefault,
18 MActorDefaultLight,
19 MActorFormattable,
20 MActorLight,
21 MActorSummary,
22 MActorSummaryFormattable, MActorUrl
23} from '../account'
24import { MVideo } from './video'
25
26type Use<K extends keyof VideoChannelModel, M> = PickWith<VideoChannelModel, K, M>
27
28// ############################################################################
29
30export type MChannel = Omit<VideoChannelModel, 'Actor' | 'Account' | 'Videos' | 'VideoPlaylists'>
31
32// ############################################################################
33
34export type MChannelId = Pick<MChannel, 'id'>
35
36// ############################################################################
37
38export type MChannelIdActor = MChannelId &
39 Use<'Actor', MActorAccountChannelId>
40
41export type MChannelUserId = Pick<MChannel, 'accountId'> &
42 Use<'Account', MAccountUserId>
43
44export type MChannelActor = MChannel &
45 Use<'Actor', MActor>
46
47export type MChannelUrl = Use<'Actor', MActorUrl>
48
49// Default scope
50export type MChannelDefault = MChannel &
51 Use<'Actor', MActorDefault>
52
53// ############################################################################
54
55// Not all association attributes
56
57export type MChannelLight = MChannel &
58 Use<'Actor', MActorDefaultLight>
59
60export type MChannelActorLight = MChannel &
61 Use<'Actor', MActorLight>
62
63export type MChannelAccountLight = MChannel &
64 Use<'Actor', MActorDefaultLight> &
65 Use<'Account', MAccountLight>
66
67// ############################################################################
68
69// Account associations
70
71export type MChannelAccountActor = MChannel &
72 Use<'Account', MAccountActor>
73
74export type MChannelAccountDefault = MChannel &
75 Use<'Actor', MActorDefault> &
76 Use<'Account', MAccountDefault>
77
78export type MChannelActorAccountActor = MChannel &
79 Use<'Account', MAccountActor> &
80 Use<'Actor', MActor>
81
82// ############################################################################
83
84// Videos associations
85export type MChannelVideos = MChannel &
86 Use<'Videos', MVideo[]>
87
88export type MChannelActorAccountDefaultVideos = MChannel &
89 Use<'Actor', MActorDefault> &
90 Use<'Account', MAccountDefault> &
91 Use<'Videos', MVideo[]>
92
93// ############################################################################
94
95// For API
96
97export type MChannelSummary = FunctionProperties<MChannel> &
98 Pick<MChannel, 'id' | 'name' | 'description' | 'actorId'> &
99 Use<'Actor', MActorSummary>
100
101export type MChannelSummaryAccount = MChannelSummary &
102 Use<'Account', MAccountSummaryBlocks>
103
104export type MChannelAPI = MChannel &
105 Use<'Actor', MActorAPI> &
106 Use<'Account', MAccountAPI>
107
108// ############################################################################
109
110// Format for API or AP object
111
112export type MChannelSummaryFormattable = FunctionProperties<MChannel> &
113 Pick<MChannel, 'id' | 'name'> &
114 Use<'Actor', MActorSummaryFormattable>
115
116export type MChannelAccountSummaryFormattable = MChannelSummaryFormattable &
117 Use<'Account', MAccountSummaryFormattable>
118
119export type MChannelFormattable = FunctionProperties<MChannel> &
120 Pick<MChannel, 'id' | 'name' | 'description' | 'createdAt' | 'updatedAt' | 'support'> &
121 Use<'Actor', MActorFormattable> &
122 PickWithOpt<VideoChannelModel, 'Account', MAccountFormattable>
123
124export type MChannelAP = Pick<MChannel, 'name' | 'description' | 'support'> &
125 Use<'Actor', MActorAP> &
126 Use<'Account', MAccountUrl>
diff --git a/server/typings/models/video/video-comment.ts b/server/typings/models/video/video-comment.ts
new file mode 100644
index 000000000..4fd1c29e8
--- /dev/null
+++ b/server/typings/models/video/video-comment.ts
@@ -0,0 +1,57 @@
1import { VideoCommentModel } from '../../../models/video/video-comment'
2import { PickWith, PickWithOpt } from '../../utils'
3import { MAccountDefault, MAccountFormattable, MAccountUrl, MActorUrl } from '../account'
4import { MVideoAccountLight, MVideoFeed, MVideoIdUrl, MVideoUrl } from './video'
5
6type Use<K extends keyof VideoCommentModel, M> = PickWith<VideoCommentModel, K, M>
7
8// ############################################################################
9
10export type MComment = Omit<VideoCommentModel, 'OriginVideoComment' | 'InReplyToVideoComment' | 'Video' | 'Account'>
11export type MCommentTotalReplies = MComment & { totalReplies?: number }
12export type MCommentId = Pick<MComment, 'id'>
13export type MCommentUrl = Pick<MComment, 'url'>
14
15// ############################################################################
16
17export type MCommentOwner = MComment &
18 Use<'Account', MAccountDefault>
19
20export type MCommentVideo = MComment &
21 Use<'Video', MVideoAccountLight>
22
23export type MCommentReply = MComment &
24 Use<'InReplyToVideoComment', MComment>
25
26export type MCommentOwnerVideo = MComment &
27 Use<'Account', MAccountDefault> &
28 Use<'Video', MVideoAccountLight>
29
30export type MCommentOwnerVideoReply = MComment &
31 Use<'Account', MAccountDefault> &
32 Use<'Video', MVideoAccountLight> &
33 Use<'InReplyToVideoComment', MComment>
34
35export type MCommentOwnerReplyVideoLight = MComment &
36 Use<'Account', MAccountDefault> &
37 Use<'InReplyToVideoComment', MComment> &
38 Use<'Video', MVideoIdUrl>
39
40export type MCommentOwnerVideoFeed = MCommentOwner &
41 Use<'Video', MVideoFeed>
42
43// ############################################################################
44
45export type MCommentAPI = MComment & { totalReplies: number }
46
47// ############################################################################
48
49// Format for API or AP object
50
51export type MCommentFormattable = MCommentTotalReplies &
52 Use<'Account', MAccountFormattable>
53
54export type MCommentAP = MComment &
55 Use<'Account', MAccountUrl> &
56 PickWithOpt<VideoCommentModel, 'Video', MVideoUrl> &
57 PickWithOpt<VideoCommentModel, 'InReplyToVideoComment', MCommentUrl>
diff --git a/server/typings/models/video/video-file.ts b/server/typings/models/video/video-file.ts
new file mode 100644
index 000000000..484351a8d
--- /dev/null
+++ b/server/typings/models/video/video-file.ts
@@ -0,0 +1,19 @@
1import { VideoFileModel } from '../../../models/video/video-file'
2import { PickWith, PickWithOpt } from '../../utils'
3import { MVideo, MVideoUUID } from './video'
4import { MVideoRedundancyFileUrl } from './video-redundancy'
5
6type Use<K extends keyof VideoFileModel, M> = PickWith<VideoFileModel, K, M>
7
8// ############################################################################
9
10export type MVideoFile = Omit<VideoFileModel, 'Video' | 'RedundancyVideos'>
11
12export type MVideoFileVideo = MVideoFile &
13 Use<'Video', MVideo>
14
15export type MVideoFileVideoUUID = MVideoFile &
16 Use<'Video', MVideoUUID>
17
18export type MVideoFileRedundanciesOpt = MVideoFile &
19 PickWithOpt<VideoFileModel, 'RedundancyVideos', MVideoRedundancyFileUrl[]>
diff --git a/server/typings/models/video/video-import.ts b/server/typings/models/video/video-import.ts
new file mode 100644
index 000000000..c6a1c5b66
--- /dev/null
+++ b/server/typings/models/video/video-import.ts
@@ -0,0 +1,31 @@
1import { VideoImportModel } from '@server/models/video/video-import'
2import { PickWith, PickWithOpt } from '@server/typings/utils'
3import { MUser, MVideo, MVideoAccountLight, MVideoFormattable, MVideoTag, MVideoThumbnail, MVideoWithFile } from '@server/typings/models'
4
5type Use<K extends keyof VideoImportModel, M> = PickWith<VideoImportModel, K, M>
6
7// ############################################################################
8
9export type MVideoImport = Omit<VideoImportModel, 'User' | 'Video'>
10
11export type MVideoImportVideo = MVideoImport &
12 Use<'Video', MVideo>
13
14// ############################################################################
15
16type VideoAssociation = MVideoTag & MVideoAccountLight & MVideoThumbnail
17
18export type MVideoImportDefault = MVideoImport &
19 Use<'User', MUser> &
20 Use<'Video', VideoAssociation>
21
22export type MVideoImportDefaultFiles = MVideoImport &
23 Use<'User', MUser> &
24 Use<'Video', VideoAssociation & MVideoWithFile>
25
26// ############################################################################
27
28// Format for API or AP object
29
30export type MVideoImportFormattable = MVideoImport &
31 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
new file mode 100644
index 000000000..7b1b993ce
--- /dev/null
+++ b/server/typings/models/video/video-playlist-element.ts
@@ -0,0 +1,34 @@
1import { VideoPlaylistElementModel } from '@server/models/video/video-playlist-element'
2import { PickWith } from '@server/typings/utils'
3import { MVideoFormattable, MVideoPlaylistPrivacy, MVideoThumbnail, MVideoUrl } from '@server/typings/models'
4
5type Use<K extends keyof VideoPlaylistElementModel, M> = PickWith<VideoPlaylistElementModel, K, M>
6
7// ############################################################################
8
9export type MVideoPlaylistElement = Omit<VideoPlaylistElementModel, 'VideoPlaylist' | 'Video'>
10
11// ############################################################################
12
13export type MVideoPlaylistElementId = Pick<MVideoPlaylistElement, 'id'>
14
15export type MVideoPlaylistElementLight = Pick<MVideoPlaylistElement, 'id' | 'videoId' | 'startTimestamp' | 'stopTimestamp'>
16
17// ############################################################################
18
19export type MVideoPlaylistVideoThumbnail = MVideoPlaylistElement &
20 Use<'Video', MVideoThumbnail>
21
22export type MVideoPlaylistElementVideoUrlPlaylistPrivacy = MVideoPlaylistElement &
23 Use<'Video', MVideoUrl> &
24 Use<'VideoPlaylist', MVideoPlaylistPrivacy>
25
26// ############################################################################
27
28// Format for API or AP object
29
30export type MVideoPlaylistElementFormattable = MVideoPlaylistElement &
31 Use<'Video', MVideoFormattable>
32
33export type MVideoPlaylistElementAP = MVideoPlaylistElement &
34 Use<'Video', MVideoUrl>
diff --git a/server/typings/models/video/video-playlist.ts b/server/typings/models/video/video-playlist.ts
new file mode 100644
index 000000000..a40c7aca9
--- /dev/null
+++ b/server/typings/models/video/video-playlist.ts
@@ -0,0 +1,92 @@
1import { VideoPlaylistModel } from '../../../models/video/video-playlist'
2import { PickWith } from '../../utils'
3import { MAccount, MAccountDefault, MAccountSummary, MAccountSummaryFormattable } from '../account'
4import { MThumbnail } from './thumbnail'
5import { MChannelDefault, MChannelSummary, MChannelSummaryFormattable, MChannelUrl } from './video-channels'
6import { MVideoPlaylistElementLight } from '@server/typings/models/video/video-playlist-element'
7
8type Use<K extends keyof VideoPlaylistModel, M> = PickWith<VideoPlaylistModel, K, M>
9
10// ############################################################################
11
12export type MVideoPlaylist = Omit<VideoPlaylistModel, 'OwnerAccount' | 'VideoChannel' | 'VideoPlaylistElements' | 'Thumbnail'>
13
14// ############################################################################
15
16export type MVideoPlaylistId = Pick<MVideoPlaylist, 'id'>
17export type MVideoPlaylistPrivacy = Pick<MVideoPlaylist, 'privacy'>
18export type MVideoPlaylistUUID = Pick<MVideoPlaylist, 'uuid'>
19export type MVideoPlaylistVideosLength = MVideoPlaylist & { videosLength?: number }
20
21// ############################################################################
22
23// With elements
24
25export type MVideoPlaylistWithElements = MVideoPlaylist &
26 Use<'VideoPlaylistElements', MVideoPlaylistElementLight[]>
27
28export type MVideoPlaylistIdWithElements = MVideoPlaylistId &
29 Use<'VideoPlaylistElements', MVideoPlaylistElementLight[]>
30
31// ############################################################################
32
33// With account
34
35export type MVideoPlaylistOwner = MVideoPlaylist &
36 Use<'OwnerAccount', MAccount>
37
38export type MVideoPlaylistOwnerDefault = MVideoPlaylist &
39 Use<'OwnerAccount', MAccountDefault>
40
41// ############################################################################
42
43// With thumbnail
44
45export type MVideoPlaylistThumbnail = MVideoPlaylist &
46 Use<'Thumbnail', MThumbnail>
47
48export type MVideoPlaylistAccountThumbnail = MVideoPlaylist &
49 Use<'OwnerAccount', MAccountDefault> &
50 Use<'Thumbnail', MThumbnail>
51
52// ############################################################################
53
54// With channel
55
56export type MVideoPlaylistAccountChannelDefault = MVideoPlaylist &
57 Use<'OwnerAccount', MAccountDefault> &
58 Use<'VideoChannel', MChannelDefault>
59
60// ############################################################################
61
62// With all associations
63
64export type MVideoPlaylistFull = MVideoPlaylist &
65 Use<'OwnerAccount', MAccountDefault> &
66 Use<'VideoChannel', MChannelDefault> &
67 Use<'Thumbnail', MThumbnail>
68
69// ############################################################################
70
71// For API
72
73export type MVideoPlaylistAccountChannelSummary = MVideoPlaylist &
74 Use<'OwnerAccount', MAccountSummary> &
75 Use<'VideoChannel', MChannelSummary>
76
77export type MVideoPlaylistFullSummary = MVideoPlaylist &
78 Use<'Thumbnail', MThumbnail> &
79 Use<'OwnerAccount', MAccountSummary> &
80 Use<'VideoChannel', MChannelSummary>
81
82// ############################################################################
83
84// Format for API or AP object
85
86export type MVideoPlaylistFormattable = MVideoPlaylistVideosLength &
87 Use<'OwnerAccount', MAccountSummaryFormattable> &
88 Use<'VideoChannel', MChannelSummaryFormattable>
89
90export type MVideoPlaylistAP = MVideoPlaylist &
91 Use<'Thumbnail', MThumbnail> &
92 Use<'VideoChannel', MChannelUrl>
diff --git a/server/typings/models/video/video-rate.ts b/server/typings/models/video/video-rate.ts
new file mode 100644
index 000000000..2ff8a625b
--- /dev/null
+++ b/server/typings/models/video/video-rate.ts
@@ -0,0 +1,23 @@
1import { AccountVideoRateModel } from '@server/models/account/account-video-rate'
2import { PickWith } from '@server/typings/utils'
3import { MAccountAudience, MAccountUrl, MVideo, MVideoFormattable } from '..'
4
5type Use<K extends keyof AccountVideoRateModel, M> = PickWith<AccountVideoRateModel, K, M>
6
7// ############################################################################
8
9export type MAccountVideoRate = Omit<AccountVideoRateModel, 'Video' | 'Account'>
10
11export type MAccountVideoRateAccountUrl = MAccountVideoRate &
12 Use<'Account', MAccountUrl>
13
14export type MAccountVideoRateAccountVideo = MAccountVideoRate &
15 Use<'Account', MAccountAudience> &
16 Use<'Video', MVideo>
17
18// ############################################################################
19
20// Format for API or AP object
21
22export type MAccountVideoRateFormattable = Pick<MAccountVideoRate, 'type'> &
23 Use<'Video', MVideoFormattable>
diff --git a/server/typings/models/video/video-redundancy.ts b/server/typings/models/video/video-redundancy.ts
new file mode 100644
index 000000000..f3846afd7
--- /dev/null
+++ b/server/typings/models/video/video-redundancy.ts
@@ -0,0 +1,38 @@
1import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy'
2import { PickWith, PickWithOpt } from '@server/typings/utils'
3import { MStreamingPlaylistVideo, MVideoFile, MVideoFileVideo, MVideoUrl } from '@server/typings/models'
4import { VideoStreamingPlaylist } from '../../../../shared/models/videos/video-streaming-playlist.model'
5import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist'
6import { VideoFile } from '../../../../shared/models/videos'
7import { VideoFileModel } from '@server/models/video/video-file'
8
9type Use<K extends keyof VideoRedundancyModel, M> = PickWith<VideoRedundancyModel, K, M>
10
11// ############################################################################
12
13export type MVideoRedundancy = Omit<VideoRedundancyModel, 'VideoFile' | 'VideoStreamingPlaylist' | 'Actor'>
14
15export type MVideoRedundancyFileUrl = Pick<MVideoRedundancy, 'fileUrl'>
16
17// ############################################################################
18
19export type MVideoRedundancyFile = MVideoRedundancy &
20 Use<'VideoFile', MVideoFile>
21
22export type MVideoRedundancyFileVideo = MVideoRedundancy &
23 Use<'VideoFile', MVideoFileVideo>
24
25export type MVideoRedundancyStreamingPlaylistVideo = MVideoRedundancy &
26 Use<'VideoStreamingPlaylist', MStreamingPlaylistVideo>
27
28export type MVideoRedundancyVideo = MVideoRedundancy &
29 Use<'VideoFile', MVideoFileVideo> &
30 Use<'VideoStreamingPlaylist', MStreamingPlaylistVideo>
31
32// ############################################################################
33
34// Format for API or AP object
35
36export type MVideoRedundancyAP = MVideoRedundancy &
37 PickWithOpt<VideoRedundancyModel, 'VideoFile', MVideoFile & PickWith<VideoFileModel, 'Video', MVideoUrl>> &
38 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
new file mode 100644
index 000000000..a7a90beeb
--- /dev/null
+++ b/server/typings/models/video/video-share.ts
@@ -0,0 +1,17 @@
1import { VideoShareModel } from '../../../models/video/video-share'
2import { PickWith } from '../../utils'
3import { MActorDefault } from '../account'
4import { MVideo } from './video'
5
6type Use<K extends keyof VideoShareModel, M> = PickWith<VideoShareModel, K, M>
7
8// ############################################################################
9
10export type MVideoShare = Omit<VideoShareModel, 'Actor' | 'Video'>
11
12export type MVideoShareActor = MVideoShare &
13 Use<'Actor', MActorDefault>
14
15export type MVideoShareFull = MVideoShare &
16 Use<'Actor', MActorDefault> &
17 Use<'Video', MVideo>
diff --git a/server/typings/models/video/video-streaming-playlist.ts b/server/typings/models/video/video-streaming-playlist.ts
new file mode 100644
index 000000000..79696bcff
--- /dev/null
+++ b/server/typings/models/video/video-streaming-playlist.ts
@@ -0,0 +1,19 @@
1import { VideoStreamingPlaylistModel } from '../../../models/video/video-streaming-playlist'
2import { PickWith, PickWithOpt } from '../../utils'
3import { MVideoRedundancyFileUrl } from './video-redundancy'
4import { MVideo, MVideoUrl } from '@server/typings/models'
5
6type Use<K extends keyof VideoStreamingPlaylistModel, M> = PickWith<VideoStreamingPlaylistModel, K, M>
7
8// ############################################################################
9
10export type MStreamingPlaylist = Omit<VideoStreamingPlaylistModel, 'Video' | 'RedundancyVideos'>
11
12export type MStreamingPlaylistVideo = MStreamingPlaylist &
13 Use<'Video', MVideo>
14
15export type MStreamingPlaylistRedundancies = MStreamingPlaylist &
16 Use<'RedundancyVideos', MVideoRedundancyFileUrl[]>
17
18export type MStreamingPlaylistRedundanciesOpt = MStreamingPlaylist &
19 PickWithOpt<VideoStreamingPlaylistModel, 'RedundancyVideos', MVideoRedundancyFileUrl[]>
diff --git a/server/typings/models/video/video.ts b/server/typings/models/video/video.ts
new file mode 100644
index 000000000..9a53bd337
--- /dev/null
+++ b/server/typings/models/video/video.ts
@@ -0,0 +1,173 @@
1import { VideoModel } from '../../../models/video/video'
2import { PickWith, PickWithOpt } from '../../utils'
3import {
4 MChannelAccountDefault,
5 MChannelAccountLight,
6 MChannelAccountSummaryFormattable,
7 MChannelActor,
8 MChannelFormattable,
9 MChannelUserId
10} from './video-channels'
11import { MTag } from './tag'
12import { MVideoCaptionLanguage } from './video-caption'
13import { MStreamingPlaylist, MStreamingPlaylistRedundancies, MStreamingPlaylistRedundanciesOpt } from './video-streaming-playlist'
14import { MVideoFile, MVideoFileRedundanciesOpt } from './video-file'
15import { MThumbnail } from './thumbnail'
16import { MVideoBlacklist, MVideoBlacklistLight, MVideoBlacklistUnfederated } from './video-blacklist'
17import { MScheduleVideoUpdate } from './schedule-video-update'
18import { MUserVideoHistoryTime } from '../user/user-video-history'
19
20type Use<K extends keyof VideoModel, M> = PickWith<VideoModel, K, M>
21
22// ############################################################################
23
24export type MVideo = Omit<VideoModel, 'VideoChannel' | 'Tags' | 'Thumbnails' | 'VideoPlaylistElements' | 'VideoAbuses' |
25 'VideoFiles' | 'VideoStreamingPlaylists' | 'VideoShares' | 'AccountVideoRates' | 'VideoComments' | 'VideoViews' | 'UserVideoHistories' |
26 'ScheduleVideoUpdate' | 'VideoBlacklist' | 'VideoImport' | 'VideoCaptions'>
27
28// ############################################################################
29
30export type MVideoId = Pick<MVideo, 'id'>
31export type MVideoUrl = Pick<MVideo, 'url'>
32export type MVideoUUID = Pick<MVideo, 'uuid'>
33
34export type MVideoIdUrl = MVideoId & MVideoUrl
35export type MVideoFeed = Pick<MVideo, 'name' | 'uuid'>
36
37// ############################################################################
38
39// Video raw associations: schedules, video files, tags, thumbnails, captions, streaming playlists
40
41// "With" to not confuse with the VideoFile model
42export type MVideoWithFile = MVideo &
43 Use<'VideoFiles', MVideoFile[]>
44
45export type MVideoThumbnail = MVideo &
46 Use<'Thumbnails', MThumbnail[]>
47
48export type MVideoIdThumbnail = MVideoId &
49 Use<'Thumbnails', MThumbnail[]>
50
51export type MVideoWithFileThumbnail = MVideo &
52 Use<'VideoFiles', MVideoFile[]> &
53 Use<'Thumbnails', MThumbnail[]>
54
55export type MVideoThumbnailBlacklist = MVideo &
56 Use<'Thumbnails', MThumbnail[]> &
57 Use<'VideoBlacklist', MVideoBlacklistLight>
58
59export type MVideoTag = MVideo &
60 Use<'Tags', MTag[]>
61
62export type MVideoWithSchedule = MVideo &
63 PickWithOpt<VideoModel, 'ScheduleVideoUpdate', MScheduleVideoUpdate>
64
65export type MVideoWithCaptions = MVideo &
66 Use<'VideoCaptions', MVideoCaptionLanguage[]>
67
68export type MVideoWithStreamingPlaylist = MVideo &
69 Use<'VideoStreamingPlaylists', MStreamingPlaylist[]>
70
71// ############################################################################
72
73// Associations with not all their attributes
74
75export type MVideoUserHistory = MVideo &
76 Use<'UserVideoHistories', MUserVideoHistoryTime[]>
77
78export type MVideoWithBlacklistLight = MVideo &
79 Use<'VideoBlacklist', MVideoBlacklistLight>
80
81export type MVideoAccountLight = MVideo &
82 Use<'VideoChannel', MChannelAccountLight>
83
84export type MVideoWithRights = MVideo &
85 Use<'VideoBlacklist', MVideoBlacklistLight> &
86 Use<'Thumbnails', MThumbnail[]> &
87 Use<'VideoChannel', MChannelUserId>
88
89// ############################################################################
90
91// All files with some additional associations
92
93export type MVideoWithAllFiles = MVideo &
94 Use<'VideoFiles', MVideoFile[]> &
95 Use<'Thumbnails', MThumbnail[]> &
96 Use<'VideoStreamingPlaylists', MStreamingPlaylist[]>
97
98export type MVideoAccountLightBlacklistAllFiles = MVideo &
99 Use<'VideoFiles', MVideoFile[]> &
100 Use<'Thumbnails', MThumbnail[]> &
101 Use<'VideoStreamingPlaylists', MStreamingPlaylist[]> &
102 Use<'VideoChannel', MChannelAccountLight> &
103 Use<'VideoBlacklist', MVideoBlacklistLight>
104
105// ############################################################################
106
107// With account
108
109export type MVideoAccountDefault = MVideo &
110 Use<'VideoChannel', MChannelAccountDefault>
111
112export type MVideoThumbnailAccountDefault = MVideo &
113 Use<'Thumbnails', MThumbnail[]> &
114 Use<'VideoChannel', MChannelAccountDefault>
115
116export type MVideoWithChannelActor = MVideo &
117 Use<'VideoChannel', MChannelActor>
118
119export type MVideoFullLight = MVideo &
120 Use<'Thumbnails', MThumbnail[]> &
121 Use<'VideoBlacklist', MVideoBlacklistLight> &
122 Use<'Tags', MTag[]> &
123 Use<'VideoChannel', MChannelAccountLight> &
124 Use<'UserVideoHistories', MUserVideoHistoryTime[]> &
125 Use<'VideoFiles', MVideoFile[]> &
126 Use<'ScheduleVideoUpdate', MScheduleVideoUpdate> &
127 Use<'VideoStreamingPlaylists', MStreamingPlaylist[]>
128
129// ############################################################################
130
131// API
132
133export type MVideoAP = MVideo &
134 Use<'Tags', MTag[]> &
135 Use<'VideoChannel', MChannelAccountLight> &
136 Use<'VideoStreamingPlaylists', MStreamingPlaylist[]> &
137 Use<'VideoCaptions', MVideoCaptionLanguage[]> &
138 Use<'VideoBlacklist', MVideoBlacklistUnfederated> &
139 Use<'VideoFiles', MVideoFileRedundanciesOpt[]>
140
141export type MVideoAPWithoutCaption = Omit<MVideoAP, 'VideoCaptions'>
142
143export type MVideoDetails = MVideo &
144 Use<'VideoBlacklist', MVideoBlacklistLight> &
145 Use<'Tags', MTag[]> &
146 Use<'VideoChannel', MChannelAccountLight> &
147 Use<'ScheduleVideoUpdate', MScheduleVideoUpdate> &
148 Use<'Thumbnails', MThumbnail[]> &
149 Use<'UserVideoHistories', MUserVideoHistoryTime[]> &
150 Use<'VideoStreamingPlaylists', MStreamingPlaylistRedundancies[]> &
151 Use<'VideoFiles', MVideoFileRedundanciesOpt[]>
152
153export type MVideoForUser = MVideo &
154 Use<'VideoChannel', MChannelAccountDefault> &
155 Use<'ScheduleVideoUpdate', MScheduleVideoUpdate> &
156 Use<'VideoBlacklist', MVideoBlacklistLight> &
157 Use<'Thumbnails', MThumbnail[]>
158
159// ############################################################################
160
161// Format for API or AP object
162
163export type MVideoFormattable = MVideo &
164 PickWithOpt<VideoModel, 'UserVideoHistories', MUserVideoHistoryTime[]> &
165 Use<'VideoChannel', MChannelAccountSummaryFormattable> &
166 PickWithOpt<VideoModel, 'ScheduleVideoUpdate', Pick<MScheduleVideoUpdate, 'updateAt' | 'privacy'>> &
167 PickWithOpt<VideoModel, 'VideoBlacklist', Pick<MVideoBlacklist, 'reason'>>
168
169export type MVideoFormattableDetails = MVideoFormattable &
170 Use<'VideoChannel', MChannelFormattable> &
171 Use<'Tags', MTag[]> &
172 Use<'VideoStreamingPlaylists', MStreamingPlaylistRedundanciesOpt[]> &
173 Use<'VideoFiles', MVideoFileRedundanciesOpt[]>
diff --git a/server/typings/utils.ts b/server/typings/utils.ts
index a86b05be2..24d43b258 100644
--- a/server/typings/utils.ts
+++ b/server/typings/utils.ts
@@ -1,3 +1,22 @@
1export type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T] 1export type FunctionPropertyNames<T> = {
2 [K in keyof T]: T[K] extends Function ? K : never
3}[keyof T]
2 4
3export type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>> 5export type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>
6
7export type PickWith<T, KT extends keyof T, V> = {
8 [P in KT]: T[P] extends V ? V : never
9}
10
11export type PickWithOpt<T, KT extends keyof T, V> = {
12 [P in KT]?: T[P] extends V ? V : never
13}
14
15// https://github.com/krzkaczor/ts-essentials Rocks!
16export type DeepPartial<T> = {
17 [P in keyof T]?: T[P] extends Array<infer U>
18 ? Array<DeepPartial<U>>
19 : T[P] extends ReadonlyArray<infer U>
20 ? ReadonlyArray<DeepPartial<U>>
21 : DeepPartial<T[P]>
22}
diff --git a/shared/extra-utils/index.ts b/shared/extra-utils/index.ts
index 53ddaa681..78acf72aa 100644
--- a/shared/extra-utils/index.ts
+++ b/shared/extra-utils/index.ts
@@ -24,4 +24,5 @@ export * from './videos/video-streaming-playlists'
24export * from './videos/videos' 24export * from './videos/videos'
25export * from './videos/video-change-ownership' 25export * from './videos/video-change-ownership'
26export * from './feeds/feeds' 26export * from './feeds/feeds'
27export * from './instances-index/mock-instances-index'
27export * from './search/videos' 28export * from './search/videos'
diff --git a/shared/extra-utils/instances-index/mock-instances-index.ts b/shared/extra-utils/instances-index/mock-instances-index.ts
new file mode 100644
index 000000000..cfa4523c1
--- /dev/null
+++ b/shared/extra-utils/instances-index/mock-instances-index.ts
@@ -0,0 +1,38 @@
1import * as express from 'express'
2
3export class MockInstancesIndex {
4 private indexInstances: { host: string, createdAt: string }[] = []
5
6 initialize () {
7 return new Promise(res => {
8 const app = express()
9
10 app.use('/', (req: express.Request, res: express.Response, next: express.NextFunction) => {
11 if (process.env.DEBUG) console.log('Receiving request on mocked server %s.', req.url)
12
13 return next()
14 })
15
16 app.get('/api/v1/instances/hosts', (req: express.Request, res: express.Response) => {
17 const since = req.query.since
18
19 const filtered = this.indexInstances.filter(i => {
20 if (!since) return true
21
22 return i.createdAt > since
23 })
24
25 return res.json({
26 total: filtered.length,
27 data: filtered
28 })
29 })
30
31 app.listen(42100, () => res())
32 })
33 }
34
35 addInstance (host: string) {
36 this.indexInstances.push({ host, createdAt: new Date().toISOString() })
37 }
38}
diff --git a/shared/extra-utils/server/config.ts b/shared/extra-utils/server/config.ts
index 8736f083f..578dd35cf 100644
--- a/shared/extra-utils/server/config.ts
+++ b/shared/extra-utils/server/config.ts
@@ -1,5 +1,7 @@
1import { makeDeleteRequest, makeGetRequest, makePutBodyRequest } from '../requests/requests' 1import { makeDeleteRequest, makeGetRequest, makePutBodyRequest } from '../requests/requests'
2import { CustomConfig } from '../../models/server/custom-config.model' 2import { CustomConfig } from '../../models/server/custom-config.model'
3import { DeepPartial } from '@server/typings/utils'
4import { merge } from 'lodash'
3 5
4function getConfig (url: string) { 6function getConfig (url: string) {
5 const path = '/api/v1/config' 7 const path = '/api/v1/config'
@@ -44,13 +46,25 @@ function updateCustomConfig (url: string, token: string, newCustomConfig: Custom
44 }) 46 })
45} 47}
46 48
47function updateCustomSubConfig (url: string, token: string, newConfig: any) { 49function updateCustomSubConfig (url: string, token: string, newConfig: DeepPartial<CustomConfig>) {
48 const updateParams: CustomConfig = { 50 const updateParams: CustomConfig = {
49 instance: { 51 instance: {
50 name: 'PeerTube updated', 52 name: 'PeerTube updated',
51 shortDescription: 'my short description', 53 shortDescription: 'my short description',
52 description: 'my super description', 54 description: 'my super description',
53 terms: 'my super terms', 55 terms: 'my super terms',
56 codeOfConduct: 'my super coc',
57
58 creationReason: 'my super creation reason',
59 moderationInformation: 'my super moderation information',
60 administrator: 'Kuja',
61 maintenanceLifetime: 'forever',
62 businessModel: 'my super business model',
63 hardwareInformation: '2vCore 3GB RAM',
64
65 languages: [ 'en', 'es' ],
66 categories: [ 1, 2 ],
67
54 defaultClientRoute: '/videos/recently-added', 68 defaultClientRoute: '/videos/recently-added',
55 isNSFW: true, 69 isNSFW: true,
56 defaultNSFWPolicy: 'blur', 70 defaultNSFWPolicy: 'blur',
@@ -130,10 +144,21 @@ function updateCustomSubConfig (url: string, token: string, newConfig: any) {
130 enabled: true, 144 enabled: true,
131 manualApproval: false 145 manualApproval: false
132 } 146 }
147 },
148 followings: {
149 instance: {
150 autoFollowBack: {
151 enabled: false
152 },
153 autoFollowIndex: {
154 indexUrl: 'https://instances.joinpeertube.org',
155 enabled: false
156 }
157 }
133 } 158 }
134 } 159 }
135 160
136 Object.assign(updateParams, newConfig) 161 merge(updateParams, newConfig)
137 162
138 return updateCustomConfig(url, token, updateParams) 163 return updateCustomConfig(url, token, updateParams)
139} 164}
diff --git a/shared/extra-utils/users/user-notifications.ts b/shared/extra-utils/users/user-notifications.ts
index f7de542bf..9a5fd7e86 100644
--- a/shared/extra-utils/users/user-notifications.ts
+++ b/shared/extra-utils/users/user-notifications.ts
@@ -279,8 +279,9 @@ async function checkNewActorFollow (
279 expect(notification.actorFollow.follower.name).to.equal(followerName) 279 expect(notification.actorFollow.follower.name).to.equal(followerName)
280 expect(notification.actorFollow.follower.host).to.not.be.undefined 280 expect(notification.actorFollow.follower.host).to.not.be.undefined
281 281
282 expect(notification.actorFollow.following.displayName).to.equal(followingDisplayName) 282 const following = notification.actorFollow.following
283 expect(notification.actorFollow.following.type).to.equal(followType) 283 expect(following.displayName).to.equal(followingDisplayName)
284 expect(following.type).to.equal(followType)
284 } else { 285 } else {
285 expect(notification).to.satisfy(n => { 286 expect(notification).to.satisfy(n => {
286 return n.type !== notificationType || 287 return n.type !== notificationType ||
@@ -327,6 +328,37 @@ async function checkNewInstanceFollower (base: CheckerBaseParams, followerHost:
327 await checkNotification(base, notificationChecker, emailFinder, type) 328 await checkNotification(base, notificationChecker, emailFinder, type)
328} 329}
329 330
331async function checkAutoInstanceFollowing (base: CheckerBaseParams, followerHost: string, followingHost: string, type: CheckerType) {
332 const notificationType = UserNotificationType.AUTO_INSTANCE_FOLLOWING
333
334 function notificationChecker (notification: UserNotification, type: CheckerType) {
335 if (type === 'presence') {
336 expect(notification).to.not.be.undefined
337 expect(notification.type).to.equal(notificationType)
338
339 const following = notification.actorFollow.following
340 checkActor(following)
341 expect(following.name).to.equal('peertube')
342 expect(following.host).to.equal(followingHost)
343
344 expect(notification.actorFollow.follower.name).to.equal('peertube')
345 expect(notification.actorFollow.follower.host).to.equal(followerHost)
346 } else {
347 expect(notification).to.satisfy(n => {
348 return n.type !== notificationType || n.actorFollow.following.host !== followingHost
349 })
350 }
351 }
352
353 function emailFinder (email: object) {
354 const text: string = email[ 'text' ]
355
356 return text.includes(' automatically followed a new instance') && text.includes(followingHost)
357 }
358
359 await checkNotification(base, notificationChecker, emailFinder, type)
360}
361
330async function checkCommentMention ( 362async function checkCommentMention (
331 base: CheckerBaseParams, 363 base: CheckerBaseParams,
332 uuid: string, 364 uuid: string,
@@ -427,8 +459,8 @@ async function checkVideoAutoBlacklistForModerators (base: CheckerBaseParams, vi
427 expect(notification).to.not.be.undefined 459 expect(notification).to.not.be.undefined
428 expect(notification.type).to.equal(notificationType) 460 expect(notification.type).to.equal(notificationType)
429 461
430 expect(notification.video.id).to.be.a('number') 462 expect(notification.videoBlacklist.video.id).to.be.a('number')
431 checkVideo(notification.video, videoName, videoUUID) 463 checkVideo(notification.videoBlacklist.video, videoName, videoUUID)
432 } else { 464 } else {
433 expect(notification).to.satisfy((n: UserNotification) => { 465 expect(notification).to.satisfy((n: UserNotification) => {
434 return n === undefined || n.video === undefined || n.video.uuid !== videoUUID 466 return n === undefined || n.video === undefined || n.video.uuid !== videoUUID
@@ -480,6 +512,7 @@ export {
480 markAsReadAllNotifications, 512 markAsReadAllNotifications,
481 checkMyVideoImportIsFinished, 513 checkMyVideoImportIsFinished,
482 checkUserRegistered, 514 checkUserRegistered,
515 checkAutoInstanceFollowing,
483 checkVideoIsPublished, 516 checkVideoIsPublished,
484 checkNewVideoFromSubscription, 517 checkNewVideoFromSubscription,
485 checkNewActorFollow, 518 checkNewActorFollow,
diff --git a/shared/extra-utils/users/users.ts b/shared/extra-utils/users/users.ts
index 30ed1bf4a..9959fd074 100644
--- a/shared/extra-utils/users/users.ts
+++ b/shared/extra-utils/users/users.ts
@@ -1,12 +1,12 @@
1import * as request from 'supertest' 1import * as request from 'supertest'
2import { makePostBodyRequest, makePutBodyRequest, updateAvatarRequest } from '../requests/requests' 2import { makePostBodyRequest, makePutBodyRequest, updateAvatarRequest } from '../requests/requests'
3import { NSFWPolicyType } from '../../models/videos/nsfw-policy.type'
4import { UserAdminFlag } from '../../models/users/user-flag.model' 3import { UserAdminFlag } from '../../models/users/user-flag.model'
5import { UserRegister } from '../../models/users/user-register.model' 4import { UserRegister } from '../../models/users/user-register.model'
6import { UserRole } from '../../models/users/user-role' 5import { UserRole } from '../../models/users/user-role'
7import { ServerInfo } from '../server/servers' 6import { ServerInfo } from '../server/servers'
8import { userLogin } from './login' 7import { userLogin } from './login'
9import { UserUpdateMe } from '../../models/users' 8import { UserUpdateMe } from '../../models/users'
9import { omit } from 'lodash'
10 10
11type CreateUserArgs = { url: string, 11type CreateUserArgs = { url: string,
12 accessToken: string, 12 accessToken: string,
@@ -214,33 +214,10 @@ function unblockUser (url: string, userId: number | string, accessToken: string,
214 .expect(expectedStatus) 214 .expect(expectedStatus)
215} 215}
216 216
217function updateMyUser (options: { 217function updateMyUser (options: { url: string, accessToken: string } & UserUpdateMe) {
218 url: string
219 accessToken: string
220 currentPassword?: string
221 newPassword?: string
222 nsfwPolicy?: NSFWPolicyType
223 email?: string
224 autoPlayVideo?: boolean
225 displayName?: string
226 description?: string
227 videosHistoryEnabled?: boolean
228 theme?: string
229}) {
230 const path = '/api/v1/users/me' 218 const path = '/api/v1/users/me'
231 219
232 const toSend: UserUpdateMe = {} 220 const toSend: UserUpdateMe = omit(options, 'url', 'accessToken')
233 if (options.currentPassword !== undefined && options.currentPassword !== null) toSend.currentPassword = options.currentPassword
234 if (options.newPassword !== undefined && options.newPassword !== null) toSend.password = options.newPassword
235 if (options.nsfwPolicy !== undefined && options.nsfwPolicy !== null) toSend.nsfwPolicy = options.nsfwPolicy
236 if (options.autoPlayVideo !== undefined && options.autoPlayVideo !== null) toSend.autoPlayVideo = options.autoPlayVideo
237 if (options.email !== undefined && options.email !== null) toSend.email = options.email
238 if (options.description !== undefined && options.description !== null) toSend.description = options.description
239 if (options.displayName !== undefined && options.displayName !== null) toSend.displayName = options.displayName
240 if (options.theme !== undefined && options.theme !== null) toSend.theme = options.theme
241 if (options.videosHistoryEnabled !== undefined && options.videosHistoryEnabled !== null) {
242 toSend.videosHistoryEnabled = options.videosHistoryEnabled
243 }
244 221
245 return makePutBodyRequest({ 222 return makePutBodyRequest({
246 url: options.url, 223 url: options.url,
diff --git a/shared/models/activitypub/activity.ts b/shared/models/activitypub/activity.ts
index 95801190d..492b672c7 100644
--- a/shared/models/activitypub/activity.ts
+++ b/shared/models/activitypub/activity.ts
@@ -91,5 +91,5 @@ export interface ActivityDislike extends BaseActivity {
91export interface ActivityFlag extends BaseActivity { 91export interface ActivityFlag extends BaseActivity {
92 type: 'Flag', 92 type: 'Flag',
93 content: string, 93 content: string,
94 object: APObject 94 object: APObject | APObject[]
95} 95}
diff --git a/shared/models/activitypub/objects/video-abuse-object.ts b/shared/models/activitypub/objects/video-abuse-object.ts
index 40e7abd57..5f1264a76 100644
--- a/shared/models/activitypub/objects/video-abuse-object.ts
+++ b/shared/models/activitypub/objects/video-abuse-object.ts
@@ -1,5 +1,5 @@
1export interface VideoAbuseObject { 1export interface VideoAbuseObject {
2 type: 'Flag', 2 type: 'Flag',
3 content: string 3 content: string
4 object: string 4 object: string | string[]
5} 5}
diff --git a/shared/models/i18n/i18n.ts b/shared/models/i18n/i18n.ts
index 218fd09ba..03a5d858a 100644
--- a/shared/models/i18n/i18n.ts
+++ b/shared/models/i18n/i18n.ts
@@ -39,7 +39,9 @@ const I18N_LOCALE_ALIAS = {
39 'pl': 'pl-PL', 39 'pl': 'pl-PL',
40 'ru': 'ru-RU', 40 'ru': 'ru-RU',
41 'nl': 'nl-NL', 41 'nl': 'nl-NL',
42 'zh': 'zh-Hans-CN' 42 'zh': 'zh-Hans-CN',
43 'zh-CN': 'zh-Hans-CN',
44 'zh-TW': 'zh-Hant-TW'
43} 45}
44 46
45export const POSSIBLE_LOCALES = Object.keys(I18N_LOCALES) 47export const POSSIBLE_LOCALES = Object.keys(I18N_LOCALES)
diff --git a/shared/models/server/about.model.ts b/shared/models/server/about.model.ts
index 10dff8b8f..6d4ba63c4 100644
--- a/shared/models/server/about.model.ts
+++ b/shared/models/server/about.model.ts
@@ -4,5 +4,17 @@ export interface About {
4 shortDescription: string 4 shortDescription: string
5 description: string 5 description: string
6 terms: string 6 terms: string
7
8 codeOfConduct: string
9 hardwareInformation: string
10
11 creationReason: string
12 moderationInformation: string
13 administrator: string
14 maintenanceLifetime: string
15 businessModel: string
16
17 languages: string[]
18 categories: number[]
7 } 19 }
8} 20}
diff --git a/shared/models/server/custom-config.model.ts b/shared/models/server/custom-config.model.ts
index a0541f5b6..c9957f825 100644
--- a/shared/models/server/custom-config.model.ts
+++ b/shared/models/server/custom-config.model.ts
@@ -6,6 +6,18 @@ export interface CustomConfig {
6 shortDescription: string 6 shortDescription: string
7 description: string 7 description: string
8 terms: string 8 terms: string
9 codeOfConduct: string
10
11 creationReason: string
12 moderationInformation: string
13 administrator: string
14 maintenanceLifetime: string
15 businessModel: string
16 hardwareInformation: string
17
18 languages: string[]
19 categories: number[]
20
9 isNSFW: boolean 21 isNSFW: boolean
10 defaultClientRoute: string 22 defaultClientRoute: string
11 defaultNSFWPolicy: NSFWPolicyType 23 defaultNSFWPolicy: NSFWPolicyType
@@ -99,4 +111,16 @@ export interface CustomConfig {
99 } 111 }
100 } 112 }
101 113
114 followings: {
115 instance: {
116 autoFollowBack: {
117 enabled: boolean
118 }
119
120 autoFollowIndex: {
121 enabled: boolean
122 indexUrl: string
123 }
124 }
125 }
102} 126}
diff --git a/shared/models/users/user-notification-setting.model.ts b/shared/models/users/user-notification-setting.model.ts
index e2a882b69..451f40d58 100644
--- a/shared/models/users/user-notification-setting.model.ts
+++ b/shared/models/users/user-notification-setting.model.ts
@@ -16,4 +16,5 @@ export interface UserNotificationSetting {
16 newFollow: UserNotificationSettingValue 16 newFollow: UserNotificationSettingValue
17 commentMention: UserNotificationSettingValue 17 commentMention: UserNotificationSettingValue
18 newInstanceFollower: UserNotificationSettingValue 18 newInstanceFollower: UserNotificationSettingValue
19 autoInstanceFollowing: UserNotificationSettingValue
19} 20}
diff --git a/shared/models/users/user-notification.model.ts b/shared/models/users/user-notification.model.ts
index fafc2b7d7..e9be1ca7f 100644
--- a/shared/models/users/user-notification.model.ts
+++ b/shared/models/users/user-notification.model.ts
@@ -19,7 +19,9 @@ export enum UserNotificationType {
19 19
20 VIDEO_AUTO_BLACKLIST_FOR_MODERATORS = 12, 20 VIDEO_AUTO_BLACKLIST_FOR_MODERATORS = 12,
21 21
22 NEW_INSTANCE_FOLLOWER = 13 22 NEW_INSTANCE_FOLLOWER = 13,
23
24 AUTO_INSTANCE_FOLLOWING = 14
23} 25}
24 26
25export interface VideoInfo { 27export interface VideoInfo {
@@ -78,10 +80,12 @@ export interface UserNotification {
78 id: number 80 id: number
79 follower: ActorInfo 81 follower: ActorInfo
80 state: FollowState 82 state: FollowState
83
81 following: { 84 following: {
82 type: 'account' | 'channel' 85 type: 'account' | 'channel' | 'instance'
83 name: string 86 name: string
84 displayName: string 87 displayName: string
88 host: string
85 } 89 }
86 } 90 }
87 91
diff --git a/shared/models/users/user-update-me.model.ts b/shared/models/users/user-update-me.model.ts
index b6c0002e5..99b9a65bd 100644
--- a/shared/models/users/user-update-me.model.ts
+++ b/shared/models/users/user-update-me.model.ts
@@ -15,4 +15,7 @@ export interface UserUpdateMe {
15 password?: string 15 password?: string
16 16
17 theme?: string 17 theme?: string
18
19 noInstanceConfigWarningModal?: boolean
20 noWelcomeModal?: boolean
18} 21}
diff --git a/shared/models/users/user.model.ts b/shared/models/users/user.model.ts
index de9825e1f..f67d262b0 100644
--- a/shared/models/users/user.model.ts
+++ b/shared/models/users/user.model.ts
@@ -10,6 +10,7 @@ export interface User {
10 username: string 10 username: string
11 email: string 11 email: string
12 pendingEmail: string | null 12 pendingEmail: string | null
13
13 emailVerified: boolean 14 emailVerified: boolean
14 nsfwPolicy: NSFWPolicyType 15 nsfwPolicy: NSFWPolicyType
15 16
@@ -18,13 +19,15 @@ export interface User {
18 autoPlayVideo: boolean 19 autoPlayVideo: boolean
19 webTorrentEnabled: boolean 20 webTorrentEnabled: boolean
20 videosHistoryEnabled: boolean 21 videosHistoryEnabled: boolean
22 videoLanguages: string[]
21 23
22 role: UserRole 24 role: UserRole
23 roleLabel: string 25 roleLabel: string
24 26
25 videoQuota: number 27 videoQuota: number
26 videoQuotaDaily: number 28 videoQuotaDaily: number
27 createdAt: Date 29 videoQuotaUsed?: number
30 videoQuotaUsedDaily?: number
28 31
29 theme: string 32 theme: string
30 33
@@ -35,5 +38,8 @@ export interface User {
35 blocked: boolean 38 blocked: boolean
36 blockedReason?: string 39 blockedReason?: string
37 40
38 videoQuotaUsed?: number 41 noInstanceConfigWarningModal: boolean
42 noWelcomeModal: boolean
43
44 createdAt: Date
39} 45}
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml
index 24ffdbc15..69717525d 100644
--- a/support/doc/api/openapi.yaml
+++ b/support/doc/api/openapi.yaml
@@ -130,9 +130,6 @@ paths:
130 summary: Get the account by name 130 summary: Get the account by name
131 parameters: 131 parameters:
132 - $ref: '#/components/parameters/name' 132 - $ref: '#/components/parameters/name'
133 - $ref: '#/components/parameters/start'
134 - $ref: '#/components/parameters/count'
135 - $ref: '#/components/parameters/sort'
136 responses: 133 responses:
137 '200': 134 '200':
138 description: successful operation 135 description: successful operation
@@ -204,6 +201,10 @@ paths:
204 tags: 201 tags:
205 - Accounts 202 - Accounts
206 summary: Get all accounts 203 summary: Get all accounts
204 parameters:
205 - $ref: '#/components/parameters/start'
206 - $ref: '#/components/parameters/count'
207 - $ref: '#/components/parameters/sort'
207 responses: 208 responses:
208 '200': 209 '200':
209 description: successful operation 210 description: successful operation
@@ -233,6 +234,10 @@ paths:
233 responses: 234 responses:
234 '200': 235 '200':
235 description: successful operation 236 description: successful operation
237 content:
238 application/json:
239 schema:
240 $ref: '#/components/schemas/ServerConfigAbout'
236 /config/custom: 241 /config/custom:
237 get: 242 get:
238 summary: Get the runtime configuration of the server 243 summary: Get the runtime configuration of the server
@@ -244,6 +249,10 @@ paths:
244 responses: 249 responses:
245 '200': 250 '200':
246 description: successful operation 251 description: successful operation
252 content:
253 application/json:
254 schema:
255 $ref: '#/components/schemas/ServerConfigCustom'
247 put: 256 put:
248 summary: Set the runtime configuration of the server 257 summary: Set the runtime configuration of the server
249 tags: 258 tags:
@@ -726,8 +735,7 @@ paths:
726 type: string 735 type: string
727 format: binary 736 format: binary
728 encoding: 737 encoding:
729 profileImage: 738 avatarfile:
730 # only accept png/jpeg
731 contentType: image/png, image/jpeg 739 contentType: image/png, image/jpeg
732 /videos: 740 /videos:
733 get: 741 get:
@@ -829,9 +837,11 @@ paths:
829 thumbnailfile: 837 thumbnailfile:
830 description: Video thumbnail file 838 description: Video thumbnail file
831 type: string 839 type: string
840 format: binary
832 previewfile: 841 previewfile:
833 description: Video preview file 842 description: Video preview file
834 type: string 843 type: string
844 format: binary
835 category: 845 category:
836 description: Video category 846 description: Video category
837 type: string 847 type: string
@@ -874,6 +884,11 @@ paths:
874 format: date-time 884 format: date-time
875 scheduleUpdate: 885 scheduleUpdate:
876 $ref: '#/components/schemas/VideoScheduledUpdate' 886 $ref: '#/components/schemas/VideoScheduledUpdate'
887 encoding:
888 thumbnailfile:
889 contentType: image/jpeg
890 previewfile:
891 contentType: image/jpeg
877 get: 892 get:
878 summary: Get a video by its id 893 summary: Get a video by its id
879 tags: 894 tags:
@@ -1029,9 +1044,11 @@ paths:
1029 thumbnailfile: 1044 thumbnailfile:
1030 description: Video thumbnail file 1045 description: Video thumbnail file
1031 type: string 1046 type: string
1047 format: binary
1032 previewfile: 1048 previewfile:
1033 description: Video preview file 1049 description: Video preview file
1034 type: string 1050 type: string
1051 format: binary
1035 privacy: 1052 privacy:
1036 $ref: '#/components/schemas/VideoPrivacySet' 1053 $ref: '#/components/schemas/VideoPrivacySet'
1037 category: 1054 category:
@@ -1080,6 +1097,13 @@ paths:
1080 - videofile 1097 - videofile
1081 - channelId 1098 - channelId
1082 - name 1099 - name
1100 encoding:
1101 videofile:
1102 contentType: video/mp4, video/webm, video/ogg, video/avi, video/quicktime, video/x-msvideo, video/x-flv, video/x-matroska, application/octet-stream
1103 thumbnailfile:
1104 contentType: image/jpeg
1105 previewfile:
1106 contentType: image/jpeg
1083 x-code-samples: 1107 x-code-samples:
1084 - lang: Shell 1108 - lang: Shell
1085 source: | 1109 source: |
@@ -1142,9 +1166,11 @@ paths:
1142 thumbnailfile: 1166 thumbnailfile:
1143 description: Video thumbnail file 1167 description: Video thumbnail file
1144 type: string 1168 type: string
1169 format: binary
1145 previewfile: 1170 previewfile:
1146 description: Video preview file 1171 description: Video preview file
1147 type: string 1172 type: string
1173 format: binary
1148 privacy: 1174 privacy:
1149 $ref: '#/components/schemas/VideoPrivacySet' 1175 $ref: '#/components/schemas/VideoPrivacySet'
1150 category: 1176 category:
@@ -1188,6 +1214,13 @@ paths:
1188 required: 1214 required:
1189 - channelId 1215 - channelId
1190 - name 1216 - name
1217 encoding:
1218 torrentfile:
1219 contentType: application/x-bittorrent
1220 thumbnailfile:
1221 contentType: image/jpeg
1222 previewfile:
1223 contentType: image/jpeg
1191 /videos/abuse: 1224 /videos/abuse:
1192 get: 1225 get:
1193 summary: Get list of reported video abuses 1226 summary: Get list of reported video abuses
@@ -1308,6 +1341,9 @@ paths:
1308 description: The file to upload. 1341 description: The file to upload.
1309 type: string 1342 type: string
1310 format: binary 1343 format: binary
1344 encoding:
1345 captionfile:
1346 contentType: text/vtt, application/x-subrip
1311 responses: 1347 responses:
1312 '204': 1348 '204':
1313 $ref: '#/paths/~1users~1me/put/responses/204' 1349 $ref: '#/paths/~1users~1me/put/responses/204'
@@ -1952,7 +1988,7 @@ components:
1952 description: 'Video file size in bytes' 1988 description: 'Video file size in bytes'
1953 torrentUrl: 1989 torrentUrl:
1954 type: string 1990 type: string
1955 torrentDownaloadUrl: 1991 torrentDownloadUrl:
1956 type: string 1992 type: string
1957 fileUrl: 1993 fileUrl:
1958 type: string 1994 type: string
@@ -2227,8 +2263,6 @@ components:
2227 properties: 2263 properties:
2228 id: 2264 id:
2229 type: number 2265 type: number
2230 uuid:
2231 type: string
2232 url: 2266 url:
2233 type: string 2267 type: string
2234 name: 2268 name:
@@ -2249,8 +2283,12 @@ components:
2249 allOf: 2283 allOf:
2250 - $ref: '#/components/schemas/Actor' 2284 - $ref: '#/components/schemas/Actor'
2251 - properties: 2285 - properties:
2286 userId:
2287 type: string
2252 displayName: 2288 displayName:
2253 type: string 2289 type: string
2290 description:
2291 type: string
2254 User: 2292 User:
2255 properties: 2293 properties:
2256 id: 2294 id:
@@ -2294,18 +2332,102 @@ components:
2294 type: number 2332 type: number
2295 ServerConfig: 2333 ServerConfig:
2296 properties: 2334 properties:
2335 instance:
2336 type: object
2337 properties:
2338 name:
2339 type: string
2340 shortDescription:
2341 type: string
2342 defaultClientRoute:
2343 type: string
2344 isNSFW:
2345 type: boolean
2346 defaultNSFWPolicy:
2347 type: string
2348 customizations:
2349 type: object
2350 properties:
2351 javascript:
2352 type: string
2353 css:
2354 type: string
2355 plugin:
2356 type: object
2357 properties:
2358 registered:
2359 type: array
2360 items:
2361 type: string
2362 theme:
2363 type: object
2364 properties:
2365 registered:
2366 type: array
2367 items:
2368 type: string
2369 email:
2370 type: object
2371 properties:
2372 enabled:
2373 type: boolean
2374 contactForm:
2375 type: object
2376 properties:
2377 enabled:
2378 type: boolean
2379 serverVersion:
2380 type: string
2381 serverCommit:
2382 type: string
2297 signup: 2383 signup:
2298 type: object 2384 type: object
2299 properties: 2385 properties:
2300 allowed: 2386 allowed:
2301 type: boolean 2387 type: boolean
2388 allowedForCurrentIP:
2389 type: boolean
2390 requiresEmailVerification:
2391 type: boolean
2302 transcoding: 2392 transcoding:
2303 type: object 2393 type: object
2304 properties: 2394 properties:
2395 hls:
2396 type: object
2397 properties:
2398 enabled:
2399 type: boolean
2305 enabledResolutions: 2400 enabledResolutions:
2306 type: array 2401 type: array
2307 items: 2402 items:
2308 type: number 2403 type: number
2404 import:
2405 type: object
2406 properties:
2407 videos:
2408 type: object
2409 properties:
2410 http:
2411 type: object
2412 properties:
2413 enabled:
2414 type: boolean
2415 torrent:
2416 type: object
2417 properties:
2418 enabled:
2419 type: boolean
2420 autoBlacklist:
2421 type: object
2422 properties:
2423 videos:
2424 type: object
2425 properties:
2426 ofUsers:
2427 type: object
2428 properties:
2429 enabled:
2430 type: boolean
2309 avatar: 2431 avatar:
2310 type: object 2432 type: object
2311 properties: 2433 properties:
@@ -2324,6 +2446,18 @@ components:
2324 video: 2446 video:
2325 type: object 2447 type: object
2326 properties: 2448 properties:
2449 image:
2450 type: object
2451 properties:
2452 extensions:
2453 type: array
2454 items:
2455 type: string
2456 size:
2457 type: object
2458 properties:
2459 max:
2460 type: number
2327 file: 2461 file:
2328 type: object 2462 type: object
2329 properties: 2463 properties:
@@ -2331,6 +2465,202 @@ components:
2331 type: array 2465 type: array
2332 items: 2466 items:
2333 type: string 2467 type: string
2468 videoCaption:
2469 type: object
2470 properties:
2471 file:
2472 type: object
2473 properties:
2474 size:
2475 type: object
2476 properties:
2477 max:
2478 type: number
2479 extensions:
2480 type: array
2481 items:
2482 type: string
2483 user:
2484 type: object
2485 properties:
2486 videoQuota:
2487 type: number
2488 videoQuotaDaily:
2489 type: number
2490 trending:
2491 type: object
2492 properties:
2493 videos:
2494 type: object
2495 properties:
2496 intervalDays:
2497 type: number
2498 tracker:
2499 ype: object
2500 properties:
2501 enabled:
2502 type: boolean
2503 ServerConfigAbout:
2504 properties:
2505 instance:
2506 type: object
2507 properties:
2508 name:
2509 type: string
2510 shortDescription:
2511 type: string
2512 description:
2513 type: string
2514 terms:
2515 type: string
2516 ServerConfigCustom:
2517 properties:
2518 instance:
2519 type: object
2520 properties:
2521 name:
2522 type: string
2523 shortDescription:
2524 type: string
2525 description:
2526 type: string
2527 terms:
2528 type: string
2529 defaultClientRoute:
2530 type: string
2531 isNSFW:
2532 type: boolean
2533 defaultNSFWPolicy:
2534 type: string
2535 customizations:
2536 type: object
2537 properties:
2538 javascript:
2539 type: string
2540 css:
2541 type: string
2542 theme:
2543 type: object
2544 properties:
2545 default:
2546 type: string
2547 services:
2548 type: object
2549 properties:
2550 twitter:
2551 type: object
2552 properties:
2553 username:
2554 type: string
2555 whitelisted:
2556 type: boolean
2557 cache:
2558 type: object
2559 properties:
2560 previews:
2561 type: object
2562 properties:
2563 size:
2564 type: number
2565 captions:
2566 type: object
2567 properties:
2568 size:
2569 type: number
2570 signup:
2571 type: object
2572 properties:
2573 enabled:
2574 type: boolean
2575 limit:
2576 type: number
2577 requiresEmailVerification:
2578 type: boolean
2579 admin:
2580 type: object
2581 properties:
2582 email:
2583 type: string
2584 contactForm:
2585 type: object
2586 properties:
2587 enabled:
2588 type: boolean
2589 user:
2590 type: object
2591 properties:
2592 videoQuota:
2593 type: number
2594 videoQuotaDaily:
2595 type: number
2596 transcoding:
2597 type: object
2598 properties:
2599 enabled:
2600 type: boolean
2601 allowAdditionalExtensions:
2602 type: boolean
2603 allowAudioFiles:
2604 type: boolean
2605 threads:
2606 type: number
2607 resolutions:
2608 type: object
2609 properties:
2610 240p:
2611 type: boolean
2612 360p:
2613 type: boolean
2614 480p:
2615 type: boolean
2616 720p:
2617 type: boolean
2618 1080p:
2619 type: boolean
2620 2160p:
2621 type: boolean
2622 hls:
2623 type: object
2624 properties:
2625 enabled:
2626 type: boolean
2627 import:
2628 type: object
2629 properties:
2630 videos:
2631 type: object
2632 properties:
2633 http:
2634 type: object
2635 properties:
2636 enabled:
2637 type: boolean
2638 torrent:
2639 type: object
2640 properties:
2641 enabled:
2642 type: boolean
2643 autoBlacklist:
2644 type: object
2645 properties:
2646 videos:
2647 type: object
2648 properties:
2649 ofUsers:
2650 type: object
2651 properties:
2652 enabled:
2653 type: boolean
2654 followers:
2655 type: object
2656 properties:
2657 instance:
2658 type: object
2659 properties:
2660 enabled:
2661 type: boolean
2662 manualApproval:
2663 type: boolean
2334 Follow: 2664 Follow:
2335 properties: 2665 properties:
2336 id: 2666 id:
diff --git a/support/doc/tools.md b/support/doc/tools.md
index cf427ec84..dd2a03db7 100644
--- a/support/doc/tools.md
+++ b/support/doc/tools.md
@@ -11,6 +11,7 @@
11 - [peertube-import-videos.js](#peertube-import-videosjs) 11 - [peertube-import-videos.js](#peertube-import-videosjs)
12 - [peertube-upload.js](#peertube-uploadjs) 12 - [peertube-upload.js](#peertube-uploadjs)
13 - [peertube-watch.js](#peertube-watchjs) 13 - [peertube-watch.js](#peertube-watchjs)
14 - [peertube-plugins.js](#peertube-pluginsjs)
14- [Server tools](#server-tools) 15- [Server tools](#server-tools)
15 - [parse-log](#parse-log) 16 - [parse-log](#parse-log)
16 - [create-transcoding-job.js](#create-transcoding-jobjs) 17 - [create-transcoding-job.js](#create-transcoding-jobjs)
@@ -19,6 +20,7 @@
19 - [optimize-old-videos.js](#optimize-old-videosjs) 20 - [optimize-old-videos.js](#optimize-old-videosjs)
20 - [update-host.js](#update-hostjs) 21 - [update-host.js](#update-hostjs)
21 - [reset-password.js](#reset-passwordjs) 22 - [reset-password.js](#reset-passwordjs)
23 - [plugin install/uninstall](#plugin-installuninstall)
22 - [REPL (Read Eval Print Loop)](#repl-read-eval-print-loop) 24 - [REPL (Read Eval Print Loop)](#repl-read-eval-print-loop)
23 - [.help](#help) 25 - [.help](#help)
24 - [Lodash example](#lodash-example) 26 - [Lodash example](#lodash-example)
@@ -182,6 +184,22 @@ It provides support for different players:
182- chromecast 184- chromecast
183 185
184 186
187#### peertube-plugins.js
188
189Install/update/uninstall or list local or NPM PeerTube plugins:
190
191```
192$ cd ${CLONE}
193$ node dist/server/tools/peertube-plugins.js --help
194$ node dist/server/tools/peertube-plugins.js list --help
195$ node dist/server/tools/peertube-plugins.js install --help
196$ node dist/server/tools/peertube-plugins.js update --help
197$ node dist/server/tools/peertube-plugins.js uninstall --help
198
199$ node dist/server/tools/peertube-plugins.js install --path /my/plugin/path
200$ node dist/server/tools/peertube-plugins.js install --npm-name peertube-theme-example
201```
202
185## Server tools 203## Server tools
186 204
187These scripts should be run on the server, in `peertube-latest` directory. 205These scripts should be run on the server, in `peertube-latest` directory.
@@ -262,22 +280,22 @@ $ sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production
262The difference with `peertube plugins` CLI is that these scripts can be used even if PeerTube is not running. 280The difference with `peertube plugins` CLI is that these scripts can be used even if PeerTube is not running.
263If PeerTube is running, you need to restart it for the changes to take effect (whereas with `peertube plugins` CLI, plugins/themes are dynamically loaded on the server). 281If PeerTube is running, you need to restart it for the changes to take effect (whereas with `peertube plugins` CLI, plugins/themes are dynamically loaded on the server).
264 282
265To install a plugin or a theme from the disk: 283To install/update a plugin or a theme from the disk:
266 284
267``` 285```
268$ sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production npm run npm run plugin:install -- --plugin-path /local/plugin/path 286$ sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production npm run plugin:install -- --plugin-path /local/plugin/path
269``` 287```
270 288
271From NPM: 289From NPM:
272 290
273``` 291```
274$ sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production npm run npm run plugin:install -- --npm-name peertube-plugin-myplugin 292$ sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production npm run plugin:install -- --npm-name peertube-plugin-myplugin
275``` 293```
276 294
277To uninstall a plugin or a theme: 295To uninstall a plugin or a theme:
278 296
279``` 297```
280$ sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production npm run npm run plugin:uninstall -- --npm-name peertube-plugin-myplugin 298$ sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production npm run plugin:uninstall -- --npm-name peertube-plugin-myplugin
281``` 299```
282 300
283### REPL ([Read Eval Print Loop](https://nodejs.org/docs/latest-v10.x/api/repl.html)) 301### REPL ([Read Eval Print Loop](https://nodejs.org/docs/latest-v10.x/api/repl.html))
diff --git a/tsconfig.json b/tsconfig.json
index 4d2bdd6ba..f2985f82b 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -14,11 +14,15 @@
14 "es2016", 14 "es2016",
15 "es2017" 15 "es2017"
16 ], 16 ],
17 "typeRoots": [ "node_modules/@types", "server/typings" ] 17 "typeRoots": [ "node_modules/@types", "server/typings" ],
18 "baseUrl": "./",
19 "paths": {
20 "@server/*": [ "server/*" ],
21 "@shared/*": [ "shared/*" ]
22 }
18 }, 23 },
19 "exclude": [ 24 "exclude": [
20 "server/tools/", 25 "server/tools/",
21 "client/node_modules",
22 "node_modules", 26 "node_modules",
23 "dist", 27 "dist",
24 "storage", 28 "storage",
diff --git a/yarn.lock b/yarn.lock
index f26763845..bab3aa162 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -828,24 +828,6 @@ bindings@~1.3.0:
828 resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.3.1.tgz#21fc7c6d67c18516ec5aaa2815b145ff77b26ea5" 828 resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.3.1.tgz#21fc7c6d67c18516ec5aaa2815b145ff77b26ea5"
829 integrity sha512-i47mqjF9UbjxJhxGf+pZ6kSxrnI3wBLlnGI2ArWJ4r0VrvDS7ZYXkprq/pLaBWYq4GM0r4zdHY+NNRqEMU7uew== 829 integrity sha512-i47mqjF9UbjxJhxGf+pZ6kSxrnI3wBLlnGI2ArWJ4r0VrvDS7ZYXkprq/pLaBWYq4GM0r4zdHY+NNRqEMU7uew==
830 830
831bitcore-lib@^0.13.7:
832 version "0.13.19"
833 resolved "https://registry.yarnpkg.com/bitcore-lib/-/bitcore-lib-0.13.19.tgz#48af1e9bda10067c1ab16263472b5add2000f3dc"
834 integrity sha1-SK8em9oQBnwasWJjRyta3SAA89w=
835 dependencies:
836 bn.js "=2.0.4"
837 bs58 "=2.0.0"
838 buffer-compare "=1.0.0"
839 elliptic "=3.0.3"
840 inherits "=2.0.1"
841 lodash "=3.10.1"
842
843"bitcore-message@github:CoMakery/bitcore-message#dist":
844 version "1.0.2"
845 resolved "https://codeload.github.com/CoMakery/bitcore-message/tar.gz/8799cc327029c3d34fc725f05b2cf981363f6ebf"
846 dependencies:
847 bitcore-lib "^0.13.7"
848
849bitfield@^2.0.0: 831bitfield@^2.0.0:
850 version "2.0.0" 832 version "2.0.0"
851 resolved "https://registry.yarnpkg.com/bitfield/-/bitfield-2.0.0.tgz#fbe6767592fe5b4c87ecf1d04126294cc1bfa837" 833 resolved "https://registry.yarnpkg.com/bitfield/-/bitfield-2.0.0.tgz#fbe6767592fe5b4c87ecf1d04126294cc1bfa837"
@@ -968,16 +950,6 @@ bluebird@^3.0.5, bluebird@^3.5.0:
968 resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.5.tgz#a8d0afd73251effbbd5fe384a77d73003c17a71f" 950 resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.5.tgz#a8d0afd73251effbbd5fe384a77d73003c17a71f"
969 integrity sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w== 951 integrity sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==
970 952
971bn.js@=2.0.4:
972 version "2.0.4"
973 resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-2.0.4.tgz#220a7cd677f7f1bfa93627ff4193776fe7819480"
974 integrity sha1-Igp81nf38b+pNif/QZN3b+eBlIA=
975
976bn.js@^2.0.0:
977 version "2.2.0"
978 resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-2.2.0.tgz#12162bc2ae71fc40a5626c33438f3a875cd37625"
979 integrity sha1-EhYrwq5x/EClYmwzQ486h1zTdiU=
980
981bn.js@^4.4.0: 953bn.js@^4.4.0:
982 version "4.11.8" 954 version "4.11.8"
983 resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" 955 resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
@@ -1043,11 +1015,6 @@ braces@^3.0.1:
1043 dependencies: 1015 dependencies:
1044 fill-range "^7.0.1" 1016 fill-range "^7.0.1"
1045 1017
1046brorand@^1.0.1:
1047 version "1.1.0"
1048 resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
1049 integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
1050
1051browser-stdout@1.3.1: 1018browser-stdout@1.3.1:
1052 version "1.3.1" 1019 version "1.3.1"
1053 resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" 1020 resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60"
@@ -1058,11 +1025,6 @@ browserify-package-json@^1.0.0:
1058 resolved "https://registry.yarnpkg.com/browserify-package-json/-/browserify-package-json-1.0.1.tgz#98dde8aa5c561fd6d3fe49bbaa102b74b396fdea" 1025 resolved "https://registry.yarnpkg.com/browserify-package-json/-/browserify-package-json-1.0.1.tgz#98dde8aa5c561fd6d3fe49bbaa102b74b396fdea"
1059 integrity sha1-mN3oqlxWH9bT/km7qhArdLOW/eo= 1026 integrity sha1-mN3oqlxWH9bT/km7qhArdLOW/eo=
1060 1027
1061bs58@=2.0.0:
1062 version "2.0.0"
1063 resolved "https://registry.yarnpkg.com/bs58/-/bs58-2.0.0.tgz#72b713bed223a0ac518bbda0e3ce3f4817f39eb5"
1064 integrity sha1-crcTvtIjoKxRi72g484/SBfznrU=
1065
1066buffer-alloc-unsafe@^1.1.0: 1028buffer-alloc-unsafe@^1.1.0:
1067 version "1.1.0" 1029 version "1.1.0"
1068 resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0" 1030 resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0"
@@ -1076,16 +1038,6 @@ buffer-alloc@^1.1.0, buffer-alloc@^1.2.0:
1076 buffer-alloc-unsafe "^1.1.0" 1038 buffer-alloc-unsafe "^1.1.0"
1077 buffer-fill "^1.0.0" 1039 buffer-fill "^1.0.0"
1078 1040
1079buffer-compare@=1.0.0:
1080 version "1.0.0"
1081 resolved "https://registry.yarnpkg.com/buffer-compare/-/buffer-compare-1.0.0.tgz#acaa7a966e98eee9fae14b31c39a5f158fb3c4a2"
1082 integrity sha1-rKp6lm6Y7un64Usxw5pfFY+zxKI=
1083
1084buffer-equal-constant-time@1.0.1:
1085 version "1.0.1"
1086 resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
1087 integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=
1088
1089buffer-equals@^1.0.3, buffer-equals@^1.0.4: 1041buffer-equals@^1.0.3, buffer-equals@^1.0.4:
1090 version "1.0.4" 1042 version "1.0.4"
1091 resolved "https://registry.yarnpkg.com/buffer-equals/-/buffer-equals-1.0.4.tgz#0353b54fd07fd9564170671ae6f66b9cf10d27f5" 1043 resolved "https://registry.yarnpkg.com/buffer-equals/-/buffer-equals-1.0.4.tgz#0353b54fd07fd9564170671ae6f66b9cf10d27f5"
@@ -2096,13 +2048,6 @@ ecc-jsbn@~0.1.1:
2096 jsbn "~0.1.0" 2048 jsbn "~0.1.0"
2097 safer-buffer "^2.1.0" 2049 safer-buffer "^2.1.0"
2098 2050
2099ecdsa-sig-formatter@1.0.11:
2100 version "1.0.11"
2101 resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf"
2102 integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==
2103 dependencies:
2104 safe-buffer "^5.0.1"
2105
2106ee-first@1.1.1: 2051ee-first@1.1.1:
2107 version "1.1.1" 2052 version "1.1.1"
2108 resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 2053 resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@@ -2113,16 +2058,6 @@ elegant-spinner@^1.0.1:
2113 resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" 2058 resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e"
2114 integrity sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4= 2059 integrity sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4=
2115 2060
2116elliptic@=3.0.3:
2117 version "3.0.3"
2118 resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-3.0.3.tgz#865c9b420bfbe55006b9f969f97a0d2c44966595"
2119 integrity sha1-hlybQgv75VAGuflp+XoNLESWZZU=
2120 dependencies:
2121 bn.js "^2.0.0"
2122 brorand "^1.0.1"
2123 hash.js "^1.0.0"
2124 inherits "^2.0.1"
2125
2126emoji-regex@^7.0.1: 2061emoji-regex@^7.0.1:
2127 version "7.0.3" 2062 version "7.0.3"
2128 resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" 2063 resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
@@ -3250,14 +3185,6 @@ has@^1.0.1, has@^1.0.3:
3250 dependencies: 3185 dependencies:
3251 function-bind "^1.1.1" 3186 function-bind "^1.1.1"
3252 3187
3253hash.js@^1.0.0:
3254 version "1.1.7"
3255 resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42"
3256 integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==
3257 dependencies:
3258 inherits "^2.0.3"
3259 minimalistic-assert "^1.0.1"
3260
3261hashish@~0.0.4: 3188hashish@~0.0.4:
3262 version "0.0.4" 3189 version "0.0.4"
3263 resolved "https://registry.yarnpkg.com/hashish/-/hashish-0.0.4.tgz#6d60bc6ffaf711b6afd60e426d077988014e6554" 3190 resolved "https://registry.yarnpkg.com/hashish/-/hashish-0.0.4.tgz#6d60bc6ffaf711b6afd60e426d077988014e6554"
@@ -3481,11 +3408,6 @@ inherits@2.0.3:
3481 resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 3408 resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
3482 integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= 3409 integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
3483 3410
3484inherits@=2.0.1:
3485 version "2.0.1"
3486 resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
3487 integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=
3488
3489ini@^1.3.4, ini@~1.3.0: 3411ini@^1.3.4, ini@~1.3.0:
3490 version "1.3.5" 3412 version "1.3.5"
3491 resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" 3413 resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
@@ -4028,25 +3950,6 @@ jsonify@~0.0.0:
4028 resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" 3950 resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
4029 integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM= 3951 integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=
4030 3952
4031"jsonld-signatures@https://github.com/Chocobozzz/jsonld-signatures#rsa2017":
4032 version "1.2.2-2"
4033 resolved "https://github.com/Chocobozzz/jsonld-signatures#77660963e722eb4541d2d255f9d9d4216329665f"
4034 dependencies:
4035 bitcore-message "github:CoMakery/bitcore-message#dist"
4036 jsonld "^0.5.12"
4037 jws "^3.1.4"
4038 node-forge "^0.7.1"
4039
4040jsonld@^0.5.12:
4041 version "0.5.21"
4042 resolved "https://registry.yarnpkg.com/jsonld/-/jsonld-0.5.21.tgz#4d5b78d717eb92bcd1ac9d88e34efad95370c0bf"
4043 integrity sha512-1dQhaw1Eb3p7Cz5ECE2DNPwLvTmK+f6D45hACBdonJaFKP1bN9zlKLZWbPZQeZtduAc/LNv10J4ML0IiTBVahw==
4044 dependencies:
4045 rdf-canonize "^0.2.1"
4046 request "^2.83.0"
4047 semver "^5.5.0"
4048 xmldom "0.1.19"
4049
4050jsonld@~1.1.0: 3953jsonld@~1.1.0:
4051 version "1.1.0" 3954 version "1.1.0"
4052 resolved "https://registry.yarnpkg.com/jsonld/-/jsonld-1.1.0.tgz#afcb168c44557a7bddead4d4513c3cbcae3bc5b9" 3955 resolved "https://registry.yarnpkg.com/jsonld/-/jsonld-1.1.0.tgz#afcb168c44557a7bddead4d4513c3cbcae3bc5b9"
@@ -4082,23 +3985,6 @@ junk@^3.1.0:
4082 resolved "https://registry.yarnpkg.com/junk/-/junk-3.1.0.tgz#31499098d902b7e98c5d9b9c80f43457a88abfa1" 3985 resolved "https://registry.yarnpkg.com/junk/-/junk-3.1.0.tgz#31499098d902b7e98c5d9b9c80f43457a88abfa1"
4083 integrity sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ== 3986 integrity sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==
4084 3987
4085jwa@^1.4.1:
4086 version "1.4.1"
4087 resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a"
4088 integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==
4089 dependencies:
4090 buffer-equal-constant-time "1.0.1"
4091 ecdsa-sig-formatter "1.0.11"
4092 safe-buffer "^5.0.1"
4093
4094jws@^3.1.4:
4095 version "3.2.2"
4096 resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
4097 integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==
4098 dependencies:
4099 jwa "^1.4.1"
4100 safe-buffer "^5.0.1"
4101
4102k-bucket@^4.0.0: 3988k-bucket@^4.0.0:
4103 version "4.0.1" 3989 version "4.0.1"
4104 resolved "https://registry.yarnpkg.com/k-bucket/-/k-bucket-4.0.1.tgz#3fc2e5693f0b7bff90d7b6b476edd6087955d542" 3990 resolved "https://registry.yarnpkg.com/k-bucket/-/k-bucket-4.0.1.tgz#3fc2e5693f0b7bff90d7b6b476edd6087955d542"
@@ -4335,11 +4221,6 @@ lodash@4.17.4:
4335 resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" 4221 resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
4336 integrity sha1-eCA6TRwyiuHYbcpkYONptX9AVa4= 4222 integrity sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=
4337 4223
4338lodash@=3.10.1:
4339 version "3.10.1"
4340 resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
4341 integrity sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=
4342
4343lodash@^4.0.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.3.0, lodash@~4.17.10: 4224lodash@^4.0.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.3.0, lodash@~4.17.10:
4344 version "4.17.15" 4225 version "4.17.15"
4345 resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" 4226 resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
@@ -4638,11 +4519,6 @@ mimic-response@^1.0.0:
4638 resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" 4519 resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
4639 integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== 4520 integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==
4640 4521
4641minimalistic-assert@^1.0.1:
4642 version "1.0.1"
4643 resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
4644 integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
4645
4646minimatch@3.0.4, minimatch@^3.0.4, minimatch@~3.0.2: 4522minimatch@3.0.4, minimatch@^3.0.4, minimatch@~3.0.2:
4647 version "3.0.4" 4523 version "3.0.4"
4648 resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" 4524 resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
@@ -4734,6 +4610,11 @@ mocha@^6.0.0:
4734 yargs-parser "13.0.0" 4610 yargs-parser "13.0.0"
4735 yargs-unparser "1.5.0" 4611 yargs-unparser "1.5.0"
4736 4612
4613module-alias@^2.2.1:
4614 version "2.2.1"
4615 resolved "https://registry.yarnpkg.com/module-alias/-/module-alias-2.2.1.tgz#553aea9dc7f99cd45fd75e34a574960dc46550da"
4616 integrity sha512-LTez0Eo+YtfUhgzhu/LqxkUzOpD+k5C0wXBLun0L1qE2BhHf6l09dqam8e7BnoMYA6mAlP0vSsGFQ8QHhGN/aQ==
4617
4737moment-timezone@^0.5.21, moment-timezone@^0.5.25: 4618moment-timezone@^0.5.21, moment-timezone@^0.5.25:
4738 version "0.5.26" 4619 version "0.5.26"
4739 resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.26.tgz#c0267ca09ae84631aa3dc33f65bedbe6e8e0d772" 4620 resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.26.tgz#c0267ca09ae84631aa3dc33f65bedbe6e8e0d772"