mirror of
https://github.com/fluxerapp/fluxer.git
synced 2026-09-03 05:10:25 +03:00
fix(api): keep the saved placeholder on sent memes (#2085)
This commit is contained in:
@@ -14,6 +14,7 @@ import type {IFavoriteMemeRepository} from '../../../favorite_meme/IFavoriteMeme
|
||||
import type {IMediaService} from '../../../infrastructure/IMediaService';
|
||||
import type {ISnowflakeService} from '../../../infrastructure/ISnowflakeService';
|
||||
import type {IStorageService} from '../../../infrastructure/IStorageService';
|
||||
import {Logger} from '../../../Logger';
|
||||
import type {FavoriteMeme} from '../../../models/FavoriteMeme';
|
||||
import type {Message} from '../../../models/Message';
|
||||
import type {User} from '../../../models/User';
|
||||
@@ -90,8 +91,21 @@ export class MessageOperationsHelpers {
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const needsAnimationProbe =
|
||||
!favoriteMeme.isGifv &&
|
||||
favoriteMeme.contentType !== 'image/gif' &&
|
||||
favoriteMeme.contentType !== 'image/apng' &&
|
||||
ANIMATION_PROBE_CONTENT_TYPES.has(favoriteMeme.contentType);
|
||||
const metadata =
|
||||
needsAnimationProbe || favoriteMeme.placeholder == null
|
||||
? await this.probeFavoriteMemeMetadata(favoriteMeme)
|
||||
: null;
|
||||
const placeholder = favoriteMeme.placeholder ?? metadata?.placeholder ?? null;
|
||||
if (placeholder != null && favoriteMeme.placeholder == null) {
|
||||
await this.repairFavoriteMemePlaceholder(favoriteMeme, placeholder);
|
||||
}
|
||||
let flags = 0;
|
||||
if (await this.isFavoriteMemeAnimated(favoriteMeme)) {
|
||||
if (this.isFavoriteMemeAnimated(favoriteMeme, metadata)) {
|
||||
flags |= MessageAttachmentFlags.IS_ANIMATED;
|
||||
}
|
||||
return {
|
||||
@@ -104,7 +118,7 @@ export class MessageOperationsHelpers {
|
||||
height: favoriteMeme.height,
|
||||
content_type: favoriteMeme.contentType,
|
||||
content_hash: favoriteMeme.contentHash,
|
||||
placeholder: null,
|
||||
placeholder,
|
||||
flags,
|
||||
duration: favoriteMeme.duration,
|
||||
nsfw: null,
|
||||
@@ -112,20 +126,31 @@ export class MessageOperationsHelpers {
|
||||
};
|
||||
}
|
||||
|
||||
private async isFavoriteMemeAnimated(favoriteMeme: FavoriteMeme): Promise<boolean> {
|
||||
if (favoriteMeme.isGifv) return true;
|
||||
if (favoriteMeme.contentType === 'image/gif' || favoriteMeme.contentType === 'image/apng') return true;
|
||||
if (!ANIMATION_PROBE_CONTENT_TYPES.has(favoriteMeme.contentType)) return false;
|
||||
private async probeFavoriteMemeMetadata(favoriteMeme: FavoriteMeme) {
|
||||
try {
|
||||
const metadata = await this.deps.mediaService.getMetadata({
|
||||
return await this.deps.mediaService.getMetadata({
|
||||
type: 's3',
|
||||
bucket: Config.s3.buckets.cdn,
|
||||
key: favoriteMeme.storageKey,
|
||||
nsfw: 'allow',
|
||||
});
|
||||
return metadata?.animated === true;
|
||||
} catch {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async repairFavoriteMemePlaceholder(favoriteMeme: FavoriteMeme, placeholder: string): Promise<void> {
|
||||
try {
|
||||
await this.deps.favoriteMemeRepository.updatePlaceholder(favoriteMeme.userId, favoriteMeme.id, placeholder);
|
||||
} catch (error) {
|
||||
Logger.warn({error, memeId: favoriteMeme.id.toString()}, 'Failed to backfill favorite meme placeholder');
|
||||
}
|
||||
}
|
||||
|
||||
private isFavoriteMemeAnimated(favoriteMeme: FavoriteMeme, metadata: {animated?: boolean | null} | null): boolean {
|
||||
if (favoriteMeme.isGifv) return true;
|
||||
if (favoriteMeme.contentType === 'image/gif' || favoriteMeme.contentType === 'image/apng') return true;
|
||||
if (!ANIMATION_PROBE_CONTENT_TYPES.has(favoriteMeme.contentType)) return false;
|
||||
return metadata?.animated === true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import type {MemeID, UserID} from '../BrandedTypes';
|
||||
import {BatchBuilder, fetchMany, fetchOne, upsertOne} from '../database/CassandraQueryExecution';
|
||||
import {Db} from '../database/CassandraTypes';
|
||||
import type {FavoriteMemeRow} from '../database/types/UserTypes';
|
||||
import {FavoriteMeme} from '../models/FavoriteMeme';
|
||||
import {FavoriteMemes, FavoriteMemesByMemeId} from '../Tables';
|
||||
@@ -99,6 +100,10 @@ export class FavoriteMemeRepository extends IFavoriteMemeRepository {
|
||||
return new FavoriteMeme(memeRow);
|
||||
}
|
||||
|
||||
async updatePlaceholder(userId: UserID, memeId: MemeID, placeholder: string): Promise<void> {
|
||||
await fetchOne(FavoriteMemes.patchByPk({user_id: userId, meme_id: memeId}, {placeholder: Db.set(placeholder)}));
|
||||
}
|
||||
|
||||
async delete(userId: UserID, memeId: MemeID): Promise<void> {
|
||||
const batch = new BatchBuilder();
|
||||
batch.addPrepared(FavoriteMemes.deleteByPk({user_id: userId, meme_id: memeId}));
|
||||
|
||||
@@ -34,6 +34,8 @@ export abstract class IFavoriteMemeRepository {
|
||||
|
||||
abstract update(userId: UserID, memeId: MemeID, data: CreateFavoriteMemeParams): Promise<FavoriteMeme>;
|
||||
|
||||
abstract updatePlaceholder(userId: UserID, memeId: MemeID, placeholder: string): Promise<void>;
|
||||
|
||||
abstract delete(userId: UserID, memeId: MemeID): Promise<void>;
|
||||
|
||||
abstract deleteAllByUserId(userId: UserID): Promise<void>;
|
||||
|
||||
@@ -37,6 +37,7 @@ export class TestMediaService extends IMediaService {
|
||||
height: 128,
|
||||
animated: stored ? this.isAnimatedImage(stored, format) : format === 'gif',
|
||||
nsfw: false,
|
||||
placeholder: this.fakePlaceholder(request.key),
|
||||
};
|
||||
}
|
||||
if (request.type === 'upload') {
|
||||
@@ -55,6 +56,7 @@ export class TestMediaService extends IMediaService {
|
||||
height: 128,
|
||||
animated: format === 'gif',
|
||||
nsfw: false,
|
||||
placeholder: this.fakePlaceholder(request.upload_filename),
|
||||
};
|
||||
}
|
||||
if (request.type === 'external') {
|
||||
@@ -67,11 +69,16 @@ export class TestMediaService extends IMediaService {
|
||||
height: 128,
|
||||
animated: false,
|
||||
nsfw: false,
|
||||
placeholder: this.fakePlaceholder(request.url),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private fakePlaceholder(seed: string): string {
|
||||
return crypto.createHash('sha256').update(seed).digest('base64').slice(0, 24);
|
||||
}
|
||||
|
||||
getExternalMediaProxyURL(): string {
|
||||
return 'https://media-proxy.test';
|
||||
}
|
||||
|
||||
@@ -3,12 +3,15 @@
|
||||
import {MessageAttachmentFlags} from '@fluxer/constants/src/ChannelConstants';
|
||||
import {afterEach, beforeEach, describe, expect, test} from 'vitest';
|
||||
import {AttachmentDecayRepository} from '../../attachment/AttachmentDecayRepository';
|
||||
import {createAttachmentID} from '../../BrandedTypes';
|
||||
import {createAttachmentID, createMemeID, createUserID} from '../../BrandedTypes';
|
||||
import {
|
||||
createTestAccountForAttachmentTests,
|
||||
sendMessageWithAttachments,
|
||||
setupTestGuildAndChannel,
|
||||
} from '../../channel/tests/AttachmentTestUtils';
|
||||
import {fetchOne} from '../../database/CassandraQueryExecution';
|
||||
import {Db} from '../../database/CassandraTypes';
|
||||
import {FavoriteMemes} from '../../Tables';
|
||||
import {type ApiTestHarness, createApiTestHarness} from '../../test/ApiTestHarness';
|
||||
import {HTTP_STATUS} from '../../test/TestConstants';
|
||||
import {createBuilder} from '../../test/TestRequestBuilder';
|
||||
@@ -42,6 +45,24 @@ interface MessageWithDecayAttachment {
|
||||
}>;
|
||||
}
|
||||
|
||||
interface MessageWithPlaceholderAttachment {
|
||||
id: string;
|
||||
attachments: Array<{
|
||||
id: string;
|
||||
filename: string;
|
||||
placeholder?: string | null;
|
||||
}>;
|
||||
}
|
||||
|
||||
async function clearFavoriteMemePlaceholder(userId: string, memeId: string) {
|
||||
await fetchOne(
|
||||
FavoriteMemes.patchByPk(
|
||||
{user_id: createUserID(BigInt(userId)), meme_id: createMemeID(BigInt(memeId))},
|
||||
{placeholder: Db.set(null)},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async function fetchDecayRow(attachmentId: string) {
|
||||
return new AttachmentDecayRepository().fetchById(createAttachmentID(BigInt(attachmentId)));
|
||||
}
|
||||
@@ -111,6 +132,43 @@ describe('Favorite Meme Operations', () => {
|
||||
expect(sent.attachments[0].filename).toBe(filename);
|
||||
expect(sent.attachments[0].flags & MessageAttachmentFlags.IS_ANIMATED).toBe(MessageAttachmentFlags.IS_ANIMATED);
|
||||
});
|
||||
test('should carry the saved placeholder onto the sent attachment', async () => {
|
||||
const account = await createTestAccountForAttachmentTests(harness);
|
||||
const {channel} = await setupTestGuildAndChannel(harness, account);
|
||||
const message = await createMessageWithImageAttachment(harness, account.token, channel.id);
|
||||
const meme = await createFavoriteMemeFromMessage(harness, account.token, channel.id, message.id, {
|
||||
attachment_id: message.attachments[0].id,
|
||||
name: 'Placeholder Meme',
|
||||
});
|
||||
expect(meme.placeholder).toBeTruthy();
|
||||
const sent = await createBuilder<MessageWithPlaceholderAttachment>(harness, account.token)
|
||||
.post(`/channels/${channel.id}/messages`)
|
||||
.body({favorite_meme_id: meme.id})
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
expect(sent.attachments[0].placeholder).toBe(meme.placeholder);
|
||||
});
|
||||
test('should read-repair a missing placeholder when sending a favorite meme', async () => {
|
||||
const account = await createTestAccountForAttachmentTests(harness);
|
||||
const {channel} = await setupTestGuildAndChannel(harness, account);
|
||||
const message = await createMessageWithImageAttachment(harness, account.token, channel.id);
|
||||
const meme = await createFavoriteMemeFromMessage(harness, account.token, channel.id, message.id, {
|
||||
attachment_id: message.attachments[0].id,
|
||||
name: 'Repair Meme',
|
||||
});
|
||||
expect(meme.placeholder).toBeTruthy();
|
||||
await clearFavoriteMemePlaceholder(account.userId, meme.id);
|
||||
const stripped = await getFavoriteMeme(harness, account.token, meme.id);
|
||||
expect(stripped.placeholder).toBeNull();
|
||||
const sent = await createBuilder<MessageWithPlaceholderAttachment>(harness, account.token)
|
||||
.post(`/channels/${channel.id}/messages`)
|
||||
.body({favorite_meme_id: meme.id})
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
expect(sent.attachments[0].placeholder).toBeTruthy();
|
||||
const repaired = await getFavoriteMeme(harness, account.token, meme.id);
|
||||
expect(repaired.placeholder).toBe(sent.attachments[0].placeholder);
|
||||
});
|
||||
test('should create decay metadata when sending favorite meme', async () => {
|
||||
const account = await createTestAccountForAttachmentTests(harness);
|
||||
const {channel} = await setupTestGuildAndChannel(harness, account);
|
||||
|
||||
Reference in New Issue
Block a user