From c44f1024da7707dd8271af18b7d560f4dc0e7744 Mon Sep 17 00:00:00 2001 From: mudkipdev Date: Sun, 7 May 2023 14:00:46 -0600 Subject: [PATCH] initial commit --- .github/workflows/black.yml | 10 + .gitignore | 132 ++++++++ LICENSE | 21 ++ README.md | 2 + example.config.toml | 22 ++ pyproject.toml | 23 ++ scnewsbot/__init__.py | 0 scnewsbot/__main__.py | 26 ++ scnewsbot/bot.py | 22 ++ scnewsbot/extensions/announcements.py | 455 ++++++++++++++++++++++++++ scnewsbot/extensions/templates.py | 103 ++++++ scnewsbot/utils.py | 52 +++ 12 files changed, 868 insertions(+) create mode 100644 .github/workflows/black.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 example.config.toml create mode 100644 pyproject.toml create mode 100644 scnewsbot/__init__.py create mode 100644 scnewsbot/__main__.py create mode 100644 scnewsbot/bot.py create mode 100644 scnewsbot/extensions/announcements.py create mode 100644 scnewsbot/extensions/templates.py create mode 100644 scnewsbot/utils.py diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml new file mode 100644 index 0000000..de76507 --- /dev/null +++ b/.github/workflows/black.yml @@ -0,0 +1,10 @@ +name: Code Formatter + +on: [push, pull_request] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: psf/black@stable \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4ff7bce --- /dev/null +++ b/.gitignore @@ -0,0 +1,132 @@ +# scnewsbot +config.toml + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7871a52 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 mudkip + +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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f6fb89c --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# scnewsbot +A bot for the r/starcitizen Discord server. \ No newline at end of file diff --git a/example.config.toml b/example.config.toml new file mode 100644 index 0000000..d79a5b1 --- /dev/null +++ b/example.config.toml @@ -0,0 +1,22 @@ +# This is important. Make sure to set this to "false" when running the bot. +debug = true + +[bot] +prefix = "&" +extensions = [ + "announcements", + "templates" +] + +[permissions] +# This works in a whitelist mode. The invoker of the command must be in an allowed guild and +# must have an allowed role. If neither of those are true, allowed_users is checked last. +allowed_guilds = [] +allowed_roles = [] +allowed_users = [] + +[permissions.debug] +# This works in addition to the regular permissions, but only if debug mode is enabled. +allowed_guilds = [] +allowed_roles = [] +allowed_users = [] \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..ba10808 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,23 @@ +[tool.poetry] +name = "scnewsbot" +version = "0.0.0" +description = "A bot for the r/starcitizen Discord server." +authors = ["mudkipdev "] +license = "MIT" +readme = "README.md" +packages = [ + { include = "scnewsbot" } +] + +[tool.poetry.dependencies] +python = "^3.11" +discord-py = "^2.2.2" +jishaku = "^2.5.1" +python-dotenv = "^1.0.0" + +[tool.poetry.group.dev.dependencies] +black = "^23.3.0" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/scnewsbot/__init__.py b/scnewsbot/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scnewsbot/__main__.py b/scnewsbot/__main__.py new file mode 100644 index 0000000..afe718a --- /dev/null +++ b/scnewsbot/__main__.py @@ -0,0 +1,26 @@ +import tomllib +import os +from dotenv import load_dotenv +from bot import Bot +from utils import Config + + +class InvalidTokenException(Exception): + pass + + +def main() -> None: + DISCORD_TOKEN = os.getenv("DISCORD_TOKEN") + if not DISCORD_TOKEN: + raise InvalidTokenException("A Discord token was not set.") + + with open("config.toml", "rb") as config_file: + config = Config(tomllib.load(config_file)) + + bot = Bot(config) + bot.run(DISCORD_TOKEN) + + +if __name__ == "__main__": + load_dotenv() + main() diff --git a/scnewsbot/bot.py b/scnewsbot/bot.py new file mode 100644 index 0000000..447ec60 --- /dev/null +++ b/scnewsbot/bot.py @@ -0,0 +1,22 @@ +from discord.ext import commands +import discord +from utils import Config + +INTENTS = discord.Intents.default() +INTENTS.message_content = True +INTENTS.members = True + + +class Bot(commands.Bot): + def __init__(self, config: Config, /) -> None: + super().__init__( + intents=INTENTS, + command_prefix=commands.when_mentioned_or(config.prefix), + allowed_mentions=discord.AllowedMentions(everyone=False), + case_insensitive=True, + ) + self.config = config + + async def setup_hook(self) -> None: + for extension in self.config.extensions: + await self.load_extension(extension) diff --git a/scnewsbot/extensions/announcements.py b/scnewsbot/extensions/announcements.py new file mode 100644 index 0000000..367a76e --- /dev/null +++ b/scnewsbot/extensions/announcements.py @@ -0,0 +1,455 @@ +from __future__ import annotations + +from typing import Any, Optional, Callable, Awaitable +from dataclasses import dataclass +from discord.ext import commands +import discord +from utils import can_publish_announcements + +ANNOUNCEMENT_BUILDER_TIMEOUT = 1200 +EMBED_COLOR = 0x1ABC9C +EMBED_THUMBNAIL = "https://imgur.com/MJnM3LU.png" + + +async def get_ping_message(message: discord.Message, /) -> Optional[discord.Message]: + async for ping in message.channel.history(limit=1, after=message.created_at): + print(ping.content) + if ping.author.id == message._state.user.id and ping.mentions: + return ping + + +class AnnouncementCog(commands.Cog, name="Announcements"): + def __init__(self, bot: commands.Bot) -> None: + self.bot = bot + + @commands.guild_only() + @commands.group( + brief="Commands relating to the r/starcitizen Discord news system.", + aliases=["announcement", "news", "embed"], + invoke_without_command=True, + ) + async def announcements(self, ctx: commands.Context) -> None: + await ctx.send_help(ctx.command) + + @commands.check(can_publish_announcements) + @announcements.command(brief="Creates and sends a new announcement.") + async def create(self, ctx: commands.Context) -> None: + announcement_builder = AnnouncementBuilder(owner=ctx.author) + embed = await announcement_builder.get_embed(bot=self.bot) + await ctx.send(embed=embed, view=announcement_builder.view) + + @commands.check(can_publish_announcements) + @announcements.command(brief="Edits an existing announcement.") + async def edit(self, ctx: commands.Context, message: discord.Message) -> None: + announcement = await Announcement.from_message(message, bot=self.bot) + announcement_builder = AnnouncementBuilder( + edit_message=message, edit_announcement=announcement, owner=ctx.author + ) + embed = await announcement_builder.get_embed(bot=self.bot) + await ctx.send(embed=embed, view=announcement_builder.view) + + @commands.check(can_publish_announcements) + @announcements.command(brief="Deletes an announcement.") + async def delete(self, ctx: commands.Context, message: discord.Message) -> None: + if message.author != self.bot.user and not message.embeds: + await ctx.reply("That is not an announcement.") + + ping_message = await get_ping_message(message) + if ping_message: + await ping_message.delete() + + await message.delete() + await ctx.reply("Deleted that announcement. 👌") + + @commands.Cog.listener() + async def on_command_error( + self, ctx: commands.Context, error: commands.CommandError + ) -> None: + if isinstance(error, commands.MessageNotFound): + await ctx.send("Could not find that message.") + else: + raise error + + +class InvalidAnnouncementException(Exception): + pass + + +@dataclass +class Option: + id: str + name: str + directions: Optional[str] = None + row: int = 0 + is_long: bool = False + + +class Announcement: + def __init__( + self, + *, + title: str = "Announcement", + url: Optional[str] = None, + description: Optional[str] = None, + image_url: Optional[str] = None, + channel: Optional[discord.abc.Messageable] = None, + ping: Optional[discord.Role] = None, + ping_preview: Optional[str] = None, + author_id: Optional[int] = None, + is_private: bool = False, + will_notify: bool = False, + ) -> None: + self.title = title + self.url = url + self.description = description + self.image_url = image_url + self.channel = channel + self.ping = ping + self.ping_preview = ping_preview + + self.author_id = author_id + self.is_private = is_private + self.will_notify = will_notify + + async def set_option( + self, option: Option, value: Any, *, guild: discord.Guild + ) -> bool: + if option is None: + setattr(self, option.id, converted_value) + return True + + if option.id == "channel": + if value.startswith("#"): + value = value[1:] + + converted_value = discord.utils.find( + lambda c: c.name.lower() == value.lower(), guild.text_channels + ) + if not converted_value and value.isnumeric(): + converted_value = guild.get_channel(int(value)) + elif option.id == "ping": + converted_value = discord.utils.find( + lambda r: r.name.lower() == value.lower(), guild.roles + ) + if not converted_value and value.isnumeric(): + converted_value = guild.get_role(int(value)) + else: + converted_value = value + + if converted_value is None: + return False + + setattr(self, option.id, converted_value) + return True + + async def get_embed(self, *, bot: commands.Bot) -> discord.Embed: + embed = discord.Embed( + color=EMBED_COLOR, + title=self.title, + url=self.url, + description=self.description, + ) + embed.set_image(url=self.image_url) + if EMBED_THUMBNAIL: + embed.set_thumbnail(url=EMBED_THUMBNAIL) + + if not self.is_private: + if self.author_id: + author = await bot.fetch_user(self.author_id) + embed.set_footer(text=f"This post was written by {author}") + else: + embed.set_footer(text="Unknown Author") + + return embed + + @classmethod + async def from_message( + cls, message: discord.Message, /, *, bot: commands.Bot + ) -> Any: + if message.author != message.guild.me: + raise InvalidAnnouncementException("This is not a bot message.") + return + + if not len(message.embeds) == 1: + raise InvalidAnnouncementException("This message does not have any embeds.") + return + + if message.components: + raise InvalidAnnouncementException("This message has not been posted yet.") + return + + embed = message.embeds[0] + author = discord.utils.find( + lambda member: str(member) == embed.author.name, message.guild.members + ) + + ping = None + ping_preview = None + ping_message = await get_ping_message(message) + + if ping_message: + split_message = ping_message.content.split(" - ") + ping = await commands.RoleConverter.convert( + await bot.get_context(ping_message), split_message[0] + ) + + if len(split_message) == 2: + ping_preview = split_message[1] + + return cls( + title=embed.title, + url=embed.url, + description=embed.description, + image_url=embed.image.url, + channel=message.channel, + ping=ping, + ping_preview=ping_preview, + author_id=author.id if author else None, + is_private=embed.author is None, + will_notify=False, + ) + + +class AnnouncementBuilder: + def __init__( + self, + *, + edit_announcement: Optional[Announcement] = None, + edit_message: Optional[discord.Message] = None, + owner: discord.User = None, + ) -> None: + self.announcement = edit_announcement or Announcement(author_id=owner.id) + self.message = edit_message + self.edit = edit_announcement is not None + self.view = AnnouncementBuilderView(self) + self.owner = owner + + _items = self.view.children + self.view.clear_items() + + self.options: list[Option] = [] + self.add_option(Option(id="title", name="Title")) + self.add_option(Option(id="url", name="URL")) + self.add_option( + Option( + id="description", + name="Description", + directions="Here are some directions.", + is_long=True, + ) + ) + self.add_option(Option(id="image_url", name="Image")) + self.add_option(Option(id="channel", name="Channel", row=1)) + self.add_option(Option(id="ping", name="Ping", row=1)) + self.add_option(Option(id="ping_preview", name="Ping Preview", row=1)) + + for item in _items: + self.view.add_item(item) + + async def get_embed(self, bot: commands.Bot) -> discord.Embed: + return await self.announcement.get_embed(bot=bot) + + def add_option(self, option: Option, /) -> None: + button = OptionButton( + self, custom_id=option.id, label=option.name, row=option.row + ) + self.options.append(option) + self.view.add_item(button) + + +class AnnouncementBuilderView(discord.ui.View): + def __init__(self, announcement_builder: AnnouncementBuilder, /) -> None: + super().__init__(timeout=ANNOUNCEMENT_BUILDER_TIMEOUT) + self.announcement_builder = announcement_builder + + def _has_permission(self, user: discord.User) -> bool: + return user == self.announcement_builder.owner + + async def _update(self, interaction: discord.Interaction, /) -> None: + if ( + self.announcement_builder.announcement.channel + and self.announcement_builder.announcement.channel.type + is discord.ChannelType.news + ): + self.toggle_notification.disabled = False + else: + self.announcement_builder.announcement.will_notify = False + self.toggle_notification.style = discord.ButtonStyle.gray + self.toggle_notification.disabled = True + + await interaction.response.edit_message( + embed=await self.announcement_builder.get_embed(bot=interaction.client), + view=self, + allowed_mentions=discord.AllowedMentions.none(), + ) + + async def button_callback( + self, interaction: discord.Interaction, button: discord.ui.Button + ) -> None: + if not self._has_permission(interaction.user): + await interaction.response.send_message( + "You cannot use this menu.", ephemeral=True + ) + return + + await interaction.response.send_modal( + ChangeOptionModal( + announcement_builder=self.announcement_builder, + option=discord.utils.get( + self.announcement_builder.options, id=button.custom_id + ), + ) + ) + + @discord.ui.button(custom_id="private", label="Private", row=2) + async def toggle_private( + self, interaction: discord.Interaction, button: discord.ui.Button + ) -> None: + self.announcement_builder.announcement.is_private = ( + not self.announcement_builder.announcement.is_private + ) + if self.announcement_builder.announcement.is_private: + button.style = discord.ButtonStyle.green + else: + button.style = discord.ButtonStyle.gray + + await self._update(interaction) + + @discord.ui.button( + custom_id="notification", label="Published", row=2, disabled=True + ) + async def toggle_notification( + self, interaction: discord.Interaction, button: discord.ui.Button + ) -> None: + self.announcement_builder.announcement.will_notify = ( + not self.announcement_builder.announcement.will_notify + ) + if self.announcement_builder.announcement.will_notify: + button.style = discord.ButtonStyle.green + else: + button.style = discord.ButtonStyle.gray + + await self._update(interaction) + + @discord.ui.button( + custom_id="publish", label="Post", style=discord.ButtonStyle.blurple, row=2 + ) + async def publish( + self, interaction: discord.Interaction, button: discord.ui.Button + ) -> None: + if not self._has_permission(interaction.user): + await interaction.response.send_message( + "You cannot use this menu.", ephemeral=True + ) + return + + announcement = self.announcement_builder.announcement + role = announcement.ping + allowed_mentions = discord.AllowedMentions( + everyone=False, + users=False, + roles=[role] + ) if role else discord.AllowedMentions.none() + + if self.announcement_builder.edit: + await interaction.response.send_message( + "Your announcement was edited! 🎉", ephemeral=True + ) + self.stop() + + embed = await self.announcement_builder.get_embed(bot=interaction.client) + embed.remove_footer() + + await self.announcement_builder.message.edit( + content=role.mention if role else None, + embed=embed + ) + return + + if not announcement.title and announcement.channel: + await interaction.response.send_message( + "You must have a title and channel selected!", ephemeral=True + ) + return + + if announcement.ping_preview and not announcement.ping: + await interaction.response.send_message( + "You cannot have a ping preview selected without a ping!", + ephemeral=True, + ) + return + + await interaction.response.send_message( + "Your announcement was posted! 🎉", ephemeral=True + ) + self.stop() + + message = await announcement.channel.send(embed=await announcement.get_embed(bot=interaction.client),) + if announcement.will_notify: + await message.publish() + + if announcement.ping and announcement.ping_preview: + await announcement.channel.send( + f"{announcement.ping.mention} - {announcement.ping_preview}", + allowed_mentions=allowed_mentions + ) + elif announcement.ping: + await announcement.channel.send( + announcement.ping.mention, + allowed_mentions=allowed_mentions + ) + + +class ChangeOptionModal(discord.ui.Modal): + def __init__( + self, announcement_builder: AnnouncementBuilder, option: Option + ) -> None: + super().__init__(title=option.name) + self.announcement_builder = announcement_builder + self.option = option + + self.option_input = discord.ui.TextInput( + custom_id=self.option.id, + label=self.option.name, + required=self.option.id in ("title", "channel"), + placeholder=self.option.directions, + style=discord.TextStyle.long + if self.option.is_long + else discord.TextStyle.short, + ) + self.add_item(self.option_input) + + async def on_submit(self, interaction: discord.Interaction, /) -> None: + option_value = self.option_input.value + conversion_success = await self.announcement_builder.announcement.set_option( + self.option, option_value, guild=interaction.guild + ) + item = discord.utils.get( + self.announcement_builder.view.children, custom_id=self.option.id + ) + + if conversion_success and not option_value: + item.style = discord.ButtonStyle.gray + await self.announcement_builder.view._update(interaction) + elif conversion_success: + item.style = discord.ButtonStyle.green + await self.announcement_builder.view._update(interaction) + else: + await interaction.response.send_message( + "Could not find that role or channel.", ephemeral=True + ) + + +class OptionButton(discord.ui.Button): + def __init__( + self, announcement_builder: AnnouncementBuilder, *args, **kwargs + ) -> None: + super().__init__(*args, **kwargs) + self.announcement_builder = announcement_builder + + async def callback(self, interaction: discord.Interaction, /) -> None: + await self.announcement_builder.view.button_callback(interaction, self) + + +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(AnnouncementCog(bot)) diff --git a/scnewsbot/extensions/templates.py b/scnewsbot/extensions/templates.py new file mode 100644 index 0000000..88f99a0 --- /dev/null +++ b/scnewsbot/extensions/templates.py @@ -0,0 +1,103 @@ +from discord.ext import commands +import discord +from extensions.announcements import Announcement + +EMBED_COLOR = 0x1ABC9C + +TEMPLATES: dict[str, Announcement] = { + "isc": Announcement(title="Inside Star Citizen | [topic] - [subtopic]"), + "scl": Announcement(title="Star Citizen Live | [topic] - [subtopic]"), + "tracker": Announcement(title="Progress Tracker Update | [date]"), + "roundup": Announcement(title="Roadmap Roundup | [date]"), + "patchnotes": Announcement( + title="Star Citizen Alpha X.XX.X XPTU.XXXXXXX Patch Notes" + ), + "galactapedia": Announcement(title="Weekly Sneak Peek | [date]"), + "devreply": Announcement(title="Dev Reply | Topic"), + "twisc": Announcement( + title="This Week in Star Citizen | Week of [date]", + ), +} +PING_PREVIEWS = """\ +**Patch Notes** +- New Wave: `@SC Patch Notes - 3.XX Wave X Release` +- PTU Update: `@SC Patch Notes - 3.XX PTU Update` +- Live Update: `@SC Patch Notes - 3.XX LIVE Update` + +**SC News** +- ISC: `@SC News - Inside Star Citizen` +- SCL: `@SC News - Star Citizen Live` +- Progress Tracker: `@SC News - Progress Tracker Update` +- Roadmap: `@SC News - Roadmap Roundup` +- SC Monthly Report: `@SC News - Star Citizen Monthly Report` +- SQ42 Monthly Report: `@SC News - Squadron 42 Monthly Report` +- Dynamic Event: `@SC News - Event Name PU/PTU` + +**General News** +- Sneak Peek: `@General News - Weekly Sneak Peek` +- Lore Post: `@General News - Lore Post: Name` +- Dev Reply: `@General News - Dev Reply` +- Subscriber Items: `@General News - Month Subscriber Promotions` +- JP: `@General News - Jump Point` +""" +CHANNELS = """\ +Patch Notes - `585952222853201941` +SC News - `569635458183856149` +General News - `803341100618219540` +""" + + +class TemplatesCog(commands.Cog, name="Templates"): + def __init__(self, bot: commands.Bot) -> None: + self.bot = bot + self.templates = TEMPLATES + + @commands.guild_only() + @commands.group( + brief="Commands that help with designing embeds for the news system.", + invoke_without_command=True, + ) + async def templates(self, ctx: commands.Context) -> None: + await ctx.send_help(ctx.command) + + @templates.command(name="list") + async def _list(self, ctx: commands.Context) -> None: + templates = "\n".join([f"- {template}" for template in self.templates]) + await ctx.reply(f"Here are all of the available templates: ```\n{templates}```") + + @templates.command() + async def view(self, ctx: commands.Context, *, template_name: str) -> None: + templates = {name.lower(): value for name, value in self.templates.items()} + template_name = template_name.lower() + template = templates.get(template_name) + + if not template: + await ctx.reply( + f"Could not find that template. Use `{ctx.prefix}templates list` to list all available templates." + ) + + embed = await template.get_embed(bot=self.bot) + embed.remove_author() + await ctx.reply(embed=embed) + + @commands.command(brief='A shortcut to the "templates view" command.') + async def template(self, ctx: commands.Context, *, template_name: str) -> None: + await ctx.invoke( + self.bot.get_command("templates").get_command("view"), + template_name=template_name, + ) + + @commands.command(name="previews", brief="Shows all the possible ping previews.") + async def ping_previews(self, ctx: commands.Context) -> None: + await ctx.reply( + embed=discord.Embed(color=EMBED_COLOR, description=PING_PREVIEWS), + allowed_mentions=discord.AllowedMentions.none(), + ) + + @commands.command(brief="Shows all the channel IDs for announcements.") + async def channels(self, ctx: commands.Context) -> None: + await ctx.reply(embed=discord.Embed(color=EMBED_COLOR, description=CHANNELS)) + + +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(TemplatesCog(bot)) diff --git a/scnewsbot/utils.py b/scnewsbot/utils.py new file mode 100644 index 0000000..ec7ecef --- /dev/null +++ b/scnewsbot/utils.py @@ -0,0 +1,52 @@ +from discord.ext import commands + + +class Config: + def __init__(self, config: dict): + self.config = config + + @property + def debug(self) -> bool: + return self.config.get("debug", False) + + @property + def prefix(self) -> str: + return self.config["bot"].get("prefix", "sc ") + + @property + def extensions(self) -> list: + return self.config["bot"].get("extensions", ["jishaku"]) + + @property + def allowed_guilds(self) -> list: + return self._get_allowed_objects("allowed_guilds") + + @property + def allowed_roles(self) -> list: + return self._get_allowed_objects("allowed_roles") + + @property + def allowed_users(self) -> list: + return self._get_allowed_objects("allowed_users") + + def _get_allowed_objects(self, object_name, /) -> list: + allowed_objects = self.config["permissions"].get(object_name, []) + if self.debug: + allowed_objects += self.config["permissions"]["debug"].get(object_name, []) + + return allowed_objects + + +def can_publish_announcements(ctx: commands.Context) -> bool: + if not ctx.guild: + return False + + if ctx.author.id in ctx.bot.config.allowed_users: + return True + + if ctx.guild.id in ctx.bot.config.allowed_guilds: + for allowed_role in ctx.bot.config.allowed_roles: + if allowed_role in ctx.author._roles: + return True + + return False