aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib
diff options
context:
space:
mode:
Diffstat (limited to 'server/lib')
-rw-r--r--server/lib/emailer.ts61
-rw-r--r--server/lib/job-queue/handlers/video-file.ts24
-rw-r--r--server/lib/job-queue/handlers/video-import.ts3
-rw-r--r--server/lib/notifier.ts76
-rw-r--r--server/lib/schedulers/update-videos-scheduler.ts14
-rw-r--r--server/lib/user.ts2
6 files changed, 170 insertions, 10 deletions
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts
index d766e655b..6dc8f2adf 100644
--- a/server/lib/emailer.ts
+++ b/server/lib/emailer.ts
@@ -10,6 +10,7 @@ import { readFileSync } from 'fs-extra'
10import { VideoCommentModel } from '../models/video/video-comment' 10import { VideoCommentModel } from '../models/video/video-comment'
11import { VideoAbuseModel } from '../models/video/video-abuse' 11import { VideoAbuseModel } from '../models/video/video-abuse'
12import { VideoBlacklistModel } from '../models/video/video-blacklist' 12import { VideoBlacklistModel } from '../models/video/video-blacklist'
13import { VideoImportModel } from '../models/video/video-import'
13 14
14class Emailer { 15class Emailer {
15 16
@@ -102,6 +103,66 @@ class Emailer {
102 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 103 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
103 } 104 }
104 105
106 myVideoPublishedNotification (to: string[], video: VideoModel) {
107 const videoUrl = CONFIG.WEBSERVER.URL + video.getWatchStaticPath()
108
109 const text = `Hi dear user,\n\n` +
110 `Your video ${video.name} has been published.` +
111 `\n\n` +
112 `You can view it on ${videoUrl} ` +
113 `\n\n` +
114 `Cheers,\n` +
115 `PeerTube.`
116
117 const emailPayload: EmailPayload = {
118 to,
119 subject: `Your video ${video.name} is published`,
120 text
121 }
122
123 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
124 }
125
126 myVideoImportSuccessNotification (to: string[], videoImport: VideoImportModel) {
127 const videoUrl = CONFIG.WEBSERVER.URL + videoImport.Video.getWatchStaticPath()
128
129 const text = `Hi dear user,\n\n` +
130 `Your video import ${videoImport.getTargetIdentifier()} is finished.` +
131 `\n\n` +
132 `You can view the imported video on ${videoUrl} ` +
133 `\n\n` +
134 `Cheers,\n` +
135 `PeerTube.`
136
137 const emailPayload: EmailPayload = {
138 to,
139 subject: `Your video import ${videoImport.getTargetIdentifier()} is finished`,
140 text
141 }
142
143 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
144 }
145
146 myVideoImportErrorNotification (to: string[], videoImport: VideoImportModel) {
147 const importUrl = CONFIG.WEBSERVER.URL + '/my-account/video-imports'
148
149 const text = `Hi dear user,\n\n` +
150 `Your video import ${videoImport.getTargetIdentifier()} encountered an error.` +
151 `\n\n` +
152 `See your videos import dashboard for more information: ${importUrl}` +
153 `\n\n` +
154 `Cheers,\n` +
155 `PeerTube.`
156
157 const emailPayload: EmailPayload = {
158 to,
159 subject: `Your video import ${videoImport.getTargetIdentifier()} encountered an error`,
160 text
161 }
162
163 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
164 }
165
105 addNewCommentOnMyVideoNotification (to: string[], comment: VideoCommentModel) { 166 addNewCommentOnMyVideoNotification (to: string[], comment: VideoCommentModel) {
106 const accountName = comment.Account.getDisplayName() 167 const accountName = comment.Account.getDisplayName()
107 const video = comment.Video 168 const video = comment.Video
diff --git a/server/lib/job-queue/handlers/video-file.ts b/server/lib/job-queue/handlers/video-file.ts
index 480d324dc..593e43cc5 100644
--- a/server/lib/job-queue/handlers/video-file.ts
+++ b/server/lib/job-queue/handlers/video-file.ts
@@ -68,17 +68,17 @@ async function processVideoFile (job: Bull.Job) {
68async function onVideoFileTranscoderOrImportSuccess (video: VideoModel) { 68async function onVideoFileTranscoderOrImportSuccess (video: VideoModel) {
69 if (video === undefined) return undefined 69 if (video === undefined) return undefined
70 70
71 const { videoDatabase, isNewVideo } = await sequelizeTypescript.transaction(async t => { 71 const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => {
72 // Maybe the video changed in database, refresh it 72 // Maybe the video changed in database, refresh it
73 let videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t) 73 let videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t)
74 // Video does not exist anymore 74 // Video does not exist anymore
75 if (!videoDatabase) return undefined 75 if (!videoDatabase) return undefined
76 76
77 let isNewVideo = false 77 let videoPublished = false
78 78
79 // We transcoded the video file in another format, now we can publish it 79 // We transcoded the video file in another format, now we can publish it
80 if (videoDatabase.state !== VideoState.PUBLISHED) { 80 if (videoDatabase.state !== VideoState.PUBLISHED) {
81 isNewVideo = true 81 videoPublished = true
82 82
83 videoDatabase.state = VideoState.PUBLISHED 83 videoDatabase.state = VideoState.PUBLISHED
84 videoDatabase.publishedAt = new Date() 84 videoDatabase.publishedAt = new Date()
@@ -86,12 +86,15 @@ async function onVideoFileTranscoderOrImportSuccess (video: VideoModel) {
86 } 86 }
87 87
88 // If the video was not published, we consider it is a new one for other instances 88 // If the video was not published, we consider it is a new one for other instances
89 await federateVideoIfNeeded(videoDatabase, isNewVideo, t) 89 await federateVideoIfNeeded(videoDatabase, videoPublished, t)
90 90
91 return { videoDatabase, isNewVideo } 91 return { videoDatabase, videoPublished }
92 }) 92 })
93 93
94 if (isNewVideo) Notifier.Instance.notifyOnNewVideo(videoDatabase) 94 if (videoPublished) {
95 Notifier.Instance.notifyOnNewVideo(videoDatabase)
96 Notifier.Instance.notifyOnPendingVideoPublished(videoDatabase)
97 }
95} 98}
96 99
97async function onVideoFileOptimizerSuccess (videoArg: VideoModel, isNewVideo: boolean) { 100async function onVideoFileOptimizerSuccess (videoArg: VideoModel, isNewVideo: boolean) {
@@ -100,7 +103,7 @@ async function onVideoFileOptimizerSuccess (videoArg: VideoModel, isNewVideo: bo
100 // Outside the transaction (IO on disk) 103 // Outside the transaction (IO on disk)
101 const { videoFileResolution } = await videoArg.getOriginalFileResolution() 104 const { videoFileResolution } = await videoArg.getOriginalFileResolution()
102 105
103 const videoDatabase = await sequelizeTypescript.transaction(async t => { 106 const { videoDatabase, videoPublished } = await sequelizeTypescript.transaction(async t => {
104 // Maybe the video changed in database, refresh it 107 // Maybe the video changed in database, refresh it
105 let videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoArg.uuid, t) 108 let videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoArg.uuid, t)
106 // Video does not exist anymore 109 // Video does not exist anymore
@@ -113,6 +116,8 @@ async function onVideoFileOptimizerSuccess (videoArg: VideoModel, isNewVideo: bo
113 { resolutions: resolutionsEnabled } 116 { resolutions: resolutionsEnabled }
114 ) 117 )
115 118
119 let videoPublished = false
120
116 if (resolutionsEnabled.length !== 0) { 121 if (resolutionsEnabled.length !== 0) {
117 const tasks: Bluebird<Bull.Job<any>>[] = [] 122 const tasks: Bluebird<Bull.Job<any>>[] = []
118 123
@@ -130,6 +135,8 @@ async function onVideoFileOptimizerSuccess (videoArg: VideoModel, isNewVideo: bo
130 135
131 logger.info('Transcoding jobs created for uuid %s.', videoDatabase.uuid, { resolutionsEnabled }) 136 logger.info('Transcoding jobs created for uuid %s.', videoDatabase.uuid, { resolutionsEnabled })
132 } else { 137 } else {
138 videoPublished = true
139
133 // No transcoding to do, it's now published 140 // No transcoding to do, it's now published
134 videoDatabase.state = VideoState.PUBLISHED 141 videoDatabase.state = VideoState.PUBLISHED
135 videoDatabase = await videoDatabase.save({ transaction: t }) 142 videoDatabase = await videoDatabase.save({ transaction: t })
@@ -139,10 +146,11 @@ async function onVideoFileOptimizerSuccess (videoArg: VideoModel, isNewVideo: bo
139 146
140 await federateVideoIfNeeded(videoDatabase, isNewVideo, t) 147 await federateVideoIfNeeded(videoDatabase, isNewVideo, t)
141 148
142 return videoDatabase 149 return { videoDatabase, videoPublished }
143 }) 150 })
144 151
145 if (isNewVideo) Notifier.Instance.notifyOnNewVideo(videoDatabase) 152 if (isNewVideo) Notifier.Instance.notifyOnNewVideo(videoDatabase)
153 if (videoPublished) Notifier.Instance.notifyOnPendingVideoPublished(videoDatabase)
146} 154}
147 155
148// --------------------------------------------------------------------------- 156// ---------------------------------------------------------------------------
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts
index 29cd1198c..12004dcd7 100644
--- a/server/lib/job-queue/handlers/video-import.ts
+++ b/server/lib/job-queue/handlers/video-import.ts
@@ -197,6 +197,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
197 }) 197 })
198 198
199 Notifier.Instance.notifyOnNewVideo(videoImportUpdated.Video) 199 Notifier.Instance.notifyOnNewVideo(videoImportUpdated.Video)
200 Notifier.Instance.notifyOnFinishedVideoImport(videoImportUpdated, true)
200 201
201 // Create transcoding jobs? 202 // Create transcoding jobs?
202 if (videoImportUpdated.Video.state === VideoState.TO_TRANSCODE) { 203 if (videoImportUpdated.Video.state === VideoState.TO_TRANSCODE) {
@@ -220,6 +221,8 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
220 videoImport.state = VideoImportState.FAILED 221 videoImport.state = VideoImportState.FAILED
221 await videoImport.save() 222 await videoImport.save()
222 223
224 Notifier.Instance.notifyOnFinishedVideoImport(videoImport, false)
225
223 throw err 226 throw err
224 } 227 }
225} 228}
diff --git a/server/lib/notifier.ts b/server/lib/notifier.ts
index a21b50b2d..11b0937e9 100644
--- a/server/lib/notifier.ts
+++ b/server/lib/notifier.ts
@@ -11,6 +11,8 @@ import { VideoPrivacy, VideoState } from '../../shared/models/videos'
11import { VideoAbuseModel } from '../models/video/video-abuse' 11import { VideoAbuseModel } from '../models/video/video-abuse'
12import { VideoBlacklistModel } from '../models/video/video-blacklist' 12import { VideoBlacklistModel } from '../models/video/video-blacklist'
13import * as Bluebird from 'bluebird' 13import * as Bluebird from 'bluebird'
14import { VideoImportModel } from '../models/video/video-import'
15import { AccountBlocklistModel } from '../models/account/account-blocklist'
14 16
15class Notifier { 17class Notifier {
16 18
@@ -26,6 +28,14 @@ class Notifier {
26 .catch(err => logger.error('Cannot notify subscribers of new video %s.', video.url, { err })) 28 .catch(err => logger.error('Cannot notify subscribers of new video %s.', video.url, { err }))
27 } 29 }
28 30
31 notifyOnPendingVideoPublished (video: VideoModel): void {
32 // Only notify on public videos that has been published while the user waited transcoding/scheduled update
33 if (video.waitTranscoding === false && !video.ScheduleVideoUpdate) return
34
35 this.notifyOwnedVideoHasBeenPublished(video)
36 .catch(err => logger.error('Cannot notify owner that its video %s has been published.', video.url, { err }))
37 }
38
29 notifyOnNewComment (comment: VideoCommentModel): void { 39 notifyOnNewComment (comment: VideoCommentModel): void {
30 this.notifyVideoOwnerOfNewComment(comment) 40 this.notifyVideoOwnerOfNewComment(comment)
31 .catch(err => logger.error('Cannot notify of new comment %s.', comment.url, { err })) 41 .catch(err => logger.error('Cannot notify of new comment %s.', comment.url, { err }))
@@ -46,6 +56,11 @@ class Notifier {
46 .catch(err => logger.error('Cannot notify video owner of new video blacklist of %s.', video.url, { err })) 56 .catch(err => logger.error('Cannot notify video owner of new video blacklist of %s.', video.url, { err }))
47 } 57 }
48 58
59 notifyOnFinishedVideoImport (videoImport: VideoImportModel, success: boolean): void {
60 this.notifyOwnerVideoImportIsFinished(videoImport, success)
61 .catch(err => logger.error('Cannot notify owner that its video import %s is finished.', videoImport.getTargetIdentifier(), { err }))
62 }
63
49 private async notifySubscribersOfNewVideo (video: VideoModel) { 64 private async notifySubscribersOfNewVideo (video: VideoModel) {
50 // List all followers that are users 65 // List all followers that are users
51 const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId) 66 const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId)
@@ -80,6 +95,9 @@ class Notifier {
80 // Not our user or user comments its own video 95 // Not our user or user comments its own video
81 if (!user || comment.Account.userId === user.id) return 96 if (!user || comment.Account.userId === user.id) return
82 97
98 const accountMuted = await AccountBlocklistModel.isAccountMutedBy(user.Account.id, comment.accountId)
99 if (accountMuted) return
100
83 logger.info('Notifying user %s of new comment %s.', user.username, comment.url) 101 logger.info('Notifying user %s of new comment %s.', user.username, comment.url)
84 102
85 function settingGetter (user: UserModel) { 103 function settingGetter (user: UserModel) {
@@ -188,6 +206,64 @@ class Notifier {
188 return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender }) 206 return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender })
189 } 207 }
190 208
209 private async notifyOwnedVideoHasBeenPublished (video: VideoModel) {
210 const user = await UserModel.loadByVideoId(video.id)
211 if (!user) return
212
213 logger.info('Notifying user %s of the publication of its video %s.', user.username, video.url)
214
215 function settingGetter (user: UserModel) {
216 return user.NotificationSetting.myVideoPublished
217 }
218
219 async function notificationCreator (user: UserModel) {
220 const notification = await UserNotificationModel.create({
221 type: UserNotificationType.MY_VIDEO_PUBLISHED,
222 userId: user.id,
223 videoId: video.id
224 })
225 notification.Video = video
226
227 return notification
228 }
229
230 function emailSender (emails: string[]) {
231 return Emailer.Instance.myVideoPublishedNotification(emails, video)
232 }
233
234 return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender })
235 }
236
237 private async notifyOwnerVideoImportIsFinished (videoImport: VideoImportModel, success: boolean) {
238 const user = await UserModel.loadByVideoImportId(videoImport.id)
239 if (!user) return
240
241 logger.info('Notifying user %s its video import %s is finished.', user.username, videoImport.getTargetIdentifier())
242
243 function settingGetter (user: UserModel) {
244 return user.NotificationSetting.myVideoImportFinished
245 }
246
247 async function notificationCreator (user: UserModel) {
248 const notification = await UserNotificationModel.create({
249 type: success ? UserNotificationType.MY_VIDEO_IMPORT_SUCCESS : UserNotificationType.MY_VIDEO_IMPORT_ERROR,
250 userId: user.id,
251 videoImportId: videoImport.id
252 })
253 notification.VideoImport = videoImport
254
255 return notification
256 }
257
258 function emailSender (emails: string[]) {
259 return success
260 ? Emailer.Instance.myVideoImportSuccessNotification(emails, videoImport)
261 : Emailer.Instance.myVideoImportErrorNotification(emails, videoImport)
262 }
263
264 return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender })
265 }
266
191 private async notify (options: { 267 private async notify (options: {
192 users: UserModel[], 268 users: UserModel[],
193 notificationCreator: (user: UserModel) => Promise<UserNotificationModel>, 269 notificationCreator: (user: UserModel) => Promise<UserNotificationModel>,
diff --git a/server/lib/schedulers/update-videos-scheduler.ts b/server/lib/schedulers/update-videos-scheduler.ts
index b7fb029f1..2618a5857 100644
--- a/server/lib/schedulers/update-videos-scheduler.ts
+++ b/server/lib/schedulers/update-videos-scheduler.ts
@@ -6,6 +6,7 @@ import { federateVideoIfNeeded } from '../activitypub'
6import { SCHEDULER_INTERVALS_MS, sequelizeTypescript } from '../../initializers' 6import { SCHEDULER_INTERVALS_MS, sequelizeTypescript } from '../../initializers'
7import { VideoPrivacy } from '../../../shared/models/videos' 7import { VideoPrivacy } from '../../../shared/models/videos'
8import { Notifier } from '../notifier' 8import { Notifier } from '../notifier'
9import { VideoModel } from '../../models/video/video'
9 10
10export class UpdateVideosScheduler extends AbstractScheduler { 11export class UpdateVideosScheduler extends AbstractScheduler {
11 12
@@ -24,8 +25,9 @@ export class UpdateVideosScheduler extends AbstractScheduler {
24 private async updateVideos () { 25 private async updateVideos () {
25 if (!await ScheduleVideoUpdateModel.areVideosToUpdate()) return undefined 26 if (!await ScheduleVideoUpdateModel.areVideosToUpdate()) return undefined
26 27
27 return sequelizeTypescript.transaction(async t => { 28 const publishedVideos = await sequelizeTypescript.transaction(async t => {
28 const schedules = await ScheduleVideoUpdateModel.listVideosToUpdate(t) 29 const schedules = await ScheduleVideoUpdateModel.listVideosToUpdate(t)
30 const publishedVideos: VideoModel[] = []
29 31
30 for (const schedule of schedules) { 32 for (const schedule of schedules) {
31 const video = schedule.Video 33 const video = schedule.Video
@@ -42,13 +44,21 @@ export class UpdateVideosScheduler extends AbstractScheduler {
42 await federateVideoIfNeeded(video, isNewVideo, t) 44 await federateVideoIfNeeded(video, isNewVideo, t)
43 45
44 if (oldPrivacy === VideoPrivacy.UNLISTED || oldPrivacy === VideoPrivacy.PRIVATE) { 46 if (oldPrivacy === VideoPrivacy.UNLISTED || oldPrivacy === VideoPrivacy.PRIVATE) {
45 Notifier.Instance.notifyOnNewVideo(video) 47 video.ScheduleVideoUpdate = schedule
48 publishedVideos.push(video)
46 } 49 }
47 } 50 }
48 51
49 await schedule.destroy({ transaction: t }) 52 await schedule.destroy({ transaction: t })
50 } 53 }
54
55 return publishedVideos
51 }) 56 })
57
58 for (const v of publishedVideos) {
59 Notifier.Instance.notifyOnNewVideo(v)
60 Notifier.Instance.notifyOnPendingVideoPublished(v)
61 }
52 } 62 }
53 63
54 static get Instance () { 64 static get Instance () {
diff --git a/server/lib/user.ts b/server/lib/user.ts
index 72127819c..481571828 100644
--- a/server/lib/user.ts
+++ b/server/lib/user.ts
@@ -100,6 +100,8 @@ function createDefaultUserNotificationSettings (user: UserModel, t: Sequelize.Tr
100 userId: user.id, 100 userId: user.id,
101 newVideoFromSubscription: UserNotificationSettingValue.WEB_NOTIFICATION, 101 newVideoFromSubscription: UserNotificationSettingValue.WEB_NOTIFICATION,
102 newCommentOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION, 102 newCommentOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION,
103 myVideoImportFinished: UserNotificationSettingValue.WEB_NOTIFICATION,
104 myVideoPublished: UserNotificationSettingValue.WEB_NOTIFICATION,
103 videoAbuseAsModerator: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL, 105 videoAbuseAsModerator: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL,
104 blacklistOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL 106 blacklistOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL
105 }, { transaction: t }) 107 }, { transaction: t })