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;
}
/** 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 note = genEvent({ kind: 1 });
await updateStats({ ...test, 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: ':ditto:',
tags: [['e', note.id], ['emoji', 'ditto', 'https://ditto.pub/favicon.ico']],
}),
});
const stats = await getEventStats(kysely, note.id);
assertEquals(stats!.reactions, JSON.stringify({ '+': 1, '😂': 1 }));
assertEquals(stats!.reactions_count, 2);
assertEquals(stats!.reactions, JSON.stringify({ '+': 1, '😂': 1, 'ditto:https://ditto.pub/favicon.ico': 1 }));
assertEquals(stats!.reactions_count, 3);
});
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 type { DittoConf } from '@ditto/conf';
import { parseEmojiInput } from '@/utils/custom-emoji.ts';
interface UpdateStatsOpts {
conf: DittoConf;
@ -154,31 +155,55 @@ async function handleEvent7(opts: UpdateStatsOpts): Promise<void> {
const { kysely, event, x = 1 } = opts;
const id = event.tags.findLast(([name]) => name === 'e')?.[1];
const emoji = event.content;
const result = parseEmojiInput(event.content);
if (id && emoji && (['+', '-'].includes(emoji) || /^\p{RGI_Emoji}$/v.test(emoji))) {
await updateEventStats(kysely, id, ({ reactions }) => {
const data: Record<string, number> = JSON.parse(reactions);
if (!id || !result) return;
// Increment or decrement the emoji count.
data[emoji] = (data[emoji] ?? 0) + x;
let url: URL | undefined;
// Remove reactions with a count of 0 or less.
for (const key of Object.keys(data)) {
if (data[key] < 1) {
delete data[key];
}
}
// Total reactions count.
const count = Object.values(data).reduce((result, value) => result + value, 0);
return {
reactions: JSON.stringify(data),
reactions_count: count,
};
});
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;
}
await updateEventStats(kysely, id, ({ reactions }) => {
const data: Record<string, number> = JSON.parse(reactions);
// Increment or decrement the emoji count.
data[key] = (data[key] ?? 0) + x;
// Remove reactions with a count of 0 or less.
for (const key of Object.keys(data)) {
if (data[key] < 1) {
delete data[key];
}
}
// Total reactions count.
const count = Object.values(data).reduce((result, value) => result + value, 0);
return {
reactions: JSON.stringify(data),
reactions_count: count,
};
});
}
/** Update stats for kind 9735 event. */