diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bdc8b64 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Jonah Lawrence + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..9f5d293 --- /dev/null +++ b/bot.py @@ -0,0 +1,49 @@ +import os + +import nextcord +from nextcord.ext import commands + +import config + + +def main(): + # Allows privledged intents for monitoring members joining, roles editing, and role assignments + # These need to be enabled in the developer portal as well + intents = nextcord.Intents.default() + + # Required in order to read messages (eg. prefix commands) + intents.message_content = True + + # To enable the guilds priveleged intent: + # intents.guilds = True + + # To enable the members priveleged intent: + # intents.members = True + + # Set custom status to "Listening to ?help" + activity = nextcord.Activity( + type=nextcord.ActivityType.listening, name=f"{config.BOT_PREFIX}help" + ) + + bot = commands.Bot( + commands.when_mentioned_or(config.BOT_PREFIX), + intents=intents, + activity=activity, + ) + + # Get the modules of all cogs whose directory structure is ./cogs/ + for folder in os.listdir("cogs"): + bot.load_extension(f"cogs.{folder}") + + @bot.listen() + async def on_ready(): + """When Discord is connected""" + assert bot.user is not None + print(f"{bot.user.name} has connected to Discord!") + + # Run Discord bot + bot.run(config.DISCORD_TOKEN) + + +if __name__ == "__main__": + main() diff --git a/cogs/app_commands/__init__.py b/cogs/app_commands/__init__.py new file mode 100644 index 0000000..42e9366 --- /dev/null +++ b/cogs/app_commands/__init__.py @@ -0,0 +1,30 @@ +import nextcord +from nextcord.ext import commands +from utils import embed_success + + +class ApplicationCommandsExample(commands.Cog, name="App Commands"): + def __init__(self, bot: commands.Bot): + self.bot = bot + + @nextcord.slash_command(description="Slash command that responds with 'Hello World'") + async def my_slash_cmd(self, interaction: nextcord.Interaction): + await interaction.send(embed=embed_success("Hello World!")) + + @nextcord.slash_command(description="Slash command that repeats an argument") + async def my_slash_cmd_2(self, interaction: nextcord.Interaction, my_arg: str): + await interaction.send(embed=embed_success(f"You said: {my_arg}")) + + @nextcord.user_command() + async def my_user_cmd(self, interaction: nextcord.Interaction, member: nextcord.Member): + await interaction.send(f"Hello, {member.mention}!") + + @nextcord.message_command() + async def my_message_cmd(self, interaction: nextcord.Interaction, message: nextcord.Message): + await interaction.send(f"Hello, {message.author.mention}!") + + +# This function will be called when this extension is loaded. +# It is necessary to add these functions to the bot. +def setup(bot: commands.Bot): + bot.add_cog(ApplicationCommandsExample(bot)) diff --git a/cogs/help/__init__.py b/cogs/help/__init__.py new file mode 100644 index 0000000..31aeac1 --- /dev/null +++ b/cogs/help/__init__.py @@ -0,0 +1,19 @@ +from nextcord.ext import commands + +from .help_command import NewHelpCommand + + +class HelpCog(commands.Cog): + def __init__(self, bot: commands.Bot): + self.bot = bot + self._original_help_command = bot.help_command + bot.help_command = NewHelpCommand() + bot.help_command.cog = self + + def cog_unload(self): + self.bot.help_command = self._original_help_command + + +# setup functions for bot +def setup(bot: commands.Bot): + bot.add_cog(HelpCog(bot)) diff --git a/cogs/help/help_command.py b/cogs/help/help_command.py new file mode 100644 index 0000000..8634b78 --- /dev/null +++ b/cogs/help/help_command.py @@ -0,0 +1,80 @@ +import config +import nextcord +from nextcord.embeds import EmptyEmbed +from nextcord.ext import commands + + +class NewHelpCommand(commands.MinimalHelpCommand): + """Custom help command override using embeds""" + + COLOUR = nextcord.Colour.blurple() + + def get_ending_note(self): + """Returns note to display at the bottom""" + invoked_with = self.invoked_with + return f"Use {config.BOT_PREFIX}{invoked_with} [command] for more info on a command." + + def get_command_signature(self, command: commands.core.Command): + """Retrieves the signature portion of the help page.""" + return f"{command.qualified_name} {command.signature}" + + async def send_bot_help(self, mapping: dict): + """implements bot command help page""" + embed = nextcord.Embed(title="Bot Commands", colour=self.COLOUR) + avatar = self.context.bot.user.avatar + avatar_url = avatar.url if avatar else EmptyEmbed + embed.set_author(name=self.context.bot.user.name, icon_url=avatar_url) + description = self.context.bot.description + if description: + embed.description = description + + for cog, commands in mapping.items(): + name = "No Category" if cog is None else cog.qualified_name + filtered = await self.filter_commands(commands, sort=True) + if filtered: + # \u2002 = middle dot + value = "\u2002".join(f"{config.BOT_PREFIX}{c.name}" for c in filtered) + if cog and cog.description: + value = f"{cog.description}\n{value}" + embed.add_field(name=name, value=value) + + embed.set_footer(text=self.get_ending_note()) + await self.get_destination().send(embed=embed) + + async def send_cog_help(self, cog: commands.Cog): + """implements cog help page""" + embed = nextcord.Embed(title=f"{cog.qualified_name} Commands", colour=self.COLOUR) + if cog.description: + embed.description = cog.description + + filtered = await self.filter_commands(cog.get_commands(), sort=True) + for command in filtered: + embed.add_field( + name=self.get_command_signature(command), + value=command.short_doc or "...", + inline=False, + ) + + embed.set_footer(text=self.get_ending_note()) + await self.get_destination().send(embed=embed) + + async def send_group_help(self, group: commands.Group): + """implements group help page and command help page""" + embed = nextcord.Embed(title=group.qualified_name, colour=self.COLOUR) + if group.help: + embed.description = group.help + + if isinstance(group, commands.Group): + filtered = await self.filter_commands(group.commands, sort=True) + for command in filtered: + embed.add_field( + name=self.get_command_signature(command), + value=command.short_doc or "...", + inline=False, + ) + + embed.set_footer(text=self.get_ending_note()) + await self.get_destination().send(embed=embed) + + # Use the same function as group help for command help + send_command_help = send_group_help # type: ignore diff --git a/cogs/ping/__init__.py b/cogs/ping/__init__.py new file mode 100644 index 0000000..f768c4e --- /dev/null +++ b/cogs/ping/__init__.py @@ -0,0 +1,26 @@ +from nextcord.ext import commands +from utils import embed_success + + +class Ping(commands.Cog, name="Ping"): + def __init__(self, bot: commands.Bot): + self.bot = bot + + @commands.command(name="ping") + async def ping(self, ctx: commands.Context): + """A command which simply acknowledges the user's ping. + Usage: + ``` + ?ping + ``` + """ + # log in console that a ping was received + print("Received ping") + # respond to the message + await ctx.send(embed=embed_success("Pong!")) + + +# This function will be called when this extension is loaded. +# It is necessary to add these functions to the bot. +def setup(bot: commands.Bot): + bot.add_cog(Ping(bot)) diff --git a/config.py b/config.py new file mode 100644 index 0000000..21eb1e4 --- /dev/null +++ b/config.py @@ -0,0 +1,9 @@ +import os + +from dotenv.main import load_dotenv + +load_dotenv() + +# Discord config +DISCORD_TOKEN = os.getenv("DISCORD_TOKEN", "") +BOT_PREFIX = "?" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2212c02 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +nextcord==2.4.2 +python-dotenv==1.0.0 diff --git a/runtime.txt b/runtime.txt new file mode 100644 index 0000000..73e24dc --- /dev/null +++ b/runtime.txt @@ -0,0 +1 @@ +python-3.10.4 diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..387d09a --- /dev/null +++ b/utils/__init__.py @@ -0,0 +1 @@ +from .embedder import * diff --git a/utils/embedder.py b/utils/embedder.py new file mode 100644 index 0000000..13a5654 --- /dev/null +++ b/utils/embedder.py @@ -0,0 +1,74 @@ +from typing import Optional + +import nextcord + +DEFAULT_COLOR = nextcord.Colour.blurple() +MAX_EMBED_DESCRIPTION_LENGTH = 4096 +MAX_EMBED_FIELD_TITLE_LENGTH = 256 +MAX_EMBED_FIELD_FOOTER_LENGTH = 2048 + + +def __trim(text: str, limit: int) -> str: + """limit text to a certain number of characters""" + return text[: limit - 3].strip() + "..." if len(text) > limit else text + + +def embed_success( + title: str, + description: Optional[str] = None, + footer: Optional[str] = None, + url: Optional[str] = None, + image: Optional[str] = None, + thumbnail: Optional[str] = None, +) -> nextcord.Embed: + """Embed a success message and an optional description, footer, and url""" + return build_embed(title, description, footer, url, nextcord.Colour.green(), image, thumbnail) + + +def embed_warning( + title: str, + description: Optional[str] = None, + footer: Optional[str] = None, + url: Optional[str] = None, + image: Optional[str] = None, + thumbnail: Optional[str] = None, +) -> nextcord.Embed: + """Embed a warning message and an optional description, footer, and url""" + return build_embed(title, description, footer, url, nextcord.Colour.gold(), image, thumbnail) + + +def embed_error( + title: str, + description: Optional[str] = None, + footer: Optional[str] = None, + url: Optional[str] = None, + image: Optional[str] = None, + thumbnail: Optional[str] = None, +) -> nextcord.Embed: + """Embed an error message and an optional description, footer, and url""" + return build_embed(title, description, footer, url, nextcord.Colour.red(), image, thumbnail) + + +def build_embed( + title: str, + description: Optional[str] = None, + footer: Optional[str] = None, + url: Optional[str] = None, + colour: nextcord.Colour = DEFAULT_COLOR, + image: Optional[str] = None, + thumbnail: Optional[str] = None, +) -> nextcord.Embed: + """Embed a message and an optional description, footer, and url""" + # create the embed + embed = nextcord.Embed( + title=__trim(title, MAX_EMBED_FIELD_TITLE_LENGTH), url=url, colour=colour + ) + if description: + embed.description = __trim(description, MAX_EMBED_DESCRIPTION_LENGTH) + if footer: + embed.set_footer(text=__trim(footer, MAX_EMBED_FIELD_FOOTER_LENGTH)) + if image: + embed.set_image(url=image) + if thumbnail: + embed.set_thumbnail(url=thumbnail) + return embed