]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/lib/redis.ts
0478bfc895d78a8ce0300c2ff507f771ff0d6c36
[github/Chocobozzz/PeerTube.git] / server / lib / redis.ts
1 import express from 'express'
2 import { createClient } from 'redis'
3 import { exists } from '@server/helpers/custom-validators/misc'
4 import { logger } from '../helpers/logger'
5 import { generateRandomString } from '../helpers/utils'
6 import { CONFIG } from '../initializers/config'
7 import {
8 CONTACT_FORM_LIFETIME,
9 RESUMABLE_UPLOAD_SESSION_LIFETIME,
10 TRACKER_RATE_LIMITS,
11 USER_EMAIL_VERIFY_LIFETIME,
12 USER_PASSWORD_CREATE_LIFETIME,
13 USER_PASSWORD_RESET_LIFETIME,
14 VIEW_LIFETIME,
15 WEBSERVER
16 } from '../initializers/constants'
17
18 // Only used for typings
19 const redisClientWrapperForType = () => createClient<{}>()
20
21 class Redis {
22
23 private static instance: Redis
24 private initialized = false
25 private connected = false
26 private client: ReturnType<typeof redisClientWrapperForType>
27 private prefix: string
28
29 private constructor () {
30 }
31
32 init () {
33 // Already initialized
34 if (this.initialized === true) return
35 this.initialized = true
36
37 this.client = createClient(Redis.getRedisClientOptions())
38
39 this.client.connect()
40 .then(() => { this.connected = true })
41 .catch(err => {
42 logger.error('Cannot connect to redis', { err })
43 process.exit(-1)
44 })
45
46 this.client.on('error', err => {
47 logger.error('Error in Redis client.', { err })
48 process.exit(-1)
49 })
50
51 this.prefix = 'redis-' + WEBSERVER.HOST + '-'
52 }
53
54 static getRedisClientOptions () {
55 return Object.assign({},
56 CONFIG.REDIS.AUTH ? { password: CONFIG.REDIS.AUTH } : {},
57 (CONFIG.REDIS.DB) ? { db: CONFIG.REDIS.DB } : {},
58 (CONFIG.REDIS.HOSTNAME && CONFIG.REDIS.PORT)
59 ? { host: CONFIG.REDIS.HOSTNAME, port: CONFIG.REDIS.PORT }
60 : { path: CONFIG.REDIS.SOCKET }
61 )
62 }
63
64 getClient () {
65 return this.client
66 }
67
68 getPrefix () {
69 return this.prefix
70 }
71
72 isConnected () {
73 return this.connected
74 }
75
76 /* ************ Forgot password ************ */
77
78 async setResetPasswordVerificationString (userId: number) {
79 const generatedString = await generateRandomString(32)
80
81 await this.setValue(this.generateResetPasswordKey(userId), generatedString, USER_PASSWORD_RESET_LIFETIME)
82
83 return generatedString
84 }
85
86 async setCreatePasswordVerificationString (userId: number) {
87 const generatedString = await generateRandomString(32)
88
89 await this.setValue(this.generateResetPasswordKey(userId), generatedString, USER_PASSWORD_CREATE_LIFETIME)
90
91 return generatedString
92 }
93
94 async removePasswordVerificationString (userId: number) {
95 return this.removeValue(this.generateResetPasswordKey(userId))
96 }
97
98 async getResetPasswordLink (userId: number) {
99 return this.getValue(this.generateResetPasswordKey(userId))
100 }
101
102 /* ************ Email verification ************ */
103
104 async setVerifyEmailVerificationString (userId: number) {
105 const generatedString = await generateRandomString(32)
106
107 await this.setValue(this.generateVerifyEmailKey(userId), generatedString, USER_EMAIL_VERIFY_LIFETIME)
108
109 return generatedString
110 }
111
112 async getVerifyEmailLink (userId: number) {
113 return this.getValue(this.generateVerifyEmailKey(userId))
114 }
115
116 /* ************ Contact form per IP ************ */
117
118 async setContactFormIp (ip: string) {
119 return this.setValue(this.generateContactFormKey(ip), '1', CONTACT_FORM_LIFETIME)
120 }
121
122 async doesContactFormIpExist (ip: string) {
123 return this.exists(this.generateContactFormKey(ip))
124 }
125
126 /* ************ Views per IP ************ */
127
128 setIPVideoView (ip: string, videoUUID: string) {
129 return this.setValue(this.generateIPViewKey(ip, videoUUID), '1', VIEW_LIFETIME.VIEW)
130 }
131
132 setIPVideoViewer (ip: string, videoUUID: string) {
133 return this.setValue(this.generateIPViewerKey(ip, videoUUID), '1', VIEW_LIFETIME.VIEWER)
134 }
135
136 async doesVideoIPViewExist (ip: string, videoUUID: string) {
137 return this.exists(this.generateIPViewKey(ip, videoUUID))
138 }
139
140 async doesVideoIPViewerExist (ip: string, videoUUID: string) {
141 return this.exists(this.generateIPViewerKey(ip, videoUUID))
142 }
143
144 /* ************ Tracker IP block ************ */
145
146 setTrackerBlockIP (ip: string) {
147 return this.setValue(this.generateTrackerBlockIPKey(ip), '1', TRACKER_RATE_LIMITS.BLOCK_IP_LIFETIME)
148 }
149
150 async doesTrackerBlockIPExist (ip: string) {
151 return this.exists(this.generateTrackerBlockIPKey(ip))
152 }
153
154 /* ************ Video views stats ************ */
155
156 addVideoViewStats (videoId: number) {
157 const { videoKey, setKey } = this.generateVideoViewStatsKeys({ videoId })
158
159 return Promise.all([
160 this.addToSet(setKey, videoId.toString()),
161 this.increment(videoKey)
162 ])
163 }
164
165 async getVideoViewsStats (videoId: number, hour: number) {
166 const { videoKey } = this.generateVideoViewStatsKeys({ videoId, hour })
167
168 const valueString = await this.getValue(videoKey)
169 const valueInt = parseInt(valueString, 10)
170
171 if (isNaN(valueInt)) {
172 logger.error('Cannot get videos views stats of video %d in hour %d: views number is NaN (%s).', videoId, hour, valueString)
173 return undefined
174 }
175
176 return valueInt
177 }
178
179 async listVideosViewedForStats (hour: number) {
180 const { setKey } = this.generateVideoViewStatsKeys({ hour })
181
182 const stringIds = await this.getSet(setKey)
183 return stringIds.map(s => parseInt(s, 10))
184 }
185
186 deleteVideoViewsStats (videoId: number, hour: number) {
187 const { setKey, videoKey } = this.generateVideoViewStatsKeys({ videoId, hour })
188
189 return Promise.all([
190 this.deleteFromSet(setKey, videoId.toString()),
191 this.deleteKey(videoKey)
192 ])
193 }
194
195 /* ************ Local video views buffer ************ */
196
197 addLocalVideoView (videoId: number) {
198 const { videoKey, setKey } = this.generateLocalVideoViewsKeys(videoId)
199
200 return Promise.all([
201 this.addToSet(setKey, videoId.toString()),
202 this.increment(videoKey)
203 ])
204 }
205
206 async getLocalVideoViews (videoId: number) {
207 const { videoKey } = this.generateLocalVideoViewsKeys(videoId)
208
209 const valueString = await this.getValue(videoKey)
210 const valueInt = parseInt(valueString, 10)
211
212 if (isNaN(valueInt)) {
213 logger.error('Cannot get videos views of video %d: views number is NaN (%s).', videoId, valueString)
214 return undefined
215 }
216
217 return valueInt
218 }
219
220 async listLocalVideosViewed () {
221 const { setKey } = this.generateLocalVideoViewsKeys()
222
223 const stringIds = await this.getSet(setKey)
224 return stringIds.map(s => parseInt(s, 10))
225 }
226
227 deleteLocalVideoViews (videoId: number) {
228 const { setKey, videoKey } = this.generateLocalVideoViewsKeys(videoId)
229
230 return Promise.all([
231 this.deleteFromSet(setKey, videoId.toString()),
232 this.deleteKey(videoKey)
233 ])
234 }
235
236 /* ************ Resumable uploads final responses ************ */
237
238 setUploadSession (uploadId: string, response?: { video: { id: number, shortUUID: string, uuid: string } }) {
239 return this.setValue(
240 'resumable-upload-' + uploadId,
241 response
242 ? JSON.stringify(response)
243 : '',
244 RESUMABLE_UPLOAD_SESSION_LIFETIME
245 )
246 }
247
248 doesUploadSessionExist (uploadId: string) {
249 return this.exists('resumable-upload-' + uploadId)
250 }
251
252 async getUploadSession (uploadId: string) {
253 const value = await this.getValue('resumable-upload-' + uploadId)
254
255 return value
256 ? JSON.parse(value)
257 : ''
258 }
259
260 deleteUploadSession (uploadId: string) {
261 return this.deleteKey('resumable-upload-' + uploadId)
262 }
263
264 /* ************ Keys generation ************ */
265
266 private generateLocalVideoViewsKeys (videoId?: Number) {
267 return { setKey: `local-video-views-buffer`, videoKey: `local-video-views-buffer-${videoId}` }
268 }
269
270 private generateVideoViewStatsKeys (options: { videoId?: number, hour?: number }) {
271 const hour = exists(options.hour)
272 ? options.hour
273 : new Date().getHours()
274
275 return { setKey: `videos-view-h${hour}`, videoKey: `video-view-${options.videoId}-h${hour}` }
276 }
277
278 private generateResetPasswordKey (userId: number) {
279 return 'reset-password-' + userId
280 }
281
282 private generateVerifyEmailKey (userId: number) {
283 return 'verify-email-' + userId
284 }
285
286 private generateIPViewKey (ip: string, videoUUID: string) {
287 return `views-${videoUUID}-${ip}`
288 }
289
290 private generateIPViewerKey (ip: string, videoUUID: string) {
291 return `viewer-${videoUUID}-${ip}`
292 }
293
294 private generateTrackerBlockIPKey (ip: string) {
295 return `tracker-block-ip-${ip}`
296 }
297
298 private generateContactFormKey (ip: string) {
299 return 'contact-form-' + ip
300 }
301
302 /* ************ Redis helpers ************ */
303
304 private getValue (key: string) {
305 return this.client.get(this.prefix + key)
306 }
307
308 private getSet (key: string) {
309 return this.client.sMembers(this.prefix + key)
310 }
311
312 private addToSet (key: string, value: string) {
313 return this.client.sAdd(this.prefix + key, value)
314 }
315
316 private deleteFromSet (key: string, value: string) {
317 return this.client.sRem(this.prefix + key, value)
318 }
319
320 private deleteKey (key: string) {
321 return this.client.del(this.prefix + key)
322 }
323
324 private async setValue (key: string, value: string, expirationMilliseconds: number) {
325 const result = await this.client.set(this.prefix + key, value, { PX: expirationMilliseconds })
326
327 if (result !== 'OK') throw new Error('Redis set result is not OK.')
328 }
329
330 private removeValue (key: string) {
331 return this.client.del(this.prefix + key)
332 }
333
334 private getObject (key: string) {
335 return this.client.hGetAll(this.prefix + key)
336 }
337
338 private increment (key: string) {
339 return this.client.incr(this.prefix + key)
340 }
341
342 private exists (key: string) {
343 return this.client.exists(this.prefix + key)
344 }
345
346 static get Instance () {
347 return this.instance || (this.instance = new this())
348 }
349 }
350
351 // ---------------------------------------------------------------------------
352
353 export {
354 Redis
355 }