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) {
136 const { body } = await doRequest(options)
137 return body.totalItems ? body.totalItems : 0
139 logger.warn('Cannot fetch remote actor count %s.', url, err)
144 async function fetchAvatarIfExists (actorJSON: ActivityPubActor) {
146 actorJSON.icon && actorJSON.icon.type === 'Image' && AVATAR_MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined &&
147 isActivityPubUrlValid(actorJSON.icon.url)
149 const extension = AVATAR_MIMETYPE_EXT[actorJSON.icon.mediaType]
151 const avatarName = uuidv4() + extension
152 const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)
154 await doRequestAndSaveToFile({
156 uri: actorJSON.icon.url
166 getOrCreateActorAndServerAndModel,
169 fetchActorTotalItems,
172 updateActorAvatarInstance
175 // ---------------------------------------------------------------------------
177 function saveActorAndServerAndModelIfNotExist (
178 result: FetchRemoteActorResult,
179 ownerActor?: ActorModel,
181 ): Bluebird<ActorModel> | Promise<ActorModel> {
182 let actor = result.actor
184 if (t !== undefined) return save(t)
186 return sequelizeTypescript.transaction(t => save(t))
188 async function save (t: Transaction) {
189 const actorHost = url.parse(actor.url).host
191 const serverOptions = {
200 const [ server ] = await ServerModel.findOrCreate(serverOptions)
202 // Save our new account in database
203 actor.set('serverId', server.id)
206 if (result.avatarName) {
207 const avatar = await AvatarModel.create({
208 filename: result.avatarName
209 }, { transaction: t })
210 actor.set('avatarId', avatar.id)
213 // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists
214 // (which could be false in a retried query)
215 const [ actorCreated ] = await ActorModel.findOrCreate({
216 defaults: actor.toJSON(),
223 if (actorCreated.type === 'Person' || actorCreated.type === 'Application') {
224 const account = await saveAccount(actorCreated, result, t)
225 actorCreated.Account = account
226 actorCreated.Account.Actor = actorCreated
227 } else if (actorCreated.type === 'Group') { // Video channel
228 const videoChannel = await saveVideoChannel(actorCreated, result, ownerActor, t)
229 actorCreated.VideoChannel = videoChannel
230 actorCreated.VideoChannel.Actor = actorCreated
237 type FetchRemoteActorResult = {
242 attributedTo: ActivityPubAttributedTo[]
244 async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResult> {
252 logger.info('Fetching remote actor %s.', actorUrl)
254 const requestResult = await doRequest(options)
255 const actorJSON: ActivityPubActor = requestResult.body
257 if (isActorObjectValid(actorJSON) === false) {
258 logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON })
262 const followersCount = await fetchActorTotalItems(actorJSON.followers)
263 const followingCount = await fetchActorTotalItems(actorJSON.following)
265 const actor = new ActorModel({
266 type: actorJSON.type,
267 uuid: actorJSON.uuid,
268 preferredUsername: actorJSON.preferredUsername,
270 publicKey: actorJSON.publicKey.publicKeyPem,
272 followersCount: followersCount,
273 followingCount: followingCount,
274 inboxUrl: actorJSON.inbox,
275 outboxUrl: actorJSON.outbox,
276 sharedInboxUrl: actorJSON.endpoints.sharedInbox,
277 followersUrl: actorJSON.followers,
278 followingUrl: actorJSON.following
281 const avatarName = await fetchAvatarIfExists(actorJSON)
283 const name = actorJSON.name || actorJSON.preferredUsername
288 summary: actorJSON.summary,
289 attributedTo: actorJSON.attributedTo
293 async function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) {
294 const [ accountCreated ] = await AccountModel.findOrCreate({
305 return accountCreated
308 async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResult, ownerActor: ActorModel, t: Transaction) {
309 const [ videoChannelCreated ] = await VideoChannelModel.findOrCreate({
312 description: result.summary,
314 accountId: ownerActor.Account.id
322 return videoChannelCreated
325 async function refreshActorIfNeeded (actor: ActorModel) {
326 if (!actor.isOutdated()) return actor
328 const actorUrl = await getUrlFromWebfinger(actor.preferredUsername, actor.getHost())
329 const result = await fetchRemoteActor(actorUrl)
330 if (result === undefined) {
331 logger.warn('Cannot fetch remote actor in refresh actor.')
335 return sequelizeTypescript.transaction(async t => {
336 updateInstanceWithAnother(actor, result.actor)
338 if (result.avatarName !== undefined) {
339 await updateActorAvatarInstance(actor, result.avatarName, t)
343 actor.setDataValue('updatedAt', new Date())
344 await actor.save({ transaction: t })
347 await actor.save({ transaction: t })
349 actor.Account.set('name', result.name)
350 await actor.Account.save({ transaction: t })
351 } else if (actor.VideoChannel) {
352 await actor.save({ transaction: t })
354 actor.VideoChannel.set('name', result.name)
355 await actor.VideoChannel.save({ transaction: t })