aboutsummaryrefslogtreecommitdiff
path: root/bot.py
diff options
context:
space:
mode:
Diffstat (limited to 'bot.py')
-rw-r--r--bot.py235
1 files changed, 120 insertions, 115 deletions
diff --git a/bot.py b/bot.py
index 597ef2c..cdb14e4 100644
--- a/bot.py
+++ b/bot.py
@@ -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)