]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/lib/activitypub/actor.ts
Use RsaSignature2017
[github/Chocobozzz/PeerTube.git] / server / lib / activitypub / actor.ts
CommitLineData
50d6de9c
C
1import * as Bluebird from 'bluebird'
2import { Transaction } from 'sequelize'
3import * as url from 'url'
4import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub'
5import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
6import { createPrivateAndPublicKeys, doRequest, logger, retryTransactionWrapper } from '../../helpers'
7import { isRemoteActorValid } from '../../helpers/custom-validators/activitypub'
8import { ACTIVITY_PUB, CONFIG, sequelizeTypescript } from '../../initializers'
9import { AccountModel } from '../../models/account/account'
10import { ActorModel } from '../../models/activitypub/actor'
11import { ServerModel } from '../../models/server/server'
12import { VideoChannelModel } from '../../models/video/video-channel'
13
14 // Set account keys, this could be long so process after the account creation and do not block the client
15function setAsyncActorKeys (actor: ActorModel) {
16 return createPrivateAndPublicKeys()
17 .then(({ publicKey, privateKey }) => {
18 actor.set('publicKey', publicKey)
19 actor.set('privateKey', privateKey)
20 return actor.save()
21 })
22 .catch(err => {
23 logger.error('Cannot set public/private keys of actor %d.', actor.uuid, err)
24 return actor
25 })
26}
27
28async function getOrCreateActorAndServerAndModel (actorUrl: string, recurseIfNeeded = true) {
29 let actor = await ActorModel.loadByUrl(actorUrl)
30
31 // We don't have this actor in our database, fetch it on remote
32 if (!actor) {
33 const result = await fetchRemoteActor(actorUrl)
34 if (result === undefined) throw new Error('Cannot fetch remote actor.')
35
36 // Create the attributed to actor
37 // In PeerTube a video channel is owned by an account
38 let ownerActor: ActorModel = undefined
39 if (recurseIfNeeded === true && result.actor.type === 'Group') {
40 const accountAttributedTo = result.attributedTo.find(a => a.type === 'Person')
41 if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actor.url)
42
43 try {
44 // Assert we don't recurse another time
45 ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, false)
46 } catch (err) {
47 logger.error('Cannot get or create account attributed to video channel ' + actor.url)
48 throw new Error(err)
49 }
50 }
51
52 const options = {
53 arguments: [ result, ownerActor ],
54 errorMessage: 'Cannot save actor and server with many retries.'
55 }
56 actor = await retryTransactionWrapper(saveActorAndServerAndModelIfNotExist, options)
57 }
58
59 return actor
60}
61
62function saveActorAndServerAndModelIfNotExist (
63 result: FetchRemoteActorResult,
64 ownerActor?: ActorModel,
65 t?: Transaction
66): Bluebird<ActorModel> | Promise<ActorModel> {
67 let actor = result.actor
68
69 if (t !== undefined) return save(t)
70
71 return sequelizeTypescript.transaction(t => save(t))
72
73 async function save (t: Transaction) {
74 const actorHost = url.parse(actor.url).host
75
76 const serverOptions = {
77 where: {
78 host: actorHost
79 },
80 defaults: {
81 host: actorHost
82 },
83 transaction: t
84 }
85 const [ server ] = await ServerModel.findOrCreate(serverOptions)
86
87 // Save our new account in database
88 actor.set('serverId', server.id)
89
90 // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists
91 // (which could be false in a retried query)
92 const actorCreated = await ActorModel.create(actor.toJSON(), { transaction: t })
93
94 if (actorCreated.type === 'Person' || actorCreated.type === 'Application') {
95 const account = await saveAccount(actorCreated, result, t)
96 actorCreated.Account = account
97 actorCreated.Account.Actor = actorCreated
98 } else if (actorCreated.type === 'Group') { // Video channel
99 const videoChannel = await saveVideoChannel(actorCreated, result, ownerActor, t)
100 actorCreated.VideoChannel = videoChannel
101 actorCreated.VideoChannel.Actor = actorCreated
102 }
103
104 return actorCreated
105 }
106}
107
108type FetchRemoteActorResult = {
109 actor: ActorModel
110 preferredUsername: string
111 summary: string
112 attributedTo: ActivityPubAttributedTo[]
113}
114async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResult> {
115 const options = {
116 uri: actorUrl,
117 method: 'GET',
118 headers: {
119 'Accept': ACTIVITY_PUB.ACCEPT_HEADER
120 }
121 }
122
123 logger.info('Fetching remote actor %s.', actorUrl)
124
125 let requestResult
126 try {
127 requestResult = await doRequest(options)
128 } catch (err) {
129 logger.warn('Cannot fetch remote actor %s.', actorUrl, err)
130 return undefined
131 }
132
133 const actorJSON: ActivityPubActor = JSON.parse(requestResult.body)
134 if (isRemoteActorValid(actorJSON) === false) {
135 logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON })
136 return undefined
137 }
138
139 const followersCount = await fetchActorTotalItems(actorJSON.followers)
140 const followingCount = await fetchActorTotalItems(actorJSON.following)
141
142 const actor = new ActorModel({
143 type: actorJSON.type,
144 uuid: actorJSON.uuid,
145 name: actorJSON.name,
146 url: actorJSON.url,
147 publicKey: actorJSON.publicKey.publicKeyPem,
148 privateKey: null,
149 followersCount: followersCount,
150 followingCount: followingCount,
151 inboxUrl: actorJSON.inbox,
152 outboxUrl: actorJSON.outbox,
153 sharedInboxUrl: actorJSON.endpoints.sharedInbox,
154 followersUrl: actorJSON.followers,
155 followingUrl: actorJSON.following
156 })
157
158 return {
159 actor,
160 preferredUsername: actorJSON.preferredUsername,
161 summary: actorJSON.summary,
162 attributedTo: actorJSON.attributedTo
163 }
164}
165
166function buildActorInstance (type: ActivityPubActorType, url: string, name: string, uuid?: string) {
167 return new ActorModel({
168 type,
169 url,
170 name,
171 uuid,
172 publicKey: null,
173 privateKey: null,
174 followersCount: 0,
175 followingCount: 0,
176 inboxUrl: url + '/inbox',
177 outboxUrl: url + '/outbox',
178 sharedInboxUrl: CONFIG.WEBSERVER.URL + '/inbox',
179 followersUrl: url + '/followers',
180 followingUrl: url + '/following'
181 })
182}
183
184export {
185 getOrCreateActorAndServerAndModel,
186 saveActorAndServerAndModelIfNotExist,
187 fetchRemoteActor,
188 buildActorInstance,
189 setAsyncActorKeys
190}
191
192// ---------------------------------------------------------------------------
193
194async function fetchActorTotalItems (url: string) {
195 const options = {
196 uri: url,
197 method: 'GET'
198 }
199
200 let requestResult
201 try {
202 requestResult = await doRequest(options)
203 } catch (err) {
204 logger.warn('Cannot fetch remote actor count %s.', url, err)
205 return undefined
206 }
207
208 return requestResult.totalItems ? requestResult.totalItems : 0
209}
210
211function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) {
212 const account = new AccountModel({
213 name: result.preferredUsername,
214 actorId: actor.id
215 })
216
217 return account.save({ transaction: t })
218}
219
220async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResult, ownerActor: ActorModel, t: Transaction) {
221 const videoChannel = new VideoChannelModel({
222 name: result.preferredUsername,
223 description: result.summary,
224 actorId: actor.id,
225 accountId: ownerActor.Account.id
226 })
227
228 return videoChannel.save({ transaction: t })
229}