From 265ba139ebf56bbdc1c65f6ea4f367774c691fc0 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 3 Jan 2018 16:38:50 +0100 Subject: [PATCH] Send account activitypub update events --- server/controllers/api/accounts.ts | 38 +++++++ server/controllers/api/index.ts | 2 + server/controllers/api/users.ts | 17 +++- .../custom-validators/activitypub/activity.ts | 5 +- .../custom-validators/activitypub/actor.ts | 37 ++++--- server/initializers/constants.ts | 1 + server/lib/activitypub/actor.ts | 86 ++++++++-------- .../lib/activitypub/process/process-update.ts | 94 +++++++++++++++++- server/lib/activitypub/send/send-update.ts | 16 +++ server/middlewares/sort.ts | 9 +- server/middlewares/validators/account.ts | 19 +++- server/middlewares/validators/sort.ts | 3 + server/models/account/account.ts | 23 ++++- server/models/activitypub/actor.ts | 2 +- server/models/video/video-share.ts | 40 ++++++++ server/tests/api/check-params/accounts.ts | 51 ++++++++++ server/tests/api/check-params/index.ts | 1 + server/tests/api/fixtures/avatar2-resized.png | Bin 0 -> 2350 bytes server/tests/api/fixtures/avatar2.png | Bin 0 -> 4850 bytes server/tests/api/index-slow.ts | 1 + .../tests/api/users/users-multiple-servers.ts | 85 ++++++++++++++++ server/tests/utils/users/accounts.ts | 29 ++++++ shared/models/activitypub/activity.ts | 3 +- 23 files changed, 493 insertions(+), 69 deletions(-) create mode 100644 server/controllers/api/accounts.ts create mode 100644 server/tests/api/check-params/accounts.ts create mode 100644 server/tests/api/fixtures/avatar2-resized.png create mode 100644 server/tests/api/fixtures/avatar2.png create mode 100644 server/tests/api/users/users-multiple-servers.ts create mode 100644 server/tests/utils/users/accounts.ts diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts new file mode 100644 index 000000000..aded581a5 --- /dev/null +++ b/server/controllers/api/accounts.ts @@ -0,0 +1,38 @@ +import * as express from 'express' +import { getFormattedObjects } from '../../helpers/utils' +import { asyncMiddleware, paginationValidator, setAccountsSort, setPagination } from '../../middlewares' +import { accountsGetValidator, accountsSortValidator } from '../../middlewares/validators' +import { AccountModel } from '../../models/account/account' + +const accountsRouter = express.Router() + +accountsRouter.get('/', + paginationValidator, + accountsSortValidator, + setAccountsSort, + setPagination, + asyncMiddleware(listAccounts) +) + +accountsRouter.get('/:id', + asyncMiddleware(accountsGetValidator), + getAccount +) + +// --------------------------------------------------------------------------- + +export { + accountsRouter +} + +// --------------------------------------------------------------------------- + +function getAccount (req: express.Request, res: express.Response, next: express.NextFunction) { + return res.json(res.locals.account.toFormattedJSON()) +} + +async function listAccounts (req: express.Request, res: express.Response, next: express.NextFunction) { + const resultList = await AccountModel.listForApi(req.query.start, req.query.count, req.query.sort) + + return res.json(getFormattedObjects(resultList.data, resultList.total)) +} diff --git a/server/controllers/api/index.ts b/server/controllers/api/index.ts index 1fd44ac11..3b499f3b7 100644 --- a/server/controllers/api/index.ts +++ b/server/controllers/api/index.ts @@ -5,6 +5,7 @@ import { jobsRouter } from './jobs' import { oauthClientsRouter } from './oauth-clients' import { serverRouter } from './server' import { usersRouter } from './users' +import { accountsRouter } from './accounts' import { videosRouter } from './videos' const apiRouter = express.Router() @@ -13,6 +14,7 @@ apiRouter.use('/server', serverRouter) apiRouter.use('/oauth-clients', oauthClientsRouter) apiRouter.use('/config', configRouter) apiRouter.use('/users', usersRouter) +apiRouter.use('/accounts', accountsRouter) apiRouter.use('/videos', videosRouter) apiRouter.use('/jobs', jobsRouter) apiRouter.use('/ping', pong) diff --git a/server/controllers/api/users.ts b/server/controllers/api/users.ts index d37813595..ef2b63f51 100644 --- a/server/controllers/api/users.ts +++ b/server/controllers/api/users.ts @@ -8,6 +8,7 @@ import { retryTransactionWrapper } from '../../helpers/database-utils' import { logger } from '../../helpers/logger' import { createReqFiles, getFormattedObjects } from '../../helpers/utils' import { AVATAR_MIMETYPE_EXT, AVATARS_SIZE, CONFIG, sequelizeTypescript } from '../../initializers' +import { sendUpdateUser } from '../../lib/activitypub/send' import { createUserAccountAndChannel } from '../../lib/user' import { asyncMiddleware, authenticate, ensureUserHasRight, ensureUserRegistrationAllowed, paginationValidator, setPagination, setUsersSort, @@ -217,7 +218,6 @@ async function removeUser (req: express.Request, res: express.Response, next: ex async function updateMe (req: express.Request, res: express.Response, next: express.NextFunction) { const body: UserUpdateMe = req.body - // FIXME: user is not already a Sequelize instance? const user = res.locals.oauth.token.user if (body.password !== undefined) user.password = body.password @@ -226,13 +226,15 @@ async function updateMe (req: express.Request, res: express.Response, next: expr if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo await user.save() + await sendUpdateUser(user, undefined) return res.sendStatus(204) } async function updateMyAvatar (req: express.Request, res: express.Response, next: express.NextFunction) { const avatarPhysicalFile = req.files['avatarfile'][0] - const actor = res.locals.oauth.token.user.Account.Actor + const user = res.locals.oauth.token.user + const actor = user.Account.Actor const avatarDir = CONFIG.STORAGE.AVATARS_DIR const source = join(avatarDir, avatarPhysicalFile.filename) @@ -252,12 +254,19 @@ async function updateMyAvatar (req: express.Request, res: express.Response, next }, { transaction: t }) if (actor.Avatar) { - await actor.Avatar.destroy({ transaction: 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) + actor.Avatar = avatar await actor.save({ transaction: t }) + await sendUpdateUser(user, undefined) + return { actor, avatar } }) @@ -278,6 +287,8 @@ async function updateUser (req: express.Request, res: express.Response, next: ex await user.save() + // Don't need to send this update to followers, these attributes are not propagated + return res.sendStatus(204) } diff --git a/server/helpers/custom-validators/activitypub/activity.ts b/server/helpers/custom-validators/activitypub/activity.ts index fbdde10ad..856c87f2c 100644 --- a/server/helpers/custom-validators/activitypub/activity.ts +++ b/server/helpers/custom-validators/activitypub/activity.ts @@ -1,6 +1,6 @@ import * as validator from 'validator' import { Activity, ActivityType } from '../../../../shared/models/activitypub' -import { isActorAcceptActivityValid, isActorDeleteActivityValid, isActorFollowActivityValid } from './actor' +import { isActorAcceptActivityValid, isActorDeleteActivityValid, isActorFollowActivityValid, isActorUpdateActivityValid } from './actor' import { isAnnounceActivityValid } from './announce' import { isActivityPubUrlValid } from './misc' import { isDislikeActivityValid, isLikeActivityValid } from './rate' @@ -64,7 +64,8 @@ function checkCreateActivity (activity: any) { } function checkUpdateActivity (activity: any) { - return isVideoTorrentUpdateActivityValid(activity) + return isVideoTorrentUpdateActivityValid(activity) || + isActorUpdateActivityValid(activity) } function checkDeleteActivity (activity: any) { diff --git a/server/helpers/custom-validators/activitypub/actor.ts b/server/helpers/custom-validators/activitypub/actor.ts index 700e06007..8820bb2a4 100644 --- a/server/helpers/custom-validators/activitypub/actor.ts +++ b/server/helpers/custom-validators/activitypub/actor.ts @@ -45,22 +45,22 @@ function isActorPrivateKeyValid (privateKey: string) { validator.isLength(privateKey, CONSTRAINTS_FIELDS.ACTORS.PRIVATE_KEY) } -function isRemoteActorValid (remoteActor: any) { - return exists(remoteActor) && - isActivityPubUrlValid(remoteActor.id) && - isActorTypeValid(remoteActor.type) && - isActivityPubUrlValid(remoteActor.following) && - isActivityPubUrlValid(remoteActor.followers) && - isActivityPubUrlValid(remoteActor.inbox) && - isActivityPubUrlValid(remoteActor.outbox) && - isActorPreferredUsernameValid(remoteActor.preferredUsername) && - isActivityPubUrlValid(remoteActor.url) && - isActorPublicKeyObjectValid(remoteActor.publicKey) && - isActorEndpointsObjectValid(remoteActor.endpoints) && - setValidAttributedTo(remoteActor) && +function isActorObjectValid (actor: any) { + return exists(actor) && + isActivityPubUrlValid(actor.id) && + isActorTypeValid(actor.type) && + isActivityPubUrlValid(actor.following) && + isActivityPubUrlValid(actor.followers) && + isActivityPubUrlValid(actor.inbox) && + isActivityPubUrlValid(actor.outbox) && + isActorPreferredUsernameValid(actor.preferredUsername) && + isActivityPubUrlValid(actor.url) && + isActorPublicKeyObjectValid(actor.publicKey) && + isActorEndpointsObjectValid(actor.endpoints) && + setValidAttributedTo(actor) && // If this is not an account, it should be attributed to an account // In PeerTube we use this to attach a video channel to a specific account - (remoteActor.type === 'Person' || remoteActor.attributedTo.length !== 0) + (actor.type === 'Person' || actor.attributedTo.length !== 0) } function isActorFollowingCountValid (value: string) { @@ -84,6 +84,11 @@ function isActorAcceptActivityValid (activity: any) { return isBaseActivityValid(activity, 'Accept') } +function isActorUpdateActivityValid (activity: any) { + return isBaseActivityValid(activity, 'Update') && + isActorObjectValid(activity.object) +} + // --------------------------------------------------------------------------- export { @@ -93,11 +98,11 @@ export { isActorPublicKeyValid, isActorPreferredUsernameValid, isActorPrivateKeyValid, - isRemoteActorValid, + isActorObjectValid, isActorFollowingCountValid, isActorFollowersCountValid, isActorFollowActivityValid, isActorAcceptActivityValid, isActorDeleteActivityValid, - isActorNameValid + isActorUpdateActivityValid } diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index d9b21b389..d2bcea443 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -22,6 +22,7 @@ const PAGINATION_COUNT_DEFAULT = 15 // Sortable columns per schema const SORTABLE_COLUMNS = { USERS: [ 'id', 'username', 'createdAt' ], + ACCOUNTS: [ 'createdAt' ], JOBS: [ 'id', 'createdAt' ], VIDEO_ABUSES: [ 'id', 'createdAt' ], VIDEO_CHANNELS: [ 'id', 'name', 'updatedAt', 'createdAt' ], diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts index e557896e8..b6ba2cc22 100644 --- a/server/lib/activitypub/actor.ts +++ b/server/lib/activitypub/actor.ts @@ -5,13 +5,13 @@ 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 { logger } from '../../helpers/logger' import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto' import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests' -import { CONFIG, sequelizeTypescript } from '../../initializers' +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' @@ -84,10 +84,52 @@ function buildActorInstance (type: ActivityPubActorType, url: string, preferredU }) } +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 } // --------------------------------------------------------------------------- @@ -166,7 +208,7 @@ async function fetchRemoteActor (actorUrl: string): Promise { @@ -54,6 +60,8 @@ async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) { const videoInstance = await VideoModel.loadByUrlAndPopulateAccount(videoAttributesToUpdate.id, t) if (!videoInstance) throw new Error('Video ' + videoAttributesToUpdate.id + ' not found.') + videoFieldsSave = videoInstance.toJSON() + const videoChannel = videoInstance.VideoChannel if (videoChannel.Account.Actor.id !== actor.id) { throw new Error('Account ' + actor.url + ' does not own video channel ' + videoChannel.Actor.url) @@ -102,3 +110,83 @@ async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) { throw err } } + +function processUpdateAccount (actor: ActorModel, activity: ActivityUpdate) { + const options = { + arguments: [ actor, activity ], + errorMessage: 'Cannot update the remote account with many retries' + } + + return retryTransactionWrapper(updateRemoteAccount, options) +} + +async function updateRemoteAccount (actor: ActorModel, 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 + + // Fetch icon? + const avatarName = await fetchAvatarIfExists(accountAttributesToUpdate) + + 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) + + if (avatarName !== undefined) { + if (actorInstance.avatarId) { + 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 }) + + actor.Account.set('name', accountAttributesToUpdate.name || accountAttributesToUpdate.preferredUsername) + await actor.Account.save({ transaction: t }) + }) + + logger.info('Remote account with uuid %s updated', accountAttributesToUpdate.uuid) + } catch (err) { + if (actorInstance !== undefined && actorFieldsSave !== undefined) { + resetSequelizeInstance(actorInstance, actorFieldsSave) + } + + if (accountInstance !== undefined && accountFieldsSave !== undefined) { + resetSequelizeInstance(accountInstance, accountFieldsSave) + } + + // This is just a debug because we will retry the insert + logger.debug('Cannot update the remote account.', err) + throw err + } +} diff --git a/server/lib/activitypub/send/send-update.ts b/server/lib/activitypub/send/send-update.ts index b623fec6c..e8f11edd0 100644 --- a/server/lib/activitypub/send/send-update.ts +++ b/server/lib/activitypub/send/send-update.ts @@ -1,6 +1,7 @@ import { Transaction } from 'sequelize' import { ActivityAudience, ActivityUpdate } from '../../../../shared/models/activitypub' import { VideoPrivacy } from '../../../../shared/models/videos' +import { UserModel } from '../../../models/account/user' import { ActorModel } from '../../../models/activitypub/actor' import { VideoModel } from '../../../models/video/video' import { VideoShareModel } from '../../../models/video/video-share' @@ -22,9 +23,24 @@ async function sendUpdateVideo (video: VideoModel, t: Transaction) { return broadcastToFollowers(data, byActor, actorsInvolved, t) } +async function sendUpdateUser (user: UserModel, t: Transaction) { + const byActor = user.Account.Actor + + const url = getUpdateActivityPubUrl(byActor.url, byActor.updatedAt.toISOString()) + const accountObject = user.Account.toActivityPubObject() + const audience = await getAudience(byActor, t) + const data = await updateActivityData(url, byActor, accountObject, t, audience) + + const actorsInvolved = await VideoShareModel.loadActorsByVideoOwner(byActor.id, t) + actorsInvolved.push(byActor) + + return broadcastToFollowers(data, byActor, actorsInvolved, t) +} + // --------------------------------------------------------------------------- export { + sendUpdateUser, sendUpdateVideo } diff --git a/server/middlewares/sort.ts b/server/middlewares/sort.ts index fdd6d419f..4f524b49a 100644 --- a/server/middlewares/sort.ts +++ b/server/middlewares/sort.ts @@ -2,6 +2,12 @@ import * as express from 'express' import 'express-validator' import { SortType } from '../helpers/utils' +function setAccountsSort (req: express.Request, res: express.Response, next: express.NextFunction) { + if (!req.query.sort) req.query.sort = '-createdAt' + + return next() +} + function setUsersSort (req: express.Request, res: express.Response, next: express.NextFunction) { if (!req.query.sort) req.query.sort = '-createdAt' @@ -82,5 +88,6 @@ export { setFollowersSort, setFollowingSort, setJobsSort, - setVideoCommentThreadsSort + setVideoCommentThreadsSort, + setAccountsSort } diff --git a/server/middlewares/validators/account.ts b/server/middlewares/validators/account.ts index 3573a9a50..ebc2fcf2d 100644 --- a/server/middlewares/validators/account.ts +++ b/server/middlewares/validators/account.ts @@ -1,6 +1,7 @@ import * as express from 'express' import { param } from 'express-validator/check' -import { isAccountNameValid, isLocalAccountNameExist } from '../../helpers/custom-validators/accounts' +import { isAccountIdExist, isAccountNameValid, isLocalAccountNameExist } from '../../helpers/custom-validators/accounts' +import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc' import { logger } from '../../helpers/logger' import { areValidationErrors } from './utils' @@ -17,8 +18,22 @@ const localAccountValidator = [ } ] +const accountsGetValidator = [ + param('id').custom(isIdOrUUIDValid).withMessage('Should have a valid id'), + + async (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking accountsGetValidator parameters', { parameters: req.params }) + + if (areValidationErrors(req, res)) return + if (!await isAccountIdExist(req.params.id, res)) return + + return next() + } +] + // --------------------------------------------------------------------------- export { - localAccountValidator + localAccountValidator, + accountsGetValidator } diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts index e1d8d7d1b..72c6b34e3 100644 --- a/server/middlewares/validators/sort.ts +++ b/server/middlewares/validators/sort.ts @@ -6,6 +6,7 @@ import { areValidationErrors } from './utils' // Initialize constants here for better performances const SORTABLE_USERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USERS) +const SORTABLE_ACCOUNTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.ACCOUNTS) const SORTABLE_JOBS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.JOBS) const SORTABLE_VIDEO_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_ABUSES) const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS) @@ -16,6 +17,7 @@ const SORTABLE_FOLLOWERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOW const SORTABLE_FOLLOWING_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWING) const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) +const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS) const jobsSortValidator = checkSort(SORTABLE_JOBS_COLUMNS) const videoAbusesSortValidator = checkSort(SORTABLE_VIDEO_ABUSES_COLUMNS) const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS) @@ -33,6 +35,7 @@ export { videoChannelsSortValidator, videosSortValidator, blacklistSortValidator, + accountsSortValidator, followersSortValidator, followingSortValidator, jobsSortValidator, diff --git a/server/models/account/account.ts b/server/models/account/account.ts index d3503aaa3..493068127 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts @@ -18,8 +18,9 @@ import { isUserUsernameValid } from '../../helpers/custom-validators/users' import { sendDeleteActor } from '../../lib/activitypub/send' import { ActorModel } from '../activitypub/actor' import { ApplicationModel } from '../application/application' +import { AvatarModel } from '../avatar/avatar' import { ServerModel } from '../server/server' -import { throwIfNotValid } from '../utils' +import { getSort, throwIfNotValid } from '../utils' import { VideoChannelModel } from '../video/video-channel' import { UserModel } from './user' @@ -32,6 +33,10 @@ import { UserModel } from './user' { model: () => ServerModel, required: false + }, + { + model: () => AvatarModel, + required: false } ] } @@ -166,6 +171,22 @@ export class AccountModel extends Model { return AccountModel.findOne(query) } + static listForApi (start: number, count: number, sort: string) { + const query = { + offset: start, + limit: count, + order: [ getSort(sort) ] + } + + return AccountModel.findAndCountAll(query) + .then(({ rows, count }) => { + return { + data: rows, + total: count + } + }) + } + toFormattedJSON (): Account { const actor = this.Actor.toFormattedJSON() const account = { diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts index ff5ab2e32..2ef7c77a2 100644 --- a/server/models/activitypub/actor.ts +++ b/server/models/activitypub/actor.ts @@ -372,6 +372,6 @@ export class ActorModel extends Model { getAvatarUrl () { if (!this.avatarId) return undefined - return CONFIG.WEBSERVER.URL + this.Avatar.getWebserverPath + return CONFIG.WEBSERVER.URL + this.Avatar.getWebserverPath() } } diff --git a/server/models/video/video-share.ts b/server/models/video/video-share.ts index c252fd646..56576f98c 100644 --- a/server/models/video/video-share.ts +++ b/server/models/video/video-share.ts @@ -1,7 +1,9 @@ import * as Sequelize from 'sequelize' import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' +import { AccountModel } from '../account/account' import { ActorModel } from '../activitypub/actor' import { VideoModel } from './video' +import { VideoChannelModel } from './video-channel' enum ScopeNames { FULL = 'FULL', @@ -99,4 +101,42 @@ export class VideoShareModel extends Model { return VideoShareModel.scope(ScopeNames.FULL).findAll(query) .then(res => res.map(r => r.Actor)) } + + static loadActorsByVideoOwner (actorOwnerId: number, t: Sequelize.Transaction) { + const query = { + attributes: [], + include: [ + { + model: ActorModel, + required: true + }, + { + attributes: [], + model: VideoModel, + required: true, + include: [ + { + attributes: [], + model: VideoChannelModel.unscoped(), + required: true, + include: [ + { + attributes: [], + model: AccountModel.unscoped(), + required: true, + where: { + actorId: actorOwnerId + } + } + ] + } + ] + } + ], + transaction: t + } + + return VideoShareModel.scope(ScopeNames.FULL).findAll(query) + .then(res => res.map(r => r.Actor)) + } } diff --git a/server/tests/api/check-params/accounts.ts b/server/tests/api/check-params/accounts.ts new file mode 100644 index 000000000..351228754 --- /dev/null +++ b/server/tests/api/check-params/accounts.ts @@ -0,0 +1,51 @@ +/* tslint:disable:no-unused-expression */ + +import 'mocha' + +import { flushTests, killallServers, runServer, ServerInfo } from '../../utils' +import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' +import { getAccount } from '../../utils/users/accounts' + +describe('Test users API validators', function () { + const path = '/api/v1/accounts/' + let server: ServerInfo + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(20000) + + await flushTests() + + server = await runServer(1) + }) + + describe('When listing accounts', function () { + it('Should fail with a bad start pagination', async function () { + await checkBadStartPagination(server.url, path, server.accessToken) + }) + + it('Should fail with a bad count pagination', async function () { + await checkBadCountPagination(server.url, path, server.accessToken) + }) + + it('Should fail with an incorrect sort', async function () { + await checkBadSortPagination(server.url, path, server.accessToken) + }) + }) + + describe('When getting an account', function () { + it('Should return 404 with a non existing id', async function () { + await getAccount(server.url, 4545454, 404) + }) + }) + + after(async function () { + killallServers([ server ]) + + // Keep the logs if the test failed + if (this['ok']) { + await flushTests() + } + }) +}) diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts index ab0aa1580..4c3b372f5 100644 --- a/server/tests/api/check-params/index.ts +++ b/server/tests/api/check-params/index.ts @@ -1,4 +1,5 @@ // Order of the tests we want to execute +import './accounts' import './follows' import './jobs' import './services' diff --git a/server/tests/api/fixtures/avatar2-resized.png b/server/tests/api/fixtures/avatar2-resized.png new file mode 100644 index 0000000000000000000000000000000000000000..a2e2613bfcb89e28f8d6f03deb7c1e921917a166 GIT binary patch literal 2350 zcmb_eX;c#07N#u2Auo2-2u&l)rA1DWQw~=zInO!gke4#&EJejRr_2e{%GA`Nho)Z1 zA)qWZQ6V!mBI2bvD=4Hmp(z-T-tYJ0{&;8YefE#D_qW#BXMf*1cU+xq4oX9$0RX^3 zJ6nXiI41sb;9l`glzfsS4pK3;zOev+%yFP?xfuVqQC<1A~QDT5F+HsBM2&RVj}VrJy1P zuSTM~qry?6cH~;SuM-^zEuJl;A_v=GgJQJ3@)l6UMf>vdatGWe4kz5CTCQ53k}^qx zaXB2$%nTVdo;c>H<%Dy`xrZT<>SY7Z8X;dCw5lC7a)TE-%vI#}_!lS+fiEiH{I$ym zuR76Hg-XarwkN#)yNE=hoSa<8c(i|1-M#4O=;h^Q*7`P;e=5IpaQ^12Zxjk;U|^uX zKW71e6$-a6y3z_;5yx9eBya66ZBa}uetSt0DcoLXZ}BEw*2{rUJWdhBATT8@+(^Sc_YMMqzbn{69M_w- ziI0ybl1Tl&)-&RJGnq__lph&CljHag<4ihVzitZid!c*;sj{z!OeWX6)c(l(D@j}- zokNbruhR1lHEX`#+>FmY*3#Ctwz)JGnxNNh%A4p+Rw)_tRAI{ zcetQ`ettewEq$Hkq+juf5WD&$>C}C{pdhxz$cAtB$I($glj@o!u5(d|>Ja#}QbGH~ z8|c?agQ}s4bV9`;`^A}=zckA3cs*}yR905b#M%h4N060uX7v%z8Y@TG>fgwPt^V3n z>Rnn*ByE`-20HHN$Yu6cSYZmb=Oeey;vxG;DkLJb5R&5&=G zoA8+2N}a^`196{G<3P1JPaFJU=$znZ(;XTZ$wWns75iuM1hcC~evGn#zOCJNyGd}! z1;JF1JSuA|sqpkLEqWNxmCq0%JJ2ty#UlJ)iV^z{7OFAE9^nwpxJ<(;z<8Ch9b z&h=;KWp04>F~SM7P(&IIk#Hm?fa&7wT=(W}c8OP$XR}vMo!x%c)9saB-NGyS6(j80 z=-#EJD4aVDW%i&>pCa44lA0bCwzxP*0XH$i1p-0wtSNi1Nbwey7SO#MXCm&%efx&S zoyU5Gdej@EOPjpOZuBIkDUjmRJ!$B4_O5McWu4~xEV9{%vaF=Ip5QmMl}fwrLuSiv zOK{fK+8E)A(mC_4vCuVFeRG3-9`sXXSHBd1IQ*o#FzqvE#0haG&0!GAzt>I2 zh)#64Q`rcld&js}EC9F-4yTb3p0k~O445+Mt#IL^f>B4+^zxRVGY|tJ3s9hamY|aJ zh|dD>$Nli>576|Fzgs1)+5YHzhRt>`N-UkKcvTO~$WQyI+Y)b%2fFLo;5#L(@+JSN zd6@6?Ya0kpXSy@6i1k-SmN^CLn+<~?C{_yE;9-v#03_BN-e>u}p8~6NNv)$Vc6N3; zYDichH&k>f#j_r#?)O{^ zimd;1oo?+_8yyOv-+1J$0YOHBtzR^2uB@zDlg1Pc9S>bes zW=s`XXHAysv|Ljipz1OZ~TFj%fLVvJ=OyV{Qo4;N!Fw_M!zC=MB0SR@2&jghW5 zcu2n_bn(peZX>}I zv5tFtd-D=ru))7MO!WlofB-b8n=j)eO!n9X>3s(kQd5rw?2y zO%NU6Y)|hBUG>xE7%@h34w657gH1i*34;qK^H?hcAJbR8%_x(j*(G&h;&e69+Y!qt zkwpeiAe>7|Q^}b%!gJTm_(13dw?ts2fSdNtLB43&PAW^Qd;zU0V(dOk6|q~YCNX3k w8$I^^J86!gEBje(*Dl~O|CoyZi?lOpQ18#It%|l<@dpTCcflEfx4NA9A3E2M82|tP literal 0 HcmV?d00001 diff --git a/server/tests/api/fixtures/avatar2.png b/server/tests/api/fixtures/avatar2.png new file mode 100644 index 0000000000000000000000000000000000000000..dae7021909026c13953210520b86a419df2ccad5 GIT binary patch literal 4850 zcmchbX*3jW-^aBhDl;YfFpaG+hO$HyBg0s-WDS`@$WnHNvJd8u5rZLFldWcC-~L&% z4B5sqA?surlV#G3?WuF0=iKN1zq_9o&x_yZI_GoF_xJXDT^IU}kuEo<04Ez88@C<; zZpy}Xq~Oo6{}ybvl4q8G9~|BYYaceY)4)G-ge^0R@3%bVt7oWlYVOD}Zm8rb?N~4y z8z5N^e#<;?l9oN;zo3MTzQiV1=jPGiQcvBdQ?omVoveEd6zUu?b$3!jzE%)QU!DJ4 zaBygcof3^+x(Mk9DmHe`6qG|@=yN%t&&Lj zoWPkvdZU4eZ64tO!ZzBY#KeMRId~LB_iecM>i+t!d>_;-U-<$8*2=n|nnKo3hJz4U z+{x)c!Bb z6Q-}GL9n!Sx+iC0%fYDI-L;OZZ`-$+|F84ClXFW+_3g5<EG!H8SfVH>Fi&Z|QpA>L^; zTf*haw)W@e4+IAa(WO9jZv7Ty1kdDNV2eZ8d|MDB zDJQfdqTme3RNFLo_s6gFu&u*Yy9{a2j5wc5Ph10t-qIxRRw-wiG)*OANqnFdL>AKjGpuDM#KjM1ZK49~geKT)?)mIA zJ(!O1+s34J*qEA^PPOg-Y%tXFLV*MoHz#q$c#z20%wfam%*gPkT6

zxJ0i1u1EK!sOG$Y018qgWXn56enG!#TY7@DzoMW$u0CBXb;){&Hbcg#=QcJ;UxS`8 z4Si;5|1)89->Teladm&4=(32X=@(9B;rib5UevzqwG$=KRkBmFip(*eo^dA1Am_@y zddNM;*?5gbVPT?nU+>3mQYh-F;^NB(k;KvJ+CA09X(b#K6Zd^;$FMZ?!cfInJQ0$+ z=d9eyexs$XyYo@ggns}GQe{JpyXd6H^jB-eZ|=W=f(D2^y)X?mC=#l-Bn)8aF+CN- z#hvh&<`Hb9uPW!Ip;be0436Ej+8*;mLN0D^ZV>%&l_8aqA6(fzyhb%XhW5+A%w61^ z87d-uB1Ie#Tl+FEK*urR4kqvE^g=&#P?8D?6;PcPy4JG2WNRK&v5kyz@6N zTiT?}ulvGyA^oRATvzXQAd=$1gRKq#q-mW9w`r&=TF34n0Xi&&KB@02BR$X=UQl@& zcO`PkDz}Gdo1Z?XVRSQj@*R62fmrA-_ z%!{s|$$2S+%7^L+{>*sZz<&gLL;2G7(X!`u-x-D6MXHj>6RE|-Tg_ihb-yyjVR|&+k;*v9?;$^08xlz8kt?eu5|EJ#o}YvW7K`I1?S= zPB|cwmI-P#-KNJ2h!FqLjaD+{hUFy;GX44fki)~#(TRdyudF^S3Rl%8{w&haXQgxU zOCGA+l7bZI%QZ^Br7$aU>RI5*^ug*+T#B%}(+kz=yFRzpzTt5_P?!hiuP&>{Gb|?p z%N&EVNXehy%7|0efNnDg!)`%IR&#HRN|i+{{HPCZw8JPBnogUYzUEIuRjsWb9+fJR z7qSwWeyGdXA_ywZIT=TTUU1bYl6(0gn-m;!z_~3;13ilri+di`MU9ID1Ar8tjjcJiC)7D;AgzW zNjMFC<`9JiQA7H?nwfmKNY>&VZvCdy1)m}ls%O;OdB;GZs<-vaNBDncXdw>{Hf*<^ z)Wi%gPm#-q8J-a8m;-NMq>r4mIgrVv#KuOP-`PgZq5Z+w2>F376I*0pR|d!dPi zJS!p8z+#$pf#%#q^6tC6+o8}_pa%oKhGsc4K{KyvQ|SJ?B!|K(UytFd81#zvO_aoP z!~B%Cd6vy9vfnE;&^-6M)JK{%l|9Dr2n7-*6lm`#03TN!F7E}8?FF;U5s=RB zv@bi_^hnbwMZtU(7sIi#o-Do6v1bF~a2|Se1s%gE`_(q3SQ@GFv&bSVHHw^J0SAj6 zrH_4p7nL)wfdz zqo;>fJtl{zLp7u!thwDVHTMBH&@O(jP+n^`AS&Y)AnGv-e6E+Cc{yKRG4g6+X1bH3 z#wqLLB>NY~5$8mY0REFNj{h&N>WDADexV&b{4 zj$=(5@4x|0{7I4VsrGLM9)LL`c?X4lEL4xsfm79ig`0PiS#g(@a%jAt6|~ z0NbX&MWjD!bi0W2OP&zqRTVM&6diRFMKq-1*t-KThCC6HuCt*Zo}iQRg!fP~-8;_7 zZPu_Rhrh%#t~RR$eY;P3DkIHT*{9K+{(%pP{TAGy5+hk1aElkE2k)zJ<#jkD28B@krf#+DELF*NCU{DM^?R@knS#GRgbgc}M z*!GS0!VFq4+C0DIAuy-e&f+;0uQah!FcGU|Fnf=e87?}(5=#HwmwnBu&=Sk1RP!T& z`bf9l!n%eJ@`!fWw)VaLAqSyge4I(EaM@r$FNg5+VWw}sPwHlyTjhB~k9cVUrETAEl~gW{Y7?h|&e<1< z^Q5d&`W@U^(9rU$QU?8LCDIh0)SN&icbGZF=Rt`ydLok z!2rvLK*mZp@;QjUgiUHmQ|DF&YI0?5RQClO(yWvZ5dxK3)Kn(dhGkw$E}^2-8MJQaFM~UkH%5LqWt_% zB&`jbb**S?fr*6i4#rpQ#xEdaOwN$3@wi61R8|=4>h6WVXNQfcH`_rYSgFjovHEl$ zQ4!j5A@Rx3FK1JP!C;o=1c>_lV_DesEXs`(#?`KaDeGA~A zZt57cZhqjcQs4bKH{N3h72-t<3Tm8Peg{kDPtriP zo~vr6^t(_ZJdB}LnD!3^Gmp_<30?<5$`=za;&bdzD&rcosR|sjFe^DHwRq{gM--g4 zFq%IpRP#RA_3$k-F-G-qS7nUx+gs^NL{U}zqL2ik)1}eYX_$=pl^Yc5nL({|R?okt zEI~A;(WYo0x61AON^I05Z~KJ9`*J-o7w;NbXoHuwpB9I|9!>XfE*+D}=Au$4XroSq z!(BhM{e+b=F@gk@QX-l8u=85_%5WuGp+@KP1nn3P`Y9I`@3BcM&vjp|7b&unZaIU9 zLu3}!hb`F6DeuJK9w4u_Rqd&+7i0O#T zAvn@!f)~D8x&NfZ>y3@SROvVu31W1h--F*5FP(V?KUixpqq<_{)}!j|!3#H8_i`Q% zESZ;Cl#aSxvv|@b&($Vx6cyNbx~hq=Yxom3tB^Fe9L%AJeTr|8*L{Q~Y~r`rv<5D%<@$y>t7ay_&nsd(b^ zURT=(3D>IKG2C#7<;KR?(dEjVIB@H9oknk^%z$Ef71>7G0`9P5V-hQ+pCA@_E0jSqi+On7N9KNw>2@FC*{j0)iKRsD9+-?MM1+ z+y>9i1j@tgvYGdk3mhCdfWvg`anAUBL|IEXBM$0Wk3u-S-@R%1Sm7}7ysS}UEwtwa zd)j~0>c1-XAGP|gqW$m6^?%#?|GWxQaCj^ a.name === 'root' && a.host === 'localhost:9001') as Account + expect(rootServer1List).not.to.be.undefined + + const resAccount = await getAccount(server.url, rootServer1List.id) + const rootServer1Get = resAccount.body as Account + expect(rootServer1Get.name).to.equal('root') + expect(rootServer1Get.host).to.equal('localhost:9001') + + const test = await testVideoImage(server.url, 'avatar2-resized', rootServer1Get.avatar.path, '.png') + expect(test).to.equal(true) + } + }) + + after(async function () { + killallServers(servers) + + // Keep the logs if the test failed + if (this[ 'ok' ]) { + await flushTests() + } + }) +}) diff --git a/server/tests/utils/users/accounts.ts b/server/tests/utils/users/accounts.ts new file mode 100644 index 000000000..71712100e --- /dev/null +++ b/server/tests/utils/users/accounts.ts @@ -0,0 +1,29 @@ +import { makeGetRequest } from '../requests/requests' + +function getAccountsList (url: string, sort = '-createdAt', statusCodeExpected = 200) { + const path = '/api/v1/accounts' + + return makeGetRequest({ + url, + query: { sort }, + path, + statusCodeExpected + }) +} + +function getAccount (url: string, accountId: number | string, statusCodeExpected = 200) { + const path = '/api/v1/accounts/' + accountId + + return makeGetRequest({ + url, + path, + statusCodeExpected + }) +} + +// --------------------------------------------------------------------------- + +export { + getAccount, + getAccountsList +} diff --git a/shared/models/activitypub/activity.ts b/shared/models/activitypub/activity.ts index 48b52d2cb..a87afc548 100644 --- a/shared/models/activitypub/activity.ts +++ b/shared/models/activitypub/activity.ts @@ -1,3 +1,4 @@ +import { ActivityPubActor } from './activitypub-actor' import { ActivityPubSignature } from './activitypub-signature' import { VideoTorrentObject } from './objects' import { DislikeObject } from './objects/dislike-object' @@ -33,7 +34,7 @@ export interface ActivityCreate extends BaseActivity { export interface ActivityUpdate extends BaseActivity { type: 'Update' - object: VideoTorrentObject + object: VideoTorrentObject | ActivityPubActor } export interface ActivityDelete extends BaseActivity {