aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2017-12-12 17:53:50 +0100
committerChocobozzz <me@florianbigard.com>2017-12-13 16:50:33 +0100
commit3fd3ab2d34d512b160a5e6084d7609be7b4f4452 (patch)
treee5ca358287fca6ecacce83defcf23af1e8e9f419 /server/models
parentc893d4514e6ecbf282c7985fe5f82b8acd8a1137 (diff)
downloadPeerTube-3fd3ab2d34d512b160a5e6084d7609be7b4f4452.tar.gz
PeerTube-3fd3ab2d34d512b160a5e6084d7609be7b4f4452.tar.zst
PeerTube-3fd3ab2d34d512b160a5e6084d7609be7b4f4452.zip
Move models to typescript-sequelize
Diffstat (limited to 'server/models')
-rw-r--r--server/models/account/account-follow-interface.ts60
-rw-r--r--server/models/account/account-follow.ts364
-rw-r--r--server/models/account/account-interface.ts76
-rw-r--r--server/models/account/account-video-rate-interface.ts31
-rw-r--r--server/models/account/account-video-rate.ts97
-rw-r--r--server/models/account/account.ts673
-rw-r--r--server/models/account/index.ts4
-rw-r--r--server/models/account/user-interface.ts67
-rw-r--r--server/models/account/user.ts460
-rw-r--r--server/models/application/application-interface.ts31
-rw-r--r--server/models/application/application.ts80
-rw-r--r--server/models/application/index.ts1
-rw-r--r--server/models/avatar/avatar-interface.ts16
-rw-r--r--server/models/avatar/avatar.ts31
-rw-r--r--server/models/avatar/index.ts1
-rw-r--r--server/models/index.ts7
-rw-r--r--server/models/job/index.ts1
-rw-r--r--server/models/job/job-interface.ts33
-rw-r--r--server/models/job/job.ts137
-rw-r--r--server/models/oauth/index.ts2
-rw-r--r--server/models/oauth/oauth-client-interface.ts31
-rw-r--r--server/models/oauth/oauth-client.ts112
-rw-r--r--server/models/oauth/oauth-token-interface.ts46
-rw-r--r--server/models/oauth/oauth-token.ts259
-rw-r--r--server/models/server/index.ts1
-rw-r--r--server/models/server/server-interface.ts24
-rw-r--r--server/models/server/server.ts183
-rw-r--r--server/models/utils.ts17
-rw-r--r--server/models/video/index.ts9
-rw-r--r--server/models/video/tag-interface.ts20
-rw-r--r--server/models/video/tag.ts105
-rw-r--r--server/models/video/video-abuse-interface.ts41
-rw-r--r--server/models/video/video-abuse.ts210
-rw-r--r--server/models/video/video-blacklist-interface.ts39
-rw-r--r--server/models/video/video-blacklist.ts142
-rw-r--r--server/models/video/video-channel-interface.ts64
-rw-r--r--server/models/video/video-channel-share-interface.ts32
-rw-r--r--server/models/video/video-channel-share.ts120
-rw-r--r--server/models/video/video-channel.ts572
-rw-r--r--server/models/video/video-file-interface.ts24
-rw-r--r--server/models/video/video-file.ts109
-rw-r--r--server/models/video/video-interface.ts150
-rw-r--r--server/models/video/video-share-interface.ts30
-rw-r--r--server/models/video/video-share.ts118
-rw-r--r--server/models/video/video-tag-interface.ts18
-rw-r--r--server/models/video/video-tag.ts43
-rw-r--r--server/models/video/video.ts1845
47 files changed, 2607 insertions, 3929 deletions
diff --git a/server/models/account/account-follow-interface.ts b/server/models/account/account-follow-interface.ts
deleted file mode 100644
index 7975a46f3..000000000
--- a/server/models/account/account-follow-interface.ts
+++ /dev/null
@@ -1,60 +0,0 @@
1import * as Bluebird from 'bluebird'
2import * as Sequelize from 'sequelize'
3import { AccountFollow, FollowState } from '../../../shared/models/accounts/follow.model'
4import { ResultList } from '../../../shared/models/result-list.model'
5import { AccountInstance } from './account-interface'
6
7export namespace AccountFollowMethods {
8 export type LoadByAccountAndTarget = (
9 accountId: number,
10 targetAccountId: number,
11 t?: Sequelize.Transaction
12 ) => Bluebird<AccountFollowInstance>
13
14 export type ListFollowingForApi = (id: number, start: number, count: number, sort: string) => Bluebird< ResultList<AccountFollowInstance>>
15 export type ListFollowersForApi = (id: number, start: number, count: number, sort: string) => Bluebird< ResultList<AccountFollowInstance>>
16
17 export type ListAcceptedFollowerUrlsForApi = (
18 accountId: number[],
19 t: Sequelize.Transaction,
20 start?: number,
21 count?: number
22 ) => Promise< ResultList<string> >
23 export type ListAcceptedFollowingUrlsForApi = (
24 accountId: number[],
25 t: Sequelize.Transaction,
26 start?: number,
27 count?: number
28 ) => Promise< ResultList<string> >
29 export type ListAcceptedFollowerSharedInboxUrls = (accountId: number[], t: Sequelize.Transaction) => Promise< ResultList<string> >
30 export type ToFormattedJSON = (this: AccountFollowInstance) => AccountFollow
31}
32
33export interface AccountFollowClass {
34 loadByAccountAndTarget: AccountFollowMethods.LoadByAccountAndTarget
35 listFollowersForApi: AccountFollowMethods.ListFollowersForApi
36 listFollowingForApi: AccountFollowMethods.ListFollowingForApi
37
38 listAcceptedFollowerUrlsForApi: AccountFollowMethods.ListAcceptedFollowerUrlsForApi
39 listAcceptedFollowingUrlsForApi: AccountFollowMethods.ListAcceptedFollowingUrlsForApi
40 listAcceptedFollowerSharedInboxUrls: AccountFollowMethods.ListAcceptedFollowerSharedInboxUrls
41}
42
43export interface AccountFollowAttributes {
44 accountId: number
45 targetAccountId: number
46 state: FollowState
47}
48
49export interface AccountFollowInstance extends AccountFollowClass, AccountFollowAttributes, Sequelize.Instance<AccountFollowAttributes> {
50 id: number
51 createdAt: Date
52 updatedAt: Date
53
54 AccountFollower?: AccountInstance
55 AccountFollowing?: AccountInstance
56
57 toFormattedJSON: AccountFollowMethods.ToFormattedJSON
58}
59
60export interface AccountFollowModel extends AccountFollowClass, Sequelize.Model<AccountFollowInstance, AccountFollowAttributes> {}
diff --git a/server/models/account/account-follow.ts b/server/models/account/account-follow.ts
index 724f37baa..975e7ee7d 100644
--- a/server/models/account/account-follow.ts
+++ b/server/models/account/account-follow.ts
@@ -1,64 +1,45 @@
1import * as Bluebird from 'bluebird'
1import { values } from 'lodash' 2import { values } from 'lodash'
2import * as Sequelize from 'sequelize' 3import * as Sequelize from 'sequelize'
3 4import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
4import { addMethodsToModel, getSort } from '../utils' 5import { FollowState } from '../../../shared/models/accounts'
5import { AccountFollowAttributes, AccountFollowInstance, AccountFollowMethods } from './account-follow-interface'
6import { FOLLOW_STATES } from '../../initializers/constants' 6import { FOLLOW_STATES } from '../../initializers/constants'
7import { ServerModel } from '../server/server'
8import { getSort } from '../utils'
9import { AccountModel } from './account'
7 10
8let AccountFollow: Sequelize.Model<AccountFollowInstance, AccountFollowAttributes> 11@Table({
9let loadByAccountAndTarget: AccountFollowMethods.LoadByAccountAndTarget 12 tableName: 'accountFollow',
10let listFollowingForApi: AccountFollowMethods.ListFollowingForApi 13 indexes: [
11let listFollowersForApi: AccountFollowMethods.ListFollowersForApi
12let listAcceptedFollowerUrlsForApi: AccountFollowMethods.ListAcceptedFollowerUrlsForApi
13let listAcceptedFollowingUrlsForApi: AccountFollowMethods.ListAcceptedFollowingUrlsForApi
14let listAcceptedFollowerSharedInboxUrls: AccountFollowMethods.ListAcceptedFollowerSharedInboxUrls
15let toFormattedJSON: AccountFollowMethods.ToFormattedJSON
16
17export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
18 AccountFollow = sequelize.define<AccountFollowInstance, AccountFollowAttributes>('AccountFollow',
19 { 14 {
20 state: { 15 fields: [ 'accountId' ]
21 type: DataTypes.ENUM(values(FOLLOW_STATES)),
22 allowNull: false
23 }
24 }, 16 },
25 { 17 {
26 indexes: [ 18 fields: [ 'targetAccountId' ]
27 { 19 },
28 fields: [ 'accountId' ] 20 {
29 }, 21 fields: [ 'accountId', 'targetAccountId' ],
30 { 22 unique: true
31 fields: [ 'targetAccountId' ]
32 },
33 {
34 fields: [ 'accountId', 'targetAccountId' ],
35 unique: true
36 }
37 ]
38 } 23 }
39 )
40
41 const classMethods = [
42 associate,
43 loadByAccountAndTarget,
44 listFollowingForApi,
45 listFollowersForApi,
46 listAcceptedFollowerUrlsForApi,
47 listAcceptedFollowingUrlsForApi,
48 listAcceptedFollowerSharedInboxUrls
49 ] 24 ]
50 const instanceMethods = [ 25})
51 toFormattedJSON 26export class AccountFollowModel extends Model<AccountFollowModel> {
52 ]
53 addMethodsToModel(AccountFollow, classMethods, instanceMethods)
54 27
55 return AccountFollow 28 @AllowNull(false)
56} 29 @Column(DataType.ENUM(values(FOLLOW_STATES)))
30 state: FollowState
57 31
58// ------------------------------ STATICS ------------------------------ 32 @CreatedAt
33 createdAt: Date
59 34
60function associate (models) { 35 @UpdatedAt
61 AccountFollow.belongsTo(models.Account, { 36 updatedAt: Date
37
38 @ForeignKey(() => AccountModel)
39 @Column
40 accountId: number
41
42 @BelongsTo(() => AccountModel, {
62 foreignKey: { 43 foreignKey: {
63 name: 'accountId', 44 name: 'accountId',
64 allowNull: false 45 allowNull: false
@@ -66,8 +47,13 @@ function associate (models) {
66 as: 'AccountFollower', 47 as: 'AccountFollower',
67 onDelete: 'CASCADE' 48 onDelete: 'CASCADE'
68 }) 49 })
50 AccountFollower: AccountModel
69 51
70 AccountFollow.belongsTo(models.Account, { 52 @ForeignKey(() => AccountModel)
53 @Column
54 targetAccountId: number
55
56 @BelongsTo(() => AccountModel, {
71 foreignKey: { 57 foreignKey: {
72 name: 'targetAccountId', 58 name: 'targetAccountId',
73 allowNull: false 59 allowNull: false
@@ -75,170 +61,168 @@ function associate (models) {
75 as: 'AccountFollowing', 61 as: 'AccountFollowing',
76 onDelete: 'CASCADE' 62 onDelete: 'CASCADE'
77 }) 63 })
78} 64 AccountFollowing: AccountModel
79 65
80toFormattedJSON = function (this: AccountFollowInstance) { 66 static loadByAccountAndTarget (accountId: number, targetAccountId: number, t?: Sequelize.Transaction) {
81 const follower = this.AccountFollower.toFormattedJSON() 67 const query = {
82 const following = this.AccountFollowing.toFormattedJSON() 68 where: {
83 69 accountId,
84 const json = { 70 targetAccountId
85 id: this.id,
86 follower,
87 following,
88 state: this.state,
89 createdAt: this.createdAt,
90 updatedAt: this.updatedAt
91 }
92
93 return json
94}
95
96loadByAccountAndTarget = function (accountId: number, targetAccountId: number, t?: Sequelize.Transaction) {
97 const query = {
98 where: {
99 accountId,
100 targetAccountId
101 },
102 include: [
103 {
104 model: AccountFollow[ 'sequelize' ].models.Account,
105 required: true,
106 as: 'AccountFollower'
107 }, 71 },
108 { 72 include: [
109 model: AccountFollow['sequelize'].models.Account, 73 {
110 required: true, 74 model: AccountModel,
111 as: 'AccountFollowing' 75 required: true,
112 } 76 as: 'AccountFollower'
113 ], 77 },
114 transaction: t 78 {
79 model: AccountModel,
80 required: true,
81 as: 'AccountFollowing'
82 }
83 ],
84 transaction: t
85 }
86
87 return AccountFollowModel.findOne(query)
115 } 88 }
116 89
117 return AccountFollow.findOne(query) 90 static listFollowingForApi (id: number, start: number, count: number, sort: string) {
118} 91 const query = {
92 distinct: true,
93 offset: start,
94 limit: count,
95 order: [ getSort(sort) ],
96 include: [
97 {
98 model: AccountModel,
99 required: true,
100 as: 'AccountFollower',
101 where: {
102 id
103 }
104 },
105 {
106 model: AccountModel,
107 as: 'AccountFollowing',
108 required: true,
109 include: [ ServerModel ]
110 }
111 ]
112 }
119 113
120listFollowingForApi = function (id: number, start: number, count: number, sort: string) { 114 return AccountFollowModel.findAndCountAll(query)
121 const query = { 115 .then(({ rows, count }) => {
122 distinct: true, 116 return {
123 offset: start, 117 data: rows,
124 limit: count, 118 total: count
125 order: [ getSort(sort) ],
126 include: [
127 {
128 model: AccountFollow[ 'sequelize' ].models.Account,
129 required: true,
130 as: 'AccountFollower',
131 where: {
132 id
133 } 119 }
134 }, 120 })
135 {
136 model: AccountFollow['sequelize'].models.Account,
137 as: 'AccountFollowing',
138 required: true,
139 include: [ AccountFollow['sequelize'].models.Server ]
140 }
141 ]
142 } 121 }
143 122
144 return AccountFollow.findAndCountAll(query).then(({ rows, count }) => { 123 static listFollowersForApi (id: number, start: number, count: number, sort: string) {
145 return { 124 const query = {
146 data: rows, 125 distinct: true,
147 total: count 126 offset: start,
127 limit: count,
128 order: [ getSort(sort) ],
129 include: [
130 {
131 model: AccountModel,
132 required: true,
133 as: 'AccountFollower',
134 include: [ ServerModel ]
135 },
136 {
137 model: AccountModel,
138 as: 'AccountFollowing',
139 required: true,
140 where: {
141 id
142 }
143 }
144 ]
148 } 145 }
149 })
150}
151 146
152listFollowersForApi = function (id: number, start: number, count: number, sort: string) { 147 return AccountFollowModel.findAndCountAll(query)
153 const query = { 148 .then(({ rows, count }) => {
154 distinct: true, 149 return {
155 offset: start, 150 data: rows,
156 limit: count, 151 total: count
157 order: [ getSort(sort) ],
158 include: [
159 {
160 model: AccountFollow[ 'sequelize' ].models.Account,
161 required: true,
162 as: 'AccountFollower',
163 include: [ AccountFollow['sequelize'].models.Server ]
164 },
165 {
166 model: AccountFollow['sequelize'].models.Account,
167 as: 'AccountFollowing',
168 required: true,
169 where: {
170 id
171 } 152 }
172 } 153 })
173 ]
174 } 154 }
175 155
176 return AccountFollow.findAndCountAll(query).then(({ rows, count }) => { 156 static listAcceptedFollowerUrlsForApi (accountIds: number[], t: Sequelize.Transaction, start?: number, count?: number) {
177 return { 157 return AccountFollowModel.createListAcceptedFollowForApiQuery('followers', accountIds, t, start, count)
178 data: rows, 158 }
179 total: count
180 }
181 })
182}
183 159
184listAcceptedFollowerUrlsForApi = function (accountIds: number[], t: Sequelize.Transaction, start?: number, count?: number) { 160 static listAcceptedFollowerSharedInboxUrls (accountIds: number[], t: Sequelize.Transaction) {
185 return createListAcceptedFollowForApiQuery('followers', accountIds, t, start, count) 161 return AccountFollowModel.createListAcceptedFollowForApiQuery('followers', accountIds, t, undefined, undefined, 'sharedInboxUrl')
186} 162 }
187 163
188listAcceptedFollowerSharedInboxUrls = function (accountIds: number[], t: Sequelize.Transaction) { 164 static listAcceptedFollowingUrlsForApi (accountIds: number[], t: Sequelize.Transaction, start?: number, count?: number) {
189 return createListAcceptedFollowForApiQuery('followers', accountIds, t, undefined, undefined, 'sharedInboxUrl') 165 return AccountFollowModel.createListAcceptedFollowForApiQuery('following', accountIds, t, start, count)
190} 166 }
191 167
192listAcceptedFollowingUrlsForApi = function (accountIds: number[], t: Sequelize.Transaction, start?: number, count?: number) { 168 private static async createListAcceptedFollowForApiQuery (type: 'followers' | 'following',
193 return createListAcceptedFollowForApiQuery('following', accountIds, t, start, count) 169 accountIds: number[],
194} 170 t: Sequelize.Transaction,
171 start?: number,
172 count?: number,
173 columnUrl = 'url') {
174 let firstJoin: string
175 let secondJoin: string
176
177 if (type === 'followers') {
178 firstJoin = 'targetAccountId'
179 secondJoin = 'accountId'
180 } else {
181 firstJoin = 'accountId'
182 secondJoin = 'targetAccountId'
183 }
195 184
196// ------------------------------ UTILS ------------------------------ 185 const selections = [ '"Follows"."' + columnUrl + '" AS "url"', 'COUNT(*) AS "total"' ]
197 186 const tasks: Bluebird<any>[] = []
198async function createListAcceptedFollowForApiQuery (
199 type: 'followers' | 'following',
200 accountIds: number[],
201 t: Sequelize.Transaction,
202 start?: number,
203 count?: number,
204 columnUrl = 'url'
205) {
206 let firstJoin: string
207 let secondJoin: string
208
209 if (type === 'followers') {
210 firstJoin = 'targetAccountId'
211 secondJoin = 'accountId'
212 } else {
213 firstJoin = 'accountId'
214 secondJoin = 'targetAccountId'
215 }
216 187
217 const selections = [ '"Follows"."' + columnUrl + '" AS "url"', 'COUNT(*) AS "total"' ] 188 for (const selection of selections) {
218 const tasks: Promise<any>[] = [] 189 let query = 'SELECT ' + selection + ' FROM "account" ' +
190 'INNER JOIN "accountFollow" ON "accountFollow"."' + firstJoin + '" = "account"."id" ' +
191 'INNER JOIN "account" AS "Follows" ON "accountFollow"."' + secondJoin + '" = "Follows"."id" ' +
192 'WHERE "account"."id" = ANY ($accountIds) AND "accountFollow"."state" = \'accepted\' '
219 193
220 for (const selection of selections) { 194 if (count !== undefined) query += 'LIMIT ' + count
221 let query = 'SELECT ' + selection + ' FROM "Accounts" ' + 195 if (start !== undefined) query += ' OFFSET ' + start
222 'INNER JOIN "AccountFollows" ON "AccountFollows"."' + firstJoin + '" = "Accounts"."id" ' +
223 'INNER JOIN "Accounts" AS "Follows" ON "AccountFollows"."' + secondJoin + '" = "Follows"."id" ' +
224 'WHERE "Accounts"."id" = ANY ($accountIds) AND "AccountFollows"."state" = \'accepted\' '
225 196
226 if (count !== undefined) query += 'LIMIT ' + count 197 const options = {
227 if (start !== undefined) query += ' OFFSET ' + start 198 bind: { accountIds },
199 type: Sequelize.QueryTypes.SELECT,
200 transaction: t
201 }
202 tasks.push(AccountFollowModel.sequelize.query(query, options))
203 }
228 204
229 const options = { 205 const [ followers, [ { total } ] ] = await
230 bind: { accountIds }, 206 Promise.all(tasks)
231 type: Sequelize.QueryTypes.SELECT, 207 const urls: string[] = followers.map(f => f.url)
232 transaction: t 208
209 return {
210 data: urls,
211 total: parseInt(total, 10)
233 } 212 }
234 tasks.push(AccountFollow['sequelize'].query(query, options))
235 } 213 }
236 214
237 const [ followers, [ { total } ]] = await Promise.all(tasks) 215 toFormattedJSON () {
238 const urls: string[] = followers.map(f => f.url) 216 const follower = this.AccountFollower.toFormattedJSON()
217 const following = this.AccountFollowing.toFormattedJSON()
239 218
240 return { 219 return {
241 data: urls, 220 id: this.id,
242 total: parseInt(total, 10) 221 follower,
222 following,
223 state: this.state,
224 createdAt: this.createdAt,
225 updatedAt: this.updatedAt
226 }
243 } 227 }
244} 228}
diff --git a/server/models/account/account-interface.ts b/server/models/account/account-interface.ts
deleted file mode 100644
index 46fe068e3..000000000
--- a/server/models/account/account-interface.ts
+++ /dev/null
@@ -1,76 +0,0 @@
1import * as Bluebird from 'bluebird'
2import * as Sequelize from 'sequelize'
3import { Account as FormattedAccount, ActivityPubActor } from '../../../shared'
4import { AvatarInstance } from '../avatar'
5import { ServerInstance } from '../server/server-interface'
6import { VideoChannelInstance } from '../video/video-channel-interface'
7
8export namespace AccountMethods {
9 export type LoadApplication = () => Bluebird<AccountInstance>
10
11 export type Load = (id: number) => Bluebird<AccountInstance>
12 export type LoadByUUID = (uuid: string) => Bluebird<AccountInstance>
13 export type LoadByUrl = (url: string, transaction?: Sequelize.Transaction) => Bluebird<AccountInstance>
14 export type LoadLocalByName = (name: string) => Bluebird<AccountInstance>
15 export type LoadByNameAndHost = (name: string, host: string) => Bluebird<AccountInstance>
16 export type ListByFollowersUrls = (followerUrls: string[], transaction: Sequelize.Transaction) => Bluebird<AccountInstance[]>
17
18 export type ToActivityPubObject = (this: AccountInstance) => ActivityPubActor
19 export type ToFormattedJSON = (this: AccountInstance) => FormattedAccount
20 export type IsOwned = (this: AccountInstance) => boolean
21 export type GetFollowerSharedInboxUrls = (this: AccountInstance, t: Sequelize.Transaction) => Bluebird<string[]>
22 export type GetFollowingUrl = (this: AccountInstance) => string
23 export type GetFollowersUrl = (this: AccountInstance) => string
24 export type GetPublicKeyUrl = (this: AccountInstance) => string
25}
26
27export interface AccountClass {
28 loadApplication: AccountMethods.LoadApplication
29 load: AccountMethods.Load
30 loadByUUID: AccountMethods.LoadByUUID
31 loadByUrl: AccountMethods.LoadByUrl
32 loadLocalByName: AccountMethods.LoadLocalByName
33 loadByNameAndHost: AccountMethods.LoadByNameAndHost
34 listByFollowersUrls: AccountMethods.ListByFollowersUrls
35}
36
37export interface AccountAttributes {
38 name: string
39 url?: string
40 publicKey: string
41 privateKey: string
42 followersCount: number
43 followingCount: number
44 inboxUrl: string
45 outboxUrl: string
46 sharedInboxUrl: string
47 followersUrl: string
48 followingUrl: string
49
50 uuid?: string
51
52 serverId?: number
53 userId?: number
54 applicationId?: number
55 avatarId?: number
56}
57
58export interface AccountInstance extends AccountClass, AccountAttributes, Sequelize.Instance<AccountAttributes> {
59 isOwned: AccountMethods.IsOwned
60 toActivityPubObject: AccountMethods.ToActivityPubObject
61 toFormattedJSON: AccountMethods.ToFormattedJSON
62 getFollowerSharedInboxUrls: AccountMethods.GetFollowerSharedInboxUrls
63 getFollowingUrl: AccountMethods.GetFollowingUrl
64 getFollowersUrl: AccountMethods.GetFollowersUrl
65 getPublicKeyUrl: AccountMethods.GetPublicKeyUrl
66
67 id: number
68 createdAt: Date
69 updatedAt: Date
70
71 Server: ServerInstance
72 VideoChannels: VideoChannelInstance[]
73 Avatar: AvatarInstance
74}
75
76export interface AccountModel extends AccountClass, Sequelize.Model<AccountInstance, AccountAttributes> {}
diff --git a/server/models/account/account-video-rate-interface.ts b/server/models/account/account-video-rate-interface.ts
deleted file mode 100644
index 1f395bc45..000000000
--- a/server/models/account/account-video-rate-interface.ts
+++ /dev/null
@@ -1,31 +0,0 @@
1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird'
3
4import { VideoRateType } from '../../../shared/models/videos/video-rate.type'
5import { AccountInstance } from './account-interface'
6
7export namespace AccountVideoRateMethods {
8 export type Load = (accountId: number, videoId: number, transaction: Sequelize.Transaction) => Promise<AccountVideoRateInstance>
9}
10
11export interface AccountVideoRateClass {
12 load: AccountVideoRateMethods.Load
13}
14
15export interface AccountVideoRateAttributes {
16 type: VideoRateType
17 accountId: number
18 videoId: number
19
20 Account?: AccountInstance
21}
22
23export interface AccountVideoRateInstance
24 extends AccountVideoRateClass, AccountVideoRateAttributes, Sequelize.Instance<AccountVideoRateAttributes> {
25 id: number
26 createdAt: Date
27 updatedAt: Date
28}
29
30export interface AccountVideoRateModel
31 extends AccountVideoRateClass, Sequelize.Model<AccountVideoRateInstance, AccountVideoRateAttributes> {}
diff --git a/server/models/account/account-video-rate.ts b/server/models/account/account-video-rate.ts
index d92834bbb..e969e4a43 100644
--- a/server/models/account/account-video-rate.ts
+++ b/server/models/account/account-video-rate.ts
@@ -1,78 +1,69 @@
1/*
2 Account rates per video.
3*/
4import { values } from 'lodash' 1import { values } from 'lodash'
5import * as Sequelize from 'sequelize' 2import { Transaction } from 'sequelize'
6 3import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
4import { IFindOptions } from 'sequelize-typescript/lib/interfaces/IFindOptions'
5import { VideoRateType } from '../../../shared/models/videos'
7import { VIDEO_RATE_TYPES } from '../../initializers' 6import { VIDEO_RATE_TYPES } from '../../initializers'
7import { VideoModel } from '../video/video'
8import { AccountModel } from './account'
8 9
9import { addMethodsToModel } from '../utils' 10/*
10import { 11 Account rates per video.
11 AccountVideoRateInstance, 12*/
12 AccountVideoRateAttributes, 13@Table({
13 14 tableName: 'accountVideoRate',
14 AccountVideoRateMethods 15 indexes: [
15} from './account-video-rate-interface'
16
17let AccountVideoRate: Sequelize.Model<AccountVideoRateInstance, AccountVideoRateAttributes>
18let load: AccountVideoRateMethods.Load
19
20export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
21 AccountVideoRate = sequelize.define<AccountVideoRateInstance, AccountVideoRateAttributes>('AccountVideoRate',
22 {
23 type: {
24 type: DataTypes.ENUM(values(VIDEO_RATE_TYPES)),
25 allowNull: false
26 }
27 },
28 { 16 {
29 indexes: [ 17 fields: [ 'videoId', 'accountId' ],
30 { 18 unique: true
31 fields: [ 'videoId', 'accountId' ],
32 unique: true
33 }
34 ]
35 } 19 }
36 ) 20 ]
21})
22export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
37 23
38 const classMethods = [ 24 @AllowNull(false)
39 associate, 25 @Column(DataType.ENUM(values(VIDEO_RATE_TYPES)))
26 type: VideoRateType
40 27
41 load 28 @CreatedAt
42 ] 29 createdAt: Date
43 addMethodsToModel(AccountVideoRate, classMethods)
44 30
45 return AccountVideoRate 31 @UpdatedAt
46} 32 updatedAt: Date
47 33
48// ------------------------------ STATICS ------------------------------ 34 @ForeignKey(() => VideoModel)
35 @Column
36 videoId: number
49 37
50function associate (models) { 38 @BelongsTo(() => VideoModel, {
51 AccountVideoRate.belongsTo(models.Video, {
52 foreignKey: { 39 foreignKey: {
53 name: 'videoId',
54 allowNull: false 40 allowNull: false
55 }, 41 },
56 onDelete: 'CASCADE' 42 onDelete: 'CASCADE'
57 }) 43 })
44 Video: VideoModel
58 45
59 AccountVideoRate.belongsTo(models.Account, { 46 @ForeignKey(() => AccountModel)
47 @Column
48 accountId: number
49
50 @BelongsTo(() => AccountModel, {
60 foreignKey: { 51 foreignKey: {
61 name: 'accountId',
62 allowNull: false 52 allowNull: false
63 }, 53 },
64 onDelete: 'CASCADE' 54 onDelete: 'CASCADE'
65 }) 55 })
66} 56 Account: AccountModel
67 57
68load = function (accountId: number, videoId: number, transaction: Sequelize.Transaction) { 58 static load (accountId: number, videoId: number, transaction: Transaction) {
69 const options: Sequelize.FindOptions<AccountVideoRateAttributes> = { 59 const options: IFindOptions<AccountVideoRateModel> = {
70 where: { 60 where: {
71 accountId, 61 accountId,
72 videoId 62 videoId
63 }
73 } 64 }
74 } 65 if (transaction) options.transaction = transaction
75 if (transaction) options.transaction = transaction
76 66
77 return AccountVideoRate.findOne(options) 67 return AccountVideoRateModel.findOne(options)
68 }
78} 69}
diff --git a/server/models/account/account.ts b/server/models/account/account.ts
index 8b0819f39..d6758fa10 100644
--- a/server/models/account/account.ts
+++ b/server/models/account/account.ts
@@ -1,253 +1,200 @@
1import { join } from 'path' 1import { join } from 'path'
2import * as Sequelize from 'sequelize' 2import * as Sequelize from 'sequelize'
3import {
4 AfterDestroy,
5 AllowNull,
6 BelongsTo,
7 Column,
8 CreatedAt,
9 DataType,
10 Default,
11 ForeignKey,
12 HasMany,
13 Is,
14 IsUUID,
15 Model,
16 Table,
17 UpdatedAt
18} from 'sequelize-typescript'
3import { Avatar } from '../../../shared/models/avatars/avatar.model' 19import { Avatar } from '../../../shared/models/avatars/avatar.model'
20import { activityPubContextify } from '../../helpers'
4import { 21import {
5 activityPubContextify,
6 isAccountFollowersCountValid, 22 isAccountFollowersCountValid,
7 isAccountFollowingCountValid, 23 isAccountFollowingCountValid,
8 isAccountPrivateKeyValid, 24 isAccountPrivateKeyValid,
9 isAccountPublicKeyValid, 25 isAccountPublicKeyValid,
10 isUserUsernameValid 26 isActivityPubUrlValid
11} from '../../helpers' 27} from '../../helpers/custom-validators/activitypub'
12import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 28import { isUserUsernameValid } from '../../helpers/custom-validators/users'
13import { AVATARS_DIR } from '../../initializers' 29import { AVATARS_DIR, CONFIG, CONSTRAINTS_FIELDS } from '../../initializers'
14import { CONFIG, CONSTRAINTS_FIELDS } from '../../initializers/constants' 30import { sendDeleteAccount } from '../../lib/activitypub/send'
15import { sendDeleteAccount } from '../../lib/activitypub/send/send-delete' 31import { ApplicationModel } from '../application/application'
16import { addMethodsToModel } from '../utils' 32import { AvatarModel } from '../avatar/avatar'
17import { AccountAttributes, AccountInstance, AccountMethods } from './account-interface' 33import { ServerModel } from '../server/server'
18 34import { throwIfNotValid } from '../utils'
19let Account: Sequelize.Model<AccountInstance, AccountAttributes> 35import { VideoChannelModel } from '../video/video-channel'
20let load: AccountMethods.Load 36import { AccountFollowModel } from './account-follow'
21let loadApplication: AccountMethods.LoadApplication 37import { UserModel } from './user'
22let loadByUUID: AccountMethods.LoadByUUID 38
23let loadByUrl: AccountMethods.LoadByUrl 39@Table({
24let loadLocalByName: AccountMethods.LoadLocalByName 40 tableName: 'account',
25let loadByNameAndHost: AccountMethods.LoadByNameAndHost 41 indexes: [
26let listByFollowersUrls: AccountMethods.ListByFollowersUrls
27let isOwned: AccountMethods.IsOwned
28let toActivityPubObject: AccountMethods.ToActivityPubObject
29let toFormattedJSON: AccountMethods.ToFormattedJSON
30let getFollowerSharedInboxUrls: AccountMethods.GetFollowerSharedInboxUrls
31let getFollowingUrl: AccountMethods.GetFollowingUrl
32let getFollowersUrl: AccountMethods.GetFollowersUrl
33let getPublicKeyUrl: AccountMethods.GetPublicKeyUrl
34
35export default function defineAccount (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
36 Account = sequelize.define<AccountInstance, AccountAttributes>('Account',
37 { 42 {
38 uuid: { 43 fields: [ 'name' ]
39 type: DataTypes.UUID,
40 defaultValue: DataTypes.UUIDV4,
41 allowNull: false,
42 validate: {
43 isUUID: 4
44 }
45 },
46 name: {
47 type: DataTypes.STRING,
48 allowNull: false,
49 validate: {
50 nameValid: value => {
51 const res = isUserUsernameValid(value)
52 if (res === false) throw new Error('Name is not valid.')
53 }
54 }
55 },
56 url: {
57 type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max),
58 allowNull: false,
59 validate: {
60 urlValid: value => {
61 const res = isActivityPubUrlValid(value)
62 if (res === false) throw new Error('URL is not valid.')
63 }
64 }
65 },
66 publicKey: {
67 type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.PUBLIC_KEY.max),
68 allowNull: true,
69 validate: {
70 publicKeyValid: value => {
71 const res = isAccountPublicKeyValid(value)
72 if (res === false) throw new Error('Public key is not valid.')
73 }
74 }
75 },
76 privateKey: {
77 type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.PRIVATE_KEY.max),
78 allowNull: true,
79 validate: {
80 privateKeyValid: value => {
81 const res = isAccountPrivateKeyValid(value)
82 if (res === false) throw new Error('Private key is not valid.')
83 }
84 }
85 },
86 followersCount: {
87 type: DataTypes.INTEGER,
88 allowNull: false,
89 validate: {
90 followersCountValid: value => {
91 const res = isAccountFollowersCountValid(value)
92 if (res === false) throw new Error('Followers count is not valid.')
93 }
94 }
95 },
96 followingCount: {
97 type: DataTypes.INTEGER,
98 allowNull: false,
99 validate: {
100 followingCountValid: value => {
101 const res = isAccountFollowingCountValid(value)
102 if (res === false) throw new Error('Following count is not valid.')
103 }
104 }
105 },
106 inboxUrl: {
107 type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max),
108 allowNull: false,
109 validate: {
110 inboxUrlValid: value => {
111 const res = isActivityPubUrlValid(value)
112 if (res === false) throw new Error('Inbox URL is not valid.')
113 }
114 }
115 },
116 outboxUrl: {
117 type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max),
118 allowNull: false,
119 validate: {
120 outboxUrlValid: value => {
121 const res = isActivityPubUrlValid(value)
122 if (res === false) throw new Error('Outbox URL is not valid.')
123 }
124 }
125 },
126 sharedInboxUrl: {
127 type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max),
128 allowNull: false,
129 validate: {
130 sharedInboxUrlValid: value => {
131 const res = isActivityPubUrlValid(value)
132 if (res === false) throw new Error('Shared inbox URL is not valid.')
133 }
134 }
135 },
136 followersUrl: {
137 type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max),
138 allowNull: false,
139 validate: {
140 followersUrlValid: value => {
141 const res = isActivityPubUrlValid(value)
142 if (res === false) throw new Error('Followers URL is not valid.')
143 }
144 }
145 },
146 followingUrl: {
147 type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max),
148 allowNull: false,
149 validate: {
150 followingUrlValid: value => {
151 const res = isActivityPubUrlValid(value)
152 if (res === false) throw new Error('Following URL is not valid.')
153 }
154 }
155 }
156 }, 44 },
157 { 45 {
158 indexes: [ 46 fields: [ 'serverId' ]
159 { 47 },
160 fields: [ 'name' ] 48 {
161 }, 49 fields: [ 'userId' ],
162 { 50 unique: true
163 fields: [ 'serverId' ] 51 },
164 }, 52 {
165 { 53 fields: [ 'applicationId' ],
166 fields: [ 'userId' ], 54 unique: true
167 unique: true 55 },
168 }, 56 {
169 { 57 fields: [ 'name', 'serverId', 'applicationId' ],
170 fields: [ 'applicationId' ], 58 unique: true
171 unique: true
172 },
173 {
174 fields: [ 'name', 'serverId', 'applicationId' ],
175 unique: true
176 }
177 ],
178 hooks: { afterDestroy }
179 } 59 }
180 )
181
182 const classMethods = [
183 associate,
184 loadApplication,
185 load,
186 loadByUUID,
187 loadByUrl,
188 loadLocalByName,
189 loadByNameAndHost,
190 listByFollowersUrls
191 ]
192 const instanceMethods = [
193 isOwned,
194 toActivityPubObject,
195 toFormattedJSON,
196 getFollowerSharedInboxUrls,
197 getFollowingUrl,
198 getFollowersUrl,
199 getPublicKeyUrl
200 ] 60 ]
201 addMethodsToModel(Account, classMethods, instanceMethods) 61})
202 62export class AccountModel extends Model<Account> {
203 return Account 63
204} 64 @AllowNull(false)
65 @Default(DataType.UUIDV4)
66 @IsUUID(4)
67 @Column(DataType.UUID)
68 uuid: string
69
70 @AllowNull(false)
71 @Is('AccountName', value => throwIfNotValid(value, isUserUsernameValid, 'account name'))
72 @Column
73 name: string
74
75 @AllowNull(false)
76 @Is('AccountUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
77 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max))
78 url: string
79
80 @AllowNull(true)
81 @Is('AccountPublicKey', value => throwIfNotValid(value, isAccountPublicKeyValid, 'public key'))
82 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.PUBLIC_KEY.max))
83 publicKey: string
84
85 @AllowNull(true)
86 @Is('AccountPublicKey', value => throwIfNotValid(value, isAccountPrivateKeyValid, 'private key'))
87 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.PRIVATE_KEY.max))
88 privateKey: string
89
90 @AllowNull(false)
91 @Is('AccountFollowersCount', value => throwIfNotValid(value, isAccountFollowersCountValid, 'followers count'))
92 @Column
93 followersCount: number
94
95 @AllowNull(false)
96 @Is('AccountFollowersCount', value => throwIfNotValid(value, isAccountFollowingCountValid, 'following count'))
97 @Column
98 followingCount: number
99
100 @AllowNull(false)
101 @Is('AccountInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'inbox url'))
102 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max))
103 inboxUrl: string
104
105 @AllowNull(false)
106 @Is('AccountOutboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'outbox url'))
107 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max))
108 outboxUrl: string
109
110 @AllowNull(false)
111 @Is('AccountSharedInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'shared inbox url'))
112 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max))
113 sharedInboxUrl: string
114
115 @AllowNull(false)
116 @Is('AccountFollowersUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'followers url'))
117 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max))
118 followersUrl: string
119
120 @AllowNull(false)
121 @Is('AccountFollowingUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'following url'))
122 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max))
123 followingUrl: string
124
125 @CreatedAt
126 createdAt: Date
127
128 @UpdatedAt
129 updatedAt: Date
130
131 @ForeignKey(() => AvatarModel)
132 @Column
133 avatarId: number
134
135 @BelongsTo(() => AvatarModel, {
136 foreignKey: {
137 allowNull: true
138 },
139 onDelete: 'cascade'
140 })
141 Avatar: AvatarModel
205 142
206// --------------------------------------------------------------------------- 143 @ForeignKey(() => ServerModel)
144 @Column
145 serverId: number
207 146
208function associate (models) { 147 @BelongsTo(() => ServerModel, {
209 Account.belongsTo(models.Server, {
210 foreignKey: { 148 foreignKey: {
211 name: 'serverId',
212 allowNull: true 149 allowNull: true
213 }, 150 },
214 onDelete: 'cascade' 151 onDelete: 'cascade'
215 }) 152 })
153 Server: ServerModel
216 154
217 Account.belongsTo(models.User, { 155 @ForeignKey(() => UserModel)
156 @Column
157 userId: number
158
159 @BelongsTo(() => UserModel, {
218 foreignKey: { 160 foreignKey: {
219 name: 'userId',
220 allowNull: true 161 allowNull: true
221 }, 162 },
222 onDelete: 'cascade' 163 onDelete: 'cascade'
223 }) 164 })
165 User: UserModel
166
167 @ForeignKey(() => ApplicationModel)
168 @Column
169 applicationId: number
224 170
225 Account.belongsTo(models.Application, { 171 @BelongsTo(() => ApplicationModel, {
226 foreignKey: { 172 foreignKey: {
227 name: 'applicationId',
228 allowNull: true 173 allowNull: true
229 }, 174 },
230 onDelete: 'cascade' 175 onDelete: 'cascade'
231 }) 176 })
177 Application: ApplicationModel
232 178
233 Account.hasMany(models.VideoChannel, { 179 @HasMany(() => VideoChannelModel, {
234 foreignKey: { 180 foreignKey: {
235 name: 'accountId',
236 allowNull: false 181 allowNull: false
237 }, 182 },
238 onDelete: 'cascade', 183 onDelete: 'cascade',
239 hooks: true 184 hooks: true
240 }) 185 })
186 VideoChannels: VideoChannelModel[]
241 187
242 Account.hasMany(models.AccountFollow, { 188 @HasMany(() => AccountFollowModel, {
243 foreignKey: { 189 foreignKey: {
244 name: 'accountId', 190 name: 'accountId',
245 allowNull: false 191 allowNull: false
246 }, 192 },
247 onDelete: 'cascade' 193 onDelete: 'cascade'
248 }) 194 })
195 AccountFollowing: AccountFollowModel[]
249 196
250 Account.hasMany(models.AccountFollow, { 197 @HasMany(() => AccountFollowModel, {
251 foreignKey: { 198 foreignKey: {
252 name: 'targetAccountId', 199 name: 'targetAccountId',
253 allowNull: false 200 allowNull: false
@@ -255,209 +202,199 @@ function associate (models) {
255 as: 'followers', 202 as: 'followers',
256 onDelete: 'cascade' 203 onDelete: 'cascade'
257 }) 204 })
205 AccountFollowers: AccountFollowModel[]
258 206
259 Account.hasOne(models.Avatar, { 207 @AfterDestroy
260 foreignKey: { 208 static sendDeleteIfOwned (instance: AccountModel) {
261 name: 'avatarId', 209 if (instance.isOwned()) {
262 allowNull: true 210 return sendDeleteAccount(instance, undefined)
263 }, 211 }
264 onDelete: 'cascade'
265 })
266}
267 212
268function afterDestroy (account: AccountInstance) { 213 return undefined
269 if (account.isOwned()) {
270 return sendDeleteAccount(account, undefined)
271 } 214 }
272 215
273 return undefined 216 static loadApplication () {
274} 217 return AccountModel.findOne({
218 include: [
219 {
220 model: ApplicationModel,
221 required: true
222 }
223 ]
224 })
225 }
275 226
276toFormattedJSON = function (this: AccountInstance) { 227 static load (id: number) {
277 let host = CONFIG.WEBSERVER.HOST 228 return AccountModel.findById(id)
278 let score: number 229 }
279 let avatar: Avatar = null
280 230
281 if (this.Avatar) { 231 static loadByUUID (uuid: string) {
282 avatar = { 232 const query = {
283 path: join(AVATARS_DIR.ACCOUNT, this.Avatar.filename), 233 where: {
284 createdAt: this.Avatar.createdAt, 234 uuid
285 updatedAt: this.Avatar.updatedAt 235 }
286 } 236 }
287 }
288 237
289 if (this.Server) { 238 return AccountModel.findOne(query)
290 host = this.Server.host
291 score = this.Server.score as number
292 } 239 }
293 240
294 const json = { 241 static loadLocalByName (name: string) {
295 id: this.id, 242 const query = {
296 uuid: this.uuid, 243 where: {
297 host, 244 name,
298 score, 245 [ Sequelize.Op.or ]: [
299 name: this.name, 246 {
300 followingCount: this.followingCount, 247 userId: {
301 followersCount: this.followersCount, 248 [ Sequelize.Op.ne ]: null
302 createdAt: this.createdAt, 249 }
303 updatedAt: this.updatedAt, 250 },
304 avatar 251 {
305 } 252 applicationId: {
253 [ Sequelize.Op.ne ]: null
254 }
255 }
256 ]
257 }
258 }
306 259
307 return json 260 return AccountModel.findOne(query)
308} 261 }
309 262
310toActivityPubObject = function (this: AccountInstance) { 263 static loadByNameAndHost (name: string, host: string) {
311 const type = this.serverId ? 'Application' as 'Application' : 'Person' as 'Person' 264 const query = {
312 265 where: {
313 const json = { 266 name
314 type, 267 },
315 id: this.url, 268 include: [
316 following: this.getFollowingUrl(), 269 {
317 followers: this.getFollowersUrl(), 270 model: ServerModel,
318 inbox: this.inboxUrl, 271 required: true,
319 outbox: this.outboxUrl, 272 where: {
320 preferredUsername: this.name, 273 host
321 url: this.url, 274 }
322 name: this.name, 275 }
323 endpoints: { 276 ]
324 sharedInbox: this.sharedInboxUrl
325 },
326 uuid: this.uuid,
327 publicKey: {
328 id: this.getPublicKeyUrl(),
329 owner: this.url,
330 publicKeyPem: this.publicKey
331 } 277 }
278
279 return AccountModel.findOne(query)
332 } 280 }
333 281
334 return activityPubContextify(json) 282 static loadByUrl (url: string, transaction?: Sequelize.Transaction) {
335} 283 const query = {
284 where: {
285 url
286 },
287 transaction
288 }
336 289
337isOwned = function (this: AccountInstance) { 290 return AccountModel.findOne(query)
338 return this.serverId === null 291 }
339}
340 292
341getFollowerSharedInboxUrls = function (this: AccountInstance, t: Sequelize.Transaction) { 293 static listByFollowersUrls (followersUrls: string[], transaction?: Sequelize.Transaction) {
342 const query: Sequelize.FindOptions<AccountAttributes> = { 294 const query = {
343 attributes: [ 'sharedInboxUrl' ], 295 where: {
344 include: [ 296 followersUrl: {
345 { 297 [ Sequelize.Op.in ]: followersUrls
346 model: Account['sequelize'].models.AccountFollow,
347 required: true,
348 as: 'followers',
349 where: {
350 targetAccountId: this.id
351 } 298 }
352 } 299 },
353 ], 300 transaction
354 transaction: t 301 }
355 }
356 302
357 return Account.findAll(query) 303 return AccountModel.findAll(query)
358 .then(accounts => accounts.map(a => a.sharedInboxUrl)) 304 }
359}
360 305
361getFollowingUrl = function (this: AccountInstance) { 306 toFormattedJSON () {
362 return this.url + '/following' 307 let host = CONFIG.WEBSERVER.HOST
363} 308 let score: number
309 let avatar: Avatar = null
364 310
365getFollowersUrl = function (this: AccountInstance) { 311 if (this.Avatar) {
366 return this.url + '/followers' 312 avatar = {
367} 313 path: join(AVATARS_DIR.ACCOUNT, this.Avatar.filename),
314 createdAt: this.Avatar.createdAt,
315 updatedAt: this.Avatar.updatedAt
316 }
317 }
368 318
369getPublicKeyUrl = function (this: AccountInstance) { 319 if (this.Server) {
370 return this.url + '#main-key' 320 host = this.Server.host
371} 321 score = this.Server.score
322 }
372 323
373// ------------------------------ STATICS ------------------------------ 324 return {
325 id: this.id,
326 uuid: this.uuid,
327 host,
328 score,
329 name: this.name,
330 followingCount: this.followingCount,
331 followersCount: this.followersCount,
332 createdAt: this.createdAt,
333 updatedAt: this.updatedAt,
334 avatar
335 }
336 }
374 337
375loadApplication = function () { 338 toActivityPubObject () {
376 return Account.findOne({ 339 const type = this.serverId ? 'Application' as 'Application' : 'Person' as 'Person'
377 include: [ 340
378 { 341 const json = {
379 model: Account['sequelize'].models.Application, 342 type,
380 required: true 343 id: this.url,
344 following: this.getFollowingUrl(),
345 followers: this.getFollowersUrl(),
346 inbox: this.inboxUrl,
347 outbox: this.outboxUrl,
348 preferredUsername: this.name,
349 url: this.url,
350 name: this.name,
351 endpoints: {
352 sharedInbox: this.sharedInboxUrl
353 },
354 uuid: this.uuid,
355 publicKey: {
356 id: this.getPublicKeyUrl(),
357 owner: this.url,
358 publicKeyPem: this.publicKey
381 } 359 }
382 ]
383 })
384}
385
386load = function (id: number) {
387 return Account.findById(id)
388}
389
390loadByUUID = function (uuid: string) {
391 const query: Sequelize.FindOptions<AccountAttributes> = {
392 where: {
393 uuid
394 } 360 }
361
362 return activityPubContextify(json)
395 } 363 }
396 364
397 return Account.findOne(query) 365 isOwned () {
398} 366 return this.serverId === null
367 }
399 368
400loadLocalByName = function (name: string) { 369 getFollowerSharedInboxUrls (t: Sequelize.Transaction) {
401 const query: Sequelize.FindOptions<AccountAttributes> = { 370 const query = {
402 where: { 371 attributes: [ 'sharedInboxUrl' ],
403 name, 372 include: [
404 [Sequelize.Op.or]: [
405 {
406 userId: {
407 [Sequelize.Op.ne]: null
408 }
409 },
410 { 373 {
411 applicationId: { 374 model: AccountFollowModel,
412 [Sequelize.Op.ne]: null 375 required: true,
376 as: 'followers',
377 where: {
378 targetAccountId: this.id
413 } 379 }
414 } 380 }
415 ] 381 ],
382 transaction: t
416 } 383 }
417 }
418 384
419 return Account.findOne(query) 385 return AccountModel.findAll(query)
420} 386 .then(accounts => accounts.map(a => a.sharedInboxUrl))
421
422loadByNameAndHost = function (name: string, host: string) {
423 const query: Sequelize.FindOptions<AccountAttributes> = {
424 where: {
425 name
426 },
427 include: [
428 {
429 model: Account['sequelize'].models.Server,
430 required: true,
431 where: {
432 host
433 }
434 }
435 ]
436 } 387 }
437 388
438 return Account.findOne(query) 389 getFollowingUrl () {
439} 390 return this.url + '/following'
440
441loadByUrl = function (url: string, transaction?: Sequelize.Transaction) {
442 const query: Sequelize.FindOptions<AccountAttributes> = {
443 where: {
444 url
445 },
446 transaction
447 } 391 }
448 392
449 return Account.findOne(query) 393 getFollowersUrl () {
450} 394 return this.url + '/followers'
451
452listByFollowersUrls = function (followersUrls: string[], transaction?: Sequelize.Transaction) {
453 const query: Sequelize.FindOptions<AccountAttributes> = {
454 where: {
455 followersUrl: {
456 [Sequelize.Op.in]: followersUrls
457 }
458 },
459 transaction
460 } 395 }
461 396
462 return Account.findAll(query) 397 getPublicKeyUrl () {
398 return this.url + '#main-key'
399 }
463} 400}
diff --git a/server/models/account/index.ts b/server/models/account/index.ts
deleted file mode 100644
index 179f66974..000000000
--- a/server/models/account/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
1export * from './account-interface'
2export * from './account-follow-interface'
3export * from './account-video-rate-interface'
4export * from './user-interface'
diff --git a/server/models/account/user-interface.ts b/server/models/account/user-interface.ts
deleted file mode 100644
index 0f0b72063..000000000
--- a/server/models/account/user-interface.ts
+++ /dev/null
@@ -1,67 +0,0 @@
1import * as Bluebird from 'bluebird'
2import * as Sequelize from 'sequelize'
3import { ResultList } from '../../../shared/models/result-list.model'
4import { UserRight } from '../../../shared/models/users/user-right.enum'
5import { UserRole } from '../../../shared/models/users/user-role'
6import { User as FormattedUser } from '../../../shared/models/users/user.model'
7import { AccountInstance } from './account-interface'
8
9export namespace UserMethods {
10 export type HasRight = (this: UserInstance, right: UserRight) => boolean
11 export type IsPasswordMatch = (this: UserInstance, password: string) => Promise<boolean>
12
13 export type ToFormattedJSON = (this: UserInstance) => FormattedUser
14 export type IsAbleToUploadVideo = (this: UserInstance, videoFile: Express.Multer.File) => Promise<boolean>
15
16 export type CountTotal = () => Bluebird<number>
17
18 export type GetByUsername = (username: string) => Bluebird<UserInstance>
19
20 export type ListForApi = (start: number, count: number, sort: string) => Bluebird< ResultList<UserInstance> >
21
22 export type LoadById = (id: number) => Bluebird<UserInstance>
23
24 export type LoadByUsername = (username: string) => Bluebird<UserInstance>
25 export type LoadByUsernameAndPopulateChannels = (username: string) => Bluebird<UserInstance>
26
27 export type LoadByUsernameOrEmail = (username: string, email: string) => Bluebird<UserInstance>
28}
29
30export interface UserClass {
31 isPasswordMatch: UserMethods.IsPasswordMatch,
32 toFormattedJSON: UserMethods.ToFormattedJSON,
33 hasRight: UserMethods.HasRight,
34 isAbleToUploadVideo: UserMethods.IsAbleToUploadVideo,
35
36 countTotal: UserMethods.CountTotal,
37 getByUsername: UserMethods.GetByUsername,
38 listForApi: UserMethods.ListForApi,
39 loadById: UserMethods.LoadById,
40 loadByUsername: UserMethods.LoadByUsername,
41 loadByUsernameAndPopulateChannels: UserMethods.LoadByUsernameAndPopulateChannels,
42 loadByUsernameOrEmail: UserMethods.LoadByUsernameOrEmail
43}
44
45export interface UserAttributes {
46 id?: number
47 password: string
48 username: string
49 email: string
50 displayNSFW?: boolean
51 role: UserRole
52 videoQuota: number
53
54 Account?: AccountInstance
55}
56
57export interface UserInstance extends UserClass, UserAttributes, Sequelize.Instance<UserAttributes> {
58 id: number
59 createdAt: Date
60 updatedAt: Date
61
62 isPasswordMatch: UserMethods.IsPasswordMatch
63 toFormattedJSON: UserMethods.ToFormattedJSON
64 hasRight: UserMethods.HasRight
65}
66
67export interface UserModel extends UserClass, Sequelize.Model<UserInstance, UserAttributes> {}
diff --git a/server/models/account/user.ts b/server/models/account/user.ts
index 3705947c0..84adad96e 100644
--- a/server/models/account/user.ts
+++ b/server/models/account/user.ts
@@ -1,301 +1,251 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import {
3 AllowNull,
4 BeforeCreate,
5 BeforeUpdate,
6 Column, CreatedAt,
7 DataType,
8 Default,
9 HasMany,
10 HasOne,
11 Is,
12 IsEmail,
13 Model,
14 Table, UpdatedAt
15} from 'sequelize-typescript'
2import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared' 16import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared'
3import { 17import {
4 comparePassword, 18 comparePassword,
5 cryptPassword, 19 cryptPassword
6 isUserDisplayNSFWValid,
7 isUserPasswordValid,
8 isUserRoleValid,
9 isUserUsernameValid,
10 isUserVideoQuotaValid
11} from '../../helpers' 20} from '../../helpers'
12import { addMethodsToModel, getSort } from '../utils' 21import {
13import { UserAttributes, UserInstance, UserMethods } from './user-interface' 22 isUserDisplayNSFWValid, isUserPasswordValid, isUserRoleValid, isUserUsernameValid,
14 23 isUserVideoQuotaValid
15let User: Sequelize.Model<UserInstance, UserAttributes> 24} from '../../helpers/custom-validators/users'
16let isPasswordMatch: UserMethods.IsPasswordMatch 25import { OAuthTokenModel } from '../oauth/oauth-token'
17let hasRight: UserMethods.HasRight 26import { getSort, throwIfNotValid } from '../utils'
18let toFormattedJSON: UserMethods.ToFormattedJSON 27import { VideoChannelModel } from '../video/video-channel'
19let countTotal: UserMethods.CountTotal 28import { AccountModel } from './account'
20let getByUsername: UserMethods.GetByUsername 29
21let listForApi: UserMethods.ListForApi 30@Table({
22let loadById: UserMethods.LoadById 31 tableName: 'user',
23let loadByUsername: UserMethods.LoadByUsername 32 indexes: [
24let loadByUsernameAndPopulateChannels: UserMethods.LoadByUsernameAndPopulateChannels
25let loadByUsernameOrEmail: UserMethods.LoadByUsernameOrEmail
26let isAbleToUploadVideo: UserMethods.IsAbleToUploadVideo
27
28export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
29 User = sequelize.define<UserInstance, UserAttributes>('User',
30 { 33 {
31 password: { 34 fields: [ 'username' ],
32 type: DataTypes.STRING, 35 unique: true
33 allowNull: false,
34 validate: {
35 passwordValid: value => {
36 const res = isUserPasswordValid(value)
37 if (res === false) throw new Error('Password not valid.')
38 }
39 }
40 },
41 username: {
42 type: DataTypes.STRING,
43 allowNull: false,
44 validate: {
45 usernameValid: value => {
46 const res = isUserUsernameValid(value)
47 if (res === false) throw new Error('Username not valid.')
48 }
49 }
50 },
51 email: {
52 type: DataTypes.STRING(400),
53 allowNull: false,
54 validate: {
55 isEmail: true
56 }
57 },
58 displayNSFW: {
59 type: DataTypes.BOOLEAN,
60 allowNull: false,
61 defaultValue: false,
62 validate: {
63 nsfwValid: value => {
64 const res = isUserDisplayNSFWValid(value)
65 if (res === false) throw new Error('Display NSFW is not valid.')
66 }
67 }
68 },
69 role: {
70 type: DataTypes.INTEGER,
71 allowNull: false,
72 validate: {
73 roleValid: value => {
74 const res = isUserRoleValid(value)
75 if (res === false) throw new Error('Role is not valid.')
76 }
77 }
78 },
79 videoQuota: {
80 type: DataTypes.BIGINT,
81 allowNull: false,
82 validate: {
83 videoQuotaValid: value => {
84 const res = isUserVideoQuotaValid(value)
85 if (res === false) throw new Error('Video quota is not valid.')
86 }
87 }
88 }
89 }, 36 },
90 { 37 {
91 indexes: [ 38 fields: [ 'email' ],
92 { 39 unique: true
93 fields: [ 'username' ],
94 unique: true
95 },
96 {
97 fields: [ 'email' ],
98 unique: true
99 }
100 ],
101 hooks: {
102 beforeCreate: beforeCreateOrUpdate,
103 beforeUpdate: beforeCreateOrUpdate
104 }
105 } 40 }
106 )
107
108 const classMethods = [
109 associate,
110
111 countTotal,
112 getByUsername,
113 listForApi,
114 loadById,
115 loadByUsername,
116 loadByUsernameAndPopulateChannels,
117 loadByUsernameOrEmail
118 ] 41 ]
119 const instanceMethods = [ 42})
120 hasRight, 43export class UserModel extends Model<UserModel> {
121 isPasswordMatch, 44
122 toFormattedJSON, 45 @AllowNull(false)
123 isAbleToUploadVideo 46 @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password'))
124 ] 47 @Column
125 addMethodsToModel(User, classMethods, instanceMethods) 48 password: string
126 49
127 return User 50 @AllowNull(false)
128} 51 @Is('UserPassword', value => throwIfNotValid(value, isUserUsernameValid, 'user name'))
52 @Column
53 username: string
54
55 @AllowNull(false)
56 @IsEmail
57 @Column(DataType.STRING(400))
58 email: string
59
60 @AllowNull(false)
61 @Default(false)
62 @Is('UserDisplayNSFW', value => throwIfNotValid(value, isUserDisplayNSFWValid, 'display NSFW boolean'))
63 @Column
64 displayNSFW: boolean
65
66 @AllowNull(false)
67 @Is('UserRole', value => throwIfNotValid(value, isUserRoleValid, 'role'))
68 @Column
69 role: number
70
71 @AllowNull(false)
72 @Is('UserVideoQuota', value => throwIfNotValid(value, isUserVideoQuotaValid, 'video quota'))
73 @Column(DataType.BIGINT)
74 videoQuota: number
75
76 @CreatedAt
77 createdAt: Date
78
79 @UpdatedAt
80 updatedAt: Date
81
82 @HasOne(() => AccountModel, {
83 foreignKey: 'userId',
84 onDelete: 'cascade'
85 })
86 Account: AccountModel
129 87
130function beforeCreateOrUpdate (user: UserInstance) { 88 @HasMany(() => OAuthTokenModel, {
131 if (user.changed('password')) { 89 foreignKey: 'userId',
132 return cryptPassword(user.password) 90 onDelete: 'cascade'
133 .then(hash => { 91 })
134 user.password = hash 92 OAuthTokens: OAuthTokenModel[]
135 return undefined 93
136 }) 94 @BeforeCreate
95 @BeforeUpdate
96 static cryptPasswordIfNeeded (instance: UserModel) {
97 if (instance.changed('password')) {
98 return cryptPassword(instance.password)
99 .then(hash => {
100 instance.password = hash
101 return undefined
102 })
103 }
137 } 104 }
138}
139
140// ------------------------------ METHODS ------------------------------
141 105
142hasRight = function (this: UserInstance, right: UserRight) { 106 static countTotal () {
143 return hasUserRight(this.role, right) 107 return this.count()
144} 108 }
145 109
146isPasswordMatch = function (this: UserInstance, password: string) { 110 static getByUsername (username: string) {
147 return comparePassword(password, this.password) 111 const query = {
148} 112 where: {
113 username: username
114 },
115 include: [ { model: AccountModel, required: true } ]
116 }
149 117
150toFormattedJSON = function (this: UserInstance) { 118 return UserModel.findOne(query)
151 const json = {
152 id: this.id,
153 username: this.username,
154 email: this.email,
155 displayNSFW: this.displayNSFW,
156 role: this.role,
157 roleLabel: USER_ROLE_LABELS[this.role],
158 videoQuota: this.videoQuota,
159 createdAt: this.createdAt,
160 account: this.Account.toFormattedJSON()
161 } 119 }
162 120
163 if (Array.isArray(this.Account.VideoChannels) === true) { 121 static listForApi (start: number, count: number, sort: string) {
164 const videoChannels = this.Account.VideoChannels 122 const query = {
165 .map(c => c.toFormattedJSON()) 123 offset: start,
166 .sort((v1, v2) => { 124 limit: count,
167 if (v1.createdAt < v2.createdAt) return -1 125 order: [ getSort(sort) ],
168 if (v1.createdAt === v2.createdAt) return 0 126 include: [ { model: AccountModel, required: true } ]
127 }
169 128
170 return 1 129 return UserModel.findAndCountAll(query)
130 .then(({ rows, count }) => {
131 return {
132 data: rows,
133 total: count
134 }
171 }) 135 })
172
173 json['videoChannels'] = videoChannels
174 } 136 }
175 137
176 return json 138 static loadById (id: number) {
177} 139 const options = {
178 140 include: [ { model: AccountModel, required: true } ]
179isAbleToUploadVideo = function (this: UserInstance, videoFile: Express.Multer.File) { 141 }
180 if (this.videoQuota === -1) return Promise.resolve(true)
181
182 return getOriginalVideoFileTotalFromUser(this).then(totalBytes => {
183 return (videoFile.size + totalBytes) < this.videoQuota
184 })
185}
186
187// ------------------------------ STATICS ------------------------------
188
189function associate (models) {
190 User.hasOne(models.Account, {
191 foreignKey: 'userId',
192 onDelete: 'cascade'
193 })
194 142
195 User.hasMany(models.OAuthToken, { 143 return UserModel.findById(id, options)
196 foreignKey: 'userId', 144 }
197 onDelete: 'cascade'
198 })
199}
200 145
201countTotal = function () { 146 static loadByUsername (username: string) {
202 return this.count() 147 const query = {
203} 148 where: {
149 username
150 },
151 include: [ { model: AccountModel, required: true } ]
152 }
204 153
205getByUsername = function (username: string) { 154 return UserModel.findOne(query)
206 const query = {
207 where: {
208 username: username
209 },
210 include: [ { model: User['sequelize'].models.Account, required: true } ]
211 } 155 }
212 156
213 return User.findOne(query) 157 static loadByUsernameAndPopulateChannels (username: string) {
214} 158 const query = {
159 where: {
160 username
161 },
162 include: [
163 {
164 model: AccountModel,
165 required: true,
166 include: [ VideoChannelModel ]
167 }
168 ]
169 }
215 170
216listForApi = function (start: number, count: number, sort: string) { 171 return UserModel.findOne(query)
217 const query = {
218 offset: start,
219 limit: count,
220 order: [ getSort(sort) ],
221 include: [ { model: User['sequelize'].models.Account, required: true } ]
222 } 172 }
223 173
224 return User.findAndCountAll(query).then(({ rows, count }) => { 174 static loadByUsernameOrEmail (username: string, email: string) {
225 return { 175 const query = {
226 data: rows, 176 include: [ { model: AccountModel, required: true } ],
227 total: count 177 where: {
178 [ Sequelize.Op.or ]: [ { username }, { email } ]
179 }
228 } 180 }
229 })
230}
231 181
232loadById = function (id: number) { 182 // FIXME: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18387
233 const options = { 183 return (UserModel as any).findOne(query)
234 include: [ { model: User['sequelize'].models.Account, required: true } ]
235 } 184 }
236 185
237 return User.findById(id, options) 186 private static getOriginalVideoFileTotalFromUser (user: UserModel) {
238} 187 // Don't use sequelize because we need to use a sub query
188 const query = 'SELECT SUM("size") AS "total" FROM ' +
189 '(SELECT MAX("videoFile"."size") AS "size" FROM "videoFile" ' +
190 'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' +
191 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
192 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
193 'INNER JOIN "user" ON "account"."userId" = "user"."id" ' +
194 'WHERE "user"."id" = $userId GROUP BY "video"."id") t'
195
196 const options = {
197 bind: { userId: user.id },
198 type: Sequelize.QueryTypes.SELECT
199 }
200 return UserModel.sequelize.query(query, options)
201 .then(([ { total } ]) => {
202 if (total === null) return 0
239 203
240loadByUsername = function (username: string) { 204 return parseInt(total, 10)
241 const query = { 205 })
242 where: {
243 username
244 },
245 include: [ { model: User['sequelize'].models.Account, required: true } ]
246 } 206 }
247 207
248 return User.findOne(query) 208 hasRight (right: UserRight) {
249} 209 return hasUserRight(this.role, right)
210 }
250 211
251loadByUsernameAndPopulateChannels = function (username: string) { 212 isPasswordMatch (password: string) {
252 const query = { 213 return comparePassword(password, this.password)
253 where: {
254 username
255 },
256 include: [
257 {
258 model: User['sequelize'].models.Account,
259 required: true,
260 include: [ User['sequelize'].models.VideoChannel ]
261 }
262 ]
263 } 214 }
264 215
265 return User.findOne(query) 216 toFormattedJSON () {
266} 217 const json = {
218 id: this.id,
219 username: this.username,
220 email: this.email,
221 displayNSFW: this.displayNSFW,
222 role: this.role,
223 roleLabel: USER_ROLE_LABELS[ this.role ],
224 videoQuota: this.videoQuota,
225 createdAt: this.createdAt,
226 account: this.Account.toFormattedJSON()
227 }
228
229 if (Array.isArray(this.Account.VideoChannels) === true) {
230 json['videoChannels'] = this.Account.VideoChannels
231 .map(c => c.toFormattedJSON())
232 .sort((v1, v2) => {
233 if (v1.createdAt < v2.createdAt) return -1
234 if (v1.createdAt === v2.createdAt) return 0
267 235
268loadByUsernameOrEmail = function (username: string, email: string) { 236 return 1
269 const query = { 237 })
270 include: [ { model: User['sequelize'].models.Account, required: true } ],
271 where: {
272 [Sequelize.Op.or]: [ { username }, { email } ]
273 } 238 }
239
240 return json
274 } 241 }
275 242
276 // FIXME: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18387 243 isAbleToUploadVideo (videoFile: Express.Multer.File) {
277 return (User as any).findOne(query) 244 if (this.videoQuota === -1) return Promise.resolve(true)
278}
279 245
280// --------------------------------------------------------------------------- 246 return UserModel.getOriginalVideoFileTotalFromUser(this)
281 247 .then(totalBytes => {
282function getOriginalVideoFileTotalFromUser (user: UserInstance) { 248 return (videoFile.size + totalBytes) < this.videoQuota
283 // Don't use sequelize because we need to use a sub query 249 })
284 const query = 'SELECT SUM("size") AS "total" FROM ' +
285 '(SELECT MAX("VideoFiles"."size") AS "size" FROM "VideoFiles" ' +
286 'INNER JOIN "Videos" ON "VideoFiles"."videoId" = "Videos"."id" ' +
287 'INNER JOIN "VideoChannels" ON "VideoChannels"."id" = "Videos"."channelId" ' +
288 'INNER JOIN "Accounts" ON "VideoChannels"."accountId" = "Accounts"."id" ' +
289 'INNER JOIN "Users" ON "Accounts"."userId" = "Users"."id" ' +
290 'WHERE "Users"."id" = $userId GROUP BY "Videos"."id") t'
291
292 const options = {
293 bind: { userId: user.id },
294 type: Sequelize.QueryTypes.SELECT
295 } 250 }
296 return User['sequelize'].query(query, options).then(([ { total } ]) => {
297 if (total === null) return 0
298
299 return parseInt(total, 10)
300 })
301} 251}
diff --git a/server/models/application/application-interface.ts b/server/models/application/application-interface.ts
deleted file mode 100644
index 2c391dba3..000000000
--- a/server/models/application/application-interface.ts
+++ /dev/null
@@ -1,31 +0,0 @@
1import * as Sequelize from 'sequelize'
2import * as Bluebird from 'bluebird'
3
4export namespace ApplicationMethods {
5 export type LoadMigrationVersion = () => Bluebird<number>
6
7 export type UpdateMigrationVersion = (
8 newVersion: number,
9 transaction: Sequelize.Transaction
10 ) => Bluebird<[ number, ApplicationInstance[] ]>
11
12 export type CountTotal = () => Bluebird<number>
13}
14
15export interface ApplicationClass {
16 loadMigrationVersion: ApplicationMethods.LoadMigrationVersion
17 updateMigrationVersion: ApplicationMethods.UpdateMigrationVersion
18 countTotal: ApplicationMethods.CountTotal
19}
20
21export interface ApplicationAttributes {
22 migrationVersion: number
23}
24
25export interface ApplicationInstance extends ApplicationClass, ApplicationAttributes, Sequelize.Instance<ApplicationAttributes> {
26 id: number
27 createdAt: Date
28 updatedAt: Date
29}
30
31export interface ApplicationModel extends ApplicationClass, Sequelize.Model<ApplicationInstance, ApplicationAttributes> {}
diff --git a/server/models/application/application.ts b/server/models/application/application.ts
index 8ba40a895..f3c0f1052 100644
--- a/server/models/application/application.ts
+++ b/server/models/application/application.ts
@@ -1,61 +1,35 @@
1import * as Sequelize from 'sequelize' 1import { Transaction } from 'sequelize'
2 2import { AllowNull, Column, Default, IsInt, Model, Table } from 'sequelize-typescript'
3import { addMethodsToModel } from '../utils' 3
4import { 4@Table({
5 ApplicationAttributes, 5 tableName: 'application'
6 ApplicationInstance, 6})
7 7export class ApplicationModel extends Model<ApplicationModel> {
8 ApplicationMethods 8
9} from './application-interface' 9 @AllowNull(false)
10 10 @Default(0)
11let Application: Sequelize.Model<ApplicationInstance, ApplicationAttributes> 11 @IsInt
12let loadMigrationVersion: ApplicationMethods.LoadMigrationVersion 12 @Column
13let updateMigrationVersion: ApplicationMethods.UpdateMigrationVersion 13 migrationVersion: number
14let countTotal: ApplicationMethods.CountTotal 14
15 static countTotal () {
16 return ApplicationModel.count()
17 }
15 18
16export default function defineApplication (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { 19 static loadMigrationVersion () {
17 Application = sequelize.define<ApplicationInstance, ApplicationAttributes>('Application', 20 const query = {
18 { 21 attributes: [ 'migrationVersion' ]
19 migrationVersion: {
20 type: DataTypes.INTEGER,
21 defaultValue: 0,
22 allowNull: false,
23 validate: {
24 isInt: true
25 }
26 }
27 } 22 }
28 )
29
30 const classMethods = [
31 countTotal,
32 loadMigrationVersion,
33 updateMigrationVersion
34 ]
35 addMethodsToModel(Application, classMethods)
36
37 return Application
38}
39 23
40// --------------------------------------------------------------------------- 24 return ApplicationModel.findOne(query).then(data => data ? data.migrationVersion : null)
41
42countTotal = function () {
43 return this.count()
44}
45
46loadMigrationVersion = function () {
47 const query = {
48 attributes: [ 'migrationVersion' ]
49 } 25 }
50 26
51 return Application.findOne(query).then(data => data ? data.migrationVersion : null) 27 static updateMigrationVersion (newVersion: number, transaction: Transaction) {
52} 28 const options = {
29 where: {},
30 transaction: transaction
31 }
53 32
54updateMigrationVersion = function (newVersion: number, transaction: Sequelize.Transaction) { 33 return ApplicationModel.update({ migrationVersion: newVersion }, options)
55 const options: Sequelize.UpdateOptions = {
56 where: {},
57 transaction: transaction
58 } 34 }
59
60 return Application.update({ migrationVersion: newVersion }, options)
61} 35}
diff --git a/server/models/application/index.ts b/server/models/application/index.ts
deleted file mode 100644
index 706f85cb9..000000000
--- a/server/models/application/index.ts
+++ /dev/null
@@ -1 +0,0 @@
1export * from './application-interface'
diff --git a/server/models/avatar/avatar-interface.ts b/server/models/avatar/avatar-interface.ts
deleted file mode 100644
index 4af2b87b7..000000000
--- a/server/models/avatar/avatar-interface.ts
+++ /dev/null
@@ -1,16 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3export namespace AvatarMethods {}
4
5export interface AvatarClass {}
6
7export interface AvatarAttributes {
8 filename: string
9}
10
11export interface AvatarInstance extends AvatarClass, AvatarAttributes, Sequelize.Instance<AvatarAttributes> {
12 createdAt: Date
13 updatedAt: Date
14}
15
16export interface AvatarModel extends AvatarClass, Sequelize.Model<AvatarInstance, AvatarAttributes> {}
diff --git a/server/models/avatar/avatar.ts b/server/models/avatar/avatar.ts
index 96308fd5f..2e7a8ae2c 100644
--- a/server/models/avatar/avatar.ts
+++ b/server/models/avatar/avatar.ts
@@ -1,24 +1,17 @@
1import * as Sequelize from 'sequelize' 1import { AllowNull, Column, CreatedAt, Model, Table, UpdatedAt } from 'sequelize-typescript'
2import { addMethodsToModel } from '../utils'
3import { AvatarAttributes, AvatarInstance } from './avatar-interface'
4 2
5let Avatar: Sequelize.Model<AvatarInstance, AvatarAttributes> 3@Table({
4 tableName: 'avatar'
5})
6export class AvatarModel extends Model<AvatarModel> {
6 7
7export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { 8 @AllowNull(false)
8 Avatar = sequelize.define<AvatarInstance, AvatarAttributes>('Avatar', 9 @Column
9 { 10 filename: string
10 filename: {
11 type: DataTypes.STRING,
12 allowNull: false
13 }
14 },
15 {}
16 )
17 11
18 const classMethods = [] 12 @CreatedAt
19 addMethodsToModel(Avatar, classMethods) 13 createdAt: Date
20 14
21 return Avatar 15 @UpdatedAt
16 updatedAt: Date
22} 17}
23
24// ------------------------------ Statics ------------------------------
diff --git a/server/models/avatar/index.ts b/server/models/avatar/index.ts
deleted file mode 100644
index 877aed1ce..000000000
--- a/server/models/avatar/index.ts
+++ /dev/null
@@ -1 +0,0 @@
1export * from './avatar-interface'
diff --git a/server/models/index.ts b/server/models/index.ts
deleted file mode 100644
index fedd97dd1..000000000
--- a/server/models/index.ts
+++ /dev/null
@@ -1,7 +0,0 @@
1export * from './application'
2export * from './avatar'
3export * from './job'
4export * from './oauth'
5export * from './server'
6export * from './account'
7export * from './video'
diff --git a/server/models/job/index.ts b/server/models/job/index.ts
deleted file mode 100644
index 56925fd32..000000000
--- a/server/models/job/index.ts
+++ /dev/null
@@ -1 +0,0 @@
1export * from './job-interface'
diff --git a/server/models/job/job-interface.ts b/server/models/job/job-interface.ts
deleted file mode 100644
index 3cfc0fbed..000000000
--- a/server/models/job/job-interface.ts
+++ /dev/null
@@ -1,33 +0,0 @@
1import * as Bluebird from 'bluebird'
2import * as Sequelize from 'sequelize'
3import { Job as FormattedJob, JobCategory, JobState } from '../../../shared/models/job.model'
4import { ResultList } from '../../../shared/models/result-list.model'
5
6export namespace JobMethods {
7 export type ListWithLimitByCategory = (limit: number, state: JobState, category: JobCategory) => Bluebird<JobInstance[]>
8 export type ListForApi = (start: number, count: number, sort: string) => Bluebird< ResultList<JobInstance> >
9
10 export type ToFormattedJSON = (this: JobInstance) => FormattedJob
11}
12
13export interface JobClass {
14 listWithLimitByCategory: JobMethods.ListWithLimitByCategory
15 listForApi: JobMethods.ListForApi,
16}
17
18export interface JobAttributes {
19 state: JobState
20 category: JobCategory
21 handlerName: string
22 handlerInputData: any
23}
24
25export interface JobInstance extends JobClass, JobAttributes, Sequelize.Instance<JobAttributes> {
26 id: number
27 createdAt: Date
28 updatedAt: Date
29
30 toFormattedJSON: JobMethods.ToFormattedJSON
31}
32
33export interface JobModel extends JobClass, Sequelize.Model<JobInstance, JobAttributes> {}
diff --git a/server/models/job/job.ts b/server/models/job/job.ts
index f428e26db..35c357e69 100644
--- a/server/models/job/job.ts
+++ b/server/models/job/job.ts
@@ -1,96 +1,79 @@
1import { values } from 'lodash' 1import { values } from 'lodash'
2import * as Sequelize from 'sequelize' 2import { AllowNull, Column, CreatedAt, DataType, Model, Table, UpdatedAt } from 'sequelize-typescript'
3import { JobCategory, JobState } from '../../../shared/models/job.model' 3import { JobCategory, JobState } from '../../../shared/models'
4import { JOB_CATEGORIES, JOB_STATES } from '../../initializers' 4import { JOB_CATEGORIES, JOB_STATES } from '../../initializers'
5import { addMethodsToModel, getSort } from '../utils' 5import { getSort } from '../utils'
6import { JobAttributes, JobInstance, JobMethods } from './job-interface'
7 6
8let Job: Sequelize.Model<JobInstance, JobAttributes> 7@Table({
9let listWithLimitByCategory: JobMethods.ListWithLimitByCategory 8 tableName: 'job',
10let listForApi: JobMethods.ListForApi 9 indexes: [
11let toFormattedJSON: JobMethods.ToFormattedJSON
12
13export default function defineJob (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
14 Job = sequelize.define<JobInstance, JobAttributes>('Job',
15 {
16 state: {
17 type: DataTypes.ENUM(values(JOB_STATES)),
18 allowNull: false
19 },
20 category: {
21 type: DataTypes.ENUM(values(JOB_CATEGORIES)),
22 allowNull: false
23 },
24 handlerName: {
25 type: DataTypes.STRING,
26 allowNull: false
27 },
28 handlerInputData: {
29 type: DataTypes.JSON,
30 allowNull: true
31 }
32 },
33 { 10 {
34 indexes: [ 11 fields: [ 'state', 'category' ]
35 {
36 fields: [ 'state', 'category' ]
37 }
38 ]
39 } 12 }
40 )
41
42 const classMethods = [
43 listWithLimitByCategory,
44 listForApi
45 ] 13 ]
46 const instanceMethods = [ 14})
47 toFormattedJSON 15export class JobModel extends Model<JobModel> {
48 ] 16 @AllowNull(false)
49 addMethodsToModel(Job, classMethods, instanceMethods) 17 @Column(DataType.ENUM(values(JOB_STATES)))
18 state: JobState
50 19
51 return Job 20 @AllowNull(false)
52} 21 @Column(DataType.ENUM(values(JOB_CATEGORIES)))
22 category: JobCategory
53 23
54toFormattedJSON = function (this: JobInstance) { 24 @AllowNull(false)
55 return { 25 @Column
56 id: this.id, 26 handlerName: string
57 state: this.state,
58 category: this.category,
59 handlerName: this.handlerName,
60 handlerInputData: this.handlerInputData,
61 createdAt: this.createdAt,
62 updatedAt: this.updatedAt
63 }
64}
65 27
66// --------------------------------------------------------------------------- 28 @AllowNull(true)
29 @Column(DataType.JSON)
30 handlerInputData: any
67 31
68listWithLimitByCategory = function (limit: number, state: JobState, jobCategory: JobCategory) { 32 @CreatedAt
69 const query = { 33 creationDate: Date
70 order: [ 34
71 [ 'id', 'ASC' ] 35 @UpdatedAt
72 ], 36 updatedOn: Date
73 limit: limit, 37
74 where: { 38 static listWithLimitByCategory (limit: number, state: JobState, jobCategory: JobCategory) {
75 state, 39 const query = {
76 category: jobCategory 40 order: [
41 [ 'id', 'ASC' ]
42 ],
43 limit: limit,
44 where: {
45 state,
46 category: jobCategory
47 }
77 } 48 }
49
50 return JobModel.findAll(query)
78 } 51 }
79 52
80 return Job.findAll(query) 53 static listForApi (start: number, count: number, sort: string) {
81} 54 const query = {
55 offset: start,
56 limit: count,
57 order: [ getSort(sort) ]
58 }
82 59
83listForApi = function (start: number, count: number, sort: string) { 60 return JobModel.findAndCountAll(query).then(({ rows, count }) => {
84 const query = { 61 return {
85 offset: start, 62 data: rows,
86 limit: count, 63 total: count
87 order: [ getSort(sort) ] 64 }
65 })
88 } 66 }
89 67
90 return Job.findAndCountAll(query).then(({ rows, count }) => { 68 toFormattedJSON () {
91 return { 69 return {
92 data: rows, 70 id: this.id,
93 total: count 71 state: this.state,
72 category: this.category,
73 handlerName: this.handlerName,
74 handlerInputData: this.handlerInputData,
75 createdAt: this.createdAt,
76 updatedAt: this.updatedAt
94 } 77 }
95 }) 78 }
96} 79}
diff --git a/server/models/oauth/index.ts b/server/models/oauth/index.ts
deleted file mode 100644
index a20d3a56a..000000000
--- a/server/models/oauth/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
1export * from './oauth-client-interface'
2export * from './oauth-token-interface'
diff --git a/server/models/oauth/oauth-client-interface.ts b/server/models/oauth/oauth-client-interface.ts
deleted file mode 100644
index 3526e4159..000000000
--- a/server/models/oauth/oauth-client-interface.ts
+++ /dev/null
@@ -1,31 +0,0 @@
1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird'
3
4export namespace OAuthClientMethods {
5 export type CountTotal = () => Promise<number>
6
7 export type LoadFirstClient = () => Promise<OAuthClientInstance>
8
9 export type GetByIdAndSecret = (clientId: string, clientSecret: string) => Promise<OAuthClientInstance>
10}
11
12export interface OAuthClientClass {
13 countTotal: OAuthClientMethods.CountTotal
14 loadFirstClient: OAuthClientMethods.LoadFirstClient
15 getByIdAndSecret: OAuthClientMethods.GetByIdAndSecret
16}
17
18export interface OAuthClientAttributes {
19 clientId: string
20 clientSecret: string
21 grants: string[]
22 redirectUris: string[]
23}
24
25export interface OAuthClientInstance extends OAuthClientClass, OAuthClientAttributes, Sequelize.Instance<OAuthClientAttributes> {
26 id: number
27 createdAt: Date
28 updatedAt: Date
29}
30
31export interface OAuthClientModel extends OAuthClientClass, Sequelize.Model<OAuthClientInstance, OAuthClientAttributes> {}
diff --git a/server/models/oauth/oauth-client.ts b/server/models/oauth/oauth-client.ts
index 9cc68771d..42c59bb79 100644
--- a/server/models/oauth/oauth-client.ts
+++ b/server/models/oauth/oauth-client.ts
@@ -1,86 +1,62 @@
1import * as Sequelize from 'sequelize' 1import { AllowNull, Column, CreatedAt, DataType, HasMany, Model, Table, UpdatedAt } from 'sequelize-typescript'
2import { OAuthTokenModel } from './oauth-token'
2 3
3import { addMethodsToModel } from '../utils' 4@Table({
4import { 5 tableName: 'oAuthClient',
5 OAuthClientInstance, 6 indexes: [
6 OAuthClientAttributes,
7
8 OAuthClientMethods
9} from './oauth-client-interface'
10
11let OAuthClient: Sequelize.Model<OAuthClientInstance, OAuthClientAttributes>
12let countTotal: OAuthClientMethods.CountTotal
13let loadFirstClient: OAuthClientMethods.LoadFirstClient
14let getByIdAndSecret: OAuthClientMethods.GetByIdAndSecret
15
16export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
17 OAuthClient = sequelize.define<OAuthClientInstance, OAuthClientAttributes>('OAuthClient',
18 { 7 {
19 clientId: { 8 fields: [ 'clientId' ],
20 type: DataTypes.STRING, 9 unique: true
21 allowNull: false
22 },
23 clientSecret: {
24 type: DataTypes.STRING,
25 allowNull: false
26 },
27 grants: {
28 type: DataTypes.ARRAY(DataTypes.STRING)
29 },
30 redirectUris: {
31 type: DataTypes.ARRAY(DataTypes.STRING)
32 }
33 }, 10 },
34 { 11 {
35 indexes: [ 12 fields: [ 'clientId', 'clientSecret' ],
36 { 13 unique: true
37 fields: [ 'clientId' ],
38 unique: true
39 },
40 {
41 fields: [ 'clientId', 'clientSecret' ],
42 unique: true
43 }
44 ]
45 } 14 }
46 ) 15 ]
16})
17export class OAuthClientModel extends Model<OAuthClientModel> {
47 18
48 const classMethods = [ 19 @AllowNull(false)
49 associate, 20 @Column
21 clientId: string
50 22
51 countTotal, 23 @AllowNull(false)
52 getByIdAndSecret, 24 @Column
53 loadFirstClient 25 clientSecret: string
54 ]
55 addMethodsToModel(OAuthClient, classMethods)
56 26
57 return OAuthClient 27 @Column(DataType.ARRAY(DataType.STRING))
58} 28 grants: string[]
29
30 @Column(DataType.ARRAY(DataType.STRING))
31 redirectUris: string[]
32
33 @CreatedAt
34 createdAt: Date
59 35
60// --------------------------------------------------------------------------- 36 @UpdatedAt
37 updatedAt: Date
61 38
62function associate (models) { 39 @HasMany(() => OAuthTokenModel, {
63 OAuthClient.hasMany(models.OAuthToken, {
64 foreignKey: 'oAuthClientId',
65 onDelete: 'cascade' 40 onDelete: 'cascade'
66 }) 41 })
67} 42 OAuthTokens: OAuthTokenModel[]
68 43
69countTotal = function () { 44 static countTotal () {
70 return OAuthClient.count() 45 return OAuthClientModel.count()
71} 46 }
72 47
73loadFirstClient = function () { 48 static loadFirstClient () {
74 return OAuthClient.findOne() 49 return OAuthClientModel.findOne()
75} 50 }
76 51
77getByIdAndSecret = function (clientId: string, clientSecret: string) { 52 static getByIdAndSecret (clientId: string, clientSecret: string) {
78 const query = { 53 const query = {
79 where: { 54 where: {
80 clientId: clientId, 55 clientId: clientId,
81 clientSecret: clientSecret 56 clientSecret: clientSecret
57 }
82 } 58 }
83 }
84 59
85 return OAuthClient.findOne(query) 60 return OAuthClientModel.findOne(query)
61 }
86} 62}
diff --git a/server/models/oauth/oauth-token-interface.ts b/server/models/oauth/oauth-token-interface.ts
deleted file mode 100644
index 47d95d5fc..000000000
--- a/server/models/oauth/oauth-token-interface.ts
+++ /dev/null
@@ -1,46 +0,0 @@
1import * as Promise from 'bluebird'
2import * as Sequelize from 'sequelize'
3
4import { UserModel } from '../account/user-interface'
5
6export type OAuthTokenInfo = {
7 refreshToken: string
8 refreshTokenExpiresAt: Date,
9 client: {
10 id: number
11 },
12 user: {
13 id: number
14 }
15}
16
17export namespace OAuthTokenMethods {
18 export type GetByRefreshTokenAndPopulateClient = (refreshToken: string) => Promise<OAuthTokenInfo>
19 export type GetByTokenAndPopulateUser = (bearerToken: string) => Promise<OAuthTokenInstance>
20 export type GetByRefreshTokenAndPopulateUser = (refreshToken: string) => Promise<OAuthTokenInstance>
21}
22
23export interface OAuthTokenClass {
24 getByRefreshTokenAndPopulateClient: OAuthTokenMethods.GetByRefreshTokenAndPopulateClient
25 getByTokenAndPopulateUser: OAuthTokenMethods.GetByTokenAndPopulateUser
26 getByRefreshTokenAndPopulateUser: OAuthTokenMethods.GetByRefreshTokenAndPopulateUser
27}
28
29export interface OAuthTokenAttributes {
30 accessToken: string
31 accessTokenExpiresAt: Date
32 refreshToken: string
33 refreshTokenExpiresAt: Date
34
35 userId?: number
36 oAuthClientId?: number
37 User?: UserModel
38}
39
40export interface OAuthTokenInstance extends OAuthTokenClass, OAuthTokenAttributes, Sequelize.Instance<OAuthTokenAttributes> {
41 id: number
42 createdAt: Date
43 updatedAt: Date
44}
45
46export interface OAuthTokenModel extends OAuthTokenClass, Sequelize.Model<OAuthTokenInstance, OAuthTokenAttributes> {}
diff --git a/server/models/oauth/oauth-token.ts b/server/models/oauth/oauth-token.ts
index a82bff130..0d21c42fd 100644
--- a/server/models/oauth/oauth-token.ts
+++ b/server/models/oauth/oauth-token.ts
@@ -1,164 +1,163 @@
1import * as Sequelize from 'sequelize' 1import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
2
3import { logger } from '../../helpers' 2import { logger } from '../../helpers'
3import { AccountModel } from '../account/account'
4import { UserModel } from '../account/user'
5import { OAuthClientModel } from './oauth-client'
6
7export type OAuthTokenInfo = {
8 refreshToken: string
9 refreshTokenExpiresAt: Date,
10 client: {
11 id: number
12 },
13 user: {
14 id: number
15 }
16}
4 17
5import { addMethodsToModel } from '../utils' 18@Table({
6import { OAuthTokenAttributes, OAuthTokenInfo, OAuthTokenInstance, OAuthTokenMethods } from './oauth-token-interface' 19 tableName: 'oAuthToken',
7 20 indexes: [
8let OAuthToken: Sequelize.Model<OAuthTokenInstance, OAuthTokenAttributes>
9let getByRefreshTokenAndPopulateClient: OAuthTokenMethods.GetByRefreshTokenAndPopulateClient
10let getByTokenAndPopulateUser: OAuthTokenMethods.GetByTokenAndPopulateUser
11let getByRefreshTokenAndPopulateUser: OAuthTokenMethods.GetByRefreshTokenAndPopulateUser
12
13export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
14 OAuthToken = sequelize.define<OAuthTokenInstance, OAuthTokenAttributes>('OAuthToken',
15 { 21 {
16 accessToken: { 22 fields: [ 'refreshToken' ],
17 type: DataTypes.STRING, 23 unique: true
18 allowNull: false
19 },
20 accessTokenExpiresAt: {
21 type: DataTypes.DATE,
22 allowNull: false
23 },
24 refreshToken: {
25 type: DataTypes.STRING,
26 allowNull: false
27 },
28 refreshTokenExpiresAt: {
29 type: DataTypes.DATE,
30 allowNull: false
31 }
32 }, 24 },
33 { 25 {
34 indexes: [ 26 fields: [ 'accessToken' ],
35 { 27 unique: true
36 fields: [ 'refreshToken' ], 28 },
37 unique: true 29 {
38 }, 30 fields: [ 'userId' ]
39 { 31 },
40 fields: [ 'accessToken' ], 32 {
41 unique: true 33 fields: [ 'oAuthClientId' ]
42 },
43 {
44 fields: [ 'userId' ]
45 },
46 {
47 fields: [ 'oAuthClientId' ]
48 }
49 ]
50 } 34 }
51 ) 35 ]
36})
37export class OAuthTokenModel extends Model<OAuthTokenModel> {
52 38
53 const classMethods = [ 39 @AllowNull(false)
54 associate, 40 @Column
41 accessToken: string
55 42
56 getByRefreshTokenAndPopulateClient, 43 @AllowNull(false)
57 getByTokenAndPopulateUser, 44 @Column
58 getByRefreshTokenAndPopulateUser 45 accessTokenExpiresAt: Date
59 ]
60 addMethodsToModel(OAuthToken, classMethods)
61 46
62 return OAuthToken 47 @AllowNull(false)
63} 48 @Column
49 refreshToken: string
64 50
65// --------------------------------------------------------------------------- 51 @AllowNull(false)
52 @Column
53 refreshTokenExpiresAt: Date
66 54
67function associate (models) { 55 @CreatedAt
68 OAuthToken.belongsTo(models.User, { 56 createdAt: Date
57
58 @UpdatedAt
59 updatedAt: Date
60
61 @ForeignKey(() => UserModel)
62 @Column
63 userId: number
64
65 @BelongsTo(() => UserModel, {
69 foreignKey: { 66 foreignKey: {
70 name: 'userId',
71 allowNull: false 67 allowNull: false
72 }, 68 },
73 onDelete: 'cascade' 69 onDelete: 'cascade'
74 }) 70 })
71 User: UserModel
75 72
76 OAuthToken.belongsTo(models.OAuthClient, { 73 @ForeignKey(() => OAuthClientModel)
74 @Column
75 oAuthClientId: number
76
77 @BelongsTo(() => OAuthClientModel, {
77 foreignKey: { 78 foreignKey: {
78 name: 'oAuthClientId',
79 allowNull: false 79 allowNull: false
80 }, 80 },
81 onDelete: 'cascade' 81 onDelete: 'cascade'
82 }) 82 })
83} 83 OAuthClients: OAuthClientModel[]
84 84
85getByRefreshTokenAndPopulateClient = function (refreshToken: string) { 85 static getByRefreshTokenAndPopulateClient (refreshToken: string) {
86 const query = { 86 const query = {
87 where: { 87 where: {
88 refreshToken: refreshToken 88 refreshToken: refreshToken
89 }, 89 },
90 include: [ OAuthToken['sequelize'].models.OAuthClient ] 90 include: [ OAuthClientModel ]
91 }
92
93 return OAuthTokenModel.findOne(query)
94 .then(token => {
95 if (!token) return null
96
97 return {
98 refreshToken: token.refreshToken,
99 refreshTokenExpiresAt: token.refreshTokenExpiresAt,
100 client: {
101 id: token.oAuthClientId
102 },
103 user: {
104 id: token.userId
105 }
106 } as OAuthTokenInfo
107 })
108 .catch(err => {
109 logger.info('getRefreshToken error.', err)
110 throw err
111 })
91 } 112 }
92 113
93 return OAuthToken.findOne(query) 114 static getByTokenAndPopulateUser (bearerToken: string) {
94 .then(token => { 115 const query = {
95 if (!token) return null 116 where: {
96 117 accessToken: bearerToken
97 const tokenInfos: OAuthTokenInfo = { 118 },
98 refreshToken: token.refreshToken, 119 include: [
99 refreshTokenExpiresAt: token.refreshTokenExpiresAt, 120 {
100 client: { 121 model: UserModel,
101 id: token.oAuthClientId 122 include: [
102 }, 123 {
103 user: { 124 model: AccountModel,
104 id: token.userId 125 required: true
126 }
127 ]
105 } 128 }
106 } 129 ]
130 }
107 131
108 return tokenInfos 132 return OAuthTokenModel.findOne(query).then(token => {
109 }) 133 if (token) token['user'] = token.User
110 .catch(err => {
111 logger.info('getRefreshToken error.', err)
112 throw err
113 })
114}
115 134
116getByTokenAndPopulateUser = function (bearerToken: string) { 135 return token
117 const query = { 136 })
118 where: {
119 accessToken: bearerToken
120 },
121 include: [
122 {
123 model: OAuthToken['sequelize'].models.User,
124 include: [
125 {
126 model: OAuthToken['sequelize'].models.Account,
127 required: true
128 }
129 ]
130 }
131 ]
132 } 137 }
133 138
134 return OAuthToken.findOne(query).then(token => { 139 static getByRefreshTokenAndPopulateUser (refreshToken: string) {
135 if (token) token['user'] = token.User 140 const query = {
141 where: {
142 refreshToken: refreshToken
143 },
144 include: [
145 {
146 model: UserModel,
147 include: [
148 {
149 model: AccountModel,
150 required: true
151 }
152 ]
153 }
154 ]
155 }
136 156
137 return token 157 return OAuthTokenModel.findOne(query).then(token => {
138 }) 158 token['user'] = token.User
139}
140 159
141getByRefreshTokenAndPopulateUser = function (refreshToken: string) { 160 return token
142 const query = { 161 })
143 where: {
144 refreshToken: refreshToken
145 },
146 include: [
147 {
148 model: OAuthToken['sequelize'].models.User,
149 include: [
150 {
151 model: OAuthToken['sequelize'].models.Account,
152 required: true
153 }
154 ]
155 }
156 ]
157 } 162 }
158
159 return OAuthToken.findOne(query).then(token => {
160 token['user'] = token.User
161
162 return token
163 })
164} 163}
diff --git a/server/models/server/index.ts b/server/models/server/index.ts
deleted file mode 100644
index 4cb2994aa..000000000
--- a/server/models/server/index.ts
+++ /dev/null
@@ -1 +0,0 @@
1export * from './server-interface'
diff --git a/server/models/server/server-interface.ts b/server/models/server/server-interface.ts
deleted file mode 100644
index be1a4917e..000000000
--- a/server/models/server/server-interface.ts
+++ /dev/null
@@ -1,24 +0,0 @@
1import * as Promise from 'bluebird'
2import * as Sequelize from 'sequelize'
3
4export namespace ServerMethods {
5 export type ListBadServers = () => Promise<ServerInstance[]>
6 export type UpdateServersScoreAndRemoveBadOnes = (goodServers: number[], badServers: number[]) => void
7}
8
9export interface ServerClass {
10 updateServersScoreAndRemoveBadOnes: ServerMethods.UpdateServersScoreAndRemoveBadOnes
11}
12
13export interface ServerAttributes {
14 id?: number
15 host?: string
16 score?: number | Sequelize.literal // Sequelize literal for 'score +' + value
17}
18
19export interface ServerInstance extends ServerClass, ServerAttributes, Sequelize.Instance<ServerAttributes> {
20 createdAt: Date
21 updatedAt: Date
22}
23
24export interface ServerModel extends ServerClass, Sequelize.Model<ServerInstance, ServerAttributes> {}
diff --git a/server/models/server/server.ts b/server/models/server/server.ts
index ebd216b08..edfd8010b 100644
--- a/server/models/server/server.ts
+++ b/server/models/server/server.ts
@@ -1,124 +1,109 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import { isHostValid, logger } from '../../helpers' 2import { AllowNull, Column, CreatedAt, Default, Is, IsInt, Max, Model, Table, UpdatedAt } from 'sequelize-typescript'
3import { logger } from '../../helpers'
4import { isHostValid } from '../../helpers/custom-validators/servers'
3import { SERVERS_SCORE } from '../../initializers' 5import { SERVERS_SCORE } from '../../initializers'
4import { addMethodsToModel } from '../utils' 6import { throwIfNotValid } from '../utils'
5import { ServerAttributes, ServerInstance, ServerMethods } from './server-interface'
6 7
7let Server: Sequelize.Model<ServerInstance, ServerAttributes> 8@Table({
8let updateServersScoreAndRemoveBadOnes: ServerMethods.UpdateServersScoreAndRemoveBadOnes 9 tableName: 'server',
9 10 indexes: [
10export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
11 Server = sequelize.define<ServerInstance, ServerAttributes>('Server',
12 { 11 {
13 host: { 12 fields: [ 'host' ],
14 type: DataTypes.STRING, 13 unique: true
15 allowNull: false,
16 validate: {
17 isHost: value => {
18 const res = isHostValid(value)
19 if (res === false) throw new Error('Host not valid.')
20 }
21 }
22 },
23 score: {
24 type: DataTypes.INTEGER,
25 defaultValue: SERVERS_SCORE.BASE,
26 allowNull: false,
27 validate: {
28 isInt: true,
29 max: SERVERS_SCORE.MAX
30 }
31 }
32 }, 14 },
33 { 15 {
34 indexes: [ 16 fields: [ 'score' ]
35 {
36 fields: [ 'host' ],
37 unique: true
38 },
39 {
40 fields: [ 'score' ]
41 }
42 ]
43 } 17 }
44 )
45
46 const classMethods = [
47 updateServersScoreAndRemoveBadOnes
48 ] 18 ]
49 addMethodsToModel(Server, classMethods) 19})
50 20export class ServerModel extends Model<ServerModel> {
51 return Server 21
52} 22 @AllowNull(false)
53 23 @Is('Host', value => throwIfNotValid(value, isHostValid, 'valid host'))
54// ------------------------------ Statics ------------------------------ 24 @Column
55 25 host: string
56updateServersScoreAndRemoveBadOnes = function (goodServers: number[], badServers: number[]) { 26
57 logger.info('Updating %d good servers and %d bad servers scores.', goodServers.length, badServers.length) 27 @AllowNull(false)
28 @Default(SERVERS_SCORE.BASE)
29 @IsInt
30 @Max(SERVERS_SCORE.MAX)
31 @Column
32 score: number
33
34 @CreatedAt
35 createdAt: Date
36
37 @UpdatedAt
38 updatedAt: Date
39
40 static updateServersScoreAndRemoveBadOnes (goodServers: number[], badServers: number[]) {
41 logger.info('Updating %d good servers and %d bad servers scores.', goodServers.length, badServers.length)
42
43 if (goodServers.length !== 0) {
44 ServerModel.incrementScores(goodServers, SERVERS_SCORE.BONUS)
45 .catch(err => {
46 logger.error('Cannot increment scores of good servers.', err)
47 })
48 }
58 49
59 if (goodServers.length !== 0) { 50 if (badServers.length !== 0) {
60 incrementScores(goodServers, SERVERS_SCORE.BONUS).catch(err => { 51 ServerModel.incrementScores(badServers, SERVERS_SCORE.PENALTY)
61 logger.error('Cannot increment scores of good servers.', err) 52 .then(() => ServerModel.removeBadServers())
62 }) 53 .catch(err => {
63 } 54 if (err) logger.error('Cannot decrement scores of bad servers.', err)
55 })
64 56
65 if (badServers.length !== 0) { 57 }
66 incrementScores(badServers, SERVERS_SCORE.PENALTY)
67 .then(() => removeBadServers())
68 .catch(err => {
69 if (err) logger.error('Cannot decrement scores of bad servers.', err)
70 })
71 } 58 }
72}
73
74// ---------------------------------------------------------------------------
75 59
76// Remove servers with a score of 0 (too many requests where they were unreachable) 60 // Remove servers with a score of 0 (too many requests where they were unreachable)
77async function removeBadServers () { 61 private static async removeBadServers () {
78 try { 62 try {
79 const servers = await listBadServers() 63 const servers = await ServerModel.listBadServers()
80 64
81 const serversRemovePromises = servers.map(server => server.destroy()) 65 const serversRemovePromises = servers.map(server => server.destroy())
82 await Promise.all(serversRemovePromises) 66 await Promise.all(serversRemovePromises)
83 67
84 const numberOfServersRemoved = servers.length 68 const numberOfServersRemoved = servers.length
85 69
86 if (numberOfServersRemoved) { 70 if (numberOfServersRemoved) {
87 logger.info('Removed %d servers.', numberOfServersRemoved) 71 logger.info('Removed %d servers.', numberOfServersRemoved)
88 } else { 72 } else {
89 logger.info('No need to remove bad servers.') 73 logger.info('No need to remove bad servers.')
74 }
75 } catch (err) {
76 logger.error('Cannot remove bad servers.', err)
90 } 77 }
91 } catch (err) {
92 logger.error('Cannot remove bad servers.', err)
93 } 78 }
94}
95 79
96function incrementScores (ids: number[], value: number) { 80 private static incrementScores (ids: number[], value: number) {
97 const update = { 81 const update = {
98 score: Sequelize.literal('score +' + value) 82 score: Sequelize.literal('score +' + value)
99 } 83 }
100 84
101 const options = { 85 const options = {
102 where: { 86 where: {
103 id: { 87 id: {
104 [Sequelize.Op.in]: ids 88 [Sequelize.Op.in]: ids
105 } 89 }
106 }, 90 },
107 // In this case score is a literal and not an integer so we do not validate it 91 // In this case score is a literal and not an integer so we do not validate it
108 validate: false 92 validate: false
109 } 93 }
110 94
111 return Server.update(update, options) 95 return ServerModel.update(update, options)
112} 96 }
113 97
114function listBadServers () { 98 private static listBadServers () {
115 const query = { 99 const query = {
116 where: { 100 where: {
117 score: { 101 score: {
118 [Sequelize.Op.lte]: 0 102 [Sequelize.Op.lte]: 0
103 }
119 } 104 }
120 } 105 }
121 }
122 106
123 return Server.findAll(query) 107 return ServerModel.findAll(query)
108 }
124} 109}
diff --git a/server/models/utils.ts b/server/models/utils.ts
index 1bf61d2a6..1606453e0 100644
--- a/server/models/utils.ts
+++ b/server/models/utils.ts
@@ -14,22 +14,23 @@ function getSort (value: string) {
14 return [ field, direction ] 14 return [ field, direction ]
15} 15}
16 16
17function addMethodsToModel (model: any, classMethods: Function[], instanceMethods: Function[] = []) {
18 classMethods.forEach(m => model[m.name] = m)
19 instanceMethods.forEach(m => model.prototype[m.name] = m)
20}
21
22function getSortOnModel (model: any, value: string) { 17function getSortOnModel (model: any, value: string) {
23 let sort = getSort(value) 18 let sort = getSort(value)
24 19
25 if (model) return [ { model: model }, sort[0], sort[1] ] 20 if (model) return [ model, sort[0], sort[1] ]
26 return sort 21 return sort
27} 22}
28 23
24function throwIfNotValid (value: any, validator: (value: any) => boolean, fieldName = 'value') {
25 if (validator(value) === false) {
26 throw new Error(`"${value}" is not a valid ${fieldName}.`)
27 }
28}
29
29// --------------------------------------------------------------------------- 30// ---------------------------------------------------------------------------
30 31
31export { 32export {
32 addMethodsToModel,
33 getSort, 33 getSort,
34 getSortOnModel 34 getSortOnModel,
35 throwIfNotValid
35} 36}
diff --git a/server/models/video/index.ts b/server/models/video/index.ts
deleted file mode 100644
index e17bbfab4..000000000
--- a/server/models/video/index.ts
+++ /dev/null
@@ -1,9 +0,0 @@
1export * from './tag-interface'
2export * from './video-abuse-interface'
3export * from './video-blacklist-interface'
4export * from './video-channel-interface'
5export * from './video-tag-interface'
6export * from './video-file-interface'
7export * from './video-interface'
8export * from './video-share-interface'
9export * from './video-channel-share-interface'
diff --git a/server/models/video/tag-interface.ts b/server/models/video/tag-interface.ts
deleted file mode 100644
index 08e5c3246..000000000
--- a/server/models/video/tag-interface.ts
+++ /dev/null
@@ -1,20 +0,0 @@
1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird'
3
4export namespace TagMethods {
5 export type FindOrCreateTags = (tags: string[], transaction: Sequelize.Transaction) => Promise<TagInstance[]>
6}
7
8export interface TagClass {
9 findOrCreateTags: TagMethods.FindOrCreateTags
10}
11
12export interface TagAttributes {
13 name: string
14}
15
16export interface TagInstance extends TagClass, TagAttributes, Sequelize.Instance<TagAttributes> {
17 id: number
18}
19
20export interface TagModel extends TagClass, Sequelize.Model<TagInstance, TagAttributes> {}
diff --git a/server/models/video/tag.ts b/server/models/video/tag.ts
index 0c0757fc8..0ae74d808 100644
--- a/server/models/video/tag.ts
+++ b/server/models/video/tag.ts
@@ -1,73 +1,60 @@
1import * as Sequelize from 'sequelize' 1import * as Bluebird from 'bluebird'
2import * as Promise from 'bluebird' 2import { Transaction } from 'sequelize'
3 3import { AllowNull, BelongsToMany, Column, CreatedAt, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
4import { addMethodsToModel } from '../utils' 4import { isVideoTagValid } from '../../helpers/custom-validators/videos'
5import { 5import { throwIfNotValid } from '../utils'
6 TagInstance, 6import { VideoModel } from './video'
7 TagAttributes, 7import { VideoTagModel } from './video-tag'
8 8
9 TagMethods 9@Table({
10} from './tag-interface' 10 tableName: 'tag',
11 11 timestamps: false,
12let Tag: Sequelize.Model<TagInstance, TagAttributes> 12 indexes: [
13let findOrCreateTags: TagMethods.FindOrCreateTags
14
15export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
16 Tag = sequelize.define<TagInstance, TagAttributes>('Tag',
17 { 13 {
18 name: { 14 fields: [ 'name' ],
19 type: DataTypes.STRING, 15 unique: true
20 allowNull: false
21 }
22 },
23 {
24 timestamps: false,
25 indexes: [
26 {
27 fields: [ 'name' ],
28 unique: true
29 }
30 ]
31 } 16 }
32 )
33
34 const classMethods = [
35 associate,
36
37 findOrCreateTags
38 ] 17 ]
39 addMethodsToModel(Tag, classMethods) 18})
19export class TagModel extends Model<TagModel> {
40 20
41 return Tag 21 @AllowNull(false)
42} 22 @Is('VideoTag', value => throwIfNotValid(value, isVideoTagValid, 'tag'))
23 @Column
24 name: string
43 25
44// --------------------------------------------------------------------------- 26 @CreatedAt
27 createdAt: Date
45 28
46function associate (models) { 29 @UpdatedAt
47 Tag.belongsToMany(models.Video, { 30 updatedAt: Date
31
32 @BelongsToMany(() => VideoModel, {
48 foreignKey: 'tagId', 33 foreignKey: 'tagId',
49 through: models.VideoTag, 34 through: () => VideoTagModel,
50 onDelete: 'CASCADE' 35 onDelete: 'CASCADE'
51 }) 36 })
52} 37 Videos: VideoModel[]
53 38
54findOrCreateTags = function (tags: string[], transaction: Sequelize.Transaction) { 39 static findOrCreateTags (tags: string[], transaction: Transaction) {
55 const tasks: Promise<TagInstance>[] = [] 40 const tasks: Bluebird<TagModel>[] = []
56 tags.forEach(tag => { 41 tags.forEach(tag => {
57 const query: Sequelize.FindOrInitializeOptions<TagAttributes> = { 42 const query = {
58 where: { 43 where: {
59 name: tag 44 name: tag
60 }, 45 },
61 defaults: { 46 defaults: {
62 name: tag 47 name: tag
48 }
63 } 49 }
64 }
65 50
66 if (transaction) query.transaction = transaction 51 if (transaction) query['transaction'] = transaction
67 52
68 const promise = Tag.findOrCreate(query).then(([ tagInstance ]) => tagInstance) 53 const promise = TagModel.findOrCreate(query)
69 tasks.push(promise) 54 .then(([ tagInstance ]) => tagInstance)
70 }) 55 tasks.push(promise)
56 })
71 57
72 return Promise.all(tasks) 58 return Promise.all(tasks)
59 }
73} 60}
diff --git a/server/models/video/video-abuse-interface.ts b/server/models/video/video-abuse-interface.ts
deleted file mode 100644
index feafc4a19..000000000
--- a/server/models/video/video-abuse-interface.ts
+++ /dev/null
@@ -1,41 +0,0 @@
1import * as Promise from 'bluebird'
2import * as Sequelize from 'sequelize'
3import { ResultList } from '../../../shared'
4import { VideoAbuse as FormattedVideoAbuse } from '../../../shared/models/videos/video-abuse.model'
5import { AccountInstance } from '../account/account-interface'
6import { ServerInstance } from '../server/server-interface'
7import { VideoInstance } from './video-interface'
8import { VideoAbuseObject } from '../../../shared/models/activitypub/objects/video-abuse-object'
9
10export namespace VideoAbuseMethods {
11 export type ToFormattedJSON = (this: VideoAbuseInstance) => FormattedVideoAbuse
12
13 export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList<VideoAbuseInstance> >
14 export type ToActivityPubObject = () => VideoAbuseObject
15}
16
17export interface VideoAbuseClass {
18 listForApi: VideoAbuseMethods.ListForApi
19 toActivityPubObject: VideoAbuseMethods.ToActivityPubObject
20}
21
22export interface VideoAbuseAttributes {
23 reason: string
24 videoId: number
25 reporterAccountId: number
26
27 Account?: AccountInstance
28 Video?: VideoInstance
29}
30
31export interface VideoAbuseInstance extends VideoAbuseClass, VideoAbuseAttributes, Sequelize.Instance<VideoAbuseAttributes> {
32 id: number
33 createdAt: Date
34 updatedAt: Date
35
36 Server: ServerInstance
37
38 toFormattedJSON: VideoAbuseMethods.ToFormattedJSON
39}
40
41export interface VideoAbuseModel extends VideoAbuseClass, Sequelize.Model<VideoAbuseInstance, VideoAbuseAttributes> {}
diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts
index d09f5f7a1..d0ee969fb 100644
--- a/server/models/video/video-abuse.ts
+++ b/server/models/video/video-abuse.ts
@@ -1,142 +1,116 @@
1import * as Sequelize from 'sequelize' 1import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
2 2import { VideoAbuseObject } from '../../../shared/models/activitypub/objects'
3import { isVideoAbuseReasonValid } from '../../helpers/custom-validators/videos'
3import { CONFIG } from '../../initializers' 4import { CONFIG } from '../../initializers'
4import { isVideoAbuseReasonValid } from '../../helpers' 5import { AccountModel } from '../account/account'
5 6import { ServerModel } from '../server/server'
6import { addMethodsToModel, getSort } from '../utils' 7import { getSort, throwIfNotValid } from '../utils'
7import { 8import { VideoModel } from './video'
8 VideoAbuseInstance, 9
9 VideoAbuseAttributes, 10@Table({
10 11 tableName: 'videoAbuse',
11 VideoAbuseMethods 12 indexes: [
12} from './video-abuse-interface'
13import { VideoAbuseObject } from '../../../shared/models/activitypub/objects/video-abuse-object'
14
15let VideoAbuse: Sequelize.Model<VideoAbuseInstance, VideoAbuseAttributes>
16let toFormattedJSON: VideoAbuseMethods.ToFormattedJSON
17let listForApi: VideoAbuseMethods.ListForApi
18let toActivityPubObject: VideoAbuseMethods.ToActivityPubObject
19
20export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
21 VideoAbuse = sequelize.define<VideoAbuseInstance, VideoAbuseAttributes>('VideoAbuse',
22 { 13 {
23 reason: { 14 fields: [ 'videoId' ]
24 type: DataTypes.STRING,
25 allowNull: false,
26 validate: {
27 reasonValid: value => {
28 const res = isVideoAbuseReasonValid(value)
29 if (res === false) throw new Error('Video abuse reason is not valid.')
30 }
31 }
32 }
33 }, 15 },
34 { 16 {
35 indexes: [ 17 fields: [ 'reporterAccountId' ]
36 {
37 fields: [ 'videoId' ]
38 },
39 {
40 fields: [ 'reporterAccountId' ]
41 }
42 ]
43 } 18 }
44 )
45
46 const classMethods = [
47 associate,
48
49 listForApi
50 ]
51 const instanceMethods = [
52 toFormattedJSON,
53 toActivityPubObject
54 ] 19 ]
55 addMethodsToModel(VideoAbuse, classMethods, instanceMethods) 20})
56 21export class VideoAbuseModel extends Model<VideoAbuseModel> {
57 return VideoAbuse
58}
59
60// ------------------------------ METHODS ------------------------------
61
62toFormattedJSON = function (this: VideoAbuseInstance) {
63 let reporterServerHost
64
65 if (this.Account.Server) {
66 reporterServerHost = this.Account.Server.host
67 } else {
68 // It means it's our video
69 reporterServerHost = CONFIG.WEBSERVER.HOST
70 }
71
72 const json = {
73 id: this.id,
74 reason: this.reason,
75 reporterUsername: this.Account.name,
76 reporterServerHost,
77 videoId: this.Video.id,
78 videoUUID: this.Video.uuid,
79 videoName: this.Video.name,
80 createdAt: this.createdAt
81 }
82 22
83 return json 23 @AllowNull(false)
84} 24 @Is('VideoAbuseReason', value => throwIfNotValid(value, isVideoAbuseReasonValid, 'reason'))
25 @Column
26 reason: string
85 27
86toActivityPubObject = function (this: VideoAbuseInstance) { 28 @CreatedAt
87 const videoAbuseObject: VideoAbuseObject = { 29 createdAt: Date
88 type: 'Flag' as 'Flag',
89 content: this.reason,
90 object: this.Video.url
91 }
92 30
93 return videoAbuseObject 31 @UpdatedAt
94} 32 updatedAt: Date
95 33
96// ------------------------------ STATICS ------------------------------ 34 @ForeignKey(() => AccountModel)
35 @Column
36 reporterAccountId: number
97 37
98function associate (models) { 38 @BelongsTo(() => AccountModel, {
99 VideoAbuse.belongsTo(models.Account, {
100 foreignKey: { 39 foreignKey: {
101 name: 'reporterAccountId',
102 allowNull: false 40 allowNull: false
103 }, 41 },
104 onDelete: 'CASCADE' 42 onDelete: 'cascade'
105 }) 43 })
44 Account: AccountModel
45
46 @ForeignKey(() => VideoModel)
47 @Column
48 videoId: number
106 49
107 VideoAbuse.belongsTo(models.Video, { 50 @BelongsTo(() => VideoModel, {
108 foreignKey: { 51 foreignKey: {
109 name: 'videoId',
110 allowNull: false 52 allowNull: false
111 }, 53 },
112 onDelete: 'CASCADE' 54 onDelete: 'cascade'
113 }) 55 })
114} 56 Video: VideoModel
57
58 static listForApi (start: number, count: number, sort: string) {
59 const query = {
60 offset: start,
61 limit: count,
62 order: [ getSort(sort) ],
63 include: [
64 {
65 model: AccountModel,
66 required: true,
67 include: [
68 {
69 model: ServerModel,
70 required: false
71 }
72 ]
73 },
74 {
75 model: VideoModel,
76 required: true
77 }
78 ]
79 }
115 80
116listForApi = function (start: number, count: number, sort: string) { 81 return VideoAbuseModel.findAndCountAll(query)
117 const query = { 82 .then(({ rows, count }) => {
118 offset: start, 83 return { total: count, data: rows }
119 limit: count, 84 })
120 order: [ getSort(sort) ],
121 include: [
122 {
123 model: VideoAbuse['sequelize'].models.Account,
124 required: true,
125 include: [
126 {
127 model: VideoAbuse['sequelize'].models.Server,
128 required: false
129 }
130 ]
131 },
132 {
133 model: VideoAbuse['sequelize'].models.Video,
134 required: true
135 }
136 ]
137 } 85 }
138 86
139 return VideoAbuse.findAndCountAll(query).then(({ rows, count }) => { 87 toFormattedJSON () {
140 return { total: count, data: rows } 88 let reporterServerHost
141 }) 89
90 if (this.Account.Server) {
91 reporterServerHost = this.Account.Server.host
92 } else {
93 // It means it's our video
94 reporterServerHost = CONFIG.WEBSERVER.HOST
95 }
96
97 return {
98 id: this.id,
99 reason: this.reason,
100 reporterUsername: this.Account.name,
101 reporterServerHost,
102 videoId: this.Video.id,
103 videoUUID: this.Video.uuid,
104 videoName: this.Video.name,
105 createdAt: this.createdAt
106 }
107 }
108
109 toActivityPubObject (): VideoAbuseObject {
110 return {
111 type: 'Flag' as 'Flag',
112 content: this.reason,
113 object: this.Video.url
114 }
115 }
142} 116}
diff --git a/server/models/video/video-blacklist-interface.ts b/server/models/video/video-blacklist-interface.ts
deleted file mode 100644
index be2483d4c..000000000
--- a/server/models/video/video-blacklist-interface.ts
+++ /dev/null
@@ -1,39 +0,0 @@
1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird'
3
4import { SortType } from '../../helpers'
5import { ResultList } from '../../../shared'
6import { VideoInstance } from './video-interface'
7
8// Don't use barrel, import just what we need
9import { BlacklistedVideo as FormattedBlacklistedVideo } from '../../../shared/models/videos/video-blacklist.model'
10
11export namespace BlacklistedVideoMethods {
12 export type ToFormattedJSON = (this: BlacklistedVideoInstance) => FormattedBlacklistedVideo
13 export type ListForApi = (start: number, count: number, sort: SortType) => Promise< ResultList<BlacklistedVideoInstance> >
14 export type LoadByVideoId = (id: number) => Promise<BlacklistedVideoInstance>
15}
16
17export interface BlacklistedVideoClass {
18 toFormattedJSON: BlacklistedVideoMethods.ToFormattedJSON
19 listForApi: BlacklistedVideoMethods.ListForApi
20 loadByVideoId: BlacklistedVideoMethods.LoadByVideoId
21}
22
23export interface BlacklistedVideoAttributes {
24 videoId: number
25
26 Video?: VideoInstance
27}
28
29export interface BlacklistedVideoInstance
30 extends BlacklistedVideoClass, BlacklistedVideoAttributes, Sequelize.Instance<BlacklistedVideoAttributes> {
31 id: number
32 createdAt: Date
33 updatedAt: Date
34
35 toFormattedJSON: BlacklistedVideoMethods.ToFormattedJSON
36}
37
38export interface BlacklistedVideoModel
39 extends BlacklistedVideoClass, Sequelize.Model<BlacklistedVideoInstance, BlacklistedVideoAttributes> {}
diff --git a/server/models/video/video-blacklist.ts b/server/models/video/video-blacklist.ts
index ae8286285..6db562719 100644
--- a/server/models/video/video-blacklist.ts
+++ b/server/models/video/video-blacklist.ts
@@ -1,104 +1,80 @@
1import * as Sequelize from 'sequelize' 1import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
2
3import { SortType } from '../../helpers' 2import { SortType } from '../../helpers'
4import { addMethodsToModel, getSortOnModel } from '../utils' 3import { getSortOnModel } from '../utils'
5import { VideoInstance } from './video-interface' 4import { VideoModel } from './video'
6import {
7 BlacklistedVideoInstance,
8 BlacklistedVideoAttributes,
9
10 BlacklistedVideoMethods
11} from './video-blacklist-interface'
12
13let BlacklistedVideo: Sequelize.Model<BlacklistedVideoInstance, BlacklistedVideoAttributes>
14let toFormattedJSON: BlacklistedVideoMethods.ToFormattedJSON
15let listForApi: BlacklistedVideoMethods.ListForApi
16let loadByVideoId: BlacklistedVideoMethods.LoadByVideoId
17 5
18export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { 6@Table({
19 BlacklistedVideo = sequelize.define<BlacklistedVideoInstance, BlacklistedVideoAttributes>('BlacklistedVideo', 7 tableName: 'videoBlacklist',
20 {}, 8 indexes: [
21 { 9 {
22 indexes: [ 10 fields: [ 'videoId' ],
23 { 11 unique: true
24 fields: [ 'videoId' ],
25 unique: true
26 }
27 ]
28 } 12 }
29 )
30
31 const classMethods = [
32 associate,
33
34 listForApi,
35 loadByVideoId
36 ] 13 ]
37 const instanceMethods = [ 14})
38 toFormattedJSON 15export class VideoBlacklistModel extends Model<VideoBlacklistModel> {
39 ]
40 addMethodsToModel(BlacklistedVideo, classMethods, instanceMethods)
41
42 return BlacklistedVideo
43}
44 16
45// ------------------------------ METHODS ------------------------------ 17 @CreatedAt
18 createdAt: Date
46 19
47toFormattedJSON = function (this: BlacklistedVideoInstance) { 20 @UpdatedAt
48 let video: VideoInstance 21 updatedAt: Date
49
50 video = this.Video
51
52 return {
53 id: this.id,
54 videoId: this.videoId,
55 createdAt: this.createdAt,
56 updatedAt: this.updatedAt,
57 name: video.name,
58 uuid: video.uuid,
59 description: video.description,
60 duration: video.duration,
61 views: video.views,
62 likes: video.likes,
63 dislikes: video.dislikes,
64 nsfw: video.nsfw
65 }
66}
67 22
68// ------------------------------ STATICS ------------------------------ 23 @ForeignKey(() => VideoModel)
24 @Column
25 videoId: number
69 26
70function associate (models) { 27 @BelongsTo(() => VideoModel, {
71 BlacklistedVideo.belongsTo(models.Video, {
72 foreignKey: { 28 foreignKey: {
73 name: 'videoId',
74 allowNull: false 29 allowNull: false
75 }, 30 },
76 onDelete: 'CASCADE' 31 onDelete: 'cascade'
77 }) 32 })
78} 33 Video: VideoModel
34
35 static listForApi (start: number, count: number, sort: SortType) {
36 const query = {
37 offset: start,
38 limit: count,
39 order: [ getSortOnModel(sort.sortModel, sort.sortValue) ],
40 include: [ { model: VideoModel } ]
41 }
79 42
80listForApi = function (start: number, count: number, sort: SortType) { 43 return VideoBlacklistModel.findAndCountAll(query)
81 const query = { 44 .then(({ rows, count }) => {
82 offset: start, 45 return {
83 limit: count, 46 data: rows,
84 order: [ getSortOnModel(sort.sortModel, sort.sortValue) ], 47 total: count
85 include: [ { model: BlacklistedVideo['sequelize'].models.Video } ] 48 }
49 })
86 } 50 }
87 51
88 return BlacklistedVideo.findAndCountAll(query).then(({ rows, count }) => { 52 static loadByVideoId (id: number) {
89 return { 53 const query = {
90 data: rows, 54 where: {
91 total: count 55 videoId: id
56 }
92 } 57 }
93 })
94}
95 58
96loadByVideoId = function (id: number) { 59 return VideoBlacklistModel.findOne(query)
97 const query = {
98 where: {
99 videoId: id
100 }
101 } 60 }
102 61
103 return BlacklistedVideo.findOne(query) 62 toFormattedJSON () {
63 const video = this.Video
64
65 return {
66 id: this.id,
67 videoId: this.videoId,
68 createdAt: this.createdAt,
69 updatedAt: this.updatedAt,
70 name: video.name,
71 uuid: video.uuid,
72 description: video.description,
73 duration: video.duration,
74 views: video.views,
75 likes: video.likes,
76 dislikes: video.dislikes,
77 nsfw: video.nsfw
78 }
79 }
104} 80}
diff --git a/server/models/video/video-channel-interface.ts b/server/models/video/video-channel-interface.ts
deleted file mode 100644
index 21f81e901..000000000
--- a/server/models/video/video-channel-interface.ts
+++ /dev/null
@@ -1,64 +0,0 @@
1import * as Promise from 'bluebird'
2import * as Sequelize from 'sequelize'
3
4import { ResultList } from '../../../shared'
5import { VideoChannelObject } from '../../../shared/models/activitypub/objects/video-channel-object'
6import { VideoChannel as FormattedVideoChannel } from '../../../shared/models/videos/video-channel.model'
7import { AccountInstance } from '../account/account-interface'
8import { VideoInstance } from './video-interface'
9import { VideoChannelShareInstance } from './video-channel-share-interface'
10
11export namespace VideoChannelMethods {
12 export type ToFormattedJSON = (this: VideoChannelInstance) => FormattedVideoChannel
13 export type ToActivityPubObject = (this: VideoChannelInstance) => VideoChannelObject
14 export type IsOwned = (this: VideoChannelInstance) => boolean
15
16 export type CountByAccount = (accountId: number) => Promise<number>
17 export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList<VideoChannelInstance> >
18 export type LoadByIdAndAccount = (id: number, accountId: number) => Promise<VideoChannelInstance>
19 export type ListByAccount = (accountId: number) => Promise< ResultList<VideoChannelInstance> >
20 export type LoadAndPopulateAccount = (id: number) => Promise<VideoChannelInstance>
21 export type LoadByUUIDAndPopulateAccount = (uuid: string) => Promise<VideoChannelInstance>
22 export type LoadByUUID = (uuid: string, t?: Sequelize.Transaction) => Promise<VideoChannelInstance>
23 export type LoadByHostAndUUID = (uuid: string, serverHost: string, t?: Sequelize.Transaction) => Promise<VideoChannelInstance>
24 export type LoadAndPopulateAccountAndVideos = (id: number) => Promise<VideoChannelInstance>
25 export type LoadByUrl = (uuid: string, t?: Sequelize.Transaction) => Promise<VideoChannelInstance>
26 export type LoadByUUIDOrUrl = (uuid: string, url: string, t?: Sequelize.Transaction) => Promise<VideoChannelInstance>
27}
28
29export interface VideoChannelClass {
30 countByAccount: VideoChannelMethods.CountByAccount
31 listForApi: VideoChannelMethods.ListForApi
32 listByAccount: VideoChannelMethods.ListByAccount
33 loadByIdAndAccount: VideoChannelMethods.LoadByIdAndAccount
34 loadAndPopulateAccount: VideoChannelMethods.LoadAndPopulateAccount
35 loadByUUIDAndPopulateAccount: VideoChannelMethods.LoadByUUIDAndPopulateAccount
36 loadAndPopulateAccountAndVideos: VideoChannelMethods.LoadAndPopulateAccountAndVideos
37 loadByUrl: VideoChannelMethods.LoadByUrl
38 loadByUUIDOrUrl: VideoChannelMethods.LoadByUUIDOrUrl
39}
40
41export interface VideoChannelAttributes {
42 id?: number
43 uuid?: string
44 name: string
45 description: string
46 remote: boolean
47 url?: string
48
49 Account?: AccountInstance
50 Videos?: VideoInstance[]
51 VideoChannelShares?: VideoChannelShareInstance[]
52}
53
54export interface VideoChannelInstance extends VideoChannelClass, VideoChannelAttributes, Sequelize.Instance<VideoChannelAttributes> {
55 id: number
56 createdAt: Date
57 updatedAt: Date
58
59 isOwned: VideoChannelMethods.IsOwned
60 toFormattedJSON: VideoChannelMethods.ToFormattedJSON
61 toActivityPubObject: VideoChannelMethods.ToActivityPubObject
62}
63
64export interface VideoChannelModel extends VideoChannelClass, Sequelize.Model<VideoChannelInstance, VideoChannelAttributes> {}
diff --git a/server/models/video/video-channel-share-interface.ts b/server/models/video/video-channel-share-interface.ts
deleted file mode 100644
index 2fff41a1b..000000000
--- a/server/models/video/video-channel-share-interface.ts
+++ /dev/null
@@ -1,32 +0,0 @@
1import * as Bluebird from 'bluebird'
2import * as Sequelize from 'sequelize'
3import { AccountInstance } from '../account/account-interface'
4import { VideoChannelInstance } from './video-channel-interface'
5
6export namespace VideoChannelShareMethods {
7 export type LoadAccountsByShare = (videoChannelId: number, t: Sequelize.Transaction) => Bluebird<AccountInstance[]>
8 export type Load = (accountId: number, videoId: number, t: Sequelize.Transaction) => Bluebird<VideoChannelShareInstance>
9}
10
11export interface VideoChannelShareClass {
12 loadAccountsByShare: VideoChannelShareMethods.LoadAccountsByShare
13 load: VideoChannelShareMethods.Load
14}
15
16export interface VideoChannelShareAttributes {
17 accountId: number
18 videoChannelId: number
19}
20
21export interface VideoChannelShareInstance
22 extends VideoChannelShareClass, VideoChannelShareAttributes, Sequelize.Instance<VideoChannelShareAttributes> {
23 id: number
24 createdAt: Date
25 updatedAt: Date
26
27 Account?: AccountInstance
28 VideoChannel?: VideoChannelInstance
29}
30
31export interface VideoChannelShareModel
32 extends VideoChannelShareClass, Sequelize.Model<VideoChannelShareInstance, VideoChannelShareAttributes> {}
diff --git a/server/models/video/video-channel-share.ts b/server/models/video/video-channel-share.ts
index 2e9b658a3..cdba32fcd 100644
--- a/server/models/video/video-channel-share.ts
+++ b/server/models/video/video-channel-share.ts
@@ -1,85 +1,79 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
3import { AccountModel } from '../account/account'
4import { VideoChannelModel } from './video-channel'
2 5
3import { addMethodsToModel } from '../utils' 6@Table({
4import { VideoChannelShareAttributes, VideoChannelShareInstance, VideoChannelShareMethods } from './video-channel-share-interface' 7 tableName: 'videoChannelShare',
5 8 indexes: [
6let VideoChannelShare: Sequelize.Model<VideoChannelShareInstance, VideoChannelShareAttributes>
7let loadAccountsByShare: VideoChannelShareMethods.LoadAccountsByShare
8let load: VideoChannelShareMethods.Load
9
10export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
11 VideoChannelShare = sequelize.define<VideoChannelShareInstance, VideoChannelShareAttributes>('VideoChannelShare',
12 { },
13 { 9 {
14 indexes: [ 10 fields: [ 'accountId' ]
15 { 11 },
16 fields: [ 'accountId' ] 12 {
17 }, 13 fields: [ 'videoChannelId' ]
18 {
19 fields: [ 'videoChannelId' ]
20 }
21 ]
22 } 14 }
23 )
24
25 const classMethods = [
26 associate,
27 load,
28 loadAccountsByShare
29 ] 15 ]
30 addMethodsToModel(VideoChannelShare, classMethods) 16})
17export class VideoChannelShareModel extends Model<VideoChannelShareModel> {
18 @CreatedAt
19 createdAt: Date
31 20
32 return VideoChannelShare 21 @UpdatedAt
33} 22 updatedAt: Date
34 23
35// ------------------------------ METHODS ------------------------------ 24 @ForeignKey(() => AccountModel)
25 @Column
26 accountId: number
36 27
37function associate (models) { 28 @BelongsTo(() => AccountModel, {
38 VideoChannelShare.belongsTo(models.Account, {
39 foreignKey: { 29 foreignKey: {
40 name: 'accountId',
41 allowNull: false 30 allowNull: false
42 }, 31 },
43 onDelete: 'cascade' 32 onDelete: 'cascade'
44 }) 33 })
34 Account: AccountModel
45 35
46 VideoChannelShare.belongsTo(models.VideoChannel, { 36 @ForeignKey(() => VideoChannelModel)
37 @Column
38 videoChannelId: number
39
40 @BelongsTo(() => VideoChannelModel, {
47 foreignKey: { 41 foreignKey: {
48 name: 'videoChannelId', 42 allowNull: false
49 allowNull: true
50 }, 43 },
51 onDelete: 'cascade' 44 onDelete: 'cascade'
52 }) 45 })
53} 46 VideoChannel: VideoChannelModel
54
55load = function (accountId: number, videoChannelId: number, t: Sequelize.Transaction) {
56 return VideoChannelShare.findOne({
57 where: {
58 accountId,
59 videoChannelId
60 },
61 include: [
62 VideoChannelShare['sequelize'].models.Account,
63 VideoChannelShare['sequelize'].models.VideoChannel
64 ],
65 transaction: t
66 })
67}
68 47
69loadAccountsByShare = function (videoChannelId: number, t: Sequelize.Transaction) { 48 static load (accountId: number, videoChannelId: number, t: Sequelize.Transaction) {
70 const query = { 49 return VideoChannelShareModel.findOne({
71 where: { 50 where: {
72 videoChannelId 51 accountId,
73 }, 52 videoChannelId
74 include: [ 53 },
75 { 54 include: [
76 model: VideoChannelShare['sequelize'].models.Account, 55 AccountModel,
77 required: true 56 VideoChannelModel
78 } 57 ],
79 ], 58 transaction: t
80 transaction: t 59 })
81 } 60 }
82 61
83 return VideoChannelShare.findAll(query) 62 static loadAccountsByShare (videoChannelId: number, t: Sequelize.Transaction) {
84 .then(res => res.map(r => r.Account)) 63 const query = {
64 where: {
65 videoChannelId
66 },
67 include: [
68 {
69 model: AccountModel,
70 required: true
71 }
72 ],
73 transaction: t
74 }
75
76 return VideoChannelShareModel.findAll(query)
77 .then(res => res.map(r => r.Account))
78 }
85} 79}
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts
index 54f12dce3..9b545a4ef 100644
--- a/server/models/video/video-channel.ts
+++ b/server/models/video/video-channel.ts
@@ -1,371 +1,341 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../../helpers' 2import {
3import { CONSTRAINTS_FIELDS } from '../../initializers/constants' 3 AfterDestroy,
4import { sendDeleteVideoChannel } from '../../lib/activitypub/send/send-delete' 4 AllowNull,
5 5 BelongsTo,
6import { addMethodsToModel, getSort } from '../utils' 6 Column,
7import { VideoChannelAttributes, VideoChannelInstance, VideoChannelMethods } from './video-channel-interface' 7 CreatedAt,
8import { getAnnounceActivityPubUrl } from '../../lib/activitypub/url' 8 DataType,
9import { activityPubCollection } from '../../helpers/activitypub' 9 Default,
10import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 10 ForeignKey,
11 11 HasMany,
12let VideoChannel: Sequelize.Model<VideoChannelInstance, VideoChannelAttributes> 12 Is,
13let toFormattedJSON: VideoChannelMethods.ToFormattedJSON 13 IsUUID,
14let toActivityPubObject: VideoChannelMethods.ToActivityPubObject 14 Model,
15let isOwned: VideoChannelMethods.IsOwned 15 Table,
16let countByAccount: VideoChannelMethods.CountByAccount 16 UpdatedAt
17let listForApi: VideoChannelMethods.ListForApi 17} from 'sequelize-typescript'
18let listByAccount: VideoChannelMethods.ListByAccount 18import { IFindOptions } from 'sequelize-typescript/lib/interfaces/IFindOptions'
19let loadByIdAndAccount: VideoChannelMethods.LoadByIdAndAccount 19import { activityPubCollection } from '../../helpers'
20let loadByUUID: VideoChannelMethods.LoadByUUID 20import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub'
21let loadAndPopulateAccount: VideoChannelMethods.LoadAndPopulateAccount 21import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../../helpers/custom-validators/video-channels'
22let loadByUUIDAndPopulateAccount: VideoChannelMethods.LoadByUUIDAndPopulateAccount 22import { CONSTRAINTS_FIELDS } from '../../initializers'
23let loadByHostAndUUID: VideoChannelMethods.LoadByHostAndUUID 23import { getAnnounceActivityPubUrl } from '../../lib/activitypub'
24let loadAndPopulateAccountAndVideos: VideoChannelMethods.LoadAndPopulateAccountAndVideos 24import { sendDeleteVideoChannel } from '../../lib/activitypub/send'
25let loadByUrl: VideoChannelMethods.LoadByUrl 25import { AccountModel } from '../account/account'
26let loadByUUIDOrUrl: VideoChannelMethods.LoadByUUIDOrUrl 26import { ServerModel } from '../server/server'
27 27import { getSort, throwIfNotValid } from '../utils'
28export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { 28import { VideoModel } from './video'
29 VideoChannel = sequelize.define<VideoChannelInstance, VideoChannelAttributes>('VideoChannel', 29import { VideoChannelShareModel } from './video-channel-share'
30
31@Table({
32 tableName: 'videoChannel',
33 indexes: [
30 { 34 {
31 uuid: { 35 fields: [ 'accountId' ]
32 type: DataTypes.UUID,
33 defaultValue: DataTypes.UUIDV4,
34 allowNull: false,
35 validate: {
36 isUUID: 4
37 }
38 },
39 name: {
40 type: DataTypes.STRING,
41 allowNull: false,
42 validate: {
43 nameValid: value => {
44 const res = isVideoChannelNameValid(value)
45 if (res === false) throw new Error('Video channel name is not valid.')
46 }
47 }
48 },
49 description: {
50 type: DataTypes.STRING,
51 allowNull: true,
52 validate: {
53 descriptionValid: value => {
54 const res = isVideoChannelDescriptionValid(value)
55 if (res === false) throw new Error('Video channel description is not valid.')
56 }
57 }
58 },
59 remote: {
60 type: DataTypes.BOOLEAN,
61 allowNull: false,
62 defaultValue: false
63 },
64 url: {
65 type: DataTypes.STRING(CONSTRAINTS_FIELDS.VIDEO_CHANNELS.URL.max),
66 allowNull: false,
67 validate: {
68 urlValid: value => {
69 const res = isActivityPubUrlValid(value)
70 if (res === false) throw new Error('Video channel URL is not valid.')
71 }
72 }
73 }
74 },
75 {
76 indexes: [
77 {
78 fields: [ 'accountId' ]
79 }
80 ],
81 hooks: {
82 afterDestroy
83 }
84 } 36 }
85 )
86
87 const classMethods = [
88 associate,
89
90 listForApi,
91 listByAccount,
92 loadByIdAndAccount,
93 loadAndPopulateAccount,
94 loadByUUIDAndPopulateAccount,
95 loadByUUID,
96 loadByHostAndUUID,
97 loadAndPopulateAccountAndVideos,
98 countByAccount,
99 loadByUrl,
100 loadByUUIDOrUrl
101 ] 37 ]
102 const instanceMethods = [ 38})
103 isOwned, 39export class VideoChannelModel extends Model<VideoChannelModel> {
104 toFormattedJSON,
105 toActivityPubObject
106 ]
107 addMethodsToModel(VideoChannel, classMethods, instanceMethods)
108 40
109 return VideoChannel 41 @AllowNull(false)
110} 42 @Default(DataType.UUIDV4)
43 @IsUUID(4)
44 @Column(DataType.UUID)
45 uuid: string
111 46
112// ------------------------------ METHODS ------------------------------ 47 @AllowNull(false)
48 @Is('VideoChannelName', value => throwIfNotValid(value, isVideoChannelNameValid, 'name'))
49 @Column
50 name: string
113 51
114isOwned = function (this: VideoChannelInstance) { 52 @AllowNull(true)
115 return this.remote === false 53 @Is('VideoChannelDescription', value => throwIfNotValid(value, isVideoChannelDescriptionValid, 'description'))
116} 54 @Column
117 55 description: string
118toFormattedJSON = function (this: VideoChannelInstance) {
119 const json = {
120 id: this.id,
121 uuid: this.uuid,
122 name: this.name,
123 description: this.description,
124 isLocal: this.isOwned(),
125 createdAt: this.createdAt,
126 updatedAt: this.updatedAt
127 }
128 56
129 if (this.Account !== undefined) { 57 @AllowNull(false)
130 json['owner'] = { 58 @Column
131 name: this.Account.name, 59 remote: boolean
132 uuid: this.Account.uuid
133 }
134 }
135 60
136 if (Array.isArray(this.Videos)) { 61 @AllowNull(false)
137 json['videos'] = this.Videos.map(v => v.toFormattedJSON()) 62 @Is('VideoChannelUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
138 } 63 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_CHANNELS.URL.max))
64 url: string
139 65
140 return json 66 @CreatedAt
141} 67 createdAt: Date
142 68
143toActivityPubObject = function (this: VideoChannelInstance) { 69 @UpdatedAt
144 let sharesObject 70 updatedAt: Date
145 if (Array.isArray(this.VideoChannelShares)) {
146 const shares: string[] = []
147 71
148 for (const videoChannelShare of this.VideoChannelShares) { 72 @ForeignKey(() => AccountModel)
149 const shareUrl = getAnnounceActivityPubUrl(this.url, videoChannelShare.Account) 73 @Column
150 shares.push(shareUrl) 74 accountId: number
151 }
152 75
153 sharesObject = activityPubCollection(shares) 76 @BelongsTo(() => AccountModel, {
154 } 77 foreignKey: {
155 78 allowNull: false
156 const json = { 79 },
157 type: 'VideoChannel' as 'VideoChannel', 80 onDelete: 'CASCADE'
158 id: this.url, 81 })
159 uuid: this.uuid, 82 Account: AccountModel
160 content: this.description,
161 name: this.name,
162 published: this.createdAt.toISOString(),
163 updated: this.updatedAt.toISOString(),
164 shares: sharesObject
165 }
166
167 return json
168}
169
170// ------------------------------ STATICS ------------------------------
171 83
172function associate (models) { 84 @HasMany(() => VideoModel, {
173 VideoChannel.belongsTo(models.Account, {
174 foreignKey: { 85 foreignKey: {
175 name: 'accountId', 86 name: 'channelId',
176 allowNull: false 87 allowNull: false
177 }, 88 },
178 onDelete: 'CASCADE' 89 onDelete: 'CASCADE'
179 }) 90 })
91 Videos: VideoModel[]
180 92
181 VideoChannel.hasMany(models.Video, { 93 @HasMany(() => VideoChannelShareModel, {
182 foreignKey: { 94 foreignKey: {
183 name: 'channelId', 95 name: 'channelId',
184 allowNull: false 96 allowNull: false
185 }, 97 },
186 onDelete: 'CASCADE' 98 onDelete: 'CASCADE'
187 }) 99 })
188} 100 VideoChannelShares: VideoChannelShareModel[]
189 101
190function afterDestroy (videoChannel: VideoChannelInstance) { 102 @AfterDestroy
191 if (videoChannel.isOwned()) { 103 static sendDeleteIfOwned (instance: VideoChannelModel) {
192 return sendDeleteVideoChannel(videoChannel, undefined) 104 if (instance.isOwned()) {
193 } 105 return sendDeleteVideoChannel(instance, undefined)
106 }
194 107
195 return undefined 108 return undefined
196} 109 }
197 110
198countByAccount = function (accountId: number) { 111 static countByAccount (accountId: number) {
199 const query = { 112 const query = {
200 where: { 113 where: {
201 accountId 114 accountId
115 }
202 } 116 }
117
118 return VideoChannelModel.count(query)
203 } 119 }
204 120
205 return VideoChannel.count(query) 121 static listForApi (start: number, count: number, sort: string) {
206} 122 const query = {
123 offset: start,
124 limit: count,
125 order: [ getSort(sort) ],
126 include: [
127 {
128 model: AccountModel,
129 required: true,
130 include: [ { model: ServerModel, required: false } ]
131 }
132 ]
133 }
207 134
208listForApi = function (start: number, count: number, sort: string) { 135 return VideoChannelModel.findAndCountAll(query)
209 const query = { 136 .then(({ rows, count }) => {
210 offset: start, 137 return { total: count, data: rows }
211 limit: count, 138 })
212 order: [ getSort(sort) ],
213 include: [
214 {
215 model: VideoChannel['sequelize'].models.Account,
216 required: true,
217 include: [ { model: VideoChannel['sequelize'].models.Server, required: false } ]
218 }
219 ]
220 } 139 }
221 140
222 return VideoChannel.findAndCountAll(query).then(({ rows, count }) => { 141 static listByAccount (accountId: number) {
223 return { total: count, data: rows } 142 const query = {
224 }) 143 order: [ getSort('createdAt') ],
225} 144 include: [
145 {
146 model: AccountModel,
147 where: {
148 id: accountId
149 },
150 required: true,
151 include: [ { model: ServerModel, required: false } ]
152 }
153 ]
154 }
226 155
227listByAccount = function (accountId: number) { 156 return VideoChannelModel.findAndCountAll(query)
228 const query = { 157 .then(({ rows, count }) => {
229 order: [ getSort('createdAt') ], 158 return { total: count, data: rows }
230 include: [ 159 })
231 {
232 model: VideoChannel['sequelize'].models.Account,
233 where: {
234 id: accountId
235 },
236 required: true,
237 include: [ { model: VideoChannel['sequelize'].models.Server, required: false } ]
238 }
239 ]
240 } 160 }
241 161
242 return VideoChannel.findAndCountAll(query).then(({ rows, count }) => { 162 static loadByUUID (uuid: string, t?: Sequelize.Transaction) {
243 return { total: count, data: rows } 163 const query: IFindOptions<VideoChannelModel> = {
244 }) 164 where: {
245} 165 uuid
166 }
167 }
168
169 if (t !== undefined) query.transaction = t
246 170
247loadByUUID = function (uuid: string, t?: Sequelize.Transaction) { 171 return VideoChannelModel.findOne(query)
248 const query: Sequelize.FindOptions<VideoChannelAttributes> = { 172 }
249 where: { 173
250 uuid 174 static loadByUrl (url: string, t?: Sequelize.Transaction) {
175 const query: IFindOptions<VideoChannelModel> = {
176 where: {
177 url
178 },
179 include: [ AccountModel ]
251 } 180 }
181
182 if (t !== undefined) query.transaction = t
183
184 return VideoChannelModel.findOne(query)
252 } 185 }
253 186
254 if (t !== undefined) query.transaction = t 187 static loadByUUIDOrUrl (uuid: string, url: string, t?: Sequelize.Transaction) {
188 const query: IFindOptions<VideoChannelModel> = {
189 where: {
190 [ Sequelize.Op.or ]: [
191 { uuid },
192 { url }
193 ]
194 }
195 }
255 196
256 return VideoChannel.findOne(query) 197 if (t !== undefined) query.transaction = t
257}
258 198
259loadByUrl = function (url: string, t?: Sequelize.Transaction) { 199 return VideoChannelModel.findOne(query)
260 const query: Sequelize.FindOptions<VideoChannelAttributes> = {
261 where: {
262 url
263 },
264 include: [ VideoChannel['sequelize'].models.Account ]
265 } 200 }
266 201
267 if (t !== undefined) query.transaction = t 202 static loadByHostAndUUID (fromHost: string, uuid: string, t?: Sequelize.Transaction) {
203 const query: IFindOptions<VideoChannelModel> = {
204 where: {
205 uuid
206 },
207 include: [
208 {
209 model: AccountModel,
210 include: [
211 {
212 model: ServerModel,
213 required: true,
214 where: {
215 host: fromHost
216 }
217 }
218 ]
219 }
220 ]
221 }
268 222
269 return VideoChannel.findOne(query) 223 if (t !== undefined) query.transaction = t
270} 224
225 return VideoChannelModel.findOne(query)
226 }
271 227
272loadByUUIDOrUrl = function (uuid: string, url: string, t?: Sequelize.Transaction) { 228 static loadByIdAndAccount (id: number, accountId: number) {
273 const query: Sequelize.FindOptions<VideoChannelAttributes> = { 229 const options = {
274 where: { 230 where: {
275 [Sequelize.Op.or]: [ 231 id,
276 { uuid }, 232 accountId
277 { url } 233 },
234 include: [
235 {
236 model: AccountModel,
237 include: [ { model: ServerModel, required: false } ]
238 }
278 ] 239 ]
279 } 240 }
241
242 return VideoChannelModel.findOne(options)
280 } 243 }
281 244
282 if (t !== undefined) query.transaction = t 245 static loadAndPopulateAccount (id: number) {
246 const options = {
247 include: [
248 {
249 model: AccountModel,
250 include: [ { model: ServerModel, required: false } ]
251 }
252 ]
253 }
283 254
284 return VideoChannel.findOne(query) 255 return VideoChannelModel.findById(id, options)
285} 256 }
286 257
287loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Transaction) { 258 static loadByUUIDAndPopulateAccount (uuid: string) {
288 const query: Sequelize.FindOptions<VideoChannelAttributes> = { 259 const options = {
289 where: { 260 where: {
290 uuid 261 uuid
291 }, 262 },
292 include: [ 263 include: [
293 { 264 {
294 model: VideoChannel['sequelize'].models.Account, 265 model: AccountModel,
295 include: [ 266 include: [ { model: ServerModel, required: false } ]
296 { 267 }
297 model: VideoChannel['sequelize'].models.Server, 268 ]
298 required: true, 269 }
299 where: { 270
300 host: fromHost 271 return VideoChannelModel.findOne(options)
301 }
302 }
303 ]
304 }
305 ]
306 } 272 }
307 273
308 if (t !== undefined) query.transaction = t 274 static loadAndPopulateAccountAndVideos (id: number) {
275 const options = {
276 include: [
277 {
278 model: AccountModel,
279 include: [ { model: ServerModel, required: false } ]
280 },
281 VideoModel
282 ]
283 }
309 284
310 return VideoChannel.findOne(query) 285 return VideoChannelModel.findById(id, options)
311} 286 }
312 287
313loadByIdAndAccount = function (id: number, accountId: number) { 288 isOwned () {
314 const options = { 289 return this.remote === false
315 where: {
316 id,
317 accountId
318 },
319 include: [
320 {
321 model: VideoChannel['sequelize'].models.Account,
322 include: [ { model: VideoChannel['sequelize'].models.Server, required: false } ]
323 }
324 ]
325 } 290 }
326 291
327 return VideoChannel.findOne(options) 292 toFormattedJSON () {
328} 293 const json = {
294 id: this.id,
295 uuid: this.uuid,
296 name: this.name,
297 description: this.description,
298 isLocal: this.isOwned(),
299 createdAt: this.createdAt,
300 updatedAt: this.updatedAt
301 }
329 302
330loadAndPopulateAccount = function (id: number) { 303 if (this.Account !== undefined) {
331 const options = { 304 json[ 'owner' ] = {
332 include: [ 305 name: this.Account.name,
333 { 306 uuid: this.Account.uuid
334 model: VideoChannel['sequelize'].models.Account,
335 include: [ { model: VideoChannel['sequelize'].models.Server, required: false } ]
336 } 307 }
337 ] 308 }
309
310 if (Array.isArray(this.Videos)) {
311 json[ 'videos' ] = this.Videos.map(v => v.toFormattedJSON())
312 }
313
314 return json
338 } 315 }
339 316
340 return VideoChannel.findById(id, options) 317 toActivityPubObject () {
341} 318 let sharesObject
319 if (Array.isArray(this.VideoChannelShares)) {
320 const shares: string[] = []
342 321
343loadByUUIDAndPopulateAccount = function (uuid: string) { 322 for (const videoChannelShare of this.VideoChannelShares) {
344 const options = { 323 const shareUrl = getAnnounceActivityPubUrl(this.url, videoChannelShare.Account)
345 where: { 324 shares.push(shareUrl)
346 uuid
347 },
348 include: [
349 {
350 model: VideoChannel['sequelize'].models.Account,
351 include: [ { model: VideoChannel['sequelize'].models.Server, required: false } ]
352 } 325 }
353 ]
354 }
355 326
356 return VideoChannel.findOne(options) 327 sharesObject = activityPubCollection(shares)
357} 328 }
358 329
359loadAndPopulateAccountAndVideos = function (id: number) { 330 return {
360 const options = { 331 type: 'VideoChannel' as 'VideoChannel',
361 include: [ 332 id: this.url,
362 { 333 uuid: this.uuid,
363 model: VideoChannel['sequelize'].models.Account, 334 content: this.description,
364 include: [ { model: VideoChannel['sequelize'].models.Server, required: false } ] 335 name: this.name,
365 }, 336 published: this.createdAt.toISOString(),
366 VideoChannel['sequelize'].models.Video 337 updated: this.updatedAt.toISOString(),
367 ] 338 shares: sharesObject
339 }
368 } 340 }
369
370 return VideoChannel.findById(id, options)
371} 341}
diff --git a/server/models/video/video-file-interface.ts b/server/models/video/video-file-interface.ts
deleted file mode 100644
index c9fb8b8ae..000000000
--- a/server/models/video/video-file-interface.ts
+++ /dev/null
@@ -1,24 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3export namespace VideoFileMethods {
4}
5
6export interface VideoFileClass {
7}
8
9export interface VideoFileAttributes {
10 resolution: number
11 size: number
12 infoHash?: string
13 extname: string
14
15 videoId?: number
16}
17
18export interface VideoFileInstance extends VideoFileClass, VideoFileAttributes, Sequelize.Instance<VideoFileAttributes> {
19 id: number
20 createdAt: Date
21 updatedAt: Date
22}
23
24export interface VideoFileModel extends VideoFileClass, Sequelize.Model<VideoFileInstance, VideoFileAttributes> {}
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts
index 600141994..df4067a4e 100644
--- a/server/models/video/video-file.ts
+++ b/server/models/video/video-file.ts
@@ -1,81 +1,56 @@
1import { values } from 'lodash' 1import { values } from 'lodash'
2import * as Sequelize from 'sequelize' 2import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
3import { isVideoFileInfoHashValid, isVideoFileResolutionValid, isVideoFileSizeValid } from '../../helpers/custom-validators/videos' 3import { isVideoFileInfoHashValid, isVideoFileResolutionValid, isVideoFileSizeValid } from '../../helpers/custom-validators/videos'
4import { CONSTRAINTS_FIELDS } from '../../initializers/constants' 4import { CONSTRAINTS_FIELDS } from '../../initializers'
5import { throwIfNotValid } from '../utils'
6import { VideoModel } from './video'
5 7
6import { addMethodsToModel } from '../utils' 8@Table({
7import { VideoFileAttributes, VideoFileInstance } from './video-file-interface' 9 tableName: 'videoFile',
8 10 indexes: [
9let VideoFile: Sequelize.Model<VideoFileInstance, VideoFileAttributes>
10
11export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
12 VideoFile = sequelize.define<VideoFileInstance, VideoFileAttributes>('VideoFile',
13 { 11 {
14 resolution: { 12 fields: [ 'videoId' ]
15 type: DataTypes.INTEGER,
16 allowNull: false,
17 validate: {
18 resolutionValid: value => {
19 const res = isVideoFileResolutionValid(value)
20 if (res === false) throw new Error('Video file resolution is not valid.')
21 }
22 }
23 },
24 size: {
25 type: DataTypes.BIGINT,
26 allowNull: false,
27 validate: {
28 sizeValid: value => {
29 const res = isVideoFileSizeValid(value)
30 if (res === false) throw new Error('Video file size is not valid.')
31 }
32 }
33 },
34 extname: {
35 type: DataTypes.ENUM(values(CONSTRAINTS_FIELDS.VIDEOS.EXTNAME)),
36 allowNull: false
37 },
38 infoHash: {
39 type: DataTypes.STRING,
40 allowNull: false,
41 validate: {
42 infoHashValid: value => {
43 const res = isVideoFileInfoHashValid(value)
44 if (res === false) throw new Error('Video file info hash is not valid.')
45 }
46 }
47 }
48 }, 13 },
49 { 14 {
50 indexes: [ 15 fields: [ 'infoHash' ]
51 {
52 fields: [ 'videoId' ]
53 },
54 {
55 fields: [ 'infoHash' ]
56 }
57 ]
58 } 16 }
59 )
60
61 const classMethods = [
62 associate
63 ] 17 ]
64 addMethodsToModel(VideoFile, classMethods) 18})
65 19export class VideoFileModel extends Model<VideoFileModel> {
66 return VideoFile 20 @CreatedAt
67} 21 createdAt: Date
68 22
69// ------------------------------ STATICS ------------------------------ 23 @UpdatedAt
70 24 updatedAt: Date
71function associate (models) { 25
72 VideoFile.belongsTo(models.Video, { 26 @AllowNull(false)
27 @Is('VideoFileResolution', value => throwIfNotValid(value, isVideoFileResolutionValid, 'resolution'))
28 @Column
29 resolution: number
30
31 @AllowNull(false)
32 @Is('VideoFileSize', value => throwIfNotValid(value, isVideoFileSizeValid, 'size'))
33 @Column(DataType.BIGINT)
34 size: number
35
36 @AllowNull(false)
37 @Column(DataType.ENUM(values(CONSTRAINTS_FIELDS.VIDEOS.EXTNAME)))
38 extname: string
39
40 @AllowNull(false)
41 @Is('VideoFileSize', value => throwIfNotValid(value, isVideoFileInfoHashValid, 'info hash'))
42 @Column
43 infoHash: string
44
45 @ForeignKey(() => VideoModel)
46 @Column
47 videoId: number
48
49 @BelongsTo(() => VideoModel, {
73 foreignKey: { 50 foreignKey: {
74 name: 'videoId',
75 allowNull: false 51 allowNull: false
76 }, 52 },
77 onDelete: 'CASCADE' 53 onDelete: 'CASCADE'
78 }) 54 })
55 Video: VideoModel
79} 56}
80
81// ------------------------------ METHODS ------------------------------
diff --git a/server/models/video/video-interface.ts b/server/models/video/video-interface.ts
deleted file mode 100644
index 2a63350af..000000000
--- a/server/models/video/video-interface.ts
+++ /dev/null
@@ -1,150 +0,0 @@
1import * as Bluebird from 'bluebird'
2import * as Sequelize from 'sequelize'
3import { VideoTorrentObject } from '../../../shared/models/activitypub/objects/video-torrent-object'
4import { ResultList } from '../../../shared/models/result-list.model'
5import { Video as FormattedVideo, VideoDetails as FormattedDetailsVideo } from '../../../shared/models/videos/video.model'
6import { AccountVideoRateInstance } from '../account/account-video-rate-interface'
7
8import { TagAttributes, TagInstance } from './tag-interface'
9import { VideoChannelInstance } from './video-channel-interface'
10import { VideoFileAttributes, VideoFileInstance } from './video-file-interface'
11import { VideoShareInstance } from './video-share-interface'
12
13export namespace VideoMethods {
14 export type GetThumbnailName = (this: VideoInstance) => string
15 export type GetPreviewName = (this: VideoInstance) => string
16 export type IsOwned = (this: VideoInstance) => boolean
17 export type ToFormattedJSON = (this: VideoInstance) => FormattedVideo
18 export type ToFormattedDetailsJSON = (this: VideoInstance) => FormattedDetailsVideo
19
20 export type GetOriginalFile = (this: VideoInstance) => VideoFileInstance
21 export type GetTorrentFileName = (this: VideoInstance, videoFile: VideoFileInstance) => string
22 export type GetVideoFilename = (this: VideoInstance, videoFile: VideoFileInstance) => string
23 export type CreatePreview = (this: VideoInstance, videoFile: VideoFileInstance) => Promise<string>
24 export type CreateThumbnail = (this: VideoInstance, videoFile: VideoFileInstance) => Promise<string>
25 export type GetVideoFilePath = (this: VideoInstance, videoFile: VideoFileInstance) => string
26 export type CreateTorrentAndSetInfoHash = (this: VideoInstance, videoFile: VideoFileInstance) => Promise<void>
27
28 export type ToActivityPubObject = (this: VideoInstance) => VideoTorrentObject
29
30 export type OptimizeOriginalVideofile = (this: VideoInstance) => Promise<void>
31 export type TranscodeOriginalVideofile = (this: VideoInstance, resolution: number) => Promise<void>
32 export type GetOriginalFileHeight = (this: VideoInstance) => Promise<number>
33 export type GetEmbedPath = (this: VideoInstance) => string
34 export type GetThumbnailPath = (this: VideoInstance) => string
35 export type GetPreviewPath = (this: VideoInstance) => string
36 export type GetDescriptionPath = (this: VideoInstance) => string
37 export type GetTruncatedDescription = (this: VideoInstance) => string
38 export type GetCategoryLabel = (this: VideoInstance) => string
39 export type GetLicenceLabel = (this: VideoInstance) => string
40 export type GetLanguageLabel = (this: VideoInstance) => string
41
42 export type List = () => Bluebird<VideoInstance[]>
43
44 export type ListAllAndSharedByAccountForOutbox = (
45 accountId: number,
46 start: number,
47 count: number
48 ) => Bluebird< ResultList<VideoInstance> >
49 export type ListForApi = (start: number, count: number, sort: string) => Bluebird< ResultList<VideoInstance> >
50 export type ListUserVideosForApi = (userId: number, start: number, count: number, sort: string) => Bluebird< ResultList<VideoInstance> >
51 export type SearchAndPopulateAccountAndServerAndTags = (
52 value: string,
53 start: number,
54 count: number,
55 sort: string
56 ) => Bluebird< ResultList<VideoInstance> >
57
58 export type Load = (id: number) => Bluebird<VideoInstance>
59 export type LoadByUUID = (uuid: string, t?: Sequelize.Transaction) => Bluebird<VideoInstance>
60 export type LoadByUrlAndPopulateAccount = (url: string, t?: Sequelize.Transaction) => Bluebird<VideoInstance>
61 export type LoadAndPopulateAccountAndServerAndTags = (id: number) => Bluebird<VideoInstance>
62 export type LoadByUUIDAndPopulateAccountAndServerAndTags = (uuid: string) => Bluebird<VideoInstance>
63 export type LoadByUUIDOrURL = (uuid: string, url: string, t?: Sequelize.Transaction) => Bluebird<VideoInstance>
64
65 export type RemoveThumbnail = (this: VideoInstance) => Promise<void>
66 export type RemovePreview = (this: VideoInstance) => Promise<void>
67 export type RemoveFile = (this: VideoInstance, videoFile: VideoFileInstance) => Promise<void>
68 export type RemoveTorrent = (this: VideoInstance, videoFile: VideoFileInstance) => Promise<void>
69}
70
71export interface VideoClass {
72 list: VideoMethods.List
73 listAllAndSharedByAccountForOutbox: VideoMethods.ListAllAndSharedByAccountForOutbox
74 listForApi: VideoMethods.ListForApi
75 listUserVideosForApi: VideoMethods.ListUserVideosForApi
76 load: VideoMethods.Load
77 loadAndPopulateAccountAndServerAndTags: VideoMethods.LoadAndPopulateAccountAndServerAndTags
78 loadByUUID: VideoMethods.LoadByUUID
79 loadByUrlAndPopulateAccount: VideoMethods.LoadByUrlAndPopulateAccount
80 loadByUUIDOrURL: VideoMethods.LoadByUUIDOrURL
81 loadByUUIDAndPopulateAccountAndServerAndTags: VideoMethods.LoadByUUIDAndPopulateAccountAndServerAndTags
82 searchAndPopulateAccountAndServerAndTags: VideoMethods.SearchAndPopulateAccountAndServerAndTags
83}
84
85export interface VideoAttributes {
86 id?: number
87 uuid?: string
88 name: string
89 category: number
90 licence: number
91 language: number
92 nsfw: boolean
93 description: string
94 duration: number
95 privacy: number
96 views?: number
97 likes?: number
98 dislikes?: number
99 remote: boolean
100 url?: string
101
102 createdAt?: Date
103 updatedAt?: Date
104
105 parentId?: number
106 channelId?: number
107
108 VideoChannel?: VideoChannelInstance
109 Tags?: TagInstance[]
110 VideoFiles?: VideoFileInstance[]
111 VideoShares?: VideoShareInstance[]
112 AccountVideoRates?: AccountVideoRateInstance[]
113}
114
115export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.Instance<VideoAttributes> {
116 createPreview: VideoMethods.CreatePreview
117 createThumbnail: VideoMethods.CreateThumbnail
118 createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash
119 getOriginalFile: VideoMethods.GetOriginalFile
120 getPreviewName: VideoMethods.GetPreviewName
121 getPreviewPath: VideoMethods.GetPreviewPath
122 getThumbnailName: VideoMethods.GetThumbnailName
123 getThumbnailPath: VideoMethods.GetThumbnailPath
124 getTorrentFileName: VideoMethods.GetTorrentFileName
125 getVideoFilename: VideoMethods.GetVideoFilename
126 getVideoFilePath: VideoMethods.GetVideoFilePath
127 isOwned: VideoMethods.IsOwned
128 removeFile: VideoMethods.RemoveFile
129 removePreview: VideoMethods.RemovePreview
130 removeThumbnail: VideoMethods.RemoveThumbnail
131 removeTorrent: VideoMethods.RemoveTorrent
132 toActivityPubObject: VideoMethods.ToActivityPubObject
133 toFormattedJSON: VideoMethods.ToFormattedJSON
134 toFormattedDetailsJSON: VideoMethods.ToFormattedDetailsJSON
135 optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile
136 transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile
137 getOriginalFileHeight: VideoMethods.GetOriginalFileHeight
138 getEmbedPath: VideoMethods.GetEmbedPath
139 getDescriptionPath: VideoMethods.GetDescriptionPath
140 getTruncatedDescription: VideoMethods.GetTruncatedDescription
141 getCategoryLabel: VideoMethods.GetCategoryLabel
142 getLicenceLabel: VideoMethods.GetLicenceLabel
143 getLanguageLabel: VideoMethods.GetLanguageLabel
144
145 setTags: Sequelize.HasManySetAssociationsMixin<TagAttributes, string>
146 addVideoFile: Sequelize.HasManyAddAssociationMixin<VideoFileAttributes, string>
147 setVideoFiles: Sequelize.HasManySetAssociationsMixin<VideoFileAttributes, string>
148}
149
150export interface VideoModel extends VideoClass, Sequelize.Model<VideoInstance, VideoAttributes> {}
diff --git a/server/models/video/video-share-interface.ts b/server/models/video/video-share-interface.ts
deleted file mode 100644
index 3946303f1..000000000
--- a/server/models/video/video-share-interface.ts
+++ /dev/null
@@ -1,30 +0,0 @@
1import * as Bluebird from 'bluebird'
2import * as Sequelize from 'sequelize'
3import { AccountInstance } from '../account/account-interface'
4import { VideoInstance } from './video-interface'
5
6export namespace VideoShareMethods {
7 export type LoadAccountsByShare = (videoId: number, t: Sequelize.Transaction) => Bluebird<AccountInstance[]>
8 export type Load = (accountId: number, videoId: number, t: Sequelize.Transaction) => Bluebird<VideoShareInstance>
9}
10
11export interface VideoShareClass {
12 loadAccountsByShare: VideoShareMethods.LoadAccountsByShare
13 load: VideoShareMethods.Load
14}
15
16export interface VideoShareAttributes {
17 accountId: number
18 videoId: number
19}
20
21export interface VideoShareInstance extends VideoShareClass, VideoShareAttributes, Sequelize.Instance<VideoShareAttributes> {
22 id: number
23 createdAt: Date
24 updatedAt: Date
25
26 Account?: AccountInstance
27 Video?: VideoInstance
28}
29
30export interface VideoShareModel extends VideoShareClass, Sequelize.Model<VideoShareInstance, VideoShareAttributes> {}
diff --git a/server/models/video/video-share.ts b/server/models/video/video-share.ts
index 37e405fa9..01b6d3d34 100644
--- a/server/models/video/video-share.ts
+++ b/server/models/video/video-share.ts
@@ -1,84 +1,78 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
3import { AccountModel } from '../account/account'
4import { VideoModel } from './video'
2 5
3import { addMethodsToModel } from '../utils' 6@Table({
4import { VideoShareAttributes, VideoShareInstance, VideoShareMethods } from './video-share-interface' 7 tableName: 'videoShare',
5 8 indexes: [
6let VideoShare: Sequelize.Model<VideoShareInstance, VideoShareAttributes>
7let loadAccountsByShare: VideoShareMethods.LoadAccountsByShare
8let load: VideoShareMethods.Load
9
10export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
11 VideoShare = sequelize.define<VideoShareInstance, VideoShareAttributes>('VideoShare',
12 { },
13 { 9 {
14 indexes: [ 10 fields: [ 'accountId' ]
15 { 11 },
16 fields: [ 'accountId' ] 12 {
17 }, 13 fields: [ 'videoId' ]
18 {
19 fields: [ 'videoId' ]
20 }
21 ]
22 } 14 }
23 )
24
25 const classMethods = [
26 associate,
27 loadAccountsByShare,
28 load
29 ] 15 ]
30 addMethodsToModel(VideoShare, classMethods) 16})
17export class VideoShareModel extends Model<VideoShareModel> {
18 @CreatedAt
19 createdAt: Date
31 20
32 return VideoShare 21 @UpdatedAt
33} 22 updatedAt: Date
34 23
35// ------------------------------ METHODS ------------------------------ 24 @ForeignKey(() => AccountModel)
25 @Column
26 accountId: number
36 27
37function associate (models) { 28 @BelongsTo(() => AccountModel, {
38 VideoShare.belongsTo(models.Account, {
39 foreignKey: { 29 foreignKey: {
40 name: 'accountId',
41 allowNull: false 30 allowNull: false
42 }, 31 },
43 onDelete: 'cascade' 32 onDelete: 'cascade'
44 }) 33 })
34 Account: AccountModel
45 35
46 VideoShare.belongsTo(models.Video, { 36 @ForeignKey(() => VideoModel)
37 @Column
38 videoId: number
39
40 @BelongsTo(() => VideoModel, {
47 foreignKey: { 41 foreignKey: {
48 name: 'videoId', 42 allowNull: false
49 allowNull: true
50 }, 43 },
51 onDelete: 'cascade' 44 onDelete: 'cascade'
52 }) 45 })
53} 46 Video: VideoModel
54
55load = function (accountId: number, videoId: number, t: Sequelize.Transaction) {
56 return VideoShare.findOne({
57 where: {
58 accountId,
59 videoId
60 },
61 include: [
62 VideoShare['sequelize'].models.Account
63 ],
64 transaction: t
65 })
66}
67 47
68loadAccountsByShare = function (videoId: number, t: Sequelize.Transaction) { 48 static load (accountId: number, videoId: number, t: Sequelize.Transaction) {
69 const query = { 49 return VideoShareModel.findOne({
70 where: { 50 where: {
71 videoId 51 accountId,
72 }, 52 videoId
73 include: [ 53 },
74 { 54 include: [
75 model: VideoShare['sequelize'].models.Account, 55 AccountModel
76 required: true 56 ],
77 } 57 transaction: t
78 ], 58 })
79 transaction: t
80 } 59 }
81 60
82 return VideoShare.findAll(query) 61 static loadAccountsByShare (videoId: number, t: Sequelize.Transaction) {
83 .then(res => res.map(r => r.Account)) 62 const query = {
63 where: {
64 videoId
65 },
66 include: [
67 {
68 model: AccountModel,
69 required: true
70 }
71 ],
72 transaction: t
73 }
74
75 return VideoShareModel.findAll(query)
76 .then(res => res.map(r => r.Account))
77 }
84} 78}
diff --git a/server/models/video/video-tag-interface.ts b/server/models/video/video-tag-interface.ts
deleted file mode 100644
index f928cecff..000000000
--- a/server/models/video/video-tag-interface.ts
+++ /dev/null
@@ -1,18 +0,0 @@
1import * as Sequelize from 'sequelize'
2
3export namespace VideoTagMethods {
4}
5
6export interface VideoTagClass {
7}
8
9export interface VideoTagAttributes {
10}
11
12export interface VideoTagInstance extends VideoTagClass, VideoTagAttributes, Sequelize.Instance<VideoTagAttributes> {
13 id: number
14 createdAt: Date
15 updatedAt: Date
16}
17
18export interface VideoTagModel extends VideoTagClass, Sequelize.Model<VideoTagInstance, VideoTagAttributes> {}
diff --git a/server/models/video/video-tag.ts b/server/models/video/video-tag.ts
index ac45374f8..ca15e3426 100644
--- a/server/models/video/video-tag.ts
+++ b/server/models/video/video-tag.ts
@@ -1,23 +1,30 @@
1import * as Sequelize from 'sequelize' 1import { Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
2import { TagModel } from './tag'
3import { VideoModel } from './video'
2 4
3import { 5@Table({
4 VideoTagInstance, 6 tableName: 'videoTag',
5 VideoTagAttributes 7 indexes: [
6} from './video-tag-interface' 8 {
9 fields: [ 'videoId' ]
10 },
11 {
12 fields: [ 'tagId' ]
13 }
14 ]
15})
16export class VideoTagModel extends Model<VideoTagModel> {
17 @CreatedAt
18 createdAt: Date
7 19
8let VideoTag: Sequelize.Model<VideoTagInstance, VideoTagAttributes> 20 @UpdatedAt
21 updatedAt: Date
9 22
10export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { 23 @ForeignKey(() => VideoModel)
11 VideoTag = sequelize.define<VideoTagInstance, VideoTagAttributes>('VideoTag', {}, { 24 @Column
12 indexes: [ 25 videoId: number
13 {
14 fields: [ 'videoId' ]
15 },
16 {
17 fields: [ 'tagId' ]
18 }
19 ]
20 })
21 26
22 return VideoTag 27 @ForeignKey(() => TagModel)
28 @Column
29 tagId: number
23} 30}
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index d46fdeebe..9e26f9bbe 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -4,21 +4,52 @@ import * as magnetUtil from 'magnet-uri'
4import * as parseTorrent from 'parse-torrent' 4import * as parseTorrent from 'parse-torrent'
5import { join } from 'path' 5import { join } from 'path'
6import * as Sequelize from 'sequelize' 6import * as Sequelize from 'sequelize'
7import {
8 AfterDestroy,
9 AllowNull,
10 BelongsTo,
11 BelongsToMany,
12 Column,
13 CreatedAt,
14 DataType,
15 Default,
16 ForeignKey,
17 HasMany,
18 IFindOptions,
19 Is,
20 IsInt,
21 IsUUID,
22 Min,
23 Model,
24 Table,
25 UpdatedAt
26} from 'sequelize-typescript'
27import { IIncludeOptions } from 'sequelize-typescript/lib/interfaces/IIncludeOptions'
7import { VideoPrivacy, VideoResolution } from '../../../shared' 28import { VideoPrivacy, VideoResolution } from '../../../shared'
8import { VideoTorrentObject } from '../../../shared/models/activitypub/objects/video-torrent-object' 29import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
9import { activityPubCollection } from '../../helpers/activitypub' 30import {
10import { createTorrentPromise, renamePromise, statPromise, unlinkPromise, writeFilePromise } from '../../helpers/core-utils' 31 activityPubCollection,
11import { isVideoCategoryValid, isVideoLanguageValid, isVideoPrivacyValid } from '../../helpers/custom-validators/videos' 32 createTorrentPromise,
12import { generateImageFromVideoFile, getVideoFileHeight, transcode } from '../../helpers/ffmpeg-utils' 33 generateImageFromVideoFile,
34 getVideoFileHeight,
35 logger,
36 renamePromise,
37 statPromise,
38 transcode,
39 unlinkPromise,
40 writeFilePromise
41} from '../../helpers'
42import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub'
13import { 43import {
14 isActivityPubUrlValid, 44 isVideoCategoryValid,
15 isVideoDescriptionValid, 45 isVideoDescriptionValid,
16 isVideoDurationValid, 46 isVideoDurationValid,
47 isVideoLanguageValid,
17 isVideoLicenceValid, 48 isVideoLicenceValid,
18 isVideoNameValid, 49 isVideoNameValid,
19 isVideoNSFWValid 50 isVideoNSFWValid,
20} from '../../helpers/index' 51 isVideoPrivacyValid
21import { logger } from '../../helpers/logger' 52} from '../../helpers/custom-validators/videos'
22import { 53import {
23 API_VERSION, 54 API_VERSION,
24 CONFIG, 55 CONFIG,
@@ -31,1169 +62,1025 @@ import {
31 VIDEO_LANGUAGES, 62 VIDEO_LANGUAGES,
32 VIDEO_LICENCES, 63 VIDEO_LICENCES,
33 VIDEO_PRIVACIES 64 VIDEO_PRIVACIES
34} from '../../initializers/constants' 65} from '../../initializers'
35import { getAnnounceActivityPubUrl } from '../../lib/activitypub/url' 66import { getAnnounceActivityPubUrl } from '../../lib/activitypub'
36import { sendDeleteVideo } from '../../lib/index' 67import { sendDeleteVideo } from '../../lib/index'
37import { addMethodsToModel, getSort } from '../utils' 68import { AccountModel } from '../account/account'
38import { TagInstance } from './tag-interface' 69import { AccountVideoRateModel } from '../account/account-video-rate'
39import { VideoFileInstance, VideoFileModel } from './video-file-interface' 70import { ServerModel } from '../server/server'
40import { VideoAttributes, VideoInstance, VideoMethods } from './video-interface' 71import { getSort, throwIfNotValid } from '../utils'
41 72import { TagModel } from './tag'
42let Video: Sequelize.Model<VideoInstance, VideoAttributes> 73import { VideoAbuseModel } from './video-abuse'
43let getOriginalFile: VideoMethods.GetOriginalFile 74import { VideoChannelModel } from './video-channel'
44let getVideoFilename: VideoMethods.GetVideoFilename 75import { VideoFileModel } from './video-file'
45let getThumbnailName: VideoMethods.GetThumbnailName 76import { VideoShareModel } from './video-share'
46let getThumbnailPath: VideoMethods.GetThumbnailPath 77import { VideoTagModel } from './video-tag'
47let getPreviewName: VideoMethods.GetPreviewName 78
48let getPreviewPath: VideoMethods.GetPreviewPath 79@Table({
49let getTorrentFileName: VideoMethods.GetTorrentFileName 80 tableName: 'video',
50let isOwned: VideoMethods.IsOwned 81 indexes: [
51let toFormattedJSON: VideoMethods.ToFormattedJSON
52let toFormattedDetailsJSON: VideoMethods.ToFormattedDetailsJSON
53let toActivityPubObject: VideoMethods.ToActivityPubObject
54let optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile
55let transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile
56let createPreview: VideoMethods.CreatePreview
57let createThumbnail: VideoMethods.CreateThumbnail
58let getVideoFilePath: VideoMethods.GetVideoFilePath
59let createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash
60let getOriginalFileHeight: VideoMethods.GetOriginalFileHeight
61let getEmbedPath: VideoMethods.GetEmbedPath
62let getDescriptionPath: VideoMethods.GetDescriptionPath
63let getTruncatedDescription: VideoMethods.GetTruncatedDescription
64let getCategoryLabel: VideoMethods.GetCategoryLabel
65let getLicenceLabel: VideoMethods.GetLicenceLabel
66let getLanguageLabel: VideoMethods.GetLanguageLabel
67
68let list: VideoMethods.List
69let listForApi: VideoMethods.ListForApi
70let listAllAndSharedByAccountForOutbox: VideoMethods.ListAllAndSharedByAccountForOutbox
71let listUserVideosForApi: VideoMethods.ListUserVideosForApi
72let load: VideoMethods.Load
73let loadByUrlAndPopulateAccount: VideoMethods.LoadByUrlAndPopulateAccount
74let loadByUUID: VideoMethods.LoadByUUID
75let loadByUUIDOrURL: VideoMethods.LoadByUUIDOrURL
76let loadAndPopulateAccountAndServerAndTags: VideoMethods.LoadAndPopulateAccountAndServerAndTags
77let loadByUUIDAndPopulateAccountAndServerAndTags: VideoMethods.LoadByUUIDAndPopulateAccountAndServerAndTags
78let searchAndPopulateAccountAndServerAndTags: VideoMethods.SearchAndPopulateAccountAndServerAndTags
79let removeThumbnail: VideoMethods.RemoveThumbnail
80let removePreview: VideoMethods.RemovePreview
81let removeFile: VideoMethods.RemoveFile
82let removeTorrent: VideoMethods.RemoveTorrent
83
84export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
85 Video = sequelize.define<VideoInstance, VideoAttributes>('Video',
86 { 82 {
87 uuid: { 83 fields: [ 'name' ]
88 type: DataTypes.UUID,
89 defaultValue: DataTypes.UUIDV4,
90 allowNull: false,
91 validate: {
92 isUUID: 4
93 }
94 },
95 name: {
96 type: DataTypes.STRING,
97 allowNull: false,
98 validate: {
99 nameValid: value => {
100 const res = isVideoNameValid(value)
101 if (res === false) throw new Error('Video name is not valid.')
102 }
103 }
104 },
105 category: {
106 type: DataTypes.INTEGER,
107 allowNull: true,
108 defaultValue: null,
109 validate: {
110 categoryValid: value => {
111 const res = isVideoCategoryValid(value)
112 if (res === false) throw new Error('Video category is not valid.')
113 }
114 }
115 },
116 licence: {
117 type: DataTypes.INTEGER,
118 allowNull: true,
119 defaultValue: null,
120 validate: {
121 licenceValid: value => {
122 const res = isVideoLicenceValid(value)
123 if (res === false) throw new Error('Video licence is not valid.')
124 }
125 }
126 },
127 language: {
128 type: DataTypes.INTEGER,
129 allowNull: true,
130 defaultValue: null,
131 validate: {
132 languageValid: value => {
133 const res = isVideoLanguageValid(value)
134 if (res === false) throw new Error('Video language is not valid.')
135 }
136 }
137 },
138 privacy: {
139 type: DataTypes.INTEGER,
140 allowNull: false,
141 validate: {
142 privacyValid: value => {
143 const res = isVideoPrivacyValid(value)
144 if (res === false) throw new Error('Video privacy is not valid.')
145 }
146 }
147 },
148 nsfw: {
149 type: DataTypes.BOOLEAN,
150 allowNull: false,
151 validate: {
152 nsfwValid: value => {
153 const res = isVideoNSFWValid(value)
154 if (res === false) throw new Error('Video nsfw attribute is not valid.')
155 }
156 }
157 },
158 description: {
159 type: DataTypes.STRING(CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max),
160 allowNull: true,
161 defaultValue: null,
162 validate: {
163 descriptionValid: value => {
164 const res = isVideoDescriptionValid(value)
165 if (res === false) throw new Error('Video description is not valid.')
166 }
167 }
168 },
169 duration: {
170 type: DataTypes.INTEGER,
171 allowNull: false,
172 validate: {
173 durationValid: value => {
174 const res = isVideoDurationValid(value)
175 if (res === false) throw new Error('Video duration is not valid.')
176 }
177 }
178 },
179 views: {
180 type: DataTypes.INTEGER,
181 allowNull: false,
182 defaultValue: 0,
183 validate: {
184 min: 0,
185 isInt: true
186 }
187 },
188 likes: {
189 type: DataTypes.INTEGER,
190 allowNull: false,
191 defaultValue: 0,
192 validate: {
193 min: 0,
194 isInt: true
195 }
196 },
197 dislikes: {
198 type: DataTypes.INTEGER,
199 allowNull: false,
200 defaultValue: 0,
201 validate: {
202 min: 0,
203 isInt: true
204 }
205 },
206 remote: {
207 type: DataTypes.BOOLEAN,
208 allowNull: false,
209 defaultValue: false
210 },
211 url: {
212 type: DataTypes.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max),
213 allowNull: false,
214 validate: {
215 urlValid: value => {
216 const res = isActivityPubUrlValid(value)
217 if (res === false) throw new Error('Video URL is not valid.')
218 }
219 }
220 }
221 }, 84 },
222 { 85 {
223 indexes: [ 86 fields: [ 'createdAt' ]
224 { 87 },
225 fields: [ 'name' ] 88 {
226 }, 89 fields: [ 'duration' ]
227 { 90 },
228 fields: [ 'createdAt' ] 91 {
229 }, 92 fields: [ 'views' ]
230 { 93 },
231 fields: [ 'duration' ] 94 {
232 }, 95 fields: [ 'likes' ]
233 { 96 },
234 fields: [ 'views' ] 97 {
235 }, 98 fields: [ 'uuid' ]
236 { 99 },
237 fields: [ 'likes' ] 100 {
238 }, 101 fields: [ 'channelId' ]
239 {
240 fields: [ 'uuid' ]
241 },
242 {
243 fields: [ 'channelId' ]
244 }
245 ],
246 hooks: {
247 afterDestroy
248 }
249 } 102 }
250 )
251
252 const classMethods = [
253 associate,
254
255 list,
256 listAllAndSharedByAccountForOutbox,
257 listForApi,
258 listUserVideosForApi,
259 load,
260 loadByUrlAndPopulateAccount,
261 loadAndPopulateAccountAndServerAndTags,
262 loadByUUIDOrURL,
263 loadByUUID,
264 loadByUUIDAndPopulateAccountAndServerAndTags,
265 searchAndPopulateAccountAndServerAndTags
266 ]
267 const instanceMethods = [
268 createPreview,
269 createThumbnail,
270 createTorrentAndSetInfoHash,
271 getPreviewName,
272 getPreviewPath,
273 getThumbnailName,
274 getThumbnailPath,
275 getTorrentFileName,
276 getVideoFilename,
277 getVideoFilePath,
278 getOriginalFile,
279 isOwned,
280 removeFile,
281 removePreview,
282 removeThumbnail,
283 removeTorrent,
284 toActivityPubObject,
285 toFormattedJSON,
286 toFormattedDetailsJSON,
287 optimizeOriginalVideofile,
288 transcodeOriginalVideofile,
289 getOriginalFileHeight,
290 getEmbedPath,
291 getTruncatedDescription,
292 getDescriptionPath,
293 getCategoryLabel,
294 getLicenceLabel,
295 getLanguageLabel
296 ] 103 ]
297 addMethodsToModel(Video, classMethods, instanceMethods) 104})
298 105export class VideoModel extends Model<VideoModel> {
299 return Video 106
300} 107 @AllowNull(false)
301 108 @Default(DataType.UUIDV4)
302// ------------------------------ METHODS ------------------------------ 109 @IsUUID(4)
303 110 @Column(DataType.UUID)
304function associate (models) { 111 uuid: string
305 Video.belongsTo(models.VideoChannel, { 112
113 @AllowNull(false)
114 @Is('VideoName', value => throwIfNotValid(value, isVideoNameValid, 'name'))
115 @Column
116 name: string
117
118 @AllowNull(true)
119 @Default(null)
120 @Is('VideoCategory', value => throwIfNotValid(value, isVideoCategoryValid, 'category'))
121 @Column
122 category: number
123
124 @AllowNull(true)
125 @Default(null)
126 @Is('VideoLicence', value => throwIfNotValid(value, isVideoLicenceValid, 'licence'))
127 @Column
128 licence: number
129
130 @AllowNull(true)
131 @Default(null)
132 @Is('VideoLanguage', value => throwIfNotValid(value, isVideoLanguageValid, 'language'))
133 @Column
134 language: number
135
136 @AllowNull(false)
137 @Is('VideoPrivacy', value => throwIfNotValid(value, isVideoPrivacyValid, 'privacy'))
138 @Column
139 privacy: number
140
141 @AllowNull(false)
142 @Is('VideoNSFW', value => throwIfNotValid(value, isVideoNSFWValid, 'NSFW boolean'))
143 @Column
144 nsfw: boolean
145
146 @AllowNull(true)
147 @Default(null)
148 @Is('VideoDescription', value => throwIfNotValid(value, isVideoDescriptionValid, 'description'))
149 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max))
150 description: string
151
152 @AllowNull(false)
153 @Is('VideoDuration', value => throwIfNotValid(value, isVideoDurationValid, 'duration'))
154 @Column
155 duration: number
156
157 @AllowNull(false)
158 @Default(0)
159 @IsInt
160 @Min(0)
161 @Column
162 views: number
163
164 @AllowNull(false)
165 @Default(0)
166 @IsInt
167 @Min(0)
168 @Column
169 likes: number
170
171 @AllowNull(false)
172 @Default(0)
173 @IsInt
174 @Min(0)
175 @Column
176 dislikes: number
177
178 @AllowNull(false)
179 @Column
180 remote: boolean
181
182 @AllowNull(false)
183 @Is('VideoUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
184 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max))
185 url: string
186
187 @CreatedAt
188 createdAt: Date
189
190 @UpdatedAt
191 updatedAt: Date
192
193 @ForeignKey(() => VideoChannelModel)
194 @Column
195 channelId: number
196
197 @BelongsTo(() => VideoChannelModel, {
306 foreignKey: { 198 foreignKey: {
307 name: 'channelId',
308 allowNull: false 199 allowNull: false
309 }, 200 },
310 onDelete: 'cascade' 201 onDelete: 'cascade'
311 }) 202 })
203 VideoChannel: VideoChannelModel
312 204
313 Video.belongsToMany(models.Tag, { 205 @BelongsToMany(() => TagModel, {
314 foreignKey: 'videoId', 206 foreignKey: 'videoId',
315 through: models.VideoTag, 207 through: () => VideoTagModel,
316 onDelete: 'cascade' 208 onDelete: 'CASCADE'
317 }) 209 })
210 Tags: TagModel[]
318 211
319 Video.hasMany(models.VideoAbuse, { 212 @HasMany(() => VideoAbuseModel, {
320 foreignKey: { 213 foreignKey: {
321 name: 'videoId', 214 name: 'videoId',
322 allowNull: false 215 allowNull: false
323 }, 216 },
324 onDelete: 'cascade' 217 onDelete: 'cascade'
325 }) 218 })
219 VideoAbuses: VideoAbuseModel[]
326 220
327 Video.hasMany(models.VideoFile, { 221 @HasMany(() => VideoFileModel, {
328 foreignKey: { 222 foreignKey: {
329 name: 'videoId', 223 name: 'videoId',
330 allowNull: false 224 allowNull: false
331 }, 225 },
332 onDelete: 'cascade' 226 onDelete: 'cascade'
333 }) 227 })
228 VideoFiles: VideoFileModel[]
334 229
335 Video.hasMany(models.VideoShare, { 230 @HasMany(() => VideoShareModel, {
336 foreignKey: { 231 foreignKey: {
337 name: 'videoId', 232 name: 'videoId',
338 allowNull: false 233 allowNull: false
339 }, 234 },
340 onDelete: 'cascade' 235 onDelete: 'cascade'
341 }) 236 })
237 VideoShares: VideoShareModel[]
342 238
343 Video.hasMany(models.AccountVideoRate, { 239 @HasMany(() => AccountVideoRateModel, {
344 foreignKey: { 240 foreignKey: {
345 name: 'videoId', 241 name: 'videoId',
346 allowNull: false 242 allowNull: false
347 }, 243 },
348 onDelete: 'cascade' 244 onDelete: 'cascade'
349 }) 245 })
350} 246 AccountVideoRates: AccountVideoRateModel[]
351
352function afterDestroy (video: VideoInstance) {
353 const tasks = []
354 247
355 tasks.push( 248 @AfterDestroy
356 video.removeThumbnail() 249 static removeFilesAndSendDelete (instance: VideoModel) {
357 ) 250 const tasks = []
358 251
359 if (video.isOwned()) {
360 tasks.push( 252 tasks.push(
361 video.removePreview(), 253 instance.removeThumbnail()
362 sendDeleteVideo(video, undefined)
363 ) 254 )
364 255
365 // Remove physical files and torrents 256 if (instance.isOwned()) {
366 video.VideoFiles.forEach(file => { 257 tasks.push(
367 tasks.push(video.removeFile(file)) 258 instance.removePreview(),
368 tasks.push(video.removeTorrent(file)) 259 sendDeleteVideo(instance, undefined)
369 }) 260 )
370 }
371
372 return Promise.all(tasks)
373 .catch(err => {
374 logger.error('Some errors when removing files of video %s in after destroy hook.', video.uuid, err)
375 })
376}
377
378getOriginalFile = function (this: VideoInstance) {
379 if (Array.isArray(this.VideoFiles) === false) return undefined
380 261
381 // The original file is the file that have the higher resolution 262 // Remove physical files and torrents
382 return maxBy(this.VideoFiles, file => file.resolution) 263 instance.VideoFiles.forEach(file => {
383} 264 tasks.push(instance.removeFile(file))
265 tasks.push(instance.removeTorrent(file))
266 })
267 }
384 268
385getVideoFilename = function (this: VideoInstance, videoFile: VideoFileInstance) { 269 return Promise.all(tasks)
386 return this.uuid + '-' + videoFile.resolution + videoFile.extname 270 .catch(err => {
387} 271 logger.error('Some errors when removing files of video %s in after destroy hook.', instance.uuid, err)
272 })
273 }
388 274
389getThumbnailName = function (this: VideoInstance) { 275 static list () {
390 // We always have a copy of the thumbnail 276 const query = {
391 const extension = '.jpg' 277 include: [ VideoFileModel ]
392 return this.uuid + extension 278 }
393}
394 279
395getPreviewName = function (this: VideoInstance) { 280 return VideoModel.findAll(query)
396 const extension = '.jpg' 281 }
397 return this.uuid + extension
398}
399 282
400getTorrentFileName = function (this: VideoInstance, videoFile: VideoFileInstance) { 283 static listAllAndSharedByAccountForOutbox (accountId: number, start: number, count: number) {
401 const extension = '.torrent' 284 function getRawQuery (select: string) {
402 return this.uuid + '-' + videoFile.resolution + extension 285 const queryVideo = 'SELECT ' + select + ' FROM "video" AS "Video" ' +
403} 286 'INNER JOIN "videoChannel" AS "VideoChannel" ON "VideoChannel"."id" = "Video"."channelId" ' +
287 'WHERE "VideoChannel"."accountId" = ' + accountId
288 const queryVideoShare = 'SELECT ' + select + ' FROM "videoShare" AS "VideoShare" ' +
289 'INNER JOIN "video" AS "Video" ON "Video"."id" = "VideoShare"."videoId" ' +
290 'WHERE "VideoShare"."accountId" = ' + accountId
404 291
405isOwned = function (this: VideoInstance) { 292 return `(${queryVideo}) UNION (${queryVideoShare})`
406 return this.remote === false 293 }
407}
408 294
409createPreview = function (this: VideoInstance, videoFile: VideoFileInstance) { 295 const rawQuery = getRawQuery('"Video"."id"')
410 const imageSize = PREVIEWS_SIZE.width + 'x' + PREVIEWS_SIZE.height 296 const rawCountQuery = getRawQuery('COUNT("Video"."id") as "total"')
297
298 const query = {
299 distinct: true,
300 offset: start,
301 limit: count,
302 order: [ getSort('createdAt'), [ 'Tags', 'name', 'ASC' ] ],
303 where: {
304 id: {
305 [Sequelize.Op.in]: Sequelize.literal('(' + rawQuery + ')')
306 }
307 },
308 include: [
309 {
310 model: VideoShareModel,
311 required: false,
312 where: {
313 [Sequelize.Op.and]: [
314 {
315 id: {
316 [Sequelize.Op.not]: null
317 }
318 },
319 {
320 accountId
321 }
322 ]
323 },
324 include: [ AccountModel ]
325 },
326 {
327 model: VideoChannelModel,
328 required: true,
329 include: [
330 {
331 model: AccountModel,
332 required: true
333 }
334 ]
335 },
336 {
337 model: AccountVideoRateModel,
338 include: [ AccountModel ]
339 },
340 VideoFileModel,
341 TagModel
342 ]
343 }
411 344
412 return generateImageFromVideoFile( 345 return Bluebird.all([
413 this.getVideoFilePath(videoFile), 346 // FIXME: typing issue
414 CONFIG.STORAGE.PREVIEWS_DIR, 347 VideoModel.findAll(query as any),
415 this.getPreviewName(), 348 VideoModel.sequelize.query(rawCountQuery, { type: Sequelize.QueryTypes.SELECT })
416 imageSize 349 ]).then(([ rows, totals ]) => {
417 ) 350 // totals: totalVideos + totalVideoShares
418} 351 let totalVideos = 0
352 let totalVideoShares = 0
353 if (totals[0]) totalVideos = parseInt(totals[0].total, 10)
354 if (totals[1]) totalVideoShares = parseInt(totals[1].total, 10)
355
356 const total = totalVideos + totalVideoShares
357 return {
358 data: rows,
359 total: total
360 }
361 })
362 }
419 363
420createThumbnail = function (this: VideoInstance, videoFile: VideoFileInstance) { 364 static listUserVideosForApi (userId: number, start: number, count: number, sort: string) {
421 const imageSize = THUMBNAILS_SIZE.width + 'x' + THUMBNAILS_SIZE.height 365 const query = {
366 distinct: true,
367 offset: start,
368 limit: count,
369 order: [ getSort(sort), [ 'Tags', 'name', 'ASC' ] ],
370 include: [
371 {
372 model: VideoChannelModel,
373 required: true,
374 include: [
375 {
376 model: AccountModel,
377 where: {
378 userId
379 },
380 required: true
381 }
382 ]
383 },
384 TagModel
385 ]
386 }
422 387
423 return generateImageFromVideoFile( 388 return VideoModel.findAndCountAll(query).then(({ rows, count }) => {
424 this.getVideoFilePath(videoFile), 389 return {
425 CONFIG.STORAGE.THUMBNAILS_DIR, 390 data: rows,
426 this.getThumbnailName(), 391 total: count
427 imageSize 392 }
428 ) 393 })
429} 394 }
430 395
431getVideoFilePath = function (this: VideoInstance, videoFile: VideoFileInstance) { 396 static listForApi (start: number, count: number, sort: string) {
432 return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile)) 397 const query = {
433} 398 distinct: true,
399 offset: start,
400 limit: count,
401 order: [ getSort(sort), [ 'Tags', 'name', 'ASC' ] ],
402 include: [
403 {
404 model: VideoChannelModel,
405 required: true,
406 include: [
407 {
408 model: AccountModel,
409 required: true,
410 include: [
411 {
412 model: ServerModel,
413 required: false
414 }
415 ]
416 }
417 ]
418 },
419 TagModel
420 ],
421 where: this.createBaseVideosWhere()
422 }
434 423
435createTorrentAndSetInfoHash = async function (this: VideoInstance, videoFile: VideoFileInstance) { 424 return VideoModel.findAndCountAll(query).then(({ rows, count }) => {
436 const options = { 425 return {
437 announceList: [ 426 data: rows,
438 [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ] 427 total: count
439 ], 428 }
440 urlList: [ 429 })
441 CONFIG.WEBSERVER.URL + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile)
442 ]
443 } 430 }
444 431
445 const torrent = await createTorrentPromise(this.getVideoFilePath(videoFile), options) 432 static load (id: number) {
433 return VideoModel.findById(id)
434 }
446 435
447 const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile)) 436 static loadByUUID (uuid: string, t?: Sequelize.Transaction) {
448 logger.info('Creating torrent %s.', filePath) 437 const query: IFindOptions<VideoModel> = {
438 where: {
439 uuid
440 },
441 include: [ VideoFileModel ]
442 }
449 443
450 await writeFilePromise(filePath, torrent) 444 if (t !== undefined) query.transaction = t
451 445
452 const parsedTorrent = parseTorrent(torrent) 446 return VideoModel.findOne(query)
453 videoFile.infoHash = parsedTorrent.infoHash 447 }
454}
455 448
456getEmbedPath = function (this: VideoInstance) { 449 static loadByUrlAndPopulateAccount (url: string, t?: Sequelize.Transaction) {
457 return '/videos/embed/' + this.uuid 450 const query: IFindOptions<VideoModel> = {
458} 451 where: {
452 url
453 },
454 include: [
455 VideoFileModel,
456 {
457 model: VideoChannelModel,
458 include: [ AccountModel ]
459 }
460 ]
461 }
459 462
460getThumbnailPath = function (this: VideoInstance) { 463 if (t !== undefined) query.transaction = t
461 return join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName())
462}
463 464
464getPreviewPath = function (this: VideoInstance) { 465 return VideoModel.findOne(query)
465 return join(STATIC_PATHS.PREVIEWS, this.getPreviewName()) 466 }
466}
467 467
468toFormattedJSON = function (this: VideoInstance) { 468 static loadByUUIDOrURL (uuid: string, url: string, t?: Sequelize.Transaction) {
469 let serverHost 469 const query: IFindOptions<VideoModel> = {
470 where: {
471 [Sequelize.Op.or]: [
472 { uuid },
473 { url }
474 ]
475 },
476 include: [ VideoFileModel ]
477 }
470 478
471 if (this.VideoChannel.Account.Server) { 479 if (t !== undefined) query.transaction = t
472 serverHost = this.VideoChannel.Account.Server.host
473 } else {
474 // It means it's our video
475 serverHost = CONFIG.WEBSERVER.HOST
476 }
477 480
478 const json = { 481 return VideoModel.findOne(query)
479 id: this.id,
480 uuid: this.uuid,
481 name: this.name,
482 category: this.category,
483 categoryLabel: this.getCategoryLabel(),
484 licence: this.licence,
485 licenceLabel: this.getLicenceLabel(),
486 language: this.language,
487 languageLabel: this.getLanguageLabel(),
488 nsfw: this.nsfw,
489 description: this.getTruncatedDescription(),
490 serverHost,
491 isLocal: this.isOwned(),
492 accountName: this.VideoChannel.Account.name,
493 duration: this.duration,
494 views: this.views,
495 likes: this.likes,
496 dislikes: this.dislikes,
497 tags: map<TagInstance, string>(this.Tags, 'name'),
498 thumbnailPath: this.getThumbnailPath(),
499 previewPath: this.getPreviewPath(),
500 embedPath: this.getEmbedPath(),
501 createdAt: this.createdAt,
502 updatedAt: this.updatedAt
503 } 482 }
504 483
505 return json 484 static loadAndPopulateAccountAndServerAndTags (id: number) {
506} 485 const options = {
486 order: [ [ 'Tags', 'name', 'ASC' ] ],
487 include: [
488 {
489 model: VideoChannelModel,
490 include: [
491 {
492 model: AccountModel,
493 include: [ { model: ServerModel, required: false } ]
494 }
495 ]
496 },
497 {
498 model: AccountVideoRateModel,
499 include: [ AccountModel ]
500 },
501 {
502 model: VideoShareModel,
503 include: [ AccountModel ]
504 },
505 TagModel,
506 VideoFileModel
507 ]
508 }
507 509
508toFormattedDetailsJSON = function (this: VideoInstance) { 510 return VideoModel.findById(id, options)
509 const formattedJson = this.toFormattedJSON() 511 }
510 512
511 // Maybe our server is not up to date and there are new privacy settings since our version 513 static loadByUUIDAndPopulateAccountAndServerAndTags (uuid: string) {
512 let privacyLabel = VIDEO_PRIVACIES[this.privacy] 514 const options = {
513 if (!privacyLabel) privacyLabel = 'Unknown' 515 order: [ [ 'Tags', 'name', 'ASC' ] ],
516 where: {
517 uuid
518 },
519 include: [
520 {
521 model: VideoChannelModel,
522 include: [
523 {
524 model: AccountModel,
525 include: [ { model: ServerModel, required: false } ]
526 }
527 ]
528 },
529 {
530 model: AccountVideoRateModel,
531 include: [ AccountModel ]
532 },
533 {
534 model: VideoShareModel,
535 include: [ AccountModel ]
536 },
537 TagModel,
538 VideoFileModel
539 ]
540 }
514 541
515 const detailsJson = { 542 return VideoModel.findOne(options)
516 privacyLabel,
517 privacy: this.privacy,
518 descriptionPath: this.getDescriptionPath(),
519 channel: this.VideoChannel.toFormattedJSON(),
520 account: this.VideoChannel.Account.toFormattedJSON(),
521 files: []
522 } 543 }
523 544
524 // Format and sort video files 545 static searchAndPopulateAccountAndServerAndTags (value: string, start: number, count: number, sort: string) {
525 const { baseUrlHttp, baseUrlWs } = getBaseUrls(this) 546 const serverInclude: IIncludeOptions = {
526 detailsJson.files = this.VideoFiles 547 model: ServerModel,
527 .map(videoFile => { 548 required: false
528 let resolutionLabel = videoFile.resolution + 'p' 549 }
529
530 const videoFileJson = {
531 resolution: videoFile.resolution,
532 resolutionLabel,
533 magnetUri: generateMagnetUri(this, videoFile, baseUrlHttp, baseUrlWs),
534 size: videoFile.size,
535 torrentUrl: getTorrentUrl(this, videoFile, baseUrlHttp),
536 fileUrl: getVideoFileUrl(this, videoFile, baseUrlHttp)
537 }
538
539 return videoFileJson
540 })
541 .sort((a, b) => {
542 if (a.resolution < b.resolution) return 1
543 if (a.resolution === b.resolution) return 0
544 return -1
545 })
546
547 return Object.assign(formattedJson, detailsJson)
548}
549
550toActivityPubObject = function (this: VideoInstance) {
551 const { baseUrlHttp, baseUrlWs } = getBaseUrls(this)
552 if (!this.Tags) this.Tags = []
553 550
554 const tag = this.Tags.map(t => ({ 551 const accountInclude: IIncludeOptions = {
555 type: 'Hashtag' as 'Hashtag', 552 model: AccountModel,
556 name: t.name 553 include: [ serverInclude ]
557 })) 554 }
558 555
559 let language 556 const videoChannelInclude: IIncludeOptions = {
560 if (this.language) { 557 model: VideoChannelModel,
561 language = { 558 include: [ accountInclude ],
562 identifier: this.language + '', 559 required: true
563 name: this.getLanguageLabel()
564 } 560 }
565 }
566 561
567 let category 562 const tagInclude: IIncludeOptions = {
568 if (this.category) { 563 model: TagModel
569 category = {
570 identifier: this.category + '',
571 name: this.getCategoryLabel()
572 } 564 }
573 }
574 565
575 let licence 566 const query: IFindOptions<VideoModel> = {
576 if (this.licence) { 567 distinct: true,
577 licence = { 568 where: this.createBaseVideosWhere(),
578 identifier: this.licence + '', 569 offset: start,
579 name: this.getLicenceLabel() 570 limit: count,
571 order: [ getSort(sort), [ 'Tags', 'name', 'ASC' ] ]
580 } 572 }
581 }
582 573
583 let likesObject 574 // TODO: search on tags too
584 let dislikesObject 575 // const escapedValue = Video['sequelize'].escape('%' + value + '%')
576 // query.where['id'][Sequelize.Op.in] = Video['sequelize'].literal(
577 // `(SELECT "VideoTags"."videoId"
578 // FROM "Tags"
579 // INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId"
580 // WHERE name ILIKE ${escapedValue}
581 // )`
582 // )
583
584 // TODO: search on account too
585 // accountInclude.where = {
586 // name: {
587 // [Sequelize.Op.iLike]: '%' + value + '%'
588 // }
589 // }
590 query.where['name'] = {
591 [Sequelize.Op.iLike]: '%' + value + '%'
592 }
585 593
586 if (Array.isArray(this.AccountVideoRates)) { 594 query.include = [
587 const likes: string[] = [] 595 videoChannelInclude, tagInclude
588 const dislikes: string[] = [] 596 ]
589 597
590 for (const rate of this.AccountVideoRates) { 598 return VideoModel.findAndCountAll(query).then(({ rows, count }) => {
591 if (rate.type === 'like') { 599 return {
592 likes.push(rate.Account.url) 600 data: rows,
593 } else if (rate.type === 'dislike') { 601 total: count
594 dislikes.push(rate.Account.url)
595 } 602 }
596 } 603 })
597
598 likesObject = activityPubCollection(likes)
599 dislikesObject = activityPubCollection(dislikes)
600 } 604 }
601 605
602 let sharesObject 606 private static createBaseVideosWhere () {
603 if (Array.isArray(this.VideoShares)) { 607 return {
604 const shares: string[] = [] 608 id: {
605 609 [Sequelize.Op.notIn]: VideoModel.sequelize.literal(
606 for (const videoShare of this.VideoShares) { 610 '(SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")'
607 const shareUrl = getAnnounceActivityPubUrl(this.url, videoShare.Account) 611 )
608 shares.push(shareUrl) 612 },
613 privacy: VideoPrivacy.PUBLIC
609 } 614 }
610
611 sharesObject = activityPubCollection(shares)
612 } 615 }
613 616
614 const url = [] 617 getOriginalFile () {
615 for (const file of this.VideoFiles) { 618 if (Array.isArray(this.VideoFiles) === false) return undefined
616 url.push({
617 type: 'Link',
618 mimeType: 'video/' + file.extname.replace('.', ''),
619 url: getVideoFileUrl(this, file, baseUrlHttp),
620 width: file.resolution,
621 size: file.size
622 })
623 619
624 url.push({ 620 // The original file is the file that have the higher resolution
625 type: 'Link', 621 return maxBy(this.VideoFiles, file => file.resolution)
626 mimeType: 'application/x-bittorrent',
627 url: getTorrentUrl(this, file, baseUrlHttp),
628 width: file.resolution
629 })
630
631 url.push({
632 type: 'Link',
633 mimeType: 'application/x-bittorrent;x-scheme-handler/magnet',
634 url: generateMagnetUri(this, file, baseUrlHttp, baseUrlWs),
635 width: file.resolution
636 })
637 } 622 }
638 623
639 // Add video url too 624 getVideoFilename (videoFile: VideoFileModel) {
640 url.push({ 625 return this.uuid + '-' + videoFile.resolution + videoFile.extname
641 type: 'Link', 626 }
642 mimeType: 'text/html',
643 url: CONFIG.WEBSERVER.URL + '/videos/watch/' + this.uuid
644 })
645 627
646 const videoObject: VideoTorrentObject = { 628 getThumbnailName () {
647 type: 'Video' as 'Video', 629 // We always have a copy of the thumbnail
648 id: this.url, 630 const extension = '.jpg'
649 name: this.name, 631 return this.uuid + extension
650 // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration
651 duration: 'PT' + this.duration + 'S',
652 uuid: this.uuid,
653 tag,
654 category,
655 licence,
656 language,
657 views: this.views,
658 nsfw: this.nsfw,
659 published: this.createdAt.toISOString(),
660 updated: this.updatedAt.toISOString(),
661 mediaType: 'text/markdown',
662 content: this.getTruncatedDescription(),
663 icon: {
664 type: 'Image',
665 url: getThumbnailUrl(this, baseUrlHttp),
666 mediaType: 'image/jpeg',
667 width: THUMBNAILS_SIZE.width,
668 height: THUMBNAILS_SIZE.height
669 },
670 url,
671 likes: likesObject,
672 dislikes: dislikesObject,
673 shares: sharesObject
674 } 632 }
675 633
676 return videoObject 634 getPreviewName () {
677} 635 const extension = '.jpg'
636 return this.uuid + extension
637 }
678 638
679getTruncatedDescription = function (this: VideoInstance) { 639 getTorrentFileName (videoFile: VideoFileModel) {
680 if (!this.description) return null 640 const extension = '.torrent'
641 return this.uuid + '-' + videoFile.resolution + extension
642 }
681 643
682 const options = { 644 isOwned () {
683 length: CONSTRAINTS_FIELDS.VIDEOS.TRUNCATED_DESCRIPTION.max 645 return this.remote === false
684 } 646 }
685 647
686 return truncate(this.description, options) 648 createPreview (videoFile: VideoFileModel) {
687} 649 const imageSize = PREVIEWS_SIZE.width + 'x' + PREVIEWS_SIZE.height
650
651 return generateImageFromVideoFile(
652 this.getVideoFilePath(videoFile),
653 CONFIG.STORAGE.PREVIEWS_DIR,
654 this.getPreviewName(),
655 imageSize
656 )
657 }
688 658
689optimizeOriginalVideofile = async function (this: VideoInstance) { 659 createThumbnail (videoFile: VideoFileModel) {
690 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR 660 const imageSize = THUMBNAILS_SIZE.width + 'x' + THUMBNAILS_SIZE.height
691 const newExtname = '.mp4'
692 const inputVideoFile = this.getOriginalFile()
693 const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile))
694 const videoOutputPath = join(videosDirectory, this.id + '-transcoded' + newExtname)
695 661
696 const transcodeOptions = { 662 return generateImageFromVideoFile(
697 inputPath: videoInputPath, 663 this.getVideoFilePath(videoFile),
698 outputPath: videoOutputPath 664 CONFIG.STORAGE.THUMBNAILS_DIR,
665 this.getThumbnailName(),
666 imageSize
667 )
699 } 668 }
700 669
701 try { 670 getVideoFilePath (videoFile: VideoFileModel) {
702 // Could be very long! 671 return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
703 await transcode(transcodeOptions) 672 }
704 673
705 await unlinkPromise(videoInputPath) 674 createTorrentAndSetInfoHash = async function (videoFile: VideoFileModel) {
675 const options = {
676 announceList: [
677 [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ]
678 ],
679 urlList: [
680 CONFIG.WEBSERVER.URL + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile)
681 ]
682 }
706 683
707 // Important to do this before getVideoFilename() to take in account the new file extension 684 const torrent = await createTorrentPromise(this.getVideoFilePath(videoFile), options)
708 inputVideoFile.set('extname', newExtname)
709 685
710 await renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile)) 686 const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
711 const stats = await statPromise(this.getVideoFilePath(inputVideoFile)) 687 logger.info('Creating torrent %s.', filePath)
712 688
713 inputVideoFile.set('size', stats.size) 689 await writeFilePromise(filePath, torrent)
714 690
715 await this.createTorrentAndSetInfoHash(inputVideoFile) 691 const parsedTorrent = parseTorrent(torrent)
716 await inputVideoFile.save() 692 videoFile.infoHash = parsedTorrent.infoHash
693 }
717 694
718 } catch (err) { 695 getEmbedPath () {
719 // Auto destruction... 696 return '/videos/embed/' + this.uuid
720 this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err)) 697 }
721 698
722 throw err 699 getThumbnailPath () {
700 return join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName())
723 } 701 }
724}
725 702
726transcodeOriginalVideofile = async function (this: VideoInstance, resolution: VideoResolution) { 703 getPreviewPath () {
727 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR 704 return join(STATIC_PATHS.PREVIEWS, this.getPreviewName())
728 const extname = '.mp4' 705 }
729 706
730 // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed 707 toFormattedJSON () {
731 const videoInputPath = join(videosDirectory, this.getVideoFilename(this.getOriginalFile())) 708 let serverHost
732 709
733 const newVideoFile = (Video['sequelize'].models.VideoFile as VideoFileModel).build({ 710 if (this.VideoChannel.Account.Server) {
734 resolution, 711 serverHost = this.VideoChannel.Account.Server.host
735 extname, 712 } else {
736 size: 0, 713 // It means it's our video
737 videoId: this.id 714 serverHost = CONFIG.WEBSERVER.HOST
738 }) 715 }
739 const videoOutputPath = join(videosDirectory, this.getVideoFilename(newVideoFile))
740 716
741 const transcodeOptions = { 717 return {
742 inputPath: videoInputPath, 718 id: this.id,
743 outputPath: videoOutputPath, 719 uuid: this.uuid,
744 resolution 720 name: this.name,
721 category: this.category,
722 categoryLabel: this.getCategoryLabel(),
723 licence: this.licence,
724 licenceLabel: this.getLicenceLabel(),
725 language: this.language,
726 languageLabel: this.getLanguageLabel(),
727 nsfw: this.nsfw,
728 description: this.getTruncatedDescription(),
729 serverHost,
730 isLocal: this.isOwned(),
731 accountName: this.VideoChannel.Account.name,
732 duration: this.duration,
733 views: this.views,
734 likes: this.likes,
735 dislikes: this.dislikes,
736 tags: map<TagModel, string>(this.Tags, 'name'),
737 thumbnailPath: this.getThumbnailPath(),
738 previewPath: this.getPreviewPath(),
739 embedPath: this.getEmbedPath(),
740 createdAt: this.createdAt,
741 updatedAt: this.updatedAt
742 }
745 } 743 }
746 744
747 await transcode(transcodeOptions) 745 toFormattedDetailsJSON () {
746 const formattedJson = this.toFormattedJSON()
748 747
749 const stats = await statPromise(videoOutputPath) 748 // Maybe our server is not up to date and there are new privacy settings since our version
749 let privacyLabel = VIDEO_PRIVACIES[this.privacy]
750 if (!privacyLabel) privacyLabel = 'Unknown'
750 751
751 newVideoFile.set('size', stats.size) 752 const detailsJson = {
753 privacyLabel,
754 privacy: this.privacy,
755 descriptionPath: this.getDescriptionPath(),
756 channel: this.VideoChannel.toFormattedJSON(),
757 account: this.VideoChannel.Account.toFormattedJSON(),
758 files: []
759 }
752 760
753 await this.createTorrentAndSetInfoHash(newVideoFile) 761 // Format and sort video files
762 const { baseUrlHttp, baseUrlWs } = this.getBaseUrls()
763 detailsJson.files = this.VideoFiles
764 .map(videoFile => {
765 let resolutionLabel = videoFile.resolution + 'p'
766
767 return {
768 resolution: videoFile.resolution,
769 resolutionLabel,
770 magnetUri: this.generateMagnetUri(videoFile, baseUrlHttp, baseUrlWs),
771 size: videoFile.size,
772 torrentUrl: this.getTorrentUrl(videoFile, baseUrlHttp),
773 fileUrl: this.getVideoFileUrl(videoFile, baseUrlHttp)
774 }
775 })
776 .sort((a, b) => {
777 if (a.resolution < b.resolution) return 1
778 if (a.resolution === b.resolution) return 0
779 return -1
780 })
781
782 return Object.assign(formattedJson, detailsJson)
783 }
754 784
755 await newVideoFile.save() 785 toActivityPubObject (): VideoTorrentObject {
786 const { baseUrlHttp, baseUrlWs } = this.getBaseUrls()
787 if (!this.Tags) this.Tags = []
756 788
757 this.VideoFiles.push(newVideoFile) 789 const tag = this.Tags.map(t => ({
758} 790 type: 'Hashtag' as 'Hashtag',
791 name: t.name
792 }))
759 793
760getOriginalFileHeight = function (this: VideoInstance) { 794 let language
761 const originalFilePath = this.getVideoFilePath(this.getOriginalFile()) 795 if (this.language) {
796 language = {
797 identifier: this.language + '',
798 name: this.getLanguageLabel()
799 }
800 }
762 801
763 return getVideoFileHeight(originalFilePath) 802 let category
764} 803 if (this.category) {
804 category = {
805 identifier: this.category + '',
806 name: this.getCategoryLabel()
807 }
808 }
765 809
766getDescriptionPath = function (this: VideoInstance) { 810 let licence
767 return `/api/${API_VERSION}/videos/${this.uuid}/description` 811 if (this.licence) {
768} 812 licence = {
813 identifier: this.licence + '',
814 name: this.getLicenceLabel()
815 }
816 }
769 817
770getCategoryLabel = function (this: VideoInstance) { 818 let likesObject
771 let categoryLabel = VIDEO_CATEGORIES[this.category] 819 let dislikesObject
772 if (!categoryLabel) categoryLabel = 'Misc'
773 820
774 return categoryLabel 821 if (Array.isArray(this.AccountVideoRates)) {
775} 822 const likes: string[] = []
823 const dislikes: string[] = []
776 824
777getLicenceLabel = function (this: VideoInstance) { 825 for (const rate of this.AccountVideoRates) {
778 let licenceLabel = VIDEO_LICENCES[this.licence] 826 if (rate.type === 'like') {
779 if (!licenceLabel) licenceLabel = 'Unknown' 827 likes.push(rate.Account.url)
828 } else if (rate.type === 'dislike') {
829 dislikes.push(rate.Account.url)
830 }
831 }
780 832
781 return licenceLabel 833 likesObject = activityPubCollection(likes)
782} 834 dislikesObject = activityPubCollection(dislikes)
835 }
783 836
784getLanguageLabel = function (this: VideoInstance) { 837 let sharesObject
785 let languageLabel = VIDEO_LANGUAGES[this.language] 838 if (Array.isArray(this.VideoShares)) {
786 if (!languageLabel) languageLabel = 'Unknown' 839 const shares: string[] = []
787 840
788 return languageLabel 841 for (const videoShare of this.VideoShares) {
789} 842 const shareUrl = getAnnounceActivityPubUrl(this.url, videoShare.Account)
843 shares.push(shareUrl)
844 }
790 845
791removeThumbnail = function (this: VideoInstance) { 846 sharesObject = activityPubCollection(shares)
792 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName()) 847 }
793 return unlinkPromise(thumbnailPath)
794}
795 848
796removePreview = function (this: VideoInstance) { 849 const url = []
797 // Same name than video thumbnail 850 for (const file of this.VideoFiles) {
798 return unlinkPromise(CONFIG.STORAGE.PREVIEWS_DIR + this.getPreviewName()) 851 url.push({
799} 852 type: 'Link',
853 mimeType: 'video/' + file.extname.replace('.', ''),
854 url: this.getVideoFileUrl(file, baseUrlHttp),
855 width: file.resolution,
856 size: file.size
857 })
858
859 url.push({
860 type: 'Link',
861 mimeType: 'application/x-bittorrent',
862 url: this.getTorrentUrl(file, baseUrlHttp),
863 width: file.resolution
864 })
865
866 url.push({
867 type: 'Link',
868 mimeType: 'application/x-bittorrent;x-scheme-handler/magnet',
869 url: this.generateMagnetUri(file, baseUrlHttp, baseUrlWs),
870 width: file.resolution
871 })
872 }
800 873
801removeFile = function (this: VideoInstance, videoFile: VideoFileInstance) { 874 // Add video url too
802 const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile)) 875 url.push({
803 return unlinkPromise(filePath) 876 type: 'Link',
804} 877 mimeType: 'text/html',
878 url: CONFIG.WEBSERVER.URL + '/videos/watch/' + this.uuid
879 })
805 880
806removeTorrent = function (this: VideoInstance, videoFile: VideoFileInstance) { 881 return {
807 const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile)) 882 type: 'Video' as 'Video',
808 return unlinkPromise(torrentPath) 883 id: this.url,
809} 884 name: this.name,
885 // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration
886 duration: 'PT' + this.duration + 'S',
887 uuid: this.uuid,
888 tag,
889 category,
890 licence,
891 language,
892 views: this.views,
893 nsfw: this.nsfw,
894 published: this.createdAt.toISOString(),
895 updated: this.updatedAt.toISOString(),
896 mediaType: 'text/markdown',
897 content: this.getTruncatedDescription(),
898 icon: {
899 type: 'Image',
900 url: this.getThumbnailUrl(baseUrlHttp),
901 mediaType: 'image/jpeg',
902 width: THUMBNAILS_SIZE.width,
903 height: THUMBNAILS_SIZE.height
904 },
905 url,
906 likes: likesObject,
907 dislikes: dislikesObject,
908 shares: sharesObject
909 }
910 }
911
912 getTruncatedDescription () {
913 if (!this.description) return null
810 914
811// ------------------------------ STATICS ------------------------------ 915 const options = {
916 length: CONSTRAINTS_FIELDS.VIDEOS.TRUNCATED_DESCRIPTION.max
917 }
812 918
813list = function () { 919 return truncate(this.description, options)
814 const query = {
815 include: [ Video['sequelize'].models.VideoFile ]
816 } 920 }
817 921
818 return Video.findAll(query) 922 optimizeOriginalVideofile = async function () {
819} 923 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
924 const newExtname = '.mp4'
925 const inputVideoFile = this.getOriginalFile()
926 const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile))
927 const videoOutputPath = join(videosDirectory, this.id + '-transcoded' + newExtname)
820 928
821listAllAndSharedByAccountForOutbox = function (accountId: number, start: number, count: number) { 929 const transcodeOptions = {
822 function getRawQuery (select: string) { 930 inputPath: videoInputPath,
823 const queryVideo = 'SELECT ' + select + ' FROM "Videos" AS "Video" ' + 931 outputPath: videoOutputPath
824 'INNER JOIN "VideoChannels" AS "VideoChannel" ON "VideoChannel"."id" = "Video"."channelId" ' + 932 }
825 'WHERE "VideoChannel"."accountId" = ' + accountId
826 const queryVideoShare = 'SELECT ' + select + ' FROM "VideoShares" AS "VideoShare" ' +
827 'INNER JOIN "Videos" AS "Video" ON "Video"."id" = "VideoShare"."videoId" ' +
828 'WHERE "VideoShare"."accountId" = ' + accountId
829 933
830 let rawQuery = `(${queryVideo}) UNION (${queryVideoShare})` 934 try {
935 // Could be very long!
936 await transcode(transcodeOptions)
831 937
832 return rawQuery 938 await unlinkPromise(videoInputPath)
833 }
834 939
835 const rawQuery = getRawQuery('"Video"."id"') 940 // Important to do this before getVideoFilename() to take in account the new file extension
836 const rawCountQuery = getRawQuery('COUNT("Video"."id") as "total"') 941 inputVideoFile.set('extname', newExtname)
837 942
838 const query = { 943 await renamePromise(videoOutputPath, this.getVideoFilePath(inputVideoFile))
839 distinct: true, 944 const stats = await statPromise(this.getVideoFilePath(inputVideoFile))
840 offset: start,
841 limit: count,
842 order: [ getSort('createdAt'), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ],
843 where: {
844 id: {
845 [Sequelize.Op.in]: Sequelize.literal('(' + rawQuery + ')')
846 }
847 },
848 include: [
849 {
850 model: Video['sequelize'].models.VideoShare,
851 required: false,
852 where: {
853 [Sequelize.Op.and]: [
854 {
855 id: {
856 [Sequelize.Op.not]: null
857 }
858 },
859 {
860 accountId
861 }
862 ]
863 },
864 include: [ Video['sequelize'].models.Account ]
865 },
866 {
867 model: Video['sequelize'].models.VideoChannel,
868 required: true,
869 include: [
870 {
871 model: Video['sequelize'].models.Account,
872 required: true
873 }
874 ]
875 },
876 {
877 model: Video['sequelize'].models.AccountVideoRate,
878 include: [ Video['sequelize'].models.Account ]
879 },
880 Video['sequelize'].models.VideoFile,
881 Video['sequelize'].models.Tag
882 ]
883 }
884 945
885 return Bluebird.all([ 946 inputVideoFile.set('size', stats.size)
886 Video.findAll(query),
887 Video['sequelize'].query(rawCountQuery, { type: Sequelize.QueryTypes.SELECT })
888 ]).then(([ rows, totals ]) => {
889 // totals: totalVideos + totalVideoShares
890 let totalVideos = 0
891 let totalVideoShares = 0
892 if (totals[0]) totalVideos = parseInt(totals[0].total, 10)
893 if (totals[1]) totalVideoShares = parseInt(totals[1].total, 10)
894
895 const total = totalVideos + totalVideoShares
896 return {
897 data: rows,
898 total: total
899 }
900 })
901}
902 947
903listUserVideosForApi = function (userId: number, start: number, count: number, sort: string) { 948 await this.createTorrentAndSetInfoHash(inputVideoFile)
904 const query = { 949 await inputVideoFile.save()
905 distinct: true,
906 offset: start,
907 limit: count,
908 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ],
909 include: [
910 {
911 model: Video['sequelize'].models.VideoChannel,
912 required: true,
913 include: [
914 {
915 model: Video['sequelize'].models.Account,
916 where: {
917 userId
918 },
919 required: true
920 }
921 ]
922 },
923 Video['sequelize'].models.Tag
924 ]
925 }
926 950
927 return Video.findAndCountAll(query).then(({ rows, count }) => { 951 } catch (err) {
928 return { 952 // Auto destruction...
929 data: rows, 953 this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', err))
930 total: count
931 }
932 })
933}
934 954
935listForApi = function (start: number, count: number, sort: string) { 955 throw err
936 const query = { 956 }
937 distinct: true,
938 offset: start,
939 limit: count,
940 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ],
941 include: [
942 {
943 model: Video['sequelize'].models.VideoChannel,
944 required: true,
945 include: [
946 {
947 model: Video['sequelize'].models.Account,
948 required: true,
949 include: [
950 {
951 model: Video['sequelize'].models.Server,
952 required: false
953 }
954 ]
955 }
956 ]
957 },
958 Video['sequelize'].models.Tag
959 ],
960 where: createBaseVideosWhere()
961 } 957 }
962 958
963 return Video.findAndCountAll(query).then(({ rows, count }) => { 959 transcodeOriginalVideofile = async function (resolution: VideoResolution) {
964 return { 960 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
965 data: rows, 961 const extname = '.mp4'
966 total: count
967 }
968 })
969}
970 962
971load = function (id: number) { 963 // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed
972 return Video.findById(id) 964 const videoInputPath = join(videosDirectory, this.getVideoFilename(this.getOriginalFile()))
973}
974 965
975loadByUUID = function (uuid: string, t?: Sequelize.Transaction) { 966 const newVideoFile = new VideoFileModel({
976 const query: Sequelize.FindOptions<VideoAttributes> = { 967 resolution,
977 where: { 968 extname,
978 uuid 969 size: 0,
979 }, 970 videoId: this.id
980 include: [ Video['sequelize'].models.VideoFile ] 971 })
981 } 972 const videoOutputPath = join(videosDirectory, this.getVideoFilename(newVideoFile))
982 973
983 if (t !== undefined) query.transaction = t 974 const transcodeOptions = {
975 inputPath: videoInputPath,
976 outputPath: videoOutputPath,
977 resolution
978 }
984 979
985 return Video.findOne(query) 980 await transcode(transcodeOptions)
986}
987 981
988loadByUrlAndPopulateAccount = function (url: string, t?: Sequelize.Transaction) { 982 const stats = await statPromise(videoOutputPath)
989 const query: Sequelize.FindOptions<VideoAttributes> = {
990 where: {
991 url
992 },
993 include: [
994 Video['sequelize'].models.VideoFile,
995 {
996 model: Video['sequelize'].models.VideoChannel,
997 include: [ Video['sequelize'].models.Account ]
998 }
999 ]
1000 }
1001 983
1002 if (t !== undefined) query.transaction = t 984 newVideoFile.set('size', stats.size)
1003 985
1004 return Video.findOne(query) 986 await this.createTorrentAndSetInfoHash(newVideoFile)
1005}
1006 987
1007loadByUUIDOrURL = function (uuid: string, url: string, t?: Sequelize.Transaction) { 988 await newVideoFile.save()
1008 const query: Sequelize.FindOptions<VideoAttributes> = { 989
1009 where: { 990 this.VideoFiles.push(newVideoFile)
1010 [Sequelize.Op.or]: [
1011 { uuid },
1012 { url }
1013 ]
1014 },
1015 include: [ Video['sequelize'].models.VideoFile ]
1016 } 991 }
1017 992
1018 if (t !== undefined) query.transaction = t 993 getOriginalFileHeight () {
994 const originalFilePath = this.getVideoFilePath(this.getOriginalFile())
1019 995
1020 return Video.findOne(query) 996 return getVideoFileHeight(originalFilePath)
1021} 997 }
1022 998
1023loadAndPopulateAccountAndServerAndTags = function (id: number) { 999 getDescriptionPath () {
1024 const options = { 1000 return `/api/${API_VERSION}/videos/${this.uuid}/description`
1025 order: [ [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ],
1026 include: [
1027 {
1028 model: Video['sequelize'].models.VideoChannel,
1029 include: [
1030 {
1031 model: Video['sequelize'].models.Account,
1032 include: [ { model: Video['sequelize'].models.Server, required: false } ]
1033 }
1034 ]
1035 },
1036 {
1037 model: Video['sequelize'].models.AccountVideoRate,
1038 include: [ Video['sequelize'].models.Account ]
1039 },
1040 {
1041 model: Video['sequelize'].models.VideoShare,
1042 include: [ Video['sequelize'].models.Account ]
1043 },
1044 Video['sequelize'].models.Tag,
1045 Video['sequelize'].models.VideoFile
1046 ]
1047 } 1001 }
1048 1002
1049 return Video.findById(id, options) 1003 getCategoryLabel () {
1050} 1004 let categoryLabel = VIDEO_CATEGORIES[this.category]
1005 if (!categoryLabel) categoryLabel = 'Misc'
1051 1006
1052loadByUUIDAndPopulateAccountAndServerAndTags = function (uuid: string) { 1007 return categoryLabel
1053 const options = {
1054 order: [ [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ],
1055 where: {
1056 uuid
1057 },
1058 include: [
1059 {
1060 model: Video['sequelize'].models.VideoChannel,
1061 include: [
1062 {
1063 model: Video['sequelize'].models.Account,
1064 include: [ { model: Video['sequelize'].models.Server, required: false } ]
1065 }
1066 ]
1067 },
1068 {
1069 model: Video['sequelize'].models.AccountVideoRate,
1070 include: [ Video['sequelize'].models.Account ]
1071 },
1072 {
1073 model: Video['sequelize'].models.VideoShare,
1074 include: [ Video['sequelize'].models.Account ]
1075 },
1076 Video['sequelize'].models.Tag,
1077 Video['sequelize'].models.VideoFile
1078 ]
1079 } 1008 }
1080 1009
1081 return Video.findOne(options) 1010 getLicenceLabel () {
1082} 1011 let licenceLabel = VIDEO_LICENCES[this.licence]
1012 if (!licenceLabel) licenceLabel = 'Unknown'
1083 1013
1084searchAndPopulateAccountAndServerAndTags = function (value: string, start: number, count: number, sort: string) { 1014 return licenceLabel
1085 const serverInclude: Sequelize.IncludeOptions = {
1086 model: Video['sequelize'].models.Server,
1087 required: false
1088 } 1015 }
1089 1016
1090 const accountInclude: Sequelize.IncludeOptions = { 1017 getLanguageLabel () {
1091 model: Video['sequelize'].models.Account, 1018 let languageLabel = VIDEO_LANGUAGES[this.language]
1092 include: [ serverInclude ] 1019 if (!languageLabel) languageLabel = 'Unknown'
1020
1021 return languageLabel
1093 } 1022 }
1094 1023
1095 const videoChannelInclude: Sequelize.IncludeOptions = { 1024 removeThumbnail () {
1096 model: Video['sequelize'].models.VideoChannel, 1025 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
1097 include: [ accountInclude ], 1026 return unlinkPromise(thumbnailPath)
1098 required: true
1099 } 1027 }
1100 1028
1101 const tagInclude: Sequelize.IncludeOptions = { 1029 removePreview () {
1102 model: Video['sequelize'].models.Tag 1030 // Same name than video thumbnail
1031 return unlinkPromise(CONFIG.STORAGE.PREVIEWS_DIR + this.getPreviewName())
1103 } 1032 }
1104 1033
1105 const query: Sequelize.FindOptions<VideoAttributes> = { 1034 removeFile (videoFile: VideoFileModel) {
1106 distinct: true, 1035 const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
1107 where: createBaseVideosWhere(), 1036 return unlinkPromise(filePath)
1108 offset: start,
1109 limit: count,
1110 order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ]
1111 } 1037 }
1112 1038
1113 // TODO: search on tags too 1039 removeTorrent (videoFile: VideoFileModel) {
1114 // const escapedValue = Video['sequelize'].escape('%' + value + '%') 1040 const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
1115 // query.where['id'][Sequelize.Op.in] = Video['sequelize'].literal( 1041 return unlinkPromise(torrentPath)
1116 // `(SELECT "VideoTags"."videoId"
1117 // FROM "Tags"
1118 // INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId"
1119 // WHERE name ILIKE ${escapedValue}
1120 // )`
1121 // )
1122
1123 // TODO: search on account too
1124 // accountInclude.where = {
1125 // name: {
1126 // [Sequelize.Op.iLike]: '%' + value + '%'
1127 // }
1128 // }
1129 query.where['name'] = {
1130 [Sequelize.Op.iLike]: '%' + value + '%'
1131 } 1042 }
1132 1043
1133 query.include = [ 1044 private getBaseUrls () {
1134 videoChannelInclude, tagInclude 1045 let baseUrlHttp
1135 ] 1046 let baseUrlWs
1136 1047
1137 return Video.findAndCountAll(query).then(({ rows, count }) => { 1048 if (this.isOwned()) {
1138 return { 1049 baseUrlHttp = CONFIG.WEBSERVER.URL
1139 data: rows, 1050 baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
1140 total: count 1051 } else {
1052 baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + this.VideoChannel.Account.Server.host
1053 baseUrlWs = REMOTE_SCHEME.WS + '://' + this.VideoChannel.Account.Server.host
1141 } 1054 }
1142 })
1143}
1144
1145// ---------------------------------------------------------------------------
1146 1055
1147function createBaseVideosWhere () { 1056 return { baseUrlHttp, baseUrlWs }
1148 return {
1149 id: {
1150 [Sequelize.Op.notIn]: Video['sequelize'].literal(
1151 '(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")'
1152 )
1153 },
1154 privacy: VideoPrivacy.PUBLIC
1155 } 1057 }
1156}
1157 1058
1158function getBaseUrls (video: VideoInstance) { 1059 private getThumbnailUrl (baseUrlHttp: string) {
1159 let baseUrlHttp 1060 return baseUrlHttp + STATIC_PATHS.THUMBNAILS + this.getThumbnailName()
1160 let baseUrlWs
1161
1162 if (video.isOwned()) {
1163 baseUrlHttp = CONFIG.WEBSERVER.URL
1164 baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
1165 } else {
1166 baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + video.VideoChannel.Account.Server.host
1167 baseUrlWs = REMOTE_SCHEME.WS + '://' + video.VideoChannel.Account.Server.host
1168 } 1061 }
1169 1062
1170 return { baseUrlHttp, baseUrlWs } 1063 private getTorrentUrl (videoFile: VideoFileModel, baseUrlHttp: string) {
1171} 1064 return baseUrlHttp + STATIC_PATHS.TORRENTS + this.getTorrentFileName(videoFile)
1172 1065 }
1173function getThumbnailUrl (video: VideoInstance, baseUrlHttp: string) {
1174 return baseUrlHttp + STATIC_PATHS.THUMBNAILS + video.getThumbnailName()
1175}
1176 1066
1177function getTorrentUrl (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string) { 1067 private getVideoFileUrl (videoFile: VideoFileModel, baseUrlHttp: string) {
1178 return baseUrlHttp + STATIC_PATHS.TORRENTS + video.getTorrentFileName(videoFile) 1068 return baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile)
1179} 1069 }
1180 1070
1181function getVideoFileUrl (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string) { 1071 private generateMagnetUri (videoFile: VideoFileModel, baseUrlHttp: string, baseUrlWs: string) {
1182 return baseUrlHttp + STATIC_PATHS.WEBSEED + video.getVideoFilename(videoFile) 1072 const xs = this.getTorrentUrl(videoFile, baseUrlHttp)
1183} 1073 const announce = [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
1074 const urlList = [ this.getVideoFileUrl(videoFile, baseUrlHttp) ]
1075
1076 const magnetHash = {
1077 xs,
1078 announce,
1079 urlList,
1080 infoHash: videoFile.infoHash,
1081 name: this.name
1082 }
1184 1083
1185function generateMagnetUri (video: VideoInstance, videoFile: VideoFileInstance, baseUrlHttp: string, baseUrlWs: string) { 1084 return magnetUtil.encode(magnetHash)
1186 const xs = getTorrentUrl(video, videoFile, baseUrlHttp)
1187 const announce = [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
1188 const urlList = [ getVideoFileUrl(video, videoFile, baseUrlHttp) ]
1189
1190 const magnetHash = {
1191 xs,
1192 announce,
1193 urlList,
1194 infoHash: videoFile.infoHash,
1195 name: video.name
1196 } 1085 }
1197
1198 return magnetUtil.encode(magnetHash)
1199} 1086}