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