Add banners support
This commit is contained in:
parent
f479685678
commit
2cb03dc1f4
|
@ -158,9 +158,9 @@ async function getConfig (req: express.Request, res: express.Response) {
|
|||
avatar: {
|
||||
file: {
|
||||
size: {
|
||||
max: CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max
|
||||
max: CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max
|
||||
},
|
||||
extensions: CONSTRAINTS_FIELDS.ACTORS.AVATAR.EXTNAME
|
||||
extensions: CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME
|
||||
}
|
||||
},
|
||||
video: {
|
||||
|
|
|
@ -2,7 +2,7 @@ import 'multer'
|
|||
import * as express from 'express'
|
||||
import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '@server/helpers/audit-logger'
|
||||
import { Hooks } from '@server/lib/plugins/hooks'
|
||||
import { UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../../shared'
|
||||
import { ActorImageType, UserUpdateMe, UserVideoRate as FormattedUserVideoRate } from '../../../../shared'
|
||||
import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
|
||||
import { UserVideoQuota } from '../../../../shared/models/users/user-video-quota.model'
|
||||
import { createReqFiles } from '../../../helpers/express-utils'
|
||||
|
@ -11,7 +11,7 @@ import { CONFIG } from '../../../initializers/config'
|
|||
import { MIMETYPES } from '../../../initializers/constants'
|
||||
import { sequelizeTypescript } from '../../../initializers/database'
|
||||
import { sendUpdateActor } from '../../../lib/activitypub/send'
|
||||
import { deleteLocalActorAvatarFile, updateLocalActorAvatarFile } from '../../../lib/actor-image'
|
||||
import { deleteLocalActorImageFile, updateLocalActorImageFile } from '../../../lib/actor-image'
|
||||
import { getOriginalVideoFileTotalDailyFromUser, getOriginalVideoFileTotalFromUser, sendVerifyUserEmail } from '../../../lib/user'
|
||||
import {
|
||||
asyncMiddleware,
|
||||
|
@ -238,7 +238,7 @@ async function updateMyAvatar (req: express.Request, res: express.Response) {
|
|||
|
||||
const userAccount = await AccountModel.load(user.Account.id)
|
||||
|
||||
const avatar = await updateLocalActorAvatarFile(userAccount, avatarPhysicalFile)
|
||||
const avatar = await updateLocalActorImageFile(userAccount, avatarPhysicalFile, ActorImageType.AVATAR)
|
||||
|
||||
return res.json({ avatar: avatar.toFormattedJSON() })
|
||||
}
|
||||
|
@ -247,7 +247,7 @@ async function deleteMyAvatar (req: express.Request, res: express.Response) {
|
|||
const user = res.locals.oauth.token.user
|
||||
|
||||
const userAccount = await AccountModel.load(user.Account.id)
|
||||
await deleteLocalActorAvatarFile(userAccount)
|
||||
await deleteLocalActorImageFile(userAccount, ActorImageType.AVATAR)
|
||||
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import 'multer'
|
||||
import * as express from 'express'
|
||||
import { sendUndoFollow } from '@server/lib/activitypub/send'
|
||||
import { VideoChannelModel } from '@server/models/video/video-channel'
|
||||
import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
|
||||
import { VideoFilter } from '../../../../shared/models/videos/video-query.type'
|
||||
import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils'
|
||||
import { getFormattedObjects } from '../../../helpers/utils'
|
||||
|
@ -26,8 +29,6 @@ import {
|
|||
} from '../../../middlewares/validators'
|
||||
import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
|
||||
import { VideoModel } from '../../../models/video/video'
|
||||
import { sendUndoFollow } from '@server/lib/activitypub/send'
|
||||
import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
|
||||
|
||||
const mySubscriptionsRouter = express.Router()
|
||||
|
||||
|
@ -66,7 +67,7 @@ mySubscriptionsRouter.post('/me/subscriptions',
|
|||
mySubscriptionsRouter.get('/me/subscriptions/:uri',
|
||||
authenticate,
|
||||
userSubscriptionGetValidator,
|
||||
getUserSubscription
|
||||
asyncMiddleware(getUserSubscription)
|
||||
)
|
||||
|
||||
mySubscriptionsRouter.delete('/me/subscriptions/:uri',
|
||||
|
@ -130,10 +131,11 @@ function addUserSubscription (req: express.Request, res: express.Response) {
|
|||
return res.status(HttpStatusCode.NO_CONTENT_204).end()
|
||||
}
|
||||
|
||||
function getUserSubscription (req: express.Request, res: express.Response) {
|
||||
async function getUserSubscription (req: express.Request, res: express.Response) {
|
||||
const subscription = res.locals.subscription
|
||||
const videoChannel = await VideoChannelModel.loadAndPopulateAccount(subscription.ActorFollowing.VideoChannel.id)
|
||||
|
||||
return res.json(subscription.ActorFollowing.VideoChannel.toFormattedJSON())
|
||||
return res.json(videoChannel.toFormattedJSON())
|
||||
}
|
||||
|
||||
async function deleteUserSubscription (req: express.Request, res: express.Response) {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import * as express from 'express'
|
||||
import { Hooks } from '@server/lib/plugins/hooks'
|
||||
import { getServerActor } from '@server/models/application/application'
|
||||
import { MChannelAccountDefault } from '@server/types/models'
|
||||
import { VideoChannelCreate, VideoChannelUpdate } from '../../../shared'
|
||||
import { MChannelBannerAccountDefault } from '@server/types/models'
|
||||
import { ActorImageType, VideoChannelCreate, VideoChannelUpdate } from '../../../shared'
|
||||
import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
|
||||
import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../helpers/audit-logger'
|
||||
import { resetSequelizeInstance } from '../../helpers/database-utils'
|
||||
|
@ -13,7 +13,7 @@ import { CONFIG } from '../../initializers/config'
|
|||
import { MIMETYPES } from '../../initializers/constants'
|
||||
import { sequelizeTypescript } from '../../initializers/database'
|
||||
import { sendUpdateActor } from '../../lib/activitypub/send'
|
||||
import { deleteLocalActorAvatarFile, updateLocalActorAvatarFile } from '../../lib/actor-image'
|
||||
import { deleteLocalActorImageFile, updateLocalActorImageFile } from '../../lib/actor-image'
|
||||
import { JobQueue } from '../../lib/job-queue'
|
||||
import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel'
|
||||
import {
|
||||
|
@ -33,7 +33,7 @@ import {
|
|||
videoPlaylistsSortValidator
|
||||
} from '../../middlewares'
|
||||
import { videoChannelsNameWithHostValidator, videoChannelsOwnSearchValidator, videosSortValidator } from '../../middlewares/validators'
|
||||
import { updateAvatarValidator } from '../../middlewares/validators/avatar'
|
||||
import { updateAvatarValidator, updateBannerValidator } from '../../middlewares/validators/avatar'
|
||||
import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists'
|
||||
import { AccountModel } from '../../models/account/account'
|
||||
import { VideoModel } from '../../models/video/video'
|
||||
|
@ -42,6 +42,7 @@ import { VideoPlaylistModel } from '../../models/video/video-playlist'
|
|||
|
||||
const auditLogger = auditLoggerFactory('channels')
|
||||
const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR })
|
||||
const reqBannerFile = createReqFiles([ 'bannerfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { bannerfile: CONFIG.STORAGE.TMP_DIR })
|
||||
|
||||
const videoChannelRouter = express.Router()
|
||||
|
||||
|
@ -69,6 +70,15 @@ videoChannelRouter.post('/:nameWithHost/avatar/pick',
|
|||
asyncMiddleware(updateVideoChannelAvatar)
|
||||
)
|
||||
|
||||
videoChannelRouter.post('/:nameWithHost/banner/pick',
|
||||
authenticate,
|
||||
reqBannerFile,
|
||||
// Check the rights
|
||||
asyncMiddleware(videoChannelsUpdateValidator),
|
||||
updateBannerValidator,
|
||||
asyncMiddleware(updateVideoChannelBanner)
|
||||
)
|
||||
|
||||
videoChannelRouter.delete('/:nameWithHost/avatar',
|
||||
authenticate,
|
||||
// Check the rights
|
||||
|
@ -76,6 +86,13 @@ videoChannelRouter.delete('/:nameWithHost/avatar',
|
|||
asyncMiddleware(deleteVideoChannelAvatar)
|
||||
)
|
||||
|
||||
videoChannelRouter.delete('/:nameWithHost/banner',
|
||||
authenticate,
|
||||
// Check the rights
|
||||
asyncMiddleware(videoChannelsUpdateValidator),
|
||||
asyncMiddleware(deleteVideoChannelBanner)
|
||||
)
|
||||
|
||||
videoChannelRouter.put('/:nameWithHost',
|
||||
authenticate,
|
||||
asyncMiddleware(videoChannelsUpdateValidator),
|
||||
|
@ -134,26 +151,41 @@ async function listVideoChannels (req: express.Request, res: express.Response) {
|
|||
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
||||
}
|
||||
|
||||
async function updateVideoChannelBanner (req: express.Request, res: express.Response) {
|
||||
const bannerPhysicalFile = req.files['bannerfile'][0]
|
||||
const videoChannel = res.locals.videoChannel
|
||||
const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON())
|
||||
|
||||
const banner = await updateLocalActorImageFile(videoChannel, bannerPhysicalFile, ActorImageType.BANNER)
|
||||
|
||||
auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys)
|
||||
|
||||
return res.json({ banner: banner.toFormattedJSON() })
|
||||
}
|
||||
async function updateVideoChannelAvatar (req: express.Request, res: express.Response) {
|
||||
const avatarPhysicalFile = req.files['avatarfile'][0]
|
||||
const videoChannel = res.locals.videoChannel
|
||||
const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON())
|
||||
|
||||
const avatar = await updateLocalActorAvatarFile(videoChannel, avatarPhysicalFile)
|
||||
const avatar = await updateLocalActorImageFile(videoChannel, avatarPhysicalFile, ActorImageType.AVATAR)
|
||||
|
||||
auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys)
|
||||
|
||||
return res
|
||||
.json({
|
||||
avatar: avatar.toFormattedJSON()
|
||||
})
|
||||
.end()
|
||||
return res.json({ avatar: avatar.toFormattedJSON() })
|
||||
}
|
||||
|
||||
async function deleteVideoChannelAvatar (req: express.Request, res: express.Response) {
|
||||
const videoChannel = res.locals.videoChannel
|
||||
|
||||
await deleteLocalActorAvatarFile(videoChannel)
|
||||
await deleteLocalActorImageFile(videoChannel, ActorImageType.AVATAR)
|
||||
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
}
|
||||
|
||||
async function deleteVideoChannelBanner (req: express.Request, res: express.Response) {
|
||||
const videoChannel = res.locals.videoChannel
|
||||
|
||||
await deleteLocalActorImageFile(videoChannel, ActorImageType.BANNER)
|
||||
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
}
|
||||
|
@ -177,7 +209,7 @@ async function addVideoChannel (req: express.Request, res: express.Response) {
|
|||
videoChannel: {
|
||||
id: videoChannelCreated.id
|
||||
}
|
||||
}).end()
|
||||
})
|
||||
}
|
||||
|
||||
async function updateVideoChannel (req: express.Request, res: express.Response) {
|
||||
|
@ -206,7 +238,7 @@ async function updateVideoChannel (req: express.Request, res: express.Response)
|
|||
}
|
||||
}
|
||||
|
||||
const videoChannelInstanceUpdated = await videoChannelInstance.save(sequelizeOptions) as MChannelAccountDefault
|
||||
const videoChannelInstanceUpdated = await videoChannelInstance.save(sequelizeOptions) as MChannelBannerAccountDefault
|
||||
await sendUpdateActor(videoChannelInstanceUpdated, t)
|
||||
|
||||
auditLogger.update(
|
||||
|
@ -252,13 +284,13 @@ async function removeVideoChannel (req: express.Request, res: express.Response)
|
|||
}
|
||||
|
||||
async function getVideoChannel (req: express.Request, res: express.Response) {
|
||||
const videoChannelWithVideos = await VideoChannelModel.loadAndPopulateAccountAndVideos(res.locals.videoChannel.id)
|
||||
const videoChannel = res.locals.videoChannel
|
||||
|
||||
if (videoChannelWithVideos.isOutdated()) {
|
||||
JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'actor', url: videoChannelWithVideos.Actor.url } })
|
||||
if (videoChannel.isOutdated()) {
|
||||
JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'actor', url: videoChannel.Actor.url } })
|
||||
}
|
||||
|
||||
return res.json(videoChannelWithVideos.toFormattedJSON())
|
||||
return res.json(videoChannel.toFormattedJSON())
|
||||
}
|
||||
|
||||
async function listVideoChannelPlaylists (req: express.Request, res: express.Response) {
|
||||
|
|
|
@ -107,7 +107,7 @@ async function acceptOwnership (req: express.Request, res: express.Response) {
|
|||
// We need more attributes for federation
|
||||
const targetVideo = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoChangeOwnership.Video.id)
|
||||
|
||||
const oldVideoChannel = await VideoChannelModel.loadByIdAndPopulateAccount(targetVideo.channelId)
|
||||
const oldVideoChannel = await VideoChannelModel.loadAndPopulateAccount(targetVideo.channelId)
|
||||
|
||||
targetVideo.channelId = channel.id
|
||||
|
||||
|
|
|
@ -64,7 +64,7 @@ async function getActorImage (req: express.Request, res: express.Response) {
|
|||
logger.info('Lazy serve remote actor image %s.', image.fileUrl)
|
||||
|
||||
try {
|
||||
await pushActorImageProcessInQueue({ filename: image.filename, fileUrl: image.fileUrl })
|
||||
await pushActorImageProcessInQueue({ filename: image.filename, fileUrl: image.fileUrl, type: image.type })
|
||||
} catch (err) {
|
||||
logger.warn('Cannot process remote actor image %s.', image.fileUrl, { err })
|
||||
return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
|
||||
|
|
|
@ -252,9 +252,9 @@ async function generateNodeinfo (req: express.Request, res: express.Response) {
|
|||
avatar: {
|
||||
file: {
|
||||
size: {
|
||||
max: CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max
|
||||
max: CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max
|
||||
},
|
||||
extensions: CONSTRAINTS_FIELDS.ACTORS.AVATAR.EXTNAME
|
||||
extensions: CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME
|
||||
}
|
||||
},
|
||||
video: {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { values } from 'lodash'
|
||||
import validator from 'validator'
|
||||
import { UserRole } from '../../../shared'
|
||||
import { isEmailEnabled } from '../../initializers/config'
|
||||
import { CONSTRAINTS_FIELDS, NSFW_POLICY_TYPES } from '../../initializers/constants'
|
||||
import { exists, isArray, isBooleanValid, isFileValid } from './misc'
|
||||
import { values } from 'lodash'
|
||||
import { isEmailEnabled } from '../../initializers/config'
|
||||
|
||||
const USERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USERS
|
||||
|
||||
|
@ -97,12 +97,12 @@ function isUserRoleValid (value: any) {
|
|||
return exists(value) && validator.isInt('' + value) && UserRole[value] !== undefined
|
||||
}
|
||||
|
||||
const avatarMimeTypes = CONSTRAINTS_FIELDS.ACTORS.AVATAR.EXTNAME
|
||||
const avatarMimeTypes = CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME
|
||||
.map(v => v.replace('.', ''))
|
||||
.join('|')
|
||||
const avatarMimeTypesRegex = `image/(${avatarMimeTypes})`
|
||||
function isAvatarFile (files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[]) {
|
||||
return isFileValid(files, avatarMimeTypesRegex, 'avatarfile', CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max)
|
||||
return isFileValid(files, avatarMimeTypesRegex, 'avatarfile', CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as express from 'express'
|
||||
import { VideoChannelModel } from '../../models/video/video-channel'
|
||||
import { MChannelAccountDefault } from '@server/types/models'
|
||||
import { MChannelBannerAccountDefault } from '@server/types/models'
|
||||
import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
|
||||
import { VideoChannelModel } from '../../models/video/video-channel'
|
||||
|
||||
async function doesLocalVideoChannelNameExist (name: string, res: express.Response) {
|
||||
const videoChannel = await VideoChannelModel.loadLocalByNameAndPopulateAccount(name)
|
||||
|
@ -29,11 +29,10 @@ export {
|
|||
doesVideoChannelNameWithHostExist
|
||||
}
|
||||
|
||||
function processVideoChannelExist (videoChannel: MChannelAccountDefault, res: express.Response) {
|
||||
function processVideoChannelExist (videoChannel: MChannelBannerAccountDefault, res: express.Response) {
|
||||
if (!videoChannel) {
|
||||
res.status(HttpStatusCode.NOT_FOUND_404)
|
||||
.json({ error: 'Video channel not found' })
|
||||
.end()
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -66,25 +66,24 @@ async function doesVideoFileOfVideoExist (id: number, videoIdOrUUID: number | st
|
|||
}
|
||||
|
||||
async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAccountId, res: Response) {
|
||||
const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId)
|
||||
|
||||
if (videoChannel === null) {
|
||||
res.status(HttpStatusCode.BAD_REQUEST_400)
|
||||
.json({ error: 'Unknown video "video channel" for this instance.' })
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Don't check account id if the user can update any video
|
||||
if (user.hasRight(UserRight.UPDATE_ANY_VIDEO) === true) {
|
||||
const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId)
|
||||
if (videoChannel === null) {
|
||||
res.status(HttpStatusCode.BAD_REQUEST_400)
|
||||
.json({ error: 'Unknown video `video channel` on this instance.' })
|
||||
.end()
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.videoChannel = videoChannel
|
||||
return true
|
||||
}
|
||||
|
||||
const videoChannel = await VideoChannelModel.loadByIdAndAccount(channelId, user.Account.id)
|
||||
if (videoChannel === null) {
|
||||
if (videoChannel.Account.id !== user.Account.id) {
|
||||
res.status(HttpStatusCode.BAD_REQUEST_400)
|
||||
.json({ error: 'Unknown video `video channel` for this account.' })
|
||||
.end()
|
||||
.json({ error: 'Unknown video "video channel" for this account.' })
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -305,7 +305,7 @@ const CONSTRAINTS_FIELDS = {
|
|||
PUBLIC_KEY: { min: 10, max: 5000 }, // Length
|
||||
PRIVATE_KEY: { min: 10, max: 5000 }, // Length
|
||||
URL: { min: 3, max: 2000 }, // Length
|
||||
AVATAR: {
|
||||
IMAGE: {
|
||||
EXTNAME: [ '.png', '.jpeg', '.jpg', '.gif', '.webp' ],
|
||||
FILE_SIZE: {
|
||||
max: 2 * 1024 * 1024 // 2MB
|
||||
|
@ -466,6 +466,8 @@ const MIMETYPES = {
|
|||
IMAGE: {
|
||||
MIMETYPE_EXT: {
|
||||
'image/png': '.png',
|
||||
'image/gif': '.gif',
|
||||
'image/webp': '.webp',
|
||||
'image/jpg': '.jpg',
|
||||
'image/jpeg': '.jpg'
|
||||
},
|
||||
|
@ -605,9 +607,15 @@ const PREVIEWS_SIZE = {
|
|||
height: 480,
|
||||
minWidth: 400
|
||||
}
|
||||
const AVATARS_SIZE = {
|
||||
width: 120,
|
||||
height: 120
|
||||
const ACTOR_IMAGES_SIZE = {
|
||||
AVATARS: {
|
||||
width: 120,
|
||||
height: 120
|
||||
},
|
||||
BANNERS: {
|
||||
width: 1920,
|
||||
height: 384
|
||||
}
|
||||
}
|
||||
|
||||
const EMBED_SIZE = {
|
||||
|
@ -755,7 +763,7 @@ if (isTestInstance() === true) {
|
|||
ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL = 10 * 1000 // 10 seconds
|
||||
ACTIVITY_PUB.VIDEO_PLAYLIST_REFRESH_INTERVAL = 10 * 1000 // 10 seconds
|
||||
|
||||
CONSTRAINTS_FIELDS.ACTORS.AVATAR.FILE_SIZE.max = 100 * 1024 // 100KB
|
||||
CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max = 100 * 1024 // 100KB
|
||||
CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max = 400 * 1024 // 400KB
|
||||
|
||||
SCHEDULER_INTERVALS_MS.actorFollowScores = 1000
|
||||
|
@ -816,7 +824,7 @@ export {
|
|||
SEARCH_INDEX,
|
||||
HLS_REDUNDANCY_DIRECTORY,
|
||||
P2P_MEDIA_LOADER_PEER_VERSION,
|
||||
AVATARS_SIZE,
|
||||
ACTOR_IMAGES_SIZE,
|
||||
ACCEPT_HEADERS,
|
||||
BCRYPT_SALT_SIZE,
|
||||
TRACKER_RATE_LIMITS,
|
||||
|
|
|
@ -4,6 +4,7 @@ import { Op, Transaction } from 'sequelize'
|
|||
import { URL } from 'url'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { getServerActor } from '@server/models/application/application'
|
||||
import { ActorImageType } from '@shared/models'
|
||||
import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
|
||||
import { ActivityPubActor, ActivityPubActorType, ActivityPubOrderedCollection } from '../../../shared/models/activitypub'
|
||||
import { ActivityPubAttributedTo } from '../../../shared/models/activitypub/objects'
|
||||
|
@ -30,10 +31,10 @@ import {
|
|||
MActorAccountChannelId,
|
||||
MActorAccountChannelIdActor,
|
||||
MActorAccountId,
|
||||
MActorDefault,
|
||||
MActorFull,
|
||||
MActorFullActor,
|
||||
MActorId,
|
||||
MActorImages,
|
||||
MChannel
|
||||
} from '../../types/models'
|
||||
import { JobQueue } from '../job-queue'
|
||||
|
@ -168,42 +169,59 @@ async function updateActorInstance (actorInstance: ActorModel, attributes: Activ
|
|||
}
|
||||
}
|
||||
|
||||
type AvatarInfo = { name: string, onDisk: boolean, fileUrl: string }
|
||||
async function updateActorAvatarInstance (actor: MActorDefault, info: AvatarInfo, t: Transaction) {
|
||||
type AvatarInfo = { name: string, onDisk: boolean, fileUrl: string, type: ActorImageType }
|
||||
async function updateActorImageInstance (actor: MActorImages, info: AvatarInfo, t: Transaction) {
|
||||
if (!info.name) return actor
|
||||
|
||||
if (actor.Avatar) {
|
||||
const oldImageModel = info.type === ActorImageType.AVATAR
|
||||
? actor.Avatar
|
||||
: actor.Banner
|
||||
|
||||
if (oldImageModel) {
|
||||
// Don't update the avatar if the file URL did not change
|
||||
if (info.fileUrl && actor.Avatar.fileUrl === info.fileUrl) return actor
|
||||
if (info.fileUrl && oldImageModel.fileUrl === info.fileUrl) return actor
|
||||
|
||||
try {
|
||||
await actor.Avatar.destroy({ transaction: t })
|
||||
await oldImageModel.destroy({ transaction: t })
|
||||
} catch (err) {
|
||||
logger.error('Cannot remove old avatar of actor %s.', actor.url, { err })
|
||||
logger.error('Cannot remove old actor image of actor %s.', actor.url, { err })
|
||||
}
|
||||
}
|
||||
|
||||
const avatar = await ActorImageModel.create({
|
||||
const imageModel = await ActorImageModel.create({
|
||||
filename: info.name,
|
||||
onDisk: info.onDisk,
|
||||
fileUrl: info.fileUrl
|
||||
fileUrl: info.fileUrl,
|
||||
type: info.type
|
||||
}, { transaction: t })
|
||||
|
||||
actor.avatarId = avatar.id
|
||||
actor.Avatar = avatar
|
||||
if (info.type === ActorImageType.AVATAR) {
|
||||
actor.avatarId = imageModel.id
|
||||
actor.Avatar = imageModel
|
||||
} else {
|
||||
actor.bannerId = imageModel.id
|
||||
actor.Banner = imageModel
|
||||
}
|
||||
|
||||
return actor
|
||||
}
|
||||
|
||||
async function deleteActorAvatarInstance (actor: MActorDefault, t: Transaction) {
|
||||
async function deleteActorImageInstance (actor: MActorImages, type: ActorImageType, t: Transaction) {
|
||||
try {
|
||||
await actor.Avatar.destroy({ transaction: t })
|
||||
} catch (err) {
|
||||
logger.error('Cannot remove old avatar of actor %s.', actor.url, { err })
|
||||
}
|
||||
if (type === ActorImageType.AVATAR) {
|
||||
await actor.Avatar.destroy({ transaction: t })
|
||||
|
||||
actor.avatarId = null
|
||||
actor.Avatar = null
|
||||
actor.avatarId = null
|
||||
actor.Avatar = null
|
||||
} else {
|
||||
await actor.Banner.destroy({ transaction: t })
|
||||
|
||||
actor.bannerId = null
|
||||
actor.Banner = null
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Cannot remove old image of actor %s.', actor.url, { err })
|
||||
}
|
||||
|
||||
return actor
|
||||
}
|
||||
|
@ -219,9 +237,11 @@ async function fetchActorTotalItems (url: string) {
|
|||
}
|
||||
}
|
||||
|
||||
function getAvatarInfoIfExists (actorJSON: ActivityPubActor) {
|
||||
function getImageInfoIfExists (actorJSON: ActivityPubActor, type: ActorImageType) {
|
||||
const mimetypes = MIMETYPES.IMAGE
|
||||
const icon = actorJSON.icon
|
||||
const icon = type === ActorImageType.AVATAR
|
||||
? actorJSON.icon
|
||||
: actorJSON.image
|
||||
|
||||
if (!icon || icon.type !== 'Image' || !isActivityPubUrlValid(icon.url)) return undefined
|
||||
|
||||
|
@ -239,7 +259,8 @@ function getAvatarInfoIfExists (actorJSON: ActivityPubActor) {
|
|||
|
||||
return {
|
||||
name: uuidv4() + extension,
|
||||
fileUrl: icon.url
|
||||
fileUrl: icon.url,
|
||||
type
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -293,10 +314,22 @@ async function refreshActorIfNeeded <T extends MActorFull | MActorAccountChannel
|
|||
const avatarInfo = {
|
||||
name: result.avatar.name,
|
||||
fileUrl: result.avatar.fileUrl,
|
||||
onDisk: false
|
||||
onDisk: false,
|
||||
type: ActorImageType.AVATAR
|
||||
}
|
||||
|
||||
await updateActorAvatarInstance(actor, avatarInfo, t)
|
||||
await updateActorImageInstance(actor, avatarInfo, t)
|
||||
}
|
||||
|
||||
if (result.banner !== undefined) {
|
||||
const bannerInfo = {
|
||||
name: result.banner.name,
|
||||
fileUrl: result.banner.fileUrl,
|
||||
onDisk: false,
|
||||
type: ActorImageType.BANNER
|
||||
}
|
||||
|
||||
await updateActorImageInstance(actor, bannerInfo, t)
|
||||
}
|
||||
|
||||
// Force update
|
||||
|
@ -338,11 +371,11 @@ export {
|
|||
buildActorInstance,
|
||||
generateAndSaveActorKeys,
|
||||
fetchActorTotalItems,
|
||||
getAvatarInfoIfExists,
|
||||
getImageInfoIfExists,
|
||||
updateActorInstance,
|
||||
deleteActorAvatarInstance,
|
||||
deleteActorImageInstance,
|
||||
refreshActorIfNeeded,
|
||||
updateActorAvatarInstance,
|
||||
updateActorImageInstance,
|
||||
addFetchOutboxJob
|
||||
}
|
||||
|
||||
|
@ -381,12 +414,25 @@ function saveActorAndServerAndModelIfNotExist (
|
|||
const avatar = await ActorImageModel.create({
|
||||
filename: result.avatar.name,
|
||||
fileUrl: result.avatar.fileUrl,
|
||||
onDisk: false
|
||||
onDisk: false,
|
||||
type: ActorImageType.AVATAR
|
||||
}, { transaction: t })
|
||||
|
||||
actor.avatarId = avatar.id
|
||||
}
|
||||
|
||||
// Banner?
|
||||
if (result.banner) {
|
||||
const banner = await ActorImageModel.create({
|
||||
filename: result.banner.name,
|
||||
fileUrl: result.banner.fileUrl,
|
||||
onDisk: false,
|
||||
type: ActorImageType.BANNER
|
||||
}, { transaction: t })
|
||||
|
||||
actor.bannerId = banner.id
|
||||
}
|
||||
|
||||
// Force the actor creation, sometimes Sequelize skips the save() when it thinks the instance already exists
|
||||
// (which could be false in a retried query)
|
||||
const [ actorCreated, created ] = await ActorModel.findOrCreate<MActorFullActor>({
|
||||
|
@ -440,6 +486,10 @@ type FetchRemoteActorResult = {
|
|||
name: string
|
||||
fileUrl: string
|
||||
}
|
||||
banner?: {
|
||||
name: string
|
||||
fileUrl: string
|
||||
}
|
||||
attributedTo: ActivityPubAttributedTo[]
|
||||
}
|
||||
async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: number, result: FetchRemoteActorResult }> {
|
||||
|
@ -479,7 +529,8 @@ async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: numbe
|
|||
: null
|
||||
})
|
||||
|
||||
const avatarInfo = await getAvatarInfoIfExists(actorJSON)
|
||||
const avatarInfo = getImageInfoIfExists(actorJSON, ActorImageType.AVATAR)
|
||||
const bannerInfo = getImageInfoIfExists(actorJSON, ActorImageType.BANNER)
|
||||
|
||||
const name = actorJSON.name || actorJSON.preferredUsername
|
||||
return {
|
||||
|
@ -488,6 +539,7 @@ async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: numbe
|
|||
actor,
|
||||
name,
|
||||
avatar: avatarInfo,
|
||||
banner: bannerInfo,
|
||||
summary: actorJSON.summary,
|
||||
support: actorJSON.support,
|
||||
playlists: actorJSON.playlists,
|
||||
|
|
|
@ -7,7 +7,7 @@ import { VideoModel } from '../../../models/video/video'
|
|||
import { VideoCommentModel } from '../../../models/video/video-comment'
|
||||
import { VideoPlaylistModel } from '../../../models/video/video-playlist'
|
||||
import { APProcessorOptions } from '../../../types/activitypub-processor.model'
|
||||
import { MAccountActor, MActor, MActorSignature, MChannelActor, MChannelActorAccountActor, MCommentOwnerVideo } from '../../../types/models'
|
||||
import { MAccountActor, MActor, MActorSignature, MChannelActor, MCommentOwnerVideo } from '../../../types/models'
|
||||
import { markCommentAsDeleted } from '../../video-comment'
|
||||
import { forwardVideoRelatedActivity } from '../send/utils'
|
||||
|
||||
|
@ -30,9 +30,7 @@ async function processDeleteActivity (options: APProcessorOptions<ActivityDelete
|
|||
} else if (byActorFull.type === 'Group') {
|
||||
if (!byActorFull.VideoChannel) throw new Error('Actor ' + byActorFull.url + ' is a group but we cannot find it in database.')
|
||||
|
||||
const channelToDelete = byActorFull.VideoChannel as MChannelActorAccountActor
|
||||
channelToDelete.Actor = byActorFull
|
||||
|
||||
const channelToDelete = Object.assign({}, byActorFull.VideoChannel, { Actor: byActorFull })
|
||||
return retryTransactionWrapper(processDeleteVideoChannel, channelToDelete)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import { sequelizeTypescript } from '../../../initializers/database'
|
|||
import { AccountModel } from '../../../models/account/account'
|
||||
import { ActorModel } from '../../../models/activitypub/actor'
|
||||
import { VideoChannelModel } from '../../../models/video/video-channel'
|
||||
import { getAvatarInfoIfExists, updateActorAvatarInstance, updateActorInstance } from '../actor'
|
||||
import { getImageInfoIfExists, updateActorImageInstance, updateActorInstance } from '../actor'
|
||||
import { getOrCreateVideoAndAccountAndChannel, getOrCreateVideoChannelFromVideoObject, updateVideoFromAP } from '../videos'
|
||||
import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos'
|
||||
import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file'
|
||||
|
@ -17,6 +17,7 @@ import { createOrUpdateVideoPlaylist } from '../playlist'
|
|||
import { APProcessorOptions } from '../../../types/activitypub-processor.model'
|
||||
import { MActorSignature, MAccountIdActor } from '../../../types/models'
|
||||
import { isRedundancyAccepted } from '@server/lib/redundancy'
|
||||
import { ActorImageType } from '@shared/models'
|
||||
|
||||
async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) {
|
||||
const { activity, byActor } = options
|
||||
|
@ -119,7 +120,8 @@ async function processUpdateActor (actor: ActorModel, activity: ActivityUpdate)
|
|||
let accountOrChannelFieldsSave: object
|
||||
|
||||
// Fetch icon?
|
||||
const avatarInfo = await getAvatarInfoIfExists(actorAttributesToUpdate)
|
||||
const avatarInfo = getImageInfoIfExists(actorAttributesToUpdate, ActorImageType.AVATAR)
|
||||
const bannerInfo = getImageInfoIfExists(actorAttributesToUpdate, ActorImageType.BANNER)
|
||||
|
||||
try {
|
||||
await sequelizeTypescript.transaction(async t => {
|
||||
|
@ -132,10 +134,12 @@ async function processUpdateActor (actor: ActorModel, activity: ActivityUpdate)
|
|||
|
||||
await updateActorInstance(actor, actorAttributesToUpdate)
|
||||
|
||||
if (avatarInfo !== undefined) {
|
||||
const avatarOptions = Object.assign({}, avatarInfo, { onDisk: false })
|
||||
for (const imageInfo of [ avatarInfo, bannerInfo ]) {
|
||||
if (!imageInfo) continue
|
||||
|
||||
await updateActorAvatarInstance(actor, avatarOptions, t)
|
||||
const imageOptions = Object.assign({}, imageInfo, { onDisk: false })
|
||||
|
||||
await updateActorImageInstance(actor, imageOptions, t)
|
||||
}
|
||||
|
||||
await actor.save({ transaction: t })
|
||||
|
|
|
@ -3,50 +3,57 @@ import { queue } from 'async'
|
|||
import * as LRUCache from 'lru-cache'
|
||||
import { extname, join } from 'path'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { ActorImageType } from '@shared/models'
|
||||
import { retryTransactionWrapper } from '../helpers/database-utils'
|
||||
import { processImage } from '../helpers/image-utils'
|
||||
import { downloadImage } from '../helpers/requests'
|
||||
import { CONFIG } from '../initializers/config'
|
||||
import { AVATARS_SIZE, LRU_CACHE, QUEUE_CONCURRENCY } from '../initializers/constants'
|
||||
import { ACTOR_IMAGES_SIZE, LRU_CACHE, QUEUE_CONCURRENCY } from '../initializers/constants'
|
||||
import { sequelizeTypescript } from '../initializers/database'
|
||||
import { MAccountDefault, MChannelDefault } from '../types/models'
|
||||
import { deleteActorAvatarInstance, updateActorAvatarInstance } from './activitypub/actor'
|
||||
import { deleteActorImageInstance, updateActorImageInstance } from './activitypub/actor'
|
||||
import { sendUpdateActor } from './activitypub/send'
|
||||
|
||||
async function updateLocalActorAvatarFile (
|
||||
async function updateLocalActorImageFile (
|
||||
accountOrChannel: MAccountDefault | MChannelDefault,
|
||||
avatarPhysicalFile: Express.Multer.File
|
||||
imagePhysicalFile: Express.Multer.File,
|
||||
type: ActorImageType
|
||||
) {
|
||||
const extension = extname(avatarPhysicalFile.filename)
|
||||
const imageSize = type === ActorImageType.AVATAR
|
||||
? ACTOR_IMAGES_SIZE.AVATARS
|
||||
: ACTOR_IMAGES_SIZE.BANNERS
|
||||
|
||||
const avatarName = uuidv4() + extension
|
||||
const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, avatarName)
|
||||
await processImage(avatarPhysicalFile.path, destination, AVATARS_SIZE)
|
||||
const extension = extname(imagePhysicalFile.filename)
|
||||
|
||||
const imageName = uuidv4() + extension
|
||||
const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, imageName)
|
||||
await processImage(imagePhysicalFile.path, destination, imageSize)
|
||||
|
||||
return retryTransactionWrapper(() => {
|
||||
return sequelizeTypescript.transaction(async t => {
|
||||
const avatarInfo = {
|
||||
name: avatarName,
|
||||
const actorImageInfo = {
|
||||
name: imageName,
|
||||
fileUrl: null,
|
||||
type,
|
||||
onDisk: true
|
||||
}
|
||||
|
||||
const updatedActor = await updateActorAvatarInstance(accountOrChannel.Actor, avatarInfo, t)
|
||||
const updatedActor = await updateActorImageInstance(accountOrChannel.Actor, actorImageInfo, t)
|
||||
await updatedActor.save({ transaction: t })
|
||||
|
||||
await sendUpdateActor(accountOrChannel, t)
|
||||
|
||||
return updatedActor.Avatar
|
||||
return type === ActorImageType.AVATAR
|
||||
? updatedActor.Avatar
|
||||
: updatedActor.Banner
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function deleteLocalActorAvatarFile (
|
||||
accountOrChannel: MAccountDefault | MChannelDefault
|
||||
) {
|
||||
async function deleteLocalActorImageFile (accountOrChannel: MAccountDefault | MChannelDefault, type: ActorImageType) {
|
||||
return retryTransactionWrapper(() => {
|
||||
return sequelizeTypescript.transaction(async t => {
|
||||
const updatedActor = await deleteActorAvatarInstance(accountOrChannel.Actor, t)
|
||||
const updatedActor = await deleteActorImageInstance(accountOrChannel.Actor, type, t)
|
||||
await updatedActor.save({ transaction: t })
|
||||
|
||||
await sendUpdateActor(accountOrChannel, t)
|
||||
|
@ -56,10 +63,14 @@ async function deleteLocalActorAvatarFile (
|
|||
})
|
||||
}
|
||||
|
||||
type DownloadImageQueueTask = { fileUrl: string, filename: string }
|
||||
type DownloadImageQueueTask = { fileUrl: string, filename: string, type: ActorImageType }
|
||||
|
||||
const downloadImageQueue = queue<DownloadImageQueueTask, Error>((task, cb) => {
|
||||
downloadImage(task.fileUrl, CONFIG.STORAGE.ACTOR_IMAGES, task.filename, AVATARS_SIZE)
|
||||
const size = task.type === ActorImageType.AVATAR
|
||||
? ACTOR_IMAGES_SIZE.AVATARS
|
||||
: ACTOR_IMAGES_SIZE.BANNERS
|
||||
|
||||
downloadImage(task.fileUrl, CONFIG.STORAGE.ACTOR_IMAGES, task.filename, size)
|
||||
.then(() => cb())
|
||||
.catch(err => cb(err))
|
||||
}, QUEUE_CONCURRENCY.ACTOR_PROCESS_IMAGE)
|
||||
|
@ -79,7 +90,7 @@ const actorImagePathUnsafeCache = new LRUCache<string, string>({ max: LRU_CACHE.
|
|||
|
||||
export {
|
||||
actorImagePathUnsafeCache,
|
||||
updateLocalActorAvatarFile,
|
||||
deleteLocalActorAvatarFile,
|
||||
updateLocalActorImageFile,
|
||||
deleteLocalActorImageFile,
|
||||
pushActorImageProcessInQueue
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import { logger } from '../helpers/logger'
|
|||
import { CONFIG } from '../initializers/config'
|
||||
import {
|
||||
ACCEPT_HEADERS,
|
||||
AVATARS_SIZE,
|
||||
ACTOR_IMAGES_SIZE,
|
||||
CUSTOM_HTML_TAG_COMMENTS,
|
||||
EMBED_SIZE,
|
||||
FILES_CONTENT_HASH,
|
||||
|
@ -246,8 +246,8 @@ class ClientHtml {
|
|||
|
||||
const image = {
|
||||
url: entity.Actor.getAvatarUrl(),
|
||||
width: AVATARS_SIZE.width,
|
||||
height: AVATARS_SIZE.height
|
||||
width: ACTOR_IMAGES_SIZE.AVATARS.width,
|
||||
height: ACTOR_IMAGES_SIZE.AVATARS.height
|
||||
}
|
||||
|
||||
const ogType = 'website'
|
||||
|
|
|
@ -405,7 +405,7 @@ class Emailer {
|
|||
async addVideoAutoBlacklistModeratorsNotification (to: string[], videoBlacklist: MVideoBlacklistLightVideo) {
|
||||
const videoAutoBlacklistUrl = WEBSERVER.URL + '/admin/moderation/video-auto-blacklist/list'
|
||||
const videoUrl = WEBSERVER.URL + videoBlacklist.Video.getWatchStaticPath()
|
||||
const channel = (await VideoChannelModel.loadByIdAndPopulateAccount(videoBlacklist.Video.channelId)).toFormattedSummaryJSON()
|
||||
const channel = (await VideoChannelModel.loadAndPopulateAccount(videoBlacklist.Video.channelId)).toFormattedSummaryJSON()
|
||||
|
||||
const emailPayload: EmailPayload = {
|
||||
template: 'video-auto-blacklist-new',
|
||||
|
|
|
@ -3,18 +3,12 @@ import { v4 as uuidv4 } from 'uuid'
|
|||
import { VideoChannelCreate } from '../../shared/models'
|
||||
import { VideoModel } from '../models/video/video'
|
||||
import { VideoChannelModel } from '../models/video/video-channel'
|
||||
import { MAccountId, MChannelDefault, MChannelId } from '../types/models'
|
||||
import { MAccountId, MChannelId } from '../types/models'
|
||||
import { buildActorInstance } from './activitypub/actor'
|
||||
import { getLocalVideoChannelActivityPubUrl } from './activitypub/url'
|
||||
import { federateVideoIfNeeded } from './activitypub/videos'
|
||||
|
||||
type CustomVideoChannelModelAccount <T extends MAccountId> = MChannelDefault & { Account?: T }
|
||||
|
||||
async function createLocalVideoChannel <T extends MAccountId> (
|
||||
videoChannelInfo: VideoChannelCreate,
|
||||
account: T,
|
||||
t: Sequelize.Transaction
|
||||
): Promise<CustomVideoChannelModelAccount<T>> {
|
||||
async function createLocalVideoChannel (videoChannelInfo: VideoChannelCreate, account: MAccountId, t: Sequelize.Transaction) {
|
||||
const uuid = uuidv4()
|
||||
const url = getLocalVideoChannelActivityPubUrl(videoChannelInfo.name)
|
||||
const actorInstance = buildActorInstance('Group', url, videoChannelInfo.name, uuid)
|
||||
|
@ -32,13 +26,11 @@ async function createLocalVideoChannel <T extends MAccountId> (
|
|||
const videoChannel = new VideoChannelModel(videoChannelData)
|
||||
|
||||
const options = { transaction: t }
|
||||
const videoChannelCreated: CustomVideoChannelModelAccount<T> = await videoChannel.save(options) as MChannelDefault
|
||||
const videoChannelCreated = await videoChannel.save(options)
|
||||
|
||||
// Do not forget to add Account/Actor information to the created video channel
|
||||
videoChannelCreated.Account = account
|
||||
videoChannelCreated.Actor = actorInstanceCreated
|
||||
|
||||
// No need to seed this empty video channel to followers
|
||||
// No need to send this empty video channel to followers
|
||||
return videoChannelCreated
|
||||
}
|
||||
|
||||
|
|
|
@ -6,21 +6,25 @@ import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
|
|||
import { logger } from '../../helpers/logger'
|
||||
import { cleanUpReqFiles } from '../../helpers/express-utils'
|
||||
|
||||
const updateAvatarValidator = [
|
||||
body('avatarfile').custom((value, { req }) => isAvatarFile(req.files)).withMessage(
|
||||
const updateActorImageValidatorFactory = (fieldname: string) => ([
|
||||
body(fieldname).custom((value, { req }) => isAvatarFile(req.files)).withMessage(
|
||||
'This file is not supported or too large. Please, make sure it is of the following type : ' +
|
||||
CONSTRAINTS_FIELDS.ACTORS.AVATAR.EXTNAME.join(', ')
|
||||
CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME.join(', ')
|
||||
),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
logger.debug('Checking updateAvatarValidator parameters', { files: req.files })
|
||||
logger.debug('Checking updateActorImageValidator parameters', { files: req.files })
|
||||
|
||||
if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
])
|
||||
|
||||
const updateAvatarValidator = updateActorImageValidatorFactory('avatarfile')
|
||||
const updateBannerValidator = updateActorImageValidatorFactory('bannerfile')
|
||||
|
||||
export {
|
||||
updateAvatarValidator
|
||||
updateAvatarValidator,
|
||||
updateBannerValidator
|
||||
}
|
||||
|
|
|
@ -68,7 +68,6 @@ const removeFollowingValidator = [
|
|||
.json({
|
||||
error: `Following ${req.params.host} not found.`
|
||||
})
|
||||
.end()
|
||||
}
|
||||
|
||||
res.locals.follow = follow
|
||||
|
|
|
@ -73,13 +73,11 @@ const videoChannelsUpdateValidator = [
|
|||
if (res.locals.videoChannel.Actor.isOwned() === false) {
|
||||
return res.status(HttpStatusCode.FORBIDDEN_403)
|
||||
.json({ error: 'Cannot update video channel of another server' })
|
||||
.end()
|
||||
}
|
||||
|
||||
if (res.locals.videoChannel.Account.userId !== res.locals.oauth.token.User.id) {
|
||||
return res.status(HttpStatusCode.FORBIDDEN_403)
|
||||
.json({ error: 'Cannot update video channel of another user' })
|
||||
.end()
|
||||
}
|
||||
|
||||
return next()
|
||||
|
|
|
@ -248,13 +248,6 @@ export class ActorFollowModel extends Model {
|
|||
}
|
||||
|
||||
return ActorFollowModel.findOne(query)
|
||||
.then(result => {
|
||||
if (result?.ActorFollowing.VideoChannel) {
|
||||
result.ActorFollowing.VideoChannel.Actor = result.ActorFollowing
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
static listSubscribedIn (actorId: number, targets: { name: string, host?: string }[]): Promise<MActorFollowFollowingHost[]> {
|
||||
|
|
|
@ -29,11 +29,19 @@ import {
|
|||
isActorPublicKeyValid
|
||||
} from '../../helpers/custom-validators/activitypub/actor'
|
||||
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
|
||||
import { ACTIVITY_PUB, ACTIVITY_PUB_ACTOR_TYPES, CONSTRAINTS_FIELDS, SERVER_ACTOR_NAME, WEBSERVER } from '../../initializers/constants'
|
||||
import {
|
||||
ACTIVITY_PUB,
|
||||
ACTIVITY_PUB_ACTOR_TYPES,
|
||||
CONSTRAINTS_FIELDS,
|
||||
MIMETYPES,
|
||||
SERVER_ACTOR_NAME,
|
||||
WEBSERVER
|
||||
} from '../../initializers/constants'
|
||||
import {
|
||||
MActor,
|
||||
MActorAccountChannelId,
|
||||
MActorAP,
|
||||
MActorAPAccount,
|
||||
MActorAPChannel,
|
||||
MActorFormattable,
|
||||
MActorFull,
|
||||
MActorHost,
|
||||
|
@ -104,6 +112,11 @@ export const unusedActorAttributesForAPI = [
|
|||
model: ActorImageModel,
|
||||
as: 'Avatar',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
model: ActorImageModel,
|
||||
as: 'Banner',
|
||||
required: false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -531,29 +544,46 @@ export class ActorModel extends Model {
|
|||
toFormattedJSON (this: MActorFormattable) {
|
||||
const base = this.toFormattedSummaryJSON()
|
||||
|
||||
let banner: ActorImage = null
|
||||
if (this.bannerId) {
|
||||
banner = this.Banner.toFormattedJSON()
|
||||
}
|
||||
|
||||
return Object.assign(base, {
|
||||
id: this.id,
|
||||
hostRedundancyAllowed: this.getRedundancyAllowed(),
|
||||
followingCount: this.followingCount,
|
||||
followersCount: this.followersCount,
|
||||
banner,
|
||||
createdAt: this.createdAt,
|
||||
updatedAt: this.updatedAt
|
||||
})
|
||||
}
|
||||
|
||||
toActivityPubObject (this: MActorAP, name: string) {
|
||||
toActivityPubObject (this: MActorAPChannel | MActorAPAccount, name: string) {
|
||||
let icon: ActivityIconObject
|
||||
let image: ActivityIconObject
|
||||
|
||||
if (this.avatarId) {
|
||||
const extension = extname(this.Avatar.filename)
|
||||
|
||||
icon = {
|
||||
type: 'Image',
|
||||
mediaType: extension === '.png' ? 'image/png' : 'image/jpeg',
|
||||
mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension],
|
||||
url: this.getAvatarUrl()
|
||||
}
|
||||
}
|
||||
|
||||
if (this.bannerId) {
|
||||
const extension = extname((this as MActorAPChannel).Banner.filename)
|
||||
|
||||
image = {
|
||||
type: 'Image',
|
||||
mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension],
|
||||
url: this.getBannerUrl()
|
||||
}
|
||||
}
|
||||
|
||||
const json = {
|
||||
type: this.type,
|
||||
id: this.url,
|
||||
|
@ -573,7 +603,8 @@ export class ActorModel extends Model {
|
|||
owner: this.url,
|
||||
publicKeyPem: this.publicKey
|
||||
},
|
||||
icon
|
||||
icon,
|
||||
image
|
||||
}
|
||||
|
||||
return activityPubContextify(json)
|
||||
|
@ -643,6 +674,12 @@ export class ActorModel extends Model {
|
|||
return WEBSERVER.URL + this.Avatar.getStaticPath()
|
||||
}
|
||||
|
||||
getBannerUrl () {
|
||||
if (!this.bannerId) return undefined
|
||||
|
||||
return WEBSERVER.URL + this.Banner.getStaticPath()
|
||||
}
|
||||
|
||||
isOutdated () {
|
||||
if (this.isOwned()) return false
|
||||
|
||||
|
|
|
@ -28,10 +28,9 @@ import {
|
|||
import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
|
||||
import { sendDeleteActor } from '../../lib/activitypub/send'
|
||||
import {
|
||||
MChannelAccountDefault,
|
||||
MChannelActor,
|
||||
MChannelActorAccountDefaultVideos,
|
||||
MChannelAP,
|
||||
MChannelBannerAccountDefault,
|
||||
MChannelFormattable,
|
||||
MChannelSummaryFormattable
|
||||
} from '../../types/models/video'
|
||||
|
@ -49,6 +48,7 @@ export enum ScopeNames {
|
|||
SUMMARY = 'SUMMARY',
|
||||
WITH_ACCOUNT = 'WITH_ACCOUNT',
|
||||
WITH_ACTOR = 'WITH_ACTOR',
|
||||
WITH_ACTOR_BANNER = 'WITH_ACTOR_BANNER',
|
||||
WITH_VIDEOS = 'WITH_VIDEOS',
|
||||
WITH_STATS = 'WITH_STATS'
|
||||
}
|
||||
|
@ -168,6 +168,20 @@ export type SummaryOptions = {
|
|||
ActorModel
|
||||
]
|
||||
},
|
||||
[ScopeNames.WITH_ACTOR_BANNER]: {
|
||||
include: [
|
||||
{
|
||||
model: ActorModel,
|
||||
include: [
|
||||
{
|
||||
model: ActorImageModel,
|
||||
required: false,
|
||||
as: 'Banner'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
[ScopeNames.WITH_VIDEOS]: {
|
||||
include: [
|
||||
VideoModel
|
||||
|
@ -442,7 +456,7 @@ export class VideoChannelModel extends Model {
|
|||
where
|
||||
}
|
||||
|
||||
const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR ]
|
||||
const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR_BANNER ]
|
||||
|
||||
if (options.withStats === true) {
|
||||
scopes.push({
|
||||
|
@ -458,32 +472,13 @@ export class VideoChannelModel extends Model {
|
|||
})
|
||||
}
|
||||
|
||||
static loadByIdAndPopulateAccount (id: number): Promise<MChannelAccountDefault> {
|
||||
static loadAndPopulateAccount (id: number): Promise<MChannelBannerAccountDefault> {
|
||||
return VideoChannelModel.unscoped()
|
||||
.scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
|
||||
.scope([ ScopeNames.WITH_ACTOR_BANNER, ScopeNames.WITH_ACCOUNT ])
|
||||
.findByPk(id)
|
||||
}
|
||||
|
||||
static loadByIdAndAccount (id: number, accountId: number): Promise<MChannelAccountDefault> {
|
||||
const query = {
|
||||
where: {
|
||||
id,
|
||||
accountId
|
||||
}
|
||||
}
|
||||
|
||||
return VideoChannelModel.unscoped()
|
||||
.scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
|
||||
.findOne(query)
|
||||
}
|
||||
|
||||
static loadAndPopulateAccount (id: number): Promise<MChannelAccountDefault> {
|
||||
return VideoChannelModel.unscoped()
|
||||
.scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
|
||||
.findByPk(id)
|
||||
}
|
||||
|
||||
static loadByUrlAndPopulateAccount (url: string): Promise<MChannelAccountDefault> {
|
||||
static loadByUrlAndPopulateAccount (url: string): Promise<MChannelBannerAccountDefault> {
|
||||
const query = {
|
||||
include: [
|
||||
{
|
||||
|
@ -491,7 +486,14 @@ export class VideoChannelModel extends Model {
|
|||
required: true,
|
||||
where: {
|
||||
url
|
||||
}
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: ActorImageModel,
|
||||
required: false,
|
||||
as: 'Banner'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -509,7 +511,7 @@ export class VideoChannelModel extends Model {
|
|||
return VideoChannelModel.loadByNameAndHostAndPopulateAccount(name, host)
|
||||
}
|
||||
|
||||
static loadLocalByNameAndPopulateAccount (name: string): Promise<MChannelAccountDefault> {
|
||||
static loadLocalByNameAndPopulateAccount (name: string): Promise<MChannelBannerAccountDefault> {
|
||||
const query = {
|
||||
include: [
|
||||
{
|
||||
|
@ -518,17 +520,24 @@ export class VideoChannelModel extends Model {
|
|||
where: {
|
||||
preferredUsername: name,
|
||||
serverId: null
|
||||
}
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: ActorImageModel,
|
||||
required: false,
|
||||
as: 'Banner'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return VideoChannelModel.unscoped()
|
||||
.scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
|
||||
.scope([ ScopeNames.WITH_ACCOUNT ])
|
||||
.findOne(query)
|
||||
}
|
||||
|
||||
static loadByNameAndHostAndPopulateAccount (name: string, host: string): Promise<MChannelAccountDefault> {
|
||||
static loadByNameAndHostAndPopulateAccount (name: string, host: string): Promise<MChannelBannerAccountDefault> {
|
||||
const query = {
|
||||
include: [
|
||||
{
|
||||
|
@ -542,6 +551,11 @@ export class VideoChannelModel extends Model {
|
|||
model: ServerModel,
|
||||
required: true,
|
||||
where: { host }
|
||||
},
|
||||
{
|
||||
model: ActorImageModel,
|
||||
required: false,
|
||||
as: 'Banner'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -549,22 +563,10 @@ export class VideoChannelModel extends Model {
|
|||
}
|
||||
|
||||
return VideoChannelModel.unscoped()
|
||||
.scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
|
||||
.scope([ ScopeNames.WITH_ACCOUNT ])
|
||||
.findOne(query)
|
||||
}
|
||||
|
||||
static loadAndPopulateAccountAndVideos (id: number): Promise<MChannelActorAccountDefaultVideos> {
|
||||
const options = {
|
||||
include: [
|
||||
VideoModel
|
||||
]
|
||||
}
|
||||
|
||||
return VideoChannelModel.unscoped()
|
||||
.scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_VIDEOS ])
|
||||
.findByPk(id, options)
|
||||
}
|
||||
|
||||
toFormattedSummaryJSON (this: MChannelSummaryFormattable): VideoChannelSummary {
|
||||
const actor = this.Actor.toFormattedSummaryJSON()
|
||||
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import { FunctionProperties, PickWith } from '@shared/core-utils'
|
||||
import { AccountModel } from '../../../models/account/account'
|
||||
import { MChannelDefault } from '../video/video-channels'
|
||||
import { MAccountBlocklistId } from './account-blocklist'
|
||||
import {
|
||||
MActor,
|
||||
MActorAP,
|
||||
MActorAPAccount,
|
||||
MActorAPI,
|
||||
MActorAudience,
|
||||
MActorDefault,
|
||||
|
@ -13,9 +16,6 @@ import {
|
|||
MActorSummaryFormattable,
|
||||
MActorUrl
|
||||
} from './actor'
|
||||
import { FunctionProperties, PickWith } from '@shared/core-utils'
|
||||
import { MAccountBlocklistId } from './account-blocklist'
|
||||
import { MChannelDefault } from '../video/video-channels'
|
||||
|
||||
type Use<K extends keyof AccountModel, M> = PickWith<AccountModel, K, M>
|
||||
|
||||
|
@ -106,4 +106,4 @@ export type MAccountFormattable =
|
|||
|
||||
export type MAccountAP =
|
||||
Pick<MAccount, 'name' | 'description'> &
|
||||
Use<'Actor', MActorAP>
|
||||
Use<'Actor', MActorAPAccount>
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
import { PickWith } from '@shared/core-utils'
|
||||
import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
|
||||
import {
|
||||
MActor,
|
||||
MActorChannelAccountActor,
|
||||
MActorDefault,
|
||||
MActorDefaultAccountChannel,
|
||||
MActorDefaultChannelId,
|
||||
MActorFormattable,
|
||||
MActorHost,
|
||||
MActorUsername
|
||||
} from './actor'
|
||||
import { PickWith } from '@shared/core-utils'
|
||||
import { ActorModel } from '@server/models/activitypub/actor'
|
||||
import { MChannelDefault } from '../video/video-channels'
|
||||
|
||||
type Use<K extends keyof ActorFollowModel, M> = PickWith<ActorFollowModel, K, M>
|
||||
|
||||
|
@ -47,14 +46,10 @@ export type MActorFollowFull =
|
|||
|
||||
// For subscriptions
|
||||
|
||||
type SubscriptionFollowing =
|
||||
MActorDefault &
|
||||
PickWith<ActorModel, 'VideoChannel', MChannelDefault>
|
||||
|
||||
export type MActorFollowActorsDefaultSubscription =
|
||||
MActorFollow &
|
||||
Use<'ActorFollower', MActorDefault> &
|
||||
Use<'ActorFollowing', SubscriptionFollowing>
|
||||
Use<'ActorFollowing', MActorDefaultChannelId>
|
||||
|
||||
export type MActorFollowSubscriptions =
|
||||
MActorFollow &
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
|
||||
import { FunctionProperties, PickWith, PickWithOpt } from '@shared/core-utils'
|
||||
import { ActorModel } from '../../../models/activitypub/actor'
|
||||
import { MServer, MServerHost, MServerHostBlocks, MServerRedundancyAllowed } from '../server'
|
||||
|
@ -6,6 +7,7 @@ import { MAccount, MAccountDefault, MAccountId, MAccountIdActor } from './accoun
|
|||
import { MActorImage, MActorImageFormattable } from './actor-image'
|
||||
|
||||
type Use<K extends keyof ActorModel, M> = PickWith<ActorModel, K, M>
|
||||
type UseOpt<K extends keyof ActorModel, M> = PickWithOpt<ActorModel, K, M>
|
||||
|
||||
// ############################################################################
|
||||
|
||||
|
@ -75,11 +77,26 @@ export type MActorServer =
|
|||
|
||||
// Complex actor associations
|
||||
|
||||
export type MActorImages =
|
||||
MActor &
|
||||
Use<'Avatar', MActorImage> &
|
||||
UseOpt<'Banner', MActorImage>
|
||||
|
||||
export type MActorDefault =
|
||||
MActor &
|
||||
Use<'Server', MServer> &
|
||||
Use<'Avatar', MActorImage>
|
||||
|
||||
export type MActorDefaultChannelId =
|
||||
MActorDefault &
|
||||
Use<'VideoChannel', MChannelId>
|
||||
|
||||
export type MActorDefaultBanner =
|
||||
MActor &
|
||||
Use<'Server', MServer> &
|
||||
Use<'Avatar', MActorImage> &
|
||||
Use<'Banner', MActorImage>
|
||||
|
||||
// Actor with channel that is associated to an account and its actor
|
||||
// Actor -> VideoChannel -> Account -> Actor
|
||||
export type MActorChannelAccountActor =
|
||||
|
@ -90,6 +107,7 @@ export type MActorFull =
|
|||
MActor &
|
||||
Use<'Server', MServer> &
|
||||
Use<'Avatar', MActorImage> &
|
||||
Use<'Banner', MActorImage> &
|
||||
Use<'Account', MAccount> &
|
||||
Use<'VideoChannel', MChannelAccountActor>
|
||||
|
||||
|
@ -98,6 +116,7 @@ export type MActorFullActor =
|
|||
MActor &
|
||||
Use<'Server', MServer> &
|
||||
Use<'Avatar', MActorImage> &
|
||||
Use<'Banner', MActorImage> &
|
||||
Use<'Account', MAccountDefault> &
|
||||
Use<'VideoChannel', MChannelAccountDefault>
|
||||
|
||||
|
@ -131,9 +150,17 @@ export type MActorSummaryFormattable =
|
|||
|
||||
export type MActorFormattable =
|
||||
MActorSummaryFormattable &
|
||||
Pick<MActor, 'id' | 'followingCount' | 'followersCount' | 'createdAt' | 'updatedAt'> &
|
||||
Use<'Server', MServerHost & Partial<Pick<MServer, 'redundancyAllowed'>>>
|
||||
Pick<MActor, 'id' | 'followingCount' | 'followersCount' | 'createdAt' | 'updatedAt' | 'bannerId' | 'avatarId'> &
|
||||
Use<'Server', MServerHost & Partial<Pick<MServer, 'redundancyAllowed'>>> &
|
||||
UseOpt<'Banner', MActorImageFormattable>
|
||||
|
||||
export type MActorAP =
|
||||
type MActorAPBase =
|
||||
MActor &
|
||||
Use<'Avatar', MActorImage>
|
||||
|
||||
export type MActorAPAccount =
|
||||
MActorAPBase
|
||||
|
||||
export type MActorAPChannel =
|
||||
MActorAPBase &
|
||||
Use<'Banner', MActorImage>
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { UserModel } from '../../../models/account/user'
|
||||
import { AccountModel } from '@server/models/account/account'
|
||||
import { MVideoPlaylist } from '@server/types/models'
|
||||
import { PickWith, PickWithOpt } from '@shared/core-utils'
|
||||
import { UserModel } from '../../../models/account/user'
|
||||
import {
|
||||
MAccount,
|
||||
MAccountDefault,
|
||||
|
@ -9,10 +11,8 @@ import {
|
|||
MAccountIdActorId,
|
||||
MAccountUrl
|
||||
} from '../account'
|
||||
import { MNotificationSetting, MNotificationSettingFormattable } from './user-notification-setting'
|
||||
import { AccountModel } from '@server/models/account/account'
|
||||
import { MChannelFormattable } from '../video/video-channels'
|
||||
import { MVideoPlaylist } from '@server/types/models'
|
||||
import { MNotificationSetting, MNotificationSettingFormattable } from './user-notification-setting'
|
||||
|
||||
type Use<K extends keyof UserModel, M> = PickWith<UserModel, K, M>
|
||||
|
||||
|
|
|
@ -12,15 +12,17 @@ import {
|
|||
MAccountUserId,
|
||||
MActor,
|
||||
MActorAccountChannelId,
|
||||
MActorAP,
|
||||
MActorAPChannel,
|
||||
MActorAPI,
|
||||
MActorDefault,
|
||||
MActorDefaultBanner,
|
||||
MActorDefaultLight,
|
||||
MActorFormattable,
|
||||
MActorHost,
|
||||
MActorLight,
|
||||
MActorSummary,
|
||||
MActorSummaryFormattable, MActorUrl
|
||||
MActorSummaryFormattable,
|
||||
MActorUrl
|
||||
} from '../account'
|
||||
import { MVideo } from './video'
|
||||
|
||||
|
@ -55,14 +57,14 @@ export type MChannelDefault =
|
|||
MChannel &
|
||||
Use<'Actor', MActorDefault>
|
||||
|
||||
export type MChannelBannerDefault =
|
||||
MChannel &
|
||||
Use<'Actor', MActorDefaultBanner>
|
||||
|
||||
// ############################################################################
|
||||
|
||||
// Not all association attributes
|
||||
|
||||
export type MChannelLight =
|
||||
MChannel &
|
||||
Use<'Actor', MActorDefaultLight>
|
||||
|
||||
export type MChannelActorLight =
|
||||
MChannel &
|
||||
Use<'Actor', MActorLight>
|
||||
|
@ -84,29 +86,23 @@ export type MChannelAccountActor =
|
|||
MChannel &
|
||||
Use<'Account', MAccountActor>
|
||||
|
||||
export type MChannelBannerAccountDefault =
|
||||
MChannel &
|
||||
Use<'Actor', MActorDefaultBanner> &
|
||||
Use<'Account', MAccountDefault>
|
||||
|
||||
export type MChannelAccountDefault =
|
||||
MChannel &
|
||||
Use<'Actor', MActorDefault> &
|
||||
Use<'Account', MAccountDefault>
|
||||
|
||||
export type MChannelActorAccountActor =
|
||||
MChannel &
|
||||
Use<'Account', MAccountActor> &
|
||||
Use<'Actor', MActor>
|
||||
|
||||
// ############################################################################
|
||||
|
||||
// Videos associations
|
||||
// Videos associations
|
||||
export type MChannelVideos =
|
||||
MChannel &
|
||||
Use<'Videos', MVideo[]>
|
||||
|
||||
export type MChannelActorAccountDefaultVideos =
|
||||
MChannel &
|
||||
Use<'Actor', MActorDefault> &
|
||||
Use<'Account', MAccountDefault> &
|
||||
Use<'Videos', MVideo[]>
|
||||
|
||||
// ############################################################################
|
||||
|
||||
// For API
|
||||
|
@ -146,5 +142,5 @@ export type MChannelFormattable =
|
|||
|
||||
export type MChannelAP =
|
||||
Pick<MChannel, 'name' | 'description' | 'support'> &
|
||||
Use<'Actor', MActorAP> &
|
||||
Use<'Actor', MActorAPChannel> &
|
||||
Use<'Account', MAccountUrl>
|
||||
|
|
|
@ -3,7 +3,10 @@ import {
|
|||
MAbuseMessage,
|
||||
MAbuseReporter,
|
||||
MAccountBlocklist,
|
||||
MActorFollowActors,
|
||||
MActorFollowActorsDefault,
|
||||
MActorUrl,
|
||||
MChannelBannerAccountDefault,
|
||||
MStreamingPlaylist,
|
||||
MVideoChangeOwnershipFull,
|
||||
MVideoFile,
|
||||
|
@ -21,10 +24,8 @@ import { RegisteredPlugin } from '../../lib/plugins/plugin-manager'
|
|||
import {
|
||||
MAccountDefault,
|
||||
MActorAccountChannelId,
|
||||
MActorFollowActorsDefault,
|
||||
MActorFollowActorsDefaultSubscription,
|
||||
MActorFull,
|
||||
MChannelAccountDefault,
|
||||
MComment,
|
||||
MCommentOwnerVideoReply,
|
||||
MUserDefault,
|
||||
|
@ -71,7 +72,7 @@ interface PeerTubeLocals {
|
|||
|
||||
videoStreamingPlaylist?: MStreamingPlaylist
|
||||
|
||||
videoChannel?: MChannelAccountDefault
|
||||
videoChannel?: MChannelBannerAccountDefault
|
||||
|
||||
videoPlaylistFull?: MVideoPlaylistFull
|
||||
videoPlaylistSummary?: MVideoPlaylistFullSummary
|
||||
|
|
|
@ -27,5 +27,6 @@ export interface ActivityPubActor {
|
|||
publicKeyPem: string
|
||||
}
|
||||
|
||||
icon: ActivityIconObject
|
||||
icon?: ActivityIconObject
|
||||
image?: ActivityIconObject
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ export interface ActivityIdentifierObject {
|
|||
export interface ActivityIconObject {
|
||||
type: 'Image'
|
||||
url: string
|
||||
mediaType: 'image/jpeg' | 'image/png'
|
||||
mediaType: string
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
|
|
|
@ -15,6 +15,8 @@ export interface VideoChannel extends Actor {
|
|||
|
||||
videosCount?: number
|
||||
viewsPerDay?: ViewsPerDate[] // chronologically ordered
|
||||
|
||||
banner?: ActorImage
|
||||
}
|
||||
|
||||
export interface VideoChannelSummary {
|
||||
|
|
Loading…
Reference in New Issue