Update .gitignore and pyproject.toml; streamline main entry point for BotBotBot

This commit is contained in:
Edgar P. Burkhart 2025-03-22 18:32:13 +01:00
parent 761111bb07
commit bc9d5a8943
Signed by: edpibu
GPG key ID: 9833D3C5A25BD227
4 changed files with 356 additions and 350 deletions

19
.gitignore vendored
View file

@ -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

View file

@ -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"))

View file

@ -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()

View file

@ -11,6 +11,9 @@ dependencies = [
"pynacl>=1.5.0",
]
[project.scripts]
hasspy = "botbotbot:main"
[dependency-groups]
dev = [
"isort>=6.0.1",