Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397// mood, 23.07.09.17.54
// 1. GET `api/mood/@handle`// Get the most recent mood from the user's handle or email.// 1.1 GET `api/mood/moods-of-the-day`// Pick the latest @jeffrey mood or any handled mood in the last 24 hours.
// 2. POST `api/mood`// Allows a logged in user to set their mood, which// updates the aesthetic.computer motd as well.
/* #region ๐ TODO - [] Write a GET request that retrieves all of a user's moods, paginated? `const moods = await collection.find({ user }).toArray();` // Get all moods. - [] Write a GET request for getting a mood feed that consists of multiple users. (Just use the latest for now?) + Later - [] Trigger a notification via FCM on a user status update. ๐ก - Actually... should the statuses be recordable text? - And should these be stored as separate files in a user's bucket, with merely an association for the status? - Or what is a cool way to store the recording in a linear fashion? + Done - [x] Test GET request. - [x] Write GET request for retrieving the latest mood from individual users. - [x] Test POST request.#endregion */
import { authorize, userIDFromHandleOrEmail, getHandleOrEmail,} from "../../backend/authorization.mjs";import { connect, moodFor, allMoods, getMoodByRkey } from "../../backend/database.mjs";import { respond, pathParams } from "../../backend/http.mjs";import { createMoodOnAtproto } from "../../backend/mood-atproto.mjs";import { shouldMirror, postMoodToBluesky } from "../../backend/bluesky-mirror.mjs";import { fetchBlueskyEngagement } from "../../backend/bluesky-engagement.mjs";// const dev = process.env.CONTEXT === "dev";
import { broadcastToTopic } from "../../../shared/push.mjs"; // Push notifications.
import { shell } from "../../backend/shell.mjs";import { publishProfileEvent } from "../../backend/profile-stream.mjs";
import { filter } from "../../backend/filter.mjs"; // Profanity filtering.// import { promises as fs } from "fs";// import path from "path";
// let app;
const MOODS_OF_THE_DAY_WINDOW_MS = 24 * 60 * 60 * 1000;
async function fetchLatestMoodForSub(database, sub) { if (!sub) return null; const collection = database.db.collection("moods"); const pipeline = [ { $match: { user: sub, deleted: { $ne: true } } }, { $sort: { when: -1 } }, { $limit: 1 }, { $lookup: { from: "@handles", localField: "user", foreignField: "_id", as: "handleInfo", }, }, { $unwind: { path: "$handleInfo", preserveNullAndEmptyArrays: true } }, { $project: { _id: 0, mood: 1, when: 1, handle: { $cond: [ { $ifNull: ["$handleInfo.handle", false] }, { $concat: ["@", "$handleInfo.handle"] }, null, ], }, }, }, ];
const records = await collection.aggregate(pipeline).toArray(); return records?.[0] || null;}
async function fetchRecentHandledMoods(database, since) { const collection = database.db.collection("moods"); const pipeline = [ { $match: { deleted: { $ne: true }, when: { $gte: since }, }, }, { $lookup: { from: "@handles", localField: "user", foreignField: "_id", as: "handleInfo", }, }, { $unwind: "$handleInfo" }, { $project: { _id: 0, mood: 1, when: 1, handle: { $concat: ["@", "$handleInfo.handle"] }, }, }, { $sort: { when: -1 } }, { $limit: 200 }, ];
return await collection.aggregate(pipeline).toArray();}
export async function handler(event, context) { if (event.httpMethod === "GET") { // 1. GET: Look up moods for user. const database = await connect(); const slug = pathParams(event.path)[2]; const handle = event.queryStringParameters?.for || null;
if (slug === "moods-of-the-day" || slug === "motd") { const since = new Date(Date.now() - MOODS_OF_THE_DAY_WINDOW_MS); const listRequested = Boolean(event.queryStringParameters?.list); const jeffreyResult = await userIDFromHandleOrEmail("@jeffrey", database); const jeffreySub = typeof jeffreyResult === "string" ? jeffreyResult : jeffreyResult?.userID;
const candidates = []; const seen = new Set();
const addCandidate = (candidate) => { if (!candidate?.mood) return; const whenValue = candidate.when ? new Date(candidate.when).getTime() : ""; const key = `${candidate.handle || ""}-${candidate.mood}-${whenValue}`; if (seen.has(key)) return; seen.add(key); candidates.push(candidate); };
const jeffreyMood = await fetchLatestMoodForSub(database, jeffreySub); addCandidate(jeffreyMood);
const recentMoods = await fetchRecentHandledMoods(database, since); for (const mood of recentMoods) addCandidate(mood);
await database.disconnect();
if (candidates.length === 0) { return respond(404, { message: "No mood found." }); }
if (listRequested) { return respond(200, { moods: candidates }); }
const pick = candidates[Math.floor(Math.random() * candidates.length)]; return respond(200, pick); } else if (slug === "all") { // List all moods from the database and return // them as { moods }. const moods = await allMoods(database, handle); await database.disconnect(); return moods && moods.length > 0 ? respond(200, { moods }) : respond(500, { message: "No mood found." }); } else if (slug === "search") { // GET /api/mood/search?q=<text>&limit=N // Case-insensitive substring search over recent moods, newest first, // with @handles resolved. Powers the universal AC search box. const qRaw = ( event.queryStringParameters?.q || event.queryStringParameters?.search || "" ).trim(); const limit = Math.min( parseInt(event.queryStringParameters?.limit || "20", 10) || 20, 100, ); if (!qRaw) { await database.disconnect(); return respond(400, { message: "Missing q/search parameter" }); } const escaped = qRaw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const collection = database.db.collection("moods"); const results = await collection .aggregate([ { $match: { deleted: { $ne: true }, mood: { $regex: escaped, $options: "i" }, }, }, { $lookup: { from: "@handles", localField: "user", foreignField: "_id", as: "handleInfo", }, }, { $unwind: "$handleInfo" }, { $project: { _id: 0, mood: 1, when: 1, handle: { $concat: ["@", "$handleInfo.handle"] }, }, }, { $sort: { when: -1 } }, { $limit: limit }, ]) .toArray(); await database.disconnect(); return respond(200, { search: qRaw, count: results.length, moods: results, }); } else if (slug === "single") { // GET /api/mood/single?handle=@jeffrey&rkey=<atproto_rkey> // Returns a single mood by handle and ATProto rkey (for permalinks) const handleParam = event.queryStringParameters?.handle; const rkeyParam = event.queryStringParameters?.rkey;
if (!handleParam || !rkeyParam) { await database.disconnect(); return respond(400, { message: "Missing handle or rkey parameter" }); }
const mood = await getMoodByRkey(database, handleParam, rkeyParam); await database.disconnect();
if (!mood) { return respond(404, { message: "Mood not found" }); }
// Fetch Bluesky engagement if mood was mirrored let engagement = null; if (mood.bluesky?.uri) { try { engagement = await fetchBlueskyEngagement(mood.bluesky.uri); } catch (e) { shell.log("โ ๏ธ Failed to fetch Bluesky engagement:", e.message); } }
return respond(200, { ...mood, engagement }); } else { shell.log("Getting user id from:", slug); const user = await userIDFromHandleOrEmail(slug, database); if (user && user.length > 0) { const mood = await moodFor(user, database); await database.disconnect(); return mood ? respond(200, mood) : respond(500, { message: "No mood found." }); } else { return respond(401, { message: "User not found." }); } } } else if (event.httpMethod !== "POST") { return respond(405, { message: "Method Not Allowed" }); }
// 2. POST: Set / post a mood. try { const body = JSON.parse(event.body); const user = await authorize(event.headers); const handle = await getHandleOrEmail(user.sub);
// console.log("๐ Posting mood for:", handle, body);
if (user && handle?.startsWith("@")) { const database = await connect(); const collection = database.db.collection("moods"); // Make tweet-like collection. // Assume a mood update. if (body.mood) { const mood = filter(body.mood.trim()); // Trim extra whitespace off mood.
// ๐ Database await collection.createIndex({ user: 1 }); // Index for `user`. await collection.createIndex({ when: 1 }); // Index for `when`.
const lastMood = await collection.findOne( { user: user.sub }, { sort: { when: -1 } }, );
if (!lastMood || lastMood.mood !== mood) { const insertResult = await collection.insertOne({ user: user.sub, mood, when: new Date(), });
publishProfileEvent({ handle, event: { type: "mood", when: Date.now(), label: `Mood: ${mood}`, }, countsDelta: { moods: 1 }, }).catch((err) => { shell.log("โ ๏ธ Mood profile-event publish failed:", err?.message || err); });
// Dual-write: Also create mood on ATProto console.log("๐ Syncing mood to ATProto..."); let atprotoRkey = null; try { const atprotoResult = await createMoodOnAtproto( database, user.sub, mood, new Date(), insertResult.insertedId.toString(), );
if (atprotoResult.rkey) { atprotoRkey = atprotoResult.rkey; // Update MongoDB with the ATProto rkey await collection.updateOne( { _id: insertResult.insertedId }, { $set: { atproto: { rkey: atprotoResult.rkey } } }, ); console.log( "โ
Mood synced to ATProto with rkey:", atprotoResult.rkey, ); } } catch (atprotoError) { // Log the error but don't fail the request - mood is already saved in MongoDB console.error("โ ๏ธ Failed to sync mood to ATProto:", atprotoError); }
// Mirror to Bluesky for configured handles (e.g., @jeffrey) if (atprotoRkey && await shouldMirror(database, handle)) { console.log(`๐ฆ Mirroring mood to Bluesky for ${handle}...`); try { const blueskyResult = await postMoodToBluesky(database, mood, handle, atprotoRkey); if (blueskyResult) { // Store Bluesky post reference for engagement tracking await collection.updateOne( { _id: insertResult.insertedId }, { $set: { bluesky: { uri: blueskyResult.uri, cid: blueskyResult.cid, rkey: blueskyResult.rkey } } }, ); console.log("โ
Mood mirrored to Bluesky:", blueskyResult.rkey); } } catch (blueskyError) { // Don't fail the request - mood is already saved console.error("โ ๏ธ Failed to mirror to Bluesky:", blueskyError); } }
console.log("๐ Setting a mood for:", handle, body.mood);
await broadcastToTopic(database.db, "mood", { title: `${handle}'s mood is`, body: `${mood}`, data: { piece: "moods" }, }); }
await database.disconnect(); return respond(200, { mood }); // Successful mood change. } else if (body.nuke !== undefined) { // Flag all a user's moods as deleted. const result = await collection.updateMany( { user: user.sub }, { $set: { deleted: body.nuke } }, ); await database.disconnect(); return respond(200, { altered: result.modifiedCount }); // Reply with number of moods modified. } } else { return respond(401, { message: "Authorization failure..." }); } } catch (error) { return respond(500, { message: error }); }}