]>
Commit | Line | Data |
---|---|---|
2295ce6c | 1 | import { join } from 'path' |
e4f97bab | 2 | import * as Sequelize from 'sequelize' |
2295ce6c | 3 | import { Avatar } from '../../../shared/models/avatars/avatar.model' |
e4f97bab | 4 | import { |
79d5caf9 | 5 | activityPubContextify, |
e4f97bab C |
6 | isAccountFollowersCountValid, |
7 | isAccountFollowingCountValid, | |
79d5caf9 C |
8 | isAccountPrivateKeyValid, |
9 | isAccountPublicKeyValid, | |
79d5caf9 | 10 | isUserUsernameValid |
e4f97bab | 11 | } from '../../helpers' |
a2431b7d | 12 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
2295ce6c | 13 | import { AVATARS_DIR } from '../../initializers' |
51548b31 | 14 | import { CONFIG, CONSTRAINTS_FIELDS } from '../../initializers/constants' |
54141398 | 15 | import { sendDeleteAccount } from '../../lib/activitypub/send/send-delete' |
79d5caf9 C |
16 | import { addMethodsToModel } from '../utils' |
17 | import { AccountAttributes, AccountInstance, AccountMethods } from './account-interface' | |
e4f97bab C |
18 | |
19 | let Account: Sequelize.Model<AccountInstance, AccountAttributes> | |
e4f97bab | 20 | let load: AccountMethods.Load |
7a7724e6 | 21 | let loadApplication: AccountMethods.LoadApplication |
e4f97bab C |
22 | let loadByUUID: AccountMethods.LoadByUUID |
23 | let loadByUrl: AccountMethods.LoadByUrl | |
350e31d6 C |
24 | let loadLocalByName: AccountMethods.LoadLocalByName |
25 | let loadByNameAndHost: AccountMethods.LoadByNameAndHost | |
63c93323 | 26 | let listByFollowersUrls: AccountMethods.ListByFollowersUrls |
e4f97bab C |
27 | let isOwned: AccountMethods.IsOwned |
28 | let toActivityPubObject: AccountMethods.ToActivityPubObject | |
7a7724e6 | 29 | let toFormattedJSON: AccountMethods.ToFormattedJSON |
e4f97bab C |
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 | { | |
38 | uuid: { | |
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: { | |
e34c85e5 | 50 | nameValid: value => { |
e4f97bab | 51 | const res = isUserUsernameValid(value) |
e34c85e5 | 52 | if (res === false) throw new Error('Name is not valid.') |
e4f97bab C |
53 | } |
54 | } | |
55 | }, | |
56 | url: { | |
e34c85e5 | 57 | type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max), |
e4f97bab C |
58 | allowNull: false, |
59 | validate: { | |
60 | urlValid: value => { | |
a2431b7d | 61 | const res = isActivityPubUrlValid(value) |
e4f97bab C |
62 | if (res === false) throw new Error('URL is not valid.') |
63 | } | |
64 | } | |
65 | }, | |
66 | publicKey: { | |
e34c85e5 | 67 | type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.PUBLIC_KEY.max), |
47e0652b | 68 | allowNull: true, |
e4f97bab C |
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: { | |
e34c85e5 | 77 | type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.PRIVATE_KEY.max), |
350e31d6 | 78 | allowNull: true, |
e4f97bab C |
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: { | |
e34c85e5 | 100 | followingCountValid: value => { |
e4f97bab C |
101 | const res = isAccountFollowingCountValid(value) |
102 | if (res === false) throw new Error('Following count is not valid.') | |
103 | } | |
104 | } | |
105 | }, | |
106 | inboxUrl: { | |
e34c85e5 | 107 | type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max), |
e4f97bab C |
108 | allowNull: false, |
109 | validate: { | |
110 | inboxUrlValid: value => { | |
a2431b7d | 111 | const res = isActivityPubUrlValid(value) |
e4f97bab C |
112 | if (res === false) throw new Error('Inbox URL is not valid.') |
113 | } | |
114 | } | |
115 | }, | |
116 | outboxUrl: { | |
e34c85e5 | 117 | type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max), |
e4f97bab C |
118 | allowNull: false, |
119 | validate: { | |
120 | outboxUrlValid: value => { | |
a2431b7d | 121 | const res = isActivityPubUrlValid(value) |
e4f97bab C |
122 | if (res === false) throw new Error('Outbox URL is not valid.') |
123 | } | |
124 | } | |
125 | }, | |
126 | sharedInboxUrl: { | |
e34c85e5 | 127 | type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max), |
e4f97bab C |
128 | allowNull: false, |
129 | validate: { | |
130 | sharedInboxUrlValid: value => { | |
a2431b7d | 131 | const res = isActivityPubUrlValid(value) |
e4f97bab C |
132 | if (res === false) throw new Error('Shared inbox URL is not valid.') |
133 | } | |
134 | } | |
135 | }, | |
136 | followersUrl: { | |
e34c85e5 | 137 | type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max), |
e4f97bab C |
138 | allowNull: false, |
139 | validate: { | |
140 | followersUrlValid: value => { | |
a2431b7d | 141 | const res = isActivityPubUrlValid(value) |
e4f97bab C |
142 | if (res === false) throw new Error('Followers URL is not valid.') |
143 | } | |
144 | } | |
145 | }, | |
146 | followingUrl: { | |
e34c85e5 | 147 | type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max), |
e4f97bab C |
148 | allowNull: false, |
149 | validate: { | |
150 | followingUrlValid: value => { | |
a2431b7d | 151 | const res = isActivityPubUrlValid(value) |
e4f97bab C |
152 | if (res === false) throw new Error('Following URL is not valid.') |
153 | } | |
154 | } | |
155 | } | |
156 | }, | |
157 | { | |
158 | indexes: [ | |
159 | { | |
160 | fields: [ 'name' ] | |
161 | }, | |
162 | { | |
60862425 | 163 | fields: [ 'serverId' ] |
e4f97bab C |
164 | }, |
165 | { | |
166 | fields: [ 'userId' ], | |
167 | unique: true | |
168 | }, | |
169 | { | |
170 | fields: [ 'applicationId' ], | |
171 | unique: true | |
172 | }, | |
173 | { | |
60862425 | 174 | fields: [ 'name', 'serverId', 'applicationId' ], |
e4f97bab C |
175 | unique: true |
176 | } | |
177 | ], | |
178 | hooks: { afterDestroy } | |
179 | } | |
180 | ) | |
181 | ||
182 | const classMethods = [ | |
183 | associate, | |
7a7724e6 | 184 | loadApplication, |
e4f97bab C |
185 | load, |
186 | loadByUUID, | |
ce548a10 | 187 | loadByUrl, |
350e31d6 | 188 | loadLocalByName, |
63c93323 C |
189 | loadByNameAndHost, |
190 | listByFollowersUrls | |
e4f97bab C |
191 | ] |
192 | const instanceMethods = [ | |
193 | isOwned, | |
194 | toActivityPubObject, | |
7a7724e6 | 195 | toFormattedJSON, |
e4f97bab C |
196 | getFollowerSharedInboxUrls, |
197 | getFollowingUrl, | |
198 | getFollowersUrl, | |
199 | getPublicKeyUrl | |
200 | ] | |
201 | addMethodsToModel(Account, classMethods, instanceMethods) | |
202 | ||
203 | return Account | |
204 | } | |
205 | ||
206 | // --------------------------------------------------------------------------- | |
207 | ||
208 | function associate (models) { | |
60862425 | 209 | Account.belongsTo(models.Server, { |
e4f97bab | 210 | foreignKey: { |
60862425 | 211 | name: 'serverId', |
e4f97bab C |
212 | allowNull: true |
213 | }, | |
214 | onDelete: 'cascade' | |
215 | }) | |
216 | ||
217 | Account.belongsTo(models.User, { | |
218 | foreignKey: { | |
219 | name: 'userId', | |
220 | allowNull: true | |
221 | }, | |
222 | onDelete: 'cascade' | |
223 | }) | |
224 | ||
225 | Account.belongsTo(models.Application, { | |
226 | foreignKey: { | |
e34c85e5 | 227 | name: 'applicationId', |
e4f97bab C |
228 | allowNull: true |
229 | }, | |
230 | onDelete: 'cascade' | |
231 | }) | |
232 | ||
233 | Account.hasMany(models.VideoChannel, { | |
234 | foreignKey: { | |
235 | name: 'accountId', | |
236 | allowNull: false | |
237 | }, | |
238 | onDelete: 'cascade', | |
239 | hooks: true | |
240 | }) | |
241 | ||
e34c85e5 | 242 | Account.hasMany(models.AccountFollow, { |
e4f97bab C |
243 | foreignKey: { |
244 | name: 'accountId', | |
245 | allowNull: false | |
246 | }, | |
247 | onDelete: 'cascade' | |
248 | }) | |
249 | ||
e34c85e5 | 250 | Account.hasMany(models.AccountFollow, { |
e4f97bab C |
251 | foreignKey: { |
252 | name: 'targetAccountId', | |
253 | allowNull: false | |
254 | }, | |
8e10cf1a | 255 | as: 'followers', |
e4f97bab C |
256 | onDelete: 'cascade' |
257 | }) | |
2295ce6c C |
258 | |
259 | Account.hasOne(models.Avatar, { | |
260 | foreignKey: { | |
261 | name: 'avatarId', | |
262 | allowNull: true | |
263 | }, | |
264 | onDelete: 'cascade' | |
265 | }) | |
e4f97bab C |
266 | } |
267 | ||
268 | function afterDestroy (account: AccountInstance) { | |
269 | if (account.isOwned()) { | |
7a7724e6 | 270 | return sendDeleteAccount(account, undefined) |
e4f97bab C |
271 | } |
272 | ||
273 | return undefined | |
274 | } | |
275 | ||
7a7724e6 | 276 | toFormattedJSON = function (this: AccountInstance) { |
60862425 C |
277 | let host = CONFIG.WEBSERVER.HOST |
278 | let score: number | |
2295ce6c C |
279 | let avatar: Avatar = null |
280 | ||
281 | if (this.Avatar) { | |
282 | avatar = { | |
283 | path: join(AVATARS_DIR.ACCOUNT, this.Avatar.filename), | |
284 | createdAt: this.Avatar.createdAt, | |
285 | updatedAt: this.Avatar.updatedAt | |
286 | } | |
287 | } | |
60862425 C |
288 | |
289 | if (this.Server) { | |
290 | host = this.Server.host | |
291 | score = this.Server.score as number | |
292 | } | |
51548b31 | 293 | |
7a7724e6 C |
294 | const json = { |
295 | id: this.id, | |
2295ce6c | 296 | uuid: this.uuid, |
51548b31 | 297 | host, |
60862425 C |
298 | score, |
299 | name: this.name, | |
2295ce6c C |
300 | followingCount: this.followingCount, |
301 | followersCount: this.followersCount, | |
60862425 | 302 | createdAt: this.createdAt, |
2295ce6c C |
303 | updatedAt: this.updatedAt, |
304 | avatar | |
7a7724e6 C |
305 | } |
306 | ||
307 | return json | |
308 | } | |
309 | ||
e4f97bab | 310 | toActivityPubObject = function (this: AccountInstance) { |
60862425 | 311 | const type = this.serverId ? 'Application' as 'Application' : 'Person' as 'Person' |
e4f97bab C |
312 | |
313 | const json = { | |
314 | type, | |
315 | id: this.url, | |
316 | following: this.getFollowingUrl(), | |
317 | followers: this.getFollowersUrl(), | |
318 | inbox: this.inboxUrl, | |
319 | outbox: this.outboxUrl, | |
320 | preferredUsername: this.name, | |
321 | url: this.url, | |
322 | name: this.name, | |
323 | endpoints: { | |
324 | sharedInbox: this.sharedInboxUrl | |
325 | }, | |
326 | uuid: this.uuid, | |
327 | publicKey: { | |
328 | id: this.getPublicKeyUrl(), | |
329 | owner: this.url, | |
330 | publicKeyPem: this.publicKey | |
331 | } | |
332 | } | |
333 | ||
334 | return activityPubContextify(json) | |
335 | } | |
336 | ||
337 | isOwned = function (this: AccountInstance) { | |
60862425 | 338 | return this.serverId === null |
e4f97bab C |
339 | } |
340 | ||
25ed141c | 341 | getFollowerSharedInboxUrls = function (this: AccountInstance, t: Sequelize.Transaction) { |
e4f97bab C |
342 | const query: Sequelize.FindOptions<AccountAttributes> = { |
343 | attributes: [ 'sharedInboxUrl' ], | |
344 | include: [ | |
345 | { | |
e34c85e5 | 346 | model: Account['sequelize'].models.AccountFollow, |
350e31d6 C |
347 | required: true, |
348 | as: 'followers', | |
e4f97bab C |
349 | where: { |
350 | targetAccountId: this.id | |
351 | } | |
352 | } | |
25ed141c C |
353 | ], |
354 | transaction: t | |
e4f97bab C |
355 | } |
356 | ||
357 | return Account.findAll(query) | |
358 | .then(accounts => accounts.map(a => a.sharedInboxUrl)) | |
359 | } | |
360 | ||
361 | getFollowingUrl = function (this: AccountInstance) { | |
51548b31 | 362 | return this.url + '/following' |
e4f97bab C |
363 | } |
364 | ||
365 | getFollowersUrl = function (this: AccountInstance) { | |
366 | return this.url + '/followers' | |
367 | } | |
368 | ||
369 | getPublicKeyUrl = function (this: AccountInstance) { | |
370 | return this.url + '#main-key' | |
371 | } | |
372 | ||
373 | // ------------------------------ STATICS ------------------------------ | |
374 | ||
7a7724e6 C |
375 | loadApplication = function () { |
376 | return Account.findOne({ | |
377 | include: [ | |
378 | { | |
350e31d6 | 379 | model: Account['sequelize'].models.Application, |
7a7724e6 C |
380 | required: true |
381 | } | |
382 | ] | |
383 | }) | |
e4f97bab C |
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 | } | |
395 | } | |
396 | ||
397 | return Account.findOne(query) | |
398 | } | |
399 | ||
350e31d6 | 400 | loadLocalByName = function (name: string) { |
e4f97bab C |
401 | const query: Sequelize.FindOptions<AccountAttributes> = { |
402 | where: { | |
403 | name, | |
350e31d6 C |
404 | [Sequelize.Op.or]: [ |
405 | { | |
406 | userId: { | |
407 | [Sequelize.Op.ne]: null | |
408 | } | |
409 | }, | |
410 | { | |
411 | applicationId: { | |
412 | [Sequelize.Op.ne]: null | |
413 | } | |
414 | } | |
415 | ] | |
416 | } | |
417 | } | |
418 | ||
419 | return Account.findOne(query) | |
420 | } | |
421 | ||
422 | loadByNameAndHost = function (name: string, host: string) { | |
423 | const query: Sequelize.FindOptions<AccountAttributes> = { | |
424 | where: { | |
425 | name | |
7a7724e6 C |
426 | }, |
427 | include: [ | |
428 | { | |
60862425 | 429 | model: Account['sequelize'].models.Server, |
350e31d6 | 430 | required: true, |
7a7724e6 C |
431 | where: { |
432 | host | |
433 | } | |
434 | } | |
435 | ] | |
e4f97bab C |
436 | } |
437 | ||
438 | return Account.findOne(query) | |
439 | } | |
440 | ||
ce548a10 | 441 | loadByUrl = function (url: string, transaction?: Sequelize.Transaction) { |
e4f97bab C |
442 | const query: Sequelize.FindOptions<AccountAttributes> = { |
443 | where: { | |
444 | url | |
ce548a10 C |
445 | }, |
446 | transaction | |
e4f97bab C |
447 | } |
448 | ||
449 | return Account.findOne(query) | |
450 | } | |
63c93323 C |
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 | } | |
461 | ||
462 | return Account.findAll(query) | |
463 | } |