diff options
author | Chocobozzz <me@florianbigard.com> | 2021-04-06 17:01:35 +0200 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2021-04-08 10:07:53 +0200 |
commit | 2cb03dc1f4e01ba491c36caff30c33fe9c5bad89 (patch) | |
tree | 08a8706d105ea1e280339c02b9e2b1dc1edb0ff9 | |
parent | f479685678406a5df864d89615b33d29085ebfc6 (diff) | |
download | PeerTube-2cb03dc1f4e01ba491c36caff30c33fe9c5bad89.tar.gz PeerTube-2cb03dc1f4e01ba491c36caff30c33fe9c5bad89.tar.zst PeerTube-2cb03dc1f4e01ba491c36caff30c33fe9c5bad89.zip |
Add banners support
33 files changed, 390 insertions, 238 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' | |||
2 | import * as express from 'express' | 2 | import * as express from 'express' |
3 | import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '@server/helpers/audit-logger' | 3 | import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '@server/helpers/audit-logger' |
4 | import { Hooks } from '@server/lib/plugins/hooks' | 4 | import { Hooks } from '@server/lib/plugins/hooks' |
5 | import { UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../../shared' | 5 | import { ActorImageType, UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../../shared' |
6 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | 6 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' |
7 | import { UserVideoQuota } from '../../../../shared/models/users/user-video-quota.model' | 7 | import { UserVideoQuota } from '../../../../shared/models/users/user-video-quota.model' |
8 | import { createReqFiles } from '../../../helpers/express-utils' | 8 | import { createReqFiles } from '../../../helpers/express-utils' |
@@ -11,7 +11,7 @@ import { CONFIG } from '../../../initializers/config' | |||
11 | import { MIMETYPES } from '../../../initializers/constants' | 11 | import { MIMETYPES } from '../../../initializers/constants' |
12 | import { sequelizeTypescript } from '../../../initializers/database' | 12 | import { sequelizeTypescript } from '../../../initializers/database' |
13 | import { sendUpdateActor } from '../../../lib/activitypub/send' | 13 | import { sendUpdateActor } from '../../../lib/activitypub/send' |
14 | import { deleteLocalActorAvatarFile, updateLocalActorAvatarFile } from '../../../lib/actor-image' | 14 | import { deleteLocalActorImageFile, updateLocalActorImageFile } from '../../../lib/actor-image' |
15 | import { getOriginalVideoFileTotalDailyFromUser, getOriginalVideoFileTotalFromUser, sendVerifyUserEmail } from '../../../lib/user' | 15 | import { getOriginalVideoFileTotalDailyFromUser, getOriginalVideoFileTotalFromUser, sendVerifyUserEmail } from '../../../lib/user' |
16 | import { | 16 | import { |
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 @@ | |||
1 | import 'multer' | 1 | import 'multer' |
2 | import * as express from 'express' | 2 | import * as express from 'express' |
3 | import { sendUndoFollow } from '@server/lib/activitypub/send' | ||
4 | import { VideoChannelModel } from '@server/models/video/video-channel' | ||
5 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
3 | import { VideoFilter } from '../../../../shared/models/videos/video-query.type' | 6 | import { VideoFilter } from '../../../../shared/models/videos/video-query.type' |
4 | import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils' | 7 | import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils' |
5 | import { getFormattedObjects } from '../../../helpers/utils' | 8 | import { getFormattedObjects } from '../../../helpers/utils' |
@@ -26,8 +29,6 @@ import { | |||
26 | } from '../../../middlewares/validators' | 29 | } from '../../../middlewares/validators' |
27 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | 30 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' |
28 | import { VideoModel } from '../../../models/video/video' | 31 | import { VideoModel } from '../../../models/video/video' |
29 | import { sendUndoFollow } from '@server/lib/activitypub/send' | ||
30 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
31 | 32 | ||
32 | const mySubscriptionsRouter = express.Router() | 33 | const mySubscriptionsRouter = express.Router() |
33 | 34 | ||
@@ -66,7 +67,7 @@ mySubscriptionsRouter.post('/me/subscriptions', | |||
66 | mySubscriptionsRouter.get('/me/subscriptions/:uri', | 67 | mySubscriptionsRouter.get('/me/subscriptions/:uri', |
67 | authenticate, | 68 | authenticate, |
68 | userSubscriptionGetValidator, | 69 | userSubscriptionGetValidator, |
69 | getUserSubscription | 70 | asyncMiddleware(getUserSubscription) |
70 | ) | 71 | ) |
71 | 72 | ||
72 | mySubscriptionsRouter.delete('/me/subscriptions/:uri', | 73 | mySubscriptionsRouter.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 | ||
133 | function getUserSubscription (req: express.Request, res: express.Response) { | 134 | async 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 | ||
139 | async function deleteUserSubscription (req: express.Request, res: express.Response) { | 141 | async 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 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { Hooks } from '@server/lib/plugins/hooks' | 2 | import { Hooks } from '@server/lib/plugins/hooks' |
3 | import { getServerActor } from '@server/models/application/application' | 3 | import { getServerActor } from '@server/models/application/application' |
4 | import { MChannelAccountDefault } from '@server/types/models' | 4 | import { MChannelBannerAccountDefault } from '@server/types/models' |
5 | import { VideoChannelCreate, VideoChannelUpdate } from '../../../shared' | 5 | import { ActorImageType, VideoChannelCreate, VideoChannelUpdate } from '../../../shared' |
6 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | 6 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' |
7 | import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger' | 7 | import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger' |
8 | import { resetSequelizeInstance } from '../../helpers/database-utils' | 8 | import { resetSequelizeInstance } from '../../helpers/database-utils' |
@@ -13,7 +13,7 @@ import { CONFIG } from '../../initializers/config' | |||
13 | import { MIMETYPES } from '../../initializers/constants' | 13 | import { MIMETYPES } from '../../initializers/constants' |
14 | import { sequelizeTypescript } from '../../initializers/database' | 14 | import { sequelizeTypescript } from '../../initializers/database' |
15 | import { sendUpdateActor } from '../../lib/activitypub/send' | 15 | import { sendUpdateActor } from '../../lib/activitypub/send' |
16 | import { deleteLocalActorAvatarFile, updateLocalActorAvatarFile } from '../../lib/actor-image' | 16 | import { deleteLocalActorImageFile, updateLocalActorImageFile } from '../../lib/actor-image' |
17 | import { JobQueue } from '../../lib/job-queue' | 17 | import { JobQueue } from '../../lib/job-queue' |
18 | import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel' | 18 | import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel' |
19 | import { | 19 | import { |
@@ -33,7 +33,7 @@ import { | |||
33 | videoPlaylistsSortValidator | 33 | videoPlaylistsSortValidator |
34 | } from '../../middlewares' | 34 | } from '../../middlewares' |
35 | import { videoChannelsNameWithHostValidator, videoChannelsOwnSearchValidator, videosSortValidator } from '../../middlewares/validators' | 35 | import { videoChannelsNameWithHostValidator, videoChannelsOwnSearchValidator, videosSortValidator } from '../../middlewares/validators' |
36 | import { updateAvatarValidator } from '../../middlewares/validators/avatar' | 36 | import { updateAvatarValidator, updateBannerValidator } from '../../middlewares/validators/avatar' |
37 | import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists' | 37 | import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists' |
38 | import { AccountModel } from '../../models/account/account' | 38 | import { AccountModel } from '../../models/account/account' |
39 | import { VideoModel } from '../../models/video/video' | 39 | import { VideoModel } from '../../models/video/video' |
@@ -42,6 +42,7 @@ import { VideoPlaylistModel } from '../../models/video/video-playlist' | |||
42 | 42 | ||
43 | const auditLogger = auditLoggerFactory('channels') | 43 | const auditLogger = auditLoggerFactory('channels') |
44 | const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR }) | 44 | const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR }) |
45 | const reqBannerFile = createReqFiles([ 'bannerfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { bannerfile: CONFIG.STORAGE.TMP_DIR }) | ||
45 | 46 | ||
46 | const videoChannelRouter = express.Router() | 47 | const videoChannelRouter = express.Router() |
47 | 48 | ||
@@ -69,6 +70,15 @@ videoChannelRouter.post('/:nameWithHost/avatar/pick', | |||
69 | asyncMiddleware(updateVideoChannelAvatar) | 70 | asyncMiddleware(updateVideoChannelAvatar) |
70 | ) | 71 | ) |
71 | 72 | ||
73 | videoChannelRouter.post('/:nameWithHost/banner/pick', | ||
74 | authenticate, | ||
75 | reqBannerFile, | ||
76 | // Check the rights | ||
77 | asyncMiddleware(videoChannelsUpdateValidator), | ||
78 | updateBannerValidator, | ||
79 | asyncMiddleware(updateVideoChannelBanner) | ||
80 | ) | ||
81 | |||
72 | videoChannelRouter.delete('/:nameWithHost/avatar', | 82 | videoChannelRouter.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 | ||
89 | videoChannelRouter.delete('/:nameWithHost/banner', | ||
90 | authenticate, | ||
91 | // Check the rights | ||
92 | asyncMiddleware(videoChannelsUpdateValidator), | ||
93 | asyncMiddleware(deleteVideoChannelBanner) | ||
94 | ) | ||
95 | |||
79 | videoChannelRouter.put('/:nameWithHost', | 96 | videoChannelRouter.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 | ||
154 | async 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 | } | ||
137 | async function updateVideoChannelAvatar (req: express.Request, res: express.Response) { | 165 | async 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 | ||
153 | async function deleteVideoChannelAvatar (req: express.Request, res: express.Response) { | 177 | async 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 | |||
185 | async 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 | ||
183 | async function updateVideoChannel (req: express.Request, res: express.Response) { | 215 | async 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 | ||
254 | async function getVideoChannel (req: express.Request, res: express.Response) { | 286 | async 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 | ||
264 | async function listVideoChannelPlaylists (req: express.Request, res: express.Response) { | 296 | async 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 @@ | |||
1 | import { values } from 'lodash' | ||
1 | import validator from 'validator' | 2 | import validator from 'validator' |
2 | import { UserRole } from '../../../shared' | 3 | import { UserRole } from '../../../shared' |
4 | import { isEmailEnabled } from '../../initializers/config' | ||
3 | import { CONSTRAINTS_FIELDS, NSFW_POLICY_TYPES } from '../../initializers/constants' | 5 | import { CONSTRAINTS_FIELDS, NSFW_POLICY_TYPES } from '../../initializers/constants' |
4 | import { exists, isArray, isBooleanValid, isFileValid } from './misc' | 6 | import { exists, isArray, isBooleanValid, isFileValid } from './misc' |
5 | import { values } from 'lodash' | ||
6 | import { isEmailEnabled } from '../../initializers/config' | ||
7 | 7 | ||
8 | const USERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USERS | 8 | const 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 | ||
100 | const avatarMimeTypes = CONSTRAINTS_FIELDS.ACTORS.AVATAR.EXTNAME | 100 | const avatarMimeTypes = CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME |
101 | .map(v => v.replace('.', '')) | 101 | .map(v => v.replace('.', '')) |
102 | .join('|') | 102 | .join('|') |
103 | const avatarMimeTypesRegex = `image/(${avatarMimeTypes})` | 103 | const avatarMimeTypesRegex = `image/(${avatarMimeTypes})` |
104 | function isAvatarFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) { | 104 | function 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 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { VideoChannelModel } from '../../models/video/video-channel' | 2 | import { MChannelBannerAccountDefault } from '@server/types/models' |
3 | import { MChannelAccountDefault } from '@server/types/models' | ||
4 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | 3 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' |
4 | import { VideoChannelModel } from '../../models/video/video-channel' | ||
5 | 5 | ||
6 | async function doesLocalVideoChannelNameExist (name: string, res: express.Response) { | 6 | async 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 | ||
32 | function processVideoChannelExist (videoChannel: MChannelAccountDefault, res: express.Response) { | 32 | function 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 | ||
68 | async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAccountId, res: Response) { | 68 | async 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 | } |
608 | const AVATARS_SIZE = { | 610 | const 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 | ||
613 | const EMBED_SIZE = { | 621 | const 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' | |||
4 | import { URL } from 'url' | 4 | import { URL } from 'url' |
5 | import { v4 as uuidv4 } from 'uuid' | 5 | import { v4 as uuidv4 } from 'uuid' |
6 | import { getServerActor } from '@server/models/application/application' | 6 | import { getServerActor } from '@server/models/application/application' |
7 | import { ActorImageType } from '@shared/models' | ||
7 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | 8 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' |
8 | import { ActivityPubActor, ActivityPubActorType, ActivityPubOrderedCollection } from '../../../shared/models/activitypub' | 9 | import { ActivityPubActor, ActivityPubActorType, ActivityPubOrderedCollection } from '../../../shared/models/activitypub' |
9 | import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects' | 10 | import { 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' |
39 | import { JobQueue } from '../job-queue' | 40 | import { JobQueue } from '../job-queue' |
@@ -168,43 +169,60 @@ async function updateActorInstance (actorInstance: ActorModel, attributes: Activ | |||
168 | } | 169 | } |
169 | } | 170 | } |
170 | 171 | ||
171 | type AvatarInfo = { name: string, onDisk: boolean, fileUrl: string } | 172 | type AvatarInfo = { name: string, onDisk: boolean, fileUrl: string, type: ActorImageType } |
172 | async function updateActorAvatarInstance (actor: MActorDefault, info: AvatarInfo, t: Transaction) { | 173 | async 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 | ||
198 | async function deleteActorAvatarInstance (actor: MActorDefault, t: Transaction) { | 209 | async 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 | ||
222 | function getAvatarInfoIfExists (actorJSON: ActivityPubActor) { | 240 | function 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 | } |
445 | async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: number, result: FetchRemoteActorResult }> { | 495 | async 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' | |||
7 | import { VideoCommentModel } from '../../../models/video/video-comment' | 7 | import { VideoCommentModel } from '../../../models/video/video-comment' |
8 | import { VideoPlaylistModel } from '../../../models/video/video-playlist' | 8 | import { VideoPlaylistModel } from '../../../models/video/video-playlist' |
9 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | 9 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' |
10 | import { MAccountActor, MActor, MActorSignature, MChannelActor, MChannelActorAccountActor, MCommentOwnerVideo } from '../../../types/models' | 10 | import { MAccountActor, MActor, MActorSignature, MChannelActor, MCommentOwnerVideo } from '../../../types/models' |
11 | import { markCommentAsDeleted } from '../../video-comment' | 11 | import { markCommentAsDeleted } from '../../video-comment' |
12 | import { forwardVideoRelatedActivity } from '../send/utils' | 12 | import { 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' | |||
6 | import { AccountModel } from '../../../models/account/account' | 6 | import { AccountModel } from '../../../models/account/account' |
7 | import { ActorModel } from '../../../models/activitypub/actor' | 7 | import { ActorModel } from '../../../models/activitypub/actor' |
8 | import { VideoChannelModel } from '../../../models/video/video-channel' | 8 | import { VideoChannelModel } from '../../../models/video/video-channel' |
9 | import { getAvatarInfoIfExists, updateActorAvatarInstance, updateActorInstance } from '../actor' | 9 | import { getImageInfoIfExists, updateActorImageInstance, updateActorInstance } from '../actor' |
10 | import { getOrCreateVideoAndAccountAndChannel, getOrCreateVideoChannelFromVideoObject, updateVideoFromAP } from '../videos' | 10 | import { getOrCreateVideoAndAccountAndChannel, getOrCreateVideoChannelFromVideoObject, updateVideoFromAP } from '../videos' |
11 | import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos' | 11 | import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos' |
12 | import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file' | 12 | import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file' |
@@ -17,6 +17,7 @@ import { createOrUpdateVideoPlaylist } from '../playlist' | |||
17 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | 17 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' |
18 | import { MActorSignature, MAccountIdActor } from '../../../types/models' | 18 | import { MActorSignature, MAccountIdActor } from '../../../types/models' |
19 | import { isRedundancyAccepted } from '@server/lib/redundancy' | 19 | import { isRedundancyAccepted } from '@server/lib/redundancy' |
20 | import { ActorImageType } from '@shared/models' | ||
20 | 21 | ||
21 | async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) { | 22 | async 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' | |||
3 | import * as LRUCache from 'lru-cache' | 3 | import * as LRUCache from 'lru-cache' |
4 | import { extname, join } from 'path' | 4 | import { extname, join } from 'path' |
5 | import { v4 as uuidv4 } from 'uuid' | 5 | import { v4 as uuidv4 } from 'uuid' |
6 | import { ActorImageType } from '@shared/models' | ||
6 | import { retryTransactionWrapper } from '../helpers/database-utils' | 7 | import { retryTransactionWrapper } from '../helpers/database-utils' |
7 | import { processImage } from '../helpers/image-utils' | 8 | import { processImage } from '../helpers/image-utils' |
8 | import { downloadImage } from '../helpers/requests' | 9 | import { downloadImage } from '../helpers/requests' |
9 | import { CONFIG } from '../initializers/config' | 10 | import { CONFIG } from '../initializers/config' |
10 | import { AVATARS_SIZE, LRU_CACHE, QUEUE_CONCURRENCY } from '../initializers/constants' | 11 | import { ACTOR_IMAGES_SIZE, LRU_CACHE, QUEUE_CONCURRENCY } from '../initializers/constants' |
11 | import { sequelizeTypescript } from '../initializers/database' | 12 | import { sequelizeTypescript } from '../initializers/database' |
12 | import { MAccountDefault, MChannelDefault } from '../types/models' | 13 | import { MAccountDefault, MChannelDefault } from '../types/models' |
13 | import { deleteActorAvatarInstance, updateActorAvatarInstance } from './activitypub/actor' | 14 | import { deleteActorImageInstance, updateActorImageInstance } from './activitypub/actor' |
14 | import { sendUpdateActor } from './activitypub/send' | 15 | import { sendUpdateActor } from './activitypub/send' |
15 | 16 | ||
16 | async function updateLocalActorAvatarFile ( | 17 | async 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 | ||
44 | async function deleteLocalActorAvatarFile ( | 53 | async 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 | ||
59 | type DownloadImageQueueTask = { fileUrl: string, filename: string } | 66 | type DownloadImageQueueTask = { fileUrl: string, filename: string, type: ActorImageType } |
60 | 67 | ||
61 | const downloadImageQueue = queue<DownloadImageQueueTask, Error>((task, cb) => { | 68 | const 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 | ||
80 | export { | 91 | export { |
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' | |||
11 | import { CONFIG } from '../initializers/config' | 11 | import { CONFIG } from '../initializers/config' |
12 | import { | 12 | import { |
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' | |||
3 | import { VideoChannelCreate } from '../../shared/models' | 3 | import { VideoChannelCreate } from '../../shared/models' |
4 | import { VideoModel } from '../models/video/video' | 4 | import { VideoModel } from '../models/video/video' |
5 | import { VideoChannelModel } from '../models/video/video-channel' | 5 | import { VideoChannelModel } from '../models/video/video-channel' |
6 | import { MAccountId, MChannelDefault, MChannelId } from '../types/models' | 6 | import { MAccountId, MChannelId } from '../types/models' |
7 | import { buildActorInstance } from './activitypub/actor' | 7 | import { buildActorInstance } from './activitypub/actor' |
8 | import { getLocalVideoChannelActivityPubUrl } from './activitypub/url' | 8 | import { getLocalVideoChannelActivityPubUrl } from './activitypub/url' |
9 | import { federateVideoIfNeeded } from './activitypub/videos' | 9 | import { federateVideoIfNeeded } from './activitypub/videos' |
10 | 10 | ||
11 | type CustomVideoChannelModelAccount <T extends MAccountId> = MChannelDefault & { Account?: T } | 11 | async function createLocalVideoChannel (videoChannelInfo: VideoChannelCreate, account: MAccountId, t: Sequelize.Transaction) { |
12 | |||
13 | async 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' | |||
6 | import { logger } from '../../helpers/logger' | 6 | import { logger } from '../../helpers/logger' |
7 | import { cleanUpReqFiles } from '../../helpers/express-utils' | 7 | import { cleanUpReqFiles } from '../../helpers/express-utils' |
8 | 8 | ||
9 | const updateAvatarValidator = [ | 9 | const 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 | |||
24 | const updateAvatarValidator = updateActorImageValidatorFactory('avatarfile') | ||
25 | const updateBannerValidator = updateActorImageValidatorFactory('bannerfile') | ||
23 | 26 | ||
24 | export { | 27 | export { |
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' |
31 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 31 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
32 | import { ACTIVITY_PUB, ACTIVITY_PUB_ACTOR_TYPES, CONSTRAINTS_FIELDS, SERVER_ACTOR_NAME, WEBSERVER } from '../../initializers/constants' | 32 | import { |
33 | ACTIVITY_PUB, | ||
34 | ACTIVITY_PUB_ACTOR_TYPES, | ||
35 | CONSTRAINTS_FIELDS, | ||
36 | MIMETYPES, | ||
37 | SERVER_ACTOR_NAME, | ||
38 | WEBSERVER | ||
39 | } from '../../initializers/constants' | ||
33 | import { | 40 | import { |
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 { | |||
28 | import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' | 28 | import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' |
29 | import { sendDeleteActor } from '../../lib/activitypub/send' | 29 | import { sendDeleteActor } from '../../lib/activitypub/send' |
30 | import { | 30 | import { |
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 @@ | |||
1 | import { FunctionProperties, PickWith } from '@shared/core-utils' | ||
1 | import { AccountModel } from '../../../models/account/account' | 2 | import { AccountModel } from '../../../models/account/account' |
3 | import { MChannelDefault } from '../video/video-channels' | ||
4 | import { MAccountBlocklistId } from './account-blocklist' | ||
2 | import { | 5 | import { |
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' |
16 | import { FunctionProperties, PickWith } from '@shared/core-utils' | ||
17 | import { MAccountBlocklistId } from './account-blocklist' | ||
18 | import { MChannelDefault } from '../video/video-channels' | ||
19 | 19 | ||
20 | type Use<K extends keyof AccountModel, M> = PickWith<AccountModel, K, M> | 20 | type Use<K extends keyof AccountModel, M> = PickWith<AccountModel, K, M> |
21 | 21 | ||
@@ -106,4 +106,4 @@ export type MAccountFormattable = | |||
106 | 106 | ||
107 | export type MAccountAP = | 107 | export 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 @@ | |||
1 | import { PickWith } from '@shared/core-utils' | ||
1 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' | 2 | import { ActorFollowModel } from '../../../models/activitypub/actor-follow' |
2 | import { | 3 | import { |
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' |
11 | import { PickWith } from '@shared/core-utils' | ||
12 | import { ActorModel } from '@server/models/activitypub/actor' | ||
13 | import { MChannelDefault } from '../video/video-channels' | ||
14 | 13 | ||
15 | type Use<K extends keyof ActorFollowModel, M> = PickWith<ActorFollowModel, K, M> | 14 | type 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 | ||
50 | type SubscriptionFollowing = | ||
51 | MActorDefault & | ||
52 | PickWith<ActorModel, 'VideoChannel', MChannelDefault> | ||
53 | |||
54 | export type MActorFollowActorsDefaultSubscription = | 49 | export type MActorFollowActorsDefaultSubscription = |
55 | MActorFollow & | 50 | MActorFollow & |
56 | Use<'ActorFollower', MActorDefault> & | 51 | Use<'ActorFollower', MActorDefault> & |
57 | Use<'ActorFollowing', SubscriptionFollowing> | 52 | Use<'ActorFollowing', MActorDefaultChannelId> |
58 | 53 | ||
59 | export type MActorFollowSubscriptions = | 54 | export 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 | |||
1 | import { FunctionProperties, PickWith, PickWithOpt } from '@shared/core-utils' | 2 | import { FunctionProperties, PickWith, PickWithOpt } from '@shared/core-utils' |
2 | import { ActorModel } from '../../../models/activitypub/actor' | 3 | import { ActorModel } from '../../../models/activitypub/actor' |
3 | import { MServer, MServerHost, MServerHostBlocks, MServerRedundancyAllowed } from '../server' | 4 | import { MServer, MServerHost, MServerHostBlocks, MServerRedundancyAllowed } from '../server' |
@@ -6,6 +7,7 @@ import { MAccount, MAccountDefault, MAccountId, MAccountIdActor } from './accoun | |||
6 | import { MActorImage, MActorImageFormattable } from './actor-image' | 7 | import { MActorImage, MActorImageFormattable } from './actor-image' |
7 | 8 | ||
8 | type Use<K extends keyof ActorModel, M> = PickWith<ActorModel, K, M> | 9 | type Use<K extends keyof ActorModel, M> = PickWith<ActorModel, K, M> |
10 | type 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 | ||
80 | export type MActorImages = | ||
81 | MActor & | ||
82 | Use<'Avatar', MActorImage> & | ||
83 | UseOpt<'Banner', MActorImage> | ||
84 | |||
78 | export type MActorDefault = | 85 | export type MActorDefault = |
79 | MActor & | 86 | MActor & |
80 | Use<'Server', MServer> & | 87 | Use<'Server', MServer> & |
81 | Use<'Avatar', MActorImage> | 88 | Use<'Avatar', MActorImage> |
82 | 89 | ||
90 | export type MActorDefaultChannelId = | ||
91 | MActorDefault & | ||
92 | Use<'VideoChannel', MChannelId> | ||
93 | |||
94 | export 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 |
85 | export type MActorChannelAccountActor = | 102 | export 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 | ||
132 | export type MActorFormattable = | 151 | export 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 | ||
137 | export type MActorAP = | 157 | type MActorAPBase = |
138 | MActor & | 158 | MActor & |
139 | Use<'Avatar', MActorImage> | 159 | Use<'Avatar', MActorImage> |
160 | |||
161 | export type MActorAPAccount = | ||
162 | MActorAPBase | ||
163 | |||
164 | export 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 @@ | |||
1 | import { UserModel } from '../../../models/account/user' | 1 | import { AccountModel } from '@server/models/account/account' |
2 | import { MVideoPlaylist } from '@server/types/models' | ||
2 | import { PickWith, PickWithOpt } from '@shared/core-utils' | 3 | import { PickWith, PickWithOpt } from '@shared/core-utils' |
4 | import { UserModel } from '../../../models/account/user' | ||
3 | import { | 5 | import { |
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' |
12 | import { MNotificationSetting, MNotificationSettingFormattable } from './user-notification-setting' | ||
13 | import { AccountModel } from '@server/models/account/account' | ||
14 | import { MChannelFormattable } from '../video/video-channels' | 14 | import { MChannelFormattable } from '../video/video-channels' |
15 | import { MVideoPlaylist } from '@server/types/models' | 15 | import { MNotificationSetting, MNotificationSettingFormattable } from './user-notification-setting' |
16 | 16 | ||
17 | type Use<K extends keyof UserModel, M> = PickWith<UserModel, K, M> | 17 | type 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' |
25 | import { MVideo } from './video' | 27 | import { 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 | ||
60 | export 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 | ||
62 | export type MChannelLight = | ||
63 | MChannel & | ||
64 | Use<'Actor', MActorDefaultLight> | ||
65 | |||
66 | export type MChannelActorLight = | 68 | export 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 | ||
87 | export type MChannelAccountDefault = | 89 | export type MChannelBannerAccountDefault = |
88 | MChannel & | 90 | MChannel & |
89 | Use<'Actor', MActorDefault> & | 91 | Use<'Actor', MActorDefaultBanner> & |
90 | Use<'Account', MAccountDefault> | 92 | Use<'Account', MAccountDefault> |
91 | 93 | ||
92 | export type MChannelActorAccountActor = | 94 | export 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 |
100 | export type MChannelVideos = | 102 | export type MChannelVideos = |
101 | MChannel & | 103 | MChannel & |
102 | Use<'Videos', MVideo[]> | 104 | Use<'Videos', MVideo[]> |
103 | 105 | ||
104 | export 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 | ||
147 | export type MChannelAP = | 143 | export 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' | |||
21 | import { | 24 | import { |
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 |
diff --git a/shared/models/activitypub/activitypub-actor.ts b/shared/models/activitypub/activitypub-actor.ts index f022f3d02..c59be3f3b 100644 --- a/shared/models/activitypub/activitypub-actor.ts +++ b/shared/models/activitypub/activitypub-actor.ts | |||
@@ -27,5 +27,6 @@ export interface ActivityPubActor { | |||
27 | publicKeyPem: string | 27 | publicKeyPem: string |
28 | } | 28 | } |
29 | 29 | ||
30 | icon: ActivityIconObject | 30 | icon?: ActivityIconObject |
31 | image?: ActivityIconObject | ||
31 | } | 32 | } |
diff --git a/shared/models/activitypub/objects/common-objects.ts b/shared/models/activitypub/objects/common-objects.ts index 76f0e3bcf..43d7f7f74 100644 --- a/shared/models/activitypub/objects/common-objects.ts +++ b/shared/models/activitypub/objects/common-objects.ts | |||
@@ -9,7 +9,7 @@ export interface ActivityIdentifierObject { | |||
9 | export interface ActivityIconObject { | 9 | export interface ActivityIconObject { |
10 | type: 'Image' | 10 | type: 'Image' |
11 | url: string | 11 | url: string |
12 | mediaType: 'image/jpeg' | 'image/png' | 12 | mediaType: string |
13 | width?: number | 13 | width?: number |
14 | height?: number | 14 | height?: number |
15 | } | 15 | } |
diff --git a/shared/models/videos/channel/video-channel.model.ts b/shared/models/videos/channel/video-channel.model.ts index ae6dea42d..56517972d 100644 --- a/shared/models/videos/channel/video-channel.model.ts +++ b/shared/models/videos/channel/video-channel.model.ts | |||
@@ -15,6 +15,8 @@ export interface VideoChannel extends Actor { | |||
15 | 15 | ||
16 | videosCount?: number | 16 | videosCount?: number |
17 | viewsPerDay?: ViewsPerDate[] // chronologically ordered | 17 | viewsPerDay?: ViewsPerDate[] // chronologically ordered |
18 | |||
19 | banner?: ActorImage | ||
18 | } | 20 | } |
19 | 21 | ||
20 | export interface VideoChannelSummary { | 22 | export interface VideoChannelSummary { |