diff --git a/.gitignore b/.gitignore index 62bf56b..0b40e9f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -.env -env -__pycache__ +.env +env +__pycache__ song.mp3 \ No newline at end of file diff --git a/README.md b/README.md index d5ea99b..11b1a75 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,68 @@ -# Duckster -A fun and all-purpose bot made in Python. - -Founded by [@iakrules](https://github.com/iakrules) and [@rzmk](https://github.com/rzmk). \ No newline at end of file +

+ +

+ +
+ + + + + + + + +
+ +--- + +# Rutgers Esports Discord Bot + +The official **Rutgers Esports Bot** on Discord for handling all internal operations. + +Built as a modular multi-purpose utility bot, with custom features such as creating embeds, moderation commands, and more. + +## Table of Contents + +- [About The Project](#about-the-project) +- [Tools & Technologies](#tools-and-technologies) +- [Contributing](#contributing) +- [Contributors](#contributors) + +## About The Project + +[![Bot Example](./images/Bot-Example.png)](https://discord.gg/RutgersEsports) + +Rutgers Esports Bot is developed for ease of use with utility commands and features for gaming communities, clubs, and organizations. + +- **Community** - Members can easily access informative commands about their user/server, and also details about Rutgers Esports such as upcoming events. +- **Moderation** - Built-in moderation commands are automatically provided and limited to server members with specific permissions. +- **Executive Board** - Restricted features providing convenient automation and information to serve board members spanning all departments. + +## Tools and Technologies + +- [Discord.py](https://discordpy.readthedocs.io/en/stable/) - Modern async/await API wrapper for Discord. +- [Heroku](https://www.heroku.com/) - Hosting and deployment cloud platform. +- [Firestore DB](https://firebase.google.com/) - Realtime NoSQL database for managing our inventory. +- [Google Calendar API](https://developers.google.com/calendar) - For instant reports of upcoming calendar events. + +## Contributing + +There's plenty of ways to contribute! +Here's a few suggestions to help: + +1. Star the project. +2. Find and report [issues](https://github.com/rutgersesports/discord-bot/issues). +3. Submit [PRs](https://github.com/rutgersesports/discord-bot/pulls) to help solve issues or add features. +4. Send feature requests [our Discord](https://discord.gg/RutgersEsports)! + +We'll be creating a contributing guide/wiki in the near future. + +For now, Rutgers Esports Technology Department officers will be maintaining the bot. [Click here to apply.](https://bit.ly/join-reebo) + +## Contributors + +This project exists thanks to all the people who contribute! + + + + diff --git a/bot.py b/bot.py index c3587cc..ba4569d 100644 --- a/bot.py +++ b/bot.py @@ -1,47 +1,35 @@ -# Import all required packages and variables -import os -import discord -from discord.ext import commands -from dotenv import load_dotenv - -load_dotenv() - -TOKEN = os.environ.get("TOKEN") -client = commands.Bot(command_prefix='d!') - -# Bot initialized -@client.event -async def on_ready(): - print(f'{client.user.name} is ready.') - await client.change_presence(activity=discord.Streaming(name="duck pictures.", url="https://www.youtube.com/watch?v=dQw4w9WgXcQ")) - -@commands.is_owner() -@client.command() -async def load(ctx, extension): - """Loads a cog""" - client.load_extension(f'cogs.{extension}') - -@commands.is_owner() -@client.command() -async def unload(ctx, extension): - """Unloads a cog""" - client.unload_extension(f'cogs.{extension}') - -# Cogs -for filename in os.listdir('./cogs/info'): - if filename.endswith('.py'): - client.load_extension(f'cogs.info.{filename[:-3]}') - -for filename in os.listdir('./cogs/moderation'): - if filename.endswith('.py'): - client.load_extension(f'cogs.moderation.{filename[:-3]}') - -for filename in os.listdir('./cogs/music'): - if filename.endswith('.py'): - client.load_extension(f'cogs.music.{filename[:-3]}') - -for filename in os.listdir('./cogs/inventory'): - if filename.endswith('.py'): - client.load_extension(f'cogs.inventory.{filename[:-3]}') - -client.run(TOKEN) \ No newline at end of file +# Import all required packages and variables +import os +import discord +from discord.ext import commands +from dotenv import load_dotenv + +load_dotenv() + +intents = discord.Intents().all() + +TOKEN = os.environ.get("TOKEN") +bot = commands.Bot(command_prefix='.', intents=intents) + +# Bot initialized +@bot.event +async def on_ready(): + print(f'{bot.user.name} is ready.') + await bot.change_presence(activity=discord.Streaming(name="Use .help to learn more!", url="https://linktr.ee/RutgersEsports")) + +class CustomHelpCommand(commands.MinimalHelpCommand): + async def send_pages(self): + destination = self.get_destination() + for page in self.paginator.pages: + embed = discord.Embed(description=page, color=0xC94949) + await destination.send(embed=embed) + +bot.help_command = CustomHelpCommand() + +# Cogs +for group in os.listdir('./cogs'): + for cog in os.listdir(f'./cogs/{group}'): + if cog.endswith('.py'): + bot.load_extension(f'cogs.{group}.{cog[:-3]}') + +bot.run(TOKEN) \ No newline at end of file diff --git a/cogs/music/musicCog.py b/cogs/audio/audioCog.py similarity index 90% rename from cogs/music/musicCog.py rename to cogs/audio/audioCog.py index 8be0488..848c250 100644 --- a/cogs/music/musicCog.py +++ b/cogs/audio/audioCog.py @@ -1,162 +1,161 @@ -import asyncio -import discord -from discord.ext import commands -import youtube_dl -import os - -ffmpeg_options = { - 'options': '-vn' -} - -ytdl_format_options = { - 'format': 'bestaudio/best', - 'outtmpl': '%(extractor)s-%(id)s-%(title)s.%(ext)s', - 'restrictfilenames': True, - 'noplaylist': True, - 'nocheckcertificate': True, - 'ignoreerrors': False, - 'logtostderr': False, - 'quiet': True, - 'no_warnings': True, - 'default_search': 'auto', - 'source_address': '0.0.0.0' # bind to ipv4 since ipv6 addresses cause issues sometimes -} - -ytdl = youtube_dl.YoutubeDL(ytdl_format_options) - -class YTDLSource(discord.PCMVolumeTransformer): - def __init__(self, source, *, data, volume=0.5): - super().__init__(source, volume) - - self.data = data - - self.title = data.get('title') - self.url = data.get('url') - - @classmethod - async def from_url(cls, url, *, loop=None, stream=False): - loop = loop or asyncio.get_event_loop() - data = await loop.run_in_executor(None, lambda: ytdl.extract_info(url, download=not stream)) - - if 'entries' in data: - # take first item from a playlist - data = data['entries'][0] - - filename = data['url'] if stream else ytdl.prepare_filename(data) - return cls(discord.FFmpegPCMAudio(filename, **ffmpeg_options), data=data) - -class MusicCog(commands.Cog): - - def __init__(self, client): - self.client = client - - # Commands - - @commands.command() - async def play(self, ctx, url): - """Streams from a url""" - - if ctx.author.voice is None: - return await ctx.send("You're not connected to a voice channel.") - - try: - channel = ctx.author.voice.channel - await channel.connect() - except: - pass - async with ctx.typing(): - player = await YTDLSource.from_url(url, loop=self.client.loop, stream=True) - ctx.voice_client.play(player, after=lambda e: print(f'Player error: {e}') if e else None) - await ctx.send(f'Now playing: {player.title}') - - @play.error - async def play_error(self, ctx, error): - if isinstance(error, commands.MissingRequiredArgument): - voice = ctx.voice_client - if voice != None and voice.is_paused(): - voice.resume() - else: - await ctx.send("Missing URL!") - - @commands.command(aliases=['disconnect, dc']) - async def leave(self, ctx): - """Disconnects bot from the voice channel""" - - if ctx.author.voice is None: - return await ctx.send("You're not connected to a voice channel.") - - voice = ctx.voice_client - if voice is not None: - if voice.is_connected(): - await voice.disconnect() - else: - await ctx.send("The bot is not connected to a voice channel.") - - @commands.command() - async def volume(self, ctx, volume: int): - """Changes the player's volume""" - - if ctx.author.voice is None: - return await ctx.send("You're not connected to a voice channel.") - - if ctx.voice_client is None: - return await ctx.send("Not connected to a voice channel.") - - ctx.voice_client.source.volume = volume / 100 - await ctx.send(f"Changed volume to {volume}%") - - @commands.command() - async def pause(self, ctx): - """Pauses audio""" - - if ctx.author.voice is None: - return await ctx.send("You're not connected to a voice channel.") - - voice = ctx.voice_client - if voice.is_playing(): - voice.pause() - else: - await ctx.send("Currently no audio is playing.") - - @commands.command() - async def resume(self, ctx): - """Resumes currently paused audio""" - - if ctx.author.voice is None: - return await ctx.send("You're not connected to a voice channel.") - - voice = ctx.voice_client - if voice.is_paused(): - voice.resume() - else: - await ctx.send("The audio is not paused.") - - @commands.command() - async def stop(self, ctx): - """Stops and disconnects the bot from voice""" - - if ctx.author.voice is None: - return await ctx.send("You're not connected to a voice channel.") - - voice = ctx.voice_client - if voice: - voice.stop() - - @commands.command(aliases=['join']) - async def connect(self, ctx): - """Connects bot to currently connected voice channel""" - - if ctx.author.voice is None: - return await ctx.send("You're not connected to a voice channel.") - - channel = ctx.author.voice.channel - try: - await channel.connect() - except: - voice = ctx.voice_client - if voice.is_connected() and voice.channel != channel: - await voice.disconnect() - await channel.connect() - -def setup(client): - client.add_cog(MusicCog(client)) \ No newline at end of file +import asyncio +import discord +from discord.ext import commands +import youtube_dl +import os + +ffmpeg_options = { + 'options': '-vn' +} + +ytdl_format_options = { + 'format': 'bestaudio/best', + 'outtmpl': '%(extractor)s-%(id)s-%(title)s.%(ext)s', + 'restrictfilenames': True, + 'noplaylist': True, + 'nocheckcertificate': True, + 'ignoreerrors': False, + 'logtostderr': False, + 'quiet': True, + 'no_warnings': True, + 'default_search': 'auto', + 'source_address': '0.0.0.0' # Bind to ipv4 since ipv6 addresses cause issues sometimes +} + +ytdl = youtube_dl.YoutubeDL(ytdl_format_options) + +class YTDLSource(discord.PCMVolumeTransformer): + def __init__(self, source, *, data, volume=0.5): + super().__init__(source, volume) + + self.data = data + + self.title = data.get('title') + self.url = data.get('url') + + @classmethod + async def from_url(cls, url, *, loop=None, stream=False): + loop = loop or asyncio.get_event_loop() + data = await loop.run_in_executor(None, lambda: ytdl.extract_info(url, download=not stream)) + + if 'entries' in data: + # Take first item from a playlist + data = data['entries'][0] + + filename = data['url'] if stream else ytdl.prepare_filename(data) + return cls(discord.FFmpegPCMAudio(filename, **ffmpeg_options), data=data) + +class AudioCog(commands.Cog): + + def __init__(self, bot): + self.bot = bot + + # Commands + @commands.command() + async def play(self, ctx, url): + """Streams from a url""" + + if ctx.author.voice is None: + return await ctx.send("You're not connected to a voice channel.") + + try: + channel = ctx.author.voice.channel + await channel.connect() + except: + pass + async with ctx.typing(): + player = await YTDLSource.from_url(url, loop=self.bot.loop, stream=True) + ctx.voice_client.play(player, after=lambda e: print(f'Player error: {e}') if e else None) + await ctx.send(f'Now playing: {player.title}') + + @play.error + async def play_error(self, ctx, error): + if isinstance(error, commands.MissingRequiredArgument): + voice = ctx.voice_client + if voice != None and voice.is_paused(): + voice.resume() + else: + await ctx.send("Missing URL!") + + @commands.command(aliases=['disconnect, dc']) + async def leave(self, ctx): + """Disconnects bot from the voice channel""" + + if ctx.author.voice is None: + return await ctx.send("You're not connected to a voice channel.") + + voice = ctx.voice_client + if voice is not None: + if voice.is_connected(): + await voice.disconnect() + else: + await ctx.send("The bot is not connected to a voice channel.") + + @commands.command() + async def volume(self, ctx, volume: int): + """Changes the player's volume""" + + if ctx.author.voice is None: + return await ctx.send("You're not connected to a voice channel.") + + if ctx.voice_client is None: + return await ctx.send("Not connected to a voice channel.") + + ctx.voice_client.source.volume = volume / 100 + await ctx.send(f"Changed volume to {volume}%") + + @commands.command() + async def pause(self, ctx): + """Pauses audio""" + + if ctx.author.voice is None: + return await ctx.send("You're not connected to a voice channel.") + + voice = ctx.voice_client + if voice.is_playing(): + voice.pause() + else: + await ctx.send("Currently no audio is playing.") + + @commands.command() + async def resume(self, ctx): + """Resumes currently paused audio""" + + if ctx.author.voice is None: + return await ctx.send("You're not connected to a voice channel.") + + voice = ctx.voice_client + if voice.is_paused(): + voice.resume() + else: + await ctx.send("The audio is not paused.") + + @commands.command() + async def stop(self, ctx): + """Stops and disconnects the bot from voice""" + + if ctx.author.voice is None: + return await ctx.send("You're not connected to a voice channel.") + + voice = ctx.voice_client + if voice: + voice.stop() + + @commands.command(aliases=['join']) + async def connect(self, ctx): + """Connects bot to currently connected voice channel""" + + if ctx.author.voice is None: + return await ctx.send("You're not connected to a voice channel.") + + channel = ctx.author.voice.channel + try: + await channel.connect() + except: + voice = ctx.voice_client + if voice.is_connected() and voice.channel != channel: + await voice.disconnect() + await channel.connect() + +def setup(bot): + bot.add_cog(AudioCog(bot)) \ No newline at end of file diff --git a/cogs/info/calendarCog.py b/cogs/info/calendarCog.py index 3f47268..f04489d 100644 --- a/cogs/info/calendarCog.py +++ b/cogs/info/calendarCog.py @@ -1,59 +1,56 @@ -import os -import discord -import DiscordUtils -import datetime -from dateutil.parser import parse -import requests -from discord.ext import commands - -class CalendarCog(commands.Cog): - - def __init__(self, client): - self.client = client - - # Commands - @commands.command() - async def events(self, ctx): - """Gets all upcoming events from Google Calendar""" - # Check if environment variable exists - if not os.environ.get("GOOGLE_CALENDAR_ENDPOINT"): - return await ctx.send("No Google Calendar endpoint specified!") - - # Get upcoming events data from calendar - current_time = datetime.datetime.utcnow() - formatted_time = current_time.isoformat("T") + "Z" - calendar = os.environ.get("GOOGLE_CALENDAR_ENDPOINT") + "&timeMin=" + formatted_time - data = requests.get(calendar).json() - - # Check if there are any events - if not data["items"]: - return await ctx.send("There are no upcoming events!") - - # Get all upcoming events as a list - list_of_events = data["items"] - embeds = [] - - for event in list_of_events: - # Create data set - title = event["summary"] - start_time = event["start"]["dateTime"] - description = "No description." - if "description" in event: - description = event["description"] - formatted_start_time = datetime.datetime.strftime(parse(start_time), format="%B %d, %Y") - - # Create embed for single event and add to embeds list - embed = discord.Embed(color=ctx.author.color, title=title, description=description) - embed.add_field(name="Starts On", value=formatted_start_time, inline=True) - embeds.append(embed) - - # Create paginator - paginator = DiscordUtils.Pagination.CustomEmbedPaginator(ctx) - paginator.add_reaction('⏮️', "first") - paginator.add_reaction('⏪', "back") - paginator.add_reaction('⏩', "next") - paginator.add_reaction('⏭️', "last") - await paginator.run(embeds) - -def setup(client): - client.add_cog(CalendarCog(client)) \ No newline at end of file +import os +import discord +import DiscordUtils +import datetime +from dateutil.parser import parse +import requests +from discord.ext import commands + +class CalendarCog(commands.Cog): + + def __init__(self, bot): + self.bot = bot + + # Commands + @commands.command() + async def events(self, ctx): + """Gets all upcoming events from Google Calendar""" + # Check if environment variable exists + if not os.environ.get("GOOGLE_CALENDAR_ENDPOINT"): + return await ctx.send("No Google Calendar endpoint specified!") + + # Get upcoming events data from calendar + current_time = datetime.datetime.utcnow() + formatted_time = current_time.isoformat("T") + "Z" + calendar = os.environ.get("GOOGLE_CALENDAR_ENDPOINT") + "&timeMin=" + formatted_time + data = requests.get(calendar).json() + + # Check if there are any events + if not data["items"]: + return await ctx.send("There are no upcoming events!") + + # Get all upcoming events as a list + list_of_events = data["items"] + embeds = [] + + for event in list_of_events: + # Create data set + title = event["summary"] + start_time = event["start"]["dateTime"] + description = event["description"] if "description" in event else "No description." + formatted_start_time = datetime.datetime.strftime(parse(start_time), format="%B %d, %Y") + # Create embed for single event and add to embeds list + embed = discord.Embed(color=ctx.author.color, title=title, description=description) + embed.add_field(name="Starts On", value=formatted_start_time, inline=True) + embeds.append(embed) + + # Create paginator + paginator = DiscordUtils.Pagination.CustomEmbedPaginator(ctx) + paginator.add_reaction('⏮️', "first") + paginator.add_reaction('⏪', "back") + paginator.add_reaction('⏩', "next") + paginator.add_reaction('⏭️', "last") + await paginator.run(embeds) + +def setup(bot): + bot.add_cog(CalendarCog(bot)) \ No newline at end of file diff --git a/cogs/info/infoCog.py b/cogs/info/infoCog.py new file mode 100644 index 0000000..67e7bcf --- /dev/null +++ b/cogs/info/infoCog.py @@ -0,0 +1,30 @@ +import discord +from discord.ext import commands + +class InfoCog(commands.Cog): + + def __init__(self, bot): + self.bot = bot + + # Commands + @commands.command(aliases=['botinfo']) + async def info(self, ctx): + embed = discord.Embed( + title=f"Rutgers Esports Bot <:RutgersEsports:608498339192766505>", + description=( + f'Rutgers Esports Bot is the official Discord bot\n' + 'for handling all internal Rutgers Esports operations.\n\n' + '🔗 Check out Rutgers Esports ' + '[here](https://linktr.ee/RutgersEsports).\n' + '🤖 To add this bot to your server use ' + '[this link](https://bit.ly/rutgers-esports-bot).\n' + "⭐ Star our repo on GitHub [here](https://github.com/rutgersesports/discord-bot)." + ), + color=0xC94949 + ) + embed.set_thumbnail(url=ctx.bot.user.avatar_url) + + await ctx.send(embed=embed) + +def setup(bot): + bot.add_cog(InfoCog(bot)) \ No newline at end of file diff --git a/cogs/info/pingCog.py b/cogs/info/pingCog.py index fdf5c33..246220b 100644 --- a/cogs/info/pingCog.py +++ b/cogs/info/pingCog.py @@ -1,16 +1,16 @@ -import discord -from discord.ext import commands - -class PingCog(commands.Cog): - - def __init__(self, client): - self.client = client - - # Commands - @commands.command(aliases=['latency']) - async def ping(self, ctx): - """Returns the bot client latency""" - await ctx.send(f'Pong! {round(self.client.latency * 1000)}ms') - -def setup(client): - client.add_cog(PingCog(client)) \ No newline at end of file +import discord +from discord.ext import commands + +class PingCog(commands.Cog): + + def __init__(self, bot): + self.bot = bot + + # Commands + @commands.command(aliases=['latency']) + async def ping(self, ctx): + """Returns the bot bot latency""" + await ctx.send(f'Pong! {round(self.bot.latency * 1000)}ms') + +def setup(bot): + bot.add_cog(PingCog(bot)) \ No newline at end of file diff --git a/cogs/info/serverCog.py b/cogs/info/serverCog.py new file mode 100644 index 0000000..54e1ef1 --- /dev/null +++ b/cogs/info/serverCog.py @@ -0,0 +1,35 @@ +import discord +from discord.ext import commands + +class ServerCog(commands.Cog): + + def __init__(self, bot): + self.bot = bot + + # Commands + @commands.command(aliases=['server', 'sinfo']) + async def serverinfo(self, ctx): + """Get information about the current server""" + guild = ctx.guild + + roles = str(len(guild.roles)) + emojis = str(len(guild.emojis)) + vchannels = str(len(guild.voice_channels)) + tchannels = str(len(guild.text_channels)) + + embed = discord.Embed(title='Server info', description=guild.name, color=ctx.guild.get_member(ctx.bot.user.id).color) + embed.set_thumbnail(url=guild.icon_url) + embed.add_field(name='ID', value=guild.id, inline=True) + embed.add_field(name='Owner', value=guild.owner, inline=True) + embed.add_field(name='Members', value=guild.member_count, inline=True) + embed.add_field(name='Text channels', value=tchannels, inline=True) + embed.add_field(name='Voice channels', value=vchannels, inline=True) + embed.add_field(name='Created on', value=guild.created_at.strftime('%B %d, %Y'), inline=True) + embed.add_field(name='Region', value=guild.region, inline=True) + embed.add_field(name='Roles', value=roles, inline=True) + embed.add_field(name='Verification', value=guild.verification_level, inline=True) + + await ctx.send(embed=embed) + +def setup(bot): + bot.add_cog(ServerCog(bot)) \ No newline at end of file diff --git a/cogs/info/userCog.py b/cogs/info/userCog.py new file mode 100644 index 0000000..4f1af81 --- /dev/null +++ b/cogs/info/userCog.py @@ -0,0 +1,44 @@ +import discord +from discord.ext import commands + +class UserCog(commands.Cog): + + def __init__(self, bot): + self.bot = bot + + # Commands + @commands.command(aliases=['whois']) + async def userinfo(self, ctx, member: discord.Member = None): + """Get information about a given user""" + # Gather data + member = ctx.author if not member else member + roles = [role for role in member.roles] + default_role = discord.utils.get(member.guild.roles, name='@everyone') + role_mentions = [f'{role.mention}' for role in sorted(member.roles, key=lambda x: x.position, reverse=True) if role != default_role] + all_perms = [x for x in dir(ctx.channel.permissions_for(member))] + permissions = [] + for perm in all_perms: + perm_name = perm + if getattr(ctx.channel.permissions_for(member), perm_name) is True: + permissions.append(perm_name.title().replace("_", " ").replace("Tts", "TTS")) + + # Create embed + embed = discord.Embed(description = member.mention, color = member.color, timestamp = ctx.message.created_at) + + embed.set_author(name = member, icon_url = member.avatar_url) + embed.set_thumbnail(url = member.avatar_url) + embed.set_footer(text = member.id) + + embed.add_field(name = 'Joined', value = member.joined_at.strftime('%a, %d %B %Y, %I:%M %p UTC'), inline=True) + embed.add_field(name = 'Registered', value = member.created_at.strftime('%a, %d %B %Y, %I:%M %p UTC'), inline=True) + + embed.add_field(name=f'Roles [{len(roles)}]', value=", ".join(role_mentions)+f', {default_role}', inline=False) + embed.add_field(name=f'Permissions [{len(permissions)}]', value=", ".join(permissions), inline=False) + + + embed.add_field(name='Nickname', value=member.nick if hasattr(member, 'nick') else 'None', inline=True) + + await ctx.send(embed = embed) + +def setup(bot): + bot.add_cog(UserCog(bot)) \ No newline at end of file diff --git a/cogs/inventory/inventoryCog.py b/cogs/inventory/inventoryCog.py index 239c0d7..d9f0dad 100644 --- a/cogs/inventory/inventoryCog.py +++ b/cogs/inventory/inventoryCog.py @@ -1,61 +1,61 @@ -import os -import discord -import DiscordUtils -import asyncio -from google.cloud import firestore -from discord.ext import commands -from dotenv import load_dotenv - -load_dotenv() - -class InventoryCog(commands.Cog): - - def __init__(self, client): - self.client = client - # Commands - - # Get a single item from inventory - @commands.command(aliases=['inv']) - @commands.is_owner() - async def inventory(self, ctx, arg1): - """Get a single item from the inventory database""" - - # Make single input embed - def single_embed(item, id): - embed = discord.Embed(title=item["item_name"], description=id) - if item["item_type"] != "": embed.add_field(name="Item Type", value=item["item_type"], inline=True) - if item["color"] != "": embed.add_field(name="Color", value=item["color"], inline=True) - if item["brand"] != "": embed.add_field(name="Brand", value=item["brand"], inline=True) - if item["model"] != "": embed.add_field(name="Model", value=item["model"], inline=True) - if item["size"] != "": embed.add_field(name="Size", value=item["size"], inline=True) - if item["quantity"] != "": embed.add_field(name="Quantity", value=item["quantity"], inline=True) - if item["box"] != "": embed.add_field(name="Box", value=item["box"], inline=True) - return embed - - # Connect to Firestore DB inventory collection and get the item arg1 as doc - if "INVENTORY_PROJECT_ID" in os.environ and "GOOGLE_APPLICATION_CREDENTIALS" in os.environ: - try: - db = firestore.AsyncClient(project=os.environ.get("INVENTORY_PROJECT_ID")) - inventory_ref = db.collection("inventory").document(arg1) - doc = await inventory_ref.get() - - if doc.exists: - await ctx.send(embed=single_embed(doc.to_dict(), doc.id)) - else: - await ctx.send("That ID doesn't exist!") - except: - await ctx.send("The DB connection is not working.") - elif "INVENTORY_PROJECT_ID" not in os.environ and "GOOGLE_APPLICATION_CREDENTIALS" in os.environ: - await ctx.send("Inventory Project ID not found!") - elif "GOOGLE_APPLICATION_CREDENTIALS" not in os.environ: - await ctx.send("GAPP Credentials not found!") - - @inventory.error - async def inventory_error(self, ctx, error): - if isinstance(error, commands.MissingRequiredArgument): - await ctx.send("Missing required argument! (e.g. d!inventory __10000__)") - if isinstance(error, commands.NotOwner): - await ctx.send("You do not have permissions to run this command.") - -def setup(client): - client.add_cog(InventoryCog(client)) \ No newline at end of file +import os +import discord +import DiscordUtils +import asyncio +from google.cloud import firestore +from discord.ext import commands +from dotenv import load_dotenv + +load_dotenv() + +class InventoryCog(commands.Cog): + + def __init__(self, bot): + self.bot = bot + + # Commands + # Get a single item from inventory + @commands.command(aliases=['inv']) + @commands.is_owner() + async def inventory(self, ctx, arg1): + """Get a single item from the inventory database""" + + # Make single input embed + def single_embed(item, id): + embed = discord.Embed(title=item["item_name"], description=id) + if item["item_type"] != "": embed.add_field(name="Item Type", value=item["item_type"], inline=True) + if item["color"] != "": embed.add_field(name="Color", value=item["color"], inline=True) + if item["brand"] != "": embed.add_field(name="Brand", value=item["brand"], inline=True) + if item["model"] != "": embed.add_field(name="Model", value=item["model"], inline=True) + if item["size"] != "": embed.add_field(name="Size", value=item["size"], inline=True) + if item["quantity"] != "": embed.add_field(name="Quantity", value=item["quantity"], inline=True) + if item["box"] != "": embed.add_field(name="Box", value=item["box"], inline=True) + return embed + + # Connect to Firestore DB inventory collection and get the item arg1 as doc + if "INVENTORY_PROJECT_ID" in os.environ and "GOOGLE_APPLICATION_CREDENTIALS" in os.environ: + try: + db = firestore.AsyncClient(project=os.environ.get("INVENTORY_PROJECT_ID")) + inventory_ref = db.collection("inventory").document(arg1) + doc = await inventory_ref.get() + + if doc.exists: + await ctx.send(embed=single_embed(doc.to_dict(), doc.id)) + else: + await ctx.send("That ID doesn't exist!") + except: + await ctx.send("The DB connection is not working.") + elif "INVENTORY_PROJECT_ID" not in os.environ and "GOOGLE_APPLICATION_CREDENTIALS" in os.environ: + await ctx.send("Inventory Project ID not found!") + elif "GOOGLE_APPLICATION_CREDENTIALS" not in os.environ: + await ctx.send("GAPP Credentials not found!") + + @inventory.error + async def inventory_error(self, ctx, error): + if isinstance(error, commands.MissingRequiredArgument): + await ctx.send("Missing required argument! (e.g. d!inventory __10000__)") + if isinstance(error, commands.NotOwner): + await ctx.send("You do not have permissions to run this command.") + +def setup(bot): + bot.add_cog(InventoryCog(bot)) \ No newline at end of file diff --git a/cogs/moderation/embedCog.py b/cogs/moderation/embedCog.py new file mode 100644 index 0000000..dfd370a --- /dev/null +++ b/cogs/moderation/embedCog.py @@ -0,0 +1,64 @@ +import discord +from discord.ext import commands + +class EmbedCog(commands.Cog): + + def __init__(self, bot): + self.bot = bot + + # Commands + @commands.command() + async def embed(self, ctx, *, data): + """Produces a customizable embed""" + # Split arguments into a list + data = data.split("%%") + + # Set default channel to current channel where command is used + channel = ctx.channel + + # Remove [''] surrounding possible channel argument input (slice value to just channel ID) + possible_channel = data[0].rstrip()[2:-1] + + # Set channel if channel argument is given + try: + channel = await commands.TextChannelConverter().convert(ctx, possible_channel) + data.pop(0) + except Exception as e: + pass + + # Check if user has permission to send in given channel + if not ctx.guild.get_member(ctx.author.id).permissions_in(channel).send_messages: + return await ctx.send(f"You don't have permission to send messages to {channel.mention}!") + + + # Add possible empty list values to deter IndexError for embed + data += [""] * (3-len(data)) + + # Create and send embed + embed = discord.Embed( + title=f"{data[0]}", + description=f"{data[1]}", + color=ctx.guild.get_member(ctx.bot.user.id).color + ) + embed.set_footer( + text=f"{data[2]}" + ) + await channel.send(embed=embed) + + @embed.error + async def embed_error(self, ctx, error): + await ctx.send( + "Type the command in the following format:```.embed #channel_name %% title %% description %% footer```" + ) + embed = discord.Embed( + title="Title", + description="Description", + color=ctx.guild.get_member(ctx.bot.user.id).color + ) + embed.set_footer( + text="Footer" + ) + await ctx.send(embed=embed) + +def setup(bot): + bot.add_cog(EmbedCog(bot)) \ No newline at end of file diff --git a/cogs/moderation/moderationCog.py b/cogs/moderation/moderationCog.py index a769e5c..2eed918 100644 --- a/cogs/moderation/moderationCog.py +++ b/cogs/moderation/moderationCog.py @@ -1,31 +1,31 @@ -import discord -from discord.ext import commands - -class ModerationCog(commands.Cog): - - def __init__(self, client): - self.client = client - - # Commands - @commands.command() - @commands.has_permissions(kick_members = True) - async def kick(self, ctx, member : commands.MemberConverter, *, reason="No reason provided."): - """Kick a user from the server""" - try: - await member.kick(reason=reason) - await ctx.send(member.mention + " has been kicked.") - except: - await ctx.send(f"Unable to kick {member.mention}.\nIs {member.mention} at the same role level or higher than {self.client.user.name}?") - - @commands.command() - @commands.has_permissions(ban_members = True) - async def ban(self, ctx, member : commands.MemberConverter, *, reason="No reason provided."): - """Ban a user from the server""" - try: - await member.ban(reason=reason) - await ctx.send(member.mention + " has been banned.") - except: - await ctx.send(f"Unable to ban {member.mention}.\nIs {member.mention} at the same role level or higher than {self.client.user.name}?") - -def setup(client): - client.add_cog(ModerationCog(client)) \ No newline at end of file +import discord +from discord.ext import commands + +class ModerationCog(commands.Cog): + + def __init__(self, bot): + self.bot = bot + + # Commands + @commands.command() + @commands.has_permissions(kick_members = True) + async def kick(self, ctx, member : commands.MemberConverter, *, reason="No reason provided."): + """Kick a user from the server""" + try: + await member.kick(reason=reason) + await ctx.send(member.mention + " has been kicked.") + except: + await ctx.send(f"Unable to kick {member.mention}.\nIs {member.mention} at the same role level or higher than {self.bot.user.name}?") + + @commands.command() + @commands.has_permissions(ban_members = True) + async def ban(self, ctx, member : commands.MemberConverter, *, reason="No reason provided."): + """Ban a user from the server""" + try: + await member.ban(reason=reason) + await ctx.send(member.mention + " has been banned.") + except: + await ctx.send(f"Unable to ban {member.mention}.\nIs {member.mention} at the same role level or higher than {self.bot.user.name}?") + +def setup(bot): + bot.add_cog(ModerationCog(bot)) \ No newline at end of file diff --git a/cogs/moderation/pruneCog.py b/cogs/moderation/pruneCog.py new file mode 100644 index 0000000..5a7cc8c --- /dev/null +++ b/cogs/moderation/pruneCog.py @@ -0,0 +1,18 @@ +import discord +from discord.ext import commands + +class PruneCog(commands.Cog): + + def __init__(self, bot): + self.bot = bot + + # Commands + @commands.command(aliases=['clear', 'prune']) + @commands.has_permissions(manage_messages=True) + async def purge(self, ctx, amount=10): + """Removes a number of messages (10 by default)""" + await ctx.channel.purge(limit=amount+1) + await ctx.send(f'`{amount}` messages deleted.', delete_after=3) + +def setup(bot): + bot.add_cog(PruneCog(bot)) \ No newline at end of file diff --git a/cogs/owner/loaderCog.py b/cogs/owner/loaderCog.py new file mode 100644 index 0000000..2f5da8a --- /dev/null +++ b/cogs/owner/loaderCog.py @@ -0,0 +1,23 @@ +import discord +from discord.ext import commands + +class LoaderCog(commands.Cog): + + def __init__(self, bot): + self.bot = bot + + # Commands + @commands.is_owner() + @commands.command() + async def load(self, ctx, extension): + """Loads a cog""" + self.bot.load_extension(f'cogs.{extension}') + + @commands.is_owner() + @commands.command() + async def unload(self, ctx, extension): + """Unloads a cog""" + self.bot.unload_extension(f'cogs.{extension}') + +def setup(bot): + bot.add_cog(LoaderCog(bot)) \ No newline at end of file diff --git a/cogs/owner/sayCog.py b/cogs/owner/sayCog.py new file mode 100644 index 0000000..f5db55e --- /dev/null +++ b/cogs/owner/sayCog.py @@ -0,0 +1,18 @@ +import discord +from discord.ext import commands + +class SayCog(commands.Cog): + + def __init__(self, bot): + self.bot = bot + + # Commands + @commands.command(aliases=['speak']) + @commands.is_owner() + async def say(self, ctx, *, content): + """Lets the bot say given arguments""" + await ctx.message.delete() + await ctx.send(content) + +def setup(bot): + bot.add_cog(SayCog(bot)) \ No newline at end of file diff --git a/cogs/owner/statusCog.py b/cogs/owner/statusCog.py new file mode 100644 index 0000000..7f4c6e3 --- /dev/null +++ b/cogs/owner/statusCog.py @@ -0,0 +1,29 @@ +import discord +from discord.ext import commands + +class StatusCog(commands.Cog): + + def __init__(self, bot): + self.bot = bot + + # Commands + @commands.command(aliases=['botstatus', 'stat']) + @commands.is_owner() + async def status(self, ctx): + """Gives bot statistics""" + # Total members of all servers bot is in + total_guild_users = 0 + for guild in ctx.bot.guilds: + total_guild_users += guild.member_count + + embed = discord.Embed( + title=f"Bot Status", + description=(f'`{ctx.bot.user.name}` is online.' + f'\nBot is in `{len(ctx.bot.guilds)}` guilds.' + f'\nGuilds have `{total_guild_users}` members.'), + color=ctx.guild.get_member(ctx.bot.user.id).color + ) + await ctx.send(embed=embed) + +def setup(bot): + bot.add_cog(StatusCog(bot)) \ No newline at end of file diff --git a/images/Bot-Example.png b/images/Bot-Example.png new file mode 100644 index 0000000..950c533 Binary files /dev/null and b/images/Bot-Example.png differ diff --git a/images/GitHub-Banner.png b/images/GitHub-Banner.png new file mode 100644 index 0000000..b60ba52 Binary files /dev/null and b/images/GitHub-Banner.png differ diff --git a/images/icons/discord.svg b/images/icons/discord.svg new file mode 100644 index 0000000..ca65400 --- /dev/null +++ b/images/icons/discord.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/icons/facebook-alt.svg b/images/icons/facebook-alt.svg new file mode 100644 index 0000000..dd748b1 --- /dev/null +++ b/images/icons/facebook-alt.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/icons/facebook.svg b/images/icons/facebook.svg new file mode 100644 index 0000000..b328631 --- /dev/null +++ b/images/icons/facebook.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/icons/github.svg b/images/icons/github.svg new file mode 100644 index 0000000..3fac1ed --- /dev/null +++ b/images/icons/github.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/icons/instagram.svg b/images/icons/instagram.svg new file mode 100644 index 0000000..9371bdc --- /dev/null +++ b/images/icons/instagram.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/icons/link.svg b/images/icons/link.svg new file mode 100644 index 0000000..d82cb90 --- /dev/null +++ b/images/icons/link.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + diff --git a/images/icons/linked-in-alt.svg b/images/icons/linked-in-alt.svg new file mode 100644 index 0000000..1f9463a --- /dev/null +++ b/images/icons/linked-in-alt.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/icons/linked-in.svg b/images/icons/linked-in.svg new file mode 100644 index 0000000..4e7b515 --- /dev/null +++ b/images/icons/linked-in.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/images/icons/mail.svg b/images/icons/mail.svg new file mode 100644 index 0000000..2b5177d --- /dev/null +++ b/images/icons/mail.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + diff --git a/images/icons/reddit.svg b/images/icons/reddit.svg new file mode 100644 index 0000000..e13d583 --- /dev/null +++ b/images/icons/reddit.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/icons/twitch.svg b/images/icons/twitch.svg new file mode 100644 index 0000000..3120fea --- /dev/null +++ b/images/icons/twitch.svg @@ -0,0 +1,21 @@ + + + + +Asset 2 + + + + + + + + + + + diff --git a/images/icons/twitter-alt.svg b/images/icons/twitter-alt.svg new file mode 100644 index 0000000..165cd7c --- /dev/null +++ b/images/icons/twitter-alt.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/icons/twitter.svg b/images/icons/twitter.svg new file mode 100644 index 0000000..1303a71 --- /dev/null +++ b/images/icons/twitter.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/icons/youtube.svg b/images/icons/youtube.svg new file mode 100644 index 0000000..62005ce --- /dev/null +++ b/images/icons/youtube.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +