Implement user and inventory, back messages, and color changing, add pokedex
This commit is contained in:
parent
11967df2ae
commit
29676fb502
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -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("[]")
|
||||
|
|
|
@ -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.");
|
|
@ -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;
|
||||
})
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
) {}
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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}`;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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.";
|
||||
}
|
||||
);
|
|
@ -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."
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
});
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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";
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
Loading…
Reference in New Issue