From bc9d5a8943f47ef20bb89afbe14d6b03b914a630 Mon Sep 17 00:00:00 2001 From: "Edgar P. Burkhart" <git@edgarpierre.fr> Date: Sat, 22 Mar 2025 18:32:13 +0100 Subject: [PATCH] Update .gitignore and pyproject.toml; streamline main entry point for BotBotBot --- .gitignore | 19 ++- botbotbot/__init__.py | 337 ++++++++++++++++++++++++++++++++++++++++ botbotbot/__main__.py | 347 +----------------------------------------- pyproject.toml | 3 + 4 files changed, 356 insertions(+), 350 deletions(-) diff --git a/.gitignore b/.gitignore index 7a7b50c..3931c59 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,14 @@ -/env -/assets -/config.toml -/wordlist.pickle -__pycache__ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv + +# BotBotBot +config.toml +wordlist.pickle diff --git a/botbotbot/__init__.py b/botbotbot/__init__.py index e69de29..0b21a27 100644 --- a/botbotbot/__init__.py +++ b/botbotbot/__init__.py @@ -0,0 +1,337 @@ +import asyncio +import logging +import pickle +import random +import tomllib + +import discord + +from botbotbot.ai import AIBot + + +def main() -> None: + description = """Discord Chaos Bot""" + + logger = logging.getLogger(__name__) + logging.basicConfig(level=logging.INFO) + + with open("config.toml", "rb") as config_file: + config = tomllib.load(config_file) + + with open("wordlist.pickle", "rb") as word_file: + word_list = pickle.load(word_file) + guild_ids = config.get("guild_ids") + delay = config.get("delay", 60) + + system_prompt = """Tu es une intelligence artificelle qui répond en français. + Tu dois faire un commentaire pertinent en lien avec ce qui te sera dit. + Ta réponse doit être très courte. + Ta réponse doit être une seule phrase. + TA RÉPONSE DOIT ÊTRE EN FRANÇAIS !!!""" + + aibot: AIBot | None = None + + if isinstance(key := config.get("mistral_api_key"), str): + aibot = AIBot( + key, + model="mistral-large-latest", + system_message=system_prompt, + ) + + intents = discord.Intents.default() + intents.members = True + intents.message_content = True + intents.reactions = True + intents.voice_states = True + + bot = discord.Bot(description=description, intents=intents) + + 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 + ) -> None: + if random.random() < 50 / 100: + logger.info(f"Copy reaction from {user}") + await reaction.message.add_reaction(reaction.emoji) + + @bot.listen("on_message_edit") + async def react_message_edit( + before: discord.Message, after: discord.Message + ) -> None: + if ( + before.content != after.content + and after.author != bot.user + and random.random() < 50 / 100 + ): + logger.info(f"React to edit from {after.author}") + await after.add_reaction("👀") + + @bot.listen("on_message") + async def rando_shuffle(message: discord.Message) -> None: + if not message.flags.ephemeral and random.random() < 5 / 100 and message.guild: + logger.info(f"Message shuffle after message from {message.author}") + await try_shuffle(message.guild) + + def save_wordlist() -> None: + logger.info("Saving updated wordlist") + with open("wordlist.pickle", "wb") as word_file: + pickle.dump(word_list, word_file) + + @bot.slash_command( + name="bibl", guild_ids=guild_ids, description="Ajouter une phrase" + ) + async def bibl(ctx: discord.ApplicationContext, phrase: str) -> None: + logger.info(f"BIBL {ctx.author} {phrase}") + word_list.append(phrase) + embed = discord.Embed( + title="BIBL", description=phrase, color=discord.Colour.green() + ) + await ctx.respond(embed=embed) + save_wordlist() + logger.info("FIN BIBL") + + @bot.slash_command( + name="tabl", guild_ids=guild_ids, description="Lister les phrases" + ) + async def tabl(ctx: discord.ApplicationContext) -> None: + logger.info(f"TABL {ctx.author}") + embed = discord.Embed( + title="TABL", description="\n".join(word_list), color=discord.Colour.green() + ) + await ctx.respond(embed=embed, ephemeral=True, delete_after=delay) + + @bot.slash_command( + name="enle", guild_ids=guild_ids, description="Enlever une phrase" + ) + async def enle(ctx: discord.ApplicationContext, phrase: str) -> None: + logger.info(f"ENLE {ctx.author} {phrase}") + try: + word_list.remove(phrase) + except ValueError: + embed = discord.Embed( + title="ERRE ENLE", description=phrase, color=discord.Colour.red() + ) + await ctx.respond(embed=embed) + logger.info("ERRE ENLE") + else: + embed = discord.Embed( + title="ENLE", description=f"~~{phrase}~~", color=discord.Colour.green() + ) + await ctx.respond(embed=embed, ephemeral=True, delete_after=delay) + save_wordlist() + logger.info("FIN ENLE") + + async def try_shuffle(guild: discord.Guild) -> bool: + if guild.id in shuffle_tasks: + return False + + shuffle_tasks.add(guild.id) + await shuffle_nicks(guild) + shuffle_tasks.discard(guild.id) + return True + + async def shuffle_nicks(guild: discord.Guild) -> None: + logger.info("Shuffle") + members = guild.members + if guild.owner: + members.remove(guild.owner) + + nicks = [member.nick for member in members] + + random.shuffle(nicks) + for member, nick in zip(members, nicks): + logger.info(f"{member} {nick}") + await member.edit(nick=nick) + logger.info("Shuffle done") + + @bot.slash_command( + name="alea", guild_ids=guild_ids, description="Modifier les pseudos" + ) + async def alea(ctx: discord.ApplicationContext) -> None: + logger.info(f"ALEA {ctx.author}") + await ctx.defer() + if await try_shuffle(ctx.guild): + embed = discord.Embed(title="ALEA", color=discord.Colour.green()) + await ctx.respond(embed=embed, ephemeral=True, delete_after=delay) + logger.info("FIN ALEA") + else: + embed = discord.Embed(title="ERRE ALEA", color=discord.Colour.red()) + await ctx.respond(embed=embed) + logger.info("ERRE ALEA") + + @bot.listen("on_voice_state_update") + async def voice_random_nicks( + member: discord.Member, before: discord.VoiceState, after: discord.VoiceState + ) -> None: + if before.channel is None and random.random() < 5 / 100: + logger.info(f"Voice shuffle from {member}") + await try_shuffle(member.guild) + + logger.debug("Voice state update") + logger.debug(before.channel) + logger.debug(after.channel) + if after.channel: + logger.debug(after.channel.members) + if ( + before.channel is None + and after.channel is not None + and random.random() < 5 / 100 + and bot not in after.channel.members + ): + logger.info(f"Voice connect from {member}") + source = await discord.FFmpegOpusAudio.from_probe("assets/allo.ogg") + + await asyncio.sleep(random.randrange(60)) + vo: discord.VoiceClient = await after.channel.connect() + + await asyncio.sleep(random.randrange(10)) + await vo.play(source, wait_finish=True) + + await asyncio.sleep(random.randrange(60)) + await vo.disconnect() + logger.info("Voice disconnect") + + @bot.slash_command( + name="indu", guild_ids=guild_ids, description="Poser une question à MistralAI" + ) + async def indu(ctx: discord.ApplicationContext, prompt: str) -> None: + if aibot is None: + return + logger.info(f"INDU {ctx.author} {prompt}") + await ctx.defer() + res_stream = await aibot.get_response_stream(prompt) + + embed = discord.Embed( + title=prompt, + description="", + thumbnail="https://mistral.ai/images/favicon/favicon-32x32.png", + color=discord.Colour.orange(), + ) + message = await ctx.respond(embed=embed) + + async for chunk in res_stream: + if chunk.data.choices[0].delta.content is not None: + embed.description += chunk.data.choices[0].delta.content + await message.edit(embed=embed) + + embed.colour = None + await message.edit(embed=embed) + logger.info("FIN INDU") + + @bot.slash_command( + name="chan", guild_ids=guild_ids, description="Donner de nouveaux pseudos" + ) + async def chan(ctx: discord.ApplicationContext, file: discord.Attachment) -> None: + logger.info(f"CHAN {ctx.author}") + await ctx.defer() + + members = ctx.guild.members + members.remove(ctx.guild.owner) + + nicks = (await file.read()).decode().splitlines() + if len(nicks) < len(members): + embed = discord.Embed(title="ERRE CHAN", color=discord.Colour.red()) + await ctx.respond(embed=embed) + return + + nicks = random.choices(nicks, k=len(members)) + for member, nick in zip(members, nicks): + logger.info(member, nick) + await member.edit(nick=nick) + + embed = discord.Embed( + title="CHAN", description="\n".join(nicks), color=discord.Colour.green() + ) + 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")) diff --git a/botbotbot/__main__.py b/botbotbot/__main__.py index f9c60cd..0e49026 100644 --- a/botbotbot/__main__.py +++ b/botbotbot/__main__.py @@ -1,346 +1,3 @@ -import asyncio -import logging -import pickle -import random -import tomllib +from botbotbot import main -import discord - -from botbotbot.ai import AIBot - -description = """BotBotBot""" - -logger = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO) - -with open("config.toml", "rb") as config_file: - config = tomllib.load(config_file) - -with open("wordlist.pickle", "rb") as word_file: - word_list = pickle.load(word_file) -guild_ids = config.get("guild_ids") -delay = config.get("delay", 60) - -system_prompt = """Tu es une intelligence artificelle qui répond en français. -Tu dois faire un commentaire pertinent en lien avec ce qui te sera dit. -Ta réponse doit être très courte. -Ta réponse doit être une seule phrase. -TA RÉPONSE DOIT ÊTRE EN FRANÇAIS !!!""" - -aibot: AIBot | None = None - -if isinstance(key := config.get("mistral_api_key"), str): - aibot = AIBot( - key, - model="mistral-large-latest", - system_message=system_prompt, - ) - -intents = discord.Intents.default() -intents.members = True -intents.message_content = True -intents.reactions = True -intents.voice_states = True - -bot = discord.Bot(description=description, intents=intents) - - -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 -) -> None: - if random.random() < 50 / 100: - logger.info(f"Copy reaction from {user}") - await reaction.message.add_reaction(reaction.emoji) - - -@bot.listen("on_message_edit") -async def react_message_edit(before: discord.Message, after: discord.Message) -> None: - if ( - before.content != after.content - and after.author != bot.user - and random.random() < 50 / 100 - ): - logger.info(f"React to edit from {after.author}") - await after.add_reaction("👀") - - -@bot.listen("on_message") -async def rando_shuffle(message: discord.Message) -> None: - if not message.flags.ephemeral and random.random() < 5 / 100 and message.guild: - logger.info(f"Message shuffle after message from {message.author}") - await try_shuffle(message.guild) - - -def save_wordlist() -> None: - logger.info("Saving updated wordlist") - with open("wordlist.pickle", "wb") as word_file: - pickle.dump(word_list, word_file) - - -@bot.slash_command(name="bibl", guild_ids=guild_ids, description="Ajouter une phrase") -async def bibl(ctx: discord.ApplicationContext, phrase: str) -> None: - logger.info(f"BIBL {ctx.author} {phrase}") - word_list.append(phrase) - embed = discord.Embed( - title="BIBL", description=phrase, color=discord.Colour.green() - ) - await ctx.respond(embed=embed) - save_wordlist() - logger.info("FIN BIBL") - - -@bot.slash_command(name="tabl", guild_ids=guild_ids, description="Lister les phrases") -async def tabl(ctx: discord.ApplicationContext) -> None: - logger.info(f"TABL {ctx.author}") - embed = discord.Embed( - title="TABL", description="\n".join(word_list), color=discord.Colour.green() - ) - await ctx.respond(embed=embed, ephemeral=True, delete_after=delay) - - -@bot.slash_command(name="enle", guild_ids=guild_ids, description="Enlever une phrase") -async def enle(ctx: discord.ApplicationContext, phrase: str) -> None: - logger.info(f"ENLE {ctx.author} {phrase}") - try: - word_list.remove(phrase) - except ValueError: - embed = discord.Embed( - title="ERRE ENLE", description=phrase, color=discord.Colour.red() - ) - await ctx.respond(embed=embed) - logger.info("ERRE ENLE") - else: - embed = discord.Embed( - title="ENLE", description=f"~~{phrase}~~", color=discord.Colour.green() - ) - await ctx.respond(embed=embed, ephemeral=True, delete_after=delay) - save_wordlist() - logger.info("FIN ENLE") - - -async def try_shuffle(guild: discord.Guild) -> bool: - if guild.id in shuffle_tasks: - return False - - shuffle_tasks.add(guild.id) - await shuffle_nicks(guild) - shuffle_tasks.discard(guild.id) - return True - - -async def shuffle_nicks(guild: discord.Guild) -> None: - logger.info("Shuffle") - members = guild.members - if guild.owner: - members.remove(guild.owner) - - nicks = [member.nick for member in members] - - random.shuffle(nicks) - for member, nick in zip(members, nicks): - logger.info(f"{member} {nick}") - await member.edit(nick=nick) - logger.info("Shuffle done") - - -@bot.slash_command(name="alea", guild_ids=guild_ids, description="Modifier les pseudos") -async def alea(ctx: discord.ApplicationContext) -> None: - logger.info(f"ALEA {ctx.author}") - await ctx.defer() - if await try_shuffle(ctx.guild): - embed = discord.Embed(title="ALEA", color=discord.Colour.green()) - await ctx.respond(embed=embed, ephemeral=True, delete_after=delay) - logger.info("FIN ALEA") - else: - embed = discord.Embed(title="ERRE ALEA", color=discord.Colour.red()) - await ctx.respond(embed=embed) - logger.info("ERRE ALEA") - - -@bot.listen("on_voice_state_update") -async def voice_random_nicks( - member: discord.Member, before: discord.VoiceState, after: discord.VoiceState -) -> None: - if before.channel is None and random.random() < 5 / 100: - logger.info(f"Voice shuffle from {member}") - await try_shuffle(member.guild) - - logger.debug("Voice state update") - logger.debug(before.channel) - logger.debug(after.channel) - if after.channel: - logger.debug(after.channel.members) - if ( - before.channel is None - and after.channel is not None - and random.random() < 5 / 100 - and bot not in after.channel.members - ): - logger.info(f"Voice connect from {member}") - source = await discord.FFmpegOpusAudio.from_probe("assets/allo.ogg") - - await asyncio.sleep(random.randrange(60)) - vo: discord.VoiceClient = await after.channel.connect() - - await asyncio.sleep(random.randrange(10)) - await vo.play(source, wait_finish=True) - - await asyncio.sleep(random.randrange(60)) - await vo.disconnect() - logger.info("Voice disconnect") - - -@bot.slash_command( - name="indu", guild_ids=guild_ids, description="Poser une question à MistralAI" -) -async def indu(ctx: discord.ApplicationContext, prompt: str) -> None: - if aibot is None: - return - logger.info(f"INDU {ctx.author} {prompt}") - await ctx.defer() - res_stream = await aibot.get_response_stream(prompt) - - embed = discord.Embed( - title=prompt, - description="", - thumbnail="https://mistral.ai/images/favicon/favicon-32x32.png", - color=discord.Colour.orange(), - ) - message = await ctx.respond(embed=embed) - - async for chunk in res_stream: - if chunk.data.choices[0].delta.content is not None: - embed.description += chunk.data.choices[0].delta.content - await message.edit(embed=embed) - - embed.colour = None - await message.edit(embed=embed) - logger.info("FIN INDU") - - -@bot.slash_command( - name="chan", guild_ids=guild_ids, description="Donner de nouveaux pseudos" -) -async def chan(ctx: discord.ApplicationContext, file: discord.Attachment) -> None: - logger.info(f"CHAN {ctx.author}") - await ctx.defer() - - members = ctx.guild.members - members.remove(ctx.guild.owner) - - nicks = (await file.read()).decode().splitlines() - if len(nicks) < len(members): - embed = discord.Embed(title="ERRE CHAN", color=discord.Colour.red()) - await ctx.respond(embed=embed) - return - - nicks = random.choices(nicks, k=len(members)) - for member, nick in zip(members, nicks): - logger.info(member, nick) - await member.edit(nick=nick) - - embed = discord.Embed( - title="CHAN", description="\n".join(nicks), color=discord.Colour.green() - ) - 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")) +main() diff --git a/pyproject.toml b/pyproject.toml index 4ac1eb2..704c48d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,9 @@ dependencies = [ "pynacl>=1.5.0", ] +[project.scripts] +hasspy = "botbotbot:main" + [dependency-groups] dev = [ "isort>=6.0.1",