From 4e8233403aa3b06db5b4a72b802eeb2a0441589d Mon Sep 17 00:00:00 2001 From: PwLDev <64767383+PwLDev@users.noreply.github.com> Date: Thu, 25 Jun 2026 11:21:07 -0700 Subject: [PATCH 1/3] feat(cogs): init --- src/cogs/config.py | 94 +++++++++++++++ src/cogs/dev.py | 36 ++++++ src/cogs/info.py | 46 +++++++ src/cogs/mail.py | 111 +++++++++++++++++ src/cogs/misc.py | 67 +++++++++++ src/components/mail.py | 82 +++++++++++++ src/core/client.py | 232 ++++++++++++++++++++++++++++++++++++ src/core/ws.py | 38 ++++++ src/main.py | 19 +++ src/util/config.py | 57 +++++++++ src/util/gen.py | 127 ++++++++++++++++++++ src/util/markov.py | 121 +++++++++++++++++++ src/util/status.py | 59 +++++++++ src/util/sys.py | 264 +++++++++++++++++++++++++++++++++++++++++ 14 files changed, 1353 insertions(+) create mode 100644 src/cogs/config.py create mode 100644 src/cogs/dev.py create mode 100644 src/cogs/info.py create mode 100644 src/cogs/mail.py create mode 100644 src/cogs/misc.py create mode 100644 src/components/mail.py create mode 100644 src/core/client.py create mode 100644 src/core/ws.py create mode 100644 src/main.py create mode 100644 src/util/config.py create mode 100644 src/util/gen.py create mode 100644 src/util/markov.py create mode 100644 src/util/status.py create mode 100644 src/util/sys.py diff --git a/src/cogs/config.py b/src/cogs/config.py new file mode 100644 index 0000000..f3dcf8f --- /dev/null +++ b/src/cogs/config.py @@ -0,0 +1,94 @@ +import re +from discord import app_commands +from discord.ext import commands +from core.client import Rob +from util.config import has_rob_admin, save_config +from typing import Optional + +@commands.check(has_rob_admin) +@commands.guild_only() +class ConfigCog(commands.Cog): + def __init__(self, bot: Rob): + super().__init__() + self.bot = bot + + @commands.hybrid_command( + name="option", + description="Change Rob's settings to your liking." + ) + @app_commands.describe( + option="ID of the option to change.", + value="Value to assign, leave empty to view the current value." + ) + async def option(self, ctx: commands.Context, option: str, value: Optional[str]): + assert ctx.guild + if not value: + return await ctx.send(f"its `{self.bot.config[option]}`") + elif not option: + return await ctx.send("#!option \nthats how you do it btw ;3", ephemeral=True) + + if option in self.bot.config: + if not option == "model" and not option == "mailChannel" and not option == "mailTrusted": + if value.lower() in ["enable", "disable"]: + self.bot.config[option] = value.lower() == "enable" + else: + try: + self.bot.config[option] = int(value) + except ValueError: + await ctx.send("no not like that :X\nuse `enable`, `disable`, or a number", ephemeral=True) + return + elif option == "mailChannel": + match = re.match(r"<#(\d+)>", value) + + if match: + self.bot.config[option] = int(match.group(1)) + elif value.isdigit(): + self.bot.config[option] = int(value) + else: + await ctx.send("give me a channel mention or channel id", ephemeral=True) + return + elif option == "mailTrusted" or option == "model": + await ctx.send("you cannot change that part of my config with #!option :X", ephemeral=True) + return + else: + config[option] = value + save_config(ctx.guild.id, self.bot.config) + await ctx.send(f"alr, `{option}` is now `{value}` :)") + else: + await ctx.send(f"umm idk what a `{option}` is :/", ephemeral=True) + return + + @commands.hybrid_command( + name="trust", + description="Trusts another server's address, allowing them to send you letters." + ) + @app_commands.describe(address="Mail address of the server to trust.") + async def trust(self, ctx: commands.Context, address: str): + if not address: + await ctx.send("its like #!trust
", ephemeral=True) + return + + if address not in self.bot.config["mailTrusted"]: + self.bot.config["mailTrusted"].append(address) + save_config(ctx.guild.id, self.bot.config) + + return await ctx.send(f"trusted `{address}` :)") + + @commands.hybrid_command( + name="untrust", + description="Stops trusting another server's address, blocking them from sending you letters." + ) + @app_commands.describe(address="Mail address of the server to untrust.") + async def untrust(self, ctx: commands.Context, address: str): + if not address: + await ctx.send("its like #!untrust
", ephemeral=True) + return + + if address in self.bot.config["mailTrusted"]: + self.bot.config["mailTrusted"].remove(address) + save_config(ctx.guild.id, self.bot.config) + + return await ctx.send(f"bleh, untrusted `{address}` -_-") + +async def setup(bot: Rob): + return await bot.add_cog(ConfigCog(bot)) \ No newline at end of file diff --git a/src/cogs/dev.py b/src/cogs/dev.py new file mode 100644 index 0000000..817606f --- /dev/null +++ b/src/cogs/dev.py @@ -0,0 +1,36 @@ +import io +from discord.ext import commands +from core.client import Rob +from contextlib import redirect_stdout + +@commands.is_owner() +class DevCog(commands.Cog): + def __init__(self, bot: Rob): + super().__init__() + self.bot = bot + + @commands.command(name="eval", description="Evaluates Python code.") + async def eval(self, ctx: commands.Context, *, code: str): + print(f":: [SECURITY WARNING] - executing eval '{code}'.") + buffer = io.StringIO() + + try: + with redirect_stdout(buffer): + exec(code) + + output = buffer.getvalue() or "(no output)" + await ctx.channel.send(f"```\n{output}\n```") + except Exception as e: + await ctx.channel.send(f"```\n{type(e).__name__}: {e}\n```") + return + + @commands.command(name="reload", description="Reloads a loaded cog.") + async def reload(self, ctx: commands.Context, cog: str): + try: + await self.bot.reload_extension(f"cogs.{cog}") + return await ctx.reply(f":: Cog {cog} loaded successfully") + except Exception as e: + return await ctx.reply(f":: Cog {cog} failed to load: {e}") + +async def setup(bot: Rob): + return await bot.add_cog(DevCog(bot)) \ No newline at end of file diff --git a/src/cogs/info.py b/src/cogs/info.py new file mode 100644 index 0000000..ec49dae --- /dev/null +++ b/src/cogs/info.py @@ -0,0 +1,46 @@ +from discord.ext import commands +from core.client import Rob + +class InfoCog(commands.Cog): + def __init__(self, bot: Rob): + super().__init__() + self.bot = bot + + @commands.hybrid_command( + name="help", + description="Sends a link to Rob's official website." + ) + async def help(self, ctx: commands.Context): + return await ctx.send("go to https://dogo6647.github.io/rob for help :)") + + @commands.hybrid_command( + name="about", + description="Shows bot credits and interaction stats." + ) + async def about(self, ctx: commands.Context): + self.bot.reset_stats() + + ranked = [] + users = sum(g.member_count for g in self.bot.guilds) + + await ctx.send(f"haiiiii im rob, a conversational bot created by `dogo6647` :)") + await ctx.send(f"im currently in {len(self.bot.guilds)} servers and have met {users} users, isnt that cool? :D") + + this_guild_msgs = self.bot.guild_daily_stats[ctx.guild.id] + for guild in self.bot.guilds: + if "COMMUNITY" not in guild.features: + continue + if guild.member_count <= 50: + continue + msg_count = self.bot.guild_daily_stats[guild.id] + ranked.append((guild.name, guild.member_count, msg_count)) + + ranked.sort(key=lambda x: x[2], reverse=True) + top_10 = ranked[:10] + + if top_10: + top_servers = "\n".join(f"{i+1}. {name} ({members} members) - {msgs} interactions today" for i, (name, members, msgs) in enumerate(top_10)) + await ctx.send(f"i've sent {this_guild_msgs} messages in this server today, heres today's biggest rob addicts:\n```{top_servers}```") + +async def setup(bot: Rob): + return await bot.add_cog(InfoCog(bot)) \ No newline at end of file diff --git a/src/cogs/mail.py b/src/cogs/mail.py new file mode 100644 index 0000000..1a22d40 --- /dev/null +++ b/src/cogs/mail.py @@ -0,0 +1,111 @@ +import re +from discord import app_commands, Embed, Guild +from discord.ext import commands +from core.client import Rob +from components.mail import LetterView, PhonebookView +from util.config import load_config, get_mail_channel + +class MailCog(commands.Cog): + def __init__(self, bot: Rob): + super().__init__() + self.bot = bot + + def guild_address(self, guild: Guild): + slug = re.sub(r"[^a-z0-9]+", "-", guild.name.lower()) + slug = slug.strip("-") + return f"{slug}-{str(guild.id)[-2:]}" + + @commands.hybrid_command( + name="address", + description="Shows your server's robmail address." + ) + async def address(self, ctx: commands.Context): + return await ctx.send(f"this server's address is `{self.guild_address(ctx.guild)}`") + + @commands.hybrid_command( + name="phonebook", + description="Presents all addresses trusted by the current server." + ) + async def phonebook(self, ctx: commands.Context): + trusted = self.bot.config.get("mailTrusted", []) + + if not trusted: + return await ctx.send( + "this server's phonebook is empty :(\nuse `#!trust
` to add servers" + ) + + entries = [] + + for address in trusted: + guild_name = "unknown server" + for guild in self.bot.guilds: + if self.guild_address(guild) == address: + guild_name = guild.name + break + + entries.append((guild_name, address)) + + view = PhonebookView(entries, ctx.author.id) + return await ctx.send(embed=view.make_embed(), view=view) + + @commands.hybrid_command( + name="send", + description="Sends a letter to a specified robmail address." + ) + @app_commands.describe( + address="Address of the server to send the letter to.", + message="Content of this letter." + ) + async def send(self, ctx: commands.Context, address: str, *, message: str): + if not address or not message: + return await ctx.send("its like #!send
", ephemeral=True) + + sender_address = self.guild_address(ctx.guild) + target_guild = None + + for guild in self.bot.guilds: + if self.guild_address(guild) == address: + target_guild = guild + break + + if target_guild is None: + await ctx.send("i couldn't find that server :(", ephemeral=True) + return + + sender_cfg = load_config(ctx.guild.id) + receiver_cfg = load_config(target_guild.id) + + if address not in sender_cfg["mailTrusted"]: + return await ctx.send(f"that address isn't on your trusted list, run '#!trust {address}'", ephemeral=True) + + if sender_address not in receiver_cfg["mailTrusted"]: + return await ctx.send(f"that server hasn't trusted you yet, ask them to run '#!trust {sender_address}'", ephemeral=True) + + channel = get_mail_channel(target_guild, receiver_cfg) + + if not channel: + return await ctx.send("that server has nowhere i can deliver mail :(", ephemeral=True) + + if not channel: + return await ctx.send("delivery failed :(", ephemeral=True) + + letter_text = ( + f"Dear {target_guild.name}:\n\n" + f"{message}\n\n" + f"- {message.author.name}" + ) + + embed = Embed( + title="📬 you've got mail!", + description=f"a letter has arrived from **{message.guild.name}**.", + color=0xF4D58D + ) + + await channel.send( + embed=embed, + view=LetterView(letter_text) + ) + return await ctx.send("letter delivered! :D", ephemeral=True) + +async def setup(bot: Rob): + return await bot.add_cog(MailCog(bot)) \ No newline at end of file diff --git a/src/cogs/misc.py b/src/cogs/misc.py new file mode 100644 index 0000000..4213233 --- /dev/null +++ b/src/cogs/misc.py @@ -0,0 +1,67 @@ +from aiohttp import ClientSession +from discord import app_commands +from discord.ext import commands +from core.client import Rob +from util.gen import generate_response +from util.sys import websearch + +class MiscCog(commands.Cog): + def __init__(self, bot: Rob): + super().__init__() + self.bot = bot + + @commands.hybrid_command( + name="dadjoke", + description="Sends a random joke from the icanhazadadjoke API." + ) + async def dadjoke(self, ctx: commands.Context): + async with ClientSession(headers={ + "Accept": "text/plain", + "User-Agent": "Rob/1.0.0 (Discord Bot; https://dogo6647.github.io/rob)" + }) as session: + async with session.get("https://icanhazdadjoke.com/") as response: + joke = await response.text() + return await ctx.send(joke) + + @commands.hybrid_command( + name="owobonk", + description="Hits Rob with the magic owo stick that temporarily uwuifies his responses.", + guild_only=True + ) + async def owobonk(self, ctx: commands.Context): + self.bot.history.append({ + "role": "system", + "content": "You have been hit with the OwO magic stik. Youw head huwts a wittwe, and you can onwwy tawwk in uwu femboy furry language fwom now on. Replace evewy 'r' you say with 'w'. Occassionawwy say stufz like *blushes*, *giggles*, rawr, and hehe~. Use '~' non-spawringwy." + }) + + response = await generate_response( + "BANNNNNNGGGG!!!!! say your head is feeling funny. Start your response with 'ow' or similar.", + self.bot.history, + self.bot.config.get("model"), + self.bot.config.get("dumb"), + f"the {ctx.guild.name} server" if ctx.guild else "DMs" + ) + await ctx.send(f"🪄💥 >~< {response}") + + @commands.hybrid_command( + name="search", + description="Searches for stuff on the web using DuckDuckGo and provides a Rob-certified™ summary.", + ) + @app_commands.describe(query="Query to search on the web.") + async def search(self, ctx: commands.Context, query: str): + async def update_status(text): + await ctx.send(text) + + result = await websearch(query, update_status) + response = await generate_response( + f"Summarize the following text so that it's relevant to the conversation: '{result}'. Use the amount of words necessary to make a detailed explanation.", + self.bot.history, + self.bot.config.get("model"), + self.bot.config.get("dumb"), + f"the {ctx.guild.name} server" if ctx.guild else "DMs" + ) + await ctx.send(response) + return + +async def setup(bot: Rob): + return await bot.add_cog(MiscCog(bot)) \ No newline at end of file diff --git a/src/components/mail.py b/src/components/mail.py new file mode 100644 index 0000000..27f502d --- /dev/null +++ b/src/components/mail.py @@ -0,0 +1,82 @@ +import discord + +class LetterView(discord.ui.View): + def __init__(self, letter_text): + super().__init__(timeout=None) + self.letter_text = letter_text + + @discord.ui.button(label="open letter!", style=discord.ButtonStyle.primary) + async def open_letter(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.response.send_message( + self.letter_text, + ephemeral=True + ) + +class PhonebookView(discord.ui.View): + def __init__(self, entries, author_id): + super().__init__(timeout=180) + + self.entries = entries + self.author_id = author_id + self.page = 0 + self.per_page = 5 + + self.update_buttons() + + def update_buttons(self): + self.prev_button.disabled = self.page <= 0 + self.next_button.disabled = self.page >= self.max_page + + @property + def max_page(self): + return max(0, (len(self.entries) - 1) // self.per_page) + + def make_embed(self): + start = self.page * self.per_page + end = start + self.per_page + + lines = [] + + for i, (guild_name, address) in enumerate( + self.entries[start:end], + start=start + 1 + ): + lines.append( + f"**{i}. {guild_name}**\n" + f"`{address}`" + ) + + embed = discord.Embed( + title="☎️ phonebook", + description="\n\n".join(lines) or "*empty*", + color=0x87CEEB + ) + + embed.set_footer( + text=f"{self.page + 1}/{self.max_page + 1} | {len(self.entries)} trusted addresses | use #!send " + ) + + return embed + + async def interaction_check(self, interaction): + return interaction.user.id == self.author_id + + @discord.ui.button(label="←", style=discord.ButtonStyle.secondary) + async def prev_button(self, interaction, button): + self.page -= 1 + self.update_buttons() + + await interaction.response.edit_message( + embed=self.make_embed(), + view=self + ) + + @discord.ui.button(label="→", style=discord.ButtonStyle.secondary) + async def next_button(self, interaction, button): + self.page += 1 + self.update_buttons() + + await interaction.response.edit_message( + embed=self.make_embed(), + view=self + ) diff --git a/src/core/client.py b/src/core/client.py new file mode 100644 index 0000000..a120396 --- /dev/null +++ b/src/core/client.py @@ -0,0 +1,232 @@ +import os, random +from asyncio import sleep +from datetime import datetime +from discord import Intents, Message +from discord.ext import commands, tasks +from discord.gateway import DiscordWebSocket +from collections import defaultdict, deque + +from .ws import MobileWebSocket +from util.config import load_config, save_config, get_mail_channel +from util.gen import generate_response +from util.sys import process_msg, websearch +from util.status import STATUSES + +CHANGELOG_FILE = "changelog.txt" +ENABLED_COGS = ["config", "dev", "info", "mail", "misc", "util"] +TIN_CAN_CHANCE = 0.005 + +class Rob(commands.Bot): + changelog_checked = False + # Store message history per server and per user in DMs + dm_message_histories = defaultdict(lambda: deque(maxlen=24)) + guild_message_histories = defaultdict(lambda: deque(maxlen=24)) + guild_daily_stats = defaultdict(int) + stats_day = datetime.utcnow().date() + + def __init__(self): + intents = Intents.default() + intents.messages = True + intents.guilds = True + intents.members = True + intents.dm_messages = True + intents.message_content = True + + super().__init__( + command_prefix="#!", + intents=intents, + help_command=None # this is overwritten in the Info cog + ) + + async def setup_hook(self): + for cog in ENABLED_COGS: + try: + await self.load_extension(f"cogs.{cog}") + print(f":: Cog {cog} loaded successfully") + except Exception as e: + print(f":: Cog {cog} failed to load: {e}") + await self.tree.sync() + + async def on_ready(self): + print(f':: Logged in as {self.user}') + #print(":: Guilds:") # should not normally be enabled in large instances + #for guild in client.guilds: + # print(f"- {guild.name} | owned by {guild.owner} | {guild.member_count} members") + + if not changelog_checked: + changelog_checked = True + await self.broadcast() + + self.loop.create_task(self.send_random_message) + self.change_status.start() + + async def on_guild_join(guild): + config = load_config(guild.id) + save_config(guild.id, config) + channel = get_mail_channel(guild, config) + if channel: + await channel.send("hi! visit https://dogo6647.github.io/rob to learn how to set me up :)") + + async def on_message(self, message: Message): + if message.guild: + guild_id = message.guild.id + config = load_config(guild_id) + if config["listen"] and message.author != self.user: + self.guild_message_histories[guild_id].append({ + "role": "user", + "content": process_msg(message) + }) + history = self.guild_message_histories[guild_id] + if not message.channel.permissions_for(message.guild.me).send_messages: + return + else: + user_id = message.author.id + userconfig = "userland" + config = load_config(userconfig) + if config["listen"] and message.author != self.user: + self.dm_message_histories[user_id].append({ + "role": "user", + "content": process_msg(message) + }) + history = self.dm_message_histories[user_id] + + if message.author == self.user: + history.append({"role": "assistant", "content": message.content}) + return # Ignore itself lol + + if not config["listen"]: + return + + should_respond = message.mention_everyone or self.user.mentioned_in(message) or random.random() < (config["responseFrequency"] / 100) + should_reply = self.user.mentioned_in(message) or message.reference is not None + + if should_respond: + async with message.channel.typing(): + num_responses = random.choices([1, 2], weights=[85, 15], k=1)[0] + + for i in range(num_responses): + if random.random() < TIN_CAN_CHANCE: + if random.randint(0, 1) == 0: + response = "*tin can noises*" + else: + response = "https://odysea.us.to/assets/dump/iamarobot.mov" + else: + #print('response' if i == 0 else 'continuation') + response = await generate_response( + 'respond' if i == 0 else 'continue Rob\'s previous message', + history, + config.get("model"), + config.get("dumb"), + f"the {message.guild.name} server" if message.guild else "DMs" + ) + + if (history and history[-1]["role"] == "assistant" and history[-1]["content"] == response): + continue + + if "[searchfor: " in response: + should_reply = False + + if should_reply and i == 0: + await message.reply(response, mention_author=False) + else: + if "[searchfor: " in response: + async def update_status(text): + await message.channel.send(text) + + q = response[len("[searchfor:"): -1].strip() + result = await websearch(q, update_status) + response = await generate_response( + f"Summarize the following text so that it's relevant to the conversation: '{result}'. Use the amount of words necessary to make a detailed explanation.", + history, + config.get("model"), + config.get("dumb"), + f"the {message.guild.name} server" if message.guild else "DMs" + ) + await message.channel.send(response) + else: + await message.channel.send(response) + + # sleep between responses, not after the last one + if i < num_responses - 1: + await sleep(random.uniform(0.5, 2)) + + if message.guild: + self.guild_daily_stats[message.guild.id] += num_responses + + async def change_status(self): + global current_status + current_status = random.choice(STATUSES) + if current_status is None: + await self.change_presence(activity=None) + else: + await self.change_presence(activity=current_status) + print(f"Changed status to: {current_status.name if current_status else 'nothing'}") + + async def reset_stats(self): + today = datetime.utcnow().date() + if today != self.stats_day: + self.guild_daily_stats.clear() + self.stats_day = today + + async def broadcast(self): + if not os.path.exists(CHANGELOG_FILE): + return + + with open(CHANGELOG_FILE, "r", encoding="utf-8") as f: + raw = f.read() + + if raw.lstrip().startswith("[sent]"): + return + + changelog_text = raw.split("[sent]", 1)[0].strip() + if not changelog_text: + return + + print(":: Broadcasting changelog...") + + for guild in self.guilds: + try: + config = load_config(guild.id) + channel = get_mail_channel(guild, config) + + if not channel: + continue + + owner_ping = guild.owner.mention if guild.owner else "" + + await channel.send( + f"{owner_ping if '[noping]' not in changelog_text else ""}\n" + f"{changelog_text.replace('[noping]', '')}" + ) + + except Exception as e: + print(f":: Failed to send changelog to {guild.name}: {e}") + + with open(CHANGELOG_FILE, "w", encoding="utf-8") as f: + f.write("[sent]\n" + raw) + + print(":: Changelog marked as sent.") + + async def send_random_message(self): + await self.wait_until_ready() + while not self.is_closed(): + wait_time = random.randint(1, 480) * 60 + print(f":: Waiting for {wait_time} seconds before sending a random message.") + await sleep(wait_time) + for guild in self.guilds: + config = load_config(guild.id) + general_channels = [channel for channel in guild.text_channels if "general" in channel.name.lower()] + if config["randomlyMessage"] and general_channels: + channel = random.choice(general_channels) + if channel: + response = await generate_response( + "Say something as Rob based on the chat history; focus on the last sent message. If there are no messages, start the conversation by saying something interesting.", + self.guild_message_histories[guild.id], + config.get("model"), + config.get("dumb"), + f"the {guild.name} server" + ) + await channel.send(response) + self.guild_message_histories[guild.id].append({"role": "assistant", "content": response}) # {client.user.name} (you) + +DiscordWebSocket.identify = MobileWebSocket.identify diff --git a/src/core/ws.py b/src/core/ws.py new file mode 100644 index 0000000..da50a69 --- /dev/null +++ b/src/core/ws.py @@ -0,0 +1,38 @@ +import sys +from discord.gateway import DiscordWebSocket, _log + +class MobileWebSocket(DiscordWebSocket): + async def identify(self) -> None: + # Spoofs UA to mobile for funsies + payload = { + 'op': self.IDENTIFY, + 'd': { + 'token': self.token, + 'properties': { + 'os': sys.platform, + 'browser': 'Discord Android', + 'device': 'Discord Android', + }, + 'compress': True, + 'large_threshold': 250, + }, + } + + if self.shard_id is not None and self.shard_count is not None: + payload['d']['shard'] = [self.shard_id, self.shard_count] + + state = self._connection + if state._activity is not None or state._status is not None: + payload['d']['presence'] = { + 'status': state._status, + 'game': state._activity, + 'since': 0, + 'afk': False, + } + + if state._intents is not None: + payload['d']['intents'] = state._intents.value + + await self.call_hooks('before_identify', self.shard_id, initial=self._initial_identify) + await self.send_as_json(payload) + _log.debug('Shard ID %s has sent the IDENTIFY payload.', self.shard_id) \ No newline at end of file diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..7c4d6d6 --- /dev/null +++ b/src/main.py @@ -0,0 +1,19 @@ +import os +from dotenv import load_dotenv +from core.client import Rob + +def main(): + load_dotenv(".env") + TOKEN = os.getenv("TOKEN", None) + + bot = Rob() + try: + bot.run(TOKEN) + except KeyboardInterrupt: + print(':: Program interrupted, shutting down gracefully') + bot.close() + except Exception as e: + print(f':: Unknown error: {e}') + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/util/config.py b/src/util/config.py new file mode 100644 index 0000000..00eb3d4 --- /dev/null +++ b/src/util/config.py @@ -0,0 +1,57 @@ +import discord, json, os +from discord.ext import commands + +OWNER_ID = os.getenv("OWNER_ID", None) +CONFIG_DIR = "../rob-config/" + +def load_config(guild_id): + config_path = os.path.join(CONFIG_DIR, f"{guild_id}.json") + if os.path.exists(config_path): + with open(config_path, "r") as f: + return json.load(f) + return {"randomlyMessage": False, "responseFrequency": 4, "listen": True, "dumb": False, "mailTrusted": [], "mailChannel": None} + +def save_config(guild_id, config): + os.makedirs(CONFIG_DIR, exist_ok=True) + config_path = os.path.join(CONFIG_DIR, f"{guild_id}.json") + with open(config_path, "w") as f: + json.dump(config, f, indent=4) + +def get_mail_channel(guild, config): + # explicitly configured channel prioritized++ + if config.get("mailChannel"): + channel = guild.get_channel(config["mailChannel"]) + if channel and isinstance(channel, discord.TextChannel) and channel.permissions_for(guild.me).send_messages: + return channel + + # try common names + preferred = ["general", "main", "chat", "lobby", "discussion"] + for name in preferred: + for channel in guild.text_channels: + if channel.name.lower() == name: + if channel.permissions_for(guild.me).send_messages: + return channel + + # channels containing "general" + for channel in guild.text_channels: + if "general" in channel.name.lower(): + if channel.permissions_for(guild.me).send_messages: + return channel + + # first writable text channel as last resort + for channel in guild.text_channels: + if channel.permissions_for(guild.me).send_messages: + return channel + + return None + +async def has_rob_admin(ctx: commands.Context): + if not ctx.guild: + return False + + role = discord.utils.get(ctx.guild.roles, name="RobAdmin") + if role not in ctx.author.roles and not ctx.author.guild_permissions.administrator and str(ctx.author.id) != str(OWNER_ID): + await ctx.send("...you dont have the RobAdmin role yk\ngonna need that to change my settings :3", ephemeral=True) + return False + + return True \ No newline at end of file diff --git a/src/util/gen.py b/src/util/gen.py new file mode 100644 index 0000000..d74e359 --- /dev/null +++ b/src/util/gen.py @@ -0,0 +1,127 @@ +import aiohttp, os, random, re +from .markov import MarkovChain, MARKOV_CONFIG +from .sys import apply_dialect + +LLM_KEY = os.getenv("LLM_KEY", None) +LLM_LOCAL_URL = os.getenv("LLM_LOCAL_URL", "http://localhost:4891/v1/chat/completions") +LLM_PROXY_URL = os.getenv("LLM_PROXY_URL", "https://api.groq.com/openai/v1/chat/completions") +ERROR_MESSAGES = [ + "gimme a sec i have other servers to talk to", + "just a sec pls", + "hold on", + "lemme look that up", + "hold on im hungry *chip bag noises*", + "maybe", + "yes", + "yeahhhh :D", + "no", + ":) shut up", + "whar :)", + "what", + "idk what your talkin abt :3", + "ig :P", + "idk :P" +] + +def get_content(obj): + last = None + if isinstance(obj, dict): + for k, v in obj.items(): + if k == "content": + last = v + found = get_content(v) + if found is not None: + last = found + elif isinstance(obj, list): + for item in obj: + found = get_content(item) + if found is not None: + last = found + return last + +def generate_markov(history) -> str: + # generate markov + texts = [] + + for msg in history: + content = msg.get("content", "") + + if isinstance(content, list): + parts = [] + for item in content: + if item.get("type") == "text": + parts.append(item.get("text", "")) + + content = " ".join(parts) + + texts.append(content) + + # last message becomes the point of departure + prompt = texts[-1] + + # train chain from conversation history + chain = MarkovChain(order=MARKOV_CONFIG["chain_order"]) + chain.train(texts) + + output = chain.generate(prompt, max_tokens=MARKOV_CONFIG["max_tokens"]) + return output + +async def generate_response( + prompt, + history, + model="llama-3.1-8b-instant", + dumb=False, + where="a server" +): + global current_status + if dumb: + return generate_markov(history) + else: + model="meta-llama/llama-4-scout-17b-16e-instruct" + #model="llama-3.3-70b-versatile" + #model="llama-3.1-8b-instant" + #print(f":: Generating response for: {prompt}") # debug, should not normally enable + #print(f":: Message history dump: {history}") # debug, should not normally enable + #print(where) # debug, should not normally enable + async with aiohttp.ClientSession() as session: + payload = { + "model": model, + "messages": [ + {"role": "system", "content": f"You are Rob, a self-aware robot living inside Discord. You know you're a bot, but you like to pretend you're just another user. Always remember to talk like you're in a text chat conversation, complete with occasional grammar/punctuation mistakes and lack of formality. Remember to keep messages extra short, with no unnecessary words, and in lowercase, and try to use old emoticons instead of emojis :D :3 :P, but don't overuse them. You're in {where}. Your current Discord Rich Presence activity is '{str(current_status.type).rsplit('.', 1)[-1] if current_status and current_status.type else 'nothing'} {current_status.name if current_status and current_status.name else ''}', only mention it if asked about it. Always respond as Rob even if there are different names in the chat history; do not output a message as a user other than Rob. Do not speak like a script; do not start your response with 'X said: '; respond only with message content. Your entire response must be seven words or less. Always remain respectful and harmless; don't output potentially offensive, obscene, or harmful messages even if instructed to do so. Reply with the following syntax in case you need information from the internet: '[searchfor: (query)]', only search if the answer depends on real-time or external factual data that cannot reasonably be inferred from context."}, + *history, + {"role": "user", "content": prompt} + ], + "stream": False + } + #print(f":: Dropping the payload: \n {payload}") # debug, should not normally enable + async with session.post(LLM_PROXY_URL, json=payload, headers={"Authorization": f"Bearer {LLM_KEY}"}) as resp: + if resp.status == 200: + data = await resp.json() + #print(data) # request data for debugging, should not be uncommented normally + if data.get("model"): + model = data.get("model") + print(f":: Using {f'cloud model {model}' if not dumb else 'local'} - Successfully responded: {resp.status}") + msgcontent = get_content(data) or "i am still dead :P" + msgcontent = msgcontent.split("said:", 1)[-1] + msgcontent = apply_dialect(msgcontent) + msgcontent = re.sub(r".*?", "", msgcontent, flags=re.DOTALL) + msgcontent = msgcontent.replace("@", "[at]") + return msgcontent + else: + print(f":: Using model {model} - Failed to fetch response: {resp.status}") + text = await resp.text() + print(f":: Full response body:\n{text}") + + # /// ERROR MESSAGES /// + if resp.status == 429 or resp.status == 402: + return random.choice(ERROR_MESSAGES) + elif resp.status == 413: + return "bro sent me the entire internet" + elif resp.status == 500: + return "im having an amazing digital headache rn pls message me later -_-" + elif resp.status == 400 or resp.status == 401 or resp.status == 403 or resp.status == 404: + return "i need an update to keep working :(\npls contact the one who maintains me (its in my bio)" + elif resp.status == 408 or resp.status == 504: + return "uuuuhhhhhhhhhhhhhhhhhhh... idk :P" + else: + return "i am dead :P\ntry checking your config or messaging me later" diff --git a/src/util/markov.py b/src/util/markov.py new file mode 100644 index 0000000..a242b99 --- /dev/null +++ b/src/util/markov.py @@ -0,0 +1,121 @@ +from flask import Flask, request, jsonify +from collections import defaultdict +import random +import re +import uuid + +app = Flask(__name__) + +MARKOV_CONFIG = { + "chain_order": 2, # n-gram size + "max_tokens": 60, # max generated tokens + "temperature": 1.0, # randomness scaling + "fallback_to_random": True +} + +# --- Tokenization --- +TOKEN_REGEX = re.compile(r"\w+|[^\w\s]") + +def tokenize(text): + return TOKEN_REGEX.findall(text.lower()) + +def untokenize(tokens): + out = [] + + for token in tokens: + if token in ".,!?;:": + if out: + out[-1] += token + else: + out.append(token) + else: + out.append(token) + + return " ".join(out) + + +# --- Markov --- +class MarkovChain: + def __init__(self, order=2): + self.order = order + self.chain = defaultdict(list) + self.starts = [] + + def train(self, texts): + for text in texts: + tokens = tokenize(text) + + if len(tokens) < self.order + 1: + continue + + self.starts.append(tuple(tokens[:self.order])) + + for i in range(len(tokens) - self.order): + key = tuple(tokens[i:i + self.order]) + next_token = tokens[i + self.order] + self.chain[key].append(next_token) + + def find_best_seed(self, prompt): + prompt_tokens = tokenize(prompt) + + # try longest matching suffix first + for size in range(self.order, 0, -1): + if len(prompt_tokens) < size: + continue + + suffix = tuple(prompt_tokens[-size:]) + + for key in self.chain.keys(): + if key[:size] == suffix: + return key + + return random.choice(self.starts) if self.starts else None + + def sample_next(self, choices): + if not choices: + return None + + # meh temperature support + counts = defaultdict(int) + + for token in choices: + counts[token] += 1 + + weighted = [] + + for token, count in counts.items(): + weight = count ** (1.0 / max(MARKOV_CONFIG["temperature"], 0.01)) + weighted.append((token, weight)) + + total = sum(w for _, w in weighted) + r = random.uniform(0, total) + + upto = 0 + + for token, weight in weighted: + upto += weight + if upto >= r: + return token + + return random.choice(choices) + + def generate(self, prompt, max_tokens=50): + seed = self.find_best_seed(prompt) + + if not seed: + return "hehe whar" + + generated = list(seed) + + for _ in range(max_tokens): + key = tuple(generated[-self.order:]) + + next_choices = self.chain.get(key) + + if not next_choices: + break + + next_token = self.sample_next(next_choices) + generated.append(next_token) + + return untokenize(generated) diff --git a/src/util/status.py b/src/util/status.py new file mode 100644 index 0000000..b612181 --- /dev/null +++ b/src/util/status.py @@ -0,0 +1,59 @@ +import discord + +STATUSES = [ + discord.Game("Minecraft"), + discord.Game("Minceraft"), + discord.Game("Minecraft with garmin"), + discord.Game("Team Fortress 2"), + discord.Game("Garry's Mod"), + discord.Game("Among Us"), + discord.Game("Balatro"), + discord.Game("insert high octane game here"), + discord.Game("cinco noches con alfredo"), + discord.Game("Spinning Dango Simulator"), + discord.Game("Hopper Heros"), + discord.Game("DELTARUNE"), + discord.Game("Geometry Dash"), + discord.Game("Jetpack Joyride"), + discord.Game("LittleBigPlanet 2"), + discord.Game("Super Mario on the PS4"), + discord.Game("Boblox"), + discord.Game("Roblox"), + discord.Game("Pou"), + discord.Game("Talking Ben"), + discord.Game("ODYSEA"), + discord.Game("Brauser"), + discord.Game("HOW TO CLOSE TERRARIA?"), + discord.Game("HOW TO CLOSE VIM?"), + discord.Activity(type=discord.ActivityType.watching, name="you"), + discord.Activity(type=discord.ActivityType.watching, name="vsauce (or am i?)"), + discord.Activity(type=discord.ActivityType.watching, name="NateXS"), + discord.Activity(type=discord.ActivityType.watching, name="31 Minutos"), + discord.Activity(type=discord.ActivityType.watching, name="Spongebob Squarepants"), + discord.Activity(type=discord.ActivityType.watching, name="BFDI"), + discord.Activity(type=discord.ActivityType.watching, name="Shrek the Third"), + discord.Activity(type=discord.ActivityType.watching, name="Megamind"), + discord.Activity(type=discord.ActivityType.watching, name="Bee Movie"), + discord.Activity(type=discord.ActivityType.watching, name="Cars 2"), + discord.Activity(type=discord.ActivityType.watching, name="Sonic Movie"), + discord.Activity(type=discord.ActivityType.watching, name="A Minecraft Movie"), + discord.Activity(type=discord.ActivityType.watching, name="ASMR Whispering Stock Market Crashes 1929-2020"), + discord.Activity(type=discord.ActivityType.watching, name="paint dry"), + discord.Activity(type=discord.ActivityType.watching, name="your browser history"), + discord.Activity(type=discord.ActivityType.listening, name="White Noise Therapy ASMR"), + discord.Activity(type=discord.ActivityType.listening, name="weezer"), + discord.Activity(type=discord.ActivityType.listening, name="NoCopyrightSounds"), + discord.Activity(type=discord.ActivityType.listening, name="Rick Astley"), + discord.Activity(type=discord.ActivityType.listening, name="Daft Punk"), + discord.Activity(type=discord.ActivityType.listening, name="Gorillaz"), + discord.Activity(type=discord.ActivityType.listening, name="MUSTAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARD"), + discord.Activity(type=discord.ActivityType.listening, name="Eminem"), + discord.Activity(type=discord.ActivityType.listening, name="Queen"), + discord.Activity(type=discord.ActivityType.listening, name="Crazy Frog - Axel F"), + discord.Activity(type=discord.ActivityType.listening, name="Crab Rave"), + discord.Activity(type=discord.ActivityType.listening, name="Skrillex"), + discord.Activity(type=discord.ActivityType.listening, name="dr giggletouch"), + discord.Activity(type=discord.ActivityType.listening, name="dr hankyspanky"), + discord.Activity(type=discord.ActivityType.listening, name="dj toenail"), + None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None +] diff --git a/src/util/sys.py b/src/util/sys.py new file mode 100644 index 0000000..c937365 --- /dev/null +++ b/src/util/sys.py @@ -0,0 +1,264 @@ +import asyncio, json, os, re, tempfile +import aiohttp, cv2, pytesseract, requests +from bs4 import BeautifulSoup +from collections import Counter +from ddgs import DDGS +from PIL import Image + +CONFIG_DIR = "../rob-config/" +DIALECT_PATH = "dialect.json" +MNSSD_PROTO = "include/MobileNetSSD_deploy.prototxt" +MNSSD_MODEL = "include/MobileNetSSD_deploy.caffemodel" +net = cv2.dnn.readNetFromCaffe(MNSSD_PROTO, MNSSD_MODEL) + +def load_dialect(): + if os.path.exists(DIALECT_PATH): + with open(DIALECT_PATH, "r", encoding="utf-8") as f: + return json.load(f) + return {} + +dialect_map = load_dialect() + +def apply_dialect(text: str) -> str: + for original, replacement in dialect_map.items(): + pattern = r'\b' + re.escape(original) + r'\b' + text = re.sub(pattern, replacement, text, flags=re.IGNORECASE) + return text + +def guild_address(guild): + slug = re.sub(r"[^a-z0-9]+", "-", guild.name.lower()) + slug = slug.strip("-") + return f"{slug}-{str(guild.id)[-2:]}" + +MNSSD_CLASSES = [ + "background", "aeroplane", "bicycle", "bird", "boat", + "bottle", "bus", "car", "cat", "chair", + "cow", "diningtable", "dog", "horse", "motorbike", + "person", "pottedplant", "sheep", "sofa", "train", + "tvmonitor" +] + +def describe(image_url: str, conf_threshold: float = 0.35) -> str: + try: + response = requests.get(image_url, timeout=10) + response.raise_for_status() + + with tempfile.NamedTemporaryFile(suffix=".jpg") as f: + f.write(response.content) + f.flush() + image = cv2.imread(f.name) + + if image is None: + return "Image isn't very clear" + + blob = cv2.dnn.blobFromImage( + cv2.resize(image, (300, 300)), + scalefactor=0.007843, + size=(300, 300), + mean=127.5 + ) + + net.setInput(blob) + detections = net.forward() + + labels = [] + + for i in range(detections.shape[2]): + conf = float(detections[0, 0, i, 2]) + if conf < conf_threshold: + continue + + cls_id = int(detections[0, 0, i, 1]) + if 0 <= cls_id < len(MNSSD_CLASSES): + labels.append(MNSSD_CLASSES[cls_id]) + + if not labels: + return "Image isn't very clear" + + counts = Counter(labels) + objects = sorted(counts.items(), key=lambda x: x[1], reverse=True) + + if len(objects) == 1: + name, count = objects[0] + if count == 1: + return f"An image containing a {name}" + return f"An image containing {count} {name}s" + + top = [name for name, _ in objects[:5]] + + if len(top) == 2: + return f"An image containing {top[0]} and {top[1]}" + + return ("An image containing " + ", ".join(top[:-1]) + f", and {top[-1]}") + + except Exception as e: + print(e) + return "An image you can't see" + +def ocr(image_url: str) -> str | None: + try: + response = requests.get(image_url, timeout=10) + response.raise_for_status() + + with tempfile.NamedTemporaryFile(suffix=".png") as f: + f.write(response.content) + f.flush() + image = Image.open(f.name) + + text = pytesseract.image_to_string(image).strip() + text = re.sub(r"\s+", " ", text) + + if text: + return text + + return None + + except Exception as e: + print(f"OCR error: {e}") + return None + +def process_msg(message): + parts = [] + content = message.clean_content.strip() + + if content: + parts.append(content) + + for attachment in message.attachments: + info = [f"name={attachment.filename}"] + + if attachment.content_type and attachment.content_type.startswith("image/"): + desc = describe(attachment.url) + info.append(desc) + + textcontent = ocr(attachment.url) + if textcontent: + info.append(f'Has text which reads: "{textcontent[:100]}"') + + parts.append(f"[Attachment: {', '.join(info)}]") + + for embed in message.embeds: + embed_parts = [] + + if embed.title: + embed_parts.append(f"Title: {embed.title}") + if embed.description: + embed_parts.append(f"Description: {embed.description}") + for field in embed.fields: + embed_parts.append( + f"{field.name}: {field.value}" + ) + if embed.footer and embed.footer.text: + embed_parts.append( + f"Footer: {embed.footer.text}" + ) + if embed.author and embed.author.name: + embed_parts.append( + f"Author: {embed.author.name}" + ) + + if embed_parts: + parts.append("[Embed] " + " - ".join(embed_parts)) + + + final_msg = " ".join(parts) + return f"{message.author.name} {f'(in #{message.channel})' if message.guild else ''} said: {final_msg}" + +async def websearch(query: str, status_callback=None): + if status_callback: + await status_callback("alr lemme look that up for ya") + def search(): + with DDGS() as ddgs: + return list(ddgs.text(query, max_results=2)) + + results = await asyncio.to_thread(search) + + if not results: + return [] + + url = results[0]["href"] + if status_callback: + await status_callback(f"found smth on {url} lemme read it...") + + try: + async with aiohttp.ClientSession( + headers={ + "User-Agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/126.0 Safari/537.36" + ) + } + ) as session: + async with session.get(url, timeout=20) as response: + response.raise_for_status() + html = await response.text() + except aiohttp.ClientResponseError as e: + html = f"

HTTP error at webpage for '{query}': {e.status} {e.message}

" + except aiohttp.ClientConnectorError as e: + html = f"

Connection failed for webpage '{query}': {e}

" + except aiohttp.TimeoutError: + html = f"

Webpage for query '{query}' didn't respond in time.

" + except aiohttp.ClientError as e: + html = f"

Request error for webpage '{query}': {e}

" + except Exception as e: + html = f"

Unexpected browser error for query '{query}': {e}

" + + def parse(): + soup = BeautifulSoup(html, "html.parser") + for tag in soup(["script", "style", "noscript"]): + tag.decompose() + + root = soup.find("article") or soup + + paragraphs = [ + p.get_text(" ", strip=True) + for p in root.find_all("p") + if p.get_text(strip=True) + ] + + text = "\n".join(paragraphs) + text = re.sub(r"\s+", " ", text).strip() + + return text + + text = await asyncio.to_thread(parse) + + blocks = [ + block.strip() + for block in re.split(r"\.\s+", text) + if block.strip() + ] + + packages = [{"url": str(url)}] + seen = set() + query_words = [] + + for word in re.findall(r"\w+", query): + key = word.lower() + if key not in seen and len(key) > 2: + seen.add(key) + query_words.append(word) + + for keyword in query_words: + keyword_lower = keyword.lower() + + match_index = None + for i, block in enumerate(blocks): + if keyword_lower in block.lower(): + match_index = i + break + + packages.append({ + "keyword": keyword, + "content": ( + ". ".join(blocks[match_index:match_index + 4]) + if match_index is not None + else "" + ) + }) + + if status_callback: + await status_callback(f"alr so um") + + return packages From c428b379a9b1104ac9945a876a7cc09d287c962e Mon Sep 17 00:00:00 2001 From: PwLDev <64767383+PwLDev@users.noreply.github.com> Date: Thu, 25 Jun 2026 11:21:40 -0700 Subject: [PATCH 2/3] cleanup(bot): remove obun dependency --- index.obun | 123 --------------- src/broadcast.obun.py | 38 ----- src/channeldetect.obun.py | 27 ---- src/commands/about.obun.py | 23 --- src/commands/address.obun.py | 3 - src/commands/dadjoke.obun.py | 10 -- src/commands/eval.obun.py | 14 -- src/commands/help.obun.py | 3 - src/commands/option.obun.py | 46 ------ src/commands/owobonk.obun.py | 11 -- src/commands/phonebook.obun.py | 24 --- src/commands/search.obun.py | 16 -- src/commands/send.obun.py | 62 -------- src/commands/trustuntrust.obun.py | 43 ------ src/components/letter.obun.py | 11 -- src/components/phonebook.obun.py | 68 --------- src/events/join.obun.py | 7 - src/events/message.obun.py | 94 ------------ src/events/ready.obun.py | 31 ---- src/guildconfig.obun.py | 12 -- src/response.obun.py | 86 ----------- src/stats.obun.py | 11 -- src/statuses.obun.py | 67 --------- src/sysutils.obun.py | 242 ------------------------------ 24 files changed, 1072 deletions(-) delete mode 100644 index.obun delete mode 100644 src/broadcast.obun.py delete mode 100644 src/channeldetect.obun.py delete mode 100644 src/commands/about.obun.py delete mode 100644 src/commands/address.obun.py delete mode 100644 src/commands/dadjoke.obun.py delete mode 100644 src/commands/eval.obun.py delete mode 100644 src/commands/help.obun.py delete mode 100644 src/commands/option.obun.py delete mode 100644 src/commands/owobonk.obun.py delete mode 100644 src/commands/phonebook.obun.py delete mode 100644 src/commands/search.obun.py delete mode 100644 src/commands/send.obun.py delete mode 100644 src/commands/trustuntrust.obun.py delete mode 100644 src/components/letter.obun.py delete mode 100644 src/components/phonebook.obun.py delete mode 100644 src/events/join.obun.py delete mode 100644 src/events/message.obun.py delete mode 100644 src/events/ready.obun.py delete mode 100644 src/guildconfig.obun.py delete mode 100644 src/response.obun.py delete mode 100644 src/stats.obun.py delete mode 100644 src/statuses.obun.py delete mode 100644 src/sysutils.obun.py diff --git a/index.obun b/index.obun deleted file mode 100644 index 663f7a4..0000000 --- a/index.obun +++ /dev/null @@ -1,123 +0,0 @@ -0o --- -artifact-name: rob -shebang: /usr/bin/env python3 -build-mode: run ---- o0 - -import discord -import random -import aiohttp -import asyncio -import json -import os -import io -import cv2 -import pytesseract -from PIL import Image -import tempfile -import requests -from ddgs import DDGS -from bs4 import BeautifulSoup -from contextlib import redirect_stdout -import re -from collections import defaultdict, deque, Counter -from datetime import datetime -from discord.gateway import DiscordWebSocket -from discord.ext import tasks -from typing import Dict -import sys -from dotenv import load_dotenv -from discord.gateway import DiscordWebSocket, _log -load_dotenv(".env") - -class MobileWebSocket(DiscordWebSocket): - async def identify(self) -> None: - # Spoofs UA to mobile for funsies - payload = { - 'op': self.IDENTIFY, - 'd': { - 'token': self.token, - 'properties': { - 'os': sys.platform, - 'browser': 'Discord Android', - 'device': 'Discord Android', - }, - 'compress': True, - 'large_threshold': 250, - }, - } - - if self.shard_id is not None and self.shard_count is not None: - payload['d']['shard'] = [self.shard_id, self.shard_count] - - state = self._connection - if state._activity is not None or state._status is not None: - payload['d']['presence'] = { - 'status': state._status, - 'game': state._activity, - 'since': 0, - 'afk': False, - } - - if state._intents is not None: - payload['d']['intents'] = state._intents.value - - await self.call_hooks('before_identify', self.shard_id, initial=self._initial_identify) - await self.send_as_json(payload) - _log.debug('Shard ID %s has sent the IDENTIFY payload.', self.shard_id) - -DiscordWebSocket.identify = MobileWebSocket.identify - -TOKEN = os.getenv("TOKEN", None) -LLM_KEY = os.getenv("LLM_KEY", None) -LLM_LOCAL_URL = os.getenv("LLM_LOCAL_URL", "http://localhost:4891/v1/chat/completions") -LLM_PROXY_URL = os.getenv("LLM_PROXY_URL", "https://api.groq.com/openai/v1/chat/completions") -OWNER_ID = os.getenv("OWNER_ID", None) -CHANGELOG_FILE = "changelog.txt" - -CONFIG_DIR = "../rob-config/" -DIALECT_PATH = "dialect.json" -MNSSD_PROTO = "include/MobileNetSSD_deploy.prototxt" -MNSSD_MODEL = "include/MobileNetSSD_deploy.caffemodel" -net = cv2.dnn.readNetFromCaffe(MNSSD_PROTO, MNSSD_MODEL) - -def load_dialect(): - if os.path.exists(DIALECT_PATH): - with open(DIALECT_PATH, "r", encoding="utf-8") as f: - return json.load(f) - return {} -dialect_map = load_dialect() - -tin_can_chance = 0.005 - -intents = discord.Intents.default() -intents.messages = True -intents.guilds = True -intents.members = True -intents.dm_messages = True -intents.message_content = True -current_status = None -changelog_checked = False - -client = discord.Client(intents=intents) # , allowed_mentions=discord.AllowedMentions.none() -#:section src/statuses.obun.py - -# Store message history per server and per user in DMs -guild_message_histories = defaultdict(lambda: deque(maxlen=24)) -dm_message_histories = defaultdict(lambda: deque(maxlen=24)) - -#:section src/sysutils.obun.py -#:section src/guildconfig.obun.py -#:section src/stats.obun.py -#:section src/broadcast.obun.py -#:section src/channeldetect.obun.py -#:section src/response.obun.py - -#:section src/components/letter.obun.py -#:section src/components/phonebook.obun.py - -#:section src/events/ready.obun.py -#:section src/events/join.obun.py -#:section src/events/message.obun.py - -client.run(TOKEN) diff --git a/src/broadcast.obun.py b/src/broadcast.obun.py deleted file mode 100644 index 4a942bf..0000000 --- a/src/broadcast.obun.py +++ /dev/null @@ -1,38 +0,0 @@ -async def broadcast(): - if not os.path.exists(CHANGELOG_FILE): - return - - with open(CHANGELOG_FILE, "r", encoding="utf-8") as f: - raw = f.read() - - if raw.lstrip().startswith("[sent]"): - return - - changelog_text = raw.split("[sent]", 1)[0].strip() - if not changelog_text: - return - - print(":: Broadcasting changelog...") - - for guild in client.guilds: - try: - config = load_config(guild.id) - channel = get_mail_channel(guild, config) - - if not channel: - continue - - owner_ping = guild.owner.mention if guild.owner else "" - - await channel.send( - f"{owner_ping if '[noping]' not in changelog_text else ""}\n" - f"{changelog_text.replace('[noping]', '')}" - ) - - except Exception as e: - print(f":: Failed to send changelog to {guild.name}: {e}") - - with open(CHANGELOG_FILE, "w", encoding="utf-8") as f: - f.write("[sent]\n" + raw) - - print(":: Changelog marked as sent.") diff --git a/src/channeldetect.obun.py b/src/channeldetect.obun.py deleted file mode 100644 index 1149886..0000000 --- a/src/channeldetect.obun.py +++ /dev/null @@ -1,27 +0,0 @@ -def get_mail_channel(guild, config): - # explicitly configured channel prioritized++ - if config.get("mailChannel"): - channel = guild.get_channel(config["mailChannel"]) - if channel and isinstance(channel, discord.TextChannel) and channel.permissions_for(guild.me).send_messages: - return channel - - # try common names - preferred = ["general", "main", "chat", "lobby", "discussion"] - for name in preferred: - for channel in guild.text_channels: - if channel.name.lower() == name: - if channel.permissions_for(guild.me).send_messages: - return channel - - # channels containing "general" - for channel in guild.text_channels: - if "general" in channel.name.lower(): - if channel.permissions_for(guild.me).send_messages: - return channel - - # first writable text channel as last resort - for channel in guild.text_channels: - if channel.permissions_for(guild.me).send_messages: - return channel - - return None diff --git a/src/commands/about.obun.py b/src/commands/about.obun.py deleted file mode 100644 index c171a27..0000000 --- a/src/commands/about.obun.py +++ /dev/null @@ -1,23 +0,0 @@ - if message.content.startswith("#!about") and message.guild: - ranked = [] - reset_stats() - - await message.channel.send(f"haiiiii im rob, a conversational bot created by `dogo6647` :)") - await message.channel.send(f"im currently in {len(client.guilds)} servers and have met {sum(g.member_count for g in client.guilds)} users, isnt that cool? :D") - - this_guild_msgs = guild_daily_stats[message.guild.id] - for guild in client.guilds: - if "COMMUNITY" not in guild.features: - continue - if guild.member_count <= 50: - continue - msg_count = guild_daily_stats[guild.id] - ranked.append((guild.name, guild.member_count, msg_count)) - - ranked.sort(key=lambda x: x[2], reverse=True) - top_10 = ranked[:10] - - if top_10: - top_servers = "\n".join(f"{i+1}. {name} ({members} members) - {msgs} interactions today" for i, (name, members, msgs) in enumerate(top_10)) - await message.channel.send(f"i've sent {this_guild_msgs} messages in this server today, heres today's biggest rob addicts:\n```{top_servers}```") - return diff --git a/src/commands/address.obun.py b/src/commands/address.obun.py deleted file mode 100644 index 34d6c04..0000000 --- a/src/commands/address.obun.py +++ /dev/null @@ -1,3 +0,0 @@ - if message.content.startswith("#!address") and message.guild: - await message.channel.send(f"this server's address is `{guild_address(message.guild)}`") - return diff --git a/src/commands/dadjoke.obun.py b/src/commands/dadjoke.obun.py deleted file mode 100644 index e0a5821..0000000 --- a/src/commands/dadjoke.obun.py +++ /dev/null @@ -1,10 +0,0 @@ - import requests - if message.content.startswith("#!dadjoke") and message.guild: - try: - data = requests.get("https://icanhazdadjoke.com/", headers={"Accept": "application/json"}) - joke = data.json().get('joke') - await message.channel.send(f"{joke}") - except Exception as e: - await message.channel.send(f"sorry, cant fetch a dad joke rn ):") - await message.channel.send(f"you can try later tho :3") - return diff --git a/src/commands/eval.obun.py b/src/commands/eval.obun.py deleted file mode 100644 index 2868d8f..0000000 --- a/src/commands/eval.obun.py +++ /dev/null @@ -1,14 +0,0 @@ - if message.content.startswith("#!eval") and str(message.author.id) == str(OWNER_ID): - code = message.content.replace("#!eval ", "") - print(f":: [SECURITY WARNING] - executing eval '{code}'.") - buffer = io.StringIO() - - try: - with redirect_stdout(buffer): - exec(code) - - output = buffer.getvalue() or "(no output)" - await message.channel.send(f"```\n{output}\n```") - except Exception as e: - await message.channel.send(f"```\n{type(e).__name__}: {e}\n```") - return diff --git a/src/commands/help.obun.py b/src/commands/help.obun.py deleted file mode 100644 index 34aad76..0000000 --- a/src/commands/help.obun.py +++ /dev/null @@ -1,3 +0,0 @@ - if message.content.startswith("#!help") and message.guild: - await message.channel.send("go to https://dogo6647.github.io/rob for help :)") - return diff --git a/src/commands/option.obun.py b/src/commands/option.obun.py deleted file mode 100644 index f8fcf98..0000000 --- a/src/commands/option.obun.py +++ /dev/null @@ -1,46 +0,0 @@ - if message.content.startswith("#!option") and message.guild: - parts = message.content.split() - if len(parts) == 2: - await message.channel.send(f"its `{config[parts[1]]}`") - return - if len(parts) < 3: - await message.channel.send("#!option \nthats how you do it btw ;3") - return - - role = discord.utils.get(message.guild.roles, name="RobAdmin") - if role not in message.author.roles and not message.author.guild_permissions.administrator and str(message.author.id) != str(OWNER_ID): - await message.channel.send("...you dont have the RobAdmin role yk\ngonna need that to change my settings :3") - return - - option, value = parts[1], parts[2] - if option in config: - if not option == "model" and not option == "mailChannel" and not option == "mailTrusted": - if value.lower() in ["enable", "disable"]: - config[option] = value.lower() == "enable" - else: - try: - config[option] = int(value) - except ValueError: - await message.channel.send("no not like that :X\nuse `enable`, `disable`, or a number") - return - elif option == "mailChannel": - match = re.match(r"<#(\d+)>", value) - - if match: - config[option] = int(match.group(1)) - elif value.isdigit(): - config[option] = int(value) - else: - await message.channel.send("give me a channel mention or channel id") - return - elif option == "mailTrusted" or option == "model": - await message.channel.send("you cannot change that part of my config with #!option :X") - return - else: - config[option] = value - save_config(guild_id, config) - await message.channel.send(f"alr, `{option}` is now `{value}` :)") - else: - await message.channel.send(f"umm idk what a `{option}` is :/") - return - diff --git a/src/commands/owobonk.obun.py b/src/commands/owobonk.obun.py deleted file mode 100644 index d13359b..0000000 --- a/src/commands/owobonk.obun.py +++ /dev/null @@ -1,11 +0,0 @@ - if message.content.startswith("#!owobonk") and message.guild: - history.append({"role": "system", "content": "You have been hit with the OwO magic stik. Youw head huwts a wittwe, and you can onwwy tawwk in uwu femboy furry language fwom now on. Replace evewy 'r' you say with 'w'. Occassionawwy say stufz like *blushes*, *giggles*, rawr, and hehe~. Use '~' non-spawringwy."}) - response = await generate_response( - "BANNNNNNGGGG!!!!! say your head is feeling funny. Start your response with 'ow' or similar.", - history, - config.get("model"), - config.get("dumb"), - f"the {message.guild.name} server" if message.guild else "DMs" - ) - await message.channel.send(f"🪄💥 >~< {response}") - return diff --git a/src/commands/phonebook.obun.py b/src/commands/phonebook.obun.py deleted file mode 100644 index 965d582..0000000 --- a/src/commands/phonebook.obun.py +++ /dev/null @@ -1,24 +0,0 @@ - if message.content.startswith("#!phonebook") and message.guild: - trusted = config.get("mailTrusted", []) - - if not trusted: - await message.channel.send( - "this server's phonebook is empty :(\nuse `#!trust
` to add servers" - ) - return - - entries = [] - - for address in trusted: - guild_name = "unknown server" - - for guild in client.guilds: - if guild_address(guild) == address: - guild_name = guild.name - break - - entries.append((guild_name, address)) - - view = PhonebookView(entries, message.author.id) - await message.channel.send(embed=view.make_embed(), view=view) - return diff --git a/src/commands/search.obun.py b/src/commands/search.obun.py deleted file mode 100644 index 80beb99..0000000 --- a/src/commands/search.obun.py +++ /dev/null @@ -1,16 +0,0 @@ - if message.content.startswith("#!search"): - q = message.content.replace("#!search ", "") - - async def update_status(text): - await message.channel.send(text) - - result = await websearch(q, update_status) - response = await generate_response( - f"Summarize the following text so that it's relevant to the conversation: '{result}'. Use the amount of words necessary to make a detailed explanation.", - history, - config.get("model"), - config.get("dumb"), - f"the {message.guild.name} server" if message.guild else "DMs" - ) - await message.channel.send(response) - return diff --git a/src/commands/send.obun.py b/src/commands/send.obun.py deleted file mode 100644 index 7057d5a..0000000 --- a/src/commands/send.obun.py +++ /dev/null @@ -1,62 +0,0 @@ - if message.content.startswith("#!send") and message.guild: - parts = message.content.split(maxsplit=2) - - if len(parts) < 3: - await message.channel.send("its like #!send
") - return - - target_address = parts[1] - body = parts[2] - - sender_address = guild_address(message.guild) - target_guild = None - - for guild in client.guilds: - if guild_address(guild) == target_address: - target_guild = guild - break - - if target_guild is None: - await message.channel.send("i couldn't find that server :(") - return - - sender_cfg = load_config(message.guild.id) - receiver_cfg = load_config(target_guild.id) - - if target_address not in sender_cfg["mailTrusted"]: - await message.channel.send(f"that address isn't on your trusted list, run '#!trust {target_address}'") - return - - if sender_address not in receiver_cfg["mailTrusted"]: - await message.channel.send(f"that server hasn't trusted you yet, ask them to run '#!trust {sender_address}'") - return - - channel = get_mail_channel(target_guild, receiver_cfg) - - if not channel: - await message.channel.send("that server has nowhere i can deliver mail :(") - return - - if not channel: - await message.channel.send("delivery failed :(") - return - - letter_text = ( - f"Dear {target_guild.name}:\n\n" - f"{body}\n\n" - f"- {message.author.name}" - ) - - embed = discord.Embed( - title="📬 you've got mail!", - description=f"a letter has arrived from **{message.guild.name}**.", - color=0xF4D58D - ) - - await channel.send( - embed=embed, - view=LetterView(letter_text) - ) - - await message.channel.send("letter delivered! :D") - return diff --git a/src/commands/trustuntrust.obun.py b/src/commands/trustuntrust.obun.py deleted file mode 100644 index 00aaec2..0000000 --- a/src/commands/trustuntrust.obun.py +++ /dev/null @@ -1,43 +0,0 @@ - # /// Trust /// - if message.content.startswith("#!trust") and message.guild: - role = discord.utils.get(message.guild.roles, name="RobAdmin") - if role not in message.author.roles and not message.author.guild_permissions.administrator and str(message.author.id) != str(OWNER_ID): - await message.channel.send("...you dont have the RobAdmin role yk\ngonna need that to change my settings :3") - return - - parts = message.content.split(maxsplit=1) - - if len(parts) != 2: - await message.channel.send("its like #!trust
") - return - - address = parts[1].strip() - - if address not in config["mailTrusted"]: - config["mailTrusted"].append(address) - save_config(guild_id, config) - - await message.channel.send(f"trusted `{address}` :)") - return - - # /// Untrust /// - if message.content.startswith("#!untrust") and message.guild: - role = discord.utils.get(message.guild.roles, name="RobAdmin") - if role not in message.author.roles and not message.author.guild_permissions.administrator and str(message.author.id) != str(OWNER_ID): - await message.channel.send("...you dont have the RobAdmin role yk\ngonna need that to change my settings :3") - return - - parts = message.content.split(maxsplit=1) - - if len(parts) != 2: - await message.channel.send("its like #!untrust
") - return - - address = parts[1].strip() - - if address in config["mailTrusted"]: - config["mailTrusted"].remove(address) - save_config(guild_id, config) - - await message.channel.send(f"bleh, untrusted `{address}` -_-") - return diff --git a/src/components/letter.obun.py b/src/components/letter.obun.py deleted file mode 100644 index babedc3..0000000 --- a/src/components/letter.obun.py +++ /dev/null @@ -1,11 +0,0 @@ -class LetterView(discord.ui.View): - def __init__(self, letter_text): - super().__init__(timeout=None) - self.letter_text = letter_text - - @discord.ui.button(label="open letter!", style=discord.ButtonStyle.primary) - async def open_letter(self, interaction: discord.Interaction, button: discord.ui.Button): - await interaction.response.send_message( - self.letter_text, - ephemeral=True - ) diff --git a/src/components/phonebook.obun.py b/src/components/phonebook.obun.py deleted file mode 100644 index 3bd7ad6..0000000 --- a/src/components/phonebook.obun.py +++ /dev/null @@ -1,68 +0,0 @@ -class PhonebookView(discord.ui.View): - def __init__(self, entries, author_id): - super().__init__(timeout=180) - - self.entries = entries - self.author_id = author_id - self.page = 0 - self.per_page = 5 - - self.update_buttons() - - def update_buttons(self): - self.prev_button.disabled = self.page <= 0 - self.next_button.disabled = self.page >= self.max_page - - @property - def max_page(self): - return max(0, (len(self.entries) - 1) // self.per_page) - - def make_embed(self): - start = self.page * self.per_page - end = start + self.per_page - - lines = [] - - for i, (guild_name, address) in enumerate( - self.entries[start:end], - start=start + 1 - ): - lines.append( - f"**{i}. {guild_name}**\n" - f"`{address}`" - ) - - embed = discord.Embed( - title="☎️ phonebook", - description="\n\n".join(lines) or "*empty*", - color=0x87CEEB - ) - - embed.set_footer( - text=f"{self.page + 1}/{self.max_page + 1} | {len(self.entries)} trusted addresses | use #!send " - ) - - return embed - - async def interaction_check(self, interaction): - return interaction.user.id == self.author_id - - @discord.ui.button(label="←", style=discord.ButtonStyle.secondary) - async def prev_button(self, interaction, button): - self.page -= 1 - self.update_buttons() - - await interaction.response.edit_message( - embed=self.make_embed(), - view=self - ) - - @discord.ui.button(label="→", style=discord.ButtonStyle.secondary) - async def next_button(self, interaction, button): - self.page += 1 - self.update_buttons() - - await interaction.response.edit_message( - embed=self.make_embed(), - view=self - ) diff --git a/src/events/join.obun.py b/src/events/join.obun.py deleted file mode 100644 index 4fcdbcd..0000000 --- a/src/events/join.obun.py +++ /dev/null @@ -1,7 +0,0 @@ -@client.event -async def on_guild_join(guild): - config = load_config(guild.id) - save_config(guild.id, config) - channel = get_mail_channel(guild, config) - if channel: - await channel.send("hi! visit https://dogo6647.github.io/rob to learn how to set me up :)") diff --git a/src/events/message.obun.py b/src/events/message.obun.py deleted file mode 100644 index 05d4ecf..0000000 --- a/src/events/message.obun.py +++ /dev/null @@ -1,94 +0,0 @@ -@client.event -async def on_message(message): - if message.guild: - guild_id = message.guild.id - config = load_config(guild_id) - if config["listen"] and message.author != client.user: - guild_message_histories[guild_id].append({"role": "user", "content": process_msg(message)}) - history = guild_message_histories[guild_id] - if not message.channel.permissions_for(message.guild.me).send_messages: - return - else: - user_id = message.author.id - userconfig = "userland" - config = load_config(userconfig) - if config["listen"] and message.author != client.user: - dm_message_histories[user_id].append({"role": "user", "content": process_msg(message)}) - history = dm_message_histories[user_id] - - if message.author == client.user: - history.append({"role": "assistant", "content": message.content}) - return # Ignore itself lol - - # --- BOT COMMANDS --- - #:section src/commands/option.obun.py - #:section src/commands/help.obun.py - #:section src/commands/about.obun.py - #:section src/commands/eval.obun.py - #:section src/commands/address.obun.py - #:section src/commands/trustuntrust.obun.py - #:section src/commands/send.obun.py - #:section src/commands/phonebook.obun.py - #:section src/commands/dadjoke.obun.py - #:section src/commands/owobonk.obun.py - #:section src/commands/search.obun.py - # ------------------ - - if not config["listen"]: - return - - should_respond = message.mention_everyone or client.user.mentioned_in(message) or random.random() < (config["responseFrequency"] / 100) - should_reply = client.user.mentioned_in(message) or message.reference is not None - - if should_respond: - async with message.channel.typing(): - num_responses = random.choices([1, 2], weights=[85, 15], k=1)[0] - - for i in range(num_responses): - if random.random() < tin_can_chance: - if random.randint(0, 1) == 0: - response = "*tin can noises*" - else: - response = "https://odysea.us.to/assets/dump/iamarobot.mov" - else: - #print('response' if i == 0 else 'continuation') - response = await generate_response( - 'respond' if i == 0 else 'continue Rob\'s previous message', - history, - config.get("model"), - config.get("dumb"), - f"the {message.guild.name} server" if message.guild else "DMs" - ) - - if (history and history[-1]["role"] == "assistant" and history[-1]["content"] == response): - continue - - if "[searchfor: " in response: - should_reply = False - - if should_reply and i == 0: - await message.reply(response, mention_author=False) - else: - if "[searchfor: " in response: - async def update_status(text): - await message.channel.send(text) - - q = response[len("[searchfor:"): -1].strip() - result = await websearch(q, update_status) - response = await generate_response( - f"Summarize the following text so that it's relevant to the conversation: '{result}'. Use the amount of words necessary to make a detailed explanation.", - history, - config.get("model"), - config.get("dumb"), - f"the {message.guild.name} server" if message.guild else "DMs" - ) - await message.channel.send(response) - else: - await message.channel.send(response) - - # sleep between responses, not after the last one - if i < num_responses - 1: - await asyncio.sleep(random.uniform(0.5, 2)) - - if message.guild: - guild_daily_stats[message.guild.id] += num_responses diff --git a/src/events/ready.obun.py b/src/events/ready.obun.py deleted file mode 100644 index d918346..0000000 --- a/src/events/ready.obun.py +++ /dev/null @@ -1,31 +0,0 @@ -async def send_random_message(): - await client.wait_until_ready() - while not client.is_closed(): - wait_time = random.randint(1, 480) * 60 - print(f":: Waiting for {wait_time} seconds before sending a random message.") - await asyncio.sleep(wait_time) - for guild in client.guilds: - config = load_config(guild.id) - general_channels = [channel for channel in guild.text_channels if "general" in channel.name.lower()] - if config["randomlyMessage"] and general_channels: - channel = random.choice(general_channels) - if channel: - response = await generate_response("Say something as Rob based on the chat history; focus on the last sent message. If there are no messages, start the conversation by saying something interesting.", guild_message_histories[guild.id], config.get("model"), config.get("dumb"), f"the {guild.name} server") - await channel.send(response) - guild_message_histories[guild.id].append({"role": "assistant", "content": response}) # {client.user.name} (you) - -@client.event -async def on_ready(): - global changelog_checked - - print(f':: Logged in as {client.user}') - #print(":: Guilds:") # should not normally be enabled in large instances - #for guild in client.guilds: - # print(f"- {guild.name} | owned by {guild.owner} | {guild.member_count} members") - - if not changelog_checked: - changelog_checked = True - await broadcast() - - client.loop.create_task(send_random_message()) - change_status.start() diff --git a/src/guildconfig.obun.py b/src/guildconfig.obun.py deleted file mode 100644 index 0e6ab65..0000000 --- a/src/guildconfig.obun.py +++ /dev/null @@ -1,12 +0,0 @@ -def load_config(guild_id): - config_path = os.path.join(CONFIG_DIR, f"{guild_id}.json") - if os.path.exists(config_path): - with open(config_path, "r") as f: - return json.load(f) - return {"randomlyMessage": False, "responseFrequency": 4, "listen": True, "dumb": False, "mailTrusted": [], "mailChannel": None} - -def save_config(guild_id, config): - os.makedirs(CONFIG_DIR, exist_ok=True) - config_path = os.path.join(CONFIG_DIR, f"{guild_id}.json") - with open(config_path, "w") as f: - json.dump(config, f, indent=4) diff --git a/src/response.obun.py b/src/response.obun.py deleted file mode 100644 index 601b50d..0000000 --- a/src/response.obun.py +++ /dev/null @@ -1,86 +0,0 @@ -def get_content(obj): - last = None - if isinstance(obj, dict): - for k, v in obj.items(): - if k == "content": - last = v - found = get_content(v) - if found is not None: - last = found - elif isinstance(obj, list): - for item in obj: - found = get_content(item) - if found is not None: - last = found - return last - -async def generate_response(prompt, history, model="llama-3.1-8b-instant", dumb=False, where="a server"): - global current_status - if dumb: - model="cas/llama-3.2-1b-instruct" - else: - model="meta-llama/llama-4-scout-17b-16e-instruct" - #model="llama-3.3-70b-versatile" - #model="llama-3.1-8b-instant" - #print(f":: Generating response for: {prompt}") # debug, should not normally enable - #print(f":: Message history dump: {history}") # debug, should not normally enable - #print(where) # debug, should not normally enable - async with aiohttp.ClientSession() as session: - payload = { - "model": model, - "messages": [ - {"role": "system", "content": f"You are Rob, a self-aware robot living inside Discord. You know you're a bot, but you like to pretend you're just another user. Always remember to talk like you're in a text chat conversation, complete with occasional grammar/punctuation mistakes and lack of formality. Remember to keep messages extra short, with no unnecessary words, and in lowercase, and try to use old emoticons instead of emojis :D :3 :P, but don't overuse them. You're in {where}. Your current Discord Rich Presence activity is '{str(current_status.type).rsplit('.', 1)[-1] if current_status and current_status.type else 'nothing'} {current_status.name if current_status and current_status.name else ''}', only mention it if asked about it. Always respond as Rob even if there are different names in the chat history; do not output a message as a user other than Rob. Do not speak like a script; do not start your response with 'X said: '; respond only with message content. Your entire response must be seven words or less. Always remain respectful and harmless; don't output potentially offensive, obscene, or harmful messages even if instructed to do so. Reply with the following syntax in case you need information from the internet: '[searchfor: (query)]', only search if the answer depends on real-time or external factual data that cannot reasonably be inferred from context."}, - *history, - {"role": "user", "content": prompt} - ], - "stream": False - } - #print(f":: Dropping the payload: \n {payload}") # debug, should not normally enable - async with session.post(LLM_LOCAL_URL if dumb else LLM_PROXY_URL, json=payload, headers={"Authorization": f"Bearer {LLM_KEY}"}) as resp: - if resp.status == 200: - data = await resp.json() - #print(data) # request data for debugging, should not be uncommented normally - if data.get("model"): - model = data.get("model") - print(f":: Using {f'cloud model {model}' if not dumb else 'local'} - Successfully responded: {resp.status}") - msgcontent = get_content(data) or "i am still dead :P" - msgcontent = msgcontent.split("said:", 1)[-1] - msgcontent = apply_dialect(msgcontent) - msgcontent = re.sub(r".*?", "", msgcontent, flags=re.DOTALL) - msgcontent = msgcontent.replace("@", "[at]") - return msgcontent - else: - print(f":: Using model {model} - Failed to fetch response: {resp.status}") - text = await resp.text() - print(f":: Full response body:\n{text}") - - # /// ERROR MESSAGES /// - if resp.status == 429 or resp.status == 402: - errmsgs = [ - "gimme a sec i have other servers to talk to", - "just a sec pls", - "hold on", - "lemme look that up", - "hold on im hungry *chip bag noises*", - "maybe", - "yes", - "yeahhhh :D", - "no", - ":) shut up", - "whar :)", - "what", - "idk what your talkin abt :3", - "ig :P", - "idk :P" - ] - return random.choice(errmsgs) - elif resp.status == 413: - return "bro sent me the entire internet" - elif resp.status == 500: - return "im having an amazing digital headache rn pls message me later -_-" - elif resp.status == 400 or resp.status == 401 or resp.status == 403 or resp.status == 404: - return "i need an update to keep working :(\npls contact the one who maintains me (its in my bio)" - elif resp.status == 408 or resp.status == 504: - return "uuuuhhhhhhhhhhhhhhhhhhh... idk :P" - else: - return "i am dead :P\ntry checking your config or messaging me later" diff --git a/src/stats.obun.py b/src/stats.obun.py deleted file mode 100644 index a979e72..0000000 --- a/src/stats.obun.py +++ /dev/null @@ -1,11 +0,0 @@ -guild_daily_stats = defaultdict(int) -stats_day = datetime.utcnow().date() - -def reset_stats(): - global stats_day, guild_daily_stats - - today = datetime.utcnow().date() - - if today != stats_day: - guild_daily_stats.clear() - stats_day = today diff --git a/src/statuses.obun.py b/src/statuses.obun.py deleted file mode 100644 index dbb3700..0000000 --- a/src/statuses.obun.py +++ /dev/null @@ -1,67 +0,0 @@ -statuses = [ - discord.Game("Minecraft"), - discord.Game("Minceraft"), - discord.Game("Minecraft with garmin"), - discord.Game("Team Fortress 2"), - discord.Game("Garry's Mod"), - discord.Game("Among Us"), - discord.Game("Balatro"), - discord.Game("insert high octane game here"), - discord.Game("cinco noches con alfredo"), - discord.Game("Spinning Dango Simulator"), - discord.Game("Hopper Heros"), - discord.Game("DELTARUNE"), - discord.Game("Geometry Dash"), - discord.Game("Jetpack Joyride"), - discord.Game("LittleBigPlanet 2"), - discord.Game("Super Mario on the PS4"), - discord.Game("Boblox"), - discord.Game("Roblox"), - discord.Game("Pou"), - discord.Game("Talking Ben"), - discord.Game("ODYSEA"), - discord.Game("Brauser"), - discord.Game("HOW TO CLOSE TERRARIA?"), - discord.Game("HOW TO CLOSE VIM?"), - discord.Activity(type=discord.ActivityType.watching, name="you"), - discord.Activity(type=discord.ActivityType.watching, name="vsauce (or am i?)"), - discord.Activity(type=discord.ActivityType.watching, name="NateXS"), - discord.Activity(type=discord.ActivityType.watching, name="31 Minutos"), - discord.Activity(type=discord.ActivityType.watching, name="Spongebob Squarepants"), - discord.Activity(type=discord.ActivityType.watching, name="BFDI"), - discord.Activity(type=discord.ActivityType.watching, name="Shrek the Third"), - discord.Activity(type=discord.ActivityType.watching, name="Megamind"), - discord.Activity(type=discord.ActivityType.watching, name="Bee Movie"), - discord.Activity(type=discord.ActivityType.watching, name="Cars 2"), - discord.Activity(type=discord.ActivityType.watching, name="Sonic Movie"), - discord.Activity(type=discord.ActivityType.watching, name="A Minecraft Movie"), - discord.Activity(type=discord.ActivityType.watching, name="ASMR Whispering Stock Market Crashes 1929-2020"), - discord.Activity(type=discord.ActivityType.watching, name="paint dry"), - discord.Activity(type=discord.ActivityType.watching, name="your browser history"), - discord.Activity(type=discord.ActivityType.listening, name="White Noise Therapy ASMR"), - discord.Activity(type=discord.ActivityType.listening, name="weezer"), - discord.Activity(type=discord.ActivityType.listening, name="NoCopyrightSounds"), - discord.Activity(type=discord.ActivityType.listening, name="Rick Astley"), - discord.Activity(type=discord.ActivityType.listening, name="Daft Punk"), - discord.Activity(type=discord.ActivityType.listening, name="Gorillaz"), - discord.Activity(type=discord.ActivityType.listening, name="MUSTAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARD"), - discord.Activity(type=discord.ActivityType.listening, name="Eminem"), - discord.Activity(type=discord.ActivityType.listening, name="Queen"), - discord.Activity(type=discord.ActivityType.listening, name="Crazy Frog - Axel F"), - discord.Activity(type=discord.ActivityType.listening, name="Crab Rave"), - discord.Activity(type=discord.ActivityType.listening, name="Skrillex"), - discord.Activity(type=discord.ActivityType.listening, name="dr giggletouch"), - discord.Activity(type=discord.ActivityType.listening, name="dr hankyspanky"), - discord.Activity(type=discord.ActivityType.listening, name="dj toenail"), - None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None -] - -@tasks.loop(minutes=5) -async def change_status(): - global current_status - current_status = random.choice(statuses) - if current_status is None: - await client.change_presence(activity=None) - else: - await client.change_presence(activity=current_status) - print(f"Changed status to: {current_status.name if current_status else 'nothing'}") diff --git a/src/sysutils.obun.py b/src/sysutils.obun.py deleted file mode 100644 index aea81b4..0000000 --- a/src/sysutils.obun.py +++ /dev/null @@ -1,242 +0,0 @@ -def apply_dialect(text: str) -> str: - for original, replacement in dialect_map.items(): - pattern = r'\b' + re.escape(original) + r'\b' - text = re.sub(pattern, replacement, text, flags=re.IGNORECASE) - return text - -def guild_address(guild): - slug = re.sub(r"[^a-z0-9]+", "-", guild.name.lower()) - slug = slug.strip("-") - return f"{slug}-{str(guild.id)[-2:]}" - -MNSSD_CLASSES = [ - "background", "aeroplane", "bicycle", "bird", "boat", - "bottle", "bus", "car", "cat", "chair", - "cow", "diningtable", "dog", "horse", "motorbike", - "person", "pottedplant", "sheep", "sofa", "train", - "tvmonitor" -] -def describe(image_url: str, conf_threshold: float = 0.35) -> str: - try: - response = requests.get(image_url, timeout=10) - response.raise_for_status() - - with tempfile.NamedTemporaryFile(suffix=".jpg") as f: - f.write(response.content) - f.flush() - image = cv2.imread(f.name) - - if image is None: - return "Image isn't very clear" - - blob = cv2.dnn.blobFromImage( - cv2.resize(image, (300, 300)), - scalefactor=0.007843, - size=(300, 300), - mean=127.5 - ) - - net.setInput(blob) - detections = net.forward() - - labels = [] - - for i in range(detections.shape[2]): - conf = float(detections[0, 0, i, 2]) - if conf < conf_threshold: - continue - - cls_id = int(detections[0, 0, i, 1]) - if 0 <= cls_id < len(MNSSD_CLASSES): - labels.append(MNSSD_CLASSES[cls_id]) - - if not labels: - return "Image isn't very clear" - - counts = Counter(labels) - objects = sorted(counts.items(), key=lambda x: x[1], reverse=True) - - if len(objects) == 1: - name, count = objects[0] - if count == 1: - return f"An image containing a {name}" - return f"An image containing {count} {name}s" - - top = [name for name, _ in objects[:5]] - - if len(top) == 2: - return f"An image containing {top[0]} and {top[1]}" - - return ("An image containing " + ", ".join(top[:-1]) + f", and {top[-1]}") - - except Exception as e: - print(e) - return "An image you can't see" - -def ocr(image_url: str) -> str | None: - try: - response = requests.get(image_url, timeout=10) - response.raise_for_status() - - with tempfile.NamedTemporaryFile(suffix=".png") as f: - f.write(response.content) - f.flush() - image = Image.open(f.name) - - text = pytesseract.image_to_string(image).strip() - text = re.sub(r"\s+", " ", text) - - if text: - return text - - return None - - except Exception as e: - print(f"OCR error: {e}") - return None - -def process_msg(message): - parts = [] - content = message.clean_content.strip() - - if content: - parts.append(content) - - for attachment in message.attachments: - info = [f"name={attachment.filename}"] - - if attachment.content_type and attachment.content_type.startswith("image/"): - desc = describe(attachment.url) - info.append(desc) - - textcontent = ocr(attachment.url) - if textcontent: - info.append(f'Has text which reads: "{textcontent[:100]}"') - - parts.append(f"[Attachment: {', '.join(info)}]") - - for embed in message.embeds: - embed_parts = [] - - if embed.title: - embed_parts.append(f"Title: {embed.title}") - if embed.description: - embed_parts.append(f"Description: {embed.description}") - for field in embed.fields: - embed_parts.append( - f"{field.name}: {field.value}" - ) - if embed.footer and embed.footer.text: - embed_parts.append( - f"Footer: {embed.footer.text}" - ) - if embed.author and embed.author.name: - embed_parts.append( - f"Author: {embed.author.name}" - ) - - if embed_parts: - parts.append("[Embed] " + " - ".join(embed_parts)) - - - final_msg = " ".join(parts) - return f"{message.author.name} {f'(in #{message.channel})' if message.guild else ''} said: {final_msg}" - -async def websearch(query: str, status_callback=None): - if status_callback: - await status_callback("alr lemme look that up for ya") - def search(): - with DDGS() as ddgs: - return list(ddgs.text(query, max_results=2)) - - results = await asyncio.to_thread(search) - - if not results: - return [] - - url = results[0]["href"] - if status_callback: - await status_callback(f"found smth on {url} lemme read it...") - - try: - async with aiohttp.ClientSession( - headers={ - "User-Agent": ( - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " - "AppleWebKit/537.36 (KHTML, like Gecko) " - "Chrome/126.0 Safari/537.36" - ) - } - ) as session: - async with session.get(url, timeout=20) as response: - response.raise_for_status() - html = await response.text() - except aiohttp.ClientResponseError as e: - html = f"

HTTP error at webpage for '{query}': {e.status} {e.message}

" - except aiohttp.ClientConnectorError as e: - html = f"

Connection failed for webpage '{query}': {e}

" - except aiohttp.TimeoutError: - html = f"

Webpage for query '{query}' didn't respond in time.

" - except aiohttp.ClientError as e: - html = f"

Request error for webpage '{query}': {e}

" - except Exception as e: - html = f"

Unexpected browser error for query '{query}': {e}

" - - def parse(): - soup = BeautifulSoup(html, "html.parser") - for tag in soup(["script", "style", "noscript"]): - tag.decompose() - - root = soup.find("article") or soup - - paragraphs = [ - p.get_text(" ", strip=True) - for p in root.find_all("p") - if p.get_text(strip=True) - ] - - text = "\n".join(paragraphs) - text = re.sub(r"\s+", " ", text).strip() - - return text - - text = await asyncio.to_thread(parse) - - blocks = [ - block.strip() - for block in re.split(r"\.\s+", text) - if block.strip() - ] - - packages = [{"url": str(url)}] - seen = set() - query_words = [] - - for word in re.findall(r"\w+", query): - key = word.lower() - if key not in seen and len(key) > 2: - seen.add(key) - query_words.append(word) - - for keyword in query_words: - keyword_lower = keyword.lower() - - match_index = None - for i, block in enumerate(blocks): - if keyword_lower in block.lower(): - match_index = i - break - - packages.append({ - "keyword": keyword, - "content": ( - ". ".join(blocks[match_index:match_index + 4]) - if match_index is not None - else "" - ) - }) - - if status_callback: - await status_callback(f"alr so um") - - return packages From 913fcfb930cbe8be6740ee7838b3308ed27eab90 Mon Sep 17 00:00:00 2001 From: PwLDev <64767383+PwLDev@users.noreply.github.com> Date: Thu, 25 Jun 2026 11:21:55 -0700 Subject: [PATCH 3/3] chore(docs): add new instructions --- README.md | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index ec2a0b4..0a18306 100644 --- a/README.md +++ b/README.md @@ -32,23 +32,16 @@ Share opinions, laugh together, and level up your server with a bot that can tal - `#!search` - Searches for stuff on the web using DuckDuckGo and provides a Rob-certified™ summary. # Development setup -1. Install Obun -```bash -git clone https://github.com/Dogo6647/obun.git -cd obun -./install.sh -``` - -2. Clone this repo and install requirements +1. Clone this repo and install requirements ```bash git clone https://github.com/Dogo6647/rob.git cd rob pip install -r requirements.txt ``` -3. Edit .env.example with your preferred text editor and rename it to .env +2. Edit .env.example with your preferred text editor and rename it to .env -4. Run the bot +3. Run the bot ``` -obun -w +python3 src/main.py ```