fix: various fixes to sentry-reported errors and more

This commit is contained in:
Hampus Kraft
2026-02-18 15:38:51 +00:00
parent 302c0d2a0c
commit 0517a966a3
357 changed files with 25420 additions and 16281 deletions

View File

@@ -36,6 +36,43 @@ export class TenorResolver extends BaseResolver {
async resolve(url: URL, content: Uint8Array, isNSFWAllowed: boolean = false): Promise<Array<MessageEmbedResponse>> {
const document = parseDocument(Buffer.from(content).toString('utf-8'));
const gifEmbed = await this.resolveFromOgImage(url, document, isNSFWAllowed);
if (gifEmbed) {
return [gifEmbed];
}
return this.resolveFromJsonLd(url, document, isNSFWAllowed);
}
private async resolveFromOgImage(
url: URL,
document: Document,
isNSFWAllowed: boolean,
): Promise<MessageEmbedResponse | null> {
const ogImageUrl = this.extractMetaContent(document, 'og:image');
if (!ogImageUrl || !this.isGifUrl(ogImageUrl)) {
return null;
}
const thumbnail = await this.resolveMediaURL(url, ogImageUrl, isNSFWAllowed);
if (!thumbnail) {
return null;
}
return {
type: 'gifv',
url: url.href,
provider: {name: 'Tenor', url: 'https://tenor.com'},
thumbnail,
};
}
private async resolveFromJsonLd(
url: URL,
document: Document,
isNSFWAllowed: boolean,
): Promise<Array<MessageEmbedResponse>> {
const jsonLdContent = this.extractJsonLdContent(document);
if (!jsonLdContent) {
return [];
@@ -53,6 +90,20 @@ export class TenorResolver extends BaseResolver {
return [embed];
}
private extractMetaContent(document: Document, property: string): string | null {
const element = selectOne(`meta[property="${property}"]`, document) as Element | null;
return element?.attribs['content'] ?? null;
}
private isGifUrl(url: string): boolean {
try {
const pathname = new URL(url).pathname;
return pathname.toLowerCase().endsWith('.gif');
} catch {
return url.toLowerCase().endsWith('.gif');
}
}
private extractJsonLdContent(document: Document): TenorJsonLd | null {
const scriptElement = selectOne('script.dynamic[type="application/ld+json"]', document) as Element | null;
if (scriptElement && scriptElement.children.length > 0) {

View File

@@ -19,9 +19,10 @@
import {TenorResolver} from '@fluxer/api/src/unfurler/resolvers/TenorResolver';
import {createMockContent, MockMediaService} from '@fluxer/api/src/unfurler/tests/ResolverTestUtils';
import {EmbedMediaFlags} from '@fluxer/constants/src/ChannelConstants';
import {afterEach, beforeEach, describe, expect, it} from 'vitest';
function createTenorHtml(options: {thumbnailUrl?: string; videoUrl?: string}): string {
function createTenorHtml(options: {thumbnailUrl?: string; videoUrl?: string; ogImage?: string}): string {
const jsonLd: Record<string, unknown> = {};
if (options.thumbnailUrl) {
@@ -31,9 +32,15 @@ function createTenorHtml(options: {thumbnailUrl?: string; videoUrl?: string}): s
jsonLd.video = {contentUrl: options.videoUrl};
}
const metaTags: Array<string> = [];
if (options.ogImage) {
metaTags.push(`<meta property="og:image" content="${options.ogImage}" />`);
}
return `<!DOCTYPE html>
<html>
<head>
${metaTags.join('\n')}
<script class="dynamic" type="application/ld+json">
${JSON.stringify(jsonLd)}
</script>
@@ -100,7 +107,51 @@ describe('TenorResolver', () => {
});
describe('resolve', () => {
it('returns gifv embed with thumbnail and video', async () => {
it('prefers og:image GIF URL over JSON-LD video', async () => {
const url = new URL('https://tenor.com/view/cat-gif-12345');
const html = createTenorHtml({
ogImage: 'https://media.tenor.com/cat-gif-AAAAC/cat.gif',
thumbnailUrl: 'https://media.tenor.com/thumbnail.png',
videoUrl: 'https://media.tenor.com/video.mp4',
});
mediaService.setMetadata('https://media.tenor.com/cat-gif-AAAAC/cat.gif', {
content_type: 'image/gif',
animated: true,
width: 320,
height: 240,
});
const embeds = await resolver.resolve(url, createMockContent(html));
expect(embeds).toHaveLength(1);
expect(embeds[0]!.type).toBe('gifv');
expect(embeds[0]!.url).toBe('https://tenor.com/view/cat-gif-12345');
expect(embeds[0]!.provider).toEqual({name: 'Tenor', url: 'https://tenor.com'});
expect(embeds[0]!.thumbnail).toBeDefined();
expect(embeds[0]!.thumbnail!.url).toBe('https://media.tenor.com/cat-gif-AAAAC/cat.gif');
expect(embeds[0]!.thumbnail!.content_type).toBe('image/gif');
expect(embeds[0]!.thumbnail!.flags).toBe(EmbedMediaFlags.IS_ANIMATED);
expect(embeds[0]!.video).toBeUndefined();
});
it('falls back to JSON-LD when og:image is not a GIF', async () => {
const url = new URL('https://tenor.com/view/cat-gif-12345');
const html = createTenorHtml({
ogImage: 'https://media.tenor.com/thumbnail.png',
thumbnailUrl: 'https://media.tenor.com/thumbnail.png',
videoUrl: 'https://media.tenor.com/video.mp4',
});
const embeds = await resolver.resolve(url, createMockContent(html));
expect(embeds).toHaveLength(1);
expect(embeds[0]!.type).toBe('gifv');
expect(embeds[0]!.thumbnail).toBeDefined();
expect(embeds[0]!.video).toBeDefined();
});
it('falls back to JSON-LD when og:image is absent', async () => {
const url = new URL('https://tenor.com/view/cat-gif-12345');
const html = createTenorHtml({
thumbnailUrl: 'https://media.tenor.com/thumbnail.png',
@@ -111,11 +162,29 @@ describe('TenorResolver', () => {
expect(embeds).toHaveLength(1);
expect(embeds[0]!.type).toBe('gifv');
expect(embeds[0]!.url).toBe('https://tenor.com/view/cat-gif-12345');
expect(embeds[0]!.provider).toEqual({name: 'Tenor', url: 'https://tenor.com'});
expect(embeds[0]!.thumbnail).toBeDefined();
expect(embeds[0]!.video).toBeDefined();
});
it('handles tenor page with only thumbnail', async () => {
it('falls back to JSON-LD when og:image GIF fails to resolve', async () => {
const url = new URL('https://tenor.com/view/cat-gif-12345');
const html = createTenorHtml({
ogImage: 'https://media.tenor.com/broken.gif',
thumbnailUrl: 'https://media.tenor.com/thumbnail.png',
videoUrl: 'https://media.tenor.com/video.mp4',
});
mediaService.markAsFailing('https://media.tenor.com/broken.gif');
const embeds = await resolver.resolve(url, createMockContent(html));
expect(embeds).toHaveLength(1);
expect(embeds[0]!.type).toBe('gifv');
expect(embeds[0]!.thumbnail).toBeDefined();
expect(embeds[0]!.video).toBeDefined();
});
it('handles tenor page with only thumbnail in JSON-LD', async () => {
const url = new URL('https://tenor.com/view/cat-gif-12345');
const html = createTenorHtml({
thumbnailUrl: 'https://media.tenor.com/thumbnail.png',
@@ -128,7 +197,7 @@ describe('TenorResolver', () => {
expect(embeds[0]!.thumbnail).toBeDefined();
});
it('handles tenor page with only video', async () => {
it('handles tenor page with only video in JSON-LD', async () => {
const url = new URL('https://tenor.com/view/cat-gif-12345');
const html = createTenorHtml({
videoUrl: 'https://media.tenor.com/video.mp4',
@@ -141,7 +210,7 @@ describe('TenorResolver', () => {
expect(embeds[0]!.video).toBeDefined();
});
it('returns empty array when no JSON-LD found', async () => {
it('returns empty array when no JSON-LD found and no og:image', async () => {
const url = new URL('https://tenor.com/view/cat-gif-12345');
const html = '<!DOCTYPE html><html><head></head><body></body></html>';
@@ -150,7 +219,7 @@ describe('TenorResolver', () => {
expect(embeds).toHaveLength(0);
});
it('returns empty array when JSON-LD is empty', async () => {
it('returns empty array when JSON-LD is empty and no og:image', async () => {
const url = new URL('https://tenor.com/view/cat-gif-12345');
const html = `<!DOCTYPE html>
<html>
@@ -167,7 +236,7 @@ describe('TenorResolver', () => {
expect(embeds).toHaveLength(1);
});
it('returns empty array for invalid JSON-LD', async () => {
it('returns empty array for invalid JSON-LD and no og:image', async () => {
const url = new URL('https://tenor.com/view/cat-gif-12345');
const html = `<!DOCTYPE html>
<html>
@@ -184,25 +253,33 @@ describe('TenorResolver', () => {
expect(embeds).toHaveLength(0);
});
it('handles NSFW content flag when allowed', async () => {
it('handles NSFW content with og:image GIF', async () => {
const url = new URL('https://tenor.com/view/adult-gif-12345');
const html = createTenorHtml({
thumbnailUrl: 'https://media.tenor.com/nsfw-thumbnail.png',
videoUrl: 'https://media.tenor.com/nsfw-video.mp4',
ogImage: 'https://media.tenor.com/nsfw.gif',
});
mediaService.markAsNsfw('https://media.tenor.com/nsfw-thumbnail.png');
mediaService.markAsNsfw('https://media.tenor.com/nsfw-video.mp4');
mediaService.markAsNsfw('https://media.tenor.com/nsfw.gif');
mediaService.setMetadata('https://media.tenor.com/nsfw.gif', {
content_type: 'image/gif',
animated: true,
});
const embeds = await resolver.resolve(url, createMockContent(html), true);
expect(embeds).toHaveLength(1);
expect(embeds[0]!.thumbnail!.flags).toBe(EmbedMediaFlags.IS_ANIMATED | EmbedMediaFlags.CONTAINS_EXPLICIT_MEDIA);
});
it('preserves URL in embed output', async () => {
const url = new URL('https://tenor.com/view/special-chars-gif%20test-12345');
const html = createTenorHtml({
thumbnailUrl: 'https://media.tenor.com/thumbnail.png',
ogImage: 'https://media.tenor.com/test.gif',
});
mediaService.setMetadata('https://media.tenor.com/test.gif', {
content_type: 'image/gif',
animated: true,
});
const embeds = await resolver.resolve(url, createMockContent(html));
@@ -210,11 +287,12 @@ describe('TenorResolver', () => {
expect(embeds[0]!.url).toBe('https://tenor.com/view/special-chars-gif%20test-12345');
});
it('handles missing dynamic class on script tag', async () => {
it('handles missing dynamic class on script tag with og:image', async () => {
const url = new URL('https://tenor.com/view/cat-gif-12345');
const html = `<!DOCTYPE html>
<html>
<head>
<meta property="og:image" content="https://media.tenor.com/cat.gif" />
<script type="application/ld+json">
{"image": {"thumbnailUrl": "https://media.tenor.com/thumbnail.png"}}
</script>
@@ -222,9 +300,62 @@ describe('TenorResolver', () => {
<body></body>
</html>`;
mediaService.setMetadata('https://media.tenor.com/cat.gif', {
content_type: 'image/gif',
animated: true,
});
const embeds = await resolver.resolve(url, createMockContent(html));
expect(embeds).toHaveLength(0);
expect(embeds).toHaveLength(1);
expect(embeds[0]!.thumbnail!.url).toBe('https://media.tenor.com/cat.gif');
expect(embeds[0]!.video).toBeUndefined();
});
it('handles og:image with uppercase .GIF extension', async () => {
const url = new URL('https://tenor.com/view/cat-gif-12345');
const html = createTenorHtml({
ogImage: 'https://media.tenor.com/cat.GIF',
});
mediaService.setMetadata('https://media.tenor.com/cat.GIF', {
content_type: 'image/gif',
animated: true,
});
const embeds = await resolver.resolve(url, createMockContent(html));
expect(embeds).toHaveLength(1);
expect(embeds[0]!.thumbnail!.url).toBe('https://media.tenor.com/cat.GIF');
expect(embeds[0]!.video).toBeUndefined();
});
it('resolves og:image GIF with only og:image present (no JSON-LD)', async () => {
const url = new URL('https://tenor.com/view/cat-gif-12345');
const html = `<!DOCTYPE html>
<html>
<head>
<meta property="og:image" content="https://media.tenor.com/cat.gif" />
</head>
<body></body>
</html>`;
mediaService.setMetadata('https://media.tenor.com/cat.gif', {
content_type: 'image/gif',
animated: true,
width: 500,
height: 400,
});
const embeds = await resolver.resolve(url, createMockContent(html));
expect(embeds).toHaveLength(1);
expect(embeds[0]!.type).toBe('gifv');
expect(embeds[0]!.thumbnail).toBeDefined();
expect(embeds[0]!.thumbnail!.url).toBe('https://media.tenor.com/cat.gif');
expect(embeds[0]!.thumbnail!.width).toBe(500);
expect(embeds[0]!.thumbnail!.height).toBe(400);
expect(embeds[0]!.video).toBeUndefined();
});
});
});