Implement user and inventory, back messages, and color changing, add pokedex

This commit is contained in:
Hri7566 2024-02-22 08:10:24 -05:00
parent 11967df2ae
commit 29676fb502
21 changed files with 29520 additions and 43 deletions

16586
config/pokedex.json Normal file

File diff suppressed because it is too large Load Diff

12540
config/pokedex.yml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -21,7 +21,7 @@ model User {
model Inventory {
id Int @id @default(autoincrement())
balance Int
balance Int @default(0)
items Json @default("[]")
fishSack Json @default("[]")

36
scripts/jsonyaml.ts Normal file
View File

@ -0,0 +1,36 @@
import { Logger } from "@util/Logger";
import { argv } from "bun";
import { existsSync, readFileSync, writeFileSync } from "fs";
import YAML from "yaml";
const logger = new Logger("JSON Converter");
const inFile = argv[2];
const outFile = argv[3];
if (typeof inFile !== "string" || typeof outFile !== "string") {
logger.error(`Usage: <infile> <outfile>`);
process.exit();
}
if (!existsSync(inFile)) {
logger.error("Input file not found");
process.exit();
}
logger.info("Reading JSON...");
let data: unknown;
try {
const jdata = readFileSync(inFile).toString();
data = JSON.parse(jdata);
} catch (err) {
logger.error(err);
logger.error("JSON read error");
process.exit();
}
logger.info("Writing file...");
writeFileSync(outFile, YAML.stringify(data));
logger.info("Done.");

View File

@ -1,6 +1,7 @@
import { getBacks, flushBacks } from "@server/backs";
import { handleCommand } from "@server/commands/handler";
import { prefixes } from "@server/commands/prefixes";
import { checkToken } from "@server/data/token";
import { checkToken, tokenToID } from "@server/data/token";
import { TRPCError, initTRPC } from "@trpc/server";
import { Logger } from "@util/Logger";
import type { CreateBunContextOptions } from "trpc-bun-adapter";
@ -8,11 +9,21 @@ import { z } from "zod";
const logger = new Logger("tRPC");
interface FishingContext {
isAuthed: boolean;
req: Request;
token: string | null;
}
interface AuthedFishingContext extends FishingContext {
token: string;
}
export const createContext = async (opts: CreateBunContextOptions) => {
return {
isAuthed: false,
req: opts.req
};
} as FishingContext;
};
export type Context = Awaited<ReturnType<typeof createContext>>;
@ -25,14 +36,15 @@ export const privateProcedure = publicProcedure.use(async opts => {
const { ctx } = opts;
const { req } = ctx;
const token = req.headers.get("authorization");
if (!token) throw new TRPCError({ code: "UNAUTHORIZED" });
opts.ctx.isAuthed = await checkToken(token);
opts.ctx.token = req.headers.get("authorization");
if (!opts.ctx.token) throw new TRPCError({ code: "UNAUTHORIZED" });
opts.ctx.isAuthed = await checkToken(opts.ctx.token);
if (!ctx.isAuthed) throw new TRPCError({ code: "UNAUTHORIZED" });
return opts.next(opts);
return opts.next({
ctx: opts.ctx as AuthedFishingContext
});
});
export const appRouter = router({
@ -54,10 +66,20 @@ export const appRouter = router({
})
)
.query(async opts => {
const id = tokenToID(opts.ctx.token);
const { command, args, prefix, user } = opts.input;
const out = await handleCommand(command, args, prefix, user);
const out = await handleCommand(id, command, args, prefix, user);
return out;
}),
backs: privateProcedure.query(async opts => {
const id = tokenToID(opts.ctx.token);
const backs = getBacks<{}>(id);
flushBacks(id);
return backs;
})
});

20
src/api/backs/index.ts Normal file
View File

@ -0,0 +1,20 @@
export const backs: Record<string, IBack<unknown>[]> = {};
export function flushBacks<T>(id: string) {
backs[id] = [];
}
export function addBack<T>(id: string, back: IBack<T>) {
if (!backs[id]) backs[id] = [];
backs[id].push(back);
}
export function hasBack<T>(id: string, back: IBack<T>) {
if (!backs[id]) return false;
if (backs[id].includes(back)) return true;
}
export function getBacks<T>(id: string) {
if (!backs[id]) return [];
return backs[id] as T;
}

View File

@ -1,3 +1,5 @@
import type { User } from "@prisma/client";
export class Command {
constructor(
public id: string,
@ -5,7 +7,7 @@ export class Command {
public description: string,
public usage: string,
public permissionNode: string,
public callback: TCommandCallback,
public callback: TCommandCallback<User>,
public visible: boolean = true
) {}
}

View File

@ -6,7 +6,7 @@ export const fish = new Command(
"Send your LURE into a water for catching fish",
"fish",
"command.fishing.fish",
async () => {
async ({ id, command, args, prefix, part, user }) => {
return "There is no fishing yet, please come back later when I write the code for it";
}
);

View File

@ -18,17 +18,30 @@ export const help = new Command(
"cammands",
"cummunds"
],
"Help command",
"Get command list or command usage",
"help [command]",
"command.general.help",
async (command, args, prefix, user) => {
async ({ id, command, args, prefix, part, user }) => {
if (!args[0]) {
return `${commandGroups
.map(
group =>
`${group.displayName}: ${group.commands
.map(cmd => (cmd.visible ? cmd.aliases[0] : "<hidden>"))
.map(cmd =>
cmd.visible ? cmd.aliases[0] : "<hidden>"
)
.join(", ")}`
)
.join("\n")}`;
} else {
const commands = commandGroups.flatMap(group => group.commands);
const foundCommand = commands.find(cmd =>
cmd.aliases.includes(args[0])
);
if (!foundCommand) return `Command "${args[0]}" not found.`;
return `Description: ${foundCommand.description} | Usage: ${foundCommand.usage}`;
}
}
);

View File

@ -1,6 +1,7 @@
import type { Command } from "../Command";
import { fish } from "./fishing/fish";
import { help } from "./general/help";
import { setcolor } from "./util/setcolor";
import { data } from "./util/data";
interface ICommandGroup {
@ -30,7 +31,7 @@ commandGroups.push(fishing);
const util: ICommandGroup = {
id: "util",
displayName: "Utility",
commands: [data]
commands: [data, setcolor]
};
commandGroups.push(util);

View File

@ -6,7 +6,7 @@ export const data = new Command(
"Data command",
"data",
"command.util.data",
async (command, args, prefix, user) => {
return JSON.stringify({ command, args, prefix, user });
async props => {
return JSON.stringify(props);
}
);

View File

@ -0,0 +1,21 @@
import { addBack } from "@server/backs";
import Command from "@server/commands/Command";
export const setcolor = new Command(
"setcolor",
["setcolor"],
"Set own user color",
"setcolor <color>",
"command.util.setcolor",
async ({ id, command, args, prefix, part, user }) => {
if (typeof args[0] !== "string") return "Please provide a color.";
addBack(id, {
m: "color",
id: part.id,
color: args[0]
});
return "Attempting to set color.";
}
);

View File

@ -1,14 +1,17 @@
import { Logger } from "@util/Logger";
import type Command from "./Command";
import { commandGroups } from "./groups";
import { createUser, getUser } from "@server/data/user";
import { createInventory, getInventory } from "@server/data/inventory";
export const logger = new Logger("Command Handler");
export async function handleCommand(
id: string,
command: string,
args: string[],
prefix: string,
user: IUser
part: IPart
): Promise<ICommandResponse | void> {
let foundCommand: Command | undefined;
@ -22,21 +25,42 @@ export async function handleCommand(
if (!foundCommand) return;
let user = await getUser(part.id);
if (!user) {
const inventory = await createInventory({});
user = await createUser({
id: part.id,
name: part.name,
color: part.color,
inventoryId: inventory.id
});
}
let inventory = await getInventory(user.inventoryId);
if (!inventory) inventory = await createInventory({ id: user.inventoryId });
// TODO Check user's (or their groups') permissions against command permission node
try {
const response = await foundCommand.callback(
const response = await foundCommand.callback({
id,
command,
args,
prefix,
part,
user
);
});
if (response) return { response };
} catch (err) {
logger.error(err);
return {
response:
"An error has occurred, but no fish were lost. If you are the fishing bot owner, check the error logs for details."
"An error has occurred, but no fish were lost. If you are the fishing bot owner, check the server's error logs for details."
};
}
}

26
src/api/data/inventory.ts Normal file
View File

@ -0,0 +1,26 @@
import prisma from "./prisma";
export async function createInventory(inventory: Partial<IInventory>) {
return await prisma.inventory.create({
data: inventory
});
}
export async function getInventory(id: IInventory["id"]) {
return await prisma.inventory.findUnique({
where: { id }
});
}
export async function updateInventory(inventory: Partial<IInventory>) {
return await prisma.inventory.update({
where: { id: inventory.id },
data: inventory
});
}
export async function deleteInventory(id: IInventory["id"]) {
return await prisma.inventory.delete({
where: { id }
});
}

View File

@ -1,4 +1,5 @@
import prisma from "./prisma";
import { createHash } from "crypto";
export async function createToken() {
const randomToken = crypto.randomUUID();
@ -29,3 +30,10 @@ export async function checkToken(token: string) {
export async function getAllTokens() {
return await prisma.authToken.findMany();
}
export function tokenToID(token: string) {
const hash = createHash("sha-256");
hash.update("ID");
hash.update(token);
return hash.digest("hex").substring(0, 24);
}

31
src/api/data/user.ts Normal file
View File

@ -0,0 +1,31 @@
import type { User } from "@prisma/client";
import prisma from "./prisma";
export async function createUser(user: User) {
return await prisma.user.create({
data: user
});
}
export async function getUser(id: string) {
const user = await prisma.user.findUnique({
where: { id }
});
return user;
}
export async function updateUser(user: Partial<User> & { id: User["id"] }) {
return await prisma.user.update({
where: {
id: user.id
},
data: user
});
}
export async function deleteUser(id: string) {
return await prisma.user.delete({
where: { id }
});
}

View File

@ -1,6 +1,7 @@
import Client from "mpp-client-net";
import { Logger } from "@util/Logger";
import trpc from "@client/api/trpc";
import { EventEmitter } from "events";
export interface MPPNetBotConfig {
uri: string;
@ -13,7 +14,7 @@ export interface MPPNetBotConfig {
export class MPPNetBot {
public client: Client;
public b = new EventEmitter();
public logger: Logger;
constructor(
@ -78,7 +79,8 @@ export class MPPNetBot {
});
if (!command) return;
if (command.response) this.sendChat(command.response);
if (command.response)
this.sendChat(command.response, (msg as any).id);
});
(this.client as unknown as any).on(
@ -143,24 +145,52 @@ export class MPPNetBot {
});
if (!command) return;
if (command.response) this.sendChat(command.response);
if (command.response) this.sendChat(command.response, msg.id);
}
);
setInterval(async () => {
try {
const backs = (await trpc.backs.query()) as IBack<unknown>[];
if (backs.length > 0) {
this.logger.debug(backs);
for (const back of backs) {
if (typeof back.m !== "string") return;
this.b.emit(back.m, back);
}
}
} catch (err) {
return;
}
}, 1000 / 20);
this.b.on("color", msg => {
if (typeof msg.color !== "string" || typeof msg.id !== "string")
return;
this.client.sendArray([
{
m: "setcolor",
_id: msg.id,
color: msg.color
}
]);
});
}
public sendChat(text: string) {
public sendChat(text: string, reply?: string) {
let lines = text.split("\n");
for (const line of lines) {
if (line.length <= 510) {
this.client.sendArray([
(this.client as any).sendArray([
{
m: "a",
message: `\u034f${line
.split("\t")
.join("")
.split("\r")
.join("")}`
.join("")}`,
reply_to: reply
}
]);
} else {

65
src/util/types.d.ts vendored
View File

@ -1,4 +1,4 @@
interface IUser {
interface IPart {
id: string;
name: string;
color: string;
@ -8,9 +8,60 @@ interface ICommandResponse {
response: string;
}
type TCommandCallback = (
command: string,
args: string[],
prefix: string,
user: IUser
) => Promise<string | void>;
type TCommandCallback<User> = (props: {
id: string;
command: string;
args: string[];
prefix: string;
part: IPart;
user: User;
}) => Promise<string | void>;
interface CountComponent {
count: number;
}
interface IFish extends JsonValue {
id: string;
name: string;
size: string;
}
interface IPokemon extends JsonValue {
id: number;
name: {
english: string;
japanese: string;
chinese: string;
french: string;
};
type: string[];
base: {
HP: number;
Attack: number;
Defense: number;
"Sp. Attack": number;
"Sp. Defense": number;
Speed: number;
};
}
type TFishSack = JsonArray & IFish[];
type TPokemonSack = JsonArray & IPokemon[];
interface IInventory {
id: number;
balance: number;
fishSack: TFishSack;
pokemon: TPokemonSack;
}
interface IBack<T extends string | unknown> extends Record<string, unknown> {
m: T;
}
interface Backs extends Record<string, IBack<unknown>> {
color: {
m: "color";
};
}

View File

@ -0,0 +1,16 @@
import {
createInventory,
deleteInventory,
getInventory
} from "@server/data/inventory";
import { test, expect } from "bun:test";
test("Inventory can be created, read, updated and deleted", async () => {
const inventory = await createInventory({});
expect(inventory.id).toBeNumber();
await deleteInventory(inventory.id);
const badInventory = await getInventory(inventory.id);
expect(badInventory).toBeNull();
});

View File

@ -1,4 +1,9 @@
import { checkToken, createToken, deleteToken } from "@server/data/token";
import {
checkToken,
createToken,
deleteToken,
tokenToID
} from "@server/data/token";
import { test, expect } from "bun:test";
test("Token can be created and deleted", async () => {
@ -26,3 +31,14 @@ test("Token can be invalidated", async () => {
const checked = await checkToken(token);
expect(checked).toBeFalsy();
});
test("Token can be digested into ID", async () => {
const token = await createToken();
expect(token).toBeString();
const id = tokenToID(token);
expect(id).toBeString();
expect(id).toHaveLength(24);
await deleteToken(token);
});

View File

@ -0,0 +1,34 @@
import type { User } from "@prisma/client";
import { createInventory } from "@server/data/inventory";
import { createUser, getUser, updateUser, deleteUser } from "@server/data/user";
import { test, expect } from "bun:test";
test("User can be created, read, updated, and deleted", async () => {
const inventory = await createInventory({});
const data = {
id: "test",
name: "test",
color: "#8d3f50",
inventoryId: inventory.id
};
await createUser(data);
const user = await getUser(data.id);
expect(user).toBeDefined();
expect(user?.id).toBeString();
expect(user?.name).toBeString();
await updateUser({
id: data.id,
name: "hi"
});
const user2 = await getUser(data.id);
expect(user2).toBeDefined();
expect(user2?.name).toBeString();
await deleteUser((user as User).id);
});