services
This commit is contained in:
parent
b1d7477889
commit
ba0c944cbb
|
@ -0,0 +1,3 @@
|
|||
DATABASE_URL="postgresql://user:password@localhost/cosmic"
|
||||
MPPNET_TOKEN=""
|
||||
CHATBOX_LICENSE_KEY=""
|
|
@ -0,0 +1,4 @@
|
|||
enableMPP: false
|
||||
enableDiscord: false
|
||||
enableConsole: true
|
||||
enableSwitchChat: true
|
|
@ -0,0 +1 @@
|
|||
ownerOnly: false
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
) {}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export class Prefix {
|
||||
constructor(public id: string, public spaced: boolean) {}
|
||||
}
|
|
@ -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`;
|
||||
}
|
||||
);
|
|
@ -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";
|
||||
}
|
||||
);
|
|
@ -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}`;
|
||||
}
|
||||
}
|
||||
);
|
|
@ -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);
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
import { loadCommands } from "./commands";
|
||||
import { ServiceLoader } from "./services";
|
||||
|
||||
loadCommands();
|
||||
ServiceLoader.loadServices();
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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
|
||||
})
|
||||
);
|
||||
}
|
|
@ -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
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue