diff options
Diffstat (limited to 'shared')
-rw-r--r-- | shared/models/users/index.ts | 2 | ||||
-rw-r--r-- | shared/models/users/user-notification-setting.model.ts | 13 | ||||
-rw-r--r-- | shared/models/users/user-notification.model.ts | 47 | ||||
-rw-r--r-- | shared/models/users/user.model.ts | 2 | ||||
-rw-r--r-- | shared/utils/server/jobs.ts | 15 | ||||
-rw-r--r-- | shared/utils/socket/socket-io.ts | 13 | ||||
-rw-r--r-- | shared/utils/users/user-notifications.ts | 232 |
7 files changed, 318 insertions, 6 deletions
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 | } | ||