Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

chat-messages: paginate via before=<ISO>; document on api.aesthetic.computer

Endpoint now accepts before=<ISO timestamp> and returns nextBefore in the
response so callers can walk back further than the 100-message limit. Adds a
"List Chat Messages" entry to api-docs.mjs (curl/JS/Python examples) and a
"Pulling Chat Messages" section to SCORE.md covering both the public endpoint
and the lith MongoDB fallback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+217 -10
+70
SCORE.md
··· 295 295 296 296 The CLI ships with every `fish lith/deploy.fish`. If you add new telemetry, bump the payload in `disk.mjs` and the phase handler in `netlify/functions/piece-log.mjs`; no schema migration needed (MongoDB collection is schemaless). 297 297 298 + ### Pulling Chat Messages (clock / system channels) 299 + 300 + Chat lives in MongoDB. Each channel is a separate collection: 301 + 302 + - `chat-system` — the main `chat` piece (`/chat`) 303 + - `chat-clock` — the `laer-klokken` / r8dio chat piece (connects via `client.connect("clock")` in [`disks/laer-klokken.mjs`](system/public/aesthetic.computer/disks/laer-klokken.mjs)) 304 + 305 + **Public read endpoint:** [`/api/chat-messages`](system/netlify/functions/chat-messages.mjs) (GET, 2-min Redis cache): 306 + 307 + ```fish 308 + # Latest 100 clock-channel messages as chronological JSON (oldest → newest) 309 + curl -s "https://aesthetic.computer/api/chat-messages?instance=clock&limit=100" | jq 310 + 311 + # Just handle + text 312 + curl -s "https://aesthetic.computer/api/chat-messages?instance=clock&limit=100" \ 313 + | jq -r '.messages[] | "\(.when) \(.from) | \(.text)"' 314 + 315 + # Filter by sender or URL pattern (e.g. YouTube links from @prutti) 316 + curl -s "https://aesthetic.computer/api/chat-messages?instance=clock&limit=100" \ 317 + | jq -r '.messages[] 318 + | select((.from == "@prutti") or (.text | test("youtu\\.?be|youtube\\.com"; "i"))) 319 + | "\(.when) \(.from) | \(.text)"' 320 + ``` 321 + 322 + Query params: 323 + - `instance` — `system` (default) or `clock`. Any other value hits `chat-system`. 324 + - `limit` — up to **100** (over 100 returns HTTP 400). Sort is `when` descending, then reversed to chronological before returning. 325 + 326 + Response shape: `{ instance, count, messages: [{ id, from, text, when, hearts }], nextBefore }`. `from` is resolved to `@handle` via the `@handles` collection, falling back to `"anon"` for unclaimed user ids. `hearts` joins the shared `hearts` collection (`type: "chat-<instance>"`). `nextBefore` is the oldest `when` in the page, ready to hand back as `before=` for the previous page. 327 + 328 + **Going back further than 100 messages** — pass `before=<ISO>` to walk back (or use `nextBefore` from the previous response): 329 + 330 + ```fish 331 + # All @prutti YouTube links in the clock channel, paginating back 332 + cursor="" 333 + while true 334 + set url "https://aesthetic.computer/api/chat-messages?instance=clock&limit=100" 335 + test -n "$cursor"; and set url "$url&before=$cursor" 336 + set page (curl -s $url) 337 + test (echo $page | jq '.count') -eq 0; and break 338 + echo $page | jq -r '.messages[] 339 + | select(.from == "@prutti" and (.text | test("youtu"; "i"))) 340 + | "\(.when) \(.text)"' 341 + set cursor (echo $page | jq -r '.nextBefore') 342 + end 343 + ``` 344 + 345 + Full docs and `curl`/JS/Python examples live at [`/api/chat-messages` on api.aesthetic.computer](https://api.aesthetic.computer) (served by [`system/netlify/functions/api-docs.mjs`](system/netlify/functions/api-docs.mjs)). 346 + 347 + If you ever need raw Mongo access (deleted messages, admin edits, heavier aggregations), go direct from lith: 348 + 349 + ```fish 350 + # On lith (or any machine with backend creds loaded): 351 + ac-host # pick lith 352 + # then in the ssh session: 353 + cd aesthetic.computer/system 354 + node -e ' 355 + import("./backend/database.mjs").then(async ({ connect }) => { 356 + const { db, disconnect } = await connect(); 357 + const rows = await db.collection("chat-clock") 358 + .find({ when: { $lt: new Date("2026-04-22T00:00:00Z") } }) 359 + .sort({ when: -1 }).limit(500).toArray(); 360 + console.log(JSON.stringify(rows, null, 2)); 361 + await disconnect(); 362 + }); 363 + ' 364 + ``` 365 + 366 + When adding `before` pagination, update the TODO at the top of [`chat-messages.mjs`](system/netlify/functions/chat-messages.mjs) and bump the cache key so stale entries don't mask the new param. 367 + 298 368 ### Keeps Market Stats (Tezos / Objkt) 299 369 300 370 Use this flow for live Keeps market checks (`jas.tez`, `keeps.tez`, contract-level stats).
+109
system/netlify/functions/api-docs.mjs
··· 241 241 }, 242 242 243 243 { 244 + name: "List Chat Messages", 245 + method: "GET", 246 + path: "/api/chat-messages", 247 + description: "Read recent messages from a chat channel. `system` backs the main `/chat` piece; `clock` backs `laer-klokken` (r8Dio). Results are chronological (oldest → newest) within each page. Paginate further back with `before`.", 248 + authentication: "None (public read)", 249 + queryParameters: { 250 + instance: { 251 + type: "string", 252 + enum: ["system", "clock"], 253 + default: "system", 254 + description: "Chat channel to read. `system` = main chat, `clock` = laer-klokken." 255 + }, 256 + limit: { 257 + type: "number", 258 + default: 50, 259 + max: 100, 260 + description: "How many messages to return. Values over 100 return HTTP 400." 261 + }, 262 + before: { 263 + type: "string", 264 + required: false, 265 + description: "ISO-8601 timestamp. Returns messages strictly older than this — pass the `nextBefore` from the previous response to page back." 266 + } 267 + }, 268 + responseBody: { 269 + schema: { 270 + instance: { type: "string", description: "Echoes the queried channel." }, 271 + count: { type: "number", description: "Number of messages in this page." }, 272 + messages: { 273 + type: "array", 274 + description: "Chronological (oldest → newest).", 275 + items: { 276 + id: { type: "string", description: "Mongo ObjectId string." }, 277 + from: { type: "string", description: "`@handle` of the sender, or `anon` if unresolved." }, 278 + text: { type: "string", description: "Message body as posted." }, 279 + when: { type: "string", description: "ISO timestamp." }, 280 + hearts: { type: "number", description: "Heart-reaction count from the shared `hearts` collection." } 281 + } 282 + }, 283 + nextBefore: { 284 + type: "string", 285 + description: "ISO timestamp of the oldest message in this page — pass as `before=` to fetch the previous page. `null` when the page is empty." 286 + } 287 + } 288 + }, 289 + examples: [ 290 + { 291 + title: "Latest 50 messages from the main chat", 292 + description: "Default channel is `system`.", 293 + curl: `curl "https://aesthetic.computer/api/chat-messages"`, 294 + javascript: `const res = await fetch("https://aesthetic.computer/api/chat-messages"); 295 + const { messages } = await res.json(); 296 + for (const m of messages) console.log(m.when, m.from, m.text);`, 297 + python: `import requests 298 + 299 + data = requests.get("https://aesthetic.computer/api/chat-messages").json() 300 + for m in data["messages"]: 301 + print(m["when"], m["from"], m["text"])` 302 + }, 303 + { 304 + title: "Latest 100 from the laer-klokken (clock) channel", 305 + curl: `curl "https://aesthetic.computer/api/chat-messages?instance=clock&limit=100"`, 306 + javascript: `const res = await fetch( 307 + "https://aesthetic.computer/api/chat-messages?instance=clock&limit=100" 308 + ); 309 + const { messages, nextBefore } = await res.json(); 310 + console.log(messages.length, "messages; older page cursor:", nextBefore);` 311 + }, 312 + { 313 + title: "Paginate back through older messages", 314 + description: "Use `nextBefore` from each response to walk back in time.", 315 + javascript: `async function* allClockMessages() { 316 + let before; 317 + while (true) { 318 + const url = new URL("https://aesthetic.computer/api/chat-messages"); 319 + url.searchParams.set("instance", "clock"); 320 + url.searchParams.set("limit", "100"); 321 + if (before) url.searchParams.set("before", before); 322 + const res = await fetch(url); 323 + const page = await res.json(); 324 + if (page.count === 0) break; 325 + yield page.messages; 326 + before = page.nextBefore; 327 + } 328 + }`, 329 + python: `import requests 330 + 331 + def all_clock_messages(): 332 + before = None 333 + while True: 334 + params = {"instance": "clock", "limit": 100} 335 + if before: 336 + params["before"] = before 337 + page = requests.get("https://aesthetic.computer/api/chat-messages", 338 + params=params).json() 339 + if page["count"] == 0: 340 + break 341 + yield page["messages"] 342 + before = page["nextBefore"]` 343 + } 344 + ], 345 + notes: [ 346 + "Responses are cached in Redis for 2 minutes, keyed on (instance, limit, before).", 347 + "`from` falls back to `anon` when a message's author has no resolved `@handle`.", 348 + "`hearts` comes from the shared `hearts` collection (`type: chat-<instance>`)." 349 + ] 350 + }, 351 + 352 + { 244 353 name: "Store JavaScript Piece", 245 354 method: "POST", 246 355 path: "/api/store-piece",
+38 -10
system/netlify/functions/chat-messages.mjs
··· 1 1 // chat-messages, 25.11.21.18.30 2 2 // GET: Returns recent chat messages from a specific chat instance. 3 3 // Examples: "clock" for Laer-Klokken, "system" for main chat 4 - // Now with Redis caching (2 min TTL). 5 - 6 - /* #region 🏁 TODO 7 - - [] Add pagination support 8 - - [] Add date range filtering 9 - #endregion */ 4 + // Query params: 5 + // instance — "clock" or "system" (default: "system") 6 + // limit — max messages to return, up to 100 (default: 50) 7 + // before — ISO timestamp; return messages strictly older than this 8 + // (use the oldest `when` from the previous page to paginate back) 9 + // Response is still chronological (oldest → newest) within each page. 10 + // Redis caching: 2 min TTL, keyed on (instance, limit, before). 10 11 11 12 import { connect } from "../../backend/database.mjs"; 12 13 import { respond } from "../../backend/http.mjs"; ··· 31 32 return respond(400, { message: "Limit cannot exceed 100" }); 32 33 } 33 34 34 - // Cache key includes instance and limit 35 - const cacheKey = `give:chat:${instance}:${limit}`; 35 + // Optional `before` cursor for paginating further back in history. 36 + // Accepts ISO timestamp; rejected if it doesn't parse. 37 + let before = null; 38 + if (params.before) { 39 + const parsed = new Date(params.before); 40 + if (isNaN(parsed.getTime())) { 41 + return respond(400, { 42 + message: "Invalid `before` timestamp; expected ISO 8601 (e.g. 2026-04-20T00:00:00Z)", 43 + }); 44 + } 45 + before = parsed; 46 + } 47 + 48 + // Cache key includes instance, limit, and pagination cursor 49 + const cacheKey = `give:chat:${instance}:${limit}:${before ? before.toISOString() : "head"}`; 36 50 37 51 const result = await getOrCompute( 38 52 cacheKey, 39 53 async () => { 40 - shell.log(`📨 Fetching ${limit} messages for chat instance: ${instance}`); 54 + shell.log( 55 + `📨 Fetching ${limit} messages for chat instance: ${instance}` + 56 + (before ? ` (before ${before.toISOString()})` : ""), 57 + ); 41 58 42 59 const database = await connect(); 43 60 ··· 48 65 shell.log(`📂 Using collection: ${collectionName} for instance: ${instance}`); 49 66 50 67 // Query for messages, sorted by timestamp descending 68 + const filter = before ? { when: { $lt: before } } : {}; 51 69 const messages = await collection 52 - .find({}) 70 + .find(filter) 53 71 .sort({ when: -1 }) 54 72 .limit(limit) 55 73 .toArray(); ··· 101 119 102 120 shell.log(`✅ Found ${messagesWithHandles.length} messages for instance: ${instance}`); 103 121 122 + // `nextBefore` is the oldest `when` in this page, ready to be passed 123 + // back as the `before=` query to fetch the previous page. 124 + const nextBefore = 125 + messagesWithHandles.length > 0 126 + ? (messagesWithHandles[0].when instanceof Date 127 + ? messagesWithHandles[0].when.toISOString() 128 + : new Date(messagesWithHandles[0].when).toISOString()) 129 + : null; 130 + 104 131 return { 105 132 instance, 106 133 count: messagesWithHandles.length, 107 134 messages: messagesWithHandles, 135 + nextBefore, 108 136 }; 109 137 }, 110 138 CACHE_TTLS.CHAT // 2 minutes