Send comment to followers and parents
[oweals/peertube.git] / server / lib / activitypub / actor.ts
index e557896e854ed273cab559e741c7c5a2624b293e..0882ab843e690ad4bd69077c2ce51900747ec36d 100644 (file)
@@ -5,13 +5,14 @@ import * as url from 'url'
 import * as uuidv4 from 'uuid/v4'
 import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub'
 import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
-import { isRemoteActorValid } from '../../helpers/custom-validators/activitypub/actor'
+import { isActorObjectValid } from '../../helpers/custom-validators/activitypub/actor'
 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
-import { retryTransactionWrapper } from '../../helpers/database-utils'
+import { retryTransactionWrapper, updateInstanceWithAnother } from '../../helpers/database-utils'
 import { logger } from '../../helpers/logger'
 import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto'
 import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
-import { CONFIG, sequelizeTypescript } from '../../initializers'
+import { getUrlFromWebfinger } from '../../helpers/webfinger'
+import { AVATAR_MIMETYPE_EXT, CONFIG, sequelizeTypescript } from '../../initializers'
 import { AccountModel } from '../../models/account/account'
 import { ActorModel } from '../../models/activitypub/actor'
 import { AvatarModel } from '../../models/avatar/avatar'
@@ -63,7 +64,7 @@ async function getOrCreateActorAndServerAndModel (actorUrl: string, recurseIfNee
     actor = await retryTransactionWrapper(saveActorAndServerAndModelIfNotExist, options)
   }
 
-  return actor
+  return refreshActorIfNeeded(actor)
 }
 
 function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string, uuid?: string) {
@@ -84,10 +85,93 @@ function buildActorInstance (type: ActivityPubActorType, url: string, preferredU
   })
 }
 
+async function updateActorInstance (actorInstance: ActorModel, attributes: ActivityPubActor) {
+  const followersCount = await fetchActorTotalItems(attributes.followers)
+  const followingCount = await fetchActorTotalItems(attributes.following)
+
+  actorInstance.set('type', attributes.type)
+  actorInstance.set('uuid', attributes.uuid)
+  actorInstance.set('preferredUsername', attributes.preferredUsername)
+  actorInstance.set('url', attributes.id)
+  actorInstance.set('publicKey', attributes.publicKey.publicKeyPem)
+  actorInstance.set('followersCount', followersCount)
+  actorInstance.set('followingCount', followingCount)
+  actorInstance.set('inboxUrl', attributes.inbox)
+  actorInstance.set('outboxUrl', attributes.outbox)
+  actorInstance.set('sharedInboxUrl', attributes.endpoints.sharedInbox)
+  actorInstance.set('followersUrl', attributes.followers)
+  actorInstance.set('followingUrl', attributes.following)
+}
+
+async function updateActorAvatarInstance (actorInstance: ActorModel, avatarName: string, t: Transaction) {
+  if (avatarName !== undefined) {
+    if (actorInstance.avatarId) {
+      try {
+        await actorInstance.Avatar.destroy({ transaction: t })
+      } catch (err) {
+        logger.error('Cannot remove old avatar of actor %s.', actorInstance.url, err)
+      }
+    }
+
+    const avatar = await AvatarModel.create({
+      filename: avatarName
+    }, { transaction: t })
+
+    actorInstance.set('avatarId', avatar.id)
+    actorInstance.Avatar = avatar
+  }
+
+  return actorInstance
+}
+
+async function fetchActorTotalItems (url: string) {
+  const options = {
+    uri: url,
+    method: 'GET',
+    json: true,
+    activityPub: true
+  }
+
+  let requestResult
+  try {
+    requestResult = await doRequest(options)
+  } catch (err) {
+    logger.warn('Cannot fetch remote actor count %s.', url, err)
+    return undefined
+  }
+
+  return requestResult.totalItems ? requestResult.totalItems : 0
+}
+
+async function fetchAvatarIfExists (actorJSON: ActivityPubActor) {
+  if (
+    actorJSON.icon && actorJSON.icon.type === 'Image' && AVATAR_MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined &&
+    isActivityPubUrlValid(actorJSON.icon.url)
+  ) {
+    const extension = AVATAR_MIMETYPE_EXT[actorJSON.icon.mediaType]
+
+    const avatarName = uuidv4() + extension
+    const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)
+
+    await doRequestAndSaveToFile({
+      method: 'GET',
+      uri: actorJSON.icon.url
+    }, destPath)
+
+    return avatarName
+  }
+
+  return undefined
+}
+
 export {
   getOrCreateActorAndServerAndModel,
   buildActorInstance,
-  setAsyncActorKeys
+  setAsyncActorKeys,
+  fetchActorTotalItems,
+  fetchAvatarIfExists,
+  updateActorInstance,
+  updateActorAvatarInstance
 }
 
 // ---------------------------------------------------------------------------
@@ -166,7 +250,7 @@ async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResu
   const requestResult = await doRequest(options)
   const actorJSON: ActivityPubActor = requestResult.body
 
-  if (isRemoteActorValid(actorJSON) === false) {
+  if (isActorObjectValid(actorJSON) === false) {
     logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON })
     return undefined
   }
@@ -190,22 +274,7 @@ async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResu
     followingUrl: actorJSON.following
   })
 
-  // Fetch icon?
-  let avatarName: string = undefined
-  if (
-    actorJSON.icon && actorJSON.icon.type === 'Image' && actorJSON.icon.mediaType === 'image/png' &&
-    isActivityPubUrlValid(actorJSON.icon.url)
-  ) {
-    const extension = actorJSON.icon.mediaType === 'image/png' ? '.png' : '.jpg'
-
-    avatarName = uuidv4() + extension
-    const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)
-
-    await doRequestAndSaveToFile({
-      method: 'GET',
-      uri: actorJSON.icon.url
-    }, destPath)
-  }
+  const avatarName = await fetchAvatarIfExists(actorJSON)
 
   const name = actorJSON.name || actorJSON.preferredUsername
   return {
@@ -217,25 +286,6 @@ async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResu
   }
 }
 
-async function fetchActorTotalItems (url: string) {
-  const options = {
-    uri: url,
-    method: 'GET',
-    json: true,
-    activityPub: true
-  }
-
-  let requestResult
-  try {
-    requestResult = await doRequest(options)
-  } catch (err) {
-    logger.warn('Cannot fetch remote actor count %s.', url, err)
-    return undefined
-  }
-
-  return requestResult.totalItems ? requestResult.totalItems : 0
-}
-
 function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) {
   const account = new AccountModel({
     name: result.name,
@@ -255,3 +305,35 @@ async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResu
 
   return videoChannel.save({ transaction: t })
 }
+
+async function refreshActorIfNeeded (actor: ActorModel) {
+  if (!actor.isOutdated()) return actor
+
+  const actorUrl = await getUrlFromWebfinger(actor.preferredUsername, actor.getHost())
+  const result = await fetchRemoteActor(actorUrl)
+  if (result === undefined) throw new Error('Cannot fetch remote actor in refresh actor.')
+
+  return sequelizeTypescript.transaction(async t => {
+    updateInstanceWithAnother(actor, result.actor)
+
+    if (result.avatarName !== undefined) {
+      await updateActorAvatarInstance(actor, result.avatarName, t)
+    }
+
+    await actor.save({ transaction: t })
+
+    if (actor.Account) {
+      await actor.save({ transaction: t })
+
+      actor.Account.set('name', result.name)
+      await actor.Account.save({ transaction: t })
+    } else if (actor.VideoChannel) {
+      await actor.save({ transaction: t })
+
+      actor.VideoChannel.set('name', result.name)
+      await actor.VideoChannel.save({ transaction: t })
+    }
+
+    return actor
+  })
+}