aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2021-04-06 17:01:35 +0200
committerChocobozzz <chocobozzz@cpy.re>2021-04-08 10:07:53 +0200
commit2cb03dc1f4e01ba491c36caff30c33fe9c5bad89 (patch)
tree08a8706d105ea1e280339c02b9e2b1dc1edb0ff9 /server
parentf479685678406a5df864d89615b33d29085ebfc6 (diff)
downloadPeerTube-2cb03dc1f4e01ba491c36caff30c33fe9c5bad89.tar.gz
PeerTube-2cb03dc1f4e01ba491c36caff30c33fe9c5bad89.tar.zst
PeerTube-2cb03dc1f4e01ba491c36caff30c33fe9c5bad89.zip
Add banners support
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/config.ts4
-rw-r--r--server/controllers/api/users/me.ts8
-rw-r--r--server/controllers/api/users/my-subscriptions.ts12
-rw-r--r--server/controllers/api/video-channel.ts66
-rw-r--r--server/controllers/api/videos/ownership.ts2
-rw-r--r--server/controllers/lazy-static.ts2
-rw-r--r--server/controllers/static.ts4
-rw-r--r--server/helpers/custom-validators/users.ts8
-rw-r--r--server/helpers/middlewares/video-channels.ts7
-rw-r--r--server/helpers/middlewares/videos.ts23
-rw-r--r--server/initializers/constants.ts20
-rw-r--r--server/lib/activitypub/actor.ts106
-rw-r--r--server/lib/activitypub/process/process-delete.ts6
-rw-r--r--server/lib/activitypub/process/process-update.ts14
-rw-r--r--server/lib/actor-image.ts51
-rw-r--r--server/lib/client-html.ts6
-rw-r--r--server/lib/emailer.ts2
-rw-r--r--server/lib/video-channel.ts16
-rw-r--r--server/middlewares/validators/avatar.ts16
-rw-r--r--server/middlewares/validators/follows.ts1
-rw-r--r--server/middlewares/validators/videos/video-channels.ts2
-rw-r--r--server/models/activitypub/actor-follow.ts7
-rw-r--r--server/models/activitypub/actor.ts47
-rw-r--r--server/models/video/video-channel.ts88
-rw-r--r--server/types/models/account/account.ts10
-rw-r--r--server/types/models/account/actor-follow.ts11
-rw-r--r--server/types/models/account/actor.ts33
-rw-r--r--server/types/models/user/user.ts8
-rw-r--r--server/types/models/video/video-channels.ts34
-rw-r--r--server/typings/express/index.d.ts7
30 files changed, 385 insertions, 236 deletions
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts
index fb108ca1c..313513cea 100644
--- a/server/controllers/api/config.ts
+++ b/server/controllers/api/config.ts
@@ -158,9 +158,9 @@ async function getConfig (req: express.Request, res: express.Response) {
158 avatar: { 158 avatar: {
159 file: { 159 file: {
160 size: { 160 size: {
161 max: CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max 161 max: CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max
162 }, 162 },
163 extensions: CONSTRAINTS_FIELDS.ACTORS.AVATAR.EXTNAME 163 extensions: CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME
164 } 164 }
165 }, 165 },
166 video: { 166 video: {
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts
index 4671ec5ac..25a18caa5 100644
--- a/server/controllers/api/users/me.ts
+++ b/server/controllers/api/users/me.ts
@@ -2,7 +2,7 @@ import 'multer'
2import * as express from 'express' 2import * as express from 'express'
3import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '@server/helpers/audit-logger' 3import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '@server/helpers/audit-logger'
4import { Hooks } from '@server/lib/plugins/hooks' 4import { Hooks } from '@server/lib/plugins/hooks'
5import { UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../../shared' 5import { ActorImageType, UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../../shared'
6import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' 6import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
7import { UserVideoQuota } from '../../../../shared/models/users/user-video-quota.model' 7import { UserVideoQuota } from '../../../../shared/models/users/user-video-quota.model'
8import { createReqFiles } from '../../../helpers/express-utils' 8import { createReqFiles } from '../../../helpers/express-utils'
@@ -11,7 +11,7 @@ import { CONFIG } from '../../../initializers/config'
11import { MIMETYPES } from '../../../initializers/constants' 11import { MIMETYPES } from '../../../initializers/constants'
12import { sequelizeTypescript } from '../../../initializers/database' 12import { sequelizeTypescript } from '../../../initializers/database'
13import { sendUpdateActor } from '../../../lib/activitypub/send' 13import { sendUpdateActor } from '../../../lib/activitypub/send'
14import { deleteLocalActorAvatarFile, updateLocalActorAvatarFile } from '../../../lib/actor-image' 14import { deleteLocalActorImageFile, updateLocalActorImageFile } from '../../../lib/actor-image'
15import { getOriginalVideoFileTotalDailyFromUser, getOriginalVideoFileTotalFromUser, sendVerifyUserEmail } from '../../../lib/user' 15import { getOriginalVideoFileTotalDailyFromUser, getOriginalVideoFileTotalFromUser, sendVerifyUserEmail } from '../../../lib/user'
16import { 16import {
17 asyncMiddleware, 17 asyncMiddleware,
@@ -238,7 +238,7 @@ async function updateMyAvatar (req: express.Request, res: express.Response) {
238 238
239 const userAccount = await AccountModel.load(user.Account.id) 239 const userAccount = await AccountModel.load(user.Account.id)
240 240
241 const avatar = await updateLocalActorAvatarFile(userAccount, avatarPhysicalFile) 241 const avatar = await updateLocalActorImageFile(userAccount, avatarPhysicalFile, ActorImageType.AVATAR)
242 242
243 return res.json({ avatar: avatar.toFormattedJSON() }) 243 return res.json({ avatar: avatar.toFormattedJSON() })
244} 244}
@@ -247,7 +247,7 @@ async function deleteMyAvatar (req: express.Request, res: express.Response) {
247 const user = res.locals.oauth.token.user 247 const user = res.locals.oauth.token.user
248 248
249 const userAccount = await AccountModel.load(user.Account.id) 249 const userAccount = await AccountModel.load(user.Account.id)
250 await deleteLocalActorAvatarFile(userAccount) 250 await deleteLocalActorImageFile(userAccount, ActorImageType.AVATAR)
251 251
252 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 252 return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
253} 253}
diff --git a/server/controllers/api/users/my-subscriptions.ts b/server/controllers/api/users/my-subscriptions.ts
index ec77ddd7a..e8949ee59 100644
--- a/server/controllers/api/users/my-subscriptions.ts
+++ b/server/controllers/api/users/my-subscriptions.ts
@@ -1,5 +1,8 @@
1import 'multer' 1import 'multer'
2import * as express from 'express' 2import * as express from 'express'
3import { sendUndoFollow } from '@server/lib/activitypub/send'
4import { VideoChannelModel } from '@server/models/video/video-channel'
5import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
3import { VideoFilter } from '../../../../shared/models/videos/video-query.type' 6import { VideoFilter } from '../../../../shared/models/videos/video-query.type'
4import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils' 7import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils'
5import { getFormattedObjects } from '../../../helpers/utils' 8import { getFormattedObjects } from '../../../helpers/utils'
@@ -26,8 +29,6 @@ import {
26} from '../../../middlewares/validators' 29} from '../../../middlewares/validators'
27import { ActorFollowModel } from '../../../models/activitypub/actor-follow' 30import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
28import { VideoModel } from '../../../models/video/video' 31import { VideoModel } from '../../../models/video/video'
29import { sendUndoFollow } from '@server/lib/activitypub/send'
30import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
31 32
32const mySubscriptionsRouter = express.Router() 33const mySubscriptionsRouter = express.Router()
33 34
@@ -66,7 +67,7 @@ mySubscriptionsRouter.post('/me/subscriptions',
66mySubscriptionsRouter.get('/me/subscriptions/:uri', 67mySubscriptionsRouter.get('/me/subscriptions/:uri',
67 authenticate, 68 authenticate,
68 userSubscriptionGetValidator, 69 userSubscriptionGetValidator,
69 getUserSubscription 70 asyncMiddleware(getUserSubscription)
70) 71)
71 72
72mySubscriptionsRouter.delete('/me/subscriptions/:uri', 73mySubscriptionsRouter.delete('/me/subscriptions/:uri',
@@ -130,10 +131,11 @@ function addUserSubscription (req: express.Request, res: express.Response) {
130 return res.status(HttpStatusCode.NO_CONTENT_204).end() 131 return res.status(HttpStatusCode.NO_CONTENT_204).end()
131} 132}
132 133
133function getUserSubscription (req: express.Request, res: express.Response) { 134async function getUserSubscription (req: express.Request, res: express.Response) {
134 const subscription = res.locals.subscription 135 const subscription = res.locals.subscription
136 const videoChannel = await VideoChannelModel.loadAndPopulateAccount(subscription.ActorFollowing.VideoChannel.id)
135 137
136 return res.json(subscription.ActorFollowing.VideoChannel.toFormattedJSON()) 138 return res.json(videoChannel.toFormattedJSON())
137} 139}
138 140
139async function deleteUserSubscription (req: express.Request, res: express.Response) { 141async function deleteUserSubscription (req: express.Request, res: express.Response) {
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts
index c9d8e1120..1c926722d 100644
--- a/server/controllers/api/video-channel.ts
+++ b/server/controllers/api/video-channel.ts
@@ -1,8 +1,8 @@
1import * as express from 'express' 1import * as express from 'express'
2import { Hooks } from '@server/lib/plugins/hooks' 2import { Hooks } from '@server/lib/plugins/hooks'
3import { getServerActor } from '@server/models/application/application' 3import { getServerActor } from '@server/models/application/application'
4import { MChannelAccountDefault } from '@server/types/models' 4import { MChannelBannerAccountDefault } from '@server/types/models'
5import { VideoChannelCreate, VideoChannelUpdate } from '../../../shared' 5import { ActorImageType, VideoChannelCreate, VideoChannelUpdate } from '../../../shared'
6import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' 6import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
7import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger' 7import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger'
8import { resetSequelizeInstance } from '../../helpers/database-utils' 8import { resetSequelizeInstance } from '../../helpers/database-utils'
@@ -13,7 +13,7 @@ import { CONFIG } from '../../initializers/config'
13import { MIMETYPES } from '../../initializers/constants' 13import { MIMETYPES } from '../../initializers/constants'
14import { sequelizeTypescript } from '../../initializers/database' 14import { sequelizeTypescript } from '../../initializers/database'
15import { sendUpdateActor } from '../../lib/activitypub/send' 15import { sendUpdateActor } from '../../lib/activitypub/send'
16import { deleteLocalActorAvatarFile, updateLocalActorAvatarFile } from '../../lib/actor-image' 16import { deleteLocalActorImageFile, updateLocalActorImageFile } from '../../lib/actor-image'
17import { JobQueue } from '../../lib/job-queue' 17import { JobQueue } from '../../lib/job-queue'
18import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel' 18import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel'
19import { 19import {
@@ -33,7 +33,7 @@ import {
33 videoPlaylistsSortValidator 33 videoPlaylistsSortValidator
34} from '../../middlewares' 34} from '../../middlewares'
35import { videoChannelsNameWithHostValidator, videoChannelsOwnSearchValidator, videosSortValidator } from '../../middlewares/validators' 35import { videoChannelsNameWithHostValidator, videoChannelsOwnSearchValidator, videosSortValidator } from '../../middlewares/validators'
36import { updateAvatarValidator } from '../../middlewares/validators/avatar' 36import { updateAvatarValidator, updateBannerValidator } from '../../middlewares/validators/avatar'
37import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists' 37import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists'
38import { AccountModel } from '../../models/account/account' 38import { AccountModel } from '../../models/account/account'
39import { VideoModel } from '../../models/video/video' 39import { VideoModel } from '../../models/video/video'
@@ -42,6 +42,7 @@ import { VideoPlaylistModel } from '../../models/video/video-playlist'
42 42
43const auditLogger = auditLoggerFactory('channels') 43const auditLogger = auditLoggerFactory('channels')
44const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR }) 44const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR })
45const reqBannerFile = createReqFiles([ 'bannerfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { bannerfile: CONFIG.STORAGE.TMP_DIR })
45 46
46const videoChannelRouter = express.Router() 47const videoChannelRouter = express.Router()
47 48
@@ -69,6 +70,15 @@ videoChannelRouter.post('/:nameWithHost/avatar/pick',
69 asyncMiddleware(updateVideoChannelAvatar) 70 asyncMiddleware(updateVideoChannelAvatar)
70) 71)
71 72
73videoChannelRouter.post('/:nameWithHost/banner/pick',
74 authenticate,
75 reqBannerFile,
76 // Check the rights
77 asyncMiddleware(videoChannelsUpdateValidator),
78 updateBannerValidator,
79 asyncMiddleware(updateVideoChannelBanner)
80)
81
72videoChannelRouter.delete('/:nameWithHost/avatar', 82videoChannelRouter.delete('/:nameWithHost/avatar',
73 authenticate, 83 authenticate,
74 // Check the rights 84 // Check the rights
@@ -76,6 +86,13 @@ videoChannelRouter.delete('/:nameWithHost/avatar',
76 asyncMiddleware(deleteVideoChannelAvatar) 86 asyncMiddleware(deleteVideoChannelAvatar)
77) 87)
78 88
89videoChannelRouter.delete('/:nameWithHost/banner',
90 authenticate,
91 // Check the rights
92 asyncMiddleware(videoChannelsUpdateValidator),
93 asyncMiddleware(deleteVideoChannelBanner)
94)
95
79videoChannelRouter.put('/:nameWithHost', 96videoChannelRouter.put('/:nameWithHost',
80 authenticate, 97 authenticate,
81 asyncMiddleware(videoChannelsUpdateValidator), 98 asyncMiddleware(videoChannelsUpdateValidator),
@@ -134,26 +151,41 @@ async function listVideoChannels (req: express.Request, res: express.Response) {
134 return res.json(getFormattedObjects(resultList.data, resultList.total)) 151 return res.json(getFormattedObjects(resultList.data, resultList.total))
135} 152}
136 153
154async function updateVideoChannelBanner (req: express.Request, res: express.Response) {
155 const bannerPhysicalFile = req.files['bannerfile'][0]
156 const videoChannel = res.locals.videoChannel
157 const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON())
158
159 const banner = await updateLocalActorImageFile(videoChannel, bannerPhysicalFile, ActorImageType.BANNER)
160
161 auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys)
162
163 return res.json({ banner: banner.toFormattedJSON() })
164}
137async function updateVideoChannelAvatar (req: express.Request, res: express.Response) { 165async function updateVideoChannelAvatar (req: express.Request, res: express.Response) {
138 const avatarPhysicalFile = req.files['avatarfile'][0] 166 const avatarPhysicalFile = req.files['avatarfile'][0]
139 const videoChannel = res.locals.videoChannel 167 const videoChannel = res.locals.videoChannel
140 const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON()) 168 const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON())
141 169
142 const avatar = await updateLocalActorAvatarFile(videoChannel, avatarPhysicalFile) 170 const avatar = await updateLocalActorImageFile(videoChannel, avatarPhysicalFile, ActorImageType.AVATAR)
143 171
144 auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys) 172 auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys)
145 173
146 return res 174 return res.json({ avatar: avatar.toFormattedJSON() })
147 .json({
148 avatar: avatar.toFormattedJSON()
149 })
150 .end()
151} 175}
152 176
153async function deleteVideoChannelAvatar (req: express.Request, res: express.Response) { 177async function deleteVideoChannelAvatar (req: express.Request, res: express.Response) {
154 const videoChannel = res.locals.videoChannel 178 const videoChannel = res.locals.videoChannel
155 179
156 await deleteLocalActorAvatarFile(videoChannel) 180 await deleteLocalActorImageFile(videoChannel, ActorImageType.AVATAR)
181
182 return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
183}
184
185async function deleteVideoChannelBanner (req: express.Request, res: express.Response) {
186 const videoChannel = res.locals.videoChannel
187
188 await deleteLocalActorImageFile(videoChannel, ActorImageType.BANNER)
157 189
158 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 190 return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
159} 191}
@@ -177,7 +209,7 @@ async function addVideoChannel (req: express.Request, res: express.Response) {
177 videoChannel: { 209 videoChannel: {
178 id: videoChannelCreated.id 210 id: videoChannelCreated.id
179 } 211 }
180 }).end() 212 })
181} 213}
182 214
183async function updateVideoChannel (req: express.Request, res: express.Response) { 215async function updateVideoChannel (req: express.Request, res: express.Response) {
@@ -206,7 +238,7 @@ async function updateVideoChannel (req: express.Request, res: express.Response)
206 } 238 }
207 } 239 }
208 240
209 const videoChannelInstanceUpdated = await videoChannelInstance.save(sequelizeOptions) as MChannelAccountDefault 241 const videoChannelInstanceUpdated = await videoChannelInstance.save(sequelizeOptions) as MChannelBannerAccountDefault
210 await sendUpdateActor(videoChannelInstanceUpdated, t) 242 await sendUpdateActor(videoChannelInstanceUpdated, t)
211 243
212 auditLogger.update( 244 auditLogger.update(
@@ -252,13 +284,13 @@ async function removeVideoChannel (req: express.Request, res: express.Response)
252} 284}
253 285
254async function getVideoChannel (req: express.Request, res: express.Response) { 286async function getVideoChannel (req: express.Request, res: express.Response) {
255 const videoChannelWithVideos = await VideoChannelModel.loadAndPopulateAccountAndVideos(res.locals.videoChannel.id) 287 const videoChannel = res.locals.videoChannel
256 288
257 if (videoChannelWithVideos.isOutdated()) { 289 if (videoChannel.isOutdated()) {
258 JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'actor', url: videoChannelWithVideos.Actor.url } }) 290 JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'actor', url: videoChannel.Actor.url } })
259 } 291 }
260 292
261 return res.json(videoChannelWithVideos.toFormattedJSON()) 293 return res.json(videoChannel.toFormattedJSON())
262} 294}
263 295
264async function listVideoChannelPlaylists (req: express.Request, res: express.Response) { 296async function listVideoChannelPlaylists (req: express.Request, res: express.Response) {
diff --git a/server/controllers/api/videos/ownership.ts b/server/controllers/api/videos/ownership.ts
index 86adb6c69..a85d7c30b 100644
--- a/server/controllers/api/videos/ownership.ts
+++ b/server/controllers/api/videos/ownership.ts
@@ -107,7 +107,7 @@ async function acceptOwnership (req: express.Request, res: express.Response) {
107 // We need more attributes for federation 107 // We need more attributes for federation
108 const targetVideo = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoChangeOwnership.Video.id) 108 const targetVideo = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoChangeOwnership.Video.id)
109 109
110 const oldVideoChannel = await VideoChannelModel.loadByIdAndPopulateAccount(targetVideo.channelId) 110 const oldVideoChannel = await VideoChannelModel.loadAndPopulateAccount(targetVideo.channelId)
111 111
112 targetVideo.channelId = channel.id 112 targetVideo.channelId = channel.id
113 113
diff --git a/server/controllers/lazy-static.ts b/server/controllers/lazy-static.ts
index 68b5c9eec..6f71fdb16 100644
--- a/server/controllers/lazy-static.ts
+++ b/server/controllers/lazy-static.ts
@@ -64,7 +64,7 @@ async function getActorImage (req: express.Request, res: express.Response) {
64 logger.info('Lazy serve remote actor image %s.', image.fileUrl) 64 logger.info('Lazy serve remote actor image %s.', image.fileUrl)
65 65
66 try { 66 try {
67 await pushActorImageProcessInQueue({ filename: image.filename, fileUrl: image.fileUrl }) 67 await pushActorImageProcessInQueue({ filename: image.filename, fileUrl: image.fileUrl, type: image.type })
68 } catch (err) { 68 } catch (err) {
69 logger.warn('Cannot process remote actor image %s.', image.fileUrl, { err }) 69 logger.warn('Cannot process remote actor image %s.', image.fileUrl, { err })
70 return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 70 return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
diff --git a/server/controllers/static.ts b/server/controllers/static.ts
index 4baa31117..e6a0628e6 100644
--- a/server/controllers/static.ts
+++ b/server/controllers/static.ts
@@ -252,9 +252,9 @@ async function generateNodeinfo (req: express.Request, res: express.Response) {
252 avatar: { 252 avatar: {
253 file: { 253 file: {
254 size: { 254 size: {
255 max: CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max 255 max: CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max
256 }, 256 },
257 extensions: CONSTRAINTS_FIELDS.ACTORS.AVATAR.EXTNAME 257 extensions: CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME
258 } 258 }
259 }, 259 },
260 video: { 260 video: {
diff --git a/server/helpers/custom-validators/users.ts b/server/helpers/custom-validators/users.ts
index d6e91ad35..85f3634c8 100644
--- a/server/helpers/custom-validators/users.ts
+++ b/server/helpers/custom-validators/users.ts
@@ -1,9 +1,9 @@
1import { values } from 'lodash'
1import validator from 'validator' 2import validator from 'validator'
2import { UserRole } from '../../../shared' 3import { UserRole } from '../../../shared'
4import { isEmailEnabled } from '../../initializers/config'
3import { CONSTRAINTS_FIELDS, NSFW_POLICY_TYPES } from '../../initializers/constants' 5import { CONSTRAINTS_FIELDS, NSFW_POLICY_TYPES } from '../../initializers/constants'
4import { exists, isArray, isBooleanValid, isFileValid } from './misc' 6import { exists, isArray, isBooleanValid, isFileValid } from './misc'
5import { values } from 'lodash'
6import { isEmailEnabled } from '../../initializers/config'
7 7
8const USERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USERS 8const USERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USERS
9 9
@@ -97,12 +97,12 @@ function isUserRoleValid (value: any) {
97 return exists(value) && validator.isInt('' + value) && UserRole[value] !== undefined 97 return exists(value) && validator.isInt('' + value) && UserRole[value] !== undefined
98} 98}
99 99
100const avatarMimeTypes = CONSTRAINTS_FIELDS.ACTORS.AVATAR.EXTNAME 100const avatarMimeTypes = CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME
101 .map(v => v.replace('.', '')) 101 .map(v => v.replace('.', ''))
102 .join('|') 102 .join('|')
103const avatarMimeTypesRegex = `image/(${avatarMimeTypes})` 103const avatarMimeTypesRegex = `image/(${avatarMimeTypes})`
104function isAvatarFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) { 104function isAvatarFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) {
105 return isFileValid(files, avatarMimeTypesRegex, 'avatarfile', CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max) 105 return isFileValid(files, avatarMimeTypesRegex, 'avatarfile', CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max)
106} 106}
107 107
108// --------------------------------------------------------------------------- 108// ---------------------------------------------------------------------------
diff --git a/server/helpers/middlewares/video-channels.ts b/server/helpers/middlewares/video-channels.ts
index 05499bb74..e6eab65a2 100644
--- a/server/helpers/middlewares/video-channels.ts
+++ b/server/helpers/middlewares/video-channels.ts
@@ -1,7 +1,7 @@
1import * as express from 'express' 1import * as express from 'express'
2import { VideoChannelModel } from '../../models/video/video-channel' 2import { MChannelBannerAccountDefault } from '@server/types/models'
3import { MChannelAccountDefault } from '@server/types/models'
4import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' 3import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
4import { VideoChannelModel } from '../../models/video/video-channel'
5 5
6async function doesLocalVideoChannelNameExist (name: string, res: express.Response) { 6async function doesLocalVideoChannelNameExist (name: string, res: express.Response) {
7 const videoChannel = await VideoChannelModel.loadLocalByNameAndPopulateAccount(name) 7 const videoChannel = await VideoChannelModel.loadLocalByNameAndPopulateAccount(name)
@@ -29,11 +29,10 @@ export {
29 doesVideoChannelNameWithHostExist 29 doesVideoChannelNameWithHostExist
30} 30}
31 31
32function processVideoChannelExist (videoChannel: MChannelAccountDefault, res: express.Response) { 32function processVideoChannelExist (videoChannel: MChannelBannerAccountDefault, res: express.Response) {
33 if (!videoChannel) { 33 if (!videoChannel) {
34 res.status(HttpStatusCode.NOT_FOUND_404) 34 res.status(HttpStatusCode.NOT_FOUND_404)
35 .json({ error: 'Video channel not found' }) 35 .json({ error: 'Video channel not found' })
36 .end()
37 36
38 return false 37 return false
39 } 38 }
diff --git a/server/helpers/middlewares/videos.ts b/server/helpers/middlewares/videos.ts
index c5eb0607a..403cae092 100644
--- a/server/helpers/middlewares/videos.ts
+++ b/server/helpers/middlewares/videos.ts
@@ -66,25 +66,24 @@ async function doesVideoFileOfVideoExist (id: number, videoIdOrUUID: number | st
66} 66}
67 67
68async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAccountId, res: Response) { 68async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAccountId, res: Response) {
69 if (user.hasRight(UserRight.UPDATE_ANY_VIDEO) === true) { 69 const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId)
70 const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId)
71 if (videoChannel === null) {
72 res.status(HttpStatusCode.BAD_REQUEST_400)
73 .json({ error: 'Unknown video `video channel` on this instance.' })
74 .end()
75 70
76 return false 71 if (videoChannel === null) {
77 } 72 res.status(HttpStatusCode.BAD_REQUEST_400)
73 .json({ error: 'Unknown video "video channel" for this instance.' })
78 74
75 return false
76 }
77
78 // Don't check account id if the user can update any video
79 if (user.hasRight(UserRight.UPDATE_ANY_VIDEO) === true) {
79 res.locals.videoChannel = videoChannel 80 res.locals.videoChannel = videoChannel
80 return true 81 return true
81 } 82 }
82 83
83 const videoChannel = await VideoChannelModel.loadByIdAndAccount(channelId, user.Account.id) 84 if (videoChannel.Account.id !== user.Account.id) {
84 if (videoChannel === null) {
85 res.status(HttpStatusCode.BAD_REQUEST_400) 85 res.status(HttpStatusCode.BAD_REQUEST_400)
86 .json({ error: 'Unknown video `video channel` for this account.' }) 86 .json({ error: 'Unknown video "video channel" for this account.' })
87 .end()
88 87
89 return false 88 return false
90 } 89 }
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 3f934688b..1e74f3eab 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -305,7 +305,7 @@ const CONSTRAINTS_FIELDS = {
305 PUBLIC_KEY: { min: 10, max: 5000 }, // Length 305 PUBLIC_KEY: { min: 10, max: 5000 }, // Length
306 PRIVATE_KEY: { min: 10, max: 5000 }, // Length 306 PRIVATE_KEY: { min: 10, max: 5000 }, // Length
307 URL: { min: 3, max: 2000 }, // Length 307 URL: { min: 3, max: 2000 }, // Length
308 AVATAR: { 308 IMAGE: {
309 EXTNAME: [ '.png', '.jpeg', '.jpg', '.gif', '.webp' ], 309 EXTNAME: [ '.png', '.jpeg', '.jpg', '.gif', '.webp' ],
310 FILE_SIZE: { 310 FILE_SIZE: {
311 max: 2 * 1024 * 1024 // 2MB 311 max: 2 * 1024 * 1024 // 2MB
@@ -466,6 +466,8 @@ const MIMETYPES = {
466 IMAGE: { 466 IMAGE: {
467 MIMETYPE_EXT: { 467 MIMETYPE_EXT: {
468 'image/png': '.png', 468 'image/png': '.png',
469 'image/gif': '.gif',
470 'image/webp': '.webp',
469 'image/jpg': '.jpg', 471 'image/jpg': '.jpg',
470 'image/jpeg': '.jpg' 472 'image/jpeg': '.jpg'
471 }, 473 },
@@ -605,9 +607,15 @@ const PREVIEWS_SIZE = {
605 height: 480, 607 height: 480,
606 minWidth: 400 608 minWidth: 400
607} 609}
608const AVATARS_SIZE = { 610const ACTOR_IMAGES_SIZE = {
609 width: 120, 611 AVATARS: {
610 height: 120 612 width: 120,
613 height: 120
614 },
615 BANNERS: {
616 width: 1920,
617 height: 384
618 }
611} 619}
612 620
613const EMBED_SIZE = { 621const EMBED_SIZE = {
@@ -755,7 +763,7 @@ if (isTestInstance() === true) {
755 ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL = 10 * 1000 // 10 seconds 763 ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL = 10 * 1000 // 10 seconds
756 ACTIVITY_PUB.VIDEO_PLAYLIST_REFRESH_INTERVAL = 10 * 1000 // 10 seconds 764 ACTIVITY_PUB.VIDEO_PLAYLIST_REFRESH_INTERVAL = 10 * 1000 // 10 seconds
757 765
758 CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max = 100 * 1024 // 100KB 766 CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max = 100 * 1024 // 100KB
759 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max = 400 * 1024 // 400KB 767 CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max = 400 * 1024 // 400KB
760 768
761 SCHEDULER_INTERVALS_MS.actorFollowScores = 1000 769 SCHEDULER_INTERVALS_MS.actorFollowScores = 1000
@@ -816,7 +824,7 @@ export {
816 SEARCH_INDEX, 824 SEARCH_INDEX,
817 HLS_REDUNDANCY_DIRECTORY, 825 HLS_REDUNDANCY_DIRECTORY,
818 P2P_MEDIA_LOADER_PEER_VERSION, 826 P2P_MEDIA_LOADER_PEER_VERSION,
819 AVATARS_SIZE, 827 ACTOR_IMAGES_SIZE,
820 ACCEPT_HEADERS, 828 ACCEPT_HEADERS,
821 BCRYPT_SALT_SIZE, 829 BCRYPT_SALT_SIZE,
822 TRACKER_RATE_LIMITS, 830 TRACKER_RATE_LIMITS,
diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts
index da831dcfd..fe4796a3d 100644
--- a/server/lib/activitypub/actor.ts
+++ b/server/lib/activitypub/actor.ts
@@ -4,6 +4,7 @@ import { Op, Transaction } from 'sequelize'
4import { URL } from 'url' 4import { URL } from 'url'
5import { v4 as uuidv4 } from 'uuid' 5import { v4 as uuidv4 } from 'uuid'
6import { getServerActor } from '@server/models/application/application' 6import { getServerActor } from '@server/models/application/application'
7import { ActorImageType } from '@shared/models'
7import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' 8import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
8import { ActivityPubActor, ActivityPubActorType, ActivityPubOrderedCollection } from '../../../shared/models/activitypub' 9import { ActivityPubActor, ActivityPubActorType, ActivityPubOrderedCollection } from '../../../shared/models/activitypub'
9import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' 10import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
@@ -30,10 +31,10 @@ import {
30 MActorAccountChannelId, 31 MActorAccountChannelId,
31 MActorAccountChannelIdActor, 32 MActorAccountChannelIdActor,
32 MActorAccountId, 33 MActorAccountId,
33 MActorDefault,
34 MActorFull, 34 MActorFull,
35 MActorFullActor, 35 MActorFullActor,
36 MActorId, 36 MActorId,
37 MActorImages,
37 MChannel 38 MChannel
38} from '../../types/models' 39} from '../../types/models'
39import { JobQueue } from '../job-queue' 40import { JobQueue } from '../job-queue'
@@ -168,43 +169,60 @@ async function updateActorInstance (actorInstance: ActorModel, attributes: Activ
168 } 169 }
169} 170}
170 171
171type AvatarInfo = { name: string, onDisk: boolean, fileUrl: string } 172type AvatarInfo = { name: string, onDisk: boolean, fileUrl: string, type: ActorImageType }
172async function updateActorAvatarInstance (actor: MActorDefault, info: AvatarInfo, t: Transaction) { 173async function updateActorImageInstance (actor: MActorImages, info: AvatarInfo, t: Transaction) {
173 if (!info.name) return actor 174 if (!info.name) return actor
174 175
175 if (actor.Avatar) { 176 const oldImageModel = info.type === ActorImageType.AVATAR
177 ? actor.Avatar
178 : actor.Banner
179
180 if (oldImageModel) {
176 // Don't update the avatar if the file URL did not change 181 // Don't update the avatar if the file URL did not change
177 if (info.fileUrl && actor.Avatar.fileUrl === info.fileUrl) return actor 182 if (info.fileUrl && oldImageModel.fileUrl === info.fileUrl) return actor
178 183
179 try { 184 try {
180 await actor.Avatar.destroy({ transaction: t }) 185 await oldImageModel.destroy({ transaction: t })
181 } catch (err) { 186 } catch (err) {
182 logger.error('Cannot remove old avatar of actor %s.', actor.url, { err }) 187 logger.error('Cannot remove old actor image of actor %s.', actor.url, { err })
183 } 188 }
184 } 189 }
185 190
186 const avatar = await ActorImageModel.create({ 191 const imageModel = await ActorImageModel.create({
187 filename: info.name, 192 filename: info.name,
188 onDisk: info.onDisk, 193 onDisk: info.onDisk,
189 fileUrl: info.fileUrl 194 fileUrl: info.fileUrl,
195 type: info.type
190 }, { transaction: t }) 196 }, { transaction: t })
191 197
192 actor.avatarId = avatar.id 198 if (info.type === ActorImageType.AVATAR) {
193 actor.Avatar = avatar 199 actor.avatarId = imageModel.id
200 actor.Avatar = imageModel
201 } else {
202 actor.bannerId = imageModel.id
203 actor.Banner = imageModel
204 }
194 205
195 return actor 206 return actor
196} 207}
197 208
198async function deleteActorAvatarInstance (actor: MActorDefault, t: Transaction) { 209async function deleteActorImageInstance (actor: MActorImages, type: ActorImageType, t: Transaction) {
199 try { 210 try {
200 await actor.Avatar.destroy({ transaction: t }) 211 if (type === ActorImageType.AVATAR) {
212 await actor.Avatar.destroy({ transaction: t })
213
214 actor.avatarId = null
215 actor.Avatar = null
216 } else {
217 await actor.Banner.destroy({ transaction: t })
218
219 actor.bannerId = null
220 actor.Banner = null
221 }
201 } catch (err) { 222 } catch (err) {
202 logger.error('Cannot remove old avatar of actor %s.', actor.url, { err }) 223 logger.error('Cannot remove old image of actor %s.', actor.url, { err })
203 } 224 }
204 225
205 actor.avatarId = null
206 actor.Avatar = null
207
208 return actor 226 return actor
209} 227}
210 228
@@ -219,9 +237,11 @@ async function fetchActorTotalItems (url: string) {
219 } 237 }
220} 238}
221 239
222function getAvatarInfoIfExists (actorJSON: ActivityPubActor) { 240function getImageInfoIfExists (actorJSON: ActivityPubActor, type: ActorImageType) {
223 const mimetypes = MIMETYPES.IMAGE 241 const mimetypes = MIMETYPES.IMAGE
224 const icon = actorJSON.icon 242 const icon = type === ActorImageType.AVATAR
243 ? actorJSON.icon
244 : actorJSON.image
225 245
226 if (!icon || icon.type !== 'Image' || !isActivityPubUrlValid(icon.url)) return undefined 246 if (!icon || icon.type !== 'Image' || !isActivityPubUrlValid(icon.url)) return undefined
227 247
@@ -239,7 +259,8 @@ function getAvatarInfoIfExists (actorJSON: ActivityPubActor) {
239 259
240 return { 260 return {
241 name: uuidv4() + extension, 261 name: uuidv4() + extension,
242 fileUrl: icon.url 262 fileUrl: icon.url,
263 type
243 } 264 }
244} 265}
245 266
@@ -293,10 +314,22 @@ async function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannel
293 const avatarInfo = { 314 const avatarInfo = {
294 name: result.avatar.name, 315 name: result.avatar.name,
295 fileUrl: result.avatar.fileUrl, 316 fileUrl: result.avatar.fileUrl,
296 onDisk: false 317 onDisk: false,
318 type: ActorImageType.AVATAR
319 }
320
321 await updateActorImageInstance(actor, avatarInfo, t)
322 }
323
324 if (result.banner !== undefined) {
325 const bannerInfo = {
326 name: result.banner.name,
327 fileUrl: result.banner.fileUrl,
328 onDisk: false,
329 type: ActorImageType.BANNER
297 } 330 }
298 331
299 await updateActorAvatarInstance(actor, avatarInfo, t) 332 await updateActorImageInstance(actor, bannerInfo, t)
300 } 333 }
301 334
302 // Force update 335 // Force update
@@ -338,11 +371,11 @@ export {
338 buildActorInstance, 371 buildActorInstance,
339 generateAndSaveActorKeys, 372 generateAndSaveActorKeys,
340 fetchActorTotalItems, 373 fetchActorTotalItems,
341 getAvatarInfoIfExists, 374 getImageInfoIfExists,
342 updateActorInstance, 375 updateActorInstance,
343 deleteActorAvatarInstance, 376 deleteActorImageInstance,
344 refreshActorIfNeeded, 377 refreshActorIfNeeded,
345 updateActorAvatarInstance, 378 updateActorImageInstance,
346 addFetchOutboxJob 379 addFetchOutboxJob
347} 380}
348 381
@@ -381,12 +414,25 @@ function saveActorAndServerAndModelIfNotExist (
381 const avatar = await ActorImageModel.create({ 414 const avatar = await ActorImageModel.create({
382 filename: result.avatar.name, 415 filename: result.avatar.name,
383 fileUrl: result.avatar.fileUrl, 416 fileUrl: result.avatar.fileUrl,
384 onDisk: false 417 onDisk: false,
418 type: ActorImageType.AVATAR
385 }, { transaction: t }) 419 }, { transaction: t })
386 420
387 actor.avatarId = avatar.id 421 actor.avatarId = avatar.id
388 } 422 }
389 423
424 // Banner?
425 if (result.banner) {
426 const banner = await ActorImageModel.create({
427 filename: result.banner.name,
428 fileUrl: result.banner.fileUrl,
429 onDisk: false,
430 type: ActorImageType.BANNER
431 }, { transaction: t })
432
433 actor.bannerId = banner.id
434 }
435
390 // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists 436 // Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists
391 // (which could be false in a retried query) 437 // (which could be false in a retried query)
392 const [ actorCreated, created ] = await ActorModel.findOrCreate<MActorFullActor>({ 438 const [ actorCreated, created ] = await ActorModel.findOrCreate<MActorFullActor>({
@@ -440,6 +486,10 @@ type FetchRemoteActorResult = {
440 name: string 486 name: string
441 fileUrl: string 487 fileUrl: string
442 } 488 }
489 banner?: {
490 name: string
491 fileUrl: string
492 }
443 attributedTo: ActivityPubAttributedTo[] 493 attributedTo: ActivityPubAttributedTo[]
444} 494}
445async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: number, result: FetchRemoteActorResult }> { 495async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: number, result: FetchRemoteActorResult }> {
@@ -479,7 +529,8 @@ async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: numbe
479 : null 529 : null
480 }) 530 })
481 531
482 const avatarInfo = await getAvatarInfoIfExists(actorJSON) 532 const avatarInfo = getImageInfoIfExists(actorJSON, ActorImageType.AVATAR)
533 const bannerInfo = getImageInfoIfExists(actorJSON, ActorImageType.BANNER)
483 534
484 const name = actorJSON.name || actorJSON.preferredUsername 535 const name = actorJSON.name || actorJSON.preferredUsername
485 return { 536 return {
@@ -488,6 +539,7 @@ async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: numbe
488 actor, 539 actor,
489 name, 540 name,
490 avatar: avatarInfo, 541 avatar: avatarInfo,
542 banner: bannerInfo,
491 summary: actorJSON.summary, 543 summary: actorJSON.summary,
492 support: actorJSON.support, 544 support: actorJSON.support,
493 playlists: actorJSON.playlists, 545 playlists: actorJSON.playlists,
diff --git a/server/lib/activitypub/process/process-delete.ts b/server/lib/activitypub/process/process-delete.ts
index a86def936..070ee0f1d 100644
--- a/server/lib/activitypub/process/process-delete.ts
+++ b/server/lib/activitypub/process/process-delete.ts
@@ -7,7 +7,7 @@ import { VideoModel } from '../../../models/video/video'
7import { VideoCommentModel } from '../../../models/video/video-comment' 7import { VideoCommentModel } from '../../../models/video/video-comment'
8import { VideoPlaylistModel } from '../../../models/video/video-playlist' 8import { VideoPlaylistModel } from '../../../models/video/video-playlist'
9import { APProcessorOptions } from '../../../types/activitypub-processor.model' 9import { APProcessorOptions } from '../../../types/activitypub-processor.model'
10import { MAccountActor, MActor, MActorSignature, MChannelActor, MChannelActorAccountActor, MCommentOwnerVideo } from '../../../types/models' 10import { MAccountActor, MActor, MActorSignature, MChannelActor, MCommentOwnerVideo } from '../../../types/models'
11import { markCommentAsDeleted } from '../../video-comment' 11import { markCommentAsDeleted } from '../../video-comment'
12import { forwardVideoRelatedActivity } from '../send/utils' 12import { forwardVideoRelatedActivity } from '../send/utils'
13 13
@@ -30,9 +30,7 @@ async function processDeleteActivity (options: APProcessorOptions<ActivityDelete
30 } else if (byActorFull.type === 'Group') { 30 } else if (byActorFull.type === 'Group') {
31 if (!byActorFull.VideoChannel) throw new Error('Actor ' + byActorFull.url + ' is a group but we cannot find it in database.') 31 if (!byActorFull.VideoChannel) throw new Error('Actor ' + byActorFull.url + ' is a group but we cannot find it in database.')
32 32
33 const channelToDelete = byActorFull.VideoChannel as MChannelActorAccountActor 33 const channelToDelete = Object.assign({}, byActorFull.VideoChannel, { Actor: byActorFull })
34 channelToDelete.Actor = byActorFull
35
36 return retryTransactionWrapper(processDeleteVideoChannel, channelToDelete) 34 return retryTransactionWrapper(processDeleteVideoChannel, channelToDelete)
37 } 35 }
38 } 36 }
diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts
index 849f70b94..ad3bb392d 100644
--- a/server/lib/activitypub/process/process-update.ts
+++ b/server/lib/activitypub/process/process-update.ts
@@ -6,7 +6,7 @@ import { sequelizeTypescript } from '../../../initializers/database'
6import { AccountModel } from '../../../models/account/account' 6import { AccountModel } from '../../../models/account/account'
7import { ActorModel } from '../../../models/activitypub/actor' 7import { ActorModel } from '../../../models/activitypub/actor'
8import { VideoChannelModel } from '../../../models/video/video-channel' 8import { VideoChannelModel } from '../../../models/video/video-channel'
9import { getAvatarInfoIfExists, updateActorAvatarInstance, updateActorInstance } from '../actor' 9import { getImageInfoIfExists, updateActorImageInstance, updateActorInstance } from '../actor'
10import { getOrCreateVideoAndAccountAndChannel, getOrCreateVideoChannelFromVideoObject, updateVideoFromAP } from '../videos' 10import { getOrCreateVideoAndAccountAndChannel, getOrCreateVideoChannelFromVideoObject, updateVideoFromAP } from '../videos'
11import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos' 11import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos'
12import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file' 12import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file'
@@ -17,6 +17,7 @@ import { createOrUpdateVideoPlaylist } from '../playlist'
17import { APProcessorOptions } from '../../../types/activitypub-processor.model' 17import { APProcessorOptions } from '../../../types/activitypub-processor.model'
18import { MActorSignature, MAccountIdActor } from '../../../types/models' 18import { MActorSignature, MAccountIdActor } from '../../../types/models'
19import { isRedundancyAccepted } from '@server/lib/redundancy' 19import { isRedundancyAccepted } from '@server/lib/redundancy'
20import { ActorImageType } from '@shared/models'
20 21
21async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) { 22async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) {
22 const { activity, byActor } = options 23 const { activity, byActor } = options
@@ -119,7 +120,8 @@ async function processUpdateActor (actor: ActorModel, activity: ActivityUpdate)
119 let accountOrChannelFieldsSave: object 120 let accountOrChannelFieldsSave: object
120 121
121 // Fetch icon? 122 // Fetch icon?
122 const avatarInfo = await getAvatarInfoIfExists(actorAttributesToUpdate) 123 const avatarInfo = getImageInfoIfExists(actorAttributesToUpdate, ActorImageType.AVATAR)
124 const bannerInfo = getImageInfoIfExists(actorAttributesToUpdate, ActorImageType.BANNER)
123 125
124 try { 126 try {
125 await sequelizeTypescript.transaction(async t => { 127 await sequelizeTypescript.transaction(async t => {
@@ -132,10 +134,12 @@ async function processUpdateActor (actor: ActorModel, activity: ActivityUpdate)
132 134
133 await updateActorInstance(actor, actorAttributesToUpdate) 135 await updateActorInstance(actor, actorAttributesToUpdate)
134 136
135 if (avatarInfo !== undefined) { 137 for (const imageInfo of [ avatarInfo, bannerInfo ]) {
136 const avatarOptions = Object.assign({}, avatarInfo, { onDisk: false }) 138 if (!imageInfo) continue
137 139
138 await updateActorAvatarInstance(actor, avatarOptions, t) 140 const imageOptions = Object.assign({}, imageInfo, { onDisk: false })
141
142 await updateActorImageInstance(actor, imageOptions, t)
139 } 143 }
140 144
141 await actor.save({ transaction: t }) 145 await actor.save({ transaction: t })
diff --git a/server/lib/actor-image.ts b/server/lib/actor-image.ts
index ca7f9658d..59afa93bd 100644
--- a/server/lib/actor-image.ts
+++ b/server/lib/actor-image.ts
@@ -3,50 +3,57 @@ import { queue } from 'async'
3import * as LRUCache from 'lru-cache' 3import * as LRUCache from 'lru-cache'
4import { extname, join } from 'path' 4import { extname, join } from 'path'
5import { v4 as uuidv4 } from 'uuid' 5import { v4 as uuidv4 } from 'uuid'
6import { ActorImageType } from '@shared/models'
6import { retryTransactionWrapper } from '../helpers/database-utils' 7import { retryTransactionWrapper } from '../helpers/database-utils'
7import { processImage } from '../helpers/image-utils' 8import { processImage } from '../helpers/image-utils'
8import { downloadImage } from '../helpers/requests' 9import { downloadImage } from '../helpers/requests'
9import { CONFIG } from '../initializers/config' 10import { CONFIG } from '../initializers/config'
10import { AVATARS_SIZE, LRU_CACHE, QUEUE_CONCURRENCY } from '../initializers/constants' 11import { ACTOR_IMAGES_SIZE, LRU_CACHE, QUEUE_CONCURRENCY } from '../initializers/constants'
11import { sequelizeTypescript } from '../initializers/database' 12import { sequelizeTypescript } from '../initializers/database'
12import { MAccountDefault, MChannelDefault } from '../types/models' 13import { MAccountDefault, MChannelDefault } from '../types/models'
13import { deleteActorAvatarInstance, updateActorAvatarInstance } from './activitypub/actor' 14import { deleteActorImageInstance, updateActorImageInstance } from './activitypub/actor'
14import { sendUpdateActor } from './activitypub/send' 15import { sendUpdateActor } from './activitypub/send'
15 16
16async function updateLocalActorAvatarFile ( 17async function updateLocalActorImageFile (
17 accountOrChannel: MAccountDefault | MChannelDefault, 18 accountOrChannel: MAccountDefault | MChannelDefault,
18 avatarPhysicalFile: Express.Multer.File 19 imagePhysicalFile: Express.Multer.File,
20 type: ActorImageType
19) { 21) {
20 const extension = extname(avatarPhysicalFile.filename) 22 const imageSize = type === ActorImageType.AVATAR
23 ? ACTOR_IMAGES_SIZE.AVATARS
24 : ACTOR_IMAGES_SIZE.BANNERS
21 25
22 const avatarName = uuidv4() + extension 26 const extension = extname(imagePhysicalFile.filename)
23 const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, avatarName) 27
24 await processImage(avatarPhysicalFile.path, destination, AVATARS_SIZE) 28 const imageName = uuidv4() + extension
29 const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, imageName)
30 await processImage(imagePhysicalFile.path, destination, imageSize)
25 31
26 return retryTransactionWrapper(() => { 32 return retryTransactionWrapper(() => {
27 return sequelizeTypescript.transaction(async t => { 33 return sequelizeTypescript.transaction(async t => {
28 const avatarInfo = { 34 const actorImageInfo = {
29 name: avatarName, 35 name: imageName,
30 fileUrl: null, 36 fileUrl: null,
37 type,
31 onDisk: true 38 onDisk: true
32 } 39 }
33 40
34 const updatedActor = await updateActorAvatarInstance(accountOrChannel.Actor, avatarInfo, t) 41 const updatedActor = await updateActorImageInstance(accountOrChannel.Actor, actorImageInfo, t)
35 await updatedActor.save({ transaction: t }) 42 await updatedActor.save({ transaction: t })
36 43
37 await sendUpdateActor(accountOrChannel, t) 44 await sendUpdateActor(accountOrChannel, t)
38 45
39 return updatedActor.Avatar 46 return type === ActorImageType.AVATAR
47 ? updatedActor.Avatar
48 : updatedActor.Banner
40 }) 49 })
41 }) 50 })
42} 51}
43 52
44async function deleteLocalActorAvatarFile ( 53async function deleteLocalActorImageFile (accountOrChannel: MAccountDefault | MChannelDefault, type: ActorImageType) {
45 accountOrChannel: MAccountDefault | MChannelDefault
46) {
47 return retryTransactionWrapper(() => { 54 return retryTransactionWrapper(() => {
48 return sequelizeTypescript.transaction(async t => { 55 return sequelizeTypescript.transaction(async t => {
49 const updatedActor = await deleteActorAvatarInstance(accountOrChannel.Actor, t) 56 const updatedActor = await deleteActorImageInstance(accountOrChannel.Actor, type, t)
50 await updatedActor.save({ transaction: t }) 57 await updatedActor.save({ transaction: t })
51 58
52 await sendUpdateActor(accountOrChannel, t) 59 await sendUpdateActor(accountOrChannel, t)
@@ -56,10 +63,14 @@ async function deleteLocalActorAvatarFile (
56 }) 63 })
57} 64}
58 65
59type DownloadImageQueueTask = { fileUrl: string, filename: string } 66type DownloadImageQueueTask = { fileUrl: string, filename: string, type: ActorImageType }
60 67
61const downloadImageQueue = queue<DownloadImageQueueTask, Error>((task, cb) => { 68const downloadImageQueue = queue<DownloadImageQueueTask, Error>((task, cb) => {
62 downloadImage(task.fileUrl, CONFIG.STORAGE.ACTOR_IMAGES, task.filename, AVATARS_SIZE) 69 const size = task.type === ActorImageType.AVATAR
70 ? ACTOR_IMAGES_SIZE.AVATARS
71 : ACTOR_IMAGES_SIZE.BANNERS
72
73 downloadImage(task.fileUrl, CONFIG.STORAGE.ACTOR_IMAGES, task.filename, size)
63 .then(() => cb()) 74 .then(() => cb())
64 .catch(err => cb(err)) 75 .catch(err => cb(err))
65}, QUEUE_CONCURRENCY.ACTOR_PROCESS_IMAGE) 76}, QUEUE_CONCURRENCY.ACTOR_PROCESS_IMAGE)
@@ -79,7 +90,7 @@ const actorImagePathUnsafeCache = new LRUCache<string, string>({ max: LRU_CACHE.
79 90
80export { 91export {
81 actorImagePathUnsafeCache, 92 actorImagePathUnsafeCache,
82 updateLocalActorAvatarFile, 93 updateLocalActorImageFile,
83 deleteLocalActorAvatarFile, 94 deleteLocalActorImageFile,
84 pushActorImageProcessInQueue 95 pushActorImageProcessInQueue
85} 96}
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts
index fcc11c7b2..6ddaa82c8 100644
--- a/server/lib/client-html.ts
+++ b/server/lib/client-html.ts
@@ -11,7 +11,7 @@ import { logger } from '../helpers/logger'
11import { CONFIG } from '../initializers/config' 11import { CONFIG } from '../initializers/config'
12import { 12import {
13 ACCEPT_HEADERS, 13 ACCEPT_HEADERS,
14 AVATARS_SIZE, 14 ACTOR_IMAGES_SIZE,
15 CUSTOM_HTML_TAG_COMMENTS, 15 CUSTOM_HTML_TAG_COMMENTS,
16 EMBED_SIZE, 16 EMBED_SIZE,
17 FILES_CONTENT_HASH, 17 FILES_CONTENT_HASH,
@@ -246,8 +246,8 @@ class ClientHtml {
246 246
247 const image = { 247 const image = {
248 url: entity.Actor.getAvatarUrl(), 248 url: entity.Actor.getAvatarUrl(),
249 width: AVATARS_SIZE.width, 249 width: ACTOR_IMAGES_SIZE.AVATARS.width,
250 height: AVATARS_SIZE.height 250 height: ACTOR_IMAGES_SIZE.AVATARS.height
251 } 251 }
252 252
253 const ogType = 'website' 253 const ogType = 'website'
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts
index ce4134d59..9ca0d5d5b 100644
--- a/server/lib/emailer.ts
+++ b/server/lib/emailer.ts
@@ -405,7 +405,7 @@ class Emailer {
405 async addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) { 405 async addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) {
406 const videoAutoBlacklistUrl = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list' 406 const videoAutoBlacklistUrl = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list'
407 const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath() 407 const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath()
408 const channel = (await VideoChannelModel.loadByIdAndPopulateAccount(videoBlacklist.Video.channelId)).toFormattedSummaryJSON() 408 const channel = (await VideoChannelModel.loadAndPopulateAccount(videoBlacklist.Video.channelId)).toFormattedSummaryJSON()
409 409
410 const emailPayload: EmailPayload = { 410 const emailPayload: EmailPayload = {
411 template: 'video-auto-blacklist-new', 411 template: 'video-auto-blacklist-new',
diff --git a/server/lib/video-channel.ts b/server/lib/video-channel.ts
index 49bdf4869..0476cb2d5 100644
--- a/server/lib/video-channel.ts
+++ b/server/lib/video-channel.ts
@@ -3,18 +3,12 @@ import { v4 as uuidv4 } from 'uuid'
3import { VideoChannelCreate } from '../../shared/models' 3import { VideoChannelCreate } from '../../shared/models'
4import { VideoModel } from '../models/video/video' 4import { VideoModel } from '../models/video/video'
5import { VideoChannelModel } from '../models/video/video-channel' 5import { VideoChannelModel } from '../models/video/video-channel'
6import { MAccountId, MChannelDefault, MChannelId } from '../types/models' 6import { MAccountId, MChannelId } from '../types/models'
7import { buildActorInstance } from './activitypub/actor' 7import { buildActorInstance } from './activitypub/actor'
8import { getLocalVideoChannelActivityPubUrl } from './activitypub/url' 8import { getLocalVideoChannelActivityPubUrl } from './activitypub/url'
9import { federateVideoIfNeeded } from './activitypub/videos' 9import { federateVideoIfNeeded } from './activitypub/videos'
10 10
11type CustomVideoChannelModelAccount <T extends MAccountId> = MChannelDefault & { Account?: T } 11async function createLocalVideoChannel (videoChannelInfo: VideoChannelCreate, account: MAccountId, t: Sequelize.Transaction) {
12
13async function createLocalVideoChannel <T extends MAccountId> (
14 videoChannelInfo: VideoChannelCreate,
15 account: T,
16 t: Sequelize.Transaction
17): Promise<CustomVideoChannelModelAccount<T>> {
18 const uuid = uuidv4() 12 const uuid = uuidv4()
19 const url = getLocalVideoChannelActivityPubUrl(videoChannelInfo.name) 13 const url = getLocalVideoChannelActivityPubUrl(videoChannelInfo.name)
20 const actorInstance = buildActorInstance('Group', url, videoChannelInfo.name, uuid) 14 const actorInstance = buildActorInstance('Group', url, videoChannelInfo.name, uuid)
@@ -32,13 +26,11 @@ async function createLocalVideoChannel <T extends MAccountId> (
32 const videoChannel = new VideoChannelModel(videoChannelData) 26 const videoChannel = new VideoChannelModel(videoChannelData)
33 27
34 const options = { transaction: t } 28 const options = { transaction: t }
35 const videoChannelCreated: CustomVideoChannelModelAccount<T> = await videoChannel.save(options) as MChannelDefault 29 const videoChannelCreated = await videoChannel.save(options)
36 30
37 // Do not forget to add Account/Actor information to the created video channel
38 videoChannelCreated.Account = account
39 videoChannelCreated.Actor = actorInstanceCreated 31 videoChannelCreated.Actor = actorInstanceCreated
40 32
41 // No need to seed this empty video channel to followers 33 // No need to send this empty video channel to followers
42 return videoChannelCreated 34 return videoChannelCreated
43} 35}
44 36
diff --git a/server/middlewares/validators/avatar.ts b/server/middlewares/validators/avatar.ts
index 2acb97483..f7eb367bd 100644
--- a/server/middlewares/validators/avatar.ts
+++ b/server/middlewares/validators/avatar.ts
@@ -6,21 +6,25 @@ import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
6import { logger } from '../../helpers/logger' 6import { logger } from '../../helpers/logger'
7import { cleanUpReqFiles } from '../../helpers/express-utils' 7import { cleanUpReqFiles } from '../../helpers/express-utils'
8 8
9const updateAvatarValidator = [ 9const updateActorImageValidatorFactory = (fieldname: string) => ([
10 body('avatarfile').custom((value, { req }) => isAvatarFile(req.files)).withMessage( 10 body(fieldname).custom((value, { req }) => isAvatarFile(req.files)).withMessage(
11 'This file is not supported or too large. Please, make sure it is of the following type : ' + 11 'This file is not supported or too large. Please, make sure it is of the following type : ' +
12 CONSTRAINTS_FIELDS.ACTORS.AVATAR.EXTNAME.join(', ') 12 CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME.join(', ')
13 ), 13 ),
14 14
15 (req: express.Request, res: express.Response, next: express.NextFunction) => { 15 (req: express.Request, res: express.Response, next: express.NextFunction) => {
16 logger.debug('Checking updateAvatarValidator parameters', { files: req.files }) 16 logger.debug('Checking updateActorImageValidator parameters', { files: req.files })
17 17
18 if (areValidationErrors(req, res)) return cleanUpReqFiles(req) 18 if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
19 19
20 return next() 20 return next()
21 } 21 }
22] 22])
23
24const updateAvatarValidator = updateActorImageValidatorFactory('avatarfile')
25const updateBannerValidator = updateActorImageValidatorFactory('bannerfile')
23 26
24export { 27export {
25 updateAvatarValidator 28 updateAvatarValidator,
29 updateBannerValidator
26} 30}
diff --git a/server/middlewares/validators/follows.ts b/server/middlewares/validators/follows.ts
index a590aca99..bb849dc72 100644
--- a/server/middlewares/validators/follows.ts
+++ b/server/middlewares/validators/follows.ts
@@ -68,7 +68,6 @@ const removeFollowingValidator = [
68 .json({ 68 .json({
69 error: `Following ${req.params.host} not found.` 69 error: `Following ${req.params.host} not found.`
70 }) 70 })
71 .end()
72 } 71 }
73 72
74 res.locals.follow = follow 73 res.locals.follow = follow
diff --git a/server/middlewares/validators/videos/video-channels.ts b/server/middlewares/validators/videos/video-channels.ts
index 57ac548b9..2463d281c 100644
--- a/server/middlewares/validators/videos/video-channels.ts
+++ b/server/middlewares/validators/videos/video-channels.ts
@@ -73,13 +73,11 @@ const videoChannelsUpdateValidator = [
73 if (res.locals.videoChannel.Actor.isOwned() === false) { 73 if (res.locals.videoChannel.Actor.isOwned() === false) {
74 return res.status(HttpStatusCode.FORBIDDEN_403) 74 return res.status(HttpStatusCode.FORBIDDEN_403)
75 .json({ error: 'Cannot update video channel of another server' }) 75 .json({ error: 'Cannot update video channel of another server' })
76 .end()
77 } 76 }
78 77
79 if (res.locals.videoChannel.Account.userId !== res.locals.oauth.token.User.id) { 78 if (res.locals.videoChannel.Account.userId !== res.locals.oauth.token.User.id) {
80 return res.status(HttpStatusCode.FORBIDDEN_403) 79 return res.status(HttpStatusCode.FORBIDDEN_403)
81 .json({ error: 'Cannot update video channel of another user' }) 80 .json({ error: 'Cannot update video channel of another user' })
82 .end()
83 } 81 }
84 82
85 return next() 83 return next()
diff --git a/server/models/activitypub/actor-follow.ts b/server/models/activitypub/actor-follow.ts
index ce6a4e267..4c5f37620 100644
--- a/server/models/activitypub/actor-follow.ts
+++ b/server/models/activitypub/actor-follow.ts
@@ -248,13 +248,6 @@ export class ActorFollowModel extends Model {
248 } 248 }
249 249
250 return ActorFollowModel.findOne(query) 250 return ActorFollowModel.findOne(query)
251 .then(result => {
252 if (result?.ActorFollowing.VideoChannel) {
253 result.ActorFollowing.VideoChannel.Actor = result.ActorFollowing
254 }
255
256 return result
257 })
258 } 251 }
259 252
260 static listSubscribedIn (actorId: number, targets: { name: string, host?: string }[]): Promise<MActorFollowFollowingHost[]> { 253 static listSubscribedIn (actorId: number, targets: { name: string, host?: string }[]): Promise<MActorFollowFollowingHost[]> {
diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts
index 09d96b24d..6595f11e2 100644
--- a/server/models/activitypub/actor.ts
+++ b/server/models/activitypub/actor.ts
@@ -29,11 +29,19 @@ import {
29 isActorPublicKeyValid 29 isActorPublicKeyValid
30} from '../../helpers/custom-validators/activitypub/actor' 30} from '../../helpers/custom-validators/activitypub/actor'
31import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 31import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
32import { ACTIVITY_PUB, ACTIVITY_PUB_ACTOR_TYPES, CONSTRAINTS_FIELDS, SERVER_ACTOR_NAME, WEBSERVER } from '../../initializers/constants' 32import {
33 ACTIVITY_PUB,
34 ACTIVITY_PUB_ACTOR_TYPES,
35 CONSTRAINTS_FIELDS,
36 MIMETYPES,
37 SERVER_ACTOR_NAME,
38 WEBSERVER
39} from '../../initializers/constants'
33import { 40import {
34 MActor, 41 MActor,
35 MActorAccountChannelId, 42 MActorAccountChannelId,
36 MActorAP, 43 MActorAPAccount,
44 MActorAPChannel,
37 MActorFormattable, 45 MActorFormattable,
38 MActorFull, 46 MActorFull,
39 MActorHost, 47 MActorHost,
@@ -104,6 +112,11 @@ export const unusedActorAttributesForAPI = [
104 model: ActorImageModel, 112 model: ActorImageModel,
105 as: 'Avatar', 113 as: 'Avatar',
106 required: false 114 required: false
115 },
116 {
117 model: ActorImageModel,
118 as: 'Banner',
119 required: false
107 } 120 }
108 ] 121 ]
109 } 122 }
@@ -531,29 +544,46 @@ export class ActorModel extends Model {
531 toFormattedJSON (this: MActorFormattable) { 544 toFormattedJSON (this: MActorFormattable) {
532 const base = this.toFormattedSummaryJSON() 545 const base = this.toFormattedSummaryJSON()
533 546
547 let banner: ActorImage = null
548 if (this.bannerId) {
549 banner = this.Banner.toFormattedJSON()
550 }
551
534 return Object.assign(base, { 552 return Object.assign(base, {
535 id: this.id, 553 id: this.id,
536 hostRedundancyAllowed: this.getRedundancyAllowed(), 554 hostRedundancyAllowed: this.getRedundancyAllowed(),
537 followingCount: this.followingCount, 555 followingCount: this.followingCount,
538 followersCount: this.followersCount, 556 followersCount: this.followersCount,
557 banner,
539 createdAt: this.createdAt, 558 createdAt: this.createdAt,
540 updatedAt: this.updatedAt 559 updatedAt: this.updatedAt
541 }) 560 })
542 } 561 }
543 562
544 toActivityPubObject (this: MActorAP, name: string) { 563 toActivityPubObject (this: MActorAPChannel | MActorAPAccount, name: string) {
545 let icon: ActivityIconObject 564 let icon: ActivityIconObject
565 let image: ActivityIconObject
546 566
547 if (this.avatarId) { 567 if (this.avatarId) {
548 const extension = extname(this.Avatar.filename) 568 const extension = extname(this.Avatar.filename)
549 569
550 icon = { 570 icon = {
551 type: 'Image', 571 type: 'Image',
552 mediaType: extension === '.png' ? 'image/png' : 'image/jpeg', 572 mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension],
553 url: this.getAvatarUrl() 573 url: this.getAvatarUrl()
554 } 574 }
555 } 575 }
556 576
577 if (this.bannerId) {
578 const extension = extname((this as MActorAPChannel).Banner.filename)
579
580 image = {
581 type: 'Image',
582 mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension],
583 url: this.getBannerUrl()
584 }
585 }
586
557 const json = { 587 const json = {
558 type: this.type, 588 type: this.type,
559 id: this.url, 589 id: this.url,
@@ -573,7 +603,8 @@ export class ActorModel extends Model {
573 owner: this.url, 603 owner: this.url,
574 publicKeyPem: this.publicKey 604 publicKeyPem: this.publicKey
575 }, 605 },
576 icon 606 icon,
607 image
577 } 608 }
578 609
579 return activityPubContextify(json) 610 return activityPubContextify(json)
@@ -643,6 +674,12 @@ export class ActorModel extends Model {
643 return WEBSERVER.URL + this.Avatar.getStaticPath() 674 return WEBSERVER.URL + this.Avatar.getStaticPath()
644 } 675 }
645 676
677 getBannerUrl () {
678 if (!this.bannerId) return undefined
679
680 return WEBSERVER.URL + this.Banner.getStaticPath()
681 }
682
646 isOutdated () { 683 isOutdated () {
647 if (this.isOwned()) return false 684 if (this.isOwned()) return false
648 685
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts
index 815fb16c0..74885edfb 100644
--- a/server/models/video/video-channel.ts
+++ b/server/models/video/video-channel.ts
@@ -28,10 +28,9 @@ import {
28import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' 28import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
29import { sendDeleteActor } from '../../lib/activitypub/send' 29import { sendDeleteActor } from '../../lib/activitypub/send'
30import { 30import {
31 MChannelAccountDefault,
32 MChannelActor, 31 MChannelActor,
33 MChannelActorAccountDefaultVideos,
34 MChannelAP, 32 MChannelAP,
33 MChannelBannerAccountDefault,
35 MChannelFormattable, 34 MChannelFormattable,
36 MChannelSummaryFormattable 35 MChannelSummaryFormattable
37} from '../../types/models/video' 36} from '../../types/models/video'
@@ -49,6 +48,7 @@ export enum ScopeNames {
49 SUMMARY = 'SUMMARY', 48 SUMMARY = 'SUMMARY',
50 WITH_ACCOUNT = 'WITH_ACCOUNT', 49 WITH_ACCOUNT = 'WITH_ACCOUNT',
51 WITH_ACTOR = 'WITH_ACTOR', 50 WITH_ACTOR = 'WITH_ACTOR',
51 WITH_ACTOR_BANNER = 'WITH_ACTOR_BANNER',
52 WITH_VIDEOS = 'WITH_VIDEOS', 52 WITH_VIDEOS = 'WITH_VIDEOS',
53 WITH_STATS = 'WITH_STATS' 53 WITH_STATS = 'WITH_STATS'
54} 54}
@@ -168,6 +168,20 @@ export type SummaryOptions = {
168 ActorModel 168 ActorModel
169 ] 169 ]
170 }, 170 },
171 [ScopeNames.WITH_ACTOR_BANNER]: {
172 include: [
173 {
174 model: ActorModel,
175 include: [
176 {
177 model: ActorImageModel,
178 required: false,
179 as: 'Banner'
180 }
181 ]
182 }
183 ]
184 },
171 [ScopeNames.WITH_VIDEOS]: { 185 [ScopeNames.WITH_VIDEOS]: {
172 include: [ 186 include: [
173 VideoModel 187 VideoModel
@@ -442,7 +456,7 @@ export class VideoChannelModel extends Model {
442 where 456 where
443 } 457 }
444 458
445 const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR ] 459 const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR_BANNER ]
446 460
447 if (options.withStats === true) { 461 if (options.withStats === true) {
448 scopes.push({ 462 scopes.push({
@@ -458,32 +472,13 @@ export class VideoChannelModel extends Model {
458 }) 472 })
459 } 473 }
460 474
461 static loadByIdAndPopulateAccount (id: number): Promise<MChannelAccountDefault> { 475 static loadAndPopulateAccount (id: number): Promise<MChannelBannerAccountDefault> {
462 return VideoChannelModel.unscoped()
463 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
464 .findByPk(id)
465 }
466
467 static loadByIdAndAccount (id: number, accountId: number): Promise<MChannelAccountDefault> {
468 const query = {
469 where: {
470 id,
471 accountId
472 }
473 }
474
475 return VideoChannelModel.unscoped()
476 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
477 .findOne(query)
478 }
479
480 static loadAndPopulateAccount (id: number): Promise<MChannelAccountDefault> {
481 return VideoChannelModel.unscoped() 476 return VideoChannelModel.unscoped()
482 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) 477 .scope([ ScopeNames.WITH_ACTOR_BANNER, ScopeNames.WITH_ACCOUNT ])
483 .findByPk(id) 478 .findByPk(id)
484 } 479 }
485 480
486 static loadByUrlAndPopulateAccount (url: string): Promise<MChannelAccountDefault> { 481 static loadByUrlAndPopulateAccount (url: string): Promise<MChannelBannerAccountDefault> {
487 const query = { 482 const query = {
488 include: [ 483 include: [
489 { 484 {
@@ -491,7 +486,14 @@ export class VideoChannelModel extends Model {
491 required: true, 486 required: true,
492 where: { 487 where: {
493 url 488 url
494 } 489 },
490 include: [
491 {
492 model: ActorImageModel,
493 required: false,
494 as: 'Banner'
495 }
496 ]
495 } 497 }
496 ] 498 ]
497 } 499 }
@@ -509,7 +511,7 @@ export class VideoChannelModel extends Model {
509 return VideoChannelModel.loadByNameAndHostAndPopulateAccount(name, host) 511 return VideoChannelModel.loadByNameAndHostAndPopulateAccount(name, host)
510 } 512 }
511 513
512 static loadLocalByNameAndPopulateAccount (name: string): Promise<MChannelAccountDefault> { 514 static loadLocalByNameAndPopulateAccount (name: string): Promise<MChannelBannerAccountDefault> {
513 const query = { 515 const query = {
514 include: [ 516 include: [
515 { 517 {
@@ -518,17 +520,24 @@ export class VideoChannelModel extends Model {
518 where: { 520 where: {
519 preferredUsername: name, 521 preferredUsername: name,
520 serverId: null 522 serverId: null
521 } 523 },
524 include: [
525 {
526 model: ActorImageModel,
527 required: false,
528 as: 'Banner'
529 }
530 ]
522 } 531 }
523 ] 532 ]
524 } 533 }
525 534
526 return VideoChannelModel.unscoped() 535 return VideoChannelModel.unscoped()
527 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) 536 .scope([ ScopeNames.WITH_ACCOUNT ])
528 .findOne(query) 537 .findOne(query)
529 } 538 }
530 539
531 static loadByNameAndHostAndPopulateAccount (name: string, host: string): Promise<MChannelAccountDefault> { 540 static loadByNameAndHostAndPopulateAccount (name: string, host: string): Promise<MChannelBannerAccountDefault> {
532 const query = { 541 const query = {
533 include: [ 542 include: [
534 { 543 {
@@ -542,6 +551,11 @@ export class VideoChannelModel extends Model {
542 model: ServerModel, 551 model: ServerModel,
543 required: true, 552 required: true,
544 where: { host } 553 where: { host }
554 },
555 {
556 model: ActorImageModel,
557 required: false,
558 as: 'Banner'
545 } 559 }
546 ] 560 ]
547 } 561 }
@@ -549,22 +563,10 @@ export class VideoChannelModel extends Model {
549 } 563 }
550 564
551 return VideoChannelModel.unscoped() 565 return VideoChannelModel.unscoped()
552 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) 566 .scope([ ScopeNames.WITH_ACCOUNT ])
553 .findOne(query) 567 .findOne(query)
554 } 568 }
555 569
556 static loadAndPopulateAccountAndVideos (id: number): Promise<MChannelActorAccountDefaultVideos> {
557 const options = {
558 include: [
559 VideoModel
560 ]
561 }
562
563 return VideoChannelModel.unscoped()
564 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_VIDEOS ])
565 .findByPk(id, options)
566 }
567
568 toFormattedSummaryJSON (this: MChannelSummaryFormattable): VideoChannelSummary { 570 toFormattedSummaryJSON (this: MChannelSummaryFormattable): VideoChannelSummary {
569 const actor = this.Actor.toFormattedSummaryJSON() 571 const actor = this.Actor.toFormattedSummaryJSON()
570 572
diff --git a/server/types/models/account/account.ts b/server/types/models/account/account.ts
index d2add9810..9513acad8 100644
--- a/server/types/models/account/account.ts
+++ b/server/types/models/account/account.ts
@@ -1,7 +1,10 @@
1import { FunctionProperties, PickWith } from '@shared/core-utils'
1import { AccountModel } from '../../../models/account/account' 2import { AccountModel } from '../../../models/account/account'
3import { MChannelDefault } from '../video/video-channels'
4import { MAccountBlocklistId } from './account-blocklist'
2import { 5import {
3 MActor, 6 MActor,
4 MActorAP, 7 MActorAPAccount,
5 MActorAPI, 8 MActorAPI,
6 MActorAudience, 9 MActorAudience,
7 MActorDefault, 10 MActorDefault,
@@ -13,9 +16,6 @@ import {
13 MActorSummaryFormattable, 16 MActorSummaryFormattable,
14 MActorUrl 17 MActorUrl
15} from './actor' 18} from './actor'
16import { FunctionProperties, PickWith } from '@shared/core-utils'
17import { MAccountBlocklistId } from './account-blocklist'
18import { MChannelDefault } from '../video/video-channels'
19 19
20type Use<K extends keyof AccountModel, M> = PickWith<AccountModel, K, M> 20type Use<K extends keyof AccountModel, M> = PickWith<AccountModel, K, M>
21 21
@@ -106,4 +106,4 @@ export type MAccountFormattable =
106 106
107export type MAccountAP = 107export type MAccountAP =
108 Pick<MAccount, 'name' | 'description'> & 108 Pick<MAccount, 'name' | 'description'> &
109 Use<'Actor', MActorAP> 109 Use<'Actor', MActorAPAccount>
diff --git a/server/types/models/account/actor-follow.ts b/server/types/models/account/actor-follow.ts
index 8c213d09c..8e19c6140 100644
--- a/server/types/models/account/actor-follow.ts
+++ b/server/types/models/account/actor-follow.ts
@@ -1,16 +1,15 @@
1import { PickWith } from '@shared/core-utils'
1import { ActorFollowModel } from '../../../models/activitypub/actor-follow' 2import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
2import { 3import {
3 MActor, 4 MActor,
4 MActorChannelAccountActor, 5 MActorChannelAccountActor,
5 MActorDefault, 6 MActorDefault,
6 MActorDefaultAccountChannel, 7 MActorDefaultAccountChannel,
8 MActorDefaultChannelId,
7 MActorFormattable, 9 MActorFormattable,
8 MActorHost, 10 MActorHost,
9 MActorUsername 11 MActorUsername
10} from './actor' 12} from './actor'
11import { PickWith } from '@shared/core-utils'
12import { ActorModel } from '@server/models/activitypub/actor'
13import { MChannelDefault } from '../video/video-channels'
14 13
15type Use<K extends keyof ActorFollowModel, M> = PickWith<ActorFollowModel, K, M> 14type Use<K extends keyof ActorFollowModel, M> = PickWith<ActorFollowModel, K, M>
16 15
@@ -47,14 +46,10 @@ export type MActorFollowFull =
47 46
48// For subscriptions 47// For subscriptions
49 48
50type SubscriptionFollowing =
51 MActorDefault &
52 PickWith<ActorModel, 'VideoChannel', MChannelDefault>
53
54export type MActorFollowActorsDefaultSubscription = 49export type MActorFollowActorsDefaultSubscription =
55 MActorFollow & 50 MActorFollow &
56 Use<'ActorFollower', MActorDefault> & 51 Use<'ActorFollower', MActorDefault> &
57 Use<'ActorFollowing', SubscriptionFollowing> 52 Use<'ActorFollowing', MActorDefaultChannelId>
58 53
59export type MActorFollowSubscriptions = 54export type MActorFollowSubscriptions =
60 MActorFollow & 55 MActorFollow &
diff --git a/server/types/models/account/actor.ts b/server/types/models/account/actor.ts
index 8af19c4da..8f3f30074 100644
--- a/server/types/models/account/actor.ts
+++ b/server/types/models/account/actor.ts
@@ -1,3 +1,4 @@
1
1import { FunctionProperties, PickWith, PickWithOpt } from '@shared/core-utils' 2import { FunctionProperties, PickWith, PickWithOpt } from '@shared/core-utils'
2import { ActorModel } from '../../../models/activitypub/actor' 3import { ActorModel } from '../../../models/activitypub/actor'
3import { MServer, MServerHost, MServerHostBlocks, MServerRedundancyAllowed } from '../server' 4import { MServer, MServerHost, MServerHostBlocks, MServerRedundancyAllowed } from '../server'
@@ -6,6 +7,7 @@ import { MAccount, MAccountDefault, MAccountId, MAccountIdActor } from './accoun
6import { MActorImage, MActorImageFormattable } from './actor-image' 7import { MActorImage, MActorImageFormattable } from './actor-image'
7 8
8type Use<K extends keyof ActorModel, M> = PickWith<ActorModel, K, M> 9type Use<K extends keyof ActorModel, M> = PickWith<ActorModel, K, M>
10type UseOpt<K extends keyof ActorModel, M> = PickWithOpt<ActorModel, K, M>
9 11
10// ############################################################################ 12// ############################################################################
11 13
@@ -75,11 +77,26 @@ export type MActorServer =
75 77
76// Complex actor associations 78// Complex actor associations
77 79
80export type MActorImages =
81 MActor &
82 Use<'Avatar', MActorImage> &
83 UseOpt<'Banner', MActorImage>
84
78export type MActorDefault = 85export type MActorDefault =
79 MActor & 86 MActor &
80 Use<'Server', MServer> & 87 Use<'Server', MServer> &
81 Use<'Avatar', MActorImage> 88 Use<'Avatar', MActorImage>
82 89
90export type MActorDefaultChannelId =
91 MActorDefault &
92 Use<'VideoChannel', MChannelId>
93
94export type MActorDefaultBanner =
95 MActor &
96 Use<'Server', MServer> &
97 Use<'Avatar', MActorImage> &
98 Use<'Banner', MActorImage>
99
83// Actor with channel that is associated to an account and its actor 100// Actor with channel that is associated to an account and its actor
84// Actor -> VideoChannel -> Account -> Actor 101// Actor -> VideoChannel -> Account -> Actor
85export type MActorChannelAccountActor = 102export type MActorChannelAccountActor =
@@ -90,6 +107,7 @@ export type MActorFull =
90 MActor & 107 MActor &
91 Use<'Server', MServer> & 108 Use<'Server', MServer> &
92 Use<'Avatar', MActorImage> & 109 Use<'Avatar', MActorImage> &
110 Use<'Banner', MActorImage> &
93 Use<'Account', MAccount> & 111 Use<'Account', MAccount> &
94 Use<'VideoChannel', MChannelAccountActor> 112 Use<'VideoChannel', MChannelAccountActor>
95 113
@@ -98,6 +116,7 @@ export type MActorFullActor =
98 MActor & 116 MActor &
99 Use<'Server', MServer> & 117 Use<'Server', MServer> &
100 Use<'Avatar', MActorImage> & 118 Use<'Avatar', MActorImage> &
119 Use<'Banner', MActorImage> &
101 Use<'Account', MAccountDefault> & 120 Use<'Account', MAccountDefault> &
102 Use<'VideoChannel', MChannelAccountDefault> 121 Use<'VideoChannel', MChannelAccountDefault>
103 122
@@ -131,9 +150,17 @@ export type MActorSummaryFormattable =
131 150
132export type MActorFormattable = 151export type MActorFormattable =
133 MActorSummaryFormattable & 152 MActorSummaryFormattable &
134 Pick<MActor, 'id' | 'followingCount' | 'followersCount' | 'createdAt' | 'updatedAt'> & 153 Pick<MActor, 'id' | 'followingCount' | 'followersCount' | 'createdAt' | 'updatedAt' | 'bannerId' | 'avatarId'> &
135 Use<'Server', MServerHost & Partial<Pick<MServer, 'redundancyAllowed'>>> 154 Use<'Server', MServerHost & Partial<Pick<MServer, 'redundancyAllowed'>>> &
155 UseOpt<'Banner', MActorImageFormattable>
136 156
137export type MActorAP = 157type MActorAPBase =
138 MActor & 158 MActor &
139 Use<'Avatar', MActorImage> 159 Use<'Avatar', MActorImage>
160
161export type MActorAPAccount =
162 MActorAPBase
163
164export type MActorAPChannel =
165 MActorAPBase &
166 Use<'Banner', MActorImage>
diff --git a/server/types/models/user/user.ts b/server/types/models/user/user.ts
index 12a68accf..fa7de9c52 100644
--- a/server/types/models/user/user.ts
+++ b/server/types/models/user/user.ts
@@ -1,5 +1,7 @@
1import { UserModel } from '../../../models/account/user' 1import { AccountModel } from '@server/models/account/account'
2import { MVideoPlaylist } from '@server/types/models'
2import { PickWith, PickWithOpt } from '@shared/core-utils' 3import { PickWith, PickWithOpt } from '@shared/core-utils'
4import { UserModel } from '../../../models/account/user'
3import { 5import {
4 MAccount, 6 MAccount,
5 MAccountDefault, 7 MAccountDefault,
@@ -9,10 +11,8 @@ import {
9 MAccountIdActorId, 11 MAccountIdActorId,
10 MAccountUrl 12 MAccountUrl
11} from '../account' 13} from '../account'
12import { MNotificationSetting, MNotificationSettingFormattable } from './user-notification-setting'
13import { AccountModel } from '@server/models/account/account'
14import { MChannelFormattable } from '../video/video-channels' 14import { MChannelFormattable } from '../video/video-channels'
15import { MVideoPlaylist } from '@server/types/models' 15import { MNotificationSetting, MNotificationSettingFormattable } from './user-notification-setting'
16 16
17type Use<K extends keyof UserModel, M> = PickWith<UserModel, K, M> 17type Use<K extends keyof UserModel, M> = PickWith<UserModel, K, M>
18 18
diff --git a/server/types/models/video/video-channels.ts b/server/types/models/video/video-channels.ts
index 77790daa4..f577807ca 100644
--- a/server/types/models/video/video-channels.ts
+++ b/server/types/models/video/video-channels.ts
@@ -12,15 +12,17 @@ import {
12 MAccountUserId, 12 MAccountUserId,
13 MActor, 13 MActor,
14 MActorAccountChannelId, 14 MActorAccountChannelId,
15 MActorAP, 15 MActorAPChannel,
16 MActorAPI, 16 MActorAPI,
17 MActorDefault, 17 MActorDefault,
18 MActorDefaultBanner,
18 MActorDefaultLight, 19 MActorDefaultLight,
19 MActorFormattable, 20 MActorFormattable,
20 MActorHost, 21 MActorHost,
21 MActorLight, 22 MActorLight,
22 MActorSummary, 23 MActorSummary,
23 MActorSummaryFormattable, MActorUrl 24 MActorSummaryFormattable,
25 MActorUrl
24} from '../account' 26} from '../account'
25import { MVideo } from './video' 27import { MVideo } from './video'
26 28
@@ -55,14 +57,14 @@ export type MChannelDefault =
55 MChannel & 57 MChannel &
56 Use<'Actor', MActorDefault> 58 Use<'Actor', MActorDefault>
57 59
60export type MChannelBannerDefault =
61 MChannel &
62 Use<'Actor', MActorDefaultBanner>
63
58// ############################################################################ 64// ############################################################################
59 65
60// Not all association attributes 66// Not all association attributes
61 67
62export type MChannelLight =
63 MChannel &
64 Use<'Actor', MActorDefaultLight>
65
66export type MChannelActorLight = 68export type MChannelActorLight =
67 MChannel & 69 MChannel &
68 Use<'Actor', MActorLight> 70 Use<'Actor', MActorLight>
@@ -84,29 +86,23 @@ export type MChannelAccountActor =
84 MChannel & 86 MChannel &
85 Use<'Account', MAccountActor> 87 Use<'Account', MAccountActor>
86 88
87export type MChannelAccountDefault = 89export type MChannelBannerAccountDefault =
88 MChannel & 90 MChannel &
89 Use<'Actor', MActorDefault> & 91 Use<'Actor', MActorDefaultBanner> &
90 Use<'Account', MAccountDefault> 92 Use<'Account', MAccountDefault>
91 93
92export type MChannelActorAccountActor = 94export type MChannelAccountDefault =
93 MChannel & 95 MChannel &
94 Use<'Account', MAccountActor> & 96 Use<'Actor', MActorDefault> &
95 Use<'Actor', MActor> 97 Use<'Account', MAccountDefault>
96 98
97// ############################################################################ 99// ############################################################################
98 100
99// Videos associations 101// Videos associations
100export type MChannelVideos = 102export type MChannelVideos =
101 MChannel & 103 MChannel &
102 Use<'Videos', MVideo[]> 104 Use<'Videos', MVideo[]>
103 105
104export type MChannelActorAccountDefaultVideos =
105 MChannel &
106 Use<'Actor', MActorDefault> &
107 Use<'Account', MAccountDefault> &
108 Use<'Videos', MVideo[]>
109
110// ############################################################################ 106// ############################################################################
111 107
112// For API 108// For API
@@ -146,5 +142,5 @@ export type MChannelFormattable =
146 142
147export type MChannelAP = 143export type MChannelAP =
148 Pick<MChannel, 'name' | 'description' | 'support'> & 144 Pick<MChannel, 'name' | 'description' | 'support'> &
149 Use<'Actor', MActorAP> & 145 Use<'Actor', MActorAPChannel> &
150 Use<'Account', MAccountUrl> 146 Use<'Account', MAccountUrl>
diff --git a/server/typings/express/index.d.ts b/server/typings/express/index.d.ts
index b0004dc7b..ee4faa35d 100644
--- a/server/typings/express/index.d.ts
+++ b/server/typings/express/index.d.ts
@@ -3,7 +3,10 @@ import {
3 MAbuseMessage, 3 MAbuseMessage,
4 MAbuseReporter, 4 MAbuseReporter,
5 MAccountBlocklist, 5 MAccountBlocklist,
6 MActorFollowActors,
7 MActorFollowActorsDefault,
6 MActorUrl, 8 MActorUrl,
9 MChannelBannerAccountDefault,
7 MStreamingPlaylist, 10 MStreamingPlaylist,
8 MVideoChangeOwnershipFull, 11 MVideoChangeOwnershipFull,
9 MVideoFile, 12 MVideoFile,
@@ -21,10 +24,8 @@ import { RegisteredPlugin } from '../../lib/plugins/plugin-manager'
21import { 24import {
22 MAccountDefault, 25 MAccountDefault,
23 MActorAccountChannelId, 26 MActorAccountChannelId,
24 MActorFollowActorsDefault,
25 MActorFollowActorsDefaultSubscription, 27 MActorFollowActorsDefaultSubscription,
26 MActorFull, 28 MActorFull,
27 MChannelAccountDefault,
28 MComment, 29 MComment,
29 MCommentOwnerVideoReply, 30 MCommentOwnerVideoReply,
30 MUserDefault, 31 MUserDefault,
@@ -71,7 +72,7 @@ interface PeerTubeLocals {
71 72
72 videoStreamingPlaylist?: MStreamingPlaylist 73 videoStreamingPlaylist?: MStreamingPlaylist
73 74
74 videoChannel?: MChannelAccountDefault 75 videoChannel?: MChannelBannerAccountDefault
75 76
76 videoPlaylistFull?: MVideoPlaylistFull 77 videoPlaylistFull?: MVideoPlaylistFull
77 videoPlaylistSummary?: MVideoPlaylistFullSummary 78 videoPlaylistSummary?: MVideoPlaylistFullSummary