This commit is contained in:
Hri7566 2023-10-18 01:05:14 -04:00
parent b1d7477889
commit ba0c944cbb
20 changed files with 426 additions and 7 deletions

3
.env.template Normal file
View File

@ -0,0 +1,3 @@
DATABASE_URL="postgresql://user:password@localhost/cosmic"
MPPNET_TOKEN=""
CHATBOX_LICENSE_KEY=""

BIN
bun.lockb

Binary file not shown.

4
config/services.yml Normal file
View File

@ -0,0 +1,4 @@
enableMPP: false
enableDiscord: false
enableConsole: true
enableSwitchChat: true

1
config/switchchat.yml Normal file
View File

@ -0,0 +1 @@
ownerOnly: false

View File

@ -12,9 +12,12 @@
"@t3-oss/env-core": "^0.7.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",
"yaml": "^2.3.3",
"zod": "^3.22.4"
}
}

20
src/commands/Command.ts Normal file
View File

@ -0,0 +1,20 @@
import { ServiceAgent } from "../services/ServiceAgent";
import { CommandMessage } from "./CommandHandler";
export class Command {
public static getUsage(usage: string, prefix: string) {
return usage.split("{prefix}").join(prefix);
}
constructor(
public id: string,
public aliases: string[],
public description: string,
public usage: string,
public callback: (
msg: CommandMessage,
agent: ServiceAgent<unknown>
) => Promise<string | void> | string | void,
public visible: boolean = true
) {}
}

View File

@ -0,0 +1,17 @@
import { Command } from "./Command";
export class CommandGroup {
public commands = new Array<Command>();
constructor(public id: string, public displayName: string) {}
public addCommand(command: Command) {
this.commands.push(command);
}
public addCommands(commands: Command[]) {
for (const command of commands) {
this.addCommand(command);
}
}
}

View File

@ -0,0 +1,78 @@
import { ServiceAgent } from "../services/ServiceAgent";
import { Command } from "./Command";
import { CommandGroup } from "./CommandGroup";
import { Prefix } from "./Prefix";
export interface CommandMessage<T = unknown> {
m: "command";
a: string;
argv: string[];
argc: number;
originalMessage: T;
}
export class CommandHandler {
public static commandGroups = new Array<CommandGroup>();
public static prefixes = new Array<Prefix>(
{
id: "cosmic",
spaced: true
},
{
id: "*",
spaced: false
}
);
public static addCommandGroup(group: CommandGroup) {
this.commandGroups.push(group);
}
public static async handleCommand(
msg: CommandMessage,
agent: ServiceAgent<unknown>
) {
let usedPrefix: Prefix | undefined;
for (const prefix of this.prefixes) {
if (msg.argv[0].startsWith(prefix.id)) {
usedPrefix = prefix;
if (prefix.spaced) {
msg.argv.splice(0, 1);
}
break;
}
}
if (!usedPrefix) return;
let usedAlias = msg.argv[0];
if (!usedPrefix.spaced)
usedAlias = msg.argv[0].substring(usedPrefix.id.length);
if (!usedAlias) return;
let usedCommand: Command | undefined;
for (const group of this.commandGroups) {
for (const command of group.commands) {
if (command.aliases.includes(usedAlias)) {
usedCommand = command;
break;
}
}
}
if (!usedCommand) return;
try {
const out = usedCommand.callback(msg, agent);
if (out) return out;
} catch (err) {
console.error(err);
return "An error has occurred.";
}
}
}

3
src/commands/Prefix.ts Normal file
View File

@ -0,0 +1,3 @@
export class Prefix {
constructor(public id: string, public spaced: boolean) {}
}

View File

@ -0,0 +1,11 @@
import { Command } from "../../Command";
export const help = new Command(
"about",
["about", "info"],
"get about bozo",
"{prefix}about",
(msg, agent) => {
return `This is a dumb chat bot`;
}
);

View File

@ -0,0 +1,11 @@
import { Command } from "../../Command";
export const help = new Command(
"help",
["help", "h", "commands", "cmds"],
"get help bozo",
"{prefix}help",
(msg, agent) => {
return "test";
}
);

View File

@ -0,0 +1,21 @@
import { Command } from "../../Command";
import { evaluate } from "mathjs";
export const math = new Command(
"math",
["math"],
"math bozo",
"{prefix}math",
(msg, agent) => {
try {
const argcat = msg.argv.slice(1, msg.argv.length).join(" ");
console.log(argcat);
const answer = evaluate(argcat);
return `Answer: ${answer}`;
} catch (err) {
console.error(err);
return `Invalid expression: ${err}`;
}
}
);

View File

14
src/commands/index.ts Normal file
View File

@ -0,0 +1,14 @@
import { CommandGroup } from "./CommandGroup";
import { CommandHandler } from "./CommandHandler";
import { help } from "./commands/general/help";
import { math } from "./commands/utility/math";
export function loadCommands() {
const general = new CommandGroup("general", "General");
general.addCommands([help]);
CommandHandler.addCommandGroup(general);
const utility = new CommandGroup("utility", "Utility");
utility.addCommands([math]);
CommandHandler.addCommandGroup(utility);
}

View File

@ -1,3 +1,5 @@
import { loadCommands } from "./commands";
import { ServiceLoader } from "./services";
loadCommands();
ServiceLoader.loadServices();

View File

@ -0,0 +1,65 @@
import { CommandHandler, CommandMessage } from "../../commands/CommandHandler";
import { loadConfig } from "../../util/config";
import { ServiceAgent } from "../ServiceAgent";
import readline from "readline";
const config = loadConfig("config/switchchat.yml", {
ownerOnly: false
});
export class SwitchChatAgent extends ServiceAgent<readline.ReadLine> {
public desiredUser = {
name: "🟇 𝙎𝙪𝙥𝙚𝙧 Cosmic",
color: "#1d0054"
};
constructor(token: string) {
const cl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
super(cl);
}
public started = false;
public start() {
if (this.started) return;
this.started = true;
this.client.setPrompt(">");
this.client.prompt(true);
}
public stop() {
if (!this.started) return;
this.started = false;
this.client.setPrompt("");
}
protected bindEventListeners(): void {
super.bindEventListeners();
this.client.on("command", async cmd => {
if (config.ownerOnly && !cmd.ownerOnly) return;
console.log(
`${cmd.user.displayName}: ${cmd.ownerOnly ? "^" : "\\"}${
cmd.command
} ${cmd.args.join(" ")}`
);
const message: CommandMessage = {
m: "command",
a: `${cmd.command} ${cmd.args.join(" ")}`,
argc: cmd.args.length + 1,
argv: [cmd.command, ...cmd.args],
originalMessage: cmd
};
const out = await CommandHandler.handleCommand(message, this);
console.log(out);
if (out) this.client.tell(cmd.user.name, out);
});
}
}

View File

@ -2,16 +2,47 @@ import EventEmitter from "events";
import { MPPAgent } from "./mpp";
import env from "../util/env";
import { ServiceAgent } from "./ServiceAgent";
import { loadConfig } from "../util/config";
import { z } from "zod";
import { SwitchChatAgent } from "./switchchat";
/**
* Services are anything (any platforms or environments) that the bot will directly communicate to users with
*/
const config = loadConfig("config/services.yml", {
enableConsole: true,
enableMPP: false,
enableDiscord: false,
enableSwitchChat: false
});
export class ServiceLoader {
public static agents = new Array<ServiceAgent<unknown>>();
public static loadServices() {
const testAgent = new MPPAgent(
"wss://mppclone.com:8443",
env.MPPNET_TOKEN
);
public static addAgent(agent: ServiceAgent<unknown>) {
this.agents.push(agent);
}
testAgent.start();
public static loadServices() {
if (config.enableMPP) {
// TODO Implement URI and channel configuration
const testAgent = new MPPAgent(
"wss://mppclone.com:8443",
env.MPPNET_TOKEN
);
testAgent.start();
this.addAgent(testAgent);
}
if (config.enableSwitchChat) {
const switchChatAgent = new SwitchChatAgent(
env.CHATBOX_LICENSE_KEY
);
switchChatAgent.start();
this.addAgent(switchChatAgent);
}
}
}

View File

@ -0,0 +1,61 @@
import { CommandHandler, CommandMessage } from "../../commands/CommandHandler";
import { loadConfig } from "../../util/config";
import { ServiceAgent } from "../ServiceAgent";
import { Client } from "switchchat";
const config = loadConfig("config/switchchat.yml", {
ownerOnly: false
});
export class SwitchChatAgent extends ServiceAgent<Client> {
public desiredUser = {
name: "🟇 𝙎𝙪𝙥𝙚𝙧 Cosmic",
color: "#1d0054"
};
constructor(token: string) {
const cl = new Client(token);
super(cl);
this.client.defaultName = this.desiredUser.name;
this.client.defaultFormattingMode = "markdown";
}
public start() {
this.client.connect();
}
public stop() {
this.client.close();
}
protected bindEventListeners(): void {
super.bindEventListeners();
this.client.on("command", async cmd => {
if (config.ownerOnly && !cmd.ownerOnly) return;
console.log(
`${cmd.user.displayName}: ${cmd.ownerOnly ? "^" : "\\"}${
cmd.command
} ${cmd.args.join(" ")}`
);
const message: CommandMessage = {
m: "command",
a: `${cmd.command} ${cmd.args.join(" ")}`,
argc: cmd.args.length + 1,
argv: [cmd.command, ...cmd.args],
originalMessage: cmd
};
const out = await CommandHandler.handleCommand(message, this);
console.log(out);
if (out) await this.client.tell(cmd.user.name, out);
});
this.client.on("rawraw", data => {
console.log(data);
});
}
}

73
src/util/config.ts Normal file
View File

@ -0,0 +1,73 @@
import { existsSync, readFileSync, writeFileSync } from "fs";
import { parse, stringify } from "yaml";
import { z } from "zod";
/**
* Load a YAML config file and set default values if config path is nonexistent
*
* Usage:
* ```ts
* const config = loadConfig("config/services.yml", {
* enableMPP: false
* });
* ```
* @param configPath Path to load config from
* @param defaultConfig Config to use if none is present (will save to path if used)
* @returns Parsed YAML config
*/
export function loadConfig<T>(configPath: string, defaultConfig: T): T {
// Config exists?
if (existsSync(configPath)) {
// Load config
const data = readFileSync(configPath);
const config = parse(data.toString());
const defRecord = defaultConfig as Record<string, any>;
let changed = false;
function mix(
obj: Record<string, unknown>,
obj2: Record<string, unknown>
) {
for (const key of Object.keys(obj2)) {
if (typeof obj[key] == "undefined") {
obj[key] = obj2[key];
changed = true;
}
if (typeof obj[key] == "object" && !Array.isArray(obj[key])) {
mix(
obj[key] as Record<string, unknown>,
obj2[key] as Record<string, unknown>
);
}
}
}
// Apply any missing default values
mix(config, defRecord);
// Save config if modified
if (changed) writeConfig(configPath, config);
return config as T;
} else {
// Write default config to disk and use that
writeConfig(configPath, defaultConfig);
return defaultConfig as T;
}
}
/**
* Write a YAML config to disk
* @param configPath
* @param config
*/
export function writeConfig<T>(configPath: string, config: T) {
writeFileSync(
configPath,
stringify(config, {
indent: 4
})
);
}

View File

@ -7,7 +7,8 @@ configDotenv();
export const env = createEnv({
isServer: true,
server: {
MPPNET_TOKEN: z.string()
MPPNET_TOKEN: z.string(),
CHATBOX_LICENSE_KEY: z.string()
},
runtimeEnv: process.env
});