Enh #106 : Add an autoPlayVideo user attribute (#159)
[oweals/peertube.git] / server / models / account / user.ts
1 import * as Sequelize from 'sequelize'
2 import {
3   AllowNull,
4   BeforeCreate,
5   BeforeUpdate,
6   Column, CreatedAt,
7   DataType,
8   Default, DefaultScope,
9   HasMany,
10   HasOne,
11   Is,
12   IsEmail,
13   Model, Scopes,
14   Table, UpdatedAt
15 } from 'sequelize-typescript'
16 import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared'
17 import {
18   comparePassword,
19   cryptPassword
20 } from '../../helpers'
21 import {
22   isUserDisplayNSFWValid, isUserPasswordValid, isUserRoleValid, isUserUsernameValid,
23   isUserVideoQuotaValid, isUserAutoPlayVideoValid
24 } from '../../helpers/custom-validators/users'
25 import { OAuthTokenModel } from '../oauth/oauth-token'
26 import { getSort, throwIfNotValid } from '../utils'
27 import { VideoChannelModel } from '../video/video-channel'
28 import { AccountModel } from './account'
29
30 @DefaultScope({
31   include: [
32     {
33       model: () => AccountModel,
34       required: true
35     }
36   ]
37 })
38 @Scopes({
39   withVideoChannel: {
40     include: [
41       {
42         model: () => AccountModel,
43         required: true,
44         include: [ () => VideoChannelModel ]
45       }
46     ]
47   }
48 })
49 @Table({
50   tableName: 'user',
51   indexes: [
52     {
53       fields: [ 'username' ],
54       unique: true
55     },
56     {
57       fields: [ 'email' ],
58       unique: true
59     }
60   ]
61 })
62 export class UserModel extends Model<UserModel> {
63
64   @AllowNull(false)
65   @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password'))
66   @Column
67   password: string
68
69   @AllowNull(false)
70   @Is('UserPassword', value => throwIfNotValid(value, isUserUsernameValid, 'user name'))
71   @Column
72   username: string
73
74   @AllowNull(false)
75   @IsEmail
76   @Column(DataType.STRING(400))
77   email: string
78
79   @AllowNull(false)
80   @Default(false)
81   @Is('UserDisplayNSFW', value => throwIfNotValid(value, isUserDisplayNSFWValid, 'display NSFW boolean'))
82   @Column
83   displayNSFW: boolean
84
85   @AllowNull(false)
86   @Default(true)
87   @Is('UserAutoPlayVideo', value => throwIfNotValid(value, isUserAutoPlayVideoValid, 'auto play video boolean'))
88   @Column
89   autoPlayVideo: boolean
90
91   @AllowNull(false)
92   @Is('UserRole', value => throwIfNotValid(value, isUserRoleValid, 'role'))
93   @Column
94   role: number
95
96   @AllowNull(false)
97   @Is('UserVideoQuota', value => throwIfNotValid(value, isUserVideoQuotaValid, 'video quota'))
98   @Column(DataType.BIGINT)
99   videoQuota: number
100
101   @CreatedAt
102   createdAt: Date
103
104   @UpdatedAt
105   updatedAt: Date
106
107   @HasOne(() => AccountModel, {
108     foreignKey: 'userId',
109     onDelete: 'cascade'
110   })
111   Account: AccountModel
112
113   @HasMany(() => OAuthTokenModel, {
114     foreignKey: 'userId',
115     onDelete: 'cascade'
116   })
117   OAuthTokens: OAuthTokenModel[]
118
119   @BeforeCreate
120   @BeforeUpdate
121   static cryptPasswordIfNeeded (instance: UserModel) {
122     if (instance.changed('password')) {
123       return cryptPassword(instance.password)
124         .then(hash => {
125           instance.password = hash
126           return undefined
127         })
128     }
129   }
130
131   static countTotal () {
132     return this.count()
133   }
134
135   static getByUsername (username: string) {
136     const query = {
137       where: {
138         username: username
139       },
140       include: [ { model: AccountModel, required: true } ]
141     }
142
143     return UserModel.findOne(query)
144   }
145
146   static listForApi (start: number, count: number, sort: string) {
147     const query = {
148       offset: start,
149       limit: count,
150       order: [ getSort(sort) ]
151     }
152
153     return UserModel.findAndCountAll(query)
154       .then(({ rows, count }) => {
155         return {
156           data: rows,
157           total: count
158         }
159       })
160   }
161
162   static loadById (id: number) {
163     return UserModel.findById(id)
164   }
165
166   static loadByUsername (username: string) {
167     const query = {
168       where: {
169         username
170       }
171     }
172
173     return UserModel.findOne(query)
174   }
175
176   static loadByUsernameAndPopulateChannels (username: string) {
177     const query = {
178       where: {
179         username
180       }
181     }
182
183     return UserModel.scope('withVideoChannel').findOne(query)
184   }
185
186   static loadByUsernameOrEmail (username: string, email: string) {
187     const query = {
188       where: {
189         [ Sequelize.Op.or ]: [ { username }, { email } ]
190       }
191     }
192
193     return UserModel.findOne(query)
194   }
195
196   private static getOriginalVideoFileTotalFromUser (user: UserModel) {
197     // Don't use sequelize because we need to use a sub query
198     const query = 'SELECT SUM("size") AS "total" FROM ' +
199       '(SELECT MAX("videoFile"."size") AS "size" FROM "videoFile" ' +
200       'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' +
201       'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
202       'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
203       'INNER JOIN "user" ON "account"."userId" = "user"."id" ' +
204       'WHERE "user"."id" = $userId GROUP BY "video"."id") t'
205
206     const options = {
207       bind: { userId: user.id },
208       type: Sequelize.QueryTypes.SELECT
209     }
210     return UserModel.sequelize.query(query, options)
211       .then(([ { total } ]) => {
212         if (total === null) return 0
213
214         return parseInt(total, 10)
215       })
216   }
217
218   hasRight (right: UserRight) {
219     return hasUserRight(this.role, right)
220   }
221
222   isPasswordMatch (password: string) {
223     return comparePassword(password, this.password)
224   }
225
226   toFormattedJSON () {
227     const json = {
228       id: this.id,
229       username: this.username,
230       email: this.email,
231       displayNSFW: this.displayNSFW,
232       autoPlayVideo: this.autoPlayVideo,
233       role: this.role,
234       roleLabel: USER_ROLE_LABELS[ this.role ],
235       videoQuota: this.videoQuota,
236       createdAt: this.createdAt,
237       account: this.Account.toFormattedJSON()
238     }
239
240     if (Array.isArray(this.Account.VideoChannels) === true) {
241       json['videoChannels'] = this.Account.VideoChannels
242         .map(c => c.toFormattedJSON())
243         .sort((v1, v2) => {
244           if (v1.createdAt < v2.createdAt) return -1
245           if (v1.createdAt === v2.createdAt) return 0
246
247           return 1
248         })
249     }
250
251     return json
252   }
253
254   isAbleToUploadVideo (videoFile: Express.Multer.File) {
255     if (this.videoQuota === -1) return Promise.resolve(true)
256
257     return UserModel.getOriginalVideoFileTotalFromUser(this)
258       .then(totalBytes => {
259         return (videoFile.size + totalBytes) < this.videoQuota
260       })
261   }
262 }