From b15838a08f366824fbeb4394b8402d08a751d551 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 5 Feb 2025 17:06:11 -0600 Subject: [PATCH 1/3] Start the streak through the pipeline --- src/pipeline.ts | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/pipeline.ts b/src/pipeline.ts index a4161233..a99ec988 100644 --- a/src/pipeline.ts +++ b/src/pipeline.ts @@ -121,6 +121,7 @@ async function handleEvent(event: DittoEvent, opts: PipelineOpts): Promise handleZaps(kysely, event), parseMetadata(event, opts.signal), generateSetEvents(event), + updateStreak(event, opts), ]) .then(() => Promise.allSettled([ @@ -335,6 +336,47 @@ async function generateSetEvents(event: NostrEvent): Promise { } } +async function updateStreak(event: DittoEvent, opts: PipelineOpts): Promise { + const { pubkey, user } = event; + + if (event.kind !== 1) { + return; // Only kind 1 events contribute to streaks. + } + + if (!event.user && opts.source !== 'api') { + return; // Create new user events only if the event was created through the API. + } + + const ts = Math.floor(Date.now() / 1000); + const signer = new AdminSigner(); + + const t = user ?? { kind: 30382, content: '', tags: [['d', pubkey]], created_at: ts }; + + const start = parseInt(t.tags.find(([name]) => name === 'ditto.streak.start')?.[1]!); + const end = parseInt(t.tags.find(([name]) => name === 'ditto.streak.end')?.[1]!); + + if (end - start > 86400) { + return; // Streak is broken. + } + + if (event.created_at <= end) { + return; // Streak cannot go backwards in time. + } + + const tags = t.tags.filter(([name]) => !['ditto.streak.start', 'ditto.streak.end'].includes(name)); + + tags.push(['ditto.streak.start', (start || event.created_at).toString()]); + tags.push(['ditto.streak.end', event.created_at.toString(), event.id]); + + const updated = await signer.signEvent({ + ...t, + tags, + created_at: ts, + }); + + await handleEvent(updated, { source: 'pipeline', signal: AbortSignal.timeout(1000) }); +} + /** Stores the event in the 'event_zaps' table */ async function handleZaps(kysely: Kysely, event: NostrEvent) { if (event.kind !== 9735) return; From 430fd196d7b55753863b5703288237c7790aed80 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 5 Feb 2025 17:23:54 -0600 Subject: [PATCH 2/3] Return streak data through the accounts API --- src/entities/MastodonAccount.ts | 4 ++++ src/views/mastodon/accounts.ts | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/src/entities/MastodonAccount.ts b/src/entities/MastodonAccount.ts index 99409c6a..9cef0ba7 100644 --- a/src/entities/MastodonAccount.ts +++ b/src/entities/MastodonAccount.ts @@ -36,6 +36,10 @@ export interface MastodonAccount { }; ditto: { captcha_solved: boolean; + streak: { + days: number; + broken: number; + }; }; }; statuses_count: number; diff --git a/src/views/mastodon/accounts.ts b/src/views/mastodon/accounts.ts index 025737c3..5beb8d81 100644 --- a/src/views/mastodon/accounts.ts +++ b/src/views/mastodon/accounts.ts @@ -69,6 +69,11 @@ async function renderAccount( verified_at: null, })) ?? []; + const streakStart = parseInt(event.user?.tags.find(([name]) => name === 'ditto.streak.start')?.[1]!); + const streakEnd = parseInt(event.user?.tags.find(([name]) => name === 'ditto.streak.end')?.[1]!); + const streakDays = Math.ceil((streakEnd - streakStart) / 86400); + const streakBroken = Math.floor((nostrNow() - streakEnd) / 86400); + return { id: pubkey, acct, @@ -103,6 +108,10 @@ async function renderAccount( }, ditto: { captcha_solved: names.has('captcha_solved'), + streak: { + days: streakDays || 0, + broken: streakBroken || 0, + }, }, } : undefined, From ca9246a20ac91d10c8085dc4fdd29abdd0f9344b Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 5 Feb 2025 18:05:07 -0600 Subject: [PATCH 3/3] Add "streak" app, letting users forfeit through the API --- src/app.ts | 2 ++ src/controllers/api/streak.ts | 24 ++++++++++++++++++++++++ src/utils/api.ts | 6 +++--- 3 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 src/controllers/api/streak.ts diff --git a/src/app.ts b/src/app.ts index 6929757f..e02e2dd0 100644 --- a/src/app.ts +++ b/src/app.ts @@ -108,6 +108,7 @@ import { zapController, zappedByController, } from '@/controllers/api/statuses.ts'; +import streakApp from '@/controllers/api/streak.ts'; import { streamingController } from '@/controllers/api/streaming.ts'; import { localSuggestionsController, @@ -382,6 +383,7 @@ app.put('/api/v1/admin/ditto/relays', requireRole('admin'), adminSetRelaysContro app.put('/api/v1/admin/ditto/instance', requireRole('admin'), updateInstanceController); +app.route('/api/v1/ditto/streak', streakApp); app.post('/api/v1/ditto/names', requireSigner, nameRequestController); app.get('/api/v1/ditto/names', requireSigner, nameRequestsController); diff --git a/src/controllers/api/streak.ts b/src/controllers/api/streak.ts new file mode 100644 index 00000000..5022c6d6 --- /dev/null +++ b/src/controllers/api/streak.ts @@ -0,0 +1,24 @@ +import { Hono } from '@hono/hono'; + +import { Conf } from '@/config.ts'; +import { requireSigner } from '@/middleware/requireSigner.ts'; +import { updateAdminEvent } from '@/utils/api.ts'; + +const app = new Hono(); + +app.post('/forfeit', requireSigner, async (c) => { + const pubkey = await c.get('signer').getPublicKey(); + + await updateAdminEvent( + { kinds: [30382], authors: [Conf.pubkey], '#d': [pubkey], limit: 1 }, + (prev) => { + const tags = prev?.tags.filter(([name]) => !['ditto.streak.start', 'ditto.streak.end'].includes(name)) ?? []; + return { ...prev, kind: 30382, tags }; + }, + c, + ); + + return c.newResponse(null, 204); +}); + +export default app; diff --git a/src/utils/api.ts b/src/utils/api.ts index 29304cbd..1e2dc70b 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -80,7 +80,7 @@ function updateListEvent( } /** Publish an admin event through the pipeline. */ -async function createAdminEvent(t: EventStub, c: AppContext): Promise { +async function createAdminEvent(t: EventStub, c: Context): Promise { const signer = new AdminSigner(); const event = await signer.signEvent({ @@ -110,7 +110,7 @@ function updateListAdminEvent( async function updateAdminEvent( filter: UpdateEventFilter, fn: (prev: NostrEvent | undefined) => E, - c: AppContext, + c: Context, ): Promise { const store = await Storages.db(); const [prev] = await store.query([filter], { limit: 1, signal: c.req.raw.signal }); @@ -156,7 +156,7 @@ async function updateNames(k: number, d: string, n: Record, c: } /** Push the event through the pipeline, rethrowing any RelayError. */ -async function publishEvent(event: NostrEvent, c: AppContext): Promise { +async function publishEvent(event: NostrEvent, c: Context): Promise { logi({ level: 'info', ns: 'ditto.event', source: 'api', id: event.id, kind: event.kind }); try { await pipeline.handleEvent(event, { source: 'api', signal: c.req.raw.signal });