Skip to content

Commit

Permalink
Merge branch 'SunRiseVersion' into 109-improve-pluie-slash-command-by…
Browse files Browse the repository at this point in the history
…-adding-raining-data-for-the-next-day
  • Loading branch information
clement-pages authored Dec 19, 2023
2 parents 3e7c4f9 + d0f1cca commit 0b9f393
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 336 deletions.
5 changes: 4 additions & 1 deletion main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ async def main():
test_mode = "FLY_ALLOC_ID" not in os.environ

await bot.add_cog(SunController(bot=bot, test_mode=test_mode))
await bot.start(os.environ['token'])
# run the bot until shut down
await bot.start(os.environ['token'], reconnect=False)

logging.info("bot was disconnected")


###########################################################################
Expand Down
258 changes: 81 additions & 177 deletions poetry.lock

Large diffs are not rendered by default.

115 changes: 32 additions & 83 deletions sunbot/SunController.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ def __init__(self, bot: commands.Bot, test_mode : bool = False) -> None:
# Dict containing all the servers to which the bot belongs
self.srv_dict: Dict[int, SunServer] = {}
# Handler for daily weather events
self.daily_weather_handler = DailyWeatherEvent(f"{self.data_mount_pt}/save/daily_weather_sub.json")
self.daily_weather_handler = DailyWeatherEvent(
f"{self.data_mount_pt}save/daily_weather_sub.json"
)

@commands.Cog.listener()
async def on_ready(self) -> None:
Expand All @@ -91,7 +93,12 @@ async def on_ready(self) -> None:
if user.id not in self.usr_dict:
self.usr_dict[user.id] = current_usr
self.srv_dict[server.id].addUser(current_usr)
loop = asyncio.get_event_loop()
# load daily weather data
await self.daily_weather_handler.load_locations_subscribers(
self.bot.get_user,
self.bot.get_channel,
)
loop = asyncio.get_running_loop()
# setup signal handlers:
loop.add_signal_handler(signal.SIGINT,
lambda: asyncio.create_task(self.on_shut_down("SIGINT")))
Expand Down Expand Up @@ -130,6 +137,7 @@ async def on_member_join(self, member: discord.Member) -> None:
system_channel = member.guild.channels[0]
system_channel.send(
f"Bienvenue sur le serveur {member.metion}! Je suis SunBot, bot spécialiste de la météo (ou pas)! Tu peux utiliser +help dans le channel des bots pour en savoir plus sur moi!")
new_usr.save_usr_data()

@commands.Cog.listener()
async def on_message(self, message: discord.Message) -> None:
Expand All @@ -149,71 +157,29 @@ async def on_message(self, message: discord.Message) -> None:
logging.info("A message was received on server n°%d", msg_srv.id)
# Commands must be processed first
await self.bot.process_commands(message)
# Enable eastereggs only on "fun" servers:
if msg_srv.fun:
# Randomly add a reaction to the message:
await self.__add_reaction(message)
lowered_msg = message.content.lower()
if lowered_msg in ["tête de pomme", "tete de pomme", "#tetedepomme"]:
msg_srv.appleHead += 1
# If the message was repeted three consecutive times, send the gif:
if msg_srv.appleHead == 3:
msg_srv.appleHead = 0
logging.info(
"Invocation of apple head on server %s!", message.guild.name)
embed2send = discord.Embed(title="Et tu savais qu'à Jean Jaurès",
color=0xff0000)
apple_head_gif = discord.File(
f"{sunbot.GIF_REPERTORY_PATH}{sunbot.APPLE_HEAD_GIF_NAME}")
embed2send.set_image(
url=f"attachment://{sunbot.APPLE_HEAD_GIF_NAME}")
await message.channel.send(embed=embed2send, file=apple_head_gif)
# Other types of messages:
else:
msg_srv.appleHead = 0
# Easter eggs:
if "me foutre au sol" in lowered_msg and np.random.uniform() > 0.5:
await message.reply("Tu sais, il y a des gens qui disaient ça \
et qui ont fini ingénieurs chez Boeing. \
Donc tu as du potentiel \U0001f31e !")
elif lowered_msg == "sinus":
await message.channel.send("Tangente")
elif lowered_msg in ["patrick", "patou", "patoche", "pata", "patrikou"] and np.random.uniform() > 0.25:
pass # TODO add the list of gifs
elif "kernel is dead" in lowered_msg:
pass # TODO add corresponding list of gifs

@commands.Cog().listener()
async def on_shut_down(self, signame : str):
"""This method is called when the SIGINT signal is trigerred"""
logging.info("%s signal received", signame)
await self.__save_data()
logging.info("Data waas saved on %s", self.data_mount_pt)
logging.info("Data was saved on %s", self.data_mount_pt)
# stop running tasks:
logging.info("Stopping running tasks...")
current_task = asyncio.current_task()
tasks = asyncio.all_tasks()
tasks.remove(current_task)
# cancel all the tasks except current and main tasks:
for task in tasks:
# Task-1 is for main task (launched with asyncio.run())
if task.get_name() != 'Task-1':
task.cancel()
await self.bot.close()
logging.info("Bot was disconnected from Discord")

# ====================================================================================
# COMMANDS PART
# ====================================================================================

# TODO Replace this classic command by it slash counterpart:
@np.deprecate_with_doc
async def set_emoji(self, ctx: commands.Context, usr_id: int, emoji: str, emoji_freq: float):
"""Set an emoji for specified user that the bot will used to randomly
react to a message from this user
## Parameters:
- `ctx`: command call context
- `usr_id`: id of the user for which the emoji will be set
- `emoji`: emoji to set
- `emoji_freq`: probability that the bot reacts to an user message using
specified emoji
## Return value:
not applicable
"""
try:
self.usr_dict[usr_id].emoji = emoji
except KeyError:
pass

@app_commands.command(name="disconnect", description="[admin] Deconnecte le bot de discord")
@app_commands.describe(debug="1=mode debug on, 0=mode debut off")
@app_commands.guilds(discord.Object(id=726063782606143618))
Expand All @@ -234,7 +200,7 @@ async def disconnect(self, interaction : discord.Interaction, debug : Optional[i
return

logging.info("Bot is disconnecting...")
self.__save_data()
await self.__save_data()
await interaction.response.send_message("La sauvegarde des données est terminée, je me déconnecte. Bonne nuit!")
# To avoid to accidently disconnect remote bot durint a debug session:
if not self.test_mode and debug:
Expand Down Expand Up @@ -357,6 +323,7 @@ async def set_daily_weather_channel(self, interaction: discord.Interaction, loca
await self.daily_weather_handler.add_sub2location(interaction.channel,
location_name, location_tz)
await interaction.response.send_message(f"C'est compris, j'enverrai désormais quotidiennement la météo du jour pour {location_name} ici 😉")
await self.daily_weather_handler.save_locations_subscribers()

@app_commands.command(name="mp_daily_weather", description="Active ou désactive l'envoi quotidien de la météo du jour pour la localisation indiquée")
@app_commands.describe(location_name="Nom de la localité")
Expand All @@ -381,6 +348,7 @@ async def set_daily_weather_pm(self, interaction: discord.Interaction, location_
await interaction.response.send_message(content=f"C'est entendu, je ne vous enverrai plus la météo quotidienne pour {location_name}")
logging.info(
"User n°%d has disabled daily weather pm for %s", user_id, location_name)
await self.daily_weather_handler.save_locations_subscribers()
return
# User has not enable the pm for the specified location, so first check
# that this city is known by the API to avoid future errors
Expand All @@ -398,6 +366,7 @@ async def set_daily_weather_pm(self, interaction: discord.Interaction, location_
logging.info(
"User n°%d has subscribed to receive daily weather for the location %s", user_id, location_name)
await interaction.response.send_message(content=f"Super ! Je vous enverrez désormais la météo pour {location_name} chaque jour en message privé! (à 7h00 heure locale de la localisation)")
await self.daily_weather_handler.save_locations_subscribers()

@app_commands.command(name='global_info', description="Envoi un message sur tous les channels système connus par le bot")
@app_commands.describe(msg="message à envoyer")
Expand All @@ -407,9 +376,12 @@ async def global_info(self, interaction : discord.Interaction, msg : str) -> Non
## Parameters:
* `interaction`: discord interaction which contains context data
## Return value:
None"""
embed2send = discord.Embed(title="Informations concernant la SunRisVersion (V2)",
description=msg)
None
"""
embed2send = discord.Embed(
title="Informations concernant la SunRisVersion (V2)",
description=msg
)
for guild in self.bot.guilds:
guild_syst_channel = guild.system_channel
# check the existence of a system channel for the current guild:
Expand All @@ -434,29 +406,6 @@ async def __save_data(self):
srv.save_srv_data()
await self.daily_weather_handler.save_locations_subscribers()

async def __add_reaction(self, msg: discord.Message) -> None:
"""Private method to add a reaction to the specified message published
by an user, according to the user probability for this action
## Parameters:
* `msg` : discord message that triggered this method
## Return value:
not applicable
"""
# Add a reaction only if the user is not a bot:
if not msg.author.bot:
# Get the user that sent the message:
user: SunUser = self.usr_dict[msg.author.id]
# If an emoji is define for this user and probability is under freqEmoji proba:
if user.emoji != "" and np.random.uniform() <= user.freqEmoji:
try:
await msg.add_reaction(user.emoji)
except discord.errors.NotFound:
logging.error(
"Reaction cannot be added because the message was deleted or the emoji %s does not exist", user.emoji)
except TypeError:
logging.error(
"Emoji %s, set for the user n°%dis not in a valid emoji format", user.emoji, user.id)

# TODO Remove this unused private method:
@np.deprecate_with_doc
async def __delete_command_msg(self, ctx: commands.Context) -> None:
Expand Down
23 changes: 7 additions & 16 deletions sunbot/SunServer.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,20 @@

class SunServer:
"""This class defines a discord server (guild) where the bot is present. A
discord server is a set of sun users (discord users) and have webhooks that
discord server is a set of sun users (discord users) and has webhooks that
allow the bot to send message without a context. It is identified by an ID
that corresponds to its ID on Discord. There are two sorts of server : normal
and 'fun' server."""
that corresponds to its ID on Discord.
"""

srv_backup_path = ""

def __init__(self, id : int, fun : bool = False) -> None:
def __init__(self, id : int) -> None:
"""Constructor of this class
## Parameter :
* `id`: discord ID for the server to create. This ID is unique on Discord"""
object.__setattr__(self, "id", id)
object.__setattr__(self, "fun", fun)
object.__setattr__(self, "usersDict", {}) #In this dict users' ID are the keys and users the values
object.__setattr__(self, "webhooksDict", {}) #In this dict links to the webhook are the keys and state of this webhooks the values
object.__setattr__(self, "appleHead", 0)

#If backup directory for servers does not already exist, create it:
if SunServer.srv_backup_path == "":
Expand All @@ -46,7 +44,6 @@ def __init__(self, id : int, fun : bool = False) -> None:
try:
serverData = json.load(serverFile)
object.__setattr__(self, "webhooksDict", serverData["webhooksDict"])
object.__setattr__(self, "fun", serverData["fun"])
except json.decoder.JSONDecodeError:
logging.error(f"An error occured when retrieving data for server n°{id}")
#Else, create a new server with a new corresponding backup file:
Expand All @@ -57,18 +54,11 @@ def __init__(self, id : int, fun : bool = False) -> None:

def __setattr__(self, __name: str, __value) -> None:
"""Special method used to update object fields. Here, redefinition
prohibites all modifications excepted fun status and appleHead
prohibites all modifications.
## Parameters:
* `__name`: name of the field to update
* `__value`: new value for the field to update"""
if __name == "fun" :
object.__setattr__(self, __name, __value)
self.save_srv_data()
logging.info(f"Fun status for server {self.id} was updated. New value : {self.fun}")
if __name == "appleHead":
object.__setattr__(self, __name, __value)
else:
logging.error("Server attributes cannot directly modified")
logging.error("Server attributes cannot be directly modified")


def __eq__(self, __o: object) -> bool:
Expand Down Expand Up @@ -177,3 +167,4 @@ def save_srv_data(self):
jsonData = json.dumps(self.__dict__, ensure_ascii=False, indent=2)
serverFile.write(jsonData)
object.__setattr__(self, "usersDict", tmpUsersDict)
os.chmod(f"{SunServer.srv_backup_path}{self.id}.json", mode=0o777)
39 changes: 12 additions & 27 deletions sunbot/SunUser.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,35 +14,27 @@


class SunUser:
"""This class represents a SunBot user. Each user has an ID that allows to
identify it in discord API. This can be used to send it a message for example.
In this class, an user is also defined by its name.
"""This class represents a SunBot user. Each user has an ID that allows to
identify it in discord API. This can be used to send it a message for example.
"""

usr_backup_path = ""

def __init__(self, id : int, emoji : str = "", freqEmoji : float = 0.5, favLocation : str = "Toulouse", mp : bool = False) -> None:
"""Constructor for this class. A SunBot user is defined by its Discord ID.
Other informations can be provided in arguments, such as an emoji that is
used to react to a message sent by this user, if the user authorize private
message from the bot, or a specific favorite location for weather notifications.
Each instance of this class is associated to a backup file. The link
between the user and its backup file is done thanks to the user ID
def __init__(self, id : int, favLocation : str = "Toulouse", mp : bool = False) -> None:
"""Constructor for this class. A SunBot user is defined by its Discord ID.
Other informations can be provided in arguments, such as if the user authorize private
message from the bot, or a specific favorite location for weather notifications.
Each instance of this class is associated to a backup file. The link
between the user and its backup file is done thanks to the user ID
(so ID is defined as a constant and cannot be modified).
## Parameters:
* `id`: discord identifiant of the SunBot user to create
* `emoji`: optional, string code corresponding to the emoji that will be
used to react to messages sent by the user
* `freqEmoji`: optional, float corresponding to frequency at which an emoji
is added to a message from the user
* `favLocation`: optional, string indicating the favourite location name
* `favLocation`: optional, string indicating the favourite location name
for user to create. Default to Toulouse
* `mp`: optional, boolean indicating if the user allows private messages
* `mp`: optional, boolean indicating if the user allows private messages
from the SunBot. Default value is `False`
"""
object.__setattr__(self, "id", id)
object.__setattr__(self, "emoji", emoji)
object.__setattr__(self, "freqEmoji", freqEmoji)
object.__setattr__(self, "favLocation", favLocation)
object.__setattr__(self, "mp", mp)

Expand All @@ -61,8 +53,6 @@ def __init__(self, id : int, emoji : str = "", freqEmoji : float = 0.5, favLocat
with open(f"{self.usr_backup_path}{id}.json", "r", encoding="UTF-8") as userFile:
try:
userData = json.load(userFile)
object.__setattr__(self, "emoji", userData["emoji"])
object.__setattr__(self, "freqEmoji", userData["freqEmoji"])
object.__setattr__(self, "favLocation", userData["favLocation"])
object.__setattr__(self, "mp", userData["mp"])
except json.decoder.JSONDecodeError:
Expand All @@ -83,12 +73,6 @@ def __setattr__(self, __name: str, __value) -> None:
#User id cannot be modified:
if __name == "id":
logging.error("User identifiant cannot be modified. Abort")
return
#Value for emoji frequency must be in [0, 1] interval:
if __name == "freqEmoji":
if __value < 0 or __value > 1:
raise ValueError
object.__setattr__(self, __name, __value)
else:
object.__setattr__(self, __name, __value)
self.save_usr_data()
Expand All @@ -98,7 +82,7 @@ def __setattr__(self, __name: str, __value) -> None:

def __eq__(self, __o: object) -> bool:
"""Test if the specified object `o` is equal to this instance
## Parameter:
## Parameter:
* `__o`: object to compare to this instance
## Return value:
`True` if the specified object and this user are equal, `False` otherwise"""
Expand All @@ -115,3 +99,4 @@ def save_usr_data(self) -> None:
with open(f"{SunUser.usr_backup_path}{self.id}.json", "w", encoding="UTF-8") as userFile:
jsonData = json.dumps(self.__dict__, ensure_ascii=False, indent=2)
userFile.write(jsonData)
os.chmod(f"{SunUser.usr_backup_path}{self.id}.json", mode=0o777)
Loading

0 comments on commit 0b9f393

Please sign in to comment.