1 import * as express from 'express'
2 import { createClient, RedisClient } from 'redis'
3 import { logger } from '../helpers/logger'
4 import { generateRandomString } from '../helpers/utils'
7 USER_EMAIL_VERIFY_LIFETIME,
8 USER_PASSWORD_RESET_LIFETIME,
11 } from '../initializers/constants'
12 import { CONFIG } from '../initializers/config'
22 private static instance: Redis
23 private initialized = false
24 private client: RedisClient
25 private prefix: string
27 private constructor () {}
30 // Already initialized
31 if (this.initialized === true) return
32 this.initialized = true
34 this.client = createClient(Redis.getRedisClient())
36 this.client.on('error', err => {
37 logger.error('Error in Redis client.', { err })
41 if (CONFIG.REDIS.AUTH) {
42 this.client.auth(CONFIG.REDIS.AUTH)
45 this.prefix = 'redis-' + WEBSERVER.HOST + '-'
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 }
58 /************* Forgot password *************/
60 async setResetPasswordVerificationString (userId: number) {
61 const generatedString = await generateRandomString(32)
63 await this.setValue(this.generateResetPasswordKey(userId), generatedString, USER_PASSWORD_RESET_LIFETIME)
65 return generatedString
68 async getResetPasswordLink (userId: number) {
69 return this.getValue(this.generateResetPasswordKey(userId))
72 /************* Email verification *************/
74 async setVerifyEmailVerificationString (userId: number) {
75 const generatedString = await generateRandomString(32)
77 await this.setValue(this.generateVerifyEmailKey(userId), generatedString, USER_EMAIL_VERIFY_LIFETIME)
79 return generatedString
82 async getVerifyEmailLink (userId: number) {
83 return this.getValue(this.generateVerifyEmailKey(userId))
86 /************* Contact form per IP *************/
88 async setContactFormIp (ip: string) {
89 return this.setValue(this.generateContactFormKey(ip), '1', CONTACT_FORM_LIFETIME)
92 async doesContactFormIpExist (ip: string) {
93 return this.exists(this.generateContactFormKey(ip))
96 /************* Views per IP *************/
98 setIPVideoView (ip: string, videoUUID: string) {
99 return this.setValue(this.generateViewKey(ip, videoUUID), '1', VIDEO_VIEW_LIFETIME)
102 async doesVideoIPViewExist (ip: string, videoUUID: string) {
103 return this.exists(this.generateViewKey(ip, videoUUID))
106 /************* API cache *************/
108 async getCachedRoute (req: express.Request) {
109 const cached = await this.getObject(this.generateCachedRouteKey(req))
111 return cached as CachedRoute
114 setCachedRoute (req: express.Request, body: any, lifetime: number, contentType?: string, statusCode?: number) {
115 const cached: CachedRoute = Object.assign({}, {
116 body: body.toString()
118 (contentType) ? { contentType } : null,
119 (statusCode) ? { statusCode: statusCode.toString() } : null
122 return this.setObject(this.generateCachedRouteKey(req), cached, lifetime)
125 /************* Video views *************/
127 addVideoView (videoId: number) {
128 const keyIncr = this.generateVideoViewKey(videoId)
129 const keySet = this.generateVideosViewKey()
132 this.addToSet(keySet, videoId.toString()),
133 this.increment(keyIncr)
137 async getVideoViews (videoId: number, hour: number) {
138 const key = this.generateVideoViewKey(videoId, hour)
140 const valueString = await this.getValue(key)
141 const valueInt = parseInt(valueString, 10)
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)
151 async getVideosIdViewed (hour: number) {
152 const key = this.generateVideosViewKey(hour)
154 const stringIds = await this.getSet(key)
155 return stringIds.map(s => parseInt(s, 10))
158 deleteVideoViews (videoId: number, hour: number) {
159 const keySet = this.generateVideosViewKey(hour)
160 const keyIncr = this.generateVideoViewKey(videoId, hour)
163 this.deleteFromSet(keySet, videoId.toString()),
164 this.deleteKey(keyIncr)
168 /************* Keys generation *************/
170 generateCachedRouteKey (req: express.Request) {
171 return req.method + '-' + req.originalUrl
174 private generateVideosViewKey (hour?: number) {
175 if (!hour) hour = new Date().getHours()
177 return `videos-view-h${hour}`
180 private generateVideoViewKey (videoId: number, hour?: number) {
181 if (!hour) hour = new Date().getHours()
183 return `video-view-${videoId}-h${hour}`
186 private generateResetPasswordKey (userId: number) {
187 return 'reset-password-' + userId
190 private generateVerifyEmailKey (userId: number) {
191 return 'verify-email-' + userId
194 private generateViewKey (ip: string, videoUUID: string) {
195 return `views-${videoUUID}-${ip}`
198 private generateContactFormKey (ip: string) {
199 return 'contact-form-' + ip
202 /************* Redis helpers *************/
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)
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)
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())
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())
236 private deleteKey (key: string) {
237 return new Promise<void>((res, rej) => {
238 this.client.del(this.prefix + key, err => err ? rej(err) : res())
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())
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)
253 if (ok !== 'OK') return rej(new Error('Redis set result is not OK.'))
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.'))
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.'))
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)
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)
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)
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)
311 return res(existsNumber === 1)
316 static get Instance () {
317 return this.instance || (this.instance = new this())
321 // ---------------------------------------------------------------------------