1
0
Fork 0
mirror of https://git.sr.ht/~roxwize/mipilin synced 2025-01-30 18:53:36 +00:00
mipilin/routes/updates.ts
roxwize fa8fae4638
mipilin r9... Fimally
Signed-off-by: roxwize <rae@roxwize.xyz>
2025-01-28 17:05:54 -05:00

357 lines
11 KiB
TypeScript

import { NodePgDatabase } from "drizzle-orm/node-postgres";
import { Express } from "express";
import {
follows,
inviteCodes,
journalEntries,
profiles,
updates,
users
} from "../db/schema.js";
import { and, count, desc, eq, sql } from "drizzle-orm";
import dayjs from "dayjs";
import {
confirm,
getMoods,
journalMoodString,
render,
render404,
UserStatus
} from "./util.js";
export default async function (app: Express, db: NodePgDatabase) {
const { moods, moodsSorted } = await getMoods();
// DASHBOARD
app.get("/dashboard", async (req, res) => {
if (!req.session["loggedIn"]) {
res.redirect("/login");
return;
}
const user = (
await db
.select({
name: users.name,
bio: profiles.bio,
website: profiles.website //! validate this
})
.from(users)
.where(eq(users.name, req.session["user"]))
.leftJoin(profiles, eq(users.id, profiles.user))
)[0];
const now = dayjs();
const moodHistory = (
await db
.select({ mood: updates.mood, date: updates.date })
.from(updates)
.where(eq(updates.user, req.session["uid"]))
.orderBy(desc(updates.date))
.limit(10)
).map((e) => {
return { mood: moods[e.mood], date: now.to(dayjs(e.date)) };
});
const recentUpdates = (
await db
.select({
user: users.name,
mood: updates.mood,
desc: updates.description,
date: updates.date
})
.from(updates)
.innerJoin(
follows,
and(
eq(follows.userId, updates.user),
eq(follows.followerId, req.session["uid"])
)
)
.leftJoin(users, eq(updates.user, users.id))
.orderBy(desc(updates.date))
.limit(25)
).map((e) => {
return {
user: e.user,
mood: moods[e.mood],
desc: e.desc,
date: e.date,
relativeDate: now.to(dayjs(e.date))
};
});
// user invite codes
const codes = (
await db
.select({
token: inviteCodes.token,
expires: inviteCodes.expires
})
.from(inviteCodes)
.where(eq(inviteCodes.user, req.session["uid"]))
).map((e) => {
return {
token: e.token,
expires: e.expires,
expiresString: now.to(dayjs(e.expires || 0))
};
});
const { codesUsed } = (
await db
.select({ codesUsed: count() })
.from(inviteCodes)
.where(
and(
eq(inviteCodes.user, req.session["uid"]),
eq(
sql`extract(month from granted)`,
sql`extract(month from current_date)`
)
)
)
)[0];
const followed = await db
.select({ uname: users.name })
.from(follows)
.where(eq(follows.followerId, req.session["uid"]))
.innerJoin(users, eq(follows.userId, users.id));
render(db, "dashboard", "dashboard", res, req, {
user,
moods,
moodsSorted,
moodHistory,
recentUpdates,
codes,
codesUsed,
followed,
isTrusted: req.session["status"] & (UserStatus.MODERATOR | UserStatus.TRUSTED),
feed: []
});
});
app.post("/update/mood", async (req, res) => {
if (!req.session["loggedIn"]) {
res.redirect("/login");
return;
}
// make sure the user isnt updating too fast
//! TODO: also do this for journal entries
const lastUpdate = (await db.select({ date: updates.date }).from(updates).where(eq(updates.user, req.session["uid"])).orderBy(desc(updates.date)).limit(1))?.[0];
if (Date.now() < lastUpdate?.date?.getTime() + 10 * 1000) {
req.flash("error", "You're updating your mood too fast! Wait ten seconds between updates.");
res.redirect(req.get("Referrer") || "/");
return;
}
const moodIndex = moods.indexOf(req.body.mood.trim());
if (moodIndex === -1) {
req.flash(
"error",
"That mood doesn't exist in the database, WTF are you trying to do??"
);
res.redirect("/dashboard");
return;
}
if (req.body.desc.length > 512) {
req.flash(
"error",
"Mood description can't be longer than 512 characters"
);
res.redirect("/dashboard");
}
await db
.insert(updates)
// @ts-expect-error
.values({
user: req.session["uid"],
mood: moodIndex,
description: req.body.desc,
date: new Date(Date.now())
});
req.flash("success", "Mood updated!");
res.redirect("/dashboard");
});
// JOURNAL
app.get("/journal", async (req, res) => {
render(db, "journal", "your journal", res, req);
});
app.get("/journal/:id", async (req, res) => {
const id = parseInt(req.params.id);
if (isNaN(id)) {
req.flash("error", "Invalid ID");
render404(db, res, req);
return;
}
const entry: {
id: number;
uid: number;
uname: string;
title: string;
content: string;
moodChange: number;
moodString?: string;
date: Date;
visibility: number;
} = (
await db
.select({
id: journalEntries.id,
uid: users.id,
uname: users.name,
title: journalEntries.title,
content: journalEntries.entry,
moodChange: journalEntries.moodChange,
date: journalEntries.date,
visibility: journalEntries.visibility
})
.from(journalEntries)
.where(eq(journalEntries.id, id))
.leftJoin(users, eq(journalEntries.user, users.id))
)[0];
//? put into util function?
//? also GOD
entry.moodString = journalMoodString(entry.moodChange);
const isMod = req.session["status"] & UserStatus.MODERATOR;
if (
!entry ||
(entry.visibility === 0 &&
entry.uname !== req.session["user"] &&
!isMod)
) {
render404(db, res, req);
return;
}
// maybe turn ([x] !== req.session["user"] && !(req.session["status"] & UserStatus.MODERATOR)) into util function
entry.content =
entry.visibility === 2 &&
entry.uid !== req.session["uid"] &&
!(req.session["status"] & UserStatus.MODERATOR)
? "This journal entry's contents have been made private."
: entry.content;
const entryTimestamp = dayjs(entry.date).fromNow();
const isSelf = entry.uid === req.session["uid"];
render(db, "journal_view", entry.title, res, req, {
entry,
entryTimestamp,
isSelf,
isMod
});
});
app.post("/journal/:id/edit", async (req, res) => {
const id = parseInt(req.params.id);
if (isNaN(id)) {
req.flash("error", "Invalid ID");
render404(db, res, req);
return;
}
const entry = (
await db
.select({
uid: journalEntries.user
})
.from(journalEntries)
.where(eq(journalEntries.id, id))
.limit(1)
)[0];
const isMod = req.session["status"] & UserStatus.MODERATOR;
if (!entry || (entry?.uid !== req.session["uid"] && !isMod)) {
render404(db, res, req);
return;
}
if (
isMod &&
entry.uid !== req.session["uid"] &&
req.body.action !== "delete"
) {
req.flash(
"error",
"Moderators can only delete other users' posts."
);
res.redirect(`/journal/${req.params.id}`);
return;
}
if (req.body.action === "edit") {
// TODO!!
} else if (req.body.action === "delete") {
if (!req.body.confirm) {
confirm(db, res, req);
return;
}
await db.delete(journalEntries).where(eq(journalEntries.id, id));
req.flash("success", "Journal entry deleted ;w;");
res.redirect("/");
} else {
render404(db, res, req);
}
});
app.post("/update/journal", async (req, res) => {
if (!req.session["loggedIn"]) {
res.redirect("/login");
return;
}
if (req.body.title.length > 64) {
req.flash("error", "Title too long!");
res.redirect("/journal");
return;
}
if (req.body.description.length > 4096) {
req.flash("error", "Entry too long!");
res.redirect("/journal");
return;
}
const moodChange = parseInt(req.body.moodDelta);
const visibility = parseInt(req.body.visibility);
if (isNaN(moodChange) || isNaN(visibility)) {
req.flash("error", "One of the values was improperly specified.");
res.redirect("/journal");
return;
}
let id: number;
try {
const entry = await db
.insert(journalEntries)
// @ts-expect-error
.values({
user: req.session["uid"],
moodChange,
visibility,
title: req.body.title,
entry: req.body.description
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll("\n", "<br>"),
date: new Date(Date.now())
})
.returning({ id: journalEntries.id });
id = entry[0].id;
} catch (err) {
req.flash(
"error",
"Failed to create your entry. Try again later or send these logs to roxwize so she can know what's up:<br><br>" +
err
);
res.redirect("/journal");
return;
}
req.flash(
"success",
`Your journal entry is now available as <a href="/journal/${id}">#${id}</a>!`
);
res.redirect("/journal");
});
}