aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--server/controllers/api/accounts.ts38
-rw-r--r--server/controllers/api/index.ts2
-rw-r--r--server/controllers/api/users.ts17
-rw-r--r--server/helpers/custom-validators/activitypub/activity.ts5
-rw-r--r--server/helpers/custom-validators/activitypub/actor.ts37
-rw-r--r--server/initializers/constants.ts1
-rw-r--r--server/lib/activitypub/actor.ts86
-rw-r--r--server/lib/activitypub/process/process-update.ts94
-rw-r--r--server/lib/activitypub/send/send-update.ts16
-rw-r--r--server/middlewares/sort.ts9
-rw-r--r--server/middlewares/validators/account.ts19
-rw-r--r--server/middlewares/validators/sort.ts3
-rw-r--r--server/models/account/account.ts23
-rw-r--r--server/models/activitypub/actor.ts2
-rw-r--r--server/models/video/video-share.ts40
-rw-r--r--server/tests/api/check-params/accounts.ts51
-rw-r--r--server/tests/api/check-params/index.ts1
-rw-r--r--server/tests/api/fixtures/avatar2-resized.pngbin0 -> 2350 bytes
-rw-r--r--server/tests/api/fixtures/avatar2.pngbin0 -> 4850 bytes
-rw-r--r--server/tests/api/index-slow.ts1
-rw-r--r--server/tests/api/users/users-multiple-servers.ts85
-rw-r--r--server/tests/utils/users/accounts.ts29
-rw-r--r--shared/models/activitypub/activity.ts3
23 files changed, 493 insertions, 69 deletions
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 @@
1import * as express from 'express'
2import { getFormattedObjects } from '../../helpers/utils'
3import { asyncMiddleware, paginationValidator, setAccountsSort, setPagination } from '../../middlewares'
4import { accountsGetValidator, accountsSortValidator } from '../../middlewares/validators'
5import { AccountModel } from '../../models/account/account'
6
7const accountsRouter = express.Router()
8
9accountsRouter.get('/',
10 paginationValidator,
11 accountsSortValidator,
12 setAccountsSort,
13 setPagination,
14 asyncMiddleware(listAccounts)
15)
16
17accountsRouter.get('/:id',
18 asyncMiddleware(accountsGetValidator),
19 getAccount
20)
21
22// ---------------------------------------------------------------------------
23
24export {
25 accountsRouter
26}
27
28// ---------------------------------------------------------------------------
29
30function getAccount (req: express.Request, res: express.Response, next: express.NextFunction) {
31 return res.json(res.locals.account.toFormattedJSON())
32}
33
34async function listAccounts (req: express.Request, res: express.Response, next: express.NextFunction) {
35 const resultList = await AccountModel.listForApi(req.query.start, req.query.count, req.query.sort)
36
37 return res.json(getFormattedObjects(resultList.data, resultList.total))
38}
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'
5import { oauthClientsRouter } from './oauth-clients' 5import { oauthClientsRouter } from './oauth-clients'
6import { serverRouter } from './server' 6import { serverRouter } from './server'
7import { usersRouter } from './users' 7import { usersRouter } from './users'
8import { accountsRouter } from './accounts'
8import { videosRouter } from './videos' 9import { videosRouter } from './videos'
9 10
10const apiRouter = express.Router() 11const apiRouter = express.Router()
@@ -13,6 +14,7 @@ apiRouter.use('/server', serverRouter)
13apiRouter.use('/oauth-clients', oauthClientsRouter) 14apiRouter.use('/oauth-clients', oauthClientsRouter)
14apiRouter.use('/config', configRouter) 15apiRouter.use('/config', configRouter)
15apiRouter.use('/users', usersRouter) 16apiRouter.use('/users', usersRouter)
17apiRouter.use('/accounts', accountsRouter)
16apiRouter.use('/videos', videosRouter) 18apiRouter.use('/videos', videosRouter)
17apiRouter.use('/jobs', jobsRouter) 19apiRouter.use('/jobs', jobsRouter)
18apiRouter.use('/ping', pong) 20apiRouter.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'
8import { logger } from '../../helpers/logger' 8import { logger } from '../../helpers/logger'
9import { createReqFiles, getFormattedObjects } from '../../helpers/utils' 9import { createReqFiles, getFormattedObjects } from '../../helpers/utils'
10import { AVATAR_MIMETYPE_EXT, AVATARS_SIZE, CONFIG, sequelizeTypescript } from '../../initializers' 10import { AVATAR_MIMETYPE_EXT, AVATARS_SIZE, CONFIG, sequelizeTypescript } from '../../initializers'
11import { sendUpdateUser } from '../../lib/activitypub/send'
11import { createUserAccountAndChannel } from '../../lib/user' 12import { createUserAccountAndChannel } from '../../lib/user'
12import { 13import {
13 asyncMiddleware, authenticate, ensureUserHasRight, ensureUserRegistrationAllowed, paginationValidator, setPagination, setUsersSort, 14 asyncMiddleware, authenticate, ensureUserHasRight, ensureUserRegistrationAllowed, paginationValidator, setPagination, setUsersSort,
@@ -217,7 +218,6 @@ async function removeUser (req: express.Request, res: express.Response, next: ex
217async function updateMe (req: express.Request, res: express.Response, next: express.NextFunction) { 218async function updateMe (req: express.Request, res: express.Response, next: express.NextFunction) {
218 const body: UserUpdateMe = req.body 219 const body: UserUpdateMe = req.body
219 220
220 // FIXME: user is not already a Sequelize instance?
221 const user = res.locals.oauth.token.user 221 const user = res.locals.oauth.token.user
222 222
223 if (body.password !== undefined) user.password = body.password 223 if (body.password !== undefined) user.password = body.password
@@ -226,13 +226,15 @@ async function updateMe (req: express.Request, res: express.Response, next: expr
226 if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo 226 if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo
227 227
228 await user.save() 228 await user.save()
229 await sendUpdateUser(user, undefined)
229 230
230 return res.sendStatus(204) 231 return res.sendStatus(204)
231} 232}
232 233
233async function updateMyAvatar (req: express.Request, res: express.Response, next: express.NextFunction) { 234async function updateMyAvatar (req: express.Request, res: express.Response, next: express.NextFunction) {
234 const avatarPhysicalFile = req.files['avatarfile'][0] 235 const avatarPhysicalFile = req.files['avatarfile'][0]
235 const actor = res.locals.oauth.token.user.Account.Actor 236 const user = res.locals.oauth.token.user
237 const actor = user.Account.Actor
236 238
237 const avatarDir = CONFIG.STORAGE.AVATARS_DIR 239 const avatarDir = CONFIG.STORAGE.AVATARS_DIR
238 const source = join(avatarDir, avatarPhysicalFile.filename) 240 const source = join(avatarDir, avatarPhysicalFile.filename)
@@ -252,12 +254,19 @@ async function updateMyAvatar (req: express.Request, res: express.Response, next
252 }, { transaction: t }) 254 }, { transaction: t })
253 255
254 if (actor.Avatar) { 256 if (actor.Avatar) {
255 await actor.Avatar.destroy({ transaction: t }) 257 try {
258 await actor.Avatar.destroy({ transaction: t })
259 } catch (err) {
260 logger.error('Cannot remove old avatar of user %s.', user.username, err)
261 }
256 } 262 }
257 263
258 actor.set('avatarId', avatar.id) 264 actor.set('avatarId', avatar.id)
265 actor.Avatar = avatar
259 await actor.save({ transaction: t }) 266 await actor.save({ transaction: t })
260 267
268 await sendUpdateUser(user, undefined)
269
261 return { actor, avatar } 270 return { actor, avatar }
262 }) 271 })
263 272
@@ -278,6 +287,8 @@ async function updateUser (req: express.Request, res: express.Response, next: ex
278 287
279 await user.save() 288 await user.save()
280 289
290 // Don't need to send this update to followers, these attributes are not propagated
291
281 return res.sendStatus(204) 292 return res.sendStatus(204)
282} 293}
283 294
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 @@
1import * as validator from 'validator' 1import * as validator from 'validator'
2import { Activity, ActivityType } from '../../../../shared/models/activitypub' 2import { Activity, ActivityType } from '../../../../shared/models/activitypub'
3import { isActorAcceptActivityValid, isActorDeleteActivityValid, isActorFollowActivityValid } from './actor' 3import { isActorAcceptActivityValid, isActorDeleteActivityValid, isActorFollowActivityValid, isActorUpdateActivityValid } from './actor'
4import { isAnnounceActivityValid } from './announce' 4import { isAnnounceActivityValid } from './announce'
5import { isActivityPubUrlValid } from './misc' 5import { isActivityPubUrlValid } from './misc'
6import { isDislikeActivityValid, isLikeActivityValid } from './rate' 6import { isDislikeActivityValid, isLikeActivityValid } from './rate'
@@ -64,7 +64,8 @@ function checkCreateActivity (activity: any) {
64} 64}
65 65
66function checkUpdateActivity (activity: any) { 66function checkUpdateActivity (activity: any) {
67 return isVideoTorrentUpdateActivityValid(activity) 67 return isVideoTorrentUpdateActivityValid(activity) ||
68 isActorUpdateActivityValid(activity)
68} 69}
69 70
70function checkDeleteActivity (activity: any) { 71function 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) {
45 validator.isLength(privateKey, CONSTRAINTS_FIELDS.ACTORS.PRIVATE_KEY) 45 validator.isLength(privateKey, CONSTRAINTS_FIELDS.ACTORS.PRIVATE_KEY)
46} 46}
47 47
48function isRemoteActorValid (remoteActor: any) { 48function isActorObjectValid (actor: any) {
49 return exists(remoteActor) && 49 return exists(actor) &&
50 isActivityPubUrlValid(remoteActor.id) && 50 isActivityPubUrlValid(actor.id) &&
51 isActorTypeValid(remoteActor.type) && 51 isActorTypeValid(actor.type) &&
52 isActivityPubUrlValid(remoteActor.following) && 52 isActivityPubUrlValid(actor.following) &&
53 isActivityPubUrlValid(remoteActor.followers) && 53 isActivityPubUrlValid(actor.followers) &&
54 isActivityPubUrlValid(remoteActor.inbox) && 54 isActivityPubUrlValid(actor.inbox) &&
55 isActivityPubUrlValid(remoteActor.outbox) && 55 isActivityPubUrlValid(actor.outbox) &&
56 isActorPreferredUsernameValid(remoteActor.preferredUsername) && 56 isActorPreferredUsernameValid(actor.preferredUsername) &&
57 isActivityPubUrlValid(remoteActor.url) && 57 isActivityPubUrlValid(actor.url) &&
58 isActorPublicKeyObjectValid(remoteActor.publicKey) && 58 isActorPublicKeyObjectValid(actor.publicKey) &&
59 isActorEndpointsObjectValid(remoteActor.endpoints) && 59 isActorEndpointsObjectValid(actor.endpoints) &&
60 setValidAttributedTo(remoteActor) && 60 setValidAttributedTo(actor) &&
61 // If this is not an account, it should be attributed to an account 61 // If this is not an account, it should be attributed to an account
62 // In PeerTube we use this to attach a video channel to a specific account 62 // In PeerTube we use this to attach a video channel to a specific account
63 (remoteActor.type === 'Person' || remoteActor.attributedTo.length !== 0) 63 (actor.type === 'Person' || actor.attributedTo.length !== 0)
64} 64}
65 65
66function isActorFollowingCountValid (value: string) { 66function isActorFollowingCountValid (value: string) {
@@ -84,6 +84,11 @@ function isActorAcceptActivityValid (activity: any) {
84 return isBaseActivityValid(activity, 'Accept') 84 return isBaseActivityValid(activity, 'Accept')
85} 85}
86 86
87function isActorUpdateActivityValid (activity: any) {
88 return isBaseActivityValid(activity, 'Update') &&
89 isActorObjectValid(activity.object)
90}
91
87// --------------------------------------------------------------------------- 92// ---------------------------------------------------------------------------
88 93
89export { 94export {
@@ -93,11 +98,11 @@ export {
93 isActorPublicKeyValid, 98 isActorPublicKeyValid,
94 isActorPreferredUsernameValid, 99 isActorPreferredUsernameValid,
95 isActorPrivateKeyValid, 100 isActorPrivateKeyValid,
96 isRemoteActorValid, 101 isActorObjectValid,
97 isActorFollowingCountValid, 102 isActorFollowingCountValid,
98 isActorFollowersCountValid, 103 isActorFollowersCountValid,
99 isActorFollowActivityValid, 104 isActorFollowActivityValid,
100 isActorAcceptActivityValid, 105 isActorAcceptActivityValid,
101 isActorDeleteActivityValid, 106 isActorDeleteActivityValid,
102 isActorNameValid 107 isActorUpdateActivityValid
103} 108}
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
22// Sortable columns per schema 22// Sortable columns per schema
23const SORTABLE_COLUMNS = { 23const SORTABLE_COLUMNS = {
24 USERS: [ 'id', 'username', 'createdAt' ], 24 USERS: [ 'id', 'username', 'createdAt' ],
25 ACCOUNTS: [ 'createdAt' ],
25 JOBS: [ 'id', 'createdAt' ], 26 JOBS: [ 'id', 'createdAt' ],
26 VIDEO_ABUSES: [ 'id', 'createdAt' ], 27 VIDEO_ABUSES: [ 'id', 'createdAt' ],
27 VIDEO_CHANNELS: [ 'id', 'name', 'updatedAt', 'createdAt' ], 28 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'
5import * as uuidv4 from 'uuid/v4' 5import * as uuidv4 from 'uuid/v4'
6import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub' 6import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub'
7import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' 7import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
8import { isRemoteActorValid } from '../../helpers/custom-validators/activitypub/actor' 8import { isActorObjectValid } from '../../helpers/custom-validators/activitypub/actor'
9import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 9import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
10import { retryTransactionWrapper } from '../../helpers/database-utils' 10import { retryTransactionWrapper } from '../../helpers/database-utils'
11import { logger } from '../../helpers/logger' 11import { logger } from '../../helpers/logger'
12import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto' 12import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto'
13import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests' 13import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
14import { CONFIG, sequelizeTypescript } from '../../initializers' 14import { AVATAR_MIMETYPE_EXT, CONFIG, sequelizeTypescript } from '../../initializers'
15import { AccountModel } from '../../models/account/account' 15import { AccountModel } from '../../models/account/account'
16import { ActorModel } from '../../models/activitypub/actor' 16import { ActorModel } from '../../models/activitypub/actor'
17import { AvatarModel } from '../../models/avatar/avatar' 17import { AvatarModel } from '../../models/avatar/avatar'
@@ -84,10 +84,52 @@ function buildActorInstance (type: ActivityPubActorType, url: string, preferredU
84 }) 84 })
85} 85}
86 86
87async function fetchActorTotalItems (url: string) {
88 const options = {
89 uri: url,
90 method: 'GET',
91 json: true,
92 activityPub: true
93 }
94
95 let requestResult
96 try {
97 requestResult = await doRequest(options)
98 } catch (err) {
99 logger.warn('Cannot fetch remote actor count %s.', url, err)
100 return undefined
101 }
102
103 return requestResult.totalItems ? requestResult.totalItems : 0
104}
105
106async function fetchAvatarIfExists (actorJSON: ActivityPubActor) {
107 if (
108 actorJSON.icon && actorJSON.icon.type === 'Image' && AVATAR_MIMETYPE_EXT[actorJSON.icon.mediaType] !== undefined &&
109 isActivityPubUrlValid(actorJSON.icon.url)
110 ) {
111 const extension = AVATAR_MIMETYPE_EXT[actorJSON.icon.mediaType]
112
113 const avatarName = uuidv4() + extension
114 const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)
115
116 await doRequestAndSaveToFile({
117 method: 'GET',
118 uri: actorJSON.icon.url
119 }, destPath)
120
121 return avatarName
122 }
123
124 return undefined
125}
126
87export { 127export {
88 getOrCreateActorAndServerAndModel, 128 getOrCreateActorAndServerAndModel,
89 buildActorInstance, 129 buildActorInstance,
90 setAsyncActorKeys 130 setAsyncActorKeys,
131 fetchActorTotalItems,
132 fetchAvatarIfExists
91} 133}
92 134
93// --------------------------------------------------------------------------- 135// ---------------------------------------------------------------------------
@@ -166,7 +208,7 @@ async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResu
166 const requestResult = await doRequest(options) 208 const requestResult = await doRequest(options)
167 const actorJSON: ActivityPubActor = requestResult.body 209 const actorJSON: ActivityPubActor = requestResult.body
168 210
169 if (isRemoteActorValid(actorJSON) === false) { 211 if (isActorObjectValid(actorJSON) === false) {
170 logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON }) 212 logger.debug('Remote actor JSON is not valid.', { actorJSON: actorJSON })
171 return undefined 213 return undefined
172 } 214 }
@@ -190,22 +232,7 @@ async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResu
190 followingUrl: actorJSON.following 232 followingUrl: actorJSON.following
191 }) 233 })
192 234
193 // Fetch icon? 235 const avatarName = await fetchAvatarIfExists(actorJSON)
194 let avatarName: string = undefined
195 if (
196 actorJSON.icon && actorJSON.icon.type === 'Image' && actorJSON.icon.mediaType === 'image/png' &&
197 isActivityPubUrlValid(actorJSON.icon.url)
198 ) {
199 const extension = actorJSON.icon.mediaType === 'image/png' ? '.png' : '.jpg'
200
201 avatarName = uuidv4() + extension
202 const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName)
203
204 await doRequestAndSaveToFile({
205 method: 'GET',
206 uri: actorJSON.icon.url
207 }, destPath)
208 }
209 236
210 const name = actorJSON.name || actorJSON.preferredUsername 237 const name = actorJSON.name || actorJSON.preferredUsername
211 return { 238 return {
@@ -217,25 +244,6 @@ async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResu
217 } 244 }
218} 245}
219 246
220async function fetchActorTotalItems (url: string) {
221 const options = {
222 uri: url,
223 method: 'GET',
224 json: true,
225 activityPub: true
226 }
227
228 let requestResult
229 try {
230 requestResult = await doRequest(options)
231 } catch (err) {
232 logger.warn('Cannot fetch remote actor count %s.', url, err)
233 return undefined
234 }
235
236 return requestResult.totalItems ? requestResult.totalItems : 0
237}
238
239function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) { 247function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) {
240 const account = new AccountModel({ 248 const account = new AccountModel({
241 name: result.name, 249 name: result.name,
diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts
index bc8ae5cc6..05ea7d272 100644
--- a/server/lib/activitypub/process/process-update.ts
+++ b/server/lib/activitypub/process/process-update.ts
@@ -1,14 +1,18 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { ActivityUpdate } from '../../../../shared/models/activitypub' 2import { ActivityUpdate } from '../../../../shared/models/activitypub'
3import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor'
4import { VideoTorrentObject } from '../../../../shared/models/activitypub/objects'
3import { retryTransactionWrapper } from '../../../helpers/database-utils' 5import { retryTransactionWrapper } from '../../../helpers/database-utils'
4import { logger } from '../../../helpers/logger' 6import { logger } from '../../../helpers/logger'
5import { resetSequelizeInstance } from '../../../helpers/utils' 7import { resetSequelizeInstance } from '../../../helpers/utils'
6import { sequelizeTypescript } from '../../../initializers' 8import { sequelizeTypescript } from '../../../initializers'
9import { AccountModel } from '../../../models/account/account'
7import { ActorModel } from '../../../models/activitypub/actor' 10import { ActorModel } from '../../../models/activitypub/actor'
11import { AvatarModel } from '../../../models/avatar/avatar'
8import { TagModel } from '../../../models/video/tag' 12import { TagModel } from '../../../models/video/tag'
9import { VideoModel } from '../../../models/video/video' 13import { VideoModel } from '../../../models/video/video'
10import { VideoFileModel } from '../../../models/video/video-file' 14import { VideoFileModel } from '../../../models/video/video-file'
11import { getOrCreateActorAndServerAndModel } from '../actor' 15import { fetchActorTotalItems, fetchAvatarIfExists, getOrCreateActorAndServerAndModel } from '../actor'
12import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc' 16import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc'
13 17
14async function processUpdateActivity (activity: ActivityUpdate) { 18async function processUpdateActivity (activity: ActivityUpdate) {
@@ -16,6 +20,8 @@ async function processUpdateActivity (activity: ActivityUpdate) {
16 20
17 if (activity.object.type === 'Video') { 21 if (activity.object.type === 'Video') {
18 return processUpdateVideo(actor, activity) 22 return processUpdateVideo(actor, activity)
23 } else if (activity.object.type === 'Person') {
24 return processUpdateAccount(actor, activity)
19 } 25 }
20 26
21 return 27 return
@@ -39,11 +45,11 @@ function processUpdateVideo (actor: ActorModel, activity: ActivityUpdate) {
39} 45}
40 46
41async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) { 47async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) {
42 const videoAttributesToUpdate = activity.object 48 const videoAttributesToUpdate = activity.object as VideoTorrentObject
43 49
44 logger.debug('Updating remote video "%s".', videoAttributesToUpdate.uuid) 50 logger.debug('Updating remote video "%s".', videoAttributesToUpdate.uuid)
45 let videoInstance: VideoModel 51 let videoInstance: VideoModel
46 let videoFieldsSave: object 52 let videoFieldsSave: any
47 53
48 try { 54 try {
49 await sequelizeTypescript.transaction(async t => { 55 await sequelizeTypescript.transaction(async t => {
@@ -54,6 +60,8 @@ async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) {
54 const videoInstance = await VideoModel.loadByUrlAndPopulateAccount(videoAttributesToUpdate.id, t) 60 const videoInstance = await VideoModel.loadByUrlAndPopulateAccount(videoAttributesToUpdate.id, t)
55 if (!videoInstance) throw new Error('Video ' + videoAttributesToUpdate.id + ' not found.') 61 if (!videoInstance) throw new Error('Video ' + videoAttributesToUpdate.id + ' not found.')
56 62
63 videoFieldsSave = videoInstance.toJSON()
64
57 const videoChannel = videoInstance.VideoChannel 65 const videoChannel = videoInstance.VideoChannel
58 if (videoChannel.Account.Actor.id !== actor.id) { 66 if (videoChannel.Account.Actor.id !== actor.id) {
59 throw new Error('Account ' + actor.url + ' does not own video channel ' + videoChannel.Actor.url) 67 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) {
102 throw err 110 throw err
103 } 111 }
104} 112}
113
114function processUpdateAccount (actor: ActorModel, activity: ActivityUpdate) {
115 const options = {
116 arguments: [ actor, activity ],
117 errorMessage: 'Cannot update the remote account with many retries'
118 }
119
120 return retryTransactionWrapper(updateRemoteAccount, options)
121}
122
123async function updateRemoteAccount (actor: ActorModel, activity: ActivityUpdate) {
124 const accountAttributesToUpdate = activity.object as ActivityPubActor
125
126 logger.debug('Updating remote account "%s".', accountAttributesToUpdate.uuid)
127 let actorInstance: ActorModel
128 let accountInstance: AccountModel
129 let actorFieldsSave: object
130 let accountFieldsSave: object
131
132 // Fetch icon?
133 const avatarName = await fetchAvatarIfExists(accountAttributesToUpdate)
134
135 try {
136 await sequelizeTypescript.transaction(async t => {
137 actorInstance = await ActorModel.loadByUrl(accountAttributesToUpdate.id, t)
138 if (!actorInstance) throw new Error('Actor ' + accountAttributesToUpdate.id + ' not found.')
139
140 actorFieldsSave = actorInstance.toJSON()
141 accountInstance = actorInstance.Account
142 accountFieldsSave = actorInstance.Account.toJSON()
143
144 const followersCount = await fetchActorTotalItems(accountAttributesToUpdate.followers)
145 const followingCount = await fetchActorTotalItems(accountAttributesToUpdate.following)
146
147 actorInstance.set('type', accountAttributesToUpdate.type)
148 actorInstance.set('uuid', accountAttributesToUpdate.uuid)
149 actorInstance.set('preferredUsername', accountAttributesToUpdate.preferredUsername)
150 actorInstance.set('url', accountAttributesToUpdate.id)
151 actorInstance.set('publicKey', accountAttributesToUpdate.publicKey.publicKeyPem)
152 actorInstance.set('followersCount', followersCount)
153 actorInstance.set('followingCount', followingCount)
154 actorInstance.set('inboxUrl', accountAttributesToUpdate.inbox)
155 actorInstance.set('outboxUrl', accountAttributesToUpdate.outbox)
156 actorInstance.set('sharedInboxUrl', accountAttributesToUpdate.endpoints.sharedInbox)
157 actorInstance.set('followersUrl', accountAttributesToUpdate.followers)
158 actorInstance.set('followingUrl', accountAttributesToUpdate.following)
159
160 if (avatarName !== undefined) {
161 if (actorInstance.avatarId) {
162 await actorInstance.Avatar.destroy({ transaction: t })
163 }
164
165 const avatar = await AvatarModel.create({
166 filename: avatarName
167 }, { transaction: t })
168
169 actor.set('avatarId', avatar.id)
170 }
171
172 await actor.save({ transaction: t })
173
174 actor.Account.set('name', accountAttributesToUpdate.name || accountAttributesToUpdate.preferredUsername)
175 await actor.Account.save({ transaction: t })
176 })
177
178 logger.info('Remote account with uuid %s updated', accountAttributesToUpdate.uuid)
179 } catch (err) {
180 if (actorInstance !== undefined && actorFieldsSave !== undefined) {
181 resetSequelizeInstance(actorInstance, actorFieldsSave)
182 }
183
184 if (accountInstance !== undefined && accountFieldsSave !== undefined) {
185 resetSequelizeInstance(accountInstance, accountFieldsSave)
186 }
187
188 // This is just a debug because we will retry the insert
189 logger.debug('Cannot update the remote account.', err)
190 throw err
191 }
192}
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 @@
1import { Transaction } from 'sequelize' 1import { Transaction } from 'sequelize'
2import { ActivityAudience, ActivityUpdate } from '../../../../shared/models/activitypub' 2import { ActivityAudience, ActivityUpdate } from '../../../../shared/models/activitypub'
3import { VideoPrivacy } from '../../../../shared/models/videos' 3import { VideoPrivacy } from '../../../../shared/models/videos'
4import { UserModel } from '../../../models/account/user'
4import { ActorModel } from '../../../models/activitypub/actor' 5import { ActorModel } from '../../../models/activitypub/actor'
5import { VideoModel } from '../../../models/video/video' 6import { VideoModel } from '../../../models/video/video'
6import { VideoShareModel } from '../../../models/video/video-share' 7import { VideoShareModel } from '../../../models/video/video-share'
@@ -22,9 +23,24 @@ async function sendUpdateVideo (video: VideoModel, t: Transaction) {
22 return broadcastToFollowers(data, byActor, actorsInvolved, t) 23 return broadcastToFollowers(data, byActor, actorsInvolved, t)
23} 24}
24 25
26async function sendUpdateUser (user: UserModel, t: Transaction) {
27 const byActor = user.Account.Actor
28
29 const url = getUpdateActivityPubUrl(byActor.url, byActor.updatedAt.toISOString())
30 const accountObject = user.Account.toActivityPubObject()
31 const audience = await getAudience(byActor, t)
32 const data = await updateActivityData(url, byActor, accountObject, t, audience)
33
34 const actorsInvolved = await VideoShareModel.loadActorsByVideoOwner(byActor.id, t)
35 actorsInvolved.push(byActor)
36
37 return broadcastToFollowers(data, byActor, actorsInvolved, t)
38}
39
25// --------------------------------------------------------------------------- 40// ---------------------------------------------------------------------------
26 41
27export { 42export {
43 sendUpdateUser,
28 sendUpdateVideo 44 sendUpdateVideo
29} 45}
30 46
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'
2import 'express-validator' 2import 'express-validator'
3import { SortType } from '../helpers/utils' 3import { SortType } from '../helpers/utils'
4 4
5function setAccountsSort (req: express.Request, res: express.Response, next: express.NextFunction) {
6 if (!req.query.sort) req.query.sort = '-createdAt'
7
8 return next()
9}
10
5function setUsersSort (req: express.Request, res: express.Response, next: express.NextFunction) { 11function setUsersSort (req: express.Request, res: express.Response, next: express.NextFunction) {
6 if (!req.query.sort) req.query.sort = '-createdAt' 12 if (!req.query.sort) req.query.sort = '-createdAt'
7 13
@@ -82,5 +88,6 @@ export {
82 setFollowersSort, 88 setFollowersSort,
83 setFollowingSort, 89 setFollowingSort,
84 setJobsSort, 90 setJobsSort,
85 setVideoCommentThreadsSort 91 setVideoCommentThreadsSort,
92 setAccountsSort
86} 93}
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 @@
1import * as express from 'express' 1import * as express from 'express'
2import { param } from 'express-validator/check' 2import { param } from 'express-validator/check'
3import { isAccountNameValid, isLocalAccountNameExist } from '../../helpers/custom-validators/accounts' 3import { isAccountIdExist, isAccountNameValid, isLocalAccountNameExist } from '../../helpers/custom-validators/accounts'
4import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc'
4import { logger } from '../../helpers/logger' 5import { logger } from '../../helpers/logger'
5import { areValidationErrors } from './utils' 6import { areValidationErrors } from './utils'
6 7
@@ -17,8 +18,22 @@ const localAccountValidator = [
17 } 18 }
18] 19]
19 20
21const accountsGetValidator = [
22 param('id').custom(isIdOrUUIDValid).withMessage('Should have a valid id'),
23
24 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
25 logger.debug('Checking accountsGetValidator parameters', { parameters: req.params })
26
27 if (areValidationErrors(req, res)) return
28 if (!await isAccountIdExist(req.params.id, res)) return
29
30 return next()
31 }
32]
33
20// --------------------------------------------------------------------------- 34// ---------------------------------------------------------------------------
21 35
22export { 36export {
23 localAccountValidator 37 localAccountValidator,
38 accountsGetValidator
24} 39}
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'
6 6
7// Initialize constants here for better performances 7// Initialize constants here for better performances
8const SORTABLE_USERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USERS) 8const SORTABLE_USERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USERS)
9const SORTABLE_ACCOUNTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.ACCOUNTS)
9const SORTABLE_JOBS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.JOBS) 10const SORTABLE_JOBS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.JOBS)
10const SORTABLE_VIDEO_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_ABUSES) 11const SORTABLE_VIDEO_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_ABUSES)
11const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS) 12const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS)
@@ -16,6 +17,7 @@ const SORTABLE_FOLLOWERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOW
16const SORTABLE_FOLLOWING_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWING) 17const SORTABLE_FOLLOWING_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWING)
17 18
18const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) 19const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS)
20const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS)
19const jobsSortValidator = checkSort(SORTABLE_JOBS_COLUMNS) 21const jobsSortValidator = checkSort(SORTABLE_JOBS_COLUMNS)
20const videoAbusesSortValidator = checkSort(SORTABLE_VIDEO_ABUSES_COLUMNS) 22const videoAbusesSortValidator = checkSort(SORTABLE_VIDEO_ABUSES_COLUMNS)
21const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS) 23const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS)
@@ -33,6 +35,7 @@ export {
33 videoChannelsSortValidator, 35 videoChannelsSortValidator,
34 videosSortValidator, 36 videosSortValidator,
35 blacklistSortValidator, 37 blacklistSortValidator,
38 accountsSortValidator,
36 followersSortValidator, 39 followersSortValidator,
37 followingSortValidator, 40 followingSortValidator,
38 jobsSortValidator, 41 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'
18import { sendDeleteActor } from '../../lib/activitypub/send' 18import { sendDeleteActor } from '../../lib/activitypub/send'
19import { ActorModel } from '../activitypub/actor' 19import { ActorModel } from '../activitypub/actor'
20import { ApplicationModel } from '../application/application' 20import { ApplicationModel } from '../application/application'
21import { AvatarModel } from '../avatar/avatar'
21import { ServerModel } from '../server/server' 22import { ServerModel } from '../server/server'
22import { throwIfNotValid } from '../utils' 23import { getSort, throwIfNotValid } from '../utils'
23import { VideoChannelModel } from '../video/video-channel' 24import { VideoChannelModel } from '../video/video-channel'
24import { UserModel } from './user' 25import { UserModel } from './user'
25 26
@@ -32,6 +33,10 @@ import { UserModel } from './user'
32 { 33 {
33 model: () => ServerModel, 34 model: () => ServerModel,
34 required: false 35 required: false
36 },
37 {
38 model: () => AvatarModel,
39 required: false
35 } 40 }
36 ] 41 ]
37 } 42 }
@@ -166,6 +171,22 @@ export class AccountModel extends Model<AccountModel> {
166 return AccountModel.findOne(query) 171 return AccountModel.findOne(query)
167 } 172 }
168 173
174 static listForApi (start: number, count: number, sort: string) {
175 const query = {
176 offset: start,
177 limit: count,
178 order: [ getSort(sort) ]
179 }
180
181 return AccountModel.findAndCountAll(query)
182 .then(({ rows, count }) => {
183 return {
184 data: rows,
185 total: count
186 }
187 })
188 }
189
169 toFormattedJSON (): Account { 190 toFormattedJSON (): Account {
170 const actor = this.Actor.toFormattedJSON() 191 const actor = this.Actor.toFormattedJSON()
171 const account = { 192 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<ActorModel> {
372 getAvatarUrl () { 372 getAvatarUrl () {
373 if (!this.avatarId) return undefined 373 if (!this.avatarId) return undefined
374 374
375 return CONFIG.WEBSERVER.URL + this.Avatar.getWebserverPath 375 return CONFIG.WEBSERVER.URL + this.Avatar.getWebserverPath()
376 } 376 }
377} 377}
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 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' 2import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
3import { AccountModel } from '../account/account'
3import { ActorModel } from '../activitypub/actor' 4import { ActorModel } from '../activitypub/actor'
4import { VideoModel } from './video' 5import { VideoModel } from './video'
6import { VideoChannelModel } from './video-channel'
5 7
6enum ScopeNames { 8enum ScopeNames {
7 FULL = 'FULL', 9 FULL = 'FULL',
@@ -99,4 +101,42 @@ export class VideoShareModel extends Model<VideoShareModel> {
99 return VideoShareModel.scope(ScopeNames.FULL).findAll(query) 101 return VideoShareModel.scope(ScopeNames.FULL).findAll(query)
100 .then(res => res.map(r => r.Actor)) 102 .then(res => res.map(r => r.Actor))
101 } 103 }
104
105 static loadActorsByVideoOwner (actorOwnerId: number, t: Sequelize.Transaction) {
106 const query = {
107 attributes: [],
108 include: [
109 {
110 model: ActorModel,
111 required: true
112 },
113 {
114 attributes: [],
115 model: VideoModel,
116 required: true,
117 include: [
118 {
119 attributes: [],
120 model: VideoChannelModel.unscoped(),
121 required: true,
122 include: [
123 {
124 attributes: [],
125 model: AccountModel.unscoped(),
126 required: true,
127 where: {
128 actorId: actorOwnerId
129 }
130 }
131 ]
132 }
133 ]
134 }
135 ],
136 transaction: t
137 }
138
139 return VideoShareModel.scope(ScopeNames.FULL).findAll(query)
140 .then(res => res.map(r => r.Actor))
141 }
102} 142}
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 @@
1/* tslint:disable:no-unused-expression */
2
3import 'mocha'
4
5import { flushTests, killallServers, runServer, ServerInfo } from '../../utils'
6import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params'
7import { getAccount } from '../../utils/users/accounts'
8
9describe('Test users API validators', function () {
10 const path = '/api/v1/accounts/'
11 let server: ServerInfo
12
13 // ---------------------------------------------------------------
14
15 before(async function () {
16 this.timeout(20000)
17
18 await flushTests()
19
20 server = await runServer(1)
21 })
22
23 describe('When listing accounts', function () {
24 it('Should fail with a bad start pagination', async function () {
25 await checkBadStartPagination(server.url, path, server.accessToken)
26 })
27
28 it('Should fail with a bad count pagination', async function () {
29 await checkBadCountPagination(server.url, path, server.accessToken)
30 })
31
32 it('Should fail with an incorrect sort', async function () {
33 await checkBadSortPagination(server.url, path, server.accessToken)
34 })
35 })
36
37 describe('When getting an account', function () {
38 it('Should return 404 with a non existing id', async function () {
39 await getAccount(server.url, 4545454, 404)
40 })
41 })
42
43 after(async function () {
44 killallServers([ server ])
45
46 // Keep the logs if the test failed
47 if (this['ok']) {
48 await flushTests()
49 }
50 })
51})
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 @@
1// Order of the tests we want to execute 1// Order of the tests we want to execute
2import './accounts'
2import './follows' 3import './follows'
3import './jobs' 4import './jobs'
4import './services' 5import './services'
diff --git a/server/tests/api/fixtures/avatar2-resized.png b/server/tests/api/fixtures/avatar2-resized.png
new file mode 100644
index 000000000..a2e2613bf
--- /dev/null
+++ b/server/tests/api/fixtures/avatar2-resized.png
Binary files differ
diff --git a/server/tests/api/fixtures/avatar2.png b/server/tests/api/fixtures/avatar2.png
new file mode 100644
index 000000000..dae702190
--- /dev/null
+++ b/server/tests/api/fixtures/avatar2.png
Binary files differ
diff --git a/server/tests/api/index-slow.ts b/server/tests/api/index-slow.ts
index 23b6526c7..fe86fc018 100644
--- a/server/tests/api/index-slow.ts
+++ b/server/tests/api/index-slow.ts
@@ -5,3 +5,4 @@ import './videos/multiple-servers'
5import './server/follows' 5import './server/follows'
6import './server/jobs' 6import './server/jobs'
7import './videos/video-comments' 7import './videos/video-comments'
8import './users/users-multiple-servers'
diff --git a/server/tests/api/users/users-multiple-servers.ts b/server/tests/api/users/users-multiple-servers.ts
new file mode 100644
index 000000000..1c7f011a8
--- /dev/null
+++ b/server/tests/api/users/users-multiple-servers.ts
@@ -0,0 +1,85 @@
1/* tslint:disable:no-unused-expression */
2
3import * as chai from 'chai'
4import 'mocha'
5import { Account } from '../../../../shared/models/actors'
6import { doubleFollow, flushAndRunMultipleServers, wait } from '../../utils'
7import {
8 flushTests, getMyUserInformation, killallServers, ServerInfo, testVideoImage, updateMyAvatar,
9 uploadVideo
10} from '../../utils/index'
11import { getAccount, getAccountsList } from '../../utils/users/accounts'
12import { setAccessTokensToServers } from '../../utils/users/login'
13
14const expect = chai.expect
15
16describe('Test users with multiple servers', function () {
17 let servers: ServerInfo[] = []
18
19 before(async function () {
20 this.timeout(120000)
21
22 servers = await flushAndRunMultipleServers(3)
23
24 // Get the access tokens
25 await setAccessTokensToServers(servers)
26
27 // Server 1 and server 2 follow each other
28 await doubleFollow(servers[0], servers[1])
29 // Server 1 and server 3 follow each other
30 await doubleFollow(servers[0], servers[2])
31 // Server 2 and server 3 follow each other
32 await doubleFollow(servers[1], servers[2])
33
34 // The root user of server 1 is propagated to servers 2 and 3
35 await uploadVideo(servers[0].url, servers[0].accessToken, {})
36
37 await wait(5000)
38 })
39
40 it('Should be able to update my avatar', async function () {
41 this.timeout(10000)
42
43 const fixture = 'avatar2.png'
44
45 await updateMyAvatar({
46 url: servers[0].url,
47 accessToken: servers[0].accessToken,
48 fixture
49 })
50
51 const res = await getMyUserInformation(servers[0].url, servers[0].accessToken)
52 const user = res.body
53
54 const test = await testVideoImage(servers[0].url, 'avatar2-resized', user.account.avatar.path, '.png')
55 expect(test).to.equal(true)
56
57 await wait(5000)
58 })
59
60 it('Should have updated my avatar on other servers too', async function () {
61 for (const server of servers) {
62 const resAccounts = await getAccountsList(server.url, '-createdAt')
63
64 const rootServer1List = resAccounts.body.data.find(a => a.name === 'root' && a.host === 'localhost:9001') as Account
65 expect(rootServer1List).not.to.be.undefined
66
67 const resAccount = await getAccount(server.url, rootServer1List.id)
68 const rootServer1Get = resAccount.body as Account
69 expect(rootServer1Get.name).to.equal('root')
70 expect(rootServer1Get.host).to.equal('localhost:9001')
71
72 const test = await testVideoImage(server.url, 'avatar2-resized', rootServer1Get.avatar.path, '.png')
73 expect(test).to.equal(true)
74 }
75 })
76
77 after(async function () {
78 killallServers(servers)
79
80 // Keep the logs if the test failed
81 if (this[ 'ok' ]) {
82 await flushTests()
83 }
84 })
85})
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 @@
1import { makeGetRequest } from '../requests/requests'
2
3function getAccountsList (url: string, sort = '-createdAt', statusCodeExpected = 200) {
4 const path = '/api/v1/accounts'
5
6 return makeGetRequest({
7 url,
8 query: { sort },
9 path,
10 statusCodeExpected
11 })
12}
13
14function getAccount (url: string, accountId: number | string, statusCodeExpected = 200) {
15 const path = '/api/v1/accounts/' + accountId
16
17 return makeGetRequest({
18 url,
19 path,
20 statusCodeExpected
21 })
22}
23
24// ---------------------------------------------------------------------------
25
26export {
27 getAccount,
28 getAccountsList
29}
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 @@
1import { ActivityPubActor } from './activitypub-actor'
1import { ActivityPubSignature } from './activitypub-signature' 2import { ActivityPubSignature } from './activitypub-signature'
2import { VideoTorrentObject } from './objects' 3import { VideoTorrentObject } from './objects'
3import { DislikeObject } from './objects/dislike-object' 4import { DislikeObject } from './objects/dislike-object'
@@ -33,7 +34,7 @@ export interface ActivityCreate extends BaseActivity {
33 34
34export interface ActivityUpdate extends BaseActivity { 35export interface ActivityUpdate extends BaseActivity {
35 type: 'Update' 36 type: 'Update'
36 object: VideoTorrentObject 37 object: VideoTorrentObject | ActivityPubActor
37} 38}
38 39
39export interface ActivityDelete extends BaseActivity { 40export interface ActivityDelete extends BaseActivity {