aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/activitypub/client.ts6
-rw-r--r--server/controllers/api/users.ts53
-rw-r--r--server/controllers/api/videos/index.ts27
-rw-r--r--server/controllers/static.ts6
-rw-r--r--server/helpers/custom-validators/users.ts21
-rw-r--r--server/helpers/utils.ts30
-rw-r--r--server/initializers/constants.ts19
-rw-r--r--server/initializers/migrations/0150-avatar-cascade.ts28
-rw-r--r--server/lib/activitypub/actor.ts87
-rw-r--r--server/lib/activitypub/url.ts2
-rw-r--r--server/middlewares/validators/users.ts22
-rw-r--r--server/models/account/account.ts6
-rw-r--r--server/models/account/user.ts8
-rw-r--r--server/models/activitypub/actor.ts37
-rw-r--r--server/models/avatar/avatar.ts30
-rw-r--r--server/models/video/video-comment.ts2
-rw-r--r--server/tests/api/check-params/users.ts22
-rw-r--r--server/tests/api/fixtures/avatar.pngbin0 -> 1674 bytes
-rw-r--r--server/tests/api/users/users.ts18
-rw-r--r--server/tests/utils/users/users.ts29
-rw-r--r--server/tests/utils/videos/videos.ts4
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
17const activityPubClientRouter = express.Router() 17const activityPubClientRouter = express.Router()
18 18
19activityPubClientRouter.get('/account/:name', 19activityPubClientRouter.get('/accounts/:name',
20 executeIfActivityPub(asyncMiddleware(localAccountValidator)), 20 executeIfActivityPub(asyncMiddleware(localAccountValidator)),
21 executeIfActivityPub(accountController) 21 executeIfActivityPub(accountController)
22) 22)
23 23
24activityPubClientRouter.get('/account/:name/followers', 24activityPubClientRouter.get('/accounts/:name/followers',
25 executeIfActivityPub(asyncMiddleware(localAccountValidator)), 25 executeIfActivityPub(asyncMiddleware(localAccountValidator)),
26 executeIfActivityPub(asyncMiddleware(accountFollowersController)) 26 executeIfActivityPub(asyncMiddleware(accountFollowersController))
27) 27)
28 28
29activityPubClientRouter.get('/account/:name/following', 29activityPubClientRouter.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 @@
1import * as express from 'express' 1import * as express from 'express'
2import { extname, join } from 'path'
3import * as uuidv4 from 'uuid/v4'
2import { UserCreate, UserRight, UserRole, UserUpdate, UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../shared' 4import { UserCreate, UserRight, UserRole, UserUpdate, UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../shared'
5import { renamePromise } from '../../helpers/core-utils'
3import { retryTransactionWrapper } from '../../helpers/database-utils' 6import { retryTransactionWrapper } from '../../helpers/database-utils'
4import { logger } from '../../helpers/logger' 7import { logger } from '../../helpers/logger'
5import { getFormattedObjects } from '../../helpers/utils' 8import { createReqFiles, getFormattedObjects } from '../../helpers/utils'
6import { CONFIG } from '../../initializers' 9import { AVATAR_MIMETYPE_EXT, CONFIG, sequelizeTypescript } from '../../initializers'
7import { createUserAccountAndChannel } from '../../lib/user' 10import { createUserAccountAndChannel } from '../../lib/user'
8import { 11import {
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'
13import { videosSortValidator } from '../../middlewares/validators' 16import { usersUpdateMyAvatarValidator, videosSortValidator } from '../../middlewares/validators'
14import { AccountVideoRateModel } from '../../models/account/account-video-rate' 17import { AccountVideoRateModel } from '../../models/account/account-video-rate'
15import { UserModel } from '../../models/account/user' 18import { UserModel } from '../../models/account/user'
19import { AvatarModel } from '../../models/avatar/avatar'
16import { VideoModel } from '../../models/video/video' 20import { VideoModel } from '../../models/video/video'
17 21
22const reqAvatarFile = createReqFiles('avatarfile', CONFIG.STORAGE.AVATARS_DIR, AVATAR_MIMETYPE_EXT)
23
18const usersRouter = express.Router() 24const usersRouter = express.Router()
19 25
20usersRouter.get('/me', 26usersRouter.get('/me',
@@ -71,6 +77,13 @@ usersRouter.put('/me',
71 asyncMiddleware(updateMe) 77 asyncMiddleware(updateMe)
72) 78)
73 79
80usersRouter.post('/me/avatar/pick',
81 authenticate,
82 reqAvatarFile,
83 usersUpdateMyAvatarValidator,
84 asyncMiddleware(updateMyAvatar)
85)
86
74usersRouter.put('/:id', 87usersRouter.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
232async 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
219async function updateUser (req: express.Request, res: express.Response, next: express.NextFunction) { 266async 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'
6import { retryTransactionWrapper } from '../../../helpers/database-utils' 6import { retryTransactionWrapper } from '../../../helpers/database-utils'
7import { getVideoFileHeight } from '../../../helpers/ffmpeg-utils' 7import { getVideoFileHeight } from '../../../helpers/ffmpeg-utils'
8import { logger } from '../../../helpers/logger' 8import { logger } from '../../../helpers/logger'
9import { generateRandomString, getFormattedObjects, getServerActor, resetSequelizeInstance } from '../../../helpers/utils' 9import { createReqFiles, generateRandomString, getFormattedObjects, getServerActor, resetSequelizeInstance } from '../../../helpers/utils'
10import { 10import {
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
30const videosRouter = express.Router() 30const videosRouter = express.Router()
31 31
32// multer configuration 32const reqVideoFile = createReqFiles('videofile', CONFIG.STORAGE.VIDEOS_DIR, VIDEO_MIMETYPE_EXT)
33const 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
53const reqFiles = multer({ storage: storage }).fields([{ name: 'videofile', maxCount: 1 }])
54 33
55videosRouter.use('/', abuseVideoRouter) 34videosRouter.use('/', abuseVideoRouter)
56videosRouter.use('/', blacklistRouter) 35videosRouter.use('/', blacklistRouter)
@@ -85,7 +64,7 @@ videosRouter.put('/:id',
85) 64)
86videosRouter.post('/upload', 65videosRouter.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
35const avatarsPhysicalPath = CONFIG.STORAGE.AVATARS_DIR
36staticRouter.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
36staticRouter.use( 42staticRouter.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 @@
1import * as validator from 'validator' 1import * as validator from 'validator'
2import 'express-validator' 2import 'express-validator'
3 3
4import { exists } from './misc' 4import { exists, isArray } from './misc'
5import { CONSTRAINTS_FIELDS } from '../../initializers' 5import { CONSTRAINTS_FIELDS } from '../../initializers'
6import { UserRole } from '../../../shared' 6import { 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
40function 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
42export { 58export {
@@ -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 @@
1import * as express from 'express' 1import * as express from 'express'
2import * as multer from 'multer'
2import { Model } from 'sequelize-typescript' 3import { Model } from 'sequelize-typescript'
3import { ResultList } from '../../shared' 4import { ResultList } from '../../shared'
4import { VideoResolution } from '../../shared/models/videos' 5import { VideoResolution } from '../../shared/models/videos'
5import { CONFIG, REMOTE_SCHEME } from '../initializers' 6import { CONFIG, REMOTE_SCHEME, VIDEO_MIMETYPE_EXT } from '../initializers'
6import { UserModel } from '../models/account/user' 7import { UserModel } from '../models/account/user'
7import { ActorModel } from '../models/activitypub/actor' 8import { ActorModel } from '../models/activitypub/actor'
8import { ApplicationModel } from '../models/application/application' 9import { 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
30function 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
29async function generateRandomString (size: number) { 54async 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
12const LAST_MIGRATION_VERSION = 145 12const 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
256const AVATAR_MIMETYPE_EXT = {
257 'image/png': '.png',
258 'image/jpg': '.jpg',
259 'image/jpeg': '.jpg'
260}
261
253// --------------------------------------------------------------------------- 262// ---------------------------------------------------------------------------
254 263
255const SERVER_ACTOR_NAME = 'peertube' 264const 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 @@
1import * as Sequelize from 'sequelize'
2
3async 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
21function down (options) {
22 throw new Error('Not implemented.')
23}
24
25export {
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 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { join } from 'path'
2import { Transaction } from 'sequelize' 3import { Transaction } from 'sequelize'
3import * as url from 'url' 4import * as url from 'url'
5import * as uuidv4 from 'uuid/v4'
4import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub' 6import { ActivityPubActor, ActivityPubActorType } from '../../../shared/models/activitypub'
5import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' 7import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
6import { isRemoteActorValid } from '../../helpers/custom-validators/activitypub/actor' 8import { isRemoteActorValid } from '../../helpers/custom-validators/activitypub/actor'
9import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
7import { retryTransactionWrapper } from '../../helpers/database-utils' 10import { retryTransactionWrapper } from '../../helpers/database-utils'
8import { logger } from '../../helpers/logger' 11import { logger } from '../../helpers/logger'
9import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto' 12import { createPrivateAndPublicKeys } from '../../helpers/peertube-crypto'
10import { doRequest } from '../../helpers/requests' 13import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
11import { CONFIG, sequelizeTypescript } from '../../initializers' 14import { CONFIG, sequelizeTypescript } from '../../initializers'
12import { AccountModel } from '../../models/account/account' 15import { AccountModel } from '../../models/account/account'
13import { ActorModel } from '../../models/activitypub/actor' 16import { ActorModel } from '../../models/activitypub/actor'
17import { AvatarModel } from '../../models/avatar/avatar'
14import { ServerModel } from '../../models/server/server' 18import { ServerModel } from '../../models/server/server'
15import { VideoChannelModel } from '../../models/video/video-channel' 19import { 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
69function 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
87export {
88 getOrCreateActorAndServerAndModel,
89 buildActorInstance,
90 setAsyncActorKeys
91}
92
93// ---------------------------------------------------------------------------
94
65function saveActorAndServerAndModelIfNotExist ( 95function 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}
117async function fetchRemoteActor (actorUrl: string): Promise<FetchRemoteActorResult> { 156async 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
163function 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
181export {
182 getOrCreateActorAndServerAndModel,
183 saveActorAndServerAndModelIfNotExist,
184 fetchRemoteActor,
185 buildActorInstance,
186 setAsyncActorKeys
187}
188
189// ---------------------------------------------------------------------------
190
191async function fetchActorTotalItems (url: string) { 220async 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
20function getAccountActivityPubUrl (accountName: string) { 20function getAccountActivityPubUrl (accountName: string) {
21 return CONFIG.WEBSERVER.URL + '/account/' + accountName 21 return CONFIG.WEBSERVER.URL + '/accounts/' + accountName
22} 22}
23 23
24function getVideoAbuseActivityPubUrl (videoAbuse: VideoAbuseModel) { 24function 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'
3import { body, param } from 'express-validator/check' 3import { body, param } from 'express-validator/check'
4import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc' 4import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc'
5import { 5import {
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'
9import { isVideoExist } from '../../helpers/custom-validators/videos' 10import { isVideoExist, isVideoFile } from '../../helpers/custom-validators/videos'
10import { logger } from '../../helpers/logger' 11import { logger } from '../../helpers/logger'
11import { isSignupAllowed } from '../../helpers/utils' 12import { isSignupAllowed } from '../../helpers/utils'
13import { CONSTRAINTS_FIELDS } from '../../initializers'
12import { UserModel } from '../../models/account/user' 14import { UserModel } from '../../models/account/user'
13import { areValidationErrors } from './utils' 15import { areValidationErrors } from './utils'
14 16
@@ -96,6 +98,21 @@ const usersUpdateMeValidator = [
96 } 98 }
97] 99]
98 100
101const 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
99const usersGetValidator = [ 116const 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'
16import { Account } from '../../../shared/models/actors'
16import { isUserUsernameValid } from '../../helpers/custom-validators/users' 17import { isUserUsernameValid } from '../../helpers/custom-validators/users'
17import { sendDeleteActor } from '../../lib/activitypub/send' 18import { sendDeleteActor } from '../../lib/activitypub/send'
18import { ActorModel } from '../activitypub/actor' 19import { 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'
6import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared' 6import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared'
7import { User } from '../../../shared/models/users'
7import { 8import {
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 @@
1import { values } from 'lodash' 1import { values } from 'lodash'
2import { join } from 'path' 2import { extname, join } from 'path'
3import * as Sequelize from 'sequelize' 3import * as Sequelize from 'sequelize'
4import { 4import {
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 @@
1import { AllowNull, Column, CreatedAt, Model, Table, UpdatedAt } from 'sequelize-typescript' 1import { join } from 'path'
2import { AfterDestroy, AllowNull, Column, CreatedAt, Model, Table, UpdatedAt } from 'sequelize-typescript'
3import { Avatar } from '../../../shared/models/avatars/avatar.model'
4import { unlinkPromise } from '../../helpers/core-utils'
5import { logger } from '../../helpers/logger'
6import { CONFIG, STATIC_PATHS } from '../../initializers'
7import { 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
3import { omit } from 'lodash' 3import { omit } from 'lodash'
4import 'mocha' 4import 'mocha'
5import { join } from "path"
5import { UserRole } from '../../../../shared' 6import { UserRole } from '../../../../shared'
6 7
7import { 8import {
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'
12import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' 14import { 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'
6import { 6import {
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'
11import { follow } from '../../utils/server/follows' 11import { follow } from '../../utils/server/follows'
12import { setAccessTokensToServers } from '../../utils/users/login' 12import { 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 @@
1import { isAbsolute, join } from 'path'
1import * as request from 'supertest' 2import * as request from 'supertest'
2import { makePutBodyRequest } from '../' 3import { makePostUploadRequest, makePutBodyRequest } from '../'
3 4
4import { UserRole } from '../../../../shared/index' 5import { UserRole } from '../../../../shared/index'
5 6
@@ -137,6 +138,29 @@ function updateMyUser (options: {
137 }) 138 })
138} 139}
139 140
141function 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
140function updateUser (options: { 164function 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
204async function testVideoImage (url: string, imageName: string, imagePath: string) { 204async 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 {