diff options
Diffstat (limited to 'server/models/account')
-rw-r--r-- | server/models/account/account-follow-interface.ts | 23 | ||||
-rw-r--r-- | server/models/account/account-follow.ts | 56 | ||||
-rw-r--r-- | server/models/account/account-interface.ts | 74 | ||||
-rw-r--r-- | server/models/account/account-video-rate-interface.ts | 26 | ||||
-rw-r--r-- | server/models/account/account-video-rate.ts | 78 | ||||
-rw-r--r-- | server/models/account/account.ts | 444 | ||||
-rw-r--r-- | server/models/account/index.ts | 4 | ||||
-rw-r--r-- | server/models/account/user-interface.ts | 69 | ||||
-rw-r--r-- | server/models/account/user.ts | 311 |
9 files changed, 1085 insertions, 0 deletions
diff --git a/server/models/account/account-follow-interface.ts b/server/models/account/account-follow-interface.ts new file mode 100644 index 000000000..3be383649 --- /dev/null +++ b/server/models/account/account-follow-interface.ts | |||
@@ -0,0 +1,23 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | import * as Promise from 'bluebird' | ||
3 | |||
4 | import { VideoRateType } from '../../../shared/models/videos/video-rate.type' | ||
5 | |||
6 | export namespace AccountFollowMethods { | ||
7 | } | ||
8 | |||
9 | export interface AccountFollowClass { | ||
10 | } | ||
11 | |||
12 | export interface AccountFollowAttributes { | ||
13 | accountId: number | ||
14 | targetAccountId: number | ||
15 | } | ||
16 | |||
17 | export interface AccountFollowInstance extends AccountFollowClass, AccountFollowAttributes, Sequelize.Instance<AccountFollowAttributes> { | ||
18 | id: number | ||
19 | createdAt: Date | ||
20 | updatedAt: Date | ||
21 | } | ||
22 | |||
23 | 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 new file mode 100644 index 000000000..9bf03b253 --- /dev/null +++ b/server/models/account/account-follow.ts | |||
@@ -0,0 +1,56 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | import { addMethodsToModel } from '../utils' | ||
4 | import { | ||
5 | AccountFollowInstance, | ||
6 | AccountFollowAttributes, | ||
7 | |||
8 | AccountFollowMethods | ||
9 | } from './account-follow-interface' | ||
10 | |||
11 | let AccountFollow: Sequelize.Model<AccountFollowInstance, AccountFollowAttributes> | ||
12 | |||
13 | export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { | ||
14 | AccountFollow = sequelize.define<AccountFollowInstance, AccountFollowAttributes>('AccountFollow', | ||
15 | { }, | ||
16 | { | ||
17 | indexes: [ | ||
18 | { | ||
19 | fields: [ 'accountId' ], | ||
20 | unique: true | ||
21 | }, | ||
22 | { | ||
23 | fields: [ 'targetAccountId' ], | ||
24 | unique: true | ||
25 | } | ||
26 | ] | ||
27 | } | ||
28 | ) | ||
29 | |||
30 | const classMethods = [ | ||
31 | associate | ||
32 | ] | ||
33 | addMethodsToModel(AccountFollow, classMethods) | ||
34 | |||
35 | return AccountFollow | ||
36 | } | ||
37 | |||
38 | // ------------------------------ STATICS ------------------------------ | ||
39 | |||
40 | function associate (models) { | ||
41 | AccountFollow.belongsTo(models.Account, { | ||
42 | foreignKey: { | ||
43 | name: 'accountId', | ||
44 | allowNull: false | ||
45 | }, | ||
46 | onDelete: 'CASCADE' | ||
47 | }) | ||
48 | |||
49 | AccountFollow.belongsTo(models.Account, { | ||
50 | foreignKey: { | ||
51 | name: 'targetAccountId', | ||
52 | allowNull: false | ||
53 | }, | ||
54 | onDelete: 'CASCADE' | ||
55 | }) | ||
56 | } | ||
diff --git a/server/models/account/account-interface.ts b/server/models/account/account-interface.ts new file mode 100644 index 000000000..2ef3e2246 --- /dev/null +++ b/server/models/account/account-interface.ts | |||
@@ -0,0 +1,74 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | import * as Bluebird from 'bluebird' | ||
3 | |||
4 | import { PodInstance } from '../pod/pod-interface' | ||
5 | import { VideoChannelInstance } from '../video/video-channel-interface' | ||
6 | import { ActivityPubActor } from '../../../shared' | ||
7 | import { ResultList } from '../../../shared/models/result-list.model' | ||
8 | |||
9 | export namespace AccountMethods { | ||
10 | export type Load = (id: number) => Bluebird<AccountInstance> | ||
11 | export type LoadByUUID = (uuid: string) => Bluebird<AccountInstance> | ||
12 | export type LoadByUrl = (url: string) => Bluebird<AccountInstance> | ||
13 | export type LoadAccountByPodAndUUID = (uuid: string, podId: number, transaction: Sequelize.Transaction) => Bluebird<AccountInstance> | ||
14 | export type LoadLocalAccountByName = (name: string) => Bluebird<AccountInstance> | ||
15 | export type ListOwned = () => Bluebird<AccountInstance[]> | ||
16 | export type ListFollowerUrlsForApi = (name: string, start: number, count: number) => Promise< ResultList<string> > | ||
17 | export type ListFollowingUrlsForApi = (name: string, start: number, count: number) => Promise< ResultList<string> > | ||
18 | |||
19 | export type ToActivityPubObject = (this: AccountInstance) => ActivityPubActor | ||
20 | export type IsOwned = (this: AccountInstance) => boolean | ||
21 | export type GetFollowerSharedInboxUrls = (this: AccountInstance) => 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 | loadAccountByPodAndUUID: AccountMethods.LoadAccountByPodAndUUID | ||
29 | load: AccountMethods.Load | ||
30 | loadByUUID: AccountMethods.LoadByUUID | ||
31 | loadByUrl: AccountMethods.LoadByUrl | ||
32 | loadLocalAccountByName: AccountMethods.LoadLocalAccountByName | ||
33 | listOwned: AccountMethods.ListOwned | ||
34 | listFollowerUrlsForApi: AccountMethods.ListFollowerUrlsForApi | ||
35 | listFollowingUrlsForApi: AccountMethods.ListFollowingUrlsForApi | ||
36 | } | ||
37 | |||
38 | export interface AccountAttributes { | ||
39 | name: string | ||
40 | url: string | ||
41 | publicKey: string | ||
42 | privateKey: string | ||
43 | followersCount: number | ||
44 | followingCount: number | ||
45 | inboxUrl: string | ||
46 | outboxUrl: string | ||
47 | sharedInboxUrl: string | ||
48 | followersUrl: string | ||
49 | followingUrl: string | ||
50 | |||
51 | uuid?: string | ||
52 | |||
53 | podId?: number | ||
54 | userId?: number | ||
55 | applicationId?: number | ||
56 | } | ||
57 | |||
58 | export interface AccountInstance extends AccountClass, AccountAttributes, Sequelize.Instance<AccountAttributes> { | ||
59 | isOwned: AccountMethods.IsOwned | ||
60 | toActivityPubObject: AccountMethods.ToActivityPubObject | ||
61 | getFollowerSharedInboxUrls: AccountMethods.GetFollowerSharedInboxUrls | ||
62 | getFollowingUrl: AccountMethods.GetFollowingUrl | ||
63 | getFollowersUrl: AccountMethods.GetFollowersUrl | ||
64 | getPublicKeyUrl: AccountMethods.GetPublicKeyUrl | ||
65 | |||
66 | id: number | ||
67 | createdAt: Date | ||
68 | updatedAt: Date | ||
69 | |||
70 | Pod: PodInstance | ||
71 | VideoChannels: VideoChannelInstance[] | ||
72 | } | ||
73 | |||
74 | 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 new file mode 100644 index 000000000..82cbe38cc --- /dev/null +++ b/server/models/account/account-video-rate-interface.ts | |||
@@ -0,0 +1,26 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | import * as Promise from 'bluebird' | ||
3 | |||
4 | import { VideoRateType } from '../../../shared/models/videos/video-rate.type' | ||
5 | |||
6 | export namespace AccountVideoRateMethods { | ||
7 | export type Load = (accountId: number, videoId: number, transaction: Sequelize.Transaction) => Promise<AccountVideoRateInstance> | ||
8 | } | ||
9 | |||
10 | export interface AccountVideoRateClass { | ||
11 | load: AccountVideoRateMethods.Load | ||
12 | } | ||
13 | |||
14 | export interface AccountVideoRateAttributes { | ||
15 | type: VideoRateType | ||
16 | accountId: number | ||
17 | videoId: number | ||
18 | } | ||
19 | |||
20 | export interface AccountVideoRateInstance extends AccountVideoRateClass, AccountVideoRateAttributes, Sequelize.Instance<AccountVideoRateAttributes> { | ||
21 | id: number | ||
22 | createdAt: Date | ||
23 | updatedAt: Date | ||
24 | } | ||
25 | |||
26 | export interface AccountVideoRateModel extends AccountVideoRateClass, Sequelize.Model<AccountVideoRateInstance, AccountVideoRateAttributes> {} | ||
diff --git a/server/models/account/account-video-rate.ts b/server/models/account/account-video-rate.ts new file mode 100644 index 000000000..7f7c97606 --- /dev/null +++ b/server/models/account/account-video-rate.ts | |||
@@ -0,0 +1,78 @@ | |||
1 | /* | ||
2 | Account rates per video. | ||
3 | */ | ||
4 | import { values } from 'lodash' | ||
5 | import * as Sequelize from 'sequelize' | ||
6 | |||
7 | import { VIDEO_RATE_TYPES } from '../../initializers' | ||
8 | |||
9 | import { addMethodsToModel } from '../utils' | ||
10 | import { | ||
11 | AccountVideoRateInstance, | ||
12 | AccountVideoRateAttributes, | ||
13 | |||
14 | AccountVideoRateMethods | ||
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 | { | ||
29 | indexes: [ | ||
30 | { | ||
31 | fields: [ 'videoId', 'accountId', 'type' ], | ||
32 | unique: true | ||
33 | } | ||
34 | ] | ||
35 | } | ||
36 | ) | ||
37 | |||
38 | const classMethods = [ | ||
39 | associate, | ||
40 | |||
41 | load | ||
42 | ] | ||
43 | addMethodsToModel(AccountVideoRate, classMethods) | ||
44 | |||
45 | return AccountVideoRate | ||
46 | } | ||
47 | |||
48 | // ------------------------------ STATICS ------------------------------ | ||
49 | |||
50 | function associate (models) { | ||
51 | AccountVideoRate.belongsTo(models.Video, { | ||
52 | foreignKey: { | ||
53 | name: 'videoId', | ||
54 | allowNull: false | ||
55 | }, | ||
56 | onDelete: 'CASCADE' | ||
57 | }) | ||
58 | |||
59 | AccountVideoRate.belongsTo(models.Account, { | ||
60 | foreignKey: { | ||
61 | name: 'accountId', | ||
62 | allowNull: false | ||
63 | }, | ||
64 | onDelete: 'CASCADE' | ||
65 | }) | ||
66 | } | ||
67 | |||
68 | load = function (accountId: number, videoId: number, transaction: Sequelize.Transaction) { | ||
69 | const options: Sequelize.FindOptions<AccountVideoRateAttributes> = { | ||
70 | where: { | ||
71 | accountId, | ||
72 | videoId | ||
73 | } | ||
74 | } | ||
75 | if (transaction) options.transaction = transaction | ||
76 | |||
77 | return AccountVideoRate.findOne(options) | ||
78 | } | ||
diff --git a/server/models/account/account.ts b/server/models/account/account.ts new file mode 100644 index 000000000..00c0aefd4 --- /dev/null +++ b/server/models/account/account.ts | |||
@@ -0,0 +1,444 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | import { | ||
4 | isUserUsernameValid, | ||
5 | isAccountPublicKeyValid, | ||
6 | isAccountUrlValid, | ||
7 | isAccountPrivateKeyValid, | ||
8 | isAccountFollowersCountValid, | ||
9 | isAccountFollowingCountValid, | ||
10 | isAccountInboxValid, | ||
11 | isAccountOutboxValid, | ||
12 | isAccountSharedInboxValid, | ||
13 | isAccountFollowersValid, | ||
14 | isAccountFollowingValid, | ||
15 | activityPubContextify | ||
16 | } from '../../helpers' | ||
17 | |||
18 | import { addMethodsToModel } from '../utils' | ||
19 | import { | ||
20 | AccountInstance, | ||
21 | AccountAttributes, | ||
22 | |||
23 | AccountMethods | ||
24 | } from './account-interface' | ||
25 | |||
26 | let Account: Sequelize.Model<AccountInstance, AccountAttributes> | ||
27 | let loadAccountByPodAndUUID: AccountMethods.LoadAccountByPodAndUUID | ||
28 | let load: AccountMethods.Load | ||
29 | let loadByUUID: AccountMethods.LoadByUUID | ||
30 | let loadByUrl: AccountMethods.LoadByUrl | ||
31 | let loadLocalAccountByName: AccountMethods.LoadLocalAccountByName | ||
32 | let listOwned: AccountMethods.ListOwned | ||
33 | let listFollowerUrlsForApi: AccountMethods.ListFollowerUrlsForApi | ||
34 | let listFollowingUrlsForApi: AccountMethods.ListFollowingUrlsForApi | ||
35 | let isOwned: AccountMethods.IsOwned | ||
36 | let toActivityPubObject: AccountMethods.ToActivityPubObject | ||
37 | let getFollowerSharedInboxUrls: AccountMethods.GetFollowerSharedInboxUrls | ||
38 | let getFollowingUrl: AccountMethods.GetFollowingUrl | ||
39 | let getFollowersUrl: AccountMethods.GetFollowersUrl | ||
40 | let getPublicKeyUrl: AccountMethods.GetPublicKeyUrl | ||
41 | |||
42 | export default function defineAccount (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { | ||
43 | Account = sequelize.define<AccountInstance, AccountAttributes>('Account', | ||
44 | { | ||
45 | uuid: { | ||
46 | type: DataTypes.UUID, | ||
47 | defaultValue: DataTypes.UUIDV4, | ||
48 | allowNull: false, | ||
49 | validate: { | ||
50 | isUUID: 4 | ||
51 | } | ||
52 | }, | ||
53 | name: { | ||
54 | type: DataTypes.STRING, | ||
55 | allowNull: false, | ||
56 | validate: { | ||
57 | usernameValid: value => { | ||
58 | const res = isUserUsernameValid(value) | ||
59 | if (res === false) throw new Error('Username is not valid.') | ||
60 | } | ||
61 | } | ||
62 | }, | ||
63 | url: { | ||
64 | type: DataTypes.STRING, | ||
65 | allowNull: false, | ||
66 | validate: { | ||
67 | urlValid: value => { | ||
68 | const res = isAccountUrlValid(value) | ||
69 | if (res === false) throw new Error('URL is not valid.') | ||
70 | } | ||
71 | } | ||
72 | }, | ||
73 | publicKey: { | ||
74 | type: DataTypes.STRING, | ||
75 | allowNull: false, | ||
76 | validate: { | ||
77 | publicKeyValid: value => { | ||
78 | const res = isAccountPublicKeyValid(value) | ||
79 | if (res === false) throw new Error('Public key is not valid.') | ||
80 | } | ||
81 | } | ||
82 | }, | ||
83 | privateKey: { | ||
84 | type: DataTypes.STRING, | ||
85 | allowNull: false, | ||
86 | validate: { | ||
87 | privateKeyValid: value => { | ||
88 | const res = isAccountPrivateKeyValid(value) | ||
89 | if (res === false) throw new Error('Private key is not valid.') | ||
90 | } | ||
91 | } | ||
92 | }, | ||
93 | followersCount: { | ||
94 | type: DataTypes.INTEGER, | ||
95 | allowNull: false, | ||
96 | validate: { | ||
97 | followersCountValid: value => { | ||
98 | const res = isAccountFollowersCountValid(value) | ||
99 | if (res === false) throw new Error('Followers count is not valid.') | ||
100 | } | ||
101 | } | ||
102 | }, | ||
103 | followingCount: { | ||
104 | type: DataTypes.INTEGER, | ||
105 | allowNull: false, | ||
106 | validate: { | ||
107 | followersCountValid: value => { | ||
108 | const res = isAccountFollowingCountValid(value) | ||
109 | if (res === false) throw new Error('Following count is not valid.') | ||
110 | } | ||
111 | } | ||
112 | }, | ||
113 | inboxUrl: { | ||
114 | type: DataTypes.STRING, | ||
115 | allowNull: false, | ||
116 | validate: { | ||
117 | inboxUrlValid: value => { | ||
118 | const res = isAccountInboxValid(value) | ||
119 | if (res === false) throw new Error('Inbox URL is not valid.') | ||
120 | } | ||
121 | } | ||
122 | }, | ||
123 | outboxUrl: { | ||
124 | type: DataTypes.STRING, | ||
125 | allowNull: false, | ||
126 | validate: { | ||
127 | outboxUrlValid: value => { | ||
128 | const res = isAccountOutboxValid(value) | ||
129 | if (res === false) throw new Error('Outbox URL is not valid.') | ||
130 | } | ||
131 | } | ||
132 | }, | ||
133 | sharedInboxUrl: { | ||
134 | type: DataTypes.STRING, | ||
135 | allowNull: false, | ||
136 | validate: { | ||
137 | sharedInboxUrlValid: value => { | ||
138 | const res = isAccountSharedInboxValid(value) | ||
139 | if (res === false) throw new Error('Shared inbox URL is not valid.') | ||
140 | } | ||
141 | } | ||
142 | }, | ||
143 | followersUrl: { | ||
144 | type: DataTypes.STRING, | ||
145 | allowNull: false, | ||
146 | validate: { | ||
147 | followersUrlValid: value => { | ||
148 | const res = isAccountFollowersValid(value) | ||
149 | if (res === false) throw new Error('Followers URL is not valid.') | ||
150 | } | ||
151 | } | ||
152 | }, | ||
153 | followingUrl: { | ||
154 | type: DataTypes.STRING, | ||
155 | allowNull: false, | ||
156 | validate: { | ||
157 | followingUrlValid: value => { | ||
158 | const res = isAccountFollowingValid(value) | ||
159 | if (res === false) throw new Error('Following URL is not valid.') | ||
160 | } | ||
161 | } | ||
162 | } | ||
163 | }, | ||
164 | { | ||
165 | indexes: [ | ||
166 | { | ||
167 | fields: [ 'name' ] | ||
168 | }, | ||
169 | { | ||
170 | fields: [ 'podId' ] | ||
171 | }, | ||
172 | { | ||
173 | fields: [ 'userId' ], | ||
174 | unique: true | ||
175 | }, | ||
176 | { | ||
177 | fields: [ 'applicationId' ], | ||
178 | unique: true | ||
179 | }, | ||
180 | { | ||
181 | fields: [ 'name', 'podId' ], | ||
182 | unique: true | ||
183 | } | ||
184 | ], | ||
185 | hooks: { afterDestroy } | ||
186 | } | ||
187 | ) | ||
188 | |||
189 | const classMethods = [ | ||
190 | associate, | ||
191 | loadAccountByPodAndUUID, | ||
192 | load, | ||
193 | loadByUUID, | ||
194 | loadLocalAccountByName, | ||
195 | listOwned, | ||
196 | listFollowerUrlsForApi, | ||
197 | listFollowingUrlsForApi | ||
198 | ] | ||
199 | const instanceMethods = [ | ||
200 | isOwned, | ||
201 | toActivityPubObject, | ||
202 | getFollowerSharedInboxUrls, | ||
203 | getFollowingUrl, | ||
204 | getFollowersUrl, | ||
205 | getPublicKeyUrl | ||
206 | ] | ||
207 | addMethodsToModel(Account, classMethods, instanceMethods) | ||
208 | |||
209 | return Account | ||
210 | } | ||
211 | |||
212 | // --------------------------------------------------------------------------- | ||
213 | |||
214 | function associate (models) { | ||
215 | Account.belongsTo(models.Pod, { | ||
216 | foreignKey: { | ||
217 | name: 'podId', | ||
218 | allowNull: true | ||
219 | }, | ||
220 | onDelete: 'cascade' | ||
221 | }) | ||
222 | |||
223 | Account.belongsTo(models.User, { | ||
224 | foreignKey: { | ||
225 | name: 'userId', | ||
226 | allowNull: true | ||
227 | }, | ||
228 | onDelete: 'cascade' | ||
229 | }) | ||
230 | |||
231 | Account.belongsTo(models.Application, { | ||
232 | foreignKey: { | ||
233 | name: 'userId', | ||
234 | allowNull: true | ||
235 | }, | ||
236 | onDelete: 'cascade' | ||
237 | }) | ||
238 | |||
239 | Account.hasMany(models.VideoChannel, { | ||
240 | foreignKey: { | ||
241 | name: 'accountId', | ||
242 | allowNull: false | ||
243 | }, | ||
244 | onDelete: 'cascade', | ||
245 | hooks: true | ||
246 | }) | ||
247 | |||
248 | Account.hasMany(models.AccountFollower, { | ||
249 | foreignKey: { | ||
250 | name: 'accountId', | ||
251 | allowNull: false | ||
252 | }, | ||
253 | onDelete: 'cascade' | ||
254 | }) | ||
255 | |||
256 | Account.hasMany(models.AccountFollower, { | ||
257 | foreignKey: { | ||
258 | name: 'targetAccountId', | ||
259 | allowNull: false | ||
260 | }, | ||
261 | onDelete: 'cascade' | ||
262 | }) | ||
263 | } | ||
264 | |||
265 | function afterDestroy (account: AccountInstance) { | ||
266 | if (account.isOwned()) { | ||
267 | const removeVideoAccountToFriendsParams = { | ||
268 | uuid: account.uuid | ||
269 | } | ||
270 | |||
271 | return removeVideoAccountToFriends(removeVideoAccountToFriendsParams) | ||
272 | } | ||
273 | |||
274 | return undefined | ||
275 | } | ||
276 | |||
277 | toActivityPubObject = function (this: AccountInstance) { | ||
278 | const type = this.podId ? 'Application' : 'Person' | ||
279 | |||
280 | const json = { | ||
281 | type, | ||
282 | id: this.url, | ||
283 | following: this.getFollowingUrl(), | ||
284 | followers: this.getFollowersUrl(), | ||
285 | inbox: this.inboxUrl, | ||
286 | outbox: this.outboxUrl, | ||
287 | preferredUsername: this.name, | ||
288 | url: this.url, | ||
289 | name: this.name, | ||
290 | endpoints: { | ||
291 | sharedInbox: this.sharedInboxUrl | ||
292 | }, | ||
293 | uuid: this.uuid, | ||
294 | publicKey: { | ||
295 | id: this.getPublicKeyUrl(), | ||
296 | owner: this.url, | ||
297 | publicKeyPem: this.publicKey | ||
298 | } | ||
299 | } | ||
300 | |||
301 | return activityPubContextify(json) | ||
302 | } | ||
303 | |||
304 | isOwned = function (this: AccountInstance) { | ||
305 | return this.podId === null | ||
306 | } | ||
307 | |||
308 | getFollowerSharedInboxUrls = function (this: AccountInstance) { | ||
309 | const query: Sequelize.FindOptions<AccountAttributes> = { | ||
310 | attributes: [ 'sharedInboxUrl' ], | ||
311 | include: [ | ||
312 | { | ||
313 | model: Account['sequelize'].models.AccountFollower, | ||
314 | where: { | ||
315 | targetAccountId: this.id | ||
316 | } | ||
317 | } | ||
318 | ] | ||
319 | } | ||
320 | |||
321 | return Account.findAll(query) | ||
322 | .then(accounts => accounts.map(a => a.sharedInboxUrl)) | ||
323 | } | ||
324 | |||
325 | getFollowingUrl = function (this: AccountInstance) { | ||
326 | return this.url + '/followers' | ||
327 | } | ||
328 | |||
329 | getFollowersUrl = function (this: AccountInstance) { | ||
330 | return this.url + '/followers' | ||
331 | } | ||
332 | |||
333 | getPublicKeyUrl = function (this: AccountInstance) { | ||
334 | return this.url + '#main-key' | ||
335 | } | ||
336 | |||
337 | // ------------------------------ STATICS ------------------------------ | ||
338 | |||
339 | listOwned = function () { | ||
340 | const query: Sequelize.FindOptions<AccountAttributes> = { | ||
341 | where: { | ||
342 | podId: null | ||
343 | } | ||
344 | } | ||
345 | |||
346 | return Account.findAll(query) | ||
347 | } | ||
348 | |||
349 | listFollowerUrlsForApi = function (name: string, start: number, count: number) { | ||
350 | return createListFollowForApiQuery('followers', name, start, count) | ||
351 | } | ||
352 | |||
353 | listFollowingUrlsForApi = function (name: string, start: number, count: number) { | ||
354 | return createListFollowForApiQuery('following', name, start, count) | ||
355 | } | ||
356 | |||
357 | load = function (id: number) { | ||
358 | return Account.findById(id) | ||
359 | } | ||
360 | |||
361 | loadByUUID = function (uuid: string) { | ||
362 | const query: Sequelize.FindOptions<AccountAttributes> = { | ||
363 | where: { | ||
364 | uuid | ||
365 | } | ||
366 | } | ||
367 | |||
368 | return Account.findOne(query) | ||
369 | } | ||
370 | |||
371 | loadLocalAccountByName = function (name: string) { | ||
372 | const query: Sequelize.FindOptions<AccountAttributes> = { | ||
373 | where: { | ||
374 | name, | ||
375 | userId: { | ||
376 | [Sequelize.Op.ne]: null | ||
377 | } | ||
378 | } | ||
379 | } | ||
380 | |||
381 | return Account.findOne(query) | ||
382 | } | ||
383 | |||
384 | loadByUrl = function (url: string) { | ||
385 | const query: Sequelize.FindOptions<AccountAttributes> = { | ||
386 | where: { | ||
387 | url | ||
388 | } | ||
389 | } | ||
390 | |||
391 | return Account.findOne(query) | ||
392 | } | ||
393 | |||
394 | loadAccountByPodAndUUID = function (uuid: string, podId: number, transaction: Sequelize.Transaction) { | ||
395 | const query: Sequelize.FindOptions<AccountAttributes> = { | ||
396 | where: { | ||
397 | podId, | ||
398 | uuid | ||
399 | }, | ||
400 | transaction | ||
401 | } | ||
402 | |||
403 | return Account.find(query) | ||
404 | } | ||
405 | |||
406 | // ------------------------------ UTILS ------------------------------ | ||
407 | |||
408 | async function createListFollowForApiQuery (type: 'followers' | 'following', name: string, start: number, count: number) { | ||
409 | let firstJoin: string | ||
410 | let secondJoin: string | ||
411 | |||
412 | if (type === 'followers') { | ||
413 | firstJoin = 'targetAccountId' | ||
414 | secondJoin = 'accountId' | ||
415 | } else { | ||
416 | firstJoin = 'accountId' | ||
417 | secondJoin = 'targetAccountId' | ||
418 | } | ||
419 | |||
420 | const selections = [ '"Followers"."url" AS "url"', 'COUNT(*) AS "total"' ] | ||
421 | const tasks: Promise<any>[] = [] | ||
422 | |||
423 | for (const selection of selections) { | ||
424 | const query = 'SELECT ' + selection + ' FROM "Account" ' + | ||
425 | 'INNER JOIN "AccountFollower" ON "AccountFollower"."' + firstJoin + '" = "Account"."id" ' + | ||
426 | 'INNER JOIN "Account" AS "Followers" ON "Followers"."id" = "AccountFollower"."' + secondJoin + '" ' + | ||
427 | 'WHERE "Account"."name" = \'$name\' ' + | ||
428 | 'LIMIT ' + start + ', ' + count | ||
429 | |||
430 | const options = { | ||
431 | bind: { name }, | ||
432 | type: Sequelize.QueryTypes.SELECT | ||
433 | } | ||
434 | tasks.push(Account['sequelize'].query(query, options)) | ||
435 | } | ||
436 | |||
437 | const [ followers, [ { total } ]] = await Promise.all(tasks) | ||
438 | const urls: string[] = followers.map(f => f.url) | ||
439 | |||
440 | return { | ||
441 | data: urls, | ||
442 | total: parseInt(total, 10) | ||
443 | } | ||
444 | } | ||
diff --git a/server/models/account/index.ts b/server/models/account/index.ts new file mode 100644 index 000000000..179f66974 --- /dev/null +++ b/server/models/account/index.ts | |||
@@ -0,0 +1,4 @@ | |||
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 new file mode 100644 index 000000000..1a04fb750 --- /dev/null +++ b/server/models/account/user-interface.ts | |||
@@ -0,0 +1,69 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | import * as Bluebird from 'bluebird' | ||
3 | |||
4 | // Don't use barrel, import just what we need | ||
5 | import { AccountInstance } from './account-interface' | ||
6 | import { User as FormattedUser } from '../../../shared/models/users/user.model' | ||
7 | import { ResultList } from '../../../shared/models/result-list.model' | ||
8 | import { UserRight } from '../../../shared/models/users/user-right.enum' | ||
9 | import { UserRole } from '../../../shared/models/users/user-role' | ||
10 | |||
11 | export namespace UserMethods { | ||
12 | export type HasRight = (this: UserInstance, right: UserRight) => boolean | ||
13 | export type IsPasswordMatch = (this: UserInstance, password: string) => Promise<boolean> | ||
14 | |||
15 | export type ToFormattedJSON = (this: UserInstance) => FormattedUser | ||
16 | export type IsAbleToUploadVideo = (this: UserInstance, videoFile: Express.Multer.File) => Promise<boolean> | ||
17 | |||
18 | export type CountTotal = () => Bluebird<number> | ||
19 | |||
20 | export type GetByUsername = (username: string) => Bluebird<UserInstance> | ||
21 | |||
22 | export type ListForApi = (start: number, count: number, sort: string) => Bluebird< ResultList<UserInstance> > | ||
23 | |||
24 | export type LoadById = (id: number) => Bluebird<UserInstance> | ||
25 | |||
26 | export type LoadByUsername = (username: string) => Bluebird<UserInstance> | ||
27 | export type LoadByUsernameAndPopulateChannels = (username: string) => Bluebird<UserInstance> | ||
28 | |||
29 | export type LoadByUsernameOrEmail = (username: string, email: string) => Bluebird<UserInstance> | ||
30 | } | ||
31 | |||
32 | export interface UserClass { | ||
33 | isPasswordMatch: UserMethods.IsPasswordMatch, | ||
34 | toFormattedJSON: UserMethods.ToFormattedJSON, | ||
35 | hasRight: UserMethods.HasRight, | ||
36 | isAbleToUploadVideo: UserMethods.IsAbleToUploadVideo, | ||
37 | |||
38 | countTotal: UserMethods.CountTotal, | ||
39 | getByUsername: UserMethods.GetByUsername, | ||
40 | listForApi: UserMethods.ListForApi, | ||
41 | loadById: UserMethods.LoadById, | ||
42 | loadByUsername: UserMethods.LoadByUsername, | ||
43 | loadByUsernameAndPopulateChannels: UserMethods.LoadByUsernameAndPopulateChannels, | ||
44 | loadByUsernameOrEmail: UserMethods.LoadByUsernameOrEmail | ||
45 | } | ||
46 | |||
47 | export interface UserAttributes { | ||
48 | id?: number | ||
49 | password: string | ||
50 | username: string | ||
51 | email: string | ||
52 | displayNSFW?: boolean | ||
53 | role: UserRole | ||
54 | videoQuota: number | ||
55 | |||
56 | Account?: AccountInstance | ||
57 | } | ||
58 | |||
59 | export interface UserInstance extends UserClass, UserAttributes, Sequelize.Instance<UserAttributes> { | ||
60 | id: number | ||
61 | createdAt: Date | ||
62 | updatedAt: Date | ||
63 | |||
64 | isPasswordMatch: UserMethods.IsPasswordMatch | ||
65 | toFormattedJSON: UserMethods.ToFormattedJSON | ||
66 | hasRight: UserMethods.HasRight | ||
67 | } | ||
68 | |||
69 | export interface UserModel extends UserClass, Sequelize.Model<UserInstance, UserAttributes> {} | ||
diff --git a/server/models/account/user.ts b/server/models/account/user.ts new file mode 100644 index 000000000..1401762c5 --- /dev/null +++ b/server/models/account/user.ts | |||
@@ -0,0 +1,311 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | import { getSort, addMethodsToModel } from '../utils' | ||
4 | import { | ||
5 | cryptPassword, | ||
6 | comparePassword, | ||
7 | isUserPasswordValid, | ||
8 | isUserUsernameValid, | ||
9 | isUserDisplayNSFWValid, | ||
10 | isUserVideoQuotaValid, | ||
11 | isUserRoleValid | ||
12 | } from '../../helpers' | ||
13 | import { UserRight, USER_ROLE_LABELS, hasUserRight } from '../../../shared' | ||
14 | |||
15 | import { | ||
16 | UserInstance, | ||
17 | UserAttributes, | ||
18 | |||
19 | UserMethods | ||
20 | } from './user-interface' | ||
21 | |||
22 | let User: Sequelize.Model<UserInstance, UserAttributes> | ||
23 | let isPasswordMatch: UserMethods.IsPasswordMatch | ||
24 | let hasRight: UserMethods.HasRight | ||
25 | let toFormattedJSON: UserMethods.ToFormattedJSON | ||
26 | let countTotal: UserMethods.CountTotal | ||
27 | let getByUsername: UserMethods.GetByUsername | ||
28 | let listForApi: UserMethods.ListForApi | ||
29 | let loadById: UserMethods.LoadById | ||
30 | let loadByUsername: UserMethods.LoadByUsername | ||
31 | let loadByUsernameAndPopulateChannels: UserMethods.LoadByUsernameAndPopulateChannels | ||
32 | let loadByUsernameOrEmail: UserMethods.LoadByUsernameOrEmail | ||
33 | let isAbleToUploadVideo: UserMethods.IsAbleToUploadVideo | ||
34 | |||
35 | export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { | ||
36 | User = sequelize.define<UserInstance, UserAttributes>('User', | ||
37 | { | ||
38 | password: { | ||
39 | type: DataTypes.STRING, | ||
40 | allowNull: false, | ||
41 | validate: { | ||
42 | passwordValid: value => { | ||
43 | const res = isUserPasswordValid(value) | ||
44 | if (res === false) throw new Error('Password not valid.') | ||
45 | } | ||
46 | } | ||
47 | }, | ||
48 | username: { | ||
49 | type: DataTypes.STRING, | ||
50 | allowNull: false, | ||
51 | validate: { | ||
52 | usernameValid: value => { | ||
53 | const res = isUserUsernameValid(value) | ||
54 | if (res === false) throw new Error('Username not valid.') | ||
55 | } | ||
56 | } | ||
57 | }, | ||
58 | email: { | ||
59 | type: DataTypes.STRING(400), | ||
60 | allowNull: false, | ||
61 | validate: { | ||
62 | isEmail: true | ||
63 | } | ||
64 | }, | ||
65 | displayNSFW: { | ||
66 | type: DataTypes.BOOLEAN, | ||
67 | allowNull: false, | ||
68 | defaultValue: false, | ||
69 | validate: { | ||
70 | nsfwValid: value => { | ||
71 | const res = isUserDisplayNSFWValid(value) | ||
72 | if (res === false) throw new Error('Display NSFW is not valid.') | ||
73 | } | ||
74 | } | ||
75 | }, | ||
76 | role: { | ||
77 | type: DataTypes.INTEGER, | ||
78 | allowNull: false, | ||
79 | validate: { | ||
80 | roleValid: value => { | ||
81 | const res = isUserRoleValid(value) | ||
82 | if (res === false) throw new Error('Role is not valid.') | ||
83 | } | ||
84 | } | ||
85 | }, | ||
86 | videoQuota: { | ||
87 | type: DataTypes.BIGINT, | ||
88 | allowNull: false, | ||
89 | validate: { | ||
90 | videoQuotaValid: value => { | ||
91 | const res = isUserVideoQuotaValid(value) | ||
92 | if (res === false) throw new Error('Video quota is not valid.') | ||
93 | } | ||
94 | } | ||
95 | } | ||
96 | }, | ||
97 | { | ||
98 | indexes: [ | ||
99 | { | ||
100 | fields: [ 'username' ], | ||
101 | unique: true | ||
102 | }, | ||
103 | { | ||
104 | fields: [ 'email' ], | ||
105 | unique: true | ||
106 | } | ||
107 | ], | ||
108 | hooks: { | ||
109 | beforeCreate: beforeCreateOrUpdate, | ||
110 | beforeUpdate: beforeCreateOrUpdate | ||
111 | } | ||
112 | } | ||
113 | ) | ||
114 | |||
115 | const classMethods = [ | ||
116 | associate, | ||
117 | |||
118 | countTotal, | ||
119 | getByUsername, | ||
120 | listForApi, | ||
121 | loadById, | ||
122 | loadByUsername, | ||
123 | loadByUsernameAndPopulateChannels, | ||
124 | loadByUsernameOrEmail | ||
125 | ] | ||
126 | const instanceMethods = [ | ||
127 | hasRight, | ||
128 | isPasswordMatch, | ||
129 | toFormattedJSON, | ||
130 | isAbleToUploadVideo | ||
131 | ] | ||
132 | addMethodsToModel(User, classMethods, instanceMethods) | ||
133 | |||
134 | return User | ||
135 | } | ||
136 | |||
137 | function beforeCreateOrUpdate (user: UserInstance) { | ||
138 | if (user.changed('password')) { | ||
139 | return cryptPassword(user.password) | ||
140 | .then(hash => { | ||
141 | user.password = hash | ||
142 | return undefined | ||
143 | }) | ||
144 | } | ||
145 | } | ||
146 | |||
147 | // ------------------------------ METHODS ------------------------------ | ||
148 | |||
149 | hasRight = function (this: UserInstance, right: UserRight) { | ||
150 | return hasUserRight(this.role, right) | ||
151 | } | ||
152 | |||
153 | isPasswordMatch = function (this: UserInstance, password: string) { | ||
154 | return comparePassword(password, this.password) | ||
155 | } | ||
156 | |||
157 | toFormattedJSON = function (this: UserInstance) { | ||
158 | const json = { | ||
159 | id: this.id, | ||
160 | username: this.username, | ||
161 | email: this.email, | ||
162 | displayNSFW: this.displayNSFW, | ||
163 | role: this.role, | ||
164 | roleLabel: USER_ROLE_LABELS[this.role], | ||
165 | videoQuota: this.videoQuota, | ||
166 | createdAt: this.createdAt, | ||
167 | author: { | ||
168 | id: this.Account.id, | ||
169 | uuid: this.Account.uuid | ||
170 | } | ||
171 | } | ||
172 | |||
173 | if (Array.isArray(this.Account.VideoChannels) === true) { | ||
174 | const videoChannels = this.Account.VideoChannels | ||
175 | .map(c => c.toFormattedJSON()) | ||
176 | .sort((v1, v2) => { | ||
177 | if (v1.createdAt < v2.createdAt) return -1 | ||
178 | if (v1.createdAt === v2.createdAt) return 0 | ||
179 | |||
180 | return 1 | ||
181 | }) | ||
182 | |||
183 | json['videoChannels'] = videoChannels | ||
184 | } | ||
185 | |||
186 | return json | ||
187 | } | ||
188 | |||
189 | isAbleToUploadVideo = function (this: UserInstance, videoFile: Express.Multer.File) { | ||
190 | if (this.videoQuota === -1) return Promise.resolve(true) | ||
191 | |||
192 | return getOriginalVideoFileTotalFromUser(this).then(totalBytes => { | ||
193 | return (videoFile.size + totalBytes) < this.videoQuota | ||
194 | }) | ||
195 | } | ||
196 | |||
197 | // ------------------------------ STATICS ------------------------------ | ||
198 | |||
199 | function associate (models) { | ||
200 | User.hasOne(models.Account, { | ||
201 | foreignKey: 'userId', | ||
202 | onDelete: 'cascade' | ||
203 | }) | ||
204 | |||
205 | User.hasMany(models.OAuthToken, { | ||
206 | foreignKey: 'userId', | ||
207 | onDelete: 'cascade' | ||
208 | }) | ||
209 | } | ||
210 | |||
211 | countTotal = function () { | ||
212 | return this.count() | ||
213 | } | ||
214 | |||
215 | getByUsername = function (username: string) { | ||
216 | const query = { | ||
217 | where: { | ||
218 | username: username | ||
219 | }, | ||
220 | include: [ { model: User['sequelize'].models.Account, required: true } ] | ||
221 | } | ||
222 | |||
223 | return User.findOne(query) | ||
224 | } | ||
225 | |||
226 | listForApi = function (start: number, count: number, sort: string) { | ||
227 | const query = { | ||
228 | offset: start, | ||
229 | limit: count, | ||
230 | order: [ getSort(sort) ], | ||
231 | include: [ { model: User['sequelize'].models.Account, required: true } ] | ||
232 | } | ||
233 | |||
234 | return User.findAndCountAll(query).then(({ rows, count }) => { | ||
235 | return { | ||
236 | data: rows, | ||
237 | total: count | ||
238 | } | ||
239 | }) | ||
240 | } | ||
241 | |||
242 | loadById = function (id: number) { | ||
243 | const options = { | ||
244 | include: [ { model: User['sequelize'].models.Account, required: true } ] | ||
245 | } | ||
246 | |||
247 | return User.findById(id, options) | ||
248 | } | ||
249 | |||
250 | loadByUsername = function (username: string) { | ||
251 | const query = { | ||
252 | where: { | ||
253 | username | ||
254 | }, | ||
255 | include: [ { model: User['sequelize'].models.Account, required: true } ] | ||
256 | } | ||
257 | |||
258 | return User.findOne(query) | ||
259 | } | ||
260 | |||
261 | loadByUsernameAndPopulateChannels = function (username: string) { | ||
262 | const query = { | ||
263 | where: { | ||
264 | username | ||
265 | }, | ||
266 | include: [ | ||
267 | { | ||
268 | model: User['sequelize'].models.Account, | ||
269 | required: true, | ||
270 | include: [ User['sequelize'].models.VideoChannel ] | ||
271 | } | ||
272 | ] | ||
273 | } | ||
274 | |||
275 | return User.findOne(query) | ||
276 | } | ||
277 | |||
278 | loadByUsernameOrEmail = function (username: string, email: string) { | ||
279 | const query = { | ||
280 | include: [ { model: User['sequelize'].models.Account, required: true } ], | ||
281 | where: { | ||
282 | [Sequelize.Op.or]: [ { username }, { email } ] | ||
283 | } | ||
284 | } | ||
285 | |||
286 | // FIXME: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18387 | ||
287 | return (User as any).findOne(query) | ||
288 | } | ||
289 | |||
290 | // --------------------------------------------------------------------------- | ||
291 | |||
292 | function getOriginalVideoFileTotalFromUser (user: UserInstance) { | ||
293 | // Don't use sequelize because we need to use a sub query | ||
294 | const query = 'SELECT SUM("size") AS "total" FROM ' + | ||
295 | '(SELECT MAX("VideoFiles"."size") AS "size" FROM "VideoFiles" ' + | ||
296 | 'INNER JOIN "Videos" ON "VideoFiles"."videoId" = "Videos"."id" ' + | ||
297 | 'INNER JOIN "VideoChannels" ON "VideoChannels"."id" = "Videos"."channelId" ' + | ||
298 | 'INNER JOIN "Accounts" ON "VideoChannels"."authorId" = "Accounts"."id" ' + | ||
299 | 'INNER JOIN "Users" ON "Accounts"."userId" = "Users"."id" ' + | ||
300 | 'WHERE "Users"."id" = $userId GROUP BY "Videos"."id") t' | ||
301 | |||
302 | const options = { | ||
303 | bind: { userId: user.id }, | ||
304 | type: Sequelize.QueryTypes.SELECT | ||
305 | } | ||
306 | return User['sequelize'].query(query, options).then(([ { total } ]) => { | ||
307 | if (total === null) return 0 | ||
308 | |||
309 | return parseInt(total, 10) | ||
310 | }) | ||
311 | } | ||