diff --git a/bot.py b/bot.py index a012827..4a13a57 100644 --- a/bot.py +++ b/bot.py @@ -85,7 +85,8 @@ 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|emoji|media):\s*([^\]]+?)\s*\]', re.I) +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 QUIET_COOLDOWN = 600 # seconds of silence before she's allowed to send a random message (10 mins) @@ -297,11 +298,11 @@ async def steal_attachments(message): ] 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 + # 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"): + 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"): media.append((part.url, hashlib.sha1(part.url.encode()).hexdigest()[:16] + ext)) for url, name in media: @@ -456,13 +457,23 @@ 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 "" - 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)) + 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) 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() @@ -473,7 +484,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 == "emoji": + elif kind == "reaction": 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) @@ -596,9 +607,10 @@ 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 result.strip(): - await send_reply(message, result) + if shown.strip(): + await send_reply(message, shown) 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"]: @@ -611,4 +623,16 @@ async def on_message(message): except Exception as e: 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)