vibecoded fixes to fix the vibecoded slop

This commit is contained in:
david committed 2026-10-23 04:44:58 -03:00
1 parent a704744f5c
commit 00930d9d75
2 files changed
+21 -59

No files matched your search

+20 -53
View File
@@ -34,7 +34,6 @@ DM_DIR = LOG_DIR / "dm" # one memory file per person so dms never end
ERROR_FILE = LOG_DIR / "errorlog.txt" ERROR_FILE = LOG_DIR / "errorlog.txt"
MEMORY_FILE = LOG_DIR / "memory.txt" MEMORY_FILE = LOG_DIR / "memory.txt"
MEDIA_FILE = MEDIA_DIR / "media.txt"
THREADS_FILE = CONFIG_DIR / "threads.txt" THREADS_FILE = CONFIG_DIR / "threads.txt"
PROMPT_FILE = CONFIG_DIR / "system_prompt.txt" PROMPT_FILE = CONFIG_DIR / "system_prompt.txt"
BLOCKED_FILE = CONFIG_DIR / "blocked.txt" # edited with admin.py, re-read on every message BLOCKED_FILE = CONFIG_DIR / "blocked.txt" # edited with admin.py, re-read on every message
@@ -46,7 +45,7 @@ DEFAULT_SETTINGS = {"admin_id": "1000000000000001", "moralfag_id": "100000000000
def init_files(): def init_files():
for d in (LOG_DIR, MEDIA_DIR, CONFIG_DIR, DM_DIR): for d in (LOG_DIR, MEDIA_DIR, CONFIG_DIR, DM_DIR):
d.mkdir(parents=True, exist_ok=True) d.mkdir(parents=True, exist_ok=True)
for f in (ERROR_FILE, MEMORY_FILE, MEDIA_FILE): for f in (ERROR_FILE, MEMORY_FILE):
f.touch(exist_ok=True) f.touch(exist_ok=True)
if not THREADS_FILE.exists(): if not THREADS_FILE.exists():
THREADS_FILE.write_text("6\n") THREADS_FILE.write_text("6\n")
@@ -85,15 +84,14 @@ QUOTA_COOLDOWN = 3600 # after gemini says it's out of quota go straight to the
quota_until = 0.0 # unix timestamp quota_until = 0.0 # unix timestamp
MAX_REACTION_OPTIONS = 25 # per kind (stickers/emojis/media), random sample so the prompt stays small MAX_REACTION_OPTIONS = 25 # per kind (stickers/emojis/media), random sample so the prompt stays small
REACTION_TAG = re.compile(r'\[(sticker|reaction|media):\s*([^\]]+?)\s*\]', re.I) REACTION_TAG = re.compile(r'\[(sticker|emoji|media):\s*([^\]]+?)\s*\]', re.I)
EMOJI_TAG = re.compile(r'\[emoji:\s*([^\]]+?)\s*\]', re.I) # inline, this one can go anywhere in the message
QUIET_COOLDOWN = 600 # seconds of silence before she's allowed to send a random message (10 mins) QUIET_COOLDOWN = 600 # seconds of silence before she's allowed to send a random message (10 mins)
MEDIA_MIMES = ("image/png", "image/jpeg", "image/webp", "image/heic", "image/heif", MEDIA_MIMES = ("image/png", "image/jpeg", "image/webp", "image/heic", "image/heif",
"audio/", "video/", "application/pdf", "text/plain") # what gemini can actually read "audio/", "video/", "application/pdf", "text/plain") # what gemini can actually read
MEDIA_MAX_BYTES = 10 * 1024 * 1024 # total per message, gemini caps inline request size (~20mb and base64 adds a third) MEDIA_MAX_BYTES = 10 * 1024 * 1024 # total per message, gemini caps inline request size (~20mb and base64 adds a third)
MEDIA_SAVE_MAX_BYTES = 19 * 1024 * 1024 # per file we keep, stays under discord's 10mb upload limit so she can send it back MEDIA_SAVE_MAX_BYTES = 19 * 1024 * 1024 # per file we keep and offer, has to fit the server's upload limit or she can't send it back
def log_error(text: str): def log_error(text: str):
with ERROR_FILE.open("a", encoding="utf-8") as f: with ERROR_FILE.open("a", encoding="utf-8") as f:
@@ -253,17 +251,13 @@ async def send_reply(message, text: str):
vector_db_ready = False # gets set to True after first rebuild so we don't keep rebuilding vector_db_ready = False # gets set to True after first rebuild so we don't keep rebuilding
saved_media = set() # file names inside MEDIA_DIR MEDIA_EXTS = (".gif", ".png", ".jpg", ".jpeg", ".webp", ".mp4", ".webm", ".mov", ".mkv")
kept_media_lines = [] def scan_media() -> set:
with MEDIA_FILE.open("r", encoding="utf-8") as f: # whatever is in the media folder is what she can send, skips media.txt and anything too big to upload
for line in f: return {p.name for p in MEDIA_DIR.iterdir() if p.is_file() and p.suffix.lower() in MEDIA_EXTS and p.stat().st_size <= MEDIA_SAVE_MAX_BYTES}
if "saved media:" in line:
name = line.split("saved media:", 1)[1].strip() saved_media = scan_media() # file names inside MEDIA_DIR
if (MEDIA_DIR / name).is_file(): # old entries were urls (those expire), they don't match a file so they get dropped
saved_media.add(name)
kept_media_lines.append(line)
MEDIA_FILE.write_text("".join(kept_media_lines), encoding="utf-8") # rewritten without the entries of deleted files
async def steal_media(url: str, name: str): async def steal_media(url: str, name: str):
# discord cdn links are signed and die after a day or so, so we keep our own copy # discord cdn links are signed and die after a day or so, so we keep our own copy
@@ -287,22 +281,19 @@ async def steal_media(url: str, name: str):
saved_media.discard(name) saved_media.discard(name)
log_error(f"[{now_str()}] failed to save media {url}: {e}") log_error(f"[{now_str()}] failed to save media {url}: {e}")
return return
with MEDIA_FILE.open("a", encoding="utf-8") as f:
f.write(f"[{now_str()}] saved media: {name}\n")
async def steal_attachments(message): async def steal_attachments(message):
#steal gifs only (always do this regardless of what else fires) #steal images and gifs and stuff (always do this regardless of what else fires)
media = [ media = [
(a.url, f"{a.id}_{Path(a.filename).name}") for a in message.attachments (a.url, f"{a.id}_{Path(a.filename).name}") for a in message.attachments
if a.content_type and a.content_type.split(";")[0].strip().lower() == "image/gif" and a.size <= MEDIA_SAVE_MAX_BYTES if a.content_type and a.content_type.startswith(("image/", "video/")) and a.size <= MEDIA_SAVE_MAX_BYTES
] ]
for embed in message.embeds: for embed in message.embeds:
# tenor/klipy links come as "gifv" embeds with an mp4 in embed.video, direct .gif links are plain image embeds
for part in (embed.image, embed.video): for part in (embed.image, embed.video):
if part and part.url: if part and part.url:
ext = Path(urlparse(part.url).path).suffix # no extension, no preview on discord, not worth keeping ext = Path(urlparse(part.url).path).suffix # no extension, no preview on discord, not worth keeping
if ext and (embed.type == "gifv" or ext.lower() == ".gif"): if ext:
media.append((part.url, hashlib.sha1(part.url.encode()).hexdigest()[:16] + ext)) media.append((part.url, hashlib.sha1(part.url.encode()).hexdigest()[:16] + ext))
for url, name in media: for url, name in media:
@@ -432,15 +423,14 @@ def sync_reactions(guild):
if n not in data[key]: if n not in data[key]:
data[key][n] = "" data[key][n] = ""
changed = True changed = True
for n in [n for n in data["media"] if n not in saved_media]: # file got deleted, drop its entry too
del data["media"][n]
changed = True
if changed: if changed:
REACTIONS_FILE.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") REACTIONS_FILE.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
def reaction_tool_prompt(guild) -> str: def reaction_tool_prompt(guild) -> str:
# the "tool": instead of (or along with) words she can write a [sticker: name] / [emoji: name] / [media: name] tag and # the "tool": instead of (or along with) words she can write a [sticker: name] / [emoji: name] / [media: name] tag and
# split_reactions picks it back out of the reply. same call as the reply, so no extra latency or gemini quota. # split_reactions picks it back out of the reply. same call as the reply, so no extra latency or gemini quota.
saved_media.clear() # ls the folder again so files added / renamed (describe.py) while she's running show up
saved_media.update(scan_media())
sync_reactions(guild) sync_reactions(guild)
data = load_reactions() data = load_reactions()
have = { have = {
@@ -457,23 +447,13 @@ def reaction_tool_prompt(guild) -> str:
sections.append(f"{key}:\n" + "\n".join(f"- {n}: {d}" if d else f"- {n}" for n, d in options)) sections.append(f"{key}:\n" + "\n".join(f"- {n}: {d}" if d else f"- {n}" for n, d in options))
if not sections: if not sections:
return "" return ""
emoji_note = ("\n\nyou can also put a server emoji inside the text of your message with [emoji: name], like \"hi [emoji: name] hello\", using a name from the emojis list. " return ("\n\nREACTIONS: you can send a sticker, an emoji reaction or a media file instead of (or along with) words, but only when it really fits the moment, most of the time just reply normally. "
"don't use emojis in every message, most of your messages should have none.") if any(s.startswith("emojis:") for s in sections) else "" "to do it put ONE tag at the end of your message: [sticker: name], [emoji: name] or [media: name], using exactly one of the names below. "
return ("\n\nREACTIONS: you can send a sticker, react to the message with an emoji or send a media file instead of (or along with) words, but only when it really fits the moment, most of the time just reply normally. " "the tag alone is fine if you have nothing to say. never invent names.\n" + "\n".join(sections))
"to do it put ONE tag at the end of your message: [sticker: name], [reaction: name] or [media: name], using exactly one of the names below ([reaction: name] uses the emoji names). "
"the tag alone is fine if you have nothing to say. never invent names.\n" + "\n".join(sections) + emoji_note)
def split_reactions(text: str) -> tuple[str, list]: def split_reactions(text: str) -> tuple[str, list]:
return REACTION_TAG.sub("", text).strip(), [(k.lower(), n) for k, n in REACTION_TAG.findall(text)] return REACTION_TAG.sub("", text).strip(), [(k.lower(), n) for k, n in REACTION_TAG.findall(text)]
def fill_emojis(text: str, guild) -> str:
# [emoji: name] inside a message becomes the real custom emoji, same check as the reactions (has to exist and be available), a made up name just disappears
def replace(m):
name = m.group(1).strip().strip(":").lower()
chosen = next((e for e in guild.emojis if e.available and e.name.lower() == name), None)
return str(chosen) if chosen else ""
return EMOJI_TAG.sub(replace, text)
async def send_reaction(message, kind: str, name: str): async def send_reaction(message, kind: str, name: str):
# looks the name up for real, a made up name just does nothing # looks the name up for real, a made up name just does nothing
name = name.strip().strip(":").lower() name = name.strip().strip(":").lower()
@@ -484,7 +464,7 @@ async def send_reaction(message, kind: str, name: str):
await message.channel.send(stickers=[chosen]) await message.channel.send(stickers=[chosen])
log_message(f"[{now_str()}] Faggot-chan sent sticker: {chosen.name}") log_message(f"[{now_str()}] Faggot-chan sent sticker: {chosen.name}")
print(f"[{now_str()}] Faggot-chan sent sticker: {chosen.name}") print(f"[{now_str()}] Faggot-chan sent sticker: {chosen.name}")
elif kind == "reaction": elif kind == "emoji":
chosen = next((e for e in message.guild.emojis if e.available and e.name.lower() == name), None) chosen = next((e for e in message.guild.emojis if e.available and e.name.lower() == name), None)
if chosen: if chosen:
await message.add_reaction(chosen) await message.add_reaction(chosen)
@@ -607,10 +587,9 @@ async def on_message(message):
if not result.strip() and not reactions: if not result.strip() and not reactions:
return return
shown = fill_emojis(result, message.guild) # what gets sent, result keeps the [emoji: x] tags for the log and history
try: try:
if shown.strip(): if result.strip():
await send_reply(message, shown) await send_reply(message, result)
print(f"[{now_str()}] {message.author} said: {content}\n[{now_str()}] 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}") log_message(f"[{now_str()}] Faggot-chan said: {result}")
if str(message.author.id) == settings["moralfag_id"]: if str(message.author.id) == settings["moralfag_id"]:
@@ -623,16 +602,4 @@ async def on_message(message):
except Exception as e: except Exception as e:
await handle_error(f"[{now_str()}] failed to send reply: {e}", notify_channel=message.channel) await handle_error(f"[{now_str()}] failed to send reply: {e}", notify_channel=message.channel)
@client.event
async def on_message_edit(before, after):
# discord builds link embeds (tenor etc) a moment after the message is sent, so message.embeds is still empty in on_message
# and the embed shows up here as an edit instead
if after.author == client.user or after.author.bot or after.guild is None:
return
if not after.embeds or before.embeds: # only when embeds just appeared
return
if after.channel.name not in ["general", "announcements"] or str(after.author.id) in load_blocked():
return
await steal_attachments(after)
client.run(TOKEN) client.run(TOKEN)
+1 -6
View File
@@ -134,7 +134,7 @@ def split_name(text: str) -> tuple[str, str]:
def rename_media(old: str, name: str) -> str: def rename_media(old: str, name: str) -> str:
# keeps the extension, adds _2 _3 if the name is taken. media.txt gets the new name too or the bot forgets the file on restart # keeps the extension, adds _2 _3 if the name is taken
suffix = Path(old).suffix suffix = Path(old).suffix
new = f"{name}{suffix}" new = f"{name}{suffix}"
n = 2 n = 2
@@ -144,11 +144,6 @@ def rename_media(old: str, name: str) -> str:
if new == old: if new == old:
return old return old
(MEDIA_DIR / old).rename(MEDIA_DIR / new) (MEDIA_DIR / old).rename(MEDIA_DIR / new)
log = MEDIA_DIR / "media.txt"
if log.exists():
lines = log.read_text(encoding="utf-8").splitlines()
lines = [l[:-len(old)] + new if l.endswith(f"saved media: {old}") else l for l in lines]
log.write_text("\n".join(lines) + "\n", encoding="utf-8")
return new return new