Add talkomatic

This commit is contained in:
Hri7566 2024-10-12 01:21:38 -04:00
parent 39d95665d4
commit 2d0fb07da2
19 changed files with 18028 additions and 157 deletions

View File

@ -6,3 +6,6 @@ DISCORD_TOKEN=
DISCORD_FISHING_TOKEN= DISCORD_FISHING_TOKEN=
CLI_FISHING_TOKEN= CLI_FISHING_TOKEN=
TALKOMATIC_SID=
TALKOMATIC_FISHING_TOKEN=

17557
build/index.js Normal file

File diff suppressed because it is too large Load Diff

BIN
bun.lockb

Binary file not shown.

View File

@ -0,0 +1,2 @@
- channel:
id: "369310"

View File

@ -1,17 +1,19 @@
{ {
"name": "fishing-api", "name": "fishing-api",
"module": "src/api/index.ts", "module": "src/api/index.ts",
"type": "module",
"scripts": { "scripts": {
"start": "bun .", "start": "bun .",
"start-bot": "bun src/mpp/index.ts", "start-bot": "bun src/mpp/index.ts",
"start-discord": "bun src/discord/index.ts" "start-discord": "bun src/discord/index.ts",
"build-talko": "bunx tsc --build tsconfig.talko.json"
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "^1.1.9" "@types/bun": "^1.1.9",
"@types/dotenv": "^8.2.0",
"esbuild": "^0.24.0"
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "^5.3.3" "typescript": "^5.6.3"
}, },
"dependencies": { "dependencies": {
"@prisma/client": "^5.19.1", "@prisma/client": "^5.19.1",
@ -20,8 +22,10 @@
"@types/node": "^20.16.5", "@types/node": "^20.16.5",
"cli-markdown": "^3.4.0", "cli-markdown": "^3.4.0",
"discord.js": "^14.16.2", "discord.js": "^14.16.2",
"dotenv": "^16.4.5",
"mpp-client-net": "^1.2.3", "mpp-client-net": "^1.2.3",
"prisma": "^5.19.1", "prisma": "^5.19.1",
"socket.io-client": "^4.8.0",
"trpc-bun-adapter": "^1.1.2", "trpc-bun-adapter": "^1.1.2",
"yaml": "^2.5.1", "yaml": "^2.5.1",
"zod": "^3.23.8" "zod": "^3.23.8"

23
scripts/build-talko.ts Normal file
View File

@ -0,0 +1,23 @@
import esbuild from "esbuild";
import fs from "fs";
try {
console.log("Making build directory...");
fs.mkdirSync("./build");
} catch (err) {
console.log("Directory already exists");
}
console.log("Creating script bundle...");
const result = await esbuild.build({
entryPoints: ["src/talkomatic/index.ts"],
bundle: true,
target: "node22.9",
platform: "node",
format: "cjs",
tsconfig: "./tsconfig.talko.json",
outdir: "./build/"
});
// console.log(result);
console.log("Done!");

View File

@ -7,10 +7,10 @@ export const reel = new Command(
"Reel in and stop fishing", "Reel in and stop fishing",
"reel", "reel",
"command.fishing.reel", "command.fishing.reel",
async ({ id, command, args, prefix, part, user }) => { async ({ id, channel, command, args, prefix, part, user, isDM }) => {
const fishing = getFishing(id, part.id); const fishing = getFishing(id, part.id);
if (fishing) { if (fishing) {
stopFishing(id, part.id); stopFishing(id, part.id, channel, isDM);
return `Our friend ${part.name} reel his/her lure back inside, temporarily decreasing his/her chances of catching a fish by 100%.`; return `Our friend ${part.name} reel his/her lure back inside, temporarily decreasing his/her chances of catching a fish by 100%.`;
} else { } else {
return `Friend ${part.name}: You haven't ${prefix}casted it.`; return `Friend ${part.name}: You haven't ${prefix}casted it.`;

View File

@ -26,6 +26,7 @@ import { chance } from "./util/chance";
import { info } from "./general/info"; import { info } from "./general/info";
import { burger } from "./util/burger"; import { burger } from "./util/burger";
import { daily } from "./pokemon/daily"; import { daily } from "./pokemon/daily";
import { give } from "./inventory/give";
// import { give } from "./inventory/give"; // import { give } from "./inventory/give";
interface ICommandGroup { interface ICommandGroup {
@ -55,7 +56,7 @@ commandGroups.push(fishingGroup);
const inventoryGroup: ICommandGroup = { const inventoryGroup: ICommandGroup = {
id: "inventory", id: "inventory",
displayName: "Inventory", displayName: "Inventory",
commands: [inventory, sack, pokemon, take, eat, yeet, burger /* give */] commands: [inventory, sack, pokemon, take, eat, yeet, burger, give]
}; };
commandGroups.push(inventoryGroup); commandGroups.push(inventoryGroup);

View File

@ -1,7 +1,7 @@
import Command from "@server/commands/Command"; import Command from "@server/commands/Command";
import { logger } from "@server/commands/handler"; import { logger } from "@server/commands/handler";
import { getInventory, updateInventory } from "@server/data/inventory"; import { getInventory, updateInventory } from "@server/data/inventory";
import { removeItem } from "@server/items"; import { findItemByNameFuzzy, removeItem } from "@server/items";
import { itemBehaviorMap, runBehavior } from "@server/items/behavior"; import { itemBehaviorMap, runBehavior } from "@server/items/behavior";
export const eat = new Command( export const eat = new Command(
@ -22,27 +22,9 @@ export const eat = new Command(
let i = 0; let i = 0;
let shouldRemove = false; let shouldRemove = false;
for (const item of inventory.items as unknown as IItem[]) { foundObject =
if (!item.name.toLowerCase().includes(eating.toLowerCase())) { findItemByNameFuzzy(inventory.items, eating) ||
i++; findItemByNameFuzzy(inventory.fishSack, eating);
continue;
}
foundObject = item;
break;
}
i = 0;
for (const fish of inventory.fishSack as TFishSack) {
if (!fish.name.toLowerCase().includes(eating.toLowerCase())) {
i++;
continue;
}
foundObject = fish;
break;
}
if (!foundObject) return `You don't have "${eating}" to eat.`; if (!foundObject) return `You don't have "${eating}" to eat.`;
@ -73,25 +55,17 @@ export const eat = new Command(
} }
if (foundObject.id == "sand") { if (foundObject.id == "sand") {
if (res) { if (res && res.and) {
if (res.and) {
return `Our friend ${part.name} ate of his/her ${foundObject.name} ${res.and}`; return `Our friend ${part.name} ate of his/her ${foundObject.name} ${res.and}`;
} else { } else {
return `Our friend ${part.name} ate of his/her ${foundObject.name}.`; return `Our friend ${part.name} ate of his/her ${foundObject.name}.`;
} }
} else { } else {
return `Our friend ${part.name} ate of his/her ${foundObject.name}.`; if (res && res.and) {
}
} else {
if (res) {
if (res.and) {
return `Our friend ${part.name} ate his/her ${foundObject.name} ${res.and}`; return `Our friend ${part.name} ate his/her ${foundObject.name} ${res.and}`;
} else { } else {
return `Our friend ${part.name} ate his/her ${foundObject.name}.`; return `Our friend ${part.name} ate his/her ${foundObject.name}.`;
} }
} else {
return `Our friend ${part.name} ate his/her ${foundObject.name}.`;
}
} }
} }
); );

View File

@ -2,7 +2,7 @@ import type { User } from "@prisma/client";
import Command from "@server/commands/Command"; import Command from "@server/commands/Command";
import { getInventory, updateInventory } from "@server/data/inventory"; import { getInventory, updateInventory } from "@server/data/inventory";
import prisma from "@server/data/prisma"; import prisma from "@server/data/prisma";
import { addItem } from "@server/items"; import { addItem, findItemByNameFuzzy, removeItem } from "@server/items";
export const give = new Command( export const give = new Command(
"give", "give",
@ -37,87 +37,28 @@ export const give = new Command(
const argcat = args.slice(1).join(" "); const argcat = args.slice(1).join(" ");
let foundObject: IObject | undefined; let foundObject: IObject | undefined;
let i = 0; foundObject =
findItemByNameFuzzy(inventory.items, argcat) ||
for (const item of inventory.items as unknown as IItem[]) { findItemByNameFuzzy(inventory.fishSack, argcat);
if (!item.name.toLowerCase().includes(argcat.toLowerCase())) {
i++;
continue;
}
foundObject = item;
break;
}
i = 0;
for (const fish of inventory.fishSack as TFishSack) {
if (!fish.name.toLowerCase().includes(argcat.toLowerCase())) {
i++;
continue;
}
foundObject = fish;
break;
}
if (!foundObject) return `You don't have any "${argcat}" to give.`; if (!foundObject) return `You don't have any "${argcat}" to give.`;
let updated = false; let updated = false;
if (foundObject.objtype == "fish") {
if (foundObject.objtype == "item") {
addItem(foundInventory.items as unknown as IItem[], foundObject); addItem(foundInventory.items as unknown as IItem[], foundObject);
updated = true; updated = true;
} else if (foundObject.objtype == "fish") { } else if (foundObject.objtype == "item") {
addItem(foundInventory.items as unknown as IItem[], foundObject); addItem(foundInventory.items as unknown as IItem[], foundObject);
updated = true; updated = true;
} }
let shouldRemove = false;
if (updated) { if (updated) {
await updateInventory(foundInventory); await updateInventory(foundInventory);
if (foundObject.objtype == "fish") { if (foundObject.objtype == "fish") {
i = 0; removeItem(inventory.fishSack, foundObject, 1);
for (const fish of inventory.fishSack as TFishSack) {
if (typeof fish.count !== "undefined") {
if (fish.count > 1) {
shouldRemove = false;
((inventory.fishSack as TFishSack)[i]
.count as number)--;
} else {
shouldRemove = true;
}
} else {
shouldRemove = true;
}
if (shouldRemove)
(inventory.fishSack as TFishSack).splice(i, 1);
break;
}
} else if (foundObject.objtype == "item") { } else if (foundObject.objtype == "item") {
i = 0; removeItem(inventory.items, foundObject, 1);
for (const item of inventory.items as unknown as IItem[]) {
if (typeof item.count == "number") {
if (item.count > 1) {
shouldRemove = false;
((inventory.items as TInventoryItems)[i]
.count as number)--;
} else {
shouldRemove = true;
}
} else {
shouldRemove = true;
}
if (shouldRemove)
(inventory.items as TInventoryItems).splice(i, 1);
break;
}
} }
return `You ${ return `You ${

View File

@ -46,3 +46,20 @@ export function removeItem(arr: IObject[], item: IObject, count = 1) {
return true; return true;
} }
export function findItemByNameFuzzy(arr: IObject[], name: string) {
let foundObject: IObject | undefined;
let i = 0;
for (const item of arr as unknown as IItem[]) {
if (!item.name.toLowerCase().includes(name.toLowerCase())) {
i++;
continue;
}
foundObject = item;
break;
}
return foundObject;
}

View File

@ -0,0 +1,283 @@
import { Logger } from "@util/Logger";
import { io, type Socket } from "socket.io-client";
import { EventEmitter } from "node:events";
import gettRPC from "@util/api/trpc";
require("dotenv").config();
export interface TalkomaticBotConfig {
channel: {
id: string;
};
}
interface TalkomaticParticipant {
id: string;
name: string;
color: string;
typingTimeout: Timer | undefined;
typingFlag: boolean;
}
const ppl: Record<string, TalkomaticParticipant> = {};
export class TalkomaticBot extends EventEmitter {
public client: Socket;
public b = new EventEmitter();
public logger: Logger;
public trpc = gettRPC(process.env.TALKOMATIC_FISHING_TOKEN as string);
public started = false;
public textColor = "#abe3d6";
constructor(public config: TalkomaticBotConfig) {
super();
this.logger = new Logger("Talkomatic - " + config.channel.id);
// this.client = new Client(config.uri, token);
// this.client = io(
// "wss://talkomatic.co/socket.io/?EIO=4&transport=websocket&sid=f_X4Z5LB8lKBlybNAdj8"
// );
// this.logger.debug(process.env.TALKOMATIC_SID);
this.client = io("https://talkomatic.co/", {
extraHeaders: {
Cookie: "connect.sid=" + process.env.TALKOMATIC_SID
},
autoConnect: false
});
}
public start() {
this.logger.debug("Starting");
this.client.connect();
// this.client.io.engine.on("packetCreate", this.logger.debug);
this.bindEventListeners();
this.setChannel(this.config.channel.id);
this.started = true;
}
public stop() {
this.client.disconnect();
this.started = false;
}
public connected = false;
public bindEventListeners() {
this.client.onAny(msg => {
if (this.connected) return;
this.connected = true;
this.logger.info("Connected to server");
});
this.client.on(
"userTyping",
(msg: { userId: string; text: string; color: string }) => {
const p = ppl[msg.userId] || {
name: "<unknown user>",
id: msg.userId,
color: msg.color,
typingFlag: false
};
// p.color = msg.color;
if (p.typingTimeout) clearTimeout(p.typingTimeout);
p.typingTimeout = setTimeout(() => {
p.typingFlag = true;
ppl[msg.userId] = p;
if (msg.text.length <= 0) return;
this.emit("command", msg);
}, 500);
ppl[msg.userId] = p;
}
);
this.client.on(
"udpateRoom",
(msg: {
users: {
id: string;
username: string;
location: string;
is_moderator: boolean;
avatar: string;
}[];
}) => {
if (!Array.isArray(msg.users)) return;
try {
for (const user of msg.users) {
const p = ppl[user.id] || {
name: user.username,
id: user.id,
color: "#abe3d6",
typingFlag: false
};
ppl[user.id] = p;
}
} catch (err) {}
}
);
this.client.on(
"roomUsers",
(msg: {
users: {
id: string;
username: string;
location: string;
is_moderator: boolean;
avatar: string;
}[];
currentUserId: string;
}) => {
if (!Array.isArray(msg.users)) return;
try {
for (const user of msg.users) {
const p = ppl[user.id] || {
name: user.username,
id: user.id,
color: "#abe3d6",
typingFlag: false
};
ppl[user.id] = p;
}
} catch (err) {}
}
);
this.on(
"command",
async (msg: { userId: string; text: string; color: string }) => {
let prefixes: string[];
try {
prefixes = await this.trpc.prefixes.query();
} catch (err) {
this.logger.error(err);
this.logger.warn("Unable to contact server");
return;
}
let usedPrefix: string | undefined = prefixes.find(pr =>
msg.text.startsWith(pr)
);
if (!usedPrefix) return;
const args = msg.text.split(" ");
let part: TalkomaticParticipant = ppl[msg.userId] || {
name: "<unknown user>",
id: msg.userId,
color: msg.color,
typingFlag: false
};
this.logger.info(`${part.name}: ${msg.text}`);
const command = await this.trpc.command.query({
channel: this.config.channel.id,
args: args.slice(1, args.length),
command: args[0].substring(usedPrefix.length),
prefix: usedPrefix,
user: part
});
if (!command) return;
if (command.response)
this.sendChat(command.response, undefined, msg.userId);
}
);
this.client.on(
"userJoined",
(msg: {
id: string;
username: string;
location: string;
avatar: string;
}) => {
const p = ppl[msg.id] || {
name: "<unknown user>",
id: msg.id,
color: "#ffffff",
typingFlag: false
};
ppl[msg.id] = p;
}
);
setInterval(async () => {
try {
const backs = (await this.trpc.backs.query()) as any;
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.textColor = msg.color;
try {
ppl[msg.id].color = msg.color;
} catch (err) {}
});
this.b.on("sendchat", msg => {
// this.logger.debug("sendchat message:", msg);
if (typeof msg.channel === "string") {
if (msg.channel !== this.config.channel) return;
}
this.sendChat(msg.message);
});
}
public sendChat(text: string, reply?: string, id?: string) {
// let lines = text.split("\n");
// for (const line of lines) {
// const splits = line.match(/.{510}|.{1,509}/gi);
// if (!splits) continue;
// for (const split of splits) {
// if (split.length <= 510) {
// const text = `\u034f${split
// .split("\t")
// .join("")
// .split("\r")
// .join("")}`;
const msg = {
roomId: this.config.channel.id,
text,
color: id ? ppl[id].color : "#ffffff"
};
this.client.emit("typing", msg);
// } else {
// this.sendChat(split);
// }
// }
// }
}
public setChannel(roomId: string) {
this.client.emit("joinRoom", { roomId });
}
}

View File

@ -0,0 +1,27 @@
import { loadConfig } from "../../util/config";
import { TalkomaticBot, type TalkomaticBotConfig } from "./TalkomaticBot";
const bots: TalkomaticBot[] = [];
const defaults = loadConfig("config/talkomatic_bots.yml", [
{
channel: {
id: "116955"
}
}
] as TalkomaticBotConfig[]);
export function connectDefaultBots() {
defaults.forEach(conf => {
initBot(conf);
});
}
export function initBot(conf: TalkomaticBotConfig) {
const bot = new TalkomaticBot(conf);
bot.start();
bots.push(bot);
}
export { TalkomaticBot as Bot };
export default TalkomaticBot;

5
src/talkomatic/index.ts Normal file
View File

@ -0,0 +1,5 @@
import { startAutorestart } from "@util/autorestart";
import { connectDefaultBots } from "./bot/index";
connectDefaultBots();
startAutorestart();

View File

@ -1,5 +1,5 @@
import { getHHMMSS } from "./time"; import { getHHMMSS } from "./time";
import type { ReadLine } from "readline"; import type { ReadLine } from "node:readline";
export class Logger { export class Logger {
private static log(...args: any[]) { private static log(...args: any[]) {

View File

@ -1,29 +1,30 @@
import { createTRPCClient, httpBatchLink } from "@trpc/client"; import { createTRPCClient, httpBatchLink } from "@trpc/client";
import type { AppRouter } from "@server/api/trpc"; import type { AppRouter } from "@server/api/trpc";
// const apiToken = process.env.FISHING_TOKEN as string; require("dotenv").config();
export function gettRPC(token: string) { export function gettRPC(token: string) {
return createTRPCClient<AppRouter>({ const fishingURL = process.env.FISHING_API_URL as string;
links: [
/*httpBatchLink({ const firstLink = httpBatchLink({
url: "http://localhost:3000", url: fishingURL,
headers() { headers() {
return { return {
Authorization: token Authorization: token
}; };
} }
}),*/ });
httpBatchLink({
const secondLink = httpBatchLink({
url: "https://fishing.hri7566.info/api", url: "https://fishing.hri7566.info/api",
headers() { headers() {
return { return {
Authorization: token Authorization: token
}; };
} }
})
]
}); });
const links = firstLink ? [firstLink, secondLink] : [secondLink];
return createTRPCClient<AppRouter>({ links });
} }
export default gettRPC; export default gettRPC;

View File

@ -1,6 +1,6 @@
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs"; import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
import YAML from "yaml"; import * as YAML from "yaml";
import { parse } from "path/posix"; import { parse } from "node:path/posix";
export function loadConfig<T>(path: string, defaultConfig: T) { export function loadConfig<T>(path: string, defaultConfig: T) {
const parsed = parse(path); const parsed = parse(path);

View File

@ -23,6 +23,6 @@
"@server/*": ["./src/api/*"], "@server/*": ["./src/api/*"],
"@client/*": ["./src/mpp/*"], "@client/*": ["./src/mpp/*"],
"@util/*": ["./src/util/*"] "@util/*": ["./src/util/*"]
} },
} }
} }

33
tsconfig.talko.json Normal file
View File

@ -0,0 +1,33 @@
{
"compilerOptions": {
"lib": ["ESNext"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
/* Linting */
"skipLibCheck": true,
"strict": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true,
"paths": {
"@server/*": ["./src/api/*"],
"@client/*": ["./src/mpp/*"],
"@util/*": ["./src/util/*"]
},
},
"include": [
"src/talkomatic",
"src/api/api/trpc.ts",
"src/util"
]
}