From 3fd3ab2d34d512b160a5e6084d7609be7b4f4452 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 12 Dec 2017 17:53:50 +0100 Subject: Move models to typescript-sequelize --- server/models/account/account-follow-interface.ts | 60 - server/models/account/account-follow.ts | 364 ++-- server/models/account/account-interface.ts | 76 - .../models/account/account-video-rate-interface.ts | 31 - server/models/account/account-video-rate.ts | 97 +- server/models/account/account.ts | 673 ++++--- server/models/account/index.ts | 4 - server/models/account/user-interface.ts | 67 - server/models/account/user.ts | 460 +++-- server/models/application/application-interface.ts | 31 - server/models/application/application.ts | 80 +- server/models/application/index.ts | 1 - server/models/avatar/avatar-interface.ts | 16 - server/models/avatar/avatar.ts | 31 +- server/models/avatar/index.ts | 1 - server/models/index.ts | 7 - server/models/job/index.ts | 1 - server/models/job/job-interface.ts | 33 - server/models/job/job.ts | 137 +- server/models/oauth/index.ts | 2 - server/models/oauth/oauth-client-interface.ts | 31 - server/models/oauth/oauth-client.ts | 112 +- server/models/oauth/oauth-token-interface.ts | 46 - server/models/oauth/oauth-token.ts | 259 ++- server/models/server/index.ts | 1 - server/models/server/server-interface.ts | 24 - server/models/server/server.ts | 183 +- server/models/utils.ts | 17 +- server/models/video/index.ts | 9 - server/models/video/tag-interface.ts | 20 - server/models/video/tag.ts | 105 +- server/models/video/video-abuse-interface.ts | 41 - server/models/video/video-abuse.ts | 210 +-- server/models/video/video-blacklist-interface.ts | 39 - server/models/video/video-blacklist.ts | 142 +- server/models/video/video-channel-interface.ts | 64 - .../models/video/video-channel-share-interface.ts | 32 - server/models/video/video-channel-share.ts | 120 +- server/models/video/video-channel.ts | 572 +++--- server/models/video/video-file-interface.ts | 24 - server/models/video/video-file.ts | 109 +- server/models/video/video-interface.ts | 150 -- server/models/video/video-share-interface.ts | 30 - server/models/video/video-share.ts | 118 +- server/models/video/video-tag-interface.ts | 18 - server/models/video/video-tag.ts | 43 +- server/models/video/video.ts | 1845 +++++++++----------- 47 files changed, 2607 insertions(+), 3929 deletions(-) delete mode 100644 server/models/account/account-follow-interface.ts delete mode 100644 server/models/account/account-interface.ts delete mode 100644 server/models/account/account-video-rate-interface.ts delete mode 100644 server/models/account/index.ts delete mode 100644 server/models/account/user-interface.ts delete mode 100644 server/models/application/application-interface.ts delete mode 100644 server/models/application/index.ts delete mode 100644 server/models/avatar/avatar-interface.ts delete mode 100644 server/models/avatar/index.ts delete mode 100644 server/models/index.ts delete mode 100644 server/models/job/index.ts delete mode 100644 server/models/job/job-interface.ts delete mode 100644 server/models/oauth/index.ts delete mode 100644 server/models/oauth/oauth-client-interface.ts delete mode 100644 server/models/oauth/oauth-token-interface.ts delete mode 100644 server/models/server/index.ts delete mode 100644 server/models/server/server-interface.ts delete mode 100644 server/models/video/index.ts delete mode 100644 server/models/video/tag-interface.ts delete mode 100644 server/models/video/video-abuse-interface.ts delete mode 100644 server/models/video/video-blacklist-interface.ts delete mode 100644 server/models/video/video-channel-interface.ts delete mode 100644 server/models/video/video-channel-share-interface.ts delete mode 100644 server/models/video/video-file-interface.ts delete mode 100644 server/models/video/video-interface.ts delete mode 100644 server/models/video/video-share-interface.ts delete mode 100644 server/models/video/video-tag-interface.ts (limited to 'server/models') diff --git a/server/models/account/account-follow-interface.ts b/server/models/account/account-follow-interface.ts deleted file mode 100644 index 7975a46f3..000000000 --- a/server/models/account/account-follow-interface.ts +++ /dev/null @@ -1,60 +0,0 @@ -import * as Bluebird from 'bluebird' -import * as Sequelize from 'sequelize' -import { AccountFollow, FollowState } from '../../../shared/models/accounts/follow.model' -import { ResultList } from '../../../shared/models/result-list.model' -import { AccountInstance } from './account-interface' - -export namespace AccountFollowMethods { - export type LoadByAccountAndTarget = ( - accountId: number, - targetAccountId: number, - t?: Sequelize.Transaction - ) => Bluebird - - export type ListFollowingForApi = (id: number, start: number, count: number, sort: string) => Bluebird< ResultList> - export type ListFollowersForApi = (id: number, start: number, count: number, sort: string) => Bluebird< ResultList> - - export type ListAcceptedFollowerUrlsForApi = ( - accountId: number[], - t: Sequelize.Transaction, - start?: number, - count?: number - ) => Promise< ResultList > - export type ListAcceptedFollowingUrlsForApi = ( - accountId: number[], - t: Sequelize.Transaction, - start?: number, - count?: number - ) => Promise< ResultList > - export type ListAcceptedFollowerSharedInboxUrls = (accountId: number[], t: Sequelize.Transaction) => Promise< ResultList > - export type ToFormattedJSON = (this: AccountFollowInstance) => AccountFollow -} - -export interface AccountFollowClass { - loadByAccountAndTarget: AccountFollowMethods.LoadByAccountAndTarget - listFollowersForApi: AccountFollowMethods.ListFollowersForApi - listFollowingForApi: AccountFollowMethods.ListFollowingForApi - - listAcceptedFollowerUrlsForApi: AccountFollowMethods.ListAcceptedFollowerUrlsForApi - listAcceptedFollowingUrlsForApi: AccountFollowMethods.ListAcceptedFollowingUrlsForApi - listAcceptedFollowerSharedInboxUrls: AccountFollowMethods.ListAcceptedFollowerSharedInboxUrls -} - -export interface AccountFollowAttributes { - accountId: number - targetAccountId: number - state: FollowState -} - -export interface AccountFollowInstance extends AccountFollowClass, AccountFollowAttributes, Sequelize.Instance { - id: number - createdAt: Date - updatedAt: Date - - AccountFollower?: AccountInstance - AccountFollowing?: AccountInstance - - toFormattedJSON: AccountFollowMethods.ToFormattedJSON -} - -export interface AccountFollowModel extends AccountFollowClass, Sequelize.Model {} diff --git a/server/models/account/account-follow.ts b/server/models/account/account-follow.ts index 724f37baa..975e7ee7d 100644 --- a/server/models/account/account-follow.ts +++ b/server/models/account/account-follow.ts @@ -1,64 +1,45 @@ +import * as Bluebird from 'bluebird' import { values } from 'lodash' import * as Sequelize from 'sequelize' - -import { addMethodsToModel, getSort } from '../utils' -import { AccountFollowAttributes, AccountFollowInstance, AccountFollowMethods } from './account-follow-interface' +import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' +import { FollowState } from '../../../shared/models/accounts' import { FOLLOW_STATES } from '../../initializers/constants' +import { ServerModel } from '../server/server' +import { getSort } from '../utils' +import { AccountModel } from './account' -let AccountFollow: Sequelize.Model -let loadByAccountAndTarget: AccountFollowMethods.LoadByAccountAndTarget -let listFollowingForApi: AccountFollowMethods.ListFollowingForApi -let listFollowersForApi: AccountFollowMethods.ListFollowersForApi -let listAcceptedFollowerUrlsForApi: AccountFollowMethods.ListAcceptedFollowerUrlsForApi -let listAcceptedFollowingUrlsForApi: AccountFollowMethods.ListAcceptedFollowingUrlsForApi -let listAcceptedFollowerSharedInboxUrls: AccountFollowMethods.ListAcceptedFollowerSharedInboxUrls -let toFormattedJSON: AccountFollowMethods.ToFormattedJSON - -export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { - AccountFollow = sequelize.define('AccountFollow', +@Table({ + tableName: 'accountFollow', + indexes: [ { - state: { - type: DataTypes.ENUM(values(FOLLOW_STATES)), - allowNull: false - } + fields: [ 'accountId' ] }, { - indexes: [ - { - fields: [ 'accountId' ] - }, - { - fields: [ 'targetAccountId' ] - }, - { - fields: [ 'accountId', 'targetAccountId' ], - unique: true - } - ] + fields: [ 'targetAccountId' ] + }, + { + fields: [ 'accountId', 'targetAccountId' ], + unique: true } - ) - - const classMethods = [ - associate, - loadByAccountAndTarget, - listFollowingForApi, - listFollowersForApi, - listAcceptedFollowerUrlsForApi, - listAcceptedFollowingUrlsForApi, - listAcceptedFollowerSharedInboxUrls ] - const instanceMethods = [ - toFormattedJSON - ] - addMethodsToModel(AccountFollow, classMethods, instanceMethods) +}) +export class AccountFollowModel extends Model { - return AccountFollow -} + @AllowNull(false) + @Column(DataType.ENUM(values(FOLLOW_STATES))) + state: FollowState -// ------------------------------ STATICS ------------------------------ + @CreatedAt + createdAt: Date -function associate (models) { - AccountFollow.belongsTo(models.Account, { + @UpdatedAt + updatedAt: Date + + @ForeignKey(() => AccountModel) + @Column + accountId: number + + @BelongsTo(() => AccountModel, { foreignKey: { name: 'accountId', allowNull: false @@ -66,8 +47,13 @@ function associate (models) { as: 'AccountFollower', onDelete: 'CASCADE' }) + AccountFollower: AccountModel - AccountFollow.belongsTo(models.Account, { + @ForeignKey(() => AccountModel) + @Column + targetAccountId: number + + @BelongsTo(() => AccountModel, { foreignKey: { name: 'targetAccountId', allowNull: false @@ -75,170 +61,168 @@ function associate (models) { as: 'AccountFollowing', onDelete: 'CASCADE' }) -} + AccountFollowing: AccountModel -toFormattedJSON = function (this: AccountFollowInstance) { - const follower = this.AccountFollower.toFormattedJSON() - const following = this.AccountFollowing.toFormattedJSON() - - const json = { - id: this.id, - follower, - following, - state: this.state, - createdAt: this.createdAt, - updatedAt: this.updatedAt - } - - return json -} - -loadByAccountAndTarget = function (accountId: number, targetAccountId: number, t?: Sequelize.Transaction) { - const query = { - where: { - accountId, - targetAccountId - }, - include: [ - { - model: AccountFollow[ 'sequelize' ].models.Account, - required: true, - as: 'AccountFollower' + static loadByAccountAndTarget (accountId: number, targetAccountId: number, t?: Sequelize.Transaction) { + const query = { + where: { + accountId, + targetAccountId }, - { - model: AccountFollow['sequelize'].models.Account, - required: true, - as: 'AccountFollowing' - } - ], - transaction: t + include: [ + { + model: AccountModel, + required: true, + as: 'AccountFollower' + }, + { + model: AccountModel, + required: true, + as: 'AccountFollowing' + } + ], + transaction: t + } + + return AccountFollowModel.findOne(query) } - return AccountFollow.findOne(query) -} + static listFollowingForApi (id: number, start: number, count: number, sort: string) { + const query = { + distinct: true, + offset: start, + limit: count, + order: [ getSort(sort) ], + include: [ + { + model: AccountModel, + required: true, + as: 'AccountFollower', + where: { + id + } + }, + { + model: AccountModel, + as: 'AccountFollowing', + required: true, + include: [ ServerModel ] + } + ] + } -listFollowingForApi = function (id: number, start: number, count: number, sort: string) { - const query = { - distinct: true, - offset: start, - limit: count, - order: [ getSort(sort) ], - include: [ - { - model: AccountFollow[ 'sequelize' ].models.Account, - required: true, - as: 'AccountFollower', - where: { - id + return AccountFollowModel.findAndCountAll(query) + .then(({ rows, count }) => { + return { + data: rows, + total: count } - }, - { - model: AccountFollow['sequelize'].models.Account, - as: 'AccountFollowing', - required: true, - include: [ AccountFollow['sequelize'].models.Server ] - } - ] + }) } - return AccountFollow.findAndCountAll(query).then(({ rows, count }) => { - return { - data: rows, - total: count + static listFollowersForApi (id: number, start: number, count: number, sort: string) { + const query = { + distinct: true, + offset: start, + limit: count, + order: [ getSort(sort) ], + include: [ + { + model: AccountModel, + required: true, + as: 'AccountFollower', + include: [ ServerModel ] + }, + { + model: AccountModel, + as: 'AccountFollowing', + required: true, + where: { + id + } + } + ] } - }) -} -listFollowersForApi = function (id: number, start: number, count: number, sort: string) { - const query = { - distinct: true, - offset: start, - limit: count, - order: [ getSort(sort) ], - include: [ - { - model: AccountFollow[ 'sequelize' ].models.Account, - required: true, - as: 'AccountFollower', - include: [ AccountFollow['sequelize'].models.Server ] - }, - { - model: AccountFollow['sequelize'].models.Account, - as: 'AccountFollowing', - required: true, - where: { - id + return AccountFollowModel.findAndCountAll(query) + .then(({ rows, count }) => { + return { + data: rows, + total: count } - } - ] + }) } - return AccountFollow.findAndCountAll(query).then(({ rows, count }) => { - return { - data: rows, - total: count - } - }) -} + static listAcceptedFollowerUrlsForApi (accountIds: number[], t: Sequelize.Transaction, start?: number, count?: number) { + return AccountFollowModel.createListAcceptedFollowForApiQuery('followers', accountIds, t, start, count) + } -listAcceptedFollowerUrlsForApi = function (accountIds: number[], t: Sequelize.Transaction, start?: number, count?: number) { - return createListAcceptedFollowForApiQuery('followers', accountIds, t, start, count) -} + static listAcceptedFollowerSharedInboxUrls (accountIds: number[], t: Sequelize.Transaction) { + return AccountFollowModel.createListAcceptedFollowForApiQuery('followers', accountIds, t, undefined, undefined, 'sharedInboxUrl') + } -listAcceptedFollowerSharedInboxUrls = function (accountIds: number[], t: Sequelize.Transaction) { - return createListAcceptedFollowForApiQuery('followers', accountIds, t, undefined, undefined, 'sharedInboxUrl') -} + static listAcceptedFollowingUrlsForApi (accountIds: number[], t: Sequelize.Transaction, start?: number, count?: number) { + return AccountFollowModel.createListAcceptedFollowForApiQuery('following', accountIds, t, start, count) + } -listAcceptedFollowingUrlsForApi = function (accountIds: number[], t: Sequelize.Transaction, start?: number, count?: number) { - return createListAcceptedFollowForApiQuery('following', accountIds, t, start, count) -} + private static async createListAcceptedFollowForApiQuery (type: 'followers' | 'following', + accountIds: number[], + t: Sequelize.Transaction, + start?: number, + count?: number, + columnUrl = 'url') { + let firstJoin: string + let secondJoin: string + + if (type === 'followers') { + firstJoin = 'targetAccountId' + secondJoin = 'accountId' + } else { + firstJoin = 'accountId' + secondJoin = 'targetAccountId' + } -// ------------------------------ UTILS ------------------------------ - -async function createListAcceptedFollowForApiQuery ( - type: 'followers' | 'following', - accountIds: number[], - t: Sequelize.Transaction, - start?: number, - count?: number, - columnUrl = 'url' -) { - let firstJoin: string - let secondJoin: string - - if (type === 'followers') { - firstJoin = 'targetAccountId' - secondJoin = 'accountId' - } else { - firstJoin = 'accountId' - secondJoin = 'targetAccountId' - } + const selections = [ '"Follows"."' + columnUrl + '" AS "url"', 'COUNT(*) AS "total"' ] + const tasks: Bluebird[] = [] - const selections = [ '"Follows"."' + columnUrl + '" AS "url"', 'COUNT(*) AS "total"' ] - const tasks: Promise[] = [] + for (const selection of selections) { + let query = 'SELECT ' + selection + ' FROM "account" ' + + 'INNER JOIN "accountFollow" ON "accountFollow"."' + firstJoin + '" = "account"."id" ' + + 'INNER JOIN "account" AS "Follows" ON "accountFollow"."' + secondJoin + '" = "Follows"."id" ' + + 'WHERE "account"."id" = ANY ($accountIds) AND "accountFollow"."state" = \'accepted\' ' - for (const selection of selections) { - let query = 'SELECT ' + selection + ' FROM "Accounts" ' + - 'INNER JOIN "AccountFollows" ON "AccountFollows"."' + firstJoin + '" = "Accounts"."id" ' + - 'INNER JOIN "Accounts" AS "Follows" ON "AccountFollows"."' + secondJoin + '" = "Follows"."id" ' + - 'WHERE "Accounts"."id" = ANY ($accountIds) AND "AccountFollows"."state" = \'accepted\' ' + if (count !== undefined) query += 'LIMIT ' + count + if (start !== undefined) query += ' OFFSET ' + start - if (count !== undefined) query += 'LIMIT ' + count - if (start !== undefined) query += ' OFFSET ' + start + const options = { + bind: { accountIds }, + type: Sequelize.QueryTypes.SELECT, + transaction: t + } + tasks.push(AccountFollowModel.sequelize.query(query, options)) + } - const options = { - bind: { accountIds }, - type: Sequelize.QueryTypes.SELECT, - transaction: t + const [ followers, [ { total } ] ] = await + Promise.all(tasks) + const urls: string[] = followers.map(f => f.url) + + return { + data: urls, + total: parseInt(total, 10) } - tasks.push(AccountFollow['sequelize'].query(query, options)) } - const [ followers, [ { total } ]] = await Promise.all(tasks) - const urls: string[] = followers.map(f => f.url) + toFormattedJSON () { + const follower = this.AccountFollower.toFormattedJSON() + const following = this.AccountFollowing.toFormattedJSON() - return { - data: urls, - total: parseInt(total, 10) + return { + id: this.id, + follower, + following, + state: this.state, + createdAt: this.createdAt, + updatedAt: this.updatedAt + } } } diff --git a/server/models/account/account-interface.ts b/server/models/account/account-interface.ts deleted file mode 100644 index 46fe068e3..000000000 --- a/server/models/account/account-interface.ts +++ /dev/null @@ -1,76 +0,0 @@ -import * as Bluebird from 'bluebird' -import * as Sequelize from 'sequelize' -import { Account as FormattedAccount, ActivityPubActor } from '../../../shared' -import { AvatarInstance } from '../avatar' -import { ServerInstance } from '../server/server-interface' -import { VideoChannelInstance } from '../video/video-channel-interface' - -export namespace AccountMethods { - export type LoadApplication = () => Bluebird - - export type Load = (id: number) => Bluebird - export type LoadByUUID = (uuid: string) => Bluebird - export type LoadByUrl = (url: string, transaction?: Sequelize.Transaction) => Bluebird - export type LoadLocalByName = (name: string) => Bluebird - export type LoadByNameAndHost = (name: string, host: string) => Bluebird - export type ListByFollowersUrls = (followerUrls: string[], transaction: Sequelize.Transaction) => Bluebird - - export type ToActivityPubObject = (this: AccountInstance) => ActivityPubActor - export type ToFormattedJSON = (this: AccountInstance) => FormattedAccount - export type IsOwned = (this: AccountInstance) => boolean - export type GetFollowerSharedInboxUrls = (this: AccountInstance, t: Sequelize.Transaction) => Bluebird - export type GetFollowingUrl = (this: AccountInstance) => string - export type GetFollowersUrl = (this: AccountInstance) => string - export type GetPublicKeyUrl = (this: AccountInstance) => string -} - -export interface AccountClass { - loadApplication: AccountMethods.LoadApplication - load: AccountMethods.Load - loadByUUID: AccountMethods.LoadByUUID - loadByUrl: AccountMethods.LoadByUrl - loadLocalByName: AccountMethods.LoadLocalByName - loadByNameAndHost: AccountMethods.LoadByNameAndHost - listByFollowersUrls: AccountMethods.ListByFollowersUrls -} - -export interface AccountAttributes { - name: string - url?: string - publicKey: string - privateKey: string - followersCount: number - followingCount: number - inboxUrl: string - outboxUrl: string - sharedInboxUrl: string - followersUrl: string - followingUrl: string - - uuid?: string - - serverId?: number - userId?: number - applicationId?: number - avatarId?: number -} - -export interface AccountInstance extends AccountClass, AccountAttributes, Sequelize.Instance { - isOwned: AccountMethods.IsOwned - toActivityPubObject: AccountMethods.ToActivityPubObject - toFormattedJSON: AccountMethods.ToFormattedJSON - getFollowerSharedInboxUrls: AccountMethods.GetFollowerSharedInboxUrls - getFollowingUrl: AccountMethods.GetFollowingUrl - getFollowersUrl: AccountMethods.GetFollowersUrl - getPublicKeyUrl: AccountMethods.GetPublicKeyUrl - - id: number - createdAt: Date - updatedAt: Date - - Server: ServerInstance - VideoChannels: VideoChannelInstance[] - Avatar: AvatarInstance -} - -export interface AccountModel extends AccountClass, Sequelize.Model {} diff --git a/server/models/account/account-video-rate-interface.ts b/server/models/account/account-video-rate-interface.ts deleted file mode 100644 index 1f395bc45..000000000 --- a/server/models/account/account-video-rate-interface.ts +++ /dev/null @@ -1,31 +0,0 @@ -import * as Sequelize from 'sequelize' -import * as Promise from 'bluebird' - -import { VideoRateType } from '../../../shared/models/videos/video-rate.type' -import { AccountInstance } from './account-interface' - -export namespace AccountVideoRateMethods { - export type Load = (accountId: number, videoId: number, transaction: Sequelize.Transaction) => Promise -} - -export interface AccountVideoRateClass { - load: AccountVideoRateMethods.Load -} - -export interface AccountVideoRateAttributes { - type: VideoRateType - accountId: number - videoId: number - - Account?: AccountInstance -} - -export interface AccountVideoRateInstance - extends AccountVideoRateClass, AccountVideoRateAttributes, Sequelize.Instance { - id: number - createdAt: Date - updatedAt: Date -} - -export interface AccountVideoRateModel - extends AccountVideoRateClass, Sequelize.Model {} diff --git a/server/models/account/account-video-rate.ts b/server/models/account/account-video-rate.ts index d92834bbb..e969e4a43 100644 --- a/server/models/account/account-video-rate.ts +++ b/server/models/account/account-video-rate.ts @@ -1,78 +1,69 @@ -/* - Account rates per video. -*/ import { values } from 'lodash' -import * as Sequelize from 'sequelize' - +import { Transaction } from 'sequelize' +import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' +import { IFindOptions } from 'sequelize-typescript/lib/interfaces/IFindOptions' +import { VideoRateType } from '../../../shared/models/videos' import { VIDEO_RATE_TYPES } from '../../initializers' +import { VideoModel } from '../video/video' +import { AccountModel } from './account' -import { addMethodsToModel } from '../utils' -import { - AccountVideoRateInstance, - AccountVideoRateAttributes, - - AccountVideoRateMethods -} from './account-video-rate-interface' - -let AccountVideoRate: Sequelize.Model -let load: AccountVideoRateMethods.Load - -export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { - AccountVideoRate = sequelize.define('AccountVideoRate', - { - type: { - type: DataTypes.ENUM(values(VIDEO_RATE_TYPES)), - allowNull: false - } - }, +/* + Account rates per video. +*/ +@Table({ + tableName: 'accountVideoRate', + indexes: [ { - indexes: [ - { - fields: [ 'videoId', 'accountId' ], - unique: true - } - ] + fields: [ 'videoId', 'accountId' ], + unique: true } - ) + ] +}) +export class AccountVideoRateModel extends Model { - const classMethods = [ - associate, + @AllowNull(false) + @Column(DataType.ENUM(values(VIDEO_RATE_TYPES))) + type: VideoRateType - load - ] - addMethodsToModel(AccountVideoRate, classMethods) + @CreatedAt + createdAt: Date - return AccountVideoRate -} + @UpdatedAt + updatedAt: Date -// ------------------------------ STATICS ------------------------------ + @ForeignKey(() => VideoModel) + @Column + videoId: number -function associate (models) { - AccountVideoRate.belongsTo(models.Video, { + @BelongsTo(() => VideoModel, { foreignKey: { - name: 'videoId', allowNull: false }, onDelete: 'CASCADE' }) + Video: VideoModel - AccountVideoRate.belongsTo(models.Account, { + @ForeignKey(() => AccountModel) + @Column + accountId: number + + @BelongsTo(() => AccountModel, { foreignKey: { - name: 'accountId', allowNull: false }, onDelete: 'CASCADE' }) -} + Account: AccountModel -load = function (accountId: number, videoId: number, transaction: Sequelize.Transaction) { - const options: Sequelize.FindOptions = { - where: { - accountId, - videoId + static load (accountId: number, videoId: number, transaction: Transaction) { + const options: IFindOptions = { + where: { + accountId, + videoId + } } - } - if (transaction) options.transaction = transaction + if (transaction) options.transaction = transaction - return AccountVideoRate.findOne(options) + return AccountVideoRateModel.findOne(options) + } } diff --git a/server/models/account/account.ts b/server/models/account/account.ts index 8b0819f39..d6758fa10 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts @@ -1,253 +1,200 @@ import { join } from 'path' import * as Sequelize from 'sequelize' +import { + AfterDestroy, + AllowNull, + BelongsTo, + Column, + CreatedAt, + DataType, + Default, + ForeignKey, + HasMany, + Is, + IsUUID, + Model, + Table, + UpdatedAt +} from 'sequelize-typescript' import { Avatar } from '../../../shared/models/avatars/avatar.model' +import { activityPubContextify } from '../../helpers' import { - activityPubContextify, isAccountFollowersCountValid, isAccountFollowingCountValid, isAccountPrivateKeyValid, isAccountPublicKeyValid, - isUserUsernameValid -} from '../../helpers' -import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' -import { AVATARS_DIR } from '../../initializers' -import { CONFIG, CONSTRAINTS_FIELDS } from '../../initializers/constants' -import { sendDeleteAccount } from '../../lib/activitypub/send/send-delete' -import { addMethodsToModel } from '../utils' -import { AccountAttributes, AccountInstance, AccountMethods } from './account-interface' - -let Account: Sequelize.Model -let load: AccountMethods.Load -let loadApplication: AccountMethods.LoadApplication -let loadByUUID: AccountMethods.LoadByUUID -let loadByUrl: AccountMethods.LoadByUrl -let loadLocalByName: AccountMethods.LoadLocalByName -let loadByNameAndHost: AccountMethods.LoadByNameAndHost -let listByFollowersUrls: AccountMethods.ListByFollowersUrls -let isOwned: AccountMethods.IsOwned -let toActivityPubObject: AccountMethods.ToActivityPubObject -let toFormattedJSON: AccountMethods.ToFormattedJSON -let getFollowerSharedInboxUrls: AccountMethods.GetFollowerSharedInboxUrls -let getFollowingUrl: AccountMethods.GetFollowingUrl -let getFollowersUrl: AccountMethods.GetFollowersUrl -let getPublicKeyUrl: AccountMethods.GetPublicKeyUrl - -export default function defineAccount (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { - Account = sequelize.define('Account', + isActivityPubUrlValid +} from '../../helpers/custom-validators/activitypub' +import { isUserUsernameValid } from '../../helpers/custom-validators/users' +import { AVATARS_DIR, CONFIG, CONSTRAINTS_FIELDS } from '../../initializers' +import { sendDeleteAccount } from '../../lib/activitypub/send' +import { ApplicationModel } from '../application/application' +import { AvatarModel } from '../avatar/avatar' +import { ServerModel } from '../server/server' +import { throwIfNotValid } from '../utils' +import { VideoChannelModel } from '../video/video-channel' +import { AccountFollowModel } from './account-follow' +import { UserModel } from './user' + +@Table({ + tableName: 'account', + indexes: [ { - uuid: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - allowNull: false, - validate: { - isUUID: 4 - } - }, - name: { - type: DataTypes.STRING, - allowNull: false, - validate: { - nameValid: value => { - const res = isUserUsernameValid(value) - if (res === false) throw new Error('Name is not valid.') - } - } - }, - url: { - type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max), - allowNull: false, - validate: { - urlValid: value => { - const res = isActivityPubUrlValid(value) - if (res === false) throw new Error('URL is not valid.') - } - } - }, - publicKey: { - type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.PUBLIC_KEY.max), - allowNull: true, - validate: { - publicKeyValid: value => { - const res = isAccountPublicKeyValid(value) - if (res === false) throw new Error('Public key is not valid.') - } - } - }, - privateKey: { - type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.PRIVATE_KEY.max), - allowNull: true, - validate: { - privateKeyValid: value => { - const res = isAccountPrivateKeyValid(value) - if (res === false) throw new Error('Private key is not valid.') - } - } - }, - followersCount: { - type: DataTypes.INTEGER, - allowNull: false, - validate: { - followersCountValid: value => { - const res = isAccountFollowersCountValid(value) - if (res === false) throw new Error('Followers count is not valid.') - } - } - }, - followingCount: { - type: DataTypes.INTEGER, - allowNull: false, - validate: { - followingCountValid: value => { - const res = isAccountFollowingCountValid(value) - if (res === false) throw new Error('Following count is not valid.') - } - } - }, - inboxUrl: { - type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max), - allowNull: false, - validate: { - inboxUrlValid: value => { - const res = isActivityPubUrlValid(value) - if (res === false) throw new Error('Inbox URL is not valid.') - } - } - }, - outboxUrl: { - type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max), - allowNull: false, - validate: { - outboxUrlValid: value => { - const res = isActivityPubUrlValid(value) - if (res === false) throw new Error('Outbox URL is not valid.') - } - } - }, - sharedInboxUrl: { - type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max), - allowNull: false, - validate: { - sharedInboxUrlValid: value => { - const res = isActivityPubUrlValid(value) - if (res === false) throw new Error('Shared inbox URL is not valid.') - } - } - }, - followersUrl: { - type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max), - allowNull: false, - validate: { - followersUrlValid: value => { - const res = isActivityPubUrlValid(value) - if (res === false) throw new Error('Followers URL is not valid.') - } - } - }, - followingUrl: { - type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max), - allowNull: false, - validate: { - followingUrlValid: value => { - const res = isActivityPubUrlValid(value) - if (res === false) throw new Error('Following URL is not valid.') - } - } - } + fields: [ 'name' ] }, { - indexes: [ - { - fields: [ 'name' ] - }, - { - fields: [ 'serverId' ] - }, - { - fields: [ 'userId' ], - unique: true - }, - { - fields: [ 'applicationId' ], - unique: true - }, - { - fields: [ 'name', 'serverId', 'applicationId' ], - unique: true - } - ], - hooks: { afterDestroy } + fields: [ 'serverId' ] + }, + { + fields: [ 'userId' ], + unique: true + }, + { + fields: [ 'applicationId' ], + unique: true + }, + { + fields: [ 'name', 'serverId', 'applicationId' ], + unique: true } - ) - - const classMethods = [ - associate, - loadApplication, - load, - loadByUUID, - loadByUrl, - loadLocalByName, - loadByNameAndHost, - listByFollowersUrls - ] - const instanceMethods = [ - isOwned, - toActivityPubObject, - toFormattedJSON, - getFollowerSharedInboxUrls, - getFollowingUrl, - getFollowersUrl, - getPublicKeyUrl ] - addMethodsToModel(Account, classMethods, instanceMethods) - - return Account -} +}) +export class AccountModel extends Model { + + @AllowNull(false) + @Default(DataType.UUIDV4) + @IsUUID(4) + @Column(DataType.UUID) + uuid: string + + @AllowNull(false) + @Is('AccountName', value => throwIfNotValid(value, isUserUsernameValid, 'account name')) + @Column + name: string + + @AllowNull(false) + @Is('AccountUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url')) + @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max)) + url: string + + @AllowNull(true) + @Is('AccountPublicKey', value => throwIfNotValid(value, isAccountPublicKeyValid, 'public key')) + @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.PUBLIC_KEY.max)) + publicKey: string + + @AllowNull(true) + @Is('AccountPublicKey', value => throwIfNotValid(value, isAccountPrivateKeyValid, 'private key')) + @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.PRIVATE_KEY.max)) + privateKey: string + + @AllowNull(false) + @Is('AccountFollowersCount', value => throwIfNotValid(value, isAccountFollowersCountValid, 'followers count')) + @Column + followersCount: number + + @AllowNull(false) + @Is('AccountFollowersCount', value => throwIfNotValid(value, isAccountFollowingCountValid, 'following count')) + @Column + followingCount: number + + @AllowNull(false) + @Is('AccountInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'inbox url')) + @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max)) + inboxUrl: string + + @AllowNull(false) + @Is('AccountOutboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'outbox url')) + @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max)) + outboxUrl: string + + @AllowNull(false) + @Is('AccountSharedInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'shared inbox url')) + @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max)) + sharedInboxUrl: string + + @AllowNull(false) + @Is('AccountFollowersUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'followers url')) + @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max)) + followersUrl: string + + @AllowNull(false) + @Is('AccountFollowingUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'following url')) + @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max)) + followingUrl: string + + @CreatedAt + createdAt: Date + + @UpdatedAt + updatedAt: Date + + @ForeignKey(() => AvatarModel) + @Column + avatarId: number + + @BelongsTo(() => AvatarModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'cascade' + }) + Avatar: AvatarModel -// --------------------------------------------------------------------------- + @ForeignKey(() => ServerModel) + @Column + serverId: number -function associate (models) { - Account.belongsTo(models.Server, { + @BelongsTo(() => ServerModel, { foreignKey: { - name: 'serverId', allowNull: true }, onDelete: 'cascade' }) + Server: ServerModel - Account.belongsTo(models.User, { + @ForeignKey(() => UserModel) + @Column + userId: number + + @BelongsTo(() => UserModel, { foreignKey: { - name: 'userId', allowNull: true }, onDelete: 'cascade' }) + User: UserModel + + @ForeignKey(() => ApplicationModel) + @Column + applicationId: number - Account.belongsTo(models.Application, { + @BelongsTo(() => ApplicationModel, { foreignKey: { - name: 'applicationId', allowNull: true }, onDelete: 'cascade' }) + Application: ApplicationModel - Account.hasMany(models.VideoChannel, { + @HasMany(() => VideoChannelModel, { foreignKey: { - name: 'accountId', allowNull: false }, onDelete: 'cascade', hooks: true }) + VideoChannels: VideoChannelModel[] - Account.hasMany(models.AccountFollow, { + @HasMany(() => AccountFollowModel, { foreignKey: { name: 'accountId', allowNull: false }, onDelete: 'cascade' }) + AccountFollowing: AccountFollowModel[] - Account.hasMany(models.AccountFollow, { + @HasMany(() => AccountFollowModel, { foreignKey: { name: 'targetAccountId', allowNull: false @@ -255,209 +202,199 @@ function associate (models) { as: 'followers', onDelete: 'cascade' }) + AccountFollowers: AccountFollowModel[] - Account.hasOne(models.Avatar, { - foreignKey: { - name: 'avatarId', - allowNull: true - }, - onDelete: 'cascade' - }) -} + @AfterDestroy + static sendDeleteIfOwned (instance: AccountModel) { + if (instance.isOwned()) { + return sendDeleteAccount(instance, undefined) + } -function afterDestroy (account: AccountInstance) { - if (account.isOwned()) { - return sendDeleteAccount(account, undefined) + return undefined } - return undefined -} + static loadApplication () { + return AccountModel.findOne({ + include: [ + { + model: ApplicationModel, + required: true + } + ] + }) + } -toFormattedJSON = function (this: AccountInstance) { - let host = CONFIG.WEBSERVER.HOST - let score: number - let avatar: Avatar = null + static load (id: number) { + return AccountModel.findById(id) + } - if (this.Avatar) { - avatar = { - path: join(AVATARS_DIR.ACCOUNT, this.Avatar.filename), - createdAt: this.Avatar.createdAt, - updatedAt: this.Avatar.updatedAt + static loadByUUID (uuid: string) { + const query = { + where: { + uuid + } } - } - if (this.Server) { - host = this.Server.host - score = this.Server.score as number + return AccountModel.findOne(query) } - const json = { - id: this.id, - uuid: this.uuid, - host, - score, - name: this.name, - followingCount: this.followingCount, - followersCount: this.followersCount, - createdAt: this.createdAt, - updatedAt: this.updatedAt, - avatar - } + static loadLocalByName (name: string) { + const query = { + where: { + name, + [ Sequelize.Op.or ]: [ + { + userId: { + [ Sequelize.Op.ne ]: null + } + }, + { + applicationId: { + [ Sequelize.Op.ne ]: null + } + } + ] + } + } - return json -} + return AccountModel.findOne(query) + } -toActivityPubObject = function (this: AccountInstance) { - const type = this.serverId ? 'Application' as 'Application' : 'Person' as 'Person' - - const json = { - type, - id: this.url, - following: this.getFollowingUrl(), - followers: this.getFollowersUrl(), - inbox: this.inboxUrl, - outbox: this.outboxUrl, - preferredUsername: this.name, - url: this.url, - name: this.name, - endpoints: { - sharedInbox: this.sharedInboxUrl - }, - uuid: this.uuid, - publicKey: { - id: this.getPublicKeyUrl(), - owner: this.url, - publicKeyPem: this.publicKey + static loadByNameAndHost (name: string, host: string) { + const query = { + where: { + name + }, + include: [ + { + model: ServerModel, + required: true, + where: { + host + } + } + ] } + + return AccountModel.findOne(query) } - return activityPubContextify(json) -} + static loadByUrl (url: string, transaction?: Sequelize.Transaction) { + const query = { + where: { + url + }, + transaction + } -isOwned = function (this: AccountInstance) { - return this.serverId === null -} + return AccountModel.findOne(query) + } -getFollowerSharedInboxUrls = function (this: AccountInstance, t: Sequelize.Transaction) { - const query: Sequelize.FindOptions = { - attributes: [ 'sharedInboxUrl' ], - include: [ - { - model: Account['sequelize'].models.AccountFollow, - required: true, - as: 'followers', - where: { - targetAccountId: this.id + static listByFollowersUrls (followersUrls: string[], transaction?: Sequelize.Transaction) { + const query = { + where: { + followersUrl: { + [ Sequelize.Op.in ]: followersUrls } - } - ], - transaction: t - } + }, + transaction + } - return Account.findAll(query) - .then(accounts => accounts.map(a => a.sharedInboxUrl)) -} + return AccountModel.findAll(query) + } -getFollowingUrl = function (this: AccountInstance) { - return this.url + '/following' -} + toFormattedJSON () { + let host = CONFIG.WEBSERVER.HOST + let score: number + let avatar: Avatar = null -getFollowersUrl = function (this: AccountInstance) { - return this.url + '/followers' -} + if (this.Avatar) { + avatar = { + path: join(AVATARS_DIR.ACCOUNT, this.Avatar.filename), + createdAt: this.Avatar.createdAt, + updatedAt: this.Avatar.updatedAt + } + } -getPublicKeyUrl = function (this: AccountInstance) { - return this.url + '#main-key' -} + if (this.Server) { + host = this.Server.host + score = this.Server.score + } -// ------------------------------ STATICS ------------------------------ + return { + id: this.id, + uuid: this.uuid, + host, + score, + name: this.name, + followingCount: this.followingCount, + followersCount: this.followersCount, + createdAt: this.createdAt, + updatedAt: this.updatedAt, + avatar + } + } -loadApplication = function () { - return Account.findOne({ - include: [ - { - model: Account['sequelize'].models.Application, - required: true + toActivityPubObject () { + const type = this.serverId ? 'Application' as 'Application' : 'Person' as 'Person' + + const json = { + type, + id: this.url, + following: this.getFollowingUrl(), + followers: this.getFollowersUrl(), + inbox: this.inboxUrl, + outbox: this.outboxUrl, + preferredUsername: this.name, + url: this.url, + name: this.name, + endpoints: { + sharedInbox: this.sharedInboxUrl + }, + uuid: this.uuid, + publicKey: { + id: this.getPublicKeyUrl(), + owner: this.url, + publicKeyPem: this.publicKey } - ] - }) -} - -load = function (id: number) { - return Account.findById(id) -} - -loadByUUID = function (uuid: string) { - const query: Sequelize.FindOptions = { - where: { - uuid } + + return activityPubContextify(json) } - return Account.findOne(query) -} + isOwned () { + return this.serverId === null + } -loadLocalByName = function (name: string) { - const query: Sequelize.FindOptions = { - where: { - name, - [Sequelize.Op.or]: [ - { - userId: { - [Sequelize.Op.ne]: null - } - }, + getFollowerSharedInboxUrls (t: Sequelize.Transaction) { + const query = { + attributes: [ 'sharedInboxUrl' ], + include: [ { - applicationId: { - [Sequelize.Op.ne]: null + model: AccountFollowModel, + required: true, + as: 'followers', + where: { + targetAccountId: this.id } } - ] + ], + transaction: t } - } - return Account.findOne(query) -} - -loadByNameAndHost = function (name: string, host: string) { - const query: Sequelize.FindOptions = { - where: { - name - }, - include: [ - { - model: Account['sequelize'].models.Server, - required: true, - where: { - host - } - } - ] + return AccountModel.findAll(query) + .then(accounts => accounts.map(a => a.sharedInboxUrl)) } - return Account.findOne(query) -} - -loadByUrl = function (url: string, transaction?: Sequelize.Transaction) { - const query: Sequelize.FindOptions = { - where: { - url - }, - transaction + getFollowingUrl () { + return this.url + '/following' } - return Account.findOne(query) -} - -listByFollowersUrls = function (followersUrls: string[], transaction?: Sequelize.Transaction) { - const query: Sequelize.FindOptions = { - where: { - followersUrl: { - [Sequelize.Op.in]: followersUrls - } - }, - transaction + getFollowersUrl () { + return this.url + '/followers' } - return Account.findAll(query) + getPublicKeyUrl () { + return this.url + '#main-key' + } } diff --git a/server/models/account/index.ts b/server/models/account/index.ts deleted file mode 100644 index 179f66974..000000000 --- a/server/models/account/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './account-interface' -export * from './account-follow-interface' -export * from './account-video-rate-interface' -export * from './user-interface' diff --git a/server/models/account/user-interface.ts b/server/models/account/user-interface.ts deleted file mode 100644 index 0f0b72063..000000000 --- a/server/models/account/user-interface.ts +++ /dev/null @@ -1,67 +0,0 @@ -import * as Bluebird from 'bluebird' -import * as Sequelize from 'sequelize' -import { ResultList } from '../../../shared/models/result-list.model' -import { UserRight } from '../../../shared/models/users/user-right.enum' -import { UserRole } from '../../../shared/models/users/user-role' -import { User as FormattedUser } from '../../../shared/models/users/user.model' -import { AccountInstance } from './account-interface' - -export namespace UserMethods { - export type HasRight = (this: UserInstance, right: UserRight) => boolean - export type IsPasswordMatch = (this: UserInstance, password: string) => Promise - - export type ToFormattedJSON = (this: UserInstance) => FormattedUser - export type IsAbleToUploadVideo = (this: UserInstance, videoFile: Express.Multer.File) => Promise - - export type CountTotal = () => Bluebird - - export type GetByUsername = (username: string) => Bluebird - - export type ListForApi = (start: number, count: number, sort: string) => Bluebird< ResultList > - - export type LoadById = (id: number) => Bluebird - - export type LoadByUsername = (username: string) => Bluebird - export type LoadByUsernameAndPopulateChannels = (username: string) => Bluebird - - export type LoadByUsernameOrEmail = (username: string, email: string) => Bluebird -} - -export interface UserClass { - isPasswordMatch: UserMethods.IsPasswordMatch, - toFormattedJSON: UserMethods.ToFormattedJSON, - hasRight: UserMethods.HasRight, - isAbleToUploadVideo: UserMethods.IsAbleToUploadVideo, - - countTotal: UserMethods.CountTotal, - getByUsername: UserMethods.GetByUsername, - listForApi: UserMethods.ListForApi, - loadById: UserMethods.LoadById, - loadByUsername: UserMethods.LoadByUsername, - loadByUsernameAndPopulateChannels: UserMethods.LoadByUsernameAndPopulateChannels, - loadByUsernameOrEmail: UserMethods.LoadByUsernameOrEmail -} - -export interface UserAttributes { - id?: number - password: string - username: string - email: string - displayNSFW?: boolean - role: UserRole - videoQuota: number - - Account?: AccountInstance -} - -export interface UserInstance extends UserClass, UserAttributes, Sequelize.Instance { - id: number - createdAt: Date - updatedAt: Date - - isPasswordMatch: UserMethods.IsPasswordMatch - toFormattedJSON: UserMethods.ToFormattedJSON - hasRight: UserMethods.HasRight -} - -export interface UserModel extends UserClass, Sequelize.Model {} diff --git a/server/models/account/user.ts b/server/models/account/user.ts index 3705947c0..84adad96e 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts @@ -1,301 +1,251 @@ import * as Sequelize from 'sequelize' +import { + AllowNull, + BeforeCreate, + BeforeUpdate, + Column, CreatedAt, + DataType, + Default, + HasMany, + HasOne, + Is, + IsEmail, + Model, + Table, UpdatedAt +} from 'sequelize-typescript' import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared' import { comparePassword, - cryptPassword, - isUserDisplayNSFWValid, - isUserPasswordValid, - isUserRoleValid, - isUserUsernameValid, - isUserVideoQuotaValid + cryptPassword } from '../../helpers' -import { addMethodsToModel, getSort } from '../utils' -import { UserAttributes, UserInstance, UserMethods } from './user-interface' - -let User: Sequelize.Model -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('User', +import { + isUserDisplayNSFWValid, isUserPasswordValid, isUserRoleValid, isUserUsernameValid, + isUserVideoQuotaValid +} from '../../helpers/custom-validators/users' +import { OAuthTokenModel } from '../oauth/oauth-token' +import { getSort, throwIfNotValid } from '../utils' +import { VideoChannelModel } from '../video/video-channel' +import { AccountModel } from './account' + +@Table({ + tableName: 'user', + indexes: [ { - 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 - } - }, - 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.') - } - } - }, - role: { - type: DataTypes.INTEGER, - allowNull: false, - validate: { - roleValid: value => { - const res = isUserRoleValid(value) - if (res === false) throw new Error('Role is not valid.') - } - } - }, - videoQuota: { - type: DataTypes.BIGINT, - allowNull: false, - validate: { - videoQuotaValid: value => { - const res = isUserVideoQuotaValid(value) - if (res === false) throw new Error('Video quota is not valid.') - } - } - } + fields: [ 'username' ], + unique: true }, { - indexes: [ - { - fields: [ 'username' ], - unique: true - }, - { - fields: [ 'email' ], - unique: true - } - ], - hooks: { - beforeCreate: beforeCreateOrUpdate, - beforeUpdate: beforeCreateOrUpdate - } + fields: [ 'email' ], + unique: true } - ) - - const classMethods = [ - associate, - - countTotal, - getByUsername, - listForApi, - loadById, - loadByUsername, - loadByUsernameAndPopulateChannels, - loadByUsernameOrEmail ] - const instanceMethods = [ - hasRight, - isPasswordMatch, - toFormattedJSON, - isAbleToUploadVideo - ] - addMethodsToModel(User, classMethods, instanceMethods) - - return User -} +}) +export class UserModel extends Model { + + @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(false) + @Default(false) + @Is('UserDisplayNSFW', value => throwIfNotValid(value, isUserDisplayNSFWValid, 'display NSFW boolean')) + @Column + displayNSFW: boolean + + @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 + + @CreatedAt + createdAt: Date + + @UpdatedAt + updatedAt: Date + + @HasOne(() => AccountModel, { + foreignKey: 'userId', + onDelete: 'cascade' + }) + Account: AccountModel -function beforeCreateOrUpdate (user: UserInstance) { - if (user.changed('password')) { - return cryptPassword(user.password) - .then(hash => { - user.password = hash - return undefined - }) + @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 + }) + } } -} - -// ------------------------------ METHODS ------------------------------ -hasRight = function (this: UserInstance, right: UserRight) { - return hasUserRight(this.role, right) -} + static countTotal () { + return this.count() + } -isPasswordMatch = function (this: UserInstance, password: string) { - return comparePassword(password, this.password) -} + static getByUsername (username: string) { + const query = { + where: { + username: username + }, + include: [ { model: AccountModel, required: true } ] + } -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.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 listForApi (start: number, count: number, sort: string) { + const query = { + offset: start, + limit: count, + order: [ getSort(sort) ], + include: [ { model: AccountModel, required: true } ] + } - return 1 + return UserModel.findAndCountAll(query) + .then(({ rows, count }) => { + return { + data: rows, + total: count + } }) - - json['videoChannels'] = videoChannels } - return json -} - -isAbleToUploadVideo = function (this: UserInstance, videoFile: Express.Multer.File) { - if (this.videoQuota === -1) return Promise.resolve(true) - - return getOriginalVideoFileTotalFromUser(this).then(totalBytes => { - return (videoFile.size + totalBytes) < this.videoQuota - }) -} - -// ------------------------------ STATICS ------------------------------ - -function associate (models) { - User.hasOne(models.Account, { - foreignKey: 'userId', - onDelete: 'cascade' - }) + static loadById (id: number) { + const options = { + include: [ { model: AccountModel, required: true } ] + } - User.hasMany(models.OAuthToken, { - foreignKey: 'userId', - onDelete: 'cascade' - }) -} + return UserModel.findById(id, options) + } -countTotal = function () { - return this.count() -} + static loadByUsername (username: string) { + const query = { + where: { + username + }, + include: [ { model: AccountModel, required: true } ] + } -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 loadByUsernameAndPopulateChannels (username: string) { + const query = { + where: { + username + }, + include: [ + { + model: AccountModel, + required: true, + include: [ VideoChannelModel ] + } + ] + } -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.findOne(query) } - return User.findAndCountAll(query).then(({ rows, count }) => { - return { - data: rows, - total: count + static loadByUsernameOrEmail (username: string, email: string) { + const query = { + include: [ { model: AccountModel, required: true } ], + where: { + [ Sequelize.Op.or ]: [ { username }, { email } ] + } } - }) -} -loadById = function (id: number) { - const options = { - include: [ { model: User['sequelize'].models.Account, required: true } ] + // FIXME: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18387 + return (UserModel as any).findOne(query) } - return User.findById(id, options) -} + private static getOriginalVideoFileTotalFromUser (user: UserModel) { + // Don't use sequelize because we need to use a sub query + const query = '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" ' + + 'INNER JOIN "user" ON "account"."userId" = "user"."id" ' + + 'WHERE "user"."id" = $userId GROUP BY "video"."id") t' + + const options = { + bind: { userId: user.id }, + type: Sequelize.QueryTypes.SELECT + } + return UserModel.sequelize.query(query, options) + .then(([ { total } ]) => { + if (total === null) return 0 -loadByUsername = function (username: string) { - const query = { - where: { - username - }, - include: [ { model: User['sequelize'].models.Account, required: true } ] + return parseInt(total, 10) + }) } - 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 ] - } - ] + isPasswordMatch (password: string) { + return comparePassword(password, this.password) } - return User.findOne(query) -} + toFormattedJSON () { + 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() + } + + 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) -} + isAbleToUploadVideo (videoFile: Express.Multer.File) { + if (this.videoQuota === -1) return Promise.resolve(true) -// --------------------------------------------------------------------------- - -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 + return UserModel.getOriginalVideoFileTotalFromUser(this) + .then(totalBytes => { + return (videoFile.size + totalBytes) < this.videoQuota + }) } - return User['sequelize'].query(query, options).then(([ { total } ]) => { - if (total === null) return 0 - - return parseInt(total, 10) - }) } diff --git a/server/models/application/application-interface.ts b/server/models/application/application-interface.ts deleted file mode 100644 index 2c391dba3..000000000 --- a/server/models/application/application-interface.ts +++ /dev/null @@ -1,31 +0,0 @@ -import * as Sequelize from 'sequelize' -import * as Bluebird from 'bluebird' - -export namespace ApplicationMethods { - export type LoadMigrationVersion = () => Bluebird - - export type UpdateMigrationVersion = ( - newVersion: number, - transaction: Sequelize.Transaction - ) => Bluebird<[ number, ApplicationInstance[] ]> - - export type CountTotal = () => Bluebird -} - -export interface ApplicationClass { - loadMigrationVersion: ApplicationMethods.LoadMigrationVersion - updateMigrationVersion: ApplicationMethods.UpdateMigrationVersion - countTotal: ApplicationMethods.CountTotal -} - -export interface ApplicationAttributes { - migrationVersion: number -} - -export interface ApplicationInstance extends ApplicationClass, ApplicationAttributes, Sequelize.Instance { - id: number - createdAt: Date - updatedAt: Date -} - -export interface ApplicationModel extends ApplicationClass, Sequelize.Model {} diff --git a/server/models/application/application.ts b/server/models/application/application.ts index 8ba40a895..f3c0f1052 100644 --- a/server/models/application/application.ts +++ b/server/models/application/application.ts @@ -1,61 +1,35 @@ -import * as Sequelize from 'sequelize' - -import { addMethodsToModel } from '../utils' -import { - ApplicationAttributes, - ApplicationInstance, - - ApplicationMethods -} from './application-interface' - -let Application: Sequelize.Model -let loadMigrationVersion: ApplicationMethods.LoadMigrationVersion -let updateMigrationVersion: ApplicationMethods.UpdateMigrationVersion -let countTotal: ApplicationMethods.CountTotal +import { Transaction } from 'sequelize' +import { AllowNull, Column, Default, IsInt, Model, Table } from 'sequelize-typescript' + +@Table({ + tableName: 'application' +}) +export class ApplicationModel extends Model { + + @AllowNull(false) + @Default(0) + @IsInt + @Column + migrationVersion: number + + static countTotal () { + return ApplicationModel.count() + } -export default function defineApplication (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { - Application = sequelize.define('Application', - { - migrationVersion: { - type: DataTypes.INTEGER, - defaultValue: 0, - allowNull: false, - validate: { - isInt: true - } - } + static loadMigrationVersion () { + const query = { + attributes: [ 'migrationVersion' ] } - ) - - const classMethods = [ - countTotal, - loadMigrationVersion, - updateMigrationVersion - ] - addMethodsToModel(Application, classMethods) - - return Application -} -// --------------------------------------------------------------------------- - -countTotal = function () { - return this.count() -} - -loadMigrationVersion = function () { - const query = { - attributes: [ 'migrationVersion' ] + return ApplicationModel.findOne(query).then(data => data ? data.migrationVersion : null) } - return Application.findOne(query).then(data => data ? data.migrationVersion : null) -} + static updateMigrationVersion (newVersion: number, transaction: Transaction) { + const options = { + where: {}, + transaction: transaction + } -updateMigrationVersion = function (newVersion: number, transaction: Sequelize.Transaction) { - const options: Sequelize.UpdateOptions = { - where: {}, - transaction: transaction + return ApplicationModel.update({ migrationVersion: newVersion }, options) } - - return Application.update({ migrationVersion: newVersion }, options) } diff --git a/server/models/application/index.ts b/server/models/application/index.ts deleted file mode 100644 index 706f85cb9..000000000 --- a/server/models/application/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './application-interface' diff --git a/server/models/avatar/avatar-interface.ts b/server/models/avatar/avatar-interface.ts deleted file mode 100644 index 4af2b87b7..000000000 --- a/server/models/avatar/avatar-interface.ts +++ /dev/null @@ -1,16 +0,0 @@ -import * as Sequelize from 'sequelize' - -export namespace AvatarMethods {} - -export interface AvatarClass {} - -export interface AvatarAttributes { - filename: string -} - -export interface AvatarInstance extends AvatarClass, AvatarAttributes, Sequelize.Instance { - createdAt: Date - updatedAt: Date -} - -export interface AvatarModel extends AvatarClass, Sequelize.Model {} diff --git a/server/models/avatar/avatar.ts b/server/models/avatar/avatar.ts index 96308fd5f..2e7a8ae2c 100644 --- a/server/models/avatar/avatar.ts +++ b/server/models/avatar/avatar.ts @@ -1,24 +1,17 @@ -import * as Sequelize from 'sequelize' -import { addMethodsToModel } from '../utils' -import { AvatarAttributes, AvatarInstance } from './avatar-interface' +import { AllowNull, Column, CreatedAt, Model, Table, UpdatedAt } from 'sequelize-typescript' -let Avatar: Sequelize.Model +@Table({ + tableName: 'avatar' +}) +export class AvatarModel extends Model { -export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { - Avatar = sequelize.define('Avatar', - { - filename: { - type: DataTypes.STRING, - allowNull: false - } - }, - {} - ) + @AllowNull(false) + @Column + filename: string - const classMethods = [] - addMethodsToModel(Avatar, classMethods) + @CreatedAt + createdAt: Date - return Avatar + @UpdatedAt + updatedAt: Date } - -// ------------------------------ Statics ------------------------------ diff --git a/server/models/avatar/index.ts b/server/models/avatar/index.ts deleted file mode 100644 index 877aed1ce..000000000 --- a/server/models/avatar/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './avatar-interface' diff --git a/server/models/index.ts b/server/models/index.ts deleted file mode 100644 index fedd97dd1..000000000 --- a/server/models/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from './application' -export * from './avatar' -export * from './job' -export * from './oauth' -export * from './server' -export * from './account' -export * from './video' diff --git a/server/models/job/index.ts b/server/models/job/index.ts deleted file mode 100644 index 56925fd32..000000000 --- a/server/models/job/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './job-interface' diff --git a/server/models/job/job-interface.ts b/server/models/job/job-interface.ts deleted file mode 100644 index 3cfc0fbed..000000000 --- a/server/models/job/job-interface.ts +++ /dev/null @@ -1,33 +0,0 @@ -import * as Bluebird from 'bluebird' -import * as Sequelize from 'sequelize' -import { Job as FormattedJob, JobCategory, JobState } from '../../../shared/models/job.model' -import { ResultList } from '../../../shared/models/result-list.model' - -export namespace JobMethods { - export type ListWithLimitByCategory = (limit: number, state: JobState, category: JobCategory) => Bluebird - export type ListForApi = (start: number, count: number, sort: string) => Bluebird< ResultList > - - export type ToFormattedJSON = (this: JobInstance) => FormattedJob -} - -export interface JobClass { - listWithLimitByCategory: JobMethods.ListWithLimitByCategory - listForApi: JobMethods.ListForApi, -} - -export interface JobAttributes { - state: JobState - category: JobCategory - handlerName: string - handlerInputData: any -} - -export interface JobInstance extends JobClass, JobAttributes, Sequelize.Instance { - id: number - createdAt: Date - updatedAt: Date - - toFormattedJSON: JobMethods.ToFormattedJSON -} - -export interface JobModel extends JobClass, Sequelize.Model {} diff --git a/server/models/job/job.ts b/server/models/job/job.ts index f428e26db..35c357e69 100644 --- a/server/models/job/job.ts +++ b/server/models/job/job.ts @@ -1,96 +1,79 @@ import { values } from 'lodash' -import * as Sequelize from 'sequelize' -import { JobCategory, JobState } from '../../../shared/models/job.model' +import { AllowNull, Column, CreatedAt, DataType, Model, Table, UpdatedAt } from 'sequelize-typescript' +import { JobCategory, JobState } from '../../../shared/models' import { JOB_CATEGORIES, JOB_STATES } from '../../initializers' -import { addMethodsToModel, getSort } from '../utils' -import { JobAttributes, JobInstance, JobMethods } from './job-interface' +import { getSort } from '../utils' -let Job: Sequelize.Model -let listWithLimitByCategory: JobMethods.ListWithLimitByCategory -let listForApi: JobMethods.ListForApi -let toFormattedJSON: JobMethods.ToFormattedJSON - -export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { - Job = sequelize.define('Job', - { - state: { - type: DataTypes.ENUM(values(JOB_STATES)), - allowNull: false - }, - category: { - type: DataTypes.ENUM(values(JOB_CATEGORIES)), - allowNull: false - }, - handlerName: { - type: DataTypes.STRING, - allowNull: false - }, - handlerInputData: { - type: DataTypes.JSON, - allowNull: true - } - }, +@Table({ + tableName: 'job', + indexes: [ { - indexes: [ - { - fields: [ 'state', 'category' ] - } - ] + fields: [ 'state', 'category' ] } - ) - - const classMethods = [ - listWithLimitByCategory, - listForApi ] - const instanceMethods = [ - toFormattedJSON - ] - addMethodsToModel(Job, classMethods, instanceMethods) +}) +export class JobModel extends Model { + @AllowNull(false) + @Column(DataType.ENUM(values(JOB_STATES))) + state: JobState - return Job -} + @AllowNull(false) + @Column(DataType.ENUM(values(JOB_CATEGORIES))) + category: JobCategory -toFormattedJSON = function (this: JobInstance) { - return { - id: this.id, - state: this.state, - category: this.category, - handlerName: this.handlerName, - handlerInputData: this.handlerInputData, - createdAt: this.createdAt, - updatedAt: this.updatedAt - } -} + @AllowNull(false) + @Column + handlerName: string -// --------------------------------------------------------------------------- + @AllowNull(true) + @Column(DataType.JSON) + handlerInputData: any -listWithLimitByCategory = function (limit: number, state: JobState, jobCategory: JobCategory) { - const query = { - order: [ - [ 'id', 'ASC' ] - ], - limit: limit, - where: { - state, - category: jobCategory + @CreatedAt + creationDate: Date + + @UpdatedAt + updatedOn: Date + + static listWithLimitByCategory (limit: number, state: JobState, jobCategory: JobCategory) { + const query = { + order: [ + [ 'id', 'ASC' ] + ], + limit: limit, + where: { + state, + category: jobCategory + } } + + return JobModel.findAll(query) } - return Job.findAll(query) -} + static listForApi (start: number, count: number, sort: string) { + const query = { + offset: start, + limit: count, + order: [ getSort(sort) ] + } -listForApi = function (start: number, count: number, sort: string) { - const query = { - offset: start, - limit: count, - order: [ getSort(sort) ] + return JobModel.findAndCountAll(query).then(({ rows, count }) => { + return { + data: rows, + total: count + } + }) } - return Job.findAndCountAll(query).then(({ rows, count }) => { + toFormattedJSON () { return { - data: rows, - total: count + id: this.id, + state: this.state, + category: this.category, + handlerName: this.handlerName, + handlerInputData: this.handlerInputData, + createdAt: this.createdAt, + updatedAt: this.updatedAt } - }) + } } diff --git a/server/models/oauth/index.ts b/server/models/oauth/index.ts deleted file mode 100644 index a20d3a56a..000000000 --- a/server/models/oauth/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './oauth-client-interface' -export * from './oauth-token-interface' diff --git a/server/models/oauth/oauth-client-interface.ts b/server/models/oauth/oauth-client-interface.ts deleted file mode 100644 index 3526e4159..000000000 --- a/server/models/oauth/oauth-client-interface.ts +++ /dev/null @@ -1,31 +0,0 @@ -import * as Sequelize from 'sequelize' -import * as Promise from 'bluebird' - -export namespace OAuthClientMethods { - export type CountTotal = () => Promise - - export type LoadFirstClient = () => Promise - - export type GetByIdAndSecret = (clientId: string, clientSecret: string) => Promise -} - -export interface OAuthClientClass { - countTotal: OAuthClientMethods.CountTotal - loadFirstClient: OAuthClientMethods.LoadFirstClient - getByIdAndSecret: OAuthClientMethods.GetByIdAndSecret -} - -export interface OAuthClientAttributes { - clientId: string - clientSecret: string - grants: string[] - redirectUris: string[] -} - -export interface OAuthClientInstance extends OAuthClientClass, OAuthClientAttributes, Sequelize.Instance { - id: number - createdAt: Date - updatedAt: Date -} - -export interface OAuthClientModel extends OAuthClientClass, Sequelize.Model {} diff --git a/server/models/oauth/oauth-client.ts b/server/models/oauth/oauth-client.ts index 9cc68771d..42c59bb79 100644 --- a/server/models/oauth/oauth-client.ts +++ b/server/models/oauth/oauth-client.ts @@ -1,86 +1,62 @@ -import * as Sequelize from 'sequelize' +import { AllowNull, Column, CreatedAt, DataType, HasMany, Model, Table, UpdatedAt } from 'sequelize-typescript' +import { OAuthTokenModel } from './oauth-token' -import { addMethodsToModel } from '../utils' -import { - OAuthClientInstance, - OAuthClientAttributes, - - OAuthClientMethods -} from './oauth-client-interface' - -let OAuthClient: Sequelize.Model -let countTotal: OAuthClientMethods.CountTotal -let loadFirstClient: OAuthClientMethods.LoadFirstClient -let getByIdAndSecret: OAuthClientMethods.GetByIdAndSecret - -export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { - OAuthClient = sequelize.define('OAuthClient', +@Table({ + tableName: 'oAuthClient', + indexes: [ { - clientId: { - type: DataTypes.STRING, - allowNull: false - }, - clientSecret: { - type: DataTypes.STRING, - allowNull: false - }, - grants: { - type: DataTypes.ARRAY(DataTypes.STRING) - }, - redirectUris: { - type: DataTypes.ARRAY(DataTypes.STRING) - } + fields: [ 'clientId' ], + unique: true }, { - indexes: [ - { - fields: [ 'clientId' ], - unique: true - }, - { - fields: [ 'clientId', 'clientSecret' ], - unique: true - } - ] + fields: [ 'clientId', 'clientSecret' ], + unique: true } - ) + ] +}) +export class OAuthClientModel extends Model { - const classMethods = [ - associate, + @AllowNull(false) + @Column + clientId: string - countTotal, - getByIdAndSecret, - loadFirstClient - ] - addMethodsToModel(OAuthClient, classMethods) + @AllowNull(false) + @Column + clientSecret: string - return OAuthClient -} + @Column(DataType.ARRAY(DataType.STRING)) + grants: string[] + + @Column(DataType.ARRAY(DataType.STRING)) + redirectUris: string[] + + @CreatedAt + createdAt: Date -// --------------------------------------------------------------------------- + @UpdatedAt + updatedAt: Date -function associate (models) { - OAuthClient.hasMany(models.OAuthToken, { - foreignKey: 'oAuthClientId', + @HasMany(() => OAuthTokenModel, { onDelete: 'cascade' }) -} + OAuthTokens: OAuthTokenModel[] -countTotal = function () { - return OAuthClient.count() -} + static countTotal () { + return OAuthClientModel.count() + } -loadFirstClient = function () { - return OAuthClient.findOne() -} + static loadFirstClient () { + return OAuthClientModel.findOne() + } -getByIdAndSecret = function (clientId: string, clientSecret: string) { - const query = { - where: { - clientId: clientId, - clientSecret: clientSecret + static getByIdAndSecret (clientId: string, clientSecret: string) { + const query = { + where: { + clientId: clientId, + clientSecret: clientSecret + } } - } - return OAuthClient.findOne(query) + return OAuthClientModel.findOne(query) + } } diff --git a/server/models/oauth/oauth-token-interface.ts b/server/models/oauth/oauth-token-interface.ts deleted file mode 100644 index 47d95d5fc..000000000 --- a/server/models/oauth/oauth-token-interface.ts +++ /dev/null @@ -1,46 +0,0 @@ -import * as Promise from 'bluebird' -import * as Sequelize from 'sequelize' - -import { UserModel } from '../account/user-interface' - -export type OAuthTokenInfo = { - refreshToken: string - refreshTokenExpiresAt: Date, - client: { - id: number - }, - user: { - id: number - } -} - -export namespace OAuthTokenMethods { - export type GetByRefreshTokenAndPopulateClient = (refreshToken: string) => Promise - export type GetByTokenAndPopulateUser = (bearerToken: string) => Promise - export type GetByRefreshTokenAndPopulateUser = (refreshToken: string) => Promise -} - -export interface OAuthTokenClass { - getByRefreshTokenAndPopulateClient: OAuthTokenMethods.GetByRefreshTokenAndPopulateClient - getByTokenAndPopulateUser: OAuthTokenMethods.GetByTokenAndPopulateUser - getByRefreshTokenAndPopulateUser: OAuthTokenMethods.GetByRefreshTokenAndPopulateUser -} - -export interface OAuthTokenAttributes { - accessToken: string - accessTokenExpiresAt: Date - refreshToken: string - refreshTokenExpiresAt: Date - - userId?: number - oAuthClientId?: number - User?: UserModel -} - -export interface OAuthTokenInstance extends OAuthTokenClass, OAuthTokenAttributes, Sequelize.Instance { - id: number - createdAt: Date - updatedAt: Date -} - -export interface OAuthTokenModel extends OAuthTokenClass, Sequelize.Model {} diff --git a/server/models/oauth/oauth-token.ts b/server/models/oauth/oauth-token.ts index a82bff130..0d21c42fd 100644 --- a/server/models/oauth/oauth-token.ts +++ b/server/models/oauth/oauth-token.ts @@ -1,164 +1,163 @@ -import * as Sequelize from 'sequelize' - +import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' import { logger } from '../../helpers' +import { AccountModel } from '../account/account' +import { UserModel } from '../account/user' +import { OAuthClientModel } from './oauth-client' + +export type OAuthTokenInfo = { + refreshToken: string + refreshTokenExpiresAt: Date, + client: { + id: number + }, + user: { + id: number + } +} -import { addMethodsToModel } from '../utils' -import { OAuthTokenAttributes, OAuthTokenInfo, OAuthTokenInstance, OAuthTokenMethods } from './oauth-token-interface' - -let OAuthToken: Sequelize.Model -let getByRefreshTokenAndPopulateClient: OAuthTokenMethods.GetByRefreshTokenAndPopulateClient -let getByTokenAndPopulateUser: OAuthTokenMethods.GetByTokenAndPopulateUser -let getByRefreshTokenAndPopulateUser: OAuthTokenMethods.GetByRefreshTokenAndPopulateUser - -export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { - OAuthToken = sequelize.define('OAuthToken', +@Table({ + tableName: 'oAuthToken', + indexes: [ { - accessToken: { - type: DataTypes.STRING, - allowNull: false - }, - accessTokenExpiresAt: { - type: DataTypes.DATE, - allowNull: false - }, - refreshToken: { - type: DataTypes.STRING, - allowNull: false - }, - refreshTokenExpiresAt: { - type: DataTypes.DATE, - allowNull: false - } + fields: [ 'refreshToken' ], + unique: true }, { - indexes: [ - { - fields: [ 'refreshToken' ], - unique: true - }, - { - fields: [ 'accessToken' ], - unique: true - }, - { - fields: [ 'userId' ] - }, - { - fields: [ 'oAuthClientId' ] - } - ] + fields: [ 'accessToken' ], + unique: true + }, + { + fields: [ 'userId' ] + }, + { + fields: [ 'oAuthClientId' ] } - ) + ] +}) +export class OAuthTokenModel extends Model { - const classMethods = [ - associate, + @AllowNull(false) + @Column + accessToken: string - getByRefreshTokenAndPopulateClient, - getByTokenAndPopulateUser, - getByRefreshTokenAndPopulateUser - ] - addMethodsToModel(OAuthToken, classMethods) + @AllowNull(false) + @Column + accessTokenExpiresAt: Date - return OAuthToken -} + @AllowNull(false) + @Column + refreshToken: string -// --------------------------------------------------------------------------- + @AllowNull(false) + @Column + refreshTokenExpiresAt: Date -function associate (models) { - OAuthToken.belongsTo(models.User, { + @CreatedAt + createdAt: Date + + @UpdatedAt + updatedAt: Date + + @ForeignKey(() => UserModel) + @Column + userId: number + + @BelongsTo(() => UserModel, { foreignKey: { - name: 'userId', allowNull: false }, onDelete: 'cascade' }) + User: UserModel - OAuthToken.belongsTo(models.OAuthClient, { + @ForeignKey(() => OAuthClientModel) + @Column + oAuthClientId: number + + @BelongsTo(() => OAuthClientModel, { foreignKey: { - name: 'oAuthClientId', allowNull: false }, onDelete: 'cascade' }) -} + OAuthClients: OAuthClientModel[] -getByRefreshTokenAndPopulateClient = function (refreshToken: string) { - const query = { - where: { - refreshToken: refreshToken - }, - include: [ OAuthToken['sequelize'].models.OAuthClient ] + static getByRefreshTokenAndPopulateClient (refreshToken: string) { + const query = { + where: { + refreshToken: refreshToken + }, + include: [ OAuthClientModel ] + } + + return OAuthTokenModel.findOne(query) + .then(token => { + if (!token) return null + + return { + refreshToken: token.refreshToken, + refreshTokenExpiresAt: token.refreshTokenExpiresAt, + client: { + id: token.oAuthClientId + }, + user: { + id: token.userId + } + } as OAuthTokenInfo + }) + .catch(err => { + logger.info('getRefreshToken error.', err) + throw err + }) } - return OAuthToken.findOne(query) - .then(token => { - if (!token) return null - - const tokenInfos: OAuthTokenInfo = { - refreshToken: token.refreshToken, - refreshTokenExpiresAt: token.refreshTokenExpiresAt, - client: { - id: token.oAuthClientId - }, - user: { - id: token.userId + static getByTokenAndPopulateUser (bearerToken: string) { + const query = { + where: { + accessToken: bearerToken + }, + include: [ + { + model: UserModel, + include: [ + { + model: AccountModel, + required: true + } + ] } - } + ] + } - return tokenInfos - }) - .catch(err => { - logger.info('getRefreshToken error.', err) - throw err - }) -} + return OAuthTokenModel.findOne(query).then(token => { + if (token) token['user'] = token.User -getByTokenAndPopulateUser = function (bearerToken: string) { - const query = { - where: { - accessToken: bearerToken - }, - include: [ - { - model: OAuthToken['sequelize'].models.User, - include: [ - { - model: OAuthToken['sequelize'].models.Account, - required: true - } - ] - } - ] + return token + }) } - return OAuthToken.findOne(query).then(token => { - if (token) token['user'] = token.User + static getByRefreshTokenAndPopulateUser (refreshToken: string) { + const query = { + where: { + refreshToken: refreshToken + }, + include: [ + { + model: UserModel, + include: [ + { + model: AccountModel, + required: true + } + ] + } + ] + } - return token - }) -} + return OAuthTokenModel.findOne(query).then(token => { + token['user'] = token.User -getByRefreshTokenAndPopulateUser = function (refreshToken: string) { - const query = { - where: { - refreshToken: refreshToken - }, - include: [ - { - model: OAuthToken['sequelize'].models.User, - include: [ - { - model: OAuthToken['sequelize'].models.Account, - required: true - } - ] - } - ] + return token + }) } - - return OAuthToken.findOne(query).then(token => { - token['user'] = token.User - - return token - }) } diff --git a/server/models/server/index.ts b/server/models/server/index.ts deleted file mode 100644 index 4cb2994aa..000000000 --- a/server/models/server/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './server-interface' diff --git a/server/models/server/server-interface.ts b/server/models/server/server-interface.ts deleted file mode 100644 index be1a4917e..000000000 --- a/server/models/server/server-interface.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as Promise from 'bluebird' -import * as Sequelize from 'sequelize' - -export namespace ServerMethods { - export type ListBadServers = () => Promise - export type UpdateServersScoreAndRemoveBadOnes = (goodServers: number[], badServers: number[]) => void -} - -export interface ServerClass { - updateServersScoreAndRemoveBadOnes: ServerMethods.UpdateServersScoreAndRemoveBadOnes -} - -export interface ServerAttributes { - id?: number - host?: string - score?: number | Sequelize.literal // Sequelize literal for 'score +' + value -} - -export interface ServerInstance extends ServerClass, ServerAttributes, Sequelize.Instance { - createdAt: Date - updatedAt: Date -} - -export interface ServerModel extends ServerClass, Sequelize.Model {} diff --git a/server/models/server/server.ts b/server/models/server/server.ts index ebd216b08..edfd8010b 100644 --- a/server/models/server/server.ts +++ b/server/models/server/server.ts @@ -1,124 +1,109 @@ import * as Sequelize from 'sequelize' -import { isHostValid, logger } from '../../helpers' +import { AllowNull, Column, CreatedAt, Default, Is, IsInt, Max, Model, Table, UpdatedAt } from 'sequelize-typescript' +import { logger } from '../../helpers' +import { isHostValid } from '../../helpers/custom-validators/servers' import { SERVERS_SCORE } from '../../initializers' -import { addMethodsToModel } from '../utils' -import { ServerAttributes, ServerInstance, ServerMethods } from './server-interface' +import { throwIfNotValid } from '../utils' -let Server: Sequelize.Model -let updateServersScoreAndRemoveBadOnes: ServerMethods.UpdateServersScoreAndRemoveBadOnes - -export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { - Server = sequelize.define('Server', +@Table({ + tableName: 'server', + indexes: [ { - host: { - type: DataTypes.STRING, - allowNull: false, - validate: { - isHost: value => { - const res = isHostValid(value) - if (res === false) throw new Error('Host not valid.') - } - } - }, - score: { - type: DataTypes.INTEGER, - defaultValue: SERVERS_SCORE.BASE, - allowNull: false, - validate: { - isInt: true, - max: SERVERS_SCORE.MAX - } - } + fields: [ 'host' ], + unique: true }, { - indexes: [ - { - fields: [ 'host' ], - unique: true - }, - { - fields: [ 'score' ] - } - ] + fields: [ 'score' ] } - ) - - const classMethods = [ - updateServersScoreAndRemoveBadOnes ] - addMethodsToModel(Server, classMethods) - - return Server -} - -// ------------------------------ Statics ------------------------------ - -updateServersScoreAndRemoveBadOnes = function (goodServers: number[], badServers: number[]) { - logger.info('Updating %d good servers and %d bad servers scores.', goodServers.length, badServers.length) +}) +export class ServerModel extends Model { + + @AllowNull(false) + @Is('Host', value => throwIfNotValid(value, isHostValid, 'valid host')) + @Column + host: string + + @AllowNull(false) + @Default(SERVERS_SCORE.BASE) + @IsInt + @Max(SERVERS_SCORE.MAX) + @Column + score: number + + @CreatedAt + createdAt: Date + + @UpdatedAt + updatedAt: Date + + static updateServersScoreAndRemoveBadOnes (goodServers: number[], badServers: number[]) { + logger.info('Updating %d good servers and %d bad servers scores.', goodServers.length, badServers.length) + + if (goodServers.length !== 0) { + ServerModel.incrementScores(goodServers, SERVERS_SCORE.BONUS) + .catch(err => { + logger.error('Cannot increment scores of good servers.', err) + }) + } - if (goodServers.length !== 0) { - incrementScores(goodServers, SERVERS_SCORE.BONUS).catch(err => { - logger.error('Cannot increment scores of good servers.', err) - }) - } + if (badServers.length !== 0) { + ServerModel.incrementScores(badServers, SERVERS_SCORE.PENALTY) + .then(() => ServerModel.removeBadServers()) + .catch(err => { + if (err) logger.error('Cannot decrement scores of bad servers.', err) + }) - if (badServers.length !== 0) { - incrementScores(badServers, SERVERS_SCORE.PENALTY) - .then(() => removeBadServers()) - .catch(err => { - if (err) logger.error('Cannot decrement scores of bad servers.', err) - }) + } } -} - -// --------------------------------------------------------------------------- -// Remove servers with a score of 0 (too many requests where they were unreachable) -async function removeBadServers () { - try { - const servers = await listBadServers() + // Remove servers with a score of 0 (too many requests where they were unreachable) + private static async removeBadServers () { + try { + const servers = await ServerModel.listBadServers() - const serversRemovePromises = servers.map(server => server.destroy()) - await Promise.all(serversRemovePromises) + const serversRemovePromises = servers.map(server => server.destroy()) + await Promise.all(serversRemovePromises) - const numberOfServersRemoved = servers.length + const numberOfServersRemoved = servers.length - if (numberOfServersRemoved) { - logger.info('Removed %d servers.', numberOfServersRemoved) - } else { - logger.info('No need to remove bad servers.') + if (numberOfServersRemoved) { + logger.info('Removed %d servers.', numberOfServersRemoved) + } else { + logger.info('No need to remove bad servers.') + } + } catch (err) { + logger.error('Cannot remove bad servers.', err) } - } catch (err) { - logger.error('Cannot remove bad servers.', err) } -} -function incrementScores (ids: number[], value: number) { - const update = { - score: Sequelize.literal('score +' + value) - } + private static incrementScores (ids: number[], value: number) { + const update = { + score: Sequelize.literal('score +' + value) + } - const options = { - where: { - id: { - [Sequelize.Op.in]: ids - } - }, - // In this case score is a literal and not an integer so we do not validate it - validate: false - } + const options = { + where: { + id: { + [Sequelize.Op.in]: ids + } + }, + // In this case score is a literal and not an integer so we do not validate it + validate: false + } - return Server.update(update, options) -} + return ServerModel.update(update, options) + } -function listBadServers () { - const query = { - where: { - score: { - [Sequelize.Op.lte]: 0 + private static listBadServers () { + const query = { + where: { + score: { + [Sequelize.Op.lte]: 0 + } } } - } - return Server.findAll(query) + return ServerModel.findAll(query) + } } diff --git a/server/models/utils.ts b/server/models/utils.ts index 1bf61d2a6..1606453e0 100644 --- a/server/models/utils.ts +++ b/server/models/utils.ts @@ -14,22 +14,23 @@ function getSort (value: string) { return [ field, direction ] } -function addMethodsToModel (model: any, classMethods: Function[], instanceMethods: Function[] = []) { - classMethods.forEach(m => model[m.name] = m) - instanceMethods.forEach(m => model.prototype[m.name] = m) -} - function getSortOnModel (model: any, value: string) { let sort = getSort(value) - if (model) return [ { model: model }, sort[0], sort[1] ] + if (model) return [ model, sort[0], sort[1] ] return sort } +function throwIfNotValid (value: any, validator: (value: any) => boolean, fieldName = 'value') { + if (validator(value) === false) { + throw new Error(`"${value}" is not a valid ${fieldName}.`) + } +} + // --------------------------------------------------------------------------- export { - addMethodsToModel, getSort, - getSortOnModel + getSortOnModel, + throwIfNotValid } diff --git a/server/models/video/index.ts b/server/models/video/index.ts deleted file mode 100644 index e17bbfab4..000000000 --- a/server/models/video/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export * from './tag-interface' -export * from './video-abuse-interface' -export * from './video-blacklist-interface' -export * from './video-channel-interface' -export * from './video-tag-interface' -export * from './video-file-interface' -export * from './video-interface' -export * from './video-share-interface' -export * from './video-channel-share-interface' diff --git a/server/models/video/tag-interface.ts b/server/models/video/tag-interface.ts deleted file mode 100644 index 08e5c3246..000000000 --- a/server/models/video/tag-interface.ts +++ /dev/null @@ -1,20 +0,0 @@ -import * as Sequelize from 'sequelize' -import * as Promise from 'bluebird' - -export namespace TagMethods { - export type FindOrCreateTags = (tags: string[], transaction: Sequelize.Transaction) => Promise -} - -export interface TagClass { - findOrCreateTags: TagMethods.FindOrCreateTags -} - -export interface TagAttributes { - name: string -} - -export interface TagInstance extends TagClass, TagAttributes, Sequelize.Instance { - id: number -} - -export interface TagModel extends TagClass, Sequelize.Model {} diff --git a/server/models/video/tag.ts b/server/models/video/tag.ts index 0c0757fc8..0ae74d808 100644 --- a/server/models/video/tag.ts +++ b/server/models/video/tag.ts @@ -1,73 +1,60 @@ -import * as Sequelize from 'sequelize' -import * as Promise from 'bluebird' - -import { addMethodsToModel } from '../utils' -import { - TagInstance, - TagAttributes, - - TagMethods -} from './tag-interface' - -let Tag: Sequelize.Model -let findOrCreateTags: TagMethods.FindOrCreateTags - -export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { - Tag = sequelize.define('Tag', +import * as Bluebird from 'bluebird' +import { Transaction } from 'sequelize' +import { AllowNull, BelongsToMany, Column, CreatedAt, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' +import { isVideoTagValid } from '../../helpers/custom-validators/videos' +import { throwIfNotValid } from '../utils' +import { VideoModel } from './video' +import { VideoTagModel } from './video-tag' + +@Table({ + tableName: 'tag', + timestamps: false, + indexes: [ { - name: { - type: DataTypes.STRING, - allowNull: false - } - }, - { - timestamps: false, - indexes: [ - { - fields: [ 'name' ], - unique: true - } - ] + fields: [ 'name' ], + unique: true } - ) - - const classMethods = [ - associate, - - findOrCreateTags ] - addMethodsToModel(Tag, classMethods) +}) +export class TagModel extends Model { - return Tag -} + @AllowNull(false) + @Is('VideoTag', value => throwIfNotValid(value, isVideoTagValid, 'tag')) + @Column + name: string -// --------------------------------------------------------------------------- + @CreatedAt + createdAt: Date -function associate (models) { - Tag.belongsToMany(models.Video, { + @UpdatedAt + updatedAt: Date + + @BelongsToMany(() => VideoModel, { foreignKey: 'tagId', - through: models.VideoTag, + through: () => VideoTagModel, onDelete: 'CASCADE' }) -} - -findOrCreateTags = function (tags: string[], transaction: Sequelize.Transaction) { - const tasks: Promise[] = [] - tags.forEach(tag => { - const query: Sequelize.FindOrInitializeOptions = { - where: { - name: tag - }, - defaults: { - name: tag + Videos: VideoModel[] + + static findOrCreateTags (tags: string[], transaction: Transaction) { + const tasks: Bluebird[] = [] + tags.forEach(tag => { + const query = { + where: { + name: tag + }, + defaults: { + name: tag + } } - } - if (transaction) query.transaction = transaction + if (transaction) query['transaction'] = transaction - const promise = Tag.findOrCreate(query).then(([ tagInstance ]) => tagInstance) - tasks.push(promise) - }) + const promise = TagModel.findOrCreate(query) + .then(([ tagInstance ]) => tagInstance) + tasks.push(promise) + }) - return Promise.all(tasks) + return Promise.all(tasks) + } } diff --git a/server/models/video/video-abuse-interface.ts b/server/models/video/video-abuse-interface.ts deleted file mode 100644 index feafc4a19..000000000 --- a/server/models/video/video-abuse-interface.ts +++ /dev/null @@ -1,41 +0,0 @@ -import * as Promise from 'bluebird' -import * as Sequelize from 'sequelize' -import { ResultList } from '../../../shared' -import { VideoAbuse as FormattedVideoAbuse } from '../../../shared/models/videos/video-abuse.model' -import { AccountInstance } from '../account/account-interface' -import { ServerInstance } from '../server/server-interface' -import { VideoInstance } from './video-interface' -import { VideoAbuseObject } from '../../../shared/models/activitypub/objects/video-abuse-object' - -export namespace VideoAbuseMethods { - export type ToFormattedJSON = (this: VideoAbuseInstance) => FormattedVideoAbuse - - export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList > - export type ToActivityPubObject = () => VideoAbuseObject -} - -export interface VideoAbuseClass { - listForApi: VideoAbuseMethods.ListForApi - toActivityPubObject: VideoAbuseMethods.ToActivityPubObject -} - -export interface VideoAbuseAttributes { - reason: string - videoId: number - reporterAccountId: number - - Account?: AccountInstance - Video?: VideoInstance -} - -export interface VideoAbuseInstance extends VideoAbuseClass, VideoAbuseAttributes, Sequelize.Instance { - id: number - createdAt: Date - updatedAt: Date - - Server: ServerInstance - - toFormattedJSON: VideoAbuseMethods.ToFormattedJSON -} - -export interface VideoAbuseModel extends VideoAbuseClass, Sequelize.Model {} diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts index d09f5f7a1..d0ee969fb 100644 --- a/server/models/video/video-abuse.ts +++ b/server/models/video/video-abuse.ts @@ -1,142 +1,116 @@ -import * as Sequelize from 'sequelize' - +import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' +import { VideoAbuseObject } from '../../../shared/models/activitypub/objects' +import { isVideoAbuseReasonValid } from '../../helpers/custom-validators/videos' import { CONFIG } from '../../initializers' -import { isVideoAbuseReasonValid } from '../../helpers' - -import { addMethodsToModel, getSort } from '../utils' -import { - VideoAbuseInstance, - VideoAbuseAttributes, - - VideoAbuseMethods -} from './video-abuse-interface' -import { VideoAbuseObject } from '../../../shared/models/activitypub/objects/video-abuse-object' - -let VideoAbuse: Sequelize.Model -let toFormattedJSON: VideoAbuseMethods.ToFormattedJSON -let listForApi: VideoAbuseMethods.ListForApi -let toActivityPubObject: VideoAbuseMethods.ToActivityPubObject - -export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { - VideoAbuse = sequelize.define('VideoAbuse', +import { AccountModel } from '../account/account' +import { ServerModel } from '../server/server' +import { getSort, throwIfNotValid } from '../utils' +import { VideoModel } from './video' + +@Table({ + tableName: 'videoAbuse', + indexes: [ { - reason: { - type: DataTypes.STRING, - allowNull: false, - validate: { - reasonValid: value => { - const res = isVideoAbuseReasonValid(value) - if (res === false) throw new Error('Video abuse reason is not valid.') - } - } - } + fields: [ 'videoId' ] }, { - indexes: [ - { - fields: [ 'videoId' ] - }, - { - fields: [ 'reporterAccountId' ] - } - ] + fields: [ 'reporterAccountId' ] } - ) - - const classMethods = [ - associate, - - listForApi - ] - const instanceMethods = [ - toFormattedJSON, - toActivityPubObject ] - addMethodsToModel(VideoAbuse, classMethods, instanceMethods) - - return VideoAbuse -} - -// ------------------------------ METHODS ------------------------------ - -toFormattedJSON = function (this: VideoAbuseInstance) { - let reporterServerHost - - if (this.Account.Server) { - reporterServerHost = this.Account.Server.host - } else { - // It means it's our video - reporterServerHost = CONFIG.WEBSERVER.HOST - } - - const json = { - id: this.id, - reason: this.reason, - reporterUsername: this.Account.name, - reporterServerHost, - videoId: this.Video.id, - videoUUID: this.Video.uuid, - videoName: this.Video.name, - createdAt: this.createdAt - } +}) +export class VideoAbuseModel extends Model { - return json -} + @AllowNull(false) + @Is('VideoAbuseReason', value => throwIfNotValid(value, isVideoAbuseReasonValid, 'reason')) + @Column + reason: string -toActivityPubObject = function (this: VideoAbuseInstance) { - const videoAbuseObject: VideoAbuseObject = { - type: 'Flag' as 'Flag', - content: this.reason, - object: this.Video.url - } + @CreatedAt + createdAt: Date - return videoAbuseObject -} + @UpdatedAt + updatedAt: Date -// ------------------------------ STATICS ------------------------------ + @ForeignKey(() => AccountModel) + @Column + reporterAccountId: number -function associate (models) { - VideoAbuse.belongsTo(models.Account, { + @BelongsTo(() => AccountModel, { foreignKey: { - name: 'reporterAccountId', allowNull: false }, - onDelete: 'CASCADE' + onDelete: 'cascade' }) + Account: AccountModel + + @ForeignKey(() => VideoModel) + @Column + videoId: number - VideoAbuse.belongsTo(models.Video, { + @BelongsTo(() => VideoModel, { foreignKey: { - name: 'videoId', allowNull: false }, - onDelete: 'CASCADE' + onDelete: 'cascade' }) -} + Video: VideoModel + + static listForApi (start: number, count: number, sort: string) { + const query = { + offset: start, + limit: count, + order: [ getSort(sort) ], + include: [ + { + model: AccountModel, + required: true, + include: [ + { + model: ServerModel, + required: false + } + ] + }, + { + model: VideoModel, + required: true + } + ] + } -listForApi = function (start: number, count: number, sort: string) { - const query = { - offset: start, - limit: count, - order: [ getSort(sort) ], - include: [ - { - model: VideoAbuse['sequelize'].models.Account, - required: true, - include: [ - { - model: VideoAbuse['sequelize'].models.Server, - required: false - } - ] - }, - { - model: VideoAbuse['sequelize'].models.Video, - required: true - } - ] + return VideoAbuseModel.findAndCountAll(query) + .then(({ rows, count }) => { + return { total: count, data: rows } + }) } - return VideoAbuse.findAndCountAll(query).then(({ rows, count }) => { - return { total: count, data: rows } - }) + toFormattedJSON () { + let reporterServerHost + + if (this.Account.Server) { + reporterServerHost = this.Account.Server.host + } else { + // It means it's our video + reporterServerHost = CONFIG.WEBSERVER.HOST + } + + return { + id: this.id, + reason: this.reason, + reporterUsername: this.Account.name, + reporterServerHost, + videoId: this.Video.id, + videoUUID: this.Video.uuid, + videoName: this.Video.name, + createdAt: this.createdAt + } + } + + toActivityPubObject (): VideoAbuseObject { + return { + type: 'Flag' as 'Flag', + content: this.reason, + object: this.Video.url + } + } } diff --git a/server/models/video/video-blacklist-interface.ts b/server/models/video/video-blacklist-interface.ts deleted file mode 100644 index be2483d4c..000000000 --- a/server/models/video/video-blacklist-interface.ts +++ /dev/null @@ -1,39 +0,0 @@ -import * as Sequelize from 'sequelize' -import * as Promise from 'bluebird' - -import { SortType } from '../../helpers' -import { ResultList } from '../../../shared' -import { VideoInstance } from './video-interface' - -// Don't use barrel, import just what we need -import { BlacklistedVideo as FormattedBlacklistedVideo } from '../../../shared/models/videos/video-blacklist.model' - -export namespace BlacklistedVideoMethods { - export type ToFormattedJSON = (this: BlacklistedVideoInstance) => FormattedBlacklistedVideo - export type ListForApi = (start: number, count: number, sort: SortType) => Promise< ResultList > - export type LoadByVideoId = (id: number) => Promise -} - -export interface BlacklistedVideoClass { - toFormattedJSON: BlacklistedVideoMethods.ToFormattedJSON - listForApi: BlacklistedVideoMethods.ListForApi - loadByVideoId: BlacklistedVideoMethods.LoadByVideoId -} - -export interface BlacklistedVideoAttributes { - videoId: number - - Video?: VideoInstance -} - -export interface BlacklistedVideoInstance - extends BlacklistedVideoClass, BlacklistedVideoAttributes, Sequelize.Instance { - id: number - createdAt: Date - updatedAt: Date - - toFormattedJSON: BlacklistedVideoMethods.ToFormattedJSON -} - -export interface BlacklistedVideoModel - extends BlacklistedVideoClass, Sequelize.Model {} diff --git a/server/models/video/video-blacklist.ts b/server/models/video/video-blacklist.ts index ae8286285..6db562719 100644 --- a/server/models/video/video-blacklist.ts +++ b/server/models/video/video-blacklist.ts @@ -1,104 +1,80 @@ -import * as Sequelize from 'sequelize' - +import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' import { SortType } from '../../helpers' -import { addMethodsToModel, getSortOnModel } from '../utils' -import { VideoInstance } from './video-interface' -import { - BlacklistedVideoInstance, - BlacklistedVideoAttributes, - - BlacklistedVideoMethods -} from './video-blacklist-interface' - -let BlacklistedVideo: Sequelize.Model -let toFormattedJSON: BlacklistedVideoMethods.ToFormattedJSON -let listForApi: BlacklistedVideoMethods.ListForApi -let loadByVideoId: BlacklistedVideoMethods.LoadByVideoId +import { getSortOnModel } from '../utils' +import { VideoModel } from './video' -export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { - BlacklistedVideo = sequelize.define('BlacklistedVideo', - {}, +@Table({ + tableName: 'videoBlacklist', + indexes: [ { - indexes: [ - { - fields: [ 'videoId' ], - unique: true - } - ] + fields: [ 'videoId' ], + unique: true } - ) - - const classMethods = [ - associate, - - listForApi, - loadByVideoId ] - const instanceMethods = [ - toFormattedJSON - ] - addMethodsToModel(BlacklistedVideo, classMethods, instanceMethods) - - return BlacklistedVideo -} +}) +export class VideoBlacklistModel extends Model { -// ------------------------------ METHODS ------------------------------ + @CreatedAt + createdAt: Date -toFormattedJSON = function (this: BlacklistedVideoInstance) { - let video: VideoInstance - - video = this.Video - - return { - id: this.id, - videoId: this.videoId, - createdAt: this.createdAt, - updatedAt: this.updatedAt, - name: video.name, - uuid: video.uuid, - description: video.description, - duration: video.duration, - views: video.views, - likes: video.likes, - dislikes: video.dislikes, - nsfw: video.nsfw - } -} + @UpdatedAt + updatedAt: Date -// ------------------------------ STATICS ------------------------------ + @ForeignKey(() => VideoModel) + @Column + videoId: number -function associate (models) { - BlacklistedVideo.belongsTo(models.Video, { + @BelongsTo(() => VideoModel, { foreignKey: { - name: 'videoId', allowNull: false }, - onDelete: 'CASCADE' + onDelete: 'cascade' }) -} + Video: VideoModel + + static listForApi (start: number, count: number, sort: SortType) { + const query = { + offset: start, + limit: count, + order: [ getSortOnModel(sort.sortModel, sort.sortValue) ], + include: [ { model: VideoModel } ] + } -listForApi = function (start: number, count: number, sort: SortType) { - const query = { - offset: start, - limit: count, - order: [ getSortOnModel(sort.sortModel, sort.sortValue) ], - include: [ { model: BlacklistedVideo['sequelize'].models.Video } ] + return VideoBlacklistModel.findAndCountAll(query) + .then(({ rows, count }) => { + return { + data: rows, + total: count + } + }) } - return BlacklistedVideo.findAndCountAll(query).then(({ rows, count }) => { - return { - data: rows, - total: count + static loadByVideoId (id: number) { + const query = { + where: { + videoId: id + } } - }) -} -loadByVideoId = function (id: number) { - const query = { - where: { - videoId: id - } + return VideoBlacklistModel.findOne(query) } - return BlacklistedVideo.findOne(query) + toFormattedJSON () { + const video = this.Video + + return { + id: this.id, + videoId: this.videoId, + createdAt: this.createdAt, + updatedAt: this.updatedAt, + name: video.name, + uuid: video.uuid, + description: video.description, + duration: video.duration, + views: video.views, + likes: video.likes, + dislikes: video.dislikes, + nsfw: video.nsfw + } + } } diff --git a/server/models/video/video-channel-interface.ts b/server/models/video/video-channel-interface.ts deleted file mode 100644 index 21f81e901..000000000 --- a/server/models/video/video-channel-interface.ts +++ /dev/null @@ -1,64 +0,0 @@ -import * as Promise from 'bluebird' -import * as Sequelize from 'sequelize' - -import { ResultList } from '../../../shared' -import { VideoChannelObject } from '../../../shared/models/activitypub/objects/video-channel-object' -import { VideoChannel as FormattedVideoChannel } from '../../../shared/models/videos/video-channel.model' -import { AccountInstance } from '../account/account-interface' -import { VideoInstance } from './video-interface' -import { VideoChannelShareInstance } from './video-channel-share-interface' - -export namespace VideoChannelMethods { - export type ToFormattedJSON = (this: VideoChannelInstance) => FormattedVideoChannel - export type ToActivityPubObject = (this: VideoChannelInstance) => VideoChannelObject - export type IsOwned = (this: VideoChannelInstance) => boolean - - export type CountByAccount = (accountId: number) => Promise - export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList > - export type LoadByIdAndAccount = (id: number, accountId: number) => Promise - export type ListByAccount = (accountId: number) => Promise< ResultList > - export type LoadAndPopulateAccount = (id: number) => Promise - export type LoadByUUIDAndPopulateAccount = (uuid: string) => Promise - export type LoadByUUID = (uuid: string, t?: Sequelize.Transaction) => Promise - export type LoadByHostAndUUID = (uuid: string, serverHost: string, t?: Sequelize.Transaction) => Promise - export type LoadAndPopulateAccountAndVideos = (id: number) => Promise - export type LoadByUrl = (uuid: string, t?: Sequelize.Transaction) => Promise - export type LoadByUUIDOrUrl = (uuid: string, url: string, t?: Sequelize.Transaction) => Promise -} - -export interface VideoChannelClass { - countByAccount: VideoChannelMethods.CountByAccount - listForApi: VideoChannelMethods.ListForApi - listByAccount: VideoChannelMethods.ListByAccount - loadByIdAndAccount: VideoChannelMethods.LoadByIdAndAccount - loadAndPopulateAccount: VideoChannelMethods.LoadAndPopulateAccount - loadByUUIDAndPopulateAccount: VideoChannelMethods.LoadByUUIDAndPopulateAccount - loadAndPopulateAccountAndVideos: VideoChannelMethods.LoadAndPopulateAccountAndVideos - loadByUrl: VideoChannelMethods.LoadByUrl - loadByUUIDOrUrl: VideoChannelMethods.LoadByUUIDOrUrl -} - -export interface VideoChannelAttributes { - id?: number - uuid?: string - name: string - description: string - remote: boolean - url?: string - - Account?: AccountInstance - Videos?: VideoInstance[] - VideoChannelShares?: VideoChannelShareInstance[] -} - -export interface VideoChannelInstance extends VideoChannelClass, VideoChannelAttributes, Sequelize.Instance { - id: number - createdAt: Date - updatedAt: Date - - isOwned: VideoChannelMethods.IsOwned - toFormattedJSON: VideoChannelMethods.ToFormattedJSON - toActivityPubObject: VideoChannelMethods.ToActivityPubObject -} - -export interface VideoChannelModel extends VideoChannelClass, Sequelize.Model {} diff --git a/server/models/video/video-channel-share-interface.ts b/server/models/video/video-channel-share-interface.ts deleted file mode 100644 index 2fff41a1b..000000000 --- a/server/models/video/video-channel-share-interface.ts +++ /dev/null @@ -1,32 +0,0 @@ -import * as Bluebird from 'bluebird' -import * as Sequelize from 'sequelize' -import { AccountInstance } from '../account/account-interface' -import { VideoChannelInstance } from './video-channel-interface' - -export namespace VideoChannelShareMethods { - export type LoadAccountsByShare = (videoChannelId: number, t: Sequelize.Transaction) => Bluebird - export type Load = (accountId: number, videoId: number, t: Sequelize.Transaction) => Bluebird -} - -export interface VideoChannelShareClass { - loadAccountsByShare: VideoChannelShareMethods.LoadAccountsByShare - load: VideoChannelShareMethods.Load -} - -export interface VideoChannelShareAttributes { - accountId: number - videoChannelId: number -} - -export interface VideoChannelShareInstance - extends VideoChannelShareClass, VideoChannelShareAttributes, Sequelize.Instance { - id: number - createdAt: Date - updatedAt: Date - - Account?: AccountInstance - VideoChannel?: VideoChannelInstance -} - -export interface VideoChannelShareModel - extends VideoChannelShareClass, Sequelize.Model {} diff --git a/server/models/video/video-channel-share.ts b/server/models/video/video-channel-share.ts index 2e9b658a3..cdba32fcd 100644 --- a/server/models/video/video-channel-share.ts +++ b/server/models/video/video-channel-share.ts @@ -1,85 +1,79 @@ import * as Sequelize from 'sequelize' +import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' +import { AccountModel } from '../account/account' +import { VideoChannelModel } from './video-channel' -import { addMethodsToModel } from '../utils' -import { VideoChannelShareAttributes, VideoChannelShareInstance, VideoChannelShareMethods } from './video-channel-share-interface' - -let VideoChannelShare: Sequelize.Model -let loadAccountsByShare: VideoChannelShareMethods.LoadAccountsByShare -let load: VideoChannelShareMethods.Load - -export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { - VideoChannelShare = sequelize.define('VideoChannelShare', - { }, +@Table({ + tableName: 'videoChannelShare', + indexes: [ { - indexes: [ - { - fields: [ 'accountId' ] - }, - { - fields: [ 'videoChannelId' ] - } - ] + fields: [ 'accountId' ] + }, + { + fields: [ 'videoChannelId' ] } - ) - - const classMethods = [ - associate, - load, - loadAccountsByShare ] - addMethodsToModel(VideoChannelShare, classMethods) +}) +export class VideoChannelShareModel extends Model { + @CreatedAt + createdAt: Date - return VideoChannelShare -} + @UpdatedAt + updatedAt: Date -// ------------------------------ METHODS ------------------------------ + @ForeignKey(() => AccountModel) + @Column + accountId: number -function associate (models) { - VideoChannelShare.belongsTo(models.Account, { + @BelongsTo(() => AccountModel, { foreignKey: { - name: 'accountId', allowNull: false }, onDelete: 'cascade' }) + Account: AccountModel - VideoChannelShare.belongsTo(models.VideoChannel, { + @ForeignKey(() => VideoChannelModel) + @Column + videoChannelId: number + + @BelongsTo(() => VideoChannelModel, { foreignKey: { - name: 'videoChannelId', - allowNull: true + allowNull: false }, onDelete: 'cascade' }) -} - -load = function (accountId: number, videoChannelId: number, t: Sequelize.Transaction) { - return VideoChannelShare.findOne({ - where: { - accountId, - videoChannelId - }, - include: [ - VideoChannelShare['sequelize'].models.Account, - VideoChannelShare['sequelize'].models.VideoChannel - ], - transaction: t - }) -} + VideoChannel: VideoChannelModel -loadAccountsByShare = function (videoChannelId: number, t: Sequelize.Transaction) { - const query = { - where: { - videoChannelId - }, - include: [ - { - model: VideoChannelShare['sequelize'].models.Account, - required: true - } - ], - transaction: t + static load (accountId: number, videoChannelId: number, t: Sequelize.Transaction) { + return VideoChannelShareModel.findOne({ + where: { + accountId, + videoChannelId + }, + include: [ + AccountModel, + VideoChannelModel + ], + transaction: t + }) } - return VideoChannelShare.findAll(query) - .then(res => res.map(r => r.Account)) + static loadAccountsByShare (videoChannelId: number, t: Sequelize.Transaction) { + const query = { + where: { + videoChannelId + }, + include: [ + { + model: AccountModel, + required: true + } + ], + transaction: t + } + + return VideoChannelShareModel.findAll(query) + .then(res => res.map(r => r.Account)) + } } diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 54f12dce3..9b545a4ef 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts @@ -1,371 +1,341 @@ import * as Sequelize from 'sequelize' -import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../../helpers' -import { CONSTRAINTS_FIELDS } from '../../initializers/constants' -import { sendDeleteVideoChannel } from '../../lib/activitypub/send/send-delete' - -import { addMethodsToModel, getSort } from '../utils' -import { VideoChannelAttributes, VideoChannelInstance, VideoChannelMethods } from './video-channel-interface' -import { getAnnounceActivityPubUrl } from '../../lib/activitypub/url' -import { activityPubCollection } from '../../helpers/activitypub' -import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' - -let VideoChannel: Sequelize.Model -let toFormattedJSON: VideoChannelMethods.ToFormattedJSON -let toActivityPubObject: VideoChannelMethods.ToActivityPubObject -let isOwned: VideoChannelMethods.IsOwned -let countByAccount: VideoChannelMethods.CountByAccount -let listForApi: VideoChannelMethods.ListForApi -let listByAccount: VideoChannelMethods.ListByAccount -let loadByIdAndAccount: VideoChannelMethods.LoadByIdAndAccount -let loadByUUID: VideoChannelMethods.LoadByUUID -let loadAndPopulateAccount: VideoChannelMethods.LoadAndPopulateAccount -let loadByUUIDAndPopulateAccount: VideoChannelMethods.LoadByUUIDAndPopulateAccount -let loadByHostAndUUID: VideoChannelMethods.LoadByHostAndUUID -let loadAndPopulateAccountAndVideos: VideoChannelMethods.LoadAndPopulateAccountAndVideos -let loadByUrl: VideoChannelMethods.LoadByUrl -let loadByUUIDOrUrl: VideoChannelMethods.LoadByUUIDOrUrl - -export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { - VideoChannel = sequelize.define('VideoChannel', +import { + AfterDestroy, + AllowNull, + BelongsTo, + Column, + CreatedAt, + DataType, + Default, + ForeignKey, + HasMany, + Is, + IsUUID, + Model, + Table, + UpdatedAt +} from 'sequelize-typescript' +import { IFindOptions } from 'sequelize-typescript/lib/interfaces/IFindOptions' +import { activityPubCollection } from '../../helpers' +import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub' +import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../../helpers/custom-validators/video-channels' +import { CONSTRAINTS_FIELDS } from '../../initializers' +import { getAnnounceActivityPubUrl } from '../../lib/activitypub' +import { sendDeleteVideoChannel } from '../../lib/activitypub/send' +import { AccountModel } from '../account/account' +import { ServerModel } from '../server/server' +import { getSort, throwIfNotValid } from '../utils' +import { VideoModel } from './video' +import { VideoChannelShareModel } from './video-channel-share' + +@Table({ + tableName: 'videoChannel', + indexes: [ { - uuid: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - allowNull: false, - validate: { - isUUID: 4 - } - }, - name: { - type: DataTypes.STRING, - allowNull: false, - validate: { - nameValid: value => { - const res = isVideoChannelNameValid(value) - if (res === false) throw new Error('Video channel name is not valid.') - } - } - }, - description: { - type: DataTypes.STRING, - allowNull: true, - validate: { - descriptionValid: value => { - const res = isVideoChannelDescriptionValid(value) - if (res === false) throw new Error('Video channel description is not valid.') - } - } - }, - remote: { - type: DataTypes.BOOLEAN, - allowNull: false, - defaultValue: false - }, - url: { - type: DataTypes.STRING(CONSTRAINTS_FIELDS.VIDEO_CHANNELS.URL.max), - allowNull: false, - validate: { - urlValid: value => { - const res = isActivityPubUrlValid(value) - if (res === false) throw new Error('Video channel URL is not valid.') - } - } - } - }, - { - indexes: [ - { - fields: [ 'accountId' ] - } - ], - hooks: { - afterDestroy - } + fields: [ 'accountId' ] } - ) - - const classMethods = [ - associate, - - listForApi, - listByAccount, - loadByIdAndAccount, - loadAndPopulateAccount, - loadByUUIDAndPopulateAccount, - loadByUUID, - loadByHostAndUUID, - loadAndPopulateAccountAndVideos, - countByAccount, - loadByUrl, - loadByUUIDOrUrl ] - const instanceMethods = [ - isOwned, - toFormattedJSON, - toActivityPubObject - ] - addMethodsToModel(VideoChannel, classMethods, instanceMethods) +}) +export class VideoChannelModel extends Model { - return VideoChannel -} + @AllowNull(false) + @Default(DataType.UUIDV4) + @IsUUID(4) + @Column(DataType.UUID) + uuid: string -// ------------------------------ METHODS ------------------------------ + @AllowNull(false) + @Is('VideoChannelName', value => throwIfNotValid(value, isVideoChannelNameValid, 'name')) + @Column + name: string -isOwned = function (this: VideoChannelInstance) { - return this.remote === false -} - -toFormattedJSON = function (this: VideoChannelInstance) { - const json = { - id: this.id, - uuid: this.uuid, - name: this.name, - description: this.description, - isLocal: this.isOwned(), - createdAt: this.createdAt, - updatedAt: this.updatedAt - } + @AllowNull(true) + @Is('VideoChannelDescription', value => throwIfNotValid(value, isVideoChannelDescriptionValid, 'description')) + @Column + description: string - if (this.Account !== undefined) { - json['owner'] = { - name: this.Account.name, - uuid: this.Account.uuid - } - } + @AllowNull(false) + @Column + remote: boolean - if (Array.isArray(this.Videos)) { - json['videos'] = this.Videos.map(v => v.toFormattedJSON()) - } + @AllowNull(false) + @Is('VideoChannelUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url')) + @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_CHANNELS.URL.max)) + url: string - return json -} + @CreatedAt + createdAt: Date -toActivityPubObject = function (this: VideoChannelInstance) { - let sharesObject - if (Array.isArray(this.VideoChannelShares)) { - const shares: string[] = [] + @UpdatedAt + updatedAt: Date - for (const videoChannelShare of this.VideoChannelShares) { - const shareUrl = getAnnounceActivityPubUrl(this.url, videoChannelShare.Account) - shares.push(shareUrl) - } + @ForeignKey(() => AccountModel) + @Column + accountId: number - sharesObject = activityPubCollection(shares) - } - - const json = { - type: 'VideoChannel' as 'VideoChannel', - id: this.url, - uuid: this.uuid, - content: this.description, - name: this.name, - published: this.createdAt.toISOString(), - updated: this.updatedAt.toISOString(), - shares: sharesObject - } - - return json -} - -// ------------------------------ STATICS ------------------------------ + @BelongsTo(() => AccountModel, { + foreignKey: { + allowNull: false + }, + onDelete: 'CASCADE' + }) + Account: AccountModel -function associate (models) { - VideoChannel.belongsTo(models.Account, { + @HasMany(() => VideoModel, { foreignKey: { - name: 'accountId', + name: 'channelId', allowNull: false }, onDelete: 'CASCADE' }) + Videos: VideoModel[] - VideoChannel.hasMany(models.Video, { + @HasMany(() => VideoChannelShareModel, { foreignKey: { name: 'channelId', allowNull: false }, onDelete: 'CASCADE' }) -} + VideoChannelShares: VideoChannelShareModel[] -function afterDestroy (videoChannel: VideoChannelInstance) { - if (videoChannel.isOwned()) { - return sendDeleteVideoChannel(videoChannel, undefined) - } + @AfterDestroy + static sendDeleteIfOwned (instance: VideoChannelModel) { + if (instance.isOwned()) { + return sendDeleteVideoChannel(instance, undefined) + } - return undefined -} + return undefined + } -countByAccount = function (accountId: number) { - const query = { - where: { - accountId + static countByAccount (accountId: number) { + const query = { + where: { + accountId + } } + + return VideoChannelModel.count(query) } - return VideoChannel.count(query) -} + static listForApi (start: number, count: number, sort: string) { + const query = { + offset: start, + limit: count, + order: [ getSort(sort) ], + include: [ + { + model: AccountModel, + required: true, + include: [ { model: ServerModel, required: false } ] + } + ] + } -listForApi = function (start: number, count: number, sort: string) { - const query = { - offset: start, - limit: count, - order: [ getSort(sort) ], - include: [ - { - model: VideoChannel['sequelize'].models.Account, - required: true, - include: [ { model: VideoChannel['sequelize'].models.Server, required: false } ] - } - ] + return VideoChannelModel.findAndCountAll(query) + .then(({ rows, count }) => { + return { total: count, data: rows } + }) } - return VideoChannel.findAndCountAll(query).then(({ rows, count }) => { - return { total: count, data: rows } - }) -} + static listByAccount (accountId: number) { + const query = { + order: [ getSort('createdAt') ], + include: [ + { + model: AccountModel, + where: { + id: accountId + }, + required: true, + include: [ { model: ServerModel, required: false } ] + } + ] + } -listByAccount = function (accountId: number) { - const query = { - order: [ getSort('createdAt') ], - include: [ - { - model: VideoChannel['sequelize'].models.Account, - where: { - id: accountId - }, - required: true, - include: [ { model: VideoChannel['sequelize'].models.Server, required: false } ] - } - ] + return VideoChannelModel.findAndCountAll(query) + .then(({ rows, count }) => { + return { total: count, data: rows } + }) } - return VideoChannel.findAndCountAll(query).then(({ rows, count }) => { - return { total: count, data: rows } - }) -} + static loadByUUID (uuid: string, t?: Sequelize.Transaction) { + const query: IFindOptions = { + where: { + uuid + } + } + + if (t !== undefined) query.transaction = t -loadByUUID = function (uuid: string, t?: Sequelize.Transaction) { - const query: Sequelize.FindOptions = { - where: { - uuid + return VideoChannelModel.findOne(query) + } + + static loadByUrl (url: string, t?: Sequelize.Transaction) { + const query: IFindOptions = { + where: { + url + }, + include: [ AccountModel ] } + + if (t !== undefined) query.transaction = t + + return VideoChannelModel.findOne(query) } - if (t !== undefined) query.transaction = t + static loadByUUIDOrUrl (uuid: string, url: string, t?: Sequelize.Transaction) { + const query: IFindOptions = { + where: { + [ Sequelize.Op.or ]: [ + { uuid }, + { url } + ] + } + } - return VideoChannel.findOne(query) -} + if (t !== undefined) query.transaction = t -loadByUrl = function (url: string, t?: Sequelize.Transaction) { - const query: Sequelize.FindOptions = { - where: { - url - }, - include: [ VideoChannel['sequelize'].models.Account ] + return VideoChannelModel.findOne(query) } - if (t !== undefined) query.transaction = t + static loadByHostAndUUID (fromHost: string, uuid: string, t?: Sequelize.Transaction) { + const query: IFindOptions = { + where: { + uuid + }, + include: [ + { + model: AccountModel, + include: [ + { + model: ServerModel, + required: true, + where: { + host: fromHost + } + } + ] + } + ] + } - return VideoChannel.findOne(query) -} + if (t !== undefined) query.transaction = t + + return VideoChannelModel.findOne(query) + } -loadByUUIDOrUrl = function (uuid: string, url: string, t?: Sequelize.Transaction) { - const query: Sequelize.FindOptions = { - where: { - [Sequelize.Op.or]: [ - { uuid }, - { url } + static loadByIdAndAccount (id: number, accountId: number) { + const options = { + where: { + id, + accountId + }, + include: [ + { + model: AccountModel, + include: [ { model: ServerModel, required: false } ] + } ] } + + return VideoChannelModel.findOne(options) } - if (t !== undefined) query.transaction = t + static loadAndPopulateAccount (id: number) { + const options = { + include: [ + { + model: AccountModel, + include: [ { model: ServerModel, required: false } ] + } + ] + } - return VideoChannel.findOne(query) -} + return VideoChannelModel.findById(id, options) + } -loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Transaction) { - const query: Sequelize.FindOptions = { - where: { - uuid - }, - include: [ - { - model: VideoChannel['sequelize'].models.Account, - include: [ - { - model: VideoChannel['sequelize'].models.Server, - required: true, - where: { - host: fromHost - } - } - ] - } - ] + static loadByUUIDAndPopulateAccount (uuid: string) { + const options = { + where: { + uuid + }, + include: [ + { + model: AccountModel, + include: [ { model: ServerModel, required: false } ] + } + ] + } + + return VideoChannelModel.findOne(options) } - if (t !== undefined) query.transaction = t + static loadAndPopulateAccountAndVideos (id: number) { + const options = { + include: [ + { + model: AccountModel, + include: [ { model: ServerModel, required: false } ] + }, + VideoModel + ] + } - return VideoChannel.findOne(query) -} + return VideoChannelModel.findById(id, options) + } -loadByIdAndAccount = function (id: number, accountId: number) { - const options = { - where: { - id, - accountId - }, - include: [ - { - model: VideoChannel['sequelize'].models.Account, - include: [ { model: VideoChannel['sequelize'].models.Server, required: false } ] - } - ] + isOwned () { + return this.remote === false } - return VideoChannel.findOne(options) -} + toFormattedJSON () { + const json = { + id: this.id, + uuid: this.uuid, + name: this.name, + description: this.description, + isLocal: this.isOwned(), + createdAt: this.createdAt, + updatedAt: this.updatedAt + } -loadAndPopulateAccount = function (id: number) { - const options = { - include: [ - { - model: VideoChannel['sequelize'].models.Account, - include: [ { model: VideoChannel['sequelize'].models.Server, required: false } ] + if (this.Account !== undefined) { + json[ 'owner' ] = { + name: this.Account.name, + uuid: this.Account.uuid } - ] + } + + if (Array.isArray(this.Videos)) { + json[ 'videos' ] = this.Videos.map(v => v.toFormattedJSON()) + } + + return json } - return VideoChannel.findById(id, options) -} + toActivityPubObject () { + let sharesObject + if (Array.isArray(this.VideoChannelShares)) { + const shares: string[] = [] -loadByUUIDAndPopulateAccount = function (uuid: string) { - const options = { - where: { - uuid - }, - include: [ - { - model: VideoChannel['sequelize'].models.Account, - include: [ { model: VideoChannel['sequelize'].models.Server, required: false } ] + for (const videoChannelShare of this.VideoChannelShares) { + const shareUrl = getAnnounceActivityPubUrl(this.url, videoChannelShare.Account) + shares.push(shareUrl) } - ] - } - return VideoChannel.findOne(options) -} + sharesObject = activityPubCollection(shares) + } -loadAndPopulateAccountAndVideos = function (id: number) { - const options = { - include: [ - { - model: VideoChannel['sequelize'].models.Account, - include: [ { model: VideoChannel['sequelize'].models.Server, required: false } ] - }, - VideoChannel['sequelize'].models.Video - ] + return { + type: 'VideoChannel' as 'VideoChannel', + id: this.url, + uuid: this.uuid, + content: this.description, + name: this.name, + published: this.createdAt.toISOString(), + updated: this.updatedAt.toISOString(), + shares: sharesObject + } } - - return VideoChannel.findById(id, options) } diff --git a/server/models/video/video-file-interface.ts b/server/models/video/video-file-interface.ts deleted file mode 100644 index c9fb8b8ae..000000000 --- a/server/models/video/video-file-interface.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as Sequelize from 'sequelize' - -export namespace VideoFileMethods { -} - -export interface VideoFileClass { -} - -export interface VideoFileAttributes { - resolution: number - size: number - infoHash?: string - extname: string - - videoId?: number -} - -export interface VideoFileInstance extends VideoFileClass, VideoFileAttributes, Sequelize.Instance { - id: number - createdAt: Date - updatedAt: Date -} - -export interface VideoFileModel extends VideoFileClass, Sequelize.Model {} diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts index 600141994..df4067a4e 100644 --- a/server/models/video/video-file.ts +++ b/server/models/video/video-file.ts @@ -1,81 +1,56 @@ import { values } from 'lodash' -import * as Sequelize from 'sequelize' +import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' import { isVideoFileInfoHashValid, isVideoFileResolutionValid, isVideoFileSizeValid } from '../../helpers/custom-validators/videos' -import { CONSTRAINTS_FIELDS } from '../../initializers/constants' +import { CONSTRAINTS_FIELDS } from '../../initializers' +import { throwIfNotValid } from '../utils' +import { VideoModel } from './video' -import { addMethodsToModel } from '../utils' -import { VideoFileAttributes, VideoFileInstance } from './video-file-interface' - -let VideoFile: Sequelize.Model - -export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { - VideoFile = sequelize.define('VideoFile', +@Table({ + tableName: 'videoFile', + indexes: [ { - resolution: { - type: DataTypes.INTEGER, - allowNull: false, - validate: { - resolutionValid: value => { - const res = isVideoFileResolutionValid(value) - if (res === false) throw new Error('Video file resolution is not valid.') - } - } - }, - size: { - type: DataTypes.BIGINT, - allowNull: false, - validate: { - sizeValid: value => { - const res = isVideoFileSizeValid(value) - if (res === false) throw new Error('Video file size is not valid.') - } - } - }, - extname: { - type: DataTypes.ENUM(values(CONSTRAINTS_FIELDS.VIDEOS.EXTNAME)), - allowNull: false - }, - infoHash: { - type: DataTypes.STRING, - allowNull: false, - validate: { - infoHashValid: value => { - const res = isVideoFileInfoHashValid(value) - if (res === false) throw new Error('Video file info hash is not valid.') - } - } - } + fields: [ 'videoId' ] }, { - indexes: [ - { - fields: [ 'videoId' ] - }, - { - fields: [ 'infoHash' ] - } - ] + fields: [ 'infoHash' ] } - ) - - const classMethods = [ - associate ] - addMethodsToModel(VideoFile, classMethods) - - return VideoFile -} - -// ------------------------------ STATICS ------------------------------ - -function associate (models) { - VideoFile.belongsTo(models.Video, { +}) +export class VideoFileModel extends Model { + @CreatedAt + createdAt: Date + + @UpdatedAt + updatedAt: Date + + @AllowNull(false) + @Is('VideoFileResolution', value => throwIfNotValid(value, isVideoFileResolutionValid, 'resolution')) + @Column + resolution: number + + @AllowNull(false) + @Is('VideoFileSize', value => throwIfNotValid(value, isVideoFileSizeValid, 'size')) + @Column(DataType.BIGINT) + size: number + + @AllowNull(false) + @Column(DataType.ENUM(values(CONSTRAINTS_FIELDS.VIDEOS.EXTNAME))) + extname: string + + @AllowNull(false) + @Is('VideoFileSize', value => throwIfNotValid(value, isVideoFileInfoHashValid, 'info hash')) + @Column + infoHash: string + + @ForeignKey(() => VideoModel) + @Column + videoId: number + + @BelongsTo(() => VideoModel, { foreignKey: { - name: 'videoId', allowNull: false }, onDelete: 'CASCADE' }) + Video: VideoModel } - -// ------------------------------ METHODS ------------------------------ diff --git a/server/models/video/video-interface.ts b/server/models/video/video-interface.ts deleted file mode 100644 index 2a63350af..000000000 --- a/server/models/video/video-interface.ts +++ /dev/null @@ -1,150 +0,0 @@ -import * as Bluebird from 'bluebird' -import * as Sequelize from 'sequelize' -import { VideoTorrentObject } from '../../../shared/models/activitypub/objects/video-torrent-object' -import { ResultList } from '../../../shared/models/result-list.model' -import { Video as FormattedVideo, VideoDetails as FormattedDetailsVideo } from '../../../shared/models/videos/video.model' -import { AccountVideoRateInstance } from '../account/account-video-rate-interface' - -import { TagAttributes, TagInstance } from './tag-interface' -import { VideoChannelInstance } from './video-channel-interface' -import { VideoFileAttributes, VideoFileInstance } from './video-file-interface' -import { VideoShareInstance } from './video-share-interface' - -export namespace VideoMethods { - export type GetThumbnailName = (this: VideoInstance) => string - export type GetPreviewName = (this: VideoInstance) => string - export type IsOwned = (this: VideoInstance) => boolean - export type ToFormattedJSON = (this: VideoInstance) => FormattedVideo - export type ToFormattedDetailsJSON = (this: VideoInstance) => FormattedDetailsVideo - - export type GetOriginalFile = (this: VideoInstance) => VideoFileInstance - export type GetTorrentFileName = (this: VideoInstance, videoFile: VideoFileInstance) => string - export type GetVideoFilename = (this: VideoInstance, videoFile: VideoFileInstance) => string - export type CreatePreview = (this: VideoInstance, videoFile: VideoFileInstance) => Promise - export type CreateThumbnail = (this: VideoInstance, videoFile: VideoFileInstance) => Promise - export type GetVideoFilePath = (this: VideoInstance, videoFile: VideoFileInstance) => string - export type CreateTorrentAndSetInfoHash = (this: VideoInstance, videoFile: VideoFileInstance) => Promise - - export type ToActivityPubObject = (this: VideoInstance) => VideoTorrentObject - - export type OptimizeOriginalVideofile = (this: VideoInstance) => Promise - export type TranscodeOriginalVideofile = (this: VideoInstance, resolution: number) => Promise - export type GetOriginalFileHeight = (this: VideoInstance) => Promise - export type GetEmbedPath = (this: VideoInstance) => string - export type GetThumbnailPath = (this: VideoInstance) => string - export type GetPreviewPath = (this: VideoInstance) => string - export type GetDescriptionPath = (this: VideoInstance) => string - export type GetTruncatedDescription = (this: VideoInstance) => string - export type GetCategoryLabel = (this: VideoInstance) => string - export type GetLicenceLabel = (this: VideoInstance) => string - export type GetLanguageLabel = (this: VideoInstance) => string - - export type List = () => Bluebird - - export type ListAllAndSharedByAccountForOutbox = ( - accountId: number, - start: number, - count: number - ) => Bluebird< ResultList > - export type ListForApi = (start: number, count: number, sort: string) => Bluebird< ResultList > - export type ListUserVideosForApi = (userId: number, start: number, count: number, sort: string) => Bluebird< ResultList > - export type SearchAndPopulateAccountAndServerAndTags = ( - value: string, - start: number, - count: number, - sort: string - ) => Bluebird< ResultList > - - export type Load = (id: number) => Bluebird - export type LoadByUUID = (uuid: string, t?: Sequelize.Transaction) => Bluebird - export type LoadByUrlAndPopulateAccount = (url: string, t?: Sequelize.Transaction) => Bluebird - export type LoadAndPopulateAccountAndServerAndTags = (id: number) => Bluebird - export type LoadByUUIDAndPopulateAccountAndServerAndTags = (uuid: string) => Bluebird - export type LoadByUUIDOrURL = (uuid: string, url: string, t?: Sequelize.Transaction) => Bluebird - - export type RemoveThumbnail = (this: VideoInstance) => Promise - export type RemovePreview = (this: VideoInstance) => Promise - export type RemoveFile = (this: VideoInstance, videoFile: VideoFileInstance) => Promise - export type RemoveTorrent = (this: VideoInstance, videoFile: VideoFileInstance) => Promise -} - -export interface VideoClass { - list: VideoMethods.List - listAllAndSharedByAccountForOutbox: VideoMethods.ListAllAndSharedByAccountForOutbox - listForApi: VideoMethods.ListForApi - listUserVideosForApi: VideoMethods.ListUserVideosForApi - load: VideoMethods.Load - loadAndPopulateAccountAndServerAndTags: VideoMethods.LoadAndPopulateAccountAndServerAndTags - loadByUUID: VideoMethods.LoadByUUID - loadByUrlAndPopulateAccount: VideoMethods.LoadByUrlAndPopulateAccount - loadByUUIDOrURL: VideoMethods.LoadByUUIDOrURL - loadByUUIDAndPopulateAccountAndServerAndTags: VideoMethods.LoadByUUIDAndPopulateAccountAndServerAndTags - searchAndPopulateAccountAndServerAndTags: VideoMethods.SearchAndPopulateAccountAndServerAndTags -} - -export interface VideoAttributes { - id?: number - uuid?: string - name: string - category: number - licence: number - language: number - nsfw: boolean - description: string - duration: number - privacy: number - views?: number - likes?: number - dislikes?: number - remote: boolean - url?: string - - createdAt?: Date - updatedAt?: Date - - parentId?: number - channelId?: number - - VideoChannel?: VideoChannelInstance - Tags?: TagInstance[] - VideoFiles?: VideoFileInstance[] - VideoShares?: VideoShareInstance[] - AccountVideoRates?: AccountVideoRateInstance[] -} - -export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.Instance { - createPreview: VideoMethods.CreatePreview - createThumbnail: VideoMethods.CreateThumbnail - createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash - getOriginalFile: VideoMethods.GetOriginalFile - getPreviewName: VideoMethods.GetPreviewName - getPreviewPath: VideoMethods.GetPreviewPath - getThumbnailName: VideoMethods.GetThumbnailName - getThumbnailPath: VideoMethods.GetThumbnailPath - getTorrentFileName: VideoMethods.GetTorrentFileName - getVideoFilename: VideoMethods.GetVideoFilename - getVideoFilePath: VideoMethods.GetVideoFilePath - isOwned: VideoMethods.IsOwned - removeFile: VideoMethods.RemoveFile - removePreview: VideoMethods.RemovePreview - removeThumbnail: VideoMethods.RemoveThumbnail - removeTorrent: VideoMethods.RemoveTorrent - toActivityPubObject: VideoMethods.ToActivityPubObject - toFormattedJSON: VideoMethods.ToFormattedJSON - toFormattedDetailsJSON: VideoMethods.ToFormattedDetailsJSON - optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile - transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile - getOriginalFileHeight: VideoMethods.GetOriginalFileHeight - getEmbedPath: VideoMethods.GetEmbedPath - getDescriptionPath: VideoMethods.GetDescriptionPath - getTruncatedDescription: VideoMethods.GetTruncatedDescription - getCategoryLabel: VideoMethods.GetCategoryLabel - getLicenceLabel: VideoMethods.GetLicenceLabel - getLanguageLabel: VideoMethods.GetLanguageLabel - - setTags: Sequelize.HasManySetAssociationsMixin - addVideoFile: Sequelize.HasManyAddAssociationMixin - setVideoFiles: Sequelize.HasManySetAssociationsMixin -} - -export interface VideoModel extends VideoClass, Sequelize.Model {} diff --git a/server/models/video/video-share-interface.ts b/server/models/video/video-share-interface.ts deleted file mode 100644 index 3946303f1..000000000 --- a/server/models/video/video-share-interface.ts +++ /dev/null @@ -1,30 +0,0 @@ -import * as Bluebird from 'bluebird' -import * as Sequelize from 'sequelize' -import { AccountInstance } from '../account/account-interface' -import { VideoInstance } from './video-interface' - -export namespace VideoShareMethods { - export type LoadAccountsByShare = (videoId: number, t: Sequelize.Transaction) => Bluebird - export type Load = (accountId: number, videoId: number, t: Sequelize.Transaction) => Bluebird -} - -export interface VideoShareClass { - loadAccountsByShare: VideoShareMethods.LoadAccountsByShare - load: VideoShareMethods.Load -} - -export interface VideoShareAttributes { - accountId: number - videoId: number -} - -export interface VideoShareInstance extends VideoShareClass, VideoShareAttributes, Sequelize.Instance { - id: number - createdAt: Date - updatedAt: Date - - Account?: AccountInstance - Video?: VideoInstance -} - -export interface VideoShareModel extends VideoShareClass, Sequelize.Model {} diff --git a/server/models/video/video-share.ts b/server/models/video/video-share.ts index 37e405fa9..01b6d3d34 100644 --- a/server/models/video/video-share.ts +++ b/server/models/video/video-share.ts @@ -1,84 +1,78 @@ import * as Sequelize from 'sequelize' +import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' +import { AccountModel } from '../account/account' +import { VideoModel } from './video' -import { addMethodsToModel } from '../utils' -import { VideoShareAttributes, VideoShareInstance, VideoShareMethods } from './video-share-interface' - -let VideoShare: Sequelize.Model -let loadAccountsByShare: VideoShareMethods.LoadAccountsByShare -let load: VideoShareMethods.Load - -export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { - VideoShare = sequelize.define('VideoShare', - { }, +@Table({ + tableName: 'videoShare', + indexes: [ { - indexes: [ - { - fields: [ 'accountId' ] - }, - { - fields: [ 'videoId' ] - } - ] + fields: [ 'accountId' ] + }, + { + fields: [ 'videoId' ] } - ) - - const classMethods = [ - associate, - loadAccountsByShare, - load ] - addMethodsToModel(VideoShare, classMethods) +}) +export class VideoShareModel extends Model { + @CreatedAt + createdAt: Date - return VideoShare -} + @UpdatedAt + updatedAt: Date -// ------------------------------ METHODS ------------------------------ + @ForeignKey(() => AccountModel) + @Column + accountId: number -function associate (models) { - VideoShare.belongsTo(models.Account, { + @BelongsTo(() => AccountModel, { foreignKey: { - name: 'accountId', allowNull: false }, onDelete: 'cascade' }) + Account: AccountModel - VideoShare.belongsTo(models.Video, { + @ForeignKey(() => VideoModel) + @Column + videoId: number + + @BelongsTo(() => VideoModel, { foreignKey: { - name: 'videoId', - allowNull: true + allowNull: false }, onDelete: 'cascade' }) -} - -load = function (accountId: number, videoId: number, t: Sequelize.Transaction) { - return VideoShare.findOne({ - where: { - accountId, - videoId - }, - include: [ - VideoShare['sequelize'].models.Account - ], - transaction: t - }) -} + Video: VideoModel -loadAccountsByShare = function (videoId: number, t: Sequelize.Transaction) { - const query = { - where: { - videoId - }, - include: [ - { - model: VideoShare['sequelize'].models.Account, - required: true - } - ], - transaction: t + static load (accountId: number, videoId: number, t: Sequelize.Transaction) { + return VideoShareModel.findOne({ + where: { + accountId, + videoId + }, + include: [ + AccountModel + ], + transaction: t + }) } - return VideoShare.findAll(query) - .then(res => res.map(r => r.Account)) + static loadAccountsByShare (videoId: number, t: Sequelize.Transaction) { + const query = { + where: { + videoId + }, + include: [ + { + model: AccountModel, + required: true + } + ], + transaction: t + } + + return VideoShareModel.findAll(query) + .then(res => res.map(r => r.Account)) + } } diff --git a/server/models/video/video-tag-interface.ts b/server/models/video/video-tag-interface.ts deleted file mode 100644 index f928cecff..000000000 --- a/server/models/video/video-tag-interface.ts +++ /dev/null @@ -1,18 +0,0 @@ -import * as Sequelize from 'sequelize' - -export namespace VideoTagMethods { -} - -export interface VideoTagClass { -} - -export interface VideoTagAttributes { -} - -export interface VideoTagInstance extends VideoTagClass, VideoTagAttributes, Sequelize.Instance { - id: number - createdAt: Date - updatedAt: Date -} - -export interface VideoTagModel extends VideoTagClass, Sequelize.Model {} diff --git a/server/models/video/video-tag.ts b/server/models/video/video-tag.ts index ac45374f8..ca15e3426 100644 --- a/server/models/video/video-tag.ts +++ b/server/models/video/video-tag.ts @@ -1,23 +1,30 @@ -import * as Sequelize from 'sequelize' +import { Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' +import { TagModel } from './tag' +import { VideoModel } from './video' -import { - VideoTagInstance, - VideoTagAttributes -} from './video-tag-interface' +@Table({ + tableName: 'videoTag', + indexes: [ + { + fields: [ 'videoId' ] + }, + { + fields: [ 'tagId' ] + } + ] +}) +export class VideoTagModel extends Model { + @CreatedAt + createdAt: Date -let VideoTag: Sequelize.Model + @UpdatedAt + updatedAt: Date -export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { - VideoTag = sequelize.define('VideoTag', {}, { - indexes: [ - { - fields: [ 'videoId' ] - }, - { - fields: [ 'tagId' ] - } - ] - }) + @ForeignKey(() => VideoModel) + @Column + videoId: number - return VideoTag + @ForeignKey(() => TagModel) + @Column + tagId: number } diff --git a/server/models/video/video.ts b/server/models/video/video.ts index d46fdeebe..9e26f9bbe 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -4,21 +4,52 @@ import * as magnetUtil from 'magnet-uri' import * as parseTorrent from 'parse-torrent' import { join } from 'path' import * as Sequelize from 'sequelize' +import { + AfterDestroy, + AllowNull, + BelongsTo, + BelongsToMany, + Column, + CreatedAt, + DataType, + Default, + ForeignKey, + HasMany, + IFindOptions, + Is, + IsInt, + IsUUID, + Min, + Model, + Table, + UpdatedAt +} from 'sequelize-typescript' +import { IIncludeOptions } from 'sequelize-typescript/lib/interfaces/IIncludeOptions' import { VideoPrivacy, VideoResolution } from '../../../shared' -import { VideoTorrentObject } from '../../../shared/models/activitypub/objects/video-torrent-object' -import { activityPubCollection } from '../../helpers/activitypub' -import { createTorrentPromise, renamePromise, statPromise, unlinkPromise, writeFilePromise } from '../../helpers/core-utils' -import { isVideoCategoryValid, isVideoLanguageValid, isVideoPrivacyValid } from '../../helpers/custom-validators/videos' -import { generateImageFromVideoFile, getVideoFileHeight, transcode } from '../../helpers/ffmpeg-utils' +import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' +import { + activityPubCollection, + createTorrentPromise, + generateImageFromVideoFile, + getVideoFileHeight, + logger, + renamePromise, + statPromise, + transcode, + unlinkPromise, + writeFilePromise +} from '../../helpers' +import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub' import { - isActivityPubUrlValid, + isVideoCategoryValid, isVideoDescriptionValid, isVideoDurationValid, + isVideoLanguageValid, isVideoLicenceValid, isVideoNameValid, - isVideoNSFWValid -} from '../../helpers/index' -import { logger } from '../../helpers/logger' + isVideoNSFWValid, + isVideoPrivacyValid +} from '../../helpers/custom-validators/videos' import { API_VERSION, CONFIG, @@ -31,1169 +62,1025 @@ import { VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES -} from '../../initializers/constants' -import { getAnnounceActivityPubUrl } from '../../lib/activitypub/url' +} from '../../initializers' +import { getAnnounceActivityPubUrl } from '../../lib/activitypub' import { sendDeleteVideo } from '../../lib/index' -import { addMethodsToModel, getSort } from '../utils' -import { TagInstance } from './tag-interface' -import { VideoFileInstance, VideoFileModel } from './video-file-interface' -import { VideoAttributes, VideoInstance, VideoMethods } from './video-interface' - -let Video: Sequelize.Model -let getOriginalFile: VideoMethods.GetOriginalFile -let getVideoFilename: VideoMethods.GetVideoFilename -let getThumbnailName: VideoMethods.GetThumbnailName -let getThumbnailPath: VideoMethods.GetThumbnailPath -let getPreviewName: VideoMethods.GetPreviewName -let getPreviewPath: VideoMethods.GetPreviewPath -let getTorrentFileName: VideoMethods.GetTorrentFileName -let isOwned: VideoMethods.IsOwned -let toFormattedJSON: VideoMethods.ToFormattedJSON -let toFormattedDetailsJSON: VideoMethods.ToFormattedDetailsJSON -let toActivityPubObject: VideoMethods.ToActivityPubObject -let optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile -let transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile -let createPreview: VideoMethods.CreatePreview -let createThumbnail: VideoMethods.CreateThumbnail -let getVideoFilePath: VideoMethods.GetVideoFilePath -let createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash -let getOriginalFileHeight: VideoMethods.GetOriginalFileHeight -let getEmbedPath: VideoMethods.GetEmbedPath -let getDescriptionPath: VideoMethods.GetDescriptionPath -let getTruncatedDescription: VideoMethods.GetTruncatedDescription -let getCategoryLabel: VideoMethods.GetCategoryLabel -let getLicenceLabel: VideoMethods.GetLicenceLabel -let getLanguageLabel: VideoMethods.GetLanguageLabel - -let list: VideoMethods.List -let listForApi: VideoMethods.ListForApi -let listAllAndSharedByAccountForOutbox: VideoMethods.ListAllAndSharedByAccountForOutbox -let listUserVideosForApi: VideoMethods.ListUserVideosForApi -let load: VideoMethods.Load -let loadByUrlAndPopulateAccount: VideoMethods.LoadByUrlAndPopulateAccount -let loadByUUID: VideoMethods.LoadByUUID -let loadByUUIDOrURL: VideoMethods.LoadByUUIDOrURL -let loadAndPopulateAccountAndServerAndTags: VideoMethods.LoadAndPopulateAccountAndServerAndTags -let loadByUUIDAndPopulateAccountAndServerAndTags: VideoMethods.LoadByUUIDAndPopulateAccountAndServerAndTags -let searchAndPopulateAccountAndServerAndTags: VideoMethods.SearchAndPopulateAccountAndServerAndTags -let removeThumbnail: VideoMethods.RemoveThumbnail -let removePreview: VideoMethods.RemovePreview -let removeFile: VideoMethods.RemoveFile -let removeTorrent: VideoMethods.RemoveTorrent - -export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { - Video = sequelize.define('Video', +import { AccountModel } from '../account/account' +import { AccountVideoRateModel } from '../account/account-video-rate' +import { ServerModel } from '../server/server' +import { getSort, throwIfNotValid } from '../utils' +import { TagModel } from './tag' +import { VideoAbuseModel } from './video-abuse' +import { VideoChannelModel } from './video-channel' +import { VideoFileModel } from './video-file' +import { VideoShareModel } from './video-share' +import { VideoTagModel } from './video-tag' + +@Table({ + tableName: 'video', + indexes: [ { - uuid: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - allowNull: false, - validate: { - isUUID: 4 - } - }, - name: { - type: DataTypes.STRING, - allowNull: false, - validate: { - nameValid: value => { - const res = isVideoNameValid(value) - if (res === false) throw new Error('Video name is not valid.') - } - } - }, - category: { - type: DataTypes.INTEGER, - allowNull: true, - defaultValue: null, - validate: { - categoryValid: value => { - const res = isVideoCategoryValid(value) - if (res === false) throw new Error('Video category is not valid.') - } - } - }, - licence: { - type: DataTypes.INTEGER, - allowNull: true, - defaultValue: null, - validate: { - licenceValid: value => { - const res = isVideoLicenceValid(value) - if (res === false) throw new Error('Video licence is not valid.') - } - } - }, - language: { - type: DataTypes.INTEGER, - allowNull: true, - defaultValue: null, - validate: { - languageValid: value => { - const res = isVideoLanguageValid(value) - if (res === false) throw new Error('Video language is not valid.') - } - } - }, - privacy: { - type: DataTypes.INTEGER, - allowNull: false, - validate: { - privacyValid: value => { - const res = isVideoPrivacyValid(value) - if (res === false) throw new Error('Video privacy is not valid.') - } - } - }, - nsfw: { - type: DataTypes.BOOLEAN, - allowNull: false, - validate: { - nsfwValid: value => { - const res = isVideoNSFWValid(value) - if (res === false) throw new Error('Video nsfw attribute is not valid.') - } - } - }, - description: { - type: DataTypes.STRING(CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max), - allowNull: true, - defaultValue: null, - validate: { - descriptionValid: value => { - const res = isVideoDescriptionValid(value) - if (res === false) throw new Error('Video description is not valid.') - } - } - }, - duration: { - type: DataTypes.INTEGER, - allowNull: false, - validate: { - durationValid: value => { - const res = isVideoDurationValid(value) - if (res === false) throw new Error('Video duration is not valid.') - } - } - }, - views: { - type: DataTypes.INTEGER, - allowNull: false, - defaultValue: 0, - validate: { - min: 0, - isInt: true - } - }, - likes: { - type: DataTypes.INTEGER, - allowNull: false, - defaultValue: 0, - validate: { - min: 0, - isInt: true - } - }, - dislikes: { - type: DataTypes.INTEGER, - allowNull: false, - defaultValue: 0, - validate: { - min: 0, - isInt: true - } - }, - remote: { - type: DataTypes.BOOLEAN, - allowNull: false, - defaultValue: false - }, - url: { - type: DataTypes.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max), - allowNull: false, - validate: { - urlValid: value => { - const res = isActivityPubUrlValid(value) - if (res === false) throw new Error('Video URL is not valid.') - } - } - } + fields: [ 'name' ] }, { - indexes: [ - { - fields: [ 'name' ] - }, - { - fields: [ 'createdAt' ] - }, - { - fields: [ 'duration' ] - }, - { - fields: [ 'views' ] - }, - { - fields: [ 'likes' ] - }, - { - fields: [ 'uuid' ] - }, - { - fields: [ 'channelId' ] - } - ], - hooks: { - afterDestroy - } + fields: [ 'createdAt' ] + }, + { + fields: [ 'duration' ] + }, + { + fields: [ 'views' ] + }, + { + fields: [ 'likes' ] + }, + { + fields: [ 'uuid' ] + }, + { + fields: [ 'channelId' ] } - ) - - const classMethods = [ - associate, - - list, - listAllAndSharedByAccountForOutbox, - listForApi, - listUserVideosForApi, - load, - loadByUrlAndPopulateAccount, - loadAndPopulateAccountAndServerAndTags, - loadByUUIDOrURL, - loadByUUID, - loadByUUIDAndPopulateAccountAndServerAndTags, - searchAndPopulateAccountAndServerAndTags - ] - const instanceMethods = [ - createPreview, - createThumbnail, - createTorrentAndSetInfoHash, - getPreviewName, - getPreviewPath, - getThumbnailName, - getThumbnailPath, - getTorrentFileName, - getVideoFilename, - getVideoFilePath, - getOriginalFile, - isOwned, - removeFile, - removePreview, - removeThumbnail, - removeTorrent, - toActivityPubObject, - toFormattedJSON, - toFormattedDetailsJSON, - optimizeOriginalVideofile, - transcodeOriginalVideofile, - getOriginalFileHeight, - getEmbedPath, - getTruncatedDescription, - getDescriptionPath, - getCategoryLabel, - getLicenceLabel, - getLanguageLabel ] - addMethodsToModel(Video, classMethods, instanceMethods) - - return Video -} - -// ------------------------------ METHODS ------------------------------ - -function associate (models) { - Video.belongsTo(models.VideoChannel, { +}) +export class VideoModel extends Model { + + @AllowNull(false) + @Default(DataType.UUIDV4) + @IsUUID(4) + @Column(DataType.UUID) + uuid: string + + @AllowNull(false) + @Is('VideoName', value => throwIfNotValid(value, isVideoNameValid, 'name')) + @Column + name: string + + @AllowNull(true) + @Default(null) + @Is('VideoCategory', value => throwIfNotValid(value, isVideoCategoryValid, 'category')) + @Column + category: number + + @AllowNull(true) + @Default(null) + @Is('VideoLicence', value => throwIfNotValid(value, isVideoLicenceValid, 'licence')) + @Column + licence: number + + @AllowNull(true) + @Default(null) + @Is('VideoLanguage', value => throwIfNotValid(value, isVideoLanguageValid, 'language')) + @Column + language: number + + @AllowNull(false) + @Is('VideoPrivacy', value => throwIfNotValid(value, isVideoPrivacyValid, 'privacy')) + @Column + privacy: number + + @AllowNull(false) + @Is('VideoNSFW', value => throwIfNotValid(value, isVideoNSFWValid, 'NSFW boolean')) + @Column + nsfw: boolean + + @AllowNull(true) + @Default(null) + @Is('VideoDescription', value => throwIfNotValid(value, isVideoDescriptionValid, 'description')) + @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max)) + description: string + + @AllowNull(false) + @Is('VideoDuration', value => throwIfNotValid(value, isVideoDurationValid, 'duration')) + @Column + duration: number + + @AllowNull(false) + @Default(0) + @IsInt + @Min(0) + @Column + views: number + + @AllowNull(false) + @Default(0) + @IsInt + @Min(0) + @Column + likes: number + + @AllowNull(false) + @Default(0) + @IsInt + @Min(0) + @Column + dislikes: number + + @AllowNull(false) + @Column + remote: boolean + + @AllowNull(false) + @Is('VideoUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url')) + @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max)) + url: string + + @CreatedAt + createdAt: Date + + @UpdatedAt + updatedAt: Date + + @ForeignKey(() => VideoChannelModel) + @Column + channelId: number + + @BelongsTo(() => VideoChannelModel, { foreignKey: { - name: 'channelId', allowNull: false }, onDelete: 'cascade' }) + VideoChannel: VideoChannelModel - Video.belongsToMany(models.Tag, { + @BelongsToMany(() => TagModel, { foreignKey: 'videoId', - through: models.VideoTag, - onDelete: 'cascade' + through: () => VideoTagModel, + onDelete: 'CASCADE' }) + Tags: TagModel[] - Video.hasMany(models.VideoAbuse, { + @HasMany(() => VideoAbuseModel, { foreignKey: { name: 'videoId', allowNull: false }, onDelete: 'cascade' }) + VideoAbuses: VideoAbuseModel[] - Video.hasMany(models.VideoFile, { + @HasMany(() => VideoFileModel, { foreignKey: { name: 'videoId', allowNull: false }, onDelete: 'cascade' }) + VideoFiles: VideoFileModel[] - Video.hasMany(models.VideoShare, { + @HasMany(() => VideoShareModel, { foreignKey: { name: 'videoId', allowNull: false }, onDelete: 'cascade' }) + VideoShares: VideoShareModel[] - Video.hasMany(models.AccountVideoRate, { + @HasMany(() => AccountVideoRateModel, { foreignKey: { name: 'videoId', allowNull: false }, onDelete: 'cascade' }) -} - -function afterDestroy (video: VideoInstance) { - const tasks = [] + AccountVideoRates: AccountVideoRateModel[] - tasks.push( - video.removeThumbnail() - ) + @AfterDestroy + static removeFilesAndSendDelete (instance: VideoModel) { + const tasks = [] - if (video.isOwned()) { tasks.push( - video.removePreview(), - sendDeleteVideo(video, undefined) + instance.removeThumbnail() ) - // Remove physical files and torrents - video.VideoFiles.forEach(file => { - tasks.push(video.removeFile(file)) - tasks.push(video.removeTorrent(file)) - }) - } - - return Promise.all(tasks) - .catch(err => { - logger.error('Some errors when removing files of video %s in after destroy hook.', video.uuid, err) - }) -} - -getOriginalFile = function (this: VideoInstance) { - if (Array.isArray(this.VideoFiles) === false) return undefined + if (instance.isOwned()) { + tasks.push( + instance.removePreview(), + sendDeleteVideo(instance, undefined) + ) - // The original file is the file that have the higher resolution - return maxBy(this.VideoFiles, file => file.resolution) -} + // Remove physical files and torrents + instance.VideoFiles.forEach(file => { + tasks.push(instance.removeFile(file)) + tasks.push(instance.removeTorrent(file)) + }) + } -getVideoFilename = function (this: VideoInstance, videoFile: VideoFileInstance) { - return this.uuid + '-' + videoFile.resolution + videoFile.extname -} + return Promise.all(tasks) + .catch(err => { + logger.error('Some errors when removing files of video %s in after destroy hook.', instance.uuid, err) + }) + } -getThumbnailName = function (this: VideoInstance) { - // We always have a copy of the thumbnail - const extension = '.jpg' - return this.uuid + extension -} + static list () { + const query = { + include: [ VideoFileModel ] + } -getPreviewName = function (this: VideoInstance) { - const extension = '.jpg' - return this.uuid + extension -} + return VideoModel.findAll(query) + } -getTorrentFileName = function (this: VideoInstance, videoFile: VideoFileInstance) { - const extension = '.torrent' - return this.uuid + '-' + videoFile.resolution + extension -} + static listAllAndSharedByAccountForOutbox (accountId: number, start: number, count: number) { + function getRawQuery (select: string) { + const queryVideo = 'SELECT ' + select + ' FROM "video" AS "Video" ' + + 'INNER JOIN "videoChannel" AS "VideoChannel" ON "VideoChannel"."id" = "Video"."channelId" ' + + 'WHERE "VideoChannel"."accountId" = ' + accountId + const queryVideoShare = 'SELECT ' + select + ' FROM "videoShare" AS "VideoShare" ' + + 'INNER JOIN "video" AS "Video" ON "Video"."id" = "VideoShare"."videoId" ' + + 'WHERE "VideoShare"."accountId" = ' + accountId -isOwned = function (this: VideoInstance) { - return this.remote === false -} + return `(${queryVideo}) UNION (${queryVideoShare})` + } -createPreview = function (this: VideoInstance, videoFile: VideoFileInstance) { - const imageSize = PREVIEWS_SIZE.width + 'x' + PREVIEWS_SIZE.height + const rawQuery = getRawQuery('"Video"."id"') + const rawCountQuery = getRawQuery('COUNT("Video"."id") as "total"') + + const query = { + distinct: true, + offset: start, + limit: count, + order: [ getSort('createdAt'), [ 'Tags', 'name', 'ASC' ] ], + where: { + id: { + [Sequelize.Op.in]: Sequelize.literal('(' + rawQuery + ')') + } + }, + include: [ + { + model: VideoShareModel, + required: false, + where: { + [Sequelize.Op.and]: [ + { + id: { + [Sequelize.Op.not]: null + } + }, + { + accountId + } + ] + }, + include: [ AccountModel ] + }, + { + model: VideoChannelModel, + required: true, + include: [ + { + model: AccountModel, + required: true + } + ] + }, + { + model: AccountVideoRateModel, + include: [ AccountModel ] + }, + VideoFileModel, + TagModel + ] + } - return generateImageFromVideoFile( - this.getVideoFilePath(videoFile), - CONFIG.STORAGE.PREVIEWS_DIR, - this.getPreviewName(), - imageSize - ) -} + return Bluebird.all([ + // FIXME: typing issue + VideoModel.findAll(query as any), + VideoModel.sequelize.query(rawCountQuery, { type: Sequelize.QueryTypes.SELECT }) + ]).then(([ rows, totals ]) => { + // totals: totalVideos + totalVideoShares + let totalVideos = 0 + let totalVideoShares = 0 + if (totals[0]) totalVideos = parseInt(totals[0].total, 10) + if (totals[1]) totalVideoShares = parseInt(totals[1].total, 10) + + const total = totalVideos + totalVideoShares + return { + data: rows, + total: total + } + }) + } -createThumbnail = function (this: VideoInstance, videoFile: VideoFileInstance) { - const imageSize = THUMBNAILS_SIZE.width + 'x' + THUMBNAILS_SIZE.height + static listUserVideosForApi (userId: number, start: number, count: number, sort: string) { + const query = { + distinct: true, + offset: start, + limit: count, + order: [ getSort(sort), [ 'Tags', 'name', 'ASC' ] ], + include: [ + { + model: VideoChannelModel, + required: true, + include: [ + { + model: AccountModel, + where: { + userId + }, + required: true + } + ] + }, + TagModel + ] + } - return generateImageFromVideoFile( - this.getVideoFilePath(videoFile), - CONFIG.STORAGE.THUMBNAILS_DIR, - this.getThumbnailName(), - imageSize - ) -} + return VideoModel.findAndCountAll(query).then(({ rows, count }) => { + return { + data: rows, + total: count + } + }) + } -getVideoFilePath = function (this: VideoInstance, videoFile: VideoFileInstance) { - return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile)) -} + static listForApi (start: number, count: number, sort: string) { + const query = { + distinct: true, + offset: start, + limit: count, + order: [ getSort(sort), [ 'Tags', 'name', 'ASC' ] ], + include: [ + { + model: VideoChannelModel, + required: true, + include: [ + { + model: AccountModel, + required: true, + include: [ + { + model: ServerModel, + required: false + } + ] + } + ] + }, + TagModel + ], + where: this.createBaseVideosWhere() + } -createTorrentAndSetInfoHash = async function (this: VideoInstance, videoFile: VideoFileInstance) { - const options = { - announceList: [ - [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ] - ], - urlList: [ - CONFIG.WEBSERVER.URL + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) - ] + return VideoModel.findAndCountAll(query).then(({ rows, count }) => { + return { + data: rows, + total: count + } + }) } - const torrent = await createTorrentPromise(this.getVideoFilePath(videoFile), options) + static load (id: number) { + return VideoModel.findById(id) + } - const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile)) - logger.info('Creating torrent %s.', filePath) + static loadByUUID (uuid: string, t?: Sequelize.Transaction) { + const query: IFindOptions = { + where: { + uuid + }, + include: [ VideoFileModel ] + } - await writeFilePromise(filePath, torrent) + if (t !== undefined) query.transaction = t - const parsedTorrent = parseTorrent(torrent) - videoFile.infoHash = parsedTorrent.infoHash -} + return VideoModel.findOne(query) + } -getEmbedPath = function (this: VideoInstance) { - return '/videos/embed/' + this.uuid -} + static loadByUrlAndPopulateAccount (url: string, t?: Sequelize.Transaction) { + const query: IFindOptions = { + where: { + url + }, + include: [ + VideoFileModel, + { + model: VideoChannelModel, + include: [ AccountModel ] + } + ] + } -getThumbnailPath = function (this: VideoInstance) { - return join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName()) -} + if (t !== undefined) query.transaction = t -getPreviewPath = function (this: VideoInstance) { - return join(STATIC_PATHS.PREVIEWS, this.getPreviewName()) -} + return VideoModel.findOne(query) + } -toFormattedJSON = function (this: VideoInstance) { - let serverHost + static loadByUUIDOrURL (uuid: string, url: string, t?: Sequelize.Transaction) { + const query: IFindOptions = { + where: { + [Sequelize.Op.or]: [ + { uuid }, + { url } + ] + }, + include: [ VideoFileModel ] + } - if (this.VideoChannel.Account.Server) { - serverHost = this.VideoChannel.Account.Server.host - } else { - // It means it's our video - serverHost = CONFIG.WEBSERVER.HOST - } + if (t !== undefined) query.transaction = t - const json = { - id: this.id, - uuid: this.uuid, - name: this.name, - category: this.category, - categoryLabel: this.getCategoryLabel(), - licence: this.licence, - licenceLabel: this.getLicenceLabel(), - language: this.language, - languageLabel: this.getLanguageLabel(), - nsfw: this.nsfw, - description: this.getTruncatedDescription(), - serverHost, - isLocal: this.isOwned(), - accountName: this.VideoChannel.Account.name, - duration: this.duration, - views: this.views, - likes: this.likes, - dislikes: this.dislikes, - tags: map(this.Tags, 'name'), - thumbnailPath: this.getThumbnailPath(), - previewPath: this.getPreviewPath(), - embedPath: this.getEmbedPath(), - createdAt: this.createdAt, - updatedAt: this.updatedAt + return VideoModel.findOne(query) } - return json -} + static loadAndPopulateAccountAndServerAndTags (id: number) { + const options = { + order: [ [ 'Tags', 'name', 'ASC' ] ], + include: [ + { + model: VideoChannelModel, + include: [ + { + model: AccountModel, + include: [ { model: ServerModel, required: false } ] + } + ] + }, + { + model: AccountVideoRateModel, + include: [ AccountModel ] + }, + { + model: VideoShareModel, + include: [ AccountModel ] + }, + TagModel, + VideoFileModel + ] + } -toFormattedDetailsJSON = function (this: VideoInstance) { - const formattedJson = this.toFormattedJSON() + return VideoModel.findById(id, options) + } - // Maybe our server is not up to date and there are new privacy settings since our version - let privacyLabel = VIDEO_PRIVACIES[this.privacy] - if (!privacyLabel) privacyLabel = 'Unknown' + static loadByUUIDAndPopulateAccountAndServerAndTags (uuid: string) { + const options = { + order: [ [ 'Tags', 'name', 'ASC' ] ], + where: { + uuid + }, + include: [ + { + model: VideoChannelModel, + include: [ + { + model: AccountModel, + include: [ { model: ServerModel, required: false } ] + } + ] + }, + { + model: AccountVideoRateModel, + include: [ AccountModel ] + }, + { + model: VideoShareModel, + include: [ AccountModel ] + }, + TagModel, + VideoFileModel + ] + } - const detailsJson = { - privacyLabel, - privacy: this.privacy, - descriptionPath: this.getDescriptionPath(), - channel: this.VideoChannel.toFormattedJSON(), - account: this.VideoChannel.Account.toFormattedJSON(), - files: [] + return VideoModel.findOne(options) } - // Format and sort video files - const { baseUrlHttp, baseUrlWs } = getBaseUrls(this) - detailsJson.files = this.VideoFiles - .map(videoFile => { - let resolutionLabel = videoFile.resolution + 'p' - - const videoFileJson = { - resolution: videoFile.resolution, - resolutionLabel, - magnetUri: generateMagnetUri(this, videoFile, baseUrlHttp, baseUrlWs), - size: videoFile.size, - torrentUrl: getTorrentUrl(this, videoFile, baseUrlHttp), - fileUrl: getVideoFileUrl(this, videoFile, baseUrlHttp) - } - - return videoFileJson - }) - .sort((a, b) => { - if (a.resolution < b.resolution) return 1 - if (a.resolution === b.resolution) return 0 - return -1 - }) - - return Object.assign(formattedJson, detailsJson) -} - -toActivityPubObject = function (this: VideoInstance) { - const { baseUrlHttp, baseUrlWs } = getBaseUrls(this) - if (!this.Tags) this.Tags = [] + static searchAndPopulateAccountAndServerAndTags (value: string, start: number, count: number, sort: string) { + const serverInclude: IIncludeOptions = { + model: ServerModel, + required: false + } - const tag = this.Tags.map(t => ({ - type: 'Hashtag' as 'Hashtag', - name: t.name - })) + const accountInclude: IIncludeOptions = { + model: AccountModel, + include: [ serverInclude ] + } - let language - if (this.language) { - language = { - identifier: this.language + '', - name: this.getLanguageLabel() + const videoChannelInclude: IIncludeOptions = { + model: VideoChannelModel, + include: [ accountInclude ], + required: true } - } - let category - if (this.category) { - category = { - identifier: this.category + '', - name: this.getCategoryLabel() + const tagInclude: IIncludeOptions = { + model: TagModel } - } - let licence - if (this.licence) { - licence = { - identifier: this.licence + '', - name: this.getLicenceLabel() + const query: IFindOptions = { + distinct: true, + where: this.createBaseVideosWhere(), + offset: start, + limit: count, + order: [ getSort(sort), [ 'Tags', 'name', 'ASC' ] ] } - } - let likesObject - let dislikesObject + // TODO: search on tags too + // const escapedValue = Video['sequelize'].escape('%' + value + '%') + // query.where['id'][Sequelize.Op.in] = Video['sequelize'].literal( + // `(SELECT "VideoTags"."videoId" + // FROM "Tags" + // INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId" + // WHERE name ILIKE ${escapedValue} + // )` + // ) + + // TODO: search on account too + // accountInclude.where = { + // name: { + // [Sequelize.Op.iLike]: '%' + value + '%' + // } + // } + query.where['name'] = { + [Sequelize.Op.iLike]: '%' + value + '%' + } - if (Array.isArray(this.AccountVideoRates)) { - const likes: string[] = [] - const dislikes: string[] = [] + query.include = [ + videoChannelInclude, tagInclude + ] - for (const rate of this.AccountVideoRates) { - if (rate.type === 'like') { - likes.push(rate.Account.url) - } else if (rate.type === 'dislike') { - dislikes.push(rate.Account.url) + return VideoModel.findAndCountAll(query).then(({ rows, count }) => { + return { + data: rows, + total: count } - } - - likesObject = activityPubCollection(likes) - dislikesObject = activityPubCollection(dislikes) + }) } - let sharesObject - if (Array.isArray(this.VideoShares)) { - const shares: string[] = [] - - for (const videoShare of this.VideoShares) { - const shareUrl = getAnnounceActivityPubUrl(this.url, videoShare.Account) - shares.push(shareUrl) + private static createBaseVideosWhere () { + return { + id: { + [Sequelize.Op.notIn]: VideoModel.sequelize.literal( + '(SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")' + ) + }, + privacy: VideoPrivacy.PUBLIC } - - sharesObject = activityPubCollection(shares) } - const url = [] - for (const file of this.VideoFiles) { - url.push({ - type: 'Link', - mimeType: 'video/' + file.extname.replace('.', ''), - url: getVideoFileUrl(this, file, baseUrlHttp), - width: file.resolution, - size: file.size - }) + getOriginalFile () { + if (Array.isArray(this.VideoFiles) === false) return undefined - url.push({ - type: 'Link', - mimeType: 'application/x-bittorrent', - url: getTorrentUrl(this, file, baseUrlHttp), - width: file.resolution - }) - - url.push({ - type: 'Link', - mimeType: 'application/x-bittorrent;x-scheme-handler/magnet', - url: generateMagnetUri(this, file, baseUrlHttp, baseUrlWs), - width: file.resolution - }) + // The original file is the file that have the higher resolution + return maxBy(this.VideoFiles, file => file.resolution) } - // Add video url too - url.push({ - type: 'Link', - mimeType: 'text/html', - url: CONFIG.WEBSERVER.URL + '/videos/watch/' + this.uuid - }) + getVideoFilename (videoFile: VideoFileModel) { + return this.uuid + '-' + videoFile.resolution + videoFile.extname + } - const videoObject: VideoTorrentObject = { - type: 'Video' as 'Video', - id: this.url, - name: this.name, - // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration - duration: 'PT' + this.duration + 'S', - uuid: this.uuid, - tag, - category, - licence, - language, - views: this.views, - nsfw: this.nsfw, - published: this.createdAt.toISOString(), - updated: this.updatedAt.toISOString(), - mediaType: 'text/markdown', - content: this.getTruncatedDescription(), - icon: { - type: 'Image', - url: getThumbnailUrl(this, baseUrlHttp), - mediaType: 'image/jpeg', - width: THUMBNAILS_SIZE.width, - height: THUMBNAILS_SIZE.height - }, - url, - likes: likesObject, - dislikes: dislikesObject, - shares: sharesObject + getThumbnailName () { + // We always have a copy of the thumbnail + const extension = '.jpg' + return this.uuid + extension } - return videoObject -} + getPreviewName () { + const extension = '.jpg' + return this.uuid + extension + } -getTruncatedDescription = function (this: VideoInstance) { - if (!this.description) return null + getTorrentFileName (videoFile: VideoFileModel) { + const extension = '.torrent' + return this.uuid + '-' + videoFile.resolution + extension + } - const options = { - length: CONSTRAINTS_FIELDS.VIDEOS.TRUNCATED_DESCRIPTION.max + isOwned () { + return this.remote === false } - return truncate(this.description, options) -} + createPreview (videoFile: VideoFileModel) { + const imageSize = PREVIEWS_SIZE.width + 'x' + PREVIEWS_SIZE.height + + return generateImageFromVideoFile( + this.getVideoFilePath(videoFile), + CONFIG.STORAGE.PREVIEWS_DIR, + this.getPreviewName(), + imageSize + ) + } -optimizeOriginalVideofile = async function (this: VideoInstance) { - const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR - const newExtname = '.mp4' - const inputVideoFile = this.getOriginalFile() - const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile)) - const videoOutputPath = join(videosDirectory, this.id + '-transcoded' + newExtname) + createThumbnail (videoFile: VideoFileModel) { + const imageSize = THUMBNAILS_SIZE.width + 'x' + THUMBNAILS_SIZE.height - const transcodeOptions = { - inputPath: videoInputPath, - outputPath: videoOutputPath + return generateImageFromVideoFile( + this.getVideoFilePath(videoFile), + CONFIG.STORAGE.THUMBNAILS_DIR, + this.getThumbnailName(), + imageSize + ) } - try { - // Could be very long! - await transcode(transcodeOptions) + getVideoFilePath (videoFile: VideoFileModel) { + return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile)) + } - await unlinkPromise(videoInputPath) + createTorrentAndSetInfoHash = async function (videoFile: VideoFileModel) { + const options = { + announceList: [ + [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ] + ], + urlList: [ + CONFIG.WEBSERVER.URL + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) + ] + } - // Important to do this before getVideoFilename() to take in account the new file extension - inputVideoFile.set('extname', newExtname) + const torrent = await createTorrentPromise(this.getVideoFilePath(videoFile), options) - await renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile)) - const stats = await statPromise(this.getVideoFilePath(inputVideoFile)) + const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile)) + logger.info('Creating torrent %s.', filePath) - inputVideoFile.set('size', stats.size) + await writeFilePromise(filePath, torrent) - await this.createTorrentAndSetInfoHash(inputVideoFile) - await inputVideoFile.save() + const parsedTorrent = parseTorrent(torrent) + videoFile.infoHash = parsedTorrent.infoHash + } - } catch (err) { - // Auto destruction... - this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err)) + getEmbedPath () { + return '/videos/embed/' + this.uuid + } - throw err + getThumbnailPath () { + return join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName()) } -} -transcodeOriginalVideofile = async function (this: VideoInstance, resolution: VideoResolution) { - const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR - const extname = '.mp4' + getPreviewPath () { + return join(STATIC_PATHS.PREVIEWS, this.getPreviewName()) + } - // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed - const videoInputPath = join(videosDirectory, this.getVideoFilename(this.getOriginalFile())) + toFormattedJSON () { + let serverHost - const newVideoFile = (Video['sequelize'].models.VideoFile as VideoFileModel).build({ - resolution, - extname, - size: 0, - videoId: this.id - }) - const videoOutputPath = join(videosDirectory, this.getVideoFilename(newVideoFile)) + if (this.VideoChannel.Account.Server) { + serverHost = this.VideoChannel.Account.Server.host + } else { + // It means it's our video + serverHost = CONFIG.WEBSERVER.HOST + } - const transcodeOptions = { - inputPath: videoInputPath, - outputPath: videoOutputPath, - resolution + return { + id: this.id, + uuid: this.uuid, + name: this.name, + category: this.category, + categoryLabel: this.getCategoryLabel(), + licence: this.licence, + licenceLabel: this.getLicenceLabel(), + language: this.language, + languageLabel: this.getLanguageLabel(), + nsfw: this.nsfw, + description: this.getTruncatedDescription(), + serverHost, + isLocal: this.isOwned(), + accountName: this.VideoChannel.Account.name, + duration: this.duration, + views: this.views, + likes: this.likes, + dislikes: this.dislikes, + tags: map(this.Tags, 'name'), + thumbnailPath: this.getThumbnailPath(), + previewPath: this.getPreviewPath(), + embedPath: this.getEmbedPath(), + createdAt: this.createdAt, + updatedAt: this.updatedAt + } } - await transcode(transcodeOptions) + toFormattedDetailsJSON () { + const formattedJson = this.toFormattedJSON() - const stats = await statPromise(videoOutputPath) + // Maybe our server is not up to date and there are new privacy settings since our version + let privacyLabel = VIDEO_PRIVACIES[this.privacy] + if (!privacyLabel) privacyLabel = 'Unknown' - newVideoFile.set('size', stats.size) + const detailsJson = { + privacyLabel, + privacy: this.privacy, + descriptionPath: this.getDescriptionPath(), + channel: this.VideoChannel.toFormattedJSON(), + account: this.VideoChannel.Account.toFormattedJSON(), + files: [] + } - await this.createTorrentAndSetInfoHash(newVideoFile) + // Format and sort video files + const { baseUrlHttp, baseUrlWs } = this.getBaseUrls() + detailsJson.files = this.VideoFiles + .map(videoFile => { + let resolutionLabel = videoFile.resolution + 'p' + + return { + resolution: videoFile.resolution, + resolutionLabel, + magnetUri: this.generateMagnetUri(videoFile, baseUrlHttp, baseUrlWs), + size: videoFile.size, + torrentUrl: this.getTorrentUrl(videoFile, baseUrlHttp), + fileUrl: this.getVideoFileUrl(videoFile, baseUrlHttp) + } + }) + .sort((a, b) => { + if (a.resolution < b.resolution) return 1 + if (a.resolution === b.resolution) return 0 + return -1 + }) + + return Object.assign(formattedJson, detailsJson) + } - await newVideoFile.save() + toActivityPubObject (): VideoTorrentObject { + const { baseUrlHttp, baseUrlWs } = this.getBaseUrls() + if (!this.Tags) this.Tags = [] - this.VideoFiles.push(newVideoFile) -} + const tag = this.Tags.map(t => ({ + type: 'Hashtag' as 'Hashtag', + name: t.name + })) -getOriginalFileHeight = function (this: VideoInstance) { - const originalFilePath = this.getVideoFilePath(this.getOriginalFile()) + let language + if (this.language) { + language = { + identifier: this.language + '', + name: this.getLanguageLabel() + } + } - return getVideoFileHeight(originalFilePath) -} + let category + if (this.category) { + category = { + identifier: this.category + '', + name: this.getCategoryLabel() + } + } -getDescriptionPath = function (this: VideoInstance) { - return `/api/${API_VERSION}/videos/${this.uuid}/description` -} + let licence + if (this.licence) { + licence = { + identifier: this.licence + '', + name: this.getLicenceLabel() + } + } -getCategoryLabel = function (this: VideoInstance) { - let categoryLabel = VIDEO_CATEGORIES[this.category] - if (!categoryLabel) categoryLabel = 'Misc' + let likesObject + let dislikesObject - return categoryLabel -} + if (Array.isArray(this.AccountVideoRates)) { + const likes: string[] = [] + const dislikes: string[] = [] -getLicenceLabel = function (this: VideoInstance) { - let licenceLabel = VIDEO_LICENCES[this.licence] - if (!licenceLabel) licenceLabel = 'Unknown' + for (const rate of this.AccountVideoRates) { + if (rate.type === 'like') { + likes.push(rate.Account.url) + } else if (rate.type === 'dislike') { + dislikes.push(rate.Account.url) + } + } - return licenceLabel -} + likesObject = activityPubCollection(likes) + dislikesObject = activityPubCollection(dislikes) + } -getLanguageLabel = function (this: VideoInstance) { - let languageLabel = VIDEO_LANGUAGES[this.language] - if (!languageLabel) languageLabel = 'Unknown' + let sharesObject + if (Array.isArray(this.VideoShares)) { + const shares: string[] = [] - return languageLabel -} + for (const videoShare of this.VideoShares) { + const shareUrl = getAnnounceActivityPubUrl(this.url, videoShare.Account) + shares.push(shareUrl) + } -removeThumbnail = function (this: VideoInstance) { - const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName()) - return unlinkPromise(thumbnailPath) -} + sharesObject = activityPubCollection(shares) + } -removePreview = function (this: VideoInstance) { - // Same name than video thumbnail - return unlinkPromise(CONFIG.STORAGE.PREVIEWS_DIR + this.getPreviewName()) -} + const url = [] + for (const file of this.VideoFiles) { + url.push({ + type: 'Link', + mimeType: 'video/' + file.extname.replace('.', ''), + url: this.getVideoFileUrl(file, baseUrlHttp), + width: file.resolution, + size: file.size + }) + + url.push({ + type: 'Link', + mimeType: 'application/x-bittorrent', + url: this.getTorrentUrl(file, baseUrlHttp), + width: file.resolution + }) + + url.push({ + type: 'Link', + mimeType: 'application/x-bittorrent;x-scheme-handler/magnet', + url: this.generateMagnetUri(file, baseUrlHttp, baseUrlWs), + width: file.resolution + }) + } -removeFile = function (this: VideoInstance, videoFile: VideoFileInstance) { - const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile)) - return unlinkPromise(filePath) -} + // Add video url too + url.push({ + type: 'Link', + mimeType: 'text/html', + url: CONFIG.WEBSERVER.URL + '/videos/watch/' + this.uuid + }) -removeTorrent = function (this: VideoInstance, videoFile: VideoFileInstance) { - const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile)) - return unlinkPromise(torrentPath) -} + return { + type: 'Video' as 'Video', + id: this.url, + name: this.name, + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration + duration: 'PT' + this.duration + 'S', + uuid: this.uuid, + tag, + category, + licence, + language, + views: this.views, + nsfw: this.nsfw, + published: this.createdAt.toISOString(), + updated: this.updatedAt.toISOString(), + mediaType: 'text/markdown', + content: this.getTruncatedDescription(), + icon: { + type: 'Image', + url: this.getThumbnailUrl(baseUrlHttp), + mediaType: 'image/jpeg', + width: THUMBNAILS_SIZE.width, + height: THUMBNAILS_SIZE.height + }, + url, + likes: likesObject, + dislikes: dislikesObject, + shares: sharesObject + } + } + + getTruncatedDescription () { + if (!this.description) return null -// ------------------------------ STATICS ------------------------------ + const options = { + length: CONSTRAINTS_FIELDS.VIDEOS.TRUNCATED_DESCRIPTION.max + } -list = function () { - const query = { - include: [ Video['sequelize'].models.VideoFile ] + return truncate(this.description, options) } - return Video.findAll(query) -} + optimizeOriginalVideofile = async function () { + const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR + const newExtname = '.mp4' + const inputVideoFile = this.getOriginalFile() + const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile)) + const videoOutputPath = join(videosDirectory, this.id + '-transcoded' + newExtname) -listAllAndSharedByAccountForOutbox = function (accountId: number, start: number, count: number) { - function getRawQuery (select: string) { - const queryVideo = 'SELECT ' + select + ' FROM "Videos" AS "Video" ' + - 'INNER JOIN "VideoChannels" AS "VideoChannel" ON "VideoChannel"."id" = "Video"."channelId" ' + - 'WHERE "VideoChannel"."accountId" = ' + accountId - const queryVideoShare = 'SELECT ' + select + ' FROM "VideoShares" AS "VideoShare" ' + - 'INNER JOIN "Videos" AS "Video" ON "Video"."id" = "VideoShare"."videoId" ' + - 'WHERE "VideoShare"."accountId" = ' + accountId + const transcodeOptions = { + inputPath: videoInputPath, + outputPath: videoOutputPath + } - let rawQuery = `(${queryVideo}) UNION (${queryVideoShare})` + try { + // Could be very long! + await transcode(transcodeOptions) - return rawQuery - } + await unlinkPromise(videoInputPath) - const rawQuery = getRawQuery('"Video"."id"') - const rawCountQuery = getRawQuery('COUNT("Video"."id") as "total"') + // Important to do this before getVideoFilename() to take in account the new file extension + inputVideoFile.set('extname', newExtname) - const query = { - distinct: true, - offset: start, - limit: count, - order: [ getSort('createdAt'), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ], - where: { - id: { - [Sequelize.Op.in]: Sequelize.literal('(' + rawQuery + ')') - } - }, - include: [ - { - model: Video['sequelize'].models.VideoShare, - required: false, - where: { - [Sequelize.Op.and]: [ - { - id: { - [Sequelize.Op.not]: null - } - }, - { - accountId - } - ] - }, - include: [ Video['sequelize'].models.Account ] - }, - { - model: Video['sequelize'].models.VideoChannel, - required: true, - include: [ - { - model: Video['sequelize'].models.Account, - required: true - } - ] - }, - { - model: Video['sequelize'].models.AccountVideoRate, - include: [ Video['sequelize'].models.Account ] - }, - Video['sequelize'].models.VideoFile, - Video['sequelize'].models.Tag - ] - } + await renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile)) + const stats = await statPromise(this.getVideoFilePath(inputVideoFile)) - return Bluebird.all([ - Video.findAll(query), - Video['sequelize'].query(rawCountQuery, { type: Sequelize.QueryTypes.SELECT }) - ]).then(([ rows, totals ]) => { - // totals: totalVideos + totalVideoShares - let totalVideos = 0 - let totalVideoShares = 0 - if (totals[0]) totalVideos = parseInt(totals[0].total, 10) - if (totals[1]) totalVideoShares = parseInt(totals[1].total, 10) - - const total = totalVideos + totalVideoShares - return { - data: rows, - total: total - } - }) -} + inputVideoFile.set('size', stats.size) -listUserVideosForApi = function (userId: number, start: number, count: number, sort: string) { - const query = { - distinct: true, - offset: start, - limit: count, - order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ], - include: [ - { - model: Video['sequelize'].models.VideoChannel, - required: true, - include: [ - { - model: Video['sequelize'].models.Account, - where: { - userId - }, - required: true - } - ] - }, - Video['sequelize'].models.Tag - ] - } + await this.createTorrentAndSetInfoHash(inputVideoFile) + await inputVideoFile.save() - return Video.findAndCountAll(query).then(({ rows, count }) => { - return { - data: rows, - total: count - } - }) -} + } catch (err) { + // Auto destruction... + this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err)) -listForApi = function (start: number, count: number, sort: string) { - const query = { - distinct: true, - offset: start, - limit: count, - order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ], - include: [ - { - model: Video['sequelize'].models.VideoChannel, - required: true, - include: [ - { - model: Video['sequelize'].models.Account, - required: true, - include: [ - { - model: Video['sequelize'].models.Server, - required: false - } - ] - } - ] - }, - Video['sequelize'].models.Tag - ], - where: createBaseVideosWhere() + throw err + } } - return Video.findAndCountAll(query).then(({ rows, count }) => { - return { - data: rows, - total: count - } - }) -} + transcodeOriginalVideofile = async function (resolution: VideoResolution) { + const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR + const extname = '.mp4' -load = function (id: number) { - return Video.findById(id) -} + // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed + const videoInputPath = join(videosDirectory, this.getVideoFilename(this.getOriginalFile())) -loadByUUID = function (uuid: string, t?: Sequelize.Transaction) { - const query: Sequelize.FindOptions = { - where: { - uuid - }, - include: [ Video['sequelize'].models.VideoFile ] - } + const newVideoFile = new VideoFileModel({ + resolution, + extname, + size: 0, + videoId: this.id + }) + const videoOutputPath = join(videosDirectory, this.getVideoFilename(newVideoFile)) - if (t !== undefined) query.transaction = t + const transcodeOptions = { + inputPath: videoInputPath, + outputPath: videoOutputPath, + resolution + } - return Video.findOne(query) -} + await transcode(transcodeOptions) -loadByUrlAndPopulateAccount = function (url: string, t?: Sequelize.Transaction) { - const query: Sequelize.FindOptions = { - where: { - url - }, - include: [ - Video['sequelize'].models.VideoFile, - { - model: Video['sequelize'].models.VideoChannel, - include: [ Video['sequelize'].models.Account ] - } - ] - } + const stats = await statPromise(videoOutputPath) - if (t !== undefined) query.transaction = t + newVideoFile.set('size', stats.size) - return Video.findOne(query) -} + await this.createTorrentAndSetInfoHash(newVideoFile) -loadByUUIDOrURL = function (uuid: string, url: string, t?: Sequelize.Transaction) { - const query: Sequelize.FindOptions = { - where: { - [Sequelize.Op.or]: [ - { uuid }, - { url } - ] - }, - include: [ Video['sequelize'].models.VideoFile ] + await newVideoFile.save() + + this.VideoFiles.push(newVideoFile) } - if (t !== undefined) query.transaction = t + getOriginalFileHeight () { + const originalFilePath = this.getVideoFilePath(this.getOriginalFile()) - return Video.findOne(query) -} + return getVideoFileHeight(originalFilePath) + } -loadAndPopulateAccountAndServerAndTags = function (id: number) { - const options = { - order: [ [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ], - include: [ - { - model: Video['sequelize'].models.VideoChannel, - include: [ - { - model: Video['sequelize'].models.Account, - include: [ { model: Video['sequelize'].models.Server, required: false } ] - } - ] - }, - { - model: Video['sequelize'].models.AccountVideoRate, - include: [ Video['sequelize'].models.Account ] - }, - { - model: Video['sequelize'].models.VideoShare, - include: [ Video['sequelize'].models.Account ] - }, - Video['sequelize'].models.Tag, - Video['sequelize'].models.VideoFile - ] + getDescriptionPath () { + return `/api/${API_VERSION}/videos/${this.uuid}/description` } - return Video.findById(id, options) -} + getCategoryLabel () { + let categoryLabel = VIDEO_CATEGORIES[this.category] + if (!categoryLabel) categoryLabel = 'Misc' -loadByUUIDAndPopulateAccountAndServerAndTags = function (uuid: string) { - const options = { - order: [ [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ], - where: { - uuid - }, - include: [ - { - model: Video['sequelize'].models.VideoChannel, - include: [ - { - model: Video['sequelize'].models.Account, - include: [ { model: Video['sequelize'].models.Server, required: false } ] - } - ] - }, - { - model: Video['sequelize'].models.AccountVideoRate, - include: [ Video['sequelize'].models.Account ] - }, - { - model: Video['sequelize'].models.VideoShare, - include: [ Video['sequelize'].models.Account ] - }, - Video['sequelize'].models.Tag, - Video['sequelize'].models.VideoFile - ] + return categoryLabel } - return Video.findOne(options) -} + getLicenceLabel () { + let licenceLabel = VIDEO_LICENCES[this.licence] + if (!licenceLabel) licenceLabel = 'Unknown' -searchAndPopulateAccountAndServerAndTags = function (value: string, start: number, count: number, sort: string) { - const serverInclude: Sequelize.IncludeOptions = { - model: Video['sequelize'].models.Server, - required: false + return licenceLabel } - const accountInclude: Sequelize.IncludeOptions = { - model: Video['sequelize'].models.Account, - include: [ serverInclude ] + getLanguageLabel () { + let languageLabel = VIDEO_LANGUAGES[this.language] + if (!languageLabel) languageLabel = 'Unknown' + + return languageLabel } - const videoChannelInclude: Sequelize.IncludeOptions = { - model: Video['sequelize'].models.VideoChannel, - include: [ accountInclude ], - required: true + removeThumbnail () { + const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName()) + return unlinkPromise(thumbnailPath) } - const tagInclude: Sequelize.IncludeOptions = { - model: Video['sequelize'].models.Tag + removePreview () { + // Same name than video thumbnail + return unlinkPromise(CONFIG.STORAGE.PREVIEWS_DIR + this.getPreviewName()) } - const query: Sequelize.FindOptions = { - distinct: true, - where: createBaseVideosWhere(), - offset: start, - limit: count, - order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ] + removeFile (videoFile: VideoFileModel) { + const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile)) + return unlinkPromise(filePath) } - // TODO: search on tags too - // const escapedValue = Video['sequelize'].escape('%' + value + '%') - // query.where['id'][Sequelize.Op.in] = Video['sequelize'].literal( - // `(SELECT "VideoTags"."videoId" - // FROM "Tags" - // INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId" - // WHERE name ILIKE ${escapedValue} - // )` - // ) - - // TODO: search on account too - // accountInclude.where = { - // name: { - // [Sequelize.Op.iLike]: '%' + value + '%' - // } - // } - query.where['name'] = { - [Sequelize.Op.iLike]: '%' + value + '%' + removeTorrent (videoFile: VideoFileModel) { + const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile)) + return unlinkPromise(torrentPath) } - query.include = [ - videoChannelInclude, tagInclude - ] + private getBaseUrls () { + let baseUrlHttp + let baseUrlWs - return Video.findAndCountAll(query).then(({ rows, count }) => { - return { - data: rows, - total: count + if (this.isOwned()) { + baseUrlHttp = CONFIG.WEBSERVER.URL + baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + } else { + baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + this.VideoChannel.Account.Server.host + baseUrlWs = REMOTE_SCHEME.WS + '://' + this.VideoChannel.Account.Server.host } - }) -} - -// --------------------------------------------------------------------------- -function createBaseVideosWhere () { - return { - id: { - [Sequelize.Op.notIn]: Video['sequelize'].literal( - '(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")' - ) - }, - privacy: VideoPrivacy.PUBLIC + return { baseUrlHttp, baseUrlWs } } -} -function getBaseUrls (video: VideoInstance) { - let baseUrlHttp - let baseUrlWs - - if (video.isOwned()) { - baseUrlHttp = CONFIG.WEBSERVER.URL - baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT - } else { - baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + video.VideoChannel.Account.Server.host - baseUrlWs = REMOTE_SCHEME.WS + '://' + video.VideoChannel.Account.Server.host + private getThumbnailUrl (baseUrlHttp: string) { + return baseUrlHttp + STATIC_PATHS.THUMBNAILS + this.getThumbnailName() } - return { baseUrlHttp, baseUrlWs } -} - -function getThumbnailUrl (video: VideoInstance, baseUrlHttp: string) { - return baseUrlHttp + STATIC_PATHS.THUMBNAILS + video.getThumbnailName() -} + private getTorrentUrl (videoFile: VideoFileModel, baseUrlHttp: string) { + return baseUrlHttp + STATIC_PATHS.TORRENTS + this.getTorrentFileName(videoFile) + } -function getTorrentUrl (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string) { - return baseUrlHttp + STATIC_PATHS.TORRENTS + video.getTorrentFileName(videoFile) -} + private getVideoFileUrl (videoFile: VideoFileModel, baseUrlHttp: string) { + return baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) + } -function getVideoFileUrl (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string) { - return baseUrlHttp + STATIC_PATHS.WEBSEED + video.getVideoFilename(videoFile) -} + private generateMagnetUri (videoFile: VideoFileModel, baseUrlHttp: string, baseUrlWs: string) { + const xs = this.getTorrentUrl(videoFile, baseUrlHttp) + const announce = [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ] + const urlList = [ this.getVideoFileUrl(videoFile, baseUrlHttp) ] + + const magnetHash = { + xs, + announce, + urlList, + infoHash: videoFile.infoHash, + name: this.name + } -function generateMagnetUri (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string, baseUrlWs: string) { - const xs = getTorrentUrl(video, videoFile, baseUrlHttp) - const announce = [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ] - const urlList = [ getVideoFileUrl(video, videoFile, baseUrlHttp) ] - - const magnetHash = { - xs, - announce, - urlList, - infoHash: videoFile.infoHash, - name: video.name + return magnetUtil.encode(magnetHash) } - - return magnetUtil.encode(magnetHash) } -- cgit v1.2.3