diff options
Diffstat (limited to 'server/models/account')
-rw-r--r-- | server/models/account/account-follow-interface.ts | 60 | ||||
-rw-r--r-- | server/models/account/account-follow.ts | 364 | ||||
-rw-r--r-- | server/models/account/account-interface.ts | 76 | ||||
-rw-r--r-- | server/models/account/account-video-rate-interface.ts | 31 | ||||
-rw-r--r-- | server/models/account/account-video-rate.ts | 97 | ||||
-rw-r--r-- | server/models/account/account.ts | 673 | ||||
-rw-r--r-- | server/models/account/index.ts | 4 | ||||
-rw-r--r-- | server/models/account/user-interface.ts | 67 | ||||
-rw-r--r-- | server/models/account/user.ts | 460 |
9 files changed, 728 insertions, 1104 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 | } |