diff --git a/bun.lockb b/bun.lockb index 47beab2..8db6677 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/config/discord.yml b/config/discord.yml new file mode 100644 index 0000000..f6b1962 --- /dev/null +++ b/config/discord.yml @@ -0,0 +1,2 @@ +serverID: "841331769051578413" +defaultChannelID: "841331769658703954" diff --git a/package.json b/package.json index a4a00dd..1df75ed 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@trpc/client": "next", "@trpc/server": "next", "cli-markdown": "^3.2.2", + "discord.js": "^14.14.1", "mpp-client-net": "^1.1.3", "prisma": "^5.9.1", "trpc-bun-adapter": "^1.1.0", diff --git a/src/api/commands/groups/general/help.ts b/src/api/commands/groups/general/help.ts index d1d1824..e5d2191 100644 --- a/src/api/commands/groups/general/help.ts +++ b/src/api/commands/groups/general/help.ts @@ -32,7 +32,7 @@ export const help = new Command( } if (list2.length > 0) - list.push(`${group.displayName}: ${list2.join(", ")}`); + list.push(`**${group.displayName}:** ${list2.join(", ")}`); } return `__Fishing:__\n${list.join("\n")}`; diff --git a/src/api/commands/groups/index.ts b/src/api/commands/groups/index.ts index 151ac58..3f4065a 100644 --- a/src/api/commands/groups/index.ts +++ b/src/api/commands/groups/index.ts @@ -21,6 +21,7 @@ import { myid } from "./general/myid"; import { yeet } from "./inventory/yeet"; import { tree } from "./fishing/tree"; import { pick } from "./fishing/pick"; +// import { give } from "./inventory/give"; interface ICommandGroup { id: string; @@ -49,7 +50,7 @@ commandGroups.push(fishingGroup); const inventoryGroup: ICommandGroup = { id: "inventory", displayName: "Inventory", - commands: [inventory, take, eat, sack, pokemon, yeet] + commands: [inventory, take, eat, sack, pokemon, yeet /* give */] }; commandGroups.push(inventoryGroup); diff --git a/src/api/commands/groups/inventory/give.ts b/src/api/commands/groups/inventory/give.ts new file mode 100644 index 0000000..34a6914 --- /dev/null +++ b/src/api/commands/groups/inventory/give.ts @@ -0,0 +1,130 @@ +import type { User } from "@prisma/client"; +import Command from "@server/commands/Command"; +import { getInventory, updateInventory } from "@server/data/inventory"; +import prisma from "@server/data/prisma"; +import { addItem } from "@server/items"; + +export const give = new Command( + "give", + ["give", "govo", "guvu", "gava", "geve", "givi", "g", "donate", "bestow"], + "Give another user something you have", + "give ", + "command.inventory.give", + async ({ id, command, args, prefix, part, user }) => { + const inventory = await getInventory(user.inventoryId); + if (!inventory) return; + + let targetFuzzy = args[0]; + if (!targetFuzzy) return `To whom will you ${prefix}${command} to?`; + + let foundUser: User = user; + foundUser = (await prisma.user.findFirst({ + where: { + name: { + contains: targetFuzzy, + mode: "insensitive" + } + } + })) as User; + + if (!foundUser) return `Who is ${targetFuzzy}? I don't know them.`; + + const foundInventory = await getInventory(foundUser.inventoryId); + if (!foundInventory) return `They have no room, apparently.`; + + if (!args[1]) + return `What are you going to ${prefix}${command} to ${foundUser.name}?`; + const argcat = args.slice(1).join(" "); + let foundObject: IObject | undefined; + + let i = 0; + + for (const item of inventory.items as unknown as IItem[]) { + 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.`; + + let updated = false; + + if (foundObject.objtype == "item") { + addItem(foundInventory.items as unknown as IItem[], foundObject); + updated = true; + } else if (foundObject.objtype == "fish") { + addItem(foundInventory.items as unknown as IItem[], foundObject); + updated = true; + } + + let shouldRemove = false; + + if (updated) { + await updateInventory(foundInventory); + + if (foundObject.objtype == "fish") { + i = 0; + + 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") { + i = 0; + + 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 ${ + command.endsWith("e") ? `${command}d` : `${command}ed` + } your ${foundObject.name} to ${foundUser.name}.`; + } else { + return `You tried to give your ${foundObject.name} away, but I messed up and the transaction was reverted.`; + } + } +); diff --git a/src/discord/bot/Bot.ts b/src/discord/bot/Bot.ts new file mode 100644 index 0000000..a8bd840 --- /dev/null +++ b/src/discord/bot/Bot.ts @@ -0,0 +1,185 @@ +import { EventEmitter } from "events"; +import Discord from "discord.js"; +import { Logger } from "@util/Logger"; +import { CosmicColor } from "@util/CosmicColor"; +import trpc from "@util/api/trpc"; + +export interface DiscordBotConfig { + serverID: string; + defaultChannelID: string; + token?: string; +} + +export class DiscordBot extends EventEmitter { + public client: Discord.Client; + public logger = new Logger("Discord Bot"); + public token?: string; + public server?: Discord.Guild; + public defaultChannel?: Discord.TextChannel; + public b = new EventEmitter(); + + constructor(public conf: DiscordBotConfig) { + super(); + + this.token = conf.token ?? process.env.DISCORD_TOKEN; + this.client = new Discord.Client({ + intents: [ + "Guilds", + "GuildMessages", + "MessageContent", + "GuildMembers" + ] + }); + + this.bindEventListeners(); + } + + public async start() { + await this.client.login(this.token); + } + + private bindEventListeners() { + this.client.on("ready", async () => { + this.logger.info("Connected to Discord"); + + this.server = await this.client.guilds.fetch(this.conf.serverID); + + const channel = await this.server.channels.fetch( + this.conf.defaultChannelID + ); + + if (!channel) throw "Unable to find default Discord channel."; + + this.defaultChannel = channel as Discord.TextChannel; + }); + + this.client.on("guildMemberAdd", async member => { + if (!this.server) return; + + const color = new CosmicColor( + Math.floor(Math.random() * 255), + Math.floor(Math.random() * 255), + Math.floor(Math.random() * 255) + ); + + const existingRole = this.server.roles.cache.find( + role => role.name === member.id + ); + + if (existingRole) { + await member.roles.add(existingRole); + return; + } + + const role = await this.server.roles.create({ + name: member.id, + color: parseInt(color.toHexa().substring(1), 16) + }); + + await member.roles.add(role); + }); + + this.client.on("messageCreate", async msg => { + if (!this.server) return; + if (msg.guildId !== this.server.id) return; + + const existingRole = this.server.roles.cache.find( + role => role.name === msg.author.id + ); + + if (!existingRole) return; + + let prefixes: string[]; + + try { + prefixes = await 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.content.startsWith(pr) + ); + + if (!usedPrefix) return; + + const args = msg.content.split(" "); + + const command = await trpc.command.query({ + args: args.slice(1, args.length), + command: args[0].substring(usedPrefix.length), + prefix: usedPrefix, + user: { + id: msg.author.id, + name: msg.author.displayName, + color: existingRole.hexColor + } + }); + + if (!command) return; + if (command.response) + msg.reply( + command.response + .split(`@${msg.author.id}`) + .join(`<@${msg.author.id}>`) + ); + }); + + setInterval(async () => { + try { + const backs = (await trpc.backs.query()) as IBack[]; + 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; + + if (!this.server) return; + + const existingRole = this.server.roles.cache.find( + role => role.name === msg.id + ); + + if (!existingRole) { + try { + const member = await this.server.members.fetch(msg.id); + if (!member) throw "no member"; + + const role = await this.server.roles.create({ + name: member.id, + color: parseInt(msg.color.substring(1), 16) + }); + + await member.roles.add(role); + return; + } catch (err) { + return; + } + } + + await existingRole.setColor(parseInt(msg.color.substring(1), 16)); + }); + + this.b.on("sendchat", msg => { + // this.logger.debug("sendchat message:", msg); + if (!this.defaultChannel) return; + this.defaultChannel.send( + msg.message + .split(`@${msg.author.id}`) + .join(`<@${msg.author.id}>`) + ); + }); + } +} diff --git a/src/discord/bot/index.ts b/src/discord/bot/index.ts new file mode 100644 index 0000000..8464004 --- /dev/null +++ b/src/discord/bot/index.ts @@ -0,0 +1,10 @@ +import { DiscordBot, type DiscordBotConfig } from "./Bot"; + +export async function initBot(conf: DiscordBotConfig) { + const bot = new DiscordBot(conf); + await bot.start(); +} + +export { DiscordBot as Bot }; + +export default DiscordBot; diff --git a/src/discord/index.ts b/src/discord/index.ts new file mode 100644 index 0000000..086894c --- /dev/null +++ b/src/discord/index.ts @@ -0,0 +1,10 @@ +import { loadConfig } from "@util/config"; +import { initBot } from "./bot"; +import type { DiscordBotConfig } from "./bot/Bot"; + +const config = loadConfig("config/discord.yml", { + serverID: "841331769051578413", + defaultChannelID: "841331769658703954" +}); + +await initBot(config);