diff --git a/.gitignore b/.gitignore index 6b9f5ee..046ee76 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,9 @@ jeevesbot/env.py -jeevesbot/secret.json +jeevesbot/databases/* +jeevesbot/__pycache__/* logs/ __init__.py *.pyc __pycache__/* cogs/__pycache__/* -jeevesbot/__pycache__/* -.vscode/ \ No newline at end of file +.vscode/ diff --git a/cogs/admin.py b/cogs/admin.py index 1f5ea92..aaade32 100644 --- a/cogs/admin.py +++ b/cogs/admin.py @@ -1,4 +1,3 @@ -import discord from discord.ext import commands from logging import getLogger import typing diff --git a/cogs/preview.py b/cogs/preview.py index 3d58fdc..3700792 100644 --- a/cogs/preview.py +++ b/cogs/preview.py @@ -1,69 +1,74 @@ import discord from discord.ext import commands -from jeevesbot import functions +from jeevesbot import functions, env from logging import getLogger import re import pytube -from pytube.exceptions import RegexMatchError +from pytube.exceptions import RegexMatchError, VideoUnavailable, ExtractError # setup logging log = getLogger(__name__) -e = discord.Embed() - - class Preview(commands.Cog): """ Ensures that high-risk channels don't display embedded links, but only gifs and youtube previews.""" + def __init__(self, bot): self.bot = bot self.video_id_regex = re.compile(r'(?:youtube\.com/(?:[^/]+/.+/|(?:v|e(?:mbed)?)/|.*[?&]v=)|youtu\.be/|m\.youtube\.com/(?:watch\?v=|embed/|v/))([^"&?/ ]{11})') - + self.e = discord.Embed() @commands.Cog.listener() async def on_message(self, message): + if message.author.bot: + return # Ignore messages from bots + + if message.content.startswith('https://tenor.com/'): - roles = functions.checkrole(message.author.roles) - channel = functions.checkchannel(message.channel.id) + is_admin = message.author.guild_permissions.administrator + is_high_risk_channel = self.check_channel(message.channel.id) embed_url = message.content follow_url = embed_url + '.gif' full_url = await functions.resolve(follow_url) gif_url = full_url.split('?')[0] - embed = e.set_image(url=gif_url) - if channel is True: - if roles is not True: - await message.channel.send(embed=embed) - logline = (str(message.author) + ' requested a gif: ' + str(gif_url)) - log.info(logline) - if message.content.endswith('.gif'): - roles = functions.checkrole(message.author.roles) - channel = functions.checkchannel(message.channel.id) - embed_url = message.content - embed = e.set_image(url=embed_url) - if channel is True: - if roles is not True: - await message.channel.send(embed=embed) - logline = (str(message.author) + ' requested a gif: ' + str(embed_url)) - log.info(logline) + embed = self.e.set_image(url=gif_url) + if is_high_risk_channel and not is_admin: + await message.channel.send(embed=embed) + logline = (str(message.author) + ' requested a gif: ' + str(gif_url)) + log.info(logline) + + if message.content.startswith('https://giphy.com/'): - roles = functions.checkrole(message.author.roles) - channel = functions.checkchannel(message.channel.id) + is_admin = message.author.guild_permissions.administrator + is_high_risk_channel = self.check_channel(message.channel.id) embed_url = message.content image_code = embed_url.split('-')[-1] gif_url = 'https://media.giphy.com/media/' + image_code + '/giphy.gif' - embed = e.set_image(url=gif_url) - if channel is True: - if roles is not True: - await message.channel.send(embed=embed) - logline = (str(message.author) + ' requested a gif: ' + str(gif_url)) - log.info(logline) - if 'https://youtu' or 'https://m.youtu' or 'https://www.youtu' in message.content(): - roles = functions.checkrole(message.author.roles) - channel = functions.checkchannel(message.channel.id) - if channel is True: - if roles is not True: - url = message.content + embed = self.e.set_image(url=gif_url) + if is_high_risk_channel and not is_admin: + await message.channel.send(embed=embed) + logline = (str(message.author) + ' requested a gif: ' + str(gif_url)) + log.info(logline) + + + if message.content.endswith('.gif'): + is_admin = message.author.guild_permissions.administrator + is_high_risk_channel = self.check_channel(message.channel.id) + embed_url = message.content + embed = self.e.set_image(url=embed_url) + if is_high_risk_channel and not is_admin: + await message.channel.send(embed=embed) + logline = (str(message.author) + ' requested a gif: ' + str(embed_url)) + log.info(logline) + + + if 'https://youtu' in message.content or 'https://m.youtu' in message.content or 'https://www.youtu' in message.content: + is_admin = message.author.guild_permissions.administrator + is_high_risk_channel = self.check_channel(message.channel.id) + if is_high_risk_channel and not is_admin: + url = message.content + try: youtube = pytube.YouTube(url) video_title = youtube.title video_author = youtube.author @@ -71,12 +76,19 @@ class Preview(commands.Cog): if video_id: embed = discord.Embed() embed.set_image(url=f"https://img.youtube.com/vi/{video_id}/hqdefault.jpg") - embed.set_author(name=f"{video_author}") - embed.add_field(name=f"", value=f"[{video_title}]({url})") + embed.set_author(name=video_author) + embed.add_field(name="", value=f"[{video_title}]({url})") await message.channel.send(embed=embed) log.info(f'User {message.author} requested preview for {url}') + except (VideoUnavailable, ExtractError, KeyError) as e: + log.error(f'Error extracting YouTube video details for {url}: {e}') + await message.channel.send('Sorry, there was an error retrieving the YouTube video details.') + def check_channel(self, channel_id): + high_risk_channels = env.PREVIEWCHANNELS + return channel_id in high_risk_channels + def extract_video_id(self, url): try: match = self.video_id_regex.search(url) diff --git a/cogs/reminders.py b/cogs/reminders.py new file mode 100644 index 0000000..5349816 --- /dev/null +++ b/cogs/reminders.py @@ -0,0 +1,41 @@ +import discord +from discord.ext import commands +from jeevesbot import env +import datetime +from jeevesbot.database import add_reminder +from logging import getLogger + + +# setup logging +log = getLogger(__name__) + + +class Reminders(commands.Cog): + """ Reminder command""" + def __init__(self, bot): + self.bot = bot + + + @discord.app_commands.command(name='remindme', description='Set a reminder - Use YY-MM-DD HH:MM:SS notation') + @discord.app_commands.guilds(discord.Object(id=env.GUILD_ID)) + async def remindme(self, interaction: discord.Interaction, time: str, message: str): + try: + reminder_time = datetime.datetime.strptime(time, '%Y-%m-%d %H:%M:%S') + add_reminder(interaction.user.id, message, reminder_time.isoformat()) + await interaction.response.send_message(f'Reminder set for {reminder_time}') + log.info(f'Reminder set by {interaction.user} for {reminder_time}: {message}') + except ValueError: + await interaction.response.send_message('Invalid time format. Use YYYY-MM-DD HH:MM:SS', ephemeral=True) + log.warn(f'Reminder set by {interaction.user} went wrong.') + + + @commands.Cog.listener() + async def on_ready(self): + log.info(f'module active') + +async def setup(bot): + await bot.add_cog(Reminders(bot)) + log.info(f'Added Reminders.remindme as command') + + + diff --git a/jeeves.py b/jeeves.py index f384c3f..3cc7078 100755 --- a/jeeves.py +++ b/jeeves.py @@ -1,66 +1,108 @@ #!/usr/bin/env python3.8 import discord -from discord.ext import commands +from discord.ext import commands, tasks from jeevesbot import env +from jeevesbot.database import init_db, get_due_reminders import os import log import logging.config from logging import getLogger +import datetime import asyncio -# setup root logger handlers -logging.config.dictConfig(log.LOGGING) +# Initialize the database +init_db() + + # setup logging +logging.config.dictConfig(log.LOGGING) log = getLogger(__name__) # setup discord.py bot intents = discord.Intents().all() -bot = commands.Bot(command_prefix='!', intents=intents, help_command=None) +intents.message_content = True e = discord.Embed() - -@bot.command(name='load', hidden=True) -@commands.has_permissions(administrator=True) -async def load(ctx, extension): - bot.load_extension(f'cogs.{extension}') - log.info(f'{ctx.message.author} loaded the {extension} module') +class Jeeves(commands.Bot): + def __init__(self): + super().__init__(command_prefix='!', intents=intents, help_command=None) + self.guild_ids = [env.GUILD_ID] -@bot.command(name='unload', hidden=True) -@commands.has_permissions(administrator=True) -async def unload(ctx, extension): - bot.unload_extension(f'cogs.{extension}') - log.info(f'{ctx.message.author} unloaded the {extension} module') + @commands.command(name='load', hidden=True) + @commands.has_permissions(administrator=True) + async def load(self, ctx, extension): + self.load_extension(f'cogs.{extension}') + log.info(f'{ctx.message.author} loaded the {extension} module') -@bot.command(name='reload', hidden=True) -@commands.has_permissions(administrator=True) -async def reload(ctx, extension): - bot.unload_extension(f'cogs.{extension}') - bot.load_extension(f'cogs.{extension}') - log.info(f'{ctx.message.author} reloaded the {extension} module') + @commands.command(name='unload', hidden=True) + @commands.has_permissions(administrator=True) + async def unload(self, ctx, extension): + self.unload_extension(f'cogs.{extension}') + log.info(f'{ctx.message.author} unloaded the {extension} module') -async def load_extensions(): - for filename in os.listdir('./cogs'): - if filename.endswith('.py'): - await bot.load_extension(f'cogs.{filename[:-3]}') + @commands.command(name='reload', hidden=True) + @commands.has_permissions(administrator=True) + async def reload(self, ctx, extension): + self.unload_extension(f'cogs.{extension}') + self.load_extension(f'cogs.{extension}') + log.info(f'{ctx.message.author} reloaded the {extension} module') -@bot.event -async def on_ready(): - log.info(f'Active with ID:{bot.user.id} as {bot.user.name}') - activity = discord.Activity(name='!help', type=discord.ActivityType.listening) - await bot.change_presence(activity=activity) + async def load_extensions(self): + for filename in os.listdir('./cogs'): + if filename.endswith('.py'): + await self.load_extension(f'cogs.{filename[:-3]}') + + + @tasks.loop(seconds=60) + async def check_reminders(self): + now = datetime.datetime.now().isoformat() + reminders = get_due_reminders(now) + for reminder in reminders: + user = self.get_user(reminder[1]) + if user: + try: + await user.send(reminder[2]) + except Exception as e: + log.error(f'Error sending reminder to user {reminder[1]}: {e}') + + + async def on_ready(self): + log.info(f'Active with ID:{self.user.id} as {self.user.name}') + activity = discord.Activity(name='!help', type=discord.ActivityType.listening) + await self.change_presence(activity=activity) + # Sync commands for all guilds + for guild_id in self.guild_ids: + guild = discord.Object(id=guild_id) + try: + await self.tree.sync(guild=guild) + log.info(f'Successfully synced commands for guild {guild_id}') + except discord.errors.Forbidden as e: + log.error(f'Failed to sync commands for guild {guild_id}: {e}') + # Start the reminder check loop + if not self.check_reminders.is_running(): + self.check_reminders.start() + + + async def on_command_error(self, ctx, error): + if isinstance(error, commands.CommandNotFound): + await ctx.send('Command not found.') + log.warning(f'Command not found: {ctx.message.content}') + else: + await ctx.send('An error occurred.') + log.error(f'An error occurred: {error}') async def main(): - async with bot: - await load_extensions() - await bot.start(env.TOKEN) + bot = Jeeves() + await bot.load_extensions() + await bot.start(env.TOKEN) - -asyncio.run(main()) +if __name__ == "__main__": + asyncio.run(main()) diff --git a/jeevesbot/database.py b/jeevesbot/database.py new file mode 100644 index 0000000..aca158d --- /dev/null +++ b/jeevesbot/database.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 + +import sqlite3 + +def init_db(): + conn = sqlite3.connect(r"jeevesbot/databases/reminders.db") + c = conn.cursor() + c.execute(''' + CREATE TABLE IF NOT EXISTS reminders ( + id INTEGER PRIMARY KEY, + user_id INTEGER, + message TEXT, + reminder_time TIMESTAMP + ) + ''') + conn.commit() + conn.close() + +def add_reminder(user_id, message, reminder_time): + conn = sqlite3.connect(r"jeevesbot/databases/reminders.db") + c = conn.cursor() + c.execute('INSERT INTO reminders (user_id, message, reminder_time) VALUES (?, ?, ?)', (user_id, message, reminder_time)) + conn.commit() + conn.close() + +def get_due_reminders(current_time): + conn = sqlite3.connect(r"jeevesbot/databases/reminders.db") + c = conn.cursor() + c.execute('SELECT id, user_id, message FROM reminders WHERE reminder_time <= ?', (current_time,)) + reminders = c.fetchall() + c.execute('DELETE FROM reminders WHERE reminder_time <= ?', (current_time,)) + conn.commit() + conn.close() + return reminders \ No newline at end of file diff --git a/jeevesbot/env.py.dist b/jeevesbot/env.py.dist index 68a3025..698827f 100644 --- a/jeevesbot/env.py.dist +++ b/jeevesbot/env.py.dist @@ -2,4 +2,5 @@ TOKEN = 'discord-bot-token-here' ADMIN_ROLE = 'role-to-exclude-from-gifbot' -PREVIEWCHANNELS = [add-channel-ids-for-bot-to-work-in] +GUILD_ID = 'id-of-guild' +PREVIEWCHANNELS = ['add-channel-ids-for-bot-to-work-in'] \ No newline at end of file diff --git a/jeevesbot/functions.py b/jeevesbot/functions.py index 9e0a838..1298974 100644 --- a/jeevesbot/functions.py +++ b/jeevesbot/functions.py @@ -17,16 +17,3 @@ def roll(notation): result = int(roll) return roll,result -# check if user has admin role and output True if it's the case. -def checkrole(roles): - for role in roles: - if str(role) == env.ADMIN_ROLE: - return True - -# check if the source channel is in the list of channels that are watched by the bot. -def checkchannel(channelid): - if channelid in env.PREVIEWCHANNELS: - return True - else: - return False - diff --git a/requirements.txt b/requirements.txt index d2af5a3..c8d793d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,13 +2,14 @@ aiohttp async-timeout dice -discord.py +discord.py[voice] docopt multidict pyparsing typing-extensions pylint pytube +sqlite3 # needs this version, otherwise TypeErrors will break stuff yarl==1.4.2