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.create(actor.toJSON(), { transaction: t })
217 if (actorCreated.type === 'Person' || actorCreated.type === 'Application') {
218 const account = await saveAccount(actorCreated, result, t)
219 actorCreated.Account = account
220 actorCreated.Account.Actor = actorCreated
221 } else if (actorCreated.type === 'Group') { // Video channel
222 const videoChannel = await saveVideoChannel(actorCreated, result, ownerActor, t)
223 actorCreated.VideoChannel = videoChannel
224 actorCreated.VideoChannel.Actor = actorCreated
231 type FetchRemoteActorResult = {
236 attributedTo: ActivityPubAttributedTo[]
238 async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResult> {
246 logger.info('Fetching remote actor %s.', actorUrl)
248 const requestResult = await doRequest(options)
249 const actorJSON: ActivityPubActor = requestResult.body
251 if (isActorObjectValid(actorJSON) === false) {
252 logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON })
256 const followersCount = await fetchActorTotalItems(actorJSON.followers)
257 const followingCount = await fetchActorTotalItems(actorJSON.following)
259 const actor = new ActorModel({
260 type: actorJSON.type,
261 uuid: actorJSON.uuid,
262 preferredUsername: actorJSON.preferredUsername,
264 publicKey: actorJSON.publicKey.publicKeyPem,
266 followersCount: followersCount,
267 followingCount: followingCount,
268 inboxUrl: actorJSON.inbox,
269 outboxUrl: actorJSON.outbox,
270 sharedInboxUrl: actorJSON.endpoints.sharedInbox,
271 followersUrl: actorJSON.followers,
272 followingUrl: actorJSON.following
275 const avatarName = await fetchAvatarIfExists(actorJSON)
277 const name = actorJSON.name || actorJSON.preferredUsername
282 summary: actorJSON.summary,
283 attributedTo: actorJSON.attributedTo
287 function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) {
288 const account = new AccountModel({
293 return account.save({ transaction: t })
296 async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResult, ownerActor: ActorModel, t: Transaction) {
297 const videoChannel = new VideoChannelModel({
299 description: result.summary,
301 accountId: ownerActor.Account.id
304 return videoChannel.save({ transaction: t })
307 async function refreshActorIfNeeded (actor: ActorModel) {
308 if (!actor.isOutdated()) return actor
310 const actorUrl = await getUrlFromWebfinger(actor.preferredUsername, actor.getHost())
311 const result = await fetchRemoteActor(actorUrl)
312 if (result === undefined) throw new Error('Cannot fetch remote actor in refresh actor.')
314 return sequelizeTypescript.transaction(async t => {
315 updateInstanceWithAnother(actor, result.actor)
317 if (result.avatarName !== undefined) {
318 await updateActorAvatarInstance(actor, result.avatarName, t)
322 actor.setDataValue('updatedAt', new Date())
323 await actor.save({ transaction: t })
326 await actor.save({ transaction: t })
328 actor.Account.set('name', result.name)
329 await actor.Account.save({ transaction: t })
330 } else if (actor.VideoChannel) {
331 await actor.save({ transaction: t })
333 actor.VideoChannel.set('name', result.name)
334 await actor.VideoChannel.save({ transaction: t })