Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178// chat-messages, 25.11.21.18.30// GET: Returns recent chat messages from a specific chat instance.// Examples: "clock" for Laer-Klokken, "system" for main chat// Query params:// instance — "clock", "system", or "all" (default: "system")// limit — max messages to return, up to 100 (default: 50)// before — ISO timestamp; return messages strictly older than this// (use the oldest `when` from the previous page to paginate back)// search/q — case-insensitive substring filter over message text.// When set, results are newest-first relevance (no reverse) and// `instance=all` searches both chat-system and chat-clock.// Response is chronological (oldest → newest) per page UNLESS searching.// Redis caching: 2 min TTL, keyed on (instance, limit, before, search).
import { connect } from "../../backend/database.mjs";import { respond } from "../../backend/http.mjs";import { shell } from "../../backend/shell.mjs";import { getOrCompute, CACHE_TTLS } from "../../backend/cache.mjs";
export async function handler(event, context) { if (event.httpMethod === "OPTIONS") { return respond(204, null); }
if (event.httpMethod !== "GET") { return respond(405, { message: "Method Not Allowed" }); }
try { const params = event.queryStringParameters || {}; const instance = params.instance || "system"; const limit = parseInt(params.limit || "50", 10);
if (limit > 100) { return respond(400, { message: "Limit cannot exceed 100" }); }
// Optional `before` cursor for paginating further back in history. // Accepts ISO timestamp; rejected if it doesn't parse. let before = null; if (params.before) { const parsed = new Date(params.before); if (isNaN(parsed.getTime())) { return respond(400, { message: "Invalid `before` timestamp; expected ISO 8601 (e.g. 2026-04-20T00:00:00Z)", }); } before = parsed; }
// Optional case-insensitive text search. When present, `instance=all` // spans both chat-system and chat-clock, and results are newest-first // relevance (not reversed to chronological). const searchRaw = (params.search || params.q || "").trim(); const searching = searchRaw.length > 0; const escaped = searchRaw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
// Cache key includes instance, limit, pagination cursor, and search const cacheKey = `give:chat:${instance}:${limit}:${before ? before.toISOString() : "head"}:${searching ? "q=" + searchRaw.toLowerCase() : "noq"}`;
const result = await getOrCompute( cacheKey, async () => { shell.log( `📨 Fetching ${limit} messages for chat instance: ${instance}` + (before ? ` (before ${before.toISOString()})` : "") + (searching ? ` (search "${searchRaw}")` : ""), );
const database = await connect();
// "all" is only meaningful while searching; otherwise single instance. const instances = instance === "all" ? ["system", "clock"] : [instance];
const baseFilter = {}; if (before) baseFilter.when = { $lt: before }; if (searching) baseFilter.text = { $regex: escaped, $options: "i" };
// Gather from each instance, tagging the source. let gathered = []; for (const inst of instances) { const collectionName = inst === "clock" ? "chat-clock" : "chat-system"; const collection = database.db.collection(collectionName); shell.log(`📂 Using collection: ${collectionName} for instance: ${inst}`); const rows = await collection .find(baseFilter) .sort({ when: -1 }) .limit(limit) .toArray(); for (const r of rows) r.__instance = inst; gathered = gathered.concat(rows); }
// Merge across instances by recency, then cap to the requested limit. gathered.sort((a, b) => { const ta = a.when instanceof Date ? a.when.getTime() : new Date(a.when).getTime(); const tb = b.when instanceof Date ? b.when.getTime() : new Date(b.when).getTime(); return tb - ta; }); const messages = gathered.slice(0, limit);
// Non-search pages stay chronological (oldest → newest) as before. if (!searching) messages.reverse();
// Resolve handles via direct DB lookup (not HTTP) to avoid N+1 self-calls const uniqueUserIds = [...new Set(messages.map(m => m.user).filter(Boolean))]; const handleMap = new Map();
if (uniqueUserIds.length > 0) { const handles = database.db.collection("@handles"); const handleRecords = await handles .find({ _id: { $in: uniqueUserIds } }) .toArray(); for (const rec of handleRecords) { handleMap.set(rec._id, "@" + rec.handle); } }
// Join heart counts per source instance (heart `type` is chat-<instance>). const heartMap = {}; for (const inst of instances) { const idsForInst = messages .filter((m) => (m.__instance || instance) === inst) .map((m) => m._id.toString()); if (idsForInst.length === 0) continue; const heartCounts = await database.db .collection("hearts") .aggregate([ { $match: { type: `chat-${inst}`, for: { $in: idsForInst } } }, { $group: { _id: "$for", count: { $sum: 1 } } }, ]) .toArray(); for (const h of heartCounts) heartMap[h._id] = h.count; }
const messagesWithHandles = messages.map((msg) => ({ id: msg._id.toString(), from: (msg.user && handleMap.get(msg.user)) || "anon", text: msg.text, when: msg.when, instance: msg.__instance || instance, hearts: heartMap[msg._id.toString()] ?? 0, }));
await database.disconnect();
shell.log(`✅ Found ${messagesWithHandles.length} messages for instance: ${instance}`);
// `nextBefore` = the oldest `when` in this page (works whether the // page is chronological or newest-first relevance). const oldest = messagesWithHandles.length > 0 ? messagesWithHandles.reduce((acc, m) => { const t = m.when instanceof Date ? m.when : new Date(m.when); return t < acc ? t : acc; }, new Date(8640000000000000)) : null; const nextBefore = oldest ? oldest.toISOString() : null;
return { instance, search: searching ? searchRaw : undefined, count: messagesWithHandles.length, messages: messagesWithHandles, nextBefore, }; }, CACHE_TTLS.CHAT // 2 minutes );
return respond(200, result); } catch (error) { shell.error("Error fetching chat messages:", error); return respond(500, { message: error.message }); }}