f77d0b62c20ba22d3a92c01da5e097b7b35cf8c1
[oweals/peertube.git] / server / lib / redis.ts
1 import * as express from 'express'
2 import { createClient, RedisClient } from 'redis'
3 import { logger } from '../helpers/logger'
4 import { generateRandomString } from '../helpers/utils'
5 import {
6   CONTACT_FORM_LIFETIME,
7   USER_EMAIL_VERIFY_LIFETIME,
8   USER_PASSWORD_RESET_LIFETIME,
9   VIDEO_VIEW_LIFETIME,
10   WEBSERVER
11 } from '../initializers/constants'
12 import { CONFIG } from '../initializers/config'
13
14 type CachedRoute = {
15   body: string,
16   contentType?: string
17   statusCode?: string
18 }
19
20 class Redis {
21
22   private static instance: Redis
23   private initialized = false
24   private client: RedisClient
25   private prefix: string
26
27   private constructor () {}
28
29   init () {
30     // Already initialized
31     if (this.initialized === true) return
32     this.initialized = true
33
34     this.client = createClient(Redis.getRedisClientOptions())
35
36     this.client.on('error', err => {
37       logger.error('Error in Redis client.', { err })
38       process.exit(-1)
39     })
40
41     if (CONFIG.REDIS.AUTH) {
42       this.client.auth(CONFIG.REDIS.AUTH)
43     }
44
45     this.prefix = 'redis-' + WEBSERVER.HOST + '-'
46   }
47
48   static getRedisClientOptions () {
49     return Object.assign({},
50       (CONFIG.REDIS.AUTH && CONFIG.REDIS.AUTH != null) ? { password: CONFIG.REDIS.AUTH } : {},
51       (CONFIG.REDIS.DB) ? { db: CONFIG.REDIS.DB } : {},
52       (CONFIG.REDIS.HOSTNAME && CONFIG.REDIS.PORT) ?
53       { host: CONFIG.REDIS.HOSTNAME, port: CONFIG.REDIS.PORT } :
54       { path: CONFIG.REDIS.SOCKET }
55     )
56   }
57
58   getClient () {
59     return this.client
60   }
61
62   getPrefix () {
63     return this.prefix
64   }
65
66   /************* Forgot password *************/
67
68   async setResetPasswordVerificationString (userId: number) {
69     const generatedString = await generateRandomString(32)
70
71     await this.setValue(this.generateResetPasswordKey(userId), generatedString, USER_PASSWORD_RESET_LIFETIME)
72
73     return generatedString
74   }
75
76   async getResetPasswordLink (userId: number) {
77     return this.getValue(this.generateResetPasswordKey(userId))
78   }
79
80   /************* Email verification *************/
81
82   async setVerifyEmailVerificationString (userId: number) {
83     const generatedString = await generateRandomString(32)
84
85     await this.setValue(this.generateVerifyEmailKey(userId), generatedString, USER_EMAIL_VERIFY_LIFETIME)
86
87     return generatedString
88   }
89
90   async getVerifyEmailLink (userId: number) {
91     return this.getValue(this.generateVerifyEmailKey(userId))
92   }
93
94   /************* Contact form per IP *************/
95
96   async setContactFormIp (ip: string) {
97     return this.setValue(this.generateContactFormKey(ip), '1', CONTACT_FORM_LIFETIME)
98   }
99
100   async doesContactFormIpExist (ip: string) {
101     return this.exists(this.generateContactFormKey(ip))
102   }
103
104   /************* Views per IP *************/
105
106   setIPVideoView (ip: string, videoUUID: string) {
107     return this.setValue(this.generateViewKey(ip, videoUUID), '1', VIDEO_VIEW_LIFETIME)
108   }
109
110   async doesVideoIPViewExist (ip: string, videoUUID: string) {
111     return this.exists(this.generateViewKey(ip, videoUUID))
112   }
113
114   /************* API cache *************/
115
116   async getCachedRoute (req: express.Request) {
117     const cached = await this.getObject(this.generateCachedRouteKey(req))
118
119     return cached as CachedRoute
120   }
121
122   setCachedRoute (req: express.Request, body: any, lifetime: number, contentType?: string, statusCode?: number) {
123     const cached: CachedRoute = Object.assign({}, {
124       body: body.toString()
125     },
126     (contentType) ? { contentType } : null,
127     (statusCode) ? { statusCode: statusCode.toString() } : null
128     )
129
130     return this.setObject(this.generateCachedRouteKey(req), cached, lifetime)
131   }
132
133   /************* Video views *************/
134
135   addVideoView (videoId: number) {
136     const keyIncr = this.generateVideoViewKey(videoId)
137     const keySet = this.generateVideosViewKey()
138
139     return Promise.all([
140       this.addToSet(keySet, videoId.toString()),
141       this.increment(keyIncr)
142     ])
143   }
144
145   async getVideoViews (videoId: number, hour: number) {
146     const key = this.generateVideoViewKey(videoId, hour)
147
148     const valueString = await this.getValue(key)
149     const valueInt = parseInt(valueString, 10)
150
151     if (isNaN(valueInt)) {
152       logger.error('Cannot get videos views of video %d in hour %d: views number is NaN (%s).', videoId, hour, valueString)
153       return undefined
154     }
155
156     return valueInt
157   }
158
159   async getVideosIdViewed (hour: number) {
160     const key = this.generateVideosViewKey(hour)
161
162     const stringIds = await this.getSet(key)
163     return stringIds.map(s => parseInt(s, 10))
164   }
165
166   deleteVideoViews (videoId: number, hour: number) {
167     const keySet = this.generateVideosViewKey(hour)
168     const keyIncr = this.generateVideoViewKey(videoId, hour)
169
170     return Promise.all([
171       this.deleteFromSet(keySet, videoId.toString()),
172       this.deleteKey(keyIncr)
173     ])
174   }
175
176   /************* Keys generation *************/
177
178   generateCachedRouteKey (req: express.Request) {
179     return req.method + '-' + req.originalUrl
180   }
181
182   private generateVideosViewKey (hour?: number) {
183     if (!hour) hour = new Date().getHours()
184
185     return `videos-view-h${hour}`
186   }
187
188   private generateVideoViewKey (videoId: number, hour?: number) {
189     if (!hour) hour = new Date().getHours()
190
191     return `video-view-${videoId}-h${hour}`
192   }
193
194   private generateResetPasswordKey (userId: number) {
195     return 'reset-password-' + userId
196   }
197
198   private generateVerifyEmailKey (userId: number) {
199     return 'verify-email-' + userId
200   }
201
202   private generateViewKey (ip: string, videoUUID: string) {
203     return `views-${videoUUID}-${ip}`
204   }
205
206   private generateContactFormKey (ip: string) {
207     return 'contact-form-' + ip
208   }
209
210   /************* Redis helpers *************/
211
212   private getValue (key: string) {
213     return new Promise<string>((res, rej) => {
214       this.client.get(this.prefix + key, (err, value) => {
215         if (err) return rej(err)
216
217         return res(value)
218       })
219     })
220   }
221
222   private getSet (key: string) {
223     return new Promise<string[]>((res, rej) => {
224       this.client.smembers(this.prefix + key, (err, value) => {
225         if (err) return rej(err)
226
227         return res(value)
228       })
229     })
230   }
231
232   private addToSet (key: string, value: string) {
233     return new Promise<string[]>((res, rej) => {
234       this.client.sadd(this.prefix + key, value, err => err ? rej(err) : res())
235     })
236   }
237
238   private deleteFromSet (key: string, value: string) {
239     return new Promise<void>((res, rej) => {
240       this.client.srem(this.prefix + key, value, err => err ? rej(err) : res())
241     })
242   }
243
244   private deleteKey (key: string) {
245     return new Promise<void>((res, rej) => {
246       this.client.del(this.prefix + key, err => err ? rej(err) : res())
247     })
248   }
249
250   private deleteFieldInHash (key: string, field: string) {
251     return new Promise<void>((res, rej) => {
252       this.client.hdel(this.prefix + key, field, err => err ? rej(err) : res())
253     })
254   }
255
256   private setValue (key: string, value: string, expirationMilliseconds: number) {
257     return new Promise<void>((res, rej) => {
258       this.client.set(this.prefix + key, value, 'PX', expirationMilliseconds, (err, ok) => {
259         if (err) return rej(err)
260
261         if (ok !== 'OK') return rej(new Error('Redis set result is not OK.'))
262
263         return res()
264       })
265     })
266   }
267
268   private setObject (key: string, obj: { [ id: string ]: string }, expirationMilliseconds: number) {
269     return new Promise<void>((res, rej) => {
270       this.client.hmset(this.prefix + key, obj, (err, ok) => {
271         if (err) return rej(err)
272         if (!ok) return rej(new Error('Redis mset result is not OK.'))
273
274         this.client.pexpire(this.prefix + key, expirationMilliseconds, (err, ok) => {
275           if (err) return rej(err)
276           if (!ok) return rej(new Error('Redis expiration result is not OK.'))
277
278           return res()
279         })
280       })
281     })
282   }
283
284   private getObject (key: string) {
285     return new Promise<{ [ id: string ]: string }>((res, rej) => {
286       this.client.hgetall(this.prefix + key, (err, value) => {
287         if (err) return rej(err)
288
289         return res(value)
290       })
291     })
292   }
293
294   private setValueInHash (key: string, field: string, value: string) {
295     return new Promise<void>((res, rej) => {
296       this.client.hset(this.prefix + key, field, value, (err) => {
297         if (err) return rej(err)
298
299         return res()
300       })
301     })
302   }
303
304   private increment (key: string) {
305     return new Promise<number>((res, rej) => {
306       this.client.incr(this.prefix + key, (err, value) => {
307         if (err) return rej(err)
308
309         return res(value)
310       })
311     })
312   }
313
314   private exists (key: string) {
315     return new Promise<boolean>((res, rej) => {
316       this.client.exists(this.prefix + key, (err, existsNumber) => {
317         if (err) return rej(err)
318
319         return res(existsNumber === 1)
320       })
321     })
322   }
323
324   static get Instance () {
325     return this.instance || (this.instance = new this())
326   }
327 }
328
329 // ---------------------------------------------------------------------------
330
331 export {
332   Redis
333 }