diff options
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 @@ | |||
1 | import * as express from 'express' | ||
2 | import { getFormattedObjects } from '../../helpers/utils' | ||
3 | import { asyncMiddleware, paginationValidator, setAccountsSort, setPagination } from '../../middlewares' | ||
4 | import { accountsGetValidator, accountsSortValidator } from '../../middlewares/validators' | ||
5 | import { AccountModel } from '../../models/account/account' | ||
6 | |||
7 | const accountsRouter = express.Router() | ||
8 | |||
9 | accountsRouter.get('/', | ||
10 | paginationValidator, | ||
11 | accountsSortValidator, | ||
12 | setAccountsSort, | ||
13 | setPagination, | ||
14 | asyncMiddleware(listAccounts) | ||
15 | ) | ||
16 | |||
17 | accountsRouter.get('/:id', | ||
18 | asyncMiddleware(accountsGetValidator), | ||
19 | getAccount | ||
20 | ) | ||
21 | |||
22 | // --------------------------------------------------------------------------- | ||
23 | |||
24 | export { | ||
25 | accountsRouter | ||
26 | } | ||
27 | |||
28 | // --------------------------------------------------------------------------- | ||
29 | |||
30 | function getAccount (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
31 | return res.json(res.locals.account.toFormattedJSON()) | ||
32 | } | ||
33 | |||
34 | async 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' | |||
5 | import { oauthClientsRouter } from './oauth-clients' | 5 | import { oauthClientsRouter } from './oauth-clients' |
6 | import { serverRouter } from './server' | 6 | import { serverRouter } from './server' |
7 | import { usersRouter } from './users' | 7 | import { usersRouter } from './users' |
8 | import { accountsRouter } from './accounts' | ||
8 | import { videosRouter } from './videos' | 9 | import { videosRouter } from './videos' |
9 | 10 | ||
10 | const apiRouter = express.Router() | 11 | const apiRouter = express.Router() |
@@ -13,6 +14,7 @@ apiRouter.use('/server', serverRouter) | |||
13 | apiRouter.use('/oauth-clients', oauthClientsRouter) | 14 | apiRouter.use('/oauth-clients', oauthClientsRouter) |
14 | apiRouter.use('/config', configRouter) | 15 | apiRouter.use('/config', configRouter) |
15 | apiRouter.use('/users', usersRouter) | 16 | apiRouter.use('/users', usersRouter) |
17 | apiRouter.use('/accounts', accountsRouter) | ||
16 | apiRouter.use('/videos', videosRouter) | 18 | apiRouter.use('/videos', videosRouter) |
17 | apiRouter.use('/jobs', jobsRouter) | 19 | apiRouter.use('/jobs', jobsRouter) |
18 | apiRouter.use('/ping', pong) | 20 | 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' | |||
8 | import { logger } from '../../helpers/logger' | 8 | import { logger } from '../../helpers/logger' |
9 | import { createReqFiles, getFormattedObjects } from '../../helpers/utils' | 9 | import { createReqFiles, getFormattedObjects } from '../../helpers/utils' |
10 | import { AVATAR_MIMETYPE_EXT, AVATARS_SIZE, CONFIG, sequelizeTypescript } from '../../initializers' | 10 | import { AVATAR_MIMETYPE_EXT, AVATARS_SIZE, CONFIG, sequelizeTypescript } from '../../initializers' |
11 | import { sendUpdateUser } from '../../lib/activitypub/send' | ||
11 | import { createUserAccountAndChannel } from '../../lib/user' | 12 | import { createUserAccountAndChannel } from '../../lib/user' |
12 | import { | 13 | import { |
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 | |||
217 | async function updateMe (req: express.Request, res: express.Response, next: express.NextFunction) { | 218 | async 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 | ||
233 | async function updateMyAvatar (req: express.Request, res: express.Response, next: express.NextFunction) { | 234 | async 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 @@ | |||
1 | import * as validator from 'validator' | 1 | import * as validator from 'validator' |
2 | import { Activity, ActivityType } from '../../../../shared/models/activitypub' | 2 | import { Activity, ActivityType } from '../../../../shared/models/activitypub' |
3 | import { isActorAcceptActivityValid, isActorDeleteActivityValid, isActorFollowActivityValid } from './actor' | 3 | import { isActorAcceptActivityValid, isActorDeleteActivityValid, isActorFollowActivityValid, isActorUpdateActivityValid } from './actor' |
4 | import { isAnnounceActivityValid } from './announce' | 4 | import { isAnnounceActivityValid } from './announce' |
5 | import { isActivityPubUrlValid } from './misc' | 5 | import { isActivityPubUrlValid } from './misc' |
6 | import { isDislikeActivityValid, isLikeActivityValid } from './rate' | 6 | import { isDislikeActivityValid, isLikeActivityValid } from './rate' |
@@ -64,7 +64,8 @@ function checkCreateActivity (activity: any) { | |||
64 | } | 64 | } |
65 | 65 | ||
66 | function checkUpdateActivity (activity: any) { | 66 | function checkUpdateActivity (activity: any) { |
67 | return isVideoTorrentUpdateActivityValid(activity) | 67 | return isVideoTorrentUpdateActivityValid(activity) || |
68 | isActorUpdateActivityValid(activity) | ||
68 | } | 69 | } |
69 | 70 | ||
70 | function checkDeleteActivity (activity: any) { | 71 | 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) { | |||
45 | validator.isLength(privateKey, CONSTRAINTS_FIELDS.ACTORS.PRIVATE_KEY) | 45 | validator.isLength(privateKey, CONSTRAINTS_FIELDS.ACTORS.PRIVATE_KEY) |
46 | } | 46 | } |
47 | 47 | ||
48 | function isRemoteActorValid (remoteActor: any) { | 48 | function 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 | ||
66 | function isActorFollowingCountValid (value: string) { | 66 | function 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 | ||
87 | function isActorUpdateActivityValid (activity: any) { | ||
88 | return isBaseActivityValid(activity, 'Update') && | ||
89 | isActorObjectValid(activity.object) | ||
90 | } | ||
91 | |||
87 | // --------------------------------------------------------------------------- | 92 | // --------------------------------------------------------------------------- |
88 | 93 | ||
89 | export { | 94 | export { |
@@ -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 |
23 | const SORTABLE_COLUMNS = { | 23 | const 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' | |||
5 | import * as uuidv4 from 'uuid/v4' | 5 | import * as uuidv4 from 'uuid/v4' |
6 | import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub' | 6 | import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub' |
7 | import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' | 7 | import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' |
8 | import { isRemoteActorValid } from '../../helpers/custom-validators/activitypub/actor' | 8 | import { isActorObjectValid } from '../../helpers/custom-validators/activitypub/actor' |
9 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 9 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
10 | import { retryTransactionWrapper } from '../../helpers/database-utils' | 10 | import { retryTransactionWrapper } from '../../helpers/database-utils' |
11 | import { logger } from '../../helpers/logger' | 11 | import { logger } from '../../helpers/logger' |
12 | import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto' | 12 | import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto' |
13 | import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests' | 13 | import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests' |
14 | import { CONFIG, sequelizeTypescript } from '../../initializers' | 14 | import { AVATAR_MIMETYPE_EXT, CONFIG, sequelizeTypescript } from '../../initializers' |
15 | import { AccountModel } from '../../models/account/account' | 15 | import { AccountModel } from '../../models/account/account' |
16 | import { ActorModel } from '../../models/activitypub/actor' | 16 | import { ActorModel } from '../../models/activitypub/actor' |
17 | import { AvatarModel } from '../../models/avatar/avatar' | 17 | import { AvatarModel } from '../../models/avatar/avatar' |
@@ -84,10 +84,52 @@ function buildActorInstance (type: ActivityPubActorType, url: string, preferredU | |||
84 | }) | 84 | }) |
85 | } | 85 | } |
86 | 86 | ||
87 | async 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 | |||
106 | async 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 | |||
87 | export { | 127 | export { |
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 | ||
220 | async 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 | |||
239 | function saveAccount (actor: ActorModel, result: FetchRemoteActorResult, t: Transaction) { | 247 | function 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 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import { ActivityUpdate } from '../../../../shared/models/activitypub' | 2 | import { ActivityUpdate } from '../../../../shared/models/activitypub' |
3 | import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor' | ||
4 | import { VideoTorrentObject } from '../../../../shared/models/activitypub/objects' | ||
3 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 5 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
4 | import { logger } from '../../../helpers/logger' | 6 | import { logger } from '../../../helpers/logger' |
5 | import { resetSequelizeInstance } from '../../../helpers/utils' | 7 | import { resetSequelizeInstance } from '../../../helpers/utils' |
6 | import { sequelizeTypescript } from '../../../initializers' | 8 | import { sequelizeTypescript } from '../../../initializers' |
9 | import { AccountModel } from '../../../models/account/account' | ||
7 | import { ActorModel } from '../../../models/activitypub/actor' | 10 | import { ActorModel } from '../../../models/activitypub/actor' |
11 | import { AvatarModel } from '../../../models/avatar/avatar' | ||
8 | import { TagModel } from '../../../models/video/tag' | 12 | import { TagModel } from '../../../models/video/tag' |
9 | import { VideoModel } from '../../../models/video/video' | 13 | import { VideoModel } from '../../../models/video/video' |
10 | import { VideoFileModel } from '../../../models/video/video-file' | 14 | import { VideoFileModel } from '../../../models/video/video-file' |
11 | import { getOrCreateActorAndServerAndModel } from '../actor' | 15 | import { fetchActorTotalItems, fetchAvatarIfExists, getOrCreateActorAndServerAndModel } from '../actor' |
12 | import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc' | 16 | import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc' |
13 | 17 | ||
14 | async function processUpdateActivity (activity: ActivityUpdate) { | 18 | async 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 | ||
41 | async function updateRemoteVideo (actor: ActorModel, activity: ActivityUpdate) { | 47 | async 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 | |||
114 | function 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 | |||
123 | async 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 @@ | |||
1 | import { Transaction } from 'sequelize' | 1 | import { Transaction } from 'sequelize' |
2 | import { ActivityAudience, ActivityUpdate } from '../../../../shared/models/activitypub' | 2 | import { ActivityAudience, ActivityUpdate } from '../../../../shared/models/activitypub' |
3 | import { VideoPrivacy } from '../../../../shared/models/videos' | 3 | import { VideoPrivacy } from '../../../../shared/models/videos' |
4 | import { UserModel } from '../../../models/account/user' | ||
4 | import { ActorModel } from '../../../models/activitypub/actor' | 5 | import { ActorModel } from '../../../models/activitypub/actor' |
5 | import { VideoModel } from '../../../models/video/video' | 6 | import { VideoModel } from '../../../models/video/video' |
6 | import { VideoShareModel } from '../../../models/video/video-share' | 7 | import { 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 | ||
26 | async 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 | ||
27 | export { | 42 | export { |
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' | |||
2 | import 'express-validator' | 2 | import 'express-validator' |
3 | import { SortType } from '../helpers/utils' | 3 | import { SortType } from '../helpers/utils' |
4 | 4 | ||
5 | function 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 | |||
5 | function setUsersSort (req: express.Request, res: express.Response, next: express.NextFunction) { | 11 | function 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 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { param } from 'express-validator/check' | 2 | import { param } from 'express-validator/check' |
3 | import { isAccountNameValid, isLocalAccountNameExist } from '../../helpers/custom-validators/accounts' | 3 | import { isAccountIdExist, isAccountNameValid, isLocalAccountNameExist } from '../../helpers/custom-validators/accounts' |
4 | import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc' | ||
4 | import { logger } from '../../helpers/logger' | 5 | import { logger } from '../../helpers/logger' |
5 | import { areValidationErrors } from './utils' | 6 | import { areValidationErrors } from './utils' |
6 | 7 | ||
@@ -17,8 +18,22 @@ const localAccountValidator = [ | |||
17 | } | 18 | } |
18 | ] | 19 | ] |
19 | 20 | ||
21 | const 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 | ||
22 | export { | 36 | export { |
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 |
8 | const SORTABLE_USERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USERS) | 8 | const SORTABLE_USERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USERS) |
9 | const SORTABLE_ACCOUNTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.ACCOUNTS) | ||
9 | const SORTABLE_JOBS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.JOBS) | 10 | const SORTABLE_JOBS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.JOBS) |
10 | const SORTABLE_VIDEO_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_ABUSES) | 11 | const SORTABLE_VIDEO_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_ABUSES) |
11 | const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS) | 12 | const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS) |
@@ -16,6 +17,7 @@ const SORTABLE_FOLLOWERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOW | |||
16 | const SORTABLE_FOLLOWING_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWING) | 17 | const SORTABLE_FOLLOWING_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWING) |
17 | 18 | ||
18 | const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) | 19 | const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) |
20 | const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS) | ||
19 | const jobsSortValidator = checkSort(SORTABLE_JOBS_COLUMNS) | 21 | const jobsSortValidator = checkSort(SORTABLE_JOBS_COLUMNS) |
20 | const videoAbusesSortValidator = checkSort(SORTABLE_VIDEO_ABUSES_COLUMNS) | 22 | const videoAbusesSortValidator = checkSort(SORTABLE_VIDEO_ABUSES_COLUMNS) |
21 | const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS) | 23 | const 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' | |||
18 | import { sendDeleteActor } from '../../lib/activitypub/send' | 18 | import { sendDeleteActor } from '../../lib/activitypub/send' |
19 | import { ActorModel } from '../activitypub/actor' | 19 | import { ActorModel } from '../activitypub/actor' |
20 | import { ApplicationModel } from '../application/application' | 20 | import { ApplicationModel } from '../application/application' |
21 | import { AvatarModel } from '../avatar/avatar' | ||
21 | import { ServerModel } from '../server/server' | 22 | import { ServerModel } from '../server/server' |
22 | import { throwIfNotValid } from '../utils' | 23 | import { getSort, throwIfNotValid } from '../utils' |
23 | import { VideoChannelModel } from '../video/video-channel' | 24 | import { VideoChannelModel } from '../video/video-channel' |
24 | import { UserModel } from './user' | 25 | import { 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' | 2 | import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' |
3 | import { AccountModel } from '../account/account' | ||
3 | import { ActorModel } from '../activitypub/actor' | 4 | import { ActorModel } from '../activitypub/actor' |
4 | import { VideoModel } from './video' | 5 | import { VideoModel } from './video' |
6 | import { VideoChannelModel } from './video-channel' | ||
5 | 7 | ||
6 | enum ScopeNames { | 8 | enum 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 | |||
3 | import 'mocha' | ||
4 | |||
5 | import { flushTests, killallServers, runServer, ServerInfo } from '../../utils' | ||
6 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' | ||
7 | import { getAccount } from '../../utils/users/accounts' | ||
8 | |||
9 | describe('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 |
2 | import './accounts' | ||
2 | import './follows' | 3 | import './follows' |
3 | import './jobs' | 4 | import './jobs' |
4 | import './services' | 5 | 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 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' | |||
5 | import './server/follows' | 5 | import './server/follows' |
6 | import './server/jobs' | 6 | import './server/jobs' |
7 | import './videos/video-comments' | 7 | import './videos/video-comments' |
8 | import './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 | |||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | ||
5 | import { Account } from '../../../../shared/models/actors' | ||
6 | import { doubleFollow, flushAndRunMultipleServers, wait } from '../../utils' | ||
7 | import { | ||
8 | flushTests, getMyUserInformation, killallServers, ServerInfo, testVideoImage, updateMyAvatar, | ||
9 | uploadVideo | ||
10 | } from '../../utils/index' | ||
11 | import { getAccount, getAccountsList } from '../../utils/users/accounts' | ||
12 | import { setAccessTokensToServers } from '../../utils/users/login' | ||
13 | |||
14 | const expect = chai.expect | ||
15 | |||
16 | describe('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 @@ | |||
1 | import { makeGetRequest } from '../requests/requests' | ||
2 | |||
3 | function 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 | |||
14 | function 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 | |||
26 | export { | ||
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 @@ | |||
1 | import { ActivityPubActor } from './activitypub-actor' | ||
1 | import { ActivityPubSignature } from './activitypub-signature' | 2 | import { ActivityPubSignature } from './activitypub-signature' |
2 | import { VideoTorrentObject } from './objects' | 3 | import { VideoTorrentObject } from './objects' |
3 | import { DislikeObject } from './objects/dislike-object' | 4 | import { DislikeObject } from './objects/dislike-object' |
@@ -33,7 +34,7 @@ export interface ActivityCreate extends BaseActivity { | |||
33 | 34 | ||
34 | export interface ActivityUpdate extends BaseActivity { | 35 | export interface ActivityUpdate extends BaseActivity { |
35 | type: 'Update' | 36 | type: 'Update' |
36 | object: VideoTorrentObject | 37 | object: VideoTorrentObject | ActivityPubActor |
37 | } | 38 | } |
38 | 39 | ||
39 | export interface ActivityDelete extends BaseActivity { | 40 | export interface ActivityDelete extends BaseActivity { |