]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/activitypub/actor.ts
Ensure local actors preferredName don't already exist
[github/Chocobozzz/PeerTube.git] / server / models / activitypub / actor.ts
CommitLineData
50d6de9c 1import { values } from 'lodash'
47564bbe 2import { extname } from 'path'
fadf619a
C
3import * as Sequelize from 'sequelize'
4import {
2422c46b
C
5 AllowNull,
6 BelongsTo,
7 Column,
8 CreatedAt,
9 DataType,
2422c46b
C
10 DefaultScope,
11 ForeignKey,
12 HasMany,
13 HasOne,
14 Is,
2422c46b
C
15 Model,
16 Scopes,
17 Table,
18 UpdatedAt
fadf619a 19} from 'sequelize-typescript'
50d6de9c 20import { ActivityPubActorType } from '../../../shared/models/activitypub'
fadf619a 21import { Avatar } from '../../../shared/models/avatars/avatar.model'
da854ddd 22import { activityPubContextify } from '../../helpers/activitypub'
fadf619a 23import {
2422c46b
C
24 isActorFollowersCountValid,
25 isActorFollowingCountValid,
26 isActorPreferredUsernameValid,
27 isActorPrivateKeyValid,
da854ddd
C
28 isActorPublicKeyValid
29} from '../../helpers/custom-validators/activitypub/actor'
30import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
74dc3bca 31import { ACTIVITY_PUB, ACTIVITY_PUB_ACTOR_TYPES, CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
50d6de9c 32import { AccountModel } from '../account/account'
fadf619a
C
33import { AvatarModel } from '../avatar/avatar'
34import { ServerModel } from '../server/server'
9f79ade6 35import { isOutdated, throwIfNotValid } from '../utils'
50d6de9c
C
36import { VideoChannelModel } from '../video/video-channel'
37import { ActorFollowModel } from './actor-follow'
e5565833 38import { VideoModel } from '../video/video'
1ca9f7c3
C
39import {
40 MActor,
41 MActorAccountChannelId,
b5fecbf4 42 MActorAP,
1ca9f7c3 43 MActorFormattable,
b5fecbf4
C
44 MActorFull,
45 MActorHost,
1ca9f7c3 46 MActorServer,
47581df0
C
47 MActorSummaryFormattable,
48 MActorWithInboxes
1ca9f7c3 49} from '../../typings/models'
453e83ea 50import * as Bluebird from 'bluebird'
77e08517 51import { Op } from 'sequelize'
fadf619a 52
50d6de9c
C
53enum ScopeNames {
54 FULL = 'FULL'
55}
56
f37dc0dd
C
57export const unusedActorAttributesForAPI = [
58 'publicKey',
59 'privateKey',
60 'inboxUrl',
61 'outboxUrl',
62 'sharedInboxUrl',
63 'followersUrl',
aa55a4da 64 'followingUrl',
f5b0af50
C
65 'url',
66 'createdAt',
67 'updatedAt'
f37dc0dd
C
68]
69
3acc5084 70@DefaultScope(() => ({
ce33ee01
C
71 include: [
72 {
3acc5084 73 model: ServerModel,
ce33ee01 74 required: false
c5911fd3
C
75 },
76 {
3acc5084 77 model: AvatarModel,
c5911fd3 78 required: false
ce33ee01
C
79 }
80 ]
3acc5084
C
81}))
82@Scopes(() => ({
50d6de9c
C
83 [ScopeNames.FULL]: {
84 include: [
85 {
3acc5084 86 model: AccountModel.unscoped(),
50d6de9c
C
87 required: false
88 },
89 {
3acc5084 90 model: VideoChannelModel.unscoped(),
c48e82b5
C
91 required: false,
92 include: [
93 {
3acc5084 94 model: AccountModel,
c48e82b5
C
95 required: true
96 }
97 ]
ce33ee01
C
98 },
99 {
3acc5084 100 model: ServerModel,
ce33ee01 101 required: false
c5911fd3
C
102 },
103 {
3acc5084 104 model: AvatarModel,
c5911fd3 105 required: false
50d6de9c 106 }
3acc5084 107 ]
50d6de9c 108 }
3acc5084 109}))
fadf619a 110@Table({
50d6de9c
C
111 tableName: 'actor',
112 indexes: [
2ccaeeb3 113 {
8cd72bd3
C
114 fields: [ 'url' ],
115 unique: true
2ccaeeb3 116 },
50d6de9c 117 {
e12a0092 118 fields: [ 'preferredUsername', 'serverId' ],
77e08517
C
119 unique: true,
120 where: {
121 serverId: {
122 [Op.ne]: null
123 }
124 }
125 },
126 {
127 fields: [ 'preferredUsername' ],
128 unique: true,
129 where: {
130 serverId: null
131 }
6502c3d4
C
132 },
133 {
134 fields: [ 'inboxUrl', 'sharedInboxUrl' ]
57c36b27 135 },
a3d1026b
C
136 {
137 fields: [ 'sharedInboxUrl' ]
138 },
57c36b27
C
139 {
140 fields: [ 'serverId' ]
141 },
142 {
143 fields: [ 'avatarId' ]
8cd72bd3 144 },
8cd72bd3
C
145 {
146 fields: [ 'followersUrl' ]
50d6de9c
C
147 }
148 ]
fadf619a
C
149})
150export class ActorModel extends Model<ActorModel> {
151
50d6de9c 152 @AllowNull(false)
3acc5084 153 @Column(DataType.ENUM(...values(ACTIVITY_PUB_ACTOR_TYPES)))
50d6de9c
C
154 type: ActivityPubActorType
155
fadf619a 156 @AllowNull(false)
e12a0092 157 @Is('ActorPreferredUsername', value => throwIfNotValid(value, isActorPreferredUsernameValid, 'actor preferred username'))
fadf619a 158 @Column
e12a0092 159 preferredUsername: string
fadf619a
C
160
161 @AllowNull(false)
162 @Is('ActorUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
01de67b9 163 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
fadf619a
C
164 url: string
165
166 @AllowNull(true)
1735c825 167 @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPublicKeyValid, 'public key', true))
01de67b9 168 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.PUBLIC_KEY.max))
fadf619a
C
169 publicKey: string
170
171 @AllowNull(true)
1735c825 172 @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPrivateKeyValid, 'private key', true))
01de67b9 173 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.PRIVATE_KEY.max))
fadf619a
C
174 privateKey: string
175
176 @AllowNull(false)
177 @Is('ActorFollowersCount', value => throwIfNotValid(value, isActorFollowersCountValid, 'followers count'))
178 @Column
179 followersCount: number
180
181 @AllowNull(false)
182 @Is('ActorFollowersCount', value => throwIfNotValid(value, isActorFollowingCountValid, 'following count'))
183 @Column
184 followingCount: number
185
186 @AllowNull(false)
187 @Is('ActorInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'inbox url'))
01de67b9 188 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
fadf619a
C
189 inboxUrl: string
190
0b5c385b
C
191 @AllowNull(true)
192 @Is('ActorOutboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'outbox url', true))
01de67b9 193 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
fadf619a
C
194 outboxUrl: string
195
47581df0
C
196 @AllowNull(true)
197 @Is('ActorSharedInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'shared inbox url', true))
01de67b9 198 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
fadf619a
C
199 sharedInboxUrl: string
200
0b5c385b
C
201 @AllowNull(true)
202 @Is('ActorFollowersUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'followers url', true))
01de67b9 203 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
fadf619a
C
204 followersUrl: string
205
0b5c385b
C
206 @AllowNull(true)
207 @Is('ActorFollowingUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'following url', true))
01de67b9 208 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
fadf619a
C
209 followingUrl: string
210
211 @CreatedAt
212 createdAt: Date
213
214 @UpdatedAt
215 updatedAt: Date
216
217 @ForeignKey(() => AvatarModel)
218 @Column
219 avatarId: number
220
221 @BelongsTo(() => AvatarModel, {
222 foreignKey: {
223 allowNull: true
224 },
f05a1c30
C
225 onDelete: 'set null',
226 hooks: true
fadf619a
C
227 })
228 Avatar: AvatarModel
229
50d6de9c 230 @HasMany(() => ActorFollowModel, {
fadf619a 231 foreignKey: {
50d6de9c 232 name: 'actorId',
fadf619a
C
233 allowNull: false
234 },
cef534ed 235 as: 'ActorFollowings',
fadf619a
C
236 onDelete: 'cascade'
237 })
54e74059 238 ActorFollowing: ActorFollowModel[]
fadf619a 239
50d6de9c 240 @HasMany(() => ActorFollowModel, {
fadf619a 241 foreignKey: {
50d6de9c 242 name: 'targetActorId',
fadf619a
C
243 allowNull: false
244 },
54e74059 245 as: 'ActorFollowers',
fadf619a
C
246 onDelete: 'cascade'
247 })
54e74059 248 ActorFollowers: ActorFollowModel[]
fadf619a
C
249
250 @ForeignKey(() => ServerModel)
251 @Column
252 serverId: number
253
254 @BelongsTo(() => ServerModel, {
255 foreignKey: {
256 allowNull: true
257 },
258 onDelete: 'cascade'
259 })
260 Server: ServerModel
261
50d6de9c
C
262 @HasOne(() => AccountModel, {
263 foreignKey: {
c5a893d5
C
264 allowNull: true
265 },
266 onDelete: 'cascade',
267 hooks: true
50d6de9c
C
268 })
269 Account: AccountModel
270
271 @HasOne(() => VideoChannelModel, {
272 foreignKey: {
c5a893d5
C
273 allowNull: true
274 },
275 onDelete: 'cascade',
276 hooks: true
50d6de9c
C
277 })
278 VideoChannel: VideoChannelModel
279
453e83ea 280 static load (id: number): Bluebird<MActor> {
9b39106d 281 return ActorModel.unscoped().findByPk(id)
50d6de9c
C
282 }
283
453e83ea
C
284 static loadFull (id: number): Bluebird<MActorFull> {
285 return ActorModel.scope(ScopeNames.FULL).findByPk(id)
286 }
287
288 static loadFromAccountByVideoId (videoId: number, transaction: Sequelize.Transaction): Bluebird<MActor> {
e5565833
C
289 const query = {
290 include: [
291 {
292 attributes: [ 'id' ],
293 model: AccountModel.unscoped(),
294 required: true,
295 include: [
296 {
297 attributes: [ 'id' ],
298 model: VideoChannelModel.unscoped(),
299 required: true,
3acc5084
C
300 include: [
301 {
302 attributes: [ 'id' ],
303 model: VideoModel.unscoped(),
304 required: true,
305 where: {
306 id: videoId
307 }
e5565833 308 }
3acc5084 309 ]
e5565833
C
310 }
311 ]
312 }
313 ],
314 transaction
315 }
316
3acc5084 317 return ActorModel.unscoped().findOne(query)
e5565833
C
318 }
319
d4defe07
C
320 static isActorUrlExist (url: string) {
321 const query = {
322 raw: true,
323 where: {
324 url
325 }
326 }
327
328 return ActorModel.unscoped().findOne(query)
329 .then(a => !!a)
330 }
331
453e83ea 332 static listByFollowersUrls (followersUrls: string[], transaction?: Sequelize.Transaction): Bluebird<MActorFull[]> {
fadf619a
C
333 const query = {
334 where: {
335 followersUrl: {
336 [ Sequelize.Op.in ]: followersUrls
337 }
338 },
339 transaction
340 }
341
50d6de9c
C
342 return ActorModel.scope(ScopeNames.FULL).findAll(query)
343 }
344
453e83ea 345 static loadLocalByName (preferredUsername: string, transaction?: Sequelize.Transaction): Bluebird<MActorFull> {
50d6de9c
C
346 const query = {
347 where: {
e12a0092 348 preferredUsername,
50d6de9c 349 serverId: null
8a19bee1
C
350 },
351 transaction
50d6de9c
C
352 }
353
354 return ActorModel.scope(ScopeNames.FULL).findOne(query)
355 }
356
453e83ea 357 static loadByNameAndHost (preferredUsername: string, host: string): Bluebird<MActorFull> {
50d6de9c
C
358 const query = {
359 where: {
e12a0092 360 preferredUsername
50d6de9c
C
361 },
362 include: [
363 {
364 model: ServerModel,
365 required: true,
366 where: {
367 host
368 }
369 }
370 ]
371 }
372
373 return ActorModel.scope(ScopeNames.FULL).findOne(query)
374 }
375
453e83ea 376 static loadByUrl (url: string, transaction?: Sequelize.Transaction): Bluebird<MActorAccountChannelId> {
e587e0ec
C
377 const query = {
378 where: {
379 url
380 },
381 transaction,
382 include: [
383 {
384 attributes: [ 'id' ],
385 model: AccountModel.unscoped(),
386 required: false
387 },
388 {
389 attributes: [ 'id' ],
390 model: VideoChannelModel.unscoped(),
391 required: false
392 }
393 ]
394 }
395
396 return ActorModel.unscoped().findOne(query)
397 }
398
453e83ea 399 static loadByUrlAndPopulateAccountAndChannel (url: string, transaction?: Sequelize.Transaction): Bluebird<MActorFull> {
50d6de9c
C
400 const query = {
401 where: {
402 url
403 },
404 transaction
405 }
406
407 return ActorModel.scope(ScopeNames.FULL).findOne(query)
fadf619a
C
408 }
409
32b2b43c 410 static incrementFollows (id: number, column: 'followersCount' | 'followingCount', by: number) {
3acc5084 411 return ActorModel.increment(column, {
32b2b43c
C
412 by,
413 where: {
414 id
415 }
416 })
417 }
418
47581df0
C
419 getSharedInbox (this: MActorWithInboxes) {
420 return this.sharedInboxUrl || this.inboxUrl
421 }
422
1ca9f7c3 423 toFormattedSummaryJSON (this: MActorSummaryFormattable) {
fadf619a
C
424 let avatar: Avatar = null
425 if (this.Avatar) {
c5911fd3 426 avatar = this.Avatar.toFormattedJSON()
fadf619a
C
427 }
428
fadf619a 429 return {
4cb6d457 430 url: this.url,
60650c77 431 name: this.preferredUsername,
e12a0092 432 host: this.getHost(),
1ca9f7c3
C
433 avatar
434 }
435 }
436
437 toFormattedJSON (this: MActorFormattable) {
438 const base = this.toFormattedSummaryJSON()
439
440 return Object.assign(base, {
441 id: this.id,
c48e82b5 442 hostRedundancyAllowed: this.getRedundancyAllowed(),
fadf619a
C
443 followingCount: this.followingCount,
444 followersCount: this.followersCount,
60650c77
C
445 createdAt: this.createdAt,
446 updatedAt: this.updatedAt
1ca9f7c3 447 })
fadf619a
C
448 }
449
8424c402 450 toActivityPubObject (this: MActorAP, name: string) {
c5911fd3
C
451 let icon = undefined
452 if (this.avatarId) {
453 const extension = extname(this.Avatar.filename)
454 icon = {
455 type: 'Image',
456 mediaType: extension === '.png' ? 'image/png' : 'image/jpeg',
457 url: this.getAvatarUrl()
458 }
459 }
460
fadf619a 461 const json = {
8424c402 462 type: this.type,
fadf619a
C
463 id: this.url,
464 following: this.getFollowingUrl(),
465 followers: this.getFollowersUrl(),
418d092a 466 playlists: this.getPlaylistsUrl(),
fadf619a
C
467 inbox: this.inboxUrl,
468 outbox: this.outboxUrl,
e12a0092 469 preferredUsername: this.preferredUsername,
fadf619a 470 url: this.url,
e12a0092 471 name,
fadf619a
C
472 endpoints: {
473 sharedInbox: this.sharedInboxUrl
474 },
fadf619a
C
475 publicKey: {
476 id: this.getPublicKeyUrl(),
477 owner: this.url,
478 publicKeyPem: this.publicKey
c5911fd3
C
479 },
480 icon
fadf619a
C
481 }
482
483 return activityPubContextify(json)
484 }
485
486 getFollowerSharedInboxUrls (t: Sequelize.Transaction) {
487 const query = {
488 attributes: [ 'sharedInboxUrl' ],
489 include: [
490 {
54e74059
C
491 attribute: [],
492 model: ActorFollowModel.unscoped(),
fadf619a 493 required: true,
d6e99e53 494 as: 'ActorFollowing',
fadf619a 495 where: {
54e74059 496 state: 'accepted',
50d6de9c 497 targetActorId: this.id
fadf619a
C
498 }
499 }
500 ],
501 transaction: t
502 }
503
504 return ActorModel.findAll(query)
505 .then(accounts => accounts.map(a => a.sharedInboxUrl))
506 }
507
508 getFollowingUrl () {
509 return this.url + '/following'
510 }
511
512 getFollowersUrl () {
513 return this.url + '/followers'
514 }
515
418d092a
C
516 getPlaylistsUrl () {
517 return this.url + '/playlists'
518 }
519
fadf619a
C
520 getPublicKeyUrl () {
521 return this.url + '#main-key'
522 }
523
524 isOwned () {
525 return this.serverId === null
526 }
e12a0092 527
1ca9f7c3 528 getWebfingerUrl (this: MActorServer) {
e12a0092
C
529 return 'acct:' + this.preferredUsername + '@' + this.getHost()
530 }
531
80e36cd9
AB
532 getIdentifier () {
533 return this.Server ? `${this.preferredUsername}@${this.Server.host}` : this.preferredUsername
534 }
535
1ca9f7c3 536 getHost (this: MActorHost) {
6dd9de95 537 return this.Server ? this.Server.host : WEBSERVER.HOST
e12a0092 538 }
c5911fd3 539
b5fecbf4 540 getRedundancyAllowed () {
c48e82b5
C
541 return this.Server ? this.Server.redundancyAllowed : false
542 }
543
c5911fd3
C
544 getAvatarUrl () {
545 if (!this.avatarId) return undefined
546
557b13ae 547 return WEBSERVER.URL + this.Avatar.getStaticPath()
c5911fd3 548 }
a5625b41
C
549
550 isOutdated () {
551 if (this.isOwned()) return false
552
9f79ade6 553 return isOutdated(this, ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL)
a5625b41 554 }
fadf619a 555}