From c40c6e8b300c8af147aa5bfed518397fff4506cb Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 15 Mar 2025 15:29:00 -0500 Subject: [PATCH] Support custom emoji reactions in event_stats --- packages/ditto/utils/custom-emoji.test.ts | 10 ++++ packages/ditto/utils/custom-emoji.ts | 21 +++++++ packages/ditto/utils/stats.test.ts | 14 ++++- packages/ditto/utils/stats.ts | 67 ++++++++++++++++------- 4 files changed, 88 insertions(+), 24 deletions(-) create mode 100644 packages/ditto/utils/custom-emoji.test.ts diff --git a/packages/ditto/utils/custom-emoji.test.ts b/packages/ditto/utils/custom-emoji.test.ts new file mode 100644 index 00000000..e9093e4e --- /dev/null +++ b/packages/ditto/utils/custom-emoji.test.ts @@ -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); +}); diff --git a/packages/ditto/utils/custom-emoji.ts b/packages/ditto/utils/custom-emoji.ts index a7a83a31..a95bacdb 100644 --- a/packages/ditto/utils/custom-emoji.ts +++ b/packages/ditto/utils/custom-emoji.ts @@ -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 }; + } +} diff --git a/packages/ditto/utils/stats.test.ts b/packages/ditto/utils/stats.test.ts index 2ebcab94..0fe7b0ae 100644 --- a/packages/ditto/utils/stats.test.ts +++ b/packages/ditto/utils/stats.test.ts @@ -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 () => { diff --git a/packages/ditto/utils/stats.ts b/packages/ditto/utils/stats.ts index 922d5dca..576c6e01 100644 --- a/packages/ditto/utils/stats.ts +++ b/packages/ditto/utils/stats.ts @@ -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 { 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 = 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 = 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. */