Add banners support

This commit is contained in:
Chocobozzz 2021-04-06 17:01:35 +02:00 committed by Chocobozzz
parent f479685678
commit 2cb03dc1f4
33 changed files with 392 additions and 240 deletions

View File

@ -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: {

View File

@ -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)
}

View File

@ -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) {

View File

@ -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) {

View File

@ -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

View File

@ -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)

View File

@ -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: {

View File

@ -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)
}
// ---------------------------------------------------------------------------

View File

@ -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
}

View File

@ -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
}

View File

@ -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,

View File

@ -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,

View File

@ -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)
}
}

View File

@ -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 })

View File

@ -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
}

View File

@ -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'

View File

@ -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',

View File

@ -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
}

View File

@ -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
}

View File

@ -68,7 +68,6 @@ const removeFollowingValidator = [
.json({
error: `Following ${req.params.host} not found.`
})
.end()
}
res.locals.follow = follow

View File

@ -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()

View File

@ -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[]> {

View File

@ -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

View File

@ -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()

View File

@ -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>

View File

@ -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 &

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -27,5 +27,6 @@ export interface ActivityPubActor {
publicKeyPem: string
}
icon: ActivityIconObject
icon?: ActivityIconObject
image?: ActivityIconObject
}

View File

@ -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
}

View File

@ -15,6 +15,8 @@ export interface VideoChannel extends Actor {
videosCount?: number
viewsPerDay?: ViewsPerDate[] // chronologically ordered
banner?: ActorImage
}
export interface VideoChannelSummary {