diff options
Diffstat (limited to 'server/models/account/account.ts')
-rw-r--r-- | server/models/account/account.ts | 444 |
1 files changed, 444 insertions, 0 deletions
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 | } | ||