2 Commits

Author SHA1 Message Date
b4b6db683a Changed data storage to a distant database 2023-11-21 12:06:09 +01:00
23c4047bf7 Added productivity feature 2023-11-19 13:47:16 +01:00
8 changed files with 676 additions and 134 deletions

3
.gitignore vendored
View File

@@ -159,4 +159,5 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder. # option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/ #.idea/
data/ data/
.vscode/

155
main.py
View File

@@ -1,11 +1,15 @@
import os, discord, json, sys import os, discord, json, sys, random
import modules.setup.db as db
from argparse import ArgumentParser from argparse import ArgumentParser
from datetime import datetime from datetime import datetime
from asyncio import create_task
from dotenv import load_dotenv from dotenv import load_dotenv
from discord import option
from discord.ext import tasks, commands from discord.ext import tasks, commands
from modules.setup.bot import bot from modules.setup.bot import bot, reload_feature
from modules.setup.logger import LOGGER from modules.setup.logger import LOGGER
from modules.setup.data import DATA_DIR, get_guild_data from modules.setup.data import get_guild_data, create_guild_data, delete_guild_data, save_guild_data
from modules.cogs.productivity import TASKS as PRODUCTIVITY_TASKS, send_reminder
PARSER = ArgumentParser(description='BotMafieux for Discord.') PARSER = ArgumentParser(description='BotMafieux for Discord.')
PARSER.add_argument('--guild_id',type=int,help='The guild ID.') PARSER.add_argument('--guild_id',type=int,help='The guild ID.')
@@ -17,20 +21,36 @@ EXEC_ARGS = PARSER.parse_args()
async def birthday_anouncements_task(): async def birthday_anouncements_task():
LOGGER.debug('birthday_anouncements_task started.') LOGGER.debug('birthday_anouncements_task started.')
for guild in bot.guilds: for guild in bot.guilds:
guild_data = get_guild_data(guild.id) birthday_settings = db.select('birthday_settings',f'guild_id = {guild.id}')
if 'birthday_announcements_channel' in guild_data['features']['birthday'].keys(): if birthday_settings:
channel = guild.get_channel(guild_data['features']['birthday']['birthday_announcements_channel']) if birthday_settings['is_enabled']:
if 'birthdays' in guild_data.keys(): for user in guild.members:
for user_id in guild_data['features']['birthday']['birthdays'].keys(): user_productivity_data = db.select('guild_user_productivity',f'user_id = {user.id} AND guild_id = {guild.id}')
user = guild.get_member(int(user_id)) if user_productivity_data:
if user: if user_productivity_data['is_enabled']:
if guild_data['features']['birthday']['birthdays'][user_id]['announcements']: user = guild.get_member(user.id)
date = guild_data['features']['birthday']['birthdays'][user_id]['date'] if user:
today = datetime.today().strftime('%d/%m') birthday = user_productivity_data['birthday']
if date == today: today = datetime.today().strftime('%d/%m')
await channel.send(f'Joyeux anniversaire {user.mention}!') if birthday == today:
channel = guild.get_channel(user_productivity_data['channel_id'])
if channel:
message = user_productivity_data['birthday_message']
if message:
message = message.replace('{user}',user.mention)
await channel.send(message)
else:
LOGGER.debug(f'Channel {user_productivity_data["channel_id"]} not found.')
else:
LOGGER.debug(f'No birthday today for user {user.name} ({user.id}).')
else:
LOGGER.debug(f'User {user.name} ({user.id}) not found.')
else:
LOGGER.debug(f'User {user.name} ({user.id}) has birthday announcements disabled.')
else:
LOGGER.debug(f'No birthday data found for user {user.name} ({user.id}).')
else: else:
LOGGER.debug(f'No birthday_announcements_channel set for guild {guild.name} ({guild.id}).') LOGGER.debug(f'No productivity settings found for guild {guild.name} ({guild.id}).')
LOGGER.debug('birthday_anouncements_task ended.') LOGGER.debug('birthday_anouncements_task ended.')
@bot.slash_command(name='help',description='Displays the help message.') @bot.slash_command(name='help',description='Displays the help message.')
@@ -45,33 +65,57 @@ async def help(ctx:discord.ApplicationContext):
await ctx.send_response(embed=embed,ephemeral=True) await ctx.send_response(embed=embed,ephemeral=True)
@bot.slash_command(name='reload',description='Reloads the bot.') @bot.slash_command(name='reload',description='Reloads the bot.')
@option(name='feature',description='The feature to reload.',required=True,choices=['birthday','productivity'])
@commands.is_owner() @commands.is_owner()
async def reload(ctx:discord.ApplicationContext): async def reload(ctx:discord.ApplicationContext, feature:str):
"""Reloads the bot.""" """Reloads the bot."""
command_name = 'reload' command_name = 'reload'
guild = ctx.guild guild = ctx.guild
channel = ctx.channel channel = ctx.channel
LOGGER.debug(f'{ctx.author.name} used /{command_name}.') LOGGER.debug(f'{ctx.author.name} used /{command_name} {feature}.')
await ctx.send_response('Reloading...',ephemeral=True) try:
args = [f'--guild_id={guild.id}',f'--channel_id={channel.id}','--reload'] reload_feature(feature)
os.execl(sys.executable, sys.executable, __file__, *args) await ctx.send_response(f'{feature} reloaded!',ephemeral=True)
except Exception as e:
LOGGER.error(f'Error reloading feature {feature}: {e}')
await ctx.send_response(f'Error reloading feature {feature}: {e}',ephemeral=True)
@bot.event
async def on_member_join(member:discord.Member):
if not member.bot:
user_id = member.id
user_name = member.name
user_mention = member.mention
db_user = db.select('global_user',f'WHERE user_id = {user_id}')
if not db_user:
db.insert('global_user',user_id=user_id,user_name=user_name,user_mention=user_mention)
@bot.event
async def on_raw_member_remove(payload:discord.RawMemberRemoveEvent):
user = payload.user
if not user.bot:
user_id = user.id
guild_id = payload.guild_id
db.delete('guild_user_birthday',f'user_id = {user_id} AND guild_id = {guild_id}')
db.delete('guild_user_productivity',f'user_id = {user_id} AND guild_id = {guild_id}')
@bot.event @bot.event
async def on_guild_join(guild:discord.Guild): async def on_guild_join(guild:discord.Guild):
LOGGER.info(f'Bot joined guild {guild.name} ({guild.id}).') guild_id = guild.id
path = os.path.join(DATA_DIR, f'{guild.id}.json') guild_name = guild.name
if not os.path.exists(path): LOGGER.info(f'Bot joined guild {guild_name} ({guild_id}).')
LOGGER.info(f'Creating data file for guild {guild.id}') db.insert('guild',guild_id=guild_id)
with open(path, 'x') as f: db.insert('birthday_settings',guild_id=guild_id,is_enabled=False,channel_id=random.choice(guild.text_channels).id)
json.dump({}, f, indent=2) db.insert('productivity_settings',guild_id=guild_id,is_enabled=False)
@bot.event @bot.event
async def on_guild_remove(guild:discord.Guild): async def on_guild_remove(guild:discord.Guild):
LOGGER.info(f'Bot left guild {guild.name} ({guild.id}).') guild_id = guild.id
path = os.path.join(DATA_DIR, f'{guild.id}.json') guild_name = guild.name
if os.path.exists(path): LOGGER.info(f'Bot left guild {guild_name} ({guild_id}).')
LOGGER.info(f'Deleting data file for guild {guild.id}.') db.delete('birthday_settings',f'guild_id = {guild_id}')
os.remove(path) db.delete('productivity_settings',f'guild_id = {guild_id}')
db.delete('guild',f'guild_id = {guild_id}')
@bot.event @bot.event
async def on_ready(): async def on_ready():
@@ -81,18 +125,43 @@ async def on_ready():
guild = bot.get_guild(EXEC_ARGS.guild_id) guild = bot.get_guild(EXEC_ARGS.guild_id)
channel = guild.get_channel_or_thread(EXEC_ARGS.channel_id) channel = guild.get_channel_or_thread(EXEC_ARGS.channel_id)
await channel.send(f'{bot.user.name} Bot reloaded!',silent=True) await channel.send(f'{bot.user.name} Bot reloaded!',silent=True)
# Creates guild data files if they don't exist
for guild in bot.guilds:
guild_id = guild.id
path = os.path.join(DATA_DIR, f'{guild_id}.json')
if not os.path.exists(path):
LOGGER.info(f'Creating data file for guild {guild_id}')
with open(path, 'x') as f:
json.dump({'features': {}}, f, indent=2)
birthday_anouncements_task.start() birthday_anouncements_task.start()
for guild in bot.guilds:
for user in guild.members:
user_id = user.id
db_user = db.select('global_user',f'user_id = {user_id}')
if not db_user:
db.insert('global_user',user_id=user_id,user_name=user.name,user_mention=user.mention)
productivity_settings = db.select('productivity_settings',f'guild_id = {guild.id}')
if productivity_settings:
if productivity_settings['is_enabled']:
for user in guild.members:
user_productivity_data = db.select('guild_user_productivity',f'user_id = {user.id} AND guild_id = {guild.id}')
if user_productivity_data:
if user_productivity_data['is_enabled']:
next_reminder = user_productivity_data['next_reminder']
if next_reminder > datetime.now().timestamp():
cooldown = next_reminder - datetime.now().timestamp()
channel = guild.get_channel(user_productivity_data['channel_id'])
reminder_message = user_productivity_data['reminder_message']
PRODUCTIVITY_TASKS[str(user.id)] = create_task(send_reminder(cooldown,user,channel,reminder_message))
else:
user_productivity_data['next_reminder'] = datetime.now().timestamp() + user_productivity_data['cooldown']
db.update('guild_user_productivity',f'user_id = {user.id} AND guild_id = {guild.id}',next_reminder=user_productivity_data['next_reminder'])
channel = guild.get_channel(user_productivity_data['channel_id'])
reminder_message = user_productivity_data['reminder_message']
PRODUCTIVITY_TASKS[str(user.id)] = create_task(send_reminder(user_productivity_data['cooldown'],user,channel,reminder_message))
else:
LOGGER.debug(f'No productivity settings found for guild {guild.name} ({guild.id}).')
if __name__ == '__main__': if __name__ == '__main__':
load_dotenv() db.check_conn()
TOKEN = os.getenv('TOKEN') result = db.select('token','token_name = \'discord\'','token_value')
bot.run(TOKEN) if result:
TOKEN = result['token_value']
bot.run(TOKEN)
else:
LOGGER.error('No token found.')
sys.exit(1)

View File

@@ -1,8 +1,10 @@
import discord, re import discord, re
from discord import option from discord import option
from discord.ext import commands from discord.ext import commands
from datetime import datetime
from ..setup.logger import LOGGER from ..setup.logger import LOGGER
from ..setup.data import get_guild_data, save_guild_data from ..setup.data import get_guild_data, save_guild_data, is_feature_enabled
from ..setup import db
class BirthdayCog(commands.Cog): class BirthdayCog(commands.Cog):
def __init__(self, bot): def __init__(self, bot):
@@ -10,89 +12,106 @@ class BirthdayCog(commands.Cog):
@commands.slash_command(description='Sets birthday date. Must be in DAY/MONTH format.') @commands.slash_command(description='Sets birthday date. Must be in DAY/MONTH format.')
@option(name='date',description='Birthday date.',required=True) @option(name='date',description='Birthday date.',required=True)
@option(name='announcements',description='Enable or disable birthday announcements.',required=True,type=bool)
@option(name='message',description='The message to send when it is the user\'s birthday.',required=False)
@option(name='for_user',description='The user to set the birthday date for.',required=False,type=discord.Member) @option(name='for_user',description='The user to set the birthday date for.',required=False,type=discord.Member)
async def birthday_set(self,ctx:discord.ApplicationContext, date:str, for_user:discord.Member): async def birthday_set(self,ctx:discord.ApplicationContext, date:str,announcements:bool,message:str,for_user:discord.Member):
"""Sets birthday date. Must be in DAY/MONTH format.""" """Sets birthday date. Must be in DAY/MONTH format."""
command_name = 'birthday_set' command_name = 'birthday_set'
guild_id = ctx.guild.id guild_id = ctx.guild.id
guild_data = get_guild_data(guild_id)
author = ctx.author author = ctx.author
if re.match(r'\d{2}\/\d{2}',date): # Check if date is in DAY/MONTH format birthday_settings = db.select('birthday_settings',f'guild_id = {guild_id}')
if for_user: if birthday_settings:
if author.guild_permissions.administrator: # Check if author is an administrator in case they want to set the birthday for another user if birthday_settings['is_enabled']:
LOGGER.debug(f'{author.name} used /{command_name} {date} for {for_user.name}.') if for_user:
if 'birthday' not in guild_data['features'].keys(): if author.guild_permissions.administrator:
guild_data['features']['birthday'] = dict() if re.match(r'^\d{1,2}\/\d{1,2}$', date):
if 'birthdays' not in guild_data['features']['birthday'].keys(): user_data = db.select('guild_user_birthday',f'guild_id = {guild_id} AND user_id = {for_user.id}')
guild_data['features']['birthday']['birthdays'] = dict() if user_data:
if str(for_user.id) not in guild_data['features']['birthday']['birthdays'].keys(): # Update user data
guild_data['features']['birthday'][str(for_user.id)] = dict() if message:
guild_data['features']['birthday']['birthdays'][str(for_user.id)]['date'] = date LOGGER.debug(f'{author.name} used /{command_name} {date} {announcements} {message} for {for_user.name}.')
guild_data['features']['birthday']['birthdays'][str(for_user.id)]['announcements'] = True db.update('guild_user_birthday',f'guild_id = {guild_id} AND user_id = {for_user.id}',birthday=date,is_enabled=announcements,birthday_message=message)
else:
save_guild_data(guild_id, guild_data) LOGGER.debug(f'{author.name} used /{command_name} {date} {announcements} for {for_user.name}.')
await ctx.send_response(f'Birthday for {for_user.name} has been set to {date}.',ephemeral=True) db.update('guild_user_birthday',f'guild_id = {guild_id} AND user_id = {for_user.id}',birthday=date,is_enabled=announcements)
await ctx.send_response(f'{for_user.name}\'s birthday has been set to {date}.',ephemeral=True)
else:
# Create user data
if message:
LOGGER.debug(f'{author.name} used /{command_name} {date} {announcements} {message} for {for_user.name}.')
db.insert('guild_user_birthday',guild_id=guild_id,user_id=for_user.id,birthday=date,is_enabled=announcements,birthday_message=message)
else:
LOGGER.debug(f'{author.name} used /{command_name} {date} {announcements} for {for_user.name}.')
db.insert('guild_user_birthday',guild_id=guild_id,user_id=for_user.id,birthday=date,is_enabled=announcements)
await ctx.send_response(f'{for_user.name}\'s birthday has been set to {date}.',ephemeral=True)
else:
LOGGER.debug(f'{author.name} tried to use /{command_name} {date} {announcements} {message} for {for_user.name} but the date format is invalid.')
await ctx.send_response('The date format must be DAY/MONTH.',ephemeral=True)
else:
LOGGER.debug(f'{author.name} tried to use /{command_name} {date} {announcements} {message} for {for_user.name} but is not an administrator.')
await ctx.send_response('You must be an administrator to run the command with "for_user".',ephemeral=True)
else: else:
LOGGER.debug(f'{author.name} tried to use /{command_name} {date} for {for_user.name} but is not an administrator.') if re.match(r'^\d{1,2}\/\d{1,2}$', date):
await ctx.send_response('You must be an administrator to run the command with "for_user".',ephemeral=True) user_data = db.select('guild_user_birthday',f'guild_id = {guild_id} AND user_id = {author.id}')
if user_data:
# Update user data
if message:
LOGGER.debug(f'{author.name} used /{command_name} {date} {announcements} {message}.')
db.update('guild_user_birthday',f'guild_id = {guild_id} AND user_id = {author.id}',birthday=date,is_enabled=announcements,birthday_message=message)
else:
LOGGER.debug(f'{author.name} used /{command_name} {date} {announcements}.')
db.update('guild_user_birthday',f'guild_id = {guild_id} AND user_id = {author.id}',birthday=date,is_enabled=announcements)
await ctx.send_response(f'Your birthday has been set to {date}.',ephemeral=True)
else:
# Create user data
if message:
LOGGER.debug(f'{author.name} used /{command_name} {date} {announcements} {message}.')
db.insert('guild_user_birthday',guild_id=guild_id,user_id=author.id,birthday=date,is_enabled=announcements,birthday_message=message)
else:
LOGGER.debug(f'{author.name} used /{command_name} {date} {announcements}.')
db.insert('guild_user_birthday',guild_id=guild_id,user_id=author.id,birthday=date,is_enabled=announcements)
await ctx.send_response(f'Your birthday has been set to {date}.',ephemeral=True)
else:
LOGGER.debug(f'{author.name} tried to use /{command_name} {date} {announcements} {message} but the date format is invalid.')
await ctx.send_response('The date format must be DAY/MONTH.',ephemeral=True)
else: else:
# Set birthday for author LOGGER.debug(f'{author.name} tried to use /{command_name} {date} {announcements} {message} but the birthday feature is not enabled.')
LOGGER.debug(f'{author.name} used /{command_name} {date}.') await ctx.send_response('The birthday feature is not enabled.',ephemeral=True)
if 'birthday' not in guild_data['features'].keys():
guild_data['features']['birthday'] = dict()
if 'birthdays' not in guild_data['features']['birthday'].keys():
guild_data['features']['birthday']['birthdays'] = dict()
if author.id not in guild_data['features']['birthday']['birthdays'].keys():
guild_data['features']['birthday']['birthdays'][str(author.id)] = dict()
guild_data['features']['birthday']['birthdays'][str(author.id)]['date'] = date
guild_data['features']['birthday']['birthdays'][str(author.id)]['announcements'] = True
save_guild_data(guild_id, guild_data)
# The status defines the human readable status of the announcements to be displayed in the response.
announcements_status = 'will be' if guild_data['features']['birthday']['birthdays'][str(author.id)]['announcements'] else 'will not be'
await ctx.send_response(f'{author.mention} has set their birthday to {date} and {announcements_status} announced.',ephemeral=True)
else: else:
LOGGER.debug(f'{author.name} used /set_birthday {date} but the format is not correct.') LOGGER.debug(f'{author.name} tried to use /{command_name} {date} {announcements} {message} but the birthday feature is not enabled.')
await ctx.send_response('The date must is DAY/MONTH format.',ephemeral=True) await ctx.send_response('The birthday feature is not enabled.',ephemeral=True)
@commands.slash_command(description='Enable or disable birthday announcements.') @commands.slash_command(description='Configurate birthday announcements.')
@option(name='enable',description='Enable birthday announcements.',required=True)
@option(name='for_user',description='The user to enable or disable birthday announcements for.',required=False,type=discord.Member)
async def birthday_announcements(self,ctx:discord.ApplicationContext, enable:bool, for_user:discord.Member):
"""Enable or disable birthday announcements."""
command_name = 'birthday_announcements'
guild_id = ctx.guild.id
guild_data = get_guild_data(guild_id) or dict()
author = ctx.author
if for_user:
if author.guild_permissions.administrator: # Check if author is an administrator in case they want to set the birthday for another user
LOGGER.debug(f'{author.name} used /{command_name} {enable} for {for_user.name}.')
guild_data['features']['birthday']['birthdays'][str(for_user.id)]['announcements'] = enable
save_guild_data(guild_id, guild_data)
await ctx.send_response(f'Announcements for {for_user.name} are set to {enable}',ephemeral=True)
else:
LOGGER.debug(f'{author.name} tried to use /{command_name} {enable} for {for_user.name} but is not an administrator.')
await ctx.send_response('You must be an administrator to run the command with "for_user".',ephemeral=True)
else:
LOGGER.debug(f'{author.name} used /{command_name} {enable}.')
guild_data['features']['birthday']['birthdays'][str(author.id)]['announcements'] = enable
save_guild_data(guild_id, guild_data)
await ctx.send_response(f'Your birthday announcements have been set to {enable}',ephemeral=True)
@commands.slash_command(description='Sets the channel to send birthday announcements to.')
@option(name='channel',description='The channel to send birthday announcements to.',required=True,type=discord.TextChannel) @option(name='channel',description='The channel to send birthday announcements to.',required=True,type=discord.TextChannel)
async def birthday_announcements_channel(self,ctx:discord.ApplicationContext, channel:discord.TextChannel): @option(name='enable',description='Enable or disable birthday announcements.',required=True,type=bool)
"""Sets the channel to send birthday announcements to.""" async def birthday_config(self,ctx:discord.ApplicationContext, channel:discord.TextChannel,enable:bool):
command_name = 'birthday_announcements_channel' """Configurate birthday announcements."""
command_name = 'birthday_config'
guild_id = ctx.guild.id guild_id = ctx.guild.id
guild_data = get_guild_data(guild_id) or dict()
author = ctx.author author = ctx.author
if author.guild_permissions.administrator: birthday_settings = db.select('birthday_settings',f'guild_id = {guild_id}')
LOGGER.debug(f'{author.name} used /{command_name} {channel.name}.') if birthday_settings:
guild_data['features']['birthday']['birthday_announcements_channel'] = channel.id # Update guild birthday settings
save_guild_data(guild_id, guild_data) if birthday_settings['is_enabled']:
await ctx.send_response(f'Birthday announcements will be sent to {channel.mention}.',ephemeral=True) if author.guild_permissions.administrator:
LOGGER.debug(f'{author.name} used /{command_name} {channel.name} {enable}.')
db.update('birthday_settings',f'guild_id = {guild_id}',channel_id=channel.id,is_enabled=enable)
await ctx.send_response(f'Birthday announcements have been {"enabled" if enable else "disabled"} in {channel.mention}.',ephemeral=True)
else:
LOGGER.debug(f'{author.name} tried to use /{command_name} {channel.name} {enable} but is not an administrator.')
await ctx.send_response('You must be an administrator to run this command.',ephemeral=True)
else:
LOGGER.debug(f'{author.name} tried to use /{command_name} {channel.name} {enable} but the birthday feature is not enabled.')
await ctx.send_response('The birthday feature is not enabled.',ephemeral=True)
else: else:
LOGGER.debug(f'{author.name} tried to use /{command_name} {channel.mention} but is not an administrator.') # Create guild birthday settings
await ctx.send_response('You must be an administrator to run this command.',ephemeral=True) if author.guild_permissions.administrator:
LOGGER.debug(f'{author.name} used /{command_name} {channel.name} {enable}.')
db.insert('birthday_settings',guild_id=guild_id,channel_id=channel.id,is_enabled=enable)
await ctx.send_response(f'Birthday announcements have been {"enabled" if enable else "disabled"} in {channel.mention}.',ephemeral=True)
else:
LOGGER.debug(f'{author.name} tried to use /{command_name} {channel.name} {enable} but is not an administrator.')
await ctx.send_response('You must be an administrator to run this command.',ephemeral=True)

View File

@@ -0,0 +1,151 @@
import discord
from datetime import datetime
from discord.ext import commands, tasks
from discord import option
from typing import Union, Optional
from asyncio import sleep, create_task
from ..setup.logger import LOGGER
from ..setup import db
from ..setup.data import get_guild_data, save_guild_data, is_feature_enabled
TASKS = {}
async def send_reminder(cooldown:int,author:discord.Member,channel:discord.TextChannel,reminder_message:str):
while True:
await sleep(cooldown)
LOGGER.debug(f'Sending productivity reminder to {author.name} in {channel.name} of {channel.guild.name}.')
message = reminder_message.replace('{user}',author.mention)
await channel.send(message)
class ProductivityCog(commands.Cog):
def __init__(self,bot:discord.Bot) -> None:
self.bot = bot
@commands.slash_command(description='Configurate productivity reminder.')
@option(name='channel',description='The channel to send productivity reminders in.',required=True)
@option(name='enable',description='Enable or disable productivity reminders.',required=True)
@option(name='days',description='Number of days to wait before sending a reminder if the user has not been active in the channel.',required=True,type=int)
@option(name='custom_message',description='Custom message to send with the reminder.',required=False,type=str)
@option(name='for_user',description='The user to configurate productivity reminders for.',required=False,type=discord.Member)
async def productivity_set(self,ctx:discord.ApplicationContext, channel:Union[discord.TextChannel,discord.Thread], enable:bool, days:int, custom_message=None, for_user:discord.Member=None):
"""Configurate productivity reminder."""
command_name = 'productivity_set'
guild_id = ctx.guild.id
author = ctx.author
productivity_settings = db.select('productivity_settings',f'guild_id = {guild_id}')
if productivity_settings:
if productivity_settings['is_enabled']:
if for_user:
if author.guild_permissions.administrator:
user_data = db.select('guild_user_productivity',f'guild_id = {guild_id} AND user_id = {for_user.id}')
cooldown = days*60*60*24
next_reminder = int(datetime.now().timestamp() + cooldown)
if user_data:
# Update user data
if custom_message:
db.update('guild_user_productivity',f'guild_id = {guild_id} AND user_id = {for_user.id}',channel_id=channel.id,cooldown=cooldown,next_reminder=next_reminder,reminder_message=custom_message,is_enabled=enable)
LOGGER.debug(f'{author.name} used /{command_name} {channel.mention} {enable} {days} {custom_message} for {for_user.name}.')
await ctx.send_response(f'Productivity reminders for {for_user.name} have been set to {channel.mention}.',ephemeral=True)
else:
db.update('guild_user_productivity',f'guild_id = {guild_id} AND user_id = {for_user.id}',channel_id=channel.id,cooldown=cooldown,next_reminder=next_reminder,is_enabled=enable)
LOGGER.debug(f'{author.name} used /{command_name} {channel.mention} {enable} {days} for {for_user.name}.')
await ctx.send_response(f'Productivity reminders for {for_user.name} have been set to {channel.mention}.',ephemeral=True)
else:
# Create user data
if custom_message:
db.insert('guild_user_productivity',guild_id=guild_id,user_id=for_user.id,channel_id=channel.id,cooldown=cooldown,next_reminder=next_reminder,reminder_message=custom_message,is_enabled=enable)
LOGGER.debug(f'{author.name} used /{command_name} {channel.mention} {enable} {days} {custom_message} for {for_user.name}.')
await ctx.send_response(f'Productivity reminders for {for_user.name} have been set to {channel.mention}.',ephemeral=True)
else:
db.insert('guild_user_productivity',guild_id=guild_id,user_id=for_user.id,channel_id=channel.id,cooldown=cooldown,next_reminder=next_reminder,is_enabled=enable)
LOGGER.debug(f'{author.name} used /{command_name} {channel.mention} {enable} {days} for {for_user.name}.')
await ctx.send_response(f'Productivity reminders for {for_user.name} have been set to {channel.mention}.',ephemeral=True)
user_data = db.select('guild_user_productivity',f'guild_id = {guild_id} AND user_id = {for_user.id}')
if str(for_user.id) in TASKS.keys():
TASKS[str(for_user.id)].cancel()
TASKS[str(for_user.id)] = create_task(send_reminder(cooldown,for_user,channel,user_data['reminder_message']))
else:
LOGGER.debug(f'{author.name} tried to use /{command_name} {channel.mention} {enable} {days} {custom_message} for {for_user.name} but is not an administrator.')
await ctx.send_response('You must be an administrator to run the command with "for_user".',ephemeral=True)
else:
user_data = db.select('guild_user_productivity',f'guild_id = {guild_id} AND user_id = {author.id}')
cooldown = days*60*60*24
next_reminder = int(datetime.now().timestamp() + cooldown)
if user_data:
# Update user data
if custom_message:
db.update('guild_user_productivity',f'guild_id = {guild_id} AND user_id = {author.id}',channel_id=channel.id,cooldown=cooldown,next_reminder=next_reminder,reminder_message=custom_message,is_enabled=enable)
LOGGER.debug(f'{author.name} used /{command_name} {channel.mention} {enable} {days} {custom_message}.')
await ctx.send_response(f'Productivity reminders for {author.name} have been set to {channel.mention}.',ephemeral=True)
else:
db.update('guild_user_productivity',f'guild_id = {guild_id} AND user_id = {author.id}',channel_id=channel.id,cooldown=cooldown,next_reminder=next_reminder,is_enabled=enable)
LOGGER.debug(f'{author.name} used /{command_name} {channel.mention} {enable} {days}.')
await ctx.send_response(f'Productivity reminders for {author.name} have been set to {channel.mention}.',ephemeral=True)
else:
# Create user data
if custom_message:
db.insert('guild_user_productivity',guild_id=guild_id,user_id=author.id,channel_id=channel.id,cooldown=cooldown,next_reminder=next_reminder,reminder_message=custom_message,is_enabled=enable)
LOGGER.debug(f'{author.name} used /{command_name} {channel.mention} {enable} {days} {custom_message}.')
await ctx.send_response(f'Productivity reminders for {author.name} have been set to {channel.mention}.',ephemeral=True)
else:
db.insert('guild_user_productivity',guild_id=guild_id,user_id=author.id,channel_id=channel.id,cooldown=cooldown,next_reminder=next_reminder,is_enabled=enable)
LOGGER.debug(f'{author.name} used /{command_name} {channel.mention} {enable} {days}.')
await ctx.send_response(f'Productivity reminders for {author.name} have been set to {channel.mention}.',ephemeral=True)
user_data = db.select('guild_user_productivity',f'guild_id = {guild_id} AND user_id = {author.id}')
if str(author.id) in TASKS.keys():
TASKS[str(author.id)].cancel()
TASKS[str(author.id)] = create_task(send_reminder(cooldown,author,channel,user_data['reminder_message']))
else:
LOGGER.debug(f'{author.name} tried to use /{command_name} {channel.mention} {enable} {days} {custom_message} but the productivity feature is not enabled.')
await ctx.send_response('The productivity feature is not enabled.',ephemeral=True)
else:
LOGGER.debug(f'{author.name} tried to use /{command_name} {channel.mention} {enable} {days} {custom_message} but no productivity settings were found.')
await ctx.send_response('No productivity settings were found.',ephemeral=True)
@commands.slash_command(description='Enable or disable productivity reminders on the server.')
@option(name='enable',description='Enable or disable productivity reminders.',required=True)
async def productivity_config(self,ctx:discord.ApplicationContext, enable:bool):
"""Enable or disable productivity reminders on the server."""
command_name = 'productivity_config'
guild_id = ctx.guild.id
author = ctx.author
productivity_settings = db.select('productivity_settings',f'guild_id = {guild_id}')
if productivity_settings:
# Update guild productivity settings
if author.guild_permissions.administrator:
LOGGER.debug(f'{author.name} used /{command_name} {enable}.')
db.update('productivity_settings',f'guild_id = {guild_id}',is_enabled=enable)
await ctx.send_response(f'Productivity reminders have been {"enabled" if enable else "disabled"}.',ephemeral=True)
else:
LOGGER.debug(f'{author.name} tried to use /{command_name} {enable} but is not an administrator.')
await ctx.send_response('You must be an administrator to run this command.',ephemeral=True)
else:
# Create guild productivity settings
if author.guild_permissions.administrator:
LOGGER.debug(f'{author.name} used /{command_name} {enable}.')
db.insert('productivity_settings',guild_id=guild_id,is_enabled=enable)
await ctx.send_response(f'Productivity reminders have been {"enabled" if enable else "disabled"}.',ephemeral=True)
else:
LOGGER.debug(f'{author.name} tried to use /{command_name} {enable} but is not an administrator.')
await ctx.send_response('You must be an administrator to run this command.',ephemeral=True)
@commands.Cog.listener()
async def on_message(self,message:discord.Message):
author = message.author
if author.id != self.bot.user.id: # Ignore messages from the bot
guild_id = message.guild.id
productivity_settings = db.select('productivity_settings',f'guild_id = {guild_id}')
if productivity_settings:
if productivity_settings['is_enabled']:
user_productivity_data = db.select('guild_user_productivity',f'user_id = {author.id} AND guild_id = {guild_id}')
if user_productivity_data:
if user_productivity_data['is_enabled']:
next_reminder = int(datetime.now().timestamp() + user_productivity_data['cooldown'])
db.update('guild_user_productivity',f'user_id = {author.id} AND guild_id = {guild_id}',next_reminder=next_reminder)
if str(author.id) in TASKS.keys():
TASKS[str(author.id)].cancel()
TASKS[str(author.id)] = create_task(send_reminder(user_productivity_data['cooldown'],author,message.channel,user_productivity_data['reminder_message']))

View File

@@ -1,5 +1,7 @@
import discord import discord
from ..cogs.birthday import BirthdayCog from ..cogs.birthday import BirthdayCog
from ..cogs.productivity import ProductivityCog
from .logger import LOGGER
bot_intents = discord.Intents.default() bot_intents = discord.Intents.default()
bot_intents.message_content = True bot_intents.message_content = True
@@ -10,4 +12,15 @@ bot_intents.reactions = True
bot = discord.Bot(command_prefix='$', intents=bot_intents) bot = discord.Bot(command_prefix='$', intents=bot_intents)
bot.add_cog(BirthdayCog(bot)) COGS = [BirthdayCog, ProductivityCog]
for cog in COGS:
bot.add_cog(cog(bot))
def reload_feature(feature:str):
LOGGER.debug(bot.cogs)
LOGGER.debug(f'Reloading {feature}...')
bot.remove_cog(feature.capitalize()+'Cog')
cog = globals()[feature.capitalize()+'Cog']
bot.add_cog(cog(bot))
LOGGER.debug(f'{feature} reloaded!')

View File

@@ -1,15 +0,0 @@
import os, json
DATA_DIR = 'data/'
def get_guild_data(guild_id:int):
"""Reads and returns the dictionary of guild data from the guild's JSON data file."""
path = os.path.join(DATA_DIR, f'{guild_id}.json')
with open(path, 'r') as f:
return json.load(f)
def save_guild_data(guild_id:int, data):
"""Saves the dictionary of guild data to the guild's JSON data file."""
path = os.path.join(DATA_DIR, f'{guild_id}.json')
with open(path, 'w') as f:
json.dump(data, f, indent=2)

304
modules/setup/db.py Normal file
View File

@@ -0,0 +1,304 @@
import os, psycopg2
from dotenv import load_dotenv
from typing import Optional, Callable, Any, Union
from .logger import LOGGER
load_dotenv()
DB_NAME = os.getenv('DB_NAME')
DB_USER = os.getenv('DB_USER')
DB_PASSWORD = os.getenv('DB_PASSWORD')
DB_HOST = os.getenv('DB_HOST')
def check_conn():
"""Tests the database connection."""
connection = psycopg2.connect(
database=DB_NAME,
user=DB_USER,
password=DB_PASSWORD,
host=DB_HOST,
port="5432"
)
if connection.status:
LOGGER.debug('Established connection to database.')
LOGGER.debug(f'Connection status: {connection.status}')
else:
LOGGER.error('Failed to connect to database.')
connection.close()
LOGGER.debug('Closed connection to database.')
def insert(table:str,**kwargs):
"""
Inserts the given data into the given table.
Global_User
user_id: int
user_mention: str
user_mention: str
Guild
guild_id: int
Birthday_Settings
guild_id: int
is_enabled: bool
channel_id: int
Productivity_Settings
guild_id: int
is_enabled: bool
Guild_User_Birthday
user_id: int
guild_id: int
birthday: int
birthday_message: str | None
is_enabled: bool
Guild_User_Productivity
user_id: int
guild_id: int
is_enabled: bool
channel_id: int
cooldown: int
next_reminder: int
reminder_message: str | None
"""
connection = psycopg2.connect(
database=DB_NAME,
user=DB_USER,
password=DB_PASSWORD,
host=DB_HOST,
port="5432",
)
cursor = connection.cursor()
query = f"INSERT INTO {table} ("
params = []
for key,value in kwargs.items():
query += f"{key},"
params.append(value)
query = query.rstrip(",") # Remove trailing comma
query += ") VALUES ("
for _ in range(len(params)):
query += "%s,"
query = query.rstrip(",") # Remove trailing comma
query += ")"
query = cursor.mogrify(query, params)
cursor.execute(query)
connection.commit()
cursor.close()
connection.close()
LOGGER.debug(f'Inserted data into {table}.')
def update(table:str,where:str,**kwargs):
"""
Updates the given data in the given table.
Global_User
user_id: int
user_mention: str
user_mention: str
Guild
guild_id: int
Birthday_Settings
guild_id: int
is_enabled: bool
channel_id: int
Productivity_Settings
guild_id: int
is_enabled: bool
Guild_User_Birthday
user_id: int
guild_id: int
birthday: int
birthday_message: str | None
is_enabled: bool
Guild_User_Productivity
user_id: int
guild_id: int
is_enabled: bool
channel_id: int
cooldown: int
next_reminder: int
reminder_message: str | None
"""
connection = psycopg2.connect(
database=DB_NAME,
user=DB_USER,
password=DB_PASSWORD,
host=DB_HOST,
port="5432",
)
cursor = connection.cursor()
query = f"UPDATE {table} SET "
params = []
for key,value in kwargs.items():
query += f"{key} = %s,"
params.append(value)
query = query.rstrip(",") # Remove trailing comma
query += f" WHERE {where}"
query = cursor.mogrify(query, params)
cursor.execute(query)
connection.commit()
cursor.close()
connection.close()
LOGGER.debug(f'Updated data in {table}.')
def delete(table:str,where:str):
"""
Deletes the given data from the given table.
Global_User
user_id: int
user_mention: str
user_mention: str
Guild
guild_id: int
Birthday_Settings
guild_id: int
is_enabled: bool
channel_id: int
Productivity_Settings
guild_id: int
is_enabled: bool
Guild_User_Birthday
user_id: int
guild_id: int
birthday: int
birthday_message: str | None
is_enabled: bool
Guild_User_Productivity
user_id: int
guild_id: int
is_enabled: bool
channel_id: int
cooldown: int
next_reminder: int
reminder_message: str | None
"""
connection = psycopg2.connect(
database=DB_NAME,
user=DB_USER,
password=DB_PASSWORD,
host=DB_HOST,
port="5432",
)
cursor = connection.cursor()
query = f"DELETE FROM {table} WHERE {where}"
cursor.execute(query)
connection.commit()
cursor.close()
connection.close()
LOGGER.debug(f'Deleted data from {table}.')
def select(table:str,where:str,*columns:str) -> dict[str,Any] | None:
"""
Selects the given data from the given table, and returns it as a dictionary.
Global_User
user_id: int
user_mention: str
user_mention: str
Guild
guild_id: int
Birthday_Settings
guild_id: int
is_enabled: bool
channel_id: int
Productivity_Settings
guild_id: int
is_enabled: bool
Guild_User_Birthday
user_id: int
guild_id: int
birthday: int
birthday_message: str | None
is_enabled: bool
Guild_User_Productivity
user_id: int
guild_id: int
is_enabled: bool
channel_id: int
cooldown: int
next_reminder: int
reminder_message: str | None
"""
connection = psycopg2.connect(
database=DB_NAME,
user=DB_USER,
password=DB_PASSWORD,
host=DB_HOST,
port="5432"
)
connection.readonly = True
cursor = connection.cursor()
query = f"SELECT "
if columns:
for column in columns:
query += f"{column},"
query = query.rstrip(",")
else:
query += "*"
query += f" FROM {table} WHERE {where}"
cursor.execute(query)
record = cursor.fetchone()
result = None
if record is None:
LOGGER.debug(f'No data found in {table} for {where}.')
else:
result = {}
if len(columns) > 0:
for i in range(len(columns)):
result[columns[i]] = record[i]
else:
if table.lower() == 'global_user':
result['user_id'] = record[0]
result['user_name'] = record[1]
result['user_mention'] = record[2]
elif table.lower() == 'guild':
result['guild_id'] = record[0]
elif table.lower() == 'birthday_settings':
result['guild_id'] = record[0]
result['is_enabled'] = record[1]
result['channel_id'] = record[2]
elif table.lower() == 'productivity_settings':
result['guild_id'] = record[0]
result['is_enabled'] = record[1]
elif table.lower() == 'guild_user_birthday':
result['user_id'] = record[0]
result['guild_id'] = record[1]
result['birthday'] = record[2]
result['is_enabled'] = record[3]
elif table.lower() == 'guild_user_productivity':
result['user_id'] = record[0]
result['guild_id'] = record[1]
result['is_enabled'] = record[2]
result['channel_id'] = record[3]
result['cooldown'] = record[4]
result['next_reminder'] = record[5]
result['reminder_message'] = record[6]
else:
LOGGER.error(f'Unknown table {table}.')
cursor.close()
connection.close()
return result

View File

@@ -3,6 +3,6 @@ import logging
logging.basicConfig(level=logging.INFO, format='[%(levelname)s] %(asctime)s: %(name)s: %(message)s') logging.basicConfig(level=logging.INFO, format='[%(levelname)s] %(asctime)s: %(name)s: %(message)s')
LOGGER = logging.getLogger('botmafieux') LOGGER = logging.getLogger('botmafieux')
LOGGER.setLevel(logging.DEBUG) LOGGER.setLevel(logging.DEBUG)
handler = logging.FileHandler(filename='botmafieux.log', encoding='utf-8', mode='w') handler = logging.FileHandler(filename='botmafieux.log', encoding='utf-8', mode='a')
handler.setFormatter(logging.Formatter('%(asctime)s:%(levelname)s:%(name)s: %(message)s')) handler.setFormatter(logging.Formatter('%(asctime)s:%(levelname)s:%(name)s: %(message)s'))
LOGGER.addHandler(handler) LOGGER.addHandler(handler)