aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib/activitypub/actors/shared/creator.ts
blob: 999aed97d862cb1f6463ff9768e51a9fb077ab60 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
import { Op, Transaction } from 'sequelize'
import { sequelizeTypescript } from '@server/initializers/database'
import { AccountModel } from '@server/models/account/account'
import { ActorModel } from '@server/models/actor/actor'
import { ServerModel } from '@server/models/server/server'
import { VideoChannelModel } from '@server/models/video/video-channel'
import { MAccount, MAccountDefault, MActor, MActorFullActor, MActorId, MActorImages, MChannel, MServer } from '@server/types/models'
import { ActivityPubActor, ActorImageType } from '@shared/models'
import { updateActorImageInstance } from '../image'
import { getActorAttributesFromObject, getActorDisplayNameFromObject, getImageInfoFromObject } from './object-to-model-attributes'
import { fetchActorFollowsCount } from './url-to-object'

export class APActorCreator {

  constructor (
    private readonly actorObject: ActivityPubActor,
    private readonly ownerActor?: MActorFullActor
  ) {

  }

  async create (): Promise<MActorFullActor> {
    const { followersCount, followingCount } = await fetchActorFollowsCount(this.actorObject)

    const actorInstance = new ActorModel(getActorAttributesFromObject(this.actorObject, followersCount, followingCount))

    return sequelizeTypescript.transaction(async t => {
      const server = await this.setServer(actorInstance, t)

      await this.setImageIfNeeded(actorInstance, ActorImageType.AVATAR, t)
      await this.setImageIfNeeded(actorInstance, ActorImageType.BANNER, t)

      const { actorCreated, created } = await this.saveActor(actorInstance, t)

      await this.tryToFixActorUrlIfNeeded(actorCreated, actorInstance, created, t)

      if (actorCreated.type === 'Person' || actorCreated.type === 'Application') { // Account or PeerTube instance
        actorCreated.Account = await this.saveAccount(actorCreated, t) as MAccountDefault
        actorCreated.Account.Actor = actorCreated
      }

      if (actorCreated.type === 'Group') { // Video channel
        const channel = await this.saveVideoChannel(actorCreated, t)
        actorCreated.VideoChannel = Object.assign(channel, { Actor: actorCreated, Account: this.ownerActor.Account })
      }

      actorCreated.Server = server

      return actorCreated
    })
  }

  private async setServer (actor: MActor, t: Transaction) {
    const actorHost = new URL(actor.url).host

    const serverOptions = {
      where: {
        host: actorHost
      },
      defaults: {
        host: actorHost
      },
      transaction: t
    }
    const [ server ] = await ServerModel.findOrCreate(serverOptions)

    // Save our new account in database
    actor.serverId = server.id

    return server as MServer
  }

  private async setImageIfNeeded (actor: MActor, type: ActorImageType, t: Transaction) {
    const imageInfo = getImageInfoFromObject(this.actorObject, type)
    if (!imageInfo) return

    return updateActorImageInstance(actor as MActorImages, type, imageInfo, t)
  }

  private async saveActor (actor: MActor, t: Transaction) {
    // Force the actor creation using findOrCreate() instead of save()
    // Sometimes Sequelize skips the save() when it thinks the instance already exists
    // (which could be false in a retried query)
    const [ actorCreated, created ] = await ActorModel.findOrCreate<MActorFullActor>({
      defaults: actor.toJSON(),
      where: {
        [Op.or]: [
          {
            url: actor.url
          },
          {
            serverId: actor.serverId,
            preferredUsername: actor.preferredUsername
          }
        ]
      },
      transaction: t
    })

    return { actorCreated, created }
  }

  private async tryToFixActorUrlIfNeeded (actorCreated: MActor, newActor: MActor, created: boolean, t: Transaction) {
    // Try to fix non HTTPS accounts of remote instances that fixed their URL afterwards
    if (created !== true && actorCreated.url !== newActor.url) {
      // Only fix http://example.com/account/djidane to https://example.com/account/djidane
      if (actorCreated.url.replace(/^http:\/\//, '') !== newActor.url.replace(/^https:\/\//, '')) {
        throw new Error(`Actor from DB with URL ${actorCreated.url} does not correspond to actor ${newActor.url}`)
      }

      actorCreated.url = newActor.url
      await actorCreated.save({ transaction: t })
    }
  }

  private async saveAccount (actor: MActorId, t: Transaction) {
    const [ accountCreated ] = await AccountModel.findOrCreate({
      defaults: {
        name: getActorDisplayNameFromObject(this.actorObject),
        description: this.actorObject.summary,
        actorId: actor.id
      },
      where: {
        actorId: actor.id
      },
      transaction: t
    })

    return accountCreated as MAccount
  }

  private async saveVideoChannel (actor: MActorId, t: Transaction) {
    const [ videoChannelCreated ] = await VideoChannelModel.findOrCreate({
      defaults: {
        name: getActorDisplayNameFromObject(this.actorObject),
        description: this.actorObject.summary,
        support: this.actorObject.support,
        actorId: actor.id,
        accountId: this.ownerActor.Account.id
      },
      where: {
        actorId: actor.id
      },
      transaction: t
    })

    return videoChannelCreated as MChannel
  }
}