X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Fmodels%2Faccount%2Fuser.ts;h=c1f22b76aaad87c4195416c0096d6e1ffacd80fe;hb=3fbc6974334ca58c068f0f9def0b0a40db2a6de1;hp=f21eff04b53912a427cc12a98a9c900768b9b4a2;hpb=d95d15598847c7f020aa056e7e6e0c02d2bbf732;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/models/account/user.ts b/server/models/account/user.ts index f21eff04b..c1f22b76a 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts @@ -1,3 +1,4 @@ +import { values } from 'lodash' import { col, FindOptions, fn, literal, Op, QueryTypes, where, WhereOptions } from 'sequelize' import { AfterDestroy, @@ -14,13 +15,27 @@ 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 { + 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 +57,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 { clearCacheByUserId } from '../../lib/oauth-model' +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' enum ScopeNames { FOR_ME_API = 'FOR_ME_API', @@ -168,28 +170,26 @@ enum ScopeNames { '(' + `SELECT concat_ws(':', "abuses", "acceptedAbuses") ` + 'FROM (' + - 'SELECT COUNT("videoAbuse"."id") AS "abuses", ' + - `COUNT("videoAbuse"."id") FILTER (WHERE "videoAbuse"."state" = ${AbuseState.ACCEPTED}) AS "acceptedAbuses" ` + - 'FROM "videoAbuse" ' + - 'INNER JOIN "video" ON "videoAbuse"."videoId" = "video"."id" ' + - 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + - 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' + + 'SELECT COUNT("abuse"."id") AS "abuses", ' + + `COUNT("abuse"."id") FILTER (WHERE "abuse"."state" = ${AbuseState.ACCEPTED}) AS "acceptedAbuses" ` + + 'FROM "abuse" ' + + 'INNER JOIN "account" ON "account"."id" = "abuse"."flaggedAccountId" ' + 'WHERE "account"."userId" = "UserModel"."id"' + ') t' + ')' ), - 'videoAbusesCount' + 'abusesCount' ], [ literal( '(' + - 'SELECT COUNT("videoAbuse"."id") ' + - 'FROM "videoAbuse" ' + - 'INNER JOIN "account" ON "account"."id" = "videoAbuse"."reporterAccountId" ' + + 'SELECT COUNT("abuse"."id") ' + + 'FROM "abuse" ' + + 'INNER JOIN "account" ON "account"."id" = "abuse"."reporterAccountId" ' + 'WHERE "account"."userId" = "UserModel"."id"' + ')' ), - 'videoAbusesCreatedCount' + 'abusesCreatedCount' ], [ literal( @@ -219,7 +219,7 @@ enum ScopeNames { } ] }) -export class UserModel extends Model { +export class UserModel extends Model { @AllowNull(true) @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password', true)) @@ -353,6 +353,12 @@ export class UserModel extends Model { @Column pluginAuth: string + @AllowNull(false) + @Default(DataType.UUIDV4) + @IsUUID(4) + @Column(DataType.UUID) + feedToken: string + @AllowNull(true) @Default(null) @Column @@ -476,7 +482,7 @@ export class UserModel extends Model { }) } - static listWithRight (right: UserRight): Bluebird { + static listWithRight (right: UserRight): Promise { const roles = Object.keys(USER_ROLE_LABELS) .map(k => parseInt(k, 10) as UserRole) .filter(role => hasUserRight(role, right)) @@ -492,7 +498,7 @@ export class UserModel extends Model { return UserModel.findAll(query) } - static listUserSubscribersOf (actorId: number): Bluebird { + static listUserSubscribersOf (actorId: number): Promise { const query = { include: [ { @@ -531,7 +537,7 @@ export class UserModel extends Model { return UserModel.unscoped().findAll(query) } - static listByUsernames (usernames: string[]): Bluebird { + static listByUsernames (usernames: string[]): Promise { const query = { where: { username: usernames @@ -541,7 +547,11 @@ export class UserModel extends Model { return UserModel.findAll(query) } - static loadById (id: number, withStats = false): Bluebird { + static loadById (id: number): Promise { + return UserModel.unscoped().findByPk(id) + } + + static loadByIdWithChannels (id: number, withStats = false): Promise { const scopes = [ ScopeNames.WITH_VIDEOCHANNELS ] @@ -551,27 +561,27 @@ export class UserModel extends Model { return UserModel.scope(scopes).findByPk(id) } - static loadByUsername (username: string): Bluebird { + static loadByUsername (username: string): Promise { const query = { where: { - username: { [Op.iLike]: username } + username } } return UserModel.findOne(query) } - static loadForMeAPI (username: string): Bluebird { + static loadForMeAPI (id: number): Promise { const query = { where: { - username: { [Op.iLike]: username } + id } } return UserModel.scope(ScopeNames.FOR_ME_API).findOne(query) } - static loadByEmail (email: string): Bluebird { + static loadByEmail (email: string): Promise { const query = { where: { email @@ -581,7 +591,7 @@ export class UserModel extends Model { return UserModel.findOne(query) } - static loadByUsernameOrEmail (username: string, email?: string): Bluebird { + static loadByUsernameOrEmail (username: string, email?: string): Promise { if (!email) email = username const query = { @@ -597,7 +607,7 @@ export class UserModel extends Model { return UserModel.findOne(query) } - static loadByVideoId (videoId: number): Bluebird { + static loadByVideoId (videoId: number): Promise { const query = { include: [ { @@ -628,7 +638,7 @@ export class UserModel extends Model { return UserModel.findOne(query) } - static loadByVideoImportId (videoImportId: number): Bluebird { + static loadByVideoImportId (videoImportId: number): Promise { const query = { include: [ { @@ -645,7 +655,7 @@ export class UserModel extends Model { return UserModel.findOne(query) } - static loadByChannelActorId (videoChannelActorId: number): Bluebird { + static loadByChannelActorId (videoChannelActorId: number): Promise { const query = { include: [ { @@ -669,7 +679,7 @@ export class UserModel extends Model { return UserModel.findOne(query) } - static loadByAccountActorId (accountActorId: number): Bluebird { + static loadByAccountActorId (accountActorId: number): Promise { const query = { include: [ { @@ -686,26 +696,85 @@ export class UserModel extends Model { 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 { + 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.getTotalRawQuery(query, user.id) + return UserModel.unscoped().findOne(query) } - // 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 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}` - return UserModel.getTotalRawQuery(query, user.id) + 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' + } + + 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) + }) } static async getStats () { @@ -725,12 +794,14 @@ export class UserModel extends Model { 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 } } @@ -748,7 +819,7 @@ export class UserModel extends Model { .then(u => u.map(u => u.username)) } - canGetVideo (video: MVideoFullLight) { + canGetVideo (video: MVideoWithRights) { const videoUserId = video.VideoChannel.Account.userId if (video.isBlacklisted()) { @@ -780,8 +851,8 @@ export class UserModel extends Model { const videoQuotaUsed = this.get('videoQuotaUsed') const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily') const videosCount = this.get('videosCount') - const [ videoAbusesCount, videoAbusesAcceptedCount ] = (this.get('videoAbusesCount') as string || ':').split(':') - const videoAbusesCreatedCount = this.get('videoAbusesCreatedCount') + const [ abusesCount, abusesAcceptedCount ] = (this.get('abusesCount') as string || ':').split(':') + const abusesCreatedCount = this.get('abusesCreatedCount') const videoCommentsCount = this.get('videoCommentsCount') const json: User = { @@ -815,14 +886,14 @@ export class UserModel extends Model { videosCount: videosCount !== undefined ? parseInt(videosCount + '', 10) : undefined, - videoAbusesCount: videoAbusesCount - ? parseInt(videoAbusesCount, 10) + abusesCount: abusesCount + ? parseInt(abusesCount, 10) : undefined, - videoAbusesAcceptedCount: videoAbusesAcceptedCount - ? parseInt(videoAbusesAcceptedCount, 10) + abusesAcceptedCount: abusesAcceptedCount + ? parseInt(abusesAcceptedCount, 10) : undefined, - videoAbusesCreatedCount: videoAbusesCreatedCount !== undefined - ? parseInt(videoAbusesCreatedCount + '', 10) + abusesCreatedCount: abusesCreatedCount !== undefined + ? parseInt(abusesCreatedCount + '', 10) : undefined, videoCommentsCount: videoCommentsCount !== undefined ? parseInt(videoCommentsCount + '', 10) @@ -875,64 +946,4 @@ export class UserModel extends Model { 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) - }) - } }