Refresh remote accounts
This commit is contained in:
parent
4cb6d45788
commit
a5625b4167
|
@ -8,6 +8,7 @@ import { retryTransactionWrapper } from '../../helpers/database-utils'
|
||||||
import { logger } from '../../helpers/logger'
|
import { logger } from '../../helpers/logger'
|
||||||
import { createReqFiles, getFormattedObjects } from '../../helpers/utils'
|
import { createReqFiles, getFormattedObjects } from '../../helpers/utils'
|
||||||
import { AVATAR_MIMETYPE_EXT, AVATARS_SIZE, CONFIG, sequelizeTypescript } from '../../initializers'
|
import { AVATAR_MIMETYPE_EXT, AVATARS_SIZE, CONFIG, sequelizeTypescript } from '../../initializers'
|
||||||
|
import { updateActorAvatarInstance } from '../../lib/activitypub'
|
||||||
import { sendUpdateUser } from '../../lib/activitypub/send'
|
import { sendUpdateUser } from '../../lib/activitypub/send'
|
||||||
import { createUserAccountAndChannel } from '../../lib/user'
|
import { createUserAccountAndChannel } from '../../lib/user'
|
||||||
import {
|
import {
|
||||||
|
@ -18,7 +19,6 @@ import {
|
||||||
import { usersUpdateMyAvatarValidator, videosSortValidator } from '../../middlewares/validators'
|
import { usersUpdateMyAvatarValidator, videosSortValidator } from '../../middlewares/validators'
|
||||||
import { AccountVideoRateModel } from '../../models/account/account-video-rate'
|
import { AccountVideoRateModel } from '../../models/account/account-video-rate'
|
||||||
import { UserModel } from '../../models/account/user'
|
import { UserModel } from '../../models/account/user'
|
||||||
import { AvatarModel } from '../../models/avatar/avatar'
|
|
||||||
import { VideoModel } from '../../models/video/video'
|
import { VideoModel } from '../../models/video/video'
|
||||||
|
|
||||||
const reqAvatarFile = createReqFiles('avatarfile', CONFIG.STORAGE.AVATARS_DIR, AVATAR_MIMETYPE_EXT)
|
const reqAvatarFile = createReqFiles('avatarfile', CONFIG.STORAGE.AVATARS_DIR, AVATAR_MIMETYPE_EXT)
|
||||||
|
@ -248,26 +248,12 @@ async function updateMyAvatar (req: express.Request, res: express.Response, next
|
||||||
|
|
||||||
await unlinkPromise(source)
|
await unlinkPromise(source)
|
||||||
|
|
||||||
const { avatar } = await sequelizeTypescript.transaction(async t => {
|
const avatar = await sequelizeTypescript.transaction(async t => {
|
||||||
const avatar = await AvatarModel.create({
|
await updateActorAvatarInstance(actor, avatarName, t)
|
||||||
filename: avatarName
|
|
||||||
}, { transaction: t })
|
|
||||||
|
|
||||||
if (actor.Avatar) {
|
await sendUpdateUser(user, t)
|
||||||
try {
|
|
||||||
await actor.Avatar.destroy({ transaction: t })
|
|
||||||
} catch (err) {
|
|
||||||
logger.error('Cannot remove old avatar of user %s.', user.username, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
actor.set('avatarId', avatar.id)
|
return avatar
|
||||||
actor.Avatar = avatar
|
|
||||||
await actor.save({ transaction: t })
|
|
||||||
|
|
||||||
await sendUpdateUser(user, undefined)
|
|
||||||
|
|
||||||
return { actor, avatar }
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import * as retry from 'async/retry'
|
import * as retry from 'async/retry'
|
||||||
import * as Bluebird from 'bluebird'
|
import * as Bluebird from 'bluebird'
|
||||||
|
import { Model } from 'sequelize-typescript'
|
||||||
import { logger } from './logger'
|
import { logger } from './logger'
|
||||||
|
|
||||||
type RetryTransactionWrapperOptions = { errorMessage: string, arguments?: any[] }
|
type RetryTransactionWrapperOptions = { errorMessage: string, arguments?: any[] }
|
||||||
|
@ -34,9 +35,18 @@ function transactionRetryer <T> (func: (err: any, data: T) => 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 {
|
export {
|
||||||
retryTransactionWrapper,
|
retryTransactionWrapper,
|
||||||
transactionRetryer
|
transactionRetryer,
|
||||||
|
updateInstanceWithAnother
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,10 @@ async function loadActorUrlOrGetFromWebfinger (name: string, host: string) {
|
||||||
const actor = await ActorModel.loadByNameAndHost(name, host)
|
const actor = await ActorModel.loadByNameAndHost(name, host)
|
||||||
if (actor) return actor.url
|
if (actor) return actor.url
|
||||||
|
|
||||||
|
return getUrlFromWebfinger(name, host)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getUrlFromWebfinger (name: string, host: string) {
|
||||||
const webfingerData: WebFingerData = await webfingerLookup(name + '@' + host)
|
const webfingerData: WebFingerData = await webfingerLookup(name + '@' + host)
|
||||||
return getLinkOrThrow(webfingerData)
|
return getLinkOrThrow(webfingerData)
|
||||||
}
|
}
|
||||||
|
@ -22,6 +26,7 @@ async function loadActorUrlOrGetFromWebfinger (name: string, host: string) {
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
getUrlFromWebfinger,
|
||||||
loadActorUrlOrGetFromWebfinger
|
loadActorUrlOrGetFromWebfinger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -278,7 +278,8 @@ const ACTIVITY_PUB = {
|
||||||
VIDEO: [ 'video/mp4', 'video/webm', 'video/ogg' ], // TODO: Merge with VIDEO_MIMETYPE_EXT
|
VIDEO: [ 'video/mp4', 'video/webm', 'video/ogg' ], // TODO: Merge with VIDEO_MIMETYPE_EXT
|
||||||
TORRENT: [ 'application/x-bittorrent' ],
|
TORRENT: [ 'application/x-bittorrent' ],
|
||||||
MAGNET: [ 'application/x-bittorrent;x-scheme-handler/magnet' ]
|
MAGNET: [ 'application/x-bittorrent;x-scheme-handler/magnet' ]
|
||||||
}
|
},
|
||||||
|
ACTOR_REFRESH_INTERVAL: 3600 * 24 // 1 day
|
||||||
}
|
}
|
||||||
|
|
||||||
const ACTIVITY_PUB_ACTOR_TYPES: { [ id: string ]: ActivityPubActorType } = {
|
const ACTIVITY_PUB_ACTOR_TYPES: { [ id: string ]: ActivityPubActorType } = {
|
||||||
|
@ -350,6 +351,7 @@ if (isTestInstance() === true) {
|
||||||
REMOTE_SCHEME.WS = 'ws'
|
REMOTE_SCHEME.WS = 'ws'
|
||||||
STATIC_MAX_AGE = '0'
|
STATIC_MAX_AGE = '0'
|
||||||
ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE = 2
|
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
|
CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max = 100 * 1024 // 100KB
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,10 +7,11 @@ import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/a
|
||||||
import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
|
import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
|
||||||
import { isActorObjectValid } from '../../helpers/custom-validators/activitypub/actor'
|
import { isActorObjectValid } from '../../helpers/custom-validators/activitypub/actor'
|
||||||
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
|
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 { logger } from '../../helpers/logger'
|
||||||
import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto'
|
import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto'
|
||||||
import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
|
import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
|
||||||
|
import { getUrlFromWebfinger } from '../../helpers/webfinger'
|
||||||
import { AVATAR_MIMETYPE_EXT, CONFIG, sequelizeTypescript } from '../../initializers'
|
import { AVATAR_MIMETYPE_EXT, CONFIG, sequelizeTypescript } from '../../initializers'
|
||||||
import { AccountModel } from '../../models/account/account'
|
import { AccountModel } from '../../models/account/account'
|
||||||
import { ActorModel } from '../../models/activitypub/actor'
|
import { ActorModel } from '../../models/activitypub/actor'
|
||||||
|
@ -63,7 +64,7 @@ async function getOrCreateActorAndServerAndModel (actorUrl: string, recurseIfNee
|
||||||
actor = await retryTransactionWrapper(saveActorAndServerAndModelIfNotExist, options)
|
actor = await retryTransactionWrapper(saveActorAndServerAndModelIfNotExist, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
return actor
|
return refreshActorIfNeeded(actor)
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string, uuid?: string) {
|
function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string, uuid?: string) {
|
||||||
|
@ -84,6 +85,45 @@ 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) {
|
async function fetchActorTotalItems (url: string) {
|
||||||
const options = {
|
const options = {
|
||||||
uri: url,
|
uri: url,
|
||||||
|
@ -129,7 +169,9 @@ export {
|
||||||
buildActorInstance,
|
buildActorInstance,
|
||||||
setAsyncActorKeys,
|
setAsyncActorKeys,
|
||||||
fetchActorTotalItems,
|
fetchActorTotalItems,
|
||||||
fetchAvatarIfExists
|
fetchAvatarIfExists,
|
||||||
|
updateActorInstance,
|
||||||
|
updateActorAvatarInstance
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
@ -263,3 +305,35 @@ async function saveVideoChannel (actor: ActorModel, result: FetchRemoteActorResu
|
||||||
|
|
||||||
return videoChannel.save({ transaction: t })
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -8,11 +8,10 @@ import { resetSequelizeInstance } from '../../../helpers/utils'
|
||||||
import { sequelizeTypescript } from '../../../initializers'
|
import { sequelizeTypescript } from '../../../initializers'
|
||||||
import { AccountModel } from '../../../models/account/account'
|
import { AccountModel } from '../../../models/account/account'
|
||||||
import { ActorModel } from '../../../models/activitypub/actor'
|
import { ActorModel } from '../../../models/activitypub/actor'
|
||||||
import { AvatarModel } from '../../../models/avatar/avatar'
|
|
||||||
import { TagModel } from '../../../models/video/tag'
|
import { TagModel } from '../../../models/video/tag'
|
||||||
import { VideoModel } from '../../../models/video/video'
|
import { VideoModel } from '../../../models/video/video'
|
||||||
import { VideoFileModel } from '../../../models/video/video-file'
|
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'
|
import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc'
|
||||||
|
|
||||||
async function processUpdateActivity (activity: ActivityUpdate) {
|
async function processUpdateActivity (activity: ActivityUpdate) {
|
||||||
|
@ -124,7 +123,6 @@ async function updateRemoteAccount (actor: ActorModel, activity: ActivityUpdate)
|
||||||
const accountAttributesToUpdate = activity.object as ActivityPubActor
|
const accountAttributesToUpdate = activity.object as ActivityPubActor
|
||||||
|
|
||||||
logger.debug('Updating remote account "%s".', accountAttributesToUpdate.uuid)
|
logger.debug('Updating remote account "%s".', accountAttributesToUpdate.uuid)
|
||||||
let actorInstance: ActorModel
|
|
||||||
let accountInstance: AccountModel
|
let accountInstance: AccountModel
|
||||||
let actorFieldsSave: object
|
let actorFieldsSave: object
|
||||||
let accountFieldsSave: object
|
let accountFieldsSave: object
|
||||||
|
@ -134,39 +132,14 @@ async function updateRemoteAccount (actor: ActorModel, activity: ActivityUpdate)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await sequelizeTypescript.transaction(async t => {
|
await sequelizeTypescript.transaction(async t => {
|
||||||
actorInstance = await ActorModel.loadByUrl(accountAttributesToUpdate.id, t)
|
actorFieldsSave = actor.toJSON()
|
||||||
if (!actorInstance) throw new Error('Actor ' + accountAttributesToUpdate.id + ' not found.')
|
accountInstance = actor.Account
|
||||||
|
accountFieldsSave = actor.Account.toJSON()
|
||||||
|
|
||||||
actorFieldsSave = actorInstance.toJSON()
|
await updateActorInstance(actor, accountAttributesToUpdate)
|
||||||
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)
|
|
||||||
|
|
||||||
if (avatarName !== undefined) {
|
if (avatarName !== undefined) {
|
||||||
if (actorInstance.avatarId) {
|
await updateActorAvatarInstance(actor, avatarName, t)
|
||||||
await actorInstance.Avatar.destroy({ transaction: t })
|
|
||||||
}
|
|
||||||
|
|
||||||
const avatar = await AvatarModel.create({
|
|
||||||
filename: avatarName
|
|
||||||
}, { transaction: t })
|
|
||||||
|
|
||||||
actor.set('avatarId', avatar.id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await actor.save({ transaction: t })
|
await actor.save({ transaction: t })
|
||||||
|
@ -177,8 +150,8 @@ async function updateRemoteAccount (actor: ActorModel, activity: ActivityUpdate)
|
||||||
|
|
||||||
logger.info('Remote account with uuid %s updated', accountAttributesToUpdate.uuid)
|
logger.info('Remote account with uuid %s updated', accountAttributesToUpdate.uuid)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (actorInstance !== undefined && actorFieldsSave !== undefined) {
|
if (actor !== undefined && actorFieldsSave !== undefined) {
|
||||||
resetSequelizeInstance(actorInstance, actorFieldsSave)
|
resetSequelizeInstance(actor, actorFieldsSave)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (accountInstance !== undefined && accountFieldsSave !== undefined) {
|
if (accountInstance !== undefined && accountFieldsSave !== undefined) {
|
||||||
|
|
|
@ -13,7 +13,7 @@ import {
|
||||||
isActorPublicKeyValid
|
isActorPublicKeyValid
|
||||||
} from '../../helpers/custom-validators/activitypub/actor'
|
} from '../../helpers/custom-validators/activitypub/actor'
|
||||||
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
|
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 { AccountModel } from '../account/account'
|
||||||
import { AvatarModel } from '../avatar/avatar'
|
import { AvatarModel } from '../avatar/avatar'
|
||||||
import { ServerModel } from '../server/server'
|
import { ServerModel } from '../server/server'
|
||||||
|
@ -375,4 +375,15 @@ export class ActorModel extends Model<ActorModel> {
|
||||||
|
|
||||||
return CONFIG.WEBSERVER.URL + this.Avatar.getWebserverPath()
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue