Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460// @handle, 23.04.30.18.12 🤚// GET: Get a user @handle out of MongoDB based on their `sub` id from auth0.// POST: Allows a logged in user to set their social `@handle`. (via MongoDB)// Or to "strip" a user of their handle.
/* #region 🏁 TODO #endregion */
import { authorize, userIDFromHandle, userIDFromHandleOrEmail, findSisterSub, handleFor, hasAdmin,} from "../../backend/authorization.mjs";
import { validateHandle } from "../../public/aesthetic.computer/lib/text.mjs";import { filter } from "../../backend/filter.mjs";import { connect } from "../../backend/database.mjs";import * as KeyValue from "../../backend/kv.mjs";import { respond } from "../../backend/http.mjs";import * as logger from "../../backend/logger.mjs";import { shell } from "../../backend/shell.mjs";import { updateAtprotoHandle } from "../../backend/at.mjs";
const dev = process.env.CONTEXT === "dev";
export async function handler(event, context) { // A GET request to get a handle from a user `sub`. if (event.httpMethod === "GET") { const accepts = (event.headers?.accept || event.headers?.Accept || "").toLowerCase(); const secFetchMode = (event.headers?.["sec-fetch-mode"] || "").toLowerCase(); const secFetchDest = (event.headers?.["sec-fetch-dest"] || "").toLowerCase(); const isBrowserNavigation = accepts.includes("text/html") || secFetchMode === "navigate" || secFetchDest === "document";
const count = event.queryStringParameters.count;
// Visiting /handle in a browser should land on a human-friendly page. // Keep API behavior for fetch/XHR callers. if ( isBrowserNavigation && !count && !event.queryStringParameters?.for ) { // Check if user is logged in - if so, redirect to prompt; otherwise to get-handle const user = await authorize(event.headers, "aesthetic").catch(() => null); const redirectTo = user ? "/prompt" : "/get-handle";
return { statusCode: 302, headers: { Location: redirectTo, "Cache-Control": "no-cache", }, body: "", }; }
if (count) { // Return total handle count.
// TODO: 👻 Count only "sotce-" prefixed primary keys if tenant is "sotce" // and discount them if tenant is "aesthetic". const tenant = event.queryStringParameters.tenant || "all";
try { const database = await connect(); // 📕 Database const collection = database.db.collection("@handles");
let handles; if (tenant === "sotce") { handles = await collection.countDocuments({ _id: { $regex: "^sotce-" }, }); } else if (tenant === "aesthetic") { handles = await collection.countDocuments({ _id: { $not: { $regex: "^sotce-" } }, }); } else { handles = await collection.estimatedDocumentCount(); // Default to all documents }
await database.disconnect();
return respond(200, { handles }); } catch (error) { return respond(500, { message: "Failed to retrieve handle count." }); } } else { // Get handle `for` const id = event.queryStringParameters.for; const includeColors = event.queryStringParameters.colors === "true";
if (!id) { return respond(400, { message: "Missing query parameter: for (or use ?count=true)", }); } const result = await handleFor(id);
if (typeof result === "string") { // If colors requested, fetch from MongoDB if (includeColors) { try { const database = await connect(); const handles = database.db.collection("@handles"); const user = await handles.findOne({ handle: { $regex: new RegExp(`^${result}$`, "i") }, }); await database.disconnect();
if (user && user.colors) { return respond(200, { handle: result, colors: user.colors }); } } catch (error) { console.error("Failed to fetch colors:", error); } } return respond(200, { handle: result }); } else if (Array.isArray(result) && result.length > 0) { return respond(200, { handles: result }); } else { return respond(404, { message: "No handle(s) found." }); } } } else if (event.httpMethod !== "POST") return respond(405, { message: "Method Not Allowed" });
// A POST request to set the handle.
// Parse the body of the HTTP request let body; try { // Make sure we have a username present to set. body = JSON.parse(event.body);
let handle = body.handle; if (handle[0] === "@") handle = handle.slice(1); // Remove any user prefixed "@".
const tenant = body.tenant || "aesthetic"; // Could be 'sotce'. const action = body.action;
if ( action !== "strip" && action !== "chat-system:mute" && action !== "chat-system:unmute" ) { // Make sure handle entry is well formed. const validated = validateHandle(handle);
if (validated !== "valid") { return respond(400, { message: validated }); }
// Filter handle for profanities. if (filter(handle) !== handle) { return respond(400, { message: "naughty" }); }
// ⚠️ "too long", "invalid", and "naughty" are invalid handle responses }
// And that we are logged in... const user = await authorize(event.headers, tenant);
// shell.log("🙆 Handle update for user:", user);
if (user && user.email_verified) { // 🔑 We are logged in!
shell.log("🤖 Connecting to MongoDB..."); const database = await connect(); // 📕 Database shell.log("🤖 Connected...");
const handles = database.db.collection("@handles");
// Make a "handle" index on the @handles collection that forces // them all to be unique, (if it doesn't already exist). await handles.createIndex({ handle: 1 }, { unique: true });
await KeyValue.connect(); // 📓 Note: Not actually needed for mute actions.
/* if (tenant === "aesthetic") */ await logger.link(database);
// Admin action to delete a user handle from the system, as opposed // to setting it. Handles are stripped regardless of what // network they were created on. if (action === "strip") { if ((await hasAdmin(user)) === false) { return respond(500, { message: "unauthorized" }); } shell.log("🩹 Stripping handle from:", handle);
let status, response; try { const sub = await userIDFromHandle(handle, database, true); const handledUser = await handles.findOne({ _id: sub });
if (handledUser) { await handles.deleteOne({ _id: sub }); await KeyValue.del("@handles", handle); // Delete original handle cache.
// Delete sister network redis cache reference. const sisterSub = await findSisterSub(sub); if (sisterSub) await KeyValue.del("userIDs", sisterSub); await KeyValue.del("userIDs", sub); // Delete original cache.
if (!sub.startsWith("sotce-")) { await logger.log(`@${handle}'s handle was stripped!`, { user: sub, action: "handle:strip", value: "???", }); } // ⚠️ // TODO: Stripping a handle from a 'sotce' user // will not refresh their client automatically. 24.08.31.01.18 status = 200; response = { message: "stripped" }; } else { status = 404; response = { message: "not found" }; } } catch (error) { status = 500; response = { message: "error" }; } finally { await database.disconnect(); await KeyValue.disconnect(); } return respond(status, response); }
if (action === "chat-system:mute" || action === "chat-system:unmute") { if ((await hasAdmin(user)) === false) { return respond(500, { message: "unauthorized" }); }
let errorMessage = "error"; let status, response;
try { const muting = action === "chat-system:mute";
if (muting) { shell.log("🩹 Muting user:", handle); } else { shell.log("🩹 Unmuting user:", handle); }
// ⭐ // 1. Pull the user sub from 'handle' which could be // either a handle, sub, or email address.
// TODO: 🧑🚒 Does this need to be sent an alternate tenancy? // 🟪 It will only work on AC for now. 25.02.07.19.05 const sub = await userIDFromHandleOrEmail(handle, database); console.log("🤐 User sub to mute:", sub);
if (!sub) { errorMessage = "user not found"; throw new Error("User not found."); }
// 2. Add or remove the user sub from mutes collection. const mutesCollection = database.db.collection("chat-system-mutes"); await mutesCollection.createIndex({ user: 1 }, { unique: true }); // ^ Force a unique index on mutes.
let inert = true;
if (muting) { // Add the sub to the mutesCollection if necessary. try { await mutesCollection.insertOne({ user: sub }); inert = false; } catch (error) { if (error.code !== 11000) { // Ignore duplicate key error, since user is already muted. errorMessage = "already muted"; throw error; } } } else { // Remove the sub from the mutesCollection if necessary.
// Check if the user is actually muted before deleting const muteRecord = await mutesCollection.findOne({ user: sub });
if (muteRecord) { await mutesCollection.deleteOne({ user: sub }); console.log(`User ${sub} unmuted.`); inert = false; } else { console.log(`User ${sub} was not muted.`); } }
// 3. Log it and create a log event to update the message buffer // for users. if (!inert) { const trueHandle = await handleFor(sub); await logger.log( `${trueHandle ? ("@" + trueHandle) : "someone"} was ${action.split(":")[1]}d!`, { user: sub, action, // value: sub, }, ); }
// 4. Return the proper status and response. status = 200; response = { message: muting ? "muted" : "unmuted" }; } catch (error) { status = 500; console.error("🔥 " + error); response = { message: errorMessage }; } finally { await database.disconnect(); await KeyValue.disconnect(); }
return respond(status, response); }
// 🌟 Otherwise assume we are creating or modifying a handle. shell.log(`💁 Setting a handle on ${tenant}:`, handle);
// Insert or update the handle using the `provider|id` key from auth0. let atprotoSync; // Declare outside try block to avoid ReferenceError try { let sub = user.sub; if (tenant === "sotce") sub = "sotce-" + user.sub; // Prefix the stored handle subs with 'sotce-' // because they are not guaranteed to be unique by auth0: https://community.auth0.com/t/ensuring-unique-user-ids-in-auth0-across-multiple-tenants/120970 let primarySub = sub; let otherSub;
// Check if a document with this user's sub already exists on the // current tenant. let existingUser = await handles.findOne({ _id: sub });
// Or try to find an existingUser on the sister tenant, via the user's // email address. if (!existingUser) { let sisterSub = await findSisterSub(sub, { prefixed: true }); existingUser = await handles.findOne({ _id: sisterSub }); if (existingUser) primarySub = sisterSub; }
if (existingUser) { shell.log("Current user handle:", existingUser.handle); // Replace existing handle or fail if the new handle is already taken // by someone else (case-insensitive). const existingHandle = await handles.findOne({ handle: { $regex: new RegExp(`^${handle}$`, 'i') } });
if ( existingHandle && (existingHandle._id !== sub || existingHandle._id !== otherSub) ) { throw new Error("taken"); }
if (existingHandle && existingHandle.handle.toLowerCase() === handle.toLowerCase()) { return respond(400, { message: "same" }); }
await handles.updateOne({ _id: primarySub }, { $set: { handle } });
if (!primarySub.startsWith("sotce-")) { await logger.log(`@${existingUser.handle} is now @${handle}`, { user: primarySub, action: "handle:update", value: handle, }); // 🪵 } } else { // Check for handle existence with case-insensitive search const existingHandle = await handles.findOne({ handle: { $regex: new RegExp(`^${handle}$`, 'i') } }); if (existingHandle) throw new Error("taken");
// Add a new `@handles` document for this user. await handles.insertOne({ _id: primarySub, handle });
if (!primarySub.startsWith("sotce-")) { await logger.log(`hi @${handle}`, { user: primarySub, action: "handle:create", value: handle, }); // 🪵 Log initial handle creation. } } atprotoSync = await updateAtprotoHandle(database, primarySub, handle); // Update the redis handle <-> userID cache... if (existingUser?.handle) await KeyValue.del("@handles", existingUser.handle);
await KeyValue.set("@handles", handle, primarySub); await KeyValue.set("userIDs", sub, handle);
if (otherSub) { await KeyValue.set("userIDs", otherSub, handle); }
// Bust /user endpoint cache so handle is immediately visible if (user.email) { for (const wh of ["true", "false"]) { for (const wt of ["true", "false"]) { await KeyValue.del("userCache", `email:${user.email}:${tenant}:${wh}:${wt}`); } } }
// 🔥 Publish the new handle association to redis for chat servers. await KeyValue.pub( "handle:update", JSON.stringify({ user: primarySub, handle }), );
// - [-] `world` needs to pick this up somehow. // - [] handle changes from others // - [] self-handle change from another window // - [] `pond` needs to also do this // - [] handle changes from others // - [] self-handle change from another window // - [] `prompt` also needs to do this } catch (error) { shell.log("👱 Handle set error:", error.message); return respond(500, { message: error.message === "taken" ? "taken" : "error", }); } finally { await database.disconnect(); await KeyValue.disconnect(); } // Successful handle change... return respond(200, { handle: body.handle, atproto: atprotoSync }); } else { if (user) { return respond(401, { message: "unverified" }); } else { return respond(401, { message: "unauthorized" }); } } } catch (error) { shell.log("👱 Handle error:", error); return respond(400, { message: "error" }); }}