1 import * as Bluebird from 'bluebird'
2 import { join } from 'path'
3 import { Transaction } from 'sequelize'
4 import * as url from 'url'
5 import * as uuidv4 from 'uuid/v4'
6 import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub'
7 import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
8 import { isActorObjectValid } from '../../helpers/custom-validators/activitypub/actor'
9 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
10 import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils'
11 import { logger } from '../../helpers/logger'
12 import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto'
13 import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
14 import { getUrlFromWebfinger } from '../../helpers/webfinger'
15 import { AVATAR_MIMETYPE_EXT, CONFIG, sequelizeTypescript } from '../../initializers'
16 import { AccountModel } from '../../models/account/account'
17 import { ActorModel } from '../../models/activitypub/actor'
18 import { AvatarModel } from '../../models/avatar/avatar'
19 import { ServerModel } from '../../models/server/server'
20 import { VideoChannelModel } from '../../models/video/video-channel'
22 // Set account keys, this could be long so process after the account creation and do not block the client
23 function setAsyncActorKeys (actor: ActorModel) {
24 return createPrivateAndPublicKeys()
25 .then(({ publicKey, privateKey }) => {
26 actor.set('publicKey', publicKey)
27 actor.set('privateKey', privateKey)
31 logger.error('Cannot set public/private keys of actor %d.', actor.uuid, err)
36 async function getOrCreateActorAndServerAndModel (actorUrl: string, recurseIfNeeded = true) {
37 let actor = await ActorModel.loadByUrl(actorUrl)
39 // We don't have this actor in our database, fetch it on remote
41 const result = await fetchRemoteActor(actorUrl)
42 if (result === undefined) throw new Error('Cannot fetch remote actor.')
44 // Create the attributed to actor
45 // In PeerTube a video channel is owned by an account
46 let ownerActor: ActorModel = undefined
47 if (recurseIfNeeded === true && result.actor.type === 'Group') {
48 const accountAttributedTo = result.attributedTo.find(a => a.type === 'Person')
49 if (!accountAttributedTo) throw new Error('Cannot find account attributed to video channel ' + actor.url)
52 // Assert we don't recurse another time
53 ownerActor = await getOrCreateActorAndServerAndModel(accountAttributedTo.id, false)
55 logger.error('Cannot get or create account attributed to video channel ' + actor.url)
61 arguments: [ result, ownerActor ],
62 errorMessage: 'Cannot save actor and server with many retries.'
64 actor = await retryTransactionWrapper(saveActorAndServerAndModelIfNotExist, options)
67 return refreshActorIfNeeded(actor)
70 function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string, uuid?: string) {
71 return new ActorModel({
80 inboxUrl: url + '/inbox',
81 outboxUrl: url + '/outbox',
82 sharedInboxUrl: CONFIG.WEBSERVER.URL + '/inbox',
83 followersUrl: url + '/followers',
84 followingUrl: url + '/following'
88 async function updateActorInstance (actorInstance: ActorModel, attributes: ActivityPubActor) {
89 const followersCount = await fetchActorTotalItems(attributes.followers)
90 const followingCount = await fetchActorTotalItems(attributes.following)
92 actorInstance.set('type', attributes.type)
93 actorInstance.set('uuid', attributes.uuid)
94 actorInstance.set('preferredUsername', attributes.preferredUsername)
95 actorInstance.set('url', attributes.id)
96 actorInstance.set('publicKey', attributes.publicKey.publicKeyPem)
97 actorInstance.set('followersCount', followersCount)
98 actorInstance.set('followingCount', followingCount)
99 actorInstance.set('inboxUrl', attributes.inbox)
100 actorInstance.set('outboxUrl', attributes.outbox)
101 actorInstance.set('sharedInboxUrl', attributes.endpoints.sharedInbox)
102 actorInstance.set('followersUrl', attributes.followers)
103 actorInstance.set('followingUrl', attributes.following)
106 async function updateActorAvatarInstance (actorInstance: ActorModel, avatarName: string, t: Transaction) {
107 if (avatarName !== undefined) {
108 if (actorInstance.avatarId) {
110 await actorInstance.Avatar.destroy({ transaction: t })
112 logger.error('Cannot remove old avatar of actor %s.', actorInstance.url, err)
116 const avatar = await AvatarModel.create({
118 }, { transaction: t })
120 actorInstance.set('avatarId', avatar.id)
121 actorInstance.Avatar = avatar
127 async function fetchActorTotalItems (url: string) {
137 requestResult = await doRequest(options)
139 logger.warn('Cannot fetch remote actor count %s.', url, err)
143 return requestResult.totalItems ? requestResult.totalItems : 0
146 async function fetchAvatarIfExists (actorJSON: ActivityPubActor) {
148 actorJSON.icon && actorJSON.icon.type === 'Image' && AVATAR_MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined &&
149 isActivityPubUrlValid(actorJSON.icon.url)
151 const extension = AVATAR_MIMETYPE_EXT[actorJSON.icon.mediaType]
153 const avatarName = uuidv4() + extension
154 const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)
156 await doRequestAndSaveToFile({
158 uri: actorJSON.icon.url
168 getOrCreateActorAndServerAndModel,
171 fetchActorTotalItems,
174 updateActorAvatarInstance
177 // ---------------------------------------------------------------------------
179 function saveActorAndServerAndModelIfNotExist (
180 result: FetchRemoteActorResult,
181 ownerActor?: ActorModel,
183 ): Bluebird<ActorModel> | Promise<ActorModel> {
184 let actor = result.actor
186 if (t !== undefined) return save(t)
188 return sequelizeTypescript.transaction(t => save(t))
190 async function save (t: Transaction) {
191 const actorHost = url.parse(actor.url).host
193 const serverOptions = {
202 const [ server ] = await ServerModel.findOrCreate(serverOptions)
204 // Save our new account in database
205 actor.set('serverId', server.id)
208 if (result.avatarName) {
209 const avatar = await AvatarModel.create({
210 filename: result.avatarName
211 }, { transaction: t })
212 actor.set('avatarId', avatar.id)
215 // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists
216 // (which could be false in a retried query)
217 const actorCreated = await ActorModel.create(actor.toJSON(), { transaction: t })
219 if (actorCreated.type === 'Person' || actorCreated.type === 'Application') {
220 const account = await saveAccount(actorCreated, result, t)
221 actorCreated.Account = account
222 actorCreated.Account.Actor = actorCreated
223 } else if (actorCreated.type === 'Group') { // Video channel
224 const videoChannel = await saveVideoChannel(actorCreated, result, ownerActor, t)
225 actorCreated.VideoChannel = videoChannel
226 actorCreated.VideoChannel.Actor = actorCreated
233 type FetchRemoteActorResult = {
238 attributedTo: ActivityPubAttributedTo[]
240 async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResult> {
248 logger.info('Fetching remote actor %s.', actorUrl)
250 const requestResult = await doRequest(options)
251 const actorJSON: ActivityPubActor = requestResult.body
253 if (isActorObjectValid(actorJSON) === false) {
254 logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON })
258 const followersCount = await fetchActorTotalItems(actorJSON.followers)
259 const followingCount = await fetchActorTotalItems(actorJSON.following)
261 const actor = new ActorModel({
262 type: actorJSON.type,
263 uuid: actorJSON.uuid,
264 preferredUsername: actorJSON.preferredUsername,
266 publicKey: actorJSON.publicKey.publicKeyPem,
268 followersCount: followersCount,
269 followingCount: followingCount,
270 inboxUrl: actorJSON.inbox,
271 outboxUrl: actorJSON.outbox,
272 sharedInboxUrl: actorJSON.endpoints.sharedInbox,
273 followersUrl: actorJSON.followers,
274 followingUrl: actorJSON.following
277 const avatarName = await fetchAvatarIfExists(actorJSON)
279 const name = actorJSON.name || actorJSON.preferredUsername
284 summary: actorJSON.summary,
285 attributedTo: actorJSON.attributedTo
289 function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) {
290 const account = new AccountModel({
295 return account.save({ transaction: t })
298 async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResult, ownerActor: ActorModel, t: Transaction) {
299 const videoChannel = new VideoChannelModel({
301 description: result.summary,
303 accountId: ownerActor.Account.id
306 return videoChannel.save({ transaction: t })
309 async function refreshActorIfNeeded (actor: ActorModel) {
310 if (!actor.isOutdated()) return actor
312 const actorUrl = await getUrlFromWebfinger(actor.preferredUsername, actor.getHost())
313 const result = await fetchRemoteActor(actorUrl)
314 if (result === undefined) throw new Error('Cannot fetch remote actor in refresh actor.')
316 return sequelizeTypescript.transaction(async t => {
317 updateInstanceWithAnother(actor, result.actor)
319 if (result.avatarName !== undefined) {
320 await updateActorAvatarInstance(actor, result.avatarName, t)
324 actor.setDataValue('updatedAt', new Date())
325 await actor.save({ transaction: t })
328 await actor.save({ transaction: t })
330 actor.Account.set('name', result.name)
331 await actor.Account.save({ transaction: t })
332 } else if (actor.VideoChannel) {
333 await actor.save({ transaction: t })
335 actor.VideoChannel.set('name', result.name)
336 await actor.VideoChannel.save({ transaction: t })