]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/models/activitypub/actor.ts
Fix federation with some actors
[github/Chocobozzz/PeerTube.git] / server / models / activitypub / actor.ts
1 import { values } from 'lodash'
2 import { extname } from 'path'
3 import * as Sequelize from 'sequelize'
4 import {
5 AllowNull,
6 BelongsTo,
7 Column,
8 CreatedAt,
9 DataType,
10 DefaultScope,
11 ForeignKey,
12 HasMany,
13 HasOne,
14 Is,
15 Model,
16 Scopes,
17 Table,
18 UpdatedAt
19 } from 'sequelize-typescript'
20 import { ActivityPubActorType } from '../../../shared/models/activitypub'
21 import { Avatar } from '../../../shared/models/avatars/avatar.model'
22 import { activityPubContextify } from '../../helpers/activitypub'
23 import {
24 isActorFollowersCountValid,
25 isActorFollowingCountValid,
26 isActorPreferredUsernameValid,
27 isActorPrivateKeyValid,
28 isActorPublicKeyValid
29 } from '../../helpers/custom-validators/activitypub/actor'
30 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
31 import { ACTIVITY_PUB, ACTIVITY_PUB_ACTOR_TYPES, CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
32 import { AccountModel } from '../account/account'
33 import { AvatarModel } from '../avatar/avatar'
34 import { ServerModel } from '../server/server'
35 import { isOutdated, throwIfNotValid } from '../utils'
36 import { VideoChannelModel } from '../video/video-channel'
37 import { ActorFollowModel } from './actor-follow'
38 import { VideoModel } from '../video/video'
39 import {
40 MActor,
41 MActorAccountChannelId,
42 MActorAP,
43 MActorFormattable,
44 MActorFull,
45 MActorHost,
46 MActorServer,
47 MActorSummaryFormattable,
48 MActorWithInboxes
49 } from '../../typings/models'
50 import * as Bluebird from 'bluebird'
51
52 enum ScopeNames {
53 FULL = 'FULL'
54 }
55
56 export const unusedActorAttributesForAPI = [
57 'publicKey',
58 'privateKey',
59 'inboxUrl',
60 'outboxUrl',
61 'sharedInboxUrl',
62 'followersUrl',
63 'followingUrl',
64 'url',
65 'createdAt',
66 'updatedAt'
67 ]
68
69 @DefaultScope(() => ({
70 include: [
71 {
72 model: ServerModel,
73 required: false
74 },
75 {
76 model: AvatarModel,
77 required: false
78 }
79 ]
80 }))
81 @Scopes(() => ({
82 [ScopeNames.FULL]: {
83 include: [
84 {
85 model: AccountModel.unscoped(),
86 required: false
87 },
88 {
89 model: VideoChannelModel.unscoped(),
90 required: false,
91 include: [
92 {
93 model: AccountModel,
94 required: true
95 }
96 ]
97 },
98 {
99 model: ServerModel,
100 required: false
101 },
102 {
103 model: AvatarModel,
104 required: false
105 }
106 ]
107 }
108 }))
109 @Table({
110 tableName: 'actor',
111 indexes: [
112 {
113 fields: [ 'url' ],
114 unique: true
115 },
116 {
117 fields: [ 'preferredUsername', 'serverId' ],
118 unique: true
119 },
120 {
121 fields: [ 'inboxUrl', 'sharedInboxUrl' ]
122 },
123 {
124 fields: [ 'sharedInboxUrl' ]
125 },
126 {
127 fields: [ 'serverId' ]
128 },
129 {
130 fields: [ 'avatarId' ]
131 },
132 {
133 fields: [ 'followersUrl' ]
134 }
135 ]
136 })
137 export class ActorModel extends Model<ActorModel> {
138
139 @AllowNull(false)
140 @Column(DataType.ENUM(...values(ACTIVITY_PUB_ACTOR_TYPES)))
141 type: ActivityPubActorType
142
143 @AllowNull(false)
144 @Is('ActorPreferredUsername', value => throwIfNotValid(value, isActorPreferredUsernameValid, 'actor preferred username'))
145 @Column
146 preferredUsername: string
147
148 @AllowNull(false)
149 @Is('ActorUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
150 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
151 url: string
152
153 @AllowNull(true)
154 @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPublicKeyValid, 'public key', true))
155 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.PUBLIC_KEY.max))
156 publicKey: string
157
158 @AllowNull(true)
159 @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPrivateKeyValid, 'private key', true))
160 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.PRIVATE_KEY.max))
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'))
175 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
176 inboxUrl: string
177
178 @AllowNull(true)
179 @Is('ActorOutboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'outbox url', true))
180 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
181 outboxUrl: string
182
183 @AllowNull(true)
184 @Is('ActorSharedInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'shared inbox url', true))
185 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
186 sharedInboxUrl: string
187
188 @AllowNull(true)
189 @Is('ActorFollowersUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'followers url', true))
190 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
191 followersUrl: string
192
193 @AllowNull(true)
194 @Is('ActorFollowingUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'following url', true))
195 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
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 },
212 onDelete: 'set null',
213 hooks: true
214 })
215 Avatar: AvatarModel
216
217 @HasMany(() => ActorFollowModel, {
218 foreignKey: {
219 name: 'actorId',
220 allowNull: false
221 },
222 as: 'ActorFollowings',
223 onDelete: 'cascade'
224 })
225 ActorFollowing: ActorFollowModel[]
226
227 @HasMany(() => ActorFollowModel, {
228 foreignKey: {
229 name: 'targetActorId',
230 allowNull: false
231 },
232 as: 'ActorFollowers',
233 onDelete: 'cascade'
234 })
235 ActorFollowers: ActorFollowModel[]
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
249 @HasOne(() => AccountModel, {
250 foreignKey: {
251 allowNull: true
252 },
253 onDelete: 'cascade',
254 hooks: true
255 })
256 Account: AccountModel
257
258 @HasOne(() => VideoChannelModel, {
259 foreignKey: {
260 allowNull: true
261 },
262 onDelete: 'cascade',
263 hooks: true
264 })
265 VideoChannel: VideoChannelModel
266
267 static load (id: number): Bluebird<MActor> {
268 return ActorModel.unscoped().findByPk(id)
269 }
270
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> {
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,
287 include: [
288 {
289 attributes: [ 'id' ],
290 model: VideoModel.unscoped(),
291 required: true,
292 where: {
293 id: videoId
294 }
295 }
296 ]
297 }
298 ]
299 }
300 ],
301 transaction
302 }
303
304 return ActorModel.unscoped().findOne(query)
305 }
306
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
319 static listByFollowersUrls (followersUrls: string[], transaction?: Sequelize.Transaction): Bluebird<MActorFull[]> {
320 const query = {
321 where: {
322 followersUrl: {
323 [ Sequelize.Op.in ]: followersUrls
324 }
325 },
326 transaction
327 }
328
329 return ActorModel.scope(ScopeNames.FULL).findAll(query)
330 }
331
332 static loadLocalByName (preferredUsername: string, transaction?: Sequelize.Transaction): Bluebird<MActorFull> {
333 const query = {
334 where: {
335 preferredUsername,
336 serverId: null
337 },
338 transaction
339 }
340
341 return ActorModel.scope(ScopeNames.FULL).findOne(query)
342 }
343
344 static loadByNameAndHost (preferredUsername: string, host: string): Bluebird<MActorFull> {
345 const query = {
346 where: {
347 preferredUsername
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
363 static loadByUrl (url: string, transaction?: Sequelize.Transaction): Bluebird<MActorAccountChannelId> {
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
386 static loadByUrlAndPopulateAccountAndChannel (url: string, transaction?: Sequelize.Transaction): Bluebird<MActorFull> {
387 const query = {
388 where: {
389 url
390 },
391 transaction
392 }
393
394 return ActorModel.scope(ScopeNames.FULL).findOne(query)
395 }
396
397 static incrementFollows (id: number, column: 'followersCount' | 'followingCount', by: number) {
398 return ActorModel.increment(column, {
399 by,
400 where: {
401 id
402 }
403 })
404 }
405
406 getSharedInbox (this: MActorWithInboxes) {
407 return this.sharedInboxUrl || this.inboxUrl
408 }
409
410 toFormattedSummaryJSON (this: MActorSummaryFormattable) {
411 let avatar: Avatar = null
412 if (this.Avatar) {
413 avatar = this.Avatar.toFormattedJSON()
414 }
415
416 return {
417 url: this.url,
418 name: this.preferredUsername,
419 host: this.getHost(),
420 avatar
421 }
422 }
423
424 toFormattedJSON (this: MActorFormattable) {
425 const base = this.toFormattedSummaryJSON()
426
427 return Object.assign(base, {
428 id: this.id,
429 hostRedundancyAllowed: this.getRedundancyAllowed(),
430 followingCount: this.followingCount,
431 followersCount: this.followersCount,
432 createdAt: this.createdAt,
433 updatedAt: this.updatedAt
434 })
435 }
436
437 toActivityPubObject (this: MActorAP, name: string) {
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
448 const json = {
449 type: this.type,
450 id: this.url,
451 following: this.getFollowingUrl(),
452 followers: this.getFollowersUrl(),
453 playlists: this.getPlaylistsUrl(),
454 inbox: this.inboxUrl,
455 outbox: this.outboxUrl,
456 preferredUsername: this.preferredUsername,
457 url: this.url,
458 name,
459 endpoints: {
460 sharedInbox: this.sharedInboxUrl
461 },
462 publicKey: {
463 id: this.getPublicKeyUrl(),
464 owner: this.url,
465 publicKeyPem: this.publicKey
466 },
467 icon
468 }
469
470 return activityPubContextify(json)
471 }
472
473 getFollowerSharedInboxUrls (t: Sequelize.Transaction) {
474 const query = {
475 attributes: [ 'sharedInboxUrl' ],
476 include: [
477 {
478 attribute: [],
479 model: ActorFollowModel.unscoped(),
480 required: true,
481 as: 'ActorFollowing',
482 where: {
483 state: 'accepted',
484 targetActorId: this.id
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
503 getPlaylistsUrl () {
504 return this.url + '/playlists'
505 }
506
507 getPublicKeyUrl () {
508 return this.url + '#main-key'
509 }
510
511 isOwned () {
512 return this.serverId === null
513 }
514
515 getWebfingerUrl (this: MActorServer) {
516 return 'acct:' + this.preferredUsername + '@' + this.getHost()
517 }
518
519 getIdentifier () {
520 return this.Server ? `${this.preferredUsername}@${this.Server.host}` : this.preferredUsername
521 }
522
523 getHost (this: MActorHost) {
524 return this.Server ? this.Server.host : WEBSERVER.HOST
525 }
526
527 getRedundancyAllowed () {
528 return this.Server ? this.Server.redundancyAllowed : false
529 }
530
531 getAvatarUrl () {
532 if (!this.avatarId) return undefined
533
534 return WEBSERVER.URL + this.Avatar.getStaticPath()
535 }
536
537 isOutdated () {
538 if (this.isOwned()) return false
539
540 return isOutdated(this, ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL)
541 }
542 }