From 74889a71fe687dda74f2a687653122327807af36 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 16 Jun 2017 09:45:46 +0200 Subject: Reorganize model files --- server/initializers/database.ts | 68 +- server/models/application-interface.ts | 26 - server/models/application.ts | 57 -- server/models/application/application-interface.ts | 26 + server/models/application/application.ts | 57 ++ server/models/application/index.ts | 1 + server/models/author-interface.ts | 27 - server/models/author.ts | 103 --- server/models/index.ts | 24 +- server/models/job-interface.ts | 24 - server/models/job.ts | 63 -- server/models/job/index.ts | 1 + server/models/job/job-interface.ts | 24 + server/models/job/job.ts | 63 ++ server/models/oauth-client-interface.ts | 32 - server/models/oauth-client.ts | 87 -- server/models/oauth-token-interface.ts | 48 -- server/models/oauth-token.ts | 160 ---- server/models/oauth/index.ts | 2 + server/models/oauth/oauth-client-interface.ts | 32 + server/models/oauth/oauth-client.ts | 87 ++ server/models/oauth/oauth-token-interface.ts | 48 ++ server/models/oauth/oauth-token.ts | 160 ++++ server/models/pod-interface.ts | 67 -- server/models/pod.ts | 274 ------ server/models/pod/index.ts | 1 + server/models/pod/pod-interface.ts | 67 ++ server/models/pod/pod.ts | 274 ++++++ server/models/request-interface.ts | 47 -- server/models/request-to-pod-interface.ts | 21 - server/models/request-to-pod.ts | 54 -- server/models/request-video-event-interface.ts | 48 -- server/models/request-video-event.ts | 185 ----- server/models/request-video-qadu-interface.ts | 46 - server/models/request-video-qadu.ts | 164 ---- server/models/request.ts | 150 ---- server/models/request/index.ts | 4 + server/models/request/request-interface.ts | 47 ++ server/models/request/request-to-pod-interface.ts | 21 + server/models/request/request-to-pod.ts | 54 ++ .../request/request-video-event-interface.ts | 48 ++ server/models/request/request-video-event.ts | 185 +++++ .../models/request/request-video-qadu-interface.ts | 46 + server/models/request/request-video-qadu.ts | 164 ++++ server/models/request/request.ts | 150 ++++ server/models/tag-interface.ts | 20 - server/models/tag.ts | 81 -- server/models/user-interface.ts | 63 -- server/models/user-video-rate-interface.ts | 22 - server/models/user-video-rate.ts | 80 -- server/models/user.ts | 221 ----- server/models/user/index.ts | 2 + server/models/user/user-interface.ts | 63 ++ server/models/user/user-video-rate-interface.ts | 22 + server/models/user/user-video-rate.ts | 80 ++ server/models/user/user.ts | 221 +++++ server/models/video-abuse-interface.ts | 28 - server/models/video-abuse.ts | 131 --- server/models/video-blacklist-interface.ts | 43 - server/models/video-blacklist.ts | 103 --- server/models/video-interface.ts | 151 ---- server/models/video-tag-interface.ts | 18 - server/models/video-tag.ts | 27 - server/models/video.ts | 921 --------------------- server/models/video/author-interface.ts | 27 + server/models/video/author.ts | 103 +++ server/models/video/index.ts | 6 + server/models/video/tag-interface.ts | 20 + server/models/video/tag.ts | 81 ++ server/models/video/video-abuse-interface.ts | 28 + server/models/video/video-abuse.ts | 131 +++ server/models/video/video-blacklist-interface.ts | 43 + server/models/video/video-blacklist.ts | 103 +++ server/models/video/video-interface.ts | 151 ++++ server/models/video/video-tag-interface.ts | 18 + server/models/video/video-tag.ts | 27 + server/models/video/video.ts | 921 +++++++++++++++++++++ 77 files changed, 3669 insertions(+), 3624 deletions(-) delete mode 100644 server/models/application-interface.ts delete mode 100644 server/models/application.ts create mode 100644 server/models/application/application-interface.ts create mode 100644 server/models/application/application.ts create mode 100644 server/models/application/index.ts delete mode 100644 server/models/author-interface.ts delete mode 100644 server/models/author.ts delete mode 100644 server/models/job-interface.ts delete mode 100644 server/models/job.ts create mode 100644 server/models/job/index.ts create mode 100644 server/models/job/job-interface.ts create mode 100644 server/models/job/job.ts delete mode 100644 server/models/oauth-client-interface.ts delete mode 100644 server/models/oauth-client.ts delete mode 100644 server/models/oauth-token-interface.ts delete mode 100644 server/models/oauth-token.ts create mode 100644 server/models/oauth/index.ts create mode 100644 server/models/oauth/oauth-client-interface.ts create mode 100644 server/models/oauth/oauth-client.ts create mode 100644 server/models/oauth/oauth-token-interface.ts create mode 100644 server/models/oauth/oauth-token.ts delete mode 100644 server/models/pod-interface.ts delete mode 100644 server/models/pod.ts create mode 100644 server/models/pod/index.ts create mode 100644 server/models/pod/pod-interface.ts create mode 100644 server/models/pod/pod.ts delete mode 100644 server/models/request-interface.ts delete mode 100644 server/models/request-to-pod-interface.ts delete mode 100644 server/models/request-to-pod.ts delete mode 100644 server/models/request-video-event-interface.ts delete mode 100644 server/models/request-video-event.ts delete mode 100644 server/models/request-video-qadu-interface.ts delete mode 100644 server/models/request-video-qadu.ts delete mode 100644 server/models/request.ts create mode 100644 server/models/request/index.ts create mode 100644 server/models/request/request-interface.ts create mode 100644 server/models/request/request-to-pod-interface.ts create mode 100644 server/models/request/request-to-pod.ts create mode 100644 server/models/request/request-video-event-interface.ts create mode 100644 server/models/request/request-video-event.ts create mode 100644 server/models/request/request-video-qadu-interface.ts create mode 100644 server/models/request/request-video-qadu.ts create mode 100644 server/models/request/request.ts delete mode 100644 server/models/tag-interface.ts delete mode 100644 server/models/tag.ts delete mode 100644 server/models/user-interface.ts delete mode 100644 server/models/user-video-rate-interface.ts delete mode 100644 server/models/user-video-rate.ts delete mode 100644 server/models/user.ts create mode 100644 server/models/user/index.ts create mode 100644 server/models/user/user-interface.ts create mode 100644 server/models/user/user-video-rate-interface.ts create mode 100644 server/models/user/user-video-rate.ts create mode 100644 server/models/user/user.ts delete mode 100644 server/models/video-abuse-interface.ts delete mode 100644 server/models/video-abuse.ts delete mode 100644 server/models/video-blacklist-interface.ts delete mode 100644 server/models/video-blacklist.ts delete mode 100644 server/models/video-interface.ts delete mode 100644 server/models/video-tag-interface.ts delete mode 100644 server/models/video-tag.ts delete mode 100644 server/models/video.ts create mode 100644 server/models/video/author-interface.ts create mode 100644 server/models/video/author.ts create mode 100644 server/models/video/index.ts create mode 100644 server/models/video/tag-interface.ts create mode 100644 server/models/video/tag.ts create mode 100644 server/models/video/video-abuse-interface.ts create mode 100644 server/models/video/video-abuse.ts create mode 100644 server/models/video/video-blacklist-interface.ts create mode 100644 server/models/video/video-blacklist.ts create mode 100644 server/models/video/video-interface.ts create mode 100644 server/models/video/video-tag-interface.ts create mode 100644 server/models/video/video-tag.ts create mode 100644 server/models/video/video.ts (limited to 'server') diff --git a/server/initializers/database.ts b/server/initializers/database.ts index 0ab9e98db..1662c1968 100644 --- a/server/initializers/database.ts +++ b/server/initializers/database.ts @@ -1,6 +1,7 @@ import * as fs from 'fs' import { join } from 'path' import * as Sequelize from 'sequelize' +import { each } from 'async' import { CONFIG } from './constants' // Do not use barrel, we need to load database first @@ -72,24 +73,13 @@ const sequelize = new Sequelize(dbname, username, password, { database.sequelize = sequelize database.init = function (silent: boolean, callback: (err: Error) => void) { - const modelDirectory = join(__dirname, '..', 'models') - fs.readdir(modelDirectory, function (err, files) { - if (err) throw err - files.filter(function (file) { - // For all models but not utils.js - if ( - file === 'index.js' || file === 'index.ts' || - file === 'utils.js' || file === 'utils.ts' || - file.endsWith('-interface.js') || file.endsWith('-interface.ts') || - file.endsWith('.js.map') - ) return false + getModelFiles(modelDirectory, function (err, filePaths) { + if (err) throw err - return true - }) - .forEach(function (file) { - const model = sequelize.import(join(modelDirectory, file)) + filePaths.forEach(function (filePath) { + const model = sequelize.import(filePath) database[model['name']] = model }) @@ -111,3 +101,51 @@ database.init = function (silent: boolean, callback: (err: Error) => void) { export { database } + +// --------------------------------------------------------------------------- + +function getModelFiles (modelDirectory: string, callback: (err: Error, filePaths: string[]) => void) { + fs.readdir(modelDirectory, function (err, files) { + if (err) throw err + + const directories = files.filter(function (directory) { + // For all models but not utils.js + if ( + directory === 'index.js' || directory === 'index.ts' || + directory === 'utils.js' || directory === 'utils.ts' + ) return false + + return true + }) + + let modelFilePaths: string[] = [] + + // For each directory we read it and append model in the modelFilePaths array + each(directories, function (directory: string, eachCallback: ErrorCallback) { + const modelDirectoryPath = join(modelDirectory, directory) + + fs.readdir(modelDirectoryPath, function (err, files) { + if (err) return eachCallback(err) + + const filteredFiles = files.filter(file => { + if ( + file === 'index.js' || file === 'index.ts' || + file === 'utils.js' || file === 'utils.ts' || + file.endsWith('-interface.js') || file.endsWith('-interface.ts') || + file.endsWith('.js.map') + ) return false + + return true + }).map(file => { + return join(modelDirectoryPath, file) + }) + + modelFilePaths = modelFilePaths.concat(filteredFiles) + + return eachCallback(null) + }) + }, function(err: Error) { + return callback(err, modelFilePaths) + }) + }) +} diff --git a/server/models/application-interface.ts b/server/models/application-interface.ts deleted file mode 100644 index c03513db1..000000000 --- a/server/models/application-interface.ts +++ /dev/null @@ -1,26 +0,0 @@ -import * as Sequelize from 'sequelize' - -export namespace ApplicationMethods { - export type LoadMigrationVersionCallback = (err: Error, version: number) => void - export type LoadMigrationVersion = (callback: LoadMigrationVersionCallback) => void - - export type UpdateMigrationVersionCallback = (err: Error, applicationInstance: ApplicationAttributes) => void - export type UpdateMigrationVersion = (newVersion: number, transaction: Sequelize.Transaction, callback: UpdateMigrationVersionCallback) => void -} - -export interface ApplicationClass { - loadMigrationVersion: ApplicationMethods.LoadMigrationVersion - updateMigrationVersion: ApplicationMethods.UpdateMigrationVersion -} - -export interface ApplicationAttributes { - migrationVersion: number -} - -export interface ApplicationInstance extends ApplicationClass, ApplicationAttributes, Sequelize.Instance { - id: number - createdAt: Date - updatedAt: Date -} - -export interface ApplicationModel extends ApplicationClass, Sequelize.Model {} diff --git a/server/models/application.ts b/server/models/application.ts deleted file mode 100644 index 55bb40d84..000000000 --- a/server/models/application.ts +++ /dev/null @@ -1,57 +0,0 @@ -import * as Sequelize from 'sequelize' - -import { addMethodsToModel } from './utils' -import { - ApplicationClass, - ApplicationAttributes, - ApplicationInstance, - - ApplicationMethods -} from './application-interface' - -let Application: Sequelize.Model -let loadMigrationVersion: ApplicationMethods.LoadMigrationVersion -let updateMigrationVersion: ApplicationMethods.UpdateMigrationVersion - -export default function defineApplication (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { - Application = sequelize.define('Application', - { - migrationVersion: { - type: DataTypes.INTEGER, - defaultValue: 0, - allowNull: false, - validate: { - isInt: true - } - } - } - ) - - const classMethods = [ loadMigrationVersion, updateMigrationVersion ] - addMethodsToModel(Application, classMethods) - - return Application -} - -// --------------------------------------------------------------------------- - -loadMigrationVersion = function (callback: ApplicationMethods.LoadMigrationVersionCallback) { - const query = { - attributes: [ 'migrationVersion' ] - } - - return Application.findOne(query).asCallback(function (err, data) { - const version = data ? data.migrationVersion : null - - return callback(err, version) - }) -} - -updateMigrationVersion = function (newVersion: number, transaction: Sequelize.Transaction, callback: ApplicationMethods.UpdateMigrationVersionCallback) { - const options: Sequelize.UpdateOptions = { - where: {}, - transaction: transaction - } - - return Application.update({ migrationVersion: newVersion }, options).asCallback(callback) -} diff --git a/server/models/application/application-interface.ts b/server/models/application/application-interface.ts new file mode 100644 index 000000000..c03513db1 --- /dev/null +++ b/server/models/application/application-interface.ts @@ -0,0 +1,26 @@ +import * as Sequelize from 'sequelize' + +export namespace ApplicationMethods { + export type LoadMigrationVersionCallback = (err: Error, version: number) => void + export type LoadMigrationVersion = (callback: LoadMigrationVersionCallback) => void + + export type UpdateMigrationVersionCallback = (err: Error, applicationInstance: ApplicationAttributes) => void + export type UpdateMigrationVersion = (newVersion: number, transaction: Sequelize.Transaction, callback: UpdateMigrationVersionCallback) => void +} + +export interface ApplicationClass { + loadMigrationVersion: ApplicationMethods.LoadMigrationVersion + updateMigrationVersion: ApplicationMethods.UpdateMigrationVersion +} + +export interface ApplicationAttributes { + migrationVersion: number +} + +export interface ApplicationInstance extends ApplicationClass, ApplicationAttributes, Sequelize.Instance { + id: number + createdAt: Date + updatedAt: Date +} + +export interface ApplicationModel extends ApplicationClass, Sequelize.Model {} diff --git a/server/models/application/application.ts b/server/models/application/application.ts new file mode 100644 index 000000000..0e9a1ebb3 --- /dev/null +++ b/server/models/application/application.ts @@ -0,0 +1,57 @@ +import * as Sequelize from 'sequelize' + +import { addMethodsToModel } from '../utils' +import { + ApplicationClass, + ApplicationAttributes, + ApplicationInstance, + + ApplicationMethods +} from './application-interface' + +let Application: Sequelize.Model +let loadMigrationVersion: ApplicationMethods.LoadMigrationVersion +let updateMigrationVersion: ApplicationMethods.UpdateMigrationVersion + +export default function defineApplication (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { + Application = sequelize.define('Application', + { + migrationVersion: { + type: DataTypes.INTEGER, + defaultValue: 0, + allowNull: false, + validate: { + isInt: true + } + } + } + ) + + const classMethods = [ loadMigrationVersion, updateMigrationVersion ] + addMethodsToModel(Application, classMethods) + + return Application +} + +// --------------------------------------------------------------------------- + +loadMigrationVersion = function (callback: ApplicationMethods.LoadMigrationVersionCallback) { + const query = { + attributes: [ 'migrationVersion' ] + } + + return Application.findOne(query).asCallback(function (err, data) { + const version = data ? data.migrationVersion : null + + return callback(err, version) + }) +} + +updateMigrationVersion = function (newVersion: number, transaction: Sequelize.Transaction, callback: ApplicationMethods.UpdateMigrationVersionCallback) { + const options: Sequelize.UpdateOptions = { + where: {}, + transaction: transaction + } + + return Application.update({ migrationVersion: newVersion }, options).asCallback(callback) +} diff --git a/server/models/application/index.ts b/server/models/application/index.ts new file mode 100644 index 000000000..706f85cb9 --- /dev/null +++ b/server/models/application/index.ts @@ -0,0 +1 @@ +export * from './application-interface' diff --git a/server/models/author-interface.ts b/server/models/author-interface.ts deleted file mode 100644 index b57ce2a6b..000000000 --- a/server/models/author-interface.ts +++ /dev/null @@ -1,27 +0,0 @@ -import * as Sequelize from 'sequelize' - -import { PodInstance } from './pod-interface' - -export namespace AuthorMethods { - export type FindOrCreateAuthorCallback = (err: Error, authorInstance?: AuthorInstance) => void - export type FindOrCreateAuthor = (name: string, podId: number, userId: number, transaction: Sequelize.Transaction, callback: FindOrCreateAuthorCallback) => void -} - -export interface AuthorClass { - findOrCreateAuthor: AuthorMethods.FindOrCreateAuthor -} - -export interface AuthorAttributes { - name: string -} - -export interface AuthorInstance extends AuthorClass, AuthorAttributes, Sequelize.Instance { - id: number - createdAt: Date - updatedAt: Date - - podId: number - Pod: PodInstance -} - -export interface AuthorModel extends AuthorClass, Sequelize.Model {} diff --git a/server/models/author.ts b/server/models/author.ts deleted file mode 100644 index e0fb250f9..000000000 --- a/server/models/author.ts +++ /dev/null @@ -1,103 +0,0 @@ -import * as Sequelize from 'sequelize' - -import { isUserUsernameValid } from '../helpers' - -import { addMethodsToModel } from './utils' -import { - AuthorClass, - AuthorInstance, - AuthorAttributes, - - AuthorMethods -} from './author-interface' - -let Author: Sequelize.Model -let findOrCreateAuthor: AuthorMethods.FindOrCreateAuthor - -export default function defineAuthor (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { - Author = sequelize.define('Author', - { - name: { - type: DataTypes.STRING, - allowNull: false, - validate: { - usernameValid: function (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 - } - ] - } - ) - - const classMethods = [ associate, findOrCreateAuthor ] - addMethodsToModel(Author, classMethods) - - 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' - }) -} - -findOrCreateAuthor = function ( - name: string, - podId: number, - userId: number, - transaction: Sequelize.Transaction, - callback: AuthorMethods.FindOrCreateAuthorCallback -) { - const author = { - name, - podId, - userId - } - - const query: any = { - where: author, - defaults: author - } - - if (transaction !== null) query.transaction = transaction - - Author.findOrCreate(query).asCallback(function (err, result) { - if (err) return callback(err) - - // [ instance, wasCreated ] - return callback(null, result[0]) - }) -} diff --git a/server/models/index.ts b/server/models/index.ts index 432c0dc4a..b392a8a77 100644 --- a/server/models/index.ts +++ b/server/models/index.ts @@ -1,17 +1,7 @@ -export * from './application-interface' -export * from './author-interface' -export * from './job-interface' -export * from './oauth-client-interface' -export * from './oauth-token-interface' -export * from './pod-interface' -export * from './request-interface' -export * from './request-to-pod-interface' -export * from './request-video-event-interface' -export * from './request-video-qadu-interface' -export * from './tag-interface' -export * from './user-video-rate-interface' -export * from './user-interface' -export * from './video-abuse-interface' -export * from './video-blacklist-interface' -export * from './video-tag-interface' -export * from './video-interface' +export * from './application' +export * from './job' +export * from './oauth' +export * from './pod' +export * from './request' +export * from './user' +export * from './video' diff --git a/server/models/job-interface.ts b/server/models/job-interface.ts deleted file mode 100644 index ab6678257..000000000 --- a/server/models/job-interface.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as Sequelize from 'sequelize' - -export namespace JobMethods { - export type ListWithLimitCallback = (err: Error, jobInstances: JobInstance[]) => void - export type ListWithLimit = (limit: number, state: string, callback: ListWithLimitCallback) => void -} - -export interface JobClass { - listWithLimit: JobMethods.ListWithLimit -} - -export interface JobAttributes { - state: string - handlerName: string - handlerInputData: object -} - -export interface JobInstance extends JobClass, JobAttributes, Sequelize.Instance { - id: number - createdAt: Date - updatedAt: Date -} - -export interface JobModel extends JobClass, Sequelize.Model {} diff --git a/server/models/job.ts b/server/models/job.ts deleted file mode 100644 index d4380a575..000000000 --- a/server/models/job.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { values } from 'lodash' -import * as Sequelize from 'sequelize' - -import { JOB_STATES } from '../initializers' - -import { addMethodsToModel } from './utils' -import { - JobClass, - JobInstance, - JobAttributes, - - JobMethods -} from './job-interface' - -let Job: Sequelize.Model -let listWithLimit: JobMethods.ListWithLimit - -export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { - Job = sequelize.define('Job', - { - state: { - type: DataTypes.ENUM(values(JOB_STATES)), - allowNull: false - }, - handlerName: { - type: DataTypes.STRING, - allowNull: false - }, - handlerInputData: { - type: DataTypes.JSON, - allowNull: true - } - }, - { - indexes: [ - { - fields: [ 'state' ] - } - ] - } - ) - - const classMethods = [ listWithLimit ] - addMethodsToModel(Job, classMethods) - - return Job -} - -// --------------------------------------------------------------------------- - -listWithLimit = function (limit: number, state: string, callback: JobMethods.ListWithLimitCallback) { - const query = { - order: [ - [ 'id', 'ASC' ] - ], - limit: limit, - where: { - state - } - } - - return Job.findAll(query).asCallback(callback) -} diff --git a/server/models/job/index.ts b/server/models/job/index.ts new file mode 100644 index 000000000..56925fd32 --- /dev/null +++ b/server/models/job/index.ts @@ -0,0 +1 @@ +export * from './job-interface' diff --git a/server/models/job/job-interface.ts b/server/models/job/job-interface.ts new file mode 100644 index 000000000..ab6678257 --- /dev/null +++ b/server/models/job/job-interface.ts @@ -0,0 +1,24 @@ +import * as Sequelize from 'sequelize' + +export namespace JobMethods { + export type ListWithLimitCallback = (err: Error, jobInstances: JobInstance[]) => void + export type ListWithLimit = (limit: number, state: string, callback: ListWithLimitCallback) => void +} + +export interface JobClass { + listWithLimit: JobMethods.ListWithLimit +} + +export interface JobAttributes { + state: string + handlerName: string + handlerInputData: object +} + +export interface JobInstance extends JobClass, JobAttributes, Sequelize.Instance { + id: number + createdAt: Date + updatedAt: Date +} + +export interface JobModel extends JobClass, Sequelize.Model {} diff --git a/server/models/job/job.ts b/server/models/job/job.ts new file mode 100644 index 000000000..60a6c551b --- /dev/null +++ b/server/models/job/job.ts @@ -0,0 +1,63 @@ +import { values } from 'lodash' +import * as Sequelize from 'sequelize' + +import { JOB_STATES } from '../../initializers' + +import { addMethodsToModel } from '../utils' +import { + JobClass, + JobInstance, + JobAttributes, + + JobMethods +} from './job-interface' + +let Job: Sequelize.Model +let listWithLimit: JobMethods.ListWithLimit + +export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { + Job = sequelize.define('Job', + { + state: { + type: DataTypes.ENUM(values(JOB_STATES)), + allowNull: false + }, + handlerName: { + type: DataTypes.STRING, + allowNull: false + }, + handlerInputData: { + type: DataTypes.JSON, + allowNull: true + } + }, + { + indexes: [ + { + fields: [ 'state' ] + } + ] + } + ) + + const classMethods = [ listWithLimit ] + addMethodsToModel(Job, classMethods) + + return Job +} + +// --------------------------------------------------------------------------- + +listWithLimit = function (limit: number, state: string, callback: JobMethods.ListWithLimitCallback) { + const query = { + order: [ + [ 'id', 'ASC' ] + ], + limit: limit, + where: { + state + } + } + + return Job.findAll(query).asCallback(callback) +} diff --git a/server/models/oauth-client-interface.ts b/server/models/oauth-client-interface.ts deleted file mode 100644 index 3b4325740..000000000 --- a/server/models/oauth-client-interface.ts +++ /dev/null @@ -1,32 +0,0 @@ -import * as Sequelize from 'sequelize' - -export namespace OAuthClientMethods { - export type CountTotalCallback = (err: Error, total: number) => void - export type CountTotal = (callback: CountTotalCallback) => void - - export type LoadFirstClientCallback = (err: Error, client: OAuthClientInstance) => void - export type LoadFirstClient = (callback: LoadFirstClientCallback) => void - - export type GetByIdAndSecret = (clientId, clientSecret) => void -} - -export interface OAuthClientClass { - countTotal: OAuthClientMethods.CountTotal - loadFirstClient: OAuthClientMethods.LoadFirstClient - getByIdAndSecret: OAuthClientMethods.GetByIdAndSecret -} - -export interface OAuthClientAttributes { - clientId: string - clientSecret: string - grants: string[] - redirectUris: string[] -} - -export interface OAuthClientInstance extends OAuthClientClass, OAuthClientAttributes, Sequelize.Instance { - id: number - createdAt: Date - updatedAt: Date -} - -export interface OAuthClientModel extends OAuthClientClass, Sequelize.Model {} diff --git a/server/models/oauth-client.ts b/server/models/oauth-client.ts deleted file mode 100644 index a5438f45a..000000000 --- a/server/models/oauth-client.ts +++ /dev/null @@ -1,87 +0,0 @@ -import * as Sequelize from 'sequelize' - -import { addMethodsToModel } from './utils' -import { - OAuthClientClass, - OAuthClientInstance, - OAuthClientAttributes, - - OAuthClientMethods -} from './oauth-client-interface' - -let OAuthClient: Sequelize.Model -let countTotal: OAuthClientMethods.CountTotal -let loadFirstClient: OAuthClientMethods.LoadFirstClient -let getByIdAndSecret: OAuthClientMethods.GetByIdAndSecret - -export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { - OAuthClient = sequelize.define('OAuthClient', - { - clientId: { - type: DataTypes.STRING, - allowNull: false - }, - clientSecret: { - type: DataTypes.STRING, - allowNull: false - }, - grants: { - type: DataTypes.ARRAY(DataTypes.STRING) - }, - redirectUris: { - type: DataTypes.ARRAY(DataTypes.STRING) - } - }, - { - indexes: [ - { - fields: [ 'clientId' ], - unique: true - }, - { - fields: [ 'clientId', 'clientSecret' ], - unique: true - } - ] - } - ) - - const classMethods = [ - associate, - - countTotal, - getByIdAndSecret, - loadFirstClient - ] - addMethodsToModel(OAuthClient, classMethods) - - return OAuthClient -} - -// --------------------------------------------------------------------------- - -function associate (models) { - OAuthClient.hasMany(models.OAuthToken, { - foreignKey: 'oAuthClientId', - onDelete: 'cascade' - }) -} - -countTotal = function (callback: OAuthClientMethods.CountTotalCallback) { - return OAuthClient.count().asCallback(callback) -} - -loadFirstClient = function (callback: OAuthClientMethods.LoadFirstClientCallback) { - return OAuthClient.findOne().asCallback(callback) -} - -getByIdAndSecret = function (clientId: string, clientSecret: string) { - const query = { - where: { - clientId: clientId, - clientSecret: clientSecret - } - } - - return OAuthClient.findOne(query) -} diff --git a/server/models/oauth-token-interface.ts b/server/models/oauth-token-interface.ts deleted file mode 100644 index 88526697e..000000000 --- a/server/models/oauth-token-interface.ts +++ /dev/null @@ -1,48 +0,0 @@ -import * as Sequelize from 'sequelize' -import * as Bluebird from 'bluebird' - -import { UserModel } from './user-interface' - -export type OAuthTokenInfo = { - refreshToken: string - refreshTokenExpiresAt: Date, - client: { - id: number - }, - user: { - id: number - } -} - -export namespace OAuthTokenMethods { - export type GetByRefreshTokenAndPopulateClient = (refreshToken: string) => Bluebird - export type GetByTokenAndPopulateUser = (bearerToken: string) => Bluebird - export type GetByRefreshTokenAndPopulateUser = (refreshToken: string) => Bluebird - - export type RemoveByUserIdCallback = (err: Error) => void - export type RemoveByUserId = (userId, callback) => void -} - -export interface OAuthTokenClass { - getByRefreshTokenAndPopulateClient: OAuthTokenMethods.GetByRefreshTokenAndPopulateClient - getByTokenAndPopulateUser: OAuthTokenMethods.GetByTokenAndPopulateUser - getByRefreshTokenAndPopulateUser: OAuthTokenMethods.GetByRefreshTokenAndPopulateUser - removeByUserId: OAuthTokenMethods.RemoveByUserId -} - -export interface OAuthTokenAttributes { - accessToken: string - accessTokenExpiresAt: Date - refreshToken: string - refreshTokenExpiresAt: Date - - User?: UserModel -} - -export interface OAuthTokenInstance extends OAuthTokenClass, OAuthTokenAttributes, Sequelize.Instance { - id: number - createdAt: Date - updatedAt: Date -} - -export interface OAuthTokenModel extends OAuthTokenClass, Sequelize.Model {} diff --git a/server/models/oauth-token.ts b/server/models/oauth-token.ts deleted file mode 100644 index 91cef11dd..000000000 --- a/server/models/oauth-token.ts +++ /dev/null @@ -1,160 +0,0 @@ -import * as Sequelize from 'sequelize' - -import { logger } from '../helpers' - -import { addMethodsToModel } from './utils' -import { - OAuthTokenClass, - OAuthTokenInstance, - OAuthTokenAttributes, - - OAuthTokenMethods, - OAuthTokenInfo -} from './oauth-token-interface' - -let OAuthToken: Sequelize.Model -let getByRefreshTokenAndPopulateClient: OAuthTokenMethods.GetByRefreshTokenAndPopulateClient -let getByTokenAndPopulateUser: OAuthTokenMethods.GetByTokenAndPopulateUser -let getByRefreshTokenAndPopulateUser: OAuthTokenMethods.GetByRefreshTokenAndPopulateUser -let removeByUserId: OAuthTokenMethods.RemoveByUserId - -export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { - OAuthToken = sequelize.define('OAuthToken', - { - accessToken: { - type: DataTypes.STRING, - allowNull: false - }, - accessTokenExpiresAt: { - type: DataTypes.DATE, - allowNull: false - }, - refreshToken: { - type: DataTypes.STRING, - allowNull: false - }, - refreshTokenExpiresAt: { - type: DataTypes.DATE, - allowNull: false - } - }, - { - indexes: [ - { - fields: [ 'refreshToken' ], - unique: true - }, - { - fields: [ 'accessToken' ], - unique: true - }, - { - fields: [ 'userId' ] - }, - { - fields: [ 'oAuthClientId' ] - } - ] - } - ) - - const classMethods = [ - associate, - - getByRefreshTokenAndPopulateClient, - getByTokenAndPopulateUser, - getByRefreshTokenAndPopulateUser, - removeByUserId - ] - addMethodsToModel(OAuthToken, classMethods) - - return OAuthToken -} - -// --------------------------------------------------------------------------- - -function associate (models) { - OAuthToken.belongsTo(models.User, { - foreignKey: { - name: 'userId', - allowNull: false - }, - onDelete: 'cascade' - }) - - OAuthToken.belongsTo(models.OAuthClient, { - foreignKey: { - name: 'oAuthClientId', - allowNull: false - }, - onDelete: 'cascade' - }) -} - -getByRefreshTokenAndPopulateClient = function (refreshToken: string) { - const query = { - where: { - refreshToken: refreshToken - }, - include: [ OAuthToken['sequelize'].models.OAuthClient ] - } - - return OAuthToken.findOne(query).then(function (token) { - if (!token) return null - - const tokenInfos: OAuthTokenInfo = { - refreshToken: token.refreshToken, - refreshTokenExpiresAt: token.refreshTokenExpiresAt, - client: { - id: token['client'].id - }, - user: { - id: token['user'] - } - } - - return tokenInfos - }).catch(function (err) { - logger.info('getRefreshToken error.', { error: err }) - }) -} - -getByTokenAndPopulateUser = function (bearerToken: string) { - const query = { - where: { - accessToken: bearerToken - }, - include: [ OAuthToken['sequelize'].models.User ] - } - - return OAuthToken.findOne(query).then(function (token) { - if (token) token['user'] = token.User - - return token - }) -} - -getByRefreshTokenAndPopulateUser = function (refreshToken: string) { - const query = { - where: { - refreshToken: refreshToken - }, - include: [ OAuthToken['sequelize'].models.User ] - } - - return OAuthToken.findOne(query).then(function (token) { - token['user'] = token.User - - return token - }) -} - -removeByUserId = function (userId, callback) { - const query = { - where: { - userId: userId - } - } - - return OAuthToken.destroy(query).asCallback(callback) -} diff --git a/server/models/oauth/index.ts b/server/models/oauth/index.ts new file mode 100644 index 000000000..a20d3a56a --- /dev/null +++ b/server/models/oauth/index.ts @@ -0,0 +1,2 @@ +export * from './oauth-client-interface' +export * from './oauth-token-interface' diff --git a/server/models/oauth/oauth-client-interface.ts b/server/models/oauth/oauth-client-interface.ts new file mode 100644 index 000000000..3b4325740 --- /dev/null +++ b/server/models/oauth/oauth-client-interface.ts @@ -0,0 +1,32 @@ +import * as Sequelize from 'sequelize' + +export namespace OAuthClientMethods { + export type CountTotalCallback = (err: Error, total: number) => void + export type CountTotal = (callback: CountTotalCallback) => void + + export type LoadFirstClientCallback = (err: Error, client: OAuthClientInstance) => void + export type LoadFirstClient = (callback: LoadFirstClientCallback) => void + + export type GetByIdAndSecret = (clientId, clientSecret) => void +} + +export interface OAuthClientClass { + countTotal: OAuthClientMethods.CountTotal + loadFirstClient: OAuthClientMethods.LoadFirstClient + getByIdAndSecret: OAuthClientMethods.GetByIdAndSecret +} + +export interface OAuthClientAttributes { + clientId: string + clientSecret: string + grants: string[] + redirectUris: string[] +} + +export interface OAuthClientInstance extends OAuthClientClass, OAuthClientAttributes, Sequelize.Instance { + id: number + createdAt: Date + updatedAt: Date +} + +export interface OAuthClientModel extends OAuthClientClass, Sequelize.Model {} diff --git a/server/models/oauth/oauth-client.ts b/server/models/oauth/oauth-client.ts new file mode 100644 index 000000000..fbc2a3393 --- /dev/null +++ b/server/models/oauth/oauth-client.ts @@ -0,0 +1,87 @@ +import * as Sequelize from 'sequelize' + +import { addMethodsToModel } from '../utils' +import { + OAuthClientClass, + OAuthClientInstance, + OAuthClientAttributes, + + OAuthClientMethods +} from './oauth-client-interface' + +let OAuthClient: Sequelize.Model +let countTotal: OAuthClientMethods.CountTotal +let loadFirstClient: OAuthClientMethods.LoadFirstClient +let getByIdAndSecret: OAuthClientMethods.GetByIdAndSecret + +export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { + OAuthClient = sequelize.define('OAuthClient', + { + clientId: { + type: DataTypes.STRING, + allowNull: false + }, + clientSecret: { + type: DataTypes.STRING, + allowNull: false + }, + grants: { + type: DataTypes.ARRAY(DataTypes.STRING) + }, + redirectUris: { + type: DataTypes.ARRAY(DataTypes.STRING) + } + }, + { + indexes: [ + { + fields: [ 'clientId' ], + unique: true + }, + { + fields: [ 'clientId', 'clientSecret' ], + unique: true + } + ] + } + ) + + const classMethods = [ + associate, + + countTotal, + getByIdAndSecret, + loadFirstClient + ] + addMethodsToModel(OAuthClient, classMethods) + + return OAuthClient +} + +// --------------------------------------------------------------------------- + +function associate (models) { + OAuthClient.hasMany(models.OAuthToken, { + foreignKey: 'oAuthClientId', + onDelete: 'cascade' + }) +} + +countTotal = function (callback: OAuthClientMethods.CountTotalCallback) { + return OAuthClient.count().asCallback(callback) +} + +loadFirstClient = function (callback: OAuthClientMethods.LoadFirstClientCallback) { + return OAuthClient.findOne().asCallback(callback) +} + +getByIdAndSecret = function (clientId: string, clientSecret: string) { + const query = { + where: { + clientId: clientId, + clientSecret: clientSecret + } + } + + return OAuthClient.findOne(query) +} diff --git a/server/models/oauth/oauth-token-interface.ts b/server/models/oauth/oauth-token-interface.ts new file mode 100644 index 000000000..815ad5eef --- /dev/null +++ b/server/models/oauth/oauth-token-interface.ts @@ -0,0 +1,48 @@ +import * as Sequelize from 'sequelize' +import * as Bluebird from 'bluebird' + +import { UserModel } from '../user' + +export type OAuthTokenInfo = { + refreshToken: string + refreshTokenExpiresAt: Date, + client: { + id: number + }, + user: { + id: number + } +} + +export namespace OAuthTokenMethods { + export type GetByRefreshTokenAndPopulateClient = (refreshToken: string) => Bluebird + export type GetByTokenAndPopulateUser = (bearerToken: string) => Bluebird + export type GetByRefreshTokenAndPopulateUser = (refreshToken: string) => Bluebird + + export type RemoveByUserIdCallback = (err: Error) => void + export type RemoveByUserId = (userId, callback) => void +} + +export interface OAuthTokenClass { + getByRefreshTokenAndPopulateClient: OAuthTokenMethods.GetByRefreshTokenAndPopulateClient + getByTokenAndPopulateUser: OAuthTokenMethods.GetByTokenAndPopulateUser + getByRefreshTokenAndPopulateUser: OAuthTokenMethods.GetByRefreshTokenAndPopulateUser + removeByUserId: OAuthTokenMethods.RemoveByUserId +} + +export interface OAuthTokenAttributes { + accessToken: string + accessTokenExpiresAt: Date + refreshToken: string + refreshTokenExpiresAt: Date + + User?: UserModel +} + +export interface OAuthTokenInstance extends OAuthTokenClass, OAuthTokenAttributes, Sequelize.Instance { + id: number + createdAt: Date + updatedAt: Date +} + +export interface OAuthTokenModel extends OAuthTokenClass, Sequelize.Model {} diff --git a/server/models/oauth/oauth-token.ts b/server/models/oauth/oauth-token.ts new file mode 100644 index 000000000..eab9cf858 --- /dev/null +++ b/server/models/oauth/oauth-token.ts @@ -0,0 +1,160 @@ +import * as Sequelize from 'sequelize' + +import { logger } from '../../helpers' + +import { addMethodsToModel } from '../utils' +import { + OAuthTokenClass, + OAuthTokenInstance, + OAuthTokenAttributes, + + OAuthTokenMethods, + OAuthTokenInfo +} from './oauth-token-interface' + +let OAuthToken: Sequelize.Model +let getByRefreshTokenAndPopulateClient: OAuthTokenMethods.GetByRefreshTokenAndPopulateClient +let getByTokenAndPopulateUser: OAuthTokenMethods.GetByTokenAndPopulateUser +let getByRefreshTokenAndPopulateUser: OAuthTokenMethods.GetByRefreshTokenAndPopulateUser +let removeByUserId: OAuthTokenMethods.RemoveByUserId + +export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { + OAuthToken = sequelize.define('OAuthToken', + { + accessToken: { + type: DataTypes.STRING, + allowNull: false + }, + accessTokenExpiresAt: { + type: DataTypes.DATE, + allowNull: false + }, + refreshToken: { + type: DataTypes.STRING, + allowNull: false + }, + refreshTokenExpiresAt: { + type: DataTypes.DATE, + allowNull: false + } + }, + { + indexes: [ + { + fields: [ 'refreshToken' ], + unique: true + }, + { + fields: [ 'accessToken' ], + unique: true + }, + { + fields: [ 'userId' ] + }, + { + fields: [ 'oAuthClientId' ] + } + ] + } + ) + + const classMethods = [ + associate, + + getByRefreshTokenAndPopulateClient, + getByTokenAndPopulateUser, + getByRefreshTokenAndPopulateUser, + removeByUserId + ] + addMethodsToModel(OAuthToken, classMethods) + + return OAuthToken +} + +// --------------------------------------------------------------------------- + +function associate (models) { + OAuthToken.belongsTo(models.User, { + foreignKey: { + name: 'userId', + allowNull: false + }, + onDelete: 'cascade' + }) + + OAuthToken.belongsTo(models.OAuthClient, { + foreignKey: { + name: 'oAuthClientId', + allowNull: false + }, + onDelete: 'cascade' + }) +} + +getByRefreshTokenAndPopulateClient = function (refreshToken: string) { + const query = { + where: { + refreshToken: refreshToken + }, + include: [ OAuthToken['sequelize'].models.OAuthClient ] + } + + return OAuthToken.findOne(query).then(function (token) { + if (!token) return null + + const tokenInfos: OAuthTokenInfo = { + refreshToken: token.refreshToken, + refreshTokenExpiresAt: token.refreshTokenExpiresAt, + client: { + id: token['client'].id + }, + user: { + id: token['user'] + } + } + + return tokenInfos + }).catch(function (err) { + logger.info('getRefreshToken error.', { error: err }) + }) +} + +getByTokenAndPopulateUser = function (bearerToken: string) { + const query = { + where: { + accessToken: bearerToken + }, + include: [ OAuthToken['sequelize'].models.User ] + } + + return OAuthToken.findOne(query).then(function (token) { + if (token) token['user'] = token.User + + return token + }) +} + +getByRefreshTokenAndPopulateUser = function (refreshToken: string) { + const query = { + where: { + refreshToken: refreshToken + }, + include: [ OAuthToken['sequelize'].models.User ] + } + + return OAuthToken.findOne(query).then(function (token) { + token['user'] = token.User + + return token + }) +} + +removeByUserId = function (userId, callback) { + const query = { + where: { + userId: userId + } + } + + return OAuthToken.destroy(query).asCallback(callback) +} diff --git a/server/models/pod-interface.ts b/server/models/pod-interface.ts deleted file mode 100644 index 8f362bd5c..000000000 --- a/server/models/pod-interface.ts +++ /dev/null @@ -1,67 +0,0 @@ -import * as Sequelize from 'sequelize' - -// Don't use barrel, import just what we need -import { Pod as FormatedPod } from '../../shared/models/pod.model' - -export namespace PodMethods { - export type ToFormatedJSON = () => FormatedPod - - export type CountAllCallback = (err: Error, total: number) => void - export type CountAll = (callback) => void - - export type IncrementScoresCallback = (err: Error) => void - export type IncrementScores = (ids: number[], value: number, callback?: IncrementScoresCallback) => void - - export type ListCallback = (err: Error, podInstances?: PodInstance[]) => void - export type List = (callback: ListCallback) => void - - export type ListAllIdsCallback = (err: Error, ids?: number[]) => void - export type ListAllIds = (transaction: Sequelize.Transaction, callback: ListAllIdsCallback) => void - - export type ListRandomPodIdsWithRequestCallback = (err: Error, podInstanceIds?: number[]) => void - export type ListRandomPodIdsWithRequest = (limit: number, tableWithPods: string, tableWithPodsJoins: string, callback: ListRandomPodIdsWithRequestCallback) => void - - export type ListBadPodsCallback = (err: Error, podInstances?: PodInstance[]) => void - export type ListBadPods = (callback: ListBadPodsCallback) => void - - export type LoadCallback = (err: Error, podInstance: PodInstance) => void - export type Load = (id: number, callback: LoadCallback) => void - - export type LoadByHostCallback = (err: Error, podInstance: PodInstance) => void - export type LoadByHost = (host: string, callback: LoadByHostCallback) => void - - export type RemoveAllCallback = (err: Error) => void - export type RemoveAll = (callback: RemoveAllCallback) => void - - export type UpdatePodsScore = (goodPods: number[], badPods: number[]) => void -} - -export interface PodClass { - countAll: PodMethods.CountAll - incrementScores: PodMethods.IncrementScores - list: PodMethods.List - listAllIds: PodMethods.ListAllIds - listRandomPodIdsWithRequest: PodMethods.ListRandomPodIdsWithRequest - listBadPods: PodMethods.ListBadPods - load: PodMethods.Load - loadByHost: PodMethods.LoadByHost - removeAll: PodMethods.RemoveAll - updatePodsScore: PodMethods.UpdatePodsScore -} - -export interface PodAttributes { - host?: string - publicKey?: string - score?: number | Sequelize.literal // Sequelize literal for 'score +' + value - email?: string -} - -export interface PodInstance extends PodClass, PodAttributes, Sequelize.Instance { - id: number - createdAt: Date - updatedAt: Date - - toFormatedJSON: PodMethods.ToFormatedJSON, -} - -export interface PodModel extends PodClass, Sequelize.Model {} diff --git a/server/models/pod.ts b/server/models/pod.ts deleted file mode 100644 index fef9d4da6..000000000 --- a/server/models/pod.ts +++ /dev/null @@ -1,274 +0,0 @@ -import { each, waterfall } from 'async' -import { map } from 'lodash' -import * as Sequelize from 'sequelize' - -import { FRIEND_SCORE, PODS_SCORE } from '../initializers' -import { logger, isHostValid } from '../helpers' - -import { addMethodsToModel } from './utils' -import { - PodClass, - PodInstance, - PodAttributes, - - PodMethods -} from './pod-interface' - -let Pod: Sequelize.Model -let toFormatedJSON: PodMethods.ToFormatedJSON -let countAll: PodMethods.CountAll -let incrementScores: PodMethods.IncrementScores -let list: PodMethods.List -let listAllIds: PodMethods.ListAllIds -let listRandomPodIdsWithRequest: PodMethods.ListRandomPodIdsWithRequest -let listBadPods: PodMethods.ListBadPods -let load: PodMethods.Load -let loadByHost: PodMethods.LoadByHost -let removeAll: PodMethods.RemoveAll -let updatePodsScore: PodMethods.UpdatePodsScore - -export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { - Pod = sequelize.define('Pod', - { - host: { - type: DataTypes.STRING, - allowNull: false, - validate: { - isHost: function (value) { - const res = isHostValid(value) - if (res === false) throw new Error('Host not valid.') - } - } - }, - publicKey: { - type: DataTypes.STRING(5000), - allowNull: false - }, - score: { - type: DataTypes.INTEGER, - defaultValue: FRIEND_SCORE.BASE, - allowNull: false, - validate: { - isInt: true, - max: FRIEND_SCORE.MAX - } - }, - email: { - type: DataTypes.STRING(400), - allowNull: false, - validate: { - isEmail: true - } - } - }, - { - indexes: [ - { - fields: [ 'host' ], - unique: true - }, - { - fields: [ 'score' ] - } - ] - } - ) - - const classMethods = [ - associate, - - countAll, - incrementScores, - list, - listAllIds, - listRandomPodIdsWithRequest, - listBadPods, - load, - loadByHost, - updatePodsScore, - removeAll - ] - const instanceMethods = [ toFormatedJSON ] - addMethodsToModel(Pod, classMethods, instanceMethods) - - return Pod -} - -// ------------------------------ METHODS ------------------------------ - -toFormatedJSON = function () { - const json = { - id: this.id, - host: this.host, - email: this.email, - score: this.score, - createdAt: this.createdAt - } - - return json -} - -// ------------------------------ Statics ------------------------------ - -function associate (models) { - Pod.belongsToMany(models.Request, { - foreignKey: 'podId', - through: models.RequestToPod, - onDelete: 'cascade' - }) -} - -countAll = function (callback: PodMethods.CountAllCallback) { - return Pod.count().asCallback(callback) -} - -incrementScores = function (ids: number[], value: number, callback?: PodMethods.IncrementScoresCallback) { - if (!callback) callback = function () { /* empty */ } - - const update = { - score: Sequelize.literal('score +' + value) - } - - const options = { - where: { - id: { - $in: ids - } - }, - // In this case score is a literal and not an integer so we do not validate it - validate: false - } - - return Pod.update(update, options).asCallback(callback) -} - -list = function (callback: PodMethods.ListCallback) { - return Pod.findAll().asCallback(callback) -} - -listAllIds = function (transaction: Sequelize.Transaction, callback: PodMethods.ListAllIdsCallback) { - const query: any = { - attributes: [ 'id' ] - } - - if (transaction !== null) query.transaction = transaction - - return Pod.findAll(query).asCallback(function (err: Error, pods) { - if (err) return callback(err) - - return callback(null, map(pods, 'id')) - }) -} - -listRandomPodIdsWithRequest = function (limit: number, tableWithPods: string, tableWithPodsJoins: string, callback: PodMethods.ListRandomPodIdsWithRequestCallback) { - Pod.count().asCallback(function (err, count) { - if (err) return callback(err) - - // Optimization... - if (count === 0) return callback(null, []) - - let start = Math.floor(Math.random() * count) - limit - if (start < 0) start = 0 - - const query = { - attributes: [ 'id' ], - order: [ - [ 'id', 'ASC' ] - ], - offset: start, - limit: limit, - where: { - id: { - $in: [ - Sequelize.literal(`SELECT DISTINCT "${tableWithPods}"."podId" FROM "${tableWithPods}" ${tableWithPodsJoins}`) - ] - } - } - } - - return Pod.findAll(query).asCallback(function (err, pods) { - if (err) return callback(err) - - return callback(null, map(pods, 'id')) - }) - }) -} - -listBadPods = function (callback: PodMethods.ListBadPodsCallback) { - const query = { - where: { - score: { $lte: 0 } - } - } - - return Pod.findAll(query).asCallback(callback) -} - -load = function (id: number, callback: PodMethods.LoadCallback) { - return Pod.findById(id).asCallback(callback) -} - -loadByHost = function (host: string, callback: PodMethods.LoadByHostCallback) { - const query = { - where: { - host: host - } - } - - return Pod.findOne(query).asCallback(callback) -} - -removeAll = function (callback: PodMethods.RemoveAllCallback) { - return Pod.destroy().asCallback(callback) -} - -updatePodsScore = function (goodPods: number[], badPods: number[]) { - logger.info('Updating %d good pods and %d bad pods scores.', goodPods.length, badPods.length) - - if (goodPods.length !== 0) { - incrementScores(goodPods, PODS_SCORE.BONUS, function (err) { - if (err) logger.error('Cannot increment scores of good pods.', { error: err }) - }) - } - - if (badPods.length !== 0) { - incrementScores(badPods, PODS_SCORE.MALUS, function (err) { - if (err) logger.error('Cannot decrement scores of bad pods.', { error: err }) - removeBadPods() - }) - } -} - -// --------------------------------------------------------------------------- - -// Remove pods with a score of 0 (too many requests where they were unreachable) -function removeBadPods () { - waterfall([ - function findBadPods (callback) { - listBadPods(function (err, pods) { - if (err) { - logger.error('Cannot find bad pods.', { error: err }) - return callback(err) - } - - return callback(null, pods) - }) - }, - - function removeTheseBadPods (pods, callback) { - each(pods, function (pod: any, callbackEach) { - pod.destroy().asCallback(callbackEach) - }, function (err) { - return callback(err, pods.length) - }) - } - ], function (err, numberOfPodsRemoved) { - if (err) { - logger.error('Cannot remove bad pods.', { error: err }) - } else if (numberOfPodsRemoved) { - logger.info('Removed %d pods.', numberOfPodsRemoved) - } else { - logger.info('No need to remove bad pods.') - } - }) -} diff --git a/server/models/pod/index.ts b/server/models/pod/index.ts new file mode 100644 index 000000000..d2bf50d4d --- /dev/null +++ b/server/models/pod/index.ts @@ -0,0 +1 @@ +export * from './pod-interface' diff --git a/server/models/pod/pod-interface.ts b/server/models/pod/pod-interface.ts new file mode 100644 index 000000000..01ccda64c --- /dev/null +++ b/server/models/pod/pod-interface.ts @@ -0,0 +1,67 @@ +import * as Sequelize from 'sequelize' + +// Don't use barrel, import just what we need +import { Pod as FormatedPod } from '../../../shared/models/pod.model' + +export namespace PodMethods { + export type ToFormatedJSON = () => FormatedPod + + export type CountAllCallback = (err: Error, total: number) => void + export type CountAll = (callback) => void + + export type IncrementScoresCallback = (err: Error) => void + export type IncrementScores = (ids: number[], value: number, callback?: IncrementScoresCallback) => void + + export type ListCallback = (err: Error, podInstances?: PodInstance[]) => void + export type List = (callback: ListCallback) => void + + export type ListAllIdsCallback = (err: Error, ids?: number[]) => void + export type ListAllIds = (transaction: Sequelize.Transaction, callback: ListAllIdsCallback) => void + + export type ListRandomPodIdsWithRequestCallback = (err: Error, podInstanceIds?: number[]) => void + export type ListRandomPodIdsWithRequest = (limit: number, tableWithPods: string, tableWithPodsJoins: string, callback: ListRandomPodIdsWithRequestCallback) => void + + export type ListBadPodsCallback = (err: Error, podInstances?: PodInstance[]) => void + export type ListBadPods = (callback: ListBadPodsCallback) => void + + export type LoadCallback = (err: Error, podInstance: PodInstance) => void + export type Load = (id: number, callback: LoadCallback) => void + + export type LoadByHostCallback = (err: Error, podInstance: PodInstance) => void + export type LoadByHost = (host: string, callback: LoadByHostCallback) => void + + export type RemoveAllCallback = (err: Error) => void + export type RemoveAll = (callback: RemoveAllCallback) => void + + export type UpdatePodsScore = (goodPods: number[], badPods: number[]) => void +} + +export interface PodClass { + countAll: PodMethods.CountAll + incrementScores: PodMethods.IncrementScores + list: PodMethods.List + listAllIds: PodMethods.ListAllIds + listRandomPodIdsWithRequest: PodMethods.ListRandomPodIdsWithRequest + listBadPods: PodMethods.ListBadPods + load: PodMethods.Load + loadByHost: PodMethods.LoadByHost + removeAll: PodMethods.RemoveAll + updatePodsScore: PodMethods.UpdatePodsScore +} + +export interface PodAttributes { + host?: string + publicKey?: string + score?: number | Sequelize.literal // Sequelize literal for 'score +' + value + email?: string +} + +export interface PodInstance extends PodClass, PodAttributes, Sequelize.Instance { + id: number + createdAt: Date + updatedAt: Date + + toFormatedJSON: PodMethods.ToFormatedJSON, +} + +export interface PodModel extends PodClass, Sequelize.Model {} diff --git a/server/models/pod/pod.ts b/server/models/pod/pod.ts new file mode 100644 index 000000000..4c6e63024 --- /dev/null +++ b/server/models/pod/pod.ts @@ -0,0 +1,274 @@ +import { each, waterfall } from 'async' +import { map } from 'lodash' +import * as Sequelize from 'sequelize' + +import { FRIEND_SCORE, PODS_SCORE } from '../../initializers' +import { logger, isHostValid } from '../../helpers' + +import { addMethodsToModel } from '../utils' +import { + PodClass, + PodInstance, + PodAttributes, + + PodMethods +} from './pod-interface' + +let Pod: Sequelize.Model +let toFormatedJSON: PodMethods.ToFormatedJSON +let countAll: PodMethods.CountAll +let incrementScores: PodMethods.IncrementScores +let list: PodMethods.List +let listAllIds: PodMethods.ListAllIds +let listRandomPodIdsWithRequest: PodMethods.ListRandomPodIdsWithRequest +let listBadPods: PodMethods.ListBadPods +let load: PodMethods.Load +let loadByHost: PodMethods.LoadByHost +let removeAll: PodMethods.RemoveAll +let updatePodsScore: PodMethods.UpdatePodsScore + +export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { + Pod = sequelize.define('Pod', + { + host: { + type: DataTypes.STRING, + allowNull: false, + validate: { + isHost: function (value) { + const res = isHostValid(value) + if (res === false) throw new Error('Host not valid.') + } + } + }, + publicKey: { + type: DataTypes.STRING(5000), + allowNull: false + }, + score: { + type: DataTypes.INTEGER, + defaultValue: FRIEND_SCORE.BASE, + allowNull: false, + validate: { + isInt: true, + max: FRIEND_SCORE.MAX + } + }, + email: { + type: DataTypes.STRING(400), + allowNull: false, + validate: { + isEmail: true + } + } + }, + { + indexes: [ + { + fields: [ 'host' ], + unique: true + }, + { + fields: [ 'score' ] + } + ] + } + ) + + const classMethods = [ + associate, + + countAll, + incrementScores, + list, + listAllIds, + listRandomPodIdsWithRequest, + listBadPods, + load, + loadByHost, + updatePodsScore, + removeAll + ] + const instanceMethods = [ toFormatedJSON ] + addMethodsToModel(Pod, classMethods, instanceMethods) + + return Pod +} + +// ------------------------------ METHODS ------------------------------ + +toFormatedJSON = function () { + const json = { + id: this.id, + host: this.host, + email: this.email, + score: this.score, + createdAt: this.createdAt + } + + return json +} + +// ------------------------------ Statics ------------------------------ + +function associate (models) { + Pod.belongsToMany(models.Request, { + foreignKey: 'podId', + through: models.RequestToPod, + onDelete: 'cascade' + }) +} + +countAll = function (callback: PodMethods.CountAllCallback) { + return Pod.count().asCallback(callback) +} + +incrementScores = function (ids: number[], value: number, callback?: PodMethods.IncrementScoresCallback) { + if (!callback) callback = function () { /* empty */ } + + const update = { + score: Sequelize.literal('score +' + value) + } + + const options = { + where: { + id: { + $in: ids + } + }, + // In this case score is a literal and not an integer so we do not validate it + validate: false + } + + return Pod.update(update, options).asCallback(callback) +} + +list = function (callback: PodMethods.ListCallback) { + return Pod.findAll().asCallback(callback) +} + +listAllIds = function (transaction: Sequelize.Transaction, callback: PodMethods.ListAllIdsCallback) { + const query: any = { + attributes: [ 'id' ] + } + + if (transaction !== null) query.transaction = transaction + + return Pod.findAll(query).asCallback(function (err: Error, pods) { + if (err) return callback(err) + + return callback(null, map(pods, 'id')) + }) +} + +listRandomPodIdsWithRequest = function (limit: number, tableWithPods: string, tableWithPodsJoins: string, callback: PodMethods.ListRandomPodIdsWithRequestCallback) { + Pod.count().asCallback(function (err, count) { + if (err) return callback(err) + + // Optimization... + if (count === 0) return callback(null, []) + + let start = Math.floor(Math.random() * count) - limit + if (start < 0) start = 0 + + const query = { + attributes: [ 'id' ], + order: [ + [ 'id', 'ASC' ] + ], + offset: start, + limit: limit, + where: { + id: { + $in: [ + Sequelize.literal(`SELECT DISTINCT "${tableWithPods}"."podId" FROM "${tableWithPods}" ${tableWithPodsJoins}`) + ] + } + } + } + + return Pod.findAll(query).asCallback(function (err, pods) { + if (err) return callback(err) + + return callback(null, map(pods, 'id')) + }) + }) +} + +listBadPods = function (callback: PodMethods.ListBadPodsCallback) { + const query = { + where: { + score: { $lte: 0 } + } + } + + return Pod.findAll(query).asCallback(callback) +} + +load = function (id: number, callback: PodMethods.LoadCallback) { + return Pod.findById(id).asCallback(callback) +} + +loadByHost = function (host: string, callback: PodMethods.LoadByHostCallback) { + const query = { + where: { + host: host + } + } + + return Pod.findOne(query).asCallback(callback) +} + +removeAll = function (callback: PodMethods.RemoveAllCallback) { + return Pod.destroy().asCallback(callback) +} + +updatePodsScore = function (goodPods: number[], badPods: number[]) { + logger.info('Updating %d good pods and %d bad pods scores.', goodPods.length, badPods.length) + + if (goodPods.length !== 0) { + incrementScores(goodPods, PODS_SCORE.BONUS, function (err) { + if (err) logger.error('Cannot increment scores of good pods.', { error: err }) + }) + } + + if (badPods.length !== 0) { + incrementScores(badPods, PODS_SCORE.MALUS, function (err) { + if (err) logger.error('Cannot decrement scores of bad pods.', { error: err }) + removeBadPods() + }) + } +} + +// --------------------------------------------------------------------------- + +// Remove pods with a score of 0 (too many requests where they were unreachable) +function removeBadPods () { + waterfall([ + function findBadPods (callback) { + listBadPods(function (err, pods) { + if (err) { + logger.error('Cannot find bad pods.', { error: err }) + return callback(err) + } + + return callback(null, pods) + }) + }, + + function removeTheseBadPods (pods, callback) { + each(pods, function (pod: any, callbackEach) { + pod.destroy().asCallback(callbackEach) + }, function (err) { + return callback(err, pods.length) + }) + } + ], function (err, numberOfPodsRemoved) { + if (err) { + logger.error('Cannot remove bad pods.', { error: err }) + } else if (numberOfPodsRemoved) { + logger.info('Removed %d pods.', numberOfPodsRemoved) + } else { + logger.info('No need to remove bad pods.') + } + }) +} diff --git a/server/models/request-interface.ts b/server/models/request-interface.ts deleted file mode 100644 index 4bbd79966..000000000 --- a/server/models/request-interface.ts +++ /dev/null @@ -1,47 +0,0 @@ -import * as Sequelize from 'sequelize' - -import { PodInstance, PodAttributes } from './pod-interface' - -export type RequestsGrouped = { - [ podId: number ]: { - request: RequestInstance, - pod: PodInstance - }[] -} - -export namespace RequestMethods { - export type CountTotalRequestsCallback = (err: Error, total: number) => void - export type CountTotalRequests = (callback: CountTotalRequestsCallback) => void - - export type ListWithLimitAndRandomCallback = (err: Error, requestsGrouped?: RequestsGrouped) => void - export type ListWithLimitAndRandom = (limitPods, limitRequestsPerPod, callback: ListWithLimitAndRandomCallback) => void - - export type RemoveWithEmptyToCallback = (err: Error) => void - export type RemoveWithEmptyTo = (callback: RemoveWithEmptyToCallback) => void - - export type RemoveAllCallback = (err: Error) => void - export type RemoveAll = (callback: RemoveAllCallback) => void -} - -export interface RequestClass { - countTotalRequests: RequestMethods.CountTotalRequests - listWithLimitAndRandom: RequestMethods.ListWithLimitAndRandom - removeWithEmptyTo: RequestMethods.RemoveWithEmptyTo - removeAll: RequestMethods.RemoveAll -} - -export interface RequestAttributes { - request: object - endpoint: string -} - -export interface RequestInstance extends RequestClass, RequestAttributes, Sequelize.Instance { - id: number - createdAt: Date - updatedAt: Date - - setPods: Sequelize.HasManySetAssociationsMixin - Pods: PodInstance[] -} - -export interface RequestModel extends RequestClass, Sequelize.Model {} diff --git a/server/models/request-to-pod-interface.ts b/server/models/request-to-pod-interface.ts deleted file mode 100644 index 6d75ca6e5..000000000 --- a/server/models/request-to-pod-interface.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as Sequelize from 'sequelize' - -export namespace RequestToPodMethods { - export type RemoveByRequestIdsAndPodCallback = (err: Error) => void - export type RemoveByRequestIdsAndPod = (requestsIds: number[], podId: number, callback?: RemoveByRequestIdsAndPodCallback) => void -} - -export interface RequestToPodClass { - removeByRequestIdsAndPod: RequestToPodMethods.RemoveByRequestIdsAndPod -} - -export interface RequestToPodAttributes { -} - -export interface RequestToPodInstance extends RequestToPodClass, RequestToPodAttributes, Sequelize.Instance { - id: number - createdAt: Date - updatedAt: Date -} - -export interface RequestToPodModel extends RequestToPodClass, Sequelize.Model {} diff --git a/server/models/request-to-pod.ts b/server/models/request-to-pod.ts deleted file mode 100644 index d2f31463c..000000000 --- a/server/models/request-to-pod.ts +++ /dev/null @@ -1,54 +0,0 @@ -import * as Sequelize from 'sequelize' - -import { addMethodsToModel } from './utils' -import { - RequestToPodClass, - RequestToPodInstance, - RequestToPodAttributes, - - RequestToPodMethods -} from './request-to-pod-interface' - -let RequestToPod: Sequelize.Model -let removeByRequestIdsAndPod: RequestToPodMethods.RemoveByRequestIdsAndPod - -export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { - RequestToPod = sequelize.define('RequestToPod', {}, { - indexes: [ - { - fields: [ 'requestId' ] - }, - { - fields: [ 'podId' ] - }, - { - fields: [ 'requestId', 'podId' ], - unique: true - } - ] - }) - - const classMethods = [ - removeByRequestIdsAndPod - ] - addMethodsToModel(RequestToPod, classMethods) - - return RequestToPod -} - -// --------------------------------------------------------------------------- - -removeByRequestIdsAndPod = function (requestsIds: number[], podId: number, callback?: RequestToPodMethods.RemoveByRequestIdsAndPodCallback) { - if (!callback) callback = function () { /* empty */ } - - const query = { - where: { - requestId: { - $in: requestsIds - }, - podId: podId - } - } - - RequestToPod.destroy(query).asCallback(callback) -} diff --git a/server/models/request-video-event-interface.ts b/server/models/request-video-event-interface.ts deleted file mode 100644 index ad576a2b1..000000000 --- a/server/models/request-video-event-interface.ts +++ /dev/null @@ -1,48 +0,0 @@ -import * as Sequelize from 'sequelize' - -import { VideoInstance } from './video-interface' -import { PodInstance } from './pod-interface' - -export type RequestsVideoEventGrouped = { - [ podId: number ]: { - id: number - type: string - count: number - video: VideoInstance - pod: PodInstance - }[] -} - -export namespace RequestVideoEventMethods { - export type CountTotalRequestsCallback = (err: Error, total: number) => void - export type CountTotalRequests = (callback: CountTotalRequestsCallback) => void - - export type ListWithLimitAndRandomCallback = (err: Error, requestsGrouped?: RequestsVideoEventGrouped) => void - export type ListWithLimitAndRandom = (limitPods: number, limitRequestsPerPod: number, callback: ListWithLimitAndRandomCallback) => void - - export type RemoveByRequestIdsAndPodCallback = () => void - export type RemoveByRequestIdsAndPod = (ids: number[], podId: number, callback: RemoveByRequestIdsAndPodCallback) => void - - export type RemoveAllCallback = () => void - export type RemoveAll = (callback: RemoveAllCallback) => void -} - -export interface RequestVideoEventClass { - countTotalRequests: RequestVideoEventMethods.CountTotalRequests - listWithLimitAndRandom: RequestVideoEventMethods.ListWithLimitAndRandom - removeByRequestIdsAndPod: RequestVideoEventMethods.RemoveByRequestIdsAndPod - removeAll: RequestVideoEventMethods.RemoveAll -} - -export interface RequestVideoEventAttributes { - type: string - count: number -} - -export interface RequestVideoEventInstance extends RequestVideoEventClass, RequestVideoEventAttributes, Sequelize.Instance { - id: number - - Video: VideoInstance -} - -export interface RequestVideoEventModel extends RequestVideoEventClass, Sequelize.Model {} diff --git a/server/models/request-video-event.ts b/server/models/request-video-event.ts deleted file mode 100644 index 32438b581..000000000 --- a/server/models/request-video-event.ts +++ /dev/null @@ -1,185 +0,0 @@ -/* - Request Video events (likes, dislikes, views...) -*/ - -import { values } from 'lodash' -import * as Sequelize from 'sequelize' - -import { database as db } from '../initializers/database' -import { REQUEST_VIDEO_EVENT_TYPES } from '../initializers' -import { isVideoEventCountValid } from '../helpers' -import { addMethodsToModel } from './utils' -import { - RequestVideoEventClass, - RequestVideoEventInstance, - RequestVideoEventAttributes, - - RequestVideoEventMethods, - RequestsVideoEventGrouped -} from './request-video-event-interface' - -let RequestVideoEvent: Sequelize.Model -let countTotalRequests: RequestVideoEventMethods.CountTotalRequests -let listWithLimitAndRandom: RequestVideoEventMethods.ListWithLimitAndRandom -let removeByRequestIdsAndPod: RequestVideoEventMethods.RemoveByRequestIdsAndPod -let removeAll: RequestVideoEventMethods.RemoveAll - -export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { - RequestVideoEvent = sequelize.define('RequestVideoEvent', - { - type: { - type: DataTypes.ENUM(values(REQUEST_VIDEO_EVENT_TYPES)), - allowNull: false - }, - count: { - type: DataTypes.INTEGER, - allowNull: false, - validate: { - countValid: function (value) { - const res = isVideoEventCountValid(value) - if (res === false) throw new Error('Video event count is not valid.') - } - } - } - }, - { - updatedAt: false, - indexes: [ - { - fields: [ 'videoId' ] - } - ] - } - ) - - const classMethods = [ - associate, - - listWithLimitAndRandom, - countTotalRequests, - removeAll, - removeByRequestIdsAndPod - ] - addMethodsToModel(RequestVideoEvent, classMethods) - - return RequestVideoEvent -} - -// ------------------------------ STATICS ------------------------------ - -function associate (models) { - RequestVideoEvent.belongsTo(models.Video, { - foreignKey: { - name: 'videoId', - allowNull: false - }, - onDelete: 'CASCADE' - }) -} - -countTotalRequests = function (callback: RequestVideoEventMethods.CountTotalRequestsCallback) { - const query = {} - return RequestVideoEvent.count(query).asCallback(callback) -} - -listWithLimitAndRandom = function (limitPods: number, limitRequestsPerPod: number, callback: RequestVideoEventMethods.ListWithLimitAndRandomCallback) { - const Pod = db.Pod - - // We make a join between videos and authors to find the podId of our video event requests - const podJoins = 'INNER JOIN "Videos" ON "Videos"."authorId" = "Authors"."id" ' + - 'INNER JOIN "RequestVideoEvents" ON "RequestVideoEvents"."videoId" = "Videos"."id"' - - Pod.listRandomPodIdsWithRequest(limitPods, 'Authors', podJoins, function (err, podIds) { - if (err) return callback(err) - - // We don't have friends that have requests - if (podIds.length === 0) return callback(null, []) - - const query = { - order: [ - [ 'id', 'ASC' ] - ], - include: [ - { - model: RequestVideoEvent['sequelize'].models.Video, - include: [ - { - model: RequestVideoEvent['sequelize'].models.Author, - include: [ - { - model: RequestVideoEvent['sequelize'].models.Pod, - where: { - id: { - $in: podIds - } - } - } - ] - } - ] - } - ] - } - - RequestVideoEvent.findAll(query).asCallback(function (err, requests) { - if (err) return callback(err) - - const requestsGrouped = groupAndTruncateRequests(requests, limitRequestsPerPod) - return callback(err, requestsGrouped) - }) - }) -} - -removeByRequestIdsAndPod = function (ids: number[], podId: number, callback: RequestVideoEventMethods.RemoveByRequestIdsAndPodCallback) { - const query = { - where: { - id: { - $in: ids - } - }, - include: [ - { - model: RequestVideoEvent['sequelize'].models.Video, - include: [ - { - model: RequestVideoEvent['sequelize'].models.Author, - where: { - podId - } - } - ] - } - ] - } - - RequestVideoEvent.destroy(query).asCallback(callback) -} - -removeAll = function (callback: RequestVideoEventMethods.RemoveAllCallback) { - // Delete all requests - RequestVideoEvent.truncate({ cascade: true }).asCallback(callback) -} - -// --------------------------------------------------------------------------- - -function groupAndTruncateRequests (events: RequestVideoEventInstance[], limitRequestsPerPod: number) { - const eventsGrouped: RequestsVideoEventGrouped = {} - - events.forEach(function (event) { - const pod = event.Video.Author.Pod - - if (!eventsGrouped[pod.id]) eventsGrouped[pod.id] = [] - - if (eventsGrouped[pod.id].length < limitRequestsPerPod) { - eventsGrouped[pod.id].push({ - id: event.id, - type: event.type, - count: event.count, - video: event.Video, - pod - }) - } - }) - - return eventsGrouped -} diff --git a/server/models/request-video-qadu-interface.ts b/server/models/request-video-qadu-interface.ts deleted file mode 100644 index 04de7f159..000000000 --- a/server/models/request-video-qadu-interface.ts +++ /dev/null @@ -1,46 +0,0 @@ -import * as Sequelize from 'sequelize' - -import { VideoInstance } from './video-interface' -import { PodInstance } from './pod-interface' - -export type RequestsVideoQaduGrouped = { - [ podId: number ]: { - request: RequestVideoQaduInstance - video: VideoInstance - pod: PodInstance - } -} - -export namespace RequestVideoQaduMethods { - export type CountTotalRequestsCallback = (err: Error, total: number) => void - export type CountTotalRequests = (callback: CountTotalRequestsCallback) => void - - export type ListWithLimitAndRandomCallback = (err: Error, requestsGrouped?: RequestsVideoQaduGrouped) => void - export type ListWithLimitAndRandom = (limitPods: number, limitRequestsPerPod: number, callback: ListWithLimitAndRandomCallback) => void - - export type RemoveByRequestIdsAndPodCallback = () => void - export type RemoveByRequestIdsAndPod = (ids: number[], podId: number, callback: RemoveByRequestIdsAndPodCallback) => void - - export type RemoveAllCallback = () => void - export type RemoveAll = (callback: RemoveAllCallback) => void -} - -export interface RequestVideoQaduClass { - countTotalRequests: RequestVideoQaduMethods.CountTotalRequests - listWithLimitAndRandom: RequestVideoQaduMethods.ListWithLimitAndRandom - removeByRequestIdsAndPod: RequestVideoQaduMethods.RemoveByRequestIdsAndPod - removeAll: RequestVideoQaduMethods.RemoveAll -} - -export interface RequestVideoQaduAttributes { - type: string -} - -export interface RequestVideoQaduInstance extends RequestVideoQaduClass, RequestVideoQaduAttributes, Sequelize.Instance { - id: number - - Pod: PodInstance - Video: VideoInstance -} - -export interface RequestVideoQaduModel extends RequestVideoQaduClass, Sequelize.Model {} diff --git a/server/models/request-video-qadu.ts b/server/models/request-video-qadu.ts deleted file mode 100644 index 27ce0ff29..000000000 --- a/server/models/request-video-qadu.ts +++ /dev/null @@ -1,164 +0,0 @@ -/* - Request Video for Quick And Dirty Updates like: - - views - - likes - - dislikes - - We can't put it in the same system than basic requests for efficiency. - Moreover we don't want to slow down the basic requests with a lot of views/likes/dislikes requests. - So we put it an independant request scheduler. -*/ - -import { values } from 'lodash' -import * as Sequelize from 'sequelize' - -import { database as db } from '../initializers/database' -import { REQUEST_VIDEO_QADU_TYPES } from '../initializers' -import { addMethodsToModel } from './utils' -import { - RequestVideoQaduClass, - RequestVideoQaduInstance, - RequestVideoQaduAttributes, - - RequestVideoQaduMethods -} from './request-video-qadu-interface' - -let RequestVideoQadu: Sequelize.Model -let countTotalRequests: RequestVideoQaduMethods.CountTotalRequests -let listWithLimitAndRandom: RequestVideoQaduMethods.ListWithLimitAndRandom -let removeByRequestIdsAndPod: RequestVideoQaduMethods.RemoveByRequestIdsAndPod -let removeAll: RequestVideoQaduMethods.RemoveAll - -export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { - RequestVideoQadu = sequelize.define('RequestVideoQadu', - { - type: { - type: DataTypes.ENUM(values(REQUEST_VIDEO_QADU_TYPES)), - allowNull: false - } - }, - { - timestamps: false, - indexes: [ - { - fields: [ 'podId' ] - }, - { - fields: [ 'videoId' ] - } - ] - } - ) - - const classMethods = [ - associate, - - listWithLimitAndRandom, - countTotalRequests, - removeAll, - removeByRequestIdsAndPod - ] - addMethodsToModel(RequestVideoQadu, classMethods) - - return RequestVideoQadu -} - -// ------------------------------ STATICS ------------------------------ - -function associate (models) { - RequestVideoQadu.belongsTo(models.Pod, { - foreignKey: { - name: 'podId', - allowNull: false - }, - onDelete: 'CASCADE' - }) - - RequestVideoQadu.belongsTo(models.Video, { - foreignKey: { - name: 'videoId', - allowNull: false - }, - onDelete: 'CASCADE' - }) -} - -countTotalRequests = function (callback: RequestVideoQaduMethods.CountTotalRequestsCallback) { - const query = {} - return RequestVideoQadu.count(query).asCallback(callback) -} - -listWithLimitAndRandom = function (limitPods: number, limitRequestsPerPod: number, callback: RequestVideoQaduMethods.ListWithLimitAndRandomCallback) { - const Pod = db.Pod - const tableJoin = '' - - Pod.listRandomPodIdsWithRequest(limitPods, 'RequestVideoQadus', tableJoin, function (err, podIds) { - if (err) return callback(err) - - // We don't have friends that have requests - if (podIds.length === 0) return callback(null, []) - - const query = { - include: [ - { - model: RequestVideoQadu['sequelize'].models.Pod, - where: { - id: { - $in: podIds - } - } - }, - { - model: RequestVideoQadu['sequelize'].models.Video - } - ] - } - - RequestVideoQadu.findAll(query).asCallback(function (err, requests) { - if (err) return callback(err) - - const requestsGrouped = groupAndTruncateRequests(requests, limitRequestsPerPod) - return callback(err, requestsGrouped) - }) - }) -} - -removeByRequestIdsAndPod = function (ids: number[], podId: number, callback: RequestVideoQaduMethods.RemoveByRequestIdsAndPodCallback) { - const query = { - where: { - id: { - $in: ids - }, - podId - } - } - - RequestVideoQadu.destroy(query).asCallback(callback) -} - -removeAll = function (callback: RequestVideoQaduMethods.RemoveAllCallback) { - // Delete all requests - RequestVideoQadu.truncate({ cascade: true }).asCallback(callback) -} - -// --------------------------------------------------------------------------- - -function groupAndTruncateRequests (requests: RequestVideoQaduInstance[], limitRequestsPerPod: number) { - const requestsGrouped = {} - - requests.forEach(function (request) { - const pod = request.Pod - - if (!requestsGrouped[pod.id]) requestsGrouped[pod.id] = [] - - if (requestsGrouped[pod.id].length < limitRequestsPerPod) { - requestsGrouped[pod.id].push({ - request: request, - video: request.Video, - pod - }) - } - }) - - return requestsGrouped -} diff --git a/server/models/request.ts b/server/models/request.ts deleted file mode 100644 index e6c367f28..000000000 --- a/server/models/request.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { values } from 'lodash' -import * as Sequelize from 'sequelize' - -import { database as db } from '../initializers/database' -import { REQUEST_ENDPOINTS } from '../initializers' -import { addMethodsToModel } from './utils' -import { - RequestClass, - RequestInstance, - RequestAttributes, - - RequestMethods, - RequestsGrouped -} from './request-interface' - -let Request: Sequelize.Model -let countTotalRequests: RequestMethods.CountTotalRequests -let listWithLimitAndRandom: RequestMethods.ListWithLimitAndRandom -let removeWithEmptyTo: RequestMethods.RemoveWithEmptyTo -let removeAll: RequestMethods.RemoveAll - -export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { - Request = sequelize.define('Request', - { - request: { - type: DataTypes.JSON, - allowNull: false - }, - endpoint: { - type: DataTypes.ENUM(values(REQUEST_ENDPOINTS)), - allowNull: false - } - } - ) - - const classMethods = [ - associate, - - listWithLimitAndRandom, - - countTotalRequests, - removeAll, - removeWithEmptyTo - ] - addMethodsToModel(Request, classMethods) - - return Request -} - -// ------------------------------ STATICS ------------------------------ - -function associate (models) { - Request.belongsToMany(models.Pod, { - foreignKey: { - name: 'requestId', - allowNull: false - }, - through: models.RequestToPod, - onDelete: 'CASCADE' - }) -} - -countTotalRequests = function (callback: RequestMethods.CountTotalRequestsCallback) { - // We need to include Pod because there are no cascade delete when a pod is removed - // So we could count requests that do not have existing pod anymore - const query = { - include: [ Request['sequelize'].models.Pod ] - } - - return Request.count(query).asCallback(callback) -} - -listWithLimitAndRandom = function (limitPods: number, limitRequestsPerPod: number, callback: RequestMethods.ListWithLimitAndRandomCallback) { - const Pod = db.Pod - const tableJoin = '' - - Pod.listRandomPodIdsWithRequest(limitPods, 'RequestToPods', '', function (err, podIds) { - if (err) return callback(err) - - // We don't have friends that have requests - if (podIds.length === 0) return callback(null, []) - - // The first x requests of these pods - // It is very important to sort by id ASC to keep the requests order! - const query = { - order: [ - [ 'id', 'ASC' ] - ], - include: [ - { - model: Request['sequelize'].models.Pod, - where: { - id: { - $in: podIds - } - } - } - ] - } - - Request.findAll(query).asCallback(function (err, requests) { - if (err) return callback(err) - - const requestsGrouped = groupAndTruncateRequests(requests, limitRequestsPerPod) - return callback(err, requestsGrouped) - }) - }) -} - -removeAll = function (callback: RequestMethods.RemoveAllCallback) { - // Delete all requests - Request.truncate({ cascade: true }).asCallback(callback) -} - -removeWithEmptyTo = function (callback?: RequestMethods.RemoveWithEmptyToCallback) { - if (!callback) callback = function () { /* empty */ } - - const query = { - where: { - id: { - $notIn: [ - Sequelize.literal('SELECT "requestId" FROM "RequestToPods"') - ] - } - } - } - - Request.destroy(query).asCallback(callback) -} - -// --------------------------------------------------------------------------- - -function groupAndTruncateRequests (requests: RequestInstance[], limitRequestsPerPod: number) { - const requestsGrouped: RequestsGrouped = {} - - requests.forEach(function (request) { - request.Pods.forEach(function (pod) { - if (!requestsGrouped[pod.id]) requestsGrouped[pod.id] = [] - - if (requestsGrouped[pod.id].length < limitRequestsPerPod) { - requestsGrouped[pod.id].push({ - request, - pod - }) - } - }) - }) - - return requestsGrouped -} diff --git a/server/models/request/index.ts b/server/models/request/index.ts new file mode 100644 index 000000000..824c140f5 --- /dev/null +++ b/server/models/request/index.ts @@ -0,0 +1,4 @@ +export * from './request-interface' +export * from './request-to-pod-interface' +export * from './request-video-event-interface' +export * from './request-video-qadu-interface' diff --git a/server/models/request/request-interface.ts b/server/models/request/request-interface.ts new file mode 100644 index 000000000..70fd734e1 --- /dev/null +++ b/server/models/request/request-interface.ts @@ -0,0 +1,47 @@ +import * as Sequelize from 'sequelize' + +import { PodInstance, PodAttributes } from '../pod' + +export type RequestsGrouped = { + [ podId: number ]: { + request: RequestInstance, + pod: PodInstance + }[] +} + +export namespace RequestMethods { + export type CountTotalRequestsCallback = (err: Error, total: number) => void + export type CountTotalRequests = (callback: CountTotalRequestsCallback) => void + + export type ListWithLimitAndRandomCallback = (err: Error, requestsGrouped?: RequestsGrouped) => void + export type ListWithLimitAndRandom = (limitPods, limitRequestsPerPod, callback: ListWithLimitAndRandomCallback) => void + + export type RemoveWithEmptyToCallback = (err: Error) => void + export type RemoveWithEmptyTo = (callback: RemoveWithEmptyToCallback) => void + + export type RemoveAllCallback = (err: Error) => void + export type RemoveAll = (callback: RemoveAllCallback) => void +} + +export interface RequestClass { + countTotalRequests: RequestMethods.CountTotalRequests + listWithLimitAndRandom: RequestMethods.ListWithLimitAndRandom + removeWithEmptyTo: RequestMethods.RemoveWithEmptyTo + removeAll: RequestMethods.RemoveAll +} + +export interface RequestAttributes { + request: object + endpoint: string +} + +export interface RequestInstance extends RequestClass, RequestAttributes, Sequelize.Instance { + id: number + createdAt: Date + updatedAt: Date + + setPods: Sequelize.HasManySetAssociationsMixin + Pods: PodInstance[] +} + +export interface RequestModel extends RequestClass, Sequelize.Model {} diff --git a/server/models/request/request-to-pod-interface.ts b/server/models/request/request-to-pod-interface.ts new file mode 100644 index 000000000..6d75ca6e5 --- /dev/null +++ b/server/models/request/request-to-pod-interface.ts @@ -0,0 +1,21 @@ +import * as Sequelize from 'sequelize' + +export namespace RequestToPodMethods { + export type RemoveByRequestIdsAndPodCallback = (err: Error) => void + export type RemoveByRequestIdsAndPod = (requestsIds: number[], podId: number, callback?: RemoveByRequestIdsAndPodCallback) => void +} + +export interface RequestToPodClass { + removeByRequestIdsAndPod: RequestToPodMethods.RemoveByRequestIdsAndPod +} + +export interface RequestToPodAttributes { +} + +export interface RequestToPodInstance extends RequestToPodClass, RequestToPodAttributes, Sequelize.Instance { + id: number + createdAt: Date + updatedAt: Date +} + +export interface RequestToPodModel extends RequestToPodClass, Sequelize.Model {} diff --git a/server/models/request/request-to-pod.ts b/server/models/request/request-to-pod.ts new file mode 100644 index 000000000..67331be1d --- /dev/null +++ b/server/models/request/request-to-pod.ts @@ -0,0 +1,54 @@ +import * as Sequelize from 'sequelize' + +import { addMethodsToModel } from '../utils' +import { + RequestToPodClass, + RequestToPodInstance, + RequestToPodAttributes, + + RequestToPodMethods +} from './request-to-pod-interface' + +let RequestToPod: Sequelize.Model +let removeByRequestIdsAndPod: RequestToPodMethods.RemoveByRequestIdsAndPod + +export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { + RequestToPod = sequelize.define('RequestToPod', {}, { + indexes: [ + { + fields: [ 'requestId' ] + }, + { + fields: [ 'podId' ] + }, + { + fields: [ 'requestId', 'podId' ], + unique: true + } + ] + }) + + const classMethods = [ + removeByRequestIdsAndPod + ] + addMethodsToModel(RequestToPod, classMethods) + + return RequestToPod +} + +// --------------------------------------------------------------------------- + +removeByRequestIdsAndPod = function (requestsIds: number[], podId: number, callback?: RequestToPodMethods.RemoveByRequestIdsAndPodCallback) { + if (!callback) callback = function () { /* empty */ } + + const query = { + where: { + requestId: { + $in: requestsIds + }, + podId: podId + } + } + + RequestToPod.destroy(query).asCallback(callback) +} diff --git a/server/models/request/request-video-event-interface.ts b/server/models/request/request-video-event-interface.ts new file mode 100644 index 000000000..219d8edc0 --- /dev/null +++ b/server/models/request/request-video-event-interface.ts @@ -0,0 +1,48 @@ +import * as Sequelize from 'sequelize' + +import { VideoInstance } from '../video' +import { PodInstance } from '../pod' + +export type RequestsVideoEventGrouped = { + [ podId: number ]: { + id: number + type: string + count: number + video: VideoInstance + pod: PodInstance + }[] +} + +export namespace RequestVideoEventMethods { + export type CountTotalRequestsCallback = (err: Error, total: number) => void + export type CountTotalRequests = (callback: CountTotalRequestsCallback) => void + + export type ListWithLimitAndRandomCallback = (err: Error, requestsGrouped?: RequestsVideoEventGrouped) => void + export type ListWithLimitAndRandom = (limitPods: number, limitRequestsPerPod: number, callback: ListWithLimitAndRandomCallback) => void + + export type RemoveByRequestIdsAndPodCallback = () => void + export type RemoveByRequestIdsAndPod = (ids: number[], podId: number, callback: RemoveByRequestIdsAndPodCallback) => void + + export type RemoveAllCallback = () => void + export type RemoveAll = (callback: RemoveAllCallback) => void +} + +export interface RequestVideoEventClass { + countTotalRequests: RequestVideoEventMethods.CountTotalRequests + listWithLimitAndRandom: RequestVideoEventMethods.ListWithLimitAndRandom + removeByRequestIdsAndPod: RequestVideoEventMethods.RemoveByRequestIdsAndPod + removeAll: RequestVideoEventMethods.RemoveAll +} + +export interface RequestVideoEventAttributes { + type: string + count: number +} + +export interface RequestVideoEventInstance extends RequestVideoEventClass, RequestVideoEventAttributes, Sequelize.Instance { + id: number + + Video: VideoInstance +} + +export interface RequestVideoEventModel extends RequestVideoEventClass, Sequelize.Model {} diff --git a/server/models/request/request-video-event.ts b/server/models/request/request-video-event.ts new file mode 100644 index 000000000..f552ef50b --- /dev/null +++ b/server/models/request/request-video-event.ts @@ -0,0 +1,185 @@ +/* + Request Video events (likes, dislikes, views...) +*/ + +import { values } from 'lodash' +import * as Sequelize from 'sequelize' + +import { database as db } from '../../initializers/database' +import { REQUEST_VIDEO_EVENT_TYPES } from '../../initializers' +import { isVideoEventCountValid } from '../../helpers' +import { addMethodsToModel } from '../utils' +import { + RequestVideoEventClass, + RequestVideoEventInstance, + RequestVideoEventAttributes, + + RequestVideoEventMethods, + RequestsVideoEventGrouped +} from './request-video-event-interface' + +let RequestVideoEvent: Sequelize.Model +let countTotalRequests: RequestVideoEventMethods.CountTotalRequests +let listWithLimitAndRandom: RequestVideoEventMethods.ListWithLimitAndRandom +let removeByRequestIdsAndPod: RequestVideoEventMethods.RemoveByRequestIdsAndPod +let removeAll: RequestVideoEventMethods.RemoveAll + +export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { + RequestVideoEvent = sequelize.define('RequestVideoEvent', + { + type: { + type: DataTypes.ENUM(values(REQUEST_VIDEO_EVENT_TYPES)), + allowNull: false + }, + count: { + type: DataTypes.INTEGER, + allowNull: false, + validate: { + countValid: function (value) { + const res = isVideoEventCountValid(value) + if (res === false) throw new Error('Video event count is not valid.') + } + } + } + }, + { + updatedAt: false, + indexes: [ + { + fields: [ 'videoId' ] + } + ] + } + ) + + const classMethods = [ + associate, + + listWithLimitAndRandom, + countTotalRequests, + removeAll, + removeByRequestIdsAndPod + ] + addMethodsToModel(RequestVideoEvent, classMethods) + + return RequestVideoEvent +} + +// ------------------------------ STATICS ------------------------------ + +function associate (models) { + RequestVideoEvent.belongsTo(models.Video, { + foreignKey: { + name: 'videoId', + allowNull: false + }, + onDelete: 'CASCADE' + }) +} + +countTotalRequests = function (callback: RequestVideoEventMethods.CountTotalRequestsCallback) { + const query = {} + return RequestVideoEvent.count(query).asCallback(callback) +} + +listWithLimitAndRandom = function (limitPods: number, limitRequestsPerPod: number, callback: RequestVideoEventMethods.ListWithLimitAndRandomCallback) { + const Pod = db.Pod + + // We make a join between videos and authors to find the podId of our video event requests + const podJoins = 'INNER JOIN "Videos" ON "Videos"."authorId" = "Authors"."id" ' + + 'INNER JOIN "RequestVideoEvents" ON "RequestVideoEvents"."videoId" = "Videos"."id"' + + Pod.listRandomPodIdsWithRequest(limitPods, 'Authors', podJoins, function (err, podIds) { + if (err) return callback(err) + + // We don't have friends that have requests + if (podIds.length === 0) return callback(null, []) + + const query = { + order: [ + [ 'id', 'ASC' ] + ], + include: [ + { + model: RequestVideoEvent['sequelize'].models.Video, + include: [ + { + model: RequestVideoEvent['sequelize'].models.Author, + include: [ + { + model: RequestVideoEvent['sequelize'].models.Pod, + where: { + id: { + $in: podIds + } + } + } + ] + } + ] + } + ] + } + + RequestVideoEvent.findAll(query).asCallback(function (err, requests) { + if (err) return callback(err) + + const requestsGrouped = groupAndTruncateRequests(requests, limitRequestsPerPod) + return callback(err, requestsGrouped) + }) + }) +} + +removeByRequestIdsAndPod = function (ids: number[], podId: number, callback: RequestVideoEventMethods.RemoveByRequestIdsAndPodCallback) { + const query = { + where: { + id: { + $in: ids + } + }, + include: [ + { + model: RequestVideoEvent['sequelize'].models.Video, + include: [ + { + model: RequestVideoEvent['sequelize'].models.Author, + where: { + podId + } + } + ] + } + ] + } + + RequestVideoEvent.destroy(query).asCallback(callback) +} + +removeAll = function (callback: RequestVideoEventMethods.RemoveAllCallback) { + // Delete all requests + RequestVideoEvent.truncate({ cascade: true }).asCallback(callback) +} + +// --------------------------------------------------------------------------- + +function groupAndTruncateRequests (events: RequestVideoEventInstance[], limitRequestsPerPod: number) { + const eventsGrouped: RequestsVideoEventGrouped = {} + + events.forEach(function (event) { + const pod = event.Video.Author.Pod + + if (!eventsGrouped[pod.id]) eventsGrouped[pod.id] = [] + + if (eventsGrouped[pod.id].length < limitRequestsPerPod) { + eventsGrouped[pod.id].push({ + id: event.id, + type: event.type, + count: event.count, + video: event.Video, + pod + }) + } + }) + + return eventsGrouped +} diff --git a/server/models/request/request-video-qadu-interface.ts b/server/models/request/request-video-qadu-interface.ts new file mode 100644 index 000000000..625b063dc --- /dev/null +++ b/server/models/request/request-video-qadu-interface.ts @@ -0,0 +1,46 @@ +import * as Sequelize from 'sequelize' + +import { VideoInstance } from '../video' +import { PodInstance } from '../pod' + +export type RequestsVideoQaduGrouped = { + [ podId: number ]: { + request: RequestVideoQaduInstance + video: VideoInstance + pod: PodInstance + } +} + +export namespace RequestVideoQaduMethods { + export type CountTotalRequestsCallback = (err: Error, total: number) => void + export type CountTotalRequests = (callback: CountTotalRequestsCallback) => void + + export type ListWithLimitAndRandomCallback = (err: Error, requestsGrouped?: RequestsVideoQaduGrouped) => void + export type ListWithLimitAndRandom = (limitPods: number, limitRequestsPerPod: number, callback: ListWithLimitAndRandomCallback) => void + + export type RemoveByRequestIdsAndPodCallback = () => void + export type RemoveByRequestIdsAndPod = (ids: number[], podId: number, callback: RemoveByRequestIdsAndPodCallback) => void + + export type RemoveAllCallback = () => void + export type RemoveAll = (callback: RemoveAllCallback) => void +} + +export interface RequestVideoQaduClass { + countTotalRequests: RequestVideoQaduMethods.CountTotalRequests + listWithLimitAndRandom: RequestVideoQaduMethods.ListWithLimitAndRandom + removeByRequestIdsAndPod: RequestVideoQaduMethods.RemoveByRequestIdsAndPod + removeAll: RequestVideoQaduMethods.RemoveAll +} + +export interface RequestVideoQaduAttributes { + type: string +} + +export interface RequestVideoQaduInstance extends RequestVideoQaduClass, RequestVideoQaduAttributes, Sequelize.Instance { + id: number + + Pod: PodInstance + Video: VideoInstance +} + +export interface RequestVideoQaduModel extends RequestVideoQaduClass, Sequelize.Model {} diff --git a/server/models/request/request-video-qadu.ts b/server/models/request/request-video-qadu.ts new file mode 100644 index 000000000..da62239f5 --- /dev/null +++ b/server/models/request/request-video-qadu.ts @@ -0,0 +1,164 @@ +/* + Request Video for Quick And Dirty Updates like: + - views + - likes + - dislikes + + We can't put it in the same system than basic requests for efficiency. + Moreover we don't want to slow down the basic requests with a lot of views/likes/dislikes requests. + So we put it an independant request scheduler. +*/ + +import { values } from 'lodash' +import * as Sequelize from 'sequelize' + +import { database as db } from '../../initializers/database' +import { REQUEST_VIDEO_QADU_TYPES } from '../../initializers' +import { addMethodsToModel } from '../utils' +import { + RequestVideoQaduClass, + RequestVideoQaduInstance, + RequestVideoQaduAttributes, + + RequestVideoQaduMethods +} from './request-video-qadu-interface' + +let RequestVideoQadu: Sequelize.Model +let countTotalRequests: RequestVideoQaduMethods.CountTotalRequests +let listWithLimitAndRandom: RequestVideoQaduMethods.ListWithLimitAndRandom +let removeByRequestIdsAndPod: RequestVideoQaduMethods.RemoveByRequestIdsAndPod +let removeAll: RequestVideoQaduMethods.RemoveAll + +export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { + RequestVideoQadu = sequelize.define('RequestVideoQadu', + { + type: { + type: DataTypes.ENUM(values(REQUEST_VIDEO_QADU_TYPES)), + allowNull: false + } + }, + { + timestamps: false, + indexes: [ + { + fields: [ 'podId' ] + }, + { + fields: [ 'videoId' ] + } + ] + } + ) + + const classMethods = [ + associate, + + listWithLimitAndRandom, + countTotalRequests, + removeAll, + removeByRequestIdsAndPod + ] + addMethodsToModel(RequestVideoQadu, classMethods) + + return RequestVideoQadu +} + +// ------------------------------ STATICS ------------------------------ + +function associate (models) { + RequestVideoQadu.belongsTo(models.Pod, { + foreignKey: { + name: 'podId', + allowNull: false + }, + onDelete: 'CASCADE' + }) + + RequestVideoQadu.belongsTo(models.Video, { + foreignKey: { + name: 'videoId', + allowNull: false + }, + onDelete: 'CASCADE' + }) +} + +countTotalRequests = function (callback: RequestVideoQaduMethods.CountTotalRequestsCallback) { + const query = {} + return RequestVideoQadu.count(query).asCallback(callback) +} + +listWithLimitAndRandom = function (limitPods: number, limitRequestsPerPod: number, callback: RequestVideoQaduMethods.ListWithLimitAndRandomCallback) { + const Pod = db.Pod + const tableJoin = '' + + Pod.listRandomPodIdsWithRequest(limitPods, 'RequestVideoQadus', tableJoin, function (err, podIds) { + if (err) return callback(err) + + // We don't have friends that have requests + if (podIds.length === 0) return callback(null, []) + + const query = { + include: [ + { + model: RequestVideoQadu['sequelize'].models.Pod, + where: { + id: { + $in: podIds + } + } + }, + { + model: RequestVideoQadu['sequelize'].models.Video + } + ] + } + + RequestVideoQadu.findAll(query).asCallback(function (err, requests) { + if (err) return callback(err) + + const requestsGrouped = groupAndTruncateRequests(requests, limitRequestsPerPod) + return callback(err, requestsGrouped) + }) + }) +} + +removeByRequestIdsAndPod = function (ids: number[], podId: number, callback: RequestVideoQaduMethods.RemoveByRequestIdsAndPodCallback) { + const query = { + where: { + id: { + $in: ids + }, + podId + } + } + + RequestVideoQadu.destroy(query).asCallback(callback) +} + +removeAll = function (callback: RequestVideoQaduMethods.RemoveAllCallback) { + // Delete all requests + RequestVideoQadu.truncate({ cascade: true }).asCallback(callback) +} + +// --------------------------------------------------------------------------- + +function groupAndTruncateRequests (requests: RequestVideoQaduInstance[], limitRequestsPerPod: number) { + const requestsGrouped = {} + + requests.forEach(function (request) { + const pod = request.Pod + + if (!requestsGrouped[pod.id]) requestsGrouped[pod.id] = [] + + if (requestsGrouped[pod.id].length < limitRequestsPerPod) { + requestsGrouped[pod.id].push({ + request: request, + video: request.Video, + pod + }) + } + }) + + return requestsGrouped +} diff --git a/server/models/request/request.ts b/server/models/request/request.ts new file mode 100644 index 000000000..66e7da845 --- /dev/null +++ b/server/models/request/request.ts @@ -0,0 +1,150 @@ +import { values } from 'lodash' +import * as Sequelize from 'sequelize' + +import { database as db } from '../../initializers/database' +import { REQUEST_ENDPOINTS } from '../../initializers' +import { addMethodsToModel } from '../utils' +import { + RequestClass, + RequestInstance, + RequestAttributes, + + RequestMethods, + RequestsGrouped +} from './request-interface' + +let Request: Sequelize.Model +let countTotalRequests: RequestMethods.CountTotalRequests +let listWithLimitAndRandom: RequestMethods.ListWithLimitAndRandom +let removeWithEmptyTo: RequestMethods.RemoveWithEmptyTo +let removeAll: RequestMethods.RemoveAll + +export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { + Request = sequelize.define('Request', + { + request: { + type: DataTypes.JSON, + allowNull: false + }, + endpoint: { + type: DataTypes.ENUM(values(REQUEST_ENDPOINTS)), + allowNull: false + } + } + ) + + const classMethods = [ + associate, + + listWithLimitAndRandom, + + countTotalRequests, + removeAll, + removeWithEmptyTo + ] + addMethodsToModel(Request, classMethods) + + return Request +} + +// ------------------------------ STATICS ------------------------------ + +function associate (models) { + Request.belongsToMany(models.Pod, { + foreignKey: { + name: 'requestId', + allowNull: false + }, + through: models.RequestToPod, + onDelete: 'CASCADE' + }) +} + +countTotalRequests = function (callback: RequestMethods.CountTotalRequestsCallback) { + // We need to include Pod because there are no cascade delete when a pod is removed + // So we could count requests that do not have existing pod anymore + const query = { + include: [ Request['sequelize'].models.Pod ] + } + + return Request.count(query).asCallback(callback) +} + +listWithLimitAndRandom = function (limitPods: number, limitRequestsPerPod: number, callback: RequestMethods.ListWithLimitAndRandomCallback) { + const Pod = db.Pod + const tableJoin = '' + + Pod.listRandomPodIdsWithRequest(limitPods, 'RequestToPods', '', function (err, podIds) { + if (err) return callback(err) + + // We don't have friends that have requests + if (podIds.length === 0) return callback(null, []) + + // The first x requests of these pods + // It is very important to sort by id ASC to keep the requests order! + const query = { + order: [ + [ 'id', 'ASC' ] + ], + include: [ + { + model: Request['sequelize'].models.Pod, + where: { + id: { + $in: podIds + } + } + } + ] + } + + Request.findAll(query).asCallback(function (err, requests) { + if (err) return callback(err) + + const requestsGrouped = groupAndTruncateRequests(requests, limitRequestsPerPod) + return callback(err, requestsGrouped) + }) + }) +} + +removeAll = function (callback: RequestMethods.RemoveAllCallback) { + // Delete all requests + Request.truncate({ cascade: true }).asCallback(callback) +} + +removeWithEmptyTo = function (callback?: RequestMethods.RemoveWithEmptyToCallback) { + if (!callback) callback = function () { /* empty */ } + + const query = { + where: { + id: { + $notIn: [ + Sequelize.literal('SELECT "requestId" FROM "RequestToPods"') + ] + } + } + } + + Request.destroy(query).asCallback(callback) +} + +// --------------------------------------------------------------------------- + +function groupAndTruncateRequests (requests: RequestInstance[], limitRequestsPerPod: number) { + const requestsGrouped: RequestsGrouped = {} + + requests.forEach(function (request) { + request.Pods.forEach(function (pod) { + if (!requestsGrouped[pod.id]) requestsGrouped[pod.id] = [] + + if (requestsGrouped[pod.id].length < limitRequestsPerPod) { + requestsGrouped[pod.id].push({ + request, + pod + }) + } + }) + }) + + return requestsGrouped +} diff --git a/server/models/tag-interface.ts b/server/models/tag-interface.ts deleted file mode 100644 index e045e7ca5..000000000 --- a/server/models/tag-interface.ts +++ /dev/null @@ -1,20 +0,0 @@ -import * as Sequelize from 'sequelize' - -export namespace TagMethods { - export type FindOrCreateTagsCallback = (err: Error, tagInstances: TagInstance[]) => void - export type FindOrCreateTags = (tags: string[], transaction: Sequelize.Transaction, callback: FindOrCreateTagsCallback) => void -} - -export interface TagClass { - findOrCreateTags: TagMethods.FindOrCreateTags -} - -export interface TagAttributes { - name: string -} - -export interface TagInstance extends TagClass, TagAttributes, Sequelize.Instance { - id: number -} - -export interface TagModel extends TagClass, Sequelize.Model {} diff --git a/server/models/tag.ts b/server/models/tag.ts deleted file mode 100644 index 54a5f79e5..000000000 --- a/server/models/tag.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { each } from 'async' -import * as Sequelize from 'sequelize' - -import { addMethodsToModel } from './utils' -import { - TagClass, - TagInstance, - TagAttributes, - - TagMethods -} from './tag-interface' - -let Tag: Sequelize.Model -let findOrCreateTags: TagMethods.FindOrCreateTags - -export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { - Tag = sequelize.define('Tag', - { - name: { - type: DataTypes.STRING, - allowNull: false - } - }, - { - timestamps: false, - indexes: [ - { - fields: [ 'name' ], - unique: true - } - ] - } - ) - - const classMethods = [ - associate, - - findOrCreateTags - ] - addMethodsToModel(Tag, classMethods) - - return Tag -} - -// --------------------------------------------------------------------------- - -function associate (models) { - Tag.belongsToMany(models.Video, { - foreignKey: 'tagId', - through: models.VideoTag, - onDelete: 'cascade' - }) -} - -findOrCreateTags = function (tags: string[], transaction: Sequelize.Transaction, callback: TagMethods.FindOrCreateTagsCallback) { - const tagInstances = [] - - each(tags, function (tag, callbackEach) { - const query: any = { - where: { - name: tag - }, - defaults: { - name: tag - } - } - - if (transaction) query.transaction = transaction - - Tag.findOrCreate(query).asCallback(function (err, res) { - if (err) return callbackEach(err) - - // res = [ tag, isCreated ] - const tag = res[0] - tagInstances.push(tag) - return callbackEach() - }) - }, function (err) { - return callback(err, tagInstances) - }) -} diff --git a/server/models/user-interface.ts b/server/models/user-interface.ts deleted file mode 100644 index 98963b743..000000000 --- a/server/models/user-interface.ts +++ /dev/null @@ -1,63 +0,0 @@ -import * as Sequelize from 'sequelize' -import * as Bluebird from 'bluebird' - -// Don't use barrel, import just what we need -import { User as FormatedUser } from '../../shared/models/user.model' - -export namespace UserMethods { - export type IsPasswordMatchCallback = (err: Error, same: boolean) => void - export type IsPasswordMatch = (password: string, callback: IsPasswordMatchCallback) => void - - export type ToFormatedJSON = () => FormatedUser - export type IsAdmin = () => boolean - - export type CountTotalCallback = (err: Error, total: number) => void - export type CountTotal = (callback: CountTotalCallback) => void - - export type GetByUsername = (username: string) => Bluebird - - export type ListCallback = (err: Error, userInstances: UserInstance[]) => void - export type List = (callback: ListCallback) => void - - export type ListForApiCallback = (err: Error, userInstances?: UserInstance[], total?: number) => void - export type ListForApi = (start: number, count: number, sort: string, callback: ListForApiCallback) => void - - export type LoadByIdCallback = (err: Error, userInstance: UserInstance) => void - export type LoadById = (id: number, callback: LoadByIdCallback) => void - - export type LoadByUsernameCallback = (err: Error, userInstance: UserInstance) => void - export type LoadByUsername = (username: string, callback: LoadByUsernameCallback) => void - - export type LoadByUsernameOrEmailCallback = (err: Error, userInstance: UserInstance) => void - export type LoadByUsernameOrEmail = (username: string, email: string, callback: LoadByUsernameOrEmailCallback) => void -} - -export interface UserClass { - isPasswordMatch: UserMethods.IsPasswordMatch, - toFormatedJSON: UserMethods.ToFormatedJSON, - isAdmin: UserMethods.IsAdmin, - - countTotal: UserMethods.CountTotal, - getByUsername: UserMethods.GetByUsername, - list: UserMethods.List, - listForApi: UserMethods.ListForApi, - loadById: UserMethods.LoadById, - loadByUsername: UserMethods.LoadByUsername, - loadByUsernameOrEmail: UserMethods.LoadByUsernameOrEmail -} - -export interface UserAttributes { - password: string - username: string - email: string - displayNSFW?: boolean - role: string -} - -export interface UserInstance extends UserClass, UserAttributes, Sequelize.Instance { - id: number - createdAt: Date - updatedAt: Date -} - -export interface UserModel extends UserClass, Sequelize.Model {} diff --git a/server/models/user-video-rate-interface.ts b/server/models/user-video-rate-interface.ts deleted file mode 100644 index e48869fd2..000000000 --- a/server/models/user-video-rate-interface.ts +++ /dev/null @@ -1,22 +0,0 @@ -import * as Sequelize from 'sequelize' - -export namespace UserVideoRateMethods { - export type LoadCallback = (err: Error, userVideoRateInstance: UserVideoRateInstance) => void - export type Load = (userId, videoId, transaction, callback) => void -} - -export interface UserVideoRateClass { - load: UserVideoRateMethods.Load -} - -export interface UserVideoRateAttributes { - type: string -} - -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-video-rate.ts b/server/models/user-video-rate.ts deleted file mode 100644 index 0326e1796..000000000 --- a/server/models/user-video-rate.ts +++ /dev/null @@ -1,80 +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 { - UserVideoRateClass, - 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, callback: UserVideoRateMethods.LoadCallback) { - const options: Sequelize.FindOptions = { - where: { - userId, - videoId - } - } - if (transaction) options.transaction = transaction - - return UserVideoRate.findOne(options).asCallback(callback) -} diff --git a/server/models/user.ts b/server/models/user.ts deleted file mode 100644 index 1a56a6dc3..000000000 --- a/server/models/user.ts +++ /dev/null @@ -1,221 +0,0 @@ -import { values } from 'lodash' -import * as Sequelize from 'sequelize' - -import { getSort } from './utils' -import { USER_ROLES } from '../initializers' -import { - cryptPassword, - comparePassword, - isUserPasswordValid, - isUserUsernameValid, - isUserDisplayNSFWValid -} from '../helpers' - -import { addMethodsToModel } from './utils' -import { - UserClass, - UserInstance, - UserAttributes, - - UserMethods -} from './user-interface' - -let User: Sequelize.Model -let isPasswordMatch: UserMethods.IsPasswordMatch -let toFormatedJSON: UserMethods.ToFormatedJSON -let isAdmin: UserMethods.IsAdmin -let countTotal: UserMethods.CountTotal -let getByUsername: UserMethods.GetByUsername -let list: UserMethods.List -let listForApi: UserMethods.ListForApi -let loadById: UserMethods.LoadById -let loadByUsername: UserMethods.LoadByUsername -let loadByUsernameOrEmail: UserMethods.LoadByUsernameOrEmail - -export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { - User = sequelize.define('User', - { - password: { - type: DataTypes.STRING, - allowNull: false, - validate: { - passwordValid: function (value) { - const res = isUserPasswordValid(value) - if (res === false) throw new Error('Password not valid.') - } - } - }, - username: { - type: DataTypes.STRING, - allowNull: false, - validate: { - usernameValid: function (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: function (value) { - const res = isUserDisplayNSFWValid(value) - if (res === false) throw new Error('Display NSFW is not valid.') - } - } - }, - role: { - type: DataTypes.ENUM(values(USER_ROLES)), - allowNull: false - } - }, - { - indexes: [ - { - fields: [ 'username' ], - unique: true - }, - { - fields: [ 'email' ], - unique: true - } - ], - hooks: { - beforeCreate: beforeCreateOrUpdate, - beforeUpdate: beforeCreateOrUpdate - } - } - ) - - const classMethods = [ - associate, - - countTotal, - getByUsername, - list, - listForApi, - loadById, - loadByUsername, - loadByUsernameOrEmail - ] - const instanceMethods = [ - isPasswordMatch, - toFormatedJSON, - isAdmin - ] - addMethodsToModel(User, classMethods, instanceMethods) - - return User -} - -function beforeCreateOrUpdate (user: UserInstance) { - return new Promise(function (resolve, reject) { - cryptPassword(user.password, function (err, hash) { - if (err) return reject(err) - - user.password = hash - - return resolve() - }) - }) -} - -// ------------------------------ METHODS ------------------------------ - -isPasswordMatch = function (password: string, callback: UserMethods.IsPasswordMatchCallback) { - return comparePassword(password, this.password, callback) -} - -toFormatedJSON = function (this: UserInstance) { - return { - id: this.id, - username: this.username, - email: this.email, - displayNSFW: this.displayNSFW, - role: this.role, - createdAt: this.createdAt - } -} - -isAdmin = function () { - return this.role === USER_ROLES.ADMIN -} - -// ------------------------------ STATICS ------------------------------ - -function associate (models) { - User.hasOne(models.Author, { - foreignKey: 'userId', - onDelete: 'cascade' - }) - - User.hasMany(models.OAuthToken, { - foreignKey: 'userId', - onDelete: 'cascade' - }) -} - -countTotal = function (callback: UserMethods.CountTotalCallback) { - return this.count().asCallback(callback) -} - -getByUsername = function (username: string) { - const query = { - where: { - username: username - } - } - - return User.findOne(query) -} - -list = function (callback: UserMethods.ListCallback) { - return User.find().asCallback(callback) -} - -listForApi = function (start: number, count: number, sort: string, callback: UserMethods.ListForApiCallback) { - const query = { - offset: start, - limit: count, - order: [ getSort(sort) ] - } - - return User.findAndCountAll(query).asCallback(function (err, result) { - if (err) return callback(err) - - return callback(null, result.rows, result.count) - }) -} - -loadById = function (id: number, callback: UserMethods.LoadByIdCallback) { - return User.findById(id).asCallback(callback) -} - -loadByUsername = function (username: string, callback: UserMethods.LoadByUsernameCallback) { - const query = { - where: { - username: username - } - } - - return User.findOne(query).asCallback(callback) -} - -loadByUsernameOrEmail = function (username: string, email: string, callback: UserMethods.LoadByUsernameOrEmailCallback) { - const query = { - where: { - $or: [ { username }, { email } ] - } - } - - return User.findOne(query).asCallback(callback) -} diff --git a/server/models/user/index.ts b/server/models/user/index.ts new file mode 100644 index 000000000..ed3689518 --- /dev/null +++ b/server/models/user/index.ts @@ -0,0 +1,2 @@ +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 new file mode 100644 index 000000000..1ba4bd800 --- /dev/null +++ b/server/models/user/user-interface.ts @@ -0,0 +1,63 @@ +import * as Sequelize from 'sequelize' +import * as Bluebird from 'bluebird' + +// Don't use barrel, import just what we need +import { User as FormatedUser } from '../../../shared/models/user.model' + +export namespace UserMethods { + export type IsPasswordMatchCallback = (err: Error, same: boolean) => void + export type IsPasswordMatch = (password: string, callback: IsPasswordMatchCallback) => void + + export type ToFormatedJSON = () => FormatedUser + export type IsAdmin = () => boolean + + export type CountTotalCallback = (err: Error, total: number) => void + export type CountTotal = (callback: CountTotalCallback) => void + + export type GetByUsername = (username: string) => Bluebird + + export type ListCallback = (err: Error, userInstances: UserInstance[]) => void + export type List = (callback: ListCallback) => void + + export type ListForApiCallback = (err: Error, userInstances?: UserInstance[], total?: number) => void + export type ListForApi = (start: number, count: number, sort: string, callback: ListForApiCallback) => void + + export type LoadByIdCallback = (err: Error, userInstance: UserInstance) => void + export type LoadById = (id: number, callback: LoadByIdCallback) => void + + export type LoadByUsernameCallback = (err: Error, userInstance: UserInstance) => void + export type LoadByUsername = (username: string, callback: LoadByUsernameCallback) => void + + export type LoadByUsernameOrEmailCallback = (err: Error, userInstance: UserInstance) => void + export type LoadByUsernameOrEmail = (username: string, email: string, callback: LoadByUsernameOrEmailCallback) => void +} + +export interface UserClass { + isPasswordMatch: UserMethods.IsPasswordMatch, + toFormatedJSON: UserMethods.ToFormatedJSON, + isAdmin: UserMethods.IsAdmin, + + countTotal: UserMethods.CountTotal, + getByUsername: UserMethods.GetByUsername, + list: UserMethods.List, + listForApi: UserMethods.ListForApi, + loadById: UserMethods.LoadById, + loadByUsername: UserMethods.LoadByUsername, + loadByUsernameOrEmail: UserMethods.LoadByUsernameOrEmail +} + +export interface UserAttributes { + password: string + username: string + email: string + displayNSFW?: boolean + role: string +} + +export interface UserInstance extends UserClass, UserAttributes, Sequelize.Instance { + id: number + createdAt: Date + updatedAt: Date +} + +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 new file mode 100644 index 000000000..e48869fd2 --- /dev/null +++ b/server/models/user/user-video-rate-interface.ts @@ -0,0 +1,22 @@ +import * as Sequelize from 'sequelize' + +export namespace UserVideoRateMethods { + export type LoadCallback = (err: Error, userVideoRateInstance: UserVideoRateInstance) => void + export type Load = (userId, videoId, transaction, callback) => void +} + +export interface UserVideoRateClass { + load: UserVideoRateMethods.Load +} + +export interface UserVideoRateAttributes { + type: string +} + +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 new file mode 100644 index 000000000..68be62fc2 --- /dev/null +++ b/server/models/user/user-video-rate.ts @@ -0,0 +1,80 @@ +/* + User rates per video. + +*/ +import { values } from 'lodash' +import * as Sequelize from 'sequelize' + +import { VIDEO_RATE_TYPES } from '../../initializers' + +import { addMethodsToModel } from '../utils' +import { + UserVideoRateClass, + 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, callback: UserVideoRateMethods.LoadCallback) { + const options: Sequelize.FindOptions = { + where: { + userId, + videoId + } + } + if (transaction) options.transaction = transaction + + return UserVideoRate.findOne(options).asCallback(callback) +} diff --git a/server/models/user/user.ts b/server/models/user/user.ts new file mode 100644 index 000000000..d78f5f845 --- /dev/null +++ b/server/models/user/user.ts @@ -0,0 +1,221 @@ +import { values } from 'lodash' +import * as Sequelize from 'sequelize' + +import { getSort } from '../utils' +import { USER_ROLES } from '../../initializers' +import { + cryptPassword, + comparePassword, + isUserPasswordValid, + isUserUsernameValid, + isUserDisplayNSFWValid +} from '../../helpers' + +import { addMethodsToModel } from '../utils' +import { + UserClass, + UserInstance, + UserAttributes, + + UserMethods +} from './user-interface' + +let User: Sequelize.Model +let isPasswordMatch: UserMethods.IsPasswordMatch +let toFormatedJSON: UserMethods.ToFormatedJSON +let isAdmin: UserMethods.IsAdmin +let countTotal: UserMethods.CountTotal +let getByUsername: UserMethods.GetByUsername +let list: UserMethods.List +let listForApi: UserMethods.ListForApi +let loadById: UserMethods.LoadById +let loadByUsername: UserMethods.LoadByUsername +let loadByUsernameOrEmail: UserMethods.LoadByUsernameOrEmail + +export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { + User = sequelize.define('User', + { + password: { + type: DataTypes.STRING, + allowNull: false, + validate: { + passwordValid: function (value) { + const res = isUserPasswordValid(value) + if (res === false) throw new Error('Password not valid.') + } + } + }, + username: { + type: DataTypes.STRING, + allowNull: false, + validate: { + usernameValid: function (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: function (value) { + const res = isUserDisplayNSFWValid(value) + if (res === false) throw new Error('Display NSFW is not valid.') + } + } + }, + role: { + type: DataTypes.ENUM(values(USER_ROLES)), + allowNull: false + } + }, + { + indexes: [ + { + fields: [ 'username' ], + unique: true + }, + { + fields: [ 'email' ], + unique: true + } + ], + hooks: { + beforeCreate: beforeCreateOrUpdate, + beforeUpdate: beforeCreateOrUpdate + } + } + ) + + const classMethods = [ + associate, + + countTotal, + getByUsername, + list, + listForApi, + loadById, + loadByUsername, + loadByUsernameOrEmail + ] + const instanceMethods = [ + isPasswordMatch, + toFormatedJSON, + isAdmin + ] + addMethodsToModel(User, classMethods, instanceMethods) + + return User +} + +function beforeCreateOrUpdate (user: UserInstance) { + return new Promise(function (resolve, reject) { + cryptPassword(user.password, function (err, hash) { + if (err) return reject(err) + + user.password = hash + + return resolve() + }) + }) +} + +// ------------------------------ METHODS ------------------------------ + +isPasswordMatch = function (password: string, callback: UserMethods.IsPasswordMatchCallback) { + return comparePassword(password, this.password, callback) +} + +toFormatedJSON = function (this: UserInstance) { + return { + id: this.id, + username: this.username, + email: this.email, + displayNSFW: this.displayNSFW, + role: this.role, + createdAt: this.createdAt + } +} + +isAdmin = function () { + return this.role === USER_ROLES.ADMIN +} + +// ------------------------------ STATICS ------------------------------ + +function associate (models) { + User.hasOne(models.Author, { + foreignKey: 'userId', + onDelete: 'cascade' + }) + + User.hasMany(models.OAuthToken, { + foreignKey: 'userId', + onDelete: 'cascade' + }) +} + +countTotal = function (callback: UserMethods.CountTotalCallback) { + return this.count().asCallback(callback) +} + +getByUsername = function (username: string) { + const query = { + where: { + username: username + } + } + + return User.findOne(query) +} + +list = function (callback: UserMethods.ListCallback) { + return User.find().asCallback(callback) +} + +listForApi = function (start: number, count: number, sort: string, callback: UserMethods.ListForApiCallback) { + const query = { + offset: start, + limit: count, + order: [ getSort(sort) ] + } + + return User.findAndCountAll(query).asCallback(function (err, result) { + if (err) return callback(err) + + return callback(null, result.rows, result.count) + }) +} + +loadById = function (id: number, callback: UserMethods.LoadByIdCallback) { + return User.findById(id).asCallback(callback) +} + +loadByUsername = function (username: string, callback: UserMethods.LoadByUsernameCallback) { + const query = { + where: { + username: username + } + } + + return User.findOne(query).asCallback(callback) +} + +loadByUsernameOrEmail = function (username: string, email: string, callback: UserMethods.LoadByUsernameOrEmailCallback) { + const query = { + where: { + $or: [ { username }, { email } ] + } + } + + return User.findOne(query).asCallback(callback) +} diff --git a/server/models/video-abuse-interface.ts b/server/models/video-abuse-interface.ts deleted file mode 100644 index d9cb93b42..000000000 --- a/server/models/video-abuse-interface.ts +++ /dev/null @@ -1,28 +0,0 @@ -import * as Sequelize from 'sequelize' - -// Don't use barrel, import just what we need -import { VideoAbuse as FormatedVideoAbuse } from '../../shared/models/video-abuse.model' - -export namespace VideoAbuseMethods { - export type toFormatedJSON = () => FormatedVideoAbuse - - export type ListForApiCallback = (err: Error, videoAbuseInstances?: VideoAbuseInstance[], total?: number) => void - export type ListForApi = (start: number, count: number, sort: string, callback: ListForApiCallback) => void -} - -export interface VideoAbuseClass { - listForApi: VideoAbuseMethods.ListForApi -} - -export interface VideoAbuseAttributes { - reporterUsername: string - reason: string -} - -export interface VideoAbuseInstance extends VideoAbuseClass, VideoAbuseAttributes, Sequelize.Instance { - id: number - createdAt: Date - updatedAt: Date -} - -export interface VideoAbuseModel extends VideoAbuseClass, Sequelize.Model {} diff --git a/server/models/video-abuse.ts b/server/models/video-abuse.ts deleted file mode 100644 index 5602ef968..000000000 --- a/server/models/video-abuse.ts +++ /dev/null @@ -1,131 +0,0 @@ -import * as Sequelize from 'sequelize' - -import { CONFIG } from '../initializers' -import { isVideoAbuseReporterUsernameValid, isVideoAbuseReasonValid } from '../helpers' - -import { addMethodsToModel, getSort } from './utils' -import { - VideoAbuseClass, - VideoAbuseInstance, - VideoAbuseAttributes, - - VideoAbuseMethods -} from './video-abuse-interface' - -let VideoAbuse: Sequelize.Model -let listForApi: VideoAbuseMethods.ListForApi - -export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { - VideoAbuse = sequelize.define('VideoAbuse', - { - reporterUsername: { - type: DataTypes.STRING, - allowNull: false, - validate: { - reporterUsernameValid: function (value) { - const res = isVideoAbuseReporterUsernameValid(value) - if (res === false) throw new Error('Video abuse reporter username is not valid.') - } - } - }, - reason: { - type: DataTypes.STRING, - allowNull: false, - validate: { - reasonValid: function (value) { - const res = isVideoAbuseReasonValid(value) - if (res === false) throw new Error('Video abuse reason is not valid.') - } - } - } - }, - { - indexes: [ - { - fields: [ 'videoId' ] - }, - { - fields: [ 'reporterPodId' ] - } - ] - } - ) - - const classMethods = [ - associate, - - listForApi - ] - const instanceMethods = [ - toFormatedJSON - ] - addMethodsToModel(VideoAbuse, classMethods, instanceMethods) - - return VideoAbuse -} - -// ------------------------------ METHODS ------------------------------ - -function toFormatedJSON () { - let reporterPodHost - - if (this.Pod) { - reporterPodHost = this.Pod.host - } else { - // It means it's our video - reporterPodHost = CONFIG.WEBSERVER.HOST - } - - const json = { - id: this.id, - reporterPodHost, - reason: this.reason, - reporterUsername: this.reporterUsername, - videoId: this.videoId, - createdAt: this.createdAt - } - - return json -} - -// ------------------------------ STATICS ------------------------------ - -function associate (models) { - VideoAbuse.belongsTo(models.Pod, { - foreignKey: { - name: 'reporterPodId', - allowNull: true - }, - onDelete: 'cascade' - }) - - VideoAbuse.belongsTo(models.Video, { - foreignKey: { - name: 'videoId', - allowNull: false - }, - onDelete: 'cascade' - }) -} - -listForApi = function (start, count, sort, callback) { - const query = { - offset: start, - limit: count, - order: [ getSort(sort) ], - include: [ - { - model: VideoAbuse['sequelize'].models.Pod, - required: false - } - ] - } - - return VideoAbuse.findAndCountAll(query).asCallback(function (err, result) { - if (err) return callback(err) - - return callback(null, result.rows, result.count) - }) -} - - diff --git a/server/models/video-blacklist-interface.ts b/server/models/video-blacklist-interface.ts deleted file mode 100644 index 974718192..000000000 --- a/server/models/video-blacklist-interface.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as Sequelize from 'sequelize' - -// Don't use barrel, import just what we need -import { BlacklistedVideo as FormatedBlacklistedVideo } from '../../shared/models/video-blacklist.model' - -export namespace BlacklistedVideoMethods { - export type ToFormatedJSON = () => FormatedBlacklistedVideo - - export type CountTotalCallback = (err: Error, total: number) => void - export type CountTotal = (callback: CountTotalCallback) => void - - export type ListCallback = (err: Error, backlistedVideoInstances: BlacklistedVideoInstance[]) => void - export type List = (callback: ListCallback) => void - - export type ListForApiCallback = (err: Error, blacklistedVIdeoInstances?: BlacklistedVideoInstance[], total?: number) => void - export type ListForApi = (start: number, count: number, sort: string, callback: ListForApiCallback) => void - - export type LoadByIdCallback = (err: Error, blacklistedVideoInstance: BlacklistedVideoInstance) => void - export type LoadById = (id: number, callback: LoadByIdCallback) => void - - export type LoadByVideoIdCallback = (err: Error, blacklistedVideoInstance: BlacklistedVideoInstance) => void - export type LoadByVideoId = (id: string, callback: LoadByVideoIdCallback) => void -} - -export interface BlacklistedVideoClass { - toFormatedJSON: BlacklistedVideoMethods.ToFormatedJSON - countTotal: BlacklistedVideoMethods.CountTotal - list: BlacklistedVideoMethods.List - listForApi: BlacklistedVideoMethods.ListForApi - loadById: BlacklistedVideoMethods.LoadById - loadByVideoId: BlacklistedVideoMethods.LoadByVideoId -} - -export interface BlacklistedVideoAttributes { -} - -export interface BlacklistedVideoInstance extends BlacklistedVideoClass, BlacklistedVideoAttributes, Sequelize.Instance { - id: number - createdAt: Date - updatedAt: Date -} - -export interface BlacklistedVideoModel extends BlacklistedVideoClass, Sequelize.Model {} diff --git a/server/models/video-blacklist.ts b/server/models/video-blacklist.ts deleted file mode 100644 index 040ed03d7..000000000 --- a/server/models/video-blacklist.ts +++ /dev/null @@ -1,103 +0,0 @@ -import * as Sequelize from 'sequelize' - -import { addMethodsToModel, getSort } from './utils' -import { - BlacklistedVideoClass, - BlacklistedVideoInstance, - BlacklistedVideoAttributes, - - BlacklistedVideoMethods -} from './video-blacklist-interface' - -let BlacklistedVideo: Sequelize.Model -let toFormatedJSON: BlacklistedVideoMethods.ToFormatedJSON -let countTotal: BlacklistedVideoMethods.CountTotal -let list: BlacklistedVideoMethods.List -let listForApi: BlacklistedVideoMethods.ListForApi -let loadById: BlacklistedVideoMethods.LoadById -let loadByVideoId: BlacklistedVideoMethods.LoadByVideoId - -export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { - BlacklistedVideo = sequelize.define('BlacklistedVideo', - {}, - { - indexes: [ - { - fields: [ 'videoId' ], - unique: true - } - ] - } - ) - - const classMethods = [ - associate, - - countTotal, - list, - listForApi, - loadById, - loadByVideoId - ] - const instanceMethods = [ - toFormatedJSON - ] - addMethodsToModel(BlacklistedVideo, classMethods, instanceMethods) - - return BlacklistedVideo -} - -// ------------------------------ METHODS ------------------------------ - -toFormatedJSON = function () { - return { - id: this.id, - videoId: this.videoId, - createdAt: this.createdAt - } -} - -// ------------------------------ STATICS ------------------------------ - -function associate (models) { - BlacklistedVideo.belongsTo(models.Video, { - foreignKey: 'videoId', - onDelete: 'cascade' - }) -} - -countTotal = function (callback: BlacklistedVideoMethods.CountTotalCallback) { - return BlacklistedVideo.count().asCallback(callback) -} - -list = function (callback: BlacklistedVideoMethods.ListCallback) { - return BlacklistedVideo.findAll().asCallback(callback) -} - -listForApi = function (start: number, count: number, sort: string, callback: BlacklistedVideoMethods.ListForApiCallback) { - const query = { - offset: start, - limit: count, - order: [ getSort(sort) ] - } - - return BlacklistedVideo.findAndCountAll(query).asCallback(function (err, result) { - if (err) return callback(err) - - return callback(null, result.rows, result.count) - }) -} - -loadById = function (id: number, callback: BlacklistedVideoMethods.LoadByIdCallback) { - return BlacklistedVideo.findById(id).asCallback(callback) -} - -loadByVideoId = function (id: string, callback: BlacklistedVideoMethods.LoadByIdCallback) { - const query = { - where: { - videoId: id - } - } - - return BlacklistedVideo.find(query).asCallback(callback) -} diff --git a/server/models/video-interface.ts b/server/models/video-interface.ts deleted file mode 100644 index 7120f91cd..000000000 --- a/server/models/video-interface.ts +++ /dev/null @@ -1,151 +0,0 @@ -import * as Sequelize from 'sequelize' - -import { AuthorInstance } from './author-interface' -import { VideoTagInstance } from './video-tag-interface' - -// Don't use barrel, import just what we need -import { Video as FormatedVideo } from '../../shared/models/video.model' - -export type FormatedAddRemoteVideo = { - name: string - category: number - licence: number - language: number - nsfw: boolean - description: string - infoHash: string - remoteId: string - author: string - duration: number - thumbnailData: string - tags: string[] - createdAt: Date - updatedAt: Date - extname: string - views: number - likes: number - dislikes: number -} - -export type FormatedUpdateRemoteVideo = { - name: string - category: number - licence: number - language: number - nsfw: boolean - description: string - infoHash: string - remoteId: string - author: string - duration: number - tags: string[] - createdAt: Date - updatedAt: Date - extname: string - views: number - likes: number - dislikes: number -} - -export namespace VideoMethods { - export type GenerateMagnetUri = () => string - export type GetVideoFilename = () => string - export type GetThumbnailName = () => string - export type GetPreviewName = () => string - export type GetTorrentName = () => string - export type IsOwned = () => boolean - export type ToFormatedJSON = () => FormatedVideo - - export type ToAddRemoteJSONCallback = (err: Error, videoFormated?: FormatedAddRemoteVideo) => void - export type ToAddRemoteJSON = (callback: ToAddRemoteJSONCallback) => void - - export type ToUpdateRemoteJSON = () => FormatedUpdateRemoteVideo - - export type TranscodeVideofileCallback = (err: Error) => void - export type TranscodeVideofile = (callback: TranscodeVideofileCallback) => void - - export type GenerateThumbnailFromDataCallback = (err: Error, thumbnailName?: string) => void - export type GenerateThumbnailFromData = (video: VideoInstance, thumbnailData: string, callback: GenerateThumbnailFromDataCallback) => void - - export type GetDurationFromFileCallback = (err: Error, duration?: number) => void - export type GetDurationFromFile = (videoPath, callback) => void - - export type ListCallback = (err: Error, videoInstances: VideoInstance[]) => void - export type List = (callback: ListCallback) => void - - export type ListForApiCallback = (err: Error, videoInstances?: VideoInstance[], total?: number) => void - export type ListForApi = (start: number, count: number, sort: string, callback: ListForApiCallback) => void - - export type LoadByHostAndRemoteIdCallback = (err: Error, videoInstance: VideoInstance) => void - export type LoadByHostAndRemoteId = (fromHost: string, remoteId: string, callback: LoadByHostAndRemoteIdCallback) => void - - export type ListOwnedAndPopulateAuthorAndTagsCallback = (err: Error, videoInstances: VideoInstance[]) => void - export type ListOwnedAndPopulateAuthorAndTags = (callback: ListOwnedAndPopulateAuthorAndTagsCallback) => void - - export type ListOwnedByAuthorCallback = (err: Error, videoInstances: VideoInstance[]) => void - export type ListOwnedByAuthor = (author: string, callback: ListOwnedByAuthorCallback) => void - - export type LoadCallback = (err: Error, videoInstance: VideoInstance) => void - export type Load = (id: string, callback: LoadCallback) => void - - export type LoadAndPopulateAuthorCallback = (err: Error, videoInstance: VideoInstance) => void - export type LoadAndPopulateAuthor = (id: string, callback: LoadAndPopulateAuthorCallback) => void - - export type LoadAndPopulateAuthorAndPodAndTagsCallback = (err: Error, videoInstance: VideoInstance) => void - export type LoadAndPopulateAuthorAndPodAndTags = (id: string, callback: LoadAndPopulateAuthorAndPodAndTagsCallback) => void - - export type SearchAndPopulateAuthorAndPodAndTagsCallback = (err: Error, videoInstances?: VideoInstance[], total?: number) => void - export type SearchAndPopulateAuthorAndPodAndTags = (value: string, field: string, start: number, count: number, sort: string, callback: SearchAndPopulateAuthorAndPodAndTagsCallback) => void -} - -export interface VideoClass { - generateMagnetUri: VideoMethods.GenerateMagnetUri - getVideoFilename: VideoMethods.GetVideoFilename - getThumbnailName: VideoMethods.GetThumbnailName - getPreviewName: VideoMethods.GetPreviewName - getTorrentName: VideoMethods.GetTorrentName - isOwned: VideoMethods.IsOwned - toFormatedJSON: VideoMethods.ToFormatedJSON - toAddRemoteJSON: VideoMethods.ToAddRemoteJSON - toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON - transcodeVideofile: VideoMethods.TranscodeVideofile - - generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData - getDurationFromFile: VideoMethods.GetDurationFromFile - list: VideoMethods.List - listForApi: VideoMethods.ListForApi - loadByHostAndRemoteId: VideoMethods.LoadByHostAndRemoteId - listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags - listOwnedByAuthor: VideoMethods.ListOwnedByAuthor - load: VideoMethods.Load - loadAndPopulateAuthor: VideoMethods.LoadAndPopulateAuthor - loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags - searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags -} - -export interface VideoAttributes { - name: string - extname: string - remoteId: string - category: number - licence: number - language: number - nsfw: boolean - description: string - infoHash?: string - duration: number - views?: number - likes?: number - dislikes?: number - - Author?: AuthorInstance - Tags?: VideoTagInstance[] -} - -export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.Instance { - id: string - createdAt: Date - updatedAt: Date -} - -export interface VideoModel extends VideoClass, Sequelize.Model {} diff --git a/server/models/video-tag-interface.ts b/server/models/video-tag-interface.ts deleted file mode 100644 index f928cecff..000000000 --- a/server/models/video-tag-interface.ts +++ /dev/null @@ -1,18 +0,0 @@ -import * as Sequelize from 'sequelize' - -export namespace VideoTagMethods { -} - -export interface VideoTagClass { -} - -export interface VideoTagAttributes { -} - -export interface VideoTagInstance extends VideoTagClass, VideoTagAttributes, Sequelize.Instance { - id: number - createdAt: Date - updatedAt: Date -} - -export interface VideoTagModel extends VideoTagClass, Sequelize.Model {} diff --git a/server/models/video-tag.ts b/server/models/video-tag.ts deleted file mode 100644 index 514eede25..000000000 --- a/server/models/video-tag.ts +++ /dev/null @@ -1,27 +0,0 @@ -import * as Sequelize from 'sequelize' - -import { addMethodsToModel } from './utils' -import { - VideoTagClass, - VideoTagInstance, - VideoTagAttributes, - - VideoTagMethods -} from './video-tag-interface' - -let VideoTag: Sequelize.Model - -export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { - VideoTag = sequelize.define('VideoTag', {}, { - indexes: [ - { - fields: [ 'videoId' ] - }, - { - fields: [ 'tagId' ] - } - ] - }) - - return VideoTag -} diff --git a/server/models/video.ts b/server/models/video.ts deleted file mode 100644 index 78119f516..000000000 --- a/server/models/video.ts +++ /dev/null @@ -1,921 +0,0 @@ -import * as safeBuffer from 'safe-buffer' -const Buffer = safeBuffer.Buffer -import * as createTorrent from 'create-torrent' -import * as ffmpeg from 'fluent-ffmpeg' -import * as fs from 'fs' -import * as magnetUtil from 'magnet-uri' -import { map, values } from 'lodash' -import { parallel, series } from 'async' -import * as parseTorrent from 'parse-torrent' -import { join } from 'path' -import * as Sequelize from 'sequelize' - -import { database as db } from '../initializers/database' -import { VideoTagInstance } from './video-tag-interface' -import { - logger, - isVideoNameValid, - isVideoCategoryValid, - isVideoLicenceValid, - isVideoLanguageValid, - isVideoNSFWValid, - isVideoDescriptionValid, - isVideoInfoHashValid, - isVideoDurationValid -} from '../helpers' -import { - CONSTRAINTS_FIELDS, - CONFIG, - REMOTE_SCHEME, - STATIC_PATHS, - VIDEO_CATEGORIES, - VIDEO_LICENCES, - VIDEO_LANGUAGES, - THUMBNAILS_SIZE -} from '../initializers' -import { JobScheduler, removeVideoToFriends } from '../lib' - -import { addMethodsToModel, getSort } from './utils' -import { - VideoClass, - VideoInstance, - VideoAttributes, - - VideoMethods -} from './video-interface' - -let Video: Sequelize.Model -let generateMagnetUri: VideoMethods.GenerateMagnetUri -let getVideoFilename: VideoMethods.GetVideoFilename -let getThumbnailName: VideoMethods.GetThumbnailName -let getPreviewName: VideoMethods.GetPreviewName -let getTorrentName: VideoMethods.GetTorrentName -let isOwned: VideoMethods.IsOwned -let toFormatedJSON: VideoMethods.ToFormatedJSON -let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON -let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON -let transcodeVideofile: VideoMethods.TranscodeVideofile - -let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData -let getDurationFromFile: VideoMethods.GetDurationFromFile -let list: VideoMethods.List -let listForApi: VideoMethods.ListForApi -let loadByHostAndRemoteId: VideoMethods.LoadByHostAndRemoteId -let listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags -let listOwnedByAuthor: VideoMethods.ListOwnedByAuthor -let load: VideoMethods.Load -let loadAndPopulateAuthor: VideoMethods.LoadAndPopulateAuthor -let loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags -let searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags - -export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { - Video = sequelize.define('Video', - { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - validate: { - isUUID: 4 - } - }, - name: { - type: DataTypes.STRING, - allowNull: false, - validate: { - nameValid: function (value) { - const res = isVideoNameValid(value) - if (res === false) throw new Error('Video name is not valid.') - } - } - }, - extname: { - type: DataTypes.ENUM(values(CONSTRAINTS_FIELDS.VIDEOS.EXTNAME)), - allowNull: false - }, - remoteId: { - type: DataTypes.UUID, - allowNull: true, - validate: { - isUUID: 4 - } - }, - category: { - type: DataTypes.INTEGER, - allowNull: false, - validate: { - categoryValid: function (value) { - const res = isVideoCategoryValid(value) - if (res === false) throw new Error('Video category is not valid.') - } - } - }, - licence: { - type: DataTypes.INTEGER, - allowNull: false, - defaultValue: null, - validate: { - licenceValid: function (value) { - const res = isVideoLicenceValid(value) - if (res === false) throw new Error('Video licence is not valid.') - } - } - }, - language: { - type: DataTypes.INTEGER, - allowNull: true, - validate: { - languageValid: function (value) { - const res = isVideoLanguageValid(value) - if (res === false) throw new Error('Video language is not valid.') - } - } - }, - nsfw: { - type: DataTypes.BOOLEAN, - allowNull: false, - validate: { - nsfwValid: function (value) { - const res = isVideoNSFWValid(value) - if (res === false) throw new Error('Video nsfw attribute is not valid.') - } - } - }, - description: { - type: DataTypes.STRING, - allowNull: false, - validate: { - descriptionValid: function (value) { - const res = isVideoDescriptionValid(value) - if (res === false) throw new Error('Video description is not valid.') - } - } - }, - infoHash: { - type: DataTypes.STRING, - allowNull: false, - validate: { - infoHashValid: function (value) { - const res = isVideoInfoHashValid(value) - if (res === false) throw new Error('Video info hash is not valid.') - } - } - }, - duration: { - type: DataTypes.INTEGER, - allowNull: false, - validate: { - durationValid: function (value) { - const res = isVideoDurationValid(value) - if (res === false) throw new Error('Video duration is not valid.') - } - } - }, - views: { - type: DataTypes.INTEGER, - allowNull: false, - defaultValue: 0, - validate: { - min: 0, - isInt: true - } - }, - likes: { - type: DataTypes.INTEGER, - allowNull: false, - defaultValue: 0, - validate: { - min: 0, - isInt: true - } - }, - dislikes: { - type: DataTypes.INTEGER, - allowNull: false, - defaultValue: 0, - validate: { - min: 0, - isInt: true - } - } - }, - { - indexes: [ - { - fields: [ 'authorId' ] - }, - { - fields: [ 'remoteId' ] - }, - { - fields: [ 'name' ] - }, - { - fields: [ 'createdAt' ] - }, - { - fields: [ 'duration' ] - }, - { - fields: [ 'infoHash' ] - }, - { - fields: [ 'views' ] - }, - { - fields: [ 'likes' ] - } - ], - hooks: { - beforeValidate, - beforeCreate, - afterDestroy - } - } - ) - - const classMethods = [ - associate, - - generateThumbnailFromData, - getDurationFromFile, - list, - listForApi, - listOwnedAndPopulateAuthorAndTags, - listOwnedByAuthor, - load, - loadByHostAndRemoteId, - loadAndPopulateAuthor, - loadAndPopulateAuthorAndPodAndTags, - searchAndPopulateAuthorAndPodAndTags - ] - const instanceMethods = [ - generateMagnetUri, - getVideoFilename, - getThumbnailName, - getPreviewName, - getTorrentName, - isOwned, - toFormatedJSON, - toAddRemoteJSON, - toUpdateRemoteJSON, - transcodeVideofile, - removeFromBlacklist - ] - addMethodsToModel(Video, classMethods, instanceMethods) - - return Video -} - -function beforeValidate (video: VideoInstance) { - // Put a fake infoHash if it does not exists yet - if (video.isOwned() && !video.infoHash) { - // 40 hexa length - video.infoHash = '0123456789abcdef0123456789abcdef01234567' - } -} - -function beforeCreate (video: VideoInstance, options: { transaction: Sequelize.Transaction }) { - return new Promise(function (resolve, reject) { - const tasks = [] - - if (video.isOwned()) { - const videoPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename()) - - tasks.push( - function createVideoTorrent (callback) { - createTorrentFromVideo(video, videoPath, callback) - }, - - function createVideoThumbnail (callback) { - createThumbnail(video, videoPath, callback) - }, - - function createVideoPreview (callback) { - createPreview(video, videoPath, callback) - } - ) - - if (CONFIG.TRANSCODING.ENABLED === true) { - tasks.push( - function createVideoTranscoderJob (callback) { - const dataInput = { - id: video.id - } - - JobScheduler.Instance.createJob(options.transaction, 'videoTranscoder', dataInput, callback) - } - ) - } - - return parallel(tasks, function (err) { - if (err) return reject(err) - - return resolve() - }) - } - - return resolve() - }) -} - -function afterDestroy (video: VideoInstance) { - return new Promise(function (resolve, reject) { - const tasks = [] - - tasks.push( - function (callback) { - removeThumbnail(video, callback) - } - ) - - if (video.isOwned()) { - tasks.push( - function removeVideoFile (callback) { - removeFile(video, callback) - }, - - function removeVideoTorrent (callback) { - removeTorrent(video, callback) - }, - - function removeVideoPreview (callback) { - removePreview(video, callback) - }, - - function notifyFriends (callback) { - const params = { - remoteId: video.id - } - - removeVideoToFriends(params) - - return callback() - } - ) - } - - parallel(tasks, function (err) { - if (err) return reject(err) - - return resolve() - }) - }) -} - -// ------------------------------ METHODS ------------------------------ - -function associate (models) { - Video.belongsTo(models.Author, { - foreignKey: { - name: 'authorId', - allowNull: false - }, - onDelete: 'cascade' - }) - - Video.belongsToMany(models.Tag, { - foreignKey: 'videoId', - through: models.VideoTag, - onDelete: 'cascade' - }) - - Video.hasMany(models.VideoAbuse, { - foreignKey: { - name: 'videoId', - allowNull: false - }, - onDelete: 'cascade' - }) -} - -generateMagnetUri = function () { - let baseUrlHttp - let baseUrlWs - - if (this.isOwned()) { - baseUrlHttp = CONFIG.WEBSERVER.URL - baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT - } else { - baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + this.Author.Pod.host - baseUrlWs = REMOTE_SCHEME.WS + '://' + this.Author.Pod.host - } - - const xs = baseUrlHttp + STATIC_PATHS.TORRENTS + this.getTorrentName() - const announce = [ baseUrlWs + '/tracker/socket' ] - const urlList = [ baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename() ] - - const magnetHash = { - xs, - announce, - urlList, - infoHash: this.infoHash, - name: this.name - } - - return magnetUtil.encode(magnetHash) -} - -getVideoFilename = function () { - if (this.isOwned()) return this.id + this.extname - - return this.remoteId + this.extname -} - -getThumbnailName = function () { - // We always have a copy of the thumbnail - return this.id + '.jpg' -} - -getPreviewName = function () { - const extension = '.jpg' - - if (this.isOwned()) return this.id + extension - - return this.remoteId + extension -} - -getTorrentName = function () { - const extension = '.torrent' - - if (this.isOwned()) return this.id + extension - - return this.remoteId + extension -} - -isOwned = function () { - return this.remoteId === null -} - -toFormatedJSON = function (this: VideoInstance) { - let podHost - - if (this.Author.Pod) { - podHost = this.Author.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, - name: this.name, - category: this.category, - categoryLabel, - licence: this.licence, - licenceLabel, - language: this.language, - languageLabel, - nsfw: this.nsfw, - description: this.description, - podHost, - isLocal: this.isOwned(), - magnetUri: this.generateMagnetUri(), - author: this.Author.name, - duration: this.duration, - views: this.views, - likes: this.likes, - dislikes: this.dislikes, - tags: map(this.Tags, 'name'), - thumbnailPath: join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName()), - createdAt: this.createdAt, - updatedAt: this.updatedAt - } - - return json -} - -toAddRemoteJSON = function (callback: VideoMethods.ToAddRemoteJSONCallback) { - // Get thumbnail data to send to the other pod - const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName()) - fs.readFile(thumbnailPath, (err, thumbnailData) => { - if (err) { - logger.error('Cannot read the thumbnail of the video') - return callback(err) - } - - const remoteVideo = { - name: this.name, - category: this.category, - licence: this.licence, - language: this.language, - nsfw: this.nsfw, - description: this.description, - infoHash: this.infoHash, - remoteId: this.id, - author: this.Author.name, - duration: this.duration, - thumbnailData: thumbnailData.toString('binary'), - tags: map(this.Tags, 'name'), - createdAt: this.createdAt, - updatedAt: this.updatedAt, - extname: this.extname, - views: this.views, - likes: this.likes, - dislikes: this.dislikes - } - - return callback(null, remoteVideo) - }) -} - -toUpdateRemoteJSON = function () { - const json = { - name: this.name, - category: this.category, - licence: this.licence, - language: this.language, - nsfw: this.nsfw, - description: this.description, - infoHash: this.infoHash, - remoteId: this.id, - author: this.Author.name, - duration: this.duration, - tags: map(this.Tags, 'name'), - createdAt: this.createdAt, - updatedAt: this.updatedAt, - extname: this.extname, - views: this.views, - likes: this.likes, - dislikes: this.dislikes - } - - return json -} - -transcodeVideofile = function (finalCallback: VideoMethods.TranscodeVideofileCallback) { - const video = this - - const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR - const newExtname = '.mp4' - const videoInputPath = join(videosDirectory, video.getVideoFilename()) - const videoOutputPath = join(videosDirectory, video.id + '-transcoded' + newExtname) - - ffmpeg(videoInputPath) - .output(videoOutputPath) - .videoCodec('libx264') - .outputOption('-threads ' + CONFIG.TRANSCODING.THREADS) - .outputOption('-movflags faststart') - .on('error', finalCallback) - .on('end', function () { - series([ - function removeOldFile (callback) { - fs.unlink(videoInputPath, callback) - }, - - function moveNewFile (callback) { - // Important to do this before getVideoFilename() to take in account the new file extension - video.set('extname', newExtname) - - const newVideoPath = join(videosDirectory, video.getVideoFilename()) - fs.rename(videoOutputPath, newVideoPath, callback) - }, - - function torrent (callback) { - const newVideoPath = join(videosDirectory, video.getVideoFilename()) - createTorrentFromVideo(video, newVideoPath, callback) - }, - - function videoExtension (callback) { - video.save().asCallback(callback) - } - - ], function (err: Error) { - if (err) { - // Autodesctruction... - video.destroy().asCallback(function (err) { - if (err) logger.error('Cannot destruct video after transcoding failure.', { error: err }) - }) - - return finalCallback(err) - } - - return finalCallback(null) - }) - }) - .run() -} - -// ------------------------------ STATICS ------------------------------ - -generateThumbnailFromData = function (video: VideoInstance, thumbnailData: string, callback: VideoMethods.GenerateThumbnailFromDataCallback) { - // Creating the thumbnail for a remote video - - const thumbnailName = video.getThumbnailName() - const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName) - fs.writeFile(thumbnailPath, Buffer.from(thumbnailData, 'binary'), function (err) { - if (err) return callback(err) - - return callback(null, thumbnailName) - }) -} - -getDurationFromFile = function (videoPath: string, callback: VideoMethods.GetDurationFromFileCallback) { - ffmpeg.ffprobe(videoPath, function (err, metadata) { - if (err) return callback(err) - - return callback(null, Math.floor(metadata.format.duration)) - }) -} - -list = function (callback: VideoMethods.ListCallback) { - return Video.findAll().asCallback(callback) -} - -listForApi = function (start: number, count: number, sort: string, callback: VideoMethods.ListForApiCallback) { - // Exclude Blakclisted videos from the list - const query = { - distinct: true, - offset: start, - limit: count, - order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ], - include: [ - { - model: Video['sequelize'].models.Author, - include: [ { model: Video['sequelize'].models.Pod, required: false } ] - }, - - Video['sequelize'].models.Tag - ], - where: createBaseVideosWhere() - } - - return Video.findAndCountAll(query).asCallback(function (err, result) { - if (err) return callback(err) - - return callback(null, result.rows, result.count) - }) -} - -loadByHostAndRemoteId = function (fromHost: string, remoteId: string, callback: VideoMethods.LoadByHostAndRemoteIdCallback) { - const query = { - where: { - remoteId: remoteId - }, - include: [ - { - model: Video['sequelize'].models.Author, - include: [ - { - model: Video['sequelize'].models.Pod, - required: true, - where: { - host: fromHost - } - } - ] - } - ] - } - - return Video.findOne(query).asCallback(callback) -} - -listOwnedAndPopulateAuthorAndTags = function (callback: VideoMethods.ListOwnedAndPopulateAuthorAndTagsCallback) { - // If remoteId is null this is *our* video - const query = { - where: { - remoteId: null - }, - include: [ Video['sequelize'].models.Author, Video['sequelize'].models.Tag ] - } - - return Video.findAll(query).asCallback(callback) -} - -listOwnedByAuthor = function (author: string, callback: VideoMethods.ListOwnedByAuthorCallback) { - const query = { - where: { - remoteId: null - }, - include: [ - { - model: Video['sequelize'].models.Author, - where: { - name: author - } - } - ] - } - - return Video.findAll(query).asCallback(callback) -} - -load = function (id: string, callback: VideoMethods.LoadCallback) { - return Video.findById(id).asCallback(callback) -} - -loadAndPopulateAuthor = function (id: string, callback: VideoMethods.LoadAndPopulateAuthorCallback) { - const options = { - include: [ Video['sequelize'].models.Author ] - } - - return Video.findById(id, options).asCallback(callback) -} - -loadAndPopulateAuthorAndPodAndTags = function (id: string, callback: VideoMethods.LoadAndPopulateAuthorAndPodAndTagsCallback) { - const options = { - include: [ - { - model: Video['sequelize'].models.Author, - include: [ { model: Video['sequelize'].models.Pod, required: false } ] - }, - Video['sequelize'].models.Tag - ] - } - - return Video.findById(id, options).asCallback(callback) -} - -searchAndPopulateAuthorAndPodAndTags = function ( - value: string, - field: string, - start: number, - count: number, - sort: string, - callback: VideoMethods.SearchAndPopulateAuthorAndPodAndTagsCallback -) { - const podInclude: any = { - model: Video['sequelize'].models.Pod, - required: false - } - - const authorInclude: any = { - model: Video['sequelize'].models.Author, - include: [ - podInclude - ] - } - - const tagInclude: any = { - model: Video['sequelize'].models.Tag - } - - const query: any = { - distinct: true, - where: createBaseVideosWhere(), - offset: start, - limit: count, - order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ] - } - - // Make an exact search with the magnet - if (field === 'magnetUri') { - const infoHash = magnetUtil.decode(value).infoHash - query.where.infoHash = infoHash - } else if (field === 'tags') { - const escapedValue = Video['sequelize'].escape('%' + value + '%') - query.where.id.$in = Video['sequelize'].literal( - '(SELECT "VideoTags"."videoId" FROM "Tags" INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId" WHERE name LIKE ' + escapedValue + ')' - ) - } else if (field === 'host') { - // FIXME: Include our pod? (not stored in the database) - podInclude.where = { - host: { - $like: '%' + value + '%' - } - } - podInclude.required = true - } else if (field === 'author') { - authorInclude.where = { - name: { - $like: '%' + value + '%' - } - } - - // authorInclude.or = true - } else { - query.where[field] = { - $like: '%' + value + '%' - } - } - - query.include = [ - authorInclude, tagInclude - ] - - if (tagInclude.where) { - // query.include.push([ Video['sequelize'].models.Tag ]) - } - - return Video.findAndCountAll(query).asCallback(function (err, result) { - if (err) return callback(err) - - return callback(null, result.rows, result.count) - }) -} - -// --------------------------------------------------------------------------- - -function createBaseVideosWhere () { - return { - id: { - $notIn: Video['sequelize'].literal( - '(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")' - ) - } - } -} - -function removeThumbnail (video: VideoInstance, callback: (err: Error) => void) { - const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName()) - fs.unlink(thumbnailPath, callback) -} - -function removeFile (video: VideoInstance, callback: (err: Error) => void) { - const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename()) - fs.unlink(filePath, callback) -} - -function removeTorrent (video: VideoInstance, callback: (err: Error) => void) { - const torrenPath = join(CONFIG.STORAGE.TORRENTS_DIR, video.getTorrentName()) - fs.unlink(torrenPath, callback) -} - -function removePreview (video: VideoInstance, callback: (err: Error) => void) { - // Same name than video thumnail - fs.unlink(CONFIG.STORAGE.PREVIEWS_DIR + video.getPreviewName(), callback) -} - -function createTorrentFromVideo (video: VideoInstance, videoPath: string, callback: (err: Error) => void) { - const options = { - announceList: [ - [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ] - ], - urlList: [ - CONFIG.WEBSERVER.URL + STATIC_PATHS.WEBSEED + video.getVideoFilename() - ] - } - - createTorrent(videoPath, options, function (err, torrent) { - if (err) return callback(err) - - const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, video.getTorrentName()) - fs.writeFile(filePath, torrent, function (err) { - if (err) return callback(err) - - const parsedTorrent = parseTorrent(torrent) - video.set('infoHash', parsedTorrent.infoHash) - video.validate().asCallback(callback) - }) - }) -} - -function createPreview (video: VideoInstance, videoPath: string, callback: (err: Error) => void) { - generateImage(video, videoPath, CONFIG.STORAGE.PREVIEWS_DIR, video.getPreviewName(), null, callback) -} - -function createThumbnail (video: VideoInstance, videoPath: string, callback: (err: Error) => void) { - generateImage(video, videoPath, CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName(), THUMBNAILS_SIZE, callback) -} - -type GenerateImageCallback = (err: Error, imageName: string) => void -function generateImage (video: VideoInstance, videoPath: string, folder: string, imageName: string, size: string, callback?: GenerateImageCallback) { - const options: any = { - filename: imageName, - count: 1, - folder - } - - if (size) { - options.size = size - } - - ffmpeg(videoPath) - .on('error', callback) - .on('end', function () { - callback(null, imageName) - }) - .thumbnail(options) -} - -function removeFromBlacklist (video: VideoInstance, callback: (err: Error) => void) { - // Find the blacklisted video - db.BlacklistedVideo.loadByVideoId(video.id, function (err, video) { - // If an error occured, stop here - if (err) { - logger.error('Error when fetching video from blacklist.', { error: err }) - return callback(err) - } - - // If we found the video, remove it from the blacklist - if (video) { - video.destroy().asCallback(callback) - } else { - // If haven't found it, simply ignore it and do nothing - return callback(null) - } - }) -} diff --git a/server/models/video/author-interface.ts b/server/models/video/author-interface.ts new file mode 100644 index 000000000..c1b30848c --- /dev/null +++ b/server/models/video/author-interface.ts @@ -0,0 +1,27 @@ +import * as Sequelize from 'sequelize' + +import { PodInstance } from '../pod' + +export namespace AuthorMethods { + export type FindOrCreateAuthorCallback = (err: Error, authorInstance?: AuthorInstance) => void + export type FindOrCreateAuthor = (name: string, podId: number, userId: number, transaction: Sequelize.Transaction, callback: FindOrCreateAuthorCallback) => void +} + +export interface AuthorClass { + findOrCreateAuthor: AuthorMethods.FindOrCreateAuthor +} + +export interface AuthorAttributes { + name: string +} + +export interface AuthorInstance extends AuthorClass, AuthorAttributes, Sequelize.Instance { + id: number + createdAt: Date + updatedAt: Date + + podId: number + Pod: PodInstance +} + +export interface AuthorModel extends AuthorClass, Sequelize.Model {} diff --git a/server/models/video/author.ts b/server/models/video/author.ts new file mode 100644 index 000000000..4a115e328 --- /dev/null +++ b/server/models/video/author.ts @@ -0,0 +1,103 @@ +import * as Sequelize from 'sequelize' + +import { isUserUsernameValid } from '../../helpers' + +import { addMethodsToModel } from '../utils' +import { + AuthorClass, + AuthorInstance, + AuthorAttributes, + + AuthorMethods +} from './author-interface' + +let Author: Sequelize.Model +let findOrCreateAuthor: AuthorMethods.FindOrCreateAuthor + +export default function defineAuthor (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { + Author = sequelize.define('Author', + { + name: { + type: DataTypes.STRING, + allowNull: false, + validate: { + usernameValid: function (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 + } + ] + } + ) + + const classMethods = [ associate, findOrCreateAuthor ] + addMethodsToModel(Author, classMethods) + + 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' + }) +} + +findOrCreateAuthor = function ( + name: string, + podId: number, + userId: number, + transaction: Sequelize.Transaction, + callback: AuthorMethods.FindOrCreateAuthorCallback +) { + const author = { + name, + podId, + userId + } + + const query: any = { + where: author, + defaults: author + } + + if (transaction !== null) query.transaction = transaction + + Author.findOrCreate(query).asCallback(function (err, result) { + if (err) return callback(err) + + // [ instance, wasCreated ] + return callback(null, result[0]) + }) +} diff --git a/server/models/video/index.ts b/server/models/video/index.ts new file mode 100644 index 000000000..84b801c72 --- /dev/null +++ b/server/models/video/index.ts @@ -0,0 +1,6 @@ +export * from './author-interface' +export * from './tag-interface' +export * from './video-abuse-interface' +export * from './video-blacklist-interface' +export * from './video-tag-interface' +export * from './video-interface' diff --git a/server/models/video/tag-interface.ts b/server/models/video/tag-interface.ts new file mode 100644 index 000000000..e045e7ca5 --- /dev/null +++ b/server/models/video/tag-interface.ts @@ -0,0 +1,20 @@ +import * as Sequelize from 'sequelize' + +export namespace TagMethods { + export type FindOrCreateTagsCallback = (err: Error, tagInstances: TagInstance[]) => void + export type FindOrCreateTags = (tags: string[], transaction: Sequelize.Transaction, callback: FindOrCreateTagsCallback) => void +} + +export interface TagClass { + findOrCreateTags: TagMethods.FindOrCreateTags +} + +export interface TagAttributes { + name: string +} + +export interface TagInstance extends TagClass, TagAttributes, Sequelize.Instance { + id: number +} + +export interface TagModel extends TagClass, Sequelize.Model {} diff --git a/server/models/video/tag.ts b/server/models/video/tag.ts new file mode 100644 index 000000000..3c657d751 --- /dev/null +++ b/server/models/video/tag.ts @@ -0,0 +1,81 @@ +import { each } from 'async' +import * as Sequelize from 'sequelize' + +import { addMethodsToModel } from '../utils' +import { + TagClass, + TagInstance, + TagAttributes, + + TagMethods +} from './tag-interface' + +let Tag: Sequelize.Model +let findOrCreateTags: TagMethods.FindOrCreateTags + +export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { + Tag = sequelize.define('Tag', + { + name: { + type: DataTypes.STRING, + allowNull: false + } + }, + { + timestamps: false, + indexes: [ + { + fields: [ 'name' ], + unique: true + } + ] + } + ) + + const classMethods = [ + associate, + + findOrCreateTags + ] + addMethodsToModel(Tag, classMethods) + + return Tag +} + +// --------------------------------------------------------------------------- + +function associate (models) { + Tag.belongsToMany(models.Video, { + foreignKey: 'tagId', + through: models.VideoTag, + onDelete: 'cascade' + }) +} + +findOrCreateTags = function (tags: string[], transaction: Sequelize.Transaction, callback: TagMethods.FindOrCreateTagsCallback) { + const tagInstances = [] + + each(tags, function (tag, callbackEach) { + const query: any = { + where: { + name: tag + }, + defaults: { + name: tag + } + } + + if (transaction) query.transaction = transaction + + Tag.findOrCreate(query).asCallback(function (err, res) { + if (err) return callbackEach(err) + + // res = [ tag, isCreated ] + const tag = res[0] + tagInstances.push(tag) + return callbackEach() + }) + }, function (err) { + return callback(err, tagInstances) + }) +} diff --git a/server/models/video/video-abuse-interface.ts b/server/models/video/video-abuse-interface.ts new file mode 100644 index 000000000..4b7f2a2ec --- /dev/null +++ b/server/models/video/video-abuse-interface.ts @@ -0,0 +1,28 @@ +import * as Sequelize from 'sequelize' + +// Don't use barrel, import just what we need +import { VideoAbuse as FormatedVideoAbuse } from '../../../shared/models/video-abuse.model' + +export namespace VideoAbuseMethods { + export type toFormatedJSON = () => FormatedVideoAbuse + + export type ListForApiCallback = (err: Error, videoAbuseInstances?: VideoAbuseInstance[], total?: number) => void + export type ListForApi = (start: number, count: number, sort: string, callback: ListForApiCallback) => void +} + +export interface VideoAbuseClass { + listForApi: VideoAbuseMethods.ListForApi +} + +export interface VideoAbuseAttributes { + reporterUsername: string + reason: string +} + +export interface VideoAbuseInstance extends VideoAbuseClass, VideoAbuseAttributes, Sequelize.Instance { + id: number + createdAt: Date + updatedAt: Date +} + +export interface VideoAbuseModel extends VideoAbuseClass, Sequelize.Model {} diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts new file mode 100644 index 000000000..e0e0bcfe6 --- /dev/null +++ b/server/models/video/video-abuse.ts @@ -0,0 +1,131 @@ +import * as Sequelize from 'sequelize' + +import { CONFIG } from '../../initializers' +import { isVideoAbuseReporterUsernameValid, isVideoAbuseReasonValid } from '../../helpers' + +import { addMethodsToModel, getSort } from '../utils' +import { + VideoAbuseClass, + VideoAbuseInstance, + VideoAbuseAttributes, + + VideoAbuseMethods +} from './video-abuse-interface' + +let VideoAbuse: Sequelize.Model +let listForApi: VideoAbuseMethods.ListForApi + +export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { + VideoAbuse = sequelize.define('VideoAbuse', + { + reporterUsername: { + type: DataTypes.STRING, + allowNull: false, + validate: { + reporterUsernameValid: function (value) { + const res = isVideoAbuseReporterUsernameValid(value) + if (res === false) throw new Error('Video abuse reporter username is not valid.') + } + } + }, + reason: { + type: DataTypes.STRING, + allowNull: false, + validate: { + reasonValid: function (value) { + const res = isVideoAbuseReasonValid(value) + if (res === false) throw new Error('Video abuse reason is not valid.') + } + } + } + }, + { + indexes: [ + { + fields: [ 'videoId' ] + }, + { + fields: [ 'reporterPodId' ] + } + ] + } + ) + + const classMethods = [ + associate, + + listForApi + ] + const instanceMethods = [ + toFormatedJSON + ] + addMethodsToModel(VideoAbuse, classMethods, instanceMethods) + + return VideoAbuse +} + +// ------------------------------ METHODS ------------------------------ + +function toFormatedJSON () { + let reporterPodHost + + if (this.Pod) { + reporterPodHost = this.Pod.host + } else { + // It means it's our video + reporterPodHost = CONFIG.WEBSERVER.HOST + } + + const json = { + id: this.id, + reporterPodHost, + reason: this.reason, + reporterUsername: this.reporterUsername, + videoId: this.videoId, + createdAt: this.createdAt + } + + return json +} + +// ------------------------------ STATICS ------------------------------ + +function associate (models) { + VideoAbuse.belongsTo(models.Pod, { + foreignKey: { + name: 'reporterPodId', + allowNull: true + }, + onDelete: 'cascade' + }) + + VideoAbuse.belongsTo(models.Video, { + foreignKey: { + name: 'videoId', + allowNull: false + }, + onDelete: 'cascade' + }) +} + +listForApi = function (start, count, sort, callback) { + const query = { + offset: start, + limit: count, + order: [ getSort(sort) ], + include: [ + { + model: VideoAbuse['sequelize'].models.Pod, + required: false + } + ] + } + + return VideoAbuse.findAndCountAll(query).asCallback(function (err, result) { + if (err) return callback(err) + + return callback(null, result.rows, result.count) + }) +} + + diff --git a/server/models/video/video-blacklist-interface.ts b/server/models/video/video-blacklist-interface.ts new file mode 100644 index 000000000..37f579422 --- /dev/null +++ b/server/models/video/video-blacklist-interface.ts @@ -0,0 +1,43 @@ +import * as Sequelize from 'sequelize' + +// Don't use barrel, import just what we need +import { BlacklistedVideo as FormatedBlacklistedVideo } from '../../../shared/models/video-blacklist.model' + +export namespace BlacklistedVideoMethods { + export type ToFormatedJSON = () => FormatedBlacklistedVideo + + export type CountTotalCallback = (err: Error, total: number) => void + export type CountTotal = (callback: CountTotalCallback) => void + + export type ListCallback = (err: Error, backlistedVideoInstances: BlacklistedVideoInstance[]) => void + export type List = (callback: ListCallback) => void + + export type ListForApiCallback = (err: Error, blacklistedVIdeoInstances?: BlacklistedVideoInstance[], total?: number) => void + export type ListForApi = (start: number, count: number, sort: string, callback: ListForApiCallback) => void + + export type LoadByIdCallback = (err: Error, blacklistedVideoInstance: BlacklistedVideoInstance) => void + export type LoadById = (id: number, callback: LoadByIdCallback) => void + + export type LoadByVideoIdCallback = (err: Error, blacklistedVideoInstance: BlacklistedVideoInstance) => void + export type LoadByVideoId = (id: string, callback: LoadByVideoIdCallback) => void +} + +export interface BlacklistedVideoClass { + toFormatedJSON: BlacklistedVideoMethods.ToFormatedJSON + countTotal: BlacklistedVideoMethods.CountTotal + list: BlacklistedVideoMethods.List + listForApi: BlacklistedVideoMethods.ListForApi + loadById: BlacklistedVideoMethods.LoadById + loadByVideoId: BlacklistedVideoMethods.LoadByVideoId +} + +export interface BlacklistedVideoAttributes { +} + +export interface BlacklistedVideoInstance extends BlacklistedVideoClass, BlacklistedVideoAttributes, Sequelize.Instance { + id: number + createdAt: Date + updatedAt: Date +} + +export interface BlacklistedVideoModel extends BlacklistedVideoClass, Sequelize.Model {} diff --git a/server/models/video/video-blacklist.ts b/server/models/video/video-blacklist.ts new file mode 100644 index 000000000..f4479986c --- /dev/null +++ b/server/models/video/video-blacklist.ts @@ -0,0 +1,103 @@ +import * as Sequelize from 'sequelize' + +import { addMethodsToModel, getSort } from '../utils' +import { + BlacklistedVideoClass, + BlacklistedVideoInstance, + BlacklistedVideoAttributes, + + BlacklistedVideoMethods +} from './video-blacklist-interface' + +let BlacklistedVideo: Sequelize.Model +let toFormatedJSON: BlacklistedVideoMethods.ToFormatedJSON +let countTotal: BlacklistedVideoMethods.CountTotal +let list: BlacklistedVideoMethods.List +let listForApi: BlacklistedVideoMethods.ListForApi +let loadById: BlacklistedVideoMethods.LoadById +let loadByVideoId: BlacklistedVideoMethods.LoadByVideoId + +export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { + BlacklistedVideo = sequelize.define('BlacklistedVideo', + {}, + { + indexes: [ + { + fields: [ 'videoId' ], + unique: true + } + ] + } + ) + + const classMethods = [ + associate, + + countTotal, + list, + listForApi, + loadById, + loadByVideoId + ] + const instanceMethods = [ + toFormatedJSON + ] + addMethodsToModel(BlacklistedVideo, classMethods, instanceMethods) + + return BlacklistedVideo +} + +// ------------------------------ METHODS ------------------------------ + +toFormatedJSON = function () { + return { + id: this.id, + videoId: this.videoId, + createdAt: this.createdAt + } +} + +// ------------------------------ STATICS ------------------------------ + +function associate (models) { + BlacklistedVideo.belongsTo(models.Video, { + foreignKey: 'videoId', + onDelete: 'cascade' + }) +} + +countTotal = function (callback: BlacklistedVideoMethods.CountTotalCallback) { + return BlacklistedVideo.count().asCallback(callback) +} + +list = function (callback: BlacklistedVideoMethods.ListCallback) { + return BlacklistedVideo.findAll().asCallback(callback) +} + +listForApi = function (start: number, count: number, sort: string, callback: BlacklistedVideoMethods.ListForApiCallback) { + const query = { + offset: start, + limit: count, + order: [ getSort(sort) ] + } + + return BlacklistedVideo.findAndCountAll(query).asCallback(function (err, result) { + if (err) return callback(err) + + return callback(null, result.rows, result.count) + }) +} + +loadById = function (id: number, callback: BlacklistedVideoMethods.LoadByIdCallback) { + return BlacklistedVideo.findById(id).asCallback(callback) +} + +loadByVideoId = function (id: string, callback: BlacklistedVideoMethods.LoadByIdCallback) { + const query = { + where: { + videoId: id + } + } + + return BlacklistedVideo.find(query).asCallback(callback) +} diff --git a/server/models/video/video-interface.ts b/server/models/video/video-interface.ts new file mode 100644 index 000000000..71b9b0a69 --- /dev/null +++ b/server/models/video/video-interface.ts @@ -0,0 +1,151 @@ +import * as Sequelize from 'sequelize' + +import { AuthorInstance } from './author-interface' +import { VideoTagInstance } from './video-tag-interface' + +// Don't use barrel, import just what we need +import { Video as FormatedVideo } from '../../../shared/models/video.model' + +export type FormatedAddRemoteVideo = { + name: string + category: number + licence: number + language: number + nsfw: boolean + description: string + infoHash: string + remoteId: string + author: string + duration: number + thumbnailData: string + tags: string[] + createdAt: Date + updatedAt: Date + extname: string + views: number + likes: number + dislikes: number +} + +export type FormatedUpdateRemoteVideo = { + name: string + category: number + licence: number + language: number + nsfw: boolean + description: string + infoHash: string + remoteId: string + author: string + duration: number + tags: string[] + createdAt: Date + updatedAt: Date + extname: string + views: number + likes: number + dislikes: number +} + +export namespace VideoMethods { + export type GenerateMagnetUri = () => string + export type GetVideoFilename = () => string + export type GetThumbnailName = () => string + export type GetPreviewName = () => string + export type GetTorrentName = () => string + export type IsOwned = () => boolean + export type ToFormatedJSON = () => FormatedVideo + + export type ToAddRemoteJSONCallback = (err: Error, videoFormated?: FormatedAddRemoteVideo) => void + export type ToAddRemoteJSON = (callback: ToAddRemoteJSONCallback) => void + + export type ToUpdateRemoteJSON = () => FormatedUpdateRemoteVideo + + export type TranscodeVideofileCallback = (err: Error) => void + export type TranscodeVideofile = (callback: TranscodeVideofileCallback) => void + + export type GenerateThumbnailFromDataCallback = (err: Error, thumbnailName?: string) => void + export type GenerateThumbnailFromData = (video: VideoInstance, thumbnailData: string, callback: GenerateThumbnailFromDataCallback) => void + + export type GetDurationFromFileCallback = (err: Error, duration?: number) => void + export type GetDurationFromFile = (videoPath, callback) => void + + export type ListCallback = (err: Error, videoInstances: VideoInstance[]) => void + export type List = (callback: ListCallback) => void + + export type ListForApiCallback = (err: Error, videoInstances?: VideoInstance[], total?: number) => void + export type ListForApi = (start: number, count: number, sort: string, callback: ListForApiCallback) => void + + export type LoadByHostAndRemoteIdCallback = (err: Error, videoInstance: VideoInstance) => void + export type LoadByHostAndRemoteId = (fromHost: string, remoteId: string, callback: LoadByHostAndRemoteIdCallback) => void + + export type ListOwnedAndPopulateAuthorAndTagsCallback = (err: Error, videoInstances: VideoInstance[]) => void + export type ListOwnedAndPopulateAuthorAndTags = (callback: ListOwnedAndPopulateAuthorAndTagsCallback) => void + + export type ListOwnedByAuthorCallback = (err: Error, videoInstances: VideoInstance[]) => void + export type ListOwnedByAuthor = (author: string, callback: ListOwnedByAuthorCallback) => void + + export type LoadCallback = (err: Error, videoInstance: VideoInstance) => void + export type Load = (id: string, callback: LoadCallback) => void + + export type LoadAndPopulateAuthorCallback = (err: Error, videoInstance: VideoInstance) => void + export type LoadAndPopulateAuthor = (id: string, callback: LoadAndPopulateAuthorCallback) => void + + export type LoadAndPopulateAuthorAndPodAndTagsCallback = (err: Error, videoInstance: VideoInstance) => void + export type LoadAndPopulateAuthorAndPodAndTags = (id: string, callback: LoadAndPopulateAuthorAndPodAndTagsCallback) => void + + export type SearchAndPopulateAuthorAndPodAndTagsCallback = (err: Error, videoInstances?: VideoInstance[], total?: number) => void + export type SearchAndPopulateAuthorAndPodAndTags = (value: string, field: string, start: number, count: number, sort: string, callback: SearchAndPopulateAuthorAndPodAndTagsCallback) => void +} + +export interface VideoClass { + generateMagnetUri: VideoMethods.GenerateMagnetUri + getVideoFilename: VideoMethods.GetVideoFilename + getThumbnailName: VideoMethods.GetThumbnailName + getPreviewName: VideoMethods.GetPreviewName + getTorrentName: VideoMethods.GetTorrentName + isOwned: VideoMethods.IsOwned + toFormatedJSON: VideoMethods.ToFormatedJSON + toAddRemoteJSON: VideoMethods.ToAddRemoteJSON + toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON + transcodeVideofile: VideoMethods.TranscodeVideofile + + generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData + getDurationFromFile: VideoMethods.GetDurationFromFile + list: VideoMethods.List + listForApi: VideoMethods.ListForApi + loadByHostAndRemoteId: VideoMethods.LoadByHostAndRemoteId + listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags + listOwnedByAuthor: VideoMethods.ListOwnedByAuthor + load: VideoMethods.Load + loadAndPopulateAuthor: VideoMethods.LoadAndPopulateAuthor + loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags + searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags +} + +export interface VideoAttributes { + name: string + extname: string + remoteId: string + category: number + licence: number + language: number + nsfw: boolean + description: string + infoHash?: string + duration: number + views?: number + likes?: number + dislikes?: number + + Author?: AuthorInstance + Tags?: VideoTagInstance[] +} + +export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.Instance { + id: string + createdAt: Date + updatedAt: Date +} + +export interface VideoModel extends VideoClass, Sequelize.Model {} diff --git a/server/models/video/video-tag-interface.ts b/server/models/video/video-tag-interface.ts new file mode 100644 index 000000000..f928cecff --- /dev/null +++ b/server/models/video/video-tag-interface.ts @@ -0,0 +1,18 @@ +import * as Sequelize from 'sequelize' + +export namespace VideoTagMethods { +} + +export interface VideoTagClass { +} + +export interface VideoTagAttributes { +} + +export interface VideoTagInstance extends VideoTagClass, VideoTagAttributes, Sequelize.Instance { + id: number + createdAt: Date + updatedAt: Date +} + +export interface VideoTagModel extends VideoTagClass, Sequelize.Model {} diff --git a/server/models/video/video-tag.ts b/server/models/video/video-tag.ts new file mode 100644 index 000000000..71ca85332 --- /dev/null +++ b/server/models/video/video-tag.ts @@ -0,0 +1,27 @@ +import * as Sequelize from 'sequelize' + +import { addMethodsToModel } from '../utils' +import { + VideoTagClass, + VideoTagInstance, + VideoTagAttributes, + + VideoTagMethods +} from './video-tag-interface' + +let VideoTag: Sequelize.Model + +export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { + VideoTag = sequelize.define('VideoTag', {}, { + indexes: [ + { + fields: [ 'videoId' ] + }, + { + fields: [ 'tagId' ] + } + ] + }) + + return VideoTag +} diff --git a/server/models/video/video.ts b/server/models/video/video.ts new file mode 100644 index 000000000..866b380cc --- /dev/null +++ b/server/models/video/video.ts @@ -0,0 +1,921 @@ +import * as safeBuffer from 'safe-buffer' +const Buffer = safeBuffer.Buffer +import * as createTorrent from 'create-torrent' +import * as ffmpeg from 'fluent-ffmpeg' +import * as fs from 'fs' +import * as magnetUtil from 'magnet-uri' +import { map, values } from 'lodash' +import { parallel, series } from 'async' +import * as parseTorrent from 'parse-torrent' +import { join } from 'path' +import * as Sequelize from 'sequelize' + +import { database as db } from '../../initializers/database' +import { VideoTagInstance } from './video-tag-interface' +import { + logger, + isVideoNameValid, + isVideoCategoryValid, + isVideoLicenceValid, + isVideoLanguageValid, + isVideoNSFWValid, + isVideoDescriptionValid, + isVideoInfoHashValid, + isVideoDurationValid +} from '../../helpers' +import { + CONSTRAINTS_FIELDS, + CONFIG, + REMOTE_SCHEME, + STATIC_PATHS, + VIDEO_CATEGORIES, + VIDEO_LICENCES, + VIDEO_LANGUAGES, + THUMBNAILS_SIZE +} from '../../initializers' +import { JobScheduler, removeVideoToFriends } from '../../lib' + +import { addMethodsToModel, getSort } from '../utils' +import { + VideoClass, + VideoInstance, + VideoAttributes, + + VideoMethods +} from './video-interface' + +let Video: Sequelize.Model +let generateMagnetUri: VideoMethods.GenerateMagnetUri +let getVideoFilename: VideoMethods.GetVideoFilename +let getThumbnailName: VideoMethods.GetThumbnailName +let getPreviewName: VideoMethods.GetPreviewName +let getTorrentName: VideoMethods.GetTorrentName +let isOwned: VideoMethods.IsOwned +let toFormatedJSON: VideoMethods.ToFormatedJSON +let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON +let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON +let transcodeVideofile: VideoMethods.TranscodeVideofile + +let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData +let getDurationFromFile: VideoMethods.GetDurationFromFile +let list: VideoMethods.List +let listForApi: VideoMethods.ListForApi +let loadByHostAndRemoteId: VideoMethods.LoadByHostAndRemoteId +let listOwnedAndPopulateAuthorAndTags: VideoMethods.ListOwnedAndPopulateAuthorAndTags +let listOwnedByAuthor: VideoMethods.ListOwnedByAuthor +let load: VideoMethods.Load +let loadAndPopulateAuthor: VideoMethods.LoadAndPopulateAuthor +let loadAndPopulateAuthorAndPodAndTags: VideoMethods.LoadAndPopulateAuthorAndPodAndTags +let searchAndPopulateAuthorAndPodAndTags: VideoMethods.SearchAndPopulateAuthorAndPodAndTags + +export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { + Video = sequelize.define('Video', + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + validate: { + isUUID: 4 + } + }, + name: { + type: DataTypes.STRING, + allowNull: false, + validate: { + nameValid: function (value) { + const res = isVideoNameValid(value) + if (res === false) throw new Error('Video name is not valid.') + } + } + }, + extname: { + type: DataTypes.ENUM(values(CONSTRAINTS_FIELDS.VIDEOS.EXTNAME)), + allowNull: false + }, + remoteId: { + type: DataTypes.UUID, + allowNull: true, + validate: { + isUUID: 4 + } + }, + category: { + type: DataTypes.INTEGER, + allowNull: false, + validate: { + categoryValid: function (value) { + const res = isVideoCategoryValid(value) + if (res === false) throw new Error('Video category is not valid.') + } + } + }, + licence: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: null, + validate: { + licenceValid: function (value) { + const res = isVideoLicenceValid(value) + if (res === false) throw new Error('Video licence is not valid.') + } + } + }, + language: { + type: DataTypes.INTEGER, + allowNull: true, + validate: { + languageValid: function (value) { + const res = isVideoLanguageValid(value) + if (res === false) throw new Error('Video language is not valid.') + } + } + }, + nsfw: { + type: DataTypes.BOOLEAN, + allowNull: false, + validate: { + nsfwValid: function (value) { + const res = isVideoNSFWValid(value) + if (res === false) throw new Error('Video nsfw attribute is not valid.') + } + } + }, + description: { + type: DataTypes.STRING, + allowNull: false, + validate: { + descriptionValid: function (value) { + const res = isVideoDescriptionValid(value) + if (res === false) throw new Error('Video description is not valid.') + } + } + }, + infoHash: { + type: DataTypes.STRING, + allowNull: false, + validate: { + infoHashValid: function (value) { + const res = isVideoInfoHashValid(value) + if (res === false) throw new Error('Video info hash is not valid.') + } + } + }, + duration: { + type: DataTypes.INTEGER, + allowNull: false, + validate: { + durationValid: function (value) { + const res = isVideoDurationValid(value) + if (res === false) throw new Error('Video duration is not valid.') + } + } + }, + views: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 0, + validate: { + min: 0, + isInt: true + } + }, + likes: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 0, + validate: { + min: 0, + isInt: true + } + }, + dislikes: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 0, + validate: { + min: 0, + isInt: true + } + } + }, + { + indexes: [ + { + fields: [ 'authorId' ] + }, + { + fields: [ 'remoteId' ] + }, + { + fields: [ 'name' ] + }, + { + fields: [ 'createdAt' ] + }, + { + fields: [ 'duration' ] + }, + { + fields: [ 'infoHash' ] + }, + { + fields: [ 'views' ] + }, + { + fields: [ 'likes' ] + } + ], + hooks: { + beforeValidate, + beforeCreate, + afterDestroy + } + } + ) + + const classMethods = [ + associate, + + generateThumbnailFromData, + getDurationFromFile, + list, + listForApi, + listOwnedAndPopulateAuthorAndTags, + listOwnedByAuthor, + load, + loadByHostAndRemoteId, + loadAndPopulateAuthor, + loadAndPopulateAuthorAndPodAndTags, + searchAndPopulateAuthorAndPodAndTags + ] + const instanceMethods = [ + generateMagnetUri, + getVideoFilename, + getThumbnailName, + getPreviewName, + getTorrentName, + isOwned, + toFormatedJSON, + toAddRemoteJSON, + toUpdateRemoteJSON, + transcodeVideofile, + removeFromBlacklist + ] + addMethodsToModel(Video, classMethods, instanceMethods) + + return Video +} + +function beforeValidate (video: VideoInstance) { + // Put a fake infoHash if it does not exists yet + if (video.isOwned() && !video.infoHash) { + // 40 hexa length + video.infoHash = '0123456789abcdef0123456789abcdef01234567' + } +} + +function beforeCreate (video: VideoInstance, options: { transaction: Sequelize.Transaction }) { + return new Promise(function (resolve, reject) { + const tasks = [] + + if (video.isOwned()) { + const videoPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename()) + + tasks.push( + function createVideoTorrent (callback) { + createTorrentFromVideo(video, videoPath, callback) + }, + + function createVideoThumbnail (callback) { + createThumbnail(video, videoPath, callback) + }, + + function createVideoPreview (callback) { + createPreview(video, videoPath, callback) + } + ) + + if (CONFIG.TRANSCODING.ENABLED === true) { + tasks.push( + function createVideoTranscoderJob (callback) { + const dataInput = { + id: video.id + } + + JobScheduler.Instance.createJob(options.transaction, 'videoTranscoder', dataInput, callback) + } + ) + } + + return parallel(tasks, function (err) { + if (err) return reject(err) + + return resolve() + }) + } + + return resolve() + }) +} + +function afterDestroy (video: VideoInstance) { + return new Promise(function (resolve, reject) { + const tasks = [] + + tasks.push( + function (callback) { + removeThumbnail(video, callback) + } + ) + + if (video.isOwned()) { + tasks.push( + function removeVideoFile (callback) { + removeFile(video, callback) + }, + + function removeVideoTorrent (callback) { + removeTorrent(video, callback) + }, + + function removeVideoPreview (callback) { + removePreview(video, callback) + }, + + function notifyFriends (callback) { + const params = { + remoteId: video.id + } + + removeVideoToFriends(params) + + return callback() + } + ) + } + + parallel(tasks, function (err) { + if (err) return reject(err) + + return resolve() + }) + }) +} + +// ------------------------------ METHODS ------------------------------ + +function associate (models) { + Video.belongsTo(models.Author, { + foreignKey: { + name: 'authorId', + allowNull: false + }, + onDelete: 'cascade' + }) + + Video.belongsToMany(models.Tag, { + foreignKey: 'videoId', + through: models.VideoTag, + onDelete: 'cascade' + }) + + Video.hasMany(models.VideoAbuse, { + foreignKey: { + name: 'videoId', + allowNull: false + }, + onDelete: 'cascade' + }) +} + +generateMagnetUri = function () { + let baseUrlHttp + let baseUrlWs + + if (this.isOwned()) { + baseUrlHttp = CONFIG.WEBSERVER.URL + baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + } else { + baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + this.Author.Pod.host + baseUrlWs = REMOTE_SCHEME.WS + '://' + this.Author.Pod.host + } + + const xs = baseUrlHttp + STATIC_PATHS.TORRENTS + this.getTorrentName() + const announce = [ baseUrlWs + '/tracker/socket' ] + const urlList = [ baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename() ] + + const magnetHash = { + xs, + announce, + urlList, + infoHash: this.infoHash, + name: this.name + } + + return magnetUtil.encode(magnetHash) +} + +getVideoFilename = function () { + if (this.isOwned()) return this.id + this.extname + + return this.remoteId + this.extname +} + +getThumbnailName = function () { + // We always have a copy of the thumbnail + return this.id + '.jpg' +} + +getPreviewName = function () { + const extension = '.jpg' + + if (this.isOwned()) return this.id + extension + + return this.remoteId + extension +} + +getTorrentName = function () { + const extension = '.torrent' + + if (this.isOwned()) return this.id + extension + + return this.remoteId + extension +} + +isOwned = function () { + return this.remoteId === null +} + +toFormatedJSON = function (this: VideoInstance) { + let podHost + + if (this.Author.Pod) { + podHost = this.Author.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, + name: this.name, + category: this.category, + categoryLabel, + licence: this.licence, + licenceLabel, + language: this.language, + languageLabel, + nsfw: this.nsfw, + description: this.description, + podHost, + isLocal: this.isOwned(), + magnetUri: this.generateMagnetUri(), + author: this.Author.name, + duration: this.duration, + views: this.views, + likes: this.likes, + dislikes: this.dislikes, + tags: map(this.Tags, 'name'), + thumbnailPath: join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName()), + createdAt: this.createdAt, + updatedAt: this.updatedAt + } + + return json +} + +toAddRemoteJSON = function (callback: VideoMethods.ToAddRemoteJSONCallback) { + // Get thumbnail data to send to the other pod + const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName()) + fs.readFile(thumbnailPath, (err, thumbnailData) => { + if (err) { + logger.error('Cannot read the thumbnail of the video') + return callback(err) + } + + const remoteVideo = { + name: this.name, + category: this.category, + licence: this.licence, + language: this.language, + nsfw: this.nsfw, + description: this.description, + infoHash: this.infoHash, + remoteId: this.id, + author: this.Author.name, + duration: this.duration, + thumbnailData: thumbnailData.toString('binary'), + tags: map(this.Tags, 'name'), + createdAt: this.createdAt, + updatedAt: this.updatedAt, + extname: this.extname, + views: this.views, + likes: this.likes, + dislikes: this.dislikes + } + + return callback(null, remoteVideo) + }) +} + +toUpdateRemoteJSON = function () { + const json = { + name: this.name, + category: this.category, + licence: this.licence, + language: this.language, + nsfw: this.nsfw, + description: this.description, + infoHash: this.infoHash, + remoteId: this.id, + author: this.Author.name, + duration: this.duration, + tags: map(this.Tags, 'name'), + createdAt: this.createdAt, + updatedAt: this.updatedAt, + extname: this.extname, + views: this.views, + likes: this.likes, + dislikes: this.dislikes + } + + return json +} + +transcodeVideofile = function (finalCallback: VideoMethods.TranscodeVideofileCallback) { + const video = this + + const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR + const newExtname = '.mp4' + const videoInputPath = join(videosDirectory, video.getVideoFilename()) + const videoOutputPath = join(videosDirectory, video.id + '-transcoded' + newExtname) + + ffmpeg(videoInputPath) + .output(videoOutputPath) + .videoCodec('libx264') + .outputOption('-threads ' + CONFIG.TRANSCODING.THREADS) + .outputOption('-movflags faststart') + .on('error', finalCallback) + .on('end', function () { + series([ + function removeOldFile (callback) { + fs.unlink(videoInputPath, callback) + }, + + function moveNewFile (callback) { + // Important to do this before getVideoFilename() to take in account the new file extension + video.set('extname', newExtname) + + const newVideoPath = join(videosDirectory, video.getVideoFilename()) + fs.rename(videoOutputPath, newVideoPath, callback) + }, + + function torrent (callback) { + const newVideoPath = join(videosDirectory, video.getVideoFilename()) + createTorrentFromVideo(video, newVideoPath, callback) + }, + + function videoExtension (callback) { + video.save().asCallback(callback) + } + + ], function (err: Error) { + if (err) { + // Autodesctruction... + video.destroy().asCallback(function (err) { + if (err) logger.error('Cannot destruct video after transcoding failure.', { error: err }) + }) + + return finalCallback(err) + } + + return finalCallback(null) + }) + }) + .run() +} + +// ------------------------------ STATICS ------------------------------ + +generateThumbnailFromData = function (video: VideoInstance, thumbnailData: string, callback: VideoMethods.GenerateThumbnailFromDataCallback) { + // Creating the thumbnail for a remote video + + const thumbnailName = video.getThumbnailName() + const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName) + fs.writeFile(thumbnailPath, Buffer.from(thumbnailData, 'binary'), function (err) { + if (err) return callback(err) + + return callback(null, thumbnailName) + }) +} + +getDurationFromFile = function (videoPath: string, callback: VideoMethods.GetDurationFromFileCallback) { + ffmpeg.ffprobe(videoPath, function (err, metadata) { + if (err) return callback(err) + + return callback(null, Math.floor(metadata.format.duration)) + }) +} + +list = function (callback: VideoMethods.ListCallback) { + return Video.findAll().asCallback(callback) +} + +listForApi = function (start: number, count: number, sort: string, callback: VideoMethods.ListForApiCallback) { + // Exclude Blakclisted videos from the list + const query = { + distinct: true, + offset: start, + limit: count, + order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ], + include: [ + { + model: Video['sequelize'].models.Author, + include: [ { model: Video['sequelize'].models.Pod, required: false } ] + }, + + Video['sequelize'].models.Tag + ], + where: createBaseVideosWhere() + } + + return Video.findAndCountAll(query).asCallback(function (err, result) { + if (err) return callback(err) + + return callback(null, result.rows, result.count) + }) +} + +loadByHostAndRemoteId = function (fromHost: string, remoteId: string, callback: VideoMethods.LoadByHostAndRemoteIdCallback) { + const query = { + where: { + remoteId: remoteId + }, + include: [ + { + model: Video['sequelize'].models.Author, + include: [ + { + model: Video['sequelize'].models.Pod, + required: true, + where: { + host: fromHost + } + } + ] + } + ] + } + + return Video.findOne(query).asCallback(callback) +} + +listOwnedAndPopulateAuthorAndTags = function (callback: VideoMethods.ListOwnedAndPopulateAuthorAndTagsCallback) { + // If remoteId is null this is *our* video + const query = { + where: { + remoteId: null + }, + include: [ Video['sequelize'].models.Author, Video['sequelize'].models.Tag ] + } + + return Video.findAll(query).asCallback(callback) +} + +listOwnedByAuthor = function (author: string, callback: VideoMethods.ListOwnedByAuthorCallback) { + const query = { + where: { + remoteId: null + }, + include: [ + { + model: Video['sequelize'].models.Author, + where: { + name: author + } + } + ] + } + + return Video.findAll(query).asCallback(callback) +} + +load = function (id: string, callback: VideoMethods.LoadCallback) { + return Video.findById(id).asCallback(callback) +} + +loadAndPopulateAuthor = function (id: string, callback: VideoMethods.LoadAndPopulateAuthorCallback) { + const options = { + include: [ Video['sequelize'].models.Author ] + } + + return Video.findById(id, options).asCallback(callback) +} + +loadAndPopulateAuthorAndPodAndTags = function (id: string, callback: VideoMethods.LoadAndPopulateAuthorAndPodAndTagsCallback) { + const options = { + include: [ + { + model: Video['sequelize'].models.Author, + include: [ { model: Video['sequelize'].models.Pod, required: false } ] + }, + Video['sequelize'].models.Tag + ] + } + + return Video.findById(id, options).asCallback(callback) +} + +searchAndPopulateAuthorAndPodAndTags = function ( + value: string, + field: string, + start: number, + count: number, + sort: string, + callback: VideoMethods.SearchAndPopulateAuthorAndPodAndTagsCallback +) { + const podInclude: any = { + model: Video['sequelize'].models.Pod, + required: false + } + + const authorInclude: any = { + model: Video['sequelize'].models.Author, + include: [ + podInclude + ] + } + + const tagInclude: any = { + model: Video['sequelize'].models.Tag + } + + const query: any = { + distinct: true, + where: createBaseVideosWhere(), + offset: start, + limit: count, + order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ] + } + + // Make an exact search with the magnet + if (field === 'magnetUri') { + const infoHash = magnetUtil.decode(value).infoHash + query.where.infoHash = infoHash + } else if (field === 'tags') { + const escapedValue = Video['sequelize'].escape('%' + value + '%') + query.where.id.$in = Video['sequelize'].literal( + '(SELECT "VideoTags"."videoId" FROM "Tags" INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId" WHERE name LIKE ' + escapedValue + ')' + ) + } else if (field === 'host') { + // FIXME: Include our pod? (not stored in the database) + podInclude.where = { + host: { + $like: '%' + value + '%' + } + } + podInclude.required = true + } else if (field === 'author') { + authorInclude.where = { + name: { + $like: '%' + value + '%' + } + } + + // authorInclude.or = true + } else { + query.where[field] = { + $like: '%' + value + '%' + } + } + + query.include = [ + authorInclude, tagInclude + ] + + if (tagInclude.where) { + // query.include.push([ Video['sequelize'].models.Tag ]) + } + + return Video.findAndCountAll(query).asCallback(function (err, result) { + if (err) return callback(err) + + return callback(null, result.rows, result.count) + }) +} + +// --------------------------------------------------------------------------- + +function createBaseVideosWhere () { + return { + id: { + $notIn: Video['sequelize'].literal( + '(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")' + ) + } + } +} + +function removeThumbnail (video: VideoInstance, callback: (err: Error) => void) { + const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName()) + fs.unlink(thumbnailPath, callback) +} + +function removeFile (video: VideoInstance, callback: (err: Error) => void) { + const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename()) + fs.unlink(filePath, callback) +} + +function removeTorrent (video: VideoInstance, callback: (err: Error) => void) { + const torrenPath = join(CONFIG.STORAGE.TORRENTS_DIR, video.getTorrentName()) + fs.unlink(torrenPath, callback) +} + +function removePreview (video: VideoInstance, callback: (err: Error) => void) { + // Same name than video thumnail + fs.unlink(CONFIG.STORAGE.PREVIEWS_DIR + video.getPreviewName(), callback) +} + +function createTorrentFromVideo (video: VideoInstance, videoPath: string, callback: (err: Error) => void) { + const options = { + announceList: [ + [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ] + ], + urlList: [ + CONFIG.WEBSERVER.URL + STATIC_PATHS.WEBSEED + video.getVideoFilename() + ] + } + + createTorrent(videoPath, options, function (err, torrent) { + if (err) return callback(err) + + const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, video.getTorrentName()) + fs.writeFile(filePath, torrent, function (err) { + if (err) return callback(err) + + const parsedTorrent = parseTorrent(torrent) + video.set('infoHash', parsedTorrent.infoHash) + video.validate().asCallback(callback) + }) + }) +} + +function createPreview (video: VideoInstance, videoPath: string, callback: (err: Error) => void) { + generateImage(video, videoPath, CONFIG.STORAGE.PREVIEWS_DIR, video.getPreviewName(), null, callback) +} + +function createThumbnail (video: VideoInstance, videoPath: string, callback: (err: Error) => void) { + generateImage(video, videoPath, CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName(), THUMBNAILS_SIZE, callback) +} + +type GenerateImageCallback = (err: Error, imageName: string) => void +function generateImage (video: VideoInstance, videoPath: string, folder: string, imageName: string, size: string, callback?: GenerateImageCallback) { + const options: any = { + filename: imageName, + count: 1, + folder + } + + if (size) { + options.size = size + } + + ffmpeg(videoPath) + .on('error', callback) + .on('end', function () { + callback(null, imageName) + }) + .thumbnail(options) +} + +function removeFromBlacklist (video: VideoInstance, callback: (err: Error) => void) { + // Find the blacklisted video + db.BlacklistedVideo.loadByVideoId(video.id, function (err, video) { + // If an error occured, stop here + if (err) { + logger.error('Error when fetching video from blacklist.', { error: err }) + return callback(err) + } + + // If we found the video, remove it from the blacklist + if (video) { + video.destroy().asCallback(callback) + } else { + // If haven't found it, simply ignore it and do nothing + return callback(null) + } + }) +} -- cgit v1.2.3