Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 30 additions & 64 deletions .github/workflows/build-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,74 +8,40 @@ on:
required: true
type: string


jobs:
build:
name: Build & Push
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v3

# The current version (v2) of Docker's build-push action uses
# buildx, which comes with BuildKit features that help us speed
# up our builds using additional cache features. Buildx also
# has a lot of other features that are not as relevant to us.
#
# See https://github.com/docker/build-push-action

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2

- name: Login to Github Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}

# Build and push the container to the GitHub Container
# Repository. The container will be tagged as "latest"
# and with the short SHA of the commit.

- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
push: ${{ github.ref == 'refs/heads/main' }}
cache-from: type=registry,ref=ghcr.io/python-discord/bot:latest
cache-to: type=inline
tags: |
ghcr.io/python-discord/bot:latest
ghcr.io/python-discord/bot:${{ inputs.sha-tag }}
build-args: |
git_sha=${{ github.sha }}

deploy:
name: Deploy
needs: build
runs-on: ubuntu-latest
if: ${{ github.ref == 'refs/heads/main' }}
environment: production
steps:
- name: Checkout Kubernetes repository
uses: actions/checkout@v3
with:
repository: python-discord/kubernetes

- uses: azure/setup-kubectl@v3

- name: Authenticate with Kubernetes
uses: azure/k8s-set-context@v3
with:
method: kubeconfig
kubeconfig: ${{ secrets.KUBECONFIG }}

- name: Deploy to Kubernetes
uses: azure/k8s-deploy@v4
with:
manifests: |
namespaces/default/bot/deployment.yaml
images: 'ghcr.io/python-discord/bot:${{ inputs.sha-tag }}'
- name: Checkout code
uses: actions/checkout@v3

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt

- name: Run tests
run: pytest

- name: Checkout Kubernetes repository
uses: actions/checkout@v3
with:
repository: python-discord/kubernetes

- uses: azure/setup-kubectl@v3

- name: Authenticate with Kubernetes
uses: azure/k8s-set-context@v3
with:
method: kubeconfig
kubeconfig: ${{ secrets.KUBECONFIG }}

- name: Deploy to Kubernetes
uses: azure/k8s-deploy@v4
with:
manifests: |
namespaces/default/bot/deployment.yaml
images: 'ghcr.io/python-discord/bot:${{ inputs.sha-tag }}'
8 changes: 1 addition & 7 deletions bot/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,7 @@ async def _create_redis_session() -> RedisSession:

async def main() -> None:
"""Entry async method for starting the bot."""
statsd_url = constants.Stats.statsd_host
if constants.DEBUG_MODE:
# Since statsd is UDP, there are no errors for sending to a down port.
# For this reason, setting the statsd host to 127.0.0.1 for development
# will effectively disable stats.
statsd_url = LOCALHOST

statsd_url = LOCALHOST if constants.DEBUG_MODE else constants.Stats.statsd_host
allowed_roles = list({discord.Object(id_) for id_ in constants.MODERATION_ROLES})
intents = discord.Intents.all()
intents.presences = False
Expand Down
29 changes: 11 additions & 18 deletions bot/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,7 @@ class ValidDiscordServerInvite(Converter):

async def convert(self, ctx: Context, server_invite: str) -> dict:
"""Check whether the string is a valid Discord server invite."""
invite_code = DISCORD_INVITE.match(server_invite)
if invite_code:
if invite_code := DISCORD_INVITE.match(server_invite):
response = await ctx.bot.http_session.get(
f"{URLs.discord_invite_api}/{invite_code.group('invite')}"
)
Expand All @@ -78,7 +77,7 @@ class Extension(Converter):
async def convert(self, ctx: Context, argument: str) -> str:
"""Fully qualify the name of an extension and ensure it exists."""
# Special values to reload all extensions
if argument == "*" or argument == "**":
if argument in {"*", "**"}:
return argument

argument = argument.lower()
Expand All @@ -89,11 +88,11 @@ async def convert(self, ctx: Context, argument: str) -> str:
if (qualified_arg := f"{exts.__name__}.{argument}") in bot_instance.all_extensions:
return qualified_arg

matches = []
for ext in bot_instance.all_extensions:
if argument == unqualify(ext):
matches.append(ext)

matches = [
ext
for ext in bot_instance.all_extensions
if argument == unqualify(ext)
]
if len(matches) > 1:
matches.sort()
names = "\n".join(matches)
Expand Down Expand Up @@ -229,12 +228,10 @@ async def convert(ctx: Context, argument: str) -> SourceType:
if argument.lower() == "help":
return ctx.bot.help_command

cog = ctx.bot.get_cog(argument)
if cog:
if cog := ctx.bot.get_cog(argument):
return cog

cmd = ctx.bot.get_command(argument)
if cmd:
if cmd := ctx.bot.get_command(argument):
return cmd

tags_cog = ctx.bot.get_cog("Tags")
Expand Down Expand Up @@ -386,11 +383,7 @@ async def convert(self, ctx: Context, datetime_string: str) -> datetime:
except ValueError:
raise BadArgument(f"`{datetime_string}` is not a valid ISO-8601 datetime string")

if dt.tzinfo:
dt = dt.astimezone(UTC)
else: # Without a timezone, assume it represents UTC.
dt = dt.replace(tzinfo=UTC)

dt = dt.astimezone(UTC) if dt.tzinfo else dt.replace(tzinfo=UTC)
return dt


Expand Down Expand Up @@ -477,7 +470,7 @@ class Infraction(Converter):

async def convert(self, ctx: Context, arg: str) -> dict | None:
"""Attempts to convert `arg` into an infraction `dict`."""
if arg in ("l", "last", "recent"):
if arg in {"l", "last", "recent"}:
params = {
"actor__id": ctx.author.id,
"ordering": "-inserted_at"
Expand Down
16 changes: 7 additions & 9 deletions bot/exts/backend/branding/_cog.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,7 @@ def extract_event_duration(event: Event) -> str:
start_date = event.meta.start_date.strftime(fmt)
end_date = event.meta.end_date.strftime(fmt)

if start_date == end_date:
return start_date

return f"{start_date} - {end_date}"
return start_date if start_date == end_date else f"{start_date} - {end_date}"


def extract_event_name(event: Event) -> str:
Expand Down Expand Up @@ -525,13 +522,14 @@ async def branding_sync_cmd(self, ctx: commands.Context) -> None:
async with ctx.typing():
banner_success, icon_success = await self.synchronise()

failed_assets = ", ".join(
if failed_assets := ", ".join(
name
for name, status in [("banner", banner_success), ("icon", icon_success)]
for name, status in [
("banner", banner_success),
("icon", icon_success),
]
if status is False
)

if failed_assets:
):
resp = make_embed("Synchronisation unsuccessful", f"Failed to apply: {failed_assets}.", success=False)
resp.set_footer(text="Check log for details.")
else:
Expand Down
10 changes: 5 additions & 5 deletions bot/exts/backend/branding/_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@ class RemoteObject:

def __init__(self, dictionary: dict[str, t.Any]) -> None:
"""Initialize by grabbing annotated attributes from `dictionary`."""
missing_keys = self.__annotations__.keys() - dictionary.keys()
if missing_keys:
if missing_keys := self.__annotations__.keys() - dictionary.keys():
raise KeyError(f"Fetched object lacks expected keys: {missing_keys}")
for annotation in self.__annotations__:
setattr(self, annotation, dictionary[annotation])
Expand Down Expand Up @@ -163,9 +162,10 @@ async def construct_event(self, directory: RemoteObject) -> Event:
"""
contents = await self.fetch_directory(directory.path)

missing_assets = {"meta.md", "server_icons", "banners"} - contents.keys()

if missing_assets:
if (
missing_assets := {"meta.md", "server_icons", "banners"}
- contents.keys()
):
raise BrandingMisconfigurationError(f"Directory is missing following assets: {missing_assets}")

server_icons = await self.fetch_directory(contents["server_icons"].path, types=("file",))
Expand Down
9 changes: 4 additions & 5 deletions bot/exts/backend/config_verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,11 @@ async def cog_load(self) -> None:
server = self.bot.get_guild(constants.Guild.id)

server_channel_ids = {channel.id for channel in server.channels}
invalid_channels = [
(channel_name, channel_id) for channel_name, channel_id in constants.Channels
if invalid_channels := [
(channel_name, channel_id)
for channel_name, channel_id in constants.Channels
if channel_id not in server_channel_ids
]

if invalid_channels:
]:
log.warning(f"Configured channels do not exist in server: {invalid_channels}.")


Expand Down
4 changes: 2 additions & 2 deletions bot/exts/backend/error_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ async def try_get_tag(self, ctx: Context) -> None:
if await tags_get_command(ctx, maybe_tag_name):
return

if not any(role.id in MODERATION_ROLES for role in ctx.author.roles):
if all(role.id not in MODERATION_ROLES for role in ctx.author.roles):
await self.send_command_suggestion(ctx, maybe_tag_name)
except Exception as err:
log.debug("Error while attempting to invoke tag fallback.")
Expand All @@ -206,7 +206,7 @@ async def try_run_fixed_codeblock(self, ctx: Context) -> bool:
msg = copy.copy(ctx.message)

command, sep, end = msg.content.partition("```")
msg.content = command + " " + sep + end
msg.content = f"{command} {sep}{end}"
new_ctx = await self.bot.get_context(msg)

if new_ctx.command is None:
Expand Down
5 changes: 1 addition & 4 deletions bot/exts/backend/sync/_syncers.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,7 @@ async def sync(cls, guild: Guild, ctx: Context | None = None) -> None:
"""
log.info(f"Starting {cls.name} syncer.")

if ctx:
message = await ctx.send(f"📊 Synchronising {cls.name}s.")
else:
message = None
message = await ctx.send(f"📊 Synchronising {cls.name}s.") if ctx else None
diff = await cls._get_diff(guild)

try:
Expand Down
2 changes: 1 addition & 1 deletion bot/exts/filtering/_filter_lists/antispam.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class AntispamList(UniquesListBase):

def __init__(self, filtering_cog: "Filtering"):
super().__init__(filtering_cog)
self.message_deletion_queue: dict[Member, DeletionContext] = dict()
self.message_deletion_queue: dict[Member, DeletionContext] = {}

def get_filter_type(self, content: str) -> type[UniqueFilter] | None:
"""Get a subclass of filter matching the filter list and the filter's content."""
Expand Down
40 changes: 17 additions & 23 deletions bot/exts/filtering/_filter_lists/filter_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,19 +95,18 @@ async def _create_filter_list_result(

relevant_filters = []
for filter_ in filters:
if not filter_.validations:
if default_answer and await filter_.triggered_on(ctx):
relevant_filters.append(filter_)
else:
if filter_.validations:
passed, failed = filter_.validations.evaluate(ctx)
if not failed and failed_by_default < passed:
if await filter_.triggered_on(ctx):
relevant_filters.append(filter_)

elif default_answer and await filter_.triggered_on(ctx):
relevant_filters.append(filter_)
if ctx.event == Event.MESSAGE_EDIT and ctx.message and self.list_type == ListType.DENY:
previously_triggered = ctx.message_cache.get_message_metadata(ctx.message.id)
# The message might not be cached.
if previously_triggered:
if previously_triggered := ctx.message_cache.get_message_metadata(
ctx.message.id
):
ignore_filters = previously_triggered[self]
# This updates the cache. Some filters are ignored, but they're necessary if there's another edit.
previously_triggered[self] = relevant_filters
Expand All @@ -120,8 +119,8 @@ def default(self, setting_name: str) -> Any:
value = self.defaults.actions.get_setting(setting_name, missing)
if value is missing:
value = self.defaults.validations.get_setting(setting_name, missing)
if value is missing:
raise ValueError(f"Couldn't find a setting named {setting_name!r}.")
if value is missing:
raise ValueError(f"Couldn't find a setting named {setting_name!r}.")
return value

def merge_actions(self, filters: list[Filter]) -> ActionSettings | None:
Expand All @@ -144,14 +143,12 @@ def merge_actions(self, filters: list[Filter]) -> ActionSettings | None:
@staticmethod
def format_messages(triggers: list[Filter], *, expand_single_filter: bool = True) -> list[str]:
"""Convert the filters into strings that can be added to the alert embed."""
if len(triggers) == 1 and expand_single_filter:
message = f"#{triggers[0].id} (`{triggers[0].content}`)"
if triggers[0].description:
message += f" - {triggers[0].description}"
messages = [message]
else:
messages = [f"{filter_.id} (`{filter_.content}`)" for filter_ in triggers]
return messages
if len(triggers) != 1 or not expand_single_filter:
return [f"{filter_.id} (`{filter_.content}`)" for filter_ in triggers]
message = f"#{triggers[0].id} (`{triggers[0].content}`)"
if triggers[0].description:
message += f" - {triggers[0].description}"
return [message]

def __hash__(self):
return hash(id(self))
Expand All @@ -177,8 +174,7 @@ def add_list(self, list_data: dict) -> AtomicList:

filters = {}
for filter_data in list_data["filters"]:
new_filter = self._create_filter(filter_data, defaults)
if new_filter:
if new_filter := self._create_filter(filter_data, defaults):
filters[filter_data["id"]] = new_filter

self[list_type] = AtomicList(
Expand Down Expand Up @@ -218,8 +214,7 @@ def _create_filter(self, filter_data: dict, defaults: Defaults) -> T | None:
"""Create a filter from the given data."""
try:
content = filter_data["content"]
filter_type = self.get_filter_type(content)
if filter_type:
if filter_type := self.get_filter_type(content):
return filter_type(filter_data, defaults)
if content not in self._already_warned:
log.warning(f"A filter named {content} was supplied, but no matching implementation found.")
Expand Down Expand Up @@ -292,8 +287,7 @@ def add_list(self, list_data: dict) -> SubscribingAtomicList:
filters = {}
events = set()
for filter_data in list_data["filters"]:
new_filter = self._create_filter(filter_data, defaults)
if new_filter:
if new_filter := self._create_filter(filter_data, defaults):
new_list.subscribe(new_filter, *new_filter.events)
filters[filter_data["id"]] = new_filter
self.loaded_types[new_filter.name] = type(new_filter)
Expand Down
Loading