aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordavid <[email protected]>2026-10-23 04:44:58 -0300
committerdavid <[email protected]>2026-10-23 04:44:58 -0300
commit00930d9d75a7dd7d4839b43379d1ef27e08d77b9 (patch)
treeefc2efea606468e81c1c7b4e532cd2d3a0a29afe
parenta704744f5c8ea5b0e97acd6d497b48a724b044ec (diff)
downloadfaggot-chan-00930d9d75a7dd7d4839b43379d1ef27e08d77b9.tar.gz
faggot-chan-00930d9d75a7dd7d4839b43379d1ef27e08d77b9.zip
vibecoded fixes to fix the vibecoded slop
-rw-r--r--bot.py73
-rw-r--r--describe.py7
2 files changed, 21 insertions, 59 deletions
diff --git a/bot.py b/bot.py
index dfa68db..8fa19b1 100644
--- a/bot.py
+++ b/bot.py
@@ -34,7 +34,6 @@ DM_DIR = LOG_DIR / "dm" # one memory file per person so dms never end
ERROR_FILE = LOG_DIR / "errorlog.txt"
MEMORY_FILE = LOG_DIR / "memory.txt"
-MEDIA_FILE = MEDIA_DIR / "media.txt"
THREADS_FILE = CONFIG_DIR / "threads.txt"
PROMPT_FILE = CONFIG_DIR / "system_prompt.txt"
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():
for d in (LOG_DIR, MEDIA_DIR, CONFIG_DIR, DM_DIR):
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)
if not THREADS_FILE.exists():
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
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)
-EMOJI_TAG = re.compile(r'\[emoji:\s*([^\]]+?)\s*\]', re.I) # inline, this one can go anywhere in the message
+REACTION_TAG = re.compile(r'\[(sticker|emoji|media):\s*([^\]]+?)\s*\]', re.I)
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",
"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_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):
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
-saved_media = set() # file names inside MEDIA_DIR
+MEDIA_EXTS = (".gif", ".png", ".jpg", ".jpeg", ".webp", ".mp4", ".webm", ".mov", ".mkv")
-kept_media_lines = []
-with MEDIA_FILE.open("r", encoding="utf-8") as f:
- for line in f:
- if "saved media:" in line:
- name = line.split("saved media:", 1)[1].strip()
- 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
+def scan_media() -> set:
+ # whatever is in the media folder is what she can send, skips media.txt and anything too big to upload
+ 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}
+
+saved_media = scan_media() # file names inside MEDIA_DIR
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
@@ -287,22 +281,19 @@ async def steal_media(url: str, name: str):
saved_media.discard(name)
log_error(f"[{now_str()}] failed to save media {url}: {e}")
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):
- #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 = [
(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:
- # 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):
if part and part.url:
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))
for url, name in media:
@@ -432,15 +423,14 @@ def sync_reactions(guild):
if n not in data[key]:
data[key][n] = ""
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:
REACTIONS_FILE.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
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
# 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)
data = load_reactions()
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))
if not sections:
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. "
- "don't use emojis in every message, most of your messages should have none.") if any(s.startswith("emojis:") for s in sections) else ""
- 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. "
- "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)
+ 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. "
+ "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. "
+ "the tag alone is fine if you have nothing to say. never invent names.\n" + "\n".join(sections))
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)]
-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):
# looks the name up for real, a made up name just does nothing
name = name.strip().strip(":").lower()
@@ -484,7 +464,7 @@ async def send_reaction(message, kind: str, name: str):
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}")
- elif kind == "reaction":
+ elif kind == "emoji":
chosen = next((e for e in message.guild.emojis if e.available and e.name.lower() == name), None)
if chosen:
await message.add_reaction(chosen)
@@ -607,10 +587,9 @@ async def on_message(message):
if not result.strip() and not reactions:
return
- shown = fill_emojis(result, message.guild) # what gets sent, result keeps the [emoji: x] tags for the log and history
try:
- if shown.strip():
- await send_reply(message, shown)
+ if result.strip():
+ await send_reply(message, 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) == settings["moralfag_id"]:
@@ -623,16 +602,4 @@ async def on_message(message):
except Exception as e:
await handle_error(f"[{now_str()}] failed to send reply: {e}", notify_channel=message.channel)
-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)
diff --git a/describe.py b/describe.py
index 1cce292..5a9621e 100644
--- a/describe.py
+++ b/describe.py
@@ -134,7 +134,7 @@ def split_name(text: str) -> tuple[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
new = f"{name}{suffix}"
n = 2
@@ -144,11 +144,6 @@ def rename_media(old: str, name: str) -> str:
if new == old:
return old
(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