Compare commits

...

9 Commits

29 changed files with 423 additions and 71 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -4,4 +4,3 @@ desiredUser:
agents:
wss://mppclone.com:
- id: "✧𝓓𝓔𝓥 𝓡𝓸𝓸𝓶✧"
- id: "test/awkward"

View File

@ -1,5 +1,7 @@
prefixes:
- id: "*"
- id: "**"
spaced: false
- id: cosmic
- id: cdebug
spaced: true
- id: d
spaced: false

View File

@ -1,5 +1,5 @@
debug: false
enableConsole: false
debug: true
enableConsole: true
enableMPP: true
enableDiscord: true
enableDiscord: false
enableSwitchChat: false

View File

@ -1,27 +1,27 @@
{
"name": "supercosmic",
"version": "0.1.0",
"module": "src/index.ts",
"type": "module",
"devDependencies": {
"bun-types": "latest"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"dependencies": {
"@prisma/client": "^5.5.2",
"@t3-oss/env-core": "^0.7.1",
"discord.js": "^14.14.1",
"dotenv": "^16.3.1",
"hyperimport": "^0.1.0",
"mathjs": "^11.11.2",
"mpp-client-net": "^1.1.3",
"mpp-client-xt": "^1.3.1",
"prisma": "^5.4.2",
"switchchat": "^3.2.1",
"typescript": "^5.3.2",
"yaml": "^2.3.3",
"zod": "^3.22.4"
}
"name": "supercosmic",
"version": "0.1.0",
"module": "src/index.ts",
"type": "module",
"devDependencies": {
"bun-types": "^1.0.16"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"dependencies": {
"@prisma/client": "^5.5.2",
"@t3-oss/env-core": "^0.7.1",
"discord.js": "^14.14.1",
"dotenv": "^16.3.1",
"hyperimport": "^0.1.0",
"mathjs": "^11.11.2",
"mpp-client-net": "^1.1.3",
"mpp-client-xt": "^1.3.1",
"prisma": "^5.4.2",
"switchchat": "^3.2.1",
"typescript": "^5.3.2",
"yaml": "^2.3.3",
"zod": "^3.22.4"
}
}

View File

@ -34,3 +34,8 @@ enum Role {
ADMINISTRATOR
OWNER
}
model KeyValueStore {
id Int @id @unique @default(1)
data Json @default("{}")
}

View File

@ -46,7 +46,7 @@ export type BaseCommandMessage<T = unknown> = Omit<
export class CommandHandler {
public static commandGroups = new Array<CommandGroup>();
public static prefixes = new Array<Prefix>();
public static prefixes = new Set<Prefix>();
public static logger = new Logger("Command Handler");
@ -172,5 +172,12 @@ export class CommandHandler {
// Add prefixes
for (const prefix of prefixConfig.prefixes) {
CommandHandler.prefixes.push(prefix);
CommandHandler.prefixes.add(prefix);
}
// Store commands for hot reload
declare global {
var commandHandler: any;
}
globalThis.commandHandler ??= CommandHandler;

View File

@ -0,0 +1,23 @@
import { KekklefruitTree } from "../../../economy/kekkle";
import { Command } from "../../Command";
export const grow = new Command(
"grow",
["grow"],
"grow bozo's kekklefruit (forcefully)",
"grow [number]",
async msg => {
let num: number;
if (msg.argv[1]) {
num = parseInt(msg.argv[1]);
if (isNaN(num)) return `Need number bozo`;
} else {
num = 1;
}
await KekklefruitTree.growFruit(num);
return `You grew ${num} fruit.`;
}
);

View File

@ -7,8 +7,9 @@ export const inventory = new Command(
"get bozo's inventory",
"inventory",
msg => {
const items = msg.inventory.items as unknown as Item[];
const list = items
const items = msg.inventory.items as string;
console.log(items);
const list = JSON.parse(items)
.map(
i =>
`${i.name}${

View File

@ -0,0 +1,20 @@
import { JsonArray, JsonValue } from "@prisma/client/runtime/library";
import { KekklefruitTree } from "../../../economy/kekkle";
import { Command } from "../../Command";
import { addItem, updateInventory } from "../../../data/inventory";
export const pick = new Command(
"pick",
["pick"],
"bozo will pick fruit off the kekklefruit tree",
"pick",
async msg => {
const fruit = await KekklefruitTree.pickFruit();
if (!fruit)
return `There are not enough fruit on the kekklefruit tree.`;
addItem(msg.p._id, fruit);
return `(insert random boring message about ${fruit.name} here)`;
}
);

View File

@ -0,0 +1,12 @@
import { KekklefruitTree } from "../../../economy/kekkle";
import { Command } from "../../Command";
export const tree = new Command(
"tree",
["tree"],
"bozo will get the amount of fruit on the kekklefruit tree",
"tree",
async msg => {
return `There are ${KekklefruitTree.getFruitCount()} kekklefruit on the tree.`;
}
);

View File

@ -0,0 +1,14 @@
import { deleteInventory } from "../../../data/inventory";
import { Command } from "../../Command";
export const delinv = new Command(
"delinv",
["delinv"],
"delete a bozo's inventory",
"delinv [id]",
async (msg) => {
let userId = msg.argv[1] ? msg.argv[1] : msg.p._id;
await deleteInventory(userId);
return `Inventory of \`${userId}\` deleted.`
}
);

View File

@ -6,7 +6,7 @@ export const id = new Command(
"get your id bozo",
"id",
(msg, agent) => {
if (agent.platform == "mpp") {
if (agent.platform === "mpp") {
return `ID: \`${
(msg.originalMessage as any).p._id
}\` Cosmic ID: \`${msg.p._id}\``;

View File

@ -1,5 +1,4 @@
import { CommandGroup } from "./CommandGroup";
import { CommandHandler } from "./CommandHandler";
import { about } from "./commands/general/about";
import { help } from "./commands/general/help";
import { id } from "./commands/utility/id";
@ -16,20 +15,24 @@ import { uptime } from "./commands/utility/uptime";
import { balance } from "./commands/economy/balance";
import { permissions } from "./commands/utility/permissions";
import { branch } from "./commands/utility/branch";
import { tree } from "./commands/economy/tree";
import { pick } from "./commands/economy/pick";
import { grow } from "./commands/economy/grow";
import { delinv } from "./commands/utility/delinv";
export function loadCommands() {
// cringe
const general = new CommandGroup("general", "⭐ General");
general.addCommands([help, about]);
CommandHandler.addCommandGroup(general);
globalThis.commandHandler.addCommandGroup(general);
const economy = new CommandGroup("economy", "💸 Economy");
economy.addCommands([inventory, balance]);
CommandHandler.addCommandGroup(economy);
economy.addCommands([inventory, balance, tree, pick, grow]);
globalThis.commandHandler.addCommandGroup(economy);
const fun = new CommandGroup("fun", "✨ Fun");
fun.addCommands([magic8ball]);
CommandHandler.addCommandGroup(fun);
globalThis.commandHandler.addCommandGroup(fun);
const utility = new CommandGroup("utility", "🔨 Utility");
utility.addCommands([
@ -43,7 +46,10 @@ export function loadCommands() {
ic,
uptime,
permissions,
branch
branch,
delinv
]);
CommandHandler.addCommandGroup(utility);
globalThis.commandHandler.addCommandGroup(utility);
}
export { CommandGroup };

View File

@ -1,5 +1,7 @@
import { Inventory } from "@prisma/client";
import { prisma } from "./prisma";
import { JsonArray } from "@prisma/client/runtime/library";
import { Item, StackableItem } from "../economy/Item";
export async function createInventory(data: Omit<Inventory, "id">) {
await prisma.inventory.create({
@ -14,13 +16,51 @@ export async function readInventory(userId: Inventory["userId"]) {
return await prisma.inventory.findUnique({ where: { userId: userId } });
}
export function collapseInventory(inventoryData: Item[]) {
for (let i = 0; i < inventoryData.length; i++) {
if (i <= 0) continue;
if (inventoryData[i].id === inventoryData[i - 1].id) {
if (
typeof (inventoryData[i - 1] as StackableItem).count ===
"number" &&
typeof (inventoryData[i] as StackableItem).count === "number"
) {
(inventoryData[i - 1] as StackableItem).count += (
inventoryData[i] as StackableItem
).count;
inventoryData.splice(i, 1);
i--;
}
}
}
}
export async function updateInventory(data: Omit<Inventory, "id">) {
collapseInventory(data.items as unknown as Item[]);
return await prisma.inventory.update({
where: { userId: data.userId },
data: {}
data: {
balance: data.balance,
items: JSON.stringify(data.items)
}
});
}
export async function deleteInventory(userId: Inventory["userId"]) {
return await prisma.inventory.delete({ where: { userId } });
}
export async function addItem<T extends Item>(userId: Inventory["userId"], item: T) {
let inventory = await readInventory(userId);
if (!inventory) return false;
console.log(inventory.items);
inventory.items = JSON.stringify(JSON.parse(inventory.items as string).push(item));
collapseInventory(inventory.items as unknown as Item[]);
await updateInventory(inventory);
return true;
}

View File

@ -1,3 +1,51 @@
import { PrismaClient } from "@prisma/client";
import { JsonObject } from "@prisma/client/runtime/library";
export const prisma = new PrismaClient();
declare global {
var prisma: PrismaClient;
}
globalThis.prisma ??= new PrismaClient();
export const prisma = globalThis.prisma;
export async function set(key: string, value: any) {
const store = await globalThis.prisma.keyValueStore.findUnique({
where: { id: 1 }
});
if (!store) {
// throw new Error("Unable to access key-value store.");
await prisma.keyValueStore.create({
data: {}
});
return set(key, value);
}
const data = store.data as JsonObject;
data[key] = value;
await globalThis.prisma.keyValueStore.update({
where: { id: 1 },
data: { data: data }
});
return;
}
export async function get<T = unknown>(key: string) {
const store = await globalThis.prisma.keyValueStore.findUnique({
where: { id: 1 }
});
if (!store) {
// throw new Error("Unable to access key-value store.");
await globalThis.prisma.keyValueStore.create({
data: {}
});
return get(key);
}
const data = store.data as JsonObject;
return data[key] as T;
}

View File

@ -6,3 +6,22 @@ export interface Item {
export interface StackableItem extends Item {
count: number;
}
export interface ConsumableItem extends Item {
consumable: true;
}
export interface FoodItem extends ConsumableItem {
edible: true;
}
export interface CakeItem extends FoodItem {
emoji: string;
icing: string;
filling: string;
}
export interface ShopItem extends Item {
buyValue: number;
sellValue: number;
}

15
src/economy/items.ts Normal file
View File

@ -0,0 +1,15 @@
import { Item } from "./Item";
const items = new Map<string, Item>();
export function getItem(key: string) {
return items.get(key);
}
export function setItem(key: string, item: Item) {
return items.set(key, item);
}
export function deleteItem(key: string) {
return items.delete(key);
}

55
src/economy/kekkle.ts Normal file
View File

@ -0,0 +1,55 @@
import { get, set } from "../data/prisma";
import { Logger } from "../util/Logger";
import { FoodItem } from "./Item";
export class KekklefruitTree {
protected static fruit: number = 0;
public static logger = new Logger("Kekklefruit Tree");
public static async saveFruit() {
return set("kekklefruit-tree", this.fruit);
}
public static async loadFruit() {
let fruit = await get<number>("kekklefruit-tree");
let save = false;
if (!fruit) {
fruit = 0;
save = true;
}
this.fruit = fruit;
if (save) this.saveFruit();
}
public static async growFruit(amount: number = 1) {
this.fruit += amount;
await this.saveFruit();
}
public static getFruitCount() {
return this.fruit;
}
public static async pickFruit() {
if (this.fruit > 0) {
this.fruit--;
await this.saveFruit();
return this.randomFruit();
} else {
return undefined;
}
}
public static randomFruit() {
return {
id: "kekklefruit",
name: "Kekklefruit",
consumable: true,
edible: true
} as FoodItem;
}
}
await KekklefruitTree.loadFruit();

View File

@ -1,12 +1,54 @@
import { loadCommands } from "./commands";
import { loadCommands, type CommandGroup } from "./commands";
import { CommandHandler } from "./commands/CommandHandler";
import { loadRoleConfig } from "./permissions";
import { ServiceLoader } from "./services";
import { ConsoleAgent } from "./services/console";
import { printStartupASCII } from "./util/ascii";
printStartupASCII();
loadRoleConfig();
loadCommands();
ServiceLoader.loadServices();
// Hot reload persistence
declare global {
var loaded: boolean;
var serviceLoader: any;
}
globalThis.loaded ??= false;
globalThis.serviceLoader ??= ServiceLoader;
function load() {
printStartupASCII();
loadRoleConfig();
loadCommands();
globalThis.serviceLoader.loadServices();
globalThis.loaded = true;
}
function reload() {
console.log("Reloading...");
// Reload commands
globalThis.commandHandler.commandGroups = new Array<CommandGroup>();
loadCommands();
// Reload services
globalThis.serviceLoader.unloadServices();
globalThis.serviceLoader.loadServices();
// Set console prompt
globalThis.serviceLoader.agents.forEach(agent => {
if (agent.platform === "console")
(agent as ConsoleAgent).client.prompt();
});
}
// Check for hot reload
if (!globalThis.loaded) {
load();
} else {
console.clear();
console.log("Hot reload triggered");
reload();
}
export function scopedEval(code: string) {
return eval(code);

View File

@ -23,9 +23,9 @@ export function handlePermission(node1: string, node2: string) {
// Check nodes in order
for (let i = 0; i < hierarchy1.length; i++) {
if (hierarchy1[i] == hierarchy2[i]) {
if (hierarchy1[i] === hierarchy2[i]) {
// Last node?
if (i == hierarchy1.length - 1 || i == hierarchy2.length - 1) {
if (i === hierarchy1.length - 1 || i === hierarchy2.length - 1) {
return true;
} else {
continue;
@ -33,8 +33,8 @@ export function handlePermission(node1: string, node2: string) {
}
// Wildcard?
if (hierarchy1[i] == "*") return true;
if (hierarchy2[i] == "*") return true;
if (hierarchy1[i] === "*") return true;
if (hierarchy2[i] === "*") return true;
return false;
}
@ -66,7 +66,12 @@ export function hasPermission(role: Role, permission: string) {
return false;
}
export const roles = new Map<Role, TRole>();
declare global {
var roles: Map<Role, TRole>;
}
globalThis.roles ??= new Map<Role, TRole>();
export const roles = globalThis.roles;
export type TRole = {
displayName: string;

View File

@ -22,7 +22,7 @@ export interface ChatMessage<T = unknown> {
function onChildMessage(msg: ChatMessage) {
const consoleAgent = ServiceLoader.agents.find(
ag => ag.platform == "console"
ag => ag.platform === "console"
) as ConsoleAgent | undefined;
if (!consoleAgent) return;
@ -34,7 +34,7 @@ function onChildMessage(msg: ChatMessage) {
function onConsoleMessage(text: string) {
const consoleAgent = ServiceLoader.agents.find(
ag => ag.platform == "console"
ag => ag.platform === "console"
) as ConsoleAgent | undefined;
if (!consoleAgent) return;
@ -77,7 +77,7 @@ export class MicroHandler {
for (let i in ServiceLoader.agents) {
const agent2 = ServiceLoader.agents[i];
if (agent2.platform == "mpp") {
if (agent2.platform === "mpp") {
agent.emit(
"log",
`${i} - ${agent2.platform} - ${
@ -101,7 +101,7 @@ export class MicroHandler {
let walkie = agent as ConsoleAgent;
let talky = ServiceLoader.agents[index];
if (index == ServiceLoader.agents.indexOf(walkie))
if (index === ServiceLoader.agents.indexOf(walkie))
return "Why would you want to chat with yourself?";
// Remove old listeners

View File

@ -37,7 +37,7 @@ export class ConsoleAgent extends ServiceAgent<readline.ReadLine> {
public stop() {
if (!this.started) return;
this.started = false;
this.client.setPrompt("");
this.client.close();
}
protected bindEventListeners(): void {
@ -65,7 +65,10 @@ export class ConsoleAgent extends ServiceAgent<readline.ReadLine> {
if (text.startsWith("/")) {
out = await MicroHandler.handleMicroCommand(message, this);
} else {
out = await CommandHandler.handleCommand(message, this);
out = await globalThis.commandHandler.handleCommand(
message,
this
);
}
if (out) {

View File

@ -56,7 +56,7 @@ export class DiscordAgent extends ServiceAgent<Discord.Client> {
let args = msg.content.split(" ");
const str = await CommandHandler.handleCommand(
const str = await globalThis.commandHandler.handleCommand(
{
m: "command",
a: msg.content,
@ -74,7 +74,7 @@ export class DiscordAgent extends ServiceAgent<Discord.Client> {
);
if (str) {
if (typeof str == "string") {
if (typeof str === "string") {
const channel = await this.client.channels.fetch(
msg.channelId
);

View File

@ -50,6 +50,10 @@ export class ServiceLoader {
this.agents.push(agent);
}
public static getAgent(index: number) {
return this.agents[index];
}
public static loadServices() {
if (config.enableMPP) {
for (const uri of Object.keys(mppConfig.agents)) {
@ -96,4 +100,11 @@ export class ServiceLoader {
this.addAgent(consoleAgent);
}
}
public static unloadServices() {
for (const agent of this.agents) {
agent.stop();
this.agents.splice(this.agents.indexOf(agent), 1);
}
}
}

View File

@ -61,7 +61,7 @@ export class MPPAgent extends ServiceAgent<Client> {
let args = msg.a.split(" ");
// Run command and get output
const str = await CommandHandler.handleCommand(
const str = await globalThis.commandHandler.handleCommand(
{
m: "command",
a: msg.a,
@ -80,7 +80,7 @@ export class MPPAgent extends ServiceAgent<Client> {
// Send message in chat
if (str) {
if (typeof str == "string") {
if (typeof str === "string") {
if (str.includes("\n")) {
let sp = str.split("\n");

View File

@ -80,9 +80,9 @@ export class CosmicColor {
let g = (~~this.g || 0).toString(16);
let b = (~~this.b || 0).toString(16);
if (r.length == 1) r = "0" + r;
if (g.length == 1) g = "0" + g;
if (b.length == 1) b = "0" + b;
if (r.length === 1) r = "0" + r;
if (g.length === 1) g = "0" + g;
if (b.length === 1) b = "0" + b;
return "#" + r + g + b;
}

View File

@ -16,6 +16,7 @@ import { parse as parsePath } from "path/posix";
* @returns Parsed YAML config
*/
export function loadConfig<T>(configPath: string, defaultConfig: T): T {
console.time(`Loading config ${configPath}`);
// Config exists?
if (existsSync(configPath)) {
// Load config
@ -30,12 +31,12 @@ export function loadConfig<T>(configPath: string, defaultConfig: T): T {
obj2: Record<string, unknown>
) {
for (const key of Object.keys(obj2)) {
if (typeof obj[key] == "undefined") {
if (typeof obj[key] === "undefined") {
obj[key] = obj2[key];
changed = true;
}
if (typeof obj[key] == "object" && !Array.isArray(obj[key])) {
if (typeof obj[key] === "object" && !Array.isArray(obj[key])) {
mix(
obj[key] as Record<string, unknown>,
obj2[key] as Record<string, unknown>
@ -45,7 +46,7 @@ export function loadConfig<T>(configPath: string, defaultConfig: T): T {
}
// Apply any missing default values
mix(config, defRecord);
// mix(config, defRecord);
// Save config if modified
if (changed) writeConfig(configPath, config);
@ -54,6 +55,7 @@ export function loadConfig<T>(configPath: string, defaultConfig: T): T {
} else {
// Write default config to disk and use that
writeConfig(configPath, defaultConfig);
return defaultConfig as T;
}
}

View File

@ -0,0 +1,23 @@
import { expect, test } from "bun:test";
import { collapseInventory } from "../../src/data/inventory";
import { StackableItem } from "../../src/economy/Item";
test("Collapse inventory", () => {
let sampleData: StackableItem[] = [
{
id: "test_item",
name: "Test Item",
count: 10
},
{
id: "test_item",
name: "Test Item",
count: 15
}
];
collapseInventory(sampleData);
expect(sampleData[0].count).toBe(25);
expect(sampleData[1]).toBe(undefined);
console.log(sampleData);
});