OK, I think that's enough

This commit is contained in:
MeowcaTheoRange 2023-06-19 00:16:00 -05:00
commit 7e92fdd379
36 changed files with 5439 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
.env
node_modules/
dist/

17
.prettierrc Normal file
View file

@ -0,0 +1,17 @@
{
"trailingComma": "none",
"tabWidth": 4,
"useTabs": false,
"printWidth": 120,
"semi": true,
"singleQuote": false,
"quoteProps": "preserve",
"jsxSingleQuote": false,
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "avoid",
"proseWrap": "never",
"endOfLine": "lf",
"embeddedLanguageFormatting": "auto",
"singleAttributePerLine": true
}

5
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,5 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"editor.tabSize": 4,
"prettier.tabWidth": 4
}

2033
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

35
package.json Normal file
View file

@ -0,0 +1,35 @@
{
"devDependencies": {
"@types/express": "^4.17.17",
"@types/lodash": "^4.14.195",
"@types/node": "^20.3.1",
"concurrently": "^8.2.0",
"nodemon": "^2.0.22",
"tsc-alias": "^1.8.6",
"typescript": "^5.1.3"
},
"name": "trollcallquick",
"version": "0.1.0",
"main": "dist/index.js",
"scripts": {
"build": "npx tsc",
"start": "node dist/index.js",
"dev": "trap \"rm -rf dist/*;fuser -k 3000/tcp\" SIGINT;concurrently \"npx tsc --watch\" \"tsc-alias -w\" \"nodemon -q dist/index.js\"",
"devonce": "npx tsc && tsc-alias -p tsconfig.json;trap \"rm -rf dist/*;fuser -k 3000/tcp\" SIGINT;node dist/index.js"
},
"author": "MeowcaTheoRange",
"license": "ISC",
"description": "",
"dependencies": {
"@types/cookie-parser": "^1.4.3",
"body-parser": "^1.20.2",
"cookie-parser": "^1.4.6",
"dotenv": "^16.3.0",
"express": "^4.18.2",
"lodash": "^4.17.21",
"mongodb": "^5.6.0",
"nanoid": "^3.3.6",
"tsconfig-paths": "^4.2.0",
"yup": "^1.2.0"
}
}

8
src/app/api/index.ts Normal file
View file

@ -0,0 +1,8 @@
import { Router } from "express";
import { trollRouter } from "./troll";
import { userRouter } from "./user";
export const apiRouter = Router();
apiRouter.use("/", trollRouter);
apiRouter.use("/", userRouter);

View file

@ -0,0 +1,21 @@
import { ServerTrollToClientTroll } from "@/lib/trollcall/convert/troll";
import { getSingleTroll } from "@/lib/trollcall/troll";
import { getSingleUser } from "@/lib/trollcall/user";
import { Router } from "express";
export const trollRouter = Router();
trollRouter.get("/user/:user/troll/:troll", async (req, res, next) => {
const { params } = req;
const user = await getSingleUser({
name: params.user
});
if (user == null) return res.sendStatus(404);
const troll = await getSingleTroll({
"name.0": params.troll,
"owners.0": user._id
});
if (troll == null) return res.sendStatus(404);
const serverTroll = await ServerTrollToClientTroll(troll);
res.json(serverTroll);
});

63
src/app/api/user/index.ts Normal file
View file

@ -0,0 +1,63 @@
import { MergeServerUsers, ServerUserToClientUser, SubmitUserToServerUser } from "@/lib/trollcall/convert/user";
import { changeUser, createUser, getSingleUser } from "@/lib/trollcall/user";
import { PartialUserSchema, SubmitUser, SubmitUserSchema } from "@/types/client/user";
import { ServerUser } from "@/types/user";
import { Router } from "express";
export const userRouter = Router();
userRouter.get("/user/:user/", async (req, res, next) => {
const { params } = req;
const user = await getSingleUser({
name: params.user
});
if (user == null) return res.sendStatus(404);
const serverUser = await ServerUserToClientUser(user);
res.json(serverUser);
});
userRouter.post("/user/", async (req, res, next) => {
const { body } = req;
let validatedUser: SubmitUser;
try {
validatedUser = await SubmitUserSchema.validate(body);
} catch (err) {
return res.status(400).send(err);
}
const checkExistingUser = await getSingleUser({
name: validatedUser.name
});
if (checkExistingUser != null) return res.sendStatus(409);
// we are sure this object is full, so cast partial
const serverUser = SubmitUserToServerUser(validatedUser) as Omit<ServerUser, "_id">;
const newUser = await createUser(serverUser);
if (newUser == null) return res.sendStatus(503);
// Give cookies
res.cookie("TROLLCALL_NAME", newUser.name, { maxAge: 31540000 })
.cookie("TROLLCALL_CODE", newUser.code, { maxAge: 31540000 })
.json(newUser);
});
userRouter.put("/user/:user/", async (req, res, next) => {
const { body, params, cookies } = req;
let validatedUser: Partial<SubmitUser>;
try {
validatedUser = (await PartialUserSchema.validate(body)) as Partial<SubmitUser>;
} catch (err) {
return res.status(400).send(err);
}
const checkExistingUser = await getSingleUser({
name: params.user
});
if (checkExistingUser == null) return res.sendStatus(404);
if (checkExistingUser.code !== cookies.TROLLCALL_CODE || checkExistingUser.name !== cookies.TROLLCALL_NAME)
return res.sendStatus(403);
const serverUser = SubmitUserToServerUser(validatedUser);
const bothUsers = MergeServerUsers(checkExistingUser, serverUser);
const newUser = await changeUser(bothUsers);
if (newUser == null) return res.sendStatus(503);
// Give cookies, redundant style
res.cookie("TROLLCALL_NAME", newUser.name, { maxAge: 31540000 })
.cookie("TROLLCALL_CODE", newUser.code, { maxAge: 31540000 })
.json(newUser);
});

6
src/app/index.ts Normal file
View file

@ -0,0 +1,6 @@
import { Router } from "express";
import { apiRouter } from "./api/index";
export const appRouter = Router();
appRouter.use("/api/", apiRouter);

18
src/index.ts Normal file
View file

@ -0,0 +1,18 @@
import dotenv from "dotenv";
dotenv.config();
import express, { Express } from "express";
import { appRouter } from "./app";
import bodyParser from "body-parser";
import cookieParser from "cookie-parser";
const app: Express = express();
app.use(bodyParser.json());
app.use(cookieParser());
app.use("/", appRouter);
app.listen(process.env.PORT, () => {
console.log(`⚡️[server]: Server is running at http://localhost:${process.env.PORT}`);
});

54
src/lib/db/crud.ts Normal file
View file

@ -0,0 +1,54 @@
import { FindCursor, Filter as MDBFilter, ObjectId } from "mongodb";
import { mainDB } from "./mongodb";
// Functionally identical to the MongoDB type functions, but this is here if you want to extend TrollCall to another NoSQL database.
export type WithId<TSchema> = Omit<TSchema, "_id"> & {
_id: ObjectId;
};
export type Document = { [key: string]: any };
export type Filter<TypeSchema> = MDBFilter<Document>;
export async function createOne(collection: string, doc: any) {
const selectedCollection = mainDB.collection(collection);
return await selectedCollection.insertOne(doc);
}
export async function readOne(collection: string, find: Filter<Document>) {
const selectedCollection = mainDB.collection(collection);
return await selectedCollection.findOne(find);
} // MongoDB and its finicky type safety
export function readMany(collection: string, find: Filter<Document>) {
const selectedCollection = mainDB.collection(collection);
return selectedCollection.find(find);
}
export async function countMany(collection: string, find: any) {
const selectedCollection = mainDB.collection(collection);
return await selectedCollection.countDocuments(find);
}
export async function replaceOne(collection: string, find: any, update: any) {
const selectedCollection = mainDB.collection(collection);
return await selectedCollection.replaceOne(find, update);
}
export async function updateOne(collection: string, find: any, update: any) {
const selectedCollection = mainDB.collection(collection);
return await selectedCollection.updateOne(find, update);
}
export async function deleteOne(collection: string, find: any) {
const selectedCollection = mainDB.collection(collection);
return await selectedCollection.findOneAndDelete(find);
}
export async function cursorToArray<T>(cursor: FindCursor<T>, func: (input: any) => any = x => x) {
let array: T[] = [];
while (await cursor.hasNext()) {
array.push(await func(await cursor.next()));
}
return array;
}

7
src/lib/db/mongodb.ts Normal file
View file

@ -0,0 +1,7 @@
import { MongoClient } from "mongodb";
if (process.env.MONGODB_DATABASE == null) process.exit();
export const client = new MongoClient(process.env.MONGODB_DATABASE, {});
export const mainDB = client.db("trollcall_test");

View file

@ -0,0 +1,11 @@
import { ClientFlair, ServerFlair } from "@/types/flair";
import { sanitize } from "../utility/merge";
export async function ServerFlairToClientFlair(serverFlair: ServerFlair): Promise<ClientFlair> {
const sanitizedFlair = sanitize(serverFlair);
let clientFlair: ClientFlair = {
...sanitizedFlair
};
return clientFlair;
}

View file

@ -0,0 +1,23 @@
import { Class, TrueSign } from "@/types/assist/extended_zodiac";
import { ClientTroll, ServerTroll } from "@/types/troll";
import { getManyFlairs } from "../flair";
import { getManyUsers } from "../user";
import { cutArray, sanitize } from "../utility/merge";
import { ServerFlairToClientFlair } from "./flair";
import { ServerUserToClientUser } from "./user";
export async function ServerTrollToClientTroll(serverTroll: ServerTroll): Promise<ClientTroll> {
const sanitizedTroll = sanitize(serverTroll);
const owners = await getManyUsers({ _id: { $in: serverTroll.owners } }, ServerUserToClientUser);
const flairs = await getManyFlairs({ _id: { $in: serverTroll.flairs } }, ServerFlairToClientFlair);
let clientTroll: ClientTroll = {
...sanitizedTroll,
trueSign: TrueSign[serverTroll.trueSign],
falseSign: serverTroll.falseSign != null ? TrueSign[serverTroll.falseSign] : null,
class: Class[serverTroll.class],
owners: cutArray(owners),
flairs: cutArray(flairs)
};
return clientTroll;
}

View file

@ -0,0 +1,38 @@
import { TrueSign } from "@/types/assist/extended_zodiac";
import { SubmitUser } from "@/types/client/user";
import { ClientUser, ServerUser } from "@/types/user";
import { nanoid } from "nanoid";
import { getManyFlairs } from "../flair";
import { cutArray, cutObject, removeCode, sanitize } from "../utility/merge";
import { ServerFlairToClientFlair } from "./flair";
export async function ServerUserToClientUser(serverUser: ServerUser): Promise<ClientUser> {
const sanitizedUser = removeCode(sanitize(serverUser));
const flairs = await getManyFlairs({ _id: { $in: serverUser.flairs } }, ServerFlairToClientFlair);
let clientUser: ClientUser = {
...sanitizedUser,
trueSign: TrueSign[serverUser.trueSign],
flairs: cutArray(flairs),
updatedDate: serverUser.updatedDate?.getTime()
};
return clientUser;
}
export function SubmitUserToServerUser(submitUser: Partial<SubmitUser>): Omit<Partial<ServerUser>, "_id"> {
let serverUser: Omit<Partial<ServerUser>, "_id"> = {
...submitUser,
flairs: [],
code: submitUser.code || nanoid(16),
updatedDate: new Date()
};
return serverUser;
}
export function MergeServerUsers(submitUser: ServerUser, merge: Partial<Omit<ServerUser, "_id">>): ServerUser {
let serverUser: ServerUser = {
...submitUser,
...cutObject(merge),
updatedDate: new Date()
};
return serverUser;
}

View file

@ -0,0 +1,28 @@
import { ServerFlair } from "@/types/flair";
import { Filter, cursorToArray, readMany, readOne } from "../db/crud";
/**
* A function that returns one ServerFlairs from the database.
* @param query A partial Find query. Can contain an ID.
* @returns A ServerFlair.
*/
export async function getSingleFlair(query: Filter<ServerFlair>): Promise<ServerFlair | null> {
const flair = (await readOne("flairs", query)) as ServerFlair | null;
return flair;
}
/**
* A function that returns many ServerFlairs from the database using a FindCursor.
* @param query A partial Find query. Can contain an ID.
* @param func A function to run on every ServerFlairs returned. Helps reduce loops.
* @returns An array of ServerFlairs.
*/
export async function getManyFlairs(
query: Filter<ServerFlair>,
func?: (input: any) => any
): Promise<(ServerFlair | null)[]> {
const flair = (await cursorToArray(readMany("flairs", query), func)) as (ServerFlair | null)[];
return flair;
}

View file

@ -0,0 +1,28 @@
import { ServerTroll } from "@/types/troll";
import { Filter, cursorToArray, readMany, readOne } from "../db/crud";
/**
* A function that returns one ServerTrolls from the database.
* @param query A partial Find query. Can contain an ID.
* @returns A ServerTroll.
*/
export async function getSingleTroll(query: Filter<ServerTroll>): Promise<ServerTroll | null> {
const troll = (await readOne("trolls", query)) as ServerTroll | null;
return troll;
}
/**
* A function that returns many ServerTrolls from the database using a FindCursor.
* @param query A partial Find query. Can contain an ID.
* @param func A function to run on every ServerTrolls returned. Helps reduce loops.
* @returns An array of ServerTrolls.
*/
export async function getManyTrolls(
query: Filter<ServerTroll>,
func?: (input: any) => any
): Promise<(ServerTroll | null)[]> {
const troll = (await cursorToArray(readMany("trolls", query), func)) as (ServerTroll | null)[];
return troll;
}

50
src/lib/trollcall/user.ts Normal file
View file

@ -0,0 +1,50 @@
import { ServerUser } from "@/types/user";
import { Filter, createOne, cursorToArray, readMany, readOne, replaceOne } from "../db/crud";
/**
* A function that returns one ServerUser from the database.
* @param query A partial Find query. Can contain an ID.
* @returns A ServerUser.
*/
export async function getSingleUser(query: Filter<ServerUser>): Promise<ServerUser | null> {
const user = (await readOne("users", query)) as ServerUser | null;
return user;
}
/**
* A function that returns many ServerUsers from the database using a FindCursor.
* @param query A partial Find query. Can contain an ID.
* @param func A function to run on every ServerUser returned. Helps reduce loops.
* @returns An array of ServerUsers.
*/
export async function getManyUsers<T>(
query: Filter<ServerUser>,
func?: (input: any) => T
): Promise<(Awaited<T> | null)[]> {
const user = (await cursorToArray(readMany("users", query), func)) as (Awaited<T> | null)[];
return user;
}
/**
* A function that puts one ServerUser into the database.
* @param user A ServerUser.
* @returns A ServerUser, or null, depending on if the operation succeeded.
*/
export async function createUser(user: Omit<ServerUser, "_id">): Promise<Omit<ServerUser, "_id"> | null> {
const newUser = await createOne("users", user);
return newUser.acknowledged ? user : null;
}
/**
* A function that changes one database user with the given params.
* @param user A ServerUser.
* @returns A ServerUser, or null, depending on if the operation succeeded.
*/
export async function changeUser(user: ServerUser): Promise<ServerUser | null> {
const newUser = await replaceOne("users", { _id: user._id }, user);
return newUser.acknowledged ? user : null;
}

View file

@ -0,0 +1,38 @@
import { WithId } from "@/lib/db/crud";
import _ from "lodash";
export function cutArray<T>(array: T[]) {
const cut: any[] = [];
array.forEach((value, i) => {
if (value == null) return;
if (Array.isArray(value)) cut[i] = cutArray(value);
else if (typeof value == "object") cut[i] = cutObject(value);
else cut[i] = value;
});
return cut as NonNullable<T>[];
}
export function cutObject<T extends { [key: string]: any }>(object: T) {
const keys = Object.keys(object);
let cut: { [key: string]: any } = {};
keys.forEach(key => {
var val = object[key];
if (val == null) return;
if (Array.isArray(val)) cut[key] = cutArray(val);
else if (_.isObject(val)) cut[key] = cutObject(val);
else cut[key] = val;
});
return cut as T;
}
export function sanitize<Type extends WithId<{}>>(serverType: Type): Omit<Type, "_id"> {
const sanitized: Partial<Type> = serverType;
delete sanitized._id;
return sanitized as Omit<Type, "_id">;
}
export function removeCode<Type extends { code: string }>(serverType: Type): Omit<Type, "code"> {
const sanitized: Partial<Type> = serverType;
delete sanitized.code;
return sanitized as Omit<Type, "code">;
}

View file

@ -0,0 +1,16 @@
// needlessly complicated branding guide. oh well
// this helps you get the gist of how we represent TrollCall within the site
export const brand = {
name: "TrollCall",
code: "TrollCallNext",
owner: "MeowcaTheoRange"
};
export const domain = {
main: "trollcall",
tld: "xyz",
owner: "Redact4K"
};
export const fullDomain = domain.main + "." + domain.tld;
export const source = "the original Hiveswap Troll Call";
export const sourceCopyright = "Homestuck and HIVESWAP ©️ Homestuck Inc.";

102
src/types/assist/color.ts Normal file
View file

@ -0,0 +1,102 @@
import * as yup from "yup";
export type ColorTypes = [number, number, number];
export const ColorSchema = yup.tuple([
yup.number().required().min(0).max(255),
yup.number().required().min(0).max(255),
yup.number().required().min(0).max(255),
]);
const clamp = (n: number, mi: number, ma: number) =>
Math.max(mi, Math.min(n, ma));
export class Color3 {
R: number;
G: number;
B: number;
constructor(red: number, green: number, blue: number) {
this.R = red;
this.G = green;
this.B = blue;
}
static clone(color3: Color3) {
return new Color3(color3.R, color3.G, color3.B);
}
static fromRGB(red: number, green: number, blue: number) {
return new Color3(red / 255, green / 255, blue / 255);
}
static fromHex(hex: string) {
// @ts-ignore
const hexSplit: [number, number, number] = (
hex.match(new RegExp(`[0-9a-f]{1,${hex.length / 3}}`, "gi")) ?? [
"0",
"0",
"0",
]
).map((x) => parseInt(x, 16) / 255);
return new Color3(...hexSplit);
}
static fromInt(int: number) {
return new Color3(
(int & 0xff0000) >> 16,
(int & 0x00ff00) >> 8,
int & 0x0000ff
);
}
static assumeColor(
value: [number, number, number] | string | number,
rgb?: boolean
) {
if (Color3.isColor(value)) {
if (Array.isArray(value))
return rgb ? Color3.fromRGB(...value) : new Color3(...value);
else if (typeof value === "string") return Color3.fromHex(value);
else if (typeof value === "number") return Color3.fromInt(value);
}
throw new Error("Not a valid color type");
}
static isColor(
value: [number, number, number] | string | number,
rgb?: boolean
) {
return (
(Array.isArray(value) &&
value.length === 3 &&
value.every((x) => typeof x === "number" && !isNaN(x))) ||
(typeof value === "string" && !isNaN(parseInt(value, 16))) ||
(typeof value === "number" && !isNaN(value))
);
}
toHex() {
return this.toInt().toString(16).padStart(6, "0");
}
toInt() {
return (
(Math.round(this.R * 255) << 16) +
(Math.round(this.G * 255) << 8) +
Math.round(this.B * 255)
);
}
toRGB(): [number, number, number] {
return [this.R * 255, this.G * 255, this.B * 255];
}
multiply(mult: number) {
return new Color3(this.R * mult, this.G * mult, this.B * mult);
}
lighten(mult: number) {
return new Color3(
clamp(this.R + (mult / 100) * (1 - this.R), 0, 1),
clamp(this.G + (mult / 100) * (1 - this.G), 0, 1),
clamp(this.B + (mult / 100) * (1 - this.B), 0, 1)
);
}
darken(mult: number) {
return new Color3(
clamp(this.R - (mult / 100) * this.R, 0, 1),
clamp(this.G - (mult / 100) * this.G, 0, 1),
clamp(this.B - (mult / 100) * this.B, 0, 1)
);
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,6 @@
import * as yup from "yup";
export const PolicySchema = yup
.string()
.oneOf(["yes", "ask", "no"])
.default("no");

View file

@ -0,0 +1,51 @@
export function AdaptivePossessive(owner: string, possession: string) {
return owner + (owner.endsWith("s") ? "'" : "'s") + " " + possession;
}
export function PronounGrouper(
pronouns: [string, string, string],
sep?: string,
amount?: number
) {
return pronouns.slice(0, amount ?? 2).join(sep ?? "/");
}
export function HeightConverter(inches: number) {
var feetandinches = Math.floor(inches / 12) + "'" + (inches % 12) + '"';
var meters = Math.floor((inches / 39.37) * 100) / 100 + "m";
return feetandinches + " (" + meters + ")";
}
export function AgeConverter(sweeps: number, years?: boolean) {
return years
? `${Math.round(sweeps * 2.1667 * 10) / 10} years`
: `${sweeps} sweeps`;
}
export function Pluralize(stringe: string) {
if (stringe.match(/(?:s|ch|sh|x|z|[^aeiou]o)$/)) return stringe + "es";
else if (stringe.match(/[aeiou](?:y|o)$/)) return stringe + "s";
else if (stringe.match(/[^aeiou]y$/)) return stringe.slice(0, -1) + "ies";
else if (stringe.match(/(?:f|fe)$/)) return stringe.slice(0, -1) + "ves";
else return stringe + "s";
}
export function ArraySample(array: any[]) {
return array[Math.floor(Math.random() * array.length)];
}
export function ProperNounCase(string: string) {
return string
.split(" ")
.map((x) => x.charAt(0).toUpperCase() + x.slice(1).toLowerCase())
.join(" ");
}
export function PesterchumNameFormatter(string: string) {
return (
string +
" [" +
string.replace(/^(([a-z])[a-z]+)(([A-Z])[a-z]+)$/, "$2$4").toUpperCase() +
"]"
);
}

View file

@ -0,0 +1,6 @@
import { ObjectId } from "mongodb";
import * as yup from "yup";
export const ObjectIdSchema = yup.mixed((value): value is ObjectId =>
ObjectId.isValid(value)
);

View file

@ -0,0 +1,53 @@
import * as yup from "yup";
export const SubmitDialogSchema = yup.object({
owners: yup.array().of(yup.string().required()).required().min(1),
name: yup.string().required().min(3).max(100),
description: yup.string().max(10000).ensure(),
characters: yup
.array()
.of(
yup
.object({
troll: yup.string().required(),
time: yup.string().ensure()
})
.required()
)
.required(),
log: yup
.array()
.of(
yup
.object({
character: yup.number().notRequired().min(0),
quirk: yup.string().default("default"),
text: yup.string().required().max(2000)
})
.required()
)
.required()
});
export type SubmitDialog = yup.InferType<typeof SubmitDialogSchema>;
export const PartialDialogSchema = yup.object({
owners: yup.array().of(yup.string()).min(1),
name: yup.string().min(3).max(100),
description: yup.string().max(10000),
characters: yup.array().of(
yup.object({
troll: yup.string(),
time: yup.string()
})
),
log: yup.array().of(
yup.object({
character: yup.number().min(0),
quirk: yup.string().default("default"),
text: yup.string().max(2000)
})
)
});
export type PartialDialog = yup.InferType<typeof PartialDialogSchema>;

26
src/types/client/flair.ts Normal file
View file

@ -0,0 +1,26 @@
import * as yup from "yup";
import { ColorSchema } from "../assist/color";
export const SubmitFlairSchema = yup
.object({
name: yup.string().required().min(10).max(50),
alt: yup.string().max(1000),
color: ColorSchema.required(),
link: yup.string().notRequired().url()
})
.required();
export type SubmitFlair = yup.InferType<typeof SubmitFlairSchema>;
export const PartialFlairSchema = yup
.object({
name: yup.string().min(10).max(50),
alt: yup.string().max(1000),
color: ColorSchema,
link: yup.string().url()
})
.required();
export type PartialFlair = yup.InferType<typeof PartialFlairSchema>;

View file

@ -0,0 +1,10 @@
import * as yup from "yup";
import { QuirkSchema } from "../quirks";
export const SubmitQuirkHolderSchema = yup
.array()
.of(yup.tuple([yup.string().required().lowercase(), QuirkSchema.required()]).required())
.required()
.test("has-default", 'Needs "default" Quirk Mode', v => v.some(([k, v]) => k === "default" || k === "Default"));
export type SubmitQuirkHolder = yup.InferType<typeof SubmitQuirkHolderSchema>;

236
src/types/client/troll.ts Normal file
View file

@ -0,0 +1,236 @@
import { ColorSchema } from "@/types/assist/color";
import * as yup from "yup";
import { ClassKeys, TrueSignKeys } from "../assist/extended_zodiac";
import { PolicySchema } from "../assist/generics";
import { SubmitQuirkHolderSchema } from "./quirks";
export const SubmitTrollSchema = yup
.object({
// Name and identification
name: yup
.tuple([
yup
.string()
.required()
.matches(/^[A-z]+$/, "Letters only")
.length(6)
.lowercase(),
yup
.string()
.required()
.matches(/^[A-z]+$/, "Letters only")
.length(6)
.lowercase()
])
.required(),
description: yup.string().max(10000).ensure(),
pronunciation: yup
.tuple([
yup
.string()
.required()
.matches(/^[A-z-]+$/, "Letters only")
.lowercase(),
yup
.string()
.required()
.matches(/^[A-z-]+$/, "Letters only")
.lowercase()
])
.required(),
pronouns: yup
.array()
.of(
yup
.tuple([
yup
.string()
.required()
.matches(/^[A-z]+$/, "Letters only")
.min(1)
.max(10)
.lowercase(), // she, he, they
yup
.string()
.required()
.matches(/^[A-z]+$/, "Letters only")
.min(1)
.max(10)
.lowercase(), // her, him, them
yup
.string()
.required()
.matches(/^[A-z]+$/, "Letters only")
.min(1)
.max(10)
.lowercase() // hers, his, theirs
])
.required()
)
.required()
.min(1),
gender: yup
.string()
.required()
.matches(/^[A-z-_]+$/, "Letters only")
.min(3)
.max(30),
// Personal
preferences: yup
.object({
love: yup.array().of(yup.string().required().min(5).max(100)).required().min(3).max(10),
hate: yup.array().of(yup.string().required().min(5).max(100)).required().min(3).max(10)
})
.required(),
facts: yup.array().of(yup.string().required().min(5).max(100)).required().min(3).max(10),
// Hiveswap identity
trueSign: yup.string().required().oneOf(TrueSignKeys),
falseSign: yup
.string()
.nullable()
.transform(v => {
return v === "" ? null : v;
})
.oneOf(TrueSignKeys), // "Keelez Bunbat"
class: yup.string().required().oneOf(ClassKeys),
// Trollian
username: yup
.string()
.required()
.matches(/^(([a-z])[a-z]+)(([A-Z])[a-z]+)$/, "Username must match Pesterchum formatting."),
textColor: ColorSchema.notRequired(), // default to trueSign color if undefined,
quirks: SubmitQuirkHolderSchema.required(), // DO NOT HANDLE RIGHT NOW.
// Handled! :D
// Physical stuff
species: yup
.string()
.notRequired()
.matches(/^([A-z-]+)|()$/, "Letters only"), // "Troll-*" if defined. Otherwise, just "Troll".
height: yup.number().required().positive(), // Inches
age: yup.number().required().positive(), // Sweeps
image: yup.string().required().url(),
// Meta stuff
policies: yup
.object({
fanart: PolicySchema.required(),
fanartOthers: PolicySchema.required(),
kinning: PolicySchema.required(),
shipping: PolicySchema.required(),
fanfiction: PolicySchema.required()
})
.required()
// owners: yup.array().of(yup.string().required()).required().min(1),
// flairs: yup.array().of(yup.mixed()).required().ensure(),
})
.required();
export type SubmitTroll = yup.InferType<typeof SubmitTrollSchema>;
export const PartialTrollSchema = yup
.object({
// Name and identification
name: yup.tuple([
yup
.string()
.matches(/^[A-z]+$/, "Letters only")
.length(6)
.lowercase(),
yup
.string()
.matches(/^[A-z]+$/, "Letters only")
.length(6)
.lowercase()
]),
description: yup.string().max(10000),
pronunciation: yup.tuple([
yup
.string()
.matches(/^[A-z-]+$/, "Letters only")
.lowercase(),
yup
.string()
.matches(/^[A-z-]+$/, "Letters only")
.lowercase()
]),
pronouns: yup
.array()
.of(
yup.tuple([
yup
.string()
.matches(/^[A-z]+$/, "Letters only")
.min(1)
.max(10)
.lowercase(), // she, he, they
yup
.string()
.matches(/^[A-z]+$/, "Letters only")
.min(1)
.max(10)
.lowercase(), // her, him, them
yup
.string()
.matches(/^[A-z]+$/, "Letters only")
.min(1)
.max(10)
.lowercase() // hers, his, theirs
])
)
.min(1),
gender: yup
.string()
.matches(/^[A-z-_]+$/, "Letters only")
.min(3)
.max(30),
// Personal
preferences: yup.object({
love: yup.array().of(yup.string().min(5).max(100)).min(3).max(10),
hate: yup.array().of(yup.string().min(5).max(100)).min(3).max(10)
}),
facts: yup.array().of(yup.string().min(5).max(100)).min(3).max(10),
// Hiveswap identity
trueSign: yup.string().oneOf(TrueSignKeys),
falseSign: yup
.string()
.nullable()
.transform(v => {
return v === "" ? null : v;
})
.oneOf(TrueSignKeys), // "Keelez Bunbat"
class: yup.string().oneOf(ClassKeys),
// Trollian
username: yup
.string()
.matches(/^(([a-z])[a-z]+)(([A-Z])[a-z]+)$/, "Username must match Pesterchum formatting."),
textColor: yup.tuple([
yup.number().min(0).max(255),
yup.number().min(0).max(255),
yup.number().min(0).max(255)
]),
quirks: SubmitQuirkHolderSchema, // DO NOT HANDLE RIGHT NOW.
// Handled! :D
// Physical stuff
species: yup.string().matches(/^([A-z-]+)|()$/, "Letters only"), // "Troll-*" if defined. Otherwise, just "Troll".
height: yup.number().positive(), // Inches
age: yup.number().positive(), // Sweeps
image: yup.string().url(),
// Meta stuff
policies: yup.object({
fanart: PolicySchema,
fanartOthers: PolicySchema,
kinning: PolicySchema,
shipping: PolicySchema,
fanfiction: PolicySchema
})
})
.required();
export type PartialTroll = yup.InferType<typeof PartialTrollSchema>;

50
src/types/client/user.ts Normal file
View file

@ -0,0 +1,50 @@
import * as yup from "yup";
import { ColorSchema } from "../assist/color";
import { TrueSignKeys } from "../assist/extended_zodiac";
export const SubmitUserSchema = yup
.object({
name: yup
.string()
.required()
.matches(/^[\w-]+$/, "No special characters or spaces")
.min(3)
.max(50)
.lowercase(),
description: yup.string().max(10000).ensure(),
url: yup.string().notRequired().url(),
trueSign: yup.string().required().oneOf(TrueSignKeys),
color: ColorSchema.required(),
pfp: yup.string().notRequired().url(),
code: yup.string().notRequired().max(256, "Too secure!!")
// flairs: yup.array().of(ClientFlairSchema).required(),
})
.required();
export type SubmitUser = yup.InferType<typeof SubmitUserSchema>;
export const PartialUserSchema = yup
.object({
name: yup
.string()
.matches(/^[\w-]+$/, "No special characters or spaces")
.min(3)
.max(50)
.lowercase(),
description: yup.string().max(10000),
url: yup.string().url(),
trueSign: yup
.string()
.nullable()
.transform(v => {
return v === "" ? null : v;
})
.oneOf(TrueSignKeys),
color: yup.tuple([yup.number().min(0).max(255), yup.number().min(0).max(255), yup.number().min(0).max(255)]),
pfp: yup.string().url(),
code: yup.string().max(256, "Too secure!!")
// flairs: yup.array().of(ClientFlairSchema).required(),
})
.required();
export type PartialUser = yup.InferType<typeof PartialUserSchema>;

45
src/types/dialoglog.ts Normal file
View file

@ -0,0 +1,45 @@
import { WithId } from "@/lib/db/crud";
import * as yup from "yup";
import { ObjectIdSchema } from "./assist/mongo";
import { SubmitDialogSchema } from "./client/dialoglog";
import { ClientTroll, ClientTrollSchema } from "./troll";
import { ClientUserSchema } from "./user";
export const ServerDialogSchema = SubmitDialogSchema.shape({
owners: yup.array().of(ObjectIdSchema.required()).required().min(1),
characters: yup
.array()
.of(
yup
.object({
troll: ObjectIdSchema.required(),
time: yup.string().ensure()
})
.required()
)
.required()
});
export type ServerDialog = WithId<yup.InferType<typeof ServerDialogSchema>>;
export const ClientDialogSchema = SubmitDialogSchema.shape({
owners: yup.array().of(ClientUserSchema.required()).required().min(1),
characters: yup
.array()
.of(
yup
.object({
troll: ClientTrollSchema.required(),
time: yup.string().ensure()
})
.required()
)
.required()
});
export interface ClientDialog extends yup.InferType<typeof ClientDialogSchema> {
characters: {
troll: ClientTroll;
time: string;
}[];
} // [SEARCH: HACK] a hack. thanks, jquense

11
src/types/flair.ts Normal file
View file

@ -0,0 +1,11 @@
import { WithId } from "@/lib/db/crud";
import * as yup from "yup";
import { SubmitFlairSchema } from "./client/flair";
export const ServerFlairSchema = SubmitFlairSchema.shape({});
export type ServerFlair = WithId<yup.InferType<typeof ServerFlairSchema>>;
export const ClientFlairSchema = SubmitFlairSchema.shape({});
export type ClientFlair = yup.InferType<typeof ClientFlairSchema>;

87
src/types/quirks.ts Normal file
View file

@ -0,0 +1,87 @@
import * as yup from "yup";
// See FlaringK's Quirk Builder for type info.
// https://flaringk.github.io/quirkbuilder/
// https://discord.com/channels/294616636726444033/1067996841532215337/1111282886038016130
export const QuirkSchema = yup
.object({
quirk: yup
.array()
.of(
yup
.object({
type: yup
.string()
.oneOf([
"prefix",
"suffix",
"simple",
"regex",
"case",
"case_simple",
"case_regex",
"case_pos"
])
.required(),
find: yup.string().notRequired(),
replace: yup
.array()
.of(
yup
.string()
.required()
.matches(/^([A-z-]+)|()$/, "Letters only")
)
.required()
.min(1),
condition: yup.string().notRequired()
})
.required()
)
.required()
})
.required();
export type Quirk = yup.InferType<typeof QuirkSchema>;
export const ServerQuirkHolderSchema = yup.mixed(); // cant do SHIT in yup
export type ServerQuirkHolder = { [key: string]: Quirk };
export const QuirkReplacementTypes: {
[key: string]: { find: string | null; replace: string };
} = {
prefix: {
find: null,
replace: "The text that precedes the main text."
},
suffix: {
find: null,
replace: "The text that proceeds the main text."
},
simple: {
find: "Text to find what you want to replace.",
replace: "The text that replaces the found text."
},
regex: {
find: "A Regular Expresion to find what you want to replace.",
replace: "A replacement expression that replaces the found text."
},
case: {
find: null,
replace: 'Change the case of all text. "lower" for lowercase. "upper" for uppercase.'
},
case_simple: {
find: "Text to find what you want to change the case of.",
replace: '"lower" for lowercase. "upper" for uppercase.'
},
case_regex: {
find: "A Regular Expresion to find what you want to change the case of.",
replace: '"lower" for lowercase. "upper" for uppercase.'
},
case_pos: {
find: 'A comma-separated array of properties.\n\n1st is number of characters (type "Infinity" for all),\n2nd is start position (start, end),\n3rd is character seperation (like Gamzee for selecting every other character),\n 4th is the union modifier. "true" means the position will affect the entire sentence. "false" or no input means the position will affect each word.\n\nExample: "2, start, 1, false" for words to look like "GrEetings EvErybody".',
replace: '"lower" for lowercase. "upper" for uppercase.'
}
};

30
src/types/troll.ts Normal file
View file

@ -0,0 +1,30 @@
import { WithId } from "@/lib/db/crud";
import * as yup from "yup";
import { ClassSchema, TrueSignSchema } from "./assist/extended_zodiac";
import { ObjectIdSchema } from "./assist/mongo";
import { SubmitTrollSchema } from "./client/troll";
import { ClientFlairSchema } from "./flair";
import { ServerQuirkHolder, ServerQuirkHolderSchema } from "./quirks";
import { ClientUserSchema } from "./user";
export const ServerTrollSchema = SubmitTrollSchema.shape({
owners: yup.array().of(ObjectIdSchema.required()).required().min(1),
flairs: yup.array().of(ObjectIdSchema.required()).required(),
quirks: ServerQuirkHolderSchema.required(),
updatedDate: yup.date().notRequired()
});
export type ServerTroll = WithId<yup.InferType<typeof ServerTrollSchema>>;
export const ClientTrollSchema = SubmitTrollSchema.shape({
owners: yup.array().of(ClientUserSchema.required()).required().min(1),
flairs: yup.array().of(ClientFlairSchema.required()).required(),
quirks: ServerQuirkHolderSchema.required(),
trueSign: TrueSignSchema.required(),
falseSign: TrueSignSchema.notRequired(),
class: ClassSchema.required()
});
export interface ClientTroll extends yup.InferType<typeof ClientTrollSchema> {
quirks: ServerQuirkHolder;
} // [SEARCH: HACK] a hack. thanks, jquense

22
src/types/user.ts Normal file
View file

@ -0,0 +1,22 @@
import { WithId } from "@/lib/db/crud";
import * as yup from "yup";
import { TrueSignSchema } from "./assist/extended_zodiac";
import { ObjectIdSchema } from "./assist/mongo";
import { SubmitUserSchema } from "./client/user";
import { ClientFlairSchema } from "./flair";
export const ServerUserSchema = SubmitUserSchema.shape({
flairs: yup.array().of(ObjectIdSchema.required()).required(),
code: yup.string().required(),
updatedDate: yup.date().notRequired()
});
export type ServerUser = WithId<yup.InferType<typeof ServerUserSchema>>;
export const ClientUserSchema = SubmitUserSchema.shape({
flairs: yup.array().of(ClientFlairSchema.required()).required(),
trueSign: TrueSignSchema.required(),
updatedDate: yup.number().notRequired()
});
export type ClientUser = yup.InferType<typeof ClientUserSchema>;

20
tsconfig.json Normal file
View file

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2021",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"jsx": "preserve",
"strict": true,
"skipLibCheck": true,
"noErrorTruncation": true,
"moduleResolution": "node",
"rootDir": "src",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"outDir": "./dist/"
},
"include": ["src/**/*.d.ts", "src/**/*.ts"]
}