+ actor = await retryTransactionWrapper(saveActorAndServerAndModelIfNotExist, result, ownerActor)
+ created = true
+ accountPlaylistsUrl = result.playlists
+ }
+
+ if (actor.Account) actor.Account.Actor = actor
+ if (actor.VideoChannel) actor.VideoChannel.Actor = actor
+
+ const { actor: actorRefreshed, refreshed } = await retryTransactionWrapper(refreshActorIfNeeded, actor, fetchType)
+ if (!actorRefreshed) throw new Error('Actor ' + actorRefreshed.url + ' does not exist anymore.')
+
+ if ((created === true || refreshed === true) && updateCollections === true) {
+ const payload = { uri: actor.outboxUrl, type: 'activity' as 'activity' }
+ await JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
+ }
+
+ // We created a new account: fetch the playlists
+ if (created === true && actor.Account && accountPlaylistsUrl) {
+ const payload = { uri: accountPlaylistsUrl, accountId: actor.Account.id, type: 'account-playlists' as 'account-playlists' }
+ await JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
+ }
+
+ return actorRefreshed
+}
+
+function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string, uuid?: string) {
+ return new ActorModel({
+ type,
+ url,
+ preferredUsername,
+ uuid,
+ publicKey: null,
+ privateKey: null,
+ followersCount: 0,
+ followingCount: 0,
+ inboxUrl: url + '/inbox',
+ outboxUrl: url + '/outbox',
+ sharedInboxUrl: WEBSERVER.URL + '/inbox',
+ followersUrl: url + '/followers',
+ followingUrl: url + '/following'
+ })
+}
+
+async function updateActorInstance (actorInstance: ActorModel, attributes: ActivityPubActor) {
+ const followersCount = await fetchActorTotalItems(attributes.followers)
+ const followingCount = await fetchActorTotalItems(attributes.following)
+
+ actorInstance.type = attributes.type
+ actorInstance.preferredUsername = attributes.preferredUsername
+ actorInstance.url = attributes.id
+ actorInstance.publicKey = attributes.publicKey.publicKeyPem
+ actorInstance.followersCount = followersCount
+ actorInstance.followingCount = followingCount
+ actorInstance.inboxUrl = attributes.inbox
+ actorInstance.outboxUrl = attributes.outbox
+ actorInstance.sharedInboxUrl = attributes.endpoints.sharedInbox
+ actorInstance.followersUrl = attributes.followers
+ actorInstance.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
+ }
+
+ try {
+ const { body } = await doRequest(options)
+ return body.totalItems ? body.totalItems : 0
+ } catch (err) {
+ logger.warn('Cannot fetch remote actor count %s.', url, { err })
+ return 0
+ }
+}
+
+async function fetchAvatarIfExists (actorJSON: ActivityPubActor) {
+ if (
+ actorJSON.icon && actorJSON.icon.type === 'Image' && MIMETYPES.IMAGE.MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined &&
+ isActivityPubUrlValid(actorJSON.icon.url)
+ ) {
+ const extension = MIMETYPES.IMAGE.MIMETYPE_EXT[actorJSON.icon.mediaType]
+
+ const avatarName = uuidv4() + extension
+ await downloadImage(actorJSON.icon.url, CONFIG.STORAGE.AVATARS_DIR, avatarName, AVATARS_SIZE)
+
+ return avatarName
+ }
+
+ return undefined
+}
+
+async function addFetchOutboxJob (actor: Pick<ActorModel, 'id' | 'outboxUrl'>) {
+ // Don't fetch ourselves
+ const serverActor = await getServerActor()
+ if (serverActor.id === actor.id) {
+ logger.error('Cannot fetch our own outbox!')
+ return undefined
+ }
+
+ const payload = {
+ uri: actor.outboxUrl,
+ type: 'activity' as 'activity'
+ }
+
+ return JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
+}
+
+async function refreshActorIfNeeded (
+ actorArg: ActorModel,
+ fetchedType: ActorFetchByUrlType
+): Promise<{ actor: ActorModel, refreshed: boolean }> {
+ if (!actorArg.isOutdated()) return { actor: actorArg, refreshed: false }
+
+ // We need more attributes
+ const actor = fetchedType === 'all' ? actorArg : await ActorModel.loadByUrlAndPopulateAccountAndChannel(actorArg.url)
+
+ try {
+ let actorUrl: string
+ try {
+ actorUrl = await getUrlFromWebfinger(actor.preferredUsername + '@' + actor.getHost())
+ } catch (err) {
+ logger.warn('Cannot get actor URL from webfinger, keeping the old one.', err)
+ actorUrl = actor.url
+ }
+
+ const { result, statusCode } = await fetchRemoteActor(actorUrl)
+
+ if (statusCode === 404) {
+ logger.info('Deleting actor %s because there is a 404 in refresh actor.', actor.url)
+ actor.Account ? actor.Account.destroy() : actor.VideoChannel.destroy()
+ return { actor: undefined, refreshed: false }