From e4f97babf701481b55cc10fb3448feab5f97c867 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 9 Nov 2017 17:51:58 +0100 Subject: Begin activitypub --- server/models/account/account-follow-interface.ts | 23 ++ server/models/account/account-follow.ts | 56 +++ server/models/account/account-interface.ts | 74 ++++ .../models/account/account-video-rate-interface.ts | 26 ++ server/models/account/account-video-rate.ts | 78 ++++ server/models/account/account.ts | 444 +++++++++++++++++++++ server/models/account/index.ts | 4 + server/models/account/user-interface.ts | 69 ++++ server/models/account/user.ts | 311 +++++++++++++++ server/models/index.ts | 2 +- server/models/job/job-interface.ts | 6 +- server/models/job/job.ts | 12 +- server/models/oauth/oauth-token-interface.ts | 2 +- server/models/pod/pod-interface.ts | 2 - server/models/pod/pod.ts | 12 - server/models/user/index.ts | 2 - server/models/user/user-interface.ts | 69 ---- server/models/user/user-video-rate-interface.ts | 26 -- server/models/user/user-video-rate.ts | 78 ---- server/models/user/user.ts | 312 --------------- server/models/video/author-interface.ts | 45 --- server/models/video/author.ts | 171 -------- server/models/video/video-channel-interface.ts | 38 +- server/models/video/video-channel.ts | 100 +++-- server/models/video/video-interface.ts | 60 +-- server/models/video/video.ts | 378 +++++++++--------- 26 files changed, 1398 insertions(+), 1002 deletions(-) create mode 100644 server/models/account/account-follow-interface.ts create mode 100644 server/models/account/account-follow.ts create mode 100644 server/models/account/account-interface.ts create mode 100644 server/models/account/account-video-rate-interface.ts create mode 100644 server/models/account/account-video-rate.ts create mode 100644 server/models/account/account.ts create mode 100644 server/models/account/index.ts create mode 100644 server/models/account/user-interface.ts create mode 100644 server/models/account/user.ts delete mode 100644 server/models/user/index.ts delete mode 100644 server/models/user/user-interface.ts delete mode 100644 server/models/user/user-video-rate-interface.ts delete mode 100644 server/models/user/user-video-rate.ts delete mode 100644 server/models/user/user.ts delete mode 100644 server/models/video/author-interface.ts delete mode 100644 server/models/video/author.ts (limited to 'server/models') diff --git a/server/models/account/account-follow-interface.ts b/server/models/account/account-follow-interface.ts new file mode 100644 index 000000000..3be383649 --- /dev/null +++ b/server/models/account/account-follow-interface.ts @@ -0,0 +1,23 @@ +import * as Sequelize from 'sequelize' +import * as Promise from 'bluebird' + +import { VideoRateType } from '../../../shared/models/videos/video-rate.type' + +export namespace AccountFollowMethods { +} + +export interface AccountFollowClass { +} + +export interface AccountFollowAttributes { + accountId: number + targetAccountId: number +} + +export interface AccountFollowInstance extends AccountFollowClass, AccountFollowAttributes, Sequelize.Instance { + id: number + createdAt: Date + updatedAt: Date +} + +export interface AccountFollowModel extends AccountFollowClass, Sequelize.Model {} diff --git a/server/models/account/account-follow.ts b/server/models/account/account-follow.ts new file mode 100644 index 000000000..9bf03b253 --- /dev/null +++ b/server/models/account/account-follow.ts @@ -0,0 +1,56 @@ +import * as Sequelize from 'sequelize' + +import { addMethodsToModel } from '../utils' +import { + AccountFollowInstance, + AccountFollowAttributes, + + AccountFollowMethods +} from './account-follow-interface' + +let AccountFollow: Sequelize.Model + +export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { + AccountFollow = sequelize.define('AccountFollow', + { }, + { + indexes: [ + { + fields: [ 'accountId' ], + unique: true + }, + { + fields: [ 'targetAccountId' ], + unique: true + } + ] + } + ) + + const classMethods = [ + associate + ] + addMethodsToModel(AccountFollow, classMethods) + + return AccountFollow +} + +// ------------------------------ STATICS ------------------------------ + +function associate (models) { + AccountFollow.belongsTo(models.Account, { + foreignKey: { + name: 'accountId', + allowNull: false + }, + onDelete: 'CASCADE' + }) + + AccountFollow.belongsTo(models.Account, { + foreignKey: { + name: 'targetAccountId', + allowNull: false + }, + onDelete: 'CASCADE' + }) +} diff --git a/server/models/account/account-interface.ts b/server/models/account/account-interface.ts new file mode 100644 index 000000000..2ef3e2246 --- /dev/null +++ b/server/models/account/account-interface.ts @@ -0,0 +1,74 @@ +import * as Sequelize from 'sequelize' +import * as Bluebird from 'bluebird' + +import { PodInstance } from '../pod/pod-interface' +import { VideoChannelInstance } from '../video/video-channel-interface' +import { ActivityPubActor } from '../../../shared' +import { ResultList } from '../../../shared/models/result-list.model' + +export namespace AccountMethods { + export type Load = (id: number) => Bluebird + export type LoadByUUID = (uuid: string) => Bluebird + export type LoadByUrl = (url: string) => Bluebird + export type LoadAccountByPodAndUUID = (uuid: string, podId: number, transaction: Sequelize.Transaction) => Bluebird + export type LoadLocalAccountByName = (name: string) => Bluebird + export type ListOwned = () => Bluebird + export type ListFollowerUrlsForApi = (name: string, start: number, count: number) => Promise< ResultList > + export type ListFollowingUrlsForApi = (name: string, start: number, count: number) => Promise< ResultList > + + export type ToActivityPubObject = (this: AccountInstance) => ActivityPubActor + export type IsOwned = (this: AccountInstance) => boolean + export type GetFollowerSharedInboxUrls = (this: AccountInstance) => Bluebird + export type GetFollowingUrl = (this: AccountInstance) => string + export type GetFollowersUrl = (this: AccountInstance) => string + export type GetPublicKeyUrl = (this: AccountInstance) => string +} + +export interface AccountClass { + loadAccountByPodAndUUID: AccountMethods.LoadAccountByPodAndUUID + load: AccountMethods.Load + loadByUUID: AccountMethods.LoadByUUID + loadByUrl: AccountMethods.LoadByUrl + loadLocalAccountByName: AccountMethods.LoadLocalAccountByName + listOwned: AccountMethods.ListOwned + listFollowerUrlsForApi: AccountMethods.ListFollowerUrlsForApi + listFollowingUrlsForApi: AccountMethods.ListFollowingUrlsForApi +} + +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 + + podId?: number + userId?: number + applicationId?: number +} + +export interface AccountInstance extends AccountClass, AccountAttributes, Sequelize.Instance { + isOwned: AccountMethods.IsOwned + toActivityPubObject: AccountMethods.ToActivityPubObject + getFollowerSharedInboxUrls: AccountMethods.GetFollowerSharedInboxUrls + getFollowingUrl: AccountMethods.GetFollowingUrl + getFollowersUrl: AccountMethods.GetFollowersUrl + getPublicKeyUrl: AccountMethods.GetPublicKeyUrl + + id: number + createdAt: Date + updatedAt: Date + + Pod: PodInstance + VideoChannels: VideoChannelInstance[] +} + +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 new file mode 100644 index 000000000..82cbe38cc --- /dev/null +++ b/server/models/account/account-video-rate-interface.ts @@ -0,0 +1,26 @@ +import * as Sequelize from 'sequelize' +import * as Promise from 'bluebird' + +import { VideoRateType } from '../../../shared/models/videos/video-rate.type' + +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 +} + +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 new file mode 100644 index 000000000..7f7c97606 --- /dev/null +++ b/server/models/account/account-video-rate.ts @@ -0,0 +1,78 @@ +/* + Account rates per video. +*/ +import { values } from 'lodash' +import * as Sequelize from 'sequelize' + +import { VIDEO_RATE_TYPES } from '../../initializers' + +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 + } + }, + { + indexes: [ + { + fields: [ 'videoId', 'accountId', 'type' ], + unique: true + } + ] + } + ) + + const classMethods = [ + associate, + + load + ] + addMethodsToModel(AccountVideoRate, classMethods) + + return AccountVideoRate +} + +// ------------------------------ STATICS ------------------------------ + +function associate (models) { + AccountVideoRate.belongsTo(models.Video, { + foreignKey: { + name: 'videoId', + allowNull: false + }, + onDelete: 'CASCADE' + }) + + AccountVideoRate.belongsTo(models.Account, { + foreignKey: { + name: 'accountId', + allowNull: false + }, + onDelete: 'CASCADE' + }) +} + +load = function (accountId: number, videoId: number, transaction: Sequelize.Transaction) { + const options: Sequelize.FindOptions = { + where: { + accountId, + videoId + } + } + if (transaction) options.transaction = transaction + + return AccountVideoRate.findOne(options) +} diff --git a/server/models/account/account.ts b/server/models/account/account.ts new file mode 100644 index 000000000..00c0aefd4 --- /dev/null +++ b/server/models/account/account.ts @@ -0,0 +1,444 @@ +import * as Sequelize from 'sequelize' + +import { + isUserUsernameValid, + isAccountPublicKeyValid, + isAccountUrlValid, + isAccountPrivateKeyValid, + isAccountFollowersCountValid, + isAccountFollowingCountValid, + isAccountInboxValid, + isAccountOutboxValid, + isAccountSharedInboxValid, + isAccountFollowersValid, + isAccountFollowingValid, + activityPubContextify +} from '../../helpers' + +import { addMethodsToModel } from '../utils' +import { + AccountInstance, + AccountAttributes, + + AccountMethods +} from './account-interface' + +let Account: Sequelize.Model +let loadAccountByPodAndUUID: AccountMethods.LoadAccountByPodAndUUID +let load: AccountMethods.Load +let loadByUUID: AccountMethods.LoadByUUID +let loadByUrl: AccountMethods.LoadByUrl +let loadLocalAccountByName: AccountMethods.LoadLocalAccountByName +let listOwned: AccountMethods.ListOwned +let listFollowerUrlsForApi: AccountMethods.ListFollowerUrlsForApi +let listFollowingUrlsForApi: AccountMethods.ListFollowingUrlsForApi +let isOwned: AccountMethods.IsOwned +let toActivityPubObject: AccountMethods.ToActivityPubObject +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', + { + uuid: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + allowNull: false, + validate: { + isUUID: 4 + } + }, + name: { + type: DataTypes.STRING, + allowNull: false, + validate: { + usernameValid: value => { + const res = isUserUsernameValid(value) + if (res === false) throw new Error('Username is not valid.') + } + } + }, + url: { + type: DataTypes.STRING, + allowNull: false, + validate: { + urlValid: value => { + const res = isAccountUrlValid(value) + if (res === false) throw new Error('URL is not valid.') + } + } + }, + publicKey: { + type: DataTypes.STRING, + allowNull: false, + validate: { + publicKeyValid: value => { + const res = isAccountPublicKeyValid(value) + if (res === false) throw new Error('Public key is not valid.') + } + } + }, + privateKey: { + type: DataTypes.STRING, + allowNull: false, + 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: { + followersCountValid: value => { + const res = isAccountFollowingCountValid(value) + if (res === false) throw new Error('Following count is not valid.') + } + } + }, + inboxUrl: { + type: DataTypes.STRING, + allowNull: false, + validate: { + inboxUrlValid: value => { + const res = isAccountInboxValid(value) + if (res === false) throw new Error('Inbox URL is not valid.') + } + } + }, + outboxUrl: { + type: DataTypes.STRING, + allowNull: false, + validate: { + outboxUrlValid: value => { + const res = isAccountOutboxValid(value) + if (res === false) throw new Error('Outbox URL is not valid.') + } + } + }, + sharedInboxUrl: { + type: DataTypes.STRING, + allowNull: false, + validate: { + sharedInboxUrlValid: value => { + const res = isAccountSharedInboxValid(value) + if (res === false) throw new Error('Shared inbox URL is not valid.') + } + } + }, + followersUrl: { + type: DataTypes.STRING, + allowNull: false, + validate: { + followersUrlValid: value => { + const res = isAccountFollowersValid(value) + if (res === false) throw new Error('Followers URL is not valid.') + } + } + }, + followingUrl: { + type: DataTypes.STRING, + allowNull: false, + validate: { + followingUrlValid: value => { + const res = isAccountFollowingValid(value) + if (res === false) throw new Error('Following URL is not valid.') + } + } + } + }, + { + indexes: [ + { + fields: [ 'name' ] + }, + { + fields: [ 'podId' ] + }, + { + fields: [ 'userId' ], + unique: true + }, + { + fields: [ 'applicationId' ], + unique: true + }, + { + fields: [ 'name', 'podId' ], + unique: true + } + ], + hooks: { afterDestroy } + } + ) + + const classMethods = [ + associate, + loadAccountByPodAndUUID, + load, + loadByUUID, + loadLocalAccountByName, + listOwned, + listFollowerUrlsForApi, + listFollowingUrlsForApi + ] + const instanceMethods = [ + isOwned, + toActivityPubObject, + getFollowerSharedInboxUrls, + getFollowingUrl, + getFollowersUrl, + getPublicKeyUrl + ] + addMethodsToModel(Account, classMethods, instanceMethods) + + return Account +} + +// --------------------------------------------------------------------------- + +function associate (models) { + Account.belongsTo(models.Pod, { + foreignKey: { + name: 'podId', + allowNull: true + }, + onDelete: 'cascade' + }) + + Account.belongsTo(models.User, { + foreignKey: { + name: 'userId', + allowNull: true + }, + onDelete: 'cascade' + }) + + Account.belongsTo(models.Application, { + foreignKey: { + name: 'userId', + allowNull: true + }, + onDelete: 'cascade' + }) + + Account.hasMany(models.VideoChannel, { + foreignKey: { + name: 'accountId', + allowNull: false + }, + onDelete: 'cascade', + hooks: true + }) + + Account.hasMany(models.AccountFollower, { + foreignKey: { + name: 'accountId', + allowNull: false + }, + onDelete: 'cascade' + }) + + Account.hasMany(models.AccountFollower, { + foreignKey: { + name: 'targetAccountId', + allowNull: false + }, + onDelete: 'cascade' + }) +} + +function afterDestroy (account: AccountInstance) { + if (account.isOwned()) { + const removeVideoAccountToFriendsParams = { + uuid: account.uuid + } + + return removeVideoAccountToFriends(removeVideoAccountToFriendsParams) + } + + return undefined +} + +toActivityPubObject = function (this: AccountInstance) { + const type = this.podId ? 'Application' : '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 + } + } + + return activityPubContextify(json) +} + +isOwned = function (this: AccountInstance) { + return this.podId === null +} + +getFollowerSharedInboxUrls = function (this: AccountInstance) { + const query: Sequelize.FindOptions = { + attributes: [ 'sharedInboxUrl' ], + include: [ + { + model: Account['sequelize'].models.AccountFollower, + where: { + targetAccountId: this.id + } + } + ] + } + + return Account.findAll(query) + .then(accounts => accounts.map(a => a.sharedInboxUrl)) +} + +getFollowingUrl = function (this: AccountInstance) { + return this.url + '/followers' +} + +getFollowersUrl = function (this: AccountInstance) { + return this.url + '/followers' +} + +getPublicKeyUrl = function (this: AccountInstance) { + return this.url + '#main-key' +} + +// ------------------------------ STATICS ------------------------------ + +listOwned = function () { + const query: Sequelize.FindOptions = { + where: { + podId: null + } + } + + return Account.findAll(query) +} + +listFollowerUrlsForApi = function (name: string, start: number, count: number) { + return createListFollowForApiQuery('followers', name, start, count) +} + +listFollowingUrlsForApi = function (name: string, start: number, count: number) { + return createListFollowForApiQuery('following', name, start, count) +} + +load = function (id: number) { + return Account.findById(id) +} + +loadByUUID = function (uuid: string) { + const query: Sequelize.FindOptions = { + where: { + uuid + } + } + + return Account.findOne(query) +} + +loadLocalAccountByName = function (name: string) { + const query: Sequelize.FindOptions = { + where: { + name, + userId: { + [Sequelize.Op.ne]: null + } + } + } + + return Account.findOne(query) +} + +loadByUrl = function (url: string) { + const query: Sequelize.FindOptions = { + where: { + url + } + } + + return Account.findOne(query) +} + +loadAccountByPodAndUUID = function (uuid: string, podId: number, transaction: Sequelize.Transaction) { + const query: Sequelize.FindOptions = { + where: { + podId, + uuid + }, + transaction + } + + return Account.find(query) +} + +// ------------------------------ UTILS ------------------------------ + +async function createListFollowForApiQuery (type: 'followers' | 'following', name: string, start: number, count: number) { + let firstJoin: string + let secondJoin: string + + if (type === 'followers') { + firstJoin = 'targetAccountId' + secondJoin = 'accountId' + } else { + firstJoin = 'accountId' + secondJoin = 'targetAccountId' + } + + const selections = [ '"Followers"."url" AS "url"', 'COUNT(*) AS "total"' ] + const tasks: Promise[] = [] + + for (const selection of selections) { + const query = 'SELECT ' + selection + ' FROM "Account" ' + + 'INNER JOIN "AccountFollower" ON "AccountFollower"."' + firstJoin + '" = "Account"."id" ' + + 'INNER JOIN "Account" AS "Followers" ON "Followers"."id" = "AccountFollower"."' + secondJoin + '" ' + + 'WHERE "Account"."name" = \'$name\' ' + + 'LIMIT ' + start + ', ' + count + + const options = { + bind: { name }, + type: Sequelize.QueryTypes.SELECT + } + tasks.push(Account['sequelize'].query(query, options)) + } + + const [ followers, [ { total } ]] = await Promise.all(tasks) + const urls: string[] = followers.map(f => f.url) + + return { + data: urls, + total: parseInt(total, 10) + } +} diff --git a/server/models/account/index.ts b/server/models/account/index.ts new file mode 100644 index 000000000..179f66974 --- /dev/null +++ b/server/models/account/index.ts @@ -0,0 +1,4 @@ +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 new file mode 100644 index 000000000..1a04fb750 --- /dev/null +++ b/server/models/account/user-interface.ts @@ -0,0 +1,69 @@ +import * as Sequelize from 'sequelize' +import * as Bluebird from 'bluebird' + +// Don't use barrel, import just what we need +import { AccountInstance } from './account-interface' +import { User as FormattedUser } from '../../../shared/models/users/user.model' +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' + +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 new file mode 100644 index 000000000..1401762c5 --- /dev/null +++ b/server/models/account/user.ts @@ -0,0 +1,311 @@ +import * as Sequelize from 'sequelize' + +import { getSort, addMethodsToModel } from '../utils' +import { + cryptPassword, + comparePassword, + isUserPasswordValid, + isUserUsernameValid, + isUserDisplayNSFWValid, + isUserVideoQuotaValid, + isUserRoleValid +} from '../../helpers' +import { UserRight, USER_ROLE_LABELS, hasUserRight } from '../../../shared' + +import { + UserInstance, + UserAttributes, + + 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', + { + 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.') + } + } + } + }, + { + indexes: [ + { + fields: [ 'username' ], + unique: true + }, + { + fields: [ 'email' ], + unique: true + } + ], + hooks: { + beforeCreate: beforeCreateOrUpdate, + beforeUpdate: beforeCreateOrUpdate + } + } + ) + + const classMethods = [ + associate, + + countTotal, + getByUsername, + listForApi, + loadById, + loadByUsername, + loadByUsernameAndPopulateChannels, + loadByUsernameOrEmail + ] + const instanceMethods = [ + hasRight, + isPasswordMatch, + toFormattedJSON, + isAbleToUploadVideo + ] + addMethodsToModel(User, classMethods, instanceMethods) + + return User +} + +function beforeCreateOrUpdate (user: UserInstance) { + if (user.changed('password')) { + return cryptPassword(user.password) + .then(hash => { + user.password = hash + return undefined + }) + } +} + +// ------------------------------ METHODS ------------------------------ + +hasRight = function (this: UserInstance, right: UserRight) { + return hasUserRight(this.role, right) +} + +isPasswordMatch = function (this: UserInstance, password: string) { + return comparePassword(password, this.password) +} + +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, + author: { + id: this.Account.id, + uuid: this.Account.uuid + } + } + + 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 + + return 1 + }) + + 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' + }) + + User.hasMany(models.OAuthToken, { + foreignKey: 'userId', + onDelete: 'cascade' + }) +} + +countTotal = function () { + return this.count() +} + +getByUsername = function (username: string) { + const query = { + where: { + username: username + }, + include: [ { model: User['sequelize'].models.Account, required: true } ] + } + + return User.findOne(query) +} + +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 User.findAndCountAll(query).then(({ rows, count }) => { + return { + data: rows, + total: count + } + }) +} + +loadById = function (id: number) { + const options = { + include: [ { model: User['sequelize'].models.Account, required: true } ] + } + + return User.findById(id, options) +} + +loadByUsername = function (username: string) { + const query = { + where: { + username + }, + include: [ { model: User['sequelize'].models.Account, required: true } ] + } + + return User.findOne(query) +} + +loadByUsernameAndPopulateChannels = function (username: string) { + const query = { + where: { + username + }, + include: [ + { + model: User['sequelize'].models.Account, + required: true, + include: [ User['sequelize'].models.VideoChannel ] + } + ] + } + + return User.findOne(query) +} + +loadByUsernameOrEmail = function (username: string, email: string) { + const query = { + include: [ { model: User['sequelize'].models.Account, required: true } ], + where: { + [Sequelize.Op.or]: [ { username }, { email } ] + } + } + + // FIXME: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18387 + return (User as any).findOne(query) +} + +// --------------------------------------------------------------------------- + +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"."authorId" = "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 User['sequelize'].query(query, options).then(([ { total } ]) => { + if (total === null) return 0 + + return parseInt(total, 10) + }) +} diff --git a/server/models/index.ts b/server/models/index.ts index b392a8a77..29479e067 100644 --- a/server/models/index.ts +++ b/server/models/index.ts @@ -3,5 +3,5 @@ export * from './job' export * from './oauth' export * from './pod' export * from './request' -export * from './user' +export * from './account' export * from './video' diff --git a/server/models/job/job-interface.ts b/server/models/job/job-interface.ts index ba5622977..163930a4f 100644 --- a/server/models/job/job-interface.ts +++ b/server/models/job/job-interface.ts @@ -1,14 +1,14 @@ import * as Sequelize from 'sequelize' import * as Promise from 'bluebird' -import { JobState } from '../../../shared/models/job.model' +import { JobCategory, JobState } from '../../../shared/models/job.model' export namespace JobMethods { - export type ListWithLimit = (limit: number, state: JobState) => Promise + export type ListWithLimitByCategory = (limit: number, state: JobState, category: JobCategory) => Promise } export interface JobClass { - listWithLimit: JobMethods.ListWithLimit + listWithLimitByCategory: JobMethods.ListWithLimitByCategory } export interface JobAttributes { diff --git a/server/models/job/job.ts b/server/models/job/job.ts index 968f9d71d..ce1203e5a 100644 --- a/server/models/job/job.ts +++ b/server/models/job/job.ts @@ -1,7 +1,7 @@ import { values } from 'lodash' import * as Sequelize from 'sequelize' -import { JOB_STATES } from '../../initializers' +import { JOB_STATES, JOB_CATEGORIES } from '../../initializers' import { addMethodsToModel } from '../utils' import { @@ -13,7 +13,7 @@ import { import { JobState } from '../../../shared/models/job.model' let Job: Sequelize.Model -let listWithLimit: JobMethods.ListWithLimit +let listWithLimitByCategory: JobMethods.ListWithLimitByCategory export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { Job = sequelize.define('Job', @@ -22,6 +22,10 @@ export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Se type: DataTypes.ENUM(values(JOB_STATES)), allowNull: false }, + category: { + type: DataTypes.ENUM(values(JOB_CATEGORIES)), + allowNull: false + }, handlerName: { type: DataTypes.STRING, allowNull: false @@ -40,7 +44,7 @@ export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Se } ) - const classMethods = [ listWithLimit ] + const classMethods = [ listWithLimitByCategory ] addMethodsToModel(Job, classMethods) return Job @@ -48,7 +52,7 @@ export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Se // --------------------------------------------------------------------------- -listWithLimit = function (limit: number, state: JobState) { +listWithLimitByCategory = function (limit: number, state: JobState) { const query = { order: [ [ 'id', 'ASC' ] diff --git a/server/models/oauth/oauth-token-interface.ts b/server/models/oauth/oauth-token-interface.ts index 0c947bde8..ef97893c4 100644 --- a/server/models/oauth/oauth-token-interface.ts +++ b/server/models/oauth/oauth-token-interface.ts @@ -1,7 +1,7 @@ import * as Sequelize from 'sequelize' import * as Promise from 'bluebird' -import { UserModel } from '../user/user-interface' +import { UserModel } from '../account/user-interface' export type OAuthTokenInfo = { refreshToken: string diff --git a/server/models/pod/pod-interface.ts b/server/models/pod/pod-interface.ts index 7e095d424..6c5aab3fa 100644 --- a/server/models/pod/pod-interface.ts +++ b/server/models/pod/pod-interface.ts @@ -48,9 +48,7 @@ export interface PodClass { export interface PodAttributes { id?: number host?: string - publicKey?: string score?: number | Sequelize.literal // Sequelize literal for 'score +' + value - email?: string } export interface PodInstance extends PodClass, PodAttributes, Sequelize.Instance { diff --git a/server/models/pod/pod.ts b/server/models/pod/pod.ts index 6b33336b8..7c8b49bf8 100644 --- a/server/models/pod/pod.ts +++ b/server/models/pod/pod.ts @@ -39,10 +39,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da } } }, - publicKey: { - type: DataTypes.STRING(5000), - allowNull: false - }, score: { type: DataTypes.INTEGER, defaultValue: FRIEND_SCORE.BASE, @@ -51,13 +47,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da isInt: true, max: FRIEND_SCORE.MAX } - }, - email: { - type: DataTypes.STRING(400), - allowNull: false, - validate: { - isEmail: true - } } }, { @@ -100,7 +89,6 @@ toFormattedJSON = function (this: PodInstance) { const json = { id: this.id, host: this.host, - email: this.email, score: this.score as number, createdAt: this.createdAt } diff --git a/server/models/user/index.ts b/server/models/user/index.ts deleted file mode 100644 index ed3689518..000000000 --- a/server/models/user/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './user-video-rate-interface' -export * from './user-interface' diff --git a/server/models/user/user-interface.ts b/server/models/user/user-interface.ts deleted file mode 100644 index 49c75aa3b..000000000 --- a/server/models/user/user-interface.ts +++ /dev/null @@ -1,69 +0,0 @@ -import * as Sequelize from 'sequelize' -import * as Promise from 'bluebird' - -// Don't use barrel, import just what we need -import { User as FormattedUser } from '../../../shared/models/users/user.model' -import { ResultList } from '../../../shared/models/result-list.model' -import { AuthorInstance } from '../video/author-interface' -import { UserRight } from '../../../shared/models/users/user-right.enum' -import { UserRole } from '../../../shared/models/users/user-role' - -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 = () => Promise - - export type GetByUsername = (username: string) => Promise - - export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList > - - export type LoadById = (id: number) => Promise - - export type LoadByUsername = (username: string) => Promise - export type LoadByUsernameAndPopulateChannels = (username: string) => Promise - - export type LoadByUsernameOrEmail = (username: string, email: string) => Promise -} - -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 - - Author?: AuthorInstance -} - -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/user/user-video-rate-interface.ts b/server/models/user/user-video-rate-interface.ts deleted file mode 100644 index ea0fdc4d9..000000000 --- a/server/models/user/user-video-rate-interface.ts +++ /dev/null @@ -1,26 +0,0 @@ -import * as Sequelize from 'sequelize' -import * as Promise from 'bluebird' - -import { VideoRateType } from '../../../shared/models/videos/video-rate.type' - -export namespace UserVideoRateMethods { - export type Load = (userId: number, videoId: number, transaction: Sequelize.Transaction) => Promise -} - -export interface UserVideoRateClass { - load: UserVideoRateMethods.Load -} - -export interface UserVideoRateAttributes { - type: VideoRateType - userId: number - videoId: number -} - -export interface UserVideoRateInstance extends UserVideoRateClass, UserVideoRateAttributes, Sequelize.Instance { - id: number - createdAt: Date - updatedAt: Date -} - -export interface UserVideoRateModel extends UserVideoRateClass, Sequelize.Model {} diff --git a/server/models/user/user-video-rate.ts b/server/models/user/user-video-rate.ts deleted file mode 100644 index 7d6dd7281..000000000 --- a/server/models/user/user-video-rate.ts +++ /dev/null @@ -1,78 +0,0 @@ -/* - User rates per video. -*/ -import { values } from 'lodash' -import * as Sequelize from 'sequelize' - -import { VIDEO_RATE_TYPES } from '../../initializers' - -import { addMethodsToModel } from '../utils' -import { - UserVideoRateInstance, - UserVideoRateAttributes, - - UserVideoRateMethods -} from './user-video-rate-interface' - -let UserVideoRate: Sequelize.Model -let load: UserVideoRateMethods.Load - -export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { - UserVideoRate = sequelize.define('UserVideoRate', - { - type: { - type: DataTypes.ENUM(values(VIDEO_RATE_TYPES)), - allowNull: false - } - }, - { - indexes: [ - { - fields: [ 'videoId', 'userId', 'type' ], - unique: true - } - ] - } - ) - - const classMethods = [ - associate, - - load - ] - addMethodsToModel(UserVideoRate, classMethods) - - return UserVideoRate -} - -// ------------------------------ STATICS ------------------------------ - -function associate (models) { - UserVideoRate.belongsTo(models.Video, { - foreignKey: { - name: 'videoId', - allowNull: false - }, - onDelete: 'CASCADE' - }) - - UserVideoRate.belongsTo(models.User, { - foreignKey: { - name: 'userId', - allowNull: false - }, - onDelete: 'CASCADE' - }) -} - -load = function (userId: number, videoId: number, transaction: Sequelize.Transaction) { - const options: Sequelize.FindOptions = { - where: { - userId, - videoId - } - } - if (transaction) options.transaction = transaction - - return UserVideoRate.findOne(options) -} diff --git a/server/models/user/user.ts b/server/models/user/user.ts deleted file mode 100644 index b974418d4..000000000 --- a/server/models/user/user.ts +++ /dev/null @@ -1,312 +0,0 @@ -import * as Sequelize from 'sequelize' -import * as Promise from 'bluebird' - -import { getSort, addMethodsToModel } from '../utils' -import { - cryptPassword, - comparePassword, - isUserPasswordValid, - isUserUsernameValid, - isUserDisplayNSFWValid, - isUserVideoQuotaValid, - isUserRoleValid -} from '../../helpers' -import { UserRight, USER_ROLE_LABELS, hasUserRight } from '../../../shared' - -import { - UserInstance, - UserAttributes, - - 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', - { - 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.') - } - } - } - }, - { - indexes: [ - { - fields: [ 'username' ], - unique: true - }, - { - fields: [ 'email' ], - unique: true - } - ], - hooks: { - beforeCreate: beforeCreateOrUpdate, - beforeUpdate: beforeCreateOrUpdate - } - } - ) - - const classMethods = [ - associate, - - countTotal, - getByUsername, - listForApi, - loadById, - loadByUsername, - loadByUsernameAndPopulateChannels, - loadByUsernameOrEmail - ] - const instanceMethods = [ - hasRight, - isPasswordMatch, - toFormattedJSON, - isAbleToUploadVideo - ] - addMethodsToModel(User, classMethods, instanceMethods) - - return User -} - -function beforeCreateOrUpdate (user: UserInstance) { - if (user.changed('password')) { - return cryptPassword(user.password) - .then(hash => { - user.password = hash - return undefined - }) - } -} - -// ------------------------------ METHODS ------------------------------ - -hasRight = function (this: UserInstance, right: UserRight) { - return hasUserRight(this.role, right) -} - -isPasswordMatch = function (this: UserInstance, password: string) { - return comparePassword(password, this.password) -} - -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, - author: { - id: this.Author.id, - uuid: this.Author.uuid - } - } - - if (Array.isArray(this.Author.VideoChannels) === true) { - const videoChannels = this.Author.VideoChannels - .map(c => c.toFormattedJSON()) - .sort((v1, v2) => { - if (v1.createdAt < v2.createdAt) return -1 - if (v1.createdAt === v2.createdAt) return 0 - - return 1 - }) - - 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.Author, { - foreignKey: 'userId', - onDelete: 'cascade' - }) - - User.hasMany(models.OAuthToken, { - foreignKey: 'userId', - onDelete: 'cascade' - }) -} - -countTotal = function () { - return this.count() -} - -getByUsername = function (username: string) { - const query = { - where: { - username: username - }, - include: [ { model: User['sequelize'].models.Author, required: true } ] - } - - return User.findOne(query) -} - -listForApi = function (start: number, count: number, sort: string) { - const query = { - offset: start, - limit: count, - order: [ getSort(sort) ], - include: [ { model: User['sequelize'].models.Author, required: true } ] - } - - return User.findAndCountAll(query).then(({ rows, count }) => { - return { - data: rows, - total: count - } - }) -} - -loadById = function (id: number) { - const options = { - include: [ { model: User['sequelize'].models.Author, required: true } ] - } - - return User.findById(id, options) -} - -loadByUsername = function (username: string) { - const query = { - where: { - username - }, - include: [ { model: User['sequelize'].models.Author, required: true } ] - } - - return User.findOne(query) -} - -loadByUsernameAndPopulateChannels = function (username: string) { - const query = { - where: { - username - }, - include: [ - { - model: User['sequelize'].models.Author, - required: true, - include: [ User['sequelize'].models.VideoChannel ] - } - ] - } - - return User.findOne(query) -} - -loadByUsernameOrEmail = function (username: string, email: string) { - const query = { - include: [ { model: User['sequelize'].models.Author, required: true } ], - where: { - [Sequelize.Op.or]: [ { username }, { email } ] - } - } - - // FIXME: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18387 - return (User as any).findOne(query) -} - -// --------------------------------------------------------------------------- - -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 "Authors" ON "VideoChannels"."authorId" = "Authors"."id" ' + - 'INNER JOIN "Users" ON "Authors"."userId" = "Users"."id" ' + - 'WHERE "Users"."id" = $userId GROUP BY "Videos"."id") t' - - const options = { - bind: { userId: user.id }, - type: Sequelize.QueryTypes.SELECT - } - return User['sequelize'].query(query, options).then(([ { total } ]) => { - if (total === null) return 0 - - return parseInt(total, 10) - }) -} diff --git a/server/models/video/author-interface.ts b/server/models/video/author-interface.ts deleted file mode 100644 index fc69ff3c2..000000000 --- a/server/models/video/author-interface.ts +++ /dev/null @@ -1,45 +0,0 @@ -import * as Sequelize from 'sequelize' -import * as Promise from 'bluebird' - -import { PodInstance } from '../pod/pod-interface' -import { RemoteVideoAuthorCreateData } from '../../../shared/models/pods/remote-video/remote-video-author-create-request.model' -import { VideoChannelInstance } from './video-channel-interface' - -export namespace AuthorMethods { - export type Load = (id: number) => Promise - export type LoadByUUID = (uuid: string) => Promise - export type LoadAuthorByPodAndUUID = (uuid: string, podId: number, transaction: Sequelize.Transaction) => Promise - export type ListOwned = () => Promise - - export type ToAddRemoteJSON = (this: AuthorInstance) => RemoteVideoAuthorCreateData - export type IsOwned = (this: AuthorInstance) => boolean -} - -export interface AuthorClass { - loadAuthorByPodAndUUID: AuthorMethods.LoadAuthorByPodAndUUID - load: AuthorMethods.Load - loadByUUID: AuthorMethods.LoadByUUID - listOwned: AuthorMethods.ListOwned -} - -export interface AuthorAttributes { - name: string - uuid?: string - - podId?: number - userId?: number -} - -export interface AuthorInstance extends AuthorClass, AuthorAttributes, Sequelize.Instance { - isOwned: AuthorMethods.IsOwned - toAddRemoteJSON: AuthorMethods.ToAddRemoteJSON - - id: number - createdAt: Date - updatedAt: Date - - Pod: PodInstance - VideoChannels: VideoChannelInstance[] -} - -export interface AuthorModel extends AuthorClass, Sequelize.Model {} diff --git a/server/models/video/author.ts b/server/models/video/author.ts deleted file mode 100644 index 43f84c3ea..000000000 --- a/server/models/video/author.ts +++ /dev/null @@ -1,171 +0,0 @@ -import * as Sequelize from 'sequelize' - -import { isUserUsernameValid } from '../../helpers' -import { removeVideoAuthorToFriends } from '../../lib' - -import { addMethodsToModel } from '../utils' -import { - AuthorInstance, - AuthorAttributes, - - AuthorMethods -} from './author-interface' - -let Author: Sequelize.Model -let loadAuthorByPodAndUUID: AuthorMethods.LoadAuthorByPodAndUUID -let load: AuthorMethods.Load -let loadByUUID: AuthorMethods.LoadByUUID -let listOwned: AuthorMethods.ListOwned -let isOwned: AuthorMethods.IsOwned -let toAddRemoteJSON: AuthorMethods.ToAddRemoteJSON - -export default function defineAuthor (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { - Author = sequelize.define('Author', - { - uuid: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - allowNull: false, - validate: { - isUUID: 4 - } - }, - name: { - type: DataTypes.STRING, - allowNull: false, - validate: { - usernameValid: value => { - const res = isUserUsernameValid(value) - if (res === false) throw new Error('Username is not valid.') - } - } - } - }, - { - indexes: [ - { - fields: [ 'name' ] - }, - { - fields: [ 'podId' ] - }, - { - fields: [ 'userId' ], - unique: true - }, - { - fields: [ 'name', 'podId' ], - unique: true - } - ], - hooks: { afterDestroy } - } - ) - - const classMethods = [ - associate, - loadAuthorByPodAndUUID, - load, - loadByUUID, - listOwned - ] - const instanceMethods = [ - isOwned, - toAddRemoteJSON - ] - addMethodsToModel(Author, classMethods, instanceMethods) - - return Author -} - -// --------------------------------------------------------------------------- - -function associate (models) { - Author.belongsTo(models.Pod, { - foreignKey: { - name: 'podId', - allowNull: true - }, - onDelete: 'cascade' - }) - - Author.belongsTo(models.User, { - foreignKey: { - name: 'userId', - allowNull: true - }, - onDelete: 'cascade' - }) - - Author.hasMany(models.VideoChannel, { - foreignKey: { - name: 'authorId', - allowNull: false - }, - onDelete: 'cascade', - hooks: true - }) -} - -function afterDestroy (author: AuthorInstance) { - if (author.isOwned()) { - const removeVideoAuthorToFriendsParams = { - uuid: author.uuid - } - - return removeVideoAuthorToFriends(removeVideoAuthorToFriendsParams) - } - - return undefined -} - -toAddRemoteJSON = function (this: AuthorInstance) { - const json = { - uuid: this.uuid, - name: this.name - } - - return json -} - -isOwned = function (this: AuthorInstance) { - return this.podId === null -} - -// ------------------------------ STATICS ------------------------------ - -listOwned = function () { - const query: Sequelize.FindOptions = { - where: { - podId: null - } - } - - return Author.findAll(query) -} - -load = function (id: number) { - return Author.findById(id) -} - -loadByUUID = function (uuid: string) { - const query: Sequelize.FindOptions = { - where: { - uuid - } - } - - return Author.findOne(query) -} - -loadAuthorByPodAndUUID = function (uuid: string, podId: number, transaction: Sequelize.Transaction) { - const query: Sequelize.FindOptions = { - where: { - podId, - uuid - }, - transaction - } - - return Author.find(query) -} diff --git a/server/models/video/video-channel-interface.ts b/server/models/video/video-channel-interface.ts index b8d3e0f42..477f97cd4 100644 --- a/server/models/video/video-channel-interface.ts +++ b/server/models/video/video-channel-interface.ts @@ -1,42 +1,42 @@ import * as Sequelize from 'sequelize' import * as Promise from 'bluebird' -import { ResultList, RemoteVideoChannelCreateData, RemoteVideoChannelUpdateData } from '../../../shared' +import { ResultList } from '../../../shared' // Don't use barrel, import just what we need import { VideoChannel as FormattedVideoChannel } from '../../../shared/models/videos/video-channel.model' -import { AuthorInstance } from './author-interface' import { VideoInstance } from './video-interface' +import { AccountInstance } from '../account/account-interface' +import { VideoChannelObject } from '../../../shared/models/activitypub/objects/video-channel-object' export namespace VideoChannelMethods { export type ToFormattedJSON = (this: VideoChannelInstance) => FormattedVideoChannel - export type ToAddRemoteJSON = (this: VideoChannelInstance) => RemoteVideoChannelCreateData - export type ToUpdateRemoteJSON = (this: VideoChannelInstance) => RemoteVideoChannelUpdateData + export type ToActivityPubObject = (this: VideoChannelInstance) => VideoChannelObject export type IsOwned = (this: VideoChannelInstance) => boolean - export type CountByAuthor = (authorId: number) => Promise + export type CountByAccount = (accountId: number) => Promise export type ListOwned = () => Promise export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList > - export type LoadByIdAndAuthor = (id: number, authorId: number) => Promise - export type ListByAuthor = (authorId: number) => Promise< ResultList > - export type LoadAndPopulateAuthor = (id: number) => Promise - export type LoadByUUIDAndPopulateAuthor = (uuid: string) => Promise + 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, podHost: string, t?: Sequelize.Transaction) => Promise - export type LoadAndPopulateAuthorAndVideos = (id: number) => Promise + export type LoadAndPopulateAccountAndVideos = (id: number) => Promise } export interface VideoChannelClass { - countByAuthor: VideoChannelMethods.CountByAuthor + countByAccount: VideoChannelMethods.CountByAccount listForApi: VideoChannelMethods.ListForApi - listByAuthor: VideoChannelMethods.ListByAuthor + listByAccount: VideoChannelMethods.ListByAccount listOwned: VideoChannelMethods.ListOwned - loadByIdAndAuthor: VideoChannelMethods.LoadByIdAndAuthor + loadByIdAndAccount: VideoChannelMethods.LoadByIdAndAccount loadByUUID: VideoChannelMethods.LoadByUUID loadByHostAndUUID: VideoChannelMethods.LoadByHostAndUUID - loadAndPopulateAuthor: VideoChannelMethods.LoadAndPopulateAuthor - loadByUUIDAndPopulateAuthor: VideoChannelMethods.LoadByUUIDAndPopulateAuthor - loadAndPopulateAuthorAndVideos: VideoChannelMethods.LoadAndPopulateAuthorAndVideos + loadAndPopulateAccount: VideoChannelMethods.LoadAndPopulateAccount + loadByUUIDAndPopulateAccount: VideoChannelMethods.LoadByUUIDAndPopulateAccount + loadAndPopulateAccountAndVideos: VideoChannelMethods.LoadAndPopulateAccountAndVideos } export interface VideoChannelAttributes { @@ -45,8 +45,9 @@ export interface VideoChannelAttributes { name: string description: string remote: boolean + url: string - Author?: AuthorInstance + Account?: AccountInstance Videos?: VideoInstance[] } @@ -57,8 +58,7 @@ export interface VideoChannelInstance extends VideoChannelClass, VideoChannelAtt isOwned: VideoChannelMethods.IsOwned toFormattedJSON: VideoChannelMethods.ToFormattedJSON - toAddRemoteJSON: VideoChannelMethods.ToAddRemoteJSON - toUpdateRemoteJSON: VideoChannelMethods.ToUpdateRemoteJSON + toActivityPubObject: VideoChannelMethods.ToActivityPubObject } export interface VideoChannelModel extends VideoChannelClass, Sequelize.Model {} diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 46c2db63f..c17828f3e 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts @@ -13,19 +13,18 @@ import { let VideoChannel: Sequelize.Model let toFormattedJSON: VideoChannelMethods.ToFormattedJSON -let toAddRemoteJSON: VideoChannelMethods.ToAddRemoteJSON -let toUpdateRemoteJSON: VideoChannelMethods.ToUpdateRemoteJSON +let toActivityPubObject: VideoChannelMethods.ToActivityPubObject let isOwned: VideoChannelMethods.IsOwned -let countByAuthor: VideoChannelMethods.CountByAuthor +let countByAccount: VideoChannelMethods.CountByAccount let listOwned: VideoChannelMethods.ListOwned let listForApi: VideoChannelMethods.ListForApi -let listByAuthor: VideoChannelMethods.ListByAuthor -let loadByIdAndAuthor: VideoChannelMethods.LoadByIdAndAuthor +let listByAccount: VideoChannelMethods.ListByAccount +let loadByIdAndAccount: VideoChannelMethods.LoadByIdAndAccount let loadByUUID: VideoChannelMethods.LoadByUUID -let loadAndPopulateAuthor: VideoChannelMethods.LoadAndPopulateAuthor -let loadByUUIDAndPopulateAuthor: VideoChannelMethods.LoadByUUIDAndPopulateAuthor +let loadAndPopulateAccount: VideoChannelMethods.LoadAndPopulateAccount +let loadByUUIDAndPopulateAccount: VideoChannelMethods.LoadByUUIDAndPopulateAccount let loadByHostAndUUID: VideoChannelMethods.LoadByHostAndUUID -let loadAndPopulateAuthorAndVideos: VideoChannelMethods.LoadAndPopulateAuthorAndVideos +let loadAndPopulateAccountAndVideos: VideoChannelMethods.LoadAndPopulateAccountAndVideos export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { VideoChannel = sequelize.define('VideoChannel', @@ -62,12 +61,19 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false + }, + url: { + type: DataTypes.STRING, + allowNull: false, + validate: { + isUrl: true + } } }, { indexes: [ { - fields: [ 'authorId' ] + fields: [ 'accountId' ] } ], hooks: { @@ -80,21 +86,20 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da associate, listForApi, - listByAuthor, + listByAccount, listOwned, - loadByIdAndAuthor, - loadAndPopulateAuthor, - loadByUUIDAndPopulateAuthor, + loadByIdAndAccount, + loadAndPopulateAccount, + loadByUUIDAndPopulateAccount, loadByUUID, loadByHostAndUUID, - loadAndPopulateAuthorAndVideos, - countByAuthor + loadAndPopulateAccountAndVideos, + countByAccount ] const instanceMethods = [ isOwned, toFormattedJSON, - toAddRemoteJSON, - toUpdateRemoteJSON + toActivityPubObject, ] addMethodsToModel(VideoChannel, classMethods, instanceMethods) @@ -118,10 +123,10 @@ toFormattedJSON = function (this: VideoChannelInstance) { updatedAt: this.updatedAt } - if (this.Author !== undefined) { + if (this.Account !== undefined) { json['owner'] = { - name: this.Author.name, - uuid: this.Author.uuid + name: this.Account.name, + uuid: this.Account.uuid } } @@ -132,27 +137,14 @@ toFormattedJSON = function (this: VideoChannelInstance) { return json } -toAddRemoteJSON = function (this: VideoChannelInstance) { - const json = { - uuid: this.uuid, - name: this.name, - description: this.description, - createdAt: this.createdAt, - updatedAt: this.updatedAt, - ownerUUID: this.Author.uuid - } - - return json -} - -toUpdateRemoteJSON = function (this: VideoChannelInstance) { +toActivityPubObject = function (this: VideoChannelInstance) { const json = { uuid: this.uuid, name: this.name, description: this.description, createdAt: this.createdAt, updatedAt: this.updatedAt, - ownerUUID: this.Author.uuid + ownerUUID: this.Account.uuid } return json @@ -161,9 +153,9 @@ toUpdateRemoteJSON = function (this: VideoChannelInstance) { // ------------------------------ STATICS ------------------------------ function associate (models) { - VideoChannel.belongsTo(models.Author, { + VideoChannel.belongsTo(models.Account, { foreignKey: { - name: 'authorId', + name: 'accountId', allowNull: false }, onDelete: 'CASCADE' @@ -190,10 +182,10 @@ function afterDestroy (videoChannel: VideoChannelInstance) { return undefined } -countByAuthor = function (authorId: number) { +countByAccount = function (accountId: number) { const query = { where: { - authorId + accountId } } @@ -205,7 +197,7 @@ listOwned = function () { where: { remote: false }, - include: [ VideoChannel['sequelize'].models.Author ] + include: [ VideoChannel['sequelize'].models.Account ] } return VideoChannel.findAll(query) @@ -218,7 +210,7 @@ listForApi = function (start: number, count: number, sort: string) { order: [ getSort(sort) ], include: [ { - model: VideoChannel['sequelize'].models.Author, + model: VideoChannel['sequelize'].models.Account, required: true, include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ] } @@ -230,14 +222,14 @@ listForApi = function (start: number, count: number, sort: string) { }) } -listByAuthor = function (authorId: number) { +listByAccount = function (accountId: number) { const query = { order: [ getSort('createdAt') ], include: [ { - model: VideoChannel['sequelize'].models.Author, + model: VideoChannel['sequelize'].models.Account, where: { - id: authorId + id: accountId }, required: true, include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ] @@ -269,7 +261,7 @@ loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Tran }, include: [ { - model: VideoChannel['sequelize'].models.Author, + model: VideoChannel['sequelize'].models.Account, include: [ { model: VideoChannel['sequelize'].models.Pod, @@ -288,15 +280,15 @@ loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Tran return VideoChannel.findOne(query) } -loadByIdAndAuthor = function (id: number, authorId: number) { +loadByIdAndAccount = function (id: number, accountId: number) { const options = { where: { id, - authorId + accountId }, include: [ { - model: VideoChannel['sequelize'].models.Author, + model: VideoChannel['sequelize'].models.Account, include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ] } ] @@ -305,11 +297,11 @@ loadByIdAndAuthor = function (id: number, authorId: number) { return VideoChannel.findOne(options) } -loadAndPopulateAuthor = function (id: number) { +loadAndPopulateAccount = function (id: number) { const options = { include: [ { - model: VideoChannel['sequelize'].models.Author, + model: VideoChannel['sequelize'].models.Account, include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ] } ] @@ -318,14 +310,14 @@ loadAndPopulateAuthor = function (id: number) { return VideoChannel.findById(id, options) } -loadByUUIDAndPopulateAuthor = function (uuid: string) { +loadByUUIDAndPopulateAccount = function (uuid: string) { const options = { where: { uuid }, include: [ { - model: VideoChannel['sequelize'].models.Author, + model: VideoChannel['sequelize'].models.Account, include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ] } ] @@ -334,11 +326,11 @@ loadByUUIDAndPopulateAuthor = function (uuid: string) { return VideoChannel.findOne(options) } -loadAndPopulateAuthorAndVideos = function (id: number) { +loadAndPopulateAccountAndVideos = function (id: number) { const options = { include: [ { - model: VideoChannel['sequelize'].models.Author, + model: VideoChannel['sequelize'].models.Account, include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ] }, VideoChannel['sequelize'].models.Video diff --git a/server/models/video/video-interface.ts b/server/models/video/video-interface.ts index cfe65f9aa..e62e25a82 100644 --- a/server/models/video/video-interface.ts +++ b/server/models/video/video-interface.ts @@ -1,5 +1,5 @@ import * as Sequelize from 'sequelize' -import * as Promise from 'bluebird' +import * as Bluebird from 'bluebird' import { TagAttributes, TagInstance } from './tag-interface' import { VideoFileAttributes, VideoFileInstance } from './video-file-interface' @@ -13,6 +13,7 @@ import { RemoteVideoUpdateData } from '../../../shared/models/pods/remote-video/ import { RemoteVideoCreateData } from '../../../shared/models/pods/remote-video/remote-video-create-request.model' import { ResultList } from '../../../shared/models/result-list.model' import { VideoChannelInstance } from './video-channel-interface' +import { VideoTorrentObject } from '../../../shared/models/activitypub/objects/video-torrent-object' export namespace VideoMethods { export type GetThumbnailName = (this: VideoInstance) => string @@ -29,8 +30,7 @@ export namespace VideoMethods { export type GetVideoFilePath = (this: VideoInstance, videoFile: VideoFileInstance) => string export type CreateTorrentAndSetInfoHash = (this: VideoInstance, videoFile: VideoFileInstance) => Promise - export type ToAddRemoteJSON = (this: VideoInstance) => Promise - export type ToUpdateRemoteJSON = (this: VideoInstance) => RemoteVideoUpdateData + export type ToActivityPubObject = (this: VideoInstance) => VideoTorrentObject export type OptimizeOriginalVideofile = (this: VideoInstance) => Promise export type TranscodeOriginalVideofile = (this: VideoInstance, resolution: number) => Promise @@ -40,31 +40,35 @@ export namespace VideoMethods { 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 // Return thumbnail name export type GenerateThumbnailFromData = (video: VideoInstance, thumbnailData: string) => Promise - export type List = () => Promise - export type ListOwnedAndPopulateAuthorAndTags = () => Promise - export type ListOwnedByAuthor = (author: string) => Promise + export type List = () => Bluebird + export type ListOwnedAndPopulateAccountAndTags = () => Bluebird + export type ListOwnedByAccount = (account: string) => Bluebird - export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList > - export type ListUserVideosForApi = (userId: number, start: number, count: number, sort: string) => Promise< ResultList > - export type SearchAndPopulateAuthorAndPodAndTags = ( + 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 SearchAndPopulateAccountAndPodAndTags = ( value: string, field: string, start: number, count: number, sort: string - ) => Promise< ResultList > + ) => Bluebird< ResultList > - export type Load = (id: number) => Promise - export type LoadByUUID = (uuid: string, t?: Sequelize.Transaction) => Promise - export type LoadLocalVideoByUUID = (uuid: string, t?: Sequelize.Transaction) => Promise - export type LoadByHostAndUUID = (fromHost: string, uuid: string, t?: Sequelize.Transaction) => Promise - export type LoadAndPopulateAuthor = (id: number) => Promise - export type LoadAndPopulateAuthorAndPodAndTags = (id: number) => Promise - export type LoadByUUIDAndPopulateAuthorAndPodAndTags = (uuid: string) => Promise + export type Load = (id: number) => Bluebird + export type LoadByUUID = (uuid: string, t?: Sequelize.Transaction) => Bluebird + export type LoadByUrl = (url: string, t?: Sequelize.Transaction) => Bluebird + export type LoadLocalVideoByUUID = (uuid: string, t?: Sequelize.Transaction) => Bluebird + export type LoadByHostAndUUID = (fromHost: string, uuid: string, t?: Sequelize.Transaction) => Bluebird + export type LoadAndPopulateAccount = (id: number) => Bluebird + export type LoadAndPopulateAccountAndPodAndTags = (id: number) => Bluebird + export type LoadByUUIDAndPopulateAccountAndPodAndTags = (uuid: string) => Bluebird export type RemoveThumbnail = (this: VideoInstance) => Promise export type RemovePreview = (this: VideoInstance) => Promise @@ -77,16 +81,17 @@ export interface VideoClass { list: VideoMethods.List listForApi: VideoMethods.ListForApi listUserVideosForApi: VideoMethods.ListUserVideosForApi - listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags - listOwnedByAuthor: VideoMethods.ListOwnedByAuthor + listOwnedAndPopulateAccountAndTags: VideoMethods.ListOwnedAndPopulateAccountAndTags + listOwnedByAccount: VideoMethods.ListOwnedByAccount load: VideoMethods.Load - loadAndPopulateAuthor: VideoMethods.LoadAndPopulateAuthor - loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags + loadAndPopulateAccount: VideoMethods.LoadAndPopulateAccount + loadAndPopulateAccountAndPodAndTags: VideoMethods.LoadAndPopulateAccountAndPodAndTags loadByHostAndUUID: VideoMethods.LoadByHostAndUUID loadByUUID: VideoMethods.LoadByUUID + loadByUrl: VideoMethods.LoadByUrl loadLocalVideoByUUID: VideoMethods.LoadLocalVideoByUUID - loadByUUIDAndPopulateAuthorAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAuthorAndPodAndTags - searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags + loadByUUIDAndPopulateAccountAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAccountAndPodAndTags + searchAndPopulateAccountAndPodAndTags: VideoMethods.SearchAndPopulateAccountAndPodAndTags } export interface VideoAttributes { @@ -104,7 +109,9 @@ export interface VideoAttributes { likes?: number dislikes?: number remote: boolean + url: string + parentId?: number channelId?: number VideoChannel?: VideoChannelInstance @@ -132,16 +139,18 @@ export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.In removePreview: VideoMethods.RemovePreview removeThumbnail: VideoMethods.RemoveThumbnail removeTorrent: VideoMethods.RemoveTorrent - toAddRemoteJSON: VideoMethods.ToAddRemoteJSON + toActivityPubObject: VideoMethods.ToActivityPubObject toFormattedJSON: VideoMethods.ToFormattedJSON toFormattedDetailsJSON: VideoMethods.ToFormattedDetailsJSON - toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON 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 @@ -149,3 +158,4 @@ export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.In } export interface VideoModel extends VideoClass, Sequelize.Model {} + diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 02dde1726..94af1ece5 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -5,7 +5,6 @@ import { map, maxBy, truncate } from 'lodash' import * as parseTorrent from 'parse-torrent' import { join } from 'path' import * as Sequelize from 'sequelize' -import * as Promise from 'bluebird' import { TagInstance } from './tag-interface' import { @@ -52,6 +51,7 @@ import { VideoMethods } from './video-interface' +import { VideoTorrentObject } from '../../../shared/models/activitypub/objects/video-torrent-object' let Video: Sequelize.Model let getOriginalFile: VideoMethods.GetOriginalFile @@ -64,8 +64,7 @@ let getTorrentFileName: VideoMethods.GetTorrentFileName let isOwned: VideoMethods.IsOwned let toFormattedJSON: VideoMethods.ToFormattedJSON let toFormattedDetailsJSON: VideoMethods.ToFormattedDetailsJSON -let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON -let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON +let toActivityPubObject: VideoMethods.ToActivityPubObject let optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile let transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile let createPreview: VideoMethods.CreatePreview @@ -76,21 +75,25 @@ 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 generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData let list: VideoMethods.List let listForApi: VideoMethods.ListForApi let listUserVideosForApi: VideoMethods.ListUserVideosForApi let loadByHostAndUUID: VideoMethods.LoadByHostAndUUID -let listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags -let listOwnedByAuthor: VideoMethods.ListOwnedByAuthor +let listOwnedAndPopulateAccountAndTags: VideoMethods.ListOwnedAndPopulateAccountAndTags +let listOwnedByAccount: VideoMethods.ListOwnedByAccount let load: VideoMethods.Load let loadByUUID: VideoMethods.LoadByUUID +let loadByUrl: VideoMethods.LoadByUrl let loadLocalVideoByUUID: VideoMethods.LoadLocalVideoByUUID -let loadAndPopulateAuthor: VideoMethods.LoadAndPopulateAuthor -let loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags -let loadByUUIDAndPopulateAuthorAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAuthorAndPodAndTags -let searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags +let loadAndPopulateAccount: VideoMethods.LoadAndPopulateAccount +let loadAndPopulateAccountAndPodAndTags: VideoMethods.LoadAndPopulateAccountAndPodAndTags +let loadByUUIDAndPopulateAccountAndPodAndTags: VideoMethods.LoadByUUIDAndPopulateAccountAndPodAndTags +let searchAndPopulateAccountAndPodAndTags: VideoMethods.SearchAndPopulateAccountAndPodAndTags let removeThumbnail: VideoMethods.RemoveThumbnail let removePreview: VideoMethods.RemovePreview let removeFile: VideoMethods.RemoveFile @@ -219,6 +222,13 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false + }, + url: { + type: DataTypes.STRING, + allowNull: false, + validate: { + isUrl: true + } } }, { @@ -243,6 +253,9 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da }, { fields: [ 'channelId' ] + }, + { + fields: [ 'parentId' ] } ], hooks: { @@ -258,16 +271,16 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da list, listForApi, listUserVideosForApi, - listOwnedAndPopulateAuthorAndTags, - listOwnedByAuthor, + listOwnedAndPopulateAccountAndTags, + listOwnedByAccount, load, - loadAndPopulateAuthor, - loadAndPopulateAuthorAndPodAndTags, + loadAndPopulateAccount, + loadAndPopulateAccountAndPodAndTags, loadByHostAndUUID, loadByUUID, loadLocalVideoByUUID, - loadByUUIDAndPopulateAuthorAndPodAndTags, - searchAndPopulateAuthorAndPodAndTags + loadByUUIDAndPopulateAccountAndPodAndTags, + searchAndPopulateAccountAndPodAndTags ] const instanceMethods = [ createPreview, @@ -286,16 +299,18 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da removePreview, removeThumbnail, removeTorrent, - toAddRemoteJSON, + toActivityPubObject, toFormattedJSON, toFormattedDetailsJSON, - toUpdateRemoteJSON, optimizeOriginalVideofile, transcodeOriginalVideofile, getOriginalFileHeight, getEmbedPath, getTruncatedDescription, - getDescriptionPath + getDescriptionPath, + getCategoryLabel, + getLicenceLabel, + getLanguageLabel ] addMethodsToModel(Video, classMethods, instanceMethods) @@ -313,6 +328,14 @@ function associate (models) { onDelete: 'cascade' }) + Video.belongsTo(models.VideoChannel, { + foreignKey: { + name: 'parentId', + allowNull: true + }, + onDelete: 'cascade' + }) + Video.belongsToMany(models.Tag, { foreignKey: 'videoId', through: models.VideoTag, @@ -423,7 +446,7 @@ getVideoFilePath = function (this: VideoInstance, videoFile: VideoFileInstance) return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile)) } -createTorrentAndSetInfoHash = function (this: VideoInstance, videoFile: VideoFileInstance) { +createTorrentAndSetInfoHash = async function (this: VideoInstance, videoFile: VideoFileInstance) { const options = { announceList: [ [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ] @@ -433,18 +456,15 @@ createTorrentAndSetInfoHash = function (this: VideoInstance, videoFile: VideoFil ] } - return createTorrentPromise(this.getVideoFilePath(videoFile), options) - .then(torrent => { - const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile)) - logger.info('Creating torrent %s.', filePath) + const torrent = await createTorrentPromise(this.getVideoFilePath(videoFile), options) - return writeFilePromise(filePath, torrent).then(() => torrent) - }) - .then(torrent => { - const parsedTorrent = parseTorrent(torrent) + const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile)) + logger.info('Creating torrent %s.', filePath) - videoFile.infoHash = parsedTorrent.infoHash - }) + await writeFilePromise(filePath, torrent) + + const parsedTorrent = parseTorrent(torrent) + videoFile.infoHash = parsedTorrent.infoHash } getEmbedPath = function (this: VideoInstance) { @@ -462,40 +482,28 @@ getPreviewPath = function (this: VideoInstance) { toFormattedJSON = function (this: VideoInstance) { let podHost - if (this.VideoChannel.Author.Pod) { - podHost = this.VideoChannel.Author.Pod.host + if (this.VideoChannel.Account.Pod) { + podHost = this.VideoChannel.Account.Pod.host } else { // It means it's our video podHost = CONFIG.WEBSERVER.HOST } - // Maybe our pod is not up to date and there are new categories since our version - let categoryLabel = VIDEO_CATEGORIES[this.category] - if (!categoryLabel) categoryLabel = 'Misc' - - // Maybe our pod is not up to date and there are new licences since our version - let licenceLabel = VIDEO_LICENCES[this.licence] - if (!licenceLabel) licenceLabel = 'Unknown' - - // Language is an optional attribute - let languageLabel = VIDEO_LANGUAGES[this.language] - if (!languageLabel) languageLabel = 'Unknown' - const json = { id: this.id, uuid: this.uuid, name: this.name, category: this.category, - categoryLabel, + categoryLabel: this.getCategoryLabel(), licence: this.licence, - licenceLabel, + licenceLabel: this.getLicenceLabel(), language: this.language, - languageLabel, + languageLabel: this.getLanguageLabel(), nsfw: this.nsfw, description: this.getTruncatedDescription(), podHost, isLocal: this.isOwned(), - author: this.VideoChannel.Author.name, + account: this.VideoChannel.Account.name, duration: this.duration, views: this.views, likes: this.likes, @@ -552,75 +560,75 @@ toFormattedDetailsJSON = function (this: VideoInstance) { return Object.assign(formattedJson, detailsJson) } -toAddRemoteJSON = function (this: VideoInstance) { - // Get thumbnail data to send to the other pod - const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName()) +toActivityPubObject = function (this: VideoInstance) { + const { baseUrlHttp, baseUrlWs } = getBaseUrls(this) - return readFileBufferPromise(thumbnailPath).then(thumbnailData => { - const remoteVideo = { - uuid: this.uuid, - name: this.name, - category: this.category, - licence: this.licence, - language: this.language, - nsfw: this.nsfw, - truncatedDescription: this.getTruncatedDescription(), - channelUUID: this.VideoChannel.uuid, - duration: this.duration, - thumbnailData: thumbnailData.toString('binary'), - tags: map(this.Tags, 'name'), - createdAt: this.createdAt, - updatedAt: this.updatedAt, - views: this.views, - likes: this.likes, - dislikes: this.dislikes, - privacy: this.privacy, - files: [] - } + const tag = this.Tags.map(t => ({ + type: 'Hashtag', + name: t.name + })) + + const url = [] + for (const file of this.VideoFiles) { + url.push({ + type: 'Link', + mimeType: 'video/' + file.extname, + url: getVideoFileUrl(this, file, baseUrlHttp), + width: file.resolution, + size: file.size + }) - this.VideoFiles.forEach(videoFile => { - remoteVideo.files.push({ - infoHash: videoFile.infoHash, - resolution: videoFile.resolution, - extname: videoFile.extname, - size: videoFile.size - }) + url.push({ + type: 'Link', + mimeType: 'application/x-bittorrent', + url: getTorrentUrl(this, file, baseUrlHttp), + width: file.resolution }) - return remoteVideo - }) -} + url.push({ + type: 'Link', + mimeType: 'application/x-bittorrent;x-scheme-handler/magnet', + url: generateMagnetUri(this, file, baseUrlHttp, baseUrlWs), + width: file.resolution + }) + } -toUpdateRemoteJSON = function (this: VideoInstance) { - const json = { - uuid: this.uuid, + const videoObject: VideoTorrentObject = { + type: 'Video', name: this.name, - category: this.category, - licence: this.licence, - language: this.language, - nsfw: this.nsfw, - truncatedDescription: this.getTruncatedDescription(), - duration: this.duration, - tags: map(this.Tags, 'name'), - createdAt: this.createdAt, - updatedAt: this.updatedAt, + // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration + duration: 'PT' + this.duration + 'S', + uuid: this.uuid, + tag, + category: { + id: this.category, + label: this.getCategoryLabel() + }, + licence: { + id: this.licence, + name: this.getLicenceLabel() + }, + language: { + id: this.language, + name: this.getLanguageLabel() + }, views: this.views, - likes: this.likes, - dislikes: this.dislikes, - privacy: this.privacy, - files: [] + nsfw: this.nsfw, + published: this.createdAt, + updated: this.updatedAt, + 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 } - this.VideoFiles.forEach(videoFile => { - json.files.push({ - infoHash: videoFile.infoHash, - resolution: videoFile.resolution, - extname: videoFile.extname, - size: videoFile.size - }) - }) - - return json + return videoObject } getTruncatedDescription = function (this: VideoInstance) { @@ -631,7 +639,7 @@ getTruncatedDescription = function (this: VideoInstance) { return truncate(this.description, options) } -optimizeOriginalVideofile = function (this: VideoInstance) { +optimizeOriginalVideofile = async function (this: VideoInstance) { const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR const newExtname = '.mp4' const inputVideoFile = this.getOriginalFile() @@ -643,40 +651,32 @@ optimizeOriginalVideofile = function (this: VideoInstance) { outputPath: videoOutputPath } - return transcode(transcodeOptions) - .then(() => { - return unlinkPromise(videoInputPath) - }) - .then(() => { - // Important to do this before getVideoFilename() to take in account the new file extension - inputVideoFile.set('extname', newExtname) + try { + // Could be very long! + await transcode(transcodeOptions) - return renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile)) - }) - .then(() => { - return statPromise(this.getVideoFilePath(inputVideoFile)) - }) - .then(stats => { - return inputVideoFile.set('size', stats.size) - }) - .then(() => { - return this.createTorrentAndSetInfoHash(inputVideoFile) - }) - .then(() => { - return inputVideoFile.save() - }) - .then(() => { - return undefined - }) - .catch(err => { - // Auto destruction... - this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err)) + await unlinkPromise(videoInputPath) - throw err - }) + // Important to do this before getVideoFilename() to take in account the new file extension + inputVideoFile.set('extname', newExtname) + + await renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile)) + const stats = await statPromise(this.getVideoFilePath(inputVideoFile)) + + inputVideoFile.set('size', stats.size) + + await this.createTorrentAndSetInfoHash(inputVideoFile) + await inputVideoFile.save() + + } catch (err) { + // Auto destruction... + this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err)) + + throw err + } } -transcodeOriginalVideofile = function (this: VideoInstance, resolution: VideoResolution) { +transcodeOriginalVideofile = async function (this: VideoInstance, resolution: VideoResolution) { const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR const extname = '.mp4' @@ -696,25 +696,18 @@ transcodeOriginalVideofile = function (this: VideoInstance, resolution: VideoRes outputPath: videoOutputPath, resolution } - return transcode(transcodeOptions) - .then(() => { - return statPromise(videoOutputPath) - }) - .then(stats => { - newVideoFile.set('size', stats.size) - return undefined - }) - .then(() => { - return this.createTorrentAndSetInfoHash(newVideoFile) - }) - .then(() => { - return newVideoFile.save() - }) - .then(() => { - return this.VideoFiles.push(newVideoFile) - }) - .then(() => undefined) + await transcode(transcodeOptions) + + const stats = await statPromise(videoOutputPath) + + newVideoFile.set('size', stats.size) + + await this.createTorrentAndSetInfoHash(newVideoFile) + + await newVideoFile.save() + + this.VideoFiles.push(newVideoFile) } getOriginalFileHeight = function (this: VideoInstance) { @@ -727,6 +720,31 @@ getDescriptionPath = function (this: VideoInstance) { return `/api/${API_VERSION}/videos/${this.uuid}/description` } +getCategoryLabel = function (this: VideoInstance) { + let categoryLabel = VIDEO_CATEGORIES[this.category] + + // Maybe our pod is not up to date and there are new categories since our version + if (!categoryLabel) categoryLabel = 'Misc' + + return categoryLabel +} + +getLicenceLabel = function (this: VideoInstance) { + let licenceLabel = VIDEO_LICENCES[this.licence] + // Maybe our pod is not up to date and there are new licences since our version + if (!licenceLabel) licenceLabel = 'Unknown' + + return licenceLabel +} + +getLanguageLabel = function (this: VideoInstance) { + // Language is an optional attribute + let languageLabel = VIDEO_LANGUAGES[this.language] + if (!languageLabel) languageLabel = 'Unknown' + + return languageLabel +} + removeThumbnail = function (this: VideoInstance) { const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName()) return unlinkPromise(thumbnailPath) @@ -779,7 +797,7 @@ listUserVideosForApi = function (userId: number, start: number, count: number, s required: true, include: [ { - model: Video['sequelize'].models.Author, + model: Video['sequelize'].models.Account, where: { userId }, @@ -810,7 +828,7 @@ listForApi = function (start: number, count: number, sort: string) { model: Video['sequelize'].models.VideoChannel, include: [ { - model: Video['sequelize'].models.Author, + model: Video['sequelize'].models.Account, include: [ { model: Video['sequelize'].models.Pod, @@ -846,7 +864,7 @@ loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Tran model: Video['sequelize'].models.VideoChannel, include: [ { - model: Video['sequelize'].models.Author, + model: Video['sequelize'].models.Account, include: [ { model: Video['sequelize'].models.Pod, @@ -867,7 +885,7 @@ loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Tran return Video.findOne(query) } -listOwnedAndPopulateAuthorAndTags = function () { +listOwnedAndPopulateAccountAndTags = function () { const query = { where: { remote: false @@ -876,7 +894,7 @@ listOwnedAndPopulateAuthorAndTags = function () { Video['sequelize'].models.VideoFile, { model: Video['sequelize'].models.VideoChannel, - include: [ Video['sequelize'].models.Author ] + include: [ Video['sequelize'].models.Account ] }, Video['sequelize'].models.Tag ] @@ -885,7 +903,7 @@ listOwnedAndPopulateAuthorAndTags = function () { return Video.findAll(query) } -listOwnedByAuthor = function (author: string) { +listOwnedByAccount = function (account: string) { const query = { where: { remote: false @@ -898,9 +916,9 @@ listOwnedByAuthor = function (author: string) { model: Video['sequelize'].models.VideoChannel, include: [ { - model: Video['sequelize'].models.Author, + model: Video['sequelize'].models.Account, where: { - name: author + name: account } } ] @@ -942,13 +960,13 @@ loadLocalVideoByUUID = function (uuid: string, t?: Sequelize.Transaction) { return Video.findOne(query) } -loadAndPopulateAuthor = function (id: number) { +loadAndPopulateAccount = function (id: number) { const options = { include: [ Video['sequelize'].models.VideoFile, { model: Video['sequelize'].models.VideoChannel, - include: [ Video['sequelize'].models.Author ] + include: [ Video['sequelize'].models.Account ] } ] } @@ -956,14 +974,14 @@ loadAndPopulateAuthor = function (id: number) { return Video.findById(id, options) } -loadAndPopulateAuthorAndPodAndTags = function (id: number) { +loadAndPopulateAccountAndPodAndTags = function (id: number) { const options = { include: [ { model: Video['sequelize'].models.VideoChannel, include: [ { - model: Video['sequelize'].models.Author, + model: Video['sequelize'].models.Account, include: [ { model: Video['sequelize'].models.Pod, required: false } ] } ] @@ -976,7 +994,7 @@ loadAndPopulateAuthorAndPodAndTags = function (id: number) { return Video.findById(id, options) } -loadByUUIDAndPopulateAuthorAndPodAndTags = function (uuid: string) { +loadByUUIDAndPopulateAccountAndPodAndTags = function (uuid: string) { const options = { where: { uuid @@ -986,7 +1004,7 @@ loadByUUIDAndPopulateAuthorAndPodAndTags = function (uuid: string) { model: Video['sequelize'].models.VideoChannel, include: [ { - model: Video['sequelize'].models.Author, + model: Video['sequelize'].models.Account, include: [ { model: Video['sequelize'].models.Pod, required: false } ] } ] @@ -999,20 +1017,20 @@ loadByUUIDAndPopulateAuthorAndPodAndTags = function (uuid: string) { return Video.findOne(options) } -searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, start: number, count: number, sort: string) { +searchAndPopulateAccountAndPodAndTags = function (value: string, field: string, start: number, count: number, sort: string) { const podInclude: Sequelize.IncludeOptions = { model: Video['sequelize'].models.Pod, required: false } - const authorInclude: Sequelize.IncludeOptions = { - model: Video['sequelize'].models.Author, + const accountInclude: Sequelize.IncludeOptions = { + model: Video['sequelize'].models.Account, include: [ podInclude ] } const videoChannelInclude: Sequelize.IncludeOptions = { model: Video['sequelize'].models.VideoChannel, - include: [ authorInclude ], + include: [ accountInclude ], required: true } @@ -1045,8 +1063,8 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s } } podInclude.required = true - } else if (field === 'author') { - authorInclude.where = { + } else if (field === 'account') { + accountInclude.where = { name: { [Sequelize.Op.iLike]: '%' + value + '%' } @@ -1090,13 +1108,17 @@ function getBaseUrls (video: VideoInstance) { baseUrlHttp = CONFIG.WEBSERVER.URL baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT } else { - baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + video.VideoChannel.Author.Pod.host - baseUrlWs = REMOTE_SCHEME.WS + '://' + video.VideoChannel.Author.Pod.host + baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + video.VideoChannel.Account.Pod.host + baseUrlWs = REMOTE_SCHEME.WS + '://' + video.VideoChannel.Account.Pod.host } return { baseUrlHttp, baseUrlWs } } +function getThumbnailUrl (video: VideoInstance, baseUrlHttp: string) { + return baseUrlHttp + STATIC_PATHS.THUMBNAILS + video.getThumbnailName() +} + function getTorrentUrl (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string) { return baseUrlHttp + STATIC_PATHS.TORRENTS + video.getTorrentFileName(videoFile) } -- cgit v1.2.3