--- /dev/null
+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))
+}
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()
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)
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,
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
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)
}, { 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 }
})
await user.save()
+ // Don't need to send this update to followers, these attributes are not propagated
+
return res.sendStatus(204)
}
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'
}
function checkUpdateActivity (activity: any) {
- return isVideoTorrentUpdateActivityValid(activity)
+ return isVideoTorrentUpdateActivityValid(activity) ||
+ isActorUpdateActivityValid(activity)
}
function checkDeleteActivity (activity: any) {
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) {
return isBaseActivityValid(activity, 'Accept')
}
+function isActorUpdateActivityValid (activity: any) {
+ return isBaseActivityValid(activity, 'Update') &&
+ isActorObjectValid(activity.object)
+}
+
// ---------------------------------------------------------------------------
export {
isActorPublicKeyValid,
isActorPreferredUsernameValid,
isActorPrivateKeyValid,
- isRemoteActorValid,
+ isActorObjectValid,
isActorFollowingCountValid,
isActorFollowersCountValid,
isActorFollowActivityValid,
isActorAcceptActivityValid,
isActorDeleteActivityValid,
- isActorNameValid
+ isActorUpdateActivityValid
}
// 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' ],
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'
})
}
+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
}
// ---------------------------------------------------------------------------
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
}
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 {
}
}
-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,
import * as Bluebird from 'bluebird'
import { ActivityUpdate } from '../../../../shared/models/activitypub'
+import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor'
+import { VideoTorrentObject } from '../../../../shared/models/activitypub/objects'
import { retryTransactionWrapper } from '../../../helpers/database-utils'
import { logger } from '../../../helpers/logger'
import { resetSequelizeInstance } from '../../../helpers/utils'
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 { getOrCreateActorAndServerAndModel } from '../actor'
+import { fetchActorTotalItems, fetchAvatarIfExists, getOrCreateActorAndServerAndModel } from '../actor'
import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc'
async function processUpdateActivity (activity: ActivityUpdate) {
if (activity.object.type === 'Video') {
return processUpdateVideo(actor, activity)
+ } else if (activity.object.type === 'Person') {
+ return processUpdateAccount(actor, activity)
}
return
}
async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) {
- const videoAttributesToUpdate = activity.object
+ const videoAttributesToUpdate = activity.object as VideoTorrentObject
logger.debug('Updating remote video "%s".', videoAttributesToUpdate.uuid)
let videoInstance: VideoModel
- let videoFieldsSave: object
+ let videoFieldsSave: any
try {
await sequelizeTypescript.transaction(async t => {
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)
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
+ }
+}
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'
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
}
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'
setFollowersSort,
setFollowingSort,
setJobsSort,
- setVideoCommentThreadsSort
+ setVideoCommentThreadsSort,
+ setAccountsSort
}
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'
}
]
+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
}
// 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)
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)
videoChannelsSortValidator,
videosSortValidator,
blacklistSortValidator,
+ accountsSortValidator,
followersSortValidator,
followingSortValidator,
jobsSortValidator,
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'
{
model: () => ServerModel,
required: false
+ },
+ {
+ model: () => AvatarModel,
+ required: false
}
]
}
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 = {
getAvatarUrl () {
if (!this.avatarId) return undefined
- return CONFIG.WEBSERVER.URL + this.Avatar.getWebserverPath
+ return CONFIG.WEBSERVER.URL + this.Avatar.getWebserverPath()
}
}
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',
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))
+ }
}
--- /dev/null
+/* 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()
+ }
+ })
+})
// Order of the tests we want to execute
+import './accounts'
import './follows'
import './jobs'
import './services'
import './server/follows'
import './server/jobs'
import './videos/video-comments'
+import './users/users-multiple-servers'
--- /dev/null
+/* tslint:disable:no-unused-expression */
+
+import * as chai from 'chai'
+import 'mocha'
+import { Account } from '../../../../shared/models/actors'
+import { doubleFollow, flushAndRunMultipleServers, wait } from '../../utils'
+import {
+ flushTests, getMyUserInformation, killallServers, ServerInfo, testVideoImage, updateMyAvatar,
+ uploadVideo
+} from '../../utils/index'
+import { getAccount, getAccountsList } from '../../utils/users/accounts'
+import { setAccessTokensToServers } from '../../utils/users/login'
+
+const expect = chai.expect
+
+describe('Test users with multiple servers', function () {
+ let servers: ServerInfo[] = []
+
+ before(async function () {
+ this.timeout(120000)
+
+ servers = await flushAndRunMultipleServers(3)
+
+ // Get the access tokens
+ await setAccessTokensToServers(servers)
+
+ // Server 1 and server 2 follow each other
+ await doubleFollow(servers[0], servers[1])
+ // Server 1 and server 3 follow each other
+ await doubleFollow(servers[0], servers[2])
+ // Server 2 and server 3 follow each other
+ await doubleFollow(servers[1], servers[2])
+
+ // The root user of server 1 is propagated to servers 2 and 3
+ await uploadVideo(servers[0].url, servers[0].accessToken, {})
+
+ await wait(5000)
+ })
+
+ it('Should be able to update my avatar', async function () {
+ this.timeout(10000)
+
+ const fixture = 'avatar2.png'
+
+ await updateMyAvatar({
+ url: servers[0].url,
+ accessToken: servers[0].accessToken,
+ fixture
+ })
+
+ const res = await getMyUserInformation(servers[0].url, servers[0].accessToken)
+ const user = res.body
+
+ const test = await testVideoImage(servers[0].url, 'avatar2-resized', user.account.avatar.path, '.png')
+ expect(test).to.equal(true)
+
+ await wait(5000)
+ })
+
+ it('Should have updated my avatar on other servers too', async function () {
+ for (const server of servers) {
+ const resAccounts = await getAccountsList(server.url, '-createdAt')
+
+ const rootServer1List = resAccounts.body.data.find(a => 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()
+ }
+ })
+})
--- /dev/null
+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
+}
+import { ActivityPubActor } from './activitypub-actor'
import { ActivityPubSignature } from './activitypub-signature'
import { VideoTorrentObject } from './objects'
import { DislikeObject } from './objects/dislike-object'
export interface ActivityUpdate extends BaseActivity {
type: 'Update'
- object: VideoTorrentObject
+ object: VideoTorrentObject | ActivityPubActor
}
export interface ActivityDelete extends BaseActivity {