aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib/activitypub
diff options
context:
space:
mode:
authorkontrollanten <6680299+kontrollanten@users.noreply.github.com>2022-02-28 08:34:43 +0100
committerGitHub <noreply@github.com>2022-02-28 08:34:43 +0100
commitd0800f7661f13fabe7bb6f4aa0ea50764f106405 (patch)
treed43e6b0b6f4a5a32e03487e6464edbcaf288be2a /server/lib/activitypub
parent5cad2ca9db9b9d138f8a33058d10b94a9fd50c69 (diff)
downloadPeerTube-d0800f7661f13fabe7bb6f4aa0ea50764f106405.tar.gz
PeerTube-d0800f7661f13fabe7bb6f4aa0ea50764f106405.tar.zst
PeerTube-d0800f7661f13fabe7bb6f4aa0ea50764f106405.zip
Implement avatar miniatures (#4639)
* client: remove unused file * refactor(client/my-actor-avatar): size from input Read size from component input instead of scss, to make it possible to use smaller avatar images when implemented. * implement avatar miniatures close #4560 * fix(test): max file size * fix(search-index): normalize res acc to avatarMini * refactor avatars to an array * client/search: resize channel avatar to 120 * refactor(client/videos): remove unused function * client(actor-avatar): set default size * fix tests and avatars full result When findOne is used only an array containting one avatar is returned. * update migration version and version notations * server/search: harmonize normalizing * Cleanup avatar miniature PR Co-authored-by: Chocobozzz <me@florianbigard.com>
Diffstat (limited to 'server/lib/activitypub')
-rw-r--r--server/lib/activitypub/actors/image.ts89
-rw-r--r--server/lib/activitypub/actors/shared/creator.ts16
-rw-r--r--server/lib/activitypub/actors/shared/object-to-model-attributes.ts56
-rw-r--r--server/lib/activitypub/actors/updater.ts12
4 files changed, 96 insertions, 77 deletions
diff --git a/server/lib/activitypub/actors/image.ts b/server/lib/activitypub/actors/image.ts
index 443ad0a63..d17c2ef1a 100644
--- a/server/lib/activitypub/actors/image.ts
+++ b/server/lib/activitypub/actors/image.ts
@@ -12,53 +12,52 @@ type ImageInfo = {
12 onDisk?: boolean 12 onDisk?: boolean
13} 13}
14 14
15async function updateActorImageInstance (actor: MActorImages, type: ActorImageType, imageInfo: ImageInfo | null, t: Transaction) { 15async function updateActorImages (actor: MActorImages, type: ActorImageType, imagesInfo: ImageInfo[], t: Transaction) {
16 const oldImageModel = type === ActorImageType.AVATAR 16 const avatarsOrBanners = type === ActorImageType.AVATAR
17 ? actor.Avatar 17 ? actor.Avatars
18 : actor.Banner 18 : actor.Banners
19 19
20 if (oldImageModel) { 20 if (imagesInfo.length === 0) {
21 // Don't update the avatar if the file URL did not change 21 await deleteActorImages(actor, type, t)
22 if (imageInfo?.fileUrl && oldImageModel.fileUrl === imageInfo.fileUrl) return actor 22 }
23
24 for (const imageInfo of imagesInfo) {
25 const oldImageModel = (avatarsOrBanners || []).find(i => i.width === imageInfo.width)
23 26
24 try { 27 if (oldImageModel) {
25 await oldImageModel.destroy({ transaction: t }) 28 // Don't update the avatar if the file URL did not change
29 if (imageInfo?.fileUrl && oldImageModel.fileUrl === imageInfo.fileUrl) {
30 continue
31 }
26 32
27 setActorImage(actor, type, null) 33 await safeDeleteActorImage(actor, oldImageModel, type, t)
28 } catch (err) {
29 logger.error('Cannot remove old actor image of actor %s.', actor.url, { err })
30 } 34 }
31 }
32 35
33 if (imageInfo) {
34 const imageModel = await ActorImageModel.create({ 36 const imageModel = await ActorImageModel.create({
35 filename: imageInfo.name, 37 filename: imageInfo.name,
36 onDisk: imageInfo.onDisk ?? false, 38 onDisk: imageInfo.onDisk ?? false,
37 fileUrl: imageInfo.fileUrl, 39 fileUrl: imageInfo.fileUrl,
38 height: imageInfo.height, 40 height: imageInfo.height,
39 width: imageInfo.width, 41 width: imageInfo.width,
40 type 42 type,
43 actorId: actor.id
41 }, { transaction: t }) 44 }, { transaction: t })
42 45
43 setActorImage(actor, type, imageModel) 46 addActorImage(actor, type, imageModel)
44 } 47 }
45 48
46 return actor 49 return actor
47} 50}
48 51
49async function deleteActorImageInstance (actor: MActorImages, type: ActorImageType, t: Transaction) { 52async function deleteActorImages (actor: MActorImages, type: ActorImageType, t: Transaction) {
50 try { 53 try {
51 if (type === ActorImageType.AVATAR) { 54 const association = buildAssociationName(type)
52 await actor.Avatar.destroy({ transaction: t })
53
54 actor.avatarId = null
55 actor.Avatar = null
56 } else {
57 await actor.Banner.destroy({ transaction: t })
58 55
59 actor.bannerId = null 56 for (const image of actor[association]) {
60 actor.Banner = null 57 await image.destroy({ transaction: t })
61 } 58 }
59
60 actor[association] = []
62 } catch (err) { 61 } catch (err) {
63 logger.error('Cannot remove old image of actor %s.', actor.url, { err }) 62 logger.error('Cannot remove old image of actor %s.', actor.url, { err })
64 } 63 }
@@ -66,29 +65,37 @@ async function deleteActorImageInstance (actor: MActorImages, type: ActorImageTy
66 return actor 65 return actor
67} 66}
68 67
68async function safeDeleteActorImage (actor: MActorImages, toDelete: MActorImage, type: ActorImageType, t: Transaction) {
69 try {
70 await toDelete.destroy({ transaction: t })
71
72 const association = buildAssociationName(type)
73 actor[association] = actor[association].filter(image => image.id !== toDelete.id)
74 } catch (err) {
75 logger.error('Cannot remove old actor image of actor %s.', actor.url, { err })
76 }
77}
78
69// --------------------------------------------------------------------------- 79// ---------------------------------------------------------------------------
70 80
71export { 81export {
72 ImageInfo, 82 ImageInfo,
73 83
74 updateActorImageInstance, 84 updateActorImages,
75 deleteActorImageInstance 85 deleteActorImages
76} 86}
77 87
78// --------------------------------------------------------------------------- 88// ---------------------------------------------------------------------------
79 89
80function setActorImage (actorModel: MActorImages, type: ActorImageType, imageModel: MActorImage) { 90function addActorImage (actor: MActorImages, type: ActorImageType, imageModel: MActorImage) {
81 const id = imageModel 91 const association = buildAssociationName(type)
82 ? imageModel.id 92 if (!actor[association]) actor[association] = []
83 : null 93
84 94 actor[association].push(imageModel)
85 if (type === ActorImageType.AVATAR) { 95}
86 actorModel.avatarId = id
87 actorModel.Avatar = imageModel
88 } else {
89 actorModel.bannerId = id
90 actorModel.Banner = imageModel
91 }
92 96
93 return actorModel 97function buildAssociationName (type: ActorImageType) {
98 return type === ActorImageType.AVATAR
99 ? 'Avatars'
100 : 'Banners'
94} 101}
diff --git a/server/lib/activitypub/actors/shared/creator.ts b/server/lib/activitypub/actors/shared/creator.ts
index 999aed97d..500bc9912 100644
--- a/server/lib/activitypub/actors/shared/creator.ts
+++ b/server/lib/activitypub/actors/shared/creator.ts
@@ -6,8 +6,8 @@ import { ServerModel } from '@server/models/server/server'
6import { VideoChannelModel } from '@server/models/video/video-channel' 6import { VideoChannelModel } from '@server/models/video/video-channel'
7import { MAccount, MAccountDefault, MActor, MActorFullActor, MActorId, MActorImages, MChannel, MServer } from '@server/types/models' 7import { MAccount, MAccountDefault, MActor, MActorFullActor, MActorId, MActorImages, MChannel, MServer } from '@server/types/models'
8import { ActivityPubActor, ActorImageType } from '@shared/models' 8import { ActivityPubActor, ActorImageType } from '@shared/models'
9import { updateActorImageInstance } from '../image' 9import { updateActorImages } from '../image'
10import { getActorAttributesFromObject, getActorDisplayNameFromObject, getImageInfoFromObject } from './object-to-model-attributes' 10import { getActorAttributesFromObject, getActorDisplayNameFromObject, getImagesInfoFromObject } from './object-to-model-attributes'
11import { fetchActorFollowsCount } from './url-to-object' 11import { fetchActorFollowsCount } from './url-to-object'
12 12
13export class APActorCreator { 13export class APActorCreator {
@@ -27,11 +27,11 @@ export class APActorCreator {
27 return sequelizeTypescript.transaction(async t => { 27 return sequelizeTypescript.transaction(async t => {
28 const server = await this.setServer(actorInstance, t) 28 const server = await this.setServer(actorInstance, t)
29 29
30 await this.setImageIfNeeded(actorInstance, ActorImageType.AVATAR, t)
31 await this.setImageIfNeeded(actorInstance, ActorImageType.BANNER, t)
32
33 const { actorCreated, created } = await this.saveActor(actorInstance, t) 30 const { actorCreated, created } = await this.saveActor(actorInstance, t)
34 31
32 await this.setImageIfNeeded(actorCreated, ActorImageType.AVATAR, t)
33 await this.setImageIfNeeded(actorCreated, ActorImageType.BANNER, t)
34
35 await this.tryToFixActorUrlIfNeeded(actorCreated, actorInstance, created, t) 35 await this.tryToFixActorUrlIfNeeded(actorCreated, actorInstance, created, t)
36 36
37 if (actorCreated.type === 'Person' || actorCreated.type === 'Application') { // Account or PeerTube instance 37 if (actorCreated.type === 'Person' || actorCreated.type === 'Application') { // Account or PeerTube instance
@@ -71,10 +71,10 @@ export class APActorCreator {
71 } 71 }
72 72
73 private async setImageIfNeeded (actor: MActor, type: ActorImageType, t: Transaction) { 73 private async setImageIfNeeded (actor: MActor, type: ActorImageType, t: Transaction) {
74 const imageInfo = getImageInfoFromObject(this.actorObject, type) 74 const imagesInfo = getImagesInfoFromObject(this.actorObject, type)
75 if (!imageInfo) return 75 if (imagesInfo.length === 0) return
76 76
77 return updateActorImageInstance(actor as MActorImages, type, imageInfo, t) 77 return updateActorImages(actor as MActorImages, type, imagesInfo, t)
78 } 78 }
79 79
80 private async saveActor (actor: MActor, t: Transaction) { 80 private async saveActor (actor: MActor, t: Transaction) {
diff --git a/server/lib/activitypub/actors/shared/object-to-model-attributes.ts b/server/lib/activitypub/actors/shared/object-to-model-attributes.ts
index 23bc972e5..f6a78c457 100644
--- a/server/lib/activitypub/actors/shared/object-to-model-attributes.ts
+++ b/server/lib/activitypub/actors/shared/object-to-model-attributes.ts
@@ -4,7 +4,7 @@ import { ActorModel } from '@server/models/actor/actor'
4import { FilteredModelAttributes } from '@server/types' 4import { FilteredModelAttributes } from '@server/types'
5import { getLowercaseExtension } from '@shared/core-utils' 5import { getLowercaseExtension } from '@shared/core-utils'
6import { buildUUID } from '@shared/extra-utils' 6import { buildUUID } from '@shared/extra-utils'
7import { ActivityPubActor, ActorImageType } from '@shared/models' 7import { ActivityIconObject, ActivityPubActor, ActorImageType } from '@shared/models'
8 8
9function getActorAttributesFromObject ( 9function getActorAttributesFromObject (
10 actorObject: ActivityPubActor, 10 actorObject: ActivityPubActor,
@@ -30,33 +30,36 @@ function getActorAttributesFromObject (
30 } 30 }
31} 31}
32 32
33function getImageInfoFromObject (actorObject: ActivityPubActor, type: ActorImageType) { 33function getImagesInfoFromObject (actorObject: ActivityPubActor, type: ActorImageType) {
34 const mimetypes = MIMETYPES.IMAGE 34 const iconsOrImages = type === ActorImageType.AVATAR
35 const icon = type === ActorImageType.AVATAR 35 ? actorObject.icons || actorObject.icon
36 ? actorObject.icon
37 : actorObject.image 36 : actorObject.image
38 37
39 if (!icon || icon.type !== 'Image' || !isActivityPubUrlValid(icon.url)) return undefined 38 return normalizeIconOrImage(iconsOrImages).map(iconOrImage => {
39 const mimetypes = MIMETYPES.IMAGE
40 40
41 let extension: string 41 if (iconOrImage.type !== 'Image' || !isActivityPubUrlValid(iconOrImage.url)) return undefined
42 42
43 if (icon.mediaType) { 43 let extension: string
44 extension = mimetypes.MIMETYPE_EXT[icon.mediaType]
45 } else {
46 const tmp = getLowercaseExtension(icon.url)
47 44
48 if (mimetypes.EXT_MIMETYPE[tmp] !== undefined) extension = tmp 45 if (iconOrImage.mediaType) {
49 } 46 extension = mimetypes.MIMETYPE_EXT[iconOrImage.mediaType]
47 } else {
48 const tmp = getLowercaseExtension(iconOrImage.url)
50 49
51 if (!extension) return undefined 50 if (mimetypes.EXT_MIMETYPE[tmp] !== undefined) extension = tmp
51 }
52 52
53 return { 53 if (!extension) return undefined
54 name: buildUUID() + extension, 54
55 fileUrl: icon.url, 55 return {
56 height: icon.height, 56 name: buildUUID() + extension,
57 width: icon.width, 57 fileUrl: iconOrImage.url,
58 type 58 height: iconOrImage.height,
59 } 59 width: iconOrImage.width,
60 type
61 }
62 })
60} 63}
61 64
62function getActorDisplayNameFromObject (actorObject: ActivityPubActor) { 65function getActorDisplayNameFromObject (actorObject: ActivityPubActor) {
@@ -65,6 +68,15 @@ function getActorDisplayNameFromObject (actorObject: ActivityPubActor) {
65 68
66export { 69export {
67 getActorAttributesFromObject, 70 getActorAttributesFromObject,
68 getImageInfoFromObject, 71 getImagesInfoFromObject,
69 getActorDisplayNameFromObject 72 getActorDisplayNameFromObject
70} 73}
74
75// ---------------------------------------------------------------------------
76
77function normalizeIconOrImage (icon: ActivityIconObject | ActivityIconObject[]): ActivityIconObject[] {
78 if (Array.isArray(icon)) return icon
79 if (icon) return [ icon ]
80
81 return []
82}
diff --git a/server/lib/activitypub/actors/updater.ts b/server/lib/activitypub/actors/updater.ts
index 042438d9c..fe94af9f1 100644
--- a/server/lib/activitypub/actors/updater.ts
+++ b/server/lib/activitypub/actors/updater.ts
@@ -5,9 +5,9 @@ import { VideoChannelModel } from '@server/models/video/video-channel'
5import { MAccount, MActor, MActorFull, MChannel } from '@server/types/models' 5import { MAccount, MActor, MActorFull, MChannel } from '@server/types/models'
6import { ActivityPubActor, ActorImageType } from '@shared/models' 6import { ActivityPubActor, ActorImageType } from '@shared/models'
7import { getOrCreateAPOwner } from './get' 7import { getOrCreateAPOwner } from './get'
8import { updateActorImageInstance } from './image' 8import { updateActorImages } from './image'
9import { fetchActorFollowsCount } from './shared' 9import { fetchActorFollowsCount } from './shared'
10import { getImageInfoFromObject } from './shared/object-to-model-attributes' 10import { getImagesInfoFromObject } from './shared/object-to-model-attributes'
11 11
12export class APActorUpdater { 12export class APActorUpdater {
13 13
@@ -29,8 +29,8 @@ export class APActorUpdater {
29 } 29 }
30 30
31 async update () { 31 async update () {
32 const avatarInfo = getImageInfoFromObject(this.actorObject, ActorImageType.AVATAR) 32 const avatarsInfo = getImagesInfoFromObject(this.actorObject, ActorImageType.AVATAR)
33 const bannerInfo = getImageInfoFromObject(this.actorObject, ActorImageType.BANNER) 33 const bannersInfo = getImagesInfoFromObject(this.actorObject, ActorImageType.BANNER)
34 34
35 try { 35 try {
36 await this.updateActorInstance(this.actor, this.actorObject) 36 await this.updateActorInstance(this.actor, this.actorObject)
@@ -47,8 +47,8 @@ export class APActorUpdater {
47 } 47 }
48 48
49 await runInReadCommittedTransaction(async t => { 49 await runInReadCommittedTransaction(async t => {
50 await updateActorImageInstance(this.actor, ActorImageType.AVATAR, avatarInfo, t) 50 await updateActorImages(this.actor, ActorImageType.BANNER, bannersInfo, t)
51 await updateActorImageInstance(this.actor, ActorImageType.BANNER, bannerInfo, t) 51 await updateActorImages(this.actor, ActorImageType.AVATAR, avatarsInfo, t)
52 }) 52 })
53 53
54 await runInReadCommittedTransaction(async t => { 54 await runInReadCommittedTransaction(async t => {