diff options
| author | david <[email protected]> | 2026-10-23 04:44:58 -0300 |
|---|---|---|
| committer | david <[email protected]> | 2026-10-23 04:44:58 -0300 |
| commit | 00930d9d75a7dd7d4839b43379d1ef27e08d77b9 (patch) | |
| tree | efc2efea606468e81c1c7b4e532cd2d3a0a29afe | |
| parent | a704744f5c8ea5b0e97acd6d497b48a724b044ec (diff) | |
| download | faggot-chan-00930d9d75a7dd7d4839b43379d1ef27e08d77b9.tar.gz faggot-chan-00930d9d75a7dd7d4839b43379d1ef27e08d77b9.zip | |
vibecoded fixes to fix the vibecoded slop
| -rw-r--r-- | bot.py | 73 | ||||
| -rw-r--r-- | describe.py | 7 |
2 files changed, 21 insertions, 59 deletions
@@ -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 |
