Merge branch 'release/1.4.0' into develop
authorChocobozzz <me@florianbigard.com>
Wed, 11 Sep 2019 09:09:18 +0000 (11:09 +0200)
committerChocobozzz <me@florianbigard.com>
Wed, 11 Sep 2019 09:09:18 +0000 (11:09 +0200)
303 files changed:
CREDITS.md
README.md
client/package.json
client/src/app/+about/about-instance/about-instance.component.html
client/src/app/+about/about-instance/about-instance.component.scss
client/src/app/+about/about-instance/about-instance.component.ts
client/src/app/+about/about-peertube/about-peertube-contributors.component.html [new file with mode: 0644]
client/src/app/+about/about-peertube/about-peertube-contributors.component.scss [new file with mode: 0644]
client/src/app/+about/about-peertube/about-peertube-contributors.component.ts [new file with mode: 0644]
client/src/app/+about/about-peertube/about-peertube.component.html
client/src/app/+about/about-peertube/about-peertube.component.scss
client/src/app/+about/about.module.ts
client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss
client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
client/src/app/+admin/system/debug/debug.component.html
client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts
client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.html
client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.ts
client/src/app/+my-account/my-account.module.ts
client/src/app/+signup/+register/register-step-user.component.html
client/src/app/+signup/+register/register-step-user.component.ts
client/src/app/+signup/+register/register.component.html
client/src/app/+signup/+register/register.component.scss
client/src/app/+signup/+register/register.component.ts
client/src/app/+signup/+register/register.module.ts
client/src/app/app.component.html
client/src/app/app.component.ts
client/src/app/app.module.ts
client/src/app/login/login.component.html
client/src/app/modal/instance-config-warning-modal.component.html [new file with mode: 0644]
client/src/app/modal/instance-config-warning-modal.component.scss [new file with mode: 0644]
client/src/app/modal/instance-config-warning-modal.component.ts [new file with mode: 0644]
client/src/app/modal/welcome-modal.component.html [new file with mode: 0644]
client/src/app/modal/welcome-modal.component.scss [new file with mode: 0644]
client/src/app/modal/welcome-modal.component.ts [new file with mode: 0644]
client/src/app/shared/angular/peertube-template.directive.ts
client/src/app/shared/forms/form-validators/custom-config-validators.service.ts
client/src/app/shared/forms/peertube-checkbox.component.html
client/src/app/shared/forms/peertube-checkbox.component.scss
client/src/app/shared/forms/peertube-checkbox.component.ts
client/src/app/shared/instance/feature-boolean.component.html [new file with mode: 0644]
client/src/app/shared/instance/feature-boolean.component.scss [new file with mode: 0644]
client/src/app/shared/instance/feature-boolean.component.ts [new file with mode: 0644]
client/src/app/shared/instance/instance-features-table.component.html
client/src/app/shared/instance/instance-features-table.component.scss
client/src/app/shared/instance/instance-features-table.component.ts
client/src/app/shared/instance/instance.service.ts
client/src/app/shared/misc/help.component.html
client/src/app/shared/misc/help.component.ts
client/src/app/shared/renderer/markdown.service.ts
client/src/app/shared/shared.module.ts
client/src/app/shared/user-subscription/remote-subscribe.component.html
client/src/app/shared/users/user-notification.model.ts
client/src/app/shared/users/user-notifications.component.html
client/src/app/shared/users/user.model.ts
client/src/app/shared/video/videos-selection.component.ts
client/src/app/videos/+video-edit/shared/video-edit.component.html
client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html
client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html
client/src/assets/images/framasoft.png [new file with mode: 0644]
client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts
client/src/assets/player/p2p-media-loader/redundancy-url-manager.ts
client/src/sass/include/_mixins.scss
client/src/sass/player/peertube-skin.scss
client/src/standalone/videos/embed.ts
client/yarn.lock
config/default.yaml
config/production.yaml.example
package.json
scripts/client-report.sh
scripts/generate-code-contributors.ts
server.ts
server/controllers/activitypub/client.ts
server/controllers/activitypub/inbox.ts
server/controllers/activitypub/outbox.ts
server/controllers/api/config.ts
server/controllers/api/search.ts
server/controllers/api/server/follows.ts
server/controllers/api/users/index.ts
server/controllers/api/users/me.ts
server/controllers/api/users/my-history.ts
server/controllers/api/users/my-notifications.ts
server/controllers/api/video-channel.ts
server/controllers/api/video-playlist.ts
server/controllers/api/videos/abuse.ts
server/controllers/api/videos/blacklist.ts
server/controllers/api/videos/captions.ts
server/controllers/api/videos/comment.ts
server/controllers/api/videos/import.ts
server/controllers/api/videos/index.ts
server/controllers/api/videos/ownership.ts
server/controllers/api/videos/rate.ts
server/controllers/api/videos/watching.ts
server/controllers/feeds.ts
server/controllers/services.ts
server/controllers/static.ts
server/controllers/webfinger.ts
server/helpers/activitypub.ts
server/helpers/actor.ts
server/helpers/captions-utils.ts
server/helpers/custom-jsonld-signature.ts
server/helpers/custom-validators/activitypub/actor.ts
server/helpers/custom-validators/plugins.ts
server/helpers/custom-validators/users.ts
server/helpers/custom-validators/video-ownership.ts
server/helpers/middlewares/accounts.ts
server/helpers/middlewares/video-abuses.ts
server/helpers/middlewares/video-captions.ts
server/helpers/middlewares/video-channels.ts
server/helpers/middlewares/video-playlists.ts
server/helpers/middlewares/videos.ts
server/helpers/peertube-crypto.ts
server/helpers/utils.ts
server/helpers/video.ts
server/helpers/webfinger.ts
server/initializers/config.ts
server/initializers/constants.ts
server/initializers/migrations/0425-nullable-actor-fields.ts [new file with mode: 0644]
server/initializers/migrations/0430-auto-follow-notification-setting.ts [new file with mode: 0644]
server/initializers/migrations/0435-user-modals.ts [new file with mode: 0644]
server/lib/activitypub/actor.ts
server/lib/activitypub/audience.ts
server/lib/activitypub/cache-file.ts
server/lib/activitypub/follow.ts [new file with mode: 0644]
server/lib/activitypub/playlist.ts
server/lib/activitypub/process/process-accept.ts
server/lib/activitypub/process/process-announce.ts
server/lib/activitypub/process/process-create.ts
server/lib/activitypub/process/process-delete.ts
server/lib/activitypub/process/process-dislike.ts
server/lib/activitypub/process/process-flag.ts
server/lib/activitypub/process/process-follow.ts
server/lib/activitypub/process/process-like.ts
server/lib/activitypub/process/process-reject.ts
server/lib/activitypub/process/process-undo.ts
server/lib/activitypub/process/process-update.ts
server/lib/activitypub/process/process-view.ts
server/lib/activitypub/process/process.ts
server/lib/activitypub/send/send-accept.ts
server/lib/activitypub/send/send-announce.ts
server/lib/activitypub/send/send-create.ts
server/lib/activitypub/send/send-delete.ts
server/lib/activitypub/send/send-dislike.ts
server/lib/activitypub/send/send-flag.ts
server/lib/activitypub/send/send-follow.ts
server/lib/activitypub/send/send-like.ts
server/lib/activitypub/send/send-reject.ts
server/lib/activitypub/send/send-undo.ts
server/lib/activitypub/send/send-update.ts
server/lib/activitypub/send/send-view.ts
server/lib/activitypub/send/utils.ts
server/lib/activitypub/share.ts
server/lib/activitypub/url.ts
server/lib/activitypub/video-comments.ts
server/lib/activitypub/video-rates.ts
server/lib/activitypub/videos.ts
server/lib/avatar.ts
server/lib/blocklist.ts
server/lib/client-html.ts
server/lib/emailer.ts
server/lib/hls.ts
server/lib/job-queue/handlers/activitypub-follow.ts
server/lib/job-queue/handlers/activitypub-http-fetcher.ts
server/lib/job-queue/handlers/utils/activitypub-http-utils.ts
server/lib/job-queue/handlers/video-file-import.ts
server/lib/job-queue/handlers/video-import.ts
server/lib/job-queue/handlers/video-transcoding.ts
server/lib/notifier.ts
server/lib/oauth-model.ts
server/lib/peertube-socket.ts
server/lib/plugins/plugin-manager.ts
server/lib/redundancy.ts
server/lib/schedulers/auto-follow-index-instances.ts [new file with mode: 0644]
server/lib/schedulers/videos-redundancy-scheduler.ts
server/lib/thumbnail.ts
server/lib/user.ts
server/lib/video-blacklist.ts
server/lib/video-channel.ts
server/lib/video-comment.ts
server/lib/video-playlist.ts
server/lib/video-transcoding.ts
server/middlewares/activitypub.ts
server/middlewares/validators/follows.ts
server/middlewares/validators/redundancy.ts
server/middlewares/validators/user-notifications.ts
server/middlewares/validators/users.ts
server/middlewares/validators/videos/video-abuses.ts
server/middlewares/validators/videos/video-blacklist.ts
server/middlewares/validators/videos/video-captions.ts
server/middlewares/validators/videos/video-channels.ts
server/middlewares/validators/videos/video-comments.ts
server/middlewares/validators/videos/video-playlists.ts
server/middlewares/validators/videos/video-shares.ts
server/middlewares/validators/videos/videos.ts
server/middlewares/validators/webfinger.ts
server/models/account/account-blocklist.ts
server/models/account/account-video-rate.ts
server/models/account/account.ts
server/models/account/user-notification-setting.ts
server/models/account/user-notification.ts
server/models/account/user-video-history.ts
server/models/account/user.ts
server/models/activitypub/actor-follow.ts
server/models/activitypub/actor.ts
server/models/avatar/avatar.ts
server/models/oauth/oauth-token.ts
server/models/redundancy/video-redundancy.ts
server/models/server/plugin.ts
server/models/server/server-blocklist.ts
server/models/server/server.ts
server/models/video/schedule-video-update.ts
server/models/video/tag.ts
server/models/video/video-abuse.ts
server/models/video/video-blacklist.ts
server/models/video/video-caption.ts
server/models/video/video-change-ownership.ts
server/models/video/video-channel.ts
server/models/video/video-comment.ts
server/models/video/video-file.ts
server/models/video/video-format-utils.ts
server/models/video/video-import.ts
server/models/video/video-playlist-element.ts
server/models/video/video-playlist.ts
server/models/video/video-share.ts
server/models/video/video-streaming-playlist.ts
server/models/video/video.ts
server/tests/api/activitypub/helpers.ts
server/tests/api/check-params/config.ts
server/tests/api/check-params/user-notifications.ts
server/tests/api/check-params/users.ts
server/tests/api/notifications/user-notifications.ts
server/tests/api/search/search-videos.ts
server/tests/api/server/auto-follows.ts [new file with mode: 0644]
server/tests/api/server/config.ts
server/tests/api/server/index.ts
server/tests/api/users/users.ts
server/tests/api/videos/video-abuse.ts
server/tests/api/videos/video-change-ownership.ts
server/tools/cli.ts
server/tools/peertube-import-videos.ts
server/typings/activitypub-processor.model.ts
server/typings/express.ts
server/typings/models/account/account-blocklist.ts [new file with mode: 0644]
server/typings/models/account/account.ts [new file with mode: 0644]
server/typings/models/account/actor-follow.ts [new file with mode: 0644]
server/typings/models/account/actor.ts [new file with mode: 0644]
server/typings/models/account/avatar.ts [new file with mode: 0644]
server/typings/models/account/index.d.ts [new file with mode: 0644]
server/typings/models/actor-follow.ts [deleted file]
server/typings/models/actor.ts [deleted file]
server/typings/models/index.d.ts
server/typings/models/oauth/index.d.ts [new file with mode: 0644]
server/typings/models/oauth/oauth-client.ts [new file with mode: 0644]
server/typings/models/oauth/oauth-token.ts [new file with mode: 0644]
server/typings/models/server/index.d.ts [new file with mode: 0644]
server/typings/models/server/plugin.ts [new file with mode: 0644]
server/typings/models/server/server-blocklist.ts [new file with mode: 0644]
server/typings/models/server/server.ts [new file with mode: 0644]
server/typings/models/user/index.d.ts [new file with mode: 0644]
server/typings/models/user/user-notification-setting.ts [new file with mode: 0644]
server/typings/models/user/user-notification.ts [new file with mode: 0644]
server/typings/models/user/user-video-history.ts [new file with mode: 0644]
server/typings/models/user/user.ts [new file with mode: 0644]
server/typings/models/video-share.ts [deleted file]
server/typings/models/video/index.d.ts [new file with mode: 0644]
server/typings/models/video/schedule-video-update.ts [new file with mode: 0644]
server/typings/models/video/tag.ts [new file with mode: 0644]
server/typings/models/video/thumbnail.ts [new file with mode: 0644]
server/typings/models/video/video-abuse.ts [new file with mode: 0644]
server/typings/models/video/video-blacklist.ts [new file with mode: 0644]
server/typings/models/video/video-caption.ts [new file with mode: 0644]
server/typings/models/video/video-change-ownership.ts [new file with mode: 0644]
server/typings/models/video/video-channels.ts [new file with mode: 0644]
server/typings/models/video/video-comment.ts [new file with mode: 0644]
server/typings/models/video/video-file.ts [new file with mode: 0644]
server/typings/models/video/video-import.ts [new file with mode: 0644]
server/typings/models/video/video-playlist-element.ts [new file with mode: 0644]
server/typings/models/video/video-playlist.ts [new file with mode: 0644]
server/typings/models/video/video-rate.ts [new file with mode: 0644]
server/typings/models/video/video-redundancy.ts [new file with mode: 0644]
server/typings/models/video/video-share.ts [new file with mode: 0644]
server/typings/models/video/video-streaming-playlist.ts [new file with mode: 0644]
server/typings/models/video/video.ts [new file with mode: 0644]
server/typings/utils.ts
shared/extra-utils/index.ts
shared/extra-utils/instances-index/mock-instances-index.ts [new file with mode: 0644]
shared/extra-utils/server/config.ts
shared/extra-utils/users/user-notifications.ts
shared/extra-utils/users/users.ts
shared/models/activitypub/activity.ts
shared/models/activitypub/objects/video-abuse-object.ts
shared/models/i18n/i18n.ts
shared/models/server/about.model.ts
shared/models/server/custom-config.model.ts
shared/models/users/user-notification-setting.model.ts
shared/models/users/user-notification.model.ts
shared/models/users/user-update-me.model.ts
shared/models/users/user.model.ts
support/doc/api/openapi.yaml
support/doc/tools.md
tsconfig.json
yarn.lock

index 1b8ef737052ccb910cd8df3361c52cc2d3d043d5..09719974d86c7a050e83fc389bc86fdd5b34c31b 100644 (file)
@@ -1,4 +1,4 @@
-# Code
+# Code contributors
 
  * [Chocobozzz](https://github.com/Chocobozzz)
  * [rigelk](https://github.com/rigelk)
@@ -8,10 +8,12 @@
  * [Jorropo](https://github.com/Jorropo)
  * [buoyantair](https://github.com/buoyantair)
  * [bnjbvr](https://github.com/bnjbvr)
+ * [frankstrater](https://github.com/frankstrater)
  * [jankeromnes](https://github.com/jankeromnes)
  * [lucas-dclrcq](https://github.com/lucas-dclrcq)
- * [DavidLibeau](https://github.com/DavidLibeau)
  * [JohnXLivingston](https://github.com/JohnXLivingston)
+ * [DavidLibeau](https://github.com/DavidLibeau)
+ * [fflorent](https://github.com/fflorent)
  * [kaiyou](https://github.com/kaiyou)
  * [ldidry](https://github.com/ldidry)
  * [McFlat](https://github.com/McFlat)
  * [NassimBounouas](https://github.com/NassimBounouas)
  * [thomaskuntzz](https://github.com/thomaskuntzz)
  * [rezonant](https://github.com/rezonant)
+ * [Wirebrass](https://github.com/Wirebrass)
  * [clementbrizard](https://github.com/clementbrizard)
  * [LecygneNoir](https://github.com/LecygneNoir)
  * [okhin](https://github.com/okhin)
  * [daftaupe](https://github.com/daftaupe)
  * [tcitworld](https://github.com/tcitworld)
- * [fflorent](https://github.com/fflorent)
  * [dedesite](https://github.com/dedesite)
  * [Nautigsam](https://github.com/Nautigsam)
  * [scanlime](https://github.com/scanlime)
  * [am97](https://github.com/am97)
  * [dadall](https://github.com/dadall)
  * [jonathanraes](https://github.com/jonathanraes)
- * [Wirebrass](https://github.com/Wirebrass)
  * [yohanboniface](https://github.com/yohanboniface)
  * [anoadragon453](https://github.com/anoadragon453)
  * [auberanger](https://github.com/auberanger)
  * [darnuria](https://github.com/darnuria)
  * [rhaamo](https://github.com/rhaamo)
  * [mrflos](https://github.com/mrflos)
+ * [Yetangitu](https://github.com/Yetangitu)
  * [jocelynj](https://github.com/jocelynj)
  * [lucaspontoexe](https://github.com/lucaspontoexe)
  * [flyingrub](https://github.com/flyingrub)
@@ -57,6 +59,7 @@
  * [Anton-Latukha](https://github.com/Anton-Latukha)
  * [noplanman](https://github.com/noplanman)
  * [austinheap](https://github.com/austinheap)
+ * [BO41](https://github.com/BO41)
  * [benabbottnz](https://github.com/benabbottnz)
  * [ewft](https://github.com/ewft)
  * [bradsk88](https://github.com/bradsk88)
@@ -67,7 +70,6 @@
  * [ebrehault](https://github.com/ebrehault)
  * [DatBewar](https://github.com/DatBewar)
  * [ReK2Fernandez](https://github.com/ReK2Fernandez)
- * [Yetangitu](https://github.com/Yetangitu)
  * [grizio](https://github.com/grizio)
  * [Glandos](https://github.com/Glandos)
  * [lanodan](https://github.com/lanodan)
@@ -80,6 +82,7 @@
  * [pichouk](https://github.com/pichouk)
  * [LeoMouyna](https://github.com/LeoMouyna)
  * [LiPeK](https://github.com/LiPeK)
+ * [Findus23](https://github.com/Findus23)
  * [zapashcanon](https://github.com/zapashcanon)
  * [mart-e](https://github.com/mart-e)
  * [0mp](https://github.com/0mp)
@@ -95,6 +98,7 @@
  * [quentinDupont](https://github.com/quentinDupont)
  * [Quenty31](https://github.com/Quenty31)
  * [sundowndev](https://github.com/sundowndev)
+ * [robinkooli](https://github.com/robinkooli)
  * [sesn](https://github.com/sesn)
  * [ALSai](https://github.com/ALSai)
  * [Simounet](https://github.com/Simounet)
  * [FrozenDroid](https://github.com/FrozenDroid)
  * [fallen](https://github.com/fallen)
  * [melongbob](https://github.com/melongbob)
- * [Zig-03](https://github.com/Zig-03)
  * [anmol26s](https://github.com/anmol26s)
  * [imbsky](https://github.com/imbsky)
  * [ctlaltdefeat](https://github.com/ctlaltdefeat)
  * [jomo](https://github.com/jomo)
  * [libertysoft3](https://github.com/libertysoft3)
  * [lsde](https://github.com/lsde)
- * [memoryboxes](https://github.com/memoryboxes)
+ * [brain-zhang](https://github.com/brain-zhang)
  * [norrist](https://github.com/norrist)
  * [osauzet](https://github.com/osauzet)
  * [SansPseudoFix](https://github.com/SansPseudoFix)
  * [ewasion](https://github.com/ewasion)
 
 
-# Translations
+# Translation contributors
 
  * [abdhessuk](https://trad.framasoft.org/zanata/profile/view/abdhessuk)
  * [abidin24](https://trad.framasoft.org/zanata/profile/view/abidin24)
  * [abidin24](https://trad.framasoft.org/zanata/profile/view/abidin24)
  * [aditoo](https://trad.framasoft.org/zanata/profile/view/aditoo)
  * [alidemirtas](https://trad.framasoft.org/zanata/profile/view/alidemirtas)
+ * [anastasia](https://trad.framasoft.org/zanata/profile/view/anastasia)
  * [ariasuni](https://trad.framasoft.org/zanata/profile/view/ariasuni)
  * [autom](https://trad.framasoft.org/zanata/profile/view/autom)
  * [balaji](https://trad.framasoft.org/zanata/profile/view/balaji)
  * [bristow](https://trad.framasoft.org/zanata/profile/view/bristow)
  * [butterflyoffire](https://trad.framasoft.org/zanata/profile/view/butterflyoffire)
  * [c0dr](https://trad.framasoft.org/zanata/profile/view/c0dr)
+ * [canony](https://trad.framasoft.org/zanata/profile/view/canony)
  * [cat](https://trad.framasoft.org/zanata/profile/view/cat)
  * [chocobozzz](https://trad.framasoft.org/zanata/profile/view/chocobozzz)
  * [clerie](https://trad.framasoft.org/zanata/profile/view/clerie)
  * [curupira](https://trad.framasoft.org/zanata/profile/view/curupira)
  * [dhsets](https://trad.framasoft.org/zanata/profile/view/dhsets)
+ * [dibek](https://trad.framasoft.org/zanata/profile/view/dibek)
  * [digitalkiller](https://trad.framasoft.org/zanata/profile/view/digitalkiller)
  * [dwsage](https://trad.framasoft.org/zanata/profile/view/dwsage)
+ * [fkohrt](https://trad.framasoft.org/zanata/profile/view/fkohrt)
  * [flauta](https://trad.framasoft.org/zanata/profile/view/flauta)
  * [frankstrater](https://trad.framasoft.org/zanata/profile/view/frankstrater)
  * [gillux](https://trad.framasoft.org/zanata/profile/view/gillux)
  * [jhertel](https://trad.framasoft.org/zanata/profile/view/jhertel)
  * [joss2lyon](https://trad.framasoft.org/zanata/profile/view/joss2lyon)
  * [kekkotranslates](https://trad.framasoft.org/zanata/profile/view/kekkotranslates)
+ * [kingu](https://trad.framasoft.org/zanata/profile/view/kingu)
  * [kittybecca](https://trad.framasoft.org/zanata/profile/view/kittybecca)
+ * [kousha](https://trad.framasoft.org/zanata/profile/view/kousha)
  * [krkk](https://trad.framasoft.org/zanata/profile/view/krkk)
+ * [lapor](https://trad.framasoft.org/zanata/profile/view/lapor)
  * [laufor](https://trad.framasoft.org/zanata/profile/view/laufor)
  * [leeroyepold48](https://trad.framasoft.org/zanata/profile/view/leeroyepold48)
  * [lstamellos](https://trad.framasoft.org/zanata/profile/view/lstamellos)
  * [mablr](https://trad.framasoft.org/zanata/profile/view/mablr)
  * [marcinmalecki](https://trad.framasoft.org/zanata/profile/view/marcinmalecki)
  * [matograine](https://trad.framasoft.org/zanata/profile/view/matograine)
+ * [mayana](https://trad.framasoft.org/zanata/profile/view/mayana)
  * [mikeorlov](https://trad.framasoft.org/zanata/profile/view/mikeorlov)
  * [nin](https://trad.framasoft.org/zanata/profile/view/nin)
+ * [noncommutativegeo](https://trad.framasoft.org/zanata/profile/view/noncommutativegeo)
  * [norbipeti](https://trad.framasoft.org/zanata/profile/view/norbipeti)
+ * [nvivant](https://trad.framasoft.org/zanata/profile/view/nvivant)
+ * [osoitz](https://trad.framasoft.org/zanata/profile/view/osoitz)
+ * [ppnplus](https://trad.framasoft.org/zanata/profile/view/ppnplus)
  * [predatorix](https://trad.framasoft.org/zanata/profile/view/predatorix)
  * [quentin](https://trad.framasoft.org/zanata/profile/view/quentin)
  * [quentind](https://trad.framasoft.org/zanata/profile/view/quentind)
  * [robin](https://trad.framasoft.org/zanata/profile/view/robin)
  * [rond](https://trad.framasoft.org/zanata/profile/view/rond)
  * [s8321414](https://trad.framasoft.org/zanata/profile/view/s8321414)
+ * [sato_ss](https://trad.framasoft.org/zanata/profile/view/sato_ss)
  * [secreet](https://trad.framasoft.org/zanata/profile/view/secreet)
+ * [sercom_kc](https://trad.framasoft.org/zanata/profile/view/sercom_kc)
  * [severo](https://trad.framasoft.org/zanata/profile/view/severo)
  * [silkevicious](https://trad.framasoft.org/zanata/profile/view/silkevicious)
  * [sporiff](https://trad.framasoft.org/zanata/profile/view/sporiff)
  * [tekuteku](https://trad.framasoft.org/zanata/profile/view/tekuteku)
+ * [thecatjustmeow](https://trad.framasoft.org/zanata/profile/view/thecatjustmeow)
  * [tirifto](https://trad.framasoft.org/zanata/profile/view/tirifto)
  * [tmota](https://trad.framasoft.org/zanata/profile/view/tmota)
  * [tuxayo](https://trad.framasoft.org/zanata/profile/view/tuxayo)
index 29478b085faf7d2c8228cc8dfb2a10771191ea21..5ed7d5b4c3deff025c1cc00873d854231c2ee50f 100644 (file)
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@ Be part of a network of multiple small federated, interoperable video hosting pr
 
 <p align="center">
   <a href="https://framasoft.org">
-    <img width="150px" src="http://lutim.cpy.re/Prd3ci7G.png" alt="Framasoft logo"/>
+    <img width="150px" src="https://lutim.cpy.re/FeRgHH8r.png" alt="Framasoft logo"/>
   </a>
 </p>
 
@@ -182,7 +182,7 @@ See the [architecture blueprint](https://docs.joinpeertube.org/#/contribute-arch
 
 See our REST API documentation:
   * OpenAPI 3.0.0 schema: [/support/doc/api/openapi.yaml](/support/doc/api/openapi.yaml)
-  * Spec explorer: [docs.joinpeertube.org/#/api-rest-reference.html](https://docs.joinpeertube.org/#/api-rest-reference.html)
+  * Spec explorer: [docs.joinpeertube.org/api-rest-reference.html](https://docs.joinpeertube.org/api-rest-reference.html)
 
 See our [ActivityPub documentation](https://docs.joinpeertube.org/#/api-activitypub).
 
index ba4f5000da84d96cb47dc4c92bb83c1781e4e6f0..39fd634340a80528651de54ae7e2d6ed4d80adc4 100644 (file)
     "ngx-pipes": "^2.1.7",
     "node-sass": "^4.9.3",
     "npm-font-source-sans-pro": "^1.0.2",
-    "p2p-media-loader-hlsjs": "^0.6.1",
+    "p2p-media-loader-hlsjs": "^0.6.2",
     "path-browserify": "^1.0.0",
     "primeng": "^8.0.2",
     "process": "^0.11.10",
index 7c27ec7607f5cd277602df33322280228898be26..25d41674079cecc99860a0a99015890c7c7bbfd0 100644 (file)
@@ -1,32 +1,97 @@
 <div class="row">
   <div class="col-md-12 col-xl-6">
+
     <div class="about-instance-title">
-      <div i18n>About {{ instanceName }} instance</div>
+      <div i18n class="title">About {{ instanceName }} instance</div>
 
-      <div *ngIf="isContactFormEnabled" (click)="openContactModal()" i18n role="button" class="contact-admin">Contact administrator</div>
+      <div i18n *ngIf="isContactFormEnabled" (click)="openContactModal()" role="button" class="contact-admin">Contact administrator</div>
+    </div>
+
+    <div class="block instance-badges">
+      <span *ngFor="let category of categories" class="badge badge-primary category">{{ category }}</span>
+
+      <span *ngFor="let language of languages" class="badge badge-secondary language">{{ language }}</span>
     </div>
 
     <div class="short-description">
-      <div>{{ shortDescription }}</div>
+      <div class="block short-description">{{ shortDescription }}</div>
+
+      <div i18n *ngIf="isNSFW" class="block dedicated-to-nsfw">This instance is dedicated to sensitive/NSFW content.</div>
+    </div>
+
+    <div i18n class="middle-title" *ngIf="html.administrator || maintenanceLifetime || businessModel">
+      Administrators & sustainability
+    </div>
+
+    <div class="block administrator" *ngIf="html.administrator">
+      <div i18n class="section-title">Who we are</div>
 
-      <div *ngIf="isNSFW" class="dedicated-to-nsfw">This instance is dedicated to sensitive/NSFW content.</div>
+      <div [innerHTML]="html.administrator"></div>
     </div>
 
-    <div class="description">
+    <div class="block creation-reason" *ngIf="creationReason">
+      <div i18n class="section-title">Why we created this instance</div>
+
+      <p>{{ creationReason }}</p>
+    </div>
+
+    <div class="block maintenance-lifetime" *ngIf="maintenanceLifetime">
+      <div i18n class="section-title">How long we plan to maintain this instance</div>
+
+      <p>{{ maintenanceLifetime }}</p>
+    </div>
+
+    <div class="block business-model" *ngIf="businessModel">
+      <div i18n class="section-title">How we will pay this instance</div>
+
+      <p>{{ businessModel }}</p>
+    </div>
+
+    <div i18n class="middle-title" *ngIf="html.description">
+      Information
+    </div>
+
+    <div class="block description">
       <div i18n class="section-title">Description</div>
 
-      <div [innerHTML]="descriptionHTML"></div>
+      <div [innerHTML]="html.description"></div>
+    </div>
+
+    <div i18n class="middle-title" *ngIf="html.moderationInformation || html.codeOfConduct || html.terms">
+      Moderation
+    </div>
+
+    <div class="block moderation-information" *ngIf="html.moderationInformation">
+      <div i18n class="section-title">Moderation information</div>
+
+      <div [innerHTML]="html.moderationInformation"></div>
     </div>
 
-    <div class="terms" id="terms-section">
+    <div class="block code-of-conduct" *ngIf="html.codeOfConduct">
+      <div i18n class="section-title">Code of conduct</div>
+
+      <div [innerHTML]="html.codeOfConduct"></div>
+    </div>
+
+    <div class="block terms">
       <div i18n class="section-title">Terms</div>
 
-      <div [innerHTML]="termsHTML"></div>
+      <div [innerHTML]="html.terms"></div>
+    </div>
+
+    <div i18n class="middle-title" *ngIf="html.hardwareInformation">
+      Other information
+    </div>
+
+    <div class="block hardware-information">
+      <div i18n class="section-title">Hardware information</div>
+
+      <div [innerHTML]="html.hardwareInformation"></div>
     </div>
   </div>
 
   <div class="col-md-12 col-xl-6">
-    <label>Features found on this instance</label>
+    <label i18n>Features found on this instance</label>
     <my-instance-features-table></my-instance-features-table>
   </div>
 </div>
index 0296ae8e9082f39bde640b576934e2fd176f5727..909ae5c21330078da2f5f0a14b1443973ce9df0a 100644 (file)
@@ -5,13 +5,12 @@
   display: flex;
   justify-content: space-between;
 
-  & > div {
+  .title {
     font-size: 20px;
-    font-weight: bold;
-    margin-bottom: 15px;
+    font-weight: $font-semibold;
   }
 
-  & > .contact-admin {
+  .contact-admin {
     @include peertube-button;
     @include orange-button;
 
   }
 }
 
+.instance-badges {
+  font-size: 16px;
+
+  .badge {
+    font-size: 12px;
+    font-weight: $font-semibold;
+    margin-right: 5px;
+
+    &.category {
+      background-color: var(--mainColor);
+    }
+  }
+}
+
 .section-title {
   font-weight: $font-semibold;
-  font-size: 20px;
+  font-size: 16px;
   margin-bottom: 5px;
+  display: flex;
+  align-items: center;
+}
+
+.middle-title {
+  @include in-content-small-title;
+
+  margin-top: 45px;
+  margin-bottom: 25px;
 }
 
-.short-description, .description, .terms, .signup {
+.block {
   margin-bottom: 30px;
+  font-size: 15px;
 }
 
 .short-description .dedicated-to-nsfw {
index a5204de275291d87bbfb07e06bbd1ff9c21d6773..16ccae2e27c6a879905b3e44692863ff6e5abe4c 100644 (file)
@@ -4,6 +4,8 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
 import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component'
 import { InstanceService } from '@app/shared/instance/instance.service'
 import { MarkdownService } from '@app/shared/renderer'
+import { forkJoin } from 'rxjs'
+import { first } from 'rxjs/operators'
 
 @Component({
   selector: 'my-about-instance',
@@ -14,8 +16,22 @@ export class AboutInstanceComponent implements OnInit {
   @ViewChild('contactAdminModal', { static: true }) contactAdminModal: ContactAdminModalComponent
 
   shortDescription = ''
-  descriptionHTML = ''
-  termsHTML = ''
+
+  html = {
+    description: '',
+    terms: '',
+    codeOfConduct: '',
+    moderationInformation: '',
+    administrator: '',
+    hardwareInformation: ''
+  }
+
+  creationReason = ''
+  maintenanceLifetime = ''
+  businessModel = ''
+
+  languages: string[] = []
+  categories: string[] = []
 
   constructor (
     private notifier: Notifier,
@@ -38,21 +54,30 @@ export class AboutInstanceComponent implements OnInit {
   }
 
   ngOnInit () {
-    this.instanceService.getAbout()
-      .subscribe(
-        async res => {
-          this.shortDescription = res.instance.shortDescription
+    forkJoin([
+      this.instanceService.getAbout(),
+      this.serverService.localeObservable.pipe(first()),
+      this.serverService.videoLanguagesLoaded.pipe(first()),
+      this.serverService.videoCategoriesLoaded.pipe(first())
+    ]).subscribe(
+      async ([ about, translations ]) => {
+        this.shortDescription = about.instance.shortDescription
 
-          this.descriptionHTML = await this.markdownService.textMarkdownToHTML(res.instance.description)
-          this.termsHTML = await this.markdownService.textMarkdownToHTML(res.instance.terms)
-        },
+        this.creationReason = about.instance.creationReason
+        this.maintenanceLifetime = about.instance.maintenanceLifetime
+        this.businessModel = about.instance.businessModel
 
-        () => this.notifier.error(this.i18n('Cannot get about information from server'))
-      )
+        this.html = await this.instanceService.buildHtml(about)
+
+        this.languages = this.instanceService.buildTranslatedLanguages(about, translations)
+        this.categories = this.instanceService.buildTranslatedCategories(about, translations)
+      },
+
+      () => this.notifier.error(this.i18n('Cannot get about information from server'))
+    )
   }
 
   openContactModal () {
     return this.contactAdminModal.show()
   }
-
 }
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 (file)
index 0000000..997a6a3
--- /dev/null
@@ -0,0 +1,13 @@
+<h3 i18n class="section-title">Who made this software?</h3>
+
+<p align="center">
+  <strong>Developed with &#10084; by <a target="_blank" rel="noopener noreferrer" href="https://framasoft.org">Framasoft</a></strong>
+</p>
+
+<p align="center">
+  <a target="_blank" rel="noopener noreferrer" href="https://framasoft.org">
+    <img width="150px" src="/client/assets/images/framasoft.png" alt="Framasoft logo"/>
+  </a>
+</p>
+
+<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 (file)
index 0000000..9c3b0a4
--- /dev/null
@@ -0,0 +1,15 @@
+@import '_variables';
+@import '_mixins';
+
+/deep/ h1 {
+  font-size: 1rem;
+}
+
+/deep/ ul {
+  padding: 0;
+
+  li {
+    display: inline-block;
+    margin-right: 10px;
+  }
+}
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 (file)
index 0000000..fa2c0da
--- /dev/null
@@ -0,0 +1,19 @@
+import { Component, OnInit } from '@angular/core'
+import { MarkdownService } from '@app/shared/renderer'
+
+@Component({
+  selector: 'my-about-peertube-contributors',
+  templateUrl: './about-peertube-contributors.component.html',
+  styleUrls: [ './about-peertube-contributors.component.scss' ]
+})
+export class AboutPeertubeContributorsComponent implements OnInit {
+  creditsHtml: string
+
+  private markdown = require('raw-loader!../../../../../CREDITS.md')
+
+  constructor (private markdownService: MarkdownService) { }
+
+  async ngOnInit () {
+    this.creditsHtml = await this.markdownService.completeMarkdownToHTML(this.markdown)
+  }
+}
index d3fc9a8288ce4662e0514a0d27a03ec4940d50df..423f7bce7a52cd88978db4af88f5269d86300b9b 100644 (file)
   </p>
 </div>
 
-<div id="p2p-privacy">
-  <h3 i18n class="section-title">P2P & Privacy</h3>
+<div class="privacy-contributors">
+  <my-about-peertube-contributors></my-about-peertube-contributors>
+
+  <div class="p2p-privacy">
+    <h3 i18n class="section-title">P2P & Privacy</h3>
+
+    <p i18n>
+      PeerTube uses the BitTorrent protocol to share bandwidth between users.
+      This implies that your IP address is stored in the instance's BitTorrent tracker as long as you download or watch the video.
+    </p>
+
+    <h6 i18n class="p2p-privacy-title">What are the consequences?</h6>
+
+    <p i18n>
+      In theory, someone with enough technical skills could create a script that tracks which IP is downloading which video.
+      In practice, this is much more difficult because:
+    </p>
+
+    <ul>
+      <li i18n>
+        An HTTP request has to be sent on each tracker for each video to spy.
+        If we want to spy all PeerTube's videos, we have to send as many requests as there are videos (so potentially a lot)
+      </li>
+
+      <li i18n>
+        For each request sent, the tracker returns random peers at a limited number.
+        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
+      </li>
+
+      <li i18n>
+        Those requests have to be sent regularly to know who starts/stops watching a video. It is easy to detect that kind of behaviour
+      </li>
+
+      <li i18n>
+        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
+      </li>
+
+      <li i18n>
+        The IP address is a vague information : usually, it regularly changes and can represent many persons or entities
+      </li>
+
+      <li i18n>
+        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.
+        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.
+        See <a href="https://github.com/yciabaud/webtorrent/blob/beps/bep_webrtc.rst">this document</a> for more information
+      </li>
+    </ul>
+
+    <p i18n>
+      The worst-case scenario of an average person spying on their friends is quite unlikely.
+      There are much more effective ways to get that kind of information.
+    </p>
+
+    <h6 i18n class="p2p-privacy-title">How does PeerTube compare with YouTube?</h6>
+
+    <p i18n>
+      The threats to privacy in YouTube are different from PeerTube's.
+      In YouTube's case, the platform gathers a huge amount of your personal information (not only your IP) to analyze them and track you.
+      Moreover, YouTube is owned by Google/Alphabet, a company that tracks you across many websites (via AdSense or Google Analytics).
+    </p>
+
+    <h6 i18n class="p2p-privacy-title">What can I do to limit the exposure of my IP address?</h6>
+
+    <p i18n>
+      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.
+      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.
+      Thinking that removing P2P from PeerTube will give you back anonymity doesn't make sense.
+    </p>
+
+    <h6 i18n class="p2p-privacy-title">What will be done to mitigate this problem?</h6>
+
+    <p i18n>
+      PeerTube is in its early stages, and want to deliver the best countermeasures possible by the time the stable is released.
+      In the meantime, we want to test different ideas related to this issue:
+    </p>
+
+    <ul>
+      <li i18n>Set a limit to the number of peers sent by the tracker</li>
+      <li i18n>Set a limit on the request frequency received by the tracker (being tested)</li>
+      <li i18n>Ring a bell if there are unusual requests (being tested)</li>
+      <li i18n>Disable P2P from the administration interface</li>
+      <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>
+    </ul>
+  </div>
 
-  <p i18n>
-    PeerTube uses the BitTorrent protocol to share bandwidth between users.
-    This implies that your IP address is stored in the instance's BitTorrent tracker as long as you download or watch the video.
-  </p>
-
-  <h6 i18n class="p2p-privacy-title">What are the consequences?</h6>
-
-  <p i18n>
-    In theory, someone with enough technical skills could create a script that tracks which IP is downloading which video.
-    In practice, this is much more difficult because:
-  </p>
-
-  <ul>
-    <li i18n>
-      An HTTP request has to be sent on each tracker for each video to spy.
-      If we want to spy all PeerTube's videos, we have to send as many requests as there are videos (so potentially a lot)
-    </li>
-
-    <li i18n>
-      For each request sent, the tracker returns random peers at a limited number.
-      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
-    </li>
-
-    <li i18n>
-      Those requests have to be sent regularly to know who starts/stops watching a video. It is easy to detect that kind of behaviour
-    </li>
-
-    <li i18n>
-      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
-    </li>
-
-    <li i18n>
-      The IP address is a vague information : usually, it regularly changes and can represent many persons or entities
-    </li>
-
-    <li i18n>
-      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.
-      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.
-      See <a href="https://github.com/yciabaud/webtorrent/blob/beps/bep_webrtc.rst">this document</a> for more information
-    </li>
-  </ul>
-
-  <p i18n>
-    The worst-case scenario of an average person spying on their friends is quite unlikely.
-    There are much more effective ways to get that kind of information.
-  </p>
-
-  <h6 i18n class="p2p-privacy-title">How does PeerTube compare with YouTube?</h6>
-
-  <p i18n>
-    The threats to privacy in YouTube are different from PeerTube's.
-    In YouTube's case, the platform gathers a huge amount of your personal information (not only your IP) to analyze them and track you.
-    Moreover, YouTube is owned by Google/Alphabet, a company that tracks you across many websites (via AdSense or Google Analytics).
-  </p>
-
-  <h6 i18n class="p2p-privacy-title">What can I do to limit the exposure of my IP address?</h6>
-
-  <p i18n>
-    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.
-    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.
-    Thinking that removing P2P from PeerTube will give you back anonymity doesn't make sense.
-  </p>
-
-  <h6 i18n class="p2p-privacy-title">What will be done to mitigate this problem?</h6>
-
-  <p i18n>
-    PeerTube is in its early stages, and want to deliver the best countermeasures possible by the time the stable is released.
-    In the meantime, we want to test different ideas related to this issue:
-  </p>
-
-  <ul>
-    <li i18n>Set a limit to the number of peers sent by the tracker</li>
-    <li i18n>Set a limit on the request frequency received by the tracker (being tested)</li>
-    <li i18n>Ring a bell if there are unusual requests (being tested)</li>
-    <li i18n>Disable P2P from the administration interface</li>
-    <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>
-  </ul>
 </div>
index 0d2e2bb68d65342919083a8b72723b7e9f021b3b..8fca53e90037b5756f089792e6b223349c378b4f 100644 (file)
@@ -2,12 +2,12 @@
 @import '_mixins';
 
 .about-peertube-title {
-  font-size: 25px;
-  font-weight: bold;
+  font-size: 20px;
+  font-weight: $font-semibold;
   margin-bottom: 15px;
 }
 
-.section-title {
+/deep/ .section-title {
   font-weight: $font-semibold;
   font-size: 20px;
   margin-bottom: 5px;
   margin-bottom: 30px;
 }
 
+.description,
+.p2p-privacy,
+my-about-peertube-contributors {
+  /deep/ {
+    p, li {
+      font-size: 15px;
+    }
+  }
+}
+
 .p2p-privacy-title {
   margin-top: 15px;
-}
\ No newline at end of file
+}
+
+.privacy-contributors {
+  display: flex;
+  flex-direction: row;
+
+  > div,
+  > my-about-peertube-contributors {
+    flex-basis: 100%;
+    display: block;
+  }
+
+  .p2p-privacy {
+    h6 {
+      font-size: 20px;
+    }
+  }
+
+  my-about-peertube-contributors {
+    margin: 0 40px 40px 0;
+  }
+
+  @media screen and (max-width: $small-view) {
+    flex-direction: column;
+  }
+}
index 49a7a52f848ad2b1813f5cc505c740eea54b339c..14bf76e55ac86da59720784bf06cf43d9377228c 100644 (file)
@@ -1,5 +1,4 @@
 import { NgModule } from '@angular/core'
-
 import { AboutRoutingModule } from './about-routing.module'
 import { AboutComponent } from './about.component'
 import { SharedModule } from '../shared'
@@ -7,6 +6,7 @@ import { AboutInstanceComponent } from '@app/+about/about-instance/about-instanc
 import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component'
 import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component'
 import { AboutFollowsComponent } from '@app/+about/about-follows/about-follows.component'
+import { AboutPeertubeContributorsComponent } from '@app/+about/about-peertube/about-peertube-contributors.component'
 
 @NgModule({
   imports: [
@@ -19,6 +19,7 @@ import { AboutFollowsComponent } from '@app/+about/about-follows/about-follows.c
     AboutInstanceComponent,
     AboutPeertubeComponent,
     AboutFollowsComponent,
+    AboutPeertubeContributorsComponent,
     ContactAdminModalComponent
   ],
 
index fe9d856d050161c5756419129ccd54f06ec9d281..54115055ad088a6f80d356e3bff5c650a0051ddc 100644 (file)
@@ -2,12 +2,13 @@
 
   <ngb-tabset class="root-tabset bootstrap">
 
-    <ngb-tab i18n-title title="Basic configuration">
+    <ngb-tab i18n-title title="Instance information">
       <ng-template ngbTabContent>
 
-        <div i18n class="inner-form-title">Instance</div>
-
         <ng-container formGroupName="instance">
+
+          <div i18n class="inner-form-title">Instance</div>
+
           <div class="form-group">
             <label i18n for="instanceName">Name</label>
             <input
@@ -20,7 +21,7 @@
           <div class="form-group">
             <label i18n for="instanceShortDescription">Short description</label>
             <textarea
-              id="instanceShortDescription" formControlName="shortDescription"
+              id="instanceShortDescription" formControlName="shortDescription" class="small"
               [ngClass]="{ 'input-error': formErrors['instance.shortDescription'] }"
             ></textarea>
             <div *ngIf="formErrors.instance.shortDescription" class="form-error">{{ formErrors.instance.shortDescription }}</div>
           </div>
 
           <div class="form-group">
-            <label i18n for="instanceTerms">Terms</label><my-help helpType="markdownText"></my-help>
-            <my-markdown-textarea
-              id="instanceTerms" formControlName="terms" textareaWidth="500px" [previewColumn]="true"
-              [ngClass]="{ 'input-error': formErrors['instance.terms'] }"
-            ></my-markdown-textarea>
-            <div *ngIf="formErrors.instance.terms" class="form-error">{{ formErrors.instance.terms }}</div>
+            <label i18n for="instanceCategories">Main instance categories</label>
+
+            <div>
+              <p-multiSelect
+                inputId="instanceCategories" [options]="categoryItems" formControlName="categories" showToggleAll="false"
+                [defaultLabel]="getDefaultCategoryLabel()" [selectedItemsLabel]="getSelectedCategoryLabel()"
+                emptyFilterMessage="No results found" i18n-emptyFilterMessage
+              ></p-multiSelect>
+            </div>
           </div>
 
           <div class="form-group">
-            <my-peertube-checkbox
-              inputName="instanceIsNSFW" formControlName="isNSFW"
-              i18n-labelText labelText="Dedicated to sensitive or NSFW content"
-              i18n-helpHtml helpHtml="Enabling it will allow other administrators to know that you are mainly federating sensitive content.<br /><br />
-              Moreover, the NSFW checkbox on video upload will be automatically checked by default."
-            ></my-peertube-checkbox>
+            <label i18n for="instanceLanguages">Main languages you/your moderators speak</label>
+
+            <div>
+              <p-multiSelect
+                inputId="instanceLanguages" [options]="languageItems" formControlName="languages" showToggleAll="false"
+                [defaultLabel]="getDefaultLanguageLabel()" [selectedItemsLabel]="getSelectedLanguageLabel()"
+                emptyFilterMessage="No results found" i18n-emptyFilterMessage
+              ></p-multiSelect>
+            </div>
           </div>
 
+          <div i18n class="inner-form-title">Moderation & NSFW</div>
+
           <div class="form-group">
-            <label i18n for="instanceDefaultClientRoute">Default client route</label>
-            <div class="peertube-select-container">
-              <select id="instanceDefaultClientRoute" formControlName="defaultClientRoute">
-                <option i18n value="/videos/overview">Videos Overview</option>
-                <option i18n value="/videos/trending">Videos Trending</option>
-                <option i18n value="/videos/recently-added">Videos Recently Added</option>
-                <option i18n value="/videos/local">Local videos</option>
-              </select>
-            </div>
-            <div *ngIf="formErrors.instance.defaultClientRoute" class="form-error">{{ formErrors.instance.defaultClientRoute }}</div>
+            <my-peertube-checkbox inputName="instanceIsNSFW" formControlName="isNSFW">
+              <ng-template ptTemplate="label">
+                <ng-container i18n>This instance is dedicated to sensitive or NSFW content</ng-container>
+              </ng-template>
+
+              <ng-template ptTemplate="help">
+                <ng-container i18n>
+                  Enabling it will allow other administrators to know that you are mainly federating sensitive content.<br /><br />
+                  Moreover, the NSFW checkbox on video upload will be automatically checked by default.
+                </ng-container>
+              </ng-template>
+            </my-peertube-checkbox>
           </div>
 
           <div class="form-group">
             <label i18n for="instanceDefaultNSFWPolicy">Policy on videos containing sensitive content</label>
-            <my-help
-              helpType="custom" i18n-customHtml
-              customHtml="With <strong>Do not list</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video."
-            ></my-help>
+
+            <my-help>
+              <ng-template ptTemplate="customHtml">
+                <ng-container i18n>
+                  With <strong>Do not list</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video.
+                </ng-container>
+              </ng-template>
+            </my-help>
 
             <div class="peertube-select-container">
               <select id="instanceDefaultNSFWPolicy" formControlName="defaultNSFWPolicy">
             </div>
             <div *ngIf="formErrors.instance.defaultNSFWPolicy" class="form-error">{{ formErrors.instance.defaultNSFWPolicy }}</div>
           </div>
+
+          <div class="form-group">
+            <label i18n for="instanceTerms">Terms</label><my-help helpType="markdownText"></my-help>
+            <my-markdown-textarea
+              id="instanceTerms" formControlName="terms" textareaWidth="500px" [previewColumn]="true"
+              [ngClass]="{ 'input-error': formErrors['instance.terms'] }"
+            ></my-markdown-textarea>
+            <div *ngIf="formErrors.instance.terms" class="form-error">{{ formErrors.instance.terms }}</div>
+          </div>
+
+          <div class="form-group">
+            <label i18n for="instanceCodeOfConduct">Code of conduct</label><my-help helpType="markdownText"></my-help>
+            <my-markdown-textarea
+              id="instanceCodeOfConduct" formControlName="codeOfConduct" textareaWidth="500px" [previewColumn]="true"
+              [ngClass]="{ 'input-error': formErrors['instance.codeOfConduct'] }"
+            ></my-markdown-textarea>
+            <div *ngIf="formErrors.instance.codeOfConduct" class="form-error">{{ formErrors.instance.codeOfConduct }}</div>
+          </div>
+
+          <div class="form-group">
+            <label i18n for="instanceModerationInformation">Moderation information</label><my-help helpType="markdownText"></my-help>
+            <div class="label-small-info">Who moderates the instance? What is the policy regarding NSFW videos? Political videos? etc</div>
+
+            <my-markdown-textarea
+              id="instanceModerationInformation" formControlName="moderationInformation" textareaWidth="500px" [previewColumn]="true"
+              [ngClass]="{ 'input-error': formErrors['instance.moderationInformation'] }"
+            ></my-markdown-textarea>
+            <div *ngIf="formErrors.instance.moderationInformation" class="form-error">{{ formErrors.instance.moderationInformation }}</div>
+          </div>
+
+          <div i18n class="inner-form-title">You and your instance</div>
+
+          <div class="form-group">
+            <label i18n for="instanceAdministrator">Who is behind the instance?</label>
+            <div class="label-small-info">A single person? A non profit? A company?</div>
+
+            <my-markdown-textarea
+              id="instanceAdministrator" formControlName="administrator" textareaWidth="500px" textareaHeight="75px" [previewColumn]="true"
+              [classes]="{ 'input-error': formErrors['instance.administrator'] }"
+            ></my-markdown-textarea>
+
+            <div *ngIf="formErrors.instance.administrator" class="form-error">{{ formErrors.instance.administrator }}</div>
+          </div>
+
+          <div class="form-group">
+            <label i18n for="instanceCreationReason">Why did you create this instance?</label>
+            <div class="label-small-info">To share your personal videos? To open registrations and allow people to upload what they want?</div>
+
+            <textarea
+              id="instanceCreationReason" formControlName="creationReason" class="small"
+              [ngClass]="{ 'input-error': formErrors['instance.creationReason'] }"
+            ></textarea>
+            <div *ngIf="formErrors.instance.creationReason" class="form-error">{{ formErrors.instance.creationReason }}</div>
+          </div>
+
+          <div class="form-group">
+            <label i18n for="instanceMaintenanceLifetime">How long do you plan to maintain this instance?</label>
+            <div class="label-small-info">It's important to know for users who want to register on your instance</div>
+
+            <textarea
+              id="instanceMaintenanceLifetime" formControlName="maintenanceLifetime" class="small"
+              [ngClass]="{ 'input-error': formErrors['instance.maintenanceLifetime'] }"
+            ></textarea>
+            <div *ngIf="formErrors.instance.maintenanceLifetime" class="form-error">{{ formErrors.instance.maintenanceLifetime }}</div>
+          </div>
+
+          <div class="form-group">
+            <label i18n for="instanceBusinessModel">How will you pay the PeerTube instance server?</label>
+            <div class="label-small-info">With you own funds? With users donations? Advertising?</div>
+
+            <textarea
+              id="instanceBusinessModel" formControlName="businessModel" class="small"
+              [ngClass]="{ 'input-error': formErrors['instance.businessModel'] }"
+            ></textarea>
+            <div *ngIf="formErrors.instance.businessModel" class="form-error">{{ formErrors.instance.businessModel }}</div>
+          </div>
+
+          <div i18n class="inner-form-title">Other information</div>
+
+          <div class="form-group">
+            <label i18n for="instanceHardwareInformation">On what server/hardware the instance runs?</label>
+            <div class="label-small-info">2vCore 2GB RAM/or directly the link to the server you rent etc</div>
+
+            <my-markdown-textarea
+              id="instanceHardwareInformation" formControlName="hardwareInformation" textareaWidth="500px" textareaHeight="75px" [previewColumn]="true"
+              [classes]="{ 'input-error': formErrors['instance.hardwareInformation'] }"
+            ></my-markdown-textarea>
+
+            <div *ngIf="formErrors.instance.hardwareInformation" class="form-error">{{ formErrors.instance.hardwareInformation }}</div>
+          </div>
+
         </ng-container>
+      </ng-template>
+    </ngb-tab>
 
+    <ngb-tab i18n-title title="Basic configuration">
+      <ng-template ngbTabContent>
 
-        <div i18n class="inner-form-title">Theme</div>
+        <div i18n class="inner-form-title">Theme & Default route</div>
 
         <ng-container formGroupName="theme">
           <div class="form-group">
         </ng-container>
 
 
+        <div class="form-group" formGroupName="instance">
+          <label i18n for="instanceDefaultClientRoute">Default client route</label>
+          <div class="peertube-select-container">
+            <select id="instanceDefaultClientRoute" formControlName="defaultClientRoute">
+              <option i18n value="/videos/overview">Videos Discover</option>
+              <option i18n value="/videos/trending">Videos Trending</option>
+              <option i18n value="/videos/recently-added">Videos Recently Added</option>
+              <option i18n value="/videos/local">Local videos</option>
+            </select>
+          </div>
+          <div *ngIf="formErrors.instance.defaultClientRoute" class="form-error">{{ formErrors.instance.defaultClientRoute }}</div>
+        </div>
+
         <div i18n class="inner-form-title">Signup</div>
 
         <ng-container formGroupName="signup">
           </ng-container>
         </ng-container>
 
+        <div i18n class="inner-form-title">Instance followings</div>
+
+        <ng-container formGroupName="followings">
+          <ng-container formGroupName="instance">
+
+            <ng-container formGroupName="autoFollowBack">
+              <div class="form-group">
+                <my-peertube-checkbox
+                  inputName="followingsInstanceAutoFollowBackEnabled" formControlName="enabled"
+                  i18n-labelText labelText="Automatically follow other instances that follow you"
+                ></my-peertube-checkbox>
+              </div>
+            </ng-container>
+
+            <ng-container formGroupName="autoFollowIndex">
+              <div class="form-group">
+                <my-peertube-checkbox
+                  inputName="followingsInstanceAutoFollowIndexEnabled" formControlName="enabled"
+                  i18n-labelText labelText="Automatically follow instance of the public index (below)"
+                ></my-peertube-checkbox>
+              </div>
+
+              <div class="form-group">
+                <label i18n for="followingsInstanceAutoFollowIndexUrl">Index URL</label>
+                <input
+                  type="text" id="followingsInstanceAutoFollowIndexUrl"
+                  formControlName="indexUrl" [ngClass]="{ 'input-error': formErrors['followings.instance.autoFollowIndex.indexUrl'] }"
+                >
+                <div *ngIf="formErrors.followings.instance.autoFollowIndex.indexUrl" class="form-error">{{ formErrors.followings.instance.autoFollowIndex.indexUrl }}</div>
+              </div>
+
+            </ng-container>
+          </ng-container>
+        </ng-container>
+
 
         <div i18n class="inner-form-title">Administrator</div>
 
 
             <div class="form-group">
               <label i18n for="signupLimit">Your Twitter username</label>
-              <my-help
-                helpType="custom" i18n-customHtml
-                customHtml="Indicates the Twitter account for the website or platform on which the content was published."
-              ></my-help>
+
+              <my-help>
+                <ng-template ptTemplate="customHtml">
+                  <ng-container i18n>Indicates the Twitter account for the website or platform on which the content was published.</ng-container>
+                </ng-template>
+              </my-help>
+
               <input
                 type="text" id="servicesTwitterUsername"
                 formControlName="username" [ngClass]="{ 'input-error': formErrors['services.twitter.username'] }"
             </div>
 
             <div class="form-group">
-              <my-peertube-checkbox
-                inputName="servicesTwitterWhitelisted" formControlName="whitelisted"
-                i18n-labelText labelText="Instance whitelisted by Twitter"
-                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 />
-        If the instance is not whitelisted, we use an image link card that will redirect on your PeerTube instance.<br /><br />
-        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."
-              ></my-peertube-checkbox>
+              <my-peertube-checkbox inputName="servicesTwitterWhitelisted" formControlName="whitelisted">
+                <ng-template ptTemplate="label">
+                  <ng-container i18n>Instance whitelisted by Twitter</ng-container>
+                </ng-template>
+
+                <ng-template ptTemplate="help">
+                  <ng-container i18n>
+                    If your instance is whitelisted by Twitter, a video player will be embedded in the Twitter feed on PeerTube video share.<br />
+                    If the instance is not whitelisted, we use an image link card that will redirect on your PeerTube instance.<br /><br />
+                    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.
+                  </ng-container>
+                </ng-template>
+              </my-peertube-checkbox>
             </div>
 
           </ng-container>
 
         <ng-container formGroupName="transcoding">
           <div class="form-group">
-            <my-peertube-checkbox
-              inputName="transcodingEnabled" formControlName="enabled"
-              i18n-labelText labelText="Transcoding enabled"
-              i18n-helpHtml helpHtml="If you disable transcoding, many videos from your users will not work!"
-            ></my-peertube-checkbox>
+            <my-peertube-checkbox inputName="transcodingEnabled" formControlName="enabled">
+              <ng-template ptTemplate="label">
+                <ng-container i18n>Transcoding enabled</ng-container>
+              </ng-template>
+
+              <ng-template ptTemplate="help">
+                <ng-container i18n>If you disable transcoding, many videos from your users will not work!</ng-container>
+              </ng-template>
+            </my-peertube-checkbox>
           </div>
 
           <ng-container *ngIf="isTranscodingEnabled()">
               <my-peertube-checkbox
                 inputName="transcodingAllowAdditionalExtensions" formControlName="allowAdditionalExtensions"
                 i18n-labelText labelText="Allow additional extensions"
-                i18n-helpHtml helpHtml="Allow your users to upload .mkv, .mov, .avi, .flv videos"
-              ></my-peertube-checkbox>
+              >
+                <ng-template ptTemplate="help">
+                  <ng-container i18n>Allow your users to upload .mkv, .mov, .avi, .flv videos</ng-container>
+                </ng-template>
+              </my-peertube-checkbox>
             </div>
 
             <div class="form-group">
               <my-peertube-checkbox
                 inputName="transcodingAllowAudioFiles" formControlName="allowAudioFiles"
                 i18n-labelText labelText="Allow audio files upload"
-                i18n-helpHtml helpHtml="Allow your users to upload audio files that will be merged with the preview file on upload"
-              ></my-peertube-checkbox>
+              >
+                <ng-template ptTemplate="help">
+                  <ng-container i18n>Allow your users to upload audio files that will be merged with the preview file on upload</ng-container>
+                </ng-template>
+              </my-peertube-checkbox>
             </div>
 
             <div class="form-group">
         <div i18n class="inner-form-title">
           Cache
 
-          <my-help
-            helpType="custom" i18n-customHtml
-            customHtml="Some files are not federated (previews, captions). We fetch them directly from the origin instance and cache them."
-          ></my-help>
+          <my-help>
+            <ng-template ptTemplate="customHtml">
+              <ng-container i18n>Some files are not federated (previews, captions). We fetch them directly from the origin instance and cache them.</ng-container>
+            </ng-template>
+          </my-help>
         </div>
 
         <ng-container formGroupName="cache">
           <ng-container formGroupName="customizations">
             <div class="form-group">
               <label i18n for="customizationJavascript">JavaScript</label>
-              <my-help
-                helpType="custom" i18n-customHtml
-                customHtml="Write directly JavaScript code.<br />Example: <pre>console.log('my instance is amazing');</pre>"
-              ></my-help>
+              <my-help>
+                <ng-template ptTemplate="customHtml">
+                  <ng-container i18n>
+                    Write directly JavaScript code.<br />Example: <pre>console.log('my instance is amazing');</pre>
+                  </ng-container>
+                </ng-template>
+              </my-help>
+
               <textarea
                 id="customizationJavascript" formControlName="javascript"
                 [ngClass]="{ 'input-error': formErrors['instance.customizations.javascript'] }"
               ></textarea>
+
               <div *ngIf="formErrors.instance.customizations.javascript" class="form-error">{{ formErrors.instance.customizations.javascript }}</div>
             </div>
 
             <div class="form-group">
               <label for="customizationCSS">CSS</label>
-              <my-help
-                  helpType="custom"
-                  i18n-customHtml
-                  customHtml="
+
+              <my-help>
+                <ng-template ptTemplate="customHtml">
+                  <ng-container i18n>
                     Write directly CSS code. Example:<br /><br />
-                    <pre>
-  #custom-css {{ '{' }}
-    color: red;
-  {{ '}' }}
-                    </pre>
+<pre>
+#custom-css {{ '{' }}
+  color: red;
+{{ '}' }}
+</pre>
 
                     Prepend with <em>#custom-css</em> to override styles. Example:<br /><br />
-                    <pre>
-  #custom-css .logged-in-email {{ '{' }}
-    color: red;
-  {{ '}' }}
-                    </pre>
-                  "
-              ></my-help>
+<pre>
+#custom-css .logged-in-email {{ '{' }}
+  color: red;
+{{ '}' }}
+</pre>
+                  </ng-container>
+                </ng-template>
+              </my-help>
+
               <textarea
                 id="customizationCSS" formControlName="css"
                 [ngClass]="{ 'input-error': formErrors['instance.customizations.css'] }"
index c90bd514155238bbe6ab7f93e927e87d8d4da269..2b4d0da2cc9e83996f00c54af0515e37f8920061 100644 (file)
@@ -1,6 +1,10 @@
 @import '_variables';
 @import '_mixins';
 
+.form-group {
+  margin-bottom: 25px;
+}
+
 input[type=text] {
   @include peertube-input-text(340px);
   display: block;
@@ -40,7 +44,12 @@ textarea {
 
   display: block;
 
-  &#instanceShortDescription {
-    height: 100px;
+  &.small {
+    height: 75px;
   }
 }
+
+.label-small-info {
+  font-style: italic;
+  margin-bottom: 10px;
+}
index 8bd7f7cf6e3ad12254c7af568067d97f24a71bd9..0a69f34819fc970f7132a83f40fb00cbc2096174 100644 (file)
@@ -6,6 +6,9 @@ import { Notifier } from '@app/core'
 import { CustomConfig } from '../../../../../../shared/models/server/custom-config.model'
 import { I18n } from '@ngx-translate/i18n-polyfill'
 import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
+import { SelectItem } from 'primeng/api'
+import { forkJoin } from 'rxjs'
+import { first } from 'rxjs/operators'
 
 @Component({
   selector: 'my-edit-custom-config',
@@ -18,6 +21,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
   resolutions: { id: string, label: string }[] = []
   transcodingThreadOptions: { label: string, value: number }[] = []
 
+  languageItems: SelectItem[] = []
+  categoryItems: SelectItem[] = []
+
   constructor (
     protected formValidatorService: FormValidatorService,
     private customConfigValidatorsService: CustomConfigValidatorsService,
@@ -88,10 +94,26 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
         name: this.customConfigValidatorsService.INSTANCE_NAME,
         shortDescription: this.customConfigValidatorsService.INSTANCE_SHORT_DESCRIPTION,
         description: null,
-        terms: null,
-        defaultClientRoute: null,
+
         isNSFW: false,
         defaultNSFWPolicy: null,
+
+        terms: null,
+        codeOfConduct: null,
+
+        creationReason: null,
+        moderationInformation: null,
+        administrator: null,
+        maintenanceLifetime: null,
+        businessModel: null,
+
+        hardwareInformation: null,
+
+        categories: null,
+        languages: null,
+
+        defaultClientRoute: null,
+
         customizations: {
           javascript: null,
           css: null
@@ -158,6 +180,17 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
           enabled: null,
           manualApproval: null
         }
+      },
+      followings: {
+        instance: {
+          autoFollowBack: {
+            enabled: null
+          },
+          autoFollowIndex: {
+            enabled: null,
+            indexUrl: this.customConfigValidatorsService.INDEX_URL
+          }
+        }
       }
     }
 
@@ -173,18 +206,27 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
 
     this.buildForm(formGroupData)
 
-    this.configService.getCustomConfig()
-      .subscribe(
-        res => {
-          this.customConfig = res
+    forkJoin([
+      this.configService.getCustomConfig(),
+      this.serverService.videoLanguagesLoaded.pipe(first()), // First so the observable completes
+      this.serverService.videoCategoriesLoaded.pipe(first())
+    ]).subscribe(
+      ([ config ]) => {
+        this.customConfig = config
 
-          this.updateForm()
-          // Force form validation
-          this.forceCheck()
-        },
+        const languages = this.serverService.getVideoLanguages()
+        this.languageItems = languages.map(l => ({ label: l.label, value: l.id }))
 
-        err => this.notifier.error(err.message)
-      )
+        const categories = this.serverService.getVideoCategories()
+        this.categoryItems = categories.map(l => ({ label: l.label, value: l.id }))
+
+        this.updateForm()
+        // Force form validation
+        this.forceCheck()
+      },
+
+      err => this.notifier.error(err.message)
+    )
   }
 
   isTranscodingEnabled () {
@@ -213,8 +255,23 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
       )
   }
 
+  getSelectedLanguageLabel () {
+    return this.i18n('{{\'{0} languages selected')
+  }
+
+  getDefaultLanguageLabel () {
+    return this.i18n('No language')
+  }
+
+  getSelectedCategoryLabel () {
+    return this.i18n('{{\'{0} categories selected')
+  }
+
+  getDefaultCategoryLabel () {
+    return this.i18n('No category')
+  }
+
   private updateForm () {
     this.form.patchValue(this.customConfig)
   }
-
 }
index f35414b37e6ad912be637eff79c629e0c793b14e..75f3df601f538ed1a65128b31c54276e2e44af06 100644 (file)
@@ -1,7 +1,7 @@
 <div class="root">
   <h4>IP</h4>
 
-  <p>PeerTube thinks your public IP is <strong>{{ debug?.ip }}</strong>.</p>
+  <p>PeerTube thinks your web browser public IP is <strong>{{ debug?.ip }}</strong>.</p>
 
   <p>If this is not your correct public IP, please consider fixing it because:</p>
   <ul>
index 34febc4577462aa5d62e7a42b370c0e6689422df..76fabb19d905aab08835395878c4ae25c9abe0cb 100644 (file)
@@ -43,7 +43,8 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
       newUserRegistration: this.i18n('A new user registered on your instance'),
       newFollow: this.i18n('You or your channel(s) has a new follower'),
       commentMention: this.i18n('Someone mentioned you in video comments'),
-      newInstanceFollower: this.i18n('Your instance has a new follower')
+      newInstanceFollower: this.i18n('Your instance has a new follower'),
+      autoInstanceFollowing: this.i18n('Your instance auto followed another instance')
     }
     this.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[]
 
@@ -51,7 +52,8 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
       videoAbuseAsModerator: UserRight.MANAGE_VIDEO_ABUSES,
       videoAutoBlacklistAsModerator: UserRight.MANAGE_VIDEO_BLACKLIST,
       newUserRegistration: UserRight.MANAGE_USERS,
-      newInstanceFollower: UserRight.MANAGE_SERVER_FOLLOW
+      newInstanceFollower: UserRight.MANAGE_SERVER_FOLLOW,
+      autoInstanceFollowing: UserRight.MANAGE_CONFIGURATION
     }
 
     this.emailEnabled = this.serverService.getConfig().email.enabled
index 2796dd2dbb75d1af9e17c90a2bdd1f1ca4f59bc8..a11238925da04f521331278a6d55cb29d2d407e2 100644 (file)
@@ -1,10 +1,13 @@
 <form role="form" (ngSubmit)="updateDetails()" [formGroup]="form">
   <div class="form-group">
     <label i18n for="nsfwPolicy">Default policy on videos containing sensitive content</label>
-    <my-help
-      helpType="custom" i18n-customHtml
-      customHtml="With <strong>Do not list</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video."
-    ></my-help>
+    <my-help>
+      <ng-template ptTemplate="customHtml">
+        <ng-container i18n>
+          With <strong>Do not list</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video.
+        </ng-container>
+      </ng-template>
+    </my-help>
 
     <div class="peertube-select-container">
       <select id="nsfwPolicy" formControlName="nsfwPolicy">
 
   <div class="form-group">
     <label i18n for="videoLanguages">Only display videos in the following languages</label>
-    <my-help i18n-customHtml
-             customHtml="In Recently added, Trending, Local and Search pages"
-    ></my-help>
+    <my-help>
+      <ng-template ptTemplate="customHtml">
+        <ng-container i18n>In Recently added, Trending, Local and Search pages</ng-container>
+      </ng-template>
+    </my-help>
 
     <div>
       <p-multiSelect
-        [options]="languageItems" formControlName="videoLanguages" showToggleAll="true"
+        inputId="videoLanguages" [options]="languageItems" formControlName="videoLanguages" showToggleAll="true"
         [defaultLabel]="getDefaultVideoLanguageLabel()" [selectedItemsLabel]="getSelectedVideoLanguageLabel()"
         emptyFilterMessage="No results found" i18n-emptyFilterMessage
       ></p-multiSelect>
index 77febf1796e4796d80b081b6e9791c0b740e43c7..4fb82808285a5012959e4a0666b4514bfab68fa8 100644 (file)
@@ -5,9 +5,9 @@ import { AuthService } from '../../../core'
 import { FormReactive, User, UserService } from '../../../shared'
 import { I18n } from '@ngx-translate/i18n-polyfill'
 import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
-import { Subject } from 'rxjs'
+import { forkJoin, Subject } from 'rxjs'
 import { SelectItem } from 'primeng/api'
-import { switchMap } from 'rxjs/operators'
+import { first } from 'rxjs/operators'
 
 @Component({
   selector: 'my-account-video-settings',
@@ -39,30 +39,31 @@ export class MyAccountVideoSettingsComponent extends FormReactive implements OnI
       videoLanguages: null
     })
 
-    this.serverService.videoLanguagesLoaded
-        .pipe(switchMap(() => this.userInformationLoaded))
-        .subscribe(() => {
-          const languages = this.serverService.getVideoLanguages()
-
-          this.languageItems = [ { label: this.i18n('Unknown language'), value: '_unknown' } ]
-          this.languageItems = this.languageItems
-                                   .concat(languages.map(l => ({ label: l.label, value: l.id })))
-
-          const videoLanguages = this.user.videoLanguages
-            ? this.user.videoLanguages
-            : this.languageItems.map(l => l.value)
-
-          this.form.patchValue({
-            nsfwPolicy: this.user.nsfwPolicy,
-            webTorrentEnabled: this.user.webTorrentEnabled,
-            autoPlayVideo: this.user.autoPlayVideo === true,
-            videoLanguages
-          })
-        })
+    forkJoin([
+      this.serverService.videoLanguagesLoaded.pipe(first()),
+      this.userInformationLoaded.pipe(first())
+    ]).subscribe(() => {
+      const languages = this.serverService.getVideoLanguages()
+
+      this.languageItems = [ { label: this.i18n('Unknown language'), value: '_unknown' } ]
+      this.languageItems = this.languageItems
+                               .concat(languages.map(l => ({ label: l.label, value: l.id })))
+
+      const videoLanguages = this.user.videoLanguages
+        ? this.user.videoLanguages
+        : this.languageItems.map(l => l.value)
+
+      this.form.patchValue({
+        nsfwPolicy: this.user.nsfwPolicy,
+        webTorrentEnabled: this.user.webTorrentEnabled,
+        autoPlayVideo: this.user.autoPlayVideo === true,
+        videoLanguages
+      })
+    })
   }
 
   updateDetails () {
-    const nsfwPolicy = this.form.value['nsfwPolicy']
+    const nsfwPolicy = this.form.value[ 'nsfwPolicy' ]
     const webTorrentEnabled = this.form.value['webTorrentEnabled']
     const autoPlayVideo = this.form.value['autoPlayVideo']
 
index 571f46de99eba5335b2298d054914a3191c5455a..6cf1499d336583373624dc3afe41822d418961a0 100644 (file)
@@ -37,7 +37,6 @@ import {
 } from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component'
 import { DragDropModule } from '@angular/cdk/drag-drop'
 import { MyAccountChangeEmailComponent } from '@app/+my-account/my-account-settings/my-account-change-email'
-import { MultiSelectModule } from 'primeng/multiselect'
 import { MyAccountInterfaceSettingsComponent } from '@app/+my-account/my-account-settings/my-account-interface'
 
 @NgModule({
@@ -48,8 +47,7 @@ import { MyAccountInterfaceSettingsComponent } from '@app/+my-account/my-account
     SharedModule,
     TableModule,
     InputSwitchModule,
-    DragDropModule,
-    MultiSelectModule
+    DragDropModule
   ],
 
   declarations: [
index 47b3be8cc5a19ffe4146b8b2fec698bd660c8e1e..4381702ae525be0255005b466a236f2f1a574d92 100644 (file)
   </div>
 
   <div class="form-group form-group-terms">
-    <my-peertube-checkbox
-      inputName="terms" formControlName="terms"
-      i18n-labelHtml
-      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"
-    ></my-peertube-checkbox>
+    <my-peertube-checkbox inputName="terms" formControlName="terms">
+      <ng-template ptTemplate="label">
+        <ng-container i18n>
+          I am at least 16 years old and agree
+          to the <a (click)="onTermsClick($event)" href='#'>Terms</a>
+          <ng-container *ngIf="hasCodeOfConduct"> and to the <a (click)="onCodeOfConductClick($event)" href='#'>Code of Conduct</a></ng-container>
+          of this instance
+        </ng-container>
+      </ng-template>
+    </my-peertube-checkbox>
 
     <div *ngIf="formErrors.terms" class="form-error">
       {{ formErrors.terms }}
index 3b71fd3c4139028536dbeba4fc626c0ccb5e452d..6c96f20b448af548bd9112a2fb828c37e822bb0b 100644 (file)
@@ -1,4 +1,4 @@
-import { Component, EventEmitter, OnInit, Output } from '@angular/core'
+import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
 import { AuthService } from '@app/core'
 import { FormReactive, UserService, UserValidatorsService } from '@app/shared'
 import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
@@ -12,7 +12,11 @@ import { concat, of } from 'rxjs'
   styleUrls: [ './register.component.scss' ]
 })
 export class RegisterStepUserComponent extends FormReactive implements OnInit {
+  @Input() hasCodeOfConduct = false
+
   @Output() formBuilt = new EventEmitter<FormGroup>()
+  @Output() termsClick = new EventEmitter<void>()
+  @Output() codeOfConductClick = new EventEmitter<void>()
 
   constructor (
     protected formValidatorService: FormValidatorService,
@@ -45,6 +49,16 @@ export class RegisterStepUserComponent extends FormReactive implements OnInit {
      .subscribe(([ oldValue, newValue ]) => this.onDisplayNameChange(oldValue, newValue))
   }
 
+  onTermsClick (event: Event) {
+    event.preventDefault()
+    this.termsClick.emit()
+  }
+
+  onCodeOfConductClick (event: Event) {
+    event.preventDefault()
+    this.codeOfConductClick.emit()
+  }
+
   private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) {
     const username = this.form.value['username'] || ''
 
index e4d647fef5b46e14999a2b2bb81e177e7841e70f..906e29aeddb3088d3aaf1d0518d0f8266adbae16 100644 (file)
@@ -7,11 +7,15 @@
   <my-signup-success *ngIf="signupDone" [message]="success"></my-signup-success>
   <div *ngIf="info" class="alert alert-info">{{ info }}</div>
 
-  <div class="wrapper" *ngIf="!signupDone">
-    <div>
+  <div class="wrapper" [hidden]="signupDone">
+    <div class="register-form">
       <my-custom-stepper linear *ngIf="!signupDone">
         <cdk-step [stepControl]="formStepUser" i18n-label label="User">
-          <my-register-step-user (formBuilt)="onUserFormBuilt($event)"></my-register-step-user>
+          <my-register-step-user
+            [hasCodeOfConduct]="!!aboutHtml.codeOfConduct"
+            (formBuilt)="onUserFormBuilt($event)" (termsClick)="onTermsClick()" (codeOfConductClick)="onCodeOfConductClick()"
+          >
+          </my-register-step-user>
 
           <button i18n cdkStepperNext [disabled]="!formStepUser || !formStepUser.valid">Next</button>
         </cdk-step>
       </my-custom-stepper>
     </div>
 
-    <div>
-      <label i18n>Features found on this instance</label>
-      <my-instance-features-table></my-instance-features-table>
+    <div class="instance-information">
+      <ngb-accordion [closeOthers]="true" #accordion="ngbAccordion">
+        <ngb-panel id="instance-features" i18n-title title="Features found on this instance">
+          <ng-template ngbPanelContent>
+            <my-instance-features-table></my-instance-features-table>
+          </ng-template>
+        </ngb-panel>
+
+        <ng-container *ngIf="about">
+          <ngb-panel
+            *ngIf="aboutHtml.administrator || about.instance.maintenanceLifetime || about.instance.businessModel"
+            id="admin-sustainability" i18n-title title="Administrators & Sustainability"
+          >
+            <ng-template ngbPanelContent>
+              <div class="block">
+                <strong i18n>Who are we?</strong>
+                <div [innerHTML]="aboutHtml.administrator"></div>
+              </div>
+
+              <div class="block">
+                <strong i18n>How long do we plan to maintain this instance?</strong>
+                <div [innerHTML]="about.instance.maintenanceLifetime"></div>
+              </div>
+
+              <div class="block">
+                <strong i18n>How will we pay this instance?</strong>
+                <div [innerHTML]="about.instance.businessModel"></div>
+              </div>
+            </ng-template>
+          </ngb-panel>
+
+          <ngb-panel *ngIf="aboutHtml.moderationInformation" id="moderation-information" i18n-title title="Moderation information">
+            <ng-template ngbPanelContent>
+              <div class="block" [innerHTML]="aboutHtml.moderationInformation"></div>
+            </ng-template>
+          </ngb-panel>
+
+          <ngb-panel *ngIf="aboutHtml.codeOfConduct" id="code-of-conduct" i18n-title title="Code of conduct">
+            <ng-template ngbPanelContent>
+              <div class="block" [innerHTML]="aboutHtml.codeOfConduct"></div>
+            </ng-template>
+          </ngb-panel>
+
+          <ngb-panel *ngIf="aboutHtml.terms" id="terms" i18n-title title="Terms">
+            <ng-template ngbPanelContent>
+              <div class="block" [innerHTML]="aboutHtml.terms"></div>
+            </ng-template>
+          </ngb-panel>
+        </ng-container>
+      </ngb-accordion>
     </div>
   </div>
 
index 9405b5293c5eee49d4171bf08c6b313f4b9f7428..2f62dd59db594a39eb87f44a0aba61ce44edbaf6 100644 (file)
@@ -1,5 +1,9 @@
 @import '_variables';
 @import '_mixins';
+@import "./_bootstrap-variables";
+
+@import '~bootstrap/scss/functions';
+@import '~bootstrap/scss/variables';
 
 .alert {
   font-size: 15px;
 
   & > div {
     margin-bottom: 40px;
-    width: 450px;
+
+    &.register-form {
+      width: 450px;
+    }
+
+    &.instance-information {
+      width: 600px;
+      margin-bottom: 40px;
+
+      .block {
+        font-size: 15px;
+        margin-bottom: 15px;
+        padding: 0 $btn-padding-x;
+      }
+
+      @media screen and (max-width: 1500px) {
+        width: 450px;
+      }
+
+      ngb-accordion ::ng-deep {
+        .btn {
+          font-weight: $font-semibold !important;
+          color: var(--mainForegroundColor) !important;
+        }
+      }
+    }
 
     @media screen and (max-width: 500px) {
       width: auto;
   }
 }
 
-my-instance-features-table {
-  display: block;
-
-  margin-bottom: 40px;
-}
-
 .form-group-terms {
   margin: 30px 0;
 }
index cd605972842e9f7c471c7ce8ad7e68d857a9b02b..d470ef4dcc1830f5f1e1f9ea4bc37f34ee5ffb9a 100644 (file)
@@ -1,21 +1,35 @@
-import { Component } from '@angular/core'
+import { Component, OnInit, ViewChild } from '@angular/core'
 import { AuthService, Notifier, RedirectService, ServerService } from '@app/core'
 import { UserService, UserValidatorsService } from '@app/shared'
 import { I18n } from '@ngx-translate/i18n-polyfill'
 import { UserRegister } from '@shared/models/users/user-register.model'
 import { FormGroup } from '@angular/forms'
+import { About } from '@shared/models/server'
+import { InstanceService } from '@app/shared/instance/instance.service'
+import { NgbAccordion } from '@ng-bootstrap/ng-bootstrap'
 
 @Component({
   selector: 'my-register',
   templateUrl: './register.component.html',
   styleUrls: [ './register.component.scss' ]
 })
-export class RegisterComponent {
+export class RegisterComponent implements OnInit {
+  @ViewChild('accordion', { static: true }) accordion: NgbAccordion
+
   info: string = null
   error: string = null
   success: string = null
   signupDone = false
 
+  about: About
+  aboutHtml = {
+    description: '',
+    terms: '',
+    codeOfConduct: '',
+    moderationInformation: '',
+    administrator: ''
+  }
+
   formStepUser: FormGroup
   formStepChannel: FormGroup
 
@@ -26,6 +40,7 @@ export class RegisterComponent {
     private userService: UserService,
     private serverService: ServerService,
     private redirectService: RedirectService,
+    private instanceService: InstanceService,
     private i18n: I18n
   ) {
   }
@@ -34,6 +49,19 @@ export class RegisterComponent {
     return this.serverService.getConfig().signup.requiresEmailVerification
   }
 
+  ngOnInit (): void {
+    this.instanceService.getAbout()
+      .subscribe(
+        async about => {
+          this.about = about
+
+          this.aboutHtml = await this.instanceService.buildHtml(about)
+        },
+
+        err => this.notifier.error(err.message)
+      )
+  }
+
   hasSameChannelAndAccountNames () {
     return this.getUsername() === this.getChannelName()
   }
@@ -58,6 +86,14 @@ export class RegisterComponent {
     this.formStepChannel = form
   }
 
+  onTermsClick () {
+    if (this.accordion) this.accordion.toggle('terms')
+  }
+
+  onCodeOfConductClick () {
+    if (this.accordion) this.accordion.toggle('code-of-conduct')
+  }
+
   signup () {
     this.error = null
 
index 46336cbd00e1c2a3fb3cdb861db8acd8aa6bc34a..e55f83990fcb41a90d23139656ba7c4f066b7295 100644 (file)
@@ -7,13 +7,15 @@ import { RegisterStepChannelComponent } from './register-step-channel.component'
 import { RegisterStepUserComponent } from './register-step-user.component'
 import { CustomStepperComponent } from './custom-stepper.component'
 import { SignupSharedModule } from '@app/+signup/shared/signup-shared.module'
+import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'
 
 @NgModule({
   imports: [
     RegisterRoutingModule,
     SharedModule,
     CdkStepperModule,
-    SignupSharedModule
+    SignupSharedModule,
+    NgbAccordionModule
   ],
 
   declarations: [
index 07a576083ca85b18d9d47e11e2cc1513d73720e7..81b4351c5e2555bb01ede56150ff216f372f5aa7 100644 (file)
@@ -54,3 +54,8 @@
     </div>
   </ng-template>
 </p-toast>
+
+<ng-template [ngIf]="isUserLoggedIn()">
+  <my-welcome-modal #welcomeModal></my-welcome-modal>
+  <my-instance-config-warning-modal #instanceConfigWarningModal></my-instance-config-warning-modal>
+</ng-template>
index 64bfb9671bf9a9b20f521bb4c2dae8bbb31d9414..6b18e5feba9958a68a90e698de25208b30548663 100644 (file)
@@ -1,10 +1,10 @@
-import { Component, OnInit } from '@angular/core'
+import { Component, OnInit, ViewChild } from '@angular/core'
 import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
 import { Event, GuardsCheckStart, NavigationEnd, Router, Scroll } from '@angular/router'
 import { AuthService, RedirectService, ServerService, ThemeService } from '@app/core'
 import { is18nPath } from '../../../shared/models/i18n'
 import { ScreenService } from '@app/shared/misc/screen.service'
-import { debounceTime, filter, map, pairwise, skip } from 'rxjs/operators'
+import { debounceTime, filter, map, pairwise, skip, switchMap } from 'rxjs/operators'
 import { Hotkey, HotkeysService } from 'angular2-hotkeys'
 import { I18n } from '@ngx-translate/i18n-polyfill'
 import { fromEvent } from 'rxjs'
@@ -13,6 +13,11 @@ import { PluginService } from '@app/core/plugins/plugin.service'
 import { HooksService } from '@app/core/plugins/hooks.service'
 import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
 import { POP_STATE_MODAL_DISMISS } from '@app/shared/misc/constants'
+import { WelcomeModalComponent } from '@app/modal/welcome-modal.component'
+import { InstanceConfigWarningModalComponent } from '@app/modal/instance-config-warning-modal.component'
+import { UserRole } from '@shared/models'
+import { User } from '@app/shared'
+import { InstanceService } from '@app/shared/instance/instance.service'
 
 @Component({
   selector: 'my-app',
@@ -20,6 +25,9 @@ import { POP_STATE_MODAL_DISMISS } from '@app/shared/misc/constants'
   styleUrls: [ './app.component.scss' ]
 })
 export class AppComponent implements OnInit {
+  @ViewChild('welcomeModal', { static: false }) welcomeModal: WelcomeModalComponent
+  @ViewChild('instanceConfigWarningModal', { static: false }) instanceConfigWarningModal: InstanceConfigWarningModalComponent
+
   isMenuDisplayed = true
   isMenuChangedByUser = false
 
@@ -32,6 +40,7 @@ export class AppComponent implements OnInit {
     private authService: AuthService,
     private serverService: ServerService,
     private pluginService: PluginService,
+    private instanceService: InstanceService,
     private domSanitizer: DomSanitizer,
     private redirectService: RedirectService,
     private screenService: ScreenService,
@@ -96,6 +105,8 @@ export class AppComponent implements OnInit {
       .subscribe(() => this.onResize())
 
     this.location.onPopState(() => this.modalService.dismissAll(POP_STATE_MODAL_DISMISS))
+
+    this.openModalsIfNeeded()
   }
 
   isUserLoggedIn () {
@@ -220,32 +231,66 @@ export class AppComponent implements OnInit {
     this.hooks.runAction('action:application.init', 'common')
   }
 
+  private async openModalsIfNeeded () {
+    this.serverService.configLoaded
+        .pipe(
+          switchMap(() => this.authService.userInformationLoaded),
+          map(() => this.authService.getUser()),
+          filter(user => user.role === UserRole.ADMINISTRATOR)
+        ).subscribe(user => setTimeout(() => this.openAdminModals(user))) // setTimeout because of ngIf in template
+  }
+
+  private async openAdminModals (user: User) {
+    if (user.noWelcomeModal !== true) return this.welcomeModal.show()
+
+    const config = this.serverService.getConfig()
+    if (user.noInstanceConfigWarningModal === true || !config.signup.allowed) return
+
+    this.instanceService.getAbout()
+      .subscribe(about => {
+        if (
+          config.instance.name.toLowerCase() === 'peertube' ||
+          !about.instance.terms ||
+          !about.instance.administrator ||
+          !about.instance.maintenanceLifetime
+        ) {
+          this.instanceConfigWarningModal.show(about)
+        }
+      })
+  }
+
   private initHotkeys () {
     this.hotkeysService.add([
       new Hotkey(['/', 's'], (event: KeyboardEvent): boolean => {
         document.getElementById('search-video').focus()
         return false
       }, undefined, this.i18n('Focus the search bar')),
+
       new Hotkey('b', (event: KeyboardEvent): boolean => {
         this.toggleMenu()
         return false
       }, undefined, this.i18n('Toggle the left menu')),
+
       new Hotkey('g o', (event: KeyboardEvent): boolean => {
         this.router.navigate([ '/videos/overview' ])
         return false
       }, undefined, this.i18n('Go to the discover videos page')),
+
       new Hotkey('g t', (event: KeyboardEvent): boolean => {
         this.router.navigate([ '/videos/trending' ])
         return false
       }, undefined, this.i18n('Go to the trending videos page')),
+
       new Hotkey('g r', (event: KeyboardEvent): boolean => {
         this.router.navigate([ '/videos/recently-added' ])
         return false
       }, undefined, this.i18n('Go to the recently added videos page')),
+
       new Hotkey('g l', (event: KeyboardEvent): boolean => {
         this.router.navigate([ '/videos/local' ])
         return false
       }, undefined, this.i18n('Go to the local videos page')),
+
       new Hotkey('g u', (event: KeyboardEvent): boolean => {
         this.router.navigate([ '/videos/upload' ])
         return false
index 1e2936a37bf6cdd7841db53a52fbe574c5be1d12..a3ea33ca98195119929a866d3cca4a52ad6ab0c5 100644 (file)
@@ -18,6 +18,8 @@ import { VideosModule } from './videos'
 import { buildFileLocale, getCompleteLocale, isDefaultLocale } from '../../../shared/models/i18n'
 import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils'
 import { SearchModule } from '@app/search'
+import { WelcomeModalComponent } from '@app/modal/welcome-modal.component'
+import { InstanceConfigWarningModalComponent } from '@app/modal/instance-config-warning-modal.component'
 
 export function metaFactory (serverService: ServerService): MetaLoader {
   return new MetaStaticLoader({
@@ -39,7 +41,10 @@ export function metaFactory (serverService: ServerService): MetaLoader {
     MenuComponent,
     LanguageChooserComponent,
     AvatarNotificationComponent,
-    HeaderComponent
+    HeaderComponent,
+
+    WelcomeModalComponent,
+    InstanceConfigWarningModalComponent
   ],
   imports: [
     BrowserModule,
index 4efe3fb222600fa02ce820f7a0b39a0e81b916be..6833559600feefd0fe26165231136246a4d5a108 100644 (file)
           or create an account on another instance
         </a>
 
-        <my-help
-          *ngIf="signupAllowed === false" helpType="custom" i18n-customHtml
-          customHtml="User registration is not allowed on this instance, but you can register on many others!"
-        ></my-help>
+        <my-help *ngIf="signupAllowed === false">
+          <ng-template ptTemplate="customHtml">
+            <ng-container i18n>User registration is not allowed on this instance, but you can register on many others!</ng-container>
+          </ng-template>
+        </my-help>
       </div>
 
       <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 (file)
index 0000000..64f14e6
--- /dev/null
@@ -0,0 +1,45 @@
+<ng-template #modal let-hide="close">
+  <div class="modal-header">
+    <h4 i18n class="modal-title">Configuration warning!</h4>
+    <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
+  </div>
+
+  <div class="modal-body">
+
+    <p i18n>Hello dear administrator. You enabled user registration on your instance but you did not configure the following fields:</p>
+
+    <ul>
+      <li i18n *ngIf="about.instance.name.toLowerCase() === 'peertube'">Instance name</li>
+      <li i18n *ngIf="isDefaultShortDescription(about.instance.shortDescription)">Instance short description</li>
+
+      <li i18n *ngIf="!about.instance.administrator">Who you are</li>
+      <li i18n *ngIf="!about.instance.maintenanceLifetime">How long you plan to maintain your instance</li>
+      <li i18n *ngIf="!about.instance.businessModel">How you plan to pay your instance</li>
+
+      <li i18n *ngIf="!about.instance.moderationInformation">How you will moderate your instance</li>
+      <li i18n *ngIf="!about.instance.terms">Instance terms</li>
+    </ul>
+
+    <p>
+      Please consider to configure these fields to help people to choose <strong>the appropriate instance</strong>.
+      Without them, your instance may not be referenced on <a target="_blank" rel="noopener noreferrer" href="https://joinpeertube.org">JoinPeerTube website</a>.
+    </p>
+
+    <div class="configure-instance">
+      <a i18n href="/admin/config/edit-custom" target="_blank" rel="noopener noreferrer">Configure these fields</a>
+    </div>
+
+  </div>
+
+  <div class="modal-footer inputs">
+    <my-peertube-checkbox
+      inputName="stopDisplayModal" [(ngModel)]="stopDisplayModal"
+      i18n-labelText labelText="Don't show me this warning anymore"
+    >
+
+    </my-peertube-checkbox>
+
+    <span i18n class="action-button action-button-cancel" (click)="hide()">Close</span>
+  </div>
+
+</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 (file)
index 0000000..ff62a1b
--- /dev/null
@@ -0,0 +1,22 @@
+@import '_mixins';
+@import '_variables';
+
+.action-button-cancel {
+  margin-right: 0 !important;
+}
+
+.modal-body {
+  font-size: 15px;
+}
+
+li {
+  margin-bottom: 10px;
+}
+
+.configure-instance {
+  text-align: center;
+  font-weight: 600;
+  font-size: 18px;
+  margin-top: 40px;
+  margin-bottom: 10px;
+}
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 (file)
index 0000000..742a7dd
--- /dev/null
@@ -0,0 +1,47 @@
+import { Component, ElementRef, ViewChild } from '@angular/core'
+import { Notifier } from '@app/core'
+import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
+import { About } from '@shared/models/server'
+import { UserService } from '@app/shared'
+
+@Component({
+  selector: 'my-instance-config-warning-modal',
+  templateUrl: './instance-config-warning-modal.component.html',
+  styleUrls: [ './instance-config-warning-modal.component.scss' ]
+})
+export class InstanceConfigWarningModalComponent {
+  @ViewChild('modal', { static: true }) modal: ElementRef
+
+  stopDisplayModal = false
+  about: About
+
+  constructor (
+    private userService: UserService,
+    private modalService: NgbModal,
+    private notifier: Notifier
+  ) { }
+
+  show (about: About) {
+    this.about = about
+
+    const ref = this.modalService.open(this.modal)
+
+    ref.result.finally(() => {
+      if (this.stopDisplayModal === true) this.doNotOpenAgain()
+    })
+  }
+
+  isDefaultShortDescription (description: string) {
+    return description === 'PeerTube, a federated (ActivityPub) video streaming platform using P2P (BitTorrent) directly ' +
+      'in the web browser with WebTorrent and Angular.'
+  }
+
+  private doNotOpenAgain () {
+    this.userService.updateMyProfile({ noInstanceConfigWarningModal: true })
+        .subscribe(
+          () => console.log('We will not open the instance config warning modal again.'),
+
+          err => this.notifier.error(err.message)
+        )
+  }
+}
diff --git a/client/src/app/modal/welcome-modal.component.html b/client/src/app/modal/welcome-modal.component.html
new file mode 100644 (file)
index 0000000..09ff216
--- /dev/null
@@ -0,0 +1,67 @@
+<ng-template #modal let-hide="close">
+  <div class="modal-header">
+    <h4 i18n class="modal-title">Welcome on PeerTube dear administrator!</h4>
+    <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
+  </div>
+
+  <div class="modal-body">
+
+    <div class="block-documentation">
+      <div i18n class="subtitle">Documentation</div>
+
+      <div class="columns">
+        <a class="link-block" href="https://docs.joinpeertube.org/#/maintain-tools" target="_blank" rel="noopener noreferrer">
+          <a class="link-title" href="https://docs.joinpeertube.org/#/maintain-tools" target="_blank" rel="noopener noreferrer">CLI</a>
+
+          <div>Upload or import videos, parse logs, prune storage directories, reset user password...</div>
+        </a>
+
+        <a class="link-block" href="https://docs.joinpeertube.org/#/admin-following-instances" target="_blank" rel="noopener noreferrer">
+          <a class="link-title" href="https://docs.joinpeertube.org/#/admin-following-instances" target="_blank" rel="noopener noreferrer">Administer</a>
+
+          <div>Managing users, following other instances, dealing with spammers...</div>
+        </a>
+
+        <a class="link-block" href="https://docs.joinpeertube.org/#/use-setup-account" target="_blank" rel="noopener noreferrer">
+          <a class="link-title" href="https://docs.joinpeertube.org/#/use-setup-account" target="_blank" rel="noopener noreferrer">Use</a>
+
+          <div>Setup your account, managing video playlists, discover third-party applications...</div>
+        </a>
+      </div>
+    </div>
+
+    <div class="block-configuration">
+      <div i18n class="subtitle">It's time to configure your instance!</div>
+
+      <p i18n>
+        Choosing your <strong>instance name</strong>, <strong>setting up a description</strong>, specifying <strong>who you are</strong>,
+        why <strong>you created your instance</strong> and <strong>how long</strong> you plan to <strong>maintain your it</strong>
+        is very important for visitors to understand on what type of instance they are.
+      </p>
+
+      <p i18n>
+        If you want to open registrations, please decide what are <strong>your moderation rules</strong>, fill your <strong>instance terms</strong>
+        and specify the categories and languages you speak. This way, you will help users to register on <strong>the appropriate</strong> PeerTube instance.
+      </p>
+
+      <div class="configure-instance">
+        <a i18n href="/admin/config/edit-custom" target="_blank" rel="noopener noreferrer">Configure your instance</a>
+      </div>
+    </div>
+
+    <div class="block-links">
+      <div i18n class="subtitle">Useful links</div>
+
+      <ul>
+        <li>Official PeerTube website (news, support, contribute...): <a href="https://joinpeertube.org" target="_blank" rel="noopener noreferrer">https://joinpeertube.org</a></li>
+
+        <li>Put your instance on the public PeerTube index: <a href="https://instances.joinpeertube.org/instances">https://instances.joinpeertube.org/instances</a></li>
+      </ul>
+    </div>
+  </div>
+
+  <div class="modal-footer inputs">
+    <span i18n class="action-button action-button-submit" (click)="hide()">Understood!</span>
+  </div>
+
+</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 (file)
index 0000000..8bb6973
--- /dev/null
@@ -0,0 +1,56 @@
+@import '_mixins';
+@import '_variables';
+
+.modal-body {
+  font-size: 15px;
+}
+
+.subtitle {
+  font-weight: $font-semibold;
+  margin-bottom: 10px;
+  font-size: 16px;
+}
+
+.block-documentation .subtitle {
+  margin-bottom: 20px;
+}
+
+.block-configuration,
+.block-instance {
+  margin-top: 30px;
+}
+
+li {
+  margin-bottom: 10px;
+}
+
+.configure-instance {
+  text-align: center;
+  font-weight: 600;
+  font-size: 18px;
+  margin: 20px 0 40px 0;
+}
+
+.columns {
+  display: flex;
+
+  .link-block {
+    @include disable-default-a-behaviour;
+
+    color: var(--mainForegroundColor);
+    padding: 10px;
+    transition: background-color 0.2s ease-in;
+
+    &:hover {
+      background-color: rgba(0, 0, 0, 0.05);
+    }
+
+    .link-title {
+      font-size: 16px;
+      font-weight: $font-semibold;
+      display: flex;
+      justify-content: center;
+      margin-bottom: 5px;
+    }
+  }
+}
diff --git a/client/src/app/modal/welcome-modal.component.ts b/client/src/app/modal/welcome-modal.component.ts
new file mode 100644 (file)
index 0000000..05412a4
--- /dev/null
@@ -0,0 +1,38 @@
+import { Component, ElementRef, ViewChild } from '@angular/core'
+import { Notifier } from '@app/core'
+import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
+import { UserService } from '@app/shared'
+
+@Component({
+  selector: 'my-welcome-modal',
+  templateUrl: './welcome-modal.component.html',
+  styleUrls: [ './welcome-modal.component.scss' ]
+})
+export class WelcomeModalComponent {
+  @ViewChild('modal', { static: true }) modal: ElementRef
+
+  constructor (
+    private userService: UserService,
+    private modalService: NgbModal,
+    private notifier: Notifier
+  ) { }
+
+  show () {
+    const ref = this.modalService.open(this.modal,{
+      backdrop: 'static',
+      keyboard: false,
+      size: 'lg'
+    })
+
+    ref.result.finally(() => this.doNotOpenAgain())
+  }
+
+  private doNotOpenAgain () {
+    this.userService.updateMyProfile({ noWelcomeModal: true })
+      .subscribe(
+        () => console.log('We will not open the welcome modal again.'),
+
+        err => this.notifier.error(err.message)
+      )
+  }
+}
index a514b6057c38d2d68ee3d0e016d2ca572ef1ac22..e04c25d9a20fbf0c1cb7ffbe9a65f5b95ddc3936 100644 (file)
@@ -3,8 +3,8 @@ import { Directive, Input, TemplateRef } from '@angular/core'
 @Directive({
   selector: '[ptTemplate]'
 })
-export class PeerTubeTemplateDirective {
-  @Input('ptTemplate') name: string
+export class PeerTubeTemplateDirective <T extends string> {
+  @Input('ptTemplate') name: T
 
   constructor (public template: TemplateRef<any>) {
     // empty
index 882e39453c982afd864f0b5f108f48f32963583f..767e3f026844632ae6a786067a7c79196de6e8ec 100644 (file)
@@ -13,6 +13,7 @@ export class CustomConfigValidatorsService {
   readonly SIGNUP_LIMIT: BuildFormValidator
   readonly ADMIN_EMAIL: BuildFormValidator
   readonly TRANSCODING_THREADS: BuildFormValidator
+  readonly INDEX_URL: BuildFormValidator
 
   constructor (private i18n: I18n) {
     this.INSTANCE_NAME = {
@@ -78,5 +79,13 @@ export class CustomConfigValidatorsService {
         'min': this.i18n('Transcoding threads must be greater or equal to 0.')
       }
     }
+
+    this.INDEX_URL = {
+      VALIDATORS: [ Validators.required, Validators.pattern(/^https:\/\//) ],
+      MESSAGES: {
+        'required': this.i18n('Index URL is required.'),
+        'pattern': this.i18n('Index URL should be a URL')
+      }
+    }
   }
 }
index 571a1a673d75484591c8880ebc116315130b59a0..f1e3bf0bf586ea4a9222fc8da0911b5d0794e848 100644 (file)
@@ -3,8 +3,15 @@
     <input type="checkbox" [(ngModel)]="checked" (ngModelChange)="onModelChange()" [id]="inputName" [disabled]="disabled" />
     <span role="checkbox" [attr.aria-checked]="checked"></span>
     <span *ngIf="labelText">{{ labelText }}</span>
-    <span *ngIf="labelHtml" [innerHTML]="labelHtml"></span>
+
+    <span *ngIf="labelTemplate">
+      <ng-container *ngTemplateOutlet="labelTemplate"></ng-container>
+    </span>
   </label>
 
-  <my-help *ngIf="helpHtml" [tooltipPlacement]="helpPlacement" helpType="custom" i18n-customHtml [customHtml]="helpHtml"></my-help>
+  <my-help *ngIf="helpTemplate" [tooltipPlacement]="helpPlacement" helpType="custom">
+    <ng-template ptTemplate="customHtml">
+      <ng-template *ngTemplateOutlet="helpTemplate"></ng-template>
+    </ng-template>
+  </my-help>
 </div>
index 84ea788af39e105bc81d9be4bd928ffc04369feb..51f98b0bc8c919bacc757b222b5ec4a0efb88ede 100644 (file)
@@ -7,7 +7,7 @@
   .form-group-checkbox {
     display: flex;
 
-    span {
+    .label-text {
       font-weight: $font-regular;
       margin: 0;
     }
index a4b72aa37ad0c3c50bb521db0e8076f49c5e7b8f..3b8f39ed080110a9dd0a8c458962f3942f620e39 100644 (file)
@@ -1,5 +1,6 @@
-import { ChangeDetectorRef, Component, forwardRef, Input, OnChanges, SimpleChanges } from '@angular/core'
+import { AfterContentInit, ChangeDetectorRef, Component, ContentChildren, forwardRef, Input, QueryList, TemplateRef } from '@angular/core'
 import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
+import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template.directive'
 
 @Component({
   selector: 'my-peertube-checkbox',
@@ -13,20 +14,35 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
     }
   ]
 })
-export class PeertubeCheckboxComponent implements ControlValueAccessor {
+export class PeertubeCheckboxComponent implements ControlValueAccessor, AfterContentInit {
   @Input() checked = false
   @Input() inputName: string
   @Input() labelText: string
-  @Input() labelHtml: string
-  @Input() helpHtml: string
   @Input() helpPlacement = 'top'
   @Input() disabled = false
 
+  @ContentChildren(PeerTubeTemplateDirective) templates: QueryList<PeerTubeTemplateDirective<'label' | 'help'>>
+
   // FIXME: https://github.com/angular/angular/issues/10816#issuecomment-307567836
   @Input() onPushWorkaround = false
 
+  labelTemplate: TemplateRef<any>
+  helpTemplate: TemplateRef<any>
+
   constructor (private cdr: ChangeDetectorRef) { }
 
+  ngAfterContentInit () {
+    {
+      const t = this.templates.find(t => t.name === 'label')
+      if (t) this.labelTemplate = t.template
+    }
+
+    {
+      const t = this.templates.find(t => t.name === 'help')
+      if (t) this.helpTemplate = t.template
+    }
+  }
+
   propagateChange = (_: any) => { /* empty */ }
 
   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 (file)
index 0000000..ac208fc
--- /dev/null
@@ -0,0 +1,3 @@
+<span *ngIf="value === true" class="glyphicon glyphicon-ok"></span>
+<span *ngIf="value === false" class="glyphicon glyphicon-remove"></span>
+
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 (file)
index 0000000..56d08af
--- /dev/null
@@ -0,0 +1,10 @@
+@import '_variables';
+@import '_mixins';
+
+.glyphicon-ok {
+  color: $green;
+}
+
+.glyphicon-remove {
+  color: $red;
+}
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 (file)
index 0000000..d02d513
--- /dev/null
@@ -0,0 +1,10 @@
+import { Component, Input } from '@angular/core'
+
+@Component({
+  selector: 'my-feature-boolean',
+  templateUrl: './feature-boolean.component.html',
+  styleUrls: [ './feature-boolean.component.scss' ]
+})
+export class FeatureBooleanComponent {
+  @Input() value: boolean
+}
index 2987bd00e90338f2bd74d6f8d24dbc2281b2c73f..d1cb8fcbe55a4ec30b39930231ff800234a79691 100644 (file)
@@ -1,28 +1,53 @@
 <div class="feature-table">
 
-  <table class="table">
+  <table class="table" *ngIf="config">
     <tr>
-      <td i18n class="label">Default NSFW/sensitive videos policy (can be redefined by the users)</td>
+      <td i18n class="label">
+        <div>Default NSFW/sensitive videos policy</div>
+        <div class="more-info">can be redefined by the users</div>
+      </td>
 
       <td class="value">{{ buildNSFWLabel() }}</td>
     </tr>
 
-    <tr *ngFor="let feature of features">
-      <td class="label">{{ feature.label }}</td>
+    <tr>
+      <td i18n class="label">User registration allowed</td>
       <td>
-        <span *ngIf="feature.value === true" class="glyphicon glyphicon-ok"></span>
-        <span *ngIf="feature.value === false" class="glyphicon glyphicon-remove"></span>
+        <my-feature-boolean [value]="config.signup.allowed"></my-feature-boolean>
       </td>
     </tr>
 
     <tr>
-      <td i18n class="label">Video quota</td>
+      <td i18n class="label" colspan="2">Video uploads</td>
+    </tr>
+
+    <tr>
+      <td i18n class="sub-label">Transcoding in multiple resolutions</td>
+      <td>
+        <my-feature-boolean [value]="config.transcoding.enabledResolutions.length !== 0"></my-feature-boolean>
+      </td>
+    </tr>
+
+    <tr>
+      <td i18n class="sub-label">Video uploads</td>
+      <td>
+        <span *ngIf="config.autoBlacklist.videos.ofUsers.enabled">Requires manual validation by moderators</span>
+        <span *ngIf="!config.autoBlacklist.videos.ofUsers.enabled">Automatically published</span>
+      </td>
+    </tr>
+
+    <tr>
+      <td i18n class="sub-label">Video quota</td>
 
       <td class="value">
         <ng-container *ngIf="initialUserVideoQuota !== -1">
           {{ initialUserVideoQuota | bytes: 0 }} <ng-container *ngIf="dailyUserVideoQuota !== -1">({{ dailyUserVideoQuota | bytes: 0 }} per day)</ng-container>
 
-          <my-help tooltipPlacement="auto" helpType="custom" [customHtml]="quotaHelpIndication"></my-help>
+          <my-help tooltipPlacement="auto" helpType="custom">
+            <ng-template ptTemplate="customHtml">
+              <div [innerHTML]="quotaHelpIndication"></div>
+            </ng-template>
+          </my-help>
         </ng-container>
 
         <ng-container i18n *ngIf="initialUserVideoQuota === -1">
         </ng-container>
       </td>
     </tr>
+
+    <tr>
+      <td i18n class="label" colspan="2">Import</td>
+    </tr>
+
+    <tr>
+      <td i18n class="sub-label">HTTP import (YouTube, Vimeo, direct URL...)</td>
+      <td>
+        <my-feature-boolean [value]="config.import.videos.http.enabled"></my-feature-boolean>
+      </td>
+    </tr>
+
+    <tr>
+      <td i18n class="sub-label">Torrent import</td>
+      <td>
+        <my-feature-boolean [value]="config.import.videos.torrent.enabled"></my-feature-boolean>
+      </td>
+    </tr>
+
+
+    <tr>
+      <td i18n class="label" colspan="2">Player</td>
+    </tr>
+
+    <tr>
+      <td i18n class="sub-label">P2P enabled</td>
+      <td>
+        <my-feature-boolean [value]="config.tracker.enabled"></my-feature-boolean>
+      </td>
+    </tr>
   </table>
 </div>
index f9bec038de861f8f1aa3cc7c209dfac71b1633ba..67f2b6c848db80324e55f1af8c38575794539876 100644 (file)
@@ -5,16 +5,28 @@ table {
   font-size: 14px;
   color: var(--mainForegroundColor);
 
-  .label {
-    font-weight: $font-semibold;
+  .label,
+  .sub-label {
     min-width: 330px;
-  }
 
-  .glyphicon-ok {
-    color: $green;
+    &.label {
+      font-weight: $font-semibold;
+    }
+
+    &.sub-label {
+      padding-left: 30px;
+    }
+
+    .more-info {
+      font-style: italic;
+      font-weight: initial;
+      font-size: 14px
+    }
   }
 
-  .glyphicon-remove {
-    color: $red;
+  td {
+    vertical-align: middle;
   }
 }
+
+
index a53082a93c234c32b2051afc887cb44bc8963389..46df4d0b29d1228f8b846a2ab06bbe8f21fb80e4 100644 (file)
@@ -1,6 +1,7 @@
 import { Component, OnInit } from '@angular/core'
 import { ServerService } from '@app/core'
 import { I18n } from '@ngx-translate/i18n-polyfill'
+import { ServerConfig } from '@shared/models'
 
 @Component({
   selector: 'my-instance-features-table',
@@ -8,8 +9,8 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
   styleUrls: [ './instance-features-table.component.scss' ]
 })
 export class InstanceFeaturesTableComponent implements OnInit {
-  features: { label: string, value?: boolean }[] = []
   quotaHelpIndication = ''
+  config: ServerConfig
 
   constructor (
     private i18n: I18n,
@@ -28,7 +29,7 @@ export class InstanceFeaturesTableComponent implements OnInit {
   ngOnInit () {
     this.serverService.configLoaded
         .subscribe(() => {
-          this.buildFeatures()
+          this.config = this.serverService.getConfig()
           this.buildQuotaHelpIndication()
         })
   }
@@ -41,37 +42,6 @@ export class InstanceFeaturesTableComponent implements OnInit {
     if (policy === 'display') return this.i18n('Displayed')
   }
 
-  private buildFeatures () {
-    const config = this.serverService.getConfig()
-
-    this.features = [
-      {
-        label: this.i18n('User registration allowed'),
-        value: config.signup.allowed
-      },
-      {
-        label: this.i18n('Video uploads require manual validation by moderators'),
-        value: config.autoBlacklist.videos.ofUsers.enabled
-      },
-      {
-        label: this.i18n('Transcode your videos in multiple resolutions'),
-        value: config.transcoding.enabledResolutions.length !== 0
-      },
-      {
-        label: this.i18n('HTTP import (YouTube, Vimeo, direct URL...)'),
-        value: config.import.videos.http.enabled
-      },
-      {
-        label: this.i18n('Torrent import'),
-        value: config.import.videos.torrent.enabled
-      },
-      {
-        label: this.i18n('P2P enabled'),
-        value: config.tracker.enabled
-      }
-    ]
-  }
-
   private getApproximateTime (seconds: number) {
     const hours = Math.floor(seconds / 3600)
     let pluralSuffix = ''
index d0c96941d168a2dbf85cfb6759d708889fbf36a2..44b413fa4263f0980de3d9ecc52d609b254d3656 100644 (file)
@@ -4,6 +4,9 @@ import { Injectable } from '@angular/core'
 import { environment } from '../../../environments/environment'
 import { RestExtractor, RestService } from '../rest'
 import { About } from '../../../../../shared/models/server'
+import { MarkdownService } from '@app/shared/renderer'
+import { peertubeTranslate } from '@shared/models'
+import { ServerService } from '@app/core'
 
 @Injectable()
 export class InstanceService {
@@ -13,7 +16,9 @@ export class InstanceService {
   constructor (
     private authHttp: HttpClient,
     private restService: RestService,
-    private restExtractor: RestExtractor
+    private restExtractor: RestExtractor,
+    private markdownService: MarkdownService,
+    private serverService: ServerService
   ) {
   }
 
@@ -34,4 +39,43 @@ export class InstanceService {
                .pipe(catchError(res => this.restExtractor.handleError(res)))
 
   }
+
+  async buildHtml (about: About) {
+    const html = {
+      description: '',
+      terms: '',
+      codeOfConduct: '',
+      moderationInformation: '',
+      administrator: '',
+      hardwareInformation: ''
+    }
+
+    for (const key of Object.keys(html)) {
+      html[ key ] = await this.markdownService.textMarkdownToHTML(about.instance[ key ])
+    }
+
+    return html
+  }
+
+  buildTranslatedLanguages (about: About, translations: any) {
+    const languagesArray = this.serverService.getVideoLanguages()
+
+    return about.instance.languages
+                .map(l => {
+                  const languageObj = languagesArray.find(la => la.id === l)
+
+                  return peertubeTranslate(languageObj.label, translations)
+                })
+  }
+
+  buildTranslatedCategories (about: About, translations: any) {
+    const categoriesArray = this.serverService.getVideoCategories()
+
+    return about.instance.categories
+                .map(c => {
+                  const categoryObj = categoriesArray.find(ca => ca.id === c)
+
+                  return peertubeTranslate(categoryObj.label, translations)
+                })
+  }
 }
index e31eef06a69204975519ca539921466df47f42fa..9a6d3e48e74290c82babbdf76441d6c5d8ab3552 100644 (file)
@@ -1,15 +1,25 @@
 <ng-template #tooltipTemplate>
-  <ng-template [ngIf]="preHtml">
-    <p [innerHTML]="preHtml"></p>
-    <br />
-  </ng-template>
+  <p *ngIf="preHtmlTemplate">
+    <ng-template *ngTemplateOutlet="preHtmlTemplate"></ng-template>
+  </p>
 
-  <p [innerHTML]="mainHtml"></p>
+  <ng-container *ngIf="preHtmlTemplate && (customHtmlTemplate || mainHtml || postHtmlTemplate)">
+    <br /><br />
+  </ng-container>
 
-  <ng-template [ngIf]="postHtml">
-    <br />
-    <p [innerHTML]="postHtml"></p>
-  </ng-template>
+  <p *ngIf="customHtmlTemplate">
+    <ng-template *ngTemplateOutlet="customHtmlTemplate"></ng-template>
+  </p>
+
+  <p *ngIf="mainHtml" [innerHTML]="mainHtml"></p>
+
+  <ng-container *ngIf="(customHtmlTemplate || mainHtml) && postHtmlTemplate">
+    <br /><br />
+  </ng-container>
+
+  <p *ngIf="postHtmlTemplate">
+    <ng-template *ngTemplateOutlet="postHtmlTemplate"></ng-template>
+  </p>
 </ng-template>
 
 <span
index f3426f70ff4246a2e8aff0ac3ce1167f06a3c454..18ba8ad5e943cfbd2ef947a1940cc55c547c7ac6 100644 (file)
@@ -1,6 +1,7 @@
-import { Component, Input, OnChanges, OnInit } from '@angular/core'
+import { AfterContentInit, Component, ContentChildren, Input, OnChanges, OnInit, QueryList, TemplateRef } from '@angular/core'
 import { I18n } from '@ngx-translate/i18n-polyfill'
 import { MarkdownService } from '@app/shared/renderer'
+import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template.directive'
 
 @Component({
   selector: 'my-help',
@@ -8,22 +9,42 @@ import { MarkdownService } from '@app/shared/renderer'
   templateUrl: './help.component.html'
 })
 
-export class HelpComponent implements OnInit, OnChanges {
-  @Input() preHtml = ''
-  @Input() postHtml = ''
-  @Input() customHtml = ''
+export class HelpComponent implements OnInit, OnChanges, AfterContentInit {
   @Input() helpType: 'custom' | 'markdownText' | 'markdownEnhanced' = 'custom'
   @Input() tooltipPlacement = 'right'
 
+  @ContentChildren(PeerTubeTemplateDirective) templates: QueryList<PeerTubeTemplateDirective<'preHtml' | 'customHtml' | 'postHtml'>>
+
   isPopoverOpened = false
   mainHtml = ''
 
+  preHtmlTemplate: TemplateRef<any>
+  customHtmlTemplate: TemplateRef<any>
+  postHtmlTemplate: TemplateRef<any>
+
   constructor (private i18n: I18n) { }
 
   ngOnInit () {
     this.init()
   }
 
+  ngAfterContentInit () {
+    {
+      const t = this.templates.find(t => t.name === 'preHtml')
+      if (t) this.preHtmlTemplate = t.template
+    }
+
+    {
+      const t = this.templates.find(t => t.name === 'customHtml')
+      if (t) this.customHtmlTemplate = t.template
+    }
+
+    {
+      const t = this.templates.find(t => t.name === 'postHtml')
+      if (t) this.postHtmlTemplate = t.template
+    }
+  }
+
   ngOnChanges () {
     this.init()
   }
@@ -37,11 +58,6 @@ export class HelpComponent implements OnInit, OnChanges {
   }
 
   private init () {
-    if (this.helpType === 'custom') {
-      this.mainHtml = this.customHtml
-      return
-    }
-
     if (this.helpType === 'markdownText') {
       this.mainHtml = this.formatMarkdownSupport(MarkdownService.TEXT_RULES)
       return
index 9a90663511c850767e91f2e5755b3f292ae6656a..0e24f3085ef93113fd0f6252ebfe79ccd7811b1d 100644 (file)
@@ -13,9 +13,11 @@ export class MarkdownService {
     'list'
   ]
   static ENHANCED_RULES = MarkdownService.TEXT_RULES.concat([ 'image' ])
+  static COMPLETE_RULES = MarkdownService.ENHANCED_RULES.concat([ 'block', 'inline', 'heading', 'html_inline', 'html_block', 'paragraph' ])
 
   private textMarkdownIt: MarkdownIt
   private enhancedMarkdownIt: MarkdownIt
+  private completeMarkdownIt: MarkdownIt
 
   async textMarkdownToHTML (markdown: string) {
     if (!markdown) return ''
@@ -39,11 +41,22 @@ export class MarkdownService {
     return this.avoidTruncatedTags(html)
   }
 
-  private async createMarkdownIt (rules: string[]) {
-    // FIXME: import('..') returns a struct module, containing a "default" field corresponding to our sanitizeHtml function
+  async completeMarkdownToHTML (markdown: string) {
+    if (!markdown) return ''
+
+    if (!this.completeMarkdownIt) {
+      this.completeMarkdownIt = await this.createMarkdownIt(MarkdownService.COMPLETE_RULES, true)
+    }
+
+    const html = this.completeMarkdownIt.render(markdown)
+    return this.avoidTruncatedTags(html)
+  }
+
+  private async createMarkdownIt (rules: string[], html = false) {
+    // FIXME: import('...') returns a struct module, containing a "default" field corresponding to our sanitizeHtml function
     const MarkdownItClass: typeof import ('markdown-it') = (await import('markdown-it') as any).default
 
-    const markdownIt = new MarkdownItClass('zero', { linkify: true, breaks: true })
+    const markdownIt = new MarkdownItClass('zero', { linkify: true, breaks: true, html })
 
     for (const rule of rules) {
       markdownIt.enable(rule)
index eb57a2fff0fc6db07c5367d9006bd8feea2399fa..65e0f21a4d9874e9a6de1b76baed229ab9aeaf57 100644 (file)
@@ -6,10 +6,8 @@ import { RouterModule } from '@angular/router'
 import { MarkdownTextareaComponent } from '@app/shared/forms/markdown-textarea.component'
 import { HelpComponent } from '@app/shared/misc/help.component'
 import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive'
-
 import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes'
 import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared'
-
 import { AUTH_INTERCEPTOR_PROVIDER } from './auth'
 import { ButtonComponent } from './buttons/button.component'
 import { DeleteButtonComponent } from './buttons/delete-button.component'
@@ -93,6 +91,8 @@ import { VideoDownloadComponent } from '@app/shared/video/modals/video-download.
 import { VideoReportComponent } from '@app/shared/video/modals/video-report.component'
 import { ClipboardModule } from 'ngx-clipboard'
 import { FollowService } from '@app/shared/instance/follow.service'
+import { MultiSelectModule } from 'primeng/multiselect'
+import { FeatureBooleanComponent } from '@app/shared/instance/feature-boolean.component'
 
 @NgModule({
   imports: [
@@ -113,7 +113,8 @@ import { FollowService } from '@app/shared/instance/follow.service'
 
     PrimeSharedModule,
     InputMaskModule,
-    NgPipesModule
+    NgPipesModule,
+    MultiSelectModule
   ],
 
   declarations: [
@@ -156,6 +157,7 @@ import { FollowService } from '@app/shared/instance/follow.service'
     SubscribeButtonComponent,
     RemoteSubscribeComponent,
     InstanceFeaturesTableComponent,
+    FeatureBooleanComponent,
     UserBanModalComponent,
     UserModerationDropdownComponent,
     TopMenuDropdownComponent,
@@ -186,6 +188,7 @@ import { FollowService } from '@app/shared/instance/follow.service'
     InputMaskModule,
     BytesPipe,
     KeysPipe,
+    MultiSelectModule,
 
     LoaderComponent,
     SmallLoaderComponent,
index ec3636b3e03bb80f55dacd9953e7ed8c9534a108..59ee1cb046319c583eb6fcef8a5d98db4ab84177 100644 (file)
     <span *ngIf="interact">Remote interact</span>
   </button>
 
-  <my-help *ngIf="!interact && showHelp"
-           helpType="custom"
-           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.">
+  <my-help *ngIf="!interact && showHelp">
+    <ng-template ptTemplate="customHtml">
+      <ng-container i18n>
+        You can subscribe to the channel via any ActivityPub-capable fediverse instance.<br /><br />
+        For instance with Mastodon or Pleroma you can type the channel URL in the search box and subscribe there.
+      </ng-container>
+    </ng-template>
   </my-help>
 
-  <my-help *ngIf="showHelp && interact"
-           helpType="custom"
-           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.">
+  <my-help *ngIf="showHelp && interact">
+    <ng-template ptTemplate="customHtml">
+      <ng-container i18n>
+        You can interact with this via any ActivityPub-capable fediverse instance.<br /><br />
+        For instance with Mastodon or Pleroma you can type the current URL in the search box and interact with it there.
+      </ng-container>
+    </ng-template>
   </my-help>
-</form>
\ No newline at end of file
+</form>
index 06eace71c94d7ba94dc3e3a3ff788edf91d6a7fc..c3f4bf429c740bd59095d200becbd87fd1001abc 100644 (file)
@@ -42,9 +42,10 @@ export class UserNotification implements UserNotificationServer {
     state: FollowState
     follower: ActorInfo & { avatarUrl?: string }
     following: {
-      type: 'account' | 'channel'
+      type: 'account' | 'channel' | 'instance'
       name: string
       displayName: string
+      host: string
     }
   }
 
@@ -112,7 +113,10 @@ export class UserNotification implements UserNotificationServer {
 
         case UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS:
           this.videoAutoBlacklistUrl = '/admin/moderation/video-auto-blacklist/list'
-          this.videoUrl = this.buildVideoUrl(this.video)
+          // Backward compatibility where we did not assign videoBlacklist to this type of notification before
+          if (!this.videoBlacklist) this.videoBlacklist = { id: null, video: this.video }
+
+          this.videoUrl = this.buildVideoUrl(this.videoBlacklist.video)
           break
 
         case UserNotificationType.BLACKLIST_ON_MY_VIDEO:
@@ -146,6 +150,10 @@ export class UserNotification implements UserNotificationServer {
         case UserNotificationType.NEW_INSTANCE_FOLLOWER:
           this.instanceFollowUrl = '/admin/follows/followers-list'
           break
+
+        case UserNotificationType.AUTO_INSTANCE_FOLLOWING:
+          this.instanceFollowUrl = '/admin/follows/following-list'
+          break
       }
     } catch (err) {
       this.type = null
index 29281342607a5d37f75837c01823005f6453c96d..a0f8e6df5170f06f99cbbcd88676760a713c72b8 100644 (file)
@@ -8,7 +8,7 @@
         <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.video.channel.avatarUrl" />
 
         <div class="message">
-          {{ notification.video.channel.displayName }} published a <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">new video</a>
+          {{ notification.video.channel.displayName }} published a new video: <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.video.name }}</a>
         </div>
       </ng-container>
 
@@ -40,7 +40,7 @@
         <my-global-icon iconName="no"></my-global-icon>
 
         <div class="message">
-          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>
+          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>
         </div>
       </ng-container>
 
           <ng-container *ngIf="notification.actorFollow.state === 'pending'"> awaiting your approval</ng-container>
         </div>
       </ng-container>
+
+      <ng-container i18n *ngSwitchCase="UserNotificationType.AUTO_INSTANCE_FOLLOWING">
+        <my-global-icon iconName="users"></my-global-icon>
+
+        <div class="message">
+          Your instance automatically followed <a (click)="markAsRead(notification)" [routerLink]="notification.instanceFollowUrl">{{ notification.actorFollow.following.host }}</a>
+        </div>
+      </ng-container>
     </ng-container>
 
     <div class="from-date">{{ notification.createdAt | myFromNow }}</div>
index 53809f82c6e6d9fd41c32d3e8616623fa0d0a20c..656b73dd25547afb4bed3e3986fc590a31866bf0 100644 (file)
@@ -9,31 +9,38 @@ export class User implements UserServerModel {
   username: string
   email: string
   pendingEmail: string | null
+
   emailVerified: boolean
   nsfwPolicy: NSFWPolicyType
 
-  role: UserRole
-  roleLabel: string
+  adminFlags?: UserAdminFlag
 
-  webTorrentEnabled: boolean
   autoPlayVideo: boolean
+  webTorrentEnabled: boolean
   videosHistoryEnabled: boolean
   videoLanguages: string[]
 
+  role: UserRole
+  roleLabel: string
+
   videoQuota: number
   videoQuotaDaily: number
-  account: Account
-  videoChannels: VideoChannel[]
-  createdAt: Date
+  videoQuotaUsed?: number
+  videoQuotaUsedDaily?: number
 
   theme: string
 
-  adminFlags?: UserAdminFlag
+  account: Account
+  notificationSettings?: UserNotificationSetting
+  videoChannels?: VideoChannel[]
 
   blocked: boolean
   blockedReason?: string
 
-  notificationSettings?: UserNotificationSetting
+  noInstanceConfigWarningModal: boolean
+  noWelcomeModal: boolean
+
+  createdAt: Date
 
   constructor (hash: Partial<UserServerModel>) {
     this.id = hash.id
@@ -43,13 +50,16 @@ export class User implements UserServerModel {
     this.role = hash.role
 
     this.videoChannels = hash.videoChannels
+
     this.videoQuota = hash.videoQuota
     this.videoQuotaDaily = hash.videoQuotaDaily
+    this.videoQuotaUsed = hash.videoQuotaUsed
+    this.videoQuotaUsedDaily = hash.videoQuotaUsedDaily
+
     this.nsfwPolicy = hash.nsfwPolicy
     this.webTorrentEnabled = hash.webTorrentEnabled
     this.videosHistoryEnabled = hash.videosHistoryEnabled
     this.autoPlayVideo = hash.autoPlayVideo
-    this.createdAt = hash.createdAt
 
     this.theme = hash.theme
 
@@ -58,8 +68,13 @@ export class User implements UserServerModel {
     this.blocked = hash.blocked
     this.blockedReason = hash.blockedReason
 
+    this.noInstanceConfigWarningModal = hash.noInstanceConfigWarningModal
+    this.noWelcomeModal = hash.noWelcomeModal
+
     this.notificationSettings = hash.notificationSettings
 
+    this.createdAt = hash.createdAt
+
     if (hash.account !== undefined) {
       this.account = new Account(hash.account)
     }
index 994e0fa1ec0774ec1322386f3a0fc17c318a76d8..0644200568c195a4767fd7d1132dde462738aea1 100644 (file)
@@ -35,7 +35,7 @@ export class VideosSelectionComponent extends AbstractVideoList implements OnIni
   @Input() titlePage: string
   @Input() miniatureDisplayOptions: MiniatureDisplayOptions
   @Input() getVideosObservableFunction: (page: number, sort?: VideoSortField) => Observable<ResultList<Video>>
-  @ContentChildren(PeerTubeTemplateDirective) templates: QueryList<PeerTubeTemplateDirective>
+  @ContentChildren(PeerTubeTemplateDirective) templates: QueryList<PeerTubeTemplateDirective<'rowButtons' | 'globalButtons'>>
 
   @Output() selectionChange = new EventEmitter<SelectionType>()
   @Output() videosModelChange = new EventEmitter<Video[]>()
index 217cadc663461a065ac1c679d08786d6aa183e0a..245ae42b67486c820e67f85dd17522ac27b46526 100644 (file)
 
             <div class="form-group">
               <label i18n class="label-tags">Tags</label>
-              <my-help i18n-preHtml preHtml="Tags could be used to suggest relevant recommendations.</br>Press Enter to add a new tag."></my-help>
+
+              <my-help>
+                <ng-template ptTemplate="customHtml">
+                  <ng-container i18n>
+                    Tags could be used to suggest relevant recommendations. <br />
+                    Press Enter to add a new tag.
+                  </ng-container>
+                </ng-template>
+              </my-help>
+
               <tag-input
                 [validators]="tagValidators" [errorMessages]="tagValidatorsMessages"
                 i18n-placeholder placeholder="+ Tag" i18n-secondaryPlaceholder secondaryPlaceholder="Enter a new tag"
 
             <div class="form-group">
               <label i18n for="description">Description</label>
-              <my-help helpType="markdownText" i18n-preHtml preHtml="Video descriptions are truncated by default and require manual action to expand them."></my-help>
+
+              <my-help helpType="markdownText">
+                <ng-template ptTemplate="preHtml">
+                  <ng-container i18n>
+                    Video descriptions are truncated by default and require manual action to expand them.
+                  </ng-container>
+                </ng-template>
+              </my-help>
+
               <my-markdown-textarea truncate="250" formControlName="description"></my-markdown-textarea>
 
               <div *ngIf="formErrors.description" class="form-error">
               </div>
             </div>
 
-            <my-peertube-checkbox
-              inputName="nsfw" formControlName="nsfw"
-              i18n-labelText labelText="This video contains mature or explicit content"
-              i18n-helpHtml helpHtml="Some instances do not list videos containing mature or explicit content by default."
-              helpPlacement="bottom-right"
-            ></my-peertube-checkbox>
-
-            <my-peertube-checkbox
-              *ngIf="waitTranscodingEnabled"
-              inputName="waitTranscoding" formControlName="waitTranscoding"
-              i18n-labelText labelText="Wait transcoding before publishing the video"
-              i18n-helpHtml helpHtml="If you decide not to wait for transcoding before publishing the video, it could be unplayable until transcoding ends."
-              helpPlacement="bottom-right"
-            ></my-peertube-checkbox>
+            <my-peertube-checkbox inputName="nsfw" formControlName="nsfw" helpPlacement="bottom-right">
+              <ng-template ptTemplate="label">
+                <ng-container i18n>This video contains mature or explicit content</ng-container>
+              </ng-template>
+
+              <ng-template ptTemplate="help">
+                <ng-container i18n>Some instances do not list videos containing mature or explicit content by default.</ng-container>
+              </ng-template>
+            </my-peertube-checkbox>
+
+            <my-peertube-checkbox *ngIf="waitTranscodingEnabled" inputName="waitTranscoding" formControlName="waitTranscoding" helpPlacement="bottom-right">
+              <ng-template ptTemplate="label">
+                <ng-container i18n>Wait transcoding before publishing the video</ng-container>
+              </ng-template>
+
+              <ng-template ptTemplate="help">
+                <ng-container i18n>If you decide not to wait for transcoding before publishing the video, it could be unplayable until transcoding ends.</ng-container>
+              </ng-template>
+            </my-peertube-checkbox>
 
           </div>
         </div>
index 7a495fea5997d400ff8e7ec86738e6d68f44d26e..c290fd4b1bb7142aa043da16960a8ce16fca9e95 100644 (file)
 
     <div class="form-group form-group-magnet-uri">
       <label i18n for="magnetUri">Paste magnet URI</label>
-      <my-help
-        helpType="custom" i18n-customHtml
-        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."
-      ></my-help>
+      <my-help>
+        <ng-template ptTemplate="customHtml">
+          <ng-container i18n>
+            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.
+          </ng-container>
+        </ng-template>
+      </my-help>
 
       <input type="text" id="magnetUri" [(ngModel)]="magnetUri" />
     </div>
index e4f19faa866ca4a618ee6994d83f9bf55d26c557..09d0b8272ae689314448f84179b0824754c5c1e0 100644 (file)
@@ -4,10 +4,16 @@
 
     <div class="form-group">
       <label i18n for="targetUrl">URL</label>
-      <my-help
-        helpType="custom" i18n-customHtml
-        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."
-      ></my-help>
+
+      <my-help>
+        <ng-template ptTemplate="customHtml">
+          <ng-container i18n>
+            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.
+          </ng-container>
+        </ng-template>
+      </my-help>
 
       <input type="text" id="targetUrl" [(ngModel)]="targetUrl" />
     </div>
diff --git a/client/src/assets/images/framasoft.png b/client/src/assets/images/framasoft.png
new file mode 100644 (file)
index 0000000..57be8c2
Binary files /dev/null and b/client/src/assets/images/framasoft.png differ
index 0c8c612ee8834eeb47ba0fcf1eeb73d71073d845..c44c184d5bb80ab646d22058788f18ea02c6ebae 100644 (file)
@@ -92,7 +92,7 @@ class P2pMediaLoaderPlugin extends Plugin {
     this.p2pEngine.on(Events.SegmentError, (segment: Segment, err) => {
       console.error('Segment error.', segment, err)
 
-      this.options.redundancyUrlManager.removeByOriginUrl(segment.url)
+      this.options.redundancyUrlManager.removeBySegmentUrl(segment.requestUrl)
     })
 
     this.statsP2PBytes.numPeers = 1 + this.options.redundancyUrlManager.countBaseUrls()
index 7fc2b6ab1e942ce51063831661339667235ce350..abab8aa99e3524c739c39d3a780cd5c2f5bbe191 100644 (file)
@@ -2,9 +2,6 @@ import { basename, dirname } from 'path'
 
 class RedundancyUrlManager {
 
-  // Remember by what new URL we replaced an origin URL
-  private replacedSegmentUrls: { [originUrl: string]: string } = {}
-
   constructor (private baseUrls: string[] = []) {
     // empty
   }
@@ -17,16 +14,7 @@ class RedundancyUrlManager {
     this.baseUrls = this.baseUrls.filter(u => u !== baseUrl && u !== baseUrl + '/')
   }
 
-  removeByOriginUrl (originUrl: string) {
-    const replaced = this.replacedSegmentUrls[originUrl]
-    if (!replaced) return
-
-    return this.removeBySegmentUrl(replaced)
-  }
-
   buildUrl (url: string) {
-    delete this.replacedSegmentUrls[url]
-
     const max = this.baseUrls.length + 1
     const i = this.getRandomInt(max)
 
@@ -35,10 +23,7 @@ class RedundancyUrlManager {
     const newBaseUrl = this.baseUrls[i]
     const slashPart = newBaseUrl.endsWith('/') ? '' : '/'
 
-    const newUrl = newBaseUrl + slashPart + basename(url)
-    this.replacedSegmentUrls[url] = newUrl
-
-    return newUrl
+    return newBaseUrl + slashPart + basename(url)
   }
 
   countBaseUrls () {
index abbc137b2b25aa175a9e7ae91b48235f87891920..26ba490c7473a484d36b1b1b4a5a5594abf4ac48 100644 (file)
   & + span {
     position: relative;
     width: 18px;
+    min-width: 18px;
     height: 18px;
     border: $border-width solid var(--mainForegroundColor);
     border-radius: 3px;
   border-radius: 50%;
   width: $size;
   height: $size;
+  min-width: $size;
 }
 
 @mixin chevron ($size, $border-width) {
index 1a5144b1190453427636fb46e243f3d3c8071bbd..4bf48a5704d546b077b6e47d23056a9f1958565d 100644 (file)
@@ -26,11 +26,6 @@ body {
   .vjs-dock-description {
     font-size: 11px;
 
-    .text::before, .text::after {
-      display: inline-block;
-      content: '\1F308';
-    }
-
     .text::before {
       margin-right: 4px;
     }
index 6ff3efef1afd6ebc551b6da4417404b253e665cd..19d2a1d023d73fb29a3c92c6e8790e656784c1eb 100644 (file)
@@ -239,7 +239,7 @@ export class PeerTubeEmbed {
 
       const config: ServerConfig = await configResponse.json()
       const description = config.tracker.enabled && this.warningTitle
-        ? '<span class="text">' + this.player.localize('Uses P2P, others may know your IP is downloading this video.') + '</span>'
+        ? '<span class="text">' + this.player.localize('Watching this video may reveal your IP address to others.') + '</span>'
         : undefined
 
       this.player.dock({
index a67ffe6d1e5e63ffc2b1116f72ac71107736f2a2..6755d7e64b106efe834b6227ad1dc20e0e9f6faa 100644 (file)
@@ -1139,6 +1139,11 @@ async-foreach@^0.1.3:
   resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542"
   integrity sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=
 
+async-limiter@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
+  integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
+
 async-limiter@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8"
@@ -1433,7 +1438,7 @@ bittorrent-protocol@^3.0.0:
     speedometer "^1.0.0"
     unordered-array-remove "^1.0.2"
 
-bittorrent-tracker@^9.0.0, bittorrent-tracker@^9.11.0:
+bittorrent-tracker@^9.0.0:
   version "9.11.0"
   resolved "https://registry.yarnpkg.com/bittorrent-tracker/-/bittorrent-tracker-9.11.0.tgz#9911f9c14e5a29f84990a0c31b3d83dd16eb2876"
   integrity sha512-T1zvW/kSeEnWT4I3JE+6c7aZbO5jtleZyQe911SyzIxFF9DvtUNWXud3p5ZUkXaoI2xXwfpvlks5VFj5SKEB+A==
@@ -1463,6 +1468,36 @@ bittorrent-tracker@^9.0.0, bittorrent-tracker@^9.11.0:
     bufferutil "^4.0.0"
     utf-8-validate "^5.0.1"
 
+bittorrent-tracker@^9.14.4:
+  version "9.14.4"
+  resolved "https://registry.yarnpkg.com/bittorrent-tracker/-/bittorrent-tracker-9.14.4.tgz#0d9661560e6fec37689dfc5045142772eac05536"
+  integrity sha512-2Y/MNRjYhysD6t4r38z7l1WTT7g23IAqRWZRsj7xnnpciFn4xE4qiKmyFwA4gtbFGAZ14K3DdaqZbiQsC3PEfQ==
+  dependencies:
+    bencode "^2.0.0"
+    bittorrent-peerid "^1.0.2"
+    bn.js "^5.0.0"
+    chrome-dgram "^3.0.2"
+    compact2string "^1.2.0"
+    debug "^4.0.1"
+    ip "^1.0.1"
+    lru "^3.0.0"
+    minimist "^1.1.1"
+    once "^1.3.0"
+    random-iterate "^1.0.1"
+    randombytes "^2.0.3"
+    run-parallel "^1.1.2"
+    run-series "^1.0.2"
+    simple-get "^3.0.0"
+    simple-peer "^9.0.0"
+    simple-websocket "^8.0.0"
+    string2compact "^1.1.1"
+    uniq "^1.0.1"
+    unordered-array-remove "^1.0.2"
+    ws "^7.0.0"
+  optionalDependencies:
+    bufferutil "^4.0.0"
+    utf-8-validate "^5.0.1"
+
 blob-to-buffer@^1.2.6:
   version "1.2.8"
   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:
   resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
   integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==
 
+bn.js@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.0.0.tgz#5c3d398021b3ddb548c1296a16f857e908f35c70"
+  integrity sha512-bVwDX8AF+72fIUNuARelKAlQUNtPOfG2fRxorbVvFk4zpHbqLrPdOGfVg5vrKwVzLLePqPBiATaOZNELQzmS0A==
+
 body-parser@1.19.0, body-parser@^1.16.1:
   version "1.19.0"
   resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
@@ -1959,6 +1999,14 @@ chownr@^1.1.1:
   resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.2.tgz#a18f1e0b269c8a6a5d3c86eb298beb14c3dd7bf6"
   integrity sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A==
 
+chrome-dgram@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/chrome-dgram/-/chrome-dgram-3.0.2.tgz#7e0e00084b57971714214372368ad18a7785ad52"
+  integrity sha512-Ay741EHF/Ib18un+LUtBNK43NrabD6GOuwVaka7uUbV0gFRLEPULm2Q05YSzRNBtSrbaO4eErmDdniiy/u8Lig==
+  dependencies:
+    inherits "^2.0.1"
+    run-series "^1.1.2"
+
 chrome-trace-event@^1.0.0:
   version "1.0.2"
   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:
   resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
   integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
 
-p2p-media-loader-core@^0.6.1:
-  version "0.6.1"
-  resolved "https://registry.yarnpkg.com/p2p-media-loader-core/-/p2p-media-loader-core-0.6.1.tgz#90cc05460cb5207897953e92059b32930f06a56f"
-  integrity sha512-bTyOdTVxbjzr1GCt6bOIxXlw7U6gPvYXOGo07EU0wufabKscn/TNyuTH4fDhVtw6NGMISn18G06td3V049tOBw==
+p2p-media-loader-core@^0.6.2:
+  version "0.6.2"
+  resolved "https://registry.yarnpkg.com/p2p-media-loader-core/-/p2p-media-loader-core-0.6.2.tgz#7e46cf8fc4357596f389e106bee850908cc974ef"
+  integrity sha512-yspgCOrVVYitVNece5CA6W/kcVA0UybvbD4kyBE5ooyhCAXQK5/q6JsIpXiVQ3VkQw8Qs4mfZjU39Vt6vEk6aw==
   dependencies:
-    bittorrent-tracker "^9.11.0"
+    bittorrent-tracker "^9.14.4"
     debug "^4.1.1"
     events "^3.0.0"
     get-browser-rtc "^1.0.2"
     sha.js "^2.4.11"
-    simple-peer "^9.4.0"
+    simple-peer "^9.5.0"
 
-p2p-media-loader-hlsjs@^0.6.1:
-  version "0.6.1"
-  resolved "https://registry.yarnpkg.com/p2p-media-loader-hlsjs/-/p2p-media-loader-hlsjs-0.6.1.tgz#558e1737241f3c17810cddafde0e992c20656886"
-  integrity sha512-JadTwrxNNKXyO4MyiK7i5zT1zOSFmaiIOlE4Gr6NjxDg8v3+Q8q09YHJPXumXexUWDNpw5vw8eHTpBdQClJ9lQ==
+p2p-media-loader-hlsjs@^0.6.2:
+  version "0.6.2"
+  resolved "https://registry.yarnpkg.com/p2p-media-loader-hlsjs/-/p2p-media-loader-hlsjs-0.6.2.tgz#b66f977a5d28986c8f6e62d2ffa297aec3c05186"
+  integrity sha512-5LgqWPDsgyST9rxoHGDpExZU1rIDZIT0qft2wAnlg8Cb8aVeaBxUsmF4Sj692Qb5/GBDsi8vLE03LW8gpvlh1g==
   dependencies:
     events "^3.0.0"
     m3u8-parser "^4.4.0"
-    p2p-media-loader-core "^0.6.1"
+    p2p-media-loader-core "^0.6.2"
 
 package-json-versionify@^1.0.2:
   version "1.0.4"
@@ -7624,7 +7672,7 @@ run-queue@^1.0.0, run-queue@^1.0.3:
   dependencies:
     aproba "^1.1.1"
 
-run-series@^1.0.2:
+run-series@^1.0.2, run-series@^1.1.2:
   version "1.1.8"
   resolved "https://registry.yarnpkg.com/run-series/-/run-series-1.1.8.tgz#2c4558f49221e01cd6371ff4e0a1e203e460fc36"
   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:
     once "^1.3.1"
     simple-concat "^1.0.0"
 
-simple-peer@^9.0.0, simple-peer@^9.4.0:
+simple-peer@^9.0.0:
   version "9.4.0"
   resolved "https://registry.yarnpkg.com/simple-peer/-/simple-peer-9.4.0.tgz#eb82ef1181e10ec0c014a94953e2eb278f3d9025"
   integrity sha512-8qF32uq6SSSVXoBq9g31uGqZYupwRD3Ta/QK9fV04U/IbnIS6mictLb8/kjFyLVa3JrD7QYyKrw3nvJJ+lNFDw==
@@ -7996,6 +8044,17 @@ simple-peer@^9.0.0, simple-peer@^9.4.0:
     randombytes "^2.0.3"
     readable-stream "^2.3.4"
 
+simple-peer@^9.5.0:
+  version "9.5.0"
+  resolved "https://registry.yarnpkg.com/simple-peer/-/simple-peer-9.5.0.tgz#67ba8bd4b54efc3acf19aceafdc118b27e24fcbc"
+  integrity sha512-3tROq3nBo/CIZI8PWlXGbAxQIlQF6KQ/zcd4lQ2pAC4+rPiV7E721hI22nTO54uw/nzb2HKbvmDtZ4Wr173+vA==
+  dependencies:
+    debug "^4.0.1"
+    get-browser-rtc "^1.0.0"
+    inherits "^2.0.1"
+    randombytes "^2.0.3"
+    readable-stream "^3.4.0"
+
 simple-sha1@^2.0.0, simple-sha1@^2.0.8, simple-sha1@^2.1.0:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/simple-sha1/-/simple-sha1-2.1.2.tgz#de40cbd5aae278fde8e3bb3250a35d74c67326b1"
@@ -8014,6 +8073,16 @@ simple-websocket@^7.0.1:
     readable-stream "^2.0.5"
     ws "^6.0.0"
 
+simple-websocket@^8.0.0:
+  version "8.0.1"
+  resolved "https://registry.yarnpkg.com/simple-websocket/-/simple-websocket-8.0.1.tgz#c28af779034b329d0cf1448a45fdd311d21fa289"
+  integrity sha512-2QKSRjf+tqFXLVmOQjf95gHeKhuyx2k1ouDjtnE0uKCYw84HfN85HsXo+GmPH+2PIh5BQql++g2AIbHgGAZU4w==
+  dependencies:
+    debug "^4.1.1"
+    randombytes "^2.0.3"
+    readable-stream "^3.1.1"
+    ws "^7.0.0"
+
 slash@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
@@ -9710,6 +9779,13 @@ ws@^6.0.0:
   dependencies:
     async-limiter "~1.0.0"
 
+ws@^7.0.0:
+  version "7.1.2"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-7.1.2.tgz#c672d1629de8bb27a9699eb599be47aeeedd8f73"
+  integrity sha512-gftXq3XI81cJCgkUiAVixA0raD9IVmXqsylCrjRygw4+UOOGzPoxnQ6r/CnVL9i+mDncJo94tSkyrtuuQVBmrg==
+  dependencies:
+    async-limiter "^1.0.0"
+
 ws@~3.3.1:
   version "3.3.3"
   resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2"
index dfba23f59bb74f6f4f764371ac0a214595bcce27..5ebfdeddb6a00ccf619aec6bed0e380c095b98db 100644 (file)
@@ -69,7 +69,7 @@ email:
 
 # From the project root directory
 storage:
-  tmp: 'storage/tmp/' # Used to download data (imports etc), store uploaded files before processing...
+  tmp: 'storage/tmp/' # Use to download data (imports etc), store uploaded files before processing...
   avatars: 'storage/avatars/'
   videos: 'storage/videos/'
   streaming_playlists: 'storage/streaming-playlists/'
@@ -85,7 +85,7 @@ storage:
 log:
   level: 'info' # debug/info/warning/error
   rotation:
-    enabled : true # Enabled by default, if disabled make sure that 'storage.logs' is pointing to a folder handled by logrotate
+    enabled : true
 
 search:
   # Add ability to fetch remote videos/actors by their URI, that may not be federated with your instance
@@ -238,7 +238,60 @@ instance:
   short_description: 'PeerTube, a federated (ActivityPub) video streaming platform using P2P (BitTorrent) directly in the web browser with WebTorrent and Angular.'
   description: 'Welcome to this PeerTube instance!' # Support markdown
   terms: 'No terms for now.' # Support markdown
+  code_of_conduct: '' # Supports markdown
+
+  # Who moderates the instance? What is the policy regarding NSFW videos? Political videos? etc
+  moderation_information: '' # Supports markdown
+
+  # Why did you create this instance?
+  creation_reason: ''
+
+  # Who is behind the instance? A single person? A non profit?
+  administrator: ''
+
+  # How long do you plan to maintain this instance?
+  maintenance_lifetime: ''
+
+  # How will you pay the PeerTube instance server? With you own funds? With users donations? Advertising?
+  business_model: ''
+
+  # If you want to explain on what type of hardware your PeerTube instance runs
+  # Example: "2 vCore, 2GB RAM..."
+  hardware_information: '' # Supports Markdown
+
+  # What are the main languages of your instance? To interact with your users for example
+  # Uncomment or add the languages you want
+  # List of supported languages: https://peertube.cpy.re/api/v1/videos/languages
+  languages:
+#    - en
+#    - es
+#    - fr
+
+  # You can specify the main categories of your instance (dedicated to music, gaming or politics etc)
+  # Uncomment or add the category ids you want
+  # List of supported categories: https://peertube.cpy.re/api/v1/videos/categories
+  categories:
+#    - 1  # Music
+#    - 2  # Films
+#    - 3  # Vehicles
+#    - 4  # Art
+#    - 5  # Sports
+#    - 6  # Travels
+#    - 7  # Gaming
+#    - 8  # People
+#    - 9  # Comedy
+#    - 10 # Entertainment
+#    - 11 # News & Politics
+#    - 12 # How To
+#    - 13 # Education
+#    - 14 # Activism
+#    - 15 # Science & Technology
+#    - 16 # Animals
+#    - 17 # Kids
+#    - 18 # Food
+
   default_client_route: '/videos/trending'
+
   # Whether or not the instance is dedicated to NSFW content
   # Enabling it will allow other administrators to know that you are mainly federating sensitive content
   # Moreover, the NSFW checkbox on video upload will be automatically checked by default
@@ -246,6 +299,7 @@ instance:
   # By default, "do_not_list" or "blur" or "display" NSFW videos
   # Could be overridden per user with a setting
   default_nsfw_policy: 'do_not_list'
+
   customizations:
     javascript: '' # Directly your JavaScript code (without <script> tags). Will be eval at runtime
     css: '' # Directly your CSS code (without <style> tags). Will be injected at runtime
@@ -273,5 +327,21 @@ followers:
     # Whether or not an administrator must manually validate a new follower
     manual_approval: false
 
+followings:
+  instance:
+    # If you want to automatically follow back new instance followers
+    # Only follows accepted followers (in case you enabled manual followers approbation)
+    # If this option is enabled, use the mute feature instead of deleting followings
+    # /!\ Don't enable this if you don't have a reactive moderation team /!\
+    auto_follow_back:
+      enabled: false
+
+    # If you want to automatically follow instances of the public index
+    # If this option is enabled, use the mute feature instead of deleting followings
+    # /!\ Don't enable this if you don't have a reactive moderation team /!\
+    auto_follow_index:
+      enabled: false
+      index_url: 'https://instances.joinpeertube.org'
+
 theme:
   default: 'default'
index 267186e082293541d5623c0d3095fd6d9d0c072c..96d676a35c65f7778bf3594d3d89e9052bb23812 100644 (file)
@@ -70,11 +70,11 @@ email:
 
 # From the project root directory
 storage:
-  tmp: '/var/www/peertube/storage/tmp/' # Used to download data (imports etc), store uploaded files before processing...
+  tmp: '/var/www/peertube/storage/tmp/' # Use to download data (imports etc), store uploaded files before processing...
   avatars: '/var/www/peertube/storage/avatars/'
   videos: '/var/www/peertube/storage/videos/'
   streaming_playlists: '/var/www/peertube/storage/streaming-playlists/'
-  redundancy: '/var/www/peertube/storage/videos/'
+  redundancy: '/var/www/peertube/storage/redundancy/'
   logs: '/var/www/peertube/storage/logs/'
   previews: '/var/www/peertube/storage/previews/'
   thumbnails: '/var/www/peertube/storage/thumbnails/'
@@ -86,7 +86,7 @@ storage:
 log:
   level: 'info' # debug/info/warning/error
   rotation:
-    enabled : true
+    enabled : true # Enabled by default, if disabled make sure that 'storage.logs' is pointing to a folder handled by logrotate
 
 search:
   # Add ability to fetch remote videos/actors by their URI, that may not be federated with your instance
@@ -157,8 +157,8 @@ views:
       max_age: -1
 
 plugins:
-  # The website PeerTube will ask for available PeerTube plugins
-  # This is an unmoderated plugin index, so only install plugins you trust
+  # The website PeerTube will ask for available PeerTube plugins and themes
+  # This is an unmoderated plugin index, so only install plugins/themes you trust
   index:
     enabled: true
     check_latest_versions_interval: '12 hours' # How often you want to check new plugins/themes versions
@@ -251,9 +251,62 @@ auto_blacklist:
 instance:
   name: 'PeerTube'
   short_description: 'PeerTube, a federated (ActivityPub) video streaming platform using P2P (BitTorrent) directly in the web browser with WebTorrent and Angular.'
-  description: '' # Support markdown
-  terms: '' # Support markdown
+  description: 'Welcome to this PeerTube instance!' # Support markdown
+  terms: 'No terms for now.' # Support markdown
+  code_of_conduct: '' # Supports markdown
+
+  # Who moderates the instance? What is the policy regarding NSFW videos? Political videos? etc
+  moderation_information: '' # Supports markdown
+
+  # Why did you create this instance?
+  creation_reason: ''
+
+  # Who is behind the instance? A single person? A non profit?
+  administrator: ''
+
+  # How long do you plan to maintain this instance?
+  maintenance_lifetime: ''
+
+  # How will you pay the PeerTube instance server? With you own funds? With users donations? Advertising?
+  business_model: ''
+
+  # If you want to explain on what type of hardware your PeerTube instance runs
+  # Example: "2 vCore, 2GB RAM..."
+  hardware_information: '' # Supports Markdown
+
+  # What are the main languages of your instance? To interact with your users for example
+  # Uncomment or add the languages you want
+  # List of supported languages: https://peertube.cpy.re/api/v1/videos/languages
+  languages:
+#    - en
+#    - es
+#    - fr
+
+  # You can specify the main categories of your instance (dedicated to music, gaming or politics etc)
+  # Uncomment or add the category ids you want
+  # List of supported categories: https://peertube.cpy.re/api/v1/videos/categories
+  categories:
+#    - 1  # Music
+#    - 2  # Films
+#    - 3  # Vehicles
+#    - 4  # Art
+#    - 5  # Sports
+#    - 6  # Travels
+#    - 7  # Gaming
+#    - 8  # People
+#    - 9  # Comedy
+#    - 10 # Entertainment
+#    - 11 # News & Politics
+#    - 12 # How To
+#    - 13 # Education
+#    - 14 # Activism
+#    - 15 # Science & Technology
+#    - 16 # Animals
+#    - 17 # Kids
+#    - 18 # Food
+
   default_client_route: '/videos/trending'
+
   # Whether or not the instance is dedicated to NSFW content
   # Enabling it will allow other administrators to know that you are mainly federating sensitive content
   # Moreover, the NSFW checkbox on video upload will be automatically checked by default
@@ -261,6 +314,7 @@ instance:
   # By default, "do_not_list" or "blur" or "display" NSFW videos
   # Could be overridden per user with a setting
   default_nsfw_policy: 'do_not_list'
+
   customizations:
     javascript: '' # Directly your JavaScript code (without <script> tags). Will be eval at runtime
     css: '' # Directly your CSS code (without <style> tags). Will be injected at runtime
@@ -278,7 +332,7 @@ services:
     username: '@Chocobozzz' # Indicates the Twitter account for the website or platform on which the content was published
     # If true, a video player will be embedded in the Twitter feed on PeerTube video share
     # If false, we use an image link card that will redirect on your PeerTube instance
-    # Test on https://cards-dev.twitter.com/validator to see if you are whitelisted
+    # Change it to "true", and then test on https://cards-dev.twitter.com/validator to see if you are whitelisted
     whitelisted: false
 
 followers:
@@ -288,5 +342,20 @@ followers:
     # Whether or not an administrator must manually validate a new follower
     manual_approval: false
 
+followings:
+  instance:
+    # If you want to automatically follow back new instance followers
+    # If this option is enabled, use the mute feature instead of deleting followings
+    # /!\ Don't enable this if you don't have a reactive moderation team /!\
+    auto_follow_back:
+      enabled: false
+
+    # If you want to automatically follow instances of the public index
+    # If this option is enabled, use the mute feature instead of deleting followings
+    # /!\ Don't enable this if you don't have a reactive moderation team /!\
+    auto_follow_index:
+      enabled: false
+      index_url: 'https://instances.joinpeertube.org'
+
 theme:
   default: 'default'
index 3a72fa4aaf204b73ccbfd0eedf755c0a7a0a3ed9..ca1f94572e1d11458fc14bc9aaf1315fc3cbccc5 100644 (file)
     "iso-639-3": "^1.0.1",
     "js-yaml": "^3.5.4",
     "jsonld": "~1.1.0",
-    "jsonld-signatures": "https://github.com/Chocobozzz/jsonld-signatures#rsa2017",
     "lodash": "^4.17.10",
     "lru-cache": "^5.1.1",
     "magnet-uri": "^5.1.4",
     "memoizee": "^0.4.14",
+    "module-alias": "^2.2.1",
     "morgan": "^1.5.3",
     "multer": "^1.1.0",
     "nodemailer": "^6.0.0",
   "scripty": {
     "silent": true
   },
-  "sasslintConfig": "client/.sass-lint.yml"
+  "sasslintConfig": "client/.sass-lint.yml",
+  "_moduleAliases": {
+    "@server": "dist/server"
+  }
 }
index df7ccda276feb896ff5db63cae72982137be887a..a758a211c22adb516d4a6afd41688a3e92940a20 100755 (executable)
@@ -5,5 +5,5 @@ set -eu
 gawk -i inplace 'BEGIN { found=0 } { if (found || $0 ~ /^{/) { found=1; print }}' ./client/dist/embed-stats.json
 
 npm run concurrently -- -k \
-    "cd client && npm run webpack-bundle-analyzer -- -p 8888 ./dist/en_US/stats.json" \
-    "cd client && npm run webpack-bundle-analyzer -- -p 8889 ./dist/embed-stats.json"
\ No newline at end of file
+    "cd client && npm run webpack-bundle-analyzer -- -p 8888 ./dist/en_US/stats-es2015.json" \
+    "cd client && npm run webpack-bundle-analyzer -- -p 8889 ./dist/embed-stats.json"
index 0d6266056780b6f593f4871a2083fbe372f95d98..c745b1cb243057b2e81edd181f40dc8ead25401b 100755 (executable)
@@ -13,7 +13,7 @@ async function run () {
   {
     const contributors = await fetchGithub('https://api.github.com/repos/chocobozzz/peertube/contributors')
 
-    console.log('# Code\n')
+    console.log('# Code contributors\n')
     for (const contributor of contributors) {
       const contributorUrl = contributor.url.replace('api.github.com/users', 'github.com')
       console.log(` * [${contributor.login}](${contributorUrl})`)
@@ -27,7 +27,7 @@ async function run () {
 
     const translators = await fetchZanata(zanataUsername, zanataToken)
 
-    console.log('\n\n# Translations\n')
+    console.log('\n\n# Translation contributors\n')
     for (const translator of translators) {
       console.log(` * [${translator.username}](https://trad.framasoft.org/zanata/profile/view/${translator.username})`)
     }
index 50511a90684c7ec3599c8818f8ded3509ebd5eb7..5cfa09445537215fd3f9c772955d45a02ddd1b9a 100644 (file)
--- a/server.ts
+++ b/server.ts
@@ -1,3 +1,5 @@
+require('module-alias/register')
+
 // FIXME: https://github.com/nodejs/node/pull/16853
 import { PluginManager } from './server/lib/plugins/plugin-manager'
 
@@ -113,6 +115,7 @@ import { UpdateVideosScheduler } from './server/lib/schedulers/update-videos-sch
 import { YoutubeDlUpdateScheduler } from './server/lib/schedulers/youtube-dl-update-scheduler'
 import { VideosRedundancyScheduler } from './server/lib/schedulers/videos-redundancy-scheduler'
 import { RemoveOldHistoryScheduler } from './server/lib/schedulers/remove-old-history-scheduler'
+import { AutoFollowIndexInstances } from './server/lib/schedulers/auto-follow-index-instances'
 import { isHTTPSignatureDigestValid } from './server/helpers/peertube-crypto'
 import { PeerTubeSocket } from './server/lib/peertube-socket'
 import { updateStreamingPlaylistsInfohashesIfNeeded } from './server/lib/hls'
@@ -258,6 +261,7 @@ async function startApplication () {
   RemoveOldHistoryScheduler.Instance.enable()
   RemoveOldViewsScheduler.Instance.enable()
   PluginsCheckScheduler.Instance.enable()
+  AutoFollowIndexInstances.Instance.enable()
 
   // Redis initialization
   Redis.Instance.init()
index 11504b35427b0883776e750f71f311d5d7a215e2..453ced8bf627c7163b02db9e4a1339fca3202a48 100644 (file)
@@ -16,7 +16,6 @@ import {
 } from '../../middlewares'
 import { getAccountVideoRateValidator, videoCommentGetValidator } from '../../middlewares/validators'
 import { AccountModel } from '../../models/account/account'
-import { ActorModel } from '../../models/activitypub/actor'
 import { ActorFollowModel } from '../../models/activitypub/actor-follow'
 import { VideoModel } from '../../models/video/video'
 import { VideoCommentModel } from '../../models/video/video-comment'
@@ -38,6 +37,7 @@ import { buildDislikeActivity } from '../../lib/activitypub/send/send-dislike'
 import { videoPlaylistElementAPGetValidator, videoPlaylistsGetValidator } from '../../middlewares/validators/videos/video-playlists'
 import { VideoPlaylistModel } from '../../models/video/video-playlist'
 import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
+import { MAccountId, MActorId, MVideo, MVideoAPWithoutCaption } from '@server/typings/models'
 
 const activityPubClientRouter = express.Router()
 
@@ -148,7 +148,7 @@ activityPubClientRouter.get('/redundancy/streaming-playlists/:streamingPlaylistT
 
 activityPubClientRouter.get('/video-playlists/:playlistId',
   executeIfActivityPub,
-  asyncMiddleware(videoPlaylistsGetValidator),
+  asyncMiddleware(videoPlaylistsGetValidator('all')),
   asyncMiddleware(videoPlaylistController)
 )
 activityPubClientRouter.get('/video-playlists/:playlistId/:videoId',
@@ -208,18 +208,19 @@ function getAccountVideoRate (rateType: VideoRateType) {
 
 async function videoController (req: express.Request, res: express.Response) {
   // We need more attributes
-  const video = await VideoModel.loadForGetAPI({ id: res.locals.video.id })
+  const video = await VideoModel.loadForGetAPI({ id: res.locals.onlyVideoWithRights.id }) as MVideoAPWithoutCaption
 
   if (video.url.startsWith(WEBSERVER.URL) === false) return res.redirect(video.url)
 
   // We need captions to render AP object
-  video.VideoCaptions = await VideoCaptionModel.listVideoCaptions(video.id)
+  const captions = await VideoCaptionModel.listVideoCaptions(video.id)
+  const videoWithCaptions = Object.assign(video, { VideoCaptions: captions })
 
-  const audience = getAudience(video.VideoChannel.Account.Actor, video.privacy === VideoPrivacy.PUBLIC)
-  const videoObject = audiencify(video.toActivityPubObject(), audience)
+  const audience = getAudience(videoWithCaptions.VideoChannel.Account.Actor, videoWithCaptions.privacy === VideoPrivacy.PUBLIC)
+  const videoObject = audiencify(videoWithCaptions.toActivityPubObject(), audience)
 
   if (req.path.endsWith('/activity')) {
-    const data = buildCreateActivity(video.url, video.VideoChannel.Account.Actor, videoObject, audience)
+    const data = buildCreateActivity(videoWithCaptions.url, video.VideoChannel.Account.Actor, videoObject, audience)
     return activityPubResponse(activityPubContextify(data), res)
   }
 
@@ -231,13 +232,13 @@ async function videoAnnounceController (req: express.Request, res: express.Respo
 
   if (share.url.startsWith(WEBSERVER.URL) === false) return res.redirect(share.url)
 
-  const { activity } = await buildAnnounceWithVideoAudience(share.Actor, share, res.locals.video, undefined)
+  const { activity } = await buildAnnounceWithVideoAudience(share.Actor, share, res.locals.videoAll, undefined)
 
   return activityPubResponse(activityPubContextify(activity), res)
 }
 
 async function videoAnnouncesController (req: express.Request, res: express.Response) {
-  const video = res.locals.video
+  const video = res.locals.onlyVideo
 
   const handler = async (start: number, count: number) => {
     const result = await VideoShareModel.listAndCountByVideoId(video.id, start, count)
@@ -252,21 +253,21 @@ async function videoAnnouncesController (req: express.Request, res: express.Resp
 }
 
 async function videoLikesController (req: express.Request, res: express.Response) {
-  const video = res.locals.video
+  const video = res.locals.onlyVideo
   const json = await videoRates(req, 'like', video, getVideoLikesActivityPubUrl(video))
 
   return activityPubResponse(activityPubContextify(json), res)
 }
 
 async function videoDislikesController (req: express.Request, res: express.Response) {
-  const video = res.locals.video
+  const video = res.locals.onlyVideo
   const json = await videoRates(req, 'dislike', video, getVideoDislikesActivityPubUrl(video))
 
   return activityPubResponse(activityPubContextify(json), res)
 }
 
 async function videoCommentsController (req: express.Request, res: express.Response) {
-  const video = res.locals.video
+  const video = res.locals.onlyVideo
 
   const handler = async (start: number, count: number) => {
     const result = await VideoCommentModel.listAndCountByVideoId(video.id, start, count)
@@ -301,7 +302,7 @@ async function videoChannelFollowingController (req: express.Request, res: expre
 }
 
 async function videoCommentController (req: express.Request, res: express.Response) {
-  const videoComment = res.locals.videoComment
+  const videoComment = res.locals.videoCommentFull
 
   if (videoComment.url.startsWith(WEBSERVER.URL) === false) return res.redirect(videoComment.url)
 
@@ -337,7 +338,7 @@ async function videoRedundancyController (req: express.Request, res: express.Res
 }
 
 async function videoPlaylistController (req: express.Request, res: express.Response) {
-  const playlist = res.locals.videoPlaylist
+  const playlist = res.locals.videoPlaylistFull
 
   // We need more attributes
   playlist.OwnerAccount = await AccountModel.load(playlist.ownerAccountId)
@@ -350,7 +351,7 @@ async function videoPlaylistController (req: express.Request, res: express.Respo
 }
 
 async function videoPlaylistElementController (req: express.Request, res: express.Response) {
-  const videoPlaylistElement = res.locals.videoPlaylistElement
+  const videoPlaylistElement = res.locals.videoPlaylistElementAP
 
   const json = videoPlaylistElement.toActivityPubObject()
   return activityPubResponse(activityPubContextify(json), res)
@@ -358,7 +359,7 @@ async function videoPlaylistElementController (req: express.Request, res: expres
 
 // ---------------------------------------------------------------------------
 
-async function actorFollowing (req: express.Request, actor: ActorModel) {
+async function actorFollowing (req: express.Request, actor: MActorId) {
   const handler = (start: number, count: number) => {
     return ActorFollowModel.listAcceptedFollowingUrlsForApi([ actor.id ], undefined, start, count)
   }
@@ -366,7 +367,7 @@ async function actorFollowing (req: express.Request, actor: ActorModel) {
   return activityPubCollectionPagination(WEBSERVER.URL + req.path, handler, req.query.page)
 }
 
-async function actorFollowers (req: express.Request, actor: ActorModel) {
+async function actorFollowers (req: express.Request, actor: MActorId) {
   const handler = (start: number, count: number) => {
     return ActorFollowModel.listAcceptedFollowerUrlsForAP([ actor.id ], undefined, start, count)
   }
@@ -374,7 +375,7 @@ async function actorFollowers (req: express.Request, actor: ActorModel) {
   return activityPubCollectionPagination(WEBSERVER.URL + req.path, handler, req.query.page)
 }
 
-async function actorPlaylists (req: express.Request, account: AccountModel) {
+async function actorPlaylists (req: express.Request, account: MAccountId) {
   const handler = (start: number, count: number) => {
     return VideoPlaylistModel.listPublicUrlsOfForAP(account.id, start, count)
   }
@@ -382,7 +383,7 @@ async function actorPlaylists (req: express.Request, account: AccountModel) {
   return activityPubCollectionPagination(WEBSERVER.URL + req.path, handler, req.query.page)
 }
 
-function videoRates (req: express.Request, rateType: VideoRateType, video: VideoModel, url: string) {
+function videoRates (req: express.Request, rateType: VideoRateType, video: MVideo, url: string) {
   const handler = async (start: number, count: number) => {
     const result = await AccountVideoRateModel.listAndCountAccountUrlsByVideoId(rateType, video.id, start, count)
     return {
index 2d3eef22247bc726011017ba54ad61071ff7bb7a..d9df253aa28f05bece936b5f8347016b15992bf8 100644 (file)
@@ -7,7 +7,7 @@ import { asyncMiddleware, checkSignature, localAccountValidator, localVideoChann
 import { activityPubValidator } from '../../middlewares/validators/activitypub/activity'
 import { queue } from 'async'
 import { ActorModel } from '../../models/activitypub/actor'
-import { SignatureActorModel } from '../../typings/models'
+import { MActorDefault, MActorSignature } from '../../typings/models'
 
 const inboxRouter = express.Router()
 
@@ -41,7 +41,8 @@ export {
 
 // ---------------------------------------------------------------------------
 
-const inboxQueue = queue<{ activities: Activity[], signatureActor?: SignatureActorModel, inboxActor?: ActorModel }, Error>((task, cb) => {
+type QueueParam = { activities: Activity[], signatureActor?: MActorSignature, inboxActor?: MActorDefault }
+const inboxQueue = queue<QueueParam, Error>((task, cb) => {
   const options = { signatureActor: task.signatureActor, inboxActor: task.inboxActor }
 
   processActivities(task.activities, options)
index 38b6ec9764bd9365c84546b5178d8eb7e163a830..f3dd2ad7d3e5fed8e1be89003813054cde20a24b 100644 (file)
@@ -6,11 +6,9 @@ import { logger } from '../../helpers/logger'
 import { buildAnnounceActivity, buildCreateActivity } from '../../lib/activitypub/send'
 import { buildAudience } from '../../lib/activitypub/audience'
 import { asyncMiddleware, localAccountValidator, localVideoChannelValidator } from '../../middlewares'
-import { AccountModel } from '../../models/account/account'
-import { ActorModel } from '../../models/activitypub/actor'
 import { VideoModel } from '../../models/video/video'
 import { activityPubResponse } from './utils'
-import { VideoChannelModel } from '../../models/video/video-channel'
+import { MActorLight } from '@server/typings/models'
 
 const outboxRouter = express.Router()
 
@@ -45,14 +43,10 @@ async function outboxController (req: express.Request, res: express.Response) {
   return activityPubResponse(activityPubContextify(json), res)
 }
 
-async function buildActivities (actor: ActorModel, start: number, count: number) {
+async function buildActivities (actor: MActorLight, start: number, count: number) {
   const data = await VideoModel.listAllAndSharedByActorForOutbox(actor.id, start, count)
   const activities: Activity[] = []
 
-  // Avoid too many SQL requests
-  const actors = data.data.map(v => v.VideoChannel.Account.Actor)
-  actors.push(actor)
-
   for (const video of data.data) {
     const byActor = video.VideoChannel.Account.Actor
     const createActivityAudience = buildAudience([ byActor.followersUrl ], video.privacy === VideoPrivacy.PUBLIC)
index 21fa85a085a3ece6acbd41bf74a433f2f0ddfdfd..39a124fc5382dd7efa08406a00885f0c6afb10c7 100644 (file)
@@ -158,7 +158,19 @@ function getAbout (req: express.Request, res: express.Response) {
       name: CONFIG.INSTANCE.NAME,
       shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
       description: CONFIG.INSTANCE.DESCRIPTION,
-      terms: CONFIG.INSTANCE.TERMS
+      terms: CONFIG.INSTANCE.TERMS,
+      codeOfConduct: CONFIG.INSTANCE.CODE_OF_CONDUCT,
+
+      hardwareInformation: CONFIG.INSTANCE.HARDWARE_INFORMATION,
+
+      creationReason: CONFIG.INSTANCE.CREATION_REASON,
+      moderationInformation: CONFIG.INSTANCE.MODERATION_INFORMATION,
+      administrator: CONFIG.INSTANCE.ADMINISTRATOR,
+      maintenanceLifetime: CONFIG.INSTANCE.MAINTENANCE_LIFETIME,
+      businessModel: CONFIG.INSTANCE.BUSINESS_MODEL,
+
+      languages: CONFIG.INSTANCE.LANGUAGES,
+      categories: CONFIG.INSTANCE.CATEGORIES
     }
   }
 
@@ -221,6 +233,18 @@ function customConfig (): CustomConfig {
       shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
       description: CONFIG.INSTANCE.DESCRIPTION,
       terms: CONFIG.INSTANCE.TERMS,
+      codeOfConduct: CONFIG.INSTANCE.CODE_OF_CONDUCT,
+
+      creationReason: CONFIG.INSTANCE.CREATION_REASON,
+      moderationInformation: CONFIG.INSTANCE.MODERATION_INFORMATION,
+      administrator: CONFIG.INSTANCE.ADMINISTRATOR,
+      maintenanceLifetime: CONFIG.INSTANCE.MAINTENANCE_LIFETIME,
+      businessModel: CONFIG.INSTANCE.BUSINESS_MODEL,
+      hardwareInformation: CONFIG.INSTANCE.HARDWARE_INFORMATION,
+
+      languages: CONFIG.INSTANCE.LANGUAGES,
+      categories: CONFIG.INSTANCE.CATEGORIES,
+
       isNSFW: CONFIG.INSTANCE.IS_NSFW,
       defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE,
       defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
@@ -300,6 +324,18 @@ function customConfig (): CustomConfig {
         enabled: CONFIG.FOLLOWERS.INSTANCE.ENABLED,
         manualApproval: CONFIG.FOLLOWERS.INSTANCE.MANUAL_APPROVAL
       }
+    },
+    followings: {
+      instance: {
+        autoFollowBack: {
+          enabled: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_BACK.ENABLED
+        },
+
+        autoFollowIndex: {
+          enabled: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.ENABLED,
+          indexUrl: CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.INDEX_URL
+        }
+      }
     }
   }
 }
index 9a1e30b8367f301fa3e9d2f111230c4152ee8046..349650aca65e290c53b33794cb895a550dd60da3 100644 (file)
@@ -19,6 +19,7 @@ import { getOrCreateActorAndServerAndModel, getOrCreateVideoAndAccountAndChannel
 import { logger } from '../../helpers/logger'
 import { VideoChannelModel } from '../../models/video/video-channel'
 import { loadActorUrlOrGetFromWebfinger } from '../../helpers/webfinger'
+import { MChannelAccountDefault, MVideoAccountLightBlacklistAllFiles } from '../../typings/models'
 
 const searchRouter = express.Router()
 
@@ -84,7 +85,7 @@ async function searchVideoChannelsDB (query: VideoChannelsSearchQuery, res: expr
 }
 
 async function searchVideoChannelURI (search: string, isWebfingerSearch: boolean, res: express.Response) {
-  let videoChannel: VideoChannelModel
+  let videoChannel: MChannelAccountDefault
   let uri = search
 
   if (isWebfingerSearch) {
@@ -137,7 +138,7 @@ async function searchVideosDB (query: VideosSearchQuery, res: express.Response)
 }
 
 async function searchVideoURI (url: string, res: express.Response) {
-  let video: VideoModel
+  let video: MVideoAccountLightBlacklistAllFiles
 
   // Check if we can fetch a remote video with the URL
   if (isUserAbleToSearchRemoteURI(res)) {
index d38ce91debdde0aa5979c8f1a1798447806db784..37647622b6b3c525a98965fdc74c2ecf75d27d5f 100644 (file)
@@ -25,6 +25,7 @@ import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
 import { JobQueue } from '../../../lib/job-queue'
 import { removeRedundancyOf } from '../../../lib/redundancy'
 import { sequelizeTypescript } from '../../../initializers/database'
+import { autoFollowBackIfNeeded } from '../../../lib/activitypub/follow'
 
 const serverFollowsRouter = express.Router()
 serverFollowsRouter.get('/following',
@@ -172,5 +173,7 @@ async function acceptFollower (req: express.Request, res: express.Response) {
   follow.state = 'accepted'
   await follow.save()
 
+  await autoFollowBackIfNeeded(follow)
+
   return res.status(204).end()
 }
index ae40e86f8c194b4972c316ac281f60dee97fe722..27351c1a954b8d68ed648fac005e5462bacc0962 100644 (file)
@@ -48,6 +48,7 @@ import { CONFIG } from '../../../initializers/config'
 import { sequelizeTypescript } from '../../../initializers/database'
 import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
 import { UserRegister } from '../../../../shared/models/users/user-register.model'
+import { MUser, MUserAccountDefault } from '@server/typings/models'
 
 const auditLogger = auditLoggerFactory('users')
 
@@ -195,7 +196,7 @@ async function createUser (req: express.Request, res: express.Response) {
     videoQuota: body.videoQuota,
     videoQuotaDaily: body.videoQuotaDaily,
     adminFlags: body.adminFlags || UserAdminFlag.NONE
-  })
+  }) as MUser
 
   const { user, account } = await createUserAccountAndChannelAndPlaylist({ userToCreate: userToCreate })
 
@@ -359,7 +360,7 @@ function success (req: express.Request, res: express.Response) {
   res.end()
 }
 
-async function changeUserBlock (res: express.Response, user: UserModel, block: boolean, reason?: string) {
+async function changeUserBlock (res: express.Response, user: MUserAccountDefault, block: boolean, reason?: string) {
   const oldUserAuditView = new UserAuditView(user.toFormattedJSON())
 
   user.blocked = block
index e7ed3de6466d7fa8ea21cdc4c36fd789403a54f7..bf872ca52d9c0206462bfc26a7e5f92e52d50418 100644 (file)
@@ -23,15 +23,12 @@ import { createReqFiles } from '../../../helpers/express-utils'
 import { UserVideoQuota } from '../../../../shared/models/users/user-video-quota.model'
 import { updateAvatarValidator } from '../../../middlewares/validators/avatar'
 import { updateActorAvatarFile } from '../../../lib/avatar'
-import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger'
 import { VideoImportModel } from '../../../models/video/video-import'
 import { AccountModel } from '../../../models/account/account'
 import { CONFIG } from '../../../initializers/config'
 import { sequelizeTypescript } from '../../../initializers/database'
 import { sendVerifyUserEmail } from '../../../lib/user'
 
-const auditLogger = auditLoggerFactory('users-me')
-
 const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR })
 
 const meRouter = express.Router()
@@ -130,7 +127,7 @@ async function getUserInformation (req: express.Request, res: express.Response)
   // We did not load channels in res.locals.user
   const user = await UserModel.loadByUsernameAndPopulateChannels(res.locals.oauth.token.user.username)
 
-  return res.json(user.toFormattedJSON({}))
+  return res.json(user.toFormattedJSON())
 }
 
 async function getUserVideoQuotaUsed (req: express.Request, res: express.Response) {
@@ -147,7 +144,7 @@ async function getUserVideoQuotaUsed (req: express.Request, res: express.Respons
 }
 
 async function getUserVideoRating (req: express.Request, res: express.Response) {
-  const videoId = res.locals.video.id
+  const videoId = res.locals.videoId.id
   const accountId = +res.locals.oauth.token.User.Account.id
 
   const ratingObj = await AccountVideoRateModel.load(accountId, videoId, null)
@@ -165,8 +162,6 @@ async function deleteMe (req: express.Request, res: express.Response) {
 
   await user.destroy()
 
-  auditLogger.delete(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON({})))
-
   return res.sendStatus(204)
 }
 
@@ -175,7 +170,6 @@ async function updateMe (req: express.Request, res: express.Response) {
   let sendVerificationEmail = false
 
   const user = res.locals.oauth.token.user
-  const oldUserAuditView = new UserAuditView(user.toFormattedJSON({}))
 
   if (body.password !== undefined) user.password = body.password
   if (body.nsfwPolicy !== undefined) user.nsfwPolicy = body.nsfwPolicy
@@ -184,6 +178,8 @@ async function updateMe (req: express.Request, res: express.Response) {
   if (body.videosHistoryEnabled !== undefined) user.videosHistoryEnabled = body.videosHistoryEnabled
   if (body.videoLanguages !== undefined) user.videoLanguages = body.videoLanguages
   if (body.theme !== undefined) user.theme = body.theme
+  if (body.noInstanceConfigWarningModal !== undefined) user.noInstanceConfigWarningModal = body.noInstanceConfigWarningModal
+  if (body.noWelcomeModal !== undefined) user.noWelcomeModal = body.noWelcomeModal
 
   if (body.email !== undefined) {
     if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) {
@@ -195,17 +191,17 @@ async function updateMe (req: express.Request, res: express.Response) {
   }
 
   await sequelizeTypescript.transaction(async t => {
-    const userAccount = await AccountModel.load(user.Account.id)
-
     await user.save({ transaction: t })
 
-    if (body.displayName !== undefined) userAccount.name = body.displayName
-    if (body.description !== undefined) userAccount.description = body.description
-    await userAccount.save({ transaction: t })
+    if (body.displayName !== undefined || body.description !== undefined) {
+      const userAccount = await AccountModel.load(user.Account.id, t)
 
-    await sendUpdateActor(userAccount, t)
+      if (body.displayName !== undefined) userAccount.name = body.displayName
+      if (body.description !== undefined) userAccount.description = body.description
+      await userAccount.save({ transaction: t })
 
-    auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON({})), oldUserAuditView)
+      await sendUpdateActor(userAccount, t)
+    }
   })
 
   if (sendVerificationEmail === true) {
@@ -218,13 +214,10 @@ async function updateMe (req: express.Request, res: express.Response) {
 async function updateMyAvatar (req: express.Request, res: express.Response) {
   const avatarPhysicalFile = req.files[ 'avatarfile' ][ 0 ]
   const user = res.locals.oauth.token.user
-  const oldUserAuditView = new UserAuditView(user.toFormattedJSON({}))
 
   const userAccount = await AccountModel.load(user.Account.id)
 
   const avatar = await updateActorAvatarFile(avatarPhysicalFile, userAccount)
 
-  auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON({})), oldUserAuditView)
-
   return res.json({ avatar: avatar.toFormattedJSON() })
 }
index 7025c0ff15e88b1aa15c29aa721b5e07737aa1c9..4da1f34963e01638947c3a9565ea0ee808ee906a 100644 (file)
@@ -7,7 +7,6 @@ import {
   setDefaultPagination,
   userHistoryRemoveValidator
 } from '../../../middlewares'
-import { UserModel } from '../../../models/account/user'
 import { getFormattedObjects } from '../../../helpers/utils'
 import { UserVideoHistoryModel } from '../../../models/account/user-video-history'
 import { sequelizeTypescript } from '../../../initializers'
index f146284e4acc50b7f9c60155533214402b6e7076..017f5219edeb5a3b65bf6bbe855243d7d35aaefb 100644 (file)
@@ -76,7 +76,8 @@ async function updateNotificationSettings (req: express.Request, res: express.Re
     newFollow: body.newFollow,
     newUserRegistration: body.newUserRegistration,
     commentMention: body.commentMention,
-    newInstanceFollower: body.newInstanceFollower
+    newInstanceFollower: body.newInstanceFollower,
+    autoInstanceFollowing: body.autoInstanceFollowing
   }
 
   await UserNotificationSettingModel.update(values, query)
index 81a03a62bc85838d8409d5ea13bbf7d9515b4eda..acc5b2987c652e6f10f69951bab16a45983b41c8 100644 (file)
@@ -19,7 +19,7 @@ import { VideoChannelModel } from '../../models/video/video-channel'
 import { videoChannelsNameWithHostValidator, videosSortValidator } from '../../middlewares/validators'
 import { sendUpdateActor } from '../../lib/activitypub/send'
 import { VideoChannelCreate, VideoChannelUpdate } from '../../../shared'
-import { createVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel'
+import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel'
 import { buildNSFWFilter, createReqFiles, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
 import { setAsyncActorKeys } from '../../lib/activitypub'
 import { AccountModel } from '../../models/account/account'
@@ -35,6 +35,7 @@ import { VideoPlaylistModel } from '../../models/video/video-playlist'
 import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists'
 import { CONFIG } from '../../initializers/config'
 import { sequelizeTypescript } from '../../initializers/database'
+import { MChannelAccountDefault } from '@server/typings/models'
 
 const auditLogger = auditLoggerFactory('channels')
 const 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
 async function addVideoChannel (req: express.Request, res: express.Response) {
   const videoChannelInfo: VideoChannelCreate = req.body
 
-  const videoChannelCreated: VideoChannelModel = await sequelizeTypescript.transaction(async t => {
+  const videoChannelCreated = await sequelizeTypescript.transaction(async t => {
     const account = await AccountModel.load(res.locals.oauth.token.User.Account.id, t)
 
-    return createVideoChannel(videoChannelInfo, account, t)
+    return createLocalVideoChannel(videoChannelInfo, account, t)
   })
 
   setAsyncActorKeys(videoChannelCreated.Actor)
@@ -181,7 +182,7 @@ async function updateVideoChannel (req: express.Request, res: express.Response)
         }
       }
 
-      const videoChannelInstanceUpdated = await videoChannelInstance.save(sequelizeOptions)
+      const videoChannelInstanceUpdated = await videoChannelInstance.save(sequelizeOptions) as MChannelAccountDefault
       await sendUpdateActor(videoChannelInstanceUpdated, t)
 
       auditLogger.update(
index bd454f553ee26a58942c33a3cb130969a75d76c4..d9f0ff9257bac20327269d5cad5e68c1982ef35f 100644 (file)
@@ -40,7 +40,7 @@ import { JobQueue } from '../../lib/job-queue'
 import { CONFIG } from '../../initializers/config'
 import { sequelizeTypescript } from '../../initializers/database'
 import { createPlaylistMiniatureFromExisting } from '../../lib/thumbnail'
-import { VideoModel } from '../../models/video/video'
+import { MVideoPlaylistFull, MVideoPlaylistThumbnail, MVideoThumbnail } from '@server/typings/models'
 
 const reqThumbnailFile = createReqFiles([ 'thumbnailfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { thumbnailfile: CONFIG.STORAGE.TMP_DIR })
 
@@ -58,7 +58,7 @@ videoPlaylistRouter.get('/',
 )
 
 videoPlaylistRouter.get('/:playlistId',
-  asyncMiddleware(videoPlaylistsGetValidator),
+  asyncMiddleware(videoPlaylistsGetValidator('summary')),
   getVideoPlaylist
 )
 
@@ -83,7 +83,7 @@ videoPlaylistRouter.delete('/:playlistId',
 )
 
 videoPlaylistRouter.get('/:playlistId/videos',
-  asyncMiddleware(videoPlaylistsGetValidator),
+  asyncMiddleware(videoPlaylistsGetValidator('summary')),
   paginationValidator,
   setDefaultPagination,
   optionalAuthenticate,
@@ -140,7 +140,7 @@ async function listVideoPlaylists (req: express.Request, res: express.Response)
 }
 
 function getVideoPlaylist (req: express.Request, res: express.Response) {
-  const videoPlaylist = res.locals.videoPlaylist
+  const videoPlaylist = res.locals.videoPlaylistSummary
 
   if (videoPlaylist.isOutdated()) {
     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) {
     description: videoPlaylistInfo.description,
     privacy: videoPlaylistInfo.privacy || VideoPlaylistPrivacy.PRIVATE,
     ownerAccountId: user.Account.id
-  })
+  }) as MVideoPlaylistFull
 
   videoPlaylist.url = getVideoPlaylistActivityPubUrl(videoPlaylist) // We use the UUID, so set the URL after building the object
 
@@ -175,8 +175,8 @@ async function addVideoPlaylist (req: express.Request, res: express.Response) {
     ? await createPlaylistMiniatureFromExisting(thumbnailField[0].path, videoPlaylist, false)
     : undefined
 
-  const videoPlaylistCreated: VideoPlaylistModel = await sequelizeTypescript.transaction(async t => {
-    const videoPlaylistCreated = await videoPlaylist.save({ transaction: t })
+  const videoPlaylistCreated = await sequelizeTypescript.transaction(async t => {
+    const videoPlaylistCreated = await videoPlaylist.save({ transaction: t }) as MVideoPlaylistFull
 
     if (thumbnailModel) {
       thumbnailModel.automaticallyGenerated = false
@@ -201,7 +201,7 @@ async function addVideoPlaylist (req: express.Request, res: express.Response) {
 }
 
 async function updateVideoPlaylist (req: express.Request, res: express.Response) {
-  const videoPlaylistInstance = res.locals.videoPlaylist
+  const videoPlaylistInstance = res.locals.videoPlaylistFull
   const videoPlaylistFieldsSave = videoPlaylistInstance.toJSON()
   const videoPlaylistInfoToUpdate = req.body as VideoPlaylistUpdate
 
@@ -275,7 +275,7 @@ async function updateVideoPlaylist (req: express.Request, res: express.Response)
 }
 
 async function removeVideoPlaylist (req: express.Request, res: express.Response) {
-  const videoPlaylistInstance = res.locals.videoPlaylist
+  const videoPlaylistInstance = res.locals.videoPlaylistSummary
 
   await sequelizeTypescript.transaction(async t => {
     await videoPlaylistInstance.destroy({ transaction: t })
@@ -290,10 +290,10 @@ async function removeVideoPlaylist (req: express.Request, res: express.Response)
 
 async function addVideoInPlaylist (req: express.Request, res: express.Response) {
   const body: VideoPlaylistElementCreate = req.body
-  const videoPlaylist = res.locals.videoPlaylist
-  const video = res.locals.video
+  const videoPlaylist = res.locals.videoPlaylistFull
+  const video = res.locals.onlyVideo
 
-  const playlistElement: VideoPlaylistElementModel = await sequelizeTypescript.transaction(async t => {
+  const playlistElement = await sequelizeTypescript.transaction(async t => {
     const position = await VideoPlaylistElementModel.getNextPositionOf(videoPlaylist.id, t)
 
     const playlistElement = await VideoPlaylistElementModel.create({
@@ -330,7 +330,7 @@ async function addVideoInPlaylist (req: express.Request, res: express.Response)
 
 async function updateVideoPlaylistElement (req: express.Request, res: express.Response) {
   const body: VideoPlaylistElementUpdate = req.body
-  const videoPlaylist = res.locals.videoPlaylist
+  const videoPlaylist = res.locals.videoPlaylistFull
   const videoPlaylistElement = res.locals.videoPlaylistElement
 
   const playlistElement: VideoPlaylistElementModel = await sequelizeTypescript.transaction(async t => {
@@ -354,7 +354,7 @@ async function updateVideoPlaylistElement (req: express.Request, res: express.Re
 
 async function removeVideoFromPlaylist (req: express.Request, res: express.Response) {
   const videoPlaylistElement = res.locals.videoPlaylistElement
-  const videoPlaylist = res.locals.videoPlaylist
+  const videoPlaylist = res.locals.videoPlaylistFull
   const positionToDelete = videoPlaylistElement.position
 
   await sequelizeTypescript.transaction(async t => {
@@ -381,7 +381,7 @@ async function removeVideoFromPlaylist (req: express.Request, res: express.Respo
 }
 
 async function reorderVideosPlaylist (req: express.Request, res: express.Response) {
-  const videoPlaylist = res.locals.videoPlaylist
+  const videoPlaylist = res.locals.videoPlaylistFull
   const body: VideoPlaylistReorder = req.body
 
   const start: number = body.startPosition
@@ -434,7 +434,7 @@ async function reorderVideosPlaylist (req: express.Request, res: express.Respons
 }
 
 async function getVideoPlaylistVideos (req: express.Request, res: express.Response) {
-  const videoPlaylistInstance = res.locals.videoPlaylist
+  const videoPlaylistInstance = res.locals.videoPlaylistSummary
   const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
   const server = await getServerActor()
 
@@ -453,7 +453,7 @@ async function getVideoPlaylistVideos (req: express.Request, res: express.Respon
   return res.json(getFormattedObjects(resultList.data, resultList.total, options))
 }
 
-async function regeneratePlaylistThumbnail (videoPlaylist: VideoPlaylistModel) {
+async function regeneratePlaylistThumbnail (videoPlaylist: MVideoPlaylistThumbnail) {
   await videoPlaylist.Thumbnail.destroy()
   videoPlaylist.Thumbnail = null
 
@@ -461,7 +461,7 @@ async function regeneratePlaylistThumbnail (videoPlaylist: VideoPlaylistModel) {
   if (firstElement) await generateThumbnailForPlaylist(videoPlaylist, firstElement.Video)
 }
 
-async function generateThumbnailForPlaylist (videoPlaylist: VideoPlaylistModel, video: VideoModel) {
+async function generateThumbnailForPlaylist (videoPlaylist: MVideoPlaylistThumbnail, video: MVideoThumbnail) {
   logger.info('Generating default thumbnail to playlist %s.', videoPlaylist.url)
 
   const inputPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getMiniature().filename)
index 77808466c3cc8128b33423d89af40c77dc3171fe..4ae899b7e7e91f5e64ed18ab169e60fb28bc5334 100644 (file)
@@ -1,7 +1,7 @@
 import * as express from 'express'
 import { UserRight, VideoAbuseCreate, VideoAbuseState } from '../../../../shared'
 import { logger } from '../../../helpers/logger'
-import { getFormattedObjects } from '../../../helpers/utils'
+import { getFormattedObjects, getServerActor } from '../../../helpers/utils'
 import { sequelizeTypescript } from '../../../initializers'
 import {
   asyncMiddleware,
@@ -21,6 +21,7 @@ import { VideoAbuseModel } from '../../../models/video/video-abuse'
 import { auditLoggerFactory, VideoAbuseAuditView } from '../../../helpers/audit-logger'
 import { Notifier } from '../../../lib/notifier'
 import { sendVideoAbuse } from '../../../lib/activitypub/send/send-flag'
+import { MVideoAbuseAccountVideo } from '../../../typings/models/video'
 
 const auditLogger = auditLoggerFactory('abuse')
 const abuseVideoRouter = express.Router()
@@ -61,7 +62,16 @@ export {
 // ---------------------------------------------------------------------------
 
 async function listVideoAbuses (req: express.Request, res: express.Response) {
-  const resultList = await VideoAbuseModel.listForApi(req.query.start, req.query.count, req.query.sort)
+  const user = res.locals.oauth.token.user
+  const serverActor = await getServerActor()
+
+  const resultList = await VideoAbuseModel.listForApi({
+    start: req.query.start,
+    count: req.query.count,
+    sort: req.query.sort,
+    serverAccountId: serverActor.Account.id,
+    user
+  })
 
   return res.json(getFormattedObjects(resultList.data, resultList.total))
 }
@@ -94,10 +104,10 @@ async function deleteVideoAbuse (req: express.Request, res: express.Response) {
 }
 
 async function reportVideoAbuse (req: express.Request, res: express.Response) {
-  const videoInstance = res.locals.video
+  const videoInstance = res.locals.videoAll
   const body: VideoAbuseCreate = req.body
 
-  const videoAbuse: VideoAbuseModel = await sequelizeTypescript.transaction(async t => {
+  const videoAbuse = await sequelizeTypescript.transaction(async t => {
     const reporterAccount = await AccountModel.load(res.locals.oauth.token.User.Account.id, t)
 
     const abuseToCreate = {
@@ -107,7 +117,7 @@ async function reportVideoAbuse (req: express.Request, res: express.Response) {
       state: VideoAbuseState.PENDING
     }
 
-    const videoAbuseInstance = await VideoAbuseModel.create(abuseToCreate, { transaction: t })
+    const videoAbuseInstance: MVideoAbuseAccountVideo = await VideoAbuseModel.create(abuseToCreate, { transaction: t })
     videoAbuseInstance.Video = videoInstance
     videoAbuseInstance.Account = reporterAccount
 
index 9ff494defbd74746ad8a551190d7db245f4b87ce..2a667480d468397438ec5e48d4f074ecafe13471 100644 (file)
@@ -1,5 +1,5 @@
 import * as express from 'express'
-import { VideoBlacklist, UserRight, VideoBlacklistCreate, VideoBlacklistType } from '../../../../shared'
+import { UserRight, VideoBlacklistCreate, VideoBlacklistType } from '../../../../shared'
 import { logger } from '../../../helpers/logger'
 import { getFormattedObjects } from '../../../helpers/utils'
 import {
@@ -11,15 +11,16 @@ import {
   setBlacklistSort,
   setDefaultPagination,
   videosBlacklistAddValidator,
+  videosBlacklistFiltersValidator,
   videosBlacklistRemoveValidator,
-  videosBlacklistUpdateValidator,
-  videosBlacklistFiltersValidator
+  videosBlacklistUpdateValidator
 } from '../../../middlewares'
 import { VideoBlacklistModel } from '../../../models/video/video-blacklist'
 import { sequelizeTypescript } from '../../../initializers'
 import { Notifier } from '../../../lib/notifier'
 import { sendDeleteVideo } from '../../../lib/activitypub/send'
 import { federateVideoIfNeeded } from '../../../lib/activitypub'
+import { MVideoBlacklistVideo } from '@server/typings/models'
 
 const blacklistRouter = express.Router()
 
@@ -64,7 +65,7 @@ export {
 // ---------------------------------------------------------------------------
 
 async function addVideoToBlacklist (req: express.Request, res: express.Response) {
-  const videoInstance = res.locals.video
+  const videoInstance = res.locals.videoAll
   const body: VideoBlacklistCreate = req.body
 
   const toCreate = {
@@ -74,7 +75,7 @@ async function addVideoToBlacklist (req: express.Request, res: express.Response)
     type: VideoBlacklistType.MANUAL
   }
 
-  const blacklist = await VideoBlacklistModel.create(toCreate)
+  const blacklist: MVideoBlacklistVideo = await VideoBlacklistModel.create(toCreate)
   blacklist.Video = videoInstance
 
   if (body.unfederate === true) {
@@ -83,7 +84,7 @@ async function addVideoToBlacklist (req: express.Request, res: express.Response)
 
   Notifier.Instance.notifyOnVideoBlacklist(blacklist)
 
-  logger.info('Video %s blacklisted.', res.locals.video.uuid)
+  logger.info('Video %s blacklisted.', videoInstance.uuid)
 
   return res.type('json').status(204).end()
 }
@@ -108,7 +109,7 @@ async function listBlacklist (req: express.Request, res: express.Response) {
 
 async function removeVideoFromBlacklistController (req: express.Request, res: express.Response) {
   const videoBlacklist = res.locals.videoBlacklist
-  const video = res.locals.video
+  const video = res.locals.videoAll
 
   const videoBlacklistType = await sequelizeTypescript.transaction(async t => {
     const unfederated = videoBlacklist.unfederated
@@ -135,7 +136,7 @@ async function removeVideoFromBlacklistController (req: express.Request, res: ex
     Notifier.Instance.notifyOnNewVideoIfNeeded(video)
   }
 
-  logger.info('Video %s removed from blacklist.', res.locals.video.uuid)
+  logger.info('Video %s removed from blacklist.', video.uuid)
 
   return res.type('json').status(204).end()
 }
index 44c255232bd83cd8d467139091d32c03c89672cd..37481d12f525b4291868bd626934d5a214a41df4 100644 (file)
@@ -10,6 +10,7 @@ import { federateVideoIfNeeded } from '../../../lib/activitypub'
 import { moveAndProcessCaptionFile } from '../../../helpers/captions-utils'
 import { CONFIG } from '../../../initializers/config'
 import { sequelizeTypescript } from '../../../initializers/database'
+import { MVideoCaptionVideo } from '@server/typings/models'
 
 const reqVideoCaptionAdd = createReqFiles(
   [ 'captionfile' ],
@@ -46,19 +47,19 @@ export {
 // ---------------------------------------------------------------------------
 
 async function listVideoCaptions (req: express.Request, res: express.Response) {
-  const data = await VideoCaptionModel.listVideoCaptions(res.locals.video.id)
+  const data = await VideoCaptionModel.listVideoCaptions(res.locals.videoId.id)
 
   return res.json(getFormattedObjects(data, data.length))
 }
 
 async function addVideoCaption (req: express.Request, res: express.Response) {
   const videoCaptionPhysicalFile = req.files['captionfile'][0]
-  const video = res.locals.video
+  const video = res.locals.videoAll
 
   const videoCaption = new VideoCaptionModel({
     videoId: video.id,
     language: req.params.captionLanguage
-  })
+  }) as MVideoCaptionVideo
   videoCaption.Video = video
 
   // Move physical file
@@ -75,7 +76,7 @@ async function addVideoCaption (req: express.Request, res: express.Response) {
 }
 
 async function deleteVideoCaption (req: express.Request, res: express.Response) {
-  const video = res.locals.video
+  const video = res.locals.videoAll
   const videoCaption = res.locals.videoCaption
 
   await sequelizeTypescript.transaction(async t => {
index bc6d81a7c38f457e7a93032fef2fdfb103c8bc74..b2b06b170ead39981e2fe35c3d01f56afe52b795 100644 (file)
@@ -27,9 +27,6 @@ import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../.
 import { AccountModel } from '../../../models/account/account'
 import { Notifier } from '../../../lib/notifier'
 import { Hooks } from '../../../lib/plugins/hooks'
-import { ActorModel } from '../../../models/activitypub/actor'
-import { VideoChannelModel } from '../../../models/video/video-channel'
-import { VideoModel } from '../../../models/video/video'
 import { sendDeleteVideoComment } from '../../../lib/activitypub/send'
 
 const auditLogger = auditLoggerFactory('comments')
@@ -75,7 +72,7 @@ export {
 // ---------------------------------------------------------------------------
 
 async function listVideoThreads (req: express.Request, res: express.Response) {
-  const video = res.locals.video
+  const video = res.locals.onlyVideo
   const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
 
   let resultList: ResultList<VideoCommentModel>
@@ -86,7 +83,7 @@ async function listVideoThreads (req: express.Request, res: express.Response) {
       start: req.query.start,
       count: req.query.count,
       sort: req.query.sort,
-      user: user
+      user
     }, 'filter:api.video-threads.list.params')
 
     resultList = await Hooks.wrapPromiseFun(
@@ -105,7 +102,7 @@ async function listVideoThreads (req: express.Request, res: express.Response) {
 }
 
 async function listVideoThreadComments (req: express.Request, res: express.Response) {
-  const video = res.locals.video
+  const video = res.locals.onlyVideo
   const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
 
   let resultList: ResultList<VideoCommentModel>
@@ -141,7 +138,7 @@ async function addVideoCommentThread (req: express.Request, res: express.Respons
     return createVideoComment({
       text: videoCommentInfo.text,
       inReplyToComment: null,
-      video: res.locals.video,
+      video: res.locals.videoAll,
       account
     }, t)
   })
@@ -164,8 +161,8 @@ async function addVideoCommentReply (req: express.Request, res: express.Response
 
     return createVideoComment({
       text: videoCommentInfo.text,
-      inReplyToComment: res.locals.videoComment,
-      video: res.locals.video,
+      inReplyToComment: res.locals.videoCommentFull,
+      video: res.locals.videoAll,
       account
     }, t)
   })
@@ -179,7 +176,7 @@ async function addVideoCommentReply (req: express.Request, res: express.Response
 }
 
 async function removeVideoComment (req: express.Request, res: express.Response) {
-  const videoCommentInstance = res.locals.videoComment
+  const videoCommentInstance = res.locals.videoCommentFull
 
   await sequelizeTypescript.transaction(async t => {
     await videoCommentInstance.destroy({ transaction: t })
index 04c9b547be7909079df9130a1c4495bdd6070a20..28ced58368b2939f0957d09cb0f69f0723793646 100644 (file)
@@ -1,6 +1,5 @@
 import * as express from 'express'
 import * as magnetUtil from 'magnet-uri'
-import 'multer'
 import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger'
 import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoImportAddValidator } from '../../../middlewares'
 import { MIMETYPES } from '../../../initializers/constants'
@@ -15,7 +14,6 @@ import { VideoImportModel } from '../../../models/video/video-import'
 import { JobQueue } from '../../../lib/job-queue/job-queue'
 import { join } from 'path'
 import { isArray } from '../../../helpers/custom-validators/misc'
-import { VideoChannelModel } from '../../../models/video/video-channel'
 import * as Bluebird from 'bluebird'
 import * as parseTorrent from 'parse-torrent'
 import { getSecureTorrentName } from '../../../helpers/utils'
@@ -25,8 +23,16 @@ import { CONFIG } from '../../../initializers/config'
 import { sequelizeTypescript } from '../../../initializers/database'
 import { createVideoMiniatureFromExisting } from '../../../lib/thumbnail'
 import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
-import { ThumbnailModel } from '../../../models/video/thumbnail'
-import { UserModel } from '../../../models/account/user'
+import {
+  MChannelAccountDefault,
+  MThumbnail,
+  MUser,
+  MVideoAccountDefault,
+  MVideoTag,
+  MVideoThumbnailAccountDefault,
+  MVideoWithBlacklistLight
+} from '@server/typings/models'
+import { MVideoImport, MVideoImportFormattable } from '@server/typings/models/video/video-import'
 
 const auditLogger = auditLoggerFactory('video-imports')
 const videoImportsRouter = express.Router()
@@ -184,8 +190,8 @@ function buildVideo (channelId: number, body: VideoImportCreate, importData: You
     category: body.category || importData.category,
     licence: body.licence || importData.licence,
     language: body.language || undefined,
-    commentsEnabled: body.commentsEnabled || true,
-    downloadEnabled: body.downloadEnabled || true,
+    commentsEnabled: body.commentsEnabled !== false, // If the value is not "false", the default is "true"
+    downloadEnabled: body.downloadEnabled !== false,
     waitTranscoding: body.waitTranscoding || false,
     state: VideoState.TO_IMPORT,
     nsfw: body.nsfw || importData.nsfw || false,
@@ -225,28 +231,28 @@ async function processPreview (req: express.Request, video: VideoModel) {
 }
 
 function insertIntoDB (parameters: {
-  video: VideoModel,
-  thumbnailModel: ThumbnailModel,
-  previewModel: ThumbnailModel,
-  videoChannel: VideoChannelModel,
+  video: MVideoThumbnailAccountDefault,
+  thumbnailModel: MThumbnail,
+  previewModel: MThumbnail,
+  videoChannel: MChannelAccountDefault,
   tags: string[],
-  videoImportAttributes: Partial<VideoImportModel>,
-  user: UserModel
-}): Bluebird<VideoImportModel> {
+  videoImportAttributes: Partial<MVideoImport>,
+  user: MUser
+}): Bluebird<MVideoImportFormattable> {
   const { video, thumbnailModel, previewModel, videoChannel, tags, videoImportAttributes, user } = parameters
 
   return sequelizeTypescript.transaction(async t => {
     const sequelizeOptions = { transaction: t }
 
     // Save video object in database
-    const videoCreated = await video.save(sequelizeOptions)
+    const videoCreated = await video.save(sequelizeOptions) as (MVideoAccountDefault & MVideoWithBlacklistLight & MVideoTag)
     videoCreated.VideoChannel = videoChannel
 
     if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
     if (previewModel) await videoCreated.addAndSaveThumbnail(previewModel, t)
 
     await autoBlacklistVideoIfNeeded({
-      video,
+      video: videoCreated,
       user,
       notify: false,
       isRemote: false,
@@ -268,7 +274,7 @@ function insertIntoDB (parameters: {
     const videoImport = await VideoImportModel.create(
       Object.assign({ videoId: videoCreated.id }, videoImportAttributes),
       sequelizeOptions
-    )
+    ) as MVideoImportFormattable
     videoImport.Video = videoCreated
 
     return videoImport
index 155ca4678f146e6aadc231ddb1d8a68be3ddb675..19da504c75860357c4520e543e521063774a6ad3 100644 (file)
@@ -63,6 +63,7 @@ import { createVideoMiniatureFromExisting, generateVideoMiniature } from '../../
 import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
 import { VideoTranscodingPayload } from '../../../lib/job-queue/handlers/video-transcoding'
 import { Hooks } from '../../../lib/plugins/hooks'
+import { MVideoDetails, MVideoFullLight } from '@server/typings/models'
 
 const auditLogger = auditLoggerFactory('videos')
 const videosRouter = express.Router()
@@ -185,7 +186,7 @@ async function addVideo (req: express.Request, res: express.Response) {
     licence: videoInfo.licence,
     language: videoInfo.language,
     commentsEnabled: videoInfo.commentsEnabled || false,
-    downloadEnabled: videoInfo.downloadEnabled || true,
+    downloadEnabled: videoInfo.downloadEnabled !== false, // If the value is not "false", the default is "true"
     waitTranscoding: videoInfo.waitTranscoding || false,
     state: CONFIG.TRANSCODING.ENABLED ? VideoState.TO_TRANSCODE : VideoState.PUBLISHED,
     nsfw: videoInfo.nsfw || false,
@@ -197,7 +198,7 @@ async function addVideo (req: express.Request, res: express.Response) {
     originallyPublishedAt: videoInfo.originallyPublishedAt
   }
 
-  const video = new VideoModel(videoData)
+  const video = new VideoModel(videoData) as MVideoDetails
   video.url = getVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object
 
   const videoFile = new VideoFileModel({
@@ -238,7 +239,7 @@ async function addVideo (req: express.Request, res: express.Response) {
   const { videoCreated } = await sequelizeTypescript.transaction(async t => {
     const sequelizeOptions = { transaction: t }
 
-    const videoCreated = await video.save(sequelizeOptions)
+    const videoCreated = await video.save(sequelizeOptions) as MVideoFullLight
 
     await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
     await videoCreated.addAndSaveThumbnail(previewModel, t)
@@ -318,7 +319,7 @@ async function addVideo (req: express.Request, res: express.Response) {
 }
 
 async function updateVideo (req: express.Request, res: express.Response) {
-  const videoInstance = res.locals.video
+  const videoInstance = res.locals.videoAll
   const videoFieldsSave = videoInstance.toJSON()
   const oldVideoAuditView = new VideoAuditView(videoInstance.toFormattedDetailsJSON())
   const videoInfoToUpdate: VideoUpdate = req.body
@@ -371,7 +372,7 @@ async function updateVideo (req: express.Request, res: express.Response) {
         }
       }
 
-      const videoInstanceUpdated = await videoInstance.save(sequelizeOptions)
+      const videoInstanceUpdated = await videoInstance.save(sequelizeOptions) as MVideoFullLight
 
       if (thumbnailModel) await videoInstanceUpdated.addAndSaveThumbnail(thumbnailModel, t)
       if (previewModel) await videoInstanceUpdated.addAndSaveThumbnail(previewModel, t)
@@ -447,7 +448,7 @@ async function getVideo (req: express.Request, res: express.Response) {
 
   const video = await Hooks.wrapPromiseFun(
     VideoModel.loadForGetAPI,
-    { id: res.locals.video.id, userId },
+    { id: res.locals.onlyVideoWithRights.id, userId },
     'filter:api.video.get.result'
   )
 
@@ -460,7 +461,7 @@ async function getVideo (req: express.Request, res: express.Response) {
 }
 
 async function viewVideo (req: express.Request, res: express.Response) {
-  const videoInstance = res.locals.video
+  const videoInstance = res.locals.videoAll
 
   const ip = req.ip
   const exists = await Redis.Instance.doesVideoIPViewExist(ip, videoInstance.uuid)
@@ -483,7 +484,7 @@ async function viewVideo (req: express.Request, res: express.Response) {
 }
 
 async function getVideoDescription (req: express.Request, res: express.Response) {
-  const videoInstance = res.locals.video
+  const videoInstance = res.locals.videoAll
   let description = ''
 
   if (videoInstance.isOwned()) {
@@ -522,7 +523,7 @@ async function listVideos (req: express.Request, res: express.Response) {
 }
 
 async function removeVideo (req: express.Request, res: express.Response) {
-  const videoInstance = res.locals.video
+  const videoInstance = res.locals.videoAll
 
   await sequelizeTypescript.transaction(async t => {
     await videoInstance.destroy({ transaction: t })
index 5272c1385030125daa36b5f9b94f5b8c0e09819b..abb34082e893c2d4382c74b7d3b947ed7f9bb29a 100644 (file)
@@ -18,6 +18,7 @@ import { getFormattedObjects } from '../../../helpers/utils'
 import { changeVideoChannelShare } from '../../../lib/activitypub'
 import { sendUpdateVideo } from '../../../lib/activitypub/send'
 import { VideoModel } from '../../../models/video/video'
+import { MVideoFullLight } from '@server/typings/models'
 
 const ownershipVideoRouter = express.Router()
 
@@ -56,7 +57,7 @@ export {
 // ---------------------------------------------------------------------------
 
 async function giveVideoOwnership (req: express.Request, res: express.Response) {
-  const videoInstance = res.locals.video
+  const videoInstance = res.locals.videoAll
   const initiatorAccountId = res.locals.oauth.token.User.Account.id
   const nextOwner = res.locals.nextOwner
 
@@ -107,7 +108,7 @@ async function acceptOwnership (req: express.Request, res: express.Response) {
 
     targetVideo.channelId = channel.id
 
-    const targetVideoUpdated = await targetVideo.save({ transaction: t })
+    const targetVideoUpdated = await targetVideo.save({ transaction: t }) as MVideoFullLight
     targetVideoUpdated.VideoChannel = channel
 
     if (targetVideoUpdated.privacy !== VideoPrivacy.PRIVATE && targetVideoUpdated.state === VideoState.PUBLISHED) {
index b65babedf1ccfa3db5670a9f7cee12104465c773..3d2f3d7281bef752ac12b193574be68dcac5fb04 100644 (file)
@@ -27,7 +27,7 @@ export {
 async function rateVideo (req: express.Request, res: express.Response) {
   const body: UserVideoRateUpdate = req.body
   const rateType = body.rating
-  const videoInstance = res.locals.video
+  const videoInstance = res.locals.videoAll
   const userAccount = res.locals.oauth.token.User.Account
 
   await sequelizeTypescript.transaction(async t => {
index dcd1f070d1662ff680a6c32ecec19e83af188f78..036e16f3af57ad62a0605e2c1cae2337828db128 100644 (file)
@@ -23,7 +23,7 @@ async function userWatchVideo (req: express.Request, res: express.Response) {
   const user = res.locals.oauth.token.User
 
   const body: UserWatchingVideo = req.body
-  const { id: videoId } = res.locals.video as { id: number }
+  const { id: videoId } = res.locals.videoId
 
   await UserVideoHistoryModel.upsert({
     videoId,
index d3f581615fb56bcf940325836886dfb3b825d91a..468f7a668dc1a43fe75b72fff956bdc072b31a93 100644 (file)
@@ -43,7 +43,7 @@ export {
 async function generateVideoCommentsFeed (req: express.Request, res: express.Response) {
   const start = 0
 
-  const video = res.locals.video
+  const video = res.locals.videoAll
   const videoId: number = video ? video.id : undefined
 
   const comments = await VideoCommentModel.listForFeed(start, FEEDS.COUNT, videoId)
index c1c53c3fc80466a72aad65ed53493e586d688a8c..ec057235fbb3f6958694dc31330b10f23026e39f 100644 (file)
@@ -23,7 +23,7 @@ export {
 // ---------------------------------------------------------------------------
 
 function generateOEmbed (req: express.Request, res: express.Response) {
-  const video = res.locals.video
+  const video = res.locals.videoAll
   const webserverUrl = WEBSERVER.URL
   const maxHeight = parseInt(req.query.maxheight, 10)
   const maxWidth = parseInt(req.query.maxwidth, 10)
index 8979ef5f388e16b6f487a7a73b7fdde12ba427ac..0f47723100ef165f9f290553b76dba237e1f6293 100644 (file)
@@ -226,14 +226,14 @@ async function generateNodeinfo (req: express.Request, res: express.Response) {
   return res.send(json).end()
 }
 
-async function downloadTorrent (req: express.Request, res: express.Response, next: express.NextFunction) {
+async function downloadTorrent (req: express.Request, res: express.Response) {
   const { video, videoFile } = getVideoAndFile(req, res)
   if (!videoFile) return res.status(404).end()
 
   return res.download(video.getTorrentFilePath(videoFile), `${video.name}-${videoFile.resolution}p.torrent`)
 }
 
-async function downloadVideoFile (req: express.Request, res: express.Response, next: express.NextFunction) {
+async function downloadVideoFile (req: express.Request, res: express.Response) {
   const { video, videoFile } = getVideoAndFile(req, res)
   if (!videoFile) return res.status(404).end()
 
@@ -242,7 +242,7 @@ async function downloadVideoFile (req: express.Request, res: express.Response, n
 
 function getVideoAndFile (req: express.Request, res: express.Response) {
   const resolution = parseInt(req.params.resolution, 10)
-  const video = res.locals.video
+  const video = res.locals.videoAll
 
   const videoFile = video.VideoFiles.find(f => f.resolution === resolution)
 
index f2ba3c8266683db9ea793a1621c252021713d53f..fc9575160bf42e15a74ffc570eb9c2560732eb1d 100644 (file)
@@ -18,7 +18,7 @@ export {
 // ---------------------------------------------------------------------------
 
 function webfingerController (req: express.Request, res: express.Response) {
-  const actor = res.locals.actor
+  const actor = res.locals.actorFull
 
   const json = {
     subject: req.query.resource,
index 951a25669d4e2efb3e079c0cf7f9a748f63b0921..97c809a0c51fae1eaf6e2d5b0f70617f282c448e 100644 (file)
@@ -7,6 +7,7 @@ import { ActorModel } from '../models/activitypub/actor'
 import { signJsonLDObject } from './peertube-crypto'
 import { pageToStartAndCount } from './core-utils'
 import { parse } from 'url'
+import { MActor } from '../typings/models'
 
 function activityPubContextify <T> (data: T) {
   return Object.assign(data, {
@@ -143,7 +144,7 @@ async function activityPubCollectionPagination (baseUrl: string, handler: Activi
 
 }
 
-function buildSignedActivity (byActor: ActorModel, data: Object) {
+function buildSignedActivity (byActor: MActor, data: Object) {
   const activity = activityPubContextify(data)
 
   return signJsonLDObject(byActor, activity) as Promise<Activity>
index 12a7ace9fb7dbc1bdca9ef6a15b67c61720ceb56..117548a60cd9db06a43317cff9190d8c0c96d4e1 100644 (file)
@@ -1,10 +1,13 @@
 import { ActorModel } from '../models/activitypub/actor'
+import * as Bluebird from 'bluebird'
+import { MActorFull, MActorAccountChannelId } from '../typings/models'
 
-type ActorFetchByUrlType = 'all' | 'actor-and-association-ids'
-function fetchActorByUrl (url: string, fetchType: ActorFetchByUrlType) {
+type ActorFetchByUrlType = 'all' | 'association-ids'
+
+function fetchActorByUrl (url: string, fetchType: ActorFetchByUrlType): Bluebird<MActorFull | MActorAccountChannelId> {
   if (fetchType === 'all') return ActorModel.loadByUrlAndPopulateAccountAndChannel(url)
 
-  if (fetchType === 'actor-and-association-ids') return ActorModel.loadByUrl(url)
+  if (fetchType === 'association-ids') return ActorModel.loadByUrl(url)
 }
 
 export {
index 7174d4654ab9208a7fc0c93057c09e2060df3ef0..2830ae01776a3cef82883f302d2510a32ea26f9e 100644 (file)
@@ -1,10 +1,10 @@
 import { join } from 'path'
 import { CONFIG } from '../initializers/config'
-import { VideoCaptionModel } from '../models/video/video-caption'
 import * as srt2vtt from 'srt-to-vtt'
-import { createReadStream, createWriteStream, remove, move } from 'fs-extra'
+import { createReadStream, createWriteStream, move, remove } from 'fs-extra'
+import { MVideoCaptionFormattable } from '@server/typings/models'
 
-async function moveAndProcessCaptionFile (physicalFile: { filename: string, path: string }, videoCaption: VideoCaptionModel) {
+async function moveAndProcessCaptionFile (physicalFile: { filename: string, path: string }, videoCaption: MVideoCaptionFormattable) {
   const videoCaptionsDir = CONFIG.STORAGE.CAPTIONS_DIR
   const destination = join(videoCaptionsDir, videoCaption.getCaptionName())
 
index a3bceb047700fc1027b0704f459ee88a88e12624..cb07fa3b28bc7ce6c08f2cdf4ad23c14887aa8c9 100644 (file)
@@ -1,6 +1,5 @@
 import * as AsyncLRU from 'async-lru'
 import * as jsonld from 'jsonld'
-import * as jsig from 'jsonld-signatures'
 import { logger } from './logger'
 
 const CACHE = {
@@ -79,6 +78,4 @@ jsonld.documentLoader = (url, cb) => {
   lru.get(url, cb)
 }
 
-jsig.use('jsonld', jsonld)
-
-export { jsig, jsonld }
+export { jsonld }
index deb331abbb8efb304d1a443c98a6fe71ff3047f8..55bc8cc96c381b22b5349405f5340db9656a7729 100644 (file)
@@ -27,7 +27,7 @@ function isActorPublicKeyValid (publicKey: string) {
     validator.isLength(publicKey, CONSTRAINTS_FIELDS.ACTORS.PUBLIC_KEY)
 }
 
-const actorNameAlphabet = '[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\\-_.]'
+const actorNameAlphabet = '[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\\-_.:]'
 const actorNameRegExp = new RegExp(`^${actorNameAlphabet}+$`)
 function isActorPreferredUsernameValid (preferredUsername: string) {
   return exists(preferredUsername) && validator.matches(preferredUsername, actorNameRegExp)
@@ -46,19 +46,20 @@ function isActorObjectValid (actor: any) {
   return exists(actor) &&
     isActivityPubUrlValid(actor.id) &&
     isActorTypeValid(actor.type) &&
-    isActivityPubUrlValid(actor.following) &&
-    isActivityPubUrlValid(actor.followers) &&
     isActivityPubUrlValid(actor.inbox) &&
-    isActivityPubUrlValid(actor.outbox) &&
     isActorPreferredUsernameValid(actor.preferredUsername) &&
     isActivityPubUrlValid(actor.url) &&
     isActorPublicKeyObjectValid(actor.publicKey) &&
     isActorEndpointsObjectValid(actor.endpoints) &&
-    setValidAttributedTo(actor) &&
 
-    // If this is not an account, it should be attributed to an account
+    (!actor.outbox || isActivityPubUrlValid(actor.outbox)) &&
+    (!actor.following || isActivityPubUrlValid(actor.following)) &&
+    (!actor.followers || isActivityPubUrlValid(actor.followers)) &&
+
+    setValidAttributedTo(actor) &&
+    // If this is a group (a channel), it should be attributed to an account
     // In PeerTube we use this to attach a video channel to a specific account
-    (actor.type === 'Person' || actor.attributedTo.length !== 0)
+    (actor.type !== 'Group' || actor.attributedTo.length !== 0)
 }
 
 function isActorFollowingCountValid (value: string) {
index 63af91a44a7ec5dcef814fb7ef1da4d68c842234..2e317574232dae9357c5e8d9febde464a1f10b74 100644 (file)
@@ -84,17 +84,65 @@ function isThemeNameValid (name: string) {
 }
 
 function isPackageJSONValid (packageJSON: PluginPackageJson, pluginType: PluginType) {
-  return isNpmPluginNameValid(packageJSON.name) &&
-    isPluginDescriptionValid(packageJSON.description) &&
-    isPluginEngineValid(packageJSON.engine) &&
-    isPluginHomepage(packageJSON.homepage) &&
-    exists(packageJSON.author) &&
-    isPluginBugs(packageJSON.bugs) &&
-    (pluginType === PluginType.THEME || isSafePath(packageJSON.library)) &&
-    areStaticDirectoriesValid(packageJSON.staticDirs) &&
-    areCSSPathsValid(packageJSON.css) &&
-    areClientScriptsValid(packageJSON.clientScripts) &&
-    areTranslationPathsValid(packageJSON.translations)
+  let result = true
+  const badFields: string[] = []
+
+  if (!isNpmPluginNameValid(packageJSON.name)) {
+    result = false
+    badFields.push('name')
+  }
+
+  if (!isPluginDescriptionValid(packageJSON.description)) {
+    result = false
+    badFields.push('description')
+  }
+
+  if (!isPluginEngineValid(packageJSON.engine)) {
+    result = false
+    badFields.push('engine')
+  }
+
+  if (!isPluginHomepage(packageJSON.homepage)) {
+    result = false
+    badFields.push('homepage')
+  }
+
+  if (!exists(packageJSON.author)) {
+    result = false
+    badFields.push('author')
+  }
+
+  if (!isPluginBugs(packageJSON.bugs)) {
+    result = false
+    badFields.push('bugs')
+  }
+
+  if (pluginType === PluginType.PLUGIN && !isSafePath(packageJSON.library)) {
+    result = false
+    badFields.push('library')
+  }
+
+  if (!areStaticDirectoriesValid(packageJSON.staticDirs)) {
+    result = false
+    badFields.push('staticDirs')
+  }
+
+  if (!areCSSPathsValid(packageJSON.css)) {
+    result = false
+    badFields.push('css')
+  }
+
+  if (!areClientScriptsValid(packageJSON.clientScripts)) {
+    result = false
+    badFields.push('clientScripts')
+  }
+
+  if (!areTranslationPathsValid(packageJSON.translations)) {
+    result = false
+    badFields.push('translations')
+  }
+
+  return { result, badFields }
 }
 
 function isLibraryCodeValid (library: any) {
index c56ae14ef81d7d89d6904fce26aab9f2b65ce861..68e84d9ebb79ef90ced517129bb5b21ef9ffb76a 100644 (file)
@@ -65,6 +65,14 @@ function isUserBlockedValid (value: any) {
   return isBooleanValid(value)
 }
 
+function isNoInstanceConfigWarningModal (value: any) {
+  return isBooleanValid(value)
+}
+
+function isNoWelcomeModal (value: any) {
+  return isBooleanValid(value)
+}
+
 function isUserBlockedReasonValid (value: any) {
   return value === null || (exists(value) && validator.isLength(value, CONSTRAINTS_FIELDS.USERS.BLOCKED_REASON))
 }
@@ -100,5 +108,7 @@ export {
   isUserAutoPlayVideoValid,
   isUserDisplayNameValid,
   isUserDescriptionValid,
+  isNoInstanceConfigWarningModal,
+  isNoWelcomeModal,
   isAvatarFile
 }
index a7771e07b2e0bf6b1266e42407e27588f90e1b75..9570b27995bd3bcc9f3b77fd163dec02a2fd165a 100644 (file)
@@ -1,10 +1,10 @@
 import { Response } from 'express'
-import * as validator from 'validator'
 import { VideoChangeOwnershipModel } from '../../models/video/video-change-ownership'
-import { UserModel } from '../../models/account/user'
+import { MVideoChangeOwnershipFull } from '@server/typings/models/video/video-change-ownership'
+import { MUserId } from '@server/typings/models'
 
-export async function doesChangeVideoOwnershipExist (id: string, res: Response): Promise<boolean> {
-  const videoChangeOwnership = await loadVideoChangeOwnership(id)
+export async function doesChangeVideoOwnershipExist (id: number, res: Response) {
+  const videoChangeOwnership = await VideoChangeOwnershipModel.load(id)
 
   if (!videoChangeOwnership) {
     res.status(404)
@@ -18,19 +18,7 @@ export async function doesChangeVideoOwnershipExist (id: string, res: Response):
   return true
 }
 
-async function loadVideoChangeOwnership (id: string): Promise<VideoChangeOwnershipModel | undefined> {
-  if (validator.isInt(id)) {
-    return VideoChangeOwnershipModel.load(parseInt(id, 10))
-  }
-
-  return undefined
-}
-
-export function checkUserCanTerminateOwnershipChange (
-  user: UserModel,
-  videoChangeOwnership: VideoChangeOwnershipModel,
-  res: Response
-): boolean {
+export function checkUserCanTerminateOwnershipChange (user: MUserId, videoChangeOwnership: MVideoChangeOwnershipFull, res: Response) {
   if (videoChangeOwnership.NextOwner.userId === user.id) {
     return true
   }
index 791022b97d0c8cbabb45e6a33dd902ebc2b67742..f5aa0badad321601f5e8f10579e2008a1add64fb 100644 (file)
@@ -1,6 +1,7 @@
 import { Response } from 'express'
 import { AccountModel } from '../../models/account/account'
 import * as Bluebird from 'bluebird'
+import { MAccountDefault } from '../../typings/models'
 
 function doesAccountIdExist (id: number, res: Response, sendNotFound = true) {
   const promise = AccountModel.load(id)
@@ -15,10 +16,12 @@ function doesLocalAccountNameExist (name: string, res: Response, sendNotFound =
 }
 
 function doesAccountNameWithHostExist (nameWithDomain: string, res: Response, sendNotFound = true) {
-  return doesAccountExist(AccountModel.loadByNameWithHost(nameWithDomain), res, sendNotFound)
+  const promise = AccountModel.loadByNameWithHost(nameWithDomain)
+
+  return doesAccountExist(promise, res, sendNotFound)
 }
 
-async function doesAccountExist (p: Bluebird<AccountModel>, res: Response, sendNotFound: boolean) {
+async function doesAccountExist (p: Bluebird<MAccountDefault>, res: Response, sendNotFound: boolean) {
   const account = await p
 
   if (!account) {
index b23f1f021b1d46d0495ffacecefd4c28c4b3dca7..1b573ca37e7ae2bf0dff4956af95ecb76ac9b2d6 100644 (file)
@@ -1,41 +1,23 @@
-import * as express from 'express'
-import { VideoChannelModel } from '../../models/video/video-channel'
+import { Response } from 'express'
+import { VideoAbuseModel } from '../../models/video/video-abuse'
 
-async function doesLocalVideoChannelNameExist (name: string, res: express.Response) {
-  const videoChannel = await VideoChannelModel.loadLocalByNameAndPopulateAccount(name)
+async function doesVideoAbuseExist (abuseId: number, videoId: number, res: Response) {
+  const videoAbuse = await VideoAbuseModel.loadByIdAndVideoId(abuseId, videoId)
 
-  return processVideoChannelExist(videoChannel, res)
-}
-
-async function doesVideoChannelIdExist (id: number, res: express.Response) {
-  const videoChannel = await VideoChannelModel.loadAndPopulateAccount(+id)
-
-  return processVideoChannelExist(videoChannel, res)
-}
-
-async function doesVideoChannelNameWithHostExist (nameWithDomain: string, res: express.Response) {
-  const videoChannel = await VideoChannelModel.loadByNameWithHostAndPopulateAccount(nameWithDomain)
-
-  return processVideoChannelExist(videoChannel, res)
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  doesLocalVideoChannelNameExist,
-  doesVideoChannelIdExist,
-  doesVideoChannelNameWithHostExist
-}
-
-function processVideoChannelExist (videoChannel: VideoChannelModel, res: express.Response) {
-  if (!videoChannel) {
+  if (videoAbuse === null) {
     res.status(404)
-       .json({ error: 'Video channel not found' })
+       .json({ error: 'Video abuse not found' })
        .end()
 
     return false
   }
 
-  res.locals.videoChannel = videoChannel
+  res.locals.videoAbuse = videoAbuse
   return true
 }
+
+// ---------------------------------------------------------------------------
+
+export {
+  doesVideoAbuseExist
+}
index dc3d0144bc079c35d4d385878bfd93d2241b934b..1b2513b60111b306dff9bf00ea64fec24409f47b 100644 (file)
@@ -1,8 +1,8 @@
-import { VideoModel } from '../../models/video/video'
 import { Response } from 'express'
 import { VideoCaptionModel } from '../../models/video/video-caption'
+import { MVideoId } from '@server/typings/models'
 
-async function doesVideoCaptionExist (video: VideoModel, language: string, res: Response) {
+async function doesVideoCaptionExist (video: MVideoId, language: string, res: Response) {
   const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(video.id, language)
 
   if (!videoCaption) {
index 1b573ca37e7ae2bf0dff4956af95ecb76ac9b2d6..1595ecd94626af0ac05150dae72e9f1aed721d65 100644 (file)
@@ -1,23 +1,42 @@
-import { Response } from 'express'
-import { VideoAbuseModel } from '../../models/video/video-abuse'
+import * as express from 'express'
+import { VideoChannelModel } from '../../models/video/video-channel'
+import { MChannelAccountDefault } from '@server/typings/models'
 
-async function doesVideoAbuseExist (abuseId: number, videoId: number, res: Response) {
-  const videoAbuse = await VideoAbuseModel.loadByIdAndVideoId(abuseId, videoId)
+async function doesLocalVideoChannelNameExist (name: string, res: express.Response) {
+  const videoChannel = await VideoChannelModel.loadLocalByNameAndPopulateAccount(name)
 
-  if (videoAbuse === null) {
-    res.status(404)
-       .json({ error: 'Video abuse not found' })
-       .end()
+  return processVideoChannelExist(videoChannel, res)
+}
 
-    return false
-  }
+async function doesVideoChannelIdExist (id: number, res: express.Response) {
+  const videoChannel = await VideoChannelModel.loadAndPopulateAccount(+id)
 
-  res.locals.videoAbuse = videoAbuse
-  return true
+  return processVideoChannelExist(videoChannel, res)
+}
+
+async function doesVideoChannelNameWithHostExist (nameWithDomain: string, res: express.Response) {
+  const videoChannel = await VideoChannelModel.loadByNameWithHostAndPopulateAccount(nameWithDomain)
+
+  return processVideoChannelExist(videoChannel, res)
 }
 
 // ---------------------------------------------------------------------------
 
 export {
-  doesVideoAbuseExist
+  doesLocalVideoChannelNameExist,
+  doesVideoChannelIdExist,
+  doesVideoChannelNameWithHostExist
+}
+
+function processVideoChannelExist (videoChannel: MChannelAccountDefault, res: express.Response) {
+  if (!videoChannel) {
+    res.status(404)
+       .json({ error: 'Video channel not found' })
+       .end()
+
+    return false
+  }
+
+  res.locals.videoChannel = videoChannel
+  return true
 }
index 735bf362f77b715c16ef4980b7eaefe9ea7f2c98..8e74844835c37e6ff63b0f65582f008edf08f3eb 100644 (file)
@@ -1,11 +1,31 @@
 import * as express from 'express'
 import { VideoPlaylistModel } from '../../models/video/video-playlist'
+import { MVideoPlaylist } from '../../typings/models/video/video-playlist'
 
-async function doesVideoPlaylistExist (id: number | string, res: express.Response, fetchType: 'summary' | 'all' = 'summary') {
-  const videoPlaylist = fetchType === 'summary'
-    ? await VideoPlaylistModel.loadWithAccountAndChannelSummary(id, undefined)
-    : await VideoPlaylistModel.loadWithAccountAndChannel(id, undefined)
+export type VideoPlaylistFetchType = 'summary' | 'all'
+async function doesVideoPlaylistExist (id: number | string, res: express.Response, fetchType: VideoPlaylistFetchType = 'summary') {
+  if (fetchType === 'summary') {
+    const videoPlaylist = await VideoPlaylistModel.loadWithAccountAndChannelSummary(id, undefined)
+    res.locals.videoPlaylistSummary = videoPlaylist
 
+    return handleVideoPlaylist(videoPlaylist, res)
+  }
+
+  const videoPlaylist = await VideoPlaylistModel.loadWithAccountAndChannel(id, undefined)
+  res.locals.videoPlaylistFull = videoPlaylist
+
+  return handleVideoPlaylist(videoPlaylist, res)
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  doesVideoPlaylistExist
+}
+
+// ---------------------------------------------------------------------------
+
+function handleVideoPlaylist (videoPlaylist: MVideoPlaylist, res: express.Response) {
   if (!videoPlaylist) {
     res.status(404)
        .json({ error: 'Video playlist not found' })
@@ -14,12 +34,5 @@ async function doesVideoPlaylistExist (id: number | string, res: express.Respons
     return false
   }
 
-  res.locals.videoPlaylist = videoPlaylist
   return true
 }
-
-// ---------------------------------------------------------------------------
-
-export {
-  doesVideoPlaylistExist
-}
index ceb1058ecb7c4e090705a6cbd0c3513eb7cb1c09..74f529804e7abeb30494f064f1fdd59743d3c9a2 100644 (file)
@@ -1,9 +1,8 @@
 import { Response } from 'express'
 import { fetchVideo, VideoFetchType } from '../video'
-import { UserModel } from '../../models/account/user'
 import { UserRight } from '../../../shared/models/users'
 import { VideoChannelModel } from '../../models/video/video-channel'
-import { VideoModel } from '../../models/video/video'
+import { MUser, MUserAccountId, MVideoAccountLight, MVideoFullLight, MVideoThumbnail, MVideoWithRights } from '@server/typings/models'
 
 async function doesVideoExist (id: number | string, res: Response, fetchType: VideoFetchType = 'all') {
   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
     return false
   }
 
-  if (fetchType !== 'none') res.locals.video = video
+  switch (fetchType) {
+    case 'all':
+      res.locals.videoAll = video as MVideoFullLight
+      break
+
+    case 'id':
+      res.locals.videoId = video
+      break
+
+    case 'only-video':
+      res.locals.onlyVideo = video as MVideoThumbnail
+      break
+
+    case 'only-video-with-rights':
+      res.locals.onlyVideoWithRights = video as MVideoWithRights
+      break
+  }
+
   return true
 }
 
-async function doesVideoChannelOfAccountExist (channelId: number, user: UserModel, res: Response) {
+async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAccountId, res: Response) {
   if (user.hasRight(UserRight.UPDATE_ANY_VIDEO) === true) {
     const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId)
     if (videoChannel === null) {
@@ -50,7 +66,7 @@ async function doesVideoChannelOfAccountExist (channelId: number, user: UserMode
   return true
 }
 
-function checkUserCanManageVideo (user: UserModel, video: VideoModel, right: UserRight, res: Response) {
+function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right: UserRight, res: Response) {
   // Retrieve the user who did the request
   if (video.isOwned() === false) {
     res.status(403)
index 1424949d0a6b6c3054eb58ac1a4fe1c70aa6545b..9eb7823026cc3222bca162ed6841bef45152300d 100644 (file)
@@ -1,13 +1,13 @@
 import { Request } from 'express'
 import { BCRYPT_SALT_SIZE, HTTP_SIGNATURE, PRIVATE_RSA_KEY_SIZE } from '../initializers/constants'
-import { ActorModel } from '../models/activitypub/actor'
 import { createPrivateKey, getPublicKey, promisify1, promisify2, sha256 } from './core-utils'
-import { jsig, jsonld } from './custom-jsonld-signature'
+import { jsonld } from './custom-jsonld-signature'
 import { logger } from './logger'
 import { cloneDeep } from 'lodash'
-import { createVerify } from 'crypto'
+import { createSign, createVerify } from 'crypto'
 import { buildDigest } from '../lib/job-queue/handlers/utils/activitypub-http-utils'
 import * as bcrypt from 'bcrypt'
+import { MActor } from '../typings/models'
 
 const bcryptComparePromise = promisify2<any, string, boolean>(bcrypt.compare)
 const bcryptGenSaltPromise = promisify1<number, string>(bcrypt.genSalt)
@@ -46,7 +46,7 @@ function isHTTPSignatureDigestValid (rawBody: Buffer, req: Request): boolean {
   return true
 }
 
-function isHTTPSignatureVerified (httpSignatureParsed: any, actor: ActorModel): boolean {
+function isHTTPSignatureVerified (httpSignatureParsed: any, actor: MActor): boolean {
   return httpSignature.verifySignature(httpSignatureParsed, actor.publicKey) === true
 }
 
@@ -56,70 +56,21 @@ function parseHTTPSignature (req: Request, clockSkew?: number) {
 
 // JSONLD
 
-async function isJsonLDSignatureVerified (fromActor: ActorModel, signedDocument: any): Promise<boolean> {
+function isJsonLDSignatureVerified (fromActor: MActor, signedDocument: any): Promise<boolean> {
   if (signedDocument.signature.type === 'RsaSignature2017') {
-    // Mastodon algorithm
-    const res = await isJsonLDRSA2017Verified(fromActor, signedDocument)
-    // Success? If no, try with our library
-    if (res === true) return true
+    return isJsonLDRSA2017Verified(fromActor, signedDocument)
   }
 
-  const publicKeyObject = {
-    '@context': jsig.SECURITY_CONTEXT_URL,
-    id: fromActor.url,
-    type: 'CryptographicKey',
-    owner: fromActor.url,
-    publicKeyPem: fromActor.publicKey
-  }
-
-  const publicKeyOwnerObject = {
-    '@context': jsig.SECURITY_CONTEXT_URL,
-    id: fromActor.url,
-    publicKey: [ publicKeyObject ]
-  }
+  logger.warn('Unknown JSON LD signature %s.', signedDocument.signature.type, signedDocument)
 
-  const options = {
-    publicKey: publicKeyObject,
-    publicKeyOwner: publicKeyOwnerObject
-  }
-
-  return jsig.promises
-             .verify(signedDocument, options)
-             .then((result: { verified: boolean }) => result.verified)
-             .catch(err => {
-               logger.error('Cannot check signature.', { err })
-               return false
-             })
+  return Promise.resolve(false)
 }
 
 // Backward compatibility with "other" implementations
-async function isJsonLDRSA2017Verified (fromActor: ActorModel, signedDocument: any) {
-  function hash (obj: any): Promise<any> {
-    return jsonld.promises
-                 .normalize(obj, {
-                   algorithm: 'URDNA2015',
-                   format: 'application/n-quads'
-                 })
-                 .then(res => sha256(res))
-  }
-
-  const signatureCopy = cloneDeep(signedDocument.signature)
-  Object.assign(signatureCopy, {
-    '@context': [
-      'https://w3id.org/security/v1',
-      { RsaSignature2017: 'https://w3id.org/security#RsaSignature2017' }
-    ]
-  })
-  delete signatureCopy.type
-  delete signatureCopy.id
-  delete signatureCopy.signatureValue
-
-  const docWithoutSignature = cloneDeep(signedDocument)
-  delete docWithoutSignature.signature
-
+async function isJsonLDRSA2017Verified (fromActor: MActor, signedDocument: any) {
   const [ documentHash, optionsHash ] = await Promise.all([
-    hash(docWithoutSignature),
-    hash(signatureCopy)
+    createDocWithoutSignatureHash(signedDocument),
+    createSignatureHash(signedDocument.signature)
   ])
 
   const toVerify = optionsHash + documentHash
@@ -130,14 +81,27 @@ async function isJsonLDRSA2017Verified (fromActor: ActorModel, signedDocument: a
   return verify.verify(fromActor.publicKey, signedDocument.signature.signatureValue, 'base64')
 }
 
-function signJsonLDObject (byActor: ActorModel, data: any) {
-  const options = {
-    privateKeyPem: byActor.privateKey,
+async function signJsonLDObject (byActor: MActor, data: any) {
+  const signature = {
+    type: 'RsaSignature2017',
     creator: byActor.url,
-    algorithm: 'RsaSignature2017'
+    created: new Date().toISOString()
   }
 
-  return jsig.promises.sign(data, options)
+  const [ documentHash, optionsHash ] = await Promise.all([
+    createDocWithoutSignatureHash(data),
+    createSignatureHash(signature)
+  ])
+
+  const toSign = optionsHash + documentHash
+
+  const sign = createSign('RSA-SHA256')
+  sign.update(toSign, 'utf8')
+
+  const signatureValue = sign.sign(byActor.privateKey, 'base64')
+  Object.assign(signature, { signatureValue })
+
+  return Object.assign(data, { signature })
 }
 
 // ---------------------------------------------------------------------------
@@ -154,3 +118,35 @@ export {
 }
 
 // ---------------------------------------------------------------------------
+
+function hash (obj: any): Promise<any> {
+  return jsonld.promises
+               .normalize(obj, {
+                 algorithm: 'URDNA2015',
+                 format: 'application/n-quads'
+               })
+               .then(res => sha256(res))
+}
+
+function createSignatureHash (signature: any) {
+  const signatureCopy = cloneDeep(signature)
+  Object.assign(signatureCopy, {
+    '@context': [
+      'https://w3id.org/security/v1',
+      { RsaSignature2017: 'https://w3id.org/security#RsaSignature2017' }
+    ]
+  })
+
+  delete signatureCopy.type
+  delete signatureCopy.id
+  delete signatureCopy.signatureValue
+
+  return hash(signatureCopy)
+}
+
+function createDocWithoutSignatureHash (doc: any) {
+  const docWithoutSignature = cloneDeep(doc)
+  delete docWithoutSignature.signature
+
+  return hash(docWithoutSignature)
+}
index 1464b147728e0cefbac7c7c4b6c97b24d7676ca3..ba07eaaf342c450410f323982b48c5c217729aa1 100644 (file)
@@ -19,7 +19,10 @@ async function generateRandomString (size: number) {
   return raw.toString('hex')
 }
 
-interface FormattableToJSON<U, V> { toFormattedJSON (args?: U): V }
+interface FormattableToJSON<U, V> {
+  toFormattedJSON (args?: U): V
+}
+
 function getFormattedObjects<U, V, T extends FormattableToJSON<U, V>> (objects: T[], objectsTotal: number, formattedArg?: U) {
   const formattedObjects = objects.map(o => o.toFormattedJSON(formattedArg))
 
index c90fe06c78e2729174f58f5dee75741d9f194cd5..d066e2b1f42854ce5c110f79955a8226606a93d9 100644 (file)
@@ -1,8 +1,30 @@
 import { VideoModel } from '../models/video/video'
+import * as Bluebird from 'bluebird'
+import {
+  MVideoAccountLightBlacklistAllFiles,
+  MVideoFullLight,
+  MVideoIdThumbnail,
+  MVideoThumbnail,
+  MVideoWithRights
+} from '@server/typings/models'
+import { Response } from 'express'
 
 type VideoFetchType = 'all' | 'only-video' | 'only-video-with-rights' | 'id' | 'none'
 
-function fetchVideo (id: number | string, fetchType: VideoFetchType, userId?: number) {
+function fetchVideo (id: number | string, fetchType: 'all', userId?: number): Bluebird<MVideoFullLight>
+function fetchVideo (id: number | string, fetchType: 'only-video', userId?: number): Bluebird<MVideoThumbnail>
+function fetchVideo (id: number | string, fetchType: 'only-video-with-rights', userId?: number): Bluebird<MVideoWithRights>
+function fetchVideo (id: number | string, fetchType: 'id' | 'none', userId?: number): Bluebird<MVideoIdThumbnail>
+function fetchVideo (
+  id: number | string,
+  fetchType: VideoFetchType,
+  userId?: number
+): Bluebird<MVideoFullLight | MVideoThumbnail | MVideoWithRights | MVideoIdThumbnail>
+function fetchVideo (
+  id: number | string,
+  fetchType: VideoFetchType,
+  userId?: number
+): Bluebird<MVideoFullLight | MVideoThumbnail | MVideoWithRights | MVideoIdThumbnail> {
   if (fetchType === 'all') return VideoModel.loadAndPopulateAccountAndServerAndTags(id, undefined, userId)
 
   if (fetchType === 'only-video-with-rights') return VideoModel.loadWithRights(id)
@@ -13,15 +35,29 @@ function fetchVideo (id: number | string, fetchType: VideoFetchType, userId?: nu
 }
 
 type VideoFetchByUrlType = 'all' | 'only-video'
-function fetchVideoByUrl (url: string, fetchType: VideoFetchByUrlType) {
+
+function fetchVideoByUrl (url: string, fetchType: 'all'): Bluebird<MVideoAccountLightBlacklistAllFiles>
+function fetchVideoByUrl (url: string, fetchType: 'only-video'): Bluebird<MVideoThumbnail>
+function fetchVideoByUrl (url: string, fetchType: VideoFetchByUrlType): Bluebird<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail>
+function fetchVideoByUrl (url: string, fetchType: VideoFetchByUrlType): Bluebird<MVideoAccountLightBlacklistAllFiles | MVideoThumbnail> {
   if (fetchType === 'all') return VideoModel.loadByUrlAndPopulateAccount(url)
 
   if (fetchType === 'only-video') return VideoModel.loadByUrl(url)
 }
 
+function getVideo (res: Response) {
+  return res.locals.videoAll || res.locals.onlyVideo || res.locals.onlyVideoWithRights || res.locals.videoId
+}
+
+function getVideoWithAttributes (res: Response) {
+  return res.locals.videoAll || res.locals.onlyVideo || res.locals.onlyVideoWithRights
+}
+
 export {
   VideoFetchType,
   VideoFetchByUrlType,
   fetchVideo,
+  getVideo,
+  getVideoWithAttributes,
   fetchVideoByUrl
 }
index d1229e28f4b8b6507acefa93c67291b1138a1260..5443a266b0087de6587659cdb30b493b1b6efa9b 100644 (file)
@@ -4,6 +4,7 @@ import { ActorModel } from '../models/activitypub/actor'
 import { isTestInstance } from './core-utils'
 import { isActivityPubUrlValid } from './custom-validators/activitypub/misc'
 import { WEBSERVER } from '../initializers/constants'
+import { MActorFull } from '../typings/models'
 
 const webfinger = new WebFinger({
   webfist_fallback: false,
@@ -17,7 +18,7 @@ async function loadActorUrlOrGetFromWebfinger (uriArg: string) {
   const uri = uriArg.startsWith('@') ? uriArg.slice(1) : uriArg
 
   const [ name, host ] = uri.split('@')
-  let actor: ActorModel
+  let actor: MActorFull
 
   if (!host || host === WEBSERVER.HOST) {
     actor = await ActorModel.loadLocalByName(name)
index 510f7d64d34a2c5d24f7a5525dfe7b3c3fbc0c27..164d714d6b94ad7fd4edd10da4324334f087830f 100644 (file)
@@ -209,6 +209,19 @@ const CONFIG = {
     get SHORT_DESCRIPTION () { return config.get<string>('instance.short_description') },
     get DESCRIPTION () { return config.get<string>('instance.description') },
     get TERMS () { return config.get<string>('instance.terms') },
+    get CODE_OF_CONDUCT () { return config.get<string>('instance.code_of_conduct') },
+
+    get CREATION_REASON () { return config.get<string>('instance.creation_reason') },
+
+    get MODERATION_INFORMATION () { return config.get<string>('instance.moderation_information') },
+    get ADMINISTRATOR () { return config.get<string>('instance.administrator') },
+    get MAINTENANCE_LIFETIME () { return config.get<string>('instance.maintenance_lifetime') },
+    get BUSINESS_MODEL () { return config.get<string>('instance.business_model') },
+    get HARDWARE_INFORMATION () { return config.get<string>('instance.hardware_information') },
+
+    get LANGUAGES () { return config.get<string[]>('instance.languages') || [] },
+    get CATEGORIES () { return config.get<number[]>('instance.categories') || [] },
+
     get IS_NSFW () { return config.get<boolean>('instance.is_nsfw') },
     get DEFAULT_CLIENT_ROUTE () { return config.get<string>('instance.default_client_route') },
     get DEFAULT_NSFW_POLICY () { return config.get<NSFWPolicyType>('instance.default_nsfw_policy') },
@@ -232,6 +245,23 @@ const CONFIG = {
       get MANUAL_APPROVAL () { return config.get<boolean>('followers.instance.manual_approval') }
     }
   },
+  FOLLOWINGS: {
+    INSTANCE: {
+      AUTO_FOLLOW_BACK: {
+        get ENABLED () {
+          return config.get<boolean>('followings.instance.auto_follow_back.enabled')
+        }
+      },
+      AUTO_FOLLOW_INDEX: {
+        get ENABLED () {
+          return config.get<boolean>('followings.instance.auto_follow_index.enabled')
+        },
+        get INDEX_URL () {
+          return config.get<string>('followings.instance.auto_follow_index.index_url')
+        }
+      }
+    }
+  },
   THEME: {
     get DEFAULT () { return config.get<string>('theme.default') }
   }
index 3dc178b117c9d63288eeaa49789c047556367be5..be4a664889b9f7bcf2937d2c75349e08b6133fb8 100644 (file)
@@ -14,7 +14,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
 
 // ---------------------------------------------------------------------------
 
-const LAST_MIGRATION_VERSION = 420
+const LAST_MIGRATION_VERSION = 435
 
 // ---------------------------------------------------------------------------
 
@@ -168,10 +168,15 @@ const SCHEDULER_INTERVALS_MS = {
   updateVideos: 60000, // 1 minute
   youtubeDLUpdate: 60000 * 60 * 24, // 1 day
   checkPlugins: CONFIG.PLUGINS.INDEX.CHECK_LATEST_VERSIONS_INTERVAL,
+  autoFollowIndexInstances: 60000 * 60 * 24, // 1 day
   removeOldViews: 60000 * 60 * 24, // 1 day
   removeOldHistory: 60000 * 60 * 24 // 1 day
 }
 
+const INSTANCES_INDEX = {
+  HOSTS_PATH: '/api/v1/instances/hosts'
+}
+
 // ---------------------------------------------------------------------------
 
 const CONSTRAINTS_FIELDS = {
@@ -633,6 +638,7 @@ if (isTestInstance() === true) {
   SCHEDULER_INTERVALS_MS.removeOldHistory = 5000
   SCHEDULER_INTERVALS_MS.removeOldViews = 5000
   SCHEDULER_INTERVALS_MS.updateVideos = 5000
+  SCHEDULER_INTERVALS_MS.autoFollowIndexInstances = 5000
   REPEAT_JOBS[ 'videos-views' ] = { every: 5000 }
 
   REDUNDANCY.VIDEOS.RANDOMIZED_FACTOR = 1
@@ -683,6 +689,7 @@ export {
   PREVIEWS_SIZE,
   REMOTE_SCHEME,
   FOLLOW_STATES,
+  INSTANCES_INDEX,
   DEFAULT_USER_THEME_NAME,
   SERVER_ACTOR_NAME,
   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 (file)
index 0000000..4e5f9e6
--- /dev/null
@@ -0,0 +1,26 @@
+import * as Sequelize from 'sequelize'
+
+async function up (utils: {
+  transaction: Sequelize.Transaction,
+  queryInterface: Sequelize.QueryInterface,
+  sequelize: Sequelize.Sequelize,
+  db: any
+}): Promise<void> {
+  const data = {
+    type: Sequelize.STRING,
+    allowNull: true
+  }
+
+  await utils.queryInterface.changeColumn('actor', 'outboxUrl', data)
+  await utils.queryInterface.changeColumn('actor', 'followersUrl', data)
+  await utils.queryInterface.changeColumn('actor', 'followingUrl', data)
+}
+
+function down (options) {
+  throw new Error('Not implemented.')
+}
+
+export {
+  up,
+  down
+}
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 (file)
index 0000000..034bdd4
--- /dev/null
@@ -0,0 +1,40 @@
+import * as Sequelize from 'sequelize'
+
+async function up (utils: {
+  transaction: Sequelize.Transaction,
+  queryInterface: Sequelize.QueryInterface,
+  sequelize: Sequelize.Sequelize,
+  db: any
+}): Promise<void> {
+  {
+    const data = {
+      type: Sequelize.INTEGER,
+      defaultValue: null,
+      allowNull: true
+    }
+    await utils.queryInterface.addColumn('userNotificationSetting', 'autoInstanceFollowing', data)
+  }
+
+  {
+    const query = 'UPDATE "userNotificationSetting" SET "autoInstanceFollowing" = 1'
+    await utils.sequelize.query(query)
+  }
+
+  {
+    const data = {
+      type: Sequelize.INTEGER,
+      defaultValue: null,
+      allowNull: false
+    }
+    await utils.queryInterface.changeColumn('userNotificationSetting', 'autoInstanceFollowing', data)
+  }
+}
+
+function down (options) {
+  throw new Error('Not implemented.')
+}
+
+export {
+  up,
+  down
+}
diff --git a/server/initializers/migrations/0435-user-modals.ts b/server/initializers/migrations/0435-user-modals.ts
new file mode 100644 (file)
index 0000000..5c2aa85
--- /dev/null
@@ -0,0 +1,40 @@
+import * as Sequelize from 'sequelize'
+
+async function up (utils: {
+  transaction: Sequelize.Transaction,
+  queryInterface: Sequelize.QueryInterface,
+  sequelize: Sequelize.Sequelize,
+  db: any
+}): Promise<void> {
+  {
+    const data = {
+      type: Sequelize.BOOLEAN,
+      allowNull: false,
+      defaultValue: false
+    }
+
+    await utils.queryInterface.addColumn('user', 'noInstanceConfigWarningModal', data)
+  }
+
+  {
+    const data = {
+      type: Sequelize.BOOLEAN,
+      allowNull: false,
+      defaultValue: true
+    }
+
+    await utils.queryInterface.addColumn('user', 'noWelcomeModal', data)
+    data.defaultValue = false
+
+    await utils.queryInterface.changeColumn('user', 'noWelcomeModal', data)
+  }
+}
+
+function down (options) {
+  throw new Error('Not implemented.')
+}
+
+export {
+  up,
+  down
+}
index 9f5d12eb4917aec0e2ff064b6362762bd8e8924d..13b73077e8ca6d6e757f09d92ac264fb3f5d97ec 100644 (file)
@@ -22,13 +22,27 @@ import { JobQueue } from '../job-queue'
 import { getServerActor } from '../../helpers/utils'
 import { ActorFetchByUrlType, fetchActorByUrl } from '../../helpers/actor'
 import { sequelizeTypescript } from '../../initializers/database'
+import {
+  MAccount,
+  MAccountDefault,
+  MActor,
+  MActorAccountChannelId,
+  MActorAccountChannelIdActor,
+  MActorAccountId,
+  MActorDefault,
+  MActorFull,
+  MActorFullActor,
+  MActorId,
+  MChannel,
+  MChannelAccountDefault
+} from '../../typings/models'
 
 // Set account keys, this could be long so process after the account creation and do not block the client
-function setAsyncActorKeys (actor: ActorModel) {
+function setAsyncActorKeys <T extends MActor> (actor: T) {
   return createPrivateAndPublicKeys()
     .then(({ publicKey, privateKey }) => {
-      actor.set('publicKey', publicKey)
-      actor.set('privateKey', privateKey)
+      actor.publicKey = publicKey
+      actor.privateKey = privateKey
       return actor.save()
     })
     .catch(err => {
@@ -37,12 +51,26 @@ function setAsyncActorKeys (actor: ActorModel) {
     })
 }
 
+function getOrCreateActorAndServerAndModel (
+  activityActor: string | ActivityPubActor,
+  fetchType: 'all',
+  recurseIfNeeded?: boolean,
+  updateCollections?: boolean
+): Promise<MActorFullActor>
+
+function getOrCreateActorAndServerAndModel (
+  activityActor: string | ActivityPubActor,
+  fetchType?: 'association-ids',
+  recurseIfNeeded?: boolean,
+  updateCollections?: boolean
+): Promise<MActorAccountChannelId>
+
 async function getOrCreateActorAndServerAndModel (
   activityActor: string | ActivityPubActor,
-  fetchType: ActorFetchByUrlType = 'actor-and-association-ids',
+  fetchType: ActorFetchByUrlType = 'association-ids',
   recurseIfNeeded = true,
   updateCollections = false
-) {
+): Promise<MActorFullActor | MActorAccountChannelId> {
   const actorUrl = getAPId(activityActor)
   let created = false
   let accountPlaylistsUrl: string
@@ -61,7 +89,7 @@ async function getOrCreateActorAndServerAndModel (
 
     // Create the attributed to actor
     // In PeerTube a video channel is owned by an account
-    let ownerActor: ActorModel = undefined
+    let ownerActor: MActorFullActor
     if (recurseIfNeeded === true && result.actor.type === 'Group') {
       const accountAttributedTo = result.attributedTo.find(a => a.type === 'Person')
       if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actor.url)
@@ -85,8 +113,8 @@ async function getOrCreateActorAndServerAndModel (
     accountPlaylistsUrl = result.playlists
   }
 
-  if (actor.Account) actor.Account.Actor = actor
-  if (actor.VideoChannel) actor.VideoChannel.Actor = actor
+  if (actor.Account) (actor as MActorAccountChannelIdActor).Account.Actor = actor
+  if (actor.VideoChannel) (actor as MActorAccountChannelIdActor).VideoChannel.Actor = actor
 
   const { actor: actorRefreshed, refreshed } = await retryTransactionWrapper(refreshActorIfNeeded, actor, fetchType)
   if (!actorRefreshed) throw new Error('Actor ' + actorRefreshed.url + ' does not exist anymore.')
@@ -120,7 +148,7 @@ function buildActorInstance (type: ActivityPubActorType, url: string, preferredU
     sharedInboxUrl: WEBSERVER.URL + '/inbox',
     followersUrl: url + '/followers',
     followingUrl: url + '/following'
-  })
+  }) as MActor
 }
 
 async function updateActorInstance (actorInstance: ActorModel, attributes: ActivityPubActor) {
@@ -140,7 +168,8 @@ async function updateActorInstance (actorInstance: ActorModel, attributes: Activ
   actorInstance.followingUrl = attributes.following
 }
 
-async function updateActorAvatarInstance (actor: ActorModel, info: { name: string, onDisk: boolean, fileUrl: string }, t: Transaction) {
+type AvatarInfo = { name: string, onDisk: boolean, fileUrl: string }
+async function updateActorAvatarInstance (actor: MActorDefault, info: AvatarInfo, t: Transaction) {
   if (info.name !== undefined) {
     if (actor.avatarId) {
       try {
@@ -212,14 +241,16 @@ async function addFetchOutboxJob (actor: Pick<ActorModel, 'id' | 'outboxUrl'>) {
   return JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
 }
 
-async function refreshActorIfNeeded (
-  actorArg: ActorModel,
+async function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannelId> (
+  actorArg: T,
   fetchedType: ActorFetchByUrlType
-): Promise<{ actor: ActorModel, refreshed: boolean }> {
+): Promise<{ actor: T | MActorFull, refreshed: boolean }> {
   if (!actorArg.isOutdated()) return { actor: actorArg, refreshed: false }
 
   // We need more attributes
-  const actor = fetchedType === 'all' ? actorArg : await ActorModel.loadByUrlAndPopulateAccountAndChannel(actorArg.url)
+  const actor = fetchedType === 'all'
+    ? actorArg as MActorFull
+    : await ActorModel.loadByUrlAndPopulateAccountAndChannel(actorArg.url)
 
   try {
     let actorUrl: string
@@ -297,9 +328,9 @@ export {
 
 function saveActorAndServerAndModelIfNotExist (
   result: FetchRemoteActorResult,
-  ownerActor?: ActorModel,
+  ownerActor?: MActorFullActor,
   t?: Transaction
-): Bluebird<ActorModel> | Promise<ActorModel> {
+): Bluebird<MActorFullActor> | Promise<MActorFullActor> {
   let actor = result.actor
 
   if (t !== undefined) return save(t)
@@ -336,7 +367,7 @@ function saveActorAndServerAndModelIfNotExist (
 
     // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists
     // (which could be false in a retried query)
-    const [ actorCreated ] = await ActorModel.findOrCreate({
+    const [ actorCreated ] = await ActorModel.findOrCreate<MActorFullActor>({
       defaults: actor.toJSON(),
       where: {
         url: actor.url
@@ -345,12 +376,11 @@ function saveActorAndServerAndModelIfNotExist (
     })
 
     if (actorCreated.type === 'Person' || actorCreated.type === 'Application') {
-      actorCreated.Account = await saveAccount(actorCreated, result, t)
+      actorCreated.Account = await saveAccount(actorCreated, result, t) as MAccountDefault
       actorCreated.Account.Actor = actorCreated
     } else if (actorCreated.type === 'Group') { // Video channel
-      actorCreated.VideoChannel = await saveVideoChannel(actorCreated, result, ownerActor, t)
-      actorCreated.VideoChannel.Actor = actorCreated
-      actorCreated.VideoChannel.Account = ownerActor.Account
+      const channel = await saveVideoChannel(actorCreated, result, ownerActor, t)
+      actorCreated.VideoChannel = Object.assign(channel, { Actor: actorCreated, Account: ownerActor.Account })
     }
 
     actorCreated.Server = server
@@ -360,7 +390,7 @@ function saveActorAndServerAndModelIfNotExist (
 }
 
 type FetchRemoteActorResult = {
-  actor: ActorModel
+  actor: MActor
   name: string
   summary: string
   support?: string
@@ -429,7 +459,7 @@ async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: numbe
   }
 }
 
-async function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) {
+async function saveAccount (actor: MActorId, result: FetchRemoteActorResult, t: Transaction) {
   const [ accountCreated ] = await AccountModel.findOrCreate({
     defaults: {
       name: result.name,
@@ -442,10 +472,10 @@ async function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t
     transaction: t
   })
 
-  return accountCreated
+  return accountCreated as MAccount
 }
 
-async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResult, ownerActor: ActorModel, t: Transaction) {
+async function saveVideoChannel (actor: MActorId, result: FetchRemoteActorResult, ownerActor: MActorAccountId, t: Transaction) {
   const [ videoChannelCreated ] = await VideoChannelModel.findOrCreate({
     defaults: {
       name: result.name,
@@ -460,5 +490,5 @@ async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResu
     transaction: t
   })
 
-  return videoChannelCreated
+  return videoChannelCreated as MChannel
 }
index 0e3d78590dfa78be4bc1a65a9a3a9a1f18e2d61e..f2ab54cf7fd1895c4a866c81b343512f11aeae17 100644 (file)
@@ -3,11 +3,10 @@ import { ActivityAudience } from '../../../shared/models/activitypub'
 import { ACTIVITY_PUB } from '../../initializers/constants'
 import { ActorModel } from '../../models/activitypub/actor'
 import { VideoModel } from '../../models/video/video'
-import { VideoCommentModel } from '../../models/video/video-comment'
 import { VideoShareModel } from '../../models/video/video-share'
-import { ActorModelOnly } from '../../typings/models'
+import { MActorFollowersUrl, MActorLight, MCommentOwner, MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../../typings/models'
 
-function getRemoteVideoAudience (video: VideoModel, actorsInvolvedInVideo: ActorModel[]): ActivityAudience {
+function getRemoteVideoAudience (video: MVideoAccountLight, actorsInvolvedInVideo: MActorFollowersUrl[]): ActivityAudience {
   return {
     to: [ video.VideoChannel.Account.Actor.url ],
     cc: actorsInvolvedInVideo.map(a => a.followersUrl)
@@ -15,9 +14,9 @@ function getRemoteVideoAudience (video: VideoModel, actorsInvolvedInVideo: Actor
 }
 
 function getVideoCommentAudience (
-  videoComment: VideoCommentModel,
-  threadParentComments: VideoCommentModel[],
-  actorsInvolvedInVideo: ActorModel[],
+  videoComment: MCommentOwnerVideo,
+  threadParentComments: MCommentOwner[],
+  actorsInvolvedInVideo: MActorFollowersUrl[],
   isOrigin = false
 ): ActivityAudience {
   const to = [ ACTIVITY_PUB.PUBLIC ]
@@ -42,26 +41,28 @@ function getVideoCommentAudience (
   }
 }
 
-function getAudienceFromFollowersOf (actorsInvolvedInObject: ActorModel[]): ActivityAudience {
+function getAudienceFromFollowersOf (actorsInvolvedInObject: MActorFollowersUrl[]): ActivityAudience {
   return {
     to: [ ACTIVITY_PUB.PUBLIC ].concat(actorsInvolvedInObject.map(a => a.followersUrl)),
     cc: []
   }
 }
 
-async function getActorsInvolvedInVideo (video: VideoModel, t: Transaction) {
-  const actors = await VideoShareModel.loadActorsByShare(video.id, t)
+async function getActorsInvolvedInVideo (video: MVideo, t: Transaction) {
+  const actors: MActorLight[] = await VideoShareModel.loadActorsByShare(video.id, t)
 
-  const videoActor = video.VideoChannel && video.VideoChannel.Account
-    ? video.VideoChannel.Account.Actor
-    : await ActorModel.loadAccountActorByVideoId(video.id, t)
+  const videoAll = video as VideoModel
+
+  const videoActor = videoAll.VideoChannel && videoAll.VideoChannel.Account
+    ? videoAll.VideoChannel.Account.Actor
+    : await ActorModel.loadFromAccountByVideoId(video.id, t)
 
   actors.push(videoActor)
 
   return actors
 }
 
-function getAudience (actorSender: ActorModelOnly, isPublic = true) {
+function getAudience (actorSender: MActorFollowersUrl, isPublic = true) {
   return buildAudience([ actorSender.followersUrl ], isPublic)
 }
 
index de5cc54accc5fddf9677cb40888a2914a5fa5b12..65b2dcb494d337317fb380276c0cb49eaac4c809 100644 (file)
@@ -1,10 +1,10 @@
 import { CacheFileObject } from '../../../shared/index'
-import { VideoModel } from '../../models/video/video'
 import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy'
 import { Transaction } from 'sequelize'
 import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
+import { MActorId, MVideoRedundancy, MVideoWithAllFiles } from '@server/typings/models'
 
-function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject, video: VideoModel, byActor: { id?: number }) {
+function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject, video: MVideoWithAllFiles, byActor: MActorId) {
 
   if (cacheFileObject.url.mediaType === 'application/x-mpegURL') {
     const url = cacheFileObject.url
@@ -39,7 +39,7 @@ function cacheFileActivityObjectToDBAttributes (cacheFileObject: CacheFileObject
   }
 }
 
-async function createOrUpdateCacheFile (cacheFileObject: CacheFileObject, video: VideoModel, byActor: { id?: number }, t: Transaction) {
+async function createOrUpdateCacheFile (cacheFileObject: CacheFileObject, video: MVideoWithAllFiles, byActor: MActorId, t: Transaction) {
   const redundancyModel = await VideoRedundancyModel.loadByUrl(cacheFileObject.id, t)
 
   if (!redundancyModel) {
@@ -49,7 +49,7 @@ async function createOrUpdateCacheFile (cacheFileObject: CacheFileObject, video:
   }
 }
 
-function createCacheFile (cacheFileObject: CacheFileObject, video: VideoModel, byActor: { id?: number }, t: Transaction) {
+function createCacheFile (cacheFileObject: CacheFileObject, video: MVideoWithAllFiles, byActor: MActorId, t: Transaction) {
   const attributes = cacheFileActivityObjectToDBAttributes(cacheFileObject, video, byActor)
 
   return VideoRedundancyModel.create(attributes, { transaction: t })
@@ -57,9 +57,9 @@ function createCacheFile (cacheFileObject: CacheFileObject, video: VideoModel, b
 
 function updateCacheFile (
   cacheFileObject: CacheFileObject,
-  redundancyModel: VideoRedundancyModel,
-  video: VideoModel,
-  byActor: { id?: number },
+  redundancyModel: MVideoRedundancy,
+  video: MVideoWithAllFiles,
+  byActor: MActorId,
   t: Transaction
 ) {
   if (redundancyModel.actorId !== byActor.id) {
diff --git a/server/lib/activitypub/follow.ts b/server/lib/activitypub/follow.ts
new file mode 100644 (file)
index 0000000..1abf43c
--- /dev/null
@@ -0,0 +1,36 @@
+import { MActorFollowActors } from '../../typings/models'
+import { CONFIG } from '../../initializers/config'
+import { SERVER_ACTOR_NAME } from '../../initializers/constants'
+import { JobQueue } from '../job-queue'
+import { logger } from '../../helpers/logger'
+import { getServerActor } from '../../helpers/utils'
+import { ServerModel } from '../../models/server/server'
+
+async function autoFollowBackIfNeeded (actorFollow: MActorFollowActors) {
+  if (!CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_BACK.ENABLED) return
+
+  const follower = actorFollow.ActorFollower
+
+  if (follower.type === 'Application' && follower.preferredUsername === SERVER_ACTOR_NAME) {
+    logger.info('Auto follow back %s.', follower.url)
+
+    const me = await getServerActor()
+
+    const server = await ServerModel.load(follower.serverId)
+    const host = server.host
+
+    const payload = {
+      host,
+      name: SERVER_ACTOR_NAME,
+      followerActorId: me.id,
+      isAutoFollow: true
+    }
+
+    JobQueue.Instance.createJob({ type: 'activitypub-follow', payload })
+            .catch(err => logger.error('Cannot create auto follow back job for %s.', host, err))
+  }
+}
+
+export {
+  autoFollowBackIfNeeded
+}
index c2e2a3283fe5e9fc16d0f73b3b616bec5a881ea4..c52b715ef212a9a942767f71b124edfbf67f9f5f 100644 (file)
@@ -1,7 +1,6 @@
 import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object'
 import { crawlCollectionPage } from './crawl'
 import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
-import { AccountModel } from '../../models/account/account'
 import { isArray } from '../../helpers/custom-validators/misc'
 import { getOrCreateActorAndServerAndModel } from './actor'
 import { logger } from '../../helpers/logger'
@@ -13,14 +12,14 @@ import { PlaylistElementObject } from '../../../shared/models/activitypub/object
 import { getOrCreateVideoAndAccountAndChannel } from './videos'
 import { isPlaylistElementObjectValid, isPlaylistObjectValid } from '../../helpers/custom-validators/activitypub/playlist'
 import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element'
-import { VideoModel } from '../../models/video/video'
 import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
 import { sequelizeTypescript } from '../../initializers/database'
 import { createPlaylistMiniatureFromUrl } from '../thumbnail'
 import { FilteredModelAttributes } from '../../typings/sequelize'
-import { AccountModelId } from '../../typings/models'
+import { MAccountDefault, MAccountId, MVideoId } from '../../typings/models'
+import { MVideoPlaylist, MVideoPlaylistId, MVideoPlaylistOwner } from '../../typings/models/video/video-playlist'
 
-function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: AccountModelId, to: string[]) {
+function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: MAccountId, to: string[]) {
   const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPlaylistPrivacy.PUBLIC : VideoPlaylistPrivacy.UNLISTED
 
   return {
@@ -36,7 +35,7 @@ function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount
   }
 }
 
-function playlistElementObjectToDBAttributes (elementObject: PlaylistElementObject, videoPlaylist: VideoPlaylistModel, video: VideoModel) {
+function playlistElementObjectToDBAttributes (elementObject: PlaylistElementObject, videoPlaylist: MVideoPlaylistId, video: MVideoId) {
   return {
     position: elementObject.position,
     url: elementObject.id,
@@ -47,7 +46,7 @@ function playlistElementObjectToDBAttributes (elementObject: PlaylistElementObje
   }
 }
 
-async function createAccountPlaylists (playlistUrls: string[], account: AccountModel) {
+async function createAccountPlaylists (playlistUrls: string[], account: MAccountDefault) {
   await Bluebird.map(playlistUrls, async playlistUrl => {
     try {
       const exists = await VideoPlaylistModel.doesPlaylistExist(playlistUrl)
@@ -75,7 +74,7 @@ async function createAccountPlaylists (playlistUrls: string[], account: AccountM
   }, { concurrency: CRAWL_REQUEST_CONCURRENCY })
 }
 
-async function createOrUpdateVideoPlaylist (playlistObject: PlaylistObject, byAccount: AccountModelId, to: string[]) {
+async function createOrUpdateVideoPlaylist (playlistObject: PlaylistObject, byAccount: MAccountId, to: string[]) {
   const playlistAttributes = playlistObjectToDBAttributes(playlistObject, byAccount, to)
 
   if (isArray(playlistObject.attributedTo) && playlistObject.attributedTo.length === 1) {
@@ -88,7 +87,7 @@ async function createOrUpdateVideoPlaylist (playlistObject: PlaylistObject, byAc
     }
   }
 
-  const [ playlist ] = await VideoPlaylistModel.upsert<VideoPlaylistModel>(playlistAttributes, { returning: true })
+  const [ playlist ] = await VideoPlaylistModel.upsert<MVideoPlaylist>(playlistAttributes, { returning: true })
 
   let accItems: string[] = []
   await crawlCollectionPage<string>(playlistObject.id, items => {
@@ -114,7 +113,7 @@ async function createOrUpdateVideoPlaylist (playlistObject: PlaylistObject, byAc
   return resetVideoPlaylistElements(accItems, refreshedPlaylist)
 }
 
-async function refreshVideoPlaylistIfNeeded (videoPlaylist: VideoPlaylistModel): Promise<VideoPlaylistModel> {
+async function refreshVideoPlaylistIfNeeded (videoPlaylist: MVideoPlaylistOwner): Promise<MVideoPlaylistOwner> {
   if (!videoPlaylist.isOutdated()) return videoPlaylist
 
   try {
@@ -157,7 +156,7 @@ export {
 
 // ---------------------------------------------------------------------------
 
-async function resetVideoPlaylistElements (elementUrls: string[], playlist: VideoPlaylistModel) {
+async function resetVideoPlaylistElements (elementUrls: string[], playlist: MVideoPlaylist) {
   const elementsToCreate: FilteredModelAttributes<VideoPlaylistElementModel>[] = []
 
   await Bluebird.map(elementUrls, async elementUrl => {
index cf27e6c32e6e5280684c05c85674e73545ebcdb7..dcfbb2c84a673ab1ca23c170fd6ed920b8608959 100644 (file)
@@ -1,9 +1,8 @@
 import { ActivityAccept } from '../../../../shared/models/activitypub'
-import { ActorModel } from '../../../models/activitypub/actor'
 import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
 import { addFetchOutboxJob } from '../actor'
 import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
-import { SignatureActorModel } from '../../../typings/models'
+import { MActorDefault, MActorSignature } from '../../../typings/models'
 
 async function processAcceptActivity (options: APProcessorOptions<ActivityAccept>) {
   const { byActor: targetActor, inboxActor } = options
@@ -20,12 +19,12 @@ export {
 
 // ---------------------------------------------------------------------------
 
-async function processAccept (actor: ActorModel, targetActor: SignatureActorModel) {
+async function processAccept (actor: MActorDefault, targetActor: MActorSignature) {
   const follow = await ActorFollowModel.loadByActorAndTarget(actor.id, targetActor.id)
   if (!follow) throw new Error('Cannot find associated follow.')
 
   if (follow.state !== 'accepted') {
-    follow.set('state', 'accepted')
+    follow.state = 'accepted'
     await follow.save()
 
     await addFetchOutboxJob(targetActor)
index b3cdc4441cb0dbe9d8db63e3e376e6a33b3ed712..7e22125d5ce644a64b7bbc527b2576f51eceebb0 100644 (file)
@@ -5,10 +5,9 @@ import { VideoShareModel } from '../../../models/video/video-share'
 import { forwardVideoRelatedActivity } from '../send/utils'
 import { getOrCreateVideoAndAccountAndChannel } from '../videos'
 import { Notifier } from '../../notifier'
-import { VideoModel } from '../../../models/video/video'
 import { logger } from '../../../helpers/logger'
 import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
-import { SignatureActorModel } from '../../../typings/models'
+import { MActorSignature, MVideoAccountLightBlacklistAllFiles } from '../../../typings/models'
 
 async function processAnnounceActivity (options: APProcessorOptions<ActivityAnnounce>) {
   const { activity, byActor: actorAnnouncer } = options
@@ -26,10 +25,10 @@ export {
 
 // ---------------------------------------------------------------------------
 
-async function processVideoShare (actorAnnouncer: SignatureActorModel, activity: ActivityAnnounce, notify: boolean) {
+async function processVideoShare (actorAnnouncer: MActorSignature, activity: ActivityAnnounce, notify: boolean) {
   const objectUri = typeof activity.object === 'string' ? activity.object : activity.object.id
 
-  let video: VideoModel
+  let video: MVideoAccountLightBlacklistAllFiles
   let videoCreated: boolean
 
   try {
index 6815c6997399398aed1bdd055d94332ccaa3f90b..bee853721033b4d8dfecdaba321f8eb7a485f0d9 100644 (file)
@@ -10,10 +10,8 @@ import { createOrUpdateCacheFile } from '../cache-file'
 import { Notifier } from '../../notifier'
 import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object'
 import { createOrUpdateVideoPlaylist } from '../playlist'
-import { VideoModel } from '../../../models/video/video'
 import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
-import { VideoCommentModel } from '../../../models/video/video-comment'
-import { SignatureActorModel } from '../../../typings/models'
+import { MActorSignature, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../../typings/models'
 
 async function processCreateActivity (options: APProcessorOptions<ActivityCreate>) {
   const { activity, byActor } = options
@@ -61,7 +59,7 @@ async function processCreateVideo (activity: ActivityCreate, notify: boolean) {
   return video
 }
 
-async function processCreateCacheFile (activity: ActivityCreate, byActor: SignatureActorModel) {
+async function processCreateCacheFile (activity: ActivityCreate, byActor: MActorSignature) {
   const cacheFile = activity.object as CacheFileObject
 
   const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: cacheFile.object })
@@ -77,15 +75,15 @@ async function processCreateCacheFile (activity: ActivityCreate, byActor: Signat
   }
 }
 
-async function processCreateVideoComment (activity: ActivityCreate, byActor: SignatureActorModel, notify: boolean) {
+async function processCreateVideoComment (activity: ActivityCreate, byActor: MActorSignature, notify: boolean) {
   const commentObject = activity.object as VideoCommentObject
   const byAccount = byActor.Account
 
   if (!byAccount) throw new Error('Cannot create video comment with the non account actor ' + byActor.url)
 
-  let video: VideoModel
+  let video: MVideoAccountLightBlacklistAllFiles
   let created: boolean
-  let comment: VideoCommentModel
+  let comment: MCommentOwnerVideo
   try {
     const resolveThreadResult = await resolveThread({ url: commentObject.id, isVideo: false })
     video = resolveThreadResult.video
@@ -110,7 +108,7 @@ async function processCreateVideoComment (activity: ActivityCreate, byActor: Sig
   if (created && notify) Notifier.Instance.notifyOnNewComment(comment)
 }
 
-async function processCreatePlaylist (activity: ActivityCreate, byActor: SignatureActorModel) {
+async function processCreatePlaylist (activity: ActivityCreate, byActor: MActorSignature) {
   const playlistObject = activity.object as PlaylistObject
   const byAccount = byActor.Account
 
index 344d14322686b9bedd77a541fb3a563302fe8004..79d0e0d79424901d8226f7a3b9ba8ab664b92cc2 100644 (file)
@@ -2,15 +2,13 @@ import { ActivityDelete } from '../../../../shared/models/activitypub'
 import { retryTransactionWrapper } from '../../../helpers/database-utils'
 import { logger } from '../../../helpers/logger'
 import { sequelizeTypescript } from '../../../initializers'
-import { AccountModel } from '../../../models/account/account'
 import { ActorModel } from '../../../models/activitypub/actor'
 import { VideoModel } from '../../../models/video/video'
-import { VideoChannelModel } from '../../../models/video/video-channel'
 import { VideoCommentModel } from '../../../models/video/video-comment'
 import { forwardVideoRelatedActivity } from '../send/utils'
 import { VideoPlaylistModel } from '../../../models/video/video-playlist'
 import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
-import { SignatureActorModel } from '../../../typings/models'
+import { MAccountActor, MActor, MActorSignature, MChannelActor, MChannelActorAccountActor } from '../../../typings/models'
 
 async function processDeleteActivity (options: APProcessorOptions<ActivityDelete>) {
   const { activity, byActor } = options
@@ -24,13 +22,17 @@ async function processDeleteActivity (options: APProcessorOptions<ActivityDelete
     if (byActorFull.type === 'Person') {
       if (!byActorFull.Account) throw new Error('Actor ' + byActorFull.url + ' is a person but we cannot find it in database.')
 
-      byActorFull.Account.Actor = await byActorFull.Account.$get('Actor') as ActorModel
-      return retryTransactionWrapper(processDeleteAccount, byActorFull.Account)
+      const accountToDelete = byActorFull.Account as MAccountActor
+      accountToDelete.Actor = byActorFull
+
+      return retryTransactionWrapper(processDeleteAccount, accountToDelete)
     } else if (byActorFull.type === 'Group') {
       if (!byActorFull.VideoChannel) throw new Error('Actor ' + byActorFull.url + ' is a group but we cannot find it in database.')
 
-      byActorFull.VideoChannel.Actor = await byActorFull.VideoChannel.$get('Actor') as ActorModel
-      return retryTransactionWrapper(processDeleteVideoChannel, byActorFull.VideoChannel)
+      const channelToDelete = byActorFull.VideoChannel as MChannelActorAccountActor
+      channelToDelete.Actor = byActorFull
+
+      return retryTransactionWrapper(processDeleteVideoChannel, channelToDelete)
     }
   }
 
@@ -70,7 +72,7 @@ export {
 
 // ---------------------------------------------------------------------------
 
-async function processDeleteVideo (actor: ActorModel, videoToDelete: VideoModel) {
+async function processDeleteVideo (actor: MActor, videoToDelete: VideoModel) {
   logger.debug('Removing remote video "%s".', videoToDelete.uuid)
 
   await sequelizeTypescript.transaction(async t => {
@@ -84,7 +86,7 @@ async function processDeleteVideo (actor: ActorModel, videoToDelete: VideoModel)
   logger.info('Remote video with uuid %s removed.', videoToDelete.uuid)
 }
 
-async function processDeleteVideoPlaylist (actor: ActorModel, playlistToDelete: VideoPlaylistModel) {
+async function processDeleteVideoPlaylist (actor: MActor, playlistToDelete: VideoPlaylistModel) {
   logger.debug('Removing remote video playlist "%s".', playlistToDelete.uuid)
 
   await sequelizeTypescript.transaction(async t => {
@@ -98,7 +100,7 @@ async function processDeleteVideoPlaylist (actor: ActorModel, playlistToDelete:
   logger.info('Remote video playlist with uuid %s removed.', playlistToDelete.uuid)
 }
 
-async function processDeleteAccount (accountToRemove: AccountModel) {
+async function processDeleteAccount (accountToRemove: MAccountActor) {
   logger.debug('Removing remote account "%s".', accountToRemove.Actor.url)
 
   await sequelizeTypescript.transaction(async t => {
@@ -108,7 +110,7 @@ async function processDeleteAccount (accountToRemove: AccountModel) {
   logger.info('Remote account %s removed.', accountToRemove.Actor.url)
 }
 
-async function processDeleteVideoChannel (videoChannelToRemove: VideoChannelModel) {
+async function processDeleteVideoChannel (videoChannelToRemove: MChannelActor) {
   logger.debug('Removing remote video channel "%s".', videoChannelToRemove.Actor.url)
 
   await sequelizeTypescript.transaction(async t => {
@@ -118,7 +120,7 @@ async function processDeleteVideoChannel (videoChannelToRemove: VideoChannelMode
   logger.info('Remote video channel %s removed.', videoChannelToRemove.Actor.url)
 }
 
-function processDeleteVideoComment (byActor: SignatureActorModel, videoComment: VideoCommentModel, activity: ActivityDelete) {
+function processDeleteVideoComment (byActor: MActorSignature, videoComment: VideoCommentModel, activity: ActivityDelete) {
   logger.debug('Removing remote video comment "%s".', videoComment.url)
 
   return sequelizeTypescript.transaction(async t => {
index 727fcfee0c7bef041f9a9b0567e2b7a39948d581..debd8a67ca43d4346d6b2d8e28761fd479f32733 100644 (file)
@@ -7,7 +7,7 @@ import { getOrCreateVideoAndAccountAndChannel } from '../videos'
 import { forwardVideoRelatedActivity } from '../send/utils'
 import { getVideoDislikeActivityPubUrl } from '../url'
 import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
-import { SignatureActorModel } from '../../../typings/models'
+import { MActorSignature } from '../../../typings/models'
 
 async function processDislikeActivity (options: APProcessorOptions<ActivityCreate | ActivityDislike>) {
   const { activity, byActor } = options
@@ -22,7 +22,7 @@ export {
 
 // ---------------------------------------------------------------------------
 
-async function processDislike (activity: ActivityCreate | ActivityDislike, byActor: SignatureActorModel) {
+async function processDislike (activity: ActivityCreate | ActivityDislike, byActor: MActorSignature) {
   const dislikeObject = activity.type === 'Dislike' ? activity.object : (activity.object as DislikeObject).object
   const byAccount = byActor.Account
 
index 1f8a80c140bb29758c718971d75a20ffda9696ad..e6e9084de2aa23edde37192b2b38e29d9fc1c062 100644 (file)
@@ -8,7 +8,7 @@ import { getOrCreateVideoAndAccountAndChannel } from '../videos'
 import { Notifier } from '../../notifier'
 import { getAPId } from '../../../helpers/activitypub'
 import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
-import { SignatureActorModel } from '../../../typings/models'
+import { MActorSignature, MVideoAbuseVideo } from '../../../typings/models'
 
 async function processFlagActivity (options: APProcessorOptions<ActivityCreate | ActivityFlag>) {
   const { activity, byActor } = options
@@ -23,31 +23,39 @@ export {
 
 // ---------------------------------------------------------------------------
 
-async function processCreateVideoAbuse (activity: ActivityCreate | ActivityFlag, byActor: SignatureActorModel) {
+async function processCreateVideoAbuse (activity: ActivityCreate | ActivityFlag, byActor: MActorSignature) {
   const flag = activity.type === 'Flag' ? activity : (activity.object as VideoAbuseObject)
 
-  logger.debug('Reporting remote abuse for video %s.', getAPId(flag.object))
-
   const account = byActor.Account
   if (!account) throw new Error('Cannot create video abuse with the non account actor ' + byActor.url)
 
-  const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: flag.object })
+  const objects = Array.isArray(flag.object) ? flag.object : [ flag.object ]
 
-  const videoAbuse = await sequelizeTypescript.transaction(async t => {
-    const videoAbuseData = {
-      reporterAccountId: account.id,
-      reason: flag.content,
-      videoId: video.id,
-      state: VideoAbuseState.PENDING
-    }
+  for (const object of objects) {
+    try {
+      logger.debug('Reporting remote abuse for video %s.', getAPId(object))
+
+      const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: object })
 
-    const videoAbuseInstance = await VideoAbuseModel.create(videoAbuseData, { transaction: t })
-    videoAbuseInstance.Video = video
+      const videoAbuse = await sequelizeTypescript.transaction(async t => {
+        const videoAbuseData = {
+          reporterAccountId: account.id,
+          reason: flag.content,
+          videoId: video.id,
+          state: VideoAbuseState.PENDING
+        }
 
-    logger.info('Remote abuse for video uuid %s created', flag.object)
+        const videoAbuseInstance = await VideoAbuseModel.create(videoAbuseData, { transaction: t }) as MVideoAbuseVideo
+        videoAbuseInstance.Video = video
 
-    return videoAbuseInstance
-  })
+        logger.info('Remote abuse for video uuid %s created', flag.object)
 
-  Notifier.Instance.notifyOnNewVideoAbuse(videoAbuse)
+        return videoAbuseInstance
+      })
+
+      Notifier.Instance.notifyOnNewVideoAbuse(videoAbuse)
+    } catch (err) {
+      logger.debug('Cannot process report of %s. (Maybe not a video abuse).', getAPId(object), { err })
+    }
+  }
 }
index 240aa57998bbaaf09ecf4cce846434326365b5d8..85f22d654ebd9611ac7cfb8a7691e3a3d7adb5e2 100644 (file)
@@ -10,8 +10,8 @@ import { getAPId } from '../../../helpers/activitypub'
 import { getServerActor } from '../../../helpers/utils'
 import { CONFIG } from '../../../initializers/config'
 import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
-import { SignatureActorModel } from '../../../typings/models'
-import { ActorFollowModelLight } from '../../../typings/models/actor-follow'
+import { MActorFollowActors, MActorSignature } from '../../../typings/models'
+import { autoFollowBackIfNeeded } from '../follow'
 
 async function processFollowActivity (options: APProcessorOptions<ActivityFollow>) {
   const { activity, byActor } = options
@@ -28,8 +28,8 @@ export {
 
 // ---------------------------------------------------------------------------
 
-async function processFollow (byActor: SignatureActorModel, targetActorURL: string) {
-  const { actorFollow, created, isFollowingInstance } = await sequelizeTypescript.transaction(async t => {
+async function processFollow (byActor: MActorSignature, targetActorURL: string) {
+  const { actorFollow, created, isFollowingInstance, targetActor } = await sequelizeTypescript.transaction(async t => {
     const targetActor = await ActorModel.loadByUrlAndPopulateAccountAndChannel(targetActorURL, t)
 
     if (!targetActor) throw new Error('Unknown actor')
@@ -43,10 +43,10 @@ async function processFollow (byActor: SignatureActorModel, targetActorURL: stri
 
       await sendReject(byActor, targetActor)
 
-      return { actorFollow: undefined }
+      return { actorFollow: undefined as MActorFollowActors }
     }
 
-    const [ actorFollow, created ] = await ActorFollowModel.findOrCreate({
+    const [ actorFollow, created ] = await ActorFollowModel.findOrCreate<MActorFollowActors>({
       where: {
         actorId: byActor.id,
         targetActorId: targetActor.id
@@ -57,7 +57,7 @@ async function processFollow (byActor: SignatureActorModel, targetActorURL: stri
         state: CONFIG.FOLLOWERS.INSTANCE.MANUAL_APPROVAL ? 'pending' : 'accepted'
       },
       transaction: t
-    }) as [ ActorFollowModelLight, boolean ]
+    })
 
     if (actorFollow.state !== 'accepted' && CONFIG.FOLLOWERS.INSTANCE.MANUAL_APPROVAL === false) {
       actorFollow.state = 'accepted'
@@ -68,17 +68,26 @@ async function processFollow (byActor: SignatureActorModel, targetActorURL: stri
     actorFollow.ActorFollowing = targetActor
 
     // Target sends to actor he accepted the follow request
-    if (actorFollow.state === 'accepted') await sendAccept(actorFollow)
+    if (actorFollow.state === 'accepted') {
+      await sendAccept(actorFollow)
+      await autoFollowBackIfNeeded(actorFollow)
+    }
 
-    return { actorFollow, created, isFollowingInstance }
+    return { actorFollow, created, isFollowingInstance, targetActor }
   })
 
   // Rejected
   if (!actorFollow) return
 
   if (created) {
-    if (isFollowingInstance) Notifier.Instance.notifyOfNewInstanceFollow(actorFollow)
-    else Notifier.Instance.notifyOfNewUserFollow(actorFollow)
+    const follower = await ActorModel.loadFull(byActor.id)
+    const actorFollowFull = Object.assign(actorFollow, { ActorFollowing: targetActor, ActorFollower: follower })
+
+    if (isFollowingInstance) {
+      Notifier.Instance.notifyOfNewInstanceFollow(actorFollowFull)
+    } else {
+      Notifier.Instance.notifyOfNewUserFollow(actorFollowFull)
+    }
   }
 
   logger.info('Actor %s is followed by actor %s.', targetActorURL, byActor.url)
index cf559af721c4c65b816ce60acbf8f913f1ecf863..62be0de42f30aa82b076f6d41e64539f5effcc1a 100644 (file)
@@ -7,7 +7,7 @@ import { getOrCreateVideoAndAccountAndChannel } from '../videos'
 import { getVideoLikeActivityPubUrl } from '../url'
 import { getAPId } from '../../../helpers/activitypub'
 import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
-import { SignatureActorModel } from '../../../typings/models'
+import { MActorSignature } from '../../../typings/models'
 
 async function processLikeActivity (options: APProcessorOptions<ActivityLike>) {
   const { activity, byActor } = options
@@ -22,7 +22,7 @@ export {
 
 // ---------------------------------------------------------------------------
 
-async function processLikeVideo (byActor: SignatureActorModel, activity: ActivityLike) {
+async function processLikeVideo (byActor: MActorSignature, activity: ActivityLike) {
   const videoUrl = getAPId(activity.object)
 
   const byAccount = byActor.Account
index 22e311cebf1e6bc9151b15d9c89c0e0c7b17c206..00e9afa109035443b328617d550e2c87e4e149dc 100644 (file)
@@ -2,7 +2,7 @@ import { ActivityReject } from '../../../../shared/models/activitypub/activity'
 import { sequelizeTypescript } from '../../../initializers'
 import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
 import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
-import { ActorModelOnly } from '../../../typings/models'
+import { MActor } from '../../../typings/models'
 
 async function processRejectActivity (options: APProcessorOptions<ActivityReject>) {
   const { byActor: targetActor, inboxActor } = options
@@ -19,7 +19,7 @@ export {
 
 // ---------------------------------------------------------------------------
 
-async function processReject (follower: ActorModelOnly, targetActor: ActorModelOnly) {
+async function processReject (follower: MActor, targetActor: MActor) {
   return sequelizeTypescript.transaction(async t => {
     const actorFollow = await ActorFollowModel.loadByActorAndTarget(follower.id, targetActor.id, t)
 
index c37ee38bb7b008e8ac82f345798c810b72c2d4fb..10643b2e99d9210535aa5998740abea0d479d4f1 100644 (file)
@@ -11,7 +11,7 @@ import { getOrCreateVideoAndAccountAndChannel } from '../videos'
 import { VideoShareModel } from '../../../models/video/video-share'
 import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy'
 import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
-import { SignatureActorModel } from '../../../typings/models'
+import { MActorSignature } from '../../../typings/models'
 
 async function processUndoActivity (options: APProcessorOptions<ActivityUndo>) {
   const { activity, byActor } = options
@@ -54,7 +54,7 @@ export {
 
 // ---------------------------------------------------------------------------
 
-async function processUndoLike (byActor: SignatureActorModel, activity: ActivityUndo) {
+async function processUndoLike (byActor: MActorSignature, activity: ActivityUndo) {
   const likeActivity = activity.object as ActivityLike
 
   const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: likeActivity.object })
@@ -77,7 +77,7 @@ async function processUndoLike (byActor: SignatureActorModel, activity: Activity
   })
 }
 
-async function processUndoDislike (byActor: SignatureActorModel, activity: ActivityUndo) {
+async function processUndoDislike (byActor: MActorSignature, activity: ActivityUndo) {
   const dislike = activity.object.type === 'Dislike'
     ? activity.object
     : activity.object.object as DislikeObject
@@ -102,7 +102,7 @@ async function processUndoDislike (byActor: SignatureActorModel, activity: Activ
   })
 }
 
-async function processUndoCacheFile (byActor: SignatureActorModel, activity: ActivityUndo) {
+async function processUndoCacheFile (byActor: MActorSignature, activity: ActivityUndo) {
   const cacheFileObject = activity.object.object as CacheFileObject
 
   const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: cacheFileObject.object })
@@ -127,7 +127,7 @@ async function processUndoCacheFile (byActor: SignatureActorModel, activity: Act
   })
 }
 
-function processUndoFollow (follower: SignatureActorModel, followActivity: ActivityFollow) {
+function processUndoFollow (follower: MActorSignature, followActivity: ActivityFollow) {
   return sequelizeTypescript.transaction(async t => {
     const following = await ActorModel.loadByUrlAndPopulateAccountAndChannel(followActivity.object, t)
     const actorFollow = await ActorFollowModel.loadByActorAndTarget(follower.id, following.id, t)
@@ -140,7 +140,7 @@ function processUndoFollow (follower: SignatureActorModel, followActivity: Activ
   })
 }
 
-function processUndoAnnounce (byActor: SignatureActorModel, announceActivity: ActivityAnnounce) {
+function processUndoAnnounce (byActor: MActorSignature, announceActivity: ActivityAnnounce) {
   return sequelizeTypescript.transaction(async t => {
     const share = await VideoShareModel.loadByUrl(announceActivity.id, t)
     if (!share) throw new Error(`Unknown video share ${announceActivity.id}.`)
index 414f9e375762d81c0b0185b8e79924a7b6a28102..a47d605d82fbf6b4d5c1434f710f0529488ba054 100644 (file)
@@ -15,7 +15,7 @@ import { forwardVideoRelatedActivity } from '../send/utils'
 import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object'
 import { createOrUpdateVideoPlaylist } from '../playlist'
 import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
-import { SignatureActorModel } from '../../../typings/models'
+import { MActorSignature, MAccountIdActor } from '../../../typings/models'
 
 async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) {
   const { activity, byActor } = options
@@ -53,7 +53,7 @@ export {
 
 // ---------------------------------------------------------------------------
 
-async function processUpdateVideo (actor: SignatureActorModel, activity: ActivityUpdate) {
+async function processUpdateVideo (actor: MActorSignature, activity: ActivityUpdate) {
   const videoObject = activity.object as VideoTorrentObject
 
   if (sanitizeAndCheckVideoTorrentObject(videoObject) === false) {
@@ -61,20 +61,23 @@ async function processUpdateVideo (actor: SignatureActorModel, activity: Activit
     return undefined
   }
 
-  const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: videoObject.id, allowRefresh: false })
+  const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: videoObject.id, allowRefresh: false, fetchType: 'all' })
   const channelActor = await getOrCreateVideoChannelFromVideoObject(videoObject)
 
+  const account = actor.Account as MAccountIdActor
+  account.Actor = actor
+
   const updateOptions = {
     video,
     videoObject,
-    account: actor.Account,
+    account,
     channel: channelActor.VideoChannel,
     overrideTo: activity.to
   }
   return updateVideoFromAP(updateOptions)
 }
 
-async function processUpdateCacheFile (byActor: SignatureActorModel, activity: ActivityUpdate) {
+async function processUpdateCacheFile (byActor: MActorSignature, activity: ActivityUpdate) {
   const cacheFileObject = activity.object as CacheFileObject
 
   if (!isCacheFileObjectValid(cacheFileObject)) {
@@ -150,7 +153,7 @@ async function processUpdateActor (actor: ActorModel, activity: ActivityUpdate)
   }
 }
 
-async function processUpdatePlaylist (byActor: SignatureActorModel, activity: ActivityUpdate) {
+async function processUpdatePlaylist (byActor: MActorSignature, activity: ActivityUpdate) {
   const playlistObject = activity.object as PlaylistObject
   const byAccount = byActor.Account
 
index e4997b8282a492d606e56cca6cf08d0bb3cab5d1..df29ee968b85fe7c22fd20c6eed427683fa1a2e8 100644 (file)
@@ -3,7 +3,7 @@ import { forwardVideoRelatedActivity } from '../send/utils'
 import { Redis } from '../../redis'
 import { ActivityCreate, ActivityView, ViewObject } from '../../../../shared/models/activitypub'
 import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
-import { SignatureActorModel } from '../../../typings/models'
+import { MActorSignature } from '../../../typings/models'
 
 async function processViewActivity (options: APProcessorOptions<ActivityCreate | ActivityView>) {
   const { activity, byActor } = options
@@ -18,11 +18,11 @@ export {
 
 // ---------------------------------------------------------------------------
 
-async function processCreateView (activity: ActivityView | ActivityCreate, byActor: SignatureActorModel) {
+async function processCreateView (activity: ActivityView | ActivityCreate, byActor: MActorSignature) {
   const videoObject = activity.type === 'View' ? activity.object : (activity.object as ViewObject).object
 
   const options = {
-    videoObject: videoObject,
+    videoObject,
     fetchType: 'only-video' as 'only-video'
   }
   const { video } = await getOrCreateVideoAndAccountAndChannel(options)
index d108fe321b4e9a59c88bdaccf42c88ace93d0628..c602bf2181edf6f2fc45df13bbc32b443a63b44a 100644 (file)
@@ -1,7 +1,6 @@
 import { Activity, ActivityType } from '../../../../shared/models/activitypub'
 import { checkUrlsSameHost, getAPId } from '../../../helpers/activitypub'
 import { logger } from '../../../helpers/logger'
-import { ActorModel } from '../../../models/activitypub/actor'
 import { processAcceptActivity } from './process-accept'
 import { processAnnounceActivity } from './process-announce'
 import { processCreateActivity } from './process-create'
@@ -16,7 +15,7 @@ import { processDislikeActivity } from './process-dislike'
 import { processFlagActivity } from './process-flag'
 import { processViewActivity } from './process-view'
 import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
-import { SignatureActorModel } from '../../../typings/models'
+import { MActorDefault, MActorSignature } from '../../../typings/models'
 
 const processActivity: { [ P in ActivityType ]: (options: APProcessorOptions<Activity>) => Promise<any> } = {
   Create: processCreateActivity,
@@ -36,15 +35,15 @@ const processActivity: { [ P in ActivityType ]: (options: APProcessorOptions<Act
 async function processActivities (
   activities: Activity[],
   options: {
-    signatureActor?: SignatureActorModel
-    inboxActor?: ActorModel
+    signatureActor?: MActorSignature
+    inboxActor?: MActorDefault
     outboxUrl?: string
     fromFetch?: boolean
   } = {}
 ) {
   const { outboxUrl, signatureActor, inboxActor, fromFetch = false } = options
 
-  const actorsCache: { [ url: string ]: SignatureActorModel } = {}
+  const actorsCache: { [ url: string ]: MActorSignature } = {}
 
   for (const activity of activities) {
     if (!signatureActor && [ 'Create', 'Announce', 'Like' ].includes(activity.type) === false) {
@@ -75,7 +74,7 @@ async function processActivities (
     }
 
     try {
-      await activityProcessor({ activity, byActor, inboxActor: inboxActor, fromFetch })
+      await activityProcessor({ activity, byActor, inboxActor, fromFetch })
     } catch (err) {
       logger.warn('Cannot process activity %s.', activity.type, { err })
     }
index 813c42e15dc643e2270369c6f970bdba45a6ad7a..9f0225b644746bf8e20421d86f831d8ddba191ab 100644 (file)
@@ -3,10 +3,9 @@ import { getActorFollowAcceptActivityPubUrl, getActorFollowActivityPubUrl } from
 import { unicastTo } from './utils'
 import { buildFollowActivity } from './send-follow'
 import { logger } from '../../../helpers/logger'
-import { ActorFollowModelLight } from '../../../typings/models/actor-follow'
-import { ActorModelOnly } from '../../../typings/models'
+import { MActor, MActorFollowActors } from '../../../typings/models'
 
-async function sendAccept (actorFollow: ActorFollowModelLight) {
+async function sendAccept (actorFollow: MActorFollowActors) {
   const follower = actorFollow.ActorFollower
   const me = actorFollow.ActorFollowing
 
@@ -34,7 +33,7 @@ export {
 
 // ---------------------------------------------------------------------------
 
-function buildAcceptActivity (url: string, byActor: ActorModelOnly, followActivityData: ActivityFollow): ActivityAccept {
+function buildAcceptActivity (url: string, byActor: MActor, followActivityData: ActivityFollow): ActivityAccept {
   return {
     type: 'Accept',
     id: url,
index 7fe4ca180f5f46b9e53b8a9e503b1982e56b55b1..a0f33852ca79a7a8a07b0f26037db975c30274a5 100644 (file)
@@ -1,16 +1,15 @@
 import { Transaction } from 'sequelize'
 import { ActivityAnnounce, ActivityAudience } from '../../../../shared/models/activitypub'
-import { VideoModel } from '../../../models/video/video'
 import { broadcastToFollowers } from './utils'
 import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf } from '../audience'
 import { logger } from '../../../helpers/logger'
-import { ActorModelOnly } from '../../../typings/models'
-import { VideoShareModelOnly } from '../../../typings/models/video-share'
+import { MActorLight, MVideo } from '../../../typings/models'
+import { MVideoShare } from '../../../typings/models/video'
 
 async function buildAnnounceWithVideoAudience (
-  byActor: ActorModelOnly,
-  videoShare: VideoShareModelOnly,
-  video: VideoModel,
+  byActor: MActorLight,
+  videoShare: MVideoShare,
+  video: MVideo,
   t: Transaction
 ) {
   const announcedObject = video.url
@@ -23,7 +22,7 @@ async function buildAnnounceWithVideoAudience (
   return { activity, actorsInvolvedInVideo }
 }
 
-async function sendVideoAnnounce (byActor: ActorModelOnly, videoShare: VideoShareModelOnly, video: VideoModel, t: Transaction) {
+async function sendVideoAnnounce (byActor: MActorLight, videoShare: MVideoShare, video: MVideo, t: Transaction) {
   const { activity, actorsInvolvedInVideo } = await buildAnnounceWithVideoAudience(byActor, videoShare, video, t)
 
   logger.info('Creating job to send announce %s.', videoShare.url)
@@ -32,7 +31,7 @@ async function sendVideoAnnounce (byActor: ActorModelOnly, videoShare: VideoShar
   return broadcastToFollowers(activity, byActor, actorsInvolvedInVideo, t, followersException)
 }
 
-function buildAnnounceActivity (url: string, byActor: ActorModelOnly, object: string, audience?: ActivityAudience): ActivityAnnounce {
+function buildAnnounceActivity (url: string, byActor: MActorLight, object: string, audience?: ActivityAudience): ActivityAnnounce {
   if (!audience) audience = getAudience(byActor)
 
   return audiencify({
index 9c21149f20c184b34febb92f89f8029bf8b8c1ad..26ec3e94873ea619796754fd5ffc50ce3525bb9e 100644 (file)
@@ -1,19 +1,23 @@
 import { Transaction } from 'sequelize'
 import { ActivityAudience, ActivityCreate } from '../../../../shared/models/activitypub'
 import { VideoPrivacy } from '../../../../shared/models/videos'
-import { ActorModel } from '../../../models/activitypub/actor'
-import { VideoModel } from '../../../models/video/video'
 import { VideoCommentModel } from '../../../models/video/video-comment'
 import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils'
 import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience'
 import { logger } from '../../../helpers/logger'
-import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy'
-import { VideoPlaylistModel } from '../../../models/video/video-playlist'
 import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
 import { getServerActor } from '../../../helpers/utils'
-import * as Bluebird from 'bluebird'
-
-async function sendCreateVideo (video: VideoModel, t: Transaction) {
+import {
+  MActorLight,
+  MCommentOwnerVideo,
+  MVideoAccountLight,
+  MVideoAP,
+  MVideoPlaylistFull,
+  MVideoRedundancyFileVideo,
+  MVideoRedundancyStreamingPlaylistVideo
+} from '../../../typings/models'
+
+async function sendCreateVideo (video: MVideoAP, t: Transaction) {
   if (video.privacy === VideoPrivacy.PRIVATE) return undefined
 
   logger.info('Creating job to send video creation of %s.', video.url)
@@ -27,7 +31,11 @@ async function sendCreateVideo (video: VideoModel, t: Transaction) {
   return broadcastToFollowers(createActivity, byActor, [ byActor ], t)
 }
 
-async function sendCreateCacheFile (byActor: ActorModel, video: VideoModel, fileRedundancy: VideoRedundancyModel) {
+async function sendCreateCacheFile (
+  byActor: MActorLight,
+  video: MVideoAccountLight,
+  fileRedundancy: MVideoRedundancyStreamingPlaylistVideo | MVideoRedundancyFileVideo
+) {
   logger.info('Creating job to send file cache of %s.', fileRedundancy.url)
 
   return sendVideoRelatedCreateActivity({
@@ -38,7 +46,7 @@ async function sendCreateCacheFile (byActor: ActorModel, video: VideoModel, file
   })
 }
 
-async function sendCreateVideoPlaylist (playlist: VideoPlaylistModel, t: Transaction) {
+async function sendCreateVideoPlaylist (playlist: MVideoPlaylistFull, t: Transaction) {
   if (playlist.privacy === VideoPlaylistPrivacy.PRIVATE) return undefined
 
   logger.info('Creating job to send create video playlist of %s.', playlist.url)
@@ -57,7 +65,7 @@ async function sendCreateVideoPlaylist (playlist: VideoPlaylistModel, t: Transac
   return broadcastToFollowers(createActivity, byActor, toFollowersOf, t)
 }
 
-async function sendCreateVideoComment (comment: VideoCommentModel, t: Transaction) {
+async function sendCreateVideoComment (comment: MCommentOwnerVideo, t: Transaction) {
   logger.info('Creating job to send comment %s.', comment.url)
 
   const isOrigin = comment.Video.isOwned()
@@ -95,7 +103,7 @@ async function sendCreateVideoComment (comment: VideoCommentModel, t: Transactio
   t.afterCommit(() => unicastTo(createActivity, byActor, comment.Video.VideoChannel.Account.Actor.sharedInboxUrl))
 }
 
-function buildCreateActivity (url: string, byActor: ActorModel, object: any, audience?: ActivityAudience): ActivityCreate {
+function buildCreateActivity (url: string, byActor: MActorLight, object: any, audience?: ActivityAudience): ActivityCreate {
   if (!audience) audience = getAudience(byActor)
 
   return audiencify(
@@ -122,8 +130,8 @@ export {
 // ---------------------------------------------------------------------------
 
 async function sendVideoRelatedCreateActivity (options: {
-  byActor: ActorModel,
-  video: VideoModel,
+  byActor: MActorLight,
+  video: MVideoAccountLight,
   url: string,
   object: any,
   transaction?: Transaction
index 6c7fb844935365e2540a1c32f922784159d9df2c..4b1ff8dc5739345aff971016c31e2010563bca02 100644 (file)
@@ -1,17 +1,17 @@
 import { Transaction } from 'sequelize'
 import { ActivityAudience, ActivityDelete } from '../../../../shared/models/activitypub'
 import { ActorModel } from '../../../models/activitypub/actor'
-import { VideoModel } from '../../../models/video/video'
 import { VideoCommentModel } from '../../../models/video/video-comment'
 import { VideoShareModel } from '../../../models/video/video-share'
 import { getDeleteActivityPubUrl } from '../url'
 import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils'
 import { audiencify, getActorsInvolvedInVideo, getVideoCommentAudience } from '../audience'
 import { logger } from '../../../helpers/logger'
-import { VideoPlaylistModel } from '../../../models/video/video-playlist'
 import { getServerActor } from '../../../helpers/utils'
+import { MCommentOwnerVideoReply, MVideoAccountLight, MVideoPlaylistFullSummary } from '../../../typings/models/video'
+import { MActorUrl } from '../../../typings/models'
 
-async function sendDeleteVideo (video: VideoModel, transaction: Transaction) {
+async function sendDeleteVideo (video: MVideoAccountLight, transaction: Transaction) {
   logger.info('Creating job to broadcast delete of video %s.', video.url)
 
   const byActor = video.VideoChannel.Account.Actor
@@ -42,7 +42,7 @@ async function sendDeleteActor (byActor: ActorModel, t: Transaction) {
   return broadcastToFollowers(activity, byActor, actorsInvolved, t)
 }
 
-async function sendDeleteVideoComment (videoComment: VideoCommentModel, t: Transaction) {
+async function sendDeleteVideoComment (videoComment: MCommentOwnerVideoReply, t: Transaction) {
   logger.info('Creating job to send delete of comment %s.', videoComment.url)
 
   const isVideoOrigin = videoComment.Video.isOwned()
@@ -74,7 +74,7 @@ async function sendDeleteVideoComment (videoComment: VideoCommentModel, t: Trans
   t.afterCommit(() => unicastTo(activity, byActor, videoComment.Video.VideoChannel.Account.Actor.sharedInboxUrl))
 }
 
-async function sendDeleteVideoPlaylist (videoPlaylist: VideoPlaylistModel, t: Transaction) {
+async function sendDeleteVideoPlaylist (videoPlaylist: MVideoPlaylistFullSummary, t: Transaction) {
   logger.info('Creating job to send delete of playlist %s.', videoPlaylist.url)
 
   const byActor = videoPlaylist.OwnerAccount.Actor
@@ -101,7 +101,7 @@ export {
 
 // ---------------------------------------------------------------------------
 
-function buildDeleteActivity (url: string, object: string, byActor: ActorModel, audience?: ActivityAudience): ActivityDelete {
+function buildDeleteActivity (url: string, object: string, byActor: MActorUrl, audience?: ActivityAudience): ActivityDelete {
   const activity = {
     type: 'Delete' as 'Delete',
     id: url,
index a88436f2c963ee061e07dbc0f531d4109212cdb6..6e41f241f74421bb080c00dce1d9065bb07ba8b1 100644 (file)
@@ -1,13 +1,12 @@
 import { Transaction } from 'sequelize'
-import { ActorModel } from '../../../models/activitypub/actor'
-import { VideoModel } from '../../../models/video/video'
 import { getVideoDislikeActivityPubUrl } from '../url'
 import { logger } from '../../../helpers/logger'
 import { ActivityAudience, ActivityDislike } from '../../../../shared/models/activitypub'
 import { sendVideoRelatedActivity } from './utils'
 import { audiencify, getAudience } from '../audience'
+import { MActor, MActorAudience, MVideoAccountLight, MVideoUrl } from '../../../typings/models'
 
-async function sendDislike (byActor: ActorModel, video: VideoModel, t: Transaction) {
+async function sendDislike (byActor: MActor, video: MVideoAccountLight, t: Transaction) {
   logger.info('Creating job to dislike %s.', video.url)
 
   const activityBuilder = (audience: ActivityAudience) => {
@@ -19,7 +18,7 @@ async function sendDislike (byActor: ActorModel, video: VideoModel, t: Transacti
   return sendVideoRelatedActivity(activityBuilder, { byActor, video, transaction: t })
 }
 
-function buildDislikeActivity (url: string, byActor: ActorModel, video: VideoModel, audience?: ActivityAudience): ActivityDislike {
+function buildDislikeActivity (url: string, byActor: MActorAudience, video: MVideoUrl, audience?: ActivityAudience): ActivityDislike {
   if (!audience) audience = getAudience(byActor)
 
   return audiencify(
index 61ee389a61157a84cee8d35ed9c1ad9f95e7f62f..5ae1614ab63d8c5f6467249bef01efc6617c91f6 100644 (file)
@@ -1,14 +1,13 @@
-import { ActorModel } from '../../../models/activitypub/actor'
-import { VideoModel } from '../../../models/video/video'
-import { VideoAbuseModel } from '../../../models/video/video-abuse'
 import { getVideoAbuseActivityPubUrl } from '../url'
 import { unicastTo } from './utils'
 import { logger } from '../../../helpers/logger'
 import { ActivityAudience, ActivityFlag } from '../../../../shared/models/activitypub'
 import { audiencify, getAudience } from '../audience'
 import { Transaction } from 'sequelize'
+import { MActor, MVideoFullLight } from '../../../typings/models'
+import { MVideoAbuseVideo } from '../../../typings/models/video'
 
-async function sendVideoAbuse (byActor: ActorModel, videoAbuse: VideoAbuseModel, video: VideoModel, t: Transaction) {
+async function sendVideoAbuse (byActor: MActor, videoAbuse: MVideoAbuseVideo, video: MVideoFullLight, t: Transaction) {
   if (!video.VideoChannel.Account.Actor.serverId) return // Local user
 
   const url = getVideoAbuseActivityPubUrl(videoAbuse)
@@ -22,7 +21,7 @@ async function sendVideoAbuse (byActor: ActorModel, videoAbuse: VideoAbuseModel,
   t.afterCommit(() => unicastTo(flagActivity, byActor, video.VideoChannel.Account.Actor.sharedInboxUrl))
 }
 
-function buildFlagActivity (url: string, byActor: ActorModel, videoAbuse: VideoAbuseModel, audience: ActivityAudience): ActivityFlag {
+function buildFlagActivity (url: string, byActor: MActor, videoAbuse: MVideoAbuseVideo, audience: ActivityAudience): ActivityFlag {
   if (!audience) audience = getAudience(byActor)
 
   const activity = Object.assign(
index a59ed50cf9d9d9d52523e19dce36cb291cb3dc56..ce400d8fffa4196412e75238a9f2bef5e964aa0c 100644 (file)
@@ -1,12 +1,11 @@
 import { ActivityFollow } from '../../../../shared/models/activitypub'
-import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
 import { getActorFollowActivityPubUrl } from '../url'
 import { unicastTo } from './utils'
 import { logger } from '../../../helpers/logger'
 import { Transaction } from 'sequelize'
-import { ActorModelOnly } from '../../../typings/models'
+import { MActor, MActorFollowActors } from '../../../typings/models'
 
-function sendFollow (actorFollow: ActorFollowModel, t: Transaction) {
+function sendFollow (actorFollow: MActorFollowActors, t: Transaction) {
   const me = actorFollow.ActorFollower
   const following = actorFollow.ActorFollowing
 
@@ -21,7 +20,7 @@ function sendFollow (actorFollow: ActorFollowModel, t: Transaction) {
   t.afterCommit(() => unicastTo(data, me, following.inboxUrl))
 }
 
-function buildFollowActivity (url: string, byActor: ActorModelOnly, targetActor: ActorModelOnly): ActivityFollow {
+function buildFollowActivity (url: string, byActor: MActor, targetActor: MActor): ActivityFollow {
   return {
     type: 'Follow',
     id: url,
index 35227887abfec4925b275e11426549cc33afa98e..e84a6f98b08112a7efd80ab7d081a365b0034a5a 100644 (file)
@@ -1,13 +1,12 @@
 import { Transaction } from 'sequelize'
 import { ActivityAudience, ActivityLike } from '../../../../shared/models/activitypub'
-import { ActorModel } from '../../../models/activitypub/actor'
-import { VideoModel } from '../../../models/video/video'
 import { getVideoLikeActivityPubUrl } from '../url'
 import { sendVideoRelatedActivity } from './utils'
 import { audiencify, getAudience } from '../audience'
 import { logger } from '../../../helpers/logger'
+import { MActor, MActorAudience, MVideoAccountLight, MVideoUrl } from '../../../typings/models'
 
-async function sendLike (byActor: ActorModel, video: VideoModel, t: Transaction) {
+async function sendLike (byActor: MActor, video: MVideoAccountLight, t: Transaction) {
   logger.info('Creating job to like %s.', video.url)
 
   const activityBuilder = (audience: ActivityAudience) => {
@@ -19,7 +18,7 @@ async function sendLike (byActor: ActorModel, video: VideoModel, t: Transaction)
   return sendVideoRelatedActivity(activityBuilder, { byActor, video, transaction: t })
 }
 
-function buildLikeActivity (url: string, byActor: ActorModel, video: VideoModel, audience?: ActivityAudience): ActivityLike {
+function buildLikeActivity (url: string, byActor: MActorAudience, video: MVideoUrl, audience?: ActivityAudience): ActivityLike {
   if (!audience) audience = getAudience(byActor)
 
   return audiencify(
index 63110b43395dbfa7e7941ad1c142d00ab5e08d38..4258a3c36335ed4ea79d31b0cea0e9b7cd233747 100644 (file)
@@ -1,12 +1,11 @@
 import { ActivityFollow, ActivityReject } from '../../../../shared/models/activitypub'
-import { ActorModel } from '../../../models/activitypub/actor'
 import { getActorFollowActivityPubUrl, getActorFollowRejectActivityPubUrl } from '../url'
 import { unicastTo } from './utils'
 import { buildFollowActivity } from './send-follow'
 import { logger } from '../../../helpers/logger'
-import { SignatureActorModel } from '../../../typings/models'
+import { MActor } from '../../../typings/models'
 
-async function sendReject (follower: SignatureActorModel, following: ActorModel) {
+async function sendReject (follower: MActor, following: MActor) {
   if (!follower.serverId) { // This should never happen
     logger.warn('Do not sending reject to local follower.')
     return
@@ -31,7 +30,7 @@ export {
 
 // ---------------------------------------------------------------------------
 
-function buildRejectActivity (url: string, byActor: ActorModel, followActivityData: ActivityFollow): ActivityReject {
+function buildRejectActivity (url: string, byActor: MActor, followActivityData: ActivityFollow): ActivityReject {
   return {
     type: 'Reject',
     id: url,
index 8fcbbac5c95a94a0dfa35e96d38f4607a078c9a4..e9ab5b3c59acaee8da7edce9fa7b2436313f4060 100644 (file)
@@ -2,13 +2,12 @@ import { Transaction } from 'sequelize'
 import {
   ActivityAnnounce,
   ActivityAudience,
-  ActivityCreate, ActivityDislike,
+  ActivityCreate,
+  ActivityDislike,
   ActivityFollow,
   ActivityLike,
   ActivityUndo
 } from '../../../../shared/models/activitypub'
-import { ActorModel } from '../../../models/activitypub/actor'
-import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
 import { VideoModel } from '../../../models/video/video'
 import { getActorFollowActivityPubUrl, getUndoActivityPubUrl, getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from '../url'
 import { broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils'
@@ -16,13 +15,20 @@ import { audiencify, getAudience } from '../audience'
 import { buildCreateActivity } from './send-create'
 import { buildFollowActivity } from './send-follow'
 import { buildLikeActivity } from './send-like'
-import { VideoShareModel } from '../../../models/video/video-share'
 import { buildAnnounceWithVideoAudience } from './send-announce'
 import { logger } from '../../../helpers/logger'
-import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy'
 import { buildDislikeActivity } from './send-dislike'
-
-async function sendUndoFollow (actorFollow: ActorFollowModel, t: Transaction) {
+import {
+  MActor, MActorAudience,
+  MActorFollowActors,
+  MActorLight,
+  MVideo,
+  MVideoAccountLight,
+  MVideoRedundancyVideo,
+  MVideoShare
+} from '../../../typings/models'
+
+async function sendUndoFollow (actorFollow: MActorFollowActors, t: Transaction) {
   const me = actorFollow.ActorFollower
   const following = actorFollow.ActorFollowing
 
@@ -40,7 +46,7 @@ async function sendUndoFollow (actorFollow: ActorFollowModel, t: Transaction) {
   t.afterCommit(() => unicastTo(undoActivity, me, following.inboxUrl))
 }
 
-async function sendUndoAnnounce (byActor: ActorModel, videoShare: VideoShareModel, video: VideoModel, t: Transaction) {
+async function sendUndoAnnounce (byActor: MActorLight, videoShare: MVideoShare, video: MVideo, t: Transaction) {
   logger.info('Creating job to undo announce %s.', videoShare.url)
 
   const undoUrl = getUndoActivityPubUrl(videoShare.url)
@@ -52,7 +58,7 @@ async function sendUndoAnnounce (byActor: ActorModel, videoShare: VideoShareMode
   return broadcastToFollowers(undoActivity, byActor, actorsInvolvedInVideo, t, followersException)
 }
 
-async function sendUndoLike (byActor: ActorModel, video: VideoModel, t: Transaction) {
+async function sendUndoLike (byActor: MActor, video: MVideoAccountLight, t: Transaction) {
   logger.info('Creating job to undo a like of video %s.', video.url)
 
   const likeUrl = getVideoLikeActivityPubUrl(byActor, video)
@@ -61,7 +67,7 @@ async function sendUndoLike (byActor: ActorModel, video: VideoModel, t: Transact
   return sendUndoVideoRelatedActivity({ byActor, video, url: likeUrl, activity: likeActivity, transaction: t })
 }
 
-async function sendUndoDislike (byActor: ActorModel, video: VideoModel, t: Transaction) {
+async function sendUndoDislike (byActor: MActor, video: MVideoAccountLight, t: Transaction) {
   logger.info('Creating job to undo a dislike of video %s.', video.url)
 
   const dislikeUrl = getVideoDislikeActivityPubUrl(byActor, video)
@@ -70,7 +76,7 @@ async function sendUndoDislike (byActor: ActorModel, video: VideoModel, t: Trans
   return sendUndoVideoRelatedActivity({ byActor, video, url: dislikeUrl, activity: dislikeActivity, transaction: t })
 }
 
-async function sendUndoCacheFile (byActor: ActorModel, redundancyModel: VideoRedundancyModel, t: Transaction) {
+async function sendUndoCacheFile (byActor: MActor, redundancyModel: MVideoRedundancyVideo, t: Transaction) {
   logger.info('Creating job to undo cache file %s.', redundancyModel.url)
 
   const videoId = redundancyModel.getVideo().id
@@ -94,7 +100,7 @@ export {
 
 function undoActivityData (
   url: string,
-  byActor: ActorModel,
+  byActor: MActorAudience,
   object: ActivityFollow | ActivityLike | ActivityDislike | ActivityCreate | ActivityAnnounce,
   audience?: ActivityAudience
 ): ActivityUndo {
@@ -112,8 +118,8 @@ function undoActivityData (
 }
 
 async function sendUndoVideoRelatedActivity (options: {
-  byActor: ActorModel,
-  video: VideoModel,
+  byActor: MActor,
+  video: MVideoAccountLight,
   url: string,
   activity: ActivityFollow | ActivityLike | ActivityDislike | ActivityCreate | ActivityAnnounce,
   transaction: Transaction
index 5bf092894eba54a65b5d6548712f018f511b444b..37517c2be5cfa3332929d91cac00dd10c916e3b4 100644 (file)
@@ -2,21 +2,29 @@ import { Transaction } from 'sequelize'
 import { ActivityAudience, ActivityUpdate } from '../../../../shared/models/activitypub'
 import { VideoPrivacy } from '../../../../shared/models/videos'
 import { AccountModel } from '../../../models/account/account'
-import { ActorModel } from '../../../models/activitypub/actor'
 import { VideoModel } from '../../../models/video/video'
-import { VideoChannelModel } from '../../../models/video/video-channel'
 import { VideoShareModel } from '../../../models/video/video-share'
 import { getUpdateActivityPubUrl } from '../url'
 import { broadcastToFollowers, sendVideoRelatedActivity } from './utils'
 import { audiencify, getActorsInvolvedInVideo, getAudience } from '../audience'
 import { logger } from '../../../helpers/logger'
 import { VideoCaptionModel } from '../../../models/video/video-caption'
-import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy'
-import { VideoPlaylistModel } from '../../../models/video/video-playlist'
 import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
 import { getServerActor } from '../../../helpers/utils'
+import {
+  MAccountDefault,
+  MActor,
+  MActorLight,
+  MChannelDefault,
+  MVideoAP,
+  MVideoAPWithoutCaption,
+  MVideoPlaylistFull,
+  MVideoRedundancyVideo
+} from '../../../typings/models'
+
+async function sendUpdateVideo (videoArg: MVideoAPWithoutCaption, t: Transaction, overrodeByActor?: MActor) {
+  const video = videoArg as MVideoAP
 
-async function sendUpdateVideo (video: VideoModel, t: Transaction, overrodeByActor?: ActorModel) {
   if (video.privacy === VideoPrivacy.PRIVATE) return undefined
 
   logger.info('Creating job to update video %s.', video.url)
@@ -41,7 +49,7 @@ async function sendUpdateVideo (video: VideoModel, t: Transaction, overrodeByAct
   return broadcastToFollowers(updateActivity, byActor, actorsInvolved, t)
 }
 
-async function sendUpdateActor (accountOrChannel: AccountModel | VideoChannelModel, t: Transaction) {
+async function sendUpdateActor (accountOrChannel: MChannelDefault | MAccountDefault, t: Transaction) {
   const byActor = accountOrChannel.Actor
 
   logger.info('Creating job to update actor %s.', byActor.url)
@@ -51,7 +59,7 @@ async function sendUpdateActor (accountOrChannel: AccountModel | VideoChannelMod
   const audience = getAudience(byActor)
   const updateActivity = buildUpdateActivity(url, byActor, accountOrChannelObject, audience)
 
-  let actorsInvolved: ActorModel[]
+  let actorsInvolved: MActor[]
   if (accountOrChannel instanceof AccountModel) {
     // Actors that shared my videos are involved too
     actorsInvolved = await VideoShareModel.loadActorsWhoSharedVideosOf(byActor.id, t)
@@ -65,7 +73,7 @@ async function sendUpdateActor (accountOrChannel: AccountModel | VideoChannelMod
   return broadcastToFollowers(updateActivity, byActor, actorsInvolved, t)
 }
 
-async function sendUpdateCacheFile (byActor: ActorModel, redundancyModel: VideoRedundancyModel) {
+async function sendUpdateCacheFile (byActor: MActorLight, redundancyModel: MVideoRedundancyVideo) {
   logger.info('Creating job to update cache file %s.', redundancyModel.url)
 
   const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(redundancyModel.getVideo().id)
@@ -80,7 +88,7 @@ async function sendUpdateCacheFile (byActor: ActorModel, redundancyModel: VideoR
   return sendVideoRelatedActivity(activityBuilder, { byActor, video })
 }
 
-async function sendUpdateVideoPlaylist (videoPlaylist: VideoPlaylistModel, t: Transaction) {
+async function sendUpdateVideoPlaylist (videoPlaylist: MVideoPlaylistFull, t: Transaction) {
   if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) return undefined
 
   const byActor = videoPlaylist.OwnerAccount.Actor
@@ -113,7 +121,7 @@ export {
 
 // ---------------------------------------------------------------------------
 
-function buildUpdateActivity (url: string, byActor: ActorModel, object: any, audience?: ActivityAudience): ActivityUpdate {
+function buildUpdateActivity (url: string, byActor: MActorLight, object: any, audience?: ActivityAudience): ActivityUpdate {
   if (!audience) audience = getAudience(byActor)
 
   return audiencify(
@@ -121,8 +129,7 @@ function buildUpdateActivity (url: string, byActor: ActorModel, object: any, aud
       type: 'Update' as 'Update',
       id: url,
       actor: byActor.url,
-      object: audiencify(object, audience
-      )
+      object: audiencify(object, audience)
     },
     audience
   )
index 8ad126be05f2eba0cf7ebbe02cec62fba0152bda..8809417f9b16bcf76df88ec81b0f75cd30ccc1eb 100644 (file)
@@ -1,13 +1,13 @@
 import { Transaction } from 'sequelize'
 import { ActivityAudience, ActivityView } from '../../../../shared/models/activitypub'
 import { ActorModel } from '../../../models/activitypub/actor'
-import { VideoModel } from '../../../models/video/video'
 import { getVideoLikeActivityPubUrl } from '../url'
 import { sendVideoRelatedActivity } from './utils'
 import { audiencify, getAudience } from '../audience'
 import { logger } from '../../../helpers/logger'
+import { MActorAudience, MVideoAccountLight, MVideoUrl } from '@server/typings/models'
 
-async function sendView (byActor: ActorModel, video: VideoModel, t: Transaction) {
+async function sendView (byActor: ActorModel, video: MVideoAccountLight, t: Transaction) {
   logger.info('Creating job to send view of %s.', video.url)
 
   const activityBuilder = (audience: ActivityAudience) => {
@@ -19,7 +19,7 @@ async function sendView (byActor: ActorModel, video: VideoModel, t: Transaction)
   return sendVideoRelatedActivity(activityBuilder, { byActor, video, transaction: t })
 }
 
-function buildViewActivity (url: string, byActor: ActorModel, video: VideoModel, audience?: ActivityAudience): ActivityView {
+function buildViewActivity (url: string, byActor: MActorAudience, video: MVideoUrl, audience?: ActivityAudience): ActivityView {
   if (!audience) audience = getAudience(byActor)
 
   return audiencify(
index 4f69afb00098efd3683916ebd7691615f032112c..8129ab32a8f617310b5b0ffcebd3722e1e6d410d 100644 (file)
@@ -4,15 +4,14 @@ import { logger } from '../../../helpers/logger'
 import { ActorModel } from '../../../models/activitypub/actor'
 import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
 import { JobQueue } from '../../job-queue'
-import { VideoModel } from '../../../models/video/video'
 import { getActorsInvolvedInVideo, getAudienceFromFollowersOf, getRemoteVideoAudience } from '../audience'
 import { getServerActor } from '../../../helpers/utils'
 import { afterCommitIfTransaction } from '../../../helpers/database-utils'
-import { ActorFollowerException, ActorModelId, ActorModelOnly } from '../../../typings/models'
+import { MActorFollowerException, MActor, MActorId, MActorLight, MVideo, MVideoAccountLight } from '../../../typings/models'
 
 async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAudience) => Activity, options: {
-  byActor: ActorModelOnly,
-  video: VideoModel,
+  byActor: MActorLight,
+  video: MVideoAccountLight,
   transaction?: Transaction
 }) {
   const { byActor, video, transaction } = options
@@ -41,8 +40,8 @@ async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAud
 async function forwardVideoRelatedActivity (
   activity: Activity,
   t: Transaction,
-  followersException: ActorFollowerException[] = [],
-  video: VideoModel
+  followersException: MActorFollowerException[] = [],
+  video: MVideo
 ) {
   // Mastodon does not add our announces in audience, so we forward to them manually
   const additionalActors = await getActorsInvolvedInVideo(video, t)
@@ -54,7 +53,7 @@ async function forwardVideoRelatedActivity (
 async function forwardActivity (
   activity: Activity,
   t: Transaction,
-  followersException: ActorFollowerException[] = [],
+  followersException: MActorFollowerException[] = [],
   additionalFollowerUrls: string[] = []
 ) {
   logger.info('Forwarding activity %s.', activity.id)
@@ -88,10 +87,10 @@ async function forwardActivity (
 
 async function broadcastToFollowers (
   data: any,
-  byActor: ActorModelId,
-  toFollowersOf: ActorModelId[],
+  byActor: MActorId,
+  toFollowersOf: MActorId[],
   t: Transaction,
-  actorsException: ActorFollowerException[] = []
+  actorsException: MActorFollowerException[] = []
 ) {
   const uris = await computeFollowerUris(toFollowersOf, actorsException, t)
 
@@ -100,16 +99,16 @@ async function broadcastToFollowers (
 
 async function broadcastToActors (
   data: any,
-  byActor: ActorModelId,
-  toActors: ActorModelOnly[],
+  byActor: MActorId,
+  toActors: MActor[],
   t?: Transaction,
-  actorsException: ActorFollowerException[] = []
+  actorsException: MActorFollowerException[] = []
 ) {
   const uris = await computeUris(toActors, actorsException)
   return afterCommitIfTransaction(t, () => broadcastTo(uris, data, byActor))
 }
 
-function broadcastTo (uris: string[], data: any, byActor: ActorModelId) {
+function broadcastTo (uris: string[], data: any, byActor: MActorId) {
   if (uris.length === 0) return undefined
 
   logger.debug('Creating broadcast job.', { uris })
@@ -123,7 +122,7 @@ function broadcastTo (uris: string[], data: any, byActor: ActorModelId) {
   return JobQueue.Instance.createJob({ type: 'activitypub-http-broadcast', payload })
 }
 
-function unicastTo (data: any, byActor: ActorModelId, toActorUrl: string) {
+function unicastTo (data: any, byActor: MActorId, toActorUrl: string) {
   logger.debug('Creating unicast job.', { uri: toActorUrl })
 
   const payload = {
@@ -148,7 +147,7 @@ export {
 
 // ---------------------------------------------------------------------------
 
-async function computeFollowerUris (toFollowersOf: ActorModelId[], actorsException: ActorFollowerException[], t: Transaction) {
+async function computeFollowerUris (toFollowersOf: MActorId[], actorsException: MActorFollowerException[], t: Transaction) {
   const toActorFollowerIds = toFollowersOf.map(a => a.id)
 
   const result = await ActorFollowModel.listAcceptedFollowerSharedInboxUrls(toActorFollowerIds, t)
@@ -157,7 +156,7 @@ async function computeFollowerUris (toFollowersOf: ActorModelId[], actorsExcepti
   return result.data.filter(sharedInbox => sharedInboxesException.indexOf(sharedInbox) === -1)
 }
 
-async function computeUris (toActors: ActorModelOnly[], actorsException: ActorFollowerException[] = []) {
+async function computeUris (toActors: MActor[], actorsException: MActorFollowerException[] = []) {
   const serverActor = await getServerActor()
   const targetUrls = toActors
     .filter(a => a.id !== serverActor.id) // Don't send to ourselves
@@ -170,7 +169,7 @@ async function computeUris (toActors: ActorModelOnly[], actorsException: ActorFo
               .filter(sharedInbox => sharedInboxesException.indexOf(sharedInbox) === -1)
 }
 
-async function buildSharedInboxesException (actorsException: ActorFollowerException[]) {
+async function buildSharedInboxesException (actorsException: MActorFollowerException[]) {
   const serverActor = await getServerActor()
 
   return actorsException
index 7f38402b666debaeedd7a519212571e6a7d8518a..fdca9bed75c3e14c487f1af3164f0544cce99cac 100644 (file)
@@ -1,19 +1,18 @@
 import { Transaction } from 'sequelize'
 import { VideoPrivacy } from '../../../shared/models/videos'
 import { getServerActor } from '../../helpers/utils'
-import { VideoModel } from '../../models/video/video'
 import { VideoShareModel } from '../../models/video/video-share'
 import { sendUndoAnnounce, sendVideoAnnounce } from './send'
 import { getVideoAnnounceActivityPubUrl } from './url'
-import { VideoChannelModel } from '../../models/video/video-channel'
 import * as Bluebird from 'bluebird'
 import { doRequest } from '../../helpers/requests'
 import { getOrCreateActorAndServerAndModel } from './actor'
 import { logger } from '../../helpers/logger'
 import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
 import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
+import { MChannelActor, MChannelActorLight, MVideo, MVideoAccountLight, MVideoId } from '../../typings/models/video'
 
-async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction) {
+async function shareVideoByServerAndChannel (video: MVideoAccountLight, t: Transaction) {
   if (video.privacy === VideoPrivacy.PRIVATE) return undefined
 
   return Promise.all([
@@ -22,7 +21,11 @@ async function shareVideoByServerAndChannel (video: VideoModel, t: Transaction)
   ])
 }
 
-async function changeVideoChannelShare (video: VideoModel, oldVideoChannel: VideoChannelModel, t: Transaction) {
+async function changeVideoChannelShare (
+  video: MVideoAccountLight,
+  oldVideoChannel: MChannelActorLight,
+  t: Transaction
+) {
   logger.info('Updating video channel of video %s: %s -> %s.', video.uuid, oldVideoChannel.name, video.VideoChannel.name)
 
   await undoShareByVideoChannel(video, oldVideoChannel, t)
@@ -30,7 +33,7 @@ async function changeVideoChannelShare (video: VideoModel, oldVideoChannel: Vide
   await shareByVideoChannel(video, t)
 }
 
-async function addVideoShares (shareUrls: string[], instance: VideoModel) {
+async function addVideoShares (shareUrls: string[], video: MVideoId) {
   await Bluebird.map(shareUrls, async shareUrl => {
     try {
       // Fetch url
@@ -50,7 +53,7 @@ async function addVideoShares (shareUrls: string[], instance: VideoModel) {
 
       const entry = {
         actorId: actor.id,
-        videoId: instance.id,
+        videoId: video.id,
         url: shareUrl
       }
 
@@ -69,7 +72,7 @@ export {
 
 // ---------------------------------------------------------------------------
 
-async function shareByServer (video: VideoModel, t: Transaction) {
+async function shareByServer (video: MVideo, t: Transaction) {
   const serverActor = await getServerActor()
 
   const serverShareUrl = getVideoAnnounceActivityPubUrl(serverActor, video)
@@ -88,7 +91,7 @@ async function shareByServer (video: VideoModel, t: Transaction) {
   return sendVideoAnnounce(serverActor, serverShare, video, t)
 }
 
-async function shareByVideoChannel (video: VideoModel, t: Transaction) {
+async function shareByVideoChannel (video: MVideoAccountLight, t: Transaction) {
   const videoChannelShareUrl = getVideoAnnounceActivityPubUrl(video.VideoChannel.Actor, video)
   const [ videoChannelShare ] = await VideoShareModel.findOrCreate({
     defaults: {
@@ -105,7 +108,7 @@ async function shareByVideoChannel (video: VideoModel, t: Transaction) {
   return sendVideoAnnounce(video.VideoChannel.Actor, videoChannelShare, video, t)
 }
 
-async function undoShareByVideoChannel (video: VideoModel, oldVideoChannel: VideoChannelModel, t: Transaction) {
+async function undoShareByVideoChannel (video: MVideo, oldVideoChannel: MChannelActorLight, t: Transaction) {
   // Load old share
   const oldShare = await VideoShareModel.load(oldVideoChannel.actorId, video.id, t)
   if (!oldShare) return new Error('Cannot find old video channel share ' + oldVideoChannel.actorId + ' for video ' + video.id)
index dfcb3c6688192c769b59dfc8cd88fcb3c641ecf0..6290af34ba91c5ecb399c07b6b521b05cefc6a98 100644 (file)
@@ -1,36 +1,42 @@
 import { WEBSERVER } from '../../initializers/constants'
-import { VideoModel } from '../../models/video/video'
-import { VideoAbuseModel } from '../../models/video/video-abuse'
-import { VideoCommentModel } from '../../models/video/video-comment'
-import { VideoFileModel } from '../../models/video/video-file'
-import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist'
-import { VideoPlaylistModel } from '../../models/video/video-playlist'
-import { ActorModelOnly, ActorModelUrl } from '../../typings/models'
-import { ActorFollowModelLight } from '../../typings/models/actor-follow'
-
-function getVideoActivityPubUrl (video: VideoModel) {
+import {
+  MActor,
+  MActorFollowActors,
+  MActorId,
+  MActorUrl,
+  MCommentId,
+  MVideoAbuseId,
+  MVideoId,
+  MVideoUrl,
+  MVideoUUID
+} from '../../typings/models'
+import { MVideoPlaylist, MVideoPlaylistUUID } from '../../typings/models/video/video-playlist'
+import { MVideoFileVideoUUID } from '../../typings/models/video/video-file'
+import { MStreamingPlaylist } from '../../typings/models/video/video-streaming-playlist'
+
+function getVideoActivityPubUrl (video: MVideoUUID) {
   return WEBSERVER.URL + '/videos/watch/' + video.uuid
 }
 
-function getVideoPlaylistActivityPubUrl (videoPlaylist: VideoPlaylistModel) {
+function getVideoPlaylistActivityPubUrl (videoPlaylist: MVideoPlaylist) {
   return WEBSERVER.URL + '/video-playlists/' + videoPlaylist.uuid
 }
 
-function getVideoPlaylistElementActivityPubUrl (videoPlaylist: VideoPlaylistModel, video: VideoModel) {
+function getVideoPlaylistElementActivityPubUrl (videoPlaylist: MVideoPlaylistUUID, video: MVideoUUID) {
   return WEBSERVER.URL + '/video-playlists/' + videoPlaylist.uuid + '/' + video.uuid
 }
 
-function getVideoCacheFileActivityPubUrl (videoFile: VideoFileModel) {
+function getVideoCacheFileActivityPubUrl (videoFile: MVideoFileVideoUUID) {
   const suffixFPS = videoFile.fps && videoFile.fps !== -1 ? '-' + videoFile.fps : ''
 
   return `${WEBSERVER.URL}/redundancy/videos/${videoFile.Video.uuid}/${videoFile.resolution}${suffixFPS}`
 }
 
-function getVideoCacheStreamingPlaylistActivityPubUrl (video: VideoModel, playlist: VideoStreamingPlaylistModel) {
+function getVideoCacheStreamingPlaylistActivityPubUrl (video: MVideoUUID, playlist: MStreamingPlaylist) {
   return `${WEBSERVER.URL}/redundancy/streaming-playlists/${playlist.getStringType()}/${video.uuid}`
 }
 
-function getVideoCommentActivityPubUrl (video: VideoModel, videoComment: VideoCommentModel) {
+function getVideoCommentActivityPubUrl (video: MVideoUUID, videoComment: MCommentId) {
   return WEBSERVER.URL + '/videos/watch/' + video.uuid + '/comments/' + videoComment.id
 }
 
@@ -42,54 +48,54 @@ function getAccountActivityPubUrl (accountName: string) {
   return WEBSERVER.URL + '/accounts/' + accountName
 }
 
-function getVideoAbuseActivityPubUrl (videoAbuse: VideoAbuseModel) {
+function getVideoAbuseActivityPubUrl (videoAbuse: MVideoAbuseId) {
   return WEBSERVER.URL + '/admin/video-abuses/' + videoAbuse.id
 }
 
-function getVideoViewActivityPubUrl (byActor: ActorModelUrl, video: VideoModel) {
+function getVideoViewActivityPubUrl (byActor: MActorUrl, video: MVideoId) {
   return byActor.url + '/views/videos/' + video.id + '/' + new Date().toISOString()
 }
 
-function getVideoLikeActivityPubUrl (byActor: ActorModelUrl, video: VideoModel | { id: number }) {
+function getVideoLikeActivityPubUrl (byActor: MActorUrl, video: MVideoId) {
   return byActor.url + '/likes/' + video.id
 }
 
-function getVideoDislikeActivityPubUrl (byActor: ActorModelUrl, video: VideoModel | { id: number }) {
+function getVideoDislikeActivityPubUrl (byActor: MActorUrl, video: MVideoId) {
   return byActor.url + '/dislikes/' + video.id
 }
 
-function getVideoSharesActivityPubUrl (video: VideoModel) {
+function getVideoSharesActivityPubUrl (video: MVideoUrl) {
   return video.url + '/announces'
 }
 
-function getVideoCommentsActivityPubUrl (video: VideoModel) {
+function getVideoCommentsActivityPubUrl (video: MVideoUrl) {
   return video.url + '/comments'
 }
 
-function getVideoLikesActivityPubUrl (video: VideoModel) {
+function getVideoLikesActivityPubUrl (video: MVideoUrl) {
   return video.url + '/likes'
 }
 
-function getVideoDislikesActivityPubUrl (video: VideoModel) {
+function getVideoDislikesActivityPubUrl (video: MVideoUrl) {
   return video.url + '/dislikes'
 }
 
-function getActorFollowActivityPubUrl (follower: ActorModelOnly, following: ActorModelOnly) {
+function getActorFollowActivityPubUrl (follower: MActor, following: MActorId) {
   return follower.url + '/follows/' + following.id
 }
 
-function getActorFollowAcceptActivityPubUrl (actorFollow: ActorFollowModelLight) {
+function getActorFollowAcceptActivityPubUrl (actorFollow: MActorFollowActors) {
   const follower = actorFollow.ActorFollower
   const me = actorFollow.ActorFollowing
 
   return follower.url + '/accepts/follows/' + me.id
 }
 
-function getActorFollowRejectActivityPubUrl (follower: ActorModelOnly, following: ActorModelOnly) {
+function getActorFollowRejectActivityPubUrl (follower: MActorUrl, following: MActorId) {
   return follower.url + '/rejects/follows/' + following.id
 }
 
-function getVideoAnnounceActivityPubUrl (byActor: ActorModelOnly, video: VideoModel) {
+function getVideoAnnounceActivityPubUrl (byActor: MActorId, video: MVideoUrl) {
   return video.url + '/announces/' + byActor.id
 }
 
index 8d2c1ade318b8e32bb439e3433f422b8e67debd5..3e8306fa4c6ecf117adbc477e410b6f3bbc68d4c 100644 (file)
@@ -2,20 +2,20 @@ import { sanitizeAndCheckVideoCommentObject } from '../../helpers/custom-validat
 import { logger } from '../../helpers/logger'
 import { doRequest } from '../../helpers/requests'
 import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
-import { VideoModel } from '../../models/video/video'
 import { VideoCommentModel } from '../../models/video/video-comment'
 import { getOrCreateActorAndServerAndModel } from './actor'
 import { getOrCreateVideoAndAccountAndChannel } from './videos'
 import * as Bluebird from 'bluebird'
 import { checkUrlsSameHost } from '../../helpers/activitypub'
+import { MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../typings/models/video'
 
 type ResolveThreadParams = {
   url: string,
-  comments?: VideoCommentModel[],
+  comments?: MCommentOwner[],
   isVideo?: boolean,
   commentCreated?: boolean
 }
-type ResolveThreadResult = Promise<{ video: VideoModel, comment: VideoCommentModel, commentCreated: boolean }>
+type ResolveThreadResult = Promise<{ video: MVideoAccountLightBlacklistAllFiles, comment: MCommentOwnerVideo, commentCreated: boolean }>
 
 async function addVideoComments (commentUrls: string[]) {
   return Bluebird.map(commentUrls, commentUrl => {
@@ -85,9 +85,9 @@ async function tryResolveThreadFromVideo (params: ResolveThreadParams) {
   const syncParam = { likes: true, dislikes: true, shares: true, comments: false, thumbnail: true, refreshVideo: false }
   const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: url, syncParam })
 
-  let resultComment: VideoCommentModel
+  let resultComment: MCommentOwnerVideo
   if (comments.length !== 0) {
-    const firstReply = comments[ comments.length - 1 ]
+    const firstReply = comments[ comments.length - 1 ] as MCommentOwnerVideo
     firstReply.inReplyToCommentId = null
     firstReply.originCommentId = null
     firstReply.videoId = video.id
@@ -97,7 +97,7 @@ async function tryResolveThreadFromVideo (params: ResolveThreadParams) {
     comments[comments.length - 1] = await firstReply.save()
 
     for (let i = comments.length - 2; i >= 0; i--) {
-      const comment = comments[ i ]
+      const comment = comments[ i ] as MCommentOwnerVideo
       comment.originCommentId = firstReply.id
       comment.inReplyToCommentId = comments[ i + 1 ].id
       comment.videoId = video.id
@@ -107,7 +107,7 @@ async function tryResolveThreadFromVideo (params: ResolveThreadParams) {
       comments[i] = await comment.save()
     }
 
-    resultComment = comments[0]
+    resultComment = comments[0] as MCommentOwnerVideo
   }
 
   return { video, comment: resultComment, commentCreated }
@@ -151,7 +151,7 @@ async function resolveParentComment (params: ResolveThreadParams) {
     originCommentId: null,
     createdAt: new Date(body.published),
     updatedAt: new Date(body.updated)
-  })
+  }) as MCommentOwner
   comment.Account = actor.Account
 
   return resolveThread({
index cda5b2981da1d523d7f3b249f40f3ff03abaf580..6bd46bb585654582bdfde6121f64f75139b27ef7 100644 (file)
@@ -1,6 +1,4 @@
 import { Transaction } from 'sequelize'
-import { AccountModel } from '../../models/account/account'
-import { VideoModel } from '../../models/video/video'
 import { sendLike, sendUndoDislike, sendUndoLike } from './send'
 import { VideoRateType } from '../../../shared/models/videos'
 import * as Bluebird from 'bluebird'
@@ -10,11 +8,11 @@ import { logger } from '../../helpers/logger'
 import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
 import { doRequest } from '../../helpers/requests'
 import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
-import { ActorModel } from '../../models/activitypub/actor'
 import { getVideoDislikeActivityPubUrl, getVideoLikeActivityPubUrl } from './url'
 import { sendDislike } from './send/send-dislike'
+import { MAccountActor, MActorUrl, MVideo, MVideoAccountLight, MVideoId } from '../../typings/models'
 
-async function createRates (ratesUrl: string[], video: VideoModel, rate: VideoRateType) {
+async function createRates (ratesUrl: string[], video: MVideo, rate: VideoRateType) {
   let rateCounts = 0
 
   await Bluebird.map(ratesUrl, async rateUrl => {
@@ -64,11 +62,13 @@ async function createRates (ratesUrl: string[], video: VideoModel, rate: VideoRa
   return
 }
 
-async function sendVideoRateChange (account: AccountModel,
-                              video: VideoModel,
-                              likes: number,
-                              dislikes: number,
-                              t: Transaction) {
+async function sendVideoRateChange (
+  account: MAccountActor,
+  video: MVideoAccountLight,
+  likes: number,
+  dislikes: number,
+  t: Transaction
+) {
   const actor = account.Actor
 
   // Keep the order: first we undo and then we create
@@ -84,8 +84,10 @@ async function sendVideoRateChange (account: AccountModel,
   if (dislikes > 0) await sendDislike(actor, video, t)
 }
 
-function getRateUrl (rateType: VideoRateType, actor: ActorModel, video: VideoModel) {
-  return rateType === 'like' ? getVideoLikeActivityPubUrl(actor, video) : getVideoDislikeActivityPubUrl(actor, video)
+function getRateUrl (rateType: VideoRateType, actor: MActorUrl, video: MVideoId) {
+  return rateType === 'like'
+    ? getVideoLikeActivityPubUrl(actor, video)
+    : getVideoDislikeActivityPubUrl(actor, video)
 }
 
 export {
index 3a8451a326116c91c78bfcf04a40dc4d2efdebc9..c318978fd4fbeb4310b8eacff032e5ffa76ef062 100644 (file)
@@ -24,7 +24,6 @@ import {
   REMOTE_SCHEME,
   STATIC_PATHS
 } from '../../initializers/constants'
-import { ActorModel } from '../../models/activitypub/actor'
 import { TagModel } from '../../models/video/tag'
 import { VideoModel } from '../../models/video/video'
 import { VideoFileModel } from '../../models/video/video-file'
@@ -38,7 +37,6 @@ import { JobQueue } from '../job-queue'
 import { ActivitypubHttpFetcherPayload } from '../job-queue/handlers/activitypub-http-fetcher'
 import { createRates } from './video-rates'
 import { addVideoShares, shareVideoByServerAndChannel } from './share'
-import { AccountModel } from '../../models/account/account'
 import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video'
 import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
 import { Notifier } from '../notifier'
@@ -49,15 +47,31 @@ import { VideoShareModel } from '../../models/video/video-share'
 import { VideoCommentModel } from '../../models/video/video-comment'
 import { sequelizeTypescript } from '../../initializers/database'
 import { createPlaceholderThumbnail, createVideoMiniatureFromUrl } from '../thumbnail'
-import { ThumbnailModel } from '../../models/video/thumbnail'
 import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
 import { join } from 'path'
 import { FilteredModelAttributes } from '../../typings/sequelize'
 import { autoBlacklistVideoIfNeeded } from '../video-blacklist'
 import { ActorFollowScoreCache } from '../files-cache'
-import { AccountModelIdActor, VideoChannelModelId, VideoChannelModelIdActor } from '../../typings/models'
+import {
+  MAccountIdActor,
+  MChannelAccountLight,
+  MChannelDefault,
+  MChannelId,
+  MVideo,
+  MVideoAccountLight,
+  MVideoAccountLightBlacklistAllFiles,
+  MVideoAP,
+  MVideoAPWithoutCaption,
+  MVideoFile,
+  MVideoFullLight,
+  MVideoId,
+  MVideoThumbnail
+} from '../../typings/models'
+import { MThumbnail } from '../../typings/models/video/thumbnail'
+
+async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVideo: boolean, transaction?: sequelize.Transaction) {
+  const video = videoArg as MVideoAP
 
-async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) {
   if (
     // Check this is not a blacklisted video, or unfederated blacklisted video
     (video.isBlacklisted() === false || (isNewVideo === false && video.VideoBlacklist.unfederated === false)) &&
@@ -102,7 +116,7 @@ async function fetchRemoteVideo (videoUrl: string): Promise<{ response: request.
   return { response, videoObject: body }
 }
 
-async function fetchRemoteVideoDescription (video: VideoModel) {
+async function fetchRemoteVideoDescription (video: MVideoAccountLight) {
   const host = video.VideoChannel.Account.Actor.Server.host
   const path = video.getDescriptionAPIPath()
   const options = {
@@ -114,14 +128,14 @@ async function fetchRemoteVideoDescription (video: VideoModel) {
   return body.description ? body.description : ''
 }
 
-function fetchRemoteVideoStaticFile (video: VideoModel, path: string, destPath: string) {
+function fetchRemoteVideoStaticFile (video: MVideoAccountLight, path: string, destPath: string) {
   const url = buildRemoteBaseUrl(video, path)
 
   // We need to provide a callback, if no we could have an uncaught exception
   return doRequestAndSaveToFile({ uri: url }, destPath)
 }
 
-function buildRemoteBaseUrl (video: VideoModel, path: string) {
+function buildRemoteBaseUrl (video: MVideoAccountLight, path: string) {
   const host = video.VideoChannel.Account.Actor.Server.host
 
   return REMOTE_SCHEME.HTTP + '://' + host + path
@@ -146,7 +160,7 @@ type SyncParam = {
   thumbnail: boolean
   refreshVideo?: boolean
 }
-async function syncVideoExternalAttributes (video: VideoModel, fetchedVideo: VideoTorrentObject, syncParam: SyncParam) {
+async function syncVideoExternalAttributes (video: MVideo, fetchedVideo: VideoTorrentObject, syncParam: SyncParam) {
   logger.info('Adding likes/dislikes/shares/comments of video %s.', video.uuid)
 
   const jobPayloads: ActivitypubHttpFetcherPayload[] = []
@@ -194,12 +208,24 @@ async function syncVideoExternalAttributes (video: VideoModel, fetchedVideo: Vid
   await Bluebird.map(jobPayloads, payload => JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload }))
 }
 
+function getOrCreateVideoAndAccountAndChannel (options: {
+  videoObject: { id: string } | string,
+  syncParam?: SyncParam,
+  fetchType?: 'all',
+  allowRefresh?: boolean
+}): Promise<{ video: MVideoAccountLightBlacklistAllFiles, created: boolean, autoBlacklisted?: boolean }>
+function getOrCreateVideoAndAccountAndChannel (options: {
+  videoObject: { id: string } | string,
+  syncParam?: SyncParam,
+  fetchType?: VideoFetchByUrlType,
+  allowRefresh?: boolean
+}): Promise<{ video: MVideoAccountLightBlacklistAllFiles | MVideoThumbnail, created: boolean, autoBlacklisted?: boolean }>
 async function getOrCreateVideoAndAccountAndChannel (options: {
   videoObject: { id: string } | string,
   syncParam?: SyncParam,
   fetchType?: VideoFetchByUrlType,
   allowRefresh?: boolean // true by default
-}) {
+}): Promise<{ video: MVideoAccountLightBlacklistAllFiles | MVideoThumbnail, created: boolean, autoBlacklisted?: boolean }> {
   // Default params
   const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false }
   const fetchType = options.fetchType || 'all'
@@ -227,8 +253,9 @@ async function getOrCreateVideoAndAccountAndChannel (options: {
   const { videoObject: fetchedVideo } = await fetchRemoteVideo(videoUrl)
   if (!fetchedVideo) throw new Error('Cannot fetch remote video with url: ' + videoUrl)
 
-  const channelActor = await getOrCreateVideoChannelFromVideoObject(fetchedVideo)
-  const { autoBlacklisted, videoCreated } = await retryTransactionWrapper(createVideo, fetchedVideo, channelActor, syncParam.thumbnail)
+  const actor = await getOrCreateVideoChannelFromVideoObject(fetchedVideo)
+  const videoChannel = actor.VideoChannel
+  const { autoBlacklisted, videoCreated } = await retryTransactionWrapper(createVideo, fetchedVideo, videoChannel, syncParam.thumbnail)
 
   await syncVideoExternalAttributes(videoCreated, fetchedVideo, syncParam)
 
@@ -236,22 +263,22 @@ async function getOrCreateVideoAndAccountAndChannel (options: {
 }
 
 async function updateVideoFromAP (options: {
-  video: VideoModel,
+  video: MVideoAccountLightBlacklistAllFiles,
   videoObject: VideoTorrentObject,
-  account: AccountModelIdActor,
-  channel: VideoChannelModelIdActor,
+  account: MAccountIdActor,
+  channel: MChannelDefault,
   overrideTo?: string[]
 }) {
   const { video, videoObject, account, channel, overrideTo } = options
 
-  logger.debug('Updating remote video "%s".', options.videoObject.uuid)
+  logger.debug('Updating remote video "%s".', options.videoObject.uuid, { account, channel })
 
   let videoFieldsSave: any
   const wasPrivateVideo = video.privacy === VideoPrivacy.PRIVATE
   const wasUnlistedVideo = video.privacy === VideoPrivacy.UNLISTED
 
   try {
-    let thumbnailModel: ThumbnailModel
+    let thumbnailModel: MThumbnail
 
     try {
       thumbnailModel = await createVideoMiniatureFromUrl(videoObject.icon.url, video, ThumbnailType.MINIATURE)
@@ -259,7 +286,7 @@ async function updateVideoFromAP (options: {
       logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err })
     }
 
-    await sequelizeTypescript.transaction(async t => {
+    const videoUpdated = await sequelizeTypescript.transaction(async t => {
       const sequelizeOptions = { transaction: t }
 
       videoFieldsSave = video.toJSON()
@@ -293,21 +320,21 @@ async function updateVideoFromAP (options: {
       video.channelId = videoData.channelId
       video.views = videoData.views
 
-      await video.save(sequelizeOptions)
+      const videoUpdated = await video.save(sequelizeOptions) as MVideoFullLight
 
-      if (thumbnailModel) await video.addAndSaveThumbnail(thumbnailModel, t)
+      if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel, t)
 
       // FIXME: use icon URL instead
-      const previewUrl = buildRemoteBaseUrl(video, join(STATIC_PATHS.PREVIEWS, video.getPreview().filename))
+      const previewUrl = buildRemoteBaseUrl(videoUpdated, join(STATIC_PATHS.PREVIEWS, videoUpdated.getPreview().filename))
       const previewModel = createPlaceholderThumbnail(previewUrl, video, ThumbnailType.PREVIEW, PREVIEWS_SIZE)
-      await video.addAndSaveThumbnail(previewModel, t)
+      await videoUpdated.addAndSaveThumbnail(previewModel, t)
 
       {
-        const videoFileAttributes = videoFileActivityUrlToDBAttributes(video, videoObject)
+        const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoUpdated, videoObject)
         const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a))
 
         // Remove video files that do not exist anymore
-        const destroyTasks = video.VideoFiles
+        const destroyTasks = videoUpdated.VideoFiles
                                   .filter(f => !newVideoFiles.find(newFile => newFile.hasSameUniqueKeysThan(f)))
                                   .map(f => f.destroy(sequelizeOptions))
         await Promise.all(destroyTasks)
@@ -318,15 +345,15 @@ async function updateVideoFromAP (options: {
             .then(([ file ]) => file)
         })
 
-        video.VideoFiles = await Promise.all(upsertTasks)
+        videoUpdated.VideoFiles = await Promise.all(upsertTasks)
       }
 
       {
-        const streamingPlaylistAttributes = streamingPlaylistActivityUrlToDBAttributes(video, videoObject, video.VideoFiles)
+        const streamingPlaylistAttributes = streamingPlaylistActivityUrlToDBAttributes(videoUpdated, videoObject, videoUpdated.VideoFiles)
         const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a))
 
         // Remove video files that do not exist anymore
-        const destroyTasks = video.VideoStreamingPlaylists
+        const destroyTasks = videoUpdated.VideoStreamingPlaylists
                                   .filter(f => !newStreamingPlaylists.find(newPlaylist => newPlaylist.hasSameUniqueKeysThan(f)))
                                   .map(f => f.destroy(sequelizeOptions))
         await Promise.all(destroyTasks)
@@ -337,38 +364,42 @@ async function updateVideoFromAP (options: {
                                .then(([ streamingPlaylist ]) => streamingPlaylist)
         })
 
-        video.VideoStreamingPlaylists = await Promise.all(upsertTasks)
+        videoUpdated.VideoStreamingPlaylists = await Promise.all(upsertTasks)
       }
 
       {
         // Update Tags
         const tags = videoObject.tag.map(tag => tag.name)
         const tagInstances = await TagModel.findOrCreateTags(tags, t)
-        await video.$set('Tags', tagInstances, sequelizeOptions)
+        await videoUpdated.$set('Tags', tagInstances, sequelizeOptions)
       }
 
       {
         // Update captions
-        await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(video.id, t)
+        await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(videoUpdated.id, t)
 
         const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => {
-          return VideoCaptionModel.insertOrReplaceLanguage(video.id, c.identifier, t)
+          return VideoCaptionModel.insertOrReplaceLanguage(videoUpdated.id, c.identifier, t)
         })
-        video.VideoCaptions = await Promise.all(videoCaptionsPromises)
+        await Promise.all(videoCaptionsPromises)
       }
+
+      return videoUpdated
     })
 
     await autoBlacklistVideoIfNeeded({
-      video,
+      video: videoUpdated,
       user: undefined,
       isRemote: true,
       isNew: false,
       transaction: undefined
     })
 
-    if (wasPrivateVideo || wasUnlistedVideo) Notifier.Instance.notifyOnNewVideoIfNeeded(video) // Notify our users?
+    if (wasPrivateVideo || wasUnlistedVideo) Notifier.Instance.notifyOnNewVideoIfNeeded(videoUpdated) // Notify our users?
 
     logger.info('Remote video with uuid %s updated', videoObject.uuid)
+
+    return videoUpdated
   } catch (err) {
     if (video !== undefined && videoFieldsSave !== undefined) {
       resetSequelizeInstance(video, videoFieldsSave)
@@ -381,15 +412,15 @@ async function updateVideoFromAP (options: {
 }
 
 async function refreshVideoIfNeeded (options: {
-  video: VideoModel,
+  video: MVideoThumbnail,
   fetchedType: VideoFetchByUrlType,
   syncParam: SyncParam
-}): Promise<VideoModel> {
+}): Promise<MVideoThumbnail> {
   if (!options.video.isOutdated()) return options.video
 
   // We need more attributes if the argument video was fetched with not enough joints
   const video = options.fetchedType === 'all'
-    ? options.video
+    ? options.video as MVideoAccountLightBlacklistAllFiles
     : await VideoModel.loadByUrlAndPopulateAccount(options.video.url)
 
   try {
@@ -410,12 +441,11 @@ async function refreshVideoIfNeeded (options: {
     }
 
     const channelActor = await getOrCreateVideoChannelFromVideoObject(videoObject)
-    const account = await AccountModel.load(channelActor.VideoChannel.accountId)
 
     const updateOptions = {
       video,
       videoObject,
-      account,
+      account: channelActor.VideoChannel.Account,
       channel: channelActor.VideoChannel
     }
     await retryTransactionWrapper(updateVideoFromAP, updateOptions)
@@ -467,15 +497,15 @@ function isAPPlaylistSegmentHashesUrlObject (tag: any): tag is ActivityPlaylistS
   return tag.name === 'sha256' && tag.type === 'Link' && urlMediaType === 'application/json'
 }
 
-async function createVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) {
+async function createVideo (videoObject: VideoTorrentObject, channel: MChannelAccountLight, waitThumbnail = false) {
   logger.debug('Adding remote video %s.', videoObject.id)
 
-  const videoData = await videoActivityObjectToDBAttributes(channelActor.VideoChannel, videoObject, videoObject.to)
-  const video = VideoModel.build(videoData)
+  const videoData = await videoActivityObjectToDBAttributes(channel, videoObject, videoObject.to)
+  const video = VideoModel.build(videoData) as MVideoThumbnail
 
   const promiseThumbnail = createVideoMiniatureFromUrl(videoObject.icon.url, video, ThumbnailType.MINIATURE)
 
-  let thumbnailModel: ThumbnailModel
+  let thumbnailModel: MThumbnail
   if (waitThumbnail === true) {
     thumbnailModel = await promiseThumbnail
   }
@@ -483,8 +513,8 @@ async function createVideo (videoObject: VideoTorrentObject, channelActor: Actor
   const { autoBlacklisted, videoCreated } = await sequelizeTypescript.transaction(async t => {
     const sequelizeOptions = { transaction: t }
 
-    const videoCreated = await video.save(sequelizeOptions)
-    videoCreated.VideoChannel = channelActor.VideoChannel
+    const videoCreated = await video.save(sequelizeOptions) as MVideoFullLight
+    videoCreated.VideoChannel = channel
 
     if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
 
@@ -517,15 +547,14 @@ async function createVideo (videoObject: VideoTorrentObject, channelActor: Actor
     const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => {
       return VideoCaptionModel.insertOrReplaceLanguage(videoCreated.id, c.identifier, t)
     })
-    const captions = await Promise.all(videoCaptionsPromises)
+    await Promise.all(videoCaptionsPromises)
 
-    video.VideoFiles = videoFiles
-    video.VideoStreamingPlaylists = streamingPlaylists
-    video.Tags = tagInstances
-    video.VideoCaptions = captions
+    videoCreated.VideoFiles = videoFiles
+    videoCreated.VideoStreamingPlaylists = streamingPlaylists
+    videoCreated.Tags = tagInstances
 
     const autoBlacklisted = await autoBlacklistVideoIfNeeded({
-      video,
+      video: videoCreated,
       user: undefined,
       isRemote: true,
       isNew: true,
@@ -548,11 +577,7 @@ async function createVideo (videoObject: VideoTorrentObject, channelActor: Actor
   return { autoBlacklisted, videoCreated }
 }
 
-async function videoActivityObjectToDBAttributes (
-  videoChannel: VideoChannelModelId,
-  videoObject: VideoTorrentObject,
-  to: string[] = []
-) {
+async function videoActivityObjectToDBAttributes (videoChannel: MChannelId, videoObject: VideoTorrentObject, to: string[] = []) {
   const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPrivacy.PUBLIC : VideoPrivacy.UNLISTED
   const duration = videoObject.duration.replace(/[^\d]+/, '')
 
@@ -603,7 +628,7 @@ async function videoActivityObjectToDBAttributes (
   }
 }
 
-function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: VideoTorrentObject) {
+function videoFileActivityUrlToDBAttributes (video: MVideo, videoObject: VideoTorrentObject) {
   const fileUrls = videoObject.url.filter(u => isAPVideoUrlObject(u)) as ActivityVideoUrlObject[]
 
   if (fileUrls.length === 0) {
@@ -641,7 +666,7 @@ function videoFileActivityUrlToDBAttributes (video: VideoModel, videoObject: Vid
   return attributes
 }
 
-function streamingPlaylistActivityUrlToDBAttributes (video: VideoModel, videoObject: VideoTorrentObject, videoFiles: VideoFileModel[]) {
+function streamingPlaylistActivityUrlToDBAttributes (video: MVideoId, videoObject: VideoTorrentObject, videoFiles: MVideoFile[]) {
   const playlistUrls = videoObject.url.filter(u => isAPStreamingPlaylistUrlObject(u)) as ActivityPlaylistUrlObject[]
   if (playlistUrls.length === 0) return []
 
index 1b38e6cb59060743240f003e212f8f4dcf0ea5d2..ad4cdd3ab80849388a63accdba1797a4ba65482e 100644 (file)
@@ -3,8 +3,6 @@ import { sendUpdateActor } from './activitypub/send'
 import { AVATARS_SIZE, LRU_CACHE, QUEUE_CONCURRENCY } from '../initializers/constants'
 import { updateActorAvatarInstance } from './activitypub'
 import { processImage } from '../helpers/image-utils'
-import { AccountModel } from '../models/account/account'
-import { VideoChannelModel } from '../models/video/video-channel'
 import { extname, join } from 'path'
 import { retryTransactionWrapper } from '../helpers/database-utils'
 import * as uuidv4 from 'uuid/v4'
@@ -13,8 +11,12 @@ import { sequelizeTypescript } from '../initializers/database'
 import * as LRUCache from 'lru-cache'
 import { queue } from 'async'
 import { downloadImage } from '../helpers/requests'
+import { MAccountDefault, MChannelDefault } from '../typings/models'
 
-async function updateActorAvatarFile (avatarPhysicalFile: Express.Multer.File, accountOrChannel: AccountModel | VideoChannelModel) {
+async function updateActorAvatarFile (
+  avatarPhysicalFile: Express.Multer.File,
+  accountOrChannel: MAccountDefault | MChannelDefault
+) {
   const extension = extname(avatarPhysicalFile.filename)
   const avatarName = uuidv4() + extension
   const destination = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)
index 1633e500cb34964b9d9252adfc43c6f41ce6c9b3..28c69b46e767744a73d267fa97f9fcabb9aaba8d 100644 (file)
@@ -1,6 +1,7 @@
 import { sequelizeTypescript } from '../initializers'
 import { AccountBlocklistModel } from '../models/account/account-blocklist'
 import { ServerBlocklistModel } from '../models/server/server-blocklist'
+import { MAccountBlocklist, MServerBlocklist } from '@server/typings/models'
 
 function addAccountInBlocklist (byAccountId: number, targetAccountId: number) {
   return sequelizeTypescript.transaction(async t => {
@@ -20,13 +21,13 @@ function addServerInBlocklist (byAccountId: number, targetServerId: number) {
   })
 }
 
-function removeAccountFromBlocklist (accountBlock: AccountBlocklistModel) {
+function removeAccountFromBlocklist (accountBlock: MAccountBlocklist) {
   return sequelizeTypescript.transaction(async t => {
     return accountBlock.destroy({ transaction: t })
   })
 }
 
-function removeServerFromBlocklist (serverBlock: ServerBlocklistModel) {
+function removeServerFromBlocklist (serverBlock: MServerBlocklist) {
   return sequelizeTypescript.transaction(async t => {
     return serverBlock.destroy({ transaction: t })
   })
index 8841dd2ac2a69f02e88c26eb0caf3760cf49c851..a1f4ae85856227ba6cf9519058903f43096fb601 100644 (file)
@@ -13,6 +13,7 @@ import { VideoChannelModel } from '../models/video/video-channel'
 import * as Bluebird from 'bluebird'
 import { CONFIG } from '../initializers/config'
 import { logger } from '../helpers/logger'
+import { MAccountActor, MChannelActor, MVideo } from '../typings/models'
 
 export class ClientHtml {
 
@@ -41,11 +42,11 @@ export class ClientHtml {
 
     const [ html, video ] = await Promise.all([
       ClientHtml.getIndexHTML(req, res),
-      VideoModel.load(videoId)
+      VideoModel.loadWithBlacklist(videoId)
     ])
 
     // Let Angular application handle errors
-    if (!video || video.privacy === VideoPrivacy.PRIVATE) {
+    if (!video || video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) {
       return ClientHtml.getIndexHTML(req, res)
     }
 
@@ -65,7 +66,7 @@ export class ClientHtml {
   }
 
   private static async getAccountOrChannelHTMLPage (
-    loader: () => Bluebird<AccountModel | VideoChannelModel>,
+    loader: () => Bluebird<MAccountActor | MChannelActor>,
     req: express.Request,
     res: express.Response
   ) {
@@ -157,7 +158,7 @@ export class ClientHtml {
     return htmlStringPage.replace('</head>', linkTag + '</head>')
   }
 
-  private static addVideoOpenGraphAndOEmbedTags (htmlStringPage: string, video: VideoModel) {
+  private static addVideoOpenGraphAndOEmbedTags (htmlStringPage: string, video: MVideo) {
     const previewUrl = WEBSERVER.URL + video.getPreviewStaticPath()
     const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
 
@@ -236,7 +237,7 @@ export class ClientHtml {
     return this.addOpenGraphAndOEmbedTags(htmlStringPage, tagsString)
   }
 
-  private static addAccountOrChannelMetaTags (htmlStringPage: string, entity: AccountModel | VideoChannelModel) {
+  private static addAccountOrChannelMetaTags (htmlStringPage: string, entity: MAccountActor | MChannelActor) {
     // SEO, use origin account or channel URL
     const metaTags = `<link rel="canonical" href="${entity.Actor.url}" />`
 
index 10e7d0479ca578167f11618888a306da691dd301..bd3d4f252eb120132bb01dd27cd5cb1655c7b59e 100644 (file)
@@ -2,17 +2,20 @@ import { createTransport, Transporter } from 'nodemailer'
 import { isTestInstance } from '../helpers/core-utils'
 import { bunyanLogger, logger } from '../helpers/logger'
 import { CONFIG } from '../initializers/config'
-import { UserModel } from '../models/account/user'
-import { VideoModel } from '../models/video/video'
 import { JobQueue } from './job-queue'
 import { EmailPayload } from './job-queue/handlers/email'
 import { readFileSync } from 'fs-extra'
-import { VideoCommentModel } from '../models/video/video-comment'
-import { VideoAbuseModel } from '../models/video/video-abuse'
-import { VideoBlacklistModel } from '../models/video/video-blacklist'
-import { VideoImportModel } from '../models/video/video-import'
-import { ActorFollowModel } from '../models/activitypub/actor-follow'
 import { WEBSERVER } from '../initializers/constants'
+import {
+  MCommentOwnerVideo,
+  MVideo,
+  MVideoAbuseVideo,
+  MVideoAccountLight,
+  MVideoBlacklistLightVideo,
+  MVideoBlacklistVideo
+} from '../typings/models/video'
+import { MActorFollowActors, MActorFollowFull, MUser } from '../typings/models'
+import { MVideoImport, MVideoImportVideo } from '@server/typings/models/video/video-import'
 
 type SendEmailOptions = {
   to: string[]
@@ -90,7 +93,7 @@ class Emailer {
     }
   }
 
-  addNewVideoFromSubscriberNotification (to: string[], video: VideoModel) {
+  addNewVideoFromSubscriberNotification (to: string[], video: MVideoAccountLight) {
     const channelName = video.VideoChannel.getDisplayName()
     const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
 
@@ -111,7 +114,7 @@ class Emailer {
     return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
   }
 
-  addNewFollowNotification (to: string[], actorFollow: ActorFollowModel, followType: 'account' | 'channel') {
+  addNewFollowNotification (to: string[], actorFollow: MActorFollowFull, followType: 'account' | 'channel') {
     const followerName = actorFollow.ActorFollower.Account.getDisplayName()
     const followingName = (actorFollow.ActorFollowing.VideoChannel || actorFollow.ActorFollowing.Account).getDisplayName()
 
@@ -130,7 +133,7 @@ class Emailer {
     return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
   }
 
-  addNewInstanceFollowerNotification (to: string[], actorFollow: ActorFollowModel) {
+  addNewInstanceFollowerNotification (to: string[], actorFollow: MActorFollowActors) {
     const awaitingApproval = actorFollow.state === 'pending' ? ' awaiting manual approval.' : ''
 
     const text = `Hi dear admin,\n\n` +
@@ -148,7 +151,23 @@ class Emailer {
     return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
   }
 
-  myVideoPublishedNotification (to: string[], video: VideoModel) {
+  addAutoInstanceFollowingNotification (to: string[], actorFollow: MActorFollowActors) {
+    const text = `Hi dear admin,\n\n` +
+      `Your instance automatically followed a new instance: ${actorFollow.ActorFollowing.url}` +
+      `\n\n` +
+      `Cheers,\n` +
+      `${CONFIG.EMAIL.BODY.SIGNATURE}`
+
+    const emailPayload: EmailPayload = {
+      to,
+      subject: CONFIG.EMAIL.SUBJECT.PREFIX + 'Auto instance following',
+      text
+    }
+
+    return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
+  }
+
+  myVideoPublishedNotification (to: string[], video: MVideo) {
     const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
 
     const text = `Hi dear user,\n\n` +
@@ -168,7 +187,7 @@ class Emailer {
     return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
   }
 
-  myVideoImportSuccessNotification (to: string[], videoImport: VideoImportModel) {
+  myVideoImportSuccessNotification (to: string[], videoImport: MVideoImportVideo) {
     const videoUrl = WEBSERVER.URL + videoImport.Video.getWatchStaticPath()
 
     const text = `Hi dear user,\n\n` +
@@ -188,7 +207,7 @@ class Emailer {
     return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
   }
 
-  myVideoImportErrorNotification (to: string[], videoImport: VideoImportModel) {
+  myVideoImportErrorNotification (to: string[], videoImport: MVideoImport) {
     const importUrl = WEBSERVER.URL + '/my-account/video-imports'
 
     const text = `Hi dear user,\n\n` +
@@ -208,7 +227,7 @@ class Emailer {
     return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
   }
 
-  addNewCommentOnMyVideoNotification (to: string[], comment: VideoCommentModel) {
+  addNewCommentOnMyVideoNotification (to: string[], comment: MCommentOwnerVideo) {
     const accountName = comment.Account.getDisplayName()
     const video = comment.Video
     const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath()
@@ -230,7 +249,7 @@ class Emailer {
     return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
   }
 
-  addNewCommentMentionNotification (to: string[], comment: VideoCommentModel) {
+  addNewCommentMentionNotification (to: string[], comment: MCommentOwnerVideo) {
     const accountName = comment.Account.getDisplayName()
     const video = comment.Video
     const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath()
@@ -252,7 +271,7 @@ class Emailer {
     return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
   }
 
-  addVideoAbuseModeratorsNotification (to: string[], videoAbuse: VideoAbuseModel) {
+  addVideoAbuseModeratorsNotification (to: string[], videoAbuse: MVideoAbuseVideo) {
     const videoUrl = WEBSERVER.URL + videoAbuse.Video.getWatchStaticPath()
 
     const text = `Hi,\n\n` +
@@ -269,9 +288,9 @@ class Emailer {
     return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
   }
 
-  addVideoAutoBlacklistModeratorsNotification (to: string[], video: VideoModel) {
+  addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) {
     const VIDEO_AUTO_BLACKLIST_URL = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list'
-    const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
+    const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath()
 
     const text = `Hi,\n\n` +
       `A recently added video was auto-blacklisted and requires moderator review before publishing.` +
@@ -292,7 +311,7 @@ class Emailer {
     return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
   }
 
-  addNewUserRegistrationNotification (to: string[], user: UserModel) {
+  addNewUserRegistrationNotification (to: string[], user: MUser) {
     const text = `Hi,\n\n` +
       `User ${user.username} just registered on ${WEBSERVER.HOST} PeerTube instance.\n\n` +
       `Cheers,\n` +
@@ -307,7 +326,7 @@ class Emailer {
     return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
   }
 
-  addVideoBlacklistNotification (to: string[], videoBlacklist: VideoBlacklistModel) {
+  addVideoBlacklistNotification (to: string[], videoBlacklist: MVideoBlacklistVideo) {
     const videoName = videoBlacklist.Video.name
     const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath()
 
@@ -329,7 +348,7 @@ class Emailer {
     return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
   }
 
-  addVideoUnblacklistNotification (to: string[], video: VideoModel) {
+  addVideoUnblacklistNotification (to: string[], video: MVideo) {
     const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
 
     const text = 'Hi,\n\n' +
@@ -381,7 +400,7 @@ class Emailer {
     return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
   }
 
-  addUserBlockJob (user: UserModel, blocked: boolean, reason?: string) {
+  addUserBlockJob (user: MUser, blocked: boolean, reason?: string) {
     const reasonString = reason ? ` for the following reason: ${reason}` : ''
     const blockedWord = blocked ? 'blocked' : 'unblocked'
     const blockedString = `Your account ${user.username} on ${WEBSERVER.HOST} has been ${blockedWord}${reasonString}.`
index 98da4dcd8a80f80683f4111c1cc8fc68e1abd74f..05136c21cdd2bc85fc83c9348da492e1cff87b91 100644 (file)
@@ -1,4 +1,3 @@
-import { VideoModel } from '../models/video/video'
 import { basename, dirname, join } from 'path'
 import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION } from '../initializers/constants'
 import { close, ensureDir, move, open, outputJSON, pathExists, read, readFile, remove, writeFile } from 'fs-extra'
@@ -12,6 +11,7 @@ import { flatten, uniq } from 'lodash'
 import { VideoFileModel } from '../models/video/video-file'
 import { CONFIG } from '../initializers/config'
 import { sequelizeTypescript } from '../initializers/database'
+import { MVideoWithFile } from '@server/typings/models'
 
 async function updateStreamingPlaylistsInfohashesIfNeeded () {
   const playlistsToUpdate = await VideoStreamingPlaylistModel.listByIncorrectPeerVersion()
@@ -28,7 +28,7 @@ async function updateStreamingPlaylistsInfohashesIfNeeded () {
   }
 }
 
-async function updateMasterHLSPlaylist (video: VideoModel) {
+async function updateMasterHLSPlaylist (video: MVideoWithFile) {
   const directory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)
   const masterPlaylists: string[] = [ '#EXTM3U', '#EXT-X-VERSION:3' ]
   const masterPlaylistPath = join(directory, VideoStreamingPlaylistModel.getMasterHlsPlaylistFilename())
@@ -55,7 +55,7 @@ async function updateMasterHLSPlaylist (video: VideoModel) {
   await writeFile(masterPlaylistPath, masterPlaylists.join('\n') + '\n')
 }
 
-async function updateSha256Segments (video: VideoModel) {
+async function updateSha256Segments (video: MVideoWithFile) {
   const json: { [filename: string]: { [range: string]: string } } = {}
 
   const playlistDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)
index 4ae66cd0165de75fce6cf939ed1b1028d8563a1e..af7c8a8383afe42bcdcadb7e6e56242f1dcb745b 100644 (file)
@@ -10,11 +10,13 @@ import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
 import { ActorModel } from '../../../models/activitypub/actor'
 import { Notifier } from '../../notifier'
 import { sequelizeTypescript } from '../../../initializers/database'
+import { MActor, MActorFollowActors, MActorFull } from '../../../typings/models'
 
 export type ActivitypubFollowPayload = {
   followerActorId: number
   name: string
   host: string
+  isAutoFollow?: boolean
 }
 
 async function processActivityPubFollow (job: Bull.Job) {
@@ -23,18 +25,18 @@ async function processActivityPubFollow (job: Bull.Job) {
 
   logger.info('Processing ActivityPub follow in job %d.', job.id)
 
-  let targetActor: ActorModel
+  let targetActor: MActorFull
   if (!host || host === WEBSERVER.HOST) {
     targetActor = await ActorModel.loadLocalByName(payload.name)
   } else {
     const sanitizedHost = sanitizeHost(host, REMOTE_SCHEME.HTTP)
     const actorUrl = await loadActorUrlOrGetFromWebfinger(payload.name + '@' + sanitizedHost)
-    targetActor = await getOrCreateActorAndServerAndModel(actorUrl)
+    targetActor = await getOrCreateActorAndServerAndModel(actorUrl, 'all')
   }
 
   const fromActor = await ActorModel.load(payload.followerActorId)
 
-  return retryTransactionWrapper(follow, fromActor, targetActor)
+  return retryTransactionWrapper(follow, fromActor, targetActor, payload.isAutoFollow)
 }
 // ---------------------------------------------------------------------------
 
@@ -44,7 +46,7 @@ export {
 
 // ---------------------------------------------------------------------------
 
-async function follow (fromActor: ActorModel, targetActor: ActorModel) {
+async function follow (fromActor: MActor, targetActor: MActorFull, isAutoFollow = false) {
   if (fromActor.id === targetActor.id) {
     throw new Error('Follower is the same than target actor.')
   }
@@ -53,7 +55,7 @@ async function follow (fromActor: ActorModel, targetActor: ActorModel) {
   const state = !fromActor.serverId && !targetActor.serverId ? 'accepted' : 'pending'
 
   const actorFollow = await sequelizeTypescript.transaction(async t => {
-    const [ actorFollow ] = await ActorFollowModel.findOrCreate({
+    const [ actorFollow ] = await ActorFollowModel.findOrCreate<MActorFollowActors>({
       where: {
         actorId: fromActor.id,
         targetActorId: targetActor.id
@@ -74,5 +76,15 @@ async function follow (fromActor: ActorModel, targetActor: ActorModel) {
     return actorFollow
   })
 
-  if (actorFollow.state === 'accepted') Notifier.Instance.notifyOfNewUserFollow(actorFollow)
+  const followerFull = await ActorModel.loadFull(fromActor.id)
+
+  const actorFollowFull = Object.assign(actorFollow, {
+    ActorFollowing: targetActor,
+    ActorFollower: followerFull
+  })
+
+  if (actorFollow.state === 'accepted') Notifier.Instance.notifyOfNewUserFollow(actorFollowFull)
+  if (isAutoFollow === true) Notifier.Instance.notifyOfAutoInstanceFollowing(actorFollowFull)
+
+  return actorFollow
 }
index c3f59dc7781186d4dfc797b66298bd0fafe50e67..0182c5169f17ac0c2be83afdee3337499f64fea0 100644 (file)
@@ -11,6 +11,7 @@ import { AccountModel } from '../../../models/account/account'
 import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
 import { VideoShareModel } from '../../../models/video/video-share'
 import { VideoCommentModel } from '../../../models/video/video-comment'
+import { MAccountDefault, MVideoFullLight } from '../../../typings/models'
 
 type FetchType = 'activity' | 'video-likes' | 'video-dislikes' | 'video-shares' | 'video-comments' | 'account-playlists'
 
@@ -26,10 +27,10 @@ async function processActivityPubHttpFetcher (job: Bull.Job) {
 
   const payload = job.data as ActivitypubHttpFetcherPayload
 
-  let video: VideoModel
+  let video: MVideoFullLight
   if (payload.videoId) video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoId)
 
-  let account: AccountModel
+  let account: MAccountDefault
   if (payload.accountId) account = await AccountModel.load(payload.accountId)
 
   const fetcherType: { [ id in FetchType ]: (items: any[]) => Promise<any> } = {
index cdee1f6fd4eb2636b803788420b5b1548535c651..d3bde6e6a25f2d57897d02e1dcc4ce6b7f79faba 100644 (file)
@@ -3,6 +3,7 @@ import { getServerActor } from '../../../../helpers/utils'
 import { ActorModel } from '../../../../models/activitypub/actor'
 import { sha256 } from '../../../../helpers/core-utils'
 import { HTTP_SIGNATURE } from '../../../../initializers/constants'
+import { MActor } from '../../../../typings/models'
 
 type Payload = { body: any, signatureActorId?: number }
 
@@ -19,7 +20,8 @@ async function computeBody (payload: Payload) {
 }
 
 async function buildSignedRequestOptions (payload: Payload) {
-  let actor: ActorModel | null
+  let actor: MActor | null
+
   if (payload.signatureActorId) {
     actor = await ActorModel.load(payload.signatureActorId)
     if (!actor) throw new Error('Unknown signature actor id.')
index 8cacb0ef3ea3b902f788b2d4e0f92fb1540b8320..5c5b7dccb77c5bac463080d492427d1b4ee5e2b3 100644 (file)
@@ -6,6 +6,7 @@ import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg
 import { copy, stat } from 'fs-extra'
 import { VideoFileModel } from '../../../models/video/video-file'
 import { extname } from 'path'
+import { MVideoFile, MVideoWithFile } from '@server/typings/models'
 
 export type VideoFileImportPayload = {
   videoUUID: string,
@@ -37,7 +38,7 @@ export {
 
 // ---------------------------------------------------------------------------
 
-async function updateVideoFile (video: VideoModel, inputFilePath: string) {
+async function updateVideoFile (video: MVideoWithFile, inputFilePath: string) {
   const { videoFileResolution } = await getVideoFileResolution(inputFilePath)
   const { size } = await stat(inputFilePath)
   const fps = await getVideoFileFPS(inputFilePath)
@@ -48,7 +49,7 @@ async function updateVideoFile (video: VideoModel, inputFilePath: string) {
     size,
     fps,
     videoId: video.id
-  })
+  }) as MVideoFile
 
   const currentVideoFile = video.VideoFiles.find(videoFile => videoFile.resolution === updatedVideoFile.resolution)
 
@@ -60,9 +61,9 @@ async function updateVideoFile (video: VideoModel, inputFilePath: string) {
     video.VideoFiles = video.VideoFiles.filter(f => f !== currentVideoFile)
 
     // Update the database
-    currentVideoFile.set('extname', updatedVideoFile.extname)
-    currentVideoFile.set('size', updatedVideoFile.size)
-    currentVideoFile.set('fps', updatedVideoFile.fps)
+    currentVideoFile.extname = updatedVideoFile.extname
+    currentVideoFile.size = updatedVideoFile.size
+    currentVideoFile.fps = updatedVideoFile.fps
 
     updatedVideoFile = currentVideoFile
   }
index 13b741180daefdc15a8d400a0a4f3b2bf3d8d0b6..93a3e9d901cc0f2819738f4b75728a33f60b3d03 100644 (file)
@@ -17,9 +17,11 @@ import { move, remove, stat } from 'fs-extra'
 import { Notifier } from '../../notifier'
 import { CONFIG } from '../../../initializers/config'
 import { sequelizeTypescript } from '../../../initializers/database'
-import { ThumbnailModel } from '../../../models/video/thumbnail'
 import { createVideoMiniatureFromUrl, generateVideoMiniature } from '../../thumbnail'
 import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
+import { MThumbnail } from '../../../typings/models/video/thumbnail'
+import { MVideoImportDefault, MVideoImportDefaultFiles, MVideoImportVideo } from '@server/typings/models/video/video-import'
+import { MVideoBlacklistVideo, MVideoBlacklist } from '@server/typings/models'
 
 type VideoImportYoutubeDLPayload = {
   type: 'youtube-dl'
@@ -110,7 +112,7 @@ type ProcessFileOptions = {
   generateThumbnail: boolean
   generatePreview: boolean
 }
-async function processFile (downloader: () => Promise<string>, videoImport: VideoImportModel, options: ProcessFileOptions) {
+async function processFile (downloader: () => Promise<string>, videoImport: MVideoImportDefault, options: ProcessFileOptions) {
   let tempVideoPath: string
   let videoDestFile: string
   let videoFile: VideoFileModel
@@ -139,41 +141,44 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
       videoId: videoImport.videoId
     }
     videoFile = new VideoFileModel(videoFileData)
+
+    const videoWithFiles = Object.assign(videoImport.Video, { VideoFiles: [ videoFile ] })
     // To clean files if the import fails
-    videoImport.Video.VideoFiles = [ videoFile ]
+    const videoImportWithFiles: MVideoImportDefaultFiles = Object.assign(videoImport, { Video: videoWithFiles })
 
     // Move file
-    videoDestFile = join(CONFIG.STORAGE.VIDEOS_DIR, videoImport.Video.getVideoFilename(videoFile))
+    videoDestFile = join(CONFIG.STORAGE.VIDEOS_DIR, videoImportWithFiles.Video.getVideoFilename(videoFile))
     await move(tempVideoPath, videoDestFile)
     tempVideoPath = null // This path is not used anymore
 
     // Process thumbnail
-    let thumbnailModel: ThumbnailModel
+    let thumbnailModel: MThumbnail
     if (options.downloadThumbnail && options.thumbnailUrl) {
-      thumbnailModel = await createVideoMiniatureFromUrl(options.thumbnailUrl, videoImport.Video, ThumbnailType.MINIATURE)
+      thumbnailModel = await createVideoMiniatureFromUrl(options.thumbnailUrl, videoImportWithFiles.Video, ThumbnailType.MINIATURE)
     } else if (options.generateThumbnail || options.downloadThumbnail) {
-      thumbnailModel = await generateVideoMiniature(videoImport.Video, videoFile, ThumbnailType.MINIATURE)
+      thumbnailModel = await generateVideoMiniature(videoImportWithFiles.Video, videoFile, ThumbnailType.MINIATURE)
     }
 
     // Process preview
-    let previewModel: ThumbnailModel
+    let previewModel: MThumbnail
     if (options.downloadPreview && options.thumbnailUrl) {
-      previewModel = await createVideoMiniatureFromUrl(options.thumbnailUrl, videoImport.Video, ThumbnailType.PREVIEW)
+      previewModel = await createVideoMiniatureFromUrl(options.thumbnailUrl, videoImportWithFiles.Video, ThumbnailType.PREVIEW)
     } else if (options.generatePreview || options.downloadPreview) {
-      previewModel = await generateVideoMiniature(videoImport.Video, videoFile, ThumbnailType.PREVIEW)
+      previewModel = await generateVideoMiniature(videoImportWithFiles.Video, videoFile, ThumbnailType.PREVIEW)
     }
 
     // Create torrent
-    await videoImport.Video.createTorrentAndSetInfoHash(videoFile)
+    await videoImportWithFiles.Video.createTorrentAndSetInfoHash(videoFile)
+
+    const { videoImportUpdated, video } = await sequelizeTypescript.transaction(async t => {
+      const videoImportToUpdate = videoImportWithFiles as MVideoImportVideo
 
-    const videoImportUpdated: VideoImportModel = await sequelizeTypescript.transaction(async t => {
       // Refresh video
-      const video = await VideoModel.load(videoImport.videoId, t)
-      if (!video) throw new Error('Video linked to import ' + videoImport.videoId + ' does not exist anymore.')
-      videoImport.Video = video
+      const video = await VideoModel.load(videoImportToUpdate.videoId, t)
+      if (!video) throw new Error('Video linked to import ' + videoImportToUpdate.videoId + ' does not exist anymore.')
 
       const videoFileCreated = await videoFile.save({ transaction: t })
-      video.VideoFiles = [ videoFileCreated ]
+      videoImportToUpdate.Video = Object.assign(video, { VideoFiles: [ videoFileCreated ] })
 
       // Update video DB object
       video.duration = duration
@@ -188,25 +193,27 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
       await federateVideoIfNeeded(videoForFederation, true, t)
 
       // Update video import object
-      videoImport.state = VideoImportState.SUCCESS
-      const videoImportUpdated = await videoImport.save({ transaction: t })
+      videoImportToUpdate.state = VideoImportState.SUCCESS
+      const videoImportUpdated = await videoImportToUpdate.save({ transaction: t }) as MVideoImportVideo
+      videoImportUpdated.Video = video
 
       logger.info('Video %s imported.', video.uuid)
 
-      videoImportUpdated.Video = videoForFederation
-      return videoImportUpdated
+      return { videoImportUpdated, video: videoForFederation }
     })
 
     Notifier.Instance.notifyOnFinishedVideoImport(videoImportUpdated, true)
 
-    if (videoImportUpdated.Video.isBlacklisted()) {
-      Notifier.Instance.notifyOnVideoAutoBlacklist(videoImportUpdated.Video)
+    if (video.isBlacklisted()) {
+      const videoBlacklist = Object.assign(video.VideoBlacklist, { Video: video })
+
+      Notifier.Instance.notifyOnVideoAutoBlacklist(videoBlacklist)
     } else {
-      Notifier.Instance.notifyOnNewVideoIfNeeded(videoImportUpdated.Video)
+      Notifier.Instance.notifyOnNewVideoIfNeeded(video)
     }
 
     // Create transcoding jobs?
-    if (videoImportUpdated.Video.state === VideoState.TO_TRANSCODE) {
+    if (video.state === VideoState.TO_TRANSCODE) {
       // Put uuid because we don't have id auto incremented for now
       const dataInput = {
         type: 'optimize' as 'optimize',
index 981daf9a1a5003e46ad920264fc0957b211dee99..2ebe15bcb0bd0a0fed813e39cf7956ac1e5439ac 100644 (file)
@@ -11,6 +11,7 @@ import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils'
 import { generateHlsPlaylist, optimizeVideofile, transcodeOriginalVideofile, mergeAudioVideofile } from '../../video-transcoding'
 import { Notifier } from '../../notifier'
 import { CONFIG } from '../../../initializers/config'
+import { MVideoUUID, MVideoWithFile } from '@server/typings/models'
 
 interface BaseTranscodingPayload {
   videoUUID: string
@@ -73,7 +74,7 @@ async function processVideoTranscoding (job: Bull.Job) {
   return video
 }
 
-async function onHlsPlaylistGenerationSuccess (video: VideoModel) {
+async function onHlsPlaylistGenerationSuccess (video: MVideoUUID) {
   if (video === undefined) return undefined
 
   await sequelizeTypescript.transaction(async t => {
@@ -87,7 +88,7 @@ async function onHlsPlaylistGenerationSuccess (video: VideoModel) {
   })
 }
 
-async function publishNewResolutionIfNeeded (video: VideoModel, payload?: NewResolutionTranscodingPayload | MergeAudioTranscodingPayload) {
+async function publishNewResolutionIfNeeded (video: MVideoUUID, payload?: NewResolutionTranscodingPayload | MergeAudioTranscodingPayload) {
   const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => {
     // Maybe the video changed in database, refresh it
     let videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t)
@@ -119,7 +120,7 @@ async function publishNewResolutionIfNeeded (video: VideoModel, payload?: NewRes
   await createHlsJobIfEnabled(payload)
 }
 
-async function onVideoFileOptimizerSuccess (videoArg: VideoModel, payload: OptimizeTranscodingPayload) {
+async function onVideoFileOptimizerSuccess (videoArg: MVideoWithFile, payload: OptimizeTranscodingPayload) {
   if (videoArg === undefined) return undefined
 
   // Outside the transaction (IO on disk)
index a7dfb09794017c1a14d64253a13c2f78d61672f6..b7cc2607d3160370f36eb5c29192b87027c7506a 100644 (file)
@@ -1,20 +1,30 @@
 import { UserNotificationSettingValue, UserNotificationType, UserRight } from '../../shared/models/users'
 import { logger } from '../helpers/logger'
-import { VideoModel } from '../models/video/video'
 import { Emailer } from './emailer'
 import { UserNotificationModel } from '../models/account/user-notification'
-import { VideoCommentModel } from '../models/video/video-comment'
 import { UserModel } from '../models/account/user'
 import { PeerTubeSocket } from './peertube-socket'
 import { CONFIG } from '../initializers/config'
 import { VideoPrivacy, VideoState } from '../../shared/models/videos'
-import { VideoAbuseModel } from '../models/video/video-abuse'
-import { VideoBlacklistModel } from '../models/video/video-blacklist'
 import * as Bluebird from 'bluebird'
-import { VideoImportModel } from '../models/video/video-import'
 import { AccountBlocklistModel } from '../models/account/account-blocklist'
-import { ActorFollowModel } from '../models/activitypub/actor-follow'
-import { AccountModel } from '../models/account/account'
+import {
+  MCommentOwnerVideo,
+  MVideoAbuseVideo,
+  MVideoAccountLight,
+  MVideoBlacklistLightVideo,
+  MVideoBlacklistVideo,
+  MVideoFullLight
+} from '../typings/models/video'
+import {
+  MUser,
+  MUserDefault,
+  MUserNotifSettingAccount,
+  MUserWithNotificationSetting,
+  UserNotificationModelForApi
+} from '@server/typings/models/user'
+import { MActorFollowFull } from '../typings/models'
+import { MVideoImportVideo } from '@server/typings/models/video/video-import'
 
 class Notifier {
 
@@ -22,7 +32,7 @@ class Notifier {
 
   private constructor () {}
 
-  notifyOnNewVideoIfNeeded (video: VideoModel): void {
+  notifyOnNewVideoIfNeeded (video: MVideoAccountLight): void {
     // Only notify on public and published videos which are not blacklisted
     if (video.privacy !== VideoPrivacy.PUBLIC || video.state !== VideoState.PUBLISHED || video.isBlacklisted()) return
 
@@ -30,7 +40,7 @@ class Notifier {
       .catch(err => logger.error('Cannot notify subscribers of new video %s.', video.url, { err }))
   }
 
-  notifyOnVideoPublishedAfterTranscoding (video: VideoModel): void {
+  notifyOnVideoPublishedAfterTranscoding (video: MVideoFullLight): void {
     // don't notify if didn't wait for transcoding or video is still blacklisted/waiting for scheduled update
     if (!video.waitTranscoding || video.VideoBlacklist || video.ScheduleVideoUpdate) return
 
@@ -38,7 +48,7 @@ class Notifier {
         .catch(err => logger.error('Cannot notify owner that its video %s has been published after transcoding.', video.url, { err }))
   }
 
-  notifyOnVideoPublishedAfterScheduledUpdate (video: VideoModel): void {
+  notifyOnVideoPublishedAfterScheduledUpdate (video: MVideoFullLight): void {
     // don't notify if video is still blacklisted or waiting for transcoding
     if (video.VideoBlacklist || (video.waitTranscoding && video.state !== VideoState.PUBLISHED)) return
 
@@ -46,7 +56,7 @@ class Notifier {
         .catch(err => logger.error('Cannot notify owner that its video %s has been published after scheduled update.', video.url, { err }))
   }
 
-  notifyOnVideoPublishedAfterRemovedFromAutoBlacklist (video: VideoModel): void {
+  notifyOnVideoPublishedAfterRemovedFromAutoBlacklist (video: MVideoFullLight): void {
     // don't notify if video is still waiting for transcoding or scheduled update
     if (video.ScheduleVideoUpdate || (video.waitTranscoding && video.state !== VideoState.PUBLISHED)) return
 
@@ -54,7 +64,7 @@ class Notifier {
         .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
   }
 
-  notifyOnNewComment (comment: VideoCommentModel): void {
+  notifyOnNewComment (comment: MCommentOwnerVideo): void {
     this.notifyVideoOwnerOfNewComment(comment)
         .catch(err => logger.error('Cannot notify video owner of new comment %s.', comment.url, { err }))
 
@@ -62,37 +72,37 @@ class Notifier {
         .catch(err => logger.error('Cannot notify mentions of comment %s.', comment.url, { err }))
   }
 
-  notifyOnNewVideoAbuse (videoAbuse: VideoAbuseModel): void {
+  notifyOnNewVideoAbuse (videoAbuse: MVideoAbuseVideo): void {
     this.notifyModeratorsOfNewVideoAbuse(videoAbuse)
       .catch(err => logger.error('Cannot notify of new video abuse of video %s.', videoAbuse.Video.url, { err }))
   }
 
-  notifyOnVideoAutoBlacklist (video: VideoModel): void {
-    this.notifyModeratorsOfVideoAutoBlacklist(video)
-      .catch(err => logger.error('Cannot notify of auto-blacklist of video %s.', video.url, { err }))
+  notifyOnVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo): void {
+    this.notifyModeratorsOfVideoAutoBlacklist(videoBlacklist)
+      .catch(err => logger.error('Cannot notify of auto-blacklist of video %s.', videoBlacklist.Video.url, { err }))
   }
 
-  notifyOnVideoBlacklist (videoBlacklist: VideoBlacklistModel): void {
+  notifyOnVideoBlacklist (videoBlacklist: MVideoBlacklistVideo): void {
     this.notifyVideoOwnerOfBlacklist(videoBlacklist)
       .catch(err => logger.error('Cannot notify video owner of new video blacklist of %s.', videoBlacklist.Video.url, { err }))
   }
 
-  notifyOnVideoUnblacklist (video: VideoModel): void {
+  notifyOnVideoUnblacklist (video: MVideoFullLight): void {
     this.notifyVideoOwnerOfUnblacklist(video)
         .catch(err => logger.error('Cannot notify video owner of unblacklist of %s.', video.url, { err }))
   }
 
-  notifyOnFinishedVideoImport (videoImport: VideoImportModel, success: boolean): void {
+  notifyOnFinishedVideoImport (videoImport: MVideoImportVideo, success: boolean): void {
     this.notifyOwnerVideoImportIsFinished(videoImport, success)
       .catch(err => logger.error('Cannot notify owner that its video import %s is finished.', videoImport.getTargetIdentifier(), { err }))
   }
 
-  notifyOnNewUserRegistration (user: UserModel): void {
+  notifyOnNewUserRegistration (user: MUserDefault): void {
     this.notifyModeratorsOfNewUserRegistration(user)
         .catch(err => logger.error('Cannot notify moderators of new user registration (%s).', user.username, { err }))
   }
 
-  notifyOfNewUserFollow (actorFollow: ActorFollowModel): void {
+  notifyOfNewUserFollow (actorFollow: MActorFollowFull): void {
     this.notifyUserOfNewActorFollow(actorFollow)
       .catch(err => {
         logger.error(
@@ -104,25 +114,32 @@ class Notifier {
       })
   }
 
-  notifyOfNewInstanceFollow (actorFollow: ActorFollowModel): void {
+  notifyOfNewInstanceFollow (actorFollow: MActorFollowFull): void {
     this.notifyAdminsOfNewInstanceFollow(actorFollow)
         .catch(err => {
           logger.error('Cannot notify administrators of new follower %s.', actorFollow.ActorFollower.url, { err })
         })
   }
 
-  private async notifySubscribersOfNewVideo (video: VideoModel) {
+  notifyOfAutoInstanceFollowing (actorFollow: MActorFollowFull): void {
+    this.notifyAdminsOfAutoInstanceFollowing(actorFollow)
+        .catch(err => {
+          logger.error('Cannot notify administrators of auto instance following %s.', actorFollow.ActorFollowing.url, { err })
+        })
+  }
+
+  private async notifySubscribersOfNewVideo (video: MVideoAccountLight) {
     // List all followers that are users
     const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId)
 
     logger.info('Notifying %d users of new video %s.', users.length, video.url)
 
-    function settingGetter (user: UserModel) {
+    function settingGetter (user: MUserWithNotificationSetting) {
       return user.NotificationSetting.newVideoFromSubscription
     }
 
-    async function notificationCreator (user: UserModel) {
-      const notification = await UserNotificationModel.create({
+    async function notificationCreator (user: MUserWithNotificationSetting) {
+      const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
         type: UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION,
         userId: user.id,
         videoId: video.id
@@ -139,7 +156,7 @@ class Notifier {
     return this.notify({ users, settingGetter, notificationCreator, emailSender })
   }
 
-  private async notifyVideoOwnerOfNewComment (comment: VideoCommentModel) {
+  private async notifyVideoOwnerOfNewComment (comment: MCommentOwnerVideo) {
     if (comment.Video.isOwned() === false) return
 
     const user = await UserModel.loadByVideoId(comment.videoId)
@@ -152,12 +169,12 @@ class Notifier {
 
     logger.info('Notifying user %s of new comment %s.', user.username, comment.url)
 
-    function settingGetter (user: UserModel) {
+    function settingGetter (user: MUserWithNotificationSetting) {
       return user.NotificationSetting.newCommentOnMyVideo
     }
 
-    async function notificationCreator (user: UserModel) {
-      const notification = await UserNotificationModel.create({
+    async function notificationCreator (user: MUserWithNotificationSetting) {
+      const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
         type: UserNotificationType.NEW_COMMENT_ON_MY_VIDEO,
         userId: user.id,
         commentId: comment.id
@@ -174,7 +191,7 @@ class Notifier {
     return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender })
   }
 
-  private async notifyOfCommentMention (comment: VideoCommentModel) {
+  private async notifyOfCommentMention (comment: MCommentOwnerVideo) {
     const extractedUsernames = comment.extractMentions()
     logger.debug(
       'Extracted %d username from comment %s.', extractedUsernames.length, comment.url,
@@ -197,14 +214,14 @@ class Notifier {
 
     logger.info('Notifying %d users of new comment %s.', users.length, comment.url)
 
-    function settingGetter (user: UserModel) {
+    function settingGetter (user: MUserNotifSettingAccount) {
       if (accountMutedHash[user.Account.id] === true) return UserNotificationSettingValue.NONE
 
       return user.NotificationSetting.commentMention
     }
 
-    async function notificationCreator (user: UserModel) {
-      const notification = await UserNotificationModel.create({
+    async function notificationCreator (user: MUserNotifSettingAccount) {
+      const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
         type: UserNotificationType.COMMENT_MENTION,
         userId: user.id,
         commentId: comment.id
@@ -221,7 +238,7 @@ class Notifier {
     return this.notify({ users, settingGetter, notificationCreator, emailSender })
   }
 
-  private async notifyUserOfNewActorFollow (actorFollow: ActorFollowModel) {
+  private async notifyUserOfNewActorFollow (actorFollow: MActorFollowFull) {
     if (actorFollow.ActorFollowing.isOwned() === false) return
 
     // Account follows one of our account?
@@ -236,9 +253,6 @@ class Notifier {
 
     if (!user) return
 
-    if (!actorFollow.ActorFollower.Account || !actorFollow.ActorFollower.Account.name) {
-      actorFollow.ActorFollower.Account = await actorFollow.ActorFollower.$get('Account') as AccountModel
-    }
     const followerAccount = actorFollow.ActorFollower.Account
 
     const accountMuted = await AccountBlocklistModel.isAccountMutedBy(user.Account.id, followerAccount.id)
@@ -246,12 +260,12 @@ class Notifier {
 
     logger.info('Notifying user %s of new follower: %s.', user.username, followerAccount.getDisplayName())
 
-    function settingGetter (user: UserModel) {
+    function settingGetter (user: MUserWithNotificationSetting) {
       return user.NotificationSetting.newFollow
     }
 
-    async function notificationCreator (user: UserModel) {
-      const notification = await UserNotificationModel.create({
+    async function notificationCreator (user: MUserWithNotificationSetting) {
+      const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
         type: UserNotificationType.NEW_FOLLOW,
         userId: user.id,
         actorFollowId: actorFollow.id
@@ -268,17 +282,17 @@ class Notifier {
     return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender })
   }
 
-  private async notifyAdminsOfNewInstanceFollow (actorFollow: ActorFollowModel) {
+  private async notifyAdminsOfNewInstanceFollow (actorFollow: MActorFollowFull) {
     const admins = await UserModel.listWithRight(UserRight.MANAGE_SERVER_FOLLOW)
 
     logger.info('Notifying %d administrators of new instance follower: %s.', admins.length, actorFollow.ActorFollower.url)
 
-    function settingGetter (user: UserModel) {
+    function settingGetter (user: MUserWithNotificationSetting) {
       return user.NotificationSetting.newInstanceFollower
     }
 
-    async function notificationCreator (user: UserModel) {
-      const notification = await UserNotificationModel.create({
+    async function notificationCreator (user: MUserWithNotificationSetting) {
+      const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
         type: UserNotificationType.NEW_INSTANCE_FOLLOWER,
         userId: user.id,
         actorFollowId: actorFollow.id
@@ -295,18 +309,45 @@ class Notifier {
     return this.notify({ users: admins, settingGetter, notificationCreator, emailSender })
   }
 
-  private async notifyModeratorsOfNewVideoAbuse (videoAbuse: VideoAbuseModel) {
+  private async notifyAdminsOfAutoInstanceFollowing (actorFollow: MActorFollowFull) {
+    const admins = await UserModel.listWithRight(UserRight.MANAGE_SERVER_FOLLOW)
+
+    logger.info('Notifying %d administrators of auto instance following: %s.', admins.length, actorFollow.ActorFollowing.url)
+
+    function settingGetter (user: MUserWithNotificationSetting) {
+      return user.NotificationSetting.autoInstanceFollowing
+    }
+
+    async function notificationCreator (user: MUserWithNotificationSetting) {
+      const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
+        type: UserNotificationType.AUTO_INSTANCE_FOLLOWING,
+        userId: user.id,
+        actorFollowId: actorFollow.id
+      })
+      notification.ActorFollow = actorFollow
+
+      return notification
+    }
+
+    function emailSender (emails: string[]) {
+      return Emailer.Instance.addAutoInstanceFollowingNotification(emails, actorFollow)
+    }
+
+    return this.notify({ users: admins, settingGetter, notificationCreator, emailSender })
+  }
+
+  private async notifyModeratorsOfNewVideoAbuse (videoAbuse: MVideoAbuseVideo) {
     const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_ABUSES)
     if (moderators.length === 0) return
 
     logger.info('Notifying %s user/moderators of new video abuse %s.', moderators.length, videoAbuse.Video.url)
 
-    function settingGetter (user: UserModel) {
+    function settingGetter (user: MUserWithNotificationSetting) {
       return user.NotificationSetting.videoAbuseAsModerator
     }
 
-    async function notificationCreator (user: UserModel) {
-      const notification = await UserNotificationModel.create({
+    async function notificationCreator (user: MUserWithNotificationSetting) {
+      const notification: UserNotificationModelForApi = await UserNotificationModel.create<UserNotificationModelForApi>({
         type: UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS,
         userId: user.id,
         videoAbuseId: videoAbuse.id
@@ -323,46 +364,46 @@ class Notifier {
     return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
   }
 
-  private async notifyModeratorsOfVideoAutoBlacklist (video: VideoModel) {
+  private async notifyModeratorsOfVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo) {
     const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_BLACKLIST)
     if (moderators.length === 0) return
 
-    logger.info('Notifying %s moderators of video auto-blacklist %s.', moderators.length, video.url)
+    logger.info('Notifying %s moderators of video auto-blacklist %s.', moderators.length, videoBlacklist.Video.url)
 
-    function settingGetter (user: UserModel) {
+    function settingGetter (user: MUserWithNotificationSetting) {
       return user.NotificationSetting.videoAutoBlacklistAsModerator
     }
-    async function notificationCreator (user: UserModel) {
 
-      const notification = await UserNotificationModel.create({
+    async function notificationCreator (user: MUserWithNotificationSetting) {
+      const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
         type: UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS,
         userId: user.id,
-        videoId: video.id
+        videoBlacklistId: videoBlacklist.id
       })
-      notification.Video = video
+      notification.VideoBlacklist = videoBlacklist
 
       return notification
     }
 
     function emailSender (emails: string[]) {
-      return Emailer.Instance.addVideoAutoBlacklistModeratorsNotification(emails, video)
+      return Emailer.Instance.addVideoAutoBlacklistModeratorsNotification(emails, videoBlacklist)
     }
 
     return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
   }
 
-  private async notifyVideoOwnerOfBlacklist (videoBlacklist: VideoBlacklistModel) {
+  private async notifyVideoOwnerOfBlacklist (videoBlacklist: MVideoBlacklistVideo) {
     const user = await UserModel.loadByVideoId(videoBlacklist.videoId)
     if (!user) return
 
     logger.info('Notifying user %s that its video %s has been blacklisted.', user.username, videoBlacklist.Video.url)
 
-    function settingGetter (user: UserModel) {
+    function settingGetter (user: MUserWithNotificationSetting) {
       return user.NotificationSetting.blacklistOnMyVideo
     }
 
-    async function notificationCreator (user: UserModel) {
-      const notification = await UserNotificationModel.create({
+    async function notificationCreator (user: MUserWithNotificationSetting) {
+      const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
         type: UserNotificationType.BLACKLIST_ON_MY_VIDEO,
         userId: user.id,
         videoBlacklistId: videoBlacklist.id
@@ -379,18 +420,18 @@ class Notifier {
     return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender })
   }
 
-  private async notifyVideoOwnerOfUnblacklist (video: VideoModel) {
+  private async notifyVideoOwnerOfUnblacklist (video: MVideoFullLight) {
     const user = await UserModel.loadByVideoId(video.id)
     if (!user) return
 
     logger.info('Notifying user %s that its video %s has been unblacklisted.', user.username, video.url)
 
-    function settingGetter (user: UserModel) {
+    function settingGetter (user: MUserWithNotificationSetting) {
       return user.NotificationSetting.blacklistOnMyVideo
     }
 
-    async function notificationCreator (user: UserModel) {
-      const notification = await UserNotificationModel.create({
+    async function notificationCreator (user: MUserWithNotificationSetting) {
+      const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
         type: UserNotificationType.UNBLACKLIST_ON_MY_VIDEO,
         userId: user.id,
         videoId: video.id
@@ -407,18 +448,18 @@ class Notifier {
     return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender })
   }
 
-  private async notifyOwnedVideoHasBeenPublished (video: VideoModel) {
+  private async notifyOwnedVideoHasBeenPublished (video: MVideoFullLight) {
     const user = await UserModel.loadByVideoId(video.id)
     if (!user) return
 
     logger.info('Notifying user %s of the publication of its video %s.', user.username, video.url)
 
-    function settingGetter (user: UserModel) {
+    function settingGetter (user: MUserWithNotificationSetting) {
       return user.NotificationSetting.myVideoPublished
     }
 
-    async function notificationCreator (user: UserModel) {
-      const notification = await UserNotificationModel.create({
+    async function notificationCreator (user: MUserWithNotificationSetting) {
+      const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
         type: UserNotificationType.MY_VIDEO_PUBLISHED,
         userId: user.id,
         videoId: video.id
@@ -435,18 +476,18 @@ class Notifier {
     return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender })
   }
 
-  private async notifyOwnerVideoImportIsFinished (videoImport: VideoImportModel, success: boolean) {
+  private async notifyOwnerVideoImportIsFinished (videoImport: MVideoImportVideo, success: boolean) {
     const user = await UserModel.loadByVideoImportId(videoImport.id)
     if (!user) return
 
     logger.info('Notifying user %s its video import %s is finished.', user.username, videoImport.getTargetIdentifier())
 
-    function settingGetter (user: UserModel) {
+    function settingGetter (user: MUserWithNotificationSetting) {
       return user.NotificationSetting.myVideoImportFinished
     }
 
-    async function notificationCreator (user: UserModel) {
-      const notification = await UserNotificationModel.create({
+    async function notificationCreator (user: MUserWithNotificationSetting) {
+      const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
         type: success ? UserNotificationType.MY_VIDEO_IMPORT_SUCCESS : UserNotificationType.MY_VIDEO_IMPORT_ERROR,
         userId: user.id,
         videoImportId: videoImport.id
@@ -465,21 +506,21 @@ class Notifier {
     return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender })
   }
 
-  private async notifyModeratorsOfNewUserRegistration (registeredUser: UserModel) {
+  private async notifyModeratorsOfNewUserRegistration (registeredUser: MUserDefault) {
     const moderators = await UserModel.listWithRight(UserRight.MANAGE_USERS)
     if (moderators.length === 0) return
 
     logger.info(
       'Notifying %s moderators of new user registration of %s.',
-      moderators.length, registeredUser.Account.Actor.preferredUsername
+      moderators.length, registeredUser.username
     )
 
-    function settingGetter (user: UserModel) {
+    function settingGetter (user: MUserWithNotificationSetting) {
       return user.NotificationSetting.newUserRegistration
     }
 
-    async function notificationCreator (user: UserModel) {
-      const notification = await UserNotificationModel.create({
+    async function notificationCreator (user: MUserWithNotificationSetting) {
+      const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
         type: UserNotificationType.NEW_USER_REGISTRATION,
         userId: user.id,
         accountId: registeredUser.Account.id
@@ -496,11 +537,11 @@ class Notifier {
     return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
   }
 
-  private async notify (options: {
-    users: UserModel[],
-    notificationCreator: (user: UserModel) => Promise<UserNotificationModel>,
+  private async notify <T extends MUserWithNotificationSetting> (options: {
+    users: T[],
+    notificationCreator: (user: T) => Promise<UserNotificationModelForApi>,
     emailSender: (emails: string[]) => Promise<any> | Bluebird<any>,
-    settingGetter: (user: UserModel) => UserNotificationSettingValue
+    settingGetter: (user: T) => UserNotificationSettingValue
   }) {
     const emails: string[] = []
 
@@ -521,7 +562,7 @@ class Notifier {
     }
   }
 
-  private isEmailEnabled (user: UserModel, value: UserNotificationSettingValue) {
+  private isEmailEnabled (user: MUser, value: UserNotificationSettingValue) {
     if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION === true && user.emailVerified === false) return false
 
     return value & UserNotificationSettingValue.EMAIL
index a1153e88a73822f83738e51dffb3a3acccda1775..086856f41e3a0fc64469f450e6b8044f8a694ba2 100644 (file)
@@ -8,10 +8,11 @@ import { LRU_CACHE } from '../initializers/constants'
 import { Transaction } from 'sequelize'
 import { CONFIG } from '../initializers/config'
 import * as LRUCache from 'lru-cache'
+import { MOAuthTokenUser } from '@server/typings/models/oauth/oauth-token'
 
 type TokenInfo = { accessToken: string, refreshToken: string, accessTokenExpiresAt: Date, refreshTokenExpiresAt: Date }
 
-const accessTokenCache = new LRUCache<string, OAuthTokenModel>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE })
+const accessTokenCache = new LRUCache<string, MOAuthTokenUser>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE })
 const userHavingToken = new LRUCache<number, string>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE })
 
 // ---------------------------------------------------------------------------
index 17748fd1813186509ab5c63a54ebfc8771475703..26ced351f3333cf1dfe756dc333ad95653c9ff88 100644 (file)
@@ -1,8 +1,8 @@
 import * as SocketIO from 'socket.io'
 import { authenticateSocket } from '../middlewares'
-import { UserNotificationModel } from '../models/account/user-notification'
 import { logger } from '../helpers/logger'
 import { Server } from 'http'
+import { UserNotificationModelForApi } from '@server/typings/models/user'
 
 class PeerTubeSocket {
 
@@ -34,13 +34,14 @@ class PeerTubeSocket {
       })
   }
 
-  sendNotification (userId: number, notification: UserNotificationModel) {
+  sendNotification (userId: number, notification: UserNotificationModelForApi) {
     const sockets = this.userNotificationSockets[userId]
 
     if (!sockets) return
 
+    const notificationMessage = notification.toFormattedJSON()
     for (const socket of sockets) {
-      socket.emit('new-notification', notification.toFormattedJSON())
+      socket.emit('new-notification', notificationMessage)
     }
   }
 
index 444162a0328ac1e1aa415a549f9bc571b1e66643..8127992b59315d45a727a9608e26b07c525b2437 100644 (file)
@@ -222,9 +222,8 @@ export class PluginManager implements ServerHook {
       const pluginName = PluginModel.normalizePluginName(npmName)
 
       const packageJSON = await this.getPackageJSON(pluginName, pluginType)
-      if (!isPackageJSONValid(packageJSON, pluginType)) {
-        throw new Error('PackageJSON is invalid.')
-      }
+
+      this.sanitizeAndCheckPackageJSONOrThrow(packageJSON, pluginType);
 
       [ plugin ] = await PluginModel.upsert({
         name: pluginName,
@@ -301,9 +300,7 @@ export class PluginManager implements ServerHook {
     const packageJSON = await this.getPackageJSON(plugin.name, plugin.type)
     const pluginPath = this.getPluginPath(plugin.name, plugin.type)
 
-    if (!isPackageJSONValid(packageJSON, plugin.type)) {
-      throw new Error('Package.JSON is invalid.')
-    }
+    this.sanitizeAndCheckPackageJSONOrThrow(packageJSON, plugin.type)
 
     let library: PluginLibrary
     if (plugin.type === PluginType.PLUGIN) {
@@ -598,6 +595,21 @@ export class PluginManager implements ServerHook {
     }
   }
 
+  private sanitizeAndCheckPackageJSONOrThrow (packageJSON: PluginPackageJson, pluginType: PluginType) {
+    if (!packageJSON.staticDirs) packageJSON.staticDirs = {}
+    if (!packageJSON.css) packageJSON.css = []
+    if (!packageJSON.clientScripts) packageJSON.clientScripts = []
+    if (!packageJSON.translations) packageJSON.translations = {}
+
+    const { result: packageJSONValid, badFields } = isPackageJSONValid(packageJSON, pluginType)
+    if (!packageJSONValid) {
+      const formattedFields = badFields.map(f => `"${f}"`)
+                              .join(', ')
+
+      throw new Error(`PackageJSON is invalid (invalid fields: ${formattedFields}).`)
+    }
+  }
+
   static get Instance () {
     return this.instance || (this.instance = new this())
   }
index 04d3ded8fabec8b0bf0f0b171c01257875510744..1b4ecd7c04875d6abc3e98475a3fc14a257fc373 100644 (file)
@@ -2,8 +2,9 @@ import { VideoRedundancyModel } from '../models/redundancy/video-redundancy'
 import { sendUndoCacheFile } from './activitypub/send'
 import { Transaction } from 'sequelize'
 import { getServerActor } from '../helpers/utils'
+import { MVideoRedundancyVideo } from '@server/typings/models'
 
-async function removeVideoRedundancy (videoRedundancy: VideoRedundancyModel, t?: Transaction) {
+async function removeVideoRedundancy (videoRedundancy: MVideoRedundancyVideo, t?: Transaction) {
   const serverActor = await getServerActor()
 
   // 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 (file)
index 0000000..ef11fc8
--- /dev/null
@@ -0,0 +1,72 @@
+import { logger } from '../../helpers/logger'
+import { AbstractScheduler } from './abstract-scheduler'
+import { INSTANCES_INDEX, SCHEDULER_INTERVALS_MS, SERVER_ACTOR_NAME } from '../../initializers/constants'
+import { CONFIG } from '../../initializers/config'
+import { chunk } from 'lodash'
+import { doRequest } from '@server/helpers/requests'
+import { ActorFollowModel } from '@server/models/activitypub/actor-follow'
+import { JobQueue } from '@server/lib/job-queue'
+import { getServerActor } from '@server/helpers/utils'
+
+export class AutoFollowIndexInstances extends AbstractScheduler {
+
+  private static instance: AbstractScheduler
+
+  protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.autoFollowIndexInstances
+
+  private lastCheck: Date
+
+  private constructor () {
+    super()
+  }
+
+  protected async internalExecute () {
+    return this.autoFollow()
+  }
+
+  private async autoFollow () {
+    if (CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.ENABLED === false) return
+
+    const indexUrl = CONFIG.FOLLOWINGS.INSTANCE.AUTO_FOLLOW_INDEX.INDEX_URL
+
+    logger.info('Auto follow instances of index %s.', indexUrl)
+
+    try {
+      const serverActor = await getServerActor()
+
+      const uri = indexUrl + INSTANCES_INDEX.HOSTS_PATH
+
+      const qs = this.lastCheck ? { since: this.lastCheck.toISOString() } : {}
+      this.lastCheck = new Date()
+
+      const { body } = await doRequest({ uri, qs, json: true })
+
+      const hosts: string[] = body.data.map(o => o.host)
+      const chunks = chunk(hosts, 20)
+
+      for (const chunk of chunks) {
+        const unfollowedHosts = await ActorFollowModel.keepUnfollowedInstance(chunk)
+
+        for (const unfollowedHost of unfollowedHosts) {
+          const payload = {
+            host: unfollowedHost,
+            name: SERVER_ACTOR_NAME,
+            followerActorId: serverActor.id,
+            isAutoFollow: true
+          }
+
+          await JobQueue.Instance.createJob({ type: 'activitypub-follow', payload })
+                  .catch(err => logger.error('Cannot create follow job for %s.', unfollowedHost, err))
+        }
+      }
+
+    } catch (err) {
+      logger.error('Cannot auto follow hosts of index %s.', indexUrl, { err })
+    }
+
+  }
+
+  static get Instance () {
+    return this.instance || (this.instance = new this())
+  }
+}
index 5f4aad66e914bf40aac591fd1d5950a9b5d35f55..1e30f6ebc2b6ef3e1bce558b8d14434f384848a1 100644 (file)
@@ -3,7 +3,6 @@ import { HLS_REDUNDANCY_DIRECTORY, REDUNDANCY, VIDEO_IMPORT_TIMEOUT, WEBSERVER }
 import { logger } from '../../helpers/logger'
 import { VideosRedundancy } from '../../../shared/models/redundancy'
 import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy'
-import { VideoFileModel } from '../../models/video/video-file'
 import { downloadWebTorrentVideo } from '../../helpers/webtorrent'
 import { join } from 'path'
 import { move } from 'fs-extra'
@@ -12,16 +11,31 @@ import { sendCreateCacheFile, sendUpdateCacheFile } from '../activitypub/send'
 import { getVideoCacheFileActivityPubUrl, getVideoCacheStreamingPlaylistActivityPubUrl } from '../activitypub/url'
 import { removeVideoRedundancy } from '../redundancy'
 import { getOrCreateVideoAndAccountAndChannel } from '../activitypub'
-import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist'
-import { VideoModel } from '../../models/video/video'
 import { downloadPlaylistSegments } from '../hls'
 import { CONFIG } from '../../initializers/config'
+import {
+  MStreamingPlaylist,
+  MStreamingPlaylistVideo,
+  MVideoAccountLight,
+  MVideoFile,
+  MVideoFileVideo,
+  MVideoRedundancyFileVideo,
+  MVideoRedundancyStreamingPlaylistVideo,
+  MVideoRedundancyVideo,
+  MVideoWithAllFiles
+} from '@server/typings/models'
 
 type CandidateToDuplicate = {
   redundancy: VideosRedundancy,
-  video: VideoModel,
-  files: VideoFileModel[],
-  streamingPlaylists: VideoStreamingPlaylistModel[]
+  video: MVideoWithAllFiles,
+  files: MVideoFile[],
+  streamingPlaylists: MStreamingPlaylist[]
+}
+
+function isMVideoRedundancyFileVideo (
+  o: MVideoRedundancyFileVideo | MVideoRedundancyStreamingPlaylistVideo
+): o is MVideoRedundancyFileVideo {
+  return !!(o as MVideoRedundancyFileVideo).VideoFile
 }
 
 export class VideosRedundancyScheduler extends AbstractScheduler {
@@ -102,7 +116,7 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
     }
   }
 
-  private async extendsRedundancy (redundancyModel: VideoRedundancyModel) {
+  private async extendsRedundancy (redundancyModel: MVideoRedundancyVideo) {
     const redundancy = CONFIG.REDUNDANCY.VIDEOS.STRATEGIES.find(s => s.strategy === redundancyModel.strategy)
     // Redundancy strategy disabled, remove our redundancy instead of extending expiration
     if (!redundancy) {
@@ -172,7 +186,8 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
     }
   }
 
-  private async createVideoFileRedundancy (redundancy: VideosRedundancy, video: VideoModel, file: VideoFileModel) {
+  private async createVideoFileRedundancy (redundancy: VideosRedundancy, video: MVideoAccountLight, fileArg: MVideoFile) {
+    const file = fileArg as MVideoFileVideo
     file.Video = video
 
     const serverActor = await getServerActor()
@@ -187,7 +202,7 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
     const destPath = join(CONFIG.STORAGE.REDUNDANCY_DIR, video.getVideoFilename(file))
     await move(tmpPath, destPath, { overwrite: true })
 
-    const createdModel = await VideoRedundancyModel.create({
+    const createdModel: MVideoRedundancyFileVideo = await VideoRedundancyModel.create({
       expiresOn: this.buildNewExpiration(redundancy.minLifetime),
       url: getVideoCacheFileActivityPubUrl(file),
       fileUrl: video.getVideoRedundancyUrl(file, WEBSERVER.URL),
@@ -203,7 +218,12 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
     logger.info('Duplicated %s - %d -> %s.', video.url, file.resolution, createdModel.url)
   }
 
-  private async createStreamingPlaylistRedundancy (redundancy: VideosRedundancy, video: VideoModel, playlist: VideoStreamingPlaylistModel) {
+  private async createStreamingPlaylistRedundancy (
+    redundancy: VideosRedundancy,
+    video: MVideoAccountLight,
+    playlistArg: MStreamingPlaylist
+  ) {
+    const playlist = playlistArg as MStreamingPlaylistVideo
     playlist.Video = video
 
     const serverActor = await getServerActor()
@@ -213,7 +233,7 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
     const destDirectory = join(HLS_REDUNDANCY_DIRECTORY, video.uuid)
     await downloadPlaylistSegments(playlist.playlistUrl, destDirectory, VIDEO_IMPORT_TIMEOUT)
 
-    const createdModel = await VideoRedundancyModel.create({
+    const createdModel: MVideoRedundancyStreamingPlaylistVideo = await VideoRedundancyModel.create({
       expiresOn: this.buildNewExpiration(redundancy.minLifetime),
       url: getVideoCacheStreamingPlaylistActivityPubUrl(video, playlist),
       fileUrl: playlist.getVideoRedundancyUrl(WEBSERVER.URL),
@@ -229,7 +249,7 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
     logger.info('Duplicated playlist %s -> %s.', playlist.playlistUrl, createdModel.url)
   }
 
-  private async extendsExpirationOf (redundancy: VideoRedundancyModel, expiresAfterMs: number) {
+  private async extendsExpirationOf (redundancy: MVideoRedundancyVideo, expiresAfterMs: number) {
     logger.info('Extending expiration of %s.', redundancy.url)
 
     const serverActor = await getServerActor()
@@ -243,7 +263,7 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
   private async purgeCacheIfNeeded (candidateToDuplicate: CandidateToDuplicate) {
     while (await this.isTooHeavy(candidateToDuplicate)) {
       const redundancy = candidateToDuplicate.redundancy
-      const toDelete = await VideoRedundancyModel.loadOldestLocalThatAlreadyExpired(redundancy.strategy, redundancy.minLifetime)
+      const toDelete = await VideoRedundancyModel.loadOldestLocalExpired(redundancy.strategy, redundancy.minLifetime)
       if (!toDelete) return
 
       await removeVideoRedundancy(toDelete)
@@ -263,19 +283,18 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
     return new Date(Date.now() + expiresAfterMs)
   }
 
-  private buildEntryLogId (object: VideoRedundancyModel) {
-    if (object.VideoFile) return `${object.VideoFile.Video.url}-${object.VideoFile.resolution}`
+  private buildEntryLogId (object: MVideoRedundancyFileVideo | MVideoRedundancyStreamingPlaylistVideo) {
+    if (isMVideoRedundancyFileVideo(object)) return `${object.VideoFile.Video.url}-${object.VideoFile.resolution}`
 
     return `${object.VideoStreamingPlaylist.playlistUrl}`
   }
 
-  private getTotalFileSizes (files: VideoFileModel[], playlists: VideoStreamingPlaylistModel[]) {
-    const fileReducer = (previous: number, current: VideoFileModel) => previous + current.size
+  private getTotalFileSizes (files: MVideoFile[], playlists: MStreamingPlaylist[]) {
+    const fileReducer = (previous: number, current: MVideoFile) => previous + current.size
 
     const totalSize = files.reduce(fileReducer, 0)
-    if (playlists.length === 0) return totalSize
 
-    return totalSize * playlists.length
+    return totalSize + (totalSize * playlists.length)
   }
 
   private async loadAndRefreshVideo (videoUrl: string) {
index a59773f5a5512f086dd8159855274a2108e8de63..84791955e0cc00c908b9882dae22377b93dd9188 100644 (file)
@@ -1,20 +1,20 @@
-import { VideoFileModel } from '../models/video/video-file'
 import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils'
 import { CONFIG } from '../initializers/config'
-import { PREVIEWS_SIZE, THUMBNAILS_SIZE, ASSETS_PATH } from '../initializers/constants'
-import { VideoModel } from '../models/video/video'
+import { ASSETS_PATH, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants'
 import { ThumbnailModel } from '../models/video/thumbnail'
 import { ThumbnailType } from '../../shared/models/videos/thumbnail.type'
 import { processImage } from '../helpers/image-utils'
 import { join } from 'path'
 import { downloadImage } from '../helpers/requests'
-import { VideoPlaylistModel } from '../models/video/video-playlist'
+import { MVideoPlaylistThumbnail } from '../typings/models/video/video-playlist'
+import { MVideoFile, MVideoThumbnail } from '../typings/models'
+import { MThumbnail } from '../typings/models/video/thumbnail'
 
 type ImageSize = { height: number, width: number }
 
 function createPlaylistMiniatureFromExisting (
   inputPath: string,
-  playlist: VideoPlaylistModel,
+  playlist: MVideoPlaylistThumbnail,
   automaticallyGenerated: boolean,
   keepOriginal = false,
   size?: ImageSize
@@ -26,7 +26,7 @@ function createPlaylistMiniatureFromExisting (
   return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, automaticallyGenerated, existingThumbnail })
 }
 
-function createPlaylistMiniatureFromUrl (fileUrl: string, playlist: VideoPlaylistModel, size?: ImageSize) {
+function createPlaylistMiniatureFromUrl (fileUrl: string, playlist: MVideoPlaylistThumbnail, size?: ImageSize) {
   const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromPlaylist(playlist, size)
   const type = ThumbnailType.MINIATURE
 
@@ -34,7 +34,7 @@ function createPlaylistMiniatureFromUrl (fileUrl: string, playlist: VideoPlaylis
   return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl })
 }
 
-function createVideoMiniatureFromUrl (fileUrl: string, video: VideoModel, type: ThumbnailType, size?: ImageSize) {
+function createVideoMiniatureFromUrl (fileUrl: string, video: MVideoThumbnail, type: ThumbnailType, size?: ImageSize) {
   const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
   const thumbnailCreator = () => downloadImage(fileUrl, basePath, filename, { width, height })
 
@@ -43,7 +43,7 @@ function createVideoMiniatureFromUrl (fileUrl: string, video: VideoModel, type:
 
 function createVideoMiniatureFromExisting (
   inputPath: string,
-  video: VideoModel,
+  video: MVideoThumbnail,
   type: ThumbnailType,
   automaticallyGenerated: boolean,
   size?: ImageSize
@@ -54,7 +54,7 @@ function createVideoMiniatureFromExisting (
   return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, automaticallyGenerated, existingThumbnail })
 }
 
-function generateVideoMiniature (video: VideoModel, videoFile: VideoFileModel, type: ThumbnailType) {
+function generateVideoMiniature (video: MVideoThumbnail, videoFile: MVideoFile, type: ThumbnailType) {
   const input = video.getVideoFilePath(videoFile)
 
   const { filename, basePath, height, width, existingThumbnail, outputPath } = buildMetadataFromVideo(video, type)
@@ -65,7 +65,7 @@ function generateVideoMiniature (video: VideoModel, videoFile: VideoFileModel, t
   return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, automaticallyGenerated: true, existingThumbnail })
 }
 
-function createPlaceholderThumbnail (fileUrl: string, video: VideoModel, type: ThumbnailType, size: ImageSize) {
+function createPlaceholderThumbnail (fileUrl: string, video: MVideoThumbnail, type: ThumbnailType, size: ImageSize) {
   const { filename, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
 
   const thumbnail = existingThumbnail ? existingThumbnail : new ThumbnailModel()
@@ -90,7 +90,7 @@ export {
   createPlaylistMiniatureFromExisting
 }
 
-function buildMetadataFromPlaylist (playlist: VideoPlaylistModel, size: ImageSize) {
+function buildMetadataFromPlaylist (playlist: MVideoPlaylistThumbnail, size: ImageSize) {
   const filename = playlist.generateThumbnailName()
   const basePath = CONFIG.STORAGE.THUMBNAILS_DIR
 
@@ -104,7 +104,7 @@ function buildMetadataFromPlaylist (playlist: VideoPlaylistModel, size: ImageSiz
   }
 }
 
-function buildMetadataFromVideo (video: VideoModel, type: ThumbnailType, size?: ImageSize) {
+function buildMetadataFromVideo (video: MVideoThumbnail, type: ThumbnailType, size?: ImageSize) {
   const existingThumbnail = Array.isArray(video.Thumbnails)
     ? video.Thumbnails.find(t => t.type === type)
     : undefined
@@ -148,7 +148,7 @@ async function createThumbnailFromFunction (parameters: {
   type: ThumbnailType,
   automaticallyGenerated?: boolean,
   fileUrl?: string,
-  existingThumbnail?: ThumbnailModel
+  existingThumbnail?: MThumbnail
 }) {
   const { thumbnailCreator, filename, width, height, type, existingThumbnail, automaticallyGenerated = null, fileUrl = null } = parameters
 
index 0e40077707fc823a8719674a891437821e004def..c45438d95b0784ec96952df92a2974f5ae908be9 100644 (file)
@@ -2,10 +2,8 @@ import * as uuidv4 from 'uuid/v4'
 import { ActivityPubActorType } from '../../shared/models/activitypub'
 import { SERVER_ACTOR_NAME, WEBSERVER } from '../initializers/constants'
 import { AccountModel } from '../models/account/account'
-import { UserModel } from '../models/account/user'
 import { buildActorInstance, getAccountActivityPubUrl, setAsyncActorKeys } from './activitypub'
-import { createVideoChannel } from './video-channel'
-import { VideoChannelModel } from '../models/video/video-channel'
+import { createLocalVideoChannel } from './video-channel'
 import { ActorModel } from '../models/activitypub/actor'
 import { UserNotificationSettingModel } from '../models/account/user-notification-setting'
 import { UserNotificationSetting, UserNotificationSettingValue } from '../../shared/models/users'
@@ -14,14 +12,17 @@ import { sequelizeTypescript } from '../initializers/database'
 import { Transaction } from 'sequelize/types'
 import { Redis } from './redis'
 import { Emailer } from './emailer'
+import { MAccountDefault, MActorDefault, MChannelActor } from '../typings/models'
+import { MUser, MUserDefault, MUserId } from '../typings/models/user'
 
 type ChannelNames = { name: string, displayName: string }
+
 async function createUserAccountAndChannelAndPlaylist (parameters: {
-  userToCreate: UserModel,
+  userToCreate: MUser,
   userDisplayName?: string,
   channelNames?: ChannelNames,
   validateUser?: boolean
-}) {
+}): Promise<{ user: MUserDefault, account: MAccountDefault, videoChannel: MChannelActor }> {
   const { userToCreate, userDisplayName, channelNames, validateUser = true } = parameters
 
   const { user, account, videoChannel } = await sequelizeTypescript.transaction(async t => {
@@ -30,7 +31,7 @@ async function createUserAccountAndChannelAndPlaylist (parameters: {
       validate: validateUser
     }
 
-    const userCreated = await userToCreate.save(userOptions)
+    const userCreated: MUserDefault = await userToCreate.save(userOptions)
     userCreated.NotificationSetting = await createDefaultUserNotificationSettings(userCreated, t)
 
     const accountCreated = await createLocalAccountWithoutKeys({
@@ -43,22 +44,22 @@ async function createUserAccountAndChannelAndPlaylist (parameters: {
     userCreated.Account = accountCreated
 
     const channelAttributes = await buildChannelAttributes(userCreated, channelNames)
-    const videoChannel = await createVideoChannel(channelAttributes, accountCreated, t)
+    const videoChannel = await createLocalVideoChannel(channelAttributes, accountCreated, t)
 
     const videoPlaylist = await createWatchLaterPlaylist(accountCreated, t)
 
     return { user: userCreated, account: accountCreated, videoChannel, videoPlaylist }
   })
 
-  const [ accountKeys, channelKeys ] = await Promise.all([
+  const [ accountActorWithKeys, channelActorWithKeys ] = await Promise.all([
     setAsyncActorKeys(account.Actor),
     setAsyncActorKeys(videoChannel.Actor)
   ])
 
-  account.Actor = accountKeys
-  videoChannel.Actor = channelKeys
+  account.Actor = accountActorWithKeys
+  videoChannel.Actor = channelActorWithKeys
 
-  return { user, account, videoChannel } as { user: UserModel, account: AccountModel, videoChannel: VideoChannelModel }
+  return { user, account, videoChannel }
 }
 
 async function createLocalAccountWithoutKeys (parameters: {
@@ -73,7 +74,7 @@ async function createLocalAccountWithoutKeys (parameters: {
   const url = getAccountActivityPubUrl(name)
 
   const actorInstance = buildActorInstance(type, url, name)
-  const actorInstanceCreated = await actorInstance.save({ transaction: t })
+  const actorInstanceCreated: MActorDefault = await actorInstance.save({ transaction: t })
 
   const accountInstance = new AccountModel({
     name: displayName || name,
@@ -82,7 +83,7 @@ async function createLocalAccountWithoutKeys (parameters: {
     actorId: actorInstanceCreated.id
   })
 
-  const accountInstanceCreated = await accountInstance.save({ transaction: t })
+  const accountInstanceCreated: MAccountDefault = await accountInstance.save({ transaction: t })
   accountInstanceCreated.Actor = actorInstanceCreated
 
   return accountInstanceCreated
@@ -102,7 +103,7 @@ async function createApplicationActor (applicationId: number) {
   return accountCreated
 }
 
-async function sendVerifyUserEmail (user: UserModel, isPendingEmail = false) {
+async function sendVerifyUserEmail (user: MUser, isPendingEmail = false) {
   const verificationString = await Redis.Instance.setVerifyEmailVerificationString(user.id)
   let url = WEBSERVER.URL + '/verify-account/email?userId=' + user.id + '&verificationString=' + verificationString
 
@@ -124,7 +125,7 @@ export {
 
 // ---------------------------------------------------------------------------
 
-function createDefaultUserNotificationSettings (user: UserModel, t: Transaction | undefined) {
+function createDefaultUserNotificationSettings (user: MUserId, t: Transaction | undefined) {
   const values: UserNotificationSetting & { userId: number } = {
     userId: user.id,
     newVideoFromSubscription: UserNotificationSettingValue.WEB,
@@ -137,13 +138,14 @@ function createDefaultUserNotificationSettings (user: UserModel, t: Transaction
     newUserRegistration: UserNotificationSettingValue.WEB,
     commentMention: UserNotificationSettingValue.WEB,
     newFollow: UserNotificationSettingValue.WEB,
-    newInstanceFollower: UserNotificationSettingValue.WEB
+    newInstanceFollower: UserNotificationSettingValue.WEB,
+    autoInstanceFollowing: UserNotificationSettingValue.WEB
   }
 
   return UserNotificationSettingModel.create(values, { transaction: t })
 }
 
-async function buildChannelAttributes (user: UserModel, channelNames?: ChannelNames) {
+async function buildChannelAttributes (user: MUser, channelNames?: ChannelNames) {
   if (channelNames) return channelNames
 
   let channelName = user.username + '_channel'
index bdaecd8e24bef5351c9a582e22b77691479c24e0..1dd45b76d9d1fe742c52d4beef611aa991f357e6 100644 (file)
@@ -2,16 +2,15 @@ import { Transaction } from 'sequelize'
 import { CONFIG } from '../initializers/config'
 import { UserRight, VideoBlacklistType } from '../../shared/models'
 import { VideoBlacklistModel } from '../models/video/video-blacklist'
-import { UserModel } from '../models/account/user'
-import { VideoModel } from '../models/video/video'
 import { logger } from '../helpers/logger'
 import { UserAdminFlag } from '../../shared/models/users/user-flag.model'
 import { Hooks } from './plugins/hooks'
 import { Notifier } from './notifier'
+import { MUser, MVideoBlacklistVideo, MVideoWithBlacklistLight } from '@server/typings/models'
 
 async function autoBlacklistVideoIfNeeded (parameters: {
-  video: VideoModel,
-  user?: UserModel,
+  video: MVideoWithBlacklistLight,
+  user?: MUser,
   isRemote: boolean,
   isNew: boolean,
   notify?: boolean,
@@ -32,7 +31,7 @@ async function autoBlacklistVideoIfNeeded (parameters: {
     reason: 'Auto-blacklisted. Moderator review required.',
     type: VideoBlacklistType.AUTO_BEFORE_PUBLISHED
   }
-  const [ videoBlacklist ] = await VideoBlacklistModel.findOrCreate({
+  const [ videoBlacklist ] = await VideoBlacklistModel.findOrCreate<MVideoBlacklistVideo>({
     where: {
       videoId: video.id
     },
@@ -41,7 +40,9 @@ async function autoBlacklistVideoIfNeeded (parameters: {
   })
   video.VideoBlacklist = videoBlacklist
 
-  if (notify) Notifier.Instance.notifyOnVideoAutoBlacklist(video)
+  videoBlacklist.Video = video
+
+  if (notify) Notifier.Instance.notifyOnVideoAutoBlacklist(videoBlacklist)
 
   logger.info('Video %s auto-blacklisted.', video.uuid)
 
@@ -49,10 +50,10 @@ async function autoBlacklistVideoIfNeeded (parameters: {
 }
 
 async function autoBlacklistNeeded (parameters: {
-  video: VideoModel,
+  video: MVideoWithBlacklistLight,
   isRemote: boolean,
   isNew: boolean,
-  user?: UserModel
+  user?: MUser
 }) {
   const { user, video, isRemote, isNew } = parameters
 
index ee0482c3612919b3715eb64aca12e59aa178ef0d..41eab456bb7491e560c25297be3d443978516ead 100644 (file)
@@ -1,12 +1,19 @@
 import * as Sequelize from 'sequelize'
 import * as uuidv4 from 'uuid/v4'
 import { VideoChannelCreate } from '../../shared/models'
-import { AccountModel } from '../models/account/account'
 import { VideoChannelModel } from '../models/video/video-channel'
 import { buildActorInstance, federateVideoIfNeeded, getVideoChannelActivityPubUrl } from './activitypub'
 import { VideoModel } from '../models/video/video'
+import { MAccountId, MChannelDefault, MChannelId } from '../typings/models'
 
-async function createVideoChannel (videoChannelInfo: VideoChannelCreate, account: AccountModel, t: Sequelize.Transaction) {
+type CustomVideoChannelModelAccount <T extends MAccountId> = MChannelDefault &
+  { Account?: T }
+
+async function createLocalVideoChannel <T extends MAccountId> (
+  videoChannelInfo: VideoChannelCreate,
+  account: T,
+  t: Sequelize.Transaction
+): Promise<CustomVideoChannelModelAccount<T>> {
   const uuid = uuidv4()
   const url = getVideoChannelActivityPubUrl(videoChannelInfo.name)
   const actorInstance = buildActorInstance('Group', url, videoChannelInfo.name, uuid)
@@ -21,10 +28,10 @@ async function createVideoChannel (videoChannelInfo: VideoChannelCreate, account
     actorId: actorInstanceCreated.id
   }
 
-  const videoChannel = VideoChannelModel.build(videoChannelData)
+  const videoChannel = new VideoChannelModel(videoChannelData)
 
   const options = { transaction: t }
-  const videoChannelCreated = await videoChannel.save(options)
+  const videoChannelCreated: CustomVideoChannelModelAccount<T> = await videoChannel.save(options) as MChannelDefault
 
   // Do not forget to add Account/Actor information to the created video channel
   videoChannelCreated.Account = account
@@ -34,7 +41,7 @@ async function createVideoChannel (videoChannelInfo: VideoChannelCreate, account
   return videoChannelCreated
 }
 
-async function federateAllVideosOfChannel (videoChannel: VideoChannelModel) {
+async function federateAllVideosOfChannel (videoChannel: MChannelId) {
   const videoIds = await VideoModel.getAllIdsFromChannel(videoChannel)
 
   for (const videoId of videoIds) {
@@ -47,6 +54,6 @@ async function federateAllVideosOfChannel (videoChannel: VideoChannelModel) {
 // ---------------------------------------------------------------------------
 
 export {
-  createVideoChannel,
+  createLocalVideoChannel,
   federateAllVideosOfChannel
 }
index 449aa74cb10a1f503bf48e58fd6229078c380f1f..bb811bd2c3d4b5749eb075b56cf3e428d15d80fe 100644 (file)
@@ -1,17 +1,16 @@
 import * as Sequelize from 'sequelize'
 import { ResultList } from '../../shared/models'
 import { VideoCommentThreadTree } from '../../shared/models/videos/video-comment.model'
-import { AccountModel } from '../models/account/account'
-import { VideoModel } from '../models/video/video'
 import { VideoCommentModel } from '../models/video/video-comment'
 import { getVideoCommentActivityPubUrl } from './activitypub'
 import { sendCreateVideoComment } from './activitypub/send'
+import { MAccountDefault, MComment, MCommentOwnerVideoReply, MVideoFullLight } from '../typings/models'
 
 async function createVideoComment (obj: {
   text: string,
-  inReplyToComment: VideoCommentModel | null,
-  video: VideoModel
-  account: AccountModel
+  inReplyToComment: MComment | null,
+  video: MVideoFullLight,
+  account: MAccountDefault
 }, t: Sequelize.Transaction) {
   let originCommentId: number | null = null
   let inReplyToCommentId: number | null = null
@@ -32,7 +31,7 @@ async function createVideoComment (obj: {
 
   comment.url = getVideoCommentActivityPubUrl(obj.video, comment)
 
-  const savedComment = await comment.save({ transaction: t })
+  const savedComment: MCommentOwnerVideoReply = await comment.save({ transaction: t })
   savedComment.InReplyToVideoComment = obj.inReplyToComment
   savedComment.Video = obj.video
   savedComment.Account = obj.account
index 6e214e60f226c852235641a714a63eb1f55f6bf2..29b70cfda0b4bd3507769c6012326d7cc0a73d1c 100644 (file)
@@ -1,12 +1,13 @@
 import * as Sequelize from 'sequelize'
-import { AccountModel } from '../models/account/account'
 import { VideoPlaylistModel } from '../models/video/video-playlist'
 import { VideoPlaylistPrivacy } from '../../shared/models/videos/playlist/video-playlist-privacy.model'
 import { getVideoPlaylistActivityPubUrl } from './activitypub'
 import { VideoPlaylistType } from '../../shared/models/videos/playlist/video-playlist-type.model'
+import { MAccount } from '../typings/models'
+import { MVideoPlaylistOwner } from '../typings/models/video/video-playlist'
 
-async function createWatchLaterPlaylist (account: AccountModel, t: Sequelize.Transaction) {
-  const videoPlaylist = new VideoPlaylistModel({
+async function createWatchLaterPlaylist (account: MAccount, t: Sequelize.Transaction) {
+  const videoPlaylist: MVideoPlaylistOwner = new VideoPlaylistModel({
     name: 'Watch later',
     privacy: VideoPlaylistPrivacy.PRIVATE,
     type: VideoPlaylistType.WATCH_LATER,
index ba6b29163c880c4ec3fb8bbfe2b2bf74abed89c4..a204c0c634d473aa8fdb901eb4811bffa31fe21e 100644 (file)
@@ -5,16 +5,16 @@ import { ensureDir, move, remove, stat } from 'fs-extra'
 import { logger } from '../helpers/logger'
 import { VideoResolution } from '../../shared/models/videos'
 import { VideoFileModel } from '../models/video/video-file'
-import { VideoModel } from '../models/video/video'
 import { updateMasterHLSPlaylist, updateSha256Segments } from './hls'
 import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
 import { VideoStreamingPlaylistType } from '../../shared/models/videos/video-streaming-playlist.type'
 import { CONFIG } from '../initializers/config'
+import { MVideoFile, MVideoWithFile, MVideoWithFileThumbnail } from '@server/typings/models'
 
 /**
  * Optimize the original video file and replace it. The resolution is not changed.
  */
-async function optimizeVideofile (video: VideoModel, inputVideoFileArg?: VideoFileModel) {
+async function optimizeVideofile (video: MVideoWithFile, inputVideoFileArg?: MVideoFile) {
   const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
   const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
   const newExtname = '.mp4'
@@ -57,7 +57,7 @@ async function optimizeVideofile (video: VideoModel, inputVideoFileArg?: VideoFi
 /**
  * Transcode the original video file to a lower resolution.
  */
-async function transcodeOriginalVideofile (video: VideoModel, resolution: VideoResolution, isPortrait: boolean) {
+async function transcodeOriginalVideofile (video: MVideoWithFile, resolution: VideoResolution, isPortrait: boolean) {
   const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
   const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
   const extname = '.mp4'
@@ -87,7 +87,7 @@ async function transcodeOriginalVideofile (video: VideoModel, resolution: VideoR
   return onVideoFileTranscoding(video, newVideoFile, videoTranscodedPath, videoOutputPath)
 }
 
-async function mergeAudioVideofile (video: VideoModel, resolution: VideoResolution) {
+async function mergeAudioVideofile (video: MVideoWithFileThumbnail, resolution: VideoResolution) {
   const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
   const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
   const newExtname = '.mp4'
@@ -117,7 +117,7 @@ async function mergeAudioVideofile (video: VideoModel, resolution: VideoResoluti
   return onVideoFileTranscoding(video, inputVideoFile, videoTranscodedPath, videoOutputPath)
 }
 
-async function generateHlsPlaylist (video: VideoModel, resolution: VideoResolution, isPortraitMode: boolean) {
+async function generateHlsPlaylist (video: MVideoWithFile, resolution: VideoResolution, isPortraitMode: boolean) {
   const baseHlsDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)
   await ensureDir(join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid))
 
@@ -165,14 +165,14 @@ export {
 
 // ---------------------------------------------------------------------------
 
-async function onVideoFileTranscoding (video: VideoModel, videoFile: VideoFileModel, transcodingPath: string, outputPath: string) {
+async function onVideoFileTranscoding (video: MVideoWithFile, videoFile: MVideoFile, transcodingPath: string, outputPath: string) {
   const stats = await stat(transcodingPath)
   const fps = await getVideoFileFPS(transcodingPath)
 
   await move(transcodingPath, outputPath)
 
-  videoFile.set('size', stats.size)
-  videoFile.set('fps', fps)
+  videoFile.size = stats.size
+  videoFile.fps = fps
 
   await video.createTorrentAndSetInfoHash(videoFile)
 
index b1e5b52369c115dadc556264262c1db4260c2f47..bea213d270af840ba0edfd1beddfb4ea5ac75905 100644 (file)
@@ -101,6 +101,8 @@ async function checkJsonLDSignature (req: Request, res: Response) {
   const verified = await isJsonLDSignatureVerified(actor, req.body)
 
   if (verified !== true) {
+    logger.warn('Signature not verified.', req.body)
+
     res.sendStatus(403)
     return false
   }
index c3d772297eb303fbf904316ba02a3b5714daea86..7887356630a54191824bdf3d039247f4ddd7fe19 100644 (file)
@@ -10,6 +10,7 @@ import { areValidationErrors } from './utils'
 import { ActorModel } from '../../models/activitypub/actor'
 import { loadActorUrlOrGetFromWebfinger } from '../../helpers/webfinger'
 import { isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor'
+import { MActorFollowActorsDefault } from '@server/typings/models'
 
 const followValidator = [
   body('hosts').custom(isEachUniqueHostValid).withMessage('Should have an array of unique hosts'),
@@ -65,7 +66,7 @@ const getFollowerValidator = [
 
     if (areValidationErrors(req, res)) return
 
-    let follow: ActorFollowModel
+    let follow: MActorFollowActorsDefault
     try {
       const actorUrl = await loadActorUrlOrGetFromWebfinger(req.params.nameWithHost)
       const actor = await ActorModel.loadByUrl(actorUrl)
index 1fdac0e4e2c38f38ad3c9a6f03130ee4f87b5900..e65d3b8d3d874c4495f3e11e1f2890e19e307cc1 100644 (file)
@@ -24,7 +24,7 @@ const videoFileRedundancyGetValidator = [
     if (areValidationErrors(req, res)) return
     if (!await doesVideoExist(req.params.videoId, res)) return
 
-    const video = res.locals.video
+    const video = res.locals.videoAll
     const videoFile = video.VideoFiles.find(f => {
       return f.resolution === req.params.resolution && (!req.params.fps || f.fps === req.params.fps)
     })
@@ -50,7 +50,7 @@ const videoPlaylistRedundancyGetValidator = [
     if (areValidationErrors(req, res)) return
     if (!await doesVideoExist(req.params.videoId, res)) return
 
-    const video = res.locals.video
+    const video = res.locals.videoAll
     const videoStreamingPlaylist = video.VideoStreamingPlaylists.find(p => p === req.params.streamingPlaylistType)
 
     if (!videoStreamingPlaylist) return res.status(404).json({ error: 'Video playlist not found.' })
index 308b326552c552ec9f105b9c5f35f7831efc66c1..fbfcb0a4ca0a0399d3235f74d7fc7ab2865a44b7 100644 (file)
@@ -43,6 +43,8 @@ const updateNotificationSettingsValidator = [
     .custom(isUserNotificationSettingValid).withMessage('Should have a valid new user registration notification setting'),
   body('newInstanceFollower')
     .custom(isUserNotificationSettingValid).withMessage('Should have a valid new instance follower notification setting'),
+  body('autoInstanceFollowing')
+    .custom(isUserNotificationSettingValid).withMessage('Should have a valid new instance following notification setting'),
 
   (req: express.Request, res: express.Response, next: express.NextFunction) => {
     logger.debug('Checking updateNotificationSettingsValidator parameters', { parameters: req.body })
index 8ee2ec1f59390be2a481bf6653751cd2ae3e6e15..544db76d73c805b124246d42c01910baca9ba80d 100644 (file)
@@ -4,6 +4,7 @@ import { body, param } from 'express-validator'
 import { omit } from 'lodash'
 import { isIdOrUUIDValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
 import {
+  isNoInstanceConfigWarningModal, isNoWelcomeModal,
   isUserAdminFlagsValid,
   isUserAutoPlayVideoValid,
   isUserBlockedReasonValid,
@@ -31,6 +32,7 @@ import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
 import { isThemeRegistered } from '../../lib/plugins/theme-utils'
 import { doesVideoExist } from '../../helpers/middlewares'
 import { UserRole } from '../../../shared/models/users'
+import { MUserDefault } from '@server/typings/models'
 
 const usersAddValidator = [
   body('username').custom(isUserUsernameValid).withMessage('Should have a valid username (lowercase alphanumeric characters)'),
@@ -215,6 +217,12 @@ const usersUpdateMeValidator = [
   body('theme')
     .optional()
     .custom(v => isThemeNameValid(v) && isThemeRegistered(v)).withMessage('Should have a valid theme'),
+  body('noInstanceConfigWarningModal')
+    .optional()
+    .custom(v => isNoInstanceConfigWarningModal(v)).withMessage('Should have a valid noInstanceConfigWarningModal boolean'),
+  body('noWelcomeModal')
+    .optional()
+    .custom(v => isNoWelcomeModal(v)).withMessage('Should have a valid noWelcomeModal boolean'),
 
   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
     logger.debug('Checking usersUpdateMe parameters', { parameters: omit(req.body, 'password') })
@@ -462,7 +470,7 @@ async function checkUserNameOrEmailDoesNotAlreadyExist (username: string, email:
   return true
 }
 
-async function checkUserExist (finder: () => Bluebird<UserModel>, res: express.Response, abortResponse = true) {
+async function checkUserExist (finder: () => Bluebird<MUserDefault>, res: express.Response, abortResponse = true) {
   const user = await finder()
 
   if (!user) {
index e27d91bb12cb4e3820de54d3e6b6f78e6fe8b448..a4aef4024a8d9e470cbe1c3f0163df9bd1e0d699 100644 (file)
@@ -33,7 +33,7 @@ const videoAbuseGetValidator = [
 
     if (areValidationErrors(req, res)) return
     if (!await doesVideoExist(req.params.videoId, res)) return
-    if (!await doesVideoAbuseExist(req.params.id, res.locals.video.id, res)) return
+    if (!await doesVideoAbuseExist(req.params.id, res.locals.videoAll.id, res)) return
 
     return next()
   }
@@ -54,7 +54,7 @@ const videoAbuseUpdateValidator = [
 
     if (areValidationErrors(req, res)) return
     if (!await doesVideoExist(req.params.videoId, res)) return
-    if (!await doesVideoAbuseExist(req.params.id, res.locals.video.id, res)) return
+    if (!await doesVideoAbuseExist(req.params.id, res.locals.videoAll.id, res)) return
 
     return next()
   }
index 3e8c5b30c96942b4f0de830b0a906206dd0456d8..5440e57e7b9b870365a60bc9f3ec20acba1ffaf1 100644 (file)
@@ -14,7 +14,7 @@ const videosBlacklistRemoveValidator = [
 
     if (areValidationErrors(req, res)) return
     if (!await doesVideoExist(req.params.videoId, res)) return
-    if (!await doesVideoBlacklistExist(res.locals.video.id, res)) return
+    if (!await doesVideoBlacklistExist(res.locals.videoAll.id, res)) return
 
     return next()
   }
@@ -36,7 +36,7 @@ const videosBlacklistAddValidator = [
     if (areValidationErrors(req, res)) return
     if (!await doesVideoExist(req.params.videoId, res)) return
 
-    const video = res.locals.video
+    const video = res.locals.videoAll
     if (req.body.unfederate === true && video.remote === true) {
       return res
         .status(409)
@@ -59,7 +59,7 @@ const videosBlacklistUpdateValidator = [
 
     if (areValidationErrors(req, res)) return
     if (!await doesVideoExist(req.params.videoId, res)) return
-    if (!await doesVideoBlacklistExist(res.locals.video.id, res)) return
+    if (!await doesVideoBlacklistExist(res.locals.videoAll.id, res)) return
 
     return next()
   }
index f5610222aceb4f0bfede12862d14869fa8b9a38e..2fb1da5ce403581efa9f503f5a4871e8231b69ef 100644 (file)
@@ -26,7 +26,7 @@ const addVideoCaptionValidator = [
 
     // Check if the user who did the request is able to update the video
     const user = res.locals.oauth.token.User
-    if (!checkUserCanManageVideo(user, res.locals.video, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
+    if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
 
     return next()
   }
@@ -41,11 +41,11 @@ const deleteVideoCaptionValidator = [
 
     if (areValidationErrors(req, res)) return
     if (!await doesVideoExist(req.params.videoId, res)) return
-    if (!await doesVideoCaptionExist(res.locals.video, req.params.captionLanguage, res)) return
+    if (!await doesVideoCaptionExist(res.locals.videoAll, req.params.captionLanguage, res)) return
 
     // Check if the user who did the request is able to update the video
     const user = res.locals.oauth.token.User
-    if (!checkUserCanManageVideo(user, res.locals.video, UserRight.UPDATE_ANY_VIDEO, res)) return
+    if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return
 
     return next()
   }
index 3ee5064fc6990002c519cde97d359dab0bdd6329..d212745277336b650f5cf12ee9d265b7e7fdb3a5 100644 (file)
@@ -7,13 +7,13 @@ import {
   isVideoChannelSupportValid
 } from '../../../helpers/custom-validators/video-channels'
 import { logger } from '../../../helpers/logger'
-import { UserModel } from '../../../models/account/user'
 import { VideoChannelModel } from '../../../models/video/video-channel'
 import { areValidationErrors } from '../utils'
 import { isActorPreferredUsernameValid } from '../../../helpers/custom-validators/activitypub/actor'
 import { ActorModel } from '../../../models/activitypub/actor'
 import { isBooleanValid } from '../../../helpers/custom-validators/misc'
 import { doesLocalVideoChannelNameExist, doesVideoChannelNameWithHostExist } from '../../../helpers/middlewares'
+import { MChannelAccountDefault, MUser } from '@server/typings/models'
 
 const videoChannelsAddValidator = [
   body('name').custom(isActorPreferredUsernameValid).withMessage('Should have a valid channel name'),
@@ -131,7 +131,7 @@ export {
 
 // ---------------------------------------------------------------------------
 
-function checkUserCanDeleteVideoChannel (user: UserModel, videoChannel: VideoChannelModel, res: express.Response) {
+function checkUserCanDeleteVideoChannel (user: MUser, videoChannel: MChannelAccountDefault, res: express.Response) {
   if (videoChannel.Actor.isOwned() === false) {
     res.status(403)
               .json({ error: 'Cannot remove video channel of another server.' })
index 83a0c24b030ca49a05467fb79a1329a10b9be9df..8adbb02ba13187a67ff4ad6d6732c12bc3b57ec8 100644 (file)
@@ -4,13 +4,13 @@ import { UserRight } from '../../../../shared'
 import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc'
 import { isValidVideoCommentText } from '../../../helpers/custom-validators/video-comments'
 import { logger } from '../../../helpers/logger'
-import { UserModel } from '../../../models/account/user'
-import { VideoModel } from '../../../models/video/video'
 import { VideoCommentModel } from '../../../models/video/video-comment'
 import { areValidationErrors } from '../utils'
 import { Hooks } from '../../../lib/plugins/hooks'
-import { isLocalVideoThreadAccepted, isLocalVideoCommentReplyAccepted, AcceptResult } from '../../../lib/moderation'
+import { AcceptResult, isLocalVideoCommentReplyAccepted, isLocalVideoThreadAccepted } from '../../../lib/moderation'
 import { doesVideoExist } from '../../../helpers/middlewares'
+import { MCommentOwner, MVideo, MVideoFullLight, MVideoId } from '../../../typings/models/video'
+import { MUser } from '@server/typings/models'
 
 const listVideoCommentThreadsValidator = [
   param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
@@ -34,7 +34,7 @@ const listVideoThreadCommentsValidator = [
 
     if (areValidationErrors(req, res)) return
     if (!await doesVideoExist(req.params.videoId, res, 'only-video')) return
-    if (!await doesVideoCommentThreadExist(req.params.threadId, res.locals.video, res)) return
+    if (!await doesVideoCommentThreadExist(req.params.threadId, res.locals.onlyVideo, res)) return
 
     return next()
   }
@@ -49,8 +49,8 @@ const addVideoCommentThreadValidator = [
 
     if (areValidationErrors(req, res)) return
     if (!await doesVideoExist(req.params.videoId, res)) return
-    if (!isVideoCommentsEnabled(res.locals.video, res)) return
-    if (!await isVideoCommentAccepted(req, res, false)) return
+    if (!isVideoCommentsEnabled(res.locals.videoAll, res)) return
+    if (!await isVideoCommentAccepted(req, res, res.locals.videoAll,false)) return
 
     return next()
   }
@@ -66,9 +66,9 @@ const addVideoCommentReplyValidator = [
 
     if (areValidationErrors(req, res)) return
     if (!await doesVideoExist(req.params.videoId, res)) return
-    if (!isVideoCommentsEnabled(res.locals.video, res)) return
-    if (!await doesVideoCommentExist(req.params.commentId, res.locals.video, res)) return
-    if (!await isVideoCommentAccepted(req, res, true)) return
+    if (!isVideoCommentsEnabled(res.locals.videoAll, res)) return
+    if (!await doesVideoCommentExist(req.params.commentId, res.locals.videoAll, res)) return
+    if (!await isVideoCommentAccepted(req, res, res.locals.videoAll, true)) return
 
     return next()
   }
@@ -83,7 +83,7 @@ const videoCommentGetValidator = [
 
     if (areValidationErrors(req, res)) return
     if (!await doesVideoExist(req.params.videoId, res, 'id')) return
-    if (!await doesVideoCommentExist(req.params.commentId, res.locals.video, res)) return
+    if (!await doesVideoCommentExist(req.params.commentId, res.locals.videoId, res)) return
 
     return next()
   }
@@ -98,10 +98,10 @@ const removeVideoCommentValidator = [
 
     if (areValidationErrors(req, res)) return
     if (!await doesVideoExist(req.params.videoId, res)) return
-    if (!await doesVideoCommentExist(req.params.commentId, res.locals.video, res)) return
+    if (!await doesVideoCommentExist(req.params.commentId, res.locals.videoAll, res)) return
 
     // Check if the user who did the request is able to delete the video
-    if (!checkUserCanDeleteVideoComment(res.locals.oauth.token.User, res.locals.videoComment, res)) return
+    if (!checkUserCanDeleteVideoComment(res.locals.oauth.token.User, res.locals.videoCommentFull, res)) return
 
     return next()
   }
@@ -120,7 +120,7 @@ export {
 
 // ---------------------------------------------------------------------------
 
-async function doesVideoCommentThreadExist (id: number, video: VideoModel, res: express.Response) {
+async function doesVideoCommentThreadExist (id: number, video: MVideoId, res: express.Response) {
   const videoComment = await VideoCommentModel.loadById(id)
 
   if (!videoComment) {
@@ -151,7 +151,7 @@ async function doesVideoCommentThreadExist (id: number, video: VideoModel, res:
   return true
 }
 
-async function doesVideoCommentExist (id: number, video: VideoModel, res: express.Response) {
+async function doesVideoCommentExist (id: number, video: MVideoId, res: express.Response) {
   const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id)
 
   if (!videoComment) {
@@ -170,11 +170,11 @@ async function doesVideoCommentExist (id: number, video: VideoModel, res: expres
     return false
   }
 
-  res.locals.videoComment = videoComment
+  res.locals.videoCommentFull = videoComment
   return true
 }
 
-function isVideoCommentsEnabled (video: VideoModel, res: express.Response) {
+function isVideoCommentsEnabled (video: MVideo, res: express.Response) {
   if (video.commentsEnabled !== true) {
     res.status(409)
       .json({ error: 'Video comments are disabled for this video.' })
@@ -186,7 +186,7 @@ function isVideoCommentsEnabled (video: VideoModel, res: express.Response) {
   return true
 }
 
-function checkUserCanDeleteVideoComment (user: UserModel, videoComment: VideoCommentModel, res: express.Response) {
+function checkUserCanDeleteVideoComment (user: MUser, videoComment: MCommentOwner, res: express.Response) {
   const account = videoComment.Account
   if (user.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT) === false && account.userId !== user.id) {
     res.status(403)
@@ -198,9 +198,9 @@ function checkUserCanDeleteVideoComment (user: UserModel, videoComment: VideoCom
   return true
 }
 
-async function isVideoCommentAccepted (req: express.Request, res: express.Response, isReply: boolean) {
+async function isVideoCommentAccepted (req: express.Request, res: express.Response, video: MVideoFullLight, isReply: boolean) {
   const acceptParameters = {
-    video: res.locals.video,
+    video,
     commentBody: req.body,
     user: res.locals.oauth.token.User
   }
@@ -208,7 +208,7 @@ async function isVideoCommentAccepted (req: express.Request, res: express.Respon
   let acceptedResult: AcceptResult
 
   if (isReply) {
-    const acceptReplyParameters = Object.assign(acceptParameters, { parentComment: res.locals.videoComment })
+    const acceptReplyParameters = Object.assign(acceptParameters, { parentComment: res.locals.videoCommentFull })
 
     acceptedResult = await Hooks.wrapFun(
       isLocalVideoCommentReplyAccepted,
index 5823795be4a7af41c59b3036711effed4539f285..27ee62b1fc569d03fafd65e203029833e59d10df 100644 (file)
@@ -2,7 +2,6 @@ import * as express from 'express'
 import { body, param, query, ValidationChain } from 'express-validator'
 import { UserRight, VideoPlaylistCreate, VideoPlaylistUpdate } from '../../../../shared'
 import { logger } from '../../../helpers/logger'
-import { UserModel } from '../../../models/account/user'
 import { areValidationErrors } from '../utils'
 import { isVideoImage } from '../../../helpers/custom-validators/videos'
 import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
@@ -22,13 +21,14 @@ import {
   isVideoPlaylistTimestampValid,
   isVideoPlaylistTypeValid
 } from '../../../helpers/custom-validators/video-playlists'
-import { VideoPlaylistModel } from '../../../models/video/video-playlist'
 import { cleanUpReqFiles } from '../../../helpers/express-utils'
 import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-element'
 import { authenticatePromiseIfNeeded } from '../../oauth'
 import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
 import { VideoPlaylistType } from '../../../../shared/models/videos/playlist/video-playlist-type.model'
-import { doesVideoChannelIdExist, doesVideoExist, doesVideoPlaylistExist } from '../../../helpers/middlewares'
+import { doesVideoChannelIdExist, doesVideoExist, doesVideoPlaylistExist, VideoPlaylistFetchType } from '../../../helpers/middlewares'
+import { MVideoPlaylist } from '../../../typings/models/video/video-playlist'
+import { MUserAccountId } from '@server/typings/models'
 
 const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([
   body('displayName')
@@ -67,9 +67,9 @@ const videoPlaylistsUpdateValidator = getCommonPlaylistEditAttributes().concat([
 
     if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return cleanUpReqFiles(req)
 
-    const videoPlaylist = res.locals.videoPlaylist
+    const videoPlaylist = getPlaylist(res)
 
-    if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, res.locals.videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) {
+    if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) {
       return cleanUpReqFiles(req)
     }
 
@@ -110,13 +110,13 @@ const videoPlaylistsDeleteValidator = [
 
     if (!await doesVideoPlaylistExist(req.params.playlistId, res)) return
 
-    const videoPlaylist = res.locals.videoPlaylist
+    const videoPlaylist = getPlaylist(res)
     if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) {
       return res.status(400)
                 .json({ error: 'Cannot delete a watch later playlist.' })
     }
 
-    if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, res.locals.videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) {
+    if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) {
       return
     }
 
@@ -124,45 +124,47 @@ const videoPlaylistsDeleteValidator = [
   }
 ]
 
-const videoPlaylistsGetValidator = [
-  param('playlistId')
-    .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'),
+const videoPlaylistsGetValidator = (fetchType: VideoPlaylistFetchType) => {
+  return [
+    param('playlistId')
+      .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'),
 
-  async (req: express.Request, res: express.Response, next: express.NextFunction) => {
-    logger.debug('Checking videoPlaylistsGetValidator parameters', { parameters: req.params })
+    async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+      logger.debug('Checking videoPlaylistsGetValidator parameters', { parameters: req.params })
 
-    if (areValidationErrors(req, res)) return
+      if (areValidationErrors(req, res)) return
 
-    if (!await doesVideoPlaylistExist(req.params.playlistId, res)) return
+      if (!await doesVideoPlaylistExist(req.params.playlistId, res, fetchType)) return
 
-    const videoPlaylist = res.locals.videoPlaylist
+      const videoPlaylist = res.locals.videoPlaylistFull || res.locals.videoPlaylistSummary
 
-    // Video is unlisted, check we used the uuid to fetch it
-    if (videoPlaylist.privacy === VideoPlaylistPrivacy.UNLISTED) {
-      if (isUUIDValid(req.params.playlistId)) return next()
+      // Video is unlisted, check we used the uuid to fetch it
+      if (videoPlaylist.privacy === VideoPlaylistPrivacy.UNLISTED) {
+        if (isUUIDValid(req.params.playlistId)) return next()
 
-      return res.status(404).end()
-    }
+        return res.status(404).end()
+      }
+
+      if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) {
+        await authenticatePromiseIfNeeded(req, res)
 
-    if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) {
-      await authenticatePromiseIfNeeded(req, res)
+        const user = res.locals.oauth ? res.locals.oauth.token.User : null
 
-      const user = res.locals.oauth ? res.locals.oauth.token.User : null
+        if (
+          !user ||
+          (videoPlaylist.OwnerAccount.id !== user.Account.id && !user.hasRight(UserRight.UPDATE_ANY_VIDEO_PLAYLIST))
+        ) {
+          return res.status(403)
+                    .json({ error: 'Cannot get this private video playlist.' })
+        }
 
-      if (
-        !user ||
-        (videoPlaylist.OwnerAccount.id !== user.Account.id && !user.hasRight(UserRight.UPDATE_ANY_VIDEO_PLAYLIST))
-      ) {
-        return res.status(403)
-                  .json({ error: 'Cannot get this private video playlist.' })
+        return next()
       }
 
       return next()
     }
-
-    return next()
-  }
-]
+  ]
+}
 
 const videoPlaylistsAddVideoValidator = [
   param('playlistId')
@@ -184,8 +186,8 @@ const videoPlaylistsAddVideoValidator = [
     if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return
     if (!await doesVideoExist(req.body.videoId, res, 'only-video')) return
 
-    const videoPlaylist = res.locals.videoPlaylist
-    const video = res.locals.video
+    const videoPlaylist = getPlaylist(res)
+    const video = res.locals.onlyVideo
 
     const videoPlaylistElement = await VideoPlaylistElementModel.loadByPlaylistAndVideo(videoPlaylist.id, video.id)
     if (videoPlaylistElement) {
@@ -196,7 +198,7 @@ const videoPlaylistsAddVideoValidator = [
       return
     }
 
-    if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, res.locals.videoPlaylist, UserRight.UPDATE_ANY_VIDEO_PLAYLIST, res)) {
+    if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.UPDATE_ANY_VIDEO_PLAYLIST, res)) {
       return
     }
 
@@ -223,7 +225,7 @@ const videoPlaylistsUpdateOrRemoveVideoValidator = [
 
     if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return
 
-    const videoPlaylist = res.locals.videoPlaylist
+    const videoPlaylist = getPlaylist(res)
 
     const videoPlaylistElement = await VideoPlaylistElementModel.loadById(req.params.playlistElementId)
     if (!videoPlaylistElement) {
@@ -265,7 +267,7 @@ const videoPlaylistElementAPGetValidator = [
       return res.status(403).end()
     }
 
-    res.locals.videoPlaylistElement = videoPlaylistElement
+    res.locals.videoPlaylistElementAP = videoPlaylistElement
 
     return next()
   }
@@ -289,7 +291,7 @@ const videoPlaylistsReorderVideosValidator = [
 
     if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return
 
-    const videoPlaylist = res.locals.videoPlaylist
+    const videoPlaylist = getPlaylist(res)
     if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.UPDATE_ANY_VIDEO_PLAYLIST, res)) return
 
     const nextPosition = await VideoPlaylistElementModel.getNextPositionOf(videoPlaylist.id)
@@ -388,7 +390,7 @@ function getCommonPlaylistEditAttributes () {
   ] as (ValidationChain | express.Handler)[]
 }
 
-function checkUserCanManageVideoPlaylist (user: UserModel, videoPlaylist: VideoPlaylistModel, right: UserRight, res: express.Response) {
+function checkUserCanManageVideoPlaylist (user: MUserAccountId, videoPlaylist: MVideoPlaylist, right: UserRight, res: express.Response) {
   if (videoPlaylist.isOwned() === false) {
     res.status(403)
        .json({ error: 'Cannot manage video playlist of another server.' })
@@ -410,3 +412,7 @@ function checkUserCanManageVideoPlaylist (user: UserModel, videoPlaylist: VideoP
 
   return true
 }
+
+function getPlaylist (res: express.Response) {
+  return res.locals.videoPlaylistFull || res.locals.videoPlaylistSummary
+}
index ace62be5cade3a1171e6043cd8cad653c356bd05..20fc962438f591f97c8bb07e62f236b67c6bc44a 100644 (file)
@@ -16,7 +16,7 @@ const videosShareValidator = [
     if (areValidationErrors(req, res)) return
     if (!await doesVideoExist(req.params.id, res)) return
 
-    const video = res.locals.video
+    const video = res.locals.videoAll
 
     const share = await VideoShareModel.load(req.params.actorId, video.id)
     if (!share) {
index af06f3c629e1426538045bfd7456ad5051f8aa27..1449903b7d8798e4f7cd4f5bd389391df34b0a99 100644 (file)
@@ -37,13 +37,14 @@ import { VideoModel } from '../../../models/video/video'
 import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership'
 import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model'
 import { AccountModel } from '../../../models/account/account'
-import { VideoFetchType } from '../../../helpers/video'
 import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
 import { getServerActor } from '../../../helpers/utils'
 import { CONFIG } from '../../../initializers/config'
 import { isLocalVideoAccepted } from '../../../lib/moderation'
 import { Hooks } from '../../../lib/plugins/hooks'
 import { checkUserCanManageVideo, doesVideoChannelOfAccountExist, doesVideoExist } from '../../../helpers/middlewares'
+import { MVideoFullLight } from '@server/typings/models'
+import { getVideoWithAttributes } from '../../../helpers/video'
 
 const videosAddValidator = getCommonVideoEditAttributes().concat([
   body('videofile')
@@ -113,7 +114,7 @@ const videosUpdateValidator = getCommonVideoEditAttributes().concat([
 
     // Check if the user who did the request is able to update the video
     const user = res.locals.oauth.token.User
-    if (!checkUserCanManageVideo(user, res.locals.video, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
+    if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
 
     if (req.body.channelId && !await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
 
@@ -122,7 +123,7 @@ const videosUpdateValidator = getCommonVideoEditAttributes().concat([
 ])
 
 async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) {
-  const video = res.locals.video
+  const video = getVideoWithAttributes(res)
 
   // Anybody can watch local videos
   if (video.isOwned() === true) return next()
@@ -146,7 +147,7 @@ async function checkVideoFollowConstraints (req: express.Request, res: express.R
             })
 }
 
-const videosCustomGetValidator = (fetchType: VideoFetchType) => {
+const videosCustomGetValidator = (fetchType: 'all' | 'only-video' | 'only-video-with-rights') => {
   return [
     param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
 
@@ -156,10 +157,11 @@ const videosCustomGetValidator = (fetchType: VideoFetchType) => {
       if (areValidationErrors(req, res)) return
       if (!await doesVideoExist(req.params.id, res, fetchType)) return
 
-      const video = res.locals.video
+      const video = getVideoWithAttributes(res)
+      const videoAll = video as MVideoFullLight
 
       // Video private or blacklisted
-      if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) {
+      if (video.privacy === VideoPrivacy.PRIVATE || videoAll.VideoBlacklist) {
         await authenticatePromiseIfNeeded(req, res)
 
         const user = res.locals.oauth ? res.locals.oauth.token.User : null
@@ -167,7 +169,7 @@ const videosCustomGetValidator = (fetchType: VideoFetchType) => {
         // Only the owner or a user that have blacklist rights can see the video
         if (
           !user ||
-          (video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST))
+          (videoAll.VideoChannel && videoAll.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST))
         ) {
           return res.status(403)
                     .json({ error: 'Cannot get this private or blacklisted video.' })
@@ -202,7 +204,7 @@ const videosRemoveValidator = [
     if (!await doesVideoExist(req.params.id, res)) return
 
     // Check if the user who did the request is able to delete the video
-    if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.REMOVE_ANY_VIDEO, res)) return
+    if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.REMOVE_ANY_VIDEO, res)) return
 
     return next()
   }
@@ -218,7 +220,7 @@ const videosChangeOwnershipValidator = [
     if (!await doesVideoExist(req.params.videoId, res)) return
 
     // Check if the user who did the request is able to change the ownership of the video
-    if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.video, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return
+    if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return
 
     const nextOwner = await AccountModel.loadLocalByName(req.body.username)
     if (!nextOwner) {
index d7cfe17f00ea41ab0c43e7f6dfc002e999109f2e..d50e6527fe4ac7ab3d9764c6372e90b96df91163 100644 (file)
@@ -18,6 +18,7 @@ const webfingerValidator = [
     const nameWithHost = getHostWithPort(req.query.resource.substr(5))
     const [ name ] = nameWithHost.split('@')
 
+    // FIXME: we don't need the full actor
     const actor = await ActorModel.loadLocalByName(name)
     if (!actor) {
       return res.status(404)
@@ -25,7 +26,7 @@ const webfingerValidator = [
         .end()
     }
 
-    res.locals.actor = actor
+    res.locals.actorFull = actor
     return next()
   }
 ]
index d5746ad7614e416b812240bc1dee87ecff2827da..8bcaca8280162c39f51015506c8fd79a37078e6e 100644 (file)
@@ -3,6 +3,8 @@ import { AccountModel } from './account'
 import { getSort } from '../utils'
 import { AccountBlock } from '../../../shared/models/blocklist'
 import { Op } from 'sequelize'
+import * as Bluebird from 'bluebird'
+import { MAccountBlocklist, MAccountBlocklistAccounts, MAccountBlocklistFormattable } from '@server/typings/models'
 
 enum ScopeNames {
   WITH_ACCOUNTS = 'WITH_ACCOUNTS'
@@ -103,7 +105,7 @@ export class AccountBlocklistModel extends Model<AccountBlocklistModel> {
                                 })
   }
 
-  static loadByAccountAndTarget (accountId: number, targetAccountId: number) {
+  static loadByAccountAndTarget (accountId: number, targetAccountId: number): Bluebird<MAccountBlocklist> {
     const query = {
       where: {
         accountId,
@@ -126,13 +128,13 @@ export class AccountBlocklistModel extends Model<AccountBlocklistModel> {
 
     return AccountBlocklistModel
       .scope([ ScopeNames.WITH_ACCOUNTS ])
-      .findAndCountAll(query)
+      .findAndCountAll<MAccountBlocklistAccounts>(query)
       .then(({ rows, count }) => {
         return { total: count, data: rows }
       })
   }
 
-  toFormattedJSON (): AccountBlock {
+  toFormattedJSON (this: MAccountBlocklistFormattable): AccountBlock {
     return {
       byAccount: this.ByAccount.toFormattedJSON(),
       blockedAccount: this.BlockedAccount.toFormattedJSON(),
index 4bd8114cf6e2a718a781b89a25fd8b4d42d64bc6..a6edbeee8b2b8ac8a2d9e48077f9a7de468166fe 100644 (file)
@@ -10,6 +10,13 @@ import { buildLocalAccountIdsIn, getSort, throwIfNotValid } from '../utils'
 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
 import { AccountVideoRate } from '../../../shared'
 import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel'
+import * as Bluebird from 'bluebird'
+import {
+  MAccountVideoRate,
+  MAccountVideoRateAccountUrl,
+  MAccountVideoRateAccountVideo,
+  MAccountVideoRateFormattable
+} from '@server/typings/models/video/video-rate'
 
 /*
   Account rates per video.
@@ -77,7 +84,7 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
   })
   Account: AccountModel
 
-  static load (accountId: number, videoId: number, transaction?: Transaction) {
+  static load (accountId: number, videoId: number, transaction?: Transaction): Bluebird<MAccountVideoRate> {
     const options: FindOptions = {
       where: {
         accountId,
@@ -89,7 +96,7 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
     return AccountVideoRateModel.findOne(options)
   }
 
-  static loadByAccountAndVideoOrUrl (accountId: number, videoId: number, url: string, transaction?: Transaction) {
+  static loadByAccountAndVideoOrUrl (accountId: number, videoId: number, url: string, t?: Transaction): Bluebird<MAccountVideoRate> {
     const options: FindOptions = {
       where: {
         [ Op.or]: [
@@ -103,7 +110,7 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
         ]
       }
     }
-    if (transaction) options.transaction = transaction
+    if (t) options.transaction = t
 
     return AccountVideoRateModel.findOne(options)
   }
@@ -140,7 +147,12 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
     return AccountVideoRateModel.findAndCountAll(query)
   }
 
-  static loadLocalAndPopulateVideo (rateType: VideoRateType, accountName: string, videoId: number, transaction?: Transaction) {
+  static loadLocalAndPopulateVideo (
+    rateType: VideoRateType,
+    accountName: string,
+    videoId: number,
+    t?: Transaction
+  ): Bluebird<MAccountVideoRateAccountVideo> {
     const options: FindOptions = {
       where: {
         videoId,
@@ -152,7 +164,7 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
           required: true,
           include: [
             {
-              attributes: [ 'id', 'url', 'preferredUsername' ],
+              attributes: [ 'id', 'url', 'followersUrl', 'preferredUsername' ],
               model: ActorModel.unscoped(),
               required: true,
               where: {
@@ -167,7 +179,7 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
         }
       ]
     }
-    if (transaction) options.transaction = transaction
+    if (t) options.transaction = t
 
     return AccountVideoRateModel.findOne(options)
   }
@@ -208,7 +220,7 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
       ]
     }
 
-    return AccountVideoRateModel.findAndCountAll(query)
+    return AccountVideoRateModel.findAndCountAll<MAccountVideoRateAccountUrl>(query)
   }
 
   static cleanOldRatesOf (videoId: number, type: VideoRateType, beforeUpdatedAt: Date) {
@@ -241,7 +253,7 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
     })
   }
 
-  toFormattedJSON (): AccountVideoRate {
+  toFormattedJSON (this: MAccountVideoRateFormattable): AccountVideoRate {
     return {
       video: this.Video.toFormattedJSON(),
       rating: this.type
index 4dc4123018a5fc2dc6713df5afe447fa2786b9ff..ba1094536f9721e4e6c3e0a248ffc84bf848e016 100644 (file)
@@ -3,7 +3,8 @@ import {
   BeforeDestroy,
   BelongsTo,
   Column,
-  CreatedAt, DataType,
+  CreatedAt,
+  DataType,
   Default,
   DefaultScope,
   ForeignKey,
@@ -31,6 +32,8 @@ import { FindOptions, IncludeOptions, Op, Transaction, WhereOptions } from 'sequ
 import { AccountBlocklistModel } from './account-blocklist'
 import { ServerBlocklistModel } from '../server/server-blocklist'
 import { ActorFollowModel } from '../activitypub/actor-follow'
+import { MAccountActor, MAccountDefault, MAccountSummaryFormattable, MAccountFormattable, MAccountAP } from '../../typings/models'
+import * as Bluebird from 'bluebird'
 
 export enum ScopeNames {
   SUMMARY = 'SUMMARY'
@@ -229,11 +232,11 @@ export class AccountModel extends Model<AccountModel> {
     return undefined
   }
 
-  static load (id: number, transaction?: Transaction) {
+  static load (id: number, transaction?: Transaction): Bluebird<MAccountDefault> {
     return AccountModel.findByPk(id, { transaction })
   }
 
-  static loadByNameWithHost (nameWithHost: string) {
+  static loadByNameWithHost (nameWithHost: string): Bluebird<MAccountDefault> {
     const [ accountName, host ] = nameWithHost.split('@')
 
     if (!host || host === WEBSERVER.HOST) return AccountModel.loadLocalByName(accountName)
@@ -241,7 +244,7 @@ export class AccountModel extends Model<AccountModel> {
     return AccountModel.loadByNameAndHost(accountName, host)
   }
 
-  static loadLocalByName (name: string) {
+  static loadLocalByName (name: string): Bluebird<MAccountDefault> {
     const query = {
       where: {
         [ Op.or ]: [
@@ -271,7 +274,7 @@ export class AccountModel extends Model<AccountModel> {
     return AccountModel.findOne(query)
   }
 
-  static loadByNameAndHost (name: string, host: string) {
+  static loadByNameAndHost (name: string, host: string): Bluebird<MAccountDefault> {
     const query = {
       include: [
         {
@@ -296,7 +299,7 @@ export class AccountModel extends Model<AccountModel> {
     return AccountModel.findOne(query)
   }
 
-  static loadByUrl (url: string, transaction?: Transaction) {
+  static loadByUrl (url: string, transaction?: Transaction): Bluebird<MAccountDefault> {
     const query = {
       include: [
         {
@@ -329,7 +332,7 @@ export class AccountModel extends Model<AccountModel> {
       })
   }
 
-  static listLocalsForSitemap (sort: string) {
+  static listLocalsForSitemap (sort: string): Bluebird<MAccountActor[]> {
     const query = {
       attributes: [ ],
       offset: 0,
@@ -350,7 +353,7 @@ export class AccountModel extends Model<AccountModel> {
       .findAll(query)
   }
 
-  toFormattedJSON (): Account {
+  toFormattedJSON (this: MAccountFormattable): Account {
     const actor = this.Actor.toFormattedJSON()
     const account = {
       id: this.id,
@@ -364,8 +367,8 @@ export class AccountModel extends Model<AccountModel> {
     return Object.assign(actor, account)
   }
 
-  toFormattedSummaryJSON (): AccountSummary {
-    const actor = this.Actor.toFormattedJSON()
+  toFormattedSummaryJSON (this: MAccountSummaryFormattable): AccountSummary {
+    const actor = this.Actor.toFormattedSummaryJSON()
 
     return {
       id: this.id,
@@ -377,8 +380,8 @@ export class AccountModel extends Model<AccountModel> {
     }
   }
 
-  toActivityPubObject () {
-    const obj = this.Actor.toActivityPubObject(this.name, 'Account')
+  toActivityPubObject (this: MAccountAP) {
+    const obj = this.Actor.toActivityPubObject(this.name)
 
     return Object.assign(obj, {
       summary: this.description
index c2fbc6d23cc150b673bd9d02d524ef345718529f..dc69a17fda585fa61e0aa329d30a2b326be02046 100644 (file)
@@ -17,6 +17,7 @@ import { UserModel } from './user'
 import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications'
 import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model'
 import { clearCacheByUserId } from '../../lib/oauth-model'
+import { MNotificationSettingFormattable } from '@server/typings/models'
 
 @Table({
   tableName: 'userNotificationSetting',
@@ -110,6 +111,15 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM
   @Column
   newInstanceFollower: UserNotificationSettingValue
 
+  @AllowNull(false)
+  @Default(null)
+  @Is(
+    'UserNotificationSettingNewInstanceFollower',
+    value => throwIfNotValid(value, isUserNotificationSettingValid, 'autoInstanceFollowing')
+  )
+  @Column
+  autoInstanceFollowing: UserNotificationSettingValue
+
   @AllowNull(false)
   @Default(null)
   @Is(
@@ -152,7 +162,7 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM
     return clearCacheByUserId(instance.userId)
   }
 
-  toFormattedJSON (): UserNotificationSetting {
+  toFormattedJSON (this: MNotificationSettingFormattable): UserNotificationSetting {
     return {
       newCommentOnMyVideo: this.newCommentOnMyVideo,
       newVideoFromSubscription: this.newVideoFromSubscription,
@@ -164,7 +174,8 @@ export class UserNotificationSettingModel extends Model<UserNotificationSettingM
       newUserRegistration: this.newUserRegistration,
       commentMention: this.commentMention,
       newFollow: this.newFollow,
-      newInstanceFollower: this.newInstanceFollower
+      newInstanceFollower: this.newInstanceFollower,
+      autoInstanceFollowing: this.autoInstanceFollowing
     }
   }
 }
index f38cd7e781346415059e03aebda6e11e49292a6a..ccb81b891f68f77a141a314335addc3e3b60fe59 100644 (file)
@@ -16,6 +16,7 @@ import { ActorModel } from '../activitypub/actor'
 import { ActorFollowModel } from '../activitypub/actor-follow'
 import { AvatarModel } from '../avatar/avatar'
 import { ServerModel } from '../server/server'
+import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/typings/models/user'
 
 enum ScopeNames {
   WITH_ALL = 'WITH_ALL'
@@ -134,13 +135,18 @@ function buildAccountInclude (required: boolean, withActor = false) {
             ]
           },
           {
-            attributes: [ 'preferredUsername' ],
+            attributes: [ 'preferredUsername', 'type' ],
             model: ActorModel.unscoped(),
             required: true,
             as: 'ActorFollowing',
             include: [
               buildChannelInclude(false),
-              buildAccountInclude(false)
+              buildAccountInclude(false),
+              {
+                attributes: [ 'host' ],
+                model: ServerModel.unscoped(),
+                required: false
+              }
             ]
           }
         ]
@@ -371,7 +377,7 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
     return UserNotificationModel.update({ read: true }, query)
   }
 
-  toFormattedJSON (): UserNotification {
+  toFormattedJSON (this: UserNotificationModelForApi): UserNotification {
     const video = this.Video
       ? Object.assign(this.formatVideo(this.Video),{ channel: this.formatActor(this.Video.VideoChannel) })
       : undefined
@@ -403,6 +409,11 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
 
     const account = this.Account ? this.formatActor(this.Account) : undefined
 
+    const actorFollowingType = {
+      Application: 'instance' as 'instance',
+      Group: 'channel' as 'channel',
+      Person: 'account' as 'account'
+    }
     const actorFollow = this.ActorFollow ? {
       id: this.ActorFollow.id,
       state: this.ActorFollow.state,
@@ -414,9 +425,10 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
         host: this.ActorFollow.ActorFollower.getHost()
       },
       following: {
-        type: this.ActorFollow.ActorFollowing.VideoChannel ? 'channel' as 'channel' : 'account' as 'account',
+        type: actorFollowingType[this.ActorFollow.ActorFollowing.type],
         displayName: (this.ActorFollow.ActorFollowing.VideoChannel || this.ActorFollow.ActorFollowing.Account).getDisplayName(),
-        name: this.ActorFollow.ActorFollowing.preferredUsername
+        name: this.ActorFollow.ActorFollowing.preferredUsername,
+        host: this.ActorFollow.ActorFollowing.getHost()
       }
     } : undefined
 
@@ -436,7 +448,7 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
     }
   }
 
-  private formatVideo (video: VideoModel) {
+  formatVideo (this: UserNotificationModelForApi, video: UserNotificationIncludes.VideoInclude) {
     return {
       id: video.id,
       uuid: video.uuid,
@@ -444,7 +456,10 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
     }
   }
 
-  private formatActor (accountOrChannel: AccountModel | VideoChannelModel) {
+  formatActor (
+    this: UserNotificationModelForApi,
+    accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor
+  ) {
     const avatar = accountOrChannel.Actor.Avatar
       ? { path: accountOrChannel.Actor.Avatar.getStaticPath() }
       : undefined
index a862fc45fb859b0d842877c888b76233aec0962a..3fe4c8db1c8823921c9e5b07209e58e799f7a5ec 100644 (file)
@@ -1,7 +1,8 @@
 import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, IsInt, Model, Table, UpdatedAt } from 'sequelize-typescript'
 import { VideoModel } from '../video/video'
 import { UserModel } from './user'
-import { Transaction, Op, DestroyOptions } from 'sequelize'
+import { DestroyOptions, Op, Transaction } from 'sequelize'
+import { MUserAccountId, MUserId } from '@server/typings/models'
 
 @Table({
   tableName: 'userVideoHistory',
@@ -54,7 +55,7 @@ export class UserVideoHistoryModel extends Model<UserVideoHistoryModel> {
   })
   User: UserModel
 
-  static listForApi (user: UserModel, start: number, count: number) {
+  static listForApi (user: MUserAccountId, start: number, count: number) {
     return VideoModel.listForApi({
       start,
       count,
@@ -67,7 +68,7 @@ export class UserVideoHistoryModel extends Model<UserVideoHistoryModel> {
     })
   }
 
-  static removeUserHistoryBefore (user: UserModel, beforeDate: string, t: Transaction) {
+  static removeUserHistoryBefore (user: MUserId, beforeDate: string, t: Transaction) {
     const query: DestroyOptions = {
       where: {
         userId: user.id
index 0041bf5770f07de24c58d11c7a6980f771c43453..451e1fd6b94d36c49a18b6719ebaec302178bb75 100644 (file)
@@ -22,6 +22,7 @@ import {
 import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared'
 import { User, UserRole } from '../../../shared/models/users'
 import {
+  isNoInstanceConfigWarningModal,
   isUserAdminFlagsValid,
   isUserAutoPlayVideoValid,
   isUserBlockedReasonValid,
@@ -35,7 +36,8 @@ import {
   isUserVideoQuotaDailyValid,
   isUserVideoQuotaValid,
   isUserVideosHistoryEnabledValid,
-  isUserWebTorrentEnabledValid
+  isUserWebTorrentEnabledValid,
+  isNoWelcomeModal
 } from '../../helpers/custom-validators/users'
 import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto'
 import { OAuthTokenModel } from '../oauth/oauth-token'
@@ -54,6 +56,14 @@ import { VideoImportModel } from '../video/video-import'
 import { UserAdminFlag } from '../../../shared/models/users/user-flag.model'
 import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
 import { getThemeOrDefault } from '../../lib/plugins/theme-utils'
+import * as Bluebird from 'bluebird'
+import {
+  MUserDefault,
+  MUserFormattable,
+  MUserId,
+  MUserNotifSettingChannelDefault,
+  MUserWithNotificationSetting
+} from '@server/typings/models'
 
 enum ScopeNames {
   WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL'
@@ -195,6 +205,24 @@ export class UserModel extends Model<UserModel> {
   @Column
   theme: string
 
+  @AllowNull(false)
+  @Default(false)
+  @Is(
+    'UserNoInstanceConfigWarningModal',
+    value => throwIfNotValid(value, isNoInstanceConfigWarningModal, 'no instance config warning modal')
+  )
+  @Column
+  noInstanceConfigWarningModal: boolean
+
+  @AllowNull(false)
+  @Default(false)
+  @Is(
+    'UserNoInstanceConfigWarningModal',
+    value => throwIfNotValid(value, isNoWelcomeModal, 'no welcome modal')
+  )
+  @Column
+  noWelcomeModal: boolean
+
   @CreatedAt
   createdAt: Date
 
@@ -303,7 +331,7 @@ export class UserModel extends Model<UserModel> {
       })
   }
 
-  static listWithRight (right: UserRight) {
+  static listWithRight (right: UserRight): Bluebird<MUserDefault[]> {
     const roles = Object.keys(USER_ROLE_LABELS)
       .map(k => parseInt(k, 10) as UserRole)
       .filter(role => hasUserRight(role, right))
@@ -319,7 +347,7 @@ export class UserModel extends Model<UserModel> {
     return UserModel.findAll(query)
   }
 
-  static listUserSubscribersOf (actorId: number) {
+  static listUserSubscribersOf (actorId: number): Bluebird<MUserWithNotificationSetting[]> {
     const query = {
       include: [
         {
@@ -358,7 +386,7 @@ export class UserModel extends Model<UserModel> {
     return UserModel.unscoped().findAll(query)
   }
 
-  static listByUsernames (usernames: string[]) {
+  static listByUsernames (usernames: string[]): Bluebird<MUserDefault[]> {
     const query = {
       where: {
         username: usernames
@@ -368,11 +396,11 @@ export class UserModel extends Model<UserModel> {
     return UserModel.findAll(query)
   }
 
-  static loadById (id: number) {
+  static loadById (id: number): Bluebird<MUserDefault> {
     return UserModel.findByPk(id)
   }
 
-  static loadByUsername (username: string) {
+  static loadByUsername (username: string): Bluebird<MUserDefault> {
     const query = {
       where: {
         username: { [ Op.iLike ]: username }
@@ -382,7 +410,7 @@ export class UserModel extends Model<UserModel> {
     return UserModel.findOne(query)
   }
 
-  static loadByUsernameAndPopulateChannels (username: string) {
+  static loadByUsernameAndPopulateChannels (username: string): Bluebird<MUserNotifSettingChannelDefault> {
     const query = {
       where: {
         username: { [ Op.iLike ]: username }
@@ -392,7 +420,7 @@ export class UserModel extends Model<UserModel> {
     return UserModel.scope(ScopeNames.WITH_VIDEO_CHANNEL).findOne(query)
   }
 
-  static loadByEmail (email: string) {
+  static loadByEmail (email: string): Bluebird<MUserDefault> {
     const query = {
       where: {
         email
@@ -402,7 +430,7 @@ export class UserModel extends Model<UserModel> {
     return UserModel.findOne(query)
   }
 
-  static loadByUsernameOrEmail (username: string, email?: string) {
+  static loadByUsernameOrEmail (username: string, email?: string): Bluebird<MUserDefault> {
     if (!email) email = username
 
     const query = {
@@ -414,7 +442,7 @@ export class UserModel extends Model<UserModel> {
     return UserModel.findOne(query)
   }
 
-  static loadByVideoId (videoId: number) {
+  static loadByVideoId (videoId: number): Bluebird<MUserDefault> {
     const query = {
       include: [
         {
@@ -445,7 +473,7 @@ export class UserModel extends Model<UserModel> {
     return UserModel.findOne(query)
   }
 
-  static loadByVideoImportId (videoImportId: number) {
+  static loadByVideoImportId (videoImportId: number): Bluebird<MUserDefault> {
     const query = {
       include: [
         {
@@ -462,7 +490,7 @@ export class UserModel extends Model<UserModel> {
     return UserModel.findOne(query)
   }
 
-  static loadByChannelActorId (videoChannelActorId: number) {
+  static loadByChannelActorId (videoChannelActorId: number): Bluebird<MUserDefault> {
     const query = {
       include: [
         {
@@ -486,7 +514,7 @@ export class UserModel extends Model<UserModel> {
     return UserModel.findOne(query)
   }
 
-  static loadByAccountActorId (accountActorId: number) {
+  static loadByAccountActorId (accountActorId: number): Bluebird<MUserDefault> {
     const query = {
       include: [
         {
@@ -503,7 +531,7 @@ export class UserModel extends Model<UserModel> {
     return UserModel.findOne(query)
   }
 
-  static getOriginalVideoFileTotalFromUser (user: UserModel) {
+  static getOriginalVideoFileTotalFromUser (user: MUserId) {
     // Don't use sequelize because we need to use a sub query
     const query = UserModel.generateUserQuotaBaseSQL()
 
@@ -511,7 +539,7 @@ export class UserModel extends Model<UserModel> {
   }
 
   // Returns cumulative size of all video files uploaded in the last 24 hours.
-  static getOriginalVideoFileTotalDailyFromUser (user: UserModel) {
+  static getOriginalVideoFileTotalDailyFromUser (user: MUserId) {
     // Don't use sequelize because we need to use a sub query
     const query = UserModel.generateUserQuotaBaseSQL('"video"."createdAt" > now() - interval \'24 hours\'')
 
@@ -552,38 +580,52 @@ export class UserModel extends Model<UserModel> {
     return comparePassword(password, this.password)
   }
 
-  toFormattedJSON (parameters: { withAdminFlags?: boolean } = {}): User {
+  toFormattedJSON (this: MUserFormattable, parameters: { withAdminFlags?: boolean } = {}): User {
     const videoQuotaUsed = this.get('videoQuotaUsed')
     const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily')
 
-    const json = {
+    const json: User = {
       id: this.id,
       username: this.username,
       email: this.email,
+      theme: getThemeOrDefault(this.theme, DEFAULT_USER_THEME_NAME),
+
       pendingEmail: this.pendingEmail,
       emailVerified: this.emailVerified,
+
       nsfwPolicy: this.nsfwPolicy,
       webTorrentEnabled: this.webTorrentEnabled,
       videosHistoryEnabled: this.videosHistoryEnabled,
       autoPlayVideo: this.autoPlayVideo,
       videoLanguages: this.videoLanguages,
+
       role: this.role,
-      theme: getThemeOrDefault(this.theme, DEFAULT_USER_THEME_NAME),
       roleLabel: USER_ROLE_LABELS[ this.role ],
+
       videoQuota: this.videoQuota,
       videoQuotaDaily: this.videoQuotaDaily,
-      createdAt: this.createdAt,
+      videoQuotaUsed: videoQuotaUsed !== undefined
+        ? parseInt(videoQuotaUsed + '', 10)
+        : undefined,
+      videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined
+        ? parseInt(videoQuotaUsedDaily + '', 10)
+        : undefined,
+
+      noInstanceConfigWarningModal: this.noInstanceConfigWarningModal,
+      noWelcomeModal: this.noWelcomeModal,
+
       blocked: this.blocked,
       blockedReason: this.blockedReason,
+
       account: this.Account.toFormattedJSON(),
-      notificationSettings: this.NotificationSetting ? this.NotificationSetting.toFormattedJSON() : undefined,
+
+      notificationSettings: this.NotificationSetting
+        ? this.NotificationSetting.toFormattedJSON()
+        : undefined,
+
       videoChannels: [],
-      videoQuotaUsed: videoQuotaUsed !== undefined
-            ? parseInt(videoQuotaUsed + '', 10)
-            : undefined,
-      videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined
-            ? parseInt(videoQuotaUsedDaily + '', 10)
-            : undefined
+
+      createdAt: this.createdAt
     }
 
     if (parameters.withAdminFlags) {
index 51b09e09be14c039d7fcc712bbf6d81ee660fc88..8498692f0fcba3b049ebf03782673b36445fd489 100644 (file)
@@ -1,5 +1,5 @@
 import * as Bluebird from 'bluebird'
-import { values } from 'lodash'
+import { values, difference } from 'lodash'
 import {
   AfterCreate,
   AfterDestroy,
@@ -21,13 +21,20 @@ import { FollowState } from '../../../shared/models/actors'
 import { ActorFollow } from '../../../shared/models/actors/follow.model'
 import { logger } from '../../helpers/logger'
 import { getServerActor } from '../../helpers/utils'
-import { ACTOR_FOLLOW_SCORE, FOLLOW_STATES } from '../../initializers/constants'
+import { ACTOR_FOLLOW_SCORE, FOLLOW_STATES, SERVER_ACTOR_NAME } from '../../initializers/constants'
 import { ServerModel } from '../server/server'
 import { createSafeIn, getSort } from '../utils'
 import { ActorModel, unusedActorAttributesForAPI } from './actor'
 import { VideoChannelModel } from '../video/video-channel'
 import { AccountModel } from '../account/account'
-import { IncludeOptions, Op, Transaction, QueryTypes } from 'sequelize'
+import { IncludeOptions, Op, QueryTypes, Transaction } from 'sequelize'
+import {
+  MActorFollowActorsDefault,
+  MActorFollowActorsDefaultSubscription,
+  MActorFollowFollowingHost,
+  MActorFollowFormattable,
+  MActorFollowSubscriptions
+} from '@server/typings/models'
 
 @Table({
   tableName: 'actorFollow',
@@ -143,7 +150,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
     if (numberOfActorFollowsRemoved) logger.info('Removed bad %d actor follows.', numberOfActorFollowsRemoved)
   }
 
-  static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Transaction) {
+  static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Transaction): Bluebird<MActorFollowActorsDefault> {
     const query = {
       where: {
         actorId,
@@ -167,7 +174,12 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
     return ActorFollowModel.findOne(query)
   }
 
-  static loadByActorAndTargetNameAndHostForAPI (actorId: number, targetName: string, targetHost: string, t?: Transaction) {
+  static loadByActorAndTargetNameAndHostForAPI (
+    actorId: number,
+    targetName: string,
+    targetHost: string,
+    t?: Transaction
+  ): Bluebird<MActorFollowActorsDefaultSubscription> {
     const actorFollowingPartInclude: IncludeOptions = {
       model: ActorModel,
       required: true,
@@ -220,7 +232,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
       })
   }
 
-  static listSubscribedIn (actorId: number, targets: { name: string, host?: string }[]) {
+  static listSubscribedIn (actorId: number, targets: { name: string, host?: string }[]): Bluebird<MActorFollowFollowingHost[]> {
     const whereTab = targets
       .map(t => {
         if (t.host) {
@@ -314,7 +326,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
       ]
     }
 
-    return ActorFollowModel.findAndCountAll(query)
+    return ActorFollowModel.findAndCountAll<MActorFollowActorsDefault>(query)
       .then(({ rows, count }) => {
         return {
           data: rows,
@@ -357,7 +369,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
       ]
     }
 
-    return ActorFollowModel.findAndCountAll(query)
+    return ActorFollowModel.findAndCountAll<MActorFollowActorsDefault>(query)
                            .then(({ rows, count }) => {
                              return {
                                data: rows,
@@ -414,7 +426,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
       ]
     }
 
-    return ActorFollowModel.findAndCountAll(query)
+    return ActorFollowModel.findAndCountAll<MActorFollowSubscriptions>(query)
                            .then(({ rows, count }) => {
                              return {
                                data: rows.map(r => r.ActorFollowing.VideoChannel),
@@ -423,6 +435,45 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
                            })
   }
 
+  static async keepUnfollowedInstance (hosts: string[]) {
+    const followerId = (await getServerActor()).id
+
+    const query = {
+      attributes: [ 'id' ],
+      where: {
+        actorId: followerId
+      },
+      include: [
+        {
+          attributes: [ 'id' ],
+          model: ActorModel.unscoped(),
+          required: true,
+          as: 'ActorFollowing',
+          where: {
+            preferredUsername: SERVER_ACTOR_NAME
+          },
+          include: [
+            {
+              attributes: [ 'host' ],
+              model: ServerModel.unscoped(),
+              required: true,
+              where: {
+                host: {
+                  [Op.in]: hosts
+                }
+              }
+            }
+          ]
+        }
+      ]
+    }
+
+    const res = await ActorFollowModel.findAll(query)
+    const followedHosts = res.map(row => row.ActorFollowing.Server.host)
+
+    return difference(hosts, followedHosts)
+  }
+
   static listAcceptedFollowerUrlsForAP (actorIds: number[], t: Transaction, start?: number, count?: number) {
     return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count)
   }
@@ -569,7 +620,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
     return ActorFollowModel.findAll(query)
   }
 
-  toFormattedJSON (): ActorFollow {
+  toFormattedJSON (this: MActorFollowFormattable): ActorFollow {
     const follower = this.ActorFollower.toFormattedJSON()
     const following = this.ActorFollowing.toFormattedJSON()
 
index 9cc53f78ab4bcc34fe83c3169979f397ddc635d5..05de1905d57a1eb6401bb98e498660089adfb73f 100644 (file)
@@ -36,6 +36,17 @@ import { isOutdated, throwIfNotValid } from '../utils'
 import { VideoChannelModel } from '../video/video-channel'
 import { ActorFollowModel } from './actor-follow'
 import { VideoModel } from '../video/video'
+import {
+  MActor,
+  MActorAccountChannelId,
+  MActorAP,
+  MActorFormattable,
+  MActorFull,
+  MActorHost,
+  MActorServer,
+  MActorSummaryFormattable
+} from '../../typings/models'
+import * as Bluebird from 'bluebird'
 
 enum ScopeNames {
   FULL = 'FULL'
@@ -163,8 +174,8 @@ export class ActorModel extends Model<ActorModel> {
   @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
   inboxUrl: string
 
-  @AllowNull(false)
-  @Is('ActorOutboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'outbox url'))
+  @AllowNull(true)
+  @Is('ActorOutboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'outbox url', true))
   @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
   outboxUrl: string
 
@@ -173,13 +184,13 @@ export class ActorModel extends Model<ActorModel> {
   @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
   sharedInboxUrl: string
 
-  @AllowNull(false)
-  @Is('ActorFollowersUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'followers url'))
+  @AllowNull(true)
+  @Is('ActorFollowersUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'followers url', true))
   @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
   followersUrl: string
 
-  @AllowNull(false)
-  @Is('ActorFollowingUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'following url'))
+  @AllowNull(true)
+  @Is('ActorFollowingUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'following url', true))
   @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
   followingUrl: string
 
@@ -252,11 +263,15 @@ export class ActorModel extends Model<ActorModel> {
   })
   VideoChannel: VideoChannelModel
 
-  static load (id: number) {
+  static load (id: number): Bluebird<MActor> {
     return ActorModel.unscoped().findByPk(id)
   }
 
-  static loadAccountActorByVideoId (videoId: number, transaction: Sequelize.Transaction) {
+  static loadFull (id: number): Bluebird<MActorFull> {
+    return ActorModel.scope(ScopeNames.FULL).findByPk(id)
+  }
+
+  static loadFromAccountByVideoId (videoId: number, transaction: Sequelize.Transaction): Bluebird<MActor> {
     const query = {
       include: [
         {
@@ -300,7 +315,7 @@ export class ActorModel extends Model<ActorModel> {
       .then(a => !!a)
   }
 
-  static listByFollowersUrls (followersUrls: string[], transaction?: Sequelize.Transaction) {
+  static listByFollowersUrls (followersUrls: string[], transaction?: Sequelize.Transaction): Bluebird<MActorFull[]> {
     const query = {
       where: {
         followersUrl: {
@@ -313,7 +328,7 @@ export class ActorModel extends Model<ActorModel> {
     return ActorModel.scope(ScopeNames.FULL).findAll(query)
   }
 
-  static loadLocalByName (preferredUsername: string, transaction?: Sequelize.Transaction) {
+  static loadLocalByName (preferredUsername: string, transaction?: Sequelize.Transaction): Bluebird<MActorFull> {
     const query = {
       where: {
         preferredUsername,
@@ -325,7 +340,7 @@ export class ActorModel extends Model<ActorModel> {
     return ActorModel.scope(ScopeNames.FULL).findOne(query)
   }
 
-  static loadByNameAndHost (preferredUsername: string, host: string) {
+  static loadByNameAndHost (preferredUsername: string, host: string): Bluebird<MActorFull> {
     const query = {
       where: {
         preferredUsername
@@ -344,7 +359,7 @@ export class ActorModel extends Model<ActorModel> {
     return ActorModel.scope(ScopeNames.FULL).findOne(query)
   }
 
-  static loadByUrl (url: string, transaction?: Sequelize.Transaction) {
+  static loadByUrl (url: string, transaction?: Sequelize.Transaction): Bluebird<MActorAccountChannelId> {
     const query = {
       where: {
         url
@@ -367,7 +382,7 @@ export class ActorModel extends Model<ActorModel> {
     return ActorModel.unscoped().findOne(query)
   }
 
-  static loadByUrlAndPopulateAccountAndChannel (url: string, transaction?: Sequelize.Transaction) {
+  static loadByUrlAndPopulateAccountAndChannel (url: string, transaction?: Sequelize.Transaction): Bluebird<MActorFull> {
     const query = {
       where: {
         url
@@ -387,35 +402,35 @@ export class ActorModel extends Model<ActorModel> {
     })
   }
 
-  toFormattedJSON () {
+  toFormattedSummaryJSON (this: MActorSummaryFormattable) {
     let avatar: Avatar = null
     if (this.Avatar) {
       avatar = this.Avatar.toFormattedJSON()
     }
 
     return {
-      id: this.id,
       url: this.url,
       name: this.preferredUsername,
       host: this.getHost(),
+      avatar
+    }
+  }
+
+  toFormattedJSON (this: MActorFormattable) {
+    const base = this.toFormattedSummaryJSON()
+
+    return Object.assign(base, {
+      id: this.id,
       hostRedundancyAllowed: this.getRedundancyAllowed(),
       followingCount: this.followingCount,
       followersCount: this.followersCount,
-      avatar,
       createdAt: this.createdAt,
       updatedAt: this.updatedAt
-    }
+    })
   }
 
-  toActivityPubObject (name: string, type: 'Account' | 'Application' | 'VideoChannel') {
+  toActivityPubObject (this: MActorAP, name: string) {
     let activityPubType
-    if (type === 'Account') {
-      activityPubType = 'Person' as 'Person'
-    } else if (type === 'Application') {
-      activityPubType = 'Application' as 'Application'
-    } else { // VideoChannel
-      activityPubType = 'Group' as 'Group'
-    }
 
     let icon = undefined
     if (this.avatarId) {
@@ -428,7 +443,7 @@ export class ActorModel extends Model<ActorModel> {
     }
 
     const json = {
-      type: activityPubType,
+      type: this.type,
       id: this.url,
       following: this.getFollowingUrl(),
       followers: this.getFollowersUrl(),
@@ -494,7 +509,7 @@ export class ActorModel extends Model<ActorModel> {
     return this.serverId === null
   }
 
-  getWebfingerUrl () {
+  getWebfingerUrl (this: MActorServer) {
     return 'acct:' + this.preferredUsername + '@' + this.getHost()
   }
 
@@ -502,7 +517,7 @@ export class ActorModel extends Model<ActorModel> {
     return this.Server ? `${this.preferredUsername}@${this.Server.host}` : this.preferredUsername
   }
 
-  getHost () {
+  getHost (this: MActorHost) {
     return this.Server ? this.Server.host : WEBSERVER.HOST
   }
 
index b4014459297fdffff140c7116b19cfcdd3b6fc5a..950e4b181464626a68b8058e8b4f28a9d7db94ee 100644 (file)
@@ -7,6 +7,7 @@ import { remove } from 'fs-extra'
 import { CONFIG } from '../../initializers/config'
 import { throwIfNotValid } from '../utils'
 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
+import { MAvatarFormattable } from '@server/typings/models'
 
 @Table({
   tableName: 'avatar',
@@ -57,7 +58,7 @@ export class AvatarModel extends Model<AvatarModel> {
     return AvatarModel.findOne(query)
   }
 
-  toFormattedJSON (): Avatar {
+  toFormattedJSON (this: MAvatarFormattable): Avatar {
     return {
       path: this.getStaticPath(),
       createdAt: this.createdAt,
index 903d551dfec8fc5e12bb14bcea4ac4c1a9dc993e..b680be237a1bb1d5a2242f5c683ad9077378cca4 100644 (file)
@@ -18,6 +18,8 @@ import { Transaction } from 'sequelize'
 import { AccountModel } from '../account/account'
 import { ActorModel } from '../activitypub/actor'
 import { clearCacheByToken } from '../../lib/oauth-model'
+import * as Bluebird from 'bluebird'
+import { MOAuthTokenUser } from '@server/typings/models/oauth/oauth-token'
 
 export type OAuthTokenInfo = {
   refreshToken: string
@@ -160,7 +162,7 @@ export class OAuthTokenModel extends Model<OAuthTokenModel> {
       })
   }
 
-  static getByTokenAndPopulateUser (bearerToken: string) {
+  static getByTokenAndPopulateUser (bearerToken: string): Bluebird<MOAuthTokenUser> {
     const query = {
       where: {
         accessToken: bearerToken
@@ -170,13 +172,13 @@ export class OAuthTokenModel extends Model<OAuthTokenModel> {
     return OAuthTokenModel.scope(ScopeNames.WITH_USER)
                           .findOne(query)
                           .then(token => {
-                            if (token) token[ 'user' ] = token.User
+                            if (!token) return null
 
-                            return token
+                            return Object.assign(token, { user: token.User })
                           })
   }
 
-  static getByRefreshTokenAndPopulateUser (refreshToken: string) {
+  static getByRefreshTokenAndPopulateUser (refreshToken: string): Bluebird<MOAuthTokenUser> {
     const query = {
       where: {
         refreshToken: refreshToken
@@ -186,12 +188,9 @@ export class OAuthTokenModel extends Model<OAuthTokenModel> {
     return OAuthTokenModel.scope(ScopeNames.WITH_USER)
       .findOne(query)
       .then(token => {
-        if (token) {
-          token['user'] = token.User
-          return token
-        } else {
-          return new OAuthTokenModel()
-        }
+        if (!token) return new OAuthTokenModel()
+
+        return Object.assign(token, { user: token.User })
       })
   }
 
index 3df1c4f9cf1fa76977ef7ac942ca444df353b6a9..61d9a56126e40437fd96269f64f3ebb51c2d4e26 100644 (file)
@@ -30,6 +30,7 @@ import * as Bluebird from 'bluebird'
 import { col, FindOptions, fn, literal, Op, Transaction } from 'sequelize'
 import { VideoStreamingPlaylistModel } from '../video/video-streaming-playlist'
 import { CONFIG } from '../../initializers/config'
+import { MVideoRedundancy, MVideoRedundancyAP, MVideoRedundancyVideo } from '@server/typings/models'
 
 export enum ScopeNames {
   WITH_VIDEO = 'WITH_VIDEO'
@@ -166,7 +167,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
     return undefined
   }
 
-  static async loadLocalByFileId (videoFileId: number) {
+  static async loadLocalByFileId (videoFileId: number): Promise<MVideoRedundancyVideo> {
     const actor = await getServerActor()
 
     const query = {
@@ -179,7 +180,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
     return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query)
   }
 
-  static async loadLocalByStreamingPlaylistId (videoStreamingPlaylistId: number) {
+  static async loadLocalByStreamingPlaylistId (videoStreamingPlaylistId: number): Promise<MVideoRedundancyVideo> {
     const actor = await getServerActor()
 
     const query = {
@@ -192,7 +193,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
     return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query)
   }
 
-  static loadByUrl (url: string, transaction?: Transaction) {
+  static loadByUrl (url: string, transaction?: Transaction): Bluebird<MVideoRedundancy> {
     const query = {
       where: {
         url
@@ -306,7 +307,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
     return VideoRedundancyModel.getVideoSample(VideoModel.unscoped().findAll(query))
   }
 
-  static async loadOldestLocalThatAlreadyExpired (strategy: VideoRedundancyStrategy, expiresAfterMs: number) {
+  static async loadOldestLocalExpired (strategy: VideoRedundancyStrategy, expiresAfterMs: number): Promise<MVideoRedundancyVideo> {
     const expiredDate = new Date()
     expiredDate.setMilliseconds(expiredDate.getMilliseconds() - expiresAfterMs)
 
@@ -487,7 +488,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
     return !!this.strategy
   }
 
-  toActivityPubObject (): CacheFileObject {
+  toActivityPubObject (this: MVideoRedundancyAP): CacheFileObject {
     if (this.VideoStreamingPlaylist) {
       return {
         id: this.url,
index a15f9a7e28c905dd94bc3519a6028846baf2d41d..d094da1f560084b9d51d39e93364941d92a0e823 100644 (file)
@@ -11,6 +11,8 @@ import { PluginType } from '../../../shared/models/plugins/plugin.type'
 import { PeerTubePlugin } from '../../../shared/models/plugins/peertube-plugin.model'
 import { FindAndCountOptions, json } from 'sequelize'
 import { RegisterServerSettingOptions } from '../../../shared/models/plugins/register-server-setting.model'
+import * as Bluebird from 'bluebird'
+import { MPlugin, MPluginFormattable } from '@server/typings/models'
 
 @DefaultScope(() => ({
   attributes: {
@@ -85,7 +87,7 @@ export class PluginModel extends Model<PluginModel> {
   @UpdatedAt
   updatedAt: Date
 
-  static listEnabledPluginsAndThemes () {
+  static listEnabledPluginsAndThemes (): Bluebird<MPlugin[]> {
     const query = {
       where: {
         enabled: true,
@@ -96,7 +98,7 @@ export class PluginModel extends Model<PluginModel> {
     return PluginModel.findAll(query)
   }
 
-  static loadByNpmName (npmName: string) {
+  static loadByNpmName (npmName: string): Bluebird<MPlugin> {
     const name = this.normalizePluginName(npmName)
     const type = this.getTypeFromNpmName(npmName)
 
@@ -206,13 +208,13 @@ export class PluginModel extends Model<PluginModel> {
     if (options.pluginType) query.where['type'] = options.pluginType
 
     return PluginModel
-      .findAndCountAll(query)
+      .findAndCountAll<MPlugin>(query)
       .then(({ rows, count }) => {
         return { total: count, data: rows }
       })
   }
 
-  static listInstalled () {
+  static listInstalled (): Bluebird<MPlugin[]> {
     const query = {
       where: {
         uninstalled: false
@@ -251,7 +253,7 @@ export class PluginModel extends Model<PluginModel> {
     return result
   }
 
-  toFormattedJSON (): PeerTubePlugin {
+  toFormattedJSON (this: MPluginFormattable): PeerTubePlugin {
     return {
       name: this.name,
       type: this.type,
index 5138b0f76367b8e6e183375515508efab2b2c0ee..3e96871911f6215b9f613d776056811e58d45a9e 100644 (file)
@@ -3,6 +3,8 @@ import { AccountModel } from '../account/account'
 import { ServerModel } from './server'
 import { ServerBlock } from '../../../shared/models/blocklist'
 import { getSort } from '../utils'
+import * as Bluebird from 'bluebird'
+import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/typings/models'
 
 enum ScopeNames {
   WITH_ACCOUNT = 'WITH_ACCOUNT',
@@ -73,7 +75,7 @@ export class ServerBlocklistModel extends Model<ServerBlocklistModel> {
   })
   BlockedServer: ServerModel
 
-  static loadByAccountAndHost (accountId: number, host: string) {
+  static loadByAccountAndHost (accountId: number, host: string): Bluebird<MServerBlocklist> {
     const query = {
       where: {
         accountId
@@ -104,13 +106,13 @@ export class ServerBlocklistModel extends Model<ServerBlocklistModel> {
 
     return ServerBlocklistModel
       .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_SERVER ])
-      .findAndCountAll(query)
+      .findAndCountAll<MServerBlocklistAccountServer>(query)
       .then(({ rows, count }) => {
         return { total: count, data: rows }
       })
   }
 
-  toFormattedJSON (): ServerBlock {
+  toFormattedJSON (this: MServerBlocklistFormattable): ServerBlock {
     return {
       byAccount: this.ByAccount.toFormattedJSON(),
       blockedServer: this.BlockedServer.toFormattedJSON(),
index 1d211f1e06b2619bd6a4142c31bfd0852d3b0792..8b07115f1f810a43c0403f7ef7b75b3702ff64a0 100644 (file)
@@ -2,8 +2,9 @@ import { AllowNull, Column, CreatedAt, Default, HasMany, Is, Model, Table, Updat
 import { isHostValid } from '../../helpers/custom-validators/servers'
 import { ActorModel } from '../activitypub/actor'
 import { throwIfNotValid } from '../utils'
-import { AccountBlocklistModel } from '../account/account-blocklist'
 import { ServerBlocklistModel } from './server-blocklist'
+import * as Bluebird from 'bluebird'
+import { MServer, MServerFormattable } from '@server/typings/models/server'
 
 @Table({
   tableName: 'server',
@@ -50,7 +51,17 @@ export class ServerModel extends Model<ServerModel> {
   })
   BlockedByAccounts: ServerBlocklistModel[]
 
-  static loadByHost (host: string) {
+  static load (id: number): Bluebird<MServer> {
+    const query = {
+      where: {
+        id
+      }
+    }
+
+    return ServerModel.findOne(query)
+  }
+
+  static loadByHost (host: string): Bluebird<MServer> {
     const query = {
       where: {
         host
@@ -64,7 +75,7 @@ export class ServerModel extends Model<ServerModel> {
     return this.BlockedByAccounts && this.BlockedByAccounts.length !== 0
   }
 
-  toFormattedJSON () {
+  toFormattedJSON (this: MServerFormattable) {
     return {
       host: this.host
     }
index 603d556924c7e4f59845963175b4662288758502..fc2a424aaaa56f3c5ab0c0120d462a4bf3078ef4 100644 (file)
@@ -2,6 +2,7 @@ import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Model, Ta
 import { ScopeNames as VideoScopeNames, VideoModel } from './video'
 import { VideoPrivacy } from '../../../shared/models/videos'
 import { Op, Transaction } from 'sequelize'
+import { MScheduleVideoUpdateFormattable } from '@server/typings/models'
 
 @Table({
   tableName: 'scheduleVideoUpdate',
@@ -96,7 +97,7 @@ export class ScheduleVideoUpdateModel extends Model<ScheduleVideoUpdateModel> {
     return ScheduleVideoUpdateModel.destroy(query)
   }
 
-  toFormattedJSON () {
+  toFormattedJSON (this: MScheduleVideoUpdateFormattable) {
     return {
       updateAt: this.updateAt,
       privacy: this.privacy || undefined
index 0fc3cfd4cfae9f8bac8c09c81a899c73ea965401..ed8df8b48aa84367003161035dda9256eac13c76 100644 (file)
@@ -1,11 +1,12 @@
 import * as Bluebird from 'bluebird'
-import { QueryTypes, Transaction } from 'sequelize'
+import { fn, QueryTypes, Transaction, col } from 'sequelize'
 import { AllowNull, BelongsToMany, Column, CreatedAt, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
 import { isVideoTagValid } from '../../helpers/custom-validators/videos'
 import { throwIfNotValid } from '../utils'
 import { VideoModel } from './video'
 import { VideoTagModel } from './video-tag'
 import { VideoPrivacy, VideoState } from '../../../shared/models/videos'
+import { MTag } from '@server/typings/models'
 
 @Table({
   tableName: 'tag',
@@ -14,6 +15,10 @@ import { VideoPrivacy, VideoState } from '../../../shared/models/videos'
     {
       fields: [ 'name' ],
       unique: true
+    },
+    {
+      name: 'tag_lower_name',
+      fields: [ fn('lower', col('name')) ] as any // FIXME: typings
     }
   ]
 })
@@ -37,10 +42,10 @@ export class TagModel extends Model<TagModel> {
   })
   Videos: VideoModel[]
 
-  static findOrCreateTags (tags: string[], transaction: Transaction) {
-    if (tags === null) return []
+  static findOrCreateTags (tags: string[], transaction: Transaction): Promise<MTag[]> {
+    if (tags === null) return Promise.resolve([])
 
-    const tasks: Bluebird<TagModel>[] = []
+    const tasks: Bluebird<MTag>[] = []
     tags.forEach(tag => {
       const query = {
         where: {
@@ -52,7 +57,7 @@ export class TagModel extends Model<TagModel> {
         transaction
       }
 
-      const promise = TagModel.findOrCreate(query)
+      const promise = TagModel.findOrCreate<MTag>(query)
         .then(([ tagInstance ]) => tagInstance)
       tasks.push(promise)
     })
index 1ac7919b31e2c70f8d06c08a6ffd881e205565c8..3636db18de73c125e02fbf5c0831c98b2d6a524c 100644 (file)
@@ -7,10 +7,13 @@ import {
   isVideoAbuseStateValid
 } from '../../helpers/custom-validators/video-abuses'
 import { AccountModel } from '../account/account'
-import { getSort, throwIfNotValid } from '../utils'
+import { buildBlockedAccountSQL, getSort, throwIfNotValid } from '../utils'
 import { VideoModel } from './video'
 import { VideoAbuseState } from '../../../shared'
 import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants'
+import { MUserAccountId, MVideoAbuse, MVideoAbuseFormattable, MVideoAbuseVideo } from '../../typings/models'
+import * as Bluebird from 'bluebird'
+import { literal, Op } from 'sequelize'
 
 @Table({
   tableName: 'videoAbuse',
@@ -73,7 +76,7 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
   })
   Video: VideoModel
 
-  static loadByIdAndVideoId (id: number, videoId: number) {
+  static loadByIdAndVideoId (id: number, videoId: number): Bluebird<MVideoAbuse> {
     const query = {
       where: {
         id,
@@ -83,11 +86,25 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
     return VideoAbuseModel.findOne(query)
   }
 
-  static listForApi (start: number, count: number, sort: string) {
+  static listForApi (parameters: {
+    start: number,
+    count: number,
+    sort: string,
+    serverAccountId: number
+    user?: MUserAccountId
+  }) {
+    const { start, count, sort, user, serverAccountId } = parameters
+    const userAccountId = user ? user.Account.id : undefined
+
     const query = {
       offset: start,
       limit: count,
       order: getSort(sort),
+      where: {
+        reporterAccountId: {
+          [Op.notIn]: literal('(' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')')
+        }
+      },
       include: [
         {
           model: AccountModel,
@@ -106,7 +123,7 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
       })
   }
 
-  toFormattedJSON (): VideoAbuse {
+  toFormattedJSON (this: MVideoAbuseFormattable): VideoAbuse {
     return {
       id: this.id,
       reason: this.reason,
@@ -125,7 +142,7 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
     }
   }
 
-  toActivityPubObject (): VideoAbuseObject {
+  toActivityPubObject (this: MVideoAbuseVideo): VideoAbuseObject {
     return {
       type: 'Flag' as 'Flag',
       content: this.reason,
index cdb725e7ad82c4db8a96f24898c84231cc4117fb..694983cb316a0d623a1aaddce51bb1f6f14b18dc 100644 (file)
@@ -1,12 +1,14 @@
 import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
-import { getBlacklistSort, getSort, SortType, throwIfNotValid } from '../utils'
-import { ScopeNames as VideoModelScopeNames, VideoModel } from './video'
+import { getBlacklistSort, SortType, throwIfNotValid } from '../utils'
+import { VideoModel } from './video'
 import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel'
 import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../helpers/custom-validators/video-blacklist'
 import { VideoBlacklist, VideoBlacklistType } from '../../../shared/models/videos'
 import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
-import { FindOptions, literal } from 'sequelize'
+import { FindOptions } from 'sequelize'
 import { ThumbnailModel } from './thumbnail'
+import * as Bluebird from 'bluebird'
+import { MVideoBlacklist, MVideoBlacklistFormattable } from '@server/typings/models'
 
 @Table({
   tableName: 'videoBlacklist',
@@ -98,7 +100,7 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> {
     })
   }
 
-  static loadByVideoId (id: number) {
+  static loadByVideoId (id: number): Bluebird<MVideoBlacklist> {
     const query = {
       where: {
         videoId: id
@@ -108,7 +110,7 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> {
     return VideoBlacklistModel.findOne(query)
   }
 
-  toFormattedJSON (): VideoBlacklist {
+  toFormattedJSON (this: MVideoBlacklistFormattable): VideoBlacklist {
     return {
       id: this.id,
       createdAt: this.createdAt,
index a01565851d2d0bfc621c33ceefec5fb1b949eb52..ad580176857b3727a0f1a00c0f81fe6a1db89d2c 100644 (file)
@@ -21,6 +21,8 @@ import { join } from 'path'
 import { logger } from '../../helpers/logger'
 import { remove } from 'fs-extra'
 import { CONFIG } from '../../initializers/config'
+import * as Bluebird from 'bluebird'
+import { MVideoCaptionFormattable, MVideoCaptionVideo } from '@server/typings/models'
 
 export enum ScopeNames {
   WITH_VIDEO_UUID_AND_REMOTE = 'WITH_VIDEO_UUID_AND_REMOTE'
@@ -30,7 +32,7 @@ export enum ScopeNames {
   [ScopeNames.WITH_VIDEO_UUID_AND_REMOTE]: {
     include: [
       {
-        attributes: [ 'uuid', 'remote' ],
+        attributes: [ 'id', 'uuid', 'remote' ],
         model: VideoModel.unscoped(),
         required: true
       }
@@ -93,7 +95,7 @@ export class VideoCaptionModel extends Model<VideoCaptionModel> {
     return undefined
   }
 
-  static loadByVideoIdAndLanguage (videoId: string | number, language: string) {
+  static loadByVideoIdAndLanguage (videoId: string | number, language: string): Bluebird<MVideoCaptionVideo> {
     const videoInclude = {
       model: VideoModel.unscoped(),
       attributes: [ 'id', 'remote', 'uuid' ],
@@ -122,7 +124,7 @@ export class VideoCaptionModel extends Model<VideoCaptionModel> {
       .then(([ caption ]) => caption)
   }
 
-  static listVideoCaptions (videoId: number) {
+  static listVideoCaptions (videoId: number): Bluebird<MVideoCaptionVideo[]> {
     const query = {
       order: [ [ 'language', 'ASC' ] ] as OrderItem[],
       where: {
@@ -152,7 +154,7 @@ export class VideoCaptionModel extends Model<VideoCaptionModel> {
     return this.Video.remote === false
   }
 
-  toFormattedJSON (): VideoCaption {
+  toFormattedJSON (this: MVideoCaptionFormattable): VideoCaption {
     return {
       language: {
         id: this.language,
@@ -162,15 +164,15 @@ export class VideoCaptionModel extends Model<VideoCaptionModel> {
     }
   }
 
-  getCaptionStaticPath () {
+  getCaptionStaticPath (this: MVideoCaptionFormattable) {
     return join(LAZY_STATIC_PATHS.VIDEO_CAPTIONS, this.getCaptionName())
   }
 
-  getCaptionName () {
+  getCaptionName (this: MVideoCaptionFormattable) {
     return `${this.Video.uuid}-${this.language}.vtt`
   }
 
-  removeCaptionFile () {
+  removeCaptionFile (this: MVideoCaptionFormattable) {
     return remove(CONFIG.STORAGE.CAPTIONS_DIR + this.getCaptionName())
   }
 }
index b545a2f8c9528f67bd6a262cb61bd9e3a72a427f..f7a351329bc7ab7999be91260e2ce713a2703b10 100644 (file)
@@ -3,6 +3,8 @@ import { AccountModel } from '../account/account'
 import { ScopeNames as VideoScopeNames, VideoModel } from './video'
 import { VideoChangeOwnership, VideoChangeOwnershipStatus } from '../../../shared/models/videos'
 import { getSort } from '../utils'
+import { MVideoChangeOwnershipFormattable, MVideoChangeOwnershipFull } from '@server/typings/models/video/video-change-ownership'
+import * as Bluebird from 'bluebird'
 
 enum ScopeNames {
   WITH_ACCOUNTS = 'WITH_ACCOUNTS',
@@ -108,16 +110,16 @@ export class VideoChangeOwnershipModel extends Model<VideoChangeOwnershipModel>
 
     return Promise.all([
       VideoChangeOwnershipModel.scope(ScopeNames.WITH_ACCOUNTS).count(query),
-      VideoChangeOwnershipModel.scope([ ScopeNames.WITH_ACCOUNTS, ScopeNames.WITH_VIDEO ]).findAll(query)
+      VideoChangeOwnershipModel.scope([ ScopeNames.WITH_ACCOUNTS, ScopeNames.WITH_VIDEO ]).findAll<MVideoChangeOwnershipFull>(query)
     ]).then(([ count, rows ]) => ({ total: count, data: rows }))
   }
 
-  static load (id: number) {
+  static load (id: number): Bluebird<MVideoChangeOwnershipFull> {
     return VideoChangeOwnershipModel.scope([ ScopeNames.WITH_ACCOUNTS, ScopeNames.WITH_VIDEO ])
                                     .findByPk(id)
   }
 
-  toFormattedJSON (): VideoChangeOwnership {
+  toFormattedJSON (this: MVideoChangeOwnershipFormattable): VideoChangeOwnership {
     return {
       id: this.id,
       status: this.status,
index 6241a75a30bfffd499cede0102bd4734781cc838..05545bd9d6f9d6e925d0b4f7a1f78065a2a6d6f7 100644 (file)
@@ -33,6 +33,15 @@ import { ServerModel } from '../server/server'
 import { FindOptions, ModelIndexesOptions, Op } from 'sequelize'
 import { AvatarModel } from '../avatar/avatar'
 import { VideoPlaylistModel } from './video-playlist'
+import * as Bluebird from 'bluebird'
+import {
+  MChannelAccountDefault,
+  MChannelActor,
+  MChannelActorAccountDefaultVideos,
+  MChannelAP,
+  MChannelFormattable,
+  MChannelSummaryFormattable
+} from '../../typings/models/video'
 
 // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
 const indexes: ModelIndexesOptions[] = [
@@ -47,7 +56,7 @@ const indexes: ModelIndexesOptions[] = [
 ]
 
 export enum ScopeNames {
-  AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
+  FOR_API = 'FOR_API',
   WITH_ACCOUNT = 'WITH_ACCOUNT',
   WITH_ACTOR = 'WITH_ACTOR',
   WITH_VIDEOS = 'WITH_VIDEOS',
@@ -74,10 +83,10 @@ export type SummaryOptions = {
 @Scopes(() => ({
   [ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => {
     const base: FindOptions = {
-      attributes: [ 'name', 'description', 'id', 'actorId' ],
+      attributes: [ 'id', 'name', 'description', 'actorId' ],
       include: [
         {
-          attributes: [ 'preferredUsername', 'url', 'serverId', 'avatarId' ],
+          attributes: [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ],
           model: ActorModel.unscoped(),
           required: true,
           include: [
@@ -106,7 +115,7 @@ export type SummaryOptions = {
 
     return base
   },
-  [ScopeNames.AVAILABLE_FOR_LIST]: (options: AvailableForListOptions) => {
+  [ScopeNames.FOR_API]: (options: AvailableForListOptions) => {
     // Only list local channels OR channels that are on an instance followed by actorId
     const inQueryInstanceFollow = buildServerIdsFollowedBy(options.actorId)
 
@@ -268,7 +277,7 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
     }
 
     const scopes = {
-      method: [ ScopeNames.AVAILABLE_FOR_LIST, { actorId } as AvailableForListOptions ]
+      method: [ ScopeNames.FOR_API, { actorId } as AvailableForListOptions ]
     }
     return VideoChannelModel
       .scope(scopes)
@@ -278,7 +287,7 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
       })
   }
 
-  static listLocalsForSitemap (sort: string) {
+  static listLocalsForSitemap (sort: string): Bluebird<MChannelActor[]> {
     const query = {
       attributes: [ ],
       offset: 0,
@@ -331,7 +340,7 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
     }
 
     const scopes = {
-      method: [ ScopeNames.AVAILABLE_FOR_LIST, { actorId: options.actorId } as AvailableForListOptions ]
+      method: [ ScopeNames.FOR_API, { actorId: options.actorId } as AvailableForListOptions ]
     }
     return VideoChannelModel
       .scope(scopes)
@@ -369,13 +378,13 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
       })
   }
 
-  static loadByIdAndPopulateAccount (id: number) {
+  static loadByIdAndPopulateAccount (id: number): Bluebird<MChannelAccountDefault> {
     return VideoChannelModel.unscoped()
       .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
       .findByPk(id)
   }
 
-  static loadByIdAndAccount (id: number, accountId: number) {
+  static loadByIdAndAccount (id: number, accountId: number): Bluebird<MChannelAccountDefault> {
     const query = {
       where: {
         id,
@@ -388,13 +397,13 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
       .findOne(query)
   }
 
-  static loadAndPopulateAccount (id: number) {
+  static loadAndPopulateAccount (id: number): Bluebird<MChannelAccountDefault> {
     return VideoChannelModel.unscoped()
       .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
       .findByPk(id)
   }
 
-  static loadByUrlAndPopulateAccount (url: string) {
+  static loadByUrlAndPopulateAccount (url: string): Bluebird<MChannelAccountDefault> {
     const query = {
       include: [
         {
@@ -420,7 +429,7 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
     return VideoChannelModel.loadByNameAndHostAndPopulateAccount(name, host)
   }
 
-  static loadLocalByNameAndPopulateAccount (name: string) {
+  static loadLocalByNameAndPopulateAccount (name: string): Bluebird<MChannelAccountDefault> {
     const query = {
       include: [
         {
@@ -439,7 +448,7 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
       .findOne(query)
   }
 
-  static loadByNameAndHostAndPopulateAccount (name: string, host: string) {
+  static loadByNameAndHostAndPopulateAccount (name: string, host: string): Bluebird<MChannelAccountDefault> {
     const query = {
       include: [
         {
@@ -464,7 +473,7 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
       .findOne(query)
   }
 
-  static loadAndPopulateAccountAndVideos (id: number) {
+  static loadAndPopulateAccountAndVideos (id: number): Bluebird<MChannelActorAccountDefaultVideos> {
     const options = {
       include: [
         VideoModel
@@ -476,7 +485,20 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
       .findByPk(id, options)
   }
 
-  toFormattedJSON (): VideoChannel {
+  toFormattedSummaryJSON (this: MChannelSummaryFormattable): VideoChannelSummary {
+    const actor = this.Actor.toFormattedSummaryJSON()
+
+    return {
+      id: this.id,
+      name: actor.name,
+      displayName: this.getDisplayName(),
+      url: actor.url,
+      host: actor.host,
+      avatar: actor.avatar
+    }
+  }
+
+  toFormattedJSON (this: MChannelFormattable): VideoChannel {
     const actor = this.Actor.toFormattedJSON()
     const videoChannel = {
       id: this.id,
@@ -494,21 +516,8 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
     return Object.assign(actor, videoChannel)
   }
 
-  toFormattedSummaryJSON (): VideoChannelSummary {
-    const actor = this.Actor.toFormattedJSON()
-
-    return {
-      id: this.id,
-      name: actor.name,
-      displayName: this.getDisplayName(),
-      url: actor.url,
-      host: actor.host,
-      avatar: actor.avatar
-    }
-  }
-
-  toActivityPubObject (): ActivityPubActor {
-    const obj = this.Actor.toActivityPubObject(this.name, 'VideoChannel')
+  toActivityPubObject (this: MChannelAP): ActivityPubActor {
+    const obj = this.Actor.toActivityPubObject(this.name)
 
     return Object.assign(obj, {
       summary: this.description,
index 58b75510dfde221371cfc84df0536552fee25595..2e4220434e3dee354d93c51977f87382c8dd6cdb 100644 (file)
@@ -1,36 +1,32 @@
-import {
-  AllowNull,
-  BeforeDestroy,
-  BelongsTo,
-  Column,
-  CreatedAt,
-  DataType,
-  ForeignKey,
-  Is,
-  Model,
-  Scopes,
-  Table,
-  UpdatedAt
-} from 'sequelize-typescript'
+import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
 import { ActivityTagObject } from '../../../shared/models/activitypub/objects/common-objects'
 import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object'
 import { VideoComment } from '../../../shared/models/videos/video-comment.model'
 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
 import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
-import { sendDeleteVideoComment } from '../../lib/activitypub/send'
 import { AccountModel } from '../account/account'
 import { ActorModel } from '../activitypub/actor'
-import { AvatarModel } from '../avatar/avatar'
-import { ServerModel } from '../server/server'
 import { buildBlockedAccountSQL, buildLocalAccountIdsIn, getSort, throwIfNotValid } from '../utils'
 import { VideoModel } from './video'
 import { VideoChannelModel } from './video-channel'
 import { getServerActor } from '../../helpers/utils'
-import { UserModel } from '../account/user'
 import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor'
 import { regexpCapture } from '../../helpers/regexp'
 import { uniq } from 'lodash'
-import { FindOptions, literal, Op, Order, ScopeOptions, Sequelize, Transaction } from 'sequelize'
+import { FindOptions, Op, Order, ScopeOptions, Sequelize, Transaction } from 'sequelize'
+import * as Bluebird from 'bluebird'
+import {
+  MComment,
+  MCommentAP,
+  MCommentFormattable,
+  MCommentId,
+  MCommentOwner,
+  MCommentOwnerReplyVideoLight,
+  MCommentOwnerVideo,
+  MCommentOwnerVideoFeed,
+  MCommentOwnerVideoReply
+} from '../../typings/models/video'
+import { MUserAccountId } from '@server/typings/models'
 
 enum ScopeNames {
   WITH_ACCOUNT = 'WITH_ACCOUNT',
@@ -68,22 +64,7 @@ enum ScopeNames {
   [ScopeNames.WITH_ACCOUNT]: {
     include: [
       {
-        model: AccountModel,
-        include: [
-          {
-            model: ActorModel,
-            include: [
-              {
-                model: ServerModel,
-                required: false
-              },
-              {
-                model: AvatarModel,
-                required: false
-              }
-            ]
-          }
-        ]
+        model: AccountModel
       }
     ]
   },
@@ -102,22 +83,12 @@ enum ScopeNames {
         required: true,
         include: [
           {
-            model: VideoChannelModel.unscoped(),
+            model: VideoChannelModel,
             required: true,
             include: [
-              {
-                model: ActorModel,
-                required: true
-              },
               {
                 model: AccountModel,
-                required: true,
-                include: [
-                  {
-                    model: ActorModel,
-                    required: true
-                  }
-                ]
+                required: true
               }
             ]
           }
@@ -212,7 +183,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
   })
   Account: AccountModel
 
-  static loadById (id: number, t?: Transaction) {
+  static loadById (id: number, t?: Transaction): Bluebird<MComment> {
     const query: FindOptions = {
       where: {
         id
@@ -224,7 +195,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
     return VideoCommentModel.findOne(query)
   }
 
-  static loadByIdAndPopulateVideoAndAccountAndReply (id: number, t?: Transaction) {
+  static loadByIdAndPopulateVideoAndAccountAndReply (id: number, t?: Transaction): Bluebird<MCommentOwnerVideoReply> {
     const query: FindOptions = {
       where: {
         id
@@ -238,7 +209,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
       .findOne(query)
   }
 
-  static loadByUrlAndPopulateAccountAndVideo (url: string, t?: Transaction) {
+  static loadByUrlAndPopulateAccountAndVideo (url: string, t?: Transaction): Bluebird<MCommentOwnerVideo> {
     const query: FindOptions = {
       where: {
         url
@@ -250,7 +221,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
     return VideoCommentModel.scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_VIDEO ]).findOne(query)
   }
 
-  static loadByUrlAndPopulateReplyAndVideoUrlAndAccount (url: string, t?: Transaction) {
+  static loadByUrlAndPopulateReplyAndVideoUrlAndAccount (url: string, t?: Transaction): Bluebird<MCommentOwnerReplyVideoLight> {
     const query: FindOptions = {
       where: {
         url
@@ -273,7 +244,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
     start: number,
     count: number,
     sort: string,
-    user?: UserModel
+    user?: MUserAccountId
   }) {
     const { videoId, start, count, sort, user } = parameters
 
@@ -314,7 +285,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
   static async listThreadCommentsForApi (parameters: {
     videoId: number,
     threadId: number,
-    user?: UserModel
+    user?: MUserAccountId
   }) {
     const { videoId, threadId, user } = parameters
 
@@ -353,7 +324,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
       })
   }
 
-  static listThreadParentComments (comment: VideoCommentModel, t: Transaction, order: 'ASC' | 'DESC' = 'ASC') {
+  static listThreadParentComments (comment: MCommentId, t: Transaction, order: 'ASC' | 'DESC' = 'ASC'): Bluebird<MCommentOwner[]> {
     const query = {
       order: [ [ 'createdAt', order ] ] as Order,
       where: {
@@ -389,10 +360,10 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
       transaction: t
     }
 
-    return VideoCommentModel.findAndCountAll(query)
+    return VideoCommentModel.findAndCountAll<MComment>(query)
   }
 
-  static listForFeed (start: number, count: number, videoId?: number) {
+  static listForFeed (start: number, count: number, videoId?: number): Bluebird<MCommentOwnerVideoFeed[]> {
     const query = {
       order: [ [ 'createdAt', 'DESC' ] ] as Order,
       offset: start,
@@ -506,7 +477,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
     return uniq(result)
   }
 
-  toFormattedJSON () {
+  toFormattedJSON (this: MCommentFormattable) {
     return {
       id: this.id,
       url: this.url,
@@ -521,7 +492,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
     } as VideoComment
   }
 
-  toActivityPubObject (threadParentComments: VideoCommentModel[]): VideoCommentObject {
+  toActivityPubObject (this: MCommentAP, threadParentComments: MCommentOwner[]): VideoCommentObject {
     let inReplyTo: string
     // New thread, so in AS we reply to the video
     if (this.inReplyToCommentId === null) {
index 05c4907594fbbd36c7d86e0e8a5373c3fe24a664..6304f741ce0aa272d2ad0bb2b1c76961ec5a2e35 100644 (file)
@@ -25,6 +25,7 @@ import { VideoRedundancyModel } from '../redundancy/video-redundancy'
 import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
 import { FindOptions, QueryTypes, Transaction } from 'sequelize'
 import { MIMETYPES } from '../../initializers/constants'
+import { MVideoFile } from '@server/typings/models'
 
 @Table({
   tableName: 'videoFile',
@@ -166,7 +167,7 @@ export class VideoFileModel extends Model<VideoFileModel> {
     return !!MIMETYPES.AUDIO.EXT_MIMETYPE[this.extname]
   }
 
-  hasSameUniqueKeysThan (other: VideoFileModel) {
+  hasSameUniqueKeysThan (other: MVideoFile) {
     return this.fps === other.fps &&
       this.resolution === other.resolution &&
       this.videoId === other.videoId
index 284539deff6551125d3c15e9df5df6f027ee1f90..2987aa780757228c925a2b6c76f2d6fe93b9ba76 100644 (file)
@@ -1,6 +1,5 @@
 import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos'
 import { VideoModel } from './video'
-import { VideoFileModel } from './video-file'
 import {
   ActivityPlaylistInfohashesObject,
   ActivityPlaylistSegmentHashesObject,
@@ -17,7 +16,9 @@ import {
 } from '../../lib/activitypub'
 import { isArray } from '../../helpers/custom-validators/misc'
 import { VideoStreamingPlaylist } from '../../../shared/models/videos/video-streaming-playlist.model'
-import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
+import { MStreamingPlaylistRedundanciesOpt, MVideo, MVideoAP, MVideoFormattable, MVideoFormattableDetails } from '../../typings/models'
+import { MStreamingPlaylistRedundancies } from '../../typings/models/video/video-streaming-playlist'
+import { MVideoFileRedundanciesOpt } from '../../typings/models/video/video-file'
 
 export type VideoFormattingJSONOptions = {
   completeDescription?: boolean
@@ -28,7 +29,7 @@ export type VideoFormattingJSONOptions = {
     blacklistInfo?: boolean
   }
 }
-function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormattingJSONOptions): Video {
+function videoModelToFormattedJSON (video: MVideoFormattable, options?: VideoFormattingJSONOptions): Video {
   const userHistory = isArray(video.UserVideoHistories) ? video.UserVideoHistories[0] : undefined
 
   const videoObject: Video = {
@@ -102,7 +103,7 @@ function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormatting
   return videoObject
 }
 
-function videoModelToFormattedDetailsJSON (video: VideoModel): VideoDetails {
+function videoModelToFormattedDetailsJSON (video: MVideoFormattableDetails): VideoDetails {
   const formattedJson = video.toFormattedJSON({
     additionalAttributes: {
       scheduledUpdate: true,
@@ -114,7 +115,7 @@ function videoModelToFormattedDetailsJSON (video: VideoModel): VideoDetails {
 
   const tags = video.Tags ? video.Tags.map(t => t.name) : []
 
-  const streamingPlaylists = streamingPlaylistsModelToFormattedJSON(video, video.VideoStreamingPlaylists)
+  const streamingPlaylists = streamingPlaylistsModelToFormattedJSON(video.VideoStreamingPlaylists)
 
   const detailsJson = {
     support: video.support,
@@ -142,7 +143,7 @@ function videoModelToFormattedDetailsJSON (video: VideoModel): VideoDetails {
   return Object.assign(formattedJson, detailsJson)
 }
 
-function streamingPlaylistsModelToFormattedJSON (video: VideoModel, playlists: VideoStreamingPlaylistModel[]): VideoStreamingPlaylist[] {
+function streamingPlaylistsModelToFormattedJSON (playlists: MStreamingPlaylistRedundanciesOpt[]): VideoStreamingPlaylist[] {
   if (isArray(playlists) === false) return []
 
   return playlists
@@ -161,7 +162,7 @@ function streamingPlaylistsModelToFormattedJSON (video: VideoModel, playlists: V
     })
 }
 
-function videoFilesModelToFormattedJSON (video: VideoModel, videoFiles: VideoFileModel[]): VideoFile[] {
+function videoFilesModelToFormattedJSON (video: MVideo, videoFiles: MVideoFileRedundanciesOpt[]): VideoFile[] {
   const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
 
   return videoFiles
@@ -189,7 +190,7 @@ function videoFilesModelToFormattedJSON (video: VideoModel, videoFiles: VideoFil
     })
 }
 
-function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject {
+function videoModelToActivityPubObject (video: MVideoAP): VideoTorrentObject {
   const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
   if (!video.Tags) video.Tags = []
 
index 480a671c883d059df97197b5d8d934cebfe5c430..af5314ce9be806fb7be7bbfbbf50f2224189b898 100644 (file)
@@ -20,6 +20,8 @@ import { isVideoImportStateValid, isVideoImportTargetUrlValid } from '../../help
 import { VideoImport, VideoImportState } from '../../../shared'
 import { isVideoMagnetUriValid } from '../../helpers/custom-validators/videos'
 import { UserModel } from '../account/user'
+import * as Bluebird from 'bluebird'
+import { MVideoImportDefault, MVideoImportFormattable } from '@server/typings/models/video/video-import'
 
 @DefaultScope(() => ({
   include: [
@@ -28,7 +30,11 @@ import { UserModel } from '../account/user'
       required: true
     },
     {
-      model: VideoModel.scope([ VideoModelScopeNames.WITH_ACCOUNT_DETAILS, VideoModelScopeNames.WITH_TAGS]),
+      model: VideoModel.scope([
+        VideoModelScopeNames.WITH_ACCOUNT_DETAILS,
+        VideoModelScopeNames.WITH_TAGS,
+        VideoModelScopeNames.WITH_THUMBNAILS
+      ]),
       required: false
     }
   ]
@@ -114,7 +120,7 @@ export class VideoImportModel extends Model<VideoImportModel> {
     return undefined
   }
 
-  static loadAndPopulateVideo (id: number) {
+  static loadAndPopulateVideo (id: number): Bluebird<MVideoImportDefault> {
     return VideoImportModel.findByPk(id)
   }
 
@@ -135,7 +141,7 @@ export class VideoImportModel extends Model<VideoImportModel> {
       }
     }
 
-    return VideoImportModel.findAndCountAll(query)
+    return VideoImportModel.findAndCountAll<MVideoImportDefault>(query)
                            .then(({ rows, count }) => {
                              return {
                                data: rows,
@@ -148,7 +154,7 @@ export class VideoImportModel extends Model<VideoImportModel> {
     return this.targetUrl || this.magnetUri || this.torrentName
   }
 
-  toFormattedJSON (): VideoImport {
+  toFormattedJSON (this: MVideoImportFormattable): VideoImport {
     const videoFormatOptions = {
       completeDescription: true,
       additionalAttributes: { state: true, waitTranscoding: true, scheduledUpdate: true }
index dd7653533d25e31775b32e5435ff3e8aeaaeacee..a2802131349715f0949d0bc313184342859558e5 100644 (file)
@@ -21,10 +21,18 @@ import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
 import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object'
 import * as validator from 'validator'
 import { AggregateOptions, Op, ScopeOptions, Sequelize, Transaction } from 'sequelize'
-import { UserModel } from '../account/user'
 import { VideoPlaylistElement, VideoPlaylistElementType } from '../../../shared/models/videos/playlist/video-playlist-element.model'
 import { AccountModel } from '../account/account'
 import { VideoPrivacy } from '../../../shared/models/videos'
+import * as Bluebird from 'bluebird'
+import {
+  MVideoPlaylistElement,
+  MVideoPlaylistElementAP,
+  MVideoPlaylistElementFormattable,
+  MVideoPlaylistElementVideoUrlPlaylistPrivacy,
+  MVideoPlaylistVideoThumbnail
+} from '@server/typings/models/video/video-playlist-element'
+import { MUserAccountId } from '@server/typings/models'
 
 @Table({
   tableName: 'videoPlaylistElement',
@@ -116,7 +124,7 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel>
     count: number,
     videoPlaylistId: number,
     serverAccount: AccountModel,
-    user?: UserModel
+    user?: MUserAccountId
   }) {
     const accountIds = [ options.serverAccount.id ]
     const videoScope: (ScopeOptions | string)[] = [
@@ -162,7 +170,7 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel>
     ]).then(([ total, data ]) => ({ total, data }))
   }
 
-  static loadByPlaylistAndVideo (videoPlaylistId: number, videoId: number) {
+  static loadByPlaylistAndVideo (videoPlaylistId: number, videoId: number): Bluebird<MVideoPlaylistElement> {
     const query = {
       where: {
         videoPlaylistId,
@@ -173,11 +181,14 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel>
     return VideoPlaylistElementModel.findOne(query)
   }
 
-  static loadById (playlistElementId: number) {
+  static loadById (playlistElementId: number): Bluebird<MVideoPlaylistElement> {
     return VideoPlaylistElementModel.findByPk(playlistElementId)
   }
 
-  static loadByPlaylistAndVideoForAP (playlistId: number | string, videoId: number | string) {
+  static loadByPlaylistAndVideoForAP (
+    playlistId: number | string,
+    videoId: number | string
+  ): Bluebird<MVideoPlaylistElementVideoUrlPlaylistPrivacy> {
     const playlistWhere = validator.isUUID('' + playlistId) ? { uuid: playlistId } : { id: playlistId }
     const videoWhere = validator.isUUID('' + videoId) ? { uuid: videoId } : { id: videoId }
 
@@ -218,7 +229,7 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel>
       })
   }
 
-  static loadFirstElementWithVideoThumbnail (videoPlaylistId: number) {
+  static loadFirstElementWithVideoThumbnail (videoPlaylistId: number): Bluebird<MVideoPlaylistVideoThumbnail> {
     const query = {
       order: getSort('position'),
       where: {
@@ -290,7 +301,7 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel>
     return VideoPlaylistElementModel.increment({ position: by }, query)
   }
 
-  getType (displayNSFW?: boolean, accountId?: number) {
+  getType (this: MVideoPlaylistElementFormattable, displayNSFW?: boolean, accountId?: number) {
     const video = this.Video
 
     if (!video) return VideoPlaylistElementType.DELETED
@@ -306,14 +317,17 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel>
     return VideoPlaylistElementType.REGULAR
   }
 
-  getVideoElement (displayNSFW?: boolean, accountId?: number) {
+  getVideoElement (this: MVideoPlaylistElementFormattable, displayNSFW?: boolean, accountId?: number) {
     if (!this.Video) return null
     if (this.getType(displayNSFW, accountId) !== VideoPlaylistElementType.REGULAR) return null
 
     return this.Video.toFormattedJSON()
   }
 
-  toFormattedJSON (options: { displayNSFW?: boolean, accountId?: number } = {}): VideoPlaylistElement {
+  toFormattedJSON (
+    this: MVideoPlaylistElementFormattable,
+    options: { displayNSFW?: boolean, accountId?: number } = {}
+  ): VideoPlaylistElement {
     return {
       id: this.id,
       position: this.position,
@@ -326,7 +340,7 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel>
     }
   }
 
-  toActivityPubObject (): PlaylistElementObject {
+  toActivityPubObject (this: MVideoPlaylistElementAP): PlaylistElementObject {
     const base: PlaylistElementObject = {
       id: this.url,
       type: 'PlaylistElement',
index c8e97c491d1866633a3432be57c239461ce33d5d..278d80ac051d2144cd34be068256b3425f3d2e93 100644 (file)
@@ -43,6 +43,15 @@ import { VideoPlaylistType } from '../../../shared/models/videos/playlist/video-
 import { ThumbnailModel } from './thumbnail'
 import { ActivityIconObject } from '../../../shared/models/activitypub/objects'
 import { FindOptions, literal, Op, ScopeOptions, Transaction, WhereOptions } from 'sequelize'
+import * as Bluebird from 'bluebird'
+import {
+  MVideoPlaylistAccountThumbnail, MVideoPlaylistAP,
+  MVideoPlaylistFormattable,
+  MVideoPlaylistFull,
+  MVideoPlaylistFullSummary,
+  MVideoPlaylistIdWithElements
+} from '../../typings/models/video/video-playlist'
+import { MThumbnail } from '../../typings/models/video/thumbnail'
 
 enum ScopeNames {
   AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
@@ -332,7 +341,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
                              })
   }
 
-  static listPlaylistIdsOf (accountId: number, videoIds: number[]) {
+  static listPlaylistIdsOf (accountId: number, videoIds: number[]): Bluebird<MVideoPlaylistIdWithElements[]> {
     const query = {
       attributes: [ 'id' ],
       where: {
@@ -368,7 +377,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
       .then(e => !!e)
   }
 
-  static loadWithAccountAndChannelSummary (id: number | string, transaction: Transaction) {
+  static loadWithAccountAndChannelSummary (id: number | string, transaction: Transaction): Bluebird<MVideoPlaylistFullSummary> {
     const where = buildWhereIdOrUUID(id)
 
     const query = {
@@ -381,7 +390,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
       .findOne(query)
   }
 
-  static loadWithAccountAndChannel (id: number | string, transaction: Transaction) {
+  static loadWithAccountAndChannel (id: number | string, transaction: Transaction): Bluebird<MVideoPlaylistFull> {
     const where = buildWhereIdOrUUID(id)
 
     const query = {
@@ -394,7 +403,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
       .findOne(query)
   }
 
-  static loadByUrlAndPopulateAccount (url: string) {
+  static loadByUrlAndPopulateAccount (url: string): Bluebird<MVideoPlaylistAccountThumbnail> {
     const query = {
       where: {
         url
@@ -423,7 +432,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
     return VideoPlaylistModel.update({ privacy: VideoPlaylistPrivacy.PRIVATE, videoChannelId: null }, query)
   }
 
-  async setAndSaveThumbnail (thumbnail: ThumbnailModel, t: Transaction) {
+  async setAndSaveThumbnail (thumbnail: MThumbnail, t: Transaction) {
     thumbnail.videoPlaylistId = this.id
 
     this.Thumbnail = await thumbnail.save({ transaction: t })
@@ -471,7 +480,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
     return isOutdated(this, ACTIVITY_PUB.VIDEO_PLAYLIST_REFRESH_INTERVAL)
   }
 
-  toFormattedJSON (): VideoPlaylist {
+  toFormattedJSON (this: MVideoPlaylistFormattable): VideoPlaylist {
     return {
       id: this.id,
       uuid: this.uuid,
@@ -501,7 +510,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
     }
   }
 
-  toActivityPubObject (page: number, t: Transaction): Promise<PlaylistObject> {
+  toActivityPubObject (this: MVideoPlaylistAP, page: number, t: Transaction): Promise<PlaylistObject> {
     const handler = (start: number, count: number) => {
       return VideoPlaylistElementModel.listUrlsOfForAP(this.id, start, count, t)
     }
index d8ed64557917ea907e3f6b34be801dd5d86929da..9019b401abeda0d0439cebb63673c91f7effd370 100644 (file)
@@ -8,6 +8,8 @@ import { buildLocalActorIdsIn, throwIfNotValid } from '../utils'
 import { VideoModel } from './video'
 import { VideoChannelModel } from './video-channel'
 import { Op, Transaction } from 'sequelize'
+import { MVideoShareActor, MVideoShareFull } from '../../typings/models/video'
+import { MActorDefault } from '../../typings/models'
 
 enum ScopeNames {
   FULL = 'FULL',
@@ -88,7 +90,7 @@ export class VideoShareModel extends Model<VideoShareModel> {
   })
   Video: VideoModel
 
-  static load (actorId: number, videoId: number, t?: Transaction) {
+  static load (actorId: number, videoId: number, t?: Transaction): Bluebird<MVideoShareActor> {
     return VideoShareModel.scope(ScopeNames.WITH_ACTOR).findOne({
       where: {
         actorId,
@@ -98,7 +100,7 @@ export class VideoShareModel extends Model<VideoShareModel> {
     })
   }
 
-  static loadByUrl (url: string, t: Transaction) {
+  static loadByUrl (url: string, t: Transaction): Bluebird<MVideoShareFull> {
     return VideoShareModel.scope(ScopeNames.FULL).findOne({
       where: {
         url
@@ -107,7 +109,7 @@ export class VideoShareModel extends Model<VideoShareModel> {
     })
   }
 
-  static loadActorsByShare (videoId: number, t: Transaction) {
+  static loadActorsByShare (videoId: number, t: Transaction): Bluebird<MActorDefault[]> {
     const query = {
       where: {
         videoId
@@ -122,10 +124,10 @@ export class VideoShareModel extends Model<VideoShareModel> {
     }
 
     return VideoShareModel.scope(ScopeNames.FULL).findAll(query)
-      .then(res => res.map(r => r.Actor))
+      .then((res: MVideoShareFull[]) => res.map(r => r.Actor))
   }
 
-  static loadActorsWhoSharedVideosOf (actorOwnerId: number, t: Transaction): Bluebird<ActorModel[]> {
+  static loadActorsWhoSharedVideosOf (actorOwnerId: number, t: Transaction): Bluebird<MActorDefault[]> {
     const query = {
       attributes: [],
       include: [
@@ -163,7 +165,7 @@ export class VideoShareModel extends Model<VideoShareModel> {
       .then(res => res.map(r => r.Actor))
   }
 
-  static loadActorsByVideoChannel (videoChannelId: number, t: Transaction): Bluebird<ActorModel[]> {
+  static loadActorsByVideoChannel (videoChannelId: number, t: Transaction): Bluebird<MActorDefault[]> {
     const query = {
       attributes: [],
       include: [
index 31dc82c541109ed56a72078928e9e9aae4b9e2f6..0ea90d28c40561a0d050ca534b042179002997b5 100644 (file)
@@ -1,16 +1,16 @@
-import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, HasMany, Is, Model, Table, UpdatedAt, DataType } from 'sequelize-typescript'
+import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
 import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
 import { throwIfNotValid } from '../utils'
 import { VideoModel } from './video'
 import { VideoRedundancyModel } from '../redundancy/video-redundancy'
 import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
-import { CONSTRAINTS_FIELDS, STATIC_PATHS, P2P_MEDIA_LOADER_PEER_VERSION } from '../../initializers/constants'
-import { VideoFileModel } from './video-file'
+import { CONSTRAINTS_FIELDS, P2P_MEDIA_LOADER_PEER_VERSION, STATIC_PATHS } from '../../initializers/constants'
 import { join } from 'path'
 import { sha1 } from '../../helpers/core-utils'
 import { isArrayOf } from '../../helpers/custom-validators/misc'
-import { QueryTypes, Op } from 'sequelize'
+import { Op, QueryTypes } from 'sequelize'
+import { MStreamingPlaylist, MVideoFile } from '@server/typings/models'
 
 @Table({
   tableName: 'videoStreamingPlaylist',
@@ -91,7 +91,7 @@ export class VideoStreamingPlaylistModel extends Model<VideoStreamingPlaylistMod
               .then(results => results.length === 1)
   }
 
-  static buildP2PMediaLoaderInfoHashes (playlistUrl: string, videoFiles: VideoFileModel[]) {
+  static buildP2PMediaLoaderInfoHashes (playlistUrl: string, videoFiles: MVideoFile[]) {
     const hashes: string[] = []
 
     // 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
     return baseUrlHttp + STATIC_PATHS.REDUNDANCY + this.getStringType() + '/' + this.Video.uuid
   }
 
-  hasSameUniqueKeysThan (other: VideoStreamingPlaylistModel) {
+  hasSameUniqueKeysThan (other: MStreamingPlaylist) {
     return this.type === other.type &&
       this.videoId === other.videoId
   }
index b59df397d470a2ccce64866bef5ed8d5133c73c5..6856dcd9f057baa5f7d0f7c895afee3d7df8e43f 100644 (file)
@@ -36,7 +36,7 @@ import {
   Table,
   UpdatedAt
 } from 'sequelize-typescript'
-import { UserRight, VideoPrivacy, VideoResolution, VideoState } from '../../../shared'
+import { UserRight, VideoPrivacy, VideoState } from '../../../shared'
 import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
 import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos'
 import { VideoFilter } from '../../../shared/models/videos/video-query.type'
@@ -111,7 +111,6 @@ import {
   videoModelToFormattedJSON
 } from './video-format-utils'
 import { UserVideoHistoryModel } from '../account/user-video-history'
-import { UserModel } from '../account/user'
 import { VideoImportModel } from './video-import'
 import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
 import { VideoPlaylistElementModel } from './video-playlist-element'
@@ -120,6 +119,29 @@ import { ThumbnailModel } from './thumbnail'
 import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
 import { createTorrentPromise } from '../../helpers/webtorrent'
 import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
+import {
+  MChannel,
+  MChannelAccountDefault,
+  MChannelId,
+  MUserAccountId,
+  MUserId,
+  MVideoAccountLight,
+  MVideoAccountLightBlacklistAllFiles,
+  MVideoAP,
+  MVideoDetails,
+  MVideoFormattable,
+  MVideoFormattableDetails,
+  MVideoForUser,
+  MVideoFullLight,
+  MVideoIdThumbnail,
+  MVideoThumbnail,
+  MVideoThumbnailBlacklist,
+  MVideoWithAllFiles,
+  MVideoWithFile,
+  MVideoWithRights
+} from '../../typings/models'
+import { MVideoFile, MVideoFileRedundanciesOpt } from '../../typings/models/video/video-file'
+import { MThumbnail } from '../../typings/models/video/thumbnail'
 
 // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
 const indexes: (ModelIndexesOptions & { where?: WhereOptions })[] = [
@@ -232,8 +254,8 @@ export type AvailableForListIDsOptions = {
   videoPlaylistId?: number
 
   trendingDays?: number
-  user?: UserModel,
-  historyOfUser?: UserModel
+  user?: MUserAccountId
+  historyOfUser?: MUserId
 
   baseWhere?: WhereOptions[]
 }
@@ -446,13 +468,15 @@ export type AvailableForListIDsOptions = {
     // FIXME: issues with sequelize count when making a join on n:m relation, so we just make a IN()
     if (options.tagsAllOf || options.tagsOneOf) {
       if (options.tagsOneOf) {
+        const tagsOneOfLower = options.tagsOneOf.map(t => t.toLowerCase())
+
         whereAnd.push({
           id: {
             [ Op.in ]: Sequelize.literal(
               '(' +
               'SELECT "videoId" FROM "videoTag" ' +
               'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' +
-              'WHERE "tag"."name" IN (' + createSafeIn(VideoModel, options.tagsOneOf) + ')' +
+              'WHERE lower("tag"."name") IN (' + createSafeIn(VideoModel, tagsOneOfLower) + ')' +
               ')'
             )
           }
@@ -460,14 +484,16 @@ export type AvailableForListIDsOptions = {
       }
 
       if (options.tagsAllOf) {
+        const tagsAllOfLower = options.tagsAllOf.map(t => t.toLowerCase())
+
         whereAnd.push({
           id: {
             [ Op.in ]: Sequelize.literal(
               '(' +
               'SELECT "videoId" FROM "videoTag" ' +
               'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' +
-              'WHERE "tag"."name" IN (' + createSafeIn(VideoModel, options.tagsAllOf) + ')' +
-              'GROUP BY "videoTag"."videoId" HAVING COUNT(*) = ' + options.tagsAllOf.length +
+              'WHERE lower("tag"."name") IN (' + createSafeIn(VideoModel, tagsAllOfLower) + ')' +
+              'GROUP BY "videoTag"."videoId" HAVING COUNT(*) = ' + tagsAllOfLower.length +
               ')'
             )
           }
@@ -634,7 +660,7 @@ export type AvailableForListIDsOptions = {
   [ ScopeNames.WITH_BLACKLISTED ]: {
     include: [
       {
-        attributes: [ 'id', 'reason' ],
+        attributes: [ 'id', 'reason', 'unfederated' ],
         model: VideoBlacklistModel,
         required: false
       }
@@ -989,18 +1015,16 @@ export class VideoModel extends Model<VideoModel> {
   VideoCaptions: VideoCaptionModel[]
 
   @BeforeDestroy
-  static async sendDelete (instance: VideoModel, options) {
+  static async sendDelete (instance: MVideoAccountLight, options) {
     if (instance.isOwned()) {
       if (!instance.VideoChannel) {
         instance.VideoChannel = await instance.$get('VideoChannel', {
           include: [
-            {
-              model: AccountModel,
-              include: [ ActorModel ]
-            }
+            ActorModel,
+            AccountModel
           ],
           transaction: options.transaction
-        }) as VideoChannelModel
+        }) as MChannelAccountDefault
       }
 
       return sendDeleteVideo(instance, options.transaction)
@@ -1039,7 +1063,7 @@ export class VideoModel extends Model<VideoModel> {
     return undefined
   }
 
-  static listLocal () {
+  static listLocal (): Bluebird<MVideoWithAllFiles[]> {
     const query = {
       where: {
         remote: false
@@ -1159,7 +1183,7 @@ export class VideoModel extends Model<VideoModel> {
     })
   }
 
-  static listUserVideosForApi (accountId: number, start: number, count: number, sort: string, withFiles = false) {
+  static listUserVideosForApi (accountId: number, start: number, count: number, sort: string) {
     function buildBaseQuery (): FindOptions {
       return {
         offset: start,
@@ -1192,16 +1216,9 @@ export class VideoModel extends Model<VideoModel> {
       ScopeNames.WITH_THUMBNAILS
     ]
 
-    if (withFiles === true) {
-      findQuery.include.push({
-        model: VideoFileModel.unscoped(),
-        required: true
-      })
-    }
-
     return Promise.all([
       VideoModel.count(countQuery),
-      VideoModel.scope(findScopes).findAll(findQuery)
+      VideoModel.scope(findScopes).findAll<MVideoForUser>(findQuery)
     ]).then(([ count, rows ]) => {
       return {
         data: rows,
@@ -1228,8 +1245,8 @@ export class VideoModel extends Model<VideoModel> {
     followerActorId?: number
     videoPlaylistId?: number,
     trendingDays?: number,
-    user?: UserModel,
-    historyOfUser?: UserModel
+    user?: MUserAccountId,
+    historyOfUser?: MUserId
   }, countVideos = true) {
     if (options.filter && options.filter === 'all-local' && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) {
       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> {
     tagsAllOf?: string[]
     durationMin?: number // seconds
     durationMax?: number // seconds
-    user?: UserModel,
+    user?: MUserAccountId,
     filter?: VideoFilter
   }) {
     const whereAnd = []
@@ -1387,7 +1404,7 @@ export class VideoModel extends Model<VideoModel> {
     return VideoModel.getAvailableForApi(query, queryOptions)
   }
 
-  static load (id: number | string, t?: Transaction) {
+  static load (id: number | string, t?: Transaction): Bluebird<MVideoThumbnail> {
     const where = buildWhereIdOrUUID(id)
     const options = {
       where,
@@ -1397,7 +1414,20 @@ export class VideoModel extends Model<VideoModel> {
     return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
   }
 
-  static loadWithRights (id: number | string, t?: Transaction) {
+  static loadWithBlacklist (id: number | string, t?: Transaction): Bluebird<MVideoThumbnailBlacklist> {
+    const where = buildWhereIdOrUUID(id)
+    const options = {
+      where,
+      transaction: t
+    }
+
+    return VideoModel.scope([
+      ScopeNames.WITH_THUMBNAILS,
+      ScopeNames.WITH_BLACKLISTED
+    ]).findOne(options)
+  }
+
+  static loadWithRights (id: number | string, t?: Transaction): Bluebird<MVideoWithRights> {
     const where = buildWhereIdOrUUID(id)
     const options = {
       where,
@@ -1411,7 +1441,7 @@ export class VideoModel extends Model<VideoModel> {
     ]).findOne(options)
   }
 
-  static loadOnlyId (id: number | string, t?: Transaction) {
+  static loadOnlyId (id: number | string, t?: Transaction): Bluebird<MVideoIdThumbnail> {
     const where = buildWhereIdOrUUID(id)
 
     const options = {
@@ -1423,7 +1453,7 @@ export class VideoModel extends Model<VideoModel> {
     return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
   }
 
-  static loadWithFiles (id: number | string, t?: Transaction, logging?: boolean) {
+  static loadWithFiles (id: number | string, t?: Transaction, logging?: boolean): Bluebird<MVideoWithAllFiles> {
     const where = buildWhereIdOrUUID(id)
 
     const query = {
@@ -1439,7 +1469,7 @@ export class VideoModel extends Model<VideoModel> {
     ]).findOne(query)
   }
 
-  static loadByUUID (uuid: string) {
+  static loadByUUID (uuid: string): Bluebird<MVideoThumbnail> {
     const options = {
       where: {
         uuid
@@ -1449,7 +1479,7 @@ export class VideoModel extends Model<VideoModel> {
     return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
   }
 
-  static loadByUrl (url: string, transaction?: Transaction) {
+  static loadByUrl (url: string, transaction?: Transaction): Bluebird<MVideoThumbnail> {
     const query: FindOptions = {
       where: {
         url
@@ -1460,7 +1490,7 @@ export class VideoModel extends Model<VideoModel> {
     return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(query)
   }
 
-  static loadByUrlAndPopulateAccount (url: string, transaction?: Transaction) {
+  static loadByUrlAndPopulateAccount (url: string, transaction?: Transaction): Bluebird<MVideoAccountLightBlacklistAllFiles> {
     const query: FindOptions = {
       where: {
         url
@@ -1472,11 +1502,12 @@ export class VideoModel extends Model<VideoModel> {
       ScopeNames.WITH_ACCOUNT_DETAILS,
       ScopeNames.WITH_FILES,
       ScopeNames.WITH_STREAMING_PLAYLISTS,
-      ScopeNames.WITH_THUMBNAILS
+      ScopeNames.WITH_THUMBNAILS,
+      ScopeNames.WITH_BLACKLISTED
     ]).findOne(query)
   }
 
-  static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Transaction, userId?: number) {
+  static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Transaction, userId?: number): Bluebird<MVideoFullLight> {
     const where = buildWhereIdOrUUID(id)
 
     const options = {
@@ -1508,7 +1539,7 @@ export class VideoModel extends Model<VideoModel> {
     id: number | string,
     t?: Transaction,
     userId?: number
-  }) {
+  }): Bluebird<MVideoDetails> {
     const { id, t, userId } = parameters
     const where = buildWhereIdOrUUID(id)
 
@@ -1586,7 +1617,7 @@ export class VideoModel extends Model<VideoModel> {
                      .then(results => results.length === 1)
   }
 
-  static bulkUpdateSupportField (videoChannel: VideoChannelModel, t: Transaction) {
+  static bulkUpdateSupportField (videoChannel: MChannel, t: Transaction) {
     const options = {
       where: {
         channelId: videoChannel.id
@@ -1597,7 +1628,7 @@ export class VideoModel extends Model<VideoModel> {
     return VideoModel.update({ support: videoChannel.support }, options)
   }
 
-  static getAllIdsFromChannel (videoChannel: VideoChannelModel) {
+  static getAllIdsFromChannel (videoChannel: MChannelId): Bluebird<number[]> {
     const query = {
       attributes: [ 'id' ],
       where: {
@@ -1756,20 +1787,20 @@ export class VideoModel extends Model<VideoModel> {
       this.VideoChannel.Account.isBlocked()
   }
 
-  getOriginalFile () {
+  getOriginalFile <T extends MVideoWithFile> (this: T) {
     if (Array.isArray(this.VideoFiles) === false) return undefined
 
     // The original file is the file that have the higher resolution
     return maxBy(this.VideoFiles, file => file.resolution)
   }
 
-  getFile (resolution: number) {
+  getFile <T extends MVideoWithFile> (this: T, resolution: number) {
     if (Array.isArray(this.VideoFiles) === false) return undefined
 
     return this.VideoFiles.find(f => f.resolution === resolution)
   }
 
-  async addAndSaveThumbnail (thumbnail: ThumbnailModel, transaction: Transaction) {
+  async addAndSaveThumbnail (thumbnail: MThumbnail, transaction: Transaction) {
     thumbnail.videoId = this.id
 
     const savedThumbnail = await thumbnail.save({ transaction })
@@ -1782,7 +1813,7 @@ export class VideoModel extends Model<VideoModel> {
     this.Thumbnails.push(savedThumbnail)
   }
 
-  getVideoFilename (videoFile: VideoFileModel) {
+  getVideoFilename (videoFile: MVideoFile) {
     return this.uuid + '-' + videoFile.resolution + videoFile.extname
   }
 
@@ -1806,7 +1837,7 @@ export class VideoModel extends Model<VideoModel> {
     return this.Thumbnails.find(t => t.type === ThumbnailType.PREVIEW)
   }
 
-  getTorrentFileName (videoFile: VideoFileModel) {
+  getTorrentFileName (videoFile: MVideoFile) {
     const extension = '.torrent'
     return this.uuid + '-' + videoFile.resolution + extension
   }
@@ -1815,15 +1846,15 @@ export class VideoModel extends Model<VideoModel> {
     return this.remote === false
   }
 
-  getTorrentFilePath (videoFile: VideoFileModel) {
+  getTorrentFilePath (videoFile: MVideoFile) {
     return join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
   }
 
-  getVideoFilePath (videoFile: VideoFileModel) {
+  getVideoFilePath (videoFile: MVideoFile) {
     return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
   }
 
-  async createTorrentAndSetInfoHash (videoFile: VideoFileModel) {
+  async createTorrentAndSetInfoHash (videoFile: MVideoFile) {
     const options = {
       // Keep the extname, it's used by the client to stream the file inside a web browser
       name: `${this.name} ${videoFile.resolution}p${videoFile.extname}`,
@@ -1869,11 +1900,11 @@ export class VideoModel extends Model<VideoModel> {
     return join(LAZY_STATIC_PATHS.PREVIEWS, preview.filename)
   }
 
-  toFormattedJSON (options?: VideoFormattingJSONOptions): Video {
+  toFormattedJSON (this: MVideoFormattable, options?: VideoFormattingJSONOptions): Video {
     return videoModelToFormattedJSON(this, options)
   }
 
-  toFormattedDetailsJSON (): VideoDetails {
+  toFormattedDetailsJSON (this: MVideoFormattableDetails): VideoDetails {
     return videoModelToFormattedDetailsJSON(this)
   }
 
@@ -1881,7 +1912,7 @@ export class VideoModel extends Model<VideoModel> {
     return videoFilesModelToFormattedJSON(this, this.VideoFiles)
   }
 
-  toActivityPubObject (): VideoTorrentObject {
+  toActivityPubObject (this: MVideoAP): VideoTorrentObject {
     return videoModelToActivityPubObject(this)
   }
 
@@ -1908,7 +1939,7 @@ export class VideoModel extends Model<VideoModel> {
     return this.VideoStreamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
   }
 
-  removeFile (videoFile: VideoFileModel, isRedundancy = false) {
+  removeFile (videoFile: MVideoFile, isRedundancy = false) {
     const baseDir = isRedundancy ? CONFIG.STORAGE.REDUNDANCY_DIR : CONFIG.STORAGE.VIDEOS_DIR
 
     const filePath = join(baseDir, this.getVideoFilename(videoFile))
@@ -1916,7 +1947,7 @@ export class VideoModel extends Model<VideoModel> {
       .catch(err => logger.warn('Cannot delete file %s.', filePath, { err }))
   }
 
-  removeTorrent (videoFile: VideoFileModel) {
+  removeTorrent (videoFile: MVideoFile) {
     const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
     return remove(torrentPath)
       .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err }))
@@ -1957,7 +1988,7 @@ export class VideoModel extends Model<VideoModel> {
     return { baseUrlHttp, baseUrlWs }
   }
 
-  generateMagnetUri (videoFile: VideoFileModel, baseUrlHttp: string, baseUrlWs: string) {
+  generateMagnetUri (videoFile: MVideoFileRedundanciesOpt, baseUrlHttp: string, baseUrlWs: string) {
     const xs = this.getTorrentUrl(videoFile, baseUrlHttp)
     const announce = this.getTrackerUrls(baseUrlHttp, baseUrlWs)
     let urlList = [ this.getVideoFileUrl(videoFile, baseUrlHttp) ]
@@ -1980,27 +2011,27 @@ export class VideoModel extends Model<VideoModel> {
     return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
   }
 
-  getTorrentUrl (videoFile: VideoFileModel, baseUrlHttp: string) {
+  getTorrentUrl (videoFile: MVideoFile, baseUrlHttp: string) {
     return baseUrlHttp + STATIC_PATHS.TORRENTS + this.getTorrentFileName(videoFile)
   }
 
-  getTorrentDownloadUrl (videoFile: VideoFileModel, baseUrlHttp: string) {
+  getTorrentDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
     return baseUrlHttp + STATIC_DOWNLOAD_PATHS.TORRENTS + this.getTorrentFileName(videoFile)
   }
 
-  getVideoFileUrl (videoFile: VideoFileModel, baseUrlHttp: string) {
+  getVideoFileUrl (videoFile: MVideoFile, baseUrlHttp: string) {
     return baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile)
   }
 
-  getVideoRedundancyUrl (videoFile: VideoFileModel, baseUrlHttp: string) {
+  getVideoRedundancyUrl (videoFile: MVideoFile, baseUrlHttp: string) {
     return baseUrlHttp + STATIC_PATHS.REDUNDANCY + this.getVideoFilename(videoFile)
   }
 
-  getVideoFileDownloadUrl (videoFile: VideoFileModel, baseUrlHttp: string) {
+  getVideoFileDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
     return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + this.getVideoFilename(videoFile)
   }
 
-  getBandwidthBits (videoFile: VideoFileModel) {
+  getBandwidthBits (videoFile: MVideoFile) {
     return Math.ceil((videoFile.size * 8) / this.duration)
   }
 }
index 365d0e1aefeba3422484148cc3d413b26cf325c2..0d1f154fe5bc503b231271bd5cea1bfa6888537d 100644 (file)
@@ -53,19 +53,6 @@ describe('Test activity pub helpers', function () {
       expect(result).to.be.false
     })
 
-    it('Should fail with an invalid PeerTube URL', async function () {
-      const keys = require('./json/peertube/keys.json')
-      const body = require('./json/peertube/announce-without-context.json')
-
-      const actorSignature = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey }
-      const signedBody = await buildSignedActivity(actorSignature as any, body)
-
-      const fromActor = { publicKey: keys.publicKey, url: 'http://localhost:9003/accounts/peertube' }
-      const result = await isJsonLDSignatureVerified(fromActor as any, signedBody)
-
-      expect(result).to.be.false
-    })
-
     it('Should succeed with a valid PeerTube signature', async function () {
       const keys = require('./json/peertube/keys.json')
       const body = require('./json/peertube/announce-without-context.json')
index 7773ae1e7db7a6cf5d8ae7b6d6a8c740f1bd3a21..9435bb1e88ed6f054269cceb3aaa6198af6915b9 100644 (file)
@@ -5,8 +5,16 @@ import 'mocha'
 import { CustomConfig } from '../../../../shared/models/server/custom-config.model'
 
 import {
-  createUser, flushTests, killallServers, makeDeleteRequest, makeGetRequest, makePutBodyRequest, flushAndRunServer, ServerInfo,
-  setAccessTokensToServers, userLogin, immutableAssign, cleanupTests
+  cleanupTests,
+  createUser,
+  flushAndRunServer,
+  immutableAssign,
+  makeDeleteRequest,
+  makeGetRequest,
+  makePutBodyRequest,
+  ServerInfo,
+  setAccessTokensToServers,
+  userLogin
 } from '../../../../shared/extra-utils'
 
 describe('Test config API validators', function () {
@@ -19,6 +27,18 @@ describe('Test config API validators', function () {
       shortDescription: 'my short description',
       description: 'my super description',
       terms: 'my super terms',
+      codeOfConduct: 'my super coc',
+
+      creationReason: 'my super reason',
+      moderationInformation: 'my super moderation information',
+      administrator: 'Kuja',
+      maintenanceLifetime: 'forever',
+      businessModel: 'my super business model',
+      hardwareInformation: '2vCore 3GB RAM',
+
+      languages: [ 'en', 'es' ],
+      categories: [ 1, 2 ],
+
       isNSFW: true,
       defaultClientRoute: '/videos/recently-added',
       defaultNSFWPolicy: 'blur',
@@ -98,6 +118,17 @@ describe('Test config API validators', function () {
         enabled: false,
         manualApproval: true
       }
+    },
+    followings: {
+      instance: {
+        autoFollowBack: {
+          enabled: true
+        },
+        autoFollowIndex: {
+          enabled: true,
+          indexUrl: 'https://index.example.com'
+        }
+      }
     }
   }
 
index 14ee20d451128076e84f8bc35cc6f588eb9e8d03..3b06be7ef496281b29f6dbfd910429d089cca7d3 100644 (file)
@@ -172,7 +172,8 @@ describe('Test user notifications API validators', function () {
       commentMention: UserNotificationSettingValue.WEB,
       newFollow: UserNotificationSettingValue.WEB,
       newUserRegistration: UserNotificationSettingValue.WEB,
-      newInstanceFollower: UserNotificationSettingValue.WEB
+      newInstanceFollower: UserNotificationSettingValue.WEB,
+      autoInstanceFollowing: UserNotificationSettingValue.WEB
     }
 
     it('Should fail with missing fields', async function () {
index 939b919edcd8929a5e8af06f54c611e88a4efd9c..55094795c459ac94887157fbd8f0487c13474d64 100644 (file)
@@ -476,6 +476,22 @@ describe('Test users API validators', function () {
       await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
     })
 
+    it('Should fail with an invalid noInstanceConfigWarningModal attribute', async function () {
+      const fields = {
+        noInstanceConfigWarningModal: -1
+      }
+
+      await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
+    })
+
+    it('Should fail with an invalid noWelcomeModal attribute', async function () {
+      const fields = {
+        noWelcomeModal: -1
+      }
+
+      await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
+    })
+
     it('Should succeed to change password with the correct params', async function () {
       const fields = {
         currentPassword: 'my super password',
@@ -483,7 +499,9 @@ describe('Test users API validators', function () {
         nsfwPolicy: 'blur',
         autoPlayVideo: false,
         email: 'super_email@example.com',
-        theme: 'default'
+        theme: 'default',
+        noInstanceConfigWarningModal: true,
+        noWelcomeModal: true
       }
 
       await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields, statusCodeExpected: 204 })
index 6fa6305623486d72b2090bea6d34584afaced017..15a34f5aab9c86dc2c6a0addd84519dea6274d03 100644 (file)
@@ -14,10 +14,13 @@ import {
   getVideoCommentThreads,
   getVideoThreadComments,
   immutableAssign,
+  MockInstancesIndex,
   registerUser,
   removeVideoFromBlacklist,
   reportVideoAbuse,
+  unfollow,
   updateCustomConfig,
+  updateCustomSubConfig,
   updateMyUser,
   updateVideo,
   updateVideoChannel,
@@ -29,6 +32,7 @@ import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/l
 import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
 import { getUserNotificationSocket } from '../../../../shared/extra-utils/socket/socket-io'
 import {
+  checkAutoInstanceFollowing,
   checkCommentMention,
   CheckerBaseParams,
   checkMyVideoImportIsFinished,
@@ -108,7 +112,8 @@ describe('Test users notifications', function () {
     commentMention: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
     newFollow: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
     newUserRegistration: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
-    newInstanceFollower: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
+    newInstanceFollower: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
+    autoInstanceFollowing: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
   }
 
   before(async function () {
@@ -873,7 +878,18 @@ describe('Test users notifications', function () {
     })
   })
 
-  describe('New instance follower', function () {
+  describe('New instance follows', function () {
+    const instanceIndexServer = new MockInstancesIndex()
+    const config = {
+      followings: {
+        instance: {
+          autoFollowIndex: {
+            indexUrl: 'http://localhost:42100',
+            enabled: true
+          }
+        }
+      }
+    }
     let baseParams: CheckerBaseParams
 
     before(async () => {
@@ -883,6 +899,9 @@ describe('Test users notifications', function () {
         socketNotifications: adminNotifications,
         token: servers[0].accessToken
       }
+
+      await instanceIndexServer.initialize()
+      instanceIndexServer.addInstance(servers[1].host)
     })
 
     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 () {
       const userOverride = { socketNotifications: userNotifications, token: userAccessToken, check: { web: true, mail: false } }
       await checkNewInstanceFollower(immutableAssign(baseParams, userOverride), 'localhost:' + servers[2].port, 'absence')
     })
+
+    it('Should send a notification on auto follow back', async function () {
+      this.timeout(40000)
+
+      await unfollow(servers[2].url, servers[2].accessToken, servers[0])
+      await waitJobs(servers)
+
+      const config = {
+        followings: {
+          instance: {
+            autoFollowBack: { enabled: true }
+          }
+        }
+      }
+      await updateCustomSubConfig(servers[0].url, servers[0].accessToken, config)
+
+      await follow(servers[2].url, [ servers[0].url ], servers[2].accessToken)
+
+      await waitJobs(servers)
+
+      const followerHost = servers[0].host
+      const followingHost = servers[2].host
+      await checkAutoInstanceFollowing(baseParams, followerHost, followingHost, 'presence')
+
+      const userOverride = { socketNotifications: userNotifications, token: userAccessToken, check: { web: true, mail: false } }
+      await checkAutoInstanceFollowing(immutableAssign(baseParams, userOverride), followerHost, followingHost, 'absence')
+
+      config.followings.instance.autoFollowBack.enabled = false
+      await updateCustomSubConfig(servers[0].url, servers[0].accessToken, config)
+      await unfollow(servers[0].url, servers[0].accessToken, servers[2])
+      await unfollow(servers[2].url, servers[2].accessToken, servers[0])
+    })
+
+    it('Should send a notification on auto instances index follow', async function () {
+      this.timeout(30000)
+      await unfollow(servers[0].url, servers[0].accessToken, servers[1])
+
+      await updateCustomSubConfig(servers[0].url, servers[0].accessToken, config)
+
+      await wait(5000)
+      await waitJobs(servers)
+
+      const followerHost = servers[0].host
+      const followingHost = servers[1].host
+      await checkAutoInstanceFollowing(baseParams, followerHost, followingHost, 'presence')
+
+      config.followings.instance.autoFollowIndex.enabled = false
+      await updateCustomSubConfig(servers[0].url, servers[0].accessToken, config)
+      await unfollow(servers[0].url, servers[0].accessToken, servers[1])
+    })
   })
 
   describe('New actor follow', function () {
index c06200ffe4fc83da99354cbeebf46d85d87fa2c1..a3e05156b189dc4d3efd825595f180da0e33d535 100644 (file)
@@ -206,7 +206,7 @@ describe('Test videos search', function () {
     const query = {
       search: '9999',
       categoryOneOf: [ 1 ],
-      tagsOneOf: [ 'aaaa', 'ffff' ]
+      tagsOneOf: [ 'aAaa', 'ffff' ]
     }
     const res1 = await advancedVideosSearch(server.url, query)
     expect(res1.body.total).to.equal(2)
@@ -219,15 +219,15 @@ describe('Test videos search', function () {
     const query = {
       search: '9999',
       categoryOneOf: [ 1 ],
-      tagsAllOf: [ 'cccc' ]
+      tagsAllOf: [ 'CCcc' ]
     }
     const res1 = await advancedVideosSearch(server.url, query)
     expect(res1.body.total).to.equal(2)
 
-    const res2 = await advancedVideosSearch(server.url, immutableAssign(query, { tagsAllOf: [ 'blabla' ] }))
+    const res2 = await advancedVideosSearch(server.url, immutableAssign(query, { tagsAllOf: [ 'blAbla' ] }))
     expect(res2.body.total).to.equal(0)
 
-    const res3 = await advancedVideosSearch(server.url, immutableAssign(query, { tagsAllOf: [ 'bbbb', 'cccc' ] }))
+    const res3 = await advancedVideosSearch(server.url, immutableAssign(query, { tagsAllOf: [ 'bbbb', 'CCCC' ] }))
     expect(res3.body.total).to.equal(1)
   })
 
diff --git a/server/tests/api/server/auto-follows.ts b/server/tests/api/server/auto-follows.ts
new file mode 100644 (file)
index 0000000..df46803
--- /dev/null
@@ -0,0 +1,211 @@
+/* tslint:disable:no-unused-expression */
+
+import * as chai from 'chai'
+import 'mocha'
+import {
+  acceptFollower,
+  cleanupTests,
+  flushAndRunMultipleServers,
+  MockInstancesIndex,
+  ServerInfo,
+  setAccessTokensToServers,
+  unfollow,
+  updateCustomSubConfig,
+  wait
+} from '../../../../shared/extra-utils/index'
+import { follow, getFollowersListPaginationAndSort, getFollowingListPaginationAndSort } from '../../../../shared/extra-utils/server/follows'
+import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
+import { ActorFollow } from '../../../../shared/models/actors'
+
+const expect = chai.expect
+
+async function checkFollow (follower: ServerInfo, following: ServerInfo, exists: boolean) {
+  {
+    const res = await getFollowersListPaginationAndSort(following.url, 0, 5, '-createdAt')
+    const follows = res.body.data as ActorFollow[]
+
+    const follow = follows.find(f => {
+      return f.follower.host === follower.host && f.state === 'accepted'
+    })
+
+    if (exists === true) {
+      expect(follow).to.exist
+    } else {
+      expect(follow).to.be.undefined
+    }
+  }
+
+  {
+    const res = await getFollowingListPaginationAndSort(follower.url, 0, 5, '-createdAt')
+    const follows = res.body.data as ActorFollow[]
+
+    const follow = follows.find(f => {
+      return f.following.host === following.host && f.state === 'accepted'
+    })
+
+    if (exists === true) {
+      expect(follow).to.exist
+    } else {
+      expect(follow).to.be.undefined
+    }
+  }
+}
+
+async function server1Follows2 (servers: ServerInfo[]) {
+  await follow(servers[0].url, [ servers[1].host ], servers[0].accessToken)
+
+  await waitJobs(servers)
+}
+
+async function resetFollows (servers: ServerInfo[]) {
+  try {
+    await unfollow(servers[ 0 ].url, servers[ 0 ].accessToken, servers[ 1 ])
+    await unfollow(servers[ 1 ].url, servers[ 1 ].accessToken, servers[ 0 ])
+  } catch { /* empty */ }
+
+  await waitJobs(servers)
+
+  await checkFollow(servers[0], servers[1], false)
+  await checkFollow(servers[1], servers[0], false)
+}
+
+describe('Test auto follows', function () {
+  let servers: ServerInfo[] = []
+
+  before(async function () {
+    this.timeout(30000)
+
+    servers = await flushAndRunMultipleServers(3)
+
+    // Get the access tokens
+    await setAccessTokensToServers(servers)
+  })
+
+  describe('Auto follow back', function () {
+
+    it('Should not auto follow back if the option is not enabled', async function () {
+      this.timeout(15000)
+
+      await server1Follows2(servers)
+
+      await checkFollow(servers[0], servers[1], true)
+      await checkFollow(servers[1], servers[0], false)
+
+      await resetFollows(servers)
+    })
+
+    it('Should auto follow back on auto accept if the option is enabled', async function () {
+      this.timeout(15000)
+
+      const config = {
+        followings: {
+          instance: {
+            autoFollowBack: { enabled: true }
+          }
+        }
+      }
+      await updateCustomSubConfig(servers[1].url, servers[1].accessToken, config)
+
+      await server1Follows2(servers)
+
+      await checkFollow(servers[0], servers[1], true)
+      await checkFollow(servers[1], servers[0], true)
+
+      await resetFollows(servers)
+    })
+
+    it('Should wait the acceptation before auto follow back', async function () {
+      this.timeout(30000)
+
+      const config = {
+        followings: {
+          instance: {
+            autoFollowBack: { enabled: true }
+          }
+        },
+        followers: {
+          instance: {
+            manualApproval: true
+          }
+        }
+      }
+      await updateCustomSubConfig(servers[1].url, servers[1].accessToken, config)
+
+      await server1Follows2(servers)
+
+      await checkFollow(servers[0], servers[1], false)
+      await checkFollow(servers[1], servers[0], false)
+
+      await acceptFollower(servers[1].url, servers[1].accessToken, 'peertube@' + servers[0].host)
+      await waitJobs(servers)
+
+      await checkFollow(servers[0], servers[1], true)
+      await checkFollow(servers[1], servers[0], true)
+
+      await resetFollows(servers)
+
+      config.followings.instance.autoFollowBack.enabled = false
+      config.followers.instance.manualApproval = false
+      await updateCustomSubConfig(servers[1].url, servers[1].accessToken, config)
+    })
+  })
+
+  describe('Auto follow index', function () {
+    const instanceIndexServer = new MockInstancesIndex()
+
+    before(async () => {
+      await instanceIndexServer.initialize()
+    })
+
+    it('Should not auto follow index if the option is not enabled', async function () {
+      this.timeout(30000)
+
+      await wait(5000)
+      await waitJobs(servers)
+
+      await checkFollow(servers[ 0 ], servers[ 1 ], false)
+      await checkFollow(servers[ 1 ], servers[ 0 ], false)
+    })
+
+    it('Should auto follow the index', async function () {
+      this.timeout(30000)
+
+      instanceIndexServer.addInstance(servers[1].host)
+
+      const config = {
+        followings: {
+          instance: {
+            autoFollowIndex: {
+              indexUrl: 'http://localhost:42100',
+              enabled: true
+            }
+          }
+        }
+      }
+      await updateCustomSubConfig(servers[0].url, servers[0].accessToken, config)
+
+      await wait(5000)
+      await waitJobs(servers)
+
+      await checkFollow(servers[ 0 ], servers[ 1 ], true)
+
+      await resetFollows(servers)
+    })
+
+    it('Should follow new added instances in the index but not old ones', async function () {
+      this.timeout(30000)
+
+      instanceIndexServer.addInstance(servers[2].host)
+
+      await wait(5000)
+      await waitJobs(servers)
+
+      await checkFollow(servers[ 0 ], servers[ 1 ], false)
+      await checkFollow(servers[ 0 ], servers[ 2 ], true)
+    })
+  })
+
+  after(async function () {
+    await cleanupTests(servers)
+  })
+})
index 78fdc9cc09479faa98919e8cb3ad7b13c3881d5b..97cc99eeaf892494bc22a8d8ab22ef7b4d9a16aa 100644 (file)
@@ -28,7 +28,19 @@ function checkInitialConfig (server: ServerInfo, data: CustomConfig) {
     'with WebTorrent and Angular.'
   )
   expect(data.instance.description).to.equal('Welcome to this PeerTube instance!')
+
   expect(data.instance.terms).to.equal('No terms for now.')
+  expect(data.instance.creationReason).to.be.empty
+  expect(data.instance.codeOfConduct).to.be.empty
+  expect(data.instance.moderationInformation).to.be.empty
+  expect(data.instance.administrator).to.be.empty
+  expect(data.instance.maintenanceLifetime).to.be.empty
+  expect(data.instance.businessModel).to.be.empty
+  expect(data.instance.hardwareInformation).to.be.empty
+
+  expect(data.instance.languages).to.have.lengthOf(0)
+  expect(data.instance.categories).to.have.lengthOf(0)
+
   expect(data.instance.defaultClientRoute).to.equal('/videos/trending')
   expect(data.instance.isNSFW).to.be.false
   expect(data.instance.defaultNSFWPolicy).to.equal('display')
@@ -68,13 +80,29 @@ function checkInitialConfig (server: ServerInfo, data: CustomConfig) {
 
   expect(data.followers.instance.enabled).to.be.true
   expect(data.followers.instance.manualApproval).to.be.false
+
+  expect(data.followings.instance.autoFollowBack.enabled).to.be.false
+  expect(data.followings.instance.autoFollowIndex.enabled).to.be.false
+  expect(data.followings.instance.autoFollowIndex.indexUrl).to.equal('https://instances.joinpeertube.org')
 }
 
 function checkUpdatedConfig (data: CustomConfig) {
   expect(data.instance.name).to.equal('PeerTube updated')
   expect(data.instance.shortDescription).to.equal('my short description')
   expect(data.instance.description).to.equal('my super description')
+
   expect(data.instance.terms).to.equal('my super terms')
+  expect(data.instance.creationReason).to.equal('my super creation reason')
+  expect(data.instance.codeOfConduct).to.equal('my super coc')
+  expect(data.instance.moderationInformation).to.equal('my super moderation information')
+  expect(data.instance.administrator).to.equal('Kuja')
+  expect(data.instance.maintenanceLifetime).to.equal('forever')
+  expect(data.instance.businessModel).to.equal('my super business model')
+  expect(data.instance.hardwareInformation).to.equal('2vCore 3GB RAM')
+
+  expect(data.instance.languages).to.deep.equal([ 'en', 'es' ])
+  expect(data.instance.categories).to.deep.equal([ 1, 2 ])
+
   expect(data.instance.defaultClientRoute).to.equal('/videos/recently-added')
   expect(data.instance.isNSFW).to.be.true
   expect(data.instance.defaultNSFWPolicy).to.equal('blur')
@@ -119,6 +147,10 @@ function checkUpdatedConfig (data: CustomConfig) {
 
   expect(data.followers.instance.enabled).to.be.false
   expect(data.followers.instance.manualApproval).to.be.true
+
+  expect(data.followings.instance.autoFollowBack.enabled).to.be.true
+  expect(data.followings.instance.autoFollowIndex.enabled).to.be.true
+  expect(data.followings.instance.autoFollowIndex.indexUrl).to.equal('https://updated.example.com')
 }
 
 describe('Test config', function () {
@@ -182,6 +214,18 @@ describe('Test config', function () {
         shortDescription: 'my short description',
         description: 'my super description',
         terms: 'my super terms',
+        codeOfConduct: 'my super coc',
+
+        creationReason: 'my super creation reason',
+        moderationInformation: 'my super moderation information',
+        administrator: 'Kuja',
+        maintenanceLifetime: 'forever',
+        businessModel: 'my super business model',
+        hardwareInformation: '2vCore 3GB RAM',
+
+        languages: [ 'en', 'es' ],
+        categories: [ 1, 2 ],
+
         defaultClientRoute: '/videos/recently-added',
         isNSFW: true,
         defaultNSFWPolicy: 'blur' as 'blur',
@@ -261,6 +305,17 @@ describe('Test config', function () {
           enabled: false,
           manualApproval: true
         }
+      },
+      followings: {
+        instance: {
+          autoFollowBack: {
+            enabled: true
+          },
+          autoFollowIndex: {
+            enabled: true,
+            indexUrl: 'https://updated.example.com'
+          }
+        }
       }
     }
     await updateCustomConfig(server.url, server.accessToken, newCustomConfig)
@@ -310,6 +365,17 @@ describe('Test config', function () {
     expect(data.instance.shortDescription).to.equal('my short description')
     expect(data.instance.description).to.equal('my super description')
     expect(data.instance.terms).to.equal('my super terms')
+    expect(data.instance.codeOfConduct).to.equal('my super coc')
+
+    expect(data.instance.creationReason).to.equal('my super creation reason')
+    expect(data.instance.moderationInformation).to.equal('my super moderation information')
+    expect(data.instance.administrator).to.equal('Kuja')
+    expect(data.instance.maintenanceLifetime).to.equal('forever')
+    expect(data.instance.businessModel).to.equal('my super business model')
+    expect(data.instance.hardwareInformation).to.equal('2vCore 3GB RAM')
+
+    expect(data.instance.languages).to.deep.equal([ 'en', 'es' ])
+    expect(data.instance.categories).to.deep.equal([ 1, 2 ])
   })
 
   it('Should remove the custom configuration', async function () {
index 3daeeb49aa971a581b23acba73645952f45fcb93..08205b2c831b79eeaedf96af7a63c7f60781ac71 100644 (file)
@@ -1,3 +1,4 @@
+import './auto-follows'
 import './config'
 import './contact-form'
 import './email'
index 3a3fabb4c5bdb458ca48911e0433a7e1f24a6087..95b1bb62603357324217039ab2306bddb00ed7e7 100644 (file)
@@ -442,7 +442,7 @@ describe('Test users', function () {
         url: server.url,
         accessToken: accessTokenUser,
         currentPassword: 'super password',
-        newPassword: 'new password'
+        password: 'new password'
       })
       user.password = 'new password'
 
@@ -543,7 +543,7 @@ describe('Test users', function () {
       })
 
       const res = await getMyUserInformation(server.url, accessTokenUser)
-      const user = res.body
+      const user: User = res.body
 
       expect(user.username).to.equal('user_1')
       expect(user.email).to.equal('updated@example.com')
@@ -552,6 +552,8 @@ describe('Test users', function () {
       expect(user.id).to.be.a('number')
       expect(user.account.displayName).to.equal('new display name')
       expect(user.account.description).to.equal('my super description updated')
+      expect(user.noWelcomeModal).to.be.false
+      expect(user.noInstanceConfigWarningModal).to.be.false
     })
 
     it('Should be able to update my theme', async function () {
@@ -568,6 +570,21 @@ describe('Test users', function () {
         expect(body.theme).to.equal(theme)
       }
     })
+
+    it('Should be able to update my modal preferences', async function () {
+      await updateMyUser({
+        url: server.url,
+        accessToken: accessTokenUser,
+        noInstanceConfigWarningModal: true,
+        noWelcomeModal: true
+      })
+
+      const res = await getMyUserInformation(server.url, accessTokenUser)
+      const user: User = res.body
+
+      expect(user.noWelcomeModal).to.be.true
+      expect(user.noInstanceConfigWarningModal).to.be.true
+    })
   })
 
   describe('Updating another user', function () {
index a2f3ee16177b414285db25b3c4172d0b4a7ebae2..0cd6f22c76252b7e69d2616700055d8ada20c088 100644 (file)
@@ -17,6 +17,12 @@ import {
 } from '../../../../shared/extra-utils/index'
 import { doubleFollow } from '../../../../shared/extra-utils/server/follows'
 import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
+import {
+  addAccountToServerBlocklist,
+  addServerToServerBlocklist,
+  removeAccountFromServerBlocklist,
+  removeServerFromServerBlocklist
+} from '../../../../shared/extra-utils/users/blocklist'
 
 const expect = chai.expect
 
@@ -163,13 +169,76 @@ describe('Test video abuses', function () {
     expect(res.body.data[0].moderationComment).to.equal('It is valid')
   })
 
+  it('Should hide video abuses from blocked accounts', async function () {
+    this.timeout(10000)
+
+    {
+      await reportVideoAbuse(servers[1].url, servers[1].accessToken, servers[0].video.uuid, 'will mute this')
+      await waitJobs(servers)
+
+      const res = await getVideoAbusesList(servers[0].url, servers[0].accessToken)
+      expect(res.body.total).to.equal(3)
+    }
+
+    const accountToBlock = 'root@localhost:' + servers[1].port
+
+    {
+      await addAccountToServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, accountToBlock)
+
+      const res = await getVideoAbusesList(servers[ 0 ].url, servers[ 0 ].accessToken)
+      expect(res.body.total).to.equal(2)
+
+      const abuse = res.body.data.find(a => a.reason === 'will mute this')
+      expect(abuse).to.be.undefined
+    }
+
+    {
+      await removeAccountFromServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, accountToBlock)
+
+      const res = await getVideoAbusesList(servers[ 0 ].url, servers[ 0 ].accessToken)
+      expect(res.body.total).to.equal(3)
+    }
+  })
+
+  it('Should hide video abuses from blocked servers', async function () {
+    const serverToBlock = servers[1].host
+
+    {
+      await addServerToServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, servers[1].host)
+
+      const res = await getVideoAbusesList(servers[ 0 ].url, servers[ 0 ].accessToken)
+      expect(res.body.total).to.equal(2)
+
+      const abuse = res.body.data.find(a => a.reason === 'will mute this')
+      expect(abuse).to.be.undefined
+    }
+
+    {
+      await removeServerFromServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, serverToBlock)
+
+      const res = await getVideoAbusesList(servers[ 0 ].url, servers[ 0 ].accessToken)
+      expect(res.body.total).to.equal(3)
+    }
+  })
+
   it('Should delete the video abuse', async function () {
+    this.timeout(10000)
+
     await deleteVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id)
 
-    const res = await getVideoAbusesList(servers[1].url, servers[1].accessToken)
-    expect(res.body.total).to.equal(0)
-    expect(res.body.data).to.be.an('array')
-    expect(res.body.data.length).to.equal(0)
+    await waitJobs(servers)
+
+    {
+      const res = await getVideoAbusesList(servers[1].url, servers[1].accessToken)
+      expect(res.body.total).to.equal(1)
+      expect(res.body.data.length).to.equal(1)
+      expect(res.body.data[0].id).to.not.equal(abuseServer2.id)
+    }
+
+    {
+      const res = await getVideoAbusesList(servers[0].url, servers[0].accessToken)
+      expect(res.body.total).to.equal(3)
+    }
   })
 
   after(async function () {
index 3a3add71b099cd744ca44e3d56fc309f7e71edbd..64ee2355af24ad2020623be4db5c626cc416caff 100644 (file)
@@ -191,7 +191,7 @@ describe('Test video change ownership - nominal', function () {
     await waitJobs(servers)
   })
 
-  it('Should have video channel updated', async function () {
+  it('Should have the channel of the video updated', async function () {
     for (const server of servers) {
       const res = await getVideo(server.url, servers[0].video.uuid)
 
index 8599a270fe03fc54b8716d252f0bcb15089aa539..58e2445ac47f0c40e9f1e4f2cafa1ff8c860ca26 100644 (file)
@@ -5,6 +5,7 @@ import { root } from '../../shared/extra-utils/miscs/miscs'
 import { getVideoChannel } from '../../shared/extra-utils/videos/video-channels'
 import { Command } from 'commander'
 import { VideoChannel, VideoPrivacy } from '../../shared/models/videos'
+import { createLogger, format, transports } from 'winston'
 
 let configName = 'PeerTube/CLI'
 if (isTestInstance()) configName += `-${getAppNumber()}`
@@ -119,6 +120,7 @@ function buildCommonVideoOptions (command: Command) {
     .option('-m, --comments-enabled', 'Enable comments')
     .option('-s, --support <support>', 'Video support text')
     .option('-w, --wait-transcoding', 'Wait transcoding before publishing the video')
+    .option('-v, --verbose <verbose>', 'Verbosity, from 0/\'error\' to 4/\'debug\'', 'info')
 }
 
 async function buildVideoAttributesFromCommander (url: string, command: Command, defaultAttributes: any = {}) {
@@ -175,11 +177,42 @@ function getServerCredentials (program: any) {
          })
 }
 
+function getLogger (logLevel = 'info') {
+  const logLevels = {
+    0: 0,
+    error: 0,
+    1: 1,
+    warn: 1,
+    2: 2,
+    info: 2,
+    3: 3,
+    verbose: 3,
+    4: 4,
+    debug: 4
+  }
+
+  const logger = createLogger({
+    levels: logLevels,
+    format: format.combine(
+      format.splat(),
+      format.simple()
+    ),
+    transports: [
+      new (transports.Console)({
+        level: logLevel
+      })
+    ]
+  })
+
+  return logger
+}
+
 // ---------------------------------------------------------------------------
 
 export {
   version,
   config,
+  getLogger,
   getSettings,
   getNetrc,
   getRemoteObjectOrDie,
index 0ebfa744263d19b807eb6427432d85c395a66309..fcb90cca3f30af56493c1cc211d47eca62d23b7a 100644 (file)
@@ -8,10 +8,11 @@ import { CONSTRAINTS_FIELDS } from '../initializers/constants'
 import { getClient, getVideoCategories, login, searchVideoWithSort, uploadVideo } from '../../shared/extra-utils/index'
 import { truncate } from 'lodash'
 import * as prompt from 'prompt'
+import { accessSync, constants } from 'fs'
 import { remove } from 'fs-extra'
 import { sha256 } from '../helpers/core-utils'
 import { buildOriginallyPublishedAt, safeGetYoutubeDL } from '../helpers/youtube-dl'
-import { buildCommonVideoOptions, buildVideoAttributesFromCommander, getServerCredentials } from './cli'
+import { buildCommonVideoOptions, buildVideoAttributesFromCommander, getServerCredentials, getLogger } from './cli'
 
 type UserInfo = {
   username: string
@@ -19,7 +20,6 @@ type UserInfo = {
 }
 
 const processOptions = {
-  cwd: __dirname,
   maxBuffer: Infinity
 }
 
@@ -35,15 +35,23 @@ command
   .option('--target-url <targetUrl>', 'Video target URL')
   .option('--since <since>', 'Publication date (inclusive) since which the videos can be imported (YYYY-MM-DD)', parseDate)
   .option('--until <until>', 'Publication date (inclusive) until which the videos can be imported (YYYY-MM-DD)', parseDate)
-  .option('-v, --verbose', 'Verbose mode')
+  .option('--first <first>', 'Process first n elements of returned playlist')
+  .option('--last <last>', 'Process last n elements of returned playlist')
+  .option('-T, --tmpdir <tmpdir>', 'Working directory', __dirname)
   .parse(process.argv)
 
+let log = getLogger(program[ 'verbose' ])
+
 getServerCredentials(command)
   .then(({ url, username, password }) => {
     if (!program[ 'targetUrl' ]) {
-      console.error('--targetUrl field is required.')
+      exitError('--target-url field is required.')
+    }
 
-      process.exit(-1)
+    try {
+      accessSync(program[ 'tmpdir' ], constants.R_OK | constants.W_OK)
+    } catch (e) {
+      exitError('--tmpdir %s: directory does not exist or is not accessible', program[ 'tmpdir' ])
     }
 
     removeEndSlashes(url)
@@ -53,8 +61,7 @@ getServerCredentials(command)
 
     run(url, user)
       .catch(err => {
-        console.error(err)
-        process.exit(-1)
+        exitError(err)
       })
   })
 
@@ -68,30 +75,32 @@ async function run (url: string, user: UserInfo) {
   const options = [ '-j', '--flat-playlist', '--playlist-reverse' ]
   youtubeDL.getInfo(program[ 'targetUrl' ], options, processOptions, async (err, info) => {
     if (err) {
-      console.log(err.message)
-      process.exit(1)
+      exitError(err.message)
     }
 
     let infoArray: any[]
 
     // Normalize utf8 fields
-    if (Array.isArray(info) === true) {
-      infoArray = info.map(i => normalizeObject(i))
-    } else {
-      infoArray = [ normalizeObject(info) ]
+    infoArray = [].concat(info);
+    if (program[ 'first' ]) {
+      infoArray = infoArray.slice(0, program[ 'first' ])
+    } else if (program[ 'last' ]) {
+      infoArray = infoArray.slice(- program[ 'last' ])
     }
-    console.log('Will download and upload %d videos.\n', infoArray.length)
+    infoArray = infoArray.map(i => normalizeObject(i))
+
+    log.info('Will download and upload %d videos.\n', infoArray.length)
 
     for (const info of infoArray) {
       await processVideo({
-        cwd: processOptions.cwd,
+        cwd: program[ 'tmpdir' ],
         url,
         user,
         youtubeInfo: info
       })
     }
 
-    console.log('Video/s for user %s imported: %s', program[ 'username' ], program[ 'targetUrl' ])
+    log.info('Video/s for user %s imported: %s', user.username, program[ 'targetUrl' ])
     process.exit(0)
   })
 }
@@ -105,21 +114,21 @@ function processVideo (parameters: {
   const { youtubeInfo, cwd, url, user } = parameters
 
   return new Promise(async res => {
-    if (program[ 'verbose' ]) console.log('Fetching object.', youtubeInfo)
+    log.debug('Fetching object.', youtubeInfo)
 
     const videoInfo = await fetchObject(youtubeInfo)
-    if (program[ 'verbose' ]) console.log('Fetched object.', videoInfo)
+    log.debug('Fetched object.', videoInfo)
 
     if (program[ 'since' ]) {
       if (buildOriginallyPublishedAt(videoInfo).getTime() < program[ 'since' ].getTime()) {
-        console.log('Video "%s" has been published before "%s", don\'t upload it.\n',
+        log.info('Video "%s" has been published before "%s", don\'t upload it.\n',
           videoInfo.title, formatDate(program[ 'since' ]));
         return res();
       }
     }
     if (program[ 'until' ]) {
       if (buildOriginallyPublishedAt(videoInfo).getTime() > program[ 'until' ].getTime()) {
-        console.log('Video "%s" has been published after "%s", don\'t upload it.\n',
+        log.info('Video "%s" has been published after "%s", don\'t upload it.\n',
           videoInfo.title, formatDate(program[ 'until' ]));
         return res();
       }
@@ -127,27 +136,27 @@ function processVideo (parameters: {
 
     const result = await searchVideoWithSort(url, videoInfo.title, '-match')
 
-    console.log('############################################################\n')
+    log.info('############################################################\n')
 
     if (result.body.data.find(v => v.name === videoInfo.title)) {
-      console.log('Video "%s" already exists, don\'t reupload it.\n', videoInfo.title)
+      log.info('Video "%s" already exists, don\'t reupload it.\n', videoInfo.title)
       return res()
     }
 
     const path = join(cwd, sha256(videoInfo.url) + '.mp4')
 
-    console.log('Downloading video "%s"...', videoInfo.title)
+    log.info('Downloading video "%s"...', videoInfo.title)
 
     const options = [ '-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best', '-o', path ]
     try {
       const youtubeDL = await safeGetYoutubeDL()
       youtubeDL.exec(videoInfo.url, options, processOptions, async (err, output) => {
         if (err) {
-          console.error(err)
+          log.error(err)
           return res()
         }
 
-        console.log(output.join('\n'))
+        log.info(output.join('\n'))
         await uploadVideoOnPeerTube({
           cwd,
           url,
@@ -158,7 +167,7 @@ function processVideo (parameters: {
         return res()
       })
     } catch (err) {
-      console.log(err.message)
+      log.error(err.message)
       return res()
     }
   })
@@ -217,7 +226,7 @@ async function uploadVideoOnPeerTube (parameters: {
     fixture: videoPath
   })
 
-  console.log('\nUploading on PeerTube video "%s".', videoAttributes.name)
+  log.info('\nUploading on PeerTube video "%s".', videoAttributes.name)
 
   let accessToken = await getAccessTokenOrDie(url, user)
 
@@ -225,21 +234,20 @@ async function uploadVideoOnPeerTube (parameters: {
     await uploadVideo(url, accessToken, videoAttributes)
   } catch (err) {
     if (err.message.indexOf('401') !== -1) {
-      console.log('Got 401 Unauthorized, token may have expired, renewing token and retry.')
+      log.info('Got 401 Unauthorized, token may have expired, renewing token and retry.')
 
       accessToken = await getAccessTokenOrDie(url, user)
 
       await uploadVideo(url, accessToken, videoAttributes)
     } else {
-      console.log(err.message)
-      process.exit(1)
+      exitError(err.message)
     }
   }
 
   await remove(videoPath)
   if (thumbnailfile) await remove(thumbnailfile)
 
-  console.log('Uploaded video "%s"!\n', videoAttributes.name)
+  log.warn('Uploaded video "%s"!\n', videoAttributes.name)
 }
 
 /* ---------------------------------------------------------- */
@@ -355,20 +363,17 @@ async function getAccessTokenOrDie (url: string, user: UserInfo) {
     const res = await login(url, client, user)
     return res.body.access_token
   } catch (err) {
-    console.error('Cannot authenticate. Please check your username/password.')
-    process.exit(-1)
+    exitError('Cannot authenticate. Please check your username/password.')
   }
 }
 
 function parseDate (dateAsStr: string): Date {
   if (!/\d{4}-\d{2}-\d{2}/.test(dateAsStr)) {
-    console.error(`Invalid date passed: ${dateAsStr}. Expected format: YYYY-MM-DD. See help for usage.`);
-    process.exit(-1);
+    exitError(`Invalid date passed: ${dateAsStr}. Expected format: YYYY-MM-DD. See help for usage.`);
   }
   const date = new Date(dateAsStr);
   if (isNaN(date.getTime())) {
-    console.error(`Invalid date passed: ${dateAsStr}. See help for usage.`);
-    process.exit(-1);
+    exitError(`Invalid date passed: ${dateAsStr}. See help for usage.`);
   }
   return date;
 }
@@ -376,3 +381,9 @@ function parseDate (dateAsStr: string): Date {
 function formatDate (date: Date): string {
   return date.toISOString().split('T')[0];
 }
+
+function exitError (message:string, ...meta: any[]) {
+  // use console.error instead of log.error here
+  console.error(message, ...meta)
+  process.exit(-1)
+}
index 37b2859dec301addd0f1cf1a22f5c6fa937625f4..7ed3a65b153cbf37cff0642a76d0e5c28d6faddb 100644 (file)
@@ -1,10 +1,9 @@
 import { Activity } from '../../shared/models/activitypub'
-import { ActorModel } from '../models/activitypub/actor'
-import { SignatureActorModel } from './models'
+import { MActorDefault, MActorSignature } from './models'
 
 export type APProcessorOptions<T extends Activity> = {
   activity: T
-  byActor: SignatureActorModel
-  inboxActor?: ActorModel
+  byActor: MActorSignature
+  inboxActor?: MActorDefault
   fromFetch?: boolean
 }
index f7da55ab0e8ed5e1c738f3717c5c7f69cf65f799..3cc7c7632809caa7295ac00f65d9d57379e70252 100644 (file)
-import { VideoChannelModel } from '../models/video/video-channel'
-import { VideoPlaylistModel } from '../models/video/video-playlist'
-import { VideoPlaylistElementModel } from '../models/video/video-playlist-element'
-import { UserModel } from '../models/account/user'
-import { VideoModel } from '../models/video/video'
-import { AccountModel } from '../models/account/account'
-import { VideoChangeOwnershipModel } from '../models/video/video-change-ownership'
-import { ActorModel } from '../models/activitypub/actor'
-import { VideoCommentModel } from '../models/video/video-comment'
-import { VideoShareModel } from '../models/video/video-share'
-import { AccountVideoRateModel } from '../models/account/account-video-rate'
-import { ActorFollowModel } from '../models/activitypub/actor-follow'
-import { ServerModel } from '../models/server/server'
-import { VideoFileModel } from '../models/video/video-file'
-import { VideoRedundancyModel } from '../models/redundancy/video-redundancy'
-import { ServerBlocklistModel } from '../models/server/server-blocklist'
-import { AccountBlocklistModel } from '../models/account/account-blocklist'
-import { VideoImportModel } from '../models/video/video-import'
-import { VideoAbuseModel } from '../models/video/video-abuse'
-import { VideoBlacklistModel } from '../models/video/video-blacklist'
-import { VideoCaptionModel } from '../models/video/video-caption'
-import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
 import { RegisteredPlugin } from '../lib/plugins/plugin-manager'
-import { PluginModel } from '../models/server/plugin'
-import { SignatureActorModel } from './models'
+import {
+  MAccountDefault,
+  MActorAccountChannelId,
+  MActorFollowActorsDefault,
+  MActorFollowActorsDefaultSubscription,
+  MActorFull,
+  MChannelAccountDefault,
+  MComment,
+  MCommentOwnerVideoReply,
+  MUserDefault,
+  MVideoAbuse,
+  MVideoBlacklist,
+  MVideoCaptionVideo,
+  MVideoFullLight,
+  MVideoIdThumbnail,
+  MVideoRedundancyVideo,
+  MVideoShareActor,
+  MVideoThumbnail,
+  MVideoWithRights
+} from './models'
+import { MVideoPlaylistFull, MVideoPlaylistFullSummary } from './models/video/video-playlist'
+import { MVideoImportDefault } from '@server/typings/models/video/video-import'
+import { MAccountBlocklist, MStreamingPlaylist, MVideoFile } from '@server/typings/models'
+import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/typings/models/video/video-playlist-element'
+import { MAccountVideoRateAccountVideo } from '@server/typings/models/video/video-rate'
+import { MVideoChangeOwnershipFull } from './models/video/video-change-ownership'
+import { MPlugin, MServer } from '@server/typings/models/server'
+import { MServerBlocklist } from './models/server/server-blocklist'
+import { MOAuthTokenUser } from '@server/typings/models/oauth/oauth-token'
 
 declare module 'express' {
 
   interface Response {
+
     locals: {
-      video?: VideoModel
-      videoShare?: VideoShareModel
-      videoFile?: VideoFileModel
+      videoAll?: MVideoFullLight
+      onlyVideo?: MVideoThumbnail
+      onlyVideoWithRights?: MVideoWithRights
+      videoId?: MVideoIdThumbnail
+
+      videoShare?: MVideoShareActor
+
+      videoFile?: MVideoFile
+
+      videoImport?: MVideoImportDefault
+
+      videoBlacklist?: MVideoBlacklist
+
+      videoCaption?: MVideoCaptionVideo
+
+      videoAbuse?: MVideoAbuse
 
-      videoImport?: VideoImportModel
+      videoStreamingPlaylist?: MStreamingPlaylist
 
-      videoBlacklist?: VideoBlacklistModel
+      videoChannel?: MChannelAccountDefault
 
-      videoCaption?: VideoCaptionModel
+      videoPlaylistFull?: MVideoPlaylistFull
+      videoPlaylistSummary?: MVideoPlaylistFullSummary
 
-      videoAbuse?: VideoAbuseModel
+      videoPlaylistElement?: MVideoPlaylistElement
+      videoPlaylistElementAP?: MVideoPlaylistElementVideoUrlPlaylistPrivacy
 
-      videoStreamingPlaylist?: VideoStreamingPlaylistModel
+      accountVideoRate?: MAccountVideoRateAccountVideo
 
-      videoChannel?: VideoChannelModel
+      videoCommentFull?: MCommentOwnerVideoReply
+      videoCommentThread?: MComment
 
-      videoPlaylist?: VideoPlaylistModel
-      videoPlaylistElement?: VideoPlaylistElementModel
+      follow?: MActorFollowActorsDefault
+      subscription?: MActorFollowActorsDefaultSubscription
 
-      accountVideoRate?: AccountVideoRateModel
+      nextOwner?: MAccountDefault
+      videoChangeOwnership?: MVideoChangeOwnershipFull
 
-      videoComment?: VideoCommentModel
-      videoCommentThread?: VideoCommentModel
+      account?: MAccountDefault
 
-      follow?: ActorFollowModel
-      subscription?: ActorFollowModel
+      actorFull?: MActorFull
 
-      nextOwner?: AccountModel
-      videoChangeOwnership?: VideoChangeOwnershipModel
-      account?: AccountModel
-      actor?: ActorModel
-      user?: UserModel
+      user?: MUserDefault
 
-      server?: ServerModel
+      server?: MServer
 
-      videoRedundancy?: VideoRedundancyModel
+      videoRedundancy?: MVideoRedundancyVideo
 
-      accountBlock?: AccountBlocklistModel
-      serverBlock?: ServerBlocklistModel
+      accountBlock?: MAccountBlocklist
+      serverBlock?: MServerBlocklist
 
       oauth?: {
-        token: {
-          User: UserModel
-          user: UserModel
-        }
+        token: MOAuthTokenUser
       }
 
       signature?: {
-        actor: SignatureActorModel
+        actor: MActorAccountChannelId
       }
 
       authenticated?: boolean
 
       registeredPlugin?: RegisteredPlugin
 
-      plugin?: PluginModel
+      plugin?: MPlugin
     }
   }
 }
diff --git a/server/typings/models/account/account-blocklist.ts b/server/typings/models/account/account-blocklist.ts
new file mode 100644 (file)
index 0000000..c9cb553
--- /dev/null
@@ -0,0 +1,25 @@
+import { AccountBlocklistModel } from '../../../models/account/account-blocklist'
+import { PickWith } from '../../utils'
+import { MAccountDefault, MAccountFormattable } from './account'
+
+type Use<K extends keyof AccountBlocklistModel, M> = PickWith<AccountBlocklistModel, K, M>
+
+// ############################################################################
+
+export type MAccountBlocklist = Omit<AccountBlocklistModel, 'ByAccount' | 'BlockedAccount'>
+
+// ############################################################################
+
+export type MAccountBlocklistId = Pick<AccountBlocklistModel, 'id'>
+
+export type MAccountBlocklistAccounts = MAccountBlocklist &
+  Use<'ByAccount', MAccountDefault> &
+  Use<'BlockedAccount', MAccountDefault>
+
+// ############################################################################
+
+// Format for API or AP object
+
+export type MAccountBlocklistFormattable = Pick<MAccountBlocklist, 'createdAt'> &
+  Use<'ByAccount', MAccountFormattable> &
+  Use<'BlockedAccount', MAccountFormattable>
diff --git a/server/typings/models/account/account.ts b/server/typings/models/account/account.ts
new file mode 100644 (file)
index 0000000..ec78fec
--- /dev/null
@@ -0,0 +1,95 @@
+import { AccountModel } from '../../../models/account/account'
+import {
+  MActor,
+  MActorAP,
+  MActorAPI,
+  MActorAudience,
+  MActorDefault,
+  MActorDefaultLight,
+  MActorFormattable,
+  MActorId,
+  MActorServer,
+  MActorSummary,
+  MActorSummaryFormattable,
+  MActorUrl
+} from './actor'
+import { FunctionProperties, PickWith } from '../../utils'
+import { MAccountBlocklistId } from './account-blocklist'
+import { MChannelDefault } from '@server/typings/models'
+
+type Use<K extends keyof AccountModel, M> = PickWith<AccountModel, K, M>
+
+// ############################################################################
+
+export type MAccount = Omit<AccountModel, 'Actor' | 'User' | 'Application' | 'VideoChannels' | 'VideoPlaylists' |
+  'VideoComments' | 'BlockedAccounts'>
+
+// ############################################################################
+
+// Only some attributes
+export type MAccountId = Pick<MAccount, 'id'>
+export type MAccountUserId = Pick<MAccount, 'userId'>
+
+// Only some Actor attributes
+export type MAccountUrl = Use<'Actor', MActorUrl>
+export type MAccountAudience = Use<'Actor', MActorAudience>
+
+export type MAccountIdActor = MAccountId &
+  Use<'Actor', MActor>
+
+export type MAccountIdActorId = MAccountId &
+  Use<'Actor', MActorId>
+
+// ############################################################################
+
+// Default scope
+export type MAccountDefault = MAccount &
+  Use<'Actor', MActorDefault>
+
+// Default with default association scopes
+export type MAccountDefaultChannelDefault = MAccount &
+  Use<'Actor', MActorDefault> &
+  Use<'VideoChannels', MChannelDefault[]>
+
+// We don't need some actors attributes
+export type MAccountLight = MAccount &
+  Use<'Actor', MActorDefaultLight>
+
+// ############################################################################
+
+// Full actor
+export type MAccountActor = MAccount &
+  Use<'Actor', MActor>
+
+// Full actor with server
+export type MAccountServer = MAccount &
+  Use<'Actor', MActorServer>
+
+// ############################################################################
+
+// For API
+
+export type MAccountSummary = FunctionProperties<MAccount> &
+  Pick<MAccount, 'id' | 'name'> &
+  Use<'Actor', MActorSummary>
+
+export type MAccountSummaryBlocks = MAccountSummary &
+  Use<'BlockedAccounts', MAccountBlocklistId[]>
+
+export type MAccountAPI = MAccount &
+  Use<'Actor', MActorAPI>
+
+// ############################################################################
+
+// Format for API or AP object
+
+export type MAccountSummaryFormattable = FunctionProperties<MAccount> &
+  Pick<MAccount, 'id' | 'name'> &
+  Use<'Actor', MActorSummaryFormattable>
+
+export type MAccountFormattable = FunctionProperties<MAccount> &
+  Pick<MAccount, 'id' | 'name' | 'description' | 'createdAt' | 'updatedAt' | 'userId'> &
+  Use<'Actor', MActorFormattable>
+
+export type MAccountAP = Pick<MAccount, 'name' | 'description'> &
+  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 (file)
index 0000000..1c66eb0
--- /dev/null
@@ -0,0 +1,63 @@
+import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
+import {
+  MActor,
+  MActorAccount,
+  MActorDefaultAccountChannel,
+  MActorChannelAccountActor,
+  MActorDefault,
+  MActorFormattable,
+  MActorHost,
+  MActorUsername
+} from './actor'
+import { PickWith } from '../../utils'
+import { ActorModel } from '@server/models/activitypub/actor'
+import { MChannelDefault } from '@server/typings/models'
+
+type Use<K extends keyof ActorFollowModel, M> = PickWith<ActorFollowModel, K, M>
+
+// ############################################################################
+
+export type MActorFollow = Omit<ActorFollowModel, 'ActorFollower' | 'ActorFollowing'>
+
+// ############################################################################
+
+export type MActorFollowFollowingHost = MActorFollow &
+  Use<'ActorFollowing', MActorUsername & MActorHost>
+
+// ############################################################################
+
+// With actors or actors default
+
+export type MActorFollowActors = MActorFollow &
+  Use<'ActorFollower', MActor> &
+  Use<'ActorFollowing', MActor>
+
+export type MActorFollowActorsDefault = MActorFollow &
+  Use<'ActorFollower', MActorDefault> &
+  Use<'ActorFollowing', MActorDefault>
+
+export type MActorFollowFull = MActorFollow &
+  Use<'ActorFollower', MActorDefaultAccountChannel> &
+  Use<'ActorFollowing', MActorDefaultAccountChannel>
+
+// ############################################################################
+
+// For subscriptions
+
+type SubscriptionFollowing = MActorDefault &
+  PickWith<ActorModel, 'VideoChannel', MChannelDefault>
+
+export type MActorFollowActorsDefaultSubscription = MActorFollow &
+  Use<'ActorFollower', MActorDefault> &
+  Use<'ActorFollowing', SubscriptionFollowing>
+
+export type MActorFollowSubscriptions = MActorFollow &
+  Use<'ActorFollowing', MActorChannelAccountActor>
+
+// ############################################################################
+
+// Format for API or AP object
+
+export type MActorFollowFormattable = Pick<MActorFollow, 'id' | 'score' | 'state' | 'createdAt' | 'updatedAt'> &
+  Use<'ActorFollower', MActorFormattable> &
+  Use<'ActorFollowing', MActorFormattable>
diff --git a/server/typings/models/account/actor.ts b/server/typings/models/account/actor.ts
new file mode 100644 (file)
index 0000000..bcacb83
--- /dev/null
@@ -0,0 +1,121 @@
+import { ActorModel } from '../../../models/activitypub/actor'
+import { FunctionProperties, PickWith, PickWithOpt } from '../../utils'
+import { MAccount, MAccountDefault, MAccountId, MAccountIdActor } from './account'
+import { MServer, MServerHost, MServerHostBlocks, MServerRedundancyAllowed } from '../server'
+import { MAvatar, MAvatarFormattable } from './avatar'
+import { MChannel, MChannelAccountActor, MChannelAccountDefault, MChannelId, MChannelIdActor } from '../video'
+
+type Use<K extends keyof ActorModel, M> = PickWith<ActorModel, K, M>
+
+// ############################################################################
+
+export type MActor = Omit<ActorModel, 'Account' | 'VideoChannel' | 'ActorFollowing' | 'Avatar' | 'ActorFollowers' | 'Server'>
+
+// ############################################################################
+
+export type MActorUrl = Pick<MActor, 'url'>
+export type MActorId = Pick<MActor, 'id'>
+export type MActorUsername = Pick<MActor, 'preferredUsername'>
+
+export type MActorFollowersUrl = Pick<MActor, 'followersUrl'>
+export type MActorAudience = MActorUrl & MActorFollowersUrl
+export type MActorFollowerException = Pick<ActorModel, 'sharedInboxUrl' | 'inboxUrl'>
+export type MActorSignature = MActorAccountChannelId
+
+export type MActorLight = Omit<MActor, 'privateKey' | 'privateKey'>
+
+// ############################################################################
+
+// Some association attributes
+
+export type MActorHost = Use<'Server', MServerHost>
+export type MActorRedundancyAllowedOpt = PickWithOpt<ActorModel, 'Server', MServerRedundancyAllowed>
+
+export type MActorDefaultLight = MActorLight &
+  Use<'Server', MServerHost> &
+  Use<'Avatar', MAvatar>
+
+export type MActorAccountId = MActor &
+  Use<'Account', MAccountId>
+export type MActorAccountIdActor = MActor &
+  Use<'Account', MAccountIdActor>
+
+export type MActorChannelId = MActor &
+  Use<'VideoChannel', MChannelId>
+export type MActorChannelIdActor = MActor &
+  Use<'VideoChannel', MChannelIdActor>
+
+export type MActorAccountChannelId = MActorAccountId & MActorChannelId
+export type MActorAccountChannelIdActor = MActorAccountIdActor & MActorChannelIdActor
+
+// ############################################################################
+
+// Include raw account/channel/server
+
+export type MActorAccount = MActor &
+  Use<'Account', MAccount>
+
+export type MActorChannel = MActor &
+  Use<'VideoChannel', MChannel>
+
+export type MActorDefaultAccountChannel = MActorDefault & MActorAccount & MActorChannel
+
+export type MActorServer = MActor &
+  Use<'Server', MServer>
+
+// ############################################################################
+
+// Complex actor associations
+
+export type MActorDefault = MActor &
+  Use<'Server', MServer> &
+  Use<'Avatar', MAvatar>
+
+// Actor with channel that is associated to an account and its actor
+// Actor -> VideoChannel -> Account -> Actor
+export type MActorChannelAccountActor = MActor &
+  Use<'VideoChannel', MChannelAccountActor>
+
+export type MActorFull = MActor &
+  Use<'Server', MServer> &
+  Use<'Avatar', MAvatar> &
+  Use<'Account', MAccount> &
+  Use<'VideoChannel', MChannelAccountActor>
+
+// Same than ActorFull, but the account and the channel have their actor
+export type MActorFullActor = MActor &
+  Use<'Server', MServer> &
+  Use<'Avatar', MAvatar> &
+  Use<'Account', MAccountDefault> &
+  Use<'VideoChannel', MChannelAccountDefault>
+
+// ############################################################################
+
+// API
+
+export type MActorSummary = FunctionProperties<MActor> &
+  Pick<MActor, 'id' | 'preferredUsername' | 'url' | 'serverId' | 'avatarId'> &
+  Use<'Server', MServerHost> &
+  Use<'Avatar', MAvatar>
+
+export type MActorSummaryBlocks = MActorSummary &
+  Use<'Server', MServerHostBlocks>
+
+export type MActorAPI = Omit<MActorDefault, 'publicKey' | 'privateKey' | 'inboxUrl' | 'outboxUrl' | 'sharedInboxUrl' |
+  'followersUrl' | 'followingUrl' | 'url' | 'createdAt' | 'updatedAt'>
+
+// ############################################################################
+
+// Format for API or AP object
+
+export type MActorSummaryFormattable = FunctionProperties<MActor> &
+  Pick<MActor, 'url' | 'preferredUsername'> &
+  Use<'Server', MServerHost> &
+  Use<'Avatar', MAvatarFormattable>
+
+export type MActorFormattable = MActorSummaryFormattable &
+  Pick<MActor, 'id' | 'followingCount' | 'followersCount' | 'createdAt' | 'updatedAt'> &
+  Use<'Server', MServerHost & Partial<Pick<MServer, 'redundancyAllowed'>>>
+
+export type MActorAP = MActor &
+  Use<'Avatar', MAvatar>
diff --git a/server/typings/models/account/avatar.ts b/server/typings/models/account/avatar.ts
new file mode 100644 (file)
index 0000000..8af6cc7
--- /dev/null
@@ -0,0 +1,11 @@
+import { AvatarModel } from '../../../models/avatar/avatar'
+import { FunctionProperties } from '@server/typings/utils'
+
+export type MAvatar = AvatarModel
+
+// ############################################################################
+
+// Format for API or AP object
+
+export type MAvatarFormattable = FunctionProperties<MAvatar> &
+  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 (file)
index 0000000..513c09c
--- /dev/null
@@ -0,0 +1,5 @@
+export * from './account'
+export * from './account-blocklist'
+export * from './actor'
+export * from './actor-follow'
+export * from './avatar'
diff --git a/server/typings/models/actor-follow.ts b/server/typings/models/actor-follow.ts
deleted file mode 100644 (file)
index 952ef87..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-import { ActorFollowModel } from '../../models/activitypub/actor-follow'
-import { ActorModelOnly } from './actor'
-
-export type ActorFollowModelOnly = Omit<ActorFollowModel, 'ActorFollower' | 'ActorFollowing'>
-export type ActorFollowModelLight = ActorFollowModelOnly & {
-  ActorFollower: ActorModelOnly
-  ActorFollowing: ActorModelOnly
-}
diff --git a/server/typings/models/actor.ts b/server/typings/models/actor.ts
deleted file mode 100644 (file)
index 2656c7b..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-import { ActorModel } from '../../models/activitypub/actor'
-import { VideoChannelModel } from '../../models/video/video-channel'
-import { AccountModel } from '../../models/account/account'
-import { FunctionProperties } from '../utils'
-
-export type VideoChannelModelId = FunctionProperties<VideoChannelModel>
-export type AccountModelId = FunctionProperties<AccountModel> | Pick<AccountModel, 'id'>
-
-export type VideoChannelModelIdActor = VideoChannelModelId & Pick<VideoChannelModel, 'Actor'>
-export type AccountModelIdActor = AccountModelId & Pick<AccountModel, 'Actor'>
-
-export type ActorModelUrl = Pick<ActorModel, 'url'>
-export type ActorModelOnly = Omit<ActorModel, 'Account' | 'VideoChannel' | 'ActorFollowing' | 'Avatar' | 'ActorFollowers' | 'Server'>
-export type ActorModelId = Pick<ActorModelOnly, 'id'>
-
-export type SignatureActorModel = ActorModelOnly & {
-  VideoChannel: VideoChannelModelIdActor
-
-  Account: AccountModelIdActor
-}
-
-export type ActorFollowerException = Pick<ActorModel, 'sharedInboxUrl' | 'inboxUrl'>
index c906569654a391a591a61796957a84acea804ee1..78b4948ce85f263f84f1fed28f4002fcfd5f7c88 100644 (file)
@@ -1 +1,5 @@
-export * from './actor'
+export * from './account'
+export * from './oauth'
+export * from './server'
+export * from './user'
+export * from './video'
diff --git a/server/typings/models/oauth/index.d.ts b/server/typings/models/oauth/index.d.ts
new file mode 100644 (file)
index 0000000..36b7ea8
--- /dev/null
@@ -0,0 +1,2 @@
+export * from './oauth-client'
+export * 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 (file)
index 0000000..904a078
--- /dev/null
@@ -0,0 +1,3 @@
+import { OAuthClientModel } from '@server/models/oauth/oauth-client'
+
+export 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 (file)
index 0000000..af34129
--- /dev/null
@@ -0,0 +1,13 @@
+import { OAuthTokenModel } from '@server/models/oauth/oauth-token'
+import { PickWith } from '@server/typings/utils'
+import { MUserAccountUrl } from '@server/typings/models'
+
+type Use<K extends keyof OAuthTokenModel, M> = PickWith<OAuthTokenModel, K, M>
+
+// ############################################################################
+
+export type MOAuthToken = Omit<OAuthTokenModel, 'User' | 'OAuthClients'>
+
+export type MOAuthTokenUser = MOAuthToken &
+  Use<'User', MUserAccountUrl> &
+  { user?: MUserAccountUrl }
diff --git a/server/typings/models/server/index.d.ts b/server/typings/models/server/index.d.ts
new file mode 100644 (file)
index 0000000..c853795
--- /dev/null
@@ -0,0 +1,3 @@
+export * from './plugin'
+export * from './server'
+export * from './server-blocklist'
diff --git a/server/typings/models/server/plugin.ts b/server/typings/models/server/plugin.ts
new file mode 100644 (file)
index 0000000..94674c3
--- /dev/null
@@ -0,0 +1,10 @@
+import { PluginModel } from '@server/models/server/plugin'
+
+export type MPlugin = PluginModel
+
+// ############################################################################
+
+// Format for API or AP object
+
+export type MPluginFormattable = Pick<MPlugin, 'name' | 'type' | 'version' | 'latestVersion' | 'enabled' | 'uninstalled'
+  | '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 (file)
index 0000000..c81f604
--- /dev/null
@@ -0,0 +1,23 @@
+import { ServerBlocklistModel } from '@server/models/server/server-blocklist'
+import { PickWith } from '@server/typings/utils'
+import { MAccountDefault, MAccountFormattable, MServer, MServerFormattable } from '@server/typings/models'
+
+type Use<K extends keyof ServerBlocklistModel, M> = PickWith<ServerBlocklistModel, K, M>
+
+// ############################################################################
+
+export type MServerBlocklist = Omit<ServerBlocklistModel, 'ByAccount' | 'BlockedServer'>
+
+// ############################################################################
+
+export type MServerBlocklistAccountServer = MServerBlocklist &
+  Use<'ByAccount', MAccountDefault> &
+  Use<'BlockedServer', MServer>
+
+// ############################################################################
+
+// Format for API or AP object
+
+export type MServerBlocklistFormattable = Pick<MServerBlocklist, 'createdAt'> &
+  Use<'ByAccount', MAccountFormattable> &
+  Use<'BlockedServer', MServerFormattable>
diff --git a/server/typings/models/server/server.ts b/server/typings/models/server/server.ts
new file mode 100644 (file)
index 0000000..190cc0c
--- /dev/null
@@ -0,0 +1,24 @@
+import { ServerModel } from '../../../models/server/server'
+import { FunctionProperties, PickWith } from '../../utils'
+import { MAccountBlocklistId } from '../account'
+
+type Use<K extends keyof ServerModel, M> = PickWith<ServerModel, K, M>
+
+// ############################################################################
+
+export type MServer = Omit<ServerModel, 'Actors' | 'BlockedByAccounts'>
+
+// ############################################################################
+
+export type MServerHost = Pick<MServer, 'host'>
+export type MServerRedundancyAllowed = Pick<MServer, 'redundancyAllowed'>
+
+export type MServerHostBlocks = MServerHost &
+  Use<'BlockedByAccounts', MAccountBlocklistId[]>
+
+// ############################################################################
+
+// Format for API or AP object
+
+export type MServerFormattable = FunctionProperties<MServer> &
+  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 (file)
index 0000000..6657b21
--- /dev/null
@@ -0,0 +1,4 @@
+export * from './user'
+export * from './user-notification'
+export * from './user-notification-setting'
+export * 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 (file)
index 0000000..c674add
--- /dev/null
@@ -0,0 +1,9 @@
+import { UserNotificationSettingModel } from '@server/models/account/user-notification-setting'
+
+export type MNotificationSetting = Omit<UserNotificationSettingModel, 'User'>
+
+// ############################################################################
+
+// Format for API or AP object
+
+export 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 (file)
index 0000000..1cdc691
--- /dev/null
@@ -0,0 +1,78 @@
+import { UserNotificationModel } from '../../../models/account/user-notification'
+import { PickWith, PickWithOpt } from '../../utils'
+import { VideoModel } from '../../../models/video/video'
+import { ActorModel } from '../../../models/activitypub/actor'
+import { ServerModel } from '../../../models/server/server'
+import { AvatarModel } from '../../../models/avatar/avatar'
+import { VideoChannelModel } from '../../../models/video/video-channel'
+import { AccountModel } from '../../../models/account/account'
+import { VideoCommentModel } from '../../../models/video/video-comment'
+import { VideoAbuseModel } from '../../../models/video/video-abuse'
+import { VideoBlacklistModel } from '../../../models/video/video-blacklist'
+import { VideoImportModel } from '../../../models/video/video-import'
+import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
+
+type Use<K extends keyof UserNotificationModel, M> = PickWith<UserNotificationModel, K, M>
+
+// ############################################################################
+
+export namespace UserNotificationIncludes {
+  export type VideoInclude = Pick<VideoModel, 'id' | 'uuid' | 'name'>
+  export type VideoIncludeChannel = VideoInclude &
+    PickWith<VideoModel, 'VideoChannel', VideoChannelIncludeActor>
+
+  export type ActorInclude = Pick<ActorModel, 'preferredUsername' | 'getHost'> &
+    PickWith<ActorModel, 'Avatar', Pick<AvatarModel, 'filename' | 'getStaticPath'>> &
+    PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>>
+
+  export type VideoChannelInclude = Pick<VideoChannelModel, 'id' | 'name' | 'getDisplayName'>
+  export type VideoChannelIncludeActor = VideoChannelInclude &
+    PickWith<VideoChannelModel, 'Actor', ActorInclude>
+
+  export type AccountInclude = Pick<AccountModel, 'id' | 'name' | 'getDisplayName'>
+  export type AccountIncludeActor = AccountInclude &
+    PickWith<AccountModel, 'Actor', ActorInclude>
+
+  export type VideoCommentInclude = Pick<VideoCommentModel, 'id' | 'originCommentId' | 'getThreadId'> &
+    PickWith<VideoCommentModel, 'Account', AccountIncludeActor> &
+    PickWith<VideoCommentModel, 'Video', VideoInclude>
+
+  export type VideoAbuseInclude = Pick<VideoAbuseModel, 'id'> &
+    PickWith<VideoAbuseModel, 'Video', VideoInclude>
+
+  export type VideoBlacklistInclude = Pick<VideoBlacklistModel, 'id'> &
+    PickWith<VideoAbuseModel, 'Video', VideoInclude>
+
+  export type VideoImportInclude = Pick<VideoImportModel, 'id' | 'magnetUri' | 'targetUrl' | 'torrentName'> &
+    PickWith<VideoImportModel, 'Video', VideoInclude>
+
+  export type ActorFollower = Pick<ActorModel, 'preferredUsername' | 'getHost'> &
+    PickWith<ActorModel, 'Account', AccountInclude> &
+    PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>> &
+    PickWithOpt<ActorModel, 'Avatar', Pick<AvatarModel, 'filename' | 'getStaticPath'>>
+
+  export type ActorFollowing = Pick<ActorModel, 'preferredUsername' | 'type' | 'getHost'> &
+    PickWith<ActorModel, 'VideoChannel', VideoChannelInclude> &
+    PickWith<ActorModel, 'Account', AccountInclude> &
+    PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>>
+
+  export type ActorFollowInclude = Pick<ActorFollowModel, 'id' | 'state'> &
+    PickWith<ActorFollowModel, 'ActorFollower', ActorFollower> &
+    PickWith<ActorFollowModel, 'ActorFollowing', ActorFollowing>
+}
+
+// ############################################################################
+
+export type MUserNotification = Omit<UserNotificationModel, 'User' | 'Video' | 'Comment' | 'VideoAbuse' | 'VideoBlacklist' |
+  'VideoImport' | 'Account' | 'ActorFollow'>
+
+// ############################################################################
+
+export type UserNotificationModelForApi = MUserNotification &
+  Use<'Video', UserNotificationIncludes.VideoIncludeChannel> &
+  Use<'Comment', UserNotificationIncludes.VideoCommentInclude> &
+  Use<'VideoAbuse', UserNotificationIncludes.VideoAbuseInclude> &
+  Use<'VideoBlacklist', UserNotificationIncludes.VideoBlacklistInclude> &
+  Use<'VideoImport', UserNotificationIncludes.VideoImportInclude> &
+  Use<'ActorFollow', UserNotificationIncludes.ActorFollowInclude> &
+  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 (file)
index 0000000..62673ab
--- /dev/null
@@ -0,0 +1,5 @@
+import { UserVideoHistoryModel } from '../../../models/account/user-video-history'
+
+export type MUserVideoHistory = Omit<UserVideoHistoryModel, 'Video' | 'User'>
+
+export 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 (file)
index 0000000..52d6d4a
--- /dev/null
@@ -0,0 +1,70 @@
+import { UserModel } from '../../../models/account/user'
+import { PickWith, PickWithOpt } from '../../utils'
+import {
+  MAccount,
+  MAccountDefault,
+  MAccountDefaultChannelDefault,
+  MAccountFormattable,
+  MAccountId,
+  MAccountIdActorId,
+  MAccountUrl
+} from '../account'
+import { MNotificationSetting, MNotificationSettingFormattable } from './user-notification-setting'
+import { AccountModel } from '@server/models/account/account'
+import { MChannelFormattable } from '@server/typings/models'
+
+type Use<K extends keyof UserModel, M> = PickWith<UserModel, K, M>
+
+// ############################################################################
+
+export type MUser = Omit<UserModel, 'Account' | 'NotificationSetting' | 'VideoImports' | 'OAuthTokens'>
+
+// ############################################################################
+
+export type MUserQuotaUsed = MUser & { videoQuotaUsed?: number, videoQuotaUsedDaily?: number }
+export type MUserId = Pick<UserModel, 'id'>
+
+// ############################################################################
+
+// With account
+
+export type MUserAccountId = MUser &
+  Use<'Account', MAccountId>
+
+export type MUserAccountUrl = MUser &
+  Use<'Account', MAccountUrl & MAccountIdActorId>
+
+export type MUserAccount = MUser &
+  Use<'Account', MAccount>
+
+export type MUserAccountDefault = MUser &
+  Use<'Account', MAccountDefault>
+
+// With channel
+
+export type MUserNotifSettingChannelDefault = MUser &
+  Use<'NotificationSetting', MNotificationSetting> &
+  Use<'Account', MAccountDefaultChannelDefault>
+
+// With notification settings
+
+export type MUserWithNotificationSetting = MUser &
+  Use<'NotificationSetting', MNotificationSetting>
+
+export type MUserNotifSettingAccount = MUser &
+  Use<'NotificationSetting', MNotificationSetting> &
+  Use<'Account', MAccount>
+
+// Default scope
+
+export type MUserDefault = MUser &
+  Use<'NotificationSetting', MNotificationSetting> &
+  Use<'Account', MAccountDefault>
+
+// ############################################################################
+
+// Format for API or AP object
+
+export type MUserFormattable = MUserQuotaUsed &
+  Use<'Account', MAccountFormattable & PickWithOpt<AccountModel, 'VideoChannels', MChannelFormattable[]>> &
+  PickWithOpt<UserModel, 'NotificationSetting', MNotificationSettingFormattable>
diff --git a/server/typings/models/video-share.ts b/server/typings/models/video-share.ts
deleted file mode 100644 (file)
index 1406749..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-import { VideoShareModel } from '../../models/video/video-share'
-
-export 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 (file)
index 0000000..bd69c8a
--- /dev/null
@@ -0,0 +1,18 @@
+export * from './schedule-video-update'
+export * from './tag'
+export * from './thumbnail'
+export * from './video'
+export * from './video-abuse'
+export * from './video-blacklist'
+export * from './video-caption'
+export * from './video-change-ownership'
+export * from './video-channels'
+export * from './video-comment'
+export * from './video-file'
+export * from './video-import'
+export * from './video-playlist'
+export * from './video-playlist-element'
+export * from './video-rate'
+export * from './video-redundancy'
+export * from './video-share'
+export * 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 (file)
index 0000000..ada9af0
--- /dev/null
@@ -0,0 +1,9 @@
+import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
+
+export type MScheduleVideoUpdate = Omit<ScheduleVideoUpdateModel, 'Video'>
+
+// ############################################################################
+
+// Format for API or AP object
+
+export 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 (file)
index 0000000..64a6887
--- /dev/null
@@ -0,0 +1,3 @@
+import { TagModel } from '../../../models/video/tag'
+
+export 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 (file)
index 0000000..c03ba55
--- /dev/null
@@ -0,0 +1,3 @@
+import { ThumbnailModel } from '../../../models/video/thumbnail'
+
+export 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 (file)
index 0000000..e38c3f5
--- /dev/null
@@ -0,0 +1,31 @@
+import { VideoAbuseModel } from '../../../models/video/video-abuse'
+import { PickWith } from '../../utils'
+import { MVideo } from './video'
+import { MAccountDefault, MAccountFormattable } from '../account'
+
+type Use<K extends keyof VideoAbuseModel, M> = PickWith<VideoAbuseModel, K, M>
+
+// ############################################################################
+
+export type MVideoAbuse = Omit<VideoAbuseModel, 'Account' | 'Video' | 'toActivityPubObject'>
+
+// ############################################################################
+
+export type MVideoAbuseId = Pick<VideoAbuseModel, 'id'>
+
+export type MVideoAbuseVideo = MVideoAbuse &
+  Pick<VideoAbuseModel, 'toActivityPubObject'> &
+  Use<'Video', MVideo>
+
+export type MVideoAbuseAccountVideo = MVideoAbuse &
+  Pick<VideoAbuseModel, 'toActivityPubObject'> &
+  Use<'Video', MVideo> &
+  Use<'Account', MAccountDefault>
+
+// ############################################################################
+
+// Format for API or AP object
+
+export type MVideoAbuseFormattable = MVideoAbuse &
+  Use<'Account', MAccountFormattable> &
+  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 (file)
index 0000000..e128804
--- /dev/null
@@ -0,0 +1,27 @@
+import { VideoBlacklistModel } from '../../../models/video/video-blacklist'
+import { PickWith } from '@server/typings/utils'
+import { MVideo, MVideoFormattable } from '@server/typings/models'
+
+type Use<K extends keyof VideoBlacklistModel, M> = PickWith<VideoBlacklistModel, K, M>
+
+// ############################################################################
+
+export type MVideoBlacklist = Omit<VideoBlacklistModel, 'Video'>
+
+export type MVideoBlacklistLight = Pick<MVideoBlacklist, 'id' | 'reason' | 'unfederated'>
+export type MVideoBlacklistUnfederated = Pick<MVideoBlacklist, 'unfederated'>
+
+// ############################################################################
+
+export type MVideoBlacklistLightVideo = MVideoBlacklistLight &
+  Use<'Video', MVideo>
+
+export type MVideoBlacklistVideo = MVideoBlacklist &
+  Use<'Video', MVideo>
+
+// ############################################################################
+
+// Format for API or AP object
+
+export type MVideoBlacklistFormattable = MVideoBlacklist &
+  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 (file)
index 0000000..7cb2a2a
--- /dev/null
@@ -0,0 +1,24 @@
+import { VideoCaptionModel } from '../../../models/video/video-caption'
+import { FunctionProperties, PickWith } from '@server/typings/utils'
+import { MVideo, MVideoUUID } from '@server/typings/models'
+
+type Use<K extends keyof VideoCaptionModel, M> = PickWith<VideoCaptionModel, K, M>
+
+// ############################################################################
+
+export type MVideoCaption = Omit<VideoCaptionModel, 'Video'>
+
+// ############################################################################
+
+export type MVideoCaptionLanguage = Pick<MVideoCaption, 'language'>
+
+export type MVideoCaptionVideo = MVideoCaption &
+  Use<'Video', Pick<MVideo, 'id' | 'remote' | 'uuid'>>
+
+// ############################################################################
+
+// Format for API or AP object
+
+export type MVideoCaptionFormattable = FunctionProperties<MVideoCaption> &
+  Pick<MVideoCaption, 'language'> &
+  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 (file)
index 0000000..72634cd
--- /dev/null
@@ -0,0 +1,23 @@
+import { VideoChangeOwnershipModel } from '@server/models/video/video-change-ownership'
+import { PickWith } from '@server/typings/utils'
+import { MAccountDefault, MAccountFormattable, MVideo, MVideoWithFileThumbnail } from '@server/typings/models'
+
+type Use<K extends keyof VideoChangeOwnershipModel, M> = PickWith<VideoChangeOwnershipModel, K, M>
+
+// ############################################################################
+
+export type MVideoChangeOwnership = Omit<VideoChangeOwnershipModel, 'Initiator' | 'NextOwner' | 'Video'>
+
+export type MVideoChangeOwnershipFull = MVideoChangeOwnership &
+  Use<'Initiator', MAccountDefault> &
+  Use<'NextOwner', MAccountDefault> &
+  Use<'Video', MVideoWithFileThumbnail>
+
+// ############################################################################
+
+// Format for API or AP object
+
+export type MVideoChangeOwnershipFormattable = Pick<MVideoChangeOwnership, 'id' | 'status' | 'createdAt'> &
+  Use<'Initiator', MAccountFormattable> &
+  Use<'NextOwner', MAccountFormattable> &
+  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 (file)
index 0000000..292d0ac
--- /dev/null
@@ -0,0 +1,126 @@
+import { FunctionProperties, PickWith, PickWithOpt } from '../../utils'
+import { VideoChannelModel } from '../../../models/video/video-channel'
+import {
+  MAccountActor,
+  MAccountAPI,
+  MAccountDefault,
+  MAccountFormattable,
+  MAccountLight,
+  MAccountSummaryBlocks,
+  MAccountSummaryFormattable,
+  MAccountUrl,
+  MAccountUserId,
+  MActor,
+  MActorAccountChannelId,
+  MActorAP,
+  MActorAPI,
+  MActorDefault,
+  MActorDefaultLight,
+  MActorFormattable,
+  MActorLight,
+  MActorSummary,
+  MActorSummaryFormattable, MActorUrl
+} from '../account'
+import { MVideo } from './video'
+
+type Use<K extends keyof VideoChannelModel, M> = PickWith<VideoChannelModel, K, M>
+
+// ############################################################################
+
+export type MChannel = Omit<VideoChannelModel, 'Actor' | 'Account' | 'Videos' | 'VideoPlaylists'>
+
+// ############################################################################
+
+export type MChannelId = Pick<MChannel, 'id'>
+
+// ############################################################################
+
+export type MChannelIdActor = MChannelId &
+  Use<'Actor', MActorAccountChannelId>
+
+export type MChannelUserId = Pick<MChannel, 'accountId'> &
+  Use<'Account', MAccountUserId>
+
+export type MChannelActor = MChannel &
+  Use<'Actor', MActor>
+
+export type MChannelUrl = Use<'Actor', MActorUrl>
+
+// Default scope
+export type MChannelDefault = MChannel &
+  Use<'Actor', MActorDefault>
+
+// ############################################################################
+
+// Not all association attributes
+
+export type MChannelLight = MChannel &
+  Use<'Actor', MActorDefaultLight>
+
+export type MChannelActorLight = MChannel &
+  Use<'Actor', MActorLight>
+
+export type MChannelAccountLight = MChannel &
+  Use<'Actor', MActorDefaultLight> &
+  Use<'Account', MAccountLight>
+
+// ############################################################################
+
+// Account associations
+
+export type MChannelAccountActor = MChannel &
+  Use<'Account', MAccountActor>
+
+export type MChannelAccountDefault = MChannel &
+  Use<'Actor', MActorDefault> &
+  Use<'Account', MAccountDefault>
+
+export type MChannelActorAccountActor = MChannel &
+  Use<'Account', MAccountActor> &
+  Use<'Actor', MActor>
+
+// ############################################################################
+
+// Videos  associations
+export type MChannelVideos = MChannel &
+  Use<'Videos', MVideo[]>
+
+export type MChannelActorAccountDefaultVideos = MChannel &
+  Use<'Actor', MActorDefault> &
+  Use<'Account', MAccountDefault> &
+  Use<'Videos', MVideo[]>
+
+// ############################################################################
+
+// For API
+
+export type MChannelSummary = FunctionProperties<MChannel> &
+  Pick<MChannel, 'id' | 'name' | 'description' | 'actorId'> &
+  Use<'Actor', MActorSummary>
+
+export type MChannelSummaryAccount = MChannelSummary &
+  Use<'Account', MAccountSummaryBlocks>
+
+export type MChannelAPI = MChannel &
+  Use<'Actor', MActorAPI> &
+  Use<'Account', MAccountAPI>
+
+// ############################################################################
+
+// Format for API or AP object
+
+export type MChannelSummaryFormattable = FunctionProperties<MChannel> &
+  Pick<MChannel, 'id' | 'name'> &
+  Use<'Actor', MActorSummaryFormattable>
+
+export type MChannelAccountSummaryFormattable = MChannelSummaryFormattable &
+  Use<'Account', MAccountSummaryFormattable>
+
+export type MChannelFormattable = FunctionProperties<MChannel> &
+  Pick<MChannel, 'id' | 'name' | 'description' | 'createdAt' | 'updatedAt' | 'support'> &
+  Use<'Actor', MActorFormattable> &
+  PickWithOpt<VideoChannelModel, 'Account', MAccountFormattable>
+
+export type MChannelAP = Pick<MChannel, 'name' | 'description' | 'support'> &
+  Use<'Actor', MActorAP> &
+  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 (file)
index 0000000..4fd1c29
--- /dev/null
@@ -0,0 +1,57 @@
+import { VideoCommentModel } from '../../../models/video/video-comment'
+import { PickWith, PickWithOpt } from '../../utils'
+import { MAccountDefault, MAccountFormattable, MAccountUrl, MActorUrl } from '../account'
+import { MVideoAccountLight, MVideoFeed, MVideoIdUrl, MVideoUrl } from './video'
+
+type Use<K extends keyof VideoCommentModel, M> = PickWith<VideoCommentModel, K, M>
+
+// ############################################################################
+
+export type MComment = Omit<VideoCommentModel, 'OriginVideoComment' | 'InReplyToVideoComment' | 'Video' | 'Account'>
+export type MCommentTotalReplies = MComment & { totalReplies?: number }
+export type MCommentId = Pick<MComment, 'id'>
+export type MCommentUrl = Pick<MComment, 'url'>
+
+// ############################################################################
+
+export type MCommentOwner = MComment &
+  Use<'Account', MAccountDefault>
+
+export type MCommentVideo = MComment &
+  Use<'Video', MVideoAccountLight>
+
+export type MCommentReply = MComment &
+  Use<'InReplyToVideoComment', MComment>
+
+export type MCommentOwnerVideo = MComment &
+  Use<'Account', MAccountDefault> &
+  Use<'Video', MVideoAccountLight>
+
+export type MCommentOwnerVideoReply = MComment &
+  Use<'Account', MAccountDefault> &
+  Use<'Video', MVideoAccountLight> &
+  Use<'InReplyToVideoComment', MComment>
+
+export type MCommentOwnerReplyVideoLight = MComment &
+  Use<'Account', MAccountDefault> &
+  Use<'InReplyToVideoComment', MComment> &
+  Use<'Video', MVideoIdUrl>
+
+export type MCommentOwnerVideoFeed = MCommentOwner &
+  Use<'Video', MVideoFeed>
+
+// ############################################################################
+
+export type MCommentAPI = MComment & { totalReplies: number }
+
+// ############################################################################
+
+// Format for API or AP object
+
+export type MCommentFormattable = MCommentTotalReplies &
+  Use<'Account', MAccountFormattable>
+
+export type MCommentAP = MComment &
+  Use<'Account', MAccountUrl> &
+  PickWithOpt<VideoCommentModel, 'Video', MVideoUrl> &
+  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 (file)
index 0000000..484351a
--- /dev/null
@@ -0,0 +1,19 @@
+import { VideoFileModel } from '../../../models/video/video-file'
+import { PickWith, PickWithOpt } from '../../utils'
+import { MVideo, MVideoUUID } from './video'
+import { MVideoRedundancyFileUrl } from './video-redundancy'
+
+type Use<K extends keyof VideoFileModel, M> = PickWith<VideoFileModel, K, M>
+
+// ############################################################################
+
+export type MVideoFile = Omit<VideoFileModel, 'Video' | 'RedundancyVideos'>
+
+export type MVideoFileVideo = MVideoFile &
+  Use<'Video', MVideo>
+
+export type MVideoFileVideoUUID = MVideoFile &
+  Use<'Video', MVideoUUID>
+
+export type MVideoFileRedundanciesOpt = MVideoFile &
+  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 (file)
index 0000000..c6a1c5b
--- /dev/null
@@ -0,0 +1,31 @@
+import { VideoImportModel } from '@server/models/video/video-import'
+import { PickWith, PickWithOpt } from '@server/typings/utils'
+import { MUser, MVideo, MVideoAccountLight, MVideoFormattable, MVideoTag, MVideoThumbnail, MVideoWithFile } from '@server/typings/models'
+
+type Use<K extends keyof VideoImportModel, M> = PickWith<VideoImportModel, K, M>
+
+// ############################################################################
+
+export type MVideoImport = Omit<VideoImportModel, 'User' | 'Video'>
+
+export type MVideoImportVideo = MVideoImport &
+  Use<'Video', MVideo>
+
+// ############################################################################
+
+type VideoAssociation = MVideoTag & MVideoAccountLight & MVideoThumbnail
+
+export type MVideoImportDefault = MVideoImport &
+  Use<'User', MUser> &
+  Use<'Video', VideoAssociation>
+
+export type MVideoImportDefaultFiles = MVideoImport &
+  Use<'User', MUser> &
+  Use<'Video', VideoAssociation & MVideoWithFile>
+
+// ############################################################################
+
+// Format for API or AP object
+
+export type MVideoImportFormattable = MVideoImport &
+  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 (file)
index 0000000..7b1b993
--- /dev/null
@@ -0,0 +1,34 @@
+import { VideoPlaylistElementModel } from '@server/models/video/video-playlist-element'
+import { PickWith } from '@server/typings/utils'
+import { MVideoFormattable, MVideoPlaylistPrivacy, MVideoThumbnail, MVideoUrl } from '@server/typings/models'
+
+type Use<K extends keyof VideoPlaylistElementModel, M> = PickWith<VideoPlaylistElementModel, K, M>
+
+// ############################################################################
+
+export type MVideoPlaylistElement = Omit<VideoPlaylistElementModel, 'VideoPlaylist' | 'Video'>
+
+// ############################################################################
+
+export type MVideoPlaylistElementId = Pick<MVideoPlaylistElement, 'id'>
+
+export type MVideoPlaylistElementLight = Pick<MVideoPlaylistElement, 'id' | 'videoId' | 'startTimestamp' | 'stopTimestamp'>
+
+// ############################################################################
+
+export type MVideoPlaylistVideoThumbnail = MVideoPlaylistElement &
+  Use<'Video', MVideoThumbnail>
+
+export type MVideoPlaylistElementVideoUrlPlaylistPrivacy = MVideoPlaylistElement &
+  Use<'Video', MVideoUrl> &
+  Use<'VideoPlaylist', MVideoPlaylistPrivacy>
+
+// ############################################################################
+
+// Format for API or AP object
+
+export type MVideoPlaylistElementFormattable = MVideoPlaylistElement &
+  Use<'Video', MVideoFormattable>
+
+export type MVideoPlaylistElementAP = MVideoPlaylistElement &
+  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 (file)
index 0000000..a40c7ac
--- /dev/null
@@ -0,0 +1,92 @@
+import { VideoPlaylistModel } from '../../../models/video/video-playlist'
+import { PickWith } from '../../utils'
+import { MAccount, MAccountDefault, MAccountSummary, MAccountSummaryFormattable } from '../account'
+import { MThumbnail } from './thumbnail'
+import { MChannelDefault, MChannelSummary, MChannelSummaryFormattable, MChannelUrl } from './video-channels'
+import { MVideoPlaylistElementLight } from '@server/typings/models/video/video-playlist-element'
+
+type Use<K extends keyof VideoPlaylistModel, M> = PickWith<VideoPlaylistModel, K, M>
+
+// ############################################################################
+
+export type MVideoPlaylist = Omit<VideoPlaylistModel, 'OwnerAccount' | 'VideoChannel' | 'VideoPlaylistElements' | 'Thumbnail'>
+
+// ############################################################################
+
+export type MVideoPlaylistId = Pick<MVideoPlaylist, 'id'>
+export type MVideoPlaylistPrivacy = Pick<MVideoPlaylist, 'privacy'>
+export type MVideoPlaylistUUID = Pick<MVideoPlaylist, 'uuid'>
+export type MVideoPlaylistVideosLength = MVideoPlaylist & { videosLength?: number }
+
+// ############################################################################
+
+// With elements
+
+export type MVideoPlaylistWithElements = MVideoPlaylist &
+  Use<'VideoPlaylistElements', MVideoPlaylistElementLight[]>
+
+export type MVideoPlaylistIdWithElements = MVideoPlaylistId &
+  Use<'VideoPlaylistElements', MVideoPlaylistElementLight[]>
+
+// ############################################################################
+
+// With account
+
+export type MVideoPlaylistOwner = MVideoPlaylist &
+  Use<'OwnerAccount', MAccount>
+
+export type MVideoPlaylistOwnerDefault = MVideoPlaylist &
+  Use<'OwnerAccount', MAccountDefault>
+
+// ############################################################################
+
+// With thumbnail
+
+export type MVideoPlaylistThumbnail = MVideoPlaylist &
+  Use<'Thumbnail', MThumbnail>
+
+export type MVideoPlaylistAccountThumbnail = MVideoPlaylist &
+  Use<'OwnerAccount', MAccountDefault> &
+  Use<'Thumbnail', MThumbnail>
+
+// ############################################################################
+
+// With channel
+
+export type MVideoPlaylistAccountChannelDefault = MVideoPlaylist &
+  Use<'OwnerAccount', MAccountDefault> &
+  Use<'VideoChannel', MChannelDefault>
+
+// ############################################################################
+
+// With all associations
+
+export type MVideoPlaylistFull = MVideoPlaylist &
+  Use<'OwnerAccount', MAccountDefault> &
+  Use<'VideoChannel', MChannelDefault> &
+  Use<'Thumbnail', MThumbnail>
+
+// ############################################################################
+
+// For API
+
+export type MVideoPlaylistAccountChannelSummary = MVideoPlaylist &
+  Use<'OwnerAccount', MAccountSummary> &
+  Use<'VideoChannel', MChannelSummary>
+
+export type MVideoPlaylistFullSummary = MVideoPlaylist &
+  Use<'Thumbnail', MThumbnail> &
+  Use<'OwnerAccount', MAccountSummary> &
+  Use<'VideoChannel', MChannelSummary>
+
+// ############################################################################
+
+// Format for API or AP object
+
+export type MVideoPlaylistFormattable = MVideoPlaylistVideosLength &
+  Use<'OwnerAccount', MAccountSummaryFormattable> &
+  Use<'VideoChannel', MChannelSummaryFormattable>
+
+export type MVideoPlaylistAP = MVideoPlaylist &
+  Use<'Thumbnail', MThumbnail> &
+  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 (file)
index 0000000..2ff8a62
--- /dev/null
@@ -0,0 +1,23 @@
+import { AccountVideoRateModel } from '@server/models/account/account-video-rate'
+import { PickWith } from '@server/typings/utils'
+import { MAccountAudience, MAccountUrl, MVideo, MVideoFormattable } from '..'
+
+type Use<K extends keyof AccountVideoRateModel, M> = PickWith<AccountVideoRateModel, K, M>
+
+// ############################################################################
+
+export type MAccountVideoRate = Omit<AccountVideoRateModel, 'Video' | 'Account'>
+
+export type MAccountVideoRateAccountUrl = MAccountVideoRate &
+  Use<'Account', MAccountUrl>
+
+export type MAccountVideoRateAccountVideo = MAccountVideoRate &
+  Use<'Account', MAccountAudience> &
+  Use<'Video', MVideo>
+
+// ############################################################################
+
+// Format for API or AP object
+
+export type MAccountVideoRateFormattable = Pick<MAccountVideoRate, 'type'> &
+  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 (file)
index 0000000..f3846af
--- /dev/null
@@ -0,0 +1,38 @@
+import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy'
+import { PickWith, PickWithOpt } from '@server/typings/utils'
+import { MStreamingPlaylistVideo, MVideoFile, MVideoFileVideo, MVideoUrl } from '@server/typings/models'
+import { VideoStreamingPlaylist } from '../../../../shared/models/videos/video-streaming-playlist.model'
+import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist'
+import { VideoFile } from '../../../../shared/models/videos'
+import { VideoFileModel } from '@server/models/video/video-file'
+
+type Use<K extends keyof VideoRedundancyModel, M> = PickWith<VideoRedundancyModel, K, M>
+
+// ############################################################################
+
+export type MVideoRedundancy = Omit<VideoRedundancyModel, 'VideoFile' | 'VideoStreamingPlaylist' | 'Actor'>
+
+export type MVideoRedundancyFileUrl = Pick<MVideoRedundancy, 'fileUrl'>
+
+// ############################################################################
+
+export type MVideoRedundancyFile = MVideoRedundancy &
+  Use<'VideoFile', MVideoFile>
+
+export type MVideoRedundancyFileVideo = MVideoRedundancy &
+  Use<'VideoFile', MVideoFileVideo>
+
+export type MVideoRedundancyStreamingPlaylistVideo = MVideoRedundancy &
+  Use<'VideoStreamingPlaylist', MStreamingPlaylistVideo>
+
+export type MVideoRedundancyVideo = MVideoRedundancy &
+  Use<'VideoFile', MVideoFileVideo> &
+  Use<'VideoStreamingPlaylist', MStreamingPlaylistVideo>
+
+// ############################################################################
+
+// Format for API or AP object
+
+export type MVideoRedundancyAP = MVideoRedundancy &
+  PickWithOpt<VideoRedundancyModel, 'VideoFile', MVideoFile & PickWith<VideoFileModel, 'Video', MVideoUrl>> &
+  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 (file)
index 0000000..a7a90be
--- /dev/null
@@ -0,0 +1,17 @@
+import { VideoShareModel } from '../../../models/video/video-share'
+import { PickWith } from '../../utils'
+import { MActorDefault } from '../account'
+import { MVideo } from './video'
+
+type Use<K extends keyof VideoShareModel, M> = PickWith<VideoShareModel, K, M>
+
+// ############################################################################
+
+export type MVideoShare = Omit<VideoShareModel, 'Actor' | 'Video'>
+
+export type MVideoShareActor = MVideoShare &
+  Use<'Actor', MActorDefault>
+
+export type MVideoShareFull = MVideoShare &
+  Use<'Actor', MActorDefault> &
+  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 (file)
index 0000000..79696bc
--- /dev/null
@@ -0,0 +1,19 @@
+import { VideoStreamingPlaylistModel } from '../../../models/video/video-streaming-playlist'
+import { PickWith, PickWithOpt } from '../../utils'
+import { MVideoRedundancyFileUrl } from './video-redundancy'
+import { MVideo, MVideoUrl } from '@server/typings/models'
+
+type Use<K extends keyof VideoStreamingPlaylistModel, M> = PickWith<VideoStreamingPlaylistModel, K, M>
+
+// ############################################################################
+
+export type MStreamingPlaylist = Omit<VideoStreamingPlaylistModel, 'Video' | 'RedundancyVideos'>
+
+export type MStreamingPlaylistVideo = MStreamingPlaylist &
+  Use<'Video', MVideo>
+
+export type MStreamingPlaylistRedundancies = MStreamingPlaylist &
+  Use<'RedundancyVideos', MVideoRedundancyFileUrl[]>
+
+export type MStreamingPlaylistRedundanciesOpt = MStreamingPlaylist &
+  PickWithOpt<VideoStreamingPlaylistModel, 'RedundancyVideos', MVideoRedundancyFileUrl[]>
diff --git a/server/typings/models/video/video.ts b/server/typings/models/video/video.ts
new file mode 100644 (file)
index 0000000..9a53bd3
--- /dev/null
@@ -0,0 +1,173 @@
+import { VideoModel } from '../../../models/video/video'
+import { PickWith, PickWithOpt } from '../../utils'
+import {
+  MChannelAccountDefault,
+  MChannelAccountLight,
+  MChannelAccountSummaryFormattable,
+  MChannelActor,
+  MChannelFormattable,
+  MChannelUserId
+} from './video-channels'
+import { MTag } from './tag'
+import { MVideoCaptionLanguage } from './video-caption'
+import { MStreamingPlaylist, MStreamingPlaylistRedundancies, MStreamingPlaylistRedundanciesOpt } from './video-streaming-playlist'
+import { MVideoFile, MVideoFileRedundanciesOpt } from './video-file'
+import { MThumbnail } from './thumbnail'
+import { MVideoBlacklist, MVideoBlacklistLight, MVideoBlacklistUnfederated } from './video-blacklist'
+import { MScheduleVideoUpdate } from './schedule-video-update'
+import { MUserVideoHistoryTime } from '../user/user-video-history'
+
+type Use<K extends keyof VideoModel, M> = PickWith<VideoModel, K, M>
+
+// ############################################################################
+
+export type MVideo = Omit<VideoModel, 'VideoChannel' | 'Tags' | 'Thumbnails' | 'VideoPlaylistElements' | 'VideoAbuses' |
+  'VideoFiles' | 'VideoStreamingPlaylists' | 'VideoShares' | 'AccountVideoRates' | 'VideoComments' | 'VideoViews' | 'UserVideoHistories' |
+  'ScheduleVideoUpdate' | 'VideoBlacklist' | 'VideoImport' | 'VideoCaptions'>
+
+// ############################################################################
+
+export type MVideoId = Pick<MVideo, 'id'>
+export type MVideoUrl = Pick<MVideo, 'url'>
+export type MVideoUUID = Pick<MVideo, 'uuid'>
+
+export type MVideoIdUrl = MVideoId & MVideoUrl
+export type MVideoFeed = Pick<MVideo, 'name' | 'uuid'>
+
+// ############################################################################
+
+// Video raw associations: schedules, video files, tags, thumbnails, captions, streaming playlists
+
+// "With" to not confuse with the VideoFile model
+export type MVideoWithFile = MVideo &
+  Use<'VideoFiles', MVideoFile[]>
+
+export type MVideoThumbnail = MVideo &
+  Use<'Thumbnails', MThumbnail[]>
+
+export type MVideoIdThumbnail = MVideoId &
+  Use<'Thumbnails', MThumbnail[]>
+
+export type MVideoWithFileThumbnail = MVideo &
+  Use<'VideoFiles', MVideoFile[]> &
+  Use<'Thumbnails', MThumbnail[]>
+
+export type MVideoThumbnailBlacklist = MVideo &
+  Use<'Thumbnails', MThumbnail[]> &
+  Use<'VideoBlacklist', MVideoBlacklistLight>
+
+export type MVideoTag = MVideo &
+  Use<'Tags', MTag[]>
+
+export type MVideoWithSchedule = MVideo &
+  PickWithOpt<VideoModel, 'ScheduleVideoUpdate', MScheduleVideoUpdate>
+
+export type MVideoWithCaptions = MVideo &
+  Use<'VideoCaptions', MVideoCaptionLanguage[]>
+
+export type MVideoWithStreamingPlaylist = MVideo &
+  Use<'VideoStreamingPlaylists', MStreamingPlaylist[]>
+
+// ############################################################################
+
+// Associations with not all their attributes
+
+export type MVideoUserHistory = MVideo &
+  Use<'UserVideoHistories', MUserVideoHistoryTime[]>
+
+export type MVideoWithBlacklistLight = MVideo &
+  Use<'VideoBlacklist', MVideoBlacklistLight>
+
+export type MVideoAccountLight = MVideo &
+  Use<'VideoChannel', MChannelAccountLight>
+
+export type MVideoWithRights = MVideo &
+  Use<'VideoBlacklist', MVideoBlacklistLight> &
+  Use<'Thumbnails', MThumbnail[]> &
+  Use<'VideoChannel', MChannelUserId>
+
+// ############################################################################
+
+// All files with some additional associations
+
+export type MVideoWithAllFiles = MVideo &
+  Use<'VideoFiles', MVideoFile[]> &
+  Use<'Thumbnails', MThumbnail[]> &
+  Use<'VideoStreamingPlaylists', MStreamingPlaylist[]>
+
+export type MVideoAccountLightBlacklistAllFiles = MVideo &
+  Use<'VideoFiles', MVideoFile[]> &
+  Use<'Thumbnails', MThumbnail[]> &
+  Use<'VideoStreamingPlaylists', MStreamingPlaylist[]> &
+  Use<'VideoChannel', MChannelAccountLight> &
+  Use<'VideoBlacklist', MVideoBlacklistLight>
+
+// ############################################################################
+
+// With account
+
+export type MVideoAccountDefault = MVideo &
+  Use<'VideoChannel', MChannelAccountDefault>
+
+export type MVideoThumbnailAccountDefault = MVideo &
+  Use<'Thumbnails', MThumbnail[]> &
+  Use<'VideoChannel', MChannelAccountDefault>
+
+export type MVideoWithChannelActor = MVideo &
+  Use<'VideoChannel', MChannelActor>
+
+export type MVideoFullLight = MVideo &
+  Use<'Thumbnails', MThumbnail[]> &
+  Use<'VideoBlacklist', MVideoBlacklistLight> &
+  Use<'Tags', MTag[]> &
+  Use<'VideoChannel', MChannelAccountLight> &
+  Use<'UserVideoHistories', MUserVideoHistoryTime[]> &
+  Use<'VideoFiles', MVideoFile[]> &
+  Use<'ScheduleVideoUpdate', MScheduleVideoUpdate> &
+  Use<'VideoStreamingPlaylists', MStreamingPlaylist[]>
+
+// ############################################################################
+
+// API
+
+export type MVideoAP = MVideo &
+  Use<'Tags', MTag[]> &
+  Use<'VideoChannel', MChannelAccountLight> &
+  Use<'VideoStreamingPlaylists', MStreamingPlaylist[]> &
+  Use<'VideoCaptions', MVideoCaptionLanguage[]> &
+  Use<'VideoBlacklist', MVideoBlacklistUnfederated> &
+  Use<'VideoFiles', MVideoFileRedundanciesOpt[]>
+
+export type MVideoAPWithoutCaption = Omit<MVideoAP, 'VideoCaptions'>
+
+export type MVideoDetails = MVideo &
+  Use<'VideoBlacklist', MVideoBlacklistLight> &
+  Use<'Tags', MTag[]> &
+  Use<'VideoChannel', MChannelAccountLight> &
+  Use<'ScheduleVideoUpdate', MScheduleVideoUpdate> &
+  Use<'Thumbnails', MThumbnail[]> &
+  Use<'UserVideoHistories', MUserVideoHistoryTime[]> &
+  Use<'VideoStreamingPlaylists', MStreamingPlaylistRedundancies[]> &
+  Use<'VideoFiles', MVideoFileRedundanciesOpt[]>
+
+export type MVideoForUser = MVideo &
+  Use<'VideoChannel', MChannelAccountDefault> &
+  Use<'ScheduleVideoUpdate', MScheduleVideoUpdate> &
+  Use<'VideoBlacklist', MVideoBlacklistLight> &
+  Use<'Thumbnails', MThumbnail[]>
+
+// ############################################################################
+
+// Format for API or AP object
+
+export type MVideoFormattable = MVideo &
+  PickWithOpt<VideoModel, 'UserVideoHistories', MUserVideoHistoryTime[]> &
+  Use<'VideoChannel', MChannelAccountSummaryFormattable> &
+  PickWithOpt<VideoModel, 'ScheduleVideoUpdate', Pick<MScheduleVideoUpdate, 'updateAt' | 'privacy'>> &
+  PickWithOpt<VideoModel, 'VideoBlacklist', Pick<MVideoBlacklist, 'reason'>>
+
+export type MVideoFormattableDetails = MVideoFormattable &
+  Use<'VideoChannel', MChannelFormattable> &
+  Use<'Tags', MTag[]> &
+  Use<'VideoStreamingPlaylists', MStreamingPlaylistRedundanciesOpt[]> &
+  Use<'VideoFiles', MVideoFileRedundanciesOpt[]>
index a86b05be2d1bf273e2bca51377809aab9c8552b8..24d43b2587007611a1836bc3a881e562e243f5c3 100644 (file)
@@ -1,3 +1,22 @@
-export type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T]
+export type FunctionPropertyNames<T> = {
+  [K in keyof T]: T[K] extends Function ? K : never
+}[keyof T]
 
 export type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>
+
+export type PickWith<T, KT extends keyof T, V> = {
+  [P in KT]: T[P] extends V ? V : never
+}
+
+export type PickWithOpt<T, KT extends keyof T, V> = {
+  [P in KT]?: T[P] extends V ? V : never
+}
+
+// https://github.com/krzkaczor/ts-essentials Rocks!
+export type DeepPartial<T> = {
+  [P in keyof T]?: T[P] extends Array<infer U>
+    ? Array<DeepPartial<U>>
+    : T[P] extends ReadonlyArray<infer U>
+      ? ReadonlyArray<DeepPartial<U>>
+      : DeepPartial<T[P]>
+}
index 53ddaa681befabe24ddb28922b8b08bbbc84c832..78acf72aa7af9d93a852e756e3c7452f39d3dbef 100644 (file)
@@ -24,4 +24,5 @@ export * from './videos/video-streaming-playlists'
 export * from './videos/videos'
 export * from './videos/video-change-ownership'
 export * from './feeds/feeds'
+export * from './instances-index/mock-instances-index'
 export * 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 (file)
index 0000000..cfa4523
--- /dev/null
@@ -0,0 +1,38 @@
+import * as express from 'express'
+
+export class MockInstancesIndex {
+  private indexInstances: { host: string, createdAt: string }[] = []
+
+  initialize () {
+    return new Promise(res => {
+      const app = express()
+
+      app.use('/', (req: express.Request, res: express.Response, next: express.NextFunction) => {
+        if (process.env.DEBUG) console.log('Receiving request on mocked server %s.', req.url)
+
+        return next()
+      })
+
+      app.get('/api/v1/instances/hosts', (req: express.Request, res: express.Response) => {
+        const since = req.query.since
+
+        const filtered = this.indexInstances.filter(i => {
+          if (!since) return true
+
+          return i.createdAt > since
+        })
+
+        return res.json({
+          total: filtered.length,
+          data: filtered
+        })
+      })
+
+      app.listen(42100, () => res())
+    })
+  }
+
+  addInstance (host: string) {
+    this.indexInstances.push({ host, createdAt: new Date().toISOString() })
+  }
+}
index 8736f083f55ea8e6c6245a1ac3abf2c77fd6fed5..578dd35cf16c33a6fe14757d534d59694f1a8a82 100644 (file)
@@ -1,5 +1,7 @@
 import { makeDeleteRequest, makeGetRequest, makePutBodyRequest } from '../requests/requests'
 import { CustomConfig } from '../../models/server/custom-config.model'
+import { DeepPartial } from '@server/typings/utils'
+import { merge } from 'lodash'
 
 function getConfig (url: string) {
   const path = '/api/v1/config'
@@ -44,13 +46,25 @@ function updateCustomConfig (url: string, token: string, newCustomConfig: Custom
   })
 }
 
-function updateCustomSubConfig (url: string, token: string, newConfig: any) {
+function updateCustomSubConfig (url: string, token: string, newConfig: DeepPartial<CustomConfig>) {
   const updateParams: CustomConfig = {
     instance: {
       name: 'PeerTube updated',
       shortDescription: 'my short description',
       description: 'my super description',
       terms: 'my super terms',
+      codeOfConduct: 'my super coc',
+
+      creationReason: 'my super creation reason',
+      moderationInformation: 'my super moderation information',
+      administrator: 'Kuja',
+      maintenanceLifetime: 'forever',
+      businessModel: 'my super business model',
+      hardwareInformation: '2vCore 3GB RAM',
+
+      languages: [ 'en', 'es' ],
+      categories: [ 1, 2 ],
+
       defaultClientRoute: '/videos/recently-added',
       isNSFW: true,
       defaultNSFWPolicy: 'blur',
@@ -130,10 +144,21 @@ function updateCustomSubConfig (url: string, token: string, newConfig: any) {
         enabled: true,
         manualApproval: false
       }
+    },
+    followings: {
+      instance: {
+        autoFollowBack: {
+          enabled: false
+        },
+        autoFollowIndex: {
+          indexUrl: 'https://instances.joinpeertube.org',
+          enabled: false
+        }
+      }
     }
   }
 
-  Object.assign(updateParams, newConfig)
+  merge(updateParams, newConfig)
 
   return updateCustomConfig(url, token, updateParams)
 }
index f7de542bfe731b87c9ce94bc493cab7ea2e0a161..9a5fd7e86824e3e27386ecc6a237be97cb70397b 100644 (file)
@@ -279,8 +279,9 @@ async function checkNewActorFollow (
       expect(notification.actorFollow.follower.name).to.equal(followerName)
       expect(notification.actorFollow.follower.host).to.not.be.undefined
 
-      expect(notification.actorFollow.following.displayName).to.equal(followingDisplayName)
-      expect(notification.actorFollow.following.type).to.equal(followType)
+      const following = notification.actorFollow.following
+      expect(following.displayName).to.equal(followingDisplayName)
+      expect(following.type).to.equal(followType)
     } else {
       expect(notification).to.satisfy(n => {
         return n.type !== notificationType ||
@@ -327,6 +328,37 @@ async function checkNewInstanceFollower (base: CheckerBaseParams, followerHost:
   await checkNotification(base, notificationChecker, emailFinder, type)
 }
 
+async function checkAutoInstanceFollowing (base: CheckerBaseParams, followerHost: string, followingHost: string, type: CheckerType) {
+  const notificationType = UserNotificationType.AUTO_INSTANCE_FOLLOWING
+
+  function notificationChecker (notification: UserNotification, type: CheckerType) {
+    if (type === 'presence') {
+      expect(notification).to.not.be.undefined
+      expect(notification.type).to.equal(notificationType)
+
+      const following = notification.actorFollow.following
+      checkActor(following)
+      expect(following.name).to.equal('peertube')
+      expect(following.host).to.equal(followingHost)
+
+      expect(notification.actorFollow.follower.name).to.equal('peertube')
+      expect(notification.actorFollow.follower.host).to.equal(followerHost)
+    } else {
+      expect(notification).to.satisfy(n => {
+        return n.type !== notificationType || n.actorFollow.following.host !== followingHost
+      })
+    }
+  }
+
+  function emailFinder (email: object) {
+    const text: string = email[ 'text' ]
+
+    return text.includes(' automatically followed a new instance') && text.includes(followingHost)
+  }
+
+  await checkNotification(base, notificationChecker, emailFinder, type)
+}
+
 async function checkCommentMention (
   base: CheckerBaseParams,
   uuid: string,
@@ -427,8 +459,8 @@ async function checkVideoAutoBlacklistForModerators (base: CheckerBaseParams, vi
       expect(notification).to.not.be.undefined
       expect(notification.type).to.equal(notificationType)
 
-      expect(notification.video.id).to.be.a('number')
-      checkVideo(notification.video, videoName, videoUUID)
+      expect(notification.videoBlacklist.video.id).to.be.a('number')
+      checkVideo(notification.videoBlacklist.video, videoName, videoUUID)
     } else {
       expect(notification).to.satisfy((n: UserNotification) => {
         return n === undefined || n.video === undefined || n.video.uuid !== videoUUID
@@ -480,6 +512,7 @@ export {
   markAsReadAllNotifications,
   checkMyVideoImportIsFinished,
   checkUserRegistered,
+  checkAutoInstanceFollowing,
   checkVideoIsPublished,
   checkNewVideoFromSubscription,
   checkNewActorFollow,
index 30ed1bf4a325eb1ab96434d36d43a46b1eeda9b9..9959fd0745b0625b4b72ab0cd9a075a625539f33 100644 (file)
@@ -1,12 +1,12 @@
 import * as request from 'supertest'
 import { makePostBodyRequest, makePutBodyRequest, updateAvatarRequest } from '../requests/requests'
-import { NSFWPolicyType } from '../../models/videos/nsfw-policy.type'
 import { UserAdminFlag } from '../../models/users/user-flag.model'
 import { UserRegister } from '../../models/users/user-register.model'
 import { UserRole } from '../../models/users/user-role'
 import { ServerInfo } from '../server/servers'
 import { userLogin } from './login'
 import { UserUpdateMe } from '../../models/users'
+import { omit } from 'lodash'
 
 type CreateUserArgs = { url: string,
   accessToken: string,
@@ -214,33 +214,10 @@ function unblockUser (url: string, userId: number | string, accessToken: string,
     .expect(expectedStatus)
 }
 
-function updateMyUser (options: {
-  url: string
-  accessToken: string
-  currentPassword?: string
-  newPassword?: string
-  nsfwPolicy?: NSFWPolicyType
-  email?: string
-  autoPlayVideo?: boolean
-  displayName?: string
-  description?: string
-  videosHistoryEnabled?: boolean
-  theme?: string
-}) {
+function updateMyUser (options: { url: string, accessToken: string } & UserUpdateMe) {
   const path = '/api/v1/users/me'
 
-  const toSend: UserUpdateMe = {}
-  if (options.currentPassword !== undefined && options.currentPassword !== null) toSend.currentPassword = options.currentPassword
-  if (options.newPassword !== undefined && options.newPassword !== null) toSend.password = options.newPassword
-  if (options.nsfwPolicy !== undefined && options.nsfwPolicy !== null) toSend.nsfwPolicy = options.nsfwPolicy
-  if (options.autoPlayVideo !== undefined && options.autoPlayVideo !== null) toSend.autoPlayVideo = options.autoPlayVideo
-  if (options.email !== undefined && options.email !== null) toSend.email = options.email
-  if (options.description !== undefined && options.description !== null) toSend.description = options.description
-  if (options.displayName !== undefined && options.displayName !== null) toSend.displayName = options.displayName
-  if (options.theme !== undefined && options.theme !== null) toSend.theme = options.theme
-  if (options.videosHistoryEnabled !== undefined && options.videosHistoryEnabled !== null) {
-    toSend.videosHistoryEnabled = options.videosHistoryEnabled
-  }
+  const toSend: UserUpdateMe = omit(options, 'url', 'accessToken')
 
   return makePutBodyRequest({
     url: options.url,
index 95801190d83d07223e0b92c699e0b1a5bdaaf787..492b672c7b4a7087864bfa0d8a705a7b4bbb5be3 100644 (file)
@@ -91,5 +91,5 @@ export interface ActivityDislike extends BaseActivity {
 export interface ActivityFlag extends BaseActivity {
   type: 'Flag',
   content: string,
-  object: APObject
+  object: APObject | APObject[]
 }
index 40e7abd576a6a7248585ca49d4694f0c685594a1..5f1264a76b68e373beea13070584a8e86758c411 100644 (file)
@@ -1,5 +1,5 @@
 export interface VideoAbuseObject {
   type: 'Flag',
   content: string
-  object: string
+  object: string | string[]
 }
index 218fd09ba040bd33ad6fb30af7697ca9bc167373..03a5d858af495be5f864839ec7795f05e182c93f 100644 (file)
@@ -39,7 +39,9 @@ const I18N_LOCALE_ALIAS = {
   'pl': 'pl-PL',
   'ru': 'ru-RU',
   'nl': 'nl-NL',
-  'zh': 'zh-Hans-CN'
+  'zh': 'zh-Hans-CN',
+  'zh-CN': 'zh-Hans-CN',
+  'zh-TW': 'zh-Hant-TW'
 }
 
 export const POSSIBLE_LOCALES = Object.keys(I18N_LOCALES)
index 10dff8b8f0e0e2e98efa9fb4abd0251f5e3fbe1f..6d4ba63c48aabf16be2883818e7afcf18baed2e6 100644 (file)
@@ -4,5 +4,17 @@ export interface About {
     shortDescription: string
     description: string
     terms: string
+
+    codeOfConduct: string
+    hardwareInformation: string
+
+    creationReason: string
+    moderationInformation: string
+    administrator: string
+    maintenanceLifetime: string
+    businessModel: string
+
+    languages: string[]
+    categories: number[]
   }
 }
index a0541f5b621f17fbf8880003bb9c308907f933b3..c9957f8256a18efeb52b4d2675036738021d0c49 100644 (file)
@@ -6,6 +6,18 @@ export interface CustomConfig {
     shortDescription: string
     description: string
     terms: string
+    codeOfConduct: string
+
+    creationReason: string
+    moderationInformation: string
+    administrator: string
+    maintenanceLifetime: string
+    businessModel: string
+    hardwareInformation: string
+
+    languages: string[]
+    categories: number[]
+
     isNSFW: boolean
     defaultClientRoute: string
     defaultNSFWPolicy: NSFWPolicyType
@@ -99,4 +111,16 @@ export interface CustomConfig {
     }
   }
 
+  followings: {
+    instance: {
+      autoFollowBack: {
+        enabled: boolean
+      }
+
+      autoFollowIndex: {
+        enabled: boolean
+        indexUrl: string
+      }
+    }
+  }
 }
index e2a882b6916e324f9c28289dfc6c3b2ea3b33081..451f40d5841754cdaeeb00918be2e86898937764 100644 (file)
@@ -16,4 +16,5 @@ export interface UserNotificationSetting {
   newFollow: UserNotificationSettingValue
   commentMention: UserNotificationSettingValue
   newInstanceFollower: UserNotificationSettingValue
+  autoInstanceFollowing: UserNotificationSettingValue
 }
index fafc2b7d74e12cca8ae6618627166d51f877bc3b..e9be1ca7fd76c6a7f844f6304dcb6f45c63ad7f8 100644 (file)
@@ -19,7 +19,9 @@ export enum UserNotificationType {
 
   VIDEO_AUTO_BLACKLIST_FOR_MODERATORS = 12,
 
-  NEW_INSTANCE_FOLLOWER = 13
+  NEW_INSTANCE_FOLLOWER = 13,
+
+  AUTO_INSTANCE_FOLLOWING = 14
 }
 
 export interface VideoInfo {
@@ -78,10 +80,12 @@ export interface UserNotification {
     id: number
     follower: ActorInfo
     state: FollowState
+
     following: {
-      type: 'account' | 'channel'
+      type: 'account' | 'channel' | 'instance'
       name: string
       displayName: string
+      host: string
     }
   }
 
index b6c0002e52e702eea665336ef1557217796f9bb3..99b9a65bd7cbfbcd9fbb90124fc0e9daad84ae5b 100644 (file)
@@ -15,4 +15,7 @@ export interface UserUpdateMe {
   password?: string
 
   theme?: string
+
+  noInstanceConfigWarningModal?: boolean
+  noWelcomeModal?: boolean
 }
index de9825e1f7583b0d6dad62787aa9f82680f60478..f67d262b036aa7ce62ad5648ca6db74923e5254e 100644 (file)
@@ -10,6 +10,7 @@ export interface User {
   username: string
   email: string
   pendingEmail: string | null
+
   emailVerified: boolean
   nsfwPolicy: NSFWPolicyType
 
@@ -18,13 +19,15 @@ export interface User {
   autoPlayVideo: boolean
   webTorrentEnabled: boolean
   videosHistoryEnabled: boolean
+  videoLanguages: string[]
 
   role: UserRole
   roleLabel: string
 
   videoQuota: number
   videoQuotaDaily: number
-  createdAt: Date
+  videoQuotaUsed?: number
+  videoQuotaUsedDaily?: number
 
   theme: string
 
@@ -35,5 +38,8 @@ export interface User {
   blocked: boolean
   blockedReason?: string
 
-  videoQuotaUsed?: number
+  noInstanceConfigWarningModal: boolean
+  noWelcomeModal: boolean
+
+  createdAt: Date
 }
index 30a250c70be0805d24399d4d99462b24deced89d..b0473c7e708964ba488aae8fb9e49dd277e73e4d 100644 (file)
@@ -130,9 +130,6 @@ paths:
       summary: Get the account by name
       parameters:
         - $ref: '#/components/parameters/name'
-        - $ref: '#/components/parameters/start'
-        - $ref: '#/components/parameters/count'
-        - $ref: '#/components/parameters/sort'
       responses:
         '200':
           description: successful operation
@@ -204,6 +201,10 @@ paths:
       tags:
         - Accounts
       summary: Get all accounts
+      parameters:
+        - $ref: '#/components/parameters/start'
+        - $ref: '#/components/parameters/count'
+        - $ref: '#/components/parameters/sort'
       responses:
         '200':
           description: successful operation
@@ -233,6 +234,10 @@ paths:
       responses:
         '200':
           description: successful operation
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ServerConfigAbout'
   /config/custom:
     get:
       summary: Get the runtime configuration of the server
@@ -244,6 +249,10 @@ paths:
       responses:
         '200':
           description: successful operation
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ServerConfigCustom'
     put:
       summary: Set the runtime configuration of the server
       tags:
@@ -726,8 +735,7 @@ paths:
                   type: string
                   format: binary
             encoding:
-              profileImage:
-                # only accept png/jpeg
+              avatarfile:
                 contentType: image/png, image/jpeg
   /videos:
     get:
@@ -829,9 +837,11 @@ paths:
                 thumbnailfile:
                   description: Video thumbnail file
                   type: string
+                  format: binary
                 previewfile:
                   description: Video preview file
                   type: string
+                  format: binary
                 category:
                   description: Video category
                   type: string
@@ -874,6 +884,11 @@ paths:
                   format: date-time
                 scheduleUpdate:
                   $ref: '#/components/schemas/VideoScheduledUpdate'
+            encoding:
+              thumbnailfile:
+                contentType: image/jpeg
+              previewfile:
+                contentType: image/jpeg
     get:
       summary: Get a video by its id
       tags:
@@ -1029,9 +1044,11 @@ paths:
                 thumbnailfile:
                   description: Video thumbnail file
                   type: string
+                  format: binary
                 previewfile:
                   description: Video preview file
                   type: string
+                  format: binary
                 privacy:
                   $ref: '#/components/schemas/VideoPrivacySet'
                 category:
@@ -1080,6 +1097,13 @@ paths:
                 - videofile
                 - channelId
                 - name
+            encoding:
+              videofile:
+                contentType: video/mp4, video/webm, video/ogg, video/avi, video/quicktime, video/x-msvideo, video/x-flv, video/x-matroska, application/octet-stream
+              thumbnailfile:
+                contentType: image/jpeg
+              previewfile:
+                contentType: image/jpeg
       x-code-samples:
         - lang: Shell
           source: |
@@ -1142,9 +1166,11 @@ paths:
                 thumbnailfile:
                   description: Video thumbnail file
                   type: string
+                  format: binary
                 previewfile:
                   description: Video preview file
                   type: string
+                  format: binary
                 privacy:
                   $ref: '#/components/schemas/VideoPrivacySet'
                 category:
@@ -1188,6 +1214,13 @@ paths:
               required:
                 - channelId
                 - name
+            encoding:
+              torrentfile:
+                contentType: application/x-bittorrent
+              thumbnailfile:
+                contentType: image/jpeg
+              previewfile:
+                contentType: image/jpeg
   /videos/abuse:
     get:
       summary: Get list of reported video abuses
@@ -1308,6 +1341,9 @@ paths:
                   description: The file to upload.
                   type: string
                   format: binary
+            encoding:
+              captionfile:
+                contentType: text/vtt, application/x-subrip
       responses:
         '204':
           $ref: '#/paths/~1users~1me/put/responses/204'
@@ -1952,7 +1988,7 @@ components:
           description: 'Video file size in bytes'
         torrentUrl:
           type: string
-        torrentDownaloadUrl:
+        torrentDownloadUrl:
           type: string
         fileUrl:
           type: string
@@ -2227,8 +2263,6 @@ components:
       properties:
         id:
           type: number
-        uuid:
-          type: string
         url:
           type: string
         name:
@@ -2249,8 +2283,12 @@ components:
       allOf:
         - $ref: '#/components/schemas/Actor'
         - properties:
+            userId:
+              type: string
             displayName:
               type: string
+            description:
+              type: string
     User:
       properties:
         id:
@@ -2294,18 +2332,102 @@ components:
           type: number
     ServerConfig:
       properties:
+        instance:
+          type: object
+          properties:
+            name:
+              type: string
+            shortDescription:
+              type: string
+            defaultClientRoute:
+              type: string
+            isNSFW:
+              type: boolean
+            defaultNSFWPolicy:
+              type: string
+            customizations:
+              type: object
+              properties:
+                javascript:
+                  type: string
+                css:
+                  type: string
+        plugin:
+          type: object
+          properties:
+            registered:
+              type: array
+              items:
+                type: string
+        theme:
+          type: object
+          properties:
+            registered:
+              type: array
+              items:
+                type: string
+        email:
+          type: object
+          properties:
+            enabled:
+              type: boolean
+        contactForm:
+          type: object
+          properties:
+            enabled:
+              type: boolean
+        serverVersion:
+          type: string
+        serverCommit:
+          type: string
         signup:
           type: object
           properties:
             allowed:
               type: boolean
+            allowedForCurrentIP:
+              type: boolean
+            requiresEmailVerification:
+              type: boolean
         transcoding:
           type: object
           properties:
+            hls:
+              type: object
+              properties:
+                enabled:
+                  type: boolean
             enabledResolutions:
               type: array
               items:
                 type: number
+        import:
+          type: object
+          properties:
+            videos:
+              type: object
+              properties:
+                http:
+                  type: object
+                  properties:
+                    enabled:
+                      type: boolean
+                torrent:
+                  type: object
+                  properties:
+                    enabled:
+                      type: boolean
+        autoBlacklist:
+          type: object
+          properties:
+            videos:
+              type: object
+              properties:
+                ofUsers:
+                  type: object
+                  properties:
+                    enabled:
+                      type: boolean
         avatar:
           type: object
           properties:
@@ -2324,6 +2446,18 @@ components:
         video:
           type: object
           properties:
+            image:
+              type: object
+              properties:
+                extensions:
+                  type: array
+                  items:
+                    type: string
+                size:
+                  type: object
+                  properties:
+                    max:
+                      type: number
             file:
               type: object
               properties:
@@ -2331,6 +2465,202 @@ components:
                   type: array
                   items:
                     type: string
+        videoCaption:
+          type: object
+          properties:
+            file:
+              type: object
+              properties:
+                size:
+                  type: object
+                  properties:
+                    max:
+                      type: number
+                extensions:
+                  type: array
+                  items:
+                    type: string
+        user:
+          type: object
+          properties:
+            videoQuota:
+              type: number
+            videoQuotaDaily:
+              type: number
+        trending:
+          type: object
+          properties:
+            videos:
+              type: object
+              properties:
+                intervalDays:
+                  type: number
+        tracker:
+          ype: object
+          properties:
+            enabled:
+              type: boolean
+    ServerConfigAbout:
+      properties:
+        instance:
+          type: object
+          properties:
+            name:
+              type: string
+            shortDescription:
+              type: string
+            description:
+              type: string
+            terms:
+              type: string
+    ServerConfigCustom:
+      properties:
+        instance:
+          type: object
+          properties:
+            name:
+              type: string
+            shortDescription:
+              type: string
+            description:
+              type: string
+            terms:
+              type: string
+            defaultClientRoute:
+              type: string
+            isNSFW:
+              type: boolean
+            defaultNSFWPolicy:
+              type: string
+            customizations:
+              type: object
+              properties:
+                javascript:
+                  type: string
+                css:
+                  type: string
+        theme:
+          type: object
+          properties:
+            default:
+              type: string
+        services:
+          type: object
+          properties:
+            twitter:
+              type: object
+              properties:
+                username:
+                  type: string
+                whitelisted:
+                  type: boolean
+        cache:
+          type: object
+          properties:
+            previews:
+              type: object
+              properties:
+                size:
+                  type: number
+            captions:
+              type: object
+              properties:
+                size:
+                  type: number
+        signup:
+          type: object
+          properties:
+            enabled:
+              type: boolean
+            limit:
+              type: number
+            requiresEmailVerification:
+              type: boolean
+        admin:
+          type: object
+          properties:
+            email:
+              type: string
+        contactForm:
+          type: object
+          properties:
+            enabled:
+              type: boolean
+        user:
+          type: object
+          properties:
+            videoQuota:
+              type: number
+            videoQuotaDaily:
+              type: number
+        transcoding:
+          type: object
+          properties:
+            enabled:
+              type: boolean
+            allowAdditionalExtensions:
+              type: boolean
+            allowAudioFiles:
+              type: boolean
+            threads:
+              type: number
+            resolutions:
+              type: object
+              properties:
+                240p:
+                  type: boolean
+                360p:
+                  type: boolean
+                480p:
+                  type: boolean
+                720p:
+                  type: boolean
+                1080p:
+                  type: boolean
+                2160p:
+                  type: boolean
+            hls:
+              type: object
+              properties:
+                enabled:
+                  type: boolean
+        import:
+          type: object
+          properties:
+            videos:
+              type: object
+              properties:
+                http:
+                  type: object
+                  properties:
+                    enabled:
+                      type: boolean
+                torrent:
+                  type: object
+                  properties:
+                    enabled:
+                      type: boolean
+        autoBlacklist:
+          type: object
+          properties:
+            videos:
+              type: object
+              properties:
+                ofUsers:
+                  type: object
+                  properties:
+                    enabled:
+                      type: boolean
+        followers:
+          type: object
+          properties:
+            instance:
+              type: object
+              properties:
+                enabled:
+                  type: boolean
+                manualApproval:
+                  type: boolean
     Follow:
       properties:
         id:
index cf427ec845d6ceb23b695745c9604bf7d1de57d9..dd2a03db7363e8cbc92c4184c1f865a804194b1f 100644 (file)
@@ -11,6 +11,7 @@
     - [peertube-import-videos.js](#peertube-import-videosjs)
     - [peertube-upload.js](#peertube-uploadjs)
     - [peertube-watch.js](#peertube-watchjs)
+    - [peertube-plugins.js](#peertube-pluginsjs)
 - [Server tools](#server-tools)
   - [parse-log](#parse-log)
   - [create-transcoding-job.js](#create-transcoding-jobjs)
@@ -19,6 +20,7 @@
   - [optimize-old-videos.js](#optimize-old-videosjs)
   - [update-host.js](#update-hostjs)
   - [reset-password.js](#reset-passwordjs)
+  - [plugin install/uninstall](#plugin-installuninstall)
   - [REPL (Read Eval Print Loop)](#repl-read-eval-print-loop)
     - [.help](#help)
     - [Lodash example](#lodash-example)
@@ -182,6 +184,22 @@ It provides support for different players:
 - chromecast
 
 
+#### peertube-plugins.js
+
+Install/update/uninstall or list local or NPM PeerTube plugins:
+
+```
+$ cd ${CLONE}
+$ node dist/server/tools/peertube-plugins.js --help
+$ node dist/server/tools/peertube-plugins.js list --help
+$ node dist/server/tools/peertube-plugins.js install --help
+$ node dist/server/tools/peertube-plugins.js update --help
+$ node dist/server/tools/peertube-plugins.js uninstall --help
+
+$ node dist/server/tools/peertube-plugins.js install --path /my/plugin/path
+$ node dist/server/tools/peertube-plugins.js install --npm-name peertube-theme-example
+```
+
 ## Server tools
 
 These 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
 The difference with `peertube plugins` CLI is that these scripts can be used even if PeerTube is not running.
 If 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).
 
-To install a plugin or a theme from the disk:
+To install/update a plugin or a theme from the disk:
 
 ```
-$ sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production npm run npm run plugin:install -- --plugin-path /local/plugin/path
+$ sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production npm run plugin:install -- --plugin-path /local/plugin/path
 ```
 
 From NPM:
 
 ```
-$ sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production npm run npm run plugin:install -- --npm-name peertube-plugin-myplugin
+$ sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production npm run plugin:install -- --npm-name peertube-plugin-myplugin
 ```
 
 To uninstall a plugin or a theme:
 
 ```
-$ sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production npm run npm run plugin:uninstall -- --npm-name peertube-plugin-myplugin
+$ sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production npm run plugin:uninstall -- --npm-name peertube-plugin-myplugin
 ```
 
 ### REPL ([Read Eval Print Loop](https://nodejs.org/docs/latest-v10.x/api/repl.html))
index 4d2bdd6baf4f2df9168ef9888b689fdea362af8b..f2985f82b1c3b408f10d18ec02068d251c2478d3 100644 (file)
       "es2016",
       "es2017"
     ],
-    "typeRoots": [ "node_modules/@types", "server/typings" ]
+    "typeRoots": [ "node_modules/@types", "server/typings" ],
+    "baseUrl": "./",
+    "paths": {
+      "@server/*": [ "server/*" ],
+      "@shared/*": [ "shared/*" ]
+    }
   },
   "exclude": [
     "server/tools/",
-    "client/node_modules",
     "node_modules",
     "dist",
     "storage",
index f26763845b9c023c3a1032aa8dd1a952fb8a66c9..bab3aa1626c2d4a0e3e0057a710f0bce86aaf97a 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
@@ -828,24 +828,6 @@ bindings@~1.3.0:
   resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.3.1.tgz#21fc7c6d67c18516ec5aaa2815b145ff77b26ea5"
   integrity sha512-i47mqjF9UbjxJhxGf+pZ6kSxrnI3wBLlnGI2ArWJ4r0VrvDS7ZYXkprq/pLaBWYq4GM0r4zdHY+NNRqEMU7uew==
 
-bitcore-lib@^0.13.7:
-  version "0.13.19"
-  resolved "https://registry.yarnpkg.com/bitcore-lib/-/bitcore-lib-0.13.19.tgz#48af1e9bda10067c1ab16263472b5add2000f3dc"
-  integrity sha1-SK8em9oQBnwasWJjRyta3SAA89w=
-  dependencies:
-    bn.js "=2.0.4"
-    bs58 "=2.0.0"
-    buffer-compare "=1.0.0"
-    elliptic "=3.0.3"
-    inherits "=2.0.1"
-    lodash "=3.10.1"
-
-"bitcore-message@github:CoMakery/bitcore-message#dist":
-  version "1.0.2"
-  resolved "https://codeload.github.com/CoMakery/bitcore-message/tar.gz/8799cc327029c3d34fc725f05b2cf981363f6ebf"
-  dependencies:
-    bitcore-lib "^0.13.7"
-
 bitfield@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/bitfield/-/bitfield-2.0.0.tgz#fbe6767592fe5b4c87ecf1d04126294cc1bfa837"
@@ -968,16 +950,6 @@ bluebird@^3.0.5, bluebird@^3.5.0:
   resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.5.tgz#a8d0afd73251effbbd5fe384a77d73003c17a71f"
   integrity sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==
 
-bn.js@=2.0.4:
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-2.0.4.tgz#220a7cd677f7f1bfa93627ff4193776fe7819480"
-  integrity sha1-Igp81nf38b+pNif/QZN3b+eBlIA=
-
-bn.js@^2.0.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-2.2.0.tgz#12162bc2ae71fc40a5626c33438f3a875cd37625"
-  integrity sha1-EhYrwq5x/EClYmwzQ486h1zTdiU=
-
 bn.js@^4.4.0:
   version "4.11.8"
   resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
@@ -1043,11 +1015,6 @@ braces@^3.0.1:
   dependencies:
     fill-range "^7.0.1"
 
-brorand@^1.0.1:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
-  integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
-
 browser-stdout@1.3.1:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60"
@@ -1058,11 +1025,6 @@ browserify-package-json@^1.0.0:
   resolved "https://registry.yarnpkg.com/browserify-package-json/-/browserify-package-json-1.0.1.tgz#98dde8aa5c561fd6d3fe49bbaa102b74b396fdea"
   integrity sha1-mN3oqlxWH9bT/km7qhArdLOW/eo=
 
-bs58@=2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/bs58/-/bs58-2.0.0.tgz#72b713bed223a0ac518bbda0e3ce3f4817f39eb5"
-  integrity sha1-crcTvtIjoKxRi72g484/SBfznrU=
-
 buffer-alloc-unsafe@^1.1.0:
   version "1.1.0"
   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:
     buffer-alloc-unsafe "^1.1.0"
     buffer-fill "^1.0.0"
 
-buffer-compare@=1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/buffer-compare/-/buffer-compare-1.0.0.tgz#acaa7a966e98eee9fae14b31c39a5f158fb3c4a2"
-  integrity sha1-rKp6lm6Y7un64Usxw5pfFY+zxKI=
-
-buffer-equal-constant-time@1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
-  integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=
-
 buffer-equals@^1.0.3, buffer-equals@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/buffer-equals/-/buffer-equals-1.0.4.tgz#0353b54fd07fd9564170671ae6f66b9cf10d27f5"
@@ -2096,13 +2048,6 @@ ecc-jsbn@~0.1.1:
     jsbn "~0.1.0"
     safer-buffer "^2.1.0"
 
-ecdsa-sig-formatter@1.0.11:
-  version "1.0.11"
-  resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf"
-  integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==
-  dependencies:
-    safe-buffer "^5.0.1"
-
 ee-first@1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@@ -2113,16 +2058,6 @@ elegant-spinner@^1.0.1:
   resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e"
   integrity sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4=
 
-elliptic@=3.0.3:
-  version "3.0.3"
-  resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-3.0.3.tgz#865c9b420bfbe55006b9f969f97a0d2c44966595"
-  integrity sha1-hlybQgv75VAGuflp+XoNLESWZZU=
-  dependencies:
-    bn.js "^2.0.0"
-    brorand "^1.0.1"
-    hash.js "^1.0.0"
-    inherits "^2.0.1"
-
 emoji-regex@^7.0.1:
   version "7.0.3"
   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:
   dependencies:
     function-bind "^1.1.1"
 
-hash.js@^1.0.0:
-  version "1.1.7"
-  resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42"
-  integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==
-  dependencies:
-    inherits "^2.0.3"
-    minimalistic-assert "^1.0.1"
-
 hashish@~0.0.4:
   version "0.0.4"
   resolved "https://registry.yarnpkg.com/hashish/-/hashish-0.0.4.tgz#6d60bc6ffaf711b6afd60e426d077988014e6554"
@@ -3481,11 +3408,6 @@ inherits@2.0.3:
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
   integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
 
-inherits@=2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
-  integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=
-
 ini@^1.3.4, ini@~1.3.0:
   version "1.3.5"
   resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
@@ -4028,25 +3950,6 @@ jsonify@~0.0.0:
   resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
   integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=
 
-"jsonld-signatures@https://github.com/Chocobozzz/jsonld-signatures#rsa2017":
-  version "1.2.2-2"
-  resolved "https://github.com/Chocobozzz/jsonld-signatures#77660963e722eb4541d2d255f9d9d4216329665f"
-  dependencies:
-    bitcore-message "github:CoMakery/bitcore-message#dist"
-    jsonld "^0.5.12"
-    jws "^3.1.4"
-    node-forge "^0.7.1"
-
-jsonld@^0.5.12:
-  version "0.5.21"
-  resolved "https://registry.yarnpkg.com/jsonld/-/jsonld-0.5.21.tgz#4d5b78d717eb92bcd1ac9d88e34efad95370c0bf"
-  integrity sha512-1dQhaw1Eb3p7Cz5ECE2DNPwLvTmK+f6D45hACBdonJaFKP1bN9zlKLZWbPZQeZtduAc/LNv10J4ML0IiTBVahw==
-  dependencies:
-    rdf-canonize "^0.2.1"
-    request "^2.83.0"
-    semver "^5.5.0"
-    xmldom "0.1.19"
-
 jsonld@~1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/jsonld/-/jsonld-1.1.0.tgz#afcb168c44557a7bddead4d4513c3cbcae3bc5b9"
@@ -4082,23 +3985,6 @@ junk@^3.1.0:
   resolved "https://registry.yarnpkg.com/junk/-/junk-3.1.0.tgz#31499098d902b7e98c5d9b9c80f43457a88abfa1"
   integrity sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==
 
-jwa@^1.4.1:
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a"
-  integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==
-  dependencies:
-    buffer-equal-constant-time "1.0.1"
-    ecdsa-sig-formatter "1.0.11"
-    safe-buffer "^5.0.1"
-
-jws@^3.1.4:
-  version "3.2.2"
-  resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
-  integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==
-  dependencies:
-    jwa "^1.4.1"
-    safe-buffer "^5.0.1"
-
 k-bucket@^4.0.0:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/k-bucket/-/k-bucket-4.0.1.tgz#3fc2e5693f0b7bff90d7b6b476edd6087955d542"
@@ -4335,11 +4221,6 @@ lodash@4.17.4:
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
   integrity sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=
 
-lodash@=3.10.1:
-  version "3.10.1"
-  resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
-  integrity sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=
-
 lodash@^4.0.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.3.0, lodash@~4.17.10:
   version "4.17.15"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
@@ -4638,11 +4519,6 @@ mimic-response@^1.0.0:
   resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
   integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==
 
-minimalistic-assert@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
-  integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
-
 minimatch@3.0.4, minimatch@^3.0.4, minimatch@~3.0.2:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
@@ -4734,6 +4610,11 @@ mocha@^6.0.0:
     yargs-parser "13.0.0"
     yargs-unparser "1.5.0"
 
+module-alias@^2.2.1:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/module-alias/-/module-alias-2.2.1.tgz#553aea9dc7f99cd45fd75e34a574960dc46550da"
+  integrity sha512-LTez0Eo+YtfUhgzhu/LqxkUzOpD+k5C0wXBLun0L1qE2BhHf6l09dqam8e7BnoMYA6mAlP0vSsGFQ8QHhGN/aQ==
+
 moment-timezone@^0.5.21, moment-timezone@^0.5.25:
   version "0.5.26"
   resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.26.tgz#c0267ca09ae84631aa3dc33f65bedbe6e8e0d772"