diff options
Diffstat (limited to 'bot.py')
| -rw-r--r-- | bot.py | 235 |
1 files changed, 120 insertions, 115 deletions
@@ -8,8 +8,8 @@ import random import traceback import discord import websockets -from datetime import datetime from pathlib import Path +from utils import nots, now_str, tail_lines def ignore_connection_closed_ok(loop, context): #this doesn't fucking work exc = context.get("exception") @@ -19,9 +19,11 @@ def ignore_connection_closed_ok(loop, context): #this doesn't fucking work def load_system_prompt() -> str: - return Path("config/system_prompt.txt").read_text(encoding="utf-8") + return (CONFIG_DIR / "system_prompt.txt").read_text(encoding="utf-8") -FILES_DIR = Path("files") +# resolve files relative to this script, not whatever dir it happens to get launched from +SCRIPT_DIR = Path(__file__).resolve().parent +FILES_DIR = SCRIPT_DIR / "files" LOG_DIR = FILES_DIR / "log" MEDIA_DIR = FILES_DIR / "media" CONFIG_DIR = FILES_DIR / "config" @@ -45,6 +47,10 @@ _config_lines = [l.strip() for l in (CONFIG_DIR / "tokens.txt").read_text(encodi TOKEN = _config_lines[0] server = int(_config_lines[1]) +_conn_lines = [l.strip() for l in (CONFIG_DIR / "connection.txt").read_text(encoding="utf-8").splitlines() if l.strip() and not l.strip().startswith("#")] +MAIN_URI = _conn_lines[0] # ws:// or wss:// address of main.py +MAIN_TOKEN = _conn_lines[1] # shared secret main.py checks on every payload + intents = discord.Intents.default() intents.message_content = True intents.members = True @@ -63,9 +69,6 @@ interaction_count = 0 QUIET_COOLDOWN = 600 # seconds of silence before she's allowed to send a random message (10 mins) -def timestamp() -> str: - return datetime.now().strftime("%Y-%m-%d %H:%M:%S") - def log_error(text: str): with ERROR_FILE.open("a", encoding="utf-8") as f: f.write(text + "\n") @@ -74,27 +77,19 @@ def log_message(text: str): with MEMORY_FILE.open("a", encoding="utf-8") as f: f.write(text + "\n") -def nots(line: str) -> str: - if line.startswith("[") and "]" in line: - return line.split("]", 1)[1].lstrip() - return line - def load_shortterm() -> list[str]: try: - with MEMORY_FILE.open("r", encoding="utf-8", errors="ignore") as f: - lines = f.readlines()[-15:] # reads bottom 15 lines - return [line.strip() for line in lines if line.strip()] + return tail_lines(MEMORY_FILE, 15) # reads bottom 15 lines without loading the whole file except Exception as e: - log_error(f"[{timestamp()}] error reading shortterm: {e}") + log_error(f"[{now_str()}] error reading shortterm: {e}") return [] def load_longterm() -> list[str]: try: - with MEMORY_FILE.open("r", encoding="utf-8", errors="ignore") as f: - lines = [nots(line.strip()) for line in f if line.strip()] - return lines[-515:-15] if len(lines) > 15 else [] + lines = [nots(l) for l in tail_lines(MEMORY_FILE, 515)] + return lines[:-15] if len(lines) > 15 else [] except Exception as e: - log_error(f"[{timestamp()}] error reading longterm: {e}") + log_error(f"[{now_str()}] error reading longterm: {e}") return [] def build_payload(user_input: str, rebuild: bool = False) -> str: @@ -108,22 +103,28 @@ def build_payload(user_input: str, rebuild: bool = False) -> str: "shortterm": load_shortterm(), "longterm": load_longterm(), "rebuild_vectors": should_rebuild, + "auth_token": MAIN_TOKEN, }) +_main_ws = None # kept open across messages instead of reconnecting every time + async def call_main(payload: str) -> tuple[bool, str]: - uri = "ws://0.0.0.0:8765" + global _main_ws try: - async with websockets.connect(uri, open_timeout=None, ping_timeout=None) as websocket: - await websocket.send(payload) - raw = await websocket.recv() - data = json.loads(raw) - if "error" in data: - return False, data["error"] - return True, data["ok"] + if _main_ws is None or _main_ws.closed: + _main_ws = await websockets.connect(MAIN_URI, open_timeout=None, ping_timeout=None) + await _main_ws.send(payload) + raw = await _main_ws.recv() + data = json.loads(raw) + if "error" in data: + return False, data["error"] + return True, data["ok"] except websockets.exceptions.ConnectionClosedOK: - return False, f"[{timestamp()}] websocket closed cleanly (no reply)" + _main_ws = None + return False, f"[{now_str()}] websocket closed cleanly (no reply)" except Exception as e: - return False, f"[{timestamp()}] websocket error: {e}" + _main_ws = None + return False, f"[{now_str()}] websocket error: {e}" async def handle_error(error_msg: str, notify_channel=None, notify_user=None): print(error_msg) @@ -163,6 +164,22 @@ def steal_media(text: str): with MEDIA_FILE.open("a", encoding="utf-8") as f: f.write(text + "\n") +def steal_attachments(message): + #steal images and gifs and stuff (always do this regardless of what else fires) + media_urls = [ + a.url for a in message.attachments + if a.content_type and a.content_type.startswith(("image/", "video/")) + ] + + for embed in message.embeds: + if embed.image and embed.image.url: + media_urls.append(embed.image.url) + if embed.video and embed.video.url: + media_urls.append(embed.video.url) + + for url in media_urls: + steal_media(f"[{now_str()}] saved media: {url}") + def resolve_mentions(content: str, guild) -> str: # thank you claude if not guild: return content @@ -205,17 +222,17 @@ async def random_dm(): dm = await victim.create_dm() await dm.send(result) dm_sent = True - log_message(f"[{timestamp()}]Faggot-chan sent a random dm to {victim}: {result}") - print(f"[{timestamp()}] random dm to {victim}: {result}") + log_message(f"[{now_str()}]Faggot-chan sent a random dm to {victim}: {result}") + print(f"[{now_str()}] random dm to {victim}: {result}") except Exception as e: - await handle_error(f"[{timestamp()}] failed to send dm to {victim}: {e}", notify_user=victim) + await handle_error(f"[{now_str()}] failed to send dm to {victim}: {e}", notify_user=victim) async def random_message(): await client.wait_until_ready() while not client.is_closed(): await asyncio.sleep(600) #every 10 minutes if time.time() - last_server_message < QUIET_COOLDOWN: #only send if server has been quiet for 10 mins continue - if random.random() < 0.0001: #0.001% + if random.random() < 0.0001: #0.01% for guild in client.guilds: channel = discord.utils.get(guild.text_channels, name="general") if channel: @@ -229,8 +246,8 @@ async def random_message(): await handle_error(result, notify_channel=channel) continue await channel.send(result) - log_message(f"[{timestamp()}] random message: {result}") - print(f"[{timestamp()}] random message: {result}") + log_message(f"[{now_str()}] random message: {result}") + print(f"[{now_str()}] random message: {result}") @client.event async def on_ready(): @@ -240,77 +257,64 @@ async def on_ready(): @client.event -async def on_random(message): - - RNG = random.randint(1,3) - - if message.author == client.user: - return - - - if not is_member_in_server(message.author): - return - - if str(message.author.id) in block_id: - return - - if not message.guild or message.guild.id != server: - return - - if message.channel.name not in ["general", "announcements"]: - return +async def on_random(message) -> bool: + # single weighted pick between sticker/emoji/media, used as a substitute for an actual + # llm reply — only called once on_message already decided this message gets a response. + # returns True if it fired (caller should skip the llm call), False if it should proceed normally. + stickers = list(message.guild.stickers) + emojis = [str(e) for e in message.guild.emojis] - mentioned = client.user.mentioned_in(message) + options = [] + if stickers: + options.append(("sticker", 0.10)) + if emojis: + options.append(("emoji", 0.13)) + if saved_media_urls: + options.append(("media", 0.07)) # ~30% combined chance of firing when all three are available - #steal images and gifs and stuff (always do this regardless of what else fires) - media_urls = [ - a.url for a in message.attachments - if a.content_type and a.content_type.startswith(("image/", "video/")) - ] + if not options: + return False - for embed in message.embeds: - if embed.image and embed.image.url: - media_urls.append(embed.image.url) - if embed.video and embed.video.url: - media_urls.append(embed.video.url) + roll = random.random() + cumulative = 0.0 + for kind, weight in options: + cumulative += weight + if roll >= cumulative: + continue - for url in media_urls: - steal_media(f"[{timestamp()}] saved media: {url}") + if kind == "sticker": + chosen = random.choice(stickers) + try: + await message.channel.send(stickers=[chosen]) + log_message(f"[{now_str()}] Faggot-chan sent sticker: {chosen.name}") + print(f"[{now_str()}] Faggot-chan sent sticker: {chosen.name}") + except websockets.exceptions.ConnectionClosedOK: + pass + except Exception as e: + log_error(f"[{now_str()}] error: {e}") + await message.reply("someone tell <@1000000000000001> there is a problem with my ai") + print(f"Sticker send failed: {e}") + return True - #stickers - early exit, skip ai generation - stickers = list(message.guild.stickers) - if stickers and not mentioned and random.random() < 0.15: #15% chance of sending a sticker - chosen = random.choice(stickers) - try: - await message.channel.send(stickers=[chosen]) - log_message(f"[{timestamp()}] Faggot-chan sent sticker: {chosen.name}") - print(f"[{timestamp()}] Faggot-chan sent sticker: {chosen.name}") - except websockets.exceptions.ConnectionClosedOK: - pass - except Exception as e: - log_error(f"[{timestamp()}] error: {e}") - await message.reply("someone tell <@1000000000000001> there is a problem with my ai") - print(f"Sticker send failed: {e}") - return + if kind == "emoji": + try: + await message.add_reaction(random.choice(emojis)) + except websockets.exceptions.ConnectionClosedOK: + pass + except Exception as e: + log_error(f"[{now_str()}] error: {e}") + await message.reply("someone tell <@1000000000000001> there is a problem with my ai") + print(f"Emoji reaction failed: {e}") + return True - emojis = [str(e) for e in message.guild.emojis] - if emojis and not mentioned and random.random() < 0.2: #20% of reacting - early exit - try: - await message.add_reaction(random.choice(emojis)) - except websockets.exceptions.ConnectionClosedOK: - pass - except Exception as e: - log_error(f"[{timestamp()}] error: {e}") - await message.reply("someone tell <@1000000000000001> there is a problem with my ai") - print(f"Emoji reaction failed: {e}") - return + if kind == "media": + chosen = random.choice(list(saved_media_urls)) + await message.channel.send(chosen) + log_message(f"[{now_str()}] Faggot-chan sent media: {chosen}") + print(f"[{now_str()}] Faggot-chan sent media: {chosen}") + return True - if random.random() < 0.1 and saved_media_urls: #10% of sending a stolen image - early exit, uses live set - chosen = random.choice(list(saved_media_urls)) - await message.channel.send(chosen) - log_message(f"[{timestamp()}] Faggot-chan sent media: {chosen}") - print(f"[{timestamp()}] Faggot-chan sent media: {chosen}") - return + return False @client.event @@ -332,6 +336,9 @@ async def on_message(message): if message.guild and message.channel.name not in ["general", "announcements"]: return + if message.guild: + steal_attachments(message) + content = resolve_mentions(message.content, message.guild) # always detect replies @@ -345,21 +352,19 @@ async def on_message(message): if replied_message.author == client.user: replied_to_bot = True replied_content = resolve_mentions(replied_message.content, message.guild) - log_message(f'[{timestamp()}] {message.author} replied to {replied_message.author} saying: "{replied_content}" with reply: "{content}"') + log_message(f'[{now_str()}] {message.author} replied to {replied_message.author} saying: "{replied_content}" with reply: "{content}"') else: - log_message(f'[{timestamp()}] {message.author} said: {content}') + log_message(f'[{now_str()}] {message.author} said: {content}') except Exception as e: - log_error(f"[{timestamp()}] failed to resolve reply: {e}") - log_message(f'[{timestamp()}] {message.author} said: {content}') + log_error(f"[{now_str()}] failed to resolve reply: {e}") + log_message(f'[{now_str()}] {message.author} said: {content}') else: - log_message(f'[{timestamp()}] {message.author} said: {content}') + log_message(f'[{now_str()}] {message.author} said: {content}') interaction_count += 1 rebuild = (interaction_count % 150 == 0) if message.guild is None: #dms now should work - if str(message.author.id) in block_id: - return dm_sent = False if replied_message: user_input = f'{message.author} replied to {replied_message.author} saying: "{replied_content}" with reply: "{content}"' @@ -375,11 +380,10 @@ async def on_message(message): return try: await message.reply(result) - print(f"[{timestamp()}] DM from {message.author}: {content} Bot replied in DM: {result}") - log_message(f"[{timestamp()}] DM from {message.author}: {content}") - log_message(f"[{timestamp()}] Faggot-chan replied in DM: {result}") + print(f"[{now_str()}] DM from {message.author}: {content} Bot replied in DM: {result}") + log_message(f"[{now_str()}] Faggot-chan said in DM: {result}") except Exception as e: - await handle_error(f"[{timestamp()}] failed to send DM reply: {e}", notify_user=message.author) + await handle_error(f"[{now_str()}] failed to send DM reply: {e}", notify_user=message.author) return mentioned = client.user.mentioned_in(message) @@ -397,6 +401,9 @@ async def on_message(message): if not mentioned and random.random() > 0.03: #3% chance of replying if not mentiond return + if await on_random(message): # ~30% chance she just sends a sticker/emoji/media instead + return + if replied_message: user_input = f'{message.author} replied to {replied_message.author} saying: "{replied_content}" with reply: "{content}"' else: @@ -413,16 +420,14 @@ async def on_message(message): try: await message.reply(result) - print(f"[{timestamp()}] {message.author} said: {content}\n[{timestamp()}] Faggot-chan said: {result}") - log_message(f"[{timestamp()}] {message.author} said: {content}\n[{timestamp()}] Faggot-chan said: {result}") + print(f"[{now_str()}] {message.author} said: {content}\n[{now_str()}] Faggot-chan said: {result}") + log_message(f"[{now_str()}] Faggot-chan said: {result}") if str(message.author.id) == moralfag_id: moralfag_counter += 1 if moralfag_counter >= faglimit: moralfag_skip = True moralfag_counter = 0 except Exception as e: - await handle_error(f"[{timestamp()}] failed to send reply: {e}", notify_channel=message.channel) - - await on_random(message) + await handle_error(f"[{now_str()}] failed to send reply: {e}", notify_channel=message.channel) client.run(TOKEN) |
