aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2018-12-26 10:36:24 +0100
committerChocobozzz <chocobozzz@cpy.re>2019-01-09 11:15:15 +0100
commitcef534ed53e4518fe0acf581bfe880788d42fc36 (patch)
tree115b51ea5136849a2336d44915c7780649f25dc2 /server/lib
parent1de1d05f4c61fe059fa5e24e79c92582f0e7e4b3 (diff)
downloadPeerTube-cef534ed53e4518fe0acf581bfe880788d42fc36.tar.gz
PeerTube-cef534ed53e4518fe0acf581bfe880788d42fc36.tar.zst
PeerTube-cef534ed53e4518fe0acf581bfe880788d42fc36.zip
Add user notification base code
Diffstat (limited to 'server/lib')
-rw-r--r--server/lib/activitypub/process/process-announce.ts8
-rw-r--r--server/lib/activitypub/process/process-create.ts14
-rw-r--r--server/lib/activitypub/video-comments.ts4
-rw-r--r--server/lib/activitypub/videos.ts15
-rw-r--r--server/lib/client-html.ts4
-rw-r--r--server/lib/emailer.ts116
-rw-r--r--server/lib/job-queue/handlers/video-file.ts5
-rw-r--r--server/lib/job-queue/handlers/video-import.ts2
-rw-r--r--server/lib/notifier.ts235
-rw-r--r--server/lib/oauth-model.ts3
-rw-r--r--server/lib/peertube-socket.ts52
-rw-r--r--server/lib/schedulers/update-videos-scheduler.ts5
-rw-r--r--server/lib/user.ts16
13 files changed, 425 insertions, 54 deletions
diff --git a/server/lib/activitypub/process/process-announce.ts b/server/lib/activitypub/process/process-announce.ts
index cc88b5423..23310b41e 100644
--- a/server/lib/activitypub/process/process-announce.ts
+++ b/server/lib/activitypub/process/process-announce.ts
@@ -5,6 +5,8 @@ import { ActorModel } from '../../../models/activitypub/actor'
5import { VideoShareModel } from '../../../models/video/video-share' 5import { VideoShareModel } from '../../../models/video/video-share'
6import { forwardVideoRelatedActivity } from '../send/utils' 6import { forwardVideoRelatedActivity } from '../send/utils'
7import { getOrCreateVideoAndAccountAndChannel } from '../videos' 7import { getOrCreateVideoAndAccountAndChannel } from '../videos'
8import { VideoPrivacy } from '../../../../shared/models/videos'
9import { Notifier } from '../../notifier'
8 10
9async function processAnnounceActivity (activity: ActivityAnnounce, actorAnnouncer: ActorModel) { 11async function processAnnounceActivity (activity: ActivityAnnounce, actorAnnouncer: ActorModel) {
10 return retryTransactionWrapper(processVideoShare, actorAnnouncer, activity) 12 return retryTransactionWrapper(processVideoShare, actorAnnouncer, activity)
@@ -21,9 +23,9 @@ export {
21async function processVideoShare (actorAnnouncer: ActorModel, activity: ActivityAnnounce) { 23async function processVideoShare (actorAnnouncer: ActorModel, activity: ActivityAnnounce) {
22 const objectUri = typeof activity.object === 'string' ? activity.object : activity.object.id 24 const objectUri = typeof activity.object === 'string' ? activity.object : activity.object.id
23 25
24 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: objectUri }) 26 const { video, created: videoCreated } = await getOrCreateVideoAndAccountAndChannel({ videoObject: objectUri })
25 27
26 return sequelizeTypescript.transaction(async t => { 28 await sequelizeTypescript.transaction(async t => {
27 // Add share entry 29 // Add share entry
28 30
29 const share = { 31 const share = {
@@ -49,4 +51,6 @@ async function processVideoShare (actorAnnouncer: ActorModel, activity: Activity
49 51
50 return undefined 52 return undefined
51 }) 53 })
54
55 if (videoCreated) Notifier.Instance.notifyOnNewVideo(video)
52} 56}
diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts
index df05ee452..2e04ee843 100644
--- a/server/lib/activitypub/process/process-create.ts
+++ b/server/lib/activitypub/process/process-create.ts
@@ -13,6 +13,7 @@ import { forwardVideoRelatedActivity } from '../send/utils'
13import { Redis } from '../../redis' 13import { Redis } from '../../redis'
14import { createOrUpdateCacheFile } from '../cache-file' 14import { createOrUpdateCacheFile } from '../cache-file'
15import { getVideoDislikeActivityPubUrl } from '../url' 15import { getVideoDislikeActivityPubUrl } from '../url'
16import { Notifier } from '../../notifier'
16 17
17async function processCreateActivity (activity: ActivityCreate, byActor: ActorModel) { 18async function processCreateActivity (activity: ActivityCreate, byActor: ActorModel) {
18 const activityObject = activity.object 19 const activityObject = activity.object
@@ -47,7 +48,9 @@ export {
47async function processCreateVideo (activity: ActivityCreate) { 48async function processCreateVideo (activity: ActivityCreate) {
48 const videoToCreateData = activity.object as VideoTorrentObject 49 const videoToCreateData = activity.object as VideoTorrentObject
49 50
50 const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: videoToCreateData }) 51 const { video, created } = await getOrCreateVideoAndAccountAndChannel({ videoObject: videoToCreateData })
52
53 if (created) Notifier.Instance.notifyOnNewVideo(video)
51 54
52 return video 55 return video
53} 56}
@@ -133,7 +136,10 @@ async function processCreateVideoAbuse (byActor: ActorModel, videoAbuseToCreateD
133 state: VideoAbuseState.PENDING 136 state: VideoAbuseState.PENDING
134 } 137 }
135 138
136 await VideoAbuseModel.create(videoAbuseData, { transaction: t }) 139 const videoAbuseInstance = await VideoAbuseModel.create(videoAbuseData, { transaction: t })
140 videoAbuseInstance.Video = video
141
142 Notifier.Instance.notifyOnNewVideoAbuse(videoAbuseInstance)
137 143
138 logger.info('Remote abuse for video uuid %s created', videoAbuseToCreateData.object) 144 logger.info('Remote abuse for video uuid %s created', videoAbuseToCreateData.object)
139 }) 145 })
@@ -147,7 +153,7 @@ async function processCreateVideoComment (byActor: ActorModel, activity: Activit
147 153
148 const { video } = await resolveThread(commentObject.inReplyTo) 154 const { video } = await resolveThread(commentObject.inReplyTo)
149 155
150 const { created } = await addVideoComment(video, commentObject.id) 156 const { comment, created } = await addVideoComment(video, commentObject.id)
151 157
152 if (video.isOwned() && created === true) { 158 if (video.isOwned() && created === true) {
153 // Don't resend the activity to the sender 159 // Don't resend the activity to the sender
@@ -155,4 +161,6 @@ async function processCreateVideoComment (byActor: ActorModel, activity: Activit
155 161
156 await forwardVideoRelatedActivity(activity, undefined, exceptions, video) 162 await forwardVideoRelatedActivity(activity, undefined, exceptions, video)
157 } 163 }
164
165 if (created === true) Notifier.Instance.notifyOnNewComment(comment)
158} 166}
diff --git a/server/lib/activitypub/video-comments.ts b/server/lib/activitypub/video-comments.ts
index 5868e7297..e87301fe7 100644
--- a/server/lib/activitypub/video-comments.ts
+++ b/server/lib/activitypub/video-comments.ts
@@ -70,7 +70,7 @@ async function addVideoComment (videoInstance: VideoModel, commentUrl: string) {
70 throw new Error(`Comment url ${commentUrl} host is different from the AP object id ${body.id}`) 70 throw new Error(`Comment url ${commentUrl} host is different from the AP object id ${body.id}`)
71 } 71 }
72 72
73 const actor = await getOrCreateActorAndServerAndModel(actorUrl) 73 const actor = await getOrCreateActorAndServerAndModel(actorUrl, 'all')
74 const entry = await videoCommentActivityObjectToDBAttributes(videoInstance, actor, body) 74 const entry = await videoCommentActivityObjectToDBAttributes(videoInstance, actor, body)
75 if (!entry) return { created: false } 75 if (!entry) return { created: false }
76 76
@@ -80,6 +80,8 @@ async function addVideoComment (videoInstance: VideoModel, commentUrl: string) {
80 }, 80 },
81 defaults: entry 81 defaults: entry
82 }) 82 })
83 comment.Account = actor.Account
84 comment.Video = videoInstance
83 85
84 return { comment, created } 86 return { comment, created }
85} 87}
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index 379c2a0d7..5794988a5 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -29,6 +29,7 @@ import { addVideoShares, shareVideoByServerAndChannel } from './share'
29import { AccountModel } from '../../models/account/account' 29import { AccountModel } from '../../models/account/account'
30import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video' 30import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video'
31import { checkUrlsSameHost, getAPUrl } from '../../helpers/activitypub' 31import { checkUrlsSameHost, getAPUrl } from '../../helpers/activitypub'
32import { Notifier } from '../notifier'
32 33
33async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) { 34async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) {
34 // If the video is not private and published, we federate it 35 // If the video is not private and published, we federate it
@@ -181,7 +182,7 @@ async function getOrCreateVideoAndAccountAndChannel (options: {
181 else await JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', videoUrl: videoFromDatabase.url } }) 182 else await JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', videoUrl: videoFromDatabase.url } })
182 } 183 }
183 184
184 return { video: videoFromDatabase } 185 return { video: videoFromDatabase, created: false }
185 } 186 }
186 187
187 const { videoObject: fetchedVideo } = await fetchRemoteVideo(videoUrl) 188 const { videoObject: fetchedVideo } = await fetchRemoteVideo(videoUrl)
@@ -192,7 +193,7 @@ async function getOrCreateVideoAndAccountAndChannel (options: {
192 193
193 await syncVideoExternalAttributes(video, fetchedVideo, syncParam) 194 await syncVideoExternalAttributes(video, fetchedVideo, syncParam)
194 195
195 return { video } 196 return { video, created: true }
196} 197}
197 198
198async function updateVideoFromAP (options: { 199async function updateVideoFromAP (options: {
@@ -213,6 +214,9 @@ async function updateVideoFromAP (options: {
213 214
214 videoFieldsSave = options.video.toJSON() 215 videoFieldsSave = options.video.toJSON()
215 216
217 const wasPrivateVideo = options.video.privacy === VideoPrivacy.PRIVATE
218 const wasUnlistedVideo = options.video.privacy === VideoPrivacy.UNLISTED
219
216 // Check actor has the right to update the video 220 // Check actor has the right to update the video
217 const videoChannel = options.video.VideoChannel 221 const videoChannel = options.video.VideoChannel
218 if (videoChannel.Account.id !== options.account.id) { 222 if (videoChannel.Account.id !== options.account.id) {
@@ -277,6 +281,13 @@ async function updateVideoFromAP (options: {
277 }) 281 })
278 options.video.VideoCaptions = await Promise.all(videoCaptionsPromises) 282 options.video.VideoCaptions = await Promise.all(videoCaptionsPromises)
279 } 283 }
284
285 {
286 // Notify our users?
287 if (wasPrivateVideo || wasUnlistedVideo) {
288 Notifier.Instance.notifyOnNewVideo(options.video)
289 }
290 }
280 }) 291 })
281 292
282 logger.info('Remote video with uuid %s updated', options.videoObject.uuid) 293 logger.info('Remote video with uuid %s updated', options.videoObject.uuid)
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts
index 2db3f8a34..1875ec1fc 100644
--- a/server/lib/client-html.ts
+++ b/server/lib/client-html.ts
@@ -115,8 +115,8 @@ export class ClientHtml {
115 } 115 }
116 116
117 private static addOpenGraphAndOEmbedTags (htmlStringPage: string, video: VideoModel) { 117 private static addOpenGraphAndOEmbedTags (htmlStringPage: string, video: VideoModel) {
118 const previewUrl = CONFIG.WEBSERVER.URL + STATIC_PATHS.PREVIEWS + video.getPreviewName() 118 const previewUrl = CONFIG.WEBSERVER.URL + video.getPreviewStaticPath()
119 const videoUrl = CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid 119 const videoUrl = CONFIG.WEBSERVER.URL + video.getWatchStaticPath()
120 120
121 const videoNameEscaped = escapeHTML(video.name) 121 const videoNameEscaped = escapeHTML(video.name)
122 const videoDescriptionEscaped = escapeHTML(video.description) 122 const videoDescriptionEscaped = escapeHTML(video.description)
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts
index 074d4ad44..d766e655b 100644
--- a/server/lib/emailer.ts
+++ b/server/lib/emailer.ts
@@ -1,5 +1,4 @@
1import { createTransport, Transporter } from 'nodemailer' 1import { createTransport, Transporter } from 'nodemailer'
2import { UserRight } from '../../shared/models/users'
3import { isTestInstance } from '../helpers/core-utils' 2import { isTestInstance } from '../helpers/core-utils'
4import { bunyanLogger, logger } from '../helpers/logger' 3import { bunyanLogger, logger } from '../helpers/logger'
5import { CONFIG } from '../initializers' 4import { CONFIG } from '../initializers'
@@ -8,6 +7,9 @@ import { VideoModel } from '../models/video/video'
8import { JobQueue } from './job-queue' 7import { JobQueue } from './job-queue'
9import { EmailPayload } from './job-queue/handlers/email' 8import { EmailPayload } from './job-queue/handlers/email'
10import { readFileSync } from 'fs-extra' 9import { readFileSync } from 'fs-extra'
10import { VideoCommentModel } from '../models/video/video-comment'
11import { VideoAbuseModel } from '../models/video/video-abuse'
12import { VideoBlacklistModel } from '../models/video/video-blacklist'
11 13
12class Emailer { 14class Emailer {
13 15
@@ -79,50 +81,57 @@ class Emailer {
79 } 81 }
80 } 82 }
81 83
82 addForgetPasswordEmailJob (to: string, resetPasswordUrl: string) { 84 addNewVideoFromSubscriberNotification (to: string[], video: VideoModel) {
85 const channelName = video.VideoChannel.getDisplayName()
86 const videoUrl = CONFIG.WEBSERVER.URL + video.getWatchStaticPath()
87
83 const text = `Hi dear user,\n\n` + 88 const text = `Hi dear user,\n\n` +
84 `It seems you forgot your password on ${CONFIG.WEBSERVER.HOST}! ` + 89 `Your subscription ${channelName} just published a new video: ${video.name}` +
85 `Please follow this link to reset it: ${resetPasswordUrl}\n\n` + 90 `\n\n` +
86 `If you are not the person who initiated this request, please ignore this email.\n\n` + 91 `You can view it on ${videoUrl} ` +
92 `\n\n` +
87 `Cheers,\n` + 93 `Cheers,\n` +
88 `PeerTube.` 94 `PeerTube.`
89 95
90 const emailPayload: EmailPayload = { 96 const emailPayload: EmailPayload = {
91 to: [ to ], 97 to,
92 subject: 'Reset your PeerTube password', 98 subject: channelName + ' just published a new video',
93 text 99 text
94 } 100 }
95 101
96 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 102 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
97 } 103 }
98 104
99 addVerifyEmailJob (to: string, verifyEmailUrl: string) { 105 addNewCommentOnMyVideoNotification (to: string[], comment: VideoCommentModel) {
100 const text = `Welcome to PeerTube,\n\n` + 106 const accountName = comment.Account.getDisplayName()
101 `To start using PeerTube on ${CONFIG.WEBSERVER.HOST} you must verify your email! ` + 107 const video = comment.Video
102 `Please follow this link to verify this email belongs to you: ${verifyEmailUrl}\n\n` + 108 const commentUrl = CONFIG.WEBSERVER.URL + comment.getCommentStaticPath()
103 `If you are not the person who initiated this request, please ignore this email.\n\n` + 109
110 const text = `Hi dear user,\n\n` +
111 `A new comment has been posted by ${accountName} on your video ${video.name}` +
112 `\n\n` +
113 `You can view it on ${commentUrl} ` +
114 `\n\n` +
104 `Cheers,\n` + 115 `Cheers,\n` +
105 `PeerTube.` 116 `PeerTube.`
106 117
107 const emailPayload: EmailPayload = { 118 const emailPayload: EmailPayload = {
108 to: [ to ], 119 to,
109 subject: 'Verify your PeerTube email', 120 subject: 'New comment on your video ' + video.name,
110 text 121 text
111 } 122 }
112 123
113 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 124 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
114 } 125 }
115 126
116 async addVideoAbuseReportJob (videoId: number) { 127 async addVideoAbuseModeratorsNotification (to: string[], videoAbuse: VideoAbuseModel) {
117 const video = await VideoModel.load(videoId) 128 const videoUrl = CONFIG.WEBSERVER.URL + videoAbuse.Video.getWatchStaticPath()
118 if (!video) throw new Error('Unknown Video id during Abuse report.')
119 129
120 const text = `Hi,\n\n` + 130 const text = `Hi,\n\n` +
121 `Your instance received an abuse for the following video ${video.url}\n\n` + 131 `${CONFIG.WEBSERVER.HOST} received an abuse for the following video ${videoUrl}\n\n` +
122 `Cheers,\n` + 132 `Cheers,\n` +
123 `PeerTube.` 133 `PeerTube.`
124 134
125 const to = await UserModel.listEmailsWithRight(UserRight.MANAGE_VIDEO_ABUSES)
126 const emailPayload: EmailPayload = { 135 const emailPayload: EmailPayload = {
127 to, 136 to,
128 subject: '[PeerTube] Received a video abuse', 137 subject: '[PeerTube] Received a video abuse',
@@ -132,16 +141,12 @@ class Emailer {
132 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 141 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
133 } 142 }
134 143
135 async addVideoBlacklistReportJob (videoId: number, reason?: string) { 144 async addVideoBlacklistNotification (to: string[], videoBlacklist: VideoBlacklistModel) {
136 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId) 145 const videoName = videoBlacklist.Video.name
137 if (!video) throw new Error('Unknown Video id during Blacklist report.') 146 const videoUrl = CONFIG.WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath()
138 // It's not our user
139 if (video.remote === true) return
140 147
141 const user = await UserModel.loadById(video.VideoChannel.Account.userId) 148 const reasonString = videoBlacklist.reason ? ` for the following reason: ${videoBlacklist.reason}` : ''
142 149 const blockedString = `Your video ${videoName} (${videoUrl} on ${CONFIG.WEBSERVER.HOST} has been blacklisted${reasonString}.`
143 const reasonString = reason ? ` for the following reason: ${reason}` : ''
144 const blockedString = `Your video ${video.name} on ${CONFIG.WEBSERVER.HOST} has been blacklisted${reasonString}.`
145 150
146 const text = 'Hi,\n\n' + 151 const text = 'Hi,\n\n' +
147 blockedString + 152 blockedString +
@@ -149,33 +154,26 @@ class Emailer {
149 'Cheers,\n' + 154 'Cheers,\n' +
150 `PeerTube.` 155 `PeerTube.`
151 156
152 const to = user.email
153 const emailPayload: EmailPayload = { 157 const emailPayload: EmailPayload = {
154 to: [ to ], 158 to,
155 subject: `[PeerTube] Video ${video.name} blacklisted`, 159 subject: `[PeerTube] Video ${videoName} blacklisted`,
156 text 160 text
157 } 161 }
158 162
159 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 163 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
160 } 164 }
161 165
162 async addVideoUnblacklistReportJob (videoId: number) { 166 async addVideoUnblacklistNotification (to: string[], video: VideoModel) {
163 const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId) 167 const videoUrl = CONFIG.WEBSERVER.URL + video.getWatchStaticPath()
164 if (!video) throw new Error('Unknown Video id during Blacklist report.')
165 // It's not our user
166 if (video.remote === true) return
167
168 const user = await UserModel.loadById(video.VideoChannel.Account.userId)
169 168
170 const text = 'Hi,\n\n' + 169 const text = 'Hi,\n\n' +
171 `Your video ${video.name} on ${CONFIG.WEBSERVER.HOST} has been unblacklisted.` + 170 `Your video ${video.name} (${videoUrl}) on ${CONFIG.WEBSERVER.HOST} has been unblacklisted.` +
172 '\n\n' + 171 '\n\n' +
173 'Cheers,\n' + 172 'Cheers,\n' +
174 `PeerTube.` 173 `PeerTube.`
175 174
176 const to = user.email
177 const emailPayload: EmailPayload = { 175 const emailPayload: EmailPayload = {
178 to: [ to ], 176 to,
179 subject: `[PeerTube] Video ${video.name} unblacklisted`, 177 subject: `[PeerTube] Video ${video.name} unblacklisted`,
180 text 178 text
181 } 179 }
@@ -183,6 +181,40 @@ class Emailer {
183 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 181 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
184 } 182 }
185 183
184 addForgetPasswordEmailJob (to: string, resetPasswordUrl: string) {
185 const text = `Hi dear user,\n\n` +
186 `It seems you forgot your password on ${CONFIG.WEBSERVER.HOST}! ` +
187 `Please follow this link to reset it: ${resetPasswordUrl}\n\n` +
188 `If you are not the person who initiated this request, please ignore this email.\n\n` +
189 `Cheers,\n` +
190 `PeerTube.`
191
192 const emailPayload: EmailPayload = {
193 to: [ to ],
194 subject: 'Reset your PeerTube password',
195 text
196 }
197
198 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
199 }
200
201 addVerifyEmailJob (to: string, verifyEmailUrl: string) {
202 const text = `Welcome to PeerTube,\n\n` +
203 `To start using PeerTube on ${CONFIG.WEBSERVER.HOST} you must verify your email! ` +
204 `Please follow this link to verify this email belongs to you: ${verifyEmailUrl}\n\n` +
205 `If you are not the person who initiated this request, please ignore this email.\n\n` +
206 `Cheers,\n` +
207 `PeerTube.`
208
209 const emailPayload: EmailPayload = {
210 to: [ to ],
211 subject: 'Verify your PeerTube email',
212 text
213 }
214
215 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
216 }
217
186 addUserBlockJob (user: UserModel, blocked: boolean, reason?: string) { 218 addUserBlockJob (user: UserModel, blocked: boolean, reason?: string) {
187 const reasonString = reason ? ` for the following reason: ${reason}` : '' 219 const reasonString = reason ? ` for the following reason: ${reason}` : ''
188 const blockedWord = blocked ? 'blocked' : 'unblocked' 220 const blockedWord = blocked ? 'blocked' : 'unblocked'
@@ -205,7 +237,7 @@ class Emailer {
205 } 237 }
206 238
207 sendMail (to: string[], subject: string, text: string) { 239 sendMail (to: string[], subject: string, text: string) {
208 if (!this.transporter) { 240 if (!this.enabled) {
209 throw new Error('Cannot send mail because SMTP is not configured.') 241 throw new Error('Cannot send mail because SMTP is not configured.')
210 } 242 }
211 243
diff --git a/server/lib/job-queue/handlers/video-file.ts b/server/lib/job-queue/handlers/video-file.ts
index 3dca2937f..959cc04fa 100644
--- a/server/lib/job-queue/handlers/video-file.ts
+++ b/server/lib/job-queue/handlers/video-file.ts
@@ -9,6 +9,7 @@ import { sequelizeTypescript } from '../../../initializers'
9import * as Bluebird from 'bluebird' 9import * as Bluebird from 'bluebird'
10import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils' 10import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils'
11import { importVideoFile, transcodeOriginalVideofile, optimizeVideofile } from '../../video-transcoding' 11import { importVideoFile, transcodeOriginalVideofile, optimizeVideofile } from '../../video-transcoding'
12import { Notifier } from '../../notifier'
12 13
13export type VideoFilePayload = { 14export type VideoFilePayload = {
14 videoUUID: string 15 videoUUID: string
@@ -86,6 +87,7 @@ async function onVideoFileTranscoderOrImportSuccess (video: VideoModel) {
86 87
87 // 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
88 await federateVideoIfNeeded(videoDatabase, isNewVideo, t) 89 await federateVideoIfNeeded(videoDatabase, isNewVideo, t)
90 if (isNewVideo) Notifier.Instance.notifyOnNewVideo(video)
89 91
90 return undefined 92 return undefined
91 }) 93 })
@@ -134,7 +136,8 @@ async function onVideoFileOptimizerSuccess (videoArg: VideoModel, isNewVideo: bo
134 logger.info('No transcoding jobs created for video %s (no resolutions).', videoDatabase.uuid, { privacy: videoDatabase.privacy }) 136 logger.info('No transcoding jobs created for video %s (no resolutions).', videoDatabase.uuid, { privacy: videoDatabase.privacy })
135 } 137 }
136 138
137 return federateVideoIfNeeded(videoDatabase, isNewVideo, t) 139 await federateVideoIfNeeded(videoDatabase, isNewVideo, t)
140 if (isNewVideo) Notifier.Instance.notifyOnNewVideo(videoDatabase)
138 }) 141 })
139} 142}
140 143
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts
index 63aacff98..82edb8d5c 100644
--- a/server/lib/job-queue/handlers/video-import.ts
+++ b/server/lib/job-queue/handlers/video-import.ts
@@ -15,6 +15,7 @@ import { VideoModel } from '../../../models/video/video'
15import { downloadWebTorrentVideo } from '../../../helpers/webtorrent' 15import { downloadWebTorrentVideo } from '../../../helpers/webtorrent'
16import { getSecureTorrentName } from '../../../helpers/utils' 16import { getSecureTorrentName } from '../../../helpers/utils'
17import { remove, move, stat } from 'fs-extra' 17import { remove, move, stat } from 'fs-extra'
18import { Notifier } from '../../notifier'
18 19
19type VideoImportYoutubeDLPayload = { 20type VideoImportYoutubeDLPayload = {
20 type: 'youtube-dl' 21 type: 'youtube-dl'
@@ -184,6 +185,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide
184 // Now we can federate the video (reload from database, we need more attributes) 185 // Now we can federate the video (reload from database, we need more attributes)
185 const videoForFederation = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t) 186 const videoForFederation = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t)
186 await federateVideoIfNeeded(videoForFederation, true, t) 187 await federateVideoIfNeeded(videoForFederation, true, t)
188 Notifier.Instance.notifyOnNewVideo(videoForFederation)
187 189
188 // Update video import object 190 // Update video import object
189 videoImport.state = VideoImportState.SUCCESS 191 videoImport.state = VideoImportState.SUCCESS
diff --git a/server/lib/notifier.ts b/server/lib/notifier.ts
new file mode 100644
index 000000000..a21b50b2d
--- /dev/null
+++ b/server/lib/notifier.ts
@@ -0,0 +1,235 @@
1import { UserNotificationSettingValue, UserNotificationType, UserRight } from '../../shared/models/users'
2import { logger } from '../helpers/logger'
3import { VideoModel } from '../models/video/video'
4import { Emailer } from './emailer'
5import { UserNotificationModel } from '../models/account/user-notification'
6import { VideoCommentModel } from '../models/video/video-comment'
7import { UserModel } from '../models/account/user'
8import { PeerTubeSocket } from './peertube-socket'
9import { CONFIG } from '../initializers/constants'
10import { VideoPrivacy, VideoState } from '../../shared/models/videos'
11import { VideoAbuseModel } from '../models/video/video-abuse'
12import { VideoBlacklistModel } from '../models/video/video-blacklist'
13import * as Bluebird from 'bluebird'
14
15class Notifier {
16
17 private static instance: Notifier
18
19 private constructor () {}
20
21 notifyOnNewVideo (video: VideoModel): void {
22 // Only notify on public and published videos
23 if (video.privacy !== VideoPrivacy.PUBLIC || video.state !== VideoState.PUBLISHED) return
24
25 this.notifySubscribersOfNewVideo(video)
26 .catch(err => logger.error('Cannot notify subscribers of new video %s.', video.url, { err }))
27 }
28
29 notifyOnNewComment (comment: VideoCommentModel): void {
30 this.notifyVideoOwnerOfNewComment(comment)
31 .catch(err => logger.error('Cannot notify of new comment %s.', comment.url, { err }))
32 }
33
34 notifyOnNewVideoAbuse (videoAbuse: VideoAbuseModel): void {
35 this.notifyModeratorsOfNewVideoAbuse(videoAbuse)
36 .catch(err => logger.error('Cannot notify of new video abuse of video %s.', videoAbuse.Video.url, { err }))
37 }
38
39 notifyOnVideoBlacklist (videoBlacklist: VideoBlacklistModel): void {
40 this.notifyVideoOwnerOfBlacklist(videoBlacklist)
41 .catch(err => logger.error('Cannot notify video owner of new video blacklist of %s.', videoBlacklist.Video.url, { err }))
42 }
43
44 notifyOnVideoUnblacklist (video: VideoModel): void {
45 this.notifyVideoOwnerOfUnblacklist(video)
46 .catch(err => logger.error('Cannot notify video owner of new video blacklist of %s.', video.url, { err }))
47 }
48
49 private async notifySubscribersOfNewVideo (video: VideoModel) {
50 // List all followers that are users
51 const users = await UserModel.listUserSubscribersOf(video.VideoChannel.actorId)
52
53 logger.info('Notifying %d users of new video %s.', users.length, video.url)
54
55 function settingGetter (user: UserModel) {
56 return user.NotificationSetting.newVideoFromSubscription
57 }
58
59 async function notificationCreator (user: UserModel) {
60 const notification = await UserNotificationModel.create({
61 type: UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION,
62 userId: user.id,
63 videoId: video.id
64 })
65 notification.Video = video
66
67 return notification
68 }
69
70 function emailSender (emails: string[]) {
71 return Emailer.Instance.addNewVideoFromSubscriberNotification(emails, video)
72 }
73
74 return this.notify({ users, settingGetter, notificationCreator, emailSender })
75 }
76
77 private async notifyVideoOwnerOfNewComment (comment: VideoCommentModel) {
78 const user = await UserModel.loadByVideoId(comment.videoId)
79
80 // Not our user or user comments its own video
81 if (!user || comment.Account.userId === user.id) return
82
83 logger.info('Notifying user %s of new comment %s.', user.username, comment.url)
84
85 function settingGetter (user: UserModel) {
86 return user.NotificationSetting.newCommentOnMyVideo
87 }
88
89 async function notificationCreator (user: UserModel) {
90 const notification = await UserNotificationModel.create({
91 type: UserNotificationType.NEW_COMMENT_ON_MY_VIDEO,
92 userId: user.id,
93 commentId: comment.id
94 })
95 notification.Comment = comment
96
97 return notification
98 }
99
100 function emailSender (emails: string[]) {
101 return Emailer.Instance.addNewCommentOnMyVideoNotification(emails, comment)
102 }
103
104 return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender })
105 }
106
107 private async notifyModeratorsOfNewVideoAbuse (videoAbuse: VideoAbuseModel) {
108 const users = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_ABUSES)
109 if (users.length === 0) return
110
111 logger.info('Notifying %s user/moderators of new video abuse %s.', users.length, videoAbuse.Video.url)
112
113 function settingGetter (user: UserModel) {
114 return user.NotificationSetting.videoAbuseAsModerator
115 }
116
117 async function notificationCreator (user: UserModel) {
118 const notification = await UserNotificationModel.create({
119 type: UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS,
120 userId: user.id,
121 videoAbuseId: videoAbuse.id
122 })
123 notification.VideoAbuse = videoAbuse
124
125 return notification
126 }
127
128 function emailSender (emails: string[]) {
129 return Emailer.Instance.addVideoAbuseModeratorsNotification(emails, videoAbuse)
130 }
131
132 return this.notify({ users, settingGetter, notificationCreator, emailSender })
133 }
134
135 private async notifyVideoOwnerOfBlacklist (videoBlacklist: VideoBlacklistModel) {
136 const user = await UserModel.loadByVideoId(videoBlacklist.videoId)
137 if (!user) return
138
139 logger.info('Notifying user %s that its video %s has been blacklisted.', user.username, videoBlacklist.Video.url)
140
141 function settingGetter (user: UserModel) {
142 return user.NotificationSetting.blacklistOnMyVideo
143 }
144
145 async function notificationCreator (user: UserModel) {
146 const notification = await UserNotificationModel.create({
147 type: UserNotificationType.BLACKLIST_ON_MY_VIDEO,
148 userId: user.id,
149 videoBlacklistId: videoBlacklist.id
150 })
151 notification.VideoBlacklist = videoBlacklist
152
153 return notification
154 }
155
156 function emailSender (emails: string[]) {
157 return Emailer.Instance.addVideoBlacklistNotification(emails, videoBlacklist)
158 }
159
160 return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender })
161 }
162
163 private async notifyVideoOwnerOfUnblacklist (video: VideoModel) {
164 const user = await UserModel.loadByVideoId(video.id)
165 if (!user) return
166
167 logger.info('Notifying user %s that its video %s has been unblacklisted.', user.username, video.url)
168
169 function settingGetter (user: UserModel) {
170 return user.NotificationSetting.blacklistOnMyVideo
171 }
172
173 async function notificationCreator (user: UserModel) {
174 const notification = await UserNotificationModel.create({
175 type: UserNotificationType.UNBLACKLIST_ON_MY_VIDEO,
176 userId: user.id,
177 videoId: video.id
178 })
179 notification.Video = video
180
181 return notification
182 }
183
184 function emailSender (emails: string[]) {
185 return Emailer.Instance.addVideoUnblacklistNotification(emails, video)
186 }
187
188 return this.notify({ users: [ user ], settingGetter, notificationCreator, emailSender })
189 }
190
191 private async notify (options: {
192 users: UserModel[],
193 notificationCreator: (user: UserModel) => Promise<UserNotificationModel>,
194 emailSender: (emails: string[]) => Promise<any> | Bluebird<any>,
195 settingGetter: (user: UserModel) => UserNotificationSettingValue
196 }) {
197 const emails: string[] = []
198
199 for (const user of options.users) {
200 if (this.isWebNotificationEnabled(options.settingGetter(user))) {
201 const notification = await options.notificationCreator(user)
202
203 PeerTubeSocket.Instance.sendNotification(user.id, notification)
204 }
205
206 if (this.isEmailEnabled(user, options.settingGetter(user))) {
207 emails.push(user.email)
208 }
209 }
210
211 if (emails.length !== 0) {
212 await options.emailSender(emails)
213 }
214 }
215
216 private isEmailEnabled (user: UserModel, value: UserNotificationSettingValue) {
217 if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION === true && user.emailVerified !== true) return false
218
219 return value === UserNotificationSettingValue.EMAIL || value === UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL
220 }
221
222 private isWebNotificationEnabled (value: UserNotificationSettingValue) {
223 return value === UserNotificationSettingValue.WEB_NOTIFICATION || value === UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL
224 }
225
226 static get Instance () {
227 return this.instance || (this.instance = new this())
228 }
229}
230
231// ---------------------------------------------------------------------------
232
233export {
234 Notifier
235}
diff --git a/server/lib/oauth-model.ts b/server/lib/oauth-model.ts
index 5cbe60b82..2cd2ae97c 100644
--- a/server/lib/oauth-model.ts
+++ b/server/lib/oauth-model.ts
@@ -1,3 +1,4 @@
1import * as Bluebird from 'bluebird'
1import { AccessDeniedError } from 'oauth2-server' 2import { AccessDeniedError } from 'oauth2-server'
2import { logger } from '../helpers/logger' 3import { logger } from '../helpers/logger'
3import { UserModel } from '../models/account/user' 4import { UserModel } from '../models/account/user'
@@ -37,7 +38,7 @@ function clearCacheByToken (token: string) {
37function getAccessToken (bearerToken: string) { 38function getAccessToken (bearerToken: string) {
38 logger.debug('Getting access token (bearerToken: ' + bearerToken + ').') 39 logger.debug('Getting access token (bearerToken: ' + bearerToken + ').')
39 40
40 if (accessTokenCache[bearerToken] !== undefined) return accessTokenCache[bearerToken] 41 if (accessTokenCache[bearerToken] !== undefined) return Bluebird.resolve(accessTokenCache[bearerToken])
41 42
42 return OAuthTokenModel.getByTokenAndPopulateUser(bearerToken) 43 return OAuthTokenModel.getByTokenAndPopulateUser(bearerToken)
43 .then(tokenModel => { 44 .then(tokenModel => {
diff --git a/server/lib/peertube-socket.ts b/server/lib/peertube-socket.ts
new file mode 100644
index 000000000..eb84ecd4b
--- /dev/null
+++ b/server/lib/peertube-socket.ts
@@ -0,0 +1,52 @@
1import * as SocketIO from 'socket.io'
2import { authenticateSocket } from '../middlewares'
3import { UserNotificationModel } from '../models/account/user-notification'
4import { logger } from '../helpers/logger'
5import { Server } from 'http'
6
7class PeerTubeSocket {
8
9 private static instance: PeerTubeSocket
10
11 private userNotificationSockets: { [ userId: number ]: SocketIO.Socket } = {}
12
13 private constructor () {}
14
15 init (server: Server) {
16 const io = SocketIO(server)
17
18 io.of('/user-notifications')
19 .use(authenticateSocket)
20 .on('connection', socket => {
21 const userId = socket.handshake.query.user.id
22
23 logger.debug('User %d connected on the notification system.', userId)
24
25 this.userNotificationSockets[userId] = socket
26
27 socket.on('disconnect', () => {
28 logger.debug('User %d disconnected from SocketIO notifications.', userId)
29
30 delete this.userNotificationSockets[userId]
31 })
32 })
33 }
34
35 sendNotification (userId: number, notification: UserNotificationModel) {
36 const socket = this.userNotificationSockets[userId]
37
38 if (!socket) return
39
40 socket.emit('new-notification', notification.toFormattedJSON())
41 }
42
43 static get Instance () {
44 return this.instance || (this.instance = new this())
45 }
46}
47
48// ---------------------------------------------------------------------------
49
50export {
51 PeerTubeSocket
52}
diff --git a/server/lib/schedulers/update-videos-scheduler.ts b/server/lib/schedulers/update-videos-scheduler.ts
index 21f071f9e..b7fb029f1 100644
--- a/server/lib/schedulers/update-videos-scheduler.ts
+++ b/server/lib/schedulers/update-videos-scheduler.ts
@@ -5,6 +5,7 @@ import { retryTransactionWrapper } from '../../helpers/database-utils'
5import { federateVideoIfNeeded } from '../activitypub' 5import { 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'
8 9
9export class UpdateVideosScheduler extends AbstractScheduler { 10export class UpdateVideosScheduler extends AbstractScheduler {
10 11
@@ -39,6 +40,10 @@ export class UpdateVideosScheduler extends AbstractScheduler {
39 40
40 await video.save({ transaction: t }) 41 await video.save({ transaction: t })
41 await federateVideoIfNeeded(video, isNewVideo, t) 42 await federateVideoIfNeeded(video, isNewVideo, t)
43
44 if (oldPrivacy === VideoPrivacy.UNLISTED || oldPrivacy === VideoPrivacy.PRIVATE) {
45 Notifier.Instance.notifyOnNewVideo(video)
46 }
42 } 47 }
43 48
44 await schedule.destroy({ transaction: t }) 49 await schedule.destroy({ transaction: t })
diff --git a/server/lib/user.ts b/server/lib/user.ts
index 29d6d087d..72127819c 100644
--- a/server/lib/user.ts
+++ b/server/lib/user.ts
@@ -9,6 +9,8 @@ import { createVideoChannel } from './video-channel'
9import { VideoChannelModel } from '../models/video/video-channel' 9import { VideoChannelModel } from '../models/video/video-channel'
10import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model' 10import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model'
11import { ActorModel } from '../models/activitypub/actor' 11import { ActorModel } from '../models/activitypub/actor'
12import { UserNotificationSettingModel } from '../models/account/user-notification-setting'
13import { UserNotificationSettingValue } from '../../shared/models/users'
12 14
13async function createUserAccountAndChannel (userToCreate: UserModel, validateUser = true) { 15async function createUserAccountAndChannel (userToCreate: UserModel, validateUser = true) {
14 const { user, account, videoChannel } = await sequelizeTypescript.transaction(async t => { 16 const { user, account, videoChannel } = await sequelizeTypescript.transaction(async t => {
@@ -18,6 +20,8 @@ async function createUserAccountAndChannel (userToCreate: UserModel, validateUse
18 } 20 }
19 21
20 const userCreated = await userToCreate.save(userOptions) 22 const userCreated = await userToCreate.save(userOptions)
23 userCreated.NotificationSetting = await createDefaultUserNotificationSettings(userCreated, t)
24
21 const accountCreated = await createLocalAccountWithoutKeys(userCreated.username, userCreated.id, null, t) 25 const accountCreated = await createLocalAccountWithoutKeys(userCreated.username, userCreated.id, null, t)
22 userCreated.Account = accountCreated 26 userCreated.Account = accountCreated
23 27
@@ -88,3 +92,15 @@ export {
88 createUserAccountAndChannel, 92 createUserAccountAndChannel,
89 createLocalAccountWithoutKeys 93 createLocalAccountWithoutKeys
90} 94}
95
96// ---------------------------------------------------------------------------
97
98function createDefaultUserNotificationSettings (user: UserModel, t: Sequelize.Transaction | undefined) {
99 return UserNotificationSettingModel.create({
100 userId: user.id,
101 newVideoFromSubscription: UserNotificationSettingValue.WEB_NOTIFICATION,
102 newCommentOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION,
103 videoAbuseAsModerator: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL,
104 blacklistOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL
105 }, { transaction: t })
106}