aboutsummaryrefslogtreecommitdiff
path: root/bot.py
diff options
context:
space:
mode:
authordavid <[email protected]>2026-03-10 12:57:05 -0300
committerdavid <[email protected]>2026-03-10 12:57:05 -0300
commita8fdeb52f075d3267a938f5298dd1549feeeafbc (patch)
treebe8ebba8699e0738c77e0c29ebc0a90e322ae323 /bot.py
downloadfaggot-chan-a8fdeb52f075d3267a938f5298dd1549feeeafbc.tar.gz
faggot-chan-a8fdeb52f075d3267a938f5298dd1549feeeafbc.zip
fixed a couple bugs, moved a couple of this. also made main stateless so i can do funny things
Diffstat (limited to 'bot.py')
-rw-r--r--bot.py387
1 files changed, 387 insertions, 0 deletions
diff --git a/bot.py b/bot.py
new file mode 100644
index 0000000..d32ea22
--- /dev/null
+++ b/bot.py
@@ -0,0 +1,387 @@
+#!/usr/bin/env python3
+import os
+import asyncio
+import json
+import time
+import random
+import traceback
+import discord
+import websockets
+from datetime import datetime
+from pathlib import Path
+
+def ignore_connection_closed_ok(loop, context): #this doesn't fucking work
+ exc = context.get("exception")
+ if isinstance(exc, websockets.exceptions.ConnectionClosedOK):
+ return
+ loop.default_exception_handler(context)
+
+system_prompt = "you are a funny discord user that uses discrd"
+
+system_prompt = "you are a funny discord user that uses discrd"
+
+FILES_DIR = Path("files")
+LOG_DIR = FILES_DIR / "log"
+MEDIA_DIR = FILES_DIR / "media"
+CONFIG_DIR = FILES_DIR / "config"
+
+ERROR_FILE = LOG_DIR / "errorlog.txt"
+MEMORY_FILE = LOG_DIR / "memory.txt"
+MEDIA_FILE = MEDIA_DIR / "media.txt"
+THREADS_FILE = CONFIG_DIR / "threads.txt"
+
+def init_files():
+ for d in (LOG_DIR, MEDIA_DIR, CONFIG_DIR):
+ d.mkdir(parents=True, exist_ok=True)
+ for f in (ERROR_FILE, MEMORY_FILE, MEDIA_FILE):
+ f.touch(exist_ok=True)
+ if not THREADS_FILE.exists():
+ THREADS_FILE.write_text("6\n")
+
+init_files()
+
+TOKEN= "your token goes here"
+
+
+server = "put your serevr id here"
+
+
+intents = discord.Intents.default()
+intents.message_content = True
+intents.members = True
+client = discord.Client(intents=intents)
+
+block_id = ["1000000000000003", "1000000000000004"] #fuck these two in particular
+
+last_server_message = 0.0 # unix timestamp of last message anyone sent in the server
+dm_sent = False
+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")
+
+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()]
+ except Exception as e:
+ log_error(f"[{timestamp()}] 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 []
+ except Exception as e:
+ log_error(f"[{timestamp()}] error reading longterm: {e}")
+ return []
+
+def build_payload(sys_prompt: str, user_input: str, rebuild: bool = False) -> str:
+ return json.dumps({
+ "system_prompt": sys_prompt,
+ "user_input": user_input,
+ "shortterm": load_shortterm(),
+ "longterm": load_longterm(),
+ "rebuild_vectors": rebuild,
+ })
+
+async def call_main(payload: str) -> tuple[bool, str]:
+ uri = "ws://0.0.0.0:8765"
+ 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"]
+ except websockets.exceptions.ConnectionClosedOK:
+ return False, f"[{timestamp()}] websocket closed cleanly (no reply)"
+ except Exception as e:
+ return False, f"[{timestamp()}] websocket error: {e}"
+
+async def handle_error(error_msg: str, notify_channel=None, notify_user=None):
+ print(error_msg)
+ log_error(error_msg)
+ notice = "someone tell <@1000000000000001> there is a problem with my ai"
+ try:
+ if notify_channel:
+ await notify_channel.send(notice)
+ elif notify_user:
+ dm = await notify_user.create_dm()
+ await dm.send(notice)
+ except Exception:
+ pass
+
+saved_media_urls = set()
+
+with MEDIA_FILE.open("r", encoding="utf-8") as f:
+ for line in f:
+ if "saved media:" in line:
+ url = line.split("saved media:", 1)[1].strip()
+ saved_media_urls.add(url)
+
+def steal_media(text: str):
+ if "saved media:" not in text:
+ return
+ url = text.split("saved media:", 1)[1].strip()
+ if url in saved_media_urls:
+ return
+ saved_media_urls.add(url)
+ with MEDIA_FILE.open("a", encoding="utf-8") as f:
+ f.write(text + "\n")
+
+def is_member_in_server(member: discord.Member) -> bool:
+ guild = client.get_guild(server)
+ return guild and guild.get_member(member.id) is not None
+
+async def random_dm():
+ global dm_sent
+ await client.wait_until_ready()
+ while not client.is_closed():
+ await asyncio.sleep(300) #every 5 minutes
+ for guild in client.guilds:
+ if random.random() < 0.01: #should be 1%
+ members = [m for m in guild.members if not m.bot]
+ if dm_sent:
+ dm_sent = False
+ continue
+ if not members:
+ continue
+ victim = random.choice(members)
+ payload = build_payload(
+ system_prompt,
+ f"System: you are sending a dm to {victim}, write a long, playful story or an over-the-top pun monologue about anything. "
+ "It should sound like something faggot-chan would post when she's bored — chaotic, funny, and full of personality (at least 20 words)."
+ "keep in mind this is a dm so make it a bit personal"
+ )
+ ok, result = await call_main(payload)
+ if not ok:
+ await handle_error(result, notify_user=victim)
+ continue
+ try: #sends the dm
+ dm = await victim.create_dm()
+ async with dm.typing():
+ 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}")
+ except Exception as e:
+ await handle_error(f"[{timestamp()}] 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(240) #every 4 minutes
+ if time.time() - last_server_message < QUIET_COOLDOWN: #only send if server has been quiet for 10 mins
+ continue
+ if random.random() < 0.01: #1%
+ for guild in client.guilds:
+ channel = discord.utils.get(guild.text_channels, name="general")
+ if channel:
+ payload = build_payload(
+ system_prompt,
+ "System: write a long, playful story or an over-the-top pun monologue about cheese. or anything random"
+ "it should sound like something faggot-chan would post when she's bored."
+ "chaotic, funny, and full of personality (at least 15 words and no more than 20)"
+ )
+ ok, result = await call_main(payload)
+ if not ok:
+ await handle_error(result, notify_channel=channel)
+ continue
+ async with channel.typing():
+ await channel.send(result)
+ log_message(f"[{timestamp()}] random message: {result}")
+ print(f"[{timestamp()}] random message: {result}")
+
+async def on_ready():
+ client.loop.create_task(random_message())
+ client.loop.create_task(random_dm())
+ print(f"faggot-chan is active as {client.user}")
+
+
+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
+
+ mentioned = client.user.mentioned_in(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"[{timestamp()}] saved media: {url}")
+
+ #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
+
+ 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 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
+
+
+async def on_message(message):
+ global dm_sent, interaction_count, last_server_message
+ if message.author == client.user:
+ return
+
+ if not is_member_in_server(message.author):
+ return
+
+ # update last activity so random_message knows the server isn't quiet
+ last_server_message = time.time()
+
+ if str(message.author.id) in block_id:
+ return
+
+ # always detect replies
+ replied_message = None
+ replied_to_bot = False
+ if message.reference:
+ try:
+ replied_message = message.reference.resolved or await message.channel.fetch_message(message.reference.message_id)
+ if replied_message:
+ if replied_message.author == client.user:
+ replied_to_bot = True
+ log_message(f'[{timestamp()}] {message.author} replied to {replied_message.author} saying: "{replied_message.content}" with reply: "{message.content}"')
+ else:
+ log_message(f'[{timestamp()}] {message.author} said: {message.content}')
+ except Exception as e:
+ log_error(f"[{timestamp()}] failed to resolve reply: {e}")
+ log_message(f'[{timestamp()}] {message.author} said: {message.content}')
+ else:
+ log_message(f'[{timestamp()}] {message.author} said: {message.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_message.content}" with reply: "{message.content}"'
+ else:
+ user_input = f"{message.author} said in DM: {message.content}"
+ payload = build_payload(system_prompt, user_input, rebuild)
+ ok, result = await call_main(payload)
+ if not ok:
+ await handle_error(result, notify_user=message.author)
+ return
+ try:
+ async with message.channel.typing():
+ await message.reply(result)
+ print(f"[{timestamp()}] DM from {message.author}: {message.content} Bot replied in DM: {result}")
+ log_message(f"[{timestamp()}] DM from {message.author}: {message.content}")
+ log_message(f"[{timestamp()}] Faggot-chan replied in DM: {result}")
+ except Exception as e:
+ await handle_error(f"[{timestamp()}] failed to send DM reply: {e}", notify_user=message.author)
+ return
+
+ if message.channel.name not in ["general", "announcements"]:
+ return
+
+ mentioned = client.user.mentioned_in(message)
+
+ # if it replied to bot, treat it as a mention automatically
+ if replied_to_bot:
+ mentioned = True
+
+ if not mentioned and random.random() > 0.05: #5% chance of replying if not mentiond
+ return
+
+ if replied_message:
+ user_input = f'{message.author} replied to {replied_message.author} saying: "{replied_message.content}" with reply: "{message.content}"'
+ else:
+ user_input = f"{message.author} said: {message.content}"
+
+ payload = build_payload(system_prompt, user_input, rebuild)
+ ok, result = await call_main(payload)
+ if not ok:
+ await handle_error(result, notify_channel=message.channel)
+ return
+
+ try:
+ async with message.channel.typing():
+ await message.reply(result)
+ print(f"[{timestamp()}] {message.author} said: {message.content}\n[{timestamp()}] Faggot-chan said: {result}")
+ log_message(f"[{timestamp()}] {message.author} said: {message.content}\n[{timestamp()}] Faggot-chan said: {result}")
+ except Exception as e:
+ await handle_error(f"[{timestamp()}] failed to send reply: {e}", notify_channel=message.channel)
+
+ await on_random(message)
+
+client.run(TOKEN)