diff options
52 files changed, 2449 insertions, 111 deletions
diff --git a/package.json b/package.json index 3983f5f2c..ea3f88e24 100644 --- a/package.json +++ b/package.json | |||
@@ -146,6 +146,7 @@ | |||
146 | "sequelize-typescript": "0.6.6", | 146 | "sequelize-typescript": "0.6.6", |
147 | "sharp": "^0.21.0", | 147 | "sharp": "^0.21.0", |
148 | "sitemap": "^2.1.0", | 148 | "sitemap": "^2.1.0", |
149 | "socket.io": "^2.2.0", | ||
149 | "srt-to-vtt": "^1.1.2", | 150 | "srt-to-vtt": "^1.1.2", |
150 | "summon-install": "^0.4.3", | 151 | "summon-install": "^0.4.3", |
151 | "useragent": "^2.3.0", | 152 | "useragent": "^2.3.0", |
@@ -189,6 +190,7 @@ | |||
189 | "@types/redis": "^2.8.5", | 190 | "@types/redis": "^2.8.5", |
190 | "@types/request": "^2.0.3", | 191 | "@types/request": "^2.0.3", |
191 | "@types/sharp": "^0.21.0", | 192 | "@types/sharp": "^0.21.0", |
193 | "@types/socket.io": "^2.1.2", | ||
192 | "@types/supertest": "^2.0.3", | 194 | "@types/supertest": "^2.0.3", |
193 | "@types/validator": "^9.4.0", | 195 | "@types/validator": "^9.4.0", |
194 | "@types/webtorrent": "^0.98.4", | 196 | "@types/webtorrent": "^0.98.4", |
@@ -28,7 +28,7 @@ import { checkMissedConfig, checkFFmpeg } from './server/initializers/checker-be | |||
28 | 28 | ||
29 | // Do not use barrels because we don't want to load all modules here (we need to initialize database first) | 29 | // Do not use barrels because we don't want to load all modules here (we need to initialize database first) |
30 | import { logger } from './server/helpers/logger' | 30 | import { logger } from './server/helpers/logger' |
31 | import { API_VERSION, CONFIG, CACHE, HTTP_SIGNATURE } from './server/initializers/constants' | 31 | import { API_VERSION, CONFIG, CACHE } from './server/initializers/constants' |
32 | 32 | ||
33 | const missed = checkMissedConfig() | 33 | const missed = checkMissedConfig() |
34 | if (missed.length !== 0) { | 34 | if (missed.length !== 0) { |
@@ -90,7 +90,7 @@ import { | |||
90 | servicesRouter, | 90 | servicesRouter, |
91 | webfingerRouter, | 91 | webfingerRouter, |
92 | trackerRouter, | 92 | trackerRouter, |
93 | createWebsocketServer, botsRouter | 93 | createWebsocketTrackerServer, botsRouter |
94 | } from './server/controllers' | 94 | } from './server/controllers' |
95 | import { advertiseDoNotTrack } from './server/middlewares/dnt' | 95 | import { advertiseDoNotTrack } from './server/middlewares/dnt' |
96 | import { Redis } from './server/lib/redis' | 96 | import { Redis } from './server/lib/redis' |
@@ -100,6 +100,7 @@ import { UpdateVideosScheduler } from './server/lib/schedulers/update-videos-sch | |||
100 | import { YoutubeDlUpdateScheduler } from './server/lib/schedulers/youtube-dl-update-scheduler' | 100 | import { YoutubeDlUpdateScheduler } from './server/lib/schedulers/youtube-dl-update-scheduler' |
101 | import { VideosRedundancyScheduler } from './server/lib/schedulers/videos-redundancy-scheduler' | 101 | import { VideosRedundancyScheduler } from './server/lib/schedulers/videos-redundancy-scheduler' |
102 | import { isHTTPSignatureDigestValid } from './server/helpers/peertube-crypto' | 102 | import { isHTTPSignatureDigestValid } from './server/helpers/peertube-crypto' |
103 | import { PeerTubeSocket } from './server/lib/peertube-socket' | ||
103 | 104 | ||
104 | // ----------- Command line ----------- | 105 | // ----------- Command line ----------- |
105 | 106 | ||
@@ -136,7 +137,7 @@ app.use(bodyParser.urlencoded({ extended: false })) | |||
136 | app.use(bodyParser.json({ | 137 | app.use(bodyParser.json({ |
137 | type: [ 'application/json', 'application/*+json' ], | 138 | type: [ 'application/json', 'application/*+json' ], |
138 | limit: '500kb', | 139 | limit: '500kb', |
139 | verify: (req: express.Request, _, buf: Buffer, encoding: string) => { | 140 | verify: (req: express.Request, _, buf: Buffer) => { |
140 | const valid = isHTTPSignatureDigestValid(buf, req) | 141 | const valid = isHTTPSignatureDigestValid(buf, req) |
141 | if (valid !== true) throw new Error('Invalid digest') | 142 | if (valid !== true) throw new Error('Invalid digest') |
142 | } | 143 | } |
@@ -189,7 +190,7 @@ app.use(function (err, req, res, next) { | |||
189 | return res.status(err.status || 500).end() | 190 | return res.status(err.status || 500).end() |
190 | }) | 191 | }) |
191 | 192 | ||
192 | const server = createWebsocketServer(app) | 193 | const server = createWebsocketTrackerServer(app) |
193 | 194 | ||
194 | // ----------- Run ----------- | 195 | // ----------- Run ----------- |
195 | 196 | ||
@@ -228,6 +229,8 @@ async function startApplication () { | |||
228 | // Redis initialization | 229 | // Redis initialization |
229 | Redis.Instance.init() | 230 | Redis.Instance.init() |
230 | 231 | ||
232 | PeerTubeSocket.Instance.init(server) | ||
233 | |||
231 | // Make server listening | 234 | // Make server listening |
232 | server.listen(port, hostname, () => { | 235 | server.listen(port, hostname, () => { |
233 | logger.info('Server listening on %s:%d', hostname, port) | 236 | logger.info('Server listening on %s:%d', hostname, port) |
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts index bc24792a2..98be46ea2 100644 --- a/server/controllers/api/users/index.ts +++ b/server/controllers/api/users/index.ts | |||
@@ -39,6 +39,7 @@ import { meRouter } from './me' | |||
39 | import { deleteUserToken } from '../../../lib/oauth-model' | 39 | import { deleteUserToken } from '../../../lib/oauth-model' |
40 | import { myBlocklistRouter } from './my-blocklist' | 40 | import { myBlocklistRouter } from './my-blocklist' |
41 | import { myVideosHistoryRouter } from './my-history' | 41 | import { myVideosHistoryRouter } from './my-history' |
42 | import { myNotificationsRouter } from './my-notifications' | ||
42 | 43 | ||
43 | const auditLogger = auditLoggerFactory('users') | 44 | const auditLogger = auditLoggerFactory('users') |
44 | 45 | ||
@@ -55,6 +56,7 @@ const askSendEmailLimiter = new RateLimit({ | |||
55 | }) | 56 | }) |
56 | 57 | ||
57 | const usersRouter = express.Router() | 58 | const usersRouter = express.Router() |
59 | usersRouter.use('/', myNotificationsRouter) | ||
58 | usersRouter.use('/', myBlocklistRouter) | 60 | usersRouter.use('/', myBlocklistRouter) |
59 | usersRouter.use('/', myVideosHistoryRouter) | 61 | usersRouter.use('/', myVideosHistoryRouter) |
60 | usersRouter.use('/', meRouter) | 62 | usersRouter.use('/', meRouter) |
diff --git a/server/controllers/api/users/my-notifications.ts b/server/controllers/api/users/my-notifications.ts new file mode 100644 index 000000000..cef1d237c --- /dev/null +++ b/server/controllers/api/users/my-notifications.ts | |||
@@ -0,0 +1,84 @@ | |||
1 | import * as express from 'express' | ||
2 | import 'multer' | ||
3 | import { | ||
4 | asyncMiddleware, | ||
5 | asyncRetryTransactionMiddleware, | ||
6 | authenticate, | ||
7 | paginationValidator, | ||
8 | setDefaultPagination, | ||
9 | setDefaultSort, | ||
10 | userNotificationsSortValidator | ||
11 | } from '../../../middlewares' | ||
12 | import { UserModel } from '../../../models/account/user' | ||
13 | import { getFormattedObjects } from '../../../helpers/utils' | ||
14 | import { UserNotificationModel } from '../../../models/account/user-notification' | ||
15 | import { meRouter } from './me' | ||
16 | import { | ||
17 | markAsReadUserNotificationsValidator, | ||
18 | updateNotificationSettingsValidator | ||
19 | } from '../../../middlewares/validators/user-notifications' | ||
20 | import { UserNotificationSetting } from '../../../../shared/models/users' | ||
21 | import { UserNotificationSettingModel } from '../../../models/account/user-notification-setting' | ||
22 | |||
23 | const myNotificationsRouter = express.Router() | ||
24 | |||
25 | meRouter.put('/me/notification-settings', | ||
26 | authenticate, | ||
27 | updateNotificationSettingsValidator, | ||
28 | asyncRetryTransactionMiddleware(updateNotificationSettings) | ||
29 | ) | ||
30 | |||
31 | myNotificationsRouter.get('/me/notifications', | ||
32 | authenticate, | ||
33 | paginationValidator, | ||
34 | userNotificationsSortValidator, | ||
35 | setDefaultSort, | ||
36 | setDefaultPagination, | ||
37 | asyncMiddleware(listUserNotifications) | ||
38 | ) | ||
39 | |||
40 | myNotificationsRouter.post('/me/notifications/read', | ||
41 | authenticate, | ||
42 | markAsReadUserNotificationsValidator, | ||
43 | asyncMiddleware(markAsReadUserNotifications) | ||
44 | ) | ||
45 | |||
46 | export { | ||
47 | myNotificationsRouter | ||
48 | } | ||
49 | |||
50 | // --------------------------------------------------------------------------- | ||
51 | |||
52 | async function updateNotificationSettings (req: express.Request, res: express.Response) { | ||
53 | const user: UserModel = res.locals.oauth.token.User | ||
54 | const body: UserNotificationSetting = req.body | ||
55 | |||
56 | const query = { | ||
57 | where: { | ||
58 | userId: user.id | ||
59 | } | ||
60 | } | ||
61 | |||
62 | await UserNotificationSettingModel.update({ | ||
63 | newVideoFromSubscription: body.newVideoFromSubscription, | ||
64 | newCommentOnMyVideo: body.newCommentOnMyVideo | ||
65 | }, query) | ||
66 | |||
67 | return res.status(204).end() | ||
68 | } | ||
69 | |||
70 | async function listUserNotifications (req: express.Request, res: express.Response) { | ||
71 | const user: UserModel = res.locals.oauth.token.User | ||
72 | |||
73 | const resultList = await UserNotificationModel.listForApi(user.id, req.query.start, req.query.count, req.query.sort) | ||
74 | |||
75 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | ||
76 | } | ||
77 | |||
78 | async function markAsReadUserNotifications (req: express.Request, res: express.Response) { | ||
79 | const user: UserModel = res.locals.oauth.token.User | ||
80 | |||
81 | await UserNotificationModel.markAsRead(user.id, req.body.ids) | ||
82 | |||
83 | return res.status(204).end() | ||
84 | } | ||
diff --git a/server/controllers/api/videos/abuse.ts b/server/controllers/api/videos/abuse.ts index d0c81804b..fe0a95cd5 100644 --- a/server/controllers/api/videos/abuse.ts +++ b/server/controllers/api/videos/abuse.ts | |||
@@ -22,6 +22,7 @@ import { VideoModel } from '../../../models/video/video' | |||
22 | import { VideoAbuseModel } from '../../../models/video/video-abuse' | 22 | import { VideoAbuseModel } from '../../../models/video/video-abuse' |
23 | import { auditLoggerFactory, VideoAbuseAuditView } from '../../../helpers/audit-logger' | 23 | import { auditLoggerFactory, VideoAbuseAuditView } from '../../../helpers/audit-logger' |
24 | import { UserModel } from '../../../models/account/user' | 24 | import { UserModel } from '../../../models/account/user' |
25 | import { Notifier } from '../../../lib/notifier' | ||
25 | 26 | ||
26 | const auditLogger = auditLoggerFactory('abuse') | 27 | const auditLogger = auditLoggerFactory('abuse') |
27 | const abuseVideoRouter = express.Router() | 28 | const abuseVideoRouter = express.Router() |
@@ -117,6 +118,8 @@ async function reportVideoAbuse (req: express.Request, res: express.Response) { | |||
117 | await sendVideoAbuse(reporterAccount.Actor, videoAbuseInstance, videoInstance) | 118 | await sendVideoAbuse(reporterAccount.Actor, videoAbuseInstance, videoInstance) |
118 | } | 119 | } |
119 | 120 | ||
121 | Notifier.Instance.notifyOnNewVideoAbuse(videoAbuseInstance) | ||
122 | |||
120 | auditLogger.create(reporterAccount.Actor.getIdentifier(), new VideoAbuseAuditView(videoAbuseInstance.toFormattedJSON())) | 123 | auditLogger.create(reporterAccount.Actor.getIdentifier(), new VideoAbuseAuditView(videoAbuseInstance.toFormattedJSON())) |
121 | 124 | ||
122 | return videoAbuseInstance | 125 | return videoAbuseInstance |
diff --git a/server/controllers/api/videos/blacklist.ts b/server/controllers/api/videos/blacklist.ts index 7f803c8e9..9ef08812b 100644 --- a/server/controllers/api/videos/blacklist.ts +++ b/server/controllers/api/videos/blacklist.ts | |||
@@ -16,6 +16,8 @@ import { | |||
16 | } from '../../../middlewares' | 16 | } from '../../../middlewares' |
17 | import { VideoBlacklistModel } from '../../../models/video/video-blacklist' | 17 | import { VideoBlacklistModel } from '../../../models/video/video-blacklist' |
18 | import { sequelizeTypescript } from '../../../initializers' | 18 | import { sequelizeTypescript } from '../../../initializers' |
19 | import { Notifier } from '../../../lib/notifier' | ||
20 | import { VideoModel } from '../../../models/video/video' | ||
19 | 21 | ||
20 | const blacklistRouter = express.Router() | 22 | const blacklistRouter = express.Router() |
21 | 23 | ||
@@ -67,13 +69,18 @@ async function addVideoToBlacklist (req: express.Request, res: express.Response) | |||
67 | reason: body.reason | 69 | reason: body.reason |
68 | } | 70 | } |
69 | 71 | ||
70 | await VideoBlacklistModel.create(toCreate) | 72 | const blacklist = await VideoBlacklistModel.create(toCreate) |
73 | blacklist.Video = videoInstance | ||
74 | |||
75 | Notifier.Instance.notifyOnVideoBlacklist(blacklist) | ||
76 | |||
77 | logger.info('Video %s blacklisted.', res.locals.video.uuid) | ||
78 | |||
71 | return res.type('json').status(204).end() | 79 | return res.type('json').status(204).end() |
72 | } | 80 | } |
73 | 81 | ||
74 | async function updateVideoBlacklistController (req: express.Request, res: express.Response) { | 82 | async function updateVideoBlacklistController (req: express.Request, res: express.Response) { |
75 | const videoBlacklist = res.locals.videoBlacklist as VideoBlacklistModel | 83 | const videoBlacklist = res.locals.videoBlacklist as VideoBlacklistModel |
76 | logger.info(videoBlacklist) | ||
77 | 84 | ||
78 | if (req.body.reason !== undefined) videoBlacklist.reason = req.body.reason | 85 | if (req.body.reason !== undefined) videoBlacklist.reason = req.body.reason |
79 | 86 | ||
@@ -92,11 +99,14 @@ async function listBlacklist (req: express.Request, res: express.Response, next: | |||
92 | 99 | ||
93 | async function removeVideoFromBlacklistController (req: express.Request, res: express.Response, next: express.NextFunction) { | 100 | async function removeVideoFromBlacklistController (req: express.Request, res: express.Response, next: express.NextFunction) { |
94 | const videoBlacklist = res.locals.videoBlacklist as VideoBlacklistModel | 101 | const videoBlacklist = res.locals.videoBlacklist as VideoBlacklistModel |
102 | const video: VideoModel = res.locals.video | ||
95 | 103 | ||
96 | await sequelizeTypescript.transaction(t => { | 104 | await sequelizeTypescript.transaction(t => { |
97 | return videoBlacklist.destroy({ transaction: t }) | 105 | return videoBlacklist.destroy({ transaction: t }) |
98 | }) | 106 | }) |
99 | 107 | ||
108 | Notifier.Instance.notifyOnVideoUnblacklist(video) | ||
109 | |||
100 | logger.info('Video %s removed from blacklist.', res.locals.video.uuid) | 110 | logger.info('Video %s removed from blacklist.', res.locals.video.uuid) |
101 | 111 | ||
102 | return res.type('json').status(204).end() | 112 | return res.type('json').status(204).end() |
diff --git a/server/controllers/api/videos/comment.ts b/server/controllers/api/videos/comment.ts index 3875c8f79..70c1148ba 100644 --- a/server/controllers/api/videos/comment.ts +++ b/server/controllers/api/videos/comment.ts | |||
@@ -26,6 +26,7 @@ import { VideoCommentModel } from '../../../models/video/video-comment' | |||
26 | import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger' | 26 | import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger' |
27 | import { AccountModel } from '../../../models/account/account' | 27 | import { AccountModel } from '../../../models/account/account' |
28 | import { UserModel } from '../../../models/account/user' | 28 | import { UserModel } from '../../../models/account/user' |
29 | import { Notifier } from '../../../lib/notifier' | ||
29 | 30 | ||
30 | const auditLogger = auditLoggerFactory('comments') | 31 | const auditLogger = auditLoggerFactory('comments') |
31 | const videoCommentRouter = express.Router() | 32 | const videoCommentRouter = express.Router() |
@@ -119,6 +120,7 @@ async function addVideoCommentThread (req: express.Request, res: express.Respons | |||
119 | }, t) | 120 | }, t) |
120 | }) | 121 | }) |
121 | 122 | ||
123 | Notifier.Instance.notifyOnNewComment(comment) | ||
122 | auditLogger.create(getAuditIdFromRes(res), new CommentAuditView(comment.toFormattedJSON())) | 124 | auditLogger.create(getAuditIdFromRes(res), new CommentAuditView(comment.toFormattedJSON())) |
123 | 125 | ||
124 | return res.json({ | 126 | return res.json({ |
@@ -140,6 +142,7 @@ async function addVideoCommentReply (req: express.Request, res: express.Response | |||
140 | }, t) | 142 | }, t) |
141 | }) | 143 | }) |
142 | 144 | ||
145 | Notifier.Instance.notifyOnNewComment(comment) | ||
143 | auditLogger.create(getAuditIdFromRes(res), new CommentAuditView(comment.toFormattedJSON())) | 146 | auditLogger.create(getAuditIdFromRes(res), new CommentAuditView(comment.toFormattedJSON())) |
144 | 147 | ||
145 | return res.json({ comment: comment.toFormattedJSON() }).end() | 148 | return res.json({ comment: comment.toFormattedJSON() }).end() |
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index 00a1302d1..94ed08fed 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts | |||
@@ -7,7 +7,8 @@ import { logger } from '../../../helpers/logger' | |||
7 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' | 7 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' |
8 | import { getFormattedObjects, getServerActor } from '../../../helpers/utils' | 8 | import { getFormattedObjects, getServerActor } from '../../../helpers/utils' |
9 | import { | 9 | import { |
10 | CONFIG, MIMETYPES, | 10 | CONFIG, |
11 | MIMETYPES, | ||
11 | PREVIEWS_SIZE, | 12 | PREVIEWS_SIZE, |
12 | sequelizeTypescript, | 13 | sequelizeTypescript, |
13 | THUMBNAILS_SIZE, | 14 | THUMBNAILS_SIZE, |
@@ -57,6 +58,7 @@ import { videoImportsRouter } from './import' | |||
57 | import { resetSequelizeInstance } from '../../../helpers/database-utils' | 58 | import { resetSequelizeInstance } from '../../../helpers/database-utils' |
58 | import { move } from 'fs-extra' | 59 | import { move } from 'fs-extra' |
59 | import { watchingRouter } from './watching' | 60 | import { watchingRouter } from './watching' |
61 | import { Notifier } from '../../../lib/notifier' | ||
60 | 62 | ||
61 | const auditLogger = auditLoggerFactory('videos') | 63 | const auditLogger = auditLoggerFactory('videos') |
62 | const videosRouter = express.Router() | 64 | const videosRouter = express.Router() |
@@ -262,6 +264,7 @@ async function addVideo (req: express.Request, res: express.Response) { | |||
262 | } | 264 | } |
263 | 265 | ||
264 | await federateVideoIfNeeded(video, true, t) | 266 | await federateVideoIfNeeded(video, true, t) |
267 | Notifier.Instance.notifyOnNewVideo(video) | ||
265 | 268 | ||
266 | auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON())) | 269 | auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON())) |
267 | logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid) | 270 | logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid) |
@@ -293,6 +296,7 @@ async function updateVideo (req: express.Request, res: express.Response) { | |||
293 | const oldVideoAuditView = new VideoAuditView(videoInstance.toFormattedDetailsJSON()) | 296 | const oldVideoAuditView = new VideoAuditView(videoInstance.toFormattedDetailsJSON()) |
294 | const videoInfoToUpdate: VideoUpdate = req.body | 297 | const videoInfoToUpdate: VideoUpdate = req.body |
295 | const wasPrivateVideo = videoInstance.privacy === VideoPrivacy.PRIVATE | 298 | const wasPrivateVideo = videoInstance.privacy === VideoPrivacy.PRIVATE |
299 | const wasUnlistedVideo = videoInstance.privacy === VideoPrivacy.UNLISTED | ||
296 | 300 | ||
297 | // Process thumbnail or create it from the video | 301 | // Process thumbnail or create it from the video |
298 | if (req.files && req.files['thumbnailfile']) { | 302 | if (req.files && req.files['thumbnailfile']) { |
@@ -363,6 +367,10 @@ async function updateVideo (req: express.Request, res: express.Response) { | |||
363 | const isNewVideo = wasPrivateVideo && videoInstanceUpdated.privacy !== VideoPrivacy.PRIVATE | 367 | const isNewVideo = wasPrivateVideo && videoInstanceUpdated.privacy !== VideoPrivacy.PRIVATE |
364 | await federateVideoIfNeeded(videoInstanceUpdated, isNewVideo, t) | 368 | await federateVideoIfNeeded(videoInstanceUpdated, isNewVideo, t) |
365 | 369 | ||
370 | if (wasUnlistedVideo || wasPrivateVideo) { | ||
371 | Notifier.Instance.notifyOnNewVideo(videoInstanceUpdated) | ||
372 | } | ||
373 | |||
366 | auditLogger.update( | 374 | auditLogger.update( |
367 | getAuditIdFromRes(res), | 375 | getAuditIdFromRes(res), |
368 | new VideoAuditView(videoInstanceUpdated.toFormattedDetailsJSON()), | 376 | new VideoAuditView(videoInstanceUpdated.toFormattedDetailsJSON()), |
diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts index ccb9b6029..960085af1 100644 --- a/server/controllers/feeds.ts +++ b/server/controllers/feeds.ts | |||
@@ -56,7 +56,7 @@ async function generateVideoCommentsFeed (req: express.Request, res: express.Res | |||
56 | 56 | ||
57 | // Adding video items to the feed, one at a time | 57 | // Adding video items to the feed, one at a time |
58 | comments.forEach(comment => { | 58 | comments.forEach(comment => { |
59 | const link = CONFIG.WEBSERVER.URL + '/videos/watch/' + comment.Video.uuid + ';threadId=' + comment.getThreadId() | 59 | const link = CONFIG.WEBSERVER.URL + comment.getCommentStaticPath() |
60 | 60 | ||
61 | feed.addItem({ | 61 | feed.addItem({ |
62 | title: `${comment.Video.name} - ${comment.Account.getDisplayName()}`, | 62 | title: `${comment.Video.name} - ${comment.Account.getDisplayName()}`, |
diff --git a/server/controllers/tracker.ts b/server/controllers/tracker.ts index 9bc7586d1..53f1653b5 100644 --- a/server/controllers/tracker.ts +++ b/server/controllers/tracker.ts | |||
@@ -59,7 +59,7 @@ const onHttpRequest = trackerServer.onHttpRequest.bind(trackerServer) | |||
59 | trackerRouter.get('/tracker/announce', (req, res) => onHttpRequest(req, res, { action: 'announce' })) | 59 | trackerRouter.get('/tracker/announce', (req, res) => onHttpRequest(req, res, { action: 'announce' })) |
60 | trackerRouter.get('/tracker/scrape', (req, res) => onHttpRequest(req, res, { action: 'scrape' })) | 60 | trackerRouter.get('/tracker/scrape', (req, res) => onHttpRequest(req, res, { action: 'scrape' })) |
61 | 61 | ||
62 | function createWebsocketServer (app: express.Application) { | 62 | function createWebsocketTrackerServer (app: express.Application) { |
63 | const server = http.createServer(app) | 63 | const server = http.createServer(app) |
64 | const wss = new WebSocketServer({ server: server, path: '/tracker/socket' }) | 64 | const wss = new WebSocketServer({ server: server, path: '/tracker/socket' }) |
65 | wss.on('connection', function (ws, req) { | 65 | wss.on('connection', function (ws, req) { |
@@ -76,7 +76,7 @@ function createWebsocketServer (app: express.Application) { | |||
76 | 76 | ||
77 | export { | 77 | export { |
78 | trackerRouter, | 78 | trackerRouter, |
79 | createWebsocketServer | 79 | createWebsocketTrackerServer |
80 | } | 80 | } |
81 | 81 | ||
82 | // --------------------------------------------------------------------------- | 82 | // --------------------------------------------------------------------------- |
diff --git a/server/helpers/custom-validators/misc.ts b/server/helpers/custom-validators/misc.ts index 6d10a65a8..a093e3e1b 100644 --- a/server/helpers/custom-validators/misc.ts +++ b/server/helpers/custom-validators/misc.ts | |||
@@ -9,6 +9,10 @@ function isArray (value: any) { | |||
9 | return Array.isArray(value) | 9 | return Array.isArray(value) |
10 | } | 10 | } |
11 | 11 | ||
12 | function isIntArray (value: any) { | ||
13 | return Array.isArray(value) && value.every(v => validator.isInt('' + v)) | ||
14 | } | ||
15 | |||
12 | function isDateValid (value: string) { | 16 | function isDateValid (value: string) { |
13 | return exists(value) && validator.isISO8601(value) | 17 | return exists(value) && validator.isISO8601(value) |
14 | } | 18 | } |
@@ -78,6 +82,7 @@ function isFileValid ( | |||
78 | 82 | ||
79 | export { | 83 | export { |
80 | exists, | 84 | exists, |
85 | isIntArray, | ||
81 | isArray, | 86 | isArray, |
82 | isIdValid, | 87 | isIdValid, |
83 | isUUIDValid, | 88 | isUUIDValid, |
diff --git a/server/helpers/custom-validators/user-notifications.ts b/server/helpers/custom-validators/user-notifications.ts new file mode 100644 index 000000000..4fb5d922d --- /dev/null +++ b/server/helpers/custom-validators/user-notifications.ts | |||
@@ -0,0 +1,19 @@ | |||
1 | import { exists } from './misc' | ||
2 | import * as validator from 'validator' | ||
3 | import { UserNotificationType } from '../../../shared/models/users' | ||
4 | import { UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' | ||
5 | |||
6 | function isUserNotificationTypeValid (value: any) { | ||
7 | return exists(value) && validator.isInt('' + value) && UserNotificationType[value] !== undefined | ||
8 | } | ||
9 | |||
10 | function isUserNotificationSettingValid (value: any) { | ||
11 | return exists(value) && | ||
12 | validator.isInt('' + value) && | ||
13 | UserNotificationSettingValue[ value ] !== undefined | ||
14 | } | ||
15 | |||
16 | export { | ||
17 | isUserNotificationSettingValid, | ||
18 | isUserNotificationTypeValid | ||
19 | } | ||
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index c3df2383a..fcfaf71a0 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -50,7 +50,9 @@ const SORTABLE_COLUMNS = { | |||
50 | VIDEO_CHANNELS_SEARCH: [ 'match', 'displayName', 'createdAt' ], | 50 | VIDEO_CHANNELS_SEARCH: [ 'match', 'displayName', 'createdAt' ], |
51 | 51 | ||
52 | ACCOUNTS_BLOCKLIST: [ 'createdAt' ], | 52 | ACCOUNTS_BLOCKLIST: [ 'createdAt' ], |
53 | SERVERS_BLOCKLIST: [ 'createdAt' ] | 53 | SERVERS_BLOCKLIST: [ 'createdAt' ], |
54 | |||
55 | USER_NOTIFICATIONS: [ 'createdAt' ] | ||
54 | } | 56 | } |
55 | 57 | ||
56 | const OAUTH_LIFETIME = { | 58 | const OAUTH_LIFETIME = { |
diff --git a/server/initializers/database.ts b/server/initializers/database.ts index 40cd659ab..84ad2079b 100644 --- a/server/initializers/database.ts +++ b/server/initializers/database.ts | |||
@@ -31,6 +31,8 @@ import { VideoRedundancyModel } from '../models/redundancy/video-redundancy' | |||
31 | import { UserVideoHistoryModel } from '../models/account/user-video-history' | 31 | import { UserVideoHistoryModel } from '../models/account/user-video-history' |
32 | import { AccountBlocklistModel } from '../models/account/account-blocklist' | 32 | import { AccountBlocklistModel } from '../models/account/account-blocklist' |
33 | import { ServerBlocklistModel } from '../models/server/server-blocklist' | 33 | import { ServerBlocklistModel } from '../models/server/server-blocklist' |
34 | import { UserNotificationModel } from '../models/account/user-notification' | ||
35 | import { UserNotificationSettingModel } from '../models/account/user-notification-setting' | ||
34 | 36 | ||
35 | require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string | 37 | require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string |
36 | 38 | ||
@@ -95,7 +97,9 @@ async function initDatabaseModels (silent: boolean) { | |||
95 | VideoRedundancyModel, | 97 | VideoRedundancyModel, |
96 | UserVideoHistoryModel, | 98 | UserVideoHistoryModel, |
97 | AccountBlocklistModel, | 99 | AccountBlocklistModel, |
98 | ServerBlocklistModel | 100 | ServerBlocklistModel, |
101 | UserNotificationModel, | ||
102 | UserNotificationSettingModel | ||
99 | ]) | 103 | ]) |
100 | 104 | ||
101 | // Check extensions exist in the database | 105 | // Check extensions exist in the database |
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' | |||
5 | import { VideoShareModel } from '../../../models/video/video-share' | 5 | import { VideoShareModel } from '../../../models/video/video-share' |
6 | import { forwardVideoRelatedActivity } from '../send/utils' | 6 | import { forwardVideoRelatedActivity } from '../send/utils' |
7 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' | 7 | import { getOrCreateVideoAndAccountAndChannel } from '../videos' |
8 | import { VideoPrivacy } from '../../../../shared/models/videos' | ||
9 | import { Notifier } from '../../notifier' | ||
8 | 10 | ||
9 | async function processAnnounceActivity (activity: ActivityAnnounce, actorAnnouncer: ActorModel) { | 11 | async function processAnnounceActivity (activity: ActivityAnnounce, actorAnnouncer: ActorModel) { |
10 | return retryTransactionWrapper(processVideoShare, actorAnnouncer, activity) | 12 | return retryTransactionWrapper(processVideoShare, actorAnnouncer, activity) |
@@ -21,9 +23,9 @@ export { | |||
21 | async function processVideoShare (actorAnnouncer: ActorModel, activity: ActivityAnnounce) { | 23 | async 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' | |||
13 | import { Redis } from '../../redis' | 13 | import { Redis } from '../../redis' |
14 | import { createOrUpdateCacheFile } from '../cache-file' | 14 | import { createOrUpdateCacheFile } from '../cache-file' |
15 | import { getVideoDislikeActivityPubUrl } from '../url' | 15 | import { getVideoDislikeActivityPubUrl } from '../url' |
16 | import { Notifier } from '../../notifier' | ||
16 | 17 | ||
17 | async function processCreateActivity (activity: ActivityCreate, byActor: ActorModel) { | 18 | async function processCreateActivity (activity: ActivityCreate, byActor: ActorModel) { |
18 | const activityObject = activity.object | 19 | const activityObject = activity.object |
@@ -47,7 +48,9 @@ export { | |||
47 | async function processCreateVideo (activity: ActivityCreate) { | 48 | async 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' | |||
29 | import { AccountModel } from '../../models/account/account' | 29 | import { AccountModel } from '../../models/account/account' |
30 | import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video' | 30 | import { fetchVideoByUrl, VideoFetchByUrlType } from '../../helpers/video' |
31 | import { checkUrlsSameHost, getAPUrl } from '../../helpers/activitypub' | 31 | import { checkUrlsSameHost, getAPUrl } from '../../helpers/activitypub' |
32 | import { Notifier } from '../notifier' | ||
32 | 33 | ||
33 | async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) { | 34 | async 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 | ||
198 | async function updateVideoFromAP (options: { | 199 | async 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 @@ | |||
1 | import { createTransport, Transporter } from 'nodemailer' | 1 | import { createTransport, Transporter } from 'nodemailer' |
2 | import { UserRight } from '../../shared/models/users' | ||
3 | import { isTestInstance } from '../helpers/core-utils' | 2 | import { isTestInstance } from '../helpers/core-utils' |
4 | import { bunyanLogger, logger } from '../helpers/logger' | 3 | import { bunyanLogger, logger } from '../helpers/logger' |
5 | import { CONFIG } from '../initializers' | 4 | import { CONFIG } from '../initializers' |
@@ -8,6 +7,9 @@ import { VideoModel } from '../models/video/video' | |||
8 | import { JobQueue } from './job-queue' | 7 | import { JobQueue } from './job-queue' |
9 | import { EmailPayload } from './job-queue/handlers/email' | 8 | import { EmailPayload } from './job-queue/handlers/email' |
10 | import { readFileSync } from 'fs-extra' | 9 | import { readFileSync } from 'fs-extra' |
10 | import { VideoCommentModel } from '../models/video/video-comment' | ||
11 | import { VideoAbuseModel } from '../models/video/video-abuse' | ||
12 | import { VideoBlacklistModel } from '../models/video/video-blacklist' | ||
11 | 13 | ||
12 | class Emailer { | 14 | class 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' | |||
9 | import * as Bluebird from 'bluebird' | 9 | import * as Bluebird from 'bluebird' |
10 | import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils' | 10 | import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils' |
11 | import { importVideoFile, transcodeOriginalVideofile, optimizeVideofile } from '../../video-transcoding' | 11 | import { importVideoFile, transcodeOriginalVideofile, optimizeVideofile } from '../../video-transcoding' |
12 | import { Notifier } from '../../notifier' | ||
12 | 13 | ||
13 | export type VideoFilePayload = { | 14 | export 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' | |||
15 | import { downloadWebTorrentVideo } from '../../../helpers/webtorrent' | 15 | import { downloadWebTorrentVideo } from '../../../helpers/webtorrent' |
16 | import { getSecureTorrentName } from '../../../helpers/utils' | 16 | import { getSecureTorrentName } from '../../../helpers/utils' |
17 | import { remove, move, stat } from 'fs-extra' | 17 | import { remove, move, stat } from 'fs-extra' |
18 | import { Notifier } from '../../notifier' | ||
18 | 19 | ||
19 | type VideoImportYoutubeDLPayload = { | 20 | type 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 @@ | |||
1 | import { UserNotificationSettingValue, UserNotificationType, UserRight } from '../../shared/models/users' | ||
2 | import { logger } from '../helpers/logger' | ||
3 | import { VideoModel } from '../models/video/video' | ||
4 | import { Emailer } from './emailer' | ||
5 | import { UserNotificationModel } from '../models/account/user-notification' | ||
6 | import { VideoCommentModel } from '../models/video/video-comment' | ||
7 | import { UserModel } from '../models/account/user' | ||
8 | import { PeerTubeSocket } from './peertube-socket' | ||
9 | import { CONFIG } from '../initializers/constants' | ||
10 | import { VideoPrivacy, VideoState } from '../../shared/models/videos' | ||
11 | import { VideoAbuseModel } from '../models/video/video-abuse' | ||
12 | import { VideoBlacklistModel } from '../models/video/video-blacklist' | ||
13 | import * as Bluebird from 'bluebird' | ||
14 | |||
15 | class 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 | |||
233 | export { | ||
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 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
1 | import { AccessDeniedError } from 'oauth2-server' | 2 | import { AccessDeniedError } from 'oauth2-server' |
2 | import { logger } from '../helpers/logger' | 3 | import { logger } from '../helpers/logger' |
3 | import { UserModel } from '../models/account/user' | 4 | import { UserModel } from '../models/account/user' |
@@ -37,7 +38,7 @@ function clearCacheByToken (token: string) { | |||
37 | function getAccessToken (bearerToken: string) { | 38 | function 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 @@ | |||
1 | import * as SocketIO from 'socket.io' | ||
2 | import { authenticateSocket } from '../middlewares' | ||
3 | import { UserNotificationModel } from '../models/account/user-notification' | ||
4 | import { logger } from '../helpers/logger' | ||
5 | import { Server } from 'http' | ||
6 | |||
7 | class 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 | |||
50 | export { | ||
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' | |||
5 | import { federateVideoIfNeeded } from '../activitypub' | 5 | import { federateVideoIfNeeded } from '../activitypub' |
6 | import { SCHEDULER_INTERVALS_MS, sequelizeTypescript } from '../../initializers' | 6 | import { SCHEDULER_INTERVALS_MS, sequelizeTypescript } from '../../initializers' |
7 | import { VideoPrivacy } from '../../../shared/models/videos' | 7 | import { VideoPrivacy } from '../../../shared/models/videos' |
8 | import { Notifier } from '../notifier' | ||
8 | 9 | ||
9 | export class UpdateVideosScheduler extends AbstractScheduler { | 10 | export 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' | |||
9 | import { VideoChannelModel } from '../models/video/video-channel' | 9 | import { VideoChannelModel } from '../models/video/video-channel' |
10 | import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model' | 10 | import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model' |
11 | import { ActorModel } from '../models/activitypub/actor' | 11 | import { ActorModel } from '../models/activitypub/actor' |
12 | import { UserNotificationSettingModel } from '../models/account/user-notification-setting' | ||
13 | import { UserNotificationSettingValue } from '../../shared/models/users' | ||
12 | 14 | ||
13 | async function createUserAccountAndChannel (userToCreate: UserModel, validateUser = true) { | 15 | async 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 | |||
98 | function 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 | } | ||
diff --git a/server/middlewares/oauth.ts b/server/middlewares/oauth.ts index 8c1df2c3e..1d193d467 100644 --- a/server/middlewares/oauth.ts +++ b/server/middlewares/oauth.ts | |||
@@ -3,6 +3,8 @@ import * as OAuthServer from 'express-oauth-server' | |||
3 | import 'express-validator' | 3 | import 'express-validator' |
4 | import { OAUTH_LIFETIME } from '../initializers' | 4 | import { OAUTH_LIFETIME } from '../initializers' |
5 | import { logger } from '../helpers/logger' | 5 | import { logger } from '../helpers/logger' |
6 | import { Socket } from 'socket.io' | ||
7 | import { getAccessToken } from '../lib/oauth-model' | ||
6 | 8 | ||
7 | const oAuthServer = new OAuthServer({ | 9 | const oAuthServer = new OAuthServer({ |
8 | useErrorHandler: true, | 10 | useErrorHandler: true, |
@@ -28,6 +30,25 @@ function authenticate (req: express.Request, res: express.Response, next: expres | |||
28 | }) | 30 | }) |
29 | } | 31 | } |
30 | 32 | ||
33 | function authenticateSocket (socket: Socket, next: (err?: any) => void) { | ||
34 | const accessToken = socket.handshake.query.accessToken | ||
35 | |||
36 | logger.debug('Checking socket access token %s.', accessToken) | ||
37 | |||
38 | getAccessToken(accessToken) | ||
39 | .then(tokenDB => { | ||
40 | const now = new Date() | ||
41 | |||
42 | if (!tokenDB || tokenDB.accessTokenExpiresAt < now || tokenDB.refreshTokenExpiresAt < now) { | ||
43 | return next(new Error('Invalid access token.')) | ||
44 | } | ||
45 | |||
46 | socket.handshake.query.user = tokenDB.User | ||
47 | |||
48 | return next() | ||
49 | }) | ||
50 | } | ||
51 | |||
31 | function authenticatePromiseIfNeeded (req: express.Request, res: express.Response) { | 52 | function authenticatePromiseIfNeeded (req: express.Request, res: express.Response) { |
32 | return new Promise(resolve => { | 53 | return new Promise(resolve => { |
33 | // Already authenticated? (or tried to) | 54 | // Already authenticated? (or tried to) |
@@ -68,6 +89,7 @@ function token (req: express.Request, res: express.Response, next: express.NextF | |||
68 | 89 | ||
69 | export { | 90 | export { |
70 | authenticate, | 91 | authenticate, |
92 | authenticateSocket, | ||
71 | authenticatePromiseIfNeeded, | 93 | authenticatePromiseIfNeeded, |
72 | optionalAuthenticate, | 94 | optionalAuthenticate, |
73 | token | 95 | token |
diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts index 4c0577d8f..5ceda845f 100644 --- a/server/middlewares/validators/sort.ts +++ b/server/middlewares/validators/sort.ts | |||
@@ -18,6 +18,7 @@ const SORTABLE_FOLLOWING_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOW | |||
18 | const SORTABLE_USER_SUBSCRIPTIONS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USER_SUBSCRIPTIONS) | 18 | const SORTABLE_USER_SUBSCRIPTIONS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USER_SUBSCRIPTIONS) |
19 | const SORTABLE_ACCOUNTS_BLOCKLIST_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.ACCOUNTS_BLOCKLIST) | 19 | const SORTABLE_ACCOUNTS_BLOCKLIST_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.ACCOUNTS_BLOCKLIST) |
20 | const SORTABLE_SERVERS_BLOCKLIST_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.SERVERS_BLOCKLIST) | 20 | const SORTABLE_SERVERS_BLOCKLIST_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.SERVERS_BLOCKLIST) |
21 | const SORTABLE_USER_NOTIFICATIONS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USER_NOTIFICATIONS) | ||
21 | 22 | ||
22 | const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) | 23 | const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) |
23 | const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS) | 24 | const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS) |
@@ -35,6 +36,7 @@ const followingSortValidator = checkSort(SORTABLE_FOLLOWING_COLUMNS) | |||
35 | const userSubscriptionsSortValidator = checkSort(SORTABLE_USER_SUBSCRIPTIONS_COLUMNS) | 36 | const userSubscriptionsSortValidator = checkSort(SORTABLE_USER_SUBSCRIPTIONS_COLUMNS) |
36 | const accountsBlocklistSortValidator = checkSort(SORTABLE_ACCOUNTS_BLOCKLIST_COLUMNS) | 37 | const accountsBlocklistSortValidator = checkSort(SORTABLE_ACCOUNTS_BLOCKLIST_COLUMNS) |
37 | const serversBlocklistSortValidator = checkSort(SORTABLE_SERVERS_BLOCKLIST_COLUMNS) | 38 | const serversBlocklistSortValidator = checkSort(SORTABLE_SERVERS_BLOCKLIST_COLUMNS) |
39 | const userNotificationsSortValidator = checkSort(SORTABLE_USER_NOTIFICATIONS_COLUMNS) | ||
38 | 40 | ||
39 | // --------------------------------------------------------------------------- | 41 | // --------------------------------------------------------------------------- |
40 | 42 | ||
@@ -54,5 +56,6 @@ export { | |||
54 | userSubscriptionsSortValidator, | 56 | userSubscriptionsSortValidator, |
55 | videoChannelsSearchSortValidator, | 57 | videoChannelsSearchSortValidator, |
56 | accountsBlocklistSortValidator, | 58 | accountsBlocklistSortValidator, |
57 | serversBlocklistSortValidator | 59 | serversBlocklistSortValidator, |
60 | userNotificationsSortValidator | ||
58 | } | 61 | } |
diff --git a/server/middlewares/validators/user-history.ts b/server/middlewares/validators/user-history.ts index 3c8971ea1..418313d09 100644 --- a/server/middlewares/validators/user-history.ts +++ b/server/middlewares/validators/user-history.ts | |||
@@ -1,13 +1,9 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import 'express-validator' | 2 | import 'express-validator' |
3 | import { body, param, query } from 'express-validator/check' | 3 | import { body } from 'express-validator/check' |
4 | import { logger } from '../../helpers/logger' | 4 | import { logger } from '../../helpers/logger' |
5 | import { areValidationErrors } from './utils' | 5 | import { areValidationErrors } from './utils' |
6 | import { ActorFollowModel } from '../../models/activitypub/actor-follow' | 6 | import { isDateValid } from '../../helpers/custom-validators/misc' |
7 | import { areValidActorHandles, isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor' | ||
8 | import { UserModel } from '../../models/account/user' | ||
9 | import { CONFIG } from '../../initializers' | ||
10 | import { isDateValid, toArray } from '../../helpers/custom-validators/misc' | ||
11 | 7 | ||
12 | const userHistoryRemoveValidator = [ | 8 | const userHistoryRemoveValidator = [ |
13 | body('beforeDate') | 9 | body('beforeDate') |
diff --git a/server/middlewares/validators/user-notifications.ts b/server/middlewares/validators/user-notifications.ts new file mode 100644 index 000000000..8202f307e --- /dev/null +++ b/server/middlewares/validators/user-notifications.ts | |||
@@ -0,0 +1,46 @@ | |||
1 | import * as express from 'express' | ||
2 | import 'express-validator' | ||
3 | import { body } from 'express-validator/check' | ||
4 | import { logger } from '../../helpers/logger' | ||
5 | import { areValidationErrors } from './utils' | ||
6 | import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' | ||
7 | import { isIntArray } from '../../helpers/custom-validators/misc' | ||
8 | |||
9 | const updateNotificationSettingsValidator = [ | ||
10 | body('newVideoFromSubscription') | ||
11 | .custom(isUserNotificationSettingValid).withMessage('Should have a valid new video from subscription notification setting'), | ||
12 | body('newCommentOnMyVideo') | ||
13 | .custom(isUserNotificationSettingValid).withMessage('Should have a valid new comment on my video notification setting'), | ||
14 | body('videoAbuseAsModerator') | ||
15 | .custom(isUserNotificationSettingValid).withMessage('Should have a valid new video abuse as moderator notification setting'), | ||
16 | body('blacklistOnMyVideo') | ||
17 | .custom(isUserNotificationSettingValid).withMessage('Should have a valid new blacklist on my video notification setting'), | ||
18 | |||
19 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
20 | logger.debug('Checking updateNotificationSettingsValidator parameters', { parameters: req.body }) | ||
21 | |||
22 | if (areValidationErrors(req, res)) return | ||
23 | |||
24 | return next() | ||
25 | } | ||
26 | ] | ||
27 | |||
28 | const markAsReadUserNotificationsValidator = [ | ||
29 | body('ids') | ||
30 | .custom(isIntArray).withMessage('Should have a valid notification ids to mark as read'), | ||
31 | |||
32 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
33 | logger.debug('Checking markAsReadUserNotificationsValidator parameters', { parameters: req.body }) | ||
34 | |||
35 | if (areValidationErrors(req, res)) return | ||
36 | |||
37 | return next() | ||
38 | } | ||
39 | ] | ||
40 | |||
41 | // --------------------------------------------------------------------------- | ||
42 | |||
43 | export { | ||
44 | updateNotificationSettingsValidator, | ||
45 | markAsReadUserNotificationsValidator | ||
46 | } | ||
diff --git a/server/models/account/user-notification-setting.ts b/server/models/account/user-notification-setting.ts new file mode 100644 index 000000000..bc24b1e33 --- /dev/null +++ b/server/models/account/user-notification-setting.ts | |||
@@ -0,0 +1,100 @@ | |||
1 | import { | ||
2 | AfterDestroy, | ||
3 | AfterUpdate, | ||
4 | AllowNull, | ||
5 | BelongsTo, | ||
6 | Column, | ||
7 | CreatedAt, | ||
8 | Default, | ||
9 | ForeignKey, | ||
10 | Is, | ||
11 | Model, | ||
12 | Table, | ||
13 | UpdatedAt | ||
14 | } from 'sequelize-typescript' | ||
15 | import { throwIfNotValid } from '../utils' | ||
16 | import { UserModel } from './user' | ||
17 | import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' | ||
18 | import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' | ||
19 | import { clearCacheByUserId } from '../../lib/oauth-model' | ||
20 | |||
21 | @Table({ | ||
22 | tableName: 'userNotificationSetting', | ||
23 | indexes: [ | ||
24 | { | ||
25 | fields: [ 'userId' ], | ||
26 | unique: true | ||
27 | } | ||
28 | ] | ||
29 | }) | ||
30 | export class UserNotificationSettingModel extends Model<UserNotificationSettingModel> { | ||
31 | |||
32 | @AllowNull(false) | ||
33 | @Default(null) | ||
34 | @Is( | ||
35 | 'UserNotificationSettingNewVideoFromSubscription', | ||
36 | value => throwIfNotValid(value, isUserNotificationSettingValid, 'newVideoFromSubscription') | ||
37 | ) | ||
38 | @Column | ||
39 | newVideoFromSubscription: UserNotificationSettingValue | ||
40 | |||
41 | @AllowNull(false) | ||
42 | @Default(null) | ||
43 | @Is( | ||
44 | 'UserNotificationSettingNewCommentOnMyVideo', | ||
45 | value => throwIfNotValid(value, isUserNotificationSettingValid, 'newCommentOnMyVideo') | ||
46 | ) | ||
47 | @Column | ||
48 | newCommentOnMyVideo: UserNotificationSettingValue | ||
49 | |||
50 | @AllowNull(false) | ||
51 | @Default(null) | ||
52 | @Is( | ||
53 | 'UserNotificationSettingVideoAbuseAsModerator', | ||
54 | value => throwIfNotValid(value, isUserNotificationSettingValid, 'videoAbuseAsModerator') | ||
55 | ) | ||
56 | @Column | ||
57 | videoAbuseAsModerator: UserNotificationSettingValue | ||
58 | |||
59 | @AllowNull(false) | ||
60 | @Default(null) | ||
61 | @Is( | ||
62 | 'UserNotificationSettingBlacklistOnMyVideo', | ||
63 | value => throwIfNotValid(value, isUserNotificationSettingValid, 'blacklistOnMyVideo') | ||
64 | ) | ||
65 | @Column | ||
66 | blacklistOnMyVideo: UserNotificationSettingValue | ||
67 | |||
68 | @ForeignKey(() => UserModel) | ||
69 | @Column | ||
70 | userId: number | ||
71 | |||
72 | @BelongsTo(() => UserModel, { | ||
73 | foreignKey: { | ||
74 | allowNull: false | ||
75 | }, | ||
76 | onDelete: 'cascade' | ||
77 | }) | ||
78 | User: UserModel | ||
79 | |||
80 | @CreatedAt | ||
81 | createdAt: Date | ||
82 | |||
83 | @UpdatedAt | ||
84 | updatedAt: Date | ||
85 | |||
86 | @AfterUpdate | ||
87 | @AfterDestroy | ||
88 | static removeTokenCache (instance: UserNotificationSettingModel) { | ||
89 | return clearCacheByUserId(instance.userId) | ||
90 | } | ||
91 | |||
92 | toFormattedJSON (): UserNotificationSetting { | ||
93 | return { | ||
94 | newCommentOnMyVideo: this.newCommentOnMyVideo, | ||
95 | newVideoFromSubscription: this.newVideoFromSubscription, | ||
96 | videoAbuseAsModerator: this.videoAbuseAsModerator, | ||
97 | blacklistOnMyVideo: this.blacklistOnMyVideo | ||
98 | } | ||
99 | } | ||
100 | } | ||
diff --git a/server/models/account/user-notification.ts b/server/models/account/user-notification.ts new file mode 100644 index 000000000..e22f0d57f --- /dev/null +++ b/server/models/account/user-notification.ts | |||
@@ -0,0 +1,256 @@ | |||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | ||
2 | import { UserNotification, UserNotificationType } from '../../../shared' | ||
3 | import { getSort, throwIfNotValid } from '../utils' | ||
4 | import { isBooleanValid } from '../../helpers/custom-validators/misc' | ||
5 | import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications' | ||
6 | import { UserModel } from './user' | ||
7 | import { VideoModel } from '../video/video' | ||
8 | import { VideoCommentModel } from '../video/video-comment' | ||
9 | import { Op } from 'sequelize' | ||
10 | import { VideoChannelModel } from '../video/video-channel' | ||
11 | import { AccountModel } from './account' | ||
12 | import { VideoAbuseModel } from '../video/video-abuse' | ||
13 | import { VideoBlacklistModel } from '../video/video-blacklist' | ||
14 | |||
15 | enum ScopeNames { | ||
16 | WITH_ALL = 'WITH_ALL' | ||
17 | } | ||
18 | |||
19 | @Scopes({ | ||
20 | [ScopeNames.WITH_ALL]: { | ||
21 | include: [ | ||
22 | { | ||
23 | attributes: [ 'id', 'uuid', 'name' ], | ||
24 | model: () => VideoModel.unscoped(), | ||
25 | required: false, | ||
26 | include: [ | ||
27 | { | ||
28 | required: true, | ||
29 | attributes: [ 'id', 'name' ], | ||
30 | model: () => VideoChannelModel.unscoped() | ||
31 | } | ||
32 | ] | ||
33 | }, | ||
34 | { | ||
35 | attributes: [ 'id' ], | ||
36 | model: () => VideoCommentModel.unscoped(), | ||
37 | required: false, | ||
38 | include: [ | ||
39 | { | ||
40 | required: true, | ||
41 | attributes: [ 'id', 'name' ], | ||
42 | model: () => AccountModel.unscoped() | ||
43 | }, | ||
44 | { | ||
45 | required: true, | ||
46 | attributes: [ 'id', 'uuid', 'name' ], | ||
47 | model: () => VideoModel.unscoped() | ||
48 | } | ||
49 | ] | ||
50 | }, | ||
51 | { | ||
52 | attributes: [ 'id' ], | ||
53 | model: () => VideoAbuseModel.unscoped(), | ||
54 | required: false, | ||
55 | include: [ | ||
56 | { | ||
57 | required: true, | ||
58 | attributes: [ 'id', 'uuid', 'name' ], | ||
59 | model: () => VideoModel.unscoped() | ||
60 | } | ||
61 | ] | ||
62 | }, | ||
63 | { | ||
64 | attributes: [ 'id' ], | ||
65 | model: () => VideoBlacklistModel.unscoped(), | ||
66 | required: false, | ||
67 | include: [ | ||
68 | { | ||
69 | required: true, | ||
70 | attributes: [ 'id', 'uuid', 'name' ], | ||
71 | model: () => VideoModel.unscoped() | ||
72 | } | ||
73 | ] | ||
74 | } | ||
75 | ] | ||
76 | } | ||
77 | }) | ||
78 | @Table({ | ||
79 | tableName: 'userNotification', | ||
80 | indexes: [ | ||
81 | { | ||
82 | fields: [ 'videoId' ] | ||
83 | }, | ||
84 | { | ||
85 | fields: [ 'commentId' ] | ||
86 | } | ||
87 | ] | ||
88 | }) | ||
89 | export class UserNotificationModel extends Model<UserNotificationModel> { | ||
90 | |||
91 | @AllowNull(false) | ||
92 | @Default(null) | ||
93 | @Is('UserNotificationType', value => throwIfNotValid(value, isUserNotificationTypeValid, 'type')) | ||
94 | @Column | ||
95 | type: UserNotificationType | ||
96 | |||
97 | @AllowNull(false) | ||
98 | @Default(false) | ||
99 | @Is('UserNotificationRead', value => throwIfNotValid(value, isBooleanValid, 'read')) | ||
100 | @Column | ||
101 | read: boolean | ||
102 | |||
103 | @CreatedAt | ||
104 | createdAt: Date | ||
105 | |||
106 | @UpdatedAt | ||
107 | updatedAt: Date | ||
108 | |||
109 | @ForeignKey(() => UserModel) | ||
110 | @Column | ||
111 | userId: number | ||
112 | |||
113 | @BelongsTo(() => UserModel, { | ||
114 | foreignKey: { | ||
115 | allowNull: false | ||
116 | }, | ||
117 | onDelete: 'cascade' | ||
118 | }) | ||
119 | User: UserModel | ||
120 | |||
121 | @ForeignKey(() => VideoModel) | ||
122 | @Column | ||
123 | videoId: number | ||
124 | |||
125 | @BelongsTo(() => VideoModel, { | ||
126 | foreignKey: { | ||
127 | allowNull: true | ||
128 | }, | ||
129 | onDelete: 'cascade' | ||
130 | }) | ||
131 | Video: VideoModel | ||
132 | |||
133 | @ForeignKey(() => VideoCommentModel) | ||
134 | @Column | ||
135 | commentId: number | ||
136 | |||
137 | @BelongsTo(() => VideoCommentModel, { | ||
138 | foreignKey: { | ||
139 | allowNull: true | ||
140 | }, | ||
141 | onDelete: 'cascade' | ||
142 | }) | ||
143 | Comment: VideoCommentModel | ||
144 | |||
145 | @ForeignKey(() => VideoAbuseModel) | ||
146 | @Column | ||
147 | videoAbuseId: number | ||
148 | |||
149 | @BelongsTo(() => VideoAbuseModel, { | ||
150 | foreignKey: { | ||
151 | allowNull: true | ||
152 | }, | ||
153 | onDelete: 'cascade' | ||
154 | }) | ||
155 | VideoAbuse: VideoAbuseModel | ||
156 | |||
157 | @ForeignKey(() => VideoBlacklistModel) | ||
158 | @Column | ||
159 | videoBlacklistId: number | ||
160 | |||
161 | @BelongsTo(() => VideoBlacklistModel, { | ||
162 | foreignKey: { | ||
163 | allowNull: true | ||
164 | }, | ||
165 | onDelete: 'cascade' | ||
166 | }) | ||
167 | VideoBlacklist: VideoBlacklistModel | ||
168 | |||
169 | static listForApi (userId: number, start: number, count: number, sort: string) { | ||
170 | const query = { | ||
171 | offset: start, | ||
172 | limit: count, | ||
173 | order: getSort(sort), | ||
174 | where: { | ||
175 | userId | ||
176 | } | ||
177 | } | ||
178 | |||
179 | return UserNotificationModel.scope(ScopeNames.WITH_ALL) | ||
180 | .findAndCountAll(query) | ||
181 | .then(({ rows, count }) => { | ||
182 | return { | ||
183 | data: rows, | ||
184 | total: count | ||
185 | } | ||
186 | }) | ||
187 | } | ||
188 | |||
189 | static markAsRead (userId: number, notificationIds: number[]) { | ||
190 | const query = { | ||
191 | where: { | ||
192 | userId, | ||
193 | id: { | ||
194 | [Op.any]: notificationIds | ||
195 | } | ||
196 | } | ||
197 | } | ||
198 | |||
199 | return UserNotificationModel.update({ read: true }, query) | ||
200 | } | ||
201 | |||
202 | toFormattedJSON (): UserNotification { | ||
203 | const video = this.Video ? { | ||
204 | id: this.Video.id, | ||
205 | uuid: this.Video.uuid, | ||
206 | name: this.Video.name, | ||
207 | channel: { | ||
208 | id: this.Video.VideoChannel.id, | ||
209 | displayName: this.Video.VideoChannel.getDisplayName() | ||
210 | } | ||
211 | } : undefined | ||
212 | |||
213 | const comment = this.Comment ? { | ||
214 | id: this.Comment.id, | ||
215 | account: { | ||
216 | id: this.Comment.Account.id, | ||
217 | displayName: this.Comment.Account.getDisplayName() | ||
218 | }, | ||
219 | video: { | ||
220 | id: this.Comment.Video.id, | ||
221 | uuid: this.Comment.Video.uuid, | ||
222 | name: this.Comment.Video.name | ||
223 | } | ||
224 | } : undefined | ||
225 | |||
226 | const videoAbuse = this.VideoAbuse ? { | ||
227 | id: this.VideoAbuse.id, | ||
228 | video: { | ||
229 | id: this.VideoAbuse.Video.id, | ||
230 | uuid: this.VideoAbuse.Video.uuid, | ||
231 | name: this.VideoAbuse.Video.name | ||
232 | } | ||
233 | } : undefined | ||
234 | |||
235 | const videoBlacklist = this.VideoBlacklist ? { | ||
236 | id: this.VideoBlacklist.id, | ||
237 | video: { | ||
238 | id: this.VideoBlacklist.Video.id, | ||
239 | uuid: this.VideoBlacklist.Video.uuid, | ||
240 | name: this.VideoBlacklist.Video.name | ||
241 | } | ||
242 | } : undefined | ||
243 | |||
244 | return { | ||
245 | id: this.id, | ||
246 | type: this.type, | ||
247 | read: this.read, | ||
248 | video, | ||
249 | comment, | ||
250 | videoAbuse, | ||
251 | videoBlacklist, | ||
252 | createdAt: this.createdAt.toISOString(), | ||
253 | updatedAt: this.updatedAt.toISOString() | ||
254 | } | ||
255 | } | ||
256 | } | ||
diff --git a/server/models/account/user.ts b/server/models/account/user.ts index 180ced810..55ec14d05 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts | |||
@@ -32,8 +32,8 @@ import { | |||
32 | isUserUsernameValid, | 32 | isUserUsernameValid, |
33 | isUserVideoQuotaDailyValid, | 33 | isUserVideoQuotaDailyValid, |
34 | isUserVideoQuotaValid, | 34 | isUserVideoQuotaValid, |
35 | isUserWebTorrentEnabledValid, | 35 | isUserVideosHistoryEnabledValid, |
36 | isUserVideosHistoryEnabledValid | 36 | isUserWebTorrentEnabledValid |
37 | } from '../../helpers/custom-validators/users' | 37 | } from '../../helpers/custom-validators/users' |
38 | import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' | 38 | import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto' |
39 | import { OAuthTokenModel } from '../oauth/oauth-token' | 39 | import { OAuthTokenModel } from '../oauth/oauth-token' |
@@ -44,6 +44,10 @@ import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type' | |||
44 | import { values } from 'lodash' | 44 | import { values } from 'lodash' |
45 | import { NSFW_POLICY_TYPES } from '../../initializers' | 45 | import { NSFW_POLICY_TYPES } from '../../initializers' |
46 | import { clearCacheByUserId } from '../../lib/oauth-model' | 46 | import { clearCacheByUserId } from '../../lib/oauth-model' |
47 | import { UserNotificationSettingModel } from './user-notification-setting' | ||
48 | import { VideoModel } from '../video/video' | ||
49 | import { ActorModel } from '../activitypub/actor' | ||
50 | import { ActorFollowModel } from '../activitypub/actor-follow' | ||
47 | 51 | ||
48 | enum ScopeNames { | 52 | enum ScopeNames { |
49 | WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL' | 53 | WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL' |
@@ -54,6 +58,10 @@ enum ScopeNames { | |||
54 | { | 58 | { |
55 | model: () => AccountModel, | 59 | model: () => AccountModel, |
56 | required: true | 60 | required: true |
61 | }, | ||
62 | { | ||
63 | model: () => UserNotificationSettingModel, | ||
64 | required: true | ||
57 | } | 65 | } |
58 | ] | 66 | ] |
59 | }) | 67 | }) |
@@ -64,6 +72,10 @@ enum ScopeNames { | |||
64 | model: () => AccountModel, | 72 | model: () => AccountModel, |
65 | required: true, | 73 | required: true, |
66 | include: [ () => VideoChannelModel ] | 74 | include: [ () => VideoChannelModel ] |
75 | }, | ||
76 | { | ||
77 | model: () => UserNotificationSettingModel, | ||
78 | required: true | ||
67 | } | 79 | } |
68 | ] | 80 | ] |
69 | } | 81 | } |
@@ -167,6 +179,13 @@ export class UserModel extends Model<UserModel> { | |||
167 | }) | 179 | }) |
168 | Account: AccountModel | 180 | Account: AccountModel |
169 | 181 | ||
182 | @HasOne(() => UserNotificationSettingModel, { | ||
183 | foreignKey: 'userId', | ||
184 | onDelete: 'cascade', | ||
185 | hooks: true | ||
186 | }) | ||
187 | NotificationSetting: UserNotificationSettingModel | ||
188 | |||
170 | @HasMany(() => OAuthTokenModel, { | 189 | @HasMany(() => OAuthTokenModel, { |
171 | foreignKey: 'userId', | 190 | foreignKey: 'userId', |
172 | onDelete: 'cascade' | 191 | onDelete: 'cascade' |
@@ -249,13 +268,12 @@ export class UserModel extends Model<UserModel> { | |||
249 | }) | 268 | }) |
250 | } | 269 | } |
251 | 270 | ||
252 | static listEmailsWithRight (right: UserRight) { | 271 | static listWithRight (right: UserRight) { |
253 | const roles = Object.keys(USER_ROLE_LABELS) | 272 | const roles = Object.keys(USER_ROLE_LABELS) |
254 | .map(k => parseInt(k, 10) as UserRole) | 273 | .map(k => parseInt(k, 10) as UserRole) |
255 | .filter(role => hasUserRight(role, right)) | 274 | .filter(role => hasUserRight(role, right)) |
256 | 275 | ||
257 | const query = { | 276 | const query = { |
258 | attribute: [ 'email' ], | ||
259 | where: { | 277 | where: { |
260 | role: { | 278 | role: { |
261 | [Sequelize.Op.in]: roles | 279 | [Sequelize.Op.in]: roles |
@@ -263,9 +281,46 @@ export class UserModel extends Model<UserModel> { | |||
263 | } | 281 | } |
264 | } | 282 | } |
265 | 283 | ||
266 | return UserModel.unscoped() | 284 | return UserModel.findAll(query) |
267 | .findAll(query) | 285 | } |
268 | .then(u => u.map(u => u.email)) | 286 | |
287 | static listUserSubscribersOf (actorId: number) { | ||
288 | const query = { | ||
289 | include: [ | ||
290 | { | ||
291 | model: UserNotificationSettingModel.unscoped(), | ||
292 | required: true | ||
293 | }, | ||
294 | { | ||
295 | attributes: [ 'userId' ], | ||
296 | model: AccountModel.unscoped(), | ||
297 | required: true, | ||
298 | include: [ | ||
299 | { | ||
300 | attributes: [ ], | ||
301 | model: ActorModel.unscoped(), | ||
302 | required: true, | ||
303 | where: { | ||
304 | serverId: null | ||
305 | }, | ||
306 | include: [ | ||
307 | { | ||
308 | attributes: [ ], | ||
309 | as: 'ActorFollowings', | ||
310 | model: ActorFollowModel.unscoped(), | ||
311 | required: true, | ||
312 | where: { | ||
313 | targetActorId: actorId | ||
314 | } | ||
315 | } | ||
316 | ] | ||
317 | } | ||
318 | ] | ||
319 | } | ||
320 | ] | ||
321 | } | ||
322 | |||
323 | return UserModel.unscoped().findAll(query) | ||
269 | } | 324 | } |
270 | 325 | ||
271 | static loadById (id: number) { | 326 | static loadById (id: number) { |
@@ -314,6 +369,37 @@ export class UserModel extends Model<UserModel> { | |||
314 | return UserModel.findOne(query) | 369 | return UserModel.findOne(query) |
315 | } | 370 | } |
316 | 371 | ||
372 | static loadByVideoId (videoId: number) { | ||
373 | const query = { | ||
374 | include: [ | ||
375 | { | ||
376 | required: true, | ||
377 | attributes: [ 'id' ], | ||
378 | model: AccountModel.unscoped(), | ||
379 | include: [ | ||
380 | { | ||
381 | required: true, | ||
382 | attributes: [ 'id' ], | ||
383 | model: VideoChannelModel.unscoped(), | ||
384 | include: [ | ||
385 | { | ||
386 | required: true, | ||
387 | attributes: [ 'id' ], | ||
388 | model: VideoModel.unscoped(), | ||
389 | where: { | ||
390 | id: videoId | ||
391 | } | ||
392 | } | ||
393 | ] | ||
394 | } | ||
395 | ] | ||
396 | } | ||
397 | ] | ||
398 | } | ||
399 | |||
400 | return UserModel.findOne(query) | ||
401 | } | ||
402 | |||
317 | static getOriginalVideoFileTotalFromUser (user: UserModel) { | 403 | static getOriginalVideoFileTotalFromUser (user: UserModel) { |
318 | // Don't use sequelize because we need to use a sub query | 404 | // Don't use sequelize because we need to use a sub query |
319 | const query = UserModel.generateUserQuotaBaseSQL() | 405 | const query = UserModel.generateUserQuotaBaseSQL() |
@@ -380,6 +466,7 @@ export class UserModel extends Model<UserModel> { | |||
380 | blocked: this.blocked, | 466 | blocked: this.blocked, |
381 | blockedReason: this.blockedReason, | 467 | blockedReason: this.blockedReason, |
382 | account: this.Account.toFormattedJSON(), | 468 | account: this.Account.toFormattedJSON(), |
469 | notificationSettings: this.NotificationSetting ? this.NotificationSetting.toFormattedJSON() : undefined, | ||
383 | videoChannels: [], | 470 | videoChannels: [], |
384 | videoQuotaUsed: videoQuotaUsed !== undefined | 471 | videoQuotaUsed: videoQuotaUsed !== undefined |
385 | ? parseInt(videoQuotaUsed, 10) | 472 | ? parseInt(videoQuotaUsed, 10) |
diff --git a/server/models/activitypub/actor-follow.ts b/server/models/activitypub/actor-follow.ts index 994f791de..796e07a42 100644 --- a/server/models/activitypub/actor-follow.ts +++ b/server/models/activitypub/actor-follow.ts | |||
@@ -307,7 +307,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
307 | }) | 307 | }) |
308 | } | 308 | } |
309 | 309 | ||
310 | static listFollowersForApi (id: number, start: number, count: number, sort: string, search?: string) { | 310 | static listFollowersForApi (actorId: number, start: number, count: number, sort: string, search?: string) { |
311 | const query = { | 311 | const query = { |
312 | distinct: true, | 312 | distinct: true, |
313 | offset: start, | 313 | offset: start, |
@@ -335,7 +335,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
335 | as: 'ActorFollowing', | 335 | as: 'ActorFollowing', |
336 | required: true, | 336 | required: true, |
337 | where: { | 337 | where: { |
338 | id | 338 | id: actorId |
339 | } | 339 | } |
340 | } | 340 | } |
341 | ] | 341 | ] |
@@ -350,7 +350,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
350 | }) | 350 | }) |
351 | } | 351 | } |
352 | 352 | ||
353 | static listSubscriptionsForApi (id: number, start: number, count: number, sort: string) { | 353 | static listSubscriptionsForApi (actorId: number, start: number, count: number, sort: string) { |
354 | const query = { | 354 | const query = { |
355 | attributes: [], | 355 | attributes: [], |
356 | distinct: true, | 356 | distinct: true, |
@@ -358,7 +358,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
358 | limit: count, | 358 | limit: count, |
359 | order: getSort(sort), | 359 | order: getSort(sort), |
360 | where: { | 360 | where: { |
361 | actorId: id | 361 | actorId: actorId |
362 | }, | 362 | }, |
363 | include: [ | 363 | include: [ |
364 | { | 364 | { |
@@ -451,9 +451,9 @@ export class ActorFollowModel extends Model<ActorFollowModel> { | |||
451 | static updateFollowScore (inboxUrl: string, value: number, t?: Sequelize.Transaction) { | 451 | static updateFollowScore (inboxUrl: string, value: number, t?: Sequelize.Transaction) { |
452 | const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` + | 452 | const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` + |
453 | 'WHERE id IN (' + | 453 | 'WHERE id IN (' + |
454 | 'SELECT "actorFollow"."id" FROM "actorFollow" ' + | 454 | 'SELECT "actorFollow"."id" FROM "actorFollow" ' + |
455 | 'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' + | 455 | 'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' + |
456 | `WHERE "actor"."inboxUrl" = '${inboxUrl}' OR "actor"."sharedInboxUrl" = '${inboxUrl}'` + | 456 | `WHERE "actor"."inboxUrl" = '${inboxUrl}' OR "actor"."sharedInboxUrl" = '${inboxUrl}'` + |
457 | ')' | 457 | ')' |
458 | 458 | ||
459 | const options = { | 459 | const options = { |
diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts index 12b83916e..dda57a8ba 100644 --- a/server/models/activitypub/actor.ts +++ b/server/models/activitypub/actor.ts | |||
@@ -219,6 +219,7 @@ export class ActorModel extends Model<ActorModel> { | |||
219 | name: 'actorId', | 219 | name: 'actorId', |
220 | allowNull: false | 220 | allowNull: false |
221 | }, | 221 | }, |
222 | as: 'ActorFollowings', | ||
222 | onDelete: 'cascade' | 223 | onDelete: 'cascade' |
223 | }) | 224 | }) |
224 | ActorFollowing: ActorFollowModel[] | 225 | ActorFollowing: ActorFollowModel[] |
diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts index dbb88ca45..4c9e2d05e 100644 --- a/server/models/video/video-abuse.ts +++ b/server/models/video/video-abuse.ts | |||
@@ -86,11 +86,6 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> { | |||
86 | }) | 86 | }) |
87 | Video: VideoModel | 87 | Video: VideoModel |
88 | 88 | ||
89 | @AfterCreate | ||
90 | static sendEmailNotification (instance: VideoAbuseModel) { | ||
91 | return Emailer.Instance.addVideoAbuseReportJob(instance.videoId) | ||
92 | } | ||
93 | |||
94 | static loadByIdAndVideoId (id: number, videoId: number) { | 89 | static loadByIdAndVideoId (id: number, videoId: number) { |
95 | const query = { | 90 | const query = { |
96 | where: { | 91 | where: { |
diff --git a/server/models/video/video-blacklist.ts b/server/models/video/video-blacklist.ts index 67f7cd487..23e992685 100644 --- a/server/models/video/video-blacklist.ts +++ b/server/models/video/video-blacklist.ts | |||
@@ -53,16 +53,6 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> { | |||
53 | }) | 53 | }) |
54 | Video: VideoModel | 54 | Video: VideoModel |
55 | 55 | ||
56 | @AfterCreate | ||
57 | static sendBlacklistEmailNotification (instance: VideoBlacklistModel) { | ||
58 | return Emailer.Instance.addVideoBlacklistReportJob(instance.videoId, instance.reason) | ||
59 | } | ||
60 | |||
61 | @AfterDestroy | ||
62 | static sendUnblacklistEmailNotification (instance: VideoBlacklistModel) { | ||
63 | return Emailer.Instance.addVideoUnblacklistReportJob(instance.videoId) | ||
64 | } | ||
65 | |||
66 | static listForApi (start: number, count: number, sort: SortType) { | 56 | static listForApi (start: number, count: number, sort: SortType) { |
67 | const query = { | 57 | const query = { |
68 | offset: start, | 58 | offset: start, |
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts index dd6d08139..d8fc2a564 100644 --- a/server/models/video/video-comment.ts +++ b/server/models/video/video-comment.ts | |||
@@ -448,6 +448,10 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
448 | } | 448 | } |
449 | } | 449 | } |
450 | 450 | ||
451 | getCommentStaticPath () { | ||
452 | return this.Video.getWatchStaticPath() + ';threadId=' + this.getThreadId() | ||
453 | } | ||
454 | |||
451 | getThreadId (): number { | 455 | getThreadId (): number { |
452 | return this.originCommentId || this.id | 456 | return this.originCommentId || this.id |
453 | } | 457 | } |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index bcf327f32..fc200e5d1 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -1527,6 +1527,10 @@ export class VideoModel extends Model<VideoModel> { | |||
1527 | videoFile.infoHash = parsedTorrent.infoHash | 1527 | videoFile.infoHash = parsedTorrent.infoHash |
1528 | } | 1528 | } |
1529 | 1529 | ||
1530 | getWatchStaticPath () { | ||
1531 | return '/videos/watch/' + this.uuid | ||
1532 | } | ||
1533 | |||
1530 | getEmbedStaticPath () { | 1534 | getEmbedStaticPath () { |
1531 | return '/videos/embed/' + this.uuid | 1535 | return '/videos/embed/' + this.uuid |
1532 | } | 1536 | } |
diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts index 877ceb0a7..7a181d1d6 100644 --- a/server/tests/api/check-params/index.ts +++ b/server/tests/api/check-params/index.ts | |||
@@ -7,6 +7,7 @@ import './jobs' | |||
7 | import './redundancy' | 7 | import './redundancy' |
8 | import './search' | 8 | import './search' |
9 | import './services' | 9 | import './services' |
10 | import './user-notifications' | ||
10 | import './user-subscriptions' | 11 | import './user-subscriptions' |
11 | import './users' | 12 | import './users' |
12 | import './video-abuses' | 13 | import './video-abuses' |
diff --git a/server/tests/api/check-params/user-notifications.ts b/server/tests/api/check-params/user-notifications.ts new file mode 100644 index 000000000..3ae36ddb3 --- /dev/null +++ b/server/tests/api/check-params/user-notifications.ts | |||
@@ -0,0 +1,249 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import 'mocha' | ||
4 | import * as io from 'socket.io-client' | ||
5 | |||
6 | import { | ||
7 | flushTests, | ||
8 | immutableAssign, | ||
9 | killallServers, | ||
10 | makeGetRequest, | ||
11 | makePostBodyRequest, | ||
12 | makePutBodyRequest, | ||
13 | runServer, | ||
14 | ServerInfo, | ||
15 | setAccessTokensToServers, | ||
16 | wait | ||
17 | } from '../../../../shared/utils' | ||
18 | import { | ||
19 | checkBadCountPagination, | ||
20 | checkBadSortPagination, | ||
21 | checkBadStartPagination | ||
22 | } from '../../../../shared/utils/requests/check-api-params' | ||
23 | import { UserNotificationSetting, UserNotificationSettingValue } from '../../../../shared/models/users' | ||
24 | |||
25 | describe('Test user notifications API validators', function () { | ||
26 | let server: ServerInfo | ||
27 | |||
28 | // --------------------------------------------------------------- | ||
29 | |||
30 | before(async function () { | ||
31 | this.timeout(30000) | ||
32 | |||
33 | await flushTests() | ||
34 | |||
35 | server = await runServer(1) | ||
36 | |||
37 | await setAccessTokensToServers([ server ]) | ||
38 | }) | ||
39 | |||
40 | describe('When listing my notifications', function () { | ||
41 | const path = '/api/v1/users/me/notifications' | ||
42 | |||
43 | it('Should fail with a bad start pagination', async function () { | ||
44 | await checkBadStartPagination(server.url, path, server.accessToken) | ||
45 | }) | ||
46 | |||
47 | it('Should fail with a bad count pagination', async function () { | ||
48 | await checkBadCountPagination(server.url, path, server.accessToken) | ||
49 | }) | ||
50 | |||
51 | it('Should fail with an incorrect sort', async function () { | ||
52 | await checkBadSortPagination(server.url, path, server.accessToken) | ||
53 | }) | ||
54 | |||
55 | it('Should fail with a non authenticated user', async function () { | ||
56 | await makeGetRequest({ | ||
57 | url: server.url, | ||
58 | path, | ||
59 | statusCodeExpected: 401 | ||
60 | }) | ||
61 | }) | ||
62 | |||
63 | it('Should succeed with the correct parameters', async function () { | ||
64 | await makeGetRequest({ | ||
65 | url: server.url, | ||
66 | path, | ||
67 | token: server.accessToken, | ||
68 | statusCodeExpected: 200 | ||
69 | }) | ||
70 | }) | ||
71 | }) | ||
72 | |||
73 | describe('When marking as read my notifications', function () { | ||
74 | const path = '/api/v1/users/me/notifications/read' | ||
75 | |||
76 | it('Should fail with wrong ids parameters', async function () { | ||
77 | await makePostBodyRequest({ | ||
78 | url: server.url, | ||
79 | path, | ||
80 | fields: { | ||
81 | ids: [ 'hello' ] | ||
82 | }, | ||
83 | token: server.accessToken, | ||
84 | statusCodeExpected: 400 | ||
85 | }) | ||
86 | |||
87 | await makePostBodyRequest({ | ||
88 | url: server.url, | ||
89 | path, | ||
90 | fields: { | ||
91 | ids: 5 | ||
92 | }, | ||
93 | token: server.accessToken, | ||
94 | statusCodeExpected: 400 | ||
95 | }) | ||
96 | }) | ||
97 | |||
98 | it('Should fail with a non authenticated user', async function () { | ||
99 | await makePostBodyRequest({ | ||
100 | url: server.url, | ||
101 | path, | ||
102 | fields: { | ||
103 | ids: [ 5 ] | ||
104 | }, | ||
105 | statusCodeExpected: 401 | ||
106 | }) | ||
107 | }) | ||
108 | |||
109 | it('Should succeed with the correct parameters', async function () { | ||
110 | await makePostBodyRequest({ | ||
111 | url: server.url, | ||
112 | path, | ||
113 | fields: { | ||
114 | ids: [ 5 ] | ||
115 | }, | ||
116 | token: server.accessToken, | ||
117 | statusCodeExpected: 204 | ||
118 | }) | ||
119 | }) | ||
120 | }) | ||
121 | |||
122 | describe('When updating my notification settings', function () { | ||
123 | const path = '/api/v1/users/me/notification-settings' | ||
124 | const correctFields: UserNotificationSetting = { | ||
125 | newVideoFromSubscription: UserNotificationSettingValue.WEB_NOTIFICATION, | ||
126 | newCommentOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION, | ||
127 | videoAbuseAsModerator: UserNotificationSettingValue.WEB_NOTIFICATION, | ||
128 | blacklistOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION | ||
129 | } | ||
130 | |||
131 | it('Should fail with missing fields', async function () { | ||
132 | await makePutBodyRequest({ | ||
133 | url: server.url, | ||
134 | path, | ||
135 | token: server.accessToken, | ||
136 | fields: { newVideoFromSubscription: UserNotificationSettingValue.WEB_NOTIFICATION }, | ||
137 | statusCodeExpected: 400 | ||
138 | }) | ||
139 | }) | ||
140 | |||
141 | it('Should fail with incorrect field values', async function () { | ||
142 | { | ||
143 | const fields = immutableAssign(correctFields, { newCommentOnMyVideo: 15 }) | ||
144 | |||
145 | await makePutBodyRequest({ | ||
146 | url: server.url, | ||
147 | path, | ||
148 | token: server.accessToken, | ||
149 | fields, | ||
150 | statusCodeExpected: 400 | ||
151 | }) | ||
152 | } | ||
153 | |||
154 | { | ||
155 | const fields = immutableAssign(correctFields, { newCommentOnMyVideo: 'toto' }) | ||
156 | |||
157 | await makePutBodyRequest({ | ||
158 | url: server.url, | ||
159 | path, | ||
160 | fields, | ||
161 | token: server.accessToken, | ||
162 | statusCodeExpected: 400 | ||
163 | }) | ||
164 | } | ||
165 | }) | ||
166 | |||
167 | it('Should fail with a non authenticated user', async function () { | ||
168 | await makePutBodyRequest({ | ||
169 | url: server.url, | ||
170 | path, | ||
171 | fields: correctFields, | ||
172 | statusCodeExpected: 401 | ||
173 | }) | ||
174 | }) | ||
175 | |||
176 | it('Should succeed with the correct parameters', async function () { | ||
177 | await makePutBodyRequest({ | ||
178 | url: server.url, | ||
179 | path, | ||
180 | token: server.accessToken, | ||
181 | fields: correctFields, | ||
182 | statusCodeExpected: 204 | ||
183 | }) | ||
184 | }) | ||
185 | }) | ||
186 | |||
187 | describe('When connecting to my notification socket', function () { | ||
188 | it('Should fail with no token', function (next) { | ||
189 | const socket = io('http://localhost:9001/user-notifications', { reconnection: false }) | ||
190 | |||
191 | socket.on('error', () => { | ||
192 | socket.removeListener('error', this) | ||
193 | socket.disconnect() | ||
194 | next() | ||
195 | }) | ||
196 | |||
197 | socket.on('connect', () => { | ||
198 | socket.disconnect() | ||
199 | next(new Error('Connected with a missing token.')) | ||
200 | }) | ||
201 | }) | ||
202 | |||
203 | it('Should fail with an invalid token', function (next) { | ||
204 | const socket = io('http://localhost:9001/user-notifications', { | ||
205 | query: { accessToken: 'bad_access_token' }, | ||
206 | reconnection: false | ||
207 | }) | ||
208 | |||
209 | socket.on('error', () => { | ||
210 | socket.removeListener('error', this) | ||
211 | socket.disconnect() | ||
212 | next() | ||
213 | }) | ||
214 | |||
215 | socket.on('connect', () => { | ||
216 | socket.disconnect() | ||
217 | next(new Error('Connected with an invalid token.')) | ||
218 | }) | ||
219 | }) | ||
220 | |||
221 | it('Should success with the correct token', function (next) { | ||
222 | const socket = io('http://localhost:9001/user-notifications', { | ||
223 | query: { accessToken: server.accessToken }, | ||
224 | reconnection: false | ||
225 | }) | ||
226 | |||
227 | const errorListener = socket.on('error', err => { | ||
228 | next(new Error('Error in connection: ' + err)) | ||
229 | }) | ||
230 | |||
231 | socket.on('connect', async () => { | ||
232 | socket.removeListener('error', errorListener) | ||
233 | socket.disconnect() | ||
234 | |||
235 | await wait(500) | ||
236 | next() | ||
237 | }) | ||
238 | }) | ||
239 | }) | ||
240 | |||
241 | after(async function () { | ||
242 | killallServers([ server ]) | ||
243 | |||
244 | // Keep the logs if the test failed | ||
245 | if (this['ok']) { | ||
246 | await flushTests() | ||
247 | } | ||
248 | }) | ||
249 | }) | ||
diff --git a/server/tests/api/users/index.ts b/server/tests/api/users/index.ts index ff433315d..63e6e827a 100644 --- a/server/tests/api/users/index.ts +++ b/server/tests/api/users/index.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import './blocklist' | 1 | import './blocklist' |
2 | import './user-subscriptions' | 2 | import './user-subscriptions' |
3 | import './user-notifications' | ||
3 | import './users' | 4 | import './users' |
4 | import './users-multiple-servers' | 5 | import './users-multiple-servers' |
5 | import './users-verification' | 6 | import './users-verification' |
diff --git a/server/tests/api/users/user-notifications.ts b/server/tests/api/users/user-notifications.ts new file mode 100644 index 000000000..ea35e6390 --- /dev/null +++ b/server/tests/api/users/user-notifications.ts | |||
@@ -0,0 +1,628 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | ||
5 | import { | ||
6 | addVideoToBlacklist, | ||
7 | createUser, | ||
8 | doubleFollow, | ||
9 | flushAndRunMultipleServers, | ||
10 | flushTests, | ||
11 | getMyUserInformation, | ||
12 | immutableAssign, | ||
13 | removeVideoFromBlacklist, | ||
14 | reportVideoAbuse, | ||
15 | updateVideo, | ||
16 | userLogin, | ||
17 | wait | ||
18 | } from '../../../../shared/utils' | ||
19 | import { killallServers, ServerInfo, uploadVideo } from '../../../../shared/utils/index' | ||
20 | import { setAccessTokensToServers } from '../../../../shared/utils/users/login' | ||
21 | import { waitJobs } from '../../../../shared/utils/server/jobs' | ||
22 | import { getUserNotificationSocket } from '../../../../shared/utils/socket/socket-io' | ||
23 | import { | ||
24 | CheckerBaseParams, | ||
25 | checkNewBlacklistOnMyVideo, | ||
26 | checkNewCommentOnMyVideo, | ||
27 | checkNewVideoAbuseForModerators, | ||
28 | checkNewVideoFromSubscription, | ||
29 | getLastNotification, | ||
30 | getUserNotifications, | ||
31 | markAsReadNotifications, | ||
32 | updateMyNotificationSettings | ||
33 | } from '../../../../shared/utils/users/user-notifications' | ||
34 | import { User, UserNotification, UserNotificationSettingValue } from '../../../../shared/models/users' | ||
35 | import { MockSmtpServer } from '../../../../shared/utils/miscs/email' | ||
36 | import { addUserSubscription } from '../../../../shared/utils/users/user-subscriptions' | ||
37 | import { VideoPrivacy } from '../../../../shared/models/videos' | ||
38 | import { getYoutubeVideoUrl, importVideo } from '../../../../shared/utils/videos/video-imports' | ||
39 | import { addVideoCommentReply, addVideoCommentThread } from '../../../../shared/utils/videos/video-comments' | ||
40 | |||
41 | const expect = chai.expect | ||
42 | |||
43 | async function uploadVideoByRemoteAccount (servers: ServerInfo[], videoNameId: number, additionalParams: any = {}) { | ||
44 | const data = Object.assign({ name: 'remote video ' + videoNameId }, additionalParams) | ||
45 | const res = await uploadVideo(servers[ 1 ].url, servers[ 1 ].accessToken, data) | ||
46 | |||
47 | await waitJobs(servers) | ||
48 | |||
49 | return res.body.video.uuid | ||
50 | } | ||
51 | |||
52 | async function uploadVideoByLocalAccount (servers: ServerInfo[], videoNameId: number, additionalParams: any = {}) { | ||
53 | const data = Object.assign({ name: 'local video ' + videoNameId }, additionalParams) | ||
54 | const res = await uploadVideo(servers[ 0 ].url, servers[ 0 ].accessToken, data) | ||
55 | |||
56 | await waitJobs(servers) | ||
57 | |||
58 | return res.body.video.uuid | ||
59 | } | ||
60 | |||
61 | describe('Test users notifications', function () { | ||
62 | let servers: ServerInfo[] = [] | ||
63 | let userAccessToken: string | ||
64 | let userNotifications: UserNotification[] = [] | ||
65 | let adminNotifications: UserNotification[] = [] | ||
66 | const emails: object[] = [] | ||
67 | |||
68 | before(async function () { | ||
69 | this.timeout(120000) | ||
70 | |||
71 | await MockSmtpServer.Instance.collectEmails(emails) | ||
72 | |||
73 | await flushTests() | ||
74 | |||
75 | const overrideConfig = { | ||
76 | smtp: { | ||
77 | hostname: 'localhost' | ||
78 | } | ||
79 | } | ||
80 | servers = await flushAndRunMultipleServers(2, overrideConfig) | ||
81 | |||
82 | // Get the access tokens | ||
83 | await setAccessTokensToServers(servers) | ||
84 | |||
85 | // Server 1 and server 2 follow each other | ||
86 | await doubleFollow(servers[0], servers[1]) | ||
87 | |||
88 | await waitJobs(servers) | ||
89 | |||
90 | const user = { | ||
91 | username: 'user_1', | ||
92 | password: 'super password' | ||
93 | } | ||
94 | await createUser(servers[0].url, servers[0].accessToken, user.username, user.password, 10 * 1000 * 1000) | ||
95 | userAccessToken = await userLogin(servers[0], user) | ||
96 | |||
97 | await updateMyNotificationSettings(servers[0].url, userAccessToken, { | ||
98 | newCommentOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL, | ||
99 | newVideoFromSubscription: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL, | ||
100 | blacklistOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL, | ||
101 | videoAbuseAsModerator: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL | ||
102 | }) | ||
103 | |||
104 | { | ||
105 | const socket = getUserNotificationSocket(servers[ 0 ].url, userAccessToken) | ||
106 | socket.on('new-notification', n => userNotifications.push(n)) | ||
107 | } | ||
108 | { | ||
109 | const socket = getUserNotificationSocket(servers[ 0 ].url, servers[0].accessToken) | ||
110 | socket.on('new-notification', n => adminNotifications.push(n)) | ||
111 | } | ||
112 | }) | ||
113 | |||
114 | describe('New video from my subscription notification', function () { | ||
115 | let baseParams: CheckerBaseParams | ||
116 | |||
117 | before(() => { | ||
118 | baseParams = { | ||
119 | server: servers[0], | ||
120 | emails, | ||
121 | socketNotifications: userNotifications, | ||
122 | token: userAccessToken | ||
123 | } | ||
124 | }) | ||
125 | |||
126 | it('Should not send notifications if the user does not follow the video publisher', async function () { | ||
127 | await uploadVideoByLocalAccount(servers, 1) | ||
128 | |||
129 | const notification = await getLastNotification(servers[ 0 ].url, userAccessToken) | ||
130 | expect(notification).to.be.undefined | ||
131 | |||
132 | expect(emails).to.have.lengthOf(0) | ||
133 | expect(userNotifications).to.have.lengthOf(0) | ||
134 | }) | ||
135 | |||
136 | it('Should send a new video notification if the user follows the local video publisher', async function () { | ||
137 | await addUserSubscription(servers[0].url, userAccessToken, 'root_channel@localhost:9001') | ||
138 | |||
139 | const videoNameId = 10 | ||
140 | const videoName = 'local video ' + videoNameId | ||
141 | |||
142 | const uuid = await uploadVideoByLocalAccount(servers, videoNameId) | ||
143 | await checkNewVideoFromSubscription(baseParams, videoName, uuid, 'presence') | ||
144 | }) | ||
145 | |||
146 | it('Should send a new video notification from a remote account', async function () { | ||
147 | this.timeout(50000) // Server 2 has transcoding enabled | ||
148 | |||
149 | await addUserSubscription(servers[0].url, userAccessToken, 'root_channel@localhost:9002') | ||
150 | |||
151 | const videoNameId = 20 | ||
152 | const videoName = 'remote video ' + videoNameId | ||
153 | |||
154 | const uuid = await uploadVideoByRemoteAccount(servers, videoNameId) | ||
155 | await checkNewVideoFromSubscription(baseParams, videoName, uuid, 'presence') | ||
156 | }) | ||
157 | |||
158 | it('Should send a new video notification on a scheduled publication', async function () { | ||
159 | this.timeout(20000) | ||
160 | |||
161 | const videoNameId = 30 | ||
162 | const videoName = 'local video ' + videoNameId | ||
163 | |||
164 | // In 2 seconds | ||
165 | let updateAt = new Date(new Date().getTime() + 2000) | ||
166 | |||
167 | const data = { | ||
168 | privacy: VideoPrivacy.PRIVATE, | ||
169 | scheduleUpdate: { | ||
170 | updateAt: updateAt.toISOString(), | ||
171 | privacy: VideoPrivacy.PUBLIC | ||
172 | } | ||
173 | } | ||
174 | const uuid = await uploadVideoByLocalAccount(servers, videoNameId, data) | ||
175 | |||
176 | await wait(6000) | ||
177 | await checkNewVideoFromSubscription(baseParams, videoName, uuid, 'presence') | ||
178 | }) | ||
179 | |||
180 | it('Should send a new video notification on a remote scheduled publication', async function () { | ||
181 | this.timeout(20000) | ||
182 | |||
183 | const videoNameId = 40 | ||
184 | const videoName = 'remote video ' + videoNameId | ||
185 | |||
186 | // In 2 seconds | ||
187 | let updateAt = new Date(new Date().getTime() + 2000) | ||
188 | |||
189 | const data = { | ||
190 | privacy: VideoPrivacy.PRIVATE, | ||
191 | scheduleUpdate: { | ||
192 | updateAt: updateAt.toISOString(), | ||
193 | privacy: VideoPrivacy.PUBLIC | ||
194 | } | ||
195 | } | ||
196 | const uuid = await uploadVideoByRemoteAccount(servers, videoNameId, data) | ||
197 | |||
198 | await wait(6000) | ||
199 | await checkNewVideoFromSubscription(baseParams, videoName, uuid, 'presence') | ||
200 | }) | ||
201 | |||
202 | it('Should not send a notification before the video is published', async function () { | ||
203 | this.timeout(20000) | ||
204 | |||
205 | const videoNameId = 50 | ||
206 | const videoName = 'local video ' + videoNameId | ||
207 | |||
208 | let updateAt = new Date(new Date().getTime() + 100000) | ||
209 | |||
210 | const data = { | ||
211 | privacy: VideoPrivacy.PRIVATE, | ||
212 | scheduleUpdate: { | ||
213 | updateAt: updateAt.toISOString(), | ||
214 | privacy: VideoPrivacy.PUBLIC | ||
215 | } | ||
216 | } | ||
217 | const uuid = await uploadVideoByLocalAccount(servers, videoNameId, data) | ||
218 | |||
219 | await wait(6000) | ||
220 | await checkNewVideoFromSubscription(baseParams, videoName, uuid, 'absence') | ||
221 | }) | ||
222 | |||
223 | it('Should send a new video notification when a video becomes public', async function () { | ||
224 | this.timeout(10000) | ||
225 | |||
226 | const videoNameId = 60 | ||
227 | const videoName = 'local video ' + videoNameId | ||
228 | |||
229 | const data = { privacy: VideoPrivacy.PRIVATE } | ||
230 | const uuid = await uploadVideoByLocalAccount(servers, videoNameId, data) | ||
231 | |||
232 | await checkNewVideoFromSubscription(baseParams, videoName, uuid, 'absence') | ||
233 | |||
234 | await updateVideo(servers[0].url, servers[0].accessToken, uuid, { privacy: VideoPrivacy.PUBLIC }) | ||
235 | |||
236 | await wait(500) | ||
237 | await checkNewVideoFromSubscription(baseParams, videoName, uuid, 'presence') | ||
238 | }) | ||
239 | |||
240 | it('Should send a new video notification when a remote video becomes public', async function () { | ||
241 | this.timeout(20000) | ||
242 | |||
243 | const videoNameId = 70 | ||
244 | const videoName = 'remote video ' + videoNameId | ||
245 | |||
246 | const data = { privacy: VideoPrivacy.PRIVATE } | ||
247 | const uuid = await uploadVideoByRemoteAccount(servers, videoNameId, data) | ||
248 | |||
249 | await checkNewVideoFromSubscription(baseParams, videoName, uuid, 'absence') | ||
250 | |||
251 | await updateVideo(servers[1].url, servers[1].accessToken, uuid, { privacy: VideoPrivacy.PUBLIC }) | ||
252 | |||
253 | await waitJobs(servers) | ||
254 | await checkNewVideoFromSubscription(baseParams, videoName, uuid, 'presence') | ||
255 | }) | ||
256 | |||
257 | it('Should not send a new video notification when a video becomes unlisted', async function () { | ||
258 | this.timeout(20000) | ||
259 | |||
260 | const videoNameId = 80 | ||
261 | const videoName = 'local video ' + videoNameId | ||
262 | |||
263 | const data = { privacy: VideoPrivacy.PRIVATE } | ||
264 | const uuid = await uploadVideoByLocalAccount(servers, videoNameId, data) | ||
265 | |||
266 | await updateVideo(servers[0].url, servers[0].accessToken, uuid, { privacy: VideoPrivacy.UNLISTED }) | ||
267 | |||
268 | await checkNewVideoFromSubscription(baseParams, videoName, uuid, 'absence') | ||
269 | }) | ||
270 | |||
271 | it('Should not send a new video notification when a remote video becomes unlisted', async function () { | ||
272 | this.timeout(20000) | ||
273 | |||
274 | const videoNameId = 90 | ||
275 | const videoName = 'remote video ' + videoNameId | ||
276 | |||
277 | const data = { privacy: VideoPrivacy.PRIVATE } | ||
278 | const uuid = await uploadVideoByRemoteAccount(servers, videoNameId, data) | ||
279 | |||
280 | await updateVideo(servers[1].url, servers[1].accessToken, uuid, { privacy: VideoPrivacy.UNLISTED }) | ||
281 | |||
282 | await waitJobs(servers) | ||
283 | await checkNewVideoFromSubscription(baseParams, videoName, uuid, 'absence') | ||
284 | }) | ||
285 | |||
286 | it('Should send a new video notification after a video import', async function () { | ||
287 | this.timeout(30000) | ||
288 | |||
289 | const resChannel = await getMyUserInformation(servers[0].url, servers[0].accessToken) | ||
290 | const channelId = resChannel.body.videoChannels[0].id | ||
291 | const videoName = 'local video 100' | ||
292 | |||
293 | const attributes = { | ||
294 | name: videoName, | ||
295 | channelId, | ||
296 | privacy: VideoPrivacy.PUBLIC, | ||
297 | targetUrl: getYoutubeVideoUrl() | ||
298 | } | ||
299 | const res = await importVideo(servers[0].url, servers[0].accessToken, attributes) | ||
300 | const uuid = res.body.video.uuid | ||
301 | |||
302 | await waitJobs(servers) | ||
303 | |||
304 | await checkNewVideoFromSubscription(baseParams, videoName, uuid, 'presence') | ||
305 | }) | ||
306 | }) | ||
307 | |||
308 | describe('Comment on my video notifications', function () { | ||
309 | let baseParams: CheckerBaseParams | ||
310 | |||
311 | before(() => { | ||
312 | baseParams = { | ||
313 | server: servers[0], | ||
314 | emails, | ||
315 | socketNotifications: userNotifications, | ||
316 | token: userAccessToken | ||
317 | } | ||
318 | }) | ||
319 | |||
320 | it('Should not send a new comment notification after a comment on another video', async function () { | ||
321 | this.timeout(10000) | ||
322 | |||
323 | const resVideo = await uploadVideo(servers[0].url, servers[0].accessToken, { name: 'super video' }) | ||
324 | const uuid = resVideo.body.video.uuid | ||
325 | |||
326 | const resComment = await addVideoCommentThread(servers[0].url, servers[0].accessToken, uuid, 'comment') | ||
327 | const commentId = resComment.body.comment.id | ||
328 | |||
329 | await wait(500) | ||
330 | await checkNewCommentOnMyVideo(baseParams, uuid, commentId, commentId, 'absence') | ||
331 | }) | ||
332 | |||
333 | it('Should not send a new comment notification if I comment my own video', async function () { | ||
334 | this.timeout(10000) | ||
335 | |||
336 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: 'super video' }) | ||
337 | const uuid = resVideo.body.video.uuid | ||
338 | |||
339 | const resComment = await addVideoCommentThread(servers[0].url, userAccessToken, uuid, 'comment') | ||
340 | const commentId = resComment.body.comment.id | ||
341 | |||
342 | await wait(500) | ||
343 | await checkNewCommentOnMyVideo(baseParams, uuid, commentId, commentId, 'absence') | ||
344 | }) | ||
345 | |||
346 | it('Should send a new comment notification after a local comment on my video', async function () { | ||
347 | this.timeout(10000) | ||
348 | |||
349 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: 'super video' }) | ||
350 | const uuid = resVideo.body.video.uuid | ||
351 | |||
352 | const resComment = await addVideoCommentThread(servers[0].url, servers[0].accessToken, uuid, 'comment') | ||
353 | const commentId = resComment.body.comment.id | ||
354 | |||
355 | await wait(500) | ||
356 | await checkNewCommentOnMyVideo(baseParams, uuid, commentId, commentId, 'presence') | ||
357 | }) | ||
358 | |||
359 | it('Should send a new comment notification after a remote comment on my video', async function () { | ||
360 | this.timeout(10000) | ||
361 | |||
362 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: 'super video' }) | ||
363 | const uuid = resVideo.body.video.uuid | ||
364 | |||
365 | await waitJobs(servers) | ||
366 | |||
367 | const resComment = await addVideoCommentThread(servers[1].url, servers[1].accessToken, uuid, 'comment') | ||
368 | const commentId = resComment.body.comment.id | ||
369 | |||
370 | await waitJobs(servers) | ||
371 | await checkNewCommentOnMyVideo(baseParams, uuid, commentId, commentId, 'presence') | ||
372 | }) | ||
373 | |||
374 | it('Should send a new comment notification after a local reply on my video', async function () { | ||
375 | this.timeout(10000) | ||
376 | |||
377 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: 'super video' }) | ||
378 | const uuid = resVideo.body.video.uuid | ||
379 | |||
380 | const resThread = await addVideoCommentThread(servers[0].url, servers[0].accessToken, uuid, 'comment') | ||
381 | const threadId = resThread.body.comment.id | ||
382 | |||
383 | const resComment = await addVideoCommentReply(servers[0].url, servers[0].accessToken, uuid, threadId, 'reply') | ||
384 | const commentId = resComment.body.comment.id | ||
385 | |||
386 | await wait(500) | ||
387 | await checkNewCommentOnMyVideo(baseParams, uuid, commentId, threadId, 'presence') | ||
388 | }) | ||
389 | |||
390 | it('Should send a new comment notification after a remote reply on my video', async function () { | ||
391 | this.timeout(10000) | ||
392 | |||
393 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: 'super video' }) | ||
394 | const uuid = resVideo.body.video.uuid | ||
395 | await waitJobs(servers) | ||
396 | |||
397 | const resThread = await addVideoCommentThread(servers[1].url, servers[1].accessToken, uuid, 'comment') | ||
398 | const threadId = resThread.body.comment.id | ||
399 | |||
400 | const resComment = await addVideoCommentReply(servers[1].url, servers[1].accessToken, uuid, threadId, 'reply') | ||
401 | const commentId = resComment.body.comment.id | ||
402 | |||
403 | await waitJobs(servers) | ||
404 | await checkNewCommentOnMyVideo(baseParams, uuid, commentId, threadId, 'presence') | ||
405 | }) | ||
406 | }) | ||
407 | |||
408 | describe('Video abuse for moderators notification' , function () { | ||
409 | let baseParams: CheckerBaseParams | ||
410 | |||
411 | before(() => { | ||
412 | baseParams = { | ||
413 | server: servers[0], | ||
414 | emails, | ||
415 | socketNotifications: adminNotifications, | ||
416 | token: servers[0].accessToken | ||
417 | } | ||
418 | }) | ||
419 | |||
420 | it('Should send a notification to moderators on local video abuse', async function () { | ||
421 | this.timeout(10000) | ||
422 | |||
423 | const videoName = 'local video 110' | ||
424 | |||
425 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: videoName }) | ||
426 | const uuid = resVideo.body.video.uuid | ||
427 | |||
428 | await reportVideoAbuse(servers[0].url, servers[0].accessToken, uuid, 'super reason') | ||
429 | |||
430 | await waitJobs(servers) | ||
431 | await checkNewVideoAbuseForModerators(baseParams, uuid, videoName, 'presence') | ||
432 | }) | ||
433 | |||
434 | it('Should send a notification to moderators on remote video abuse', async function () { | ||
435 | this.timeout(10000) | ||
436 | |||
437 | const videoName = 'remote video 120' | ||
438 | |||
439 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: videoName }) | ||
440 | const uuid = resVideo.body.video.uuid | ||
441 | |||
442 | await waitJobs(servers) | ||
443 | |||
444 | await reportVideoAbuse(servers[1].url, servers[1].accessToken, uuid, 'super reason') | ||
445 | |||
446 | await waitJobs(servers) | ||
447 | await checkNewVideoAbuseForModerators(baseParams, uuid, videoName, 'presence') | ||
448 | }) | ||
449 | }) | ||
450 | |||
451 | describe('Video blacklist on my video', function () { | ||
452 | let baseParams: CheckerBaseParams | ||
453 | |||
454 | before(() => { | ||
455 | baseParams = { | ||
456 | server: servers[0], | ||
457 | emails, | ||
458 | socketNotifications: userNotifications, | ||
459 | token: userAccessToken | ||
460 | } | ||
461 | }) | ||
462 | |||
463 | it('Should send a notification to video owner on blacklist', async function () { | ||
464 | this.timeout(10000) | ||
465 | |||
466 | const videoName = 'local video 130' | ||
467 | |||
468 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: videoName }) | ||
469 | const uuid = resVideo.body.video.uuid | ||
470 | |||
471 | await addVideoToBlacklist(servers[0].url, servers[0].accessToken, uuid) | ||
472 | |||
473 | await waitJobs(servers) | ||
474 | await checkNewBlacklistOnMyVideo(baseParams, uuid, videoName, 'blacklist') | ||
475 | }) | ||
476 | |||
477 | it('Should send a notification to video owner on unblacklist', async function () { | ||
478 | this.timeout(10000) | ||
479 | |||
480 | const videoName = 'local video 130' | ||
481 | |||
482 | const resVideo = await uploadVideo(servers[0].url, userAccessToken, { name: videoName }) | ||
483 | const uuid = resVideo.body.video.uuid | ||
484 | |||
485 | await addVideoToBlacklist(servers[0].url, servers[0].accessToken, uuid) | ||
486 | |||
487 | await waitJobs(servers) | ||
488 | await removeVideoFromBlacklist(servers[0].url, servers[0].accessToken, uuid) | ||
489 | await waitJobs(servers) | ||
490 | |||
491 | await wait(500) | ||
492 | await checkNewBlacklistOnMyVideo(baseParams, uuid, videoName, 'unblacklist') | ||
493 | }) | ||
494 | }) | ||
495 | |||
496 | describe('Mark as read', function () { | ||
497 | it('Should mark as read some notifications', async function () { | ||
498 | const res = await getUserNotifications(servers[0].url, userAccessToken, 2, 3) | ||
499 | const ids = res.body.data.map(n => n.id) | ||
500 | |||
501 | await markAsReadNotifications(servers[0].url, userAccessToken, ids) | ||
502 | }) | ||
503 | |||
504 | it('Should have the notifications marked as read', async function () { | ||
505 | const res = await getUserNotifications(servers[0].url, userAccessToken, 0, 10) | ||
506 | |||
507 | const notifications = res.body.data as UserNotification[] | ||
508 | expect(notifications[0].read).to.be.false | ||
509 | expect(notifications[1].read).to.be.false | ||
510 | expect(notifications[2].read).to.be.true | ||
511 | expect(notifications[3].read).to.be.true | ||
512 | expect(notifications[4].read).to.be.true | ||
513 | expect(notifications[5].read).to.be.false | ||
514 | }) | ||
515 | }) | ||
516 | |||
517 | describe('Notification settings', function () { | ||
518 | const baseUpdateNotificationParams = { | ||
519 | newCommentOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL, | ||
520 | newVideoFromSubscription: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL, | ||
521 | videoAbuseAsModerator: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL, | ||
522 | blacklistOnMyVideo: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL | ||
523 | } | ||
524 | let baseParams: CheckerBaseParams | ||
525 | |||
526 | before(() => { | ||
527 | baseParams = { | ||
528 | server: servers[0], | ||
529 | emails, | ||
530 | socketNotifications: userNotifications, | ||
531 | token: userAccessToken | ||
532 | } | ||
533 | }) | ||
534 | |||
535 | it('Should not have notifications', async function () { | ||
536 | await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(baseUpdateNotificationParams, { | ||
537 | newVideoFromSubscription: UserNotificationSettingValue.NONE | ||
538 | })) | ||
539 | |||
540 | { | ||
541 | const res = await getMyUserInformation(servers[0].url, userAccessToken) | ||
542 | const info = res.body as User | ||
543 | expect(info.notificationSettings.newVideoFromSubscription).to.equal(UserNotificationSettingValue.NONE) | ||
544 | } | ||
545 | |||
546 | const videoNameId = 42 | ||
547 | const videoName = 'local video ' + videoNameId | ||
548 | const uuid = await uploadVideoByLocalAccount(servers, videoNameId) | ||
549 | |||
550 | const check = { web: true, mail: true } | ||
551 | await checkNewVideoFromSubscription(immutableAssign(baseParams, { check }), videoName, uuid, 'absence') | ||
552 | }) | ||
553 | |||
554 | it('Should only have web notifications', async function () { | ||
555 | await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(baseUpdateNotificationParams, { | ||
556 | newVideoFromSubscription: UserNotificationSettingValue.WEB_NOTIFICATION | ||
557 | })) | ||
558 | |||
559 | { | ||
560 | const res = await getMyUserInformation(servers[0].url, userAccessToken) | ||
561 | const info = res.body as User | ||
562 | expect(info.notificationSettings.newVideoFromSubscription).to.equal(UserNotificationSettingValue.WEB_NOTIFICATION) | ||
563 | } | ||
564 | |||
565 | const videoNameId = 52 | ||
566 | const videoName = 'local video ' + videoNameId | ||
567 | const uuid = await uploadVideoByLocalAccount(servers, videoNameId) | ||
568 | |||
569 | { | ||
570 | const check = { mail: true, web: false } | ||
571 | await checkNewVideoFromSubscription(immutableAssign(baseParams, { check }), videoName, uuid, 'absence') | ||
572 | } | ||
573 | |||
574 | { | ||
575 | const check = { mail: false, web: true } | ||
576 | await checkNewVideoFromSubscription(immutableAssign(baseParams, { check }), videoName, uuid, 'presence') | ||
577 | } | ||
578 | }) | ||
579 | |||
580 | it('Should only have mail notifications', async function () { | ||
581 | await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(baseUpdateNotificationParams, { | ||
582 | newVideoFromSubscription: UserNotificationSettingValue.EMAIL | ||
583 | })) | ||
584 | |||
585 | { | ||
586 | const res = await getMyUserInformation(servers[0].url, userAccessToken) | ||
587 | const info = res.body as User | ||
588 | expect(info.notificationSettings.newVideoFromSubscription).to.equal(UserNotificationSettingValue.EMAIL) | ||
589 | } | ||
590 | |||
591 | const videoNameId = 62 | ||
592 | const videoName = 'local video ' + videoNameId | ||
593 | const uuid = await uploadVideoByLocalAccount(servers, videoNameId) | ||
594 | |||
595 | { | ||
596 | const check = { mail: false, web: true } | ||
597 | await checkNewVideoFromSubscription(immutableAssign(baseParams, { check }), videoName, uuid, 'absence') | ||
598 | } | ||
599 | |||
600 | { | ||
601 | const check = { mail: true, web: false } | ||
602 | await checkNewVideoFromSubscription(immutableAssign(baseParams, { check }), videoName, uuid, 'presence') | ||
603 | } | ||
604 | }) | ||
605 | |||
606 | it('Should have email and web notifications', async function () { | ||
607 | await updateMyNotificationSettings(servers[0].url, userAccessToken, immutableAssign(baseUpdateNotificationParams, { | ||
608 | newVideoFromSubscription: UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL | ||
609 | })) | ||
610 | |||
611 | { | ||
612 | const res = await getMyUserInformation(servers[0].url, userAccessToken) | ||
613 | const info = res.body as User | ||
614 | expect(info.notificationSettings.newVideoFromSubscription).to.equal(UserNotificationSettingValue.WEB_NOTIFICATION_AND_EMAIL) | ||
615 | } | ||
616 | |||
617 | const videoNameId = 72 | ||
618 | const videoName = 'local video ' + videoNameId | ||
619 | const uuid = await uploadVideoByLocalAccount(servers, videoNameId) | ||
620 | |||
621 | await checkNewVideoFromSubscription(baseParams, videoName, uuid, 'presence') | ||
622 | }) | ||
623 | }) | ||
624 | |||
625 | after(async function () { | ||
626 | killallServers(servers) | ||
627 | }) | ||
628 | }) | ||
diff --git a/shared/models/users/index.ts b/shared/models/users/index.ts index 7114741e0..cd07cf320 100644 --- a/shared/models/users/index.ts +++ b/shared/models/users/index.ts | |||
@@ -1,6 +1,8 @@ | |||
1 | export * from './user.model' | 1 | export * from './user.model' |
2 | export * from './user-create.model' | 2 | export * from './user-create.model' |
3 | export * from './user-login.model' | 3 | export * from './user-login.model' |
4 | export * from './user-notification.model' | ||
5 | export * from './user-notification-setting.model' | ||
4 | export * from './user-refresh-token.model' | 6 | export * from './user-refresh-token.model' |
5 | export * from './user-update.model' | 7 | export * from './user-update.model' |
6 | export * from './user-update-me.model' | 8 | export * from './user-update-me.model' |
diff --git a/shared/models/users/user-notification-setting.model.ts b/shared/models/users/user-notification-setting.model.ts new file mode 100644 index 000000000..7cecd70a2 --- /dev/null +++ b/shared/models/users/user-notification-setting.model.ts | |||
@@ -0,0 +1,13 @@ | |||
1 | export enum UserNotificationSettingValue { | ||
2 | NONE = 1, | ||
3 | WEB_NOTIFICATION = 2, | ||
4 | EMAIL = 3, | ||
5 | WEB_NOTIFICATION_AND_EMAIL = 4 | ||
6 | } | ||
7 | |||
8 | export interface UserNotificationSetting { | ||
9 | newVideoFromSubscription: UserNotificationSettingValue | ||
10 | newCommentOnMyVideo: UserNotificationSettingValue | ||
11 | videoAbuseAsModerator: UserNotificationSettingValue | ||
12 | blacklistOnMyVideo: UserNotificationSettingValue | ||
13 | } | ||
diff --git a/shared/models/users/user-notification.model.ts b/shared/models/users/user-notification.model.ts new file mode 100644 index 000000000..39beb2350 --- /dev/null +++ b/shared/models/users/user-notification.model.ts | |||
@@ -0,0 +1,47 @@ | |||
1 | export enum UserNotificationType { | ||
2 | NEW_VIDEO_FROM_SUBSCRIPTION = 1, | ||
3 | NEW_COMMENT_ON_MY_VIDEO = 2, | ||
4 | NEW_VIDEO_ABUSE_FOR_MODERATORS = 3, | ||
5 | BLACKLIST_ON_MY_VIDEO = 4, | ||
6 | UNBLACKLIST_ON_MY_VIDEO = 5 | ||
7 | } | ||
8 | |||
9 | interface VideoInfo { | ||
10 | id: number | ||
11 | uuid: string | ||
12 | name: string | ||
13 | } | ||
14 | |||
15 | export interface UserNotification { | ||
16 | id: number | ||
17 | type: UserNotificationType | ||
18 | read: boolean | ||
19 | |||
20 | video?: VideoInfo & { | ||
21 | channel: { | ||
22 | id: number | ||
23 | displayName: string | ||
24 | } | ||
25 | } | ||
26 | |||
27 | comment?: { | ||
28 | id: number | ||
29 | account: { | ||
30 | id: number | ||
31 | displayName: string | ||
32 | } | ||
33 | } | ||
34 | |||
35 | videoAbuse?: { | ||
36 | id: number | ||
37 | video: VideoInfo | ||
38 | } | ||
39 | |||
40 | videoBlacklist?: { | ||
41 | id: number | ||
42 | video: VideoInfo | ||
43 | } | ||
44 | |||
45 | createdAt: string | ||
46 | updatedAt: string | ||
47 | } | ||
diff --git a/shared/models/users/user.model.ts b/shared/models/users/user.model.ts index 2aabff494..af783d389 100644 --- a/shared/models/users/user.model.ts +++ b/shared/models/users/user.model.ts | |||
@@ -2,6 +2,7 @@ import { Account } from '../actors' | |||
2 | import { VideoChannel } from '../videos/channel/video-channel.model' | 2 | import { VideoChannel } from '../videos/channel/video-channel.model' |
3 | import { UserRole } from './user-role' | 3 | import { UserRole } from './user-role' |
4 | import { NSFWPolicyType } from '../videos/nsfw-policy.type' | 4 | import { NSFWPolicyType } from '../videos/nsfw-policy.type' |
5 | import { UserNotificationSetting } from './user-notification-setting.model' | ||
5 | 6 | ||
6 | export interface User { | 7 | export interface User { |
7 | id: number | 8 | id: number |
@@ -19,6 +20,7 @@ export interface User { | |||
19 | videoQuotaDaily: number | 20 | videoQuotaDaily: number |
20 | createdAt: Date | 21 | createdAt: Date |
21 | account: Account | 22 | account: Account |
23 | notificationSettings?: UserNotificationSetting | ||
22 | videoChannels?: VideoChannel[] | 24 | videoChannels?: VideoChannel[] |
23 | 25 | ||
24 | blocked: boolean | 26 | blocked: boolean |
diff --git a/shared/utils/server/jobs.ts b/shared/utils/server/jobs.ts index f4623f896..6218c0b66 100644 --- a/shared/utils/server/jobs.ts +++ b/shared/utils/server/jobs.ts | |||
@@ -35,10 +35,10 @@ async function waitJobs (serversArg: ServerInfo[] | ServerInfo) { | |||
35 | else servers = serversArg as ServerInfo[] | 35 | else servers = serversArg as ServerInfo[] |
36 | 36 | ||
37 | const states: JobState[] = [ 'waiting', 'active', 'delayed' ] | 37 | const states: JobState[] = [ 'waiting', 'active', 'delayed' ] |
38 | const tasks: Promise<any>[] = [] | 38 | let pendingRequests = false |
39 | let pendingRequests: boolean | ||
40 | 39 | ||
41 | do { | 40 | function tasksBuilder () { |
41 | const tasks: Promise<any>[] = [] | ||
42 | pendingRequests = false | 42 | pendingRequests = false |
43 | 43 | ||
44 | // Check if each server has pending request | 44 | // Check if each server has pending request |
@@ -54,13 +54,16 @@ async function waitJobs (serversArg: ServerInfo[] | ServerInfo) { | |||
54 | } | 54 | } |
55 | } | 55 | } |
56 | 56 | ||
57 | await Promise.all(tasks) | 57 | return tasks |
58 | } | ||
59 | |||
60 | do { | ||
61 | await Promise.all(tasksBuilder()) | ||
58 | 62 | ||
59 | // Retry, in case of new jobs were created | 63 | // Retry, in case of new jobs were created |
60 | if (pendingRequests === false) { | 64 | if (pendingRequests === false) { |
61 | await wait(2000) | 65 | await wait(2000) |
62 | 66 | await Promise.all(tasksBuilder()) | |
63 | await Promise.all(tasks) | ||
64 | } | 67 | } |
65 | 68 | ||
66 | if (pendingRequests) { | 69 | if (pendingRequests) { |
diff --git a/shared/utils/socket/socket-io.ts b/shared/utils/socket/socket-io.ts new file mode 100644 index 000000000..854ab71af --- /dev/null +++ b/shared/utils/socket/socket-io.ts | |||
@@ -0,0 +1,13 @@ | |||
1 | import * as io from 'socket.io-client' | ||
2 | |||
3 | function getUserNotificationSocket (serverUrl: string, accessToken: string) { | ||
4 | return io(serverUrl + '/user-notifications', { | ||
5 | query: { accessToken } | ||
6 | }) | ||
7 | } | ||
8 | |||
9 | // --------------------------------------------------------------------------- | ||
10 | |||
11 | export { | ||
12 | getUserNotificationSocket | ||
13 | } | ||
diff --git a/shared/utils/users/user-notifications.ts b/shared/utils/users/user-notifications.ts new file mode 100644 index 000000000..dbe87559e --- /dev/null +++ b/shared/utils/users/user-notifications.ts | |||
@@ -0,0 +1,232 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import { makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests' | ||
4 | import { UserNotification, UserNotificationSetting, UserNotificationType } from '../../models/users' | ||
5 | import { ServerInfo } from '..' | ||
6 | import { expect } from 'chai' | ||
7 | |||
8 | function updateMyNotificationSettings (url: string, token: string, settings: UserNotificationSetting, statusCodeExpected = 204) { | ||
9 | const path = '/api/v1/users/me/notification-settings' | ||
10 | |||
11 | return makePutBodyRequest({ | ||
12 | url, | ||
13 | path, | ||
14 | token, | ||
15 | fields: settings, | ||
16 | statusCodeExpected | ||
17 | }) | ||
18 | } | ||
19 | |||
20 | function getUserNotifications (url: string, token: string, start: number, count: number, sort = '-createdAt', statusCodeExpected = 200) { | ||
21 | const path = '/api/v1/users/me/notifications' | ||
22 | |||
23 | return makeGetRequest({ | ||
24 | url, | ||
25 | path, | ||
26 | token, | ||
27 | query: { | ||
28 | start, | ||
29 | count, | ||
30 | sort | ||
31 | }, | ||
32 | statusCodeExpected | ||
33 | }) | ||
34 | } | ||
35 | |||
36 | function markAsReadNotifications (url: string, token: string, ids: number[], statusCodeExpected = 204) { | ||
37 | const path = '/api/v1/users/me/notifications/read' | ||
38 | |||
39 | return makePostBodyRequest({ | ||
40 | url, | ||
41 | path, | ||
42 | token, | ||
43 | fields: { ids }, | ||
44 | statusCodeExpected | ||
45 | }) | ||
46 | } | ||
47 | |||
48 | async function getLastNotification (serverUrl: string, accessToken: string) { | ||
49 | const res = await getUserNotifications(serverUrl, accessToken, 0, 1, '-createdAt') | ||
50 | |||
51 | if (res.body.total === 0) return undefined | ||
52 | |||
53 | return res.body.data[0] as UserNotification | ||
54 | } | ||
55 | |||
56 | type CheckerBaseParams = { | ||
57 | server: ServerInfo | ||
58 | emails: object[] | ||
59 | socketNotifications: UserNotification[] | ||
60 | token: string, | ||
61 | check?: { web: boolean, mail: boolean } | ||
62 | } | ||
63 | |||
64 | type CheckerType = 'presence' | 'absence' | ||
65 | |||
66 | async function checkNotification ( | ||
67 | base: CheckerBaseParams, | ||
68 | lastNotificationChecker: (notification: UserNotification) => void, | ||
69 | socketNotificationFinder: (notification: UserNotification) => boolean, | ||
70 | emailNotificationFinder: (email: object) => boolean, | ||
71 | checkType: 'presence' | 'absence' | ||
72 | ) { | ||
73 | const check = base.check || { web: true, mail: true } | ||
74 | |||
75 | if (check.web) { | ||
76 | const notification = await getLastNotification(base.server.url, base.token) | ||
77 | lastNotificationChecker(notification) | ||
78 | |||
79 | const socketNotification = base.socketNotifications.find(n => socketNotificationFinder(n)) | ||
80 | |||
81 | if (checkType === 'presence') expect(socketNotification, 'The socket notification is absent.').to.not.be.undefined | ||
82 | else expect(socketNotification, 'The socket notification is present.').to.be.undefined | ||
83 | } | ||
84 | |||
85 | if (check.mail) { | ||
86 | // Last email | ||
87 | const email = base.emails | ||
88 | .slice() | ||
89 | .reverse() | ||
90 | .find(e => emailNotificationFinder(e)) | ||
91 | |||
92 | if (checkType === 'presence') expect(email, 'The email is present.').to.not.be.undefined | ||
93 | else expect(email, 'The email is absent.').to.be.undefined | ||
94 | } | ||
95 | } | ||
96 | |||
97 | async function checkNewVideoFromSubscription (base: CheckerBaseParams, videoName: string, videoUUID: string, type: CheckerType) { | ||
98 | const notificationType = UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION | ||
99 | |||
100 | function lastNotificationChecker (notification: UserNotification) { | ||
101 | if (type === 'presence') { | ||
102 | expect(notification).to.not.be.undefined | ||
103 | expect(notification.type).to.equal(notificationType) | ||
104 | expect(notification.video.name).to.equal(videoName) | ||
105 | } else { | ||
106 | expect(notification.video).to.satisfy(v => v === undefined || v.name !== videoName) | ||
107 | } | ||
108 | } | ||
109 | |||
110 | function socketFinder (notification: UserNotification) { | ||
111 | return notification.type === notificationType && notification.video.name === videoName | ||
112 | } | ||
113 | |||
114 | function emailFinder (email: object) { | ||
115 | return email[ 'text' ].indexOf(videoUUID) !== -1 | ||
116 | } | ||
117 | |||
118 | await checkNotification(base, lastNotificationChecker, socketFinder, emailFinder, type) | ||
119 | } | ||
120 | |||
121 | let lastEmailCount = 0 | ||
122 | async function checkNewCommentOnMyVideo (base: CheckerBaseParams, uuid: string, commentId: number, threadId: number, type: CheckerType) { | ||
123 | const notificationType = UserNotificationType.NEW_COMMENT_ON_MY_VIDEO | ||
124 | |||
125 | function lastNotificationChecker (notification: UserNotification) { | ||
126 | if (type === 'presence') { | ||
127 | expect(notification).to.not.be.undefined | ||
128 | expect(notification.type).to.equal(notificationType) | ||
129 | expect(notification.comment.id).to.equal(commentId) | ||
130 | expect(notification.comment.account.displayName).to.equal('root') | ||
131 | } else { | ||
132 | expect(notification).to.satisfy((n: UserNotification) => { | ||
133 | return n === undefined || n.comment === undefined || n.comment.id !== commentId | ||
134 | }) | ||
135 | } | ||
136 | } | ||
137 | |||
138 | function socketFinder (notification: UserNotification) { | ||
139 | return notification.type === notificationType && | ||
140 | notification.comment.id === commentId && | ||
141 | notification.comment.account.displayName === 'root' | ||
142 | } | ||
143 | |||
144 | const commentUrl = `http://localhost:9001/videos/watch/${uuid};threadId=${threadId}` | ||
145 | function emailFinder (email: object) { | ||
146 | return email[ 'text' ].indexOf(commentUrl) !== -1 | ||
147 | } | ||
148 | |||
149 | await checkNotification(base, lastNotificationChecker, socketFinder, emailFinder, type) | ||
150 | |||
151 | if (type === 'presence') { | ||
152 | // We cannot detect email duplicates, so check we received another email | ||
153 | expect(base.emails).to.have.length.above(lastEmailCount) | ||
154 | lastEmailCount = base.emails.length | ||
155 | } | ||
156 | } | ||
157 | |||
158 | async function checkNewVideoAbuseForModerators (base: CheckerBaseParams, videoUUID: string, videoName: string, type: CheckerType) { | ||
159 | const notificationType = UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS | ||
160 | |||
161 | function lastNotificationChecker (notification: UserNotification) { | ||
162 | if (type === 'presence') { | ||
163 | expect(notification).to.not.be.undefined | ||
164 | expect(notification.type).to.equal(notificationType) | ||
165 | expect(notification.videoAbuse.video.uuid).to.equal(videoUUID) | ||
166 | expect(notification.videoAbuse.video.name).to.equal(videoName) | ||
167 | } else { | ||
168 | expect(notification).to.satisfy((n: UserNotification) => { | ||
169 | return n === undefined || n.videoAbuse === undefined || n.videoAbuse.video.uuid !== videoUUID | ||
170 | }) | ||
171 | } | ||
172 | } | ||
173 | |||
174 | function socketFinder (notification: UserNotification) { | ||
175 | return notification.type === notificationType && notification.videoAbuse.video.uuid === videoUUID | ||
176 | } | ||
177 | |||
178 | function emailFinder (email: object) { | ||
179 | const text = email[ 'text' ] | ||
180 | return text.indexOf(videoUUID) !== -1 && text.indexOf('abuse') !== -1 | ||
181 | } | ||
182 | |||
183 | await checkNotification(base, lastNotificationChecker, socketFinder, emailFinder, type) | ||
184 | } | ||
185 | |||
186 | async function checkNewBlacklistOnMyVideo ( | ||
187 | base: CheckerBaseParams, | ||
188 | videoUUID: string, | ||
189 | videoName: string, | ||
190 | blacklistType: 'blacklist' | 'unblacklist' | ||
191 | ) { | ||
192 | const notificationType = blacklistType === 'blacklist' | ||
193 | ? UserNotificationType.BLACKLIST_ON_MY_VIDEO | ||
194 | : UserNotificationType.UNBLACKLIST_ON_MY_VIDEO | ||
195 | |||
196 | function lastNotificationChecker (notification: UserNotification) { | ||
197 | expect(notification).to.not.be.undefined | ||
198 | expect(notification.type).to.equal(notificationType) | ||
199 | |||
200 | const video = blacklistType === 'blacklist' ? notification.videoBlacklist.video : notification.video | ||
201 | |||
202 | expect(video.uuid).to.equal(videoUUID) | ||
203 | expect(video.name).to.equal(videoName) | ||
204 | } | ||
205 | |||
206 | function socketFinder (notification: UserNotification) { | ||
207 | return notification.type === notificationType && (notification.video || notification.videoBlacklist.video).uuid === videoUUID | ||
208 | } | ||
209 | |||
210 | function emailFinder (email: object) { | ||
211 | const text = email[ 'text' ] | ||
212 | return text.indexOf(videoUUID) !== -1 && text.indexOf(' ' + blacklistType) !== -1 | ||
213 | } | ||
214 | |||
215 | await checkNotification(base, lastNotificationChecker, socketFinder, emailFinder, 'presence') | ||
216 | } | ||
217 | |||
218 | // --------------------------------------------------------------------------- | ||
219 | |||
220 | export { | ||
221 | CheckerBaseParams, | ||
222 | CheckerType, | ||
223 | checkNotification, | ||
224 | checkNewVideoFromSubscription, | ||
225 | checkNewCommentOnMyVideo, | ||
226 | checkNewBlacklistOnMyVideo, | ||
227 | updateMyNotificationSettings, | ||
228 | checkNewVideoAbuseForModerators, | ||
229 | getUserNotifications, | ||
230 | markAsReadNotifications, | ||
231 | getLastNotification | ||
232 | } | ||
@@ -346,6 +346,13 @@ | |||
346 | dependencies: | 346 | dependencies: |
347 | "@types/node" "*" | 347 | "@types/node" "*" |
348 | 348 | ||
349 | "@types/socket.io@^2.1.2": | ||
350 | version "2.1.2" | ||
351 | resolved "https://registry.yarnpkg.com/@types/socket.io/-/socket.io-2.1.2.tgz#7165c2587cc3b86b44aa78e2a0060140551de211" | ||
352 | integrity sha512-Ind+4qMNfQ62llyB4IMs1D8znMEBsMKohZBPqfBUIXqLQ9bdtWIbNTBWwtdcBWJKnokMZGcmWOOKslatni5vtA== | ||
353 | dependencies: | ||
354 | "@types/node" "*" | ||
355 | |||
349 | "@types/superagent@*": | 356 | "@types/superagent@*": |
350 | version "3.8.4" | 357 | version "3.8.4" |
351 | resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-3.8.4.tgz#24a5973c7d1a9c024b4bbda742a79267c33fb86a" | 358 | resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-3.8.4.tgz#24a5973c7d1a9c024b4bbda742a79267c33fb86a" |
@@ -423,7 +430,7 @@ accepts@~1.2.12: | |||
423 | mime-types "~2.1.6" | 430 | mime-types "~2.1.6" |
424 | negotiator "0.5.3" | 431 | negotiator "0.5.3" |
425 | 432 | ||
426 | accepts@~1.3.5: | 433 | accepts@~1.3.4, accepts@~1.3.5: |
427 | version "1.3.5" | 434 | version "1.3.5" |
428 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2" | 435 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2" |
429 | integrity sha1-63d99gEXI6OxTopywIBcjoZ0a9I= | 436 | integrity sha1-63d99gEXI6OxTopywIBcjoZ0a9I= |
@@ -652,6 +659,11 @@ arraybuffer.slice@0.0.6: | |||
652 | resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz#f33b2159f0532a3f3107a272c0ccfbd1ad2979ca" | 659 | resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz#f33b2159f0532a3f3107a272c0ccfbd1ad2979ca" |
653 | integrity sha1-8zshWfBTKj8xB6JywMz70a0peco= | 660 | integrity sha1-8zshWfBTKj8xB6JywMz70a0peco= |
654 | 661 | ||
662 | arraybuffer.slice@~0.0.7: | ||
663 | version "0.0.7" | ||
664 | resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675" | ||
665 | integrity sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog== | ||
666 | |||
655 | arrify@^1.0.0, arrify@^1.0.1: | 667 | arrify@^1.0.0, arrify@^1.0.1: |
656 | version "1.0.1" | 668 | version "1.0.1" |
657 | resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" | 669 | resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" |
@@ -977,6 +989,11 @@ blob@0.0.4: | |||
977 | resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.4.tgz#bcf13052ca54463f30f9fc7e95b9a47630a94921" | 989 | resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.4.tgz#bcf13052ca54463f30f9fc7e95b9a47630a94921" |
978 | integrity sha1-vPEwUspURj8w+fx+lbmkdjCpSSE= | 990 | integrity sha1-vPEwUspURj8w+fx+lbmkdjCpSSE= |
979 | 991 | ||
992 | blob@0.0.5: | ||
993 | version "0.0.5" | ||
994 | resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683" | ||
995 | integrity sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig== | ||
996 | |||
980 | block-stream2@^1.0.0: | 997 | block-stream2@^1.0.0: |
981 | version "1.1.0" | 998 | version "1.1.0" |
982 | resolved "https://registry.yarnpkg.com/block-stream2/-/block-stream2-1.1.0.tgz#c738e3a91ba977ebb5e1fef431e13ca11d8639e2" | 999 | resolved "https://registry.yarnpkg.com/block-stream2/-/block-stream2-1.1.0.tgz#c738e3a91ba977ebb5e1fef431e13ca11d8639e2" |
@@ -1995,7 +2012,7 @@ debug@2.6.9, debug@^2.1.1, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6. | |||
1995 | dependencies: | 2012 | dependencies: |
1996 | ms "2.0.0" | 2013 | ms "2.0.0" |
1997 | 2014 | ||
1998 | debug@3.1.0: | 2015 | debug@3.1.0, debug@~3.1.0: |
1999 | version "3.1.0" | 2016 | version "3.1.0" |
2000 | resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" | 2017 | resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" |
2001 | integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== | 2018 | integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== |
@@ -2016,6 +2033,13 @@ debug@^4.0.1: | |||
2016 | dependencies: | 2033 | dependencies: |
2017 | ms "^2.1.1" | 2034 | ms "^2.1.1" |
2018 | 2035 | ||
2036 | debug@~4.1.0: | ||
2037 | version "4.1.1" | ||
2038 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" | ||
2039 | integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== | ||
2040 | dependencies: | ||
2041 | ms "^2.1.1" | ||
2042 | |||
2019 | debuglog@^1.0.0, debuglog@^1.0.1: | 2043 | debuglog@^1.0.0, debuglog@^1.0.1: |
2020 | version "1.0.1" | 2044 | version "1.0.1" |
2021 | resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" | 2045 | resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" |
@@ -2367,6 +2391,23 @@ engine.io-client@1.8.3: | |||
2367 | xmlhttprequest-ssl "1.5.3" | 2391 | xmlhttprequest-ssl "1.5.3" |
2368 | yeast "0.1.2" | 2392 | yeast "0.1.2" |
2369 | 2393 | ||
2394 | engine.io-client@~3.3.1: | ||
2395 | version "3.3.1" | ||
2396 | resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.3.1.tgz#afedb4a07b2ea48b7190c3136bfea98fdd4f0f03" | ||
2397 | integrity sha512-q66JBFuQcy7CSlfAz9L3jH+v7DTT3i6ZEadYcVj2pOs8/0uJHLxKX3WBkGTvULJMdz0tUCyJag0aKT/dpXL9BQ== | ||
2398 | dependencies: | ||
2399 | component-emitter "1.2.1" | ||
2400 | component-inherit "0.0.3" | ||
2401 | debug "~3.1.0" | ||
2402 | engine.io-parser "~2.1.1" | ||
2403 | has-cors "1.1.0" | ||
2404 | indexof "0.0.1" | ||
2405 | parseqs "0.0.5" | ||
2406 | parseuri "0.0.5" | ||
2407 | ws "~6.1.0" | ||
2408 | xmlhttprequest-ssl "~1.5.4" | ||
2409 | yeast "0.1.2" | ||
2410 | |||
2370 | engine.io-parser@1.3.2: | 2411 | engine.io-parser@1.3.2: |
2371 | version "1.3.2" | 2412 | version "1.3.2" |
2372 | resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-1.3.2.tgz#937b079f0007d0893ec56d46cb220b8cb435220a" | 2413 | resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-1.3.2.tgz#937b079f0007d0893ec56d46cb220b8cb435220a" |
@@ -2379,6 +2420,17 @@ engine.io-parser@1.3.2: | |||
2379 | has-binary "0.1.7" | 2420 | has-binary "0.1.7" |
2380 | wtf-8 "1.0.0" | 2421 | wtf-8 "1.0.0" |
2381 | 2422 | ||
2423 | engine.io-parser@~2.1.0, engine.io-parser@~2.1.1: | ||
2424 | version "2.1.3" | ||
2425 | resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-2.1.3.tgz#757ab970fbf2dfb32c7b74b033216d5739ef79a6" | ||
2426 | integrity sha512-6HXPre2O4Houl7c4g7Ic/XzPnHBvaEmN90vtRO9uLmwtRqQmTOw0QMevL1TOfL2Cpu1VzsaTmMotQgMdkzGkVA== | ||
2427 | dependencies: | ||
2428 | after "0.8.2" | ||
2429 | arraybuffer.slice "~0.0.7" | ||
2430 | base64-arraybuffer "0.1.5" | ||
2431 | blob "0.0.5" | ||
2432 | has-binary2 "~1.0.2" | ||
2433 | |||
2382 | engine.io@1.8.3: | 2434 | engine.io@1.8.3: |
2383 | version "1.8.3" | 2435 | version "1.8.3" |
2384 | resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-1.8.3.tgz#8de7f97895d20d39b85f88eeee777b2bd42b13d4" | 2436 | resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-1.8.3.tgz#8de7f97895d20d39b85f88eeee777b2bd42b13d4" |
@@ -2391,6 +2443,18 @@ engine.io@1.8.3: | |||
2391 | engine.io-parser "1.3.2" | 2443 | engine.io-parser "1.3.2" |
2392 | ws "1.1.2" | 2444 | ws "1.1.2" |
2393 | 2445 | ||
2446 | engine.io@~3.3.1: | ||
2447 | version "3.3.2" | ||
2448 | resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-3.3.2.tgz#18cbc8b6f36e9461c5c0f81df2b830de16058a59" | ||
2449 | integrity sha512-AsaA9KG7cWPXWHp5FvHdDWY3AMWeZ8x+2pUVLcn71qE5AtAzgGbxuclOytygskw8XGmiQafTmnI9Bix3uihu2w== | ||
2450 | dependencies: | ||
2451 | accepts "~1.3.4" | ||
2452 | base64id "1.0.0" | ||
2453 | cookie "0.3.1" | ||
2454 | debug "~3.1.0" | ||
2455 | engine.io-parser "~2.1.0" | ||
2456 | ws "~6.1.0" | ||
2457 | |||
2394 | env-variable@0.0.x: | 2458 | env-variable@0.0.x: |
2395 | version "0.0.5" | 2459 | version "0.0.5" |
2396 | resolved "https://registry.yarnpkg.com/env-variable/-/env-variable-0.0.5.tgz#913dd830bef11e96a039c038d4130604eba37f88" | 2460 | resolved "https://registry.yarnpkg.com/env-variable/-/env-variable-0.0.5.tgz#913dd830bef11e96a039c038d4130604eba37f88" |
@@ -3389,6 +3453,13 @@ has-ansi@^2.0.0: | |||
3389 | dependencies: | 3453 | dependencies: |
3390 | ansi-regex "^2.0.0" | 3454 | ansi-regex "^2.0.0" |
3391 | 3455 | ||
3456 | has-binary2@~1.0.2: | ||
3457 | version "1.0.3" | ||
3458 | resolved "https://registry.yarnpkg.com/has-binary2/-/has-binary2-1.0.3.tgz#7776ac627f3ea77250cfc332dab7ddf5e4f5d11d" | ||
3459 | integrity sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw== | ||
3460 | dependencies: | ||
3461 | isarray "2.0.1" | ||
3462 | |||
3392 | has-binary@0.1.7: | 3463 | has-binary@0.1.7: |
3393 | version "0.1.7" | 3464 | version "0.1.7" |
3394 | resolved "https://registry.yarnpkg.com/has-binary/-/has-binary-0.1.7.tgz#68e61eb16210c9545a0a5cce06a873912fe1e68c" | 3465 | resolved "https://registry.yarnpkg.com/has-binary/-/has-binary-0.1.7.tgz#68e61eb16210c9545a0a5cce06a873912fe1e68c" |
@@ -4131,6 +4202,11 @@ isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: | |||
4131 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" | 4202 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" |
4132 | integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= | 4203 | integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= |
4133 | 4204 | ||
4205 | isarray@2.0.1: | ||
4206 | version "2.0.1" | ||
4207 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e" | ||
4208 | integrity sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4= | ||
4209 | |||
4134 | isexe@^2.0.0: | 4210 | isexe@^2.0.0: |
4135 | version "2.0.0" | 4211 | version "2.0.0" |
4136 | resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" | 4212 | resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" |
@@ -7542,6 +7618,11 @@ socket.io-adapter@0.5.0: | |||
7542 | debug "2.3.3" | 7618 | debug "2.3.3" |
7543 | socket.io-parser "2.3.1" | 7619 | socket.io-parser "2.3.1" |
7544 | 7620 | ||
7621 | socket.io-adapter@~1.1.0: | ||
7622 | version "1.1.1" | ||
7623 | resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz#2a805e8a14d6372124dd9159ad4502f8cb07f06b" | ||
7624 | integrity sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs= | ||
7625 | |||
7545 | socket.io-client@1.7.3: | 7626 | socket.io-client@1.7.3: |
7546 | version "1.7.3" | 7627 | version "1.7.3" |
7547 | resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-1.7.3.tgz#b30e86aa10d5ef3546601c09cde4765e381da377" | 7628 | resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-1.7.3.tgz#b30e86aa10d5ef3546601c09cde4765e381da377" |
@@ -7559,6 +7640,26 @@ socket.io-client@1.7.3: | |||
7559 | socket.io-parser "2.3.1" | 7640 | socket.io-parser "2.3.1" |
7560 | to-array "0.1.4" | 7641 | to-array "0.1.4" |
7561 | 7642 | ||
7643 | socket.io-client@2.2.0: | ||
7644 | version "2.2.0" | ||
7645 | resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.2.0.tgz#84e73ee3c43d5020ccc1a258faeeb9aec2723af7" | ||
7646 | integrity sha512-56ZrkTDbdTLmBIyfFYesgOxsjcLnwAKoN4CiPyTVkMQj3zTUh0QAx3GbvIvLpFEOvQWu92yyWICxB0u7wkVbYA== | ||
7647 | dependencies: | ||
7648 | backo2 "1.0.2" | ||
7649 | base64-arraybuffer "0.1.5" | ||
7650 | component-bind "1.0.0" | ||
7651 | component-emitter "1.2.1" | ||
7652 | debug "~3.1.0" | ||
7653 | engine.io-client "~3.3.1" | ||
7654 | has-binary2 "~1.0.2" | ||
7655 | has-cors "1.1.0" | ||
7656 | indexof "0.0.1" | ||
7657 | object-component "0.0.3" | ||
7658 | parseqs "0.0.5" | ||
7659 | parseuri "0.0.5" | ||
7660 | socket.io-parser "~3.3.0" | ||
7661 | to-array "0.1.4" | ||
7662 | |||
7562 | socket.io-parser@2.3.1: | 7663 | socket.io-parser@2.3.1: |
7563 | version "2.3.1" | 7664 | version "2.3.1" |
7564 | resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-2.3.1.tgz#dd532025103ce429697326befd64005fcfe5b4a0" | 7665 | resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-2.3.1.tgz#dd532025103ce429697326befd64005fcfe5b4a0" |
@@ -7569,6 +7670,15 @@ socket.io-parser@2.3.1: | |||
7569 | isarray "0.0.1" | 7670 | isarray "0.0.1" |
7570 | json3 "3.3.2" | 7671 | json3 "3.3.2" |
7571 | 7672 | ||
7673 | socket.io-parser@~3.3.0: | ||
7674 | version "3.3.0" | ||
7675 | resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.3.0.tgz#2b52a96a509fdf31440ba40fed6094c7d4f1262f" | ||
7676 | integrity sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng== | ||
7677 | dependencies: | ||
7678 | component-emitter "1.2.1" | ||
7679 | debug "~3.1.0" | ||
7680 | isarray "2.0.1" | ||
7681 | |||
7572 | socket.io@1.7.3: | 7682 | socket.io@1.7.3: |
7573 | version "1.7.3" | 7683 | version "1.7.3" |
7574 | resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-1.7.3.tgz#b8af9caba00949e568e369f1327ea9be9ea2461b" | 7684 | resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-1.7.3.tgz#b8af9caba00949e568e369f1327ea9be9ea2461b" |
@@ -7582,6 +7692,18 @@ socket.io@1.7.3: | |||
7582 | socket.io-client "1.7.3" | 7692 | socket.io-client "1.7.3" |
7583 | socket.io-parser "2.3.1" | 7693 | socket.io-parser "2.3.1" |
7584 | 7694 | ||
7695 | socket.io@^2.2.0: | ||
7696 | version "2.2.0" | ||
7697 | resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-2.2.0.tgz#f0f633161ef6712c972b307598ecd08c9b1b4d5b" | ||
7698 | integrity sha512-wxXrIuZ8AILcn+f1B4ez4hJTPG24iNgxBBDaJfT6MsyOhVYiTXWexGoPkd87ktJG8kQEcL/NBvRi64+9k4Kc0w== | ||
7699 | dependencies: | ||
7700 | debug "~4.1.0" | ||
7701 | engine.io "~3.3.1" | ||
7702 | has-binary2 "~1.0.2" | ||
7703 | socket.io-adapter "~1.1.0" | ||
7704 | socket.io-client "2.2.0" | ||
7705 | socket.io-parser "~3.3.0" | ||
7706 | |||
7585 | socks-proxy-agent@^3.0.1: | 7707 | socks-proxy-agent@^3.0.1: |
7586 | version "3.0.1" | 7708 | version "3.0.1" |
7587 | resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-3.0.1.tgz#2eae7cf8e2a82d34565761539a7f9718c5617659" | 7709 | resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-3.0.1.tgz#2eae7cf8e2a82d34565761539a7f9718c5617659" |
@@ -8954,7 +9076,7 @@ ws@1.1.2: | |||
8954 | options ">=0.0.5" | 9076 | options ">=0.0.5" |
8955 | ultron "1.0.x" | 9077 | ultron "1.0.x" |
8956 | 9078 | ||
8957 | ws@^6.0.0: | 9079 | ws@^6.0.0, ws@~6.1.0: |
8958 | version "6.1.2" | 9080 | version "6.1.2" |
8959 | resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.2.tgz#3cc7462e98792f0ac679424148903ded3b9c3ad8" | 9081 | resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.2.tgz#3cc7462e98792f0ac679424148903ded3b9c3ad8" |
8960 | integrity sha512-rfUqzvz0WxmSXtJpPMX2EeASXabOrSMk1ruMOV3JBTBjo4ac2lDjGGsbQSyxj8Odhw5fBib8ZKEjDNvgouNKYw== | 9082 | integrity sha512-rfUqzvz0WxmSXtJpPMX2EeASXabOrSMk1ruMOV3JBTBjo4ac2lDjGGsbQSyxj8Odhw5fBib8ZKEjDNvgouNKYw== |
@@ -9028,6 +9150,11 @@ xmlhttprequest-ssl@1.5.3: | |||
9028 | resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz#185a888c04eca46c3e4070d99f7b49de3528992d" | 9150 | resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz#185a888c04eca46c3e4070d99f7b49de3528992d" |
9029 | integrity sha1-GFqIjATspGw+QHDZn3tJ3jUomS0= | 9151 | integrity sha1-GFqIjATspGw+QHDZn3tJ3jUomS0= |
9030 | 9152 | ||
9153 | xmlhttprequest-ssl@~1.5.4: | ||
9154 | version "1.5.5" | ||
9155 | resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e" | ||
9156 | integrity sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4= | ||
9157 | |||
9031 | "xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1: | 9158 | "xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1: |
9032 | version "4.0.1" | 9159 | version "4.0.1" |
9033 | resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" | 9160 | resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" |