mirror of
https://github.com/hencaric/SCNewsBot.git
synced 2026-08-16 22:46:03 +01:00
initial commit
This commit is contained in:
10
.github/workflows/black.yml
vendored
Normal file
10
.github/workflows/black.yml
vendored
Normal file
@@ -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
|
||||
132
.gitignore
vendored
Normal file
132
.gitignore
vendored
Normal file
@@ -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/
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -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.
|
||||
2
README.md
Normal file
2
README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# scnewsbot
|
||||
A bot for the r/starcitizen Discord server.
|
||||
22
example.config.toml
Normal file
22
example.config.toml
Normal file
@@ -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 = []
|
||||
23
pyproject.toml
Normal file
23
pyproject.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
[tool.poetry]
|
||||
name = "scnewsbot"
|
||||
version = "0.0.0"
|
||||
description = "A bot for the r/starcitizen Discord server."
|
||||
authors = ["mudkipdev <mudkip@mudkip.dev>"]
|
||||
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"
|
||||
0
scnewsbot/__init__.py
Normal file
0
scnewsbot/__init__.py
Normal file
26
scnewsbot/__main__.py
Normal file
26
scnewsbot/__main__.py
Normal file
@@ -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()
|
||||
22
scnewsbot/bot.py
Normal file
22
scnewsbot/bot.py
Normal file
@@ -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)
|
||||
455
scnewsbot/extensions/announcements.py
Normal file
455
scnewsbot/extensions/announcements.py
Normal file
@@ -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))
|
||||
103
scnewsbot/extensions/templates.py
Normal file
103
scnewsbot/extensions/templates.py
Normal file
@@ -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))
|
||||
52
scnewsbot/utils.py
Normal file
52
scnewsbot/utils.py
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user