Agnostic actor image storage

This commit is contained in:
Chocobozzz 2021-04-06 11:35:56 +02:00 committed by Chocobozzz
parent 968aaed206
commit f479685678
36 changed files with 186 additions and 147 deletions

View File

@ -1,7 +1,7 @@
import { Account } from '@app/shared/shared-main/account/account.model' import { Account } from '@app/shared/shared-main/account/account.model'
import { hasUserRight } from '@shared/core-utils/users' import { hasUserRight } from '@shared/core-utils/users'
import { import {
Avatar, ActorImage,
NSFWPolicyType, NSFWPolicyType,
User as UserServerModel, User as UserServerModel,
UserAdminFlag, UserAdminFlag,
@ -131,7 +131,7 @@ export class User implements UserServerModel {
} }
} }
updateAccountAvatar (newAccountAvatar?: Avatar) { updateAccountAvatar (newAccountAvatar?: ActorImage) {
if (newAccountAvatar) this.account.updateAvatar(newAccountAvatar) if (newAccountAvatar) this.account.updateAvatar(newAccountAvatar)
else this.account.resetAvatar() else this.account.resetAvatar()
} }

View File

@ -7,8 +7,7 @@ import { AuthService } from '@app/core/auth'
import { getBytes } from '@root-helpers/bytes' import { getBytes } from '@root-helpers/bytes'
import { UserLocalStorageKeys } from '@root-helpers/users' import { UserLocalStorageKeys } from '@root-helpers/users'
import { import {
Avatar, ActorImage,
NSFWPolicyType,
ResultList, ResultList,
User as UserServerModel, User as UserServerModel,
UserCreate, UserCreate,
@ -136,7 +135,7 @@ export class UserService {
changeAvatar (avatarForm: FormData) { changeAvatar (avatarForm: FormData) {
const url = UserService.BASE_USERS_URL + 'me/avatar/pick' const url = UserService.BASE_USERS_URL + 'me/avatar/pick'
return this.authHttp.post<{ avatar: Avatar }>(url, avatarForm) return this.authHttp.post<{ avatar: ActorImage }>(url, avatarForm)
.pipe(catchError(err => this.restExtractor.handleError(err))) .pipe(catchError(err => this.restExtractor.handleError(err)))
} }

View File

@ -1,4 +1,4 @@
import { Account as ServerAccount, Avatar } from '@shared/models' import { Account as ServerAccount, ActorImage } from '@shared/models'
import { Actor } from './actor.model' import { Actor } from './actor.model'
export class Account extends Actor implements ServerAccount { export class Account extends Actor implements ServerAccount {
@ -38,7 +38,7 @@ export class Account extends Actor implements ServerAccount {
this.mutedServerByInstance = false this.mutedServerByInstance = false
} }
updateAvatar (newAvatar: Avatar) { updateAvatar (newAvatar: ActorImage) {
this.avatar = newAvatar this.avatar = newAvatar
this.updateComputedAttributes() this.updateComputedAttributes()

View File

@ -1,4 +1,4 @@
import { Actor as ActorServer, Avatar } from '@shared/models' import { Actor as ActorServer, ActorImage } from '@shared/models'
import { getAbsoluteAPIUrl } from '@app/helpers' import { getAbsoluteAPIUrl } from '@app/helpers'
export abstract class Actor implements ActorServer { export abstract class Actor implements ActorServer {
@ -10,7 +10,7 @@ export abstract class Actor implements ActorServer {
followersCount: number followersCount: number
createdAt: Date | string createdAt: Date | string
updatedAt: Date | string updatedAt: Date | string
avatar: Avatar avatar: ActorImage
avatarUrl: string avatarUrl: string

View File

@ -1,4 +1,4 @@
import { Account as ServerAccount, Avatar, VideoChannel as ServerVideoChannel, ViewsPerDate } from '@shared/models' import { Account as ServerAccount, ActorImage, VideoChannel as ServerVideoChannel, ViewsPerDate } from '@shared/models'
import { Account } from '../account/account.model' import { Account } from '../account/account.model'
import { Actor } from '../account/actor.model' import { Actor } from '../account/actor.model'
@ -51,7 +51,7 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
} }
} }
updateAvatar (newAvatar: Avatar) { updateAvatar (newAvatar: ActorImage) {
this.avatar = newAvatar this.avatar = newAvatar
this.updateComputedAttributes() this.updateComputedAttributes()

View File

@ -3,7 +3,7 @@ import { catchError, map, tap } from 'rxjs/operators'
import { HttpClient, HttpParams } from '@angular/common/http' import { HttpClient, HttpParams } from '@angular/common/http'
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { ComponentPaginationLight, RestExtractor, RestService } from '@app/core' import { ComponentPaginationLight, RestExtractor, RestService } from '@app/core'
import { Avatar, ResultList, VideoChannel as VideoChannelServer, VideoChannelCreate, VideoChannelUpdate } from '@shared/models' import { ActorImage, ResultList, VideoChannel as VideoChannelServer, VideoChannelCreate, VideoChannelUpdate } from '@shared/models'
import { environment } from '../../../../environments/environment' import { environment } from '../../../../environments/environment'
import { Account } from '../account' import { Account } from '../account'
import { AccountService } from '../account/account.service' import { AccountService } from '../account/account.service'
@ -85,7 +85,7 @@ export class VideoChannelService {
changeVideoChannelAvatar (videoChannelName: string, avatarForm: FormData) { changeVideoChannelAvatar (videoChannelName: string, avatarForm: FormData) {
const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/avatar/pick' const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/avatar/pick'
return this.authHttp.post<{ avatar: Avatar }>(url, avatarForm) return this.authHttp.post<{ avatar: ActorImage }>(url, avatarForm)
.pipe(catchError(err => this.restExtractor.handleError(err))) .pipe(catchError(err => this.restExtractor.handleError(err)))
} }

View File

@ -6,7 +6,7 @@ import { Actor } from '@app/shared/shared-main/account/actor.model'
import { VideoChannel } from '@app/shared/shared-main/video-channel/video-channel.model' import { VideoChannel } from '@app/shared/shared-main/video-channel/video-channel.model'
import { peertubeTranslate } from '@shared/core-utils/i18n' import { peertubeTranslate } from '@shared/core-utils/i18n'
import { import {
Avatar, ActorImage,
ServerConfig, ServerConfig,
UserRight, UserRight,
Video as VideoServerModel, Video as VideoServerModel,
@ -72,7 +72,7 @@ export class Video implements VideoServerModel {
displayName: string displayName: string
url: string url: string
host: string host: string
avatar?: Avatar avatar?: ActorImage
} }
channel: { channel: {
@ -81,7 +81,7 @@ export class Video implements VideoServerModel {
displayName: string displayName: string
url: string url: string
host: string host: string
avatar?: Avatar avatar?: ActorImage
} }
userHistory?: { userHistory?: {

View File

@ -11,7 +11,7 @@ import { VideoRedundancyModel } from '../server/models/redundancy/video-redundan
import * as Bluebird from 'bluebird' import * as Bluebird from 'bluebird'
import { getUUIDFromFilename } from '../server/helpers/utils' import { getUUIDFromFilename } from '../server/helpers/utils'
import { ThumbnailModel } from '../server/models/video/thumbnail' import { ThumbnailModel } from '../server/models/video/thumbnail'
import { AvatarModel } from '../server/models/avatar/avatar' import { ActorImageModel } from '../server/models/account/actor-image'
import { uniq, values } from 'lodash' import { uniq, values } from 'lodash'
import { ThumbnailType } from '@shared/models' import { ThumbnailType } from '@shared/models'
@ -43,7 +43,7 @@ async function run () {
await pruneDirectory(CONFIG.STORAGE.PREVIEWS_DIR, doesThumbnailExist(true, ThumbnailType.PREVIEW)), await pruneDirectory(CONFIG.STORAGE.PREVIEWS_DIR, doesThumbnailExist(true, ThumbnailType.PREVIEW)),
await pruneDirectory(CONFIG.STORAGE.THUMBNAILS_DIR, doesThumbnailExist(false, ThumbnailType.MINIATURE)), await pruneDirectory(CONFIG.STORAGE.THUMBNAILS_DIR, doesThumbnailExist(false, ThumbnailType.MINIATURE)),
await pruneDirectory(CONFIG.STORAGE.AVATARS_DIR, doesAvatarExist) await pruneDirectory(CONFIG.STORAGE.ACTOR_IMAGES, doesActorImageExist)
) )
const tmpFiles = await readdir(CONFIG.STORAGE.TMP_DIR) const tmpFiles = await readdir(CONFIG.STORAGE.TMP_DIR)
@ -107,10 +107,10 @@ function doesThumbnailExist (keepOnlyOwned: boolean, type: ThumbnailType) {
} }
} }
async function doesAvatarExist (file: string) { async function doesActorImageExist (file: string) {
const avatar = await AvatarModel.loadByName(file) const image = await ActorImageModel.loadByName(file)
return !!avatar return !!image
} }
async function doesRedundancyExist (file: string) { async function doesRedundancyExist (file: string) {

View File

@ -11,7 +11,7 @@ import { CONFIG } from '../../../initializers/config'
import { MIMETYPES } from '../../../initializers/constants' import { MIMETYPES } from '../../../initializers/constants'
import { sequelizeTypescript } from '../../../initializers/database' import { sequelizeTypescript } from '../../../initializers/database'
import { sendUpdateActor } from '../../../lib/activitypub/send' import { sendUpdateActor } from '../../../lib/activitypub/send'
import { deleteLocalActorAvatarFile, updateLocalActorAvatarFile } from '../../../lib/avatar' import { deleteLocalActorAvatarFile, updateLocalActorAvatarFile } from '../../../lib/actor-image'
import { getOriginalVideoFileTotalDailyFromUser, getOriginalVideoFileTotalFromUser, sendVerifyUserEmail } from '../../../lib/user' import { getOriginalVideoFileTotalDailyFromUser, getOriginalVideoFileTotalFromUser, sendVerifyUserEmail } from '../../../lib/user'
import { import {
asyncMiddleware, asyncMiddleware,

View File

@ -13,7 +13,7 @@ import { CONFIG } from '../../initializers/config'
import { MIMETYPES } from '../../initializers/constants' import { MIMETYPES } from '../../initializers/constants'
import { sequelizeTypescript } from '../../initializers/database' import { sequelizeTypescript } from '../../initializers/database'
import { sendUpdateActor } from '../../lib/activitypub/send' import { sendUpdateActor } from '../../lib/activitypub/send'
import { deleteLocalActorAvatarFile, updateLocalActorAvatarFile } from '../../lib/avatar' import { deleteLocalActorAvatarFile, updateLocalActorAvatarFile } from '../../lib/actor-image'
import { JobQueue } from '../../lib/job-queue' import { JobQueue } from '../../lib/job-queue'
import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel' import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel'
import { import {

View File

@ -4,10 +4,10 @@ import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache
import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes' import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
import { logger } from '../helpers/logger' import { logger } from '../helpers/logger'
import { LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/constants' import { LAZY_STATIC_PATHS, STATIC_MAX_AGE } from '../initializers/constants'
import { avatarPathUnsafeCache, pushAvatarProcessInQueue } from '../lib/avatar' import { actorImagePathUnsafeCache, pushActorImageProcessInQueue } from '../lib/actor-image'
import { VideosCaptionCache, VideosPreviewCache } from '../lib/files-cache' import { VideosCaptionCache, VideosPreviewCache } from '../lib/files-cache'
import { asyncMiddleware } from '../middlewares' import { asyncMiddleware } from '../middlewares'
import { AvatarModel } from '../models/avatar/avatar' import { ActorImageModel } from '../models/account/actor-image'
const lazyStaticRouter = express.Router() const lazyStaticRouter = express.Router()
@ -15,7 +15,12 @@ lazyStaticRouter.use(cors())
lazyStaticRouter.use( lazyStaticRouter.use(
LAZY_STATIC_PATHS.AVATARS + ':filename', LAZY_STATIC_PATHS.AVATARS + ':filename',
asyncMiddleware(getAvatar) asyncMiddleware(getActorImage)
)
lazyStaticRouter.use(
LAZY_STATIC_PATHS.BANNERS + ':filename',
asyncMiddleware(getActorImage)
) )
lazyStaticRouter.use( lazyStaticRouter.use(
@ -43,36 +48,36 @@ export {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
async function getAvatar (req: express.Request, res: express.Response) { async function getActorImage (req: express.Request, res: express.Response) {
const filename = req.params.filename const filename = req.params.filename
if (avatarPathUnsafeCache.has(filename)) { if (actorImagePathUnsafeCache.has(filename)) {
return res.sendFile(avatarPathUnsafeCache.get(filename), { maxAge: STATIC_MAX_AGE.SERVER }) return res.sendFile(actorImagePathUnsafeCache.get(filename), { maxAge: STATIC_MAX_AGE.SERVER })
} }
const avatar = await AvatarModel.loadByName(filename) const image = await ActorImageModel.loadByName(filename)
if (!avatar) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) if (!image) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
if (avatar.onDisk === false) { if (image.onDisk === false) {
if (!avatar.fileUrl) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) if (!image.fileUrl) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
logger.info('Lazy serve remote avatar image %s.', avatar.fileUrl) logger.info('Lazy serve remote actor image %s.', image.fileUrl)
try { try {
await pushAvatarProcessInQueue({ filename: avatar.filename, fileUrl: avatar.fileUrl }) await pushActorImageProcessInQueue({ filename: image.filename, fileUrl: image.fileUrl })
} catch (err) { } catch (err) {
logger.warn('Cannot process remote avatar %s.', avatar.fileUrl, { err }) logger.warn('Cannot process remote actor image %s.', image.fileUrl, { err })
return res.sendStatus(HttpStatusCode.NOT_FOUND_404) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
} }
avatar.onDisk = true image.onDisk = true
avatar.save() image.save()
.catch(err => logger.error('Cannot save new avatar disk state.', { err })) .catch(err => logger.error('Cannot save new actor image disk state.', { err }))
} }
const path = avatar.getPath() const path = image.getPath()
avatarPathUnsafeCache.set(filename, path) actorImagePathUnsafeCache.set(filename, path)
return res.sendFile(path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }) return res.sendFile(path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER })
} }

View File

@ -59,7 +59,7 @@ const CONFIG = {
}, },
STORAGE: { STORAGE: {
TMP_DIR: buildPath(config.get<string>('storage.tmp')), TMP_DIR: buildPath(config.get<string>('storage.tmp')),
AVATARS_DIR: buildPath(config.get<string>('storage.avatars')), ACTOR_IMAGES: buildPath(config.get<string>('storage.avatars')),
LOG_DIR: buildPath(config.get<string>('storage.logs')), LOG_DIR: buildPath(config.get<string>('storage.logs')),
VIDEOS_DIR: buildPath(config.get<string>('storage.videos')), VIDEOS_DIR: buildPath(config.get<string>('storage.videos')),
STREAMING_PLAYLISTS_DIR: buildPath(config.get<string>('storage.streaming_playlists')), STREAMING_PLAYLISTS_DIR: buildPath(config.get<string>('storage.streaming_playlists')),

View File

@ -580,6 +580,7 @@ const STATIC_DOWNLOAD_PATHS = {
HLS_VIDEOS: '/download/streaming-playlists/hls/videos/' HLS_VIDEOS: '/download/streaming-playlists/hls/videos/'
} }
const LAZY_STATIC_PATHS = { const LAZY_STATIC_PATHS = {
BANNERS: '/lazy-static/banners/',
AVATARS: '/lazy-static/avatars/', AVATARS: '/lazy-static/avatars/',
PREVIEWS: '/lazy-static/previews/', PREVIEWS: '/lazy-static/previews/',
VIDEO_CAPTIONS: '/lazy-static/video-captions/', VIDEO_CAPTIONS: '/lazy-static/video-captions/',
@ -634,7 +635,7 @@ const LRU_CACHE = {
USER_TOKENS: { USER_TOKENS: {
MAX_SIZE: 1000 MAX_SIZE: 1000
}, },
AVATAR_STATIC: { ACTOR_IMAGE_STATIC: {
MAX_SIZE: 500 MAX_SIZE: 500
} }
} }
@ -671,7 +672,7 @@ const MEMOIZE_LENGTH = {
} }
const QUEUE_CONCURRENCY = { const QUEUE_CONCURRENCY = {
AVATAR_PROCESS_IMAGE: 3 ACTOR_PROCESS_IMAGE: 3
} }
const REDUNDANCY = { const REDUNDANCY = {

View File

@ -1,7 +1,7 @@
import { TrackerModel } from '@server/models/server/tracker'
import { VideoTrackerModel } from '@server/models/server/video-tracker'
import { QueryTypes, Transaction } from 'sequelize' import { QueryTypes, Transaction } from 'sequelize'
import { Sequelize as SequelizeTypescript } from 'sequelize-typescript' import { Sequelize as SequelizeTypescript } from 'sequelize-typescript'
import { TrackerModel } from '@server/models/server/tracker'
import { VideoTrackerModel } from '@server/models/server/video-tracker'
import { isTestInstance } from '../helpers/core-utils' import { isTestInstance } from '../helpers/core-utils'
import { logger } from '../helpers/logger' import { logger } from '../helpers/logger'
import { AbuseModel } from '../models/abuse/abuse' import { AbuseModel } from '../models/abuse/abuse'
@ -11,6 +11,7 @@ import { VideoCommentAbuseModel } from '../models/abuse/video-comment-abuse'
import { AccountModel } from '../models/account/account' import { AccountModel } from '../models/account/account'
import { AccountBlocklistModel } from '../models/account/account-blocklist' import { AccountBlocklistModel } from '../models/account/account-blocklist'
import { AccountVideoRateModel } from '../models/account/account-video-rate' import { AccountVideoRateModel } from '../models/account/account-video-rate'
import { ActorImageModel } from '../models/account/actor-image'
import { UserModel } from '../models/account/user' import { UserModel } from '../models/account/user'
import { UserNotificationModel } from '../models/account/user-notification' import { UserNotificationModel } from '../models/account/user-notification'
import { UserNotificationSettingModel } from '../models/account/user-notification-setting' import { UserNotificationSettingModel } from '../models/account/user-notification-setting'
@ -18,7 +19,6 @@ import { UserVideoHistoryModel } from '../models/account/user-video-history'
import { ActorModel } from '../models/activitypub/actor' import { ActorModel } from '../models/activitypub/actor'
import { ActorFollowModel } from '../models/activitypub/actor-follow' import { ActorFollowModel } from '../models/activitypub/actor-follow'
import { ApplicationModel } from '../models/application/application' import { ApplicationModel } from '../models/application/application'
import { AvatarModel } from '../models/avatar/avatar'
import { OAuthClientModel } from '../models/oauth/oauth-client' import { OAuthClientModel } from '../models/oauth/oauth-client'
import { OAuthTokenModel } from '../models/oauth/oauth-token' import { OAuthTokenModel } from '../models/oauth/oauth-token'
import { VideoRedundancyModel } from '../models/redundancy/video-redundancy' import { VideoRedundancyModel } from '../models/redundancy/video-redundancy'
@ -95,7 +95,7 @@ async function initDatabaseModels (silent: boolean) {
ApplicationModel, ApplicationModel,
ActorModel, ActorModel,
ActorFollowModel, ActorFollowModel,
AvatarModel, ActorImageModel,
AccountModel, AccountModel,
OAuthClientModel, OAuthClientModel,
OAuthTokenModel, OAuthTokenModel,

View File

@ -19,8 +19,8 @@ import { getUrlFromWebfinger } from '../../helpers/webfinger'
import { MIMETYPES, WEBSERVER } from '../../initializers/constants' import { MIMETYPES, WEBSERVER } from '../../initializers/constants'
import { sequelizeTypescript } from '../../initializers/database' import { sequelizeTypescript } from '../../initializers/database'
import { AccountModel } from '../../models/account/account' import { AccountModel } from '../../models/account/account'
import { ActorImageModel } from '../../models/account/actor-image'
import { ActorModel } from '../../models/activitypub/actor' import { ActorModel } from '../../models/activitypub/actor'
import { AvatarModel } from '../../models/avatar/avatar'
import { ServerModel } from '../../models/server/server' import { ServerModel } from '../../models/server/server'
import { VideoChannelModel } from '../../models/video/video-channel' import { VideoChannelModel } from '../../models/video/video-channel'
import { import {
@ -183,7 +183,7 @@ async function updateActorAvatarInstance (actor: MActorDefault, info: AvatarInfo
} }
} }
const avatar = await AvatarModel.create({ const avatar = await ActorImageModel.create({
filename: info.name, filename: info.name,
onDisk: info.onDisk, onDisk: info.onDisk,
fileUrl: info.fileUrl fileUrl: info.fileUrl
@ -378,7 +378,7 @@ function saveActorAndServerAndModelIfNotExist (
// Avatar? // Avatar?
if (result.avatar) { if (result.avatar) {
const avatar = await AvatarModel.create({ const avatar = await ActorImageModel.create({
filename: result.avatar.name, filename: result.avatar.name,
fileUrl: result.avatar.fileUrl, fileUrl: result.avatar.fileUrl,
onDisk: false onDisk: false

View File

@ -1,17 +1,17 @@
import 'multer' import 'multer'
import { sendUpdateActor } from './activitypub/send'
import { AVATARS_SIZE, LRU_CACHE, QUEUE_CONCURRENCY } from '../initializers/constants'
import { updateActorAvatarInstance, deleteActorAvatarInstance } from './activitypub/actor'
import { processImage } from '../helpers/image-utils'
import { extname, join } from 'path'
import { retryTransactionWrapper } from '../helpers/database-utils'
import { v4 as uuidv4 } from 'uuid'
import { CONFIG } from '../initializers/config'
import { sequelizeTypescript } from '../initializers/database'
import * as LRUCache from 'lru-cache'
import { queue } from 'async' import { queue } from 'async'
import * as LRUCache from 'lru-cache'
import { extname, join } from 'path'
import { v4 as uuidv4 } from 'uuid'
import { retryTransactionWrapper } from '../helpers/database-utils'
import { processImage } from '../helpers/image-utils'
import { downloadImage } from '../helpers/requests' import { downloadImage } from '../helpers/requests'
import { CONFIG } from '../initializers/config'
import { AVATARS_SIZE, LRU_CACHE, QUEUE_CONCURRENCY } from '../initializers/constants'
import { sequelizeTypescript } from '../initializers/database'
import { MAccountDefault, MChannelDefault } from '../types/models' import { MAccountDefault, MChannelDefault } from '../types/models'
import { deleteActorAvatarInstance, updateActorAvatarInstance } from './activitypub/actor'
import { sendUpdateActor } from './activitypub/send'
async function updateLocalActorAvatarFile ( async function updateLocalActorAvatarFile (
accountOrChannel: MAccountDefault | MChannelDefault, accountOrChannel: MAccountDefault | MChannelDefault,
@ -20,7 +20,7 @@ async function updateLocalActorAvatarFile (
const extension = extname(avatarPhysicalFile.filename) const extension = extname(avatarPhysicalFile.filename)
const avatarName = uuidv4() + extension const avatarName = uuidv4() + extension
const destination = join(CONFIG.STORAGE.AVATARS_DIR, avatarName) const destination = join(CONFIG.STORAGE.ACTOR_IMAGES, avatarName)
await processImage(avatarPhysicalFile.path, destination, AVATARS_SIZE) await processImage(avatarPhysicalFile.path, destination, AVATARS_SIZE)
return retryTransactionWrapper(() => { return retryTransactionWrapper(() => {
@ -59,12 +59,12 @@ async function deleteLocalActorAvatarFile (
type DownloadImageQueueTask = { fileUrl: string, filename: string } type DownloadImageQueueTask = { fileUrl: string, filename: string }
const downloadImageQueue = queue<DownloadImageQueueTask, Error>((task, cb) => { const downloadImageQueue = queue<DownloadImageQueueTask, Error>((task, cb) => {
downloadImage(task.fileUrl, CONFIG.STORAGE.AVATARS_DIR, task.filename, AVATARS_SIZE) downloadImage(task.fileUrl, CONFIG.STORAGE.ACTOR_IMAGES, task.filename, AVATARS_SIZE)
.then(() => cb()) .then(() => cb())
.catch(err => cb(err)) .catch(err => cb(err))
}, QUEUE_CONCURRENCY.AVATAR_PROCESS_IMAGE) }, QUEUE_CONCURRENCY.ACTOR_PROCESS_IMAGE)
function pushAvatarProcessInQueue (task: DownloadImageQueueTask) { function pushActorImageProcessInQueue (task: DownloadImageQueueTask) {
return new Promise<void>((res, rej) => { return new Promise<void>((res, rej) => {
downloadImageQueue.push(task, err => { downloadImageQueue.push(task, err => {
if (err) return rej(err) if (err) return rej(err)
@ -75,11 +75,11 @@ function pushAvatarProcessInQueue (task: DownloadImageQueueTask) {
} }
// Unsafe so could returns paths that does not exist anymore // Unsafe so could returns paths that does not exist anymore
const avatarPathUnsafeCache = new LRUCache<string, string>({ max: LRU_CACHE.AVATAR_STATIC.MAX_SIZE }) const actorImagePathUnsafeCache = new LRUCache<string, string>({ max: LRU_CACHE.ACTOR_IMAGE_STATIC.MAX_SIZE })
export { export {
avatarPathUnsafeCache, actorImagePathUnsafeCache,
updateLocalActorAvatarFile, updateLocalActorAvatarFile,
deleteLocalActorAvatarFile, deleteLocalActorAvatarFile,
pushAvatarProcessInQueue pushActorImageProcessInQueue
} }

View File

@ -33,7 +33,7 @@ import {
import { ActorModel } from '../activitypub/actor' import { ActorModel } from '../activitypub/actor'
import { ActorFollowModel } from '../activitypub/actor-follow' import { ActorFollowModel } from '../activitypub/actor-follow'
import { ApplicationModel } from '../application/application' import { ApplicationModel } from '../application/application'
import { AvatarModel } from '../avatar/avatar' import { ActorImageModel } from './actor-image'
import { ServerModel } from '../server/server' import { ServerModel } from '../server/server'
import { ServerBlocklistModel } from '../server/server-blocklist' import { ServerBlocklistModel } from '../server/server-blocklist'
import { getSort, throwIfNotValid } from '../utils' import { getSort, throwIfNotValid } from '../utils'
@ -82,7 +82,8 @@ export type SummaryOptions = {
serverInclude, serverInclude,
{ {
model: AvatarModel.unscoped(), model: ActorImageModel.unscoped(),
as: 'Avatar',
required: false required: false
} }
] ]

View File

@ -1,16 +1,17 @@
import { remove } from 'fs-extra'
import { join } from 'path' import { join } from 'path'
import { AfterDestroy, AllowNull, Column, CreatedAt, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' import { AfterDestroy, AllowNull, Column, CreatedAt, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
import { Avatar } from '../../../shared/models/avatars/avatar.model' import { MActorImageFormattable } from '@server/types/models'
import { LAZY_STATIC_PATHS } from '../../initializers/constants' import { ActorImageType } from '@shared/models'
import { logger } from '../../helpers/logger' import { ActorImage } from '../../../shared/models/actors/actor-image.model'
import { remove } from 'fs-extra'
import { CONFIG } from '../../initializers/config'
import { throwIfNotValid } from '../utils'
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
import { MAvatarFormattable } from '@server/types/models' import { logger } from '../../helpers/logger'
import { CONFIG } from '../../initializers/config'
import { LAZY_STATIC_PATHS } from '../../initializers/constants'
import { throwIfNotValid } from '../utils'
@Table({ @Table({
tableName: 'avatar', tableName: 'actorImage',
indexes: [ indexes: [
{ {
fields: [ 'filename' ], fields: [ 'filename' ],
@ -18,14 +19,14 @@ import { MAvatarFormattable } from '@server/types/models'
} }
] ]
}) })
export class AvatarModel extends Model { export class ActorImageModel extends Model {
@AllowNull(false) @AllowNull(false)
@Column @Column
filename: string filename: string
@AllowNull(true) @AllowNull(true)
@Is('AvatarFileUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'fileUrl', true)) @Is('ActorImageFileUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'fileUrl', true))
@Column @Column
fileUrl: string fileUrl: string
@ -33,6 +34,10 @@ export class AvatarModel extends Model {
@Column @Column
onDisk: boolean onDisk: boolean
@AllowNull(false)
@Column
type: ActorImageType
@CreatedAt @CreatedAt
createdAt: Date createdAt: Date
@ -40,12 +45,12 @@ export class AvatarModel extends Model {
updatedAt: Date updatedAt: Date
@AfterDestroy @AfterDestroy
static removeFilesAndSendDelete (instance: AvatarModel) { static removeFilesAndSendDelete (instance: ActorImageModel) {
logger.info('Removing avatar file %s.', instance.filename) logger.info('Removing actor image file %s.', instance.filename)
// Don't block the transaction // Don't block the transaction
instance.removeAvatar() instance.removeImage()
.catch(err => logger.error('Cannot remove avatar file %s.', instance.filename, err)) .catch(err => logger.error('Cannot remove actor image file %s.', instance.filename, err))
} }
static loadByName (filename: string) { static loadByName (filename: string) {
@ -55,10 +60,10 @@ export class AvatarModel extends Model {
} }
} }
return AvatarModel.findOne(query) return ActorImageModel.findOne(query)
} }
toFormattedJSON (this: MAvatarFormattable): Avatar { toFormattedJSON (this: MActorImageFormattable): ActorImage {
return { return {
path: this.getStaticPath(), path: this.getStaticPath(),
createdAt: this.createdAt, createdAt: this.createdAt,
@ -71,11 +76,11 @@ export class AvatarModel extends Model {
} }
getPath () { getPath () {
return join(CONFIG.STORAGE.AVATARS_DIR, this.filename) return join(CONFIG.STORAGE.ACTOR_IMAGES, this.filename)
} }
removeAvatar () { removeImage () {
const avatarPath = join(CONFIG.STORAGE.AVATARS_DIR, this.filename) const imagePath = join(CONFIG.STORAGE.ACTOR_IMAGES, this.filename)
return remove(avatarPath) return remove(imagePath)
} }
} }

View File

@ -10,7 +10,6 @@ import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse'
import { ActorModel } from '../activitypub/actor' import { ActorModel } from '../activitypub/actor'
import { ActorFollowModel } from '../activitypub/actor-follow' import { ActorFollowModel } from '../activitypub/actor-follow'
import { ApplicationModel } from '../application/application' import { ApplicationModel } from '../application/application'
import { AvatarModel } from '../avatar/avatar'
import { PluginModel } from '../server/plugin' import { PluginModel } from '../server/plugin'
import { ServerModel } from '../server/server' import { ServerModel } from '../server/server'
import { getSort, throwIfNotValid } from '../utils' import { getSort, throwIfNotValid } from '../utils'
@ -20,6 +19,7 @@ import { VideoChannelModel } from '../video/video-channel'
import { VideoCommentModel } from '../video/video-comment' import { VideoCommentModel } from '../video/video-comment'
import { VideoImportModel } from '../video/video-import' import { VideoImportModel } from '../video/video-import'
import { AccountModel } from './account' import { AccountModel } from './account'
import { ActorImageModel } from './actor-image'
import { UserModel } from './user' import { UserModel } from './user'
enum ScopeNames { enum ScopeNames {
@ -34,7 +34,8 @@ function buildActorWithAvatarInclude () {
include: [ include: [
{ {
attributes: [ 'filename' ], attributes: [ 'filename' ],
model: AvatarModel.unscoped(), as: 'Avatar',
model: ActorImageModel.unscoped(),
required: false required: false
}, },
{ {
@ -172,7 +173,8 @@ function buildAccountInclude (required: boolean, withActor = false) {
}, },
{ {
attributes: [ 'filename' ], attributes: [ 'filename' ],
model: AvatarModel.unscoped(), as: 'Avatar',
model: ActorImageModel.unscoped(),
required: false required: false
}, },
{ {

View File

@ -19,7 +19,7 @@ import {
} from 'sequelize-typescript' } from 'sequelize-typescript'
import { ModelCache } from '@server/models/model-cache' import { ModelCache } from '@server/models/model-cache'
import { ActivityIconObject, ActivityPubActorType } from '../../../shared/models/activitypub' import { ActivityIconObject, ActivityPubActorType } from '../../../shared/models/activitypub'
import { Avatar } from '../../../shared/models/avatars/avatar.model' import { ActorImage } from '../../../shared/models/actors/actor-image.model'
import { activityPubContextify } from '../../helpers/activitypub' import { activityPubContextify } from '../../helpers/activitypub'
import { import {
isActorFollowersCountValid, isActorFollowersCountValid,
@ -43,7 +43,7 @@ import {
MActorWithInboxes MActorWithInboxes
} from '../../types/models' } from '../../types/models'
import { AccountModel } from '../account/account' import { AccountModel } from '../account/account'
import { AvatarModel } from '../avatar/avatar' import { ActorImageModel } from '../account/actor-image'
import { ServerModel } from '../server/server' import { ServerModel } from '../server/server'
import { isOutdated, throwIfNotValid } from '../utils' import { isOutdated, throwIfNotValid } from '../utils'
import { VideoModel } from '../video/video' import { VideoModel } from '../video/video'
@ -73,7 +73,8 @@ export const unusedActorAttributesForAPI = [
required: false required: false
}, },
{ {
model: AvatarModel, model: ActorImageModel,
as: 'Avatar',
required: false required: false
} }
] ]
@ -100,7 +101,8 @@ export const unusedActorAttributesForAPI = [
required: false required: false
}, },
{ {
model: AvatarModel, model: ActorImageModel,
as: 'Avatar',
required: false required: false
} }
] ]
@ -213,18 +215,35 @@ export class ActorModel extends Model {
@UpdatedAt @UpdatedAt
updatedAt: Date updatedAt: Date
@ForeignKey(() => AvatarModel) @ForeignKey(() => ActorImageModel)
@Column @Column
avatarId: number avatarId: number
@BelongsTo(() => AvatarModel, { @ForeignKey(() => ActorImageModel)
@Column
bannerId: number
@BelongsTo(() => ActorImageModel, {
foreignKey: { foreignKey: {
name: 'avatarId',
allowNull: true allowNull: true
}, },
as: 'Avatar',
onDelete: 'set null', onDelete: 'set null',
hooks: true hooks: true
}) })
Avatar: AvatarModel Avatar: ActorImageModel
@BelongsTo(() => ActorImageModel, {
foreignKey: {
name: 'bannerId',
allowNull: true
},
as: 'Banner',
onDelete: 'set null',
hooks: true
})
Banner: ActorImageModel
@HasMany(() => ActorFollowModel, { @HasMany(() => ActorFollowModel, {
foreignKey: { foreignKey: {
@ -496,7 +515,7 @@ export class ActorModel extends Model {
} }
toFormattedSummaryJSON (this: MActorSummaryFormattable) { toFormattedSummaryJSON (this: MActorSummaryFormattable) {
let avatar: Avatar = null let avatar: ActorImage = null
if (this.Avatar) { if (this.Avatar) {
avatar = this.Avatar.toFormattedJSON() avatar = this.Avatar.toFormattedJSON()
} }

View File

@ -36,9 +36,9 @@ import {
MChannelSummaryFormattable MChannelSummaryFormattable
} from '../../types/models/video' } from '../../types/models/video'
import { AccountModel, ScopeNames as AccountModelScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account' import { AccountModel, ScopeNames as AccountModelScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account'
import { ActorImageModel } from '../account/actor-image'
import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor'
import { ActorFollowModel } from '../activitypub/actor-follow' import { ActorFollowModel } from '../activitypub/actor-follow'
import { AvatarModel } from '../avatar/avatar'
import { ServerModel } from '../server/server' import { ServerModel } from '../server/server'
import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils'
import { VideoModel } from './video' import { VideoModel } from './video'
@ -130,7 +130,8 @@ export type SummaryOptions = {
required: false required: false
}, },
{ {
model: AvatarModel.unscoped(), model: ActorImageModel.unscoped(),
as: 'Avatar',
required: false required: false
} }
] ]

View File

@ -490,12 +490,13 @@ function wrapForAPIResults (baseQuery: string, replacements: any, options: Build
'INNER JOIN "actor" AS "VideoChannel->Account->Actor" ON "VideoChannel->Account"."actorId" = "VideoChannel->Account->Actor"."id"', 'INNER JOIN "actor" AS "VideoChannel->Account->Actor" ON "VideoChannel->Account"."actorId" = "VideoChannel->Account->Actor"."id"',
'LEFT OUTER JOIN "server" AS "VideoChannel->Actor->Server" ON "VideoChannel->Actor"."serverId" = "VideoChannel->Actor->Server"."id"', 'LEFT OUTER JOIN "server" AS "VideoChannel->Actor->Server" ON "VideoChannel->Actor"."serverId" = "VideoChannel->Actor->Server"."id"',
'LEFT OUTER JOIN "avatar" AS "VideoChannel->Actor->Avatar" ON "VideoChannel->Actor"."avatarId" = "VideoChannel->Actor->Avatar"."id"', 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Actor->Avatar" ' +
'ON "VideoChannel->Actor"."avatarId" = "VideoChannel->Actor->Avatar"."id"',
'LEFT OUTER JOIN "server" AS "VideoChannel->Account->Actor->Server" ' + 'LEFT OUTER JOIN "server" AS "VideoChannel->Account->Actor->Server" ' +
'ON "VideoChannel->Account->Actor"."serverId" = "VideoChannel->Account->Actor->Server"."id"', 'ON "VideoChannel->Account->Actor"."serverId" = "VideoChannel->Account->Actor->Server"."id"',
'LEFT OUTER JOIN "avatar" AS "VideoChannel->Account->Actor->Avatar" ' + 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Account->Actor->Avatar" ' +
'ON "VideoChannel->Account->Actor"."avatarId" = "VideoChannel->Account->Actor->Avatar"."id"', 'ON "VideoChannel->Account->Actor"."avatarId" = "VideoChannel->Account->Actor->Avatar"."id"',
'LEFT OUTER JOIN "thumbnail" AS "Thumbnails" ON "video"."id" = "Thumbnails"."videoId"' 'LEFT OUTER JOIN "thumbnail" AS "Thumbnails" ON "video"."id" = "Thumbnails"."videoId"'

View File

@ -100,10 +100,10 @@ import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models
import { VideoAbuseModel } from '../abuse/video-abuse' import { VideoAbuseModel } from '../abuse/video-abuse'
import { AccountModel } from '../account/account' import { AccountModel } from '../account/account'
import { AccountVideoRateModel } from '../account/account-video-rate' import { AccountVideoRateModel } from '../account/account-video-rate'
import { ActorImageModel } from '../account/actor-image'
import { UserModel } from '../account/user' import { UserModel } from '../account/user'
import { UserVideoHistoryModel } from '../account/user-video-history' import { UserVideoHistoryModel } from '../account/user-video-history'
import { ActorModel } from '../activitypub/actor' import { ActorModel } from '../activitypub/actor'
import { AvatarModel } from '../avatar/avatar'
import { VideoRedundancyModel } from '../redundancy/video-redundancy' import { VideoRedundancyModel } from '../redundancy/video-redundancy'
import { ServerModel } from '../server/server' import { ServerModel } from '../server/server'
import { TrackerModel } from '../server/tracker' import { TrackerModel } from '../server/tracker'
@ -286,7 +286,8 @@ export type AvailableForListIDsOptions = {
required: false required: false
}, },
{ {
model: AvatarModel.unscoped(), model: ActorImageModel.unscoped(),
as: 'Avatar',
required: false required: false
} }
] ]
@ -308,7 +309,8 @@ export type AvailableForListIDsOptions = {
required: false required: false
}, },
{ {
model: AvatarModel.unscoped(), model: ActorImageModel.unscoped(),
as: 'Avatar',
required: false required: false
} }
] ]
@ -1703,7 +1705,7 @@ export class VideoModel extends Model {
function buildActor (rowActor: any) { function buildActor (rowActor: any) {
const avatarModel = rowActor.Avatar.id !== null const avatarModel = rowActor.Avatar.id !== null
? new AvatarModel(pick(rowActor.Avatar, avatarKeys), buildOpts) ? new ActorImageModel(pick(rowActor.Avatar, avatarKeys), buildOpts)
: null : null
const serverModel = rowActor.Server.id !== null const serverModel = rowActor.Server.id !== null

View File

@ -0,0 +1,12 @@
import { ActorImageModel } from '../../../models/account/actor-image'
import { FunctionProperties } from '@shared/core-utils'
export type MActorImage = ActorImageModel
// ############################################################################
// Format for API or AP object
export type MActorImageFormattable =
FunctionProperties<MActorImage> &
Pick<MActorImage, 'filename' | 'createdAt' | 'updatedAt'>

View File

@ -1,15 +1,15 @@
import { ActorModel } from '../../../models/activitypub/actor'
import { FunctionProperties, PickWith, PickWithOpt } from '@shared/core-utils' import { FunctionProperties, PickWith, PickWithOpt } from '@shared/core-utils'
import { MAccount, MAccountDefault, MAccountId, MAccountIdActor } from './account' import { ActorModel } from '../../../models/activitypub/actor'
import { MServer, MServerHost, MServerHostBlocks, MServerRedundancyAllowed } from '../server' import { MServer, MServerHost, MServerHostBlocks, MServerRedundancyAllowed } from '../server'
import { MAvatar, MAvatarFormattable } from './avatar'
import { MChannel, MChannelAccountActor, MChannelAccountDefault, MChannelId, MChannelIdActor } from '../video' import { MChannel, MChannelAccountActor, MChannelAccountDefault, MChannelId, MChannelIdActor } from '../video'
import { MAccount, MAccountDefault, MAccountId, MAccountIdActor } from './account'
import { MActorImage, MActorImageFormattable } from './actor-image'
type Use<K extends keyof ActorModel, M> = PickWith<ActorModel, K, M> type Use<K extends keyof ActorModel, M> = PickWith<ActorModel, K, M>
// ############################################################################ // ############################################################################
export type MActor = Omit<ActorModel, 'Account' | 'VideoChannel' | 'ActorFollowing' | 'Avatar' | 'ActorFollowers' | 'Server'> export type MActor = Omit<ActorModel, 'Account' | 'VideoChannel' | 'ActorFollowing' | 'Avatar' | 'ActorFollowers' | 'Server' | 'Banner'>
// ############################################################################ // ############################################################################
@ -34,7 +34,7 @@ export type MActorRedundancyAllowedOpt = PickWithOpt<ActorModel, 'Server', MServ
export type MActorDefaultLight = export type MActorDefaultLight =
MActorLight & MActorLight &
Use<'Server', MServerHost> & Use<'Server', MServerHost> &
Use<'Avatar', MAvatar> Use<'Avatar', MActorImage>
export type MActorAccountId = export type MActorAccountId =
MActor & MActor &
@ -78,7 +78,7 @@ export type MActorServer =
export type MActorDefault = export type MActorDefault =
MActor & MActor &
Use<'Server', MServer> & Use<'Server', MServer> &
Use<'Avatar', MAvatar> Use<'Avatar', MActorImage>
// Actor with channel that is associated to an account and its actor // Actor with channel that is associated to an account and its actor
// Actor -> VideoChannel -> Account -> Actor // Actor -> VideoChannel -> Account -> Actor
@ -89,7 +89,7 @@ export type MActorChannelAccountActor =
export type MActorFull = export type MActorFull =
MActor & MActor &
Use<'Server', MServer> & Use<'Server', MServer> &
Use<'Avatar', MAvatar> & Use<'Avatar', MActorImage> &
Use<'Account', MAccount> & Use<'Account', MAccount> &
Use<'VideoChannel', MChannelAccountActor> Use<'VideoChannel', MChannelAccountActor>
@ -97,7 +97,7 @@ export type MActorFull =
export type MActorFullActor = export type MActorFullActor =
MActor & MActor &
Use<'Server', MServer> & Use<'Server', MServer> &
Use<'Avatar', MAvatar> & Use<'Avatar', MActorImage> &
Use<'Account', MAccountDefault> & Use<'Account', MAccountDefault> &
Use<'VideoChannel', MChannelAccountDefault> Use<'VideoChannel', MChannelAccountDefault>
@ -109,7 +109,7 @@ export type MActorSummary =
FunctionProperties<MActor> & FunctionProperties<MActor> &
Pick<MActor, 'id' | 'preferredUsername' | 'url' | 'serverId' | 'avatarId'> & Pick<MActor, 'id' | 'preferredUsername' | 'url' | 'serverId' | 'avatarId'> &
Use<'Server', MServerHost> & Use<'Server', MServerHost> &
Use<'Avatar', MAvatar> Use<'Avatar', MActorImage>
export type MActorSummaryBlocks = export type MActorSummaryBlocks =
MActorSummary & MActorSummary &
@ -127,7 +127,7 @@ export type MActorSummaryFormattable =
FunctionProperties<MActor> & FunctionProperties<MActor> &
Pick<MActor, 'url' | 'preferredUsername'> & Pick<MActor, 'url' | 'preferredUsername'> &
Use<'Server', MServerHost> & Use<'Server', MServerHost> &
Use<'Avatar', MAvatarFormattable> Use<'Avatar', MActorImageFormattable>
export type MActorFormattable = export type MActorFormattable =
MActorSummaryFormattable & MActorSummaryFormattable &
@ -136,4 +136,4 @@ export type MActorFormattable =
export type MActorAP = export type MActorAP =
MActor & MActor &
Use<'Avatar', MAvatar> Use<'Avatar', MActorImage>

View File

@ -1,12 +0,0 @@
import { AvatarModel } from '../../../models/avatar/avatar'
import { FunctionProperties } from '@shared/core-utils'
export type MAvatar = AvatarModel
// ############################################################################
// Format for API or AP object
export type MAvatarFormattable =
FunctionProperties<MAvatar> &
Pick<MAvatar, 'filename' | 'createdAt' | 'updatedAt'>

View File

@ -1,5 +1,5 @@
export * from './account' export * from './account'
export * from './account-blocklist' export * from './account-blocklist'
export * from './actor'
export * from './actor-follow' export * from './actor-follow'
export * from './avatar' export * from './actor-image'
export * from './actor'

View File

@ -5,10 +5,10 @@ import { PluginModel } from '@server/models/server/plugin'
import { PickWith, PickWithOpt } from '@shared/core-utils' import { PickWith, PickWithOpt } from '@shared/core-utils'
import { AbuseModel } from '../../../models/abuse/abuse' import { AbuseModel } from '../../../models/abuse/abuse'
import { AccountModel } from '../../../models/account/account' import { AccountModel } from '../../../models/account/account'
import { ActorImageModel } from '../../../models/account/actor-image'
import { UserNotificationModel } from '../../../models/account/user-notification' import { UserNotificationModel } from '../../../models/account/user-notification'
import { ActorModel } from '../../../models/activitypub/actor' import { ActorModel } from '../../../models/activitypub/actor'
import { ActorFollowModel } from '../../../models/activitypub/actor-follow' import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
import { AvatarModel } from '../../../models/avatar/avatar'
import { ServerModel } from '../../../models/server/server' import { ServerModel } from '../../../models/server/server'
import { VideoModel } from '../../../models/video/video' import { VideoModel } from '../../../models/video/video'
import { VideoBlacklistModel } from '../../../models/video/video-blacklist' import { VideoBlacklistModel } from '../../../models/video/video-blacklist'
@ -29,7 +29,7 @@ export module UserNotificationIncludes {
export type ActorInclude = export type ActorInclude =
Pick<ActorModel, 'preferredUsername' | 'getHost'> & Pick<ActorModel, 'preferredUsername' | 'getHost'> &
PickWith<ActorModel, 'Avatar', Pick<AvatarModel, 'filename' | 'getStaticPath'>> & PickWith<ActorModel, 'Avatar', Pick<ActorImageModel, 'filename' | 'getStaticPath'>> &
PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>> PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>>
export type VideoChannelInclude = Pick<VideoChannelModel, 'id' | 'name' | 'getDisplayName'> export type VideoChannelInclude = Pick<VideoChannelModel, 'id' | 'name' | 'getDisplayName'>
@ -75,7 +75,7 @@ export module UserNotificationIncludes {
Pick<ActorModel, 'preferredUsername' | 'getHost'> & Pick<ActorModel, 'preferredUsername' | 'getHost'> &
PickWith<ActorModel, 'Account', AccountInclude> & PickWith<ActorModel, 'Account', AccountInclude> &
PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>> & PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>> &
PickWithOpt<ActorModel, 'Avatar', Pick<AvatarModel, 'filename' | 'getStaticPath'>> PickWithOpt<ActorModel, 'Avatar', Pick<ActorImageModel, 'filename' | 'getStaticPath'>>
export type ActorFollowing = export type ActorFollowing =
Pick<ActorModel, 'preferredUsername' | 'type' | 'getHost'> & Pick<ActorModel, 'preferredUsername' | 'type' | 'getHost'> &

View File

@ -1,5 +1,5 @@
import { ActorImage } from './actor-image.model'
import { Actor } from './actor.model' import { Actor } from './actor.model'
import { Avatar } from '../avatars'
export interface Account extends Actor { export interface Account extends Actor {
displayName: string displayName: string
@ -14,5 +14,5 @@ export interface AccountSummary {
displayName: string displayName: string
url: string url: string
host: string host: string
avatar?: Avatar avatar?: ActorImage
} }

View File

@ -1,4 +1,4 @@
export interface Avatar { export interface ActorImage {
path: string path: string
url?: string url?: string

View File

@ -0,0 +1,4 @@
export const enum ActorImageType {
AVATAR = 1,
BANNER = 2
}

View File

@ -1,4 +1,4 @@
import { Avatar } from '../avatars/avatar.model' import { ActorImage } from './actor-image.model'
export interface Actor { export interface Actor {
id: number id: number
@ -9,5 +9,5 @@ export interface Actor {
followersCount: number followersCount: number
createdAt: Date | string createdAt: Date | string
updatedAt: Date | string updatedAt: Date | string
avatar?: Avatar avatar?: ActorImage
} }

View File

@ -1,3 +1,5 @@
export * from './account.model' export * from './account.model'
export * from './actor-image.model'
export * from './actor-image.type'
export * from './actor.model' export * from './actor.model'
export * from './follow.model' export * from './follow.model'

View File

@ -1 +0,0 @@
export * from './avatar.model'

View File

@ -1,6 +1,5 @@
export * from './activitypub' export * from './activitypub'
export * from './actors' export * from './actors'
export * from './avatars'
export * from './moderation' export * from './moderation'
export * from './bulk' export * from './bulk'
export * from './redundancy' export * from './redundancy'

View File

@ -1,6 +1,5 @@
import { Actor } from '../../actors/actor.model' import { Actor } from '../../actors/actor.model'
import { Account } from '../../actors/index' import { Account, ActorImage } from '../../actors'
import { Avatar } from '../../avatars'
export type ViewsPerDate = { export type ViewsPerDate = {
date: Date date: Date
@ -24,5 +23,5 @@ export interface VideoChannelSummary {
displayName: string displayName: string
url: string url: string
host: string host: string
avatar?: Avatar avatar?: ActorImage
} }