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