]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blobdiff - server/models/account/user.ts
Fix live ending banner
[github/Chocobozzz/PeerTube.git] / server / models / account / user.ts
index 5f45f8e7cd02376fc737b5f78c7ab2d3ecb808f4..513455773f2eb24a17f46e9760c14b5ad4d64a26 100644 (file)
@@ -1,3 +1,4 @@
+import { values } from 'lodash'
 import { col, FindOptions, fn, literal, Op, QueryTypes, where, WhereOptions } from 'sequelize'
 import {
   AfterDestroy,
@@ -14,13 +15,28 @@ import {
   HasOne,
   Is,
   IsEmail,
+  IsUUID,
   Model,
   Scopes,
   Table,
   UpdatedAt
 } from 'sequelize-typescript'
-import { hasUserRight, MyUser, USER_ROLE_LABELS, UserRight, AbuseState, VideoPlaylistType, VideoPrivacy } from '../../../shared'
+import { TokensCache } from '@server/lib/auth/tokens-cache'
+import {
+  MMyUserFormattable,
+  MUser,
+  MUserDefault,
+  MUserFormattable,
+  MUserNotifSettingChannelDefault,
+  MUserWithNotificationSetting,
+  MVideoWithRights
+} from '@server/types/models'
+import { hasUserRight, USER_ROLE_LABELS } from '../../../shared/core-utils/users'
+import { AbuseState, MyUser, UserRight, VideoPlaylistType, VideoPrivacy } from '../../../shared/models'
 import { User, UserRole } from '../../../shared/models/users'
+import { UserAdminFlag } from '../../../shared/models/users/user-flag.model'
+import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type'
+import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
 import {
   isNoInstanceConfigWarningModal,
   isNoWelcomeModal,
@@ -42,33 +58,20 @@ import {
   isUserWebTorrentEnabledValid
 } from '../../helpers/custom-validators/users'
 import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto'
+import { DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants'
+import { getThemeOrDefault } from '../../lib/plugins/theme-utils'
+import { ActorModel } from '../activitypub/actor'
+import { ActorFollowModel } from '../activitypub/actor-follow'
 import { OAuthTokenModel } from '../oauth/oauth-token'
 import { getSort, throwIfNotValid } from '../utils'
+import { VideoModel } from '../video/video'
 import { VideoChannelModel } from '../video/video-channel'
+import { VideoImportModel } from '../video/video-import'
+import { VideoLiveModel } from '../video/video-live'
 import { VideoPlaylistModel } from '../video/video-playlist'
 import { AccountModel } from './account'
-import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type'
-import { values } from 'lodash'
-import { 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 { isThemeNameValid } from '../../helpers/custom-validators/plugins'
-import { getThemeOrDefault } from '../../lib/plugins/theme-utils'
-import * as Bluebird from 'bluebird'
-import {
-  MMyUserFormattable,
-  MUserDefault,
-  MUserFormattable,
-  MUserId,
-  MUserNotifSettingChannelDefault,
-  MUserWithNotificationSetting,
-  MVideoFullLight
-} from '@server/types/models'
+import { ActorImageModel } from './actor-image'
 
 enum ScopeNames {
   FOR_ME_API = 'FOR_ME_API',
@@ -95,7 +98,20 @@ enum ScopeNames {
         model: AccountModel,
         include: [
           {
-            model: VideoChannelModel
+            model: VideoChannelModel.unscoped(),
+            include: [
+              {
+                model: ActorModel,
+                required: true,
+                include: [
+                  {
+                    model: ActorImageModel,
+                    as: 'Banner',
+                    required: false
+                  }
+                ]
+              }
+            ]
           },
           {
             attributes: [ 'id', 'name', 'type' ],
@@ -217,7 +233,7 @@ enum ScopeNames {
     }
   ]
 })
-export class UserModel extends Model<UserModel> {
+export class UserModel extends Model {
 
   @AllowNull(true)
   @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password', true))
@@ -351,6 +367,12 @@ export class UserModel extends Model<UserModel> {
   @Column
   pluginAuth: string
 
+  @AllowNull(false)
+  @Default(DataType.UUIDV4)
+  @IsUUID(4)
+  @Column(DataType.UUID)
+  feedToken: string
+
   @AllowNull(true)
   @Default(null)
   @Column
@@ -403,7 +425,7 @@ export class UserModel extends Model<UserModel> {
   @AfterUpdate
   @AfterDestroy
   static removeTokenCache (instance: UserModel) {
-    return clearCacheByUserId(instance.id)
+    return TokensCache.Instance.clearCacheByUserId(instance.id)
   }
 
   static countTotal () {
@@ -474,7 +496,7 @@ export class UserModel extends Model<UserModel> {
                     })
   }
 
-  static listWithRight (right: UserRight): Bluebird<MUserDefault[]> {
+  static listWithRight (right: UserRight): Promise<MUserDefault[]> {
     const roles = Object.keys(USER_ROLE_LABELS)
                         .map(k => parseInt(k, 10) as UserRole)
                         .filter(role => hasUserRight(role, right))
@@ -490,7 +512,7 @@ export class UserModel extends Model<UserModel> {
     return UserModel.findAll(query)
   }
 
-  static listUserSubscribersOf (actorId: number): Bluebird<MUserWithNotificationSetting[]> {
+  static listUserSubscribersOf (actorId: number): Promise<MUserWithNotificationSetting[]> {
     const query = {
       include: [
         {
@@ -529,7 +551,7 @@ export class UserModel extends Model<UserModel> {
     return UserModel.unscoped().findAll(query)
   }
 
-  static listByUsernames (usernames: string[]): Bluebird<MUserDefault[]> {
+  static listByUsernames (usernames: string[]): Promise<MUserDefault[]> {
     const query = {
       where: {
         username: usernames
@@ -539,7 +561,15 @@ export class UserModel extends Model<UserModel> {
     return UserModel.findAll(query)
   }
 
-  static loadById (id: number, withStats = false): Bluebird<MUserDefault> {
+  static loadById (id: number): Promise<MUser> {
+    return UserModel.unscoped().findByPk(id)
+  }
+
+  static loadByIdFull (id: number): Promise<MUserDefault> {
+    return UserModel.findByPk(id)
+  }
+
+  static loadByIdWithChannels (id: number, withStats = false): Promise<MUserDefault> {
     const scopes = [
       ScopeNames.WITH_VIDEOCHANNELS
     ]
@@ -549,27 +579,27 @@ export class UserModel extends Model<UserModel> {
     return UserModel.scope(scopes).findByPk(id)
   }
 
-  static loadByUsername (username: string): Bluebird<MUserDefault> {
+  static loadByUsername (username: string): Promise<MUserDefault> {
     const query = {
       where: {
-        username: { [Op.iLike]: username }
+        username
       }
     }
 
     return UserModel.findOne(query)
   }
 
-  static loadForMeAPI (username: string): Bluebird<MUserNotifSettingChannelDefault> {
+  static loadForMeAPI (id: number): Promise<MUserNotifSettingChannelDefault> {
     const query = {
       where: {
-        username: { [Op.iLike]: username }
+        id
       }
     }
 
     return UserModel.scope(ScopeNames.FOR_ME_API).findOne(query)
   }
 
-  static loadByEmail (email: string): Bluebird<MUserDefault> {
+  static loadByEmail (email: string): Promise<MUserDefault> {
     const query = {
       where: {
         email
@@ -579,7 +609,7 @@ export class UserModel extends Model<UserModel> {
     return UserModel.findOne(query)
   }
 
-  static loadByUsernameOrEmail (username: string, email?: string): Bluebird<MUserDefault> {
+  static loadByUsernameOrEmail (username: string, email?: string): Promise<MUserDefault> {
     if (!email) email = username
 
     const query = {
@@ -595,7 +625,7 @@ export class UserModel extends Model<UserModel> {
     return UserModel.findOne(query)
   }
 
-  static loadByVideoId (videoId: number): Bluebird<MUserDefault> {
+  static loadByVideoId (videoId: number): Promise<MUserDefault> {
     const query = {
       include: [
         {
@@ -626,7 +656,7 @@ export class UserModel extends Model<UserModel> {
     return UserModel.findOne(query)
   }
 
-  static loadByVideoImportId (videoImportId: number): Bluebird<MUserDefault> {
+  static loadByVideoImportId (videoImportId: number): Promise<MUserDefault> {
     const query = {
       include: [
         {
@@ -643,7 +673,7 @@ export class UserModel extends Model<UserModel> {
     return UserModel.findOne(query)
   }
 
-  static loadByChannelActorId (videoChannelActorId: number): Bluebird<MUserDefault> {
+  static loadByChannelActorId (videoChannelActorId: number): Promise<MUserDefault> {
     const query = {
       include: [
         {
@@ -667,7 +697,7 @@ export class UserModel extends Model<UserModel> {
     return UserModel.findOne(query)
   }
 
-  static loadByAccountActorId (accountActorId: number): Bluebird<MUserDefault> {
+  static loadByAccountActorId (accountActorId: number): Promise<MUserDefault> {
     const query = {
       include: [
         {
@@ -684,26 +714,85 @@ export class UserModel extends Model<UserModel> {
     return UserModel.findOne(query)
   }
 
-  static getOriginalVideoFileTotalFromUser (user: MUserId) {
-    // Don't use sequelize because we need to use a sub query
-    const query = UserModel.generateUserQuotaBaseSQL({
-      withSelect: true,
-      whereUserId: '$userId'
-    })
+  static loadByLiveId (liveId: number): Promise<MUser> {
+    const query = {
+      include: [
+        {
+          attributes: [ 'id' ],
+          model: AccountModel.unscoped(),
+          required: true,
+          include: [
+            {
+              attributes: [ 'id' ],
+              model: VideoChannelModel.unscoped(),
+              required: true,
+              include: [
+                {
+                  attributes: [ 'id' ],
+                  model: VideoModel.unscoped(),
+                  required: true,
+                  include: [
+                    {
+                      attributes: [],
+                      model: VideoLiveModel.unscoped(),
+                      required: true,
+                      where: {
+                        id: liveId
+                      }
+                    }
+                  ]
+                }
+              ]
+            }
+          ]
+        }
+      ]
+    }
+
+    return UserModel.unscoped().findOne(query)
+  }
+
+  static generateUserQuotaBaseSQL (options: {
+    whereUserId: '$userId' | '"UserModel"."id"'
+    withSelect: boolean
+    where?: string
+  }) {
+    const andWhere = options.where
+      ? 'AND ' + options.where
+      : ''
+
+    const videoChannelJoin = 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
+      'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
+      `WHERE "account"."userId" = ${options.whereUserId} ${andWhere}`
+
+    const webtorrentFiles = 'SELECT "videoFile"."size" AS "size", "video"."id" AS "videoId" FROM "videoFile" ' +
+      'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' +
+      videoChannelJoin
 
-    return UserModel.getTotalRawQuery(query, user.id)
+    const hlsFiles = 'SELECT "videoFile"."size" AS "size", "video"."id" AS "videoId" FROM "videoFile" ' +
+      'INNER JOIN "videoStreamingPlaylist" ON "videoFile"."videoStreamingPlaylistId" = "videoStreamingPlaylist".id ' +
+      'INNER JOIN "video" ON "videoStreamingPlaylist"."videoId" = "video"."id" ' +
+      videoChannelJoin
+
+    return 'SELECT COALESCE(SUM("size"), 0) AS "total" ' +
+      'FROM (' +
+        `SELECT MAX("t1"."size") AS "size" FROM (${webtorrentFiles} UNION ${hlsFiles}) t1 ` +
+        'GROUP BY "t1"."videoId"' +
+      ') t2'
   }
 
-  // Returns cumulative size of all video files uploaded in the last 24 hours.
-  static getOriginalVideoFileTotalDailyFromUser (user: MUserId) {
-    // Don't use sequelize because we need to use a sub query
-    const query = UserModel.generateUserQuotaBaseSQL({
-      withSelect: true,
-      whereUserId: '$userId',
-      where: '"video"."createdAt" > now() - interval \'24 hours\''
-    })
+  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 UserModel.getTotalRawQuery(query, user.id)
+                      return parseInt(total, 10)
+                    })
   }
 
   static async getStats () {
@@ -723,12 +812,14 @@ export class UserModel extends Model<UserModel> {
     const totalDailyActiveUsers = await getActiveUsers(1)
     const totalWeeklyActiveUsers = await getActiveUsers(7)
     const totalMonthlyActiveUsers = await getActiveUsers(30)
+    const totalHalfYearActiveUsers = await getActiveUsers(180)
 
     return {
       totalUsers,
       totalDailyActiveUsers,
       totalWeeklyActiveUsers,
-      totalMonthlyActiveUsers
+      totalMonthlyActiveUsers,
+      totalHalfYearActiveUsers
     }
   }
 
@@ -746,7 +837,7 @@ export class UserModel extends Model<UserModel> {
                     .then(u => u.map(u => u.username))
   }
 
-  canGetVideo (video: MVideoFullLight) {
+  canGetVideo (video: MVideoWithRights) {
     const videoUserId = video.VideoChannel.Account.userId
 
     if (video.isBlacklisted()) {
@@ -873,64 +964,4 @@ export class UserModel extends Model<UserModel> {
 
     return Object.assign(formatted, { specialPlaylists })
   }
-
-  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)
-    ])
-
-    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
-  }
-
-  private static generateUserQuotaBaseSQL (options: {
-    whereUserId: '$userId' | '"UserModel"."id"'
-    withSelect: boolean
-    where?: string
-  }) {
-    const andWhere = options.where
-      ? 'AND ' + options.where
-      : ''
-
-    const videoChannelJoin = 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
-      'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
-      `WHERE "account"."userId" = ${options.whereUserId} ${andWhere}`
-
-    const webtorrentFiles = 'SELECT "videoFile"."size" AS "size", "video"."id" AS "videoId" FROM "videoFile" ' +
-      'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' +
-      videoChannelJoin
-
-    const hlsFiles = 'SELECT "videoFile"."size" AS "size", "video"."id" AS "videoId" FROM "videoFile" ' +
-      'INNER JOIN "videoStreamingPlaylist" ON "videoFile"."videoStreamingPlaylistId" = "videoStreamingPlaylist".id ' +
-      'INNER JOIN "video" ON "videoStreamingPlaylist"."videoId" = "video"."id" ' +
-      videoChannelJoin
-
-    return 'SELECT COALESCE(SUM("size"), 0) AS "total" ' +
-      'FROM (' +
-        `SELECT MAX("t1"."size") AS "size" FROM (${webtorrentFiles} UNION ${hlsFiles}) t1 ` +
-        'GROUP BY "t1"."videoId"' +
-      ') t2'
-  }
-
-  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)
-                    })
-  }
 }