]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - server/models/account/user.ts
WIP plugins: load theme on client side
[github/Chocobozzz/PeerTube.git] / server / models / account / user.ts
index 3705947c067b485153ad821f7395c12d8a584bff..6f0b0e00f4f477599ce47134318e768d3ecf75bc 100644 (file)
-import * as Sequelize from 'sequelize'
+import { FindOptions, literal, Op, QueryTypes } from 'sequelize'
+import {
+  AfterDestroy,
+  AfterUpdate,
+  AllowNull,
+  BeforeCreate,
+  BeforeUpdate,
+  Column,
+  CreatedAt,
+  DataType,
+  Default,
+  DefaultScope,
+  HasMany,
+  HasOne,
+  Is,
+  IsEmail,
+  Model,
+  Scopes,
+  Table,
+  UpdatedAt
+} from 'sequelize-typescript'
 import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared'
+import { User, UserRole } from '../../../shared/models/users'
 import {
-  comparePassword,
-  cryptPassword,
-  isUserDisplayNSFWValid,
+  isUserAdminFlagsValid,
+  isUserAutoPlayVideoValid,
+  isUserBlockedReasonValid,
+  isUserBlockedValid,
+  isUserEmailVerifiedValid,
+  isUserNSFWPolicyValid,
   isUserPasswordValid,
   isUserRoleValid,
   isUserUsernameValid,
-  isUserVideoQuotaValid
-} from '../../helpers'
-import { addMethodsToModel, getSort } from '../utils'
-import { UserAttributes, UserInstance, UserMethods } from './user-interface'
-
-let User: Sequelize.Model<UserInstance, UserAttributes>
-let isPasswordMatch: UserMethods.IsPasswordMatch
-let hasRight: UserMethods.HasRight
-let toFormattedJSON: UserMethods.ToFormattedJSON
-let countTotal: UserMethods.CountTotal
-let getByUsername: UserMethods.GetByUsername
-let listForApi: UserMethods.ListForApi
-let loadById: UserMethods.LoadById
-let loadByUsername: UserMethods.LoadByUsername
-let loadByUsernameAndPopulateChannels: UserMethods.LoadByUsernameAndPopulateChannels
-let loadByUsernameOrEmail: UserMethods.LoadByUsernameOrEmail
-let isAbleToUploadVideo: UserMethods.IsAbleToUploadVideo
-
-export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
-  User = sequelize.define<UserInstance, UserAttributes>('User',
+  isUserVideoLanguages,
+  isUserVideoQuotaDailyValid,
+  isUserVideoQuotaValid,
+  isUserVideosHistoryEnabledValid,
+  isUserWebTorrentEnabledValid
+} from '../../helpers/custom-validators/users'
+import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto'
+import { OAuthTokenModel } from '../oauth/oauth-token'
+import { getSort, throwIfNotValid } from '../utils'
+import { VideoChannelModel } from '../video/video-channel'
+import { AccountModel } from './account'
+import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type'
+import { values } from 'lodash'
+import { DEFAULT_THEME_NAME, DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants'
+import { clearCacheByUserId } from '../../lib/oauth-model'
+import { UserNotificationSettingModel } from './user-notification-setting'
+import { VideoModel } from '../video/video'
+import { ActorModel } from '../activitypub/actor'
+import { ActorFollowModel } from '../activitypub/actor-follow'
+import { VideoImportModel } from '../video/video-import'
+import { UserAdminFlag } from '../../../shared/models/users/user-flag.model'
+import { isThemeValid } from '../../helpers/custom-validators/plugins'
+import { getThemeOrDefault } from '../../lib/plugins/theme-utils'
+
+enum ScopeNames {
+  WITH_VIDEO_CHANNEL = 'WITH_VIDEO_CHANNEL'
+}
+
+@DefaultScope(() => ({
+  include: [
     {
-      password: {
-        type: DataTypes.STRING,
-        allowNull: false,
-        validate: {
-          passwordValid: value => {
-            const res = isUserPasswordValid(value)
-            if (res === false) throw new Error('Password not valid.')
-          }
-        }
-      },
-      username: {
-        type: DataTypes.STRING,
-        allowNull: false,
-        validate: {
-          usernameValid: value => {
-            const res = isUserUsernameValid(value)
-            if (res === false) throw new Error('Username not valid.')
-          }
-        }
-      },
-      email: {
-        type: DataTypes.STRING(400),
-        allowNull: false,
-        validate: {
-          isEmail: true
-        }
+      model: AccountModel,
+      required: true
+    },
+    {
+      model: UserNotificationSettingModel,
+      required: true
+    }
+  ]
+}))
+@Scopes(() => ({
+  [ScopeNames.WITH_VIDEO_CHANNEL]: {
+    include: [
+      {
+        model: AccountModel,
+        required: true,
+        include: [ VideoChannelModel ]
       },
-      displayNSFW: {
-        type: DataTypes.BOOLEAN,
-        allowNull: false,
-        defaultValue: false,
-        validate: {
-          nsfwValid: value => {
-            const res = isUserDisplayNSFWValid(value)
-            if (res === false) throw new Error('Display NSFW is not valid.')
+      {
+        model: UserNotificationSettingModel,
+        required: true
+      }
+    ]
+  }
+}))
+@Table({
+  tableName: 'user',
+  indexes: [
+    {
+      fields: [ 'username' ],
+      unique: true
+    },
+    {
+      fields: [ 'email' ],
+      unique: true
+    }
+  ]
+})
+export class UserModel extends Model<UserModel> {
+
+  @AllowNull(false)
+  @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password'))
+  @Column
+  password: string
+
+  @AllowNull(false)
+  @Is('UserPassword', value => throwIfNotValid(value, isUserUsernameValid, 'user name'))
+  @Column
+  username: string
+
+  @AllowNull(false)
+  @IsEmail
+  @Column(DataType.STRING(400))
+  email: string
+
+  @AllowNull(true)
+  @IsEmail
+  @Column(DataType.STRING(400))
+  pendingEmail: string
+
+  @AllowNull(true)
+  @Default(null)
+  @Is('UserEmailVerified', value => throwIfNotValid(value, isUserEmailVerifiedValid, 'email verified boolean', true))
+  @Column
+  emailVerified: boolean
+
+  @AllowNull(false)
+  @Is('UserNSFWPolicy', value => throwIfNotValid(value, isUserNSFWPolicyValid, 'NSFW policy'))
+  @Column(DataType.ENUM(...values(NSFW_POLICY_TYPES)))
+  nsfwPolicy: NSFWPolicyType
+
+  @AllowNull(false)
+  @Default(true)
+  @Is('UserWebTorrentEnabled', value => throwIfNotValid(value, isUserWebTorrentEnabledValid, 'WebTorrent enabled'))
+  @Column
+  webTorrentEnabled: boolean
+
+  @AllowNull(false)
+  @Default(true)
+  @Is('UserVideosHistoryEnabled', value => throwIfNotValid(value, isUserVideosHistoryEnabledValid, 'Videos history enabled'))
+  @Column
+  videosHistoryEnabled: boolean
+
+  @AllowNull(false)
+  @Default(true)
+  @Is('UserAutoPlayVideo', value => throwIfNotValid(value, isUserAutoPlayVideoValid, 'auto play video boolean'))
+  @Column
+  autoPlayVideo: boolean
+
+  @AllowNull(true)
+  @Default(null)
+  @Is('UserVideoLanguages', value => throwIfNotValid(value, isUserVideoLanguages, 'video languages'))
+  @Column(DataType.ARRAY(DataType.STRING))
+  videoLanguages: string[]
+
+  @AllowNull(false)
+  @Default(UserAdminFlag.NONE)
+  @Is('UserAdminFlags', value => throwIfNotValid(value, isUserAdminFlagsValid, 'user admin flags'))
+  @Column
+  adminFlags?: UserAdminFlag
+
+  @AllowNull(false)
+  @Default(false)
+  @Is('UserBlocked', value => throwIfNotValid(value, isUserBlockedValid, 'blocked boolean'))
+  @Column
+  blocked: boolean
+
+  @AllowNull(true)
+  @Default(null)
+  @Is('UserBlockedReason', value => throwIfNotValid(value, isUserBlockedReasonValid, 'blocked reason', true))
+  @Column
+  blockedReason: string
+
+  @AllowNull(false)
+  @Is('UserRole', value => throwIfNotValid(value, isUserRoleValid, 'role'))
+  @Column
+  role: number
+
+  @AllowNull(false)
+  @Is('UserVideoQuota', value => throwIfNotValid(value, isUserVideoQuotaValid, 'video quota'))
+  @Column(DataType.BIGINT)
+  videoQuota: number
+
+  @AllowNull(false)
+  @Is('UserVideoQuotaDaily', value => throwIfNotValid(value, isUserVideoQuotaDailyValid, 'video quota daily'))
+  @Column(DataType.BIGINT)
+  videoQuotaDaily: number
+
+  @AllowNull(false)
+  @Default(DEFAULT_THEME_NAME)
+  @Is('UserTheme', value => throwIfNotValid(value, isThemeValid, 'theme'))
+  @Column
+  theme: string
+
+  @CreatedAt
+  createdAt: Date
+
+  @UpdatedAt
+  updatedAt: Date
+
+  @HasOne(() => AccountModel, {
+    foreignKey: 'userId',
+    onDelete: 'cascade',
+    hooks: true
+  })
+  Account: AccountModel
+
+  @HasOne(() => UserNotificationSettingModel, {
+    foreignKey: 'userId',
+    onDelete: 'cascade',
+    hooks: true
+  })
+  NotificationSetting: UserNotificationSettingModel
+
+  @HasMany(() => VideoImportModel, {
+    foreignKey: 'userId',
+    onDelete: 'cascade'
+  })
+  VideoImports: VideoImportModel[]
+
+  @HasMany(() => OAuthTokenModel, {
+    foreignKey: 'userId',
+    onDelete: 'cascade'
+  })
+  OAuthTokens: OAuthTokenModel[]
+
+  @BeforeCreate
+  @BeforeUpdate
+  static cryptPasswordIfNeeded (instance: UserModel) {
+    if (instance.changed('password')) {
+      return cryptPassword(instance.password)
+        .then(hash => {
+          instance.password = hash
+          return undefined
+        })
+    }
+  }
+
+  @AfterUpdate
+  @AfterDestroy
+  static removeTokenCache (instance: UserModel) {
+    return clearCacheByUserId(instance.id)
+  }
+
+  static countTotal () {
+    return this.count()
+  }
+
+  static listForApi (start: number, count: number, sort: string, search?: string) {
+    let where = undefined
+    if (search) {
+      where = {
+        [Op.or]: [
+          {
+            email: {
+              [Op.iLike]: '%' + search + '%'
+            }
+          },
+          {
+            username: {
+              [ Op.iLike ]: '%' + search + '%'
+            }
           }
-        }
+        ]
+      }
+    }
+
+    const query: FindOptions = {
+      attributes: {
+        include: [
+          [
+            literal(
+              '(' +
+                'SELECT COALESCE(SUM("size"), 0) ' +
+                'FROM (' +
+                  'SELECT MAX("videoFile"."size") AS "size" FROM "videoFile" ' +
+                  'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' +
+                  'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
+                  'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
+                  'WHERE "account"."userId" = "UserModel"."id" GROUP BY "video"."id"' +
+                ') t' +
+              ')'
+            ),
+            'videoQuotaUsed'
+          ]
+        ]
       },
-      role: {
-        type: DataTypes.INTEGER,
-        allowNull: false,
-        validate: {
-          roleValid: value => {
-            const res = isUserRoleValid(value)
-            if (res === false) throw new Error('Role is not valid.')
-          }
+      offset: start,
+      limit: count,
+      order: getSort(sort),
+      where
+    }
+
+    return UserModel.findAndCountAll(query)
+      .then(({ rows, count }) => {
+        return {
+          data: rows,
+          total: count
         }
-      },
-      videoQuota: {
-        type: DataTypes.BIGINT,
-        allowNull: false,
-        validate: {
-          videoQuotaValid: value => {
-            const res = isUserVideoQuotaValid(value)
-            if (res === false) throw new Error('Video quota is not valid.')
-          }
+      })
+  }
+
+  static listWithRight (right: UserRight) {
+    const roles = Object.keys(USER_ROLE_LABELS)
+      .map(k => parseInt(k, 10) as UserRole)
+      .filter(role => hasUserRight(role, right))
+
+    const query = {
+      where: {
+        role: {
+          [Op.in]: roles
         }
       }
-    },
-    {
-      indexes: [
+    }
+
+    return UserModel.findAll(query)
+  }
+
+  static listUserSubscribersOf (actorId: number) {
+    const query = {
+      include: [
         {
-          fields: [ 'username' ],
-          unique: true
+          model: UserNotificationSettingModel.unscoped(),
+          required: true
         },
         {
-          fields: [ 'email' ],
-          unique: true
+          attributes: [ 'userId' ],
+          model: AccountModel.unscoped(),
+          required: true,
+          include: [
+            {
+              attributes: [ ],
+              model: ActorModel.unscoped(),
+              required: true,
+              where: {
+                serverId: null
+              },
+              include: [
+                {
+                  attributes: [ ],
+                  as: 'ActorFollowings',
+                  model: ActorFollowModel.unscoped(),
+                  required: true,
+                  where: {
+                    targetActorId: actorId
+                  }
+                }
+              ]
+            }
+          ]
         }
-      ],
-      hooks: {
-        beforeCreate: beforeCreateOrUpdate,
-        beforeUpdate: beforeCreateOrUpdate
+      ]
+    }
+
+    return UserModel.unscoped().findAll(query)
+  }
+
+  static listByUsernames (usernames: string[]) {
+    const query = {
+      where: {
+        username: usernames
       }
     }
-  )
-
-  const classMethods = [
-    associate,
-
-    countTotal,
-    getByUsername,
-    listForApi,
-    loadById,
-    loadByUsername,
-    loadByUsernameAndPopulateChannels,
-    loadByUsernameOrEmail
-  ]
-  const instanceMethods = [
-    hasRight,
-    isPasswordMatch,
-    toFormattedJSON,
-    isAbleToUploadVideo
-  ]
-  addMethodsToModel(User, classMethods, instanceMethods)
 
-  return User
-}
+    return UserModel.findAll(query)
+  }
 
-function beforeCreateOrUpdate (user: UserInstance) {
-  if (user.changed('password')) {
-    return cryptPassword(user.password)
-      .then(hash => {
-        user.password = hash
-        return undefined
-      })
+  static loadById (id: number) {
+    return UserModel.findByPk(id)
   }
-}
 
-// ------------------------------ METHODS ------------------------------
+  static loadByUsername (username: string) {
+    const query = {
+      where: {
+        username: { [ Op.iLike ]: username }
+      }
+    }
 
-hasRight = function (this: UserInstance, right: UserRight) {
-  return hasUserRight(this.role, right)
-}
+    return UserModel.findOne(query)
+  }
 
-isPasswordMatch = function (this: UserInstance, password: string) {
-  return comparePassword(password, this.password)
-}
+  static loadByUsernameAndPopulateChannels (username: string) {
+    const query = {
+      where: {
+        username: { [ Op.iLike ]: username }
+      }
+    }
 
-toFormattedJSON = function (this: UserInstance) {
-  const json = {
-    id: this.id,
-    username: this.username,
-    email: this.email,
-    displayNSFW: this.displayNSFW,
-    role: this.role,
-    roleLabel: USER_ROLE_LABELS[this.role],
-    videoQuota: this.videoQuota,
-    createdAt: this.createdAt,
-    account: this.Account.toFormattedJSON()
+    return UserModel.scope(ScopeNames.WITH_VIDEO_CHANNEL).findOne(query)
   }
 
-  if (Array.isArray(this.Account.VideoChannels) === true) {
-    const videoChannels = this.Account.VideoChannels
-      .map(c => c.toFormattedJSON())
-      .sort((v1, v2) => {
-        if (v1.createdAt < v2.createdAt) return -1
-        if (v1.createdAt === v2.createdAt) return 0
+  static loadByEmail (email: string) {
+    const query = {
+      where: {
+        email
+      }
+    }
 
-        return 1
-      })
+    return UserModel.findOne(query)
+  }
+
+  static loadByUsernameOrEmail (username: string, email?: string) {
+    if (!email) email = username
+
+    const query = {
+      where: {
+        [ Op.or ]: [ { username: { [ Op.iLike ]: username } }, { email } ]
+      }
+    }
 
-    json['videoChannels'] = videoChannels
+    return UserModel.findOne(query)
   }
 
-  return json
-}
+  static loadByVideoId (videoId: number) {
+    const query = {
+      include: [
+        {
+          required: true,
+          attributes: [ 'id' ],
+          model: AccountModel.unscoped(),
+          include: [
+            {
+              required: true,
+              attributes: [ 'id' ],
+              model: VideoChannelModel.unscoped(),
+              include: [
+                {
+                  required: true,
+                  attributes: [ 'id' ],
+                  model: VideoModel.unscoped(),
+                  where: {
+                    id: videoId
+                  }
+                }
+              ]
+            }
+          ]
+        }
+      ]
+    }
 
-isAbleToUploadVideo = function (this: UserInstance, videoFile: Express.Multer.File) {
-  if (this.videoQuota === -1) return Promise.resolve(true)
+    return UserModel.findOne(query)
+  }
 
-  return getOriginalVideoFileTotalFromUser(this).then(totalBytes => {
-    return (videoFile.size + totalBytes) < this.videoQuota
-  })
-}
+  static loadByVideoImportId (videoImportId: number) {
+    const query = {
+      include: [
+        {
+          required: true,
+          attributes: [ 'id' ],
+          model: VideoImportModel.unscoped(),
+          where: {
+            id: videoImportId
+          }
+        }
+      ]
+    }
 
-// ------------------------------ STATICS ------------------------------
+    return UserModel.findOne(query)
+  }
 
-function associate (models) {
-  User.hasOne(models.Account, {
-    foreignKey: 'userId',
-    onDelete: 'cascade'
-  })
+  static loadByChannelActorId (videoChannelActorId: number) {
+    const query = {
+      include: [
+        {
+          required: true,
+          attributes: [ 'id' ],
+          model: AccountModel.unscoped(),
+          include: [
+            {
+              required: true,
+              attributes: [ 'id' ],
+              model: VideoChannelModel.unscoped(),
+              where: {
+                actorId: videoChannelActorId
+              }
+            }
+          ]
+        }
+      ]
+    }
 
-  User.hasMany(models.OAuthToken, {
-    foreignKey: 'userId',
-    onDelete: 'cascade'
-  })
-}
+    return UserModel.findOne(query)
+  }
 
-countTotal = function () {
-  return this.count()
-}
+  static loadByAccountActorId (accountActorId: number) {
+    const query = {
+      include: [
+        {
+          required: true,
+          attributes: [ 'id' ],
+          model: AccountModel.unscoped(),
+          where: {
+            actorId: accountActorId
+          }
+        }
+      ]
+    }
 
-getByUsername = function (username: string) {
-  const query = {
-    where: {
-      username: username
-    },
-    include: [ { model: User['sequelize'].models.Account, required: true } ]
+    return UserModel.findOne(query)
   }
 
-  return User.findOne(query)
-}
+  static getOriginalVideoFileTotalFromUser (user: UserModel) {
+    // Don't use sequelize because we need to use a sub query
+    const query = UserModel.generateUserQuotaBaseSQL()
 
-listForApi = function (start: number, count: number, sort: string) {
-  const query = {
-    offset: start,
-    limit: count,
-    order: [ getSort(sort) ],
-    include: [ { model: User['sequelize'].models.Account, required: true } ]
+    return UserModel.getTotalRawQuery(query, user.id)
   }
 
-  return User.findAndCountAll(query).then(({ rows, count }) => {
+  // Returns cumulative size of all video files uploaded in the last 24 hours.
+  static getOriginalVideoFileTotalDailyFromUser (user: UserModel) {
+    // Don't use sequelize because we need to use a sub query
+    const query = UserModel.generateUserQuotaBaseSQL('"video"."createdAt" > now() - interval \'24 hours\'')
+
+    return UserModel.getTotalRawQuery(query, user.id)
+  }
+
+  static async getStats () {
+    const totalUsers = await UserModel.count()
+
     return {
-      data: rows,
-      total: count
+      totalUsers
     }
-  })
-}
-
-loadById = function (id: number) {
-  const options = {
-    include: [ { model: User['sequelize'].models.Account, required: true } ]
   }
 
-  return User.findById(id, options)
-}
+  static autoComplete (search: string) {
+    const query = {
+      where: {
+        username: {
+          [ Op.like ]: `%${search}%`
+        }
+      },
+      limit: 10
+    }
 
-loadByUsername = function (username: string) {
-  const query = {
-    where: {
-      username
-    },
-    include: [ { model: User['sequelize'].models.Account, required: true } ]
+    return UserModel.findAll(query)
+                    .then(u => u.map(u => u.username))
   }
 
-  return User.findOne(query)
-}
+  hasRight (right: UserRight) {
+    return hasUserRight(this.role, right)
+  }
 
-loadByUsernameAndPopulateChannels = function (username: string) {
-  const query = {
-    where: {
-      username
-    },
-    include: [
-      {
-        model: User['sequelize'].models.Account,
-        required: true,
-        include: [ User['sequelize'].models.VideoChannel ]
-      }
-    ]
+  hasAdminFlag (flag: UserAdminFlag) {
+    return this.adminFlags & flag
   }
 
-  return User.findOne(query)
-}
+  isPasswordMatch (password: string) {
+    return comparePassword(password, this.password)
+  }
+
+  toFormattedJSON (parameters: { withAdminFlags?: boolean } = {}): User {
+    const videoQuotaUsed = this.get('videoQuotaUsed')
+    const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily')
+
+    const json = {
+      id: this.id,
+      username: this.username,
+      email: this.email,
+      pendingEmail: this.pendingEmail,
+      emailVerified: this.emailVerified,
+      nsfwPolicy: this.nsfwPolicy,
+      webTorrentEnabled: this.webTorrentEnabled,
+      videosHistoryEnabled: this.videosHistoryEnabled,
+      autoPlayVideo: this.autoPlayVideo,
+      videoLanguages: this.videoLanguages,
+      role: this.role,
+      theme: getThemeOrDefault(this.theme, DEFAULT_USER_THEME_NAME),
+      roleLabel: USER_ROLE_LABELS[ this.role ],
+      videoQuota: this.videoQuota,
+      videoQuotaDaily: this.videoQuotaDaily,
+      createdAt: this.createdAt,
+      blocked: this.blocked,
+      blockedReason: this.blockedReason,
+      account: this.Account.toFormattedJSON(),
+      notificationSettings: this.NotificationSetting ? this.NotificationSetting.toFormattedJSON() : undefined,
+      videoChannels: [],
+      videoQuotaUsed: videoQuotaUsed !== undefined
+            ? parseInt(videoQuotaUsed + '', 10)
+            : undefined,
+      videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined
+            ? parseInt(videoQuotaUsedDaily + '', 10)
+            : undefined
+    }
+
+    if (parameters.withAdminFlags) {
+      Object.assign(json, { adminFlags: this.adminFlags })
+    }
+
+    if (Array.isArray(this.Account.VideoChannels) === true) {
+      json.videoChannels = this.Account.VideoChannels
+        .map(c => c.toFormattedJSON())
+        .sort((v1, v2) => {
+          if (v1.createdAt < v2.createdAt) return -1
+          if (v1.createdAt === v2.createdAt) return 0
 
-loadByUsernameOrEmail = function (username: string, email: string) {
-  const query = {
-    include: [ { model: User['sequelize'].models.Account, required: true } ],
-    where: {
-      [Sequelize.Op.or]: [ { username }, { email } ]
+          return 1
+        })
     }
+
+    return json
   }
 
-  // FIXME: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18387
-  return (User as any).findOne(query)
-}
+  async isAbleToUploadVideo (videoFile: { size: number }) {
+    if (this.videoQuota === -1 && this.videoQuotaDaily === -1) return Promise.resolve(true)
+
+    const [ totalBytes, totalBytesDaily ] = await Promise.all([
+      UserModel.getOriginalVideoFileTotalFromUser(this),
+      UserModel.getOriginalVideoFileTotalDailyFromUser(this)
+    ])
 
-// ---------------------------------------------------------------------------
-
-function getOriginalVideoFileTotalFromUser (user: UserInstance) {
-  // Don't use sequelize because we need to use a sub query
-  const query = 'SELECT SUM("size") AS "total" FROM ' +
-                '(SELECT MAX("VideoFiles"."size") AS "size" FROM "VideoFiles" ' +
-                'INNER JOIN "Videos" ON "VideoFiles"."videoId" = "Videos"."id" ' +
-                'INNER JOIN "VideoChannels" ON "VideoChannels"."id" = "Videos"."channelId" ' +
-                'INNER JOIN "Accounts" ON "VideoChannels"."accountId" = "Accounts"."id" ' +
-                'INNER JOIN "Users" ON "Accounts"."userId" = "Users"."id" ' +
-                'WHERE "Users"."id" = $userId GROUP BY "Videos"."id") t'
-
-  const options = {
-    bind: { userId: user.id },
-    type: Sequelize.QueryTypes.SELECT
+    const uploadedTotal = videoFile.size + totalBytes
+    const uploadedDaily = videoFile.size + totalBytesDaily
+
+    if (this.videoQuotaDaily === -1) return uploadedTotal < this.videoQuota
+    if (this.videoQuota === -1) return uploadedDaily < this.videoQuotaDaily
+
+    return uploadedTotal < this.videoQuota && uploadedDaily < this.videoQuotaDaily
   }
-  return User['sequelize'].query(query, options).then(([ { total } ]) => {
-    if (total === null) return 0
 
-    return parseInt(total, 10)
-  })
+  private static generateUserQuotaBaseSQL (where?: string) {
+    const andWhere = where ? 'AND ' + where : ''
+
+    return 'SELECT SUM("size") AS "total" ' +
+      'FROM (' +
+        'SELECT MAX("videoFile"."size") AS "size" FROM "videoFile" ' +
+        'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' +
+        'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
+        'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
+        'WHERE "account"."userId" = $userId ' + andWhere +
+        'GROUP BY "video"."id"' +
+      ') t'
+  }
+
+  private static getTotalRawQuery (query: string, userId: number) {
+    const options = {
+      bind: { userId },
+      type: QueryTypes.SELECT as QueryTypes.SELECT
+    }
+
+    return UserModel.sequelize.query<{ total: string }>(query, options)
+                    .then(([ { total } ]) => {
+                      if (total === null) return 0
+
+                      return parseInt(total, 10)
+                    })
+  }
 }