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
|
import discord
|
||||||
|
|
||||||
from botbotbot.ai import AIBot
|
from botbotbot.ai import AIBot
|
||||||
|
from botbotbot.text import TextBot
|
||||||
from botbotbot.tts import CambAI
|
from botbotbot.tts import CambAI
|
||||||
|
|
||||||
|
|
||||||
|
@ -50,81 +51,15 @@ def main() -> None:
|
||||||
|
|
||||||
bot = discord.Bot(description=description, intents=intents)
|
bot = discord.Bot(description=description, intents=intents)
|
||||||
|
|
||||||
|
text_bot = TextBot(bot, aibot, word_list)
|
||||||
|
text_bot.init_events()
|
||||||
|
|
||||||
shuffle_tasks = set()
|
shuffle_tasks = set()
|
||||||
|
|
||||||
@bot.listen("on_ready")
|
@bot.listen("on_ready")
|
||||||
async def on_ready() -> None:
|
async def on_ready() -> None:
|
||||||
logger.info(f"We have logged in as {bot.user}")
|
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")
|
@bot.listen("on_reaction_add")
|
||||||
async def add_more_reaction(
|
async def add_more_reaction(
|
||||||
reaction: discord.Reaction, user: discord.Member | discord.User
|
reaction: discord.Reaction, user: discord.Member | discord.User
|
||||||
|
@ -329,18 +264,4 @@ def main() -> None:
|
||||||
await ctx.respond(embed=embed)
|
await ctx.respond(embed=embed)
|
||||||
logger.info("FIN CHAN")
|
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"))
|
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"
|
requires-python = ">=3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"audioop-lts>=0.2.1",
|
"audioop-lts>=0.2.1",
|
||||||
|
"emoji>=2.14.1",
|
||||||
"mistralai>=1.6.0",
|
"mistralai>=1.6.0",
|
||||||
"py-cord>=2.6.1",
|
"py-cord>=2.6.1",
|
||||||
"pynacl>=1.5.0",
|
"pynacl>=1.5.0",
|
||||||
|
@ -21,6 +22,7 @@ dev = [
|
||||||
"mypy>=1.15.0",
|
"mypy>=1.15.0",
|
||||||
"pre-commit>=4.2.0",
|
"pre-commit>=4.2.0",
|
||||||
"ruff>=0.11.2",
|
"ruff>=0.11.2",
|
||||||
|
"types-requests>=2.32.0.20250306",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
|
|
25
uv.lock
generated
25
uv.lock
generated
|
@ -133,6 +133,7 @@ version = "0.1.0"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "audioop-lts" },
|
{ name = "audioop-lts" },
|
||||||
|
{ name = "emoji" },
|
||||||
{ name = "mistralai" },
|
{ name = "mistralai" },
|
||||||
{ name = "py-cord" },
|
{ name = "py-cord" },
|
||||||
{ name = "pynacl" },
|
{ name = "pynacl" },
|
||||||
|
@ -145,11 +146,13 @@ dev = [
|
||||||
{ name = "mypy" },
|
{ name = "mypy" },
|
||||||
{ name = "pre-commit" },
|
{ name = "pre-commit" },
|
||||||
{ name = "ruff" },
|
{ name = "ruff" },
|
||||||
|
{ name = "types-requests" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "audioop-lts", specifier = ">=0.2.1" },
|
{ name = "audioop-lts", specifier = ">=0.2.1" },
|
||||||
|
{ name = "emoji", specifier = ">=2.14.1" },
|
||||||
{ name = "mistralai", specifier = ">=1.6.0" },
|
{ name = "mistralai", specifier = ">=1.6.0" },
|
||||||
{ name = "py-cord", specifier = ">=2.6.1" },
|
{ name = "py-cord", specifier = ">=2.6.1" },
|
||||||
{ name = "pynacl", specifier = ">=1.5.0" },
|
{ name = "pynacl", specifier = ">=1.5.0" },
|
||||||
|
@ -162,6 +165,7 @@ dev = [
|
||||||
{ name = "mypy", specifier = ">=1.15.0" },
|
{ name = "mypy", specifier = ">=1.15.0" },
|
||||||
{ name = "pre-commit", specifier = ">=4.2.0" },
|
{ name = "pre-commit", specifier = ">=4.2.0" },
|
||||||
{ name = "ruff", specifier = ">=0.11.2" },
|
{ name = "ruff", specifier = ">=0.11.2" },
|
||||||
|
{ name = "types-requests", specifier = ">=2.32.0.20250306" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[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 },
|
{ 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]]
|
[[package]]
|
||||||
name = "eval-type-backport"
|
name = "eval-type-backport"
|
||||||
version = "0.2.2"
|
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 },
|
{ 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]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.12.2"
|
version = "4.12.2"
|
||||||
|
|
Loading…
Add table
Reference in a new issue