Skip to content
Open
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
16 changes: 15 additions & 1 deletion prod-config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@ pretix_cache_file = "pretix_cache.json"
# speakers
"Presenter" = ["Participants", "Onsite Participants", "Speakers"]
# onsite volunteers
"On-site Volunteer" = ["Participants", "Onsite Participants", "Volunteers", "Onsite Volunteers"]
"On-site Volunteer" = [
"Participants",
"Onsite Participants",
"Volunteers",
"Onsite Volunteers",
]
# beginners' day
"Beginners’ Day Unconference / Humble Data Access" = ["Beginners Day"]

Expand Down Expand Up @@ -60,3 +65,12 @@ main_notification_channel_name = "programme-notifications"

[guild_statistics]
required_role = "Organizers"

[dog]
# this section is optional; defaults are used if omitted
error_messages = [
"The dogs are on strike today! Try again later. 🐾🪧",
"A wild error appeared! The dog got away... 🐕💨",
"Dog API is fetching a stick. Throw it again! 🦴",
"404: Dog not found. Have you checked under the couch? 🛋️",
]
4 changes: 4 additions & 0 deletions src/europython_discord/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

from europython_discord.cogs.guild_statistics import GuildStatisticsCog, GuildStatisticsConfig
from europython_discord.cogs.ping import PingCog
from europython_discord.dog.cog import DogCog
from europython_discord.dog.config import DogConfig
from europython_discord.program_notifications.cog import ProgramNotificationsCog
from europython_discord.program_notifications.config import ProgramNotificationsConfig
from europython_discord.registration.cog import RegistrationCog
Expand All @@ -32,6 +34,7 @@ class Config(BaseModel):
registration: RegistrationConfig
program_notifications: ProgramNotificationsConfig
guild_statistics: GuildStatisticsConfig
dog: DogConfig = DogConfig()


async def run_bot(config: Config, auth_token: str) -> None:
Expand All @@ -44,6 +47,7 @@ async def run_bot(config: Config, auth_token: str) -> None:

async with commands.Bot(intents=intents, command_prefix="$") as bot:
await bot.add_cog(PingCog(bot))
await bot.add_cog(DogCog(bot, config.dog))
await bot.add_cog(RegistrationCog(bot, config.registration))
await bot.add_cog(ProgramNotificationsCog(bot, config.program_notifications))
await bot.add_cog(GuildStatisticsCog(bot, config.guild_statistics))
Expand Down
Empty file.
37 changes: 37 additions & 0 deletions src/europython_discord/dog/cog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from __future__ import annotations

import logging
import random

import discord
from discord.ext import commands

from europython_discord.dog.config import DogConfig
from europython_discord.dog.dogclient import DogClient

_logger = logging.getLogger(__name__)


class DogCog(commands.Cog):
def __init__(
self,
bot: commands.Bot,
config: DogConfig,
client: DogClient | None = None,
) -> None:
self.bot = bot
self.config = config
self._client = client or DogClient()
_logger.info("Cog 'Dog' has been initialized")

@commands.hybrid_command(name="dog", description="Get a random dog picture")
async def dog_command(self, ctx: commands.Context) -> None:
image_url = await self._client.fetch_random_dog()
if image_url is None:
message = random.choice(self.config.error_messages) # noqa: S311
await ctx.send(message)
return

embed = discord.Embed()
embed.set_image(url=image_url)
await ctx.send(embed=embed)
5 changes: 5 additions & 0 deletions src/europython_discord/dog/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from pydantic import BaseModel


class DogConfig(BaseModel):
error_messages: list[str] = ["404: Dog not found. Have you checked under the couch? 🛋️"]
28 changes: 28 additions & 0 deletions src/europython_discord/dog/dogclient.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from __future__ import annotations

import logging

import aiohttp

_logger = logging.getLogger(__name__)

DOG_API_URL = "https://dog.ceo/api/breeds/image/random"


class DogClient:
def __init__(self) -> None:
self._session = aiohttp.ClientSession()

async def close(self) -> None:
await self._session.close()

async def fetch_random_dog(self) -> str | None:
try:
async with self._session.get(DOG_API_URL) as response:
response.raise_for_status()
data = await response.json()
except Exception:
_logger.exception("Failed to fetch dog image")
return None

return data["message"]
15 changes: 14 additions & 1 deletion test-config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@ pretix_cache_file = "pretix_cache.json"
# speakers
"Presenter" = ["Participants", "Onsite Participants", "Speakers"]
# onsite volunteers
"On-site Volunteer" = ["Participants", "Onsite Participants", "Volunteers", "Onsite Volunteers"]
"On-site Volunteer" = [
"Participants",
"Onsite Participants",
"Volunteers",
"Onsite Volunteers",
]
# beginners' day
"Beginners’ Day Unconference / Humble Data Access" = ["Beginners Day"]

Expand Down Expand Up @@ -60,3 +65,11 @@ fast_mode = true

[guild_statistics]
required_role = "Organizers"

[dog]
error_messages = [
"The dogs are on strike today! Try again later. 🐾🪧",
"A wild error appeared! The dog got away... 🐕💨",
"Dog API is fetching a stick. Throw it again! 🦴",
"404: Dog not found. Have you checked under the couch? 🛋️",
]
Empty file added tests/dog/__init__.py
Empty file.
61 changes: 61 additions & 0 deletions tests/dog/test_cog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from unittest.mock import AsyncMock, MagicMock

import pytest
from discord.ext import commands

from europython_discord.dog.cog import DogCog
from europython_discord.dog.config import DogConfig
from europython_discord.dog.dogclient import DogClient


@pytest.fixture
def config() -> DogConfig:
return DogConfig()


@pytest.fixture
def bot() -> MagicMock:
return MagicMock(spec=commands.Bot)


@pytest.fixture
def dog_url() -> str:
return "https://images.dog.ceo/dog.jpg"


@pytest.fixture
def mock_client(dog_url: str) -> DogClient:
client = MagicMock(spec=DogClient)
client.fetch_random_dog.return_value = dog_url
return client


@pytest.fixture
def cog(bot: MagicMock, config: DogConfig, mock_client: DogClient) -> DogCog:
return DogCog(bot, config, client=mock_client)


@pytest.fixture
def ctx() -> AsyncMock:
mock = AsyncMock(spec=commands.Context)
mock.send = AsyncMock()
return mock


async def test_dog_command_success(cog: DogCog, ctx: AsyncMock, dog_url: str) -> None:
await cog.dog_command.callback(cog, ctx)

ctx.send.assert_awaited_once()
embed = ctx.send.call_args.kwargs["embed"]
assert embed.image.url == dog_url


async def test_dog_command_api_error(cog: DogCog, ctx: AsyncMock, mock_client: DogClient) -> None:
mock_client.fetch_random_dog.return_value = None

await cog.dog_command.callback(cog, ctx)

ctx.send.assert_awaited_once()
text = ctx.send.call_args.args[0]

assert text in cog.config.error_messages