diff --git a/src/app.ts b/src/app.ts index 9b82bee..86faa24 100644 --- a/src/app.ts +++ b/src/app.ts @@ -74,7 +74,7 @@ import { homeTimelineController, publicTimelineController, } from '@/controllers/api/timelines.ts'; -import { trendingTagsController } from '@/controllers/api/trends.ts'; +import { trendingStatusesController, trendingTagsController } from '@/controllers/api/trends.ts'; import { indexController } from '@/controllers/site.ts'; import { nodeInfoController, nodeInfoSchemaController } from '@/controllers/well-known/nodeinfo.ts'; import { nostrController } from '@/controllers/well-known/nostr.ts'; @@ -194,6 +194,7 @@ app.get('/api/v2/search', searchController); app.get('/api/pleroma/frontend_configurations', frontendConfigController); +app.get('/api/v1/trends/statuses', trendingStatusesController); app.get('/api/v1/trends/tags', trendingTagsController); app.get('/api/v1/trends', trendingTagsController); diff --git a/src/controllers/api/trends.ts b/src/controllers/api/trends.ts index 64b3019..b5a56a4 100644 --- a/src/controllers/api/trends.ts +++ b/src/controllers/api/trends.ts @@ -1,29 +1,35 @@ +import { NostrEvent } from '@nostrify/nostrify'; +import { sql } from 'kysely'; import { z } from 'zod'; import { type AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; +import { DittoDB } from '@/db/DittoDB.ts'; +import { hydrateEvents } from '@/storages/hydrate.ts'; +import { Storages } from '@/storages.ts'; import { Time } from '@/utils.ts'; import { stripTime } from '@/utils/time.ts'; +import { renderStatus } from '@/views/mastodon/statuses.ts'; import { TrendsWorker } from '@/workers/trends.ts'; await TrendsWorker.open('data/trends.sqlite3'); const limitSchema = z.coerce.number().catch(10).transform((value) => Math.min(Math.max(value, 0), 20)); -let cache = getTrends(); +let trendingHashtagsCache = getTrendingHashtags(); Deno.cron('update trends cache', { minute: { every: 15 } }, async () => { - const trends = await getTrends(); - cache = Promise.resolve(trends); + const trends = await getTrendingHashtags(); + trendingHashtagsCache = Promise.resolve(trends); }); const trendingTagsController: AppController = async (c) => { const limit = limitSchema.parse(c.req.query('limit')); - const trends = await cache; + const trends = await trendingHashtagsCache; return c.json(trends.slice(0, limit)); }; -async function getTrends() { +async function getTrendingHashtags() { const now = new Date(); const yesterday = new Date(now.getTime() - Time.days(1)); const lastWeek = new Date(now.getTime() - Time.days(7)); @@ -62,4 +68,49 @@ async function getTrends() { }))); } -export { trendingTagsController }; +let trendingNotesCache = getTrendingNotes(); + +Deno.cron('update trending notes cache', { minute: { every: 15 } }, async () => { + const events = await getTrendingNotes(); + trendingNotesCache = Promise.resolve(events); +}); + +const trendingStatusesController: AppController = async (c) => { + const store = await Storages.db(); + const limit = limitSchema.parse(c.req.query('limit')); + + const events = await trendingNotesCache + .then((events) => events.slice(0, limit)) + .then((events) => hydrateEvents({ events, store })); + + const statuses = await Promise.all( + events.map((event) => renderStatus(event, {})), + ); + + return c.json(statuses.filter(Boolean)); +}; + +async function getTrendingNotes(): Promise { + const kysely = await DittoDB.getInstance(); + const since = Math.floor((Date.now() - Time.days(1)) / 1000); + + const rows = await kysely + .selectFrom('nostr_events') + .selectAll('nostr_events') + .innerJoin('event_stats', 'event_stats.event_id', 'nostr_events.id') + .where('nostr_events.kind', '=', 1) + .where('nostr_events.created_at', '>', since) + .orderBy( + sql`(event_stats.reposts_count * 2) + (event_stats.replies_count) + (event_stats.reactions_count)`, + 'desc', + ) + .limit(20) + .execute(); + + return rows.map((row) => ({ + ...row, + tags: JSON.parse(row.tags), + })); +} + +export { trendingStatusesController, trendingTagsController };