diff options
Diffstat (limited to 'server/models')
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 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | import * as Sequelize from 'sequelize' | ||
3 | import { AccountFollow, FollowState } from '../../../shared/models/accounts/follow.model' | ||
4 | import { ResultList } from '../../../shared/models/result-list.model' | ||
5 | import { AccountInstance } from './account-interface' | ||
6 | |||
7 | export 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 | |||
33 | export 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 | |||
43 | export interface AccountFollowAttributes { | ||
44 | accountId: number | ||
45 | targetAccountId: number | ||
46 | state: FollowState | ||
47 | } | ||
48 | |||
49 | export 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 | |||
60 | export 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 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
1 | import { values } from 'lodash' | 2 | import { values } from 'lodash' |
2 | import * as Sequelize from 'sequelize' | 3 | import * as Sequelize from 'sequelize' |
3 | 4 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' | |
4 | import { addMethodsToModel, getSort } from '../utils' | 5 | import { FollowState } from '../../../shared/models/accounts' |
5 | import { AccountFollowAttributes, AccountFollowInstance, AccountFollowMethods } from './account-follow-interface' | ||
6 | import { FOLLOW_STATES } from '../../initializers/constants' | 6 | import { FOLLOW_STATES } from '../../initializers/constants' |
7 | import { ServerModel } from '../server/server' | ||
8 | import { getSort } from '../utils' | ||
9 | import { AccountModel } from './account' | ||
7 | 10 | ||
8 | let AccountFollow: Sequelize.Model<AccountFollowInstance, AccountFollowAttributes> | 11 | @Table({ |
9 | let loadByAccountAndTarget: AccountFollowMethods.LoadByAccountAndTarget | 12 | tableName: 'accountFollow', |
10 | let listFollowingForApi: AccountFollowMethods.ListFollowingForApi | 13 | indexes: [ |
11 | let listFollowersForApi: AccountFollowMethods.ListFollowersForApi | ||
12 | let listAcceptedFollowerUrlsForApi: AccountFollowMethods.ListAcceptedFollowerUrlsForApi | ||
13 | let listAcceptedFollowingUrlsForApi: AccountFollowMethods.ListAcceptedFollowingUrlsForApi | ||
14 | let listAcceptedFollowerSharedInboxUrls: AccountFollowMethods.ListAcceptedFollowerSharedInboxUrls | ||
15 | let toFormattedJSON: AccountFollowMethods.ToFormattedJSON | ||
16 | |||
17 | export 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 | 26 | export 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 | ||
60 | function 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 | ||
80 | toFormattedJSON = 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 | |||
96 | loadByAccountAndTarget = 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 | ||
120 | listFollowingForApi = 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 | ||
152 | listFollowersForApi = 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 | ||
184 | listAcceptedFollowerUrlsForApi = 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 | ||
188 | listAcceptedFollowerSharedInboxUrls = 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 | ||
192 | listAcceptedFollowingUrlsForApi = 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>[] = [] | |
198 | async 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 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | import * as Sequelize from 'sequelize' | ||
3 | import { Account as FormattedAccount, ActivityPubActor } from '../../../shared' | ||
4 | import { AvatarInstance } from '../avatar' | ||
5 | import { ServerInstance } from '../server/server-interface' | ||
6 | import { VideoChannelInstance } from '../video/video-channel-interface' | ||
7 | |||
8 | export 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 | |||
27 | export 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 | |||
37 | export 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 | |||
58 | export 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 | |||
76 | export 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | import * as Promise from 'bluebird' | ||
3 | |||
4 | import { VideoRateType } from '../../../shared/models/videos/video-rate.type' | ||
5 | import { AccountInstance } from './account-interface' | ||
6 | |||
7 | export namespace AccountVideoRateMethods { | ||
8 | export type Load = (accountId: number, videoId: number, transaction: Sequelize.Transaction) => Promise<AccountVideoRateInstance> | ||
9 | } | ||
10 | |||
11 | export interface AccountVideoRateClass { | ||
12 | load: AccountVideoRateMethods.Load | ||
13 | } | ||
14 | |||
15 | export interface AccountVideoRateAttributes { | ||
16 | type: VideoRateType | ||
17 | accountId: number | ||
18 | videoId: number | ||
19 | |||
20 | Account?: AccountInstance | ||
21 | } | ||
22 | |||
23 | export interface AccountVideoRateInstance | ||
24 | extends AccountVideoRateClass, AccountVideoRateAttributes, Sequelize.Instance<AccountVideoRateAttributes> { | ||
25 | id: number | ||
26 | createdAt: Date | ||
27 | updatedAt: Date | ||
28 | } | ||
29 | |||
30 | export 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 | */ | ||
4 | import { values } from 'lodash' | 1 | import { values } from 'lodash' |
5 | import * as Sequelize from 'sequelize' | 2 | import { Transaction } from 'sequelize' |
6 | 3 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' | |
4 | import { IFindOptions } from 'sequelize-typescript/lib/interfaces/IFindOptions' | ||
5 | import { VideoRateType } from '../../../shared/models/videos' | ||
7 | import { VIDEO_RATE_TYPES } from '../../initializers' | 6 | import { VIDEO_RATE_TYPES } from '../../initializers' |
7 | import { VideoModel } from '../video/video' | ||
8 | import { AccountModel } from './account' | ||
8 | 9 | ||
9 | import { addMethodsToModel } from '../utils' | 10 | /* |
10 | import { | 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 | |||
17 | let AccountVideoRate: Sequelize.Model<AccountVideoRateInstance, AccountVideoRateAttributes> | ||
18 | let load: AccountVideoRateMethods.Load | ||
19 | |||
20 | export 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 | }) | ||
22 | export 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 | ||
50 | function 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 | ||
68 | load = 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 @@ | |||
1 | import { join } from 'path' | 1 | import { join } from 'path' |
2 | import * as Sequelize from 'sequelize' | 2 | import * as Sequelize from 'sequelize' |
3 | import { | ||
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' | ||
3 | import { Avatar } from '../../../shared/models/avatars/avatar.model' | 19 | import { Avatar } from '../../../shared/models/avatars/avatar.model' |
20 | import { activityPubContextify } from '../../helpers' | ||
4 | import { | 21 | import { |
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' |
12 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 28 | import { isUserUsernameValid } from '../../helpers/custom-validators/users' |
13 | import { AVATARS_DIR } from '../../initializers' | 29 | import { AVATARS_DIR, CONFIG, CONSTRAINTS_FIELDS } from '../../initializers' |
14 | import { CONFIG, CONSTRAINTS_FIELDS } from '../../initializers/constants' | 30 | import { sendDeleteAccount } from '../../lib/activitypub/send' |
15 | import { sendDeleteAccount } from '../../lib/activitypub/send/send-delete' | 31 | import { ApplicationModel } from '../application/application' |
16 | import { addMethodsToModel } from '../utils' | 32 | import { AvatarModel } from '../avatar/avatar' |
17 | import { AccountAttributes, AccountInstance, AccountMethods } from './account-interface' | 33 | import { ServerModel } from '../server/server' |
18 | 34 | import { throwIfNotValid } from '../utils' | |
19 | let Account: Sequelize.Model<AccountInstance, AccountAttributes> | 35 | import { VideoChannelModel } from '../video/video-channel' |
20 | let load: AccountMethods.Load | 36 | import { AccountFollowModel } from './account-follow' |
21 | let loadApplication: AccountMethods.LoadApplication | 37 | import { UserModel } from './user' |
22 | let loadByUUID: AccountMethods.LoadByUUID | 38 | |
23 | let loadByUrl: AccountMethods.LoadByUrl | 39 | @Table({ |
24 | let loadLocalByName: AccountMethods.LoadLocalByName | 40 | tableName: 'account', |
25 | let loadByNameAndHost: AccountMethods.LoadByNameAndHost | 41 | indexes: [ |
26 | let listByFollowersUrls: AccountMethods.ListByFollowersUrls | ||
27 | let isOwned: AccountMethods.IsOwned | ||
28 | let toActivityPubObject: AccountMethods.ToActivityPubObject | ||
29 | let toFormattedJSON: AccountMethods.ToFormattedJSON | ||
30 | let getFollowerSharedInboxUrls: AccountMethods.GetFollowerSharedInboxUrls | ||
31 | let getFollowingUrl: AccountMethods.GetFollowingUrl | ||
32 | let getFollowersUrl: AccountMethods.GetFollowersUrl | ||
33 | let getPublicKeyUrl: AccountMethods.GetPublicKeyUrl | ||
34 | |||
35 | export 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 | 62 | export 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 | ||
208 | function 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 | ||
268 | function 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 | ||
276 | toFormattedJSON = 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 | ||
310 | toActivityPubObject = 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 | ||
337 | isOwned = function (this: AccountInstance) { | 290 | return AccountModel.findOne(query) |
338 | return this.serverId === null | 291 | } |
339 | } | ||
340 | 292 | ||
341 | getFollowerSharedInboxUrls = 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 | ||
361 | getFollowingUrl = 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 | ||
365 | getFollowersUrl = 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 | ||
369 | getPublicKeyUrl = 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 | ||
375 | loadApplication = 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 | |||
386 | load = function (id: number) { | ||
387 | return Account.findById(id) | ||
388 | } | ||
389 | |||
390 | loadByUUID = 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 | ||
400 | loadLocalByName = 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 | |||
422 | loadByNameAndHost = 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 | |||
441 | loadByUrl = 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 | |||
452 | listByFollowersUrls = 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 @@ | |||
1 | export * from './account-interface' | ||
2 | export * from './account-follow-interface' | ||
3 | export * from './account-video-rate-interface' | ||
4 | export * from './user-interface' | ||
diff --git a/server/models/account/user-interface.ts b/server/models/account/user-interface.ts deleted file mode 100644 index 0f0b72063..000000000 --- a/server/models/account/user-interface.ts +++ /dev/null | |||
@@ -1,67 +0,0 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | import * as Sequelize from 'sequelize' | ||
3 | import { ResultList } from '../../../shared/models/result-list.model' | ||
4 | import { UserRight } from '../../../shared/models/users/user-right.enum' | ||
5 | import { UserRole } from '../../../shared/models/users/user-role' | ||
6 | import { User as FormattedUser } from '../../../shared/models/users/user.model' | ||
7 | import { AccountInstance } from './account-interface' | ||
8 | |||
9 | export 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 | |||
30 | export 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 | |||
45 | export 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 | |||
57 | export 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 | |||
67 | export 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import { | ||
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' | ||
2 | import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared' | 16 | import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared' |
3 | import { | 17 | import { |
4 | comparePassword, | 18 | comparePassword, |
5 | cryptPassword, | 19 | cryptPassword |
6 | isUserDisplayNSFWValid, | ||
7 | isUserPasswordValid, | ||
8 | isUserRoleValid, | ||
9 | isUserUsernameValid, | ||
10 | isUserVideoQuotaValid | ||
11 | } from '../../helpers' | 20 | } from '../../helpers' |
12 | import { addMethodsToModel, getSort } from '../utils' | 21 | import { |
13 | import { UserAttributes, UserInstance, UserMethods } from './user-interface' | 22 | isUserDisplayNSFWValid, isUserPasswordValid, isUserRoleValid, isUserUsernameValid, |
14 | 23 | isUserVideoQuotaValid | |
15 | let User: Sequelize.Model<UserInstance, UserAttributes> | 24 | } from '../../helpers/custom-validators/users' |
16 | let isPasswordMatch: UserMethods.IsPasswordMatch | 25 | import { OAuthTokenModel } from '../oauth/oauth-token' |
17 | let hasRight: UserMethods.HasRight | 26 | import { getSort, throwIfNotValid } from '../utils' |
18 | let toFormattedJSON: UserMethods.ToFormattedJSON | 27 | import { VideoChannelModel } from '../video/video-channel' |
19 | let countTotal: UserMethods.CountTotal | 28 | import { AccountModel } from './account' |
20 | let getByUsername: UserMethods.GetByUsername | 29 | |
21 | let listForApi: UserMethods.ListForApi | 30 | @Table({ |
22 | let loadById: UserMethods.LoadById | 31 | tableName: 'user', |
23 | let loadByUsername: UserMethods.LoadByUsername | 32 | indexes: [ |
24 | let loadByUsernameAndPopulateChannels: UserMethods.LoadByUsernameAndPopulateChannels | ||
25 | let loadByUsernameOrEmail: UserMethods.LoadByUsernameOrEmail | ||
26 | let isAbleToUploadVideo: UserMethods.IsAbleToUploadVideo | ||
27 | |||
28 | export 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, | 43 | export 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 | ||
130 | function 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 | ||
142 | hasRight = function (this: UserInstance, right: UserRight) { | 106 | static countTotal () { |
143 | return hasUserRight(this.role, right) | 107 | return this.count() |
144 | } | 108 | } |
145 | 109 | ||
146 | isPasswordMatch = 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 | ||
150 | toFormattedJSON = 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 } ] | |
179 | isAbleToUploadVideo = 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 | |||
189 | function 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 | ||
201 | countTotal = 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 | ||
205 | getByUsername = 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 | ||
216 | listForApi = 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 | ||
232 | loadById = 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 | ||
240 | loadByUsername = 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 | ||
251 | loadByUsernameAndPopulateChannels = 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 | ||
268 | loadByUsernameOrEmail = 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 => { | |
282 | function 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | import * as Bluebird from 'bluebird' | ||
3 | |||
4 | export 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 | |||
15 | export interface ApplicationClass { | ||
16 | loadMigrationVersion: ApplicationMethods.LoadMigrationVersion | ||
17 | updateMigrationVersion: ApplicationMethods.UpdateMigrationVersion | ||
18 | countTotal: ApplicationMethods.CountTotal | ||
19 | } | ||
20 | |||
21 | export interface ApplicationAttributes { | ||
22 | migrationVersion: number | ||
23 | } | ||
24 | |||
25 | export interface ApplicationInstance extends ApplicationClass, ApplicationAttributes, Sequelize.Instance<ApplicationAttributes> { | ||
26 | id: number | ||
27 | createdAt: Date | ||
28 | updatedAt: Date | ||
29 | } | ||
30 | |||
31 | export 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import { Transaction } from 'sequelize' |
2 | 2 | import { AllowNull, Column, Default, IsInt, Model, Table } from 'sequelize-typescript' | |
3 | import { addMethodsToModel } from '../utils' | 3 | |
4 | import { | 4 | @Table({ |
5 | ApplicationAttributes, | 5 | tableName: 'application' |
6 | ApplicationInstance, | 6 | }) |
7 | 7 | export class ApplicationModel extends Model<ApplicationModel> { | |
8 | ApplicationMethods | 8 | |
9 | } from './application-interface' | 9 | @AllowNull(false) |
10 | 10 | @Default(0) | |
11 | let Application: Sequelize.Model<ApplicationInstance, ApplicationAttributes> | 11 | @IsInt |
12 | let loadMigrationVersion: ApplicationMethods.LoadMigrationVersion | 12 | @Column |
13 | let updateMigrationVersion: ApplicationMethods.UpdateMigrationVersion | 13 | migrationVersion: number |
14 | let countTotal: ApplicationMethods.CountTotal | 14 | |
15 | static countTotal () { | ||
16 | return ApplicationModel.count() | ||
17 | } | ||
15 | 18 | ||
16 | export 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 | |||
42 | countTotal = function () { | ||
43 | return this.count() | ||
44 | } | ||
45 | |||
46 | loadMigrationVersion = 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 | ||
54 | updateMigrationVersion = 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 @@ | |||
1 | export * from './application-interface' | ||
diff --git a/server/models/avatar/avatar-interface.ts b/server/models/avatar/avatar-interface.ts deleted file mode 100644 index 4af2b87b7..000000000 --- a/server/models/avatar/avatar-interface.ts +++ /dev/null | |||
@@ -1,16 +0,0 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | export namespace AvatarMethods {} | ||
4 | |||
5 | export interface AvatarClass {} | ||
6 | |||
7 | export interface AvatarAttributes { | ||
8 | filename: string | ||
9 | } | ||
10 | |||
11 | export interface AvatarInstance extends AvatarClass, AvatarAttributes, Sequelize.Instance<AvatarAttributes> { | ||
12 | createdAt: Date | ||
13 | updatedAt: Date | ||
14 | } | ||
15 | |||
16 | export 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import { AllowNull, Column, CreatedAt, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | import { addMethodsToModel } from '../utils' | ||
3 | import { AvatarAttributes, AvatarInstance } from './avatar-interface' | ||
4 | 2 | ||
5 | let Avatar: Sequelize.Model<AvatarInstance, AvatarAttributes> | 3 | @Table({ |
4 | tableName: 'avatar' | ||
5 | }) | ||
6 | export class AvatarModel extends Model<AvatarModel> { | ||
6 | 7 | ||
7 | export 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 @@ | |||
1 | export * from './avatar-interface' | ||
diff --git a/server/models/index.ts b/server/models/index.ts deleted file mode 100644 index fedd97dd1..000000000 --- a/server/models/index.ts +++ /dev/null | |||
@@ -1,7 +0,0 @@ | |||
1 | export * from './application' | ||
2 | export * from './avatar' | ||
3 | export * from './job' | ||
4 | export * from './oauth' | ||
5 | export * from './server' | ||
6 | export * from './account' | ||
7 | export * from './video' | ||
diff --git a/server/models/job/index.ts b/server/models/job/index.ts deleted file mode 100644 index 56925fd32..000000000 --- a/server/models/job/index.ts +++ /dev/null | |||
@@ -1 +0,0 @@ | |||
1 | export * from './job-interface' | ||
diff --git a/server/models/job/job-interface.ts b/server/models/job/job-interface.ts deleted file mode 100644 index 3cfc0fbed..000000000 --- a/server/models/job/job-interface.ts +++ /dev/null | |||
@@ -1,33 +0,0 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | import * as Sequelize from 'sequelize' | ||
3 | import { Job as FormattedJob, JobCategory, JobState } from '../../../shared/models/job.model' | ||
4 | import { ResultList } from '../../../shared/models/result-list.model' | ||
5 | |||
6 | export 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 | |||
13 | export interface JobClass { | ||
14 | listWithLimitByCategory: JobMethods.ListWithLimitByCategory | ||
15 | listForApi: JobMethods.ListForApi, | ||
16 | } | ||
17 | |||
18 | export interface JobAttributes { | ||
19 | state: JobState | ||
20 | category: JobCategory | ||
21 | handlerName: string | ||
22 | handlerInputData: any | ||
23 | } | ||
24 | |||
25 | export interface JobInstance extends JobClass, JobAttributes, Sequelize.Instance<JobAttributes> { | ||
26 | id: number | ||
27 | createdAt: Date | ||
28 | updatedAt: Date | ||
29 | |||
30 | toFormattedJSON: JobMethods.ToFormattedJSON | ||
31 | } | ||
32 | |||
33 | export 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 @@ | |||
1 | import { values } from 'lodash' | 1 | import { values } from 'lodash' |
2 | import * as Sequelize from 'sequelize' | 2 | import { AllowNull, Column, CreatedAt, DataType, Model, Table, UpdatedAt } from 'sequelize-typescript' |
3 | import { JobCategory, JobState } from '../../../shared/models/job.model' | 3 | import { JobCategory, JobState } from '../../../shared/models' |
4 | import { JOB_CATEGORIES, JOB_STATES } from '../../initializers' | 4 | import { JOB_CATEGORIES, JOB_STATES } from '../../initializers' |
5 | import { addMethodsToModel, getSort } from '../utils' | 5 | import { getSort } from '../utils' |
6 | import { JobAttributes, JobInstance, JobMethods } from './job-interface' | ||
7 | 6 | ||
8 | let Job: Sequelize.Model<JobInstance, JobAttributes> | 7 | @Table({ |
9 | let listWithLimitByCategory: JobMethods.ListWithLimitByCategory | 8 | tableName: 'job', |
10 | let listForApi: JobMethods.ListForApi | 9 | indexes: [ |
11 | let toFormattedJSON: JobMethods.ToFormattedJSON | ||
12 | |||
13 | export 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 | 15 | export 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 | ||
54 | toFormattedJSON = 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 | ||
68 | listWithLimitByCategory = 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 | ||
83 | listForApi = 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 @@ | |||
1 | export * from './oauth-client-interface' | ||
2 | export * from './oauth-token-interface' | ||
diff --git a/server/models/oauth/oauth-client-interface.ts b/server/models/oauth/oauth-client-interface.ts deleted file mode 100644 index 3526e4159..000000000 --- a/server/models/oauth/oauth-client-interface.ts +++ /dev/null | |||
@@ -1,31 +0,0 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | import * as Promise from 'bluebird' | ||
3 | |||
4 | export 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 | |||
12 | export interface OAuthClientClass { | ||
13 | countTotal: OAuthClientMethods.CountTotal | ||
14 | loadFirstClient: OAuthClientMethods.LoadFirstClient | ||
15 | getByIdAndSecret: OAuthClientMethods.GetByIdAndSecret | ||
16 | } | ||
17 | |||
18 | export interface OAuthClientAttributes { | ||
19 | clientId: string | ||
20 | clientSecret: string | ||
21 | grants: string[] | ||
22 | redirectUris: string[] | ||
23 | } | ||
24 | |||
25 | export interface OAuthClientInstance extends OAuthClientClass, OAuthClientAttributes, Sequelize.Instance<OAuthClientAttributes> { | ||
26 | id: number | ||
27 | createdAt: Date | ||
28 | updatedAt: Date | ||
29 | } | ||
30 | |||
31 | export 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import { AllowNull, Column, CreatedAt, DataType, HasMany, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | import { OAuthTokenModel } from './oauth-token' | ||
2 | 3 | ||
3 | import { addMethodsToModel } from '../utils' | 4 | @Table({ |
4 | import { | 5 | tableName: 'oAuthClient', |
5 | OAuthClientInstance, | 6 | indexes: [ |
6 | OAuthClientAttributes, | ||
7 | |||
8 | OAuthClientMethods | ||
9 | } from './oauth-client-interface' | ||
10 | |||
11 | let OAuthClient: Sequelize.Model<OAuthClientInstance, OAuthClientAttributes> | ||
12 | let countTotal: OAuthClientMethods.CountTotal | ||
13 | let loadFirstClient: OAuthClientMethods.LoadFirstClient | ||
14 | let getByIdAndSecret: OAuthClientMethods.GetByIdAndSecret | ||
15 | |||
16 | export 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 | }) | ||
17 | export 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 | ||
62 | function 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 | ||
69 | countTotal = function () { | 44 | static countTotal () { |
70 | return OAuthClient.count() | 45 | return OAuthClientModel.count() |
71 | } | 46 | } |
72 | 47 | ||
73 | loadFirstClient = function () { | 48 | static loadFirstClient () { |
74 | return OAuthClient.findOne() | 49 | return OAuthClientModel.findOne() |
75 | } | 50 | } |
76 | 51 | ||
77 | getByIdAndSecret = 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 @@ | |||
1 | import * as Promise from 'bluebird' | ||
2 | import * as Sequelize from 'sequelize' | ||
3 | |||
4 | import { UserModel } from '../account/user-interface' | ||
5 | |||
6 | export type OAuthTokenInfo = { | ||
7 | refreshToken: string | ||
8 | refreshTokenExpiresAt: Date, | ||
9 | client: { | ||
10 | id: number | ||
11 | }, | ||
12 | user: { | ||
13 | id: number | ||
14 | } | ||
15 | } | ||
16 | |||
17 | export 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 | |||
23 | export interface OAuthTokenClass { | ||
24 | getByRefreshTokenAndPopulateClient: OAuthTokenMethods.GetByRefreshTokenAndPopulateClient | ||
25 | getByTokenAndPopulateUser: OAuthTokenMethods.GetByTokenAndPopulateUser | ||
26 | getByRefreshTokenAndPopulateUser: OAuthTokenMethods.GetByRefreshTokenAndPopulateUser | ||
27 | } | ||
28 | |||
29 | export 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 | |||
40 | export interface OAuthTokenInstance extends OAuthTokenClass, OAuthTokenAttributes, Sequelize.Instance<OAuthTokenAttributes> { | ||
41 | id: number | ||
42 | createdAt: Date | ||
43 | updatedAt: Date | ||
44 | } | ||
45 | |||
46 | export 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | |||
3 | import { logger } from '../../helpers' | 2 | import { logger } from '../../helpers' |
3 | import { AccountModel } from '../account/account' | ||
4 | import { UserModel } from '../account/user' | ||
5 | import { OAuthClientModel } from './oauth-client' | ||
6 | |||
7 | export type OAuthTokenInfo = { | ||
8 | refreshToken: string | ||
9 | refreshTokenExpiresAt: Date, | ||
10 | client: { | ||
11 | id: number | ||
12 | }, | ||
13 | user: { | ||
14 | id: number | ||
15 | } | ||
16 | } | ||
4 | 17 | ||
5 | import { addMethodsToModel } from '../utils' | 18 | @Table({ |
6 | import { OAuthTokenAttributes, OAuthTokenInfo, OAuthTokenInstance, OAuthTokenMethods } from './oauth-token-interface' | 19 | tableName: 'oAuthToken', |
7 | 20 | indexes: [ | |
8 | let OAuthToken: Sequelize.Model<OAuthTokenInstance, OAuthTokenAttributes> | ||
9 | let getByRefreshTokenAndPopulateClient: OAuthTokenMethods.GetByRefreshTokenAndPopulateClient | ||
10 | let getByTokenAndPopulateUser: OAuthTokenMethods.GetByTokenAndPopulateUser | ||
11 | let getByRefreshTokenAndPopulateUser: OAuthTokenMethods.GetByRefreshTokenAndPopulateUser | ||
12 | |||
13 | export 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 | }) | ||
37 | export 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 | ||
67 | function 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 | ||
85 | getByRefreshTokenAndPopulateClient = 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 | ||
116 | getByTokenAndPopulateUser = 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 | ||
141 | getByRefreshTokenAndPopulateUser = 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 @@ | |||
1 | export * from './server-interface' | ||
diff --git a/server/models/server/server-interface.ts b/server/models/server/server-interface.ts deleted file mode 100644 index be1a4917e..000000000 --- a/server/models/server/server-interface.ts +++ /dev/null | |||
@@ -1,24 +0,0 @@ | |||
1 | import * as Promise from 'bluebird' | ||
2 | import * as Sequelize from 'sequelize' | ||
3 | |||
4 | export namespace ServerMethods { | ||
5 | export type ListBadServers = () => Promise<ServerInstance[]> | ||
6 | export type UpdateServersScoreAndRemoveBadOnes = (goodServers: number[], badServers: number[]) => void | ||
7 | } | ||
8 | |||
9 | export interface ServerClass { | ||
10 | updateServersScoreAndRemoveBadOnes: ServerMethods.UpdateServersScoreAndRemoveBadOnes | ||
11 | } | ||
12 | |||
13 | export interface ServerAttributes { | ||
14 | id?: number | ||
15 | host?: string | ||
16 | score?: number | Sequelize.literal // Sequelize literal for 'score +' + value | ||
17 | } | ||
18 | |||
19 | export interface ServerInstance extends ServerClass, ServerAttributes, Sequelize.Instance<ServerAttributes> { | ||
20 | createdAt: Date | ||
21 | updatedAt: Date | ||
22 | } | ||
23 | |||
24 | export 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import { isHostValid, logger } from '../../helpers' | 2 | import { AllowNull, Column, CreatedAt, Default, Is, IsInt, Max, Model, Table, UpdatedAt } from 'sequelize-typescript' |
3 | import { logger } from '../../helpers' | ||
4 | import { isHostValid } from '../../helpers/custom-validators/servers' | ||
3 | import { SERVERS_SCORE } from '../../initializers' | 5 | import { SERVERS_SCORE } from '../../initializers' |
4 | import { addMethodsToModel } from '../utils' | 6 | import { throwIfNotValid } from '../utils' |
5 | import { ServerAttributes, ServerInstance, ServerMethods } from './server-interface' | ||
6 | 7 | ||
7 | let Server: Sequelize.Model<ServerInstance, ServerAttributes> | 8 | @Table({ |
8 | let updateServersScoreAndRemoveBadOnes: ServerMethods.UpdateServersScoreAndRemoveBadOnes | 9 | tableName: 'server', |
9 | 10 | indexes: [ | |
10 | export 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 | 20 | export 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 | |
56 | updateServersScoreAndRemoveBadOnes = 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) |
77 | async 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 | ||
96 | function 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 | ||
114 | function 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 | ||
17 | function 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 | |||
22 | function getSortOnModel (model: any, value: string) { | 17 | function 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 | ||
24 | function 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 | ||
31 | export { | 32 | export { |
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 @@ | |||
1 | export * from './tag-interface' | ||
2 | export * from './video-abuse-interface' | ||
3 | export * from './video-blacklist-interface' | ||
4 | export * from './video-channel-interface' | ||
5 | export * from './video-tag-interface' | ||
6 | export * from './video-file-interface' | ||
7 | export * from './video-interface' | ||
8 | export * from './video-share-interface' | ||
9 | export * from './video-channel-share-interface' | ||
diff --git a/server/models/video/tag-interface.ts b/server/models/video/tag-interface.ts deleted file mode 100644 index 08e5c3246..000000000 --- a/server/models/video/tag-interface.ts +++ /dev/null | |||
@@ -1,20 +0,0 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | import * as Promise from 'bluebird' | ||
3 | |||
4 | export namespace TagMethods { | ||
5 | export type FindOrCreateTags = (tags: string[], transaction: Sequelize.Transaction) => Promise<TagInstance[]> | ||
6 | } | ||
7 | |||
8 | export interface TagClass { | ||
9 | findOrCreateTags: TagMethods.FindOrCreateTags | ||
10 | } | ||
11 | |||
12 | export interface TagAttributes { | ||
13 | name: string | ||
14 | } | ||
15 | |||
16 | export interface TagInstance extends TagClass, TagAttributes, Sequelize.Instance<TagAttributes> { | ||
17 | id: number | ||
18 | } | ||
19 | |||
20 | export 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Bluebird from 'bluebird' |
2 | import * as Promise from 'bluebird' | 2 | import { Transaction } from 'sequelize' |
3 | 3 | import { AllowNull, BelongsToMany, Column, CreatedAt, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | |
4 | import { addMethodsToModel } from '../utils' | 4 | import { isVideoTagValid } from '../../helpers/custom-validators/videos' |
5 | import { | 5 | import { throwIfNotValid } from '../utils' |
6 | TagInstance, | 6 | import { VideoModel } from './video' |
7 | TagAttributes, | 7 | import { VideoTagModel } from './video-tag' |
8 | 8 | ||
9 | TagMethods | 9 | @Table({ |
10 | } from './tag-interface' | 10 | tableName: 'tag', |
11 | 11 | timestamps: false, | |
12 | let Tag: Sequelize.Model<TagInstance, TagAttributes> | 12 | indexes: [ |
13 | let findOrCreateTags: TagMethods.FindOrCreateTags | ||
14 | |||
15 | export 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 | }) |
19 | export 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 | ||
46 | function 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 | ||
54 | findOrCreateTags = 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 @@ | |||
1 | import * as Promise from 'bluebird' | ||
2 | import * as Sequelize from 'sequelize' | ||
3 | import { ResultList } from '../../../shared' | ||
4 | import { VideoAbuse as FormattedVideoAbuse } from '../../../shared/models/videos/video-abuse.model' | ||
5 | import { AccountInstance } from '../account/account-interface' | ||
6 | import { ServerInstance } from '../server/server-interface' | ||
7 | import { VideoInstance } from './video-interface' | ||
8 | import { VideoAbuseObject } from '../../../shared/models/activitypub/objects/video-abuse-object' | ||
9 | |||
10 | export 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 | |||
17 | export interface VideoAbuseClass { | ||
18 | listForApi: VideoAbuseMethods.ListForApi | ||
19 | toActivityPubObject: VideoAbuseMethods.ToActivityPubObject | ||
20 | } | ||
21 | |||
22 | export interface VideoAbuseAttributes { | ||
23 | reason: string | ||
24 | videoId: number | ||
25 | reporterAccountId: number | ||
26 | |||
27 | Account?: AccountInstance | ||
28 | Video?: VideoInstance | ||
29 | } | ||
30 | |||
31 | export 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 | |||
41 | export 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | 2 | import { VideoAbuseObject } from '../../../shared/models/activitypub/objects' | |
3 | import { isVideoAbuseReasonValid } from '../../helpers/custom-validators/videos' | ||
3 | import { CONFIG } from '../../initializers' | 4 | import { CONFIG } from '../../initializers' |
4 | import { isVideoAbuseReasonValid } from '../../helpers' | 5 | import { AccountModel } from '../account/account' |
5 | 6 | import { ServerModel } from '../server/server' | |
6 | import { addMethodsToModel, getSort } from '../utils' | 7 | import { getSort, throwIfNotValid } from '../utils' |
7 | import { | 8 | import { VideoModel } from './video' |
8 | VideoAbuseInstance, | 9 | |
9 | VideoAbuseAttributes, | 10 | @Table({ |
10 | 11 | tableName: 'videoAbuse', | |
11 | VideoAbuseMethods | 12 | indexes: [ |
12 | } from './video-abuse-interface' | ||
13 | import { VideoAbuseObject } from '../../../shared/models/activitypub/objects/video-abuse-object' | ||
14 | |||
15 | let VideoAbuse: Sequelize.Model<VideoAbuseInstance, VideoAbuseAttributes> | ||
16 | let toFormattedJSON: VideoAbuseMethods.ToFormattedJSON | ||
17 | let listForApi: VideoAbuseMethods.ListForApi | ||
18 | let toActivityPubObject: VideoAbuseMethods.ToActivityPubObject | ||
19 | |||
20 | export 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 | 21 | export class VideoAbuseModel extends Model<VideoAbuseModel> { | |
57 | return VideoAbuse | ||
58 | } | ||
59 | |||
60 | // ------------------------------ METHODS ------------------------------ | ||
61 | |||
62 | toFormattedJSON = 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 | ||
86 | toActivityPubObject = 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 | ||
98 | function 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 | ||
116 | listForApi = 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | import * as Promise from 'bluebird' | ||
3 | |||
4 | import { SortType } from '../../helpers' | ||
5 | import { ResultList } from '../../../shared' | ||
6 | import { VideoInstance } from './video-interface' | ||
7 | |||
8 | // Don't use barrel, import just what we need | ||
9 | import { BlacklistedVideo as FormattedBlacklistedVideo } from '../../../shared/models/videos/video-blacklist.model' | ||
10 | |||
11 | export 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 | |||
17 | export interface BlacklistedVideoClass { | ||
18 | toFormattedJSON: BlacklistedVideoMethods.ToFormattedJSON | ||
19 | listForApi: BlacklistedVideoMethods.ListForApi | ||
20 | loadByVideoId: BlacklistedVideoMethods.LoadByVideoId | ||
21 | } | ||
22 | |||
23 | export interface BlacklistedVideoAttributes { | ||
24 | videoId: number | ||
25 | |||
26 | Video?: VideoInstance | ||
27 | } | ||
28 | |||
29 | export 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 | |||
38 | export 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | |||
3 | import { SortType } from '../../helpers' | 2 | import { SortType } from '../../helpers' |
4 | import { addMethodsToModel, getSortOnModel } from '../utils' | 3 | import { getSortOnModel } from '../utils' |
5 | import { VideoInstance } from './video-interface' | 4 | import { VideoModel } from './video' |
6 | import { | ||
7 | BlacklistedVideoInstance, | ||
8 | BlacklistedVideoAttributes, | ||
9 | |||
10 | BlacklistedVideoMethods | ||
11 | } from './video-blacklist-interface' | ||
12 | |||
13 | let BlacklistedVideo: Sequelize.Model<BlacklistedVideoInstance, BlacklistedVideoAttributes> | ||
14 | let toFormattedJSON: BlacklistedVideoMethods.ToFormattedJSON | ||
15 | let listForApi: BlacklistedVideoMethods.ListForApi | ||
16 | let loadByVideoId: BlacklistedVideoMethods.LoadByVideoId | ||
17 | 5 | ||
18 | export 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 | 15 | export 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 | ||
47 | toFormattedJSON = 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 | ||
70 | function 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 | ||
80 | listForApi = 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 | ||
96 | loadByVideoId = 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 @@ | |||
1 | import * as Promise from 'bluebird' | ||
2 | import * as Sequelize from 'sequelize' | ||
3 | |||
4 | import { ResultList } from '../../../shared' | ||
5 | import { VideoChannelObject } from '../../../shared/models/activitypub/objects/video-channel-object' | ||
6 | import { VideoChannel as FormattedVideoChannel } from '../../../shared/models/videos/video-channel.model' | ||
7 | import { AccountInstance } from '../account/account-interface' | ||
8 | import { VideoInstance } from './video-interface' | ||
9 | import { VideoChannelShareInstance } from './video-channel-share-interface' | ||
10 | |||
11 | export 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 | |||
29 | export 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 | |||
41 | export 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 | |||
54 | export 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 | |||
64 | export 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 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | import * as Sequelize from 'sequelize' | ||
3 | import { AccountInstance } from '../account/account-interface' | ||
4 | import { VideoChannelInstance } from './video-channel-interface' | ||
5 | |||
6 | export 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 | |||
11 | export interface VideoChannelShareClass { | ||
12 | loadAccountsByShare: VideoChannelShareMethods.LoadAccountsByShare | ||
13 | load: VideoChannelShareMethods.Load | ||
14 | } | ||
15 | |||
16 | export interface VideoChannelShareAttributes { | ||
17 | accountId: number | ||
18 | videoChannelId: number | ||
19 | } | ||
20 | |||
21 | export 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 | |||
31 | export 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
3 | import { AccountModel } from '../account/account' | ||
4 | import { VideoChannelModel } from './video-channel' | ||
2 | 5 | ||
3 | import { addMethodsToModel } from '../utils' | 6 | @Table({ |
4 | import { VideoChannelShareAttributes, VideoChannelShareInstance, VideoChannelShareMethods } from './video-channel-share-interface' | 7 | tableName: 'videoChannelShare', |
5 | 8 | indexes: [ | |
6 | let VideoChannelShare: Sequelize.Model<VideoChannelShareInstance, VideoChannelShareAttributes> | ||
7 | let loadAccountsByShare: VideoChannelShareMethods.LoadAccountsByShare | ||
8 | let load: VideoChannelShareMethods.Load | ||
9 | |||
10 | export 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 | }) |
17 | export 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 | ||
37 | function 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 | |||
55 | load = 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 | ||
69 | loadAccountsByShare = 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../../helpers' | 2 | import { |
3 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' | 3 | AfterDestroy, |
4 | import { sendDeleteVideoChannel } from '../../lib/activitypub/send/send-delete' | 4 | AllowNull, |
5 | 5 | BelongsTo, | |
6 | import { addMethodsToModel, getSort } from '../utils' | 6 | Column, |
7 | import { VideoChannelAttributes, VideoChannelInstance, VideoChannelMethods } from './video-channel-interface' | 7 | CreatedAt, |
8 | import { getAnnounceActivityPubUrl } from '../../lib/activitypub/url' | 8 | DataType, |
9 | import { activityPubCollection } from '../../helpers/activitypub' | 9 | Default, |
10 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 10 | ForeignKey, |
11 | 11 | HasMany, | |
12 | let VideoChannel: Sequelize.Model<VideoChannelInstance, VideoChannelAttributes> | 12 | Is, |
13 | let toFormattedJSON: VideoChannelMethods.ToFormattedJSON | 13 | IsUUID, |
14 | let toActivityPubObject: VideoChannelMethods.ToActivityPubObject | 14 | Model, |
15 | let isOwned: VideoChannelMethods.IsOwned | 15 | Table, |
16 | let countByAccount: VideoChannelMethods.CountByAccount | 16 | UpdatedAt |
17 | let listForApi: VideoChannelMethods.ListForApi | 17 | } from 'sequelize-typescript' |
18 | let listByAccount: VideoChannelMethods.ListByAccount | 18 | import { IFindOptions } from 'sequelize-typescript/lib/interfaces/IFindOptions' |
19 | let loadByIdAndAccount: VideoChannelMethods.LoadByIdAndAccount | 19 | import { activityPubCollection } from '../../helpers' |
20 | let loadByUUID: VideoChannelMethods.LoadByUUID | 20 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub' |
21 | let loadAndPopulateAccount: VideoChannelMethods.LoadAndPopulateAccount | 21 | import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../../helpers/custom-validators/video-channels' |
22 | let loadByUUIDAndPopulateAccount: VideoChannelMethods.LoadByUUIDAndPopulateAccount | 22 | import { CONSTRAINTS_FIELDS } from '../../initializers' |
23 | let loadByHostAndUUID: VideoChannelMethods.LoadByHostAndUUID | 23 | import { getAnnounceActivityPubUrl } from '../../lib/activitypub' |
24 | let loadAndPopulateAccountAndVideos: VideoChannelMethods.LoadAndPopulateAccountAndVideos | 24 | import { sendDeleteVideoChannel } from '../../lib/activitypub/send' |
25 | let loadByUrl: VideoChannelMethods.LoadByUrl | 25 | import { AccountModel } from '../account/account' |
26 | let loadByUUIDOrUrl: VideoChannelMethods.LoadByUUIDOrUrl | 26 | import { ServerModel } from '../server/server' |
27 | 27 | import { getSort, throwIfNotValid } from '../utils' | |
28 | export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { | 28 | import { VideoModel } from './video' |
29 | VideoChannel = sequelize.define<VideoChannelInstance, VideoChannelAttributes>('VideoChannel', | 29 | import { 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, | 39 | export 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 | ||
114 | isOwned = 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 | |
118 | toFormattedJSON = 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 | ||
143 | toActivityPubObject = 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 | ||
172 | function 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 | ||
190 | function 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 | ||
198 | countByAccount = 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 | ||
208 | listForApi = 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 | ||
227 | listByAccount = 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 | ||
247 | loadByUUID = 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 | ||
259 | loadByUrl = 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 | ||
272 | loadByUUIDOrUrl = 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 | ||
287 | loadByHostAndUUID = 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 | ||
313 | loadByIdAndAccount = 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 | ||
330 | loadAndPopulateAccount = 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 | ||
343 | loadByUUIDAndPopulateAccount = 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 | ||
359 | loadAndPopulateAccountAndVideos = 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | export namespace VideoFileMethods { | ||
4 | } | ||
5 | |||
6 | export interface VideoFileClass { | ||
7 | } | ||
8 | |||
9 | export interface VideoFileAttributes { | ||
10 | resolution: number | ||
11 | size: number | ||
12 | infoHash?: string | ||
13 | extname: string | ||
14 | |||
15 | videoId?: number | ||
16 | } | ||
17 | |||
18 | export interface VideoFileInstance extends VideoFileClass, VideoFileAttributes, Sequelize.Instance<VideoFileAttributes> { | ||
19 | id: number | ||
20 | createdAt: Date | ||
21 | updatedAt: Date | ||
22 | } | ||
23 | |||
24 | export 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 @@ | |||
1 | import { values } from 'lodash' | 1 | import { values } from 'lodash' |
2 | import * as Sequelize from 'sequelize' | 2 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' |
3 | import { isVideoFileInfoHashValid, isVideoFileResolutionValid, isVideoFileSizeValid } from '../../helpers/custom-validators/videos' | 3 | import { isVideoFileInfoHashValid, isVideoFileResolutionValid, isVideoFileSizeValid } from '../../helpers/custom-validators/videos' |
4 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' | 4 | import { CONSTRAINTS_FIELDS } from '../../initializers' |
5 | import { throwIfNotValid } from '../utils' | ||
6 | import { VideoModel } from './video' | ||
5 | 7 | ||
6 | import { addMethodsToModel } from '../utils' | 8 | @Table({ |
7 | import { VideoFileAttributes, VideoFileInstance } from './video-file-interface' | 9 | tableName: 'videoFile', |
8 | 10 | indexes: [ | |
9 | let VideoFile: Sequelize.Model<VideoFileInstance, VideoFileAttributes> | ||
10 | |||
11 | export 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 | 19 | export class VideoFileModel extends Model<VideoFileModel> { | |
66 | return VideoFile | 20 | @CreatedAt |
67 | } | 21 | createdAt: Date |
68 | 22 | ||
69 | // ------------------------------ STATICS ------------------------------ | 23 | @UpdatedAt |
70 | 24 | updatedAt: Date | |
71 | function 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 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | import * as Sequelize from 'sequelize' | ||
3 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects/video-torrent-object' | ||
4 | import { ResultList } from '../../../shared/models/result-list.model' | ||
5 | import { Video as FormattedVideo, VideoDetails as FormattedDetailsVideo } from '../../../shared/models/videos/video.model' | ||
6 | import { AccountVideoRateInstance } from '../account/account-video-rate-interface' | ||
7 | |||
8 | import { TagAttributes, TagInstance } from './tag-interface' | ||
9 | import { VideoChannelInstance } from './video-channel-interface' | ||
10 | import { VideoFileAttributes, VideoFileInstance } from './video-file-interface' | ||
11 | import { VideoShareInstance } from './video-share-interface' | ||
12 | |||
13 | export 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 | |||
71 | export 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 | |||
85 | export 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 | |||
115 | export 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 | |||
150 | export 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 @@ | |||
1 | import * as Bluebird from 'bluebird' | ||
2 | import * as Sequelize from 'sequelize' | ||
3 | import { AccountInstance } from '../account/account-interface' | ||
4 | import { VideoInstance } from './video-interface' | ||
5 | |||
6 | export 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 | |||
11 | export interface VideoShareClass { | ||
12 | loadAccountsByShare: VideoShareMethods.LoadAccountsByShare | ||
13 | load: VideoShareMethods.Load | ||
14 | } | ||
15 | |||
16 | export interface VideoShareAttributes { | ||
17 | accountId: number | ||
18 | videoId: number | ||
19 | } | ||
20 | |||
21 | export 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 | |||
30 | export 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
3 | import { AccountModel } from '../account/account' | ||
4 | import { VideoModel } from './video' | ||
2 | 5 | ||
3 | import { addMethodsToModel } from '../utils' | 6 | @Table({ |
4 | import { VideoShareAttributes, VideoShareInstance, VideoShareMethods } from './video-share-interface' | 7 | tableName: 'videoShare', |
5 | 8 | indexes: [ | |
6 | let VideoShare: Sequelize.Model<VideoShareInstance, VideoShareAttributes> | ||
7 | let loadAccountsByShare: VideoShareMethods.LoadAccountsByShare | ||
8 | let load: VideoShareMethods.Load | ||
9 | |||
10 | export 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 | }) |
17 | export 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 | ||
37 | function 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 | |||
55 | load = 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 | ||
68 | loadAccountsByShare = 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | export namespace VideoTagMethods { | ||
4 | } | ||
5 | |||
6 | export interface VideoTagClass { | ||
7 | } | ||
8 | |||
9 | export interface VideoTagAttributes { | ||
10 | } | ||
11 | |||
12 | export interface VideoTagInstance extends VideoTagClass, VideoTagAttributes, Sequelize.Instance<VideoTagAttributes> { | ||
13 | id: number | ||
14 | createdAt: Date | ||
15 | updatedAt: Date | ||
16 | } | ||
17 | |||
18 | export 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import { Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | import { TagModel } from './tag' | ||
3 | import { VideoModel } from './video' | ||
2 | 4 | ||
3 | import { | 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 | }) | ||
16 | export class VideoTagModel extends Model<VideoTagModel> { | ||
17 | @CreatedAt | ||
18 | createdAt: Date | ||
7 | 19 | ||
8 | let VideoTag: Sequelize.Model<VideoTagInstance, VideoTagAttributes> | 20 | @UpdatedAt |
21 | updatedAt: Date | ||
9 | 22 | ||
10 | export 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' | |||
4 | import * as parseTorrent from 'parse-torrent' | 4 | import * as parseTorrent from 'parse-torrent' |
5 | import { join } from 'path' | 5 | import { join } from 'path' |
6 | import * as Sequelize from 'sequelize' | 6 | import * as Sequelize from 'sequelize' |
7 | import { | ||
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' | ||
27 | import { IIncludeOptions } from 'sequelize-typescript/lib/interfaces/IIncludeOptions' | ||
7 | import { VideoPrivacy, VideoResolution } from '../../../shared' | 28 | import { VideoPrivacy, VideoResolution } from '../../../shared' |
8 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects/video-torrent-object' | 29 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' |
9 | import { activityPubCollection } from '../../helpers/activitypub' | 30 | import { |
10 | import { createTorrentPromise, renamePromise, statPromise, unlinkPromise, writeFilePromise } from '../../helpers/core-utils' | 31 | activityPubCollection, |
11 | import { isVideoCategoryValid, isVideoLanguageValid, isVideoPrivacyValid } from '../../helpers/custom-validators/videos' | 32 | createTorrentPromise, |
12 | import { 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' | ||
42 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub' | ||
13 | import { | 43 | import { |
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 |
21 | import { logger } from '../../helpers/logger' | 52 | } from '../../helpers/custom-validators/videos' |
22 | import { | 53 | import { |
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' |
35 | import { getAnnounceActivityPubUrl } from '../../lib/activitypub/url' | 66 | import { getAnnounceActivityPubUrl } from '../../lib/activitypub' |
36 | import { sendDeleteVideo } from '../../lib/index' | 67 | import { sendDeleteVideo } from '../../lib/index' |
37 | import { addMethodsToModel, getSort } from '../utils' | 68 | import { AccountModel } from '../account/account' |
38 | import { TagInstance } from './tag-interface' | 69 | import { AccountVideoRateModel } from '../account/account-video-rate' |
39 | import { VideoFileInstance, VideoFileModel } from './video-file-interface' | 70 | import { ServerModel } from '../server/server' |
40 | import { VideoAttributes, VideoInstance, VideoMethods } from './video-interface' | 71 | import { getSort, throwIfNotValid } from '../utils' |
41 | 72 | import { TagModel } from './tag' | |
42 | let Video: Sequelize.Model<VideoInstance, VideoAttributes> | 73 | import { VideoAbuseModel } from './video-abuse' |
43 | let getOriginalFile: VideoMethods.GetOriginalFile | 74 | import { VideoChannelModel } from './video-channel' |
44 | let getVideoFilename: VideoMethods.GetVideoFilename | 75 | import { VideoFileModel } from './video-file' |
45 | let getThumbnailName: VideoMethods.GetThumbnailName | 76 | import { VideoShareModel } from './video-share' |
46 | let getThumbnailPath: VideoMethods.GetThumbnailPath | 77 | import { VideoTagModel } from './video-tag' |
47 | let getPreviewName: VideoMethods.GetPreviewName | 78 | |
48 | let getPreviewPath: VideoMethods.GetPreviewPath | 79 | @Table({ |
49 | let getTorrentFileName: VideoMethods.GetTorrentFileName | 80 | tableName: 'video', |
50 | let isOwned: VideoMethods.IsOwned | 81 | indexes: [ |
51 | let toFormattedJSON: VideoMethods.ToFormattedJSON | ||
52 | let toFormattedDetailsJSON: VideoMethods.ToFormattedDetailsJSON | ||
53 | let toActivityPubObject: VideoMethods.ToActivityPubObject | ||
54 | let optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile | ||
55 | let transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile | ||
56 | let createPreview: VideoMethods.CreatePreview | ||
57 | let createThumbnail: VideoMethods.CreateThumbnail | ||
58 | let getVideoFilePath: VideoMethods.GetVideoFilePath | ||
59 | let createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash | ||
60 | let getOriginalFileHeight: VideoMethods.GetOriginalFileHeight | ||
61 | let getEmbedPath: VideoMethods.GetEmbedPath | ||
62 | let getDescriptionPath: VideoMethods.GetDescriptionPath | ||
63 | let getTruncatedDescription: VideoMethods.GetTruncatedDescription | ||
64 | let getCategoryLabel: VideoMethods.GetCategoryLabel | ||
65 | let getLicenceLabel: VideoMethods.GetLicenceLabel | ||
66 | let getLanguageLabel: VideoMethods.GetLanguageLabel | ||
67 | |||
68 | let list: VideoMethods.List | ||
69 | let listForApi: VideoMethods.ListForApi | ||
70 | let listAllAndSharedByAccountForOutbox: VideoMethods.ListAllAndSharedByAccountForOutbox | ||
71 | let listUserVideosForApi: VideoMethods.ListUserVideosForApi | ||
72 | let load: VideoMethods.Load | ||
73 | let loadByUrlAndPopulateAccount: VideoMethods.LoadByUrlAndPopulateAccount | ||
74 | let loadByUUID: VideoMethods.LoadByUUID | ||
75 | let loadByUUIDOrURL: VideoMethods.LoadByUUIDOrURL | ||
76 | let loadAndPopulateAccountAndServerAndTags: VideoMethods.LoadAndPopulateAccountAndServerAndTags | ||
77 | let loadByUUIDAndPopulateAccountAndServerAndTags: VideoMethods.LoadByUUIDAndPopulateAccountAndServerAndTags | ||
78 | let searchAndPopulateAccountAndServerAndTags: VideoMethods.SearchAndPopulateAccountAndServerAndTags | ||
79 | let removeThumbnail: VideoMethods.RemoveThumbnail | ||
80 | let removePreview: VideoMethods.RemovePreview | ||
81 | let removeFile: VideoMethods.RemoveFile | ||
82 | let removeTorrent: VideoMethods.RemoveTorrent | ||
83 | |||
84 | export 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 | 105 | export 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) | |
304 | function 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 | |||
352 | function 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 | |||
378 | getOriginalFile = 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 | ||
385 | getVideoFilename = 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 | ||
389 | getThumbnailName = 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 | ||
395 | getPreviewName = function (this: VideoInstance) { | 280 | return VideoModel.findAll(query) |
396 | const extension = '.jpg' | 281 | } |
397 | return this.uuid + extension | ||
398 | } | ||
399 | 282 | ||
400 | getTorrentFileName = 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 | ||
405 | isOwned = function (this: VideoInstance) { | 292 | return `(${queryVideo}) UNION (${queryVideoShare})` |
406 | return this.remote === false | 293 | } |
407 | } | ||
408 | 294 | ||
409 | createPreview = 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 | ||
420 | createThumbnail = 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 | ||
431 | getVideoFilePath = 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 | ||
435 | createTorrentAndSetInfoHash = 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 | ||
456 | getEmbedPath = 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 | ||
460 | getThumbnailPath = function (this: VideoInstance) { | 463 | if (t !== undefined) query.transaction = t |
461 | return join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName()) | ||
462 | } | ||
463 | 464 | ||
464 | getPreviewPath = function (this: VideoInstance) { | 465 | return VideoModel.findOne(query) |
465 | return join(STATIC_PATHS.PREVIEWS, this.getPreviewName()) | 466 | } |
466 | } | ||
467 | 467 | ||
468 | toFormattedJSON = 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 | ||
508 | toFormattedDetailsJSON = 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 | |||
550 | toActivityPubObject = 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 | ||
679 | getTruncatedDescription = 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 | ||
689 | optimizeOriginalVideofile = 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 | ||
726 | transcodeOriginalVideofile = 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 | ||
760 | getOriginalFileHeight = 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 | ||
766 | getDescriptionPath = 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 | ||
770 | getCategoryLabel = 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 | ||
777 | getLicenceLabel = 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 | ||
784 | getLanguageLabel = 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 | ||
791 | removeThumbnail = 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 | ||
796 | removePreview = 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 | ||
801 | removeFile = 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 | ||
806 | removeTorrent = 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 | ||
813 | list = 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 | ||
821 | listAllAndSharedByAccountForOutbox = 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 | ||
903 | listUserVideosForApi = 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 | ||
935 | listForApi = 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 | ||
971 | load = 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 | ||
975 | loadByUUID = 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 | ||
988 | loadByUrlAndPopulateAccount = 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 | ||
1007 | loadByUUIDOrURL = 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 | ||
1023 | loadAndPopulateAccountAndServerAndTags = 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 | ||
1052 | loadByUUIDAndPopulateAccountAndServerAndTags = 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 | ||
1084 | searchAndPopulateAccountAndServerAndTags = 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 | ||
1147 | function 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 | ||
1158 | function 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 | } | |
1173 | function getThumbnailUrl (video: VideoInstance, baseUrlHttp: string) { | ||
1174 | return baseUrlHttp + STATIC_PATHS.THUMBNAILS + video.getThumbnailName() | ||
1175 | } | ||
1176 | 1066 | ||
1177 | function 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 | ||
1181 | function 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 | ||
1185 | function 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 | } |