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