diff --git a/src/app.ts b/src/app.ts index df8919d..d80bf37 100644 --- a/src/app.ts +++ b/src/app.ts @@ -31,6 +31,7 @@ import { blocksController } from '@/controllers/api/blocks.ts'; import { bookmarksController } from '@/controllers/api/bookmarks.ts'; import { emptyArrayController, emptyObjectController, notImplementedController } from '@/controllers/api/fallback.ts'; import { instanceController } from '@/controllers/api/instance.ts'; +import { markersController, updateMarkersController } from '@/controllers/api/markers.ts'; import { mediaController } from '@/controllers/api/media.ts'; import { mutesController } from '@/controllers/api/mutes.ts'; import { notificationsController } from '@/controllers/api/notifications.ts'; @@ -191,6 +192,9 @@ app.get('/api/v1/bookmarks', requirePubkey, bookmarksController); app.get('/api/v1/blocks', requirePubkey, blocksController); app.get('/api/v1/mutes', requirePubkey, mutesController); +app.get('/api/v1/markers', requireProof(), markersController); +app.post('/api/v1/markers', requireProof(), updateMarkersController); + app.get('/api/v1/admin/accounts', requireRole('admin'), adminAccountsController); app.get('/api/v1/pleroma/admin/config', requireRole('admin'), configController); app.post('/api/v1/pleroma/admin/config', requireRole('admin'), updateConfigController); diff --git a/src/controllers/api/markers.ts b/src/controllers/api/markers.ts new file mode 100644 index 0000000..ce1c4ec --- /dev/null +++ b/src/controllers/api/markers.ts @@ -0,0 +1,64 @@ +import { z } from 'zod'; + +import { AppController } from '@/app.ts'; +import { parseBody } from '@/utils/api.ts'; + +const kv = await Deno.openKv(); + +type Timeline = 'home' | 'notifications'; + +interface Marker { + last_read_id: string; + version: number; + updated_at: string; +} + +export const markersController: AppController = async (c) => { + const pubkey = c.get('pubkey')!; + const timelines = c.req.queries('timeline[]') ?? []; + + const results = await kv.getMany( + timelines.map((timeline) => ['markers', pubkey, timeline]), + ); + + const marker = results.reduce>((acc, { key, value }) => { + if (value) { + const timeline = key[key.length - 1] as string; + acc[timeline] = value; + } + return acc; + }, {}); + + return c.json(marker); +}; + +const markerDataSchema = z.object({ + last_read_id: z.string(), +}); + +export const updateMarkersController: AppController = async (c) => { + const pubkey = c.get('pubkey')!; + const record = z.record(z.enum(['home', 'notifications']), markerDataSchema).parse(await parseBody(c.req.raw)); + const timelines = Object.keys(record) as Timeline[]; + + const markers: Record = {}; + + const entries = await kv.getMany( + timelines.map((timeline) => ['markers', pubkey, timeline]), + ); + + for (const timeline of timelines) { + const last = entries.find(({ key }) => key[key.length - 1] === timeline); + + const marker: Marker = { + last_read_id: record[timeline]!.last_read_id, + version: last?.value ? last.value.version + 1 : 1, + updated_at: new Date().toISOString(), + }; + + await kv.set(['markers', pubkey, timeline], marker); + markers[timeline] = marker; + } + + return c.json(markers); +};