diff --git a/src/cron.ts b/src/cron.ts index 53714e8..b0d08f9 100644 --- a/src/cron.ts +++ b/src/cron.ts @@ -1,11 +1,11 @@ import { Stickynotes } from '@soapbox/stickynotes'; import { DittoDB } from '@/db/DittoDB.ts'; -import { getTrendingEvents } from '@/trends/trending-events.ts'; -import { Time } from '@/utils/time.ts'; -import { AdminSigner } from '@/signers/AdminSigner.ts'; import { handleEvent } from '@/pipeline.ts'; -import { getTrendingHashtags } from '@/trends/trending-hashtags.ts'; +import { AdminSigner } from '@/signers/AdminSigner.ts'; +import { getTrendingEvents } from '@/trends/trending-events.ts'; +import { getTrendingTagValues } from '@/trends/trending-tag-values.ts'; +import { Time } from '@/utils/time.ts'; const console = new Stickynotes('ditto:trends'); @@ -44,10 +44,17 @@ async function updateTrendingNotesCache() { async function updateTrendingHashtagsCache() { console.info('Updating trending hashtags cache...'); const kysely = await DittoDB.getInstance(); - const yesterday = Math.floor((Date.now() - Time.days(1)) / 1000); const signal = AbortSignal.timeout(1000); - const hashtags = await getTrendingHashtags(kysely, { since: yesterday, limit: 20, threshold: 3 }); + const yesterday = Math.floor((Date.now() - Time.days(1)) / 1000); + const now = Math.floor(Date.now() / 1000); + + const hashtags = await getTrendingTagValues(kysely, 't', { + since: yesterday, + until: now, + limit: 20, + }); + const signer = new AdminSigner(); const label = await signer.signEvent({ @@ -56,7 +63,7 @@ async function updateTrendingHashtagsCache() { tags: [ ['L', 'pub.ditto.trends'], ['l', 'hashtags', 'pub.ditto.trends'], - ...hashtags.map(({ tag }) => ['t', tag]), + ...hashtags.map(({ value }) => ['t', value]), ], created_at: Math.floor(Date.now() / 1000), }); diff --git a/src/trends/trending-hashtags.ts b/src/trends/trending-hashtags.ts deleted file mode 100644 index a70acb9..0000000 --- a/src/trends/trending-hashtags.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Kysely } from 'kysely'; - -import { DittoTables } from '@/db/DittoTables.ts'; - -interface GetTrendingHashtagsOpts { - /** Unix timestamp in _seconds_ for the starting point of this query. */ - since: number; - /** Maximum number of trending hashtags to return. */ - limit: number; - /** Minimum number of unique accounts that have used a hashtag to be considered trending. */ - threshold: number; -} - -/** Get the trending hashtags in the given time frame. */ -export async function getTrendingHashtags( - /** Kysely instance to execute queries on. */ - kysely: Kysely, - /** Options for this query. */ - opts: GetTrendingHashtagsOpts, -): Promise<{ tag: string; accounts: number; uses: number }[]> { - const { since, limit, threshold } = opts; - - return await kysely - .selectFrom('nostr_tags') - .innerJoin('nostr_events', 'nostr_events.id', 'nostr_tags.event_id') - .select(({ fn }) => [ - 'nostr_tags.value as tag', - fn.agg('count', ['nostr_events.pubkey']).distinct().as('accounts'), - fn.countAll().as('uses'), - ]) - .where('nostr_tags.name', '=', 't') - .where('nostr_events.created_at', '>', since) - .groupBy('nostr_tags.value') - .having((c) => c(c.fn.agg('count', ['nostr_events.pubkey']).distinct(), '>=', threshold)) - .orderBy((c) => c.fn.agg('count', ['nostr_events.pubkey']).distinct(), 'desc') - .limit(limit) - .execute(); -} diff --git a/src/trends/trending-tag-values.ts b/src/trends/trending-tag-values.ts new file mode 100644 index 0000000..aec0ddf --- /dev/null +++ b/src/trends/trending-tag-values.ts @@ -0,0 +1,47 @@ +import { NostrFilter } from '@nostrify/nostrify'; +import { Kysely } from 'kysely'; + +import { DittoTables } from '@/db/DittoTables.ts'; + +/** Get trending tag values for a given tag in the given time frame. */ +export async function getTrendingTagValues( + /** Kysely instance to execute queries on. */ + kysely: Kysely, + /** Tag name to filter by, eg `t` or `r`. */ + tagName: string, + /** Filter of eligible events. */ + filter: NostrFilter, +): Promise<{ value: string; authors: number; uses: number }[]> { + let query = kysely + .selectFrom('nostr_tags') + .innerJoin('nostr_events', 'nostr_events.id', 'nostr_tags.event_id') + .select(({ fn }) => [ + 'nostr_tags.value', + fn.agg('count', ['nostr_events.pubkey']).distinct().as('authors'), + fn.countAll().as('uses'), + ]) + .where('nostr_tags.name', '=', tagName) + .groupBy('nostr_tags.value') + .orderBy((c) => c.fn.agg('count', ['nostr_events.pubkey']).distinct(), 'desc'); + + if (filter.kinds) { + query = query.where('nostr_events.kind', 'in', filter.kinds); + } + if (typeof filter.since === 'number') { + query = query.where('nostr_events.created_at', '>=', filter.since); + } + if (typeof filter.until === 'number') { + query = query.where('nostr_events.created_at', '<=', filter.until); + } + if (typeof filter.limit === 'number') { + query = query.limit(filter.limit); + } + + const rows = await query.execute(); + + return rows.map((row) => ({ + value: row.value, + authors: Number(row.authors), + uses: Number(row.uses), + })); +}