import { logger } from '../../helpers/logger'
import { createReqFiles, getFormattedObjects } from '../../helpers/utils'
import { AVATAR_MIMETYPE_EXT, AVATARS_SIZE, CONFIG, sequelizeTypescript } from '../../initializers'
+import { updateActorAvatarInstance } from '../../lib/activitypub'
import { sendUpdateUser } from '../../lib/activitypub/send'
import { createUserAccountAndChannel } from '../../lib/user'
import {
import { usersUpdateMyAvatarValidator, videosSortValidator } from '../../middlewares/validators'
import { AccountVideoRateModel } from '../../models/account/account-video-rate'
import { UserModel } from '../../models/account/user'
-import { AvatarModel } from '../../models/avatar/avatar'
import { VideoModel } from '../../models/video/video'
const reqAvatarFile = createReqFiles('avatarfile', CONFIG.STORAGE.AVATARS_DIR, AVATAR_MIMETYPE_EXT)
await unlinkPromise(source)
- const { avatar } = await sequelizeTypescript.transaction(async t => {
- const avatar = await AvatarModel.create({
- filename: avatarName
- }, { transaction: t })
+ const avatar = await sequelizeTypescript.transaction(async t => {
+ await updateActorAvatarInstance(actor, avatarName, t)
- if (actor.Avatar) {
- try {
- await actor.Avatar.destroy({ transaction: t })
- } catch (err) {
- logger.error('Cannot remove old avatar of user %s.', user.username, err)
- }
- }
+ await sendUpdateUser(user, t)
- actor.set('avatarId', avatar.id)
- actor.Avatar = avatar
- await actor.save({ transaction: t })
-
- await sendUpdateUser(user, undefined)
-
- return { actor, avatar }
+ return avatar
})
return res
import * as retry from 'async/retry'
import * as Bluebird from 'bluebird'
+import { Model } from 'sequelize-typescript'
import { logger } from './logger'
type RetryTransactionWrapperOptions = { errorMessage: string, arguments?: any[] }
})
}
+function updateInstanceWithAnother <T> (instanceToUpdate: Model<T>, baseInstance: Model<T>) {
+ const obj = baseInstance.toJSON()
+
+ for (const key of Object.keys(obj)) {
+ instanceToUpdate.set(key, obj[key])
+ }
+}
+
// ---------------------------------------------------------------------------
export {
retryTransactionWrapper,
- transactionRetryer
+ transactionRetryer,
+ updateInstanceWithAnother
}
const actor = await ActorModel.loadByNameAndHost(name, host)
if (actor) return actor.url
+ return getUrlFromWebfinger(name, host)
+}
+
+async function getUrlFromWebfinger (name: string, host: string) {
const webfingerData: WebFingerData = await webfingerLookup(name + '@' + host)
return getLinkOrThrow(webfingerData)
}
// ---------------------------------------------------------------------------
export {
+ getUrlFromWebfinger,
loadActorUrlOrGetFromWebfinger
}
VIDEO: [ 'video/mp4', 'video/webm', 'video/ogg' ], // TODO: Merge with VIDEO_MIMETYPE_EXT
TORRENT: [ 'application/x-bittorrent' ],
MAGNET: [ 'application/x-bittorrent;x-scheme-handler/magnet' ]
- }
+ },
+ ACTOR_REFRESH_INTERVAL: 3600 * 24 // 1 day
}
const ACTIVITY_PUB_ACTOR_TYPES: { [ id: string ]: ActivityPubActorType } = {
REMOTE_SCHEME.WS = 'ws'
STATIC_MAX_AGE = '0'
ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE = 2
+ ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL = 60 // 1 minute
CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max = 100 * 1024 // 100KB
}
import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
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 { getUrlFromWebfinger } from '../../helpers/webfinger'
import { AVATAR_MIMETYPE_EXT, CONFIG, sequelizeTypescript } from '../../initializers'
import { AccountModel } from '../../models/account/account'
import { ActorModel } from '../../models/activitypub/actor'
actor = await retryTransactionWrapper(saveActorAndServerAndModelIfNotExist, options)
}
- return actor
+ return refreshActorIfNeeded(actor)
}
function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string, uuid?: string) {
})
}
+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,
buildActorInstance,
setAsyncActorKeys,
fetchActorTotalItems,
- fetchAvatarIfExists
+ fetchAvatarIfExists,
+ updateActorInstance,
+ updateActorAvatarInstance
}
// ---------------------------------------------------------------------------
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
+ })
+}
import { sequelizeTypescript } from '../../../initializers'
import { AccountModel } from '../../../models/account/account'
import { ActorModel } from '../../../models/activitypub/actor'
-import { AvatarModel } from '../../../models/avatar/avatar'
import { TagModel } from '../../../models/video/tag'
import { VideoModel } from '../../../models/video/video'
import { VideoFileModel } from '../../../models/video/video-file'
-import { fetchActorTotalItems, fetchAvatarIfExists, getOrCreateActorAndServerAndModel } from '../actor'
+import { fetchAvatarIfExists, getOrCreateActorAndServerAndModel, updateActorAvatarInstance, updateActorInstance } from '../actor'
import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc'
async function processUpdateActivity (activity: ActivityUpdate) {
const accountAttributesToUpdate = activity.object as ActivityPubActor
logger.debug('Updating remote account "%s".', accountAttributesToUpdate.uuid)
- let actorInstance: ActorModel
let accountInstance: AccountModel
let actorFieldsSave: object
let accountFieldsSave: object
try {
await sequelizeTypescript.transaction(async t => {
- actorInstance = await ActorModel.loadByUrl(accountAttributesToUpdate.id, t)
- if (!actorInstance) throw new Error('Actor ' + accountAttributesToUpdate.id + ' not found.')
-
- actorFieldsSave = actorInstance.toJSON()
- accountInstance = actorInstance.Account
- accountFieldsSave = actorInstance.Account.toJSON()
-
- const followersCount = await fetchActorTotalItems(accountAttributesToUpdate.followers)
- const followingCount = await fetchActorTotalItems(accountAttributesToUpdate.following)
-
- actorInstance.set('type', accountAttributesToUpdate.type)
- actorInstance.set('uuid', accountAttributesToUpdate.uuid)
- actorInstance.set('preferredUsername', accountAttributesToUpdate.preferredUsername)
- actorInstance.set('url', accountAttributesToUpdate.id)
- actorInstance.set('publicKey', accountAttributesToUpdate.publicKey.publicKeyPem)
- actorInstance.set('followersCount', followersCount)
- actorInstance.set('followingCount', followingCount)
- actorInstance.set('inboxUrl', accountAttributesToUpdate.inbox)
- actorInstance.set('outboxUrl', accountAttributesToUpdate.outbox)
- actorInstance.set('sharedInboxUrl', accountAttributesToUpdate.endpoints.sharedInbox)
- actorInstance.set('followersUrl', accountAttributesToUpdate.followers)
- actorInstance.set('followingUrl', accountAttributesToUpdate.following)
+ actorFieldsSave = actor.toJSON()
+ accountInstance = actor.Account
+ accountFieldsSave = actor.Account.toJSON()
- if (avatarName !== undefined) {
- if (actorInstance.avatarId) {
- await actorInstance.Avatar.destroy({ transaction: t })
- }
-
- const avatar = await AvatarModel.create({
- filename: avatarName
- }, { transaction: t })
+ await updateActorInstance(actor, accountAttributesToUpdate)
- actor.set('avatarId', avatar.id)
+ if (avatarName !== undefined) {
+ await updateActorAvatarInstance(actor, avatarName, t)
}
await actor.save({ transaction: t })
logger.info('Remote account with uuid %s updated', accountAttributesToUpdate.uuid)
} catch (err) {
- if (actorInstance !== undefined && actorFieldsSave !== undefined) {
- resetSequelizeInstance(actorInstance, actorFieldsSave)
+ if (actor !== undefined && actorFieldsSave !== undefined) {
+ resetSequelizeInstance(actor, actorFieldsSave)
}
if (accountInstance !== undefined && accountFieldsSave !== undefined) {
isActorPublicKeyValid
} from '../../helpers/custom-validators/activitypub/actor'
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
-import { ACTIVITY_PUB_ACTOR_TYPES, CONFIG, CONSTRAINTS_FIELDS } from '../../initializers'
+import { ACTIVITY_PUB, ACTIVITY_PUB_ACTOR_TYPES, CONFIG, CONSTRAINTS_FIELDS } from '../../initializers'
import { AccountModel } from '../account/account'
import { AvatarModel } from '../avatar/avatar'
import { ServerModel } from '../server/server'
return CONFIG.WEBSERVER.URL + this.Avatar.getWebserverPath()
}
+
+ isOutdated () {
+ if (this.isOwned()) return false
+
+ const now = Date.now()
+ const createdAtTime = this.createdAt.getTime()
+ const updatedAtTime = this.updatedAt.getTime()
+
+ return (now - createdAtTime) > ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL &&
+ (now - updatedAtTime) > ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL
+ }
}