Compare commits

...

5 Commits

Author SHA1 Message Date
Hri7566 a50af8abfe Update talko and backend to support saving colors 2024-10-14 01:37:36 -04:00
Hri7566 ca89644205 Add dialogue 2024-10-12 09:43:41 -04:00
Hri7566 2168ead6e1 Talkomatic stuff 2024-10-12 09:39:57 -04:00
Hri7566 2d0fb07da2 Add talkomatic 2024-10-12 01:21:38 -04:00
Hri7566 39d95665d4 Update packages, fix Discord and MPP imports 2024-10-11 19:12:24 -04:00
33 changed files with 18645 additions and 223 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=

17788
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:
name: test/fishing

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,11 @@
"@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",
"markdown-to-unicode": "^1.0.2",
"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!");

17
scripts/talko.ts Normal file
View File

@ -0,0 +1,17 @@
const roomName = "test/fishing";
const roomType = "private";
const response = await fetch("https://talkomatic.co/create-and-join-room", {
method: "POST",
headers: {
"Content-Type": "application/json",
Cookie: "updatePopupClosed1=true; userColor=darkTurquoise; connect.sid=s%3ATTw_3S9yiLjLTGqgn-8q_NvMVyZeKbzL.8s6fsBX70UGA2joKD60nW3pm9XmCELWT4io%2FIfv1yd4"
},
credentials: "include",
body: JSON.stringify({ roomName, roomType })
});
console.log(response);
console.log(
new TextDecoder().decode((await response.body?.getReader().read())?.value)
);

View File

@ -2,6 +2,7 @@ import { getBacks, flushBacks } from "@server/backs";
import { commandGroups } from "@server/commands/groups"; import { commandGroups } from "@server/commands/groups";
import { handleCommand } from "@server/commands/handler"; import { handleCommand } from "@server/commands/handler";
import { prefixes } from "@server/commands/prefixes"; import { prefixes } from "@server/commands/prefixes";
import { kvGet, kvSet } from "@server/data/keyValueStore";
import { checkToken, tokenToID } from "@server/data/token"; import { checkToken, tokenToID } from "@server/data/token";
import { TRPCError, initTRPC } from "@trpc/server"; import { TRPCError, initTRPC } from "@trpc/server";
import { Logger } from "@util/Logger"; import { Logger } from "@util/Logger";
@ -97,7 +98,12 @@ export const appRouter = router({
isDM isDM
); );
try {
return out; return out;
} catch (err) {
logger.error(err);
return undefined;
}
}), }),
backs: privateProcedure.query(async opts => { backs: privateProcedure.query(async opts => {
@ -106,7 +112,45 @@ export const appRouter = router({
const backs = getBacks<{}>(id); const backs = getBacks<{}>(id);
flushBacks(id); flushBacks(id);
try {
return backs; return backs;
} catch (err) {
logger.error(err);
return undefined;
}
}),
saveColor: privateProcedure
.input(
z.object({
userId: z.string(),
color: z.string()
})
)
.query(async opts => {
const { id, json } = await kvSet(`usercolor~${opts.input.userId}`, {
color: opts.input.color
});
return {
success: true,
id,
json
};
}),
getUserColor: privateProcedure
.input(
z.object({
userId: z.string()
})
)
.query(async opts => {
const color = await kvGet(`usercolor~${opts.input.userId}`);
return {
color
};
}) })
}); });

View File

@ -2,19 +2,6 @@ import Command from "@server/commands/Command";
import { getInventory } from "@server/data/inventory"; import { getInventory } from "@server/data/inventory";
import { locations } from "@server/fish/locations"; import { locations } from "@server/fish/locations";
const answers = [
"You are at {loc}.",
"You appear to be at {loc}.",
"According to your map, you are at {loc}.",
"The map says you are at {loc}.",
"Looking at your world atlas, you appear to be at {loc}.",
"The world atlas defines your location as {loc}.",
"Judging by the wind direction, hemisphere sun angle, and the size of the waves in the water, you deduce that you must be at {loc}.",
"You run your fingers down the map until you find {loc}. This is where you must be.",
"Your cell phone's maps app shows your location as {loc}.",
"You pull your cell phone out of your pocket and turn it on. Opening the maps app, you realize you are at {loc}."
];
export const location = new Command( export const location = new Command(
"location", "location",
["location", "loc", "where", "whereami", "l"], ["location", "loc", "where", "whereami", "l"],
@ -32,3 +19,28 @@ export const location = new Command(
return answer.replaceAll("{loc}", loc.name); return answer.replaceAll("{loc}", loc.name);
} }
); );
const answers = [
"You are at {loc}.",
"You appear to be at {loc}.",
"According to your map, you are at {loc}.",
"The map says you are at {loc}.",
"Looking at your world atlas, you appear to be at {loc}.",
"The world atlas defines your location as {loc}.",
"Judging by the wind direction, hemisphere sun angle, and the size of the waves in the water, you deduce that you must be at {loc}.",
"You run your fingers down the map until you find {loc}. This is where you must be.",
"Your cell phone's maps app shows your location as {loc}.",
"You pull your cell phone out of your pocket and turn it on. Opening the maps app, you realize you are at {loc}.",
"You currently happen to be somewhere near {loc}.",
"You look all over the piece of paper you're holding. It's actually a map, and it says you must be near {loc}.",
"Having run your fingers all over the paper, you finally come to the conclusion you must be somewhere in the vicinity of {loc}",
"Your eyes travel your map as you realize you have to be somewhere near {loc}.",
"Having found the rock that you see on your map, you are likely to be near {loc}.",
"You are likely to be near {loc}.",
"Near {loc}, you feel the wind blow on your face.",
"The wind here is strong, so you must be near {loc}.",
"Nowadays, we consider this location to be {loc}.",
"The ancient tribe long ago has given this place the name of {loc}.",
"Over the many years, people have travelled these lands and decided to name this area {loc}",
"You pull your cell phone out of your pocket and open the Maps app. The screen loads for a while, until showing you the name {loc}"
];

View File

@ -14,7 +14,11 @@ export const pick = new Command(
if (!inventory) return; if (!inventory) return;
if (!(await hasFruit())) if (!(await hasFruit()))
return crazy[Math.floor(Math.random() * crazy.length)]; return crazy[Math.floor(Math.random() * crazy.length)]
.split("$PART_NAME")
.join(part.name)
.split("$PREFIX")
.join(prefix);
const fruit = await genFruitAndRemove(); const fruit = await genFruitAndRemove();
addItem(inventory.items as unknown as IObject[], fruit); addItem(inventory.items as unknown as IObject[], fruit);
@ -314,5 +318,84 @@ const crazy = [
"You thought you spied a fruit, but were unable to procure any.", "You thought you spied a fruit, but were unable to procure any.",
"You climb all over that tree and don't find a single pickable", "You climb all over that tree and don't find a single pickable",
"You wouldn't steal a fruit from a tree with no fruit.", "You wouldn't steal a fruit from a tree with no fruit.",
"Are you sure there aren't any fruit just lying around on the ground that you can /take?" "Are you sure there aren't any fruit just lying around on the ground that you can /take?",
"Would you please stop begging for fruit? It's time for you to learn how to wait (and then maybe you will earn a watch)",
"Are you still trying to get the fruit?",
"Last time I checked, the fruit on the tree were not there.",
"When the fruit are gone, you can't have fun. Wait, that doesn't rhyme.",
"I'm Thomas Jefferson. Please wait for the fruit.",
"If I told you I was famous, would you listen to me? The fruit will not grow on command.",
"You kids can't imagine the inevitable collapse of capitalism? Incredible! The fruit are almost ready.",
"You picked up a ruler! Now you can figure out the length of things! You went to measure the fruit on the tree. Oh, wait, there's no fruit. Well, the ruler is useless, so you threw it away.",
"Fishing is a better idea right now, please keep your focus off of the fruit-less tree.",
"You want fruit from the bare tree? After all, it could only cost your life, and you got that for free!",
"This is the Earth's belly button! I don't see any fruit!",
"Go play EarhtBound, and then maybe when you have finished the game, there will be fruit.",
"Haven't you tried doing anything else? Are you still looking for the stupid fruit?",
"What is your favorite food? Go eat that, instead of the nonexistent kekklefruit.",
"Every day, the tree gets older, which means it's closer to the end of its life. Lately, there hasn't been much fruit, because people like you come and rub your sticky hands all over it.",
"Go wash your hands, you're filthy. The fruit hates dirt, and refuses to grow around people like you.",
"Patience is the key.",
"Impatience is the key.",
"For every time you decide to play the waiting game, your chances of getting fruit grows higher. So stop constantly begging for fruit, and maybe it will grow in great amounts.",
"Have you tried spending time outside? I heard there's this great place at the North Pole where they make other things to eat.",
"I have sort of neglected doing my housework... I know it's a bit of a pig sty, but anyway... I'm not growing any fruit for you.",
"You've travelled very far from home... and still, you have yet to see any fruit on the tree.",
"Maybe the tree was an illusion this whole time.",
"You are no longer alone in your adventure, some baby kekklefruit sprouts have appeared on the tree.",
"Only time can tell when you'll learn from your mistakes.",
"Everyone collectively has to stop trying to pick, and then the fruit will grow.",
"This tree hasn't grown fruit in 11 years.",
"Well, the tree has no fruit. I wish you luck...",
"Sometimes, we all want to eat to fish better, but I think you'll do fine without the fruit for now.",
"I wouldn't give you fruit even if you paid with a Diamond.",
"Go buy a hamburger instead.",
"Monkey: Meow Meow fss fss fssss? (What a strange chattering for a monkey.) Coo coo pepepe. (I think he's talking about the tree.) Croak croak breeeeeep? (Do you know how to wait for the fruit to grow?)",
"Hello! This is the kekklefruit tree. Sorry I haven't called lately. I'm still working on the way to change seeds into Kekklefruit. It's taking longer than I thought... I'm going to really work at it, though... talk to me later. *click*",
"You caught a glimpse of a small, cute Kekklefruit. No, wait, it's just an acorn.",
"What a rebellious kid! Come to the Police Station later! We have some prizes for you. Wait, you only want fruit? Well, nevermind, then. We don't have any of those fruit.",
"Tough out of luck! The kekklefruit tree is completely bare.",
"Your Luck ncreased by 5! You were filled with the Power of the Kekklefruit Tree! Your IQ decreased by 10! There's no fruit, dummy!",
"Hmm... maybe someone else has it.",
"wE fEEl GROOvE! Hi HO. mE mR. kEKkleFruIT tREe. tHis PlacE, all aRE nO FruiT.",
"Stinky! PEEEE-yEUUU! Get those disgusting hands off of the tree! There's no fruit for you!",
"You have caught all the fish, and now you want all the fruit? You are a menace to fish-ciety!",
"The boats came early this morning and took all your fruit. I guess you'll never get any...",
"Our friend $PART_NAME picked 1 stag beetle from the kekklefruit tree and instantly $PREFIXyeeted it in fear.",
'If only you knew what the word "wait" meant.',
"How many times do I have to tell you? Waiting is key!",
"Goober.",
"Go ahead and make yourself comfortable. You will be waiting for a very long time.",
"You're in for a surprise! There's no fruit again.",
"No fruit (again)",
"fruit = 0",
"Umm... the fruit left.",
"The fruit just got up... and walked away...",
"There's 104 days of waiting for fruit, and a school of fish comes along just to end it, but the annual problem of our generation is finding a good way to spend it. Perhaps, waiting even longer would be a great idea.",
"Sometimes, there's just no fruit.",
"No fruit for you, $PART_NAME.",
"A piece of paper falls out of the sky: To $PART_NAME: LEAVE THE TREE ALONE! Sincerely, the Sun.",
"Have you visited home recently? There's no fruit there either.",
"You reach for fruit, but the branches thwack you in the face, and you feel miserable.",
"Here comes the fruit delivery! Any minute now...",
"Don't worry about the fruit. You will be able to take care of yourself.",
"You understand, kekklefruit branches are the ruler of fruit in the end.",
"I was going to ask you to be my partner, but I know you'll refuse. It's written all over your face. If you accepted, I was going to give you some money. All you said you wanted was that darn fruit.",
"There's a monkey in the kekklefruit tree. He says: Uku kyee (I stole all the fruit.)",
"The kekklefruit monkey stole all of the fruit. I'm sorry.",
"You're too full of fish to pick the fruit.",
"Something smells fishy around here... oh, it's just fish. There's no fruit in the tree. There's no fruit anywhere.",
"You reach for the kekklefruit tree's branches. Then you realize, your hands are in the ground, and you picked up a handful of dirt instead.",
"How about some stew? No? Well, it was Kekklfruit Stew. I'm keeping it to myself, now.",
"Our friend $PART_NAME casts HAND into a kekklefruit tree for catching kekklefruit. Our friend $PART_NAME caught a red 🍍Kekklefruit! ready to $PREFIXeat or... oh... it was a mirage.",
"Hold on...",
"Are you sure this is the way it works?",
"Fishing is available, too, you know.",
"You could have spent your time buying 🍔McDonald's instead.",
"https://there-is-no-fruit-on-the-tree.com (real)",
"Are you sure this is what you want?",
"Our friend $PART_NAME picked 1 nothing from the kekklefruit tree and placed it into his/her thin air.",
"This is absolutely amazing! You got nothing from the tree.",
"I am once again asking you to please wait for the fruit to grow.",
"Maybe the Kekklefruit Tree will turn into a Super-Kekklefruit tree and grow lots of fruit some day..."
]; ];

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

@ -2,7 +2,7 @@ import Command from "@server/commands/Command";
export const myid = new Command( export const myid = new Command(
"myid", "myid",
["myid"], ["myid", "myuuid"],
"Get your own user ID", "Get your own user ID",
"myid", "myid",
"command.general.myid", "command.general.myid",

View File

@ -26,6 +26,8 @@ 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 { group } from "./util/permission";
// import { give } from "./inventory/give"; // import { give } from "./inventory/give";
interface ICommandGroup { interface ICommandGroup {
@ -55,7 +57,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);
@ -71,7 +73,7 @@ commandGroups.push(pokemonGroup);
const utilGroup: ICommandGroup = { const utilGroup: ICommandGroup = {
id: "util", id: "util",
displayName: "Utility", displayName: "Utility",
commands: [data, setcolor, memory, autofish, fid, chance] commands: [data, setcolor, memory, autofish, fid, chance, group]
}; };
commandGroups.push(utilGroup); commandGroups.push(utilGroup);

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(
@ -12,7 +12,8 @@ export const eat = new Command(
"command.inventory.eat", "command.inventory.eat",
async props => { async props => {
const { args, prefix, part, user } = props; const { args, prefix, part, user } = props;
const eating = args[0]; // const eating = args[0];
const eating = args.join(" ");
if (!eating) return `What do you want to ${prefix}eat?`; if (!eating) return `What do you want to ${prefix}eat?`;
const inventory = await getInventory(user.inventoryId); const inventory = await getInventory(user.inventoryId);
@ -22,27 +23,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 +56,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

@ -5,7 +5,18 @@ import type { User } from "@prisma/client";
export const sack = new Command( export const sack = new Command(
"sack", "sack",
["sack", "caught", "catched", "sock", "fish-sack", "fishies", "myfish", "mysack", "sacks"], [
"sack",
"caught",
"catched",
"sock",
"fish-sack",
"fishies",
"myfish",
"mysack",
"sacks",
"ʂасκ"
],
"List your caught fish", "List your caught fish",
"sack [user ID]", "sack [user ID]",
"command.inventory.sack", "command.inventory.sack",
@ -37,10 +48,12 @@ export const sack = new Command(
const fishSack = inv.fishSack as TFishSack; const fishSack = inv.fishSack as TFishSack;
return `Contents of ${foundUser.name}'s fish sack: ${fishSack return `Contents of ${foundUser.name}'s fish sack: ${
fishSack
.map( .map(
(fish: IFish) => (fish: IFish) =>
`${fish.emoji || "🐟"}${fish.name}${fish.count ? ` (x${fish.count})` : "" `${fish.emoji || "🐟"}${fish.name}${
fish.count ? ` (x${fish.count})` : ""
}` }`
) )
.join(", ") || "(none)" .join(", ") || "(none)"
@ -50,10 +63,12 @@ export const sack = new Command(
if (!inv) return; if (!inv) return;
const fishSack = inv.fishSack as TFishSack; const fishSack = inv.fishSack as TFishSack;
return `Contents of ${part.name}'s fish sack: ${fishSack return `Contents of ${part.name}'s fish sack: ${
fishSack
.map( .map(
(fish: IFish) => (fish: IFish) =>
`${fish.emoji || "🐟"}${fish.name}${fish.count ? ` (x${fish.count})` : "" `${fish.emoji || "🐟"}${fish.name}${
fish.count ? ` (x${fish.count})` : ""
}` }`
) )
.join(", ") || "(none)" .join(", ") || "(none)"

View File

@ -1,13 +0,0 @@
import Command from "@server/commands/Command";
export const group = new Command(
"group",
["group"],
"Get user group",
"group",
"command.util.group",
async props => {
return JSON.stringify(props);
},
false
);

View File

@ -1 +1 @@
export const prefixes = ["/"]; export const prefixes = ["/", "fish/", "fishing/", "f/", "fosh/", "foshong/"];

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

@ -22,7 +22,7 @@ export async function claimDailyPokemon(userID: string) {
} }
} }
logger.debug("Time remaining:", Date.now() - timestamp); // logger.debug("Time remaining:", Date.now() - timestamp);
// Check if it has been over a day // Check if it has been over a day
if (Date.now() - timestamp > oneDay) { if (Date.now() - timestamp > oneDay) {

View File

@ -27,11 +27,12 @@ const user = {
(globalThis as unknown as any).rl = rl; (globalThis as unknown as any).rl = rl;
rl.setPrompt("> "); rl.setPrompt("> ");
rl.prompt(); rl.prompt();
setPrompt(); await setPrompt();
function setPrompt() { async function setPrompt() {
const color = new CosmicColor(user.color); let color = await trpc.getUserColor.query({ userId: user._id });
rl.setPrompt(`\x1b[38;2;${color.r};${color.g};${color.b}m> `); const c = new CosmicColor(user.color);
rl.setPrompt(`\x1b[38;2;${c.r};${c.g};${c.b}m> `);
} }
rl.on("line", async line => { rl.on("line", async line => {
@ -98,10 +99,17 @@ setInterval(async () => {
} }
}, 1000 / 20); }, 1000 / 20);
b.on("color", msg => { b.on("color", async msg => {
if (typeof msg.color !== "string" || typeof msg.id !== "string") return; if (typeof msg.color !== "string" || typeof msg.id !== "string") return;
user.color = msg.color; user.color = msg.color;
setPrompt();
trpc.saveColor.query({
userId: user._id,
color: msg.color
});
await setPrompt();
}); });
b.on("sendchat", msg => { b.on("sendchat", msg => {

View File

@ -1,5 +1,6 @@
import { EventEmitter } from "events"; import { EventEmitter } from "events";
import Discord, { SlashCommandBuilder } from "discord.js"; import * as Discord from "discord.js";
import { SlashCommandBuilder } from "discord.js";
import { Logger } from "@util/Logger"; import { Logger } from "@util/Logger";
import { CosmicColor } from "@util/CosmicColor"; import { CosmicColor } from "@util/CosmicColor";
import gettRPC from "@util/api/trpc"; import gettRPC from "@util/api/trpc";

View File

@ -1,7 +1,7 @@
import Client from "mpp-client-net"; import Client from "mpp-client-net";
import { Logger } from "@util/Logger"; import { Logger } from "@util/Logger";
import gettRPC from "@util/api/trpc"; import gettRPC from "@util/api/trpc";
import { EventEmitter } from "events"; import EventEmitter from "events";
export interface MPPNetBotConfig { export interface MPPNetBotConfig {
uri: string; uri: string;

View File

@ -1,9 +1,8 @@
import { loadConfig } from "@util/config"; import { loadConfig } from "@util/config";
import { MPPNetBot, type MPPNetBotConfig } from "./Bot"; import { MPPNetBot, type MPPNetBotConfig } from "./Bot";
import { Logger } from "@util/Logger"; // import { Logger } from "@util/Logger";
const logger = new Logger("big brain");
// const logger = new Logger("big brain");
const bots: MPPNetBot[] = []; const bots: MPPNetBot[] = [];
const defaults = loadConfig("config/mpp_bots.yml", [ const defaults = loadConfig("config/mpp_bots.yml", [

View File

@ -0,0 +1,430 @@
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();
const convertMarkdownToUnicode = require("markdown-to-unicode");
export interface TalkomaticBotConfig {
channel: {
name: string;
type: "public" | "private";
};
}
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 defaultColor = "#abe3d6";
public channelId: string = "";
constructor(public config: TalkomaticBotConfig) {
super();
this.logger = new Logger("Talkomatic - " + config.channel.name);
this.client = io("https://talkomatic.co/", {
extraHeaders: {
Cookie: "connect.sid=" + process.env.TALKOMATIC_SID
},
autoConnect: false
});
this.bindEventListeners();
}
public async start() {
this.logger.info("Starting");
this.client.connect();
// this.client.io.engine.on("packetCreate", this.logger.debug);
let data =
(await this.findChannel(this.config.channel.name)) ||
(await this.createChannel(
this.config.channel.name,
this.config.channel.type
));
this.logger.debug(data);
if (typeof data !== "undefined") {
try {
this.channelId = (data as Record<string, any>).room.room_id;
this.setChannel(this.channelId);
this.started = true;
} catch (err) {
this.logger.error(err);
}
}
}
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
};
// this.logger.debug(msg);
// 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",
async (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) {
let color = (
await this.trpc.getUserColor.query({
userId: user.id
})
).color;
this.logger.debug(
"(updateRoom) user color from api:",
color
);
const p = ppl[user.id] || {
name: user.username,
id: user.id,
color,
typingFlag: false
};
ppl[user.id] = p;
}
} catch (err) {
this.logger.warn("Unable to set user data:", err);
}
}
);
this.client.on(
"roomUsers",
async (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) {
let color = (
await this.trpc.getUserColor.query({
userId: user.id
})
).color;
if (!color) color = this.defaultColor;
this.logger.debug(
"(roomUsers) user color from api:",
color
);
const p = ppl[user.id] || {
name: user.username,
id: user.id,
color,
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)
);
let color = (
await this.trpc.getUserColor.query({
userId: msg.userId
})
).color;
if (!color) color = this.defaultColor;
if (!usedPrefix) return;
const args = msg.text.split(" ");
let part: TalkomaticParticipant = ppl[msg.userId] || {
name: "<unknown user>",
id: msg.userId,
color,
typingFlag: false
};
this.logger.info(`${part.name}: ${msg.text}`);
const command = await this.trpc.command.query({
channel: this.channelId,
args: args.slice(1, args.length),
command: args[0].substring(usedPrefix.length),
prefix: usedPrefix,
user: {
id: part.id,
name: part.name,
color: part.color
}
});
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: msg.username,
id: msg.id,
color: "#abe3d6",
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", async msg => {
if (typeof msg.color !== "string" || typeof msg.id !== "string")
return;
// this.textColor = msg.color;
try {
ppl[msg.id].color = msg.color;
await this.trpc.saveColor.query({
userId: msg.id,
color: msg.color
});
} catch (err) {
this.logger.warn("Unable to save user color:", err);
}
});
this.b.on(
"sendchat",
(msg: { m: "sendchat"; channel: string; message: string }) => {
// this.logger.debug("sendchat message:", msg);
if (typeof msg.channel === "string") {
if (msg.channel !== this.channelId) return;
}
this.sendChat(msg.message);
}
);
}
private oldText: string = "";
public sendChat(text: string, reply?: string, id?: string) {
const fixedOld = this.oldText.split("\n")[-1];
if (text.toLowerCase().includes("autofish"))
text = `${fixedOld ? fixedOld + "\n" : ""}${text}`;
const msg = {
roomId: this.channelId,
// text: text.split("sack").join("ʂасκ"),
text: text.split("sack").join("caught"),
color: id ? ppl[id].color : this.defaultColor
};
for (const uuid of Object.keys(ppl)) {
const p = ppl[uuid];
msg.text = msg.text.split(`@${uuid}`).join(p.name);
if (!p) continue;
if (uuid !== id) continue;
msg.color = p.color;
}
try {
msg.text = convertMarkdownToUnicode(msg.text);
} catch (err) {
this.logger.warn("Unable to parse markdown:", err);
}
this.client.emit("typing", msg);
this.oldText = text;
}
public setChannel(roomId: string) {
this.client.emit("joinRoom", { roomId });
}
public async createChannel(
roomName: string,
roomType: "public" | "private" = "public"
) {
const response = await fetch(
"https://talkomatic.co/create-and-join-room",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Cookie: "connect.sid=" + process.env.TALKOMATIC_SID
},
credentials: "include",
body: JSON.stringify({
roomName,
roomType
})
}
);
if (!response.ok)
return void this.logger.warn(
"Unable to create channel:",
new TextDecoder().decode(
(await response.body?.getReader().read())?.value
)
);
try {
const data = new TextDecoder().decode(
(await response.body?.getReader().read())?.value
);
return JSON.parse(data.toString());
} catch (err) {
this.logger.warn(
"Unable to decode channel creation response data:",
err
);
}
}
public async findChannel(name: string) {
const response = await fetch("https://talkomatic.co/rooms", {
method: "GET"
});
if (!response.ok)
return void this.logger.warn(
"Unable to create channel:",
new TextDecoder().decode(
(await response.body?.getReader().read())?.value
)
);
try {
const data = new TextDecoder().decode(
(await response.body?.getReader().read())?.value
);
const rooms = JSON.parse(data.toString());
for (const room of rooms.rooms) {
if (room.room_name == name) {
return { room };
}
}
} catch (err) {
this.logger.warn(
"Unable to decode channel creation response data:",
err
);
}
}
}

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: {
name: "test/fishing"
}
}
] 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

@ -2,9 +2,13 @@ import { kvGet, kvSet } from "@server/data/keyValueStore";
import { test, expect } from "bun:test"; import { test, expect } from "bun:test";
test("Key value store saves, loads, and deletes", async () => { test("Key value store saves, loads, and deletes", async () => {
await kvSet("test", 1); const stuff = {
potatoes: 30
};
await kvSet("test", stuff);
const val = await kvGet("test"); const val = await kvGet("test");
expect(val).toBe(1); expect(val.potatoes).toBe(30);
await kvSet("test", undefined); await kvSet("test", undefined);
const val2 = await kvGet("test"); const val2 = await kvGet("test");

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"
]
}