Refactor message handling by introducing TextBot class; enhance reply and reaction logic
This commit is contained in:
parent
be0567ff0f
commit
bab22747b4
4 changed files with 177 additions and 83 deletions
|
@ -6,6 +6,7 @@ import tomllib
|
|||
import discord
|
||||
|
||||
from botbotbot.ai import AIBot
|
||||
from botbotbot.text import TextBot
|
||||
from botbotbot.tts import CambAI
|
||||
|
||||
|
||||
|
@ -50,81 +51,15 @@ def main() -> None:
|
|||
|
||||
bot = discord.Bot(description=description, intents=intents)
|
||||
|
||||
text_bot = TextBot(bot, aibot, word_list)
|
||||
text_bot.init_events()
|
||||
|
||||
shuffle_tasks = set()
|
||||
|
||||
@bot.listen("on_ready")
|
||||
async def on_ready() -> None:
|
||||
logger.info(f"We have logged in as {bot.user}")
|
||||
|
||||
async def reply(message: discord.Message) -> None:
|
||||
logger.info(f"Reply to {message.author}")
|
||||
mention = random.choices(
|
||||
[f"<@{message.author.id}>", "@everyone", "@here"], weights=(98, 1, 1)
|
||||
)[0]
|
||||
content = random.choice(
|
||||
(
|
||||
f"{mention}, {random.choice(word_list)}",
|
||||
f"{random.choice(word_list)}",
|
||||
)
|
||||
)
|
||||
|
||||
if isinstance(message.channel, discord.TextChannel) and random.random() < 0.1:
|
||||
await send_as_webhook(
|
||||
message.channel,
|
||||
message.author.display_name,
|
||||
message.author.avatar.url if message.author.avatar else None,
|
||||
content,
|
||||
)
|
||||
else:
|
||||
fct = random.choice((message.reply, message.channel.send))
|
||||
|
||||
await fct(content)
|
||||
|
||||
async def ai_reply(message: discord.Message) -> None:
|
||||
if aibot is None:
|
||||
return
|
||||
|
||||
logger.info(f"AI Reply to {message.author}")
|
||||
prompt = message.clean_content
|
||||
if prompt == "" and message.embeds and message.embeds[0].description:
|
||||
prompt = message.embeds[0].description
|
||||
|
||||
answer = aibot.answer(prompt)
|
||||
if not isinstance(answer, str):
|
||||
return
|
||||
|
||||
if len(answer) > 2000:
|
||||
embed = discord.Embed(
|
||||
description=answer,
|
||||
thumbnail="https://mistral.ai/images/favicon/favicon-32x32.png",
|
||||
)
|
||||
await message.reply(embed=embed)
|
||||
else:
|
||||
await message.reply(answer)
|
||||
|
||||
async def react(message: discord.Message) -> None:
|
||||
if message.guild is None:
|
||||
return
|
||||
await message.add_reaction(random.choice(message.guild.emojis))
|
||||
|
||||
@bot.listen("on_message")
|
||||
async def on_message(message: discord.Message) -> None:
|
||||
if message.flags.ephemeral:
|
||||
return
|
||||
|
||||
if message.author != bot.user and bot.user in message.mentions:
|
||||
logger.info(f"{message.author} metion")
|
||||
await random.choice((reply, react))(message)
|
||||
return
|
||||
|
||||
probas = [10, 5, 10]
|
||||
|
||||
func = random.choices(
|
||||
(reply, ai_reply, react, None), weights=probas + [100 - sum(probas)]
|
||||
)[0]
|
||||
if func is not None:
|
||||
await func(message)
|
||||
|
||||
@bot.listen("on_reaction_add")
|
||||
async def add_more_reaction(
|
||||
reaction: discord.Reaction, user: discord.Member | discord.User
|
||||
|
@ -329,18 +264,4 @@ def main() -> None:
|
|||
await ctx.respond(embed=embed)
|
||||
logger.info("FIN CHAN")
|
||||
|
||||
async def send_as_webhook(
|
||||
channel: discord.TextChannel,
|
||||
name: str,
|
||||
avatar_url: str | None,
|
||||
content: str,
|
||||
) -> None:
|
||||
webhooks = await channel.webhooks()
|
||||
webhook = discord.utils.get(webhooks, name="BotbotbotHook")
|
||||
|
||||
if webhook is None:
|
||||
webhook = await channel.create_webhook(name="BotbotbotHook")
|
||||
|
||||
await webhook.send(content=content, username=name, avatar_url=avatar_url)
|
||||
|
||||
bot.run(config.get("token"))
|
||||
|
|
146
botbotbot/text.py
Normal file
146
botbotbot/text.py
Normal file
|
@ -0,0 +1,146 @@
|
|||
import logging
|
||||
import random
|
||||
from typing import Any, Callable, Coroutine
|
||||
|
||||
import discord
|
||||
import emoji
|
||||
|
||||
from botbotbot.ai import AIBot
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TextBot:
|
||||
uni_emojis = tuple(k for k in emoji.EMOJI_DATA.keys() if len(k) == 1)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
bot: discord.Bot,
|
||||
aibot: AIBot | None = None,
|
||||
wordlist: list[str] = [],
|
||||
rnd_weights: list[float] = [10, 5, 10],
|
||||
) -> None:
|
||||
self.bot = bot
|
||||
self.aibot = aibot
|
||||
self.word_list = wordlist
|
||||
self._rnd_weights = rnd_weights
|
||||
|
||||
def init_events(self) -> None:
|
||||
self.bot.add_listener(self.on_message, "on_message")
|
||||
|
||||
@property
|
||||
def rnd_weights(self) -> list[float]:
|
||||
return self._rnd_weights + [100 - sum(self._rnd_weights)]
|
||||
|
||||
@property
|
||||
def rnd_functions(
|
||||
self,
|
||||
) -> list[Callable[[discord.Message], Coroutine[Any, Any, None]]]:
|
||||
return [self.reply, self.ai_reply, self.react]
|
||||
|
||||
@property
|
||||
def rnd_functions_or_not(
|
||||
self,
|
||||
) -> list[Callable[[discord.Message], Coroutine[Any, Any, None]] | None]:
|
||||
return [*self.rnd_functions, None]
|
||||
|
||||
async def on_message(self, message: discord.Message) -> None:
|
||||
logger.debug(
|
||||
f"Received message from <{message.author}> on channel <{message.channel}>."
|
||||
)
|
||||
if message.flags.ephemeral:
|
||||
logger.debug("Ephemeral message, ignoring.")
|
||||
return
|
||||
|
||||
if message.author != self.bot.user and self.bot.user in message.mentions:
|
||||
logger.info(
|
||||
f"Mention from <{message.author}> in channel <{message.channel}>."
|
||||
)
|
||||
await random.choices(self.rnd_functions, weights=self.rnd_weights[:-1])[0](
|
||||
message
|
||||
)
|
||||
return
|
||||
|
||||
func = random.choices(self.rnd_functions_or_not, weights=self.rnd_weights)[0]
|
||||
|
||||
if func is None:
|
||||
logger.debug("No action.")
|
||||
return
|
||||
|
||||
await func(message)
|
||||
|
||||
async def reply(self, message: discord.Message) -> None:
|
||||
logger.info(f"Replying to <{message.author}> in channel <{message.channel}>.")
|
||||
|
||||
mention = random.choices(
|
||||
[f"<@{message.author.id}>", "@everyone", "@here"], weights=(97, 1, 2)
|
||||
)[0]
|
||||
content = random.choice(
|
||||
(
|
||||
f"{mention}, {random.choice(self.word_list)}",
|
||||
f"{random.choice(self.word_list)}",
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
isinstance(message.channel, discord.TextChannel)
|
||||
and random.random() < 10 / 100
|
||||
):
|
||||
await self.send_as_webhook(
|
||||
message.channel,
|
||||
message.author.display_name,
|
||||
message.author.avatar.url if message.author.avatar else None,
|
||||
content,
|
||||
)
|
||||
else:
|
||||
fct = random.choice((message.reply, message.channel.send))
|
||||
|
||||
await fct(content)
|
||||
|
||||
async def ai_reply(self, message: discord.Message) -> None:
|
||||
if self.aibot is None:
|
||||
logger.debug("No AI bot, ignoring.")
|
||||
return
|
||||
|
||||
logger.info(f"AI Reply to {message.author}")
|
||||
prompt = message.clean_content
|
||||
if prompt == "" and message.embeds and message.embeds[0].description:
|
||||
prompt = message.embeds[0].description
|
||||
|
||||
answer = self.aibot.answer(prompt)
|
||||
if not isinstance(answer, str):
|
||||
logger.error(f"Got unexpected result from AIBot : {answer}")
|
||||
return
|
||||
|
||||
if len(answer) > 2000:
|
||||
logger.debug("Answer too long, sending as embed.")
|
||||
embed = discord.Embed(
|
||||
description=answer,
|
||||
thumbnail="https://mistral.ai/images/favicon/favicon-32x32.png",
|
||||
)
|
||||
await message.reply(embed=embed)
|
||||
else:
|
||||
await message.reply(answer)
|
||||
|
||||
async def react(self, message: discord.Message) -> None:
|
||||
emojis: tuple[str | discord.Emoji, ...] = self.uni_emojis
|
||||
if message.guild is not None and random.random() < 50 / 100:
|
||||
emojis = message.guild.emojis
|
||||
|
||||
emo: str | discord.Emoji = random.choice(emojis)
|
||||
await message.add_reaction(emo)
|
||||
|
||||
async def send_as_webhook(
|
||||
self,
|
||||
channel: discord.TextChannel,
|
||||
name: str,
|
||||
avatar_url: str | None,
|
||||
content: str,
|
||||
) -> None:
|
||||
webhooks = await channel.webhooks()
|
||||
webhook = discord.utils.get(webhooks, name="BotbotbotHook")
|
||||
|
||||
if webhook is None:
|
||||
webhook = await channel.create_webhook(name="BotbotbotHook")
|
||||
|
||||
await webhook.send(content=content, username=name, avatar_url=avatar_url)
|
|
@ -6,6 +6,7 @@ readme = "README.md"
|
|||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"audioop-lts>=0.2.1",
|
||||
"emoji>=2.14.1",
|
||||
"mistralai>=1.6.0",
|
||||
"py-cord>=2.6.1",
|
||||
"pynacl>=1.5.0",
|
||||
|
@ -21,6 +22,7 @@ dev = [
|
|||
"mypy>=1.15.0",
|
||||
"pre-commit>=4.2.0",
|
||||
"ruff>=0.11.2",
|
||||
"types-requests>=2.32.0.20250306",
|
||||
]
|
||||
|
||||
[tool.mypy]
|
||||
|
|
25
uv.lock
generated
25
uv.lock
generated
|
@ -133,6 +133,7 @@ version = "0.1.0"
|
|||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "audioop-lts" },
|
||||
{ name = "emoji" },
|
||||
{ name = "mistralai" },
|
||||
{ name = "py-cord" },
|
||||
{ name = "pynacl" },
|
||||
|
@ -145,11 +146,13 @@ dev = [
|
|||
{ name = "mypy" },
|
||||
{ name = "pre-commit" },
|
||||
{ name = "ruff" },
|
||||
{ name = "types-requests" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "audioop-lts", specifier = ">=0.2.1" },
|
||||
{ name = "emoji", specifier = ">=2.14.1" },
|
||||
{ name = "mistralai", specifier = ">=1.6.0" },
|
||||
{ name = "py-cord", specifier = ">=2.6.1" },
|
||||
{ name = "pynacl", specifier = ">=1.5.0" },
|
||||
|
@ -162,6 +165,7 @@ dev = [
|
|||
{ name = "mypy", specifier = ">=1.15.0" },
|
||||
{ name = "pre-commit", specifier = ">=4.2.0" },
|
||||
{ name = "ruff", specifier = ">=0.11.2" },
|
||||
{ name = "types-requests", specifier = ">=2.32.0.20250306" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -235,6 +239,15 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "emoji"
|
||||
version = "2.14.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cb/7d/01cddcbb6f5cc0ba72e00ddf9b1fa206c802d557fd0a20b18e130edf1336/emoji-2.14.1.tar.gz", hash = "sha256:f8c50043d79a2c1410ebfae833ae1868d5941a67a6cd4d18377e2eb0bd79346b", size = 597182 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/91/db/a0335710caaa6d0aebdaa65ad4df789c15d89b7babd9a30277838a7d9aac/emoji-2.14.1-py3-none-any.whl", hash = "sha256:35a8a486c1460addb1499e3bf7929d3889b2e2841a57401903699fef595e942b", size = 590617 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "eval-type-backport"
|
||||
version = "0.2.2"
|
||||
|
@ -666,6 +679,18 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "types-requests"
|
||||
version = "2.32.0.20250306"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/09/1a/beaeff79ef9efd186566ba5f0d95b44ae21f6d31e9413bcfbef3489b6ae3/types_requests-2.32.0.20250306.tar.gz", hash = "sha256:0962352694ec5b2f95fda877ee60a159abdf84a0fc6fdace599f20acb41a03d1", size = 23012 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/99/26/645d89f56004aa0ba3b96fec27793e3c7e62b40982ee069e52568922b6db/types_requests-2.32.0.20250306-py3-none-any.whl", hash = "sha256:25f2cbb5c8710b2022f8bbee7b2b66f319ef14aeea2f35d80f18c9dbf3b60a0b", size = 20673 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.12.2"
|
||||
|
|
Loading…
Add table
Reference in a new issue