mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 11:29:46 +00:00
Merge branch 'streak' into 'main'
Draft: Streak API See merge request soapbox-pub/ditto!638
This commit is contained in:
commit
d5b3a1f6d3
6 changed files with 84 additions and 3 deletions
|
|
@ -108,6 +108,7 @@ import {
|
||||||
zapController,
|
zapController,
|
||||||
zappedByController,
|
zappedByController,
|
||||||
} from '@/controllers/api/statuses.ts';
|
} from '@/controllers/api/statuses.ts';
|
||||||
|
import streakApp from '@/controllers/api/streak.ts';
|
||||||
import { streamingController } from '@/controllers/api/streaming.ts';
|
import { streamingController } from '@/controllers/api/streaming.ts';
|
||||||
import {
|
import {
|
||||||
localSuggestionsController,
|
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.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.post('/api/v1/ditto/names', requireSigner, nameRequestController);
|
||||||
app.get('/api/v1/ditto/names', requireSigner, nameRequestsController);
|
app.get('/api/v1/ditto/names', requireSigner, nameRequestsController);
|
||||||
|
|
||||||
|
|
|
||||||
24
src/controllers/api/streak.ts
Normal file
24
src/controllers/api/streak.ts
Normal file
|
|
@ -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;
|
||||||
|
|
@ -36,6 +36,10 @@ export interface MastodonAccount {
|
||||||
};
|
};
|
||||||
ditto: {
|
ditto: {
|
||||||
captcha_solved: boolean;
|
captcha_solved: boolean;
|
||||||
|
streak: {
|
||||||
|
days: number;
|
||||||
|
broken: number;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
statuses_count: number;
|
statuses_count: number;
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,7 @@ async function handleEvent(event: DittoEvent, opts: PipelineOpts): Promise<void>
|
||||||
handleZaps(kysely, event),
|
handleZaps(kysely, event),
|
||||||
parseMetadata(event, opts.signal),
|
parseMetadata(event, opts.signal),
|
||||||
generateSetEvents(event),
|
generateSetEvents(event),
|
||||||
|
updateStreak(event, opts),
|
||||||
])
|
])
|
||||||
.then(() =>
|
.then(() =>
|
||||||
Promise.allSettled([
|
Promise.allSettled([
|
||||||
|
|
@ -335,6 +336,47 @@ async function generateSetEvents(event: NostrEvent): Promise<void> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateStreak(event: DittoEvent, opts: PipelineOpts): Promise<void> {
|
||||||
|
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 */
|
/** Stores the event in the 'event_zaps' table */
|
||||||
async function handleZaps(kysely: Kysely<DittoTables>, event: NostrEvent) {
|
async function handleZaps(kysely: Kysely<DittoTables>, event: NostrEvent) {
|
||||||
if (event.kind !== 9735) return;
|
if (event.kind !== 9735) return;
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ function updateListEvent(
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Publish an admin event through the pipeline. */
|
/** Publish an admin event through the pipeline. */
|
||||||
async function createAdminEvent(t: EventStub, c: AppContext): Promise<NostrEvent> {
|
async function createAdminEvent(t: EventStub, c: Context): Promise<NostrEvent> {
|
||||||
const signer = new AdminSigner();
|
const signer = new AdminSigner();
|
||||||
|
|
||||||
const event = await signer.signEvent({
|
const event = await signer.signEvent({
|
||||||
|
|
@ -110,7 +110,7 @@ function updateListAdminEvent(
|
||||||
async function updateAdminEvent<E extends EventStub>(
|
async function updateAdminEvent<E extends EventStub>(
|
||||||
filter: UpdateEventFilter,
|
filter: UpdateEventFilter,
|
||||||
fn: (prev: NostrEvent | undefined) => E,
|
fn: (prev: NostrEvent | undefined) => E,
|
||||||
c: AppContext,
|
c: Context,
|
||||||
): Promise<NostrEvent> {
|
): Promise<NostrEvent> {
|
||||||
const store = await Storages.db();
|
const store = await Storages.db();
|
||||||
const [prev] = await store.query([filter], { limit: 1, signal: c.req.raw.signal });
|
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<string, boolean>, c:
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Push the event through the pipeline, rethrowing any RelayError. */
|
/** Push the event through the pipeline, rethrowing any RelayError. */
|
||||||
async function publishEvent(event: NostrEvent, c: AppContext): Promise<NostrEvent> {
|
async function publishEvent(event: NostrEvent, c: Context): Promise<NostrEvent> {
|
||||||
logi({ level: 'info', ns: 'ditto.event', source: 'api', id: event.id, kind: event.kind });
|
logi({ level: 'info', ns: 'ditto.event', source: 'api', id: event.id, kind: event.kind });
|
||||||
try {
|
try {
|
||||||
await pipeline.handleEvent(event, { source: 'api', signal: c.req.raw.signal });
|
await pipeline.handleEvent(event, { source: 'api', signal: c.req.raw.signal });
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,11 @@ async function renderAccount(
|
||||||
verified_at: null,
|
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 {
|
return {
|
||||||
id: pubkey,
|
id: pubkey,
|
||||||
acct,
|
acct,
|
||||||
|
|
@ -103,6 +108,10 @@ async function renderAccount(
|
||||||
},
|
},
|
||||||
ditto: {
|
ditto: {
|
||||||
captcha_solved: names.has('captcha_solved'),
|
captcha_solved: names.has('captcha_solved'),
|
||||||
|
streak: {
|
||||||
|
days: streakDays || 0,
|
||||||
|
broken: streakBroken || 0,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue