]>
Commit | Line | Data |
---|---|---|
e4f97bab C |
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 | ||
7a7724e6 | 18 | import { addMethodsToModel, getSort } from '../utils' |
e4f97bab C |
19 | import { |
20 | AccountInstance, | |
21 | AccountAttributes, | |
22 | ||
23 | AccountMethods | |
24 | } from './account-interface' | |
7a7724e6 C |
25 | import LoadApplication = AccountMethods.LoadApplication |
26 | import { sendDeleteAccount } from '../../lib/activitypub/send-request' | |
e4f97bab C |
27 | |
28 | let Account: Sequelize.Model<AccountInstance, AccountAttributes> | |
29 | let loadAccountByPodAndUUID: AccountMethods.LoadAccountByPodAndUUID | |
30 | let load: AccountMethods.Load | |
7a7724e6 | 31 | let loadApplication: AccountMethods.LoadApplication |
e4f97bab C |
32 | let loadByUUID: AccountMethods.LoadByUUID |
33 | let loadByUrl: AccountMethods.LoadByUrl | |
7a7724e6 | 34 | let loadLocalAccountByNameAndPod: AccountMethods.LoadLocalAccountByNameAndPod |
e4f97bab | 35 | let listOwned: AccountMethods.ListOwned |
8e696487 C |
36 | let listAcceptedFollowerUrlsForApi: AccountMethods.ListAcceptedFollowerUrlsForApi |
37 | let listAcceptedFollowingUrlsForApi: AccountMethods.ListAcceptedFollowingUrlsForApi | |
7a7724e6 C |
38 | let listFollowingForApi: AccountMethods.ListFollowingForApi |
39 | let listFollowersForApi: AccountMethods.ListFollowersForApi | |
e4f97bab C |
40 | let isOwned: AccountMethods.IsOwned |
41 | let toActivityPubObject: AccountMethods.ToActivityPubObject | |
7a7724e6 | 42 | let toFormattedJSON: AccountMethods.ToFormattedJSON |
e4f97bab C |
43 | let getFollowerSharedInboxUrls: AccountMethods.GetFollowerSharedInboxUrls |
44 | let getFollowingUrl: AccountMethods.GetFollowingUrl | |
45 | let getFollowersUrl: AccountMethods.GetFollowersUrl | |
46 | let getPublicKeyUrl: AccountMethods.GetPublicKeyUrl | |
47 | ||
48 | export default function defineAccount (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { | |
49 | Account = sequelize.define<AccountInstance, AccountAttributes>('Account', | |
50 | { | |
51 | uuid: { | |
52 | type: DataTypes.UUID, | |
53 | defaultValue: DataTypes.UUIDV4, | |
54 | allowNull: false, | |
55 | validate: { | |
56 | isUUID: 4 | |
57 | } | |
58 | }, | |
59 | name: { | |
60 | type: DataTypes.STRING, | |
61 | allowNull: false, | |
62 | validate: { | |
63 | usernameValid: value => { | |
64 | const res = isUserUsernameValid(value) | |
65 | if (res === false) throw new Error('Username is not valid.') | |
66 | } | |
67 | } | |
68 | }, | |
69 | url: { | |
70 | type: DataTypes.STRING, | |
71 | allowNull: false, | |
72 | validate: { | |
73 | urlValid: value => { | |
74 | const res = isAccountUrlValid(value) | |
75 | if (res === false) throw new Error('URL is not valid.') | |
76 | } | |
77 | } | |
78 | }, | |
79 | publicKey: { | |
80 | type: DataTypes.STRING, | |
81 | allowNull: false, | |
82 | validate: { | |
83 | publicKeyValid: value => { | |
84 | const res = isAccountPublicKeyValid(value) | |
85 | if (res === false) throw new Error('Public key is not valid.') | |
86 | } | |
87 | } | |
88 | }, | |
89 | privateKey: { | |
90 | type: DataTypes.STRING, | |
91 | allowNull: false, | |
92 | validate: { | |
93 | privateKeyValid: value => { | |
94 | const res = isAccountPrivateKeyValid(value) | |
95 | if (res === false) throw new Error('Private key is not valid.') | |
96 | } | |
97 | } | |
98 | }, | |
99 | followersCount: { | |
100 | type: DataTypes.INTEGER, | |
101 | allowNull: false, | |
102 | validate: { | |
103 | followersCountValid: value => { | |
104 | const res = isAccountFollowersCountValid(value) | |
105 | if (res === false) throw new Error('Followers count is not valid.') | |
106 | } | |
107 | } | |
108 | }, | |
109 | followingCount: { | |
110 | type: DataTypes.INTEGER, | |
111 | allowNull: false, | |
112 | validate: { | |
113 | followersCountValid: value => { | |
114 | const res = isAccountFollowingCountValid(value) | |
115 | if (res === false) throw new Error('Following count is not valid.') | |
116 | } | |
117 | } | |
118 | }, | |
119 | inboxUrl: { | |
120 | type: DataTypes.STRING, | |
121 | allowNull: false, | |
122 | validate: { | |
123 | inboxUrlValid: value => { | |
124 | const res = isAccountInboxValid(value) | |
125 | if (res === false) throw new Error('Inbox URL is not valid.') | |
126 | } | |
127 | } | |
128 | }, | |
129 | outboxUrl: { | |
130 | type: DataTypes.STRING, | |
131 | allowNull: false, | |
132 | validate: { | |
133 | outboxUrlValid: value => { | |
134 | const res = isAccountOutboxValid(value) | |
135 | if (res === false) throw new Error('Outbox URL is not valid.') | |
136 | } | |
137 | } | |
138 | }, | |
139 | sharedInboxUrl: { | |
140 | type: DataTypes.STRING, | |
141 | allowNull: false, | |
142 | validate: { | |
143 | sharedInboxUrlValid: value => { | |
144 | const res = isAccountSharedInboxValid(value) | |
145 | if (res === false) throw new Error('Shared inbox URL is not valid.') | |
146 | } | |
147 | } | |
148 | }, | |
149 | followersUrl: { | |
150 | type: DataTypes.STRING, | |
151 | allowNull: false, | |
152 | validate: { | |
153 | followersUrlValid: value => { | |
154 | const res = isAccountFollowersValid(value) | |
155 | if (res === false) throw new Error('Followers URL is not valid.') | |
156 | } | |
157 | } | |
158 | }, | |
159 | followingUrl: { | |
160 | type: DataTypes.STRING, | |
161 | allowNull: false, | |
162 | validate: { | |
163 | followingUrlValid: value => { | |
164 | const res = isAccountFollowingValid(value) | |
165 | if (res === false) throw new Error('Following URL is not valid.') | |
166 | } | |
167 | } | |
168 | } | |
169 | }, | |
170 | { | |
171 | indexes: [ | |
172 | { | |
173 | fields: [ 'name' ] | |
174 | }, | |
175 | { | |
176 | fields: [ 'podId' ] | |
177 | }, | |
178 | { | |
179 | fields: [ 'userId' ], | |
180 | unique: true | |
181 | }, | |
182 | { | |
183 | fields: [ 'applicationId' ], | |
184 | unique: true | |
185 | }, | |
186 | { | |
187 | fields: [ 'name', 'podId' ], | |
188 | unique: true | |
189 | } | |
190 | ], | |
191 | hooks: { afterDestroy } | |
192 | } | |
193 | ) | |
194 | ||
195 | const classMethods = [ | |
196 | associate, | |
197 | loadAccountByPodAndUUID, | |
7a7724e6 | 198 | loadApplication, |
e4f97bab C |
199 | load, |
200 | loadByUUID, | |
ce548a10 | 201 | loadByUrl, |
7a7724e6 | 202 | loadLocalAccountByNameAndPod, |
e4f97bab | 203 | listOwned, |
8e696487 C |
204 | listAcceptedFollowerUrlsForApi, |
205 | listAcceptedFollowingUrlsForApi, | |
7a7724e6 C |
206 | listFollowingForApi, |
207 | listFollowersForApi | |
e4f97bab C |
208 | ] |
209 | const instanceMethods = [ | |
210 | isOwned, | |
211 | toActivityPubObject, | |
7a7724e6 | 212 | toFormattedJSON, |
e4f97bab C |
213 | getFollowerSharedInboxUrls, |
214 | getFollowingUrl, | |
215 | getFollowersUrl, | |
216 | getPublicKeyUrl | |
217 | ] | |
218 | addMethodsToModel(Account, classMethods, instanceMethods) | |
219 | ||
220 | return Account | |
221 | } | |
222 | ||
223 | // --------------------------------------------------------------------------- | |
224 | ||
225 | function associate (models) { | |
226 | Account.belongsTo(models.Pod, { | |
227 | foreignKey: { | |
228 | name: 'podId', | |
229 | allowNull: true | |
230 | }, | |
231 | onDelete: 'cascade' | |
232 | }) | |
233 | ||
234 | Account.belongsTo(models.User, { | |
235 | foreignKey: { | |
236 | name: 'userId', | |
237 | allowNull: true | |
238 | }, | |
239 | onDelete: 'cascade' | |
240 | }) | |
241 | ||
242 | Account.belongsTo(models.Application, { | |
243 | foreignKey: { | |
244 | name: 'userId', | |
245 | allowNull: true | |
246 | }, | |
247 | onDelete: 'cascade' | |
248 | }) | |
249 | ||
250 | Account.hasMany(models.VideoChannel, { | |
251 | foreignKey: { | |
252 | name: 'accountId', | |
253 | allowNull: false | |
254 | }, | |
255 | onDelete: 'cascade', | |
256 | hooks: true | |
257 | }) | |
258 | ||
259 | Account.hasMany(models.AccountFollower, { | |
260 | foreignKey: { | |
261 | name: 'accountId', | |
262 | allowNull: false | |
263 | }, | |
7a7724e6 | 264 | as: 'following', |
e4f97bab C |
265 | onDelete: 'cascade' |
266 | }) | |
267 | ||
268 | Account.hasMany(models.AccountFollower, { | |
269 | foreignKey: { | |
270 | name: 'targetAccountId', | |
271 | allowNull: false | |
272 | }, | |
7a7724e6 | 273 | as: 'followers', |
e4f97bab C |
274 | onDelete: 'cascade' |
275 | }) | |
276 | } | |
277 | ||
278 | function afterDestroy (account: AccountInstance) { | |
279 | if (account.isOwned()) { | |
7a7724e6 | 280 | return sendDeleteAccount(account, undefined) |
e4f97bab C |
281 | } |
282 | ||
283 | return undefined | |
284 | } | |
285 | ||
7a7724e6 C |
286 | toFormattedJSON = function (this: AccountInstance) { |
287 | const json = { | |
288 | id: this.id, | |
289 | host: this.Pod.host, | |
290 | name: this.name | |
291 | } | |
292 | ||
293 | return json | |
294 | } | |
295 | ||
e4f97bab | 296 | toActivityPubObject = function (this: AccountInstance) { |
571389d4 | 297 | const type = this.podId ? 'Application' as 'Application' : 'Person' as 'Person' |
e4f97bab C |
298 | |
299 | const json = { | |
300 | type, | |
301 | id: this.url, | |
302 | following: this.getFollowingUrl(), | |
303 | followers: this.getFollowersUrl(), | |
304 | inbox: this.inboxUrl, | |
305 | outbox: this.outboxUrl, | |
306 | preferredUsername: this.name, | |
307 | url: this.url, | |
308 | name: this.name, | |
309 | endpoints: { | |
310 | sharedInbox: this.sharedInboxUrl | |
311 | }, | |
312 | uuid: this.uuid, | |
313 | publicKey: { | |
314 | id: this.getPublicKeyUrl(), | |
315 | owner: this.url, | |
316 | publicKeyPem: this.publicKey | |
317 | } | |
318 | } | |
319 | ||
320 | return activityPubContextify(json) | |
321 | } | |
322 | ||
323 | isOwned = function (this: AccountInstance) { | |
324 | return this.podId === null | |
325 | } | |
326 | ||
327 | getFollowerSharedInboxUrls = function (this: AccountInstance) { | |
328 | const query: Sequelize.FindOptions<AccountAttributes> = { | |
329 | attributes: [ 'sharedInboxUrl' ], | |
330 | include: [ | |
331 | { | |
332 | model: Account['sequelize'].models.AccountFollower, | |
333 | where: { | |
334 | targetAccountId: this.id | |
335 | } | |
336 | } | |
337 | ] | |
338 | } | |
339 | ||
340 | return Account.findAll(query) | |
341 | .then(accounts => accounts.map(a => a.sharedInboxUrl)) | |
342 | } | |
343 | ||
344 | getFollowingUrl = function (this: AccountInstance) { | |
345 | return this.url + '/followers' | |
346 | } | |
347 | ||
348 | getFollowersUrl = function (this: AccountInstance) { | |
349 | return this.url + '/followers' | |
350 | } | |
351 | ||
352 | getPublicKeyUrl = function (this: AccountInstance) { | |
353 | return this.url + '#main-key' | |
354 | } | |
355 | ||
356 | // ------------------------------ STATICS ------------------------------ | |
357 | ||
358 | listOwned = function () { | |
359 | const query: Sequelize.FindOptions<AccountAttributes> = { | |
360 | where: { | |
361 | podId: null | |
362 | } | |
363 | } | |
364 | ||
365 | return Account.findAll(query) | |
366 | } | |
367 | ||
8e696487 C |
368 | listAcceptedFollowerUrlsForApi = function (id: number, start: number, count?: number) { |
369 | return createListAcceptedFollowForApiQuery('followers', id, start, count) | |
7a7724e6 C |
370 | } |
371 | ||
8e696487 C |
372 | listAcceptedFollowingUrlsForApi = function (id: number, start: number, count?: number) { |
373 | return createListAcceptedFollowForApiQuery('following', id, start, count) | |
7a7724e6 C |
374 | } |
375 | ||
376 | listFollowingForApi = function (id: number, start: number, count: number, sort: string) { | |
377 | const query = { | |
378 | distinct: true, | |
379 | offset: start, | |
380 | limit: count, | |
381 | order: [ getSort(sort) ], | |
382 | include: [ | |
383 | { | |
384 | model: Account['sequelize'].models.AccountFollow, | |
385 | required: true, | |
386 | as: 'following', | |
387 | include: [ | |
388 | { | |
389 | model: Account['sequelize'].models.Account, | |
390 | as: 'following', | |
391 | required: true, | |
392 | include: [ Account['sequelize'].models.Pod ] | |
393 | } | |
394 | ] | |
395 | } | |
396 | ] | |
397 | } | |
398 | ||
399 | return Account.findAndCountAll(query).then(({ rows, count }) => { | |
400 | return { | |
401 | data: rows, | |
402 | total: count | |
403 | } | |
404 | }) | |
405 | } | |
406 | ||
407 | listFollowersForApi = function (id: number, start: number, count: number, sort: string) { | |
408 | const query = { | |
409 | distinct: true, | |
410 | offset: start, | |
411 | limit: count, | |
412 | order: [ getSort(sort) ], | |
413 | include: [ | |
414 | { | |
415 | model: Account['sequelize'].models.AccountFollow, | |
416 | required: true, | |
417 | as: 'followers', | |
418 | include: [ | |
419 | { | |
420 | model: Account['sequelize'].models.Account, | |
421 | as: 'followers', | |
422 | required: true, | |
423 | include: [ Account['sequelize'].models.Pod ] | |
424 | } | |
425 | ] | |
426 | } | |
427 | ] | |
428 | } | |
429 | ||
430 | return Account.findAndCountAll(query).then(({ rows, count }) => { | |
431 | return { | |
432 | data: rows, | |
433 | total: count | |
434 | } | |
435 | }) | |
e4f97bab C |
436 | } |
437 | ||
7a7724e6 C |
438 | loadApplication = function () { |
439 | return Account.findOne({ | |
440 | include: [ | |
441 | { | |
442 | model: Account['sequelize'].model.Application, | |
443 | required: true | |
444 | } | |
445 | ] | |
446 | }) | |
e4f97bab C |
447 | } |
448 | ||
449 | load = function (id: number) { | |
450 | return Account.findById(id) | |
451 | } | |
452 | ||
453 | loadByUUID = function (uuid: string) { | |
454 | const query: Sequelize.FindOptions<AccountAttributes> = { | |
455 | where: { | |
456 | uuid | |
457 | } | |
458 | } | |
459 | ||
460 | return Account.findOne(query) | |
461 | } | |
462 | ||
7a7724e6 | 463 | loadLocalAccountByNameAndPod = function (name: string, host: string) { |
e4f97bab C |
464 | const query: Sequelize.FindOptions<AccountAttributes> = { |
465 | where: { | |
466 | name, | |
467 | userId: { | |
468 | [Sequelize.Op.ne]: null | |
469 | } | |
7a7724e6 C |
470 | }, |
471 | include: [ | |
472 | { | |
473 | model: Account['sequelize'].models.Pod, | |
474 | where: { | |
475 | host | |
476 | } | |
477 | } | |
478 | ] | |
e4f97bab C |
479 | } |
480 | ||
481 | return Account.findOne(query) | |
482 | } | |
483 | ||
ce548a10 | 484 | loadByUrl = function (url: string, transaction?: Sequelize.Transaction) { |
e4f97bab C |
485 | const query: Sequelize.FindOptions<AccountAttributes> = { |
486 | where: { | |
487 | url | |
ce548a10 C |
488 | }, |
489 | transaction | |
e4f97bab C |
490 | } |
491 | ||
492 | return Account.findOne(query) | |
493 | } | |
494 | ||
495 | loadAccountByPodAndUUID = function (uuid: string, podId: number, transaction: Sequelize.Transaction) { | |
496 | const query: Sequelize.FindOptions<AccountAttributes> = { | |
497 | where: { | |
498 | podId, | |
499 | uuid | |
500 | }, | |
501 | transaction | |
502 | } | |
503 | ||
504 | return Account.find(query) | |
505 | } | |
506 | ||
507 | // ------------------------------ UTILS ------------------------------ | |
508 | ||
8e696487 | 509 | async function createListAcceptedFollowForApiQuery (type: 'followers' | 'following', id: number, start: number, count?: number) { |
e4f97bab C |
510 | let firstJoin: string |
511 | let secondJoin: string | |
512 | ||
513 | if (type === 'followers') { | |
514 | firstJoin = 'targetAccountId' | |
515 | secondJoin = 'accountId' | |
516 | } else { | |
517 | firstJoin = 'accountId' | |
518 | secondJoin = 'targetAccountId' | |
519 | } | |
520 | ||
521 | const selections = [ '"Followers"."url" AS "url"', 'COUNT(*) AS "total"' ] | |
522 | const tasks: Promise<any>[] = [] | |
523 | ||
524 | for (const selection of selections) { | |
571389d4 | 525 | let query = 'SELECT ' + selection + ' FROM "Account" ' + |
e4f97bab | 526 | 'INNER JOIN "AccountFollower" ON "AccountFollower"."' + firstJoin + '" = "Account"."id" ' + |
7a7724e6 | 527 | 'INNER JOIN "Account" AS "Follows" ON "Followers"."id" = "Follows"."' + secondJoin + '" ' + |
8e696487 | 528 | 'WHERE "Account"."id" = $id AND "AccountFollower"."state" = \'accepted\' ' + |
571389d4 C |
529 | 'LIMIT ' + start |
530 | ||
531 | if (count !== undefined) query += ', ' + count | |
e4f97bab C |
532 | |
533 | const options = { | |
7a7724e6 | 534 | bind: { id }, |
e4f97bab C |
535 | type: Sequelize.QueryTypes.SELECT |
536 | } | |
537 | tasks.push(Account['sequelize'].query(query, options)) | |
538 | } | |
539 | ||
540 | const [ followers, [ { total } ]] = await Promise.all(tasks) | |
541 | const urls: string[] = followers.map(f => f.url) | |
542 | ||
543 | return { | |
544 | data: urls, | |
545 | total: parseInt(total, 10) | |
546 | } | |
547 | } |