diff options
Diffstat (limited to 'server/lib')
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' | |||
4 | import { ActorLoadByUrlType } from '@server/lib/model-loaders' | 4 | import { ActorLoadByUrlType } from '@server/lib/model-loaders' |
5 | import { ActorModel } from '@server/models/actor/actor' | 5 | import { ActorModel } from '@server/models/actor/actor' |
6 | import { MActorAccountChannelId, MActorFull } from '@server/types/models' | 6 | import { MActorAccountChannelId, MActorFull } from '@server/types/models' |
7 | import { HttpStatusCode } from '@shared/core-utils' | 7 | import { HttpStatusCode } from '@shared/models' |
8 | import { fetchRemoteActor } from './shared' | 8 | import { fetchRemoteActor } from './shared' |
9 | import { APActorUpdater } from './updater' | 9 | import { APActorUpdater } from './updater' |
10 | import { getUrlFromWebfinger } from './webfinger' | 10 | import { 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 @@ | |||
1 | import { retryTransactionWrapper } from '@server/helpers/database-utils' | ||
1 | import * as Bluebird from 'bluebird' | 2 | import * as Bluebird from 'bluebird' |
2 | import { URL } from 'url' | 3 | import { URL } from 'url' |
3 | import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub' | 4 | import { 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 | ||
57 | export { | 58 | export { |
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 | ||
35 | function 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 | |||
34 | export { | 48 | export { |
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' | |||
2 | import { PeerTubeRequestError } from '@server/helpers/requests' | 2 | import { PeerTubeRequestError } from '@server/helpers/requests' |
3 | import { JobQueue } from '@server/lib/job-queue' | 3 | import { JobQueue } from '@server/lib/job-queue' |
4 | import { MVideoPlaylist, MVideoPlaylistOwner } from '@server/types/models' | 4 | import { MVideoPlaylist, MVideoPlaylistOwner } from '@server/types/models' |
5 | import { HttpStatusCode } from '@shared/core-utils' | 5 | import { HttpStatusCode } from '@shared/models' |
6 | import { createOrUpdateVideoPlaylist } from './create-update' | 6 | import { createOrUpdateVideoPlaylist } from './create-update' |
7 | import { fetchRemoteVideoPlaylist } from './shared' | 7 | import { 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 @@ | |||
1 | import { WEBSERVER } from '../../initializers/constants' | 1 | import { WEBSERVER } from '../../initializers/constants' |
2 | import { | 2 | import { |
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 | |||
118 | function getAbuseTargetUrl (abuse: MAbuseFull) { | ||
119 | return abuse.VideoAbuse?.Video?.url || | ||
120 | abuse.VideoCommentAbuse?.VideoComment?.url || | ||
121 | abuse.FlaggedAccount.Actor.url | ||
122 | } | ||
123 | |||
115 | export { | 124 | export { |
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' | |||
4 | import { VideoLoadByUrlType } from '@server/lib/model-loaders' | 4 | import { VideoLoadByUrlType } from '@server/lib/model-loaders' |
5 | import { VideoModel } from '@server/models/video/video' | 5 | import { VideoModel } from '@server/models/video/video' |
6 | import { MVideoAccountLightBlacklistAllFiles, MVideoThumbnail } from '@server/types/models' | 6 | import { MVideoAccountLightBlacklistAllFiles, MVideoThumbnail } from '@server/types/models' |
7 | import { HttpStatusCode } from '@shared/core-utils' | 7 | import { HttpStatusCode } from '@shared/models' |
8 | import { fetchRemoteVideo, SyncParam, syncVideoExternalAttributes } from './shared' | 8 | import { fetchRemoteVideo, SyncParam, syncVideoExternalAttributes } from './shared' |
9 | import { APVideoUpdater } from './updater' | 9 | import { 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 @@ | |||
1 | import { Transaction } from 'sequelize/types' | 1 | import { Transaction } from 'sequelize/types' |
2 | import { checkUrlsSameHost } from '@server/helpers/activitypub' | 2 | import { checkUrlsSameHost } from '@server/helpers/activitypub' |
3 | import { deleteNonExistingModels } from '@server/helpers/database-utils' | 3 | import { deleteAllModels, filterNonExistingModels } from '@server/helpers/database-utils' |
4 | import { logger, LoggerTagsFn } from '@server/helpers/logger' | 4 | import { logger, LoggerTagsFn } from '@server/helpers/logger' |
5 | import { updatePlaceholderThumbnail, updateVideoMiniatureFromUrl } from '@server/lib/thumbnail' | 5 | import { updatePlaceholderThumbnail, updateVideoMiniatureFromUrl } from '@server/lib/thumbnail' |
6 | import { setVideoTags } from '@server/lib/video' | 6 | import { 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' | |||
7 | import { getExtFromMimetype } from '@server/helpers/video' | 7 | import { getExtFromMimetype } from '@server/helpers/video' |
8 | import { ACTIVITY_PUB, MIMETYPES, P2P_MEDIA_LOADER_PEER_VERSION, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '@server/initializers/constants' | 8 | import { ACTIVITY_PUB, MIMETYPES, P2P_MEDIA_LOADER_PEER_VERSION, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '@server/initializers/constants' |
9 | import { generateTorrentFileName } from '@server/lib/video-paths' | 9 | import { generateTorrentFileName } from '@server/lib/video-paths' |
10 | import { VideoCaptionModel } from '@server/models/video/video-caption' | ||
10 | import { VideoFileModel } from '@server/models/video/video-file' | 11 | import { VideoFileModel } from '@server/models/video/video-file' |
11 | import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' | 12 | import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' |
12 | import { FilteredModelAttributes } from '@server/types' | 13 | import { FilteredModelAttributes } from '@server/types' |
13 | import { MChannelId, MStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoId } from '@server/types/models' | 14 | import { isStreamingPlaylist, MChannelId, MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoId } from '@server/types/models' |
14 | import { | 15 | import { |
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' |
26 | import { VideoCaptionModel } from '@server/models/video/video-caption' | ||
27 | 27 | ||
28 | function getThumbnailFromIcons (videoObject: VideoObject) { | 28 | function 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' | |||
5 | import { escapeHTML } from '@shared/core-utils/renderer' | 5 | import { escapeHTML } from '@shared/core-utils/renderer' |
6 | import { HTMLServerConfig } from '@shared/models' | 6 | import { HTMLServerConfig } from '@shared/models' |
7 | import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/core-utils/i18n/i18n' | 7 | import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/core-utils/i18n/i18n' |
8 | import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' | 8 | import { HttpStatusCode } from '../../shared/models/http/http-error-codes' |
9 | import { VideoPlaylistPrivacy, VideoPrivacy } from '../../shared/models/videos' | 9 | import { VideoPlaylistPrivacy, VideoPrivacy } from '../../shared/models/videos' |
10 | import { isTestInstance, sha256 } from '../helpers/core-utils' | 10 | import { isTestInstance, sha256 } from '../helpers/core-utils' |
11 | import { logger } from '../helpers/logger' | 11 | import { 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 @@ | |||
1 | import { readFileSync } from 'fs-extra' | 1 | import { readFileSync } from 'fs-extra' |
2 | import { merge } from 'lodash' | 2 | import { isArray, merge } from 'lodash' |
3 | import { createTransport, Transporter } from 'nodemailer' | 3 | import { createTransport, Transporter } from 'nodemailer' |
4 | import { join } from 'path' | 4 | import { join } from 'path' |
5 | import { VideoChannelModel } from '@server/models/video/video-channel' | 5 | import { EmailPayload } from '@shared/models' |
6 | import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist' | ||
7 | import { MVideoImport, MVideoImportVideo } from '@server/types/models/video/video-import' | ||
8 | import { AbuseState, EmailPayload, UserAbuse } from '@shared/models' | ||
9 | import { SendEmailDefaultOptions } from '../../shared/models/server/emailer.model' | 6 | import { SendEmailDefaultOptions } from '../../shared/models/server/emailer.model' |
10 | import { isTestInstance, root } from '../helpers/core-utils' | 7 | import { isTestInstance, root } from '../helpers/core-utils' |
11 | import { bunyanLogger, logger } from '../helpers/logger' | 8 | import { bunyanLogger, logger } from '../helpers/logger' |
12 | import { CONFIG, isEmailEnabled } from '../initializers/config' | 9 | import { CONFIG, isEmailEnabled } from '../initializers/config' |
13 | import { WEBSERVER } from '../initializers/constants' | 10 | import { WEBSERVER } from '../initializers/constants' |
14 | import { MAbuseFull, MAbuseMessage, MAccountDefault, MActorFollowActors, MActorFollowFull, MPlugin, MUser } from '../types/models' | 11 | import { MUser } from '../types/models' |
15 | import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video' | ||
16 | import { JobQueue } from './job-queue' | 12 | import { JobQueue } from './job-queue' |
17 | import { toSafeHtml } from '../helpers/markdown' | ||
18 | 13 | ||
19 | const Email = require('email-templates') | 14 | const 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 @@ | |||
1 | import { close, ensureDir, move, open, outputJSON, pathExists, read, readFile, remove, writeFile } from 'fs-extra' | 1 | import { close, ensureDir, move, open, outputJSON, pathExists, read, readFile, remove, writeFile } from 'fs-extra' |
2 | import { flatten, uniq } from 'lodash' | 2 | import { flatten, uniq } from 'lodash' |
3 | import { basename, dirname, join } from 'path' | 3 | import { basename, dirname, join } from 'path' |
4 | import { MVideoWithFile } from '@server/types/models' | 4 | import { MStreamingPlaylistFilesVideo, MVideoWithFile } from '@server/types/models' |
5 | import { sha256 } from '../helpers/core-utils' | 5 | import { sha256 } from '../helpers/core-utils' |
6 | import { getAudioStreamCodec, getVideoStreamCodec, getVideoStreamSize } from '../helpers/ffprobe-utils' | 6 | import { getAudioStreamCodec, getVideoStreamCodec, getVideoStreamSize } from '../helpers/ffprobe-utils' |
7 | import { logger } from '../helpers/logger' | 7 | import { logger } from '../helpers/logger' |
@@ -12,7 +12,7 @@ import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION } from | |||
12 | import { sequelizeTypescript } from '../initializers/database' | 12 | import { sequelizeTypescript } from '../initializers/database' |
13 | import { VideoFileModel } from '../models/video/video-file' | 13 | import { VideoFileModel } from '../models/video/video-file' |
14 | import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' | 14 | import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' |
15 | import { getVideoFilePath } from './video-paths' | 15 | import { getHlsResolutionPlaylistFilename, getVideoFilePath } from './video-paths' |
16 | 16 | ||
17 | async function updateStreamingPlaylistsInfohashesIfNeeded () { | 17 | async 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 | ||
32 | async function updateMasterHLSPlaylist (video: MVideoWithFile) { | 33 | async 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 | ||
67 | async function updateSha256VODSegments (video: MVideoWithFile) { | 71 | async 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' | |||
12 | import { VideoModel } from '@server/models/video/video' | 12 | import { VideoModel } from '@server/models/video/video' |
13 | import { VideoCommentModel } from '@server/models/video/video-comment' | 13 | import { VideoCommentModel } from '@server/models/video/video-comment' |
14 | import { VideoShareModel } from '@server/models/video/video-share' | 14 | import { VideoShareModel } from '@server/models/video/video-share' |
15 | import { HttpStatusCode } from '@shared/core-utils' | 15 | import { HttpStatusCode } from '@shared/models' |
16 | import { logger } from '../../../helpers/logger' | 16 | import { logger } from '../../../helpers/logger' |
17 | import { AccountVideoRateModel } from '../../../models/account/account-video-rate' | 17 | import { 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' | |||
2 | import { copy, stat } from 'fs-extra' | 2 | import { copy, stat } from 'fs-extra' |
3 | import { getLowercaseExtension } from '@server/helpers/core-utils' | 3 | import { getLowercaseExtension } from '@server/helpers/core-utils' |
4 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' | 4 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' |
5 | import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths' | 5 | import { generateWebTorrentVideoFilename, getVideoFilePath } from '@server/lib/video-paths' |
6 | import { UserModel } from '@server/models/user/user' | 6 | import { UserModel } from '@server/models/user/user' |
7 | import { MVideoFullLight } from '@server/types/models' | 7 | import { MVideoFullLight } from '@server/types/models' |
8 | import { VideoFileImportPayload } from '@shared/models' | 8 | import { 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' | |||
8 | import { ServerConfigManager } from '@server/lib/server-config-manager' | 8 | import { ServerConfigManager } from '@server/lib/server-config-manager' |
9 | import { isAbleToUploadVideo } from '@server/lib/user' | 9 | import { isAbleToUploadVideo } from '@server/lib/user' |
10 | import { addOptimizeOrMergeAudioJob } from '@server/lib/video' | 10 | import { addOptimizeOrMergeAudioJob } from '@server/lib/video' |
11 | import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths' | 11 | import { generateWebTorrentVideoFilename, getVideoFilePath } from '@server/lib/video-paths' |
12 | import { ThumbnailModel } from '@server/models/video/thumbnail' | 12 | import { ThumbnailModel } from '@server/models/video/thumbnail' |
13 | import { MVideoImportDefault, MVideoImportDefaultFiles, MVideoImportVideo } from '@server/types/models/video/video-import' | 13 | import { MVideoImportDefault, MVideoImportDefaultFiles, MVideoImportVideo } from '@server/types/models/video/video-import' |
14 | import { | 14 | import { |
@@ -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 | |||
7 | import { generateVideoMiniature } from '@server/lib/thumbnail' | 7 | import { generateVideoMiniature } from '@server/lib/thumbnail' |
8 | import { generateHlsPlaylistResolutionFromTS } from '@server/lib/transcoding/video-transcoding' | 8 | import { generateHlsPlaylistResolutionFromTS } from '@server/lib/transcoding/video-transcoding' |
9 | import { publishAndFederateIfNeeded } from '@server/lib/video' | 9 | import { publishAndFederateIfNeeded } from '@server/lib/video' |
10 | import { getHLSDirectory } from '@server/lib/video-paths' | 10 | import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename, getHLSDirectory } from '@server/lib/video-paths' |
11 | import { VideoModel } from '@server/models/video/video' | 11 | import { VideoModel } from '@server/models/video/video' |
12 | import { VideoFileModel } from '@server/models/video/video-file' | 12 | import { VideoFileModel } from '@server/models/video/video-file' |
13 | import { VideoLiveModel } from '@server/models/video/video-live' | 13 | import { VideoLiveModel } from '@server/models/video/video-live' |
14 | import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' | 14 | import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' |
15 | import { MVideo, MVideoLive } from '@server/types/models' | 15 | import { MStreamingPlaylist, MVideo, MVideoLive } from '@server/types/models' |
16 | import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models' | 16 | import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models' |
17 | import { logger } from '../../../helpers/logger' | 17 | import { 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 | ||
57 | async function saveLive (video: MVideo, live: MVideoLive) { | 57 | async 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 | ||
29 | type HandlerFunction = (job: Bull.Job, payload: VideoTranscodingPayload, video: MVideoFullLight, user: MUser) => Promise<any> | 29 | type HandlerFunction = (job: Bull.Job, payload: VideoTranscodingPayload, video: MVideoFullLight, user: MUser) => Promise<any> |
30 | 30 | ||
31 | const handlers: { [ id: string ]: HandlerFunction } = { | 31 | const 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' | |||
4 | import { computeResolutionsToTranscode, getVideoFileFPS, getVideoFileResolution } from '@server/helpers/ffprobe-utils' | 4 | import { computeResolutionsToTranscode, getVideoFileFPS, getVideoFileResolution } from '@server/helpers/ffprobe-utils' |
5 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | 5 | import { logger, loggerTagsFactory } from '@server/helpers/logger' |
6 | import { CONFIG, registerConfigChangedHandler } from '@server/initializers/config' | 6 | import { CONFIG, registerConfigChangedHandler } from '@server/initializers/config' |
7 | import { P2P_MEDIA_LOADER_PEER_VERSION, VIDEO_LIVE, VIEW_LIFETIME, WEBSERVER } from '@server/initializers/constants' | 7 | import { P2P_MEDIA_LOADER_PEER_VERSION, VIDEO_LIVE, VIEW_LIFETIME } from '@server/initializers/constants' |
8 | import { UserModel } from '@server/models/user/user' | 8 | import { UserModel } from '@server/models/user/user' |
9 | import { VideoModel } from '@server/models/video/video' | 9 | import { VideoModel } from '@server/models/video/video' |
10 | import { VideoLiveModel } from '@server/models/video/video-live' | 10 | import { VideoLiveModel } from '@server/models/video/video-live' |
11 | import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' | 11 | import { VideoStreamingPlaylistModel } from '@server/models/video/video-streaming-playlist' |
12 | import { MStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoLiveVideo } from '@server/types/models' | 12 | import { MStreamingPlaylistVideo, MVideo, MVideoLiveVideo } from '@server/types/models' |
13 | import { VideoState, VideoStreamingPlaylistType } from '@shared/models' | 13 | import { VideoState, VideoStreamingPlaylistType } from '@shared/models' |
14 | import { federateVideoIfNeeded } from '../activitypub/videos' | 14 | import { federateVideoIfNeeded } from '../activitypub/videos' |
15 | import { JobQueue } from '../job-queue' | 15 | import { JobQueue } from '../job-queue' |
16 | import { PeerTubeSocket } from '../peertube-socket' | 16 | import { PeerTubeSocket } from '../peertube-socket' |
17 | import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename } from '../video-paths' | ||
17 | import { LiveQuotaStore } from './live-quota-store' | 18 | import { LiveQuotaStore } from './live-quota-store' |
18 | import { LiveSegmentShaStore } from './live-segment-sha-store' | 19 | import { LiveSegmentShaStore } from './live-segment-sha-store' |
19 | import { cleanupLive } from './live-utils' | 20 | import { cleanupLive } from './live-utils' |
20 | import { MuxingSession } from './shared' | 21 | import { MuxingSession } from './shared' |
21 | 22 | ||
22 | const NodeRtmpSession = require('node-media-server/node_rtmp_session') | 23 | const NodeRtmpSession = require('node-media-server/src/node_rtmp_session') |
23 | const context = require('node-media-server/node_core_ctx') | 24 | const context = require('node-media-server/src/node_core_ctx') |
24 | const nodeMediaServerLogger = require('node-media-server/node_core_logger') | 25 | const nodeMediaServerLogger = require('node-media-server/src/node_core_logger') |
25 | 26 | ||
26 | // Disable node media server logs | 27 | // Disable node media server logs |
27 | nodeMediaServerLogger.setLogType(0) | 28 | nodeMediaServerLogger.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' | |||
23 | import { VideoObject } from '../../shared/models/activitypub/objects' | 23 | import { VideoObject } from '../../shared/models/activitypub/objects' |
24 | import { VideoCommentObject } from '../../shared/models/activitypub/objects/video-comment-object' | 24 | import { VideoCommentObject } from '../../shared/models/activitypub/objects/video-comment-object' |
25 | import { LiveVideoCreate, VideoCreate, VideoImportCreate } from '../../shared/models/videos' | 25 | import { LiveVideoCreate, VideoCreate, VideoImportCreate } from '../../shared/models/videos' |
26 | import { VideoCommentCreate } from '../../shared/models/videos/comment/video-comment.model' | 26 | import { VideoCommentCreate } from '../../shared/models/videos/comment' |
27 | import { ActorModel } from '../models/actor/actor' | 27 | import { ActorModel } from '../models/actor/actor' |
28 | import { UserModel } from '../models/user/user' | 28 | import { UserModel } from '../models/user/user' |
29 | import { VideoModel } from '../models/video/video' | 29 | import { 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 @@ | |||
1 | import { AccountModel } from '@server/models/account/account' | ||
2 | import { getServerActor } from '@server/models/application/application' | ||
3 | import { ServerBlocklistModel } from '@server/models/server/server-blocklist' | ||
4 | import { | ||
5 | MUser, | ||
6 | MUserAccount, | ||
7 | MUserDefault, | ||
8 | MUserNotifSettingAccount, | ||
9 | MUserWithNotificationSetting, | ||
10 | UserNotificationModelForApi | ||
11 | } from '@server/types/models/user' | ||
12 | import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist' | ||
13 | import { MVideoImportVideo } from '@server/types/models/video/video-import' | ||
14 | import { UserAbuse } from '@shared/models' | ||
15 | import { UserNotificationSettingValue, UserNotificationType, UserRight } from '../../shared/models/users' | ||
16 | import { VideoPrivacy, VideoState } from '../../shared/models/videos' | ||
17 | import { logger } from '../helpers/logger' | ||
18 | import { CONFIG } from '../initializers/config' | ||
19 | import { AccountBlocklistModel } from '../models/account/account-blocklist' | ||
20 | import { UserModel } from '../models/user/user' | ||
21 | import { UserNotificationModel } from '../models/user/user-notification' | ||
22 | import { MAbuseFull, MAbuseMessage, MAccountServer, MActorFollowFull, MApplication, MPlugin } from '../types/models' | ||
23 | import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video' | ||
24 | import { isBlockedByServerOrAccount } from './blocklist' | ||
25 | import { Emailer } from './emailer' | ||
26 | import { PeerTubeSocket } from './peertube-socket' | ||
27 | |||
28 | class 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 | |||
794 | export { | ||
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 @@ | |||
1 | import { MUser, MUserDefault } from '@server/types/models/user' | ||
2 | import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist' | ||
3 | import { UserNotificationSettingValue } from '../../../shared/models/users' | ||
4 | import { logger } from '../../helpers/logger' | ||
5 | import { CONFIG } from '../../initializers/config' | ||
6 | import { MAbuseFull, MAbuseMessage, MActorFollowFull, MApplication, MPlugin } from '../../types/models' | ||
7 | import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../../types/models/video' | ||
8 | import { JobQueue } from '../job-queue' | ||
9 | import { PeerTubeSocket } from '../peertube-socket' | ||
10 | import { | ||
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 | |||
36 | class 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 | |||
257 | export { | ||
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 @@ | |||
1 | import { WEBSERVER } from '@server/initializers/constants' | ||
2 | import { AccountModel } from '@server/models/account/account' | ||
3 | import { UserNotificationModel } from '@server/models/user/user-notification' | ||
4 | import { MAbuseFull, MAbuseMessage, MAccountDefault, MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/types/models' | ||
5 | import { UserNotificationType } from '@shared/models' | ||
6 | import { AbstractNotification } from '../common/abstract-notification' | ||
7 | |||
8 | export type NewAbuseMessagePayload = { | ||
9 | abuse: MAbuseFull | ||
10 | message: MAbuseMessage | ||
11 | } | ||
12 | |||
13 | export 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 @@ | |||
1 | import { logger } from '@server/helpers/logger' | ||
2 | import { WEBSERVER } from '@server/initializers/constants' | ||
3 | import { getAbuseTargetUrl } from '@server/lib/activitypub/url' | ||
4 | import { UserModel } from '@server/models/user/user' | ||
5 | import { UserNotificationModel } from '@server/models/user/user-notification' | ||
6 | import { MAbuseFull, MUserDefault, MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/types/models' | ||
7 | import { AbuseState, UserNotificationType } from '@shared/models' | ||
8 | import { AbstractNotification } from '../common/abstract-notification' | ||
9 | |||
10 | export 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 @@ | |||
1 | export * from './abuse-state-change-for-reporter' | ||
2 | export * from './new-abuse-for-moderators' | ||
3 | export * from './new-abuse-message-for-reporter' | ||
4 | export * 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 @@ | |||
1 | import { logger } from '@server/helpers/logger' | ||
2 | import { WEBSERVER } from '@server/initializers/constants' | ||
3 | import { getAbuseTargetUrl } from '@server/lib/activitypub/url' | ||
4 | import { UserModel } from '@server/models/user/user' | ||
5 | import { UserNotificationModel } from '@server/models/user/user-notification' | ||
6 | import { MAbuseFull, MUserDefault, MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/types/models' | ||
7 | import { UserAbuse, UserNotificationType, UserRight } from '@shared/models' | ||
8 | import { AbstractNotification } from '../common/abstract-notification' | ||
9 | |||
10 | export type NewAbusePayload = { abuse: UserAbuse, abuseInstance: MAbuseFull, reporter: string } | ||
11 | |||
12 | export 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 @@ | |||
1 | import { logger } from '@server/helpers/logger' | ||
2 | import { getAbuseTargetUrl } from '@server/lib/activitypub/url' | ||
3 | import { UserModel } from '@server/models/user/user' | ||
4 | import { MUserDefault } from '@server/types/models' | ||
5 | import { UserRight } from '@shared/models' | ||
6 | import { AbstractNewAbuseMessage } from './abstract-new-abuse-message' | ||
7 | |||
8 | export 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 @@ | |||
1 | import { logger } from '@server/helpers/logger' | ||
2 | import { getAbuseTargetUrl } from '@server/lib/activitypub/url' | ||
3 | import { UserModel } from '@server/models/user/user' | ||
4 | import { MUserDefault } from '@server/types/models' | ||
5 | import { AbstractNewAbuseMessage } from './abstract-new-abuse-message' | ||
6 | |||
7 | export 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 @@ | |||
1 | export * from './new-auto-blacklist-for-moderators' | ||
2 | export * from './new-blacklist-for-owner' | ||
3 | export * 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 @@ | |||
1 | import { logger } from '@server/helpers/logger' | ||
2 | import { WEBSERVER } from '@server/initializers/constants' | ||
3 | import { UserModel } from '@server/models/user/user' | ||
4 | import { UserNotificationModel } from '@server/models/user/user-notification' | ||
5 | import { VideoChannelModel } from '@server/models/video/video-channel' | ||
6 | import { MUserDefault, MUserWithNotificationSetting, MVideoBlacklistLightVideo, UserNotificationModelForApi } from '@server/types/models' | ||
7 | import { UserNotificationType, UserRight } from '@shared/models' | ||
8 | import { AbstractNotification } from '../common/abstract-notification' | ||
9 | |||
10 | export 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 @@ | |||
1 | import { logger } from '@server/helpers/logger' | ||
2 | import { CONFIG } from '@server/initializers/config' | ||
3 | import { WEBSERVER } from '@server/initializers/constants' | ||
4 | import { UserModel } from '@server/models/user/user' | ||
5 | import { UserNotificationModel } from '@server/models/user/user-notification' | ||
6 | import { MUserDefault, MUserWithNotificationSetting, MVideoBlacklistVideo, UserNotificationModelForApi } from '@server/types/models' | ||
7 | import { UserNotificationType } from '@shared/models' | ||
8 | import { AbstractNotification } from '../common/abstract-notification' | ||
9 | |||
10 | export 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 @@ | |||
1 | import { logger } from '@server/helpers/logger' | ||
2 | import { CONFIG } from '@server/initializers/config' | ||
3 | import { WEBSERVER } from '@server/initializers/constants' | ||
4 | import { UserModel } from '@server/models/user/user' | ||
5 | import { UserNotificationModel } from '@server/models/user/user-notification' | ||
6 | import { MUserDefault, MUserWithNotificationSetting, MVideoFullLight, UserNotificationModelForApi } from '@server/types/models' | ||
7 | import { UserNotificationType } from '@shared/models' | ||
8 | import { AbstractNotification } from '../common/abstract-notification' | ||
9 | |||
10 | export 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 @@ | |||
1 | import { logger } from '@server/helpers/logger' | ||
2 | import { toSafeHtml } from '@server/helpers/markdown' | ||
3 | import { WEBSERVER } from '@server/initializers/constants' | ||
4 | import { AccountBlocklistModel } from '@server/models/account/account-blocklist' | ||
5 | import { getServerActor } from '@server/models/application/application' | ||
6 | import { ServerBlocklistModel } from '@server/models/server/server-blocklist' | ||
7 | import { UserModel } from '@server/models/user/user' | ||
8 | import { UserNotificationModel } from '@server/models/user/user-notification' | ||
9 | import { | ||
10 | MCommentOwnerVideo, | ||
11 | MUserDefault, | ||
12 | MUserNotifSettingAccount, | ||
13 | MUserWithNotificationSetting, | ||
14 | UserNotificationModelForApi | ||
15 | } from '@server/types/models' | ||
16 | import { UserNotificationSettingValue, UserNotificationType } from '@shared/models' | ||
17 | import { AbstractNotification } from '../common' | ||
18 | |||
19 | export 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 @@ | |||
1 | export * from './comment-mention' | ||
2 | export * 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 @@ | |||
1 | import { logger } from '@server/helpers/logger' | ||
2 | import { toSafeHtml } from '@server/helpers/markdown' | ||
3 | import { WEBSERVER } from '@server/initializers/constants' | ||
4 | import { isBlockedByServerOrAccount } from '@server/lib/blocklist' | ||
5 | import { UserModel } from '@server/models/user/user' | ||
6 | import { UserNotificationModel } from '@server/models/user/user-notification' | ||
7 | import { MCommentOwnerVideo, MUserDefault, MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/types/models' | ||
8 | import { UserNotificationType } from '@shared/models' | ||
9 | import { AbstractNotification } from '../common/abstract-notification' | ||
10 | |||
11 | export 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 @@ | |||
1 | import { MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/types/models' | ||
2 | import { EmailPayload, UserNotificationSettingValue } from '@shared/models' | ||
3 | |||
4 | export 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 @@ | |||
1 | import { logger } from '@server/helpers/logger' | ||
2 | import { UserModel } from '@server/models/user/user' | ||
3 | import { UserNotificationModel } from '@server/models/user/user-notification' | ||
4 | import { MActorFollowFull, MUserDefault, MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/types/models' | ||
5 | import { UserNotificationType, UserRight } from '@shared/models' | ||
6 | import { AbstractNotification } from '../common/abstract-notification' | ||
7 | |||
8 | export 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 @@ | |||
1 | import { logger } from '@server/helpers/logger' | ||
2 | import { WEBSERVER } from '@server/initializers/constants' | ||
3 | import { isBlockedByServerOrAccount } from '@server/lib/blocklist' | ||
4 | import { UserModel } from '@server/models/user/user' | ||
5 | import { UserNotificationModel } from '@server/models/user/user-notification' | ||
6 | import { MActorFollowFull, MUserDefault, MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/types/models' | ||
7 | import { UserNotificationType, UserRight } from '@shared/models' | ||
8 | import { AbstractNotification } from '../common/abstract-notification' | ||
9 | |||
10 | export 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 @@ | |||
1 | import { logger } from '@server/helpers/logger' | ||
2 | import { isBlockedByServerOrAccount } from '@server/lib/blocklist' | ||
3 | import { UserModel } from '@server/models/user/user' | ||
4 | import { UserNotificationModel } from '@server/models/user/user-notification' | ||
5 | import { MActorFollowFull, MUserDefault, MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/types/models' | ||
6 | import { UserNotificationType } from '@shared/models' | ||
7 | import { AbstractNotification } from '../common/abstract-notification' | ||
8 | |||
9 | export 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 @@ | |||
1 | export * from './auto-follow-for-instance' | ||
2 | export * from './follow-for-instance' | ||
3 | export * 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 @@ | |||
1 | export * from './abuse' | ||
2 | export * from './blacklist' | ||
3 | export * from './comment' | ||
4 | export * from './common' | ||
5 | export * from './follow' | ||
6 | export * from './instance' | ||
7 | export * 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 @@ | |||
1 | export * from './new-peertube-version-for-admins' | ||
2 | export * from './new-plugin-version-for-admins' | ||
3 | export * 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 @@ | |||
1 | import { logger } from '@server/helpers/logger' | ||
2 | import { UserModel } from '@server/models/user/user' | ||
3 | import { UserNotificationModel } from '@server/models/user/user-notification' | ||
4 | import { MApplication, MUserDefault, MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/types/models' | ||
5 | import { UserNotificationType, UserRight } from '@shared/models' | ||
6 | import { AbstractNotification } from '../common/abstract-notification' | ||
7 | |||
8 | export type NewPeerTubeVersionForAdminsPayload = { | ||
9 | application: MApplication | ||
10 | latestVersion: string | ||
11 | } | ||
12 | |||
13 | export 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 @@ | |||
1 | import { logger } from '@server/helpers/logger' | ||
2 | import { WEBSERVER } from '@server/initializers/constants' | ||
3 | import { UserModel } from '@server/models/user/user' | ||
4 | import { UserNotificationModel } from '@server/models/user/user-notification' | ||
5 | import { MPlugin, MUserDefault, MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/types/models' | ||
6 | import { UserNotificationType, UserRight } from '@shared/models' | ||
7 | import { AbstractNotification } from '../common/abstract-notification' | ||
8 | |||
9 | export 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 @@ | |||
1 | import { logger } from '@server/helpers/logger' | ||
2 | import { CONFIG } from '@server/initializers/config' | ||
3 | import { UserModel } from '@server/models/user/user' | ||
4 | import { UserNotificationModel } from '@server/models/user/user-notification' | ||
5 | import { MUserDefault, MUserWithNotificationSetting, UserNotificationModelForApi } from '@server/types/models' | ||
6 | import { UserNotificationType, UserRight } from '@shared/models' | ||
7 | import { AbstractNotification } from '../common/abstract-notification' | ||
8 | |||
9 | export 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 @@ | |||
1 | import { logger } from '@server/helpers/logger' | ||
2 | import { WEBSERVER } from '@server/initializers/constants' | ||
3 | import { UserModel } from '@server/models/user/user' | ||
4 | import { UserNotificationModel } from '@server/models/user/user-notification' | ||
5 | import { MUserDefault, MUserWithNotificationSetting, MVideoFullLight, UserNotificationModelForApi } from '@server/types/models' | ||
6 | import { UserNotificationType } from '@shared/models' | ||
7 | import { AbstractNotification } from '../common/abstract-notification' | ||
8 | |||
9 | export 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 @@ | |||
1 | import { logger } from '@server/helpers/logger' | ||
2 | import { WEBSERVER } from '@server/initializers/constants' | ||
3 | import { UserModel } from '@server/models/user/user' | ||
4 | import { UserNotificationModel } from '@server/models/user/user-notification' | ||
5 | import { MUserDefault, MUserWithNotificationSetting, MVideoImportVideo, UserNotificationModelForApi } from '@server/types/models' | ||
6 | import { UserNotificationType } from '@shared/models' | ||
7 | import { AbstractNotification } from '../common/abstract-notification' | ||
8 | |||
9 | export type ImportFinishedForOwnerPayload = { | ||
10 | videoImport: MVideoImportVideo | ||
11 | success: boolean | ||
12 | } | ||
13 | |||
14 | export 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 @@ | |||
1 | export * from './new-video-for-subscribers' | ||
2 | export * from './import-finished-for-owner' | ||
3 | export * from './owned-publication-after-auto-unblacklist' | ||
4 | export * from './owned-publication-after-schedule-update' | ||
5 | export * 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 @@ | |||
1 | import { logger } from '@server/helpers/logger' | ||
2 | import { WEBSERVER } from '@server/initializers/constants' | ||
3 | import { UserModel } from '@server/models/user/user' | ||
4 | import { UserNotificationModel } from '@server/models/user/user-notification' | ||
5 | import { MUserWithNotificationSetting, MVideoAccountLight, UserNotificationModelForApi } from '@server/types/models' | ||
6 | import { UserNotificationType, VideoPrivacy, VideoState } from '@shared/models' | ||
7 | import { AbstractNotification } from '../common/abstract-notification' | ||
8 | |||
9 | export 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 | |||
2 | import { VideoState } from '@shared/models' | ||
3 | import { AbstractOwnedVideoPublication } from './abstract-owned-video-publication' | ||
4 | |||
5 | export 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 @@ | |||
1 | import { VideoState } from '@shared/models' | ||
2 | import { AbstractOwnedVideoPublication } from './abstract-owned-video-publication' | ||
3 | |||
4 | export 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 @@ | |||
1 | import { AbstractOwnedVideoPublication } from './abstract-owned-video-publication' | ||
2 | |||
3 | export 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 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { logger } from '@server/helpers/logger' | 2 | import { logger } from '@server/helpers/logger' |
3 | import { | ||
4 | VIDEO_CATEGORIES, | ||
5 | VIDEO_LANGUAGES, | ||
6 | VIDEO_LICENCES, | ||
7 | VIDEO_PLAYLIST_PRIVACIES, | ||
8 | VIDEO_PRIVACIES | ||
9 | } from '@server/initializers/constants' | ||
10 | import { onExternalUserAuthenticated } from '@server/lib/auth/external-auth' | 3 | import { onExternalUserAuthenticated } from '@server/lib/auth/external-auth' |
4 | import { VideoConstantManagerFactory } from '@server/lib/plugins/video-constant-manager-factory' | ||
11 | import { PluginModel } from '@server/models/server/plugin' | 5 | import { PluginModel } from '@server/models/server/plugin' |
12 | import { | 6 | import { |
13 | RegisterServerAuthExternalOptions, | 7 | RegisterServerAuthExternalOptions, |
@@ -18,41 +12,18 @@ import { | |||
18 | } from '@server/types/plugins' | 12 | } from '@server/types/plugins' |
19 | import { | 13 | import { |
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' |
32 | import { VideoTranscodingProfilesManager } from '../transcoding/video-transcoding-profiles' | 23 | import { VideoTranscodingProfilesManager } from '../transcoding/video-transcoding-profiles' |
33 | import { buildPluginHelpers } from './plugin-helpers-builder' | 24 | import { buildPluginHelpers } from './plugin-helpers-builder' |
34 | 25 | ||
35 | type AlterableVideoConstant = 'language' | 'licence' | 'category' | 'privacy' | 'playlistPrivacy' | ||
36 | type VideoConstant = { [key in number | string]: string } | ||
37 | |||
38 | type 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 | |||
47 | export class RegisterHelpers { | 26 | export 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 @@ | |||
1 | import { logger } from '@server/helpers/logger' | ||
2 | import { | ||
3 | VIDEO_CATEGORIES, | ||
4 | VIDEO_LANGUAGES, | ||
5 | VIDEO_LICENCES, | ||
6 | VIDEO_PLAYLIST_PRIVACIES, | ||
7 | VIDEO_PRIVACIES | ||
8 | } from '@server/initializers/constants' | ||
9 | import { ConstantManager } from '@shared/models/plugins/server/plugin-constant-manager.model' | ||
10 | |||
11 | type AlterableVideoConstant = 'language' | 'licence' | 'category' | 'privacy' | 'playlistPrivacy' | ||
12 | type VideoConstant = Record<number | string, string> | ||
13 | |||
14 | type UpdatedVideoConstant = { | ||
15 | [name in AlterableVideoConstant]: { | ||
16 | [ npmName: string]: { | ||
17 | added: VideoConstant[] | ||
18 | deleted: VideoConstant[] | ||
19 | } | ||
20 | } | ||
21 | } | ||
22 | |||
23 | const 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 | |||
31 | export 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 @@ | |||
1 | import { chunk } from 'lodash' | ||
2 | import { compareSemVer } from '@shared/core-utils' | ||
1 | import { logger } from '../../helpers/logger' | 3 | import { logger } from '../../helpers/logger' |
2 | import { AbstractScheduler } from './abstract-scheduler' | ||
3 | import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants' | ||
4 | import { CONFIG } from '../../initializers/config' | 4 | import { CONFIG } from '../../initializers/config' |
5 | import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants' | ||
5 | import { PluginModel } from '../../models/server/plugin' | 6 | import { PluginModel } from '../../models/server/plugin' |
6 | import { chunk } from 'lodash' | ||
7 | import { getLatestPluginsVersion } from '../plugins/plugin-index' | ||
8 | import { compareSemVer } from '../../../shared/core-utils/miscs/miscs' | ||
9 | import { Notifier } from '../notifier' | 7 | import { Notifier } from '../notifier' |
8 | import { getLatestPluginsVersion } from '../plugins/plugin-index' | ||
9 | import { AbstractScheduler } from './abstract-scheduler' | ||
10 | 10 | ||
11 | export class PluginsCheckScheduler extends AbstractScheduler { | 11 | export 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 | |||
10 | import { canDoQuickTranscode, getDurationFromVideoFile, getMetadataFromFile, getVideoFileFPS } from '../../helpers/ffprobe-utils' | 10 | import { canDoQuickTranscode, getDurationFromVideoFile, getMetadataFromFile, getVideoFileFPS } from '../../helpers/ffprobe-utils' |
11 | import { logger } from '../../helpers/logger' | 11 | import { logger } from '../../helpers/logger' |
12 | import { CONFIG } from '../../initializers/config' | 12 | import { CONFIG } from '../../initializers/config' |
13 | import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSERVER } from '../../initializers/constants' | 13 | import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION } from '../../initializers/constants' |
14 | import { VideoFileModel } from '../../models/video/video-file' | 14 | import { VideoFileModel } from '../../models/video/video-file' |
15 | import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist' | 15 | import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist' |
16 | import { updateMasterHLSPlaylist, updateSha256VODSegments } from '../hls' | 16 | import { updateMasterHLSPlaylist, updateSha256VODSegments } from '../hls' |
17 | import { generateVideoFilename, generateVideoStreamingPlaylistName, getVideoFilePath } from '../video-paths' | 17 | import { |
18 | generateHLSMasterPlaylistFilename, | ||
19 | generateHlsSha256SegmentsFilename, | ||
20 | generateHLSVideoFilename, | ||
21 | generateWebTorrentVideoFilename, | ||
22 | getHlsResolutionPlaylistFilename, | ||
23 | getVideoFilePath | ||
24 | } from '../video-paths' | ||
18 | import { VideoTranscodingProfilesManager } from './video-transcoding-profiles' | 25 | import { 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' | |||
3 | import { CONFIG } from '@server/initializers/config' | 3 | import { CONFIG } from '@server/initializers/config' |
4 | import { HLS_REDUNDANCY_DIRECTORY, HLS_STREAMING_PLAYLIST_DIRECTORY, STATIC_PATHS, WEBSERVER } from '@server/initializers/constants' | 4 | import { HLS_REDUNDANCY_DIRECTORY, HLS_STREAMING_PLAYLIST_DIRECTORY, STATIC_PATHS, WEBSERVER } from '@server/initializers/constants' |
5 | import { isStreamingPlaylist, MStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoUUID } from '@server/types/models' | 5 | import { isStreamingPlaylist, MStreamingPlaylist, MStreamingPlaylistVideo, MVideo, MVideoFile, MVideoUUID } from '@server/types/models' |
6 | import { buildUUID } from '@server/helpers/uuid' | ||
7 | import { removeFragmentedMP4Ext } from '@shared/core-utils' | ||
6 | 8 | ||
7 | // ################## Video file name ################## | 9 | // ################## Video file name ################## |
8 | 10 | ||
9 | function generateVideoFilename (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, isHls: boolean, resolution: number, extname: string) { | 11 | function 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 | ||
23 | function generateVideoStreamingPlaylistName (uuid: string, resolution: number) { | 15 | function generateHLSVideoFilename (resolution: number) { |
24 | return `${uuid}-${resolution}-fragmented.mp4` | 16 | return `${buildUUID()}-${resolution}-fragmented.mp4` |
25 | } | ||
26 | |||
27 | function generateWebTorrentVideoName (uuid: string, resolution: number, extname: string) { | ||
28 | return uuid + '-' + resolution + extname | ||
29 | } | 17 | } |
30 | 18 | ||
31 | function getVideoFilePath (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, videoFile: MVideoFile, isRedundancy = false) { | 19 | function 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 | ||
54 | function getHlsResolutionPlaylistFilename (videoFilename: string) { | ||
55 | // Video file name already contain resolution | ||
56 | return removeFragmentedMP4Ext(videoFilename) + '.m3u8' | ||
57 | } | ||
58 | |||
59 | function generateHLSMasterPlaylistFilename (isLive = false) { | ||
60 | if (isLive) return 'master.m3u8' | ||
61 | |||
62 | return buildUUID() + '-master.m3u8' | ||
63 | } | ||
64 | |||
65 | function 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 | ||
68 | function generateTorrentFileName (videoOrPlaylist: MVideo | MStreamingPlaylistVideo, resolution: number) { | 73 | function 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 | ||
97 | export { | 98 | export { |
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' | |||
5 | import { TagModel } from '@server/models/video/tag' | 5 | import { TagModel } from '@server/models/video/tag' |
6 | import { VideoModel } from '@server/models/video/video' | 6 | import { VideoModel } from '@server/models/video/video' |
7 | import { FilteredModelAttributes } from '@server/types' | 7 | import { FilteredModelAttributes } from '@server/types' |
8 | import { MThumbnail, MUserId, MVideo, MVideoFile, MVideoTag, MVideoThumbnail, MVideoUUID } from '@server/types/models' | 8 | import { MThumbnail, MUserId, MVideoFile, MVideoTag, MVideoThumbnail, MVideoUUID } from '@server/types/models' |
9 | import { ThumbnailType, VideoCreate, VideoPrivacy, VideoTranscodingPayload } from '@shared/models' | 9 | import { ThumbnailType, VideoCreate, VideoPrivacy, VideoTranscodingPayload } from '@shared/models' |
10 | import { federateVideoIfNeeded } from './activitypub/videos' | 10 | import { federateVideoIfNeeded } from './activitypub/videos' |
11 | import { JobQueue } from './job-queue/job-queue' | 11 | import { JobQueue } from './job-queue/job-queue' |
@@ -105,7 +105,7 @@ async function publishAndFederateIfNeeded (video: MVideoUUID, wasLive = false) { | |||
105 | } | 105 | } |
106 | } | 106 | } |
107 | 107 | ||
108 | async function addOptimizeOrMergeAudioJob (video: MVideo, videoFile: MVideoFile, user: MUserId) { | 108 | async 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()) { |