aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.github/FUNDING.yml2
-rw-r--r--.github/workflows/stats.yml1
-rw-r--r--.github/workflows/test.yml1
-rw-r--r--.gitignore2
-rw-r--r--client/src/app/+about/about-follows/about-follows.component.html2
-rw-r--r--client/src/app/+about/about-instance/about-instance.component.html2
-rw-r--r--client/src/app/+accounts/account-about/account-about.component.html15
-rw-r--r--client/src/app/+accounts/account-about/account-about.component.scss12
-rw-r--r--client/src/app/+accounts/account-about/account-about.component.ts40
-rw-r--r--client/src/app/+accounts/account-search/account-search.component.ts7
-rw-r--r--client/src/app/+accounts/account-video-channels/account-video-channels.component.html43
-rw-r--r--client/src/app/+accounts/account-video-channels/account-video-channels.component.scss172
-rw-r--r--client/src/app/+accounts/account-video-channels/account-video-channels.component.ts47
-rw-r--r--client/src/app/+accounts/account-videos/account-videos.component.ts10
-rw-r--r--client/src/app/+accounts/accounts-routing.module.ts16
-rw-r--r--client/src/app/+accounts/accounts.component.html116
-rw-r--r--client/src/app/+accounts/accounts.component.scss184
-rw-r--r--client/src/app/+accounts/accounts.component.ts121
-rw-r--r--client/src/app/+accounts/accounts.module.ts4
-rw-r--r--client/src/app/+admin/admin.module.ts2
-rw-r--r--client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts3
-rw-r--r--client/src/app/+admin/plugins/plugin-search/plugin-search.component.html6
-rw-r--r--client/src/app/+admin/plugins/shared/plugin-list.component.scss2
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.component.html2
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.component.scss8
-rw-r--r--client/src/app/+login/login.component.html2
-rw-r--r--client/src/app/+login/login.component.ts7
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts8
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-settings.component.html2
-rw-r--r--client/src/app/+my-account/my-account.module.ts6
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts70
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html21
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss7
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts7
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts48
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channels.component.scss3
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts4
-rw-r--r--client/src/app/+my-library/my-history/my-history.component.html4
-rw-r--r--client/src/app/+my-library/my-history/my-history.component.scss45
-rw-r--r--client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html2
-rw-r--r--client/src/app/+my-library/my-subscriptions/my-subscriptions.component.scss88
-rw-r--r--client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.html6
-rw-r--r--client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.scss46
-rw-r--r--client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html14
-rw-r--r--client/src/app/+my-library/my-video-playlists/my-video-playlists.component.scss69
-rw-r--r--client/src/app/+my-library/my-videos/my-videos.component.html12
-rw-r--r--client/src/app/+my-library/my-videos/my-videos.component.scss94
-rw-r--r--client/src/app/+my-library/my-videos/my-videos.component.ts14
-rw-r--r--client/src/app/+search/search.component.html14
-rw-r--r--client/src/app/+search/search.component.scss234
-rw-r--r--client/src/app/+video-channels/video-channel-about/video-channel-about.component.html22
-rw-r--r--client/src/app/+video-channels/video-channel-about/video-channel-about.component.scss12
-rw-r--r--client/src/app/+video-channels/video-channel-about/video-channel-about.component.ts43
-rw-r--r--client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.html10
-rw-r--r--client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.scss28
-rw-r--r--client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts9
-rw-r--r--client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts25
-rw-r--r--client/src/app/+video-channels/video-channels-routing.module.ts10
-rw-r--r--client/src/app/+video-channels/video-channels.component.html147
-rw-r--r--client/src/app/+video-channels/video-channels.component.scss327
-rw-r--r--client/src/app/+video-channels/video-channels.component.ts62
-rw-r--r--client/src/app/+video-channels/video-channels.module.ts6
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts13
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.scss4
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts13
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts15
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts15
-rw-r--r--client/src/app/+videos/+video-edit/video-add.component.scss4
-rw-r--r--client/src/app/+videos/+video-watch/comment/video-comments.component.html15
-rw-r--r--client/src/app/+videos/+video-watch/comment/video-comments.component.ts1
-rw-r--r--client/src/app/+videos/+video-watch/modal/video-support.component.ts31
-rw-r--r--client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.html4
-rw-r--r--client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.scss33
-rw-r--r--client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.ts2
-rw-r--r--client/src/app/+videos/+video-watch/video-avatar-channel.component.html (renamed from client/src/app/shared/shared-main/account/video-avatar-channel.component.html)5
-rw-r--r--client/src/app/+videos/+video-watch/video-avatar-channel.component.scss (renamed from client/src/app/shared/shared-main/account/video-avatar-channel.component.scss)8
-rw-r--r--client/src/app/+videos/+video-watch/video-avatar-channel.component.ts (renamed from client/src/app/shared/shared-main/account/video-avatar-channel.component.ts)2
-rw-r--r--client/src/app/+videos/+video-watch/video-watch.component.html5
-rw-r--r--client/src/app/+videos/+video-watch/video-watch.component.scss59
-rw-r--r--client/src/app/+videos/+video-watch/video-watch.component.ts62
-rw-r--r--client/src/app/+videos/+video-watch/video-watch.module.ts9
-rw-r--r--client/src/app/+videos/video-list/overview/video-overview.component.html6
-rw-r--r--client/src/app/+videos/video-list/overview/video-overview.component.scss79
-rw-r--r--client/src/app/+videos/video-list/video-user-subscriptions.component.ts3
-rw-r--r--client/src/app/app.component.scss9
-rw-r--r--client/src/app/core/notification/peertube-socket.service.ts7
-rw-r--r--client/src/app/core/plugins/hooks.service.ts18
-rw-r--r--client/src/app/core/plugins/plugin.service.ts6
-rw-r--r--client/src/app/core/server/server.service.ts6
-rw-r--r--client/src/app/core/users/user.model.ts4
-rw-r--r--client/src/app/core/users/user.service.ts5
-rw-r--r--client/src/app/core/wrappers/screen.service.ts9
-rw-r--r--client/src/app/header/search-typeahead.component.html4
-rw-r--r--client/src/app/header/search-typeahead.component.scss2
-rw-r--r--client/src/app/menu/menu.component.scss325
-rw-r--r--client/src/app/menu/menu.component.ts5
-rw-r--r--client/src/app/menu/notification.component.scss3
-rw-r--r--client/src/app/modal/instance-config-warning-modal.component.html2
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts3
-rw-r--r--client/src/app/shared/shared-actor-image/actor-avatar-edit.component.html41
-rw-r--r--client/src/app/shared/shared-actor-image/actor-avatar-edit.component.scss54
-rw-r--r--client/src/app/shared/shared-actor-image/actor-avatar-edit.component.ts (renamed from client/src/app/shared/shared-main/account/actor-avatar-info.component.ts)42
-rw-r--r--client/src/app/shared/shared-actor-image/actor-banner-edit.component.html34
-rw-r--r--client/src/app/shared/shared-actor-image/actor-banner-edit.component.scss27
-rw-r--r--client/src/app/shared/shared-actor-image/actor-banner-edit.component.ts76
-rw-r--r--client/src/app/shared/shared-actor-image/actor-image-edit.scss35
-rw-r--r--client/src/app/shared/shared-actor-image/index.ts1
-rw-r--r--client/src/app/shared/shared-actor-image/shared-actor-image.module.ts29
-rw-r--r--client/src/app/shared/shared-forms/input-toggle-hidden.component.html5
-rw-r--r--client/src/app/shared/shared-forms/markdown-textarea.component.scss2
-rw-r--r--client/src/app/shared/shared-forms/select/select-options.component.ts9
-rw-r--r--client/src/app/shared/shared-instance/instance-about-accordion.component.scss2
-rw-r--r--client/src/app/shared/shared-instance/instance-features-table.component.html2
-rw-r--r--client/src/app/shared/shared-instance/instance-features-table.component.ts10
-rw-r--r--client/src/app/shared/shared-main/account/account.model.ts4
-rw-r--r--client/src/app/shared/shared-main/account/actor-avatar-info.component.html43
-rw-r--r--client/src/app/shared/shared-main/account/actor-avatar-info.component.scss86
-rw-r--r--client/src/app/shared/shared-main/account/actor.model.ts19
-rw-r--r--client/src/app/shared/shared-main/account/index.ts2
-rw-r--r--client/src/app/shared/shared-main/angular/autofocus.directive.ts12
-rw-r--r--client/src/app/shared/shared-main/angular/index.ts1
-rw-r--r--client/src/app/shared/shared-main/auth/auth-interceptor.service.ts4
-rw-r--r--client/src/app/shared/shared-main/misc/simple-search-input.component.html13
-rw-r--r--client/src/app/shared/shared-main/misc/simple-search-input.component.scss36
-rw-r--r--client/src/app/shared/shared-main/misc/simple-search-input.component.ts52
-rw-r--r--client/src/app/shared/shared-main/peertube-modal/index.ts1
-rw-r--r--client/src/app/shared/shared-main/peertube-modal/peertube-modal.service.ts7
-rw-r--r--client/src/app/shared/shared-main/shared-main.module.ts17
-rw-r--r--client/src/app/shared/shared-main/users/user-notification.model.ts33
-rw-r--r--client/src/app/shared/shared-main/users/user-notifications.component.html48
-rw-r--r--client/src/app/shared/shared-main/users/user-notifications.component.ts9
-rw-r--r--client/src/app/shared/shared-main/video-channel/video-channel.model.ts53
-rw-r--r--client/src/app/shared/shared-main/video-channel/video-channel.service.ts12
-rw-r--r--client/src/app/shared/shared-main/video/video.model.ts6
-rw-r--r--client/src/app/shared/shared-moderation/moderation.scss2
-rw-r--r--client/src/app/shared/shared-moderation/report-modals/report.component.scss2
-rw-r--r--client/src/app/shared/shared-moderation/report-modals/video-report.component.ts3
-rw-r--r--client/src/app/shared/shared-share-modal/video-share.component.ts4
-rw-r--r--client/src/app/shared/shared-support-modal/index.ts3
-rw-r--r--client/src/app/shared/shared-support-modal/shared-support-modal.module.ts24
-rw-r--r--client/src/app/shared/shared-support-modal/support-modal.component.html (renamed from client/src/app/+videos/+video-watch/modal/video-support.component.html)4
-rw-r--r--client/src/app/shared/shared-support-modal/support-modal.component.scss (renamed from client/src/app/+videos/+video-watch/modal/video-support.component.scss)0
-rw-r--r--client/src/app/shared/shared-support-modal/support-modal.component.ts40
-rw-r--r--client/src/app/shared/shared-video-miniature/abstract-video-list.html5
-rw-r--r--client/src/app/shared/shared-video-miniature/abstract-video-list.scss11
-rw-r--r--client/src/app/shared/shared-video-miniature/abstract-video-list.ts8
-rw-r--r--client/src/app/shared/shared-video-miniature/video-download.component.html149
-rw-r--r--client/src/app/shared/shared-video-miniature/video-download.component.scss37
-rw-r--r--client/src/app/shared/shared-video-miniature/video-download.component.ts38
-rw-r--r--client/src/app/shared/shared-video-miniature/video-miniature.component.html8
-rw-r--r--client/src/app/shared/shared-video-miniature/video-miniature.component.scss305
-rw-r--r--client/src/app/shared/shared-video-miniature/video-miniature.component.ts35
-rw-r--r--client/src/app/shared/shared-video-miniature/videos-selection.component.html3
-rw-r--r--client/src/app/shared/shared-video-miniature/videos-selection.component.scss42
-rw-r--r--client/src/app/shared/shared-video-miniature/videos-selection.component.ts3
-rw-r--r--client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.html2
-rw-r--r--client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.scss119
-rw-r--r--client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts1
-rw-r--r--client/src/assets/images/feather/cloud-download.svg2
-rw-r--r--client/src/assets/images/feather/subscriptions.svg19
-rw-r--r--client/src/assets/images/misc/language.svg8
-rw-r--r--client/src/assets/images/misc/npm.svg2
-rw-r--r--client/src/assets/images/misc/peertube-x.svg25
-rw-r--r--client/src/assets/images/misc/playlist-add.svg4
-rw-r--r--client/src/assets/images/misc/support.svg4
-rw-r--r--client/src/assets/images/misc/video-lang.svg2
-rw-r--r--client/src/assets/player/peertube-player-local-storage.ts50
-rw-r--r--client/src/assets/player/peertube-player-manager.ts16
-rw-r--r--client/src/assets/player/peertube-plugin.ts13
-rw-r--r--client/src/assets/player/peertube-videojs-typings.ts2
-rw-r--r--client/src/assets/player/utils.ts5
-rw-r--r--client/src/sass/application.scss183
-rw-r--r--client/src/sass/bootstrap.scss4
-rw-r--r--client/src/sass/classes.scss22
-rw-r--r--client/src/sass/include/_actor.scss92
-rw-r--r--client/src/sass/include/_miniature.scss172
-rw-r--r--client/src/sass/include/_mixins.scss223
-rw-r--r--client/src/sass/include/_variables.scss44
-rw-r--r--client/src/sass/ng-select.scss14
-rw-r--r--client/src/sass/player/context-menu.scss2
-rw-r--r--client/src/sass/player/peertube-skin.scss18
-rw-r--r--client/src/sass/primeng-custom.scss57
-rw-r--r--client/src/standalone/videos/embed.ts6
-rw-r--r--client/src/types/register-client-option.model.ts3
-rw-r--r--client/yarn.lock1022
-rw-r--r--config/default.yaml11
-rw-r--r--config/production.yaml.example10
-rw-r--r--config/test.yaml6
-rw-r--r--package.json13
-rw-r--r--scripts/benchmark.ts101
-rwxr-xr-xscripts/parse-log.ts23
-rwxr-xr-xscripts/prune-storage.ts10
-rw-r--r--scripts/regenerate-thumbnails.ts68
-rwxr-xr-xscripts/upgrade.sh3
-rw-r--r--server.ts11
-rw-r--r--server/controllers/api/config.ts12
-rw-r--r--server/controllers/api/jobs.ts4
-rw-r--r--server/controllers/api/plugins.ts1
-rw-r--r--server/controllers/api/search.ts43
-rw-r--r--server/controllers/api/users/index.ts8
-rw-r--r--server/controllers/api/users/me.ts10
-rw-r--r--server/controllers/api/users/my-notifications.ts4
-rw-r--r--server/controllers/api/users/my-subscriptions.ts12
-rw-r--r--server/controllers/api/users/token.ts72
-rw-r--r--server/controllers/api/video-channel.ts66
-rw-r--r--server/controllers/api/videos/index.ts11
-rw-r--r--server/controllers/api/videos/ownership.ts2
-rw-r--r--server/controllers/client.ts28
-rw-r--r--server/controllers/download.ts85
-rw-r--r--server/controllers/feeds.ts10
-rw-r--r--server/controllers/lazy-static.ts41
-rw-r--r--server/controllers/plugins.ts14
-rw-r--r--server/controllers/services.ts4
-rw-r--r--server/controllers/static.ts4
-rw-r--r--server/helpers/activitypub.ts5
-rw-r--r--server/helpers/core-utils.ts38
-rw-r--r--server/helpers/custom-validators/activitypub/activity.ts110
-rw-r--r--server/helpers/custom-validators/activitypub/flag.ts14
-rw-r--r--server/helpers/custom-validators/activitypub/rate.ts18
-rw-r--r--server/helpers/custom-validators/activitypub/share.ts11
-rw-r--r--server/helpers/custom-validators/activitypub/view.ts13
-rw-r--r--server/helpers/custom-validators/actor-images.ts17
-rw-r--r--server/helpers/custom-validators/user-notifications.ts5
-rw-r--r--server/helpers/custom-validators/users.ts17
-rw-r--r--server/helpers/ffmpeg-utils.ts21
-rw-r--r--server/helpers/image-utils.ts6
-rw-r--r--server/helpers/logger.ts10
-rw-r--r--server/helpers/middlewares/video-channels.ts7
-rw-r--r--server/helpers/middlewares/videos.ts23
-rw-r--r--server/helpers/peertube-crypto.ts2
-rw-r--r--server/helpers/requests.ts199
-rw-r--r--server/helpers/youtube-dl.ts71
-rw-r--r--server/initializers/checker-after-init.ts27
-rw-r--r--server/initializers/checker-before-init.ts6
-rw-r--r--server/initializers/config.ts8
-rw-r--r--server/initializers/constants.ts35
-rw-r--r--server/initializers/database.ts10
-rw-r--r--server/initializers/migrations/0610-views-index copy.ts (renamed from server/initializers/migrations/0610-views-index.ts)0
-rw-r--r--server/initializers/migrations/0615-latest-versions-notification-settings.ts44
-rw-r--r--server/initializers/migrations/0620-latest-versions-application.ts27
-rw-r--r--server/initializers/migrations/0625-latest-versions-notification.ts26
-rw-r--r--server/initializers/migrations/0630-banner.ts50
-rw-r--r--server/initializers/migrations/0635-actor-image-size.ts35
-rw-r--r--server/lib/activitypub/actor.ts212
-rw-r--r--server/lib/activitypub/crawl.ts25
-rw-r--r--server/lib/activitypub/playlist.ts69
-rw-r--r--server/lib/activitypub/process/process-delete.ts6
-rw-r--r--server/lib/activitypub/process/process-update.ts13
-rw-r--r--server/lib/activitypub/send/send-create.ts10
-rw-r--r--server/lib/activitypub/share.ts30
-rw-r--r--server/lib/activitypub/video-comments.ts22
-rw-r--r--server/lib/activitypub/video-rates.ts22
-rw-r--r--server/lib/activitypub/videos.ts81
-rw-r--r--server/lib/actor-image.ts97
-rw-r--r--server/lib/auth/external-auth.ts (renamed from server/lib/auth.ts)129
-rw-r--r--server/lib/auth/oauth-model.ts (renamed from server/lib/oauth-model.ts)137
-rw-r--r--server/lib/auth/oauth.ts180
-rw-r--r--server/lib/auth/tokens-cache.ts52
-rw-r--r--server/lib/avatar.ts85
-rw-r--r--server/lib/client-html.ts9
-rw-r--r--server/lib/emailer.ts81
-rw-r--r--server/lib/emails/peertube-version-new/html.pug9
-rw-r--r--server/lib/emails/plugin-version-new/html.pug9
-rw-r--r--server/lib/files-cache/videos-caption-cache.ts2
-rw-r--r--server/lib/files-cache/videos-preview-cache.ts2
-rw-r--r--server/lib/files-cache/videos-torrent-cache.ts15
-rw-r--r--server/lib/hls.ts4
-rw-r--r--server/lib/job-queue/handlers/activitypub-cleaner.ts63
-rw-r--r--server/lib/job-queue/handlers/activitypub-http-broadcast.ts5
-rw-r--r--server/lib/job-queue/handlers/activitypub-http-unicast.ts5
-rw-r--r--server/lib/job-queue/handlers/utils/activitypub-http-utils.ts15
-rw-r--r--server/lib/notifier.ts74
-rw-r--r--server/lib/plugins/plugin-index.ts26
-rw-r--r--server/lib/plugins/register-helpers.ts2
-rw-r--r--server/lib/schedulers/auto-follow-index-instances.ts8
-rw-r--r--server/lib/schedulers/peertube-version-check-scheduler.ts55
-rw-r--r--server/lib/schedulers/plugins-check-scheduler.ts6
-rw-r--r--server/lib/thumbnail.ts9
-rw-r--r--server/lib/user.ts4
-rw-r--r--server/lib/video-blacklist.ts6
-rw-r--r--server/lib/video-channel.ts16
-rw-r--r--server/middlewares/auth.ts (renamed from server/middlewares/oauth.ts)24
-rw-r--r--server/middlewares/index.ts2
-rw-r--r--server/middlewares/validators/activitypub/signature.ts2
-rw-r--r--server/middlewares/validators/actor-image.ts30
-rw-r--r--server/middlewares/validators/avatar.ts26
-rw-r--r--server/middlewares/validators/follows.ts1
-rw-r--r--server/middlewares/validators/index.ts1
-rw-r--r--server/middlewares/validators/jobs.ts6
-rw-r--r--server/middlewares/validators/pagination.ts33
-rw-r--r--server/middlewares/validators/sort.ts2
-rw-r--r--server/middlewares/validators/utils.ts4
-rw-r--r--server/middlewares/validators/videos/video-channels.ts2
-rw-r--r--server/middlewares/validators/videos/video-comments.ts2
-rw-r--r--server/middlewares/validators/videos/video-playlists.ts2
-rw-r--r--server/middlewares/validators/videos/videos.ts2
-rw-r--r--server/models/account/account.ts5
-rw-r--r--server/models/account/actor-image.ts100
-rw-r--r--server/models/account/user-notification-setting.ts26
-rw-r--r--server/models/account/user-notification.ts98
-rw-r--r--server/models/account/user.ts20
-rw-r--r--server/models/activitypub/actor-follow.ts7
-rw-r--r--server/models/activitypub/actor.ts87
-rw-r--r--server/models/application/application.ts4
-rw-r--r--server/models/avatar/avatar.ts81
-rw-r--r--server/models/oauth/oauth-token.ts11
-rw-r--r--server/models/redundancy/video-redundancy.ts9
-rw-r--r--server/models/utils.ts8
-rw-r--r--server/models/video/video-channel.ts102
-rw-r--r--server/models/video/video-query-builder.ts5
-rw-r--r--server/models/video/video.ts19
-rw-r--r--server/tests/api/activitypub/security.ts116
-rw-r--r--server/tests/api/check-params/user-notifications.ts4
-rw-r--r--server/tests/api/check-params/users.ts2
-rw-r--r--server/tests/api/check-params/video-channels.ts70
-rw-r--r--server/tests/api/notifications/admin-notifications.ts165
-rw-r--r--server/tests/api/notifications/index.ts1
-rw-r--r--server/tests/api/server/handle-down.ts2
-rw-r--r--server/tests/api/server/services.ts12
-rw-r--r--server/tests/api/users/users.ts51
-rw-r--r--server/tests/api/videos/video-channels.ts99
-rw-r--r--server/tests/cli/index.ts1
-rw-r--r--server/tests/cli/regenerate-thumbnails.ts124
-rw-r--r--server/tests/feeds/feeds.ts17
-rw-r--r--server/tests/fixtures/banner-resized.jpgbin0 -> 88780 bytes
-rw-r--r--server/tests/fixtures/banner.jpgbin0 -> 31648 bytes
-rw-r--r--server/tests/fixtures/peertube-plugin-test/main.js70
-rw-r--r--server/tests/fixtures/thumbnail-playlist.jpgbin2520 -> 4981 bytes
-rw-r--r--server/tests/fixtures/video_import_thumbnail.jpgbin5885 -> 10980 bytes
-rw-r--r--server/tests/fixtures/video_short.mp4.jpgbin2618 -> 4981 bytes
-rw-r--r--server/tests/fixtures/video_short.ogv.jpgbin2618 -> 4981 bytes
-rw-r--r--server/tests/fixtures/video_short.webm.jpgbin3598 -> 4981 bytes
-rw-r--r--server/tests/fixtures/video_short1.webm.jpgbin4616 -> 6309 bytes
-rw-r--r--server/tests/fixtures/video_short2.webm.jpgbin4221 -> 5506 bytes
-rw-r--r--server/tests/fixtures/video_short3.webm.jpgbin3972 -> 4981 bytes
-rw-r--r--server/tests/helpers/request.ts16
-rw-r--r--server/tests/plugins/external-auth.ts2
-rw-r--r--server/tests/plugins/filter-hooks.ts178
-rw-r--r--server/tools/peertube-import-videos.ts5
-rw-r--r--server/types/models/account/account.ts10
-rw-r--r--server/types/models/account/actor-follow.ts11
-rw-r--r--server/types/models/account/actor-image.ts12
-rw-r--r--server/types/models/account/actor.ts55
-rw-r--r--server/types/models/account/avatar.ts12
-rw-r--r--server/types/models/account/index.ts4
-rw-r--r--server/types/models/application/application.ts5
-rw-r--r--server/types/models/application/index.ts1
-rw-r--r--server/types/models/index.ts1
-rw-r--r--server/types/models/user/user-notification.ts18
-rw-r--r--server/types/models/user/user.ts8
-rw-r--r--server/types/models/video/video-channels.ts34
-rw-r--r--server/typings/express/index.d.ts23
-rw-r--r--shared/core-utils/renderer/html.ts18
-rw-r--r--shared/extra-utils/index.ts2
-rw-r--r--shared/extra-utils/miscs/sql.ts28
-rw-r--r--shared/extra-utils/mock-servers/joinpeertube-versions.ts31
-rw-r--r--shared/extra-utils/mock-servers/mock-instances-index.ts (renamed from shared/extra-utils/instances-index/mock-instances-index.ts)0
-rw-r--r--shared/extra-utils/requests/activitypub.ts9
-rw-r--r--shared/extra-utils/requests/requests.ts7
-rw-r--r--shared/extra-utils/server/servers.ts1
-rw-r--r--shared/extra-utils/users/user-notifications.ts72
-rw-r--r--shared/extra-utils/users/users.ts4
-rw-r--r--shared/extra-utils/videos/video-channels.ts29
-rw-r--r--shared/models/activitypub/activitypub-actor.ts3
-rw-r--r--shared/models/activitypub/objects/common-objects.ts2
-rw-r--r--shared/models/actors/account.model.ts4
-rw-r--r--shared/models/actors/actor-image.model.ts (renamed from shared/models/avatars/avatar.model.ts)2
-rw-r--r--shared/models/actors/actor-image.type.ts4
-rw-r--r--shared/models/actors/actor.model.ts4
-rw-r--r--shared/models/actors/index.ts2
-rw-r--r--shared/models/avatars/index.ts1
-rw-r--r--shared/models/index.ts2
-rw-r--r--shared/models/joinpeertube/index.ts1
-rw-r--r--shared/models/joinpeertube/versions.model.ts5
-rw-r--r--shared/models/plugins/client-hook.model.ts21
-rw-r--r--shared/models/plugins/server-hook.model.ts20
-rw-r--r--shared/models/server/emailer.model.ts53
-rw-r--r--shared/models/server/job.model.ts2
-rw-r--r--shared/models/server/server-config.model.ts9
-rw-r--r--shared/models/users/user-notification-setting.model.ts3
-rw-r--r--shared/models/users/user-notification.model.ts18
-rw-r--r--shared/models/videos/channel/video-channel.model.ts7
-rw-r--r--support/doc/api/openapi.yaml132
-rw-r--r--support/doc/dependencies.md21
-rw-r--r--support/doc/development/release.md2
-rw-r--r--support/doc/plugins/guide.md10
-rw-r--r--support/doc/tools.md17
-rw-r--r--support/docker/production/config/custom-environment-variables.yaml4
-rw-r--r--support/systemd/peertube.service2
-rw-r--r--yarn.lock644
390 files changed, 8207 insertions, 4809 deletions
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index b06143c8f..ac2e6ce92 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1 +1 @@
custom: ["https://joinpeertube.org/roadmap", "https://soutenir.framasoft.org/en/"] custom: ["https://soutenir.framasoft.org/en/"]
diff --git a/.github/workflows/stats.yml b/.github/workflows/stats.yml
index b5fb6d2a6..a2f0945b3 100644
--- a/.github/workflows/stats.yml
+++ b/.github/workflows/stats.yml
@@ -5,6 +5,7 @@ on:
5 branches: 5 branches:
6 - develop 6 - develop
7 - ci 7 - ci
8 - next
8 pull_request: 9 pull_request:
9 types: [synchronize, opened] 10 types: [synchronize, opened]
10 11
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index f8706d4be..442317ce2 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -6,6 +6,7 @@ on:
6 - develop 6 - develop
7 - master 7 - master
8 - ci 8 - ci
9 - next
9 pull_request: 10 pull_request:
10 types: [synchronize, opened] 11 types: [synchronize, opened]
11 schedule: 12 schedule:
diff --git a/.gitignore b/.gitignore
index 07b2fb7ef..98f337490 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,9 +30,11 @@ yarn-error.log
30# IDE 30# IDE
31/*.sublime-project 31/*.sublime-project
32/*.sublime-workspace 32/*.sublime-workspace
33/*.vscode
33/**/.idea 34/**/.idea
34/dist 35/dist
35/PeerTube.iml 36/PeerTube.iml
37*.swp
36 38
37# Zanata 39# Zanata
38/.zanata-cache 40/.zanata-cache
diff --git a/client/src/app/+about/about-follows/about-follows.component.html b/client/src/app/+about/about-follows/about-follows.component.html
index 2cf890acf..e9139b503 100644
--- a/client/src/app/+about/about-follows/about-follows.component.html
+++ b/client/src/app/+about/about-follows/about-follows.component.html
@@ -1,7 +1,7 @@
1<div class="row"> 1<div class="row">
2 <h1 class="sr-only" i18n>Follows</h1> 2 <h1 class="sr-only" i18n>Follows</h1>
3 <div class="col-xl-6 col-md-12"> 3 <div class="col-xl-6 col-md-12">
4 <h2 i18n class="subtitle">Followers instances ({{ followersPagination.totalItems }})</h2> 4 <h2 i18n class="subtitle">Follower instances ({{ followersPagination.totalItems }})</h2>
5 5
6 <div i18n class="no-results" *ngIf="followersPagination.totalItems === 0">This instance does not have instances followers.</div> 6 <div i18n class="no-results" *ngIf="followersPagination.totalItems === 0">This instance does not have instances followers.</div>
7 7
diff --git a/client/src/app/+about/about-instance/about-instance.component.html b/client/src/app/+about/about-instance/about-instance.component.html
index d8794d602..1f372090e 100644
--- a/client/src/app/+about/about-instance/about-instance.component.html
+++ b/client/src/app/+about/about-instance/about-instance.component.html
@@ -83,7 +83,7 @@
83 fragment="business-model" 83 fragment="business-model"
84 #anchorLink 84 #anchorLink
85 (click)="onClickCopyLink(anchorLink)"> 85 (click)="onClickCopyLink(anchorLink)">
86 <h3 i18n class="section-title">How we will pay for this instance</h3> 86 <h3 i18n class="section-title">How we will pay for keeping our instance running</h3>
87 </a> 87 </a>
88 88
89 <div [innerHTML]="html.businessModel"></div> 89 <div [innerHTML]="html.businessModel"></div>
diff --git a/client/src/app/+accounts/account-about/account-about.component.html b/client/src/app/+accounts/account-about/account-about.component.html
deleted file mode 100644
index e9e0e4079..000000000
--- a/client/src/app/+accounts/account-about/account-about.component.html
+++ /dev/null
@@ -1,15 +0,0 @@
1<h1 class="sr-only" i18n>About</h1>
2<div class="margin-content">
3 <div *ngIf="account" class="row no-gutters">
4 <div class="block col-md-6 col-sm-12 pr-2">
5 <h2 i18n class="small-title">DESCRIPTION</h2>
6 <div class="content" [innerHtml]="getAccountDescription()"></div>
7 </div>
8
9 <div class="block col-md-6 col-sm-12">
10 <h2 i18n class="small-title">STATS</h2>
11
12 <div i18n class="content">Joined {{ account.createdAt | date }}</div>
13 </div>
14 </div>
15</div>
diff --git a/client/src/app/+accounts/account-about/account-about.component.scss b/client/src/app/+accounts/account-about/account-about.component.scss
deleted file mode 100644
index 5bcd4b561..000000000
--- a/client/src/app/+accounts/account-about/account-about.component.scss
+++ /dev/null
@@ -1,12 +0,0 @@
1@import '_variables';
2@import '_mixins';
3
4.block {
5 margin-bottom: 40px;
6
7 .small-title {
8 @include in-content-small-title;
9
10 margin-bottom: 20px;
11 }
12}
diff --git a/client/src/app/+accounts/account-about/account-about.component.ts b/client/src/app/+accounts/account-about/account-about.component.ts
deleted file mode 100644
index 6cf846d72..000000000
--- a/client/src/app/+accounts/account-about/account-about.component.ts
+++ /dev/null
@@ -1,40 +0,0 @@
1import { Subscription } from 'rxjs'
2import { Component, OnDestroy, OnInit } from '@angular/core'
3import { MarkdownService } from '@app/core'
4import { Account, AccountService } from '@app/shared/shared-main'
5
6@Component({
7 selector: 'my-account-about',
8 templateUrl: './account-about.component.html',
9 styleUrls: [ './account-about.component.scss' ]
10})
11export class AccountAboutComponent implements OnInit, OnDestroy {
12 account: Account
13 descriptionHTML = ''
14
15 private accountSub: Subscription
16
17 constructor (
18 private accountService: AccountService,
19 private markdownService: MarkdownService
20 ) { }
21
22 ngOnInit () {
23 // Parent get the account for us
24 this.accountSub = this.accountService.accountLoaded
25 .subscribe(async account => {
26 this.account = account
27 this.descriptionHTML = await this.markdownService.textMarkdownToHTML(this.account.description, true)
28 })
29 }
30
31 ngOnDestroy () {
32 if (this.accountSub) this.accountSub.unsubscribe()
33 }
34
35 getAccountDescription () {
36 if (this.descriptionHTML) return this.descriptionHTML
37
38 return $localize`No description`
39 }
40}
diff --git a/client/src/app/+accounts/account-search/account-search.component.ts b/client/src/app/+accounts/account-search/account-search.component.ts
index dda4bf0c7..f54ab846a 100644
--- a/client/src/app/+accounts/account-search/account-search.component.ts
+++ b/client/src/app/+accounts/account-search/account-search.component.ts
@@ -64,9 +64,14 @@ export class AccountSearchComponent extends AbstractVideoList implements OnInit,
64 } 64 }
65 65
66 updateSearch (value: string) { 66 updateSearch (value: string) {
67 if (value === '') this.router.navigate(['../videos'], { relativeTo: this.route })
68 this.search = value 67 this.search = value
69 68
69 if (!this.search) {
70 this.router.navigate([ '../videos' ], { relativeTo: this.route })
71 return
72 }
73
74 this.videos = []
70 this.reloadVideos() 75 this.reloadVideos()
71 } 76 }
72 77
diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.html b/client/src/app/+accounts/account-video-channels/account-video-channels.component.html
index 5dbb341d2..19a4b3c9c 100644
--- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.html
+++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.html
@@ -1,33 +1,50 @@
1<h1 class="sr-only" i18n>Video channels</h1> 1<h1 class="sr-only" i18n>Video channels</h1>
2
2<div class="margin-content"> 3<div class="margin-content">
3 4
4 <div class="no-results" i18n *ngIf="channelPagination.totalItems === 0">This account does not have channels.</div> 5 <div class="no-results" i18n *ngIf="channelPagination.totalItems === 0">This account does not have channels.</div>
5 6
6 <div class="channels" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" [dataObservable]="onChannelDataSubject.asObservable()"> 7 <div class="channels" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" [dataObservable]="onChannelDataSubject.asObservable()">
7 <div class="section channel" *ngFor="let videoChannel of videoChannels"> 8 <div class="channel" *ngFor="let videoChannel of videoChannels">
8 <div class="section-title"> 9
9 <a [routerLink]="getVideoChannelLink(videoChannel)" i18n-title title="See this video channel"> 10 <div class="channel-avatar-row">
11 <a class="avatar-link" [routerLink]="getVideoChannelLink(videoChannel)" i18n-title title="See this video channel">
10 <img [src]="videoChannel.avatarUrl" alt="Avatar" /> 12 <img [src]="videoChannel.avatarUrl" alt="Avatar" />
13 </a>
11 14
12 <h2 class="section-title">{{ videoChannel.displayName }}</h2> 15 <h2>
16 <a [routerLink]="getVideoChannelLink(videoChannel)" i18n-title title="See this video channel">
17 {{ videoChannel.displayName }}
18 </a>
19 </h2>
20
21 <div class="actor-counters">
13 <div class="followers" i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div> 22 <div class="followers" i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div>
14 </a>
15 23
16 <my-subscribe-button [videoChannels]="[videoChannel]"></my-subscribe-button> 24 <span class="videos-count" *ngIf="getTotalVideosOf(videoChannel) !== undefined" i18n>
25 {getTotalVideosOf(videoChannel), plural, =1 {1 videos} other {{{ getTotalVideosOf(videoChannel) }} videos}}
26 </span>
27 </div>
28
29 <div class="description-html" [innerHTML]="getChannelDescription(videoChannel)"></div>
17 </div> 30 </div>
18 31
19 <div *ngIf="getVideosOf(videoChannel)" class="videos"> 32 <my-subscribe-button [videoChannels]="[videoChannel]"></my-subscribe-button>
20 <div class="no-results my-5" i18n *ngIf="getVideosOf(videoChannel).length === 0">This channel doesn't have any videos.</div> 33
34 <a i18n class="button-show-channel peertube-button-link orange-button-inverted" [routerLink]="getVideoChannelLink(videoChannel)">Show this channel</a>
35
36 <div class="videos">
37 <div class="no-results" i18n *ngIf="getTotalVideosOf(videoChannel) === 0">This channel doesn't have any videos.</div>
21 38
22 <my-video-miniature 39 <my-video-miniature
23 *ngFor="let video of getVideosOf(videoChannel)" 40 *ngFor="let video of getVideosOf(videoChannel)"
24 [video]="video" [user]="userMiniature" [displayVideoActions]="true" 41 [video]="video" [user]="userMiniature" [displayVideoActions]="true" [displayOptions]="miniatureDisplayOptions"
25 ></my-video-miniature> 42 ></my-video-miniature>
26 </div>
27 43
28 <a *ngIf="getVideosOf(videoChannel).length !== 0" class="show-more" i18n [routerLink]="getVideoChannelLink(videoChannel)"> 44 <div *ngIf="getTotalVideosOf(videoChannel)" class="miniature-show-channel">
29 SHOW THIS CHANNEL 45 <a i18n [routerLink]="getVideoChannelLink(videoChannel)">SHOW THIS CHANNEL ></a>
30 </a> 46 </div>
47 </div>
31 </div> 48 </div>
32 </div> 49 </div>
33</div> 50</div>
diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss b/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss
index 4957e91d7..7e88802f3 100644
--- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss
+++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss
@@ -3,37 +3,175 @@
3@import '_miniature'; 3@import '_miniature';
4 4
5.margin-content { 5.margin-content {
6 @include fluid-videos-miniature-layout; 6 @include grid-videos-miniature-margins;
7} 7}
8 8
9.section { 9.channel {
10 @include miniature-rows; 10 max-width: $max-channels-width;
11 background-color: pvar(--channelBackgroundColor);
12 padding: 30px;
11 13
12 padding-top: 0 !important; 14 margin: 30px 0;
13 15
14 .section-title { 16 display: grid;
17 grid-template-columns: 1fr auto;
18 grid-template-rows: auto auto;
19 column-gap: 15px;
20}
21
22.channel-avatar-row {
23 grid-column: 1;
24 grid-row: 1;
25
26 display: grid;
27 grid-template-columns: auto auto 1fr;
28 grid-template-rows: auto 1fr;
29
30 .avatar-link {
31 grid-column: 1;
32 grid-row: 1 / 3;
33 margin-right: 30px;
34 }
35
36 img {
37 @include channel-avatar(75px);
38 }
39
40 a {
41 color: pvar(--mainForegroundColor);
42 }
43
44 h2 {
45 grid-row: 1;
46 grid-column: 2;
47 font-size: 20px;
48 line-height: 1;
49 font-weight: $font-bold;
50 margin: 0;
51 }
52
53 .actor-counters {
54 grid-row: 1;
55 grid-column: 3;
56 color: pvar(--greyForegroundColor);
57 font-size: 16px;
58 display: flex;
15 align-items: center; 59 align-items: center;
60 margin-left: 15px;
16 } 61 }
17 62
18 .videos { 63 .actor-counters > *:not(:last-child)::after {
19 overflow: hidden; 64 content: '•';
65 margin: 0 10px;
66 color: pvar(--mainColor);
67 }
20 68
21 .no-results { 69 .description-html {
22 height: 50px; 70 grid-column: 2 / 4;
23 } 71 grid-row: 2;
72
73 max-height: 80px;
74 font-size: 16px;
75
76 @include fade-text(30px, pvar(--channelBackgroundColor));
77 }
78}
79
80my-subscribe-button {
81 grid-row: 1;
82 grid-column: 2;
83}
84
85.videos {
86 display: flex;
87 grid-column: 1 / 3;
88 grid-row: 2;
89 margin-top: 30px;
90
91 position: relative;
92 overflow: hidden;
93
94 my-video-miniature {
95 margin-right: 15px;
96 min-width: $video-thumbnail-medium-width;
97 max-width: $video-thumbnail-medium-width;
98 }
99
100 .no-results {
101 height: auto;
24 } 102 }
103}
25 104
26 my-video-miniature ::ng-deep my-video-actions-dropdown > my-action-dropdown { 105.miniature-show-channel {
27 // Fix our overflow 106 height: 100%;
28 position: absolute; 107 position: absolute;
108 right: 0;
109 background: linear-gradient(90deg, transparent 0, pvar(--channelBackgroundColor) 45px);
110 padding: ($video-thumbnail-medium-height / 2 - 10px) 15px 0 60px;
111 z-index: z(miniature) + 1;
112
113 a {
114 color: pvar(--mainColor);
115 font-size: 16px;
116 font-weight: $font-semibold;
29 } 117 }
30} 118}
31 119
120.button-show-channel {
121 display: none;
122}
123
32@media screen and (max-width: $mobile-view) { 124@media screen and (max-width: $mobile-view) {
33 .section { 125 .channel {
34 .section-title { 126 padding: 15px;
35 flex-direction: column; 127 }
36 align-items: normal; 128
129 .channel-avatar-row {
130 grid-template-columns: auto auto auto 1fr;
131
132 .avatar-link {
133 grid-row: 1 / 4;
134 }
135
136 h2 {
137 font-size: 16px;
37 } 138 }
139
140 .actor-counters {
141 margin: 0;
142 font-size: 13px;
143 grid-row: 2;
144 grid-column: 2 / 4;
145 }
146
147 .description-html {
148 grid-row: 3;
149 font-size: 14px;
150 }
151 }
152
153 .show-channel a {
154 @include peertube-button-link;
155 @include orange-button-inverted;
156 }
157
158 .videos {
159 display: none;
160 }
161
162 my-subscribe-button,
163 .button-show-channel {
164 grid-column: 1 / 4;
165 grid-row: 3;
166 margin-top: 15px;
167 }
168
169 my-subscribe-button {
170 justify-self: start;
171 }
172
173 .button-show-channel {
174 display: block;
175 justify-self: end;
38 } 176 }
39} 177}
diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts b/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts
index f2beb6689..0628c7a96 100644
--- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts
+++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts
@@ -1,9 +1,10 @@
1import { from, Subject, Subscription } from 'rxjs' 1import { from, Subject, Subscription } from 'rxjs'
2import { concatMap, map, switchMap, tap } from 'rxjs/operators' 2import { concatMap, map, switchMap, tap } from 'rxjs/operators'
3import { Component, OnDestroy, OnInit } from '@angular/core' 3import { Component, OnDestroy, OnInit } from '@angular/core'
4import { ComponentPagination, hasMoreItems, ScreenService, User, UserService } from '@app/core' 4import { ComponentPagination, hasMoreItems, MarkdownService, ScreenService, User, UserService } from '@app/core'
5import { Account, AccountService, Video, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main' 5import { Account, AccountService, Video, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main'
6import { NSFWPolicyType, VideoSortField } from '@shared/models' 6import { NSFWPolicyType, VideoSortField } from '@shared/models'
7import { MiniatureDisplayOptions } from '@app/shared/shared-video-miniature'
7 8
8@Component({ 9@Component({
9 selector: 'my-account-video-channels', 10 selector: 'my-account-video-channels',
@@ -13,7 +14,10 @@ import { NSFWPolicyType, VideoSortField } from '@shared/models'
13export class AccountVideoChannelsComponent implements OnInit, OnDestroy { 14export class AccountVideoChannelsComponent implements OnInit, OnDestroy {
14 account: Account 15 account: Account
15 videoChannels: VideoChannel[] = [] 16 videoChannels: VideoChannel[] = []
16 videos: { [id: number]: Video[] } = {} 17
18 videos: { [id: number]: { total: number, videos: Video[] } } = {}
19
20 channelsDescriptionHTML: { [ id: number ]: string } = {}
17 21
18 channelPagination: ComponentPagination = { 22 channelPagination: ComponentPagination = {
19 currentPage: 1, 23 currentPage: 1,
@@ -23,7 +27,7 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy {
23 27
24 videosPagination: ComponentPagination = { 28 videosPagination: ComponentPagination = {
25 currentPage: 1, 29 currentPage: 1,
26 itemsPerPage: 12, 30 itemsPerPage: 5,
27 totalItems: null 31 totalItems: null
28 } 32 }
29 videosSort: VideoSortField = '-publishedAt' 33 videosSort: VideoSortField = '-publishedAt'
@@ -32,6 +36,16 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy {
32 36
33 userMiniature: User 37 userMiniature: User
34 nsfwPolicy: NSFWPolicyType 38 nsfwPolicy: NSFWPolicyType
39 miniatureDisplayOptions: MiniatureDisplayOptions = {
40 date: true,
41 views: true,
42 by: false,
43 avatar: false,
44 privacyLabel: false,
45 privacyText: false,
46 state: false,
47 blacklistInfo: false
48 }
35 49
36 private accountSub: Subscription 50 private accountSub: Subscription
37 51
@@ -39,7 +53,7 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy {
39 private accountService: AccountService, 53 private accountService: AccountService,
40 private videoChannelService: VideoChannelService, 54 private videoChannelService: VideoChannelService,
41 private videoService: VideoService, 55 private videoService: VideoService,
42 private screenService: ScreenService, 56 private markdown: MarkdownService,
43 private userService: UserService 57 private userService: UserService
44 ) { } 58 ) { }
45 59
@@ -78,23 +92,36 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy {
78 } 92 }
79 93
80 return this.videoService.getVideoChannelVideos(options) 94 return this.videoService.getVideoChannelVideos(options)
81 .pipe(map(data => ({ videoChannel, videos: data.data }))) 95 .pipe(map(data => ({ videoChannel, videos: data.data, total: data.total })))
82 }) 96 })
83 ) 97 )
84 .subscribe(({ videoChannel, videos }) => { 98 .subscribe(async ({ videoChannel, videos, total }) => {
99 this.channelsDescriptionHTML[videoChannel.id] = await this.markdown.textMarkdownToHTML(videoChannel.description)
100
85 this.videoChannels.push(videoChannel) 101 this.videoChannels.push(videoChannel)
86 102
87 this.videos[videoChannel.id] = videos 103 this.videos[videoChannel.id] = { videos, total }
88 104
89 this.onChannelDataSubject.next([ videoChannel ]) 105 this.onChannelDataSubject.next([ videoChannel ])
90 }) 106 })
91 } 107 }
92 108
93 getVideosOf (videoChannel: VideoChannel) { 109 getVideosOf (videoChannel: VideoChannel) {
94 const numberOfVideos = this.screenService.getNumberOfAvailableMiniatures() 110 const obj = this.videos[ videoChannel.id ]
111 if (!obj) return []
112
113 return obj.videos
114 }
115
116 getTotalVideosOf (videoChannel: VideoChannel) {
117 const obj = this.videos[ videoChannel.id ]
118 if (!obj) return undefined
119
120 return obj.total
121 }
95 122
96 // 2 rows 123 getChannelDescription (videoChannel: VideoChannel) {
97 return this.videos[ videoChannel.id ].slice(0, numberOfVideos * 2) 124 return this.channelsDescriptionHTML[videoChannel.id]
98 } 125 }
99 126
100 onNearOfBottom () { 127 onNearOfBottom () {
diff --git a/client/src/app/+accounts/account-videos/account-videos.component.ts b/client/src/app/+accounts/account-videos/account-videos.component.ts
index 484d60e25..75af45e90 100644
--- a/client/src/app/+accounts/account-videos/account-videos.component.ts
+++ b/client/src/app/+accounts/account-videos/account-videos.component.ts
@@ -16,6 +16,7 @@ import { VideoFilter } from '@shared/models'
16 ] 16 ]
17}) 17})
18export class AccountVideosComponent extends AbstractVideoList implements OnInit, OnDestroy { 18export class AccountVideosComponent extends AbstractVideoList implements OnInit, OnDestroy {
19 // No value because we don't want a page title
19 titlePage: string 20 titlePage: string
20 loadOnInit = false 21 loadOnInit = false
21 loadUserVideoPreferences = true 22 loadUserVideoPreferences = true
@@ -77,11 +78,6 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit,
77 78
78 return this.videoService 79 return this.videoService
79 .getAccountVideos(options) 80 .getAccountVideos(options)
80 .pipe(
81 tap(({ total }) => {
82 this.titlePage = $localize`Published ${total} videos`
83 })
84 )
85 } 81 }
86 82
87 toggleModerationDisplay () { 83 toggleModerationDisplay () {
@@ -93,4 +89,8 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit,
93 generateSyndicationList () { 89 generateSyndicationList () {
94 this.syndicationItems = this.videoService.getAccountFeedUrls(this.account.id) 90 this.syndicationItems = this.videoService.getAccountFeedUrls(this.account.id)
95 } 91 }
92
93 displayAsRow () {
94 return this.screenService.isInMobileView()
95 }
96} 96}
diff --git a/client/src/app/+accounts/accounts-routing.module.ts b/client/src/app/+accounts/accounts-routing.module.ts
index 15937a67b..3bf0f7185 100644
--- a/client/src/app/+accounts/accounts-routing.module.ts
+++ b/client/src/app/+accounts/accounts-routing.module.ts
@@ -1,11 +1,10 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { MetaGuard } from '@ngx-meta/core' 3import { MetaGuard } from '@ngx-meta/core'
4import { AccountsComponent } from './accounts.component'
5import { AccountVideosComponent } from './account-videos/account-videos.component'
6import { AccountAboutComponent } from './account-about/account-about.component'
7import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component'
8import { AccountSearchComponent } from './account-search/account-search.component' 4import { AccountSearchComponent } from './account-search/account-search.component'
5import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component'
6import { AccountVideosComponent } from './account-videos/account-videos.component'
7import { AccountsComponent } from './accounts.component'
9 8
10const accountsRoutes: Routes = [ 9const accountsRoutes: Routes = [
11 { 10 {
@@ -32,15 +31,6 @@ const accountsRoutes: Routes = [
32 } 31 }
33 }, 32 },
34 { 33 {
35 path: 'about',
36 component: AccountAboutComponent,
37 data: {
38 meta: {
39 title: $localize`About account`
40 }
41 }
42 },
43 {
44 path: 'videos', 34 path: 'videos',
45 component: AccountVideosComponent, 35 component: AccountVideosComponent,
46 data: { 36 data: {
diff --git a/client/src/app/+accounts/accounts.component.html b/client/src/app/+accounts/accounts.component.html
index 5bd7b0824..03d083bb6 100644
--- a/client/src/app/+accounts/accounts.component.html
+++ b/client/src/app/+accounts/accounts.component.html
@@ -1,57 +1,89 @@
1<div *ngIf="account" class="row"> 1<div *ngIf="account" class="root">
2 <div class="sub-menu"> 2 <div class="account-info">
3 3
4 <div class="actor"> 4 <div class="account-avatar-row">
5 <img [src]="account.avatarUrl" alt="Avatar" /> 5 <img class="account-avatar" [src]="account.avatarUrl" alt="Avatar" />
6 6
7 <div class="actor-info"> 7 <div>
8 <div class="actor-names"> 8 <div class="section-label" i18n>PEERTUBE ACCOUNT</div>
9 <div class="actor-display-name">{{ account.displayName }}</div> 9
10 <div class="actor-name"> 10 <div class="actor-info">
11 <span>{{ account.nameWithHost }}</span> 11 <div>
12 <button [cdkCopyToClipboard]="account.nameWithHostForced" (click)="activateCopiedMessage()" 12 <div class="actor-display-name">
13 class="btn btn-outline-secondary btn-sm copy-button" 13 <h1>{{ account.displayName }}</h1>
14 > 14
15 <span class="glyphicon glyphicon-copy"></span> 15 <my-user-moderation-dropdown
16 </button> 16 [prependActions]="prependModerationActions"
17 buttonSize="small" [account]="account" [user]="accountUser" placement="bottom-left auto"
18 (userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()"
19 ></my-user-moderation-dropdown>
20
21 <span *ngIf="accountUser?.blocked" [ngbTooltip]="accountUser.blockedReason" class="badge badge-danger" i18n>Banned</span>
22 <span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span>
23 <span *ngIf="account.mutedServerByUser" class="badge badge-danger" i18n>Instance muted</span>
24 <span *ngIf="account.mutedByInstance" class="badge badge-danger" i18n>Muted by your instance</span>
25 <span *ngIf="account.mutedServerByInstance" class="badge badge-danger" i18n>Instance muted by your instance</span>
26 </div>
27
28 <div class="actor-handle">
29 <span>@{{ account.nameWithHost }}</span>
30 <button [cdkCopyToClipboard]="account.nameWithHostForced" (click)="activateCopiedMessage()"
31 class="btn btn-outline-secondary btn-sm copy-button" title="Copy account handle" i18n-title
32 >
33 <span class="glyphicon glyphicon-duplicate"></span>
34 </button>
35 </div>
36
37 <div class="actor-counters">
38 <span i18n>{naiveAggregatedSubscribers(), plural, =1 {1 subscriber} other {{{ naiveAggregatedSubscribers() }} subscribers}}</span>
39
40 <span class="videos-count" *ngIf="accountVideosCount !== undefined" i18n>
41 {accountVideosCount, plural, =1 {1 videos} other {{{ accountVideosCount }} videos}}
42 </span>
43 </div>
17 </div> 44 </div>
18 <span *ngIf="accountUser?.blocked" [ngbTooltip]="accountUser.blockedReason" class="badge badge-danger" i18n>Banned</span>
19 <span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span>
20 <span *ngIf="account.mutedServerByUser" class="badge badge-danger" i18n>Instance muted</span>
21 <span *ngIf="account.mutedByInstance" class="badge badge-danger" i18n>Muted by your instance</span>
22 <span *ngIf="account.mutedServerByInstance" class="badge badge-danger" i18n>Instance muted by your instance</span>
23
24 <my-user-moderation-dropdown
25 [prependActions]="prependModerationActions"
26 buttonSize="small" [account]="account" [user]="accountUser" placement="bottom-left auto"
27 (userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()"
28 ></my-user-moderation-dropdown>
29 </div>
30 <div class="actor-followers" [title]="accountFollowerTitle">
31 {{ subscribersDisplayFor(naiveAggregatedSubscribers) }}
32 </div> 45 </div>
33 </div> 46 </div>
47 </div>
34 48
35 <div class="right-buttons"> 49 <div class="description" [ngClass]="{ expanded: accountDescriptionExpanded }">
36 <a *ngIf="isAccountManageable && !isInSmallView" routerLink="/my-account" class="btn btn-outline-tertiary mr-2" i18n>Manage account</a> 50 <div class="description-html" [innerHTML]="accountDescriptionHTML"></div>
37 <my-subscribe-button *ngIf="videoChannels" [account]="account" [videoChannels]="videoChannels"></my-subscribe-button> 51
38 </div> 52 <div class="created-at" i18n>Account created on {{ account.createdAt | date }}</div>
39 </div> 53 </div>
40 54
41 <div class="links w-100"> 55 <div *ngIf="hasShowMoreDescription()" class="show-more" role="button"
42 <ng-template #linkTemplate let-item="item"> 56 (click)="accountDescriptionExpanded = !accountDescriptionExpanded"
43 <a [routerLink]="item.routerLink" routerLinkActive="active" class="title-page">{{ item.label }}</a> 57 title="Show the complete description" i18n-title i18n
44 </ng-template> 58 >
59 Show more...
60 </div>
45 61
46 <list-overflow [items]="links" [itemTemplate]="linkTemplate"></list-overflow> 62 <div class="buttons">
63 <a *ngIf="isManageable()" routerLink="/my-account" class="peertube-button-link orange-button" i18n>
64 Manage account
65 </a>
47 66
48 <simple-search-input (searchChanged)="searchChanged($event)" name="search-videos" i18n-placeholder placeholder="Search videos"></simple-search-input> 67 <my-subscribe-button *ngIf="hasVideoChannels() && !isManageable()" [account]="account" [videoChannels]="videoChannels"></my-subscribe-button>
49 </div> 68 </div>
50 </div> 69 </div>
51 70
52 <div class="margin-content"> 71 <div class="links">
53 <router-outlet (activate)="onOutletLoaded($event)"></router-outlet> 72 <ng-template #linkTemplate let-item="item">
73 <a [routerLink]="item.routerLink" routerLinkActive="active" class="title-page">{{ item.label }}</a>
74 </ng-template>
75
76 <list-overflow [hidden]="hideMenu" [items]="links" [itemTemplate]="linkTemplate"></list-overflow>
77
78 <simple-search-input
79 [alwaysShow]="!isInSmallView()" (searchChanged)="searchChanged($event)"
80 (inputDisplayChanged)="onSearchInputDisplayChanged($event)" name="search-videos"
81 i18n-iconTitle icon-title="Search account videos"
82 i18n-placeholder placeholder="Search account videos"
83 ></simple-search-input>
54 </div> 84 </div>
85
86 <router-outlet (activate)="onOutletLoaded($event)"></router-outlet>
55</div> 87</div>
56 88
57<ng-container *ngIf="prependModerationActions"> 89<ng-container *ngIf="prependModerationActions">
diff --git a/client/src/app/+accounts/accounts.component.scss b/client/src/app/+accounts/accounts.component.scss
index 40c6b6493..a836e84ce 100644
--- a/client/src/app/+accounts/accounts.component.scss
+++ b/client/src/app/+accounts/accounts.component.scss
@@ -1,48 +1,29 @@
1// Bootstrap grid utilities require functions, variables and mixins
2@import 'node_modules/bootstrap/scss/functions';
3@import 'node_modules/bootstrap/scss/variables';
4@import 'node_modules/bootstrap/scss/mixins';
5@import 'node_modules/bootstrap/scss/grid';
6
7@import '_variables'; 1@import '_variables';
8@import '_mixins'; 2@import '_mixins';
9 3@import '_actor';
10.sub-menu { 4@import '_miniature';
11 @include sub-menu-with-actor; 5
12 6.root {
13 .actor { 7 --myGlobalTopPadding: 60px;
14 width: 100%; 8 --myImgMargin: 30px;
15 } 9 --myFontSize: 16px;
10 --myGreyFontSize: 16px;
16} 11}
17 12
18.margin-content { 13.section-label {
19 // margin-content is required, but child views have their own margins 14 @include section-label-responsive;
20 // that match views outside the scope of accounts, so we only align
21 // them with the margins of .sub-menu when required.
22 margin: 0;
23} 15}
24 16
25.right-buttons { 17.links {
26 display: flex; 18 @include grid-videos-miniature-margins;
27 height: max-content;
28 margin-left: auto;
29 margin-top: 10px;
30
31 @include media-breakpoint-down(lg) {
32 flex-flow: column-reverse;
33 19
34 a { 20 display: flex;
35 margin-top: 0.25rem; 21 justify-content: space-between;
36 margin-right: 0 !important; 22 align-items: center;
37 } 23 max-width: $max-channels-width;
38 }
39
40 a {
41 @include peertube-button-outline;
42 }
43 24
44 my-subscribe-button { 25 simple-search-input {
45 min-height: 30px; 26 margin-left: auto;
46 } 27 }
47} 28}
48 29
@@ -60,39 +41,106 @@ my-user-moderation-dropdown,
60 41
61.copy-button { 42.copy-button {
62 border: none; 43 border: none;
63 padding: 5px; 44}
64 margin-top: -2px; 45
46.account-info {
47 @include grid-videos-miniature-margins(false, 15px);
48
49 display: grid;
50 grid-template-columns: 1fr min-content;
51 grid-template-rows: auto auto;
52
53 background-color: pvar(--submenuBackgroundColor);
54 margin-bottom: 45px;
55 padding-top: var(--myGlobalTopPadding);
56 font-size: var(--myFontSize);
57}
58
59.account-avatar-row {
60 @include avatar-row-responsive(var(--myImgMargin), var(--myGreyFontSize));
61}
62
63.description {
64 grid-column: 1 / 3;
65 max-width: 1000px;
66 word-break: break-word;
67}
68
69.created-at {
70 margin-top: 15px;
71 color: pvar(--greyForegroundColor);
72 padding-bottom: 60px;
73}
74
75.show-more {
76 @include show-more-description;
77
78 display: none;
79 text-align: center;
80}
81
82.buttons {
83 grid-column: 2;
84 grid-row: 1;
85
86 display: flex;
87 flex-wrap: wrap;
88 justify-content: flex-end;
89 align-content: flex-start;
90
91 > *:not(:last-child) {
92 margin-bottom: 15px;
93 }
94
95 > a {
96 white-space: nowrap;
97 }
98}
99
100@media screen and (max-width: $small-view) {
101 .root {
102 --myGlobalTopPadding: 45px;
103 --myChannelImgMargin: 15px;
104 }
105
106 .account-info {
107 display: block;
108 padding-bottom: 60px;
109 }
110
111 .description:not(.expanded) {
112 max-height: 70px;
113
114 @include fade-text(30px, pvar(--submenuBackgroundColor));
115 }
116
117 .show-more {
118 display: block;
119 }
120
121 .buttons {
122 justify-content: center;
123 }
65} 124}
66 125
67@media screen and (max-width: $mobile-view) { 126@media screen and (max-width: $mobile-view) {
68 .sub-menu { 127 .root {
69 .actor { 128 --myGlobalTopPadding: 15px;
70 flex-direction: column; 129 --myFontSize: 14px;
71 align-items: center; 130 --myGreyFontSize: 13px;
72 131 }
73 img, 132
74 .actor-info .actor-names .actor-display-name { 133 .account-info {
75 margin-right: 0; 134 display: block;
76 } 135 padding-bottom: 30px;
77 136 }
78 .actor-info { 137
79 .actor-names { 138 .links {
80 flex-direction: column; 139 margin: auto !important;
81 align-items: center; 140 width: min-content;
82 } 141 }
83 142
84 my-user-moderation-dropdown { 143 .show-more {
85 margin-left: 0; 144 margin-bottom: 30px;
86 }
87
88 .actor-followers {
89 text-align: center;
90 }
91 }
92
93 .right-buttons {
94 margin-left: 0;
95 }
96 }
97 } 145 }
98} 146}
diff --git a/client/src/app/+accounts/accounts.component.ts b/client/src/app/+accounts/accounts.component.ts
index e6a5a5d5e..fbd7380a9 100644
--- a/client/src/app/+accounts/accounts.component.ts
+++ b/client/src/app/+accounts/accounts.component.ts
@@ -2,11 +2,19 @@ import { Subscription } from 'rxjs'
2import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators' 2import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators'
3import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core' 3import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'
4import { ActivatedRoute } from '@angular/router' 4import { ActivatedRoute } from '@angular/router'
5import { AuthService, Notifier, RedirectService, RestExtractor, ScreenService, UserService } from '@app/core' 5import { AuthService, MarkdownService, Notifier, RedirectService, RestExtractor, ScreenService, UserService } from '@app/core'
6import { Account, AccountService, DropdownAction, ListOverflowItem, VideoChannel, VideoChannelService } from '@app/shared/shared-main' 6import {
7 Account,
8 AccountService,
9 DropdownAction,
10 ListOverflowItem,
11 VideoChannel,
12 VideoChannelService,
13 VideoService
14} from '@app/shared/shared-main'
7import { AccountReportComponent } from '@app/shared/shared-moderation' 15import { AccountReportComponent } from '@app/shared/shared-moderation'
8import { User, UserRight } from '@shared/models'
9import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' 16import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
17import { User, UserRight } from '@shared/models'
10import { AccountSearchComponent } from './account-search/account-search.component' 18import { AccountSearchComponent } from './account-search/account-search.component'
11 19
12@Component({ 20@Component({
@@ -15,16 +23,23 @@ import { AccountSearchComponent } from './account-search/account-search.componen
15}) 23})
16export class AccountsComponent implements OnInit, OnDestroy { 24export class AccountsComponent implements OnInit, OnDestroy {
17 @ViewChild('accountReportModal') accountReportModal: AccountReportComponent 25 @ViewChild('accountReportModal') accountReportModal: AccountReportComponent
26
18 accountSearch: AccountSearchComponent 27 accountSearch: AccountSearchComponent
19 28
20 account: Account 29 account: Account
21 accountUser: User 30 accountUser: User
31
22 videoChannels: VideoChannel[] = [] 32 videoChannels: VideoChannel[] = []
33
23 links: ListOverflowItem[] = [] 34 links: ListOverflowItem[] = []
35 hideMenu = false
24 36
25 isAccountManageable = false
26 accountFollowerTitle = '' 37 accountFollowerTitle = ''
27 38
39 accountVideosCount: number
40 accountDescriptionHTML = ''
41 accountDescriptionExpanded = false
42
28 prependModerationActions: DropdownAction<any>[] 43 prependModerationActions: DropdownAction<any>[]
29 44
30 private routeSub: Subscription 45 private routeSub: Subscription
@@ -38,6 +53,8 @@ export class AccountsComponent implements OnInit, OnDestroy {
38 private restExtractor: RestExtractor, 53 private restExtractor: RestExtractor,
39 private redirectService: RedirectService, 54 private redirectService: RedirectService,
40 private authService: AuthService, 55 private authService: AuthService,
56 private videoService: VideoService,
57 private markdown: MarkdownService,
41 private screenService: ScreenService 58 private screenService: ScreenService
42 ) { 59 ) {
43 } 60 }
@@ -62,9 +79,8 @@ export class AccountsComponent implements OnInit, OnDestroy {
62 ) 79 )
63 80
64 this.links = [ 81 this.links = [
65 { label: $localize`VIDEO CHANNELS`, routerLink: 'video-channels' }, 82 { label: $localize`CHANNELS`, routerLink: 'video-channels' },
66 { label: $localize`VIDEOS`, routerLink: 'videos' }, 83 { label: $localize`VIDEOS`, routerLink: 'videos' }
67 { label: $localize`ABOUT`, routerLink: 'about' }
68 ] 84 ]
69 } 85 }
70 86
@@ -72,19 +88,29 @@ export class AccountsComponent implements OnInit, OnDestroy {
72 if (this.routeSub) this.routeSub.unsubscribe() 88 if (this.routeSub) this.routeSub.unsubscribe()
73 } 89 }
74 90
75 get naiveAggregatedSubscribers () { 91 naiveAggregatedSubscribers () {
76 return this.videoChannels.reduce( 92 return this.videoChannels.reduce(
77 (acc, val) => acc + val.followersCount, 93 (acc, val) => acc + val.followersCount,
78 this.account.followersCount // accumulator starts with the base number of subscribers the account has 94 this.account.followersCount // accumulator starts with the base number of subscribers the account has
79 ) 95 )
80 } 96 }
81 97
82 get isInSmallView () { 98 isUserLoggedIn () {
99 return this.authService.isLoggedIn()
100 }
101
102 isInSmallView () {
83 return this.screenService.isInSmallView() 103 return this.screenService.isInSmallView()
84 } 104 }
85 105
106 isManageable () {
107 if (!this.isUserLoggedIn()) return false
108
109 return this.account?.userId === this.authService.getUser().id
110 }
111
86 onUserChanged () { 112 onUserChanged () {
87 this.getUserIfNeeded(this.account) 113 this.loadUserIfNeeded(this.account)
88 } 114 }
89 115
90 onUserDeleted () { 116 onUserDeleted () {
@@ -113,40 +139,38 @@ export class AccountsComponent implements OnInit, OnDestroy {
113 if (this.accountSearch) this.accountSearch.updateSearch(search) 139 if (this.accountSearch) this.accountSearch.updateSearch(search)
114 } 140 }
115 141
116 private onAccount (account: Account) { 142 onSearchInputDisplayChanged (displayed: boolean) {
143 this.hideMenu = this.isInSmallView() && displayed
144 }
145
146 hasVideoChannels () {
147 return this.videoChannels.length !== 0
148 }
149
150 hasShowMoreDescription () {
151 return !this.accountDescriptionExpanded && this.accountDescriptionHTML.length > 100
152 }
153
154 private async onAccount (account: Account) {
155 this.accountFollowerTitle = $localize`${account.followersCount} direct account followers`
156
117 this.prependModerationActions = undefined 157 this.prependModerationActions = undefined
118 158
119 this.account = account 159 this.accountDescriptionHTML = await this.markdown.textMarkdownToHTML(account.description)
120 160
121 if (this.authService.isLoggedIn()) { 161 // After the markdown renderer to avoid layout changes
122 this.authService.userInformationLoaded.subscribe( 162 this.account = account
123 () => {
124 this.isAccountManageable = this.account.userId && this.account.userId === this.authService.getUser().id
125
126 const followers = this.subscribersDisplayFor(account.followersCount)
127 this.accountFollowerTitle = $localize`${followers} direct account followers`
128
129 // It's not our account, we can report it
130 if (!this.isAccountManageable) {
131 this.prependModerationActions = [
132 {
133 label: $localize`Report this account`,
134 handler: () => this.showReportModal()
135 }
136 ]
137 }
138 }
139 )
140 }
141 163
142 this.getUserIfNeeded(account) 164 this.updateModerationActions()
165 this.loadUserIfNeeded(account)
166 this.loadAccountVideosCount()
143 } 167 }
144 168
145 private showReportModal () { 169 private showReportModal () {
146 this.accountReportModal.show() 170 this.accountReportModal.show()
147 } 171 }
148 172
149 private getUserIfNeeded (account: Account) { 173 private loadUserIfNeeded (account: Account) {
150 if (!account.userId || !this.authService.isLoggedIn()) return 174 if (!account.userId || !this.authService.isLoggedIn()) return
151 175
152 const user = this.authService.getUser() 176 const user = this.authService.getUser()
@@ -158,4 +182,33 @@ export class AccountsComponent implements OnInit, OnDestroy {
158 ) 182 )
159 } 183 }
160 } 184 }
185
186 private updateModerationActions () {
187 if (!this.authService.isLoggedIn()) return
188
189 this.authService.userInformationLoaded.subscribe(
190 () => {
191 if (this.isManageable()) return
192
193 // It's not our account, we can report it
194 this.prependModerationActions = [
195 {
196 label: $localize`Report this account`,
197 handler: () => this.showReportModal()
198 }
199 ]
200 }
201 )
202 }
203
204 private loadAccountVideosCount () {
205 this.videoService.getAccountVideos({
206 account: this.account,
207 videoPagination: {
208 currentPage: 1,
209 itemsPerPage: 0
210 },
211 sort: '-publishedAt'
212 }).subscribe(res => this.accountVideosCount = res.total)
213 }
161} 214}
diff --git a/client/src/app/+accounts/accounts.module.ts b/client/src/app/+accounts/accounts.module.ts
index 6da65cbc1..3354b4189 100644
--- a/client/src/app/+accounts/accounts.module.ts
+++ b/client/src/app/+accounts/accounts.module.ts
@@ -5,10 +5,9 @@ import { SharedMainModule } from '@app/shared/shared-main'
5import { SharedModerationModule } from '@app/shared/shared-moderation' 5import { SharedModerationModule } from '@app/shared/shared-moderation'
6import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription' 6import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription'
7import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' 7import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature'
8import { AccountAboutComponent } from './account-about/account-about.component' 8import { AccountSearchComponent } from './account-search/account-search.component'
9import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component' 9import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component'
10import { AccountVideosComponent } from './account-videos/account-videos.component' 10import { AccountVideosComponent } from './account-videos/account-videos.component'
11import { AccountSearchComponent } from './account-search/account-search.component'
12import { AccountsRoutingModule } from './accounts-routing.module' 11import { AccountsRoutingModule } from './accounts-routing.module'
13import { AccountsComponent } from './accounts.component' 12import { AccountsComponent } from './accounts.component'
14 13
@@ -28,7 +27,6 @@ import { AccountsComponent } from './accounts.component'
28 AccountsComponent, 27 AccountsComponent,
29 AccountVideosComponent, 28 AccountVideosComponent,
30 AccountVideoChannelsComponent, 29 AccountVideoChannelsComponent,
31 AccountAboutComponent,
32 AccountSearchComponent 30 AccountSearchComponent
33 ], 31 ],
34 32
diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts
index fd648a425..bac65c88e 100644
--- a/client/src/app/+admin/admin.module.ts
+++ b/client/src/app/+admin/admin.module.ts
@@ -3,6 +3,7 @@ import { SelectButtonModule } from 'primeng/selectbutton'
3import { TableModule } from 'primeng/table' 3import { TableModule } from 'primeng/table'
4import { NgModule } from '@angular/core' 4import { NgModule } from '@angular/core'
5import { SharedAbuseListModule } from '@app/shared/shared-abuse-list' 5import { SharedAbuseListModule } from '@app/shared/shared-abuse-list'
6import { SharedActorImageModule } from '@app/shared/shared-actor-image'
6import { SharedFormModule } from '@app/shared/shared-forms' 7import { SharedFormModule } from '@app/shared/shared-forms'
7import { SharedGlobalIconModule } from '@app/shared/shared-icons' 8import { SharedGlobalIconModule } from '@app/shared/shared-icons'
8import { SharedMainModule } from '@app/shared/shared-main' 9import { SharedMainModule } from '@app/shared/shared-main'
@@ -49,6 +50,7 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom
49 SharedGlobalIconModule, 50 SharedGlobalIconModule,
50 SharedAbuseListModule, 51 SharedAbuseListModule,
51 SharedVideoCommentModule, 52 SharedVideoCommentModule,
53 SharedActorImageModule,
52 54
53 TableModule, 55 TableModule,
54 SelectButtonModule, 56 SelectButtonModule,
diff --git a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts
index 82c371f4d..d6aca10e7 100644
--- a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts
+++ b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts
@@ -164,7 +164,8 @@ export class VideoBlockListComponent extends RestTable implements OnInit, AfterV
164 baseUrl: `${environment.originServerUrl}/videos/embed/${entry.video.uuid}`, 164 baseUrl: `${environment.originServerUrl}/videos/embed/${entry.video.uuid}`,
165 title: false, 165 title: false,
166 warningTitle: false 166 warningTitle: false
167 }) 167 }),
168 entry.video.name
168 ) 169 )
169 } 170 }
170 171
diff --git a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html
index 1b5fe45c6..8edf03a89 100644
--- a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html
+++ b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html
@@ -3,7 +3,7 @@
3</div> 3</div>
4 4
5<div class="search-bar"> 5<div class="search-bar">
6 <input type="text" (input)="onSearchChange($event)" i18n-placeholder placeholder="Search..."/> 6 <input type="text" (input)="onSearchChange($event)" i18n-placeholder placeholder="Search..." autofocus />
7</div> 7</div>
8 8
9<div class="alert alert-info" i18n *ngIf="pluginInstalled"> 9<div class="alert alert-info" i18n *ngIf="pluginInstalled">
@@ -20,8 +20,8 @@
20 <my-global-icon iconName="search"></my-global-icon> 20 <my-global-icon iconName="search"></my-global-icon>
21 21
22 <ng-container i18n> 22 <ng-container i18n>
23 {{ pagination.totalItems }} {pagination.totalItems, plural, =1 {result} other {results}} for "{{ search }}" 23 {{ pagination.totalItems }} {pagination.totalItems, plural, =1 {result} other {results}} for {{ search }}"
24 </ng-container> 24 </ng-container>
25 </ng-container> 25 </ng-container>
26</div> 26</div>
27 27
diff --git a/client/src/app/+admin/plugins/shared/plugin-list.component.scss b/client/src/app/+admin/plugins/shared/plugin-list.component.scss
index 83030b7e0..f59a01b74 100644
--- a/client/src/app/+admin/plugins/shared/plugin-list.component.scss
+++ b/client/src/app/+admin/plugins/shared/plugin-list.component.scss
@@ -3,7 +3,7 @@
3 3
4.plugin { 4.plugin {
5 margin: 15px 0; 5 margin: 15px 0;
6 background-color: pvar(--submenuColor); 6 background-color: pvar(--submenuBackgroundColor);
7} 7}
8 8
9.first-row { 9.first-row {
diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.html b/client/src/app/+admin/users/user-edit/user-edit.component.html
index 243c6556a..5e92c0f36 100644
--- a/client/src/app/+admin/users/user-edit/user-edit.component.html
+++ b/client/src/app/+admin/users/user-edit/user-edit.component.html
@@ -72,7 +72,7 @@
72 <div class="anchor" id="user"></div> <!-- user anchor --> 72 <div class="anchor" id="user"></div> <!-- user anchor -->
73 <div *ngIf="isCreation()" class="account-title" i18n>NEW USER</div> 73 <div *ngIf="isCreation()" class="account-title" i18n>NEW USER</div>
74 <div *ngIf="!isCreation() && user" class="account-title"> 74 <div *ngIf="!isCreation() && user" class="account-title">
75 <my-actor-avatar-info [actor]="user.account"></my-actor-avatar-info> 75 <my-actor-avatar-edit [actor]="user.account" [editable]="false" [displaySubscribers]="false" [displayUsername]="false"></my-actor-avatar-edit>
76 </div> 76 </div>
77 </div> 77 </div>
78 78
diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.scss b/client/src/app/+admin/users/user-edit/user-edit.component.scss
index aa87b8d6d..8b0ac8783 100644
--- a/client/src/app/+admin/users/user-edit/user-edit.component.scss
+++ b/client/src/app/+admin/users/user-edit/user-edit.component.scss
@@ -72,11 +72,3 @@ input[type=submit], button {
72 @include dashboard; 72 @include dashboard;
73 max-width: 900px; 73 max-width: 900px;
74} 74}
75
76my-actor-avatar-info ::ng-deep {
77 .actor-img-edit-container,
78 .actor-info-followers,
79 .actor-info-username {
80 display: none;
81 }
82}
diff --git a/client/src/app/+login/login.component.html b/client/src/app/+login/login.component.html
index 3171e5b0f..0167066a0 100644
--- a/client/src/app/+login/login.component.html
+++ b/client/src/app/+login/login.component.html
@@ -21,7 +21,7 @@
21 <label i18n for="username">User</label> 21 <label i18n for="username">User</label>
22 <input 22 <input
23 type="text" id="username" i18n-placeholder placeholder="Username or email address" required tabindex="1" 23 type="text" id="username" i18n-placeholder placeholder="Username or email address" required tabindex="1"
24 formControlName="username" class="form-control" [ngClass]="{ 'input-error': formErrors['username'] }" #usernameInput 24 formControlName="username" class="form-control" [ngClass]="{ 'input-error': formErrors['username'] }" autofocus
25 > 25 >
26 </div> 26 </div>
27 27
diff --git a/client/src/app/+login/login.component.ts b/client/src/app/+login/login.component.ts
index af747b7fa..d8ad49081 100644
--- a/client/src/app/+login/login.component.ts
+++ b/client/src/app/+login/login.component.ts
@@ -3,9 +3,9 @@ import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angula
3import { ActivatedRoute } from '@angular/router' 3import { ActivatedRoute } from '@angular/router'
4import { AuthService, Notifier, RedirectService, UserService } from '@app/core' 4import { AuthService, Notifier, RedirectService, UserService } from '@app/core'
5import { HooksService } from '@app/core/plugins/hooks.service' 5import { HooksService } from '@app/core/plugins/hooks.service'
6import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance'
7import { LOGIN_PASSWORD_VALIDATOR, LOGIN_USERNAME_VALIDATOR } from '@app/shared/form-validators/login-validators' 6import { LOGIN_PASSWORD_VALIDATOR, LOGIN_USERNAME_VALIDATOR } from '@app/shared/form-validators/login-validators'
8import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 7import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
8import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance'
9import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' 9import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
10import { RegisteredExternalAuthConfig, ServerConfig } from '@shared/models' 10import { RegisteredExternalAuthConfig, ServerConfig } from '@shared/models'
11 11
@@ -16,7 +16,6 @@ import { RegisteredExternalAuthConfig, ServerConfig } from '@shared/models'
16}) 16})
17 17
18export class LoginComponent extends FormReactive implements OnInit, AfterViewInit { 18export class LoginComponent extends FormReactive implements OnInit, AfterViewInit {
19 @ViewChild('usernameInput', { static: false }) usernameInput: ElementRef
20 @ViewChild('forgotPasswordModal', { static: true }) forgotPasswordModal: ElementRef 19 @ViewChild('forgotPasswordModal', { static: true }) forgotPasswordModal: ElementRef
21 20
22 accordion: NgbAccordion 21 accordion: NgbAccordion
@@ -91,10 +90,6 @@ export class LoginComponent extends FormReactive implements OnInit, AfterViewIni
91 } 90 }
92 91
93 ngAfterViewInit () { 92 ngAfterViewInit () {
94 if (this.usernameInput) {
95 this.usernameInput.nativeElement.focus()
96 }
97
98 this.hooks.runAction('action:login.init', 'login') 93 this.hooks.runAction('action:login.init', 'login')
99 } 94 }
100 95
diff --git a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts
index ad7497f45..c7e173038 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts
+++ b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts
@@ -42,7 +42,9 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
42 newInstanceFollower: $localize`Your instance has a new follower`, 42 newInstanceFollower: $localize`Your instance has a new follower`,
43 autoInstanceFollowing: $localize`Your instance automatically followed another instance`, 43 autoInstanceFollowing: $localize`Your instance automatically followed another instance`,
44 abuseNewMessage: $localize`An abuse report received a new message`, 44 abuseNewMessage: $localize`An abuse report received a new message`,
45 abuseStateChange: $localize`One of your abuse reports has been accepted or rejected by moderators` 45 abuseStateChange: $localize`One of your abuse reports has been accepted or rejected by moderators`,
46 newPeerTubeVersion: $localize`A new PeerTube version is available`,
47 newPluginVersion: $localize`One of your plugin/theme has a new available version`
46 } 48 }
47 this.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[] 49 this.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[]
48 50
@@ -51,7 +53,9 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
51 videoAutoBlacklistAsModerator: UserRight.MANAGE_VIDEO_BLACKLIST, 53 videoAutoBlacklistAsModerator: UserRight.MANAGE_VIDEO_BLACKLIST,
52 newUserRegistration: UserRight.MANAGE_USERS, 54 newUserRegistration: UserRight.MANAGE_USERS,
53 newInstanceFollower: UserRight.MANAGE_SERVER_FOLLOW, 55 newInstanceFollower: UserRight.MANAGE_SERVER_FOLLOW,
54 autoInstanceFollowing: UserRight.MANAGE_CONFIGURATION 56 autoInstanceFollowing: UserRight.MANAGE_CONFIGURATION,
57 newPeerTubeVersion: UserRight.MANAGE_DEBUG,
58 newPluginVersion: UserRight.MANAGE_DEBUG
55 } 59 }
56 } 60 }
57 61
diff --git a/client/src/app/+my-account/my-account-settings/my-account-settings.component.html b/client/src/app/+my-account/my-account-settings/my-account-settings.component.html
index b0d2ec58d..48d06280b 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-settings.component.html
+++ b/client/src/app/+my-account/my-account-settings/my-account-settings.component.html
@@ -3,7 +3,7 @@
3 <div class="form-group col-12 col-lg-4 col-xl-3"></div> 3 <div class="form-group col-12 col-lg-4 col-xl-3"></div>
4 4
5 <div class="form-group col-12 col-lg-8 col-xl-9"> 5 <div class="form-group col-12 col-lg-8 col-xl-9">
6 <my-actor-avatar-info [actor]="user.account" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"></my-actor-avatar-info> 6 <my-actor-avatar-edit [actor]="user.account" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"></my-actor-avatar-edit>
7 </div> 7 </div>
8</div> 8</div>
9 9
diff --git a/client/src/app/+my-account/my-account.module.ts b/client/src/app/+my-account/my-account.module.ts
index 076864563..3df48d0aa 100644
--- a/client/src/app/+my-account/my-account.module.ts
+++ b/client/src/app/+my-account/my-account.module.ts
@@ -3,6 +3,7 @@ import { TableModule } from 'primeng/table'
3import { DragDropModule } from '@angular/cdk/drag-drop' 3import { DragDropModule } from '@angular/cdk/drag-drop'
4import { NgModule } from '@angular/core' 4import { NgModule } from '@angular/core'
5import { SharedAbuseListModule } from '@app/shared/shared-abuse-list' 5import { SharedAbuseListModule } from '@app/shared/shared-abuse-list'
6import { SharedActorImageModule } from '@app/shared/shared-actor-image'
6import { SharedFormModule } from '@app/shared/shared-forms' 7import { SharedFormModule } from '@app/shared/shared-forms'
7import { SharedGlobalIconModule } from '@app/shared/shared-icons' 8import { SharedGlobalIconModule } from '@app/shared/shared-icons'
8import { SharedMainModule } from '@app/shared/shared-main' 9import { SharedMainModule } from '@app/shared/shared-main'
@@ -10,6 +11,7 @@ import { SharedModerationModule } from '@app/shared/shared-moderation'
10import { SharedShareModal } from '@app/shared/shared-share-modal' 11import { SharedShareModal } from '@app/shared/shared-share-modal'
11import { SharedUserInterfaceSettingsModule } from '@app/shared/shared-user-settings' 12import { SharedUserInterfaceSettingsModule } from '@app/shared/shared-user-settings'
12import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component' 13import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component'
14import { MyAccountApplicationsComponent } from './my-account-applications/my-account-applications.component'
13import { MyAccountBlocklistComponent } from './my-account-blocklist/my-account-blocklist.component' 15import { MyAccountBlocklistComponent } from './my-account-blocklist/my-account-blocklist.component'
14import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component' 16import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component'
15import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component' 17import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component'
@@ -20,7 +22,6 @@ import { MyAccountDangerZoneComponent } from './my-account-settings/my-account-d
20import { MyAccountNotificationPreferencesComponent } from './my-account-settings/my-account-notification-preferences' 22import { MyAccountNotificationPreferencesComponent } from './my-account-settings/my-account-notification-preferences'
21import { MyAccountProfileComponent } from './my-account-settings/my-account-profile/my-account-profile.component' 23import { MyAccountProfileComponent } from './my-account-settings/my-account-profile/my-account-profile.component'
22import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component' 24import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component'
23import { MyAccountApplicationsComponent } from './my-account-applications/my-account-applications.component'
24import { MyAccountComponent } from './my-account.component' 25import { MyAccountComponent } from './my-account.component'
25 26
26@NgModule({ 27@NgModule({
@@ -37,7 +38,8 @@ import { MyAccountComponent } from './my-account.component'
37 SharedUserInterfaceSettingsModule, 38 SharedUserInterfaceSettingsModule,
38 SharedGlobalIconModule, 39 SharedGlobalIconModule,
39 SharedAbuseListModule, 40 SharedAbuseListModule,
40 SharedShareModal 41 SharedShareModal,
42 SharedActorImageModule
41 ], 43 ],
42 44
43 declarations: [ 45 declarations: [
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts b/client/src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts
index a625493de..b3265210f 100644
--- a/client/src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-create.component.ts
@@ -8,10 +8,12 @@ import {
8 VIDEO_CHANNEL_SUPPORT_VALIDATOR 8 VIDEO_CHANNEL_SUPPORT_VALIDATOR
9} from '@app/shared/form-validators/video-channel-validators' 9} from '@app/shared/form-validators/video-channel-validators'
10import { FormValidatorService } from '@app/shared/shared-forms' 10import { FormValidatorService } from '@app/shared/shared-forms'
11import { VideoChannelService } from '@app/shared/shared-main' 11import { VideoChannel, VideoChannelService } from '@app/shared/shared-main'
12import { VideoChannelCreate } from '@shared/models' 12import { VideoChannelCreate } from '@shared/models'
13import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' 13import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
14import { MyVideoChannelEdit } from './my-video-channel-edit' 14import { MyVideoChannelEdit } from './my-video-channel-edit'
15import { switchMap } from 'rxjs/operators'
16import { of } from 'rxjs'
15 17
16@Component({ 18@Component({
17 templateUrl: './my-video-channel-edit.component.html', 19 templateUrl: './my-video-channel-edit.component.html',
@@ -19,6 +21,10 @@ import { MyVideoChannelEdit } from './my-video-channel-edit'
19}) 21})
20export class MyVideoChannelCreateComponent extends MyVideoChannelEdit implements OnInit { 22export class MyVideoChannelCreateComponent extends MyVideoChannelEdit implements OnInit {
21 error: string 23 error: string
24 videoChannel = new VideoChannel({})
25
26 private avatar: FormData
27 private banner: FormData
22 28
23 constructor ( 29 constructor (
24 protected formValidatorService: FormValidatorService, 30 protected formValidatorService: FormValidatorService,
@@ -50,23 +56,43 @@ export class MyVideoChannelCreateComponent extends MyVideoChannelEdit implements
50 support: body.support || null 56 support: body.support || null
51 } 57 }
52 58
53 this.videoChannelService.createVideoChannel(videoChannelCreate).subscribe( 59 this.videoChannelService.createVideoChannel(videoChannelCreate)
54 () => { 60 .pipe(
55 this.authService.refreshUserInformation() 61 switchMap(() => this.uploadAvatar()),
62 switchMap(() => this.uploadBanner())
63 ).subscribe(
64 () => {
65 this.authService.refreshUserInformation()
66
67 this.notifier.success($localize`Video channel ${videoChannelCreate.displayName} created.`)
68 this.router.navigate(['/my-library', 'video-channels'])
69 },
56 70
57 this.notifier.success($localize`Video channel ${videoChannelCreate.displayName} created.`) 71 err => {
58 this.router.navigate([ '/my-library', 'video-channels' ]) 72 if (err.status === HttpStatusCode.CONFLICT_409) {
59 }, 73 this.error = $localize`This name already exists on this instance.`
74 return
75 }
60 76
61 err => { 77 this.error = err.message
62 if (err.status === HttpStatusCode.CONFLICT_409) {
63 this.error = $localize`This name already exists on this instance.`
64 return
65 } 78 }
79 )
80 }
81
82 onAvatarChange (formData: FormData) {
83 this.avatar = formData
84 }
85
86 onAvatarDelete () {
87 this.avatar = null
88 }
89
90 onBannerChange (formData: FormData) {
91 this.banner = formData
92 }
66 93
67 this.error = err.message 94 onBannerDelete () {
68 } 95 this.banner = null
69 )
70 } 96 }
71 97
72 isCreation () { 98 isCreation () {
@@ -76,4 +102,20 @@ export class MyVideoChannelCreateComponent extends MyVideoChannelEdit implements
76 getFormButtonTitle () { 102 getFormButtonTitle () {
77 return $localize`Create` 103 return $localize`Create`
78 } 104 }
105
106 getUsername () {
107 return this.form.value.name
108 }
109
110 private uploadAvatar () {
111 if (!this.avatar) return of(undefined)
112
113 return this.videoChannelService.changeVideoChannelImage(this.getUsername(), this.avatar, 'avatar')
114 }
115
116 private uploadBanner () {
117 if (!this.banner) return of(undefined)
118
119 return this.videoChannelService.changeVideoChannelImage(this.getUsername(), this.banner, 'banner')
120 }
79} 121}
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html
index 735f9e3ba..2910dffad 100644
--- a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html
@@ -10,7 +10,7 @@
10 <ng-container *ngIf="!isCreation()"> 10 <ng-container *ngIf="!isCreation()">
11 <li class="breadcrumb-item active" i18n>Edit</li> 11 <li class="breadcrumb-item active" i18n>Edit</li>
12 <li class="breadcrumb-item active" aria-current="page"> 12 <li class="breadcrumb-item active" aria-current="page">
13 <a *ngIf="videoChannelToUpdate" [routerLink]="[ '/my-library/video-channels/update', videoChannelToUpdate?.nameWithHost ]">{{ videoChannelToUpdate?.displayName }}</a> 13 <a *ngIf="videoChannel" [routerLink]="[ '/my-library/video-channels/update', videoChannel?.nameWithHost ]">{{ videoChannel?.displayName }}</a>
14 </li> 14 </li>
15 </ng-container> 15 </ng-container>
16 </ol> 16 </ol>
@@ -23,10 +23,22 @@
23 <div class="form-row"> <!-- channel grid --> 23 <div class="form-row"> <!-- channel grid -->
24 <div class="form-group col-12 col-lg-4 col-xl-3"> 24 <div class="form-group col-12 col-lg-4 col-xl-3">
25 <div *ngIf="isCreation()" class="video-channel-title" i18n>NEW CHANNEL</div> 25 <div *ngIf="isCreation()" class="video-channel-title" i18n>NEW CHANNEL</div>
26 <div *ngIf="!isCreation() && videoChannelToUpdate" class="video-channel-title" i18n>CHANNEL</div> 26 <div *ngIf="!isCreation() && videoChannel" class="video-channel-title" i18n>CHANNEL</div>
27 </div> 27 </div>
28 28
29 <div class="form-group col-12 col-lg-8 col-xl-9"> 29 <div class="form-group col-12 col-lg-8 col-xl-9">
30 <h6 i18n>Banner image of your channel</h6>
31
32 <my-actor-banner-edit
33 *ngIf="videoChannel" [previewImage]="isCreation()"
34 [actor]="videoChannel" (bannerChange)="onBannerChange($event)" (bannerDelete)="onBannerDelete()"
35 ></my-actor-banner-edit>
36
37 <my-actor-avatar-edit
38 *ngIf="videoChannel" [previewImage]="isCreation()"
39 [actor]="videoChannel" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"
40 [displayUsername]="!isCreation()" [displaySubscribers]="!isCreation()"
41 ></my-actor-avatar-edit>
30 42
31 <div class="form-group" *ngIf="isCreation()"> 43 <div class="form-group" *ngIf="isCreation()">
32 <label i18n for="name">Name</label> 44 <label i18n for="name">Name</label>
@@ -44,11 +56,6 @@
44 </div> 56 </div>
45 </div> 57 </div>
46 58
47 <my-actor-avatar-info
48 *ngIf="!isCreation() && videoChannelToUpdate"
49 [actor]="videoChannelToUpdate" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"
50 ></my-actor-avatar-info>
51
52 <div class="form-group"> 59 <div class="form-group">
53 <label i18n for="display-name">Display name</label> 60 <label i18n for="display-name">Display name</label>
54 <input 61 <input
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss
index 8f8af655c..22de103d1 100644
--- a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.scss
@@ -10,11 +10,16 @@ label {
10 @include settings-big-title; 10 @include settings-big-title;
11} 11}
12 12
13my-actor-avatar-info { 13my-actor-avatar-edit,
14my-actor-banner-edit {
14 display: block; 15 display: block;
15 margin-bottom: 20px; 16 margin-bottom: 20px;
16} 17}
17 18
19my-actor-banner-edit {
20 max-width: 500px;
21}
22
18.input-group { 23.input-group {
19 @include peertube-input-group(fit-content); 24 @include peertube-input-group(fit-content);
20} 25}
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts
index 3e20a27ee..33bb90f14 100644
--- a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts
@@ -2,8 +2,7 @@ import { FormReactive } from '@app/shared/shared-forms'
2import { VideoChannel } from '@app/shared/shared-main' 2import { VideoChannel } from '@app/shared/shared-main'
3 3
4export abstract class MyVideoChannelEdit extends FormReactive { 4export abstract class MyVideoChannelEdit extends FormReactive {
5 // We need it even in the create component because it's used in the edit template 5 videoChannel: VideoChannel
6 videoChannelToUpdate: VideoChannel
7 6
8 abstract isCreation (): boolean 7 abstract isCreation (): boolean
9 abstract getFormButtonTitle (): string 8 abstract getFormButtonTitle (): string
@@ -12,10 +11,6 @@ export abstract class MyVideoChannelEdit extends FormReactive {
12 return window.location.host 11 return window.location.host
13 } 12 }
14 13
15 // We need this method so angular does not complain in child template that doesn't need this
16 onAvatarChange (formData: FormData) { /* empty */ }
17 onAvatarDelete () { /* empty */ }
18
19 // Should be implemented by the child 14 // Should be implemented by the child
20 isBulkUpdateVideosDisplayed () { 15 isBulkUpdateVideosDisplayed () {
21 return false 16 return false
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts b/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts
index 6cd1ff503..a29af176c 100644
--- a/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts
@@ -1,7 +1,9 @@
1import { Subscription } from 'rxjs' 1import { Subscription } from 'rxjs'
2import { HttpErrorResponse } from '@angular/common/http'
2import { Component, OnDestroy, OnInit } from '@angular/core' 3import { Component, OnDestroy, OnInit } from '@angular/core'
3import { ActivatedRoute, Router } from '@angular/router' 4import { ActivatedRoute, Router } from '@angular/router'
4import { AuthService, Notifier, ServerService } from '@app/core' 5import { AuthService, Notifier, ServerService } from '@app/core'
6import { uploadErrorHandler } from '@app/helpers'
5import { 7import {
6 VIDEO_CHANNEL_DESCRIPTION_VALIDATOR, 8 VIDEO_CHANNEL_DESCRIPTION_VALIDATOR,
7 VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, 9 VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR,
@@ -11,8 +13,6 @@ import { FormValidatorService } from '@app/shared/shared-forms'
11import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' 13import { VideoChannel, VideoChannelService } from '@app/shared/shared-main'
12import { ServerConfig, VideoChannelUpdate } from '@shared/models' 14import { ServerConfig, VideoChannelUpdate } from '@shared/models'
13import { MyVideoChannelEdit } from './my-video-channel-edit' 15import { MyVideoChannelEdit } from './my-video-channel-edit'
14import { HttpErrorResponse } from '@angular/common/http'
15import { uploadErrorHandler } from '@app/helpers'
16 16
17@Component({ 17@Component({
18 selector: 'my-video-channel-update', 18 selector: 'my-video-channel-update',
@@ -21,7 +21,7 @@ import { uploadErrorHandler } from '@app/helpers'
21}) 21})
22export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements OnInit, OnDestroy { 22export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements OnInit, OnDestroy {
23 error: string 23 error: string
24 videoChannelToUpdate: VideoChannel 24 videoChannel: VideoChannel
25 25
26 private paramsSub: Subscription 26 private paramsSub: Subscription
27 private oldSupportField: string 27 private oldSupportField: string
@@ -56,7 +56,7 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements
56 56
57 this.videoChannelService.getVideoChannel(videoChannelId).subscribe( 57 this.videoChannelService.getVideoChannel(videoChannelId).subscribe(
58 videoChannelToUpdate => { 58 videoChannelToUpdate => {
59 this.videoChannelToUpdate = videoChannelToUpdate 59 this.videoChannel = videoChannelToUpdate
60 60
61 this.oldSupportField = videoChannelToUpdate.support 61 this.oldSupportField = videoChannelToUpdate.support
62 62
@@ -87,7 +87,7 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements
87 bulkVideosSupportUpdate: body.bulkVideosSupportUpdate || false 87 bulkVideosSupportUpdate: body.bulkVideosSupportUpdate || false
88 } 88 }
89 89
90 this.videoChannelService.updateVideoChannel(this.videoChannelToUpdate.name, videoChannelUpdate).subscribe( 90 this.videoChannelService.updateVideoChannel(this.videoChannel.name, videoChannelUpdate).subscribe(
91 () => { 91 () => {
92 this.authService.refreshUserInformation() 92 this.authService.refreshUserInformation()
93 93
@@ -101,12 +101,12 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements
101 } 101 }
102 102
103 onAvatarChange (formData: FormData) { 103 onAvatarChange (formData: FormData) {
104 this.videoChannelService.changeVideoChannelAvatar(this.videoChannelToUpdate.name, formData) 104 this.videoChannelService.changeVideoChannelImage(this.videoChannel.name, formData, 'avatar')
105 .subscribe( 105 .subscribe(
106 data => { 106 data => {
107 this.notifier.success($localize`Avatar changed.`) 107 this.notifier.success($localize`Avatar changed.`)
108 108
109 this.videoChannelToUpdate.updateAvatar(data.avatar) 109 this.videoChannel.updateAvatar(data.avatar)
110 }, 110 },
111 111
112 (err: HttpErrorResponse) => uploadErrorHandler({ 112 (err: HttpErrorResponse) => uploadErrorHandler({
@@ -118,12 +118,42 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements
118 } 118 }
119 119
120 onAvatarDelete () { 120 onAvatarDelete () {
121 this.videoChannelService.deleteVideoChannelAvatar(this.videoChannelToUpdate.name) 121 this.videoChannelService.deleteVideoChannelImage(this.videoChannel.name, 'avatar')
122 .subscribe( 122 .subscribe(
123 data => { 123 data => {
124 this.notifier.success($localize`Avatar deleted.`) 124 this.notifier.success($localize`Avatar deleted.`)
125 125
126 this.videoChannelToUpdate.resetAvatar() 126 this.videoChannel.resetAvatar()
127 },
128
129 err => this.notifier.error(err.message)
130 )
131 }
132
133 onBannerChange (formData: FormData) {
134 this.videoChannelService.changeVideoChannelImage(this.videoChannel.name, formData, 'banner')
135 .subscribe(
136 data => {
137 this.notifier.success($localize`Banner changed.`)
138
139 this.videoChannel.updateBanner(data.banner)
140 },
141
142 (err: HttpErrorResponse) => uploadErrorHandler({
143 err,
144 name: $localize`banner`,
145 notifier: this.notifier
146 })
147 )
148 }
149
150 onBannerDelete () {
151 this.videoChannelService.deleteVideoChannelImage(this.videoChannel.name, 'banner')
152 .subscribe(
153 data => {
154 this.notifier.success($localize`Banner deleted.`)
155
156 this.videoChannel.resetBanner()
127 }, 157 },
128 158
129 err => this.notifier.error(err.message) 159 err => this.notifier.error(err.message)
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.scss b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.scss
index f2f42459f..8804fa95c 100644
--- a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.scss
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.scss
@@ -17,10 +17,11 @@ input[type=text] {
17 17
18.video-channel { 18.video-channel {
19 @include row-blocks; 19 @include row-blocks;
20
20 padding-bottom: 0; 21 padding-bottom: 0;
21 22
22 img { 23 img {
23 @include avatar(80px); 24 @include channel-avatar(80px);
24 25
25 margin-right: 10px; 26 margin-right: 10px;
26 } 27 }
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts b/client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts
index 92b56db49..53557ca02 100644
--- a/client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channels.module.ts
@@ -1,5 +1,6 @@
1import { ChartModule } from 'primeng/chart' 1import { ChartModule } from 'primeng/chart'
2import { NgModule } from '@angular/core' 2import { NgModule } from '@angular/core'
3import { SharedActorImageModule } from '@app/shared/shared-actor-image'
3import { SharedFormModule } from '@app/shared/shared-forms' 4import { SharedFormModule } from '@app/shared/shared-forms'
4import { SharedGlobalIconModule } from '@app/shared/shared-icons' 5import { SharedGlobalIconModule } from '@app/shared/shared-icons'
5import { SharedMainModule } from '@app/shared/shared-main' 6import { SharedMainModule } from '@app/shared/shared-main'
@@ -16,7 +17,8 @@ import { MyVideoChannelsComponent } from './my-video-channels.component'
16 17
17 SharedMainModule, 18 SharedMainModule,
18 SharedFormModule, 19 SharedFormModule,
19 SharedGlobalIconModule 20 SharedGlobalIconModule,
21 SharedActorImageModule
20 ], 22 ],
21 23
22 declarations: [ 24 declarations: [
diff --git a/client/src/app/+my-library/my-history/my-history.component.html b/client/src/app/+my-library/my-history/my-history.component.html
index c180161e7..9dec64645 100644
--- a/client/src/app/+my-library/my-history/my-history.component.html
+++ b/client/src/app/+my-library/my-history/my-history.component.html
@@ -4,7 +4,7 @@
4</h1> 4</h1>
5 5
6<div class="top-buttons"> 6<div class="top-buttons">
7 <div> 7 <div class="search-wrapper">
8 <div class="input-group has-feedback has-clear"> 8 <div class="input-group has-feedback has-clear">
9 <input 9 <input
10 type="text" name="history-search" id="history-search" i18n-placeholder placeholder="Search your history" 10 type="text" name="history-search" id="history-search" i18n-placeholder placeholder="Search your history"
@@ -15,7 +15,7 @@
15 </div> 15 </div>
16 </div> 16 </div>
17 17
18 <div class="history-switch ml-auto mr-3"> 18 <div class="history-switch">
19 <my-input-switch [(ngModel)]="videosHistoryEnabled" (ngModelChange)="onVideosHistoryChange()"></my-input-switch> 19 <my-input-switch [(ngModel)]="videosHistoryEnabled" (ngModelChange)="onVideosHistoryChange()"></my-input-switch>
20 <label i18n>Track watch history</label> 20 <label i18n>Track watch history</label>
21 </div> 21 </div>
diff --git a/client/src/app/+my-library/my-history/my-history.component.scss b/client/src/app/+my-library/my-history/my-history.component.scss
index 928a8a3da..af4a34b4b 100644
--- a/client/src/app/+my-library/my-history/my-history.component.scss
+++ b/client/src/app/+my-library/my-history/my-history.component.scss
@@ -11,16 +11,24 @@
11 11
12.top-buttons { 12.top-buttons {
13 margin-bottom: 30px; 13 margin-bottom: 30px;
14 display: flex; 14 display: grid;
15 grid-template-columns: 250px 1fr auto auto;
15 align-items: center; 16 align-items: center;
16 flex-wrap: wrap;
17 17
18 #history-search { 18 .search-wrapper {
19 @include peertube-input-text(250px); 19 grid-column: 1;
20
21 input {
22 @include peertube-input-text(250px);
23 }
20 } 24 }
21 25
22 .history-switch { 26 .history-switch {
27 grid-column: 3;
28
23 display: flex; 29 display: flex;
30 margin-left: auto;
31 margin-right: 15px;
24 32
25 label { 33 label {
26 margin: 0 0 0 5px; 34 margin: 0 0 0 5px;
@@ -31,6 +39,8 @@
31 } 39 }
32 40
33 .delete-history { 41 .delete-history {
42 grid-column: 4;
43
34 @include peertube-button; 44 @include peertube-button;
35 @include grey-button; 45 @include grey-button;
36 @include button-with-icon; 46 @include button-with-icon;
@@ -40,26 +50,27 @@
40} 50}
41 51
42.video { 52.video {
43 @include row-blocks; 53 @include row-blocks($column-responsive: false);
44
45 .my-video-miniature {
46 flex-grow: 1;
47 }
48} 54}
49 55
50@media screen and (max-width: $mobile-view) { 56@media screen and (max-width: $small-view) {
51 .top-buttons { 57 .top-buttons {
52 .history-switch label, .delete-history { 58 grid-template-columns: auto 1fr auto;
53 @include ellipsis; 59 row-gap: 20px;
54 }
55 60
56 .history-switch label { 61 .history-switch {
57 width: 60%; 62 grid-row: 1;
63 grid-column: 1;
64 margin: 0;
58 } 65 }
59 66
60 .delete-history { 67 .delete-history {
61 margin-left: auto; 68 grid-row: 1;
62 max-width: 32%; 69 grid-column: 3;
70 }
71
72 .search-wrapper {
73 grid-column: 1 / 4;
63 } 74 }
64 } 75 }
65} 76}
diff --git a/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html b/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html
index 510b400c0..ff448ad87 100644
--- a/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html
+++ b/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html
@@ -6,7 +6,7 @@
6 </span> 6 </span>
7</h1> 7</h1>
8 8
9<div class="video-subscriptions-header d-flex justify-content-between"> 9<div class="video-subscriptions-header">
10 <div class="has-feedback has-clear"> 10 <div class="has-feedback has-clear">
11 <input type="text" placeholder="Search your subscriptions" i18n-placeholder [(ngModel)]="subscriptionsSearch" 11 <input type="text" placeholder="Search your subscriptions" i18n-placeholder [(ngModel)]="subscriptionsSearch"
12 (ngModelChange)="onSubscriptionsSearchChanged()" /> 12 (ngModelChange)="onSubscriptionsSearchChanged()" />
diff --git a/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.scss b/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.scss
index 5ead45dd8..3c1a4d2ad 100644
--- a/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.scss
+++ b/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.scss
@@ -9,40 +9,40 @@ input[type=text] {
9 @include row-blocks; 9 @include row-blocks;
10 10
11 img { 11 img {
12 @include avatar(80px); 12 @include channel-avatar(80px);
13 13
14 margin-right: 10px; 14 margin-right: 10px;
15 } 15 }
16}
16 17
17 .video-channel-info { 18.video-channel-info {
18 flex-grow: 1; 19 flex-grow: 1;
19 20
20 a.video-channel-names { 21 a.video-channel-names {
21 @include disable-default-a-behaviour; 22 @include disable-default-a-behaviour;
22 23
23 width: fit-content; 24 width: fit-content;
24 display: flex; 25 display: flex;
25 align-items: baseline; 26 align-items: baseline;
26 color: pvar(--mainForegroundColor); 27 color: pvar(--mainForegroundColor);
27 28
28 .video-channel-display-name { 29 .video-channel-display-name {
29 font-weight: $font-semibold; 30 font-weight: $font-semibold;
30 font-size: 18px; 31 font-size: 18px;
31 } 32 }
32 33
33 .video-channel-name { 34 .video-channel-name {
34 font-size: 14px; 35 font-size: 14px;
35 color: $grey-actor-name; 36 color: $grey-actor-name;
36 margin-left: 5px; 37 margin-left: 5px;
37 }
38 } 38 }
39 } 39 }
40}
40 41
41 .actor-owner { 42.actor-owner {
42 @include actor-owner; 43 @include actor-owner;
43 44
44 margin-top: 0; 45 margin-top: 0;
45 }
46} 46}
47 47
48.video-subscriptions-header { 48.video-subscriptions-header {
@@ -50,32 +50,22 @@ input[type=text] {
50} 50}
51 51
52@media screen and (max-width: $small-view) { 52@media screen and (max-width: $small-view) {
53 .video-channel { 53 .video-subscriptions-header input[type=text] {
54 .video-channel-info { 54 width: 100% !important;
55 padding-bottom: 10px;
56 text-align: center;
57
58 .video-channel-names {
59 flex-direction: column;
60 align-items: center !important;
61 margin: auto;
62 }
63 }
64
65 img {
66 margin-right: 0;
67 }
68 } 55 }
69}
70 56
71@media screen and (max-width: $mobile-view) { 57 .video-channel-info {
72 .video-subscriptions-header { 58 padding-bottom: 10px;
73 flex-direction: column; 59 text-align: center;
74 60
75 input[type=text] { 61 .video-channel-names {
76 width: 100% !important; 62 flex-direction: column;
63 align-items: center !important;
64 margin: auto;
77 } 65 }
78 } 66 }
79}
80
81 67
68 img {
69 margin-right: 0;
70 }
71}
diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.html b/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.html
index a97b2b4fb..e7e3c17b3 100644
--- a/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.html
+++ b/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.html
@@ -1,6 +1,6 @@
1<div class="row"> 1<div class="root">
2 2
3 <div class="playlist-info col-xs-12 col-md-5 col-xl-3"> 3 <div class="playlist-info">
4 <my-video-playlist-miniature 4 <my-video-playlist-miniature
5 *ngIf="playlist" [playlist]="playlist" [toManage]="false" [displayChannel]="true" 5 *ngIf="playlist" [playlist]="playlist" [toManage]="false" [displayChannel]="true"
6 [displayDescription]="true" [displayPrivacy]="true" 6 [displayDescription]="true" [displayPrivacy]="true"
@@ -20,7 +20,7 @@
20 20
21 </div> 21 </div>
22 22
23 <div class="playlist-elements col-xs-12 col-md-7 col-xl-9"> 23 <div class="playlist-elements">
24 <div class="no-results" *ngIf="pagination.totalItems === 0"> 24 <div class="no-results" *ngIf="pagination.totalItems === 0">
25 <div i18n>No videos in this playlist.</div> 25 <div i18n>No videos in this playlist.</div>
26 26
diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.scss b/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.scss
index de7e1993f..0c68dedf6 100644
--- a/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.scss
+++ b/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.scss
@@ -2,21 +2,25 @@
2@import '_mixins'; 2@import '_mixins';
3@import '_miniature'; 3@import '_miniature';
4 4
5.root {
6 display: grid;
7 grid-template-columns: auto 1fr;
8}
9
5.playlist-info { 10.playlist-info {
6 background-color: pvar(--submenuColor); 11 grid-column: 1;
7 margin-left: -$not-expanded-horizontal-margins; 12 background-color: pvar(--submenuBackgroundColor);
13 margin-left: calc(#{pvar(--horizontalMarginContent)} * -1);
8 margin-top: -$sub-menu-margin-bottom; 14 margin-top: -$sub-menu-margin-bottom;
9 15
10 padding: 10px; 16 padding: 15px;
11 17
12 display: flex; 18 display: flex;
13 flex-direction: column; 19 flex-direction: column;
14 justify-content: flex-start;
15 align-items: center;
16 20
17 /* fix ellipsis dots background color */ 21 /* fix ellipsis dots background color */
18 ::ng-deep .miniature-name::after { 22 ::ng-deep .miniature-name::after {
19 background-color: pvar(--submenuColor) !important; 23 background-color: pvar(--submenuBackgroundColor) !important;
20 } 24 }
21} 25}
22 26
@@ -59,15 +63,35 @@
59 transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); 63 transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
60} 64}
61 65
62@media screen and (max-width: $small-view) { 66.playlist-elements {
67 grid-column: 2;
68}
69
70my-video-playlist-miniature {
71 width: $video-thumbnail-width;
72}
73
74@include on-small-main-col {
75 my-video-playlist-miniature {
76 width: $video-thumbnail-medium-width;
77 }
78}
79
80@include on-mobile-main-col {
81 .root {
82 display: block;
83 }
84
63 .playlist-info { 85 .playlist-info {
64 width: 100vw; 86 width: calc(100% + (2 * var(--horizontalMarginContent)));
65 padding-top: 20px; 87 padding-top: 20px;
66 margin-left: calc(#{var(--expanded-horizontal-margin-content)} * -1); 88 margin-bottom: 10px;
67 } 89 }
68 90
69 .playlist-elements { 91 my-video-playlist-miniature,
70 padding: 0 !important; 92 .playlist-buttons {
93 margin-left: auto;
94 margin-right: auto;
71 } 95 }
72 96
73 ::ng-deep my-video-playlist-element-miniature { 97 ::ng-deep my-video-playlist-element-miniature {
diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html b/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html
index afcf6a084..b88ea3db7 100644
--- a/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html
+++ b/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html
@@ -1,8 +1,6 @@
1<h1> 1<h1>
2 <span> 2 <my-global-icon iconName="playlists" aria-hidden="true"></my-global-icon>
3 <my-global-icon iconName="playlists" aria-hidden="true"></my-global-icon> 3 <ng-container i18n>My playlists</ng-container> <span class="badge badge-secondary">{{ pagination.totalItems }}</span>
4 <ng-container i18n>My playlists</ng-container> <span class="badge badge-secondary">{{ pagination.totalItems }}</span>
5 </span>
6</h1> 4</h1>
7 5
8<div class="video-playlists-header d-flex justify-content-between"> 6<div class="video-playlists-header d-flex justify-content-between">
@@ -21,10 +19,10 @@
21 19
22<div class="video-playlists" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()"> 20<div class="video-playlists" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()">
23 <div *ngFor="let playlist of videoPlaylists" class="video-playlist"> 21 <div *ngFor="let playlist of videoPlaylists" class="video-playlist">
24 <div class="miniature-wrapper"> 22 <my-video-playlist-miniature
25 <my-video-playlist-miniature [playlist]="playlist" [toManage]="true" [displayChannel]="true" [displayDescription]="true" [displayPrivacy]="true" 23 [playlist]="playlist" [toManage]="true" [displayChannel]="true"
26 ></my-video-playlist-miniature> 24 [displayDescription]="true" [displayPrivacy]="true" [displayAsRow]="true"
27 </div> 25 ></my-video-playlist-miniature>
28 26
29 <div *ngIf="isRegularPlaylist(playlist)" class="video-playlist-buttons"> 27 <div *ngIf="isRegularPlaylist(playlist)" class="video-playlist-buttons">
30 <my-delete-button label (click)="deleteVideoPlaylist(playlist)"></my-delete-button> 28 <my-delete-button label (click)="deleteVideoPlaylist(playlist)"></my-delete-button>
diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.scss b/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.scss
index 2b7c88246..94187efd4 100644
--- a/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.scss
+++ b/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.scss
@@ -1,6 +1,10 @@
1@import '_variables'; 1@import '_variables';
2@import '_mixins'; 2@import '_mixins';
3 3
4h1 {
5 display: flex;
6}
7
4.create-button { 8.create-button {
5 @include create-button; 9 @include create-button;
6} 10}
@@ -9,64 +13,45 @@ input[type=text] {
9 @include peertube-input-text(300px); 13 @include peertube-input-text(300px);
10} 14}
11 15
12::ng-deep .action-button {
13 &.action-button-delete {
14 margin-right: 10px;
15 }
16}
17
18.video-playlist { 16.video-playlist {
19 @include row-blocks; 17 @include row-blocks($column-responsive: false);
20 18}
21 .miniature-wrapper {
22 flex-grow: 1;
23
24 ::ng-deep .miniature {
25 display: flex;
26
27 .miniature-info {
28 margin-left: 10px;
29 width: auto;
30 }
31 }
32 }
33 19
34 .video-playlist-buttons { 20.video-playlist-buttons {
35 min-width: 190px; 21 display: flex;
36 height: max-content; 22 margin-left: 10px;
37 } 23 align-self: flex-end;
38} 24}
39 25
40.video-playlists-header { 26.video-playlists-header {
41 margin-bottom: 30px; 27 margin-bottom: 30px;
42} 28}
43 29
44@media screen and (max-width: $small-view) { 30my-video-playlist-miniature {
31 display: block;
32 flex-grow: 1;
33}
34
35my-delete-button {
36 margin-right: 10px;
37}
38
39@include on-small-main-col {
45 .video-playlists-header { 40 .video-playlists-header {
46 text-align: center; 41 text-align: center;
47 } 42 }
48 43
49 .video-playlist { 44 .video-playlist {
50 45 flex-wrap: wrap;
51 .video-playlist-buttons {
52 margin-top: 10px;
53 }
54 } 46 }
55 47
56 my-video-playlist-miniature ::ng-deep .miniature { 48 .video-playlist-buttons {
57 flex-direction: column; 49 margin-top: 10px;
58 50 margin-left: auto;
59 .miniature-info {
60 margin-left: 0 !important;
61 }
62
63 .miniature-name {
64 max-width: $video-thumbnail-width;
65 }
66 } 51 }
67} 52}
68 53
69@media screen and (max-width: $mobile-view) { 54@include on-mobile-main-col {
70 .video-playlists-header { 55 .video-playlists-header {
71 flex-direction: column; 56 flex-direction: column;
72 57
@@ -75,4 +60,8 @@ input[type=text] {
75 margin-bottom: 12px; 60 margin-bottom: 12px;
76 } 61 }
77 } 62 }
63
64 .action-button {
65 margin-left: 0;
66 }
78} 67}
diff --git a/client/src/app/+my-library/my-videos/my-videos.component.html b/client/src/app/+my-library/my-videos/my-videos.component.html
index 5fa4c02ec..e9f436378 100644
--- a/client/src/app/+my-library/my-videos/my-videos.component.html
+++ b/client/src/app/+my-library/my-videos/my-videos.component.html
@@ -25,6 +25,17 @@
25 <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a> 25 <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetSearch()"></a>
26 <span class="sr-only" i18n>Clear filters</span> 26 <span class="sr-only" i18n>Clear filters</span>
27 </div> 27 </div>
28
29 <div class="peertube-select-container peertube-select-button">
30 <select [(ngModel)]="sort" (ngModelChange)="onChangeSortColumn()" class="form-control">
31 <option value="undefined" disabled>Sort by</option>
32 <option value="-publishedAt" i18n>Last published first</option>
33 <option value="-createdAt" i18n>Last created first</option>
34 <option value="-views" i18n>Most viewed first</option>
35 <option value="-likes" i18n>Most liked first</option>
36 <option value="-duration" i18n>Longest first</option>
37 </select>
38 </div>
28</div> 39</div>
29 40
30<my-videos-selection 41<my-videos-selection
@@ -34,7 +45,6 @@
34 [miniatureDisplayOptions]="miniatureDisplayOptions" 45 [miniatureDisplayOptions]="miniatureDisplayOptions"
35 [titlePage]="titlePage" 46 [titlePage]="titlePage"
36 [getVideosObservableFunction]="getVideosObservableFunction" 47 [getVideosObservableFunction]="getVideosObservableFunction"
37 [ownerDisplayType]="ownerDisplayType"
38 [user]="user" 48 [user]="user"
39 #videosSelection 49 #videosSelection
40> 50>
diff --git a/client/src/app/+my-library/my-videos/my-videos.component.scss b/client/src/app/+my-library/my-videos/my-videos.component.scss
index 59fc5fe80..aaf21126b 100644
--- a/client/src/app/+my-library/my-videos/my-videos.component.scss
+++ b/client/src/app/+my-library/my-videos/my-videos.component.scss
@@ -5,6 +5,11 @@ input[type=text] {
5 @include peertube-input-text(300px); 5 @include peertube-input-text(300px);
6} 6}
7 7
8.peertube-select-container {
9 @include peertube-select-container(auto);
10 margin-left: 0.5rem;
11}
12
8h1 { 13h1 {
9 display: flex; 14 display: flex;
10 justify-content: space-between; 15 justify-content: space-between;
@@ -32,36 +37,9 @@ h1 {
32 } 37 }
33} 38}
34 39
35::ng-deep {
36 .video {
37 flex-wrap: wrap;
38 }
39
40 .action-button span {
41 white-space: nowrap;
42 }
43
44 .video-miniature {
45 &.display-as-row {
46 // width: min-content !important;
47 width: 100% !important;
48
49 .video-bottom .video-miniature-information {
50 width: max-content !important;
51 min-width: unset !important;
52 }
53 }
54
55 .video-bottom {
56 max-width: 350px;
57 }
58 }
59}
60
61.action-button { 40.action-button {
62 display: flex; 41 display: flex;
63 margin-left: 55px; 42 margin-left: 10px;
64 margin-top: 10px;
65 align-self: flex-end; 43 align-self: flex-end;
66} 44}
67 45
@@ -69,7 +47,7 @@ my-edit-button {
69 margin-right: 10px; 47 margin-right: 10px;
70} 48}
71 49
72@media screen and (max-width: $small-view) { 50@include on-small-main-col {
73 h1 { 51 h1 {
74 flex-direction: column; 52 flex-direction: column;
75 53
@@ -80,59 +58,25 @@ my-edit-button {
80 } 58 }
81 59
82 .action-button { 60 .action-button {
83 flex-direction: column; 61 margin-top: 10px;
84 align-self: center; 62 margin-left: auto;
85 align-items: center;
86 margin-left: 0px;
87 }
88
89 my-edit-button {
90 margin: 15px 0 5px 0;
91 width: 100%;
92 text-align: center;
93
94 ::ng-deep {
95 .action-button {
96 /* same width than a.video-thumbnail */
97 width: $video-thumbnail-width;
98 }
99 }
100 }
101
102 ::ng-deep {
103 .video-miniature {
104 align-items: center;
105
106 .video-bottom,
107 .video-bottom .video-miniature-information {
108 /* same width than a.video-thumbnail */
109 max-width: $video-thumbnail-width !important;
110 }
111 }
112 } 63 }
113} 64}
114 65
115// Adapt my-video-miniature on small screens with menu 66@include on-mobile-main-col {
116@media screen and (min-width: $small-view) and (max-width: #{breakpoint(lg) + ($not-expanded-horizontal-margins / 3) * 2}) {
117 :host-context(.main-col:not(.expanded)) {
118 ::ng-deep {
119 .video-miniature {
120 flex-direction: column;
121
122 .video-miniature-name {
123 max-width: $video-thumbnail-width;
124 }
125 }
126 }
127 }
128}
129
130@media screen and (max-width: $mobile-view) {
131 .videos-header { 67 .videos-header {
132 flex-direction: column; 68 flex-direction: column;
133 69
134 input[type=text] { 70 input[type=text] {
135 width: 100% !important; 71 width: 100%;
72 margin-bottom: 12px;
73 }
74 .peertube-select-container {
75 margin-left: 0;
136 } 76 }
137 } 77 }
78
79 .action-button {
80 margin-left: 0;
81 }
138} 82}
diff --git a/client/src/app/+my-library/my-videos/my-videos.component.ts b/client/src/app/+my-library/my-videos/my-videos.component.ts
index 6a2a62608..356e158d6 100644
--- a/client/src/app/+my-library/my-videos/my-videos.component.ts
+++ b/client/src/app/+my-library/my-videos/my-videos.component.ts
@@ -2,12 +2,12 @@ import { concat, Observable, Subject } from 'rxjs'
2import { debounceTime, tap, toArray } from 'rxjs/operators' 2import { debounceTime, tap, toArray } from 'rxjs/operators'
3import { Component, OnInit, ViewChild } from '@angular/core' 3import { Component, OnInit, ViewChild } from '@angular/core'
4import { ActivatedRoute, Router } from '@angular/router' 4import { ActivatedRoute, Router } from '@angular/router'
5import { AuthService, ComponentPagination, ConfirmService, Notifier, ScreenService, ServerService, User, UserService } from '@app/core' 5import { AuthService, ComponentPagination, ConfirmService, Notifier, ScreenService, ServerService, User } from '@app/core'
6import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' 6import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook'
7import { immutableAssign } from '@app/helpers' 7import { immutableAssign } from '@app/helpers'
8import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' 8import { DropdownAction, Video, VideoService } from '@app/shared/shared-main'
9import { LiveStreamInformationComponent } from '@app/shared/shared-video-live' 9import { LiveStreamInformationComponent } from '@app/shared/shared-video-live'
10import { MiniatureDisplayOptions, OwnerDisplayType, SelectionType, VideosSelectionComponent } from '@app/shared/shared-video-miniature' 10import { MiniatureDisplayOptions, SelectionType, VideosSelectionComponent } from '@app/shared/shared-video-miniature'
11import { VideoSortField } from '@shared/models' 11import { VideoSortField } from '@shared/models'
12import { VideoChangeOwnershipComponent } from './modals/video-change-ownership.component' 12import { VideoChangeOwnershipComponent } from './modals/video-change-ownership.component'
13 13
@@ -36,7 +36,6 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
36 state: true, 36 state: true,
37 blacklistInfo: true 37 blacklistInfo: true
38 } 38 }
39 ownerDisplayType: OwnerDisplayType = 'videoChannel'
40 39
41 videoActions: DropdownAction<{ video: Video }>[] = [] 40 videoActions: DropdownAction<{ video: Video }>[] = []
42 41
@@ -44,6 +43,7 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
44 videosSearch: string 43 videosSearch: string
45 videosSearchChanged = new Subject<string>() 44 videosSearchChanged = new Subject<string>()
46 getVideosObservableFunction = this.getVideosObservable.bind(this) 45 getVideosObservableFunction = this.getVideosObservable.bind(this)
46 sort: VideoSortField = '-publishedAt'
47 47
48 user: User 48 user: User
49 49
@@ -81,6 +81,10 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
81 this.videosSearchChanged.next() 81 this.videosSearchChanged.next()
82 } 82 }
83 83
84 onChangeSortColumn () {
85 this.videosSelection.reloadVideos()
86 }
87
84 disableForReuse () { 88 disableForReuse () {
85 this.videosSelection.disableForReuse() 89 this.videosSelection.disableForReuse()
86 } 90 }
@@ -89,10 +93,10 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
89 this.videosSelection.enabledForReuse() 93 this.videosSelection.enabledForReuse()
90 } 94 }
91 95
92 getVideosObservable (page: number, sort: VideoSortField) { 96 getVideosObservable (page: number) {
93 const newPagination = immutableAssign(this.pagination, { currentPage: page }) 97 const newPagination = immutableAssign(this.pagination, { currentPage: page })
94 98
95 return this.videoService.getMyVideos(newPagination, sort, this.videosSearch) 99 return this.videoService.getMyVideos(newPagination, this.sort, this.videosSearch)
96 .pipe( 100 .pipe(
97 tap(res => this.pagination.totalItems = res.total) 101 tap(res => this.pagination.totalItems = res.total)
98 ) 102 )
diff --git a/client/src/app/+search/search.component.html b/client/src/app/+search/search.component.html
index 84be4fb14..65d4b6ecd 100644
--- a/client/src/app/+search/search.component.html
+++ b/client/src/app/+search/search.component.html
@@ -2,14 +2,12 @@
2 <div class="results-header"> 2 <div class="results-header">
3 <div class="first-line"> 3 <div class="first-line">
4 <div class="results-counter" *ngIf="pagination.totalItems"> 4 <div class="results-counter" *ngIf="pagination.totalItems">
5 <span i18n>{{ pagination.totalItems | myNumberFormatter }} {pagination.totalItems, plural, =1 {result} other {results}} </span> 5 <span class="mr-1" i18n>{{ pagination.totalItems | myNumberFormatter }} {pagination.totalItems, plural, =1 {result} other {results}}</span>
6 6
7 <span i18n *ngIf="advancedSearch.searchTarget === 'local'">on this instance</span> 7 <span class="mr-1" i18n *ngIf="advancedSearch.searchTarget === 'local'">on this instance</span>
8 <span i18n *ngIf="advancedSearch.searchTarget === 'search-index'">on the vidiverse</span> 8 <span class="mr-1" i18n *ngIf="advancedSearch.searchTarget === 'search-index'">on the vidiverse</span>
9 9
10 <span *ngIf="currentSearch" i18n> 10 <span *ngIf="currentSearch" i18n>for <span class="search-value">{{ currentSearch }}</span></span>
11 for <span class="search-value">{{ currentSearch }}</span>
12 </span>
13 </div> 11 </div>
14 12
15 <div 13 <div
@@ -35,11 +33,11 @@
35 33
36 <ng-container *ngFor="let result of results"> 34 <ng-container *ngFor="let result of results">
37 <div *ngIf="isVideoChannel(result)" class="entry video-channel"> 35 <div *ngIf="isVideoChannel(result)" class="entry video-channel">
38 <a *ngIf="!isExternalChannelUrl()" [routerLink]="getChannelUrl(result)"> 36 <a class="link-avatar" *ngIf="!isExternalChannelUrl()" [routerLink]="getChannelUrl(result)">
39 <img [src]="result.avatarUrl" alt="Avatar" /> 37 <img [src]="result.avatarUrl" alt="Avatar" />
40 </a> 38 </a>
41 39
42 <a *ngIf="isExternalChannelUrl()" [href]="getChannelUrl(result)" target="_blank"> 40 <a class="link-avatar" *ngIf="isExternalChannelUrl()" [href]="getChannelUrl(result)" target="_blank">
43 <img [src]="result.avatarUrl" alt="Avatar" /> 41 <img [src]="result.avatarUrl" alt="Avatar" />
44 </a> 42 </a>
45 43
diff --git a/client/src/app/+search/search.component.scss b/client/src/app/+search/search.component.scss
index 64927fa4b..91c8272d7 100644
--- a/client/src/app/+search/search.component.scss
+++ b/client/src/app/+search/search.component.scss
@@ -1,159 +1,122 @@
1@import '_variables'; 1@import '_variables';
2@import '_mixins'; 2@import '_mixins';
3 3
4@mixin build-channel-img-size ($video-img-width) {
5 $image-size: min(130px, $video-img-width);
6 $margin-size: ($video-img-width - $image-size) / 2; // So we have the same width than the video miniature
7
8 @include channel-avatar($image-size);
9
10 margin: 0 $margin-size 0 $margin-size;
11}
12
4.search-result { 13.search-result {
5 padding: 40px; 14 padding: 40px;
15}
6 16
7 .results-header { 17.results-header {
8 font-size: 16px; 18 font-size: 16px;
9 padding-bottom: 20px; 19 padding-bottom: 20px;
10 margin-bottom: 30px; 20 margin-bottom: 30px;
11 border-bottom: 1px solid #DADADA; 21 border-bottom: 1px solid #DADADA;
12 22
13 .first-line { 23 .first-line {
14 display: flex; 24 display: flex;
15 flex-direction: row; 25 flex-direction: row;
16 26
17 .results-counter { 27 .results-counter {
18 flex-grow: 1; 28 flex-grow: 1;
19 29
20 .search-value { 30 .search-value {
21 font-weight: $font-semibold; 31 font-weight: $font-semibold;
22 }
23 } 32 }
33 }
24 34
25 .results-filter-button { 35 .results-filter-button {
26 cursor: pointer; 36 cursor: pointer;
27 37
28 .icon.icon-filter { 38 .icon.icon-filter {
29 @include icon(20px); 39 @include icon(20px);
30 40
31 position: relative; 41 position: relative;
32 top: -1px; 42 top: -1px;
33 margin-right: 5px; 43 margin-right: 5px;
34 background-image: url('../../assets/images/feather/filter.svg'); 44 background-image: url('../../assets/images/feather/filter.svg');
35 }
36 } 45 }
37 } 46 }
38 } 47 }
48}
39 49
40 .entry { 50.entry {
41 display: flex; 51 display: flex;
42 min-height: 130px; 52 margin-bottom: 40px;
43 padding-bottom: 20px; 53 max-width: 800px;
44 margin-bottom: 20px; 54}
45 55
46 &.video-channel { 56.video-channel {
47 img { 57 img {
48 $image-size: 130px; 58 @include build-channel-img-size($video-thumbnail-width);
49 $margin-size: ($video-thumbnail-width - $image-size) / 2; // So we have the same width than the video miniature 59 }
60}
50 61
51 @include avatar($image-size); 62.video-channel-info {
63 flex-grow: 1;
64 margin: 0 10px;
65 width: fit-content;
66}
52 67
53 margin: 0 ($margin-size + 10) 0 $margin-size; 68.video-channel-names {
54 } 69 @include disable-default-a-behaviour;
55 70
56 .video-channel-info { 71 display: flex;
57 flex-grow: 1; 72 align-items: baseline;
58 width: fit-content; 73 color: pvar(--mainForegroundColor);
59 74 width: fit-content;
60 .video-channel-names {
61 @include disable-default-a-behaviour;
62
63 display: flex;
64 align-items: baseline;
65 color: pvar(--mainForegroundColor);
66 width: fit-content;
67
68 .video-channel-display-name {
69 font-weight: $font-semibold;
70 font-size: 18px;
71 }
72
73 .video-channel-name {
74 font-size: 14px;
75 color: $grey-actor-name;
76 margin-left: 5px;
77 }
78 }
79 }
80 }
81 }
82} 75}
83 76
84@media screen and (min-width: $small-view) and (max-width: breakpoint(xl)) { 77.video-channel-display-name {
85 .video-channel-info .video-channel-names { 78 font-weight: $font-semibold;
86 flex-direction: column !important; 79 font-size: $video-miniature-row-name-font-size;
80}
87 81
88 .video-channel-name { 82.video-channel-name {
89 @include ellipsis; // Ellipsis and max-width on channel-name to not break screen 83 font-size: $video-miniature-row-info-font-size;
84 color: pvar(--greyForegroundColor);
85 margin-left: 5px;
86}
90 87
91 max-width: 250px; 88// Use the same breakpoints than in video-miniature
92 margin-left: 0 !important; 89@include on-small-main-col {
93 } 90 .video-channel {
94 } 91 display: grid;
92 grid-template-columns: auto 1fr;
93 grid-template-rows: auto auto;
95 94
96 :host-context(.main-col:not(.expanded)) { 95 .link-avatar {
97 // Override the min-width: 500px to not break screen 96 grid-column: 1;
98 ::ng-deep .video-miniature-information { 97 grid-row: 1 / -1;
99 min-width: 300px !important;
100 } 98 }
101 }
102}
103 99
104@media screen and (min-width: $small-view) and (max-width: breakpoint(lg)) { 100 img {
105 :host-context(.main-col:not(.expanded)) { 101 @include build-channel-img-size($video-thumbnail-medium-width);
106 .video-channel-info .video-channel-names {
107 .video-channel-name {
108 max-width: 160px;
109 }
110 } 102 }
103 }
111 104
112 // Override the min-width: 500px to not break screen 105 .video-channel-info {
113 ::ng-deep .video-miniature-information { 106 grid-column: 2;
114 min-width: $video-thumbnail-width !important; 107 grid-row: 1;
115 }
116 } 108 }
117 109
118 :host-context(.expanded) { 110 my-subscribe-button {
119 // Override the min-width: 500px to not break screen 111 grid-column: 2;
120 ::ng-deep .video-miniature-information { 112 grid-row: 2;
121 min-width: 300px !important; 113 align-self: end;
122 }
123 } 114 }
124} 115}
125 116
126@media screen and (max-width: $small-view) { 117@include on-mobile-main-col {
127 .search-result { 118 .video-channel img {
128 .entry.video-channel, 119 @include build-channel-img-size($video-thumbnail-small-width);
129 .entry.video {
130 flex-direction: column;
131 height: auto;
132 justify-content: center;
133 align-items: center;
134 text-align: center;
135
136 img {
137 margin: 0;
138 }
139
140 img {
141 margin: 0;
142 }
143
144 .video-channel-info .video-channel-names {
145 align-items: center;
146 flex-direction: column !important;
147
148 .video-channel-name {
149 margin-left: 0 !important;
150 }
151 }
152
153 my-subscribe-button {
154 margin-top: 5px;
155 }
156 }
157 } 120 }
158} 121}
159 122
@@ -164,28 +127,13 @@
164 .results-header { 127 .results-header {
165 font-size: 15px !important; 128 font-size: 15px !important;
166 } 129 }
130 }
167 131
168 .entry { 132 .video-channel-display-name {
169 &.video { 133 font-size: $video-miniature-row-mobile-name-font-size;
170 .video-info-name, 134 }
171 .video-info-account { 135
172 margin: auto; 136 .video-channel-name {
173 } 137 font-size: $video-miniature-row-mobile-info-font-size;
174
175 my-video-thumbnail {
176 margin-right: 0 !important;
177
178 ::ng-deep .video-thumbnail {
179 width: 100%;
180 height: auto;
181
182 img {
183 width: 100%;
184 height: auto;
185 }
186 }
187 }
188 }
189 }
190 } 138 }
191} 139}
diff --git a/client/src/app/+video-channels/video-channel-about/video-channel-about.component.html b/client/src/app/+video-channels/video-channel-about/video-channel-about.component.html
deleted file mode 100644
index 8dff8ba91..000000000
--- a/client/src/app/+video-channels/video-channel-about/video-channel-about.component.html
+++ /dev/null
@@ -1,22 +0,0 @@
1<div class="margin-content">
2 <div *ngIf="videoChannel" class="row no-gutters">
3 <div class="description col-md-6 col-sm-12 pr-2">
4 <div class="block">
5 <div i18n class="small-title">DESCRIPTION</div>
6 <div class="content" [innerHtml]="getVideoChannelDescription()"></div>
7 </div>
8
9 <div class="block" *ngIf="supportHTML">
10 <div i18n class="small-title">SUPPORT THIS CHANNEL</div>
11 <div class="content" [innerHtml]="supportHTML"></div>
12 </div>
13 </div>
14
15 <div class="stats col-md-6 col-sm-12">
16 <div class="block">
17 <div i18n class="small-title">STATS</div>
18 <div i18n class="content">Created {{ videoChannel.createdAt | date }}</div>
19 </div>
20 </div>
21 </div>
22</div>
diff --git a/client/src/app/+video-channels/video-channel-about/video-channel-about.component.scss b/client/src/app/+video-channels/video-channel-about/video-channel-about.component.scss
deleted file mode 100644
index 5bcd4b561..000000000
--- a/client/src/app/+video-channels/video-channel-about/video-channel-about.component.scss
+++ /dev/null
@@ -1,12 +0,0 @@
1@import '_variables';
2@import '_mixins';
3
4.block {
5 margin-bottom: 40px;
6
7 .small-title {
8 @include in-content-small-title;
9
10 margin-bottom: 20px;
11 }
12}
diff --git a/client/src/app/+video-channels/video-channel-about/video-channel-about.component.ts b/client/src/app/+video-channels/video-channel-about/video-channel-about.component.ts
deleted file mode 100644
index 537c7d08e..000000000
--- a/client/src/app/+video-channels/video-channel-about/video-channel-about.component.ts
+++ /dev/null
@@ -1,43 +0,0 @@
1import { Subscription } from 'rxjs'
2import { Component, OnDestroy, OnInit } from '@angular/core'
3import { MarkdownService } from '@app/core'
4import { VideoChannel, VideoChannelService } from '@app/shared/shared-main'
5
6@Component({
7 selector: 'my-video-channel-about',
8 templateUrl: './video-channel-about.component.html',
9 styleUrls: [ './video-channel-about.component.scss' ]
10})
11export class VideoChannelAboutComponent implements OnInit, OnDestroy {
12 videoChannel: VideoChannel
13 descriptionHTML = ''
14 supportHTML = ''
15
16 private videoChannelSub: Subscription
17
18 constructor (
19 private videoChannelService: VideoChannelService,
20 private markdownService: MarkdownService
21 ) { }
22
23 ngOnInit () {
24 // Parent get the video channel for us
25 this.videoChannelSub = this.videoChannelService.videoChannelLoaded
26 .subscribe(async videoChannel => {
27 this.videoChannel = videoChannel
28
29 this.descriptionHTML = await this.markdownService.textMarkdownToHTML(this.videoChannel.description)
30 this.supportHTML = await this.markdownService.enhancedMarkdownToHTML(this.videoChannel.support)
31 })
32 }
33
34 ngOnDestroy () {
35 if (this.videoChannelSub) this.videoChannelSub.unsubscribe()
36 }
37
38 getVideoChannelDescription () {
39 if (this.descriptionHTML) return this.descriptionHTML
40
41 return $localize`No description`
42 }
43}
diff --git a/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.html b/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.html
index 03770ceec..b69d1682a 100644
--- a/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.html
+++ b/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.html
@@ -1,13 +1,13 @@
1<div class="margin-content"> 1<div class="margin-content">
2 <div i18n class="title-page title-page-single"> 2 <div i18n class="title-page title-page-single" *ngIf="pagination.totalItems">
3 Created {{ pagination.totalItems }} playlists 3 Created {pagination.totalItems, plural, =1 {1 playlist} other {{{ pagination.totalItems }} playlists}}
4 </div> 4 </div>
5 5
6 <div i18n class="no-results" *ngIf="pagination.totalItems === 0">This channel does not have playlists.</div> 6 <div i18n class="no-results" *ngIf="pagination.totalItems === 0">This channel does not have playlists.</div>
7 7
8 <div class="video-playlist" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" [dataObservable]="onDataSubject.asObservable()"> 8 <div class="playlists" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" [dataObservable]="onDataSubject.asObservable()">
9 <div *ngFor="let playlist of videoPlaylists" class="playlist-miniature-container"> 9 <div *ngFor="let playlist of videoPlaylists" class="playlist-wrapper">
10 <my-video-playlist-miniature [playlist]="playlist" [toManage]="false"></my-video-playlist-miniature> 10 <my-video-playlist-miniature [playlist]="playlist" [toManage]="false" [displayAsRow]="displayAsRow()"></my-video-playlist-miniature>
11 </div> 11 </div>
12 </div> 12 </div>
13</div> 13</div>
diff --git a/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.scss b/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.scss
index cb2931858..acd2e409e 100644
--- a/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.scss
+++ b/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.scss
@@ -1,14 +1,32 @@
1.title-page { 1@import '_variables';
2 margin-top: 0; 2@import '_mixins';
3} 3@import '_miniature';
4 4
5.video-playlist { 5.playlists {
6 display: flex; 6 display: flex;
7 flex-wrap: wrap; 7 flex-wrap: wrap;
8 justify-content: center; 8 justify-content: center;
9 9
10 .playlist-miniature-container { 10 .playlist-wrapper {
11 margin-right: 15px; 11 margin-right: 15px;
12 margin-bottom: 30px; 12 margin-bottom: 30px;
13 } 13 }
14} 14}
15
16.margin-content {
17 @include grid-videos-miniature-layout;
18}
19
20@media screen and (max-width: $mobile-view) {
21 .title-page {
22 display: block;
23 text-align: center;
24 }
25
26 .playlists {
27 justify-content: left;
28
29 margin-left: pvar(--horizontalMarginContent) !important;
30 margin-right: pvar(--horizontalMarginContent) !important;
31 }
32}
diff --git a/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts b/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts
index 8b507c626..14465bb8d 100644
--- a/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts
+++ b/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts
@@ -1,6 +1,6 @@
1import { Subject, Subscription } from 'rxjs' 1import { Subject, Subscription } from 'rxjs'
2import { Component, OnDestroy, OnInit } from '@angular/core' 2import { Component, OnDestroy, OnInit } from '@angular/core'
3import { ComponentPagination, hasMoreItems } from '@app/core' 3import { ComponentPagination, hasMoreItems, ScreenService } from '@app/core'
4import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' 4import { VideoChannel, VideoChannelService } from '@app/shared/shared-main'
5import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' 5import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist'
6 6
@@ -25,7 +25,8 @@ export class VideoChannelPlaylistsComponent implements OnInit, OnDestroy {
25 25
26 constructor ( 26 constructor (
27 private videoPlaylistService: VideoPlaylistService, 27 private videoPlaylistService: VideoPlaylistService,
28 private videoChannelService: VideoChannelService 28 private videoChannelService: VideoChannelService,
29 private screenService: ScreenService
29 ) {} 30 ) {}
30 31
31 ngOnInit () { 32 ngOnInit () {
@@ -48,6 +49,10 @@ export class VideoChannelPlaylistsComponent implements OnInit, OnDestroy {
48 this.loadVideoPlaylists() 49 this.loadVideoPlaylists()
49 } 50 }
50 51
52 displayAsRow () {
53 return this.screenService.isInMobileView()
54 }
55
51 private loadVideoPlaylists () { 56 private loadVideoPlaylists () {
52 this.videoPlaylistService.listChannelPlaylists(this.videoChannel, this.pagination) 57 this.videoPlaylistService.listChannelPlaylists(this.videoChannel, this.pagination)
53 .subscribe(res => { 58 .subscribe(res => {
diff --git a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts
index 803651505..d83fc1324 100644
--- a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts
+++ b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts
@@ -5,7 +5,7 @@ import { ActivatedRoute, Router } from '@angular/router'
5import { AuthService, ConfirmService, LocalStorageService, Notifier, ScreenService, ServerService, UserService } from '@app/core' 5import { AuthService, ConfirmService, LocalStorageService, Notifier, ScreenService, ServerService, UserService } from '@app/core'
6import { immutableAssign } from '@app/helpers' 6import { immutableAssign } from '@app/helpers'
7import { VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main' 7import { VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main'
8import { AbstractVideoList } from '@app/shared/shared-video-miniature' 8import { AbstractVideoList, MiniatureDisplayOptions } from '@app/shared/shared-video-miniature'
9import { VideoFilter } from '@shared/models' 9import { VideoFilter } from '@shared/models'
10 10
11@Component({ 11@Component({
@@ -16,12 +16,24 @@ import { VideoFilter } from '@shared/models'
16 ] 16 ]
17}) 17})
18export class VideoChannelVideosComponent extends AbstractVideoList implements OnInit, OnDestroy { 18export class VideoChannelVideosComponent extends AbstractVideoList implements OnInit, OnDestroy {
19 // No value because we don't want a page title
19 titlePage: string 20 titlePage: string
20 loadOnInit = false 21 loadOnInit = false
21 loadUserVideoPreferences = true 22 loadUserVideoPreferences = true
22 23
23 filter: VideoFilter = null 24 filter: VideoFilter = null
24 25
26 displayOptions: MiniatureDisplayOptions = {
27 date: true,
28 views: true,
29 by: false,
30 avatar: false,
31 privacyLabel: true,
32 privacyText: false,
33 state: false,
34 blacklistInfo: false
35 }
36
25 private videoChannel: VideoChannel 37 private videoChannel: VideoChannel
26 private videoChannelSub: Subscription 38 private videoChannelSub: Subscription
27 39
@@ -83,13 +95,6 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On
83 95
84 return this.videoService 96 return this.videoService
85 .getVideoChannelVideos(options) 97 .getVideoChannelVideos(options)
86 .pipe(
87 tap(({ total }) => {
88 this.titlePage = total === 1
89 ? $localize`Published 1 video`
90 : $localize`Published ${total} videos`
91 })
92 )
93 } 98 }
94 99
95 generateSyndicationList () { 100 generateSyndicationList () {
@@ -101,4 +106,8 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On
101 106
102 this.reloadVideos() 107 this.reloadVideos()
103 } 108 }
109
110 displayAsRow () {
111 return this.screenService.isInMobileView()
112 }
104} 113}
diff --git a/client/src/app/+video-channels/video-channels-routing.module.ts b/client/src/app/+video-channels/video-channels-routing.module.ts
index f8c32f14e..fcaad8934 100644
--- a/client/src/app/+video-channels/video-channels-routing.module.ts
+++ b/client/src/app/+video-channels/video-channels-routing.module.ts
@@ -1,7 +1,6 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { MetaGuard } from '@ngx-meta/core' 3import { MetaGuard } from '@ngx-meta/core'
4import { VideoChannelAboutComponent } from './video-channel-about/video-channel-about.component'
5import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-channel-playlists.component' 4import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-channel-playlists.component'
6import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component' 5import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component'
7import { VideoChannelsComponent } from './video-channels.component' 6import { VideoChannelsComponent } from './video-channels.component'
@@ -38,15 +37,6 @@ const videoChannelsRoutes: Routes = [
38 title: $localize`Video channel playlists` 37 title: $localize`Video channel playlists`
39 } 38 }
40 } 39 }
41 },
42 {
43 path: 'about',
44 component: VideoChannelAboutComponent,
45 data: {
46 meta: {
47 title: $localize`About video channel`
48 }
49 }
50 } 40 }
51 ] 41 ]
52 } 42 }
diff --git a/client/src/app/+video-channels/video-channels.component.html b/client/src/app/+video-channels/video-channels.component.html
index 4b0d12b6e..1312a1b3c 100644
--- a/client/src/app/+video-channels/video-channels.component.html
+++ b/client/src/app/+video-channels/video-channels.component.html
@@ -1,50 +1,129 @@
1<div *ngIf="videoChannel" class="row"> 1<div class="root" *ngIf="videoChannel">
2 <div class="sub-menu"> 2 <div class="banner" *ngIf="videoChannel.bannerUrl">
3 3 <img [src]="videoChannel.bannerUrl" alt="Channel banner">
4 <div class="actor"> 4 </div>
5 <img [src]="videoChannel.avatarUrl" alt="Avatar" /> 5
6 6 <div class="channel-info">
7 <div class="actor-info"> 7
8 <div class="actor-names"> 8 <ng-template #buttonsTemplate>
9 <div class="actor-display-name">{{ videoChannel.displayName }}</div> 9 <a *ngIf="isManageable()" [routerLink]="[ '/my-library/video-channels/update', videoChannel.nameWithHost ]" class="peertube-button-link orange-button" i18n>
10 <div class="actor-name"> 10 Manage channel
11 <span>{{ videoChannel.nameWithHost }}</span> 11 </a>
12 <button [cdkCopyToClipboard]="videoChannel.nameWithHostForced" (click)="activateCopiedMessage()" 12
13 class="btn btn-outline-secondary btn-sm copy-button" 13 <my-subscribe-button *ngIf="!isManageable()" #subscribeButton [videoChannels]="[videoChannel]"></my-subscribe-button>
14 > 14
15 <span class="glyphicon glyphicon-copy"></span> 15 <button *ngIf="videoChannel.support" (click)="showSupportModal()" class="support-button peertube-button orange-button-inverted">
16 </button> 16 <my-global-icon iconName="support" aria-hidden="true"></my-global-icon>
17 <span class="icon-text" i18n>Support</span>
18 </button>
19 </ng-template>
20
21 <ng-template #ownerTemplate>
22 <div class="owner-block">
23 <div class="avatar-row">
24 <a [routerLink]="getAccountUrl()" title="View account" i18n-title>
25 <img class="account-avatar" [src]="videoChannel.ownerAvatarUrl" alt="Owner account avatar" />
26 </a>
27
28 <div class="actor-info">
29 <h4>
30 <a [routerLink]="getAccountUrl()" title="View account" i18n-title>{{ videoChannel.ownerAccount.displayName }}</a>
31 </h4>
32
33 <div class="actor-handle">@{{ videoChannel.ownerBy }}</div>
17 </div> 34 </div>
18 </div> 35 </div>
19 36
20 <div class="right-buttons"> 37 <div class="owner-description">
21 <a *ngIf="isChannelManageable && !isInSmallView" [routerLink]="[ '/my-library/video-channels/update', videoChannel.nameWithHost ]" class="btn btn-outline-tertiary mr-2" i18n> 38 <div class="description-html" [innerHTML]="ownerDescriptionHTML"></div>
22 Manage channel
23 </a>
24 <my-subscribe-button #subscribeButton [videoChannels]="[videoChannel]"></my-subscribe-button>
25 </div> 39 </div>
26 40
27 <div class="actor-lower"> 41 <a class="view-account short" [routerLink]="getAccountUrl()" i18n>
28 <div class="actor-followers" i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div> 42 View account
43 </a>
29 44
30 <a [routerLink]="[ '/accounts', videoChannel.ownerBy ]" i18n-title title="Go the owner account page" class="actor-owner"> 45 <a class="view-account complete" [routerLink]="getAccountUrl()" i18n>
31 <span class="d-inline-flex"><span i18n class="d-none d-sm-block mr-1">Created by</span>{{ videoChannel.ownerBy }}</span> 46 View owner account
32 <img [src]="videoChannel.ownerAvatarUrl" alt="Owner account avatar" /> 47 </a>
33 </a> 48 </div>
49 </ng-template>
50
51 <div class="channel-avatar-row">
52 <img class="channel-avatar" [src]="videoChannel.avatarUrl" alt="Avatar" />
53
54 <div>
55 <div class="section-label" i18n>VIDEO CHANNEL</div>
56
57 <div class="actor-info">
58 <div>
59 <div class="actor-display-name">
60 <h1>{{ videoChannel.displayName }}</h1>
61 </div>
62
63 <div class="actor-handle">
64 <span>@{{ videoChannel.nameWithHost }}</span>
65 <button [cdkCopyToClipboard]="videoChannel.nameWithHostForced" (click)="activateCopiedMessage()"
66 class="btn btn-outline-secondary btn-sm copy-button" title="Copy channel handle" i18n-title
67 >
68 <span class="glyphicon glyphicon-duplicate"></span>
69 </button>
70 </div>
71
72 <div class="actor-counters">
73 <span i18n>{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</span>
74
75 <span class="videos-count" *ngIf="channelVideosCount !== undefined" i18n>
76 {channelVideosCount, plural, =1 {1 videos} other {{{ channelVideosCount }} videos}}
77 </span>
78 </div>
79 </div>
80
81 <div class="channel-buttons right">
82 <ng-template *ngTemplateOutlet="buttonsTemplate"></ng-template>
83 </div>
34 </div> 84 </div>
35 </div> 85 </div>
36 </div> 86 </div>
37 87
38 <div class="links w-100"> 88 <div class="channel-description" [ngClass]="{ expanded: channelDescriptionExpanded }">
39 <ng-template #linkTemplate let-item="item"> 89 <div class="description-html" [innerHTML]="channelDescriptionHTML"></div>
40 <a [routerLink]="item.routerLink" routerLinkActive="active" class="title-page">{{ item.label }}</a>
41 </ng-template>
42 90
43 <list-overflow [items]="links" [itemTemplate]="linkTemplate"></list-overflow> 91 <div class="created-at" i18n>Channel created on {{ videoChannel.createdAt | date }}</div>
44 </div> 92 </div>
93
94 <div *ngIf="hasShowMoreDescription()" class="show-more" role="button"
95 (click)="channelDescriptionExpanded = !channelDescriptionExpanded"
96 title="Show the complete description" i18n-title i18n
97 >
98 Show more...
99 </div>
100
101 <div class="channel-buttons bottom">
102 <ng-template *ngTemplateOutlet="buttonsTemplate"></ng-template>
103 </div>
104
105 <div class="owner-card">
106 <div class="section-label" i18n>OWNER ACCOUNT</div>
107
108 <ng-template *ngTemplateOutlet="ownerTemplate"></ng-template>
109 </div>
110 </div>
111
112 <div class="bottom-owner">
113 <div class="section-label" i18n>OWNER ACCOUNT</div>
114
115 <ng-template *ngTemplateOutlet="ownerTemplate"></ng-template>
45 </div> 116 </div>
46 117
47 <div class="margin-content"> 118 <div class="links">
48 <router-outlet></router-outlet> 119 <ng-template #linkTemplate let-item="item">
120 <a [routerLink]="item.routerLink" routerLinkActive="active" class="title-page">{{ item.label }}</a>
121 </ng-template>
122
123 <list-overflow [items]="links" [itemTemplate]="linkTemplate"></list-overflow>
49 </div> 124 </div>
125
126 <router-outlet></router-outlet>
50</div> 127</div>
128
129<my-support-modal #supportModal [videoChannel]="videoChannel"></my-support-modal>
diff --git a/client/src/app/+video-channels/video-channels.component.scss b/client/src/app/+video-channels/video-channels.component.scss
index 22f21dcc6..b19b4c81b 100644
--- a/client/src/app/+video-channels/video-channels.component.scss
+++ b/client/src/app/+video-channels/video-channels.component.scss
@@ -1,89 +1,308 @@
1// Bootstrap grid utilities require functions, variables and mixins
2@import 'node_modules/bootstrap/scss/functions';
3@import 'node_modules/bootstrap/scss/variables';
4@import 'node_modules/bootstrap/scss/mixins';
5@import 'node_modules/bootstrap/scss/grid';
6
7@import '_variables'; 1@import '_variables';
8@import '_mixins'; 2@import '_mixins';
3@import '_actor';
4@import '_miniature';
9 5
10.sub-menu { 6.root {
11 @include sub-menu-with-actor; 7 --myGlobalTopPadding: 60px;
8 --myChannelImgMargin: 30px;
9 --myFontSize: 16px;
10 --myGreyChannelFontSize: 16px;
11 --myGreyOwnerFontSize: 14px;
12}
12 13
13 .actor, .actor-info { 14.banner {
14 width: 100%; 15 @include block-ratio('img', $banner-inverted-ratio);
15 } 16}
16 17
17 .actor-info { 18.section-label {
18 display: grid !important; 19 @include section-label-responsive;
19 grid-template-columns: 1fr auto; 20}
20 grid-template-rows: 1fr auto / 1fr auto;
21 grid-template-areas: "name buttons" "lower buttons";
22 21
23 @include media-breakpoint-down(lg) { 22.links {
24 grid-template-areas: "name name" "lower buttons"; 23 @include grid-videos-miniature-margins;
25 } 24}
25
26.actor-info {
27 min-width: 1px;
28 width: 100%;
29
30 > h4,
31 > .actor-handle {
32 @include ellipsis;
26 } 33 }
34}
35
36.channel-info {
37 @include grid-videos-miniature-margins(false, 15px);
38
39 display: grid;
40 grid-template-columns: 1fr auto;
41 grid-template-rows: auto auto;
42
43 background-color: pvar(--channelBackgroundColor);
44 margin-bottom: 45px;
45 padding-top: var(--myGlobalTopPadding);
46 font-size: var(--myFontSize);
47}
48
49.channel-avatar-row {
50 @include avatar-row-responsive(var(--myChannelImgMargin), var(--myGreyChannelFontSize));
51}
52
53.support-button {
54 @include button-with-icon(21px, 0, -1px);
55}
56
57.channel-description {
58 grid-column: 1;
59 word-break: break-word;
60}
61
62.show-more {
63 @include show-more-description;
64
65 display: none;
66}
67
68.channel-buttons {
69 display: flex;
70 flex-wrap: wrap;
27 71
28 .actor-names { 72 > *:not(:last-child) {
29 grid-area: name; 73 margin-right: 15px;
30 } 74 }
75}
76
77.channel-buttons.right {
78 margin-left: 45px;
79}
80
81// Only used by mobile
82.channel-buttons.bottom {
83 display: none;
84}
85
86.created-at {
87 margin-top: 15px;
88 color: pvar(--greyForegroundColor);
89 padding-bottom: 60px;
90}
91
92.owner-card {
93 margin-left: 105px;
94 grid-column: 2;
95 // Takes all the column
96 grid-row: 1 / 3;
97 place-self: end;
98}
99
100// Only used on mobile
101.bottom-owner {
102 display: none;
103}
104
105.owner-block {
106 background-color: pvar(--mainBackgroundColor);
107 padding: 30px;
108 width: 300px;
109 font-size: var(--myFontSize);
31 110
32 .actor-name { 111 .avatar-row {
33 flex-grow: 1; 112 display: flex;
113 margin-bottom: 15px;
34 114
35 .copy-button { 115 img {
36 border: none; 116 @include avatar(48px);
37 padding: 5px;
38 margin-top: -2px;
39 } 117 }
118
119 .actor-info {
120 margin-left: 15px;
121 }
122
123 h4 {
124 font-size: 18px;
125 margin: 0;
126
127 a {
128 color: pvar(--mainForegroundColor);
129 }
130 }
131
132 .actor-handle {
133 font-size: var(--myGreyOwnerFontSize);
134 color: pvar(--greyForegroundColor);
135 }
136 }
137
138 .owner-description {
139 max-height: 140px;
140 word-break: break-word;
141
142 @include fade-text(120px, pvar(--mainBackgroundColor));
40 } 143 }
41} 144}
42 145
43.margin-content { 146.view-account.short {
44 // margin-content is required, but child views have their own margins 147 @include peertube-button-link;
45 // that match views outside the scope of accounts, so we only align 148 @include orange-button-inverted;
46 // them with the margins of .sub-menu when required. 149
47 margin: 0; 150 margin-top: 30px;
151}
152
153.view-account.complete {
154 display: none;
48} 155}
49 156
50.right-buttons { 157.copy-button {
51 display: flex; 158 border: none;
52 height: max-content; 159}
53 margin-left: auto; 160
54 margin-top: 10px; 161@media screen and (max-width: 1400px) {
162 // Takes all the row width
163 .channel-avatar-row {
164 grid-column: 1 / 3;
165 }
166
167 .owner-card {
168 grid-row: 2;
169 margin-left: 60px;
170 }
171}
172
173@media screen and (max-width: 1100px) {
174 .root {
175 --myGlobalTopPadding: 45px;
176 --myChannelImgMargin: 15px;
177 }
178
179 .channel-info {
180 display: flex;
181 flex-direction: column;
182 margin-bottom: 0;
183 }
184
185 .channel-description:not(.expanded) {
186 max-height: 70px;
187
188 @include fade-text(30px, pvar(--channelBackgroundColor));
189 }
190
191 .show-more {
192 display: inline-block;
193 }
194
195 .channel-buttons.bottom {
196 display: flex;
197 justify-content: center;
198 margin-bottom: 30px;
199 }
200
201 .channel-buttons.right {
202 display: none;
203 }
204
205 .owner-card {
206 display: none;
207 }
208
209 .bottom-owner {
210 display: block;
211 width: 100%;
212 border-bottom: 2px solid $separator-border-color;
213 padding: var(--myGlobalTopPadding) 45px;
214 margin-bottom: 60px;
215 }
55 216
56 grid-row: buttons-start / span buttons-end; 217 .owner-block {
57 grid-column: buttons-start; 218 display: grid;
219 width: 100%;
220 padding: 0;
58 221
59 @include media-breakpoint-down(lg) { 222 .avatar-row {
60 flex-flow: column-reverse; 223 grid-column: 1;
224 margin-right: 30px;
225 }
226
227 .owner-description {
228 grid-column: 2;
229 max-height: 70px;
230
231 @include fade-text(30px, pvar(--mainBackgroundColor));
232 }
61 233
62 a { 234 .view-account {
63 margin-top: 0.25rem; 235 grid-column: 2;
64 margin-right: 0 !important;
65 } 236 }
66 } 237 }
67 238
68 a { 239 .view-account.complete {
69 @include peertube-button-outline; 240 display: block;
70 line-height: 1.8; 241 text-align: right;
242 margin-top: 10px;
243 color: pvar(--mainColor);
71 } 244 }
72 245
73 my-subscribe-button { 246 .view-account.short {
74 height: min-content; 247 display: none;
75 } 248 }
76} 249}
77 250
78@media screen and (max-width: $mobile-view) { 251@media screen and (max-width: $mobile-view) {
79 .sub-menu { 252 .root {
80 .actor { 253 --myGlobalTopPadding: 15px;
81 flex-direction: column; 254 --myFontSize: 14px;
255 --myGreyChannelFontSize: 13px;
256 --myGreyOwnerFontSize: 13px;
257 }
258
259 .links {
260 margin: auto !important;
261 width: min-content;
262 }
263
264 .show-more {
265 margin-bottom: 30px;
266 }
267
268 .bottom-owner {
269 padding: 15px;
270 margin-bottom: 30px;
82 271
83 .actor-info .actor-names { 272 .section-label {
273 display: none;
274 }
275 }
276
277 .owner-block {
278 display: block;
279
280 .avatar-row {
281 display: flex;
282 flex-direction: row-reverse;
283 margin: 0;
284
285 h4 {
286 font-size: 16px;
287 }
288
289 .actor-info {
290 display: flex;
84 flex-direction: column; 291 flex-direction: column;
85 align-items: normal; 292 align-items: flex-end;
293 justify-content: flex-end;
294 margin-top: -5px;
295 }
296
297 img {
298 @include channel-avatar(64px);
299
300 margin: -30px 0 0 15px;
86 } 301 }
87 } 302 }
303
304 .owner-description {
305 display: none;
306 }
88 } 307 }
89} 308}
diff --git a/client/src/app/+video-channels/video-channels.component.ts b/client/src/app/+video-channels/video-channels.component.ts
index bb601e227..41fdb5e79 100644
--- a/client/src/app/+video-channels/video-channels.component.ts
+++ b/client/src/app/+video-channels/video-channels.component.ts
@@ -3,8 +3,9 @@ import { Subscription } from 'rxjs'
3import { catchError, distinctUntilChanged, map, switchMap } from 'rxjs/operators' 3import { catchError, distinctUntilChanged, map, switchMap } from 'rxjs/operators'
4import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core' 4import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'
5import { ActivatedRoute } from '@angular/router' 5import { ActivatedRoute } from '@angular/router'
6import { AuthService, Notifier, RestExtractor, ScreenService } from '@app/core' 6import { AuthService, MarkdownService, Notifier, RestExtractor, ScreenService } from '@app/core'
7import { ListOverflowItem, VideoChannel, VideoChannelService } from '@app/shared/shared-main' 7import { ListOverflowItem, VideoChannel, VideoChannelService, VideoService } from '@app/shared/shared-main'
8import { SupportModalComponent } from '@app/shared/shared-support-modal'
8import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' 9import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription'
9import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' 10import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
10 11
@@ -14,12 +15,18 @@ import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
14}) 15})
15export class VideoChannelsComponent implements OnInit, OnDestroy { 16export class VideoChannelsComponent implements OnInit, OnDestroy {
16 @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent 17 @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent
18 @ViewChild('supportModal') supportModal: SupportModalComponent
17 19
18 videoChannel: VideoChannel 20 videoChannel: VideoChannel
19 hotkeys: Hotkey[] 21 hotkeys: Hotkey[]
20 links: ListOverflowItem[] = [] 22 links: ListOverflowItem[] = []
21 isChannelManageable = false 23 isChannelManageable = false
22 24
25 channelVideosCount: number
26 ownerDescriptionHTML = ''
27 channelDescriptionHTML = ''
28 channelDescriptionExpanded = false
29
23 private routeSub: Subscription 30 private routeSub: Subscription
24 31
25 constructor ( 32 constructor (
@@ -27,9 +34,11 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
27 private notifier: Notifier, 34 private notifier: Notifier,
28 private authService: AuthService, 35 private authService: AuthService,
29 private videoChannelService: VideoChannelService, 36 private videoChannelService: VideoChannelService,
37 private videoService: VideoService,
30 private restExtractor: RestExtractor, 38 private restExtractor: RestExtractor,
31 private hotkeysService: HotkeysService, 39 private hotkeysService: HotkeysService,
32 private screenService: ScreenService 40 private screenService: ScreenService,
41 private markdown: MarkdownService
33 ) { } 42 ) { }
34 43
35 ngOnInit () { 44 ngOnInit () {
@@ -43,16 +52,14 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
43 HttpStatusCode.NOT_FOUND_404 52 HttpStatusCode.NOT_FOUND_404
44 ])) 53 ]))
45 ) 54 )
46 .subscribe(videoChannel => { 55 .subscribe(async videoChannel => {
56 this.channelDescriptionHTML = await this.markdown.textMarkdownToHTML(videoChannel.description)
57 this.ownerDescriptionHTML = await this.markdown.textMarkdownToHTML(videoChannel.ownerAccount.description)
58
59 // After the markdown renderer to avoid layout changes
47 this.videoChannel = videoChannel 60 this.videoChannel = videoChannel
48 61
49 if (this.authService.isLoggedIn()) { 62 this.loadChannelVideosCount()
50 this.authService.userInformationLoaded
51 .subscribe(() => {
52 const channelUserId = this.videoChannel.ownerAccount.userId
53 this.isChannelManageable = channelUserId && channelUserId === this.authService.getUser().id
54 })
55 }
56 }) 63 })
57 64
58 this.hotkeys = [ 65 this.hotkeys = [
@@ -67,8 +74,7 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
67 74
68 this.links = [ 75 this.links = [
69 { label: $localize`VIDEOS`, routerLink: 'videos' }, 76 { label: $localize`VIDEOS`, routerLink: 'videos' },
70 { label: $localize`VIDEO PLAYLISTS`, routerLink: 'video-playlists' }, 77 { label: $localize`PLAYLISTS`, routerLink: 'video-playlists' }
71 { label: $localize`ABOUT`, routerLink: 'about' }
72 ] 78 ]
73 } 79 }
74 80
@@ -79,7 +85,7 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
79 if (this.isUserLoggedIn()) this.hotkeysService.remove(this.hotkeys) 85 if (this.isUserLoggedIn()) this.hotkeysService.remove(this.hotkeys)
80 } 86 }
81 87
82 get isInSmallView () { 88 isInSmallView () {
83 return this.screenService.isInSmallView() 89 return this.screenService.isInSmallView()
84 } 90 }
85 91
@@ -87,12 +93,36 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
87 return this.authService.isLoggedIn() 93 return this.authService.isLoggedIn()
88 } 94 }
89 95
90 get isManageable () { 96 isManageable () {
91 if (!this.isUserLoggedIn()) return false 97 if (!this.isUserLoggedIn()) return false
92 return this.videoChannel.ownerAccount.userId === this.authService.getUser().id 98
99 return this.videoChannel?.ownerAccount.userId === this.authService.getUser().id
93 } 100 }
94 101
95 activateCopiedMessage () { 102 activateCopiedMessage () {
96 this.notifier.success($localize`Username copied`) 103 this.notifier.success($localize`Username copied`)
97 } 104 }
105
106 hasShowMoreDescription () {
107 return !this.channelDescriptionExpanded && this.channelDescriptionHTML.length > 100
108 }
109
110 showSupportModal () {
111 this.supportModal.show()
112 }
113
114 getAccountUrl () {
115 return [ '/accounts', this.videoChannel.ownerBy ]
116 }
117
118 private loadChannelVideosCount () {
119 this.videoService.getVideoChannelVideos({
120 videoChannel: this.videoChannel,
121 videoPagination: {
122 currentPage: 1,
123 itemsPerPage: 0
124 },
125 sort: '-publishedAt'
126 }).subscribe(res => this.channelVideosCount = res.total)
127 }
98} 128}
diff --git a/client/src/app/+video-channels/video-channels.module.ts b/client/src/app/+video-channels/video-channels.module.ts
index 05236ff85..408f86225 100644
--- a/client/src/app/+video-channels/video-channels.module.ts
+++ b/client/src/app/+video-channels/video-channels.module.ts
@@ -2,10 +2,10 @@ import { NgModule } from '@angular/core'
2import { SharedFormModule } from '@app/shared/shared-forms' 2import { SharedFormModule } from '@app/shared/shared-forms'
3import { SharedGlobalIconModule } from '@app/shared/shared-icons' 3import { SharedGlobalIconModule } from '@app/shared/shared-icons'
4import { SharedMainModule } from '@app/shared/shared-main' 4import { SharedMainModule } from '@app/shared/shared-main'
5import { SharedSupportModal } from '@app/shared/shared-support-modal'
5import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription' 6import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription'
6import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' 7import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature'
7import { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist' 8import { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist'
8import { VideoChannelAboutComponent } from './video-channel-about/video-channel-about.component'
9import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-channel-playlists.component' 9import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-channel-playlists.component'
10import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component' 10import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component'
11import { VideoChannelsRoutingModule } from './video-channels-routing.module' 11import { VideoChannelsRoutingModule } from './video-channels-routing.module'
@@ -20,13 +20,13 @@ import { VideoChannelsComponent } from './video-channels.component'
20 SharedVideoPlaylistModule, 20 SharedVideoPlaylistModule,
21 SharedVideoMiniatureModule, 21 SharedVideoMiniatureModule,
22 SharedUserSubscriptionModule, 22 SharedUserSubscriptionModule,
23 SharedGlobalIconModule 23 SharedGlobalIconModule,
24 SharedSupportModal
24 ], 25 ],
25 26
26 declarations: [ 27 declarations: [
27 VideoChannelsComponent, 28 VideoChannelsComponent,
28 VideoChannelVideosComponent, 29 VideoChannelVideosComponent,
29 VideoChannelAboutComponent,
30 VideoChannelPlaylistsComponent 30 VideoChannelPlaylistsComponent
31 ], 31 ],
32 32
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts
index 8780ca567..8e035b6bb 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts
@@ -1,8 +1,8 @@
1 1
2import { forkJoin } from 'rxjs' 2import { forkJoin } from 'rxjs'
3import { Component, EventEmitter, OnInit, Output } from '@angular/core' 3import { AfterViewChecked, AfterViewInit, Component, EventEmitter, OnInit, Output } from '@angular/core'
4import { Router } from '@angular/router' 4import { Router } from '@angular/router'
5import { AuthService, CanComponentDeactivate, Notifier, ServerService } from '@app/core' 5import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService } from '@app/core'
6import { scrollToTop } from '@app/helpers' 6import { scrollToTop } from '@app/helpers'
7import { FormValidatorService } from '@app/shared/shared-forms' 7import { FormValidatorService } from '@app/shared/shared-forms'
8import { VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' 8import { VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main'
@@ -19,7 +19,7 @@ import { VideoSend } from './video-send'
19 './video-send.scss' 19 './video-send.scss'
20 ] 20 ]
21}) 21})
22export class VideoGoLiveComponent extends VideoSend implements OnInit, CanComponentDeactivate { 22export class VideoGoLiveComponent extends VideoSend implements OnInit, AfterViewInit, CanComponentDeactivate {
23 @Output() firstStepDone = new EventEmitter<string>() 23 @Output() firstStepDone = new EventEmitter<string>()
24 @Output() firstStepError = new EventEmitter<void>() 24 @Output() firstStepError = new EventEmitter<void>()
25 25
@@ -41,7 +41,8 @@ export class VideoGoLiveComponent extends VideoSend implements OnInit, CanCompon
41 protected videoService: VideoService, 41 protected videoService: VideoService,
42 protected videoCaptionService: VideoCaptionService, 42 protected videoCaptionService: VideoCaptionService,
43 private liveVideoService: LiveVideoService, 43 private liveVideoService: LiveVideoService,
44 private router: Router 44 private router: Router,
45 private hooks: HooksService
45 ) { 46 ) {
46 super() 47 super()
47 } 48 }
@@ -50,6 +51,10 @@ export class VideoGoLiveComponent extends VideoSend implements OnInit, CanCompon
50 super.ngOnInit() 51 super.ngOnInit()
51 } 52 }
52 53
54 ngAfterViewInit () {
55 this.hooks.runAction('action:go-live.init', 'video-edit')
56 }
57
53 canDeactivate () { 58 canDeactivate () {
54 return { canDeactivate: true } 59 return { canDeactivate: true }
55 } 60 }
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.scss b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.scss
index 1fef74994..dd87641fc 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.scss
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.scss
@@ -3,8 +3,8 @@
3 3
4.first-step-block { 4.first-step-block {
5 .torrent-or-magnet { 5 .torrent-or-magnet {
6 @include divider($color: pvar(--inputPlaceholderColor), $background: pvar(--submenuColor)); 6 @include divider($color: pvar(--inputPlaceholderColor), $background: pvar(--submenuBackgroundColor));
7 7
8 &[data-content] { 8 &[data-content] {
9 margin: 1.5rem 0; 9 margin: 1.5rem 0;
10 } 10 }
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts
index 01087e525..3aae24732 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts
@@ -1,6 +1,6 @@
1import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' 1import { AfterViewInit, Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
2import { Router } from '@angular/router' 2import { Router } from '@angular/router'
3import { AuthService, CanComponentDeactivate, Notifier, ServerService } from '@app/core' 3import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService } from '@app/core'
4import { scrollToTop } from '@app/helpers' 4import { scrollToTop } from '@app/helpers'
5import { FormValidatorService } from '@app/shared/shared-forms' 5import { FormValidatorService } from '@app/shared/shared-forms'
6import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main' 6import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main'
@@ -18,7 +18,7 @@ import { VideoSend } from './video-send'
18 './video-send.scss' 18 './video-send.scss'
19 ] 19 ]
20}) 20})
21export class VideoImportTorrentComponent extends VideoSend implements OnInit, CanComponentDeactivate { 21export class VideoImportTorrentComponent extends VideoSend implements OnInit, AfterViewInit, CanComponentDeactivate {
22 @Output() firstStepDone = new EventEmitter<string>() 22 @Output() firstStepDone = new EventEmitter<string>()
23 @Output() firstStepError = new EventEmitter<void>() 23 @Output() firstStepError = new EventEmitter<void>()
24 @ViewChild('torrentfileInput') torrentfileInput: ElementRef<HTMLInputElement> 24 @ViewChild('torrentfileInput') torrentfileInput: ElementRef<HTMLInputElement>
@@ -43,7 +43,8 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
43 protected videoService: VideoService, 43 protected videoService: VideoService,
44 protected videoCaptionService: VideoCaptionService, 44 protected videoCaptionService: VideoCaptionService,
45 private router: Router, 45 private router: Router,
46 private videoImportService: VideoImportService 46 private videoImportService: VideoImportService,
47 private hooks: HooksService
47 ) { 48 ) {
48 super() 49 super()
49 } 50 }
@@ -52,6 +53,10 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
52 super.ngOnInit() 53 super.ngOnInit()
53 } 54 }
54 55
56 ngAfterViewInit () {
57 this.hooks.runAction('action:video-torrent-import.init', 'video-edit')
58 }
59
55 canDeactivate () { 60 canDeactivate () {
56 return { canDeactivate: true } 61 return { canDeactivate: true }
57 } 62 }
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts
index c447c179d..7a9fe369f 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts
@@ -1,7 +1,7 @@
1import { map, switchMap } from 'rxjs/operators' 1import { map, switchMap } from 'rxjs/operators'
2import { Component, EventEmitter, OnInit, Output } from '@angular/core' 2import { AfterViewInit, Component, EventEmitter, OnInit, Output } from '@angular/core'
3import { Router } from '@angular/router' 3import { Router } from '@angular/router'
4import { AuthService, CanComponentDeactivate, Notifier, ServerService } from '@app/core' 4import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService } from '@app/core'
5import { getAbsoluteAPIUrl, scrollToTop } from '@app/helpers' 5import { getAbsoluteAPIUrl, scrollToTop } from '@app/helpers'
6import { FormValidatorService } from '@app/shared/shared-forms' 6import { FormValidatorService } from '@app/shared/shared-forms'
7import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main' 7import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main'
@@ -18,7 +18,7 @@ import { VideoSend } from './video-send'
18 './video-send.scss' 18 './video-send.scss'
19 ] 19 ]
20}) 20})
21export class VideoImportUrlComponent extends VideoSend implements OnInit, CanComponentDeactivate { 21export class VideoImportUrlComponent extends VideoSend implements OnInit, AfterViewInit, CanComponentDeactivate {
22 @Output() firstStepDone = new EventEmitter<string>() 22 @Output() firstStepDone = new EventEmitter<string>()
23 @Output() firstStepError = new EventEmitter<void>() 23 @Output() firstStepError = new EventEmitter<void>()
24 24
@@ -42,8 +42,9 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom
42 protected videoService: VideoService, 42 protected videoService: VideoService,
43 protected videoCaptionService: VideoCaptionService, 43 protected videoCaptionService: VideoCaptionService,
44 private router: Router, 44 private router: Router,
45 private videoImportService: VideoImportService 45 private videoImportService: VideoImportService,
46 ) { 46 private hooks: HooksService
47 ) {
47 super() 48 super()
48 } 49 }
49 50
@@ -51,6 +52,10 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom
51 super.ngOnInit() 52 super.ngOnInit()
52 } 53 }
53 54
55 ngAfterViewInit () {
56 this.hooks.runAction('action:video-url-import.init', 'video-edit')
57 }
58
54 canDeactivate () { 59 canDeactivate () {
55 return { canDeactivate: true } 60 return { canDeactivate: true }
56 } 61 }
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts
index ca21b61cd..effb37077 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts
@@ -1,15 +1,15 @@
1import { Subscription } from 'rxjs' 1import { Subscription } from 'rxjs'
2import { HttpErrorResponse, HttpEventType, HttpResponse } from '@angular/common/http' 2import { HttpErrorResponse, HttpEventType, HttpResponse } from '@angular/common/http'
3import { Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core' 3import { AfterViewInit, Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
4import { Router } from '@angular/router' 4import { Router } from '@angular/router'
5import { AuthService, CanComponentDeactivate, Notifier, ServerService, UserService } from '@app/core' 5import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService, UserService } from '@app/core'
6import { scrollToTop, uploadErrorHandler } from '@app/helpers' 6import { scrollToTop, uploadErrorHandler } from '@app/helpers'
7import { FormValidatorService } from '@app/shared/shared-forms' 7import { FormValidatorService } from '@app/shared/shared-forms'
8import { BytesPipe, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' 8import { BytesPipe, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main'
9import { LoadingBarService } from '@ngx-loading-bar/core' 9import { LoadingBarService } from '@ngx-loading-bar/core'
10import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
10import { VideoPrivacy } from '@shared/models' 11import { VideoPrivacy } from '@shared/models'
11import { VideoSend } from './video-send' 12import { VideoSend } from './video-send'
12import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
13 13
14@Component({ 14@Component({
15 selector: 'my-video-upload', 15 selector: 'my-video-upload',
@@ -20,7 +20,7 @@ import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
20 './video-send.scss' 20 './video-send.scss'
21 ] 21 ]
22}) 22})
23export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, CanComponentDeactivate { 23export class VideoUploadComponent extends VideoSend implements OnInit, AfterViewInit, OnDestroy, CanComponentDeactivate {
24 @Output() firstStepDone = new EventEmitter<string>() 24 @Output() firstStepDone = new EventEmitter<string>()
25 @Output() firstStepError = new EventEmitter<void>() 25 @Output() firstStepError = new EventEmitter<void>()
26 @ViewChild('videofileInput') videofileInput: ElementRef<HTMLInputElement> 26 @ViewChild('videofileInput') videofileInput: ElementRef<HTMLInputElement>
@@ -60,7 +60,8 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
60 protected videoService: VideoService, 60 protected videoService: VideoService,
61 protected videoCaptionService: VideoCaptionService, 61 protected videoCaptionService: VideoCaptionService,
62 private userService: UserService, 62 private userService: UserService,
63 private router: Router 63 private router: Router,
64 private hooks: HooksService
64 ) { 65 ) {
65 super() 66 super()
66 } 67 }
@@ -79,6 +80,10 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
79 }) 80 })
80 } 81 }
81 82
83 ngAfterViewInit () {
84 this.hooks.runAction('action:video-upload.init', 'video-edit')
85 }
86
82 ngOnDestroy () { 87 ngOnDestroy () {
83 if (this.videoUploadObservable) this.videoUploadObservable.unsubscribe() 88 if (this.videoUploadObservable) this.videoUploadObservable.unsubscribe()
84 } 89 }
diff --git a/client/src/app/+videos/+video-edit/video-add.component.scss b/client/src/app/+videos/+video-edit/video-add.component.scss
index 5db9e823d..1ebee946b 100644
--- a/client/src/app/+videos/+video-edit/video-add.component.scss
+++ b/client/src/app/+videos/+video-edit/video-add.component.scss
@@ -67,7 +67,7 @@ $nav-link-height: 40px;
67 &.active { 67 &.active {
68 border-color: $border-color; 68 border-color: $border-color;
69 border-bottom-color: transparent; 69 border-bottom-color: transparent;
70 background-color: pvar(--submenuColor) !important; 70 background-color: pvar(--submenuBackgroundColor) !important;
71 71
72 span { 72 span {
73 border-bottom-color: pvar(--mainColor); 73 border-bottom-color: pvar(--mainColor);
@@ -84,7 +84,7 @@ $nav-link-height: 40px;
84 border: $border-width $border-type $border-color; 84 border: $border-width $border-type $border-color;
85 border-top: transparent; 85 border-top: transparent;
86 86
87 background-color: pvar(--submenuColor); 87 background-color: pvar(--submenuBackgroundColor);
88 border-bottom-left-radius: 3px; 88 border-bottom-left-radius: 3px;
89 border-bottom-right-radius: 3px; 89 border-bottom-right-radius: 3px;
90 width: 100%; 90 width: 100%;
diff --git a/client/src/app/+videos/+video-watch/comment/video-comments.component.html b/client/src/app/+videos/+video-watch/comment/video-comments.component.html
index 4a6426d30..9e6fde2e0 100644
--- a/client/src/app/+videos/+video-watch/comment/video-comments.component.html
+++ b/client/src/app/+videos/+video-watch/comment/video-comments.component.html
@@ -1,12 +1,7 @@
1<div> 1<div>
2 <div class="title-block"> 2 <div class="title-block">
3 <h2 class="title-page title-page-single"> 3 <h2 class="title-page title-page-single">
4 <ng-container *ngIf="totalNotDeletedComments > 0; then hasComments; else noComments"></ng-container> 4 {totalNotDeletedComments, plural, =0 {Comments} =1 {1 Comment} other {{{totalNotDeletedComments}} Comments}}
5 <ng-template #hasComments>
6 <ng-container i18n *ngIf="totalNotDeletedComments === 1; else manyComments">1 Comment</ng-container>
7 <ng-template i18n #manyComments>{{ totalNotDeletedComments }} Comments</ng-template>
8 </ng-template>
9 <ng-template i18n #noComments>Comments</ng-template>
10 </h2> 5 </h2>
11 6
12 <my-feed [syndicationItems]="syndicationItems"></my-feed> 7 <my-feed [syndicationItems]="syndicationItems"></my-feed>
@@ -79,15 +74,17 @@
79 <span class="glyphicon glyphicon-menu-down"></span> 74 <span class="glyphicon glyphicon-menu-down"></span>
80 75
81 <ng-container *ngIf="comment.totalRepliesFromVideoAuthor > 0; then hasAuthorComments; else noAuthorComments"></ng-container> 76 <ng-container *ngIf="comment.totalRepliesFromVideoAuthor > 0; then hasAuthorComments; else noAuthorComments"></ng-container>
77
82 <ng-template #hasAuthorComments> 78 <ng-template #hasAuthorComments>
83 <ng-container *ngIf="comment.totalReplies !== comment.totalRepliesFromVideoAuthor; else onlyAuthorComments" i18n> 79 <ng-container *ngIf="comment.totalReplies !== comment.totalRepliesFromVideoAuthor; else onlyAuthorComments" i18n>
84 View {{ comment.totalReplies }} replies from {{ video?.account?.displayName || 'the author' }} and others 80 View {comment.totalReplies, plural, =1 {1 reply} other {{{ comment.totalReplies }} replies}} from {{ video?.account?.displayName || 'the author' }} and others
85 </ng-container> 81 </ng-container>
86 <ng-template i18n #onlyAuthorComments> 82 <ng-template i18n #onlyAuthorComments>
87 View {{ comment.totalReplies }} replies from {{ video?.account?.displayName || 'the author' }} 83 View {comment.totalReplies, plural, =1 {1 reply} other {{{ comment.totalReplies }} replies}} from {{ video?.account?.displayName || 'the author' }}
88 </ng-template> 84 </ng-template>
89 </ng-template> 85 </ng-template>
90 <ng-template i18n #noAuthorComments>View {{ comment.totalReplies }} replies</ng-template> 86
87 <ng-template i18n #noAuthorComments>View {comment.totalReplies, plural, =1 {1 reply} other {{{ comment.totalReplies }} replies}}</ng-template>
91 88
92 <my-small-loader class="comment-thread-loading ml-1" [loading]="threadLoading[comment.id]"></my-small-loader> 89 <my-small-loader class="comment-thread-loading ml-1" [loading]="threadLoading[comment.id]"></my-small-loader>
93 </div> 90 </div>
diff --git a/client/src/app/+videos/+video-watch/comment/video-comments.component.ts b/client/src/app/+videos/+video-watch/comment/video-comments.component.ts
index d36dd9e34..210236b61 100644
--- a/client/src/app/+videos/+video-watch/comment/video-comments.component.ts
+++ b/client/src/app/+videos/+video-watch/comment/video-comments.component.ts
@@ -5,7 +5,6 @@ import { AuthService, ComponentPagination, ConfirmService, hasMoreItems, Notifie
5import { HooksService } from '@app/core/plugins/hooks.service' 5import { HooksService } from '@app/core/plugins/hooks.service'
6import { Syndication, VideoDetails } from '@app/shared/shared-main' 6import { Syndication, VideoDetails } from '@app/shared/shared-main'
7import { VideoComment, VideoCommentService, VideoCommentThreadTree } from '@app/shared/shared-video-comment' 7import { VideoComment, VideoCommentService, VideoCommentThreadTree } from '@app/shared/shared-video-comment'
8import { ThisReceiver } from '@angular/compiler'
9 8
10@Component({ 9@Component({
11 selector: 'my-video-comments', 10 selector: 'my-video-comments',
diff --git a/client/src/app/+videos/+video-watch/modal/video-support.component.ts b/client/src/app/+videos/+video-watch/modal/video-support.component.ts
deleted file mode 100644
index bd5290a72..000000000
--- a/client/src/app/+videos/+video-watch/modal/video-support.component.ts
+++ /dev/null
@@ -1,31 +0,0 @@
1import { Component, Input, ViewChild } from '@angular/core'
2import { MarkdownService } from '@app/core'
3import { VideoDetails } from '@app/shared/shared-main'
4import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
5
6@Component({
7 selector: 'my-video-support',
8 templateUrl: './video-support.component.html',
9 styleUrls: [ './video-support.component.scss' ]
10})
11export class VideoSupportComponent {
12 @Input() video: VideoDetails = null
13
14 @ViewChild('modal', { static: true }) modal: NgbModal
15
16 videoHTMLSupport = ''
17
18 constructor (
19 private markdownService: MarkdownService,
20 private modalService: NgbModal
21 ) { }
22
23 show () {
24 const modalRef = this.modalService.open(this.modal, { centered: true })
25
26 this.markdownService.enhancedMarkdownToHTML(this.video.support)
27 .then(r => this.videoHTMLSupport = r)
28
29 return modalRef
30 }
31}
diff --git a/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.html b/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.html
index 3c7c679b8..e0e9f92e7 100644
--- a/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.html
+++ b/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.html
@@ -1,4 +1,4 @@
1<div class="other-videos"> 1<div class="other-videos" [ngClass]="{ 'display-as-row': displayAsRow }">
2 <ng-container *ngIf="hasVideos$ | async"> 2 <ng-container *ngIf="hasVideos$ | async">
3 <div class="title-page-container"> 3 <div class="title-page-container">
4 <h2 i18n class="title-page title-page-single"> 4 <h2 i18n class="title-page title-page-single">
@@ -14,7 +14,7 @@
14 14
15 <ng-container *ngFor="let video of (videos$ | async); let i = index; let length = count"> 15 <ng-container *ngFor="let video of (videos$ | async); let i = index; let length = count">
16 <my-video-miniature 16 <my-video-miniature
17 [displayOptions]="displayOptions" [video]="video" [user]="userMiniature" 17 [displayOptions]="displayOptions" [video]="video" [user]="userMiniature" [displayAsRow]="displayAsRow"
18 (videoBlocked)="onVideoRemoved()" (videoRemoved)="onVideoRemoved()" (videoAccountMuted)="onVideoRemoved()"> 18 (videoBlocked)="onVideoRemoved()" (videoRemoved)="onVideoRemoved()" (videoAccountMuted)="onVideoRemoved()">
19 </my-video-miniature> 19 </my-video-miniature>
20 20
diff --git a/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.scss b/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.scss
index b278c9654..c9fae6f27 100644
--- a/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.scss
+++ b/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.scss
@@ -1,3 +1,6 @@
1@import '_variables';
2@import '_mixins';
3
1.title-page-container { 4.title-page-container {
2 display: flex; 5 display: flex;
3 justify-content: space-between; 6 justify-content: space-between;
@@ -11,6 +14,10 @@
11 } 14 }
12} 15}
13 16
17.title-page {
18 margin-top: 0;
19}
20
14.title-page-autoplay { 21.title-page-autoplay {
15 display: flex; 22 display: flex;
16 width: max-content; 23 width: max-content;
@@ -29,3 +36,29 @@
29hr { 36hr {
30 margin-top: 0; 37 margin-top: 0;
31} 38}
39
40my-video-miniature {
41 display: block;
42}
43
44.other-videos:not(.display-as-row) my-video-miniature {
45 min-width: $video-thumbnail-medium-width;
46 max-width: $video-thumbnail-medium-width;
47}
48
49.display-as-row {
50 my-video-miniature {
51 margin-bottom: 20px;
52 }
53
54 hr {
55 display: none;
56 }
57
58 @media screen and (max-width: $mobile-view) {
59 my-video-miniature {
60 margin-bottom: 10px;
61 }
62 }
63}
64
diff --git a/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.ts b/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.ts
index a1c8e0661..89b9c01b6 100644
--- a/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.ts
+++ b/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.ts
@@ -16,6 +16,8 @@ import { RecommendedVideosStore } from './recommended-videos.store'
16export class RecommendedVideosComponent implements OnInit, OnChanges { 16export class RecommendedVideosComponent implements OnInit, OnChanges {
17 @Input() inputRecommendation: RecommendationInfo 17 @Input() inputRecommendation: RecommendationInfo
18 @Input() playlist: VideoPlaylist 18 @Input() playlist: VideoPlaylist
19 @Input() displayAsRow: boolean
20
19 @Output() gotRecommendations = new EventEmitter<Video[]>() 21 @Output() gotRecommendations = new EventEmitter<Video[]>()
20 22
21 autoPlayNextVideo: boolean 23 autoPlayNextVideo: boolean
diff --git a/client/src/app/shared/shared-main/account/video-avatar-channel.component.html b/client/src/app/+videos/+video-watch/video-avatar-channel.component.html
index 310cc926f..5058f05dd 100644
--- a/client/src/app/shared/shared-main/account/video-avatar-channel.component.html
+++ b/client/src/app/+videos/+video-watch/video-avatar-channel.component.html
@@ -1,8 +1,9 @@
1<div class="wrapper" [ngClass]="'avatar-' + size"> 1<div class="wrapper" [ngClass]="'avatar-' + size">
2 <ng-container *ngIf="!isChannelAvatarNull() && !genericChannel"> 2 <ng-container *ngIf="!isChannelAvatarNull() && !genericChannel">
3 <a [routerLink]="[ '/video-channels', video.byVideoChannel ]" [title]="channelLinkTitle"> 3 <a [routerLink]="[ '/video-channels', video.byVideoChannel ]" [title]="channelLinkTitle">
4 <img [src]="video.videoChannelAvatarUrl" i18n-alt alt="Channel avatar" /> 4 <img [src]="video.videoChannelAvatarUrl" i18n-alt alt="Channel avatar" class="channel-avatar" />
5 </a> 5 </a>
6
6 <a [routerLink]="[ '/accounts', video.byAccount ]" [title]="accountLinkTitle"> 7 <a [routerLink]="[ '/accounts', video.byAccount ]" [title]="accountLinkTitle">
7 <img [src]="video.accountAvatarUrl" i18n-alt alt="Account avatar" /> 8 <img [src]="video.accountAvatarUrl" i18n-alt alt="Account avatar" />
8 </a> 9 </a>
@@ -14,7 +15,7 @@
14 </a> 15 </a>
15 16
16 <a [routerLink]="[ '/video-channels', video.byVideoChannel ]" [title]="channelLinkTitle"> 17 <a [routerLink]="[ '/video-channels', video.byVideoChannel ]" [title]="channelLinkTitle">
17 <img [src]="video.videoChannelAvatarUrl" i18n-alt alt="Channel avatar" /> 18 <img [src]="video.videoChannelAvatarUrl" i18n-alt alt="Channel avatar" class="channel-avatar" />
18 </a> 19 </a>
19 </ng-container> 20 </ng-container>
20 21
diff --git a/client/src/app/shared/shared-main/account/video-avatar-channel.component.scss b/client/src/app/+videos/+video-watch/video-avatar-channel.component.scss
index 37709fce6..4998e85fa 100644
--- a/client/src/app/shared/shared-main/account/video-avatar-channel.component.scss
+++ b/client/src/app/+videos/+video-watch/video-avatar-channel.component.scss
@@ -25,8 +25,12 @@
25 position: absolute; 25 position: absolute;
26 top:50%; 26 top:50%;
27 left:50%; 27 left:50%;
28 border-radius: 50%; 28 transform: translate(-50%,-50%);
29 transform: translate(-50%,-50%) 29 border-radius: 5px;
30
31 &:not(.channel-avatar) {
32 border-radius: 50%;
33 }
30 } 34 }
31 35
32 a:nth-of-type(2) img { 36 a:nth-of-type(2) img {
diff --git a/client/src/app/shared/shared-main/account/video-avatar-channel.component.ts b/client/src/app/+videos/+video-watch/video-avatar-channel.component.ts
index 440e2b522..0b6e796df 100644
--- a/client/src/app/shared/shared-main/account/video-avatar-channel.component.ts
+++ b/client/src/app/+videos/+video-watch/video-avatar-channel.component.ts
@@ -1,5 +1,5 @@
1import { Component, Input, OnInit } from '@angular/core' 1import { Component, Input, OnInit } from '@angular/core'
2import { Video } from '../video/video.model' 2import { Video } from '@app/shared/shared-main/video'
3 3
4@Component({ 4@Component({
5 selector: 'my-video-avatar-channel', 5 selector: 'my-video-avatar-channel',
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.html b/client/src/app/+videos/+video-watch/video-watch.component.html
index b17f898ce..99103c2c3 100644
--- a/client/src/app/+videos/+video-watch/video-watch.component.html
+++ b/client/src/app/+videos/+video-watch/video-watch.component.html
@@ -142,7 +142,7 @@
142 <ng-container *ngIf="isUserLoggedIn()"> 142 <ng-container *ngIf="isUserLoggedIn()">
143 <my-video-actions-dropdown 143 <my-video-actions-dropdown
144 placement="bottom auto" buttonDirection="horizontal" [buttonStyled]="true" [video]="video" [videoCaptions]="videoCaptions" 144 placement="bottom auto" buttonDirection="horizontal" [buttonStyled]="true" [video]="video" [videoCaptions]="videoCaptions"
145 [displayOptions]="videoActionsOptions" (videoRemoved)="onVideoRemoved()" (modalOpened)="onModalOpened()" 145 [displayOptions]="videoActionsOptions" (videoRemoved)="onVideoRemoved()"
146 ></my-video-actions-dropdown> 146 ></my-video-actions-dropdown>
147 </ng-container> 147 </ng-container>
148 </div> 148 </div>
@@ -289,6 +289,7 @@
289 </div> 289 </div>
290 290
291 <my-recommended-videos 291 <my-recommended-videos
292 [displayAsRow]="displayOtherVideosAsRow()"
292 [inputRecommendation]="{ uuid: video.uuid, tags: video.tags }" 293 [inputRecommendation]="{ uuid: video.uuid, tags: video.tags }"
293 [playlist]="playlist" 294 [playlist]="playlist"
294 (gotRecommendations)="onRecommendations($event)" 295 (gotRecommendations)="onRecommendations($event)"
@@ -313,6 +314,6 @@
313</div> 314</div>
314 315
315<ng-container *ngIf="video !== null"> 316<ng-container *ngIf="video !== null">
316 <my-video-support #videoSupportModal [video]="video"></my-video-support> 317 <my-support-modal #supportModal [video]="video"></my-support-modal>
317 <my-video-share #videoShareModal [video]="video" [videoCaptions]="videoCaptions" [playlist]="playlist"></my-video-share> 318 <my-video-share #videoShareModal [video]="video" [videoCaptions]="videoCaptions" [playlist]="playlist"></my-video-share>
318</ng-container> 319</ng-container>
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.scss b/client/src/app/+videos/+video-watch/video-watch.component.scss
index 555126cbc..2e566e3fb 100644
--- a/client/src/app/+videos/+video-watch/video-watch.component.scss
+++ b/client/src/app/+videos/+video-watch/video-watch.component.scss
@@ -3,7 +3,7 @@
3@import '_bootstrap-variables'; 3@import '_bootstrap-variables';
4@import '_miniature'; 4@import '_miniature';
5 5
6$player-factor: 1.7; // 16/9 6$player-factor: #{16/9};
7$video-info-margin-left: 44px; 7$video-info-margin-left: 44px;
8 8
9@function getPlayerHeight($width){ 9@function getPlayerHeight($width){
@@ -179,12 +179,6 @@ $video-info-margin-left: 44px;
179 &:hover { 179 &:hover {
180 opacity: 0.8; 180 opacity: 0.8;
181 } 181 }
182
183 img {
184 @include avatar(18px);
185
186 margin: -2px 5px 0 0;
187 }
188 } 182 }
189 183
190 .video-info-channel-left { 184 .video-info-channel-left {
@@ -413,37 +407,12 @@ $video-info-margin-left: 44px;
413 } 407 }
414 } 408 }
415 } 409 }
410}
416 411
417 ::ng-deep .other-videos { 412my-recommended-videos {
418 padding-left: 15px; 413 display: block;
419 min-width: $video-miniature-width; 414 padding-left: 15px;
420 415 min-width: 250px;
421 @media screen and (min-width: 1800px - (3* $video-miniature-width)) {
422 width: min-content;
423 }
424
425 .title-page {
426 margin: 0 !important;
427 }
428
429 .video-miniature {
430 display: flex;
431 width: max-content;
432 height: 100%;
433 padding-bottom: 20px;
434 flex-wrap: wrap;
435 }
436
437 .video-bottom {
438 @media screen and (max-width: 1800px - (3* $video-miniature-width)) {
439 margin-left: 1rem;
440 }
441 @media screen and (max-width: 500px) {
442 margin-left: 0;
443 margin-top: .5rem;
444 }
445 }
446 }
447} 416}
448 417
449my-video-comments { 418my-video-comments {
@@ -537,6 +506,7 @@ my-video-comments {
537 } 506 }
538} 507}
539 508
509// Use the same breakpoint than in the typescript component to display the other video miniatures as row
540@media screen and (max-width: 1100px) { 510@media screen and (max-width: 1100px) {
541 #video-wrapper { 511 #video-wrapper {
542 flex-direction: column; 512 flex-direction: column;
@@ -549,15 +519,10 @@ my-video-comments {
549 519
550 .video-bottom { 520 .video-bottom {
551 flex-direction: column; 521 flex-direction: column;
522 }
552 523
553 ::ng-deep .other-videos { 524 my-recommended-videos {
554 padding-left: 0 !important; 525 padding-left: 0;
555
556 ::ng-deep .video-miniature {
557 flex-direction: row;
558 width: auto;
559 }
560 }
561 } 526 }
562} 527}
563 528
@@ -579,10 +544,6 @@ my-video-comments {
579 } 544 }
580 } 545 }
581 546
582 ::ng-deep .other-videos .video-miniature {
583 flex-direction: column;
584 }
585
586 .privacy-concerns { 547 .privacy-concerns {
587 width: 100%; 548 width: 100%;
588 } 549 }
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.ts b/client/src/app/+videos/+video-watch/video-watch.component.ts
index 7a98cab3b..de5fb4ed0 100644
--- a/client/src/app/+videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/+videos/+video-watch/video-watch.component.ts
@@ -21,6 +21,7 @@ import { RedirectService } from '@app/core/routing/redirect.service'
21import { isXPercentInViewport, scrollToTop } from '@app/helpers' 21import { isXPercentInViewport, scrollToTop } from '@app/helpers'
22import { Video, VideoCaptionService, VideoDetails, VideoService } from '@app/shared/shared-main' 22import { Video, VideoCaptionService, VideoDetails, VideoService } from '@app/shared/shared-main'
23import { VideoShareComponent } from '@app/shared/shared-share-modal' 23import { VideoShareComponent } from '@app/shared/shared-share-modal'
24import { SupportModalComponent } from '@app/shared/shared-support-modal'
24import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' 25import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription'
25import { VideoActionsDisplayType, VideoDownloadComponent } from '@app/shared/shared-video-miniature' 26import { VideoActionsDisplayType, VideoDownloadComponent } from '@app/shared/shared-video-miniature'
26import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' 27import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist'
@@ -28,7 +29,12 @@ import { MetaService } from '@ngx-meta/core'
28import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage' 29import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
29import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' 30import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
30import { ServerConfig, ServerErrorCode, UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '@shared/models' 31import { ServerConfig, ServerErrorCode, UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '@shared/models'
31import { getStoredP2PEnabled, getStoredTheater } from '../../../assets/player/peertube-player-local-storage' 32import {
33 cleanupVideoWatch,
34 getStoredP2PEnabled,
35 getStoredTheater,
36 getStoredVideoWatchHistory
37} from '../../../assets/player/peertube-player-local-storage'
32import { 38import {
33 CustomizationOptions, 39 CustomizationOptions,
34 P2PMediaLoaderOptions, 40 P2PMediaLoaderOptions,
@@ -39,7 +45,6 @@ import {
39} from '../../../assets/player/peertube-player-manager' 45} from '../../../assets/player/peertube-player-manager'
40import { isWebRTCDisabled, timeToInt } from '../../../assets/player/utils' 46import { isWebRTCDisabled, timeToInt } from '../../../assets/player/utils'
41import { environment } from '../../../environments/environment' 47import { environment } from '../../../environments/environment'
42import { VideoSupportComponent } from './modal/video-support.component'
43import { VideoWatchPlaylistComponent } from './video-watch-playlist.component' 48import { VideoWatchPlaylistComponent } from './video-watch-playlist.component'
44 49
45type URLOptions = CustomizationOptions & { playerMode: PlayerMode } 50type URLOptions = CustomizationOptions & { playerMode: PlayerMode }
@@ -54,7 +59,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
54 59
55 @ViewChild('videoWatchPlaylist', { static: true }) videoWatchPlaylist: VideoWatchPlaylistComponent 60 @ViewChild('videoWatchPlaylist', { static: true }) videoWatchPlaylist: VideoWatchPlaylistComponent
56 @ViewChild('videoShareModal') videoShareModal: VideoShareComponent 61 @ViewChild('videoShareModal') videoShareModal: VideoShareComponent
57 @ViewChild('videoSupportModal') videoSupportModal: VideoSupportComponent 62 @ViewChild('supportModal') supportModal: SupportModalComponent
58 @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent 63 @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent
59 @ViewChild('videoDownloadModal') videoDownloadModal: VideoDownloadComponent 64 @ViewChild('videoDownloadModal') videoDownloadModal: VideoDownloadComponent
60 65
@@ -195,6 +200,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
195 this.theaterEnabled = getStoredTheater() 200 this.theaterEnabled = getStoredTheater()
196 201
197 this.hooks.runAction('action:video-watch.init', 'video-watch') 202 this.hooks.runAction('action:video-watch.init', 'video-watch')
203
204 setTimeout(cleanupVideoWatch, 1500) // Run in timeout to ensure we're not blocking the UI
198 } 205 }
199 206
200 ngOnDestroy () { 207 ngOnDestroy () {
@@ -277,23 +284,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
277 } 284 }
278 285
279 showSupportModal () { 286 showSupportModal () {
280 // Check video was playing before opening support modal 287 this.supportModal.show()
281 const isVideoPlaying = this.isPlaying()
282
283 this.pausePlayer()
284
285 const modalRef = this.videoSupportModal.show()
286
287 modalRef.result.then(() => {
288 if (isVideoPlaying) {
289 this.resumePlayer()
290 }
291 })
292 } 288 }
293 289
294 showShareModal () { 290 showShareModal () {
295 this.pausePlayer()
296
297 this.videoShareModal.show(this.currentTime, this.videoWatchPlaylist.currentPlaylistPosition) 291 this.videoShareModal.show(this.currentTime, this.videoWatchPlaylist.currentPlaylistPosition)
298 } 292 }
299 293
@@ -316,10 +310,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
316 } 310 }
317 } 311 }
318 312
319 onModalOpened () {
320 this.pausePlayer()
321 }
322
323 onVideoRemoved () { 313 onVideoRemoved () {
324 this.redirectService.redirectToHomepage() 314 this.redirectService.redirectToHomepage()
325 } 315 }
@@ -396,6 +386,11 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
396 this.loadVideo(videoId) 386 this.loadVideo(videoId)
397 } 387 }
398 388
389 displayOtherVideosAsRow () {
390 // Use the same value as in the SASS file
391 return this.screenService.getWindowInnerWidth() <= 1100
392 }
393
399 private loadVideo (videoId: string) { 394 private loadVideo (videoId: string) {
400 // Video did not change 395 // Video did not change
401 if (this.video && this.video.uuid === videoId) return 396 if (this.video && this.video.uuid === videoId) return
@@ -768,9 +763,11 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
768 const getStartTime = () => { 763 const getStartTime = () => {
769 const byUrl = urlOptions.startTime !== undefined 764 const byUrl = urlOptions.startTime !== undefined
770 const byHistory = video.userHistory && (!this.playlist || urlOptions.resume !== undefined) 765 const byHistory = video.userHistory && (!this.playlist || urlOptions.resume !== undefined)
766 const byLocalStorage = getStoredVideoWatchHistory(video.uuid)
771 767
772 if (byUrl) return timeToInt(urlOptions.startTime) 768 if (byUrl) return timeToInt(urlOptions.startTime)
773 if (byHistory) return video.userHistory.currentTime 769 if (byHistory) return video.userHistory.currentTime
770 if (byLocalStorage) return byLocalStorage.duration
774 771
775 return 0 772 return 0
776 } 773 }
@@ -815,6 +812,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
815 ? this.videoService.getVideoViewUrl(video.uuid) 812 ? this.videoService.getVideoViewUrl(video.uuid)
816 : null, 813 : null,
817 embedUrl: video.embedUrl, 814 embedUrl: video.embedUrl,
815 embedTitle: video.name,
818 816
819 isLive: video.isLive, 817 isLive: video.isLive,
820 818
@@ -827,7 +825,9 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
827 825
828 serverUrl: environment.apiUrl, 826 serverUrl: environment.apiUrl,
829 827
830 videoCaptions: playerCaptions 828 videoCaptions: playerCaptions,
829
830 videoUUID: video.uuid
831 }, 831 },
832 832
833 webtorrent: { 833 webtorrent: {
@@ -867,24 +867,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
867 return { playerMode: mode, playerOptions: options } 867 return { playerMode: mode, playerOptions: options }
868 } 868 }
869 869
870 private pausePlayer () {
871 if (!this.player) return
872
873 this.player.pause()
874 }
875
876 private resumePlayer () {
877 if (!this.player) return
878
879 this.player.play()
880 }
881
882 private isPlaying () {
883 if (!this.player) return
884
885 return !this.player.paused()
886 }
887
888 private async subscribeToLiveEventsIfNeeded (oldVideo: VideoDetails, newVideo: VideoDetails) { 870 private async subscribeToLiveEventsIfNeeded (oldVideo: VideoDetails, newVideo: VideoDetails) {
889 if (!this.liveVideosSub) { 871 if (!this.liveVideosSub) {
890 this.liveVideosSub = this.buildLiveEventsSubscription() 872 this.liveVideosSub = this.buildLiveEventsSubscription()
diff --git a/client/src/app/+videos/+video-watch/video-watch.module.ts b/client/src/app/+videos/+video-watch/video-watch.module.ts
index fbda9b9c4..3e9f3822e 100644
--- a/client/src/app/+videos/+video-watch/video-watch.module.ts
+++ b/client/src/app/+videos/+video-watch/video-watch.module.ts
@@ -4,6 +4,7 @@ import { SharedGlobalIconModule } from '@app/shared/shared-icons'
4import { SharedMainModule } from '@app/shared/shared-main' 4import { SharedMainModule } from '@app/shared/shared-main'
5import { SharedModerationModule } from '@app/shared/shared-moderation' 5import { SharedModerationModule } from '@app/shared/shared-moderation'
6import { SharedShareModal } from '@app/shared/shared-share-modal' 6import { SharedShareModal } from '@app/shared/shared-share-modal'
7import { SharedSupportModal } from '@app/shared/shared-support-modal'
7import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription' 8import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription'
8import { SharedVideoModule } from '@app/shared/shared-video' 9import { SharedVideoModule } from '@app/shared/shared-video'
9import { SharedVideoCommentModule } from '@app/shared/shared-video-comment' 10import { SharedVideoCommentModule } from '@app/shared/shared-video-comment'
@@ -13,9 +14,9 @@ import { VideoCommentService } from '../../shared/shared-video-comment/video-com
13import { VideoCommentAddComponent } from './comment/video-comment-add.component' 14import { VideoCommentAddComponent } from './comment/video-comment-add.component'
14import { VideoCommentComponent } from './comment/video-comment.component' 15import { VideoCommentComponent } from './comment/video-comment.component'
15import { VideoCommentsComponent } from './comment/video-comments.component' 16import { VideoCommentsComponent } from './comment/video-comments.component'
16import { VideoSupportComponent } from './modal/video-support.component'
17import { RecommendationsModule } from './recommendations/recommendations.module' 17import { RecommendationsModule } from './recommendations/recommendations.module'
18import { TimestampRouteTransformerDirective } from './timestamp-route-transformer.directive' 18import { TimestampRouteTransformerDirective } from './timestamp-route-transformer.directive'
19import { VideoAvatarChannelComponent } from './video-avatar-channel.component'
19import { VideoWatchPlaylistComponent } from './video-watch-playlist.component' 20import { VideoWatchPlaylistComponent } from './video-watch-playlist.component'
20import { VideoWatchRoutingModule } from './video-watch-routing.module' 21import { VideoWatchRoutingModule } from './video-watch-routing.module'
21import { VideoWatchComponent } from './video-watch.component' 22import { VideoWatchComponent } from './video-watch.component'
@@ -34,18 +35,20 @@ import { VideoWatchComponent } from './video-watch.component'
34 SharedGlobalIconModule, 35 SharedGlobalIconModule,
35 SharedVideoCommentModule, 36 SharedVideoCommentModule,
36 SharedShareModal, 37 SharedShareModal,
37 SharedVideoModule 38 SharedVideoModule,
39 SharedSupportModal
38 ], 40 ],
39 41
40 declarations: [ 42 declarations: [
41 VideoWatchComponent, 43 VideoWatchComponent,
42 VideoWatchPlaylistComponent, 44 VideoWatchPlaylistComponent,
43 45
44 VideoSupportComponent,
45 VideoCommentsComponent, 46 VideoCommentsComponent,
46 VideoCommentAddComponent, 47 VideoCommentAddComponent,
47 VideoCommentComponent, 48 VideoCommentComponent,
48 49
50 VideoAvatarChannelComponent,
51
49 TimestampRouteTransformerDirective, 52 TimestampRouteTransformerDirective,
50 TimestampRouteTransformerDirective 53 TimestampRouteTransformerDirective
51 ], 54 ],
diff --git a/client/src/app/+videos/video-list/overview/video-overview.component.html b/client/src/app/+videos/video-list/overview/video-overview.component.html
index ca986c634..639a96c43 100644
--- a/client/src/app/+videos/video-list/overview/video-overview.component.html
+++ b/client/src/app/+videos/video-list/overview/video-overview.component.html
@@ -14,7 +14,7 @@
14 </h1> 14 </h1>
15 15
16 <div class="video-wrapper" *ngFor="let video of buildVideos(object.videos)"> 16 <div class="video-wrapper" *ngFor="let video of buildVideos(object.videos)">
17 <my-video-miniature [video]="video" [fitWidth]="true" [user]="userMiniature" [displayVideoActions]="true"> 17 <my-video-miniature [video]="video" [user]="userMiniature" [displayVideoActions]="true">
18 </my-video-miniature> 18 </my-video-miniature>
19 </div> 19 </div>
20 </div> 20 </div>
@@ -25,7 +25,7 @@
25 </h2> 25 </h2>
26 26
27 <div class="video-wrapper" *ngFor="let video of buildVideos(object.videos)"> 27 <div class="video-wrapper" *ngFor="let video of buildVideos(object.videos)">
28 <my-video-miniature [video]="video" [fitWidth]="true" [user]="userMiniature" [displayVideoActions]="true"> 28 <my-video-miniature [video]="video" [user]="userMiniature" [displayVideoActions]="true">
29 </my-video-miniature> 29 </my-video-miniature>
30 </div> 30 </div>
31 </div> 31 </div>
@@ -40,7 +40,7 @@
40 </div> 40 </div>
41 41
42 <div class="video-wrapper" *ngFor="let video of buildVideos(object.videos)"> 42 <div class="video-wrapper" *ngFor="let video of buildVideos(object.videos)">
43 <my-video-miniature [video]="video" [fitWidth]="true" [user]="userMiniature" [displayVideoActions]="true"> 43 <my-video-miniature [video]="video" [user]="userMiniature" [displayVideoActions]="true">
44 </my-video-miniature> 44 </my-video-miniature>
45 </div> 45 </div>
46 </div> 46 </div>
diff --git a/client/src/app/+videos/video-list/overview/video-overview.component.scss b/client/src/app/+videos/video-list/overview/video-overview.component.scss
index c1d10188a..ec73c628c 100644
--- a/client/src/app/+videos/video-list/overview/video-overview.component.scss
+++ b/client/src/app/+videos/video-list/overview/video-overview.component.scss
@@ -8,9 +8,84 @@
8} 8}
9 9
10.margin-content { 10.margin-content {
11 @include fluid-videos-miniature-layout; 11 @include grid-videos-miniature-layout;
12} 12}
13 13
14.section { 14.section {
15 @include miniature-rows; 15 &:first-child {
16 padding-top: 30px;
17
18 .section-title {
19 border-top: none !important;
20 }
21 }
22
23 .section-title {
24 font-size: 24px;
25 font-weight: $font-semibold;
26 padding-top: 15px;
27 margin-bottom: 15px;
28 display: flex;
29 justify-content: space-between;
30
31 &:not(h2) {
32 border-top: 1px solid $separator-border-color;
33 }
34
35 a {
36 &:hover, &:focus:not(.focus-visible), &:active {
37 text-decoration: none;
38 outline: none;
39 }
40
41 color: pvar(--mainForegroundColor);
42 }
43 }
44
45 &.channel {
46 .section-title {
47 a {
48 display: flex;
49 width: fit-content;
50 align-items: center;
51
52 img {
53 @include channel-avatar(28px);
54
55 margin-right: 8px;
56 }
57 }
58
59 .followers {
60 color: pvar(--greyForegroundColor);
61 font-weight: normal;
62 font-size: 14px;
63 margin-left: 10px;
64 position: relative;
65 top: 2px;
66 }
67 }
68 }
69
70 .show-more {
71 position: relative;
72 top: -5px;
73 display: inline-block;
74 font-size: 16px;
75 text-transform: uppercase;
76 color: pvar(--greyForegroundColor);
77 margin-bottom: 10px;
78 font-weight: $font-semibold;
79 text-decoration: none;
80 }
81
82 @media screen and (max-width: $mobile-view) {
83 max-height: initial;
84 overflow: initial;
85
86 .section-title {
87 font-size: 17px;
88 margin-left: 10px;
89 }
90 }
16} 91}
diff --git a/client/src/app/+videos/video-list/video-user-subscriptions.component.ts b/client/src/app/+videos/video-list/video-user-subscriptions.component.ts
index e352a2b2c..6aabb93a5 100644
--- a/client/src/app/+videos/video-list/video-user-subscriptions.component.ts
+++ b/client/src/app/+videos/video-list/video-user-subscriptions.component.ts
@@ -7,7 +7,7 @@ import { HooksService } from '@app/core/plugins/hooks.service'
7import { immutableAssign } from '@app/helpers' 7import { immutableAssign } from '@app/helpers'
8import { VideoService } from '@app/shared/shared-main' 8import { VideoService } from '@app/shared/shared-main'
9import { UserSubscriptionService } from '@app/shared/shared-user-subscription' 9import { UserSubscriptionService } from '@app/shared/shared-user-subscription'
10import { AbstractVideoList, OwnerDisplayType } from '@app/shared/shared-video-miniature' 10import { AbstractVideoList } from '@app/shared/shared-video-miniature'
11import { FeedFormat, VideoSortField } from '@shared/models' 11import { FeedFormat, VideoSortField } from '@shared/models'
12import { environment } from '../../../environments/environment' 12import { environment } from '../../../environments/environment'
13import { copyToClipboard } from '../../../root-helpers/utils' 13import { copyToClipboard } from '../../../root-helpers/utils'
@@ -20,7 +20,6 @@ import { copyToClipboard } from '../../../root-helpers/utils'
20export class VideoUserSubscriptionsComponent extends AbstractVideoList implements OnInit, OnDestroy { 20export class VideoUserSubscriptionsComponent extends AbstractVideoList implements OnInit, OnDestroy {
21 titlePage: string 21 titlePage: string
22 sort = '-publishedAt' as VideoSortField 22 sort = '-publishedAt' as VideoSortField
23 ownerDisplayType: OwnerDisplayType = 'auto'
24 groupByDate = true 23 groupByDate = true
25 24
26 constructor ( 25 constructor (
diff --git a/client/src/app/app.component.scss b/client/src/app/app.component.scss
index e8447719a..42293e412 100644
--- a/client/src/app/app.component.scss
+++ b/client/src/app/app.component.scss
@@ -43,6 +43,10 @@ $assets-path: '../assets';
43 background-color: pvar(--mainForegroundColor); 43 background-color: pvar(--mainForegroundColor);
44 mask-image: url('#{$assets-path}/images/misc/menu.svg'); 44 mask-image: url('#{$assets-path}/images/misc/menu.svg');
45 margin: 0 18px 0 20px; 45 margin: 0 18px 0 20px;
46
47 @media screen and (max-width: $mobile-view) {
48 margin: 0 10px;
49 }
46 } 50 }
47 } 51 }
48 52
@@ -71,16 +75,11 @@ $assets-path: '../assets';
71 } 75 }
72 76
73 @media screen and (max-width: $mobile-view) { 77 @media screen and (max-width: $mobile-view) {
74 width: 70px;
75 78
76 .peertube-title { 79 .peertube-title {
77 display: none; 80 display: none;
78 } 81 }
79 } 82 }
80
81 @media screen and (max-width: 350px) {
82 flex: auto;
83 }
84 } 83 }
85 84
86 .header-right { 85 .header-right {
diff --git a/client/src/app/core/notification/peertube-socket.service.ts b/client/src/app/core/notification/peertube-socket.service.ts
index bc3f7b893..eab1c63f2 100644
--- a/client/src/app/core/notification/peertube-socket.service.ts
+++ b/client/src/app/core/notification/peertube-socket.service.ts
@@ -58,12 +58,11 @@ export class PeerTubeSocket {
58 this.notificationSocket = this.io(environment.apiUrl + '/user-notifications', { 58 this.notificationSocket = this.io(environment.apiUrl + '/user-notifications', {
59 query: { accessToken: this.auth.getAccessToken() } 59 query: { accessToken: this.auth.getAccessToken() }
60 }) 60 })
61
62 this.notificationSocket.on('new-notification', (n: UserNotificationServer) => {
63 this.ngZone.run(() => this.dispatchNotificationEvent('new', n))
64 })
65 }) 61 })
66 62
63 this.notificationSocket.on('new-notification', (n: UserNotificationServer) => {
64 this.ngZone.run(() => this.dispatchNotificationEvent('new', n))
65 })
67 } 66 }
68 67
69 private async initLiveVideosSocket () { 68 private async initLiveVideosSocket () {
diff --git a/client/src/app/core/plugins/hooks.service.ts b/client/src/app/core/plugins/hooks.service.ts
index ec47aa48c..ddde198d2 100644
--- a/client/src/app/core/plugins/hooks.service.ts
+++ b/client/src/app/core/plugins/hooks.service.ts
@@ -3,13 +3,29 @@ import { mergeMap, switchMap } from 'rxjs/operators'
3import { Injectable } from '@angular/core' 3import { Injectable } from '@angular/core'
4import { PluginService } from '@app/core/plugins/plugin.service' 4import { PluginService } from '@app/core/plugins/plugin.service'
5import { ClientActionHookName, ClientFilterHookName, PluginClientScope } from '@shared/models' 5import { ClientActionHookName, ClientFilterHookName, PluginClientScope } from '@shared/models'
6import { AuthService, AuthStatus } from '../auth'
6 7
7type RawFunction<U, T> = (params: U) => T 8type RawFunction<U, T> = (params: U) => T
8type ObservableFunction<U, T> = RawFunction<U, Observable<T>> 9type ObservableFunction<U, T> = RawFunction<U, Observable<T>>
9 10
10@Injectable() 11@Injectable()
11export class HooksService { 12export class HooksService {
12 constructor (private pluginService: PluginService) { } 13 constructor (
14 private authService: AuthService,
15 private pluginService: PluginService
16 ) {
17 // Run auth hooks
18 this.authService.userInformationLoaded
19 .subscribe(() => this.runAction('action:auth-user.information-loaded', 'common', { user: this.authService.getUser() }))
20
21 this.authService.loginChangedSource.subscribe(obj => {
22 if (obj === AuthStatus.LoggedIn) {
23 this.runAction('action:auth-user.logged-in', 'common')
24 } else if (obj === AuthStatus.LoggedOut) {
25 this.runAction('action:auth-user.logged-out', 'common')
26 }
27 })
28 }
13 29
14 wrapObsFun 30 wrapObsFun
15 <P, R, H1 extends ClientFilterHookName, H2 extends ClientFilterHookName> 31 <P, R, H1 extends ClientFilterHookName, H2 extends ClientFilterHookName>
diff --git a/client/src/app/core/plugins/plugin.service.ts b/client/src/app/core/plugins/plugin.service.ts
index b755fda2c..54dba5e17 100644
--- a/client/src/app/core/plugins/plugin.service.ts
+++ b/client/src/app/core/plugins/plugin.service.ts
@@ -235,6 +235,12 @@ export class PluginService implements ClientHook {
235 .toPromise() 235 .toPromise()
236 }, 236 },
237 237
238 getServerConfig: () => {
239 return this.server.getConfig()
240 .pipe(catchError(res => this.restExtractor.handleError(res)))
241 .toPromise()
242 },
243
238 isLoggedIn: () => { 244 isLoggedIn: () => {
239 return this.authService.isLoggedIn() 245 return this.authService.isLoggedIn()
240 }, 246 },
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts
index 11288fc54..906191ae1 100644
--- a/client/src/app/core/server/server.service.ts
+++ b/client/src/app/core/server/server.service.ts
@@ -98,6 +98,12 @@ export class ServerService {
98 extensions: [] 98 extensions: []
99 } 99 }
100 }, 100 },
101 banner: {
102 file: {
103 size: { max: 0 },
104 extensions: []
105 }
106 },
101 video: { 107 video: {
102 image: { 108 image: {
103 size: { max: 0 }, 109 size: { max: 0 },
diff --git a/client/src/app/core/users/user.model.ts b/client/src/app/core/users/user.model.ts
index 15a4f7f82..8aaaa238d 100644
--- a/client/src/app/core/users/user.model.ts
+++ b/client/src/app/core/users/user.model.ts
@@ -1,7 +1,7 @@
1import { Account } from '@app/shared/shared-main/account/account.model' 1import { Account } from '@app/shared/shared-main/account/account.model'
2import { hasUserRight } from '@shared/core-utils/users' 2import { hasUserRight } from '@shared/core-utils/users'
3import { 3import {
4 Avatar, 4 ActorImage,
5 NSFWPolicyType, 5 NSFWPolicyType,
6 User as UserServerModel, 6 User as UserServerModel,
7 UserAdminFlag, 7 UserAdminFlag,
@@ -131,7 +131,7 @@ export class User implements UserServerModel {
131 } 131 }
132 } 132 }
133 133
134 updateAccountAvatar (newAccountAvatar?: Avatar) { 134 updateAccountAvatar (newAccountAvatar?: ActorImage) {
135 if (newAccountAvatar) this.account.updateAvatar(newAccountAvatar) 135 if (newAccountAvatar) this.account.updateAvatar(newAccountAvatar)
136 else this.account.resetAvatar() 136 else this.account.resetAvatar()
137 } 137 }
diff --git a/client/src/app/core/users/user.service.ts b/client/src/app/core/users/user.service.ts
index 33cc1f668..3de83152c 100644
--- a/client/src/app/core/users/user.service.ts
+++ b/client/src/app/core/users/user.service.ts
@@ -7,8 +7,7 @@ import { AuthService } from '@app/core/auth'
7import { getBytes } from '@root-helpers/bytes' 7import { getBytes } from '@root-helpers/bytes'
8import { UserLocalStorageKeys } from '@root-helpers/users' 8import { UserLocalStorageKeys } from '@root-helpers/users'
9import { 9import {
10 Avatar, 10 ActorImage,
11 NSFWPolicyType,
12 ResultList, 11 ResultList,
13 User as UserServerModel, 12 User as UserServerModel,
14 UserCreate, 13 UserCreate,
@@ -136,7 +135,7 @@ export class UserService {
136 changeAvatar (avatarForm: FormData) { 135 changeAvatar (avatarForm: FormData) {
137 const url = UserService.BASE_USERS_URL + 'me/avatar/pick' 136 const url = UserService.BASE_USERS_URL + 'me/avatar/pick'
138 137
139 return this.authHttp.post<{ avatar: Avatar }>(url, avatarForm) 138 return this.authHttp.post<{ avatar: ActorImage }>(url, avatarForm)
140 .pipe(catchError(err => this.restExtractor.handleError(err))) 139 .pipe(catchError(err => this.restExtractor.handleError(err)))
141 } 140 }
142 141
diff --git a/client/src/app/core/wrappers/screen.service.ts b/client/src/app/core/wrappers/screen.service.ts
index a085e5bdc..c133b5fe9 100644
--- a/client/src/app/core/wrappers/screen.service.ts
+++ b/client/src/app/core/wrappers/screen.service.ts
@@ -38,11 +38,10 @@ export class ScreenService {
38 38
39 let numberOfVideos = 1 39 let numberOfVideos = 1
40 40
41 if (screenWidth > 1850) numberOfVideos = 7 41 if (screenWidth > 1850) numberOfVideos = 5
42 else if (screenWidth > 1600) numberOfVideos = 6 42 else if (screenWidth > 1600) numberOfVideos = 4
43 else if (screenWidth > 1370) numberOfVideos = 5 43 else if (screenWidth > 1370) numberOfVideos = 3
44 else if (screenWidth > 1100) numberOfVideos = 4 44 else if (screenWidth > 1100) numberOfVideos = 2
45 else if (screenWidth > 850) numberOfVideos = 3
46 45
47 return numberOfVideos 46 return numberOfVideos
48 } 47 }
diff --git a/client/src/app/header/search-typeahead.component.html b/client/src/app/header/search-typeahead.component.html
index 03e86b8e6..f84086b4a 100644
--- a/client/src/app/header/search-typeahead.component.html
+++ b/client/src/app/header/search-typeahead.component.html
@@ -34,7 +34,8 @@
34 34
35 <!-- search instructions, when search input is empty --> 35 <!-- search instructions, when search input is empty -->
36 <div *ngIf="areInstructionsDisplayed()" id="typeahead-instructions" class="overflow-hidden"> 36 <div *ngIf="areInstructionsDisplayed()" id="typeahead-instructions" class="overflow-hidden">
37 <div class="d-flex justify-content-between"> 37 <span class="text-muted" i18n>Your query will be matched against video names or descriptions, channel names.</span>
38 <div class="d-flex justify-content-between mt-3">
38 <label class="small-title" i18n>ADVANCED SEARCH</label> 39 <label class="small-title" i18n>ADVANCED SEARCH</label>
39 <div class="advanced-search-status c-help"> 40 <div class="advanced-search-status c-help">
40 <span [ngClass]="canSearchAnyURI ? 'text-success' : 'text-muted'" i18n-title title="Determines whether you can resolve any distant content, or if this instance only allows doing so for instances it follows."> 41 <span [ngClass]="canSearchAnyURI ? 'text-success' : 'text-muted'" i18n-title title="Determines whether you can resolve any distant content, or if this instance only allows doing so for instances it follows.">
@@ -55,7 +56,6 @@
55 <em>UUID</em> <span class="text-muted" i18n>will list the matching video</span> 56 <em>UUID</em> <span class="text-muted" i18n>will list the matching video</span>
56 </li> 57 </li>
57 </ul> 58 </ul>
58 <span class="text-muted" i18n>Any other input will return matching video or channel names.</span>
59 </div> 59 </div>
60 </div> 60 </div>
61 61
diff --git a/client/src/app/header/search-typeahead.component.scss b/client/src/app/header/search-typeahead.component.scss
index f8d68e986..a60aa38d6 100644
--- a/client/src/app/header/search-typeahead.component.scss
+++ b/client/src/app/header/search-typeahead.component.scss
@@ -86,7 +86,7 @@ li.suggestion {
86 flex: 1; 86 flex: 1;
87 87
88 input { 88 input {
89 width: unset; 89 width: 70px;
90 } 90 }
91 } 91 }
92 92
diff --git a/client/src/app/menu/menu.component.scss b/client/src/app/menu/menu.component.scss
index 2ea66e57d..aa247d268 100644
--- a/client/src/app/menu/menu.component.scss
+++ b/client/src/app/menu/menu.component.scss
@@ -3,6 +3,7 @@
3 3
4$menu-link-icon-size: 22px; 4$menu-link-icon-size: 22px;
5$menu-link-icon-margin-right: 18px; 5$menu-link-icon-margin-right: 18px;
6$footer-links-base-opacity: .8;
6 7
7@mixin menu-link { 8@mixin menu-link {
8 display: flex; 9 display: flex;
@@ -91,168 +92,168 @@ menu {
91 align-items: center; 92 align-items: center;
92 justify-content: left; 93 justify-content: left;
93 94
94 .logged-in-more { 95 my-notification {
95 $main-radius: 25px; 96 margin-left: auto;
97 margin-right: 15px;
98 }
99 }
100}
96 101
97 flex: 1; 102.logged-in-more {
98 margin-left: 13px; 103 $main-radius: 25px;
99 border-radius: $main-radius;
100 transition: all .1s ease-in-out;
101 cursor: pointer;
102 104
103 *, & { 105 flex: 1;
104 line-height: 1; 106 margin-left: 13px;
105 } 107 border-radius: $main-radius;
108 transition: all .1s ease-in-out;
109 cursor: pointer;
106 110
107 &.show { 111 *, & {
108 background-color: rgba(255, 255, 255, 0.20); 112 line-height: 1;
109 box-shadow: inset 0 3px 5px rgba(0, 0, 0, .325); 113 }
110 }
111 114
112 @mixin display-hints($is-mobile: false) { 115 &.show {
113 background-color: rgba(255, 255, 255, 0.15); 116 background-color: rgba(255, 255, 255, 0.20);
114 117 box-shadow: inset 0 3px 5px rgba(0, 0, 0, .325);
115 @if $is-mobile { 118 }
116 .dropdown-toggle-indicator { 119
117 display: inherit !important; 120 @mixin display-hints($is-mobile: false) {
118 } 121 background-color: rgba(255, 255, 255, 0.15);
119 .dropdown-toggle:first-child {
120 padding-right: 30px !important;
121 }
122 }
123 }
124 122
125 &:hover { 123 @if $is-mobile {
126 @include display-hints; 124 .dropdown-toggle-indicator {
125 display: inherit !important;
126 }
127 .dropdown-toggle:first-child {
128 padding-right: 30px !important;
127 } 129 }
130 }
131 }
128 132
129 /* smartphones and touchscreens */ 133 &:hover {
130 @media (hover: none) and (pointer: coarse) { 134 @include display-hints;
131 @include display-hints($is-mobile: true); 135 }
132 136
133 /* fill space when on mobile */ 137 /* smartphones and touchscreens */
134 max-width: calc(100% - 80px); 138 @media (hover: none) and (pointer: coarse) {
135 .dropdown-toggle { 139 @include display-hints($is-mobile: true);
136 max-width: 100%;
137 }
138 .logged-in-info {
139 max-width: calc(100% - 45px) !important;
140 }
141 140
142 } 141 /* fill space when on mobile */
142 max-width: calc(100% - 80px);
143 .dropdown-toggle {
144 max-width: 100%;
145 }
146 .logged-in-info {
147 max-width: calc(100% - 45px) !important;
148 }
143 149
144 .dropdown-toggle-indicator { 150 }
145 position: relative;
146 width: 0;
147 display: none;
148
149 span {
150 position: absolute;
151 right: -35px;
152 top: -8px;
153 color: grey;
154 width: $main-radius;
155 }
156 }
157 151
158 .dropdown-toggle { 152 .dropdown-toggle-indicator {
159 &::after { 153 position: relative;
160 border: none; 154 width: 0;
161 } 155 display: none;
162 }
163 156
164 .dropdown-toggle:first-child { 157 span {
165 display: flex; 158 position: absolute;
166 align-items: center; 159 right: -35px;
167 padding: 5px 7px; 160 top: -8px;
168 border-radius: $main-radius; 161 color: grey;
169 } 162 width: $main-radius;
163 }
164 }
170 165
171 img { 166 .dropdown-toggle {
172 @include avatar(34px); 167 &::after {
168 border: none;
169 }
170 }
173 171
174 margin-right: 10px; 172 .dropdown-toggle:first-child {
175 } 173 display: flex;
174 align-items: center;
175 padding: 5px 7px;
176 border-radius: $main-radius;
177 }
178
179 img {
180 @include avatar(34px);
176 181
177 .logged-in-info { 182 margin-right: 10px;
178 max-width: 105px; 183 }
184}
179 185
180 flex-grow: 1; 186.logged-in-info {
187 max-width: 105px;
181 188
182 .logged-in-display-name, 189 flex-grow: 1;
183 .logged-in-username {
184 @include ellipsis;
185 }
186 190
187 .logged-in-display-name { 191 .logged-in-display-name,
188 font-size: 16px; 192 .logged-in-username {
189 font-weight: $font-semibold; 193 @include ellipsis;
190 color: pvar(--menuForegroundColor); 194 }
191 195
192 @include disable-default-a-behaviour; 196 .logged-in-display-name {
193 } 197 font-size: 16px;
198 font-weight: $font-semibold;
199 color: pvar(--menuForegroundColor);
194 200
195 .logged-in-username { 201 @include disable-default-a-behaviour;
196 font-size: 13px; 202 }
197 color: #C6C6C6;
198 margin-top: 3px;
199 }
200 }
201 }
202 203
203 my-notification { 204 .logged-in-username {
204 margin-left: auto; 205 font-size: 13px;
205 margin-right: 15px; 206 color: #C6C6C6;
206 } 207 margin-top: 3px;
207 } 208 }
209}
208 210
209 .logged-in-menu { 211.logged-in-menu {
210 display: flex; 212 display: flex;
211 flex-direction: column; 213 flex-direction: column;
212 align-items: flex-start; 214 align-items: flex-start;
213 border-top: 1px solid var(--greyForegroundColor); 215 border-top: 1px solid var(--greyForegroundColor);
214 line-height: $line-height-normal; 216 line-height: $line-height-normal;
215 217
216 a { 218 a {
217 @include menu-link; 219 @include menu-link;
218 @include disable-default-a-behaviour; 220 @include disable-default-a-behaviour;
219 221
220 $icon-size: 13px; 222 $icon-size: 13px;
221 $additional-margin: ($menu-link-icon-size - $icon-size) / 2; 223 $additional-margin: ($menu-link-icon-size - $icon-size) / 2;
222 224
223 font-size: 14px; 225 font-size: 14px;
224 width: 100%; 226 width: 100%;
225 min-height: 35px; 227 min-height: 35px;
226 228
227 my-global-icon { 229 my-global-icon {
228 width: $icon-size; 230 width: $icon-size;
229 height: $icon-size; 231 height: $icon-size;
230 232
231 // Keep aligned with other icons 233 // Keep aligned with other icons
232 margin-left: $additional-margin; 234 margin-left: $additional-margin;
233 235
234 &[iconName="channel"] { 236 &[iconName="channel"] {
235 margin-top: -2px; 237 margin-top: -2px;
236 }
237 } 238 }
239 }
238 240
239 &.active, 241 &.active,
240 &:hover, 242 &:hover,
241 &:focus-visible { 243 &:focus-visible {
242 my-global-icon { 244 my-global-icon {
243 @include apply-svg-color(var(--menuForegroundColor)); 245 @include apply-svg-color(var(--menuForegroundColor));
244 }
245 } 246 }
247 }
246 248
247 &.active { 249 &.active {
248 $border-left-width: 4px; 250 $border-left-width: 4px;
249 251
250 font-weight: $font-semibold; 252 font-weight: $font-semibold;
251 border-left: $border-left-width solid var(--mainColor); 253 border-left: $border-left-width solid var(--mainColor);
252 254
253 my-global-icon { 255 my-global-icon {
254 margin-left: $additional-margin - $border-left-width; 256 margin-left: $additional-margin - $border-left-width;
255 }
256 } 257 }
257 } 258 }
258 } 259 }
@@ -333,50 +334,48 @@ menu {
333 flex-direction: column; 334 flex-direction: column;
334 padding: 0 $menu-lateral-padding; 335 padding: 0 $menu-lateral-padding;
335 } 336 }
337}
336 338
337 $footer-links-base-opacity: .8; 339.footer-links {
338 340 &, > div {
339 .footer-links { 341 display: flex;
340 &, > div { 342 flex-wrap: wrap;
341 display: flex; 343 }
342 flex-wrap: wrap;
343 }
344 344
345 a, span[role=button] { 345 a, span[role=button] {
346 display: inline-block; 346 display: inline-block;
347 text-decoration: none; 347 text-decoration: none;
348 color: pvar(--menuForegroundColor); 348 color: pvar(--menuForegroundColor);
349 opacity: $footer-links-base-opacity; 349 opacity: $footer-links-base-opacity;
350 white-space: nowrap;
351 font-size: 90%;
352 font-weight: 500;
353 line-height: 1.4rem;
354 margin-right: 8px;
355
356 &.inline-global-icon {
357 display: inline-flex;
358 align-items: center;
350 white-space: nowrap; 359 white-space: nowrap;
351 font-size: 90%; 360 height: 1.4rem;
352 font-weight: 500; 361
353 line-height: 1.4rem; 362 my-global-icon {
354 margin-right: 8px; 363 @include apply-svg-color(pvar(--menuForegroundColor));
355 364
356 &.inline-global-icon { 365 display: flex;
357 display: inline-flex; 366 width: auto;
358 align-items: center; 367 height: 90%;
359 white-space: nowrap; 368 margin-right: .2rem;
360 height: 1.4rem;
361
362 my-global-icon {
363 @include apply-svg-color(pvar(--menuForegroundColor));
364
365 display: flex;
366 width: auto;
367 height: 90%;
368 margin-right: .2rem;
369 }
370 } 369 }
371 } 370 }
372 } 371 }
372}
373 373
374 .footer-copyleft small a { 374.footer-copyleft small a {
375 @include disable-default-a-behaviour; 375 @include disable-default-a-behaviour;
376 376
377 color: pvar(--menuForegroundColor); 377 color: pvar(--menuForegroundColor);
378 opacity: $footer-links-base-opacity - .2; 378 opacity: $footer-links-base-opacity - .2;
379 }
380} 379}
381 380
382.dropdown { 381.dropdown {
diff --git a/client/src/app/menu/menu.component.ts b/client/src/app/menu/menu.component.ts
index ed20d9c01..9b6b7cda5 100644
--- a/client/src/app/menu/menu.component.ts
+++ b/client/src/app/menu/menu.component.ts
@@ -10,6 +10,7 @@ import { LanguageChooserComponent } from '@app/menu/language-chooser.component'
10import { QuickSettingsModalComponent } from '@app/modal/quick-settings-modal.component' 10import { QuickSettingsModalComponent } from '@app/modal/quick-settings-modal.component'
11import { ServerConfig, UserRight, VideoConstant } from '@shared/models' 11import { ServerConfig, UserRight, VideoConstant } from '@shared/models'
12import { NgbDropdown, NgbDropdownConfig } from '@ng-bootstrap/ng-bootstrap' 12import { NgbDropdown, NgbDropdownConfig } from '@ng-bootstrap/ng-bootstrap'
13import { PeertubeModalService } from '@app/shared/shared-main/peertube-modal/peertube-modal.service'
13 14
14const logger = debug('peertube:menu:MenuComponent') 15const logger = debug('peertube:menu:MenuComponent')
15 16
@@ -54,6 +55,7 @@ export class MenuComponent implements OnInit {
54 private hotkeysService: HotkeysService, 55 private hotkeysService: HotkeysService,
55 private screenService: ScreenService, 56 private screenService: ScreenService,
56 private menuService: MenuService, 57 private menuService: MenuService,
58 private modalService: PeertubeModalService,
57 private dropdownConfig: NgbDropdownConfig, 59 private dropdownConfig: NgbDropdownConfig,
58 private router: Router 60 private router: Router
59 ) { 61 ) {
@@ -130,6 +132,9 @@ export class MenuComponent implements OnInit {
130 this.authService.userInformationLoaded 132 this.authService.userInformationLoaded
131 .subscribe(() => this.buildUserLanguages()) 133 .subscribe(() => this.buildUserLanguages())
132 }) 134 })
135
136 this.modalService.openQuickSettingsSubject
137 .subscribe(() => this.openQuickSettings())
133 } 138 }
134 139
135 isRegistrationAllowed () { 140 isRegistrationAllowed () {
diff --git a/client/src/app/menu/notification.component.scss b/client/src/app/menu/notification.component.scss
index 40feb9e66..c65787779 100644
--- a/client/src/app/menu/notification.component.scss
+++ b/client/src/app/menu/notification.component.scss
@@ -1,6 +1,9 @@
1@import '_variables'; 1@import '_variables';
2@import '_mixins'; 2@import '_mixins';
3 3
4.content {
5 scrollbar-color: auto;
6}
4 7
5.notification-inbox-popover { 8.notification-inbox-popover {
6 padding: 10px; 9 padding: 10px;
diff --git a/client/src/app/modal/instance-config-warning-modal.component.html b/client/src/app/modal/instance-config-warning-modal.component.html
index 5a8adf726..498adfeff 100644
--- a/client/src/app/modal/instance-config-warning-modal.component.html
+++ b/client/src/app/modal/instance-config-warning-modal.component.html
@@ -15,7 +15,7 @@
15 15
16 <li i18n *ngIf="!about.instance.administrator">Who you are</li> 16 <li i18n *ngIf="!about.instance.administrator">Who you are</li>
17 <li i18n *ngIf="!about.instance.maintenanceLifetime">How long you plan to maintain your instance</li> 17 <li i18n *ngIf="!about.instance.maintenanceLifetime">How long you plan to maintain your instance</li>
18 <li i18n *ngIf="!about.instance.businessModel">How you plan to pay your instance</li> 18 <li i18n *ngIf="!about.instance.businessModel">How you plan to pay for keeping your instance running</li>
19 19
20 <li i18n *ngIf="!about.instance.moderationInformation">How you will moderate your instance</li> 20 <li i18n *ngIf="!about.instance.moderationInformation">How you will moderate your instance</li>
21 <li i18n *ngIf="!about.instance.terms">Instance terms</li> 21 <li i18n *ngIf="!about.instance.terms">Instance terms</li>
diff --git a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts
index e34836a18..eeb9f128b 100644
--- a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts
+++ b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts
@@ -117,7 +117,8 @@ export class AbuseListTableComponent extends RestTable implements OnInit, AfterV
117 warningTitle: false, 117 warningTitle: false,
118 startTime: abuse.video.startAt, 118 startTime: abuse.video.startAt,
119 stopTime: abuse.video.endAt 119 stopTime: abuse.video.endAt
120 }) 120 }),
121 abuse.video.name
121 ) 122 )
122 } 123 }
123 124
diff --git a/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.html b/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.html
new file mode 100644
index 000000000..0829263f4
--- /dev/null
+++ b/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.html
@@ -0,0 +1,41 @@
1<div class="actor" *ngIf="actor">
2 <div class="d-flex">
3 <img [ngClass]="{ channel: isChannel() }" [src]="preview || actor.avatarUrl" alt="Avatar" />
4
5 <div class="actor-img-edit-container">
6
7 <div *ngIf="editable && !hasAvatar()" class="actor-img-edit-button" [ngbTooltip]="avatarFormat" placement="right" container="body">
8 <my-global-icon iconName="upload"></my-global-icon>
9 <label class="sr-only" for="avatarfile" i18n>Upload a new avatar</label>
10 <input #avatarfileInput type="file" name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange(avatarfileInput)"/>
11 </div>
12
13 <div
14 *ngIf="editable && hasAvatar()" class="actor-img-edit-button"
15 #avatarPopover="ngbPopover" [ngbPopover]="avatarEditContent" popoverClass="popover-image-info" autoClose="outside" placement="right"
16 >
17 <my-global-icon iconName="edit"></my-global-icon>
18 <label class="sr-only" for="avatarMenu" i18n>Change your avatar</label>
19 </div>
20
21 </div>
22 </div>
23
24 <div class="actor-info">
25 <div class="actor-info-display-name">{{ actor.displayName }}</div>
26 <div *ngIf="displayUsername" class="actor-info-username">{{ actor.name }}</div>
27 <div *ngIf="displaySubscribers" i18n class="actor-info-followers">{{ actor.followersCount }} subscribers</div>
28 </div>
29</div>
30
31<ng-template #avatarEditContent>
32 <div class="dropdown-item c-hand" [ngbTooltip]="avatarFormat" placement="right" container="body">
33 <my-global-icon iconName="upload"></my-global-icon>
34 <span for="avatarfile" i18n>Upload a new avatar</span>
35 <input #avatarfileInput type="file" name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange(avatarfileInput)"/>
36 </div>
37 <div class="dropdown-item c-hand" (click)="deleteAvatar()" (key.enter)="deleteAvatar()">
38 <my-global-icon iconName="delete"></my-global-icon>
39 <span i18n>Remove avatar</span>
40 </div>
41</ng-template>
diff --git a/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.scss b/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.scss
new file mode 100644
index 000000000..8b0172315
--- /dev/null
+++ b/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.scss
@@ -0,0 +1,54 @@
1@import '_variables';
2@import '_mixins';
3
4.actor {
5 display: flex;
6
7 img {
8 margin-right: 15px;
9
10 &:not(.channel) {
11 @include avatar(100px);
12 }
13
14 &.channel {
15 @include channel-avatar(100px);
16 }
17 }
18
19 .actor-info {
20 display: inline-flex;
21 flex-direction: column;
22
23 .actor-info-display-name {
24 font-size: 20px;
25 font-weight: $font-bold;
26
27 @media screen and (max-width: $small-view) {
28 font-size: 16px;
29 }
30 }
31
32 .actor-info-username {
33 position: relative;
34 font-size: 14px;
35 color: pvar(--greyForegroundColor);
36 }
37
38 .actor-info-followers {
39 font-size: 15px;
40 padding-bottom: .5rem;
41 }
42 }
43}
44
45.actor-img-edit-container {
46 position: relative;
47 width: 0;
48}
49
50.actor-img-edit-button {
51 top: 55px;
52 right: 45px;
53 border-radius: 50%;
54}
diff --git a/client/src/app/shared/shared-main/account/actor-avatar-info.component.ts b/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.ts
index b459c591f..d0d269489 100644
--- a/client/src/app/shared/shared-main/account/actor-avatar-info.component.ts
+++ b/client/src/app/shared/shared-actor-image/actor-avatar-edit.component.ts
@@ -1,21 +1,27 @@
1import { Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core' 1import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
2import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'
2import { Notifier, ServerService } from '@app/core' 3import { Notifier, ServerService } from '@app/core'
4import { Account, VideoChannel } from '@app/shared/shared-main'
3import { NgbPopover } from '@ng-bootstrap/ng-bootstrap' 5import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
4import { getBytes } from '@root-helpers/bytes' 6import { getBytes } from '@root-helpers/bytes'
5import { Account } from '../account/account.model'
6import { VideoChannel } from '../video-channel/video-channel.model'
7import { Actor } from './actor.model'
8 7
9@Component({ 8@Component({
10 selector: 'my-actor-avatar-info', 9 selector: 'my-actor-avatar-edit',
11 templateUrl: './actor-avatar-info.component.html', 10 templateUrl: './actor-avatar-edit.component.html',
12 styleUrls: [ './actor-avatar-info.component.scss' ] 11 styleUrls: [
12 './actor-image-edit.scss',
13 './actor-avatar-edit.component.scss'
14 ]
13}) 15})
14export class ActorAvatarInfoComponent implements OnInit, OnChanges { 16export class ActorAvatarEditComponent implements OnInit {
15 @ViewChild('avatarfileInput') avatarfileInput: ElementRef<HTMLInputElement> 17 @ViewChild('avatarfileInput') avatarfileInput: ElementRef<HTMLInputElement>
16 @ViewChild('avatarPopover') avatarPopover: NgbPopover 18 @ViewChild('avatarPopover') avatarPopover: NgbPopover
17 19
18 @Input() actor: VideoChannel | Account 20 @Input() actor: VideoChannel | Account
21 @Input() editable = true
22 @Input() displaySubscribers = true
23 @Input() displayUsername = true
24 @Input() previewImage = false
19 25
20 @Output() avatarChange = new EventEmitter<FormData>() 26 @Output() avatarChange = new EventEmitter<FormData>()
21 @Output() avatarDelete = new EventEmitter<void>() 27 @Output() avatarDelete = new EventEmitter<void>()
@@ -24,9 +30,10 @@ export class ActorAvatarInfoComponent implements OnInit, OnChanges {
24 maxAvatarSize = 0 30 maxAvatarSize = 0
25 avatarExtensions = '' 31 avatarExtensions = ''
26 32
27 private avatarUrl: string 33 preview: SafeResourceUrl
28 34
29 constructor ( 35 constructor (
36 private sanitizer: DomSanitizer,
30 private serverService: ServerService, 37 private serverService: ServerService,
31 private notifier: Notifier 38 private notifier: Notifier
32 ) { } 39 ) { }
@@ -42,12 +49,6 @@ export class ActorAvatarInfoComponent implements OnInit, OnChanges {
42 }) 49 })
43 } 50 }
44 51
45 ngOnChanges (changes: SimpleChanges) {
46 if (changes['actor']) {
47 this.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.actor)
48 }
49 }
50
51 onAvatarChange (input: HTMLInputElement) { 52 onAvatarChange (input: HTMLInputElement) {
52 this.avatarfileInput = new ElementRef(input) 53 this.avatarfileInput = new ElementRef(input)
53 54
@@ -61,13 +62,22 @@ export class ActorAvatarInfoComponent implements OnInit, OnChanges {
61 formData.append('avatarfile', avatarfile) 62 formData.append('avatarfile', avatarfile)
62 this.avatarPopover?.close() 63 this.avatarPopover?.close()
63 this.avatarChange.emit(formData) 64 this.avatarChange.emit(formData)
65
66 if (this.previewImage) {
67 this.preview = this.sanitizer.bypassSecurityTrustResourceUrl(URL.createObjectURL(avatarfile))
68 }
64 } 69 }
65 70
66 deleteAvatar () { 71 deleteAvatar () {
72 this.preview = undefined
67 this.avatarDelete.emit() 73 this.avatarDelete.emit()
68 } 74 }
69 75
70 hasAvatar () { 76 hasAvatar () {
71 return !!this.avatarUrl 77 return !!this.preview || !!this.actor.avatar
78 }
79
80 isChannel () {
81 return !!(this.actor as VideoChannel).ownerAccount
72 } 82 }
73} 83}
diff --git a/client/src/app/shared/shared-actor-image/actor-banner-edit.component.html b/client/src/app/shared/shared-actor-image/actor-banner-edit.component.html
new file mode 100644
index 000000000..266fc26c5
--- /dev/null
+++ b/client/src/app/shared/shared-actor-image/actor-banner-edit.component.html
@@ -0,0 +1,34 @@
1<div class="actor" *ngIf="actor">
2 <div class="actor-img-edit-container">
3 <div class="banner-placeholder">
4 <img *ngIf="hasBanner()" [src]="preview || actor.bannerUrl" alt="Banner" />
5 </div>
6
7 <div *ngIf="!hasBanner()" class="actor-img-edit-button" [ngbTooltip]="bannerFormat" placement="right" container="body">
8 <my-global-icon iconName="upload"></my-global-icon>
9 <label for="bannerfile" i18n>Upload a new banner</label>
10 <input #bannerfileInput type="file" name="bannerfile" id="bannerfile" [accept]="bannerExtensions" (change)="onBannerChange(bannerfileInput)"/>
11 </div>
12
13 <div
14 *ngIf="hasBanner()" class="actor-img-edit-button"
15 #bannerPopover="ngbPopover" [ngbPopover]="bannerEditContent" popoverClass="popover-image-info" autoClose="outside" placement="right"
16 >
17 <my-global-icon iconName="edit"></my-global-icon>
18 <label for="bannerMenu" i18n>Change your banner</label>
19 </div>
20 </div>
21</div>
22
23<ng-template #bannerEditContent>
24 <div class="dropdown-item c-hand" [ngbTooltip]="bannerFormat" placement="right" container="body">
25 <my-global-icon iconName="upload"></my-global-icon>
26 <span for="bannerfile" i18n>Upload a new banner</span>
27 <input #bannerfileInput type="file" name="bannerfile" id="bannerfile" [accept]="bannerExtensions" (change)="onBannerChange(bannerfileInput)"/>
28 </div>
29
30 <div class="dropdown-item c-hand" (click)="deleteBanner()" (key.enter)="deleteBanner()">
31 <my-global-icon iconName="delete"></my-global-icon>
32 <span i18n>Remove banner</span>
33 </div>
34</ng-template>
diff --git a/client/src/app/shared/shared-actor-image/actor-banner-edit.component.scss b/client/src/app/shared/shared-actor-image/actor-banner-edit.component.scss
new file mode 100644
index 000000000..23606f871
--- /dev/null
+++ b/client/src/app/shared/shared-actor-image/actor-banner-edit.component.scss
@@ -0,0 +1,27 @@
1@import '_variables';
2@import '_mixins';
3
4.banner-placeholder {
5 @include block-ratio('> div, > img', $banner-inverted-ratio);
6}
7
8.banner-placeholder {
9 background-color: pvar(--greyBackgroundColor);
10}
11
12.actor-img-edit-container {
13 position: relative;
14 display: flex;
15 justify-content: center;
16 align-items: center;
17}
18
19.actor-img-edit-button {
20 position: absolute;
21 width: auto;
22
23 label {
24 font-weight: $font-semibold;
25 margin-bottom: 0;
26 }
27}
diff --git a/client/src/app/shared/shared-actor-image/actor-banner-edit.component.ts b/client/src/app/shared/shared-actor-image/actor-banner-edit.component.ts
new file mode 100644
index 000000000..8c12d3c4c
--- /dev/null
+++ b/client/src/app/shared/shared-actor-image/actor-banner-edit.component.ts
@@ -0,0 +1,76 @@
1import { Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core'
2import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'
3import { Notifier, ServerService } from '@app/core'
4import { VideoChannel } from '@app/shared/shared-main'
5import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
6import { getBytes } from '@root-helpers/bytes'
7
8@Component({
9 selector: 'my-actor-banner-edit',
10 templateUrl: './actor-banner-edit.component.html',
11 styleUrls: [
12 './actor-image-edit.scss',
13 './actor-banner-edit.component.scss'
14 ]
15})
16export class ActorBannerEditComponent implements OnInit {
17 @ViewChild('bannerfileInput') bannerfileInput: ElementRef<HTMLInputElement>
18 @ViewChild('bannerPopover') bannerPopover: NgbPopover
19
20 @Input() actor: VideoChannel
21 @Input() previewImage = false
22
23 @Output() bannerChange = new EventEmitter<FormData>()
24 @Output() bannerDelete = new EventEmitter<void>()
25
26 bannerFormat = ''
27 maxBannerSize = 0
28 bannerExtensions = ''
29
30 preview: SafeResourceUrl
31
32 constructor (
33 private sanitizer: DomSanitizer,
34 private serverService: ServerService,
35 private notifier: Notifier
36 ) { }
37
38 ngOnInit (): void {
39 this.serverService.getConfig()
40 .subscribe(config => {
41 this.maxBannerSize = config.banner.file.size.max
42 this.bannerExtensions = config.banner.file.extensions.join(', ')
43
44 // tslint:disable:max-line-length
45 this.bannerFormat = $localize`ratio 6/1, recommended size: 1600x266, max size: ${getBytes(this.maxBannerSize)}, extensions: ${this.bannerExtensions}`
46 })
47 }
48
49 onBannerChange (input: HTMLInputElement) {
50 this.bannerfileInput = new ElementRef(input)
51
52 const bannerfile = this.bannerfileInput.nativeElement.files[ 0 ]
53 if (bannerfile.size > this.maxBannerSize) {
54 this.notifier.error('Error', $localize`This image is too large.`)
55 return
56 }
57
58 const formData = new FormData()
59 formData.append('bannerfile', bannerfile)
60 this.bannerPopover?.close()
61 this.bannerChange.emit(formData)
62
63 if (this.previewImage) {
64 this.preview = this.sanitizer.bypassSecurityTrustResourceUrl(URL.createObjectURL(bannerfile))
65 }
66 }
67
68 deleteBanner () {
69 this.preview = undefined
70 this.bannerDelete.emit()
71 }
72
73 hasBanner () {
74 return !!this.preview || !!this.actor.bannerUrl
75 }
76}
diff --git a/client/src/app/shared/shared-actor-image/actor-image-edit.scss b/client/src/app/shared/shared-actor-image/actor-image-edit.scss
new file mode 100644
index 000000000..918955a89
--- /dev/null
+++ b/client/src/app/shared/shared-actor-image/actor-image-edit.scss
@@ -0,0 +1,35 @@
1@import '_variables';
2@import '_mixins';
3
4.actor ::ng-deep .popover-image-info .popover-body {
5 padding: 0;
6
7 .dropdown-item {
8 padding: 6px 10px;
9 border-radius: 4px;
10
11 &:first-child {
12 @include peertube-file;
13 display: block;
14 }
15 }
16}
17
18.actor-img-edit-button {
19 @include peertube-button-file(21px);
20 @include button-with-icon(19px);
21 @include orange-button;
22
23 margin-top: 10px;
24 margin-bottom: 5px;
25 cursor: pointer;
26
27 input {
28 width: 30px;
29 height: 30px;
30 }
31
32 my-global-icon {
33 right: 7px;
34 }
35}
diff --git a/client/src/app/shared/shared-actor-image/index.ts b/client/src/app/shared/shared-actor-image/index.ts
new file mode 100644
index 000000000..18a9038eb
--- /dev/null
+++ b/client/src/app/shared/shared-actor-image/index.ts
@@ -0,0 +1 @@
export * from './shared-actor-image.module'
diff --git a/client/src/app/shared/shared-actor-image/shared-actor-image.module.ts b/client/src/app/shared/shared-actor-image/shared-actor-image.module.ts
new file mode 100644
index 000000000..6044f9925
--- /dev/null
+++ b/client/src/app/shared/shared-actor-image/shared-actor-image.module.ts
@@ -0,0 +1,29 @@
1
2import { CommonModule } from '@angular/common'
3import { NgModule } from '@angular/core'
4import { SharedGlobalIconModule } from '../shared-icons'
5import { SharedMainModule } from '../shared-main'
6import { ActorAvatarEditComponent } from './actor-avatar-edit.component'
7import { ActorBannerEditComponent } from './actor-banner-edit.component'
8
9@NgModule({
10 imports: [
11 CommonModule,
12
13 SharedMainModule,
14 SharedGlobalIconModule
15 ],
16
17 declarations: [
18 ActorAvatarEditComponent,
19 ActorBannerEditComponent
20 ],
21
22 exports: [
23 ActorAvatarEditComponent,
24 ActorBannerEditComponent
25 ],
26
27 providers: [ ]
28})
29export class SharedActorImageModule { }
diff --git a/client/src/app/shared/shared-forms/input-toggle-hidden.component.html b/client/src/app/shared/shared-forms/input-toggle-hidden.component.html
index e7441e4c1..9f252f299 100644
--- a/client/src/app/shared/shared-forms/input-toggle-hidden.component.html
+++ b/client/src/app/shared/shared-forms/input-toggle-hidden.component.html
@@ -12,9 +12,10 @@
12 12
13 <button 13 <button
14 *ngIf="withCopy" [cdkCopyToClipboard]="input.value" (click)="activateCopiedMessage()" type="button" 14 *ngIf="withCopy" [cdkCopyToClipboard]="input.value" (click)="activateCopiedMessage()" type="button"
15 class="btn btn-outline-secondary" i18n-title title="Copy" 15 class="btn btn-outline-secondary text-uppercase" i18n-title title="Copy"
16 > 16 >
17 <span class="glyphicon glyphicon-copy"></span> 17 <span class="glyphicon glyphicon-duplicate"></span>
18 Copy
18 </button> 19 </button>
19 </div> 20 </div>
20</div> 21</div>
diff --git a/client/src/app/shared/shared-forms/markdown-textarea.component.scss b/client/src/app/shared/shared-forms/markdown-textarea.component.scss
index fcddfea03..8203c7d1c 100644
--- a/client/src/app/shared/shared-forms/markdown-textarea.component.scss
+++ b/client/src/app/shared/shared-forms/markdown-textarea.component.scss
@@ -131,7 +131,7 @@ $input-border-radius: 3px;
131 border-right: none; 131 border-right: none;
132 132
133 :last-child { 133 :last-child {
134 margin-right: $not-expanded-horizontal-margins; 134 margin-right: pvar(--horizontalMarginContent);
135 } 135 }
136 } 136 }
137 137
diff --git a/client/src/app/shared/shared-forms/select/select-options.component.ts b/client/src/app/shared/shared-forms/select/select-options.component.ts
index 2890670e5..8482b9dea 100644
--- a/client/src/app/shared/shared-forms/select/select-options.component.ts
+++ b/client/src/app/shared/shared-forms/select/select-options.component.ts
@@ -1,4 +1,4 @@
1import { Component, forwardRef, Input } from '@angular/core' 1import { Component, forwardRef, HostListener, Input } from '@angular/core'
2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' 2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
3import { SelectOptionsItem } from '../../../../types/select-options-item.model' 3import { SelectOptionsItem } from '../../../../types/select-options-item.model'
4 4
@@ -26,6 +26,13 @@ export class SelectOptionsComponent implements ControlValueAccessor {
26 26
27 propagateChange = (_: any) => { /* empty */ } 27 propagateChange = (_: any) => { /* empty */ }
28 28
29 // Allow plugins to update our value
30 @HostListener('change', [ '$event.target' ])
31 handleChange (event: any) {
32 this.writeValue(event.value)
33 this.onModelChange()
34 }
35
29 writeValue (id: number | string) { 36 writeValue (id: number | string) {
30 this.selectedId = id 37 this.selectedId = id
31 } 38 }
diff --git a/client/src/app/shared/shared-instance/instance-about-accordion.component.scss b/client/src/app/shared/shared-instance/instance-about-accordion.component.scss
index 275600d60..2f6b420e3 100644
--- a/client/src/app/shared/shared-instance/instance-about-accordion.component.scss
+++ b/client/src/app/shared/shared-instance/instance-about-accordion.component.scss
@@ -31,7 +31,7 @@ ngb-accordion ::ng-deep {
31 padding: 0; 31 padding: 0;
32 32
33 & + .collapse.show { 33 & + .collapse.show {
34 background-color: var(--submenuColor); 34 background-color: var(--submenuBackgroundColor);
35 } 35 }
36 } 36 }
37 } 37 }
diff --git a/client/src/app/shared/shared-instance/instance-features-table.component.html b/client/src/app/shared/shared-instance/instance-features-table.component.html
index ce2557147..d505b6739 100644
--- a/client/src/app/shared/shared-instance/instance-features-table.component.html
+++ b/client/src/app/shared/shared-instance/instance-features-table.component.html
@@ -11,7 +11,7 @@
11 <tr> 11 <tr>
12 <th i18n class="label" scope="row"> 12 <th i18n class="label" scope="row">
13 <div>Default NSFW/sensitive videos policy</div> 13 <div>Default NSFW/sensitive videos policy</div>
14 <div class="more-info">can be redefined by the users</div> 14 <div class="c-hand more-info" (click)="openQuickSettingsHighlight()">can be redefined by the users</div>
15 </th> 15 </th>
16 16
17 <td class="value">{{ buildNSFWLabel() }}</td> 17 <td class="value">{{ buildNSFWLabel() }}</td>
diff --git a/client/src/app/shared/shared-instance/instance-features-table.component.ts b/client/src/app/shared/shared-instance/instance-features-table.component.ts
index 0166157f9..c3b3dfdfd 100644
--- a/client/src/app/shared/shared-instance/instance-features-table.component.ts
+++ b/client/src/app/shared/shared-instance/instance-features-table.component.ts
@@ -1,6 +1,7 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { ServerService } from '@app/core' 2import { ServerService } from '@app/core'
3import { ServerConfig } from '@shared/models' 3import { ServerConfig } from '@shared/models'
4import { PeertubeModalService } from '../shared-main/peertube-modal/peertube-modal.service'
4 5
5@Component({ 6@Component({
6 selector: 'my-instance-features-table', 7 selector: 'my-instance-features-table',
@@ -11,7 +12,10 @@ export class InstanceFeaturesTableComponent implements OnInit {
11 quotaHelpIndication = '' 12 quotaHelpIndication = ''
12 serverConfig: ServerConfig 13 serverConfig: ServerConfig
13 14
14 constructor (private serverService: ServerService) { } 15 constructor (
16 private serverService: ServerService,
17 private modalService: PeertubeModalService
18 ) { }
15 19
16 get initialUserVideoQuota () { 20 get initialUserVideoQuota () {
17 return this.serverConfig.user.videoQuota 21 return this.serverConfig.user.videoQuota
@@ -56,6 +60,10 @@ export class InstanceFeaturesTableComponent implements OnInit {
56 return this.serverService.getServerVersionAndCommit() 60 return this.serverService.getServerVersionAndCommit()
57 } 61 }
58 62
63 openQuickSettingsHighlight () {
64 this.modalService.openQuickSettingsSubject.next()
65 }
66
59 private getApproximateTime (seconds: number) { 67 private getApproximateTime (seconds: number) {
60 const hours = Math.floor(seconds / 3600) 68 const hours = Math.floor(seconds / 3600)
61 let pluralSuffix = '' 69 let pluralSuffix = ''
diff --git a/client/src/app/shared/shared-main/account/account.model.ts b/client/src/app/shared/shared-main/account/account.model.ts
index b71a893d1..17fddff09 100644
--- a/client/src/app/shared/shared-main/account/account.model.ts
+++ b/client/src/app/shared/shared-main/account/account.model.ts
@@ -1,4 +1,4 @@
1import { Account as ServerAccount, Avatar } from '@shared/models' 1import { Account as ServerAccount, ActorImage } from '@shared/models'
2import { Actor } from './actor.model' 2import { Actor } from './actor.model'
3 3
4export class Account extends Actor implements ServerAccount { 4export class Account extends Actor implements ServerAccount {
@@ -38,7 +38,7 @@ export class Account extends Actor implements ServerAccount {
38 this.mutedServerByInstance = false 38 this.mutedServerByInstance = false
39 } 39 }
40 40
41 updateAvatar (newAvatar: Avatar) { 41 updateAvatar (newAvatar: ActorImage) {
42 this.avatar = newAvatar 42 this.avatar = newAvatar
43 43
44 this.updateComputedAttributes() 44 this.updateComputedAttributes()
diff --git a/client/src/app/shared/shared-main/account/actor-avatar-info.component.html b/client/src/app/shared/shared-main/account/actor-avatar-info.component.html
deleted file mode 100644
index 30584fd00..000000000
--- a/client/src/app/shared/shared-main/account/actor-avatar-info.component.html
+++ /dev/null
@@ -1,43 +0,0 @@
1<ng-container *ngIf="actor">
2 <div class="actor">
3 <div class="d-flex">
4 <img [src]="actor.avatarUrl" alt="Avatar" />
5
6 <div class="actor-img-edit-container">
7
8 <div *ngIf="!hasAvatar()" class="actor-img-edit-button" [ngbTooltip]="avatarFormat" placement="right" container="body">
9 <my-global-icon iconName="upload"></my-global-icon>
10 <label class="sr-only" for="avatarfile" i18n>Upload a new avatar</label>
11 <input #avatarfileInput type="file" title=" " name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange(avatarfileInput)"/>
12 </div>
13
14 <div *ngIf="hasAvatar()" class="actor-img-edit-button" #avatarPopover="ngbPopover" [ngbPopover]="avatarEditContent" popoverClass="popover-avatar-info" autoClose="outside" placement="right">
15 <my-global-icon iconName="edit"></my-global-icon>
16 <label class="sr-only" for="avatarMenu" i18n>Change your avatar</label>
17 </div>
18
19 </div>
20 </div>
21
22
23 <div class="actor-info">
24 <div class="actor-info-names">
25 <div class="actor-info-display-name">{{ actor.displayName }}</div>
26 <div class="actor-info-username">{{ actor.name }}</div>
27 </div>
28 <div i18n class="actor-info-followers">{{ actor.followersCount }} subscribers</div>
29 </div>
30 </div>
31</ng-container>
32
33<ng-template #avatarEditContent>
34 <div class="dropdown-item c-hand" [ngbTooltip]="avatarFormat" placement="right" container="body">
35 <my-global-icon iconName="upload"></my-global-icon>
36 <span for="avatarfile" i18n>Upload a new avatar</span>
37 <input #avatarfileInput type="file" title=" " name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange(avatarfileInput)"/>
38 </div>
39 <div class="dropdown-item c-hand" (click)="deleteAvatar()" (key.enter)="deleteAvatar()">
40 <my-global-icon iconName="delete"></my-global-icon>
41 <span i18n>Remove avatar</span>
42 </div>
43</ng-template>
diff --git a/client/src/app/shared/shared-main/account/actor-avatar-info.component.scss b/client/src/app/shared/shared-main/account/actor-avatar-info.component.scss
deleted file mode 100644
index 57c298508..000000000
--- a/client/src/app/shared/shared-main/account/actor-avatar-info.component.scss
+++ /dev/null
@@ -1,86 +0,0 @@
1@import '_variables';
2@import '_mixins';
3
4.actor {
5 display: flex;
6
7 img {
8 @include avatar(100px);
9
10 margin-right: 15px;
11 }
12
13 .actor-img-edit-container {
14 position: relative;
15 width: 0;
16
17 .actor-img-edit-button {
18 @include peertube-button-file(21px);
19 @include button-with-icon(19px);
20 @include orange-button;
21
22 margin-top: 10px;
23 margin-bottom: 5px;
24 border-radius: 50%;
25 top: 55px;
26 right: 45px;
27 cursor: pointer;
28
29 input {
30 width: 30px;
31 height: 30px;
32 }
33
34 my-global-icon {
35 right: 7px;
36 }
37 }
38 }
39
40 .actor-info {
41 justify-content: center;
42 display: inline-flex;
43 flex-direction: column;
44
45 .actor-info-names {
46 display: flex;
47 align-items: center;
48
49 .actor-info-display-name {
50 font-size: 20px;
51 font-weight: $font-bold;
52
53 @media screen and (max-width: $small-view) {
54 font-size: 16px;
55 }
56 }
57
58 .actor-info-username {
59 margin-left: 7px;
60 position: relative;
61 top: 2px;
62 font-size: 14px;
63 color: $grey-actor-name;
64 }
65 }
66
67 .actor-info-followers {
68 font-size: 15px;
69 padding-bottom: .5rem;
70 }
71 }
72}
73
74.actor-img-edit-container ::ng-deep .popover-avatar-info .popover-body {
75 padding: 0;
76
77 .dropdown-item {
78 padding: 6px 10px;
79 border-radius: 4px;
80
81 &:first-child {
82 @include peertube-file;
83 display: block;
84 }
85 }
86}
diff --git a/client/src/app/shared/shared-main/account/actor.model.ts b/client/src/app/shared/shared-main/account/actor.model.ts
index 8222c9769..1ee0c297e 100644
--- a/client/src/app/shared/shared-main/account/actor.model.ts
+++ b/client/src/app/shared/shared-main/account/actor.model.ts
@@ -1,17 +1,20 @@
1import { Actor as ActorServer, Avatar } from '@shared/models' 1import { Actor as ActorServer, ActorImage } from '@shared/models'
2import { getAbsoluteAPIUrl } from '@app/helpers' 2import { getAbsoluteAPIUrl } from '@app/helpers'
3 3
4export abstract class Actor implements ActorServer { 4export abstract class Actor implements ActorServer {
5 id: number 5 id: number
6 url: string
7 name: string 6 name: string
7
8 host: string 8 host: string
9 url: string
10
9 followingCount: number 11 followingCount: number
10 followersCount: number 12 followersCount: number
13
11 createdAt: Date | string 14 createdAt: Date | string
12 updatedAt: Date | string 15 updatedAt: Date | string
13 avatar: Avatar
14 16
17 avatar: ActorImage
15 avatarUrl: string 18 avatarUrl: string
16 19
17 isLocal: boolean 20 isLocal: boolean
@@ -24,6 +27,8 @@ export abstract class Actor implements ActorServer {
24 27
25 return absoluteAPIUrl + actor.avatar.path 28 return absoluteAPIUrl + actor.avatar.path
26 } 29 }
30
31 return ''
27 } 32 }
28 33
29 static CREATE_BY_STRING (accountName: string, host: string, forceHostname = false) { 34 static CREATE_BY_STRING (accountName: string, host: string, forceHostname = false) {
@@ -42,11 +47,11 @@ export abstract class Actor implements ActorServer {
42 return host.trim() === thisHost 47 return host.trim() === thisHost
43 } 48 }
44 49
45 protected constructor (hash: ActorServer) { 50 protected constructor (hash: Partial<ActorServer>) {
46 this.id = hash.id 51 this.id = hash.id
47 this.url = hash.url 52 this.url = hash.url ?? ''
48 this.name = hash.name 53 this.name = hash.name ?? ''
49 this.host = hash.host 54 this.host = hash.host ?? ''
50 this.followingCount = hash.followingCount 55 this.followingCount = hash.followingCount
51 this.followersCount = hash.followersCount 56 this.followersCount = hash.followersCount
52 57
diff --git a/client/src/app/shared/shared-main/account/index.ts b/client/src/app/shared/shared-main/account/index.ts
index 61c800e56..b80ddb9f5 100644
--- a/client/src/app/shared/shared-main/account/index.ts
+++ b/client/src/app/shared/shared-main/account/index.ts
@@ -1,5 +1,3 @@
1export * from './account.model' 1export * from './account.model'
2export * from './account.service' 2export * from './account.service'
3export * from './actor-avatar-info.component'
4export * from './actor.model' 3export * from './actor.model'
5export * from './video-avatar-channel.component'
diff --git a/client/src/app/shared/shared-main/angular/autofocus.directive.ts b/client/src/app/shared/shared-main/angular/autofocus.directive.ts
new file mode 100644
index 000000000..5f087d79d
--- /dev/null
+++ b/client/src/app/shared/shared-main/angular/autofocus.directive.ts
@@ -0,0 +1,12 @@
1import { AfterViewInit, Directive, ElementRef } from '@angular/core'
2
3@Directive({
4 selector: '[autofocus]'
5})
6export class AutofocusDirective implements AfterViewInit {
7 constructor (private host: ElementRef) { }
8
9 ngAfterViewInit () {
10 this.host.nativeElement.focus()
11 }
12}
diff --git a/client/src/app/shared/shared-main/angular/index.ts b/client/src/app/shared/shared-main/angular/index.ts
index 29f8b3650..8ea47bb33 100644
--- a/client/src/app/shared/shared-main/angular/index.ts
+++ b/client/src/app/shared/shared-main/angular/index.ts
@@ -1,3 +1,4 @@
1export * from './autofocus.directive'
1export * from './bytes.pipe' 2export * from './bytes.pipe'
2export * from './duration-formatter.pipe' 3export * from './duration-formatter.pipe'
3export * from './from-now.pipe' 4export * from './from-now.pipe'
diff --git a/client/src/app/shared/shared-main/auth/auth-interceptor.service.ts b/client/src/app/shared/shared-main/auth/auth-interceptor.service.ts
index 3ddaffbdf..4fe3b964d 100644
--- a/client/src/app/shared/shared-main/auth/auth-interceptor.service.ts
+++ b/client/src/app/shared/shared-main/auth/auth-interceptor.service.ts
@@ -27,7 +27,9 @@ export class AuthInterceptor implements HttpInterceptor {
27 catchError((err: HttpErrorResponse) => { 27 catchError((err: HttpErrorResponse) => {
28 if (err.status === HttpStatusCode.UNAUTHORIZED_401 && err.error && err.error.code === 'invalid_token') { 28 if (err.status === HttpStatusCode.UNAUTHORIZED_401 && err.error && err.error.code === 'invalid_token') {
29 return this.handleTokenExpired(req, next) 29 return this.handleTokenExpired(req, next)
30 } else if (err.status === HttpStatusCode.UNAUTHORIZED_401) { 30 }
31
32 if (err.status === HttpStatusCode.UNAUTHORIZED_401) {
31 return this.handleNotAuthenticated(err) 33 return this.handleNotAuthenticated(err)
32 } 34 }
33 35
diff --git a/client/src/app/shared/shared-main/misc/simple-search-input.component.html b/client/src/app/shared/shared-main/misc/simple-search-input.component.html
index fb0d97122..c20c02e23 100644
--- a/client/src/app/shared/shared-main/misc/simple-search-input.component.html
+++ b/client/src/app/shared/shared-main/misc/simple-search-input.component.html
@@ -1,14 +1,15 @@
1<span> 1<div class="root">
2 <my-global-icon iconName="search" aria-label="Search" role="button" (click)="showInput()"></my-global-icon>
3
4 <input 2 <input
5 #ref 3 #ref
6 type="text" 4 type="text"
7 [(ngModel)]="value" 5 [(ngModel)]="value"
8 (focusout)="focusLost()"
9 (keyup.enter)="searchChange()" 6 (keyup.enter)="searchChange()"
10 [hidden]="!shown" 7 [hidden]="!inputShown"
11 [name]="name" 8 [name]="name"
12 [placeholder]="placeholder" 9 [placeholder]="placeholder"
13 > 10 >
14</span> 11
12 <my-global-icon iconName="search" aria-label="Search" role="button" (click)="onIconClick()" [title]="iconTitle"></my-global-icon>
13
14 <my-global-icon *ngIf="!alwaysShow && inputShown" i18n-title title="Close search" iconName="cross" (click)="hideInput()"></my-global-icon>
15</div>
diff --git a/client/src/app/shared/shared-main/misc/simple-search-input.component.scss b/client/src/app/shared/shared-main/misc/simple-search-input.component.scss
index 591b04fb2..5ae48f81b 100644
--- a/client/src/app/shared/shared-main/misc/simple-search-input.component.scss
+++ b/client/src/app/shared/shared-main/misc/simple-search-input.component.scss
@@ -1,29 +1,29 @@
1@import '_variables'; 1@import '_variables';
2@import '_mixins'; 2@import '_mixins';
3 3
4span { 4.root {
5 opacity: .6; 5 display: flex;
6
7 &:focus-within {
8 opacity: 1;
9 }
10} 6}
11 7
12my-global-icon { 8my-global-icon {
13 height: 18px; 9 height: 28px;
14 position: relative; 10 width: 28px;
15 top: -2px; 11 margin-left: 10px;
16} 12 cursor: pointer;
17 13
18input { 14 &:hover {
19 @include peertube-input-text(150px); 15 color: pvar(--mainHoverColor);
16 }
20 17
21 height: 22px; // maximum height for the account/video-channels links 18 &[iconName=search] {
22 padding-left: 10px; 19 color: pvar(--mainForegroundColor);
23 background-color: transparent; 20 }
24 border: none;
25 21
26 &::placeholder { 22 &[iconName=cross] {
27 font-size: 15px; 23 color: pvar(--mainForegroundColor);
28 } 24 }
29} 25}
26
27input {
28 @include peertube-input-text(200px);
29}
diff --git a/client/src/app/shared/shared-main/misc/simple-search-input.component.ts b/client/src/app/shared/shared-main/misc/simple-search-input.component.ts
index 86ae9ab42..224d71134 100644
--- a/client/src/app/shared/shared-main/misc/simple-search-input.component.ts
+++ b/client/src/app/shared/shared-main/misc/simple-search-input.component.ts
@@ -1,7 +1,7 @@
1import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router'
3import { Subject } from 'rxjs' 1import { Subject } from 'rxjs'
4import { debounceTime, distinctUntilChanged } from 'rxjs/operators' 2import { debounceTime, distinctUntilChanged } from 'rxjs/operators'
3import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
4import { ActivatedRoute, Router } from '@angular/router'
5 5
6@Component({ 6@Component({
7 selector: 'simple-search-input', 7 selector: 'simple-search-input',
@@ -13,11 +13,14 @@ export class SimpleSearchInputComponent implements OnInit {
13 13
14 @Input() name = 'search' 14 @Input() name = 'search'
15 @Input() placeholder = $localize`Search` 15 @Input() placeholder = $localize`Search`
16 @Input() iconTitle = $localize`Search`
17 @Input() alwaysShow = true
16 18
17 @Output() searchChanged = new EventEmitter<string>() 19 @Output() searchChanged = new EventEmitter<string>()
20 @Output() inputDisplayChanged = new EventEmitter<boolean>()
18 21
19 value = '' 22 value = ''
20 shown: boolean 23 inputShown: boolean
21 24
22 private searchSubject = new Subject<string>() 25 private searchSubject = new Subject<string>()
23 26
@@ -35,20 +38,51 @@ export class SimpleSearchInputComponent implements OnInit {
35 .subscribe(value => this.searchChanged.emit(value)) 38 .subscribe(value => this.searchChanged.emit(value))
36 39
37 this.searchSubject.next(this.value) 40 this.searchSubject.next(this.value)
41
42 if (this.isInputShown()) this.showInput(false)
38 } 43 }
39 44
40 showInput () { 45 isInputShown () {
41 this.shown = true 46 if (this.alwaysShow) return true
42 setTimeout(() => this.input.nativeElement.focus()) 47
48 return this.inputShown
49 }
50
51 onIconClick () {
52 if (!this.isInputShown()) {
53 this.showInput()
54 return
55 }
56
57 this.searchChange()
58 }
59
60 showInput (focus = true) {
61 this.inputShown = true
62 this.inputDisplayChanged.emit(this.inputShown)
63
64 if (focus) {
65 setTimeout(() => this.input.nativeElement.focus())
66 }
67 }
68
69 hideInput () {
70 this.inputShown = false
71
72 if (this.isInputShown() === false) {
73 this.inputDisplayChanged.emit(this.inputShown)
74 }
43 } 75 }
44 76
45 focusLost () { 77 focusLost () {
46 if (this.value !== '') return 78 if (this.value) return
47 this.shown = false 79
80 this.hideInput()
48 } 81 }
49 82
50 searchChange () { 83 searchChange () {
51 this.router.navigate(['./search'], { relativeTo: this.route }) 84 this.router.navigate([ './search' ], { relativeTo: this.route })
85
52 this.searchSubject.next(this.value) 86 this.searchSubject.next(this.value)
53 } 87 }
54} 88}
diff --git a/client/src/app/shared/shared-main/peertube-modal/index.ts b/client/src/app/shared/shared-main/peertube-modal/index.ts
new file mode 100644
index 000000000..d631522e4
--- /dev/null
+++ b/client/src/app/shared/shared-main/peertube-modal/index.ts
@@ -0,0 +1 @@
export * from './peertube-modal.service'
diff --git a/client/src/app/shared/shared-main/peertube-modal/peertube-modal.service.ts b/client/src/app/shared/shared-main/peertube-modal/peertube-modal.service.ts
new file mode 100644
index 000000000..79da08a5c
--- /dev/null
+++ b/client/src/app/shared/shared-main/peertube-modal/peertube-modal.service.ts
@@ -0,0 +1,7 @@
1import { Injectable } from '@angular/core'
2import { Subject } from 'rxjs'
3
4@Injectable({ providedIn: 'root' })
5export class PeertubeModalService {
6 openQuickSettingsSubject = new Subject<void>()
7}
diff --git a/client/src/app/shared/shared-main/shared-main.module.ts b/client/src/app/shared/shared-main/shared-main.module.ts
index 9d550996d..16d230f46 100644
--- a/client/src/app/shared/shared-main/shared-main.module.ts
+++ b/client/src/app/shared/shared-main/shared-main.module.ts
@@ -6,19 +6,20 @@ import { NgModule } from '@angular/core'
6import { FormsModule, ReactiveFormsModule } from '@angular/forms' 6import { FormsModule, ReactiveFormsModule } from '@angular/forms'
7import { RouterModule } from '@angular/router' 7import { RouterModule } from '@angular/router'
8import { 8import {
9 NgbButtonsModule,
9 NgbCollapseModule, 10 NgbCollapseModule,
10 NgbDropdownModule, 11 NgbDropdownModule,
11 NgbModalModule, 12 NgbModalModule,
12 NgbNavModule, 13 NgbNavModule,
13 NgbPopoverModule, 14 NgbPopoverModule,
14 NgbTooltipModule, 15 NgbTooltipModule
15 NgbButtonsModule
16} from '@ng-bootstrap/ng-bootstrap' 16} from '@ng-bootstrap/ng-bootstrap'
17import { LoadingBarModule } from '@ngx-loading-bar/core' 17import { LoadingBarModule } from '@ngx-loading-bar/core'
18import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client' 18import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client'
19import { SharedGlobalIconModule } from '../shared-icons' 19import { SharedGlobalIconModule } from '../shared-icons'
20import { AccountService, ActorAvatarInfoComponent, VideoAvatarChannelComponent } from './account' 20import { AccountService } from './account'
21import { 21import {
22 AutofocusDirective,
22 BytesPipe, 23 BytesPipe,
23 DurationFormatterPipe, 24 DurationFormatterPipe,
24 FromNowPipe, 25 FromNowPipe,
@@ -31,7 +32,7 @@ import { ActionDropdownComponent, ButtonComponent, DeleteButtonComponent, EditBu
31import { DateToggleComponent } from './date' 32import { DateToggleComponent } from './date'
32import { FeedComponent } from './feeds' 33import { FeedComponent } from './feeds'
33import { LoaderComponent, SmallLoaderComponent } from './loaders' 34import { LoaderComponent, SmallLoaderComponent } from './loaders'
34import { HelpComponent, ListOverflowComponent, TopMenuDropdownComponent, SimpleSearchInputComponent } from './misc' 35import { HelpComponent, ListOverflowComponent, SimpleSearchInputComponent, TopMenuDropdownComponent } from './misc'
35import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users' 36import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users'
36import { RedundancyService, VideoImportService, VideoOwnershipService, VideoService } from './video' 37import { RedundancyService, VideoImportService, VideoOwnershipService, VideoService } from './video'
37import { VideoCaptionService } from './video-caption' 38import { VideoCaptionService } from './video-caption'
@@ -64,13 +65,11 @@ import { VideoChannelService } from './video-channel'
64 ], 65 ],
65 66
66 declarations: [ 67 declarations: [
67 VideoAvatarChannelComponent,
68 ActorAvatarInfoComponent,
69
70 FromNowPipe, 68 FromNowPipe,
71 NumberFormatterPipe, 69 NumberFormatterPipe,
72 BytesPipe, 70 BytesPipe,
73 DurationFormatterPipe, 71 DurationFormatterPipe,
72 AutofocusDirective,
74 73
75 InfiniteScrollerDirective, 74 InfiniteScrollerDirective,
76 PeerTubeTemplateDirective, 75 PeerTubeTemplateDirective,
@@ -118,13 +117,11 @@ import { VideoChannelService } from './video-channel'
118 117
119 PrimeSharedModule, 118 PrimeSharedModule,
120 119
121 VideoAvatarChannelComponent,
122 ActorAvatarInfoComponent,
123
124 FromNowPipe, 120 FromNowPipe,
125 BytesPipe, 121 BytesPipe,
126 NumberFormatterPipe, 122 NumberFormatterPipe,
127 DurationFormatterPipe, 123 DurationFormatterPipe,
124 AutofocusDirective,
128 125
129 InfiniteScrollerDirective, 126 InfiniteScrollerDirective,
130 PeerTubeTemplateDirective, 127 PeerTubeTemplateDirective,
diff --git a/client/src/app/shared/shared-main/users/user-notification.model.ts b/client/src/app/shared/shared-main/users/user-notification.model.ts
index 1211995fd..88a4811da 100644
--- a/client/src/app/shared/shared-main/users/user-notification.model.ts
+++ b/client/src/app/shared/shared-main/users/user-notification.model.ts
@@ -6,6 +6,7 @@ import {
6 AbuseState, 6 AbuseState,
7 ActorInfo, 7 ActorInfo,
8 FollowState, 8 FollowState,
9 PluginType,
9 UserNotification as UserNotificationServer, 10 UserNotification as UserNotificationServer,
10 UserNotificationType, 11 UserNotificationType,
11 UserRight, 12 UserRight,
@@ -74,20 +75,40 @@ export class UserNotification implements UserNotificationServer {
74 } 75 }
75 } 76 }
76 77
78 plugin?: {
79 name: string
80 type: PluginType
81 latestVersion: string
82 }
83
84 peertube?: {
85 latestVersion: string
86 }
87
77 createdAt: string 88 createdAt: string
78 updatedAt: string 89 updatedAt: string
79 90
80 // Additional fields 91 // Additional fields
81 videoUrl?: string 92 videoUrl?: string
82 commentUrl?: any[] 93 commentUrl?: any[]
94
83 abuseUrl?: string 95 abuseUrl?: string
84 abuseQueryParams?: { [id: string]: string } = {} 96 abuseQueryParams?: { [id: string]: string } = {}
97
85 videoAutoBlacklistUrl?: string 98 videoAutoBlacklistUrl?: string
99
86 accountUrl?: string 100 accountUrl?: string
101
87 videoImportIdentifier?: string 102 videoImportIdentifier?: string
88 videoImportUrl?: string 103 videoImportUrl?: string
104
89 instanceFollowUrl?: string 105 instanceFollowUrl?: string
90 106
107 peertubeVersionLink?: string
108
109 pluginUrl?: string
110 pluginQueryParams?: { [id: string]: string } = {}
111
91 constructor (hash: UserNotificationServer, user: AuthUser) { 112 constructor (hash: UserNotificationServer, user: AuthUser) {
92 this.id = hash.id 113 this.id = hash.id
93 this.type = hash.type 114 this.type = hash.type
@@ -114,6 +135,9 @@ export class UserNotification implements UserNotificationServer {
114 this.actorFollow = hash.actorFollow 135 this.actorFollow = hash.actorFollow
115 if (this.actorFollow) this.setAccountAvatarUrl(this.actorFollow.follower) 136 if (this.actorFollow) this.setAccountAvatarUrl(this.actorFollow.follower)
116 137
138 this.plugin = hash.plugin
139 this.peertube = hash.peertube
140
117 this.createdAt = hash.createdAt 141 this.createdAt = hash.createdAt
118 this.updatedAt = hash.updatedAt 142 this.updatedAt = hash.updatedAt
119 143
@@ -197,6 +221,15 @@ export class UserNotification implements UserNotificationServer {
197 case UserNotificationType.AUTO_INSTANCE_FOLLOWING: 221 case UserNotificationType.AUTO_INSTANCE_FOLLOWING:
198 this.instanceFollowUrl = '/admin/follows/following-list' 222 this.instanceFollowUrl = '/admin/follows/following-list'
199 break 223 break
224
225 case UserNotificationType.NEW_PEERTUBE_VERSION:
226 this.peertubeVersionLink = 'https://joinpeertube.org/news'
227 break
228
229 case UserNotificationType.NEW_PLUGIN_VERSION:
230 this.pluginUrl = `/admin/plugins/list-installed`
231 this.pluginQueryParams.pluginType = this.plugin.type + ''
232 break
200 } 233 }
201 } catch (err) { 234 } catch (err) {
202 this.type = null 235 this.type = null
diff --git a/client/src/app/shared/shared-main/users/user-notifications.component.html b/client/src/app/shared/shared-main/users/user-notifications.component.html
index 265af8d55..325f0eaae 100644
--- a/client/src/app/shared/shared-main/users/user-notifications.component.html
+++ b/client/src/app/shared/shared-main/users/user-notifications.component.html
@@ -4,7 +4,7 @@
4 <div *ngFor="let notification of notifications" class="notification" [ngClass]="{ unread: !notification.read }" (click)="markAsRead(notification)"> 4 <div *ngFor="let notification of notifications" class="notification" [ngClass]="{ unread: !notification.read }" (click)="markAsRead(notification)">
5 5
6 <ng-container [ngSwitch]="notification.type"> 6 <ng-container [ngSwitch]="notification.type">
7 <ng-container *ngSwitchCase="UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION"> 7 <ng-container *ngSwitchCase="1"> <!-- UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION -->
8 <ng-container *ngIf="notification.video; then hasVideo; else noVideo"></ng-container> 8 <ng-container *ngIf="notification.video; then hasVideo; else noVideo"></ng-container>
9 9
10 <ng-template #hasVideo> 10 <ng-template #hasVideo>
@@ -26,7 +26,7 @@
26 </ng-template> 26 </ng-template>
27 </ng-container> 27 </ng-container>
28 28
29 <ng-container *ngSwitchCase="UserNotificationType.UNBLACKLIST_ON_MY_VIDEO"> 29 <ng-container *ngSwitchCase="5"> <!-- UserNotificationType.UNBLACKLIST_ON_MY_VIDEO -->
30 <my-global-icon iconName="undo" aria-hidden="true"></my-global-icon> 30 <my-global-icon iconName="undo" aria-hidden="true"></my-global-icon>
31 31
32 <div class="message" i18n> 32 <div class="message" i18n>
@@ -34,7 +34,7 @@
34 </div> 34 </div>
35 </ng-container> 35 </ng-container>
36 36
37 <ng-container *ngSwitchCase="UserNotificationType.BLACKLIST_ON_MY_VIDEO"> 37 <ng-container *ngSwitchCase="4"> <!-- UserNotificationType.BLACKLIST_ON_MY_VIDEO -->
38 <my-global-icon iconName="no" aria-hidden="true"></my-global-icon> 38 <my-global-icon iconName="no" aria-hidden="true"></my-global-icon>
39 39
40 <div class="message" i18n> 40 <div class="message" i18n>
@@ -42,7 +42,7 @@
42 </div> 42 </div>
43 </ng-container> 43 </ng-container>
44 44
45 <ng-container *ngSwitchCase="UserNotificationType.NEW_ABUSE_FOR_MODERATORS"> 45 <ng-container *ngSwitchCase="3"> <!-- UserNotificationType.NEW_ABUSE_FOR_MODERATORS -->
46 <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon> 46 <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon>
47 47
48 <div class="message" *ngIf="notification.videoUrl" i18n> 48 <div class="message" *ngIf="notification.videoUrl" i18n>
@@ -63,7 +63,7 @@
63 </div> 63 </div>
64 </ng-container> 64 </ng-container>
65 65
66 <ng-container *ngSwitchCase="UserNotificationType.ABUSE_STATE_CHANGE"> 66 <ng-container *ngSwitchCase="15"> <!-- UserNotificationType.ABUSE_STATE_CHANGE -->
67 <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon> 67 <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon>
68 68
69 <div class="message" i18n> 69 <div class="message" i18n>
@@ -73,7 +73,7 @@
73 </div> 73 </div>
74 </ng-container> 74 </ng-container>
75 75
76 <ng-container *ngSwitchCase="UserNotificationType.ABUSE_NEW_MESSAGE"> 76 <ng-container *ngSwitchCase="16"> <!-- UserNotificationType.ABUSE_NEW_MESSAGE -->
77 <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon> 77 <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon>
78 78
79 <div class="message" i18n> 79 <div class="message" i18n>
@@ -81,7 +81,7 @@
81 </div> 81 </div>
82 </ng-container> 82 </ng-container>
83 83
84 <ng-container *ngSwitchCase="UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS"> 84 <ng-container *ngSwitchCase="12"> <!-- UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS -->
85 <my-global-icon iconName="no" aria-hidden="true"></my-global-icon> 85 <my-global-icon iconName="no" aria-hidden="true"></my-global-icon>
86 86
87 <div class="message" i18n> 87 <div class="message" i18n>
@@ -89,7 +89,7 @@
89 </div> 89 </div>
90 </ng-container> 90 </ng-container>
91 91
92 <ng-container *ngSwitchCase="UserNotificationType.NEW_COMMENT_ON_MY_VIDEO"> 92 <ng-container *ngSwitchCase="2">
93 <ng-container *ngIf="notification.comment"> 93 <ng-container *ngIf="notification.comment">
94 <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl"> 94 <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">
95 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" /> 95 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" />
@@ -109,7 +109,7 @@
109 </ng-container> 109 </ng-container>
110 </ng-container> 110 </ng-container>
111 111
112 <ng-container *ngSwitchCase="UserNotificationType.MY_VIDEO_PUBLISHED"> 112 <ng-container *ngSwitchCase="6"> <!-- UserNotificationType.MY_VIDEO_PUBLISHED -->
113 <my-global-icon iconName="film" aria-hidden="true"></my-global-icon> 113 <my-global-icon iconName="film" aria-hidden="true"></my-global-icon>
114 114
115 <div class="message" i18n> 115 <div class="message" i18n>
@@ -117,7 +117,7 @@
117 </div> 117 </div>
118 </ng-container> 118 </ng-container>
119 119
120 <ng-container *ngSwitchCase="UserNotificationType.MY_VIDEO_IMPORT_SUCCESS"> 120 <ng-container *ngSwitchCase="7"> <!-- UserNotificationType.MY_VIDEO_IMPORT_SUCCESS -->
121 <my-global-icon iconName="cloud-download" aria-hidden="true"></my-global-icon> 121 <my-global-icon iconName="cloud-download" aria-hidden="true"></my-global-icon>
122 122
123 <div class="message" i18n> 123 <div class="message" i18n>
@@ -125,7 +125,7 @@
125 </div> 125 </div>
126 </ng-container> 126 </ng-container>
127 127
128 <ng-container *ngSwitchCase="UserNotificationType.MY_VIDEO_IMPORT_ERROR"> 128 <ng-container *ngSwitchCase="8"> <!-- UserNotificationType.MY_VIDEO_IMPORT_ERROR -->
129 <my-global-icon iconName="cloud-error" aria-hidden="true"></my-global-icon> 129 <my-global-icon iconName="cloud-error" aria-hidden="true"></my-global-icon>
130 130
131 <div class="message" i18n> 131 <div class="message" i18n>
@@ -133,7 +133,7 @@
133 </div> 133 </div>
134 </ng-container> 134 </ng-container>
135 135
136 <ng-container *ngSwitchCase="UserNotificationType.NEW_USER_REGISTRATION"> 136 <ng-container *ngSwitchCase="9"> <!-- UserNotificationType.NEW_USER_REGISTRATION -->
137 <my-global-icon iconName="user-add" aria-hidden="true"></my-global-icon> 137 <my-global-icon iconName="user-add" aria-hidden="true"></my-global-icon>
138 138
139 <div class="message" i18n> 139 <div class="message" i18n>
@@ -141,7 +141,7 @@
141 </div> 141 </div>
142 </ng-container> 142 </ng-container>
143 143
144 <ng-container *ngSwitchCase="UserNotificationType.NEW_FOLLOW"> 144 <ng-container *ngSwitchCase="10"> <!-- UserNotificationType.NEW_FOLLOW -->
145 <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl"> 145 <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">
146 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.actorFollow.follower.avatarUrl" /> 146 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.actorFollow.follower.avatarUrl" />
147 </a> 147 </a>
@@ -154,7 +154,7 @@
154 </div> 154 </div>
155 </ng-container> 155 </ng-container>
156 156
157 <ng-container *ngSwitchCase="UserNotificationType.COMMENT_MENTION"> 157 <ng-container *ngSwitchCase="11">
158 <ng-container *ngIf="notification.comment"> 158 <ng-container *ngIf="notification.comment">
159 <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl"> 159 <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">
160 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" /> 160 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" />
@@ -174,7 +174,7 @@
174 </ng-container> 174 </ng-container>
175 </ng-container> 175 </ng-container>
176 176
177 <ng-container *ngSwitchCase="UserNotificationType.NEW_INSTANCE_FOLLOWER"> 177 <ng-container *ngSwitchCase="13"> <!-- UserNotificationType.NEW_INSTANCE_FOLLOWER -->
178 <my-global-icon iconName="users" aria-hidden="true"></my-global-icon> 178 <my-global-icon iconName="users" aria-hidden="true"></my-global-icon>
179 179
180 <div class="message" i18n> 180 <div class="message" i18n>
@@ -183,7 +183,7 @@
183 </div> 183 </div>
184 </ng-container> 184 </ng-container>
185 185
186 <ng-container *ngSwitchCase="UserNotificationType.AUTO_INSTANCE_FOLLOWING"> 186 <ng-container *ngSwitchCase="14"> <!-- UserNotificationType.AUTO_INSTANCE_FOLLOWING -->
187 <my-global-icon iconName="users" aria-hidden="true"></my-global-icon> 187 <my-global-icon iconName="users" aria-hidden="true"></my-global-icon>
188 188
189 <div class="message" i18n> 189 <div class="message" i18n>
@@ -191,6 +191,22 @@
191 </div> 191 </div>
192 </ng-container> 192 </ng-container>
193 193
194 <ng-container *ngSwitchCase="17"> <!-- UserNotificationType.NEW_PLUGIN_VERSION -->
195 <my-global-icon iconName="cog" aria-hidden="true"></my-global-icon>
196
197 <div class="message" i18n>
198 <a (click)="markAsRead(notification)" [routerLink]="notification.pluginUrl" [queryParams]="notification.pluginQueryParams">A new version of the plugin/theme {{ notification.plugin.name }}</a> is available: {{ notification.plugin.latestVersion }}
199 </div>
200 </ng-container>
201
202 <ng-container *ngSwitchCase="18"> <!-- UserNotificationType.NEW_PEERTUBE_VERSION -->
203 <my-global-icon iconName="cog" aria-hidden="true"></my-global-icon>
204
205 <div class="message" i18n>
206 <a (click)="markAsRead(notification)" [href]="notification.peertubeVersionLink" target="_blank" rel="noopener noreferer">A new version of PeerTube</a> is available: {{ notification.peertube.latestVersion }}
207 </div>
208 </ng-container>
209
194 <ng-container *ngSwitchDefault> 210 <ng-container *ngSwitchDefault>
195 <my-global-icon iconName="alert" aria-hidden="true"></my-global-icon> 211 <my-global-icon iconName="alert" aria-hidden="true"></my-global-icon>
196 212
diff --git a/client/src/app/shared/shared-main/users/user-notifications.component.ts b/client/src/app/shared/shared-main/users/user-notifications.component.ts
index 387c49d94..d7c722355 100644
--- a/client/src/app/shared/shared-main/users/user-notifications.component.ts
+++ b/client/src/app/shared/shared-main/users/user-notifications.component.ts
@@ -21,9 +21,6 @@ export class UserNotificationsComponent implements OnInit {
21 notifications: UserNotification[] = [] 21 notifications: UserNotification[] = []
22 sortField = 'createdAt' 22 sortField = 'createdAt'
23 23
24 // So we can access it in the template
25 UserNotificationType = UserNotificationType
26
27 componentPagination: ComponentPagination 24 componentPagination: ComponentPagination
28 25
29 onDataSubject = new Subject<any[]>() 26 onDataSubject = new Subject<any[]>()
@@ -48,7 +45,7 @@ export class UserNotificationsComponent implements OnInit {
48 } 45 }
49 46
50 loadNotifications (reset?: boolean) { 47 loadNotifications (reset?: boolean) {
51 this.userNotificationService.listMyNotifications({ 48 const options = {
52 pagination: this.componentPagination, 49 pagination: this.componentPagination,
53 ignoreLoadingBar: this.ignoreLoadingBar, 50 ignoreLoadingBar: this.ignoreLoadingBar,
54 sort: { 51 sort: {
@@ -56,7 +53,9 @@ export class UserNotificationsComponent implements OnInit {
56 // if we order by creation date, we want DESC. all other fields are ASC (like unread). 53 // if we order by creation date, we want DESC. all other fields are ASC (like unread).
57 order: this.sortField === 'createdAt' ? -1 : 1 54 order: this.sortField === 'createdAt' ? -1 : 1
58 } 55 }
59 }) 56 }
57
58 this.userNotificationService.listMyNotifications(options)
60 .subscribe( 59 .subscribe(
61 result => { 60 result => {
62 this.notifications = reset ? result.data : this.notifications.concat(result.data) 61 this.notifications = reset ? result.data : this.notifications.concat(result.data)
diff --git a/client/src/app/shared/shared-main/video-channel/video-channel.model.ts b/client/src/app/shared/shared-main/video-channel/video-channel.model.ts
index c6a63fe6c..1ba3fcc0e 100644
--- a/client/src/app/shared/shared-main/video-channel/video-channel.model.ts
+++ b/client/src/app/shared/shared-main/video-channel/video-channel.model.ts
@@ -1,15 +1,22 @@
1import { VideoChannel as ServerVideoChannel, ViewsPerDate, Account, Avatar } from '@shared/models' 1import { getAbsoluteAPIUrl } from '@app/helpers'
2import { Account as ServerAccount, ActorImage, VideoChannel as ServerVideoChannel, ViewsPerDate } from '@shared/models'
3import { Account } from '../account/account.model'
2import { Actor } from '../account/actor.model' 4import { Actor } from '../account/actor.model'
3 5
4export class VideoChannel extends Actor implements ServerVideoChannel { 6export class VideoChannel extends Actor implements ServerVideoChannel {
5 displayName: string 7 displayName: string
6 description: string 8 description: string
7 support: string 9 support: string
10
8 isLocal: boolean 11 isLocal: boolean
12
9 nameWithHost: string 13 nameWithHost: string
10 nameWithHostForced: string 14 nameWithHostForced: string
11 15
12 ownerAccount?: Account 16 banner: ActorImage
17 bannerUrl: string
18
19 ownerAccount?: ServerAccount
13 ownerBy?: string 20 ownerBy?: string
14 ownerAvatarUrl?: string 21 ownerAvatarUrl?: string
15 22
@@ -21,19 +28,33 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
21 return Actor.GET_ACTOR_AVATAR_URL(actor) || this.GET_DEFAULT_AVATAR_URL() 28 return Actor.GET_ACTOR_AVATAR_URL(actor) || this.GET_DEFAULT_AVATAR_URL()
22 } 29 }
23 30
31 static GET_ACTOR_BANNER_URL (channel: ServerVideoChannel) {
32 if (channel?.banner?.url) return channel.banner.url
33
34 if (channel && channel.banner) {
35 const absoluteAPIUrl = getAbsoluteAPIUrl()
36
37 return absoluteAPIUrl + channel.banner.path
38 }
39
40 return ''
41 }
42
24 static GET_DEFAULT_AVATAR_URL () { 43 static GET_DEFAULT_AVATAR_URL () {
25 return `${window.location.origin}/client/assets/images/default-avatar-videochannel.png` 44 return `${window.location.origin}/client/assets/images/default-avatar-videochannel.png`
26 } 45 }
27 46
28 constructor (hash: ServerVideoChannel) { 47 constructor (hash: Partial<ServerVideoChannel>) {
29 super(hash) 48 super(hash)
30 49
31 this.updateComputedAttributes()
32
33 this.displayName = hash.displayName 50 this.displayName = hash.displayName
34 this.description = hash.description 51 this.description = hash.description
35 this.support = hash.support 52 this.support = hash.support
53
54 this.banner = hash.banner
55
36 this.isLocal = hash.isLocal 56 this.isLocal = hash.isLocal
57
37 this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host) 58 this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host)
38 this.nameWithHostForced = Actor.CREATE_BY_STRING(this.name, this.host, true) 59 this.nameWithHostForced = Actor.CREATE_BY_STRING(this.name, this.host, true)
39 60
@@ -46,22 +67,34 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
46 if (hash.ownerAccount) { 67 if (hash.ownerAccount) {
47 this.ownerAccount = hash.ownerAccount 68 this.ownerAccount = hash.ownerAccount
48 this.ownerBy = Actor.CREATE_BY_STRING(hash.ownerAccount.name, hash.ownerAccount.host) 69 this.ownerBy = Actor.CREATE_BY_STRING(hash.ownerAccount.name, hash.ownerAccount.host)
49 this.ownerAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.ownerAccount) 70 this.ownerAvatarUrl = Account.GET_ACTOR_AVATAR_URL(this.ownerAccount)
50 } 71 }
72
73 this.updateComputedAttributes()
51 } 74 }
52 75
53 updateAvatar (newAvatar: Avatar) { 76 updateAvatar (newAvatar: ActorImage) {
54 this.avatar = newAvatar 77 this.avatar = newAvatar
55 78
56 this.updateComputedAttributes() 79 this.updateComputedAttributes()
57 } 80 }
58 81
59 resetAvatar () { 82 resetAvatar () {
60 this.avatar = null 83 this.updateAvatar(null)
61 this.avatarUrl = VideoChannel.GET_DEFAULT_AVATAR_URL() 84 }
85
86 updateBanner (newBanner: ActorImage) {
87 this.banner = newBanner
88
89 this.updateComputedAttributes()
90 }
91
92 resetBanner () {
93 this.updateBanner(null)
62 } 94 }
63 95
64 private updateComputedAttributes () { 96 updateComputedAttributes () {
65 this.avatarUrl = VideoChannel.GET_ACTOR_AVATAR_URL(this) 97 this.avatarUrl = VideoChannel.GET_ACTOR_AVATAR_URL(this)
98 this.bannerUrl = VideoChannel.GET_ACTOR_BANNER_URL(this)
66 } 99 }
67} 100}
diff --git a/client/src/app/shared/shared-main/video-channel/video-channel.service.ts b/client/src/app/shared/shared-main/video-channel/video-channel.service.ts
index eff3fad4d..e65261763 100644
--- a/client/src/app/shared/shared-main/video-channel/video-channel.service.ts
+++ b/client/src/app/shared/shared-main/video-channel/video-channel.service.ts
@@ -3,7 +3,7 @@ import { catchError, map, tap } from 'rxjs/operators'
3import { HttpClient, HttpParams } from '@angular/common/http' 3import { HttpClient, HttpParams } from '@angular/common/http'
4import { Injectable } from '@angular/core' 4import { Injectable } from '@angular/core'
5import { ComponentPaginationLight, RestExtractor, RestService } from '@app/core' 5import { ComponentPaginationLight, RestExtractor, RestService } from '@app/core'
6import { Avatar, ResultList, VideoChannel as VideoChannelServer, VideoChannelCreate, VideoChannelUpdate } from '@shared/models' 6import { ActorImage, ResultList, VideoChannel as VideoChannelServer, VideoChannelCreate, VideoChannelUpdate } from '@shared/models'
7import { environment } from '../../../../environments/environment' 7import { environment } from '../../../../environments/environment'
8import { Account } from '../account' 8import { Account } from '../account'
9import { AccountService } from '../account/account.service' 9import { AccountService } from '../account/account.service'
@@ -82,15 +82,15 @@ export class VideoChannelService {
82 ) 82 )
83 } 83 }
84 84
85 changeVideoChannelAvatar (videoChannelName: string, avatarForm: FormData) { 85 changeVideoChannelImage (videoChannelName: string, avatarForm: FormData, type: 'avatar' | 'banner') {
86 const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/avatar/pick' 86 const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/' + type + '/pick'
87 87
88 return this.authHttp.post<{ avatar: Avatar }>(url, avatarForm) 88 return this.authHttp.post<{ avatar?: ActorImage, banner?: ActorImage }>(url, avatarForm)
89 .pipe(catchError(err => this.restExtractor.handleError(err))) 89 .pipe(catchError(err => this.restExtractor.handleError(err)))
90 } 90 }
91 91
92 deleteVideoChannelAvatar (videoChannelName: string) { 92 deleteVideoChannelImage (videoChannelName: string, type: 'avatar' | 'banner') {
93 const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/avatar' 93 const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/' + type
94 94
95 return this.authHttp.delete(url) 95 return this.authHttp.delete(url)
96 .pipe( 96 .pipe(
diff --git a/client/src/app/shared/shared-main/video/video.model.ts b/client/src/app/shared/shared-main/video/video.model.ts
index adb6e884f..1c2c4a575 100644
--- a/client/src/app/shared/shared-main/video/video.model.ts
+++ b/client/src/app/shared/shared-main/video/video.model.ts
@@ -6,7 +6,7 @@ import { Actor } from '@app/shared/shared-main/account/actor.model'
6import { VideoChannel } from '@app/shared/shared-main/video-channel/video-channel.model' 6import { VideoChannel } from '@app/shared/shared-main/video-channel/video-channel.model'
7import { peertubeTranslate } from '@shared/core-utils/i18n' 7import { peertubeTranslate } from '@shared/core-utils/i18n'
8import { 8import {
9 Avatar, 9 ActorImage,
10 ServerConfig, 10 ServerConfig,
11 UserRight, 11 UserRight,
12 Video as VideoServerModel, 12 Video as VideoServerModel,
@@ -72,7 +72,7 @@ export class Video implements VideoServerModel {
72 displayName: string 72 displayName: string
73 url: string 73 url: string
74 host: string 74 host: string
75 avatar?: Avatar 75 avatar?: ActorImage
76 } 76 }
77 77
78 channel: { 78 channel: {
@@ -81,7 +81,7 @@ export class Video implements VideoServerModel {
81 displayName: string 81 displayName: string
82 url: string 82 url: string
83 host: string 83 host: string
84 avatar?: Avatar 84 avatar?: ActorImage
85 } 85 }
86 86
87 userHistory?: { 87 userHistory?: {
diff --git a/client/src/app/shared/shared-moderation/moderation.scss b/client/src/app/shared/shared-moderation/moderation.scss
index 4a4e05535..cdcc12fe0 100644
--- a/client/src/app/shared/shared-moderation/moderation.scss
+++ b/client/src/app/shared/shared-moderation/moderation.scss
@@ -32,7 +32,7 @@
32 color: pvar(--inputPlaceholderColor); 32 color: pvar(--inputPlaceholderColor);
33 } 33 }
34 34
35 @include large-screen-ratio($selector: 'div, ::ng-deep iframe') { 35 @include block-ratio($selector: 'div, ::ng-deep iframe') {
36 width: 100% !important; 36 width: 100% !important;
37 height: 100% !important; 37 height: 100% !important;
38 left: 0; 38 left: 0;
diff --git a/client/src/app/shared/shared-moderation/report-modals/report.component.scss b/client/src/app/shared/shared-moderation/report-modals/report.component.scss
index b2606cbd8..0567330f5 100644
--- a/client/src/app/shared/shared-moderation/report-modals/report.component.scss
+++ b/client/src/app/shared/shared-moderation/report-modals/report.component.scss
@@ -21,7 +21,7 @@ textarea {
21} 21}
22 22
23.screenratio { 23.screenratio {
24 @include large-screen-ratio($selector: 'div, ::ng-deep iframe') { 24 @include block-ratio($selector: 'div, ::ng-deep iframe') {
25 left: 0; 25 left: 0;
26 }; 26 };
27} 27}
diff --git a/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts b/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts
index 5b06c0bc7..4ca6f52ad 100644
--- a/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts
+++ b/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts
@@ -61,7 +61,8 @@ export class VideoReportComponent extends FormReactive implements OnInit {
61 baseUrl: this.video.embedUrl, 61 baseUrl: this.video.embedUrl,
62 title: false, 62 title: false,
63 warningTitle: false 63 warningTitle: false
64 }) 64 }),
65 this.video.name
65 ) 66 )
66 ) 67 )
67 } 68 }
diff --git a/client/src/app/shared/shared-share-modal/video-share.component.ts b/client/src/app/shared/shared-share-modal/video-share.component.ts
index b06ff3751..e8760bfcc 100644
--- a/client/src/app/shared/shared-share-modal/video-share.component.ts
+++ b/client/src/app/shared/shared-share-modal/video-share.component.ts
@@ -86,14 +86,14 @@ export class VideoShareComponent {
86 const options = this.getVideoOptions(this.video.embedUrl) 86 const options = this.getVideoOptions(this.video.embedUrl)
87 87
88 const embedUrl = buildVideoLink(options) 88 const embedUrl = buildVideoLink(options)
89 return buildVideoOrPlaylistEmbed(embedUrl) 89 return buildVideoOrPlaylistEmbed(embedUrl, this.video.name)
90 } 90 }
91 91
92 getPlaylistIframeCode () { 92 getPlaylistIframeCode () {
93 const options = this.getPlaylistOptions(this.playlist.embedUrl) 93 const options = this.getPlaylistOptions(this.playlist.embedUrl)
94 94
95 const embedUrl = buildPlaylistLink(options) 95 const embedUrl = buildPlaylistLink(options)
96 return buildVideoOrPlaylistEmbed(embedUrl) 96 return buildVideoOrPlaylistEmbed(embedUrl, this.playlist.displayName)
97 } 97 }
98 98
99 getVideoUrl () { 99 getVideoUrl () {
diff --git a/client/src/app/shared/shared-support-modal/index.ts b/client/src/app/shared/shared-support-modal/index.ts
new file mode 100644
index 000000000..f41bb4bc2
--- /dev/null
+++ b/client/src/app/shared/shared-support-modal/index.ts
@@ -0,0 +1,3 @@
1export * from './support-modal.component'
2
3export * from './shared-support-modal.module'
diff --git a/client/src/app/shared/shared-support-modal/shared-support-modal.module.ts b/client/src/app/shared/shared-support-modal/shared-support-modal.module.ts
new file mode 100644
index 000000000..1101d5535
--- /dev/null
+++ b/client/src/app/shared/shared-support-modal/shared-support-modal.module.ts
@@ -0,0 +1,24 @@
1import { NgModule } from '@angular/core'
2import { SharedFormModule } from '../shared-forms'
3import { SharedGlobalIconModule } from '../shared-icons'
4import { SharedMainModule } from '../shared-main/shared-main.module'
5import { SupportModalComponent } from './support-modal.component'
6
7@NgModule({
8 imports: [
9 SharedMainModule,
10 SharedFormModule,
11 SharedGlobalIconModule
12 ],
13
14 declarations: [
15 SupportModalComponent
16 ],
17
18 exports: [
19 SupportModalComponent
20 ],
21
22 providers: [ ]
23})
24export class SharedSupportModal { }
diff --git a/client/src/app/+videos/+video-watch/modal/video-support.component.html b/client/src/app/shared/shared-support-modal/support-modal.component.html
index 935656d23..4a967987f 100644
--- a/client/src/app/+videos/+video-watch/modal/video-support.component.html
+++ b/client/src/app/shared/shared-support-modal/support-modal.component.html
@@ -1,10 +1,10 @@
1<ng-template #modal let-hide="close"> 1<ng-template #modal let-hide="close">
2 <div class="modal-header"> 2 <div class="modal-header">
3 <h4 i18n class="modal-title">Support {{ video.account.displayName }}</h4> 3 <h4 i18n class="modal-title">Support {{ displayName }}</h4>
4 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> 4 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
5 </div> 5 </div>
6 6
7 <div class="modal-body" [innerHTML]="videoHTMLSupport"></div> 7 <div class="modal-body" [innerHTML]="htmlSupport"></div>
8 8
9 <div class="modal-footer inputs"> 9 <div class="modal-footer inputs">
10 <input 10 <input
diff --git a/client/src/app/+videos/+video-watch/modal/video-support.component.scss b/client/src/app/shared/shared-support-modal/support-modal.component.scss
index 184e09027..184e09027 100644
--- a/client/src/app/+videos/+video-watch/modal/video-support.component.scss
+++ b/client/src/app/shared/shared-support-modal/support-modal.component.scss
diff --git a/client/src/app/shared/shared-support-modal/support-modal.component.ts b/client/src/app/shared/shared-support-modal/support-modal.component.ts
new file mode 100644
index 000000000..ae603c7a8
--- /dev/null
+++ b/client/src/app/shared/shared-support-modal/support-modal.component.ts
@@ -0,0 +1,40 @@
1import { Component, Input, ViewChild } from '@angular/core'
2import { MarkdownService } from '@app/core'
3import { VideoDetails } from '@app/shared/shared-main'
4import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
5import { VideoChannel } from '@shared/models'
6
7@Component({
8 selector: 'my-support-modal',
9 templateUrl: './support-modal.component.html',
10 styleUrls: [ './support-modal.component.scss' ]
11})
12export class SupportModalComponent {
13 @Input() video: VideoDetails = null
14 @Input() videoChannel: VideoChannel = null
15
16 @ViewChild('modal', { static: true }) modal: NgbModal
17
18 htmlSupport = ''
19 displayName = ''
20
21 constructor (
22 private markdownService: MarkdownService,
23 private modalService: NgbModal
24 ) { }
25
26 show () {
27 const modalRef = this.modalService.open(this.modal, { centered: true })
28
29 const support = this.video?.support || this.videoChannel.support
30
31 this.markdownService.enhancedMarkdownToHTML(support)
32 .then(r => this.htmlSupport = r)
33
34 this.displayName = this.video
35 ? this.video.channel.displayName
36 : this.videoChannel.displayName
37
38 return modalRef
39 }
40}
diff --git a/client/src/app/shared/shared-video-miniature/abstract-video-list.html b/client/src/app/shared/shared-video-miniature/abstract-video-list.html
index 07f79cd6d..ee5df28be 100644
--- a/client/src/app/shared/shared-video-miniature/abstract-video-list.html
+++ b/client/src/app/shared/shared-video-miniature/abstract-video-list.html
@@ -43,7 +43,7 @@
43 <div class="no-results" i18n *ngIf="hasDoneFirstQuery && videos.length === 0">No results.</div> 43 <div class="no-results" i18n *ngIf="hasDoneFirstQuery && videos.length === 0">No results.</div>
44 <div 44 <div
45 myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" [dataObservable]="onDataSubject.asObservable()" 45 myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" [dataObservable]="onDataSubject.asObservable()"
46 class="videos" 46 class="videos" [ngClass]="{ 'display-as-row': displayAsRow() }"
47 > 47 >
48 <ng-container *ngFor="let video of videos; trackBy: videoById;"> 48 <ng-container *ngFor="let video of videos; trackBy: videoById;">
49 <h2 class="date-title" *ngIf="getCurrentGroupedDateLabel(video)"> 49 <h2 class="date-title" *ngIf="getCurrentGroupedDateLabel(video)">
@@ -52,8 +52,7 @@
52 52
53 <div class="video-wrapper"> 53 <div class="video-wrapper">
54 <my-video-miniature 54 <my-video-miniature
55 [fitWidth]="true" 55 [video]="video" [user]="userMiniature" [displayAsRow]="displayAsRow()"
56 [video]="video" [user]="userMiniature" [ownerDisplayType]="ownerDisplayType"
57 [displayVideoActions]="displayVideoActions" [displayOptions]="displayOptions" 56 [displayVideoActions]="displayVideoActions" [displayOptions]="displayOptions"
58 (videoBlocked)="removeVideoFromArray(video)" (videoRemoved)="removeVideoFromArray(video)" 57 (videoBlocked)="removeVideoFromArray(video)" (videoRemoved)="removeVideoFromArray(video)"
59 > 58 >
diff --git a/client/src/app/shared/shared-video-miniature/abstract-video-list.scss b/client/src/app/shared/shared-video-miniature/abstract-video-list.scss
index 0a8aa8fa4..6570b63d0 100644
--- a/client/src/app/shared/shared-video-miniature/abstract-video-list.scss
+++ b/client/src/app/shared/shared-video-miniature/abstract-video-list.scss
@@ -69,7 +69,16 @@ $iconSize: 16px;
69} 69}
70 70
71.margin-content { 71.margin-content {
72 @include fluid-videos-miniature-layout; 72 @include grid-videos-miniature-layout;
73}
74
75.display-as-row.videos {
76 margin-left: pvar(--horizontalMarginContent);
77 margin-right: pvar(--horizontalMarginContent);
78
79 .video-wrapper {
80 margin-bottom: 15px;
81 }
73} 82}
74 83
75@media screen and (max-width: $mobile-view) { 84@media screen and (max-width: $mobile-view) {
diff --git a/client/src/app/shared/shared-video-miniature/abstract-video-list.ts b/client/src/app/shared/shared-video-miniature/abstract-video-list.ts
index c13cb3748..f83380513 100644
--- a/client/src/app/shared/shared-video-miniature/abstract-video-list.ts
+++ b/client/src/app/shared/shared-video-miniature/abstract-video-list.ts
@@ -28,8 +28,8 @@ import { isLastMonth, isLastWeek, isThisMonth, isToday, isYesterday } from '@sha
28import { ServerConfig, UserRight, VideoFilter, VideoSortField } from '@shared/models' 28import { ServerConfig, UserRight, VideoFilter, VideoSortField } from '@shared/models'
29import { NSFWPolicyType } from '@shared/models/videos/nsfw-policy.type' 29import { NSFWPolicyType } from '@shared/models/videos/nsfw-policy.type'
30import { Syndication, Video } from '../shared-main' 30import { Syndication, Video } from '../shared-main'
31import { MiniatureDisplayOptions, OwnerDisplayType } from './video-miniature.component'
32import { GenericHeaderComponent, VideoListHeaderComponent } from './video-list-header.component' 31import { GenericHeaderComponent, VideoListHeaderComponent } from './video-list-header.component'
32import { MiniatureDisplayOptions } from './video-miniature.component'
33 33
34enum GroupDate { 34enum GroupDate {
35 UNKNOWN = 0, 35 UNKNOWN = 0,
@@ -65,7 +65,6 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, AfterConte
65 loadOnInit = true 65 loadOnInit = true
66 loadUserVideoPreferences = false 66 loadUserVideoPreferences = false
67 67
68 ownerDisplayType: OwnerDisplayType = 'account'
69 displayModerationBlock = false 68 displayModerationBlock = false
70 titleTooltip: string 69 titleTooltip: string
71 displayVideoActions = true 70 displayVideoActions = true
@@ -320,6 +319,11 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, AfterConte
320 viewContainerRef.createComponent(componentFactory, 0, injector) 319 viewContainerRef.createComponent(componentFactory, 0, injector)
321 } 320 }
322 321
322 // Can be redefined by child
323 displayAsRow () {
324 return false
325 }
326
323 // On videos hook for children that want to do something 327 // On videos hook for children that want to do something
324 protected onMoreVideos () { /* empty */ } 328 protected onMoreVideos () { /* empty */ }
325 329
diff --git a/client/src/app/shared/shared-video-miniature/video-download.component.html b/client/src/app/shared/shared-video-miniature/video-download.component.html
index 4608e93e7..8a9218343 100644
--- a/client/src/app/shared/shared-video-miniature/video-download.component.html
+++ b/client/src/app/shared/shared-video-miniature/video-download.component.html
@@ -17,85 +17,116 @@
17 </div> 17 </div>
18 18
19 <div class="modal-body"> 19 <div class="modal-body">
20 <div class="form-group"> 20 <div class="alert alert-warning" *ngIf="isConfidentialVideo()" i18n>
21 <div class="alert alert-warning" *ngIf="isConfidentialVideo()" i18n> 21 The following link contains a private token and should not be shared with anyone.
22 The following link contains a private token and should not be shared with anyone. 22 </div>
23 </div>
24 23
24 <ng-container *ngIf="type === 'subtitles'">
25 <div class="input-group input-group-sm"> 25 <div class="input-group input-group-sm">
26 <div class="input-group-prepend peertube-select-container">
27 <select *ngIf="type === 'video'" [(ngModel)]="resolutionId" (ngModelChange)="onResolutionIdChange()">
28 <option *ngFor="let file of getVideoFiles()" [value]="file.resolution.id">{{ file.resolution.label }}</option>
29 </select>
30
31 <select *ngIf="type === 'subtitles'" [(ngModel)]="subtitleLanguageId">
32 <option *ngFor="let caption of videoCaptions" [value]="caption.language.id">{{ caption.language.label }}</option>
33 </select>
34 </div>
35
36 <input #urlInput (click)="urlInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="getLink()" /> 26 <input #urlInput (click)="urlInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="getLink()" />
37 <div class="input-group-append" *ngIf="!isConfidentialVideo()"> 27 <div class="input-group-append" *ngIf="!isConfidentialVideo()">
38 <button [cdkCopyToClipboard]="urlInput.value" (click)="activateCopiedMessage()" type="button" class="btn btn-outline-secondary"> 28 <button [cdkCopyToClipboard]="urlInput.value" (click)="activateCopiedMessage()" type="button" class="btn btn-outline-secondary">
39 <span class="glyphicon glyphicon-copy"></span> 29 <span class="glyphicon glyphicon-duplicate"></span>
40 </button> 30 </button>
41 </div> 31 </div>
42 </div> 32 </div>
43 </div> 33 </ng-container>
44 34
45 <ng-container *ngIf="type === 'video' && videoFile?.metadata"> 35 <ng-container *ngIf="type === 'video'">
46 <div ngbNav #nav="ngbNav" class="nav-tabs"> 36 <div ngbNav #resolutionNav="ngbNav" class="nav-tabs" [activeId]="resolutionId" (activeIdChange)="onResolutionIdChange($event)">
47 37
48 <ng-container ngbNavItem> 38 <ng-container *ngFor="let file of getVideoFiles()" [ngbNavItem]="file.resolution.id">
49 <a ngbNavLink i18n>Format</a> 39 <a ngbNavLink i18n>{{ file.resolution.label }}</a>
40
50 <ng-template ngbNavContent> 41 <ng-template ngbNavContent>
51 <div class="file-metadata"> 42 <div class="nav-content">
52 <div class="metadata-attribute metadata-attribute-tags" *ngFor="let item of videoFileMetadataFormat | keyvalue"> 43 <div class="input-group input-group-sm">
53 <span i18n class="metadata-attribute-label">{{ item.value.label }}</span> 44 <input #urlInput (click)="urlInput.select()" type="text" class="form-control input-sm readonly" readonly [value]="getLink()" />
54 <span class="metadata-attribute-value">{{ item.value.value }}</span> 45 <div class="input-group-append" *ngIf="!isConfidentialVideo()">
46 <button [cdkCopyToClipboard]="urlInput.value" (click)="activateCopiedMessage()" type="button" class="btn btn-outline-secondary">
47 <span class="glyphicon glyphicon-duplicate"></span>
48 </button>
49 </div>
55 </div> 50 </div>
56 </div> 51 </div>
57 </ng-template> 52 </ng-template>
58 </ng-container> 53 </ng-container>
59 54 </div>
60 <ng-container ngbNavItem [disabled]="videoFileMetadataVideoStream === undefined"> 55 <div [ngbNavOutlet]="resolutionNav"></div>
61 <a ngbNavLink i18n>Video stream</a> 56
62 <ng-template ngbNavContent> 57 <div class="advanced-filters collapse-transition" [ngbCollapse]="isAdvancedCustomizationCollapsed">
63 <div class="file-metadata"> 58 <ng-container *ngIf="videoFile?.metadata">
64 <div class="metadata-attribute metadata-attribute-tags" *ngFor="let item of videoFileMetadataVideoStream | keyvalue"> 59 <div ngbNav #nav="ngbNav" class="nav-tabs nav-metadata">
65 <span i18n class="metadata-attribute-label">{{ item.value.label }}</span> 60 <ng-container ngbNavItem>
66 <span class="metadata-attribute-value">{{ item.value.value }}</span> 61 <a ngbNavLink i18n>Format</a>
67 </div> 62 <ng-template ngbNavContent>
63 <div class="file-metadata">
64 <div class="metadata-attribute metadata-attribute-tags" *ngFor="let item of videoFileMetadataFormat | keyvalue">
65 <span i18n class="metadata-attribute-label">{{ item.value.label }}</span>
66 <span class="metadata-attribute-value">{{ item.value.value }}</span>
67 </div>
68 </div>
69 </ng-template>
70
71 <ng-container ngbNavItem [disabled]="videoFileMetadataVideoStream === undefined">
72 <a ngbNavLink i18n>Video stream</a>
73 <ng-template ngbNavContent>
74 <div class="file-metadata">
75 <div class="metadata-attribute metadata-attribute-tags" *ngFor="let item of videoFileMetadataVideoStream | keyvalue">
76 <span i18n class="metadata-attribute-label">{{ item.value.label }}</span>
77 <span class="metadata-attribute-value">{{ item.value.value }}</span>
78 </div>
79 </div>
80 </ng-template>
81 </ng-container>
82
83 <ng-container ngbNavItem [disabled]="videoFileMetadataAudioStream === undefined">
84 <a ngbNavLink i18n>Audio stream</a>
85 <ng-template ngbNavContent>
86 <div class="file-metadata">
87 <div class="metadata-attribute metadata-attribute-tags" *ngFor="let item of videoFileMetadataAudioStream | keyvalue">
88 <span i18n class="metadata-attribute-label">{{ item.value.label }}</span>
89 <span class="metadata-attribute-value">{{ item.value.value }}</span>
90 </div>
91 </div>
92 </ng-template>
93 </ng-container>
94
95 </ng-container>
96 </div>
97 <div [ngbNavOutlet]="nav"></div>
98 <div class="download-type">
99 <div class="peertube-radio-container">
100 <input type="radio" name="download" id="download-direct" [(ngModel)]="downloadType" value="direct">
101 <label i18n for="download-direct">Direct download</label>
68 </div> 102 </div>
69 </ng-template> 103 <div class="peertube-radio-container">
70 </ng-container> 104 <input type="radio" name="download" id="download-torrent" [(ngModel)]="downloadType" value="torrent">
71 105 <label i18n for="download-torrent">Torrent (.torrent file)</label>
72 <ng-container ngbNavItem [disabled]="videoFileMetadataAudioStream === undefined">
73 <a ngbNavLink i18n>Audio stream</a>
74 <ng-template ngbNavContent>
75 <div class="file-metadata">
76 <div class="metadata-attribute metadata-attribute-tags" *ngFor="let item of videoFileMetadataAudioStream | keyvalue">
77 <span i18n class="metadata-attribute-label">{{ item.value.label }}</span>
78 <span class="metadata-attribute-value">{{ item.value.value }}</span>
79 </div>
80 </div> 106 </div>
81 </ng-template> 107 </div>
82 </ng-container> 108 </ng-container>
83 </div> 109 </div>
84 110
85 <div [ngbNavOutlet]="nav"></div> 111 <div (click)="isAdvancedCustomizationCollapsed = !isAdvancedCustomizationCollapsed" role="button" class="advanced-filters-button"
86 </ng-container> 112 [attr.aria-expanded]="!isAdvancedCustomizationCollapsed" aria-controls="collapseBasic">
87 113 <ng-container *ngIf="isAdvancedCustomizationCollapsed">
88 <div class="download-type" *ngIf="type === 'video'"> 114 <span class="glyphicon glyphicon-menu-down"></span>
89 <div class="peertube-radio-container"> 115
90 <input type="radio" name="download" id="download-direct" [(ngModel)]="downloadType" value="direct"> 116 <ng-container i18n>
91 <label i18n for="download-direct">Direct download</label> 117 Advanced
92 </div> 118 </ng-container>
93 119 </ng-container>
94 <div class="peertube-radio-container"> 120
95 <input type="radio" name="download" id="download-torrent" [(ngModel)]="downloadType" value="torrent"> 121 <ng-container *ngIf="!isAdvancedCustomizationCollapsed">
96 <label i18n for="download-torrent">Torrent (.torrent file)</label> 122 <span class="glyphicon glyphicon-menu-up"></span>
123
124 <ng-container i18n>
125 Simple
126 </ng-container>
127 </ng-container>
97 </div> 128 </div>
98 </div> 129 </ng-container>
99 </div> 130 </div>
100 131
101 <div class="modal-footer inputs"> 132 <div class="modal-footer inputs">
diff --git a/client/src/app/shared/shared-video-miniature/video-download.component.scss b/client/src/app/shared/shared-video-miniature/video-download.component.scss
index d407e9531..199c3dac8 100644
--- a/client/src/app/shared/shared-video-miniature/video-download.component.scss
+++ b/client/src/app/shared/shared-video-miniature/video-download.component.scss
@@ -1,6 +1,28 @@
1@import 'variables'; 1@import 'variables';
2@import 'mixins'; 2@import 'mixins';
3 3
4.nav-content {
5 margin-top: 30px;
6}
7
8.advanced-filters-button {
9 display: flex;
10 justify-content: center;
11 align-items: center;
12 margin-top: 20px;
13 font-size: 16px;
14 font-weight: 600;
15 cursor: pointer;
16
17 .nav-tabs {
18 margin-top: 10x;
19 }
20
21 .glyphicon {
22 margin-right: 5px;
23 }
24}
25
4.peertube-select-container { 26.peertube-select-container {
5 @include peertube-select-container(85px); 27 @include peertube-select-container(85px);
6 28
@@ -15,12 +37,21 @@
15 } 37 }
16} 38}
17 39
40.action-button-cancel {
41 @include peertube-button-link;
42}
43
44.action-button-submit {
45 @include peertube-button-link;
46 @include orange-button;
47}
48
18#dropdownDownloadType { 49#dropdownDownloadType {
19 cursor: pointer; 50 cursor: pointer;
20} 51}
21 52
22.download-type { 53.download-type {
23 margin-top: 30px; 54 margin-top: 20px;
24 55
25 .peertube-radio-container { 56 .peertube-radio-container {
26 @include peertube-radio-container; 57 @include peertube-radio-container;
@@ -30,6 +61,10 @@
30 } 61 }
31} 62}
32 63
64.nav-metadata {
65 margin-top: 20px;
66}
67
33.file-metadata { 68.file-metadata {
34 padding: 1rem; 69 padding: 1rem;
35} 70}
diff --git a/client/src/app/shared/shared-video-miniature/video-download.component.ts b/client/src/app/shared/shared-video-miniature/video-download.component.ts
index 90f4daf7c..1e3745d94 100644
--- a/client/src/app/shared/shared-video-miniature/video-download.component.ts
+++ b/client/src/app/shared/shared-video-miniature/video-download.component.ts
@@ -1,7 +1,9 @@
1import { mapValues, pick } from 'lodash-es' 1import { mapValues, pick } from 'lodash-es'
2import { pipe } from 'rxjs'
3import { tap } from 'rxjs/operators'
2import { Component, ElementRef, Inject, LOCALE_ID, ViewChild } from '@angular/core' 4import { Component, ElementRef, Inject, LOCALE_ID, ViewChild } from '@angular/core'
3import { AuthService, Notifier } from '@app/core' 5import { AuthService, HooksService, Notifier } from '@app/core'
4import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap' 6import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
5import { VideoCaption, VideoFile, VideoPrivacy } from '@shared/models' 7import { VideoCaption, VideoFile, VideoPrivacy } from '@shared/models'
6import { BytesPipe, NumberFormatterPipe, VideoDetails, VideoService } from '../shared-main' 8import { BytesPipe, NumberFormatterPipe, VideoDetails, VideoService } from '../shared-main'
7 9
@@ -16,7 +18,7 @@ type FileMetadata = { [key: string]: { label: string, value: string }}
16export class VideoDownloadComponent { 18export class VideoDownloadComponent {
17 @ViewChild('modal', { static: true }) modal: ElementRef 19 @ViewChild('modal', { static: true }) modal: ElementRef
18 20
19 downloadType: 'direct' | 'torrent' = 'torrent' 21 downloadType: 'direct' | 'torrent' = 'direct'
20 resolutionId: number | string = -1 22 resolutionId: number | string = -1
21 subtitleLanguageId: string 23 subtitleLanguageId: string
22 24
@@ -26,7 +28,9 @@ export class VideoDownloadComponent {
26 videoFileMetadataVideoStream: FileMetadata | undefined 28 videoFileMetadataVideoStream: FileMetadata | undefined
27 videoFileMetadataAudioStream: FileMetadata | undefined 29 videoFileMetadataAudioStream: FileMetadata | undefined
28 videoCaptions: VideoCaption[] 30 videoCaptions: VideoCaption[]
29 activeModal: NgbActiveModal 31 activeModal: NgbModalRef
32
33 isAdvancedCustomizationCollapsed = true
30 34
31 type: DownloadType = 'video' 35 type: DownloadType = 'video'
32 36
@@ -38,7 +42,8 @@ export class VideoDownloadComponent {
38 private notifier: Notifier, 42 private notifier: Notifier,
39 private modalService: NgbModal, 43 private modalService: NgbModal,
40 private videoService: VideoService, 44 private videoService: VideoService,
41 private auth: AuthService 45 private auth: AuthService,
46 private hooks: HooksService
42 ) { 47 ) {
43 this.bytesPipe = new BytesPipe() 48 this.bytesPipe = new BytesPipe()
44 this.numbersPipe = new NumberFormatterPipe(this.localeId) 49 this.numbersPipe = new NumberFormatterPipe(this.localeId)
@@ -62,9 +67,13 @@ export class VideoDownloadComponent {
62 67
63 this.activeModal = this.modalService.open(this.modal, { centered: true }) 68 this.activeModal = this.modalService.open(this.modal, { centered: true })
64 69
65 this.resolutionId = this.getVideoFiles()[0].resolution.id 70 this.onResolutionIdChange(this.getVideoFiles()[0].resolution.id)
66 this.onResolutionIdChange() 71
67 if (this.videoCaptions) this.subtitleLanguageId = this.videoCaptions[0].language.id 72 if (this.videoCaptions) this.subtitleLanguageId = this.videoCaptions[0].language.id
73
74 this.activeModal.shown.subscribe(() => {
75 this.hooks.runAction('action:modal.video-download.shown', 'common')
76 })
68 } 77 }
69 78
70 onClose () { 79 onClose () {
@@ -83,11 +92,15 @@ export class VideoDownloadComponent {
83 : this.getVideoFileLink() 92 : this.getVideoFileLink()
84 } 93 }
85 94
86 async onResolutionIdChange () { 95 async onResolutionIdChange (resolutionId: number) {
96 this.resolutionId = resolutionId
87 this.videoFile = this.getVideoFile() 97 this.videoFile = this.getVideoFile()
88 if (this.videoFile.metadata || !this.videoFile.metadataUrl) return
89 98
90 await this.hydrateMetadataFromMetadataUrl(this.videoFile) 99 if (!this.videoFile.metadata) {
100 if (!this.videoFile.metadataUrl) return
101
102 await this.hydrateMetadataFromMetadataUrl(this.videoFile)
103 }
91 104
92 this.videoFileMetadataFormat = this.videoFile 105 this.videoFileMetadataFormat = this.videoFile
93 ? this.getMetadataFormat(this.videoFile.metadata.format) 106 ? this.getMetadataFormat(this.videoFile.metadata.format)
@@ -101,9 +114,6 @@ export class VideoDownloadComponent {
101 } 114 }
102 115
103 getVideoFile () { 116 getVideoFile () {
104 // HTML select send us a string, so convert it to a number
105 this.resolutionId = parseInt(this.resolutionId.toString(), 10)
106
107 const file = this.getVideoFiles().find(f => f.resolution.id === this.resolutionId) 117 const file = this.getVideoFiles().find(f => f.resolution.id === this.resolutionId)
108 if (!file) { 118 if (!file) {
109 console.error('Could not find file with resolution %d.', this.resolutionId) 119 console.error('Could not find file with resolution %d.', this.resolutionId)
@@ -201,7 +211,7 @@ export class VideoDownloadComponent {
201 211
202 private hydrateMetadataFromMetadataUrl (file: VideoFile) { 212 private hydrateMetadataFromMetadataUrl (file: VideoFile) {
203 const observable = this.videoService.getVideoFileMetadata(file.metadataUrl) 213 const observable = this.videoService.getVideoFileMetadata(file.metadataUrl)
204 observable.subscribe(res => file.metadata = res) 214 .pipe(tap(res => file.metadata = res))
205 215
206 return observable.toPromise() 216 return observable.toPromise()
207 } 217 }
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.html b/client/src/app/shared/shared-video-miniature/video-miniature.component.html
index 7a6df7b64..bac8bcc2d 100644
--- a/client/src/app/shared/shared-video-miniature/video-miniature.component.html
+++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.html
@@ -1,4 +1,4 @@
1<div class="video-miniature" [ngClass]="{ 'display-as-row': displayAsRow, 'fit-width': fitWidth }" (mouseenter)="loadActions()"> 1<div class="video-miniature" [ngClass]="getClasses()" (mouseenter)="loadActions()">
2 <my-video-thumbnail 2 <my-video-thumbnail
3 [video]="video" [nsfw]="isVideoBlur" [videoRouterLink]="videoRouterLink" [videoHref]="videoHref" [videoTarget]="videoTarget" 3 [video]="video" [nsfw]="isVideoBlur" [videoRouterLink]="videoRouterLink" [videoHref]="videoHref" [videoTarget]="videoTarget"
4 [displayWatchLaterPlaylist]="isWatchLaterPlaylistDisplayed()" [inWatchLaterPlaylist]="inWatchLaterPlaylist" (watchLaterClick)="onWatchLaterClick($event)" 4 [displayWatchLaterPlaylist]="isWatchLaterPlaylistDisplayed()" [inWatchLaterPlaylist]="inWatchLaterPlaylist" (watchLaterClick)="onWatchLaterClick($event)"
@@ -9,9 +9,9 @@
9 9
10 <div class="video-bottom"> 10 <div class="video-bottom">
11 <div class="video-miniature-information"> 11 <div class="video-miniature-information">
12 <div class="d-inline-flex video-miniature-meta"> 12 <div class="d-flex video-miniature-meta">
13 <a *ngIf="displayOptions.avatar" class="avatar" [routerLink]="[ '/video-channels', video.byVideoChannel ]" [title]="channelLinkTitle"> 13 <a *ngIf="displayOptions.avatar" class="avatar" [routerLink]="[ '/video-channels', video.byVideoChannel ]" [title]="channelLinkTitle">
14 <img [src]="getAvatarUrl()" alt="" /> 14 <img [src]="getAvatarUrl()" alt="" [ngClass]="{ channel: displayOwnerVideoChannel() }" />
15 </a> 15 </a>
16 16
17 <div class="w-100 d-flex flex-column"> 17 <div class="w-100 d-flex flex-column">
@@ -33,7 +33,7 @@
33 </span> 33 </span>
34 </span> 34 </span>
35 35
36 <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerAccount()" class="video-miniature-account" [routerLink]="[ '/accounts', video.byAccount ]"> 36 <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerAccount()" class="video-miniature-account" [routerLink]="[ '/video-channels', video.byVideoChannel ]">
37 {{ video.byAccount }} 37 {{ video.byAccount }}
38 </a> 38 </a>
39 <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerVideoChannel()" class="video-miniature-channel" [routerLink]="[ '/video-channels', video.byVideoChannel ]"> 39 <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerVideoChannel()" class="video-miniature-channel" [routerLink]="[ '/video-channels', video.byVideoChannel ]">
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.scss b/client/src/app/shared/shared-video-miniature/video-miniature.component.scss
index 38cac5b6e..621951919 100644
--- a/client/src/app/shared/shared-video-miniature/video-miniature.component.scss
+++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.scss
@@ -3,198 +3,205 @@
3@import '_miniature'; 3@import '_miniature';
4 4
5$more-button-width: 40px; 5$more-button-width: 40px;
6$more-margin-right: 15px;
7 6
8.video-miniature { 7.video-miniature-name {
9 display: inline-flex; 8 @include miniature-name;
10 flex-direction: column; 9}
11 padding-bottom: $video-miniature-margin-bottom;
12 vertical-align: top;
13 10
14 .video-bottom { 11.video-miniature-information {
15 display: flex; 12 width: calc(100% - #{$more-button-width});
13}
16 14
17 .video-miniature-information { 15.avatar {
18 width: $video-miniature-width - $more-button-width - $more-margin-right; 16 margin: 10px 10px 0 0;
19 line-height: normal;
20 17
21 .avatar { 18 img:not(.channel) {
22 margin: 10px 10px 0 0; 19 @include avatar(40px);
20 }
23 21
24 img { 22 img.channel {
25 @include avatar(40px); 23 @include channel-avatar(40px);
26 } 24 }
27 } 25}
28 26
29 .video-miniature-name { 27.video-miniature-created-at-views {
30 @include miniature-name; 28 font-size: 13px;
31 width: calc(100% - #{$more-button-width}); 29}
32 }
33 30
34 .video-miniature-meta { 31.video-miniature-account,
35 width: calc(100% + #{$more-button-width}); 32.video-miniature-channel {
36 overflow: hidden; 33 @include disable-default-a-behaviour;
37 } 34 @include ellipsis;
38 35
39 .video-miniature-created-at-views { 36 display: block;
40 display: block; 37 font-size: 13px;
41 font-size: 13px; 38 color: pvar(--greyForegroundColor);
42 }
43 39
44 .video-miniature-account, 40 &:hover {
45 .video-miniature-channel { 41 color: $grey-foreground-hover-color;
46 @include disable-default-a-behaviour; 42 }
47 @include ellipsis; 43}
48 44
49 display: block; 45.video-info-privacy,
50 font-size: 13px; 46.video-info-blocked .blocked-label,
51 color: pvar(--greyForegroundColor); 47.video-info-nsfw {
48 font-weight: $font-semibold;
49}
52 50
53 &:hover { 51.video-info-blocked {
54 color: $grey-foreground-hover-color; 52 color: red;
55 }
56 }
57 53
58 .video-info-privacy, 54 .blocked-reason::before {
59 .video-info-blocked .blocked-label, 55 content: ' - ';
60 .video-info-nsfw { 56 }
61 font-weight: $font-semibold; 57}
62 }
63 58
64 .video-info-blocked { 59.video-info-nsfw {
65 color: red; 60 color: red;
61}
66 62
67 .blocked-reason::before { 63.video-actions {
68 content: ' - '; 64 width: $more-button-width;
69 } 65 height: 30px;
70 }
71 66
72 .video-info-nsfw { 67 ::ng-deep .dropdown-root:not(.show) {
73 color: red; 68 opacity: 0;
74 } 69 }
75 }
76 70
77 .video-actions { 71 ::ng-deep .playlist-dropdown.show + my-action-dropdown .dropdown-root {
78 margin-top: 3px; 72 opacity: 1;
79 width: $more-button-width; 73 }
80 height: 30px;
81 74
82 ::ng-deep .dropdown-root:not(.show) { 75 ::ng-deep .more-icon {
83 opacity: 0; 76 opacity: .6;
84 }
85 77
86 ::ng-deep .playlist-dropdown.show + my-action-dropdown .dropdown-root { 78 &:hover {
87 opacity: 1; 79 opacity: 1;
88 } 80 }
81 }
82}
89 83
90 ::ng-deep .more-icon { 84.video-miniature {
91 opacity: .6; 85 &:hover ::ng-deep .video-thumbnail-actions-overlay,
86 &:hover .video-actions ::ng-deep .dropdown-root {
87 opacity: 1 !important;
88 }
89}
92 90
93 &:hover { 91// Grid mode
94 opacity: 1; 92// Takes all the width on mobile
95 } 93.video-miniature:not(.display-as-row) {
96 } 94 display: flex;
97 } 95 flex-direction: column;
96 padding-bottom: $video-miniature-margin-bottom;
97 width: 100%;
98 98
99 @media screen and (max-width: $small-view) { 99 my-video-thumbnail {
100 .video-miniature-information { 100 @include block-ratio($selector: '::ng-deep .video-thumbnail');
101 margin: 0 10px; 101 }
102 }
103 102
104 .video-actions { 103 .video-bottom {
105 margin: 0; 104 display: flex;
106 top: -3px; 105 width: 100%;
106 }
107 107
108 ::ng-deep .dropdown-root { 108 .video-miniature-name {
109 opacity: 1 !important; 109 margin-top: 10px;
110 } 110 margin-bottom: 5px;
111 }
112 }
113 } 111 }
114 112
115 &:hover ::ng-deep .video-thumbnail .video-thumbnail-actions-overlay, 113 .video-miniature-created-at-views {
116 &:hover .video-bottom .video-actions ::ng-deep .dropdown-root { 114 display: block;
117 opacity: 1;
118 } 115 }
119 116
120 &.fit-width { 117 .video-actions {
118 margin-top: 3px;
119 }
120
121 @media screen and (max-width: $small-view) {
121 width: 100%; 122 width: 100%;
123 margin-bottom: 25px;
124
125 .video-miniature-information {
126 margin: 0 10px;
127
128 width: 100%;
129 text-align: left;
130 }
122 131
123 .video-bottom { 132 .video-actions {
124 width: 100% !important; 133 margin: 0;
134 top: -3px;
125 135
126 .video-miniature-information { 136 ::ng-deep .dropdown-root {
127 width: calc(100% - #{$more-button-width}) !important; 137 opacity: 1 !important;
128 } 138 }
129 } 139 }
130 140
131 my-video-thumbnail { 141 ::ng-deep .video-thumbnail {
132 @include large-screen-ratio($selector: '::ng-deep .video-thumbnail'); 142 border-radius: 0;
133 } 143 }
134 } 144 }
145}
146
147.video-miniature.display-as-row {
148 --rowThumbnailWidth: #{$video-thumbnail-width};
149 --rowThumbnailHeight: #{$video-thumbnail-height};
150
151 display: flex;
152 flex-direction: row;
135 153
136 &.display-as-row { 154 .video-bottom {
137 flex-direction: row;
138 padding-bottom: 0;
139 height: auto;
140 display: flex; 155 display: flex;
141 flex-grow: 1; 156 }
142 157
143 my-video-thumbnail { 158 // We don't display avatar in row mode
144 margin-right: 10px; 159 .avatar {
145 } 160 display: none;
161 }
146 162
147 .video-bottom { 163 my-video-thumbnail {
148 .video-miniature-information { 164 min-width: var(--rowThumbnailWidth);
149 @media screen and (min-width: $small-view) { 165 max-width: var(--rowThumbnailWidth);
150 width: auto; 166 height: var(--rowThumbnailHeight);
151 min-width: 500px; 167 margin-right: 10px;
152 } 168 }
153
154 .video-miniature-name {
155 @include ellipsis-multiline(1.3em, 2);
156
157 margin-top: 2px;
158 margin-bottom: 5px;
159 }
160
161 .video-miniature-created-at-views,
162 .video-miniature-account,
163 .video-miniature-channel {
164 font-size: 95%;
165 width: fit-content;
166 }
167
168 .video-miniature-created-at-views + .video-miniature-channel {
169 margin-top: 5px;
170 }
171
172 .video-info-privacy {
173 margin-top: 5px;
174 }
175
176 .video-info-blocked {
177 margin-top: 3px;
178 }
179 }
180 169
181 .video-actions { 170 .video-miniature-name {
182 margin: 0; 171 @include ellipsis-multiline($video-miniature-row-name-font-size, 2);
183 top: -3px; 172 }
184 } 173
185 } 174 .video-miniature-created-at-views,
175 .video-miniature-account,
176 .video-miniature-channel {
177 font-size: $video-miniature-row-info-font-size;
178 }
186 179
187 @media screen and (max-width: $small-view) { 180 .video-actions {
188 flex-direction: column; 181 margin-top: -3px;
189 height: auto; 182 }
183}
190 184
191 my-video-thumbnail { 185@include on-small-main-col {
192 margin-right: 0; 186 .video-miniature.display-as-row {
193 } 187 --rowThumbnailWidth: #{$video-thumbnail-medium-width};
188 --rowThumbnailHeight: #{$video-thumbnail-medium-height};
189 }
190}
194 191
195 .video-miniature-information { 192@include on-mobile-main-col {
196 min-width: initial; 193 .video-miniature.display-as-row {
197 } 194 --rowThumbnailWidth: #{$video-thumbnail-small-width};
195 --rowThumbnailHeight: #{$video-thumbnail-small-height};
196
197 .video-miniature-name {
198 font-size: $video-miniature-row-info-font-size;
199 }
200
201 .video-miniature-created-at-views,
202 .video-miniature-account,
203 .video-miniature-channel {
204 font-size: $video-miniature-row-mobile-info-font-size;
198 } 205 }
199 } 206 }
200} 207}
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts
index cc5665ab1..48da92d6b 100644
--- a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts
+++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts
@@ -16,7 +16,6 @@ import { Video } from '../shared-main'
16import { VideoPlaylistService } from '../shared-video-playlist' 16import { VideoPlaylistService } from '../shared-video-playlist'
17import { VideoActionsDisplayType } from './video-actions-dropdown.component' 17import { VideoActionsDisplayType } from './video-actions-dropdown.component'
18 18
19export type OwnerDisplayType = 'account' | 'videoChannel' | 'auto'
20export type MiniatureDisplayOptions = { 19export type MiniatureDisplayOptions = {
21 date?: boolean 20 date?: boolean
22 views?: boolean 21 views?: boolean
@@ -40,7 +39,6 @@ export class VideoMiniatureComponent implements OnInit {
40 @Input() user: User 39 @Input() user: User
41 @Input() video: Video 40 @Input() video: Video
42 41
43 @Input() ownerDisplayType: OwnerDisplayType = 'account'
44 @Input() displayOptions: MiniatureDisplayOptions = { 42 @Input() displayOptions: MiniatureDisplayOptions = {
45 date: true, 43 date: true,
46 views: true, 44 views: true,
@@ -51,9 +49,9 @@ export class VideoMiniatureComponent implements OnInit {
51 state: false, 49 state: false,
52 blacklistInfo: false 50 blacklistInfo: false
53 } 51 }
54 @Input() displayAsRow = false
55 @Input() displayVideoActions = true 52 @Input() displayVideoActions = true
56 @Input() fitWidth = false 53
54 @Input() displayAsRow = false
57 55
58 @Input() videoLinkType: VideoLinkType = 'internal' 56 @Input() videoLinkType: VideoLinkType = 'internal'
59 57
@@ -89,7 +87,7 @@ export class VideoMiniatureComponent implements OnInit {
89 videoHref: string 87 videoHref: string
90 videoTarget: string 88 videoTarget: string
91 89
92 private ownerDisplayTypeChosen: 'account' | 'videoChannel' 90 private ownerDisplayType: 'account' | 'videoChannel'
93 91
94 constructor ( 92 constructor (
95 private screenService: ScreenService, 93 private screenService: ScreenService,
@@ -140,11 +138,11 @@ export class VideoMiniatureComponent implements OnInit {
140 } 138 }
141 139
142 displayOwnerAccount () { 140 displayOwnerAccount () {
143 return this.ownerDisplayTypeChosen === 'account' 141 return this.ownerDisplayType === 'account'
144 } 142 }
145 143
146 displayOwnerVideoChannel () { 144 displayOwnerVideoChannel () {
147 return this.ownerDisplayTypeChosen === 'videoChannel' 145 return this.ownerDisplayType === 'videoChannel'
148 } 146 }
149 147
150 isUnlistedVideo () { 148 isUnlistedVideo () {
@@ -183,7 +181,7 @@ export class VideoMiniatureComponent implements OnInit {
183 } 181 }
184 182
185 getAvatarUrl () { 183 getAvatarUrl () {
186 if (this.ownerDisplayTypeChosen === 'account') { 184 if (this.displayOwnerAccount()) {
187 return this.video.accountAvatarUrl 185 return this.video.accountAvatarUrl
188 } 186 }
189 187
@@ -244,21 +242,26 @@ export class VideoMiniatureComponent implements OnInit {
244 return this.displayVideoActions && this.isUserLoggedIn() && this.inWatchLaterPlaylist !== undefined 242 return this.displayVideoActions && this.isUserLoggedIn() && this.inWatchLaterPlaylist !== undefined
245 } 243 }
246 244
247 private setUpBy () { 245 getClasses () {
248 if (this.ownerDisplayType === 'account' || this.ownerDisplayType === 'videoChannel') { 246 return {
249 this.ownerDisplayTypeChosen = this.ownerDisplayType 247 'display-as-row': this.displayAsRow
250 return
251 } 248 }
249 }
250
251 private setUpBy () {
252 const accountName = this.video.account.name
252 253
253 // If the video channel name an UUID (not really displayable, we changed this behaviour in v1.0.0-beta.12) 254 // If the video channel name is an UUID (not really displayable, we changed this behaviour in v1.0.0-beta.12)
255 // Or has not been customized (default created channel display name)
254 // -> Use the account name 256 // -> Use the account name
255 if ( 257 if (
256 this.video.channel.name === `${this.video.account.name}_channel` || 258 this.video.channel.displayName === `Default ${accountName} channel` ||
259 this.video.channel.displayName === `Main ${accountName} channel` ||
257 this.video.channel.name.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/) 260 this.video.channel.name.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)
258 ) { 261 ) {
259 this.ownerDisplayTypeChosen = 'account' 262 this.ownerDisplayType = 'account'
260 } else { 263 } else {
261 this.ownerDisplayTypeChosen = 'videoChannel' 264 this.ownerDisplayType = 'videoChannel'
262 } 265 }
263 } 266 }
264 267
diff --git a/client/src/app/shared/shared-video-miniature/videos-selection.component.html b/client/src/app/shared/shared-video-miniature/videos-selection.component.html
index 8caeaf092..dec9e99f3 100644
--- a/client/src/app/shared/shared-video-miniature/videos-selection.component.html
+++ b/client/src/app/shared/shared-video-miniature/videos-selection.component.html
@@ -9,8 +9,7 @@
9 9
10 <my-video-miniature 10 <my-video-miniature
11 [video]="video" [displayAsRow]="true" [displayOptions]="miniatureDisplayOptions" 11 [video]="video" [displayAsRow]="true" [displayOptions]="miniatureDisplayOptions"
12 [displayVideoActions]="false" [ownerDisplayType]="ownerDisplayType" 12 [displayVideoActions]="false" [user]="user"
13 [user]="user"
14 ></my-video-miniature> 13 ></my-video-miniature>
15 14
16 <!-- Display only once --> 15 <!-- Display only once -->
diff --git a/client/src/app/shared/shared-video-miniature/videos-selection.component.scss b/client/src/app/shared/shared-video-miniature/videos-selection.component.scss
index c33e11889..a2939d521 100644
--- a/client/src/app/shared/shared-video-miniature/videos-selection.component.scss
+++ b/client/src/app/shared/shared-video-miniature/videos-selection.component.scss
@@ -5,24 +5,24 @@
5 display: flex; 5 display: flex;
6 justify-content: flex-end; 6 justify-content: flex-end;
7 flex-grow: 1; 7 flex-grow: 1;
8}
8 9
9 .action-selection-mode-child { 10.action-selection-mode-child {
10 position: fixed; 11 position: fixed;
11
12 .action-button {
13 display: block;
14 margin-left: 55px;
15 }
16 12
17 .action-button-cancel-selection { 13 .action-button {
18 @include peertube-button; 14 display: block;
19 @include grey-button; 15 margin-left: 55px;
20 }
21 } 16 }
22} 17}
23 18
19.action-button-cancel-selection {
20 @include peertube-button;
21 @include grey-button;
22}
23
24.video { 24.video {
25 @include row-blocks; 25 @include row-blocks($column-responsive: false);
26 26
27 &:first-child { 27 &:first-child {
28 margin-top: 47px; 28 margin-top: 47px;
@@ -40,18 +40,16 @@
40 } 40 }
41} 41}
42 42
43@media screen and (max-width: $small-view) {
44 .video {
45 flex-direction: column;
46 height: auto;
47 43
48 .checkbox-container { 44@include on-small-main-col {
49 display: none; 45 .video {
50 } 46 flex-wrap: wrap;
47 }
48}
51 49
52 my-button { 50@include on-mobile-main-col {
53 margin-top: 10px; 51 .checkbox-container {
54 } 52 display: none;
55 } 53 }
56 54
57 .action-selection-mode { 55 .action-selection-mode {
diff --git a/client/src/app/shared/shared-video-miniature/videos-selection.component.ts b/client/src/app/shared/shared-video-miniature/videos-selection.component.ts
index ca1cf2264..f8c3800d7 100644
--- a/client/src/app/shared/shared-video-miniature/videos-selection.component.ts
+++ b/client/src/app/shared/shared-video-miniature/videos-selection.component.ts
@@ -17,7 +17,7 @@ import { AuthService, ComponentPagination, LocalStorageService, Notifier, Screen
17import { ResultList, VideoSortField } from '@shared/models' 17import { ResultList, VideoSortField } from '@shared/models'
18import { PeerTubeTemplateDirective, Video } from '../shared-main' 18import { PeerTubeTemplateDirective, Video } from '../shared-main'
19import { AbstractVideoList } from './abstract-video-list' 19import { AbstractVideoList } from './abstract-video-list'
20import { MiniatureDisplayOptions, OwnerDisplayType } from './video-miniature.component' 20import { MiniatureDisplayOptions } from './video-miniature.component'
21 21
22export type SelectionType = { [ id: number ]: boolean } 22export type SelectionType = { [ id: number ]: boolean }
23 23
@@ -31,7 +31,6 @@ export class VideosSelectionComponent extends AbstractVideoList implements OnIni
31 @Input() pagination: ComponentPagination 31 @Input() pagination: ComponentPagination
32 @Input() titlePage: string 32 @Input() titlePage: string
33 @Input() miniatureDisplayOptions: MiniatureDisplayOptions 33 @Input() miniatureDisplayOptions: MiniatureDisplayOptions
34 @Input() ownerDisplayType: OwnerDisplayType
35 34
36 @Input() getVideosObservableFunction: (page: number, sort?: VideoSortField) => Observable<ResultList<Video>> 35 @Input() getVideosObservableFunction: (page: number, sort?: VideoSortField) => Observable<ResultList<Video>>
37 36
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.html b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.html
index 86f6664cb..f50f95003 100644
--- a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.html
+++ b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.html
@@ -1,4 +1,4 @@
1<div class="miniature" [ngClass]="{ 'no-videos': playlist.videosLength === 0, 'to-manage': toManage }"> 1<div class="miniature" [ngClass]="{ 'no-videos': playlist.videosLength === 0, 'to-manage': toManage, 'display-as-row': displayAsRow }">
2 <a 2 <a
3 [routerLink]="getPlaylistUrl()" [attr.title]="playlist.description" 3 [routerLink]="getPlaylistUrl()" [attr.title]="playlist.description"
4 class="miniature-thumbnail" 4 class="miniature-thumbnail"
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.scss b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.scss
index 1b16dbb01..c5be5f292 100644
--- a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.scss
+++ b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.scss
@@ -4,6 +4,7 @@
4 4
5.miniature { 5.miniature {
6 display: inline-block; 6 display: inline-block;
7 width: 100%;
7 8
8 &.no-videos:not(.to-manage){ 9 &.no-videos:not(.to-manage){
9 a { 10 a {
@@ -17,62 +18,92 @@
17 display: none; 18 display: none;
18 } 19 }
19 } 20 }
21}
20 22
21 .miniature-thumbnail { 23.miniature-thumbnail {
22 @include miniature-thumbnail; 24 @include miniature-thumbnail;
23 25
24 .miniature-playlist-info-overlay { 26 .miniature-playlist-info-overlay {
25 @include static-thumbnail-overlay; 27 @include static-thumbnail-overlay;
26 28
27 position: absolute; 29 position: absolute;
28 right: 0; 30 right: 0;
29 bottom: 0; 31 bottom: 0;
30 height: $video-thumbnail-height; 32 height: 100%;
31 padding: 0 10px; 33 padding: 0 10px;
32 display: flex; 34 display: flex;
33 align-items: center; 35 align-items: center;
34 font-size: 14px; 36 font-size: 14px;
35 font-weight: $font-semibold; 37 font-weight: $font-semibold;
36 }
37 } 38 }
39}
38 40
39 .miniature-info { 41.miniature-info {
40 width: 200px;
41 margin-top: 2px;
42 line-height: normal;
43
44 .miniature-name {
45 @include miniature-name;
46 42
47 @include ellipsis-multiline(1.3em, 2); 43 .miniature-name {
44 @include miniature-name;
45 @include ellipsis-multiline(1.3em, 2);
48 46
49 margin: 0; 47 margin: 0;
50 } 48 }
51 49
52 .by { 50 .by {
53 @include disable-default-a-behaviour; 51 @include disable-default-a-behaviour;
54 52
55 display: block; 53 display: block;
56 color: pvar(--greyForegroundColor); 54 color: pvar(--greyForegroundColor);
57 } 55 }
58 56
59 .privacy-date { 57 .privacy-date {
60 margin-top: 5px; 58 margin-top: 5px;
61 59
62 .video-info-privacy { 60 .video-info-privacy {
63 font-size: 14px; 61 font-size: 14px;
64 font-weight: $font-semibold; 62 font-weight: $font-semibold;
65 63
66 &::after { 64 &::after {
67 content: '-'; 65 content: '-';
68 margin: 0 3px; 66 margin: 0 3px;
69 }
70 } 67 }
71 } 68 }
69 }
72 70
73 .video-info-description { 71 .video-info-description {
74 margin-top: 10px; 72 margin-top: 10px;
75 color: pvar(--greyForegroundColor); 73 color: pvar(--greyForegroundColor);
76 } 74 }
75}
76
77.miniature:not(.display-as-row) {
78 .miniature-thumbnail {
79 margin-top: 10px;
80 margin-bottom: 5px;
81 }
82}
83
84.miniature.display-as-row {
85 --rowThumbnailWidth: #{$video-thumbnail-width};
86 --rowThumbnailHeight: #{$video-thumbnail-height};
87
88 display: flex;
89
90 .miniature-thumbnail {
91 width: var(--rowThumbnailWidth);
92 height: var(--rowThumbnailHeight);
93 margin-right: 10px;
94 }
95}
96
97@include on-small-main-col {
98 .miniature.display-as-row {
99 --rowThumbnailWidth: #{$video-thumbnail-medium-width};
100 --rowThumbnailHeight: #{$video-thumbnail-medium-height};
101 }
102}
103
104@include on-mobile-main-col {
105 .miniature.display-as-row {
106 --rowThumbnailWidth: #{$video-thumbnail-small-width};
107 --rowThumbnailHeight: #{$video-thumbnail-small-height};
77 } 108 }
78} 109}
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts
index 251aa868a..6b0b1056f 100644
--- a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts
+++ b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts
@@ -12,6 +12,7 @@ export class VideoPlaylistMiniatureComponent {
12 @Input() displayChannel = false 12 @Input() displayChannel = false
13 @Input() displayDescription = false 13 @Input() displayDescription = false
14 @Input() displayPrivacy = false 14 @Input() displayPrivacy = false
15 @Input() displayAsRow = false
15 16
16 getPlaylistUrl () { 17 getPlaylistUrl () {
17 if (this.toManage) return [ '/my-library/video-playlists', this.playlist.uuid ] 18 if (this.toManage) return [ '/my-library/video-playlists', this.playlist.uuid ]
diff --git a/client/src/assets/images/feather/cloud-download.svg b/client/src/assets/images/feather/cloud-download.svg
index 3a4e58df1..16526d338 100644
--- a/client/src/assets/images/feather/cloud-download.svg
+++ b/client/src/assets/images/feather/cloud-download.svg
@@ -1,6 +1,6 @@
1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> 1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
2 <defs/> 2 <defs/>
3 <g fill="none" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-width="2"> 3 <g fill="none" fill-rule="evenodd" stroke="currentColor" stroke-linecap="round" stroke-width="2">
4 <path stroke-linejoin="round" d="M8 17H5h0a4 4 0 111-7.9v-.6a5.5 5.5 0 0110.8-1.4A5 5 0 0123 12a5 5 0 01-5 5h-2"/> 4 <path stroke-linejoin="round" d="M8 17H5h0a4 4 0 111-7.9v-.6a5.5 5.5 0 0110.8-1.4A5 5 0 0123 12a5 5 0 01-5 5h-2"/>
5 <path d="M12 13v8"/> 5 <path d="M12 13v8"/>
6 <path stroke-linejoin="round" d="M15 20l-3 3-3-3"/> 6 <path stroke-linejoin="round" d="M15 20l-3 3-3-3"/>
diff --git a/client/src/assets/images/feather/subscriptions.svg b/client/src/assets/images/feather/subscriptions.svg
deleted file mode 100644
index c7216352a..000000000
--- a/client/src/assets/images/feather/subscriptions.svg
+++ /dev/null
@@ -1,19 +0,0 @@
1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
2 <defs/>
3 <defs>
4 <linearGradient id="a" x1="50%" x2="50%" y1="0%" y2="97.33%">
5 <stop stop-color="#000" offset="0%"/>
6 <stop stop-color="#000" offset="100%" stop-opacity=".25"/>
7 </linearGradient>
8 <linearGradient id="b" x1="50%" x2="50%" y1="0%" y2="97.86%">
9 <stop stop-color="#000" offset="0%"/>
10 <stop stop-color="#000" offset="100%" stop-opacity=".25"/>
11 </linearGradient>
12 </defs>
13 <g fill="none" fill-rule="evenodd">
14 <circle cx="12" cy="10" r="3" fill="#000"/>
15 <path fill="url(#a)" fill-rule="nonzero" d="M16.39 13.85A5.68 5.68 0 0018 10c0-3.26-2.74-6-6-6s-6 2.74-6 6c0 1.42.58 2.7 1.62 3.85a.5.5 0 00.74-.67A4.7 4.7 0 017 10c0-2.7 2.3-5 5-5s5 2.3 5 5a4.7 4.7 0 01-1.36 3.18.5.5 0 10.75.67z"/>
16 <path fill="url(#b)" fill-rule="nonzero" d="M17.57 18.3A9.99 9.99 0 0012 0a10 10 0 00-5.56 18.31 1 1 0 101.11-1.66 7.99 7.99 0 118.9 0 1 1 0 101.12 1.66z"/>
17 <path fill="#000" d="M9.33 15.98A1.64 1.64 0 0111 14h2c1.1 0 1.85.88 1.67 1.98l-1 6.04c-.1.54-.61.98-1.17.98h-1c-.55 0-1.07-.43-1.16-.98l-1.01-6.04z"/>
18 </g>
19</svg>
diff --git a/client/src/assets/images/misc/language.svg b/client/src/assets/images/misc/language.svg
index 8fd1d0ba8..204136f0b 100644
--- a/client/src/assets/images/misc/language.svg
+++ b/client/src/assets/images/misc/language.svg
@@ -1,7 +1,7 @@
1<svg xmlns="http://www.w3.org/2000/svg" transform="scale(1.2)" viewBox="0 0 200 200"> 1<svg xmlns="http://www.w3.org/2000/svg" transform="scale(1.2)" viewBox="0 0 200 200">
2 <defs/> 2 <defs/>
3 <path stroke="#000" stroke-width="3" d="M93 155H42a18 18 0 01-18-18V29a5 5 0 015-5h89a5 5 0 015 6L98 151a5 5 0 01-5 4zM34 34v103a8 8 0 008 8h47l22-111z"/> 3 <path stroke="currentColor" stroke-width="3" d="M93 155H42a18 18 0 01-18-18V29a5 5 0 015-5h89a5 5 0 015 6L98 151a5 5 0 01-5 4zM34 34v103a8 8 0 008 8h47l22-111z"/>
4 <path stroke="#000" stroke-width="3" d="M171 176H75a5 5 0 01-5-6l4-21a5 5 0 0110 2l-3 15h85V63a8 8 0 00-8-8h-45a5 5 0 010-10h45a18 18 0 0118 18v108a5 5 0 01-5 5zM50 92h0a5 5 0 01-5-5V63a17 17 0 0135 0v24a5 5 0 01-10 0V62a7 7 0 00-15 0v25a5 5 0 01-5 5z"/> 4 <path stroke="currentColor" stroke-width="3" d="M171 176H75a5 5 0 01-5-6l4-21a5 5 0 0110 2l-3 15h85V63a8 8 0 00-8-8h-45a5 5 0 010-10h45a18 18 0 0118 18v108a5 5 0 01-5 5zM50 92h0a5 5 0 01-5-5V63a17 17 0 0135 0v24a5 5 0 01-10 0V62a7 7 0 00-15 0v25a5 5 0 01-5 5z"/>
5 <path stroke="#000" stroke-width="3" d="M75 76H50a5 5 0 010-10h25a5 5 0 010 10zM120 155a5 5 0 01-3-9l21-21h-18a5 5 0 010-10h30a5 5 0 014 9l-30 30a5 5 0 01-4 1z"/> 5 <path stroke="currentColor" stroke-width="3" d="M75 76H50a5 5 0 010-10h25a5 5 0 010 10zM120 155a5 5 0 01-3-9l21-21h-18a5 5 0 010-10h30a5 5 0 014 9l-30 30a5 5 0 01-4 1z"/>
6 <path stroke="#000" stroke-width="3" d="M150 155a5 5 0 01-4-1l-14-15a5 5 0 017-7l15 14a5 5 0 01-4 9zM143 110h-15a5 5 0 110-10h15a5 5 0 010 10z"/> 6 <path stroke="currentColor" stroke-width="3" d="M150 155a5 5 0 01-4-1l-14-15a5 5 0 017-7l15 14a5 5 0 01-4 9zM143 110h-15a5 5 0 110-10h15a5 5 0 010 10z"/>
7</svg> 7</svg>
diff --git a/client/src/assets/images/misc/npm.svg b/client/src/assets/images/misc/npm.svg
index 1d1d82784..8a4869f12 100644
--- a/client/src/assets/images/misc/npm.svg
+++ b/client/src/assets/images/misc/npm.svg
@@ -1,5 +1,5 @@
1<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="24px" height="24px" viewBox="0 0 18 7" style="transform: scale(1.3) translateY(1px);"> 1<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="24px" height="24px" viewBox="0 0 18 7" style="transform: scale(1.3) translateY(1px);">
2 <path fill="#000" d="M0,0h18v6H9v1H5V6H0V0z M1,5h2V2h1v3h1V1H1V5z M6,1v5h2V5h2V1H6z M8,2h1v2H8V2z M11,1v4h2V2h1v3h1V2h1v3h1V1H11z"/> 2 <path fill="currentColor" d="M0,0h18v6H9v1H5V6H0V0z M1,5h2V2h1v3h1V1H1V5z M6,1v5h2V5h2V1H6z M8,2h1v2H8V2z M11,1v4h2V2h1v3h1V2h1v3h1V1H11z"/>
3 <polygon fill="#FFFFFF" points="1,5 3,5 3,2 4,2 4,5 5,5 5,1 1,1 "/> 3 <polygon fill="#FFFFFF" points="1,5 3,5 3,2 4,2 4,5 5,5 5,1 1,1 "/>
4 <polygon fill="#FFFFFF" d="M6,1v5h2V5h2V1H6z M9,4H8V2h1V4z"/> 4 <polygon fill="#FFFFFF" d="M6,1v5h2V5h2V1H6z M9,4H8V2h1V4z"/>
5 <polygon fill="#FFFFFF" points="11,1 11,5 13,5 13,2 14,2 14,5 15,5 15,2 16,2 16,5 17,5 17,1 "/> 5 <polygon fill="#FFFFFF" points="11,1 11,5 13,5 13,2 14,2 14,5 15,5 15,2 16,2 16,5 17,5 17,1 "/>
diff --git a/client/src/assets/images/misc/peertube-x.svg b/client/src/assets/images/misc/peertube-x.svg
index 0099e627d..30ab665e7 100644
--- a/client/src/assets/images/misc/peertube-x.svg
+++ b/client/src/assets/images/misc/peertube-x.svg
@@ -1,20 +1,17 @@
1<?xml version="1.0" encoding="utf-8"?> 1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
2<!-- Generator: Adobe Illustrator 23.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> 2 <style type="text/css">
3<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" 3 .st0{fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;}
4 viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve"> 4 .st1{fill:currentColor;}
5<style type="text/css">
6 .st0{fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;}
7 .st1{fill:#211F20;}
8</style> 5</style>
9<line class="st0" x1="17.1" y1="9.5" x2="22.1" y2="14.5"/> 6 <line class="st0" x1="17.1" y1="9.5" x2="22.1" y2="14.5" />
10<line class="st0" x1="22.1" y1="9.5" x2="17.1" y2="14.5"/> 7 <line class="st0" x1="22.1" y1="9.5" x2="17.1" y2="14.5" />
11<g>
12 <g> 8 <g>
13 <g> 9 <g>
14 <path class="st1" d="M2,2.6V12l6.9-4.3"/> 10 <g>
15 <path class="st1" d="M2,12v9.4l6.9-5.2"/> 11 <path class="st1" d="M2,2.6V12l6.9-4.3" />
16 <path class="st1" d="M8.9,7.7v8.6l6.9-4.3"/> 12 <path class="st1" d="M2,12v9.4l6.9-5.2" />
13 <path class="st1" d="M8.9,7.7v8.6l6.9-4.3" />
14 </g>
17 </g> 15 </g>
18 </g> 16 </g>
19</g>
20</svg> 17</svg>
diff --git a/client/src/assets/images/misc/playlist-add.svg b/client/src/assets/images/misc/playlist-add.svg
index 7ec77b851..4be495e83 100644
--- a/client/src/assets/images/misc/playlist-add.svg
+++ b/client/src/assets/images/misc/playlist-add.svg
@@ -1,5 +1,5 @@
1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 426.7 426.7"> 1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 426.7 426.7">
2 <defs/> 2 <defs/>
3 <path fill="#000" d="M0 64h256v42.7H0zM0 149.3h256V192H0zM0 234.7h170.7v42.7H0z"/> 3 <path fill="currentColor" d="M0 64h256v42.7H0zM0 149.3h256V192H0zM0 234.7h170.7v42.7H0z"/>
4 <path fill="#000" d="M341.3 234.7v-85.4h-42.6v85.4h-85.4v42.6h85.4v85.4h42.6v-85.4h85.4v-42.6z"/> 4 <path fill="currentColor" d="M341.3 234.7v-85.4h-42.6v85.4h-85.4v42.6h85.4v85.4h42.6v-85.4h85.4v-42.6z"/>
5</svg> 5</svg>
diff --git a/client/src/assets/images/misc/support.svg b/client/src/assets/images/misc/support.svg
index 66280e18d..be3f58c24 100644
--- a/client/src/assets/images/misc/support.svg
+++ b/client/src/assets/images/misc/support.svg
@@ -6,9 +6,9 @@
6 <g transform="translate(2.669496,27.625894)"> 6 <g transform="translate(2.669496,27.625894)">
7 <g transform="matrix(0.1,0,0,-0.1,0,511)"> 7 <g transform="matrix(0.1,0,0,-0.1,0,511)">
8 <path d="m 3744.3542,4564.3712 c -217.4,-34.2 -520.3,-200.3 -693.7,-376.2 -263.8,-263.8 -388.4,-571.6 -388.4,-952.6 0,-256.5 44,-437.2 173.4,-684 75.7,-144.1 197.9,-280.9 747.5,-842.7 1106.5,-1133.40001 1138.2,-1165.20001 1253,-1194.50001 188.1,-51.3 214.9,-29.3 1162.7,938.00001 498.3,508.1 911.1,950.2 962.4,1030.8 263.8,415.3 283.3,964.9 48.8,1409.4 -180.8,342 -581.3,620.4 -972.2,676.6 -332.2,48.9 -671.7,-36.6 -967.3,-236.9 l -156.3,-109.9 -119.7,87.9 c -158.8,117.2 -351.8,202.7 -554.5,244.3 -183.1,39.1 -295.4,41.6 -495.7,9.8 z" 8 <path d="m 3744.3542,4564.3712 c -217.4,-34.2 -520.3,-200.3 -693.7,-376.2 -263.8,-263.8 -388.4,-571.6 -388.4,-952.6 0,-256.5 44,-437.2 173.4,-684 75.7,-144.1 197.9,-280.9 747.5,-842.7 1106.5,-1133.40001 1138.2,-1165.20001 1253,-1194.50001 188.1,-51.3 214.9,-29.3 1162.7,938.00001 498.3,508.1 911.1,950.2 962.4,1030.8 263.8,415.3 283.3,964.9 48.8,1409.4 -180.8,342 -581.3,620.4 -972.2,676.6 -332.2,48.9 -671.7,-36.6 -967.3,-236.9 l -156.3,-109.9 -119.7,87.9 c -158.8,117.2 -351.8,202.7 -554.5,244.3 -183.1,39.1 -295.4,41.6 -495.7,9.8 z"
9 fill="#000"/> 9 fill="currentColor"/>
10 <path d="m 7991.4051,47.633899 c -39.1,-19.5 -473.9,-437.299999 -964.9,-925.800029 l -891.6,-891.59997 h -830.5 c -757.2,0 -837.8,4.9 -913.6,44 -207.6,112.4 -227.2,415.2 -39.1,561.8 66,53.7 83,53.7 950.2,53.7 989.3,0 1008.8,2.5 1094.3,173.49997 56.2,105 56.2,317.50003 4.9,427.50003 -83.1,175.9 4.8,168.5 -1915.1,168.5 h -1722 l -173.4,-63.5 c -95.3,-34.2 -232.1,-102.6 -305.3,-151.5 -73.3,-48.9 -442.1,-400.60003 -823.2,-779.2 l -688.80006,-693.7 664.40006,-647.3 c 366.4,-354.2 779.2,-754.8 918.4,-889.1 l 251.6,-241.8 481.2,481.2 481.2,481.2 h 1487.6 c 1294.6,0 1494.9,4.9 1565.8,39.1 58.6,26.9 339.6,368.8 1028.4,1248.2 522.8,666.89997 964.9,1243.3 982,1284.9 41.5,92.8 2.5,212.499999 -95.3,297.999999 -66,53.7 -95.3,61.1 -273.6,61.1 -132,-0.1 -224.8,-12.3 -273.6,-39.2 z" 10 <path d="m 7991.4051,47.633899 c -39.1,-19.5 -473.9,-437.299999 -964.9,-925.800029 l -891.6,-891.59997 h -830.5 c -757.2,0 -837.8,4.9 -913.6,44 -207.6,112.4 -227.2,415.2 -39.1,561.8 66,53.7 83,53.7 950.2,53.7 989.3,0 1008.8,2.5 1094.3,173.49997 56.2,105 56.2,317.50003 4.9,427.50003 -83.1,175.9 4.8,168.5 -1915.1,168.5 h -1722 l -173.4,-63.5 c -95.3,-34.2 -232.1,-102.6 -305.3,-151.5 -73.3,-48.9 -442.1,-400.60003 -823.2,-779.2 l -688.80006,-693.7 664.40006,-647.3 c 366.4,-354.2 779.2,-754.8 918.4,-889.1 l 251.6,-241.8 481.2,481.2 481.2,481.2 h 1487.6 c 1294.6,0 1494.9,4.9 1565.8,39.1 58.6,26.9 339.6,368.8 1028.4,1248.2 522.8,666.89997 964.9,1243.3 982,1284.9 41.5,92.8 2.5,212.499999 -95.3,297.999999 -66,53.7 -95.3,61.1 -273.6,61.1 -132,-0.1 -224.8,-12.3 -273.6,-39.2 z"
11 fill="#000"/> 11 fill="currentColor"/>
12 </g> 12 </g>
13 </g> 13 </g>
14</svg> 14</svg>
diff --git a/client/src/assets/images/misc/video-lang.svg b/client/src/assets/images/misc/video-lang.svg
index 5ffed18da..6bcaeb9be 100644
--- a/client/src/assets/images/misc/video-lang.svg
+++ b/client/src/assets/images/misc/video-lang.svg
@@ -1,7 +1,7 @@
1<svg xmlns="http://www.w3.org/2000/svg" transform="scale(1.1)" viewBox="0 0 24 24"> 1<svg xmlns="http://www.w3.org/2000/svg" transform="scale(1.1)" viewBox="0 0 24 24">
2 <defs/> 2 <defs/>
3 <g class="layer"> 3 <g class="layer">
4 <path fill="#fff" fill-rule="evenodd" stroke="#000" stroke-width="1.8" d="M20.5 6.7s-.2-1.4-.8-2c-.7-.8-1.6-.8-2-.9-2.7-.2-6.9-.2-6.9-.2h0s-4.2 0-7 .2c-.3 0-1.2 0-2 .9-.5.6-.7 2-.7 2L.9 10v1.6l.2 3.3s.2 1.4.8 2c.7.8 1.7.8 2.2.9 1.6.2 6.7.2 6.7.2s4.2 0 7-.2c.3 0 1.2 0 2-.9.5-.6.7-2 .7-2l.2-3.3V10l-.2-3.3h0z"/> 4 <path fill="#fff" fill-rule="evenodd" stroke="currentColor" stroke-width="1.8" d="M20.5 6.7s-.2-1.4-.8-2c-.7-.8-1.6-.8-2-.9-2.7-.2-6.9-.2-6.9-.2h0s-4.2 0-7 .2c-.3 0-1.2 0-2 .9-.5.6-.7 2-.7 2L.9 10v1.6l.2 3.3s.2 1.4.8 2c.7.8 1.7.8 2.2.9 1.6.2 6.7.2 6.7.2s4.2 0 7-.2c.3 0 1.2 0 2-.9.5-.6.7-2 .7-2l.2-3.3V10l-.2-3.3h0z"/>
5 <path d="M8.7 14.7a.7.7 0 01-.5-1.2l2.9-3H8.7a.7.7 0 010-1.3h4a.7.7 0 01.5 1.2l-4 4a.7.7 0 01-.5.3zM11.7 8.6h-2a.7.7 0 110-1.4h2a.7.7 0 010 1.4z"/> 5 <path d="M8.7 14.7a.7.7 0 01-.5-1.2l2.9-3H8.7a.7.7 0 010-1.3h4a.7.7 0 01.5 1.2l-4 4a.7.7 0 01-.5.3zM11.7 8.6h-2a.7.7 0 110-1.4h2a.7.7 0 010 1.4z"/>
6 </g> 6 </g>
7</svg> 7</svg>
diff --git a/client/src/assets/player/peertube-player-local-storage.ts b/client/src/assets/player/peertube-player-local-storage.ts
index 75ccfe618..cf2cfb472 100644
--- a/client/src/assets/player/peertube-player-local-storage.ts
+++ b/client/src/assets/player/peertube-player-local-storage.ts
@@ -68,6 +68,51 @@ function getStoredLastSubtitle () {
68 return getLocalStorage('last-subtitle') 68 return getLocalStorage('last-subtitle')
69} 69}
70 70
71function saveVideoWatchHistory(videoUUID: string, duration: number) {
72 return setLocalStorage(`video-watch-history`, JSON.stringify({
73 ...getStoredVideoWatchHistory(),
74 [videoUUID]: {
75 duration,
76 date: `${(new Date()).toISOString()}`
77 }
78 }))
79}
80
81function getStoredVideoWatchHistory(videoUUID?: string) {
82 let data
83
84 try {
85 data = JSON.parse(getLocalStorage('video-watch-history'))
86 } catch (error) {
87 console.error('Cannot parse video watch history from local storage: ', error)
88 }
89
90 data = data || {}
91
92 if (videoUUID) return data[videoUUID]
93
94 return data
95}
96
97function cleanupVideoWatch() {
98 const data = getStoredVideoWatchHistory()
99
100 const newData = Object.keys(data).reduce((acc, videoUUID) => {
101 const date = Date.parse(data[videoUUID].date)
102
103 const diff = Math.ceil(((new Date()).getTime() - date) / (1000 * 3600 * 24))
104
105 if (diff > 30) return acc
106
107 return {
108 ...acc,
109 [videoUUID]: data[videoUUID]
110 }
111 }, {})
112
113 setLocalStorage('video-watch-history', JSON.stringify(newData))
114}
115
71// --------------------------------------------------------------------------- 116// ---------------------------------------------------------------------------
72 117
73export { 118export {
@@ -81,7 +126,10 @@ export {
81 saveAverageBandwidth, 126 saveAverageBandwidth,
82 getAverageBandwidthInStore, 127 getAverageBandwidthInStore,
83 saveLastSubtitle, 128 saveLastSubtitle,
84 getStoredLastSubtitle 129 getStoredLastSubtitle,
130 saveVideoWatchHistory,
131 getStoredVideoWatchHistory,
132 cleanupVideoWatch
85} 133}
86 134
87// --------------------------------------------------------------------------- 135// ---------------------------------------------------------------------------
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts
index 2cbef48ea..119dec379 100644
--- a/client/src/assets/player/peertube-player-manager.ts
+++ b/client/src/assets/player/peertube-player-manager.ts
@@ -98,6 +98,7 @@ export interface CommonOptions extends CustomizationOptions {
98 98
99 videoViewUrl: string 99 videoViewUrl: string
100 embedUrl: string 100 embedUrl: string
101 embedTitle: string
101 102
102 isLive: boolean 103 isLive: boolean
103 104
@@ -105,6 +106,8 @@ export interface CommonOptions extends CustomizationOptions {
105 106
106 videoCaptions: VideoJSCaption[] 107 videoCaptions: VideoJSCaption[]
107 108
109 videoUUID: string
110
108 userWatching?: UserWatching 111 userWatching?: UserWatching
109 112
110 serverUrl: string 113 serverUrl: string
@@ -165,7 +168,7 @@ export class PeertubePlayerManager {
165 PeertubePlayerManager.alreadyPlayed = true 168 PeertubePlayerManager.alreadyPlayed = true
166 }) 169 })
167 170
168 self.addContextMenu(mode, player, options.common.embedUrl) 171 self.addContextMenu(mode, player, options.common.embedUrl, options.common.embedTitle)
169 172
170 player.bezels() 173 player.bezels()
171 174
@@ -203,7 +206,7 @@ export class PeertubePlayerManager {
203 videojs(newVideoElement, videojsOptions, function (this: videojs.Player) { 206 videojs(newVideoElement, videojsOptions, function (this: videojs.Player) {
204 const player = this 207 const player = this
205 208
206 self.addContextMenu(mode, player, options.common.embedUrl) 209 self.addContextMenu(mode, player, options.common.embedUrl, options.common.embedTitle)
207 210
208 PeertubePlayerManager.onPlayerChange(player) 211 PeertubePlayerManager.onPlayerChange(player)
209 }) 212 })
@@ -230,7 +233,8 @@ export class PeertubePlayerManager {
230 subtitle: commonOptions.subtitle, 233 subtitle: commonOptions.subtitle,
231 videoCaptions: commonOptions.videoCaptions, 234 videoCaptions: commonOptions.videoCaptions,
232 stopTime: commonOptions.stopTime, 235 stopTime: commonOptions.stopTime,
233 isLive: commonOptions.isLive 236 isLive: commonOptions.isLive,
237 videoUUID: commonOptions.videoUUID
234 } 238 }
235 } 239 }
236 240
@@ -271,7 +275,7 @@ export class PeertubePlayerManager {
271 275
272 poster: commonOptions.poster, 276 poster: commonOptions.poster,
273 inactivityTimeout: commonOptions.inactivityTimeout, 277 inactivityTimeout: commonOptions.inactivityTimeout,
274 playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 2 ], 278 playbackRates: [ 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2 ],
275 279
276 plugins, 280 plugins,
277 281
@@ -492,7 +496,7 @@ export class PeertubePlayerManager {
492 return children 496 return children
493 } 497 }
494 498
495 private static addContextMenu (mode: PlayerMode, player: videojs.Player, videoEmbedUrl: string) { 499 private static addContextMenu (mode: PlayerMode, player: videojs.Player, videoEmbedUrl: string, videoEmbedTitle: string) {
496 const content = [ 500 const content = [
497 { 501 {
498 label: player.localize('Copy the video URL'), 502 label: player.localize('Copy the video URL'),
@@ -509,7 +513,7 @@ export class PeertubePlayerManager {
509 { 513 {
510 label: player.localize('Copy embed code'), 514 label: player.localize('Copy embed code'),
511 listener: () => { 515 listener: () => {
512 copyToClipboard(buildVideoOrPlaylistEmbed(videoEmbedUrl)) 516 copyToClipboard(buildVideoOrPlaylistEmbed(videoEmbedUrl, videoEmbedTitle))
513 } 517 }
514 } 518 }
515 ] 519 ]
diff --git a/client/src/assets/player/peertube-plugin.ts b/client/src/assets/player/peertube-plugin.ts
index 75a6e662e..07c7e33f6 100644
--- a/client/src/assets/player/peertube-plugin.ts
+++ b/client/src/assets/player/peertube-plugin.ts
@@ -13,6 +13,7 @@ import {
13 getStoredVolume, 13 getStoredVolume,
14 saveLastSubtitle, 14 saveLastSubtitle,
15 saveMuteInStore, 15 saveMuteInStore,
16 saveVideoWatchHistory,
16 saveVolumeInStore 17 saveVolumeInStore
17} from './peertube-player-local-storage' 18} from './peertube-player-local-storage'
18 19
@@ -120,7 +121,7 @@ class PeerTubePlugin extends Plugin {
120 this.initializePlayer() 121 this.initializePlayer()
121 this.runViewAdd() 122 this.runViewAdd()
122 123
123 if (options.userWatching) this.runUserWatchVideo(options.userWatching) 124 this.runUserWatchVideo(options.userWatching, options.videoUUID)
124 }) 125 })
125 } 126 }
126 127
@@ -178,7 +179,7 @@ class PeerTubePlugin extends Plugin {
178 }, 1000) 179 }, 1000)
179 } 180 }
180 181
181 private runUserWatchVideo (options: UserWatching) { 182 private runUserWatchVideo (options: UserWatching, videoUUID: string) {
182 let lastCurrentTime = 0 183 let lastCurrentTime = 0
183 184
184 this.userWatchingVideoInterval = setInterval(() => { 185 this.userWatchingVideoInterval = setInterval(() => {
@@ -187,8 +188,12 @@ class PeerTubePlugin extends Plugin {
187 if (currentTime - lastCurrentTime >= 1) { 188 if (currentTime - lastCurrentTime >= 1) {
188 lastCurrentTime = currentTime 189 lastCurrentTime = currentTime
189 190
190 this.notifyUserIsWatching(currentTime, options.url, options.authorizationHeader) 191 if (options) {
191 .catch(err => console.error('Cannot notify user is watching.', err)) 192 this.notifyUserIsWatching(currentTime, options.url, options.authorizationHeader)
193 .catch(err => console.error('Cannot notify user is watching.', err))
194 } else {
195 saveVideoWatchHistory(videoUUID, currentTime)
196 }
192 } 197 }
193 }, this.CONSTANTS.USER_WATCHING_VIDEO_INTERVAL) 198 }, this.CONSTANTS.USER_WATCHING_VIDEO_INTERVAL)
194 } 199 }
diff --git a/client/src/assets/player/peertube-videojs-typings.ts b/client/src/assets/player/peertube-videojs-typings.ts
index e5259092c..4a6c80247 100644
--- a/client/src/assets/player/peertube-videojs-typings.ts
+++ b/client/src/assets/player/peertube-videojs-typings.ts
@@ -108,6 +108,8 @@ type PeerTubePluginOptions = {
108 stopTime: number | string 108 stopTime: number | string
109 109
110 isLive: boolean 110 isLive: boolean
111
112 videoUUID: string
111} 113}
112 114
113type PlaylistPluginOptions = { 115type PlaylistPluginOptions = {
diff --git a/client/src/assets/player/utils.ts b/client/src/assets/player/utils.ts
index 6767459ce..d7451fa1d 100644
--- a/client/src/assets/player/utils.ts
+++ b/client/src/assets/player/utils.ts
@@ -1,4 +1,5 @@
1import { VideoFile } from '@shared/models' 1import { VideoFile } from '@shared/models'
2import { escapeHTML } from '@shared/core-utils/renderer'
2 3
3function toTitleCase (str: string) { 4function toTitleCase (str: string) {
4 return str.charAt(0).toUpperCase() + str.slice(1) 5 return str.charAt(0).toUpperCase() + str.slice(1)
@@ -170,9 +171,11 @@ function secondsToTime (seconds: number, full = false, symbol?: string) {
170 return time 171 return time
171} 172}
172 173
173function buildVideoOrPlaylistEmbed (embedUrl: string) { 174function buildVideoOrPlaylistEmbed (embedUrl: string, embedTitle: string) {
175 const title = escapeHTML(embedTitle)
174 return '<iframe width="560" height="315" ' + 176 return '<iframe width="560" height="315" ' +
175 'sandbox="allow-same-origin allow-scripts allow-popups" ' + 177 'sandbox="allow-same-origin allow-scripts allow-popups" ' +
178 'title="' + title + '" ' +
176 'src="' + embedUrl + '" ' + 179 'src="' + embedUrl + '" ' +
177 'frameborder="0" allowfullscreen>' + 180 'frameborder="0" allowfullscreen>' +
178 '</iframe>' 181 '</iframe>'
diff --git a/client/src/sass/application.scss b/client/src/sass/application.scss
index a0009eecc..c01938147 100644
--- a/client/src/sass/application.scss
+++ b/client/src/sass/application.scss
@@ -15,6 +15,8 @@ $assets-path: '../../assets/';
15@import './primeng-custom'; 15@import './primeng-custom';
16@import './ng-select.scss'; 16@import './ng-select.scss';
17 17
18@import './classes.scss';
19
18[hidden] { 20[hidden] {
19 display: none !important; 21 display: none !important;
20} 22}
@@ -36,7 +38,9 @@ body {
36 38
37 --menuBackgroundColor: #{$menu-background}; 39 --menuBackgroundColor: #{$menu-background};
38 --menuForegroundColor: #{$menu-color}; 40 --menuForegroundColor: #{$menu-color};
39 --submenuColor: #{$sub-menu-color}; 41
42 --submenuBackgroundColor: #{$sub-menu-background-color};
43 --channelBackgroundColor: #{$channel-background-color};
40 44
41 --inputForegroundColor: #{$input-foreground-color}; 45 --inputForegroundColor: #{$input-foreground-color};
42 --inputBackgroundColor: #{$input-background-color}; 46 --inputBackgroundColor: #{$input-background-color};
@@ -53,7 +57,9 @@ body {
53 57
54 --activatedActionButtonColor: #{$activated-action-button-color}; 58 --activatedActionButtonColor: #{$activated-action-button-color};
55 59
56 --expanded-horizontal-margin-content: #{$expanded-horizontal-margins}; 60 --horizontalMarginContent: #{$not-expanded-horizontal-margins};
61 --videosHorizontalMarginContent: 6vw;
62 --mainColWidth: calc(100vw - #{$menu-width});
57 63
58 font-family: $main-fonts; 64 font-family: $main-fonts;
59 font-weight: $font-regular; 65 font-weight: $font-regular;
@@ -146,24 +152,26 @@ my-input-toggle-hidden ::ng-deep input {
146 outline: none; 152 outline: none;
147 153
148 .margin-content { 154 .margin-content {
149 margin-left: $not-expanded-horizontal-margins; 155 margin-left: pvar(--horizontalMarginContent);
150 margin-right: $not-expanded-horizontal-margins; 156 margin-right: pvar(--horizontalMarginContent);
151 flex-grow: 1; 157 flex-grow: 1;
152 } 158 }
153 159
154 .sub-menu { 160 .sub-menu {
155 background-color: pvar(--submenuColor); 161 background-color: pvar(--submenuBackgroundColor);
156 width: 100%; 162 width: 100%;
157 display: flex; 163 display: flex;
158 align-items: center; 164 align-items: center;
159 padding-left: $not-expanded-horizontal-margins; 165 padding-left: pvar(--horizontalMarginContent);
160 padding-right: $not-expanded-horizontal-margins; 166 padding-right: pvar(--horizontalMarginContent);
161 height: $sub-menu-height; 167 height: $sub-menu-height;
162 margin-bottom: $sub-menu-margin-bottom; 168 margin-bottom: $sub-menu-margin-bottom;
169 overflow-x: auto;
163 170
164 &.sub-menu-fixed { 171 &.sub-menu-fixed {
165 position: fixed; 172 position: fixed;
166 z-index: #{z('sub-menu') - 1}; 173 z-index: #{z('sub-menu') - 1};
174 max-width: pvar(--mainColWidth);
167 } 175 }
168 } 176 }
169 177
@@ -174,18 +182,11 @@ my-input-toggle-hidden ::ng-deep input {
174 182
175 // Override some properties if the main content is expanded (no menu on the left) 183 // Override some properties if the main content is expanded (no menu on the left)
176 &.expanded { 184 &.expanded {
185 --horizontalMarginContent: #{$expanded-horizontal-margins};
186 --mainColWidth: 100vw;
187
177 margin-left: 0; 188 margin-left: 0;
178 width: 100%; 189 width: 100%;
179
180 .margin-content {
181 margin-left: var(--expanded-horizontal-margin-content);
182 margin-right: var(--expanded-horizontal-margin-content);
183 }
184
185 .sub-menu {
186 padding-left: var(--expanded-horizontal-margin-content);
187 padding-right: var(--expanded-horizontal-margin-content);
188 }
189 } 190 }
190 191
191 &.lock-scroll .main-row > router-outlet + * { 192 &.lock-scroll .main-row > router-outlet + * {
@@ -263,7 +264,7 @@ my-input-toggle-hidden ::ng-deep input {
263 opacity: 0.6; 264 opacity: 0.6;
264 265
265 &.active { 266 &.active {
266 background-color: pvar(--submenuColor); 267 background-color: pvar(--submenuBackgroundColor);
267 } 268 }
268 269
269 &.active, &:hover, &:active, &:focus { 270 &.active, &:hover, &:active, &:focus {
@@ -277,11 +278,6 @@ my-input-toggle-hidden ::ng-deep input {
277 font-weight: bold; 278 font-weight: bold;
278} 279}
279 280
280@keyframes spin {
281 from { transform: scale(1) rotate(0deg);}
282 to { transform: scale(1) rotate(360deg);}
283}
284
285// In tables, don't have a hover different background 281// In tables, don't have a hover different background
286table { 282table {
287 .action-button-edit, .action-button-delete { 283 .action-button-edit, .action-button-delete {
@@ -338,29 +334,34 @@ ngx-loading-bar {
338 334
339@media screen and (max-width: #{breakpoint(xxl)}) { 335@media screen and (max-width: #{breakpoint(xxl)}) {
340 .main-col { 336 .main-col {
337 & {
338 --horizontalMarginContent: #{$not-expanded-horizontal-margins / 2};
339 }
340
341 &.expanded { 341 &.expanded {
342 .margin-content { 342 --horizontalMarginContent: #{$expanded-horizontal-margins / 2};
343 --expanded-horizontal-margin-content: #{$expanded-horizontal-margins/2};
344 }
345 } 343 }
344
345 --videosHorizontalMarginContent: 30px;
346 } 346 }
347} 347}
348 348
349@media screen and (max-width: #{breakpoint(lg)}) { 349@media screen and (max-width: #{breakpoint(lg)}) {
350 /* the following applies from 500px to 900px and is partially overriden from 500px to 800px by changes below to $small-view */
351 .main-col { 350 .main-col {
352 &, &.expanded { 351 --videosHorizontalMarginContent: #{pvar(--horizontalMarginContent)};
353 .margin-content { 352 }
354 --expanded-horizontal-margin-content: #{$expanded-horizontal-margins/3}; 353
355 } 354 /* the following applies from 500px to 900px and is partially overriden from 500px to 800px by changes below to $small-view */
355 .main-col,
356 .main-col.expanded {
357 --horizontalMarginContent: #{$expanded-horizontal-margins / 3};
356 358
357 .sub-menu { 359 .sub-menu {
358 padding-left: 50px; 360 padding-left: 50px;
359 padding-right: 50px; 361 padding-right: 50px;
360 362
361 .title-page { 363 .title-page {
362 font-size: 17px; 364 font-size: 17px;
363 }
364 } 365 }
365 } 366 }
366 } 367 }
@@ -373,98 +374,46 @@ ngx-loading-bar {
373} 374}
374 375
375@media screen and (max-width: $small-view) { 376@media screen and (max-width: $small-view) {
376 .main-col { 377 .main-col,
377 margin-left: 0; 378 .main-col.expanded {
378 379 --horizontalMarginContent: 15px;
379 &, &.expanded {
380 .margin-content {
381 --expanded-horizontal-margin-content: 15px;
382 }
383
384 .sub-menu {
385 width: 100vw;
386 padding-left: 15px;
387 padding-right: 15px;
388 margin-bottom: $sub-menu-margin-bottom-small-view;
389 overflow-x: auto;
390 }
391
392 // Use an appropriate offset top when sub-menu fixed
393 .margin-content.offset-content {
394 padding-top: $sub-menu-height + $sub-menu-margin-bottom-small-view;
395 }
396
397 .admin-sub-header {
398 @include admin-sub-header-responsive(15px*2);
399 }
400 380
401 my-markdown-textarea { 381 margin-left: 0;
402 .root {
403 max-width: 100% !important;
404 }
405 }
406
407 input[type=text],
408 input[type=password],
409 input[type=email],
410 textarea,
411 .peertube-select-container {
412 flex-grow: 1;
413 }
414 382
415 .caption input[type=text] { 383 .sub-menu {
416 width: unset !important; 384 width: 100vw;
417 flex-grow: 1; 385 padding-left: 15px;
418 } 386 padding-right: 15px;
387 margin-bottom: $sub-menu-margin-bottom-small-view;
388 overflow-x: auto;
419 } 389 }
420 }
421}
422 390
423// overflow-databale responsive rules 391 // Use an appropriate offset top when sub-menu fixed
424@media screen and (min-width: #{breakpoint(lg)}) { 392 .margin-content.offset-content {
425 .main-col { 393 padding-top: $sub-menu-height + $sub-menu-margin-bottom-small-view;
426 &.expanded {
427 @include overflow-datatable(breakpoint(lg), $expanded-horizontal-margins/2, $mobile-paginator: false);
428 } 394 }
429 395
430 &:not(.expanded) { 396 .admin-sub-header {
431 @include overflow-datatable(breakpoint(lg), $not-expanded-horizontal-margins + $menu-width/2, $mobile-paginator: false); 397 @include admin-sub-header-responsive;
432 } 398 }
433 }
434}
435 399
436@media screen and (max-width: #{breakpoint(lg)}) { 400 my-markdown-textarea {
437 .main-col { 401 .root {
438 &.expanded { 402 max-width: 100% !important;
439 @include overflow-datatable(breakpoint(lg), $expanded-horizontal-margins/3); 403 }
440 } 404 }
441 405
442 &:not(.expanded) { 406 input[type=text],
443 @include overflow-datatable(breakpoint(lg), $expanded-horizontal-margins/3 + $menu-width/2); 407 input[type=password],
444 } 408 input[type=email],
445 } 409 textarea,
446} 410 .peertube-select-container {
447 411 flex-grow: 1;
448@media screen and (max-width: $small-view) {
449 .main-col {
450 &:not(.expanded),
451 &.expanded {
452 @include overflow-datatable(breakpoint(lg), 15px);
453 } 412 }
454 }
455}
456 413
457@media screen and (min-width: $small-view) and (max-width: #{$small-view + $menu-width}) { 414 .caption input[type=text] {
458 .main-col { 415 width: unset !important;
459 &:not(.expanded) { 416 flex-grow: 1;
460 .admin-sub-header {
461 @include admin-sub-header-responsive($expanded-horizontal-margins/3 + $menu-width/2);
462 }
463
464 .sub-menu {
465 overflow-x: auto;
466 width: calc(100vw - #{$menu-width});
467 }
468 } 417 }
469 } 418 }
470} 419}
diff --git a/client/src/sass/bootstrap.scss b/client/src/sass/bootstrap.scss
index 7047f6e03..75dc91d7a 100644
--- a/client/src/sass/bootstrap.scss
+++ b/client/src/sass/bootstrap.scss
@@ -9,6 +9,10 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
9 animation: spin .7s infinite linear; 9 animation: spin .7s infinite linear;
10} 10}
11 11
12.glyphicon-duplicate {
13 font-size: 70%;
14}
15
12.flex-auto { 16.flex-auto {
13 flex: auto; 17 flex: auto;
14} 18}
diff --git a/client/src/sass/classes.scss b/client/src/sass/classes.scss
new file mode 100644
index 000000000..af8e39573
--- /dev/null
+++ b/client/src/sass/classes.scss
@@ -0,0 +1,22 @@
1@import '_variables';
2@import '_mixins';
3
4.peertube-button {
5 @include peertube-button;
6}
7
8.peertube-button-link {
9 @include peertube-button-link;
10}
11
12.orange-button {
13 @include orange-button;
14}
15
16.orange-button-inverted {
17 @include orange-button-inverted;
18}
19
20.grey-button {
21 @include grey-button;
22}
diff --git a/client/src/sass/include/_actor.scss b/client/src/sass/include/_actor.scss
new file mode 100644
index 000000000..8d82a042c
--- /dev/null
+++ b/client/src/sass/include/_actor.scss
@@ -0,0 +1,92 @@
1@import '_variables';
2
3@mixin section-label-responsive {
4 color: pvar(--mainColor);
5 font-size: 12px;
6 margin-bottom: 15px;
7 font-weight: $font-bold;
8 letter-spacing: 2.5px;
9
10 @media screen and (max-width: $mobile-view) {
11 font-size: 10px;
12 letter-spacing: 2.1px;
13 margin-bottom: 5px;
14 }
15}
16
17@mixin show-more-description {
18 color: pvar(--mainColor);
19 cursor: pointer;
20 margin: 10px auto 45px auto;
21}
22
23@mixin avatar-row-responsive ($img-margin, $grey-font-size) {
24 display: flex;
25 grid-column: 1;
26 margin-bottom: 30px;
27
28 .channel-avatar {
29 @include channel-avatar(120px);
30 }
31
32 .account-avatar {
33 @include avatar(120px);
34 }
35
36 > div {
37 margin-left: $img-margin;
38 min-width: 1px;
39 }
40
41 .actor-info {
42 display: flex;
43
44 > div:first-child {
45 flex-grow: 1;
46 min-width: 1px;
47 }
48 }
49
50 .actor-display-name {
51 display: flex;
52 flex-wrap: wrap;
53 }
54
55 h1 {
56 font-size: 28px;
57 font-weight: $font-bold;
58 margin: 0;
59 }
60
61 .actor-handle {
62 @include ellipsis;
63 }
64
65 .actor-handle,
66 .actor-counters {
67 color: pvar(--greyForegroundColor);
68 font-size: $grey-font-size;
69 }
70
71 .actor-counters > *:not(:last-child)::after {
72 content: '•';
73 margin: 0 10px;
74 color: pvar(--mainColor);
75 }
76
77 @media screen and (max-width: $mobile-view) {
78 margin-bottom: 15px;
79
80 h1 {
81 font-size: 22px;
82 }
83
84 .channel-avatar {
85 @include channel-avatar(80px);
86 }
87
88 .account-avatar {
89 @include avatar(120px);
90 }
91 }
92}
diff --git a/client/src/sass/include/_miniature.scss b/client/src/sass/include/_miniature.scss
index 134b307b1..3b86f29b4 100644
--- a/client/src/sass/include/_miniature.scss
+++ b/client/src/sass/include/_miniature.scss
@@ -4,11 +4,11 @@
4@mixin miniature-name { 4@mixin miniature-name {
5 @include ellipsis-multiline(1.1em, 2); 5 @include ellipsis-multiline(1.1em, 2);
6 6
7 word-break: break-all;
8 word-wrap: break-word;
7 transition: color 0.2s; 9 transition: color 0.2s;
8 font-weight: $font-semibold; 10 font-weight: $font-semibold;
9 color: pvar(--mainForegroundColor); 11 color: pvar(--mainForegroundColor);
10 margin-top: 10px;
11 margin-bottom: 5px;
12 12
13 &:hover { 13 &:hover {
14 text-decoration: none; 14 text-decoration: none;
@@ -20,20 +20,20 @@
20 } 20 }
21} 21}
22 22
23$play-overlay-transition: 0.2s ease;
24$play-overlay-height: 26px;
25$play-overlay-width: 18px;
26
27@mixin miniature-thumbnail { 23@mixin miniature-thumbnail {
28 @include disable-outline; 24 @include disable-outline;
29 25
26 $play-overlay-transition: 0.2s ease;
27 $play-overlay-height: 26px;
28 $play-overlay-width: 18px;
29
30 display: flex; 30 display: flex;
31 flex-direction: column; 31 flex-direction: column;
32 position: relative; 32 position: relative;
33 border-radius: 3px; 33 border-radius: 3px;
34 width: 100%;
35 height: 100%;
34 overflow: hidden; 36 overflow: hidden;
35 width: $video-thumbnail-width;
36 height: $video-thumbnail-height;
37 background-color: #ececec; 37 background-color: #ececec;
38 transition: filter $play-overlay-transition; 38 transition: filter $play-overlay-transition;
39 39
@@ -97,154 +97,64 @@ $play-overlay-width: 18px;
97 color: #fff; 97 color: #fff;
98} 98}
99 99
100@mixin miniature-rows { 100// Use margin by default, or padding if $margin is false
101 &:first-child { 101@mixin grid-videos-miniature-margins ($margin: true, $min-margin: 0) {
102 padding-top: 30px; 102 --gridVideosMiniatureMargins: #{pvar(--videosHorizontalMarginContent)};
103 103
104 .section-title { 104 @if $margin {
105 border-top: none !important; 105 margin-left: var(--gridVideosMiniatureMargins) !important;
106 } 106 margin-right: var(--gridVideosMiniatureMargins) !important;
107 } 107 } @else {
108 108 padding-left: var(--gridVideosMiniatureMargins) !important;
109 .section-title { 109 padding-right: var(--gridVideosMiniatureMargins) !important;
110 font-size: 24px;
111 font-weight: $font-semibold;
112 padding-top: 15px;
113 margin-bottom: 15px;
114 display: flex;
115 justify-content: space-between;
116
117 &:not(h2) {
118 border-top: 1px solid $separator-border-color;
119 }
120
121 a {
122 &:hover, &:focus:not(.focus-visible), &:active {
123 text-decoration: none;
124 outline: none;
125 }
126
127 color: pvar(--mainForegroundColor);
128 }
129 }
130
131 &.channel {
132 .section-title {
133 a {
134 display: flex;
135 width: fit-content;
136 align-items: center;
137
138 img {
139 @include avatar(28px);
140
141 margin-right: 8px;
142 }
143 }
144
145 .followers {
146 color: pvar(--greyForegroundColor);
147 font-weight: normal;
148 font-size: 14px;
149 margin-left: 10px;
150 position: relative;
151 top: 2px;
152 }
153 }
154 }
155
156 .show-more {
157 position: relative;
158 top: -5px;
159 display: inline-block;
160 font-size: 16px;
161 text-transform: uppercase;
162 color: pvar(--greyForegroundColor);
163 margin-bottom: 10px;
164 font-weight: $font-semibold;
165 text-decoration: none;
166 } 110 }
167 111
168 @media screen and (max-width: $mobile-view) { 112 @media screen and (max-width: $mobile-view) {
169 max-height: initial; 113 --gridVideosMiniatureMargins: #{$min-margin};
170 overflow: initial;
171
172 .section-title {
173 font-size: 17px;
174 margin-left: 10px;
175 }
176 }
177}
178 114
179@mixin fluid-videos-miniature-layout {
180 margin-left: $not-expanded-horizontal-margins !important;
181 margin-right: $not-expanded-horizontal-margins !important;
182
183 @media screen and (max-width: $mobile-view) {
184 width: auto; 115 width: auto;
185 margin: 0 !important;
186
187 .videos {
188 text-align: center;
189
190 ::ng-deep .video-miniature {
191 padding-right: 0;
192 height: auto;
193 width: 100%;
194 margin-bottom: 25px;
195
196 .video-miniature-information {
197 width: 100% !important;
198 text-align: left;
199
200 span {
201 width: 100%;
202 }
203 }
204
205 .video-thumbnail {
206 border-radius: 0;
207 }
208 }
209 }
210 } 116 }
117}
211 118
212 @media screen and (min-width: #{breakpoint(fhd)}) { 119@mixin grid-videos-miniature-layout {
213 margin-left: 6vw !important; 120 @include grid-videos-miniature-margins;
214 margin-right: 6vw !important;
215 }
216 121
217 @media screen and (min-width: $mobile-view) { 122 @media screen and (min-width: $mobile-view) {
218 123 .videos,
219 .videos { 124 .playlists {
220 --miniature-min-width: #{$video-thumbnail-width - 15px}; 125 --miniatureMinWidth: #{$video-thumbnail-width - 25px};
221 --miniature-max-width: #{$video-thumbnail-width}; 126 --miniatureMaxWidth: #{$video-thumbnail-width};
222 127
223 display: grid; 128 display: grid;
224 column-gap: 5px; 129 column-gap: 30px;
225 grid-template-columns: repeat( 130 grid-template-columns: repeat(
226 auto-fill, 131 auto-fill,
227 minmax( 132 minmax(
228 var(--miniature-min-width), 133 var(--miniatureMinWidth),
229 1fr 134 1fr
230 ) 135 )
231 ); 136 );
232 137
233 @media screen and (min-width: #{breakpoint(fhd)}) { 138 .video-wrapper,
234 column-gap: 1%; 139 .playlist-wrapper {
235 --miniature-min-width: #{$video-thumbnail-width};
236 }
237
238 .video-wrapper {
239 margin: 0 auto; 140 margin: 0 auto;
240 width: 100%; 141 width: 100%;
241 142
242 my-video-miniature { 143 my-video-miniature,
144 my-video-playlist-miniature {
243 display: block; 145 display: block;
244 min-width: var(--miniature-min-width); 146 min-width: var(--miniatureMinWidth);
245 max-width: var(--miniature-max-width); 147 max-width: var(--miniatureMaxWidth);
246 } 148 }
247 } 149 }
150
151 @media screen and (min-width: #{breakpoint(xm)}) {
152 column-gap: 15px;
153 }
154
155 @media screen and (min-width: #{breakpoint(fhd)}) {
156 column-gap: 2%;
157 }
248 } 158 }
249 } 159 }
250} 160}
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss
index ca11488cb..bf844ac5d 100644
--- a/client/src/sass/include/_mixins.scss
+++ b/client/src/sass/include/_mixins.scss
@@ -23,17 +23,28 @@
23 display: block; 23 display: block;
24 /* Fallback for non-webkit */ 24 /* Fallback for non-webkit */
25 display: -webkit-box; 25 display: -webkit-box;
26 max-height: $font-size * $number-of-lines; 26 -webkit-line-clamp: $number-of-lines;
27 /* Fallback for non-webkit */ 27 /* Fallback for non-webkit */
28 font-size: $font-size; 28 font-size: $font-size;
29 line-height: $font-size; 29 line-height: $font-size;
30 overflow: hidden; 30 overflow: hidden;
31 text-overflow: ellipsis; 31 text-overflow: ellipsis;
32 max-height: $font-size * $number-of-lines;
32} 33}
33 34
34@mixin prefix($property, $parameters...) { 35@mixin fade-text ($fade-after, $background-color) {
35 @each $prefix in -webkit-, -moz-, -ms-, -o-, "" { 36 position: relative;
36 #{$prefix}#{$property}: $parameters; 37 overflow: hidden;
38
39 &:after {
40 content: '';
41 pointer-events: none;
42 width: 100%;
43 height: 100%;
44 position: absolute;
45 left: 0;
46 top: 0;
47 background: linear-gradient(transparent $fade-after, $background-color);
37 } 48 }
38} 49}
39 50
@@ -41,9 +52,6 @@
41 word-break: break-word; 52 word-break: break-word;
42 word-wrap: break-word; 53 word-wrap: break-word;
43 overflow-wrap: break-word; 54 overflow-wrap: break-word;
44 -webkit-hyphens: auto;
45 -ms-hyphens: auto;
46 -moz-hyphens: auto;
47 hyphens: auto; 55 hyphens: auto;
48} 56}
49 57
@@ -52,28 +60,6 @@
52 ::ng-deep .material { 60 ::ng-deep .material {
53 color: $color; 61 color: $color;
54 } 62 }
55
56 ::ng-deep svg {
57 path[fill="#000"],
58 g[fill="#000"],
59 rect[fill="#000"],
60 circle[fill="#000"],
61 polygon[fill="#000"] {
62 fill: $color;
63 }
64
65 path[stroke="#000"],
66 g[stroke="#000"],
67 rect[stroke="#000"],
68 circle[stroke="#000"],
69 polygon[stroke="#000"] {
70 stroke: $color;
71 }
72
73 stop[stop-color="#000"] {
74 stop-color: $color;
75 }
76 }
77} 63}
78 64
79@mixin fill-svg-color ($color) { 65@mixin fill-svg-color ($color) {
@@ -163,6 +149,33 @@
163 } 149 }
164} 150}
165 151
152@mixin orange-button-inverted {
153 @include button-focus(pvar(--mainColorLightest));
154
155 border: 2px solid pvar(--mainColor);
156 font-weight: $font-semibold;
157
158 &, &:active, &:focus {
159 color: pvar(--mainColor);
160 background-color: pvar(--mainBackgroundColor);
161 }
162
163 &:hover {
164 color: pvar(--mainColor);
165 background-color: pvar(--mainColorLightest);
166 }
167
168 &[disabled], &.disabled {
169 cursor: default;
170 color: pvar(--mainColor);
171 background-color: #C6C6C6;
172 }
173
174 my-global-icon {
175 @include apply-svg-color(pvar(--mainColor))
176 }
177}
178
166@mixin tertiary-button { 179@mixin tertiary-button {
167 @include button-focus($grey-button-outline-color); 180 @include button-focus($grey-button-outline-color);
168 181
@@ -534,6 +547,14 @@
534 min-height: $size; 547 min-height: $size;
535} 548}
536 549
550@mixin channel-avatar ($size) {
551 width: $size;
552 height: $size;
553 min-width: $size;
554 min-height: $size;
555 border-radius: 5px;
556}
557
537@mixin chevron ($size, $border-width) { 558@mixin chevron ($size, $border-width) {
538 border-style: solid; 559 border-style: solid;
539 border-width: $border-width $border-width 0 0; 560 border-width: $border-width $border-width 0 0;
@@ -593,103 +614,29 @@
593 } 614 }
594} 615}
595 616
596@mixin sub-menu-with-actor {
597 position: initial;
598 z-index: unset;
599 height: max-content;
600 display: flex;
601 flex-direction: column;
602 align-items: flex-start;
603
604 .actor {
605 display: flex;
606 margin-top: 20px;
607 margin-bottom: 20px;
608
609 img {
610 @include avatar(80px);
611
612 margin-right: 20px;
613 }
614
615 .actor-info {
616 display: flex;
617 flex-direction: column;
618 justify-content: center;
619
620 .actor-names {
621 display: flex;
622 align-items: center;
623 flex-wrap: wrap;
624
625 .actor-display-name {
626 font-size: 23px;
627 font-weight: $font-bold;
628 margin-right: 7px;
629 }
630
631 .actor-name {
632 position: relative;
633 top: 3px;
634 font-size: 14px;
635 color: $grey-actor-name;
636 }
637 }
638
639 .actor-lower {
640 grid-area: lower;
641 }
642
643 .actor-followers {
644 font-size: 15px;
645 }
646
647 .actor-owner {
648 @include actor-owner;
649 }
650 }
651 }
652
653 .links {
654 margin-top: 0;
655 margin-bottom: 15px;
656
657 a {
658 margin-top: 0;
659 margin-bottom: 0;
660 text-transform: uppercase;
661 font-weight: 600;
662 font-size: 110%;
663
664 @media screen and (max-width: $mobile-view) {
665 font-size: 130%;
666 }
667 }
668
669 list-overflow {
670 display: inline-block;
671 width: max-content;
672 }
673 }
674}
675
676@mixin create-button { 617@mixin create-button {
677 @include peertube-button-link; 618 @include peertube-button-link;
678 @include orange-button; 619 @include orange-button;
679 @include button-with-icon(20px, 5px, -1px); 620 @include button-with-icon(20px, 5px, -1px);
680} 621}
681 622
682@mixin row-blocks { 623@mixin row-blocks ($column-responsive: true) {
683 display: flex; 624 display: flex;
684 min-height: 130px; 625 min-height: 130px;
685 padding-bottom: 20px; 626 padding-bottom: 20px;
686 margin-bottom: 20px; 627 margin-bottom: 20px;
687 border-bottom: 1px solid #C6C6C6; 628 border-bottom: 1px solid #C6C6C6;
688 629
689 @media screen and (max-width: 800px) { 630 @media screen and (max-width: $small-view) {
690 flex-direction: column; 631 @if $column-responsive {
691 height: auto; 632 flex-direction: column;
692 align-items: center; 633 height: auto;
634 align-items: center;
635 } @else {
636 min-height: initial;
637 padding-bottom: 10px;
638 margin-bottom: 10px;
639 }
693 } 640 }
694} 641}
695 642
@@ -756,7 +703,7 @@
756 padding: 0.75rem 1rem; 703 padding: 0.75rem 1rem;
757 margin-bottom: 1rem; 704 margin-bottom: 1rem;
758 list-style: none; 705 list-style: none;
759 background-color: pvar(--submenuColor); 706 background-color: pvar(--submenuBackgroundColor);
760 border-radius: 0.25rem; 707 border-radius: 0.25rem;
761 708
762 .breadcrumb-item { 709 .breadcrumb-item {
@@ -811,7 +758,7 @@
811 & > a, 758 & > a,
812 & > div { 759 & > div {
813 padding: 20px; 760 padding: 20px;
814 background: pvar(--submenuColor); 761 background: pvar(--submenuBackgroundColor);
815 border-radius: 4px; 762 border-radius: 4px;
816 box-sizing: border-box; 763 box-sizing: border-box;
817 height: 100%; 764 height: 100%;
@@ -833,7 +780,7 @@
833 } 780 }
834} 781}
835 782
836@mixin divider($color: pvar(--submenuColor), $background: pvar(--mainBackgroundColor)) { 783@mixin divider($color: pvar(--submenuBackgroundColor), $background: pvar(--mainBackgroundColor)) {
837 width: 95%; 784 width: 95%;
838 border-top: .05rem solid $color; 785 border-top: .05rem solid $color;
839 height: .05rem; 786 height: .05rem;
@@ -916,7 +863,7 @@
916 } 863 }
917} 864}
918 865
919@mixin admin-sub-header-responsive ($horizontal-margins) { 866@mixin admin-sub-header-responsive {
920 flex-direction: column; 867 flex-direction: column;
921 868
922 .form-sub-title { 869 .form-sub-title {
@@ -931,7 +878,7 @@
931 white-space: nowrap; 878 white-space: nowrap;
932 height: 50px; 879 height: 50px;
933 padding: 10px 0; 880 padding: 10px 0;
934 width: calc(100vw - #{$horizontal-margins*2}); 881 width: 100%;
935 882
936 a { 883 a {
937 margin-left: 5px; 884 margin-left: 5px;
@@ -939,14 +886,16 @@
939 } 886 }
940} 887}
941 888
942// applies 16:9 ratio to a child element (using $selector) only using 889// applies ratio (default to 16:9) to a child element (using $selector) only using
943// an immediate's parent size. This allows 16:9 ratio without explicit 890// an immediate's parent size. This allows to set a ratio without explicit
944// dimensions, as width/height cannot be computed from each other. 891// dimensions, as width/height cannot be computed from each other.
945@mixin large-screen-ratio ($selector: 'div') { 892@mixin block-ratio ($selector: 'div', $inverted-ratio: 9/16) {
893 $padding-percent: percentage($inverted-ratio);
894
946 position: relative; 895 position: relative;
947 height: 0; 896 height: 0;
948 width: 100%; 897 width: 100%;
949 padding-top: 56%; 898 padding-top: $padding-percent;
950 899
951 #{$selector} { 900 #{$selector} {
952 position: absolute; 901 position: absolute;
@@ -991,3 +940,31 @@
991 940
992 border-left: $width solid rgba(255, 255, 255, 0.95); 941 border-left: $width solid rgba(255, 255, 255, 0.95);
993} 942}
943
944@mixin on-small-main-col () {
945 :host-context(.main-col:not(.expanded)) {
946 @media screen and (max-width: $small-view + $menu-width) {
947 @content;
948 }
949 }
950
951 :host-context(.main-col.expanded) {
952 @media screen and (max-width: $small-view) {
953 @content;
954 }
955 }
956}
957
958@mixin on-mobile-main-col () {
959 :host-context(.main-col:not(.expanded)) {
960 @media screen and (max-width: $mobile-view + $menu-width) {
961 @content;
962 }
963 }
964
965 :host-context(.main-col.expanded) {
966 @media screen and (max-width: $mobile-view) {
967 @content;
968 }
969 }
970}
diff --git a/client/src/sass/include/_variables.scss b/client/src/sass/include/_variables.scss
index c8316473d..d2a5d2bd9 100644
--- a/client/src/sass/include/_variables.scss
+++ b/client/src/sass/include/_variables.scss
@@ -16,9 +16,10 @@ $grey-foreground-hover-color: #303030;
16$grey-button-outline-color: scale-color($grey-foreground-color, $alpha: -95%); 16$grey-button-outline-color: scale-color($grey-foreground-color, $alpha: -95%);
17 17
18$main-color: hsl(24, 90%, 50%); 18$main-color: hsl(24, 90%, 50%);
19$main-hover-color: lighten($main-color, 5%);
20$main-color-lighter: lighten($main-color, 10%); 19$main-color-lighter: lighten($main-color, 10%);
21$main-color-lightest: lighten($main-color, 40%); 20$main-color-lightest: lighten($main-color, 40%);
21$main-hover-color: lighten($main-color, 5%);
22
22$secondary-color: hsl(187, 77%, 34%); 23$secondary-color: hsl(187, 77%, 34%);
23 24
24$support-button: inherit; 25$support-button: inherit;
@@ -47,18 +48,34 @@ $menu-bottom-color: #C6C6C6;
47$menu-width: 240px; 48$menu-width: 240px;
48$menu-lateral-padding: 26px; 49$menu-lateral-padding: 26px;
49 50
50$sub-menu-color: #F7F7F7; 51$sub-menu-background-color: #F7F7F7;
51$sub-menu-height: 81px; 52$sub-menu-height: 81px;
52 53
54$channel-background-color: #f6ede8;
55
56$banner-inverted-ratio: 1/6;
57
58$max-channels-width: 1200px;
59
53$footer-height: 30px; 60$footer-height: 30px;
54$footer-margin: 30px; 61$footer-margin: 30px;
55 62
56$separator-border-color: rgba(0, 0, 0, 0.10); 63$separator-border-color: rgba(0, 0, 0, 0.10);
57 64
58$video-miniature-width: 238px;
59$video-miniature-margin-bottom: 15px; 65$video-miniature-margin-bottom: 15px;
60$video-thumbnail-height: 122px; 66
61$video-thumbnail-width: 223px; 67$video-miniature-row-name-font-size: 1.3em;
68$video-miniature-row-mobile-name-font-size: 14px;
69
70$video-miniature-row-info-font-size: 14px;
71$video-miniature-row-mobile-info-font-size: 12px;
72
73$video-thumbnail-height: 153px;
74$video-thumbnail-width: 280px;
75$video-thumbnail-medium-height: 114px;
76$video-thumbnail-medium-width: 201px;
77$video-thumbnail-small-height: 71px;
78$video-thumbnail-small-width: 125px;
62 79
63$theater-bottom-space: 115px; 80$theater-bottom-space: 115px;
64 81
@@ -98,7 +115,9 @@ $variables: (
98 115
99 --menuBackgroundColor: var(--menuBackgroundColor), 116 --menuBackgroundColor: var(--menuBackgroundColor),
100 --menuForegroundColor: var(--menuForegroundColor), 117 --menuForegroundColor: var(--menuForegroundColor),
101 --submenuColor: var(--submenuColor), 118
119 --submenuBackgroundColor: var(--submenuBackgroundColor),
120 --channelBackgroundColor: var(--channelBackgroundColor),
102 121
103 --inputForegroundColor: var(--inputForegroundColor), 122 --inputForegroundColor: var(--inputForegroundColor),
104 --inputBackgroundColor: var(--inputBackgroundColor), 123 --inputBackgroundColor: var(--inputBackgroundColor),
@@ -116,11 +135,20 @@ $variables: (
116 --supportButtonHeartColor: var(--supportButtonHeartColor), 135 --supportButtonHeartColor: var(--supportButtonHeartColor),
117 136
118 --embedForegroundColor: var(--embedForegroundColor), 137 --embedForegroundColor: var(--embedForegroundColor),
119 --embedBigPlayBackgroundColor: var(--embedBigPlayBackgroundColor) 138 --embedBigPlayBackgroundColor: var(--embedBigPlayBackgroundColor),
139
140 --horizontalMarginContent: var(--horizontalMarginContent),
141 --videosHorizontalMarginContent: var(--videosHorizontalMarginContent),
142 --mainColWidth: var(--mainColWidth)
120); 143);
121 144
145// SASS type check our CSS variables
122@function pvar($variable) { 146@function pvar($variable) {
123 @return map-get($variables, $variable); 147 @if map-has-key($variables, $variable) {
148 @return map-get($variables, $variable);
149 } @else {
150 @error "ERROR: Variable #{$variable} does not exist";
151 }
124} 152}
125 153
126/*** z-index groups ***/ 154/*** z-index groups ***/
diff --git a/client/src/sass/ng-select.scss b/client/src/sass/ng-select.scss
index 54c805ccf..13fc1d6c2 100644
--- a/client/src/sass/ng-select.scss
+++ b/client/src/sass/ng-select.scss
@@ -11,7 +11,7 @@ $ng-select-highlight: #f2690d;
11$ng-select-box-shadow: #{$focus-box-shadow-form} pvar(--mainColorLightest); 11$ng-select-box-shadow: #{$focus-box-shadow-form} pvar(--mainColorLightest);
12// $ng-select-placeholder: lighten($ng-select-primary-text, 40) !default; 12// $ng-select-placeholder: lighten($ng-select-primary-text, 40) !default;
13$ng-select-height: 30px; 13$ng-select-height: 30px;
14// $ng-select-value-padding-left: 10px !default; 14$ng-select-value-padding-left: 15px;
15// $ng-select-value-font-size: 0.9em !default; 15// $ng-select-value-font-size: 0.9em !default;
16 16
17@import "~@ng-select/ng-select/scss/default.theme.scss"; 17@import "~@ng-select/ng-select/scss/default.theme.scss";
@@ -20,11 +20,6 @@ $ng-select-height: 30px;
20 font-size: .9em; 20 font-size: .9em;
21} 21}
22 22
23.ng-input,
24.ng-select .ng-select-container .ng-value-container {
25 padding-left: 15px !important;
26}
27
28.ng-select { 23.ng-select {
29 &.ng-select-focused { 24 &.ng-select-focused {
30 &:not(.ng-select-opened) > .ng-select-container { 25 &:not(.ng-select-opened) > .ng-select-container {
@@ -44,4 +39,11 @@ $ng-select-height: 30px;
44 &.ng-select-single .ng-value-container .ng-value { 39 &.ng-select-single .ng-value-container .ng-value {
45 color: pvar(--inputForegroundColor); 40 color: pvar(--inputForegroundColor);
46 } 41 }
42
43 &.ng-select-multiple .ng-select-container .ng-value-container {
44 padding-left: 12px;
45 .ng-value {
46 margin-left: 3px;
47 }
48 }
47} 49}
diff --git a/client/src/sass/player/context-menu.scss b/client/src/sass/player/context-menu.scss
index f3a28ead0..ad673eea7 100644
--- a/client/src/sass/player/context-menu.scss
+++ b/client/src/sass/player/context-menu.scss
@@ -14,7 +14,7 @@ $context-menu-width: 350px;
14 14
15 .vjs-menu-content { 15 .vjs-menu-content {
16 opacity: $primary-foreground-opacity; 16 opacity: $primary-foreground-opacity;
17 color: pvar(--embedForegroundCsolor); 17 color: pvar(--embedForegroundColor);
18 font-size: $font-size !important; 18 font-size: $font-size !important;
19 font-weight: $font-semibold; 19 font-weight: $font-semibold;
20 } 20 }
diff --git a/client/src/sass/player/peertube-skin.scss b/client/src/sass/player/peertube-skin.scss
index 0144e89fb..81aacf1d7 100644
--- a/client/src/sass/player/peertube-skin.scss
+++ b/client/src/sass/player/peertube-skin.scss
@@ -43,10 +43,6 @@ body {
43 } 43 }
44 } 44 }
45 45
46 .vjs-button > .vjs-icon-placeholder::before {
47 line-height: $control-bar-height;
48 }
49
50 .vjs-volume-level::before { 46 .vjs-volume-level::before {
51 content: ''; /* Remove Circle From Progress Bar */ 47 content: ''; /* Remove Circle From Progress Bar */
52 } 48 }
@@ -242,8 +238,19 @@ body {
242 @include disable-outline; 238 @include disable-outline;
243 239
244 cursor: pointer; 240 cursor: pointer;
245 font-size: $play-control-font-size;
246 width: 2em; 241 width: 2em;
242
243 .vjs-icon-placeholder {
244 line-height: $control-bar-height;
245 position: relative;
246 top: -1px;
247
248 &::before {
249 font-size: 28px;
250 line-height: unset;
251 position: relative;
252 }
253 }
247 } 254 }
248 255
249 .vjs-time-control { 256 .vjs-time-control {
@@ -375,7 +382,6 @@ body {
375 .vjs-mute-control { 382 .vjs-mute-control {
376 @include disable-outline; 383 @include disable-outline;
377 384
378 line-height: $control-bar-height;
379 padding: 0; 385 padding: 0;
380 width: 30px; 386 width: 30px;
381 387
diff --git a/client/src/sass/primeng-custom.scss b/client/src/sass/primeng-custom.scss
index afa577819..9c9b5d4fc 100644
--- a/client/src/sass/primeng-custom.scss
+++ b/client/src/sass/primeng-custom.scss
@@ -547,7 +547,7 @@ p-table {
547 height: 46px; 547 height: 46px;
548 548
549 &.p-highlight { 549 &.p-highlight {
550 background-color: pvar(--submenuColor) !important; 550 background-color: pvar(--submenuBackgroundColor) !important;
551 551
552 td, td > a { 552 td, td > a {
553 color: pvar(--mainForegroundColor) !important; 553 color: pvar(--mainForegroundColor) !important;
@@ -558,7 +558,7 @@ p-table {
558 .p-datatable-tbody { 558 .p-datatable-tbody {
559 tr { 559 tr {
560 &:hover { 560 &:hover {
561 background-color: pvar(--submenuColor) !important; 561 background-color: pvar(--submenuBackgroundColor) !important;
562 } 562 }
563 563
564 td { 564 td {
@@ -590,16 +590,16 @@ p-table {
590 th { 590 th {
591 border: none !important; 591 border: none !important;
592 border-bottom: 1px solid !important; 592 border-bottom: 1px solid !important;
593 border-color: pvar(--submenuColor) !important; 593 border-color: pvar(--submenuBackgroundColor) !important;
594 text-align: left !important; 594 text-align: left !important;
595 padding: 5px 0 5px 15px !important; 595 padding: 5px 0 5px 15px !important;
596 font-weight: $font-semibold !important; 596 font-weight: $font-semibold !important;
597 color: pvar(--mainForegroundColor) !important; 597 color: pvar(--mainForegroundColor) !important;
598 598
599 &.p-sortable-column:hover { 599 &.p-sortable-column:hover {
600 background-color: pvar(--submenuColor) !important; 600 background-color: pvar(--submenuBackgroundColor) !important;
601 border: 1px solid !important; 601 border: 1px solid !important;
602 border-color: pvar(--submenuColor) !important; 602 border-color: pvar(--submenuBackgroundColor) !important;
603 border-width: 0 1px !important; 603 border-width: 0 1px !important;
604 604
605 &:first-child { 605 &:first-child {
@@ -608,7 +608,7 @@ p-table {
608 } 608 }
609 609
610 &.p-highlight { 610 &.p-highlight {
611 background-color: pvar(--submenuColor) !important; 611 background-color: pvar(--submenuBackgroundColor) !important;
612 612
613 .pi { 613 .pi {
614 @extend .glyphicon; 614 @extend .glyphicon;
@@ -654,7 +654,7 @@ p-table {
654 position: relative; 654 position: relative;
655 border: none; 655 border: none;
656 border-top: 1px solid !important; 656 border-top: 1px solid !important;
657 border-color: pvar(--submenuColor) !important; 657 border-color: pvar(--submenuBackgroundColor) !important;
658 height: 40px; 658 height: 40px;
659 display: flex; 659 display: flex;
660 justify-content: center; 660 justify-content: center;
@@ -753,29 +753,32 @@ p-table {
753} 753}
754 754
755// overflow data table 755// overflow data table
756@mixin overflow-datatable ($table-min-width, $horizontal-margins, $mobile-paginator: true) { 756p-table {
757 p-table { 757 .p-datatable-wrapper {
758 .p-datatable-wrapper { 758 overflow-x: auto;
759 overflow-x: auto; 759 max-width: 100%;
760 max-width: calc(100vw - #{$horizontal-margins * 2});
761
762 table {
763 min-width: $table-min-width;
764 }
765 }
766 760
767 @if $mobile-paginator { 761 table {
768 p-paginator .p-paginator-bottom { 762 min-width: breakpoint(lg);
769 display: block; 763 }
764 }
770 765
771 .p-paginator-current { 766 @media screen and (max-width: #{breakpoint(lg)}) {
772 position: relative; 767 // Prevent overflow
773 display: block; 768 p-paginator {
774 } 769 .p-paginator-current,
770 .p-dropdown {
771 top: 0;
772 margin-top: 30px;
773 }
774 }
775 }
775 776
776 a, .p-paginator-pages { 777 @media screen and (max-width: $mobile-view) {
777 vertical-align: middle; 778 // Prevent overflow
778 } 779 p-paginator {
780 .p-paginator-pages > .p-paginator-page:not(.p-highlight) {
781 display: none;
779 } 782 }
780 } 783 }
781 } 784 }
diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts
index cf4bc6f03..ae8f176b7 100644
--- a/client/src/standalone/videos/embed.ts
+++ b/client/src/standalone/videos/embed.ts
@@ -531,6 +531,7 @@ export class PeerTubeEmbed {
531 videoCaptions, 531 videoCaptions,
532 inactivityTimeout: 2500, 532 inactivityTimeout: 2500,
533 videoViewUrl: this.getVideoUrl(videoInfo.uuid) + '/views', 533 videoViewUrl: this.getVideoUrl(videoInfo.uuid) + '/views',
534 videoUUID: videoInfo.uuid,
534 535
535 isLive: videoInfo.isLive, 536 isLive: videoInfo.isLive,
536 537
@@ -545,7 +546,8 @@ export class PeerTubeEmbed {
545 546
546 serverUrl: window.location.origin, 547 serverUrl: window.location.origin,
547 language: navigator.language, 548 language: navigator.language,
548 embedUrl: window.location.origin + videoInfo.embedPath 549 embedUrl: window.location.origin + videoInfo.embedPath,
550 embedTitle: videoInfo.name
549 }, 551 },
550 552
551 webtorrent: { 553 webtorrent: {
@@ -783,6 +785,8 @@ export class PeerTubeEmbed {
783 785
784 showModal: unimplemented, 786 showModal: unimplemented,
785 787
788 getServerConfig: unimplemented,
789
786 markdownRenderer: { 790 markdownRenderer: {
787 textMarkdownToHTML: unimplemented, 791 textMarkdownToHTML: unimplemented,
788 enhancedMarkdownToHTML: unimplemented 792 enhancedMarkdownToHTML: unimplemented
diff --git a/client/src/types/register-client-option.model.ts b/client/src/types/register-client-option.model.ts
index e3c6d803d..7e5356a2b 100644
--- a/client/src/types/register-client-option.model.ts
+++ b/client/src/types/register-client-option.model.ts
@@ -1,5 +1,6 @@
1import { RegisterClientFormFieldOptions, RegisterClientVideoFieldOptions } from '@shared/models/plugins/register-client-form-field.model' 1import { RegisterClientFormFieldOptions, RegisterClientVideoFieldOptions } from '@shared/models/plugins/register-client-form-field.model'
2import { RegisterClientHookOptions } from '@shared/models/plugins/register-client-hook.model' 2import { RegisterClientHookOptions } from '@shared/models/plugins/register-client-hook.model'
3import { ServerConfig } from '@shared/models/server'
3 4
4export type RegisterClientOptions = { 5export type RegisterClientOptions = {
5 registerHook: (options: RegisterClientHookOptions) => void 6 registerHook: (options: RegisterClientHookOptions) => void
@@ -16,6 +17,8 @@ export type RegisterClientHelpers = {
16 17
17 getSettings: () => Promise<{ [ name: string ]: string }> 18 getSettings: () => Promise<{ [ name: string ]: string }>
18 19
20 getServerConfig: () => Promise<ServerConfig>
21
19 notifier: { 22 notifier: {
20 info: (text: string, title?: string, timeout?: number) => void, 23 info: (text: string, title?: string, timeout?: number) => void,
21 error: (text: string, title?: string, timeout?: number) => void, 24 error: (text: string, title?: string, timeout?: number) => void,
diff --git a/client/yarn.lock b/client/yarn.lock
index 79ab1e2a8..75548e83f 100644
--- a/client/yarn.lock
+++ b/client/yarn.lock
@@ -2,23 +2,23 @@
2# yarn lockfile v1 2# yarn lockfile v1
3 3
4 4
5"@angular-devkit/architect@0.1102.2": 5"@angular-devkit/architect@0.1102.5":
6 version "0.1102.2" 6 version "0.1102.5"
7 resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1102.2.tgz#3b3eb654ae7c8c204b248bba76982ce8de2f7b6c" 7 resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1102.5.tgz#431df157af0c6477e5951f64ff12f3d5d5f075ee"
8 integrity sha512-FE7DeT13elqDlELF23QqvEFnT2BkxeC5t31/QW85IN/OR5Tf/q7XEpj7giJXyzKFQ60M3ZzbznZyRz0EqtfaBQ== 8 integrity sha512-lVc6NmEAZZPzvc18GzMFLoxqKKvPlNOg4vEtFsFldZmrydLJJGFi4KAs2WaJd8qVR1XuY4el841cjDQAJSq6sQ==
9 dependencies: 9 dependencies:
10 "@angular-devkit/core" "11.2.2" 10 "@angular-devkit/core" "11.2.5"
11 rxjs "6.6.3" 11 rxjs "6.6.3"
12 12
13"@angular-devkit/build-angular@^0.1102.2": 13"@angular-devkit/build-angular@^0.1102.2":
14 version "0.1102.2" 14 version "0.1102.5"
15 resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-0.1102.2.tgz#c850818fd8bb4dd4fda6288390868475c4b3236e" 15 resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-0.1102.5.tgz#7db51dfc33a8683458fa714d434f8c09fdc1f648"
16 integrity sha512-AjnvHrzkYTzDGzp0r5RmGoP9fyZXtaVFo0598PRusi1oWp1sW6B5FKPWw896iREOlotRXw3dsjqrGwbMcz0qyg== 16 integrity sha512-iAq/KbRq6kuA17rQZ67/0zQHEzpC9RzvtMZQ3wiiFsOmW5AIV5scjP7e6dn+F6vXZA44X4gCH5AUUkOLXyEtfg==
17 dependencies: 17 dependencies:
18 "@angular-devkit/architect" "0.1102.2" 18 "@angular-devkit/architect" "0.1102.5"
19 "@angular-devkit/build-optimizer" "0.1102.2" 19 "@angular-devkit/build-optimizer" "0.1102.5"
20 "@angular-devkit/build-webpack" "0.1102.2" 20 "@angular-devkit/build-webpack" "0.1102.5"
21 "@angular-devkit/core" "11.2.2" 21 "@angular-devkit/core" "11.2.5"
22 "@babel/core" "7.12.10" 22 "@babel/core" "7.12.10"
23 "@babel/generator" "7.12.11" 23 "@babel/generator" "7.12.11"
24 "@babel/plugin-transform-async-to-generator" "7.12.1" 24 "@babel/plugin-transform-async-to-generator" "7.12.1"
@@ -26,8 +26,9 @@
26 "@babel/preset-env" "7.12.11" 26 "@babel/preset-env" "7.12.11"
27 "@babel/runtime" "7.12.5" 27 "@babel/runtime" "7.12.5"
28 "@babel/template" "7.12.7" 28 "@babel/template" "7.12.7"
29 "@discoveryjs/json-ext" "0.5.2"
29 "@jsdevtools/coverage-istanbul-loader" "3.0.5" 30 "@jsdevtools/coverage-istanbul-loader" "3.0.5"
30 "@ngtools/webpack" "11.2.2" 31 "@ngtools/webpack" "11.2.5"
31 ansi-colors "4.1.1" 32 ansi-colors "4.1.1"
32 autoprefixer "10.2.4" 33 autoprefixer "10.2.4"
33 babel-loader "8.2.2" 34 babel-loader "8.2.2"
@@ -88,30 +89,30 @@
88 webpack-subresource-integrity "1.5.2" 89 webpack-subresource-integrity "1.5.2"
89 worker-plugin "5.0.0" 90 worker-plugin "5.0.0"
90 91
91"@angular-devkit/build-optimizer@0.1102.2": 92"@angular-devkit/build-optimizer@0.1102.5":
92 version "0.1102.2" 93 version "0.1102.5"
93 resolved "https://registry.yarnpkg.com/@angular-devkit/build-optimizer/-/build-optimizer-0.1102.2.tgz#a306fee0bc648983405320953f05ad1fc60b6b84" 94 resolved "https://registry.yarnpkg.com/@angular-devkit/build-optimizer/-/build-optimizer-0.1102.5.tgz#5c17d82a8c4f03ec0a14110838c2c3da6cb24dfd"
94 integrity sha512-TCWWqAe+pWZzLp/g2gG8Z5NC8JSgDNfyEuMBWxEUfo1Sm3BluXoz0BbmnietuhXJZ+fPAp9rLLzEGZlHvOlmOA== 95 integrity sha512-ujTwrevgMRNyWir4IdnJEdDRkVSLqugRpL6cU9OeqGn6Bu+zEzZQokLkMZvbw00eEKlf5Siej4hEeF1Hnx+LUA==
95 dependencies: 96 dependencies:
96 loader-utils "2.0.0" 97 loader-utils "2.0.0"
97 source-map "0.7.3" 98 source-map "0.7.3"
98 tslib "2.1.0" 99 tslib "2.1.0"
99 typescript "4.1.3" 100 typescript "4.1.5"
100 webpack-sources "2.2.0" 101 webpack-sources "2.2.0"
101 102
102"@angular-devkit/build-webpack@0.1102.2": 103"@angular-devkit/build-webpack@0.1102.5":
103 version "0.1102.2" 104 version "0.1102.5"
104 resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.1102.2.tgz#f48501426a5d01b0610dafce33b4eb84d07181e6" 105 resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.1102.5.tgz#e111acf7c0cbed761ae382089052a5c2dee71d96"
105 integrity sha512-59CBbwbdN8lI5/whuNeAZHRJxPlOmDc5ux8aJJNwWI9w54fz0ut/MLT3iuPk+WZuKlGdpS1sGkObfZwWen5kIQ== 106 integrity sha512-VMsi+mFwgPUQi7eEc2oKcf7X0xD0R1xfoguLS/+HGy3sfh+b7oJy3BU4+TRzDPBtGj6vWvENK2rwHFN3cBWvxA==
106 dependencies: 107 dependencies:
107 "@angular-devkit/architect" "0.1102.2" 108 "@angular-devkit/architect" "0.1102.5"
108 "@angular-devkit/core" "11.2.2" 109 "@angular-devkit/core" "11.2.5"
109 rxjs "6.6.3" 110 rxjs "6.6.3"
110 111
111"@angular-devkit/core@11.2.2": 112"@angular-devkit/core@11.2.5":
112 version "11.2.2" 113 version "11.2.5"
113 resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-11.2.2.tgz#c6b40f941b24d2af447831fc958b744316cd7d87" 114 resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-11.2.5.tgz#f9ba8288a6cc388808ee639c383dada50d64d06a"
114 integrity sha512-LUDO1AdIjereiMh0j5p9xJcdr9ifhbWCPxlZqfu5wHzUfhCx9gO2Lvjp6rZXQ3OedXg5IZUnyxHlzkszQOsgiw== 115 integrity sha512-DRFvEHRKoC+hTwcOAJqLe6UQa+bpXc/1IGCMHWEbuply0KIFIGQOlmaYwFZKixz3HdFZlmoCMcAVkAXvyaWVsQ==
115 dependencies: 116 dependencies:
116 ajv "6.12.6" 117 ajv "6.12.6"
117 fast-json-stable-stringify "2.1.0" 118 fast-json-stable-stringify "2.1.0"
@@ -119,41 +120,41 @@
119 rxjs "6.6.3" 120 rxjs "6.6.3"
120 source-map "0.7.3" 121 source-map "0.7.3"
121 122
122"@angular-devkit/schematics@11.2.2": 123"@angular-devkit/schematics@11.2.5":
123 version "11.2.2" 124 version "11.2.5"
124 resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-11.2.2.tgz#0c8c4b98a30f00649dcbb7794d3783b9a067209f" 125 resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-11.2.5.tgz#ddcb966f3f1dc910e55f03067036f1f6a01b8222"
125 integrity sha512-6bIxMwafz/+lwdtcshwOuFfhxTMU4RLma1uxBS34DXupMauPGl0IIXAy5cK9dXPlHLxuGsjeBiOM6eq033RLgw== 126 integrity sha512-7RoWgpMvhljPhW9CMz1EtqkwNnGpnsPyy0N29ClHPUq+o8wLR0hvbLBDz1fKSF7j1AwRccaQSNTj8KWsjzQJLQ==
126 dependencies: 127 dependencies:
127 "@angular-devkit/core" "11.2.2" 128 "@angular-devkit/core" "11.2.5"
128 ora "5.3.0" 129 ora "5.3.0"
129 rxjs "6.6.3" 130 rxjs "6.6.3"
130 131
131"@angular/animations@^11.1.1": 132"@angular/animations@^11.1.1":
132 version "11.2.3" 133 version "11.2.6"
133 resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-11.2.3.tgz#518183e5f7b8c3b304020ea86d12cc3216142cc9" 134 resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-11.2.6.tgz#36935bc0fe33f1486ed889f8b5e12915858ccf5a"
134 integrity sha512-Z6sHIeTeeZrRAW83NI7FO7THF50cPCFkkuvVah3qmCqopY6FuoHKUBEENyGzQGH69LbGFYhEppY8KM/6JtVF6Q== 135 integrity sha512-fci034QakkoIrFeY/uOmDvf6AupZ7ziU1FlBMs/wn4HOqwsPCofpawvFQnfj5nez1+KM5JOJ1VHmZKJupkWfgw==
135 dependencies: 136 dependencies:
136 tslib "^2.0.0" 137 tslib "^2.0.0"
137 138
138"@angular/cdk@^11.0.0": 139"@angular/cdk@^11.0.0":
139 version "11.2.2" 140 version "11.2.5"
140 resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-11.2.2.tgz#f541069db3f5705d8c064138f6cd94568fe1b658" 141 resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-11.2.5.tgz#e0cce8b28ca635b6151b834c6e1c4bc0a8dd7c04"
141 integrity sha512-p3lRDPlnOuJtLWEd020QOyn0ERyc1LF7OLi90hTdzMMxe9fT3v6sQJVRs8jIY3NTmpIm/pNDGi77+1/vKerLPQ== 142 integrity sha512-ugalSDLME5E9JlxcRR8RGlOYlaV6rIzxOVQrGRBzY2tdhMT4Ng+BFtCkq1K88AU1sTLHq54xg9Xkfn7b5W2kiA==
142 dependencies: 143 dependencies:
143 tslib "^2.0.0" 144 tslib "^2.0.0"
144 optionalDependencies: 145 optionalDependencies:
145 parse5 "^5.0.0" 146 parse5 "^5.0.0"
146 147
147"@angular/cli@^11.1.2": 148"@angular/cli@^11.1.2":
148 version "11.2.2" 149 version "11.2.5"
149 resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-11.2.2.tgz#ca56894f1a4d1f4e411408b8185b711614c3195a" 150 resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-11.2.5.tgz#3cf3e6432db41cebb364da2dcf3d44588535a34a"
150 integrity sha512-rOVBzDzrMuOgJY43O46/7yYbncx0egGfr+DMJDQdazePGH1H3INN/eA9gkVcVK53ztCYb9X1sbZKOs9TUhF6nw== 151 integrity sha512-GIwK8l6wtg/++8aDYW++LSf7v1uqDtB6so2rPjNlOm7oYk5iqM73KaorQb/1A52oxWE3IRSJLNQaSyUlWvHvSA==
151 dependencies: 152 dependencies:
152 "@angular-devkit/architect" "0.1102.2" 153 "@angular-devkit/architect" "0.1102.5"
153 "@angular-devkit/core" "11.2.2" 154 "@angular-devkit/core" "11.2.5"
154 "@angular-devkit/schematics" "11.2.2" 155 "@angular-devkit/schematics" "11.2.5"
155 "@schematics/angular" "11.2.2" 156 "@schematics/angular" "11.2.5"
156 "@schematics/update" "0.1102.2" 157 "@schematics/update" "0.1102.5"
157 "@yarnpkg/lockfile" "1.1.0" 158 "@yarnpkg/lockfile" "1.1.0"
158 ansi-colors "4.1.1" 159 ansi-colors "4.1.1"
159 debug "4.3.1" 160 debug "4.3.1"
@@ -173,16 +174,16 @@
173 uuid "8.3.2" 174 uuid "8.3.2"
174 175
175"@angular/common@^11.1.1": 176"@angular/common@^11.1.1":
176 version "11.2.3" 177 version "11.2.6"
177 resolved "https://registry.yarnpkg.com/@angular/common/-/common-11.2.3.tgz#e71d645fb6bdef9463f23a551cc072ef276c1d84" 178 resolved "https://registry.yarnpkg.com/@angular/common/-/common-11.2.6.tgz#9985b9f1b3d82588f85bb74b1967749b0134d017"
178 integrity sha512-51gVmr942SZtAFmhVfp7/3fcTQ+Tia7UxWjv6iUtYF3oCvTWbo/J1zki2VNSfmMNKJV8MaMq6XUw8UWbHA0sgQ== 179 integrity sha512-q1yR6bktd5p987gLEKiFY4CrHcmBxks9R6GcdgzGneQsucDtGESzEKdcJ0uaMXE+9teS+fQy5GvXel6DlA/J+w==
179 dependencies: 180 dependencies:
180 tslib "^2.0.0" 181 tslib "^2.0.0"
181 182
182"@angular/compiler-cli@^11.1.1": 183"@angular/compiler-cli@^11.1.1":
183 version "11.2.3" 184 version "11.2.6"
184 resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-11.2.3.tgz#5307215b9aa6e32d772906fd3b2960ba03a7565d" 185 resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-11.2.6.tgz#456844d71079df3ca3f025aaa9d9df9ed5a79006"
185 integrity sha512-ObQVI6q2c0VTWbsDnWJDdUZv2Jz/u1jiQNcrdtu/rjtJARaldEno9dMakN838Q6Nw4FzKUO6uYZXmnvKCUjfxQ== 186 integrity sha512-1OC8UkySaLzaw3aSrm8A6SA88CxQAdA4ffaOhBLE/Ee6CxpneVxn3ORlnccqnS8zWyEpschbootPJV56U3Azeg==
186 dependencies: 187 dependencies:
187 "@babel/core" "^7.8.6" 188 "@babel/core" "^7.8.6"
188 "@babel/types" "^7.8.6" 189 "@babel/types" "^7.8.6"
@@ -206,9 +207,9 @@
206 integrity sha512-ctjwuntPfZZT2mNj2NDIVu51t9cvbhl/16epc5xEwyzyDt76pX9UgwvY+MbXrf/C/FWwdtmNtfP698BKI+9leQ== 207 integrity sha512-ctjwuntPfZZT2mNj2NDIVu51t9cvbhl/16epc5xEwyzyDt76pX9UgwvY+MbXrf/C/FWwdtmNtfP698BKI+9leQ==
207 208
208"@angular/compiler@^11.1.1": 209"@angular/compiler@^11.1.1":
209 version "11.2.3" 210 version "11.2.6"
210 resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-11.2.3.tgz#72427d57b992bf6840fb7268357a466095caf8eb" 211 resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-11.2.6.tgz#8b69cd2f2c3bb0fbc6f95ded1ccbe20e6858daed"
211 integrity sha512-De8BwtSwPVYGdvQa6CDq2C1SLmB78YjS0t/KNlvfp85cl4Gb3BdjTDsKMkJXkm/3ubnIXi1BaRIsFNVTCCF70Q== 212 integrity sha512-3ijsCxnCLU1V1hy4UMf9qtMz5LR+wCdVFDqktEQccN9YEkN0cNtOc8Nu9EV9/mc2tqd1Q4xSBpb2o2mvpy7AhQ==
212 dependencies: 213 dependencies:
213 tslib "^2.0.0" 214 tslib "^2.0.0"
214 215
@@ -218,53 +219,53 @@
218 integrity sha512-6Pxgsrf0qF9iFFqmIcWmjJGkkCaCm6V5QNnxMy2KloO3SDq6QuMVRbN9RtC8Urmo25LP+eZ6ZgYqFYpdD8Hd9w== 219 integrity sha512-6Pxgsrf0qF9iFFqmIcWmjJGkkCaCm6V5QNnxMy2KloO3SDq6QuMVRbN9RtC8Urmo25LP+eZ6ZgYqFYpdD8Hd9w==
219 220
220"@angular/core@^11.1.1": 221"@angular/core@^11.1.1":
221 version "11.2.3" 222 version "11.2.6"
222 resolved "https://registry.yarnpkg.com/@angular/core/-/core-11.2.3.tgz#7dd59f35e0b2410543a61be6048c474c18a43f40" 223 resolved "https://registry.yarnpkg.com/@angular/core/-/core-11.2.6.tgz#c38ee7834519d3c94e51be62156784a984cd93d2"
223 integrity sha512-+G7rZj21Mcmf6nWjQ79EwomwEOVQ1WLqw6YvCXWzgJ9ZlVjLi/Sti0/jIzUpgK0E0Fn86yuXw/vgYq5kjGeOcQ== 224 integrity sha512-lS5JOQ/Y9gbk5WiMnCp5Zyz2pRIoZ+IWLOXHU5rkQeXy0zE3eMJhw0FfpEK+X5CeSNl2EPVSPLT0MtDtbNPodg==
224 dependencies: 225 dependencies:
225 tslib "^2.0.0" 226 tslib "^2.0.0"
226 227
227"@angular/forms@^11.1.1": 228"@angular/forms@^11.1.1":
228 version "11.2.3" 229 version "11.2.6"
229 resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-11.2.3.tgz#57460a110e6601b50362f878fc0f67701c76dc24" 230 resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-11.2.6.tgz#d82a1c655754d48ec861b9b3af370e6ee1e841cb"
230 integrity sha512-VfyKV8IxHTclcHQmt5gjGFmKC1kGz7sdNLYsEM+M0y88Bsufh3VIhK4kspfO4nhJxVfh6HFOt1JVQ5bvo6PDlQ== 231 integrity sha512-0xxayXCNc8lPQhDj5q/hAcG55cmDXPSBn2cxX4V+uDSGwKU1+h2CQID6gJdBJBh5wOaeMe6h8dK2s1pRgok66A==
231 dependencies: 232 dependencies:
232 tslib "^2.0.0" 233 tslib "^2.0.0"
233 234
234"@angular/localize@^11.1.1": 235"@angular/localize@^11.1.1":
235 version "11.2.3" 236 version "11.2.6"
236 resolved "https://registry.yarnpkg.com/@angular/localize/-/localize-11.2.3.tgz#df2e605341be53c2d4cead2d8b274415af8b3136" 237 resolved "https://registry.yarnpkg.com/@angular/localize/-/localize-11.2.6.tgz#465f2541c5bcdc396725504becaec3b96c718ec8"
237 integrity sha512-SCpum70G+MuoRitbv+u92fjDlKEbYizTosukxryh56QNa47iO3/rkVp8P2R75FDYJVJrxqoTiMGl0Q9tKdrEGA== 238 integrity sha512-8K+SdqKqIaRlNRegDBy//VAtf2rlwoZAmqoFfiM5ujuB4SFt32NAduxDUlFGWdZD5V3iPorFBrceq04bt695AA==
238 dependencies: 239 dependencies:
239 "@babel/core" "7.8.3" 240 "@babel/core" "7.8.3"
240 glob "7.1.2" 241 glob "7.1.2"
241 yargs "^16.1.1" 242 yargs "^16.1.1"
242 243
243"@angular/platform-browser-dynamic@^11.1.1": 244"@angular/platform-browser-dynamic@^11.1.1":
244 version "11.2.3" 245 version "11.2.6"
245 resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-11.2.3.tgz#3d7eb15ba4bcc9e227f68f13bf20258fa16efad1" 246 resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-11.2.6.tgz#26acbe4de315019ebe1e925ee826eda20c95d881"
246 integrity sha512-QUPCvack7De6u5AqWcW8O6FzczwqoL858R1NlnqojnNbcnN/dCtXtKvvETEEgp/9VMwLfcuLd1BWdBJSah7f6A== 247 integrity sha512-B56b8yPW3vAmPe4VONiBYEMZ6B1i5CUkJvit8qWWK3y7t5XrYOihIiGC0UqEDaw/uAg72GXjixspcxZWan5e9w==
247 dependencies: 248 dependencies:
248 tslib "^2.0.0" 249 tslib "^2.0.0"
249 250
250"@angular/platform-browser@^11.1.1": 251"@angular/platform-browser@^11.1.1":
251 version "11.2.3" 252 version "11.2.6"
252 resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-11.2.3.tgz#0c6b537500a1c6304829fab19cf8c12daa2b48b9" 253 resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-11.2.6.tgz#d2af4323275f501e279ee2aa821ac5599c11feae"
253 integrity sha512-S0IP/kGinIH18+gfnX0gLFLbP0Euw1RBceDt/WipYhUeFZZryQHvot/6KFLFtO+8rVunfrg+UyBiaK65/TT9Og== 254 integrity sha512-xnYpfoqWyQOUngfbHefsZMyelCSAaxpopu/WYP0gpbYh9qJiVhsN9s6zRMqOIPueq9lmvlEuGBMgaJjeD6Ei7Q==
254 dependencies: 255 dependencies:
255 tslib "^2.0.0" 256 tslib "^2.0.0"
256 257
257"@angular/router@^11.1.1": 258"@angular/router@^11.1.1":
258 version "11.2.3" 259 version "11.2.6"
259 resolved "https://registry.yarnpkg.com/@angular/router/-/router-11.2.3.tgz#407a0797845c1cac963663537b30872e39e4b229" 260 resolved "https://registry.yarnpkg.com/@angular/router/-/router-11.2.6.tgz#5845ef37e85400aeeaf0ffe670802a58569638cc"
260 integrity sha512-lRuEIlNj2BcBZ17mt5SZY7v80PsvlS4J6EbKSOFeSYhALM/AQnaaCdrrMlQ1WyEa5bBUabxGT9/zvahBosy2yA== 261 integrity sha512-n/3Sp36slXzRXUcUO9nVs3CkgFxa6U9A8GENeyxq9XQtcE912jOP4dzjDi3hlaNKbX9ijOyEh505KpqmiSYATg==
261 dependencies: 262 dependencies:
262 tslib "^2.0.0" 263 tslib "^2.0.0"
263 264
264"@angular/service-worker@^11.1.1": 265"@angular/service-worker@^11.1.1":
265 version "11.2.3" 266 version "11.2.6"
266 resolved "https://registry.yarnpkg.com/@angular/service-worker/-/service-worker-11.2.3.tgz#316bfc07ccebdc5af1a9cbc825082880c551c0b9" 267 resolved "https://registry.yarnpkg.com/@angular/service-worker/-/service-worker-11.2.6.tgz#65e895a7a1dc309c9365ea801806549f7572646c"
267 integrity sha512-/JgA4rCH2SyIK/v0+sCqNgiBEV/pXQUcUoqfm//2zfc3VwerehvF3RtRBfabtLBpdwdO5a9DZ4nX+djvTJypvw== 268 integrity sha512-nZGwVhHZ6eLptnPzIjiFiktnl4ImC+4kejR3AaElTX8PgS9TykhYhgENB+ILU49bZOGMe3RVnNthgx/JkIEgjQ==
268 dependencies: 269 dependencies:
269 tslib "^2.0.0" 270 tslib "^2.0.0"
270 271
@@ -275,10 +276,10 @@
275 dependencies: 276 dependencies:
276 "@babel/highlight" "^7.12.13" 277 "@babel/highlight" "^7.12.13"
277 278
278"@babel/compat-data@^7.12.7", "@babel/compat-data@^7.13.0": 279"@babel/compat-data@^7.12.7", "@babel/compat-data@^7.13.8":
279 version "7.13.6" 280 version "7.13.12"
280 resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.13.6.tgz#11972d07db4c2317afdbf41d6feb3a730301ef4e" 281 resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.13.12.tgz#a8a5ccac19c200f9dd49624cac6e19d7be1236a1"
281 integrity sha512-VhgqKOWYVm7lQXlvbJnWOzwfAQATd2nV52koT0HZ/LdDH0m4DUDwkKYsH+IwpXb+bKPyBJzawA4I6nBKqZcpQw== 282 integrity sha512-3eJJ841uKxeV8dcN/2yGEUy+RfgQspPEgQat85umsE1rotuquQ2AbIub4S6j7c50a2d+4myc+zSlnXeIHrOnhQ==
282 283
283"@babel/core@7.12.10": 284"@babel/core@7.12.10":
284 version "7.12.10" 285 version "7.12.10"
@@ -323,16 +324,16 @@
323 source-map "^0.5.0" 324 source-map "^0.5.0"
324 325
325"@babel/core@^7.7.5", "@babel/core@^7.8.6": 326"@babel/core@^7.7.5", "@babel/core@^7.8.6":
326 version "7.13.1" 327 version "7.13.10"
327 resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.13.1.tgz#7ddd027176debe40f13bb88bac0c21218c5b1ecf" 328 resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.13.10.tgz#07de050bbd8193fcd8a3c27918c0890613a94559"
328 integrity sha512-FzeKfFBG2rmFtGiiMdXZPFt/5R5DXubVi82uYhjGX4Msf+pgYQMCFIqFXZWs5vbIYbf14VeBIgdGI03CDOOM1w== 329 integrity sha512-bfIYcT0BdKeAZrovpMqX2Mx5NrgAckGbwT982AkdS5GNfn3KMGiprlBAtmBcFZRUmpaufS6WZFP8trvx8ptFDw==
329 dependencies: 330 dependencies:
330 "@babel/code-frame" "^7.12.13" 331 "@babel/code-frame" "^7.12.13"
331 "@babel/generator" "^7.13.0" 332 "@babel/generator" "^7.13.9"
332 "@babel/helper-compilation-targets" "^7.13.0" 333 "@babel/helper-compilation-targets" "^7.13.10"
333 "@babel/helper-module-transforms" "^7.13.0" 334 "@babel/helper-module-transforms" "^7.13.0"
334 "@babel/helpers" "^7.13.0" 335 "@babel/helpers" "^7.13.10"
335 "@babel/parser" "^7.13.0" 336 "@babel/parser" "^7.13.10"
336 "@babel/template" "^7.12.13" 337 "@babel/template" "^7.12.13"
337 "@babel/traverse" "^7.13.0" 338 "@babel/traverse" "^7.13.0"
338 "@babel/types" "^7.13.0" 339 "@babel/types" "^7.13.0"
@@ -341,7 +342,7 @@
341 gensync "^1.0.0-beta.2" 342 gensync "^1.0.0-beta.2"
342 json5 "^2.1.2" 343 json5 "^2.1.2"
343 lodash "^4.17.19" 344 lodash "^4.17.19"
344 semver "7.0.0" 345 semver "^6.3.0"
345 source-map "^0.5.0" 346 source-map "^0.5.0"
346 347
347"@babel/generator@7.12.11": 348"@babel/generator@7.12.11":
@@ -353,10 +354,10 @@
353 jsesc "^2.5.1" 354 jsesc "^2.5.1"
354 source-map "^0.5.0" 355 source-map "^0.5.0"
355 356
356"@babel/generator@^7.12.10", "@babel/generator@^7.13.0", "@babel/generator@^7.8.3": 357"@babel/generator@^7.12.10", "@babel/generator@^7.13.0", "@babel/generator@^7.13.9", "@babel/generator@^7.8.3":
357 version "7.13.0" 358 version "7.13.9"
358 resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.13.0.tgz#bd00d4394ca22f220390c56a0b5b85568ec1ec0c" 359 resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.13.9.tgz#3a7aa96f9efb8e2be42d38d80e2ceb4c64d8de39"
359 integrity sha512-zBZfgvBB/ywjx0Rgc2+BwoH/3H+lDtlgD4hBOpEv5LxRnYsm/753iRuLepqnYlynpjC3AdQxtxsoeHJoEEwOAw== 360 integrity sha512-mHOOmY0Axl/JCTkxTU6Lf5sWOg/v8nUa+Xkt4zMTftX0wqmb6Sh7J8gvcehBw7q0AhrhAR+FDacKjCZ2X8K+Sw==
360 dependencies: 361 dependencies:
361 "@babel/types" "^7.13.0" 362 "@babel/types" "^7.13.0"
362 jsesc "^2.5.1" 363 jsesc "^2.5.1"
@@ -377,20 +378,20 @@
377 "@babel/helper-explode-assignable-expression" "^7.12.13" 378 "@babel/helper-explode-assignable-expression" "^7.12.13"
378 "@babel/types" "^7.12.13" 379 "@babel/types" "^7.12.13"
379 380
380"@babel/helper-compilation-targets@^7.12.5", "@babel/helper-compilation-targets@^7.13.0": 381"@babel/helper-compilation-targets@^7.12.5", "@babel/helper-compilation-targets@^7.13.10", "@babel/helper-compilation-targets@^7.13.8":
381 version "7.13.0" 382 version "7.13.10"
382 resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.0.tgz#c9cf29b82a76fd637f0faa35544c4ace60a155a1" 383 resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.10.tgz#1310a1678cb8427c07a753750da4f8ce442bdd0c"
383 integrity sha512-SOWD0JK9+MMIhTQiUVd4ng8f3NXhPVQvTv7D3UN4wbp/6cAHnB2EmMaU1zZA2Hh1gwme+THBrVSqTFxHczTh0Q== 384 integrity sha512-/Xju7Qg1GQO4mHZ/Kcs6Au7gfafgZnwm+a7sy/ow/tV1sHeraRUHbjdat8/UvDor4Tez+siGKDk6zIKtCPKVJA==
384 dependencies: 385 dependencies:
385 "@babel/compat-data" "^7.13.0" 386 "@babel/compat-data" "^7.13.8"
386 "@babel/helper-validator-option" "^7.12.17" 387 "@babel/helper-validator-option" "^7.12.17"
387 browserslist "^4.14.5" 388 browserslist "^4.14.5"
388 semver "7.0.0" 389 semver "^6.3.0"
389 390
390"@babel/helper-create-class-features-plugin@^7.13.0": 391"@babel/helper-create-class-features-plugin@^7.13.0":
391 version "7.13.0" 392 version "7.13.11"
392 resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.13.0.tgz#28d04ad9cfbd1ed1d8b988c9ea7b945263365846" 393 resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.13.11.tgz#30d30a005bca2c953f5653fc25091a492177f4f6"
393 integrity sha512-twwzhthM4/+6o9766AW2ZBHpIHPSGrPGk1+WfHiu13u/lBnggXGNYCpeAyVfNwGDKfkhEDp+WOD/xafoJ2iLjA== 394 integrity sha512-ays0I7XYq9xbjCSvT+EvysLgfc3tOkwCULHjrnscGT3A9qD4sk3wXnJ3of0MAWsWGjdinFvajHU2smYuqXKMrw==
394 dependencies: 395 dependencies:
395 "@babel/helper-function-name" "^7.12.13" 396 "@babel/helper-function-name" "^7.12.13"
396 "@babel/helper-member-expression-to-functions" "^7.13.0" 397 "@babel/helper-member-expression-to-functions" "^7.13.0"
@@ -429,7 +430,7 @@
429 dependencies: 430 dependencies:
430 "@babel/types" "^7.12.13" 431 "@babel/types" "^7.12.13"
431 432
432"@babel/helper-hoist-variables@^7.12.13": 433"@babel/helper-hoist-variables@^7.13.0":
433 version "7.13.0" 434 version "7.13.0"
434 resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.13.0.tgz#5d5882e855b5c5eda91e0cadc26c6e7a2c8593d8" 435 resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.13.0.tgz#5d5882e855b5c5eda91e0cadc26c6e7a2c8593d8"
435 integrity sha512-0kBzvXiIKfsCA0y6cFEIJf4OdzfpRuNk4+YTeHZpGGc666SATFKTz6sRncwFnQk7/ugJ4dSrCj6iJuvW4Qwr2g== 436 integrity sha512-0kBzvXiIKfsCA0y6cFEIJf4OdzfpRuNk4+YTeHZpGGc666SATFKTz6sRncwFnQk7/ugJ4dSrCj6iJuvW4Qwr2g==
@@ -437,34 +438,33 @@
437 "@babel/traverse" "^7.13.0" 438 "@babel/traverse" "^7.13.0"
438 "@babel/types" "^7.13.0" 439 "@babel/types" "^7.13.0"
439 440
440"@babel/helper-member-expression-to-functions@^7.13.0": 441"@babel/helper-member-expression-to-functions@^7.13.0", "@babel/helper-member-expression-to-functions@^7.13.12":
441 version "7.13.0" 442 version "7.13.12"
442 resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.13.0.tgz#6aa4bb678e0f8c22f58cdb79451d30494461b091" 443 resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.13.12.tgz#dfe368f26d426a07299d8d6513821768216e6d72"
443 integrity sha512-yvRf8Ivk62JwisqV1rFRMxiSMDGnN6KH1/mDMmIrij4jztpQNRoHqqMG3U6apYbGRPJpgPalhva9Yd06HlUxJQ== 444 integrity sha512-48ql1CLL59aKbU94Y88Xgb2VFy7a95ykGRbJJaaVv+LX5U8wFpLfiGXJJGUozsmA1oEh/o5Bp60Voq7ACyA/Sw==
444 dependencies: 445 dependencies:
445 "@babel/types" "^7.13.0" 446 "@babel/types" "^7.13.12"
446 447
447"@babel/helper-module-imports@^7.12.1", "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.12.5": 448"@babel/helper-module-imports@^7.12.1", "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.12.5", "@babel/helper-module-imports@^7.13.12":
448 version "7.12.13" 449 version "7.13.12"
449 resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.12.13.tgz#ec67e4404f41750463e455cc3203f6a32e93fcb0" 450 resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz#c6a369a6f3621cb25da014078684da9196b61977"
450 integrity sha512-NGmfvRp9Rqxy0uHSSVP+SRIW1q31a7Ji10cLBcqSDUngGentY4FRiHOFZFE1CLU5eiL0oE8reH7Tg1y99TDM/g== 451 integrity sha512-4cVvR2/1B693IuOvSI20xqqa/+bl7lqAMR59R4iu39R9aOX8/JoYY1sFaNvUMyMBGnHdwvJgUrzNLoUZxXypxA==
451 dependencies: 452 dependencies:
452 "@babel/types" "^7.12.13" 453 "@babel/types" "^7.13.12"
453 454
454"@babel/helper-module-transforms@^7.12.1", "@babel/helper-module-transforms@^7.12.13", "@babel/helper-module-transforms@^7.13.0": 455"@babel/helper-module-transforms@^7.12.1", "@babel/helper-module-transforms@^7.13.0":
455 version "7.13.0" 456 version "7.13.12"
456 resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.13.0.tgz#42eb4bd8eea68bab46751212c357bfed8b40f6f1" 457 resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.13.12.tgz#600e58350490828d82282631a1422268e982ba96"
457 integrity sha512-Ls8/VBwH577+pw7Ku1QkUWIyRRNHpYlts7+qSqBBFCW3I8QteB9DxfcZ5YJpOwH6Ihe/wn8ch7fMGOP1OhEIvw== 458 integrity sha512-7zVQqMO3V+K4JOOj40kxiCrMf6xlQAkewBB0eu2b03OO/Q21ZutOzjpfD79A5gtE/2OWi1nv625MrDlGlkbknQ==
458 dependencies: 459 dependencies:
459 "@babel/helper-module-imports" "^7.12.13" 460 "@babel/helper-module-imports" "^7.13.12"
460 "@babel/helper-replace-supers" "^7.13.0" 461 "@babel/helper-replace-supers" "^7.13.12"
461 "@babel/helper-simple-access" "^7.12.13" 462 "@babel/helper-simple-access" "^7.13.12"
462 "@babel/helper-split-export-declaration" "^7.12.13" 463 "@babel/helper-split-export-declaration" "^7.12.13"
463 "@babel/helper-validator-identifier" "^7.12.11" 464 "@babel/helper-validator-identifier" "^7.12.11"
464 "@babel/template" "^7.12.13" 465 "@babel/template" "^7.12.13"
465 "@babel/traverse" "^7.13.0" 466 "@babel/traverse" "^7.13.0"
466 "@babel/types" "^7.13.0" 467 "@babel/types" "^7.13.12"
467 lodash "^4.17.19"
468 468
469"@babel/helper-optimise-call-expression@^7.12.13": 469"@babel/helper-optimise-call-expression@^7.12.13":
470 version "7.12.13" 470 version "7.12.13"
@@ -487,22 +487,22 @@
487 "@babel/helper-wrap-function" "^7.13.0" 487 "@babel/helper-wrap-function" "^7.13.0"
488 "@babel/types" "^7.13.0" 488 "@babel/types" "^7.13.0"
489 489
490"@babel/helper-replace-supers@^7.12.13", "@babel/helper-replace-supers@^7.13.0": 490"@babel/helper-replace-supers@^7.12.13", "@babel/helper-replace-supers@^7.13.0", "@babel/helper-replace-supers@^7.13.12":
491 version "7.13.0" 491 version "7.13.12"
492 resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.13.0.tgz#6034b7b51943094cb41627848cb219cb02be1d24" 492 resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.13.12.tgz#6442f4c1ad912502481a564a7386de0c77ff3804"
493 integrity sha512-Segd5me1+Pz+rmN/NFBOplMbZG3SqRJOBlY+mA0SxAv6rjj7zJqr1AVr3SfzUVTLCv7ZLU5FycOM/SBGuLPbZw== 493 integrity sha512-Gz1eiX+4yDO8mT+heB94aLVNCL+rbuT2xy4YfyNqu8F+OI6vMvJK891qGBTqL9Uc8wxEvRW92Id6G7sDen3fFw==
494 dependencies: 494 dependencies:
495 "@babel/helper-member-expression-to-functions" "^7.13.0" 495 "@babel/helper-member-expression-to-functions" "^7.13.12"
496 "@babel/helper-optimise-call-expression" "^7.12.13" 496 "@babel/helper-optimise-call-expression" "^7.12.13"
497 "@babel/traverse" "^7.13.0" 497 "@babel/traverse" "^7.13.0"
498 "@babel/types" "^7.13.0" 498 "@babel/types" "^7.13.12"
499 499
500"@babel/helper-simple-access@^7.12.13": 500"@babel/helper-simple-access@^7.12.13", "@babel/helper-simple-access@^7.13.12":
501 version "7.12.13" 501 version "7.13.12"
502 resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.12.13.tgz#8478bcc5cacf6aa1672b251c1d2dde5ccd61a6c4" 502 resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.13.12.tgz#dd6c538afb61819d205a012c31792a39c7a5eaf6"
503 integrity sha512-0ski5dyYIHEfwpWGx5GPWhH35j342JaflmCeQmsPWcrOQDtCN6C1zKAVRFVbK53lPW2c9TsuLLSUDf0tIGJ5hA== 503 integrity sha512-7FEjbrx5SL9cWvXioDbnlYTppcZGuCY6ow3/D5vMggb2Ywgu4dMrpTJX0JdQAIcRRUElOIxF3yEooa9gUb9ZbA==
504 dependencies: 504 dependencies:
505 "@babel/types" "^7.12.13" 505 "@babel/types" "^7.13.12"
506 506
507"@babel/helper-skip-transparent-expression-wrappers@^7.12.1": 507"@babel/helper-skip-transparent-expression-wrappers@^7.12.1":
508 version "7.12.1" 508 version "7.12.1"
@@ -538,37 +538,37 @@
538 "@babel/traverse" "^7.13.0" 538 "@babel/traverse" "^7.13.0"
539 "@babel/types" "^7.13.0" 539 "@babel/types" "^7.13.0"
540 540
541"@babel/helpers@^7.12.5", "@babel/helpers@^7.13.0", "@babel/helpers@^7.8.3": 541"@babel/helpers@^7.12.5", "@babel/helpers@^7.13.10", "@babel/helpers@^7.8.3":
542 version "7.13.0" 542 version "7.13.10"
543 resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.13.0.tgz#7647ae57377b4f0408bf4f8a7af01c42e41badc0" 543 resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.13.10.tgz#fd8e2ba7488533cdeac45cc158e9ebca5e3c7df8"
544 integrity sha512-aan1MeFPxFacZeSz6Ld7YZo5aPuqnKlD7+HZY75xQsueczFccP9A7V05+oe0XpLwHK3oLorPe9eaAUljL7WEaQ== 544 integrity sha512-4VO883+MWPDUVRF3PhiLBUFHoX/bsLTGFpFK/HqvvfBZz2D57u9XzPVNFVBTc0PW/CWR9BXTOKt8NF4DInUHcQ==
545 dependencies: 545 dependencies:
546 "@babel/template" "^7.12.13" 546 "@babel/template" "^7.12.13"
547 "@babel/traverse" "^7.13.0" 547 "@babel/traverse" "^7.13.0"
548 "@babel/types" "^7.13.0" 548 "@babel/types" "^7.13.0"
549 549
550"@babel/highlight@^7.12.13": 550"@babel/highlight@^7.12.13":
551 version "7.12.13" 551 version "7.13.10"
552 resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.12.13.tgz#8ab538393e00370b26271b01fa08f7f27f2e795c" 552 resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.13.10.tgz#a8b2a66148f5b27d666b15d81774347a731d52d1"
553 integrity sha512-kocDQvIbgMKlWxXe9fof3TQ+gkIPOUSEYhJjqUjvKMez3krV7vbzYCDq39Oj11UAVK7JqPVGQPlgE85dPNlQww== 553 integrity sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg==
554 dependencies: 554 dependencies:
555 "@babel/helper-validator-identifier" "^7.12.11" 555 "@babel/helper-validator-identifier" "^7.12.11"
556 chalk "^2.0.0" 556 chalk "^2.0.0"
557 js-tokens "^4.0.0" 557 js-tokens "^4.0.0"
558 558
559"@babel/parser@^7.12.10", "@babel/parser@^7.12.13", "@babel/parser@^7.12.7", "@babel/parser@^7.13.0", "@babel/parser@^7.8.3": 559"@babel/parser@^7.12.10", "@babel/parser@^7.12.13", "@babel/parser@^7.12.7", "@babel/parser@^7.13.0", "@babel/parser@^7.13.10", "@babel/parser@^7.8.3":
560 version "7.13.4" 560 version "7.13.12"
561 resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.4.tgz#340211b0da94a351a6f10e63671fa727333d13ab" 561 resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.12.tgz#ba320059420774394d3b0c0233ba40e4250b81d1"
562 integrity sha512-uvoOulWHhI+0+1f9L4BoozY7U5cIkZ9PgJqvb041d6vypgUmtVPG4vmGm4pSggjl8BELzvHyUeJSUyEMY6b+qA== 562 integrity sha512-4T7Pb244rxH24yR116LAuJ+adxXXnHhZaLJjegJVKSdoNCe4x1eDBaud5YIcQFcqzsaD5BHvJw5BQ0AZapdCRw==
563 563
564"@babel/plugin-proposal-async-generator-functions@^7.12.1": 564"@babel/plugin-proposal-async-generator-functions@^7.12.1":
565 version "7.13.5" 565 version "7.13.8"
566 resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.13.5.tgz#69e3fbb9958949b09036e27b26eba1aafa1ba3db" 566 resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.13.8.tgz#87aacb574b3bc4b5603f6fe41458d72a5a2ec4b1"
567 integrity sha512-8cErJEDzhZgNKzYyjCKsHuyPqtWxG8gc9h4OFSUDJu0vCAOsObPU2LcECnW0kJwh/b+uUz46lObVzIXw0fzAbA== 567 integrity sha512-rPBnhj+WgoSmgq+4gQUtXx/vOcU+UYtjy1AA/aeD61Hwj410fwYyqfUcRP3lR8ucgliVJL/G7sXcNUecC75IXA==
568 dependencies: 568 dependencies:
569 "@babel/helper-plugin-utils" "^7.13.0" 569 "@babel/helper-plugin-utils" "^7.13.0"
570 "@babel/helper-remap-async-to-generator" "^7.13.0" 570 "@babel/helper-remap-async-to-generator" "^7.13.0"
571 "@babel/plugin-syntax-async-generators" "^7.8.0" 571 "@babel/plugin-syntax-async-generators" "^7.8.4"
572 572
573"@babel/plugin-proposal-class-properties@^7.12.1": 573"@babel/plugin-proposal-class-properties@^7.12.1":
574 version "7.13.0" 574 version "7.13.0"
@@ -579,12 +579,12 @@
579 "@babel/helper-plugin-utils" "^7.13.0" 579 "@babel/helper-plugin-utils" "^7.13.0"
580 580
581"@babel/plugin-proposal-dynamic-import@^7.12.1": 581"@babel/plugin-proposal-dynamic-import@^7.12.1":
582 version "7.12.17" 582 version "7.13.8"
583 resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.12.17.tgz#e0ebd8db65acc37eac518fa17bead2174e224512" 583 resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.13.8.tgz#876a1f6966e1dec332e8c9451afda3bebcdf2e1d"
584 integrity sha512-ZNGoFZqrnuy9H2izB2jLlnNDAfVPlGl5NhFEiFe4D84ix9GQGygF+CWMGHKuE+bpyS/AOuDQCnkiRNqW2IzS1Q== 584 integrity sha512-ONWKj0H6+wIRCkZi9zSbZtE/r73uOhMVHh256ys0UzfM7I3d4n+spZNWjOnJv2gzopumP2Wxi186vI8N0Y2JyQ==
585 dependencies: 585 dependencies:
586 "@babel/helper-plugin-utils" "^7.12.13" 586 "@babel/helper-plugin-utils" "^7.13.0"
587 "@babel/plugin-syntax-dynamic-import" "^7.8.0" 587 "@babel/plugin-syntax-dynamic-import" "^7.8.3"
588 588
589"@babel/plugin-proposal-export-namespace-from@^7.12.1": 589"@babel/plugin-proposal-export-namespace-from@^7.12.1":
590 version "7.12.13" 590 version "7.12.13"
@@ -595,28 +595,28 @@
595 "@babel/plugin-syntax-export-namespace-from" "^7.8.3" 595 "@babel/plugin-syntax-export-namespace-from" "^7.8.3"
596 596
597"@babel/plugin-proposal-json-strings@^7.12.1": 597"@babel/plugin-proposal-json-strings@^7.12.1":
598 version "7.12.13" 598 version "7.13.8"
599 resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.12.13.tgz#ced7888a2db92a3d520a2e35eb421fdb7fcc9b5d" 599 resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.13.8.tgz#bf1fb362547075afda3634ed31571c5901afef7b"
600 integrity sha512-v9eEi4GiORDg8x+Dmi5r8ibOe0VXoKDeNPYcTTxdGN4eOWikrJfDJCJrr1l5gKGvsNyGJbrfMftC2dTL6oz7pg== 600 integrity sha512-w4zOPKUFPX1mgvTmL/fcEqy34hrQ1CRcGxdphBc6snDnnqJ47EZDIyop6IwXzAC8G916hsIuXB2ZMBCExC5k7Q==
601 dependencies: 601 dependencies:
602 "@babel/helper-plugin-utils" "^7.12.13" 602 "@babel/helper-plugin-utils" "^7.13.0"
603 "@babel/plugin-syntax-json-strings" "^7.8.0" 603 "@babel/plugin-syntax-json-strings" "^7.8.3"
604 604
605"@babel/plugin-proposal-logical-assignment-operators@^7.12.1": 605"@babel/plugin-proposal-logical-assignment-operators@^7.12.1":
606 version "7.12.13" 606 version "7.13.8"
607 resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.12.13.tgz#575b5d9a08d8299eeb4db6430da6e16e5cf14350" 607 resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.13.8.tgz#93fa78d63857c40ce3c8c3315220fd00bfbb4e1a"
608 integrity sha512-fqmiD3Lz7jVdK6kabeSr1PZlWSUVqSitmHEe3Z00dtGTKieWnX9beafvavc32kjORa5Bai4QNHgFDwWJP+WtSQ== 608 integrity sha512-aul6znYB4N4HGweImqKn59Su9RS8lbUIqxtXTOcAGtNIDczoEFv+l1EhmX8rUBp3G1jMjKJm8m0jXVp63ZpS4A==
609 dependencies: 609 dependencies:
610 "@babel/helper-plugin-utils" "^7.12.13" 610 "@babel/helper-plugin-utils" "^7.13.0"
611 "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" 611 "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4"
612 612
613"@babel/plugin-proposal-nullish-coalescing-operator@^7.12.1": 613"@babel/plugin-proposal-nullish-coalescing-operator@^7.12.1":
614 version "7.13.0" 614 version "7.13.8"
615 resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.13.0.tgz#1a96fdf2c43109cfe5568513c5379015a23f5380" 615 resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.13.8.tgz#3730a31dafd3c10d8ccd10648ed80a2ac5472ef3"
616 integrity sha512-UkAvFA/9+lBBL015gjA68NvKiCReNxqFLm3SdNKaM3XXoDisA7tMAIX4PmIwatFoFqMxxT3WyG9sK3MO0Kting== 616 integrity sha512-iePlDPBn//UhxExyS9KyeYU7RM9WScAG+D3Hhno0PLJebAEpDZMocbDe64eqynhNAnwz/vZoL/q/QB2T1OH39A==
617 dependencies: 617 dependencies:
618 "@babel/helper-plugin-utils" "^7.13.0" 618 "@babel/helper-plugin-utils" "^7.13.0"
619 "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" 619 "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3"
620 620
621"@babel/plugin-proposal-numeric-separator@^7.12.7": 621"@babel/plugin-proposal-numeric-separator@^7.12.7":
622 version "7.12.13" 622 version "7.12.13"
@@ -627,30 +627,32 @@
627 "@babel/plugin-syntax-numeric-separator" "^7.10.4" 627 "@babel/plugin-syntax-numeric-separator" "^7.10.4"
628 628
629"@babel/plugin-proposal-object-rest-spread@^7.12.1": 629"@babel/plugin-proposal-object-rest-spread@^7.12.1":
630 version "7.13.0" 630 version "7.13.8"
631 resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.13.0.tgz#8f19ad247bb96bd5ad2d4107e6eddfe0a789937b" 631 resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.13.8.tgz#5d210a4d727d6ce3b18f9de82cc99a3964eed60a"
632 integrity sha512-B4qphdSTp0nLsWcuei07JPKeZej4+Hd22MdnulJXQa1nCcGSBlk8FiqenGERaPZ+PuYhz4Li2Wjc8yfJvHgUMw== 632 integrity sha512-DhB2EuB1Ih7S3/IRX5AFVgZ16k3EzfRbq97CxAVI1KSYcW+lexV8VZb7G7L8zuPVSdQMRn0kiBpf/Yzu9ZKH0g==
633 dependencies: 633 dependencies:
634 "@babel/compat-data" "^7.13.8"
635 "@babel/helper-compilation-targets" "^7.13.8"
634 "@babel/helper-plugin-utils" "^7.13.0" 636 "@babel/helper-plugin-utils" "^7.13.0"
635 "@babel/plugin-syntax-object-rest-spread" "^7.8.0" 637 "@babel/plugin-syntax-object-rest-spread" "^7.8.3"
636 "@babel/plugin-transform-parameters" "^7.13.0" 638 "@babel/plugin-transform-parameters" "^7.13.0"
637 639
638"@babel/plugin-proposal-optional-catch-binding@^7.12.1": 640"@babel/plugin-proposal-optional-catch-binding@^7.12.1":
639 version "7.12.13" 641 version "7.13.8"
640 resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.12.13.tgz#4640520afe57728af14b4d1574ba844f263bcae5" 642 resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.13.8.tgz#3ad6bd5901506ea996fc31bdcf3ccfa2bed71107"
641 integrity sha512-9+MIm6msl9sHWg58NvqpNpLtuFbmpFYk37x8kgnGzAHvX35E1FyAwSUt5hIkSoWJFSAH+iwU8bJ4fcD1zKXOzg== 643 integrity sha512-0wS/4DUF1CuTmGo+NiaHfHcVSeSLj5S3e6RivPTg/2k3wOv3jO35tZ6/ZWsQhQMvdgI7CwphjQa/ccarLymHVA==
642 dependencies: 644 dependencies:
643 "@babel/helper-plugin-utils" "^7.12.13" 645 "@babel/helper-plugin-utils" "^7.13.0"
644 "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" 646 "@babel/plugin-syntax-optional-catch-binding" "^7.8.3"
645 647
646"@babel/plugin-proposal-optional-chaining@^7.12.7": 648"@babel/plugin-proposal-optional-chaining@^7.12.7":
647 version "7.13.0" 649 version "7.13.12"
648 resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.13.0.tgz#75b41ce0d883d19e8fe635fc3f846be3b1664f4d" 650 resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.13.12.tgz#ba9feb601d422e0adea6760c2bd6bbb7bfec4866"
649 integrity sha512-OVRQOZEBP2luZrvEbNSX5FfWDousthhdEoAOpej+Tpe58HFLvqRClT89RauIvBuCDFEip7GW1eT86/5lMy2RNA== 651 integrity sha512-fcEdKOkIB7Tf4IxrgEVeFC4zeJSTr78no9wTdBuZZbqF64kzllU0ybo2zrzm7gUQfxGhBgq4E39oRs8Zx/RMYQ==
650 dependencies: 652 dependencies:
651 "@babel/helper-plugin-utils" "^7.13.0" 653 "@babel/helper-plugin-utils" "^7.13.0"
652 "@babel/helper-skip-transparent-expression-wrappers" "^7.12.1" 654 "@babel/helper-skip-transparent-expression-wrappers" "^7.12.1"
653 "@babel/plugin-syntax-optional-chaining" "^7.8.0" 655 "@babel/plugin-syntax-optional-chaining" "^7.8.3"
654 656
655"@babel/plugin-proposal-private-methods@^7.12.1": 657"@babel/plugin-proposal-private-methods@^7.12.1":
656 version "7.13.0" 658 version "7.13.0"
@@ -668,7 +670,7 @@
668 "@babel/helper-create-regexp-features-plugin" "^7.12.13" 670 "@babel/helper-create-regexp-features-plugin" "^7.12.13"
669 "@babel/helper-plugin-utils" "^7.12.13" 671 "@babel/helper-plugin-utils" "^7.12.13"
670 672
671"@babel/plugin-syntax-async-generators@^7.8.0": 673"@babel/plugin-syntax-async-generators@^7.8.0", "@babel/plugin-syntax-async-generators@^7.8.4":
672 version "7.8.4" 674 version "7.8.4"
673 resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" 675 resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d"
674 integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== 676 integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==
@@ -682,7 +684,7 @@
682 dependencies: 684 dependencies:
683 "@babel/helper-plugin-utils" "^7.12.13" 685 "@babel/helper-plugin-utils" "^7.12.13"
684 686
685"@babel/plugin-syntax-dynamic-import@^7.8.0": 687"@babel/plugin-syntax-dynamic-import@^7.8.0", "@babel/plugin-syntax-dynamic-import@^7.8.3":
686 version "7.8.3" 688 version "7.8.3"
687 resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" 689 resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3"
688 integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== 690 integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==
@@ -696,7 +698,7 @@
696 dependencies: 698 dependencies:
697 "@babel/helper-plugin-utils" "^7.8.3" 699 "@babel/helper-plugin-utils" "^7.8.3"
698 700
699"@babel/plugin-syntax-json-strings@^7.8.0": 701"@babel/plugin-syntax-json-strings@^7.8.0", "@babel/plugin-syntax-json-strings@^7.8.3":
700 version "7.8.3" 702 version "7.8.3"
701 resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" 703 resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a"
702 integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== 704 integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==
@@ -710,7 +712,7 @@
710 dependencies: 712 dependencies:
711 "@babel/helper-plugin-utils" "^7.10.4" 713 "@babel/helper-plugin-utils" "^7.10.4"
712 714
713"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.0": 715"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.0", "@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3":
714 version "7.8.3" 716 version "7.8.3"
715 resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" 717 resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9"
716 integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== 718 integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==
@@ -724,21 +726,21 @@
724 dependencies: 726 dependencies:
725 "@babel/helper-plugin-utils" "^7.10.4" 727 "@babel/helper-plugin-utils" "^7.10.4"
726 728
727"@babel/plugin-syntax-object-rest-spread@^7.8.0": 729"@babel/plugin-syntax-object-rest-spread@^7.8.0", "@babel/plugin-syntax-object-rest-spread@^7.8.3":
728 version "7.8.3" 730 version "7.8.3"
729 resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" 731 resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871"
730 integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== 732 integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==
731 dependencies: 733 dependencies:
732 "@babel/helper-plugin-utils" "^7.8.0" 734 "@babel/helper-plugin-utils" "^7.8.0"
733 735
734"@babel/plugin-syntax-optional-catch-binding@^7.8.0": 736"@babel/plugin-syntax-optional-catch-binding@^7.8.0", "@babel/plugin-syntax-optional-catch-binding@^7.8.3":
735 version "7.8.3" 737 version "7.8.3"
736 resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" 738 resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1"
737 integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== 739 integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==
738 dependencies: 740 dependencies:
739 "@babel/helper-plugin-utils" "^7.8.0" 741 "@babel/helper-plugin-utils" "^7.8.0"
740 742
741"@babel/plugin-syntax-optional-chaining@^7.8.0": 743"@babel/plugin-syntax-optional-chaining@^7.8.0", "@babel/plugin-syntax-optional-chaining@^7.8.3":
742 version "7.8.3" 744 version "7.8.3"
743 resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" 745 resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a"
744 integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== 746 integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==
@@ -880,9 +882,9 @@
880 babel-plugin-dynamic-import-node "^2.3.3" 882 babel-plugin-dynamic-import-node "^2.3.3"
881 883
882"@babel/plugin-transform-modules-commonjs@^7.12.1": 884"@babel/plugin-transform-modules-commonjs@^7.12.1":
883 version "7.13.0" 885 version "7.13.8"
884 resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.13.0.tgz#276932693a20d12c9776093fdc99c0d9995e34c6" 886 resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.13.8.tgz#7b01ad7c2dcf2275b06fa1781e00d13d420b3e1b"
885 integrity sha512-j7397PkIB4lcn25U2dClK6VLC6pr2s3q+wbE8R3vJvY6U1UTBBj0n6F+5v6+Fd/UwfDPAorMOs2TV+T4M+owpQ== 887 integrity sha512-9QiOx4MEGglfYZ4XOnU79OHr6vIWUakIj9b4mioN8eQIoEh+pf5p/zEB36JpDFWA12nNMiRf7bfoRvl9Rn79Bw==
886 dependencies: 888 dependencies:
887 "@babel/helper-module-transforms" "^7.13.0" 889 "@babel/helper-module-transforms" "^7.13.0"
888 "@babel/helper-plugin-utils" "^7.13.0" 890 "@babel/helper-plugin-utils" "^7.13.0"
@@ -890,13 +892,13 @@
890 babel-plugin-dynamic-import-node "^2.3.3" 892 babel-plugin-dynamic-import-node "^2.3.3"
891 893
892"@babel/plugin-transform-modules-systemjs@^7.12.1": 894"@babel/plugin-transform-modules-systemjs@^7.12.1":
893 version "7.12.13" 895 version "7.13.8"
894 resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.12.13.tgz#351937f392c7f07493fc79b2118201d50404a3c5" 896 resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.13.8.tgz#6d066ee2bff3c7b3d60bf28dec169ad993831ae3"
895 integrity sha512-aHfVjhZ8QekaNF/5aNdStCGzwTbU7SI5hUybBKlMzqIMC7w7Ho8hx5a4R/DkTHfRfLwHGGxSpFt9BfxKCoXKoA== 897 integrity sha512-hwqctPYjhM6cWvVIlOIe27jCIBgHCsdH2xCJVAYQm7V5yTMoilbVMi9f6wKg0rpQAOn6ZG4AOyvCqFF/hUh6+A==
896 dependencies: 898 dependencies:
897 "@babel/helper-hoist-variables" "^7.12.13" 899 "@babel/helper-hoist-variables" "^7.13.0"
898 "@babel/helper-module-transforms" "^7.12.13" 900 "@babel/helper-module-transforms" "^7.13.0"
899 "@babel/helper-plugin-utils" "^7.12.13" 901 "@babel/helper-plugin-utils" "^7.13.0"
900 "@babel/helper-validator-identifier" "^7.12.11" 902 "@babel/helper-validator-identifier" "^7.12.11"
901 babel-plugin-dynamic-import-node "^2.3.3" 903 babel-plugin-dynamic-import-node "^2.3.3"
902 904
@@ -1109,9 +1111,9 @@
1109 regenerator-runtime "^0.13.4" 1111 regenerator-runtime "^0.13.4"
1110 1112
1111"@babel/runtime@^7.12.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": 1113"@babel/runtime@^7.12.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2":
1112 version "7.13.7" 1114 version "7.13.10"
1113 resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.7.tgz#d494e39d198ee9ca04f4dcb76d25d9d7a1dc961a" 1115 resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.10.tgz#47d42a57b6095f4468da440388fdbad8bebf0d7d"
1114 integrity sha512-h+ilqoX998mRVM5FtB5ijRuHUDVt5l3yfoOi2uh18Z/O3hvyaHQ39NpxVkCIG5yFs+mLq/ewFp8Bss6zmWv6ZA== 1116 integrity sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==
1115 dependencies: 1117 dependencies:
1116 regenerator-runtime "^0.13.4" 1118 regenerator-runtime "^0.13.4"
1117 1119
@@ -1148,16 +1150,16 @@
1148 globals "^11.1.0" 1150 globals "^11.1.0"
1149 lodash "^4.17.19" 1151 lodash "^4.17.19"
1150 1152
1151"@babel/types@^7.12.1", "@babel/types@^7.12.10", "@babel/types@^7.12.11", "@babel/types@^7.12.13", "@babel/types@^7.12.7", "@babel/types@^7.13.0", "@babel/types@^7.4.4", "@babel/types@^7.8.3", "@babel/types@^7.8.6": 1153"@babel/types@^7.12.1", "@babel/types@^7.12.10", "@babel/types@^7.12.11", "@babel/types@^7.12.13", "@babel/types@^7.12.7", "@babel/types@^7.13.0", "@babel/types@^7.13.12", "@babel/types@^7.4.4", "@babel/types@^7.8.3", "@babel/types@^7.8.6":
1152 version "7.13.0" 1154 version "7.13.12"
1153 resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.0.tgz#74424d2816f0171b4100f0ab34e9a374efdf7f80" 1155 resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.12.tgz#edbf99208ef48852acdff1c8a681a1e4ade580cd"
1154 integrity sha512-hE+HE8rnG1Z6Wzo+MhaKE5lM5eMx71T4EHJgku2E3xIfaULhDcxiiRxUYgwX8qwP1BBSlag+TdGOt6JAidIZTA== 1156 integrity sha512-K4nY2xFN4QMvQwkQ+zmBDp6ANMbVNw6BbxWmYA4qNjhR9W+Lj/8ky5MEY2Me5r+B2c6/v6F53oMndG+f9s3IiA==
1155 dependencies: 1157 dependencies:
1156 "@babel/helper-validator-identifier" "^7.12.11" 1158 "@babel/helper-validator-identifier" "^7.12.11"
1157 lodash "^4.17.19" 1159 lodash "^4.17.19"
1158 to-fast-properties "^2.0.0" 1160 to-fast-properties "^2.0.0"
1159 1161
1160"@discoveryjs/json-ext@^0.5.0": 1162"@discoveryjs/json-ext@0.5.2", "@discoveryjs/json-ext@^0.5.0":
1161 version "0.5.2" 1163 version "0.5.2"
1162 resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.2.tgz#8f03a22a04de437254e8ce8cc84ba39689288752" 1164 resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.2.tgz#8f03a22a04de437254e8ce8cc84ba39689288752"
1163 integrity sha512-HyYEUDeIj5rRQU2Hk5HTB2uHsbRQpF70nvMhVzi+VJR0X+xNEhjPui4/kBf3VeH/wqD28PT4sVOm8qqLjBrSZg== 1165 integrity sha512-HyYEUDeIj5rRQU2Hk5HTB2uHsbRQpF70nvMhVzi+VJR0X+xNEhjPui4/kBf3VeH/wqD28PT4sVOm8qqLjBrSZg==
@@ -1197,12 +1199,12 @@
1197 dependencies: 1199 dependencies:
1198 tslib "^2.0.0" 1200 tslib "^2.0.0"
1199 1201
1200"@ngtools/webpack@11.2.2": 1202"@ngtools/webpack@11.2.5":
1201 version "11.2.2" 1203 version "11.2.5"
1202 resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-11.2.2.tgz#647862ed19761796c7f84d5fb3305661d2a3af67" 1204 resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-11.2.5.tgz#3e2265145d19fcdda9ec2894ccded83658b1fa66"
1203 integrity sha512-X1M/Xs0kLi9FrOIU6yJ74q3pCzhgwPQowO1XjJ68KLOoMbj/DM6Qm0Hi9N0Ay8h0s7BIdjKEu/C3pCdGu1Q54w== 1205 integrity sha512-7fhg8hvqTiTS5ESiEN4xR2qRnOVX0rhVSckMXbAFvNYTwQOuS865RiBrYCJ4CsKhGJ9P7XS5i2EIwA3/aLSivg==
1204 dependencies: 1206 dependencies:
1205 "@angular-devkit/core" "11.2.2" 1207 "@angular-devkit/core" "11.2.5"
1206 enhanced-resolve "5.7.0" 1208 enhanced-resolve "5.7.0"
1207 webpack-sources "2.2.0" 1209 webpack-sources "2.2.0"
1208 1210
@@ -1331,15 +1333,14 @@
1331 infer-owner "^1.0.4" 1333 infer-owner "^1.0.4"
1332 1334
1333"@npmcli/run-script@^1.3.0": 1335"@npmcli/run-script@^1.3.0":
1334 version "1.8.3" 1336 version "1.8.4"
1335 resolved "https://registry.yarnpkg.com/@npmcli/run-script/-/run-script-1.8.3.tgz#07f440ed492400bb1114369bc37315eeaaae2bb3" 1337 resolved "https://registry.yarnpkg.com/@npmcli/run-script/-/run-script-1.8.4.tgz#03ced92503a6fe948cbc0975ce39210bc5e824d6"
1336 integrity sha512-ELPGWAVU/xyU+A+H3pEPj0QOvYwLTX71RArXcClFzeiyJ/b/McsZ+d0QxpznvfFtZzxGN/gz/1cvlqICR4/suQ== 1338 integrity sha512-Yd9HXTtF1JGDXZw0+SOn+mWLYS0e7bHBHVC/2C8yqs4wUrs/k8rwBSinD7rfk+3WG/MFGRZKxjyoD34Pch2E/A==
1337 dependencies: 1339 dependencies:
1338 "@npmcli/node-gyp" "^1.0.2" 1340 "@npmcli/node-gyp" "^1.0.2"
1339 "@npmcli/promise-spawn" "^1.3.2" 1341 "@npmcli/promise-spawn" "^1.3.2"
1340 infer-owner "^1.0.4" 1342 infer-owner "^1.0.4"
1341 node-gyp "^7.1.0" 1343 node-gyp "^7.1.0"
1342 puka "^1.0.1"
1343 read-package-json-fast "^2.0.1" 1344 read-package-json-fast "^2.0.1"
1344 1345
1345"@polka/url@^1.0.0-next.9": 1346"@polka/url@^1.0.0-next.9":
@@ -1347,22 +1348,22 @@
1347 resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.11.tgz#aeb16f50649a91af79dbe36574b66d0f9e4d9f71" 1348 resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.11.tgz#aeb16f50649a91af79dbe36574b66d0f9e4d9f71"
1348 integrity sha512-3NsZsJIA/22P3QUyrEDNA2D133H4j224twJrdipXN38dpnIOzAbUDtOwkcJ5pXmn75w7LSQDjA4tO9dm1XlqlA== 1349 integrity sha512-3NsZsJIA/22P3QUyrEDNA2D133H4j224twJrdipXN38dpnIOzAbUDtOwkcJ5pXmn75w7LSQDjA4tO9dm1XlqlA==
1349 1350
1350"@schematics/angular@11.2.2": 1351"@schematics/angular@11.2.5":
1351 version "11.2.2" 1352 version "11.2.5"
1352 resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-11.2.2.tgz#ff69a66b6e1acf5aa36ed0795973f3f57d893d0b" 1353 resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-11.2.5.tgz#c984687c95be32d3fa6016faa8b5a61715a830f5"
1353 integrity sha512-TcxPy58adUnkirGXyZVVSMuKkA0eIz2PWSQWEgB9l7kO+5LvDOn+RMoc6AVx0s/bU9nH+eozBUJ1XAD/E8QnYQ== 1354 integrity sha512-pjaK0gZyqhzgAVxMKElG6cDpAvNZ3adVCTA8dhEixpH+JaQdoczl59hMn7rH75yQW0PApe+8g7HMwVK6bLRmxQ==
1354 dependencies: 1355 dependencies:
1355 "@angular-devkit/core" "11.2.2" 1356 "@angular-devkit/core" "11.2.5"
1356 "@angular-devkit/schematics" "11.2.2" 1357 "@angular-devkit/schematics" "11.2.5"
1357 jsonc-parser "3.0.0" 1358 jsonc-parser "3.0.0"
1358 1359
1359"@schematics/update@0.1102.2": 1360"@schematics/update@0.1102.5":
1360 version "0.1102.2" 1361 version "0.1102.5"
1361 resolved "https://registry.yarnpkg.com/@schematics/update/-/update-0.1102.2.tgz#f8aed68bbcefdc8633c7804e47ff891ef06bd5ef" 1362 resolved "https://registry.yarnpkg.com/@schematics/update/-/update-0.1102.5.tgz#538493f0a7d06d794d521cca4f2ff588f05cc733"
1362 integrity sha512-Nz8kjeixzDnOw00bnZznq3qrbIv8yWEWNb9eDkRBqgOUXQwlhKJY/sYBK58JF2D+conaRVuEqMsBlX08GlFtIA== 1363 integrity sha512-iz9pM8mabieqQnPZjrqP5jfRFvPm81/uIg46kY3KjtDtSBi4GAF2dnFyX1dC2mG1rq+e+8zeQLvOvhdLifYlEA==
1363 dependencies: 1364 dependencies:
1364 "@angular-devkit/core" "11.2.2" 1365 "@angular-devkit/core" "11.2.5"
1365 "@angular-devkit/schematics" "11.2.2" 1366 "@angular-devkit/schematics" "11.2.5"
1366 "@yarnpkg/lockfile" "1.1.0" 1367 "@yarnpkg/lockfile" "1.1.0"
1367 ini "2.0.0" 1368 ini "2.0.0"
1368 npm-package-arg "^8.0.0" 1369 npm-package-arg "^8.0.0"
@@ -1388,9 +1389,9 @@
1388 "@types/node" "*" 1389 "@types/node" "*"
1389 1390
1390"@types/chart.js@^2.9.16": 1391"@types/chart.js@^2.9.16":
1391 version "2.9.30" 1392 version "2.9.31"
1392 resolved "https://registry.yarnpkg.com/@types/chart.js/-/chart.js-2.9.30.tgz#34b99897f4f5ef0f74c8fe4ced70ac52b4d752dd" 1393 resolved "https://registry.yarnpkg.com/@types/chart.js/-/chart.js-2.9.31.tgz#e8ebc7ed18eb0e5114c69bd46ef8e0037c89d39d"
1393 integrity sha512-EgjxUUZFvf6ls3kW2CwyrnSJhgyKxgwrlp/W5G9wqyPEO9iFatO63zAA7L24YqgMxiDjQ+tG7ODU+2yWH91lPg== 1394 integrity sha512-hzS6phN/kx3jClk3iYqEHNnYIRSi4RZrIGJ8CDLjgatpHoftCezvC44uqB3o3OUm9ftU1m7sHG8+RLyPTlACrA==
1394 dependencies: 1395 dependencies:
1395 moment "^2.10.2" 1396 moment "^2.10.2"
1396 1397
@@ -1443,9 +1444,9 @@
1443 integrity sha512-giAlZwstKbmvMk1OO7WXSj4OZ0keXAcl2TQq4LWHiiPH2ByaH7WeUzng+Qej8UPxxv+8lRTuouo0iaNDBuzIBA== 1444 integrity sha512-giAlZwstKbmvMk1OO7WXSj4OZ0keXAcl2TQq4LWHiiPH2ByaH7WeUzng+Qej8UPxxv+8lRTuouo0iaNDBuzIBA==
1444 1445
1445"@types/jasmine@*", "@types/jasmine@^3.3.15": 1446"@types/jasmine@*", "@types/jasmine@^3.3.15":
1446 version "3.6.4" 1447 version "3.6.7"
1447 resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.6.4.tgz#22ade1b692d5656f859ef9bc6c62d88632cc27e0" 1448 resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.6.7.tgz#e762d3ead78538efb7900ab932d7daf334acb0b4"
1448 integrity sha512-CTdMERA4iGNcxeqzD7pavb4WLIFq6bGnx6nIJD+1D4Knx24GE6QBPrWVhO8UlIy7gf7rbIt3ZD7iIzryRD2TgA== 1449 integrity sha512-8dtfiykrpe4Ysn6ONj0tOjmpDIh1vWxPk80eutSeWmyaJvAZXZ84219fS4gLrvz05eidhp7BP17WVQBaXHSyXQ==
1449 1450
1450"@types/jasminewd2@^2.0.3": 1451"@types/jasminewd2@^2.0.3":
1451 version "2.0.8" 1452 version "2.0.8"
@@ -1465,9 +1466,9 @@
1465 integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA== 1466 integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==
1466 1467
1467"@types/linkify-it@*": 1468"@types/linkify-it@*":
1468 version "3.0.0" 1469 version "3.0.1"
1469 resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.0.tgz#c0ca4c253664492dbf47a646f31cfd483a6bbc95" 1470 resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-3.0.1.tgz#4d26a9efe3aa2caf829234ec5a39580fc88b6001"
1470 integrity sha512-x9OaQQTb1N2hPZ/LWJsqushexDvz7NgzuZxiRmZio44WPuolTZNHDBCrOxCzRVOMwamJRO2dWax5NbygOf1OTQ== 1471 integrity sha512-pQv3Sygwxxh6jYQzXaiyWDAHevJqWtqDUv6t11Sa9CPGiXny66II7Pl6PR8QO5OVysD6HYOkHMeBgIjLnk9SkQ==
1471 1472
1472"@types/linkifyjs@^2.1.2": 1473"@types/linkifyjs@^2.1.2":
1473 version "2.1.3" 1474 version "2.1.3"
@@ -1519,10 +1520,10 @@
1519 resolved "https://registry.yarnpkg.com/@types/mousetrap/-/mousetrap-1.6.3.tgz#3159a01a2b21c9155a3d8f85588885d725dc987d" 1520 resolved "https://registry.yarnpkg.com/@types/mousetrap/-/mousetrap-1.6.3.tgz#3159a01a2b21c9155a3d8f85588885d725dc987d"
1520 integrity sha512-13gmo3M2qVvjQrWNseqM3+cR6S2Ss3grbR2NZltgMq94wOwqJYQdgn8qzwDshzgXqMlSUtyPZjysImmktu22ew== 1521 integrity sha512-13gmo3M2qVvjQrWNseqM3+cR6S2Ss3grbR2NZltgMq94wOwqJYQdgn8qzwDshzgXqMlSUtyPZjysImmktu22ew==
1521 1522
1522"@types/node@*", "@types/node@^14.0.14", "@types/node@^14.14.10": 1523"@types/node@*", "@types/node@>=10.0.0", "@types/node@^14.0.14":
1523 version "14.14.31" 1524 version "14.14.35"
1524 resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.31.tgz#72286bd33d137aa0d152d47ec7c1762563d34055" 1525 resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.35.tgz#42c953a4e2b18ab931f72477e7012172f4ffa313"
1525 integrity sha512-vFHy/ezP5qI0rFgJ7aQnjDXwAMrG0KqqIH7tQG5PPv3BWBayOPIQNBjVc/P6hhdZfMx51REc6tfDNXHUio893g== 1526 integrity sha512-Lt+wj8NVPx0zUmUwumiVXapmaLUcAk3yPuHCFVXras9k5VT9TdhJqKqGVUQCD60OTMCl0qxJ57OiTL0Mic3Iag==
1526 1527
1527"@types/parse-json@^4.0.0": 1528"@types/parse-json@^4.0.0":
1528 version "4.0.0" 1529 version "4.0.0"
@@ -1561,11 +1562,12 @@
1561 integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug== 1562 integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==
1562 1563
1563"@types/react@*": 1564"@types/react@*":
1564 version "17.0.2" 1565 version "17.0.3"
1565 resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.2.tgz#3de24c4efef902dd9795a49c75f760cbe4f7a5a8" 1566 resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.3.tgz#ba6e215368501ac3826951eef2904574c262cc79"
1566 integrity sha512-Xt40xQsrkdvjn1EyWe1Bc0dJLcil/9x2vAuW7ya+PuQip4UYUaXyhzWmAbwRsdMgwOFHpfp7/FFZebDU6Y8VHA== 1567 integrity sha512-wYOUxIgs2HZZ0ACNiIayItyluADNbONl7kt8lkLjVK8IitMH5QMyAh75Fwhmo37r1m7L2JaFj03sIfxBVDvRAg==
1567 dependencies: 1568 dependencies:
1568 "@types/prop-types" "*" 1569 "@types/prop-types" "*"
1570 "@types/scheduler" "*"
1569 csstype "^3.0.2" 1571 csstype "^3.0.2"
1570 1572
1571"@types/sanitize-html@1.27.1": 1573"@types/sanitize-html@1.27.1":
@@ -1575,6 +1577,11 @@
1575 dependencies: 1577 dependencies:
1576 htmlparser2 "^4.1.0" 1578 htmlparser2 "^4.1.0"
1577 1579
1580"@types/scheduler@*":
1581 version "0.16.1"
1582 resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275"
1583 integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==
1584
1578"@types/selenium-webdriver@^3.0.0": 1585"@types/selenium-webdriver@^3.0.0":
1579 version "3.0.17" 1586 version "3.0.17"
1580 resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-3.0.17.tgz#50bea0c3c2acc31c959c5b1e747798b3b3d06d4b" 1587 resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-3.0.17.tgz#50bea0c3c2acc31c959c5b1e747798b3b3d06d4b"
@@ -1605,9 +1612,9 @@
1605 integrity sha512-W+bw9ds02rAQaMvaLYxAbJ6cvguW/iJXNT6lTssS1ps6QdrMKttqEAMEG/b5CR8TZl3/L7/lH0ZV5nNR1LXikA== 1612 integrity sha512-W+bw9ds02rAQaMvaLYxAbJ6cvguW/iJXNT6lTssS1ps6QdrMKttqEAMEG/b5CR8TZl3/L7/lH0ZV5nNR1LXikA==
1606 1613
1607"@types/uglify-js@*": 1614"@types/uglify-js@*":
1608 version "3.12.0" 1615 version "3.13.0"
1609 resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.12.0.tgz#2bb061c269441620d46b946350c8f16d52ef37c5" 1616 resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.13.0.tgz#1cad8df1fb0b143c5aba08de5712ea9d1ff71124"
1610 integrity sha512-sYAF+CF9XZ5cvEBkI7RtrG9g2GtMBkviTnBxYYyq+8BWvO4QtXfwwR6a2LFwCi4evMKZfpv6U43ViYvv17Wz3Q== 1617 integrity sha512-EGkrJD5Uy+Pg0NUR8uA4bJ5WMfljyad0G+784vLCNUkD+QwOJXUbBYExXfVGf7YtyzdQp3L/XMYcliB987kL5Q==
1611 dependencies: 1618 dependencies:
1612 source-map "^0.6.1" 1619 source-map "^0.6.1"
1613 1620
@@ -1925,9 +1932,9 @@ acorn@^6.4.1:
1925 integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== 1932 integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==
1926 1933
1927acorn@^8.0.4: 1934acorn@^8.0.4:
1928 version "8.0.5" 1935 version "8.1.0"
1929 resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.0.5.tgz#a3bfb872a74a6a7f661bc81b9849d9cac12601b7" 1936 resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.1.0.tgz#52311fd7037ae119cbb134309e901aa46295b3fe"
1930 integrity sha512-v+DieK/HJkJOpFBETDJioequtc3PfxsWMaxIdIwujtF7FEV/MAyDQLlm6/zPvr7Mix07mLh6ccVwIsloceodlg== 1937 integrity sha512-LWCF/Wn0nfHOmJ9rzQApGnxnvgfROzGilS8936rqN/lfcYkY9MYZzdMqN+2NJ4SlTc+m5HiSa+kNfDtI64dwUA==
1931 1938
1932addr-to-ip-port@^1.0.1, addr-to-ip-port@^1.5.1: 1939addr-to-ip-port@^1.0.1, addr-to-ip-port@^1.5.1:
1933 version "1.5.1" 1940 version "1.5.1"
@@ -2793,7 +2800,7 @@ bytes@3.1.0:
2793 resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" 2800 resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
2794 integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== 2801 integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
2795 2802
2796cacache@15.0.5, cacache@^15.0.5: 2803cacache@15.0.5:
2797 version "15.0.5" 2804 version "15.0.5"
2798 resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.0.5.tgz#69162833da29170d6732334643c60e005f5f17d0" 2805 resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.0.5.tgz#69162833da29170d6732334643c60e005f5f17d0"
2799 integrity sha512-lloiL22n7sOjEEXdL8NAjTgv9a1u43xICE9/203qonkZUCj5X1UEWIdf2/Y0d6QcCtMzbKQyhrcDbdvlZTs/+A== 2806 integrity sha512-lloiL22n7sOjEEXdL8NAjTgv9a1u43xICE9/203qonkZUCj5X1UEWIdf2/Y0d6QcCtMzbKQyhrcDbdvlZTs/+A==
@@ -2837,6 +2844,29 @@ cacache@^12.0.2:
2837 unique-filename "^1.1.1" 2844 unique-filename "^1.1.1"
2838 y18n "^4.0.0" 2845 y18n "^4.0.0"
2839 2846
2847cacache@^15.0.5:
2848 version "15.0.6"
2849 resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.0.6.tgz#65a8c580fda15b59150fb76bf3f3a8e45d583099"
2850 integrity sha512-g1WYDMct/jzW+JdWEyjaX2zoBkZ6ZT9VpOyp2I/VMtDsNLffNat3kqPFfi1eDRSK9/SuKGyORDHcQMcPF8sQ/w==
2851 dependencies:
2852 "@npmcli/move-file" "^1.0.1"
2853 chownr "^2.0.0"
2854 fs-minipass "^2.0.0"
2855 glob "^7.1.4"
2856 infer-owner "^1.0.4"
2857 lru-cache "^6.0.0"
2858 minipass "^3.1.1"
2859 minipass-collect "^1.0.2"
2860 minipass-flush "^1.0.5"
2861 minipass-pipeline "^1.2.2"
2862 mkdirp "^1.0.3"
2863 p-map "^4.0.0"
2864 promise-inflight "^1.0.1"
2865 rimraf "^3.0.2"
2866 ssri "^8.0.1"
2867 tar "^6.0.2"
2868 unique-filename "^1.1.1"
2869
2840cache-base@^1.0.1: 2870cache-base@^1.0.1:
2841 version "1.0.1" 2871 version "1.0.1"
2842 resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" 2872 resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
@@ -2937,9 +2967,9 @@ caniuse-api@^3.0.0:
2937 lodash.uniq "^4.5.0" 2967 lodash.uniq "^4.5.0"
2938 2968
2939caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001032, caniuse-lite@^1.0.30001181: 2969caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001032, caniuse-lite@^1.0.30001181:
2940 version "1.0.30001192" 2970 version "1.0.30001204"
2941 resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001192.tgz#b848ebc0ab230cf313d194a4775a30155d50ae40" 2971 resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001204.tgz#256c85709a348ec4d175e847a3b515c66e79f2aa"
2942 integrity sha512-63OrUnwJj5T1rUmoyqYTdRWBqFFxZFlyZnRRjDR8NSUQFB6A+j/uBORU/SyJ5WzDLg4SPiZH40hQCBNdZ/jmAw== 2972 integrity sha512-JUdjWpcxfJ9IPamy2f5JaRDCaqJOxDzOSKtbdx4rH9VivMd1vIzoPumsJa9LoMIi4Fx2BV2KZOxWhNkBjaYivQ==
2943 2973
2944canonical-path@1.0.0: 2974canonical-path@1.0.0:
2945 version "1.0.0" 2975 version "1.0.0"
@@ -2971,7 +3001,7 @@ chalk@^2.0.0, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2:
2971 escape-string-regexp "^1.0.5" 3001 escape-string-regexp "^1.0.5"
2972 supports-color "^5.3.0" 3002 supports-color "^5.3.0"
2973 3003
2974chalk@^4.0.0, chalk@^4.1.0: 3004chalk@^4.1.0:
2975 version "4.1.0" 3005 version "4.1.0"
2976 resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" 3006 resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a"
2977 integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== 3007 integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==
@@ -3081,9 +3111,9 @@ chrome-trace-event@^1.0.2:
3081 tslib "^1.9.0" 3111 tslib "^1.9.0"
3082 3112
3083chunk-store-stream@^4.1.1: 3113chunk-store-stream@^4.1.1:
3084 version "4.2.0" 3114 version "4.3.0"
3085 resolved "https://registry.yarnpkg.com/chunk-store-stream/-/chunk-store-stream-4.2.0.tgz#18f673c495946c4cdcf14124a3ebd5f31eb0ea35" 3115 resolved "https://registry.yarnpkg.com/chunk-store-stream/-/chunk-store-stream-4.3.0.tgz#3de5f4dfe19729366c29bb7ed52d139f9af29f0e"
3086 integrity sha512-90iueoPoqT2isnmy1fyqwzgFy5FokuaxQuijOQG1VgC/6DaXRfeYN0da8iWENkzqElWhqLxo8pWc7pH9dmxlcA== 3116 integrity sha512-qby+/RXoiMoTVtPiylWZt7KFF1jy6M829TzMi2hxZtBIH9ptV19wxcft6zGiXLokJgCbuZPGNGab6DWHqiSEKw==
3087 dependencies: 3117 dependencies:
3088 block-stream2 "^2.0.0" 3118 block-stream2 "^2.0.0"
3089 readable-stream "^3.6.0" 3119 readable-stream "^3.6.0"
@@ -3143,9 +3173,9 @@ cli-cursor@^3.1.0:
3143 restore-cursor "^3.1.0" 3173 restore-cursor "^3.1.0"
3144 3174
3145cli-spinners@^2.5.0: 3175cli-spinners@^2.5.0:
3146 version "2.5.0" 3176 version "2.6.0"
3147 resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.5.0.tgz#12763e47251bf951cb75c201dfa58ff1bcb2d047" 3177 resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.0.tgz#36c7dc98fb6a9a76bd6238ec3f77e2425627e939"
3148 integrity sha512-PC+AmIuK04E6aeSs/pUccSujsTzBhu4HzC2dL+CfJB/Jcc2qTRbEwZQDfIUpt2Xl8BodYBEq8w4fc0kU2I9DjQ== 3178 integrity sha512-t+4/y50K/+4xcCRosKkA7W4gTr1MySvLV0q+PxmG7FJ5g+66ChKurYjxBCjHggHH3HA5Hh9cy+lcUGWDqVH+4Q==
3149 3179
3150cli-width@^2.0.0: 3180cli-width@^2.0.0:
3151 version "2.2.1" 3181 version "2.2.1"
@@ -3279,9 +3309,9 @@ color-name@^1.0.0, color-name@~1.1.4:
3279 integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== 3309 integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
3280 3310
3281color-string@^1.5.4: 3311color-string@^1.5.4:
3282 version "1.5.4" 3312 version "1.5.5"
3283 resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.4.tgz#dd51cd25cfee953d138fe4002372cc3d0e504cb6" 3313 resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.5.tgz#65474a8f0e7439625f3d27a6a19d89fc45223014"
3284 integrity sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw== 3314 integrity sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg==
3285 dependencies: 3315 dependencies:
3286 color-name "^1.0.0" 3316 color-name "^1.0.0"
3287 simple-swizzle "^0.2.2" 3317 simple-swizzle "^0.2.2"
@@ -3294,10 +3324,10 @@ color@^3.0.0:
3294 color-convert "^1.9.1" 3324 color-convert "^1.9.1"
3295 color-string "^1.5.4" 3325 color-string "^1.5.4"
3296 3326
3297colorette@^1.2.1: 3327colorette@^1.2.1, colorette@^1.2.2:
3298 version "1.2.1" 3328 version "1.2.2"
3299 resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" 3329 resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94"
3300 integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== 3330 integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==
3301 3331
3302colors@1.4.0, colors@^1.4.0: 3332colors@1.4.0, colors@^1.4.0:
3303 version "1.4.0" 3333 version "1.4.0"
@@ -3327,9 +3357,9 @@ commander@^6.2.0:
3327 integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== 3357 integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
3328 3358
3329commander@^7.0.0: 3359commander@^7.0.0:
3330 version "7.1.0" 3360 version "7.2.0"
3331 resolved "https://registry.yarnpkg.com/commander/-/commander-7.1.0.tgz#f2eaecf131f10e36e07d894698226e36ae0eb5ff" 3361 resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
3332 integrity sha512-pRxBna3MJe6HKnBGsDyMv8ETbptw3axEdYHoqNh7gu5oDcew8fs0xnivZGm06Ogk8zGAJ9VX+OPEr2GXEQK4dg== 3362 integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
3333 3363
3334commondir@^1.0.1: 3364commondir@^1.0.1:
3335 version "1.0.1" 3365 version "1.0.1"
@@ -3501,9 +3531,9 @@ copy-webpack-plugin@6.3.2:
3501 webpack-sources "^1.4.3" 3531 webpack-sources "^1.4.3"
3502 3532
3503core-js-compat@^3.8.0: 3533core-js-compat@^3.8.0:
3504 version "3.9.0" 3534 version "3.9.1"
3505 resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.9.0.tgz#29da39385f16b71e1915565aa0385c4e0963ad56" 3535 resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.9.1.tgz#4e572acfe90aff69d76d8c37759d21a5c59bb455"
3506 integrity sha512-YK6fwFjCOKWwGnjFUR3c544YsnA/7DoLL0ysncuOJ4pwbriAtOpvM2bygdlcXbvQCQZ7bBU9CL4t7tGl7ETRpQ== 3536 integrity sha512-jXAirMQxrkbiiLsCx9bQPJFA6llDadKMpYrBJQJ3/c4/vsPP/fAf29h24tviRlvwUL6AmY5CHLu2GvjuYviQqA==
3507 dependencies: 3537 dependencies:
3508 browserslist "^4.16.3" 3538 browserslist "^4.16.3"
3509 semver "7.0.0" 3539 semver "7.0.0"
@@ -3514,9 +3544,9 @@ core-js@3.8.3:
3514 integrity sha512-KPYXeVZYemC2TkNEkX/01I+7yd+nX3KddKwZ1Ww7SKWdI2wQprSgLmrTddT8nw92AjEklTsPBoSdQBhbI1bQ6Q== 3544 integrity sha512-KPYXeVZYemC2TkNEkX/01I+7yd+nX3KddKwZ1Ww7SKWdI2wQprSgLmrTddT8nw92AjEklTsPBoSdQBhbI1bQ6Q==
3515 3545
3516core-js@^3.1.4: 3546core-js@^3.1.4:
3517 version "3.9.0" 3547 version "3.9.1"
3518 resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.9.0.tgz#790b1bb11553a2272b36e2625c7179db345492f8" 3548 resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.9.1.tgz#cec8de593db8eb2a85ffb0dbdeb312cb6e5460ae"
3519 integrity sha512-PyFBJaLq93FlyYdsndE5VaueA9K5cNB7CGzeCj191YYLhkQM0gdZR2SKihM70oF0wdqKSKClv/tEBOpoRmdOVQ== 3549 integrity sha512-gSjRvzkxQc1zjM/5paAmL4idJBFzuJoo+jDjF1tStYFMV2ERfD02HhahhCGXUyHxQRG4yFKVSdO6g62eoRMcDg==
3520 3550
3521core-util-is@1.0.2, core-util-is@~1.0.0: 3551core-util-is@1.0.2, core-util-is@~1.0.0:
3522 version "1.0.2" 3552 version "1.0.2"
@@ -3691,15 +3721,15 @@ css-loader@5.0.1:
3691 semver "^7.3.2" 3721 semver "^7.3.2"
3692 3722
3693css-loader@^5.0.1: 3723css-loader@^5.0.1:
3694 version "5.0.2" 3724 version "5.1.3"
3695 resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.0.2.tgz#24f758dae349bad0a440c50d7e2067742e0899cb" 3725 resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.1.3.tgz#87f6fc96816b20debe3cf682f85c7e56a963d0d1"
3696 integrity sha512-gbkBigdcHbmNvZ1Cg6aV6qh6k9N6XOr8YWzISLQGrwk2mgOH8LLrizhkxbDhQtaLtktyKHD4970S0xwz5btfTA== 3726 integrity sha512-CoPZvyh8sLiGARK3gqczpfdedbM74klGWurF2CsNZ2lhNaXdLIUks+3Mfax3WBeRuHoglU+m7KG/+7gY6G4aag==
3697 dependencies: 3727 dependencies:
3698 camelcase "^6.2.0" 3728 camelcase "^6.2.0"
3699 cssesc "^3.0.0" 3729 cssesc "^3.0.0"
3700 icss-utils "^5.1.0" 3730 icss-utils "^5.1.0"
3701 loader-utils "^2.0.0" 3731 loader-utils "^2.0.0"
3702 postcss "^8.2.4" 3732 postcss "^8.2.8"
3703 postcss-modules-extract-imports "^3.0.0" 3733 postcss-modules-extract-imports "^3.0.0"
3704 postcss-modules-local-by-default "^4.0.0" 3734 postcss-modules-local-by-default "^4.0.0"
3705 postcss-modules-scope "^3.0.0" 3735 postcss-modules-scope "^3.0.0"
@@ -4081,9 +4111,9 @@ destroy@~1.0.4:
4081 integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= 4111 integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
4082 4112
4083detect-node@^2.0.4: 4113detect-node@^2.0.4:
4084 version "2.0.4" 4114 version "2.0.5"
4085 resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" 4115 resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.5.tgz#9d270aa7eaa5af0b72c4c9d9b814e7f4ce738b79"
4086 integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw== 4116 integrity sha512-qi86tE6hRcFHy8jI1m2VG+LaPUR1LhqDa5G8tVjuUXmOrpuAgqsA1pN0+ldgr3aKUH+QLI9hCY/OcRYisERejw==
4087 4117
4088dexie@^3.0.0: 4118dexie@^3.0.0:
4089 version "3.0.3" 4119 version "3.0.3"
@@ -4241,9 +4271,9 @@ domutils@^1.5.1, domutils@^1.7.0:
4241 domelementtype "1" 4271 domelementtype "1"
4242 4272
4243domutils@^2.0.0, domutils@^2.4.4: 4273domutils@^2.0.0, domutils@^2.4.4:
4244 version "2.4.4" 4274 version "2.5.0"
4245 resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.4.4.tgz#282739c4b150d022d34699797369aad8d19bbbd3" 4275 resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.5.0.tgz#42f49cffdabb92ad243278b331fd761c1c2d3039"
4246 integrity sha512-jBC0vOsECI4OMdD0GC9mGn7NXPLb+Qt6KW1YDQzeQYRUFKmNG8lh7mO5HiELfr+lLQE7loDVI4QcAxV80HS+RA== 4276 integrity sha512-Ho16rzNMOFk2fPwChGh3D2D9OEHAfG19HgmRR2l+WLSsIstNsAYBzePH412bL0y5T44ejABIVfTHQ8nqi/tBCg==
4247 dependencies: 4277 dependencies:
4248 dom-serializer "^1.0.1" 4278 dom-serializer "^1.0.1"
4249 domelementtype "^2.0.1" 4279 domelementtype "^2.0.1"
@@ -4293,9 +4323,9 @@ ee-first@1.1.1:
4293 integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= 4323 integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
4294 4324
4295electron-to-chromium@^1.3.649: 4325electron-to-chromium@^1.3.649:
4296 version "1.3.673" 4326 version "1.3.695"
4297 resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.673.tgz#b4f81c930b388f962b7eba20d0483299aaa40913" 4327 resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.695.tgz#955f419cf99137226180cc4cca2e59015a4e248d"
4298 integrity sha512-ms+QR2ckfrrpEAjXweLx6kNCbpAl66DcW//3BZD4BV5KhUgr0RZRce1ON/9J3QyA3JO28nzgb5Xv8DnPr05ILg== 4328 integrity sha512-lz66RliUqLHU1Ojxx1A4QUxKydjiQ79Y4dZyPobs2Dmxj5aVL2TM3KoQ2Gs7HS703Bfny+ukI3KOxwAB0xceHQ==
4299 4329
4300elliptic@^6.5.3: 4330elliptic@^6.5.3:
4301 version "6.5.4" 4331 version "6.5.4"
@@ -4357,9 +4387,9 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0:
4357 once "^1.4.0" 4387 once "^1.4.0"
4358 4388
4359engine.io-client@~4.1.0: 4389engine.io-client@~4.1.0:
4360 version "4.1.1" 4390 version "4.1.2"
4361 resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-4.1.1.tgz#109942705079f15a4fcf1090bc86d3a1341c0a61" 4391 resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-4.1.2.tgz#823b4f005360321c41445fc23ce8ee028ef2e36b"
4362 integrity sha512-iYasV/EttP/2pLrdowe9G3zwlNIFhwny8VSIh+vPlMnYZqSzLsTzSLa9hFy015OrH1s4fzoYxeHjVkO8hSFKwg== 4392 integrity sha512-1mwvwKYMa0AaCy+sPgvJ/SnKyO5MJZ1HEeXfA3Rm/KHkHGiYD5bQVq8QzvIrkI01FuVtOdZC5lWdRw1BGXB2NQ==
4363 dependencies: 4393 dependencies:
4364 base64-arraybuffer "0.1.4" 4394 base64-arraybuffer "0.1.4"
4365 component-emitter "~1.3.0" 4395 component-emitter "~1.3.0"
@@ -4437,9 +4467,9 @@ entities@~2.1.0:
4437 integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== 4467 integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==
4438 4468
4439env-paths@^2.2.0: 4469env-paths@^2.2.0:
4440 version "2.2.0" 4470 version "2.2.1"
4441 resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.0.tgz#cdca557dc009152917d6166e2febe1f039685e43" 4471 resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2"
4442 integrity sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA== 4472 integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==
4443 4473
4444envinfo@^7.7.3: 4474envinfo@^7.7.3:
4445 version "7.7.4" 4475 version "7.7.4"
@@ -4470,42 +4500,27 @@ error-ex@^1.2.0, error-ex@^1.3.1:
4470 dependencies: 4500 dependencies:
4471 is-arrayish "^0.2.1" 4501 is-arrayish "^0.2.1"
4472 4502
4473es-abstract@^1.17.2: 4503es-abstract@^1.17.2, es-abstract@^1.18.0-next.2:
4474 version "1.17.7" 4504 version "1.18.0"
4475 resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c" 4505 resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0.tgz#ab80b359eecb7ede4c298000390bc5ac3ec7b5a4"
4476 integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g== 4506 integrity sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw==
4477 dependencies:
4478 es-to-primitive "^1.2.1"
4479 function-bind "^1.1.1"
4480 has "^1.0.3"
4481 has-symbols "^1.0.1"
4482 is-callable "^1.2.2"
4483 is-regex "^1.1.1"
4484 object-inspect "^1.8.0"
4485 object-keys "^1.1.1"
4486 object.assign "^4.1.1"
4487 string.prototype.trimend "^1.0.1"
4488 string.prototype.trimstart "^1.0.1"
4489
4490es-abstract@^1.18.0-next.2:
4491 version "1.18.0-next.2"
4492 resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.2.tgz#088101a55f0541f595e7e057199e27ddc8f3a5c2"
4493 integrity sha512-Ih4ZMFHEtZupnUh6497zEL4y2+w8+1ljnCyaTa+adcoafI1GOvMwFlDjBLfWR7y9VLfrjRJe9ocuHY1PSR9jjw==
4494 dependencies: 4507 dependencies:
4495 call-bind "^1.0.2" 4508 call-bind "^1.0.2"
4496 es-to-primitive "^1.2.1" 4509 es-to-primitive "^1.2.1"
4497 function-bind "^1.1.1" 4510 function-bind "^1.1.1"
4498 get-intrinsic "^1.0.2" 4511 get-intrinsic "^1.1.1"
4499 has "^1.0.3" 4512 has "^1.0.3"
4500 has-symbols "^1.0.1" 4513 has-symbols "^1.0.2"
4501 is-callable "^1.2.2" 4514 is-callable "^1.2.3"
4502 is-negative-zero "^2.0.1" 4515 is-negative-zero "^2.0.1"
4503 is-regex "^1.1.1" 4516 is-regex "^1.1.2"
4517 is-string "^1.0.5"
4504 object-inspect "^1.9.0" 4518 object-inspect "^1.9.0"
4505 object-keys "^1.1.1" 4519 object-keys "^1.1.1"
4506 object.assign "^4.1.2" 4520 object.assign "^4.1.2"
4507 string.prototype.trimend "^1.0.3" 4521 string.prototype.trimend "^1.0.4"
4508 string.prototype.trimstart "^1.0.3" 4522 string.prototype.trimstart "^1.0.4"
4523 unbox-primitive "^1.0.0"
4509 4524
4510es-to-primitive@^1.2.1: 4525es-to-primitive@^1.2.1:
4511 version "1.2.1" 4526 version "1.2.1"
@@ -4731,14 +4746,14 @@ eventemitter3@^4.0.0, eventemitter3@^4.0.3:
4731 integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== 4746 integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
4732 4747
4733events@^3.0.0: 4748events@^3.0.0:
4734 version "3.2.0" 4749 version "3.3.0"
4735 resolved "https://registry.yarnpkg.com/events/-/events-3.2.0.tgz#93b87c18f8efcd4202a461aec4dfc0556b639379" 4750 resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
4736 integrity sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg== 4751 integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
4737 4752
4738eventsource@^1.0.7: 4753eventsource@^1.0.7:
4739 version "1.0.7" 4754 version "1.1.0"
4740 resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.0.7.tgz#8fbc72c93fcd34088090bc0a4e64f4b5cee6d8d0" 4755 resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.1.0.tgz#00e8ca7c92109e94b0ddf32dac677d841028cfaf"
4741 integrity sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ== 4756 integrity sha512-VSJjT5oCNrFvCS6igjzPAt5hBzQ2qPBFIbJ03zLI9SE0mxwZpMw6BfJrbFHm1a141AavMEB8JHmBhWAd66PfCg==
4742 dependencies: 4757 dependencies:
4743 original "^1.0.0" 4758 original "^1.0.0"
4744 4759
@@ -5109,9 +5124,9 @@ focus-visible@^5.0.2:
5109 integrity sha512-Rwix9pBtC1Nuy5wysTmKy+UjbDJpIfg8eHjw0rjZ1mX4GNLz1Bmd16uDpI3Gk1i70Fgcs8Csg2lPm8HULFg9DQ== 5124 integrity sha512-Rwix9pBtC1Nuy5wysTmKy+UjbDJpIfg8eHjw0rjZ1mX4GNLz1Bmd16uDpI3Gk1i70Fgcs8Csg2lPm8HULFg9DQ==
5110 5125
5111follow-redirects@^1.0.0: 5126follow-redirects@^1.0.0:
5112 version "1.13.2" 5127 version "1.13.3"
5113 resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.2.tgz#dd73c8effc12728ba5cf4259d760ea5fb83e3147" 5128 resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.3.tgz#e5598ad50174c1bc4e872301e82ac2cd97f90267"
5114 integrity sha512-6mPTgLxYm3r6Bkkg0vNM0HTjfGrOEtsfbhagQvbxDEsEkpNhw582upBaoRZylzen6krEmxXJgt9Ju6HiI4O7BA== 5129 integrity sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==
5115 5130
5116for-in@^1.0.2: 5131for-in@^1.0.2:
5117 version "1.0.2" 5132 version "1.0.2"
@@ -5301,7 +5316,7 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5:
5301 resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" 5316 resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
5302 integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== 5317 integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
5303 5318
5304get-intrinsic@^1.0.2: 5319get-intrinsic@^1.0.2, get-intrinsic@^1.1.1:
5305 version "1.1.1" 5320 version "1.1.1"
5306 resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" 5321 resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6"
5307 integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== 5322 integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==
@@ -5353,9 +5368,9 @@ glob-parent@^3.1.0:
5353 path-dirname "^1.0.0" 5368 path-dirname "^1.0.0"
5354 5369
5355glob-parent@^5.1.0, glob-parent@^5.1.1, glob-parent@~5.1.0: 5370glob-parent@^5.1.0, glob-parent@^5.1.1, glob-parent@~5.1.0:
5356 version "5.1.1" 5371 version "5.1.2"
5357 resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" 5372 resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
5358 integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== 5373 integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
5359 dependencies: 5374 dependencies:
5360 is-glob "^4.0.1" 5375 is-glob "^4.0.1"
5361 5376
@@ -5410,9 +5425,9 @@ globals@^9.2.0:
5410 integrity sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ== 5425 integrity sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==
5411 5426
5412globby@^11.0.1: 5427globby@^11.0.1:
5413 version "11.0.2" 5428 version "11.0.3"
5414 resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.2.tgz#1af538b766a3b540ebfb58a32b2e2d5897321d83" 5429 resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.3.tgz#9b1f0cb523e171dd1ad8c7b2a9fb4b644b9593cb"
5415 integrity sha512-2ZThXDvvV8fYFRVIxnrMQBipZQDr7MxKAmQK1vujaj9/7eF0efG7BPUKJ7jP7G5SLF37xKDXvO4S/KKLj/Z0og== 5430 integrity sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg==
5416 dependencies: 5431 dependencies:
5417 array-union "^2.1.0" 5432 array-union "^2.1.0"
5418 dir-glob "^3.0.1" 5433 dir-glob "^3.0.1"
@@ -5497,6 +5512,11 @@ has-ansi@^2.0.0:
5497 dependencies: 5512 dependencies:
5498 ansi-regex "^2.0.0" 5513 ansi-regex "^2.0.0"
5499 5514
5515has-bigints@^1.0.0:
5516 version "1.0.1"
5517 resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113"
5518 integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==
5519
5500has-cors@1.1.0: 5520has-cors@1.1.0:
5501 version "1.1.0" 5521 version "1.1.0"
5502 resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39" 5522 resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39"
@@ -5512,10 +5532,10 @@ has-flag@^4.0.0:
5512 resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" 5532 resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
5513 integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== 5533 integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
5514 5534
5515has-symbols@^1.0.1: 5535has-symbols@^1.0.0, has-symbols@^1.0.1, has-symbols@^1.0.2:
5516 version "1.0.1" 5536 version "1.0.2"
5517 resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" 5537 resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423"
5518 integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== 5538 integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==
5519 5539
5520has-unicode@^2.0.0: 5540has-unicode@^2.0.0:
5521 version "2.0.1" 5541 version "2.0.1"
@@ -5616,6 +5636,13 @@ hosted-git-info@^3.0.6:
5616 dependencies: 5636 dependencies:
5617 lru-cache "^6.0.0" 5637 lru-cache "^6.0.0"
5618 5638
5639hosted-git-info@^4.0.1:
5640 version "4.0.1"
5641 resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.0.1.tgz#710ef5452ea429a844abc33c981056e7371edab7"
5642 integrity sha512-eT7NrxAsppPRQEBSwKSosReE+v8OzABwEScQYk5d4uxaEPlzxTIku7LINXtBGalthkLhJnq5lBI89PfK43zAKg==
5643 dependencies:
5644 lru-cache "^6.0.0"
5645
5619hpack.js@^2.1.6: 5646hpack.js@^2.1.6:
5620 version "2.1.6" 5647 version "2.1.6"
5621 resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" 5648 resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2"
@@ -5712,9 +5739,9 @@ htmlparser2@^4.1.0:
5712 entities "^2.0.0" 5739 entities "^2.0.0"
5713 5740
5714htmlparser2@^6.0.0: 5741htmlparser2@^6.0.0:
5715 version "6.0.0" 5742 version "6.0.1"
5716 resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.0.0.tgz#c2da005030390908ca4c91e5629e418e0665ac01" 5743 resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.0.1.tgz#422521231ef6d42e56bd411da8ba40aa36e91446"
5717 integrity sha512-numTQtDZMoh78zJpaNdJ9MXb2cv5G3jwUoe3dMQODubZvLoGvTE/Ofp6sHvH8OGKcN/8A47pGLi/k58xHP/Tfw== 5744 integrity sha512-GDKPd+vk4jvSuvCbyuzx/unmXkk090Azec7LovXP8as1Hn8q9p3hbjmDGbUqqhknw0ajwit6LiiWqfiTUPMK7w==
5718 dependencies: 5745 dependencies:
5719 domelementtype "^2.0.1" 5746 domelementtype "^2.0.1"
5720 domhandler "^4.0.0" 5747 domhandler "^4.0.0"
@@ -6133,6 +6160,11 @@ is-ascii@^1.0.0:
6133 resolved "https://registry.yarnpkg.com/is-ascii/-/is-ascii-1.0.0.tgz#f02ad0259a0921cd199ff21ce1b09e0f6b4e3929" 6160 resolved "https://registry.yarnpkg.com/is-ascii/-/is-ascii-1.0.0.tgz#f02ad0259a0921cd199ff21ce1b09e0f6b4e3929"
6134 integrity sha1-8CrQJZoJIc0Zn/Ic4bCeD2tOOSk= 6161 integrity sha1-8CrQJZoJIc0Zn/Ic4bCeD2tOOSk=
6135 6162
6163is-bigint@^1.0.1:
6164 version "1.0.1"
6165 resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.1.tgz#6923051dfcbc764278540b9ce0e6b3213aa5ebc2"
6166 integrity sha512-J0ELF4yHFxHy0cmSxZuheDOz2luOdVvqjwmEcj8H/L1JHeuEDSDbeRP+Dk9kFVk5RTFzbucJ2Kb9F7ixY2QaCg==
6167
6136is-binary-path@^1.0.0: 6168is-binary-path@^1.0.0:
6137 version "1.0.1" 6169 version "1.0.1"
6138 resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" 6170 resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898"
@@ -6147,12 +6179,19 @@ is-binary-path@~2.1.0:
6147 dependencies: 6179 dependencies:
6148 binary-extensions "^2.0.0" 6180 binary-extensions "^2.0.0"
6149 6181
6182is-boolean-object@^1.1.0:
6183 version "1.1.0"
6184 resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.0.tgz#e2aaad3a3a8fca34c28f6eee135b156ed2587ff0"
6185 integrity sha512-a7Uprx8UtD+HWdyYwnD1+ExtTgqQtD2k/1yJgtXP6wnMm8byhkoTZRl+95LLThpzNZJ5aEvi46cdH+ayMFRwmA==
6186 dependencies:
6187 call-bind "^1.0.0"
6188
6150is-buffer@^1.1.5: 6189is-buffer@^1.1.5:
6151 version "1.1.6" 6190 version "1.1.6"
6152 resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" 6191 resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
6153 integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== 6192 integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
6154 6193
6155is-callable@^1.1.4, is-callable@^1.2.2: 6194is-callable@^1.1.4, is-callable@^1.2.3:
6156 version "1.2.3" 6195 version "1.2.3"
6157 resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e" 6196 resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e"
6158 integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ== 6197 integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==
@@ -6312,6 +6351,11 @@ is-negative-zero@^2.0.1:
6312 resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" 6351 resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24"
6313 integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w== 6352 integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==
6314 6353
6354is-number-object@^1.0.4:
6355 version "1.0.4"
6356 resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197"
6357 integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==
6358
6315is-number@^3.0.0: 6359is-number@^3.0.0:
6316 version "3.0.0" 6360 version "3.0.0"
6317 resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" 6361 resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
@@ -6384,7 +6428,7 @@ is-property@^1.0.0, is-property@^1.0.2:
6384 resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" 6428 resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84"
6385 integrity sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ= 6429 integrity sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=
6386 6430
6387is-regex@^1.0.4, is-regex@^1.1.1: 6431is-regex@^1.0.4, is-regex@^1.1.2:
6388 version "1.1.2" 6432 version "1.1.2"
6389 resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.2.tgz#81c8ebde4db142f2cf1c53fc86d6a45788266251" 6433 resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.2.tgz#81c8ebde4db142f2cf1c53fc86d6a45788266251"
6390 integrity sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg== 6434 integrity sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg==
@@ -6407,6 +6451,11 @@ is-stream@^2.0.0:
6407 resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" 6451 resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3"
6408 integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== 6452 integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==
6409 6453
6454is-string@^1.0.5:
6455 version "1.0.5"
6456 resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6"
6457 integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==
6458
6410is-svg@^3.0.0: 6459is-svg@^3.0.0:
6411 version "3.0.0" 6460 version "3.0.0"
6412 resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-3.0.0.tgz#9321dbd29c212e5ca99c4fa9794c714bcafa2f75" 6461 resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-3.0.0.tgz#9321dbd29c212e5ca99c4fa9794c714bcafa2f75"
@@ -6414,7 +6463,7 @@ is-svg@^3.0.0:
6414 dependencies: 6463 dependencies:
6415 html-comment-regex "^1.1.0" 6464 html-comment-regex "^1.1.0"
6416 6465
6417is-symbol@^1.0.2: 6466is-symbol@^1.0.2, is-symbol@^1.0.3:
6418 version "1.0.3" 6467 version "1.0.3"
6419 resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" 6468 resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937"
6420 integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== 6469 integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==
@@ -6426,6 +6475,11 @@ is-typedarray@^1.0.0, is-typedarray@~1.0.0:
6426 resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" 6475 resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
6427 integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= 6476 integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
6428 6477
6478is-unicode-supported@^0.1.0:
6479 version "0.1.0"
6480 resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7"
6481 integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==
6482
6429is-what@^3.12.0: 6483is-what@^3.12.0:
6430 version "3.14.1" 6484 version "3.14.1"
6431 resolved "https://registry.yarnpkg.com/is-what/-/is-what-3.14.1.tgz#e1222f46ddda85dead0fd1c9df131760e77755c1" 6485 resolved "https://registry.yarnpkg.com/is-what/-/is-what-3.14.1.tgz#e1222f46ddda85dead0fd1c9df131760e77755c1"
@@ -6538,16 +6592,21 @@ istanbul-reports@^3.0.2:
6538 html-escaper "^2.0.0" 6592 html-escaper "^2.0.0"
6539 istanbul-lib-report "^3.0.0" 6593 istanbul-lib-report "^3.0.0"
6540 6594
6541jasmine-core@^3.6.0, jasmine-core@~3.6.0: 6595jasmine-core@^3.6.0:
6542 version "3.6.0" 6596 version "3.7.1"
6543 resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.6.0.tgz#491f3bb23941799c353ceb7a45b38a950ebc5a20" 6597 resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.7.1.tgz#0401327f6249eac993d47bbfa18d4e8efacfb561"
6544 integrity sha512-8uQYa7zJN8hq9z+g8z1bqCfdC8eoDAeVnM5sfqs7KHv9/ifoJ500m018fpFc7RDaO6SWCLCXwo/wPSNcdYTgcw== 6598 integrity sha512-DH3oYDS/AUvvr22+xUBW62m1Xoy7tUlY1tsxKEJvl5JeJ7q8zd1K5bUwiOxdH+erj6l2vAMM3hV25Xs9/WrmuQ==
6545 6599
6546jasmine-core@~2.8.0: 6600jasmine-core@~2.8.0:
6547 version "2.8.0" 6601 version "2.8.0"
6548 resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.8.0.tgz#bcc979ae1f9fd05701e45e52e65d3a5d63f1a24e" 6602 resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.8.0.tgz#bcc979ae1f9fd05701e45e52e65d3a5d63f1a24e"
6549 integrity sha1-vMl5rh+f0FcB5F5S5l06XWPxok4= 6603 integrity sha1-vMl5rh+f0FcB5F5S5l06XWPxok4=
6550 6604
6605jasmine-core@~3.6.0:
6606 version "3.6.0"
6607 resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.6.0.tgz#491f3bb23941799c353ceb7a45b38a950ebc5a20"
6608 integrity sha512-8uQYa7zJN8hq9z+g8z1bqCfdC8eoDAeVnM5sfqs7KHv9/ifoJ500m018fpFc7RDaO6SWCLCXwo/wPSNcdYTgcw==
6609
6551jasmine-spec-reporter@~6.0.0: 6610jasmine-spec-reporter@~6.0.0:
6552 version "6.0.0" 6611 version "6.0.0"
6553 resolved "https://registry.yarnpkg.com/jasmine-spec-reporter/-/jasmine-spec-reporter-6.0.0.tgz#3b9c85689676a351f343ba8dd6d3957f11a4bf1d" 6612 resolved "https://registry.yarnpkg.com/jasmine-spec-reporter/-/jasmine-spec-reporter-6.0.0.tgz#3b9c85689676a351f343ba8dd6d3957f11a4bf1d"
@@ -6785,9 +6844,9 @@ karma-source-map-support@1.4.0:
6785 source-map-support "^0.5.5" 6844 source-map-support "^0.5.5"
6786 6845
6787karma@~6.1.0: 6846karma@~6.1.0:
6788 version "6.1.1" 6847 version "6.1.2"
6789 resolved "https://registry.yarnpkg.com/karma/-/karma-6.1.1.tgz#a7539618cca0f2cbb26d5497120ec31fe340c2a1" 6848 resolved "https://registry.yarnpkg.com/karma/-/karma-6.1.2.tgz#9d7394559f5deb150b3021c1860960281c3a0e50"
6790 integrity sha512-vVDFxFGAsclgmFjZA/qGw5xqWdZIWxVD7xLyCukYUYd5xs/uGzYbXGOT5zOruVBQleKEmXIr4H2hzGCTn+M9Cg== 6849 integrity sha512-mKbxgsJrt3UHBPdKfCxC2eg3lpqyt6hQRFhNWJ2sk0wUnbnLPEiCpgIgiycuLSra0vC6TaK9OPJiMGATGzgH/A==
6791 dependencies: 6850 dependencies:
6792 body-parser "^1.19.0" 6851 body-parser "^1.19.0"
6793 braces "^3.0.2" 6852 braces "^3.0.2"
@@ -7042,11 +7101,12 @@ lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.1
7042 integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== 7101 integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
7043 7102
7044log-symbols@^4.0.0: 7103log-symbols@^4.0.0:
7045 version "4.0.0" 7104 version "4.1.0"
7046 resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920" 7105 resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503"
7047 integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA== 7106 integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==
7048 dependencies: 7107 dependencies:
7049 chalk "^4.0.0" 7108 chalk "^4.1.0"
7109 is-unicode-supported "^0.1.0"
7050 7110
7051log4js@^6.2.1: 7111log4js@^6.2.1:
7052 version "6.3.0" 7112 version "6.3.0"
@@ -7110,9 +7170,9 @@ m3u8-parser@4.5.0:
7110 global "^4.3.2" 7170 global "^4.3.2"
7111 7171
7112m3u8-parser@^4.4.0: 7172m3u8-parser@^4.4.0:
7113 version "4.5.2" 7173 version "4.6.0"
7114 resolved "https://registry.yarnpkg.com/m3u8-parser/-/m3u8-parser-4.5.2.tgz#f7d48a60112466e528324624c4e66d52ed341a75" 7174 resolved "https://registry.yarnpkg.com/m3u8-parser/-/m3u8-parser-4.6.0.tgz#a0e2f5dcf8391c9a6e59895a084fa38f27b52124"
7115 integrity sha512-sN/lu3TiRxmG2RFjZxo5c0/7Dr4RrEztl43jXrWwj5gFZ7vfa2iIxGfiPx485dm5QCazaIcKk+vNkUso8Aq0Ag== 7175 integrity sha512-dKhhpMcPqDM/KzULVrNyDZ/z766peQjwUghDTcl6TE7DQKAt/vm74/IMUAxpO34f6LDpM+OH/dYGQwW1eM4yWw==
7116 dependencies: 7176 dependencies:
7117 "@babel/runtime" "^7.12.5" 7177 "@babel/runtime" "^7.12.5"
7118 "@videojs/vhs-utils" "^3.0.0" 7178 "@videojs/vhs-utils" "^3.0.0"
@@ -7381,9 +7441,9 @@ mini-css-extract-plugin@1.3.5:
7381 webpack-sources "^1.1.0" 7441 webpack-sources "^1.1.0"
7382 7442
7383mini-css-extract-plugin@^1.3.1: 7443mini-css-extract-plugin@^1.3.1:
7384 version "1.3.8" 7444 version "1.3.9"
7385 resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-1.3.8.tgz#639047b78c2ee728704285aa468d2a5a8d91d566" 7445 resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-1.3.9.tgz#47a32132b0fd97a119acd530e8421e8f6ab16d5e"
7386 integrity sha512-u+2kVov/Gcs74iz+x3phEBWMAGw2djjnKfYez+Pl/b5dyXL7aM4Lp5QQtIq16CDwRHT/woUJki49gBNMhfm1eA== 7446 integrity sha512-Ac4s+xhVbqlyhXS5J/Vh/QXUz3ycXlCqoCPpg0vdfhsIBH9eg/It/9L1r1XhSCH737M1lqcWnMuWL13zcygn5A==
7387 dependencies: 7447 dependencies:
7388 loader-utils "^2.0.0" 7448 loader-utils "^2.0.0"
7389 schema-utils "^3.0.0" 7449 schema-utils "^3.0.0"
@@ -7630,9 +7690,9 @@ nan@^2.12.1:
7630 integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ== 7690 integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==
7631 7691
7632nanoid@^3.1.20: 7692nanoid@^3.1.20:
7633 version "3.1.20" 7693 version "3.1.22"
7634 resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788" 7694 resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.22.tgz#b35f8fb7d151990a8aebd5aa5015c03cf726f844"
7635 integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw== 7695 integrity sha512-/2ZUaJX2ANuLtTvqTlgqBQNJoQO398KyJgZloL0PZkC0dpysjncRUPsFe3DUPzz/y3h+u7C46np8RMuvF3jsSQ==
7636 7696
7637nanomatch@^1.2.9: 7697nanomatch@^1.2.9:
7638 version "1.2.13" 7698 version "1.2.13"
@@ -7830,13 +7890,13 @@ npm-package-arg@8.1.0:
7830 semver "^7.0.0" 7890 semver "^7.0.0"
7831 validate-npm-package-name "^3.0.0" 7891 validate-npm-package-name "^3.0.0"
7832 7892
7833npm-package-arg@^8.0.0, npm-package-arg@^8.0.1: 7893npm-package-arg@^8.0.0, npm-package-arg@^8.0.1, npm-package-arg@^8.1.2:
7834 version "8.1.1" 7894 version "8.1.2"
7835 resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-8.1.1.tgz#00ebf16ac395c63318e67ce66780a06db6df1b04" 7895 resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-8.1.2.tgz#b868016ae7de5619e729993fbd8d11dc3c52ab62"
7836 integrity sha512-CsP95FhWQDwNqiYS+Q0mZ7FAEDytDZAkNxQqea6IaAFJTAY9Lhhqyl0irU/6PMc7BGfUmnsbHcqxJD7XuVM/rg== 7896 integrity sha512-6Eem455JsSMJY6Kpd3EyWE+n5hC+g9bSyHr9K9U2zqZb7+02+hObQ2c0+8iDk/mNF+8r1MhY44WypKJAkySIYA==
7837 dependencies: 7897 dependencies:
7838 hosted-git-info "^3.0.6" 7898 hosted-git-info "^4.0.1"
7839 semver "^7.0.0" 7899 semver "^7.3.4"
7840 validate-npm-package-name "^3.0.0" 7900 validate-npm-package-name "^3.0.0"
7841 7901
7842npm-packlist@^2.1.4: 7902npm-packlist@^2.1.4:
@@ -7849,7 +7909,7 @@ npm-packlist@^2.1.4:
7849 npm-bundled "^1.1.1" 7909 npm-bundled "^1.1.1"
7850 npm-normalize-package-bin "^1.0.1" 7910 npm-normalize-package-bin "^1.0.1"
7851 7911
7852npm-pick-manifest@6.1.0, npm-pick-manifest@^6.0.0: 7912npm-pick-manifest@6.1.0:
7853 version "6.1.0" 7913 version "6.1.0"
7854 resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-6.1.0.tgz#2befed87b0fce956790f62d32afb56d7539c022a" 7914 resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-6.1.0.tgz#2befed87b0fce956790f62d32afb56d7539c022a"
7855 integrity sha512-ygs4k6f54ZxJXrzT0x34NybRlLeZ4+6nECAIbr2i0foTnijtS1TJiyzpqtuUAJOps/hO0tNDr8fRV5g+BtRlTw== 7915 integrity sha512-ygs4k6f54ZxJXrzT0x34NybRlLeZ4+6nECAIbr2i0foTnijtS1TJiyzpqtuUAJOps/hO0tNDr8fRV5g+BtRlTw==
@@ -7858,6 +7918,16 @@ npm-pick-manifest@6.1.0, npm-pick-manifest@^6.0.0:
7858 npm-package-arg "^8.0.0" 7918 npm-package-arg "^8.0.0"
7859 semver "^7.0.0" 7919 semver "^7.0.0"
7860 7920
7921npm-pick-manifest@^6.0.0:
7922 version "6.1.1"
7923 resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-6.1.1.tgz#7b5484ca2c908565f43b7f27644f36bb816f5148"
7924 integrity sha512-dBsdBtORT84S8V8UTad1WlUyKIY9iMsAmqxHbLdeEeBNMLQDlDWWra3wYUx9EBEIiG/YwAy0XyNHDd2goAsfuA==
7925 dependencies:
7926 npm-install-checks "^4.0.0"
7927 npm-normalize-package-bin "^1.0.1"
7928 npm-package-arg "^8.1.2"
7929 semver "^7.3.4"
7930
7861npm-registry-fetch@^9.0.0: 7931npm-registry-fetch@^9.0.0:
7862 version "9.0.0" 7932 version "9.0.0"
7863 resolved "https://registry.yarnpkg.com/npm-registry-fetch/-/npm-registry-fetch-9.0.0.tgz#86f3feb4ce00313bc0b8f1f8f69daae6face1661" 7933 resolved "https://registry.yarnpkg.com/npm-registry-fetch/-/npm-registry-fetch-9.0.0.tgz#86f3feb4ce00313bc0b8f1f8f69daae6face1661"
@@ -7927,7 +7997,7 @@ object-copy@^0.1.0:
7927 define-property "^0.2.5" 7997 define-property "^0.2.5"
7928 kind-of "^3.0.3" 7998 kind-of "^3.0.3"
7929 7999
7930object-inspect@^1.8.0, object-inspect@^1.9.0: 8000object-inspect@^1.9.0:
7931 version "1.9.0" 8001 version "1.9.0"
7932 resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a" 8002 resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a"
7933 integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw== 8003 integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==
@@ -7952,7 +8022,7 @@ object-visit@^1.0.0:
7952 dependencies: 8022 dependencies:
7953 isobject "^3.0.0" 8023 isobject "^3.0.0"
7954 8024
7955object.assign@^4.1.0, object.assign@^4.1.1, object.assign@^4.1.2: 8025object.assign@^4.1.0, object.assign@^4.1.2:
7956 version "4.1.2" 8026 version "4.1.2"
7957 resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" 8027 resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940"
7958 integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== 8028 integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==
@@ -8896,12 +8966,12 @@ postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.27:
8896 source-map "^0.6.1" 8966 source-map "^0.6.1"
8897 supports-color "^6.1.0" 8967 supports-color "^6.1.0"
8898 8968
8899postcss@^8.0.2, postcss@^8.1.4, postcss@^8.2.4: 8969postcss@^8.0.2, postcss@^8.1.4, postcss@^8.2.8:
8900 version "8.2.6" 8970 version "8.2.8"
8901 resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.6.tgz#5d69a974543b45f87e464bc4c3e392a97d6be9fe" 8971 resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.8.tgz#0b90f9382efda424c4f0f69a2ead6f6830d08ece"
8902 integrity sha512-xpB8qYxgPuly166AGlpRjUdEYtmOWx2iCwGmrv4vqZL9YPVviDVPZPRXxnXr6xPZOdxQ9lp3ZBFCRgWJ7LE3Sg== 8972 integrity sha512-1F0Xb2T21xET7oQV9eKuctbM9S7BC0fetoHCc4H13z0PT6haiRLP4T0ZY4XWh7iLP0usgqykT6p9B2RtOf4FPw==
8903 dependencies: 8973 dependencies:
8904 colorette "^1.2.1" 8974 colorette "^1.2.2"
8905 nanoid "^3.1.20" 8975 nanoid "^3.1.20"
8906 source-map "^0.6.1" 8976 source-map "^0.6.1"
8907 8977
@@ -8924,9 +8994,9 @@ pretty-error@^2.1.1:
8924 renderkid "^2.0.4" 8994 renderkid "^2.0.4"
8925 8995
8926primeng@^11.0.0-rc.1: 8996primeng@^11.0.0-rc.1:
8927 version "11.2.3" 8997 version "11.3.1"
8928 resolved "https://registry.yarnpkg.com/primeng/-/primeng-11.2.3.tgz#66e3d817fe27c9a7703726537c03ddcc1998bb44" 8998 resolved "https://registry.yarnpkg.com/primeng/-/primeng-11.3.1.tgz#644dd59d1f0808227a9529ea6ffaad31bdb5e5df"
8929 integrity sha512-8elRAGal8a+qXJ4egRKXU+bUvIyfCxsiCerXgOPbwbo/TU/DBK7WBXGGGi6KJOamFqClAqj/FO3WLAdofKQSRQ== 8999 integrity sha512-B86/su/3sNP2GfhyegvZh2MpHcUZHas+13bPL98QmZhoiPBQp2jz3H0iD716+piC00Wee6pi/PPm7e9y9qxGDg==
8930 dependencies: 9000 dependencies:
8931 tslib "^2.0.0" 9001 tslib "^2.0.0"
8932 9002
@@ -9027,11 +9097,6 @@ public-encrypt@^4.0.0:
9027 randombytes "^2.0.1" 9097 randombytes "^2.0.1"
9028 safe-buffer "^5.1.2" 9098 safe-buffer "^5.1.2"
9029 9099
9030puka@^1.0.1:
9031 version "1.0.1"
9032 resolved "https://registry.yarnpkg.com/puka/-/puka-1.0.1.tgz#a2df782b7eb4cf9564e4c93a5da422de0dfacc02"
9033 integrity sha512-ssjRZxBd7BT3dte1RR3VoeT2cT/ODH8x+h0rUF1rMqB0srHYf48stSDWfiYakTp5UBZMxroZhB2+ExLDHm7W3g==
9034
9035pump@^2.0.0: 9100pump@^2.0.0:
9036 version "2.0.1" 9101 version "2.0.1"
9037 resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" 9102 resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909"
@@ -9133,15 +9198,15 @@ querystringify@^2.1.1:
9133 resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" 9198 resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
9134 integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== 9199 integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==
9135 9200
9136queue-microtask@^1.1.2, queue-microtask@^1.2.0, queue-microtask@^1.2.2: 9201queue-microtask@^1.2.0, queue-microtask@^1.2.2:
9137 version "1.2.2" 9202 version "1.2.3"
9138 resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.2.tgz#abf64491e6ecf0f38a6502403d4cda04f372dfd3" 9203 resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
9139 integrity sha512-dB15eXv3p2jDlbOiNLyMabYg1/sXvppd8DP2J3EOCQ0AkuSXCW2tP7mnVouVLJKgUMY6yP0kcQDVpLCN13h4Xg== 9204 integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
9140 9205
9141random-access-file@^2.0.1: 9206random-access-file@^2.0.1:
9142 version "2.1.5" 9207 version "2.2.0"
9143 resolved "https://registry.yarnpkg.com/random-access-file/-/random-access-file-2.1.5.tgz#27af6115b920a9adabb44559e29ea9944bb35bfe" 9208 resolved "https://registry.yarnpkg.com/random-access-file/-/random-access-file-2.2.0.tgz#b49b999efefb374afb7587f219071fec5ce66546"
9144 integrity sha512-lqmUGgF9X+LD0XSeWSHcs7U2nSLYp+RQvkDDqKWoxW8jcd13tZ00G6PHV32OZqDIHmS9ewoEUEa6jcvyB7UCvg== 9209 integrity sha512-B744003Mj7v3EcuPl9hCiB2Ot4aZjgtU2mV6yFY1THiWU/XfGf1uSadR+SlQdJcwHgAWeG7Lbos0aUqjtj8FQg==
9145 dependencies: 9210 dependencies:
9146 mkdirp-classic "^0.5.2" 9211 mkdirp-classic "^0.5.2"
9147 random-access-storage "^1.1.1" 9212 random-access-storage "^1.1.1"
@@ -9370,9 +9435,9 @@ regjsgen@^0.5.1:
9370 integrity sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A== 9435 integrity sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==
9371 9436
9372regjsparser@^0.6.4: 9437regjsparser@^0.6.4:
9373 version "0.6.7" 9438 version "0.6.8"
9374 resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.7.tgz#c00164e1e6713c2e3ee641f1701c4b7aa0a7f86c" 9439 resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.8.tgz#4532c3da36d75d56e3f394ce2ea6842bde7496bd"
9375 integrity sha512-ib77G0uxsA2ovgiYbCVGx4Pv3PSttAx2vIwidqQzbL2U5S4Q+j00HdSAneSBuyVcMvEnTXMjiGgB+DlXozVhpQ== 9440 integrity sha512-3weFrFQREJhJ2PW+iCGaG6TenyzNSZgsBKZ/oEf6Trme31COSeIWhHw9O6FPkuXktfx+b6Hf/5e6dKPHaROq2g==
9376 dependencies: 9441 dependencies:
9377 jsesc "~0.5.0" 9442 jsesc "~0.5.0"
9378 9443
@@ -9593,9 +9658,9 @@ rework@1.0.1, rework@^1.0.1:
9593 css "^2.0.0" 9658 css "^2.0.0"
9594 9659
9595rfdc@^1.1.4: 9660rfdc@^1.1.4:
9596 version "1.2.0" 9661 version "1.3.0"
9597 resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.2.0.tgz#9e9894258f48f284b43c3143c68070a4f373b949" 9662 resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b"
9598 integrity sha512-ijLyszTMmUrXvjSooucVQwimGUk84eRcmCuLV8Xghe3UO85mjUtRAHRyoMM6XtyqbECaXuBWx18La3523sXINA== 9663 integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==
9599 9664
9600rgb-regex@^1.0.1: 9665rgb-regex@^1.0.1:
9601 version "1.0.1" 9666 version "1.0.1"
@@ -9681,7 +9746,7 @@ run-series@^1.1.8, run-series@^1.1.9:
9681 resolved "https://registry.yarnpkg.com/run-series/-/run-series-1.1.9.tgz#15ba9cb90e6a6c054e67c98e1dc063df0ecc113a" 9746 resolved "https://registry.yarnpkg.com/run-series/-/run-series-1.1.9.tgz#15ba9cb90e6a6c054e67c98e1dc063df0ecc113a"
9682 integrity sha512-Arc4hUN896vjkqCYrUXquBFtRZdv1PfLbTYP71efP6butxyQ0kWpiNJyAgsxscmQg1cqvHY32/UCBzXedTpU2g== 9747 integrity sha512-Arc4hUN896vjkqCYrUXquBFtRZdv1PfLbTYP71efP6butxyQ0kWpiNJyAgsxscmQg1cqvHY32/UCBzXedTpU2g==
9683 9748
9684rusha@^0.8.1: 9749rusha@^0.8.13:
9685 version "0.8.13" 9750 version "0.8.13"
9686 resolved "https://registry.yarnpkg.com/rusha/-/rusha-0.8.13.tgz#9a084e7b860b17bff3015b92c67a6a336191513a" 9751 resolved "https://registry.yarnpkg.com/rusha/-/rusha-0.8.13.tgz#9a084e7b860b17bff3015b92c67a6a336191513a"
9687 integrity sha1-mghOe4YLF7/zAVuSxnpqM2GRUTo= 9752 integrity sha1-mghOe4YLF7/zAVuSxnpqM2GRUTo=
@@ -9742,9 +9807,9 @@ safe-regex@^1.1.0:
9742 integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== 9807 integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
9743 9808
9744sanitize-html@^2.1.2: 9809sanitize-html@^2.1.2:
9745 version "2.3.2" 9810 version "2.3.3"
9746 resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.3.2.tgz#a1954aea877a096c408aca7b0c260bef6e4fc402" 9811 resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.3.3.tgz#3db382c9a621cce4c46d90f10c64f1e9da9e8353"
9747 integrity sha512-p7neuskvC8pSurUjdVmbWPXmc9A4+QpOXIL+4gwFC+av5h+lYCXFT8uEneqsFQg/wEA1IH+cKQA60AaQI6p3cg== 9812 integrity sha512-DCFXPt7Di0c6JUnlT90eIgrjs6TsJl/8HYU3KLdmrVclFN4O0heTcVbJiMa23OKVr6aR051XYtsgd8EWwEBwUA==
9748 dependencies: 9813 dependencies:
9749 deepmerge "^4.2.2" 9814 deepmerge "^4.2.2"
9750 escape-string-regexp "^4.0.0" 9815 escape-string-regexp "^4.0.0"
@@ -9894,7 +9959,7 @@ semver@7.0.0:
9894 resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" 9959 resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e"
9895 integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== 9960 integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==
9896 9961
9897semver@7.3.4, semver@^7.0.0, semver@^7.1.1, semver@^7.3.2, semver@^7.3.4: 9962semver@7.3.4:
9898 version "7.3.4" 9963 version "7.3.4"
9899 resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97" 9964 resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97"
9900 integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw== 9965 integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==
@@ -9906,6 +9971,13 @@ semver@^6.0.0, semver@^6.3.0:
9906 resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" 9971 resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
9907 integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== 9972 integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
9908 9973
9974semver@^7.0.0, semver@^7.1.1, semver@^7.3.2, semver@^7.3.4:
9975 version "7.3.5"
9976 resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
9977 integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
9978 dependencies:
9979 lru-cache "^6.0.0"
9980
9909send@0.17.1: 9981send@0.17.1:
9910 version "0.17.1" 9982 version "0.17.1"
9911 resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" 9983 resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
@@ -10061,9 +10133,9 @@ simple-get@^4.0.0:
10061 simple-concat "^1.0.0" 10133 simple-concat "^1.0.0"
10062 10134
10063simple-peer@^9.5.0, simple-peer@^9.7.1, simple-peer@^9.9.3: 10135simple-peer@^9.5.0, simple-peer@^9.7.1, simple-peer@^9.9.3:
10064 version "9.9.3" 10136 version "9.10.0"
10065 resolved "https://registry.yarnpkg.com/simple-peer/-/simple-peer-9.9.3.tgz#b52c39d1173620d06c8b29ada7ee2ad3384bb469" 10137 resolved "https://registry.yarnpkg.com/simple-peer/-/simple-peer-9.10.0.tgz#f458444300f635e6fcc2f5a5166c45d71eafb57f"
10066 integrity sha512-T3wuv0UqBpDTV0x0pJPPsz4thy0tC0fTOHE4g9+AF43RUxxT+MWeXVtdQcK5Xuzv/XTVrB2NrGzdfO1IFBqOkw== 10138 integrity sha512-sKrKtca1UdmwdZIbvuT3iEL05tDGt/xdLP6+ej8rh1ADgtDk44yLaEZjIyPJ6c34zsSih46Ou7zUIT7e4hPK7g==
10067 dependencies: 10139 dependencies:
10068 buffer "^6.0.2" 10140 buffer "^6.0.2"
10069 debug "^4.2.0" 10141 debug "^4.2.0"
@@ -10074,12 +10146,12 @@ simple-peer@^9.5.0, simple-peer@^9.7.1, simple-peer@^9.9.3:
10074 readable-stream "^3.6.0" 10146 readable-stream "^3.6.0"
10075 10147
10076simple-sha1@^3.0.0, simple-sha1@^3.0.1: 10148simple-sha1@^3.0.0, simple-sha1@^3.0.1:
10077 version "3.0.1" 10149 version "3.1.0"
10078 resolved "https://registry.yarnpkg.com/simple-sha1/-/simple-sha1-3.0.1.tgz#b34c3c978d74ac4baf99b6555c1e6736e0d6e700" 10150 resolved "https://registry.yarnpkg.com/simple-sha1/-/simple-sha1-3.1.0.tgz#40cac8436dfaf9924332fc46a5c7bca45f656131"
10079 integrity sha512-q7ehqWfHc1VhOm7sW099YDZ4I0yYX7rqyhqqhHV1IYeUTjPOhHyD3mXvv8k2P+rO7+7c8R4/D+8ffzC9BE7Cqg== 10151 integrity sha512-ArTptMRC1v08H8ihPD6l0wesKvMfF9e8XL5rIHPanI7kGOsSsbY514MwVu6X1PITHCTB2F08zB7cyEbfc4wQjg==
10080 dependencies: 10152 dependencies:
10081 queue-microtask "^1.1.2" 10153 queue-microtask "^1.2.2"
10082 rusha "^0.8.1" 10154 rusha "^0.8.13"
10083 10155
10084simple-swizzle@^0.2.2: 10156simple-swizzle@^0.2.2:
10085 version "0.2.2" 10157 version "0.2.2"
@@ -10159,9 +10231,9 @@ socket.io-adapter@~2.1.0:
10159 integrity sha512-+vDov/aTsLjViYTwS9fPy5pEtTkrbEKsw2M+oVSoFGw6OD1IpvlV1VPhUzNbofCQ8oyMbdYJqDtGdmHQK6TdPg== 10231 integrity sha512-+vDov/aTsLjViYTwS9fPy5pEtTkrbEKsw2M+oVSoFGw6OD1IpvlV1VPhUzNbofCQ8oyMbdYJqDtGdmHQK6TdPg==
10160 10232
10161socket.io-client@^3.0.3: 10233socket.io-client@^3.0.3:
10162 version "3.1.1" 10234 version "3.1.3"
10163 resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-3.1.1.tgz#43dfc3feddbb675b274a724f685d6b6af319b3e3" 10235 resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-3.1.3.tgz#57ddcefea58cfab71f0e94c21124de8e3c5aa3e2"
10164 integrity sha512-BLgIuCjI7Sf3mDHunKddX9zKR/pbkP7IACM3sJS3jha+zJ6/pGKRV6Fz5XSBHCfUs9YzT8kYIqNwOOuFNLtnYA== 10236 integrity sha512-4sIGOGOmCg3AOgGi7EEr6ZkTZRkrXwub70bBB/F0JSkMOUFpA77WsL87o34DffQQ31PkbMUIadGOk+3tx1KGbw==
10165 dependencies: 10237 dependencies:
10166 "@types/component-emitter" "^1.2.10" 10238 "@types/component-emitter" "^1.2.10"
10167 backo2 "~1.0.2" 10239 backo2 "~1.0.2"
@@ -10181,13 +10253,13 @@ socket.io-parser@~4.0.3, socket.io-parser@~4.0.4:
10181 debug "~4.3.1" 10253 debug "~4.3.1"
10182 10254
10183socket.io@^3.1.0: 10255socket.io@^3.1.0:
10184 version "3.1.1" 10256 version "3.1.2"
10185 resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-3.1.1.tgz#905e3d4a3b37d8e7970e67a4a6eb81110a5778ba" 10257 resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-3.1.2.tgz#06e27caa1c4fc9617547acfbb5da9bc1747da39a"
10186 integrity sha512-7cBWdsDC7bbyEF6WbBqffjizc/H4YF1wLdZoOzuYfo2uMNSFjJKuQ36t0H40o9B20DO6p+mSytEd92oP4S15bA== 10258 integrity sha512-JubKZnTQ4Z8G4IZWtaAZSiRP3I/inpy8c/Bsx2jrwGrTbKeVU5xd6qkKMHpChYeM3dWZSO0QACiGK+obhBNwYw==
10187 dependencies: 10259 dependencies:
10188 "@types/cookie" "^0.4.0" 10260 "@types/cookie" "^0.4.0"
10189 "@types/cors" "^2.8.8" 10261 "@types/cors" "^2.8.8"
10190 "@types/node" "^14.14.10" 10262 "@types/node" ">=10.0.0"
10191 accepts "~1.3.4" 10263 accepts "~1.3.4"
10192 base64id "~2.0.0" 10264 base64id "~2.0.0"
10193 debug "~4.3.1" 10265 debug "~4.3.1"
@@ -10226,9 +10298,9 @@ socks-proxy-agent@^5.0.0:
10226 socks "^2.3.3" 10298 socks "^2.3.3"
10227 10299
10228socks@^2.3.3: 10300socks@^2.3.3:
10229 version "2.5.1" 10301 version "2.6.0"
10230 resolved "https://registry.yarnpkg.com/socks/-/socks-2.5.1.tgz#7720640b6b5ec9a07d556419203baa3f0596df5f" 10302 resolved "https://registry.yarnpkg.com/socks/-/socks-2.6.0.tgz#6b984928461d39871b3666754b9000ecf39dfac2"
10231 integrity sha512-oZCsJJxapULAYJaEYBSzMcz8m3jqgGrHaGhkmU/o/PQfFWYWxkAaA0UMGImb6s6tEXfKi959X6VJjMMQ3P6TTQ== 10303 integrity sha512-mNmr9owlinMplev0Wd7UHFlqI4ofnBnNzFuzrm63PPaHgbkqCFe4T5LzwKmtQ/f2tX0NTpcdVLyD/FHxFBstYw==
10232 dependencies: 10304 dependencies:
10233 ip "^1.1.5" 10305 ip "^1.1.5"
10234 smart-buffer "^4.1.0" 10306 smart-buffer "^4.1.0"
@@ -10416,7 +10488,7 @@ ssri@^6.0.1:
10416 dependencies: 10488 dependencies:
10417 figgy-pudding "^3.5.1" 10489 figgy-pudding "^3.5.1"
10418 10490
10419ssri@^8.0.0: 10491ssri@^8.0.0, ssri@^8.0.1:
10420 version "8.0.1" 10492 version "8.0.1"
10421 resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af" 10493 resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af"
10422 integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ== 10494 integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==
@@ -10546,15 +10618,15 @@ string-width@^3.0.0, string-width@^3.1.0:
10546 strip-ansi "^5.1.0" 10618 strip-ansi "^5.1.0"
10547 10619
10548string-width@^4.1.0, string-width@^4.2.0: 10620string-width@^4.1.0, string-width@^4.2.0:
10549 version "4.2.0" 10621 version "4.2.2"
10550 resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" 10622 resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5"
10551 integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== 10623 integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==
10552 dependencies: 10624 dependencies:
10553 emoji-regex "^8.0.0" 10625 emoji-regex "^8.0.0"
10554 is-fullwidth-code-point "^3.0.0" 10626 is-fullwidth-code-point "^3.0.0"
10555 strip-ansi "^6.0.0" 10627 strip-ansi "^6.0.0"
10556 10628
10557string.prototype.trimend@^1.0.1, string.prototype.trimend@^1.0.3: 10629string.prototype.trimend@^1.0.4:
10558 version "1.0.4" 10630 version "1.0.4"
10559 resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" 10631 resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80"
10560 integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A== 10632 integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==
@@ -10562,7 +10634,7 @@ string.prototype.trimend@^1.0.1, string.prototype.trimend@^1.0.3:
10562 call-bind "^1.0.2" 10634 call-bind "^1.0.2"
10563 define-properties "^1.1.3" 10635 define-properties "^1.1.3"
10564 10636
10565string.prototype.trimstart@^1.0.1, string.prototype.trimstart@^1.0.3: 10637string.prototype.trimstart@^1.0.4:
10566 version "1.0.4" 10638 version "1.0.4"
10567 resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed" 10639 resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed"
10568 integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw== 10640 integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==
@@ -10815,9 +10887,9 @@ terser@^4.1.2, terser@^4.6.3:
10815 source-map-support "~0.5.12" 10887 source-map-support "~0.5.12"
10816 10888
10817terser@^5.3.4: 10889terser@^5.3.4:
10818 version "5.6.0" 10890 version "5.6.1"
10819 resolved "https://registry.yarnpkg.com/terser/-/terser-5.6.0.tgz#138cdf21c5e3100b1b3ddfddf720962f88badcd2" 10891 resolved "https://registry.yarnpkg.com/terser/-/terser-5.6.1.tgz#a48eeac5300c0a09b36854bf90d9c26fb201973c"
10820 integrity sha512-vyqLMoqadC1uR0vywqOZzriDYzgEkNJFK4q9GeyOBHIbiECHiWLKcWfbQWAUaPfxkjDhapSlZB9f7fkMrvkVjA== 10892 integrity sha512-yv9YLFQQ+3ZqgWCUk+pvNJwgUTdlIxUk1WTN+RnaFJe2L7ipG2csPT0ra2XRm7Cs8cxN7QXmK1rFzEwYEQkzXw==
10821 dependencies: 10893 dependencies:
10822 commander "^2.20.0" 10894 commander "^2.20.0"
10823 source-map "~0.7.2" 10895 source-map "~0.7.2"
@@ -10976,9 +11048,9 @@ tree-kill@1.2.2:
10976 integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== 11048 integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
10977 11049
10978ts-loader@^8.0.14: 11050ts-loader@^8.0.14:
10979 version "8.0.17" 11051 version "8.0.18"
10980 resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-8.0.17.tgz#98f2ccff9130074f4079fd89b946b4c637b1f2fc" 11052 resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-8.0.18.tgz#b2385cbe81c34ad9f997915129cdde3ad92a61ea"
10981 integrity sha512-OeVfSshx6ot/TCxRwpBHQ/4lRzfgyTkvi7ghDVrLXOHzTbSK413ROgu/xNqM72i3AFeAIJgQy78FwSMKmOW68w== 11053 integrity sha512-hRZzkydPX30XkLaQwJTDcWDoxZHK6IrEMDQpNd7tgcakFruFkeUp/aY+9hBb7BUGb+ZWKI0jiOGMo0MckwzdDQ==
10982 dependencies: 11054 dependencies:
10983 chalk "^4.1.0" 11055 chalk "^4.1.0"
10984 enhanced-resolve "^4.0.0" 11056 enhanced-resolve "^4.0.0"
@@ -11054,9 +11126,9 @@ tsutils@^2.29.0:
11054 tslib "^1.8.1" 11126 tslib "^1.8.1"
11055 11127
11056tsutils@^3.0.0: 11128tsutils@^3.0.0:
11057 version "3.20.0" 11129 version "3.21.0"
11058 resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.20.0.tgz#ea03ea45462e146b53d70ce0893de453ff24f698" 11130 resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
11059 integrity sha512-RYbuQuvkhuqVeXweWT3tJLKOEJ/UUw9GjNEZGWdrLLlM+611o1gwLHBpxoFJKKl25fLprp2eVthtKs5JOrNeXg== 11131 integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==
11060 dependencies: 11132 dependencies:
11061 tslib "^1.8.1" 11133 tslib "^1.8.1"
11062 11134
@@ -11103,9 +11175,9 @@ type@^1.0.1:
11103 integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== 11175 integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==
11104 11176
11105type@^2.0.0: 11177type@^2.0.0:
11106 version "2.3.0" 11178 version "2.5.0"
11107 resolved "https://registry.yarnpkg.com/type/-/type-2.3.0.tgz#ada7c045f07ead08abf9e2edd29be1a0c0661132" 11179 resolved "https://registry.yarnpkg.com/type/-/type-2.5.0.tgz#0a2e78c2e77907b252abe5f298c1b01c63f0db3d"
11108 integrity sha512-rgPIqOdfK/4J9FhiVrZ3cveAjRRo5rsQBAIhnylX874y1DX/kEKSVdLsnuHB6l1KTjHyU01VjiMBHgU2adejyg== 11180 integrity sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw==
11109 11181
11110typedarray-to-buffer@^3.0.0: 11182typedarray-to-buffer@^3.0.0:
11111 version "3.1.5" 11183 version "3.1.5"
@@ -11119,12 +11191,7 @@ typedarray@^0.0.6:
11119 resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" 11191 resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
11120 integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= 11192 integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
11121 11193
11122typescript@4.1.3: 11194typescript@4.1.5, typescript@~4.1.3:
11123 version "4.1.3"
11124 resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.3.tgz#519d582bd94cba0cf8934c7d8e8467e473f53bb7"
11125 integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==
11126
11127typescript@~4.1.3:
11128 version "4.1.5" 11195 version "4.1.5"
11129 resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.5.tgz#123a3b214aaff3be32926f0d8f1f6e704eb89a72" 11196 resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.5.tgz#123a3b214aaff3be32926f0d8f1f6e704eb89a72"
11130 integrity sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA== 11197 integrity sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA==
@@ -11140,9 +11207,9 @@ uc.micro@^1.0.1, uc.micro@^1.0.5:
11140 integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== 11207 integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==
11141 11208
11142uglify-js@^3.0.6: 11209uglify-js@^3.0.6:
11143 version "3.12.8" 11210 version "3.13.2"
11144 resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.12.8.tgz#a82e6e53c9be14f7382de3d068ef1e26e7d4aaf8" 11211 resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.13.2.tgz#fe10319861bccc8682bfe2e8151fbdd8aa921c44"
11145 integrity sha512-fvBeuXOsvqjecUtF/l1dwsrrf5y2BCUk9AOJGzGcm6tE7vegku5u/YvqjyDaAGr422PLoLnrxg3EnRvTqsdC1w== 11212 integrity sha512-SbMu4D2Vo95LMC/MetNaso1194M1htEA+JrqE9Hk+G2DhI+itfS9TRu9ZKeCahLDNa/J3n4MqUJ/fOHMzQpRWw==
11146 11213
11147uint64be@^2.0.2: 11214uint64be@^2.0.2:
11148 version "2.0.2" 11215 version "2.0.2"
@@ -11151,6 +11218,16 @@ uint64be@^2.0.2:
11151 dependencies: 11218 dependencies:
11152 buffer-alloc "^1.1.0" 11219 buffer-alloc "^1.1.0"
11153 11220
11221unbox-primitive@^1.0.0:
11222 version "1.0.0"
11223 resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.0.tgz#eeacbc4affa28e9b3d36b5eaeccc50b3251b1d3f"
11224 integrity sha512-P/51NX+JXyxK/aigg1/ZgyccdAxm5K1+n8+tvqSntjOivPt19gvm1VC49RWYetsiub8WViUchdxl/KWHHB0kzA==
11225 dependencies:
11226 function-bind "^1.1.1"
11227 has-bigints "^1.0.0"
11228 has-symbols "^1.0.0"
11229 which-boxed-primitive "^1.0.1"
11230
11154unicode-canonical-property-names-ecmascript@^1.0.4: 11231unicode-canonical-property-names-ecmascript@^1.0.4:
11155 version "1.0.4" 11232 version "1.0.4"
11156 resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" 11233 resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"
@@ -11402,9 +11479,9 @@ uuid@^3.0.0, uuid@^3.3.2, uuid@^3.4.0:
11402 integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== 11479 integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
11403 11480
11404v8-compile-cache@^2.2.0: 11481v8-compile-cache@^2.2.0:
11405 version "2.2.0" 11482 version "2.3.0"
11406 resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz#9471efa3ef9128d2f7c6a7ca39c4dd6b5055b132" 11483 resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
11407 integrity sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q== 11484 integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
11408 11485
11409validate-npm-package-license@^3.0.1: 11486validate-npm-package-license@^3.0.1:
11410 version "3.0.4" 11487 version "3.0.4"
@@ -11801,15 +11878,26 @@ webtorrent@^0.112.3:
11801 utp-native "^2.3.0" 11878 utp-native "^2.3.0"
11802 11879
11803whatwg-fetch@^3.0.0: 11880whatwg-fetch@^3.0.0:
11804 version "3.6.1" 11881 version "3.6.2"
11805 resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.1.tgz#93bc4005af6c2cc30ba3e42ec3125947c8f54ed3" 11882 resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c"
11806 integrity sha512-IEmN/ZfmMw6G1hgZpVd0LuZXOQDisrMOZrzYd5x3RAK4bMPlJohKUZWZ9t/QsTvH0dV9TbPDcc2OSuIDcihnHA== 11883 integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==
11807 11884
11808whatwg-mimetype@^2.3.0: 11885whatwg-mimetype@^2.3.0:
11809 version "2.3.0" 11886 version "2.3.0"
11810 resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" 11887 resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
11811 integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== 11888 integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
11812 11889
11890which-boxed-primitive@^1.0.1:
11891 version "1.0.2"
11892 resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"
11893 integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==
11894 dependencies:
11895 is-bigint "^1.0.1"
11896 is-boolean-object "^1.1.0"
11897 is-number-object "^1.0.4"
11898 is-string "^1.0.5"
11899 is-symbol "^1.0.3"
11900
11813which-module@^2.0.0: 11901which-module@^2.0.0:
11814 version "2.0.0" 11902 version "2.0.0"
11815 resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" 11903 resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
@@ -11915,9 +12003,9 @@ ws@^6.2.1:
11915 async-limiter "~1.0.0" 12003 async-limiter "~1.0.0"
11916 12004
11917ws@^7.3.0, ws@^7.3.1, ws@^7.4.2, ws@~7.4.2: 12005ws@^7.3.0, ws@^7.3.1, ws@^7.4.2, ws@~7.4.2:
11918 version "7.4.3" 12006 version "7.4.4"
11919 resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.3.tgz#1f9643de34a543b8edb124bdcbc457ae55a6e5cd" 12007 resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.4.tgz#383bc9742cb202292c9077ceab6f6047b17f2d59"
11920 integrity sha512-hr6vCR76GsossIRsr8OLR9acVVm1jyfEWvhbNjtgPOrfvAlKzvyeg/P6r8RuDjRyrcQoPQT7K0DGEPc7Ae6jzA== 12008 integrity sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==
11921 12009
11922xml2js@^0.4.17: 12010xml2js@^0.4.17:
11923 version "0.4.23" 12011 version "0.4.23"
@@ -11978,9 +12066,9 @@ yallist@^4.0.0:
11978 integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== 12066 integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
11979 12067
11980yaml@^1.10.0: 12068yaml@^1.10.0:
11981 version "1.10.0" 12069 version "1.10.2"
11982 resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e" 12070 resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
11983 integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg== 12071 integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
11984 12072
11985yargs-parser@^13.1.2: 12073yargs-parser@^13.1.2:
11986 version "13.1.2" 12074 version "13.1.2"
@@ -11999,9 +12087,9 @@ yargs-parser@^18.1.2:
11999 decamelize "^1.2.0" 12087 decamelize "^1.2.0"
12000 12088
12001yargs-parser@^20.2.2: 12089yargs-parser@^20.2.2:
12002 version "20.2.6" 12090 version "20.2.7"
12003 resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.6.tgz#69f920addf61aafc0b8b89002f5d66e28f2d8b20" 12091 resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.7.tgz#61df85c113edfb5a7a4e36eb8aa60ef423cbc90a"
12004 integrity sha512-AP1+fQIWSM/sMiET8fyayjx/J+JmTPt2Mr0FkrgqB4todtfa53sOsrSAcIrJRD5XS20bKUwaDIuMkWKCEiQLKA== 12092 integrity sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==
12005 12093
12006yargs-parser@^7.0.0: 12094yargs-parser@^7.0.0:
12007 version "7.0.0" 12095 version "7.0.0"
diff --git a/config/default.yaml b/config/default.yaml
index a09d20b9d..f9b6c50a3 100644
--- a/config/default.yaml
+++ b/config/default.yaml
@@ -198,6 +198,13 @@ federation:
198 # We still suggest you to enable this setting even if your users will loose most of their video's likes/dislikes 198 # We still suggest you to enable this setting even if your users will loose most of their video's likes/dislikes
199 cleanup_remote_interactions: false 199 cleanup_remote_interactions: false
200 200
201peertube:
202 check_latest_version:
203 # Check and notify admins of new PeerTube versions
204 enabled: true
205 # You can use a custom URL if your want, that respect the format behind https://joinpeertube.org/api/v1/versions.json
206 url: 'https://joinpeertube.org/api/v1/versions.json'
207
201cache: 208cache:
202 previews: 209 previews:
203 size: 500 # Max number of previews you want to cache 210 size: 500 # Max number of previews you want to cache
@@ -265,7 +272,7 @@ transcoding:
265 # If you also enabled the hls format, it will multiply videos storage by 2 272 # If you also enabled the hls format, it will multiply videos storage by 2
266 # If disabled, breaks federation with PeerTube instances < 2.1 273 # If disabled, breaks federation with PeerTube instances < 2.1
267 webtorrent: 274 webtorrent:
268 enabled: true 275 enabled: false
269 276
270 # /!\ Requires ffmpeg >= 4.1 277 # /!\ Requires ffmpeg >= 4.1
271 # Generate HLS playlists and fragmented MP4 files. Better playback than with WebTorrent: 278 # Generate HLS playlists and fragmented MP4 files. Better playback than with WebTorrent:
@@ -274,7 +281,7 @@ transcoding:
274 # * More stable playback (less bugs/infinite loading) 281 # * More stable playback (less bugs/infinite loading)
275 # If you also enabled the webtorrent format, it will multiply videos storage by 2 282 # If you also enabled the webtorrent format, it will multiply videos storage by 2
276 hls: 283 hls:
277 enabled: false 284 enabled: true
278 285
279live: 286live:
280 enabled: false 287 enabled: false
diff --git a/config/production.yaml.example b/config/production.yaml.example
index 31c0e6b96..f2e75af32 100644
--- a/config/production.yaml.example
+++ b/config/production.yaml.example
@@ -196,6 +196,12 @@ federation:
196 # We still suggest you to enable this setting even if your users will loose most of their video's likes/dislikes 196 # We still suggest you to enable this setting even if your users will loose most of their video's likes/dislikes
197 cleanup_remote_interactions: false 197 cleanup_remote_interactions: false
198 198
199peertube:
200 check_latest_version:
201 # Check and notify admins of new PeerTube versions
202 enabled: true
203 # You can use a custom URL if your want, that respect the format behind https://joinpeertube.org/api/v1/versions.json
204 url: 'https://joinpeertube.org/api/v1/versions.json'
199 205
200############################################################################### 206###############################################################################
201# 207#
@@ -276,7 +282,7 @@ transcoding:
276 # If you also enabled the hls format, it will multiply videos storage by 2 282 # If you also enabled the hls format, it will multiply videos storage by 2
277 # If disabled, breaks federation with PeerTube instances < 2.1 283 # If disabled, breaks federation with PeerTube instances < 2.1
278 webtorrent: 284 webtorrent:
279 enabled: true 285 enabled: false
280 286
281 # /!\ Requires ffmpeg >= 4.1 287 # /!\ Requires ffmpeg >= 4.1
282 # Generate HLS playlists and fragmented MP4 files. Better playback than with WebTorrent: 288 # Generate HLS playlists and fragmented MP4 files. Better playback than with WebTorrent:
@@ -285,7 +291,7 @@ transcoding:
285 # * More stable playback (less bugs/infinite loading) 291 # * More stable playback (less bugs/infinite loading)
286 # If you also enabled the webtorrent format, it will multiply videos storage by 2 292 # If you also enabled the webtorrent format, it will multiply videos storage by 2
287 hls: 293 hls:
288 enabled: false 294 enabled: true
289 295
290live: 296live:
291 enabled: false 297 enabled: false
diff --git a/config/test.yaml b/config/test.yaml
index 33c11afc3..9a522a983 100644
--- a/config/test.yaml
+++ b/config/test.yaml
@@ -38,6 +38,10 @@ log:
38contact_form: 38contact_form:
39 enabled: true 39 enabled: true
40 40
41peertube:
42 check_latest_version:
43 enabled: false
44
41redundancy: 45redundancy:
42 videos: 46 videos:
43 check_interval: '1 minute' 47 check_interval: '1 minute'
@@ -83,6 +87,8 @@ transcoding:
83 1080p: true 87 1080p: true
84 1440p: true 88 1440p: true
85 2160p: true 89 2160p: true
90 webtorrent:
91 enabled: true
86 hls: 92 hls:
87 enabled: true 93 enabled: true
88 94
diff --git a/package.json b/package.json
index 5719a116c..e3766e318 100644
--- a/package.json
+++ b/package.json
@@ -53,6 +53,7 @@
53 "start:server": "node dist/server --no-client", 53 "start:server": "node dist/server --no-client",
54 "update-host": "node ./dist/scripts/update-host.js", 54 "update-host": "node ./dist/scripts/update-host.js",
55 "create-transcoding-job": "node ./dist/scripts/create-transcoding-job.js", 55 "create-transcoding-job": "node ./dist/scripts/create-transcoding-job.js",
56 "regenerate-thumbnails": "node ./dist/scripts/regenerate-thumbnails.js",
56 "create-import-video-file-job": "node ./dist/scripts/create-import-video-file-job.js", 57 "create-import-video-file-job": "node ./dist/scripts/create-import-video-file-job.js",
57 "print-transcode-command": "node ./dist/scripts/print-transcode-command.js", 58 "print-transcode-command": "node ./dist/scripts/print-transcode-command.js",
58 "test": "scripty", 59 "test": "scripty",
@@ -82,10 +83,6 @@
82 "swagger-cli": "swagger-cli", 83 "swagger-cli": "swagger-cli",
83 "sass-lint": "sass-lint" 84 "sass-lint": "sass-lint"
84 }, 85 },
85 "resolutions": {
86 "oauth2-server": "3.1.0-beta.1",
87 "http-signature": "1.3.5"
88 },
89 "dependencies": { 86 "dependencies": {
90 "apicache": "1.6.2", 87 "apicache": "1.6.2",
91 "async": "^3.0.1", 88 "async": "^3.0.1",
@@ -105,12 +102,12 @@
105 "deep-object-diff": "^1.1.0", 102 "deep-object-diff": "^1.1.0",
106 "email-templates": "^8.0.3", 103 "email-templates": "^8.0.3",
107 "express": "^4.12.4", 104 "express": "^4.12.4",
108 "express-oauth-server": "^2.0.0",
109 "express-rate-limit": "^5.0.0", 105 "express-rate-limit": "^5.0.0",
110 "express-validator": "^6.4.0", 106 "express-validator": "^6.4.0",
111 "flat": "^5.0.0", 107 "flat": "^5.0.0",
112 "fluent-ffmpeg": "^2.1.0", 108 "fluent-ffmpeg": "^2.1.0",
113 "fs-extra": "^9.0.0", 109 "fs-extra": "^9.0.0",
110 "got": "^11.8.2",
114 "helmet": "^4.1.0", 111 "helmet": "^4.1.0",
115 "http-signature": "1.3.5", 112 "http-signature": "1.3.5",
116 "ip-anonymize": "^0.1.0", 113 "ip-anonymize": "^0.1.0",
@@ -130,7 +127,7 @@
130 "multer": "^1.1.0", 127 "multer": "^1.1.0",
131 "node-media-server": "^2.1.4", 128 "node-media-server": "^2.1.4",
132 "nodemailer": "^6.0.0", 129 "nodemailer": "^6.0.0",
133 "oauth2-server": "3.1.0-beta.1", 130 "oauth2-server": "3.1.1",
134 "parse-torrent": "^9.1.0", 131 "parse-torrent": "^9.1.0",
135 "password-generator": "^2.0.2", 132 "password-generator": "^2.0.2",
136 "pem": "^1.12.3", 133 "pem": "^1.12.3",
@@ -140,7 +137,6 @@
140 "pug": "^3.0.0", 137 "pug": "^3.0.0",
141 "redis": "^3.0.2", 138 "redis": "^3.0.2",
142 "reflect-metadata": "^0.1.12", 139 "reflect-metadata": "^0.1.12",
143 "request": "^2.81.0",
144 "sanitize-html": "2.x", 140 "sanitize-html": "2.x",
145 "scripty": "^2.0.0", 141 "scripty": "^2.0.0",
146 "sequelize": "6.5.0", 142 "sequelize": "6.5.0",
@@ -178,7 +174,6 @@
178 "@types/express-rate-limit": "^5.0.0", 174 "@types/express-rate-limit": "^5.0.0",
179 "@types/fluent-ffmpeg": "^2.1.16", 175 "@types/fluent-ffmpeg": "^2.1.16",
180 "@types/fs-extra": "^9.0.1", 176 "@types/fs-extra": "^9.0.1",
181 "@types/libxmljs": "^0.18.0",
182 "@types/lodash": "^4.14.64", 177 "@types/lodash": "^4.14.64",
183 "@types/lru-cache": "^5.1.0", 178 "@types/lru-cache": "^5.1.0",
184 "@types/magnet-uri": "^5.1.1", 179 "@types/magnet-uri": "^5.1.1",
@@ -210,7 +205,7 @@
210 "eslint-plugin-node": "^11.0.0", 205 "eslint-plugin-node": "^11.0.0",
211 "eslint-plugin-promise": "^4.2.1", 206 "eslint-plugin-promise": "^4.2.1",
212 "eslint-plugin-standard": "^5.0.0", 207 "eslint-plugin-standard": "^5.0.0",
213 "libxmljs": "0.19.7", 208 "fast-xml-parser": "^3.19.0",
214 "maildev": "^1.0.0-rc3", 209 "maildev": "^1.0.0-rc3",
215 "marked": "^2.0.1", 210 "marked": "^2.0.1",
216 "marked-man": "^0.7.0", 211 "marked-man": "^0.7.0",
diff --git a/scripts/benchmark.ts b/scripts/benchmark.ts
index 45b2a7a79..0cadb36d9 100644
--- a/scripts/benchmark.ts
+++ b/scripts/benchmark.ts
@@ -50,126 +50,102 @@ async function run () {
50 title: 'AP - account peertube', 50 title: 'AP - account peertube',
51 path: '/accounts/peertube', 51 path: '/accounts/peertube',
52 headers: buildAPHeader(), 52 headers: buildAPHeader(),
53 expecter: (client, statusCode) => { 53 expecter: (body, status) => {
54 const body = client.resData[0].body 54 return status === 200 && body.startsWith('{"type":')
55
56 return statusCode === 200 && body.startsWith('{"type":')
57 } 55 }
58 }, 56 },
59 { 57 {
60 title: 'AP - video', 58 title: 'AP - video',
61 path: '/videos/watch/' + video.uuid, 59 path: '/videos/watch/' + video.uuid,
62 headers: buildAPHeader(), 60 headers: buildAPHeader(),
63 expecter: (client, statusCode) => { 61 expecter: (body, status) => {
64 const body = client.resData[0].body 62 return status === 200 && body.startsWith('{"type":"Video"')
65
66 return statusCode === 200 && body.startsWith('{"type":"Video"')
67 } 63 }
68 }, 64 },
69 { 65 {
70 title: 'Misc - webfinger peertube', 66 title: 'Misc - webfinger peertube',
71 path: '/.well-known/webfinger?resource=acct:peertube@' + server.host, 67 path: '/.well-known/webfinger?resource=acct:peertube@' + server.host,
72 expecter: (client, statusCode) => { 68 expecter: (body, status) => {
73 const body = client.resData[0].body 69 return status === 200 && body.startsWith('{"subject":')
74
75 return statusCode === 200 && body.startsWith('{"subject":')
76 } 70 }
77 }, 71 },
78 { 72 {
79 title: 'API - unread notifications', 73 title: 'API - unread notifications',
80 path: '/api/v1/users/me/notifications?start=0&count=0&unread=true', 74 path: '/api/v1/users/me/notifications?start=0&count=0&unread=true',
81 headers: buildAuthorizationHeader(), 75 headers: buildAuthorizationHeader(),
82 expecter: (_client, statusCode) => { 76 expecter: (_body, status) => {
83 return statusCode === 200 77 return status === 200
84 } 78 }
85 }, 79 },
86 { 80 {
87 title: 'API - me', 81 title: 'API - me',
88 path: '/api/v1/users/me', 82 path: '/api/v1/users/me',
89 headers: buildAuthorizationHeader(), 83 headers: buildAuthorizationHeader(),
90 expecter: (client, statusCode) => { 84 expecter: (body, status) => {
91 const body = client.resData[0].body 85 return status === 200 && body.startsWith('{"id":')
92
93 return statusCode === 200 && body.startsWith('{"id":')
94 } 86 }
95 }, 87 },
96 { 88 {
97 title: 'API - videos list', 89 title: 'API - videos list',
98 path: '/api/v1/videos', 90 path: '/api/v1/videos',
99 expecter: (client, statusCode) => { 91 expecter: (body, status) => {
100 const body = client.resData[0].body 92 return status === 200 && body.startsWith('{"total":10')
101
102 return statusCode === 200 && body.startsWith('{"total":10')
103 } 93 }
104 }, 94 },
105 { 95 {
106 title: 'API - video get', 96 title: 'API - video get',
107 path: '/api/v1/videos/' + video.uuid, 97 path: '/api/v1/videos/' + video.uuid,
108 expecter: (client, statusCode) => { 98 expecter: (body, status) => {
109 const body = client.resData[0].body 99 return status === 200 && body.startsWith('{"id":')
110
111 return statusCode === 200 && body.startsWith('{"id":')
112 } 100 }
113 }, 101 },
114 { 102 {
115 title: 'API - video captions', 103 title: 'API - video captions',
116 path: '/api/v1/videos/' + video.uuid + '/captions', 104 path: '/api/v1/videos/' + video.uuid + '/captions',
117 expecter: (client, statusCode) => { 105 expecter: (body, status) => {
118 const body = client.resData[0].body 106 return status === 200 && body.startsWith('{"total":4')
119
120 return statusCode === 200 && body.startsWith('{"total":4')
121 } 107 }
122 }, 108 },
123 { 109 {
124 title: 'API - video threads', 110 title: 'API - video threads',
125 path: '/api/v1/videos/' + video.uuid + '/comment-threads', 111 path: '/api/v1/videos/' + video.uuid + '/comment-threads',
126 expecter: (client, statusCode) => { 112 expecter: (body, status) => {
127 const body = client.resData[0].body 113 return status === 200 && body.startsWith('{"total":10')
128
129 return statusCode === 200 && body.startsWith('{"total":10')
130 } 114 }
131 }, 115 },
132 { 116 {
133 title: 'API - video replies', 117 title: 'API - video replies',
134 path: '/api/v1/videos/' + video.uuid + '/comment-threads/' + threadId, 118 path: '/api/v1/videos/' + video.uuid + '/comment-threads/' + threadId,
135 expecter: (client, statusCode) => { 119 expecter: (body, status) => {
136 const body = client.resData[0].body 120 return status === 200 && body.startsWith('{"comment":{')
137
138 return statusCode === 200 && body.startsWith('{"comment":{')
139 } 121 }
140 }, 122 },
141 { 123 {
142 title: 'HTML - video watch', 124 title: 'HTML - video watch',
143 path: '/videos/watch/' + video.uuid, 125 path: '/videos/watch/' + video.uuid,
144 expecter: (client, statusCode) => { 126 expecter: (body, status) => {
145 const body = client.resData[0].body 127 return status === 200 && body.includes('<title>my super')
146
147 return statusCode === 200 && body.includes('<title>my super')
148 } 128 }
149 }, 129 },
150 { 130 {
151 title: 'HTML - video embed', 131 title: 'HTML - video embed',
152 path: '/videos/embed/' + video.uuid, 132 path: '/videos/embed/' + video.uuid,
153 expecter: (client, statusCode) => { 133 expecter: (body, status) => {
154 const body = client.resData[0].body 134 return status === 200 && body.includes('embed')
155
156 return statusCode === 200 && body.includes('embed')
157 } 135 }
158 }, 136 },
159 { 137 {
160 title: 'HTML - homepage', 138 title: 'HTML - homepage',
161 path: '/', 139 path: '/',
162 expecter: (_client, statusCode) => { 140 expecter: (_body, status) => {
163 return statusCode === 200 141 return status === 200
164 } 142 }
165 }, 143 },
166 { 144 {
167 title: 'API - config', 145 title: 'API - config',
168 path: '/api/v1/config', 146 path: '/api/v1/config',
169 expecter: (client, statusCode) => { 147 expecter: (body, status) => {
170 const body = client.resData[0].body 148 return status === 200 && body.startsWith('{"instance":')
171
172 return statusCode === 200 && body.startsWith('{"instance":')
173 } 149 }
174 } 150 }
175 ] 151 ]
@@ -197,24 +173,27 @@ function runBenchmark (options: {
197 const { path, expecter, headers } = options 173 const { path, expecter, headers } = options
198 174
199 return new Promise((res, rej) => { 175 return new Promise((res, rej) => {
200 const instance = autocannon({ 176 autocannon({
201 url: server.url + path, 177 url: server.url + path,
202 connections: 20, 178 connections: 20,
203 headers, 179 headers,
204 pipelining: 1, 180 pipelining: 1,
205 duration: 10 181 duration: 10,
182 requests: [
183 {
184 onResponse: (status, body) => {
185 if (expecter(body, status) !== true) {
186 console.error('Expected result failed.', { body, status })
187 throw new Error('Invalid expectation')
188 }
189 }
190 }
191 ]
206 }, (err, result) => { 192 }, (err, result) => {
207 if (err) return rej(err) 193 if (err) return rej(err)
208 194
209 return res(result) 195 return res(result)
210 }) 196 })
211
212 instance.on('response', (client, statusCode) => {
213 if (expecter(client, statusCode) !== true) {
214 console.error('Expected result failed.', { data: client.resData })
215 process.exit(-1)
216 }
217 })
218 }) 197 })
219} 198}
220 199
diff --git a/scripts/parse-log.ts b/scripts/parse-log.ts
index 3679dab74..5f4480c88 100755
--- a/scripts/parse-log.ts
+++ b/scripts/parse-log.ts
@@ -15,6 +15,8 @@ import { format as sqlFormat } from 'sql-formatter'
15program 15program
16 .option('-l, --level [level]', 'Level log (debug/info/warn/error)') 16 .option('-l, --level [level]', 'Level log (debug/info/warn/error)')
17 .option('-f, --files [file...]', 'Files to parse. If not provided, the script will parse the latest log file from config)') 17 .option('-f, --files [file...]', 'Files to parse. If not provided, the script will parse the latest log file from config)')
18 .option('-t, --tags [tags...]', 'Display only lines with these tags')
19 .option('-nt, --not-tags [tags...]', 'Donrt display lines containing these tags')
18 .parse(process.argv) 20 .parse(process.argv)
19 21
20const options = program.opts() 22const options = program.opts()
@@ -24,6 +26,7 @@ const excludedKeys = {
24 message: true, 26 message: true,
25 splat: true, 27 splat: true,
26 timestamp: true, 28 timestamp: true,
29 tags: true,
27 label: true, 30 label: true,
28 sql: true 31 sql: true
29} 32}
@@ -93,6 +96,14 @@ function run () {
93 rl.on('line', line => { 96 rl.on('line', line => {
94 try { 97 try {
95 const log = JSON.parse(line) 98 const log = JSON.parse(line)
99 if (options.tags && !containsTags(log.tags, options.tags)) {
100 return
101 }
102
103 if (options.notTags && containsTags(log.tags, options.notTags)) {
104 return
105 }
106
96 // Don't know why but loggerFormat does not remove splat key 107 // Don't know why but loggerFormat does not remove splat key
97 Object.assign(log, { splat: undefined }) 108 Object.assign(log, { splat: undefined })
98 109
@@ -131,3 +142,15 @@ function toTimeFormat (time: string) {
131 142
132 return new Date(timestamp).toISOString() 143 return new Date(timestamp).toISOString()
133} 144}
145
146function containsTags (loggerTags: string[], optionsTags: string[]) {
147 if (!loggerTags) return false
148
149 for (const lt of loggerTags) {
150 for (const ot of optionsTags) {
151 if (lt === ot) return true
152 }
153 }
154
155 return false
156}
diff --git a/scripts/prune-storage.ts b/scripts/prune-storage.ts
index dcb1fcf90..bdfb335c6 100755
--- a/scripts/prune-storage.ts
+++ b/scripts/prune-storage.ts
@@ -11,7 +11,7 @@ import { VideoRedundancyModel } from '../server/models/redundancy/video-redundan
11import * as Bluebird from 'bluebird' 11import * as Bluebird from 'bluebird'
12import { getUUIDFromFilename } from '../server/helpers/utils' 12import { getUUIDFromFilename } from '../server/helpers/utils'
13import { ThumbnailModel } from '../server/models/video/thumbnail' 13import { ThumbnailModel } from '../server/models/video/thumbnail'
14import { AvatarModel } from '../server/models/avatar/avatar' 14import { ActorImageModel } from '../server/models/account/actor-image'
15import { uniq, values } from 'lodash' 15import { uniq, values } from 'lodash'
16import { ThumbnailType } from '@shared/models' 16import { ThumbnailType } from '@shared/models'
17 17
@@ -43,7 +43,7 @@ async function run () {
43 await pruneDirectory(CONFIG.STORAGE.PREVIEWS_DIR, doesThumbnailExist(true, ThumbnailType.PREVIEW)), 43 await pruneDirectory(CONFIG.STORAGE.PREVIEWS_DIR, doesThumbnailExist(true, ThumbnailType.PREVIEW)),
44 await pruneDirectory(CONFIG.STORAGE.THUMBNAILS_DIR, doesThumbnailExist(false, ThumbnailType.MINIATURE)), 44 await pruneDirectory(CONFIG.STORAGE.THUMBNAILS_DIR, doesThumbnailExist(false, ThumbnailType.MINIATURE)),
45 45
46 await pruneDirectory(CONFIG.STORAGE.AVATARS_DIR, doesAvatarExist) 46 await pruneDirectory(CONFIG.STORAGE.ACTOR_IMAGES, doesActorImageExist)
47 ) 47 )
48 48
49 const tmpFiles = await readdir(CONFIG.STORAGE.TMP_DIR) 49 const tmpFiles = await readdir(CONFIG.STORAGE.TMP_DIR)
@@ -107,10 +107,10 @@ function doesThumbnailExist (keepOnlyOwned: boolean, type: ThumbnailType) {
107 } 107 }
108} 108}
109 109
110async function doesAvatarExist (file: string) { 110async function doesActorImageExist (file: string) {
111 const avatar = await AvatarModel.loadByName(file) 111 const image = await ActorImageModel.loadByName(file)
112 112
113 return !!avatar 113 return !!image
114} 114}
115 115
116async function doesRedundancyExist (file: string) { 116async function doesRedundancyExist (file: string) {
diff --git a/scripts/regenerate-thumbnails.ts b/scripts/regenerate-thumbnails.ts
new file mode 100644
index 000000000..b95343c0b
--- /dev/null
+++ b/scripts/regenerate-thumbnails.ts
@@ -0,0 +1,68 @@
1import { registerTSPaths } from '../server/helpers/register-ts-paths'
2registerTSPaths()
3
4import * as Bluebird from 'bluebird'
5import * as program from 'commander'
6import { pathExists, remove } from 'fs-extra'
7import { generateImageFilename, processImage } from '@server/helpers/image-utils'
8import { THUMBNAILS_SIZE } from '@server/initializers/constants'
9import { VideoModel } from '@server/models/video/video'
10import { MVideo } from '@server/types/models'
11import { initDatabaseModels } from '@server/initializers/database'
12
13program
14 .description('Regenerate local thumbnails using preview files')
15 .parse(process.argv)
16
17run()
18 .then(() => process.exit(0))
19 .catch(err => console.error(err))
20
21async function run () {
22 await initDatabaseModels(true)
23
24 const videos = await VideoModel.listLocal()
25
26 await Bluebird.map(videos, v => {
27 return processVideo(v)
28 .catch(err => console.error('Cannot process video %s.', v.url, err))
29 }, { concurrency: 20 })
30}
31
32async function processVideo (videoArg: MVideo) {
33 const video = await VideoModel.loadWithFiles(videoArg.id)
34
35 console.log('Processing video %s.', video.name)
36
37 const thumbnail = video.getMiniature()
38 const preview = video.getPreview()
39
40 const previewPath = preview.getPath()
41
42 if (!await pathExists(previewPath)) {
43 throw new Error(`Preview ${previewPath} does not exist on disk`)
44 }
45
46 const size = {
47 width: THUMBNAILS_SIZE.width,
48 height: THUMBNAILS_SIZE.height
49 }
50
51 const oldPath = thumbnail.getPath()
52
53 // Update thumbnail
54 thumbnail.filename = generateImageFilename()
55 thumbnail.width = size.width
56 thumbnail.height = size.height
57
58 const thumbnailPath = thumbnail.getPath()
59 await processImage(previewPath, thumbnailPath, size, true)
60
61 // Save new attributes
62 await thumbnail.save()
63
64 // Remove old thumbnail
65 await remove(oldPath)
66
67 // Don't federate, remote instances will refresh the thumbnails after a while
68}
diff --git a/scripts/upgrade.sh b/scripts/upgrade.sh
index f5f3219af..64a7b18fd 100755
--- a/scripts/upgrade.sh
+++ b/scripts/upgrade.sh
@@ -39,8 +39,9 @@ then
39 DB_PASS=$(node -e "console.log(require('js-yaml').load(fs.readFileSync('$PEERTUBE_PATH/config/production.yaml', 'utf8'))['database']['password'])") 39 DB_PASS=$(node -e "console.log(require('js-yaml').load(fs.readFileSync('$PEERTUBE_PATH/config/production.yaml', 'utf8'))['database']['password'])")
40 DB_HOST=$(node -e "console.log(require('js-yaml').load(fs.readFileSync('$PEERTUBE_PATH/config/production.yaml', 'utf8'))['database']['hostname'])") 40 DB_HOST=$(node -e "console.log(require('js-yaml').load(fs.readFileSync('$PEERTUBE_PATH/config/production.yaml', 'utf8'))['database']['hostname'])")
41 DB_SUFFIX=$(node -e "console.log(require('js-yaml').load(fs.readFileSync('$PEERTUBE_PATH/config/production.yaml', 'utf8'))['database']['suffix'])") 41 DB_SUFFIX=$(node -e "console.log(require('js-yaml').load(fs.readFileSync('$PEERTUBE_PATH/config/production.yaml', 'utf8'))['database']['suffix'])")
42 DB_NAME=$(node -e "console.log(require('js-yaml').load(fs.readFileSync('$PEERTUBE_PATH/config/production.yaml', 'utf8'))['database']['name'] || '')")
42 mkdir -p $PEERTUBE_PATH/backup 43 mkdir -p $PEERTUBE_PATH/backup
43 PGPASSWORD=$DB_PASS pg_dump -U $DB_USER -h $DB_HOST -F c "peertube${DB_SUFFIX}" -f "$SQL_BACKUP_PATH" 44 PGPASSWORD=$DB_PASS pg_dump -U $DB_USER -h $DB_HOST -F c "${DB_NAME:-'peertube${DB_SUFFIX}'}" -f "$SQL_BACKUP_PATH"
44else 45else
45 echo "pg_dump not found. Cannot make a SQL backup!" 46 echo "pg_dump not found. Cannot make a SQL backup!"
46fi 47fi
diff --git a/server.ts b/server.ts
index 00cd87e20..f44202c9a 100644
--- a/server.ts
+++ b/server.ts
@@ -44,7 +44,7 @@ checkFFmpeg(CONFIG)
44 44
45checkNodeVersion() 45checkNodeVersion()
46 46
47import { checkConfig, checkActivityPubUrls } from './server/initializers/checker-after-init' 47import { checkConfig, checkActivityPubUrls, checkFFmpegVersion } from './server/initializers/checker-after-init'
48 48
49const errorMessage = checkConfig() 49const errorMessage = checkConfig()
50if (errorMessage !== null) { 50if (errorMessage !== null) {
@@ -120,6 +120,7 @@ import { isHTTPSignatureDigestValid } from './server/helpers/peertube-crypto'
120import { PeerTubeSocket } from './server/lib/peertube-socket' 120import { PeerTubeSocket } from './server/lib/peertube-socket'
121import { updateStreamingPlaylistsInfohashesIfNeeded } from './server/lib/hls' 121import { updateStreamingPlaylistsInfohashesIfNeeded } from './server/lib/hls'
122import { PluginsCheckScheduler } from './server/lib/schedulers/plugins-check-scheduler' 122import { PluginsCheckScheduler } from './server/lib/schedulers/plugins-check-scheduler'
123import { PeerTubeVersionCheckScheduler } from './server/lib/schedulers/peertube-version-check-scheduler'
123import { Hooks } from './server/lib/plugins/hooks' 124import { Hooks } from './server/lib/plugins/hooks'
124import { PluginManager } from './server/lib/plugins/plugin-manager' 125import { PluginManager } from './server/lib/plugins/plugin-manager'
125import { LiveManager } from './server/lib/live-manager' 126import { LiveManager } from './server/lib/live-manager'
@@ -160,7 +161,9 @@ morgan.token('user-agent', (req: express.Request) => {
160 return req.get('user-agent') 161 return req.get('user-agent')
161}) 162})
162app.use(morgan('combined', { 163app.use(morgan('combined', {
163 stream: { write: logger.info.bind(logger) }, 164 stream: {
165 write: (str: string) => logger.info(str, { tags: [ 'http' ] })
166 },
164 skip: req => CONFIG.LOG.LOG_PING_REQUESTS === false && req.originalUrl === '/api/v1/ping' 167 skip: req => CONFIG.LOG.LOG_PING_REQUESTS === false && req.originalUrl === '/api/v1/ping'
165})) 168}))
166 169
@@ -250,6 +253,9 @@ async function startApplication () {
250 process.exit(-1) 253 process.exit(-1)
251 }) 254 })
252 255
256 checkFFmpegVersion()
257 .catch(err => logger.error('Cannot check ffmpeg version', { err }))
258
253 // Email initialization 259 // Email initialization
254 Emailer.Instance.init() 260 Emailer.Instance.init()
255 261
@@ -272,6 +278,7 @@ async function startApplication () {
272 RemoveOldHistoryScheduler.Instance.enable() 278 RemoveOldHistoryScheduler.Instance.enable()
273 RemoveOldViewsScheduler.Instance.enable() 279 RemoveOldViewsScheduler.Instance.enable()
274 PluginsCheckScheduler.Instance.enable() 280 PluginsCheckScheduler.Instance.enable()
281 PeerTubeVersionCheckScheduler.Instance.enable()
275 AutoFollowIndexInstances.Instance.enable() 282 AutoFollowIndexInstances.Instance.enable()
276 283
277 // Redis initialization 284 // Redis initialization
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts
index fb108ca1c..e28f7502d 100644
--- a/server/controllers/api/config.ts
+++ b/server/controllers/api/config.ts
@@ -158,9 +158,17 @@ async function getConfig (req: express.Request, res: express.Response) {
158 avatar: { 158 avatar: {
159 file: { 159 file: {
160 size: { 160 size: {
161 max: CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max 161 max: CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max
162 }, 162 },
163 extensions: CONSTRAINTS_FIELDS.ACTORS.AVATAR.EXTNAME 163 extensions: CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME
164 }
165 },
166 banner: {
167 file: {
168 size: {
169 max: CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max
170 },
171 extensions: CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME
164 } 172 }
165 }, 173 },
166 video: { 174 video: {
diff --git a/server/controllers/api/jobs.ts b/server/controllers/api/jobs.ts
index 861cc22b9..d7cee1605 100644
--- a/server/controllers/api/jobs.ts
+++ b/server/controllers/api/jobs.ts
@@ -9,10 +9,10 @@ import {
9 authenticate, 9 authenticate,
10 ensureUserHasRight, 10 ensureUserHasRight,
11 jobsSortValidator, 11 jobsSortValidator,
12 paginationValidatorBuilder,
12 setDefaultPagination, 13 setDefaultPagination,
13 setDefaultSort 14 setDefaultSort
14} from '../../middlewares' 15} from '../../middlewares'
15import { paginationValidator } from '../../middlewares/validators'
16import { listJobsValidator } from '../../middlewares/validators/jobs' 16import { listJobsValidator } from '../../middlewares/validators/jobs'
17 17
18const jobsRouter = express.Router() 18const jobsRouter = express.Router()
@@ -20,7 +20,7 @@ const jobsRouter = express.Router()
20jobsRouter.get('/:state?', 20jobsRouter.get('/:state?',
21 authenticate, 21 authenticate,
22 ensureUserHasRight(UserRight.MANAGE_JOBS), 22 ensureUserHasRight(UserRight.MANAGE_JOBS),
23 paginationValidator, 23 paginationValidatorBuilder([ 'jobs' ]),
24 jobsSortValidator, 24 jobsSortValidator,
25 setDefaultSort, 25 setDefaultSort,
26 setDefaultPagination, 26 setDefaultPagination,
diff --git a/server/controllers/api/plugins.ts b/server/controllers/api/plugins.ts
index 1c0b5edb1..bb69f25a1 100644
--- a/server/controllers/api/plugins.ts
+++ b/server/controllers/api/plugins.ts
@@ -205,7 +205,6 @@ async function listAvailablePlugins (req: express.Request, res: express.Response
205 if (!resultList) { 205 if (!resultList) {
206 return res.status(HttpStatusCode.SERVICE_UNAVAILABLE_503) 206 return res.status(HttpStatusCode.SERVICE_UNAVAILABLE_503)
207 .json({ error: 'Plugin index unavailable. Please retry later' }) 207 .json({ error: 'Plugin index unavailable. Please retry later' })
208 .end()
209 } 208 }
210 209
211 return res.json(resultList) 210 return res.json(resultList)
diff --git a/server/controllers/api/search.ts b/server/controllers/api/search.ts
index 7e1b7b230..f0cdf3a89 100644
--- a/server/controllers/api/search.ts
+++ b/server/controllers/api/search.ts
@@ -1,8 +1,9 @@
1import * as express from 'express' 1import * as express from 'express'
2import { sanitizeUrl } from '@server/helpers/core-utils' 2import { sanitizeUrl } from '@server/helpers/core-utils'
3import { doRequest } from '@server/helpers/requests' 3import { doJSONRequest } from '@server/helpers/requests'
4import { CONFIG } from '@server/initializers/config' 4import { CONFIG } from '@server/initializers/config'
5import { getOrCreateVideoAndAccountAndChannel } from '@server/lib/activitypub/videos' 5import { getOrCreateVideoAndAccountAndChannel } from '@server/lib/activitypub/videos'
6import { Hooks } from '@server/lib/plugins/hooks'
6import { AccountBlocklistModel } from '@server/models/account/account-blocklist' 7import { AccountBlocklistModel } from '@server/models/account/account-blocklist'
7import { getServerActor } from '@server/models/application/application' 8import { getServerActor } from '@server/models/application/application'
8import { ServerBlocklistModel } from '@server/models/server/server-blocklist' 9import { ServerBlocklistModel } from '@server/models/server/server-blocklist'
@@ -22,8 +23,8 @@ import {
22 paginationValidator, 23 paginationValidator,
23 setDefaultPagination, 24 setDefaultPagination,
24 setDefaultSearchSort, 25 setDefaultSearchSort,
25 videoChannelsSearchSortValidator,
26 videoChannelsListSearchValidator, 26 videoChannelsListSearchValidator,
27 videoChannelsSearchSortValidator,
27 videosSearchSortValidator, 28 videosSearchSortValidator,
28 videosSearchValidator 29 videosSearchValidator
29} from '../../middlewares' 30} from '../../middlewares'
@@ -87,16 +88,17 @@ function searchVideoChannels (req: express.Request, res: express.Response) {
87async function searchVideoChannelsIndex (query: VideoChannelsSearchQuery, res: express.Response) { 88async function searchVideoChannelsIndex (query: VideoChannelsSearchQuery, res: express.Response) {
88 const result = await buildMutedForSearchIndex(res) 89 const result = await buildMutedForSearchIndex(res)
89 90
90 const body = Object.assign(query, result) 91 const body = await Hooks.wrapObject(Object.assign(query, result), 'filter:api.search.video-channels.index.list.params')
91 92
92 const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/video-channels' 93 const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/video-channels'
93 94
94 try { 95 try {
95 logger.debug('Doing video channels search index request on %s.', url, { body }) 96 logger.debug('Doing video channels search index request on %s.', url, { body })
96 97
97 const searchIndexResult = await doRequest<ResultList<VideoChannel>>({ uri: url, body, json: true }) 98 const { body: searchIndexResult } = await doJSONRequest<ResultList<VideoChannel>>(url, { method: 'POST', json: body })
99 const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.video-channels.index.list.result')
98 100
99 return res.json(searchIndexResult.body) 101 return res.json(jsonResult)
100 } catch (err) { 102 } catch (err) {
101 logger.warn('Cannot use search index to make video channels search.', { err }) 103 logger.warn('Cannot use search index to make video channels search.', { err })
102 104
@@ -107,14 +109,19 @@ async function searchVideoChannelsIndex (query: VideoChannelsSearchQuery, res: e
107async function searchVideoChannelsDB (query: VideoChannelsSearchQuery, res: express.Response) { 109async function searchVideoChannelsDB (query: VideoChannelsSearchQuery, res: express.Response) {
108 const serverActor = await getServerActor() 110 const serverActor = await getServerActor()
109 111
110 const options = { 112 const apiOptions = await Hooks.wrapObject({
111 actorId: serverActor.id, 113 actorId: serverActor.id,
112 search: query.search, 114 search: query.search,
113 start: query.start, 115 start: query.start,
114 count: query.count, 116 count: query.count,
115 sort: query.sort 117 sort: query.sort
116 } 118 }, 'filter:api.search.video-channels.local.list.params')
117 const resultList = await VideoChannelModel.searchForApi(options) 119
120 const resultList = await Hooks.wrapPromiseFun(
121 VideoChannelModel.searchForApi,
122 apiOptions,
123 'filter:api.search.video-channels.local.list.result'
124 )
118 125
119 return res.json(getFormattedObjects(resultList.data, resultList.total)) 126 return res.json(getFormattedObjects(resultList.data, resultList.total))
120} 127}
@@ -168,7 +175,7 @@ function searchVideos (req: express.Request, res: express.Response) {
168async function searchVideosIndex (query: VideosSearchQuery, res: express.Response) { 175async function searchVideosIndex (query: VideosSearchQuery, res: express.Response) {
169 const result = await buildMutedForSearchIndex(res) 176 const result = await buildMutedForSearchIndex(res)
170 177
171 const body: VideosSearchQuery = Object.assign(query, result) 178 let body: VideosSearchQuery = Object.assign(query, result)
172 179
173 // Use the default instance NSFW policy if not specified 180 // Use the default instance NSFW policy if not specified
174 if (!body.nsfw) { 181 if (!body.nsfw) {
@@ -181,14 +188,17 @@ async function searchVideosIndex (query: VideosSearchQuery, res: express.Respons
181 : 'both' 188 : 'both'
182 } 189 }
183 190
191 body = await Hooks.wrapObject(body, 'filter:api.search.videos.index.list.params')
192
184 const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/videos' 193 const url = sanitizeUrl(CONFIG.SEARCH.SEARCH_INDEX.URL) + '/api/v1/search/videos'
185 194
186 try { 195 try {
187 logger.debug('Doing videos search index request on %s.', url, { body }) 196 logger.debug('Doing videos search index request on %s.', url, { body })
188 197
189 const searchIndexResult = await doRequest<ResultList<Video>>({ uri: url, body, json: true }) 198 const { body: searchIndexResult } = await doJSONRequest<ResultList<Video>>(url, { method: 'POST', json: body })
199 const jsonResult = await Hooks.wrapObject(searchIndexResult, 'filter:api.search.videos.index.list.result')
190 200
191 return res.json(searchIndexResult.body) 201 return res.json(jsonResult)
192 } catch (err) { 202 } catch (err) {
193 logger.warn('Cannot use search index to make video search.', { err }) 203 logger.warn('Cannot use search index to make video search.', { err })
194 204
@@ -197,13 +207,18 @@ async function searchVideosIndex (query: VideosSearchQuery, res: express.Respons
197} 207}
198 208
199async function searchVideosDB (query: VideosSearchQuery, res: express.Response) { 209async function searchVideosDB (query: VideosSearchQuery, res: express.Response) {
200 const options = Object.assign(query, { 210 const apiOptions = await Hooks.wrapObject(Object.assign(query, {
201 includeLocalVideos: true, 211 includeLocalVideos: true,
202 nsfw: buildNSFWFilter(res, query.nsfw), 212 nsfw: buildNSFWFilter(res, query.nsfw),
203 filter: query.filter, 213 filter: query.filter,
204 user: res.locals.oauth ? res.locals.oauth.token.User : undefined 214 user: res.locals.oauth ? res.locals.oauth.token.User : undefined
205 }) 215 }), 'filter:api.search.videos.local.list.params')
206 const resultList = await VideoModel.searchAndPopulateAccountAndServer(options) 216
217 const resultList = await Hooks.wrapPromiseFun(
218 VideoModel.searchAndPopulateAccountAndServer,
219 apiOptions,
220 'filter:api.search.videos.local.list.result'
221 )
207 222
208 return res.json(getFormattedObjects(resultList.data, resultList.total)) 223 return res.json(getFormattedObjects(resultList.data, resultList.total))
209} 224}
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts
index 3be1d55ae..e2b1ea7cd 100644
--- a/server/controllers/api/users/index.ts
+++ b/server/controllers/api/users/index.ts
@@ -2,8 +2,10 @@ import * as express from 'express'
2import * as RateLimit from 'express-rate-limit' 2import * as RateLimit from 'express-rate-limit'
3import { tokensRouter } from '@server/controllers/api/users/token' 3import { tokensRouter } from '@server/controllers/api/users/token'
4import { Hooks } from '@server/lib/plugins/hooks' 4import { Hooks } from '@server/lib/plugins/hooks'
5import { OAuthTokenModel } from '@server/models/oauth/oauth-token'
5import { MUser, MUserAccountDefault } from '@server/types/models' 6import { MUser, MUserAccountDefault } from '@server/types/models'
6import { UserCreate, UserRight, UserRole, UserUpdate } from '../../../../shared' 7import { UserCreate, UserRight, UserRole, UserUpdate } from '../../../../shared'
8import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
7import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' 9import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
8import { UserRegister } from '../../../../shared/models/users/user-register.model' 10import { UserRegister } from '../../../../shared/models/users/user-register.model'
9import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger' 11import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../helpers/audit-logger'
@@ -14,7 +16,6 @@ import { WEBSERVER } from '../../../initializers/constants'
14import { sequelizeTypescript } from '../../../initializers/database' 16import { sequelizeTypescript } from '../../../initializers/database'
15import { Emailer } from '../../../lib/emailer' 17import { Emailer } from '../../../lib/emailer'
16import { Notifier } from '../../../lib/notifier' 18import { Notifier } from '../../../lib/notifier'
17import { deleteUserToken } from '../../../lib/oauth-model'
18import { Redis } from '../../../lib/redis' 19import { Redis } from '../../../lib/redis'
19import { createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user' 20import { createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user'
20import { 21import {
@@ -52,7 +53,6 @@ import { myVideosHistoryRouter } from './my-history'
52import { myNotificationsRouter } from './my-notifications' 53import { myNotificationsRouter } from './my-notifications'
53import { mySubscriptionsRouter } from './my-subscriptions' 54import { mySubscriptionsRouter } from './my-subscriptions'
54import { myVideoPlaylistsRouter } from './my-video-playlists' 55import { myVideoPlaylistsRouter } from './my-video-playlists'
55import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
56 56
57const auditLogger = auditLoggerFactory('users') 57const auditLogger = auditLoggerFactory('users')
58 58
@@ -335,7 +335,7 @@ async function updateUser (req: express.Request, res: express.Response) {
335 const user = await userToUpdate.save() 335 const user = await userToUpdate.save()
336 336
337 // Destroy user token to refresh rights 337 // Destroy user token to refresh rights
338 if (roleChanged || body.password !== undefined) await deleteUserToken(userToUpdate.id) 338 if (roleChanged || body.password !== undefined) await OAuthTokenModel.deleteUserToken(userToUpdate.id)
339 339
340 auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()), oldUserAuditView) 340 auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()), oldUserAuditView)
341 341
@@ -395,7 +395,7 @@ async function changeUserBlock (res: express.Response, user: MUserAccountDefault
395 user.blockedReason = reason || null 395 user.blockedReason = reason || null
396 396
397 await sequelizeTypescript.transaction(async t => { 397 await sequelizeTypescript.transaction(async t => {
398 await deleteUserToken(user.id, t) 398 await OAuthTokenModel.deleteUserToken(user.id, t)
399 399
400 await user.save({ transaction: t }) 400 await user.save({ transaction: t })
401 }) 401 })
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts
index 5a3e9e51a..9f9d2d77f 100644
--- a/server/controllers/api/users/me.ts
+++ b/server/controllers/api/users/me.ts
@@ -2,7 +2,7 @@ import 'multer'
2import * as express from 'express' 2import * as express from 'express'
3import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '@server/helpers/audit-logger' 3import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '@server/helpers/audit-logger'
4import { Hooks } from '@server/lib/plugins/hooks' 4import { Hooks } from '@server/lib/plugins/hooks'
5import { UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../../shared' 5import { ActorImageType, UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../../shared'
6import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' 6import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
7import { UserVideoQuota } from '../../../../shared/models/users/user-video-quota.model' 7import { UserVideoQuota } from '../../../../shared/models/users/user-video-quota.model'
8import { createReqFiles } from '../../../helpers/express-utils' 8import { createReqFiles } from '../../../helpers/express-utils'
@@ -11,7 +11,7 @@ import { CONFIG } from '../../../initializers/config'
11import { MIMETYPES } from '../../../initializers/constants' 11import { MIMETYPES } from '../../../initializers/constants'
12import { sequelizeTypescript } from '../../../initializers/database' 12import { sequelizeTypescript } from '../../../initializers/database'
13import { sendUpdateActor } from '../../../lib/activitypub/send' 13import { sendUpdateActor } from '../../../lib/activitypub/send'
14import { deleteLocalActorAvatarFile, updateLocalActorAvatarFile } from '../../../lib/avatar' 14import { deleteLocalActorImageFile, updateLocalActorImageFile } from '../../../lib/actor-image'
15import { getOriginalVideoFileTotalDailyFromUser, getOriginalVideoFileTotalFromUser, sendVerifyUserEmail } from '../../../lib/user' 15import { getOriginalVideoFileTotalDailyFromUser, getOriginalVideoFileTotalFromUser, sendVerifyUserEmail } from '../../../lib/user'
16import { 16import {
17 asyncMiddleware, 17 asyncMiddleware,
@@ -25,7 +25,7 @@ import {
25 usersVideoRatingValidator 25 usersVideoRatingValidator
26} from '../../../middlewares' 26} from '../../../middlewares'
27import { deleteMeValidator, videoImportsSortValidator, videosSortValidator } from '../../../middlewares/validators' 27import { deleteMeValidator, videoImportsSortValidator, videosSortValidator } from '../../../middlewares/validators'
28import { updateAvatarValidator } from '../../../middlewares/validators/avatar' 28import { updateAvatarValidator } from '../../../middlewares/validators/actor-image'
29import { AccountModel } from '../../../models/account/account' 29import { AccountModel } from '../../../models/account/account'
30import { AccountVideoRateModel } from '../../../models/account/account-video-rate' 30import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
31import { UserModel } from '../../../models/account/user' 31import { UserModel } from '../../../models/account/user'
@@ -238,7 +238,7 @@ async function updateMyAvatar (req: express.Request, res: express.Response) {
238 238
239 const userAccount = await AccountModel.load(user.Account.id) 239 const userAccount = await AccountModel.load(user.Account.id)
240 240
241 const avatar = await updateLocalActorAvatarFile(userAccount, avatarPhysicalFile) 241 const avatar = await updateLocalActorImageFile(userAccount, avatarPhysicalFile, ActorImageType.AVATAR)
242 242
243 return res.json({ avatar: avatar.toFormattedJSON() }) 243 return res.json({ avatar: avatar.toFormattedJSON() })
244} 244}
@@ -247,7 +247,7 @@ async function deleteMyAvatar (req: express.Request, res: express.Response) {
247 const user = res.locals.oauth.token.user 247 const user = res.locals.oauth.token.user
248 248
249 const userAccount = await AccountModel.load(user.Account.id) 249 const userAccount = await AccountModel.load(user.Account.id)
250 await deleteLocalActorAvatarFile(userAccount) 250 await deleteLocalActorImageFile(userAccount, ActorImageType.AVATAR)
251 251
252 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 252 return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
253} 253}
diff --git a/server/controllers/api/users/my-notifications.ts b/server/controllers/api/users/my-notifications.ts
index 5f5e4c5e6..0a9101a46 100644
--- a/server/controllers/api/users/my-notifications.ts
+++ b/server/controllers/api/users/my-notifications.ts
@@ -80,7 +80,9 @@ async function updateNotificationSettings (req: express.Request, res: express.Re
80 newInstanceFollower: body.newInstanceFollower, 80 newInstanceFollower: body.newInstanceFollower,
81 autoInstanceFollowing: body.autoInstanceFollowing, 81 autoInstanceFollowing: body.autoInstanceFollowing,
82 abuseNewMessage: body.abuseNewMessage, 82 abuseNewMessage: body.abuseNewMessage,
83 abuseStateChange: body.abuseStateChange 83 abuseStateChange: body.abuseStateChange,
84 newPeerTubeVersion: body.newPeerTubeVersion,
85 newPluginVersion: body.newPluginVersion
84 } 86 }
85 87
86 await UserNotificationSettingModel.update(values, query) 88 await UserNotificationSettingModel.update(values, query)
diff --git a/server/controllers/api/users/my-subscriptions.ts b/server/controllers/api/users/my-subscriptions.ts
index ec77ddd7a..e8949ee59 100644
--- a/server/controllers/api/users/my-subscriptions.ts
+++ b/server/controllers/api/users/my-subscriptions.ts
@@ -1,5 +1,8 @@
1import 'multer' 1import 'multer'
2import * as express from 'express' 2import * as express from 'express'
3import { sendUndoFollow } from '@server/lib/activitypub/send'
4import { VideoChannelModel } from '@server/models/video/video-channel'
5import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
3import { VideoFilter } from '../../../../shared/models/videos/video-query.type' 6import { VideoFilter } from '../../../../shared/models/videos/video-query.type'
4import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils' 7import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils'
5import { getFormattedObjects } from '../../../helpers/utils' 8import { getFormattedObjects } from '../../../helpers/utils'
@@ -26,8 +29,6 @@ import {
26} from '../../../middlewares/validators' 29} from '../../../middlewares/validators'
27import { ActorFollowModel } from '../../../models/activitypub/actor-follow' 30import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
28import { VideoModel } from '../../../models/video/video' 31import { VideoModel } from '../../../models/video/video'
29import { sendUndoFollow } from '@server/lib/activitypub/send'
30import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
31 32
32const mySubscriptionsRouter = express.Router() 33const mySubscriptionsRouter = express.Router()
33 34
@@ -66,7 +67,7 @@ mySubscriptionsRouter.post('/me/subscriptions',
66mySubscriptionsRouter.get('/me/subscriptions/:uri', 67mySubscriptionsRouter.get('/me/subscriptions/:uri',
67 authenticate, 68 authenticate,
68 userSubscriptionGetValidator, 69 userSubscriptionGetValidator,
69 getUserSubscription 70 asyncMiddleware(getUserSubscription)
70) 71)
71 72
72mySubscriptionsRouter.delete('/me/subscriptions/:uri', 73mySubscriptionsRouter.delete('/me/subscriptions/:uri',
@@ -130,10 +131,11 @@ function addUserSubscription (req: express.Request, res: express.Response) {
130 return res.status(HttpStatusCode.NO_CONTENT_204).end() 131 return res.status(HttpStatusCode.NO_CONTENT_204).end()
131} 132}
132 133
133function getUserSubscription (req: express.Request, res: express.Response) { 134async function getUserSubscription (req: express.Request, res: express.Response) {
134 const subscription = res.locals.subscription 135 const subscription = res.locals.subscription
136 const videoChannel = await VideoChannelModel.loadAndPopulateAccount(subscription.ActorFollowing.VideoChannel.id)
135 137
136 return res.json(subscription.ActorFollowing.VideoChannel.toFormattedJSON()) 138 return res.json(videoChannel.toFormattedJSON())
137} 139}
138 140
139async function deleteUserSubscription (req: express.Request, res: express.Response) { 141async function deleteUserSubscription (req: express.Request, res: express.Response) {
diff --git a/server/controllers/api/users/token.ts b/server/controllers/api/users/token.ts
index 821429358..694bb0a92 100644
--- a/server/controllers/api/users/token.ts
+++ b/server/controllers/api/users/token.ts
@@ -1,11 +1,14 @@
1import { handleLogin, handleTokenRevocation } from '@server/lib/auth' 1import * as express from 'express'
2import * as RateLimit from 'express-rate-limit' 2import * as RateLimit from 'express-rate-limit'
3import { v4 as uuidv4 } from 'uuid'
4import { logger } from '@server/helpers/logger'
3import { CONFIG } from '@server/initializers/config' 5import { CONFIG } from '@server/initializers/config'
4import * as express from 'express' 6import { getAuthNameFromRefreshGrant, getBypassFromExternalAuth, getBypassFromPasswordGrant } from '@server/lib/auth/external-auth'
7import { handleOAuthToken } from '@server/lib/auth/oauth'
8import { BypassLogin, revokeToken } from '@server/lib/auth/oauth-model'
5import { Hooks } from '@server/lib/plugins/hooks' 9import { Hooks } from '@server/lib/plugins/hooks'
6import { asyncMiddleware, authenticate } from '@server/middlewares' 10import { asyncMiddleware, authenticate } from '@server/middlewares'
7import { ScopedToken } from '@shared/models/users/user-scoped-token' 11import { ScopedToken } from '@shared/models/users/user-scoped-token'
8import { v4 as uuidv4 } from 'uuid'
9 12
10const tokensRouter = express.Router() 13const tokensRouter = express.Router()
11 14
@@ -16,8 +19,7 @@ const loginRateLimiter = RateLimit({
16 19
17tokensRouter.post('/token', 20tokensRouter.post('/token',
18 loginRateLimiter, 21 loginRateLimiter,
19 handleLogin, 22 asyncMiddleware(handleToken)
20 tokenSuccess
21) 23)
22 24
23tokensRouter.post('/revoke-token', 25tokensRouter.post('/revoke-token',
@@ -42,10 +44,53 @@ export {
42} 44}
43// --------------------------------------------------------------------------- 45// ---------------------------------------------------------------------------
44 46
45function tokenSuccess (req: express.Request) { 47async function handleToken (req: express.Request, res: express.Response, next: express.NextFunction) {
46 const username = req.body.username 48 const grantType = req.body.grant_type
49
50 try {
51 const bypassLogin = await buildByPassLogin(req, grantType)
52
53 const refreshTokenAuthName = grantType === 'refresh_token'
54 ? await getAuthNameFromRefreshGrant(req.body.refresh_token)
55 : undefined
56
57 const options = {
58 refreshTokenAuthName,
59 bypassLogin
60 }
61
62 const token = await handleOAuthToken(req, options)
63
64 res.set('Cache-Control', 'no-store')
65 res.set('Pragma', 'no-cache')
66
67 Hooks.runAction('action:api.user.oauth2-got-token', { username: token.user.username, ip: req.ip })
68
69 return res.json({
70 token_type: 'Bearer',
47 71
48 Hooks.runAction('action:api.user.oauth2-got-token', { username, ip: req.ip }) 72 access_token: token.accessToken,
73 refresh_token: token.refreshToken,
74
75 expires_in: token.accessTokenExpiresIn,
76 refresh_token_expires_in: token.refreshTokenExpiresIn
77 })
78 } catch (err) {
79 logger.warn('Login error', { err })
80
81 return res.status(err.code || 400).json({
82 code: err.name,
83 error: err.message
84 })
85 }
86}
87
88async function handleTokenRevocation (req: express.Request, res: express.Response) {
89 const token = res.locals.oauth.token
90
91 const result = await revokeToken(token, { req, explicitLogout: true })
92
93 return res.json(result)
49} 94}
50 95
51function getScopedTokens (req: express.Request, res: express.Response) { 96function getScopedTokens (req: express.Request, res: express.Response) {
@@ -66,3 +111,14 @@ async function renewScopedTokens (req: express.Request, res: express.Response) {
66 feedToken: user.feedToken 111 feedToken: user.feedToken
67 } as ScopedToken) 112 } as ScopedToken)
68} 113}
114
115async function buildByPassLogin (req: express.Request, grantType: string): Promise<BypassLogin> {
116 if (grantType !== 'password') return undefined
117
118 if (req.body.externalAuthToken) {
119 // Consistency with the getBypassFromPasswordGrant promise
120 return getBypassFromExternalAuth(req.body.username, req.body.externalAuthToken)
121 }
122
123 return getBypassFromPasswordGrant(req.body.username, req.body.password)
124}
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts
index 03617dc8d..149d6cfb4 100644
--- a/server/controllers/api/video-channel.ts
+++ b/server/controllers/api/video-channel.ts
@@ -1,8 +1,8 @@
1import * as express from 'express' 1import * as express from 'express'
2import { Hooks } from '@server/lib/plugins/hooks' 2import { Hooks } from '@server/lib/plugins/hooks'
3import { getServerActor } from '@server/models/application/application' 3import { getServerActor } from '@server/models/application/application'
4import { MChannelAccountDefault } from '@server/types/models' 4import { MChannelBannerAccountDefault } from '@server/types/models'
5import { VideoChannelCreate, VideoChannelUpdate } from '../../../shared' 5import { ActorImageType, VideoChannelCreate, VideoChannelUpdate } from '../../../shared'
6import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' 6import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
7import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger' 7import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger'
8import { resetSequelizeInstance } from '../../helpers/database-utils' 8import { resetSequelizeInstance } from '../../helpers/database-utils'
@@ -13,7 +13,7 @@ import { CONFIG } from '../../initializers/config'
13import { MIMETYPES } from '../../initializers/constants' 13import { MIMETYPES } from '../../initializers/constants'
14import { sequelizeTypescript } from '../../initializers/database' 14import { sequelizeTypescript } from '../../initializers/database'
15import { sendUpdateActor } from '../../lib/activitypub/send' 15import { sendUpdateActor } from '../../lib/activitypub/send'
16import { deleteLocalActorAvatarFile, updateLocalActorAvatarFile } from '../../lib/avatar' 16import { deleteLocalActorImageFile, updateLocalActorImageFile } from '../../lib/actor-image'
17import { JobQueue } from '../../lib/job-queue' 17import { JobQueue } from '../../lib/job-queue'
18import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel' 18import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel'
19import { 19import {
@@ -33,7 +33,7 @@ import {
33 videoPlaylistsSortValidator 33 videoPlaylistsSortValidator
34} from '../../middlewares' 34} from '../../middlewares'
35import { videoChannelsNameWithHostValidator, videoChannelsOwnSearchValidator, videosSortValidator } from '../../middlewares/validators' 35import { videoChannelsNameWithHostValidator, videoChannelsOwnSearchValidator, videosSortValidator } from '../../middlewares/validators'
36import { updateAvatarValidator } from '../../middlewares/validators/avatar' 36import { updateAvatarValidator, updateBannerValidator } from '../../middlewares/validators/actor-image'
37import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists' 37import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists'
38import { AccountModel } from '../../models/account/account' 38import { AccountModel } from '../../models/account/account'
39import { VideoModel } from '../../models/video/video' 39import { VideoModel } from '../../models/video/video'
@@ -42,6 +42,7 @@ import { VideoPlaylistModel } from '../../models/video/video-playlist'
42 42
43const auditLogger = auditLoggerFactory('channels') 43const auditLogger = auditLoggerFactory('channels')
44const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR }) 44const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR })
45const reqBannerFile = createReqFiles([ 'bannerfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { bannerfile: CONFIG.STORAGE.TMP_DIR })
45 46
46const videoChannelRouter = express.Router() 47const videoChannelRouter = express.Router()
47 48
@@ -69,6 +70,15 @@ videoChannelRouter.post('/:nameWithHost/avatar/pick',
69 asyncMiddleware(updateVideoChannelAvatar) 70 asyncMiddleware(updateVideoChannelAvatar)
70) 71)
71 72
73videoChannelRouter.post('/:nameWithHost/banner/pick',
74 authenticate,
75 reqBannerFile,
76 // Check the rights
77 asyncMiddleware(videoChannelsUpdateValidator),
78 updateBannerValidator,
79 asyncMiddleware(updateVideoChannelBanner)
80)
81
72videoChannelRouter.delete('/:nameWithHost/avatar', 82videoChannelRouter.delete('/:nameWithHost/avatar',
73 authenticate, 83 authenticate,
74 // Check the rights 84 // Check the rights
@@ -76,6 +86,13 @@ videoChannelRouter.delete('/:nameWithHost/avatar',
76 asyncMiddleware(deleteVideoChannelAvatar) 86 asyncMiddleware(deleteVideoChannelAvatar)
77) 87)
78 88
89videoChannelRouter.delete('/:nameWithHost/banner',
90 authenticate,
91 // Check the rights
92 asyncMiddleware(videoChannelsUpdateValidator),
93 asyncMiddleware(deleteVideoChannelBanner)
94)
95
79videoChannelRouter.put('/:nameWithHost', 96videoChannelRouter.put('/:nameWithHost',
80 authenticate, 97 authenticate,
81 asyncMiddleware(videoChannelsUpdateValidator), 98 asyncMiddleware(videoChannelsUpdateValidator),
@@ -134,26 +151,41 @@ async function listVideoChannels (req: express.Request, res: express.Response) {
134 return res.json(getFormattedObjects(resultList.data, resultList.total)) 151 return res.json(getFormattedObjects(resultList.data, resultList.total))
135} 152}
136 153
154async function updateVideoChannelBanner (req: express.Request, res: express.Response) {
155 const bannerPhysicalFile = req.files['bannerfile'][0]
156 const videoChannel = res.locals.videoChannel
157 const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON())
158
159 const banner = await updateLocalActorImageFile(videoChannel, bannerPhysicalFile, ActorImageType.BANNER)
160
161 auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys)
162
163 return res.json({ banner: banner.toFormattedJSON() })
164}
137async function updateVideoChannelAvatar (req: express.Request, res: express.Response) { 165async function updateVideoChannelAvatar (req: express.Request, res: express.Response) {
138 const avatarPhysicalFile = req.files['avatarfile'][0] 166 const avatarPhysicalFile = req.files['avatarfile'][0]
139 const videoChannel = res.locals.videoChannel 167 const videoChannel = res.locals.videoChannel
140 const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON()) 168 const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON())
141 169
142 const avatar = await updateLocalActorAvatarFile(videoChannel, avatarPhysicalFile) 170 const avatar = await updateLocalActorImageFile(videoChannel, avatarPhysicalFile, ActorImageType.AVATAR)
143 171
144 auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys) 172 auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys)
145 173
146 return res 174 return res.json({ avatar: avatar.toFormattedJSON() })
147 .json({
148 avatar: avatar.toFormattedJSON()
149 })
150 .end()
151} 175}
152 176
153async function deleteVideoChannelAvatar (req: express.Request, res: express.Response) { 177async function deleteVideoChannelAvatar (req: express.Request, res: express.Response) {
154 const videoChannel = res.locals.videoChannel 178 const videoChannel = res.locals.videoChannel
155 179
156 await deleteLocalActorAvatarFile(videoChannel) 180 await deleteLocalActorImageFile(videoChannel, ActorImageType.AVATAR)
181
182 return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
183}
184
185async function deleteVideoChannelBanner (req: express.Request, res: express.Response) {
186 const videoChannel = res.locals.videoChannel
187
188 await deleteLocalActorImageFile(videoChannel, ActorImageType.BANNER)
157 189
158 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 190 return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
159} 191}
@@ -177,7 +209,7 @@ async function addVideoChannel (req: express.Request, res: express.Response) {
177 videoChannel: { 209 videoChannel: {
178 id: videoChannelCreated.id 210 id: videoChannelCreated.id
179 } 211 }
180 }).end() 212 })
181} 213}
182 214
183async function updateVideoChannel (req: express.Request, res: express.Response) { 215async function updateVideoChannel (req: express.Request, res: express.Response) {
@@ -206,7 +238,7 @@ async function updateVideoChannel (req: express.Request, res: express.Response)
206 } 238 }
207 } 239 }
208 240
209 const videoChannelInstanceUpdated = await videoChannelInstance.save(sequelizeOptions) as MChannelAccountDefault 241 const videoChannelInstanceUpdated = await videoChannelInstance.save(sequelizeOptions) as MChannelBannerAccountDefault
210 await sendUpdateActor(videoChannelInstanceUpdated, t) 242 await sendUpdateActor(videoChannelInstanceUpdated, t)
211 243
212 auditLogger.update( 244 auditLogger.update(
@@ -252,13 +284,13 @@ async function removeVideoChannel (req: express.Request, res: express.Response)
252} 284}
253 285
254async function getVideoChannel (req: express.Request, res: express.Response) { 286async function getVideoChannel (req: express.Request, res: express.Response) {
255 const videoChannelWithVideos = await VideoChannelModel.loadAndPopulateAccountAndVideos(res.locals.videoChannel.id) 287 const videoChannel = res.locals.videoChannel
256 288
257 if (videoChannelWithVideos.isOutdated()) { 289 if (videoChannel.isOutdated()) {
258 JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'actor', url: videoChannelWithVideos.Actor.url } }) 290 JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'actor', url: videoChannel.Actor.url } })
259 } 291 }
260 292
261 return res.json(videoChannelWithVideos.toFormattedJSON()) 293 return res.json(videoChannel.toFormattedJSON())
262} 294}
263 295
264async function listVideoChannelPlaylists (req: express.Request, res: express.Response) { 296async function listVideoChannelPlaylists (req: express.Request, res: express.Response) {
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index 2447c1288..7fee278f2 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -17,7 +17,7 @@ import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../
17import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils' 17import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils'
18import { buildNSFWFilter, createReqFiles, getCountVideos } from '../../../helpers/express-utils' 18import { buildNSFWFilter, createReqFiles, getCountVideos } from '../../../helpers/express-utils'
19import { getMetadataFromFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils' 19import { getMetadataFromFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils'
20import { logger } from '../../../helpers/logger' 20import { logger, loggerTagsFactory } from '../../../helpers/logger'
21import { getFormattedObjects } from '../../../helpers/utils' 21import { getFormattedObjects } from '../../../helpers/utils'
22import { CONFIG } from '../../../initializers/config' 22import { CONFIG } from '../../../initializers/config'
23import { 23import {
@@ -67,6 +67,7 @@ import { ownershipVideoRouter } from './ownership'
67import { rateVideoRouter } from './rate' 67import { rateVideoRouter } from './rate'
68import { watchingRouter } from './watching' 68import { watchingRouter } from './watching'
69 69
70const lTags = loggerTagsFactory('api', 'video')
70const auditLogger = auditLoggerFactory('videos') 71const auditLogger = auditLoggerFactory('videos')
71const videosRouter = express.Router() 72const videosRouter = express.Router()
72 73
@@ -257,14 +258,14 @@ async function addVideo (req: express.Request, res: express.Response) {
257 }) 258 })
258 259
259 auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON())) 260 auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON()))
260 logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid) 261 logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid, lTags(videoCreated.uuid))
261 262
262 return { videoCreated } 263 return { videoCreated }
263 }) 264 })
264 265
265 // Create the torrent file in async way because it could be long 266 // Create the torrent file in async way because it could be long
266 createTorrentAndSetInfoHashAsync(video, videoFile) 267 createTorrentAndSetInfoHashAsync(video, videoFile)
267 .catch(err => logger.error('Cannot create torrent file for video %s', video.url, { err })) 268 .catch(err => logger.error('Cannot create torrent file for video %s', video.url, { err, ...lTags(video.uuid) }))
268 .then(() => VideoModel.loadAndPopulateAccountAndServerAndTags(video.id)) 269 .then(() => VideoModel.loadAndPopulateAccountAndServerAndTags(video.id))
269 .then(refreshedVideo => { 270 .then(refreshedVideo => {
270 if (!refreshedVideo) return 271 if (!refreshedVideo) return
@@ -276,7 +277,7 @@ async function addVideo (req: express.Request, res: express.Response) {
276 return sequelizeTypescript.transaction(t => federateVideoIfNeeded(refreshedVideo, true, t)) 277 return sequelizeTypescript.transaction(t => federateVideoIfNeeded(refreshedVideo, true, t))
277 }) 278 })
278 }) 279 })
279 .catch(err => logger.error('Cannot federate or notify video creation %s', video.url, { err })) 280 .catch(err => logger.error('Cannot federate or notify video creation %s', video.url, { err, ...lTags(video.uuid) }))
280 281
281 if (video.state === VideoState.TO_TRANSCODE) { 282 if (video.state === VideoState.TO_TRANSCODE) {
282 await addOptimizeOrMergeAudioJob(videoCreated, videoFile, res.locals.oauth.token.User) 283 await addOptimizeOrMergeAudioJob(videoCreated, videoFile, res.locals.oauth.token.User)
@@ -389,7 +390,7 @@ async function updateVideo (req: express.Request, res: express.Response) {
389 new VideoAuditView(videoInstanceUpdated.toFormattedDetailsJSON()), 390 new VideoAuditView(videoInstanceUpdated.toFormattedDetailsJSON()),
390 oldVideoAuditView 391 oldVideoAuditView
391 ) 392 )
392 logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid) 393 logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid, lTags(videoInstance.uuid))
393 394
394 return videoInstanceUpdated 395 return videoInstanceUpdated
395 }) 396 })
diff --git a/server/controllers/api/videos/ownership.ts b/server/controllers/api/videos/ownership.ts
index 86adb6c69..a85d7c30b 100644
--- a/server/controllers/api/videos/ownership.ts
+++ b/server/controllers/api/videos/ownership.ts
@@ -107,7 +107,7 @@ async function acceptOwnership (req: express.Request, res: express.Response) {
107 // We need more attributes for federation 107 // We need more attributes for federation
108 const targetVideo = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoChangeOwnership.Video.id) 108 const targetVideo = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoChangeOwnership.Video.id)
109 109
110 const oldVideoChannel = await VideoChannelModel.loadByIdAndPopulateAccount(targetVideo.channelId) 110 const oldVideoChannel = await VideoChannelModel.loadAndPopulateAccount(targetVideo.channelId)
111 111
112 targetVideo.channelId = channel.id 112 targetVideo.channelId = channel.id
113 113
diff --git a/server/controllers/client.ts b/server/controllers/client.ts
index 557cbfdfb..022a17ff4 100644
--- a/server/controllers/client.ts
+++ b/server/controllers/client.ts
@@ -2,7 +2,9 @@ import * as express from 'express'
2import { constants, promises as fs } from 'fs' 2import { constants, promises as fs } from 'fs'
3import { readFile } from 'fs-extra' 3import { readFile } from 'fs-extra'
4import { join } from 'path' 4import { join } from 'path'
5import { logger } from '@server/helpers/logger'
5import { CONFIG } from '@server/initializers/config' 6import { CONFIG } from '@server/initializers/config'
7import { Hooks } from '@server/lib/plugins/hooks'
6import { HttpStatusCode } from '@shared/core-utils' 8import { HttpStatusCode } from '@shared/core-utils'
7import { buildFileLocale, getCompleteLocale, is18nLocale, LOCALE_FILES } from '@shared/core-utils/i18n' 9import { buildFileLocale, getCompleteLocale, is18nLocale, LOCALE_FILES } from '@shared/core-utils/i18n'
8import { root } from '../helpers/core-utils' 10import { root } from '../helpers/core-utils'
@@ -27,6 +29,7 @@ const embedMiddlewares = [
27 ? embedCSP 29 ? embedCSP
28 : (req: express.Request, res: express.Response, next: express.NextFunction) => next(), 30 : (req: express.Request, res: express.Response, next: express.NextFunction) => next(),
29 31
32 // Set headers
30 (req: express.Request, res: express.Response, next: express.NextFunction) => { 33 (req: express.Request, res: express.Response, next: express.NextFunction) => {
31 res.removeHeader('X-Frame-Options') 34 res.removeHeader('X-Frame-Options')
32 35
@@ -105,6 +108,24 @@ function serveServerTranslations (req: express.Request, res: express.Response) {
105} 108}
106 109
107async function generateEmbedHtmlPage (req: express.Request, res: express.Response) { 110async function generateEmbedHtmlPage (req: express.Request, res: express.Response) {
111 const hookName = req.originalUrl.startsWith('/video-playlists/')
112 ? 'filter:html.embed.video-playlist.allowed.result'
113 : 'filter:html.embed.video.allowed.result'
114
115 const allowParameters = { req }
116
117 const allowedResult = await Hooks.wrapFun(
118 isEmbedAllowed,
119 allowParameters,
120 hookName
121 )
122
123 if (!allowedResult || allowedResult.allowed !== true) {
124 logger.info('Embed is not allowed.', { allowedResult })
125
126 return sendHTML(allowedResult?.html || '', res)
127 }
128
108 const html = await ClientHtml.getEmbedHTML() 129 const html = await ClientHtml.getEmbedHTML()
109 130
110 return sendHTML(html, res) 131 return sendHTML(html, res)
@@ -158,3 +179,10 @@ function serveClientOverride (path: string) {
158 } 179 }
159 } 180 }
160} 181}
182
183type AllowedResult = { allowed: boolean, html?: string }
184function isEmbedAllowed (_object: {
185 req: express.Request
186}): AllowedResult {
187 return { allowed: true }
188}
diff --git a/server/controllers/download.ts b/server/controllers/download.ts
index 27caa1518..9a8194c5c 100644
--- a/server/controllers/download.ts
+++ b/server/controllers/download.ts
@@ -1,8 +1,10 @@
1import * as cors from 'cors' 1import * as cors from 'cors'
2import * as express from 'express' 2import * as express from 'express'
3import { logger } from '@server/helpers/logger'
3import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache' 4import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache'
5import { Hooks } from '@server/lib/plugins/hooks'
4import { getVideoFilePath } from '@server/lib/video-paths' 6import { getVideoFilePath } from '@server/lib/video-paths'
5import { MVideoFile, MVideoFullLight } from '@server/types/models' 7import { MStreamingPlaylist, MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
6import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' 8import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
7import { VideoStreamingPlaylistType } from '@shared/models' 9import { VideoStreamingPlaylistType } from '@shared/models'
8import { STATIC_DOWNLOAD_PATHS } from '../initializers/constants' 10import { STATIC_DOWNLOAD_PATHS } from '../initializers/constants'
@@ -14,19 +16,19 @@ downloadRouter.use(cors())
14 16
15downloadRouter.use( 17downloadRouter.use(
16 STATIC_DOWNLOAD_PATHS.TORRENTS + ':filename', 18 STATIC_DOWNLOAD_PATHS.TORRENTS + ':filename',
17 downloadTorrent 19 asyncMiddleware(downloadTorrent)
18) 20)
19 21
20downloadRouter.use( 22downloadRouter.use(
21 STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension', 23 STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension',
22 asyncMiddleware(videosDownloadValidator), 24 asyncMiddleware(videosDownloadValidator),
23 downloadVideoFile 25 asyncMiddleware(downloadVideoFile)
24) 26)
25 27
26downloadRouter.use( 28downloadRouter.use(
27 STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + ':id-:resolution([0-9]+)-fragmented.:extension', 29 STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + ':id-:resolution([0-9]+)-fragmented.:extension',
28 asyncMiddleware(videosDownloadValidator), 30 asyncMiddleware(videosDownloadValidator),
29 downloadHLSVideoFile 31 asyncMiddleware(downloadHLSVideoFile)
30) 32)
31 33
32// --------------------------------------------------------------------------- 34// ---------------------------------------------------------------------------
@@ -41,28 +43,58 @@ async function downloadTorrent (req: express.Request, res: express.Response) {
41 const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename) 43 const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename)
42 if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 44 if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
43 45
46 const allowParameters = { torrentPath: result.path, downloadName: result.downloadName }
47
48 const allowedResult = await Hooks.wrapFun(
49 isTorrentDownloadAllowed,
50 allowParameters,
51 'filter:api.download.torrent.allowed.result'
52 )
53
54 if (!checkAllowResult(res, allowParameters, allowedResult)) return
55
44 return res.download(result.path, result.downloadName) 56 return res.download(result.path, result.downloadName)
45} 57}
46 58
47function downloadVideoFile (req: express.Request, res: express.Response) { 59async function downloadVideoFile (req: express.Request, res: express.Response) {
48 const video = res.locals.videoAll 60 const video = res.locals.videoAll
49 61
50 const videoFile = getVideoFile(req, video.VideoFiles) 62 const videoFile = getVideoFile(req, video.VideoFiles)
51 if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end() 63 if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end()
52 64
65 const allowParameters = { video, videoFile }
66
67 const allowedResult = await Hooks.wrapFun(
68 isVideoDownloadAllowed,
69 allowParameters,
70 'filter:api.download.video.allowed.result'
71 )
72
73 if (!checkAllowResult(res, allowParameters, allowedResult)) return
74
53 return res.download(getVideoFilePath(video, videoFile), `${video.name}-${videoFile.resolution}p${videoFile.extname}`) 75 return res.download(getVideoFilePath(video, videoFile), `${video.name}-${videoFile.resolution}p${videoFile.extname}`)
54} 76}
55 77
56function downloadHLSVideoFile (req: express.Request, res: express.Response) { 78async function downloadHLSVideoFile (req: express.Request, res: express.Response) {
57 const video = res.locals.videoAll 79 const video = res.locals.videoAll
58 const playlist = getHLSPlaylist(video) 80 const streamingPlaylist = getHLSPlaylist(video)
59 if (!playlist) return res.status(HttpStatusCode.NOT_FOUND_404).end 81 if (!streamingPlaylist) return res.status(HttpStatusCode.NOT_FOUND_404).end
60 82
61 const videoFile = getVideoFile(req, playlist.VideoFiles) 83 const videoFile = getVideoFile(req, streamingPlaylist.VideoFiles)
62 if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end() 84 if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end()
63 85
64 const filename = `${video.name}-${videoFile.resolution}p-${playlist.getStringType()}${videoFile.extname}` 86 const allowParameters = { video, streamingPlaylist, videoFile }
65 return res.download(getVideoFilePath(playlist, videoFile), filename) 87
88 const allowedResult = await Hooks.wrapFun(
89 isVideoDownloadAllowed,
90 allowParameters,
91 'filter:api.download.video.allowed.result'
92 )
93
94 if (!checkAllowResult(res, allowParameters, allowedResult)) return
95
96 const filename = `${video.name}-${videoFile.resolution}p-${streamingPlaylist.getStringType()}${videoFile.extname}`
97 return res.download(getVideoFilePath(streamingPlaylist, videoFile), filename)
66} 98}
67 99
68function getVideoFile (req: express.Request, files: MVideoFile[]) { 100function getVideoFile (req: express.Request, files: MVideoFile[]) {
@@ -76,3 +108,34 @@ function getHLSPlaylist (video: MVideoFullLight) {
76 108
77 return Object.assign(playlist, { Video: video }) 109 return Object.assign(playlist, { Video: video })
78} 110}
111
112type AllowedResult = {
113 allowed: boolean
114 errorMessage?: string
115}
116
117function isTorrentDownloadAllowed (_object: {
118 torrentPath: string
119}): AllowedResult {
120 return { allowed: true }
121}
122
123function isVideoDownloadAllowed (_object: {
124 video: MVideo
125 videoFile: MVideoFile
126 streamingPlaylist?: MStreamingPlaylist
127}): AllowedResult {
128 return { allowed: true }
129}
130
131function checkAllowResult (res: express.Response, allowParameters: any, result?: AllowedResult) {
132 if (!result || result.allowed !== true) {
133 logger.info('Download is not allowed.', { result, allowParameters })
134 res.status(HttpStatusCode.FORBIDDEN_403)
135 .json({ error: result?.errorMessage || 'Refused download' })
136
137 return false
138 }
139
140 return true
141}
diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts
index e29a8fe1d..921067e65 100644
--- a/server/controllers/feeds.ts
+++ b/server/controllers/feeds.ts
@@ -1,8 +1,9 @@
1import * as express from 'express' 1import * as express from 'express'
2import * as Feed from 'pfeed' 2import * as Feed from 'pfeed'
3import { VideoFilter } from '../../shared/models/videos/video-query.type'
3import { buildNSFWFilter } from '../helpers/express-utils' 4import { buildNSFWFilter } from '../helpers/express-utils'
4import { CONFIG } from '../initializers/config' 5import { CONFIG } from '../initializers/config'
5import { FEEDS, ROUTE_CACHE_LIFETIME, THUMBNAILS_SIZE, WEBSERVER } from '../initializers/constants' 6import { FEEDS, PREVIEWS_SIZE, ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants'
6import { 7import {
7 asyncMiddleware, 8 asyncMiddleware,
8 commonVideosFiltersValidator, 9 commonVideosFiltersValidator,
@@ -17,7 +18,6 @@ import {
17import { cacheRoute } from '../middlewares/cache' 18import { cacheRoute } from '../middlewares/cache'
18import { VideoModel } from '../models/video/video' 19import { VideoModel } from '../models/video/video'
19import { VideoCommentModel } from '../models/video/video-comment' 20import { VideoCommentModel } from '../models/video/video-comment'
20import { VideoFilter } from '../../shared/models/videos/video-query.type'
21 21
22const feedsRouter = express.Router() 22const feedsRouter = express.Router()
23 23
@@ -318,9 +318,9 @@ function addVideosToFeed (feed, videos: VideoModel[]) {
318 }, 318 },
319 thumbnail: [ 319 thumbnail: [
320 { 320 {
321 url: WEBSERVER.URL + video.getMiniatureStaticPath(), 321 url: WEBSERVER.URL + video.getPreviewStaticPath(),
322 height: THUMBNAILS_SIZE.height, 322 height: PREVIEWS_SIZE.height,
323 width: THUMBNAILS_SIZE.width 323 width: PREVIEWS_SIZE.width
324 } 324 }
325 ] 325 ]
326 }) 326 })
diff --git a/server/controllers/lazy-static.ts b/server/controllers/lazy-static.ts
index 4e553479b..6f71fdb16 100644
--- a/server/controllers/lazy-static.ts
+++ b/server/controllers/lazy-static.ts
@@ -4,10 +4,10 @@ import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache
4import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' 4import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
5import { logger } from '../helpers/logger' 5import { logger } from '../helpers/logger'
6import { LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/constants' 6import { LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/constants'
7import { avatarPathUnsafeCache, pushAvatarProcessInQueue } from '../lib/avatar' 7import { actorImagePathUnsafeCache, pushActorImageProcessInQueue } from '../lib/actor-image'
8import { VideosCaptionCache, VideosPreviewCache } from '../lib/files-cache' 8import { VideosCaptionCache, VideosPreviewCache } from '../lib/files-cache'
9import { asyncMiddleware } from '../middlewares' 9import { asyncMiddleware } from '../middlewares'
10import { AvatarModel } from '../models/avatar/avatar' 10import { ActorImageModel } from '../models/account/actor-image'
11 11
12const lazyStaticRouter = express.Router() 12const lazyStaticRouter = express.Router()
13 13
@@ -15,7 +15,12 @@ lazyStaticRouter.use(cors())
15 15
16lazyStaticRouter.use( 16lazyStaticRouter.use(
17 LAZY_STATIC_PATHS.AVATARS + ':filename', 17 LAZY_STATIC_PATHS.AVATARS + ':filename',
18 asyncMiddleware(getAvatar) 18 asyncMiddleware(getActorImage)
19)
20
21lazyStaticRouter.use(
22 LAZY_STATIC_PATHS.BANNERS + ':filename',
23 asyncMiddleware(getActorImage)
19) 24)
20 25
21lazyStaticRouter.use( 26lazyStaticRouter.use(
@@ -43,36 +48,36 @@ export {
43 48
44// --------------------------------------------------------------------------- 49// ---------------------------------------------------------------------------
45 50
46async function getAvatar (req: express.Request, res: express.Response) { 51async function getActorImage (req: express.Request, res: express.Response) {
47 const filename = req.params.filename 52 const filename = req.params.filename
48 53
49 if (avatarPathUnsafeCache.has(filename)) { 54 if (actorImagePathUnsafeCache.has(filename)) {
50 return res.sendFile(avatarPathUnsafeCache.get(filename), { maxAge: STATIC_MAX_AGE.SERVER }) 55 return res.sendFile(actorImagePathUnsafeCache.get(filename), { maxAge: STATIC_MAX_AGE.SERVER })
51 } 56 }
52 57
53 const avatar = await AvatarModel.loadByName(filename) 58 const image = await ActorImageModel.loadByName(filename)
54 if (!avatar) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 59 if (!image) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
55 60
56 if (avatar.onDisk === false) { 61 if (image.onDisk === false) {
57 if (!avatar.fileUrl) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 62 if (!image.fileUrl) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
58 63
59 logger.info('Lazy serve remote avatar image %s.', avatar.fileUrl) 64 logger.info('Lazy serve remote actor image %s.', image.fileUrl)
60 65
61 try { 66 try {
62 await pushAvatarProcessInQueue({ filename: avatar.filename, fileUrl: avatar.fileUrl }) 67 await pushActorImageProcessInQueue({ filename: image.filename, fileUrl: image.fileUrl, type: image.type })
63 } catch (err) { 68 } catch (err) {
64 logger.warn('Cannot process remote avatar %s.', avatar.fileUrl, { err }) 69 logger.warn('Cannot process remote actor image %s.', image.fileUrl, { err })
65 return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 70 return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
66 } 71 }
67 72
68 avatar.onDisk = true 73 image.onDisk = true
69 avatar.save() 74 image.save()
70 .catch(err => logger.error('Cannot save new avatar disk state.', { err })) 75 .catch(err => logger.error('Cannot save new actor image disk state.', { err }))
71 } 76 }
72 77
73 const path = avatar.getPath() 78 const path = image.getPath()
74 79
75 avatarPathUnsafeCache.set(filename, path) 80 actorImagePathUnsafeCache.set(filename, path)
76 return res.sendFile(path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }) 81 return res.sendFile(path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER })
77} 82}
78 83
diff --git a/server/controllers/plugins.ts b/server/controllers/plugins.ts
index 6a1ccc0bf..105f51518 100644
--- a/server/controllers/plugins.ts
+++ b/server/controllers/plugins.ts
@@ -1,15 +1,15 @@
1import * as express from 'express' 1import * as express from 'express'
2import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants'
3import { join } from 'path' 2import { join } from 'path'
4import { PluginManager, RegisteredPlugin } from '../lib/plugins/plugin-manager' 3import { logger } from '@server/helpers/logger'
5import { getPluginValidator, pluginStaticDirectoryValidator, getExternalAuthValidator } from '../middlewares/validators/plugins' 4import { optionalAuthenticate } from '@server/middlewares/auth'
6import { serveThemeCSSValidator } from '../middlewares/validators/themes'
7import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
8import { getCompleteLocale, is18nLocale } from '../../shared/core-utils/i18n' 5import { getCompleteLocale, is18nLocale } from '../../shared/core-utils/i18n'
6import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
9import { PluginType } from '../../shared/models/plugins/plugin.type' 7import { PluginType } from '../../shared/models/plugins/plugin.type'
10import { isTestInstance } from '../helpers/core-utils' 8import { isTestInstance } from '../helpers/core-utils'
11import { logger } from '@server/helpers/logger' 9import { PLUGIN_GLOBAL_CSS_PATH } from '../initializers/constants'
12import { optionalAuthenticate } from '@server/middlewares/oauth' 10import { PluginManager, RegisteredPlugin } from '../lib/plugins/plugin-manager'
11import { getExternalAuthValidator, getPluginValidator, pluginStaticDirectoryValidator } from '../middlewares/validators/plugins'
12import { serveThemeCSSValidator } from '../middlewares/validators/themes'
13 13
14const sendFileOptions = { 14const sendFileOptions = {
15 maxAge: '30 days', 15 maxAge: '30 days',
diff --git a/server/controllers/services.ts b/server/controllers/services.ts
index d0217c30a..189e1651b 100644
--- a/server/controllers/services.ts
+++ b/server/controllers/services.ts
@@ -3,6 +3,7 @@ import { EMBED_SIZE, PREVIEWS_SIZE, WEBSERVER, THUMBNAILS_SIZE } from '../initia
3import { asyncMiddleware, oembedValidator } from '../middlewares' 3import { asyncMiddleware, oembedValidator } from '../middlewares'
4import { accountNameWithHostGetValidator } from '../middlewares/validators' 4import { accountNameWithHostGetValidator } from '../middlewares/validators'
5import { MChannelSummary } from '@server/types/models' 5import { MChannelSummary } from '@server/types/models'
6import { escapeHTML } from '@shared/core-utils/renderer'
6 7
7const servicesRouter = express.Router() 8const servicesRouter = express.Router()
8 9
@@ -79,6 +80,7 @@ function buildOEmbed (options: {
79 const embedUrl = webserverUrl + embedPath 80 const embedUrl = webserverUrl + embedPath
80 let embedWidth = EMBED_SIZE.width 81 let embedWidth = EMBED_SIZE.width
81 let embedHeight = EMBED_SIZE.height 82 let embedHeight = EMBED_SIZE.height
83 const embedTitle = escapeHTML(title)
82 84
83 let thumbnailUrl = previewPath 85 let thumbnailUrl = previewPath
84 ? webserverUrl + previewPath 86 ? webserverUrl + previewPath
@@ -96,7 +98,7 @@ function buildOEmbed (options: {
96 } 98 }
97 99
98 const html = `<iframe width="${embedWidth}" height="${embedHeight}" sandbox="allow-same-origin allow-scripts" ` + 100 const html = `<iframe width="${embedWidth}" height="${embedHeight}" sandbox="allow-same-origin allow-scripts" ` +
99 `src="${embedUrl}" frameborder="0" allowfullscreen></iframe>` 101 `title="${embedTitle}" src="${embedUrl}" frameborder="0" allowfullscreen></iframe>`
100 102
101 const json: any = { 103 const json: any = {
102 type: 'video', 104 type: 'video',
diff --git a/server/controllers/static.ts b/server/controllers/static.ts
index 4baa31117..e6a0628e6 100644
--- a/server/controllers/static.ts
+++ b/server/controllers/static.ts
@@ -252,9 +252,9 @@ async function generateNodeinfo (req: express.Request, res: express.Response) {
252 avatar: { 252 avatar: {
253 file: { 253 file: {
254 size: { 254 size: {
255 max: CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max 255 max: CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max
256 }, 256 },
257 extensions: CONSTRAINTS_FIELDS.ACTORS.AVATAR.EXTNAME 257 extensions: CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME
258 } 258 }
259 }, 259 },
260 video: { 260 video: {
diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts
index 08aef2908..e0754b501 100644
--- a/server/helpers/activitypub.ts
+++ b/server/helpers/activitypub.ts
@@ -3,7 +3,6 @@ import { URL } from 'url'
3import validator from 'validator' 3import validator from 'validator'
4import { ContextType } from '@shared/models/activitypub/context' 4import { ContextType } from '@shared/models/activitypub/context'
5import { ResultList } from '../../shared/models' 5import { ResultList } from '../../shared/models'
6import { Activity } from '../../shared/models/activitypub'
7import { ACTIVITY_PUB, REMOTE_SCHEME } from '../initializers/constants' 6import { ACTIVITY_PUB, REMOTE_SCHEME } from '../initializers/constants'
8import { MActor, MVideoWithHost } from '../types/models' 7import { MActor, MVideoWithHost } from '../types/models'
9import { pageToStartAndCount } from './core-utils' 8import { pageToStartAndCount } from './core-utils'
@@ -182,10 +181,10 @@ async function activityPubCollectionPagination (
182 181
183} 182}
184 183
185function buildSignedActivity (byActor: MActor, data: Object, contextType?: ContextType) { 184function buildSignedActivity <T> (byActor: MActor, data: T, contextType?: ContextType) {
186 const activity = activityPubContextify(data, contextType) 185 const activity = activityPubContextify(data, contextType)
187 186
188 return signJsonLDObject(byActor, activity) as Promise<Activity> 187 return signJsonLDObject(byActor, activity)
189} 188}
190 189
191function getAPId (activity: string | { id: string }) { 190function getAPId (activity: string | { id: string }) {
diff --git a/server/helpers/core-utils.ts b/server/helpers/core-utils.ts
index 935fd22d9..b93868c12 100644
--- a/server/helpers/core-utils.ts
+++ b/server/helpers/core-utils.ts
@@ -10,7 +10,9 @@ import { BinaryToTextEncoding, createHash, randomBytes } from 'crypto'
10import { truncate } from 'lodash' 10import { truncate } from 'lodash'
11import { basename, isAbsolute, join, resolve } from 'path' 11import { basename, isAbsolute, join, resolve } from 'path'
12import * as pem from 'pem' 12import * as pem from 'pem'
13import { pipeline } from 'stream'
13import { URL } from 'url' 14import { URL } from 'url'
15import { promisify } from 'util'
14 16
15const objectConverter = (oldObject: any, keyConverter: (e: string) => string, valueConverter: (e: any) => any) => { 17const objectConverter = (oldObject: any, keyConverter: (e: string) => string, valueConverter: (e: any) => any) => {
16 if (!oldObject || typeof oldObject !== 'object') { 18 if (!oldObject || typeof oldObject !== 'object') {
@@ -152,24 +154,6 @@ function root () {
152 return rootPath 154 return rootPath
153} 155}
154 156
155// Thanks: https://stackoverflow.com/a/12034334
156function escapeHTML (stringParam) {
157 if (!stringParam) return ''
158
159 const entityMap = {
160 '&': '&amp;',
161 '<': '&lt;',
162 '>': '&gt;',
163 '"': '&quot;',
164 '\'': '&#39;',
165 '/': '&#x2F;',
166 '`': '&#x60;',
167 '=': '&#x3D;'
168 }
169
170 return String(stringParam).replace(/[&<>"'`=/]/g, s => entityMap[s])
171}
172
173function pageToStartAndCount (page: number, itemsPerPage: number) { 157function pageToStartAndCount (page: number, itemsPerPage: number) {
174 const start = (page - 1) * itemsPerPage 158 const start = (page - 1) * itemsPerPage
175 159
@@ -249,11 +233,23 @@ function promisify2<T, U, A> (func: (arg1: T, arg2: U, cb: (err: any, result: A)
249 } 233 }
250} 234}
251 235
236type SemVersion = { major: number, minor: number, patch: number }
237function parseSemVersion (s: string) {
238 const parsed = s.match(/^v?(\d+)\.(\d+)\.(\d+)$/i)
239
240 return {
241 major: parseInt(parsed[1]),
242 minor: parseInt(parsed[2]),
243 patch: parseInt(parsed[3])
244 } as SemVersion
245}
246
252const randomBytesPromise = promisify1<number, Buffer>(randomBytes) 247const randomBytesPromise = promisify1<number, Buffer>(randomBytes)
253const createPrivateKey = promisify1<number, { key: string }>(pem.createPrivateKey) 248const createPrivateKey = promisify1<number, { key: string }>(pem.createPrivateKey)
254const getPublicKey = promisify1<string, { publicKey: string }>(pem.getPublicKey) 249const getPublicKey = promisify1<string, { publicKey: string }>(pem.getPublicKey)
255const execPromise2 = promisify2<string, any, string>(exec) 250const execPromise2 = promisify2<string, any, string>(exec)
256const execPromise = promisify1<string, string>(exec) 251const execPromise = promisify1<string, string>(exec)
252const pipelinePromise = promisify(pipeline)
257 253
258// --------------------------------------------------------------------------- 254// ---------------------------------------------------------------------------
259 255
@@ -264,7 +260,6 @@ export {
264 260
265 objectConverter, 261 objectConverter,
266 root, 262 root,
267 escapeHTML,
268 pageToStartAndCount, 263 pageToStartAndCount,
269 sanitizeUrl, 264 sanitizeUrl,
270 sanitizeHost, 265 sanitizeHost,
@@ -284,5 +279,8 @@ export {
284 createPrivateKey, 279 createPrivateKey,
285 getPublicKey, 280 getPublicKey,
286 execPromise2, 281 execPromise2,
287 execPromise 282 execPromise,
283 pipelinePromise,
284
285 parseSemVersion
288} 286}
diff --git a/server/helpers/custom-validators/activitypub/activity.ts b/server/helpers/custom-validators/activitypub/activity.ts
index da79b2782..b5c96f6e7 100644
--- a/server/helpers/custom-validators/activitypub/activity.ts
+++ b/server/helpers/custom-validators/activitypub/activity.ts
@@ -1,16 +1,13 @@
1import validator from 'validator' 1import validator from 'validator'
2import { Activity, ActivityType } from '../../../../shared/models/activitypub' 2import { Activity, ActivityType } from '../../../../shared/models/activitypub'
3import { isAbuseReasonValid } from '../abuses'
3import { exists } from '../misc' 4import { exists } from '../misc'
4import { sanitizeAndCheckActorObject } from './actor' 5import { sanitizeAndCheckActorObject } from './actor'
5import { isCacheFileObjectValid } from './cache-file' 6import { isCacheFileObjectValid } from './cache-file'
6import { isFlagActivityValid } from './flag'
7import { isActivityPubUrlValid, isBaseActivityValid, isObjectValid } from './misc' 7import { isActivityPubUrlValid, isBaseActivityValid, isObjectValid } from './misc'
8import { isPlaylistObjectValid } from './playlist' 8import { isPlaylistObjectValid } from './playlist'
9import { isDislikeActivityValid, isLikeActivityValid } from './rate'
10import { isShareActivityValid } from './share'
11import { sanitizeAndCheckVideoCommentObject } from './video-comments' 9import { sanitizeAndCheckVideoCommentObject } from './video-comments'
12import { sanitizeAndCheckVideoTorrentObject } from './videos' 10import { sanitizeAndCheckVideoTorrentObject } from './videos'
13import { isViewActivityValid } from './view'
14 11
15function isRootActivityValid (activity: any) { 12function isRootActivityValid (activity: any) {
16 return isCollection(activity) || isActivity(activity) 13 return isCollection(activity) || isActivity(activity)
@@ -29,18 +26,18 @@ function isActivity (activity: any) {
29} 26}
30 27
31const activityCheckers: { [ P in ActivityType ]: (activity: Activity) => boolean } = { 28const activityCheckers: { [ P in ActivityType ]: (activity: Activity) => boolean } = {
32 Create: checkCreateActivity, 29 Create: isCreateActivityValid,
33 Update: checkUpdateActivity, 30 Update: isUpdateActivityValid,
34 Delete: checkDeleteActivity, 31 Delete: isDeleteActivityValid,
35 Follow: checkFollowActivity, 32 Follow: isFollowActivityValid,
36 Accept: checkAcceptActivity, 33 Accept: isAcceptActivityValid,
37 Reject: checkRejectActivity, 34 Reject: isRejectActivityValid,
38 Announce: checkAnnounceActivity, 35 Announce: isAnnounceActivityValid,
39 Undo: checkUndoActivity, 36 Undo: isUndoActivityValid,
40 Like: checkLikeActivity, 37 Like: isLikeActivityValid,
41 View: checkViewActivity, 38 View: isViewActivityValid,
42 Flag: checkFlagActivity, 39 Flag: isFlagActivityValid,
43 Dislike: checkDislikeActivity 40 Dislike: isDislikeActivityValid
44} 41}
45 42
46function isActivityValid (activity: any) { 43function isActivityValid (activity: any) {
@@ -51,34 +48,34 @@ function isActivityValid (activity: any) {
51 return checker(activity) 48 return checker(activity)
52} 49}
53 50
54// --------------------------------------------------------------------------- 51function isFlagActivityValid (activity: any) {
55 52 return isBaseActivityValid(activity, 'Flag') &&
56export { 53 isAbuseReasonValid(activity.content) &&
57 isRootActivityValid, 54 isActivityPubUrlValid(activity.object)
58 isActivityValid
59} 55}
60 56
61// --------------------------------------------------------------------------- 57function isLikeActivityValid (activity: any) {
62 58 return isBaseActivityValid(activity, 'Like') &&
63function checkViewActivity (activity: any) { 59 isObjectValid(activity.object)
64 return isBaseActivityValid(activity, 'View') &&
65 isViewActivityValid(activity)
66} 60}
67 61
68function checkFlagActivity (activity: any) { 62function isDislikeActivityValid (activity: any) {
69 return isBaseActivityValid(activity, 'Flag') && 63 return isBaseActivityValid(activity, 'Dislike') &&
70 isFlagActivityValid(activity) 64 isObjectValid(activity.object)
71} 65}
72 66
73function checkDislikeActivity (activity: any) { 67function isAnnounceActivityValid (activity: any) {
74 return isDislikeActivityValid(activity) 68 return isBaseActivityValid(activity, 'Announce') &&
69 isObjectValid(activity.object)
75} 70}
76 71
77function checkLikeActivity (activity: any) { 72function isViewActivityValid (activity: any) {
78 return isLikeActivityValid(activity) 73 return isBaseActivityValid(activity, 'View') &&
74 isActivityPubUrlValid(activity.actor) &&
75 isActivityPubUrlValid(activity.object)
79} 76}
80 77
81function checkCreateActivity (activity: any) { 78function isCreateActivityValid (activity: any) {
82 return isBaseActivityValid(activity, 'Create') && 79 return isBaseActivityValid(activity, 'Create') &&
83 ( 80 (
84 isViewActivityValid(activity.object) || 81 isViewActivityValid(activity.object) ||
@@ -92,7 +89,7 @@ function checkCreateActivity (activity: any) {
92 ) 89 )
93} 90}
94 91
95function checkUpdateActivity (activity: any) { 92function isUpdateActivityValid (activity: any) {
96 return isBaseActivityValid(activity, 'Update') && 93 return isBaseActivityValid(activity, 'Update') &&
97 ( 94 (
98 isCacheFileObjectValid(activity.object) || 95 isCacheFileObjectValid(activity.object) ||
@@ -102,36 +99,51 @@ function checkUpdateActivity (activity: any) {
102 ) 99 )
103} 100}
104 101
105function checkDeleteActivity (activity: any) { 102function isDeleteActivityValid (activity: any) {
106 // We don't really check objects 103 // We don't really check objects
107 return isBaseActivityValid(activity, 'Delete') && 104 return isBaseActivityValid(activity, 'Delete') &&
108 isObjectValid(activity.object) 105 isObjectValid(activity.object)
109} 106}
110 107
111function checkFollowActivity (activity: any) { 108function isFollowActivityValid (activity: any) {
112 return isBaseActivityValid(activity, 'Follow') && 109 return isBaseActivityValid(activity, 'Follow') &&
113 isObjectValid(activity.object) 110 isObjectValid(activity.object)
114} 111}
115 112
116function checkAcceptActivity (activity: any) { 113function isAcceptActivityValid (activity: any) {
117 return isBaseActivityValid(activity, 'Accept') 114 return isBaseActivityValid(activity, 'Accept')
118} 115}
119 116
120function checkRejectActivity (activity: any) { 117function isRejectActivityValid (activity: any) {
121 return isBaseActivityValid(activity, 'Reject') 118 return isBaseActivityValid(activity, 'Reject')
122} 119}
123 120
124function checkAnnounceActivity (activity: any) { 121function isUndoActivityValid (activity: any) {
125 return isShareActivityValid(activity)
126}
127
128function checkUndoActivity (activity: any) {
129 return isBaseActivityValid(activity, 'Undo') && 122 return isBaseActivityValid(activity, 'Undo') &&
130 ( 123 (
131 checkFollowActivity(activity.object) || 124 isFollowActivityValid(activity.object) ||
132 checkLikeActivity(activity.object) || 125 isLikeActivityValid(activity.object) ||
133 checkDislikeActivity(activity.object) || 126 isDislikeActivityValid(activity.object) ||
134 checkAnnounceActivity(activity.object) || 127 isAnnounceActivityValid(activity.object) ||
135 checkCreateActivity(activity.object) 128 isCreateActivityValid(activity.object)
136 ) 129 )
137} 130}
131
132// ---------------------------------------------------------------------------
133
134export {
135 isRootActivityValid,
136 isActivityValid,
137 isFlagActivityValid,
138 isLikeActivityValid,
139 isDislikeActivityValid,
140 isAnnounceActivityValid,
141 isViewActivityValid,
142 isCreateActivityValid,
143 isUpdateActivityValid,
144 isDeleteActivityValid,
145 isFollowActivityValid,
146 isAcceptActivityValid,
147 isRejectActivityValid,
148 isUndoActivityValid
149}
diff --git a/server/helpers/custom-validators/activitypub/flag.ts b/server/helpers/custom-validators/activitypub/flag.ts
deleted file mode 100644
index dc90b3667..000000000
--- a/server/helpers/custom-validators/activitypub/flag.ts
+++ /dev/null
@@ -1,14 +0,0 @@
1import { isActivityPubUrlValid } from './misc'
2import { isAbuseReasonValid } from '../abuses'
3
4function isFlagActivityValid (activity: any) {
5 return activity.type === 'Flag' &&
6 isAbuseReasonValid(activity.content) &&
7 isActivityPubUrlValid(activity.object)
8}
9
10// ---------------------------------------------------------------------------
11
12export {
13 isFlagActivityValid
14}
diff --git a/server/helpers/custom-validators/activitypub/rate.ts b/server/helpers/custom-validators/activitypub/rate.ts
deleted file mode 100644
index aafdda443..000000000
--- a/server/helpers/custom-validators/activitypub/rate.ts
+++ /dev/null
@@ -1,18 +0,0 @@
1import { isBaseActivityValid, isObjectValid } from './misc'
2
3function isLikeActivityValid (activity: any) {
4 return isBaseActivityValid(activity, 'Like') &&
5 isObjectValid(activity.object)
6}
7
8function isDislikeActivityValid (activity: any) {
9 return isBaseActivityValid(activity, 'Dislike') &&
10 isObjectValid(activity.object)
11}
12
13// ---------------------------------------------------------------------------
14
15export {
16 isDislikeActivityValid,
17 isLikeActivityValid
18}
diff --git a/server/helpers/custom-validators/activitypub/share.ts b/server/helpers/custom-validators/activitypub/share.ts
deleted file mode 100644
index fb5e4c05e..000000000
--- a/server/helpers/custom-validators/activitypub/share.ts
+++ /dev/null
@@ -1,11 +0,0 @@
1import { isBaseActivityValid, isObjectValid } from './misc'
2
3function isShareActivityValid (activity: any) {
4 return isBaseActivityValid(activity, 'Announce') &&
5 isObjectValid(activity.object)
6}
7// ---------------------------------------------------------------------------
8
9export {
10 isShareActivityValid
11}
diff --git a/server/helpers/custom-validators/activitypub/view.ts b/server/helpers/custom-validators/activitypub/view.ts
deleted file mode 100644
index 41d16469f..000000000
--- a/server/helpers/custom-validators/activitypub/view.ts
+++ /dev/null
@@ -1,13 +0,0 @@
1import { isActivityPubUrlValid } from './misc'
2
3function isViewActivityValid (activity: any) {
4 return activity.type === 'View' &&
5 isActivityPubUrlValid(activity.actor) &&
6 isActivityPubUrlValid(activity.object)
7}
8
9// ---------------------------------------------------------------------------
10
11export {
12 isViewActivityValid
13}
diff --git a/server/helpers/custom-validators/actor-images.ts b/server/helpers/custom-validators/actor-images.ts
new file mode 100644
index 000000000..4fb0b7c70
--- /dev/null
+++ b/server/helpers/custom-validators/actor-images.ts
@@ -0,0 +1,17 @@
1
2import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
3import { isFileValid } from './misc'
4
5const imageMimeTypes = CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME
6 .map(v => v.replace('.', ''))
7 .join('|')
8const imageMimeTypesRegex = `image/(${imageMimeTypes})`
9function isActorImageFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[], fieldname: string) {
10 return isFileValid(files, imageMimeTypesRegex, fieldname, CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max)
11}
12
13// ---------------------------------------------------------------------------
14
15export {
16 isActorImageFile
17}
diff --git a/server/helpers/custom-validators/user-notifications.ts b/server/helpers/custom-validators/user-notifications.ts
index 8a33b895b..252c107db 100644
--- a/server/helpers/custom-validators/user-notifications.ts
+++ b/server/helpers/custom-validators/user-notifications.ts
@@ -1,10 +1,9 @@
1import { exists } from './misc'
2import validator from 'validator' 1import validator from 'validator'
3import { UserNotificationType } from '../../../shared/models/users'
4import { UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' 2import { UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model'
3import { exists } from './misc'
5 4
6function isUserNotificationTypeValid (value: any) { 5function isUserNotificationTypeValid (value: any) {
7 return exists(value) && validator.isInt('' + value) && UserNotificationType[value] !== undefined 6 return exists(value) && validator.isInt('' + value)
8} 7}
9 8
10function isUserNotificationSettingValid (value: any) { 9function isUserNotificationSettingValid (value: any) {
diff --git a/server/helpers/custom-validators/users.ts b/server/helpers/custom-validators/users.ts
index d6e91ad35..5b21c3529 100644
--- a/server/helpers/custom-validators/users.ts
+++ b/server/helpers/custom-validators/users.ts
@@ -1,9 +1,9 @@
1import { values } from 'lodash'
1import validator from 'validator' 2import validator from 'validator'
2import { UserRole } from '../../../shared' 3import { UserRole } from '../../../shared'
3import { CONSTRAINTS_FIELDS, NSFW_POLICY_TYPES } from '../../initializers/constants'
4import { exists, isArray, isBooleanValid, isFileValid } from './misc'
5import { values } from 'lodash'
6import { isEmailEnabled } from '../../initializers/config' 4import { isEmailEnabled } from '../../initializers/config'
5import { CONSTRAINTS_FIELDS, NSFW_POLICY_TYPES } from '../../initializers/constants'
6import { exists, isArray, isBooleanValid } from './misc'
7 7
8const USERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USERS 8const USERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USERS
9 9
@@ -97,14 +97,6 @@ function isUserRoleValid (value: any) {
97 return exists(value) && validator.isInt('' + value) && UserRole[value] !== undefined 97 return exists(value) && validator.isInt('' + value) && UserRole[value] !== undefined
98} 98}
99 99
100const avatarMimeTypes = CONSTRAINTS_FIELDS.ACTORS.AVATAR.EXTNAME
101 .map(v => v.replace('.', ''))
102 .join('|')
103const avatarMimeTypesRegex = `image/(${avatarMimeTypes})`
104function isAvatarFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) {
105 return isFileValid(files, avatarMimeTypesRegex, 'avatarfile', CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max)
106}
107
108// --------------------------------------------------------------------------- 100// ---------------------------------------------------------------------------
109 101
110export { 102export {
@@ -128,6 +120,5 @@ export {
128 isUserDisplayNameValid, 120 isUserDisplayNameValid,
129 isUserDescriptionValid, 121 isUserDescriptionValid,
130 isNoInstanceConfigWarningModal, 122 isNoInstanceConfigWarningModal,
131 isNoWelcomeModal, 123 isNoWelcomeModal
132 isAvatarFile
133} 124}
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts
index 620025966..01c3aa5f7 100644
--- a/server/helpers/ffmpeg-utils.ts
+++ b/server/helpers/ffmpeg-utils.ts
@@ -5,7 +5,7 @@ import { dirname, join } from 'path'
5import { FFMPEG_NICE, VIDEO_LIVE } from '@server/initializers/constants' 5import { FFMPEG_NICE, VIDEO_LIVE } from '@server/initializers/constants'
6import { AvailableEncoders, EncoderOptionsBuilder, EncoderProfile, VideoResolution } from '../../shared/models/videos' 6import { AvailableEncoders, EncoderOptionsBuilder, EncoderProfile, VideoResolution } from '../../shared/models/videos'
7import { CONFIG } from '../initializers/config' 7import { CONFIG } from '../initializers/config'
8import { promisify0 } from './core-utils' 8import { execPromise, promisify0 } from './core-utils'
9import { computeFPS, getAudioStream, getVideoFileFPS } from './ffprobe-utils' 9import { computeFPS, getAudioStream, getVideoFileFPS } from './ffprobe-utils'
10import { processImage } from './image-utils' 10import { processImage } from './image-utils'
11import { logger } from './logger' 11import { logger } from './logger'
@@ -649,6 +649,24 @@ function getFFmpeg (input: string, type: 'live' | 'vod') {
649 return command 649 return command
650} 650}
651 651
652function getFFmpegVersion () {
653 return new Promise<string>((res, rej) => {
654 (ffmpeg() as any)._getFfmpegPath((err, ffmpegPath) => {
655 if (err) return rej(err)
656 if (!ffmpegPath) return rej(new Error('Could not find ffmpeg path'))
657
658 return execPromise(`${ffmpegPath} -version`)
659 .then(stdout => {
660 const parsed = stdout.match(/ffmpeg version .?(\d+\.\d+\.\d+)/)
661 if (!parsed || !parsed[1]) return rej(new Error(`Could not find ffmpeg version in ${stdout}`))
662
663 return res(parsed[1])
664 })
665 .catch(err => rej(err))
666 })
667 })
668}
669
652async function runCommand (options: { 670async function runCommand (options: {
653 command: ffmpeg.FfmpegCommand 671 command: ffmpeg.FfmpegCommand
654 silent?: boolean // false 672 silent?: boolean // false
@@ -695,6 +713,7 @@ export {
695 TranscodeOptionsType, 713 TranscodeOptionsType,
696 transcode, 714 transcode,
697 runCommand, 715 runCommand,
716 getFFmpegVersion,
698 717
699 resetSupportedEncoders, 718 resetSupportedEncoders,
700 719
diff --git a/server/helpers/image-utils.ts b/server/helpers/image-utils.ts
index 9285c12fc..6f6f8d4da 100644
--- a/server/helpers/image-utils.ts
+++ b/server/helpers/image-utils.ts
@@ -1,9 +1,14 @@
1import { copy, readFile, remove, rename } from 'fs-extra' 1import { copy, readFile, remove, rename } from 'fs-extra'
2import * as Jimp from 'jimp' 2import * as Jimp from 'jimp'
3import { extname } from 'path' 3import { extname } from 'path'
4import { v4 as uuidv4 } from 'uuid'
4import { convertWebPToJPG, processGIF } from './ffmpeg-utils' 5import { convertWebPToJPG, processGIF } from './ffmpeg-utils'
5import { logger } from './logger' 6import { logger } from './logger'
6 7
8function generateImageFilename (extension = '.jpg') {
9 return uuidv4() + extension
10}
11
7async function processImage ( 12async function processImage (
8 path: string, 13 path: string,
9 destination: string, 14 destination: string,
@@ -31,6 +36,7 @@ async function processImage (
31// --------------------------------------------------------------------------- 36// ---------------------------------------------------------------------------
32 37
33export { 38export {
39 generateImageFilename,
34 processImage 40 processImage
35} 41}
36 42
diff --git a/server/helpers/logger.ts b/server/helpers/logger.ts
index 6917a64d9..a112fd300 100644
--- a/server/helpers/logger.ts
+++ b/server/helpers/logger.ts
@@ -48,7 +48,7 @@ function getLoggerReplacer () {
48} 48}
49 49
50const consoleLoggerFormat = winston.format.printf(info => { 50const consoleLoggerFormat = winston.format.printf(info => {
51 const toOmit = [ 'label', 'timestamp', 'level', 'message', 'sql' ] 51 const toOmit = [ 'label', 'timestamp', 'level', 'message', 'sql', 'tags' ]
52 52
53 const obj = omit(info, ...toOmit) 53 const obj = omit(info, ...toOmit)
54 54
@@ -150,6 +150,13 @@ const bunyanLogger = {
150 error: bunyanLogFactory('error'), 150 error: bunyanLogFactory('error'),
151 fatal: bunyanLogFactory('error') 151 fatal: bunyanLogFactory('error')
152} 152}
153
154function loggerTagsFactory (...defaultTags: string[]) {
155 return (...tags: string[]) => {
156 return { tags: defaultTags.concat(tags) }
157 }
158}
159
153// --------------------------------------------------------------------------- 160// ---------------------------------------------------------------------------
154 161
155export { 162export {
@@ -159,5 +166,6 @@ export {
159 consoleLoggerFormat, 166 consoleLoggerFormat,
160 jsonLoggerFormat, 167 jsonLoggerFormat,
161 logger, 168 logger,
169 loggerTagsFactory,
162 bunyanLogger 170 bunyanLogger
163} 171}
diff --git a/server/helpers/middlewares/video-channels.ts b/server/helpers/middlewares/video-channels.ts
index 05499bb74..e6eab65a2 100644
--- a/server/helpers/middlewares/video-channels.ts
+++ b/server/helpers/middlewares/video-channels.ts
@@ -1,7 +1,7 @@
1import * as express from 'express' 1import * as express from 'express'
2import { VideoChannelModel } from '../../models/video/video-channel' 2import { MChannelBannerAccountDefault } from '@server/types/models'
3import { MChannelAccountDefault } from '@server/types/models'
4import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' 3import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
4import { VideoChannelModel } from '../../models/video/video-channel'
5 5
6async function doesLocalVideoChannelNameExist (name: string, res: express.Response) { 6async function doesLocalVideoChannelNameExist (name: string, res: express.Response) {
7 const videoChannel = await VideoChannelModel.loadLocalByNameAndPopulateAccount(name) 7 const videoChannel = await VideoChannelModel.loadLocalByNameAndPopulateAccount(name)
@@ -29,11 +29,10 @@ export {
29 doesVideoChannelNameWithHostExist 29 doesVideoChannelNameWithHostExist
30} 30}
31 31
32function processVideoChannelExist (videoChannel: MChannelAccountDefault, res: express.Response) { 32function processVideoChannelExist (videoChannel: MChannelBannerAccountDefault, res: express.Response) {
33 if (!videoChannel) { 33 if (!videoChannel) {
34 res.status(HttpStatusCode.NOT_FOUND_404) 34 res.status(HttpStatusCode.NOT_FOUND_404)
35 .json({ error: 'Video channel not found' }) 35 .json({ error: 'Video channel not found' })
36 .end()
37 36
38 return false 37 return false
39 } 38 }
diff --git a/server/helpers/middlewares/videos.ts b/server/helpers/middlewares/videos.ts
index c5eb0607a..403cae092 100644
--- a/server/helpers/middlewares/videos.ts
+++ b/server/helpers/middlewares/videos.ts
@@ -66,25 +66,24 @@ async function doesVideoFileOfVideoExist (id: number, videoIdOrUUID: number | st
66} 66}
67 67
68async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAccountId, res: Response) { 68async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAccountId, res: Response) {
69 if (user.hasRight(UserRight.UPDATE_ANY_VIDEO) === true) { 69 const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId)
70 const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId)
71 if (videoChannel === null) {
72 res.status(HttpStatusCode.BAD_REQUEST_400)
73 .json({ error: 'Unknown video `video channel` on this instance.' })
74 .end()
75 70
76 return false 71 if (videoChannel === null) {
77 } 72 res.status(HttpStatusCode.BAD_REQUEST_400)
73 .json({ error: 'Unknown video "video channel" for this instance.' })
78 74
75 return false
76 }
77
78 // Don't check account id if the user can update any video
79 if (user.hasRight(UserRight.UPDATE_ANY_VIDEO) === true) {
79 res.locals.videoChannel = videoChannel 80 res.locals.videoChannel = videoChannel
80 return true 81 return true
81 } 82 }
82 83
83 const videoChannel = await VideoChannelModel.loadByIdAndAccount(channelId, user.Account.id) 84 if (videoChannel.Account.id !== user.Account.id) {
84 if (videoChannel === null) {
85 res.status(HttpStatusCode.BAD_REQUEST_400) 85 res.status(HttpStatusCode.BAD_REQUEST_400)
86 .json({ error: 'Unknown video `video channel` for this account.' }) 86 .json({ error: 'Unknown video "video channel" for this account.' })
87 .end()
88 87
89 return false 88 return false
90 } 89 }
diff --git a/server/helpers/peertube-crypto.ts b/server/helpers/peertube-crypto.ts
index 994f725d8..bc6f1d074 100644
--- a/server/helpers/peertube-crypto.ts
+++ b/server/helpers/peertube-crypto.ts
@@ -84,7 +84,7 @@ async function isJsonLDRSA2017Verified (fromActor: MActor, signedDocument: any)
84 return verify.verify(fromActor.publicKey, signedDocument.signature.signatureValue, 'base64') 84 return verify.verify(fromActor.publicKey, signedDocument.signature.signatureValue, 'base64')
85} 85}
86 86
87async function signJsonLDObject (byActor: MActor, data: any) { 87async function signJsonLDObject <T> (byActor: MActor, data: T) {
88 const signature = { 88 const signature = {
89 type: 'RsaSignature2017', 89 type: 'RsaSignature2017',
90 creator: byActor.url, 90 creator: byActor.url,
diff --git a/server/helpers/requests.ts b/server/helpers/requests.ts
index b556c392e..fd2a56f30 100644
--- a/server/helpers/requests.ts
+++ b/server/helpers/requests.ts
@@ -1,58 +1,141 @@
1import * as Bluebird from 'bluebird'
2import { createWriteStream, remove } from 'fs-extra' 1import { createWriteStream, remove } from 'fs-extra'
3import * as request from 'request' 2import got, { CancelableRequest, Options as GotOptions, RequestError } from 'got'
3import { join } from 'path'
4import { CONFIG } from '../initializers/config'
4import { ACTIVITY_PUB, PEERTUBE_VERSION, WEBSERVER } from '../initializers/constants' 5import { ACTIVITY_PUB, PEERTUBE_VERSION, WEBSERVER } from '../initializers/constants'
6import { pipelinePromise } from './core-utils'
5import { processImage } from './image-utils' 7import { processImage } from './image-utils'
6import { join } from 'path'
7import { logger } from './logger' 8import { logger } from './logger'
8import { CONFIG } from '../initializers/config'
9 9
10function doRequest <T> ( 10export interface PeerTubeRequestError extends Error {
11 requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean }, 11 statusCode?: number
12 bodyKBLimit = 1000 // 1MB 12 responseBody?: any
13): Bluebird<{ response: request.RequestResponse, body: T }> { 13}
14 if (!(requestOptions.headers)) requestOptions.headers = {}
15 requestOptions.headers['User-Agent'] = getUserAgent()
16 14
17 if (requestOptions.activityPub === true) { 15const httpSignature = require('http-signature')
18 requestOptions.headers['accept'] = ACTIVITY_PUB.ACCEPT_HEADER 16
17type PeerTubeRequestOptions = {
18 activityPub?: boolean
19 bodyKBLimit?: number // 1MB
20 httpSignature?: {
21 algorithm: string
22 authorizationHeaderName: string
23 keyId: string
24 key: string
25 headers: string[]
19 } 26 }
27 jsonResponse?: boolean
28} & Pick<GotOptions, 'headers' | 'json' | 'method' | 'searchParams'>
29
30const peertubeGot = got.extend({
31 headers: {
32 'user-agent': getUserAgent()
33 },
34
35 handlers: [
36 (options, next) => {
37 const promiseOrStream = next(options) as CancelableRequest<any>
38 const bodyKBLimit = options.context?.bodyKBLimit as number
39 if (!bodyKBLimit) throw new Error('No KB limit for this request')
40
41 const bodyLimit = bodyKBLimit * 1000
42
43 /* eslint-disable @typescript-eslint/no-floating-promises */
44 promiseOrStream.on('downloadProgress', progress => {
45 if (progress.transferred > bodyLimit && progress.percent !== 1) {
46 const message = `Exceeded the download limit of ${bodyLimit} B`
47 logger.warn(message)
48
49 // CancelableRequest
50 if (promiseOrStream.cancel) {
51 promiseOrStream.cancel()
52 return
53 }
54
55 // Stream
56 (promiseOrStream as any).destroy()
57 }
58 })
20 59
21 return new Bluebird<{ response: request.RequestResponse, body: T }>((res, rej) => { 60 return promiseOrStream
22 request(requestOptions, (err, response, body) => err ? rej(err) : res({ response, body })) 61 }
23 .on('data', onRequestDataLengthCheck(bodyKBLimit)) 62 ],
24 }) 63
64 hooks: {
65 beforeRequest: [
66 options => {
67 const headers = options.headers || {}
68 headers['host'] = options.url.host
69 },
70
71 options => {
72 const httpSignatureOptions = options.context?.httpSignature
73
74 if (httpSignatureOptions) {
75 const method = options.method ?? 'GET'
76 const path = options.path ?? options.url.pathname
77
78 if (!method || !path) {
79 throw new Error(`Cannot sign request without method (${method}) or path (${path}) ${options}`)
80 }
81
82 httpSignature.signRequest({
83 getHeader: function (header) {
84 return options.headers[header]
85 },
86
87 setHeader: function (header, value) {
88 options.headers[header] = value
89 },
90
91 method,
92 path
93 }, httpSignatureOptions)
94 }
95 }
96 ]
97 }
98})
99
100function doRequest (url: string, options: PeerTubeRequestOptions = {}) {
101 const gotOptions = buildGotOptions(options)
102
103 return peertubeGot(url, gotOptions)
104 .catch(err => { throw buildRequestError(err) })
25} 105}
26 106
27function doRequestAndSaveToFile ( 107function doJSONRequest <T> (url: string, options: PeerTubeRequestOptions = {}) {
28 requestOptions: request.CoreOptions & request.UriOptions, 108 const gotOptions = buildGotOptions(options)
109
110 return peertubeGot<T>(url, { ...gotOptions, responseType: 'json' })
111 .catch(err => { throw buildRequestError(err) })
112}
113
114async function doRequestAndSaveToFile (
115 url: string,
29 destPath: string, 116 destPath: string,
30 bodyKBLimit = 10000 // 10MB 117 options: PeerTubeRequestOptions = {}
31) { 118) {
32 if (!requestOptions.headers) requestOptions.headers = {} 119 const gotOptions = buildGotOptions(options)
33 requestOptions.headers['User-Agent'] = getUserAgent()
34
35 return new Bluebird<void>((res, rej) => {
36 const file = createWriteStream(destPath)
37 file.on('finish', () => res())
38 120
39 request(requestOptions) 121 const outFile = createWriteStream(destPath)
40 .on('data', onRequestDataLengthCheck(bodyKBLimit))
41 .on('error', err => {
42 file.close()
43 122
44 remove(destPath) 123 try {
45 .catch(err => logger.error('Cannot remove %s after request failure.', destPath, { err })) 124 await pipelinePromise(
125 peertubeGot.stream(url, gotOptions),
126 outFile
127 )
128 } catch (err) {
129 remove(destPath)
130 .catch(err => logger.error('Cannot remove %s after request failure.', destPath, { err }))
46 131
47 return rej(err) 132 throw buildRequestError(err)
48 }) 133 }
49 .pipe(file)
50 })
51} 134}
52 135
53async function downloadImage (url: string, destDir: string, destName: string, size: { width: number, height: number }) { 136async function downloadImage (url: string, destDir: string, destName: string, size: { width: number, height: number }) {
54 const tmpPath = join(CONFIG.STORAGE.TMP_DIR, 'pending-' + destName) 137 const tmpPath = join(CONFIG.STORAGE.TMP_DIR, 'pending-' + destName)
55 await doRequestAndSaveToFile({ method: 'GET', uri: url }, tmpPath) 138 await doRequestAndSaveToFile(url, tmpPath)
56 139
57 const destPath = join(destDir, destName) 140 const destPath = join(destDir, destName)
58 141
@@ -73,24 +156,46 @@ function getUserAgent () {
73 156
74export { 157export {
75 doRequest, 158 doRequest,
159 doJSONRequest,
76 doRequestAndSaveToFile, 160 doRequestAndSaveToFile,
77 downloadImage 161 downloadImage
78} 162}
79 163
80// --------------------------------------------------------------------------- 164// ---------------------------------------------------------------------------
81 165
82// Thanks to https://github.com/request/request/issues/2470#issuecomment-268929907 <3 166function buildGotOptions (options: PeerTubeRequestOptions) {
83function onRequestDataLengthCheck (bodyKBLimit: number) { 167 const { activityPub, bodyKBLimit = 1000 } = options
84 let bufferLength = 0
85 const bytesLimit = bodyKBLimit * 1000
86 168
87 return function (chunk) { 169 const context = { bodyKBLimit, httpSignature: options.httpSignature }
88 bufferLength += chunk.length
89 if (bufferLength > bytesLimit) {
90 this.abort()
91 170
92 const error = new Error(`Response was too large - aborted after ${bytesLimit} bytes.`) 171 let headers = options.headers || {}
93 this.emit('error', error) 172
94 } 173 if (!headers.date) {
174 headers = { ...headers, date: new Date().toUTCString() }
175 }
176
177 if (activityPub && !headers.accept) {
178 headers = { ...headers, accept: ACTIVITY_PUB.ACCEPT_HEADER }
95 } 179 }
180
181 return {
182 method: options.method,
183 json: options.json,
184 searchParams: options.searchParams,
185 headers,
186 context
187 }
188}
189
190function buildRequestError (error: RequestError) {
191 const newError: PeerTubeRequestError = new Error(error.message)
192 newError.name = error.name
193 newError.stack = error.stack
194
195 if (error.response) {
196 newError.responseBody = error.response.body
197 newError.statusCode = error.response.statusCode
198 }
199
200 return newError
96} 201}
diff --git a/server/helpers/youtube-dl.ts b/server/helpers/youtube-dl.ts
index 5b46f704a..fac3da6ba 100644
--- a/server/helpers/youtube-dl.ts
+++ b/server/helpers/youtube-dl.ts
@@ -1,13 +1,13 @@
1import { createWriteStream } from 'fs' 1import { createWriteStream } from 'fs'
2import { ensureDir, move, pathExists, remove, writeFile } from 'fs-extra' 2import { ensureDir, move, pathExists, remove, writeFile } from 'fs-extra'
3import got from 'got'
3import { join } from 'path' 4import { join } from 'path'
4import * as request from 'request'
5import { CONFIG } from '@server/initializers/config' 5import { CONFIG } from '@server/initializers/config'
6import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' 6import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
7import { VideoResolution } from '../../shared/models/videos' 7import { VideoResolution } from '../../shared/models/videos'
8import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../initializers/constants' 8import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../initializers/constants'
9import { getEnabledResolutions } from '../lib/video-transcoding' 9import { getEnabledResolutions } from '../lib/video-transcoding'
10import { peertubeTruncate, root } from './core-utils' 10import { peertubeTruncate, pipelinePromise, root } from './core-utils'
11import { isVideoFileExtnameValid } from './custom-validators/videos' 11import { isVideoFileExtnameValid } from './custom-validators/videos'
12import { logger } from './logger' 12import { logger } from './logger'
13import { generateVideoImportTmpPath } from './utils' 13import { generateVideoImportTmpPath } from './utils'
@@ -195,55 +195,32 @@ async function updateYoutubeDLBinary () {
195 195
196 await ensureDir(binDirectory) 196 await ensureDir(binDirectory)
197 197
198 return new Promise<void>(res => { 198 try {
199 request.get(url, { followRedirect: false }, (err, result) => { 199 const result = await got(url, { followRedirect: false })
200 if (err) {
201 logger.error('Cannot update youtube-dl.', { err })
202 return res()
203 }
204
205 if (result.statusCode !== HttpStatusCode.FOUND_302) {
206 logger.error('youtube-dl update error: did not get redirect for the latest version link. Status %d', result.statusCode)
207 return res()
208 }
209
210 const url = result.headers.location
211 const downloadFile = request.get(url)
212 const newVersion = /yt-dl\.org\/downloads\/(\d{4}\.\d\d\.\d\d(\.\d)?)\/youtube-dl/.exec(url)[1]
213
214 downloadFile.on('response', result => {
215 if (result.statusCode !== HttpStatusCode.OK_200) {
216 logger.error('Cannot update youtube-dl: new version response is not 200, it\'s %d.', result.statusCode)
217 return res()
218 }
219
220 const writeStream = createWriteStream(bin, { mode: 493 }).on('error', err => {
221 logger.error('youtube-dl update error in write stream', { err })
222 return res()
223 })
224 200
225 downloadFile.pipe(writeStream) 201 if (result.statusCode !== HttpStatusCode.FOUND_302) {
226 }) 202 logger.error('youtube-dl update error: did not get redirect for the latest version link. Status %d', result.statusCode)
203 return
204 }
227 205
228 downloadFile.on('error', err => { 206 const newUrl = result.headers.location
229 logger.error('youtube-dl update error.', { err }) 207 const newVersion = /yt-dl\.org\/downloads\/(\d{4}\.\d\d\.\d\d(\.\d)?)\/youtube-dl/.exec(newUrl)[1]
230 return res()
231 })
232 208
233 downloadFile.on('end', () => { 209 const downloadFileStream = got.stream(newUrl)
234 const details = JSON.stringify({ version: newVersion, path: bin, exec: 'youtube-dl' }) 210 const writeStream = createWriteStream(bin, { mode: 493 })
235 writeFile(detailsPath, details, { encoding: 'utf8' }, err => {
236 if (err) {
237 logger.error('youtube-dl update error: cannot write details.', { err })
238 return res()
239 }
240 211
241 logger.info('youtube-dl updated to version %s.', newVersion) 212 await pipelinePromise(
242 return res() 213 downloadFileStream,
243 }) 214 writeStream
244 }) 215 )
245 }) 216
246 }) 217 const details = JSON.stringify({ version: newVersion, path: bin, exec: 'youtube-dl' })
218 await writeFile(detailsPath, details, { encoding: 'utf8' })
219
220 logger.info('youtube-dl updated to version %s.', newVersion)
221 } catch (err) {
222 logger.error('Cannot update youtube-dl.', { err })
223 }
247} 224}
248 225
249async function safeGetYoutubeDL () { 226async function safeGetYoutubeDL () {
diff --git a/server/initializers/checker-after-init.ts b/server/initializers/checker-after-init.ts
index 2b00e2047..a93c8b7fd 100644
--- a/server/initializers/checker-after-init.ts
+++ b/server/initializers/checker-after-init.ts
@@ -1,16 +1,17 @@
1import * as config from 'config' 1import * as config from 'config'
2import { isProdInstance, isTestInstance } from '../helpers/core-utils' 2import { uniq } from 'lodash'
3import { UserModel } from '../models/account/user'
4import { getServerActor, ApplicationModel } from '../models/application/application'
5import { OAuthClientModel } from '../models/oauth/oauth-client'
6import { URL } from 'url' 3import { URL } from 'url'
7import { CONFIG, isEmailEnabled } from './config' 4import { getFFmpegVersion } from '@server/helpers/ffmpeg-utils'
8import { logger } from '../helpers/logger' 5import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type'
9import { RecentlyAddedStrategy } from '../../shared/models/redundancy' 6import { RecentlyAddedStrategy } from '../../shared/models/redundancy'
7import { isProdInstance, isTestInstance, parseSemVersion } from '../helpers/core-utils'
10import { isArray } from '../helpers/custom-validators/misc' 8import { isArray } from '../helpers/custom-validators/misc'
11import { uniq } from 'lodash' 9import { logger } from '../helpers/logger'
10import { UserModel } from '../models/account/user'
11import { ApplicationModel, getServerActor } from '../models/application/application'
12import { OAuthClientModel } from '../models/oauth/oauth-client'
13import { CONFIG, isEmailEnabled } from './config'
12import { WEBSERVER } from './constants' 14import { WEBSERVER } from './constants'
13import { VideoRedundancyConfigFilter } from '@shared/models/redundancy/video-redundancy-config-filter.type'
14 15
15async function checkActivityPubUrls () { 16async function checkActivityPubUrls () {
16 const actor = await getServerActor() 17 const actor = await getServerActor()
@@ -176,11 +177,21 @@ async function applicationExist () {
176 return totalApplication !== 0 177 return totalApplication !== 0
177} 178}
178 179
180async function checkFFmpegVersion () {
181 const version = await getFFmpegVersion()
182 const { major, minor } = parseSemVersion(version)
183
184 if (major < 4 || (major === 4 && minor < 1)) {
185 logger.warn('Your ffmpeg version (%s) is outdated. PeerTube supports ffmpeg >= 4.1. Please upgrade.', version)
186 }
187}
188
179// --------------------------------------------------------------------------- 189// ---------------------------------------------------------------------------
180 190
181export { 191export {
182 checkConfig, 192 checkConfig,
183 clientsExist, 193 clientsExist,
194 checkFFmpegVersion,
184 usersExist, 195 usersExist,
185 applicationExist, 196 applicationExist,
186 checkActivityPubUrls 197 checkActivityPubUrls
diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts
index 565e0d1fa..e92cc4d2c 100644
--- a/server/initializers/checker-before-init.ts
+++ b/server/initializers/checker-before-init.ts
@@ -1,5 +1,5 @@
1import * as config from 'config' 1import * as config from 'config'
2import { promisify0 } from '../helpers/core-utils' 2import { parseSemVersion, promisify0 } from '../helpers/core-utils'
3import { logger } from '../helpers/logger' 3import { logger } from '../helpers/logger'
4 4
5// ONLY USE CORE MODULES IN THIS FILE! 5// ONLY USE CORE MODULES IN THIS FILE!
@@ -37,6 +37,7 @@ function checkMissedConfig () {
37 'theme.default', 37 'theme.default',
38 'remote_redundancy.videos.accept_from', 38 'remote_redundancy.videos.accept_from',
39 'federation.videos.federate_unlisted', 'federation.videos.cleanup_remote_interactions', 39 'federation.videos.federate_unlisted', 'federation.videos.cleanup_remote_interactions',
40 'peertube.check_latest_version.enabled', 'peertube.check_latest_version.url',
40 'search.remote_uri.users', 'search.remote_uri.anonymous', 'search.search_index.enabled', 'search.search_index.url', 41 'search.remote_uri.users', 'search.remote_uri.anonymous', 'search.search_index.enabled', 'search.search_index.url',
41 'search.search_index.disable_local_search', 'search.search_index.is_default_search', 42 'search.search_index.disable_local_search', 'search.search_index.is_default_search',
42 'live.enabled', 'live.allow_replay', 'live.max_duration', 'live.max_user_lives', 'live.max_instance_lives', 43 'live.enabled', 'live.allow_replay', 'live.max_duration', 'live.max_user_lives', 'live.max_instance_lives',
@@ -102,8 +103,7 @@ async function checkFFmpeg (CONFIG: { TRANSCODING: { ENABLED: boolean } }) {
102 103
103function checkNodeVersion () { 104function checkNodeVersion () {
104 const v = process.version 105 const v = process.version
105 const majorString = v.split('.')[0].replace('v', '') 106 const { major } = parseSemVersion(v)
106 const major = parseInt(majorString, 10)
107 107
108 logger.debug('Checking NodeJS version %s.', v) 108 logger.debug('Checking NodeJS version %s.', v)
109 109
diff --git a/server/initializers/config.ts b/server/initializers/config.ts
index c16b63c33..93dd5ac04 100644
--- a/server/initializers/config.ts
+++ b/server/initializers/config.ts
@@ -59,7 +59,7 @@ const CONFIG = {
59 }, 59 },
60 STORAGE: { 60 STORAGE: {
61 TMP_DIR: buildPath(config.get<string>('storage.tmp')), 61 TMP_DIR: buildPath(config.get<string>('storage.tmp')),
62 AVATARS_DIR: buildPath(config.get<string>('storage.avatars')), 62 ACTOR_IMAGES: buildPath(config.get<string>('storage.avatars')),
63 LOG_DIR: buildPath(config.get<string>('storage.logs')), 63 LOG_DIR: buildPath(config.get<string>('storage.logs')),
64 VIDEOS_DIR: buildPath(config.get<string>('storage.videos')), 64 VIDEOS_DIR: buildPath(config.get<string>('storage.videos')),
65 STREAMING_PLAYLISTS_DIR: buildPath(config.get<string>('storage.streaming_playlists')), 65 STREAMING_PLAYLISTS_DIR: buildPath(config.get<string>('storage.streaming_playlists')),
@@ -163,6 +163,12 @@ const CONFIG = {
163 CLEANUP_REMOTE_INTERACTIONS: config.get<boolean>('federation.videos.cleanup_remote_interactions') 163 CLEANUP_REMOTE_INTERACTIONS: config.get<boolean>('federation.videos.cleanup_remote_interactions')
164 } 164 }
165 }, 165 },
166 PEERTUBE: {
167 CHECK_LATEST_VERSION: {
168 ENABLED: config.get<boolean>('peertube.check_latest_version.enabled'),
169 URL: config.get<string>('peertube.check_latest_version.url')
170 }
171 },
166 ADMIN: { 172 ADMIN: {
167 get EMAIL () { return config.get<string>('admin.email') } 173 get EMAIL () { return config.get<string>('admin.email') }
168 }, 174 },
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 50467f408..1802257df 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -24,12 +24,12 @@ import { CONFIG, registerConfigChangedHandler } from './config'
24 24
25// --------------------------------------------------------------------------- 25// ---------------------------------------------------------------------------
26 26
27const LAST_MIGRATION_VERSION = 612 27const LAST_MIGRATION_VERSION = 635
28 28
29// --------------------------------------------------------------------------- 29// ---------------------------------------------------------------------------
30 30
31const API_VERSION = 'v1' 31const API_VERSION = 'v1'
32const PEERTUBE_VERSION = require(join(root(), 'package.json')).version 32const PEERTUBE_VERSION: string = require(join(root(), 'package.json')).version
33 33
34const PAGINATION = { 34const PAGINATION = {
35 GLOBAL: { 35 GLOBAL: {
@@ -207,6 +207,7 @@ const SCHEDULER_INTERVALS_MS = {
207 updateVideos: 60000, // 1 minute 207 updateVideos: 60000, // 1 minute
208 youtubeDLUpdate: 60000 * 60 * 24, // 1 day 208 youtubeDLUpdate: 60000 * 60 * 24, // 1 day
209 checkPlugins: CONFIG.PLUGINS.INDEX.CHECK_LATEST_VERSIONS_INTERVAL, 209 checkPlugins: CONFIG.PLUGINS.INDEX.CHECK_LATEST_VERSIONS_INTERVAL,
210 checkPeerTubeVersion: 60000 * 60 * 24, // 1 day
210 autoFollowIndexInstances: 60000 * 60 * 24, // 1 day 211 autoFollowIndexInstances: 60000 * 60 * 24, // 1 day
211 removeOldViews: 60000 * 60 * 24, // 1 day 212 removeOldViews: 60000 * 60 * 24, // 1 day
212 removeOldHistory: 60000 * 60 * 24, // 1 day 213 removeOldHistory: 60000 * 60 * 24, // 1 day
@@ -304,7 +305,7 @@ const CONSTRAINTS_FIELDS = {
304 PUBLIC_KEY: { min: 10, max: 5000 }, // Length 305 PUBLIC_KEY: { min: 10, max: 5000 }, // Length
305 PRIVATE_KEY: { min: 10, max: 5000 }, // Length 306 PRIVATE_KEY: { min: 10, max: 5000 }, // Length
306 URL: { min: 3, max: 2000 }, // Length 307 URL: { min: 3, max: 2000 }, // Length
307 AVATAR: { 308 IMAGE: {
308 EXTNAME: [ '.png', '.jpeg', '.jpg', '.gif', '.webp' ], 309 EXTNAME: [ '.png', '.jpeg', '.jpg', '.gif', '.webp' ],
309 FILE_SIZE: { 310 FILE_SIZE: {
310 max: 2 * 1024 * 1024 // 2MB 311 max: 2 * 1024 * 1024 // 2MB
@@ -465,6 +466,8 @@ const MIMETYPES = {
465 IMAGE: { 466 IMAGE: {
466 MIMETYPE_EXT: { 467 MIMETYPE_EXT: {
467 'image/png': '.png', 468 'image/png': '.png',
469 'image/gif': '.gif',
470 'image/webp': '.webp',
468 'image/jpg': '.jpg', 471 'image/jpg': '.jpg',
469 'image/jpeg': '.jpg' 472 'image/jpeg': '.jpg'
470 }, 473 },
@@ -579,6 +582,7 @@ const STATIC_DOWNLOAD_PATHS = {
579 HLS_VIDEOS: '/download/streaming-playlists/hls/videos/' 582 HLS_VIDEOS: '/download/streaming-playlists/hls/videos/'
580} 583}
581const LAZY_STATIC_PATHS = { 584const LAZY_STATIC_PATHS = {
585 BANNERS: '/lazy-static/banners/',
582 AVATARS: '/lazy-static/avatars/', 586 AVATARS: '/lazy-static/avatars/',
583 PREVIEWS: '/lazy-static/previews/', 587 PREVIEWS: '/lazy-static/previews/',
584 VIDEO_CAPTIONS: '/lazy-static/video-captions/', 588 VIDEO_CAPTIONS: '/lazy-static/video-captions/',
@@ -594,8 +598,8 @@ const STATIC_MAX_AGE = {
594 598
595// Videos thumbnail size 599// Videos thumbnail size
596const THUMBNAILS_SIZE = { 600const THUMBNAILS_SIZE = {
597 width: 223, 601 width: 280,
598 height: 122, 602 height: 157,
599 minWidth: 150 603 minWidth: 150
600} 604}
601const PREVIEWS_SIZE = { 605const PREVIEWS_SIZE = {
@@ -603,9 +607,15 @@ const PREVIEWS_SIZE = {
603 height: 480, 607 height: 480,
604 minWidth: 400 608 minWidth: 400
605} 609}
606const AVATARS_SIZE = { 610const ACTOR_IMAGES_SIZE = {
607 width: 120, 611 AVATARS: {
608 height: 120 612 width: 120,
613 height: 120
614 },
615 BANNERS: {
616 width: 1920,
617 height: 317 // 6/1 ratio
618 }
609} 619}
610 620
611const EMBED_SIZE = { 621const EMBED_SIZE = {
@@ -633,7 +643,7 @@ const LRU_CACHE = {
633 USER_TOKENS: { 643 USER_TOKENS: {
634 MAX_SIZE: 1000 644 MAX_SIZE: 1000
635 }, 645 },
636 AVATAR_STATIC: { 646 ACTOR_IMAGE_STATIC: {
637 MAX_SIZE: 500 647 MAX_SIZE: 500
638 } 648 }
639} 649}
@@ -670,7 +680,7 @@ const MEMOIZE_LENGTH = {
670} 680}
671 681
672const QUEUE_CONCURRENCY = { 682const QUEUE_CONCURRENCY = {
673 AVATAR_PROCESS_IMAGE: 3 683 ACTOR_PROCESS_IMAGE: 3
674} 684}
675 685
676const REDUNDANCY = { 686const REDUNDANCY = {
@@ -753,7 +763,7 @@ if (isTestInstance() === true) {
753 ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL = 10 * 1000 // 10 seconds 763 ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL = 10 * 1000 // 10 seconds
754 ACTIVITY_PUB.VIDEO_PLAYLIST_REFRESH_INTERVAL = 10 * 1000 // 10 seconds 764 ACTIVITY_PUB.VIDEO_PLAYLIST_REFRESH_INTERVAL = 10 * 1000 // 10 seconds
755 765
756 CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max = 100 * 1024 // 100KB 766 CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max = 100 * 1024 // 100KB
757 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max = 400 * 1024 // 400KB 767 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max = 400 * 1024 // 400KB
758 768
759 SCHEDULER_INTERVALS_MS.actorFollowScores = 1000 769 SCHEDULER_INTERVALS_MS.actorFollowScores = 1000
@@ -763,6 +773,7 @@ if (isTestInstance() === true) {
763 SCHEDULER_INTERVALS_MS.updateVideos = 5000 773 SCHEDULER_INTERVALS_MS.updateVideos = 5000
764 SCHEDULER_INTERVALS_MS.autoFollowIndexInstances = 5000 774 SCHEDULER_INTERVALS_MS.autoFollowIndexInstances = 5000
765 SCHEDULER_INTERVALS_MS.updateInboxStats = 5000 775 SCHEDULER_INTERVALS_MS.updateInboxStats = 5000
776 SCHEDULER_INTERVALS_MS.checkPeerTubeVersion = 2000
766 REPEAT_JOBS['videos-views'] = { every: 5000 } 777 REPEAT_JOBS['videos-views'] = { every: 5000 }
767 REPEAT_JOBS['activitypub-cleaner'] = { every: 5000 } 778 REPEAT_JOBS['activitypub-cleaner'] = { every: 5000 }
768 779
@@ -813,7 +824,7 @@ export {
813 SEARCH_INDEX, 824 SEARCH_INDEX,
814 HLS_REDUNDANCY_DIRECTORY, 825 HLS_REDUNDANCY_DIRECTORY,
815 P2P_MEDIA_LOADER_PEER_VERSION, 826 P2P_MEDIA_LOADER_PEER_VERSION,
816 AVATARS_SIZE, 827 ACTOR_IMAGES_SIZE,
817 ACCEPT_HEADERS, 828 ACCEPT_HEADERS,
818 BCRYPT_SALT_SIZE, 829 BCRYPT_SALT_SIZE,
819 TRACKER_RATE_LIMITS, 830 TRACKER_RATE_LIMITS,
diff --git a/server/initializers/database.ts b/server/initializers/database.ts
index 1f2b6d521..4c9d7c610 100644
--- a/server/initializers/database.ts
+++ b/server/initializers/database.ts
@@ -1,7 +1,7 @@
1import { TrackerModel } from '@server/models/server/tracker'
2import { VideoTrackerModel } from '@server/models/server/video-tracker'
3import { QueryTypes, Transaction } from 'sequelize' 1import { QueryTypes, Transaction } from 'sequelize'
4import { Sequelize as SequelizeTypescript } from 'sequelize-typescript' 2import { Sequelize as SequelizeTypescript } from 'sequelize-typescript'
3import { TrackerModel } from '@server/models/server/tracker'
4import { VideoTrackerModel } from '@server/models/server/video-tracker'
5import { isTestInstance } from '../helpers/core-utils' 5import { isTestInstance } from '../helpers/core-utils'
6import { logger } from '../helpers/logger' 6import { logger } from '../helpers/logger'
7import { AbuseModel } from '../models/abuse/abuse' 7import { AbuseModel } from '../models/abuse/abuse'
@@ -11,6 +11,7 @@ import { VideoCommentAbuseModel } from '../models/abuse/video-comment-abuse'
11import { AccountModel } from '../models/account/account' 11import { AccountModel } from '../models/account/account'
12import { AccountBlocklistModel } from '../models/account/account-blocklist' 12import { AccountBlocklistModel } from '../models/account/account-blocklist'
13import { AccountVideoRateModel } from '../models/account/account-video-rate' 13import { AccountVideoRateModel } from '../models/account/account-video-rate'
14import { ActorImageModel } from '../models/account/actor-image'
14import { UserModel } from '../models/account/user' 15import { UserModel } from '../models/account/user'
15import { UserNotificationModel } from '../models/account/user-notification' 16import { UserNotificationModel } from '../models/account/user-notification'
16import { UserNotificationSettingModel } from '../models/account/user-notification-setting' 17import { UserNotificationSettingModel } from '../models/account/user-notification-setting'
@@ -18,7 +19,6 @@ import { UserVideoHistoryModel } from '../models/account/user-video-history'
18import { ActorModel } from '../models/activitypub/actor' 19import { ActorModel } from '../models/activitypub/actor'
19import { ActorFollowModel } from '../models/activitypub/actor-follow' 20import { ActorFollowModel } from '../models/activitypub/actor-follow'
20import { ApplicationModel } from '../models/application/application' 21import { ApplicationModel } from '../models/application/application'
21import { AvatarModel } from '../models/avatar/avatar'
22import { OAuthClientModel } from '../models/oauth/oauth-client' 22import { OAuthClientModel } from '../models/oauth/oauth-client'
23import { OAuthTokenModel } from '../models/oauth/oauth-token' 23import { OAuthTokenModel } from '../models/oauth/oauth-token'
24import { VideoRedundancyModel } from '../models/redundancy/video-redundancy' 24import { VideoRedundancyModel } from '../models/redundancy/video-redundancy'
@@ -76,7 +76,7 @@ const sequelizeTypescript = new SequelizeTypescript({
76 newMessage += ' in ' + benchmark + 'ms' 76 newMessage += ' in ' + benchmark + 'ms'
77 } 77 }
78 78
79 logger.debug(newMessage, { sql: message }) 79 logger.debug(newMessage, { sql: message, tags: [ 'sql' ] })
80 } 80 }
81}) 81})
82 82
@@ -95,7 +95,7 @@ async function initDatabaseModels (silent: boolean) {
95 ApplicationModel, 95 ApplicationModel,
96 ActorModel, 96 ActorModel,
97 ActorFollowModel, 97 ActorFollowModel,
98 AvatarModel, 98 ActorImageModel,
99 AccountModel, 99 AccountModel,
100 OAuthClientModel, 100 OAuthClientModel,
101 OAuthTokenModel, 101 OAuthTokenModel,
diff --git a/server/initializers/migrations/0610-views-index.ts b/server/initializers/migrations/0610-views-index copy.ts
index 02ee21172..02ee21172 100644
--- a/server/initializers/migrations/0610-views-index.ts
+++ b/server/initializers/migrations/0610-views-index copy.ts
diff --git a/server/initializers/migrations/0615-latest-versions-notification-settings.ts b/server/initializers/migrations/0615-latest-versions-notification-settings.ts
new file mode 100644
index 000000000..86bf56009
--- /dev/null
+++ b/server/initializers/migrations/0615-latest-versions-notification-settings.ts
@@ -0,0 +1,44 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 {
10 const notificationSettingColumns = [ 'newPeerTubeVersion', 'newPluginVersion' ]
11
12 for (const column of notificationSettingColumns) {
13 const data = {
14 type: Sequelize.INTEGER,
15 defaultValue: null,
16 allowNull: true
17 }
18 await utils.queryInterface.addColumn('userNotificationSetting', column, data)
19 }
20
21 {
22 const query = 'UPDATE "userNotificationSetting" SET "newPeerTubeVersion" = 3, "newPluginVersion" = 1'
23 await utils.sequelize.query(query)
24 }
25
26 for (const column of notificationSettingColumns) {
27 const data = {
28 type: Sequelize.INTEGER,
29 defaultValue: null,
30 allowNull: false
31 }
32 await utils.queryInterface.changeColumn('userNotificationSetting', column, data)
33 }
34 }
35}
36
37function down (options) {
38 throw new Error('Not implemented.')
39}
40
41export {
42 up,
43 down
44}
diff --git a/server/initializers/migrations/0620-latest-versions-application.ts b/server/initializers/migrations/0620-latest-versions-application.ts
new file mode 100644
index 000000000..a689b18fc
--- /dev/null
+++ b/server/initializers/migrations/0620-latest-versions-application.ts
@@ -0,0 +1,27 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9
10 {
11 const data = {
12 type: Sequelize.STRING,
13 defaultValue: null,
14 allowNull: true
15 }
16 await utils.queryInterface.addColumn('application', 'latestPeerTubeVersion', data)
17 }
18}
19
20function down (options) {
21 throw new Error('Not implemented.')
22}
23
24export {
25 up,
26 down
27}
diff --git a/server/initializers/migrations/0625-latest-versions-notification.ts b/server/initializers/migrations/0625-latest-versions-notification.ts
new file mode 100644
index 000000000..77f395ce4
--- /dev/null
+++ b/server/initializers/migrations/0625-latest-versions-notification.ts
@@ -0,0 +1,26 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9
10 {
11 await utils.sequelize.query(`
12 ALTER TABLE "userNotification"
13 ADD COLUMN "applicationId" INTEGER REFERENCES "application" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
14 ADD COLUMN "pluginId" INTEGER REFERENCES "plugin" ("id") ON DELETE SET NULL ON UPDATE CASCADE
15 `)
16 }
17}
18
19function down (options) {
20 throw new Error('Not implemented.')
21}
22
23export {
24 up,
25 down
26}
diff --git a/server/initializers/migrations/0630-banner.ts b/server/initializers/migrations/0630-banner.ts
new file mode 100644
index 000000000..5766bb171
--- /dev/null
+++ b/server/initializers/migrations/0630-banner.ts
@@ -0,0 +1,50 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9
10 {
11 await utils.sequelize.query(`ALTER TABLE "avatar" RENAME to "actorImage"`)
12 }
13
14 {
15 const data = {
16 type: Sequelize.INTEGER,
17 defaultValue: null,
18 allowNull: true
19 }
20 await utils.queryInterface.addColumn('actorImage', 'type', data)
21 }
22
23 {
24 await utils.sequelize.query(`UPDATE "actorImage" SET "type" = 1`)
25 }
26
27 {
28 const data = {
29 type: Sequelize.INTEGER,
30 defaultValue: null,
31 allowNull: false
32 }
33 await utils.queryInterface.changeColumn('actorImage', 'type', data)
34 }
35
36 {
37 await utils.sequelize.query(
38 `ALTER TABLE "actor" ADD COLUMN "bannerId" INTEGER REFERENCES "actorImage" ("id") ON DELETE SET NULL ON UPDATE CASCADE`
39 )
40 }
41}
42
43function down (options) {
44 throw new Error('Not implemented.')
45}
46
47export {
48 up,
49 down
50}
diff --git a/server/initializers/migrations/0635-actor-image-size.ts b/server/initializers/migrations/0635-actor-image-size.ts
new file mode 100644
index 000000000..d7c5da8c3
--- /dev/null
+++ b/server/initializers/migrations/0635-actor-image-size.ts
@@ -0,0 +1,35 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 {
10 const data = {
11 type: Sequelize.INTEGER,
12 defaultValue: null,
13 allowNull: true
14 }
15 await utils.queryInterface.addColumn('actorImage', 'height', data)
16 }
17
18 {
19 const data = {
20 type: Sequelize.INTEGER,
21 defaultValue: null,
22 allowNull: true
23 }
24 await utils.queryInterface.addColumn('actorImage', 'width', data)
25 }
26}
27
28function down (options) {
29 throw new Error('Not implemented.')
30}
31
32export {
33 up,
34 down
35}
diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts
index a726f9e20..eec951d4e 100644
--- a/server/lib/activitypub/actor.ts
+++ b/server/lib/activitypub/actor.ts
@@ -1,26 +1,29 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { extname } from 'path'
2import { Op, Transaction } from 'sequelize' 3import { Op, Transaction } from 'sequelize'
3import { URL } from 'url' 4import { URL } from 'url'
4import { v4 as uuidv4 } from 'uuid' 5import { v4 as uuidv4 } from 'uuid'
6import { getServerActor } from '@server/models/application/application'
7import { ActorImageType } from '@shared/models'
8import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
5import { ActivityPubActor, ActivityPubActorType, ActivityPubOrderedCollection } from '../../../shared/models/activitypub' 9import { ActivityPubActor, ActivityPubActorType, ActivityPubOrderedCollection } from '../../../shared/models/activitypub'
6import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' 10import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
7import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub' 11import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
12import { ActorFetchByUrlType, fetchActorByUrl } from '../../helpers/actor'
8import { sanitizeAndCheckActorObject } from '../../helpers/custom-validators/activitypub/actor' 13import { sanitizeAndCheckActorObject } from '../../helpers/custom-validators/activitypub/actor'
9import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 14import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
10import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils' 15import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils'
11import { logger } from '../../helpers/logger' 16import { logger } from '../../helpers/logger'
12import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto' 17import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto'
13import { doRequest } from '../../helpers/requests' 18import { doJSONRequest, PeerTubeRequestError } from '../../helpers/requests'
14import { getUrlFromWebfinger } from '../../helpers/webfinger' 19import { getUrlFromWebfinger } from '../../helpers/webfinger'
15import { MIMETYPES, WEBSERVER } from '../../initializers/constants' 20import { MIMETYPES, WEBSERVER } from '../../initializers/constants'
21import { sequelizeTypescript } from '../../initializers/database'
16import { AccountModel } from '../../models/account/account' 22import { AccountModel } from '../../models/account/account'
23import { ActorImageModel } from '../../models/account/actor-image'
17import { ActorModel } from '../../models/activitypub/actor' 24import { ActorModel } from '../../models/activitypub/actor'
18import { AvatarModel } from '../../models/avatar/avatar'
19import { ServerModel } from '../../models/server/server' 25import { ServerModel } from '../../models/server/server'
20import { VideoChannelModel } from '../../models/video/video-channel' 26import { VideoChannelModel } from '../../models/video/video-channel'
21import { JobQueue } from '../job-queue'
22import { ActorFetchByUrlType, fetchActorByUrl } from '../../helpers/actor'
23import { sequelizeTypescript } from '../../initializers/database'
24import { 27import {
25 MAccount, 28 MAccount,
26 MAccountDefault, 29 MAccountDefault,
@@ -28,15 +31,14 @@ import {
28 MActorAccountChannelId, 31 MActorAccountChannelId,
29 MActorAccountChannelIdActor, 32 MActorAccountChannelIdActor,
30 MActorAccountId, 33 MActorAccountId,
31 MActorDefault,
32 MActorFull, 34 MActorFull,
33 MActorFullActor, 35 MActorFullActor,
34 MActorId, 36 MActorId,
37 MActorImage,
38 MActorImages,
35 MChannel 39 MChannel
36} from '../../types/models' 40} from '../../types/models'
37import { extname } from 'path' 41import { JobQueue } from '../job-queue'
38import { getServerActor } from '@server/models/application/application'
39import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
40 42
41// Set account keys, this could be long so process after the account creation and do not block the client 43// Set account keys, this could be long so process after the account creation and do not block the client
42async function generateAndSaveActorKeys <T extends MActor> (actor: T) { 44async function generateAndSaveActorKeys <T extends MActor> (actor: T) {
@@ -168,66 +170,83 @@ async function updateActorInstance (actorInstance: ActorModel, attributes: Activ
168 } 170 }
169} 171}
170 172
171type AvatarInfo = { name: string, onDisk: boolean, fileUrl: string } 173type ImageInfo = {
172async function updateActorAvatarInstance (actor: MActorDefault, info: AvatarInfo, t: Transaction) { 174 name: string
173 if (!info.name) return actor 175 fileUrl: string
176 height: number
177 width: number
178 onDisk?: boolean
179}
180async function updateActorImageInstance (actor: MActorImages, type: ActorImageType, imageInfo: ImageInfo | null, t: Transaction) {
181 const oldImageModel = type === ActorImageType.AVATAR
182 ? actor.Avatar
183 : actor.Banner
174 184
175 if (actor.Avatar) { 185 if (oldImageModel) {
176 // Don't update the avatar if the file URL did not change 186 // Don't update the avatar if the file URL did not change
177 if (info.fileUrl && actor.Avatar.fileUrl === info.fileUrl) return actor 187 if (imageInfo?.fileUrl && oldImageModel.fileUrl === imageInfo.fileUrl) return actor
178 188
179 try { 189 try {
180 await actor.Avatar.destroy({ transaction: t }) 190 await oldImageModel.destroy({ transaction: t })
191
192 setActorImage(actor, type, null)
181 } catch (err) { 193 } catch (err) {
182 logger.error('Cannot remove old avatar of actor %s.', actor.url, { err }) 194 logger.error('Cannot remove old actor image of actor %s.', actor.url, { err })
183 } 195 }
184 } 196 }
185 197
186 const avatar = await AvatarModel.create({ 198 if (imageInfo) {
187 filename: info.name, 199 const imageModel = await ActorImageModel.create({
188 onDisk: info.onDisk, 200 filename: imageInfo.name,
189 fileUrl: info.fileUrl 201 onDisk: imageInfo.onDisk ?? false,
190 }, { transaction: t }) 202 fileUrl: imageInfo.fileUrl,
191 203 height: imageInfo.height,
192 actor.avatarId = avatar.id 204 width: imageInfo.width,
193 actor.Avatar = avatar 205 type
206 }, { transaction: t })
207
208 setActorImage(actor, type, imageModel)
209 }
194 210
195 return actor 211 return actor
196} 212}
197 213
198async function deleteActorAvatarInstance (actor: MActorDefault, t: Transaction) { 214async function deleteActorImageInstance (actor: MActorImages, type: ActorImageType, t: Transaction) {
199 try { 215 try {
200 await actor.Avatar.destroy({ transaction: t }) 216 if (type === ActorImageType.AVATAR) {
217 await actor.Avatar.destroy({ transaction: t })
218
219 actor.avatarId = null
220 actor.Avatar = null
221 } else {
222 await actor.Banner.destroy({ transaction: t })
223
224 actor.bannerId = null
225 actor.Banner = null
226 }
201 } catch (err) { 227 } catch (err) {
202 logger.error('Cannot remove old avatar of actor %s.', actor.url, { err }) 228 logger.error('Cannot remove old image of actor %s.', actor.url, { err })
203 } 229 }
204 230
205 actor.avatarId = null
206 actor.Avatar = null
207
208 return actor 231 return actor
209} 232}
210 233
211async function fetchActorTotalItems (url: string) { 234async function fetchActorTotalItems (url: string) {
212 const options = {
213 uri: url,
214 method: 'GET',
215 json: true,
216 activityPub: true
217 }
218
219 try { 235 try {
220 const { body } = await doRequest<ActivityPubOrderedCollection<unknown>>(options) 236 const { body } = await doJSONRequest<ActivityPubOrderedCollection<unknown>>(url, { activityPub: true })
221 return body.totalItems ? body.totalItems : 0 237
238 return body.totalItems || 0
222 } catch (err) { 239 } catch (err) {
223 logger.warn('Cannot fetch remote actor count %s.', url, { err }) 240 logger.warn('Cannot fetch remote actor count %s.', url, { err })
224 return 0 241 return 0
225 } 242 }
226} 243}
227 244
228function getAvatarInfoIfExists (actorJSON: ActivityPubActor) { 245function getImageInfoIfExists (actorJSON: ActivityPubActor, type: ActorImageType) {
229 const mimetypes = MIMETYPES.IMAGE 246 const mimetypes = MIMETYPES.IMAGE
230 const icon = actorJSON.icon 247 const icon = type === ActorImageType.AVATAR
248 ? actorJSON.icon
249 : actorJSON.image
231 250
232 if (!icon || icon.type !== 'Image' || !isActivityPubUrlValid(icon.url)) return undefined 251 if (!icon || icon.type !== 'Image' || !isActivityPubUrlValid(icon.url)) return undefined
233 252
@@ -245,7 +264,10 @@ function getAvatarInfoIfExists (actorJSON: ActivityPubActor) {
245 264
246 return { 265 return {
247 name: uuidv4() + extension, 266 name: uuidv4() + extension,
248 fileUrl: icon.url 267 fileUrl: icon.url,
268 height: icon.height,
269 width: icon.width,
270 type
249 } 271 }
250} 272}
251 273
@@ -285,16 +307,7 @@ async function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannel
285 actorUrl = actor.url 307 actorUrl = actor.url
286 } 308 }
287 309
288 const { result, statusCode } = await fetchRemoteActor(actorUrl) 310 const { result } = await fetchRemoteActor(actorUrl)
289
290 if (statusCode === HttpStatusCode.NOT_FOUND_404) {
291 logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url)
292 actor.Account
293 ? await actor.Account.destroy()
294 : await actor.VideoChannel.destroy()
295
296 return { actor: undefined, refreshed: false }
297 }
298 311
299 if (result === undefined) { 312 if (result === undefined) {
300 logger.warn('Cannot fetch remote actor in refresh actor.') 313 logger.warn('Cannot fetch remote actor in refresh actor.')
@@ -304,15 +317,8 @@ async function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannel
304 return sequelizeTypescript.transaction(async t => { 317 return sequelizeTypescript.transaction(async t => {
305 updateInstanceWithAnother(actor, result.actor) 318 updateInstanceWithAnother(actor, result.actor)
306 319
307 if (result.avatar !== undefined) { 320 await updateActorImageInstance(actor, ActorImageType.AVATAR, result.avatar, t)
308 const avatarInfo = { 321 await updateActorImageInstance(actor, ActorImageType.BANNER, result.banner, t)
309 name: result.avatar.name,
310 fileUrl: result.avatar.fileUrl,
311 onDisk: false
312 }
313
314 await updateActorAvatarInstance(actor, avatarInfo, t)
315 }
316 322
317 // Force update 323 // Force update
318 actor.setDataValue('updatedAt', new Date()) 324 actor.setDataValue('updatedAt', new Date())
@@ -334,6 +340,15 @@ async function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannel
334 return { refreshed: true, actor } 340 return { refreshed: true, actor }
335 }) 341 })
336 } catch (err) { 342 } catch (err) {
343 if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) {
344 logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url)
345 actor.Account
346 ? await actor.Account.destroy()
347 : await actor.VideoChannel.destroy()
348
349 return { actor: undefined, refreshed: false }
350 }
351
337 logger.warn('Cannot refresh actor %s.', actor.url, { err }) 352 logger.warn('Cannot refresh actor %s.', actor.url, { err })
338 return { actor, refreshed: false } 353 return { actor, refreshed: false }
339 } 354 }
@@ -344,16 +359,32 @@ export {
344 buildActorInstance, 359 buildActorInstance,
345 generateAndSaveActorKeys, 360 generateAndSaveActorKeys,
346 fetchActorTotalItems, 361 fetchActorTotalItems,
347 getAvatarInfoIfExists, 362 getImageInfoIfExists,
348 updateActorInstance, 363 updateActorInstance,
349 deleteActorAvatarInstance, 364 deleteActorImageInstance,
350 refreshActorIfNeeded, 365 refreshActorIfNeeded,
351 updateActorAvatarInstance, 366 updateActorImageInstance,
352 addFetchOutboxJob 367 addFetchOutboxJob
353} 368}
354 369
355// --------------------------------------------------------------------------- 370// ---------------------------------------------------------------------------
356 371
372function setActorImage (actorModel: MActorImages, type: ActorImageType, imageModel: MActorImage) {
373 const id = imageModel
374 ? imageModel.id
375 : null
376
377 if (type === ActorImageType.AVATAR) {
378 actorModel.avatarId = id
379 actorModel.Avatar = imageModel
380 } else {
381 actorModel.bannerId = id
382 actorModel.Banner = imageModel
383 }
384
385 return actorModel
386}
387
357function saveActorAndServerAndModelIfNotExist ( 388function saveActorAndServerAndModelIfNotExist (
358 result: FetchRemoteActorResult, 389 result: FetchRemoteActorResult,
359 ownerActor?: MActorFullActor, 390 ownerActor?: MActorFullActor,
@@ -384,15 +415,32 @@ function saveActorAndServerAndModelIfNotExist (
384 415
385 // Avatar? 416 // Avatar?
386 if (result.avatar) { 417 if (result.avatar) {
387 const avatar = await AvatarModel.create({ 418 const avatar = await ActorImageModel.create({
388 filename: result.avatar.name, 419 filename: result.avatar.name,
389 fileUrl: result.avatar.fileUrl, 420 fileUrl: result.avatar.fileUrl,
390 onDisk: false 421 width: result.avatar.width,
422 height: result.avatar.height,
423 onDisk: false,
424 type: ActorImageType.AVATAR
391 }, { transaction: t }) 425 }, { transaction: t })
392 426
393 actor.avatarId = avatar.id 427 actor.avatarId = avatar.id
394 } 428 }
395 429
430 // Banner?
431 if (result.banner) {
432 const banner = await ActorImageModel.create({
433 filename: result.banner.name,
434 fileUrl: result.banner.fileUrl,
435 width: result.banner.width,
436 height: result.banner.height,
437 onDisk: false,
438 type: ActorImageType.BANNER
439 }, { transaction: t })
440
441 actor.bannerId = banner.id
442 }
443
396 // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists 444 // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists
397 // (which could be false in a retried query) 445 // (which could be false in a retried query)
398 const [ actorCreated, created ] = await ActorModel.findOrCreate<MActorFullActor>({ 446 const [ actorCreated, created ] = await ActorModel.findOrCreate<MActorFullActor>({
@@ -436,39 +484,37 @@ function saveActorAndServerAndModelIfNotExist (
436 } 484 }
437} 485}
438 486
487type ImageResult = {
488 name: string
489 fileUrl: string
490 height: number
491 width: number
492}
493
439type FetchRemoteActorResult = { 494type FetchRemoteActorResult = {
440 actor: MActor 495 actor: MActor
441 name: string 496 name: string
442 summary: string 497 summary: string
443 support?: string 498 support?: string
444 playlists?: string 499 playlists?: string
445 avatar?: { 500 avatar?: ImageResult
446 name: string 501 banner?: ImageResult
447 fileUrl: string
448 }
449 attributedTo: ActivityPubAttributedTo[] 502 attributedTo: ActivityPubAttributedTo[]
450} 503}
451async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: number, result: FetchRemoteActorResult }> { 504async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: number, result: FetchRemoteActorResult }> {
452 const options = {
453 uri: actorUrl,
454 method: 'GET',
455 json: true,
456 activityPub: true
457 }
458
459 logger.info('Fetching remote actor %s.', actorUrl) 505 logger.info('Fetching remote actor %s.', actorUrl)
460 506
461 const requestResult = await doRequest<ActivityPubActor>(options) 507 const requestResult = await doJSONRequest<ActivityPubActor>(actorUrl, { activityPub: true })
462 const actorJSON = requestResult.body 508 const actorJSON = requestResult.body
463 509
464 if (sanitizeAndCheckActorObject(actorJSON) === false) { 510 if (sanitizeAndCheckActorObject(actorJSON) === false) {
465 logger.debug('Remote actor JSON is not valid.', { actorJSON }) 511 logger.debug('Remote actor JSON is not valid.', { actorJSON })
466 return { result: undefined, statusCode: requestResult.response.statusCode } 512 return { result: undefined, statusCode: requestResult.statusCode }
467 } 513 }
468 514
469 if (checkUrlsSameHost(actorJSON.id, actorUrl) !== true) { 515 if (checkUrlsSameHost(actorJSON.id, actorUrl) !== true) {
470 logger.warn('Actor url %s has not the same host than its AP id %s', actorUrl, actorJSON.id) 516 logger.warn('Actor url %s has not the same host than its AP id %s', actorUrl, actorJSON.id)
471 return { result: undefined, statusCode: requestResult.response.statusCode } 517 return { result: undefined, statusCode: requestResult.statusCode }
472 } 518 }
473 519
474 const followersCount = await fetchActorTotalItems(actorJSON.followers) 520 const followersCount = await fetchActorTotalItems(actorJSON.followers)
@@ -492,15 +538,17 @@ async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: numbe
492 : null 538 : null
493 }) 539 })
494 540
495 const avatarInfo = await getAvatarInfoIfExists(actorJSON) 541 const avatarInfo = getImageInfoIfExists(actorJSON, ActorImageType.AVATAR)
542 const bannerInfo = getImageInfoIfExists(actorJSON, ActorImageType.BANNER)
496 543
497 const name = actorJSON.name || actorJSON.preferredUsername 544 const name = actorJSON.name || actorJSON.preferredUsername
498 return { 545 return {
499 statusCode: requestResult.response.statusCode, 546 statusCode: requestResult.statusCode,
500 result: { 547 result: {
501 actor, 548 actor,
502 name, 549 name,
503 avatar: avatarInfo, 550 avatar: avatarInfo,
551 banner: bannerInfo,
504 summary: actorJSON.summary, 552 summary: actorJSON.summary,
505 support: actorJSON.support, 553 support: actorJSON.support,
506 playlists: actorJSON.playlists, 554 playlists: actorJSON.playlists,
diff --git a/server/lib/activitypub/crawl.ts b/server/lib/activitypub/crawl.ts
index 1ed105bbe..278abf7de 100644
--- a/server/lib/activitypub/crawl.ts
+++ b/server/lib/activitypub/crawl.ts
@@ -1,27 +1,26 @@
1import { ACTIVITY_PUB, REQUEST_TIMEOUT, WEBSERVER } from '../../initializers/constants'
2import { doRequest } from '../../helpers/requests'
3import { logger } from '../../helpers/logger'
4import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
5import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub'
6import { URL } from 'url' 2import { URL } from 'url'
3import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub'
4import { logger } from '../../helpers/logger'
5import { doJSONRequest } from '../../helpers/requests'
6import { ACTIVITY_PUB, REQUEST_TIMEOUT, WEBSERVER } from '../../initializers/constants'
7 7
8type HandlerFunction<T> = (items: T[]) => (Promise<any> | Bluebird<any>) 8type HandlerFunction<T> = (items: T[]) => (Promise<any> | Bluebird<any>)
9type CleanerFunction = (startedDate: Date) => (Promise<any> | Bluebird<any>) 9type CleanerFunction = (startedDate: Date) => (Promise<any> | Bluebird<any>)
10 10
11async function crawlCollectionPage <T> (uri: string, handler: HandlerFunction<T>, cleaner?: CleanerFunction) { 11async function crawlCollectionPage <T> (argUrl: string, handler: HandlerFunction<T>, cleaner?: CleanerFunction) {
12 logger.info('Crawling ActivityPub data on %s.', uri) 12 let url = argUrl
13
14 logger.info('Crawling ActivityPub data on %s.', url)
13 15
14 const options = { 16 const options = {
15 method: 'GET',
16 uri,
17 json: true,
18 activityPub: true, 17 activityPub: true,
19 timeout: REQUEST_TIMEOUT 18 timeout: REQUEST_TIMEOUT
20 } 19 }
21 20
22 const startDate = new Date() 21 const startDate = new Date()
23 22
24 const response = await doRequest<ActivityPubOrderedCollection<T>>(options) 23 const response = await doJSONRequest<ActivityPubOrderedCollection<T>>(url, options)
25 const firstBody = response.body 24 const firstBody = response.body
26 25
27 const limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT 26 const limit = ACTIVITY_PUB.FETCH_PAGE_LIMIT
@@ -35,9 +34,9 @@ async function crawlCollectionPage <T> (uri: string, handler: HandlerFunction<T>
35 const remoteHost = new URL(nextLink).host 34 const remoteHost = new URL(nextLink).host
36 if (remoteHost === WEBSERVER.HOST) continue 35 if (remoteHost === WEBSERVER.HOST) continue
37 36
38 options.uri = nextLink 37 url = nextLink
39 38
40 const res = await doRequest<ActivityPubOrderedCollection<T>>(options) 39 const res = await doJSONRequest<ActivityPubOrderedCollection<T>>(url, options)
41 body = res.body 40 body = res.body
42 } else { 41 } else {
43 // nextLink is already the object we want 42 // nextLink is already the object we want
@@ -49,7 +48,7 @@ async function crawlCollectionPage <T> (uri: string, handler: HandlerFunction<T>
49 48
50 if (Array.isArray(body.orderedItems)) { 49 if (Array.isArray(body.orderedItems)) {
51 const items = body.orderedItems 50 const items = body.orderedItems
52 logger.info('Processing %i ActivityPub items for %s.', items.length, options.uri) 51 logger.info('Processing %i ActivityPub items for %s.', items.length, url)
53 52
54 await handler(items) 53 await handler(items)
55 } 54 }
diff --git a/server/lib/activitypub/playlist.ts b/server/lib/activitypub/playlist.ts
index d5a3ef7c8..7166c68a6 100644
--- a/server/lib/activitypub/playlist.ts
+++ b/server/lib/activitypub/playlist.ts
@@ -1,24 +1,24 @@
1import * as Bluebird from 'bluebird'
2import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
3import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object'
1import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' 4import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object'
2import { crawlCollectionPage } from './crawl' 5import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
3import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' 6import { checkUrlsSameHost } from '../../helpers/activitypub'
7import { isPlaylistElementObjectValid, isPlaylistObjectValid } from '../../helpers/custom-validators/activitypub/playlist'
4import { isArray } from '../../helpers/custom-validators/misc' 8import { isArray } from '../../helpers/custom-validators/misc'
5import { getOrCreateActorAndServerAndModel } from './actor'
6import { logger } from '../../helpers/logger' 9import { logger } from '../../helpers/logger'
10import { doJSONRequest, PeerTubeRequestError } from '../../helpers/requests'
11import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
12import { sequelizeTypescript } from '../../initializers/database'
7import { VideoPlaylistModel } from '../../models/video/video-playlist' 13import { VideoPlaylistModel } from '../../models/video/video-playlist'
8import { doRequest } from '../../helpers/requests'
9import { checkUrlsSameHost } from '../../helpers/activitypub'
10import * as Bluebird from 'bluebird'
11import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object'
12import { getOrCreateVideoAndAccountAndChannel } from './videos'
13import { isPlaylistElementObjectValid, isPlaylistObjectValid } from '../../helpers/custom-validators/activitypub/playlist'
14import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element' 14import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element'
15import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
16import { sequelizeTypescript } from '../../initializers/database'
17import { createPlaylistMiniatureFromUrl } from '../thumbnail'
18import { FilteredModelAttributes } from '../../types/sequelize'
19import { MAccountDefault, MAccountId, MVideoId } from '../../types/models' 15import { MAccountDefault, MAccountId, MVideoId } from '../../types/models'
20import { MVideoPlaylist, MVideoPlaylistId, MVideoPlaylistOwner } from '../../types/models/video/video-playlist' 16import { MVideoPlaylist, MVideoPlaylistId, MVideoPlaylistOwner } from '../../types/models/video/video-playlist'
21import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' 17import { FilteredModelAttributes } from '../../types/sequelize'
18import { createPlaylistMiniatureFromUrl } from '../thumbnail'
19import { getOrCreateActorAndServerAndModel } from './actor'
20import { crawlCollectionPage } from './crawl'
21import { getOrCreateVideoAndAccountAndChannel } from './videos'
22 22
23function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: MAccountId, to: string[]) { 23function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: MAccountId, to: string[]) {
24 const privacy = to.includes(ACTIVITY_PUB.PUBLIC) 24 const privacy = to.includes(ACTIVITY_PUB.PUBLIC)
@@ -56,11 +56,7 @@ async function createAccountPlaylists (playlistUrls: string[], account: MAccount
56 if (exists === true) return 56 if (exists === true) return
57 57
58 // Fetch url 58 // Fetch url
59 const { body } = await doRequest<PlaylistObject>({ 59 const { body } = await doJSONRequest<PlaylistObject>(playlistUrl, { activityPub: true })
60 uri: playlistUrl,
61 json: true,
62 activityPub: true
63 })
64 60
65 if (!isPlaylistObjectValid(body)) { 61 if (!isPlaylistObjectValid(body)) {
66 throw new Error(`Invalid playlist object when fetch account playlists: ${JSON.stringify(body)}`) 62 throw new Error(`Invalid playlist object when fetch account playlists: ${JSON.stringify(body)}`)
@@ -120,13 +116,7 @@ async function refreshVideoPlaylistIfNeeded (videoPlaylist: MVideoPlaylistOwner)
120 if (!videoPlaylist.isOutdated()) return videoPlaylist 116 if (!videoPlaylist.isOutdated()) return videoPlaylist
121 117
122 try { 118 try {
123 const { statusCode, playlistObject } = await fetchRemoteVideoPlaylist(videoPlaylist.url) 119 const { playlistObject } = await fetchRemoteVideoPlaylist(videoPlaylist.url)
124 if (statusCode === HttpStatusCode.NOT_FOUND_404) {
125 logger.info('Cannot refresh remote video playlist %s: it does not exist anymore. Deleting it.', videoPlaylist.url)
126
127 await videoPlaylist.destroy()
128 return undefined
129 }
130 120
131 if (playlistObject === undefined) { 121 if (playlistObject === undefined) {
132 logger.warn('Cannot refresh remote playlist %s: invalid body.', videoPlaylist.url) 122 logger.warn('Cannot refresh remote playlist %s: invalid body.', videoPlaylist.url)
@@ -140,6 +130,13 @@ async function refreshVideoPlaylistIfNeeded (videoPlaylist: MVideoPlaylistOwner)
140 130
141 return videoPlaylist 131 return videoPlaylist
142 } catch (err) { 132 } catch (err) {
133 if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) {
134 logger.info('Cannot refresh remote video playlist %s: it does not exist anymore. Deleting it.', videoPlaylist.url)
135
136 await videoPlaylist.destroy()
137 return undefined
138 }
139
143 logger.warn('Cannot refresh video playlist %s.', videoPlaylist.url, { err }) 140 logger.warn('Cannot refresh video playlist %s.', videoPlaylist.url, { err })
144 141
145 await videoPlaylist.setAsRefreshed() 142 await videoPlaylist.setAsRefreshed()
@@ -164,12 +161,7 @@ async function resetVideoPlaylistElements (elementUrls: string[], playlist: MVid
164 161
165 await Bluebird.map(elementUrls, async elementUrl => { 162 await Bluebird.map(elementUrls, async elementUrl => {
166 try { 163 try {
167 // Fetch url 164 const { body } = await doJSONRequest<PlaylistElementObject>(elementUrl, { activityPub: true })
168 const { body } = await doRequest<PlaylistElementObject>({
169 uri: elementUrl,
170 json: true,
171 activityPub: true
172 })
173 165
174 if (!isPlaylistElementObjectValid(body)) throw new Error(`Invalid body in video get playlist element ${elementUrl}`) 166 if (!isPlaylistElementObjectValid(body)) throw new Error(`Invalid body in video get playlist element ${elementUrl}`)
175 167
@@ -199,21 +191,14 @@ async function resetVideoPlaylistElements (elementUrls: string[], playlist: MVid
199} 191}
200 192
201async function fetchRemoteVideoPlaylist (playlistUrl: string): Promise<{ statusCode: number, playlistObject: PlaylistObject }> { 193async function fetchRemoteVideoPlaylist (playlistUrl: string): Promise<{ statusCode: number, playlistObject: PlaylistObject }> {
202 const options = {
203 uri: playlistUrl,
204 method: 'GET',
205 json: true,
206 activityPub: true
207 }
208
209 logger.info('Fetching remote playlist %s.', playlistUrl) 194 logger.info('Fetching remote playlist %s.', playlistUrl)
210 195
211 const { response, body } = await doRequest<any>(options) 196 const { body, statusCode } = await doJSONRequest<any>(playlistUrl, { activityPub: true })
212 197
213 if (isPlaylistObjectValid(body) === false || checkUrlsSameHost(body.id, playlistUrl) !== true) { 198 if (isPlaylistObjectValid(body) === false || checkUrlsSameHost(body.id, playlistUrl) !== true) {
214 logger.debug('Remote video playlist JSON is not valid.', { body }) 199 logger.debug('Remote video playlist JSON is not valid.', { body })
215 return { statusCode: response.statusCode, playlistObject: undefined } 200 return { statusCode, playlistObject: undefined }
216 } 201 }
217 202
218 return { statusCode: response.statusCode, playlistObject: body } 203 return { statusCode, playlistObject: body }
219} 204}
diff --git a/server/lib/activitypub/process/process-delete.ts b/server/lib/activitypub/process/process-delete.ts
index a86def936..070ee0f1d 100644
--- a/server/lib/activitypub/process/process-delete.ts
+++ b/server/lib/activitypub/process/process-delete.ts
@@ -7,7 +7,7 @@ import { VideoModel } from '../../../models/video/video'
7import { VideoCommentModel } from '../../../models/video/video-comment' 7import { VideoCommentModel } from '../../../models/video/video-comment'
8import { VideoPlaylistModel } from '../../../models/video/video-playlist' 8import { VideoPlaylistModel } from '../../../models/video/video-playlist'
9import { APProcessorOptions } from '../../../types/activitypub-processor.model' 9import { APProcessorOptions } from '../../../types/activitypub-processor.model'
10import { MAccountActor, MActor, MActorSignature, MChannelActor, MChannelActorAccountActor, MCommentOwnerVideo } from '../../../types/models' 10import { MAccountActor, MActor, MActorSignature, MChannelActor, MCommentOwnerVideo } from '../../../types/models'
11import { markCommentAsDeleted } from '../../video-comment' 11import { markCommentAsDeleted } from '../../video-comment'
12import { forwardVideoRelatedActivity } from '../send/utils' 12import { forwardVideoRelatedActivity } from '../send/utils'
13 13
@@ -30,9 +30,7 @@ async function processDeleteActivity (options: APProcessorOptions<ActivityDelete
30 } else if (byActorFull.type === 'Group') { 30 } else if (byActorFull.type === 'Group') {
31 if (!byActorFull.VideoChannel) throw new Error('Actor ' + byActorFull.url + ' is a group but we cannot find it in database.') 31 if (!byActorFull.VideoChannel) throw new Error('Actor ' + byActorFull.url + ' is a group but we cannot find it in database.')
32 32
33 const channelToDelete = byActorFull.VideoChannel as MChannelActorAccountActor 33 const channelToDelete = Object.assign({}, byActorFull.VideoChannel, { Actor: byActorFull })
34 channelToDelete.Actor = byActorFull
35
36 return retryTransactionWrapper(processDeleteVideoChannel, channelToDelete) 34 return retryTransactionWrapper(processDeleteVideoChannel, channelToDelete)
37 } 35 }
38 } 36 }
diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts
index 849f70b94..6df9b93b2 100644
--- a/server/lib/activitypub/process/process-update.ts
+++ b/server/lib/activitypub/process/process-update.ts
@@ -6,7 +6,7 @@ import { sequelizeTypescript } from '../../../initializers/database'
6import { AccountModel } from '../../../models/account/account' 6import { AccountModel } from '../../../models/account/account'
7import { ActorModel } from '../../../models/activitypub/actor' 7import { ActorModel } from '../../../models/activitypub/actor'
8import { VideoChannelModel } from '../../../models/video/video-channel' 8import { VideoChannelModel } from '../../../models/video/video-channel'
9import { getAvatarInfoIfExists, updateActorAvatarInstance, updateActorInstance } from '../actor' 9import { getImageInfoIfExists, updateActorImageInstance, updateActorInstance } from '../actor'
10import { getOrCreateVideoAndAccountAndChannel, getOrCreateVideoChannelFromVideoObject, updateVideoFromAP } from '../videos' 10import { getOrCreateVideoAndAccountAndChannel, getOrCreateVideoChannelFromVideoObject, updateVideoFromAP } from '../videos'
11import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos' 11import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos'
12import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file' 12import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file'
@@ -17,6 +17,7 @@ import { createOrUpdateVideoPlaylist } from '../playlist'
17import { APProcessorOptions } from '../../../types/activitypub-processor.model' 17import { APProcessorOptions } from '../../../types/activitypub-processor.model'
18import { MActorSignature, MAccountIdActor } from '../../../types/models' 18import { MActorSignature, MAccountIdActor } from '../../../types/models'
19import { isRedundancyAccepted } from '@server/lib/redundancy' 19import { isRedundancyAccepted } from '@server/lib/redundancy'
20import { ActorImageType } from '@shared/models'
20 21
21async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) { 22async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) {
22 const { activity, byActor } = options 23 const { activity, byActor } = options
@@ -119,7 +120,8 @@ async function processUpdateActor (actor: ActorModel, activity: ActivityUpdate)
119 let accountOrChannelFieldsSave: object 120 let accountOrChannelFieldsSave: object
120 121
121 // Fetch icon? 122 // Fetch icon?
122 const avatarInfo = await getAvatarInfoIfExists(actorAttributesToUpdate) 123 const avatarInfo = getImageInfoIfExists(actorAttributesToUpdate, ActorImageType.AVATAR)
124 const bannerInfo = getImageInfoIfExists(actorAttributesToUpdate, ActorImageType.BANNER)
123 125
124 try { 126 try {
125 await sequelizeTypescript.transaction(async t => { 127 await sequelizeTypescript.transaction(async t => {
@@ -132,11 +134,8 @@ async function processUpdateActor (actor: ActorModel, activity: ActivityUpdate)
132 134
133 await updateActorInstance(actor, actorAttributesToUpdate) 135 await updateActorInstance(actor, actorAttributesToUpdate)
134 136
135 if (avatarInfo !== undefined) { 137 await updateActorImageInstance(actor, ActorImageType.AVATAR, avatarInfo, t)
136 const avatarOptions = Object.assign({}, avatarInfo, { onDisk: false }) 138 await updateActorImageInstance(actor, ActorImageType.BANNER, bannerInfo, t)
137
138 await updateActorAvatarInstance(actor, avatarOptions, t)
139 }
140 139
141 await actor.save({ transaction: t }) 140 await actor.save({ transaction: t })
142 141
diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts
index 9fb218224..baded642a 100644
--- a/server/lib/activitypub/send/send-create.ts
+++ b/server/lib/activitypub/send/send-create.ts
@@ -4,7 +4,7 @@ import { VideoPrivacy } from '../../../../shared/models/videos'
4import { VideoCommentModel } from '../../../models/video/video-comment' 4import { VideoCommentModel } from '../../../models/video/video-comment'
5import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils' 5import { broadcastToActors, broadcastToFollowers, sendVideoRelatedActivity, unicastTo } from './utils'
6import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience' 6import { audiencify, getActorsInvolvedInVideo, getAudience, getAudienceFromFollowersOf, getVideoCommentAudience } from '../audience'
7import { logger } from '../../../helpers/logger' 7import { logger, loggerTagsFactory } from '../../../helpers/logger'
8import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' 8import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
9import { 9import {
10 MActorLight, 10 MActorLight,
@@ -18,10 +18,12 @@ import {
18import { getServerActor } from '@server/models/application/application' 18import { getServerActor } from '@server/models/application/application'
19import { ContextType } from '@shared/models/activitypub/context' 19import { ContextType } from '@shared/models/activitypub/context'
20 20
21const lTags = loggerTagsFactory('ap', 'create')
22
21async function sendCreateVideo (video: MVideoAP, t: Transaction) { 23async function sendCreateVideo (video: MVideoAP, t: Transaction) {
22 if (!video.hasPrivacyForFederation()) return undefined 24 if (!video.hasPrivacyForFederation()) return undefined
23 25
24 logger.info('Creating job to send video creation of %s.', video.url) 26 logger.info('Creating job to send video creation of %s.', video.url, lTags(video.uuid))
25 27
26 const byActor = video.VideoChannel.Account.Actor 28 const byActor = video.VideoChannel.Account.Actor
27 const videoObject = video.toActivityPubObject() 29 const videoObject = video.toActivityPubObject()
@@ -37,7 +39,7 @@ async function sendCreateCacheFile (
37 video: MVideoAccountLight, 39 video: MVideoAccountLight,
38 fileRedundancy: MVideoRedundancyStreamingPlaylistVideo | MVideoRedundancyFileVideo 40 fileRedundancy: MVideoRedundancyStreamingPlaylistVideo | MVideoRedundancyFileVideo
39) { 41) {
40 logger.info('Creating job to send file cache of %s.', fileRedundancy.url) 42 logger.info('Creating job to send file cache of %s.', fileRedundancy.url, lTags(video.uuid))
41 43
42 return sendVideoRelatedCreateActivity({ 44 return sendVideoRelatedCreateActivity({
43 byActor, 45 byActor,
@@ -51,7 +53,7 @@ async function sendCreateCacheFile (
51async function sendCreateVideoPlaylist (playlist: MVideoPlaylistFull, t: Transaction) { 53async function sendCreateVideoPlaylist (playlist: MVideoPlaylistFull, t: Transaction) {
52 if (playlist.privacy === VideoPlaylistPrivacy.PRIVATE) return undefined 54 if (playlist.privacy === VideoPlaylistPrivacy.PRIVATE) return undefined
53 55
54 logger.info('Creating job to send create video playlist of %s.', playlist.url) 56 logger.info('Creating job to send create video playlist of %s.', playlist.url, lTags(playlist.uuid))
55 57
56 const byActor = playlist.OwnerAccount.Actor 58 const byActor = playlist.OwnerAccount.Actor
57 const audience = getAudience(byActor, playlist.privacy === VideoPlaylistPrivacy.PUBLIC) 59 const audience = getAudience(byActor, playlist.privacy === VideoPlaylistPrivacy.PUBLIC)
diff --git a/server/lib/activitypub/share.ts b/server/lib/activitypub/share.ts
index 1f8a8f3c4..c22fa0893 100644
--- a/server/lib/activitypub/share.ts
+++ b/server/lib/activitypub/share.ts
@@ -1,15 +1,17 @@
1import * as Bluebird from 'bluebird'
1import { Transaction } from 'sequelize' 2import { Transaction } from 'sequelize'
3import { getServerActor } from '@server/models/application/application'
4import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
5import { logger, loggerTagsFactory } from '../../helpers/logger'
6import { doJSONRequest } from '../../helpers/requests'
7import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
2import { VideoShareModel } from '../../models/video/video-share' 8import { VideoShareModel } from '../../models/video/video-share'
9import { MChannelActorLight, MVideo, MVideoAccountLight, MVideoId } from '../../types/models/video'
10import { getOrCreateActorAndServerAndModel } from './actor'
3import { sendUndoAnnounce, sendVideoAnnounce } from './send' 11import { sendUndoAnnounce, sendVideoAnnounce } from './send'
4import { getLocalVideoAnnounceActivityPubUrl } from './url' 12import { getLocalVideoAnnounceActivityPubUrl } from './url'
5import * as Bluebird from 'bluebird' 13
6import { doRequest } from '../../helpers/requests' 14const lTags = loggerTagsFactory('share')
7import { getOrCreateActorAndServerAndModel } from './actor'
8import { logger } from '../../helpers/logger'
9import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
10import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
11import { MChannelActorLight, MVideo, MVideoAccountLight, MVideoId } from '../../types/models/video'
12import { getServerActor } from '@server/models/application/application'
13 15
14async function shareVideoByServerAndChannel (video: MVideoAccountLight, t: Transaction) { 16async function shareVideoByServerAndChannel (video: MVideoAccountLight, t: Transaction) {
15 if (!video.hasPrivacyForFederation()) return undefined 17 if (!video.hasPrivacyForFederation()) return undefined
@@ -25,7 +27,10 @@ async function changeVideoChannelShare (
25 oldVideoChannel: MChannelActorLight, 27 oldVideoChannel: MChannelActorLight,
26 t: Transaction 28 t: Transaction
27) { 29) {
28 logger.info('Updating video channel of video %s: %s -> %s.', video.uuid, oldVideoChannel.name, video.VideoChannel.name) 30 logger.info(
31 'Updating video channel of video %s: %s -> %s.', video.uuid, oldVideoChannel.name, video.VideoChannel.name,
32 lTags(video.uuid)
33 )
29 34
30 await undoShareByVideoChannel(video, oldVideoChannel, t) 35 await undoShareByVideoChannel(video, oldVideoChannel, t)
31 36
@@ -35,12 +40,7 @@ async function changeVideoChannelShare (
35async function addVideoShares (shareUrls: string[], video: MVideoId) { 40async function addVideoShares (shareUrls: string[], video: MVideoId) {
36 await Bluebird.map(shareUrls, async shareUrl => { 41 await Bluebird.map(shareUrls, async shareUrl => {
37 try { 42 try {
38 // Fetch url 43 const { body } = await doJSONRequest<any>(shareUrl, { activityPub: true })
39 const { body } = await doRequest<any>({
40 uri: shareUrl,
41 json: true,
42 activityPub: true
43 })
44 if (!body || !body.actor) throw new Error('Body or body actor is invalid') 44 if (!body || !body.actor) throw new Error('Body or body actor is invalid')
45 45
46 const actorUrl = getAPId(body.actor) 46 const actorUrl = getAPId(body.actor)
diff --git a/server/lib/activitypub/video-comments.ts b/server/lib/activitypub/video-comments.ts
index d025ed7f1..e23e0c0e7 100644
--- a/server/lib/activitypub/video-comments.ts
+++ b/server/lib/activitypub/video-comments.ts
@@ -1,13 +1,13 @@
1import * as Bluebird from 'bluebird'
2import { checkUrlsSameHost } from '../../helpers/activitypub'
1import { sanitizeAndCheckVideoCommentObject } from '../../helpers/custom-validators/activitypub/video-comments' 3import { sanitizeAndCheckVideoCommentObject } from '../../helpers/custom-validators/activitypub/video-comments'
2import { logger } from '../../helpers/logger' 4import { logger } from '../../helpers/logger'
3import { doRequest } from '../../helpers/requests' 5import { doJSONRequest } from '../../helpers/requests'
4import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' 6import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
5import { VideoCommentModel } from '../../models/video/video-comment' 7import { VideoCommentModel } from '../../models/video/video-comment'
8import { MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../types/models/video'
6import { getOrCreateActorAndServerAndModel } from './actor' 9import { getOrCreateActorAndServerAndModel } from './actor'
7import { getOrCreateVideoAndAccountAndChannel } from './videos' 10import { getOrCreateVideoAndAccountAndChannel } from './videos'
8import * as Bluebird from 'bluebird'
9import { checkUrlsSameHost } from '../../helpers/activitypub'
10import { MCommentOwner, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../types/models/video'
11 11
12type ResolveThreadParams = { 12type ResolveThreadParams = {
13 url: string 13 url: string
@@ -18,8 +18,12 @@ type ResolveThreadParams = {
18type ResolveThreadResult = Promise<{ video: MVideoAccountLightBlacklistAllFiles, comment: MCommentOwnerVideo, commentCreated: boolean }> 18type ResolveThreadResult = Promise<{ video: MVideoAccountLightBlacklistAllFiles, comment: MCommentOwnerVideo, commentCreated: boolean }>
19 19
20async function addVideoComments (commentUrls: string[]) { 20async function addVideoComments (commentUrls: string[]) {
21 return Bluebird.map(commentUrls, commentUrl => { 21 return Bluebird.map(commentUrls, async commentUrl => {
22 return resolveThread({ url: commentUrl, isVideo: false }) 22 try {
23 await resolveThread({ url: commentUrl, isVideo: false })
24 } catch (err) {
25 logger.warn('Cannot resolve thread %s.', commentUrl, { err })
26 }
23 }, { concurrency: CRAWL_REQUEST_CONCURRENCY }) 27 }, { concurrency: CRAWL_REQUEST_CONCURRENCY })
24} 28}
25 29
@@ -126,11 +130,7 @@ async function resolveRemoteParentComment (params: ResolveThreadParams) {
126 throw new Error('Recursion limit reached when resolving a thread') 130 throw new Error('Recursion limit reached when resolving a thread')
127 } 131 }
128 132
129 const { body } = await doRequest<any>({ 133 const { body } = await doJSONRequest<any>(url, { activityPub: true })
130 uri: url,
131 json: true,
132 activityPub: true
133 })
134 134
135 if (sanitizeAndCheckVideoCommentObject(body) === false) { 135 if (sanitizeAndCheckVideoCommentObject(body) === false) {
136 throw new Error(`Remote video comment JSON ${url} is not valid:` + JSON.stringify(body)) 136 throw new Error(`Remote video comment JSON ${url} is not valid:` + JSON.stringify(body))
diff --git a/server/lib/activitypub/video-rates.ts b/server/lib/activitypub/video-rates.ts
index e246b1313..f40c07fea 100644
--- a/server/lib/activitypub/video-rates.ts
+++ b/server/lib/activitypub/video-rates.ts
@@ -1,26 +1,22 @@
1import * as Bluebird from 'bluebird'
1import { Transaction } from 'sequelize' 2import { Transaction } from 'sequelize'
2import { sendLike, sendUndoDislike, sendUndoLike } from './send' 3import { doJSONRequest } from '@server/helpers/requests'
3import { VideoRateType } from '../../../shared/models/videos' 4import { VideoRateType } from '../../../shared/models/videos'
4import * as Bluebird from 'bluebird' 5import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
5import { getOrCreateActorAndServerAndModel } from './actor'
6import { AccountVideoRateModel } from '../../models/account/account-video-rate'
7import { logger } from '../../helpers/logger' 6import { logger } from '../../helpers/logger'
8import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' 7import { CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants'
9import { doRequest } from '../../helpers/requests' 8import { AccountVideoRateModel } from '../../models/account/account-video-rate'
10import { checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
11import { getVideoDislikeActivityPubUrlByLocalActor, getVideoLikeActivityPubUrlByLocalActor } from './url'
12import { sendDislike } from './send/send-dislike'
13import { MAccountActor, MActorUrl, MVideo, MVideoAccountLight, MVideoId } from '../../types/models' 9import { MAccountActor, MActorUrl, MVideo, MVideoAccountLight, MVideoId } from '../../types/models'
10import { getOrCreateActorAndServerAndModel } from './actor'
11import { sendLike, sendUndoDislike, sendUndoLike } from './send'
12import { sendDislike } from './send/send-dislike'
13import { getVideoDislikeActivityPubUrlByLocalActor, getVideoLikeActivityPubUrlByLocalActor } from './url'
14 14
15async function createRates (ratesUrl: string[], video: MVideo, rate: VideoRateType) { 15async function createRates (ratesUrl: string[], video: MVideo, rate: VideoRateType) {
16 await Bluebird.map(ratesUrl, async rateUrl => { 16 await Bluebird.map(ratesUrl, async rateUrl => {
17 try { 17 try {
18 // Fetch url 18 // Fetch url
19 const { body } = await doRequest<any>({ 19 const { body } = await doJSONRequest<any>(rateUrl, { activityPub: true })
20 uri: rateUrl,
21 json: true,
22 activityPub: true
23 })
24 if (!body || !body.actor) throw new Error('Body or body actor is invalid') 20 if (!body || !body.actor) throw new Error('Body or body actor is invalid')
25 21
26 const actorUrl = getAPId(body.actor) 22 const actorUrl = getAPId(body.actor)
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index c02578aad..9014791c0 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -1,8 +1,7 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { maxBy, minBy } from 'lodash' 2import { maxBy, minBy } from 'lodash'
3import * as magnetUtil from 'magnet-uri' 3import * as magnetUtil from 'magnet-uri'
4import { basename, join } from 'path' 4import { basename } from 'path'
5import * as request from 'request'
6import { Transaction } from 'sequelize/types' 5import { Transaction } from 'sequelize/types'
7import { TrackerModel } from '@server/models/server/tracker' 6import { TrackerModel } from '@server/models/server/tracker'
8import { VideoLiveModel } from '@server/models/video/video-live' 7import { VideoLiveModel } from '@server/models/video/video-live'
@@ -17,7 +16,7 @@ import {
17 ActivityUrlObject, 16 ActivityUrlObject,
18 ActivityVideoUrlObject 17 ActivityVideoUrlObject
19} from '../../../shared/index' 18} from '../../../shared/index'
20import { ActivityIconObject, ActivityTrackerUrlObject, VideoObject } from '../../../shared/models/activitypub/objects' 19import { ActivityTrackerUrlObject, VideoObject } from '../../../shared/models/activitypub/objects'
21import { VideoPrivacy } from '../../../shared/models/videos' 20import { VideoPrivacy } from '../../../shared/models/videos'
22import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' 21import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
23import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' 22import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
@@ -31,11 +30,10 @@ import { isArray } from '../../helpers/custom-validators/misc'
31import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' 30import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
32import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' 31import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils'
33import { logger } from '../../helpers/logger' 32import { logger } from '../../helpers/logger'
34import { doRequest } from '../../helpers/requests' 33import { doJSONRequest, PeerTubeRequestError } from '../../helpers/requests'
35import { fetchVideoByUrl, getExtFromMimetype, VideoFetchByUrlType } from '../../helpers/video' 34import { fetchVideoByUrl, getExtFromMimetype, VideoFetchByUrlType } from '../../helpers/video'
36import { 35import {
37 ACTIVITY_PUB, 36 ACTIVITY_PUB,
38 LAZY_STATIC_PATHS,
39 MIMETYPES, 37 MIMETYPES,
40 P2P_MEDIA_LOADER_PEER_VERSION, 38 P2P_MEDIA_LOADER_PEER_VERSION,
41 PREVIEWS_SIZE, 39 PREVIEWS_SIZE,
@@ -115,36 +113,26 @@ async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVid
115 } 113 }
116} 114}
117 115
118async function fetchRemoteVideo (videoUrl: string): Promise<{ response: request.RequestResponse, videoObject: VideoObject }> { 116async function fetchRemoteVideo (videoUrl: string): Promise<{ statusCode: number, videoObject: VideoObject }> {
119 const options = {
120 uri: videoUrl,
121 method: 'GET',
122 json: true,
123 activityPub: true
124 }
125
126 logger.info('Fetching remote video %s.', videoUrl) 117 logger.info('Fetching remote video %s.', videoUrl)
127 118
128 const { response, body } = await doRequest<any>(options) 119 const { statusCode, body } = await doJSONRequest<any>(videoUrl, { activityPub: true })
129 120
130 if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) { 121 if (sanitizeAndCheckVideoTorrentObject(body) === false || checkUrlsSameHost(body.id, videoUrl) !== true) {
131 logger.debug('Remote video JSON is not valid.', { body }) 122 logger.debug('Remote video JSON is not valid.', { body })
132 return { response, videoObject: undefined } 123 return { statusCode, videoObject: undefined }
133 } 124 }
134 125
135 return { response, videoObject: body } 126 return { statusCode, videoObject: body }
136} 127}
137 128
138async function fetchRemoteVideoDescription (video: MVideoAccountLight) { 129async function fetchRemoteVideoDescription (video: MVideoAccountLight) {
139 const host = video.VideoChannel.Account.Actor.Server.host 130 const host = video.VideoChannel.Account.Actor.Server.host
140 const path = video.getDescriptionAPIPath() 131 const path = video.getDescriptionAPIPath()
141 const options = { 132 const url = REMOTE_SCHEME.HTTP + '://' + host + path
142 uri: REMOTE_SCHEME.HTTP + '://' + host + path,
143 json: true
144 }
145 133
146 const { body } = await doRequest<any>(options) 134 const { body } = await doJSONRequest<any>(url)
147 return body.description ? body.description : '' 135 return body.description || ''
148} 136}
149 137
150function getOrCreateVideoChannelFromVideoObject (videoObject: VideoObject) { 138function getOrCreateVideoChannelFromVideoObject (videoObject: VideoObject) {
@@ -378,13 +366,13 @@ async function updateVideoFromAP (options: {
378 366
379 if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel, t) 367 if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel, t)
380 368
381 if (videoUpdated.getPreview()) { 369 const previewIcon = getPreviewFromIcons(videoObject)
382 const previewUrl = getPreviewUrl(getPreviewFromIcons(videoObject), video) 370 if (videoUpdated.getPreview() && previewIcon) {
383 const previewModel = createPlaceholderThumbnail({ 371 const previewModel = createPlaceholderThumbnail({
384 fileUrl: previewUrl, 372 fileUrl: previewIcon.url,
385 video, 373 video,
386 type: ThumbnailType.PREVIEW, 374 type: ThumbnailType.PREVIEW,
387 size: PREVIEWS_SIZE 375 size: previewIcon
388 }) 376 })
389 await videoUpdated.addAndSaveThumbnail(previewModel, t) 377 await videoUpdated.addAndSaveThumbnail(previewModel, t)
390 } 378 }
@@ -534,14 +522,7 @@ async function refreshVideoIfNeeded (options: {
534 : await VideoModel.loadByUrlAndPopulateAccount(options.video.url) 522 : await VideoModel.loadByUrlAndPopulateAccount(options.video.url)
535 523
536 try { 524 try {
537 const { response, videoObject } = await fetchRemoteVideo(video.url) 525 const { videoObject } = await fetchRemoteVideo(video.url)
538 if (response.statusCode === HttpStatusCode.NOT_FOUND_404) {
539 logger.info('Cannot refresh remote video %s: video does not exist anymore. Deleting it.', video.url)
540
541 // Video does not exist anymore
542 await video.destroy()
543 return undefined
544 }
545 526
546 if (videoObject === undefined) { 527 if (videoObject === undefined) {
547 logger.warn('Cannot refresh remote video %s: invalid body.', video.url) 528 logger.warn('Cannot refresh remote video %s: invalid body.', video.url)
@@ -565,6 +546,14 @@ async function refreshVideoIfNeeded (options: {
565 546
566 return video 547 return video
567 } catch (err) { 548 } catch (err) {
549 if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) {
550 logger.info('Cannot refresh remote video %s: video does not exist anymore. Deleting it.', video.url)
551
552 // Video does not exist anymore
553 await video.destroy()
554 return undefined
555 }
556
568 logger.warn('Cannot refresh video %s.', options.video.url, { err }) 557 logger.warn('Cannot refresh video %s.', options.video.url, { err })
569 558
570 ActorFollowScoreCache.Instance.addBadServerId(video.VideoChannel.Actor.serverId) 559 ActorFollowScoreCache.Instance.addBadServerId(video.VideoChannel.Actor.serverId)
@@ -638,15 +627,17 @@ async function createVideo (videoObject: VideoObject, channel: MChannelAccountLi
638 627
639 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t) 628 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
640 629
641 const previewUrl = getPreviewUrl(getPreviewFromIcons(videoObject), videoCreated) 630 const previewIcon = getPreviewFromIcons(videoObject)
642 const previewModel = createPlaceholderThumbnail({ 631 if (previewIcon) {
643 fileUrl: previewUrl, 632 const previewModel = createPlaceholderThumbnail({
644 video: videoCreated, 633 fileUrl: previewIcon.url,
645 type: ThumbnailType.PREVIEW, 634 video: videoCreated,
646 size: PREVIEWS_SIZE 635 type: ThumbnailType.PREVIEW,
647 }) 636 size: previewIcon
637 })
648 638
649 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(previewModel, t) 639 await videoCreated.addAndSaveThumbnail(previewModel, t)
640 }
650 641
651 // Process files 642 // Process files
652 const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject.url) 643 const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject.url)
@@ -906,12 +897,6 @@ function getPreviewFromIcons (videoObject: VideoObject) {
906 return maxBy(validIcons, 'width') 897 return maxBy(validIcons, 'width')
907} 898}
908 899
909function getPreviewUrl (previewIcon: ActivityIconObject, video: MVideoWithHost) {
910 return previewIcon
911 ? previewIcon.url
912 : buildRemoteVideoBaseUrl(video, join(LAZY_STATIC_PATHS.PREVIEWS, video.generatePreviewName()))
913}
914
915function getTrackerUrls (object: VideoObject, video: MVideoWithHost) { 900function getTrackerUrls (object: VideoObject, video: MVideoWithHost) {
916 let wsFound = false 901 let wsFound = false
917 902
diff --git a/server/lib/actor-image.ts b/server/lib/actor-image.ts
new file mode 100644
index 000000000..f271f0b5b
--- /dev/null
+++ b/server/lib/actor-image.ts
@@ -0,0 +1,97 @@
1import 'multer'
2import { queue } from 'async'
3import * as LRUCache from 'lru-cache'
4import { extname, join } from 'path'
5import { v4 as uuidv4 } from 'uuid'
6import { ActorImageType } from '@shared/models'
7import { retryTransactionWrapper } from '../helpers/database-utils'
8import { processImage } from '../helpers/image-utils'
9import { downloadImage } from '../helpers/requests'
10import { CONFIG } from '../initializers/config'
11import { ACTOR_IMAGES_SIZE, LRU_CACHE, QUEUE_CONCURRENCY } from '../initializers/constants'
12import { sequelizeTypescript } from '../initializers/database'
13import { MAccountDefault, MChannelDefault } from '../types/models'
14import { deleteActorImageInstance, updateActorImageInstance } from './activitypub/actor'
15import { sendUpdateActor } from './activitypub/send'
16
17async function updateLocalActorImageFile (
18 accountOrChannel: MAccountDefault | MChannelDefault,
19 imagePhysicalFile: Express.Multer.File,
20 type: ActorImageType
21) {
22 const imageSize = type === ActorImageType.AVATAR
23 ? ACTOR_IMAGES_SIZE.AVATARS
24 : ACTOR_IMAGES_SIZE.BANNERS
25
26 const extension = extname(imagePhysicalFile.filename)
27
28 const imageName = uuidv4() + extension
29 const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, imageName)
30 await processImage(imagePhysicalFile.path, destination, imageSize)
31
32 return retryTransactionWrapper(() => {
33 return sequelizeTypescript.transaction(async t => {
34 const actorImageInfo = {
35 name: imageName,
36 fileUrl: null,
37 height: imageSize.height,
38 width: imageSize.width,
39 onDisk: true
40 }
41
42 const updatedActor = await updateActorImageInstance(accountOrChannel.Actor, type, actorImageInfo, t)
43 await updatedActor.save({ transaction: t })
44
45 await sendUpdateActor(accountOrChannel, t)
46
47 return type === ActorImageType.AVATAR
48 ? updatedActor.Avatar
49 : updatedActor.Banner
50 })
51 })
52}
53
54async function deleteLocalActorImageFile (accountOrChannel: MAccountDefault | MChannelDefault, type: ActorImageType) {
55 return retryTransactionWrapper(() => {
56 return sequelizeTypescript.transaction(async t => {
57 const updatedActor = await deleteActorImageInstance(accountOrChannel.Actor, type, t)
58 await updatedActor.save({ transaction: t })
59
60 await sendUpdateActor(accountOrChannel, t)
61
62 return updatedActor.Avatar
63 })
64 })
65}
66
67type DownloadImageQueueTask = { fileUrl: string, filename: string, type: ActorImageType }
68
69const downloadImageQueue = queue<DownloadImageQueueTask, Error>((task, cb) => {
70 const size = task.type === ActorImageType.AVATAR
71 ? ACTOR_IMAGES_SIZE.AVATARS
72 : ACTOR_IMAGES_SIZE.BANNERS
73
74 downloadImage(task.fileUrl, CONFIG.STORAGE.ACTOR_IMAGES, task.filename, size)
75 .then(() => cb())
76 .catch(err => cb(err))
77}, QUEUE_CONCURRENCY.ACTOR_PROCESS_IMAGE)
78
79function pushActorImageProcessInQueue (task: DownloadImageQueueTask) {
80 return new Promise<void>((res, rej) => {
81 downloadImageQueue.push(task, err => {
82 if (err) return rej(err)
83
84 return res()
85 })
86 })
87}
88
89// Unsafe so could returns paths that does not exist anymore
90const actorImagePathUnsafeCache = new LRUCache<string, string>({ max: LRU_CACHE.ACTOR_IMAGE_STATIC.MAX_SIZE })
91
92export {
93 actorImagePathUnsafeCache,
94 updateLocalActorImageFile,
95 deleteLocalActorImageFile,
96 pushActorImageProcessInQueue
97}
diff --git a/server/lib/auth.ts b/server/lib/auth/external-auth.ts
index dbd421a7b..80f5064b6 100644
--- a/server/lib/auth.ts
+++ b/server/lib/auth/external-auth.ts
@@ -1,28 +1,16 @@
1
1import { isUserDisplayNameValid, isUserRoleValid, isUserUsernameValid } from '@server/helpers/custom-validators/users' 2import { isUserDisplayNameValid, isUserRoleValid, isUserUsernameValid } from '@server/helpers/custom-validators/users'
2import { logger } from '@server/helpers/logger' 3import { logger } from '@server/helpers/logger'
3import { generateRandomString } from '@server/helpers/utils' 4import { generateRandomString } from '@server/helpers/utils'
4import { OAUTH_LIFETIME, PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } from '@server/initializers/constants' 5import { PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } from '@server/initializers/constants'
5import { revokeToken } from '@server/lib/oauth-model'
6import { PluginManager } from '@server/lib/plugins/plugin-manager' 6import { PluginManager } from '@server/lib/plugins/plugin-manager'
7import { OAuthTokenModel } from '@server/models/oauth/oauth-token' 7import { OAuthTokenModel } from '@server/models/oauth/oauth-token'
8import { UserRole } from '@shared/models'
9import { 8import {
10 RegisterServerAuthenticatedResult, 9 RegisterServerAuthenticatedResult,
11 RegisterServerAuthPassOptions, 10 RegisterServerAuthPassOptions,
12 RegisterServerExternalAuthenticatedResult 11 RegisterServerExternalAuthenticatedResult
13} from '@server/types/plugins/register-server-auth.model' 12} from '@server/types/plugins/register-server-auth.model'
14import * as express from 'express' 13import { UserRole } from '@shared/models'
15import * as OAuthServer from 'express-oauth-server'
16import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
17
18const oAuthServer = new OAuthServer({
19 useErrorHandler: true,
20 accessTokenLifetime: OAUTH_LIFETIME.ACCESS_TOKEN,
21 refreshTokenLifetime: OAUTH_LIFETIME.REFRESH_TOKEN,
22 allowExtendedTokenAttributes: true,
23 continueMiddleware: true,
24 model: require('./oauth-model')
25})
26 14
27// Token is the key, expiration date is the value 15// Token is the key, expiration date is the value
28const authBypassTokens = new Map<string, { 16const authBypassTokens = new Map<string, {
@@ -37,42 +25,6 @@ const authBypassTokens = new Map<string, {
37 npmName: string 25 npmName: string
38}>() 26}>()
39 27
40async function handleLogin (req: express.Request, res: express.Response, next: express.NextFunction) {
41 const grantType = req.body.grant_type
42
43 if (grantType === 'password') {
44 if (req.body.externalAuthToken) proxifyExternalAuthBypass(req, res)
45 else await proxifyPasswordGrant(req, res)
46 } else if (grantType === 'refresh_token') {
47 await proxifyRefreshGrant(req, res)
48 }
49
50 return forwardTokenReq(req, res, next)
51}
52
53async function handleTokenRevocation (req: express.Request, res: express.Response) {
54 const token = res.locals.oauth.token
55
56 res.locals.explicitLogout = true
57 const result = await revokeToken(token)
58
59 // FIXME: uncomment when https://github.com/oauthjs/node-oauth2-server/pull/289 is released
60 // oAuthServer.revoke(req, res, err => {
61 // if (err) {
62 // logger.warn('Error in revoke token handler.', { err })
63 //
64 // return res.status(err.status)
65 // .json({
66 // error: err.message,
67 // code: err.name
68 // })
69 // .end()
70 // }
71 // })
72
73 return res.json(result)
74}
75
76async function onExternalUserAuthenticated (options: { 28async function onExternalUserAuthenticated (options: {
77 npmName: string 29 npmName: string
78 authName: string 30 authName: string
@@ -107,7 +59,7 @@ async function onExternalUserAuthenticated (options: {
107 authName 59 authName
108 }) 60 })
109 61
110 // Cleanup 62 // Cleanup expired tokens
111 const now = new Date() 63 const now = new Date()
112 for (const [ key, value ] of authBypassTokens) { 64 for (const [ key, value ] of authBypassTokens) {
113 if (value.expires.getTime() < now.getTime()) { 65 if (value.expires.getTime() < now.getTime()) {
@@ -118,37 +70,15 @@ async function onExternalUserAuthenticated (options: {
118 res.redirect(`/login?externalAuthToken=${bypassToken}&username=${user.username}`) 70 res.redirect(`/login?externalAuthToken=${bypassToken}&username=${user.username}`)
119} 71}
120 72
121// --------------------------------------------------------------------------- 73async function getAuthNameFromRefreshGrant (refreshToken?: string) {
122 74 if (!refreshToken) return undefined
123export { oAuthServer, handleLogin, onExternalUserAuthenticated, handleTokenRevocation }
124
125// ---------------------------------------------------------------------------
126
127function forwardTokenReq (req: express.Request, res: express.Response, next?: express.NextFunction) {
128 return oAuthServer.token()(req, res, err => {
129 if (err) {
130 logger.warn('Login error.', { err })
131
132 return res.status(err.status)
133 .json({
134 error: err.message,
135 code: err.name
136 })
137 }
138
139 if (next) return next()
140 })
141}
142
143async function proxifyRefreshGrant (req: express.Request, res: express.Response) {
144 const refreshToken = req.body.refresh_token
145 if (!refreshToken) return
146 75
147 const tokenModel = await OAuthTokenModel.loadByRefreshToken(refreshToken) 76 const tokenModel = await OAuthTokenModel.loadByRefreshToken(refreshToken)
148 if (tokenModel?.authName) res.locals.refreshTokenAuthName = tokenModel.authName 77
78 return tokenModel?.authName
149} 79}
150 80
151async function proxifyPasswordGrant (req: express.Request, res: express.Response) { 81async function getBypassFromPasswordGrant (username: string, password: string) {
152 const plugins = PluginManager.Instance.getIdAndPassAuths() 82 const plugins = PluginManager.Instance.getIdAndPassAuths()
153 const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = [] 83 const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = []
154 84
@@ -174,8 +104,8 @@ async function proxifyPasswordGrant (req: express.Request, res: express.Response
174 }) 104 })
175 105
176 const loginOptions = { 106 const loginOptions = {
177 id: req.body.username, 107 id: username,
178 password: req.body.password 108 password
179 } 109 }
180 110
181 for (const pluginAuth of pluginAuths) { 111 for (const pluginAuth of pluginAuths) {
@@ -199,49 +129,41 @@ async function proxifyPasswordGrant (req: express.Request, res: express.Response
199 authName, npmName, loginOptions.id 129 authName, npmName, loginOptions.id
200 ) 130 )
201 131
202 res.locals.bypassLogin = { 132 return {
203 bypass: true, 133 bypass: true,
204 pluginName: pluginAuth.npmName, 134 pluginName: pluginAuth.npmName,
205 authName: authOptions.authName, 135 authName: authOptions.authName,
206 user: buildUserResult(loginResult) 136 user: buildUserResult(loginResult)
207 } 137 }
208
209 return
210 } catch (err) { 138 } catch (err) {
211 logger.error('Error in auth method %s of plugin %s', authOptions.authName, pluginAuth.npmName, { err }) 139 logger.error('Error in auth method %s of plugin %s', authOptions.authName, pluginAuth.npmName, { err })
212 } 140 }
213 } 141 }
142
143 return undefined
214} 144}
215 145
216function proxifyExternalAuthBypass (req: express.Request, res: express.Response) { 146function getBypassFromExternalAuth (username: string, externalAuthToken: string) {
217 const obj = authBypassTokens.get(req.body.externalAuthToken) 147 const obj = authBypassTokens.get(externalAuthToken)
218 if (!obj) { 148 if (!obj) throw new Error('Cannot authenticate user with unknown bypass token')
219 logger.error('Cannot authenticate user with unknown bypass token')
220 return res.sendStatus(HttpStatusCode.BAD_REQUEST_400)
221 }
222 149
223 const { expires, user, authName, npmName } = obj 150 const { expires, user, authName, npmName } = obj
224 151
225 const now = new Date() 152 const now = new Date()
226 if (now.getTime() > expires.getTime()) { 153 if (now.getTime() > expires.getTime()) {
227 logger.error('Cannot authenticate user with an expired external auth token') 154 throw new Error('Cannot authenticate user with an expired external auth token')
228 return res.sendStatus(HttpStatusCode.BAD_REQUEST_400)
229 } 155 }
230 156
231 if (user.username !== req.body.username) { 157 if (user.username !== username) {
232 logger.error('Cannot authenticate user %s with invalid username %s.', req.body.username) 158 throw new Error(`Cannot authenticate user ${user.username} with invalid username ${username}`)
233 return res.sendStatus(HttpStatusCode.BAD_REQUEST_400)
234 } 159 }
235 160
236 // Bypass oauth library validation
237 req.body.password = 'fake'
238
239 logger.info( 161 logger.info(
240 'Auth success with external auth method %s of plugin %s for %s.', 162 'Auth success with external auth method %s of plugin %s for %s.',
241 authName, npmName, user.email 163 authName, npmName, user.email
242 ) 164 )
243 165
244 res.locals.bypassLogin = { 166 return {
245 bypass: true, 167 bypass: true,
246 pluginName: npmName, 168 pluginName: npmName,
247 authName: authName, 169 authName: authName,
@@ -286,3 +208,12 @@ function buildUserResult (pluginResult: RegisterServerAuthenticatedResult) {
286 displayName: pluginResult.displayName || pluginResult.username 208 displayName: pluginResult.displayName || pluginResult.username
287 } 209 }
288} 210}
211
212// ---------------------------------------------------------------------------
213
214export {
215 onExternalUserAuthenticated,
216 getBypassFromExternalAuth,
217 getAuthNameFromRefreshGrant,
218 getBypassFromPasswordGrant
219}
diff --git a/server/lib/oauth-model.ts b/server/lib/auth/oauth-model.ts
index a2c53a2c9..b9c69eb2d 100644
--- a/server/lib/oauth-model.ts
+++ b/server/lib/auth/oauth-model.ts
@@ -1,49 +1,36 @@
1import * as express from 'express' 1import * as express from 'express'
2import * as LRUCache from 'lru-cache'
3import { AccessDeniedError } from 'oauth2-server' 2import { AccessDeniedError } from 'oauth2-server'
4import { Transaction } from 'sequelize'
5import { PluginManager } from '@server/lib/plugins/plugin-manager' 3import { PluginManager } from '@server/lib/plugins/plugin-manager'
6import { ActorModel } from '@server/models/activitypub/actor' 4import { ActorModel } from '@server/models/activitypub/actor'
5import { MOAuthClient } from '@server/types/models'
7import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' 6import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token'
8import { MUser } from '@server/types/models/user/user' 7import { MUser } from '@server/types/models/user/user'
9import { UserAdminFlag } from '@shared/models/users/user-flag.model' 8import { UserAdminFlag } from '@shared/models/users/user-flag.model'
10import { UserRole } from '@shared/models/users/user-role' 9import { UserRole } from '@shared/models/users/user-role'
11import { logger } from '../helpers/logger' 10import { logger } from '../../helpers/logger'
12import { CONFIG } from '../initializers/config' 11import { CONFIG } from '../../initializers/config'
13import { LRU_CACHE } from '../initializers/constants' 12import { UserModel } from '../../models/account/user'
14import { UserModel } from '../models/account/user' 13import { OAuthClientModel } from '../../models/oauth/oauth-client'
15import { OAuthClientModel } from '../models/oauth/oauth-client' 14import { OAuthTokenModel } from '../../models/oauth/oauth-token'
16import { OAuthTokenModel } from '../models/oauth/oauth-token' 15import { createUserAccountAndChannelAndPlaylist } from '../user'
17import { createUserAccountAndChannelAndPlaylist } from './user' 16import { TokensCache } from './tokens-cache'
18 17
19type TokenInfo = { accessToken: string, refreshToken: string, accessTokenExpiresAt: Date, refreshTokenExpiresAt: Date } 18type TokenInfo = {
20 19 accessToken: string
21const accessTokenCache = new LRUCache<string, MOAuthTokenUser>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE }) 20 refreshToken: string
22const userHavingToken = new LRUCache<number, string>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE }) 21 accessTokenExpiresAt: Date
23 22 refreshTokenExpiresAt: Date
24// ---------------------------------------------------------------------------
25
26function deleteUserToken (userId: number, t?: Transaction) {
27 clearCacheByUserId(userId)
28
29 return OAuthTokenModel.deleteUserToken(userId, t)
30} 23}
31 24
32function clearCacheByUserId (userId: number) { 25export type BypassLogin = {
33 const token = userHavingToken.get(userId) 26 bypass: boolean
34 27 pluginName: string
35 if (token !== undefined) { 28 authName?: string
36 accessTokenCache.del(token) 29 user: {
37 userHavingToken.del(userId) 30 username: string
38 } 31 email: string
39} 32 displayName: string
40 33 role: UserRole
41function clearCacheByToken (token: string) {
42 const tokenModel = accessTokenCache.get(token)
43
44 if (tokenModel !== undefined) {
45 userHavingToken.del(tokenModel.userId)
46 accessTokenCache.del(token)
47 } 34 }
48} 35}
49 36
@@ -54,15 +41,12 @@ async function getAccessToken (bearerToken: string) {
54 41
55 let tokenModel: MOAuthTokenUser 42 let tokenModel: MOAuthTokenUser
56 43
57 if (accessTokenCache.has(bearerToken)) { 44 if (TokensCache.Instance.hasToken(bearerToken)) {
58 tokenModel = accessTokenCache.get(bearerToken) 45 tokenModel = TokensCache.Instance.getByToken(bearerToken)
59 } else { 46 } else {
60 tokenModel = await OAuthTokenModel.getByTokenAndPopulateUser(bearerToken) 47 tokenModel = await OAuthTokenModel.getByTokenAndPopulateUser(bearerToken)
61 48
62 if (tokenModel) { 49 if (tokenModel) TokensCache.Instance.setToken(tokenModel)
63 accessTokenCache.set(bearerToken, tokenModel)
64 userHavingToken.set(tokenModel.userId, tokenModel.accessToken)
65 }
66 } 50 }
67 51
68 if (!tokenModel) return undefined 52 if (!tokenModel) return undefined
@@ -99,16 +83,13 @@ async function getRefreshToken (refreshToken: string) {
99 return tokenInfo 83 return tokenInfo
100} 84}
101 85
102async function getUser (usernameOrEmail?: string, password?: string) { 86async function getUser (usernameOrEmail?: string, password?: string, bypassLogin?: BypassLogin) {
103 const res: express.Response = this.request.res
104
105 // Special treatment coming from a plugin 87 // Special treatment coming from a plugin
106 if (res.locals.bypassLogin && res.locals.bypassLogin.bypass === true) { 88 if (bypassLogin && bypassLogin.bypass === true) {
107 const obj = res.locals.bypassLogin 89 logger.info('Bypassing oauth login by plugin %s.', bypassLogin.pluginName)
108 logger.info('Bypassing oauth login by plugin %s.', obj.pluginName)
109 90
110 let user = await UserModel.loadByEmail(obj.user.email) 91 let user = await UserModel.loadByEmail(bypassLogin.user.email)
111 if (!user) user = await createUserFromExternal(obj.pluginName, obj.user) 92 if (!user) user = await createUserFromExternal(bypassLogin.pluginName, bypassLogin.user)
112 93
113 // Cannot create a user 94 // Cannot create a user
114 if (!user) throw new AccessDeniedError('Cannot create such user: an actor with that name already exists.') 95 if (!user) throw new AccessDeniedError('Cannot create such user: an actor with that name already exists.')
@@ -117,7 +98,7 @@ async function getUser (usernameOrEmail?: string, password?: string) {
117 // Then we just go through a regular login process 98 // Then we just go through a regular login process
118 if (user.pluginAuth !== null) { 99 if (user.pluginAuth !== null) {
119 // This user does not belong to this plugin, skip it 100 // This user does not belong to this plugin, skip it
120 if (user.pluginAuth !== obj.pluginName) return null 101 if (user.pluginAuth !== bypassLogin.pluginName) return null
121 102
122 checkUserValidityOrThrow(user) 103 checkUserValidityOrThrow(user)
123 104
@@ -143,18 +124,25 @@ async function getUser (usernameOrEmail?: string, password?: string) {
143 return user 124 return user
144} 125}
145 126
146async function revokeToken (tokenInfo: { refreshToken: string }): Promise<{ success: boolean, redirectUrl?: string }> { 127async function revokeToken (
147 const res: express.Response = this.request.res 128 tokenInfo: { refreshToken: string },
129 options: {
130 req?: express.Request
131 explicitLogout?: boolean
132 } = {}
133): Promise<{ success: boolean, redirectUrl?: string }> {
134 const { req, explicitLogout } = options
135
148 const token = await OAuthTokenModel.getByRefreshTokenAndPopulateUser(tokenInfo.refreshToken) 136 const token = await OAuthTokenModel.getByRefreshTokenAndPopulateUser(tokenInfo.refreshToken)
149 137
150 if (token) { 138 if (token) {
151 let redirectUrl: string 139 let redirectUrl: string
152 140
153 if (res.locals.explicitLogout === true && token.User.pluginAuth && token.authName) { 141 if (explicitLogout === true && token.User.pluginAuth && token.authName) {
154 redirectUrl = await PluginManager.Instance.onLogout(token.User.pluginAuth, token.authName, token.User, this.request) 142 redirectUrl = await PluginManager.Instance.onLogout(token.User.pluginAuth, token.authName, token.User, req)
155 } 143 }
156 144
157 clearCacheByToken(token.accessToken) 145 TokensCache.Instance.clearCacheByToken(token.accessToken)
158 146
159 token.destroy() 147 token.destroy()
160 .catch(err => logger.error('Cannot destroy token when revoking token.', { err })) 148 .catch(err => logger.error('Cannot destroy token when revoking token.', { err }))
@@ -165,14 +153,22 @@ async function revokeToken (tokenInfo: { refreshToken: string }): Promise<{ succ
165 return { success: false } 153 return { success: false }
166} 154}
167 155
168async function saveToken (token: TokenInfo, client: OAuthClientModel, user: UserModel) { 156async function saveToken (
169 const res: express.Response = this.request.res 157 token: TokenInfo,
170 158 client: MOAuthClient,
159 user: MUser,
160 options: {
161 refreshTokenAuthName?: string
162 bypassLogin?: BypassLogin
163 } = {}
164) {
165 const { refreshTokenAuthName, bypassLogin } = options
171 let authName: string = null 166 let authName: string = null
172 if (res.locals.bypassLogin?.bypass === true) { 167
173 authName = res.locals.bypassLogin.authName 168 if (bypassLogin?.bypass === true) {
174 } else if (res.locals.refreshTokenAuthName) { 169 authName = bypassLogin.authName
175 authName = res.locals.refreshTokenAuthName 170 } else if (refreshTokenAuthName) {
171 authName = refreshTokenAuthName
176 } 172 }
177 173
178 logger.debug('Saving token ' + token.accessToken + ' for client ' + client.id + ' and user ' + user.id + '.') 174 logger.debug('Saving token ' + token.accessToken + ' for client ' + client.id + ' and user ' + user.id + '.')
@@ -199,17 +195,12 @@ async function saveToken (token: TokenInfo, client: OAuthClientModel, user: User
199 refreshTokenExpiresAt: tokenCreated.refreshTokenExpiresAt, 195 refreshTokenExpiresAt: tokenCreated.refreshTokenExpiresAt,
200 client, 196 client,
201 user, 197 user,
202 refresh_token_expires_in: Math.floor((tokenCreated.refreshTokenExpiresAt.getTime() - new Date().getTime()) / 1000) 198 accessTokenExpiresIn: buildExpiresIn(tokenCreated.accessTokenExpiresAt),
199 refreshTokenExpiresIn: buildExpiresIn(tokenCreated.refreshTokenExpiresAt)
203 } 200 }
204} 201}
205 202
206// ---------------------------------------------------------------------------
207
208// See https://github.com/oauthjs/node-oauth2-server/wiki/Model-specification for the model specifications
209export { 203export {
210 deleteUserToken,
211 clearCacheByUserId,
212 clearCacheByToken,
213 getAccessToken, 204 getAccessToken,
214 getClient, 205 getClient,
215 getRefreshToken, 206 getRefreshToken,
@@ -218,6 +209,8 @@ export {
218 saveToken 209 saveToken
219} 210}
220 211
212// ---------------------------------------------------------------------------
213
221async function createUserFromExternal (pluginAuth: string, options: { 214async function createUserFromExternal (pluginAuth: string, options: {
222 username: string 215 username: string
223 email: string 216 email: string
@@ -252,3 +245,7 @@ async function createUserFromExternal (pluginAuth: string, options: {
252function checkUserValidityOrThrow (user: MUser) { 245function checkUserValidityOrThrow (user: MUser) {
253 if (user.blocked) throw new AccessDeniedError('User is blocked.') 246 if (user.blocked) throw new AccessDeniedError('User is blocked.')
254} 247}
248
249function buildExpiresIn (expiresAt: Date) {
250 return Math.floor((expiresAt.getTime() - new Date().getTime()) / 1000)
251}
diff --git a/server/lib/auth/oauth.ts b/server/lib/auth/oauth.ts
new file mode 100644
index 000000000..5b6130d56
--- /dev/null
+++ b/server/lib/auth/oauth.ts
@@ -0,0 +1,180 @@
1import * as express from 'express'
2import {
3 InvalidClientError,
4 InvalidGrantError,
5 InvalidRequestError,
6 Request,
7 Response,
8 UnauthorizedClientError,
9 UnsupportedGrantTypeError
10} from 'oauth2-server'
11import { randomBytesPromise, sha1 } from '@server/helpers/core-utils'
12import { MOAuthClient } from '@server/types/models'
13import { OAUTH_LIFETIME } from '../../initializers/constants'
14import { BypassLogin, getClient, getRefreshToken, getUser, revokeToken, saveToken } from './oauth-model'
15
16/**
17 *
18 * Reimplement some functions of OAuth2Server to inject external auth methods
19 *
20 */
21
22const oAuthServer = new (require('oauth2-server'))({
23 accessTokenLifetime: OAUTH_LIFETIME.ACCESS_TOKEN,
24 refreshTokenLifetime: OAUTH_LIFETIME.REFRESH_TOKEN,
25
26 // See https://github.com/oauthjs/node-oauth2-server/wiki/Model-specification for the model specifications
27 model: require('./oauth-model')
28})
29
30// ---------------------------------------------------------------------------
31
32async function handleOAuthToken (req: express.Request, options: { refreshTokenAuthName?: string, bypassLogin?: BypassLogin }) {
33 const request = new Request(req)
34 const { refreshTokenAuthName, bypassLogin } = options
35
36 if (request.method !== 'POST') {
37 throw new InvalidRequestError('Invalid request: method must be POST')
38 }
39
40 if (!request.is([ 'application/x-www-form-urlencoded' ])) {
41 throw new InvalidRequestError('Invalid request: content must be application/x-www-form-urlencoded')
42 }
43
44 const clientId = request.body.client_id
45 const clientSecret = request.body.client_secret
46
47 if (!clientId || !clientSecret) {
48 throw new InvalidClientError('Invalid client: cannot retrieve client credentials')
49 }
50
51 const client = await getClient(clientId, clientSecret)
52 if (!client) {
53 throw new InvalidClientError('Invalid client: client is invalid')
54 }
55
56 const grantType = request.body.grant_type
57 if (!grantType) {
58 throw new InvalidRequestError('Missing parameter: `grant_type`')
59 }
60
61 if (![ 'password', 'refresh_token' ].includes(grantType)) {
62 throw new UnsupportedGrantTypeError('Unsupported grant type: `grant_type` is invalid')
63 }
64
65 if (!client.grants.includes(grantType)) {
66 throw new UnauthorizedClientError('Unauthorized client: `grant_type` is invalid')
67 }
68
69 if (grantType === 'password') {
70 return handlePasswordGrant({
71 request,
72 client,
73 bypassLogin
74 })
75 }
76
77 return handleRefreshGrant({
78 request,
79 client,
80 refreshTokenAuthName
81 })
82}
83
84async function handleOAuthAuthenticate (
85 req: express.Request,
86 res: express.Response,
87 authenticateInQuery = false
88) {
89 const options = authenticateInQuery
90 ? { allowBearerTokensInQueryString: true }
91 : {}
92
93 return oAuthServer.authenticate(new Request(req), new Response(res), options)
94}
95
96export {
97 handleOAuthToken,
98 handleOAuthAuthenticate
99}
100
101// ---------------------------------------------------------------------------
102
103async function handlePasswordGrant (options: {
104 request: Request
105 client: MOAuthClient
106 bypassLogin?: BypassLogin
107}) {
108 const { request, client, bypassLogin } = options
109
110 if (!request.body.username) {
111 throw new InvalidRequestError('Missing parameter: `username`')
112 }
113
114 if (!bypassLogin && !request.body.password) {
115 throw new InvalidRequestError('Missing parameter: `password`')
116 }
117
118 const user = await getUser(request.body.username, request.body.password, bypassLogin)
119 if (!user) throw new InvalidGrantError('Invalid grant: user credentials are invalid')
120
121 const token = await buildToken()
122
123 return saveToken(token, client, user, { bypassLogin })
124}
125
126async function handleRefreshGrant (options: {
127 request: Request
128 client: MOAuthClient
129 refreshTokenAuthName: string
130}) {
131 const { request, client, refreshTokenAuthName } = options
132
133 if (!request.body.refresh_token) {
134 throw new InvalidRequestError('Missing parameter: `refresh_token`')
135 }
136
137 const refreshToken = await getRefreshToken(request.body.refresh_token)
138
139 if (!refreshToken) {
140 throw new InvalidGrantError('Invalid grant: refresh token is invalid')
141 }
142
143 if (refreshToken.client.id !== client.id) {
144 throw new InvalidGrantError('Invalid grant: refresh token is invalid')
145 }
146
147 if (refreshToken.refreshTokenExpiresAt && refreshToken.refreshTokenExpiresAt < new Date()) {
148 throw new InvalidGrantError('Invalid grant: refresh token has expired')
149 }
150
151 await revokeToken({ refreshToken: refreshToken.refreshToken })
152
153 const token = await buildToken()
154
155 return saveToken(token, client, refreshToken.user, { refreshTokenAuthName })
156}
157
158function generateRandomToken () {
159 return randomBytesPromise(256)
160 .then(buffer => sha1(buffer))
161}
162
163function getTokenExpiresAt (type: 'access' | 'refresh') {
164 const lifetime = type === 'access'
165 ? OAUTH_LIFETIME.ACCESS_TOKEN
166 : OAUTH_LIFETIME.REFRESH_TOKEN
167
168 return new Date(Date.now() + lifetime * 1000)
169}
170
171async function buildToken () {
172 const [ accessToken, refreshToken ] = await Promise.all([ generateRandomToken(), generateRandomToken() ])
173
174 return {
175 accessToken,
176 refreshToken,
177 accessTokenExpiresAt: getTokenExpiresAt('access'),
178 refreshTokenExpiresAt: getTokenExpiresAt('refresh')
179 }
180}
diff --git a/server/lib/auth/tokens-cache.ts b/server/lib/auth/tokens-cache.ts
new file mode 100644
index 000000000..b027ce69a
--- /dev/null
+++ b/server/lib/auth/tokens-cache.ts
@@ -0,0 +1,52 @@
1import * as LRUCache from 'lru-cache'
2import { MOAuthTokenUser } from '@server/types/models'
3import { LRU_CACHE } from '../../initializers/constants'
4
5export class TokensCache {
6
7 private static instance: TokensCache
8
9 private readonly accessTokenCache = new LRUCache<string, MOAuthTokenUser>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE })
10 private readonly userHavingToken = new LRUCache<number, string>({ max: LRU_CACHE.USER_TOKENS.MAX_SIZE })
11
12 private constructor () { }
13
14 static get Instance () {
15 return this.instance || (this.instance = new this())
16 }
17
18 hasToken (token: string) {
19 return this.accessTokenCache.has(token)
20 }
21
22 getByToken (token: string) {
23 return this.accessTokenCache.get(token)
24 }
25
26 setToken (token: MOAuthTokenUser) {
27 this.accessTokenCache.set(token.accessToken, token)
28 this.userHavingToken.set(token.userId, token.accessToken)
29 }
30
31 deleteUserToken (userId: number) {
32 this.clearCacheByUserId(userId)
33 }
34
35 clearCacheByUserId (userId: number) {
36 const token = this.userHavingToken.get(userId)
37
38 if (token !== undefined) {
39 this.accessTokenCache.del(token)
40 this.userHavingToken.del(userId)
41 }
42 }
43
44 clearCacheByToken (token: string) {
45 const tokenModel = this.accessTokenCache.get(token)
46
47 if (tokenModel !== undefined) {
48 this.userHavingToken.del(tokenModel.userId)
49 this.accessTokenCache.del(token)
50 }
51 }
52}
diff --git a/server/lib/avatar.ts b/server/lib/avatar.ts
deleted file mode 100644
index 86f1e7bdb..000000000
--- a/server/lib/avatar.ts
+++ /dev/null
@@ -1,85 +0,0 @@
1import 'multer'
2import { sendUpdateActor } from './activitypub/send'
3import { AVATARS_SIZE, LRU_CACHE, QUEUE_CONCURRENCY } from '../initializers/constants'
4import { updateActorAvatarInstance, deleteActorAvatarInstance } from './activitypub/actor'
5import { processImage } from '../helpers/image-utils'
6import { extname, join } from 'path'
7import { retryTransactionWrapper } from '../helpers/database-utils'
8import { v4 as uuidv4 } from 'uuid'
9import { CONFIG } from '../initializers/config'
10import { sequelizeTypescript } from '../initializers/database'
11import * as LRUCache from 'lru-cache'
12import { queue } from 'async'
13import { downloadImage } from '../helpers/requests'
14import { MAccountDefault, MChannelDefault } from '../types/models'
15
16async function updateLocalActorAvatarFile (
17 accountOrChannel: MAccountDefault | MChannelDefault,
18 avatarPhysicalFile: Express.Multer.File
19) {
20 const extension = extname(avatarPhysicalFile.filename)
21
22 const avatarName = uuidv4() + extension
23 const destination = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)
24 await processImage(avatarPhysicalFile.path, destination, AVATARS_SIZE)
25
26 return retryTransactionWrapper(() => {
27 return sequelizeTypescript.transaction(async t => {
28 const avatarInfo = {
29 name: avatarName,
30 fileUrl: null,
31 onDisk: true
32 }
33
34 const updatedActor = await updateActorAvatarInstance(accountOrChannel.Actor, avatarInfo, t)
35 await updatedActor.save({ transaction: t })
36
37 await sendUpdateActor(accountOrChannel, t)
38
39 return updatedActor.Avatar
40 })
41 })
42}
43
44async function deleteLocalActorAvatarFile (
45 accountOrChannel: MAccountDefault | MChannelDefault
46) {
47 return retryTransactionWrapper(() => {
48 return sequelizeTypescript.transaction(async t => {
49 const updatedActor = await deleteActorAvatarInstance(accountOrChannel.Actor, t)
50 await updatedActor.save({ transaction: t })
51
52 await sendUpdateActor(accountOrChannel, t)
53
54 return updatedActor.Avatar
55 })
56 })
57}
58
59type DownloadImageQueueTask = { fileUrl: string, filename: string }
60
61const downloadImageQueue = queue<DownloadImageQueueTask, Error>((task, cb) => {
62 downloadImage(task.fileUrl, CONFIG.STORAGE.AVATARS_DIR, task.filename, AVATARS_SIZE)
63 .then(() => cb())
64 .catch(err => cb(err))
65}, QUEUE_CONCURRENCY.AVATAR_PROCESS_IMAGE)
66
67function pushAvatarProcessInQueue (task: DownloadImageQueueTask) {
68 return new Promise<void>((res, rej) => {
69 downloadImageQueue.push(task, err => {
70 if (err) return rej(err)
71
72 return res()
73 })
74 })
75}
76
77// Unsafe so could returns paths that does not exist anymore
78const avatarPathUnsafeCache = new LRUCache<string, string>({ max: LRU_CACHE.AVATAR_STATIC.MAX_SIZE })
79
80export {
81 avatarPathUnsafeCache,
82 updateLocalActorAvatarFile,
83 deleteLocalActorAvatarFile,
84 pushAvatarProcessInQueue
85}
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts
index f19ec7df0..6ddaa82c8 100644
--- a/server/lib/client-html.ts
+++ b/server/lib/client-html.ts
@@ -5,12 +5,13 @@ import validator from 'validator'
5import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/core-utils/i18n/i18n' 5import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/core-utils/i18n/i18n'
6import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' 6import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
7import { VideoPlaylistPrivacy, VideoPrivacy } from '../../shared/models/videos' 7import { VideoPlaylistPrivacy, VideoPrivacy } from '../../shared/models/videos'
8import { escapeHTML, isTestInstance, sha256 } from '../helpers/core-utils' 8import { isTestInstance, sha256 } from '../helpers/core-utils'
9import { escapeHTML } from '@shared/core-utils/renderer'
9import { logger } from '../helpers/logger' 10import { logger } from '../helpers/logger'
10import { CONFIG } from '../initializers/config' 11import { CONFIG } from '../initializers/config'
11import { 12import {
12 ACCEPT_HEADERS, 13 ACCEPT_HEADERS,
13 AVATARS_SIZE, 14 ACTOR_IMAGES_SIZE,
14 CUSTOM_HTML_TAG_COMMENTS, 15 CUSTOM_HTML_TAG_COMMENTS,
15 EMBED_SIZE, 16 EMBED_SIZE,
16 FILES_CONTENT_HASH, 17 FILES_CONTENT_HASH,
@@ -245,8 +246,8 @@ class ClientHtml {
245 246
246 const image = { 247 const image = {
247 url: entity.Actor.getAvatarUrl(), 248 url: entity.Actor.getAvatarUrl(),
248 width: AVATARS_SIZE.width, 249 width: ACTOR_IMAGES_SIZE.AVATARS.width,
249 height: AVATARS_SIZE.height 250 height: ACTOR_IMAGES_SIZE.AVATARS.height
250 } 251 }
251 252
252 const ogType = 'website' 253 const ogType = 'website'
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts
index 969eae77b..9ca0d5d5b 100644
--- a/server/lib/emailer.ts
+++ b/server/lib/emailer.ts
@@ -7,12 +7,12 @@ import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/m
7import { MVideoImport, MVideoImportVideo } from '@server/types/models/video/video-import' 7import { MVideoImport, MVideoImportVideo } from '@server/types/models/video/video-import'
8import { SANITIZE_OPTIONS, TEXT_WITH_HTML_RULES } from '@shared/core-utils' 8import { SANITIZE_OPTIONS, TEXT_WITH_HTML_RULES } from '@shared/core-utils'
9import { AbuseState, EmailPayload, UserAbuse } from '@shared/models' 9import { AbuseState, EmailPayload, UserAbuse } from '@shared/models'
10import { SendEmailOptions } from '../../shared/models/server/emailer.model' 10import { SendEmailDefaultOptions } from '../../shared/models/server/emailer.model'
11import { isTestInstance, root } from '../helpers/core-utils' 11import { isTestInstance, root } from '../helpers/core-utils'
12import { bunyanLogger, logger } from '../helpers/logger' 12import { bunyanLogger, logger } from '../helpers/logger'
13import { CONFIG, isEmailEnabled } from '../initializers/config' 13import { CONFIG, isEmailEnabled } from '../initializers/config'
14import { WEBSERVER } from '../initializers/constants' 14import { WEBSERVER } from '../initializers/constants'
15import { MAbuseFull, MAbuseMessage, MAccountDefault, MActorFollowActors, MActorFollowFull, MUser } from '../types/models' 15import { MAbuseFull, MAbuseMessage, MAccountDefault, MActorFollowActors, MActorFollowFull, MPlugin, MUser } from '../types/models'
16import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video' 16import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video'
17import { JobQueue } from './job-queue' 17import { JobQueue } from './job-queue'
18 18
@@ -403,9 +403,9 @@ class Emailer {
403 } 403 }
404 404
405 async addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) { 405 async addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) {
406 const VIDEO_AUTO_BLACKLIST_URL = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list' 406 const videoAutoBlacklistUrl = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list'
407 const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath() 407 const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath()
408 const channel = (await VideoChannelModel.loadByIdAndPopulateAccount(videoBlacklist.Video.channelId)).toFormattedSummaryJSON() 408 const channel = (await VideoChannelModel.loadAndPopulateAccount(videoBlacklist.Video.channelId)).toFormattedSummaryJSON()
409 409
410 const emailPayload: EmailPayload = { 410 const emailPayload: EmailPayload = {
411 template: 'video-auto-blacklist-new', 411 template: 'video-auto-blacklist-new',
@@ -417,7 +417,7 @@ class Emailer {
417 videoName: videoBlacklist.Video.name, 417 videoName: videoBlacklist.Video.name,
418 action: { 418 action: {
419 text: 'Review autoblacklist', 419 text: 'Review autoblacklist',
420 url: VIDEO_AUTO_BLACKLIST_URL 420 url: videoAutoBlacklistUrl
421 } 421 }
422 } 422 }
423 } 423 }
@@ -472,6 +472,36 @@ class Emailer {
472 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 472 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
473 } 473 }
474 474
475 addNewPeerTubeVersionNotification (to: string[], latestVersion: string) {
476 const emailPayload: EmailPayload = {
477 to,
478 template: 'peertube-version-new',
479 subject: `A new PeerTube version is available: ${latestVersion}`,
480 locals: {
481 latestVersion
482 }
483 }
484
485 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
486 }
487
488 addNewPlugionVersionNotification (to: string[], plugin: MPlugin) {
489 const pluginUrl = WEBSERVER.URL + '/admin/plugins/list-installed?pluginType=' + plugin.type
490
491 const emailPayload: EmailPayload = {
492 to,
493 template: 'plugin-version-new',
494 subject: `A new plugin/theme version is available: ${plugin.name}@${plugin.latestVersion}`,
495 locals: {
496 pluginName: plugin.name,
497 latestVersion: plugin.latestVersion,
498 pluginUrl
499 }
500 }
501
502 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
503 }
504
475 addPasswordResetEmailJob (username: string, to: string, resetPasswordUrl: string) { 505 addPasswordResetEmailJob (username: string, to: string, resetPasswordUrl: string) {
476 const emailPayload: EmailPayload = { 506 const emailPayload: EmailPayload = {
477 template: 'password-reset', 507 template: 'password-reset',
@@ -569,26 +599,27 @@ class Emailer {
569 }) 599 })
570 600
571 for (const to of options.to) { 601 for (const to of options.to) {
572 await email 602 const baseOptions: SendEmailDefaultOptions = {
573 .send(merge( 603 template: 'common',
574 { 604 message: {
575 template: 'common', 605 to,
576 message: { 606 from: options.from,
577 to, 607 subject: options.subject,
578 from: options.from, 608 replyTo: options.replyTo
579 subject: options.subject, 609 },
580 replyTo: options.replyTo 610 locals: { // default variables available in all templates
581 }, 611 WEBSERVER,
582 locals: { // default variables available in all templates 612 EMAIL: CONFIG.EMAIL,
583 WEBSERVER, 613 instanceName: CONFIG.INSTANCE.NAME,
584 EMAIL: CONFIG.EMAIL, 614 text: options.text,
585 instanceName: CONFIG.INSTANCE.NAME, 615 subject: options.subject
586 text: options.text, 616 }
587 subject: options.subject 617 }
588 } 618
589 }, 619 // overriden/new variables given for a specific template in the payload
590 options // overriden/new variables given for a specific template in the payload 620 const sendOptions = merge(baseOptions, options)
591 ) as SendEmailOptions) 621
622 await email.send(sendOptions)
592 .then(res => logger.debug('Sent email.', { res })) 623 .then(res => logger.debug('Sent email.', { res }))
593 .catch(err => logger.error('Error in email sender.', { err })) 624 .catch(err => logger.error('Error in email sender.', { err }))
594 } 625 }
diff --git a/server/lib/emails/peertube-version-new/html.pug b/server/lib/emails/peertube-version-new/html.pug
new file mode 100644
index 000000000..2f4d9399d
--- /dev/null
+++ b/server/lib/emails/peertube-version-new/html.pug
@@ -0,0 +1,9 @@
1extends ../common/greetings
2
3block title
4 | New PeerTube version available
5
6block content
7 p
8 | A new version of PeerTube is available: #{latestVersion}.
9 | You can check the latest news on #[a(href="https://joinpeertube.org/news") JoinPeerTube].
diff --git a/server/lib/emails/plugin-version-new/html.pug b/server/lib/emails/plugin-version-new/html.pug
new file mode 100644
index 000000000..86d3d87e8
--- /dev/null
+++ b/server/lib/emails/plugin-version-new/html.pug
@@ -0,0 +1,9 @@
1extends ../common/greetings
2
3block title
4 | New plugin version available
5
6block content
7 p
8 | A new version of the plugin/theme #{pluginName} is available: #{latestVersion}.
9 | You might want to upgrade it on #[a(href=pluginUrl) the PeerTube admin interface].
diff --git a/server/lib/files-cache/videos-caption-cache.ts b/server/lib/files-cache/videos-caption-cache.ts
index ee0447010..58e2260b6 100644
--- a/server/lib/files-cache/videos-caption-cache.ts
+++ b/server/lib/files-cache/videos-caption-cache.ts
@@ -41,7 +41,7 @@ class VideosCaptionCache extends AbstractVideoStaticFileCache <string> {
41 const remoteUrl = videoCaption.getFileUrl(video) 41 const remoteUrl = videoCaption.getFileUrl(video)
42 const destPath = join(FILES_CACHE.VIDEO_CAPTIONS.DIRECTORY, videoCaption.filename) 42 const destPath = join(FILES_CACHE.VIDEO_CAPTIONS.DIRECTORY, videoCaption.filename)
43 43
44 await doRequestAndSaveToFile({ uri: remoteUrl }, destPath) 44 await doRequestAndSaveToFile(remoteUrl, destPath)
45 45
46 return { isOwned: false, path: destPath } 46 return { isOwned: false, path: destPath }
47 } 47 }
diff --git a/server/lib/files-cache/videos-preview-cache.ts b/server/lib/files-cache/videos-preview-cache.ts
index ee72cd3f9..dd3a84aca 100644
--- a/server/lib/files-cache/videos-preview-cache.ts
+++ b/server/lib/files-cache/videos-preview-cache.ts
@@ -39,7 +39,7 @@ class VideosPreviewCache extends AbstractVideoStaticFileCache <string> {
39 const destPath = join(FILES_CACHE.PREVIEWS.DIRECTORY, preview.filename) 39 const destPath = join(FILES_CACHE.PREVIEWS.DIRECTORY, preview.filename)
40 40
41 const remoteUrl = preview.getFileUrl(video) 41 const remoteUrl = preview.getFileUrl(video)
42 await doRequestAndSaveToFile({ uri: remoteUrl }, destPath) 42 await doRequestAndSaveToFile(remoteUrl, destPath)
43 43
44 logger.debug('Fetched remote preview %s to %s.', remoteUrl, destPath) 44 logger.debug('Fetched remote preview %s to %s.', remoteUrl, destPath)
45 45
diff --git a/server/lib/files-cache/videos-torrent-cache.ts b/server/lib/files-cache/videos-torrent-cache.ts
index ca0e1770d..23217f140 100644
--- a/server/lib/files-cache/videos-torrent-cache.ts
+++ b/server/lib/files-cache/videos-torrent-cache.ts
@@ -5,6 +5,7 @@ import { CONFIG } from '../../initializers/config'
5import { FILES_CACHE } from '../../initializers/constants' 5import { FILES_CACHE } from '../../initializers/constants'
6import { VideoModel } from '../../models/video/video' 6import { VideoModel } from '../../models/video/video'
7import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache' 7import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache'
8import { MVideo, MVideoFile } from '@server/types/models'
8 9
9class VideosTorrentCache extends AbstractVideoStaticFileCache <string> { 10class VideosTorrentCache extends AbstractVideoStaticFileCache <string> {
10 11
@@ -22,7 +23,11 @@ class VideosTorrentCache extends AbstractVideoStaticFileCache <string> {
22 const file = await VideoFileModel.loadWithVideoOrPlaylistByTorrentFilename(filename) 23 const file = await VideoFileModel.loadWithVideoOrPlaylistByTorrentFilename(filename)
23 if (!file) return undefined 24 if (!file) return undefined
24 25
25 if (file.getVideo().isOwned()) return { isOwned: true, path: join(CONFIG.STORAGE.TORRENTS_DIR, file.torrentFilename) } 26 if (file.getVideo().isOwned()) {
27 const downloadName = this.buildDownloadName(file.getVideo(), file)
28
29 return { isOwned: true, path: join(CONFIG.STORAGE.TORRENTS_DIR, file.torrentFilename), downloadName }
30 }
26 31
27 return this.loadRemoteFile(filename) 32 return this.loadRemoteFile(filename)
28 } 33 }
@@ -41,12 +46,16 @@ class VideosTorrentCache extends AbstractVideoStaticFileCache <string> {
41 const remoteUrl = file.getRemoteTorrentUrl(video) 46 const remoteUrl = file.getRemoteTorrentUrl(video)
42 const destPath = join(FILES_CACHE.TORRENTS.DIRECTORY, file.torrentFilename) 47 const destPath = join(FILES_CACHE.TORRENTS.DIRECTORY, file.torrentFilename)
43 48
44 await doRequestAndSaveToFile({ uri: remoteUrl }, destPath) 49 await doRequestAndSaveToFile(remoteUrl, destPath)
45 50
46 const downloadName = `${video.name}-${file.resolution}p.torrent` 51 const downloadName = this.buildDownloadName(video, file)
47 52
48 return { isOwned: false, path: destPath, downloadName } 53 return { isOwned: false, path: destPath, downloadName }
49 } 54 }
55
56 private buildDownloadName (video: MVideo, file: MVideoFile) {
57 return `${video.name}-${file.resolution}p.torrent`
58 }
50} 59}
51 60
52export { 61export {
diff --git a/server/lib/hls.ts b/server/lib/hls.ts
index 04187668c..84539e2c1 100644
--- a/server/lib/hls.ts
+++ b/server/lib/hls.ts
@@ -135,7 +135,7 @@ function downloadPlaylistSegments (playlistUrl: string, destinationDir: string,
135 const destPath = join(tmpDirectory, basename(fileUrl)) 135 const destPath = join(tmpDirectory, basename(fileUrl))
136 136
137 const bodyKBLimit = 10 * 1000 * 1000 // 10GB 137 const bodyKBLimit = 10 * 1000 * 1000 // 10GB
138 await doRequestAndSaveToFile({ uri: fileUrl }, destPath, bodyKBLimit) 138 await doRequestAndSaveToFile(fileUrl, destPath, { bodyKBLimit })
139 } 139 }
140 140
141 clearTimeout(timer) 141 clearTimeout(timer)
@@ -156,7 +156,7 @@ function downloadPlaylistSegments (playlistUrl: string, destinationDir: string,
156 } 156 }
157 157
158 async function fetchUniqUrls (playlistUrl: string) { 158 async function fetchUniqUrls (playlistUrl: string) {
159 const { body } = await doRequest<string>({ uri: playlistUrl }) 159 const { body } = await doRequest(playlistUrl)
160 160
161 if (!body) return [] 161 if (!body) return []
162 162
diff --git a/server/lib/job-queue/handlers/activitypub-cleaner.ts b/server/lib/job-queue/handlers/activitypub-cleaner.ts
index b58bbc983..1caca1dcc 100644
--- a/server/lib/job-queue/handlers/activitypub-cleaner.ts
+++ b/server/lib/job-queue/handlers/activitypub-cleaner.ts
@@ -1,10 +1,13 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import * as Bull from 'bull' 2import * as Bull from 'bull'
3import { checkUrlsSameHost } from '@server/helpers/activitypub' 3import { checkUrlsSameHost } from '@server/helpers/activitypub'
4import { isDislikeActivityValid, isLikeActivityValid } from '@server/helpers/custom-validators/activitypub/rate' 4import {
5import { isShareActivityValid } from '@server/helpers/custom-validators/activitypub/share' 5 isAnnounceActivityValid,
6 isDislikeActivityValid,
7 isLikeActivityValid
8} from '@server/helpers/custom-validators/activitypub/activity'
6import { sanitizeAndCheckVideoCommentObject } from '@server/helpers/custom-validators/activitypub/video-comments' 9import { sanitizeAndCheckVideoCommentObject } from '@server/helpers/custom-validators/activitypub/video-comments'
7import { doRequest } from '@server/helpers/requests' 10import { doJSONRequest, PeerTubeRequestError } from '@server/helpers/requests'
8import { AP_CLEANER_CONCURRENCY } from '@server/initializers/constants' 11import { AP_CLEANER_CONCURRENCY } from '@server/initializers/constants'
9import { VideoModel } from '@server/models/video/video' 12import { VideoModel } from '@server/models/video/video'
10import { VideoCommentModel } from '@server/models/video/video-comment' 13import { VideoCommentModel } from '@server/models/video/video-comment'
@@ -78,44 +81,44 @@ async function updateObjectIfNeeded <T> (
78 updater: (url: string, newUrl: string) => Promise<T>, 81 updater: (url: string, newUrl: string) => Promise<T>,
79 deleter: (url: string) => Promise<T> 82 deleter: (url: string) => Promise<T>
80): Promise<{ data: T, status: 'deleted' | 'updated' } | null> { 83): Promise<{ data: T, status: 'deleted' | 'updated' } | null> {
81 // Fetch url 84 const on404OrTombstone = async () => {
82 const { response, body } = await doRequest<any>({
83 uri: url,
84 json: true,
85 activityPub: true
86 })
87
88 // Does not exist anymore, remove entry
89 if (response.statusCode === HttpStatusCode.NOT_FOUND_404) {
90 logger.info('Removing remote AP object %s.', url) 85 logger.info('Removing remote AP object %s.', url)
91 const data = await deleter(url) 86 const data = await deleter(url)
92 87
93 return { status: 'deleted', data } 88 return { status: 'deleted' as 'deleted', data }
94 } 89 }
95 90
96 // If not same id, check same host and update 91 try {
97 if (!body || !body.id || !bodyValidator(body)) throw new Error(`Body or body id of ${url} is invalid`) 92 const { body } = await doJSONRequest<any>(url, { activityPub: true })
98 93
99 if (body.type === 'Tombstone') { 94 // If not same id, check same host and update
100 logger.info('Removing remote AP object %s.', url) 95 if (!body || !body.id || !bodyValidator(body)) throw new Error(`Body or body id of ${url} is invalid`)
101 const data = await deleter(url)
102 96
103 return { status: 'deleted', data } 97 if (body.type === 'Tombstone') {
104 } 98 return on404OrTombstone()
99 }
105 100
106 const newUrl = body.id 101 const newUrl = body.id
107 if (newUrl !== url) { 102 if (newUrl !== url) {
108 if (checkUrlsSameHost(newUrl, url) !== true) { 103 if (checkUrlsSameHost(newUrl, url) !== true) {
109 throw new Error(`New url ${newUrl} has not the same host than old url ${url}`) 104 throw new Error(`New url ${newUrl} has not the same host than old url ${url}`)
105 }
106
107 logger.info('Updating remote AP object %s.', url)
108 const data = await updater(url, newUrl)
109
110 return { status: 'updated', data }
110 } 111 }
111 112
112 logger.info('Updating remote AP object %s.', url) 113 return null
113 const data = await updater(url, newUrl) 114 } catch (err) {
115 // Does not exist anymore, remove entry
116 if ((err as PeerTubeRequestError).statusCode === HttpStatusCode.NOT_FOUND_404) {
117 return on404OrTombstone()
118 }
114 119
115 return { status: 'updated', data } 120 throw err
116 } 121 }
117
118 return null
119} 122}
120 123
121function rateOptionsFactory () { 124function rateOptionsFactory () {
@@ -149,7 +152,7 @@ function rateOptionsFactory () {
149 152
150function shareOptionsFactory () { 153function shareOptionsFactory () {
151 return { 154 return {
152 bodyValidator: (body: any) => isShareActivityValid(body), 155 bodyValidator: (body: any) => isAnnounceActivityValid(body),
153 156
154 updater: async (url: string, newUrl: string) => { 157 updater: async (url: string, newUrl: string) => {
155 const share = await VideoShareModel.loadByUrl(url, undefined) 158 const share = await VideoShareModel.loadByUrl(url, undefined)
diff --git a/server/lib/job-queue/handlers/activitypub-http-broadcast.ts b/server/lib/job-queue/handlers/activitypub-http-broadcast.ts
index 7174786d6..c69ff9e83 100644
--- a/server/lib/job-queue/handlers/activitypub-http-broadcast.ts
+++ b/server/lib/job-queue/handlers/activitypub-http-broadcast.ts
@@ -16,8 +16,7 @@ async function processActivityPubHttpBroadcast (job: Bull.Job) {
16 const httpSignatureOptions = await buildSignedRequestOptions(payload) 16 const httpSignatureOptions = await buildSignedRequestOptions(payload)
17 17
18 const options = { 18 const options = {
19 method: 'POST', 19 method: 'POST' as 'POST',
20 uri: '',
21 json: body, 20 json: body,
22 httpSignature: httpSignatureOptions, 21 httpSignature: httpSignatureOptions,
23 timeout: REQUEST_TIMEOUT, 22 timeout: REQUEST_TIMEOUT,
@@ -28,7 +27,7 @@ async function processActivityPubHttpBroadcast (job: Bull.Job) {
28 const goodUrls: string[] = [] 27 const goodUrls: string[] = []
29 28
30 await Bluebird.map(payload.uris, uri => { 29 await Bluebird.map(payload.uris, uri => {
31 return doRequest(Object.assign({}, options, { uri })) 30 return doRequest(uri, options)
32 .then(() => goodUrls.push(uri)) 31 .then(() => goodUrls.push(uri))
33 .catch(() => badUrls.push(uri)) 32 .catch(() => badUrls.push(uri))
34 }, { concurrency: BROADCAST_CONCURRENCY }) 33 }, { concurrency: BROADCAST_CONCURRENCY })
diff --git a/server/lib/job-queue/handlers/activitypub-http-unicast.ts b/server/lib/job-queue/handlers/activitypub-http-unicast.ts
index 74989d62e..585dad671 100644
--- a/server/lib/job-queue/handlers/activitypub-http-unicast.ts
+++ b/server/lib/job-queue/handlers/activitypub-http-unicast.ts
@@ -16,8 +16,7 @@ async function processActivityPubHttpUnicast (job: Bull.Job) {
16 const httpSignatureOptions = await buildSignedRequestOptions(payload) 16 const httpSignatureOptions = await buildSignedRequestOptions(payload)
17 17
18 const options = { 18 const options = {
19 method: 'POST', 19 method: 'POST' as 'POST',
20 uri,
21 json: body, 20 json: body,
22 httpSignature: httpSignatureOptions, 21 httpSignature: httpSignatureOptions,
23 timeout: REQUEST_TIMEOUT, 22 timeout: REQUEST_TIMEOUT,
@@ -25,7 +24,7 @@ async function processActivityPubHttpUnicast (job: Bull.Job) {
25 } 24 }
26 25
27 try { 26 try {
28 await doRequest(options) 27 await doRequest(uri, options)
29 ActorFollowScoreCache.Instance.updateActorFollowsScore([ uri ], []) 28 ActorFollowScoreCache.Instance.updateActorFollowsScore([ uri ], [])
30 } catch (err) { 29 } catch (err) {
31 ActorFollowScoreCache.Instance.updateActorFollowsScore([], [ uri ]) 30 ActorFollowScoreCache.Instance.updateActorFollowsScore([], [ uri ])
diff --git a/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts b/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts
index c030d31ef..e8a91450d 100644
--- a/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts
+++ b/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts
@@ -6,21 +6,24 @@ import { getServerActor } from '@server/models/application/application'
6import { buildDigest } from '@server/helpers/peertube-crypto' 6import { buildDigest } from '@server/helpers/peertube-crypto'
7import { ContextType } from '@shared/models/activitypub/context' 7import { ContextType } from '@shared/models/activitypub/context'
8 8
9type Payload = { body: any, contextType?: ContextType, signatureActorId?: number } 9type Payload <T> = { body: T, contextType?: ContextType, signatureActorId?: number }
10 10
11async function computeBody (payload: Payload) { 11async function computeBody <T> (
12 payload: Payload<T>
13): Promise<T | T & { type: 'RsaSignature2017', creator: string, created: string }> {
12 let body = payload.body 14 let body = payload.body
13 15
14 if (payload.signatureActorId) { 16 if (payload.signatureActorId) {
15 const actorSignature = await ActorModel.load(payload.signatureActorId) 17 const actorSignature = await ActorModel.load(payload.signatureActorId)
16 if (!actorSignature) throw new Error('Unknown signature actor id.') 18 if (!actorSignature) throw new Error('Unknown signature actor id.')
19
17 body = await buildSignedActivity(actorSignature, payload.body, payload.contextType) 20 body = await buildSignedActivity(actorSignature, payload.body, payload.contextType)
18 } 21 }
19 22
20 return body 23 return body
21} 24}
22 25
23async function buildSignedRequestOptions (payload: Payload) { 26async function buildSignedRequestOptions (payload: Payload<any>) {
24 let actor: MActor | null 27 let actor: MActor | null
25 28
26 if (payload.signatureActorId) { 29 if (payload.signatureActorId) {
@@ -43,9 +46,9 @@ async function buildSignedRequestOptions (payload: Payload) {
43 46
44function buildGlobalHeaders (body: any) { 47function buildGlobalHeaders (body: any) {
45 return { 48 return {
46 'Digest': buildDigest(body), 49 'digest': buildDigest(body),
47 'Content-Type': 'application/activity+json', 50 'content-type': 'application/activity+json',
48 'Accept': ACTIVITY_PUB.ACCEPT_HEADER 51 'accept': ACTIVITY_PUB.ACCEPT_HEADER
49 } 52 }
50} 53}
51 54
diff --git a/server/lib/notifier.ts b/server/lib/notifier.ts
index 740c274d7..da7f7cc05 100644
--- a/server/lib/notifier.ts
+++ b/server/lib/notifier.ts
@@ -19,7 +19,7 @@ import { CONFIG } from '../initializers/config'
19import { AccountBlocklistModel } from '../models/account/account-blocklist' 19import { AccountBlocklistModel } from '../models/account/account-blocklist'
20import { UserModel } from '../models/account/user' 20import { UserModel } from '../models/account/user'
21import { UserNotificationModel } from '../models/account/user-notification' 21import { UserNotificationModel } from '../models/account/user-notification'
22import { MAbuseFull, MAbuseMessage, MAccountServer, MActorFollowFull } from '../types/models' 22import { MAbuseFull, MAbuseMessage, MAccountServer, MActorFollowFull, MApplication, MPlugin } from '../types/models'
23import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video' 23import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video'
24import { isBlockedByServerOrAccount } from './blocklist' 24import { isBlockedByServerOrAccount } from './blocklist'
25import { Emailer } from './emailer' 25import { Emailer } from './emailer'
@@ -144,6 +144,20 @@ class Notifier {
144 }) 144 })
145 } 145 }
146 146
147 notifyOfNewPeerTubeVersion (application: MApplication, latestVersion: string) {
148 this.notifyAdminsOfNewPeerTubeVersion(application, latestVersion)
149 .catch(err => {
150 logger.error('Cannot notify on new PeerTubeb version %s.', latestVersion, { err })
151 })
152 }
153
154 notifyOfNewPluginVersion (plugin: MPlugin) {
155 this.notifyAdminsOfNewPluginVersion(plugin)
156 .catch(err => {
157 logger.error('Cannot notify on new plugin version %s.', plugin.name, { err })
158 })
159 }
160
147 private async notifySubscribersOfNewVideo (video: MVideoAccountLight) { 161 private async notifySubscribersOfNewVideo (video: MVideoAccountLight) {
148 // List all followers that are users 162 // List all followers that are users
149 const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId) 163 const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId)
@@ -667,6 +681,64 @@ class Notifier {
667 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender }) 681 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
668 } 682 }
669 683
684 private async notifyAdminsOfNewPeerTubeVersion (application: MApplication, latestVersion: string) {
685 // Use the debug right to know who is an administrator
686 const admins = await UserModel.listWithRight(UserRight.MANAGE_DEBUG)
687 if (admins.length === 0) return
688
689 logger.info('Notifying %s admins of new PeerTube version %s.', admins.length, latestVersion)
690
691 function settingGetter (user: MUserWithNotificationSetting) {
692 return user.NotificationSetting.newPeerTubeVersion
693 }
694
695 async function notificationCreator (user: MUserWithNotificationSetting) {
696 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
697 type: UserNotificationType.NEW_PEERTUBE_VERSION,
698 userId: user.id,
699 applicationId: application.id
700 })
701 notification.Application = application
702
703 return notification
704 }
705
706 function emailSender (emails: string[]) {
707 return Emailer.Instance.addNewPeerTubeVersionNotification(emails, latestVersion)
708 }
709
710 return this.notify({ users: admins, settingGetter, notificationCreator, emailSender })
711 }
712
713 private async notifyAdminsOfNewPluginVersion (plugin: MPlugin) {
714 // Use the debug right to know who is an administrator
715 const admins = await UserModel.listWithRight(UserRight.MANAGE_DEBUG)
716 if (admins.length === 0) return
717
718 logger.info('Notifying %s admins of new plugin version %s@%s.', admins.length, plugin.name, plugin.latestVersion)
719
720 function settingGetter (user: MUserWithNotificationSetting) {
721 return user.NotificationSetting.newPluginVersion
722 }
723
724 async function notificationCreator (user: MUserWithNotificationSetting) {
725 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
726 type: UserNotificationType.NEW_PLUGIN_VERSION,
727 userId: user.id,
728 pluginId: plugin.id
729 })
730 notification.Plugin = plugin
731
732 return notification
733 }
734
735 function emailSender (emails: string[]) {
736 return Emailer.Instance.addNewPlugionVersionNotification(emails, plugin)
737 }
738
739 return this.notify({ users: admins, settingGetter, notificationCreator, emailSender })
740 }
741
670 private async notify<T extends MUserWithNotificationSetting> (options: { 742 private async notify<T extends MUserWithNotificationSetting> (options: {
671 users: T[] 743 users: T[]
672 notificationCreator: (user: T) => Promise<UserNotificationModelForApi> 744 notificationCreator: (user: T) => Promise<UserNotificationModelForApi>
diff --git a/server/lib/plugins/plugin-index.ts b/server/lib/plugins/plugin-index.ts
index 7bcb6ed4c..624f5da1d 100644
--- a/server/lib/plugins/plugin-index.ts
+++ b/server/lib/plugins/plugin-index.ts
@@ -1,22 +1,22 @@
1import { doRequest } from '../../helpers/requests' 1import { sanitizeUrl } from '@server/helpers/core-utils'
2import { CONFIG } from '../../initializers/config' 2import { ResultList } from '../../../shared/models'
3import { PeertubePluginIndexList } from '../../../shared/models/plugins/peertube-plugin-index-list.model'
4import { PeerTubePluginIndex } from '../../../shared/models/plugins/peertube-plugin-index.model'
3import { 5import {
4 PeertubePluginLatestVersionRequest, 6 PeertubePluginLatestVersionRequest,
5 PeertubePluginLatestVersionResponse 7 PeertubePluginLatestVersionResponse
6} from '../../../shared/models/plugins/peertube-plugin-latest-version.model' 8} from '../../../shared/models/plugins/peertube-plugin-latest-version.model'
7import { PeertubePluginIndexList } from '../../../shared/models/plugins/peertube-plugin-index-list.model'
8import { ResultList } from '../../../shared/models'
9import { PeerTubePluginIndex } from '../../../shared/models/plugins/peertube-plugin-index.model'
10import { PluginModel } from '../../models/server/plugin'
11import { PluginManager } from './plugin-manager'
12import { logger } from '../../helpers/logger' 9import { logger } from '../../helpers/logger'
10import { doJSONRequest } from '../../helpers/requests'
11import { CONFIG } from '../../initializers/config'
13import { PEERTUBE_VERSION } from '../../initializers/constants' 12import { PEERTUBE_VERSION } from '../../initializers/constants'
14import { sanitizeUrl } from '@server/helpers/core-utils' 13import { PluginModel } from '../../models/server/plugin'
14import { PluginManager } from './plugin-manager'
15 15
16async function listAvailablePluginsFromIndex (options: PeertubePluginIndexList) { 16async function listAvailablePluginsFromIndex (options: PeertubePluginIndexList) {
17 const { start = 0, count = 20, search, sort = 'npmName', pluginType } = options 17 const { start = 0, count = 20, search, sort = 'npmName', pluginType } = options
18 18
19 const qs: PeertubePluginIndexList = { 19 const searchParams: PeertubePluginIndexList & Record<string, string | number> = {
20 start, 20 start,
21 count, 21 count,
22 sort, 22 sort,
@@ -28,7 +28,7 @@ async function listAvailablePluginsFromIndex (options: PeertubePluginIndexList)
28 const uri = CONFIG.PLUGINS.INDEX.URL + '/api/v1/plugins' 28 const uri = CONFIG.PLUGINS.INDEX.URL + '/api/v1/plugins'
29 29
30 try { 30 try {
31 const { body } = await doRequest<any>({ uri, qs, json: true }) 31 const { body } = await doJSONRequest<any>(uri, { searchParams })
32 32
33 logger.debug('Got result from PeerTube index.', { body }) 33 logger.debug('Got result from PeerTube index.', { body })
34 34
@@ -58,7 +58,11 @@ async function getLatestPluginsVersion (npmNames: string[]): Promise<PeertubePlu
58 58
59 const uri = sanitizeUrl(CONFIG.PLUGINS.INDEX.URL) + '/api/v1/plugins/latest-version' 59 const uri = sanitizeUrl(CONFIG.PLUGINS.INDEX.URL) + '/api/v1/plugins/latest-version'
60 60
61 const { body } = await doRequest<any>({ uri, body: bodyRequest, json: true, method: 'POST' }) 61 const options = {
62 json: bodyRequest,
63 method: 'POST' as 'POST'
64 }
65 const { body } = await doJSONRequest<PeertubePluginLatestVersionResponse>(uri, options)
62 66
63 return body 67 return body
64} 68}
diff --git a/server/lib/plugins/register-helpers.ts b/server/lib/plugins/register-helpers.ts
index 1f2a88c27..9b5e1a546 100644
--- a/server/lib/plugins/register-helpers.ts
+++ b/server/lib/plugins/register-helpers.ts
@@ -7,7 +7,7 @@ import {
7 VIDEO_PLAYLIST_PRIVACIES, 7 VIDEO_PLAYLIST_PRIVACIES,
8 VIDEO_PRIVACIES 8 VIDEO_PRIVACIES
9} from '@server/initializers/constants' 9} from '@server/initializers/constants'
10import { onExternalUserAuthenticated } from '@server/lib/auth' 10import { onExternalUserAuthenticated } from '@server/lib/auth/external-auth'
11import { PluginModel } from '@server/models/server/plugin' 11import { PluginModel } from '@server/models/server/plugin'
12import { 12import {
13 RegisterServerAuthExternalOptions, 13 RegisterServerAuthExternalOptions,
diff --git a/server/lib/schedulers/auto-follow-index-instances.ts b/server/lib/schedulers/auto-follow-index-instances.ts
index f62f52f9c..0b8cd1389 100644
--- a/server/lib/schedulers/auto-follow-index-instances.ts
+++ b/server/lib/schedulers/auto-follow-index-instances.ts
@@ -1,5 +1,5 @@
1import { chunk } from 'lodash' 1import { chunk } from 'lodash'
2import { doRequest } from '@server/helpers/requests' 2import { doJSONRequest } from '@server/helpers/requests'
3import { JobQueue } from '@server/lib/job-queue' 3import { JobQueue } from '@server/lib/job-queue'
4import { ActorFollowModel } from '@server/models/activitypub/actor-follow' 4import { ActorFollowModel } from '@server/models/activitypub/actor-follow'
5import { getServerActor } from '@server/models/application/application' 5import { getServerActor } from '@server/models/application/application'
@@ -34,12 +34,12 @@ export class AutoFollowIndexInstances extends AbstractScheduler {
34 try { 34 try {
35 const serverActor = await getServerActor() 35 const serverActor = await getServerActor()
36 36
37 const qs = { count: 1000 } 37 const searchParams = { count: 1000 }
38 if (this.lastCheck) Object.assign(qs, { since: this.lastCheck.toISOString() }) 38 if (this.lastCheck) Object.assign(searchParams, { since: this.lastCheck.toISOString() })
39 39
40 this.lastCheck = new Date() 40 this.lastCheck = new Date()
41 41
42 const { body } = await doRequest<any>({ uri: indexUrl, qs, json: true }) 42 const { body } = await doJSONRequest<any>(indexUrl, { searchParams })
43 if (!body.data || Array.isArray(body.data) === false) { 43 if (!body.data || Array.isArray(body.data) === false) {
44 logger.error('Cannot auto follow instances of index %s. Please check the auto follow URL.', indexUrl, { body }) 44 logger.error('Cannot auto follow instances of index %s. Please check the auto follow URL.', indexUrl, { body })
45 return 45 return
diff --git a/server/lib/schedulers/peertube-version-check-scheduler.ts b/server/lib/schedulers/peertube-version-check-scheduler.ts
new file mode 100644
index 000000000..c8960465c
--- /dev/null
+++ b/server/lib/schedulers/peertube-version-check-scheduler.ts
@@ -0,0 +1,55 @@
1
2import { doJSONRequest } from '@server/helpers/requests'
3import { ApplicationModel } from '@server/models/application/application'
4import { compareSemVer } from '@shared/core-utils'
5import { JoinPeerTubeVersions } from '@shared/models'
6import { logger } from '../../helpers/logger'
7import { CONFIG } from '../../initializers/config'
8import { PEERTUBE_VERSION, SCHEDULER_INTERVALS_MS } from '../../initializers/constants'
9import { Notifier } from '../notifier'
10import { AbstractScheduler } from './abstract-scheduler'
11
12export class PeerTubeVersionCheckScheduler extends AbstractScheduler {
13
14 private static instance: AbstractScheduler
15
16 protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.checkPeerTubeVersion
17
18 private constructor () {
19 super()
20 }
21
22 protected async internalExecute () {
23 return this.checkLatestVersion()
24 }
25
26 private async checkLatestVersion () {
27 if (CONFIG.PEERTUBE.CHECK_LATEST_VERSION.ENABLED === false) return
28
29 logger.info('Checking latest PeerTube version.')
30
31 const { body } = await doJSONRequest<JoinPeerTubeVersions>(CONFIG.PEERTUBE.CHECK_LATEST_VERSION.URL)
32
33 if (!body?.peertube?.latestVersion) {
34 logger.warn('Cannot check latest PeerTube version: body is invalid.', { body })
35 return
36 }
37
38 const latestVersion = body.peertube.latestVersion
39 const application = await ApplicationModel.load()
40
41 // Already checked this version
42 if (application.latestPeerTubeVersion === latestVersion) return
43
44 if (compareSemVer(PEERTUBE_VERSION, latestVersion) < 0) {
45 application.latestPeerTubeVersion = latestVersion
46 await application.save()
47
48 Notifier.Instance.notifyOfNewPeerTubeVersion(application, latestVersion)
49 }
50 }
51
52 static get Instance () {
53 return this.instance || (this.instance = new this())
54 }
55}
diff --git a/server/lib/schedulers/plugins-check-scheduler.ts b/server/lib/schedulers/plugins-check-scheduler.ts
index 014993e94..9a1ae3ec5 100644
--- a/server/lib/schedulers/plugins-check-scheduler.ts
+++ b/server/lib/schedulers/plugins-check-scheduler.ts
@@ -6,6 +6,7 @@ import { PluginModel } from '../../models/server/plugin'
6import { chunk } from 'lodash' 6import { chunk } from 'lodash'
7import { getLatestPluginsVersion } from '../plugins/plugin-index' 7import { getLatestPluginsVersion } from '../plugins/plugin-index'
8import { compareSemVer } from '../../../shared/core-utils/miscs/miscs' 8import { compareSemVer } from '../../../shared/core-utils/miscs/miscs'
9import { Notifier } from '../notifier'
9 10
10export class PluginsCheckScheduler extends AbstractScheduler { 11export class PluginsCheckScheduler extends AbstractScheduler {
11 12
@@ -53,6 +54,11 @@ export class PluginsCheckScheduler extends AbstractScheduler {
53 plugin.latestVersion = result.latestVersion 54 plugin.latestVersion = result.latestVersion
54 await plugin.save() 55 await plugin.save()
55 56
57 // Notify if there is an higher plugin version available
58 if (compareSemVer(plugin.version, result.latestVersion) < 0) {
59 Notifier.Instance.notifyOfNewPluginVersion(plugin)
60 }
61
56 logger.info('Plugin %s has a new latest version %s.', result.npmName, plugin.latestVersion) 62 logger.info('Plugin %s has a new latest version %s.', result.npmName, plugin.latestVersion)
57 } 63 }
58 } 64 }
diff --git a/server/lib/thumbnail.ts b/server/lib/thumbnail.ts
index 106f5fdaa..cfee69cfc 100644
--- a/server/lib/thumbnail.ts
+++ b/server/lib/thumbnail.ts
@@ -1,7 +1,8 @@
1import { join } from 'path' 1import { join } from 'path'
2
2import { ThumbnailType } from '../../shared/models/videos/thumbnail.type' 3import { ThumbnailType } from '../../shared/models/videos/thumbnail.type'
3import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils' 4import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils'
4import { processImage } from '../helpers/image-utils' 5import { generateImageFilename, processImage } from '../helpers/image-utils'
5import { downloadImage } from '../helpers/requests' 6import { downloadImage } from '../helpers/requests'
6import { CONFIG } from '../initializers/config' 7import { CONFIG } from '../initializers/config'
7import { ASSETS_PATH, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants' 8import { ASSETS_PATH, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants'
@@ -11,7 +12,7 @@ import { MThumbnail } from '../types/models/video/thumbnail'
11import { MVideoPlaylistThumbnail } from '../types/models/video/video-playlist' 12import { MVideoPlaylistThumbnail } from '../types/models/video/video-playlist'
12import { getVideoFilePath } from './video-paths' 13import { getVideoFilePath } from './video-paths'
13 14
14type ImageSize = { height: number, width: number } 15type ImageSize = { height?: number, width?: number }
15 16
16function createPlaylistMiniatureFromExisting (options: { 17function createPlaylistMiniatureFromExisting (options: {
17 inputPath: string 18 inputPath: string
@@ -200,7 +201,7 @@ function buildMetadataFromVideo (video: MVideoThumbnail, type: ThumbnailType, si
200 : undefined 201 : undefined
201 202
202 if (type === ThumbnailType.MINIATURE) { 203 if (type === ThumbnailType.MINIATURE) {
203 const filename = video.generateThumbnailName() 204 const filename = generateImageFilename()
204 const basePath = CONFIG.STORAGE.THUMBNAILS_DIR 205 const basePath = CONFIG.STORAGE.THUMBNAILS_DIR
205 206
206 return { 207 return {
@@ -214,7 +215,7 @@ function buildMetadataFromVideo (video: MVideoThumbnail, type: ThumbnailType, si
214 } 215 }
215 216
216 if (type === ThumbnailType.PREVIEW) { 217 if (type === ThumbnailType.PREVIEW) {
217 const filename = video.generatePreviewName() 218 const filename = generateImageFilename()
218 const basePath = CONFIG.STORAGE.PREVIEWS_DIR 219 const basePath = CONFIG.STORAGE.PREVIEWS_DIR
219 220
220 return { 221 return {
diff --git a/server/lib/user.ts b/server/lib/user.ts
index e1892f22c..9b0a0a2f1 100644
--- a/server/lib/user.ts
+++ b/server/lib/user.ts
@@ -193,7 +193,9 @@ function createDefaultUserNotificationSettings (user: MUserId, t: Transaction |
193 newInstanceFollower: UserNotificationSettingValue.WEB, 193 newInstanceFollower: UserNotificationSettingValue.WEB,
194 abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 194 abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
195 abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 195 abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
196 autoInstanceFollowing: UserNotificationSettingValue.WEB 196 autoInstanceFollowing: UserNotificationSettingValue.WEB,
197 newPeerTubeVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
198 newPluginVersion: UserNotificationSettingValue.WEB
197 } 199 }
198 200
199 return UserNotificationSettingModel.create(values, { transaction: t }) 201 return UserNotificationSettingModel.create(values, { transaction: t })
diff --git a/server/lib/video-blacklist.ts b/server/lib/video-blacklist.ts
index dbb37e0b2..37c43c3b0 100644
--- a/server/lib/video-blacklist.ts
+++ b/server/lib/video-blacklist.ts
@@ -11,7 +11,7 @@ import {
11} from '@server/types/models' 11} from '@server/types/models'
12import { UserRight, VideoBlacklistCreate, VideoBlacklistType } from '../../shared/models' 12import { UserRight, VideoBlacklistCreate, VideoBlacklistType } from '../../shared/models'
13import { UserAdminFlag } from '../../shared/models/users/user-flag.model' 13import { UserAdminFlag } from '../../shared/models/users/user-flag.model'
14import { logger } from '../helpers/logger' 14import { logger, loggerTagsFactory } from '../helpers/logger'
15import { CONFIG } from '../initializers/config' 15import { CONFIG } from '../initializers/config'
16import { VideoBlacklistModel } from '../models/video/video-blacklist' 16import { VideoBlacklistModel } from '../models/video/video-blacklist'
17import { sendDeleteVideo } from './activitypub/send' 17import { sendDeleteVideo } from './activitypub/send'
@@ -20,6 +20,8 @@ import { LiveManager } from './live-manager'
20import { Notifier } from './notifier' 20import { Notifier } from './notifier'
21import { Hooks } from './plugins/hooks' 21import { Hooks } from './plugins/hooks'
22 22
23const lTags = loggerTagsFactory('blacklist')
24
23async function autoBlacklistVideoIfNeeded (parameters: { 25async function autoBlacklistVideoIfNeeded (parameters: {
24 video: MVideoWithBlacklistLight 26 video: MVideoWithBlacklistLight
25 user?: MUser 27 user?: MUser
@@ -60,7 +62,7 @@ async function autoBlacklistVideoIfNeeded (parameters: {
60 }) 62 })
61 } 63 }
62 64
63 logger.info('Video %s auto-blacklisted.', video.uuid) 65 logger.info('Video %s auto-blacklisted.', video.uuid, lTags(video.uuid))
64 66
65 return true 67 return true
66} 68}
diff --git a/server/lib/video-channel.ts b/server/lib/video-channel.ts
index 49bdf4869..0476cb2d5 100644
--- a/server/lib/video-channel.ts
+++ b/server/lib/video-channel.ts
@@ -3,18 +3,12 @@ import { v4 as uuidv4 } from 'uuid'
3import { VideoChannelCreate } from '../../shared/models' 3import { VideoChannelCreate } from '../../shared/models'
4import { VideoModel } from '../models/video/video' 4import { VideoModel } from '../models/video/video'
5import { VideoChannelModel } from '../models/video/video-channel' 5import { VideoChannelModel } from '../models/video/video-channel'
6import { MAccountId, MChannelDefault, MChannelId } from '../types/models' 6import { MAccountId, MChannelId } from '../types/models'
7import { buildActorInstance } from './activitypub/actor' 7import { buildActorInstance } from './activitypub/actor'
8import { getLocalVideoChannelActivityPubUrl } from './activitypub/url' 8import { getLocalVideoChannelActivityPubUrl } from './activitypub/url'
9import { federateVideoIfNeeded } from './activitypub/videos' 9import { federateVideoIfNeeded } from './activitypub/videos'
10 10
11type CustomVideoChannelModelAccount <T extends MAccountId> = MChannelDefault & { Account?: T } 11async function createLocalVideoChannel (videoChannelInfo: VideoChannelCreate, account: MAccountId, t: Sequelize.Transaction) {
12
13async function createLocalVideoChannel <T extends MAccountId> (
14 videoChannelInfo: VideoChannelCreate,
15 account: T,
16 t: Sequelize.Transaction
17): Promise<CustomVideoChannelModelAccount<T>> {
18 const uuid = uuidv4() 12 const uuid = uuidv4()
19 const url = getLocalVideoChannelActivityPubUrl(videoChannelInfo.name) 13 const url = getLocalVideoChannelActivityPubUrl(videoChannelInfo.name)
20 const actorInstance = buildActorInstance('Group', url, videoChannelInfo.name, uuid) 14 const actorInstance = buildActorInstance('Group', url, videoChannelInfo.name, uuid)
@@ -32,13 +26,11 @@ async function createLocalVideoChannel <T extends MAccountId> (
32 const videoChannel = new VideoChannelModel(videoChannelData) 26 const videoChannel = new VideoChannelModel(videoChannelData)
33 27
34 const options = { transaction: t } 28 const options = { transaction: t }
35 const videoChannelCreated: CustomVideoChannelModelAccount<T> = await videoChannel.save(options) as MChannelDefault 29 const videoChannelCreated = await videoChannel.save(options)
36 30
37 // Do not forget to add Account/Actor information to the created video channel
38 videoChannelCreated.Account = account
39 videoChannelCreated.Actor = actorInstanceCreated 31 videoChannelCreated.Actor = actorInstanceCreated
40 32
41 // No need to seed this empty video channel to followers 33 // No need to send this empty video channel to followers
42 return videoChannelCreated 34 return videoChannelCreated
43} 35}
44 36
diff --git a/server/middlewares/oauth.ts b/server/middlewares/auth.ts
index 280595acc..f38373624 100644
--- a/server/middlewares/oauth.ts
+++ b/server/middlewares/auth.ts
@@ -1,15 +1,19 @@
1import * as express from 'express' 1import * as express from 'express'
2import { Socket } from 'socket.io' 2import { Socket } from 'socket.io'
3import { oAuthServer } from '@server/lib/auth' 3import { getAccessToken } from '@server/lib/auth/oauth-model'
4import { logger } from '../helpers/logger'
5import { getAccessToken } from '../lib/oauth-model'
6import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' 4import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
5import { logger } from '../helpers/logger'
6import { handleOAuthAuthenticate } from '../lib/auth/oauth'
7 7
8function authenticate (req: express.Request, res: express.Response, next: express.NextFunction, authenticateInQuery = false) { 8function authenticate (req: express.Request, res: express.Response, next: express.NextFunction, authenticateInQuery = false) {
9 const options = authenticateInQuery ? { allowBearerTokensInQueryString: true } : {} 9 handleOAuthAuthenticate(req, res, authenticateInQuery)
10 .then((token: any) => {
11 res.locals.oauth = { token }
12 res.locals.authenticated = true
10 13
11 oAuthServer.authenticate(options)(req, res, err => { 14 return next()
12 if (err) { 15 })
16 .catch(err => {
13 logger.warn('Cannot authenticate.', { err }) 17 logger.warn('Cannot authenticate.', { err })
14 18
15 return res.status(err.status) 19 return res.status(err.status)
@@ -17,13 +21,7 @@ function authenticate (req: express.Request, res: express.Response, next: expres
17 error: 'Token is invalid.', 21 error: 'Token is invalid.',
18 code: err.name 22 code: err.name
19 }) 23 })
20 .end() 24 })
21 }
22
23 res.locals.authenticated = true
24
25 return next()
26 })
27} 25}
28 26
29function authenticateSocket (socket: Socket, next: (err?: any) => void) { 27function authenticateSocket (socket: Socket, next: (err?: any) => void) {
diff --git a/server/middlewares/index.ts b/server/middlewares/index.ts
index b758a8586..3e280e16f 100644
--- a/server/middlewares/index.ts
+++ b/server/middlewares/index.ts
@@ -1,7 +1,7 @@
1export * from './validators' 1export * from './validators'
2export * from './activitypub' 2export * from './activitypub'
3export * from './async' 3export * from './async'
4export * from './oauth' 4export * from './auth'
5export * from './pagination' 5export * from './pagination'
6export * from './servers' 6export * from './servers'
7export * from './sort' 7export * from './sort'
diff --git a/server/middlewares/validators/activitypub/signature.ts b/server/middlewares/validators/activitypub/signature.ts
index 02b191480..7c4e49463 100644
--- a/server/middlewares/validators/activitypub/signature.ts
+++ b/server/middlewares/validators/activitypub/signature.ts
@@ -23,7 +23,7 @@ const signatureValidator = [
23 .custom(isSignatureValueValid).withMessage('Should have a valid signature value'), 23 .custom(isSignatureValueValid).withMessage('Should have a valid signature value'),
24 24
25 (req: express.Request, res: express.Response, next: express.NextFunction) => { 25 (req: express.Request, res: express.Response, next: express.NextFunction) => {
26 logger.debug('Checking activitypub signature parameter', { parameters: { signature: req.body.signature } }) 26 logger.debug('Checking Linked Data Signature parameter', { parameters: { signature: req.body.signature } })
27 27
28 if (areValidationErrors(req, res)) return 28 if (areValidationErrors(req, res)) return
29 29
diff --git a/server/middlewares/validators/actor-image.ts b/server/middlewares/validators/actor-image.ts
new file mode 100644
index 000000000..961d7a7e5
--- /dev/null
+++ b/server/middlewares/validators/actor-image.ts
@@ -0,0 +1,30 @@
1import * as express from 'express'
2import { body } from 'express-validator'
3import { isActorImageFile } from '@server/helpers/custom-validators/actor-images'
4import { cleanUpReqFiles } from '../../helpers/express-utils'
5import { logger } from '../../helpers/logger'
6import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
7import { areValidationErrors } from './utils'
8
9const updateActorImageValidatorFactory = (fieldname: string) => ([
10 body(fieldname).custom((value, { req }) => isActorImageFile(req.files, fieldname)).withMessage(
11 'This file is not supported or too large. Please, make sure it is of the following type : ' +
12 CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME.join(', ')
13 ),
14
15 (req: express.Request, res: express.Response, next: express.NextFunction) => {
16 logger.debug('Checking updateActorImageValidator parameters', { files: req.files })
17
18 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
19
20 return next()
21 }
22])
23
24const updateAvatarValidator = updateActorImageValidatorFactory('avatarfile')
25const updateBannerValidator = updateActorImageValidatorFactory('bannerfile')
26
27export {
28 updateAvatarValidator,
29 updateBannerValidator
30}
diff --git a/server/middlewares/validators/avatar.ts b/server/middlewares/validators/avatar.ts
deleted file mode 100644
index 2acb97483..000000000
--- a/server/middlewares/validators/avatar.ts
+++ /dev/null
@@ -1,26 +0,0 @@
1import * as express from 'express'
2import { body } from 'express-validator'
3import { isAvatarFile } from '../../helpers/custom-validators/users'
4import { areValidationErrors } from './utils'
5import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
6import { logger } from '../../helpers/logger'
7import { cleanUpReqFiles } from '../../helpers/express-utils'
8
9const updateAvatarValidator = [
10 body('avatarfile').custom((value, { req }) => isAvatarFile(req.files)).withMessage(
11 'This file is not supported or too large. Please, make sure it is of the following type : ' +
12 CONSTRAINTS_FIELDS.ACTORS.AVATAR.EXTNAME.join(', ')
13 ),
14
15 (req: express.Request, res: express.Response, next: express.NextFunction) => {
16 logger.debug('Checking updateAvatarValidator parameters', { files: req.files })
17
18 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
19
20 return next()
21 }
22]
23
24export {
25 updateAvatarValidator
26}
diff --git a/server/middlewares/validators/follows.ts b/server/middlewares/validators/follows.ts
index a590aca99..bb849dc72 100644
--- a/server/middlewares/validators/follows.ts
+++ b/server/middlewares/validators/follows.ts
@@ -68,7 +68,6 @@ const removeFollowingValidator = [
68 .json({ 68 .json({
69 error: `Following ${req.params.host} not found.` 69 error: `Following ${req.params.host} not found.`
70 }) 70 })
71 .end()
72 } 71 }
73 72
74 res.locals.follow = follow 73 res.locals.follow = follow
diff --git a/server/middlewares/validators/index.ts b/server/middlewares/validators/index.ts
index 4086d77aa..24faeea3e 100644
--- a/server/middlewares/validators/index.ts
+++ b/server/middlewares/validators/index.ts
@@ -1,5 +1,6 @@
1export * from './abuse' 1export * from './abuse'
2export * from './account' 2export * from './account'
3export * from './actor-image'
3export * from './blocklist' 4export * from './blocklist'
4export * from './oembed' 5export * from './oembed'
5export * from './activitypub' 6export * from './activitypub'
diff --git a/server/middlewares/validators/jobs.ts b/server/middlewares/validators/jobs.ts
index 99ef25e0a..d87b28c06 100644
--- a/server/middlewares/validators/jobs.ts
+++ b/server/middlewares/validators/jobs.ts
@@ -1,9 +1,11 @@
1import * as express from 'express' 1import * as express from 'express'
2import { param, query } from 'express-validator' 2import { param, query } from 'express-validator'
3import { isValidJobState, isValidJobType } from '../../helpers/custom-validators/jobs' 3import { isValidJobState, isValidJobType } from '../../helpers/custom-validators/jobs'
4import { logger } from '../../helpers/logger' 4import { logger, loggerTagsFactory } from '../../helpers/logger'
5import { areValidationErrors } from './utils' 5import { areValidationErrors } from './utils'
6 6
7const lTags = loggerTagsFactory('validators', 'jobs')
8
7const listJobsValidator = [ 9const listJobsValidator = [
8 param('state') 10 param('state')
9 .optional() 11 .optional()
@@ -14,7 +16,7 @@ const listJobsValidator = [
14 .custom(isValidJobType).withMessage('Should have a valid job state'), 16 .custom(isValidJobType).withMessage('Should have a valid job state'),
15 17
16 (req: express.Request, res: express.Response, next: express.NextFunction) => { 18 (req: express.Request, res: express.Response, next: express.NextFunction) => {
17 logger.debug('Checking listJobsValidator parameters.', { parameters: req.params }) 19 logger.debug('Checking listJobsValidator parameters.', { parameters: req.params, ...lTags() })
18 20
19 if (areValidationErrors(req, res)) return 21 if (areValidationErrors(req, res)) return
20 22
diff --git a/server/middlewares/validators/pagination.ts b/server/middlewares/validators/pagination.ts
index 1cae7848c..6b0a83d80 100644
--- a/server/middlewares/validators/pagination.ts
+++ b/server/middlewares/validators/pagination.ts
@@ -4,25 +4,30 @@ import { logger } from '../../helpers/logger'
4import { areValidationErrors } from './utils' 4import { areValidationErrors } from './utils'
5import { PAGINATION } from '@server/initializers/constants' 5import { PAGINATION } from '@server/initializers/constants'
6 6
7const paginationValidator = [ 7const paginationValidator = paginationValidatorBuilder()
8 query('start')
9 .optional()
10 .isInt({ min: 0 }).withMessage('Should have a number start'),
11 query('count')
12 .optional()
13 .isInt({ min: 0, max: PAGINATION.GLOBAL.COUNT.MAX }).withMessage(`Should have a number count (max: ${PAGINATION.GLOBAL.COUNT.MAX})`),
14 8
15 (req: express.Request, res: express.Response, next: express.NextFunction) => { 9function paginationValidatorBuilder (tags: string[] = []) {
16 logger.debug('Checking pagination parameters', { parameters: req.query }) 10 return [
11 query('start')
12 .optional()
13 .isInt({ min: 0 }).withMessage('Should have a number start'),
14 query('count')
15 .optional()
16 .isInt({ min: 0, max: PAGINATION.GLOBAL.COUNT.MAX }).withMessage(`Should have a number count (max: ${PAGINATION.GLOBAL.COUNT.MAX})`),
17 17
18 if (areValidationErrors(req, res)) return 18 (req: express.Request, res: express.Response, next: express.NextFunction) => {
19 logger.debug('Checking pagination parameters', { parameters: req.query, tags })
19 20
20 return next() 21 if (areValidationErrors(req, res)) return
21 } 22
22] 23 return next()
24 }
25 ]
26}
23 27
24// --------------------------------------------------------------------------- 28// ---------------------------------------------------------------------------
25 29
26export { 30export {
27 paginationValidator 31 paginationValidator,
32 paginationValidatorBuilder
28} 33}
diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts
index e93ceb200..beecc155b 100644
--- a/server/middlewares/validators/sort.ts
+++ b/server/middlewares/validators/sort.ts
@@ -28,7 +28,7 @@ const SORTABLE_VIDEO_REDUNDANCIES_COLUMNS = createSortableColumns(SORTABLE_COLUM
28 28
29const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) 29const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS)
30const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS) 30const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS)
31const jobsSortValidator = checkSort(SORTABLE_JOBS_COLUMNS) 31const jobsSortValidator = checkSort(SORTABLE_JOBS_COLUMNS, [ 'jobs' ])
32const abusesSortValidator = checkSort(SORTABLE_ABUSES_COLUMNS) 32const abusesSortValidator = checkSort(SORTABLE_ABUSES_COLUMNS)
33const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS) 33const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS)
34const videoImportsSortValidator = checkSort(SORTABLE_VIDEO_IMPORTS_COLUMNS) 34const videoImportsSortValidator = checkSort(SORTABLE_VIDEO_IMPORTS_COLUMNS)
diff --git a/server/middlewares/validators/utils.ts b/server/middlewares/validators/utils.ts
index 2899bed6f..4167f6d43 100644
--- a/server/middlewares/validators/utils.ts
+++ b/server/middlewares/validators/utils.ts
@@ -17,12 +17,12 @@ function areValidationErrors (req: express.Request, res: express.Response) {
17 return false 17 return false
18} 18}
19 19
20function checkSort (sortableColumns: string[]) { 20function checkSort (sortableColumns: string[], tags: string[] = []) {
21 return [ 21 return [
22 query('sort').optional().isIn(sortableColumns).withMessage('Should have correct sortable column'), 22 query('sort').optional().isIn(sortableColumns).withMessage('Should have correct sortable column'),
23 23
24 (req: express.Request, res: express.Response, next: express.NextFunction) => { 24 (req: express.Request, res: express.Response, next: express.NextFunction) => {
25 logger.debug('Checking sort parameters', { parameters: req.query }) 25 logger.debug('Checking sort parameters', { parameters: req.query, tags })
26 26
27 if (areValidationErrors(req, res)) return 27 if (areValidationErrors(req, res)) return
28 28
diff --git a/server/middlewares/validators/videos/video-channels.ts b/server/middlewares/validators/videos/video-channels.ts
index 57ac548b9..2463d281c 100644
--- a/server/middlewares/validators/videos/video-channels.ts
+++ b/server/middlewares/validators/videos/video-channels.ts
@@ -73,13 +73,11 @@ const videoChannelsUpdateValidator = [
73 if (res.locals.videoChannel.Actor.isOwned() === false) { 73 if (res.locals.videoChannel.Actor.isOwned() === false) {
74 return res.status(HttpStatusCode.FORBIDDEN_403) 74 return res.status(HttpStatusCode.FORBIDDEN_403)
75 .json({ error: 'Cannot update video channel of another server' }) 75 .json({ error: 'Cannot update video channel of another server' })
76 .end()
77 } 76 }
78 77
79 if (res.locals.videoChannel.Account.userId !== res.locals.oauth.token.User.id) { 78 if (res.locals.videoChannel.Account.userId !== res.locals.oauth.token.User.id) {
80 return res.status(HttpStatusCode.FORBIDDEN_403) 79 return res.status(HttpStatusCode.FORBIDDEN_403)
81 .json({ error: 'Cannot update video channel of another user' }) 80 .json({ error: 'Cannot update video channel of another user' })
82 .end()
83 } 81 }
84 82
85 return next() 83 return next()
diff --git a/server/middlewares/validators/videos/video-comments.ts b/server/middlewares/validators/videos/video-comments.ts
index 226c9d436..1afacfed8 100644
--- a/server/middlewares/validators/videos/video-comments.ts
+++ b/server/middlewares/validators/videos/video-comments.ts
@@ -216,7 +216,7 @@ async function isVideoCommentAccepted (req: express.Request, res: express.Respon
216 if (!acceptedResult || acceptedResult.accepted !== true) { 216 if (!acceptedResult || acceptedResult.accepted !== true) {
217 logger.info('Refused local comment.', { acceptedResult, acceptParameters }) 217 logger.info('Refused local comment.', { acceptedResult, acceptParameters })
218 res.status(HttpStatusCode.FORBIDDEN_403) 218 res.status(HttpStatusCode.FORBIDDEN_403)
219 .json({ error: acceptedResult.errorMessage || 'Refused local comment' }) 219 .json({ error: acceptedResult?.errorMessage || 'Refused local comment' })
220 220
221 return false 221 return false
222 } 222 }
diff --git a/server/middlewares/validators/videos/video-playlists.ts b/server/middlewares/validators/videos/video-playlists.ts
index 0fba4f5fd..c872d045e 100644
--- a/server/middlewares/validators/videos/video-playlists.ts
+++ b/server/middlewares/validators/videos/video-playlists.ts
@@ -29,7 +29,7 @@ import { doesVideoChannelIdExist, doesVideoExist, doesVideoPlaylistExist, VideoP
29import { CONSTRAINTS_FIELDS } from '../../../initializers/constants' 29import { CONSTRAINTS_FIELDS } from '../../../initializers/constants'
30import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-element' 30import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-element'
31import { MVideoPlaylist } from '../../../types/models/video/video-playlist' 31import { MVideoPlaylist } from '../../../types/models/video/video-playlist'
32import { authenticatePromiseIfNeeded } from '../../oauth' 32import { authenticatePromiseIfNeeded } from '../../auth'
33import { areValidationErrors } from '../utils' 33import { areValidationErrors } from '../utils'
34 34
35const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([ 35const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts
index 37cc07b94..4d31d3dcb 100644
--- a/server/middlewares/validators/videos/videos.ts
+++ b/server/middlewares/validators/videos/videos.ts
@@ -54,7 +54,7 @@ import { isLocalVideoAccepted } from '../../../lib/moderation'
54import { Hooks } from '../../../lib/plugins/hooks' 54import { Hooks } from '../../../lib/plugins/hooks'
55import { AccountModel } from '../../../models/account/account' 55import { AccountModel } from '../../../models/account/account'
56import { VideoModel } from '../../../models/video/video' 56import { VideoModel } from '../../../models/video/video'
57import { authenticatePromiseIfNeeded } from '../../oauth' 57import { authenticatePromiseIfNeeded } from '../../auth'
58import { areValidationErrors } from '../utils' 58import { areValidationErrors } from '../utils'
59 59
60const videosAddValidator = getCommonVideoEditAttributes().concat([ 60const videosAddValidator = getCommonVideoEditAttributes().concat([
diff --git a/server/models/account/account.ts b/server/models/account/account.ts
index c72f9c63d..312451abe 100644
--- a/server/models/account/account.ts
+++ b/server/models/account/account.ts
@@ -33,7 +33,7 @@ import {
33import { ActorModel } from '../activitypub/actor' 33import { ActorModel } from '../activitypub/actor'
34import { ActorFollowModel } from '../activitypub/actor-follow' 34import { ActorFollowModel } from '../activitypub/actor-follow'
35import { ApplicationModel } from '../application/application' 35import { ApplicationModel } from '../application/application'
36import { AvatarModel } from '../avatar/avatar' 36import { ActorImageModel } from './actor-image'
37import { ServerModel } from '../server/server' 37import { ServerModel } from '../server/server'
38import { ServerBlocklistModel } from '../server/server-blocklist' 38import { ServerBlocklistModel } from '../server/server-blocklist'
39import { getSort, throwIfNotValid } from '../utils' 39import { getSort, throwIfNotValid } from '../utils'
@@ -82,7 +82,8 @@ export type SummaryOptions = {
82 serverInclude, 82 serverInclude,
83 83
84 { 84 {
85 model: AvatarModel.unscoped(), 85 model: ActorImageModel.unscoped(),
86 as: 'Avatar',
86 required: false 87 required: false
87 } 88 }
88 ] 89 ]
diff --git a/server/models/account/actor-image.ts b/server/models/account/actor-image.ts
new file mode 100644
index 000000000..ae05b4969
--- /dev/null
+++ b/server/models/account/actor-image.ts
@@ -0,0 +1,100 @@
1import { remove } from 'fs-extra'
2import { join } from 'path'
3import { AfterDestroy, AllowNull, Column, CreatedAt, Default, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
4import { MActorImageFormattable } from '@server/types/models'
5import { ActorImageType } from '@shared/models'
6import { ActorImage } from '../../../shared/models/actors/actor-image.model'
7import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
8import { logger } from '../../helpers/logger'
9import { CONFIG } from '../../initializers/config'
10import { LAZY_STATIC_PATHS } from '../../initializers/constants'
11import { throwIfNotValid } from '../utils'
12
13@Table({
14 tableName: 'actorImage',
15 indexes: [
16 {
17 fields: [ 'filename' ],
18 unique: true
19 }
20 ]
21})
22export class ActorImageModel extends Model {
23
24 @AllowNull(false)
25 @Column
26 filename: string
27
28 @AllowNull(true)
29 @Default(null)
30 @Column
31 height: number
32
33 @AllowNull(true)
34 @Default(null)
35 @Column
36 width: number
37
38 @AllowNull(true)
39 @Is('ActorImageFileUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'fileUrl', true))
40 @Column
41 fileUrl: string
42
43 @AllowNull(false)
44 @Column
45 onDisk: boolean
46
47 @AllowNull(false)
48 @Column
49 type: ActorImageType
50
51 @CreatedAt
52 createdAt: Date
53
54 @UpdatedAt
55 updatedAt: Date
56
57 @AfterDestroy
58 static removeFilesAndSendDelete (instance: ActorImageModel) {
59 logger.info('Removing actor image file %s.', instance.filename)
60
61 // Don't block the transaction
62 instance.removeImage()
63 .catch(err => logger.error('Cannot remove actor image file %s.', instance.filename, err))
64 }
65
66 static loadByName (filename: string) {
67 const query = {
68 where: {
69 filename
70 }
71 }
72
73 return ActorImageModel.findOne(query)
74 }
75
76 toFormattedJSON (this: MActorImageFormattable): ActorImage {
77 return {
78 path: this.getStaticPath(),
79 createdAt: this.createdAt,
80 updatedAt: this.updatedAt
81 }
82 }
83
84 getStaticPath () {
85 if (this.type === ActorImageType.AVATAR) {
86 return join(LAZY_STATIC_PATHS.AVATARS, this.filename)
87 }
88
89 return join(LAZY_STATIC_PATHS.BANNERS, this.filename)
90 }
91
92 getPath () {
93 return join(CONFIG.STORAGE.ACTOR_IMAGES, this.filename)
94 }
95
96 removeImage () {
97 const imagePath = join(CONFIG.STORAGE.ACTOR_IMAGES, this.filename)
98 return remove(imagePath)
99 }
100}
diff --git a/server/models/account/user-notification-setting.ts b/server/models/account/user-notification-setting.ts
index ebab8b6d2..138051528 100644
--- a/server/models/account/user-notification-setting.ts
+++ b/server/models/account/user-notification-setting.ts
@@ -12,10 +12,10 @@ import {
12 Table, 12 Table,
13 UpdatedAt 13 UpdatedAt
14} from 'sequelize-typescript' 14} from 'sequelize-typescript'
15import { TokensCache } from '@server/lib/auth/tokens-cache'
15import { MNotificationSettingFormattable } from '@server/types/models' 16import { MNotificationSettingFormattable } from '@server/types/models'
16import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' 17import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model'
17import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' 18import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications'
18import { clearCacheByUserId } from '../../lib/oauth-model'
19import { throwIfNotValid } from '../utils' 19import { throwIfNotValid } from '../utils'
20import { UserModel } from './user' 20import { UserModel } from './user'
21 21
@@ -156,6 +156,24 @@ export class UserNotificationSettingModel extends Model {
156 @Column 156 @Column
157 abuseNewMessage: UserNotificationSettingValue 157 abuseNewMessage: UserNotificationSettingValue
158 158
159 @AllowNull(false)
160 @Default(null)
161 @Is(
162 'UserNotificationSettingNewPeerTubeVersion',
163 value => throwIfNotValid(value, isUserNotificationSettingValid, 'newPeerTubeVersion')
164 )
165 @Column
166 newPeerTubeVersion: UserNotificationSettingValue
167
168 @AllowNull(false)
169 @Default(null)
170 @Is(
171 'UserNotificationSettingNewPeerPluginVersion',
172 value => throwIfNotValid(value, isUserNotificationSettingValid, 'newPluginVersion')
173 )
174 @Column
175 newPluginVersion: UserNotificationSettingValue
176
159 @ForeignKey(() => UserModel) 177 @ForeignKey(() => UserModel)
160 @Column 178 @Column
161 userId: number 179 userId: number
@@ -177,7 +195,7 @@ export class UserNotificationSettingModel extends Model {
177 @AfterUpdate 195 @AfterUpdate
178 @AfterDestroy 196 @AfterDestroy
179 static removeTokenCache (instance: UserNotificationSettingModel) { 197 static removeTokenCache (instance: UserNotificationSettingModel) {
180 return clearCacheByUserId(instance.userId) 198 return TokensCache.Instance.clearCacheByUserId(instance.userId)
181 } 199 }
182 200
183 toFormattedJSON (this: MNotificationSettingFormattable): UserNotificationSetting { 201 toFormattedJSON (this: MNotificationSettingFormattable): UserNotificationSetting {
@@ -195,7 +213,9 @@ export class UserNotificationSettingModel extends Model {
195 newInstanceFollower: this.newInstanceFollower, 213 newInstanceFollower: this.newInstanceFollower,
196 autoInstanceFollowing: this.autoInstanceFollowing, 214 autoInstanceFollowing: this.autoInstanceFollowing,
197 abuseNewMessage: this.abuseNewMessage, 215 abuseNewMessage: this.abuseNewMessage,
198 abuseStateChange: this.abuseStateChange 216 abuseStateChange: this.abuseStateChange,
217 newPeerTubeVersion: this.newPeerTubeVersion,
218 newPluginVersion: this.newPluginVersion
199 } 219 }
200 } 220 }
201} 221}
diff --git a/server/models/account/user-notification.ts b/server/models/account/user-notification.ts
index add129644..805095002 100644
--- a/server/models/account/user-notification.ts
+++ b/server/models/account/user-notification.ts
@@ -9,7 +9,8 @@ import { VideoAbuseModel } from '../abuse/video-abuse'
9import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' 9import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse'
10import { ActorModel } from '../activitypub/actor' 10import { ActorModel } from '../activitypub/actor'
11import { ActorFollowModel } from '../activitypub/actor-follow' 11import { ActorFollowModel } from '../activitypub/actor-follow'
12import { AvatarModel } from '../avatar/avatar' 12import { ApplicationModel } from '../application/application'
13import { PluginModel } from '../server/plugin'
13import { ServerModel } from '../server/server' 14import { ServerModel } from '../server/server'
14import { getSort, throwIfNotValid } from '../utils' 15import { getSort, throwIfNotValid } from '../utils'
15import { VideoModel } from '../video/video' 16import { VideoModel } from '../video/video'
@@ -18,6 +19,7 @@ import { VideoChannelModel } from '../video/video-channel'
18import { VideoCommentModel } from '../video/video-comment' 19import { VideoCommentModel } from '../video/video-comment'
19import { VideoImportModel } from '../video/video-import' 20import { VideoImportModel } from '../video/video-import'
20import { AccountModel } from './account' 21import { AccountModel } from './account'
22import { ActorImageModel } from './actor-image'
21import { UserModel } from './user' 23import { UserModel } from './user'
22 24
23enum ScopeNames { 25enum ScopeNames {
@@ -32,7 +34,8 @@ function buildActorWithAvatarInclude () {
32 include: [ 34 include: [
33 { 35 {
34 attributes: [ 'filename' ], 36 attributes: [ 'filename' ],
35 model: AvatarModel.unscoped(), 37 as: 'Avatar',
38 model: ActorImageModel.unscoped(),
36 required: false 39 required: false
37 }, 40 },
38 { 41 {
@@ -96,7 +99,7 @@ function buildAccountInclude (required: boolean, withActor = false) {
96 attributes: [ 'id' ], 99 attributes: [ 'id' ],
97 model: VideoAbuseModel.unscoped(), 100 model: VideoAbuseModel.unscoped(),
98 required: false, 101 required: false,
99 include: [ buildVideoInclude(true) ] 102 include: [ buildVideoInclude(false) ]
100 }, 103 },
101 { 104 {
102 attributes: [ 'id' ], 105 attributes: [ 'id' ],
@@ -106,12 +109,12 @@ function buildAccountInclude (required: boolean, withActor = false) {
106 { 109 {
107 attributes: [ 'id', 'originCommentId' ], 110 attributes: [ 'id', 'originCommentId' ],
108 model: VideoCommentModel.unscoped(), 111 model: VideoCommentModel.unscoped(),
109 required: true, 112 required: false,
110 include: [ 113 include: [
111 { 114 {
112 attributes: [ 'id', 'name', 'uuid' ], 115 attributes: [ 'id', 'name', 'uuid' ],
113 model: VideoModel.unscoped(), 116 model: VideoModel.unscoped(),
114 required: true 117 required: false
115 } 118 }
116 ] 119 ]
117 } 120 }
@@ -120,7 +123,7 @@ function buildAccountInclude (required: boolean, withActor = false) {
120 { 123 {
121 model: AccountModel, 124 model: AccountModel,
122 as: 'FlaggedAccount', 125 as: 'FlaggedAccount',
123 required: true, 126 required: false,
124 include: [ buildActorWithAvatarInclude() ] 127 include: [ buildActorWithAvatarInclude() ]
125 } 128 }
126 ] 129 ]
@@ -141,6 +144,18 @@ function buildAccountInclude (required: boolean, withActor = false) {
141 }, 144 },
142 145
143 { 146 {
147 attributes: [ 'id', 'name', 'type', 'latestVersion' ],
148 model: PluginModel.unscoped(),
149 required: false
150 },
151
152 {
153 attributes: [ 'id', 'latestPeerTubeVersion' ],
154 model: ApplicationModel.unscoped(),
155 required: false
156 },
157
158 {
144 attributes: [ 'id', 'state' ], 159 attributes: [ 'id', 'state' ],
145 model: ActorFollowModel.unscoped(), 160 model: ActorFollowModel.unscoped(),
146 required: false, 161 required: false,
@@ -158,7 +173,8 @@ function buildAccountInclude (required: boolean, withActor = false) {
158 }, 173 },
159 { 174 {
160 attributes: [ 'filename' ], 175 attributes: [ 'filename' ],
161 model: AvatarModel.unscoped(), 176 as: 'Avatar',
177 model: ActorImageModel.unscoped(),
162 required: false 178 required: false
163 }, 179 },
164 { 180 {
@@ -251,6 +267,22 @@ function buildAccountInclude (required: boolean, withActor = false) {
251 [Op.ne]: null 267 [Op.ne]: null
252 } 268 }
253 } 269 }
270 },
271 {
272 fields: [ 'pluginId' ],
273 where: {
274 pluginId: {
275 [Op.ne]: null
276 }
277 }
278 },
279 {
280 fields: [ 'applicationId' ],
281 where: {
282 applicationId: {
283 [Op.ne]: null
284 }
285 }
254 } 286 }
255 ] as (ModelIndexesOptions & { where?: WhereOptions })[] 287 ] as (ModelIndexesOptions & { where?: WhereOptions })[]
256}) 288})
@@ -370,6 +402,30 @@ export class UserNotificationModel extends Model {
370 }) 402 })
371 ActorFollow: ActorFollowModel 403 ActorFollow: ActorFollowModel
372 404
405 @ForeignKey(() => PluginModel)
406 @Column
407 pluginId: number
408
409 @BelongsTo(() => PluginModel, {
410 foreignKey: {
411 allowNull: true
412 },
413 onDelete: 'cascade'
414 })
415 Plugin: PluginModel
416
417 @ForeignKey(() => ApplicationModel)
418 @Column
419 applicationId: number
420
421 @BelongsTo(() => ApplicationModel, {
422 foreignKey: {
423 allowNull: true
424 },
425 onDelete: 'cascade'
426 })
427 Application: ApplicationModel
428
373 static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) { 429 static listForApi (userId: number, start: number, count: number, sort: string, unread?: boolean) {
374 const where = { userId } 430 const where = { userId }
375 431
@@ -524,6 +580,18 @@ export class UserNotificationModel extends Model {
524 } 580 }
525 : undefined 581 : undefined
526 582
583 const plugin = this.Plugin
584 ? {
585 name: this.Plugin.name,
586 type: this.Plugin.type,
587 latestVersion: this.Plugin.latestVersion
588 }
589 : undefined
590
591 const peertube = this.Application
592 ? { latestVersion: this.Application.latestPeerTubeVersion }
593 : undefined
594
527 return { 595 return {
528 id: this.id, 596 id: this.id,
529 type: this.type, 597 type: this.type,
@@ -535,6 +603,8 @@ export class UserNotificationModel extends Model {
535 videoBlacklist, 603 videoBlacklist,
536 account, 604 account,
537 actorFollow, 605 actorFollow,
606 plugin,
607 peertube,
538 createdAt: this.createdAt.toISOString(), 608 createdAt: this.createdAt.toISOString(),
539 updatedAt: this.updatedAt.toISOString() 609 updatedAt: this.updatedAt.toISOString()
540 } 610 }
@@ -553,17 +623,19 @@ export class UserNotificationModel extends Model {
553 ? { 623 ? {
554 threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(), 624 threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(),
555 625
556 video: { 626 video: abuse.VideoCommentAbuse.VideoComment.Video
557 id: abuse.VideoCommentAbuse.VideoComment.Video.id, 627 ? {
558 name: abuse.VideoCommentAbuse.VideoComment.Video.name, 628 id: abuse.VideoCommentAbuse.VideoComment.Video.id,
559 uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid 629 name: abuse.VideoCommentAbuse.VideoComment.Video.name,
560 } 630 uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid
631 }
632 : undefined
561 } 633 }
562 : undefined 634 : undefined
563 635
564 const videoAbuse = abuse.VideoAbuse?.Video ? this.formatVideo(abuse.VideoAbuse.Video) : undefined 636 const videoAbuse = abuse.VideoAbuse?.Video ? this.formatVideo(abuse.VideoAbuse.Video) : undefined
565 637
566 const accountAbuse = (!commentAbuse && !videoAbuse) ? this.formatActor(abuse.FlaggedAccount) : undefined 638 const accountAbuse = (!commentAbuse && !videoAbuse && abuse.FlaggedAccount) ? this.formatActor(abuse.FlaggedAccount) : undefined
567 639
568 return { 640 return {
569 id: abuse.id, 641 id: abuse.id,
diff --git a/server/models/account/user.ts b/server/models/account/user.ts
index c1f22b76a..00c6d73aa 100644
--- a/server/models/account/user.ts
+++ b/server/models/account/user.ts
@@ -21,6 +21,7 @@ import {
21 Table, 21 Table,
22 UpdatedAt 22 UpdatedAt
23} from 'sequelize-typescript' 23} from 'sequelize-typescript'
24import { TokensCache } from '@server/lib/auth/tokens-cache'
24import { 25import {
25 MMyUserFormattable, 26 MMyUserFormattable,
26 MUser, 27 MUser,
@@ -58,7 +59,6 @@ import {
58} from '../../helpers/custom-validators/users' 59} from '../../helpers/custom-validators/users'
59import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' 60import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto'
60import { DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants' 61import { DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants'
61import { clearCacheByUserId } from '../../lib/oauth-model'
62import { getThemeOrDefault } from '../../lib/plugins/theme-utils' 62import { getThemeOrDefault } from '../../lib/plugins/theme-utils'
63import { ActorModel } from '../activitypub/actor' 63import { ActorModel } from '../activitypub/actor'
64import { ActorFollowModel } from '../activitypub/actor-follow' 64import { ActorFollowModel } from '../activitypub/actor-follow'
@@ -71,6 +71,7 @@ import { VideoLiveModel } from '../video/video-live'
71import { VideoPlaylistModel } from '../video/video-playlist' 71import { VideoPlaylistModel } from '../video/video-playlist'
72import { AccountModel } from './account' 72import { AccountModel } from './account'
73import { UserNotificationSettingModel } from './user-notification-setting' 73import { UserNotificationSettingModel } from './user-notification-setting'
74import { ActorImageModel } from './actor-image'
74 75
75enum ScopeNames { 76enum ScopeNames {
76 FOR_ME_API = 'FOR_ME_API', 77 FOR_ME_API = 'FOR_ME_API',
@@ -97,7 +98,20 @@ enum ScopeNames {
97 model: AccountModel, 98 model: AccountModel,
98 include: [ 99 include: [
99 { 100 {
100 model: VideoChannelModel 101 model: VideoChannelModel.unscoped(),
102 include: [
103 {
104 model: ActorModel,
105 required: true,
106 include: [
107 {
108 model: ActorImageModel,
109 as: 'Banner',
110 required: false
111 }
112 ]
113 }
114 ]
101 }, 115 },
102 { 116 {
103 attributes: [ 'id', 'name', 'type' ], 117 attributes: [ 'id', 'name', 'type' ],
@@ -411,7 +425,7 @@ export class UserModel extends Model {
411 @AfterUpdate 425 @AfterUpdate
412 @AfterDestroy 426 @AfterDestroy
413 static removeTokenCache (instance: UserModel) { 427 static removeTokenCache (instance: UserModel) {
414 return clearCacheByUserId(instance.id) 428 return TokensCache.Instance.clearCacheByUserId(instance.id)
415 } 429 }
416 430
417 static countTotal () { 431 static countTotal () {
diff --git a/server/models/activitypub/actor-follow.ts b/server/models/activitypub/actor-follow.ts
index ce6a4e267..4c5f37620 100644
--- a/server/models/activitypub/actor-follow.ts
+++ b/server/models/activitypub/actor-follow.ts
@@ -248,13 +248,6 @@ export class ActorFollowModel extends Model {
248 } 248 }
249 249
250 return ActorFollowModel.findOne(query) 250 return ActorFollowModel.findOne(query)
251 .then(result => {
252 if (result?.ActorFollowing.VideoChannel) {
253 result.ActorFollowing.VideoChannel.Actor = result.ActorFollowing
254 }
255
256 return result
257 })
258 } 251 }
259 252
260 static listSubscribedIn (actorId: number, targets: { name: string, host?: string }[]): Promise<MActorFollowFollowingHost[]> { 253 static listSubscribedIn (actorId: number, targets: { name: string, host?: string }[]): Promise<MActorFollowFollowingHost[]> {
diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts
index 3b98e8841..a6c724f26 100644
--- a/server/models/activitypub/actor.ts
+++ b/server/models/activitypub/actor.ts
@@ -19,7 +19,7 @@ import {
19} from 'sequelize-typescript' 19} from 'sequelize-typescript'
20import { ModelCache } from '@server/models/model-cache' 20import { ModelCache } from '@server/models/model-cache'
21import { ActivityIconObject, ActivityPubActorType } from '../../../shared/models/activitypub' 21import { ActivityIconObject, ActivityPubActorType } from '../../../shared/models/activitypub'
22import { Avatar } from '../../../shared/models/avatars/avatar.model' 22import { ActorImage } from '../../../shared/models/actors/actor-image.model'
23import { activityPubContextify } from '../../helpers/activitypub' 23import { activityPubContextify } from '../../helpers/activitypub'
24import { 24import {
25 isActorFollowersCountValid, 25 isActorFollowersCountValid,
@@ -29,11 +29,19 @@ import {
29 isActorPublicKeyValid 29 isActorPublicKeyValid
30} from '../../helpers/custom-validators/activitypub/actor' 30} from '../../helpers/custom-validators/activitypub/actor'
31import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 31import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
32import { ACTIVITY_PUB, ACTIVITY_PUB_ACTOR_TYPES, CONSTRAINTS_FIELDS, SERVER_ACTOR_NAME, WEBSERVER } from '../../initializers/constants' 32import {
33 ACTIVITY_PUB,
34 ACTIVITY_PUB_ACTOR_TYPES,
35 CONSTRAINTS_FIELDS,
36 MIMETYPES,
37 SERVER_ACTOR_NAME,
38 WEBSERVER
39} from '../../initializers/constants'
33import { 40import {
34 MActor, 41 MActor,
35 MActorAccountChannelId, 42 MActorAccountChannelId,
36 MActorAP, 43 MActorAPAccount,
44 MActorAPChannel,
37 MActorFormattable, 45 MActorFormattable,
38 MActorFull, 46 MActorFull,
39 MActorHost, 47 MActorHost,
@@ -43,7 +51,7 @@ import {
43 MActorWithInboxes 51 MActorWithInboxes
44} from '../../types/models' 52} from '../../types/models'
45import { AccountModel } from '../account/account' 53import { AccountModel } from '../account/account'
46import { AvatarModel } from '../avatar/avatar' 54import { ActorImageModel } from '../account/actor-image'
47import { ServerModel } from '../server/server' 55import { ServerModel } from '../server/server'
48import { isOutdated, throwIfNotValid } from '../utils' 56import { isOutdated, throwIfNotValid } from '../utils'
49import { VideoModel } from '../video/video' 57import { VideoModel } from '../video/video'
@@ -73,7 +81,8 @@ export const unusedActorAttributesForAPI = [
73 required: false 81 required: false
74 }, 82 },
75 { 83 {
76 model: AvatarModel, 84 model: ActorImageModel,
85 as: 'Avatar',
77 required: false 86 required: false
78 } 87 }
79 ] 88 ]
@@ -100,7 +109,13 @@ export const unusedActorAttributesForAPI = [
100 required: false 109 required: false
101 }, 110 },
102 { 111 {
103 model: AvatarModel, 112 model: ActorImageModel,
113 as: 'Avatar',
114 required: false
115 },
116 {
117 model: ActorImageModel,
118 as: 'Banner',
104 required: false 119 required: false
105 } 120 }
106 ] 121 ]
@@ -213,18 +228,35 @@ export class ActorModel extends Model {
213 @UpdatedAt 228 @UpdatedAt
214 updatedAt: Date 229 updatedAt: Date
215 230
216 @ForeignKey(() => AvatarModel) 231 @ForeignKey(() => ActorImageModel)
217 @Column 232 @Column
218 avatarId: number 233 avatarId: number
219 234
220 @BelongsTo(() => AvatarModel, { 235 @ForeignKey(() => ActorImageModel)
236 @Column
237 bannerId: number
238
239 @BelongsTo(() => ActorImageModel, {
221 foreignKey: { 240 foreignKey: {
241 name: 'avatarId',
222 allowNull: true 242 allowNull: true
223 }, 243 },
244 as: 'Avatar',
224 onDelete: 'set null', 245 onDelete: 'set null',
225 hooks: true 246 hooks: true
226 }) 247 })
227 Avatar: AvatarModel 248 Avatar: ActorImageModel
249
250 @BelongsTo(() => ActorImageModel, {
251 foreignKey: {
252 name: 'bannerId',
253 allowNull: true
254 },
255 as: 'Banner',
256 onDelete: 'set null',
257 hooks: true
258 })
259 Banner: ActorImageModel
228 260
229 @HasMany(() => ActorFollowModel, { 261 @HasMany(() => ActorFollowModel, {
230 foreignKey: { 262 foreignKey: {
@@ -496,7 +528,7 @@ export class ActorModel extends Model {
496 } 528 }
497 529
498 toFormattedSummaryJSON (this: MActorSummaryFormattable) { 530 toFormattedSummaryJSON (this: MActorSummaryFormattable) {
499 let avatar: Avatar = null 531 let avatar: ActorImage = null
500 if (this.Avatar) { 532 if (this.Avatar) {
501 avatar = this.Avatar.toFormattedJSON() 533 avatar = this.Avatar.toFormattedJSON()
502 } 534 }
@@ -512,29 +544,51 @@ export class ActorModel extends Model {
512 toFormattedJSON (this: MActorFormattable) { 544 toFormattedJSON (this: MActorFormattable) {
513 const base = this.toFormattedSummaryJSON() 545 const base = this.toFormattedSummaryJSON()
514 546
547 let banner: ActorImage = null
548 if (this.bannerId) {
549 banner = this.Banner.toFormattedJSON()
550 }
551
515 return Object.assign(base, { 552 return Object.assign(base, {
516 id: this.id, 553 id: this.id,
517 hostRedundancyAllowed: this.getRedundancyAllowed(), 554 hostRedundancyAllowed: this.getRedundancyAllowed(),
518 followingCount: this.followingCount, 555 followingCount: this.followingCount,
519 followersCount: this.followersCount, 556 followersCount: this.followersCount,
557 banner,
520 createdAt: this.createdAt, 558 createdAt: this.createdAt,
521 updatedAt: this.updatedAt 559 updatedAt: this.updatedAt
522 }) 560 })
523 } 561 }
524 562
525 toActivityPubObject (this: MActorAP, name: string) { 563 toActivityPubObject (this: MActorAPChannel | MActorAPAccount, name: string) {
526 let icon: ActivityIconObject 564 let icon: ActivityIconObject
565 let image: ActivityIconObject
527 566
528 if (this.avatarId) { 567 if (this.avatarId) {
529 const extension = extname(this.Avatar.filename) 568 const extension = extname(this.Avatar.filename)
530 569
531 icon = { 570 icon = {
532 type: 'Image', 571 type: 'Image',
533 mediaType: extension === '.png' ? 'image/png' : 'image/jpeg', 572 mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension],
573 height: this.Avatar.height,
574 width: this.Avatar.width,
534 url: this.getAvatarUrl() 575 url: this.getAvatarUrl()
535 } 576 }
536 } 577 }
537 578
579 if (this.bannerId) {
580 const banner = (this as MActorAPChannel).Banner
581 const extension = extname(banner.filename)
582
583 image = {
584 type: 'Image',
585 mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension],
586 height: banner.height,
587 width: banner.width,
588 url: this.getBannerUrl()
589 }
590 }
591
538 const json = { 592 const json = {
539 type: this.type, 593 type: this.type,
540 id: this.url, 594 id: this.url,
@@ -554,7 +608,8 @@ export class ActorModel extends Model {
554 owner: this.url, 608 owner: this.url,
555 publicKeyPem: this.publicKey 609 publicKeyPem: this.publicKey
556 }, 610 },
557 icon 611 icon,
612 image
558 } 613 }
559 614
560 return activityPubContextify(json) 615 return activityPubContextify(json)
@@ -624,6 +679,12 @@ export class ActorModel extends Model {
624 return WEBSERVER.URL + this.Avatar.getStaticPath() 679 return WEBSERVER.URL + this.Avatar.getStaticPath()
625 } 680 }
626 681
682 getBannerUrl () {
683 if (!this.bannerId) return undefined
684
685 return WEBSERVER.URL + this.Banner.getStaticPath()
686 }
687
627 isOutdated () { 688 isOutdated () {
628 if (this.isOwned()) return false 689 if (this.isOwned()) return false
629 690
diff --git a/server/models/application/application.ts b/server/models/application/application.ts
index 909569de1..21f8b1cbc 100644
--- a/server/models/application/application.ts
+++ b/server/models/application/application.ts
@@ -32,6 +32,10 @@ export class ApplicationModel extends Model {
32 @Column 32 @Column
33 migrationVersion: number 33 migrationVersion: number
34 34
35 @AllowNull(true)
36 @Column
37 latestPeerTubeVersion: string
38
35 @HasOne(() => AccountModel, { 39 @HasOne(() => AccountModel, {
36 foreignKey: { 40 foreignKey: {
37 allowNull: true 41 allowNull: true
diff --git a/server/models/avatar/avatar.ts b/server/models/avatar/avatar.ts
deleted file mode 100644
index 0d246a144..000000000
--- a/server/models/avatar/avatar.ts
+++ /dev/null
@@ -1,81 +0,0 @@
1import { join } from 'path'
2import { AfterDestroy, AllowNull, Column, CreatedAt, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
3import { Avatar } from '../../../shared/models/avatars/avatar.model'
4import { LAZY_STATIC_PATHS } from '../../initializers/constants'
5import { logger } from '../../helpers/logger'
6import { remove } from 'fs-extra'
7import { CONFIG } from '../../initializers/config'
8import { throwIfNotValid } from '../utils'
9import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
10import { MAvatarFormattable } from '@server/types/models'
11
12@Table({
13 tableName: 'avatar',
14 indexes: [
15 {
16 fields: [ 'filename' ],
17 unique: true
18 }
19 ]
20})
21export class AvatarModel extends Model {
22
23 @AllowNull(false)
24 @Column
25 filename: string
26
27 @AllowNull(true)
28 @Is('AvatarFileUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'fileUrl', true))
29 @Column
30 fileUrl: string
31
32 @AllowNull(false)
33 @Column
34 onDisk: boolean
35
36 @CreatedAt
37 createdAt: Date
38
39 @UpdatedAt
40 updatedAt: Date
41
42 @AfterDestroy
43 static removeFilesAndSendDelete (instance: AvatarModel) {
44 logger.info('Removing avatar file %s.', instance.filename)
45
46 // Don't block the transaction
47 instance.removeAvatar()
48 .catch(err => logger.error('Cannot remove avatar file %s.', instance.filename, err))
49 }
50
51 static loadByName (filename: string) {
52 const query = {
53 where: {
54 filename
55 }
56 }
57
58 return AvatarModel.findOne(query)
59 }
60
61 toFormattedJSON (this: MAvatarFormattable): Avatar {
62 return {
63 path: this.getStaticPath(),
64 createdAt: this.createdAt,
65 updatedAt: this.updatedAt
66 }
67 }
68
69 getStaticPath () {
70 return join(LAZY_STATIC_PATHS.AVATARS, this.filename)
71 }
72
73 getPath () {
74 return join(CONFIG.STORAGE.AVATARS_DIR, this.filename)
75 }
76
77 removeAvatar () {
78 const avatarPath = join(CONFIG.STORAGE.AVATARS_DIR, this.filename)
79 return remove(avatarPath)
80 }
81}
diff --git a/server/models/oauth/oauth-token.ts b/server/models/oauth/oauth-token.ts
index 6bc6cf27c..27e643aa7 100644
--- a/server/models/oauth/oauth-token.ts
+++ b/server/models/oauth/oauth-token.ts
@@ -12,9 +12,10 @@ import {
12 Table, 12 Table,
13 UpdatedAt 13 UpdatedAt
14} from 'sequelize-typescript' 14} from 'sequelize-typescript'
15import { TokensCache } from '@server/lib/auth/tokens-cache'
16import { MUserAccountId } from '@server/types/models'
15import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' 17import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token'
16import { logger } from '../../helpers/logger' 18import { logger } from '../../helpers/logger'
17import { clearCacheByToken } from '../../lib/oauth-model'
18import { AccountModel } from '../account/account' 19import { AccountModel } from '../account/account'
19import { UserModel } from '../account/user' 20import { UserModel } from '../account/user'
20import { ActorModel } from '../activitypub/actor' 21import { ActorModel } from '../activitypub/actor'
@@ -26,9 +27,7 @@ export type OAuthTokenInfo = {
26 client: { 27 client: {
27 id: number 28 id: number
28 } 29 }
29 user: { 30 user: MUserAccountId
30 id: number
31 }
32 token: MOAuthTokenUser 31 token: MOAuthTokenUser
33} 32}
34 33
@@ -133,7 +132,7 @@ export class OAuthTokenModel extends Model {
133 @AfterUpdate 132 @AfterUpdate
134 @AfterDestroy 133 @AfterDestroy
135 static removeTokenCache (token: OAuthTokenModel) { 134 static removeTokenCache (token: OAuthTokenModel) {
136 return clearCacheByToken(token.accessToken) 135 return TokensCache.Instance.clearCacheByToken(token.accessToken)
137 } 136 }
138 137
139 static loadByRefreshToken (refreshToken: string) { 138 static loadByRefreshToken (refreshToken: string) {
@@ -206,6 +205,8 @@ export class OAuthTokenModel extends Model {
206 } 205 }
207 206
208 static deleteUserToken (userId: number, t?: Transaction) { 207 static deleteUserToken (userId: number, t?: Transaction) {
208 TokensCache.Instance.deleteUserToken(userId)
209
209 const query = { 210 const query = {
210 where: { 211 where: {
211 userId 212 userId
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts
index 53293df37..53ebadeaf 100644
--- a/server/models/redundancy/video-redundancy.ts
+++ b/server/models/redundancy/video-redundancy.ts
@@ -32,6 +32,7 @@ import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers/constants'
32import { ActorModel } from '../activitypub/actor' 32import { ActorModel } from '../activitypub/actor'
33import { ServerModel } from '../server/server' 33import { ServerModel } from '../server/server'
34import { getSort, getVideoSort, parseAggregateResult, throwIfNotValid } from '../utils' 34import { getSort, getVideoSort, parseAggregateResult, throwIfNotValid } from '../utils'
35import { ScheduleVideoUpdateModel } from '../video/schedule-video-update'
35import { VideoModel } from '../video/video' 36import { VideoModel } from '../video/video'
36import { VideoChannelModel } from '../video/video-channel' 37import { VideoChannelModel } from '../video/video-channel'
37import { VideoFileModel } from '../video/video-file' 38import { VideoFileModel } from '../video/video-file'
@@ -374,7 +375,13 @@ export class VideoRedundancyModel extends Model {
374 ...this.buildVideoIdsForDuplication(peertubeActor) 375 ...this.buildVideoIdsForDuplication(peertubeActor)
375 }, 376 },
376 include: [ 377 include: [
377 VideoRedundancyModel.buildServerRedundancyInclude() 378 VideoRedundancyModel.buildServerRedundancyInclude(),
379
380 // Required by publishedAt sort
381 {
382 model: ScheduleVideoUpdateModel.unscoped(),
383 required: false
384 }
378 ] 385 ]
379 } 386 }
380 387
diff --git a/server/models/utils.ts b/server/models/utils.ts
index 5337ae75d..ec51c66bf 100644
--- a/server/models/utils.ts
+++ b/server/models/utils.ts
@@ -56,6 +56,14 @@ function getVideoSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): Or
56 56
57 lastSort 57 lastSort
58 ] 58 ]
59 } else if (field === 'publishedAt') {
60 return [
61 [ 'ScheduleVideoUpdate', 'updateAt', direction + ' NULLS LAST' ],
62
63 [ Sequelize.col('VideoModel.publishedAt'), direction ],
64
65 lastSort
66 ]
59 } 67 }
60 68
61 let finalField: string | Col 69 let finalField: string | Col
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts
index 178878c55..d2a055f5b 100644
--- a/server/models/video/video-channel.ts
+++ b/server/models/video/video-channel.ts
@@ -28,17 +28,16 @@ import {
28import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' 28import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
29import { sendDeleteActor } from '../../lib/activitypub/send' 29import { sendDeleteActor } from '../../lib/activitypub/send'
30import { 30import {
31 MChannelAccountDefault,
32 MChannelActor, 31 MChannelActor,
33 MChannelActorAccountDefaultVideos,
34 MChannelAP, 32 MChannelAP,
33 MChannelBannerAccountDefault,
35 MChannelFormattable, 34 MChannelFormattable,
36 MChannelSummaryFormattable 35 MChannelSummaryFormattable
37} from '../../types/models/video' 36} from '../../types/models/video'
38import { AccountModel, ScopeNames as AccountModelScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account' 37import { AccountModel, ScopeNames as AccountModelScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account'
38import { ActorImageModel } from '../account/actor-image'
39import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' 39import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor'
40import { ActorFollowModel } from '../activitypub/actor-follow' 40import { ActorFollowModel } from '../activitypub/actor-follow'
41import { AvatarModel } from '../avatar/avatar'
42import { ServerModel } from '../server/server' 41import { ServerModel } from '../server/server'
43import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' 42import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils'
44import { VideoModel } from './video' 43import { VideoModel } from './video'
@@ -49,6 +48,7 @@ export enum ScopeNames {
49 SUMMARY = 'SUMMARY', 48 SUMMARY = 'SUMMARY',
50 WITH_ACCOUNT = 'WITH_ACCOUNT', 49 WITH_ACCOUNT = 'WITH_ACCOUNT',
51 WITH_ACTOR = 'WITH_ACTOR', 50 WITH_ACTOR = 'WITH_ACTOR',
51 WITH_ACTOR_BANNER = 'WITH_ACTOR_BANNER',
52 WITH_VIDEOS = 'WITH_VIDEOS', 52 WITH_VIDEOS = 'WITH_VIDEOS',
53 WITH_STATS = 'WITH_STATS' 53 WITH_STATS = 'WITH_STATS'
54} 54}
@@ -99,7 +99,14 @@ export type SummaryOptions = {
99 } 99 }
100 } 100 }
101 ] 101 ]
102 } 102 },
103 include: [
104 {
105 model: ActorImageModel,
106 as: 'Banner',
107 required: false
108 }
109 ]
103 }, 110 },
104 { 111 {
105 model: AccountModel, 112 model: AccountModel,
@@ -130,7 +137,8 @@ export type SummaryOptions = {
130 required: false 137 required: false
131 }, 138 },
132 { 139 {
133 model: AvatarModel.unscoped(), 140 model: ActorImageModel.unscoped(),
141 as: 'Avatar',
134 required: false 142 required: false
135 } 143 }
136 ] 144 ]
@@ -167,6 +175,20 @@ export type SummaryOptions = {
167 ActorModel 175 ActorModel
168 ] 176 ]
169 }, 177 },
178 [ScopeNames.WITH_ACTOR_BANNER]: {
179 include: [
180 {
181 model: ActorModel,
182 include: [
183 {
184 model: ActorImageModel,
185 required: false,
186 as: 'Banner'
187 }
188 ]
189 }
190 ]
191 },
170 [ScopeNames.WITH_VIDEOS]: { 192 [ScopeNames.WITH_VIDEOS]: {
171 include: [ 193 include: [
172 VideoModel 194 VideoModel
@@ -441,7 +463,7 @@ export class VideoChannelModel extends Model {
441 where 463 where
442 } 464 }
443 465
444 const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR ] 466 const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR_BANNER ]
445 467
446 if (options.withStats === true) { 468 if (options.withStats === true) {
447 scopes.push({ 469 scopes.push({
@@ -457,32 +479,13 @@ export class VideoChannelModel extends Model {
457 }) 479 })
458 } 480 }
459 481
460 static loadByIdAndPopulateAccount (id: number): Promise<MChannelAccountDefault> { 482 static loadAndPopulateAccount (id: number): Promise<MChannelBannerAccountDefault> {
461 return VideoChannelModel.unscoped() 483 return VideoChannelModel.unscoped()
462 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) 484 .scope([ ScopeNames.WITH_ACTOR_BANNER, ScopeNames.WITH_ACCOUNT ])
463 .findByPk(id) 485 .findByPk(id)
464 } 486 }
465 487
466 static loadByIdAndAccount (id: number, accountId: number): Promise<MChannelAccountDefault> { 488 static loadByUrlAndPopulateAccount (url: string): Promise<MChannelBannerAccountDefault> {
467 const query = {
468 where: {
469 id,
470 accountId
471 }
472 }
473
474 return VideoChannelModel.unscoped()
475 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
476 .findOne(query)
477 }
478
479 static loadAndPopulateAccount (id: number): Promise<MChannelAccountDefault> {
480 return VideoChannelModel.unscoped()
481 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
482 .findByPk(id)
483 }
484
485 static loadByUrlAndPopulateAccount (url: string): Promise<MChannelAccountDefault> {
486 const query = { 489 const query = {
487 include: [ 490 include: [
488 { 491 {
@@ -490,7 +493,14 @@ export class VideoChannelModel extends Model {
490 required: true, 493 required: true,
491 where: { 494 where: {
492 url 495 url
493 } 496 },
497 include: [
498 {
499 model: ActorImageModel,
500 required: false,
501 as: 'Banner'
502 }
503 ]
494 } 504 }
495 ] 505 ]
496 } 506 }
@@ -508,7 +518,7 @@ export class VideoChannelModel extends Model {
508 return VideoChannelModel.loadByNameAndHostAndPopulateAccount(name, host) 518 return VideoChannelModel.loadByNameAndHostAndPopulateAccount(name, host)
509 } 519 }
510 520
511 static loadLocalByNameAndPopulateAccount (name: string): Promise<MChannelAccountDefault> { 521 static loadLocalByNameAndPopulateAccount (name: string): Promise<MChannelBannerAccountDefault> {
512 const query = { 522 const query = {
513 include: [ 523 include: [
514 { 524 {
@@ -517,17 +527,24 @@ export class VideoChannelModel extends Model {
517 where: { 527 where: {
518 preferredUsername: name, 528 preferredUsername: name,
519 serverId: null 529 serverId: null
520 } 530 },
531 include: [
532 {
533 model: ActorImageModel,
534 required: false,
535 as: 'Banner'
536 }
537 ]
521 } 538 }
522 ] 539 ]
523 } 540 }
524 541
525 return VideoChannelModel.unscoped() 542 return VideoChannelModel.unscoped()
526 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) 543 .scope([ ScopeNames.WITH_ACCOUNT ])
527 .findOne(query) 544 .findOne(query)
528 } 545 }
529 546
530 static loadByNameAndHostAndPopulateAccount (name: string, host: string): Promise<MChannelAccountDefault> { 547 static loadByNameAndHostAndPopulateAccount (name: string, host: string): Promise<MChannelBannerAccountDefault> {
531 const query = { 548 const query = {
532 include: [ 549 include: [
533 { 550 {
@@ -541,6 +558,11 @@ export class VideoChannelModel extends Model {
541 model: ServerModel, 558 model: ServerModel,
542 required: true, 559 required: true,
543 where: { host } 560 where: { host }
561 },
562 {
563 model: ActorImageModel,
564 required: false,
565 as: 'Banner'
544 } 566 }
545 ] 567 ]
546 } 568 }
@@ -548,22 +570,10 @@ export class VideoChannelModel extends Model {
548 } 570 }
549 571
550 return VideoChannelModel.unscoped() 572 return VideoChannelModel.unscoped()
551 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) 573 .scope([ ScopeNames.WITH_ACCOUNT ])
552 .findOne(query) 574 .findOne(query)
553 } 575 }
554 576
555 static loadAndPopulateAccountAndVideos (id: number): Promise<MChannelActorAccountDefaultVideos> {
556 const options = {
557 include: [
558 VideoModel
559 ]
560 }
561
562 return VideoChannelModel.unscoped()
563 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_VIDEOS ])
564 .findByPk(id, options)
565 }
566
567 toFormattedSummaryJSON (this: MChannelSummaryFormattable): VideoChannelSummary { 577 toFormattedSummaryJSON (this: MChannelSummaryFormattable): VideoChannelSummary {
568 const actor = this.Actor.toFormattedSummaryJSON() 578 const actor = this.Actor.toFormattedSummaryJSON()
569 579
diff --git a/server/models/video/video-query-builder.ts b/server/models/video/video-query-builder.ts
index 96df0a7f8..4d95ddee2 100644
--- a/server/models/video/video-query-builder.ts
+++ b/server/models/video/video-query-builder.ts
@@ -490,12 +490,13 @@ function wrapForAPIResults (baseQuery: string, replacements: any, options: Build
490 'INNER JOIN "actor" AS "VideoChannel->Account->Actor" ON "VideoChannel->Account"."actorId" = "VideoChannel->Account->Actor"."id"', 490 'INNER JOIN "actor" AS "VideoChannel->Account->Actor" ON "VideoChannel->Account"."actorId" = "VideoChannel->Account->Actor"."id"',
491 491
492 'LEFT OUTER JOIN "server" AS "VideoChannel->Actor->Server" ON "VideoChannel->Actor"."serverId" = "VideoChannel->Actor->Server"."id"', 492 'LEFT OUTER JOIN "server" AS "VideoChannel->Actor->Server" ON "VideoChannel->Actor"."serverId" = "VideoChannel->Actor->Server"."id"',
493 'LEFT OUTER JOIN "avatar" AS "VideoChannel->Actor->Avatar" ON "VideoChannel->Actor"."avatarId" = "VideoChannel->Actor->Avatar"."id"', 493 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Actor->Avatar" ' +
494 'ON "VideoChannel->Actor"."avatarId" = "VideoChannel->Actor->Avatar"."id"',
494 495
495 'LEFT OUTER JOIN "server" AS "VideoChannel->Account->Actor->Server" ' + 496 'LEFT OUTER JOIN "server" AS "VideoChannel->Account->Actor->Server" ' +
496 'ON "VideoChannel->Account->Actor"."serverId" = "VideoChannel->Account->Actor->Server"."id"', 497 'ON "VideoChannel->Account->Actor"."serverId" = "VideoChannel->Account->Actor->Server"."id"',
497 498
498 'LEFT OUTER JOIN "avatar" AS "VideoChannel->Account->Actor->Avatar" ' + 499 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Account->Actor->Avatar" ' +
499 'ON "VideoChannel->Account->Actor"."avatarId" = "VideoChannel->Account->Actor->Avatar"."id"', 500 'ON "VideoChannel->Account->Actor"."avatarId" = "VideoChannel->Account->Actor->Avatar"."id"',
500 501
501 'LEFT OUTER JOIN "thumbnail" AS "Thumbnails" ON "video"."id" = "Thumbnails"."videoId"' 502 'LEFT OUTER JOIN "thumbnail" AS "Thumbnails" ON "video"."id" = "Thumbnails"."videoId"'
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 3c4f3d3df..e9afb2c18 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -24,7 +24,6 @@ import {
24 Table, 24 Table,
25 UpdatedAt 25 UpdatedAt
26} from 'sequelize-typescript' 26} from 'sequelize-typescript'
27import { v4 as uuidv4 } from 'uuid'
28import { buildNSFWFilter } from '@server/helpers/express-utils' 27import { buildNSFWFilter } from '@server/helpers/express-utils'
29import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' 28import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video'
30import { LiveManager } from '@server/lib/live-manager' 29import { LiveManager } from '@server/lib/live-manager'
@@ -100,10 +99,10 @@ import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models
100import { VideoAbuseModel } from '../abuse/video-abuse' 99import { VideoAbuseModel } from '../abuse/video-abuse'
101import { AccountModel } from '../account/account' 100import { AccountModel } from '../account/account'
102import { AccountVideoRateModel } from '../account/account-video-rate' 101import { AccountVideoRateModel } from '../account/account-video-rate'
102import { ActorImageModel } from '../account/actor-image'
103import { UserModel } from '../account/user' 103import { UserModel } from '../account/user'
104import { UserVideoHistoryModel } from '../account/user-video-history' 104import { UserVideoHistoryModel } from '../account/user-video-history'
105import { ActorModel } from '../activitypub/actor' 105import { ActorModel } from '../activitypub/actor'
106import { AvatarModel } from '../avatar/avatar'
107import { VideoRedundancyModel } from '../redundancy/video-redundancy' 106import { VideoRedundancyModel } from '../redundancy/video-redundancy'
108import { ServerModel } from '../server/server' 107import { ServerModel } from '../server/server'
109import { TrackerModel } from '../server/tracker' 108import { TrackerModel } from '../server/tracker'
@@ -286,7 +285,8 @@ export type AvailableForListIDsOptions = {
286 required: false 285 required: false
287 }, 286 },
288 { 287 {
289 model: AvatarModel.unscoped(), 288 model: ActorImageModel.unscoped(),
289 as: 'Avatar',
290 required: false 290 required: false
291 } 291 }
292 ] 292 ]
@@ -308,7 +308,8 @@ export type AvailableForListIDsOptions = {
308 required: false 308 required: false
309 }, 309 },
310 { 310 {
311 model: AvatarModel.unscoped(), 311 model: ActorImageModel.unscoped(),
312 as: 'Avatar',
312 required: false 313 required: false
313 } 314 }
314 ] 315 ]
@@ -1703,7 +1704,7 @@ export class VideoModel extends Model {
1703 1704
1704 function buildActor (rowActor: any) { 1705 function buildActor (rowActor: any) {
1705 const avatarModel = rowActor.Avatar.id !== null 1706 const avatarModel = rowActor.Avatar.id !== null
1706 ? new AvatarModel(pick(rowActor.Avatar, avatarKeys), buildOpts) 1707 ? new ActorImageModel(pick(rowActor.Avatar, avatarKeys), buildOpts)
1707 : null 1708 : null
1708 1709
1709 const serverModel = rowActor.Server.id !== null 1710 const serverModel = rowActor.Server.id !== null
@@ -1869,20 +1870,12 @@ export class VideoModel extends Model {
1869 this.Thumbnails.push(savedThumbnail) 1870 this.Thumbnails.push(savedThumbnail)
1870 } 1871 }
1871 1872
1872 generateThumbnailName () {
1873 return uuidv4() + '.jpg'
1874 }
1875
1876 getMiniature () { 1873 getMiniature () {
1877 if (Array.isArray(this.Thumbnails) === false) return undefined 1874 if (Array.isArray(this.Thumbnails) === false) return undefined
1878 1875
1879 return this.Thumbnails.find(t => t.type === ThumbnailType.MINIATURE) 1876 return this.Thumbnails.find(t => t.type === ThumbnailType.MINIATURE)
1880 } 1877 }
1881 1878
1882 generatePreviewName () {
1883 return uuidv4() + '.jpg'
1884 }
1885
1886 hasPreview () { 1879 hasPreview () {
1887 return !!this.getPreview() 1880 return !!this.getPreview()
1888 } 1881 }
diff --git a/server/tests/api/activitypub/security.ts b/server/tests/api/activitypub/security.ts
index 8bde54a40..364b53e0f 100644
--- a/server/tests/api/activitypub/security.ts
+++ b/server/tests/api/activitypub/security.ts
@@ -8,6 +8,8 @@ import {
8 cleanupTests, 8 cleanupTests,
9 closeAllSequelize, 9 closeAllSequelize,
10 flushAndRunMultipleServers, 10 flushAndRunMultipleServers,
11 killallServers,
12 reRunServer,
11 ServerInfo, 13 ServerInfo,
12 setActorField, 14 setActorField,
13 wait 15 wait
@@ -20,21 +22,32 @@ import { buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activi
20const expect = chai.expect 22const expect = chai.expect
21 23
22function setKeysOfServer (onServer: ServerInfo, ofServer: ServerInfo, publicKey: string, privateKey: string) { 24function setKeysOfServer (onServer: ServerInfo, ofServer: ServerInfo, publicKey: string, privateKey: string) {
25 const url = 'http://localhost:' + ofServer.port + '/accounts/peertube'
26
23 return Promise.all([ 27 return Promise.all([
24 setActorField(onServer.internalServerNumber, 'http://localhost:' + ofServer.port + '/accounts/peertube', 'publicKey', publicKey), 28 setActorField(onServer.internalServerNumber, url, 'publicKey', publicKey),
25 setActorField(onServer.internalServerNumber, 'http://localhost:' + ofServer.port + '/accounts/peertube', 'privateKey', privateKey) 29 setActorField(onServer.internalServerNumber, url, 'privateKey', privateKey)
26 ]) 30 ])
27} 31}
28 32
29function getAnnounceWithoutContext (server2: ServerInfo) { 33function setUpdatedAtOfServer (onServer: ServerInfo, ofServer: ServerInfo, updatedAt: string) {
34 const url = 'http://localhost:' + ofServer.port + '/accounts/peertube'
35
36 return Promise.all([
37 setActorField(onServer.internalServerNumber, url, 'createdAt', updatedAt),
38 setActorField(onServer.internalServerNumber, url, 'updatedAt', updatedAt)
39 ])
40}
41
42function getAnnounceWithoutContext (server: ServerInfo) {
30 const json = require('./json/peertube/announce-without-context.json') 43 const json = require('./json/peertube/announce-without-context.json')
31 const result: typeof json = {} 44 const result: typeof json = {}
32 45
33 for (const key of Object.keys(json)) { 46 for (const key of Object.keys(json)) {
34 if (Array.isArray(json[key])) { 47 if (Array.isArray(json[key])) {
35 result[key] = json[key].map(v => v.replace(':9002', `:${server2.port}`)) 48 result[key] = json[key].map(v => v.replace(':9002', `:${server.port}`))
36 } else { 49 } else {
37 result[key] = json[key].replace(':9002', `:${server2.port}`) 50 result[key] = json[key].replace(':9002', `:${server.port}`)
38 } 51 }
39 } 52 }
40 53
@@ -64,7 +77,8 @@ describe('Test ActivityPub security', function () {
64 77
65 url = servers[0].url + '/inbox' 78 url = servers[0].url + '/inbox'
66 79
67 await setKeysOfServer(servers[0], servers[1], keys.publicKey, keys.privateKey) 80 await setKeysOfServer(servers[0], servers[1], keys.publicKey, null)
81 await setKeysOfServer(servers[1], servers[1], keys.publicKey, keys.privateKey)
68 82
69 const to = { url: 'http://localhost:' + servers[0].port + '/accounts/peertube' } 83 const to = { url: 'http://localhost:' + servers[0].port + '/accounts/peertube' }
70 const by = { url: 'http://localhost:' + servers[1].port + '/accounts/peertube', privateKey: keys.privateKey } 84 const by = { url: 'http://localhost:' + servers[1].port + '/accounts/peertube', privateKey: keys.privateKey }
@@ -79,9 +93,12 @@ describe('Test ActivityPub security', function () {
79 Digest: buildDigest({ hello: 'coucou' }) 93 Digest: buildDigest({ hello: 'coucou' })
80 } 94 }
81 95
82 const { response } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers) 96 try {
83 97 await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
84 expect(response.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) 98 expect(true, 'Did not throw').to.be.false
99 } catch (err) {
100 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
101 }
85 }) 102 })
86 103
87 it('Should fail with an invalid date', async function () { 104 it('Should fail with an invalid date', async function () {
@@ -89,9 +106,12 @@ describe('Test ActivityPub security', function () {
89 const headers = buildGlobalHeaders(body) 106 const headers = buildGlobalHeaders(body)
90 headers['date'] = 'Wed, 21 Oct 2015 07:28:00 GMT' 107 headers['date'] = 'Wed, 21 Oct 2015 07:28:00 GMT'
91 108
92 const { response } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers) 109 try {
93 110 await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
94 expect(response.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) 111 expect(true, 'Did not throw').to.be.false
112 } catch (err) {
113 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
114 }
95 }) 115 })
96 116
97 it('Should fail with bad keys', async function () { 117 it('Should fail with bad keys', async function () {
@@ -101,9 +121,12 @@ describe('Test ActivityPub security', function () {
101 const body = activityPubContextify(getAnnounceWithoutContext(servers[1])) 121 const body = activityPubContextify(getAnnounceWithoutContext(servers[1]))
102 const headers = buildGlobalHeaders(body) 122 const headers = buildGlobalHeaders(body)
103 123
104 const { response } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers) 124 try {
105 125 await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
106 expect(response.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) 126 expect(true, 'Did not throw').to.be.false
127 } catch (err) {
128 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
129 }
107 }) 130 })
108 131
109 it('Should reject requests without appropriate signed headers', async function () { 132 it('Should reject requests without appropriate signed headers', async function () {
@@ -123,8 +146,12 @@ describe('Test ActivityPub security', function () {
123 for (const badHeaders of badHeadersMatrix) { 146 for (const badHeaders of badHeadersMatrix) {
124 signatureOptions.headers = badHeaders 147 signatureOptions.headers = badHeaders
125 148
126 const { response } = await makePOSTAPRequest(url, body, signatureOptions, headers) 149 try {
127 expect(response.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) 150 await makePOSTAPRequest(url, body, signatureOptions, headers)
151 expect(true, 'Did not throw').to.be.false
152 } catch (err) {
153 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
154 }
128 } 155 }
129 }) 156 })
130 157
@@ -132,27 +159,32 @@ describe('Test ActivityPub security', function () {
132 const body = activityPubContextify(getAnnounceWithoutContext(servers[1])) 159 const body = activityPubContextify(getAnnounceWithoutContext(servers[1]))
133 const headers = buildGlobalHeaders(body) 160 const headers = buildGlobalHeaders(body)
134 161
135 const { response } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers) 162 const { statusCode } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
136 163 expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
137 expect(response.statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
138 }) 164 })
139 165
140 it('Should refresh the actor keys', async function () { 166 it('Should refresh the actor keys', async function () {
141 this.timeout(20000) 167 this.timeout(20000)
142 168
143 // Wait refresh invalidation
144 await wait(10000)
145
146 // Update keys of server 2 to invalid keys 169 // Update keys of server 2 to invalid keys
147 // Server 1 should refresh the actor and fail 170 // Server 1 should refresh the actor and fail
148 await setKeysOfServer(servers[1], servers[1], invalidKeys.publicKey, invalidKeys.privateKey) 171 await setKeysOfServer(servers[1], servers[1], invalidKeys.publicKey, invalidKeys.privateKey)
172 await setUpdatedAtOfServer(servers[0], servers[1], '2015-07-17 22:00:00+00')
173
174 // Invalid peertube actor cache
175 killallServers([ servers[1] ])
176 await reRunServer(servers[1])
149 177
150 const body = activityPubContextify(getAnnounceWithoutContext(servers[1])) 178 const body = activityPubContextify(getAnnounceWithoutContext(servers[1]))
151 const headers = buildGlobalHeaders(body) 179 const headers = buildGlobalHeaders(body)
152 180
153 const { response } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers) 181 try {
154 182 await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
155 expect(response.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) 183 expect(true, 'Did not throw').to.be.false
184 } catch (err) {
185 console.error(err)
186 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
187 }
156 }) 188 })
157 }) 189 })
158 190
@@ -183,9 +215,12 @@ describe('Test ActivityPub security', function () {
183 215
184 const headers = buildGlobalHeaders(signedBody) 216 const headers = buildGlobalHeaders(signedBody)
185 217
186 const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers) 218 try {
187 219 await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
188 expect(response.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) 220 expect(true, 'Did not throw').to.be.false
221 } catch (err) {
222 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
223 }
189 }) 224 })
190 225
191 it('Should fail with an altered body', async function () { 226 it('Should fail with an altered body', async function () {
@@ -204,9 +239,12 @@ describe('Test ActivityPub security', function () {
204 239
205 const headers = buildGlobalHeaders(signedBody) 240 const headers = buildGlobalHeaders(signedBody)
206 241
207 const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers) 242 try {
208 243 await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
209 expect(response.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) 244 expect(true, 'Did not throw').to.be.false
245 } catch (err) {
246 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
247 }
210 }) 248 })
211 249
212 it('Should succeed with a valid signature', async function () { 250 it('Should succeed with a valid signature', async function () {
@@ -220,9 +258,8 @@ describe('Test ActivityPub security', function () {
220 258
221 const headers = buildGlobalHeaders(signedBody) 259 const headers = buildGlobalHeaders(signedBody)
222 260
223 const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers) 261 const { statusCode } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
224 262 expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
225 expect(response.statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
226 }) 263 })
227 264
228 it('Should refresh the actor keys', async function () { 265 it('Should refresh the actor keys', async function () {
@@ -243,9 +280,12 @@ describe('Test ActivityPub security', function () {
243 280
244 const headers = buildGlobalHeaders(signedBody) 281 const headers = buildGlobalHeaders(signedBody)
245 282
246 const { response } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers) 283 try {
247 284 await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
248 expect(response.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) 285 expect(true, 'Did not throw').to.be.false
286 } catch (err) {
287 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
288 }
249 }) 289 })
250 }) 290 })
251 291
diff --git a/server/tests/api/check-params/user-notifications.ts b/server/tests/api/check-params/user-notifications.ts
index 05a78b0ad..26d4423f9 100644
--- a/server/tests/api/check-params/user-notifications.ts
+++ b/server/tests/api/check-params/user-notifications.ts
@@ -176,7 +176,9 @@ describe('Test user notifications API validators', function () {
176 newInstanceFollower: UserNotificationSettingValue.WEB, 176 newInstanceFollower: UserNotificationSettingValue.WEB,
177 autoInstanceFollowing: UserNotificationSettingValue.WEB, 177 autoInstanceFollowing: UserNotificationSettingValue.WEB,
178 abuseNewMessage: UserNotificationSettingValue.WEB, 178 abuseNewMessage: UserNotificationSettingValue.WEB,
179 abuseStateChange: UserNotificationSettingValue.WEB 179 abuseStateChange: UserNotificationSettingValue.WEB,
180 newPeerTubeVersion: UserNotificationSettingValue.WEB,
181 newPluginVersion: UserNotificationSettingValue.WEB
180 } 182 }
181 183
182 it('Should fail with missing fields', async function () { 184 it('Should fail with missing fields', async function () {
diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts
index 0a13f5b67..2b03fde2d 100644
--- a/server/tests/api/check-params/users.ts
+++ b/server/tests/api/check-params/users.ts
@@ -241,7 +241,7 @@ describe('Test users API validators', function () {
241 }) 241 })
242 242
243 it('Should succeed with no password on a server with smtp enabled', async function () { 243 it('Should succeed with no password on a server with smtp enabled', async function () {
244 this.timeout(10000) 244 this.timeout(20000)
245 245
246 killallServers([ server ]) 246 killallServers([ server ])
247 247
diff --git a/server/tests/api/check-params/video-channels.ts b/server/tests/api/check-params/video-channels.ts
index 0dd436426..bc2e6192e 100644
--- a/server/tests/api/check-params/video-channels.ts
+++ b/server/tests/api/check-params/video-channels.ts
@@ -234,7 +234,8 @@ describe('Test video channels API validator', function () {
234 }) 234 })
235 }) 235 })
236 236
237 describe('When updating video channel avatar', function () { 237 describe('When updating video channel avatar/banner', function () {
238 const types = [ 'avatar', 'banner' ]
238 let path: string 239 let path: string
239 240
240 before(async function () { 241 before(async function () {
@@ -242,48 +243,57 @@ describe('Test video channels API validator', function () {
242 }) 243 })
243 244
244 it('Should fail with an incorrect input file', async function () { 245 it('Should fail with an incorrect input file', async function () {
245 const fields = {} 246 for (const type of types) {
246 const attaches = { 247 const fields = {}
247 avatarfile: join(__dirname, '..', '..', 'fixtures', 'video_short.mp4') 248 const attaches = {
249 [type + 'file']: join(__dirname, '..', '..', 'fixtures', 'video_short.mp4')
250 }
251
252 await makeUploadRequest({ url: server.url, path: `${path}/${type}/pick`, token: server.accessToken, fields, attaches })
248 } 253 }
249 await makeUploadRequest({ url: server.url, path: path + '/avatar/pick', token: server.accessToken, fields, attaches })
250 }) 254 })
251 255
252 it('Should fail with a big file', async function () { 256 it('Should fail with a big file', async function () {
253 const fields = {} 257 for (const type of types) {
254 const attaches = { 258 const fields = {}
255 avatarfile: join(__dirname, '..', '..', 'fixtures', 'avatar-big.png') 259 const attaches = {
260 [type + 'file']: join(__dirname, '..', '..', 'fixtures', 'avatar-big.png')
261 }
262 await makeUploadRequest({ url: server.url, path: `${path}/${type}/pick`, token: server.accessToken, fields, attaches })
256 } 263 }
257 await makeUploadRequest({ url: server.url, path: path + '/avatar/pick', token: server.accessToken, fields, attaches })
258 }) 264 })
259 265
260 it('Should fail with an unauthenticated user', async function () { 266 it('Should fail with an unauthenticated user', async function () {
261 const fields = {} 267 for (const type of types) {
262 const attaches = { 268 const fields = {}
263 avatarfile: join(__dirname, '..', '..', 'fixtures', 'avatar.png') 269 const attaches = {
270 [type + 'file']: join(__dirname, '..', '..', 'fixtures', 'avatar.png')
271 }
272 await makeUploadRequest({
273 url: server.url,
274 path: `${path}/${type}/pick`,
275 fields,
276 attaches,
277 statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
278 })
264 } 279 }
265 await makeUploadRequest({
266 url: server.url,
267 path: path + '/avatar/pick',
268 fields,
269 attaches,
270 statusCodeExpected: HttpStatusCode.UNAUTHORIZED_401
271 })
272 }) 280 })
273 281
274 it('Should succeed with the correct params', async function () { 282 it('Should succeed with the correct params', async function () {
275 const fields = {} 283 for (const type of types) {
276 const attaches = { 284 const fields = {}
277 avatarfile: join(__dirname, '..', '..', 'fixtures', 'avatar.png') 285 const attaches = {
286 [type + 'file']: join(__dirname, '..', '..', 'fixtures', 'avatar.png')
287 }
288 await makeUploadRequest({
289 url: server.url,
290 path: `${path}/${type}/pick`,
291 token: server.accessToken,
292 fields,
293 attaches,
294 statusCodeExpected: HttpStatusCode.OK_200
295 })
278 } 296 }
279 await makeUploadRequest({
280 url: server.url,
281 path: path + '/avatar/pick',
282 token: server.accessToken,
283 fields,
284 attaches,
285 statusCodeExpected: HttpStatusCode.OK_200
286 })
287 }) 297 })
288 }) 298 })
289 299
diff --git a/server/tests/api/notifications/admin-notifications.ts b/server/tests/api/notifications/admin-notifications.ts
new file mode 100644
index 000000000..e07327d74
--- /dev/null
+++ b/server/tests/api/notifications/admin-notifications.ts
@@ -0,0 +1,165 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4import { expect } from 'chai'
5import { MockJoinPeerTubeVersions } from '@shared/extra-utils/mock-servers/joinpeertube-versions'
6import { cleanupTests, installPlugin, setPluginLatestVersion, setPluginVersion, wait } from '../../../../shared/extra-utils'
7import { ServerInfo } from '../../../../shared/extra-utils/index'
8import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email'
9import {
10 CheckerBaseParams,
11 checkNewPeerTubeVersion,
12 checkNewPluginVersion,
13 prepareNotificationsTest
14} from '../../../../shared/extra-utils/users/user-notifications'
15import { UserNotification, UserNotificationType } from '../../../../shared/models/users'
16import { PluginType } from '@shared/models'
17
18describe('Test admin notifications', function () {
19 let server: ServerInfo
20 let userNotifications: UserNotification[] = []
21 let adminNotifications: UserNotification[] = []
22 let emails: object[] = []
23 let baseParams: CheckerBaseParams
24 let joinPeerTubeServer: MockJoinPeerTubeVersions
25
26 before(async function () {
27 this.timeout(120000)
28
29 const config = {
30 peertube: {
31 check_latest_version: {
32 enabled: true,
33 url: 'http://localhost:42102/versions.json'
34 }
35 },
36 plugins: {
37 index: {
38 enabled: true,
39 check_latest_versions_interval: '5 seconds'
40 }
41 }
42 }
43
44 const res = await prepareNotificationsTest(1, config)
45 emails = res.emails
46 server = res.servers[0]
47
48 userNotifications = res.userNotifications
49 adminNotifications = res.adminNotifications
50
51 baseParams = {
52 server: server,
53 emails,
54 socketNotifications: adminNotifications,
55 token: server.accessToken
56 }
57
58 await installPlugin({
59 url: server.url,
60 accessToken: server.accessToken,
61 npmName: 'peertube-plugin-hello-world'
62 })
63
64 await installPlugin({
65 url: server.url,
66 accessToken: server.accessToken,
67 npmName: 'peertube-theme-background-red'
68 })
69
70 joinPeerTubeServer = new MockJoinPeerTubeVersions()
71 await joinPeerTubeServer.initialize()
72 })
73
74 describe('Latest PeerTube version notification', function () {
75
76 it('Should not send a notification to admins if there is not a new version', async function () {
77 this.timeout(30000)
78
79 joinPeerTubeServer.setLatestVersion('1.4.2')
80
81 await wait(3000)
82 await checkNewPeerTubeVersion(baseParams, '1.4.2', 'absence')
83 })
84
85 it('Should send a notification to admins on new plugin version', async function () {
86 this.timeout(30000)
87
88 joinPeerTubeServer.setLatestVersion('15.4.2')
89
90 await wait(3000)
91 await checkNewPeerTubeVersion(baseParams, '15.4.2', 'presence')
92 })
93
94 it('Should not send the same notification to admins', async function () {
95 this.timeout(30000)
96
97 await wait(3000)
98 expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(1)
99 })
100
101 it('Should not have sent a notification to users', async function () {
102 this.timeout(30000)
103
104 expect(userNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(0)
105 })
106
107 it('Should send a new notification after a new release', async function () {
108 this.timeout(30000)
109
110 joinPeerTubeServer.setLatestVersion('15.4.3')
111
112 await wait(3000)
113 await checkNewPeerTubeVersion(baseParams, '15.4.3', 'presence')
114 expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(2)
115 })
116 })
117
118 describe('Latest plugin version notification', function () {
119
120 it('Should not send a notification to admins if there is no new plugin version', async function () {
121 this.timeout(30000)
122
123 await wait(6000)
124 await checkNewPluginVersion(baseParams, PluginType.PLUGIN, 'hello-world', 'absence')
125 })
126
127 it('Should send a notification to admins on new plugin version', async function () {
128 this.timeout(30000)
129
130 await setPluginVersion(server.internalServerNumber, 'hello-world', '0.0.1')
131 await setPluginLatestVersion(server.internalServerNumber, 'hello-world', '0.0.1')
132 await wait(6000)
133
134 await checkNewPluginVersion(baseParams, PluginType.PLUGIN, 'hello-world', 'presence')
135 })
136
137 it('Should not send the same notification to admins', async function () {
138 this.timeout(30000)
139
140 await wait(6000)
141
142 expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PLUGIN_VERSION)).to.have.lengthOf(1)
143 })
144
145 it('Should not have sent a notification to users', async function () {
146 expect(userNotifications.filter(n => n.type === UserNotificationType.NEW_PLUGIN_VERSION)).to.have.lengthOf(0)
147 })
148
149 it('Should send a new notification after a new plugin release', async function () {
150 this.timeout(30000)
151
152 await setPluginVersion(server.internalServerNumber, 'hello-world', '0.0.1')
153 await setPluginLatestVersion(server.internalServerNumber, 'hello-world', '0.0.1')
154 await wait(6000)
155
156 expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(2)
157 })
158 })
159
160 after(async function () {
161 MockSmtpServer.Instance.kill()
162
163 await cleanupTests([ server ])
164 })
165})
diff --git a/server/tests/api/notifications/index.ts b/server/tests/api/notifications/index.ts
index bd07a339e..8caa30a3d 100644
--- a/server/tests/api/notifications/index.ts
+++ b/server/tests/api/notifications/index.ts
@@ -1,3 +1,4 @@
1import './admin-notifications'
1import './comments-notifications' 2import './comments-notifications'
2import './moderation-notifications' 3import './moderation-notifications'
3import './notifications-api' 4import './notifications-api'
diff --git a/server/tests/api/server/handle-down.ts b/server/tests/api/server/handle-down.ts
index 043754e70..f3ba11950 100644
--- a/server/tests/api/server/handle-down.ts
+++ b/server/tests/api/server/handle-down.ts
@@ -348,8 +348,8 @@ describe('Test handle downs', function () {
348 348
349 for (let i = 0; i < 3; i++) { 349 for (let i = 0; i < 3; i++) {
350 await getVideo(servers[1].url, videoIdsServer1[i]) 350 await getVideo(servers[1].url, videoIdsServer1[i])
351 await wait(1000)
352 await waitJobs([ servers[1] ]) 351 await waitJobs([ servers[1] ])
352 await wait(1500)
353 } 353 }
354 354
355 for (const id of videoIdsServer1) { 355 for (const id of videoIdsServer1) {
diff --git a/server/tests/api/server/services.ts b/server/tests/api/server/services.ts
index df910c111..f0fa91674 100644
--- a/server/tests/api/server/services.ts
+++ b/server/tests/api/server/services.ts
@@ -20,6 +20,7 @@ const expect = chai.expect
20describe('Test services', function () { 20describe('Test services', function () {
21 let server: ServerInfo = null 21 let server: ServerInfo = null
22 let playlistUUID: string 22 let playlistUUID: string
23 let playlistDisplayName: string
23 let video: Video 24 let video: Video
24 25
25 before(async function () { 26 before(async function () {
@@ -52,6 +53,7 @@ describe('Test services', function () {
52 }) 53 })
53 54
54 playlistUUID = res.body.videoPlaylist.uuid 55 playlistUUID = res.body.videoPlaylist.uuid
56 playlistDisplayName = 'The Life and Times of Scrooge McDuck'
55 57
56 await addVideoInPlaylist({ 58 await addVideoInPlaylist({
57 url: server.url, 59 url: server.url,
@@ -69,7 +71,7 @@ describe('Test services', function () {
69 71
70 const res = await getOEmbed(server.url, oembedUrl) 72 const res = await getOEmbed(server.url, oembedUrl)
71 const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts" ' + 73 const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts" ' +
72 `src="http://localhost:${server.port}/videos/embed/${video.uuid}" ` + 74 `title="${video.name}" src="http://localhost:${server.port}/videos/embed/${video.uuid}" ` +
73 'frameborder="0" allowfullscreen></iframe>' 75 'frameborder="0" allowfullscreen></iframe>'
74 const expectedThumbnailUrl = 'http://localhost:' + server.port + video.previewPath 76 const expectedThumbnailUrl = 'http://localhost:' + server.port + video.previewPath
75 77
@@ -88,7 +90,7 @@ describe('Test services', function () {
88 90
89 const res = await getOEmbed(server.url, oembedUrl) 91 const res = await getOEmbed(server.url, oembedUrl)
90 const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts" ' + 92 const expectedHtml = '<iframe width="560" height="315" sandbox="allow-same-origin allow-scripts" ' +
91 `src="http://localhost:${server.port}/video-playlists/embed/${playlistUUID}" ` + 93 `title="${playlistDisplayName}" src="http://localhost:${server.port}/video-playlists/embed/${playlistUUID}" ` +
92 'frameborder="0" allowfullscreen></iframe>' 94 'frameborder="0" allowfullscreen></iframe>'
93 95
94 expect(res.body.html).to.equal(expectedHtml) 96 expect(res.body.html).to.equal(expectedHtml)
@@ -97,8 +99,8 @@ describe('Test services', function () {
97 expect(res.body.width).to.equal(560) 99 expect(res.body.width).to.equal(560)
98 expect(res.body.height).to.equal(315) 100 expect(res.body.height).to.equal(315)
99 expect(res.body.thumbnail_url).exist 101 expect(res.body.thumbnail_url).exist
100 expect(res.body.thumbnail_width).to.equal(223) 102 expect(res.body.thumbnail_width).to.equal(280)
101 expect(res.body.thumbnail_height).to.equal(122) 103 expect(res.body.thumbnail_height).to.equal(157)
102 }) 104 })
103 105
104 it('Should have a valid oEmbed response with small max height query', async function () { 106 it('Should have a valid oEmbed response with small max height query', async function () {
@@ -109,7 +111,7 @@ describe('Test services', function () {
109 111
110 const res = await getOEmbed(server.url, oembedUrl, format, maxHeight, maxWidth) 112 const res = await getOEmbed(server.url, oembedUrl, format, maxHeight, maxWidth)
111 const expectedHtml = '<iframe width="50" height="50" sandbox="allow-same-origin allow-scripts" ' + 113 const expectedHtml = '<iframe width="50" height="50" sandbox="allow-same-origin allow-scripts" ' +
112 `src="http://localhost:${server.port}/videos/embed/${video.uuid}" ` + 114 `title="${video.name}" src="http://localhost:${server.port}/videos/embed/${video.uuid}" ` +
113 'frameborder="0" allowfullscreen></iframe>' 115 'frameborder="0" allowfullscreen></iframe>'
114 116
115 expect(res.body.html).to.equal(expectedHtml) 117 expect(res.body.html).to.equal(expectedHtml)
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts
index 62a59033f..cea98aac7 100644
--- a/server/tests/api/users/users.ts
+++ b/server/tests/api/users/users.ts
@@ -4,10 +4,12 @@ import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { AbuseState, AbuseUpdate, MyUser, User, UserRole, Video, VideoPlaylistType } from '@shared/models' 5import { AbuseState, AbuseUpdate, MyUser, User, UserRole, Video, VideoPlaylistType } from '@shared/models'
6import { CustomConfig } from '@shared/models/server' 6import { CustomConfig } from '@shared/models/server'
7import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
7import { 8import {
8 addVideoCommentThread, 9 addVideoCommentThread,
9 blockUser, 10 blockUser,
10 cleanupTests, 11 cleanupTests,
12 closeAllSequelize,
11 createUser, 13 createUser,
12 deleteMe, 14 deleteMe,
13 flushAndRunServer, 15 flushAndRunServer,
@@ -24,6 +26,7 @@ import {
24 getVideoChannel, 26 getVideoChannel,
25 getVideosList, 27 getVideosList,
26 installPlugin, 28 installPlugin,
29 killallServers,
27 login, 30 login,
28 makePutBodyRequest, 31 makePutBodyRequest,
29 rateVideo, 32 rateVideo,
@@ -31,7 +34,9 @@ import {
31 removeUser, 34 removeUser,
32 removeVideo, 35 removeVideo,
33 reportAbuse, 36 reportAbuse,
37 reRunServer,
34 ServerInfo, 38 ServerInfo,
39 setTokenField,
35 testImage, 40 testImage,
36 unblockUser, 41 unblockUser,
37 updateAbuse, 42 updateAbuse,
@@ -44,10 +49,9 @@ import {
44 waitJobs 49 waitJobs
45} from '../../../../shared/extra-utils' 50} from '../../../../shared/extra-utils'
46import { follow } from '../../../../shared/extra-utils/server/follows' 51import { follow } from '../../../../shared/extra-utils/server/follows'
47import { logout, serverLogin, setAccessTokensToServers } from '../../../../shared/extra-utils/users/login' 52import { logout, refreshToken, setAccessTokensToServers } from '../../../../shared/extra-utils/users/login'
48import { getMyVideos } from '../../../../shared/extra-utils/videos/videos' 53import { getMyVideos } from '../../../../shared/extra-utils/videos/videos'
49import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model' 54import { UserAdminFlag } from '../../../../shared/models/users/user-flag.model'
50import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
51 55
52const expect = chai.expect 56const expect = chai.expect
53 57
@@ -89,6 +93,7 @@ describe('Test users', function () {
89 const client = { id: 'client', secret: server.client.secret } 93 const client = { id: 'client', secret: server.client.secret }
90 const res = await login(server.url, client, server.user, HttpStatusCode.BAD_REQUEST_400) 94 const res = await login(server.url, client, server.user, HttpStatusCode.BAD_REQUEST_400)
91 95
96 expect(res.body.code).to.equal('invalid_client')
92 expect(res.body.error).to.contain('client is invalid') 97 expect(res.body.error).to.contain('client is invalid')
93 }) 98 })
94 99
@@ -96,6 +101,7 @@ describe('Test users', function () {
96 const client = { id: server.client.id, secret: 'coucou' } 101 const client = { id: server.client.id, secret: 'coucou' }
97 const res = await login(server.url, client, server.user, HttpStatusCode.BAD_REQUEST_400) 102 const res = await login(server.url, client, server.user, HttpStatusCode.BAD_REQUEST_400)
98 103
104 expect(res.body.code).to.equal('invalid_client')
99 expect(res.body.error).to.contain('client is invalid') 105 expect(res.body.error).to.contain('client is invalid')
100 }) 106 })
101 }) 107 })
@@ -106,6 +112,7 @@ describe('Test users', function () {
106 const user = { username: 'captain crochet', password: server.user.password } 112 const user = { username: 'captain crochet', password: server.user.password }
107 const res = await login(server.url, server.client, user, HttpStatusCode.BAD_REQUEST_400) 113 const res = await login(server.url, server.client, user, HttpStatusCode.BAD_REQUEST_400)
108 114
115 expect(res.body.code).to.equal('invalid_grant')
109 expect(res.body.error).to.contain('credentials are invalid') 116 expect(res.body.error).to.contain('credentials are invalid')
110 }) 117 })
111 118
@@ -113,6 +120,7 @@ describe('Test users', function () {
113 const user = { username: server.user.username, password: 'mew_three' } 120 const user = { username: server.user.username, password: 'mew_three' }
114 const res = await login(server.url, server.client, user, HttpStatusCode.BAD_REQUEST_400) 121 const res = await login(server.url, server.client, user, HttpStatusCode.BAD_REQUEST_400)
115 122
123 expect(res.body.code).to.equal('invalid_grant')
116 expect(res.body.error).to.contain('credentials are invalid') 124 expect(res.body.error).to.contain('credentials are invalid')
117 }) 125 })
118 126
@@ -245,12 +253,44 @@ describe('Test users', function () {
245 }) 253 })
246 254
247 it('Should be able to login again', async function () { 255 it('Should be able to login again', async function () {
248 server.accessToken = await serverLogin(server) 256 const res = await login(server.url, server.client, server.user)
257 server.accessToken = res.body.access_token
258 server.refreshToken = res.body.refresh_token
259 })
260
261 it('Should be able to get my user information again', async function () {
262 await getMyUserInformation(server.url, server.accessToken)
263 })
264
265 it('Should have an expired access token', async function () {
266 this.timeout(15000)
267
268 await setTokenField(server.internalServerNumber, server.accessToken, 'accessTokenExpiresAt', new Date().toISOString())
269 await setTokenField(server.internalServerNumber, server.accessToken, 'refreshTokenExpiresAt', new Date().toISOString())
270
271 killallServers([ server ])
272 await reRunServer(server)
273
274 await getMyUserInformation(server.url, server.accessToken, 401)
275 })
276
277 it('Should not be able to refresh an access token with an expired refresh token', async function () {
278 await refreshToken(server, server.refreshToken, 400)
249 }) 279 })
250 280
251 it('Should have an expired access token') 281 it('Should refresh the token', async function () {
282 this.timeout(15000)
283
284 const futureDate = new Date(new Date().getTime() + 1000 * 60).toISOString()
285 await setTokenField(server.internalServerNumber, server.accessToken, 'refreshTokenExpiresAt', futureDate)
252 286
253 it('Should refresh the token') 287 killallServers([ server ])
288 await reRunServer(server)
289
290 const res = await refreshToken(server, server.refreshToken)
291 server.accessToken = res.body.access_token
292 server.refreshToken = res.body.refresh_token
293 })
254 294
255 it('Should be able to get my user information again', async function () { 295 it('Should be able to get my user information again', async function () {
256 await getMyUserInformation(server.url, server.accessToken) 296 await getMyUserInformation(server.url, server.accessToken)
@@ -976,6 +1016,7 @@ describe('Test users', function () {
976 }) 1016 })
977 1017
978 after(async function () { 1018 after(async function () {
1019 await closeAllSequelize([ server ])
979 await cleanupTests([ server ]) 1020 await cleanupTests([ server ])
980 }) 1021 })
981}) 1022})
diff --git a/server/tests/api/videos/video-channels.ts b/server/tests/api/videos/video-channels.ts
index 367f99fdd..d12d58e75 100644
--- a/server/tests/api/videos/video-channels.ts
+++ b/server/tests/api/videos/video-channels.ts
@@ -2,16 +2,20 @@
2 2
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { basename } from 'path'
5import { 6import {
6 cleanupTests, 7 cleanupTests,
7 createUser, 8 createUser,
9 deleteVideoChannelImage,
8 doubleFollow, 10 doubleFollow,
9 flushAndRunMultipleServers, 11 flushAndRunMultipleServers,
12 getActorImage,
10 getVideo, 13 getVideo,
14 getVideoChannel,
11 getVideoChannelVideos, 15 getVideoChannelVideos,
12 testImage, 16 testImage,
13 updateVideo, 17 updateVideo,
14 updateVideoChannelAvatar, 18 updateVideoChannelImage,
15 uploadVideo, 19 uploadVideo,
16 userLogin, 20 userLogin,
17 wait 21 wait
@@ -21,7 +25,6 @@ import {
21 deleteVideoChannel, 25 deleteVideoChannel,
22 getAccountVideoChannelsList, 26 getAccountVideoChannelsList,
23 getMyUserInformation, 27 getMyUserInformation,
24 getVideoChannel,
25 getVideoChannelsList, 28 getVideoChannelsList,
26 ServerInfo, 29 ServerInfo,
27 setAccessTokensToServers, 30 setAccessTokensToServers,
@@ -30,9 +33,17 @@ import {
30} from '../../../../shared/extra-utils/index' 33} from '../../../../shared/extra-utils/index'
31import { waitJobs } from '../../../../shared/extra-utils/server/jobs' 34import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
32import { User, Video, VideoChannel, VideoDetails } from '../../../../shared/index' 35import { User, Video, VideoChannel, VideoDetails } from '../../../../shared/index'
36import { ACTOR_IMAGES_SIZE } from '@server/initializers/constants'
33 37
34const expect = chai.expect 38const expect = chai.expect
35 39
40async function findChannel (server: ServerInfo, channelId: number) {
41 const res = await getVideoChannelsList(server.url, 0, 5, '-name')
42 const videoChannel = res.body.data.find(c => c.id === channelId)
43
44 return videoChannel as VideoChannel
45}
46
36describe('Test video channels', function () { 47describe('Test video channels', function () {
37 let servers: ServerInfo[] 48 let servers: ServerInfo[]
38 let userInfo: User 49 let userInfo: User
@@ -262,38 +273,94 @@ describe('Test video channels', function () {
262 }) 273 })
263 274
264 it('Should update video channel avatar', async function () { 275 it('Should update video channel avatar', async function () {
265 this.timeout(5000) 276 this.timeout(15000)
266 277
267 const fixture = 'avatar.png' 278 const fixture = 'avatar.png'
268 279
269 await updateVideoChannelAvatar({ 280 await updateVideoChannelImage({
270 url: servers[0].url, 281 url: servers[0].url,
271 accessToken: servers[0].accessToken, 282 accessToken: servers[0].accessToken,
272 videoChannelName: 'second_video_channel', 283 videoChannelName: 'second_video_channel',
273 fixture 284 fixture,
285 type: 'avatar'
274 }) 286 })
275 287
276 await waitJobs(servers) 288 await waitJobs(servers)
289
290 for (const server of servers) {
291 const videoChannel = await findChannel(server, secondVideoChannelId)
292
293 await testImage(server.url, 'avatar-resized', videoChannel.avatar.path, '.png')
294
295 const row = await getActorImage(server.internalServerNumber, basename(videoChannel.avatar.path))
296 expect(row.height).to.equal(ACTOR_IMAGES_SIZE.AVATARS.height)
297 expect(row.width).to.equal(ACTOR_IMAGES_SIZE.AVATARS.width)
298 }
277 }) 299 })
278 300
279 it('Should have video channel avatar updated', async function () { 301 it('Should update video channel banner', async function () {
302 this.timeout(15000)
303
304 const fixture = 'banner.jpg'
305
306 await updateVideoChannelImage({
307 url: servers[0].url,
308 accessToken: servers[0].accessToken,
309 videoChannelName: 'second_video_channel',
310 fixture,
311 type: 'banner'
312 })
313
314 await waitJobs(servers)
315
280 for (const server of servers) { 316 for (const server of servers) {
281 const res = await getVideoChannelsList(server.url, 0, 1, '-name') 317 const res = await getVideoChannel(server.url, 'second_video_channel@' + servers[0].host)
318 const videoChannel = res.body
282 319
283 const videoChannel = res.body.data.find(c => c.id === secondVideoChannelId) 320 await testImage(server.url, 'banner-resized', videoChannel.banner.path)
284 321
285 await testImage(server.url, 'avatar-resized', videoChannel.avatar.path, '.png') 322 const row = await getActorImage(server.internalServerNumber, basename(videoChannel.banner.path))
323 expect(row.height).to.equal(ACTOR_IMAGES_SIZE.BANNERS.height)
324 expect(row.width).to.equal(ACTOR_IMAGES_SIZE.BANNERS.width)
325 }
326 })
327
328 it('Should delete the video channel avatar', async function () {
329 this.timeout(15000)
330
331 await deleteVideoChannelImage({
332 url: servers[0].url,
333 accessToken: servers[0].accessToken,
334 videoChannelName: 'second_video_channel',
335 type: 'avatar'
336 })
337
338 await waitJobs(servers)
339
340 for (const server of servers) {
341 const videoChannel = await findChannel(server, secondVideoChannelId)
342
343 expect(videoChannel.avatar).to.be.null
286 } 344 }
287 }) 345 })
288 346
289 it('Should get video channel', async function () { 347 it('Should delete the video channel banner', async function () {
290 const res = await getVideoChannel(servers[0].url, 'second_video_channel') 348 this.timeout(15000)
349
350 await deleteVideoChannelImage({
351 url: servers[0].url,
352 accessToken: servers[0].accessToken,
353 videoChannelName: 'second_video_channel',
354 type: 'banner'
355 })
356
357 await waitJobs(servers)
358
359 for (const server of servers) {
360 const videoChannel = await findChannel(server, secondVideoChannelId)
291 361
292 const videoChannel = res.body 362 expect(videoChannel.banner).to.be.null
293 expect(videoChannel.name).to.equal('second_video_channel') 363 }
294 expect(videoChannel.displayName).to.equal('video channel updated')
295 expect(videoChannel.description).to.equal('video channel description updated')
296 expect(videoChannel.support).to.equal('video channel support text updated')
297 }) 364 })
298 365
299 it('Should list the second video channel videos', async function () { 366 it('Should list the second video channel videos', async function () {
diff --git a/server/tests/cli/index.ts b/server/tests/cli/index.ts
index 242589010..7e6eebd17 100644
--- a/server/tests/cli/index.ts
+++ b/server/tests/cli/index.ts
@@ -6,5 +6,6 @@ import './peertube'
6import './plugins' 6import './plugins'
7import './print-transcode-command' 7import './print-transcode-command'
8import './prune-storage' 8import './prune-storage'
9import './regenerate-thumbnails'
9import './reset-password' 10import './reset-password'
10import './update-host' 11import './update-host'
diff --git a/server/tests/cli/regenerate-thumbnails.ts b/server/tests/cli/regenerate-thumbnails.ts
new file mode 100644
index 000000000..8acb9f263
--- /dev/null
+++ b/server/tests/cli/regenerate-thumbnails.ts
@@ -0,0 +1,124 @@
1import 'mocha'
2import { expect } from 'chai'
3import { writeFile } from 'fs-extra'
4import { basename, join } from 'path'
5import { Video, VideoDetails } from '@shared/models'
6import {
7 buildServerDirectory,
8 cleanupTests,
9 doubleFollow,
10 execCLI,
11 flushAndRunMultipleServers,
12 getEnvCli,
13 getVideo,
14 makeRawRequest,
15 ServerInfo,
16 setAccessTokensToServers,
17 uploadVideoAndGetId,
18 waitJobs
19} from '../../../shared/extra-utils'
20import { HttpStatusCode } from '@shared/core-utils'
21
22async function testThumbnail (server: ServerInfo, videoId: number | string) {
23 const res = await getVideo(server.url, videoId)
24 const video: VideoDetails = res.body
25
26 const res1 = await makeRawRequest(join(server.url, video.thumbnailPath), HttpStatusCode.OK_200)
27 expect(res1.body).to.not.have.lengthOf(0)
28
29 const res2 = await makeRawRequest(join(server.url, video.thumbnailPath), HttpStatusCode.OK_200)
30 expect(res2.body).to.not.have.lengthOf(0)
31}
32
33describe('Test regenerate thumbnails script', function () {
34 let servers: ServerInfo[]
35
36 let video1: Video
37 let video2: Video
38 let remoteVideo: Video
39
40 let thumbnail1Path: string
41 let thumbnailRemotePath: string
42
43 before(async function () {
44 this.timeout(60000)
45
46 servers = await flushAndRunMultipleServers(2)
47 await setAccessTokensToServers(servers)
48
49 await doubleFollow(servers[0], servers[1])
50
51 {
52 const videoUUID1 = (await uploadVideoAndGetId({ server: servers[0], videoName: 'video 1' })).uuid
53 video1 = await (getVideo(servers[0].url, videoUUID1).then(res => res.body))
54
55 thumbnail1Path = join(buildServerDirectory(servers[0], 'thumbnails'), basename(video1.thumbnailPath))
56
57 const videoUUID2 = (await uploadVideoAndGetId({ server: servers[0], videoName: 'video 2' })).uuid
58 video2 = await (getVideo(servers[0].url, videoUUID2).then(res => res.body))
59 }
60
61 {
62 const videoUUID = (await uploadVideoAndGetId({ server: servers[1], videoName: 'video 3' })).uuid
63 await waitJobs(servers)
64
65 remoteVideo = await (getVideo(servers[0].url, videoUUID).then(res => res.body))
66
67 thumbnailRemotePath = join(buildServerDirectory(servers[0], 'thumbnails'), basename(remoteVideo.thumbnailPath))
68 }
69
70 await writeFile(thumbnail1Path, '')
71 await writeFile(thumbnailRemotePath, '')
72 })
73
74 it('Should have empty thumbnails', async function () {
75 {
76 const res = await makeRawRequest(join(servers[0].url, video1.thumbnailPath), HttpStatusCode.OK_200)
77 expect(res.body).to.have.lengthOf(0)
78 }
79
80 {
81 const res = await makeRawRequest(join(servers[0].url, video2.thumbnailPath), HttpStatusCode.OK_200)
82 expect(res.body).to.not.have.lengthOf(0)
83 }
84
85 {
86 const res = await makeRawRequest(join(servers[0].url, remoteVideo.thumbnailPath), HttpStatusCode.OK_200)
87 expect(res.body).to.have.lengthOf(0)
88 }
89 })
90
91 it('Should regenerate local thumbnails from the CLI', async function () {
92 this.timeout(15000)
93
94 const env = getEnvCli(servers[0])
95 await execCLI(`${env} npm run regenerate-thumbnails`)
96 })
97
98 it('Should have generated new thumbnail files', async function () {
99 await testThumbnail(servers[0], video1.uuid)
100 await testThumbnail(servers[0], video2.uuid)
101
102 const res = await makeRawRequest(join(servers[0].url, remoteVideo.thumbnailPath), HttpStatusCode.OK_200)
103 expect(res.body).to.have.lengthOf(0)
104 })
105
106 it('Should have deleted old thumbnail files', async function () {
107 {
108 await makeRawRequest(join(servers[0].url, video1.thumbnailPath), HttpStatusCode.NOT_FOUND_404)
109 }
110
111 {
112 await makeRawRequest(join(servers[0].url, video2.thumbnailPath), HttpStatusCode.NOT_FOUND_404)
113 }
114
115 {
116 const res = await makeRawRequest(join(servers[0].url, remoteVideo.thumbnailPath), HttpStatusCode.OK_200)
117 expect(res.body).to.have.lengthOf(0)
118 }
119 })
120
121 after(async function () {
122 await cleanupTests(servers)
123 })
124})
diff --git a/server/tests/feeds/feeds.ts b/server/tests/feeds/feeds.ts
index f1055ea44..7bad81751 100644
--- a/server/tests/feeds/feeds.ts
+++ b/server/tests/feeds/feeds.ts
@@ -2,7 +2,7 @@
2 2
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import * as libxmljs from 'libxmljs' 5import * as xmlParser from 'fast-xml-parser'
6import { 6import {
7 addAccountToAccountBlocklist, 7 addAccountToAccountBlocklist,
8 addAccountToServerBlocklist, 8 addAccountToServerBlocklist,
@@ -139,12 +139,15 @@ describe('Test syndication feeds', () => {
139 it('Should contain a valid enclosure (covers RSS 2.0 endpoint)', async function () { 139 it('Should contain a valid enclosure (covers RSS 2.0 endpoint)', async function () {
140 for (const server of servers) { 140 for (const server of servers) {
141 const rss = await getXMLfeed(server.url, 'videos') 141 const rss = await getXMLfeed(server.url, 'videos')
142 const xmlDoc = libxmljs.parseXmlString(rss.text) 142 expect(xmlParser.validate(rss.text)).to.be.true
143 const xmlEnclosure = xmlDoc.get('/rss/channel/item/enclosure') 143
144 expect(xmlEnclosure).to.exist 144 const xmlDoc = xmlParser.parse(rss.text, { parseAttributeValue: true, ignoreAttributes: false })
145 expect(xmlEnclosure.attr('type').value()).to.be.equal('application/x-bittorrent') 145
146 expect(xmlEnclosure.attr('length').value()).to.be.equal('218910') 146 const enclosure = xmlDoc.rss.channel.item[0].enclosure
147 expect(xmlEnclosure.attr('url').value()).to.contain('720.torrent') 147 expect(enclosure).to.exist
148 expect(enclosure['@_type']).to.equal('application/x-bittorrent')
149 expect(enclosure['@_length']).to.equal(218910)
150 expect(enclosure['@_url']).to.contain('720.torrent')
148 } 151 }
149 }) 152 })
150 153
diff --git a/server/tests/fixtures/banner-resized.jpg b/server/tests/fixtures/banner-resized.jpg
new file mode 100644
index 000000000..13ea422cb
--- /dev/null
+++ b/server/tests/fixtures/banner-resized.jpg
Binary files differ
diff --git a/server/tests/fixtures/banner.jpg b/server/tests/fixtures/banner.jpg
new file mode 100644
index 000000000..e5f284f59
--- /dev/null
+++ b/server/tests/fixtures/banner.jpg
Binary files differ
diff --git a/server/tests/fixtures/peertube-plugin-test/main.js b/server/tests/fixtures/peertube-plugin-test/main.js
index 305d92002..ee0bc39f3 100644
--- a/server/tests/fixtures/peertube-plugin-test/main.js
+++ b/server/tests/fixtures/peertube-plugin-test/main.js
@@ -184,6 +184,76 @@ async function register ({ registerHook, registerSetting, settingsManager, stora
184 return result 184 return result
185 } 185 }
186 }) 186 })
187
188 registerHook({
189 target: 'filter:api.download.torrent.allowed.result',
190 handler: (result, params) => {
191 if (params && params.downloadName.includes('bad torrent')) {
192 return { allowed: false, errorMessage: 'Liu Bei' }
193 }
194
195 return result
196 }
197 })
198
199 registerHook({
200 target: 'filter:api.download.video.allowed.result',
201 handler: (result, params) => {
202 if (params && !params.streamingPlaylist && params.video.name.includes('bad file')) {
203 return { allowed: false, errorMessage: 'Cao Cao' }
204 }
205
206 if (params && params.streamingPlaylist && params.video.name.includes('bad playlist file')) {
207 return { allowed: false, errorMessage: 'Sun Jian' }
208 }
209
210 return result
211 }
212 })
213
214 registerHook({
215 target: 'filter:html.embed.video.allowed.result',
216 handler: (result, params) => {
217 return {
218 allowed: false,
219 html: 'Lu Bu'
220 }
221 }
222 })
223
224 registerHook({
225 target: 'filter:html.embed.video-playlist.allowed.result',
226 handler: (result, params) => {
227 return {
228 allowed: false,
229 html: 'Diao Chan'
230 }
231 }
232 })
233
234 {
235 const searchHooks = [
236 'filter:api.search.videos.local.list.params',
237 'filter:api.search.videos.local.list.result',
238 'filter:api.search.videos.index.list.params',
239 'filter:api.search.videos.index.list.result',
240 'filter:api.search.video-channels.local.list.params',
241 'filter:api.search.video-channels.local.list.result',
242 'filter:api.search.video-channels.index.list.params',
243 'filter:api.search.video-channels.index.list.result',
244 ]
245
246 for (const h of searchHooks) {
247 registerHook({
248 target: h,
249 handler: (obj) => {
250 peertubeHelpers.logger.debug('Run hook %s.', h)
251
252 return obj
253 }
254 })
255 }
256 }
187} 257}
188 258
189async function unregister () { 259async function unregister () {
diff --git a/server/tests/fixtures/thumbnail-playlist.jpg b/server/tests/fixtures/thumbnail-playlist.jpg
index 19db4f18c..62cd77435 100644
--- a/server/tests/fixtures/thumbnail-playlist.jpg
+++ b/server/tests/fixtures/thumbnail-playlist.jpg
Binary files differ
diff --git a/server/tests/fixtures/video_import_thumbnail.jpg b/server/tests/fixtures/video_import_thumbnail.jpg
index fcc50b75f..9ee1bc382 100644
--- a/server/tests/fixtures/video_import_thumbnail.jpg
+++ b/server/tests/fixtures/video_import_thumbnail.jpg
Binary files differ
diff --git a/server/tests/fixtures/video_short.mp4.jpg b/server/tests/fixtures/video_short.mp4.jpg
index 48790ffec..62cd77435 100644
--- a/server/tests/fixtures/video_short.mp4.jpg
+++ b/server/tests/fixtures/video_short.mp4.jpg
Binary files differ
diff --git a/server/tests/fixtures/video_short.ogv.jpg b/server/tests/fixtures/video_short.ogv.jpg
index c4c1d00e5..62cd77435 100644
--- a/server/tests/fixtures/video_short.ogv.jpg
+++ b/server/tests/fixtures/video_short.ogv.jpg
Binary files differ
diff --git a/server/tests/fixtures/video_short.webm.jpg b/server/tests/fixtures/video_short.webm.jpg
index 7f8047516..62cd77435 100644
--- a/server/tests/fixtures/video_short.webm.jpg
+++ b/server/tests/fixtures/video_short.webm.jpg
Binary files differ
diff --git a/server/tests/fixtures/video_short1.webm.jpg b/server/tests/fixtures/video_short1.webm.jpg
index 582eb9ea3..615cb2a5d 100644
--- a/server/tests/fixtures/video_short1.webm.jpg
+++ b/server/tests/fixtures/video_short1.webm.jpg
Binary files differ
diff --git a/server/tests/fixtures/video_short2.webm.jpg b/server/tests/fixtures/video_short2.webm.jpg
index b331aba3b..aa3126381 100644
--- a/server/tests/fixtures/video_short2.webm.jpg
+++ b/server/tests/fixtures/video_short2.webm.jpg
Binary files differ
diff --git a/server/tests/fixtures/video_short3.webm.jpg b/server/tests/fixtures/video_short3.webm.jpg
index ec8652167..62cd77435 100644
--- a/server/tests/fixtures/video_short3.webm.jpg
+++ b/server/tests/fixtures/video_short3.webm.jpg
Binary files differ
diff --git a/server/tests/helpers/request.ts b/server/tests/helpers/request.ts
index f8b2d599b..5e77f129e 100644
--- a/server/tests/helpers/request.ts
+++ b/server/tests/helpers/request.ts
@@ -1,11 +1,11 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import 'mocha' 3import 'mocha'
4import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
5import { get4KFileUrl, root, wait } from '../../../shared/extra-utils'
6import { join } from 'path'
7import { pathExists, remove } from 'fs-extra'
8import { expect } from 'chai' 4import { expect } from 'chai'
5import { pathExists, remove } from 'fs-extra'
6import { join } from 'path'
7import { get4KFileUrl, root, wait } from '../../../shared/extra-utils'
8import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
9 9
10describe('Request helpers', function () { 10describe('Request helpers', function () {
11 const destPath1 = join(root(), 'test-output-1.txt') 11 const destPath1 = join(root(), 'test-output-1.txt')
@@ -13,7 +13,7 @@ describe('Request helpers', function () {
13 13
14 it('Should throw an error when the bytes limit is exceeded for request', async function () { 14 it('Should throw an error when the bytes limit is exceeded for request', async function () {
15 try { 15 try {
16 await doRequest({ uri: get4KFileUrl() }, 3) 16 await doRequest(get4KFileUrl(), { bodyKBLimit: 3 })
17 } catch { 17 } catch {
18 return 18 return
19 } 19 }
@@ -23,7 +23,7 @@ describe('Request helpers', function () {
23 23
24 it('Should throw an error when the bytes limit is exceeded for request and save file', async function () { 24 it('Should throw an error when the bytes limit is exceeded for request and save file', async function () {
25 try { 25 try {
26 await doRequestAndSaveToFile({ uri: get4KFileUrl() }, destPath1, 3) 26 await doRequestAndSaveToFile(get4KFileUrl(), destPath1, { bodyKBLimit: 3 })
27 } catch { 27 } catch {
28 28
29 await wait(500) 29 await wait(500)
@@ -35,8 +35,8 @@ describe('Request helpers', function () {
35 }) 35 })
36 36
37 it('Should succeed if the file is below the limit', async function () { 37 it('Should succeed if the file is below the limit', async function () {
38 await doRequest({ uri: get4KFileUrl() }, 5) 38 await doRequest(get4KFileUrl(), { bodyKBLimit: 5 })
39 await doRequestAndSaveToFile({ uri: get4KFileUrl() }, destPath2, 5) 39 await doRequestAndSaveToFile(get4KFileUrl(), destPath2, { bodyKBLimit: 5 })
40 40
41 expect(await pathExists(destPath2)).to.be.true 41 expect(await pathExists(destPath2)).to.be.true
42 }) 42 })
diff --git a/server/tests/plugins/external-auth.ts b/server/tests/plugins/external-auth.ts
index a1b5e8f5d..5addb45c7 100644
--- a/server/tests/plugins/external-auth.ts
+++ b/server/tests/plugins/external-auth.ts
@@ -137,7 +137,7 @@ describe('Test external auth plugins', function () {
137 137
138 await loginUsingExternalToken(server, 'cyan', externalAuthToken, HttpStatusCode.BAD_REQUEST_400) 138 await loginUsingExternalToken(server, 'cyan', externalAuthToken, HttpStatusCode.BAD_REQUEST_400)
139 139
140 await waitUntilLog(server, 'expired external auth token') 140 await waitUntilLog(server, 'expired external auth token', 2)
141 }) 141 })
142 142
143 it('Should auto login Cyan, create the user and use the token', async function () { 143 it('Should auto login Cyan, create the user and use the token', async function () {
diff --git a/server/tests/plugins/filter-hooks.ts b/server/tests/plugins/filter-hooks.ts
index d88170201..ac958c5f5 100644
--- a/server/tests/plugins/filter-hooks.ts
+++ b/server/tests/plugins/filter-hooks.ts
@@ -2,11 +2,15 @@
2 2
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { advancedVideoChannelSearch } from '@shared/extra-utils/search/video-channels'
5import { ServerConfig } from '@shared/models' 6import { ServerConfig } from '@shared/models'
7import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
6import { 8import {
7 addVideoCommentReply, 9 addVideoCommentReply,
8 addVideoCommentThread, 10 addVideoCommentThread,
11 advancedVideosSearch,
9 createLive, 12 createLive,
13 createVideoPlaylist,
10 doubleFollow, 14 doubleFollow,
11 getAccountVideos, 15 getAccountVideos,
12 getConfig, 16 getConfig,
@@ -15,24 +19,33 @@ import {
15 getVideo, 19 getVideo,
16 getVideoChannelVideos, 20 getVideoChannelVideos,
17 getVideoCommentThreads, 21 getVideoCommentThreads,
22 getVideoPlaylist,
18 getVideosList, 23 getVideosList,
19 getVideosListPagination, 24 getVideosListPagination,
20 getVideoThreadComments, 25 getVideoThreadComments,
21 getVideoWithToken, 26 getVideoWithToken,
22 installPlugin, 27 installPlugin,
28 makeRawRequest,
23 registerUser, 29 registerUser,
24 setAccessTokensToServers, 30 setAccessTokensToServers,
25 setDefaultVideoChannel, 31 setDefaultVideoChannel,
26 updateCustomSubConfig, 32 updateCustomSubConfig,
27 updateVideo, 33 updateVideo,
28 uploadVideo, 34 uploadVideo,
35 uploadVideoAndGetId,
29 waitJobs 36 waitJobs
30} from '../../../shared/extra-utils' 37} from '../../../shared/extra-utils'
31import { cleanupTests, flushAndRunMultipleServers, ServerInfo } from '../../../shared/extra-utils/server/servers' 38import { cleanupTests, flushAndRunMultipleServers, ServerInfo, waitUntilLog } from '../../../shared/extra-utils/server/servers'
32import { getGoodVideoUrl, getMyVideoImports, importVideo } from '../../../shared/extra-utils/videos/video-imports' 39import { getGoodVideoUrl, getMyVideoImports, importVideo } from '../../../shared/extra-utils/videos/video-imports'
33import { VideoDetails, VideoImport, VideoImportState, VideoPrivacy } from '../../../shared/models/videos' 40import {
41 VideoDetails,
42 VideoImport,
43 VideoImportState,
44 VideoPlaylist,
45 VideoPlaylistPrivacy,
46 VideoPrivacy
47} from '../../../shared/models/videos'
34import { VideoCommentThreadTree } from '../../../shared/models/videos/video-comment.model' 48import { VideoCommentThreadTree } from '../../../shared/models/videos/video-comment.model'
35import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
36 49
37const expect = chai.expect 50const expect = chai.expect
38 51
@@ -355,6 +368,165 @@ describe('Test plugin filter hooks', function () {
355 }) 368 })
356 }) 369 })
357 370
371 describe('Download hooks', function () {
372 const downloadVideos: VideoDetails[] = []
373
374 before(async function () {
375 this.timeout(60000)
376
377 await updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
378 transcoding: {
379 webtorrent: {
380 enabled: true
381 },
382 hls: {
383 enabled: true
384 }
385 }
386 })
387
388 const uuids: string[] = []
389
390 for (const name of [ 'bad torrent', 'bad file', 'bad playlist file' ]) {
391 const uuid = (await uploadVideoAndGetId({ server: servers[0], videoName: name })).uuid
392 uuids.push(uuid)
393 }
394
395 await waitJobs(servers)
396
397 for (const uuid of uuids) {
398 const res = await getVideo(servers[0].url, uuid)
399 downloadVideos.push(res.body)
400 }
401 })
402
403 it('Should run filter:api.download.torrent.allowed.result', async function () {
404 const res = await makeRawRequest(downloadVideos[0].files[0].torrentDownloadUrl, 403)
405 expect(res.body.error).to.equal('Liu Bei')
406
407 await makeRawRequest(downloadVideos[1].files[0].torrentDownloadUrl, 200)
408 await makeRawRequest(downloadVideos[2].files[0].torrentDownloadUrl, 200)
409 })
410
411 it('Should run filter:api.download.video.allowed.result', async function () {
412 {
413 const res = await makeRawRequest(downloadVideos[1].files[0].fileDownloadUrl, 403)
414 expect(res.body.error).to.equal('Cao Cao')
415
416 await makeRawRequest(downloadVideos[0].files[0].fileDownloadUrl, 200)
417 await makeRawRequest(downloadVideos[2].files[0].fileDownloadUrl, 200)
418 }
419
420 {
421 const res = await makeRawRequest(downloadVideos[2].streamingPlaylists[0].files[0].fileDownloadUrl, 403)
422 expect(res.body.error).to.equal('Sun Jian')
423
424 await makeRawRequest(downloadVideos[2].files[0].fileDownloadUrl, 200)
425
426 await makeRawRequest(downloadVideos[0].streamingPlaylists[0].files[0].fileDownloadUrl, 200)
427 await makeRawRequest(downloadVideos[1].streamingPlaylists[0].files[0].fileDownloadUrl, 200)
428 }
429 })
430 })
431
432 describe('Embed filters', function () {
433 const embedVideos: VideoDetails[] = []
434 const embedPlaylists: VideoPlaylist[] = []
435
436 before(async function () {
437 this.timeout(60000)
438
439 await updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
440 transcoding: {
441 enabled: false
442 }
443 })
444
445 for (const name of [ 'bad embed', 'good embed' ]) {
446 {
447 const uuid = (await uploadVideoAndGetId({ server: servers[0], videoName: name })).uuid
448 const res = await getVideo(servers[0].url, uuid)
449 embedVideos.push(res.body)
450 }
451
452 {
453 const playlistAttrs = { displayName: name, videoChannelId: servers[0].videoChannel.id, privacy: VideoPlaylistPrivacy.PUBLIC }
454 const res = await createVideoPlaylist({ url: servers[0].url, token: servers[0].accessToken, playlistAttrs })
455
456 const resPlaylist = await getVideoPlaylist(servers[0].url, res.body.videoPlaylist.id)
457 embedPlaylists.push(resPlaylist.body)
458 }
459 }
460 })
461
462 it('Should run filter:html.embed.video.allowed.result', async function () {
463 const res = await makeRawRequest(servers[0].url + embedVideos[0].embedPath, 200)
464 expect(res.text).to.equal('Lu Bu')
465 })
466
467 it('Should run filter:html.embed.video-playlist.allowed.result', async function () {
468 const res = await makeRawRequest(servers[0].url + embedPlaylists[0].embedPath, 200)
469 expect(res.text).to.equal('Diao Chan')
470 })
471 })
472
473 describe('Search filters', function () {
474
475 before(async function () {
476 await updateCustomSubConfig(servers[0].url, servers[0].accessToken, {
477 search: {
478 searchIndex: {
479 enabled: true,
480 isDefaultSearch: false,
481 disableLocalSearch: false
482 }
483 }
484 })
485 })
486
487 it('Should run filter:api.search.videos.local.list.{params,result}', async function () {
488 await advancedVideosSearch(servers[0].url, {
489 search: 'Sun Quan'
490 })
491
492 await waitUntilLog(servers[0], 'Run hook filter:api.search.videos.local.list.params', 1)
493 await waitUntilLog(servers[0], 'Run hook filter:api.search.videos.local.list.result', 1)
494 })
495
496 it('Should run filter:api.search.videos.index.list.{params,result}', async function () {
497 await advancedVideosSearch(servers[0].url, {
498 search: 'Sun Quan',
499 searchTarget: 'search-index'
500 })
501
502 await waitUntilLog(servers[0], 'Run hook filter:api.search.videos.local.list.params', 1)
503 await waitUntilLog(servers[0], 'Run hook filter:api.search.videos.local.list.result', 1)
504 await waitUntilLog(servers[0], 'Run hook filter:api.search.videos.index.list.params', 1)
505 await waitUntilLog(servers[0], 'Run hook filter:api.search.videos.index.list.result', 1)
506 })
507
508 it('Should run filter:api.search.video-channels.local.list.{params,result}', async function () {
509 await advancedVideoChannelSearch(servers[0].url, {
510 search: 'Sun Ce'
511 })
512
513 await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.local.list.params', 1)
514 await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.local.list.result', 1)
515 })
516
517 it('Should run filter:api.search.video-channels.index.list.{params,result}', async function () {
518 await advancedVideoChannelSearch(servers[0].url, {
519 search: 'Sun Ce',
520 searchTarget: 'search-index'
521 })
522
523 await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.local.list.params', 1)
524 await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.local.list.result', 1)
525 await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.index.list.params', 1)
526 await waitUntilLog(servers[0], 'Run hook filter:api.search.video-channels.index.list.result', 1)
527 })
528 })
529
358 after(async function () { 530 after(async function () {
359 await cleanupTests(servers) 531 await cleanupTests(servers)
360 }) 532 })
diff --git a/server/tools/peertube-import-videos.ts b/server/tools/peertube-import-videos.ts
index 9be0834ba..915995031 100644
--- a/server/tools/peertube-import-videos.ts
+++ b/server/tools/peertube-import-videos.ts
@@ -202,10 +202,7 @@ async function uploadVideoOnPeerTube (parameters: {
202 if (videoInfo.thumbnail) { 202 if (videoInfo.thumbnail) {
203 thumbnailfile = join(cwd, sha256(videoInfo.thumbnail) + '.jpg') 203 thumbnailfile = join(cwd, sha256(videoInfo.thumbnail) + '.jpg')
204 204
205 await doRequestAndSaveToFile({ 205 await doRequestAndSaveToFile(videoInfo.thumbnail, thumbnailfile)
206 method: 'GET',
207 uri: videoInfo.thumbnail
208 }, thumbnailfile)
209 } 206 }
210 207
211 const originallyPublishedAt = buildOriginallyPublishedAt(videoInfo) 208 const originallyPublishedAt = buildOriginallyPublishedAt(videoInfo)
diff --git a/server/types/models/account/account.ts b/server/types/models/account/account.ts
index d2add9810..9513acad8 100644
--- a/server/types/models/account/account.ts
+++ b/server/types/models/account/account.ts
@@ -1,7 +1,10 @@
1import { FunctionProperties, PickWith } from '@shared/core-utils'
1import { AccountModel } from '../../../models/account/account' 2import { AccountModel } from '../../../models/account/account'
3import { MChannelDefault } from '../video/video-channels'
4import { MAccountBlocklistId } from './account-blocklist'
2import { 5import {
3 MActor, 6 MActor,
4 MActorAP, 7 MActorAPAccount,
5 MActorAPI, 8 MActorAPI,
6 MActorAudience, 9 MActorAudience,
7 MActorDefault, 10 MActorDefault,
@@ -13,9 +16,6 @@ import {
13 MActorSummaryFormattable, 16 MActorSummaryFormattable,
14 MActorUrl 17 MActorUrl
15} from './actor' 18} from './actor'
16import { FunctionProperties, PickWith } from '@shared/core-utils'
17import { MAccountBlocklistId } from './account-blocklist'
18import { MChannelDefault } from '../video/video-channels'
19 19
20type Use<K extends keyof AccountModel, M> = PickWith<AccountModel, K, M> 20type Use<K extends keyof AccountModel, M> = PickWith<AccountModel, K, M>
21 21
@@ -106,4 +106,4 @@ export type MAccountFormattable =
106 106
107export type MAccountAP = 107export type MAccountAP =
108 Pick<MAccount, 'name' | 'description'> & 108 Pick<MAccount, 'name' | 'description'> &
109 Use<'Actor', MActorAP> 109 Use<'Actor', MActorAPAccount>
diff --git a/server/types/models/account/actor-follow.ts b/server/types/models/account/actor-follow.ts
index 8c213d09c..8e19c6140 100644
--- a/server/types/models/account/actor-follow.ts
+++ b/server/types/models/account/actor-follow.ts
@@ -1,16 +1,15 @@
1import { PickWith } from '@shared/core-utils'
1import { ActorFollowModel } from '../../../models/activitypub/actor-follow' 2import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
2import { 3import {
3 MActor, 4 MActor,
4 MActorChannelAccountActor, 5 MActorChannelAccountActor,
5 MActorDefault, 6 MActorDefault,
6 MActorDefaultAccountChannel, 7 MActorDefaultAccountChannel,
8 MActorDefaultChannelId,
7 MActorFormattable, 9 MActorFormattable,
8 MActorHost, 10 MActorHost,
9 MActorUsername 11 MActorUsername
10} from './actor' 12} from './actor'
11import { PickWith } from '@shared/core-utils'
12import { ActorModel } from '@server/models/activitypub/actor'
13import { MChannelDefault } from '../video/video-channels'
14 13
15type Use<K extends keyof ActorFollowModel, M> = PickWith<ActorFollowModel, K, M> 14type Use<K extends keyof ActorFollowModel, M> = PickWith<ActorFollowModel, K, M>
16 15
@@ -47,14 +46,10 @@ export type MActorFollowFull =
47 46
48// For subscriptions 47// For subscriptions
49 48
50type SubscriptionFollowing =
51 MActorDefault &
52 PickWith<ActorModel, 'VideoChannel', MChannelDefault>
53
54export type MActorFollowActorsDefaultSubscription = 49export type MActorFollowActorsDefaultSubscription =
55 MActorFollow & 50 MActorFollow &
56 Use<'ActorFollower', MActorDefault> & 51 Use<'ActorFollower', MActorDefault> &
57 Use<'ActorFollowing', SubscriptionFollowing> 52 Use<'ActorFollowing', MActorDefaultChannelId>
58 53
59export type MActorFollowSubscriptions = 54export type MActorFollowSubscriptions =
60 MActorFollow & 55 MActorFollow &
diff --git a/server/types/models/account/actor-image.ts b/server/types/models/account/actor-image.ts
new file mode 100644
index 000000000..e59f8b141
--- /dev/null
+++ b/server/types/models/account/actor-image.ts
@@ -0,0 +1,12 @@
1import { ActorImageModel } from '../../../models/account/actor-image'
2import { FunctionProperties } from '@shared/core-utils'
3
4export type MActorImage = ActorImageModel
5
6// ############################################################################
7
8// Format for API or AP object
9
10export type MActorImageFormattable =
11 FunctionProperties<MActorImage> &
12 Pick<MActorImage, 'filename' | 'createdAt' | 'updatedAt'>
diff --git a/server/types/models/account/actor.ts b/server/types/models/account/actor.ts
index ee0d05f4e..8f3f30074 100644
--- a/server/types/models/account/actor.ts
+++ b/server/types/models/account/actor.ts
@@ -1,15 +1,17 @@
1import { ActorModel } from '../../../models/activitypub/actor' 1
2import { FunctionProperties, PickWith, PickWithOpt } from '@shared/core-utils' 2import { FunctionProperties, PickWith, PickWithOpt } from '@shared/core-utils'
3import { MAccount, MAccountDefault, MAccountId, MAccountIdActor } from './account' 3import { ActorModel } from '../../../models/activitypub/actor'
4import { MServer, MServerHost, MServerHostBlocks, MServerRedundancyAllowed } from '../server' 4import { MServer, MServerHost, MServerHostBlocks, MServerRedundancyAllowed } from '../server'
5import { MAvatar, MAvatarFormattable } from './avatar'
6import { MChannel, MChannelAccountActor, MChannelAccountDefault, MChannelId, MChannelIdActor } from '../video' 5import { MChannel, MChannelAccountActor, MChannelAccountDefault, MChannelId, MChannelIdActor } from '../video'
6import { MAccount, MAccountDefault, MAccountId, MAccountIdActor } from './account'
7import { MActorImage, MActorImageFormattable } from './actor-image'
7 8
8type Use<K extends keyof ActorModel, M> = PickWith<ActorModel, K, M> 9type Use<K extends keyof ActorModel, M> = PickWith<ActorModel, K, M>
10type UseOpt<K extends keyof ActorModel, M> = PickWithOpt<ActorModel, K, M>
9 11
10// ############################################################################ 12// ############################################################################
11 13
12export type MActor = Omit<ActorModel, 'Account' | 'VideoChannel' | 'ActorFollowing' | 'Avatar' | 'ActorFollowers' | 'Server'> 14export type MActor = Omit<ActorModel, 'Account' | 'VideoChannel' | 'ActorFollowing' | 'Avatar' | 'ActorFollowers' | 'Server' | 'Banner'>
13 15
14// ############################################################################ 16// ############################################################################
15 17
@@ -34,7 +36,7 @@ export type MActorRedundancyAllowedOpt = PickWithOpt<ActorModel, 'Server', MServ
34export type MActorDefaultLight = 36export type MActorDefaultLight =
35 MActorLight & 37 MActorLight &
36 Use<'Server', MServerHost> & 38 Use<'Server', MServerHost> &
37 Use<'Avatar', MAvatar> 39 Use<'Avatar', MActorImage>
38 40
39export type MActorAccountId = 41export type MActorAccountId =
40 MActor & 42 MActor &
@@ -75,10 +77,25 @@ export type MActorServer =
75 77
76// Complex actor associations 78// Complex actor associations
77 79
80export type MActorImages =
81 MActor &
82 Use<'Avatar', MActorImage> &
83 UseOpt<'Banner', MActorImage>
84
78export type MActorDefault = 85export type MActorDefault =
79 MActor & 86 MActor &
80 Use<'Server', MServer> & 87 Use<'Server', MServer> &
81 Use<'Avatar', MAvatar> 88 Use<'Avatar', MActorImage>
89
90export type MActorDefaultChannelId =
91 MActorDefault &
92 Use<'VideoChannel', MChannelId>
93
94export type MActorDefaultBanner =
95 MActor &
96 Use<'Server', MServer> &
97 Use<'Avatar', MActorImage> &
98 Use<'Banner', MActorImage>
82 99
83// Actor with channel that is associated to an account and its actor 100// Actor with channel that is associated to an account and its actor
84// Actor -> VideoChannel -> Account -> Actor 101// Actor -> VideoChannel -> Account -> Actor
@@ -89,7 +106,8 @@ export type MActorChannelAccountActor =
89export type MActorFull = 106export type MActorFull =
90 MActor & 107 MActor &
91 Use<'Server', MServer> & 108 Use<'Server', MServer> &
92 Use<'Avatar', MAvatar> & 109 Use<'Avatar', MActorImage> &
110 Use<'Banner', MActorImage> &
93 Use<'Account', MAccount> & 111 Use<'Account', MAccount> &
94 Use<'VideoChannel', MChannelAccountActor> 112 Use<'VideoChannel', MChannelAccountActor>
95 113
@@ -97,7 +115,8 @@ export type MActorFull =
97export type MActorFullActor = 115export type MActorFullActor =
98 MActor & 116 MActor &
99 Use<'Server', MServer> & 117 Use<'Server', MServer> &
100 Use<'Avatar', MAvatar> & 118 Use<'Avatar', MActorImage> &
119 Use<'Banner', MActorImage> &
101 Use<'Account', MAccountDefault> & 120 Use<'Account', MAccountDefault> &
102 Use<'VideoChannel', MChannelAccountDefault> 121 Use<'VideoChannel', MChannelAccountDefault>
103 122
@@ -109,7 +128,7 @@ export type MActorSummary =
109 FunctionProperties<MActor> & 128 FunctionProperties<MActor> &
110 Pick<MActor, 'id' | 'preferredUsername' | 'url' | 'serverId' | 'avatarId'> & 129 Pick<MActor, 'id' | 'preferredUsername' | 'url' | 'serverId' | 'avatarId'> &
111 Use<'Server', MServerHost> & 130 Use<'Server', MServerHost> &
112 Use<'Avatar', MAvatar> 131 Use<'Avatar', MActorImage>
113 132
114export type MActorSummaryBlocks = 133export type MActorSummaryBlocks =
115 MActorSummary & 134 MActorSummary &
@@ -127,13 +146,21 @@ export type MActorSummaryFormattable =
127 FunctionProperties<MActor> & 146 FunctionProperties<MActor> &
128 Pick<MActor, 'url' | 'preferredUsername'> & 147 Pick<MActor, 'url' | 'preferredUsername'> &
129 Use<'Server', MServerHost> & 148 Use<'Server', MServerHost> &
130 Use<'Avatar', MAvatarFormattable> 149 Use<'Avatar', MActorImageFormattable>
131 150
132export type MActorFormattable = 151export type MActorFormattable =
133 MActorSummaryFormattable & 152 MActorSummaryFormattable &
134 Pick<MActor, 'id' | 'followingCount' | 'followersCount' | 'createdAt' | 'updatedAt'> & 153 Pick<MActor, 'id' | 'followingCount' | 'followersCount' | 'createdAt' | 'updatedAt' | 'bannerId' | 'avatarId'> &
135 Use<'Server', MServerHost & Partial<Pick<MServer, 'redundancyAllowed'>>> 154 Use<'Server', MServerHost & Partial<Pick<MServer, 'redundancyAllowed'>>> &
155 UseOpt<'Banner', MActorImageFormattable>
136 156
137export type MActorAP = 157type MActorAPBase =
138 MActor & 158 MActor &
139 Use<'Avatar', MAvatar> 159 Use<'Avatar', MActorImage>
160
161export type MActorAPAccount =
162 MActorAPBase
163
164export type MActorAPChannel =
165 MActorAPBase &
166 Use<'Banner', MActorImage>
diff --git a/server/types/models/account/avatar.ts b/server/types/models/account/avatar.ts
deleted file mode 100644
index 0489a8599..000000000
--- a/server/types/models/account/avatar.ts
+++ /dev/null
@@ -1,12 +0,0 @@
1import { AvatarModel } from '../../../models/avatar/avatar'
2import { FunctionProperties } from '@shared/core-utils'
3
4export type MAvatar = AvatarModel
5
6// ############################################################################
7
8// Format for API or AP object
9
10export type MAvatarFormattable =
11 FunctionProperties<MAvatar> &
12 Pick<MAvatar, 'filename' | 'createdAt' | 'updatedAt'>
diff --git a/server/types/models/account/index.ts b/server/types/models/account/index.ts
index 513c09c40..e3fc00f94 100644
--- a/server/types/models/account/index.ts
+++ b/server/types/models/account/index.ts
@@ -1,5 +1,5 @@
1export * from './account' 1export * from './account'
2export * from './account-blocklist' 2export * from './account-blocklist'
3export * from './actor'
4export * from './actor-follow' 3export * from './actor-follow'
5export * from './avatar' 4export * from './actor-image'
5export * from './actor'
diff --git a/server/types/models/application/application.ts b/server/types/models/application/application.ts
new file mode 100644
index 000000000..9afb9ad70
--- /dev/null
+++ b/server/types/models/application/application.ts
@@ -0,0 +1,5 @@
1import { ApplicationModel } from '@server/models/application/application'
2
3// ############################################################################
4
5export type MApplication = Omit<ApplicationModel, 'Account'>
diff --git a/server/types/models/application/index.ts b/server/types/models/application/index.ts
new file mode 100644
index 000000000..26e4b031f
--- /dev/null
+++ b/server/types/models/application/index.ts
@@ -0,0 +1 @@
export * from './application'
diff --git a/server/types/models/index.ts b/server/types/models/index.ts
index affa17425..b4fdb1ff3 100644
--- a/server/types/models/index.ts
+++ b/server/types/models/index.ts
@@ -1,4 +1,5 @@
1export * from './account' 1export * from './account'
2export * from './application'
2export * from './moderation' 3export * from './moderation'
3export * from './oauth' 4export * from './oauth'
4export * from './server' 5export * from './server'
diff --git a/server/types/models/user/user-notification.ts b/server/types/models/user/user-notification.ts
index 58764a748..7ebb0485d 100644
--- a/server/types/models/user/user-notification.ts
+++ b/server/types/models/user/user-notification.ts
@@ -1,12 +1,14 @@
1import { VideoAbuseModel } from '@server/models/abuse/video-abuse' 1import { VideoAbuseModel } from '@server/models/abuse/video-abuse'
2import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse' 2import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse'
3import { ApplicationModel } from '@server/models/application/application'
4import { PluginModel } from '@server/models/server/plugin'
3import { PickWith, PickWithOpt } from '@shared/core-utils' 5import { PickWith, PickWithOpt } from '@shared/core-utils'
4import { AbuseModel } from '../../../models/abuse/abuse' 6import { AbuseModel } from '../../../models/abuse/abuse'
5import { AccountModel } from '../../../models/account/account' 7import { AccountModel } from '../../../models/account/account'
8import { ActorImageModel } from '../../../models/account/actor-image'
6import { UserNotificationModel } from '../../../models/account/user-notification' 9import { UserNotificationModel } from '../../../models/account/user-notification'
7import { ActorModel } from '../../../models/activitypub/actor' 10import { ActorModel } from '../../../models/activitypub/actor'
8import { ActorFollowModel } from '../../../models/activitypub/actor-follow' 11import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
9import { AvatarModel } from '../../../models/avatar/avatar'
10import { ServerModel } from '../../../models/server/server' 12import { ServerModel } from '../../../models/server/server'
11import { VideoModel } from '../../../models/video/video' 13import { VideoModel } from '../../../models/video/video'
12import { VideoBlacklistModel } from '../../../models/video/video-blacklist' 14import { VideoBlacklistModel } from '../../../models/video/video-blacklist'
@@ -27,7 +29,7 @@ export module UserNotificationIncludes {
27 29
28 export type ActorInclude = 30 export type ActorInclude =
29 Pick<ActorModel, 'preferredUsername' | 'getHost'> & 31 Pick<ActorModel, 'preferredUsername' | 'getHost'> &
30 PickWith<ActorModel, 'Avatar', Pick<AvatarModel, 'filename' | 'getStaticPath'>> & 32 PickWith<ActorModel, 'Avatar', Pick<ActorImageModel, 'filename' | 'getStaticPath'>> &
31 PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>> 33 PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>>
32 34
33 export type VideoChannelInclude = Pick<VideoChannelModel, 'id' | 'name' | 'getDisplayName'> 35 export type VideoChannelInclude = Pick<VideoChannelModel, 'id' | 'name' | 'getDisplayName'>
@@ -73,7 +75,7 @@ export module UserNotificationIncludes {
73 Pick<ActorModel, 'preferredUsername' | 'getHost'> & 75 Pick<ActorModel, 'preferredUsername' | 'getHost'> &
74 PickWith<ActorModel, 'Account', AccountInclude> & 76 PickWith<ActorModel, 'Account', AccountInclude> &
75 PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>> & 77 PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>> &
76 PickWithOpt<ActorModel, 'Avatar', Pick<AvatarModel, 'filename' | 'getStaticPath'>> 78 PickWithOpt<ActorModel, 'Avatar', Pick<ActorImageModel, 'filename' | 'getStaticPath'>>
77 79
78 export type ActorFollowing = 80 export type ActorFollowing =
79 Pick<ActorModel, 'preferredUsername' | 'type' | 'getHost'> & 81 Pick<ActorModel, 'preferredUsername' | 'type' | 'getHost'> &
@@ -85,13 +87,19 @@ export module UserNotificationIncludes {
85 Pick<ActorFollowModel, 'id' | 'state'> & 87 Pick<ActorFollowModel, 'id' | 'state'> &
86 PickWith<ActorFollowModel, 'ActorFollower', ActorFollower> & 88 PickWith<ActorFollowModel, 'ActorFollower', ActorFollower> &
87 PickWith<ActorFollowModel, 'ActorFollowing', ActorFollowing> 89 PickWith<ActorFollowModel, 'ActorFollowing', ActorFollowing>
90
91 export type PluginInclude =
92 Pick<PluginModel, 'id' | 'name' | 'type' | 'latestVersion'>
93
94 export type ApplicationInclude =
95 Pick<ApplicationModel, 'latestPeerTubeVersion'>
88} 96}
89 97
90// ############################################################################ 98// ############################################################################
91 99
92export type MUserNotification = 100export type MUserNotification =
93 Omit<UserNotificationModel, 'User' | 'Video' | 'Comment' | 'Abuse' | 'VideoBlacklist' | 101 Omit<UserNotificationModel, 'User' | 'Video' | 'Comment' | 'Abuse' | 'VideoBlacklist' |
94 'VideoImport' | 'Account' | 'ActorFollow'> 102 'VideoImport' | 'Account' | 'ActorFollow' | 'Plugin' | 'Application'>
95 103
96// ############################################################################ 104// ############################################################################
97 105
@@ -103,4 +111,6 @@ export type UserNotificationModelForApi =
103 Use<'VideoBlacklist', UserNotificationIncludes.VideoBlacklistInclude> & 111 Use<'VideoBlacklist', UserNotificationIncludes.VideoBlacklistInclude> &
104 Use<'VideoImport', UserNotificationIncludes.VideoImportInclude> & 112 Use<'VideoImport', UserNotificationIncludes.VideoImportInclude> &
105 Use<'ActorFollow', UserNotificationIncludes.ActorFollowInclude> & 113 Use<'ActorFollow', UserNotificationIncludes.ActorFollowInclude> &
114 Use<'Plugin', UserNotificationIncludes.PluginInclude> &
115 Use<'Application', UserNotificationIncludes.ApplicationInclude> &
106 Use<'Account', UserNotificationIncludes.AccountIncludeActor> 116 Use<'Account', UserNotificationIncludes.AccountIncludeActor>
diff --git a/server/types/models/user/user.ts b/server/types/models/user/user.ts
index 12a68accf..fa7de9c52 100644
--- a/server/types/models/user/user.ts
+++ b/server/types/models/user/user.ts
@@ -1,5 +1,7 @@
1import { UserModel } from '../../../models/account/user' 1import { AccountModel } from '@server/models/account/account'
2import { MVideoPlaylist } from '@server/types/models'
2import { PickWith, PickWithOpt } from '@shared/core-utils' 3import { PickWith, PickWithOpt } from '@shared/core-utils'
4import { UserModel } from '../../../models/account/user'
3import { 5import {
4 MAccount, 6 MAccount,
5 MAccountDefault, 7 MAccountDefault,
@@ -9,10 +11,8 @@ import {
9 MAccountIdActorId, 11 MAccountIdActorId,
10 MAccountUrl 12 MAccountUrl
11} from '../account' 13} from '../account'
12import { MNotificationSetting, MNotificationSettingFormattable } from './user-notification-setting'
13import { AccountModel } from '@server/models/account/account'
14import { MChannelFormattable } from '../video/video-channels' 14import { MChannelFormattable } from '../video/video-channels'
15import { MVideoPlaylist } from '@server/types/models' 15import { MNotificationSetting, MNotificationSettingFormattable } from './user-notification-setting'
16 16
17type Use<K extends keyof UserModel, M> = PickWith<UserModel, K, M> 17type Use<K extends keyof UserModel, M> = PickWith<UserModel, K, M>
18 18
diff --git a/server/types/models/video/video-channels.ts b/server/types/models/video/video-channels.ts
index 77790daa4..f577807ca 100644
--- a/server/types/models/video/video-channels.ts
+++ b/server/types/models/video/video-channels.ts
@@ -12,15 +12,17 @@ import {
12 MAccountUserId, 12 MAccountUserId,
13 MActor, 13 MActor,
14 MActorAccountChannelId, 14 MActorAccountChannelId,
15 MActorAP, 15 MActorAPChannel,
16 MActorAPI, 16 MActorAPI,
17 MActorDefault, 17 MActorDefault,
18 MActorDefaultBanner,
18 MActorDefaultLight, 19 MActorDefaultLight,
19 MActorFormattable, 20 MActorFormattable,
20 MActorHost, 21 MActorHost,
21 MActorLight, 22 MActorLight,
22 MActorSummary, 23 MActorSummary,
23 MActorSummaryFormattable, MActorUrl 24 MActorSummaryFormattable,
25 MActorUrl
24} from '../account' 26} from '../account'
25import { MVideo } from './video' 27import { MVideo } from './video'
26 28
@@ -55,14 +57,14 @@ export type MChannelDefault =
55 MChannel & 57 MChannel &
56 Use<'Actor', MActorDefault> 58 Use<'Actor', MActorDefault>
57 59
60export type MChannelBannerDefault =
61 MChannel &
62 Use<'Actor', MActorDefaultBanner>
63
58// ############################################################################ 64// ############################################################################
59 65
60// Not all association attributes 66// Not all association attributes
61 67
62export type MChannelLight =
63 MChannel &
64 Use<'Actor', MActorDefaultLight>
65
66export type MChannelActorLight = 68export type MChannelActorLight =
67 MChannel & 69 MChannel &
68 Use<'Actor', MActorLight> 70 Use<'Actor', MActorLight>
@@ -84,29 +86,23 @@ export type MChannelAccountActor =
84 MChannel & 86 MChannel &
85 Use<'Account', MAccountActor> 87 Use<'Account', MAccountActor>
86 88
87export type MChannelAccountDefault = 89export type MChannelBannerAccountDefault =
88 MChannel & 90 MChannel &
89 Use<'Actor', MActorDefault> & 91 Use<'Actor', MActorDefaultBanner> &
90 Use<'Account', MAccountDefault> 92 Use<'Account', MAccountDefault>
91 93
92export type MChannelActorAccountActor = 94export type MChannelAccountDefault =
93 MChannel & 95 MChannel &
94 Use<'Account', MAccountActor> & 96 Use<'Actor', MActorDefault> &
95 Use<'Actor', MActor> 97 Use<'Account', MAccountDefault>
96 98
97// ############################################################################ 99// ############################################################################
98 100
99// Videos associations 101// Videos associations
100export type MChannelVideos = 102export type MChannelVideos =
101 MChannel & 103 MChannel &
102 Use<'Videos', MVideo[]> 104 Use<'Videos', MVideo[]>
103 105
104export type MChannelActorAccountDefaultVideos =
105 MChannel &
106 Use<'Actor', MActorDefault> &
107 Use<'Account', MAccountDefault> &
108 Use<'Videos', MVideo[]>
109
110// ############################################################################ 106// ############################################################################
111 107
112// For API 108// For API
@@ -146,5 +142,5 @@ export type MChannelFormattable =
146 142
147export type MChannelAP = 143export type MChannelAP =
148 Pick<MChannel, 'name' | 'description' | 'support'> & 144 Pick<MChannel, 'name' | 'description' | 'support'> &
149 Use<'Actor', MActorAP> & 145 Use<'Actor', MActorAPChannel> &
150 Use<'Account', MAccountUrl> 146 Use<'Account', MAccountUrl>
diff --git a/server/typings/express/index.d.ts b/server/typings/express/index.d.ts
index 66acfb3f5..cf3e7ae34 100644
--- a/server/typings/express/index.d.ts
+++ b/server/typings/express/index.d.ts
@@ -3,7 +3,9 @@ import {
3 MAbuseMessage, 3 MAbuseMessage,
4 MAbuseReporter, 4 MAbuseReporter,
5 MAccountBlocklist, 5 MAccountBlocklist,
6 MActorFollowActorsDefault,
6 MActorUrl, 7 MActorUrl,
8 MChannelBannerAccountDefault,
7 MStreamingPlaylist, 9 MStreamingPlaylist,
8 MVideoChangeOwnershipFull, 10 MVideoChangeOwnershipFull,
9 MVideoFile, 11 MVideoFile,
@@ -17,15 +19,12 @@ import { MPlugin, MServer, MServerBlocklist } from '@server/types/models/server'
17import { MVideoImportDefault } from '@server/types/models/video/video-import' 19import { MVideoImportDefault } from '@server/types/models/video/video-import'
18import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/types/models/video/video-playlist-element' 20import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/types/models/video/video-playlist-element'
19import { MAccountVideoRateAccountVideo } from '@server/types/models/video/video-rate' 21import { MAccountVideoRateAccountVideo } from '@server/types/models/video/video-rate'
20import { UserRole } from '@shared/models'
21import { RegisteredPlugin } from '../../lib/plugins/plugin-manager' 22import { RegisteredPlugin } from '../../lib/plugins/plugin-manager'
22import { 23import {
23 MAccountDefault, 24 MAccountDefault,
24 MActorAccountChannelId, 25 MActorAccountChannelId,
25 MActorFollowActorsDefault,
26 MActorFollowActorsDefaultSubscription, 26 MActorFollowActorsDefaultSubscription,
27 MActorFull, 27 MActorFull,
28 MChannelAccountDefault,
29 MComment, 28 MComment,
30 MCommentOwnerVideoReply, 29 MCommentOwnerVideoReply,
31 MUserDefault, 30 MUserDefault,
@@ -49,22 +48,6 @@ declare module 'express' {
49} 48}
50 49
51interface PeerTubeLocals { 50interface PeerTubeLocals {
52 bypassLogin?: {
53 bypass: boolean
54 pluginName: string
55 authName?: string
56 user: {
57 username: string
58 email: string
59 displayName: string
60 role: UserRole
61 }
62 }
63
64 refreshTokenAuthName?: string
65
66 explicitLogout?: boolean
67
68 videoAll?: MVideoFullLight 51 videoAll?: MVideoFullLight
69 onlyImmutableVideo?: MVideoImmutable 52 onlyImmutableVideo?: MVideoImmutable
70 onlyVideo?: MVideoThumbnail 53 onlyVideo?: MVideoThumbnail
@@ -88,7 +71,7 @@ interface PeerTubeLocals {
88 71
89 videoStreamingPlaylist?: MStreamingPlaylist 72 videoStreamingPlaylist?: MStreamingPlaylist
90 73
91 videoChannel?: MChannelAccountDefault 74 videoChannel?: MChannelBannerAccountDefault
92 75
93 videoPlaylistFull?: MVideoPlaylistFull 76 videoPlaylistFull?: MVideoPlaylistFull
94 videoPlaylistSummary?: MVideoPlaylistFullSummary 77 videoPlaylistSummary?: MVideoPlaylistFullSummary
diff --git a/shared/core-utils/renderer/html.ts b/shared/core-utils/renderer/html.ts
index 1220848a0..de4ad47ac 100644
--- a/shared/core-utils/renderer/html.ts
+++ b/shared/core-utils/renderer/html.ts
@@ -19,3 +19,21 @@ export const SANITIZE_OPTIONS = {
19 } 19 }
20 } 20 }
21} 21}
22
23// Thanks: https://stackoverflow.com/a/12034334
24export function escapeHTML (stringParam: string) {
25 if (!stringParam) return ''
26
27 const entityMap = {
28 '&': '&amp;',
29 '<': '&lt;',
30 '>': '&gt;',
31 '"': '&quot;',
32 '\'': '&#39;',
33 '/': '&#x2F;',
34 '`': '&#x60;',
35 '=': '&#x3D;'
36 }
37
38 return String(stringParam).replace(/[&<>"'`=/]/g, s => entityMap[s])
39}
diff --git a/shared/extra-utils/index.ts b/shared/extra-utils/index.ts
index 5c95a1b3e..898a92d43 100644
--- a/shared/extra-utils/index.ts
+++ b/shared/extra-utils/index.ts
@@ -1,7 +1,7 @@
1export * from './bulk/bulk' 1export * from './bulk/bulk'
2export * from './cli/cli' 2export * from './cli/cli'
3export * from './feeds/feeds' 3export * from './feeds/feeds'
4export * from './instances-index/mock-instances-index' 4export * from './mock-servers/mock-instances-index'
5export * from './miscs/miscs' 5export * from './miscs/miscs'
6export * from './miscs/sql' 6export * from './miscs/sql'
7export * from './miscs/stubs' 7export * from './miscs/stubs'
diff --git a/shared/extra-utils/miscs/sql.ts b/shared/extra-utils/miscs/sql.ts
index 740f0c2d6..65a0aa5fe 100644
--- a/shared/extra-utils/miscs/sql.ts
+++ b/shared/extra-utils/miscs/sql.ts
@@ -82,6 +82,11 @@ async function countVideoViewsOf (internalServerNumber: number, uuid: string) {
82 return parseInt(total + '', 10) 82 return parseInt(total + '', 10)
83} 83}
84 84
85function getActorImage (internalServerNumber: number, filename: string) {
86 return selectQuery(internalServerNumber, `SELECT * FROM "actorImage" WHERE filename = '${filename}'`)
87 .then(rows => rows[0])
88}
89
85function selectQuery (internalServerNumber: number, query: string) { 90function selectQuery (internalServerNumber: number, query: string) {
86 const seq = getSequelize(internalServerNumber) 91 const seq = getSequelize(internalServerNumber)
87 const options = { type: QueryTypes.SELECT as QueryTypes.SELECT } 92 const options = { type: QueryTypes.SELECT as QueryTypes.SELECT }
@@ -106,12 +111,20 @@ async function closeAllSequelize (servers: ServerInfo[]) {
106 } 111 }
107} 112}
108 113
109function setPluginVersion (internalServerNumber: number, pluginName: string, newVersion: string) { 114function setPluginField (internalServerNumber: number, pluginName: string, field: string, value: string) {
110 const seq = getSequelize(internalServerNumber) 115 const seq = getSequelize(internalServerNumber)
111 116
112 const options = { type: QueryTypes.UPDATE } 117 const options = { type: QueryTypes.UPDATE }
113 118
114 return seq.query(`UPDATE "plugin" SET "version" = '${newVersion}' WHERE "name" = '${pluginName}'`, options) 119 return seq.query(`UPDATE "plugin" SET "${field}" = '${value}' WHERE "name" = '${pluginName}'`, options)
120}
121
122function setPluginVersion (internalServerNumber: number, pluginName: string, newVersion: string) {
123 return setPluginField(internalServerNumber, pluginName, 'version', newVersion)
124}
125
126function setPluginLatestVersion (internalServerNumber: number, pluginName: string, newVersion: string) {
127 return setPluginField(internalServerNumber, pluginName, 'latestVersion', newVersion)
115} 128}
116 129
117function setActorFollowScores (internalServerNumber: number, newScore: number) { 130function setActorFollowScores (internalServerNumber: number, newScore: number) {
@@ -122,14 +135,25 @@ function setActorFollowScores (internalServerNumber: number, newScore: number) {
122 return seq.query(`UPDATE "actorFollow" SET "score" = ${newScore}`, options) 135 return seq.query(`UPDATE "actorFollow" SET "score" = ${newScore}`, options)
123} 136}
124 137
138function setTokenField (internalServerNumber: number, accessToken: string, field: string, value: string) {
139 const seq = getSequelize(internalServerNumber)
140
141 const options = { type: QueryTypes.UPDATE }
142
143 return seq.query(`UPDATE "oAuthToken" SET "${field}" = '${value}' WHERE "accessToken" = '${accessToken}'`, options)
144}
145
125export { 146export {
126 setVideoField, 147 setVideoField,
127 setPlaylistField, 148 setPlaylistField,
128 setActorField, 149 setActorField,
129 countVideoViewsOf, 150 countVideoViewsOf,
130 setPluginVersion, 151 setPluginVersion,
152 setPluginLatestVersion,
131 selectQuery, 153 selectQuery,
154 getActorImage,
132 deleteAll, 155 deleteAll,
156 setTokenField,
133 updateQuery, 157 updateQuery,
134 setActorFollowScores, 158 setActorFollowScores,
135 closeAllSequelize, 159 closeAllSequelize,
diff --git a/shared/extra-utils/mock-servers/joinpeertube-versions.ts b/shared/extra-utils/mock-servers/joinpeertube-versions.ts
new file mode 100644
index 000000000..d7d5b2c49
--- /dev/null
+++ b/shared/extra-utils/mock-servers/joinpeertube-versions.ts
@@ -0,0 +1,31 @@
1import * as express from 'express'
2
3export class MockJoinPeerTubeVersions {
4 private latestVersion: string
5
6 initialize () {
7 return new Promise<void>(res => {
8 const app = express()
9
10 app.use('/', (req: express.Request, res: express.Response, next: express.NextFunction) => {
11 if (process.env.DEBUG) console.log('Receiving request on mocked server %s.', req.url)
12
13 return next()
14 })
15
16 app.get('/versions.json', (req: express.Request, res: express.Response) => {
17 return res.json({
18 peertube: {
19 latestVersion: this.latestVersion
20 }
21 })
22 })
23
24 app.listen(42102, () => res())
25 })
26 }
27
28 setLatestVersion (latestVersion: string) {
29 this.latestVersion = latestVersion
30 }
31}
diff --git a/shared/extra-utils/instances-index/mock-instances-index.ts b/shared/extra-utils/mock-servers/mock-instances-index.ts
index 2604eda03..2604eda03 100644
--- a/shared/extra-utils/instances-index/mock-instances-index.ts
+++ b/shared/extra-utils/mock-servers/mock-instances-index.ts
diff --git a/shared/extra-utils/requests/activitypub.ts b/shared/extra-utils/requests/activitypub.ts
index 4762a8665..ecd8ce823 100644
--- a/shared/extra-utils/requests/activitypub.ts
+++ b/shared/extra-utils/requests/activitypub.ts
@@ -5,20 +5,19 @@ import { activityPubContextify } from '../../../server/helpers/activitypub'
5 5
6function makePOSTAPRequest (url: string, body: any, httpSignature: any, headers: any) { 6function makePOSTAPRequest (url: string, body: any, httpSignature: any, headers: any) {
7 const options = { 7 const options = {
8 method: 'POST', 8 method: 'POST' as 'POST',
9 uri: url,
10 json: body, 9 json: body,
11 httpSignature, 10 httpSignature,
12 headers 11 headers
13 } 12 }
14 13
15 return doRequest(options) 14 return doRequest(url, options)
16} 15}
17 16
18async function makeFollowRequest (to: { url: string }, by: { url: string, privateKey }) { 17async function makeFollowRequest (to: { url: string }, by: { url: string, privateKey }) {
19 const follow = { 18 const follow = {
20 type: 'Follow', 19 type: 'Follow',
21 id: by.url + '/toto', 20 id: by.url + '/' + new Date().getTime(),
22 actor: by.url, 21 actor: by.url,
23 object: to.url 22 object: to.url
24 } 23 }
@@ -34,7 +33,7 @@ async function makeFollowRequest (to: { url: string }, by: { url: string, privat
34 } 33 }
35 const headers = buildGlobalHeaders(body) 34 const headers = buildGlobalHeaders(body)
36 35
37 return makePOSTAPRequest(to.url, body, httpSignature, headers) 36 return makePOSTAPRequest(to.url + '/inbox', body, httpSignature, headers)
38} 37}
39 38
40export { 39export {
diff --git a/shared/extra-utils/requests/requests.ts b/shared/extra-utils/requests/requests.ts
index 3e773ee03..8b5cddf4a 100644
--- a/shared/extra-utils/requests/requests.ts
+++ b/shared/extra-utils/requests/requests.ts
@@ -152,11 +152,12 @@ function makeHTMLRequest (url: string, path: string) {
152 .expect(HttpStatusCode.OK_200) 152 .expect(HttpStatusCode.OK_200)
153} 153}
154 154
155function updateAvatarRequest (options: { 155function updateImageRequest (options: {
156 url: string 156 url: string
157 path: string 157 path: string
158 accessToken: string 158 accessToken: string
159 fixture: string 159 fixture: string
160 fieldname: string
160}) { 161}) {
161 let filePath = '' 162 let filePath = ''
162 if (isAbsolute(options.fixture)) { 163 if (isAbsolute(options.fixture)) {
@@ -170,7 +171,7 @@ function updateAvatarRequest (options: {
170 path: options.path, 171 path: options.path,
171 token: options.accessToken, 172 token: options.accessToken,
172 fields: {}, 173 fields: {},
173 attaches: { avatarfile: filePath }, 174 attaches: { [options.fieldname]: filePath },
174 statusCodeExpected: HttpStatusCode.OK_200 175 statusCodeExpected: HttpStatusCode.OK_200
175 }) 176 })
176} 177}
@@ -191,5 +192,5 @@ export {
191 makePutBodyRequest, 192 makePutBodyRequest,
192 makeDeleteRequest, 193 makeDeleteRequest,
193 makeRawRequest, 194 makeRawRequest,
194 updateAvatarRequest 195 updateImageRequest
195} 196}
diff --git a/shared/extra-utils/server/servers.ts b/shared/extra-utils/server/servers.ts
index 08d05ef36..779a3cc36 100644
--- a/shared/extra-utils/server/servers.ts
+++ b/shared/extra-utils/server/servers.ts
@@ -37,6 +37,7 @@ interface ServerInfo {
37 customConfigFile?: string 37 customConfigFile?: string
38 38
39 accessToken?: string 39 accessToken?: string
40 refreshToken?: string
40 videoChannel?: VideoChannel 41 videoChannel?: VideoChannel
41 42
42 video?: { 43 video?: {
diff --git a/shared/extra-utils/users/user-notifications.ts b/shared/extra-utils/users/user-notifications.ts
index 467a3d959..249e82925 100644
--- a/shared/extra-utils/users/user-notifications.ts
+++ b/shared/extra-utils/users/user-notifications.ts
@@ -2,7 +2,8 @@
2 2
3import { expect } from 'chai' 3import { expect } from 'chai'
4import { inspect } from 'util' 4import { inspect } from 'util'
5import { AbuseState } from '@shared/models' 5import { AbuseState, PluginType } from '@shared/models'
6import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
6import { UserNotification, UserNotificationSetting, UserNotificationSettingValue, UserNotificationType } from '../../models/users' 7import { UserNotification, UserNotificationSetting, UserNotificationSettingValue, UserNotificationType } from '../../models/users'
7import { MockSmtpServer } from '../miscs/email' 8import { MockSmtpServer } from '../miscs/email'
8import { makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests' 9import { makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests'
@@ -11,7 +12,6 @@ import { flushAndRunMultipleServers, ServerInfo } from '../server/servers'
11import { getUserNotificationSocket } from '../socket/socket-io' 12import { getUserNotificationSocket } from '../socket/socket-io'
12import { setAccessTokensToServers, userLogin } from './login' 13import { setAccessTokensToServers, userLogin } from './login'
13import { createUser, getMyUserInformation } from './users' 14import { createUser, getMyUserInformation } from './users'
14import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
15 15
16function updateMyNotificationSettings ( 16function updateMyNotificationSettings (
17 url: string, 17 url: string,
@@ -629,7 +629,59 @@ async function checkNewBlacklistOnMyVideo (
629 await checkNotification(base, notificationChecker, emailNotificationFinder, 'presence') 629 await checkNotification(base, notificationChecker, emailNotificationFinder, 'presence')
630} 630}
631 631
632function getAllNotificationsSettings () { 632async function checkNewPeerTubeVersion (base: CheckerBaseParams, latestVersion: string, type: CheckerType) {
633 const notificationType = UserNotificationType.NEW_PEERTUBE_VERSION
634
635 function notificationChecker (notification: UserNotification, type: CheckerType) {
636 if (type === 'presence') {
637 expect(notification).to.not.be.undefined
638 expect(notification.type).to.equal(notificationType)
639
640 expect(notification.peertube).to.exist
641 expect(notification.peertube.latestVersion).to.equal(latestVersion)
642 } else {
643 expect(notification).to.satisfy((n: UserNotification) => {
644 return n === undefined || n.peertube === undefined || n.peertube.latestVersion !== latestVersion
645 })
646 }
647 }
648
649 function emailNotificationFinder (email: object) {
650 const text = email['text']
651
652 return text.includes(latestVersion)
653 }
654
655 await checkNotification(base, notificationChecker, emailNotificationFinder, type)
656}
657
658async function checkNewPluginVersion (base: CheckerBaseParams, pluginType: PluginType, pluginName: string, type: CheckerType) {
659 const notificationType = UserNotificationType.NEW_PLUGIN_VERSION
660
661 function notificationChecker (notification: UserNotification, type: CheckerType) {
662 if (type === 'presence') {
663 expect(notification).to.not.be.undefined
664 expect(notification.type).to.equal(notificationType)
665
666 expect(notification.plugin.name).to.equal(pluginName)
667 expect(notification.plugin.type).to.equal(pluginType)
668 } else {
669 expect(notification).to.satisfy((n: UserNotification) => {
670 return n === undefined || n.plugin === undefined || n.plugin.name !== pluginName
671 })
672 }
673 }
674
675 function emailNotificationFinder (email: object) {
676 const text = email['text']
677
678 return text.includes(pluginName)
679 }
680
681 await checkNotification(base, notificationChecker, emailNotificationFinder, type)
682}
683
684function getAllNotificationsSettings (): UserNotificationSetting {
633 return { 685 return {
634 newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 686 newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
635 newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 687 newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
@@ -644,11 +696,13 @@ function getAllNotificationsSettings () {
644 newInstanceFollower: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 696 newInstanceFollower: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
645 abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 697 abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
646 abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, 698 abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
647 autoInstanceFollowing: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL 699 autoInstanceFollowing: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
648 } as UserNotificationSetting 700 newPeerTubeVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
701 newPluginVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
702 }
649} 703}
650 704
651async function prepareNotificationsTest (serversCount = 3) { 705async function prepareNotificationsTest (serversCount = 3, overrideConfigArg: any = {}) {
652 const userNotifications: UserNotification[] = [] 706 const userNotifications: UserNotification[] = []
653 const adminNotifications: UserNotification[] = [] 707 const adminNotifications: UserNotification[] = []
654 const adminNotificationsServer2: UserNotification[] = [] 708 const adminNotificationsServer2: UserNotification[] = []
@@ -665,7 +719,7 @@ async function prepareNotificationsTest (serversCount = 3) {
665 limit: 20 719 limit: 20
666 } 720 }
667 } 721 }
668 const servers = await flushAndRunMultipleServers(serversCount, overrideConfig) 722 const servers = await flushAndRunMultipleServers(serversCount, Object.assign(overrideConfig, overrideConfigArg))
669 723
670 await setAccessTokensToServers(servers) 724 await setAccessTokensToServers(servers)
671 725
@@ -749,5 +803,7 @@ export {
749 checkNewInstanceFollower, 803 checkNewInstanceFollower,
750 prepareNotificationsTest, 804 prepareNotificationsTest,
751 checkNewCommentAbuseForModerators, 805 checkNewCommentAbuseForModerators,
752 checkNewAccountAbuseForModerators 806 checkNewAccountAbuseForModerators,
807 checkNewPeerTubeVersion,
808 checkNewPluginVersion
753} 809}
diff --git a/shared/extra-utils/users/users.ts b/shared/extra-utils/users/users.ts
index db532dbb0..6040dd9c0 100644
--- a/shared/extra-utils/users/users.ts
+++ b/shared/extra-utils/users/users.ts
@@ -4,7 +4,7 @@ import { UserUpdateMe } from '../../models/users'
4import { UserAdminFlag } from '../../models/users/user-flag.model' 4import { UserAdminFlag } from '../../models/users/user-flag.model'
5import { UserRegister } from '../../models/users/user-register.model' 5import { UserRegister } from '../../models/users/user-register.model'
6import { UserRole } from '../../models/users/user-role' 6import { UserRole } from '../../models/users/user-role'
7import { makeGetRequest, makePostBodyRequest, makePutBodyRequest, updateAvatarRequest } from '../requests/requests' 7import { makeGetRequest, makePostBodyRequest, makePutBodyRequest, updateImageRequest } from '../requests/requests'
8import { ServerInfo } from '../server/servers' 8import { ServerInfo } from '../server/servers'
9import { userLogin } from './login' 9import { userLogin } from './login'
10import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' 10import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
@@ -275,7 +275,7 @@ function updateMyAvatar (options: {
275}) { 275}) {
276 const path = '/api/v1/users/me/avatar/pick' 276 const path = '/api/v1/users/me/avatar/pick'
277 277
278 return updateAvatarRequest(Object.assign(options, { path })) 278 return updateImageRequest({ ...options, path, fieldname: 'avatarfile' })
279} 279}
280 280
281function updateUser (options: { 281function updateUser (options: {
diff --git a/shared/extra-utils/videos/video-channels.ts b/shared/extra-utils/videos/video-channels.ts
index 3ff445c2a..d0dfb5856 100644
--- a/shared/extra-utils/videos/video-channels.ts
+++ b/shared/extra-utils/videos/video-channels.ts
@@ -3,7 +3,7 @@
3import * as request from 'supertest' 3import * as request from 'supertest'
4import { VideoChannelUpdate } from '../../models/videos/channel/video-channel-update.model' 4import { VideoChannelUpdate } from '../../models/videos/channel/video-channel-update.model'
5import { VideoChannelCreate } from '../../models/videos/channel/video-channel-create.model' 5import { VideoChannelCreate } from '../../models/videos/channel/video-channel-create.model'
6import { makeGetRequest, updateAvatarRequest } from '../requests/requests' 6import { makeDeleteRequest, makeGetRequest, updateImageRequest } from '../requests/requests'
7import { ServerInfo } from '../server/servers' 7import { ServerInfo } from '../server/servers'
8import { User } from '../../models/users/user.model' 8import { User } from '../../models/users/user.model'
9import { getMyUserInformation } from '../users/users' 9import { getMyUserInformation } from '../users/users'
@@ -129,16 +129,32 @@ function getVideoChannel (url: string, channelName: string) {
129 .expect('Content-Type', /json/) 129 .expect('Content-Type', /json/)
130} 130}
131 131
132function updateVideoChannelAvatar (options: { 132function updateVideoChannelImage (options: {
133 url: string 133 url: string
134 accessToken: string 134 accessToken: string
135 fixture: string 135 fixture: string
136 videoChannelName: string | number 136 videoChannelName: string | number
137 type: 'avatar' | 'banner'
137}) { 138}) {
139 const path = `/api/v1/video-channels/${options.videoChannelName}/${options.type}/pick`
138 140
139 const path = '/api/v1/video-channels/' + options.videoChannelName + '/avatar/pick' 141 return updateImageRequest({ ...options, path, fieldname: options.type + 'file' })
142}
143
144function deleteVideoChannelImage (options: {
145 url: string
146 accessToken: string
147 videoChannelName: string | number
148 type: 'avatar' | 'banner'
149}) {
150 const path = `/api/v1/video-channels/${options.videoChannelName}/${options.type}`
140 151
141 return updateAvatarRequest(Object.assign(options, { path })) 152 return makeDeleteRequest({
153 url: options.url,
154 token: options.accessToken,
155 path,
156 statusCodeExpected: 204
157 })
142} 158}
143 159
144function setDefaultVideoChannel (servers: ServerInfo[]) { 160function setDefaultVideoChannel (servers: ServerInfo[]) {
@@ -157,12 +173,13 @@ function setDefaultVideoChannel (servers: ServerInfo[]) {
157// --------------------------------------------------------------------------- 173// ---------------------------------------------------------------------------
158 174
159export { 175export {
160 updateVideoChannelAvatar, 176 updateVideoChannelImage,
161 getVideoChannelsList, 177 getVideoChannelsList,
162 getAccountVideoChannelsList, 178 getAccountVideoChannelsList,
163 addVideoChannel, 179 addVideoChannel,
164 updateVideoChannel, 180 updateVideoChannel,
165 deleteVideoChannel, 181 deleteVideoChannel,
166 getVideoChannel, 182 getVideoChannel,
167 setDefaultVideoChannel 183 setDefaultVideoChannel,
184 deleteVideoChannelImage
168} 185}
diff --git a/shared/models/activitypub/activitypub-actor.ts b/shared/models/activitypub/activitypub-actor.ts
index f022f3d02..c59be3f3b 100644
--- a/shared/models/activitypub/activitypub-actor.ts
+++ b/shared/models/activitypub/activitypub-actor.ts
@@ -27,5 +27,6 @@ export interface ActivityPubActor {
27 publicKeyPem: string 27 publicKeyPem: string
28 } 28 }
29 29
30 icon: ActivityIconObject 30 icon?: ActivityIconObject
31 image?: ActivityIconObject
31} 32}
diff --git a/shared/models/activitypub/objects/common-objects.ts b/shared/models/activitypub/objects/common-objects.ts
index 76f0e3bcf..43d7f7f74 100644
--- a/shared/models/activitypub/objects/common-objects.ts
+++ b/shared/models/activitypub/objects/common-objects.ts
@@ -9,7 +9,7 @@ export interface ActivityIdentifierObject {
9export interface ActivityIconObject { 9export interface ActivityIconObject {
10 type: 'Image' 10 type: 'Image'
11 url: string 11 url: string
12 mediaType: 'image/jpeg' | 'image/png' 12 mediaType: string
13 width?: number 13 width?: number
14 height?: number 14 height?: number
15} 15}
diff --git a/shared/models/actors/account.model.ts b/shared/models/actors/account.model.ts
index 2ff4b9f5e..120dec271 100644
--- a/shared/models/actors/account.model.ts
+++ b/shared/models/actors/account.model.ts
@@ -1,5 +1,5 @@
1import { ActorImage } from './actor-image.model'
1import { Actor } from './actor.model' 2import { Actor } from './actor.model'
2import { Avatar } from '../avatars'
3 3
4export interface Account extends Actor { 4export interface Account extends Actor {
5 displayName: string 5 displayName: string
@@ -14,5 +14,5 @@ export interface AccountSummary {
14 displayName: string 14 displayName: string
15 url: string 15 url: string
16 host: string 16 host: string
17 avatar?: Avatar 17 avatar?: ActorImage
18} 18}
diff --git a/shared/models/avatars/avatar.model.ts b/shared/models/actors/actor-image.model.ts
index f7fa16f49..ad5eab627 100644
--- a/shared/models/avatars/avatar.model.ts
+++ b/shared/models/actors/actor-image.model.ts
@@ -1,4 +1,4 @@
1export interface Avatar { 1export interface ActorImage {
2 path: string 2 path: string
3 3
4 url?: string 4 url?: string
diff --git a/shared/models/actors/actor-image.type.ts b/shared/models/actors/actor-image.type.ts
new file mode 100644
index 000000000..ac8eb6bf2
--- /dev/null
+++ b/shared/models/actors/actor-image.type.ts
@@ -0,0 +1,4 @@
1export const enum ActorImageType {
2 AVATAR = 1,
3 BANNER = 2
4}
diff --git a/shared/models/actors/actor.model.ts b/shared/models/actors/actor.model.ts
index 1dbf5f638..7d9f35b10 100644
--- a/shared/models/actors/actor.model.ts
+++ b/shared/models/actors/actor.model.ts
@@ -1,4 +1,4 @@
1import { Avatar } from '../avatars/avatar.model' 1import { ActorImage } from './actor-image.model'
2 2
3export interface Actor { 3export interface Actor {
4 id: number 4 id: number
@@ -9,5 +9,5 @@ export interface Actor {
9 followersCount: number 9 followersCount: number
10 createdAt: Date | string 10 createdAt: Date | string
11 updatedAt: Date | string 11 updatedAt: Date | string
12 avatar?: Avatar 12 avatar?: ActorImage
13} 13}
diff --git a/shared/models/actors/index.ts b/shared/models/actors/index.ts
index c7a92399d..156f83248 100644
--- a/shared/models/actors/index.ts
+++ b/shared/models/actors/index.ts
@@ -1,3 +1,5 @@
1export * from './account.model' 1export * from './account.model'
2export * from './actor-image.model'
3export * from './actor-image.type'
2export * from './actor.model' 4export * from './actor.model'
3export * from './follow.model' 5export * from './follow.model'
diff --git a/shared/models/avatars/index.ts b/shared/models/avatars/index.ts
deleted file mode 100644
index 65e8e0882..000000000
--- a/shared/models/avatars/index.ts
+++ /dev/null
@@ -1 +0,0 @@
1export * from './avatar.model'
diff --git a/shared/models/index.ts b/shared/models/index.ts
index 2214f7ca3..dff5fdf0e 100644
--- a/shared/models/index.ts
+++ b/shared/models/index.ts
@@ -1,12 +1,12 @@
1export * from './activitypub' 1export * from './activitypub'
2export * from './actors' 2export * from './actors'
3export * from './avatars'
4export * from './moderation' 3export * from './moderation'
5export * from './bulk' 4export * from './bulk'
6export * from './redundancy' 5export * from './redundancy'
7export * from './users' 6export * from './users'
8export * from './videos' 7export * from './videos'
9export * from './feeds' 8export * from './feeds'
9export * from './joinpeertube'
10export * from './overviews' 10export * from './overviews'
11export * from './plugins' 11export * from './plugins'
12export * from './search' 12export * from './search'
diff --git a/shared/models/joinpeertube/index.ts b/shared/models/joinpeertube/index.ts
new file mode 100644
index 000000000..9681c35ad
--- /dev/null
+++ b/shared/models/joinpeertube/index.ts
@@ -0,0 +1 @@
export * from './versions.model'
diff --git a/shared/models/joinpeertube/versions.model.ts b/shared/models/joinpeertube/versions.model.ts
new file mode 100644
index 000000000..60a769150
--- /dev/null
+++ b/shared/models/joinpeertube/versions.model.ts
@@ -0,0 +1,5 @@
1export interface JoinPeerTubeVersions {
2 peertube: {
3 latestVersion: string
4 }
5}
diff --git a/shared/models/plugins/client-hook.model.ts b/shared/models/plugins/client-hook.model.ts
index 7b7144676..f8ca32771 100644
--- a/shared/models/plugins/client-hook.model.ts
+++ b/shared/models/plugins/client-hook.model.ts
@@ -85,8 +85,27 @@ export const clientActionHookObject = {
85 // Fired when the registration page is being initialized 85 // Fired when the registration page is being initialized
86 'action:signup.register.init': true, 86 'action:signup.register.init': true,
87 87
88 // Fired when the video upload page is being initalized
89 'action:video-upload.init': true,
90 // Fired when the video import by URL page is being initalized
91 'action:video-url-import.init': true,
92 // Fired when the video import by torrent/magnet URI page is being initalized
93 'action:video-torrent-import.init': true,
94 // Fired when the "Go Live" page is being initalized
95 'action:go-live.init': true,
96
97 // Fired when the user explicitely logged in/logged out
98 'action:auth-user.logged-in': true,
99 'action:auth-user.logged-out': true,
100 // Fired when the application loaded user information (using tokens from the local storage or after a successful login)
101 'action:auth-user.information-loaded': true,
102
103 // Fired when the modal to download a video/caption is shown
104 'action:modal.video-download.shown': true,
105
88 // ####### Embed hooks ####### 106 // ####### Embed hooks #######
89 // In embed scope, peertube helpers are not available 107 // /!\ In embed scope, peertube helpers are not available
108 // ###########################
90 109
91 // Fired when the embed loaded the player 110 // Fired when the embed loaded the player
92 'action:embed.player.loaded': true 111 'action:embed.player.loaded': true
diff --git a/shared/models/plugins/server-hook.model.ts b/shared/models/plugins/server-hook.model.ts
index 082b4b591..88277af5a 100644
--- a/shared/models/plugins/server-hook.model.ts
+++ b/shared/models/plugins/server-hook.model.ts
@@ -18,6 +18,16 @@ export const serverFilterHookObject = {
18 'filter:api.user.me.videos.list.params': true, 18 'filter:api.user.me.videos.list.params': true,
19 'filter:api.user.me.videos.list.result': true, 19 'filter:api.user.me.videos.list.result': true,
20 20
21 // Filter params/results to search videos/channels in the DB or on the remote index
22 'filter:api.search.videos.local.list.params': true,
23 'filter:api.search.videos.local.list.result': true,
24 'filter:api.search.videos.index.list.params': true,
25 'filter:api.search.videos.index.list.result': true,
26 'filter:api.search.video-channels.local.list.params': true,
27 'filter:api.search.video-channels.local.list.result': true,
28 'filter:api.search.video-channels.index.list.params': true,
29 'filter:api.search.video-channels.index.list.result': true,
30
21 // Filter the result of the get function 31 // Filter the result of the get function
22 // Used to get detailed video information (video watch page for example) 32 // Used to get detailed video information (video watch page for example)
23 'filter:api.video.get.result': true, 33 'filter:api.video.get.result': true,
@@ -50,7 +60,15 @@ export const serverFilterHookObject = {
50 'filter:video.auto-blacklist.result': true, 60 'filter:video.auto-blacklist.result': true,
51 61
52 // Filter result used to check if a user can register on the instance 62 // Filter result used to check if a user can register on the instance
53 'filter:api.user.signup.allowed.result': true 63 'filter:api.user.signup.allowed.result': true,
64
65 // Filter result used to check if video/torrent download is allowed
66 'filter:api.download.video.allowed.result': true,
67 'filter:api.download.torrent.allowed.result': true,
68
69 // Filter result to check if the embed is allowed for a particular request
70 'filter:html.embed.video.allowed.result': true,
71 'filter:html.embed.video-playlist.allowed.result': true
54} 72}
55 73
56export type ServerFilterHookName = keyof typeof serverFilterHookObject 74export type ServerFilterHookName = keyof typeof serverFilterHookObject
diff --git a/shared/models/server/emailer.model.ts b/shared/models/server/emailer.model.ts
index 069ef0bab..39512d306 100644
--- a/shared/models/server/emailer.model.ts
+++ b/shared/models/server/emailer.model.ts
@@ -1,12 +1,49 @@
1export type SendEmailOptions = { 1type From = string | { name?: string, address: string }
2 to: string[]
3 2
4 template?: string 3interface Base extends Partial<SendEmailDefaultMessageOptions> {
4 to: string[] | string
5}
6
7interface MailTemplate extends Base {
8 template: string
5 locals?: { [key: string]: any } 9 locals?: { [key: string]: any }
10 text?: undefined
11}
12
13interface MailText extends Base {
14 text: string
6 15
7 // override defaults 16 locals?: Partial<SendEmailDefaultLocalsOptions> & {
8 subject?: string 17 title?: string
9 text?: string 18 action?: {
10 from?: string | { name?: string, address: string } 19 url: string
11 replyTo?: string 20 text: string
21 }
22 }
12} 23}
24
25interface SendEmailDefaultLocalsOptions {
26 instanceName: string
27 text: string
28 subject: string
29}
30
31interface SendEmailDefaultMessageOptions {
32 to: string[] | string
33 from: From
34 subject: string
35 replyTo: string
36}
37
38export type SendEmailDefaultOptions = {
39 template: 'common'
40
41 message: SendEmailDefaultMessageOptions
42
43 locals: SendEmailDefaultLocalsOptions & {
44 WEBSERVER: any
45 EMAIL: any
46 }
47}
48
49export type SendEmailOptions = MailTemplate | MailText
diff --git a/shared/models/server/job.model.ts b/shared/models/server/job.model.ts
index 83ef84457..e4acfee8d 100644
--- a/shared/models/server/job.model.ts
+++ b/shared/models/server/job.model.ts
@@ -59,7 +59,7 @@ export type ActivitypubHttpFetcherPayload = {
59export type ActivitypubHttpUnicastPayload = { 59export type ActivitypubHttpUnicastPayload = {
60 uri: string 60 uri: string
61 signatureActorId?: number 61 signatureActorId?: number
62 body: any 62 body: object
63 contextType?: ContextType 63 contextType?: ContextType
64} 64}
65 65
diff --git a/shared/models/server/server-config.model.ts b/shared/models/server/server-config.model.ts
index efde4ad9d..85d84af44 100644
--- a/shared/models/server/server-config.model.ts
+++ b/shared/models/server/server-config.model.ts
@@ -151,6 +151,15 @@ export interface ServerConfig {
151 } 151 }
152 } 152 }
153 153
154 banner: {
155 file: {
156 size: {
157 max: number
158 }
159 extensions: string[]
160 }
161 }
162
154 video: { 163 video: {
155 image: { 164 image: {
156 size: { 165 size: {
diff --git a/shared/models/users/user-notification-setting.model.ts b/shared/models/users/user-notification-setting.model.ts
index 473148062..977e6b985 100644
--- a/shared/models/users/user-notification-setting.model.ts
+++ b/shared/models/users/user-notification-setting.model.ts
@@ -24,4 +24,7 @@ export interface UserNotificationSetting {
24 24
25 abuseStateChange: UserNotificationSettingValue 25 abuseStateChange: UserNotificationSettingValue
26 abuseNewMessage: UserNotificationSettingValue 26 abuseNewMessage: UserNotificationSettingValue
27
28 newPeerTubeVersion: UserNotificationSettingValue
29 newPluginVersion: UserNotificationSettingValue
27} 30}
diff --git a/shared/models/users/user-notification.model.ts b/shared/models/users/user-notification.model.ts
index e2f2234e4..8b33e3fbd 100644
--- a/shared/models/users/user-notification.model.ts
+++ b/shared/models/users/user-notification.model.ts
@@ -1,7 +1,8 @@
1import { FollowState } from '../actors' 1import { FollowState } from '../actors'
2import { AbuseState } from '../moderation' 2import { AbuseState } from '../moderation'
3import { PluginType } from '../plugins'
3 4
4export enum UserNotificationType { 5export const enum UserNotificationType {
5 NEW_VIDEO_FROM_SUBSCRIPTION = 1, 6 NEW_VIDEO_FROM_SUBSCRIPTION = 1,
6 NEW_COMMENT_ON_MY_VIDEO = 2, 7 NEW_COMMENT_ON_MY_VIDEO = 2,
7 NEW_ABUSE_FOR_MODERATORS = 3, 8 NEW_ABUSE_FOR_MODERATORS = 3,
@@ -26,7 +27,10 @@ export enum UserNotificationType {
26 27
27 ABUSE_STATE_CHANGE = 15, 28 ABUSE_STATE_CHANGE = 15,
28 29
29 ABUSE_NEW_MESSAGE = 16 30 ABUSE_NEW_MESSAGE = 16,
31
32 NEW_PLUGIN_VERSION = 17,
33 NEW_PEERTUBE_VERSION = 18
30} 34}
31 35
32export interface VideoInfo { 36export interface VideoInfo {
@@ -108,6 +112,16 @@ export interface UserNotification {
108 } 112 }
109 } 113 }
110 114
115 plugin?: {
116 name: string
117 type: PluginType
118 latestVersion: string
119 }
120
121 peertube?: {
122 latestVersion: string
123 }
124
111 createdAt: string 125 createdAt: string
112 updatedAt: string 126 updatedAt: string
113} 127}
diff --git a/shared/models/videos/channel/video-channel.model.ts b/shared/models/videos/channel/video-channel.model.ts
index 32829e92a..56517972d 100644
--- a/shared/models/videos/channel/video-channel.model.ts
+++ b/shared/models/videos/channel/video-channel.model.ts
@@ -1,6 +1,5 @@
1import { Actor } from '../../actors/actor.model' 1import { Actor } from '../../actors/actor.model'
2import { Account } from '../../actors/index' 2import { Account, ActorImage } from '../../actors'
3import { Avatar } from '../../avatars'
4 3
5export type ViewsPerDate = { 4export type ViewsPerDate = {
6 date: Date 5 date: Date
@@ -16,6 +15,8 @@ export interface VideoChannel extends Actor {
16 15
17 videosCount?: number 16 videosCount?: number
18 viewsPerDay?: ViewsPerDate[] // chronologically ordered 17 viewsPerDay?: ViewsPerDate[] // chronologically ordered
18
19 banner?: ActorImage
19} 20}
20 21
21export interface VideoChannelSummary { 22export interface VideoChannelSummary {
@@ -24,5 +25,5 @@ export interface VideoChannelSummary {
24 displayName: string 25 displayName: string
25 url: string 26 url: string
26 host: string 27 host: string
27 avatar?: Avatar 28 avatar?: ActorImage
28} 29}
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml
index a47654f69..373b17ddf 100644
--- a/support/doc/api/openapi.yaml
+++ b/support/doc/api/openapi.yaml
@@ -974,7 +974,10 @@ paths:
974 content: 974 content:
975 application/json: 975 application/json:
976 schema: 976 schema:
977 $ref: '#/components/schemas/Avatar' 977 type: object
978 properties:
979 avatar:
980 $ref: '#/components/schemas/ActorImage'
978 '413': 981 '413':
979 description: image file too large 982 description: image file too large
980 headers: 983 headers:
@@ -996,6 +999,17 @@ paths:
996 encoding: 999 encoding:
997 avatarfile: 1000 avatarfile:
998 contentType: image/png, image/jpeg 1001 contentType: image/png, image/jpeg
1002 /users/me/avatar:
1003 delete:
1004 summary: Delete my avatar
1005 security:
1006 - OAuth2: []
1007 tags:
1008 - My User
1009 responses:
1010 '204':
1011 description: successful operation
1012
999 /videos/ownership: 1013 /videos/ownership:
1000 get: 1014 get:
1001 summary: List video ownership changes 1015 summary: List video ownership changes
@@ -2185,6 +2199,112 @@ paths:
2185 application/json: 2199 application/json:
2186 schema: 2200 schema:
2187 $ref: '#/components/schemas/VideoListResponse' 2201 $ref: '#/components/schemas/VideoListResponse'
2202 '/video-channels/{channelHandle}/avatar/pick':
2203 post:
2204 summary: Update channel avatar
2205 security:
2206 - OAuth2: []
2207 tags:
2208 - Video Channels
2209 parameters:
2210 - $ref: '#/components/parameters/channelHandle'
2211 responses:
2212 '200':
2213 description: successful operation
2214 content:
2215 application/json:
2216 schema:
2217 type: object
2218 properties:
2219 avatar:
2220 $ref: '#/components/schemas/ActorImage'
2221 '413':
2222 description: image file too large
2223 headers:
2224 X-File-Maximum-Size:
2225 schema:
2226 type: string
2227 format: Nginx size
2228 description: Maximum file size for the avatar
2229 requestBody:
2230 content:
2231 multipart/form-data:
2232 schema:
2233 type: object
2234 properties:
2235 avatarfile:
2236 description: The file to upload.
2237 type: string
2238 format: binary
2239 encoding:
2240 avatarfile:
2241 contentType: image/png, image/jpeg
2242 '/video-channels/{channelHandle}/avatar':
2243 delete:
2244 summary: Delete channel avatar
2245 security:
2246 - OAuth2: []
2247 tags:
2248 - Video Channels
2249 parameters:
2250 - $ref: '#/components/parameters/channelHandle'
2251 responses:
2252 '204':
2253 description: successful operation
2254
2255
2256 '/video-channels/{channelHandle}/banner/pick':
2257 post:
2258 summary: Update channel banner
2259 security:
2260 - OAuth2: []
2261 tags:
2262 - Video Channels
2263 parameters:
2264 - $ref: '#/components/parameters/channelHandle'
2265 responses:
2266 '200':
2267 description: successful operation
2268 content:
2269 application/json:
2270 schema:
2271 type: object
2272 properties:
2273 banner:
2274 $ref: '#/components/schemas/ActorImage'
2275 '413':
2276 description: image file too large
2277 headers:
2278 X-File-Maximum-Size:
2279 schema:
2280 type: string
2281 format: Nginx size
2282 description: Maximum file size for the banner
2283 requestBody:
2284 content:
2285 multipart/form-data:
2286 schema:
2287 type: object
2288 properties:
2289 bannerfile:
2290 description: The file to upload.
2291 type: string
2292 format: binary
2293 encoding:
2294 bannerfile:
2295 contentType: image/png, image/jpeg
2296 '/video-channels/{channelHandle}/banner':
2297 delete:
2298 summary: Delete channel banner
2299 security:
2300 - OAuth2: []
2301 tags:
2302 - Video Channels
2303 parameters:
2304 - $ref: '#/components/parameters/channelHandle'
2305 responses:
2306 '204':
2307 description: successful operation
2188 2308
2189 /video-playlists/privacies: 2309 /video-playlists/privacies:
2190 get: 2310 get:
@@ -3989,7 +4109,7 @@ components:
3989 avatar: 4109 avatar:
3990 nullable: true 4110 nullable: true
3991 allOf: 4111 allOf:
3992 - $ref: '#/components/schemas/Avatar' 4112 - $ref: '#/components/schemas/ActorImage'
3993 VideoChannelSummary: 4113 VideoChannelSummary:
3994 properties: 4114 properties:
3995 id: 4115 id:
@@ -4007,7 +4127,7 @@ components:
4007 avatar: 4127 avatar:
4008 nullable: true 4128 nullable: true
4009 allOf: 4129 allOf:
4010 - $ref: '#/components/schemas/Avatar' 4130 - $ref: '#/components/schemas/ActorImage'
4011 PlaylistElement: 4131 PlaylistElement:
4012 properties: 4132 properties:
4013 position: 4133 position:
@@ -4460,7 +4580,7 @@ components:
4460 $ref: '#/components/schemas/VideoConstantString' 4580 $ref: '#/components/schemas/VideoConstantString'
4461 captionPath: 4581 captionPath:
4462 type: string 4582 type: string
4463 Avatar: 4583 ActorImage:
4464 properties: 4584 properties:
4465 path: 4585 path:
4466 type: string 4586 type: string
@@ -4512,7 +4632,7 @@ components:
4512 type: string 4632 type: string
4513 format: date-time 4633 format: date-time
4514 avatar: 4634 avatar:
4515 $ref: '#/components/schemas/Avatar' 4635 $ref: '#/components/schemas/ActorImage'
4516 Account: 4636 Account:
4517 allOf: 4637 allOf:
4518 - $ref: '#/components/schemas/Actor' 4638 - $ref: '#/components/schemas/Actor'
@@ -5694,6 +5814,8 @@ components:
5694 description: User can stream multiple times in a permanent live 5814 description: User can stream multiple times in a permanent live
5695 type: boolean 5815 type: boolean
5696 5816
5817
5818
5697 callbacks: 5819 callbacks:
5698 searchIndex: 5820 searchIndex:
5699 'https://search.example.org/api/v1/search/videos': 5821 'https://search.example.org/api/v1/search/videos':
diff --git a/support/doc/dependencies.md b/support/doc/dependencies.md
index 0fdbdfc82..9666d72af 100644
--- a/support/doc/dependencies.md
+++ b/support/doc/dependencies.md
@@ -281,17 +281,34 @@ service nginx start
281 281
2821. Add the packages: 2821. Add the packages:
283 283
284```sh
285brew install bash ffmpeg nginx postgresql openssl gcc make redis git yarn
284``` 286```
285brew install ffmpeg nginx postgresql openssl gcc make redis git yarn 287
288You may need to update your default version of bash.
289
290**How to change your default shell**
291
292```sh
293which -a bash # Check where bash is installed
294bash --version # You need a version at least as recent as 4.0
295sudo vim /etc/shells # Add in this file : /usr/local/bin/bash
296chsh -s /usr/local/bin/bash # To set the brew-installed bash as default bash
286``` 297```
287 298
299In a new shell, type `bash --version` to assert your changes took effect and
300correctly modified your default bash version.
301
2882. Run the services: 3022. Run the services:
289 303
290``` 304```sh
291brew services run postgresql 305brew services run postgresql
292brew services run redis 306brew services run redis
293``` 307```
294 308
309On macOS, the `postgresql` user can be `_postgres` instead of `postgres`.
310If `sudo -u postgres createuser -P peertube` gives you an error, you can try `sudo -u _postgres createuser -U peertube`.
311
295## Gentoo 312## Gentoo
296 313
2971. Add this to ``/etc/portage/sets/peertube``: 3141. Add this to ``/etc/portage/sets/peertube``:
diff --git a/support/doc/development/release.md b/support/doc/development/release.md
index 39c2c5608..5cd735eda 100644
--- a/support/doc/development/release.md
+++ b/support/doc/development/release.md
@@ -19,4 +19,4 @@ NODE_APP_INSTANCE=6 NODE_ENV=test npm run start
19 * Check the release is okay: https://github.com/Chocobozzz/PeerTube/releases 19 * Check the release is okay: https://github.com/Chocobozzz/PeerTube/releases
20 * Update https://peertube3.cpy.re and check it works correctly 20 * Update https://peertube3.cpy.re and check it works correctly
21 * Update all other instances and check it works correctly 21 * Update all other instances and check it works correctly
22 * Communicate 22 * After a couple of days, update https://joinpeertube.org/api/v1/versions.json
diff --git a/support/doc/plugins/guide.md b/support/doc/plugins/guide.md
index bc10e624d..20cbec5c7 100644
--- a/support/doc/plugins/guide.md
+++ b/support/doc/plugins/guide.md
@@ -22,6 +22,7 @@
22 - [Custom Modal](#custom-modal) 22 - [Custom Modal](#custom-modal)
23 - [Translate](#translate) 23 - [Translate](#translate)
24 - [Get public settings](#get-public-settings) 24 - [Get public settings](#get-public-settings)
25 - [Get server config](#get-server-config)
25 - [Add custom fields to video form](#add-custom-fields-to-video-form) 26 - [Add custom fields to video form](#add-custom-fields-to-video-form)
26 - [Publishing](#publishing) 27 - [Publishing](#publishing)
27- [Write a plugin/theme](#write-a-plugintheme) 28- [Write a plugin/theme](#write-a-plugintheme)
@@ -470,6 +471,15 @@ peertubeHelpers.getSettings()
470 }) 471 })
471``` 472```
472 473
474#### Get server config
475
476```js
477peertubeHelpers.getServerConfig()
478 .then(config => {
479 console.log('Fetched server config.', config)
480 })
481```
482
473#### Add custom fields to video form 483#### Add custom fields to video form
474 484
475To add custom fields in the video form (in *Plugin settings* tab): 485To add custom fields in the video form (in *Plugin settings* tab):
diff --git a/support/doc/tools.md b/support/doc/tools.md
index 452b3d039..175c22cd8 100644
--- a/support/doc/tools.md
+++ b/support/doc/tools.md
@@ -15,6 +15,7 @@
15 - [peertube-redundancy.js](#peertube-redundancyjs) 15 - [peertube-redundancy.js](#peertube-redundancyjs)
16- [Server tools](#server-tools) 16- [Server tools](#server-tools)
17 - [parse-log](#parse-log) 17 - [parse-log](#parse-log)
18 - [regenerate-thumbnails.js](#regenerate-thumbnailsjs)
18 - [create-transcoding-job.js](#create-transcoding-jobjs) 19 - [create-transcoding-job.js](#create-transcoding-jobjs)
19 - [create-import-video-file-job.js](#create-import-video-file-jobjs) 20 - [create-import-video-file-job.js](#create-import-video-file-jobjs)
20 - [prune-storage.js](#prune-storagejs) 21 - [prune-storage.js](#prune-storagejs)
@@ -244,6 +245,22 @@ $ sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production
244 245
245`--level` is optional and could be `info`/`warn`/`error` 246`--level` is optional and could be `info`/`warn`/`error`
246 247
248You can also remove SQL or HTTP logs using `--not-tags`:
249
250```
251$ cd /var/www/peertube/peertube-latest
252$ sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production npm run parse-log -- --level debug --not-tags http sql
253```
254
255### regenerate-thumbnails.js
256
257Regenerating local video thumbnails could be useful because new PeerTube releases may increase thumbnail sizes:
258
259```
260$ cd /var/www/peertube/peertube-latest
261$ sudo -u peertube NODE_CONFIG_DIR=/var/www/peertube/config NODE_ENV=production npm run regenerate-thumbnails
262```
263
247### create-transcoding-job.js 264### create-transcoding-job.js
248 265
249You can use this script to force transcoding of an existing video. PeerTube needs to be running. 266You can use this script to force transcoding of an existing video. PeerTube needs to be running.
diff --git a/support/docker/production/config/custom-environment-variables.yaml b/support/docker/production/config/custom-environment-variables.yaml
index 63459d8a0..8226715e0 100644
--- a/support/docker/production/config/custom-environment-variables.yaml
+++ b/support/docker/production/config/custom-environment-variables.yaml
@@ -117,6 +117,10 @@ transcoding:
117 2160p: 117 2160p:
118 __name: "PEERTUBE_TRANSCODING_2160P" 118 __name: "PEERTUBE_TRANSCODING_2160P"
119 __format: "json" 119 __format: "json"
120 webtorrent:
121 enabled:
122 __name: "PEERTUBE_TRANSCODING_WEBTORRENT_ENABLED"
123 __format: "json"
120 hls: 124 hls:
121 enabled: 125 enabled:
122 __name: "PEERTUBE_TRANSCODING_HLS_ENABLED" 126 __name: "PEERTUBE_TRANSCODING_HLS_ENABLED"
diff --git a/support/systemd/peertube.service b/support/systemd/peertube.service
index cf4e7b417..bdeb76b51 100644
--- a/support/systemd/peertube.service
+++ b/support/systemd/peertube.service
@@ -8,7 +8,7 @@ Environment=NODE_ENV=production
8Environment=NODE_CONFIG_DIR=/var/www/peertube/config 8Environment=NODE_CONFIG_DIR=/var/www/peertube/config
9User=peertube 9User=peertube
10Group=peertube 10Group=peertube
11ExecStart=/usr/bin/npm start 11ExecStart=/usr/bin/node dist/server
12WorkingDirectory=/var/www/peertube/peertube-latest 12WorkingDirectory=/var/www/peertube/peertube-latest
13StandardOutput=syslog 13StandardOutput=syslog
14StandardError=syslog 14StandardError=syslog
diff --git a/yarn.lock b/yarn.lock
index b2d5a594c..77710cfd3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -68,23 +68,23 @@
68 integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw== 68 integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==
69 69
70"@babel/highlight@^7.10.4", "@babel/highlight@^7.12.13": 70"@babel/highlight@^7.10.4", "@babel/highlight@^7.12.13":
71 version "7.13.8" 71 version "7.13.10"
72 resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.13.8.tgz#10b2dac78526424dfc1f47650d0e415dfd9dc481" 72 resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.13.10.tgz#a8b2a66148f5b27d666b15d81774347a731d52d1"
73 integrity sha512-4vrIhfJyfNf+lCtXC2ck1rKSzDwciqF7IWFhXXrSOUC2O5DrVp+w4c6ed4AllTxhTkUP5x2tYj41VaxdVMMRDw== 73 integrity sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg==
74 dependencies: 74 dependencies:
75 "@babel/helper-validator-identifier" "^7.12.11" 75 "@babel/helper-validator-identifier" "^7.12.11"
76 chalk "^2.0.0" 76 chalk "^2.0.0"
77 js-tokens "^4.0.0" 77 js-tokens "^4.0.0"
78 78
79"@babel/parser@^7.6.0", "@babel/parser@^7.9.6": 79"@babel/parser@^7.6.0", "@babel/parser@^7.9.6":
80 version "7.13.9" 80 version "7.13.10"
81 resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.9.tgz#ca34cb95e1c2dd126863a84465ae8ef66114be99" 81 resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.10.tgz#8f8f9bf7b3afa3eabd061f7a5bcdf4fec3c48409"
82 integrity sha512-nEUfRiARCcaVo3ny3ZQjURjHQZUo/JkEw7rLlSZy/psWGnvwXFtPcr6jb7Yb41DVW5LTe6KRq9LGleRNsg1Frw== 82 integrity sha512-0s7Mlrw9uTWkYua7xWr99Wpk2bnGa0ANleKfksYAES8LpWH4gW1OUr42vqKNf0us5UQNfru2wPqMqRITzq/SIQ==
83 83
84"@babel/runtime@^7.7.2": 84"@babel/runtime@^7.7.2":
85 version "7.13.9" 85 version "7.13.10"
86 resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.9.tgz#97dbe2116e2630c489f22e0656decd60aaa1fcee" 86 resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.10.tgz#47d42a57b6095f4468da440388fdbad8bebf0d7d"
87 integrity sha512-aY2kU+xgJ3dJ1eU6FMB9EH8dIe8dmusF1xEku52joLvw6eAFN0AI+WxCLDnpev2LEejWBAy2sBvBOBAjI3zmvA== 87 integrity sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==
88 dependencies: 88 dependencies:
89 regenerator-runtime "^0.13.4" 89 regenerator-runtime "^0.13.4"
90 90
@@ -447,13 +447,13 @@
447 tlds "^1.218.0" 447 tlds "^1.218.0"
448 448
449"@mapbox/node-pre-gyp@^1.0.0": 449"@mapbox/node-pre-gyp@^1.0.0":
450 version "1.0.0" 450 version "1.0.1"
451 resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.0.tgz#2b809e701da0f6729b47fe78ad4b9dc187a7d2e5" 451 resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.1.tgz#1b23a8decb5e6356b04770d586067d2bff2703dd"
452 integrity sha512-mEaiD1CURETR/dBIiJAwz0M0Q0mH3gCW4pPMaIlNt97mdzYUVeqGcTJSamgJpS6Tg4tBHDrOJpjdh5fJTLnyNQ== 452 integrity sha512-CUBdThIZMoLEQQxACwhLsPg/puxBca0abTH3ixuvBQkhjJ80Hdp99jmVjxFCOa52/tZqN9d70IbGUf+OuKDHGA==
453 dependencies: 453 dependencies:
454 detect-libc "^1.0.3" 454 detect-libc "^1.0.3"
455 http-proxy-agent "^4.0.1" 455 http-proxy-agent "^4.0.1"
456 mkdirp "^1.0.4" 456 make-dir "^3.1.0"
457 node-fetch "^2.6.1" 457 node-fetch "^2.6.1"
458 nopt "^5.0.0" 458 nopt "^5.0.0"
459 npmlog "^4.1.2" 459 npmlog "^4.1.2"
@@ -461,20 +461,20 @@
461 semver "^7.3.4" 461 semver "^7.3.4"
462 tar "^6.1.0" 462 tar "^6.1.0"
463 463
464"@nestjs/common@7.6.13": 464"@nestjs/common@7.6.14":
465 version "7.6.13" 465 version "7.6.14"
466 resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-7.6.13.tgz#597558afedfddeb5021fe8a154327ee082279ab8" 466 resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-7.6.14.tgz#abdad360ef107482345b111eeee74fbef00620c9"
467 integrity sha512-xijw6so4yA8Ywi8mnrA7Kz97ZC36u20Eyb5/XvmokdLcgTcTyHVdE39r44JYnjHPf8SKZoJ965zdu/fKl4s4GQ== 467 integrity sha512-XJrGoGttCsIOvG2+EXl09pl9iCmYXnhPjx3ndPPigMRdXQGLVpF38OdzroWTD7aYU5rHo3Co21G9cYl8aqdt2Q==
468 dependencies: 468 dependencies:
469 axios "0.21.1" 469 axios "0.21.1"
470 iterare "1.2.1" 470 iterare "1.2.1"
471 tslib "2.1.0" 471 tslib "2.1.0"
472 uuid "8.3.2" 472 uuid "8.3.2"
473 473
474"@nestjs/core@7.6.13": 474"@nestjs/core@7.6.14":
475 version "7.6.13" 475 version "7.6.14"
476 resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-7.6.13.tgz#b7518dceb436e6ed2c1fad2cff86ddf69b143e73" 476 resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-7.6.14.tgz#b3be15506aee33b847abce993a7371439b292dd9"
477 integrity sha512-8oY8yZSgri2DngqmvBMtwYw1GIAaXbUXS7Y0mp/iSZ6jP7CQqYCybdcMPneunrt5PG8rtJsq6n+4JNRvxXrVmA== 477 integrity sha512-iAeQIsC79xcLTpga3he48ROX4g561VFsfbksicqotrFy0k9czKxVtHxevsnwo8KzFsYXQqOCO6XYI8MsuAjMcg==
478 dependencies: 478 dependencies:
479 "@nuxtjs/opencollective" "0.3.2" 479 "@nuxtjs/opencollective" "0.3.2"
480 fast-safe-stringify "2.0.7" 480 fast-safe-stringify "2.0.7"
@@ -515,12 +515,12 @@
515 node-fetch "^2.6.1" 515 node-fetch "^2.6.1"
516 516
517"@openapitools/openapi-generator-cli@^2.1.4": 517"@openapitools/openapi-generator-cli@^2.1.4":
518 version "2.1.26" 518 version "2.2.2"
519 resolved "https://registry.yarnpkg.com/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.1.26.tgz#69108458c0c1a0a3964d9b3e2f0360b195c8ea5f" 519 resolved "https://registry.yarnpkg.com/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.2.2.tgz#12b2171a0404731e35aa89a2e0c146186480f51c"
520 integrity sha512-wr4LHQCoZCvLhf0/UY/9AZYTVi3nWvvOT+/JFjZYWDA/TIqC4eWxPjzM5tnSzGed6gBTuNHEh8gUonDz6WOZDw== 520 integrity sha512-Hl0/5bvv/ETYFuPpTPXqAtChHE2+lLrH0ATl8MtNDxtdXRLoQGCeT8jdT600VvCqJToRkNvQ1JPHbcg/hehyBw==
521 dependencies: 521 dependencies:
522 "@nestjs/common" "7.6.13" 522 "@nestjs/common" "7.6.14"
523 "@nestjs/core" "7.6.13" 523 "@nestjs/core" "7.6.14"
524 "@nuxtjs/opencollective" "0.3.2" 524 "@nuxtjs/opencollective" "0.3.2"
525 chalk "4.1.0" 525 chalk "4.1.0"
526 commander "6.2.1" 526 commander "6.2.1"
@@ -763,13 +763,6 @@
763 dependencies: 763 dependencies:
764 "@types/node" "*" 764 "@types/node" "*"
765 765
766"@types/libxmljs@^0.18.0":
767 version "0.18.6"
768 resolved "https://registry.yarnpkg.com/@types/libxmljs/-/libxmljs-0.18.6.tgz#06cb685b791568c56224486338a50c00dbfbf629"
769 integrity sha512-xVUs71CwL5wYYfx5oH344DYWdoE2hVWlnRxlXFYyA8BcueN+Ey/h4FyhzEikbIJSXBKyPpJKhGu5c3NOx15nww==
770 dependencies:
771 "@types/node" "*"
772
773"@types/lodash@^4.14.64": 766"@types/lodash@^4.14.64":
774 version "4.14.168" 767 version "4.14.168"
775 resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.168.tgz#fe24632e79b7ade3f132891afff86caa5e5ce008" 768 resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.168.tgz#fe24632e79b7ade3f132891afff86caa5e5ce008"
@@ -836,9 +829,9 @@
836 "@types/express" "*" 829 "@types/express" "*"
837 830
838"@types/node@*", "@types/node@>=10.0.0", "@types/node@^14.14.28", "@types/node@^14.14.31": 831"@types/node@*", "@types/node@>=10.0.0", "@types/node@^14.14.28", "@types/node@^14.14.31":
839 version "14.14.31" 832 version "14.14.34"
840 resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.31.tgz#72286bd33d137aa0d152d47ec7c1762563d34055" 833 resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.34.tgz#07935194fc049069a1c56c0c274265abeddf88da"
841 integrity sha512-vFHy/ezP5qI0rFgJ7aQnjDXwAMrG0KqqIH7tQG5PPv3BWBayOPIQNBjVc/P6hhdZfMx51REc6tfDNXHUio893g== 834 integrity sha512-dBPaxocOK6UVyvhbnpFIj2W+S+1cBTkHQbFQfeeJhoKFbzYcVUGHvddeWPSucKATb3F0+pgDq0i6ghEaZjsugA==
842 835
843"@types/nodemailer@^6.2.0": 836"@types/nodemailer@^6.2.0":
844 version "6.4.0" 837 version "6.4.0"
@@ -883,9 +876,9 @@
883 "@types/node" "*" 876 "@types/node" "*"
884 877
885"@types/qs@*": 878"@types/qs@*":
886 version "6.9.5" 879 version "6.9.6"
887 resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.5.tgz#434711bdd49eb5ee69d90c1d67c354a9a8ecb18b" 880 resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.6.tgz#df9c3c8b31a247ec315e6996566be3171df4b3b1"
888 integrity sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ== 881 integrity sha512-0/HnwIfW4ki2D8L8c9GVcG5I72s9jP5GSLVF0VIXDW00kmIpA6O33G7a8n59Tmh7Nz0WUC3rSb7PTY/sdW2JzA==
889 882
890"@types/range-parser@*": 883"@types/range-parser@*":
891 version "1.2.3" 884 version "1.2.3"
@@ -986,12 +979,12 @@
986 "@types/node" "*" 979 "@types/node" "*"
987 980
988"@typescript-eslint/eslint-plugin@^4.8.1": 981"@typescript-eslint/eslint-plugin@^4.8.1":
989 version "4.16.1" 982 version "4.17.0"
990 resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.16.1.tgz#2caf6a79dd19c3853b8d39769a27fccb24e4e651" 983 resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.17.0.tgz#6f856eca4e6a52ce9cf127dfd349096ad936aa2d"
991 integrity sha512-SK777klBdlkUZpZLC1mPvyOWk9yAFCWmug13eAjVQ4/Q1LATE/NbcQL1xDHkptQkZOLnPmLUA1Y54m8dqYwnoQ== 984 integrity sha512-/fKFDcoHg8oNan39IKFOb5WmV7oWhQe1K6CDaAVfJaNWEhmfqlA24g+u1lqU5bMH7zuNasfMId4LaYWC5ijRLw==
992 dependencies: 985 dependencies:
993 "@typescript-eslint/experimental-utils" "4.16.1" 986 "@typescript-eslint/experimental-utils" "4.17.0"
994 "@typescript-eslint/scope-manager" "4.16.1" 987 "@typescript-eslint/scope-manager" "4.17.0"
995 debug "^4.1.1" 988 debug "^4.1.1"
996 functional-red-black-tree "^1.0.1" 989 functional-red-black-tree "^1.0.1"
997 lodash "^4.17.15" 990 lodash "^4.17.15"
@@ -999,60 +992,60 @@
999 semver "^7.3.2" 992 semver "^7.3.2"
1000 tsutils "^3.17.1" 993 tsutils "^3.17.1"
1001 994
1002"@typescript-eslint/experimental-utils@4.16.1": 995"@typescript-eslint/experimental-utils@4.17.0":
1003 version "4.16.1" 996 version "4.17.0"
1004 resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.16.1.tgz#da7a396dc7d0e01922acf102b76efff17320b328" 997 resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.17.0.tgz#762c44aaa1a6a3c05b6d63a8648fb89b89f84c80"
1005 integrity sha512-0Hm3LSlMYFK17jO4iY3un1Ve9x1zLNn4EM50Lia+0EV99NdbK+cn0er7HC7IvBA23mBg3P+8dUkMXy4leL33UQ== 998 integrity sha512-ZR2NIUbnIBj+LGqCFGQ9yk2EBQrpVVFOh9/Kd0Lm6gLpSAcCuLLe5lUCibKGCqyH9HPwYC0GIJce2O1i8VYmWA==
1006 dependencies: 999 dependencies:
1007 "@types/json-schema" "^7.0.3" 1000 "@types/json-schema" "^7.0.3"
1008 "@typescript-eslint/scope-manager" "4.16.1" 1001 "@typescript-eslint/scope-manager" "4.17.0"
1009 "@typescript-eslint/types" "4.16.1" 1002 "@typescript-eslint/types" "4.17.0"
1010 "@typescript-eslint/typescript-estree" "4.16.1" 1003 "@typescript-eslint/typescript-estree" "4.17.0"
1011 eslint-scope "^5.0.0" 1004 eslint-scope "^5.0.0"
1012 eslint-utils "^2.0.0" 1005 eslint-utils "^2.0.0"
1013 1006
1014"@typescript-eslint/parser@^4.0.0": 1007"@typescript-eslint/parser@^4.0.0":
1015 version "4.16.1" 1008 version "4.17.0"
1016 resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.16.1.tgz#3bbd3234dd3c5b882b2bcd9899bc30e1e1586d2a" 1009 resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.17.0.tgz#141b647ffc72ebebcbf9b0fe6087f65b706d3215"
1017 integrity sha512-/c0LEZcDL5y8RyI1zLcmZMvJrsR6SM1uetskFkoh3dvqDKVXPsXI+wFB/CbVw7WkEyyTKobC1mUNp/5y6gRvXg== 1010 integrity sha512-KYdksiZQ0N1t+6qpnl6JeK9ycCFprS9xBAiIrw4gSphqONt8wydBw4BXJi3C11ywZmyHulvMaLjWsxDjUSDwAw==
1018 dependencies: 1011 dependencies:
1019 "@typescript-eslint/scope-manager" "4.16.1" 1012 "@typescript-eslint/scope-manager" "4.17.0"
1020 "@typescript-eslint/types" "4.16.1" 1013 "@typescript-eslint/types" "4.17.0"
1021 "@typescript-eslint/typescript-estree" "4.16.1" 1014 "@typescript-eslint/typescript-estree" "4.17.0"
1022 debug "^4.1.1" 1015 debug "^4.1.1"
1023 1016
1024"@typescript-eslint/scope-manager@4.16.1": 1017"@typescript-eslint/scope-manager@4.17.0":
1025 version "4.16.1" 1018 version "4.17.0"
1026 resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.16.1.tgz#244e2006bc60cfe46987e9987f4ff49c9e3f00d5" 1019 resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.17.0.tgz#f4edf94eff3b52a863180f7f89581bf963e3d37d"
1027 integrity sha512-6IlZv9JaurqV0jkEg923cV49aAn8V6+1H1DRfhRcvZUrptQ+UtSKHb5kwTayzOYTJJ/RsYZdcvhOEKiBLyc0Cw== 1020 integrity sha512-OJ+CeTliuW+UZ9qgULrnGpPQ1bhrZNFpfT/Bc0pzNeyZwMik7/ykJ0JHnQ7krHanFN9wcnPK89pwn84cRUmYjw==
1028 dependencies: 1021 dependencies:
1029 "@typescript-eslint/types" "4.16.1" 1022 "@typescript-eslint/types" "4.17.0"
1030 "@typescript-eslint/visitor-keys" "4.16.1" 1023 "@typescript-eslint/visitor-keys" "4.17.0"
1031 1024
1032"@typescript-eslint/types@4.16.1": 1025"@typescript-eslint/types@4.17.0":
1033 version "4.16.1" 1026 version "4.17.0"
1034 resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.16.1.tgz#5ba2d3e38b1a67420d2487519e193163054d9c15" 1027 resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.17.0.tgz#f57d8fc7f31b348db946498a43050083d25f40ad"
1035 integrity sha512-nnKqBwMgRlhzmJQF8tnFDZWfunXmJyuXj55xc8Kbfup4PbkzdoDXZvzN8//EiKR27J6vUSU8j4t37yUuYPiLqA== 1028 integrity sha512-RN5z8qYpJ+kXwnLlyzZkiJwfW2AY458Bf8WqllkondQIcN2ZxQowAToGSd9BlAUZDB5Ea8I6mqL2quGYCLT+2g==
1036 1029
1037"@typescript-eslint/typescript-estree@4.16.1": 1030"@typescript-eslint/typescript-estree@4.17.0":
1038 version "4.16.1" 1031 version "4.17.0"
1039 resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.16.1.tgz#c2fc46b05a48fbf8bbe8b66a63f0a9ba04b356f1" 1032 resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.17.0.tgz#b835d152804f0972b80dbda92477f9070a72ded1"
1040 integrity sha512-m8I/DKHa8YbeHt31T+UGd/l8Kwr0XCTCZL3H4HMvvLCT7HU9V7yYdinTOv1gf/zfqNeDcCgaFH2BMsS8x6NvJg== 1033 integrity sha512-lRhSFIZKUEPPWpWfwuZBH9trYIEJSI0vYsrxbvVvNyIUDoKWaklOAelsSkeh3E2VBSZiNe9BZ4E5tYBZbUczVQ==
1041 dependencies: 1034 dependencies:
1042 "@typescript-eslint/types" "4.16.1" 1035 "@typescript-eslint/types" "4.17.0"
1043 "@typescript-eslint/visitor-keys" "4.16.1" 1036 "@typescript-eslint/visitor-keys" "4.17.0"
1044 debug "^4.1.1" 1037 debug "^4.1.1"
1045 globby "^11.0.1" 1038 globby "^11.0.1"
1046 is-glob "^4.0.1" 1039 is-glob "^4.0.1"
1047 semver "^7.3.2" 1040 semver "^7.3.2"
1048 tsutils "^3.17.1" 1041 tsutils "^3.17.1"
1049 1042
1050"@typescript-eslint/visitor-keys@4.16.1": 1043"@typescript-eslint/visitor-keys@4.17.0":
1051 version "4.16.1" 1044 version "4.17.0"
1052 resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.16.1.tgz#d7571fb580749fae621520deeb134370bbfc7293" 1045 resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.17.0.tgz#9c304cfd20287c14a31d573195a709111849b14d"
1053 integrity sha512-s/aIP1XcMkEqCNcPQtl60ogUYjSM8FU2mq1O7y5cFf3Xcob1z1iXWNB6cC43Op+NGRTFgGolri6s8z/efA9i1w== 1046 integrity sha512-WfuMN8mm5SSqXuAr9NM+fItJ0SVVphobWYkWOwQ1odsfC014Vdxk/92t4JwS1Q6fCA/ABfCKpa3AVtpUKTNKGQ==
1054 dependencies: 1047 dependencies:
1055 "@typescript-eslint/types" "4.16.1" 1048 "@typescript-eslint/types" "4.17.0"
1056 eslint-visitor-keys "^2.0.0" 1049 eslint-visitor-keys "^2.0.0"
1057 1050
1058"@ungap/promise-all-settled@1.1.2": 1051"@ungap/promise-all-settled@1.1.2":
@@ -1116,9 +1109,9 @@ ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4:
1116 uri-js "^4.2.2" 1109 uri-js "^4.2.2"
1117 1110
1118ajv@^7.0.2: 1111ajv@^7.0.2:
1119 version "7.1.1" 1112 version "7.2.1"
1120 resolved "https://registry.yarnpkg.com/ajv/-/ajv-7.1.1.tgz#1e6b37a454021fa9941713f38b952fc1c8d32a84" 1113 resolved "https://registry.yarnpkg.com/ajv/-/ajv-7.2.1.tgz#a5ac226171912447683524fa2f1248fcf8bac83d"
1121 integrity sha512-ga/aqDYnUy/o7vbsRTFhhTsNeXiYb5JWDIcRIeZfwRNCefwjNTVYCGdGSUrEmiu3yDK3vFvNbgJxvrQW4JXrYQ== 1114 integrity sha512-+nu0HDv7kNSOua9apAVc979qd932rrZeb3WOvoiD31A/p1mIE5/9bN2027pE2rOPYEdS3UHzsvof4hY+lM9/WQ==
1122 dependencies: 1115 dependencies:
1123 fast-deep-equal "^3.1.1" 1116 fast-deep-equal "^3.1.1"
1124 json-schema-traverse "^1.0.0" 1117 json-schema-traverse "^1.0.0"
@@ -1373,11 +1366,12 @@ at-least-node@^1.0.0:
1373 integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== 1366 integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==
1374 1367
1375autocannon@^7.0.4: 1368autocannon@^7.0.4:
1376 version "7.0.4" 1369 version "7.0.5"
1377 resolved "https://registry.yarnpkg.com/autocannon/-/autocannon-7.0.4.tgz#c812c11af283254bff4bd75cce8383e79550c882" 1370 resolved "https://registry.yarnpkg.com/autocannon/-/autocannon-7.0.5.tgz#7c195ba09ae3b299d6f7532950d1e07041538b29"
1378 integrity sha512-+A+kSsVrx9F9fFbPAD7YytGQfCKgkaCIut4KrnYBbY2hmboAT065ClxqBsVqstokvFfdBAfSMPh0VSc6ktiimg== 1371 integrity sha512-VMOfWf0e9EB5Crr7/snXTb64oC7I3lofpAjBcPWvHGet94DKjHCsbj05iIt2WTenPKub++6PETb/H9qleV9yJg==
1379 dependencies: 1372 dependencies:
1380 chalk "^4.1.0" 1373 chalk "^4.1.0"
1374 char-spinner "^1.0.1"
1381 cli-table3 "^0.6.0" 1375 cli-table3 "^0.6.0"
1382 clone "^2.1.2" 1376 clone "^2.1.2"
1383 color-support "^1.1.1" 1377 color-support "^1.1.1"
@@ -1391,11 +1385,10 @@ autocannon@^7.0.4:
1391 manage-path "^2.0.0" 1385 manage-path "^2.0.0"
1392 minimist "^1.2.0" 1386 minimist "^1.2.0"
1393 on-net-listen "^1.1.1" 1387 on-net-listen "^1.1.1"
1394 ora "^5.1.0"
1395 pretty-bytes "^5.4.1" 1388 pretty-bytes "^5.4.1"
1396 progress "^2.0.3" 1389 progress "^2.0.3"
1397 reinterval "^1.1.0" 1390 reinterval "^1.1.0"
1398 retimer "^2.0.0" 1391 retimer "^3.0.0"
1399 semver "^7.3.2" 1392 semver "^7.3.2"
1400 timestring "^6.0.0" 1393 timestring "^6.0.0"
1401 1394
@@ -1468,7 +1461,7 @@ basic-auth-connect@^1.0.0:
1468 resolved "https://registry.yarnpkg.com/basic-auth-connect/-/basic-auth-connect-1.0.0.tgz#fdb0b43962ca7b40456a7c2bb48fe173da2d2122" 1461 resolved "https://registry.yarnpkg.com/basic-auth-connect/-/basic-auth-connect-1.0.0.tgz#fdb0b43962ca7b40456a7c2bb48fe173da2d2122"
1469 integrity sha1-/bC0OWLKe0BFanwrtI/hc9otISI= 1462 integrity sha1-/bC0OWLKe0BFanwrtI/hc9otISI=
1470 1463
1471basic-auth@^2.0.0, basic-auth@~2.0.1: 1464basic-auth@2.0.1, basic-auth@~2.0.1:
1472 version "2.0.1" 1465 version "2.0.1"
1473 resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" 1466 resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a"
1474 integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== 1467 integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==
@@ -1545,11 +1538,6 @@ binary-search@^1.3.4:
1545 resolved "https://registry.yarnpkg.com/binary-search/-/binary-search-1.3.6.tgz#e32426016a0c5092f0f3598836a1c7da3560565c" 1538 resolved "https://registry.yarnpkg.com/binary-search/-/binary-search-1.3.6.tgz#e32426016a0c5092f0f3598836a1c7da3560565c"
1546 integrity sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA== 1539 integrity sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA==
1547 1540
1548bindings@~1.3.0:
1549 version "1.3.1"
1550 resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.3.1.tgz#21fc7c6d67c18516ec5aaa2815b145ff77b26ea5"
1551 integrity sha512-i47mqjF9UbjxJhxGf+pZ6kSxrnI3wBLlnGI2ArWJ4r0VrvDS7ZYXkprq/pLaBWYq4GM0r4zdHY+NNRqEMU7uew==
1552
1553bitfield@^4.0.0: 1541bitfield@^4.0.0:
1554 version "4.0.0" 1542 version "4.0.0"
1555 resolved "https://registry.yarnpkg.com/bitfield/-/bitfield-4.0.0.tgz#3094123c870030dc6198a283d779639bd2a8e256" 1543 resolved "https://registry.yarnpkg.com/bitfield/-/bitfield-4.0.0.tgz#3094123c870030dc6198a283d779639bd2a8e256"
@@ -1626,15 +1614,6 @@ bittorrent-tracker@^9.0.0:
1626 bufferutil "^4.0.1" 1614 bufferutil "^4.0.1"
1627 utf-8-validate "^5.0.2" 1615 utf-8-validate "^5.0.2"
1628 1616
1629bl@^4.0.3:
1630 version "4.1.0"
1631 resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
1632 integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==
1633 dependencies:
1634 buffer "^5.5.0"
1635 inherits "^2.0.4"
1636 readable-stream "^3.4.0"
1637
1638blob-to-buffer@^1.2.9: 1617blob-to-buffer@^1.2.9:
1639 version "1.2.9" 1618 version "1.2.9"
1640 resolved "https://registry.yarnpkg.com/blob-to-buffer/-/blob-to-buffer-1.2.9.tgz#a17fd6c1c564011408f8971e451544245daaa84a" 1619 resolved "https://registry.yarnpkg.com/blob-to-buffer/-/blob-to-buffer-1.2.9.tgz#a17fd6c1c564011408f8971e451544245daaa84a"
@@ -1652,16 +1631,16 @@ block-stream2@^2.0.0, block-stream2@^2.1.0:
1652 dependencies: 1631 dependencies:
1653 readable-stream "^3.4.0" 1632 readable-stream "^3.4.0"
1654 1633
1634bluebird@3.7.2, bluebird@^3.5.0, bluebird@^3.7.2:
1635 version "3.7.2"
1636 resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
1637 integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
1638
1655bluebird@^2.10.0: 1639bluebird@^2.10.0:
1656 version "2.11.0" 1640 version "2.11.0"
1657 resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1" 1641 resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1"
1658 integrity sha1-U0uQM8AiyVecVro7Plpcqvu2UOE= 1642 integrity sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=
1659 1643
1660bluebird@^3.0.5, bluebird@^3.5.0, bluebird@^3.5.1, bluebird@^3.7.2:
1661 version "3.7.2"
1662 resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
1663 integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
1664
1665bmp-js@^0.1.0: 1644bmp-js@^0.1.0:
1666 version "0.1.0" 1645 version "0.1.0"
1667 resolved "https://registry.yarnpkg.com/bmp-js/-/bmp-js-0.1.0.tgz#e05a63f796a6c1ff25f4771ec7adadc148c07233" 1646 resolved "https://registry.yarnpkg.com/bmp-js/-/bmp-js-0.1.0.tgz#e05a63f796a6c1ff25f4771ec7adadc148c07233"
@@ -1770,7 +1749,7 @@ buffer-writer@2.0.0:
1770 resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04" 1749 resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04"
1771 integrity sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw== 1750 integrity sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==
1772 1751
1773buffer@^5.2.0, buffer@^5.5.0: 1752buffer@^5.2.0:
1774 version "5.7.1" 1753 version "5.7.1"
1775 resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" 1754 resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
1776 integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== 1755 integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
@@ -1926,9 +1905,9 @@ chai-xml@^0.4.0:
1926 xml2js "^0.4.23" 1905 xml2js "^0.4.23"
1927 1906
1928chai@^4.1.1: 1907chai@^4.1.1:
1929 version "4.3.1" 1908 version "4.3.3"
1930 resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.1.tgz#6fc6af447610709818e5c45116207d60b8a49cfd" 1909 resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.3.tgz#f2b2ad9736999d07a7ff95cf1e7086c43a76f72d"
1931 integrity sha512-JClPZFGRcSl7X8dYzlCJY7v+X1fBA+9Y339Y8EqhBVfp0QC1hTnaf7nMfR+XZ74clkBC64b0iEw2cWKHt3EVqA== 1910 integrity sha512-MPSLOZwxxnA0DhLE84klnGPojWFK5KuhP7/j5dTsxpr2S3XlkqJP5WbyYl1gCTWvG2Z5N+HD4F472WsbEZL6Pw==
1932 dependencies: 1911 dependencies:
1933 assertion-error "^1.1.0" 1912 assertion-error "^1.1.0"
1934 check-error "^1.0.2" 1913 check-error "^1.0.2"
@@ -1962,6 +1941,11 @@ chalk@^3.0.0:
1962 ansi-styles "^4.1.0" 1941 ansi-styles "^4.1.0"
1963 supports-color "^7.1.0" 1942 supports-color "^7.1.0"
1964 1943
1944char-spinner@^1.0.1:
1945 version "1.0.1"
1946 resolved "https://registry.yarnpkg.com/char-spinner/-/char-spinner-1.0.1.tgz#e6ea67bd247e107112983b7ab0479ed362800081"
1947 integrity sha1-5upnvSR+EHESmDt6sEee02KAAIE=
1948
1965character-parser@^2.2.0: 1949character-parser@^2.2.0:
1966 version "2.2.0" 1950 version "2.2.0"
1967 resolved "https://registry.yarnpkg.com/character-parser/-/character-parser-2.2.0.tgz#c7ce28f36d4bcd9744e5ffc2c5fcde1c73261fc0" 1951 resolved "https://registry.yarnpkg.com/character-parser/-/character-parser-2.2.0.tgz#c7ce28f36d4bcd9744e5ffc2c5fcde1c73261fc0"
@@ -2028,11 +2012,6 @@ chokidar@3.5.1, chokidar@^3.2.2, chokidar@^3.4.2:
2028 optionalDependencies: 2012 optionalDependencies:
2029 fsevents "~2.3.1" 2013 fsevents "~2.3.1"
2030 2014
2031chownr@^1.1.1:
2032 version "1.1.4"
2033 resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
2034 integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
2035
2036chownr@^2.0.0: 2015chownr@^2.0.0:
2037 version "2.0.0" 2016 version "2.0.0"
2038 resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" 2017 resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
@@ -2061,9 +2040,9 @@ chrome-net@^3.3.2, chrome-net@^3.3.3, chrome-net@^3.3.4:
2061 inherits "^2.0.1" 2040 inherits "^2.0.1"
2062 2041
2063chunk-store-stream@^4.2.0: 2042chunk-store-stream@^4.2.0:
2064 version "4.2.0" 2043 version "4.3.0"
2065 resolved "https://registry.yarnpkg.com/chunk-store-stream/-/chunk-store-stream-4.2.0.tgz#18f673c495946c4cdcf14124a3ebd5f31eb0ea35" 2044 resolved "https://registry.yarnpkg.com/chunk-store-stream/-/chunk-store-stream-4.3.0.tgz#3de5f4dfe19729366c29bb7ed52d139f9af29f0e"
2066 integrity sha512-90iueoPoqT2isnmy1fyqwzgFy5FokuaxQuijOQG1VgC/6DaXRfeYN0da8iWENkzqElWhqLxo8pWc7pH9dmxlcA== 2045 integrity sha512-qby+/RXoiMoTVtPiylWZt7KFF1jy6M829TzMi2hxZtBIH9ptV19wxcft6zGiXLokJgCbuZPGNGab6DWHqiSEKw==
2067 dependencies: 2046 dependencies:
2068 block-stream2 "^2.0.0" 2047 block-stream2 "^2.0.0"
2069 readable-stream "^3.6.0" 2048 readable-stream "^3.6.0"
@@ -2092,11 +2071,6 @@ cli-cursor@^3.1.0:
2092 dependencies: 2071 dependencies:
2093 restore-cursor "^3.1.0" 2072 restore-cursor "^3.1.0"
2094 2073
2095cli-spinners@^2.5.0:
2096 version "2.5.0"
2097 resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.5.0.tgz#12763e47251bf951cb75c201dfa58ff1bcb2d047"
2098 integrity sha512-PC+AmIuK04E6aeSs/pUccSujsTzBhu4HzC2dL+CfJB/Jcc2qTRbEwZQDfIUpt2Xl8BodYBEq8w4fc0kU2I9DjQ==
2099
2100cli-table3@^0.6.0: 2074cli-table3@^0.6.0:
2101 version "0.6.0" 2075 version "0.6.0"
2102 resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.0.tgz#b7b1bc65ca8e7b5cef9124e13dc2b21e2ce4faee" 2076 resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.0.tgz#b7b1bc65ca8e7b5cef9124e13dc2b21e2ce4faee"
@@ -2204,9 +2178,9 @@ color-name@^1.0.0, color-name@~1.1.4:
2204 integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== 2178 integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
2205 2179
2206color-string@^1.5.2: 2180color-string@^1.5.2:
2207 version "1.5.4" 2181 version "1.5.5"
2208 resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.4.tgz#dd51cd25cfee953d138fe4002372cc3d0e504cb6" 2182 resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.5.tgz#65474a8f0e7439625f3d27a6a19d89fc45223014"
2209 integrity sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw== 2183 integrity sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg==
2210 dependencies: 2184 dependencies:
2211 color-name "^1.0.0" 2185 color-name "^1.0.0"
2212 simple-swizzle "^0.2.2" 2186 simple-swizzle "^0.2.2"
@@ -2224,7 +2198,7 @@ color@3.0.x:
2224 color-convert "^1.9.1" 2198 color-convert "^1.9.1"
2225 color-string "^1.5.2" 2199 color-string "^1.5.2"
2226 2200
2227colorette@^1.2.1: 2201colorette@^1.2.2:
2228 version "1.2.2" 2202 version "1.2.2"
2229 resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" 2203 resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94"
2230 integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== 2204 integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==
@@ -2352,9 +2326,9 @@ concurrently@^6.0.0:
2352 yargs "^16.2.0" 2326 yargs "^16.2.0"
2353 2327
2354config@^3.0.0: 2328config@^3.0.0:
2355 version "3.3.4" 2329 version "3.3.6"
2356 resolved "https://registry.yarnpkg.com/config/-/config-3.3.4.tgz#55811abc2752b38a7b806cbdbc2da79c428312b7" 2330 resolved "https://registry.yarnpkg.com/config/-/config-3.3.6.tgz#b87799db7399cc34988f55379b5f43465b1b065c"
2357 integrity sha512-URO0m6z+rtENGHqtzO7W7C35iF+H9KVe7JJFps+3TIqZEOHl83NqTAgp5h8ah96m4NPQnx08nPBfbtDU+PgjVA== 2331 integrity sha512-Hj5916C5HFawjYJat1epbyY2PlAgLpBtDUlr0MxGLgo3p5+7kylyvnRY18PqJHgnNWXcdd0eWDemT7eYWuFgwg==
2358 dependencies: 2332 dependencies:
2359 json5 "^2.1.1" 2333 json5 "^2.1.1"
2360 2334
@@ -2584,9 +2558,9 @@ dashdash@^1.12.0:
2584 assert-plus "^1.0.0" 2558 assert-plus "^1.0.0"
2585 2559
2586date-fns@^2.0.1, date-fns@^2.16.1: 2560date-fns@^2.0.1, date-fns@^2.16.1:
2587 version "2.18.0" 2561 version "2.19.0"
2588 resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.18.0.tgz#08e50aee300ad0d2c5e054e3f0d10d8f9cdfe09e" 2562 resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.19.0.tgz#65193348635a28d5d916c43ec7ce6fbd145059e1"
2589 integrity sha512-NYyAg4wRmGVU4miKq5ivRACOODdZRY3q5WLmOJSq8djyzftYphU7dTHLcEtLqEvfqMKQ0jVv91P4BAwIjsXIcw== 2563 integrity sha512-X3bf2iTPgCAQp9wvjOQytnf5vO5rESYRXlPIVcgSbtT5OTScPcsf9eZU+B/YIkKAtYr5WeCii58BgATrNitlWg==
2590 2564
2591dateformat@^3.0.3: 2565dateformat@^3.0.3:
2592 version "3.0.3" 2566 version "3.0.3"
@@ -2756,7 +2730,7 @@ destroy@~1.0.4:
2756 resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" 2730 resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
2757 integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= 2731 integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
2758 2732
2759detect-libc@^1.0.2, detect-libc@^1.0.3: 2733detect-libc@^1.0.3:
2760 version "1.0.3" 2734 version "1.0.3"
2761 resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" 2735 resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
2762 integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= 2736 integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=
@@ -2840,9 +2814,9 @@ domhandler@^4.0.0:
2840 domelementtype "^2.1.0" 2814 domelementtype "^2.1.0"
2841 2815
2842domutils@^2.0.0, domutils@^2.4.3, domutils@^2.4.4: 2816domutils@^2.0.0, domutils@^2.4.3, domutils@^2.4.4:
2843 version "2.4.4" 2817 version "2.5.0"
2844 resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.4.4.tgz#282739c4b150d022d34699797369aad8d19bbbd3" 2818 resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.5.0.tgz#42f49cffdabb92ad243278b331fd761c1c2d3039"
2845 integrity sha512-jBC0vOsECI4OMdD0GC9mGn7NXPLb+Qt6KW1YDQzeQYRUFKmNG8lh7mO5HiELfr+lLQE7loDVI4QcAxV80HS+RA== 2819 integrity sha512-Ho16rzNMOFk2fPwChGh3D2D9OEHAfG19HgmRR2l+WLSsIstNsAYBzePH412bL0y5T44ejABIVfTHQ8nqi/tBCg==
2846 dependencies: 2820 dependencies:
2847 dom-serializer "^1.0.1" 2821 dom-serializer "^1.0.1"
2848 domelementtype "^2.0.1" 2822 domelementtype "^2.0.1"
@@ -3047,27 +3021,10 @@ error-ex@^1.2.0, error-ex@^1.3.1:
3047 dependencies: 3021 dependencies:
3048 is-arrayish "^0.2.1" 3022 is-arrayish "^0.2.1"
3049 3023
3050es-abstract@^1.17.0-next.0: 3024es-abstract@^1.17.0-next.0, es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2:
3051 version "1.17.7" 3025 version "1.18.0"
3052 resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c" 3026 resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0.tgz#ab80b359eecb7ede4c298000390bc5ac3ec7b5a4"
3053 integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g== 3027 integrity sha512-LJzK7MrQa8TS0ja2w3YNLzUgJCGPdPOV1yVvezjNnS89D+VR08+Szt2mz3YB2Dck/+w5tfIq/RoUAFqJJGM2yw==
3054 dependencies:
3055 es-to-primitive "^1.2.1"
3056 function-bind "^1.1.1"
3057 has "^1.0.3"
3058 has-symbols "^1.0.1"
3059 is-callable "^1.2.2"
3060 is-regex "^1.1.1"
3061 object-inspect "^1.8.0"
3062 object-keys "^1.1.1"
3063 object.assign "^4.1.1"
3064 string.prototype.trimend "^1.0.1"
3065 string.prototype.trimstart "^1.0.1"
3066
3067es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2:
3068 version "1.18.0-next.3"
3069 resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.3.tgz#56bc8b5cc36b2cca25a13be07f3c02c2343db6b7"
3070 integrity sha512-VMzHx/Bczjg59E6jZOQjHeN3DEoptdhejpARgflAViidlqSpjdq9zA6lKwlhRRs/lOw1gHJv2xkkSFRgvEwbQg==
3071 dependencies: 3028 dependencies:
3072 call-bind "^1.0.2" 3029 call-bind "^1.0.2"
3073 es-to-primitive "^1.2.1" 3030 es-to-primitive "^1.2.1"
@@ -3401,15 +3358,6 @@ exif-parser@^0.1.12:
3401 resolved "https://registry.yarnpkg.com/exif-parser/-/exif-parser-0.1.12.tgz#58a9d2d72c02c1f6f02a0ef4a9166272b7760922" 3358 resolved "https://registry.yarnpkg.com/exif-parser/-/exif-parser-0.1.12.tgz#58a9d2d72c02c1f6f02a0ef4a9166272b7760922"
3402 integrity sha1-WKnS1ywCwfbwKg70qRZicrd2CSI= 3359 integrity sha1-WKnS1ywCwfbwKg70qRZicrd2CSI=
3403 3360
3404express-oauth-server@^2.0.0:
3405 version "2.0.0"
3406 resolved "https://registry.yarnpkg.com/express-oauth-server/-/express-oauth-server-2.0.0.tgz#57b08665c1201532f52c4c02f19709238b99a48d"
3407 integrity sha1-V7CGZcEgFTL1LEwC8ZcJI4uZpI0=
3408 dependencies:
3409 bluebird "^3.0.5"
3410 express "^4.13.3"
3411 oauth2-server "3.0.0"
3412
3413express-rate-limit@^5.0.0: 3361express-rate-limit@^5.0.0:
3414 version "5.2.6" 3362 version "5.2.6"
3415 resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-5.2.6.tgz#b454e1be8a252081bda58460e0a25bf43ee0f7b0" 3363 resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-5.2.6.tgz#b454e1be8a252081bda58460e0a25bf43ee0f7b0"
@@ -3423,7 +3371,7 @@ express-validator@^6.4.0:
3423 lodash "^4.17.20" 3371 lodash "^4.17.20"
3424 validator "^13.5.2" 3372 validator "^13.5.2"
3425 3373
3426express@^4.12.4, express@^4.13.3, express@^4.16.4, express@^4.17.1: 3374express@^4.12.4, express@^4.16.4, express@^4.17.1:
3427 version "4.17.1" 3375 version "4.17.1"
3428 resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" 3376 resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
3429 integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== 3377 integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==
@@ -3527,6 +3475,11 @@ fast-safe-stringify@2.0.7, fast-safe-stringify@^2.0.4, fast-safe-stringify@^2.0.
3527 resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743" 3475 resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743"
3528 integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA== 3476 integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==
3529 3477
3478fast-xml-parser@^3.19.0:
3479 version "3.19.0"
3480 resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-3.19.0.tgz#cb637ec3f3999f51406dd8ff0e6fc4d83e520d01"
3481 integrity sha512-4pXwmBplsCPv8FOY1WRakF970TjNGnGnfbOnLqjlYvMiF1SR3yOHyxMR/YCXpPTOspNF5gwudqktIP4VsWkvBg==
3482
3530fastq@^1.6.0: 3483fastq@^1.6.0:
3531 version "1.11.0" 3484 version "1.11.0"
3532 resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.0.tgz#bb9fb955a07130a918eb63c1f5161cc32a5d0858" 3485 resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.0.tgz#bb9fb955a07130a918eb63c1f5161cc32a5d0858"
@@ -3749,13 +3702,6 @@ fs-extra@9.1.0, fs-extra@^9.0.0:
3749 jsonfile "^6.0.1" 3702 jsonfile "^6.0.1"
3750 universalify "^2.0.0" 3703 universalify "^2.0.0"
3751 3704
3752fs-minipass@^1.2.5:
3753 version "1.2.7"
3754 resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7"
3755 integrity sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==
3756 dependencies:
3757 minipass "^2.6.0"
3758
3759fs-minipass@^2.0.0: 3705fs-minipass@^2.0.0:
3760 version "2.1.0" 3706 version "2.1.0"
3761 resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" 3707 resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
@@ -3873,9 +3819,9 @@ gifwrap@^0.9.2:
3873 omggif "^1.0.10" 3819 omggif "^1.0.10"
3874 3820
3875glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@~5.1.0: 3821glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@~5.1.0:
3876 version "5.1.1" 3822 version "5.1.2"
3877 resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" 3823 resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
3878 integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== 3824 integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
3879 dependencies: 3825 dependencies:
3880 is-glob "^4.0.1" 3826 is-glob "^4.0.1"
3881 3827
@@ -3925,6 +3871,23 @@ globby@^11.0.1:
3925 merge2 "^1.3.0" 3871 merge2 "^1.3.0"
3926 slash "^3.0.0" 3872 slash "^3.0.0"
3927 3873
3874got@^11.8.2, got@~11.8.1:
3875 version "11.8.2"
3876 resolved "https://registry.yarnpkg.com/got/-/got-11.8.2.tgz#7abb3959ea28c31f3576f1576c1effce23f33599"
3877 integrity sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==
3878 dependencies:
3879 "@sindresorhus/is" "^4.0.0"
3880 "@szmarczak/http-timer" "^4.0.5"
3881 "@types/cacheable-request" "^6.0.1"
3882 "@types/responselike" "^1.0.0"
3883 cacheable-lookup "^5.0.3"
3884 cacheable-request "^7.0.1"
3885 decompress-response "^6.0.0"
3886 http2-wrapper "^1.0.0-beta.5.2"
3887 lowercase-keys "^2.0.0"
3888 p-cancelable "^2.0.0"
3889 responselike "^2.0.0"
3890
3928got@^9.6.0: 3891got@^9.6.0:
3929 version "9.6.0" 3892 version "9.6.0"
3930 resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" 3893 resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85"
@@ -3942,23 +3905,6 @@ got@^9.6.0:
3942 to-readable-stream "^1.0.0" 3905 to-readable-stream "^1.0.0"
3943 url-parse-lax "^3.0.0" 3906 url-parse-lax "^3.0.0"
3944 3907
3945got@~11.8.1:
3946 version "11.8.2"
3947 resolved "https://registry.yarnpkg.com/got/-/got-11.8.2.tgz#7abb3959ea28c31f3576f1576c1effce23f33599"
3948 integrity sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==
3949 dependencies:
3950 "@sindresorhus/is" "^4.0.0"
3951 "@szmarczak/http-timer" "^4.0.5"
3952 "@types/cacheable-request" "^6.0.1"
3953 "@types/responselike" "^1.0.0"
3954 cacheable-lookup "^5.0.3"
3955 cacheable-request "^7.0.1"
3956 decompress-response "^6.0.0"
3957 http2-wrapper "^1.0.0-beta.5.2"
3958 lowercase-keys "^2.0.0"
3959 p-cancelable "^2.0.0"
3960 responselike "^2.0.0"
3961
3962graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0: 3908graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0:
3963 version "4.2.6" 3909 version "4.2.6"
3964 resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" 3910 resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee"
@@ -4104,9 +4050,9 @@ htmlparser2@^4.0.0, htmlparser2@^4.1.0:
4104 entities "^2.0.0" 4050 entities "^2.0.0"
4105 4051
4106htmlparser2@^6.0.0: 4052htmlparser2@^6.0.0:
4107 version "6.0.0" 4053 version "6.0.1"
4108 resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.0.0.tgz#c2da005030390908ca4c91e5629e418e0665ac01" 4054 resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.0.1.tgz#422521231ef6d42e56bd411da8ba40aa36e91446"
4109 integrity sha512-numTQtDZMoh78zJpaNdJ9MXb2cv5G3jwUoe3dMQODubZvLoGvTE/Ofp6sHvH8OGKcN/8A47pGLi/k58xHP/Tfw== 4055 integrity sha512-GDKPd+vk4jvSuvCbyuzx/unmXkk090Azec7LovXP8as1Hn8q9p3hbjmDGbUqqhknw0ajwit6LiiWqfiTUPMK7w==
4110 dependencies: 4056 dependencies:
4111 domelementtype "^2.0.1" 4057 domelementtype "^2.0.1"
4112 domhandler "^4.0.0" 4058 domhandler "^4.0.0"
@@ -4167,7 +4113,7 @@ http-proxy-agent@^4.0.1:
4167 agent-base "6" 4113 agent-base "6"
4168 debug "4" 4114 debug "4"
4169 4115
4170http-signature@1.3.5, http-signature@~1.2.0: 4116http-signature@1.3.5:
4171 version "1.3.5" 4117 version "1.3.5"
4172 resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.3.5.tgz#9f19496ffbf3227298d7b5f156e0e1a948678683" 4118 resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.3.5.tgz#9f19496ffbf3227298d7b5f156e0e1a948678683"
4173 integrity sha512-NwoTQYSJoFt34jSBbwzDHDofoA61NGXzu6wXh95o1Ry62EnmKjXb/nR/RknLeZ3G/uGwrlKNY2z7uPt+Cdl7Tw== 4119 integrity sha512-NwoTQYSJoFt34jSBbwzDHDofoA61NGXzu6wXh95o1Ry62EnmKjXb/nR/RknLeZ3G/uGwrlKNY2z7uPt+Cdl7Tw==
@@ -4176,6 +4122,15 @@ http-signature@1.3.5, http-signature@~1.2.0:
4176 jsprim "^1.2.2" 4122 jsprim "^1.2.2"
4177 sshpk "^1.14.1" 4123 sshpk "^1.14.1"
4178 4124
4125http-signature@~1.2.0:
4126 version "1.2.0"
4127 resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
4128 integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=
4129 dependencies:
4130 assert-plus "^1.0.0"
4131 jsprim "^1.2.2"
4132 sshpk "^1.7.0"
4133
4179http2-wrapper@^1.0.0-beta.5.2: 4134http2-wrapper@^1.0.0-beta.5.2:
4180 version "1.0.3" 4135 version "1.0.3"
4181 resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz#b8f55e0c1f25d4ebd08b3b0c2c079f9590800b3d" 4136 resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz#b8f55e0c1f25d4ebd08b3b0c2c079f9590800b3d"
@@ -4245,13 +4200,6 @@ ignore-by-default@^1.0.1:
4245 resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" 4200 resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09"
4246 integrity sha1-SMptcvbGo68Aqa1K5odr44ieKwk= 4201 integrity sha1-SMptcvbGo68Aqa1K5odr44ieKwk=
4247 4202
4248ignore-walk@^3.0.1:
4249 version "3.0.3"
4250 resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.3.tgz#017e2447184bfeade7c238e4aefdd1e8f95b1e37"
4251 integrity sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==
4252 dependencies:
4253 minimatch "^3.0.4"
4254
4255ignore@^4.0.6: 4203ignore@^4.0.6:
4256 version "4.0.6" 4204 version "4.0.6"
4257 resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" 4205 resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
@@ -4446,7 +4394,7 @@ is-buffer@~1.1.6:
4446 resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" 4394 resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
4447 integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== 4395 integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
4448 4396
4449is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.2, is-callable@^1.2.3: 4397is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.3:
4450 version "1.2.3" 4398 version "1.2.3"
4451 resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e" 4399 resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e"
4452 integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ== 4400 integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==
@@ -4542,11 +4490,6 @@ is-installed-globally@^0.3.1:
4542 global-dirs "^2.0.1" 4490 global-dirs "^2.0.1"
4543 is-path-inside "^3.0.1" 4491 is-path-inside "^3.0.1"
4544 4492
4545is-interactive@^1.0.0:
4546 version "1.0.0"
4547 resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e"
4548 integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==
4549
4550is-nan@^1.3.0: 4493is-nan@^1.3.0:
4551 version "1.3.2" 4494 version "1.3.2"
4552 resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d" 4495 resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d"
@@ -4581,9 +4524,9 @@ is-obj@^2.0.0:
4581 integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== 4524 integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==
4582 4525
4583is-path-inside@^3.0.1: 4526is-path-inside@^3.0.1:
4584 version "3.0.2" 4527 version "3.0.3"
4585 resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.2.tgz#f5220fc82a3e233757291dddc9c5877f2a1f3017" 4528 resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
4586 integrity sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg== 4529 integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
4587 4530
4588is-plain-obj@^1.1.0: 4531is-plain-obj@^1.1.0:
4589 version "1.1.0" 4532 version "1.1.0"
@@ -4605,7 +4548,7 @@ is-promise@^2.0.0, is-promise@^2.2.2:
4605 resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" 4548 resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1"
4606 integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== 4549 integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==
4607 4550
4608is-regex@^1.0.3, is-regex@^1.1.1, is-regex@^1.1.2: 4551is-regex@^1.0.3, is-regex@^1.1.2:
4609 version "1.1.2" 4552 version "1.1.2"
4610 resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.2.tgz#81c8ebde4db142f2cf1c53fc86d6a45788266251" 4553 resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.2.tgz#81c8ebde4db142f2cf1c53fc86d6a45788266251"
4611 integrity sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg== 4554 integrity sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg==
@@ -4947,15 +4890,6 @@ libqp@1.1.0:
4947 resolved "https://registry.yarnpkg.com/libqp/-/libqp-1.1.0.tgz#f5e6e06ad74b794fb5b5b66988bf728ef1dedbe8" 4890 resolved "https://registry.yarnpkg.com/libqp/-/libqp-1.1.0.tgz#f5e6e06ad74b794fb5b5b66988bf728ef1dedbe8"
4948 integrity sha1-9ebgatdLeU+1tbZpiL9yjvHe2+g= 4891 integrity sha1-9ebgatdLeU+1tbZpiL9yjvHe2+g=
4949 4892
4950libxmljs@0.19.7:
4951 version "0.19.7"
4952 resolved "https://registry.yarnpkg.com/libxmljs/-/libxmljs-0.19.7.tgz#96c2151b0b73f33dd29917edec82902587004e5a"
4953 integrity sha512-lFJyG9T1mVwTzNTw6ZkvIt0O+NsIR+FTE+RcC2QDFGU8YMnQrnyEOGrj6HWSe1AdwQK7s37BOp4NL+pcAqfK2g==
4954 dependencies:
4955 bindings "~1.3.0"
4956 nan "~2.14.0"
4957 node-pre-gyp "~0.11.0"
4958
4959lines-and-columns@^1.1.6: 4893lines-and-columns@^1.1.6:
4960 version "1.1.6" 4894 version "1.1.6"
4961 resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" 4895 resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
@@ -5063,12 +4997,17 @@ lodash.isequal@^4.5.0:
5063 resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" 4997 resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
5064 integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= 4998 integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
5065 4999
5000lodash@4.17.19:
5001 version "4.17.19"
5002 resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
5003 integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==
5004
5066lodash@4.17.21, lodash@>=4.17.13, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21: 5005lodash@4.17.21, lodash@>=4.17.13, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21:
5067 version "4.17.21" 5006 version "4.17.21"
5068 resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" 5007 resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
5069 integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== 5008 integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
5070 5009
5071log-symbols@4.0.0, log-symbols@^4.0.0: 5010log-symbols@4.0.0:
5072 version "4.0.0" 5011 version "4.0.0"
5073 resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920" 5012 resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920"
5074 integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA== 5013 integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==
@@ -5199,7 +5138,7 @@ mailsplit@5.0.1:
5199 libmime "5.0.0" 5138 libmime "5.0.0"
5200 libqp "1.1.0" 5139 libqp "1.1.0"
5201 5140
5202make-dir@^3.0.0: 5141make-dir@^3.0.0, make-dir@^3.1.0:
5203 version "3.1.0" 5142 version "3.1.0"
5204 resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" 5143 resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
5205 integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== 5144 integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==
@@ -5449,14 +5388,6 @@ minimist@^1.1.0, minimist@^1.2.0, minimist@^1.2.5:
5449 resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" 5388 resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
5450 integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== 5389 integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
5451 5390
5452minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0:
5453 version "2.9.0"
5454 resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6"
5455 integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==
5456 dependencies:
5457 safe-buffer "^5.1.2"
5458 yallist "^3.0.0"
5459
5460minipass@^3.0.0: 5391minipass@^3.0.0:
5461 version "3.1.3" 5392 version "3.1.3"
5462 resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd" 5393 resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd"
@@ -5464,13 +5395,6 @@ minipass@^3.0.0:
5464 dependencies: 5395 dependencies:
5465 yallist "^4.0.0" 5396 yallist "^4.0.0"
5466 5397
5467minizlib@^1.2.1:
5468 version "1.3.3"
5469 resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d"
5470 integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==
5471 dependencies:
5472 minipass "^2.9.0"
5473
5474minizlib@^2.1.1: 5398minizlib@^2.1.1:
5475 version "2.1.2" 5399 version "2.1.2"
5476 resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" 5400 resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
@@ -5484,7 +5408,7 @@ mkdirp-classic@^0.5.2:
5484 resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" 5408 resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
5485 integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== 5409 integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
5486 5410
5487mkdirp@0.x.x, mkdirp@^0.5.0, mkdirp@^0.5.1: 5411mkdirp@0.x.x, mkdirp@^0.5.1:
5488 version "0.5.5" 5412 version "0.5.5"
5489 resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" 5413 resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
5490 integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== 5414 integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
@@ -5496,15 +5420,15 @@ mkdirp@1.0.3:
5496 resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.3.tgz#4cf2e30ad45959dddea53ad97d518b6c8205e1ea" 5420 resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.3.tgz#4cf2e30ad45959dddea53ad97d518b6c8205e1ea"
5497 integrity sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g== 5421 integrity sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g==
5498 5422
5499mkdirp@^1.0.3, mkdirp@^1.0.4, mkdirp@~1.0.4: 5423mkdirp@^1.0.3, mkdirp@~1.0.4:
5500 version "1.0.4" 5424 version "1.0.4"
5501 resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" 5425 resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
5502 integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== 5426 integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
5503 5427
5504mocha@^8.0.1: 5428mocha@^8.0.1:
5505 version "8.3.0" 5429 version "8.3.2"
5506 resolved "https://registry.yarnpkg.com/mocha/-/mocha-8.3.0.tgz#a83a7432d382ae1ca29686062d7fdc2c36f63fe5" 5430 resolved "https://registry.yarnpkg.com/mocha/-/mocha-8.3.2.tgz#53406f195fa86fbdebe71f8b1c6fb23221d69fcc"
5507 integrity sha512-TQqyC89V1J/Vxx0DhJIXlq9gbbL9XFNdeLQ1+JsnZsVaSOV1z3tWfw0qZmQJGQRIfkvZcs7snQnZnOCKoldq1Q== 5431 integrity sha512-UdmISwr/5w+uXLPKspgoV7/RXZwKRTiTjJ2/AC5ZiEztIoOYdfKb19+9jNmEInzx5pBsCyJQzarAxqIGBNYJhg==
5508 dependencies: 5432 dependencies:
5509 "@ungap/promise-all-settled" "1.1.2" 5433 "@ungap/promise-all-settled" "1.1.2"
5510 ansi-colors "4.1.1" 5434 ansi-colors "4.1.1"
@@ -5635,16 +5559,16 @@ mute-stream@0.0.8, mute-stream@~0.0.4:
5635 resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" 5559 resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
5636 integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== 5560 integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==
5637 5561
5638nan@~2.14.0: 5562nanoid@3.1.20:
5639 version "2.14.2"
5640 resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19"
5641 integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==
5642
5643nanoid@3.1.20, nanoid@^3.1.20:
5644 version "3.1.20" 5563 version "3.1.20"
5645 resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788" 5564 resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788"
5646 integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw== 5565 integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==
5647 5566
5567nanoid@^3.1.20:
5568 version "3.1.21"
5569 resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.21.tgz#25bfee7340ac4185866fbfb2c9006d299da1be7f"
5570 integrity sha512-A6oZraK4DJkAOICstsGH98dvycPr/4GGDH7ZWKmMdd3vGcOurZ6JmWFUt0DA5bzrrn2FrUjmv6mFNWvv8jpppA==
5571
5648napi-macros@^2.0.0: 5572napi-macros@^2.0.0:
5649 version "2.0.0" 5573 version "2.0.0"
5650 resolved "https://registry.yarnpkg.com/napi-macros/-/napi-macros-2.0.0.tgz#2b6bae421e7b96eb687aa6c77a7858640670001b" 5574 resolved "https://registry.yarnpkg.com/napi-macros/-/napi-macros-2.0.0.tgz#2b6bae421e7b96eb687aa6c77a7858640670001b"
@@ -5660,15 +5584,6 @@ ncp@1.0.x:
5660 resolved "https://registry.yarnpkg.com/ncp/-/ncp-1.0.1.tgz#d15367e5cb87432ba117d2bf80fdf45aecfb4246" 5584 resolved "https://registry.yarnpkg.com/ncp/-/ncp-1.0.1.tgz#d15367e5cb87432ba117d2bf80fdf45aecfb4246"
5661 integrity sha1-0VNn5cuHQyuhF9K/gP30Wuz7QkY= 5585 integrity sha1-0VNn5cuHQyuhF9K/gP30Wuz7QkY=
5662 5586
5663needle@^2.2.1:
5664 version "2.6.0"
5665 resolved "https://registry.yarnpkg.com/needle/-/needle-2.6.0.tgz#24dbb55f2509e2324b4a99d61f413982013ccdbe"
5666 integrity sha512-KKYdza4heMsEfSWD7VPUIz3zX2XDwOyX2d+geb4vrERZMT5RMU6ujjaD+I5Yr54uZxQ2w6XRTAhHBbSCyovZBg==
5667 dependencies:
5668 debug "^3.2.6"
5669 iconv-lite "^0.4.4"
5670 sax "^1.2.4"
5671
5672negotiator@0.6.2: 5587negotiator@0.6.2:
5673 version "0.6.2" 5588 version "0.6.2"
5674 resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" 5589 resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
@@ -5727,22 +5642,6 @@ node-media-server@^2.1.4:
5727 mkdirp "1.0.3" 5642 mkdirp "1.0.3"
5728 ws "^5.2.2" 5643 ws "^5.2.2"
5729 5644
5730node-pre-gyp@~0.11.0:
5731 version "0.11.0"
5732 resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz#db1f33215272f692cd38f03238e3e9b47c5dd054"
5733 integrity sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==
5734 dependencies:
5735 detect-libc "^1.0.2"
5736 mkdirp "^0.5.1"
5737 needle "^2.2.1"
5738 nopt "^4.0.1"
5739 npm-packlist "^1.1.6"
5740 npmlog "^4.0.2"
5741 rc "^1.2.7"
5742 rimraf "^2.6.1"
5743 semver "^5.3.0"
5744 tar "^4"
5745
5746nodemailer@5.0.0: 5645nodemailer@5.0.0:
5747 version "5.0.0" 5646 version "5.0.0"
5748 resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-5.0.0.tgz#bcb409eca613114e85de42646d0ce7f1fa70b716" 5647 resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-5.0.0.tgz#bcb409eca613114e85de42646d0ce7f1fa70b716"
@@ -5779,14 +5678,6 @@ nodemon@^2.0.1:
5779 undefsafe "^2.0.3" 5678 undefsafe "^2.0.3"
5780 update-notifier "^4.1.0" 5679 update-notifier "^4.1.0"
5781 5680
5782nopt@^4.0.1:
5783 version "4.0.3"
5784 resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.3.tgz#a375cad9d02fd921278d954c2254d5aa57e15e48"
5785 integrity sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==
5786 dependencies:
5787 abbrev "1"
5788 osenv "^0.1.4"
5789
5790nopt@^5.0.0: 5681nopt@^5.0.0:
5791 version "5.0.0" 5682 version "5.0.0"
5792 resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" 5683 resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88"
@@ -5821,27 +5712,6 @@ normalize-url@^4.1.0:
5821 resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129" 5712 resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129"
5822 integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ== 5713 integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==
5823 5714
5824npm-bundled@^1.0.1:
5825 version "1.1.1"
5826 resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.1.1.tgz#1edd570865a94cdb1bc8220775e29466c9fb234b"
5827 integrity sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==
5828 dependencies:
5829 npm-normalize-package-bin "^1.0.1"
5830
5831npm-normalize-package-bin@^1.0.1:
5832 version "1.0.1"
5833 resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz#6e79a41f23fd235c0623218228da7d9c23b8f6e2"
5834 integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==
5835
5836npm-packlist@^1.1.6:
5837 version "1.4.8"
5838 resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.8.tgz#56ee6cc135b9f98ad3d51c1c95da22bbb9b2ef3e"
5839 integrity sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==
5840 dependencies:
5841 ignore-walk "^3.0.1"
5842 npm-bundled "^1.0.1"
5843 npm-normalize-package-bin "^1.0.1"
5844
5845npm-run-path@^2.0.0: 5715npm-run-path@^2.0.0:
5846 version "2.0.2" 5716 version "2.0.2"
5847 resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" 5717 resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
@@ -5856,7 +5726,7 @@ npm-run-path@^4.0.1:
5856 dependencies: 5726 dependencies:
5857 path-key "^3.0.0" 5727 path-key "^3.0.0"
5858 5728
5859npmlog@^4.0.2, npmlog@^4.1.2: 5729npmlog@^4.1.2:
5860 version "4.1.2" 5730 version "4.1.2"
5861 resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" 5731 resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
5862 integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== 5732 integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
@@ -5883,17 +5753,17 @@ oauth-sign@~0.9.0:
5883 resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" 5753 resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
5884 integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== 5754 integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
5885 5755
5886oauth2-server@3.0.0, oauth2-server@3.1.0-beta.1: 5756oauth2-server@3.1.1:
5887 version "3.1.0-beta.1" 5757 version "3.1.1"
5888 resolved "https://registry.yarnpkg.com/oauth2-server/-/oauth2-server-3.1.0-beta.1.tgz#159ee4d32d148c2dc7a39f7b1ce872e039b91a41" 5758 resolved "https://registry.yarnpkg.com/oauth2-server/-/oauth2-server-3.1.1.tgz#be291da840a307a50368736ab766bd68f2eeb3a9"
5889 integrity sha512-FWLl/YC5NGvGzAtclhmlY9fG0nKwDP7xPiPOi5fZ4APO34BmF/vxsEp22spJNuSOrGEsp9W7jKtFCI3UBSvx5w== 5759 integrity sha512-4dv+fE9hrK+xTaCygOLh/kQeFzbFr7UqSyHvBDbrQq8Hg52sAkV2vTsyH3Z42hoeaKpbhM7udhL8Y4GYbl6TGQ==
5890 dependencies: 5760 dependencies:
5891 basic-auth "^2.0.0" 5761 basic-auth "2.0.1"
5892 bluebird "^3.5.1" 5762 bluebird "3.7.2"
5893 lodash "^4.17.10" 5763 lodash "4.17.19"
5894 promisify-any "^2.0.1" 5764 promisify-any "2.0.1"
5895 statuses "^1.5.0" 5765 statuses "1.5.0"
5896 type-is "^1.6.16" 5766 type-is "1.6.18"
5897 5767
5898object-assign@^4, object-assign@^4.1.0, object-assign@^4.1.1: 5768object-assign@^4, object-assign@^4.1.0, object-assign@^4.1.1:
5899 version "4.1.1" 5769 version "4.1.1"
@@ -5910,7 +5780,7 @@ object-hash@2.1.1:
5910 resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.1.1.tgz#9447d0279b4fcf80cff3259bf66a1dc73afabe09" 5780 resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.1.1.tgz#9447d0279b4fcf80cff3259bf66a1dc73afabe09"
5911 integrity sha512-VOJmgmS+7wvXf8CjbQmimtCnEx3IAoLxI3fp2fbWehxrWBcAQFbk+vcwb6vzR0VZv/eNCJ/27j151ZTwqW/JeQ== 5781 integrity sha512-VOJmgmS+7wvXf8CjbQmimtCnEx3IAoLxI3fp2fbWehxrWBcAQFbk+vcwb6vzR0VZv/eNCJ/27j151ZTwqW/JeQ==
5912 5782
5913object-inspect@^1.8.0, object-inspect@^1.9.0: 5783object-inspect@^1.9.0:
5914 version "1.9.0" 5784 version "1.9.0"
5915 resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a" 5785 resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a"
5916 integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw== 5786 integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==
@@ -5920,7 +5790,7 @@ object-keys@^1.0.12, object-keys@^1.1.1:
5920 resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" 5790 resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
5921 integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== 5791 integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
5922 5792
5923object.assign@^4.1.1, object.assign@^4.1.2: 5793object.assign@^4.1.2:
5924 version "4.1.2" 5794 version "4.1.2"
5925 resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" 5795 resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940"
5926 integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== 5796 integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==
@@ -6029,47 +5899,20 @@ optionator@^0.9.1:
6029 type-check "^0.4.0" 5899 type-check "^0.4.0"
6030 word-wrap "^1.2.3" 5900 word-wrap "^1.2.3"
6031 5901
6032ora@^5.1.0: 5902os-tmpdir@^1.0.1, os-tmpdir@~1.0.2:
6033 version "5.3.0"
6034 resolved "https://registry.yarnpkg.com/ora/-/ora-5.3.0.tgz#fb832899d3a1372fe71c8b2c534bbfe74961bb6f"
6035 integrity sha512-zAKMgGXUim0Jyd6CXK9lraBnD3H5yPGBPPOkC23a2BG6hsm4Zu6OQSjQuEtV0BHDf4aKHcUFvJiGRrFuW3MG8g==
6036 dependencies:
6037 bl "^4.0.3"
6038 chalk "^4.1.0"
6039 cli-cursor "^3.1.0"
6040 cli-spinners "^2.5.0"
6041 is-interactive "^1.0.0"
6042 log-symbols "^4.0.0"
6043 strip-ansi "^6.0.0"
6044 wcwidth "^1.0.1"
6045
6046os-homedir@^1.0.0:
6047 version "1.0.2"
6048 resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
6049 integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M=
6050
6051os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2:
6052 version "1.0.2" 5903 version "1.0.2"
6053 resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" 5904 resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
6054 integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= 5905 integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
6055 5906
6056osenv@^0.1.4:
6057 version "0.1.5"
6058 resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410"
6059 integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==
6060 dependencies:
6061 os-homedir "^1.0.0"
6062 os-tmpdir "^1.0.0"
6063
6064p-cancelable@^1.0.0: 5907p-cancelable@^1.0.0:
6065 version "1.1.0" 5908 version "1.1.0"
6066 resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" 5909 resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc"
6067 integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== 5910 integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==
6068 5911
6069p-cancelable@^2.0.0: 5912p-cancelable@^2.0.0:
6070 version "2.0.0" 5913 version "2.1.0"
6071 resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.0.0.tgz#4a3740f5bdaf5ed5d7c3e34882c6fb5d6b266a6e" 5914 resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.0.tgz#4d51c3b91f483d02a0d300765321fca393d758dd"
6072 integrity sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg== 5915 integrity sha512-HAZyB3ZodPo+BDpb4/Iu7Jv4P6cSazBz9ZM0ChhEXp70scx834aWCEjQRwgt41UzzejUAPdbqqONfRWTPYrPAQ==
6073 5916
6074p-finally@^1.0.0: 5917p-finally@^1.0.0:
6075 version "1.0.0" 5918 version "1.0.0"
@@ -6490,11 +6333,11 @@ pngjs@^3.0.0, pngjs@^3.3.3:
6490 integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w== 6333 integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==
6491 6334
6492postcss@^8.0.2: 6335postcss@^8.0.2:
6493 version "8.2.6" 6336 version "8.2.8"
6494 resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.6.tgz#5d69a974543b45f87e464bc4c3e392a97d6be9fe" 6337 resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.8.tgz#0b90f9382efda424c4f0f69a2ead6f6830d08ece"
6495 integrity sha512-xpB8qYxgPuly166AGlpRjUdEYtmOWx2iCwGmrv4vqZL9YPVviDVPZPRXxnXr6xPZOdxQ9lp3ZBFCRgWJ7LE3Sg== 6338 integrity sha512-1F0Xb2T21xET7oQV9eKuctbM9S7BC0fetoHCc4H13z0PT6haiRLP4T0ZY4XWh7iLP0usgqykT6p9B2RtOf4FPw==
6496 dependencies: 6339 dependencies:
6497 colorette "^1.2.1" 6340 colorette "^1.2.2"
6498 nanoid "^3.1.20" 6341 nanoid "^3.1.20"
6499 source-map "^0.6.1" 6342 source-map "^0.6.1"
6500 6343
@@ -6579,7 +6422,7 @@ promise@^7.0.1:
6579 dependencies: 6422 dependencies:
6580 asap "~2.0.3" 6423 asap "~2.0.3"
6581 6424
6582promisify-any@^2.0.1: 6425promisify-any@2.0.1:
6583 version "2.0.1" 6426 version "2.0.1"
6584 resolved "https://registry.yarnpkg.com/promisify-any/-/promisify-any-2.0.1.tgz#403e00a8813f175242ab50fe33a69f8eece47305" 6427 resolved "https://registry.yarnpkg.com/promisify-any/-/promisify-any-2.0.1.tgz#403e00a8813f175242ab50fe33a69f8eece47305"
6585 integrity sha1-QD4AqIE/F1JCq1D+M6afjuzkcwU= 6428 integrity sha1-QD4AqIE/F1JCq1D+M6afjuzkcwU=
@@ -6841,7 +6684,7 @@ raw-body@2.4.0:
6841 iconv-lite "0.4.24" 6684 iconv-lite "0.4.24"
6842 unpipe "1.0.0" 6685 unpipe "1.0.0"
6843 6686
6844rc@^1.2.7, rc@^1.2.8: 6687rc@^1.2.8:
6845 version "1.2.8" 6688 version "1.2.8"
6846 resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" 6689 resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
6847 integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== 6690 integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
@@ -7060,7 +6903,7 @@ render-media@^4.1.0:
7060 stream-to-blob-url "^3.0.2" 6903 stream-to-blob-url "^3.0.2"
7061 videostream "^3.2.2" 6904 videostream "^3.2.2"
7062 6905
7063request@^2.81.0, request@^2.88.0: 6906request@^2.88.0:
7064 version "2.88.2" 6907 version "2.88.2"
7065 resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" 6908 resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
7066 integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== 6909 integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
@@ -7153,10 +6996,10 @@ restore-cursor@^3.1.0:
7153 onetime "^5.1.0" 6996 onetime "^5.1.0"
7154 signal-exit "^3.0.2" 6997 signal-exit "^3.0.2"
7155 6998
7156retimer@^2.0.0: 6999retimer@^3.0.0:
7157 version "2.0.0" 7000 version "3.0.0"
7158 resolved "https://registry.yarnpkg.com/retimer/-/retimer-2.0.0.tgz#e8bd68c5e5a8ec2f49ccb5c636db84c04063bbca" 7001 resolved "https://registry.yarnpkg.com/retimer/-/retimer-3.0.0.tgz#98b751b1feaf1af13eb0228f8ea68b8f9da530df"
7159 integrity sha512-KLXY85WkEq2V2bKex/LOO1ViXVn2KGYe4PYysAdYdjmraYIUsVkXu8O4am+8+5UbaaGl1qho4aqAAPHNQ4GSbg== 7002 integrity sha512-WKE0j11Pa0ZJI5YIk0nflGI7SQsfl2ljihVy7ogh7DeQSeYAUi0ubZ/yEueGtDfUPk6GH5LRw1hBdLq4IwUBWA==
7160 7003
7161retry-as-promised@^3.2.0: 7004retry-as-promised@^3.2.0:
7162 version "3.2.0" 7005 version "3.2.0"
@@ -7175,7 +7018,7 @@ revalidator@0.1.x:
7175 resolved "https://registry.yarnpkg.com/revalidator/-/revalidator-0.1.8.tgz#fece61bfa0c1b52a206bd6b18198184bdd523a3b" 7018 resolved "https://registry.yarnpkg.com/revalidator/-/revalidator-0.1.8.tgz#fece61bfa0c1b52a206bd6b18198184bdd523a3b"
7176 integrity sha1-/s5hv6DBtSoga9axgZgYS91SOjs= 7019 integrity sha1-/s5hv6DBtSoga9axgZgYS91SOjs=
7177 7020
7178rimraf@2.x.x, rimraf@^2.6.1, rimraf@^2.6.3: 7021rimraf@2.x.x, rimraf@^2.6.3:
7179 version "2.7.1" 7022 version "2.7.1"
7180 resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" 7023 resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
7181 integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== 7024 integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
@@ -7544,9 +7387,9 @@ socket.io-client@2.2.0:
7544 to-array "0.1.4" 7387 to-array "0.1.4"
7545 7388
7546socket.io-client@^3.0.2: 7389socket.io-client@^3.0.2:
7547 version "3.1.2" 7390 version "3.1.3"
7548 resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-3.1.2.tgz#77be8c180cef29121970856e8f48e5463631020a" 7391 resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-3.1.3.tgz#57ddcefea58cfab71f0e94c21124de8e3c5aa3e2"
7549 integrity sha512-fXhF8plHrd7U14A7K0JPOmZzpmGkLpIS6623DzrBZqYzI/yvlP4fA3LnxwthEVgiHmn2uJ4KjdnQD8A03PuBWQ== 7392 integrity sha512-4sIGOGOmCg3AOgGi7EEr6ZkTZRkrXwub70bBB/F0JSkMOUFpA77WsL87o34DffQQ31PkbMUIadGOk+3tx1KGbw==
7550 dependencies: 7393 dependencies:
7551 "@types/component-emitter" "^1.2.10" 7394 "@types/component-emitter" "^1.2.10"
7552 backo2 "~1.0.2" 7395 backo2 "~1.0.2"
@@ -7700,7 +7543,7 @@ srt-to-vtt@^1.1.2:
7700 through2 "^0.6.3" 7543 through2 "^0.6.3"
7701 to-utf-8 "^1.2.0" 7544 to-utf-8 "^1.2.0"
7702 7545
7703sshpk@^1.14.1: 7546sshpk@^1.14.1, sshpk@^1.7.0:
7704 version "1.16.1" 7547 version "1.16.1"
7705 resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" 7548 resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
7706 integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== 7549 integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==
@@ -7725,7 +7568,7 @@ standard-as-callback@^2.0.1:
7725 resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.0.1.tgz#ed8bb25648e15831759b6023bdb87e6b60b38126" 7568 resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.0.1.tgz#ed8bb25648e15831759b6023bdb87e6b60b38126"
7726 integrity sha512-NQOxSeB8gOI5WjSaxjBgog2QFw55FV8TkS6Y07BiB3VJ8xNTvUYm0wl0s8ObgQ5NhdpnNfigMIKjgPESzgr4tg== 7569 integrity sha512-NQOxSeB8gOI5WjSaxjBgog2QFw55FV8TkS6Y07BiB3VJ8xNTvUYm0wl0s8ObgQ5NhdpnNfigMIKjgPESzgr4tg==
7727 7570
7728"statuses@>= 1.5.0 < 2", statuses@^1.5.0, statuses@~1.5.0: 7571statuses@1.5.0, "statuses@>= 1.5.0 < 2", statuses@~1.5.0:
7729 version "1.5.0" 7572 version "1.5.0"
7730 resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" 7573 resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
7731 integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= 7574 integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
@@ -7811,7 +7654,7 @@ string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0:
7811 is-fullwidth-code-point "^3.0.0" 7654 is-fullwidth-code-point "^3.0.0"
7812 strip-ansi "^6.0.0" 7655 strip-ansi "^6.0.0"
7813 7656
7814string.prototype.trimend@^1.0.1, string.prototype.trimend@^1.0.4: 7657string.prototype.trimend@^1.0.4:
7815 version "1.0.4" 7658 version "1.0.4"
7816 resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" 7659 resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80"
7817 integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A== 7660 integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==
@@ -7819,7 +7662,7 @@ string.prototype.trimend@^1.0.1, string.prototype.trimend@^1.0.4:
7819 call-bind "^1.0.2" 7662 call-bind "^1.0.2"
7820 define-properties "^1.1.3" 7663 define-properties "^1.1.3"
7821 7664
7822string.prototype.trimstart@^1.0.1, string.prototype.trimstart@^1.0.4: 7665string.prototype.trimstart@^1.0.4:
7823 version "1.0.4" 7666 version "1.0.4"
7824 resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed" 7667 resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed"
7825 integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw== 7668 integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==
@@ -7982,19 +7825,6 @@ table@^6.0.4:
7982 slice-ansi "^4.0.0" 7825 slice-ansi "^4.0.0"
7983 string-width "^4.2.0" 7826 string-width "^4.2.0"
7984 7827
7985tar@^4:
7986 version "4.4.13"
7987 resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525"
7988 integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==
7989 dependencies:
7990 chownr "^1.1.1"
7991 fs-minipass "^1.2.5"
7992 minipass "^2.8.6"
7993 minizlib "^1.2.1"
7994 mkdirp "^0.5.0"
7995 safe-buffer "^5.1.2"
7996 yallist "^3.0.3"
7997
7998tar@^6.1.0: 7828tar@^6.1.0:
7999 version "6.1.0" 7829 version "6.1.0"
8000 resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.0.tgz#d1724e9bcc04b977b18d5c573b333a2207229a83" 7830 resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.0.tgz#d1724e9bcc04b977b18d5c573b333a2207229a83"
@@ -8248,9 +8078,9 @@ tslib@^1.8.1, tslib@^1.9.0:
8248 integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== 8078 integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
8249 8079
8250tsutils@^3.17.1: 8080tsutils@^3.17.1:
8251 version "3.20.0" 8081 version "3.21.0"
8252 resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.20.0.tgz#ea03ea45462e146b53d70ce0893de453ff24f698" 8082 resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
8253 integrity sha512-RYbuQuvkhuqVeXweWT3tJLKOEJ/UUw9GjNEZGWdrLLlM+611o1gwLHBpxoFJKKl25fLprp2eVthtKs5JOrNeXg== 8083 integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==
8254 dependencies: 8084 dependencies:
8255 tslib "^1.8.1" 8085 tslib "^1.8.1"
8256 8086
@@ -8298,7 +8128,7 @@ type-fest@^0.8.1:
8298 resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" 8128 resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
8299 integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== 8129 integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
8300 8130
8301type-is@^1.6.16, type-is@^1.6.4, type-is@~1.6.17, type-is@~1.6.18: 8131type-is@1.6.18, type-is@^1.6.4, type-is@~1.6.17, type-is@~1.6.18:
8302 version "1.6.18" 8132 version "1.6.18"
8303 resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" 8133 resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
8304 integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== 8134 integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
@@ -8312,9 +8142,9 @@ type@^1.0.1:
8312 integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== 8142 integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==
8313 8143
8314type@^2.0.0: 8144type@^2.0.0:
8315 version "2.3.0" 8145 version "2.5.0"
8316 resolved "https://registry.yarnpkg.com/type/-/type-2.3.0.tgz#ada7c045f07ead08abf9e2edd29be1a0c0661132" 8146 resolved "https://registry.yarnpkg.com/type/-/type-2.5.0.tgz#0a2e78c2e77907b252abe5f298c1b01c63f0db3d"
8317 integrity sha512-rgPIqOdfK/4J9FhiVrZ3cveAjRRo5rsQBAIhnylX874y1DX/kEKSVdLsnuHB6l1KTjHyU01VjiMBHgU2adejyg== 8147 integrity sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw==
8318 8148
8319typedarray-to-buffer@^3.0.0, typedarray-to-buffer@^3.1.5: 8149typedarray-to-buffer@^3.0.0, typedarray-to-buffer@^3.1.5:
8320 version "3.1.5" 8150 version "3.1.5"
@@ -8329,9 +8159,9 @@ typedarray@^0.0.6:
8329 integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= 8159 integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
8330 8160
8331typescript@^4.0.5: 8161typescript@^4.0.5:
8332 version "4.2.2" 8162 version "4.2.3"
8333 resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.2.tgz#1450f020618f872db0ea17317d16d8da8ddb8c4c" 8163 resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.3.tgz#39062d8019912d43726298f09493d598048c1ce3"
8334 integrity sha512-tbb+NVrLfnsJy3M59lsDgrzWIflR4d4TIUjz+heUnHZwdF7YsrMTKoRERiIvI2lvBG95dfpLxB21WZhys1bgaQ== 8164 integrity sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==
8335 8165
8336uc.micro@^1.0.1, uc.micro@^1.0.5: 8166uc.micro@^1.0.1, uc.micro@^1.0.5:
8337 version "1.0.6" 8167 version "1.0.6"
@@ -8541,9 +8371,9 @@ uuid@^3.3.2, uuid@^3.4.0:
8541 integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== 8371 integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
8542 8372
8543v8-compile-cache@^2.0.3: 8373v8-compile-cache@^2.0.3:
8544 version "2.2.0" 8374 version "2.3.0"
8545 resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz#9471efa3ef9128d2f7c6a7ca39c4dd6b5055b132" 8375 resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
8546 integrity sha512-gTpR5XQNKFwOd4clxfnhaqvfqMpqEwr4tOtCyz4MtYZX2JYhfr1JvBFKdS+7K/9rfpZR3VLX+YWBbKoxCgS43Q== 8376 integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
8547 8377
8548valid-data-url@^3.0.0: 8378valid-data-url@^3.0.0:
8549 version "3.0.1" 8379 version "3.0.1"
@@ -8604,7 +8434,7 @@ void-elements@^3.1.0:
8604 resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" 8434 resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09"
8605 integrity sha1-YU9/v42AHwu18GYfWy9XhXUOTwk= 8435 integrity sha1-YU9/v42AHwu18GYfWy9XhXUOTwk=
8606 8436
8607wcwidth@>=1.0.1, wcwidth@^1.0.1: 8437wcwidth@>=1.0.1:
8608 version "1.0.1" 8438 version "1.0.1"
8609 resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" 8439 resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"
8610 integrity sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g= 8440 integrity sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=
@@ -8631,9 +8461,9 @@ webfinger.js@^2.6.6:
8631 xhr2 "^0.1.4" 8461 xhr2 "^0.1.4"
8632 8462
8633webtorrent@^0.115.1: 8463webtorrent@^0.115.1:
8634 version "0.115.1" 8464 version "0.115.3"
8635 resolved "https://registry.yarnpkg.com/webtorrent/-/webtorrent-0.115.1.tgz#3984e6b17fdb8ad68b5cdbd42c46a2288e1b3bb2" 8465 resolved "https://registry.yarnpkg.com/webtorrent/-/webtorrent-0.115.3.tgz#2d0a53b65326ffd0124b3592950c4c75e299730a"
8636 integrity sha512-8kq498EMUjYu18wlfoZ42wvz9oUAJrobJbHQGRHl0sbrPVBt17H4FVoAc502XSMCbFzhMx5Vqd7Wz4JTTCPvuQ== 8466 integrity sha512-DNryTNoAHse+zxArBZg25U8B97KNPeVjGzrjRB+oDnGROuKfQcvLh8/9K79FDfQTYVpInMmr9l0ksIsEjz/L2g==
8637 dependencies: 8467 dependencies:
8638 addr-to-ip-port "^1.5.1" 8468 addr-to-ip-port "^1.5.1"
8639 bitfield "^4.0.0" 8469 bitfield "^4.0.0"
@@ -8839,9 +8669,9 @@ ws@^5.2.2:
8839 async-limiter "~1.0.0" 8669 async-limiter "~1.0.0"
8840 8670
8841ws@^7.0.0, ws@^7.3.0, ws@^7.4.2, ws@~7.4.2: 8671ws@^7.0.0, ws@^7.3.0, ws@^7.4.2, ws@~7.4.2:
8842 version "7.4.3" 8672 version "7.4.4"
8843 resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.3.tgz#1f9643de34a543b8edb124bdcbc457ae55a6e5cd" 8673 resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.4.tgz#383bc9742cb202292c9077ceab6f6047b17f2d59"
8844 integrity sha512-hr6vCR76GsossIRsr8OLR9acVVm1jyfEWvhbNjtgPOrfvAlKzvyeg/P6r8RuDjRyrcQoPQT7K0DGEPc7Ae6jzA== 8674 integrity sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==
8845 8675
8846ws@~6.1.0: 8676ws@~6.1.0:
8847 version "6.1.4" 8677 version "6.1.4"
@@ -8918,7 +8748,7 @@ yallist@^2.1.2:
8918 resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" 8748 resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
8919 integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= 8749 integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=
8920 8750
8921yallist@^3.0.0, yallist@^3.0.2, yallist@^3.0.3: 8751yallist@^3.0.2:
8922 version "3.1.1" 8752 version "3.1.1"
8923 resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" 8753 resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
8924 integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== 8754 integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
@@ -8957,9 +8787,9 @@ yargs-parser@^18.1.2:
8957 decamelize "^1.2.0" 8787 decamelize "^1.2.0"
8958 8788
8959yargs-parser@^20.2.2: 8789yargs-parser@^20.2.2:
8960 version "20.2.6" 8790 version "20.2.7"
8961 resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.6.tgz#69f920addf61aafc0b8b89002f5d66e28f2d8b20" 8791 resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.7.tgz#61df85c113edfb5a7a4e36eb8aa60ef423cbc90a"
8962 integrity sha512-AP1+fQIWSM/sMiET8fyayjx/J+JmTPt2Mr0FkrgqB4todtfa53sOsrSAcIrJRD5XS20bKUwaDIuMkWKCEiQLKA== 8792 integrity sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==
8963 8793
8964yargs-unparser@2.0.0: 8794yargs-unparser@2.0.0:
8965 version "2.0.0" 8795 version "2.0.0"