Support custom emoji reactions in event_stats

This commit is contained in:
Alex Gleason 2025-03-15 15:29:00 -05:00
parent 755ed884d4
commit c40c6e8b30
No known key found for this signature in database
GPG key ID: 7211D1F99744FBB7
4 changed files with 88 additions and 24 deletions

View file

@ -0,0 +1,10 @@
import { assertEquals } from '@std/assert';
import { parseEmojiInput } from './custom-emoji.ts';
Deno.test('parseEmojiInput', () => {
assertEquals(parseEmojiInput('+'), { type: 'basic', value: '+' });
assertEquals(parseEmojiInput('🚀'), { type: 'native', native: '🚀' });
assertEquals(parseEmojiInput(':ditto:'), { type: 'custom', shortcode: 'ditto' });
assertEquals(parseEmojiInput('x'), undefined);
});

View file

@ -70,3 +70,24 @@ export async function getCustomEmojis(
return emojis; return emojis;
} }
/** Determine if the input is a native or custom emoji, returning a structured object or throwing an error. */
export function parseEmojiInput(input: string):
| { type: 'basic'; value: '+' | '-' }
| { type: 'native'; native: string }
| { type: 'custom'; shortcode: string }
| undefined {
if (input === '+' || input === '-') {
return { type: 'basic', value: input };
}
if (/^\p{RGI_Emoji}$/v.test(input)) {
return { type: 'native', native: input };
}
const match = input.match(/^:(\w+):$/);
if (match) {
const [, shortcode] = match;
return { type: 'custom', shortcode };
}
}

View file

@ -141,16 +141,24 @@ Deno.test('updateStats with kind 7 increments reactions count', async () => {
const { kysely, relay } = test; const { kysely, relay } = test;
const note = genEvent({ kind: 1 }); const note = genEvent({ kind: 1 });
await updateStats({ ...test, event: note });
await relay.event(note); await relay.event(note);
await updateStats({ ...test, event: genEvent({ kind: 7, content: '+', tags: [['e', note.id]] }) }); await updateStats({ ...test, event: genEvent({ kind: 7, content: '+', tags: [['e', note.id]] }) });
await updateStats({ ...test, event: genEvent({ kind: 7, content: '😂', tags: [['e', note.id]] }) }); await updateStats({ ...test, event: genEvent({ kind: 7, content: '😂', tags: [['e', note.id]] }) });
await updateStats({
...test,
event: genEvent({
kind: 7,
content: ':ditto:',
tags: [['e', note.id], ['emoji', 'ditto', 'https://ditto.pub/favicon.ico']],
}),
});
const stats = await getEventStats(kysely, note.id); const stats = await getEventStats(kysely, note.id);
assertEquals(stats!.reactions, JSON.stringify({ '+': 1, '😂': 1 })); assertEquals(stats!.reactions, JSON.stringify({ '+': 1, '😂': 1, 'ditto:https://ditto.pub/favicon.ico': 1 }));
assertEquals(stats!.reactions_count, 2); assertEquals(stats!.reactions_count, 3);
}); });
Deno.test('updateStats with kind 5 decrements reactions count', async () => { Deno.test('updateStats with kind 5 decrements reactions count', async () => {

View file

@ -7,6 +7,7 @@ import { z } from 'zod';
import { findQuoteTag, findReplyTag, getTagSet } from '@/utils/tags.ts'; import { findQuoteTag, findReplyTag, getTagSet } from '@/utils/tags.ts';
import type { DittoConf } from '@ditto/conf'; import type { DittoConf } from '@ditto/conf';
import { parseEmojiInput } from '@/utils/custom-emoji.ts';
interface UpdateStatsOpts { interface UpdateStatsOpts {
conf: DittoConf; conf: DittoConf;
@ -154,14 +155,39 @@ async function handleEvent7(opts: UpdateStatsOpts): Promise<void> {
const { kysely, event, x = 1 } = opts; const { kysely, event, x = 1 } = opts;
const id = event.tags.findLast(([name]) => name === 'e')?.[1]; const id = event.tags.findLast(([name]) => name === 'e')?.[1];
const emoji = event.content; const result = parseEmojiInput(event.content);
if (!id || !result) return;
let url: URL | undefined;
if (result.type === 'custom') {
const tag = event.tags.find(([name, value]) => name === 'emoji' && value === result.shortcode);
try {
url = new URL(tag![2]);
} catch {
return;
}
}
let key: string;
switch (result.type) {
case 'basic':
key = result.value;
break;
case 'native':
key = result.native;
break;
case 'custom':
key = `${result.shortcode}:${url}`;
break;
}
if (id && emoji && (['+', '-'].includes(emoji) || /^\p{RGI_Emoji}$/v.test(emoji))) {
await updateEventStats(kysely, id, ({ reactions }) => { await updateEventStats(kysely, id, ({ reactions }) => {
const data: Record<string, number> = JSON.parse(reactions); const data: Record<string, number> = JSON.parse(reactions);
// Increment or decrement the emoji count. // Increment or decrement the emoji count.
data[emoji] = (data[emoji] ?? 0) + x; data[key] = (data[key] ?? 0) + x;
// Remove reactions with a count of 0 or less. // Remove reactions with a count of 0 or less.
for (const key of Object.keys(data)) { for (const key of Object.keys(data)) {
@ -179,7 +205,6 @@ async function handleEvent7(opts: UpdateStatsOpts): Promise<void> {
}; };
}); });
} }
}
/** Update stats for kind 9735 event. */ /** Update stats for kind 9735 event. */
async function handleEvent9735(opts: UpdateStatsOpts): Promise<void> { async function handleEvent9735(opts: UpdateStatsOpts): Promise<void> {