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'
9 RESUMABLE_UPLOAD_SESSION_LIFETIME,
11 USER_EMAIL_VERIFY_LIFETIME,
12 USER_PASSWORD_CREATE_LIFETIME,
13 USER_PASSWORD_RESET_LIFETIME,
16 } from '../initializers/constants'
18 // Only used for typings
19 const redisClientWrapperForType = () => createClient<{}>()
23 private static instance: Redis
24 private initialized = false
25 private connected = false
26 private client: ReturnType<typeof redisClientWrapperForType>
27 private prefix: string
29 private constructor () {
33 // Already initialized
34 if (this.initialized === true) return
35 this.initialized = true
37 this.client = createClient(Redis.getRedisClientOptions())
40 .then(() => { this.connected = true })
42 logger.error('Cannot connect to redis', { err })
46 this.client.on('error', err => {
47 logger.error('Error in Redis client.', { err })
51 this.prefix = 'redis-' + WEBSERVER.HOST + '-'
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 }
76 /* ************ Forgot password ************ */
78 async setResetPasswordVerificationString (userId: number) {
79 const generatedString = await generateRandomString(32)
81 await this.setValue(this.generateResetPasswordKey(userId), generatedString, USER_PASSWORD_RESET_LIFETIME)
83 return generatedString
86 async setCreatePasswordVerificationString (userId: number) {
87 const generatedString = await generateRandomString(32)
89 await this.setValue(this.generateResetPasswordKey(userId), generatedString, USER_PASSWORD_CREATE_LIFETIME)
91 return generatedString
94 async removePasswordVerificationString (userId: number) {
95 return this.removeValue(this.generateResetPasswordKey(userId))
98 async getResetPasswordLink (userId: number) {
99 return this.getValue(this.generateResetPasswordKey(userId))
102 /* ************ Email verification ************ */
104 async setVerifyEmailVerificationString (userId: number) {
105 const generatedString = await generateRandomString(32)
107 await this.setValue(this.generateVerifyEmailKey(userId), generatedString, USER_EMAIL_VERIFY_LIFETIME)
109 return generatedString
112 async getVerifyEmailLink (userId: number) {
113 return this.getValue(this.generateVerifyEmailKey(userId))
116 /* ************ Contact form per IP ************ */
118 async setContactFormIp (ip: string) {
119 return this.setValue(this.generateContactFormKey(ip), '1', CONTACT_FORM_LIFETIME)
122 async doesContactFormIpExist (ip: string) {
123 return this.exists(this.generateContactFormKey(ip))
126 /* ************ Views per IP ************ */
128 setIPVideoView (ip: string, videoUUID: string) {
129 return this.setValue(this.generateIPViewKey(ip, videoUUID), '1', VIEW_LIFETIME.VIEW)
132 setIPVideoViewer (ip: string, videoUUID: string) {
133 return this.setValue(this.generateIPViewerKey(ip, videoUUID), '1', VIEW_LIFETIME.VIEWER)
136 async doesVideoIPViewExist (ip: string, videoUUID: string) {
137 return this.exists(this.generateIPViewKey(ip, videoUUID))
140 async doesVideoIPViewerExist (ip: string, videoUUID: string) {
141 return this.exists(this.generateIPViewerKey(ip, videoUUID))
144 /* ************ Tracker IP block ************ */
146 setTrackerBlockIP (ip: string) {
147 return this.setValue(this.generateTrackerBlockIPKey(ip), '1', TRACKER_RATE_LIMITS.BLOCK_IP_LIFETIME)
150 async doesTrackerBlockIPExist (ip: string) {
151 return this.exists(this.generateTrackerBlockIPKey(ip))
154 /* ************ Video views stats ************ */
156 addVideoViewStats (videoId: number) {
157 const { videoKey, setKey } = this.generateVideoViewStatsKeys({ videoId })
160 this.addToSet(setKey, videoId.toString()),
161 this.increment(videoKey)
165 async getVideoViewsStats (videoId: number, hour: number) {
166 const { videoKey } = this.generateVideoViewStatsKeys({ videoId, hour })
168 const valueString = await this.getValue(videoKey)
169 const valueInt = parseInt(valueString, 10)
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)
179 async listVideosViewedForStats (hour: number) {
180 const { setKey } = this.generateVideoViewStatsKeys({ hour })
182 const stringIds = await this.getSet(setKey)
183 return stringIds.map(s => parseInt(s, 10))
186 deleteVideoViewsStats (videoId: number, hour: number) {
187 const { setKey, videoKey } = this.generateVideoViewStatsKeys({ videoId, hour })
190 this.deleteFromSet(setKey, videoId.toString()),
191 this.deleteKey(videoKey)
195 /* ************ Local video views buffer ************ */
197 addLocalVideoView (videoId: number) {
198 const { videoKey, setKey } = this.generateLocalVideoViewsKeys(videoId)
201 this.addToSet(setKey, videoId.toString()),
202 this.increment(videoKey)
206 async getLocalVideoViews (videoId: number) {
207 const { videoKey } = this.generateLocalVideoViewsKeys(videoId)
209 const valueString = await this.getValue(videoKey)
210 const valueInt = parseInt(valueString, 10)
212 if (isNaN(valueInt)) {
213 logger.error('Cannot get videos views of video %d: views number is NaN (%s).', videoId, valueString)
220 async listLocalVideosViewed () {
221 const { setKey } = this.generateLocalVideoViewsKeys()
223 const stringIds = await this.getSet(setKey)
224 return stringIds.map(s => parseInt(s, 10))
227 deleteLocalVideoViews (videoId: number) {
228 const { setKey, videoKey } = this.generateLocalVideoViewsKeys(videoId)
231 this.deleteFromSet(setKey, videoId.toString()),
232 this.deleteKey(videoKey)
236 /* ************ Resumable uploads final responses ************ */
238 setUploadSession (uploadId: string, response?: { video: { id: number, shortUUID: string, uuid: string } }) {
239 return this.setValue(
240 'resumable-upload-' + uploadId,
242 ? JSON.stringify(response)
244 RESUMABLE_UPLOAD_SESSION_LIFETIME
248 doesUploadSessionExist (uploadId: string) {
249 return this.exists('resumable-upload-' + uploadId)
252 async getUploadSession (uploadId: string) {
253 const value = await this.getValue('resumable-upload-' + uploadId)
260 deleteUploadSession (uploadId: string) {
261 return this.deleteKey('resumable-upload-' + uploadId)
264 /* ************ Keys generation ************ */
266 private generateLocalVideoViewsKeys (videoId?: Number) {
267 return { setKey: `local-video-views-buffer`, videoKey: `local-video-views-buffer-${videoId}` }
270 private generateVideoViewStatsKeys (options: { videoId?: number, hour?: number }) {
271 const hour = exists(options.hour)
273 : new Date().getHours()
275 return { setKey: `videos-view-h${hour}`, videoKey: `video-view-${options.videoId}-h${hour}` }
278 private generateResetPasswordKey (userId: number) {
279 return 'reset-password-' + userId
282 private generateVerifyEmailKey (userId: number) {
283 return 'verify-email-' + userId
286 private generateIPViewKey (ip: string, videoUUID: string) {
287 return `views-${videoUUID}-${ip}`
290 private generateIPViewerKey (ip: string, videoUUID: string) {
291 return `viewer-${videoUUID}-${ip}`
294 private generateTrackerBlockIPKey (ip: string) {
295 return `tracker-block-ip-${ip}`
298 private generateContactFormKey (ip: string) {
299 return 'contact-form-' + ip
302 /* ************ Redis helpers ************ */
304 private getValue (key: string) {
305 return this.client.get(this.prefix + key)
308 private getSet (key: string) {
309 return this.client.sMembers(this.prefix + key)
312 private addToSet (key: string, value: string) {
313 return this.client.sAdd(this.prefix + key, value)
316 private deleteFromSet (key: string, value: string) {
317 return this.client.sRem(this.prefix + key, value)
320 private deleteKey (key: string) {
321 return this.client.del(this.prefix + key)
324 private async setValue (key: string, value: string, expirationMilliseconds: number) {
325 const result = await this.client.set(this.prefix + key, value, { PX: expirationMilliseconds })
327 if (result !== 'OK') throw new Error('Redis set result is not OK.')
330 private removeValue (key: string) {
331 return this.client.del(this.prefix + key)
334 private getObject (key: string) {
335 return this.client.hGetAll(this.prefix + key)
338 private increment (key: string) {
339 return this.client.incr(this.prefix + key)
342 private exists (key: string) {
343 return this.client.exists(this.prefix + key)
346 static get Instance () {
347 return this.instance || (this.instance = new this())
351 // ---------------------------------------------------------------------------