]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/models/activitypub/actor.ts
Merge branch 'release/2.1.0' into develop
[github/Chocobozzz/PeerTube.git] / server / models / activitypub / actor.ts
1 import { values } from 'lodash'
2 import { extname } from 'path'
3 import {
4 AllowNull,
5 BelongsTo,
6 Column,
7 CreatedAt,
8 DataType,
9 DefaultScope,
10 ForeignKey,
11 HasMany,
12 HasOne,
13 Is,
14 Model,
15 Scopes,
16 Table,
17 UpdatedAt
18 } from 'sequelize-typescript'
19 import { ActivityIconObject, ActivityPubActorType } from '../../../shared/models/activitypub'
20 import { Avatar } from '../../../shared/models/avatars/avatar.model'
21 import { activityPubContextify } from '../../helpers/activitypub'
22 import {
23 isActorFollowersCountValid,
24 isActorFollowingCountValid,
25 isActorPreferredUsernameValid,
26 isActorPrivateKeyValid,
27 isActorPublicKeyValid
28 } from '../../helpers/custom-validators/activitypub/actor'
29 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
30 import { ACTIVITY_PUB, ACTIVITY_PUB_ACTOR_TYPES, CONSTRAINTS_FIELDS, SERVER_ACTOR_NAME, WEBSERVER } from '../../initializers/constants'
31 import { AccountModel } from '../account/account'
32 import { AvatarModel } from '../avatar/avatar'
33 import { ServerModel } from '../server/server'
34 import { isOutdated, throwIfNotValid } from '../utils'
35 import { VideoChannelModel } from '../video/video-channel'
36 import { ActorFollowModel } from './actor-follow'
37 import { VideoModel } from '../video/video'
38 import {
39 MActor,
40 MActorAccountChannelId,
41 MActorAP,
42 MActorFormattable,
43 MActorFull,
44 MActorHost,
45 MActorServer,
46 MActorSummaryFormattable, MActorUrl,
47 MActorWithInboxes
48 } from '../../typings/models'
49 import * as Bluebird from 'bluebird'
50 import { Op, Transaction, literal } from 'sequelize'
51 import { ModelCache } from '@server/models/model-cache'
52
53 enum ScopeNames {
54 FULL = 'FULL'
55 }
56
57 export const unusedActorAttributesForAPI = [
58 'publicKey',
59 'privateKey',
60 'inboxUrl',
61 'outboxUrl',
62 'sharedInboxUrl',
63 'followersUrl',
64 'followingUrl',
65 'url',
66 'createdAt',
67 'updatedAt'
68 ]
69
70 @DefaultScope(() => ({
71 include: [
72 {
73 model: ServerModel,
74 required: false
75 },
76 {
77 model: AvatarModel,
78 required: false
79 }
80 ]
81 }))
82 @Scopes(() => ({
83 [ScopeNames.FULL]: {
84 include: [
85 {
86 model: AccountModel.unscoped(),
87 required: false
88 },
89 {
90 model: VideoChannelModel.unscoped(),
91 required: false,
92 include: [
93 {
94 model: AccountModel,
95 required: true
96 }
97 ]
98 },
99 {
100 model: ServerModel,
101 required: false
102 },
103 {
104 model: AvatarModel,
105 required: false
106 }
107 ]
108 }
109 }))
110 @Table({
111 tableName: 'actor',
112 indexes: [
113 {
114 fields: [ 'url' ],
115 unique: true
116 },
117 {
118 fields: [ 'preferredUsername', 'serverId' ],
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 // }
132 // },
133 {
134 fields: [ 'inboxUrl', 'sharedInboxUrl' ]
135 },
136 {
137 fields: [ 'sharedInboxUrl' ]
138 },
139 {
140 fields: [ 'serverId' ]
141 },
142 {
143 fields: [ 'avatarId' ]
144 },
145 {
146 fields: [ 'followersUrl' ]
147 }
148 ]
149 })
150 export class ActorModel extends Model<ActorModel> {
151
152 @AllowNull(false)
153 @Column(DataType.ENUM(...values(ACTIVITY_PUB_ACTOR_TYPES)))
154 type: ActivityPubActorType
155
156 @AllowNull(false)
157 @Is('ActorPreferredUsername', value => throwIfNotValid(value, isActorPreferredUsernameValid, 'actor preferred username'))
158 @Column
159 preferredUsername: string
160
161 @AllowNull(false)
162 @Is('ActorUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
163 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
164 url: string
165
166 @AllowNull(true)
167 @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPublicKeyValid, 'public key', true))
168 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.PUBLIC_KEY.max))
169 publicKey: string
170
171 @AllowNull(true)
172 @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPrivateKeyValid, 'private key', true))
173 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.PRIVATE_KEY.max))
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'))
188 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
189 inboxUrl: string
190
191 @AllowNull(true)
192 @Is('ActorOutboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'outbox url', true))
193 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
194 outboxUrl: string
195
196 @AllowNull(true)
197 @Is('ActorSharedInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'shared inbox url', true))
198 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
199 sharedInboxUrl: string
200
201 @AllowNull(true)
202 @Is('ActorFollowersUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'followers url', true))
203 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
204 followersUrl: string
205
206 @AllowNull(true)
207 @Is('ActorFollowingUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'following url', true))
208 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
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 },
225 onDelete: 'set null',
226 hooks: true
227 })
228 Avatar: AvatarModel
229
230 @HasMany(() => ActorFollowModel, {
231 foreignKey: {
232 name: 'actorId',
233 allowNull: false
234 },
235 as: 'ActorFollowings',
236 onDelete: 'cascade'
237 })
238 ActorFollowing: ActorFollowModel[]
239
240 @HasMany(() => ActorFollowModel, {
241 foreignKey: {
242 name: 'targetActorId',
243 allowNull: false
244 },
245 as: 'ActorFollowers',
246 onDelete: 'cascade'
247 })
248 ActorFollowers: ActorFollowModel[]
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
262 @HasOne(() => AccountModel, {
263 foreignKey: {
264 allowNull: true
265 },
266 onDelete: 'cascade',
267 hooks: true
268 })
269 Account: AccountModel
270
271 @HasOne(() => VideoChannelModel, {
272 foreignKey: {
273 allowNull: true
274 },
275 onDelete: 'cascade',
276 hooks: true
277 })
278 VideoChannel: VideoChannelModel
279
280 static load (id: number): Bluebird<MActor> {
281 return ActorModel.unscoped().findByPk(id)
282 }
283
284 static loadFull (id: number): Bluebird<MActorFull> {
285 return ActorModel.scope(ScopeNames.FULL).findByPk(id)
286 }
287
288 static loadFromAccountByVideoId (videoId: number, transaction: Transaction): Bluebird<MActor> {
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,
300 include: [
301 {
302 attributes: [ 'id' ],
303 model: VideoModel.unscoped(),
304 required: true,
305 where: {
306 id: videoId
307 }
308 }
309 ]
310 }
311 ]
312 }
313 ],
314 transaction
315 }
316
317 return ActorModel.unscoped().findOne(query)
318 }
319
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
332 static listByFollowersUrls (followersUrls: string[], transaction?: Transaction): Bluebird<MActorFull[]> {
333 const query = {
334 where: {
335 followersUrl: {
336 [Op.in]: followersUrls
337 }
338 },
339 transaction
340 }
341
342 return ActorModel.scope(ScopeNames.FULL).findAll(query)
343 }
344
345 static loadLocalByName (preferredUsername: string, transaction?: Transaction): Bluebird<MActorFull> {
346 const fun = () => {
347 const query = {
348 where: {
349 preferredUsername,
350 serverId: null
351 },
352 transaction
353 }
354
355 return ActorModel.scope(ScopeNames.FULL)
356 .findOne(query)
357 }
358
359 return ModelCache.Instance.doCache({
360 cacheType: 'local-actor-name',
361 key: preferredUsername,
362 // The server actor never change, so we can easily cache it
363 whitelist: () => preferredUsername === SERVER_ACTOR_NAME,
364 fun
365 })
366 }
367
368 static loadLocalUrlByName (preferredUsername: string, transaction?: Transaction): Bluebird<MActorUrl> {
369 const fun = () => {
370 const query = {
371 attributes: [ 'url' ],
372 where: {
373 preferredUsername,
374 serverId: null
375 },
376 transaction
377 }
378
379 return ActorModel.unscoped()
380 .findOne(query)
381 }
382
383 return ModelCache.Instance.doCache({
384 cacheType: 'local-actor-name',
385 key: preferredUsername,
386 // The server actor never change, so we can easily cache it
387 whitelist: () => preferredUsername === SERVER_ACTOR_NAME,
388 fun
389 })
390 }
391
392 static loadByNameAndHost (preferredUsername: string, host: string): Bluebird<MActorFull> {
393 const query = {
394 where: {
395 preferredUsername
396 },
397 include: [
398 {
399 model: ServerModel,
400 required: true,
401 where: {
402 host
403 }
404 }
405 ]
406 }
407
408 return ActorModel.scope(ScopeNames.FULL).findOne(query)
409 }
410
411 static loadByUrl (url: string, transaction?: Transaction): Bluebird<MActorAccountChannelId> {
412 const query = {
413 where: {
414 url
415 },
416 transaction,
417 include: [
418 {
419 attributes: [ 'id' ],
420 model: AccountModel.unscoped(),
421 required: false
422 },
423 {
424 attributes: [ 'id' ],
425 model: VideoChannelModel.unscoped(),
426 required: false
427 }
428 ]
429 }
430
431 return ActorModel.unscoped().findOne(query)
432 }
433
434 static loadByUrlAndPopulateAccountAndChannel (url: string, transaction?: Transaction): Bluebird<MActorFull> {
435 const query = {
436 where: {
437 url
438 },
439 transaction
440 }
441
442 return ActorModel.scope(ScopeNames.FULL).findOne(query)
443 }
444
445 static rebuildFollowsCount (ofId: number, type: 'followers' | 'following', transaction?: Transaction) {
446 const sanitizedOfId = parseInt(ofId + '', 10)
447 const where = { id: sanitizedOfId }
448
449 let columnToUpdate: string
450 let columnOfCount: string
451
452 if (type === 'followers') {
453 columnToUpdate = 'followersCount'
454 columnOfCount = 'targetActorId'
455 } else {
456 columnToUpdate = 'followingCount'
457 columnOfCount = 'actorId'
458 }
459
460 return ActorModel.update({
461 [columnToUpdate]: literal(`(SELECT COUNT(*) FROM "actorFollow" WHERE "${columnOfCount}" = ${sanitizedOfId})`)
462 }, { where, transaction })
463 }
464
465 static loadAccountActorByVideoId (videoId: number): Bluebird<MActor> {
466 const query = {
467 include: [
468 {
469 attributes: [ 'id' ],
470 model: AccountModel.unscoped(),
471 required: true,
472 include: [
473 {
474 attributes: [ 'id', 'accountId' ],
475 model: VideoChannelModel.unscoped(),
476 required: true,
477 include: [
478 {
479 attributes: [ 'id', 'channelId' ],
480 model: VideoModel.unscoped(),
481 where: {
482 id: videoId
483 }
484 }
485 ]
486 }
487 ]
488 }
489 ]
490 }
491
492 return ActorModel.unscoped().findOne(query)
493 }
494
495 getSharedInbox (this: MActorWithInboxes) {
496 return this.sharedInboxUrl || this.inboxUrl
497 }
498
499 toFormattedSummaryJSON (this: MActorSummaryFormattable) {
500 let avatar: Avatar = null
501 if (this.Avatar) {
502 avatar = this.Avatar.toFormattedJSON()
503 }
504
505 return {
506 url: this.url,
507 name: this.preferredUsername,
508 host: this.getHost(),
509 avatar
510 }
511 }
512
513 toFormattedJSON (this: MActorFormattable) {
514 const base = this.toFormattedSummaryJSON()
515
516 return Object.assign(base, {
517 id: this.id,
518 hostRedundancyAllowed: this.getRedundancyAllowed(),
519 followingCount: this.followingCount,
520 followersCount: this.followersCount,
521 createdAt: this.createdAt,
522 updatedAt: this.updatedAt
523 })
524 }
525
526 toActivityPubObject (this: MActorAP, name: string) {
527 let icon: ActivityIconObject
528
529 if (this.avatarId) {
530 const extension = extname(this.Avatar.filename)
531
532 icon = {
533 type: 'Image',
534 mediaType: extension === '.png' ? 'image/png' : 'image/jpeg',
535 url: this.getAvatarUrl()
536 }
537 }
538
539 const json = {
540 type: this.type,
541 id: this.url,
542 following: this.getFollowingUrl(),
543 followers: this.getFollowersUrl(),
544 playlists: this.getPlaylistsUrl(),
545 inbox: this.inboxUrl,
546 outbox: this.outboxUrl,
547 preferredUsername: this.preferredUsername,
548 url: this.url,
549 name,
550 endpoints: {
551 sharedInbox: this.sharedInboxUrl
552 },
553 publicKey: {
554 id: this.getPublicKeyUrl(),
555 owner: this.url,
556 publicKeyPem: this.publicKey
557 },
558 icon
559 }
560
561 return activityPubContextify(json)
562 }
563
564 getFollowerSharedInboxUrls (t: Transaction) {
565 const query = {
566 attributes: [ 'sharedInboxUrl' ],
567 include: [
568 {
569 attribute: [],
570 model: ActorFollowModel.unscoped(),
571 required: true,
572 as: 'ActorFollowing',
573 where: {
574 state: 'accepted',
575 targetActorId: this.id
576 }
577 }
578 ],
579 transaction: t
580 }
581
582 return ActorModel.findAll(query)
583 .then(accounts => accounts.map(a => a.sharedInboxUrl))
584 }
585
586 getFollowingUrl () {
587 return this.url + '/following'
588 }
589
590 getFollowersUrl () {
591 return this.url + '/followers'
592 }
593
594 getPlaylistsUrl () {
595 return this.url + '/playlists'
596 }
597
598 getPublicKeyUrl () {
599 return this.url + '#main-key'
600 }
601
602 isOwned () {
603 return this.serverId === null
604 }
605
606 getWebfingerUrl (this: MActorServer) {
607 return 'acct:' + this.preferredUsername + '@' + this.getHost()
608 }
609
610 getIdentifier () {
611 return this.Server ? `${this.preferredUsername}@${this.Server.host}` : this.preferredUsername
612 }
613
614 getHost (this: MActorHost) {
615 return this.Server ? this.Server.host : WEBSERVER.HOST
616 }
617
618 getRedundancyAllowed () {
619 return this.Server ? this.Server.redundancyAllowed : false
620 }
621
622 getAvatarUrl () {
623 if (!this.avatarId) return undefined
624
625 return WEBSERVER.URL + this.Avatar.getStaticPath()
626 }
627
628 isOutdated () {
629 if (this.isOwned()) return false
630
631 return isOutdated(this, ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL)
632 }
633 }