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