aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib
diff options
context:
space:
mode:
Diffstat (limited to 'server/lib')
-rw-r--r--server/lib/activitypub/actors/refresh.ts2
-rw-r--r--server/lib/activitypub/crawl.ts3
-rw-r--r--server/lib/activitypub/follow.ts17
-rw-r--r--server/lib/activitypub/playlists/refresh.ts2
-rw-r--r--server/lib/activitypub/url.ts12
-rw-r--r--server/lib/activitypub/videos/refresh.ts2
-rw-r--r--server/lib/activitypub/videos/shared/abstract-builder.ts12
-rw-r--r--server/lib/activitypub/videos/shared/object-to-model-attributes.ts13
-rw-r--r--server/lib/client-html.ts15
-rw-r--r--server/lib/emailer.ts440
-rw-r--r--server/lib/hls.ts39
-rw-r--r--server/lib/job-queue/handlers/activitypub-cleaner.ts2
-rw-r--r--server/lib/job-queue/handlers/video-file-import.ts7
-rw-r--r--server/lib/job-queue/handlers/video-import.ts8
-rw-r--r--server/lib/job-queue/handlers/video-live-ending.ts15
-rw-r--r--server/lib/job-queue/handlers/video-transcoding.ts16
-rw-r--r--server/lib/live/live-manager.ts32
-rw-r--r--server/lib/live/shared/muxing-session.ts7
-rw-r--r--server/lib/moderation.ts2
-rw-r--r--server/lib/notifier.ts796
-rw-r--r--server/lib/notifier/index.ts1
-rw-r--r--server/lib/notifier/notifier.ts259
-rw-r--r--server/lib/notifier/shared/abuse/abstract-new-abuse-message.ts67
-rw-r--r--server/lib/notifier/shared/abuse/abuse-state-change-for-reporter.ts74
-rw-r--r--server/lib/notifier/shared/abuse/index.ts4
-rw-r--r--server/lib/notifier/shared/abuse/new-abuse-for-moderators.ts119
-rw-r--r--server/lib/notifier/shared/abuse/new-abuse-message-for-moderators.ts32
-rw-r--r--server/lib/notifier/shared/abuse/new-abuse-message-for-reporter.ts36
-rw-r--r--server/lib/notifier/shared/blacklist/index.ts3
-rw-r--r--server/lib/notifier/shared/blacklist/new-auto-blacklist-for-moderators.ts60
-rw-r--r--server/lib/notifier/shared/blacklist/new-blacklist-for-owner.ts58
-rw-r--r--server/lib/notifier/shared/blacklist/unblacklist-for-owner.ts55
-rw-r--r--server/lib/notifier/shared/comment/comment-mention.ts111
-rw-r--r--server/lib/notifier/shared/comment/index.ts2
-rw-r--r--server/lib/notifier/shared/comment/new-comment-for-video-owner.ts76
-rw-r--r--server/lib/notifier/shared/common/abstract-notification.ts23
-rw-r--r--server/lib/notifier/shared/common/index.ts1
-rw-r--r--server/lib/notifier/shared/follow/auto-follow-for-instance.ts51
-rw-r--r--server/lib/notifier/shared/follow/follow-for-instance.ts68
-rw-r--r--server/lib/notifier/shared/follow/follow-for-user.ts82
-rw-r--r--server/lib/notifier/shared/follow/index.ts3
-rw-r--r--server/lib/notifier/shared/index.ts7
-rw-r--r--server/lib/notifier/shared/instance/index.ts3
-rw-r--r--server/lib/notifier/shared/instance/new-peertube-version-for-admins.ts54
-rw-r--r--server/lib/notifier/shared/instance/new-plugin-version-for-admins.ts58
-rw-r--r--server/lib/notifier/shared/instance/registration-for-moderators.ts49
-rw-r--r--server/lib/notifier/shared/video-publication/abstract-owned-video-publication.ts57
-rw-r--r--server/lib/notifier/shared/video-publication/import-finished-for-owner.ts97
-rw-r--r--server/lib/notifier/shared/video-publication/index.ts5
-rw-r--r--server/lib/notifier/shared/video-publication/new-video-for-subscribers.ts61
-rw-r--r--server/lib/notifier/shared/video-publication/owned-publication-after-auto-unblacklist.ts11
-rw-r--r--server/lib/notifier/shared/video-publication/owned-publication-after-schedule-update.ts10
-rw-r--r--server/lib/notifier/shared/video-publication/owned-publication-after-transcoding.ts9
-rw-r--r--server/lib/plugins/register-helpers.ts222
-rw-r--r--server/lib/plugins/video-constant-manager-factory.ts139
-rw-r--r--server/lib/schedulers/plugins-check-scheduler.ts10
-rw-r--r--server/lib/schedulers/videos-redundancy-scheduler.ts7
-rw-r--r--server/lib/transcoding/video-transcoding.ts81
-rw-r--r--server/lib/video-paths.ts56
-rw-r--r--server/lib/video.ts4
60 files changed, 2002 insertions, 1565 deletions
diff --git a/server/lib/activitypub/actors/refresh.ts b/server/lib/activitypub/actors/refresh.ts
index b2fe3932f..0acaa9f62 100644
--- a/server/lib/activitypub/actors/refresh.ts
+++ b/server/lib/activitypub/actors/refresh.ts
@@ -4,7 +4,7 @@ import { PeerTubeRequestError } from '@server/helpers/requests'
4import { ActorLoadByUrlType } from '@server/lib/model-loaders' 4import { ActorLoadByUrlType } from '@server/lib/model-loaders'
5import { ActorModel } from '@server/models/actor/actor' 5import { ActorModel } from '@server/models/actor/actor'
6import { MActorAccountChannelId, MActorFull } from '@server/types/models' 6import { MActorAccountChannelId, MActorFull } from '@server/types/models'
7import { HttpStatusCode } from '@shared/core-utils' 7import { HttpStatusCode } from '@shared/models'
8import { fetchRemoteActor } from './shared' 8import { fetchRemoteActor } from './shared'
9import { APActorUpdater } from './updater' 9import { APActorUpdater } from './updater'
10import { getUrlFromWebfinger } from './webfinger' 10import { getUrlFromWebfinger } from './webfinger'
diff --git a/server/lib/activitypub/crawl.ts b/server/lib/activitypub/crawl.ts
index cd117f571..28ff5225a 100644
--- a/server/lib/activitypub/crawl.ts
+++ b/server/lib/activitypub/crawl.ts
@@ -1,3 +1,4 @@
1import { retryTransactionWrapper } from '@server/helpers/database-utils'
1import * as Bluebird from 'bluebird' 2import * as Bluebird from 'bluebird'
2import { URL } from 'url' 3import { URL } from 'url'
3import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub' 4import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub'
@@ -51,7 +52,7 @@ async function crawlCollectionPage <T> (argUrl: string, handler: HandlerFunction
51 } 52 }
52 } 53 }
53 54
54 if (cleaner) await cleaner(startDate) 55 if (cleaner) await retryTransactionWrapper(cleaner, startDate)
55} 56}
56 57
57export { 58export {
diff --git a/server/lib/activitypub/follow.ts b/server/lib/activitypub/follow.ts
index c1bd667e0..741b54df5 100644
--- a/server/lib/activitypub/follow.ts
+++ b/server/lib/activitypub/follow.ts
@@ -31,6 +31,21 @@ async function autoFollowBackIfNeeded (actorFollow: MActorFollowActors, transact
31 } 31 }
32} 32}
33 33
34// If we only have an host, use a default account handle
35function getRemoteNameAndHost (handleOrHost: string) {
36 let name = SERVER_ACTOR_NAME
37 let host = handleOrHost
38
39 const splitted = handleOrHost.split('@')
40 if (splitted.length === 2) {
41 name = splitted[0]
42 host = splitted[1]
43 }
44
45 return { name, host }
46}
47
34export { 48export {
35 autoFollowBackIfNeeded 49 autoFollowBackIfNeeded,
50 getRemoteNameAndHost
36} 51}
diff --git a/server/lib/activitypub/playlists/refresh.ts b/server/lib/activitypub/playlists/refresh.ts
index ef3cb3fe4..493e8c7ec 100644
--- a/server/lib/activitypub/playlists/refresh.ts
+++ b/server/lib/activitypub/playlists/refresh.ts
@@ -2,7 +2,7 @@ import { logger, loggerTagsFactory } from '@server/helpers/logger'
2import { PeerTubeRequestError } from '@server/helpers/requests' 2import { PeerTubeRequestError } from '@server/helpers/requests'
3import { JobQueue } from '@server/lib/job-queue' 3import { JobQueue } from '@server/lib/job-queue'
4import { MVideoPlaylist, MVideoPlaylistOwner } from '@server/types/models' 4import { MVideoPlaylist, MVideoPlaylistOwner } from '@server/types/models'
5import { HttpStatusCode } from '@shared/core-utils' 5import { HttpStatusCode } from '@shared/models'
6import { createOrUpdateVideoPlaylist } from './create-update' 6import { createOrUpdateVideoPlaylist } from './create-update'
7import { fetchRemoteVideoPlaylist } from './shared' 7import { fetchRemoteVideoPlaylist } from './shared'
8 8
diff --git a/server/lib/activitypub/url.ts b/server/lib/activitypub/url.ts
index 7816b0be0..338398f2b 100644
--- a/server/lib/activitypub/url.ts
+++ b/server/lib/activitypub/url.ts
@@ -1,5 +1,6 @@
1import { WEBSERVER } from '../../initializers/constants' 1import { WEBSERVER } from '../../initializers/constants'
2import { 2import {
3 MAbuseFull,
3 MAbuseId, 4 MAbuseId,
4 MActor, 5 MActor,
5 MActorFollowActors, 6 MActorFollowActors,
@@ -112,6 +113,14 @@ function getUndoActivityPubUrl (originalUrl: string) {
112 return originalUrl + '/undo' 113 return originalUrl + '/undo'
113} 114}
114 115
116// ---------------------------------------------------------------------------
117
118function getAbuseTargetUrl (abuse: MAbuseFull) {
119 return abuse.VideoAbuse?.Video?.url ||
120 abuse.VideoCommentAbuse?.VideoComment?.url ||
121 abuse.FlaggedAccount.Actor.url
122}
123
115export { 124export {
116 getLocalVideoActivityPubUrl, 125 getLocalVideoActivityPubUrl,
117 getLocalVideoPlaylistActivityPubUrl, 126 getLocalVideoPlaylistActivityPubUrl,
@@ -135,5 +144,6 @@ export {
135 getLocalVideoSharesActivityPubUrl, 144 getLocalVideoSharesActivityPubUrl,
136 getLocalVideoCommentsActivityPubUrl, 145 getLocalVideoCommentsActivityPubUrl,
137 getLocalVideoLikesActivityPubUrl, 146 getLocalVideoLikesActivityPubUrl,
138 getLocalVideoDislikesActivityPubUrl 147 getLocalVideoDislikesActivityPubUrl,
148 getAbuseTargetUrl
139} 149}
diff --git a/server/lib/activitypub/videos/refresh.ts b/server/lib/activitypub/videos/refresh.ts
index a7b82f286..3af08acf4 100644
--- a/server/lib/activitypub/videos/refresh.ts
+++ b/server/lib/activitypub/videos/refresh.ts
@@ -4,7 +4,7 @@ import { ActorFollowScoreCache } from '@server/lib/files-cache'
4import { VideoLoadByUrlType } from '@server/lib/model-loaders' 4import { VideoLoadByUrlType } from '@server/lib/model-loaders'
5import { VideoModel } from '@server/models/video/video' 5import { VideoModel } from '@server/models/video/video'
6import { MVideoAccountLightBlacklistAllFiles, MVideoThumbnail } from '@server/types/models' 6import { MVideoAccountLightBlacklistAllFiles, MVideoThumbnail } from '@server/types/models'
7import { HttpStatusCode } from '@shared/core-utils' 7import { HttpStatusCode } from '@shared/models'
8import { fetchRemoteVideo, SyncParam, syncVideoExternalAttributes } from './shared' 8import { fetchRemoteVideo, SyncParam, syncVideoExternalAttributes } from './shared'
9import { APVideoUpdater } from './updater' 9import { APVideoUpdater } from './updater'
10 10
diff --git a/server/lib/activitypub/videos/shared/abstract-builder.ts b/server/lib/activitypub/videos/shared/abstract-builder.ts
index e89c94bcd..f995fe637 100644
--- a/server/lib/activitypub/videos/shared/abstract-builder.ts
+++ b/server/lib/activitypub/videos/shared/abstract-builder.ts
@@ -1,6 +1,6 @@
1import { Transaction } from 'sequelize/types' 1import { Transaction } from 'sequelize/types'
2import { checkUrlsSameHost } from '@server/helpers/activitypub' 2import { checkUrlsSameHost } from '@server/helpers/activitypub'
3import { deleteNonExistingModels } from '@server/helpers/database-utils' 3import { deleteAllModels, filterNonExistingModels } from '@server/helpers/database-utils'
4import { logger, LoggerTagsFn } from '@server/helpers/logger' 4import { logger, LoggerTagsFn } from '@server/helpers/logger'
5import { updatePlaceholderThumbnail, updateVideoMiniatureFromUrl } from '@server/lib/thumbnail' 5import { updatePlaceholderThumbnail, updateVideoMiniatureFromUrl } from '@server/lib/thumbnail'
6import { setVideoTags } from '@server/lib/video' 6import { setVideoTags } from '@server/lib/video'
@@ -111,8 +111,7 @@ export abstract class APVideoAbstractBuilder {
111 const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a)) 111 const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a))
112 112
113 // Remove video files that do not exist anymore 113 // Remove video files that do not exist anymore
114 const destroyTasks = deleteNonExistingModels(video.VideoFiles || [], newVideoFiles, t) 114 await deleteAllModels(filterNonExistingModels(video.VideoFiles || [], newVideoFiles), t)
115 await Promise.all(destroyTasks)
116 115
117 // Update or add other one 116 // Update or add other one
118 const upsertTasks = newVideoFiles.map(f => VideoFileModel.customUpsert(f, 'video', t)) 117 const upsertTasks = newVideoFiles.map(f => VideoFileModel.customUpsert(f, 'video', t))
@@ -124,13 +123,11 @@ export abstract class APVideoAbstractBuilder {
124 const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a)) 123 const newStreamingPlaylists = streamingPlaylistAttributes.map(a => new VideoStreamingPlaylistModel(a))
125 124
126 // Remove video playlists that do not exist anymore 125 // Remove video playlists that do not exist anymore
127 const destroyTasks = deleteNonExistingModels(video.VideoStreamingPlaylists || [], newStreamingPlaylists, t) 126 await deleteAllModels(filterNonExistingModels(video.VideoStreamingPlaylists || [], newStreamingPlaylists), t)
128 await Promise.all(destroyTasks)
129 127
130 video.VideoStreamingPlaylists = [] 128 video.VideoStreamingPlaylists = []
131 129
132 for (const playlistAttributes of streamingPlaylistAttributes) { 130 for (const playlistAttributes of streamingPlaylistAttributes) {
133
134 const streamingPlaylistModel = await this.insertOrReplaceStreamingPlaylist(playlistAttributes, t) 131 const streamingPlaylistModel = await this.insertOrReplaceStreamingPlaylist(playlistAttributes, t)
135 streamingPlaylistModel.Video = video 132 streamingPlaylistModel.Video = video
136 133
@@ -163,8 +160,7 @@ export abstract class APVideoAbstractBuilder {
163 160
164 const newVideoFiles: MVideoFile[] = getFileAttributesFromUrl(playlistModel, tagObjects).map(a => new VideoFileModel(a)) 161 const newVideoFiles: MVideoFile[] = getFileAttributesFromUrl(playlistModel, tagObjects).map(a => new VideoFileModel(a))
165 162
166 const destroyTasks = deleteNonExistingModels(oldStreamingPlaylistFiles, newVideoFiles, t) 163 await deleteAllModels(filterNonExistingModels(oldStreamingPlaylistFiles, newVideoFiles), t)
167 await Promise.all(destroyTasks)
168 164
169 // Update or add other one 165 // Update or add other one
170 const upsertTasks = newVideoFiles.map(f => VideoFileModel.customUpsert(f, 'streaming-playlist', t)) 166 const upsertTasks = newVideoFiles.map(f => VideoFileModel.customUpsert(f, 'streaming-playlist', t))
diff --git a/server/lib/activitypub/videos/shared/object-to-model-attributes.ts b/server/lib/activitypub/videos/shared/object-to-model-attributes.ts
index 85548428c..1fa16295d 100644
--- a/server/lib/activitypub/videos/shared/object-to-model-attributes.ts
+++ b/server/lib/activitypub/videos/shared/object-to-model-attributes.ts
@@ -7,10 +7,11 @@ import { logger } from '@server/helpers/logger'
7import { getExtFromMimetype } from '@server/helpers/video' 7import { getExtFromMimetype } from '@server/helpers/video'
8import { ACTIVITY_PUB, MIMETYPES, P2P_MEDIA_LOADER_PEER_VERSION, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '@server/initializers/constants' 8import { ACTIVITY_PUB, MIMETYPES, P2P_MEDIA_LOADER_PEER_VERSION, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '@server/initializers/constants'
9import { generateTorrentFileName } from '@server/lib/video-paths' 9import { generateTorrentFileName } from '@server/lib/video-paths'
10import { VideoCaptionModel } from '@server/models/video/video-caption'
10import { VideoFileModel } from '@server/models/video/video-file' 11import { VideoFileModel } from '@server/models/video/video-file'
11import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' 12import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist'
12import { FilteredModelAttributes } from '@server/types' 13import { FilteredModelAttributes } from '@server/types'
13import { MChannelId, MStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoId } from '@server/types/models' 14import { isStreamingPlaylist, MChannelId, MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoId } from '@server/types/models'
14import { 15import {
15 ActivityHashTagObject, 16 ActivityHashTagObject,
16 ActivityMagnetUrlObject, 17 ActivityMagnetUrlObject,
@@ -23,7 +24,6 @@ import {
23 VideoPrivacy, 24 VideoPrivacy,
24 VideoStreamingPlaylistType 25 VideoStreamingPlaylistType
25} from '@shared/models' 26} from '@shared/models'
26import { VideoCaptionModel } from '@server/models/video/video-caption'
27 27
28function getThumbnailFromIcons (videoObject: VideoObject) { 28function getThumbnailFromIcons (videoObject: VideoObject) {
29 let validIcons = videoObject.icon.filter(i => i.width > THUMBNAILS_SIZE.minWidth) 29 let validIcons = videoObject.icon.filter(i => i.width > THUMBNAILS_SIZE.minWidth)
@@ -80,8 +80,8 @@ function getFileAttributesFromUrl (
80 80
81 const extname = getExtFromMimetype(MIMETYPES.VIDEO.MIMETYPE_EXT, fileUrl.mediaType) 81 const extname = getExtFromMimetype(MIMETYPES.VIDEO.MIMETYPE_EXT, fileUrl.mediaType)
82 const resolution = fileUrl.height 82 const resolution = fileUrl.height
83 const videoId = (videoOrPlaylist as MStreamingPlaylist).playlistUrl ? null : videoOrPlaylist.id 83 const videoId = isStreamingPlaylist(videoOrPlaylist) ? null : videoOrPlaylist.id
84 const videoStreamingPlaylistId = (videoOrPlaylist as MStreamingPlaylist).playlistUrl ? videoOrPlaylist.id : null 84 const videoStreamingPlaylistId = isStreamingPlaylist(videoOrPlaylist) ? videoOrPlaylist.id : null
85 85
86 const attribute = { 86 const attribute = {
87 extname, 87 extname,
@@ -130,8 +130,13 @@ function getStreamingPlaylistAttributesFromObject (video: MVideoId, videoObject:
130 130
131 const attribute = { 131 const attribute = {
132 type: VideoStreamingPlaylistType.HLS, 132 type: VideoStreamingPlaylistType.HLS,
133
134 playlistFilename: basename(playlistUrlObject.href),
133 playlistUrl: playlistUrlObject.href, 135 playlistUrl: playlistUrlObject.href,
136
137 segmentsSha256Filename: basename(segmentsSha256UrlObject.href),
134 segmentsSha256Url: segmentsSha256UrlObject.href, 138 segmentsSha256Url: segmentsSha256UrlObject.href,
139
135 p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrlObject.href, files), 140 p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrlObject.href, files),
136 p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION, 141 p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION,
137 videoId: video.id, 142 videoId: video.id,
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts
index 72194416d..e093d35f7 100644
--- a/server/lib/client-html.ts
+++ b/server/lib/client-html.ts
@@ -5,7 +5,7 @@ import validator from 'validator'
5import { escapeHTML } from '@shared/core-utils/renderer' 5import { escapeHTML } from '@shared/core-utils/renderer'
6import { HTMLServerConfig } from '@shared/models' 6import { HTMLServerConfig } from '@shared/models'
7import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/core-utils/i18n/i18n' 7import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/core-utils/i18n/i18n'
8import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' 8import { HttpStatusCode } from '../../shared/models/http/http-error-codes'
9import { VideoPlaylistPrivacy, VideoPrivacy } from '../../shared/models/videos' 9import { VideoPlaylistPrivacy, VideoPrivacy } from '../../shared/models/videos'
10import { isTestInstance, sha256 } from '../helpers/core-utils' 10import { isTestInstance, sha256 } from '../helpers/core-utils'
11import { logger } from '../helpers/logger' 11import { logger } from '../helpers/logger'
@@ -44,6 +44,8 @@ type Tags = {
44 originUrl: string 44 originUrl: string
45 description: string 45 description: string
46 46
47 disallowIndexation?: boolean
48
47 embed?: { 49 embed?: {
48 url: string 50 url: string
49 createdAt: string 51 createdAt: string
@@ -162,7 +164,7 @@ class ClientHtml {
162 let customHtml = ClientHtml.addTitleTag(html, escapeHTML(videoPlaylist.name)) 164 let customHtml = ClientHtml.addTitleTag(html, escapeHTML(videoPlaylist.name))
163 customHtml = ClientHtml.addDescriptionTag(customHtml, mdToPlainText(videoPlaylist.description)) 165 customHtml = ClientHtml.addDescriptionTag(customHtml, mdToPlainText(videoPlaylist.description))
164 166
165 const url = videoPlaylist.getWatchUrl() 167 const url = WEBSERVER.URL + videoPlaylist.getWatchStaticPath()
166 const originUrl = videoPlaylist.url 168 const originUrl = videoPlaylist.url
167 const title = escapeHTML(videoPlaylist.name) 169 const title = escapeHTML(videoPlaylist.name)
168 const siteName = escapeHTML(CONFIG.INSTANCE.NAME) 170 const siteName = escapeHTML(CONFIG.INSTANCE.NAME)
@@ -285,7 +287,8 @@ class ClientHtml {
285 image, 287 image,
286 ogType, 288 ogType,
287 twitterCard, 289 twitterCard,
288 schemaType 290 schemaType,
291 disallowIndexation: !entity.Actor.isOwned()
289 }) 292 })
290 293
291 return customHtml 294 return customHtml
@@ -488,7 +491,7 @@ class ClientHtml {
488 const twitterCardMetaTags = this.generateTwitterCardMetaTags(tagsValues) 491 const twitterCardMetaTags = this.generateTwitterCardMetaTags(tagsValues)
489 const schemaTags = this.generateSchemaTags(tagsValues) 492 const schemaTags = this.generateSchemaTags(tagsValues)
490 493
491 const { url, title, embed, originUrl } = tagsValues 494 const { url, title, embed, originUrl, disallowIndexation } = tagsValues
492 495
493 const oembedLinkTags: { type: string, href: string, title: string }[] = [] 496 const oembedLinkTags: { type: string, href: string, title: string }[] = []
494 497
@@ -536,6 +539,10 @@ class ClientHtml {
536 // SEO, use origin URL 539 // SEO, use origin URL
537 tagsString += `<link rel="canonical" href="${originUrl}" />` 540 tagsString += `<link rel="canonical" href="${originUrl}" />`
538 541
542 if (disallowIndexation) {
543 tagsString += `<meta name="robots" content="noindex" />`
544 }
545
539 return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.META_TAGS, tagsString) 546 return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.META_TAGS, tagsString)
540 } 547 }
541} 548}
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts
index 458214f88..6bb61484b 100644
--- a/server/lib/emailer.ts
+++ b/server/lib/emailer.ts
@@ -1,20 +1,15 @@
1import { readFileSync } from 'fs-extra' 1import { readFileSync } from 'fs-extra'
2import { merge } from 'lodash' 2import { isArray, merge } from 'lodash'
3import { createTransport, Transporter } from 'nodemailer' 3import { createTransport, Transporter } from 'nodemailer'
4import { join } from 'path' 4import { join } from 'path'
5import { VideoChannelModel } from '@server/models/video/video-channel' 5import { EmailPayload } from '@shared/models'
6import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist'
7import { MVideoImport, MVideoImportVideo } from '@server/types/models/video/video-import'
8import { AbuseState, EmailPayload, UserAbuse } from '@shared/models'
9import { SendEmailDefaultOptions } from '../../shared/models/server/emailer.model' 6import { SendEmailDefaultOptions } from '../../shared/models/server/emailer.model'
10import { isTestInstance, root } from '../helpers/core-utils' 7import { isTestInstance, root } from '../helpers/core-utils'
11import { bunyanLogger, logger } from '../helpers/logger' 8import { bunyanLogger, logger } from '../helpers/logger'
12import { CONFIG, isEmailEnabled } from '../initializers/config' 9import { CONFIG, isEmailEnabled } from '../initializers/config'
13import { WEBSERVER } from '../initializers/constants' 10import { WEBSERVER } from '../initializers/constants'
14import { MAbuseFull, MAbuseMessage, MAccountDefault, MActorFollowActors, MActorFollowFull, MPlugin, MUser } from '../types/models' 11import { MUser } from '../types/models'
15import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video'
16import { JobQueue } from './job-queue' 12import { JobQueue } from './job-queue'
17import { toSafeHtml } from '../helpers/markdown'
18 13
19const Email = require('email-templates') 14const Email = require('email-templates')
20 15
@@ -59,429 +54,6 @@ class Emailer {
59 } 54 }
60 } 55 }
61 56
62 addNewVideoFromSubscriberNotification (to: string[], video: MVideoAccountLight) {
63 const channelName = video.VideoChannel.getDisplayName()
64 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
65
66 const emailPayload: EmailPayload = {
67 to,
68 subject: channelName + ' just published a new video',
69 text: `Your subscription ${channelName} just published a new video: "${video.name}".`,
70 locals: {
71 title: 'New content ',
72 action: {
73 text: 'View video',
74 url: videoUrl
75 }
76 }
77 }
78
79 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
80 }
81
82 addNewFollowNotification (to: string[], actorFollow: MActorFollowFull, followType: 'account' | 'channel') {
83 const followingName = (actorFollow.ActorFollowing.VideoChannel || actorFollow.ActorFollowing.Account).getDisplayName()
84
85 const emailPayload: EmailPayload = {
86 template: 'follower-on-channel',
87 to,
88 subject: `New follower on your channel ${followingName}`,
89 locals: {
90 followerName: actorFollow.ActorFollower.Account.getDisplayName(),
91 followerUrl: actorFollow.ActorFollower.url,
92 followingName,
93 followingUrl: actorFollow.ActorFollowing.url,
94 followType
95 }
96 }
97
98 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
99 }
100
101 addNewInstanceFollowerNotification (to: string[], actorFollow: MActorFollowActors) {
102 const awaitingApproval = actorFollow.state === 'pending' ? ' awaiting manual approval.' : ''
103
104 const emailPayload: EmailPayload = {
105 to,
106 subject: 'New instance follower',
107 text: `Your instance has a new follower: ${actorFollow.ActorFollower.url}${awaitingApproval}.`,
108 locals: {
109 title: 'New instance follower',
110 action: {
111 text: 'Review followers',
112 url: WEBSERVER.URL + '/admin/follows/followers-list'
113 }
114 }
115 }
116
117 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
118 }
119
120 addAutoInstanceFollowingNotification (to: string[], actorFollow: MActorFollowActors) {
121 const instanceUrl = actorFollow.ActorFollowing.url
122 const emailPayload: EmailPayload = {
123 to,
124 subject: 'Auto instance following',
125 text: `Your instance automatically followed a new instance: <a href="${instanceUrl}">${instanceUrl}</a>.`
126 }
127
128 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
129 }
130
131 myVideoPublishedNotification (to: string[], video: MVideo) {
132 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
133
134 const emailPayload: EmailPayload = {
135 to,
136 subject: `Your video ${video.name} has been published`,
137 text: `Your video "${video.name}" has been published.`,
138 locals: {
139 title: 'You video is live',
140 action: {
141 text: 'View video',
142 url: videoUrl
143 }
144 }
145 }
146
147 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
148 }
149
150 myVideoImportSuccessNotification (to: string[], videoImport: MVideoImportVideo) {
151 const videoUrl = WEBSERVER.URL + videoImport.Video.getWatchStaticPath()
152
153 const emailPayload: EmailPayload = {
154 to,
155 subject: `Your video import ${videoImport.getTargetIdentifier()} is complete`,
156 text: `Your video "${videoImport.getTargetIdentifier()}" just finished importing.`,
157 locals: {
158 title: 'Import complete',
159 action: {
160 text: 'View video',
161 url: videoUrl
162 }
163 }
164 }
165
166 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
167 }
168
169 myVideoImportErrorNotification (to: string[], videoImport: MVideoImport) {
170 const importUrl = WEBSERVER.URL + '/my-library/video-imports'
171
172 const text =
173 `Your video import "${videoImport.getTargetIdentifier()}" encountered an error.` +
174 '\n\n' +
175 `See your videos import dashboard for more information: <a href="${importUrl}">${importUrl}</a>.`
176
177 const emailPayload: EmailPayload = {
178 to,
179 subject: `Your video import "${videoImport.getTargetIdentifier()}" encountered an error`,
180 text,
181 locals: {
182 title: 'Import failed',
183 action: {
184 text: 'Review imports',
185 url: importUrl
186 }
187 }
188 }
189
190 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
191 }
192
193 addNewCommentOnMyVideoNotification (to: string[], comment: MCommentOwnerVideo) {
194 const video = comment.Video
195 const videoUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath()
196 const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath()
197 const commentHtml = toSafeHtml(comment.text)
198
199 const emailPayload: EmailPayload = {
200 template: 'video-comment-new',
201 to,
202 subject: 'New comment on your video ' + video.name,
203 locals: {
204 accountName: comment.Account.getDisplayName(),
205 accountUrl: comment.Account.Actor.url,
206 comment,
207 commentHtml,
208 video,
209 videoUrl,
210 action: {
211 text: 'View comment',
212 url: commentUrl
213 }
214 }
215 }
216
217 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
218 }
219
220 addNewCommentMentionNotification (to: string[], comment: MCommentOwnerVideo) {
221 const accountName = comment.Account.getDisplayName()
222 const video = comment.Video
223 const videoUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath()
224 const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath()
225 const commentHtml = toSafeHtml(comment.text)
226
227 const emailPayload: EmailPayload = {
228 template: 'video-comment-mention',
229 to,
230 subject: 'Mention on video ' + video.name,
231 locals: {
232 comment,
233 commentHtml,
234 video,
235 videoUrl,
236 accountName,
237 action: {
238 text: 'View comment',
239 url: commentUrl
240 }
241 }
242 }
243
244 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
245 }
246
247 addAbuseModeratorsNotification (to: string[], parameters: {
248 abuse: UserAbuse
249 abuseInstance: MAbuseFull
250 reporter: string
251 }) {
252 const { abuse, abuseInstance, reporter } = parameters
253
254 const action = {
255 text: 'View report #' + abuse.id,
256 url: WEBSERVER.URL + '/admin/moderation/abuses/list?search=%23' + abuse.id
257 }
258
259 let emailPayload: EmailPayload
260
261 if (abuseInstance.VideoAbuse) {
262 const video = abuseInstance.VideoAbuse.Video
263 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
264
265 emailPayload = {
266 template: 'video-abuse-new',
267 to,
268 subject: `New video abuse report from ${reporter}`,
269 locals: {
270 videoUrl,
271 isLocal: video.remote === false,
272 videoCreatedAt: new Date(video.createdAt).toLocaleString(),
273 videoPublishedAt: new Date(video.publishedAt).toLocaleString(),
274 videoName: video.name,
275 reason: abuse.reason,
276 videoChannel: abuse.video.channel,
277 reporter,
278 action
279 }
280 }
281 } else if (abuseInstance.VideoCommentAbuse) {
282 const comment = abuseInstance.VideoCommentAbuse.VideoComment
283 const commentUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath() + ';threadId=' + comment.getThreadId()
284
285 emailPayload = {
286 template: 'video-comment-abuse-new',
287 to,
288 subject: `New comment abuse report from ${reporter}`,
289 locals: {
290 commentUrl,
291 videoName: comment.Video.name,
292 isLocal: comment.isOwned(),
293 commentCreatedAt: new Date(comment.createdAt).toLocaleString(),
294 reason: abuse.reason,
295 flaggedAccount: abuseInstance.FlaggedAccount.getDisplayName(),
296 reporter,
297 action
298 }
299 }
300 } else {
301 const account = abuseInstance.FlaggedAccount
302 const accountUrl = account.getClientUrl()
303
304 emailPayload = {
305 template: 'account-abuse-new',
306 to,
307 subject: `New account abuse report from ${reporter}`,
308 locals: {
309 accountUrl,
310 accountDisplayName: account.getDisplayName(),
311 isLocal: account.isOwned(),
312 reason: abuse.reason,
313 reporter,
314 action
315 }
316 }
317 }
318
319 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
320 }
321
322 addAbuseStateChangeNotification (to: string[], abuse: MAbuseFull) {
323 const text = abuse.state === AbuseState.ACCEPTED
324 ? 'Report #' + abuse.id + ' has been accepted'
325 : 'Report #' + abuse.id + ' has been rejected'
326
327 const abuseUrl = WEBSERVER.URL + '/my-account/abuses?search=%23' + abuse.id
328
329 const action = {
330 text,
331 url: abuseUrl
332 }
333
334 const emailPayload: EmailPayload = {
335 template: 'abuse-state-change',
336 to,
337 subject: text,
338 locals: {
339 action,
340 abuseId: abuse.id,
341 abuseUrl,
342 isAccepted: abuse.state === AbuseState.ACCEPTED
343 }
344 }
345
346 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
347 }
348
349 addAbuseNewMessageNotification (
350 to: string[],
351 options: {
352 target: 'moderator' | 'reporter'
353 abuse: MAbuseFull
354 message: MAbuseMessage
355 accountMessage: MAccountDefault
356 }) {
357 const { abuse, target, message, accountMessage } = options
358
359 const text = 'New message on report #' + abuse.id
360 const abuseUrl = target === 'moderator'
361 ? WEBSERVER.URL + '/admin/moderation/abuses/list?search=%23' + abuse.id
362 : WEBSERVER.URL + '/my-account/abuses?search=%23' + abuse.id
363
364 const action = {
365 text,
366 url: abuseUrl
367 }
368
369 const emailPayload: EmailPayload = {
370 template: 'abuse-new-message',
371 to,
372 subject: text,
373 locals: {
374 abuseId: abuse.id,
375 abuseUrl: action.url,
376 messageAccountName: accountMessage.getDisplayName(),
377 messageText: message.message,
378 action
379 }
380 }
381
382 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
383 }
384
385 async addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) {
386 const videoAutoBlacklistUrl = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list'
387 const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath()
388 const channel = (await VideoChannelModel.loadAndPopulateAccount(videoBlacklist.Video.channelId)).toFormattedSummaryJSON()
389
390 const emailPayload: EmailPayload = {
391 template: 'video-auto-blacklist-new',
392 to,
393 subject: 'A new video is pending moderation',
394 locals: {
395 channel,
396 videoUrl,
397 videoName: videoBlacklist.Video.name,
398 action: {
399 text: 'Review autoblacklist',
400 url: videoAutoBlacklistUrl
401 }
402 }
403 }
404
405 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
406 }
407
408 addNewUserRegistrationNotification (to: string[], user: MUser) {
409 const emailPayload: EmailPayload = {
410 template: 'user-registered',
411 to,
412 subject: `a new user registered on ${CONFIG.INSTANCE.NAME}: ${user.username}`,
413 locals: {
414 user
415 }
416 }
417
418 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
419 }
420
421 addVideoBlacklistNotification (to: string[], videoBlacklist: MVideoBlacklistVideo) {
422 const videoName = videoBlacklist.Video.name
423 const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath()
424
425 const reasonString = videoBlacklist.reason ? ` for the following reason: ${videoBlacklist.reason}` : ''
426 const blockedString = `Your video ${videoName} (${videoUrl} on ${CONFIG.INSTANCE.NAME} has been blacklisted${reasonString}.`
427
428 const emailPayload: EmailPayload = {
429 to,
430 subject: `Video ${videoName} blacklisted`,
431 text: blockedString,
432 locals: {
433 title: 'Your video was blacklisted'
434 }
435 }
436
437 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
438 }
439
440 addVideoUnblacklistNotification (to: string[], video: MVideo) {
441 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
442
443 const emailPayload: EmailPayload = {
444 to,
445 subject: `Video ${video.name} unblacklisted`,
446 text: `Your video "${video.name}" (${videoUrl}) on ${CONFIG.INSTANCE.NAME} has been unblacklisted.`,
447 locals: {
448 title: 'Your video was unblacklisted'
449 }
450 }
451
452 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
453 }
454
455 addNewPeerTubeVersionNotification (to: string[], latestVersion: string) {
456 const emailPayload: EmailPayload = {
457 to,
458 template: 'peertube-version-new',
459 subject: `A new PeerTube version is available: ${latestVersion}`,
460 locals: {
461 latestVersion
462 }
463 }
464
465 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
466 }
467
468 addNewPlugionVersionNotification (to: string[], plugin: MPlugin) {
469 const pluginUrl = WEBSERVER.URL + '/admin/plugins/list-installed?pluginType=' + plugin.type
470
471 const emailPayload: EmailPayload = {
472 to,
473 template: 'plugin-version-new',
474 subject: `A new plugin/theme version is available: ${plugin.name}@${plugin.latestVersion}`,
475 locals: {
476 pluginName: plugin.name,
477 latestVersion: plugin.latestVersion,
478 pluginUrl
479 }
480 }
481
482 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
483 }
484
485 addPasswordResetEmailJob (username: string, to: string, resetPasswordUrl: string) { 57 addPasswordResetEmailJob (username: string, to: string, resetPasswordUrl: string) {
486 const emailPayload: EmailPayload = { 58 const emailPayload: EmailPayload = {
487 template: 'password-reset', 59 template: 'password-reset',
@@ -578,7 +150,11 @@ class Emailer {
578 subjectPrefix: CONFIG.EMAIL.SUBJECT.PREFIX 150 subjectPrefix: CONFIG.EMAIL.SUBJECT.PREFIX
579 }) 151 })
580 152
581 for (const to of options.to) { 153 const toEmails = isArray(options.to)
154 ? options.to
155 : [ options.to ]
156
157 for (const to of toEmails) {
582 const baseOptions: SendEmailDefaultOptions = { 158 const baseOptions: SendEmailDefaultOptions = {
583 template: 'common', 159 template: 'common',
584 message: { 160 message: {
diff --git a/server/lib/hls.ts b/server/lib/hls.ts
index 05be403f3..32b02bc26 100644
--- a/server/lib/hls.ts
+++ b/server/lib/hls.ts
@@ -1,7 +1,7 @@
1import { close, ensureDir, move, open, outputJSON, pathExists, read, readFile, remove, writeFile } from 'fs-extra' 1import { close, ensureDir, move, open, outputJSON, pathExists, read, readFile, remove, writeFile } from 'fs-extra'
2import { flatten, uniq } from 'lodash' 2import { flatten, uniq } from 'lodash'
3import { basename, dirname, join } from 'path' 3import { basename, dirname, join } from 'path'
4import { MVideoWithFile } from '@server/types/models' 4import { MStreamingPlaylistFilesVideo, MVideoWithFile } from '@server/types/models'
5import { sha256 } from '../helpers/core-utils' 5import { sha256 } from '../helpers/core-utils'
6import { getAudioStreamCodec, getVideoStreamCodec, getVideoStreamSize } from '../helpers/ffprobe-utils' 6import { getAudioStreamCodec, getVideoStreamCodec, getVideoStreamSize } from '../helpers/ffprobe-utils'
7import { logger } from '../helpers/logger' 7import { logger } from '../helpers/logger'
@@ -12,7 +12,7 @@ import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION } from
12import { sequelizeTypescript } from '../initializers/database' 12import { sequelizeTypescript } from '../initializers/database'
13import { VideoFileModel } from '../models/video/video-file' 13import { VideoFileModel } from '../models/video/video-file'
14import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' 14import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
15import { getVideoFilePath } from './video-paths' 15import { getHlsResolutionPlaylistFilename, getVideoFilePath } from './video-paths'
16 16
17async function updateStreamingPlaylistsInfohashesIfNeeded () { 17async function updateStreamingPlaylistsInfohashesIfNeeded () {
18 const playlistsToUpdate = await VideoStreamingPlaylistModel.listByIncorrectPeerVersion() 18 const playlistsToUpdate = await VideoStreamingPlaylistModel.listByIncorrectPeerVersion()
@@ -22,25 +22,29 @@ async function updateStreamingPlaylistsInfohashesIfNeeded () {
22 await sequelizeTypescript.transaction(async t => { 22 await sequelizeTypescript.transaction(async t => {
23 const videoFiles = await VideoFileModel.listByStreamingPlaylist(playlist.id, t) 23 const videoFiles = await VideoFileModel.listByStreamingPlaylist(playlist.id, t)
24 24
25 playlist.p2pMediaLoaderInfohashes = VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlist.playlistUrl, videoFiles) 25 playlist.assignP2PMediaLoaderInfoHashes(playlist.Video, videoFiles)
26 playlist.p2pMediaLoaderPeerVersion = P2P_MEDIA_LOADER_PEER_VERSION 26 playlist.p2pMediaLoaderPeerVersion = P2P_MEDIA_LOADER_PEER_VERSION
27
27 await playlist.save({ transaction: t }) 28 await playlist.save({ transaction: t })
28 }) 29 })
29 } 30 }
30} 31}
31 32
32async function updateMasterHLSPlaylist (video: MVideoWithFile) { 33async function updateMasterHLSPlaylist (video: MVideoWithFile, playlist: MStreamingPlaylistFilesVideo) {
33 const directory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid) 34 const directory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)
35
34 const masterPlaylists: string[] = [ '#EXTM3U', '#EXT-X-VERSION:3' ] 36 const masterPlaylists: string[] = [ '#EXTM3U', '#EXT-X-VERSION:3' ]
35 const masterPlaylistPath = join(directory, VideoStreamingPlaylistModel.getMasterHlsPlaylistFilename())
36 const streamingPlaylist = video.getHLSPlaylist()
37 37
38 for (const file of streamingPlaylist.VideoFiles) { 38 const masterPlaylistPath = join(directory, playlist.playlistFilename)
39
40 for (const file of playlist.VideoFiles) {
41 const playlistFilename = getHlsResolutionPlaylistFilename(file.filename)
42
39 // If we did not generated a playlist for this resolution, skip 43 // If we did not generated a playlist for this resolution, skip
40 const filePlaylistPath = join(directory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(file.resolution)) 44 const filePlaylistPath = join(directory, playlistFilename)
41 if (await pathExists(filePlaylistPath) === false) continue 45 if (await pathExists(filePlaylistPath) === false) continue
42 46
43 const videoFilePath = getVideoFilePath(streamingPlaylist, file) 47 const videoFilePath = getVideoFilePath(playlist, file)
44 48
45 const size = await getVideoStreamSize(videoFilePath) 49 const size = await getVideoStreamSize(videoFilePath)
46 50
@@ -58,29 +62,28 @@ async function updateMasterHLSPlaylist (video: MVideoWithFile) {
58 line += `,CODECS="${codecs.filter(c => !!c).join(',')}"` 62 line += `,CODECS="${codecs.filter(c => !!c).join(',')}"`
59 63
60 masterPlaylists.push(line) 64 masterPlaylists.push(line)
61 masterPlaylists.push(VideoStreamingPlaylistModel.getHlsPlaylistFilename(file.resolution)) 65 masterPlaylists.push(playlistFilename)
62 } 66 }
63 67
64 await writeFile(masterPlaylistPath, masterPlaylists.join('\n') + '\n') 68 await writeFile(masterPlaylistPath, masterPlaylists.join('\n') + '\n')
65} 69}
66 70
67async function updateSha256VODSegments (video: MVideoWithFile) { 71async function updateSha256VODSegments (video: MVideoWithFile, playlist: MStreamingPlaylistFilesVideo) {
68 const json: { [filename: string]: { [range: string]: string } } = {} 72 const json: { [filename: string]: { [range: string]: string } } = {}
69 73
70 const playlistDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid) 74 const playlistDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)
71 const hlsPlaylist = video.getHLSPlaylist()
72 75
73 // For all the resolutions available for this video 76 // For all the resolutions available for this video
74 for (const file of hlsPlaylist.VideoFiles) { 77 for (const file of playlist.VideoFiles) {
75 const rangeHashes: { [range: string]: string } = {} 78 const rangeHashes: { [range: string]: string } = {}
76 79
77 const videoPath = getVideoFilePath(hlsPlaylist, file) 80 const videoPath = getVideoFilePath(playlist, file)
78 const playlistPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(file.resolution)) 81 const resolutionPlaylistPath = join(playlistDirectory, getHlsResolutionPlaylistFilename(file.filename))
79 82
80 // Maybe the playlist is not generated for this resolution yet 83 // Maybe the playlist is not generated for this resolution yet
81 if (!await pathExists(playlistPath)) continue 84 if (!await pathExists(resolutionPlaylistPath)) continue
82 85
83 const playlistContent = await readFile(playlistPath) 86 const playlistContent = await readFile(resolutionPlaylistPath)
84 const ranges = getRangesFromPlaylist(playlistContent.toString()) 87 const ranges = getRangesFromPlaylist(playlistContent.toString())
85 88
86 const fd = await open(videoPath, 'r') 89 const fd = await open(videoPath, 'r')
@@ -96,7 +99,7 @@ async function updateSha256VODSegments (video: MVideoWithFile) {
96 json[videoFilename] = rangeHashes 99 json[videoFilename] = rangeHashes
97 } 100 }
98 101
99 const outputPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsSha256SegmentsFilename()) 102 const outputPath = join(playlistDirectory, playlist.segmentsSha256Filename)
100 await outputJSON(outputPath, json) 103 await outputJSON(outputPath, json)
101} 104}
102 105
diff --git a/server/lib/job-queue/handlers/activitypub-cleaner.ts b/server/lib/job-queue/handlers/activitypub-cleaner.ts
index 1caca1dcc..56e2b0ceb 100644
--- a/server/lib/job-queue/handlers/activitypub-cleaner.ts
+++ b/server/lib/job-queue/handlers/activitypub-cleaner.ts
@@ -12,7 +12,7 @@ import { AP_CLEANER_CONCURRENCY } from '@server/initializers/constants'
12import { VideoModel } from '@server/models/video/video' 12import { VideoModel } from '@server/models/video/video'
13import { VideoCommentModel } from '@server/models/video/video-comment' 13import { VideoCommentModel } from '@server/models/video/video-comment'
14import { VideoShareModel } from '@server/models/video/video-share' 14import { VideoShareModel } from '@server/models/video/video-share'
15import { HttpStatusCode } from '@shared/core-utils' 15import { HttpStatusCode } from '@shared/models'
16import { logger } from '../../../helpers/logger' 16import { logger } from '../../../helpers/logger'
17import { AccountVideoRateModel } from '../../../models/account/account-video-rate' 17import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
18 18
diff --git a/server/lib/job-queue/handlers/video-file-import.ts b/server/lib/job-queue/handlers/video-file-import.ts
index 187cb652e..4d199f247 100644
--- a/server/lib/job-queue/handlers/video-file-import.ts
+++ b/server/lib/job-queue/handlers/video-file-import.ts
@@ -2,7 +2,7 @@ import * as Bull from 'bull'
2import { copy, stat } from 'fs-extra' 2import { copy, stat } from 'fs-extra'
3import { getLowercaseExtension } from '@server/helpers/core-utils' 3import { getLowercaseExtension } from '@server/helpers/core-utils'
4import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' 4import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
5import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths' 5import { generateWebTorrentVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
6import { UserModel } from '@server/models/user/user' 6import { UserModel } from '@server/models/user/user'
7import { MVideoFullLight } from '@server/types/models' 7import { MVideoFullLight } from '@server/types/models'
8import { VideoFileImportPayload } from '@shared/models' 8import { VideoFileImportPayload } from '@shared/models'
@@ -61,8 +61,7 @@ async function updateVideoFile (video: MVideoFullLight, inputFilePath: string) {
61 61
62 if (currentVideoFile) { 62 if (currentVideoFile) {
63 // Remove old file and old torrent 63 // Remove old file and old torrent
64 await video.removeFile(currentVideoFile) 64 await video.removeFileAndTorrent(currentVideoFile)
65 await currentVideoFile.removeTorrent()
66 // Remove the old video file from the array 65 // Remove the old video file from the array
67 video.VideoFiles = video.VideoFiles.filter(f => f !== currentVideoFile) 66 video.VideoFiles = video.VideoFiles.filter(f => f !== currentVideoFile)
68 67
@@ -72,7 +71,7 @@ async function updateVideoFile (video: MVideoFullLight, inputFilePath: string) {
72 const newVideoFile = new VideoFileModel({ 71 const newVideoFile = new VideoFileModel({
73 resolution: videoFileResolution, 72 resolution: videoFileResolution,
74 extname: fileExt, 73 extname: fileExt,
75 filename: generateVideoFilename(video, false, videoFileResolution, fileExt), 74 filename: generateWebTorrentVideoFilename(videoFileResolution, fileExt),
76 size, 75 size,
77 fps, 76 fps,
78 videoId: video.id 77 videoId: video.id
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts
index 55498003d..5fd2039b1 100644
--- a/server/lib/job-queue/handlers/video-import.ts
+++ b/server/lib/job-queue/handlers/video-import.ts
@@ -8,7 +8,7 @@ import { Hooks } from '@server/lib/plugins/hooks'
8import { ServerConfigManager } from '@server/lib/server-config-manager' 8import { ServerConfigManager } from '@server/lib/server-config-manager'
9import { isAbleToUploadVideo } from '@server/lib/user' 9import { isAbleToUploadVideo } from '@server/lib/user'
10import { addOptimizeOrMergeAudioJob } from '@server/lib/video' 10import { addOptimizeOrMergeAudioJob } from '@server/lib/video'
11import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths' 11import { generateWebTorrentVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
12import { ThumbnailModel } from '@server/models/video/thumbnail' 12import { ThumbnailModel } from '@server/models/video/thumbnail'
13import { MVideoImportDefault, MVideoImportDefaultFiles, MVideoImportVideo } from '@server/types/models/video/video-import' 13import { MVideoImportDefault, MVideoImportDefaultFiles, MVideoImportVideo } from '@server/types/models/video/video-import'
14import { 14import {
@@ -124,7 +124,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
124 extname: fileExt, 124 extname: fileExt,
125 resolution: videoFileResolution, 125 resolution: videoFileResolution,
126 size: stats.size, 126 size: stats.size,
127 filename: generateVideoFilename(videoImport.Video, false, videoFileResolution, fileExt), 127 filename: generateWebTorrentVideoFilename(videoFileResolution, fileExt),
128 fps, 128 fps,
129 videoId: videoImport.videoId 129 videoId: videoImport.videoId
130 } 130 }
@@ -235,7 +235,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
235 }) 235 })
236 }) 236 })
237 237
238 Notifier.Instance.notifyOnFinishedVideoImport(videoImportUpdated, true) 238 Notifier.Instance.notifyOnFinishedVideoImport({ videoImport: videoImportUpdated, success: true })
239 239
240 if (video.isBlacklisted()) { 240 if (video.isBlacklisted()) {
241 const videoBlacklist = Object.assign(video.VideoBlacklist, { Video: video }) 241 const videoBlacklist = Object.assign(video.VideoBlacklist, { Video: video })
@@ -263,7 +263,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
263 } 263 }
264 await videoImport.save() 264 await videoImport.save()
265 265
266 Notifier.Instance.notifyOnFinishedVideoImport(videoImport, false) 266 Notifier.Instance.notifyOnFinishedVideoImport({ videoImport, success: false })
267 267
268 throw err 268 throw err
269 } 269 }
diff --git a/server/lib/job-queue/handlers/video-live-ending.ts b/server/lib/job-queue/handlers/video-live-ending.ts
index 9eba41bf8..386ccdc7b 100644
--- a/server/lib/job-queue/handlers/video-live-ending.ts
+++ b/server/lib/job-queue/handlers/video-live-ending.ts
@@ -7,12 +7,12 @@ import { buildConcatenatedName, cleanupLive, LiveSegmentShaStore } from '@server
7import { generateVideoMiniature } from '@server/lib/thumbnail' 7import { generateVideoMiniature } from '@server/lib/thumbnail'
8import { generateHlsPlaylistResolutionFromTS } from '@server/lib/transcoding/video-transcoding' 8import { generateHlsPlaylistResolutionFromTS } from '@server/lib/transcoding/video-transcoding'
9import { publishAndFederateIfNeeded } from '@server/lib/video' 9import { publishAndFederateIfNeeded } from '@server/lib/video'
10import { getHLSDirectory } from '@server/lib/video-paths' 10import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename, getHLSDirectory } from '@server/lib/video-paths'
11import { VideoModel } from '@server/models/video/video' 11import { VideoModel } from '@server/models/video/video'
12import { VideoFileModel } from '@server/models/video/video-file' 12import { VideoFileModel } from '@server/models/video/video-file'
13import { VideoLiveModel } from '@server/models/video/video-live' 13import { VideoLiveModel } from '@server/models/video/video-live'
14import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' 14import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist'
15import { MVideo, MVideoLive } from '@server/types/models' 15import { MStreamingPlaylist, MVideo, MVideoLive } from '@server/types/models'
16import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models' 16import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models'
17import { logger } from '../../../helpers/logger' 17import { logger } from '../../../helpers/logger'
18 18
@@ -43,7 +43,7 @@ async function processVideoLiveEnding (job: Bull.Job) {
43 return cleanupLive(video, streamingPlaylist) 43 return cleanupLive(video, streamingPlaylist)
44 } 44 }
45 45
46 return saveLive(video, live) 46 return saveLive(video, live, streamingPlaylist)
47} 47}
48 48
49// --------------------------------------------------------------------------- 49// ---------------------------------------------------------------------------
@@ -54,14 +54,14 @@ export {
54 54
55// --------------------------------------------------------------------------- 55// ---------------------------------------------------------------------------
56 56
57async function saveLive (video: MVideo, live: MVideoLive) { 57async function saveLive (video: MVideo, live: MVideoLive, streamingPlaylist: MStreamingPlaylist) {
58 const hlsDirectory = getHLSDirectory(video, false) 58 const hlsDirectory = getHLSDirectory(video, false)
59 const replayDirectory = join(hlsDirectory, VIDEO_LIVE.REPLAY_DIRECTORY) 59 const replayDirectory = join(hlsDirectory, VIDEO_LIVE.REPLAY_DIRECTORY)
60 60
61 const rootFiles = await readdir(hlsDirectory) 61 const rootFiles = await readdir(hlsDirectory)
62 62
63 const playlistFiles = rootFiles.filter(file => { 63 const playlistFiles = rootFiles.filter(file => {
64 return file.endsWith('.m3u8') && file !== 'master.m3u8' 64 return file.endsWith('.m3u8') && file !== streamingPlaylist.playlistFilename
65 }) 65 })
66 66
67 await cleanupLiveFiles(hlsDirectory) 67 await cleanupLiveFiles(hlsDirectory)
@@ -80,7 +80,12 @@ async function saveLive (video: MVideo, live: MVideoLive) {
80 80
81 const hlsPlaylist = videoWithFiles.getHLSPlaylist() 81 const hlsPlaylist = videoWithFiles.getHLSPlaylist()
82 await VideoFileModel.removeHLSFilesOfVideoId(hlsPlaylist.id) 82 await VideoFileModel.removeHLSFilesOfVideoId(hlsPlaylist.id)
83
84 // Reset playlist
83 hlsPlaylist.VideoFiles = [] 85 hlsPlaylist.VideoFiles = []
86 hlsPlaylist.playlistFilename = generateHLSMasterPlaylistFilename()
87 hlsPlaylist.segmentsSha256Filename = generateHlsSha256SegmentsFilename()
88 await hlsPlaylist.save()
84 89
85 let durationDone = false 90 let durationDone = false
86 91
diff --git a/server/lib/job-queue/handlers/video-transcoding.ts b/server/lib/job-queue/handlers/video-transcoding.ts
index f5ba6f435..2abb351ce 100644
--- a/server/lib/job-queue/handlers/video-transcoding.ts
+++ b/server/lib/job-queue/handlers/video-transcoding.ts
@@ -28,21 +28,10 @@ import { JobQueue } from '../job-queue'
28 28
29type HandlerFunction = (job: Bull.Job, payload: VideoTranscodingPayload, video: MVideoFullLight, user: MUser) => Promise<any> 29type HandlerFunction = (job: Bull.Job, payload: VideoTranscodingPayload, video: MVideoFullLight, user: MUser) => Promise<any>
30 30
31const handlers: { [ id: string ]: HandlerFunction } = { 31const handlers: { [ id in VideoTranscodingPayload['type'] ]: HandlerFunction } = {
32 // Deprecated, introduced in 3.1
33 'hls': handleHLSJob,
34 'new-resolution-to-hls': handleHLSJob, 32 'new-resolution-to-hls': handleHLSJob,
35
36 // Deprecated, introduced in 3.1
37 'new-resolution': handleNewWebTorrentResolutionJob,
38 'new-resolution-to-webtorrent': handleNewWebTorrentResolutionJob, 33 'new-resolution-to-webtorrent': handleNewWebTorrentResolutionJob,
39
40 // Deprecated, introduced in 3.1
41 'merge-audio': handleWebTorrentMergeAudioJob,
42 'merge-audio-to-webtorrent': handleWebTorrentMergeAudioJob, 34 'merge-audio-to-webtorrent': handleWebTorrentMergeAudioJob,
43
44 // Deprecated, introduced in 3.1
45 'optimize': handleWebTorrentOptimizeJob,
46 'optimize-to-webtorrent': handleWebTorrentOptimizeJob 35 'optimize-to-webtorrent': handleWebTorrentOptimizeJob
47} 36}
48 37
@@ -125,8 +114,7 @@ async function onHlsPlaylistGeneration (video: MVideoFullLight, user: MUser, pay
125 if (payload.isMaxQuality && CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false) { 114 if (payload.isMaxQuality && CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false) {
126 // Remove webtorrent files if not enabled 115 // Remove webtorrent files if not enabled
127 for (const file of video.VideoFiles) { 116 for (const file of video.VideoFiles) {
128 await video.removeFile(file) 117 await video.removeFileAndTorrent(file)
129 await file.removeTorrent()
130 await file.destroy() 118 await file.destroy()
131 } 119 }
132 120
diff --git a/server/lib/live/live-manager.ts b/server/lib/live/live-manager.ts
index 014cd3fcf..f106d69fb 100644
--- a/server/lib/live/live-manager.ts
+++ b/server/lib/live/live-manager.ts
@@ -4,24 +4,25 @@ import { isTestInstance } from '@server/helpers/core-utils'
4import { computeResolutionsToTranscode, getVideoFileFPS, getVideoFileResolution } from '@server/helpers/ffprobe-utils' 4import { computeResolutionsToTranscode, getVideoFileFPS, getVideoFileResolution } from '@server/helpers/ffprobe-utils'
5import { logger, loggerTagsFactory } from '@server/helpers/logger' 5import { logger, loggerTagsFactory } from '@server/helpers/logger'
6import { CONFIG, registerConfigChangedHandler } from '@server/initializers/config' 6import { CONFIG, registerConfigChangedHandler } from '@server/initializers/config'
7import { P2P_MEDIA_LOADER_PEER_VERSION, VIDEO_LIVE, VIEW_LIFETIME, WEBSERVER } from '@server/initializers/constants' 7import { P2P_MEDIA_LOADER_PEER_VERSION, VIDEO_LIVE, VIEW_LIFETIME } from '@server/initializers/constants'
8import { UserModel } from '@server/models/user/user' 8import { UserModel } from '@server/models/user/user'
9import { VideoModel } from '@server/models/video/video' 9import { VideoModel } from '@server/models/video/video'
10import { VideoLiveModel } from '@server/models/video/video-live' 10import { VideoLiveModel } from '@server/models/video/video-live'
11import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' 11import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist'
12import { MStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoLiveVideo } from '@server/types/models' 12import { MStreamingPlaylistVideo, MVideo, MVideoLiveVideo } from '@server/types/models'
13import { VideoState, VideoStreamingPlaylistType } from '@shared/models' 13import { VideoState, VideoStreamingPlaylistType } from '@shared/models'
14import { federateVideoIfNeeded } from '../activitypub/videos' 14import { federateVideoIfNeeded } from '../activitypub/videos'
15import { JobQueue } from '../job-queue' 15import { JobQueue } from '../job-queue'
16import { PeerTubeSocket } from '../peertube-socket' 16import { PeerTubeSocket } from '../peertube-socket'
17import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename } from '../video-paths'
17import { LiveQuotaStore } from './live-quota-store' 18import { LiveQuotaStore } from './live-quota-store'
18import { LiveSegmentShaStore } from './live-segment-sha-store' 19import { LiveSegmentShaStore } from './live-segment-sha-store'
19import { cleanupLive } from './live-utils' 20import { cleanupLive } from './live-utils'
20import { MuxingSession } from './shared' 21import { MuxingSession } from './shared'
21 22
22const NodeRtmpSession = require('node-media-server/node_rtmp_session') 23const NodeRtmpSession = require('node-media-server/src/node_rtmp_session')
23const context = require('node-media-server/node_core_ctx') 24const context = require('node-media-server/src/node_core_ctx')
24const nodeMediaServerLogger = require('node-media-server/node_core_logger') 25const nodeMediaServerLogger = require('node-media-server/src/node_core_logger')
25 26
26// Disable node media server logs 27// Disable node media server logs
27nodeMediaServerLogger.setLogType(0) 28nodeMediaServerLogger.setLogType(0)
@@ -392,19 +393,18 @@ class LiveManager {
392 return resolutionsEnabled.concat([ originResolution ]) 393 return resolutionsEnabled.concat([ originResolution ])
393 } 394 }
394 395
395 private async createLivePlaylist (video: MVideo, allResolutions: number[]) { 396 private async createLivePlaylist (video: MVideo, allResolutions: number[]): Promise<MStreamingPlaylistVideo> {
396 const playlistUrl = WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsMasterPlaylistStaticPath(video.uuid) 397 const playlist = await VideoStreamingPlaylistModel.loadOrGenerate(video)
397 const [ videoStreamingPlaylist ] = await VideoStreamingPlaylistModel.upsert({
398 videoId: video.id,
399 playlistUrl,
400 segmentsSha256Url: WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsSha256SegmentsStaticPath(video.uuid, video.isLive),
401 p2pMediaLoaderInfohashes: VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(playlistUrl, allResolutions),
402 p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION,
403 398
404 type: VideoStreamingPlaylistType.HLS 399 playlist.playlistFilename = generateHLSMasterPlaylistFilename(true)
405 }, { returning: true }) as [ MStreamingPlaylist, boolean ] 400 playlist.segmentsSha256Filename = generateHlsSha256SegmentsFilename(true)
406 401
407 return Object.assign(videoStreamingPlaylist, { Video: video }) 402 playlist.p2pMediaLoaderPeerVersion = P2P_MEDIA_LOADER_PEER_VERSION
403 playlist.type = VideoStreamingPlaylistType.HLS
404
405 playlist.assignP2PMediaLoaderInfoHashes(video, allResolutions)
406
407 return playlist.save()
408 } 408 }
409 409
410 static get Instance () { 410 static get Instance () {
diff --git a/server/lib/live/shared/muxing-session.ts b/server/lib/live/shared/muxing-session.ts
index 26467f060..709d6c615 100644
--- a/server/lib/live/shared/muxing-session.ts
+++ b/server/lib/live/shared/muxing-session.ts
@@ -112,13 +112,16 @@ class MuxingSession extends EventEmitter {
112 this.ffmpegCommand = CONFIG.LIVE.TRANSCODING.ENABLED 112 this.ffmpegCommand = CONFIG.LIVE.TRANSCODING.ENABLED
113 ? await getLiveTranscodingCommand({ 113 ? await getLiveTranscodingCommand({
114 rtmpUrl: this.rtmpUrl, 114 rtmpUrl: this.rtmpUrl,
115
115 outPath, 116 outPath,
117 masterPlaylistName: this.streamingPlaylist.playlistFilename,
118
116 resolutions: this.allResolutions, 119 resolutions: this.allResolutions,
117 fps: this.fps, 120 fps: this.fps,
118 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), 121 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
119 profile: CONFIG.LIVE.TRANSCODING.PROFILE 122 profile: CONFIG.LIVE.TRANSCODING.PROFILE
120 }) 123 })
121 : getLiveMuxingCommand(this.rtmpUrl, outPath) 124 : getLiveMuxingCommand(this.rtmpUrl, outPath, this.streamingPlaylist.playlistFilename)
122 125
123 logger.info('Running live muxing/transcoding for %s.', this.videoUUID, this.lTags) 126 logger.info('Running live muxing/transcoding for %s.', this.videoUUID, this.lTags)
124 127
@@ -182,7 +185,7 @@ class MuxingSession extends EventEmitter {
182 } 185 }
183 186
184 private watchMasterFile (outPath: string) { 187 private watchMasterFile (outPath: string) {
185 this.masterWatcher = chokidar.watch(outPath + '/master.m3u8') 188 this.masterWatcher = chokidar.watch(outPath + '/' + this.streamingPlaylist.playlistFilename)
186 189
187 this.masterWatcher.on('add', async () => { 190 this.masterWatcher.on('add', async () => {
188 this.emit('master-playlist-created', { videoId: this.videoId }) 191 this.emit('master-playlist-created', { videoId: this.videoId })
diff --git a/server/lib/moderation.ts b/server/lib/moderation.ts
index 14e00518e..a42ab5b7f 100644
--- a/server/lib/moderation.ts
+++ b/server/lib/moderation.ts
@@ -23,7 +23,7 @@ import { ActivityCreate } from '../../shared/models/activitypub'
23import { VideoObject } from '../../shared/models/activitypub/objects' 23import { VideoObject } from '../../shared/models/activitypub/objects'
24import { VideoCommentObject } from '../../shared/models/activitypub/objects/video-comment-object' 24import { VideoCommentObject } from '../../shared/models/activitypub/objects/video-comment-object'
25import { LiveVideoCreate, VideoCreate, VideoImportCreate } from '../../shared/models/videos' 25import { LiveVideoCreate, VideoCreate, VideoImportCreate } from '../../shared/models/videos'
26import { VideoCommentCreate } from '../../shared/models/videos/comment/video-comment.model' 26import { VideoCommentCreate } from '../../shared/models/videos/comment'
27import { ActorModel } from '../models/actor/actor' 27import { ActorModel } from '../models/actor/actor'
28import { UserModel } from '../models/user/user' 28import { UserModel } from '../models/user/user'
29import { VideoModel } from '../models/video/video' 29import { VideoModel } from '../models/video/video'
diff --git a/server/lib/notifier.ts b/server/lib/notifier.ts
deleted file mode 100644
index 1f9ff16df..000000000
--- a/server/lib/notifier.ts
+++ /dev/null
@@ -1,796 +0,0 @@
1import { AccountModel } from '@server/models/account/account'
2import { getServerActor } from '@server/models/application/application'
3import { ServerBlocklistModel } from '@server/models/server/server-blocklist'
4import {
5 MUser,
6 MUserAccount,
7 MUserDefault,
8 MUserNotifSettingAccount,
9 MUserWithNotificationSetting,
10 UserNotificationModelForApi
11} from '@server/types/models/user'
12import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist'
13import { MVideoImportVideo } from '@server/types/models/video/video-import'
14import { UserAbuse } from '@shared/models'
15import { UserNotificationSettingValue, UserNotificationType, UserRight } from '../../shared/models/users'
16import { VideoPrivacy, VideoState } from '../../shared/models/videos'
17import { logger } from '../helpers/logger'
18import { CONFIG } from '../initializers/config'
19import { AccountBlocklistModel } from '../models/account/account-blocklist'
20import { UserModel } from '../models/user/user'
21import { UserNotificationModel } from '../models/user/user-notification'
22import { MAbuseFull, MAbuseMessage, MAccountServer, MActorFollowFull, MApplication, MPlugin } from '../types/models'
23import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video'
24import { isBlockedByServerOrAccount } from './blocklist'
25import { Emailer } from './emailer'
26import { PeerTubeSocket } from './peertube-socket'
27
28class Notifier {
29
30 private static instance: Notifier
31
32 private constructor () {
33 }
34
35 notifyOnNewVideoIfNeeded (video: MVideoAccountLight): void {
36 // Only notify on public and published videos which are not blacklisted
37 if (video.privacy !== VideoPrivacy.PUBLIC || video.state !== VideoState.PUBLISHED || video.isBlacklisted()) return
38
39 this.notifySubscribersOfNewVideo(video)
40 .catch(err => logger.error('Cannot notify subscribers of new video %s.', video.url, { err }))
41 }
42
43 notifyOnVideoPublishedAfterTranscoding (video: MVideoFullLight): void {
44 // don't notify if didn't wait for transcoding or video is still blacklisted/waiting for scheduled update
45 if (!video.waitTranscoding || video.VideoBlacklist || video.ScheduleVideoUpdate) return
46
47 this.notifyOwnedVideoHasBeenPublished(video)
48 .catch(err => logger.error('Cannot notify owner that its video %s has been published after transcoding.', video.url, { err }))
49 }
50
51 notifyOnVideoPublishedAfterScheduledUpdate (video: MVideoFullLight): void {
52 // don't notify if video is still blacklisted or waiting for transcoding
53 if (video.VideoBlacklist || (video.waitTranscoding && video.state !== VideoState.PUBLISHED)) return
54
55 this.notifyOwnedVideoHasBeenPublished(video)
56 .catch(err => logger.error('Cannot notify owner that its video %s has been published after scheduled update.', video.url, { err }))
57 }
58
59 notifyOnVideoPublishedAfterRemovedFromAutoBlacklist (video: MVideoFullLight): void {
60 // don't notify if video is still waiting for transcoding or scheduled update
61 if (video.ScheduleVideoUpdate || (video.waitTranscoding && video.state !== VideoState.PUBLISHED)) return
62
63 this.notifyOwnedVideoHasBeenPublished(video)
64 .catch(err => {
65 logger.error('Cannot notify owner that its video %s has been published after removed from auto-blacklist.', video.url, { err })
66 })
67 }
68
69 notifyOnNewComment (comment: MCommentOwnerVideo): void {
70 this.notifyVideoOwnerOfNewComment(comment)
71 .catch(err => logger.error('Cannot notify video owner of new comment %s.', comment.url, { err }))
72
73 this.notifyOfCommentMention(comment)
74 .catch(err => logger.error('Cannot notify mentions of comment %s.', comment.url, { err }))
75 }
76
77 notifyOnNewAbuse (parameters: { abuse: UserAbuse, abuseInstance: MAbuseFull, reporter: string }): void {
78 this.notifyModeratorsOfNewAbuse(parameters)
79 .catch(err => logger.error('Cannot notify of new abuse %d.', parameters.abuseInstance.id, { err }))
80 }
81
82 notifyOnVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo): void {
83 this.notifyModeratorsOfVideoAutoBlacklist(videoBlacklist)
84 .catch(err => logger.error('Cannot notify of auto-blacklist of video %s.', videoBlacklist.Video.url, { err }))
85 }
86
87 notifyOnVideoBlacklist (videoBlacklist: MVideoBlacklistVideo): void {
88 this.notifyVideoOwnerOfBlacklist(videoBlacklist)
89 .catch(err => logger.error('Cannot notify video owner of new video blacklist of %s.', videoBlacklist.Video.url, { err }))
90 }
91
92 notifyOnVideoUnblacklist (video: MVideoFullLight): void {
93 this.notifyVideoOwnerOfUnblacklist(video)
94 .catch(err => logger.error('Cannot notify video owner of unblacklist of %s.', video.url, { err }))
95 }
96
97 notifyOnFinishedVideoImport (videoImport: MVideoImportVideo, success: boolean): void {
98 this.notifyOwnerVideoImportIsFinished(videoImport, success)
99 .catch(err => logger.error('Cannot notify owner that its video import %s is finished.', videoImport.getTargetIdentifier(), { err }))
100 }
101
102 notifyOnNewUserRegistration (user: MUserDefault): void {
103 this.notifyModeratorsOfNewUserRegistration(user)
104 .catch(err => logger.error('Cannot notify moderators of new user registration (%s).', user.username, { err }))
105 }
106
107 notifyOfNewUserFollow (actorFollow: MActorFollowFull): void {
108 this.notifyUserOfNewActorFollow(actorFollow)
109 .catch(err => {
110 logger.error(
111 'Cannot notify owner of channel %s of a new follow by %s.',
112 actorFollow.ActorFollowing.VideoChannel.getDisplayName(),
113 actorFollow.ActorFollower.Account.getDisplayName(),
114 { err }
115 )
116 })
117 }
118
119 notifyOfNewInstanceFollow (actorFollow: MActorFollowFull): void {
120 this.notifyAdminsOfNewInstanceFollow(actorFollow)
121 .catch(err => {
122 logger.error('Cannot notify administrators of new follower %s.', actorFollow.ActorFollower.url, { err })
123 })
124 }
125
126 notifyOfAutoInstanceFollowing (actorFollow: MActorFollowFull): void {
127 this.notifyAdminsOfAutoInstanceFollowing(actorFollow)
128 .catch(err => {
129 logger.error('Cannot notify administrators of auto instance following %s.', actorFollow.ActorFollowing.url, { err })
130 })
131 }
132
133 notifyOnAbuseStateChange (abuse: MAbuseFull): void {
134 this.notifyReporterOfAbuseStateChange(abuse)
135 .catch(err => {
136 logger.error('Cannot notify reporter of abuse %d state change.', abuse.id, { err })
137 })
138 }
139
140 notifyOnAbuseMessage (abuse: MAbuseFull, message: MAbuseMessage): void {
141 this.notifyOfNewAbuseMessage(abuse, message)
142 .catch(err => {
143 logger.error('Cannot notify on new abuse %d message.', abuse.id, { err })
144 })
145 }
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
161 private async notifySubscribersOfNewVideo (video: MVideoAccountLight) {
162 // List all followers that are users
163 const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId)
164
165 logger.info('Notifying %d users of new video %s.', users.length, video.url)
166
167 function settingGetter (user: MUserWithNotificationSetting) {
168 return user.NotificationSetting.newVideoFromSubscription
169 }
170
171 async function notificationCreator (user: MUserWithNotificationSetting) {
172 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
173 type: UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION,
174 userId: user.id,
175 videoId: video.id
176 })
177 notification.Video = video
178
179 return notification
180 }
181
182 function emailSender (emails: string[]) {
183 return Emailer.Instance.addNewVideoFromSubscriberNotification(emails, video)
184 }
185
186 return this.notify({ users, settingGetter, notificationCreator, emailSender })
187 }
188
189 private async notifyVideoOwnerOfNewComment (comment: MCommentOwnerVideo) {
190 if (comment.Video.isOwned() === false) return
191
192 const user = await UserModel.loadByVideoId(comment.videoId)
193
194 // Not our user or user comments its own video
195 if (!user || comment.Account.userId === user.id) return
196
197 if (await this.isBlockedByServerOrUser(comment.Account, user)) return
198
199 logger.info('Notifying user %s of new comment %s.', user.username, comment.url)
200
201 function settingGetter (user: MUserWithNotificationSetting) {
202 return user.NotificationSetting.newCommentOnMyVideo
203 }
204
205 async function notificationCreator (user: MUserWithNotificationSetting) {
206 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
207 type: UserNotificationType.NEW_COMMENT_ON_MY_VIDEO,
208 userId: user.id,
209 commentId: comment.id
210 })
211 notification.Comment = comment
212
213 return notification
214 }
215
216 function emailSender (emails: string[]) {
217 return Emailer.Instance.addNewCommentOnMyVideoNotification(emails, comment)
218 }
219
220 return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender })
221 }
222
223 private async notifyOfCommentMention (comment: MCommentOwnerVideo) {
224 const extractedUsernames = comment.extractMentions()
225 logger.debug(
226 'Extracted %d username from comment %s.', extractedUsernames.length, comment.url,
227 { usernames: extractedUsernames, text: comment.text }
228 )
229
230 let users = await UserModel.listByUsernames(extractedUsernames)
231
232 if (comment.Video.isOwned()) {
233 const userException = await UserModel.loadByVideoId(comment.videoId)
234 users = users.filter(u => u.id !== userException.id)
235 }
236
237 // Don't notify if I mentioned myself
238 users = users.filter(u => u.Account.id !== comment.accountId)
239
240 if (users.length === 0) return
241
242 const serverAccountId = (await getServerActor()).Account.id
243 const sourceAccounts = users.map(u => u.Account.id).concat([ serverAccountId ])
244
245 const accountMutedHash = await AccountBlocklistModel.isAccountMutedByMulti(sourceAccounts, comment.accountId)
246 const instanceMutedHash = await ServerBlocklistModel.isServerMutedByMulti(sourceAccounts, comment.Account.Actor.serverId)
247
248 logger.info('Notifying %d users of new comment %s.', users.length, comment.url)
249
250 function settingGetter (user: MUserNotifSettingAccount) {
251 const accountId = user.Account.id
252 if (
253 accountMutedHash[accountId] === true || instanceMutedHash[accountId] === true ||
254 accountMutedHash[serverAccountId] === true || instanceMutedHash[serverAccountId] === true
255 ) {
256 return UserNotificationSettingValue.NONE
257 }
258
259 return user.NotificationSetting.commentMention
260 }
261
262 async function notificationCreator (user: MUserNotifSettingAccount) {
263 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
264 type: UserNotificationType.COMMENT_MENTION,
265 userId: user.id,
266 commentId: comment.id
267 })
268 notification.Comment = comment
269
270 return notification
271 }
272
273 function emailSender (emails: string[]) {
274 return Emailer.Instance.addNewCommentMentionNotification(emails, comment)
275 }
276
277 return this.notify({ users, settingGetter, notificationCreator, emailSender })
278 }
279
280 private async notifyUserOfNewActorFollow (actorFollow: MActorFollowFull) {
281 if (actorFollow.ActorFollowing.isOwned() === false) return
282
283 // Account follows one of our account?
284 let followType: 'account' | 'channel' = 'channel'
285 let user = await UserModel.loadByChannelActorId(actorFollow.ActorFollowing.id)
286
287 // Account follows one of our channel?
288 if (!user) {
289 user = await UserModel.loadByAccountActorId(actorFollow.ActorFollowing.id)
290 followType = 'account'
291 }
292
293 if (!user) return
294
295 const followerAccount = actorFollow.ActorFollower.Account
296 const followerAccountWithActor = Object.assign(followerAccount, { Actor: actorFollow.ActorFollower })
297
298 if (await this.isBlockedByServerOrUser(followerAccountWithActor, user)) return
299
300 logger.info('Notifying user %s of new follower: %s.', user.username, followerAccount.getDisplayName())
301
302 function settingGetter (user: MUserWithNotificationSetting) {
303 return user.NotificationSetting.newFollow
304 }
305
306 async function notificationCreator (user: MUserWithNotificationSetting) {
307 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
308 type: UserNotificationType.NEW_FOLLOW,
309 userId: user.id,
310 actorFollowId: actorFollow.id
311 })
312 notification.ActorFollow = actorFollow
313
314 return notification
315 }
316
317 function emailSender (emails: string[]) {
318 return Emailer.Instance.addNewFollowNotification(emails, actorFollow, followType)
319 }
320
321 return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender })
322 }
323
324 private async notifyAdminsOfNewInstanceFollow (actorFollow: MActorFollowFull) {
325 const admins = await UserModel.listWithRight(UserRight.MANAGE_SERVER_FOLLOW)
326
327 const follower = Object.assign(actorFollow.ActorFollower.Account, { Actor: actorFollow.ActorFollower })
328 if (await this.isBlockedByServerOrUser(follower)) return
329
330 logger.info('Notifying %d administrators of new instance follower: %s.', admins.length, actorFollow.ActorFollower.url)
331
332 function settingGetter (user: MUserWithNotificationSetting) {
333 return user.NotificationSetting.newInstanceFollower
334 }
335
336 async function notificationCreator (user: MUserWithNotificationSetting) {
337 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
338 type: UserNotificationType.NEW_INSTANCE_FOLLOWER,
339 userId: user.id,
340 actorFollowId: actorFollow.id
341 })
342 notification.ActorFollow = actorFollow
343
344 return notification
345 }
346
347 function emailSender (emails: string[]) {
348 return Emailer.Instance.addNewInstanceFollowerNotification(emails, actorFollow)
349 }
350
351 return this.notify({ users: admins, settingGetter, notificationCreator, emailSender })
352 }
353
354 private async notifyAdminsOfAutoInstanceFollowing (actorFollow: MActorFollowFull) {
355 const admins = await UserModel.listWithRight(UserRight.MANAGE_SERVER_FOLLOW)
356
357 logger.info('Notifying %d administrators of auto instance following: %s.', admins.length, actorFollow.ActorFollowing.url)
358
359 function settingGetter (user: MUserWithNotificationSetting) {
360 return user.NotificationSetting.autoInstanceFollowing
361 }
362
363 async function notificationCreator (user: MUserWithNotificationSetting) {
364 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
365 type: UserNotificationType.AUTO_INSTANCE_FOLLOWING,
366 userId: user.id,
367 actorFollowId: actorFollow.id
368 })
369 notification.ActorFollow = actorFollow
370
371 return notification
372 }
373
374 function emailSender (emails: string[]) {
375 return Emailer.Instance.addAutoInstanceFollowingNotification(emails, actorFollow)
376 }
377
378 return this.notify({ users: admins, settingGetter, notificationCreator, emailSender })
379 }
380
381 private async notifyModeratorsOfNewAbuse (parameters: {
382 abuse: UserAbuse
383 abuseInstance: MAbuseFull
384 reporter: string
385 }) {
386 const { abuse, abuseInstance } = parameters
387
388 const moderators = await UserModel.listWithRight(UserRight.MANAGE_ABUSES)
389 if (moderators.length === 0) return
390
391 const url = this.getAbuseUrl(abuseInstance)
392
393 logger.info('Notifying %s user/moderators of new abuse %s.', moderators.length, url)
394
395 function settingGetter (user: MUserWithNotificationSetting) {
396 return user.NotificationSetting.abuseAsModerator
397 }
398
399 async function notificationCreator (user: MUserWithNotificationSetting) {
400 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
401 type: UserNotificationType.NEW_ABUSE_FOR_MODERATORS,
402 userId: user.id,
403 abuseId: abuse.id
404 })
405 notification.Abuse = abuseInstance
406
407 return notification
408 }
409
410 function emailSender (emails: string[]) {
411 return Emailer.Instance.addAbuseModeratorsNotification(emails, parameters)
412 }
413
414 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
415 }
416
417 private async notifyReporterOfAbuseStateChange (abuse: MAbuseFull) {
418 // Only notify our users
419 if (abuse.ReporterAccount.isOwned() !== true) return
420
421 const url = this.getAbuseUrl(abuse)
422
423 logger.info('Notifying reporter of abuse % of state change.', url)
424
425 const reporter = await UserModel.loadByAccountActorId(abuse.ReporterAccount.actorId)
426
427 function settingGetter (user: MUserWithNotificationSetting) {
428 return user.NotificationSetting.abuseStateChange
429 }
430
431 async function notificationCreator (user: MUserWithNotificationSetting) {
432 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
433 type: UserNotificationType.ABUSE_STATE_CHANGE,
434 userId: user.id,
435 abuseId: abuse.id
436 })
437 notification.Abuse = abuse
438
439 return notification
440 }
441
442 function emailSender (emails: string[]) {
443 return Emailer.Instance.addAbuseStateChangeNotification(emails, abuse)
444 }
445
446 return this.notify({ users: [ reporter ], settingGetter, notificationCreator, emailSender })
447 }
448
449 private async notifyOfNewAbuseMessage (abuse: MAbuseFull, message: MAbuseMessage) {
450 const url = this.getAbuseUrl(abuse)
451 logger.info('Notifying reporter and moderators of new abuse message on %s.', url)
452
453 const accountMessage = await AccountModel.load(message.accountId)
454
455 function settingGetter (user: MUserWithNotificationSetting) {
456 return user.NotificationSetting.abuseNewMessage
457 }
458
459 async function notificationCreator (user: MUserWithNotificationSetting) {
460 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
461 type: UserNotificationType.ABUSE_NEW_MESSAGE,
462 userId: user.id,
463 abuseId: abuse.id
464 })
465 notification.Abuse = abuse
466
467 return notification
468 }
469
470 function emailSenderReporter (emails: string[]) {
471 return Emailer.Instance.addAbuseNewMessageNotification(emails, { target: 'reporter', abuse, message, accountMessage })
472 }
473
474 function emailSenderModerators (emails: string[]) {
475 return Emailer.Instance.addAbuseNewMessageNotification(emails, { target: 'moderator', abuse, message, accountMessage })
476 }
477
478 async function buildReporterOptions () {
479 // Only notify our users
480 if (abuse.ReporterAccount.isOwned() !== true) return undefined
481
482 const reporter = await UserModel.loadByAccountActorId(abuse.ReporterAccount.actorId)
483 // Don't notify my own message
484 if (reporter.Account.id === message.accountId) return undefined
485
486 return { users: [ reporter ], settingGetter, notificationCreator, emailSender: emailSenderReporter }
487 }
488
489 async function buildModeratorsOptions () {
490 let moderators = await UserModel.listWithRight(UserRight.MANAGE_ABUSES)
491 // Don't notify my own message
492 moderators = moderators.filter(m => m.Account.id !== message.accountId)
493
494 if (moderators.length === 0) return undefined
495
496 return { users: moderators, settingGetter, notificationCreator, emailSender: emailSenderModerators }
497 }
498
499 const options = await Promise.all([
500 buildReporterOptions(),
501 buildModeratorsOptions()
502 ])
503
504 return Promise.all(
505 options
506 .filter(opt => !!opt)
507 .map(opt => this.notify(opt))
508 )
509 }
510
511 private async notifyModeratorsOfVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo) {
512 const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_BLACKLIST)
513 if (moderators.length === 0) return
514
515 logger.info('Notifying %s moderators of video auto-blacklist %s.', moderators.length, videoBlacklist.Video.url)
516
517 function settingGetter (user: MUserWithNotificationSetting) {
518 return user.NotificationSetting.videoAutoBlacklistAsModerator
519 }
520
521 async function notificationCreator (user: MUserWithNotificationSetting) {
522 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
523 type: UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS,
524 userId: user.id,
525 videoBlacklistId: videoBlacklist.id
526 })
527 notification.VideoBlacklist = videoBlacklist
528
529 return notification
530 }
531
532 function emailSender (emails: string[]) {
533 return Emailer.Instance.addVideoAutoBlacklistModeratorsNotification(emails, videoBlacklist)
534 }
535
536 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
537 }
538
539 private async notifyVideoOwnerOfBlacklist (videoBlacklist: MVideoBlacklistVideo) {
540 const user = await UserModel.loadByVideoId(videoBlacklist.videoId)
541 if (!user) return
542
543 logger.info('Notifying user %s that its video %s has been blacklisted.', user.username, videoBlacklist.Video.url)
544
545 function settingGetter (user: MUserWithNotificationSetting) {
546 return user.NotificationSetting.blacklistOnMyVideo
547 }
548
549 async function notificationCreator (user: MUserWithNotificationSetting) {
550 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
551 type: UserNotificationType.BLACKLIST_ON_MY_VIDEO,
552 userId: user.id,
553 videoBlacklistId: videoBlacklist.id
554 })
555 notification.VideoBlacklist = videoBlacklist
556
557 return notification
558 }
559
560 function emailSender (emails: string[]) {
561 return Emailer.Instance.addVideoBlacklistNotification(emails, videoBlacklist)
562 }
563
564 return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender })
565 }
566
567 private async notifyVideoOwnerOfUnblacklist (video: MVideoFullLight) {
568 const user = await UserModel.loadByVideoId(video.id)
569 if (!user) return
570
571 logger.info('Notifying user %s that its video %s has been unblacklisted.', user.username, video.url)
572
573 function settingGetter (user: MUserWithNotificationSetting) {
574 return user.NotificationSetting.blacklistOnMyVideo
575 }
576
577 async function notificationCreator (user: MUserWithNotificationSetting) {
578 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
579 type: UserNotificationType.UNBLACKLIST_ON_MY_VIDEO,
580 userId: user.id,
581 videoId: video.id
582 })
583 notification.Video = video
584
585 return notification
586 }
587
588 function emailSender (emails: string[]) {
589 return Emailer.Instance.addVideoUnblacklistNotification(emails, video)
590 }
591
592 return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender })
593 }
594
595 private async notifyOwnedVideoHasBeenPublished (video: MVideoFullLight) {
596 const user = await UserModel.loadByVideoId(video.id)
597 if (!user) return
598
599 logger.info('Notifying user %s of the publication of its video %s.', user.username, video.url)
600
601 function settingGetter (user: MUserWithNotificationSetting) {
602 return user.NotificationSetting.myVideoPublished
603 }
604
605 async function notificationCreator (user: MUserWithNotificationSetting) {
606 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
607 type: UserNotificationType.MY_VIDEO_PUBLISHED,
608 userId: user.id,
609 videoId: video.id
610 })
611 notification.Video = video
612
613 return notification
614 }
615
616 function emailSender (emails: string[]) {
617 return Emailer.Instance.myVideoPublishedNotification(emails, video)
618 }
619
620 return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender })
621 }
622
623 private async notifyOwnerVideoImportIsFinished (videoImport: MVideoImportVideo, success: boolean) {
624 const user = await UserModel.loadByVideoImportId(videoImport.id)
625 if (!user) return
626
627 logger.info('Notifying user %s its video import %s is finished.', user.username, videoImport.getTargetIdentifier())
628
629 function settingGetter (user: MUserWithNotificationSetting) {
630 return user.NotificationSetting.myVideoImportFinished
631 }
632
633 async function notificationCreator (user: MUserWithNotificationSetting) {
634 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
635 type: success ? UserNotificationType.MY_VIDEO_IMPORT_SUCCESS : UserNotificationType.MY_VIDEO_IMPORT_ERROR,
636 userId: user.id,
637 videoImportId: videoImport.id
638 })
639 notification.VideoImport = videoImport
640
641 return notification
642 }
643
644 function emailSender (emails: string[]) {
645 return success
646 ? Emailer.Instance.myVideoImportSuccessNotification(emails, videoImport)
647 : Emailer.Instance.myVideoImportErrorNotification(emails, videoImport)
648 }
649
650 return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender })
651 }
652
653 private async notifyModeratorsOfNewUserRegistration (registeredUser: MUserDefault) {
654 const moderators = await UserModel.listWithRight(UserRight.MANAGE_USERS)
655 if (moderators.length === 0) return
656
657 logger.info(
658 'Notifying %s moderators of new user registration of %s.',
659 moderators.length, registeredUser.username
660 )
661
662 function settingGetter (user: MUserWithNotificationSetting) {
663 return user.NotificationSetting.newUserRegistration
664 }
665
666 async function notificationCreator (user: MUserWithNotificationSetting) {
667 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
668 type: UserNotificationType.NEW_USER_REGISTRATION,
669 userId: user.id,
670 accountId: registeredUser.Account.id
671 })
672 notification.Account = registeredUser.Account
673
674 return notification
675 }
676
677 function emailSender (emails: string[]) {
678 return Emailer.Instance.addNewUserRegistrationNotification(emails, registeredUser)
679 }
680
681 return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
682 }
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
742 private async notify<T extends MUserWithNotificationSetting> (options: {
743 users: T[]
744 notificationCreator: (user: T) => Promise<UserNotificationModelForApi>
745 emailSender: (emails: string[]) => void
746 settingGetter: (user: T) => UserNotificationSettingValue
747 }) {
748 const emails: string[] = []
749
750 for (const user of options.users) {
751 if (this.isWebNotificationEnabled(options.settingGetter(user))) {
752 const notification = await options.notificationCreator(user)
753
754 PeerTubeSocket.Instance.sendNotification(user.id, notification)
755 }
756
757 if (this.isEmailEnabled(user, options.settingGetter(user))) {
758 emails.push(user.email)
759 }
760 }
761
762 if (emails.length !== 0) {
763 options.emailSender(emails)
764 }
765 }
766
767 private isEmailEnabled (user: MUser, value: UserNotificationSettingValue) {
768 if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION === true && user.emailVerified === false) return false
769
770 return value & UserNotificationSettingValue.EMAIL
771 }
772
773 private isWebNotificationEnabled (value: UserNotificationSettingValue) {
774 return value & UserNotificationSettingValue.WEB
775 }
776
777 private isBlockedByServerOrUser (targetAccount: MAccountServer, user?: MUserAccount) {
778 return isBlockedByServerOrAccount(targetAccount, user?.Account)
779 }
780
781 private getAbuseUrl (abuse: MAbuseFull) {
782 return abuse.VideoAbuse?.Video?.url ||
783 abuse.VideoCommentAbuse?.VideoComment?.url ||
784 abuse.FlaggedAccount.Actor.url
785 }
786
787 static get Instance () {
788 return this.instance || (this.instance = new this())
789 }
790}
791
792// ---------------------------------------------------------------------------
793
794export {
795 Notifier
796}
diff --git a/server/lib/notifier/index.ts b/server/lib/notifier/index.ts
new file mode 100644
index 000000000..5bc2f5f50
--- /dev/null
+++ b/server/lib/notifier/index.ts
@@ -0,0 +1 @@
export * from './notifier'
diff --git a/server/lib/notifier/notifier.ts b/server/lib/notifier/notifier.ts
new file mode 100644
index 000000000..8b68d2e69
--- /dev/null
+++ b/server/lib/notifier/notifier.ts
@@ -0,0 +1,259 @@
1import { MUser, MUserDefault } from '@server/types/models/user'
2import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist'
3import { UserNotificationSettingValue } from '../../../shared/models/users'
4import { logger } from '../../helpers/logger'
5import { CONFIG } from '../../initializers/config'
6import { MAbuseFull, MAbuseMessage, MActorFollowFull, MApplication, MPlugin } from '../../types/models'
7import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../../types/models/video'
8import { JobQueue } from '../job-queue'
9import { PeerTubeSocket } from '../peertube-socket'
10import {
11 AbstractNotification,
12 AbuseStateChangeForReporter,
13 AutoFollowForInstance,
14 CommentMention,
15 FollowForInstance,
16 FollowForUser,
17 ImportFinishedForOwner,
18 ImportFinishedForOwnerPayload,
19 NewAbuseForModerators,
20 NewAbuseMessageForModerators,
21 NewAbuseMessageForReporter,
22 NewAbusePayload,
23 NewAutoBlacklistForModerators,
24 NewBlacklistForOwner,
25 NewCommentForVideoOwner,
26 NewPeerTubeVersionForAdmins,
27 NewPluginVersionForAdmins,
28 NewVideoForSubscribers,
29 OwnedPublicationAfterAutoUnblacklist,
30 OwnedPublicationAfterScheduleUpdate,
31 OwnedPublicationAfterTranscoding,
32 RegistrationForModerators,
33 UnblacklistForOwner
34} from './shared'
35
36class Notifier {
37
38 private readonly notificationModels = {
39 newVideo: [ NewVideoForSubscribers ],
40 publicationAfterTranscoding: [ OwnedPublicationAfterTranscoding ],
41 publicationAfterScheduleUpdate: [ OwnedPublicationAfterScheduleUpdate ],
42 publicationAfterAutoUnblacklist: [ OwnedPublicationAfterAutoUnblacklist ],
43 newComment: [ CommentMention, NewCommentForVideoOwner ],
44 newAbuse: [ NewAbuseForModerators ],
45 newBlacklist: [ NewBlacklistForOwner ],
46 unblacklist: [ UnblacklistForOwner ],
47 importFinished: [ ImportFinishedForOwner ],
48 userRegistration: [ RegistrationForModerators ],
49 userFollow: [ FollowForUser ],
50 instanceFollow: [ FollowForInstance ],
51 autoInstanceFollow: [ AutoFollowForInstance ],
52 newAutoBlacklist: [ NewAutoBlacklistForModerators ],
53 abuseStateChange: [ AbuseStateChangeForReporter ],
54 newAbuseMessage: [ NewAbuseMessageForReporter, NewAbuseMessageForModerators ],
55 newPeertubeVersion: [ NewPeerTubeVersionForAdmins ],
56 newPluginVersion: [ NewPluginVersionForAdmins ]
57 }
58
59 private static instance: Notifier
60
61 private constructor () {
62 }
63
64 notifyOnNewVideoIfNeeded (video: MVideoAccountLight): void {
65 const models = this.notificationModels.newVideo
66
67 this.sendNotifications(models, video)
68 .catch(err => logger.error('Cannot notify subscribers of new video %s.', video.url, { err }))
69 }
70
71 notifyOnVideoPublishedAfterTranscoding (video: MVideoFullLight): void {
72 const models = this.notificationModels.publicationAfterTranscoding
73
74 this.sendNotifications(models, video)
75 .catch(err => logger.error('Cannot notify owner that its video %s has been published after transcoding.', video.url, { err }))
76 }
77
78 notifyOnVideoPublishedAfterScheduledUpdate (video: MVideoFullLight): void {
79 const models = this.notificationModels.publicationAfterScheduleUpdate
80
81 this.sendNotifications(models, video)
82 .catch(err => logger.error('Cannot notify owner that its video %s has been published after scheduled update.', video.url, { err }))
83 }
84
85 notifyOnVideoPublishedAfterRemovedFromAutoBlacklist (video: MVideoFullLight): void {
86 const models = this.notificationModels.publicationAfterAutoUnblacklist
87
88 this.sendNotifications(models, video)
89 .catch(err => {
90 logger.error('Cannot notify owner that its video %s has been published after removed from auto-blacklist.', video.url, { err })
91 })
92 }
93
94 notifyOnNewComment (comment: MCommentOwnerVideo): void {
95 const models = this.notificationModels.newComment
96
97 this.sendNotifications(models, comment)
98 .catch(err => logger.error('Cannot notify of new comment.', comment.url, { err }))
99 }
100
101 notifyOnNewAbuse (payload: NewAbusePayload): void {
102 const models = this.notificationModels.newAbuse
103
104 this.sendNotifications(models, payload)
105 .catch(err => logger.error('Cannot notify of new abuse %d.', payload.abuseInstance.id, { err }))
106 }
107
108 notifyOnVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo): void {
109 const models = this.notificationModels.newAutoBlacklist
110
111 this.sendNotifications(models, videoBlacklist)
112 .catch(err => logger.error('Cannot notify of auto-blacklist of video %s.', videoBlacklist.Video.url, { err }))
113 }
114
115 notifyOnVideoBlacklist (videoBlacklist: MVideoBlacklistVideo): void {
116 const models = this.notificationModels.newBlacklist
117
118 this.sendNotifications(models, videoBlacklist)
119 .catch(err => logger.error('Cannot notify video owner of new video blacklist of %s.', videoBlacklist.Video.url, { err }))
120 }
121
122 notifyOnVideoUnblacklist (video: MVideoFullLight): void {
123 const models = this.notificationModels.unblacklist
124
125 this.sendNotifications(models, video)
126 .catch(err => logger.error('Cannot notify video owner of unblacklist of %s.', video.url, { err }))
127 }
128
129 notifyOnFinishedVideoImport (payload: ImportFinishedForOwnerPayload): void {
130 const models = this.notificationModels.importFinished
131
132 this.sendNotifications(models, payload)
133 .catch(err => {
134 logger.error('Cannot notify owner that its video import %s is finished.', payload.videoImport.getTargetIdentifier(), { err })
135 })
136 }
137
138 notifyOnNewUserRegistration (user: MUserDefault): void {
139 const models = this.notificationModels.userRegistration
140
141 this.sendNotifications(models, user)
142 .catch(err => logger.error('Cannot notify moderators of new user registration (%s).', user.username, { err }))
143 }
144
145 notifyOfNewUserFollow (actorFollow: MActorFollowFull): void {
146 const models = this.notificationModels.userFollow
147
148 this.sendNotifications(models, actorFollow)
149 .catch(err => {
150 logger.error(
151 'Cannot notify owner of channel %s of a new follow by %s.',
152 actorFollow.ActorFollowing.VideoChannel.getDisplayName(),
153 actorFollow.ActorFollower.Account.getDisplayName(),
154 { err }
155 )
156 })
157 }
158
159 notifyOfNewInstanceFollow (actorFollow: MActorFollowFull): void {
160 const models = this.notificationModels.instanceFollow
161
162 this.sendNotifications(models, actorFollow)
163 .catch(err => logger.error('Cannot notify administrators of new follower %s.', actorFollow.ActorFollower.url, { err }))
164 }
165
166 notifyOfAutoInstanceFollowing (actorFollow: MActorFollowFull): void {
167 const models = this.notificationModels.autoInstanceFollow
168
169 this.sendNotifications(models, actorFollow)
170 .catch(err => logger.error('Cannot notify administrators of auto instance following %s.', actorFollow.ActorFollowing.url, { err }))
171 }
172
173 notifyOnAbuseStateChange (abuse: MAbuseFull): void {
174 const models = this.notificationModels.abuseStateChange
175
176 this.sendNotifications(models, abuse)
177 .catch(err => logger.error('Cannot notify of abuse %d state change.', abuse.id, { err }))
178 }
179
180 notifyOnAbuseMessage (abuse: MAbuseFull, message: MAbuseMessage): void {
181 const models = this.notificationModels.newAbuseMessage
182
183 this.sendNotifications(models, { abuse, message })
184 .catch(err => logger.error('Cannot notify on new abuse %d message.', abuse.id, { err }))
185 }
186
187 notifyOfNewPeerTubeVersion (application: MApplication, latestVersion: string) {
188 const models = this.notificationModels.newPeertubeVersion
189
190 this.sendNotifications(models, { application, latestVersion })
191 .catch(err => logger.error('Cannot notify on new PeerTubeb version %s.', latestVersion, { err }))
192 }
193
194 notifyOfNewPluginVersion (plugin: MPlugin) {
195 const models = this.notificationModels.newPluginVersion
196
197 this.sendNotifications(models, plugin)
198 .catch(err => logger.error('Cannot notify on new plugin version %s.', plugin.name, { err }))
199 }
200
201 private async notify <T> (object: AbstractNotification<T>) {
202 await object.prepare()
203
204 const users = object.getTargetUsers()
205
206 if (users.length === 0) return
207 if (await object.isDisabled()) return
208
209 object.log()
210
211 const toEmails: string[] = []
212
213 for (const user of users) {
214 const setting = object.getSetting(user)
215
216 if (this.isWebNotificationEnabled(setting)) {
217 const notification = await object.createNotification(user)
218
219 PeerTubeSocket.Instance.sendNotification(user.id, notification)
220 }
221
222 if (this.isEmailEnabled(user, setting)) {
223 toEmails.push(user.email)
224 }
225 }
226
227 for (const to of toEmails) {
228 const payload = await object.createEmail(to)
229 JobQueue.Instance.createJob({ type: 'email', payload })
230 }
231 }
232
233 private isEmailEnabled (user: MUser, value: UserNotificationSettingValue) {
234 if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION === true && user.emailVerified === false) return false
235
236 return value & UserNotificationSettingValue.EMAIL
237 }
238
239 private isWebNotificationEnabled (value: UserNotificationSettingValue) {
240 return value & UserNotificationSettingValue.WEB
241 }
242
243 private async sendNotifications <T> (models: (new (payload: T) => AbstractNotification<T>)[], payload: T) {
244 for (const model of models) {
245 // eslint-disable-next-line new-cap
246 await this.notify(new model(payload))
247 }
248 }
249
250 static get Instance () {
251 return this.instance || (this.instance = new this())
252 }
253}
254
255// ---------------------------------------------------------------------------
256
257export {
258 Notifier
259}
diff --git a/server/lib/notifier/shared/abuse/abstract-new-abuse-message.ts b/server/lib/notifier/shared/abuse/abstract-new-abuse-message.ts
new file mode 100644
index 000000000..1425c38ec
--- /dev/null
+++ b/server/lib/notifier/shared/abuse/abstract-new-abuse-message.ts
@@ -0,0 +1,67 @@
1import { WEBSERVER } from '@server/initializers/constants'
2import { AccountModel } from '@server/models/account/account'
3import { UserNotificationModel } from '@server/models/user/user-notification'
4import { MAbuseFull, MAbuseMessage, MAccountDefault, MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/types/models'
5import { UserNotificationType } from '@shared/models'
6import { AbstractNotification } from '../common/abstract-notification'
7
8export type NewAbuseMessagePayload = {
9 abuse: MAbuseFull
10 message: MAbuseMessage
11}
12
13export abstract class AbstractNewAbuseMessage extends AbstractNotification <NewAbuseMessagePayload> {
14 protected messageAccount: MAccountDefault
15
16 async loadMessageAccount () {
17 this.messageAccount = await AccountModel.load(this.message.accountId)
18 }
19
20 getSetting (user: MUserWithNotificationSetting) {
21 return user.NotificationSetting.abuseNewMessage
22 }
23
24 async createNotification (user: MUserWithNotificationSetting) {
25 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
26 type: UserNotificationType.ABUSE_NEW_MESSAGE,
27 userId: user.id,
28 abuseId: this.abuse.id
29 })
30 notification.Abuse = this.abuse
31
32 return notification
33 }
34
35 protected createEmailFor (to: string, target: 'moderator' | 'reporter') {
36 const text = 'New message on report #' + this.abuse.id
37 const abuseUrl = target === 'moderator'
38 ? WEBSERVER.URL + '/admin/moderation/abuses/list?search=%23' + this.abuse.id
39 : WEBSERVER.URL + '/my-account/abuses?search=%23' + this.abuse.id
40
41 const action = {
42 text,
43 url: abuseUrl
44 }
45
46 return {
47 template: 'abuse-new-message',
48 to,
49 subject: text,
50 locals: {
51 abuseId: this.abuse.id,
52 abuseUrl: action.url,
53 messageAccountName: this.messageAccount.getDisplayName(),
54 messageText: this.message.message,
55 action
56 }
57 }
58 }
59
60 protected get abuse () {
61 return this.payload.abuse
62 }
63
64 protected get message () {
65 return this.payload.message
66 }
67}
diff --git a/server/lib/notifier/shared/abuse/abuse-state-change-for-reporter.ts b/server/lib/notifier/shared/abuse/abuse-state-change-for-reporter.ts
new file mode 100644
index 000000000..968b5bca9
--- /dev/null
+++ b/server/lib/notifier/shared/abuse/abuse-state-change-for-reporter.ts
@@ -0,0 +1,74 @@
1import { logger } from '@server/helpers/logger'
2import { WEBSERVER } from '@server/initializers/constants'
3import { getAbuseTargetUrl } from '@server/lib/activitypub/url'
4import { UserModel } from '@server/models/user/user'
5import { UserNotificationModel } from '@server/models/user/user-notification'
6import { MAbuseFull, MUserDefault, MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/types/models'
7import { AbuseState, UserNotificationType } from '@shared/models'
8import { AbstractNotification } from '../common/abstract-notification'
9
10export class AbuseStateChangeForReporter extends AbstractNotification <MAbuseFull> {
11
12 private user: MUserDefault
13
14 async prepare () {
15 const reporter = this.abuse.ReporterAccount
16 if (reporter.isOwned() !== true) return
17
18 this.user = await UserModel.loadByAccountActorId(this.abuse.ReporterAccount.actorId)
19 }
20
21 log () {
22 logger.info('Notifying reporter of abuse % of state change.', getAbuseTargetUrl(this.abuse))
23 }
24
25 getSetting (user: MUserWithNotificationSetting) {
26 return user.NotificationSetting.abuseStateChange
27 }
28
29 getTargetUsers () {
30 if (!this.user) return []
31
32 return [ this.user ]
33 }
34
35 async createNotification (user: MUserWithNotificationSetting) {
36 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
37 type: UserNotificationType.ABUSE_STATE_CHANGE,
38 userId: user.id,
39 abuseId: this.abuse.id
40 })
41 notification.Abuse = this.abuse
42
43 return notification
44 }
45
46 createEmail (to: string) {
47 const text = this.abuse.state === AbuseState.ACCEPTED
48 ? 'Report #' + this.abuse.id + ' has been accepted'
49 : 'Report #' + this.abuse.id + ' has been rejected'
50
51 const abuseUrl = WEBSERVER.URL + '/my-account/abuses?search=%23' + this.abuse.id
52
53 const action = {
54 text,
55 url: abuseUrl
56 }
57
58 return {
59 template: 'abuse-state-change',
60 to,
61 subject: text,
62 locals: {
63 action,
64 abuseId: this.abuse.id,
65 abuseUrl,
66 isAccepted: this.abuse.state === AbuseState.ACCEPTED
67 }
68 }
69 }
70
71 private get abuse () {
72 return this.payload
73 }
74}
diff --git a/server/lib/notifier/shared/abuse/index.ts b/server/lib/notifier/shared/abuse/index.ts
new file mode 100644
index 000000000..7b54c5591
--- /dev/null
+++ b/server/lib/notifier/shared/abuse/index.ts
@@ -0,0 +1,4 @@
1export * from './abuse-state-change-for-reporter'
2export * from './new-abuse-for-moderators'
3export * from './new-abuse-message-for-reporter'
4export * from './new-abuse-message-for-moderators'
diff --git a/server/lib/notifier/shared/abuse/new-abuse-for-moderators.ts b/server/lib/notifier/shared/abuse/new-abuse-for-moderators.ts
new file mode 100644
index 000000000..c3c7c5515
--- /dev/null
+++ b/server/lib/notifier/shared/abuse/new-abuse-for-moderators.ts
@@ -0,0 +1,119 @@
1import { logger } from '@server/helpers/logger'
2import { WEBSERVER } from '@server/initializers/constants'
3import { getAbuseTargetUrl } from '@server/lib/activitypub/url'
4import { UserModel } from '@server/models/user/user'
5import { UserNotificationModel } from '@server/models/user/user-notification'
6import { MAbuseFull, MUserDefault, MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/types/models'
7import { UserAbuse, UserNotificationType, UserRight } from '@shared/models'
8import { AbstractNotification } from '../common/abstract-notification'
9
10export type NewAbusePayload = { abuse: UserAbuse, abuseInstance: MAbuseFull, reporter: string }
11
12export class NewAbuseForModerators extends AbstractNotification <NewAbusePayload> {
13 private moderators: MUserDefault[]
14
15 async prepare () {
16 this.moderators = await UserModel.listWithRight(UserRight.MANAGE_ABUSES)
17 }
18
19 log () {
20 logger.info('Notifying %s user/moderators of new abuse %s.', this.moderators.length, getAbuseTargetUrl(this.payload.abuseInstance))
21 }
22
23 getSetting (user: MUserWithNotificationSetting) {
24 return user.NotificationSetting.abuseAsModerator
25 }
26
27 getTargetUsers () {
28 return this.moderators
29 }
30
31 async createNotification (user: MUserWithNotificationSetting) {
32 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
33 type: UserNotificationType.NEW_ABUSE_FOR_MODERATORS,
34 userId: user.id,
35 abuseId: this.payload.abuseInstance.id
36 })
37 notification.Abuse = this.payload.abuseInstance
38
39 return notification
40 }
41
42 createEmail (to: string) {
43 const abuseInstance = this.payload.abuseInstance
44
45 if (abuseInstance.VideoAbuse) return this.createVideoAbuseEmail(to)
46 if (abuseInstance.VideoCommentAbuse) return this.createCommentAbuseEmail(to)
47
48 return this.createAccountAbuseEmail(to)
49 }
50
51 private createVideoAbuseEmail (to: string) {
52 const video = this.payload.abuseInstance.VideoAbuse.Video
53 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
54
55 return {
56 template: 'video-abuse-new',
57 to,
58 subject: `New video abuse report from ${this.payload.reporter}`,
59 locals: {
60 videoUrl,
61 isLocal: video.remote === false,
62 videoCreatedAt: new Date(video.createdAt).toLocaleString(),
63 videoPublishedAt: new Date(video.publishedAt).toLocaleString(),
64 videoName: video.name,
65 reason: this.payload.abuse.reason,
66 videoChannel: this.payload.abuse.video.channel,
67 reporter: this.payload.reporter,
68 action: this.buildEmailAction()
69 }
70 }
71 }
72
73 private createCommentAbuseEmail (to: string) {
74 const comment = this.payload.abuseInstance.VideoCommentAbuse.VideoComment
75 const commentUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath() + ';threadId=' + comment.getThreadId()
76
77 return {
78 template: 'video-comment-abuse-new',
79 to,
80 subject: `New comment abuse report from ${this.payload.reporter}`,
81 locals: {
82 commentUrl,
83 videoName: comment.Video.name,
84 isLocal: comment.isOwned(),
85 commentCreatedAt: new Date(comment.createdAt).toLocaleString(),
86 reason: this.payload.abuse.reason,
87 flaggedAccount: this.payload.abuseInstance.FlaggedAccount.getDisplayName(),
88 reporter: this.payload.reporter,
89 action: this.buildEmailAction()
90 }
91 }
92 }
93
94 private createAccountAbuseEmail (to: string) {
95 const account = this.payload.abuseInstance.FlaggedAccount
96 const accountUrl = account.getClientUrl()
97
98 return {
99 template: 'account-abuse-new',
100 to,
101 subject: `New account abuse report from ${this.payload.reporter}`,
102 locals: {
103 accountUrl,
104 accountDisplayName: account.getDisplayName(),
105 isLocal: account.isOwned(),
106 reason: this.payload.abuse.reason,
107 reporter: this.payload.reporter,
108 action: this.buildEmailAction()
109 }
110 }
111 }
112
113 private buildEmailAction () {
114 return {
115 text: 'View report #' + this.payload.abuseInstance.id,
116 url: WEBSERVER.URL + '/admin/moderation/abuses/list?search=%23' + this.payload.abuseInstance.id
117 }
118 }
119}
diff --git a/server/lib/notifier/shared/abuse/new-abuse-message-for-moderators.ts b/server/lib/notifier/shared/abuse/new-abuse-message-for-moderators.ts
new file mode 100644
index 000000000..9d0629690
--- /dev/null
+++ b/server/lib/notifier/shared/abuse/new-abuse-message-for-moderators.ts
@@ -0,0 +1,32 @@
1import { logger } from '@server/helpers/logger'
2import { getAbuseTargetUrl } from '@server/lib/activitypub/url'
3import { UserModel } from '@server/models/user/user'
4import { MUserDefault } from '@server/types/models'
5import { UserRight } from '@shared/models'
6import { AbstractNewAbuseMessage } from './abstract-new-abuse-message'
7
8export class NewAbuseMessageForModerators extends AbstractNewAbuseMessage {
9 private moderators: MUserDefault[]
10
11 async prepare () {
12 this.moderators = await UserModel.listWithRight(UserRight.MANAGE_ABUSES)
13
14 // Don't notify my own message
15 this.moderators = this.moderators.filter(m => m.Account.id !== this.message.accountId)
16 if (this.moderators.length === 0) return
17
18 await this.loadMessageAccount()
19 }
20
21 log () {
22 logger.info('Notifying moderators of new abuse message on %s.', getAbuseTargetUrl(this.abuse))
23 }
24
25 getTargetUsers () {
26 return this.moderators
27 }
28
29 createEmail (to: string) {
30 return this.createEmailFor(to, 'moderator')
31 }
32}
diff --git a/server/lib/notifier/shared/abuse/new-abuse-message-for-reporter.ts b/server/lib/notifier/shared/abuse/new-abuse-message-for-reporter.ts
new file mode 100644
index 000000000..c5bbb5447
--- /dev/null
+++ b/server/lib/notifier/shared/abuse/new-abuse-message-for-reporter.ts
@@ -0,0 +1,36 @@
1import { logger } from '@server/helpers/logger'
2import { getAbuseTargetUrl } from '@server/lib/activitypub/url'
3import { UserModel } from '@server/models/user/user'
4import { MUserDefault } from '@server/types/models'
5import { AbstractNewAbuseMessage } from './abstract-new-abuse-message'
6
7export class NewAbuseMessageForReporter extends AbstractNewAbuseMessage {
8 private reporter: MUserDefault
9
10 async prepare () {
11 // Only notify our users
12 if (this.abuse.ReporterAccount.isOwned() !== true) return
13
14 await this.loadMessageAccount()
15
16 const reporter = await UserModel.loadByAccountActorId(this.abuse.ReporterAccount.actorId)
17 // Don't notify my own message
18 if (reporter.Account.id === this.message.accountId) return
19
20 this.reporter = reporter
21 }
22
23 log () {
24 logger.info('Notifying reporter of new abuse message on %s.', getAbuseTargetUrl(this.abuse))
25 }
26
27 getTargetUsers () {
28 if (!this.reporter) return []
29
30 return [ this.reporter ]
31 }
32
33 createEmail (to: string) {
34 return this.createEmailFor(to, 'reporter')
35 }
36}
diff --git a/server/lib/notifier/shared/blacklist/index.ts b/server/lib/notifier/shared/blacklist/index.ts
new file mode 100644
index 000000000..2f98d88ae
--- /dev/null
+++ b/server/lib/notifier/shared/blacklist/index.ts
@@ -0,0 +1,3 @@
1export * from './new-auto-blacklist-for-moderators'
2export * from './new-blacklist-for-owner'
3export * from './unblacklist-for-owner'
diff --git a/server/lib/notifier/shared/blacklist/new-auto-blacklist-for-moderators.ts b/server/lib/notifier/shared/blacklist/new-auto-blacklist-for-moderators.ts
new file mode 100644
index 000000000..a92a49a0c
--- /dev/null
+++ b/server/lib/notifier/shared/blacklist/new-auto-blacklist-for-moderators.ts
@@ -0,0 +1,60 @@
1import { logger } from '@server/helpers/logger'
2import { WEBSERVER } from '@server/initializers/constants'
3import { UserModel } from '@server/models/user/user'
4import { UserNotificationModel } from '@server/models/user/user-notification'
5import { VideoChannelModel } from '@server/models/video/video-channel'
6import { MUserDefault, MUserWithNotificationSetting, MVideoBlacklistLightVideo, UserNotificationModelForApi } from '@server/types/models'
7import { UserNotificationType, UserRight } from '@shared/models'
8import { AbstractNotification } from '../common/abstract-notification'
9
10export class NewAutoBlacklistForModerators extends AbstractNotification <MVideoBlacklistLightVideo> {
11 private moderators: MUserDefault[]
12
13 async prepare () {
14 this.moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_BLACKLIST)
15 }
16
17 log () {
18 logger.info('Notifying %s moderators of video auto-blacklist %s.', this.moderators.length, this.payload.Video.url)
19 }
20
21 getSetting (user: MUserWithNotificationSetting) {
22 return user.NotificationSetting.videoAutoBlacklistAsModerator
23 }
24
25 getTargetUsers () {
26 return this.moderators
27 }
28
29 async createNotification (user: MUserWithNotificationSetting) {
30 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
31 type: UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS,
32 userId: user.id,
33 videoBlacklistId: this.payload.id
34 })
35 notification.VideoBlacklist = this.payload
36
37 return notification
38 }
39
40 async createEmail (to: string) {
41 const videoAutoBlacklistUrl = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list'
42 const videoUrl = WEBSERVER.URL + this.payload.Video.getWatchStaticPath()
43 const channel = await VideoChannelModel.loadAndPopulateAccount(this.payload.Video.channelId)
44
45 return {
46 template: 'video-auto-blacklist-new',
47 to,
48 subject: 'A new video is pending moderation',
49 locals: {
50 channel: channel.toFormattedSummaryJSON(),
51 videoUrl,
52 videoName: this.payload.Video.name,
53 action: {
54 text: 'Review autoblacklist',
55 url: videoAutoBlacklistUrl
56 }
57 }
58 }
59 }
60}
diff --git a/server/lib/notifier/shared/blacklist/new-blacklist-for-owner.ts b/server/lib/notifier/shared/blacklist/new-blacklist-for-owner.ts
new file mode 100644
index 000000000..45bc30eb2
--- /dev/null
+++ b/server/lib/notifier/shared/blacklist/new-blacklist-for-owner.ts
@@ -0,0 +1,58 @@
1import { logger } from '@server/helpers/logger'
2import { CONFIG } from '@server/initializers/config'
3import { WEBSERVER } from '@server/initializers/constants'
4import { UserModel } from '@server/models/user/user'
5import { UserNotificationModel } from '@server/models/user/user-notification'
6import { MUserDefault, MUserWithNotificationSetting, MVideoBlacklistVideo, UserNotificationModelForApi } from '@server/types/models'
7import { UserNotificationType } from '@shared/models'
8import { AbstractNotification } from '../common/abstract-notification'
9
10export class NewBlacklistForOwner extends AbstractNotification <MVideoBlacklistVideo> {
11 private user: MUserDefault
12
13 async prepare () {
14 this.user = await UserModel.loadByVideoId(this.payload.videoId)
15 }
16
17 log () {
18 logger.info('Notifying user %s that its video %s has been blacklisted.', this.user.username, this.payload.Video.url)
19 }
20
21 getSetting (user: MUserWithNotificationSetting) {
22 return user.NotificationSetting.blacklistOnMyVideo
23 }
24
25 getTargetUsers () {
26 if (!this.user) return []
27
28 return [ this.user ]
29 }
30
31 async createNotification (user: MUserWithNotificationSetting) {
32 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
33 type: UserNotificationType.BLACKLIST_ON_MY_VIDEO,
34 userId: user.id,
35 videoBlacklistId: this.payload.id
36 })
37 notification.VideoBlacklist = this.payload
38
39 return notification
40 }
41
42 createEmail (to: string) {
43 const videoName = this.payload.Video.name
44 const videoUrl = WEBSERVER.URL + this.payload.Video.getWatchStaticPath()
45
46 const reasonString = this.payload.reason ? ` for the following reason: ${this.payload.reason}` : ''
47 const blockedString = `Your video ${videoName} (${videoUrl} on ${CONFIG.INSTANCE.NAME} has been blacklisted${reasonString}.`
48
49 return {
50 to,
51 subject: `Video ${videoName} blacklisted`,
52 text: blockedString,
53 locals: {
54 title: 'Your video was blacklisted'
55 }
56 }
57 }
58}
diff --git a/server/lib/notifier/shared/blacklist/unblacklist-for-owner.ts b/server/lib/notifier/shared/blacklist/unblacklist-for-owner.ts
new file mode 100644
index 000000000..21f5a1c2d
--- /dev/null
+++ b/server/lib/notifier/shared/blacklist/unblacklist-for-owner.ts
@@ -0,0 +1,55 @@
1import { logger } from '@server/helpers/logger'
2import { CONFIG } from '@server/initializers/config'
3import { WEBSERVER } from '@server/initializers/constants'
4import { UserModel } from '@server/models/user/user'
5import { UserNotificationModel } from '@server/models/user/user-notification'
6import { MUserDefault, MUserWithNotificationSetting, MVideoFullLight, UserNotificationModelForApi } from '@server/types/models'
7import { UserNotificationType } from '@shared/models'
8import { AbstractNotification } from '../common/abstract-notification'
9
10export class UnblacklistForOwner extends AbstractNotification <MVideoFullLight> {
11 private user: MUserDefault
12
13 async prepare () {
14 this.user = await UserModel.loadByVideoId(this.payload.id)
15 }
16
17 log () {
18 logger.info('Notifying user %s that its video %s has been unblacklisted.', this.user.username, this.payload.url)
19 }
20
21 getSetting (user: MUserWithNotificationSetting) {
22 return user.NotificationSetting.blacklistOnMyVideo
23 }
24
25 getTargetUsers () {
26 if (!this.user) return []
27
28 return [ this.user ]
29 }
30
31 async createNotification (user: MUserWithNotificationSetting) {
32 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
33 type: UserNotificationType.UNBLACKLIST_ON_MY_VIDEO,
34 userId: user.id,
35 videoId: this.payload.id
36 })
37 notification.Video = this.payload
38
39 return notification
40 }
41
42 createEmail (to: string) {
43 const video = this.payload
44 const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
45
46 return {
47 to,
48 subject: `Video ${video.name} unblacklisted`,
49 text: `Your video "${video.name}" (${videoUrl}) on ${CONFIG.INSTANCE.NAME} has been unblacklisted.`,
50 locals: {
51 title: 'Your video was unblacklisted'
52 }
53 }
54 }
55}
diff --git a/server/lib/notifier/shared/comment/comment-mention.ts b/server/lib/notifier/shared/comment/comment-mention.ts
new file mode 100644
index 000000000..4f84d8dea
--- /dev/null
+++ b/server/lib/notifier/shared/comment/comment-mention.ts
@@ -0,0 +1,111 @@
1import { logger } from '@server/helpers/logger'
2import { toSafeHtml } from '@server/helpers/markdown'
3import { WEBSERVER } from '@server/initializers/constants'
4import { AccountBlocklistModel } from '@server/models/account/account-blocklist'
5import { getServerActor } from '@server/models/application/application'
6import { ServerBlocklistModel } from '@server/models/server/server-blocklist'
7import { UserModel } from '@server/models/user/user'
8import { UserNotificationModel } from '@server/models/user/user-notification'
9import {
10 MCommentOwnerVideo,
11 MUserDefault,
12 MUserNotifSettingAccount,
13 MUserWithNotificationSetting,
14 UserNotificationModelForApi
15} from '@server/types/models'
16import { UserNotificationSettingValue, UserNotificationType } from '@shared/models'
17import { AbstractNotification } from '../common'
18
19export class CommentMention extends AbstractNotification <MCommentOwnerVideo, MUserNotifSettingAccount> {
20 private users: MUserDefault[]
21
22 private serverAccountId: number
23
24 private accountMutedHash: { [ id: number ]: boolean }
25 private instanceMutedHash: { [ id: number ]: boolean }
26
27 async prepare () {
28 const extractedUsernames = this.payload.extractMentions()
29 logger.debug(
30 'Extracted %d username from comment %s.', extractedUsernames.length, this.payload.url,
31 { usernames: extractedUsernames, text: this.payload.text }
32 )
33
34 this.users = await UserModel.listByUsernames(extractedUsernames)
35
36 if (this.payload.Video.isOwned()) {
37 const userException = await UserModel.loadByVideoId(this.payload.videoId)
38 this.users = this.users.filter(u => u.id !== userException.id)
39 }
40
41 // Don't notify if I mentioned myself
42 this.users = this.users.filter(u => u.Account.id !== this.payload.accountId)
43
44 if (this.users.length === 0) return
45
46 this.serverAccountId = (await getServerActor()).Account.id
47
48 const sourceAccounts = this.users.map(u => u.Account.id).concat([ this.serverAccountId ])
49
50 this.accountMutedHash = await AccountBlocklistModel.isAccountMutedByMulti(sourceAccounts, this.payload.accountId)
51 this.instanceMutedHash = await ServerBlocklistModel.isServerMutedByMulti(sourceAccounts, this.payload.Account.Actor.serverId)
52 }
53
54 log () {
55 logger.info('Notifying %d users of new comment %s.', this.users.length, this.payload.url)
56 }
57
58 getSetting (user: MUserNotifSettingAccount) {
59 const accountId = user.Account.id
60 if (
61 this.accountMutedHash[accountId] === true || this.instanceMutedHash[accountId] === true ||
62 this.accountMutedHash[this.serverAccountId] === true || this.instanceMutedHash[this.serverAccountId] === true
63 ) {
64 return UserNotificationSettingValue.NONE
65 }
66
67 return user.NotificationSetting.commentMention
68 }
69
70 getTargetUsers () {
71 return this.users
72 }
73
74 async createNotification (user: MUserWithNotificationSetting) {
75 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
76 type: UserNotificationType.COMMENT_MENTION,
77 userId: user.id,
78 commentId: this.payload.id
79 })
80 notification.Comment = this.payload
81
82 return notification
83 }
84
85 createEmail (to: string) {
86 const comment = this.payload
87
88 const accountName = comment.Account.getDisplayName()
89 const video = comment.Video
90 const videoUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath()
91 const commentUrl = WEBSERVER.URL + comment.getCommentStaticPath()
92 const commentHtml = toSafeHtml(comment.text)
93
94 return {
95 template: 'video-comment-mention',
96 to,
97 subject: 'Mention on video ' + video.name,
98 locals: {
99 comment,
100 commentHtml,
101 video,
102 videoUrl,
103 accountName,
104 action: {
105 text: 'View comment',
106 url: commentUrl
107 }
108 }
109 }
110 }
111}
diff --git a/server/lib/notifier/shared/comment/index.ts b/server/lib/notifier/shared/comment/index.ts
new file mode 100644
index 000000000..ae01a9646
--- /dev/null
+++ b/server/lib/notifier/shared/comment/index.ts
@@ -0,0 +1,2 @@
1export * from './comment-mention'
2export * from './new-comment-for-video-owner'
diff --git a/server/lib/notifier/shared/comment/new-comment-for-video-owner.ts b/server/lib/notifier/shared/comment/new-comment-for-video-owner.ts
new file mode 100644
index 000000000..b76fc15bf
--- /dev/null
+++ b/server/lib/notifier/shared/comment/new-comment-for-video-owner.ts
@@ -0,0 +1,76 @@
1import { logger } from '@server/helpers/logger'
2import { toSafeHtml } from '@server/helpers/markdown'
3import { WEBSERVER } from '@server/initializers/constants'
4import { isBlockedByServerOrAccount } from '@server/lib/blocklist'
5import { UserModel } from '@server/models/user/user'
6import { UserNotificationModel } from '@server/models/user/user-notification'
7import { MCommentOwnerVideo, MUserDefault, MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/types/models'
8import { UserNotificationType } from '@shared/models'
9import { AbstractNotification } from '../common/abstract-notification'
10
11export class NewCommentForVideoOwner extends AbstractNotification <MCommentOwnerVideo> {
12 private user: MUserDefault
13
14 async prepare () {
15 this.user = await UserModel.loadByVideoId(this.payload.videoId)
16 }
17
18 log () {
19 logger.info('Notifying owner of a video %s of new comment %s.', this.user.username, this.payload.url)
20 }
21
22 isDisabled () {
23 if (this.payload.Video.isOwned() === false) return true
24
25 // Not our user or user comments its own video
26 if (!this.user || this.payload.Account.userId === this.user.id) return true
27
28 return isBlockedByServerOrAccount(this.payload.Account, this.user.Account)
29 }
30
31 getSetting (user: MUserWithNotificationSetting) {
32 return user.NotificationSetting.newCommentOnMyVideo
33 }
34
35 getTargetUsers () {
36 if (!this.user) return []
37
38 return [ this.user ]
39 }
40
41 async createNotification (user: MUserWithNotificationSetting) {
42 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
43 type: UserNotificationType.NEW_COMMENT_ON_MY_VIDEO,
44 userId: user.id,
45 commentId: this.payload.id
46 })
47 notification.Comment = this.payload
48
49 return notification
50 }
51
52 createEmail (to: string) {
53 const video = this.payload.Video
54 const videoUrl = WEBSERVER.URL + this.payload.Video.getWatchStaticPath()
55 const commentUrl = WEBSERVER.URL + this.payload.getCommentStaticPath()
56 const commentHtml = toSafeHtml(this.payload.text)
57
58 return {
59 template: 'video-comment-new',
60 to,
61 subject: 'New comment on your video ' + video.name,
62 locals: {
63 accountName: this.payload.Account.getDisplayName(),
64 accountUrl: this.payload.Account.Actor.url,
65 comment: this.payload,
66 commentHtml,
67 video,
68 videoUrl,
69 action: {
70 text: 'View comment',
71 url: commentUrl
72 }
73 }
74 }
75 }
76}
diff --git a/server/lib/notifier/shared/common/abstract-notification.ts b/server/lib/notifier/shared/common/abstract-notification.ts
new file mode 100644
index 000000000..53e2e02d5
--- /dev/null
+++ b/server/lib/notifier/shared/common/abstract-notification.ts
@@ -0,0 +1,23 @@
1import { MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/types/models'
2import { EmailPayload, UserNotificationSettingValue } from '@shared/models'
3
4export abstract class AbstractNotification <T, U = MUserWithNotificationSetting> {
5
6 constructor (protected readonly payload: T) {
7
8 }
9
10 abstract prepare (): Promise<void>
11 abstract log (): void
12
13 abstract getSetting (user: U): UserNotificationSettingValue
14 abstract getTargetUsers (): U[]
15
16 abstract createNotification (user: U): Promise<UserNotificationModelForApi>
17 abstract createEmail (to: string): EmailPayload | Promise<EmailPayload>
18
19 isDisabled (): boolean | Promise<boolean> {
20 return false
21 }
22
23}
diff --git a/server/lib/notifier/shared/common/index.ts b/server/lib/notifier/shared/common/index.ts
new file mode 100644
index 000000000..0b2570278
--- /dev/null
+++ b/server/lib/notifier/shared/common/index.ts
@@ -0,0 +1 @@
export * from './abstract-notification'
diff --git a/server/lib/notifier/shared/follow/auto-follow-for-instance.ts b/server/lib/notifier/shared/follow/auto-follow-for-instance.ts
new file mode 100644
index 000000000..16cc62984
--- /dev/null
+++ b/server/lib/notifier/shared/follow/auto-follow-for-instance.ts
@@ -0,0 +1,51 @@
1import { logger } from '@server/helpers/logger'
2import { UserModel } from '@server/models/user/user'
3import { UserNotificationModel } from '@server/models/user/user-notification'
4import { MActorFollowFull, MUserDefault, MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/types/models'
5import { UserNotificationType, UserRight } from '@shared/models'
6import { AbstractNotification } from '../common/abstract-notification'
7
8export class AutoFollowForInstance extends AbstractNotification <MActorFollowFull> {
9 private admins: MUserDefault[]
10
11 async prepare () {
12 this.admins = await UserModel.listWithRight(UserRight.MANAGE_SERVER_FOLLOW)
13 }
14
15 log () {
16 logger.info('Notifying %d administrators of auto instance following: %s.', this.admins.length, this.actorFollow.ActorFollowing.url)
17 }
18
19 getSetting (user: MUserWithNotificationSetting) {
20 return user.NotificationSetting.autoInstanceFollowing
21 }
22
23 getTargetUsers () {
24 return this.admins
25 }
26
27 async createNotification (user: MUserWithNotificationSetting) {
28 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
29 type: UserNotificationType.AUTO_INSTANCE_FOLLOWING,
30 userId: user.id,
31 actorFollowId: this.actorFollow.id
32 })
33 notification.ActorFollow = this.actorFollow
34
35 return notification
36 }
37
38 async createEmail (to: string) {
39 const instanceUrl = this.actorFollow.ActorFollowing.url
40
41 return {
42 to,
43 subject: 'Auto instance following',
44 text: `Your instance automatically followed a new instance: <a href="${instanceUrl}">${instanceUrl}</a>.`
45 }
46 }
47
48 private get actorFollow () {
49 return this.payload
50 }
51}
diff --git a/server/lib/notifier/shared/follow/follow-for-instance.ts b/server/lib/notifier/shared/follow/follow-for-instance.ts
new file mode 100644
index 000000000..9ab269cf1
--- /dev/null
+++ b/server/lib/notifier/shared/follow/follow-for-instance.ts
@@ -0,0 +1,68 @@
1import { logger } from '@server/helpers/logger'
2import { WEBSERVER } from '@server/initializers/constants'
3import { isBlockedByServerOrAccount } from '@server/lib/blocklist'
4import { UserModel } from '@server/models/user/user'
5import { UserNotificationModel } from '@server/models/user/user-notification'
6import { MActorFollowFull, MUserDefault, MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/types/models'
7import { UserNotificationType, UserRight } from '@shared/models'
8import { AbstractNotification } from '../common/abstract-notification'
9
10export class FollowForInstance extends AbstractNotification <MActorFollowFull> {
11 private admins: MUserDefault[]
12
13 async prepare () {
14 this.admins = await UserModel.listWithRight(UserRight.MANAGE_SERVER_FOLLOW)
15 }
16
17 isDisabled () {
18 const follower = Object.assign(this.actorFollow.ActorFollower.Account, { Actor: this.actorFollow.ActorFollower })
19
20 return isBlockedByServerOrAccount(follower)
21 }
22
23 log () {
24 logger.info('Notifying %d administrators of new instance follower: %s.', this.admins.length, this.actorFollow.ActorFollower.url)
25 }
26
27 getSetting (user: MUserWithNotificationSetting) {
28 return user.NotificationSetting.newInstanceFollower
29 }
30
31 getTargetUsers () {
32 return this.admins
33 }
34
35 async createNotification (user: MUserWithNotificationSetting) {
36 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
37 type: UserNotificationType.NEW_INSTANCE_FOLLOWER,
38 userId: user.id,
39 actorFollowId: this.actorFollow.id
40 })
41 notification.ActorFollow = this.actorFollow
42
43 return notification
44 }
45
46 async createEmail (to: string) {
47 const awaitingApproval = this.actorFollow.state === 'pending'
48 ? ' awaiting manual approval.'
49 : ''
50
51 return {
52 to,
53 subject: 'New instance follower',
54 text: `Your instance has a new follower: ${this.actorFollow.ActorFollower.url}${awaitingApproval}.`,
55 locals: {
56 title: 'New instance follower',
57 action: {
58 text: 'Review followers',
59 url: WEBSERVER.URL + '/admin/follows/followers-list'
60 }
61 }
62 }
63 }
64
65 private get actorFollow () {
66 return this.payload
67 }
68}
diff --git a/server/lib/notifier/shared/follow/follow-for-user.ts b/server/lib/notifier/shared/follow/follow-for-user.ts
new file mode 100644
index 000000000..2d0f675a8
--- /dev/null
+++ b/server/lib/notifier/shared/follow/follow-for-user.ts
@@ -0,0 +1,82 @@
1import { logger } from '@server/helpers/logger'
2import { isBlockedByServerOrAccount } from '@server/lib/blocklist'
3import { UserModel } from '@server/models/user/user'
4import { UserNotificationModel } from '@server/models/user/user-notification'
5import { MActorFollowFull, MUserDefault, MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/types/models'
6import { UserNotificationType } from '@shared/models'
7import { AbstractNotification } from '../common/abstract-notification'
8
9export class FollowForUser extends AbstractNotification <MActorFollowFull> {
10 private followType: 'account' | 'channel'
11 private user: MUserDefault
12
13 async prepare () {
14 // Account follows one of our account?
15 this.followType = 'channel'
16 this.user = await UserModel.loadByChannelActorId(this.actorFollow.ActorFollowing.id)
17
18 // Account follows one of our channel?
19 if (!this.user) {
20 this.user = await UserModel.loadByAccountActorId(this.actorFollow.ActorFollowing.id)
21 this.followType = 'account'
22 }
23 }
24
25 async isDisabled () {
26 if (this.payload.ActorFollowing.isOwned() === false) return true
27
28 const followerAccount = this.actorFollow.ActorFollower.Account
29 const followerAccountWithActor = Object.assign(followerAccount, { Actor: this.actorFollow.ActorFollower })
30
31 return isBlockedByServerOrAccount(followerAccountWithActor, this.user.Account)
32 }
33
34 log () {
35 logger.info('Notifying user %s of new follower: %s.', this.user.username, this.actorFollow.ActorFollower.Account.getDisplayName())
36 }
37
38 getSetting (user: MUserWithNotificationSetting) {
39 return user.NotificationSetting.newFollow
40 }
41
42 getTargetUsers () {
43 if (!this.user) return []
44
45 return [ this.user ]
46 }
47
48 async createNotification (user: MUserWithNotificationSetting) {
49 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
50 type: UserNotificationType.NEW_FOLLOW,
51 userId: user.id,
52 actorFollowId: this.actorFollow.id
53 })
54 notification.ActorFollow = this.actorFollow
55
56 return notification
57 }
58
59 async createEmail (to: string) {
60 const following = this.actorFollow.ActorFollowing
61 const follower = this.actorFollow.ActorFollower
62
63 const followingName = (following.VideoChannel || following.Account).getDisplayName()
64
65 return {
66 template: 'follower-on-channel',
67 to,
68 subject: `New follower on your channel ${followingName}`,
69 locals: {
70 followerName: follower.Account.getDisplayName(),
71 followerUrl: follower.url,
72 followingName,
73 followingUrl: following.url,
74 followType: this.followType
75 }
76 }
77 }
78
79 private get actorFollow () {
80 return this.payload
81 }
82}
diff --git a/server/lib/notifier/shared/follow/index.ts b/server/lib/notifier/shared/follow/index.ts
new file mode 100644
index 000000000..27f5289d9
--- /dev/null
+++ b/server/lib/notifier/shared/follow/index.ts
@@ -0,0 +1,3 @@
1export * from './auto-follow-for-instance'
2export * from './follow-for-instance'
3export * from './follow-for-user'
diff --git a/server/lib/notifier/shared/index.ts b/server/lib/notifier/shared/index.ts
new file mode 100644
index 000000000..cc3ce8c7c
--- /dev/null
+++ b/server/lib/notifier/shared/index.ts
@@ -0,0 +1,7 @@
1export * from './abuse'
2export * from './blacklist'
3export * from './comment'
4export * from './common'
5export * from './follow'
6export * from './instance'
7export * from './video-publication'
diff --git a/server/lib/notifier/shared/instance/index.ts b/server/lib/notifier/shared/instance/index.ts
new file mode 100644
index 000000000..c3bb22aec
--- /dev/null
+++ b/server/lib/notifier/shared/instance/index.ts
@@ -0,0 +1,3 @@
1export * from './new-peertube-version-for-admins'
2export * from './new-plugin-version-for-admins'
3export * from './registration-for-moderators'
diff --git a/server/lib/notifier/shared/instance/new-peertube-version-for-admins.ts b/server/lib/notifier/shared/instance/new-peertube-version-for-admins.ts
new file mode 100644
index 000000000..ab5bfb1ac
--- /dev/null
+++ b/server/lib/notifier/shared/instance/new-peertube-version-for-admins.ts
@@ -0,0 +1,54 @@
1import { logger } from '@server/helpers/logger'
2import { UserModel } from '@server/models/user/user'
3import { UserNotificationModel } from '@server/models/user/user-notification'
4import { MApplication, MUserDefault, MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/types/models'
5import { UserNotificationType, UserRight } from '@shared/models'
6import { AbstractNotification } from '../common/abstract-notification'
7
8export type NewPeerTubeVersionForAdminsPayload = {
9 application: MApplication
10 latestVersion: string
11}
12
13export class NewPeerTubeVersionForAdmins extends AbstractNotification <NewPeerTubeVersionForAdminsPayload> {
14 private admins: MUserDefault[]
15
16 async prepare () {
17 // Use the debug right to know who is an administrator
18 this.admins = await UserModel.listWithRight(UserRight.MANAGE_DEBUG)
19 }
20
21 log () {
22 logger.info('Notifying %s admins of new PeerTube version %s.', this.admins.length, this.payload.latestVersion)
23 }
24
25 getSetting (user: MUserWithNotificationSetting) {
26 return user.NotificationSetting.newPeerTubeVersion
27 }
28
29 getTargetUsers () {
30 return this.admins
31 }
32
33 async createNotification (user: MUserWithNotificationSetting) {
34 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
35 type: UserNotificationType.NEW_PEERTUBE_VERSION,
36 userId: user.id,
37 applicationId: this.payload.application.id
38 })
39 notification.Application = this.payload.application
40
41 return notification
42 }
43
44 async createEmail (to: string) {
45 return {
46 to,
47 template: 'peertube-version-new',
48 subject: `A new PeerTube version is available: ${this.payload.latestVersion}`,
49 locals: {
50 latestVersion: this.payload.latestVersion
51 }
52 }
53 }
54}
diff --git a/server/lib/notifier/shared/instance/new-plugin-version-for-admins.ts b/server/lib/notifier/shared/instance/new-plugin-version-for-admins.ts
new file mode 100644
index 000000000..e5e456a70
--- /dev/null
+++ b/server/lib/notifier/shared/instance/new-plugin-version-for-admins.ts
@@ -0,0 +1,58 @@
1import { logger } from '@server/helpers/logger'
2import { WEBSERVER } from '@server/initializers/constants'
3import { UserModel } from '@server/models/user/user'
4import { UserNotificationModel } from '@server/models/user/user-notification'
5import { MPlugin, MUserDefault, MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/types/models'
6import { UserNotificationType, UserRight } from '@shared/models'
7import { AbstractNotification } from '../common/abstract-notification'
8
9export class NewPluginVersionForAdmins extends AbstractNotification <MPlugin> {
10 private admins: MUserDefault[]
11
12 async prepare () {
13 // Use the debug right to know who is an administrator
14 this.admins = await UserModel.listWithRight(UserRight.MANAGE_DEBUG)
15 }
16
17 log () {
18 logger.info('Notifying %s admins of new PeerTube version %s.', this.admins.length, this.payload.latestVersion)
19 }
20
21 getSetting (user: MUserWithNotificationSetting) {
22 return user.NotificationSetting.newPluginVersion
23 }
24
25 getTargetUsers () {
26 return this.admins
27 }
28
29 async createNotification (user: MUserWithNotificationSetting) {
30 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
31 type: UserNotificationType.NEW_PLUGIN_VERSION,
32 userId: user.id,
33 pluginId: this.plugin.id
34 })
35 notification.Plugin = this.plugin
36
37 return notification
38 }
39
40 async createEmail (to: string) {
41 const pluginUrl = WEBSERVER.URL + '/admin/plugins/list-installed?pluginType=' + this.plugin.type
42
43 return {
44 to,
45 template: 'plugin-version-new',
46 subject: `A new plugin/theme version is available: ${this.plugin.name}@${this.plugin.latestVersion}`,
47 locals: {
48 pluginName: this.plugin.name,
49 latestVersion: this.plugin.latestVersion,
50 pluginUrl
51 }
52 }
53 }
54
55 private get plugin () {
56 return this.payload
57 }
58}
diff --git a/server/lib/notifier/shared/instance/registration-for-moderators.ts b/server/lib/notifier/shared/instance/registration-for-moderators.ts
new file mode 100644
index 000000000..4deb5a2cc
--- /dev/null
+++ b/server/lib/notifier/shared/instance/registration-for-moderators.ts
@@ -0,0 +1,49 @@
1import { logger } from '@server/helpers/logger'
2import { CONFIG } from '@server/initializers/config'
3import { UserModel } from '@server/models/user/user'
4import { UserNotificationModel } from '@server/models/user/user-notification'
5import { MUserDefault, MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/types/models'
6import { UserNotificationType, UserRight } from '@shared/models'
7import { AbstractNotification } from '../common/abstract-notification'
8
9export class RegistrationForModerators extends AbstractNotification <MUserDefault> {
10 private moderators: MUserDefault[]
11
12 async prepare () {
13 this.moderators = await UserModel.listWithRight(UserRight.MANAGE_USERS)
14 }
15
16 log () {
17 logger.info('Notifying %s moderators of new user registration of %s.', this.moderators.length, this.payload.username)
18 }
19
20 getSetting (user: MUserWithNotificationSetting) {
21 return user.NotificationSetting.newUserRegistration
22 }
23
24 getTargetUsers () {
25 return this.moderators
26 }
27
28 async createNotification (user: MUserWithNotificationSetting) {
29 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
30 type: UserNotificationType.NEW_USER_REGISTRATION,
31 userId: user.id,
32 accountId: this.payload.Account.id
33 })
34 notification.Account = this.payload.Account
35
36 return notification
37 }
38
39 async createEmail (to: string) {
40 return {
41 template: 'user-registered',
42 to,
43 subject: `a new user registered on ${CONFIG.INSTANCE.NAME}: ${this.payload.username}`,
44 locals: {
45 user: this.payload
46 }
47 }
48 }
49}
diff --git a/server/lib/notifier/shared/video-publication/abstract-owned-video-publication.ts b/server/lib/notifier/shared/video-publication/abstract-owned-video-publication.ts
new file mode 100644
index 000000000..fd06e080d
--- /dev/null
+++ b/server/lib/notifier/shared/video-publication/abstract-owned-video-publication.ts
@@ -0,0 +1,57 @@
1import { logger } from '@server/helpers/logger'
2import { WEBSERVER } from '@server/initializers/constants'
3import { UserModel } from '@server/models/user/user'
4import { UserNotificationModel } from '@server/models/user/user-notification'
5import { MUserDefault, MUserWithNotificationSetting, MVideoFullLight, UserNotificationModelForApi } from '@server/types/models'
6import { UserNotificationType } from '@shared/models'
7import { AbstractNotification } from '../common/abstract-notification'
8
9export abstract class AbstractOwnedVideoPublication extends AbstractNotification <MVideoFullLight> {
10 protected user: MUserDefault
11
12 async prepare () {
13 this.user = await UserModel.loadByVideoId(this.payload.id)
14 }
15
16 log () {
17 logger.info('Notifying user %s of the publication of its video %s.', this.user.username, this.payload.url)
18 }
19
20 getSetting (user: MUserWithNotificationSetting) {
21 return user.NotificationSetting.myVideoPublished
22 }
23
24 getTargetUsers () {
25 if (!this.user) return []
26
27 return [ this.user ]
28 }
29
30 async createNotification (user: MUserWithNotificationSetting) {
31 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
32 type: UserNotificationType.MY_VIDEO_PUBLISHED,
33 userId: user.id,
34 videoId: this.payload.id
35 })
36 notification.Video = this.payload
37
38 return notification
39 }
40
41 createEmail (to: string) {
42 const videoUrl = WEBSERVER.URL + this.payload.getWatchStaticPath()
43
44 return {
45 to,
46 subject: `Your video ${this.payload.name} has been published`,
47 text: `Your video "${this.payload.name}" has been published.`,
48 locals: {
49 title: 'You video is live',
50 action: {
51 text: 'View video',
52 url: videoUrl
53 }
54 }
55 }
56 }
57}
diff --git a/server/lib/notifier/shared/video-publication/import-finished-for-owner.ts b/server/lib/notifier/shared/video-publication/import-finished-for-owner.ts
new file mode 100644
index 000000000..9f374b6f9
--- /dev/null
+++ b/server/lib/notifier/shared/video-publication/import-finished-for-owner.ts
@@ -0,0 +1,97 @@
1import { logger } from '@server/helpers/logger'
2import { WEBSERVER } from '@server/initializers/constants'
3import { UserModel } from '@server/models/user/user'
4import { UserNotificationModel } from '@server/models/user/user-notification'
5import { MUserDefault, MUserWithNotificationSetting, MVideoImportVideo, UserNotificationModelForApi } from '@server/types/models'
6import { UserNotificationType } from '@shared/models'
7import { AbstractNotification } from '../common/abstract-notification'
8
9export type ImportFinishedForOwnerPayload = {
10 videoImport: MVideoImportVideo
11 success: boolean
12}
13
14export class ImportFinishedForOwner extends AbstractNotification <ImportFinishedForOwnerPayload> {
15 private user: MUserDefault
16
17 async prepare () {
18 this.user = await UserModel.loadByVideoImportId(this.videoImport.id)
19 }
20
21 log () {
22 logger.info('Notifying user %s its video import %s is finished.', this.user.username, this.videoImport.getTargetIdentifier())
23 }
24
25 getSetting (user: MUserWithNotificationSetting) {
26 return user.NotificationSetting.myVideoImportFinished
27 }
28
29 getTargetUsers () {
30 if (!this.user) return []
31
32 return [ this.user ]
33 }
34
35 async createNotification (user: MUserWithNotificationSetting) {
36 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
37 type: this.payload.success
38 ? UserNotificationType.MY_VIDEO_IMPORT_SUCCESS
39 : UserNotificationType.MY_VIDEO_IMPORT_ERROR,
40
41 userId: user.id,
42 videoImportId: this.videoImport.id
43 })
44 notification.VideoImport = this.videoImport
45
46 return notification
47 }
48
49 createEmail (to: string) {
50 if (this.payload.success) return this.createSuccessEmail(to)
51
52 return this.createFailEmail(to)
53 }
54
55 private createSuccessEmail (to: string) {
56 const videoUrl = WEBSERVER.URL + this.videoImport.Video.getWatchStaticPath()
57
58 return {
59 to,
60 subject: `Your video import ${this.videoImport.getTargetIdentifier()} is complete`,
61 text: `Your video "${this.videoImport.getTargetIdentifier()}" just finished importing.`,
62 locals: {
63 title: 'Import complete',
64 action: {
65 text: 'View video',
66 url: videoUrl
67 }
68 }
69 }
70 }
71
72 private createFailEmail (to: string) {
73 const importUrl = WEBSERVER.URL + '/my-library/video-imports'
74
75 const text =
76 `Your video import "${this.videoImport.getTargetIdentifier()}" encountered an error.` +
77 '\n\n' +
78 `See your videos import dashboard for more information: <a href="${importUrl}">${importUrl}</a>.`
79
80 return {
81 to,
82 subject: `Your video import "${this.videoImport.getTargetIdentifier()}" encountered an error`,
83 text,
84 locals: {
85 title: 'Import failed',
86 action: {
87 text: 'Review imports',
88 url: importUrl
89 }
90 }
91 }
92 }
93
94 private get videoImport () {
95 return this.payload.videoImport
96 }
97}
diff --git a/server/lib/notifier/shared/video-publication/index.ts b/server/lib/notifier/shared/video-publication/index.ts
new file mode 100644
index 000000000..940774504
--- /dev/null
+++ b/server/lib/notifier/shared/video-publication/index.ts
@@ -0,0 +1,5 @@
1export * from './new-video-for-subscribers'
2export * from './import-finished-for-owner'
3export * from './owned-publication-after-auto-unblacklist'
4export * from './owned-publication-after-schedule-update'
5export * from './owned-publication-after-transcoding'
diff --git a/server/lib/notifier/shared/video-publication/new-video-for-subscribers.ts b/server/lib/notifier/shared/video-publication/new-video-for-subscribers.ts
new file mode 100644
index 000000000..4253a0930
--- /dev/null
+++ b/server/lib/notifier/shared/video-publication/new-video-for-subscribers.ts
@@ -0,0 +1,61 @@
1import { logger } from '@server/helpers/logger'
2import { WEBSERVER } from '@server/initializers/constants'
3import { UserModel } from '@server/models/user/user'
4import { UserNotificationModel } from '@server/models/user/user-notification'
5import { MUserWithNotificationSetting, MVideoAccountLight, UserNotificationModelForApi } from '@server/types/models'
6import { UserNotificationType, VideoPrivacy, VideoState } from '@shared/models'
7import { AbstractNotification } from '../common/abstract-notification'
8
9export class NewVideoForSubscribers extends AbstractNotification <MVideoAccountLight> {
10 private users: MUserWithNotificationSetting[]
11
12 async prepare () {
13 // List all followers that are users
14 this.users = await UserModel.listUserSubscribersOf(this.payload.VideoChannel.actorId)
15 }
16
17 log () {
18 logger.info('Notifying %d users of new video %s.', this.users.length, this.payload.url)
19 }
20
21 isDisabled () {
22 return this.payload.privacy !== VideoPrivacy.PUBLIC || this.payload.state !== VideoState.PUBLISHED || this.payload.isBlacklisted()
23 }
24
25 getSetting (user: MUserWithNotificationSetting) {
26 return user.NotificationSetting.newVideoFromSubscription
27 }
28
29 getTargetUsers () {
30 return this.users
31 }
32
33 async createNotification (user: MUserWithNotificationSetting) {
34 const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
35 type: UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION,
36 userId: user.id,
37 videoId: this.payload.id
38 })
39 notification.Video = this.payload
40
41 return notification
42 }
43
44 createEmail (to: string) {
45 const channelName = this.payload.VideoChannel.getDisplayName()
46 const videoUrl = WEBSERVER.URL + this.payload.getWatchStaticPath()
47
48 return {
49 to,
50 subject: channelName + ' just published a new video',
51 text: `Your subscription ${channelName} just published a new video: "${this.payload.name}".`,
52 locals: {
53 title: 'New content ',
54 action: {
55 text: 'View video',
56 url: videoUrl
57 }
58 }
59 }
60 }
61}
diff --git a/server/lib/notifier/shared/video-publication/owned-publication-after-auto-unblacklist.ts b/server/lib/notifier/shared/video-publication/owned-publication-after-auto-unblacklist.ts
new file mode 100644
index 000000000..27d89a5c7
--- /dev/null
+++ b/server/lib/notifier/shared/video-publication/owned-publication-after-auto-unblacklist.ts
@@ -0,0 +1,11 @@
1
2import { VideoState } from '@shared/models'
3import { AbstractOwnedVideoPublication } from './abstract-owned-video-publication'
4
5export class OwnedPublicationAfterAutoUnblacklist extends AbstractOwnedVideoPublication {
6
7 isDisabled () {
8 // Don't notify if video is still waiting for transcoding or scheduled update
9 return !!this.payload.ScheduleVideoUpdate || (this.payload.waitTranscoding && this.payload.state !== VideoState.PUBLISHED)
10 }
11}
diff --git a/server/lib/notifier/shared/video-publication/owned-publication-after-schedule-update.ts b/server/lib/notifier/shared/video-publication/owned-publication-after-schedule-update.ts
new file mode 100644
index 000000000..2e253b358
--- /dev/null
+++ b/server/lib/notifier/shared/video-publication/owned-publication-after-schedule-update.ts
@@ -0,0 +1,10 @@
1import { VideoState } from '@shared/models'
2import { AbstractOwnedVideoPublication } from './abstract-owned-video-publication'
3
4export class OwnedPublicationAfterScheduleUpdate extends AbstractOwnedVideoPublication {
5
6 isDisabled () {
7 // Don't notify if video is still blacklisted or waiting for transcoding
8 return !!this.payload.VideoBlacklist || (this.payload.waitTranscoding && this.payload.state !== VideoState.PUBLISHED)
9 }
10}
diff --git a/server/lib/notifier/shared/video-publication/owned-publication-after-transcoding.ts b/server/lib/notifier/shared/video-publication/owned-publication-after-transcoding.ts
new file mode 100644
index 000000000..4fab1090f
--- /dev/null
+++ b/server/lib/notifier/shared/video-publication/owned-publication-after-transcoding.ts
@@ -0,0 +1,9 @@
1import { AbstractOwnedVideoPublication } from './abstract-owned-video-publication'
2
3export class OwnedPublicationAfterTranscoding extends AbstractOwnedVideoPublication {
4
5 isDisabled () {
6 // Don't notify if didn't wait for transcoding or video is still blacklisted/waiting for scheduled update
7 return !this.payload.waitTranscoding || !!this.payload.VideoBlacklist || !!this.payload.ScheduleVideoUpdate
8 }
9}
diff --git a/server/lib/plugins/register-helpers.ts b/server/lib/plugins/register-helpers.ts
index 09275f9ba..af533effd 100644
--- a/server/lib/plugins/register-helpers.ts
+++ b/server/lib/plugins/register-helpers.ts
@@ -1,13 +1,7 @@
1import * as express from 'express' 1import * as express from 'express'
2import { logger } from '@server/helpers/logger' 2import { logger } from '@server/helpers/logger'
3import {
4 VIDEO_CATEGORIES,
5 VIDEO_LANGUAGES,
6 VIDEO_LICENCES,
7 VIDEO_PLAYLIST_PRIVACIES,
8 VIDEO_PRIVACIES
9} from '@server/initializers/constants'
10import { onExternalUserAuthenticated } from '@server/lib/auth/external-auth' 3import { onExternalUserAuthenticated } from '@server/lib/auth/external-auth'
4import { VideoConstantManagerFactory } from '@server/lib/plugins/video-constant-manager-factory'
11import { PluginModel } from '@server/models/server/plugin' 5import { PluginModel } from '@server/models/server/plugin'
12import { 6import {
13 RegisterServerAuthExternalOptions, 7 RegisterServerAuthExternalOptions,
@@ -18,41 +12,18 @@ import {
18} from '@server/types/plugins' 12} from '@server/types/plugins'
19import { 13import {
20 EncoderOptionsBuilder, 14 EncoderOptionsBuilder,
21 PluginPlaylistPrivacyManager,
22 PluginSettingsManager, 15 PluginSettingsManager,
23 PluginStorageManager, 16 PluginStorageManager,
24 PluginVideoCategoryManager,
25 PluginVideoLanguageManager,
26 PluginVideoLicenceManager,
27 PluginVideoPrivacyManager,
28 RegisterServerHookOptions, 17 RegisterServerHookOptions,
29 RegisterServerSettingOptions, 18 RegisterServerSettingOptions,
30 serverHookObject 19 serverHookObject,
20 VideoPlaylistPrivacy,
21 VideoPrivacy
31} from '@shared/models' 22} from '@shared/models'
32import { VideoTranscodingProfilesManager } from '../transcoding/video-transcoding-profiles' 23import { VideoTranscodingProfilesManager } from '../transcoding/video-transcoding-profiles'
33import { buildPluginHelpers } from './plugin-helpers-builder' 24import { buildPluginHelpers } from './plugin-helpers-builder'
34 25
35type AlterableVideoConstant = 'language' | 'licence' | 'category' | 'privacy' | 'playlistPrivacy'
36type VideoConstant = { [key in number | string]: string }
37
38type UpdatedVideoConstant = {
39 [name in AlterableVideoConstant]: {
40 [ npmName: string]: {
41 added: { key: number | string, label: string }[]
42 deleted: { key: number | string, label: string }[]
43 }
44 }
45}
46
47export class RegisterHelpers { 26export class RegisterHelpers {
48 private readonly updatedVideoConstants: UpdatedVideoConstant = {
49 playlistPrivacy: { },
50 privacy: { },
51 language: { },
52 licence: { },
53 category: { }
54 }
55
56 private readonly transcodingProfiles: { 27 private readonly transcodingProfiles: {
57 [ npmName: string ]: { 28 [ npmName: string ]: {
58 type: 'vod' | 'live' 29 type: 'vod' | 'live'
@@ -78,6 +49,7 @@ export class RegisterHelpers {
78 private readonly onSettingsChangeCallbacks: ((settings: any) => Promise<any>)[] = [] 49 private readonly onSettingsChangeCallbacks: ((settings: any) => Promise<any>)[] = []
79 50
80 private readonly router: express.Router 51 private readonly router: express.Router
52 private readonly videoConstantManagerFactory: VideoConstantManagerFactory
81 53
82 constructor ( 54 constructor (
83 private readonly npmName: string, 55 private readonly npmName: string,
@@ -85,6 +57,7 @@ export class RegisterHelpers {
85 private readonly onHookAdded: (options: RegisterServerHookOptions) => void 57 private readonly onHookAdded: (options: RegisterServerHookOptions) => void
86 ) { 58 ) {
87 this.router = express.Router() 59 this.router = express.Router()
60 this.videoConstantManagerFactory = new VideoConstantManagerFactory(this.npmName)
88 } 61 }
89 62
90 buildRegisterHelpers (): RegisterServerOptions { 63 buildRegisterHelpers (): RegisterServerOptions {
@@ -96,13 +69,13 @@ export class RegisterHelpers {
96 const settingsManager = this.buildSettingsManager() 69 const settingsManager = this.buildSettingsManager()
97 const storageManager = this.buildStorageManager() 70 const storageManager = this.buildStorageManager()
98 71
99 const videoLanguageManager = this.buildVideoLanguageManager() 72 const videoLanguageManager = this.videoConstantManagerFactory.createVideoConstantManager<string>('language')
100 73
101 const videoLicenceManager = this.buildVideoLicenceManager() 74 const videoLicenceManager = this.videoConstantManagerFactory.createVideoConstantManager<number>('licence')
102 const videoCategoryManager = this.buildVideoCategoryManager() 75 const videoCategoryManager = this.videoConstantManagerFactory.createVideoConstantManager<number>('category')
103 76
104 const videoPrivacyManager = this.buildVideoPrivacyManager() 77 const videoPrivacyManager = this.videoConstantManagerFactory.createVideoConstantManager<VideoPrivacy>('privacy')
105 const playlistPrivacyManager = this.buildPlaylistPrivacyManager() 78 const playlistPrivacyManager = this.videoConstantManagerFactory.createVideoConstantManager<VideoPlaylistPrivacy>('playlistPrivacy')
106 79
107 const transcodingManager = this.buildTranscodingManager() 80 const transcodingManager = this.buildTranscodingManager()
108 81
@@ -122,12 +95,38 @@ export class RegisterHelpers {
122 settingsManager, 95 settingsManager,
123 storageManager, 96 storageManager,
124 97
125 videoLanguageManager, 98 videoLanguageManager: {
126 videoCategoryManager, 99 ...videoLanguageManager,
127 videoLicenceManager, 100 /** @deprecated use `addConstant` instead **/
101 addLanguage: videoLanguageManager.addConstant,
102 /** @deprecated use `deleteConstant` instead **/
103 deleteLanguage: videoLanguageManager.deleteConstant
104 },
105 videoCategoryManager: {
106 ...videoCategoryManager,
107 /** @deprecated use `addConstant` instead **/
108 addCategory: videoCategoryManager.addConstant,
109 /** @deprecated use `deleteConstant` instead **/
110 deleteCategory: videoCategoryManager.deleteConstant
111 },
112 videoLicenceManager: {
113 ...videoLicenceManager,
114 /** @deprecated use `addConstant` instead **/
115 addLicence: videoLicenceManager.addConstant,
116 /** @deprecated use `deleteConstant` instead **/
117 deleteLicence: videoLicenceManager.deleteConstant
118 },
128 119
129 videoPrivacyManager, 120 videoPrivacyManager: {
130 playlistPrivacyManager, 121 ...videoPrivacyManager,
122 /** @deprecated use `deleteConstant` instead **/
123 deletePrivacy: videoPrivacyManager.deleteConstant
124 },
125 playlistPrivacyManager: {
126 ...playlistPrivacyManager,
127 /** @deprecated use `deleteConstant` instead **/
128 deletePlaylistPrivacy: playlistPrivacyManager.deleteConstant
129 },
131 130
132 transcodingManager, 131 transcodingManager,
133 132
@@ -141,29 +140,7 @@ export class RegisterHelpers {
141 } 140 }
142 141
143 reinitVideoConstants (npmName: string) { 142 reinitVideoConstants (npmName: string) {
144 const hash = { 143 this.videoConstantManagerFactory.resetVideoConstants(npmName)
145 language: VIDEO_LANGUAGES,
146 licence: VIDEO_LICENCES,
147 category: VIDEO_CATEGORIES,
148 privacy: VIDEO_PRIVACIES,
149 playlistPrivacy: VIDEO_PLAYLIST_PRIVACIES
150 }
151 const types: AlterableVideoConstant[] = [ 'language', 'licence', 'category', 'privacy', 'playlistPrivacy' ]
152
153 for (const type of types) {
154 const updatedConstants = this.updatedVideoConstants[type][npmName]
155 if (!updatedConstants) continue
156
157 for (const added of updatedConstants.added) {
158 delete hash[type][added.key]
159 }
160
161 for (const deleted of updatedConstants.deleted) {
162 hash[type][deleted.key] = deleted.label
163 }
164
165 delete this.updatedVideoConstants[type][npmName]
166 }
167 } 144 }
168 145
169 reinitTranscodingProfilesAndEncoders (npmName: string) { 146 reinitTranscodingProfilesAndEncoders (npmName: string) {
@@ -291,119 +268,6 @@ export class RegisterHelpers {
291 } 268 }
292 } 269 }
293 270
294 private buildVideoLanguageManager (): PluginVideoLanguageManager {
295 return {
296 addLanguage: (key: string, label: string) => {
297 return this.addConstant({ npmName: this.npmName, type: 'language', obj: VIDEO_LANGUAGES, key, label })
298 },
299
300 deleteLanguage: (key: string) => {
301 return this.deleteConstant({ npmName: this.npmName, type: 'language', obj: VIDEO_LANGUAGES, key })
302 }
303 }
304 }
305
306 private buildVideoCategoryManager (): PluginVideoCategoryManager {
307 return {
308 addCategory: (key: number, label: string) => {
309 return this.addConstant({ npmName: this.npmName, type: 'category', obj: VIDEO_CATEGORIES, key, label })
310 },
311
312 deleteCategory: (key: number) => {
313 return this.deleteConstant({ npmName: this.npmName, type: 'category', obj: VIDEO_CATEGORIES, key })
314 }
315 }
316 }
317
318 private buildVideoPrivacyManager (): PluginVideoPrivacyManager {
319 return {
320 deletePrivacy: (key: number) => {
321 return this.deleteConstant({ npmName: this.npmName, type: 'privacy', obj: VIDEO_PRIVACIES, key })
322 }
323 }
324 }
325
326 private buildPlaylistPrivacyManager (): PluginPlaylistPrivacyManager {
327 return {
328 deletePlaylistPrivacy: (key: number) => {
329 return this.deleteConstant({ npmName: this.npmName, type: 'playlistPrivacy', obj: VIDEO_PLAYLIST_PRIVACIES, key })
330 }
331 }
332 }
333
334 private buildVideoLicenceManager (): PluginVideoLicenceManager {
335 return {
336 addLicence: (key: number, label: string) => {
337 return this.addConstant({ npmName: this.npmName, type: 'licence', obj: VIDEO_LICENCES, key, label })
338 },
339
340 deleteLicence: (key: number) => {
341 return this.deleteConstant({ npmName: this.npmName, type: 'licence', obj: VIDEO_LICENCES, key })
342 }
343 }
344 }
345
346 private addConstant<T extends string | number> (parameters: {
347 npmName: string
348 type: AlterableVideoConstant
349 obj: VideoConstant
350 key: T
351 label: string
352 }) {
353 const { npmName, type, obj, key, label } = parameters
354
355 if (obj[key]) {
356 logger.warn('Cannot add %s %s by plugin %s: key already exists.', type, npmName, key)
357 return false
358 }
359
360 if (!this.updatedVideoConstants[type][npmName]) {
361 this.updatedVideoConstants[type][npmName] = {
362 added: [],
363 deleted: []
364 }
365 }
366
367 this.updatedVideoConstants[type][npmName].added.push({ key, label })
368 obj[key] = label
369
370 return true
371 }
372
373 private deleteConstant<T extends string | number> (parameters: {
374 npmName: string
375 type: AlterableVideoConstant
376 obj: VideoConstant
377 key: T
378 }) {
379 const { npmName, type, obj, key } = parameters
380
381 if (!obj[key]) {
382 logger.warn('Cannot delete %s by plugin %s: key %s does not exist.', type, npmName, key)
383 return false
384 }
385
386 if (!this.updatedVideoConstants[type][npmName]) {
387 this.updatedVideoConstants[type][npmName] = {
388 added: [],
389 deleted: []
390 }
391 }
392
393 const updatedConstants = this.updatedVideoConstants[type][npmName]
394
395 const alreadyAdded = updatedConstants.added.find(a => a.key === key)
396 if (alreadyAdded) {
397 updatedConstants.added.filter(a => a.key !== key)
398 } else if (obj[key]) {
399 updatedConstants.deleted.push({ key, label: obj[key] })
400 }
401
402 delete obj[key]
403
404 return true
405 }
406
407 private buildTranscodingManager () { 271 private buildTranscodingManager () {
408 const self = this 272 const self = this
409 273
diff --git a/server/lib/plugins/video-constant-manager-factory.ts b/server/lib/plugins/video-constant-manager-factory.ts
new file mode 100644
index 000000000..f04dde29f
--- /dev/null
+++ b/server/lib/plugins/video-constant-manager-factory.ts
@@ -0,0 +1,139 @@
1import { logger } from '@server/helpers/logger'
2import {
3 VIDEO_CATEGORIES,
4 VIDEO_LANGUAGES,
5 VIDEO_LICENCES,
6 VIDEO_PLAYLIST_PRIVACIES,
7 VIDEO_PRIVACIES
8} from '@server/initializers/constants'
9import { ConstantManager } from '@shared/models/plugins/server/plugin-constant-manager.model'
10
11type AlterableVideoConstant = 'language' | 'licence' | 'category' | 'privacy' | 'playlistPrivacy'
12type VideoConstant = Record<number | string, string>
13
14type UpdatedVideoConstant = {
15 [name in AlterableVideoConstant]: {
16 [ npmName: string]: {
17 added: VideoConstant[]
18 deleted: VideoConstant[]
19 }
20 }
21}
22
23const constantsHash: { [key in AlterableVideoConstant]: VideoConstant } = {
24 language: VIDEO_LANGUAGES,
25 licence: VIDEO_LICENCES,
26 category: VIDEO_CATEGORIES,
27 privacy: VIDEO_PRIVACIES,
28 playlistPrivacy: VIDEO_PLAYLIST_PRIVACIES
29}
30
31export class VideoConstantManagerFactory {
32 private readonly updatedVideoConstants: UpdatedVideoConstant = {
33 playlistPrivacy: { },
34 privacy: { },
35 language: { },
36 licence: { },
37 category: { }
38 }
39
40 constructor (
41 private readonly npmName: string
42 ) {}
43
44 public resetVideoConstants (npmName: string) {
45 const types: AlterableVideoConstant[] = [ 'language', 'licence', 'category', 'privacy', 'playlistPrivacy' ]
46 for (const type of types) {
47 this.resetConstants({ npmName, type })
48 }
49 }
50
51 private resetConstants (parameters: { npmName: string, type: AlterableVideoConstant }) {
52 const { npmName, type } = parameters
53 const updatedConstants = this.updatedVideoConstants[type][npmName]
54
55 if (!updatedConstants) return
56
57 for (const added of updatedConstants.added) {
58 delete constantsHash[type][added.key]
59 }
60
61 for (const deleted of updatedConstants.deleted) {
62 constantsHash[type][deleted.key] = deleted.label
63 }
64
65 delete this.updatedVideoConstants[type][npmName]
66 }
67
68 public createVideoConstantManager<K extends number | string>(type: AlterableVideoConstant): ConstantManager<K> {
69 const { npmName } = this
70 return {
71 addConstant: (key: K, label: string) => this.addConstant({ npmName, type, key, label }),
72 deleteConstant: (key: K) => this.deleteConstant({ npmName, type, key }),
73 getConstantValue: (key: K) => constantsHash[type][key],
74 getConstants: () => constantsHash[type] as Record<K, string>,
75 resetConstants: () => this.resetConstants({ npmName, type })
76 }
77 }
78
79 private addConstant<T extends string | number> (parameters: {
80 npmName: string
81 type: AlterableVideoConstant
82 key: T
83 label: string
84 }) {
85 const { npmName, type, key, label } = parameters
86 const obj = constantsHash[type]
87
88 if (obj[key]) {
89 logger.warn('Cannot add %s %s by plugin %s: key already exists.', type, npmName, key)
90 return false
91 }
92
93 if (!this.updatedVideoConstants[type][npmName]) {
94 this.updatedVideoConstants[type][npmName] = {
95 added: [],
96 deleted: []
97 }
98 }
99
100 this.updatedVideoConstants[type][npmName].added.push({ key: key, label } as VideoConstant)
101 obj[key] = label
102
103 return true
104 }
105
106 private deleteConstant<T extends string | number> (parameters: {
107 npmName: string
108 type: AlterableVideoConstant
109 key: T
110 }) {
111 const { npmName, type, key } = parameters
112 const obj = constantsHash[type]
113
114 if (!obj[key]) {
115 logger.warn('Cannot delete %s by plugin %s: key %s does not exist.', type, npmName, key)
116 return false
117 }
118
119 if (!this.updatedVideoConstants[type][npmName]) {
120 this.updatedVideoConstants[type][npmName] = {
121 added: [],
122 deleted: []
123 }
124 }
125
126 const updatedConstants = this.updatedVideoConstants[type][npmName]
127
128 const alreadyAdded = updatedConstants.added.find(a => a.key === key)
129 if (alreadyAdded) {
130 updatedConstants.added.filter(a => a.key !== key)
131 } else if (obj[key]) {
132 updatedConstants.deleted.push({ key, label: obj[key] } as VideoConstant)
133 }
134
135 delete obj[key]
136
137 return true
138 }
139}
diff --git a/server/lib/schedulers/plugins-check-scheduler.ts b/server/lib/schedulers/plugins-check-scheduler.ts
index 9a1ae3ec5..c95e109b0 100644
--- a/server/lib/schedulers/plugins-check-scheduler.ts
+++ b/server/lib/schedulers/plugins-check-scheduler.ts
@@ -1,12 +1,12 @@
1import { chunk } from 'lodash'
2import { compareSemVer } from '@shared/core-utils'
1import { logger } from '../../helpers/logger' 3import { logger } from '../../helpers/logger'
2import { AbstractScheduler } from './abstract-scheduler'
3import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants'
4import { CONFIG } from '../../initializers/config' 4import { CONFIG } from '../../initializers/config'
5import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants'
5import { PluginModel } from '../../models/server/plugin' 6import { PluginModel } from '../../models/server/plugin'
6import { chunk } from 'lodash'
7import { getLatestPluginsVersion } from '../plugins/plugin-index'
8import { compareSemVer } from '../../../shared/core-utils/miscs/miscs'
9import { Notifier } from '../notifier' 7import { Notifier } from '../notifier'
8import { getLatestPluginsVersion } from '../plugins/plugin-index'
9import { AbstractScheduler } from './abstract-scheduler'
10 10
11export class PluginsCheckScheduler extends AbstractScheduler { 11export class PluginsCheckScheduler extends AbstractScheduler {
12 12
diff --git a/server/lib/schedulers/videos-redundancy-scheduler.ts b/server/lib/schedulers/videos-redundancy-scheduler.ts
index b5a5eb697..103ab1fab 100644
--- a/server/lib/schedulers/videos-redundancy-scheduler.ts
+++ b/server/lib/schedulers/videos-redundancy-scheduler.ts
@@ -267,7 +267,8 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
267 logger.info('Duplicating %s streaming playlist in videos redundancy with "%s" strategy.', video.url, strategy) 267 logger.info('Duplicating %s streaming playlist in videos redundancy with "%s" strategy.', video.url, strategy)
268 268
269 const destDirectory = join(HLS_REDUNDANCY_DIRECTORY, video.uuid) 269 const destDirectory = join(HLS_REDUNDANCY_DIRECTORY, video.uuid)
270 await downloadPlaylistSegments(playlist.playlistUrl, destDirectory, VIDEO_IMPORT_TIMEOUT) 270 const masterPlaylistUrl = playlist.getMasterPlaylistUrl(video)
271 await downloadPlaylistSegments(masterPlaylistUrl, destDirectory, VIDEO_IMPORT_TIMEOUT)
271 272
272 const createdModel: MVideoRedundancyStreamingPlaylistVideo = await VideoRedundancyModel.create({ 273 const createdModel: MVideoRedundancyStreamingPlaylistVideo = await VideoRedundancyModel.create({
273 expiresOn, 274 expiresOn,
@@ -282,7 +283,7 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
282 283
283 await sendCreateCacheFile(serverActor, video, createdModel) 284 await sendCreateCacheFile(serverActor, video, createdModel)
284 285
285 logger.info('Duplicated playlist %s -> %s.', playlist.playlistUrl, createdModel.url) 286 logger.info('Duplicated playlist %s -> %s.', masterPlaylistUrl, createdModel.url)
286 } 287 }
287 288
288 private async extendsExpirationOf (redundancy: MVideoRedundancyVideo, expiresAfterMs: number) { 289 private async extendsExpirationOf (redundancy: MVideoRedundancyVideo, expiresAfterMs: number) {
@@ -330,7 +331,7 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
330 private buildEntryLogId (object: MVideoRedundancyFileVideo | MVideoRedundancyStreamingPlaylistVideo) { 331 private buildEntryLogId (object: MVideoRedundancyFileVideo | MVideoRedundancyStreamingPlaylistVideo) {
331 if (isMVideoRedundancyFileVideo(object)) return `${object.VideoFile.Video.url}-${object.VideoFile.resolution}` 332 if (isMVideoRedundancyFileVideo(object)) return `${object.VideoFile.Video.url}-${object.VideoFile.resolution}`
332 333
333 return `${object.VideoStreamingPlaylist.playlistUrl}` 334 return `${object.VideoStreamingPlaylist.getMasterPlaylistUrl(object.VideoStreamingPlaylist.Video)}`
334 } 335 }
335 336
336 private getTotalFileSizes (files: MVideoFile[], playlists: MStreamingPlaylistFiles[]) { 337 private getTotalFileSizes (files: MVideoFile[], playlists: MStreamingPlaylistFiles[]) {
diff --git a/server/lib/transcoding/video-transcoding.ts b/server/lib/transcoding/video-transcoding.ts
index 1ad63baf3..d2a556360 100644
--- a/server/lib/transcoding/video-transcoding.ts
+++ b/server/lib/transcoding/video-transcoding.ts
@@ -10,11 +10,18 @@ import { transcode, TranscodeOptions, TranscodeOptionsType } from '../../helpers
10import { canDoQuickTranscode, getDurationFromVideoFile, getMetadataFromFile, getVideoFileFPS } from '../../helpers/ffprobe-utils' 10import { canDoQuickTranscode, getDurationFromVideoFile, getMetadataFromFile, getVideoFileFPS } from '../../helpers/ffprobe-utils'
11import { logger } from '../../helpers/logger' 11import { logger } from '../../helpers/logger'
12import { CONFIG } from '../../initializers/config' 12import { CONFIG } from '../../initializers/config'
13import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSERVER } from '../../initializers/constants' 13import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION } from '../../initializers/constants'
14import { VideoFileModel } from '../../models/video/video-file' 14import { VideoFileModel } from '../../models/video/video-file'
15import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist' 15import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist'
16import { updateMasterHLSPlaylist, updateSha256VODSegments } from '../hls' 16import { updateMasterHLSPlaylist, updateSha256VODSegments } from '../hls'
17import { generateVideoFilename, generateVideoStreamingPlaylistName, getVideoFilePath } from '../video-paths' 17import {
18 generateHLSMasterPlaylistFilename,
19 generateHlsSha256SegmentsFilename,
20 generateHLSVideoFilename,
21 generateWebTorrentVideoFilename,
22 getHlsResolutionPlaylistFilename,
23 getVideoFilePath
24} from '../video-paths'
18import { VideoTranscodingProfilesManager } from './video-transcoding-profiles' 25import { VideoTranscodingProfilesManager } from './video-transcoding-profiles'
19 26
20/** 27/**
@@ -60,7 +67,7 @@ async function optimizeOriginalVideofile (video: MVideoFullLight, inputVideoFile
60 67
61 // Important to do this before getVideoFilename() to take in account the new filename 68 // Important to do this before getVideoFilename() to take in account the new filename
62 inputVideoFile.extname = newExtname 69 inputVideoFile.extname = newExtname
63 inputVideoFile.filename = generateVideoFilename(video, false, resolution, newExtname) 70 inputVideoFile.filename = generateWebTorrentVideoFilename(resolution, newExtname)
64 71
65 const videoOutputPath = getVideoFilePath(video, inputVideoFile) 72 const videoOutputPath = getVideoFilePath(video, inputVideoFile)
66 73
@@ -86,7 +93,7 @@ async function transcodeNewWebTorrentResolution (video: MVideoFullLight, resolut
86 const newVideoFile = new VideoFileModel({ 93 const newVideoFile = new VideoFileModel({
87 resolution, 94 resolution,
88 extname, 95 extname,
89 filename: generateVideoFilename(video, false, resolution, extname), 96 filename: generateWebTorrentVideoFilename(resolution, extname),
90 size: 0, 97 size: 0,
91 videoId: video.id 98 videoId: video.id
92 }) 99 })
@@ -169,7 +176,7 @@ async function mergeAudioVideofile (video: MVideoFullLight, resolution: VideoRes
169 176
170 // Important to do this before getVideoFilename() to take in account the new file extension 177 // Important to do this before getVideoFilename() to take in account the new file extension
171 inputVideoFile.extname = newExtname 178 inputVideoFile.extname = newExtname
172 inputVideoFile.filename = generateVideoFilename(video, false, inputVideoFile.resolution, newExtname) 179 inputVideoFile.filename = generateWebTorrentVideoFilename(inputVideoFile.resolution, newExtname)
173 180
174 const videoOutputPath = getVideoFilePath(video, inputVideoFile) 181 const videoOutputPath = getVideoFilePath(video, inputVideoFile)
175 // ffmpeg generated a new video file, so update the video duration 182 // ffmpeg generated a new video file, so update the video duration
@@ -271,15 +278,15 @@ async function generateHlsPlaylistCommon (options: {
271 const videoTranscodedBasePath = join(transcodeDirectory, type) 278 const videoTranscodedBasePath = join(transcodeDirectory, type)
272 await ensureDir(videoTranscodedBasePath) 279 await ensureDir(videoTranscodedBasePath)
273 280
274 const videoFilename = generateVideoStreamingPlaylistName(video.uuid, resolution) 281 const videoFilename = generateHLSVideoFilename(resolution)
275 const playlistFilename = VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution) 282 const resolutionPlaylistFilename = getHlsResolutionPlaylistFilename(videoFilename)
276 const playlistFileTranscodePath = join(videoTranscodedBasePath, playlistFilename) 283 const resolutionPlaylistFileTranscodePath = join(videoTranscodedBasePath, resolutionPlaylistFilename)
277 284
278 const transcodeOptions = { 285 const transcodeOptions = {
279 type, 286 type,
280 287
281 inputPath, 288 inputPath,
282 outputPath: playlistFileTranscodePath, 289 outputPath: resolutionPlaylistFileTranscodePath,
283 290
284 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), 291 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
285 profile: CONFIG.TRANSCODING.PROFILE, 292 profile: CONFIG.TRANSCODING.PROFILE,
@@ -299,19 +306,23 @@ async function generateHlsPlaylistCommon (options: {
299 306
300 await transcode(transcodeOptions) 307 await transcode(transcodeOptions)
301 308
302 const playlistUrl = WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsMasterPlaylistStaticPath(video.uuid)
303
304 // Create or update the playlist 309 // Create or update the playlist
305 const [ videoStreamingPlaylist ] = await VideoStreamingPlaylistModel.upsert({ 310 const playlist = await VideoStreamingPlaylistModel.loadOrGenerate(video)
306 videoId: video.id, 311
307 playlistUrl, 312 if (!playlist.playlistFilename) {
308 segmentsSha256Url: WEBSERVER.URL + VideoStreamingPlaylistModel.getHlsSha256SegmentsStaticPath(video.uuid, video.isLive), 313 playlist.playlistFilename = generateHLSMasterPlaylistFilename(video.isLive)
309 p2pMediaLoaderInfohashes: [], 314 }
310 p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION, 315
316 if (!playlist.segmentsSha256Filename) {
317 playlist.segmentsSha256Filename = generateHlsSha256SegmentsFilename(video.isLive)
318 }
319
320 playlist.p2pMediaLoaderInfohashes = []
321 playlist.p2pMediaLoaderPeerVersion = P2P_MEDIA_LOADER_PEER_VERSION
311 322
312 type: VideoStreamingPlaylistType.HLS 323 playlist.type = VideoStreamingPlaylistType.HLS
313 }, { returning: true }) as [ MStreamingPlaylistFilesVideo, boolean ] 324
314 videoStreamingPlaylist.Video = video 325 await playlist.save()
315 326
316 // Build the new playlist file 327 // Build the new playlist file
317 const extname = extnameUtil(videoFilename) 328 const extname = extnameUtil(videoFilename)
@@ -319,20 +330,20 @@ async function generateHlsPlaylistCommon (options: {
319 resolution, 330 resolution,
320 extname, 331 extname,
321 size: 0, 332 size: 0,
322 filename: generateVideoFilename(video, true, resolution, extname), 333 filename: videoFilename,
323 fps: -1, 334 fps: -1,
324 videoStreamingPlaylistId: videoStreamingPlaylist.id 335 videoStreamingPlaylistId: playlist.id
325 }) 336 })
326 337
327 const videoFilePath = getVideoFilePath(videoStreamingPlaylist, newVideoFile) 338 const videoFilePath = getVideoFilePath(playlist, newVideoFile)
328 339
329 // Move files from tmp transcoded directory to the appropriate place 340 // Move files from tmp transcoded directory to the appropriate place
330 const baseHlsDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid) 341 const baseHlsDirectory = join(HLS_STREAMING_PLAYLIST_DIRECTORY, video.uuid)
331 await ensureDir(baseHlsDirectory) 342 await ensureDir(baseHlsDirectory)
332 343
333 // Move playlist file 344 // Move playlist file
334 const playlistPath = join(baseHlsDirectory, playlistFilename) 345 const resolutionPlaylistPath = join(baseHlsDirectory, resolutionPlaylistFilename)
335 await move(playlistFileTranscodePath, playlistPath, { overwrite: true }) 346 await move(resolutionPlaylistFileTranscodePath, resolutionPlaylistPath, { overwrite: true })
336 // Move video file 347 // Move video file
337 await move(join(videoTranscodedBasePath, videoFilename), videoFilePath, { overwrite: true }) 348 await move(join(videoTranscodedBasePath, videoFilename), videoFilePath, { overwrite: true })
338 349
@@ -342,20 +353,20 @@ async function generateHlsPlaylistCommon (options: {
342 newVideoFile.fps = await getVideoFileFPS(videoFilePath) 353 newVideoFile.fps = await getVideoFileFPS(videoFilePath)
343 newVideoFile.metadata = await getMetadataFromFile(videoFilePath) 354 newVideoFile.metadata = await getMetadataFromFile(videoFilePath)
344 355
345 await createTorrentAndSetInfoHash(videoStreamingPlaylist, newVideoFile) 356 await createTorrentAndSetInfoHash(playlist, newVideoFile)
346 357
347 await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined) 358 await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined)
348 videoStreamingPlaylist.VideoFiles = await videoStreamingPlaylist.$get('VideoFiles')
349 359
350 videoStreamingPlaylist.p2pMediaLoaderInfohashes = VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes( 360 const playlistWithFiles = playlist as MStreamingPlaylistFilesVideo
351 playlistUrl, videoStreamingPlaylist.VideoFiles 361 playlistWithFiles.VideoFiles = await playlist.$get('VideoFiles')
352 ) 362 playlist.assignP2PMediaLoaderInfoHashes(video, playlistWithFiles.VideoFiles)
353 await videoStreamingPlaylist.save() 363
364 await playlist.save()
354 365
355 video.setHLSPlaylist(videoStreamingPlaylist) 366 video.setHLSPlaylist(playlist)
356 367
357 await updateMasterHLSPlaylist(video) 368 await updateMasterHLSPlaylist(video, playlistWithFiles)
358 await updateSha256VODSegments(video) 369 await updateSha256VODSegments(video, playlistWithFiles)
359 370
360 return playlistPath 371 return resolutionPlaylistPath
361} 372}
diff --git a/server/lib/video-paths.ts b/server/lib/video-paths.ts
index 1708c479a..1e4382108 100644
--- a/server/lib/video-paths.ts
+++ b/server/lib/video-paths.ts
@@ -3,29 +3,17 @@ import { extractVideo } from '@server/helpers/video'
3import { CONFIG } from '@server/initializers/config' 3import { CONFIG } from '@server/initializers/config'
4import { HLS_REDUNDANCY_DIRECTORY, HLS_STREAMING_PLAYLIST_DIRECTORY, STATIC_PATHS, WEBSERVER } from '@server/initializers/constants' 4import { HLS_REDUNDANCY_DIRECTORY, HLS_STREAMING_PLAYLIST_DIRECTORY, STATIC_PATHS, WEBSERVER } from '@server/initializers/constants'
5import { isStreamingPlaylist, MStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoUUID } from '@server/types/models' 5import { isStreamingPlaylist, MStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoUUID } from '@server/types/models'
6import { buildUUID } from '@server/helpers/uuid'
7import { removeFragmentedMP4Ext } from '@shared/core-utils'
6 8
7// ################## Video file name ################## 9// ################## Video file name ##################
8 10
9function generateVideoFilename (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, isHls: boolean, resolution: number, extname: string) { 11function generateWebTorrentVideoFilename (resolution: number, extname: string) {
10 const video = extractVideo(videoOrPlaylist) 12 return buildUUID() + '-' + resolution + extname
11
12 // FIXME: use a generated uuid instead, that will break compatibility with PeerTube < 3.1
13 // const uuid = uuidv4()
14 const uuid = video.uuid
15
16 if (isHls) {
17 return generateVideoStreamingPlaylistName(uuid, resolution)
18 }
19
20 return generateWebTorrentVideoName(uuid, resolution, extname)
21} 13}
22 14
23function generateVideoStreamingPlaylistName (uuid: string, resolution: number) { 15function generateHLSVideoFilename (resolution: number) {
24 return `${uuid}-${resolution}-fragmented.mp4` 16 return `${buildUUID()}-${resolution}-fragmented.mp4`
25}
26
27function generateWebTorrentVideoName (uuid: string, resolution: number, extname: string) {
28 return uuid + '-' + resolution + extname
29} 17}
30 18
31function getVideoFilePath (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile, isRedundancy = false) { 19function getVideoFilePath (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile, isRedundancy = false) {
@@ -63,15 +51,28 @@ function getHLSDirectory (video: MVideoUUID, isRedundancy = false) {
63 return join(baseDir, video.uuid) 51 return join(baseDir, video.uuid)
64} 52}
65 53
54function getHlsResolutionPlaylistFilename (videoFilename: string) {
55 // Video file name already contain resolution
56 return removeFragmentedMP4Ext(videoFilename) + '.m3u8'
57}
58
59function generateHLSMasterPlaylistFilename (isLive = false) {
60 if (isLive) return 'master.m3u8'
61
62 return buildUUID() + '-master.m3u8'
63}
64
65function generateHlsSha256SegmentsFilename (isLive = false) {
66 if (isLive) return 'segments-sha256.json'
67
68 return buildUUID() + '-segments-sha256.json'
69}
70
66// ################## Torrents ################## 71// ################## Torrents ##################
67 72
68function generateTorrentFileName (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, resolution: number) { 73function generateTorrentFileName (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, resolution: number) {
69 const video = extractVideo(videoOrPlaylist)
70 const extension = '.torrent' 74 const extension = '.torrent'
71 75 const uuid = buildUUID()
72 // FIXME: use a generated uuid instead, that will break compatibility with PeerTube < 3.1
73 // const uuid = uuidv4()
74 const uuid = video.uuid
75 76
76 if (isStreamingPlaylist(videoOrPlaylist)) { 77 if (isStreamingPlaylist(videoOrPlaylist)) {
77 return `${uuid}-${resolution}-${videoOrPlaylist.getStringType()}${extension}` 78 return `${uuid}-${resolution}-${videoOrPlaylist.getStringType()}${extension}`
@@ -95,15 +96,18 @@ function getLocalVideoFileMetadataUrl (video: MVideoUUID, videoFile: MVideoFile)
95// --------------------------------------------------------------------------- 96// ---------------------------------------------------------------------------
96 97
97export { 98export {
98 generateVideoStreamingPlaylistName, 99 generateHLSVideoFilename,
99 generateWebTorrentVideoName, 100 generateWebTorrentVideoFilename,
100 generateVideoFilename, 101
101 getVideoFilePath, 102 getVideoFilePath,
102 103
103 generateTorrentFileName, 104 generateTorrentFileName,
104 getTorrentFilePath, 105 getTorrentFilePath,
105 106
106 getHLSDirectory, 107 getHLSDirectory,
108 generateHLSMasterPlaylistFilename,
109 generateHlsSha256SegmentsFilename,
110 getHlsResolutionPlaylistFilename,
107 111
108 getLocalVideoFileMetadataUrl, 112 getLocalVideoFileMetadataUrl,
109 113
diff --git a/server/lib/video.ts b/server/lib/video.ts
index daf998704..61fee4949 100644
--- a/server/lib/video.ts
+++ b/server/lib/video.ts
@@ -5,7 +5,7 @@ import { sequelizeTypescript } from '@server/initializers/database'
5import { TagModel } from '@server/models/video/tag' 5import { TagModel } from '@server/models/video/tag'
6import { VideoModel } from '@server/models/video/video' 6import { VideoModel } from '@server/models/video/video'
7import { FilteredModelAttributes } from '@server/types' 7import { FilteredModelAttributes } from '@server/types'
8import { MThumbnail, MUserId, MVideo, MVideoFile, MVideoTag, MVideoThumbnail, MVideoUUID } from '@server/types/models' 8import { MThumbnail, MUserId, MVideoFile, MVideoTag, MVideoThumbnail, MVideoUUID } from '@server/types/models'
9import { ThumbnailType, VideoCreate, VideoPrivacy, VideoTranscodingPayload } from '@shared/models' 9import { ThumbnailType, VideoCreate, VideoPrivacy, VideoTranscodingPayload } from '@shared/models'
10import { federateVideoIfNeeded } from './activitypub/videos' 10import { federateVideoIfNeeded } from './activitypub/videos'
11import { JobQueue } from './job-queue/job-queue' 11import { JobQueue } from './job-queue/job-queue'
@@ -105,7 +105,7 @@ async function publishAndFederateIfNeeded (video: MVideoUUID, wasLive = false) {
105 } 105 }
106} 106}
107 107
108async function addOptimizeOrMergeAudioJob (video: MVideo, videoFile: MVideoFile, user: MUserId) { 108async function addOptimizeOrMergeAudioJob (video: MVideoUUID, videoFile: MVideoFile, user: MUserId) {
109 let dataInput: VideoTranscodingPayload 109 let dataInput: VideoTranscodingPayload
110 110
111 if (videoFile.isAudio()) { 111 if (videoFile.isAudio()) {