diff options
author | Chocobozzz <me@florianbigard.com> | 2017-12-29 19:10:13 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2017-12-29 19:10:13 +0100 |
commit | c5911fd347c76e8bdc05ea9f3ee9efed4a58c236 (patch) | |
tree | b8d287daca6c45305090cbec9da97d1155f275bd /server | |
parent | 8b0d42ee372de6589796be26b83e5bffb1b69cdf (diff) | |
download | PeerTube-c5911fd347c76e8bdc05ea9f3ee9efed4a58c236.tar.gz PeerTube-c5911fd347c76e8bdc05ea9f3ee9efed4a58c236.tar.zst PeerTube-c5911fd347c76e8bdc05ea9f3ee9efed4a58c236.zip |
Begin to add avatar to actors
Diffstat (limited to 'server')
-rw-r--r-- | server/controllers/activitypub/client.ts | 6 | ||||
-rw-r--r-- | server/controllers/api/users.ts | 53 | ||||
-rw-r--r-- | server/controllers/api/videos/index.ts | 27 | ||||
-rw-r--r-- | server/controllers/static.ts | 6 | ||||
-rw-r--r-- | server/helpers/custom-validators/users.ts | 21 | ||||
-rw-r--r-- | server/helpers/utils.ts | 30 | ||||
-rw-r--r-- | server/initializers/constants.ts | 19 | ||||
-rw-r--r-- | server/initializers/migrations/0150-avatar-cascade.ts | 28 | ||||
-rw-r--r-- | server/lib/activitypub/actor.ts | 87 | ||||
-rw-r--r-- | server/lib/activitypub/url.ts | 2 | ||||
-rw-r--r-- | server/middlewares/validators/users.ts | 22 | ||||
-rw-r--r-- | server/models/account/account.ts | 6 | ||||
-rw-r--r-- | server/models/account/user.ts | 8 | ||||
-rw-r--r-- | server/models/activitypub/actor.ts | 37 | ||||
-rw-r--r-- | server/models/avatar/avatar.ts | 30 | ||||
-rw-r--r-- | server/models/video/video-comment.ts | 2 | ||||
-rw-r--r-- | server/tests/api/check-params/users.ts | 22 | ||||
-rw-r--r-- | server/tests/api/fixtures/avatar.png | bin | 0 -> 1674 bytes | |||
-rw-r--r-- | server/tests/api/users/users.ts | 18 | ||||
-rw-r--r-- | server/tests/utils/users/users.ts | 29 | ||||
-rw-r--r-- | server/tests/utils/videos/videos.ts | 4 |
21 files changed, 366 insertions, 91 deletions
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts index 71e706346..e0ab3188b 100644 --- a/server/controllers/activitypub/client.ts +++ b/server/controllers/activitypub/client.ts | |||
@@ -16,17 +16,17 @@ import { VideoShareModel } from '../../models/video/video-share' | |||
16 | 16 | ||
17 | const activityPubClientRouter = express.Router() | 17 | const activityPubClientRouter = express.Router() |
18 | 18 | ||
19 | activityPubClientRouter.get('/account/:name', | 19 | activityPubClientRouter.get('/accounts/:name', |
20 | executeIfActivityPub(asyncMiddleware(localAccountValidator)), | 20 | executeIfActivityPub(asyncMiddleware(localAccountValidator)), |
21 | executeIfActivityPub(accountController) | 21 | executeIfActivityPub(accountController) |
22 | ) | 22 | ) |
23 | 23 | ||
24 | activityPubClientRouter.get('/account/:name/followers', | 24 | activityPubClientRouter.get('/accounts/:name/followers', |
25 | executeIfActivityPub(asyncMiddleware(localAccountValidator)), | 25 | executeIfActivityPub(asyncMiddleware(localAccountValidator)), |
26 | executeIfActivityPub(asyncMiddleware(accountFollowersController)) | 26 | executeIfActivityPub(asyncMiddleware(accountFollowersController)) |
27 | ) | 27 | ) |
28 | 28 | ||
29 | activityPubClientRouter.get('/account/:name/following', | 29 | activityPubClientRouter.get('/accounts/:name/following', |
30 | executeIfActivityPub(asyncMiddleware(localAccountValidator)), | 30 | executeIfActivityPub(asyncMiddleware(localAccountValidator)), |
31 | executeIfActivityPub(asyncMiddleware(accountFollowingController)) | 31 | executeIfActivityPub(asyncMiddleware(accountFollowingController)) |
32 | ) | 32 | ) |
diff --git a/server/controllers/api/users.ts b/server/controllers/api/users.ts index 75393ad17..57b98b84a 100644 --- a/server/controllers/api/users.ts +++ b/server/controllers/api/users.ts | |||
@@ -1,20 +1,26 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { extname, join } from 'path' | ||
3 | import * as uuidv4 from 'uuid/v4' | ||
2 | import { UserCreate, UserRight, UserRole, UserUpdate, UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../shared' | 4 | import { UserCreate, UserRight, UserRole, UserUpdate, UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../shared' |
5 | import { renamePromise } from '../../helpers/core-utils' | ||
3 | import { retryTransactionWrapper } from '../../helpers/database-utils' | 6 | import { retryTransactionWrapper } from '../../helpers/database-utils' |
4 | import { logger } from '../../helpers/logger' | 7 | import { logger } from '../../helpers/logger' |
5 | import { getFormattedObjects } from '../../helpers/utils' | 8 | import { createReqFiles, getFormattedObjects } from '../../helpers/utils' |
6 | import { CONFIG } from '../../initializers' | 9 | import { AVATAR_MIMETYPE_EXT, CONFIG, sequelizeTypescript } from '../../initializers' |
7 | import { createUserAccountAndChannel } from '../../lib/user' | 10 | import { createUserAccountAndChannel } from '../../lib/user' |
8 | import { | 11 | import { |
9 | asyncMiddleware, authenticate, ensureUserHasRight, ensureUserRegistrationAllowed, paginationValidator, setPagination, setUsersSort, | 12 | asyncMiddleware, authenticate, ensureUserHasRight, ensureUserRegistrationAllowed, paginationValidator, setPagination, setUsersSort, |
10 | setVideosSort, token, usersAddValidator, usersGetValidator, usersRegisterValidator, usersRemoveValidator, usersSortValidator, | 13 | setVideosSort, token, usersAddValidator, usersGetValidator, usersRegisterValidator, usersRemoveValidator, usersSortValidator, |
11 | usersUpdateMeValidator, usersUpdateValidator, usersVideoRatingValidator | 14 | usersUpdateMeValidator, usersUpdateValidator, usersVideoRatingValidator |
12 | } from '../../middlewares' | 15 | } from '../../middlewares' |
13 | import { videosSortValidator } from '../../middlewares/validators' | 16 | import { usersUpdateMyAvatarValidator, videosSortValidator } from '../../middlewares/validators' |
14 | import { AccountVideoRateModel } from '../../models/account/account-video-rate' | 17 | import { AccountVideoRateModel } from '../../models/account/account-video-rate' |
15 | import { UserModel } from '../../models/account/user' | 18 | import { UserModel } from '../../models/account/user' |
19 | import { AvatarModel } from '../../models/avatar/avatar' | ||
16 | import { VideoModel } from '../../models/video/video' | 20 | import { VideoModel } from '../../models/video/video' |
17 | 21 | ||
22 | const reqAvatarFile = createReqFiles('avatarfile', CONFIG.STORAGE.AVATARS_DIR, AVATAR_MIMETYPE_EXT) | ||
23 | |||
18 | const usersRouter = express.Router() | 24 | const usersRouter = express.Router() |
19 | 25 | ||
20 | usersRouter.get('/me', | 26 | usersRouter.get('/me', |
@@ -71,6 +77,13 @@ usersRouter.put('/me', | |||
71 | asyncMiddleware(updateMe) | 77 | asyncMiddleware(updateMe) |
72 | ) | 78 | ) |
73 | 79 | ||
80 | usersRouter.post('/me/avatar/pick', | ||
81 | authenticate, | ||
82 | reqAvatarFile, | ||
83 | usersUpdateMyAvatarValidator, | ||
84 | asyncMiddleware(updateMyAvatar) | ||
85 | ) | ||
86 | |||
74 | usersRouter.put('/:id', | 87 | usersRouter.put('/:id', |
75 | authenticate, | 88 | authenticate, |
76 | ensureUserHasRight(UserRight.MANAGE_USERS), | 89 | ensureUserHasRight(UserRight.MANAGE_USERS), |
@@ -216,6 +229,40 @@ async function updateMe (req: express.Request, res: express.Response, next: expr | |||
216 | return res.sendStatus(204) | 229 | return res.sendStatus(204) |
217 | } | 230 | } |
218 | 231 | ||
232 | async function updateMyAvatar (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
233 | const avatarPhysicalFile = req.files['avatarfile'][0] | ||
234 | const actor = res.locals.oauth.token.user.Account.Actor | ||
235 | |||
236 | const avatarDir = CONFIG.STORAGE.AVATARS_DIR | ||
237 | const source = join(avatarDir, avatarPhysicalFile.filename) | ||
238 | const extension = extname(avatarPhysicalFile.filename) | ||
239 | const avatarName = uuidv4() + extension | ||
240 | const destination = join(avatarDir, avatarName) | ||
241 | |||
242 | await renamePromise(source, destination) | ||
243 | |||
244 | const { avatar } = await sequelizeTypescript.transaction(async t => { | ||
245 | const avatar = await AvatarModel.create({ | ||
246 | filename: avatarName | ||
247 | }, { transaction: t }) | ||
248 | |||
249 | if (actor.Avatar) { | ||
250 | await actor.Avatar.destroy({ transaction: t }) | ||
251 | } | ||
252 | |||
253 | actor.set('avatarId', avatar.id) | ||
254 | await actor.save({ transaction: t }) | ||
255 | |||
256 | return { actor, avatar } | ||
257 | }) | ||
258 | |||
259 | return res | ||
260 | .json({ | ||
261 | avatar: avatar.toFormattedJSON() | ||
262 | }) | ||
263 | .end() | ||
264 | } | ||
265 | |||
219 | async function updateUser (req: express.Request, res: express.Response, next: express.NextFunction) { | 266 | async function updateUser (req: express.Request, res: express.Response, next: express.NextFunction) { |
220 | const body: UserUpdate = req.body | 267 | const body: UserUpdate = req.body |
221 | const user = res.locals.user as UserModel | 268 | const user = res.locals.user as UserModel |
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index 11e3da5cc..ff0d967e1 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts | |||
@@ -6,7 +6,7 @@ import { renamePromise } from '../../../helpers/core-utils' | |||
6 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 6 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
7 | import { getVideoFileHeight } from '../../../helpers/ffmpeg-utils' | 7 | import { getVideoFileHeight } from '../../../helpers/ffmpeg-utils' |
8 | import { logger } from '../../../helpers/logger' | 8 | import { logger } from '../../../helpers/logger' |
9 | import { generateRandomString, getFormattedObjects, getServerActor, resetSequelizeInstance } from '../../../helpers/utils' | 9 | import { createReqFiles, generateRandomString, getFormattedObjects, getServerActor, resetSequelizeInstance } from '../../../helpers/utils' |
10 | import { | 10 | import { |
11 | CONFIG, sequelizeTypescript, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_MIMETYPE_EXT, | 11 | CONFIG, sequelizeTypescript, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_MIMETYPE_EXT, |
12 | VIDEO_PRIVACIES | 12 | VIDEO_PRIVACIES |
@@ -29,28 +29,7 @@ import { rateVideoRouter } from './rate' | |||
29 | 29 | ||
30 | const videosRouter = express.Router() | 30 | const videosRouter = express.Router() |
31 | 31 | ||
32 | // multer configuration | 32 | const reqVideoFile = createReqFiles('videofile', CONFIG.STORAGE.VIDEOS_DIR, VIDEO_MIMETYPE_EXT) |
33 | const storage = multer.diskStorage({ | ||
34 | destination: (req, file, cb) => { | ||
35 | cb(null, CONFIG.STORAGE.VIDEOS_DIR) | ||
36 | }, | ||
37 | |||
38 | filename: async (req, file, cb) => { | ||
39 | const extension = VIDEO_MIMETYPE_EXT[file.mimetype] | ||
40 | let randomString = '' | ||
41 | |||
42 | try { | ||
43 | randomString = await generateRandomString(16) | ||
44 | } catch (err) { | ||
45 | logger.error('Cannot generate random string for file name.', err) | ||
46 | randomString = 'fake-random-string' | ||
47 | } | ||
48 | |||
49 | cb(null, randomString + extension) | ||
50 | } | ||
51 | }) | ||
52 | |||
53 | const reqFiles = multer({ storage: storage }).fields([{ name: 'videofile', maxCount: 1 }]) | ||
54 | 33 | ||
55 | videosRouter.use('/', abuseVideoRouter) | 34 | videosRouter.use('/', abuseVideoRouter) |
56 | videosRouter.use('/', blacklistRouter) | 35 | videosRouter.use('/', blacklistRouter) |
@@ -85,7 +64,7 @@ videosRouter.put('/:id', | |||
85 | ) | 64 | ) |
86 | videosRouter.post('/upload', | 65 | videosRouter.post('/upload', |
87 | authenticate, | 66 | authenticate, |
88 | reqFiles, | 67 | reqVideoFile, |
89 | asyncMiddleware(videosAddValidator), | 68 | asyncMiddleware(videosAddValidator), |
90 | asyncMiddleware(addVideoRetryWrapper) | 69 | asyncMiddleware(addVideoRetryWrapper) |
91 | ) | 70 | ) |
diff --git a/server/controllers/static.ts b/server/controllers/static.ts index ccae60517..eece9c06b 100644 --- a/server/controllers/static.ts +++ b/server/controllers/static.ts | |||
@@ -32,6 +32,12 @@ staticRouter.use( | |||
32 | express.static(thumbnailsPhysicalPath, { maxAge: STATIC_MAX_AGE }) | 32 | express.static(thumbnailsPhysicalPath, { maxAge: STATIC_MAX_AGE }) |
33 | ) | 33 | ) |
34 | 34 | ||
35 | const avatarsPhysicalPath = CONFIG.STORAGE.AVATARS_DIR | ||
36 | staticRouter.use( | ||
37 | STATIC_PATHS.AVATARS, | ||
38 | express.static(avatarsPhysicalPath, { maxAge: STATIC_MAX_AGE }) | ||
39 | ) | ||
40 | |||
35 | // Video previews path for express | 41 | // Video previews path for express |
36 | staticRouter.use( | 42 | staticRouter.use( |
37 | STATIC_PATHS.PREVIEWS + ':uuid.jpg', | 43 | STATIC_PATHS.PREVIEWS + ':uuid.jpg', |
diff --git a/server/helpers/custom-validators/users.ts b/server/helpers/custom-validators/users.ts index 159c2a700..6ed60c1c4 100644 --- a/server/helpers/custom-validators/users.ts +++ b/server/helpers/custom-validators/users.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import * as validator from 'validator' | 1 | import * as validator from 'validator' |
2 | import 'express-validator' | 2 | import 'express-validator' |
3 | 3 | ||
4 | import { exists } from './misc' | 4 | import { exists, isArray } from './misc' |
5 | import { CONSTRAINTS_FIELDS } from '../../initializers' | 5 | import { CONSTRAINTS_FIELDS } from '../../initializers' |
6 | import { UserRole } from '../../../shared' | 6 | import { UserRole } from '../../../shared' |
7 | 7 | ||
@@ -37,6 +37,22 @@ function isUserRoleValid (value: any) { | |||
37 | return exists(value) && validator.isInt('' + value) && UserRole[value] !== undefined | 37 | return exists(value) && validator.isInt('' + value) && UserRole[value] !== undefined |
38 | } | 38 | } |
39 | 39 | ||
40 | function isAvatarFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) { | ||
41 | // Should have files | ||
42 | if (!files) return false | ||
43 | if (isArray(files)) return false | ||
44 | |||
45 | // Should have videofile file | ||
46 | const avatarfile = files['avatarfile'] | ||
47 | if (!avatarfile || avatarfile.length === 0) return false | ||
48 | |||
49 | // The file should exist | ||
50 | const file = avatarfile[0] | ||
51 | if (!file || !file.originalname) return false | ||
52 | |||
53 | return new RegExp('^image/(png|jpeg)$', 'i').test(file.mimetype) | ||
54 | } | ||
55 | |||
40 | // --------------------------------------------------------------------------- | 56 | // --------------------------------------------------------------------------- |
41 | 57 | ||
42 | export { | 58 | export { |
@@ -45,5 +61,6 @@ export { | |||
45 | isUserVideoQuotaValid, | 61 | isUserVideoQuotaValid, |
46 | isUserUsernameValid, | 62 | isUserUsernameValid, |
47 | isUserDisplayNSFWValid, | 63 | isUserDisplayNSFWValid, |
48 | isUserAutoPlayVideoValid | 64 | isUserAutoPlayVideoValid, |
65 | isAvatarFile | ||
49 | } | 66 | } |
diff --git a/server/helpers/utils.ts b/server/helpers/utils.ts index 769aa83c6..7a32e286c 100644 --- a/server/helpers/utils.ts +++ b/server/helpers/utils.ts | |||
@@ -1,8 +1,9 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import * as multer from 'multer' | ||
2 | import { Model } from 'sequelize-typescript' | 3 | import { Model } from 'sequelize-typescript' |
3 | import { ResultList } from '../../shared' | 4 | import { ResultList } from '../../shared' |
4 | import { VideoResolution } from '../../shared/models/videos' | 5 | import { VideoResolution } from '../../shared/models/videos' |
5 | import { CONFIG, REMOTE_SCHEME } from '../initializers' | 6 | import { CONFIG, REMOTE_SCHEME, VIDEO_MIMETYPE_EXT } from '../initializers' |
6 | import { UserModel } from '../models/account/user' | 7 | import { UserModel } from '../models/account/user' |
7 | import { ActorModel } from '../models/activitypub/actor' | 8 | import { ActorModel } from '../models/activitypub/actor' |
8 | import { ApplicationModel } from '../models/application/application' | 9 | import { ApplicationModel } from '../models/application/application' |
@@ -26,6 +27,30 @@ function badRequest (req: express.Request, res: express.Response, next: express. | |||
26 | return res.type('json').status(400).end() | 27 | return res.type('json').status(400).end() |
27 | } | 28 | } |
28 | 29 | ||
30 | function createReqFiles (fieldName: string, storageDir: string, mimeTypes: { [ id: string ]: string }) { | ||
31 | const storage = multer.diskStorage({ | ||
32 | destination: (req, file, cb) => { | ||
33 | cb(null, storageDir) | ||
34 | }, | ||
35 | |||
36 | filename: async (req, file, cb) => { | ||
37 | const extension = mimeTypes[file.mimetype] | ||
38 | let randomString = '' | ||
39 | |||
40 | try { | ||
41 | randomString = await generateRandomString(16) | ||
42 | } catch (err) { | ||
43 | logger.error('Cannot generate random string for file name.', err) | ||
44 | randomString = 'fake-random-string' | ||
45 | } | ||
46 | |||
47 | cb(null, randomString + extension) | ||
48 | } | ||
49 | }) | ||
50 | |||
51 | return multer({ storage }).fields([{ name: fieldName, maxCount: 1 }]) | ||
52 | } | ||
53 | |||
29 | async function generateRandomString (size: number) { | 54 | async function generateRandomString (size: number) { |
30 | const raw = await pseudoRandomBytesPromise(size) | 55 | const raw = await pseudoRandomBytesPromise(size) |
31 | 56 | ||
@@ -122,5 +147,6 @@ export { | |||
122 | resetSequelizeInstance, | 147 | resetSequelizeInstance, |
123 | getServerActor, | 148 | getServerActor, |
124 | SortType, | 149 | SortType, |
125 | getHostWithPort | 150 | getHostWithPort, |
151 | createReqFiles | ||
126 | } | 152 | } |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 3a5a557d4..50a29dc43 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -9,7 +9,7 @@ import { isTestInstance, root, sanitizeHost, sanitizeUrl } from '../helpers/core | |||
9 | 9 | ||
10 | // --------------------------------------------------------------------------- | 10 | // --------------------------------------------------------------------------- |
11 | 11 | ||
12 | const LAST_MIGRATION_VERSION = 145 | 12 | const LAST_MIGRATION_VERSION = 150 |
13 | 13 | ||
14 | // --------------------------------------------------------------------------- | 14 | // --------------------------------------------------------------------------- |
15 | 15 | ||
@@ -172,7 +172,10 @@ const CONSTRAINTS_FIELDS = { | |||
172 | ACTOR: { | 172 | ACTOR: { |
173 | PUBLIC_KEY: { min: 10, max: 5000 }, // Length | 173 | PUBLIC_KEY: { min: 10, max: 5000 }, // Length |
174 | PRIVATE_KEY: { min: 10, max: 5000 }, // Length | 174 | PRIVATE_KEY: { min: 10, max: 5000 }, // Length |
175 | URL: { min: 3, max: 2000 } // Length | 175 | URL: { min: 3, max: 2000 }, // Length |
176 | AVATAR: { | ||
177 | EXTNAME: [ '.png', '.jpeg', '.jpg' ] | ||
178 | } | ||
176 | }, | 179 | }, |
177 | VIDEO_EVENTS: { | 180 | VIDEO_EVENTS: { |
178 | COUNT: { min: 0 } | 181 | COUNT: { min: 0 } |
@@ -250,6 +253,12 @@ const VIDEO_MIMETYPE_EXT = { | |||
250 | 'video/mp4': '.mp4' | 253 | 'video/mp4': '.mp4' |
251 | } | 254 | } |
252 | 255 | ||
256 | const AVATAR_MIMETYPE_EXT = { | ||
257 | 'image/png': '.png', | ||
258 | 'image/jpg': '.jpg', | ||
259 | 'image/jpeg': '.jpg' | ||
260 | } | ||
261 | |||
253 | // --------------------------------------------------------------------------- | 262 | // --------------------------------------------------------------------------- |
254 | 263 | ||
255 | const SERVER_ACTOR_NAME = 'peertube' | 264 | const SERVER_ACTOR_NAME = 'peertube' |
@@ -291,7 +300,8 @@ const STATIC_PATHS = { | |||
291 | PREVIEWS: '/static/previews/', | 300 | PREVIEWS: '/static/previews/', |
292 | THUMBNAILS: '/static/thumbnails/', | 301 | THUMBNAILS: '/static/thumbnails/', |
293 | TORRENTS: '/static/torrents/', | 302 | TORRENTS: '/static/torrents/', |
294 | WEBSEED: '/static/webseed/' | 303 | WEBSEED: '/static/webseed/', |
304 | AVATARS: '/static/avatars/' | ||
295 | } | 305 | } |
296 | 306 | ||
297 | // Cache control | 307 | // Cache control |
@@ -376,5 +386,6 @@ export { | |||
376 | VIDEO_PRIVACIES, | 386 | VIDEO_PRIVACIES, |
377 | VIDEO_LICENCES, | 387 | VIDEO_LICENCES, |
378 | VIDEO_RATE_TYPES, | 388 | VIDEO_RATE_TYPES, |
379 | VIDEO_MIMETYPE_EXT | 389 | VIDEO_MIMETYPE_EXT, |
390 | AVATAR_MIMETYPE_EXT | ||
380 | } | 391 | } |
diff --git a/server/initializers/migrations/0150-avatar-cascade.ts b/server/initializers/migrations/0150-avatar-cascade.ts new file mode 100644 index 000000000..821696717 --- /dev/null +++ b/server/initializers/migrations/0150-avatar-cascade.ts | |||
@@ -0,0 +1,28 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction, | ||
5 | queryInterface: Sequelize.QueryInterface, | ||
6 | sequelize: Sequelize.Sequelize | ||
7 | }): Promise<void> { | ||
8 | await utils.queryInterface.removeConstraint('actor', 'actor_avatarId_fkey') | ||
9 | |||
10 | await utils.queryInterface.addConstraint('actor', [ 'avatarId' ], { | ||
11 | type: 'foreign key', | ||
12 | references: { | ||
13 | table: 'avatar', | ||
14 | field: 'id' | ||
15 | }, | ||
16 | onDelete: 'set null', | ||
17 | onUpdate: 'CASCADE' | ||
18 | }) | ||
19 | } | ||
20 | |||
21 | function down (options) { | ||
22 | throw new Error('Not implemented.') | ||
23 | } | ||
24 | |||
25 | export { | ||
26 | up, | ||
27 | down | ||
28 | } | ||
diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts index e590dc72d..e557896e8 100644 --- a/server/lib/activitypub/actor.ts +++ b/server/lib/activitypub/actor.ts | |||
@@ -1,16 +1,20 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import { join } from 'path' | ||
2 | import { Transaction } from 'sequelize' | 3 | import { Transaction } from 'sequelize' |
3 | import * as url from 'url' | 4 | import * as url from 'url' |
5 | import * as uuidv4 from 'uuid/v4' | ||
4 | import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub' | 6 | import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub' |
5 | import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' | 7 | import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' |
6 | import { isRemoteActorValid } from '../../helpers/custom-validators/activitypub/actor' | 8 | import { isRemoteActorValid } from '../../helpers/custom-validators/activitypub/actor' |
9 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | ||
7 | import { retryTransactionWrapper } from '../../helpers/database-utils' | 10 | import { retryTransactionWrapper } from '../../helpers/database-utils' |
8 | import { logger } from '../../helpers/logger' | 11 | import { logger } from '../../helpers/logger' |
9 | import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto' | 12 | import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto' |
10 | import { doRequest } from '../../helpers/requests' | 13 | import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests' |
11 | import { CONFIG, sequelizeTypescript } from '../../initializers' | 14 | import { CONFIG, sequelizeTypescript } from '../../initializers' |
12 | import { AccountModel } from '../../models/account/account' | 15 | import { AccountModel } from '../../models/account/account' |
13 | import { ActorModel } from '../../models/activitypub/actor' | 16 | import { ActorModel } from '../../models/activitypub/actor' |
17 | import { AvatarModel } from '../../models/avatar/avatar' | ||
14 | import { ServerModel } from '../../models/server/server' | 18 | import { ServerModel } from '../../models/server/server' |
15 | import { VideoChannelModel } from '../../models/video/video-channel' | 19 | import { VideoChannelModel } from '../../models/video/video-channel' |
16 | 20 | ||
@@ -62,6 +66,32 @@ async function getOrCreateActorAndServerAndModel (actorUrl: string, recurseIfNee | |||
62 | return actor | 66 | return actor |
63 | } | 67 | } |
64 | 68 | ||
69 | function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string, uuid?: string) { | ||
70 | return new ActorModel({ | ||
71 | type, | ||
72 | url, | ||
73 | preferredUsername, | ||
74 | uuid, | ||
75 | publicKey: null, | ||
76 | privateKey: null, | ||
77 | followersCount: 0, | ||
78 | followingCount: 0, | ||
79 | inboxUrl: url + '/inbox', | ||
80 | outboxUrl: url + '/outbox', | ||
81 | sharedInboxUrl: CONFIG.WEBSERVER.URL + '/inbox', | ||
82 | followersUrl: url + '/followers', | ||
83 | followingUrl: url + '/following' | ||
84 | }) | ||
85 | } | ||
86 | |||
87 | export { | ||
88 | getOrCreateActorAndServerAndModel, | ||
89 | buildActorInstance, | ||
90 | setAsyncActorKeys | ||
91 | } | ||
92 | |||
93 | // --------------------------------------------------------------------------- | ||
94 | |||
65 | function saveActorAndServerAndModelIfNotExist ( | 95 | function saveActorAndServerAndModelIfNotExist ( |
66 | result: FetchRemoteActorResult, | 96 | result: FetchRemoteActorResult, |
67 | ownerActor?: ActorModel, | 97 | ownerActor?: ActorModel, |
@@ -90,6 +120,14 @@ function saveActorAndServerAndModelIfNotExist ( | |||
90 | // Save our new account in database | 120 | // Save our new account in database |
91 | actor.set('serverId', server.id) | 121 | actor.set('serverId', server.id) |
92 | 122 | ||
123 | // Avatar? | ||
124 | if (result.avatarName) { | ||
125 | const avatar = await AvatarModel.create({ | ||
126 | filename: result.avatarName | ||
127 | }, { transaction: t }) | ||
128 | actor.set('avatarId', avatar.id) | ||
129 | } | ||
130 | |||
93 | // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists | 131 | // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists |
94 | // (which could be false in a retried query) | 132 | // (which could be false in a retried query) |
95 | const actorCreated = await ActorModel.create(actor.toJSON(), { transaction: t }) | 133 | const actorCreated = await ActorModel.create(actor.toJSON(), { transaction: t }) |
@@ -112,6 +150,7 @@ type FetchRemoteActorResult = { | |||
112 | actor: ActorModel | 150 | actor: ActorModel |
113 | name: string | 151 | name: string |
114 | summary: string | 152 | summary: string |
153 | avatarName?: string | ||
115 | attributedTo: ActivityPubAttributedTo[] | 154 | attributedTo: ActivityPubAttributedTo[] |
116 | } | 155 | } |
117 | async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResult> { | 156 | async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResult> { |
@@ -151,43 +190,33 @@ async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResu | |||
151 | followingUrl: actorJSON.following | 190 | followingUrl: actorJSON.following |
152 | }) | 191 | }) |
153 | 192 | ||
193 | // Fetch icon? | ||
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 | |||
154 | const name = actorJSON.name || actorJSON.preferredUsername | 210 | const name = actorJSON.name || actorJSON.preferredUsername |
155 | return { | 211 | return { |
156 | actor, | 212 | actor, |
157 | name, | 213 | name, |
214 | avatarName, | ||
158 | summary: actorJSON.summary, | 215 | summary: actorJSON.summary, |
159 | attributedTo: actorJSON.attributedTo | 216 | attributedTo: actorJSON.attributedTo |
160 | } | 217 | } |
161 | } | 218 | } |
162 | 219 | ||
163 | function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string, uuid?: string) { | ||
164 | return new ActorModel({ | ||
165 | type, | ||
166 | url, | ||
167 | preferredUsername, | ||
168 | uuid, | ||
169 | publicKey: null, | ||
170 | privateKey: null, | ||
171 | followersCount: 0, | ||
172 | followingCount: 0, | ||
173 | inboxUrl: url + '/inbox', | ||
174 | outboxUrl: url + '/outbox', | ||
175 | sharedInboxUrl: CONFIG.WEBSERVER.URL + '/inbox', | ||
176 | followersUrl: url + '/followers', | ||
177 | followingUrl: url + '/following' | ||
178 | }) | ||
179 | } | ||
180 | |||
181 | export { | ||
182 | getOrCreateActorAndServerAndModel, | ||
183 | saveActorAndServerAndModelIfNotExist, | ||
184 | fetchRemoteActor, | ||
185 | buildActorInstance, | ||
186 | setAsyncActorKeys | ||
187 | } | ||
188 | |||
189 | // --------------------------------------------------------------------------- | ||
190 | |||
191 | async function fetchActorTotalItems (url: string) { | 220 | async function fetchActorTotalItems (url: string) { |
192 | const options = { | 221 | const options = { |
193 | uri: url, | 222 | uri: url, |
diff --git a/server/lib/activitypub/url.ts b/server/lib/activitypub/url.ts index 3d5f0523c..0d76922e0 100644 --- a/server/lib/activitypub/url.ts +++ b/server/lib/activitypub/url.ts | |||
@@ -18,7 +18,7 @@ function getVideoChannelActivityPubUrl (videoChannelUUID: string) { | |||
18 | } | 18 | } |
19 | 19 | ||
20 | function getAccountActivityPubUrl (accountName: string) { | 20 | function getAccountActivityPubUrl (accountName: string) { |
21 | return CONFIG.WEBSERVER.URL + '/account/' + accountName | 21 | return CONFIG.WEBSERVER.URL + '/accounts/' + accountName |
22 | } | 22 | } |
23 | 23 | ||
24 | function getVideoAbuseActivityPubUrl (videoAbuse: VideoAbuseModel) { | 24 | function getVideoAbuseActivityPubUrl (videoAbuse: VideoAbuseModel) { |
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts index db40a5c88..42ebddd56 100644 --- a/server/middlewares/validators/users.ts +++ b/server/middlewares/validators/users.ts | |||
@@ -3,12 +3,14 @@ import 'express-validator' | |||
3 | import { body, param } from 'express-validator/check' | 3 | import { body, param } from 'express-validator/check' |
4 | import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc' | 4 | import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc' |
5 | import { | 5 | import { |
6 | isAvatarFile, | ||
6 | isUserAutoPlayVideoValid, isUserDisplayNSFWValid, isUserPasswordValid, isUserRoleValid, isUserUsernameValid, | 7 | isUserAutoPlayVideoValid, isUserDisplayNSFWValid, isUserPasswordValid, isUserRoleValid, isUserUsernameValid, |
7 | isUserVideoQuotaValid | 8 | isUserVideoQuotaValid |
8 | } from '../../helpers/custom-validators/users' | 9 | } from '../../helpers/custom-validators/users' |
9 | import { isVideoExist } from '../../helpers/custom-validators/videos' | 10 | import { isVideoExist, isVideoFile } from '../../helpers/custom-validators/videos' |
10 | import { logger } from '../../helpers/logger' | 11 | import { logger } from '../../helpers/logger' |
11 | import { isSignupAllowed } from '../../helpers/utils' | 12 | import { isSignupAllowed } from '../../helpers/utils' |
13 | import { CONSTRAINTS_FIELDS } from '../../initializers' | ||
12 | import { UserModel } from '../../models/account/user' | 14 | import { UserModel } from '../../models/account/user' |
13 | import { areValidationErrors } from './utils' | 15 | import { areValidationErrors } from './utils' |
14 | 16 | ||
@@ -96,6 +98,21 @@ const usersUpdateMeValidator = [ | |||
96 | } | 98 | } |
97 | ] | 99 | ] |
98 | 100 | ||
101 | const usersUpdateMyAvatarValidator = [ | ||
102 | body('avatarfile').custom((value, { req }) => isAvatarFile(req.files)).withMessage( | ||
103 | 'This file is not supported. Please, make sure it is of the following type : ' | ||
104 | + CONSTRAINTS_FIELDS.ACTOR.AVATAR.EXTNAME.join(', ') | ||
105 | ), | ||
106 | |||
107 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
108 | logger.debug('Checking usersUpdateMyAvatarValidator parameters', { parameters: req.body }) | ||
109 | |||
110 | if (areValidationErrors(req, res)) return | ||
111 | |||
112 | return next() | ||
113 | } | ||
114 | ] | ||
115 | |||
99 | const usersGetValidator = [ | 116 | const usersGetValidator = [ |
100 | param('id').isInt().not().isEmpty().withMessage('Should have a valid id'), | 117 | param('id').isInt().not().isEmpty().withMessage('Should have a valid id'), |
101 | 118 | ||
@@ -145,7 +162,8 @@ export { | |||
145 | usersUpdateMeValidator, | 162 | usersUpdateMeValidator, |
146 | usersVideoRatingValidator, | 163 | usersVideoRatingValidator, |
147 | ensureUserRegistrationAllowed, | 164 | ensureUserRegistrationAllowed, |
148 | usersGetValidator | 165 | usersGetValidator, |
166 | usersUpdateMyAvatarValidator | ||
149 | } | 167 | } |
150 | 168 | ||
151 | // --------------------------------------------------------------------------- | 169 | // --------------------------------------------------------------------------- |
diff --git a/server/models/account/account.ts b/server/models/account/account.ts index 1ee232537..d3503aaa3 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts | |||
@@ -13,6 +13,7 @@ import { | |||
13 | Table, | 13 | Table, |
14 | UpdatedAt | 14 | UpdatedAt |
15 | } from 'sequelize-typescript' | 15 | } from 'sequelize-typescript' |
16 | import { Account } from '../../../shared/models/actors' | ||
16 | import { isUserUsernameValid } from '../../helpers/custom-validators/users' | 17 | import { isUserUsernameValid } from '../../helpers/custom-validators/users' |
17 | import { sendDeleteActor } from '../../lib/activitypub/send' | 18 | import { sendDeleteActor } from '../../lib/activitypub/send' |
18 | import { ActorModel } from '../activitypub/actor' | 19 | import { ActorModel } from '../activitypub/actor' |
@@ -165,11 +166,12 @@ export class AccountModel extends Model<AccountModel> { | |||
165 | return AccountModel.findOne(query) | 166 | return AccountModel.findOne(query) |
166 | } | 167 | } |
167 | 168 | ||
168 | toFormattedJSON () { | 169 | toFormattedJSON (): Account { |
169 | const actor = this.Actor.toFormattedJSON() | 170 | const actor = this.Actor.toFormattedJSON() |
170 | const account = { | 171 | const account = { |
171 | id: this.id, | 172 | id: this.id, |
172 | name: this.name, | 173 | name: this.Actor.preferredUsername, |
174 | displayName: this.name, | ||
173 | createdAt: this.createdAt, | 175 | createdAt: this.createdAt, |
174 | updatedAt: this.updatedAt | 176 | updatedAt: this.updatedAt |
175 | } | 177 | } |
diff --git a/server/models/account/user.ts b/server/models/account/user.ts index d7e09e328..4226bcb35 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts | |||
@@ -4,6 +4,7 @@ import { | |||
4 | Scopes, Table, UpdatedAt | 4 | Scopes, Table, UpdatedAt |
5 | } from 'sequelize-typescript' | 5 | } from 'sequelize-typescript' |
6 | import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared' | 6 | import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared' |
7 | import { User } from '../../../shared/models/users' | ||
7 | import { | 8 | import { |
8 | isUserAutoPlayVideoValid, isUserDisplayNSFWValid, isUserPasswordValid, isUserRoleValid, isUserUsernameValid, | 9 | isUserAutoPlayVideoValid, isUserDisplayNSFWValid, isUserPasswordValid, isUserRoleValid, isUserUsernameValid, |
9 | isUserVideoQuotaValid | 10 | isUserVideoQuotaValid |
@@ -210,7 +211,7 @@ export class UserModel extends Model<UserModel> { | |||
210 | return comparePassword(password, this.password) | 211 | return comparePassword(password, this.password) |
211 | } | 212 | } |
212 | 213 | ||
213 | toFormattedJSON () { | 214 | toFormattedJSON (): User { |
214 | const json = { | 215 | const json = { |
215 | id: this.id, | 216 | id: this.id, |
216 | username: this.username, | 217 | username: this.username, |
@@ -221,11 +222,12 @@ export class UserModel extends Model<UserModel> { | |||
221 | roleLabel: USER_ROLE_LABELS[ this.role ], | 222 | roleLabel: USER_ROLE_LABELS[ this.role ], |
222 | videoQuota: this.videoQuota, | 223 | videoQuota: this.videoQuota, |
223 | createdAt: this.createdAt, | 224 | createdAt: this.createdAt, |
224 | account: this.Account.toFormattedJSON() | 225 | account: this.Account.toFormattedJSON(), |
226 | videoChannels: [] | ||
225 | } | 227 | } |
226 | 228 | ||
227 | if (Array.isArray(this.Account.VideoChannels) === true) { | 229 | if (Array.isArray(this.Account.VideoChannels) === true) { |
228 | json['videoChannels'] = this.Account.VideoChannels | 230 | json.videoChannels = this.Account.VideoChannels |
229 | .map(c => c.toFormattedJSON()) | 231 | .map(c => c.toFormattedJSON()) |
230 | .sort((v1, v2) => { | 232 | .sort((v1, v2) => { |
231 | if (v1.createdAt < v2.createdAt) return -1 | 233 | if (v1.createdAt < v2.createdAt) return -1 |
diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts index 3d96b3706..8422653df 100644 --- a/server/models/activitypub/actor.ts +++ b/server/models/activitypub/actor.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { values } from 'lodash' | 1 | import { values } from 'lodash' |
2 | import { join } from 'path' | 2 | import { extname, join } from 'path' |
3 | import * as Sequelize from 'sequelize' | 3 | import * as Sequelize from 'sequelize' |
4 | import { | 4 | import { |
5 | AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, DefaultScope, ForeignKey, HasMany, HasOne, Is, IsUUID, Model, Scopes, | 5 | AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, DefaultScope, ForeignKey, HasMany, HasOne, Is, IsUUID, Model, Scopes, |
@@ -30,6 +30,10 @@ enum ScopeNames { | |||
30 | { | 30 | { |
31 | model: () => ServerModel, | 31 | model: () => ServerModel, |
32 | required: false | 32 | required: false |
33 | }, | ||
34 | { | ||
35 | model: () => AvatarModel, | ||
36 | required: false | ||
33 | } | 37 | } |
34 | ] | 38 | ] |
35 | }) | 39 | }) |
@@ -47,6 +51,10 @@ enum ScopeNames { | |||
47 | { | 51 | { |
48 | model: () => ServerModel, | 52 | model: () => ServerModel, |
49 | required: false | 53 | required: false |
54 | }, | ||
55 | { | ||
56 | model: () => AvatarModel, | ||
57 | required: false | ||
50 | } | 58 | } |
51 | ] | 59 | ] |
52 | } | 60 | } |
@@ -141,7 +149,7 @@ export class ActorModel extends Model<ActorModel> { | |||
141 | foreignKey: { | 149 | foreignKey: { |
142 | allowNull: true | 150 | allowNull: true |
143 | }, | 151 | }, |
144 | onDelete: 'cascade' | 152 | onDelete: 'set null' |
145 | }) | 153 | }) |
146 | Avatar: AvatarModel | 154 | Avatar: AvatarModel |
147 | 155 | ||
@@ -253,11 +261,7 @@ export class ActorModel extends Model<ActorModel> { | |||
253 | toFormattedJSON () { | 261 | toFormattedJSON () { |
254 | let avatar: Avatar = null | 262 | let avatar: Avatar = null |
255 | if (this.Avatar) { | 263 | if (this.Avatar) { |
256 | avatar = { | 264 | avatar = this.Avatar.toFormattedJSON() |
257 | path: join(AVATARS_DIR.ACCOUNT, this.Avatar.filename), | ||
258 | createdAt: this.Avatar.createdAt, | ||
259 | updatedAt: this.Avatar.updatedAt | ||
260 | } | ||
261 | } | 265 | } |
262 | 266 | ||
263 | let score: number | 267 | let score: number |
@@ -286,6 +290,16 @@ export class ActorModel extends Model<ActorModel> { | |||
286 | activityPubType = 'Group' as 'Group' | 290 | activityPubType = 'Group' as 'Group' |
287 | } | 291 | } |
288 | 292 | ||
293 | let icon = undefined | ||
294 | if (this.avatarId) { | ||
295 | const extension = extname(this.Avatar.filename) | ||
296 | icon = { | ||
297 | type: 'Image', | ||
298 | mediaType: extension === '.png' ? 'image/png' : 'image/jpeg', | ||
299 | url: this.getAvatarUrl() | ||
300 | } | ||
301 | } | ||
302 | |||
289 | const json = { | 303 | const json = { |
290 | type: activityPubType, | 304 | type: activityPubType, |
291 | id: this.url, | 305 | id: this.url, |
@@ -304,7 +318,8 @@ export class ActorModel extends Model<ActorModel> { | |||
304 | id: this.getPublicKeyUrl(), | 318 | id: this.getPublicKeyUrl(), |
305 | owner: this.url, | 319 | owner: this.url, |
306 | publicKeyPem: this.publicKey | 320 | publicKeyPem: this.publicKey |
307 | } | 321 | }, |
322 | icon | ||
308 | } | 323 | } |
309 | 324 | ||
310 | return activityPubContextify(json) | 325 | return activityPubContextify(json) |
@@ -353,4 +368,10 @@ export class ActorModel extends Model<ActorModel> { | |||
353 | getHost () { | 368 | getHost () { |
354 | return this.Server ? this.Server.host : CONFIG.WEBSERVER.HOST | 369 | return this.Server ? this.Server.host : CONFIG.WEBSERVER.HOST |
355 | } | 370 | } |
371 | |||
372 | getAvatarUrl () { | ||
373 | if (!this.avatarId) return undefined | ||
374 | |||
375 | return CONFIG.WEBSERVER.URL + this.Avatar.getWebserverPath | ||
376 | } | ||
356 | } | 377 | } |
diff --git a/server/models/avatar/avatar.ts b/server/models/avatar/avatar.ts index 2e7a8ae2c..7493c3d75 100644 --- a/server/models/avatar/avatar.ts +++ b/server/models/avatar/avatar.ts | |||
@@ -1,4 +1,10 @@ | |||
1 | import { AllowNull, Column, CreatedAt, Model, Table, UpdatedAt } from 'sequelize-typescript' | 1 | import { join } from 'path' |
2 | import { AfterDestroy, AllowNull, Column, CreatedAt, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
3 | import { Avatar } from '../../../shared/models/avatars/avatar.model' | ||
4 | import { unlinkPromise } from '../../helpers/core-utils' | ||
5 | import { logger } from '../../helpers/logger' | ||
6 | import { CONFIG, STATIC_PATHS } from '../../initializers' | ||
7 | import { sendDeleteVideo } from '../../lib/activitypub/send' | ||
2 | 8 | ||
3 | @Table({ | 9 | @Table({ |
4 | tableName: 'avatar' | 10 | tableName: 'avatar' |
@@ -14,4 +20,26 @@ export class AvatarModel extends Model<AvatarModel> { | |||
14 | 20 | ||
15 | @UpdatedAt | 21 | @UpdatedAt |
16 | updatedAt: Date | 22 | updatedAt: Date |
23 | |||
24 | @AfterDestroy | ||
25 | static removeFilesAndSendDelete (instance: AvatarModel) { | ||
26 | return instance.removeAvatar() | ||
27 | } | ||
28 | |||
29 | toFormattedJSON (): Avatar { | ||
30 | return { | ||
31 | path: this.getWebserverPath(), | ||
32 | createdAt: this.createdAt, | ||
33 | updatedAt: this.updatedAt | ||
34 | } | ||
35 | } | ||
36 | |||
37 | getWebserverPath () { | ||
38 | return join(STATIC_PATHS.AVATARS, this.filename) | ||
39 | } | ||
40 | |||
41 | removeAvatar () { | ||
42 | const avatarPath = join(CONFIG.STORAGE.AVATARS_DIR, this.filename) | ||
43 | return unlinkPromise(avatarPath) | ||
44 | } | ||
17 | } | 45 | } |
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts index d381ccafa..829022a51 100644 --- a/server/models/video/video-comment.ts +++ b/server/models/video/video-comment.ts | |||
@@ -214,7 +214,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> { | |||
214 | 214 | ||
215 | static listThreadCommentsForApi (videoId: number, threadId: number) { | 215 | static listThreadCommentsForApi (videoId: number, threadId: number) { |
216 | const query = { | 216 | const query = { |
217 | order: [ [ 'id', 'ASC' ] ], | 217 | order: [ [ 'createdAt', 'DESC' ] ], |
218 | where: { | 218 | where: { |
219 | videoId, | 219 | videoId, |
220 | [ Sequelize.Op.or ]: [ | 220 | [ Sequelize.Op.or ]: [ |
diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts index 0c126dbff..44412ad82 100644 --- a/server/tests/api/check-params/users.ts +++ b/server/tests/api/check-params/users.ts | |||
@@ -2,11 +2,13 @@ | |||
2 | 2 | ||
3 | import { omit } from 'lodash' | 3 | import { omit } from 'lodash' |
4 | import 'mocha' | 4 | import 'mocha' |
5 | import { join } from "path" | ||
5 | import { UserRole } from '../../../../shared' | 6 | import { UserRole } from '../../../../shared' |
6 | 7 | ||
7 | import { | 8 | import { |
8 | createUser, flushTests, getMyUserInformation, getMyUserVideoRating, getUsersList, immutableAssign, killallServers, makeGetRequest, | 9 | createUser, flushTests, getMyUserInformation, getMyUserVideoRating, getUsersList, immutableAssign, killallServers, makeGetRequest, |
9 | makePostBodyRequest, makePutBodyRequest, registerUser, removeUser, runServer, ServerInfo, setAccessTokensToServers, updateUser, | 10 | makePostBodyRequest, makePostUploadRequest, makePutBodyRequest, registerUser, removeUser, runServer, ServerInfo, setAccessTokensToServers, |
11 | updateUser, | ||
10 | uploadVideo, userLogin | 12 | uploadVideo, userLogin |
11 | } from '../../utils' | 13 | } from '../../utils' |
12 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' | 14 | import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' |
@@ -266,6 +268,24 @@ describe('Test users API validators', function () { | |||
266 | }) | 268 | }) |
267 | }) | 269 | }) |
268 | 270 | ||
271 | describe('When updating my avatar', function () { | ||
272 | it('Should fail without an incorrect input file', async function () { | ||
273 | const fields = {} | ||
274 | const attaches = { | ||
275 | 'avatarfile': join(__dirname, '..', 'fixtures', 'video_short.mp4') | ||
276 | } | ||
277 | await makePostUploadRequest({ url: server.url, path: path + '/me/avatar/pick', token: server.accessToken, fields, attaches }) | ||
278 | }) | ||
279 | |||
280 | it('Should succeed with the correct params', async function () { | ||
281 | const fields = {} | ||
282 | const attaches = { | ||
283 | 'avatarfile': join(__dirname, '..', 'fixtures', 'avatar.png') | ||
284 | } | ||
285 | await makePostUploadRequest({ url: server.url, path: path + '/me/avatar/pick', token: server.accessToken, fields, attaches }) | ||
286 | }) | ||
287 | }) | ||
288 | |||
269 | describe('When updating a user', function () { | 289 | describe('When updating a user', function () { |
270 | 290 | ||
271 | before(async function () { | 291 | before(async function () { |
diff --git a/server/tests/api/fixtures/avatar.png b/server/tests/api/fixtures/avatar.png new file mode 100644 index 000000000..4b7fd2c0a --- /dev/null +++ b/server/tests/api/fixtures/avatar.png | |||
Binary files differ | |||
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts index 19549acdd..3390b2d56 100644 --- a/server/tests/api/users/users.ts +++ b/server/tests/api/users/users.ts | |||
@@ -6,7 +6,7 @@ import { UserRole } from '../../../../shared/index' | |||
6 | import { | 6 | import { |
7 | createUser, flushTests, getBlacklistedVideosList, getMyUserInformation, getMyUserVideoRating, getUserInformation, getUsersList, | 7 | createUser, flushTests, getBlacklistedVideosList, getMyUserInformation, getMyUserVideoRating, getUserInformation, getUsersList, |
8 | getUsersListPaginationAndSort, getVideosList, killallServers, login, makePutBodyRequest, rateVideo, registerUser, removeUser, removeVideo, | 8 | getUsersListPaginationAndSort, getVideosList, killallServers, login, makePutBodyRequest, rateVideo, registerUser, removeUser, removeVideo, |
9 | runServer, ServerInfo, serverLogin, updateMyUser, updateUser, uploadVideo | 9 | runServer, ServerInfo, serverLogin, testVideoImage, updateMyAvatar, updateMyUser, updateUser, uploadVideo |
10 | } from '../../utils/index' | 10 | } from '../../utils/index' |
11 | import { follow } from '../../utils/server/follows' | 11 | import { follow } from '../../utils/server/follows' |
12 | import { setAccessTokensToServers } from '../../utils/users/login' | 12 | import { setAccessTokensToServers } from '../../utils/users/login' |
@@ -340,6 +340,22 @@ describe('Test users', function () { | |||
340 | expect(user.id).to.be.a('number') | 340 | expect(user.id).to.be.a('number') |
341 | }) | 341 | }) |
342 | 342 | ||
343 | it('Should be able to update my avatar', async function () { | ||
344 | const fixture = 'avatar.png' | ||
345 | |||
346 | await updateMyAvatar({ | ||
347 | url: server.url, | ||
348 | accessToken: accessTokenUser, | ||
349 | fixture | ||
350 | }) | ||
351 | |||
352 | const res = await getMyUserInformation(server.url, accessTokenUser) | ||
353 | const user = res.body | ||
354 | |||
355 | const test = await testVideoImage(server.url, 'avatar', user.account.avatar.path, '.png') | ||
356 | expect(test).to.equal(true) | ||
357 | }) | ||
358 | |||
343 | it('Should be able to update another user', async function () { | 359 | it('Should be able to update another user', async function () { |
344 | await updateUser({ | 360 | await updateUser({ |
345 | url: server.url, | 361 | url: server.url, |
diff --git a/server/tests/utils/users/users.ts b/server/tests/utils/users/users.ts index e0cca3f51..90b1ca0a6 100644 --- a/server/tests/utils/users/users.ts +++ b/server/tests/utils/users/users.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import { isAbsolute, join } from 'path' | ||
1 | import * as request from 'supertest' | 2 | import * as request from 'supertest' |
2 | import { makePutBodyRequest } from '../' | 3 | import { makePostUploadRequest, makePutBodyRequest } from '../' |
3 | 4 | ||
4 | import { UserRole } from '../../../../shared/index' | 5 | import { UserRole } from '../../../../shared/index' |
5 | 6 | ||
@@ -137,6 +138,29 @@ function updateMyUser (options: { | |||
137 | }) | 138 | }) |
138 | } | 139 | } |
139 | 140 | ||
141 | function updateMyAvatar (options: { | ||
142 | url: string, | ||
143 | accessToken: string, | ||
144 | fixture: string | ||
145 | }) { | ||
146 | const path = '/api/v1/users/me/avatar/pick' | ||
147 | let filePath = '' | ||
148 | if (isAbsolute(options.fixture)) { | ||
149 | filePath = options.fixture | ||
150 | } else { | ||
151 | filePath = join(__dirname, '..', '..', 'api', 'fixtures', options.fixture) | ||
152 | } | ||
153 | |||
154 | return makePostUploadRequest({ | ||
155 | url: options.url, | ||
156 | path, | ||
157 | token: options.accessToken, | ||
158 | fields: {}, | ||
159 | attaches: { avatarfile: filePath }, | ||
160 | statusCodeExpected: 200 | ||
161 | }) | ||
162 | } | ||
163 | |||
140 | function updateUser (options: { | 164 | function updateUser (options: { |
141 | url: string | 165 | url: string |
142 | userId: number, | 166 | userId: number, |
@@ -173,5 +197,6 @@ export { | |||
173 | removeUser, | 197 | removeUser, |
174 | updateUser, | 198 | updateUser, |
175 | updateMyUser, | 199 | updateMyUser, |
176 | getUserInformation | 200 | getUserInformation, |
201 | updateMyAvatar | ||
177 | } | 202 | } |
diff --git a/server/tests/utils/videos/videos.ts b/server/tests/utils/videos/videos.ts index d6bf27dc7..aca51ee5d 100644 --- a/server/tests/utils/videos/videos.ts +++ b/server/tests/utils/videos/videos.ts | |||
@@ -201,7 +201,7 @@ function searchVideoWithSort (url: string, search: string, sort: string) { | |||
201 | .expect('Content-Type', /json/) | 201 | .expect('Content-Type', /json/) |
202 | } | 202 | } |
203 | 203 | ||
204 | async function testVideoImage (url: string, imageName: string, imagePath: string) { | 204 | async function testVideoImage (url: string, imageName: string, imagePath: string, extension = '.jpg') { |
205 | // Don't test images if the node env is not set | 205 | // Don't test images if the node env is not set |
206 | // Because we need a special ffmpeg version for this test | 206 | // Because we need a special ffmpeg version for this test |
207 | if (process.env['NODE_TEST_IMAGE']) { | 207 | if (process.env['NODE_TEST_IMAGE']) { |
@@ -209,7 +209,7 @@ async function testVideoImage (url: string, imageName: string, imagePath: string | |||
209 | .get(imagePath) | 209 | .get(imagePath) |
210 | .expect(200) | 210 | .expect(200) |
211 | 211 | ||
212 | const data = await readFilePromise(join(__dirname, '..', '..', 'api', 'fixtures', imageName + '.jpg')) | 212 | const data = await readFilePromise(join(__dirname, '..', '..', 'api', 'fixtures', imageName + extension)) |
213 | 213 | ||
214 | return data.equals(res.body) | 214 | return data.equals(res.body) |
215 | } else { | 215 | } else { |