From 2cb03dc1f4e01ba491c36caff30c33fe9c5bad89 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 6 Apr 2021 17:01:35 +0200 Subject: Add banners support --- server/lib/activitypub/actor.ts | 106 +++++++++++++++++------ server/lib/activitypub/process/process-delete.ts | 6 +- server/lib/activitypub/process/process-update.ts | 14 +-- server/lib/actor-image.ts | 51 ++++++----- server/lib/client-html.ts | 6 +- server/lib/emailer.ts | 2 +- server/lib/video-channel.ts | 16 +--- 7 files changed, 129 insertions(+), 72 deletions(-) (limited to 'server/lib') diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts index da831dcfd..fe4796a3d 100644 --- a/server/lib/activitypub/actor.ts +++ b/server/lib/activitypub/actor.ts @@ -4,6 +4,7 @@ import { Op, Transaction } from 'sequelize' 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,43 +169,60 @@ 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 }) + if (type === ActorImageType.AVATAR) { + await actor.Avatar.destroy({ transaction: t }) + + 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 avatar of actor %s.', actor.url, { err }) + logger.error('Cannot remove old image of actor %s.', actor.url, { err }) } - actor.avatarId = null - actor.Avatar = null - 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 ({ @@ -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, diff --git a/server/lib/activitypub/process/process-delete.ts b/server/lib/activitypub/process/process-delete.ts index a86def936..070ee0f1d 100644 --- a/server/lib/activitypub/process/process-delete.ts +++ b/server/lib/activitypub/process/process-delete.ts @@ -7,7 +7,7 @@ import { VideoModel } from '../../../models/video/video' 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) { 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 }) diff --git a/server/lib/actor-image.ts b/server/lib/actor-image.ts index ca7f9658d..59afa93bd 100644 --- a/server/lib/actor-image.ts +++ b/server/lib/actor-image.ts @@ -3,50 +3,57 @@ import { queue } from 'async' 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((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({ max: LRU_CACHE. export { actorImagePathUnsafeCache, - updateLocalActorAvatarFile, - deleteLocalActorAvatarFile, + updateLocalActorImageFile, + deleteLocalActorImageFile, pushActorImageProcessInQueue } diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts index fcc11c7b2..6ddaa82c8 100644 --- a/server/lib/client-html.ts +++ b/server/lib/client-html.ts @@ -11,7 +11,7 @@ import { logger } from '../helpers/logger' 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' diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts index ce4134d59..9ca0d5d5b 100644 --- a/server/lib/emailer.ts +++ b/server/lib/emailer.ts @@ -405,7 +405,7 @@ class Emailer { 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', diff --git a/server/lib/video-channel.ts b/server/lib/video-channel.ts index 49bdf4869..0476cb2d5 100644 --- a/server/lib/video-channel.ts +++ b/server/lib/video-channel.ts @@ -3,18 +3,12 @@ import { v4 as uuidv4 } from 'uuid' 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 = MChannelDefault & { Account?: T } - -async function createLocalVideoChannel ( - videoChannelInfo: VideoChannelCreate, - account: T, - t: Sequelize.Transaction -): Promise> { +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 ( const videoChannel = new VideoChannelModel(videoChannelData) const options = { transaction: t } - const videoChannelCreated: CustomVideoChannelModelAccount = 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 } -- cgit v1.2.3