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