5 Commits
Author SHA1 Message Date
imcompiling bbacaa8bb9 Fix birthday loop.
build_develop / build (push) Failing after 17s
2025-03-17 17:51:28 -04:00
imcompiling eef85b424d Add pytz to project. 2025-03-17 17:51:18 -04:00
imcompiling 3a4ac066a2 Add database code. Starting with birthday table. 2025-03-17 17:40:21 -04:00
imcompiling 7fd3fb8458 Fix some smells. 2025-03-17 17:40:07 -04:00
imcompiling dae0d205e5 Add the cakeday loop and some of birthday functionality. 2025-03-17 17:29:47 -04:00
11 changed files with 169 additions and 6 deletions
+5 -1
View File
@@ -3,5 +3,9 @@
"reportOptionalMemberAccess": "warning", "reportOptionalMemberAccess": "warning",
"reportArgumentType": "information", "reportArgumentType": "information",
"reportAttributeAccessIssue": "warning" "reportAttributeAccessIssue": "warning"
} },
"cSpell.words": [
"cakeday",
"cakedays"
]
} }
+10
View File
@@ -0,0 +1,10 @@
"""Initializing code for all loops."""
from nextcord.ext import commands
from cogs.loops.daily_loop import DailyLoop
def setup(bot: commands.Bot) -> None:
"""Add functions to bot."""
bot.add_cog(DailyLoop(bot))
+81
View File
@@ -0,0 +1,81 @@
"""Defines the daily loop that runs the Off Cooldown bot functions."""
from datetime import UTC, datetime, time
import nextcord
from nextcord.ext import commands, tasks
from pytz import timezone
from sqlalchemy import select
from sqlalchemy.orm import Session
from sqlmodel import col
from database.db import engine
from database.models import UserBirthday
from logger import ocd_logger
from main import birthday_list
from settings import settings
class DailyLoop(commands.Cog, name="Daily Loop"):
"""Class for performing daily functions."""
def __init__(self, bot: commands.Bot) -> None:
self.bot = bot
self.current_election = None
self.update_birthdays()
self.daily_loop.start()
@tasks.loop(time=time(hour=0, tzinfo=timezone("UTC")))
async def daily_loop(self) -> None:
"""Run daily repeated functions."""
self.update_birthdays()
await self.cakedays()
@daily_loop.before_loop
async def before_loop(self) -> None:
"""Actions run before the loop starts. Want to wait for bot to be ready and get the current cycle."""
await self.bot.wait_until_ready()
def update_birthdays(self) -> None:
"""Goes through database to collect all birthdays that are today. Adds user's id to birthday list."""
birthday_list.clear()
today = datetime.now(tz=UTC).date()
stmt = (
select(col(UserBirthday.user_id))
.where(col(UserBirthday.month) == today.month)
.where(col(UserBirthday.day) == today.day)
)
with Session(engine) as session:
to_add = session.scalars(stmt)
birthday_list.extend(to_add.all())
async def cakedays(self) -> None:
"""
Loops through the guild's humans checking for joined_at dates that match today's date.
Sends a message saying happy cakeday.
"""
today = datetime.now(tz=UTC).date()
if (guild := self.bot.get_guild(settings.params.guild_id)) is None:
ocd_logger.warning("Guild not found in bot.")
return
if (announce_channel := guild.get_channel(settings.channel_ids.general)) is None or not isinstance(
announce_channel, nextcord.TextChannel
):
ocd_logger.warning("Set general channel isn't found or isn't a text channel.")
return
cakedays: list[nextcord.Member] = [
human
for human in guild.humans
if human.joined_at is not None and human.joined_at.month == today.month and human.joined_at.day == today.day
]
for member in cakedays:
years = today.year - member.joined_at.year # type: ignore reportOptionalMemberAccess
year_str = "years" if years > 1 else "year"
msg = f"Happy Cake day! <@{member.id}> joined the discord {years} {year_str} ago!"
await announce_channel.send(msg)
+1
View File
@@ -0,0 +1 @@
"""Package for database interaction."""
+11
View File
@@ -0,0 +1,11 @@
"""Simple file to set up the sqlmodel connection to database."""
from sqlmodel import SQLModel, create_engine
from . import models # noqa: F401 - Need this import for creating the models.
url = "sqlite:///database.db"
engine = create_engine(url)
# Don't need to create all when connected to prod.
SQLModel.metadata.create_all(engine)
+20
View File
@@ -0,0 +1,20 @@
"""File holds all model definitions of SQL tables."""
from sqlalchemy.dialects.mysql import BIGINT, SMALLINT
from sqlmodel import Column, Field, SQLModel
class UserBirthday(SQLModel, table=True):
"""
Represents a user's birthday.
Attributes:
user_id: The user's discord id. Primary key.
month: The month the user was born.
day: The day the user was born.
"""
user_id: int = Field(sa_column=Column(BIGINT(unsigned=True), primary_key=True, nullable=False))
month: int = Field(sa_column=Column(SMALLINT(unsigned=True), nullable=False))
day: int = Field(sa_column=Column(SMALLINT(unsigned=True), nullable=False))
+10
View File
@@ -0,0 +1,10 @@
"""Creates logger for Agon bot."""
import logging
import sys
ocd_logger = logging.getLogger("default")
ocd_logger.setLevel(logging.INFO)
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(name)s: %(message)s"))
ocd_logger.addHandler(handler)
+10 -5
View File
@@ -1,11 +1,14 @@
"""Main file for bot.""" """Main file for bot."""
from __future__ import annotations
import logging import logging
import os import pathlib
import nextcord import nextcord
from nextcord.ext import commands from nextcord.ext import commands
from logger import ocd_logger
from settings import settings from settings import settings
nextcord_logger = logging.getLogger("nextcord") nextcord_logger = logging.getLogger("nextcord")
@@ -14,6 +17,8 @@ nextcord_handler = logging.FileHandler(filename="nextcord.log", encoding="utf-8"
nextcord_handler.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(name)s: %(message)s")) nextcord_handler.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(name)s: %(message)s"))
nextcord_logger.addHandler(nextcord_handler) nextcord_logger.addHandler(nextcord_handler)
birthday_list: list[int] = []
def main() -> None: def main() -> None:
"""Set config, intents, then connects the bot to discord.""" """Set config, intents, then connects the bot to discord."""
@@ -28,21 +33,21 @@ def main() -> None:
# intents.guilds = True # intents.guilds = True
# To enable the members privileged intent: # To enable the members privileged intent:
# intents.members = True intents.members = True
bot = commands.Bot(intents=intents) bot = commands.Bot(intents=intents)
# Get the modules of all cogs whose directory structure is ./cogs/<module_name> # Get the modules of all cogs whose directory structure is ./cogs/<module_name>
for folder in os.listdir("cogs"): for folder in pathlib.Path("cogs").iterdir():
bot.load_extension(f"cogs.{folder}") bot.load_extension(f"cogs.{folder}")
@bot.listen() @bot.listen()
async def on_ready() -> None: async def on_ready() -> None:
"""When Discord is connected.""" """When Discord is connected."""
if bot.user is None: if bot.user is None:
print("Discord connection failed.") ocd_logger.critical("Discord connection failed.")
return return
print(f"{bot.user.name} has connected to Discord!") ocd_logger.info(f"{bot.user.name} has connected to Discord!")
# Run Discord bot # Run Discord bot
bot.run(settings.params.discord_token) bot.run(settings.params.discord_token)
+1
View File
@@ -9,6 +9,7 @@ dependencies = [
"python-dotenv>=1.0.1", "python-dotenv>=1.0.1",
"sqlmodel>=0.0.22", "sqlmodel>=0.0.22",
"dataclass-wizard[yaml]>=0.22.3,<1.0.0", "dataclass-wizard[yaml]>=0.22.3,<1.0.0",
"pytz>=2025.1",
] ]
requires-python = ">=3.12" requires-python = ">=3.12"
+9
View File
@@ -7,17 +7,26 @@ from dataclass_wizard import YAMLWizard
CONFIG_LOCATION = "conf/config.yaml" CONFIG_LOCATION = "conf/config.yaml"
@dataclass
class ChannelIds:
"""Class holds all channel ids needed by bot."""
general: int
@dataclass @dataclass
class Params: class Params:
"""Class defines start parameters that shouldn't be changed.""" """Class defines start parameters that shouldn't be changed."""
discord_token: str discord_token: str
guild_id: int
@dataclass @dataclass
class Settings(YAMLWizard): class Settings(YAMLWizard):
"""Class defines all configs required for off-cooldown-bot.""" """Class defines all configs required for off-cooldown-bot."""
channel_ids: ChannelIds
params: Params params: Params
def save(self) -> None: def save(self) -> None:
Generated
+11
View File
@@ -307,6 +307,7 @@ dependencies = [
{ name = "dataclass-wizard", extra = ["yaml"] }, { name = "dataclass-wizard", extra = ["yaml"] },
{ name = "nextcord" }, { name = "nextcord" },
{ name = "python-dotenv" }, { name = "python-dotenv" },
{ name = "pytz" },
{ name = "sqlmodel" }, { name = "sqlmodel" },
] ]
@@ -320,6 +321,7 @@ requires-dist = [
{ name = "dataclass-wizard", extras = ["yaml"], specifier = ">=0.22.3,<1.0.0" }, { name = "dataclass-wizard", extras = ["yaml"], specifier = ">=0.22.3,<1.0.0" },
{ name = "nextcord", specifier = ">=2.6.0" }, { name = "nextcord", specifier = ">=2.6.0" },
{ name = "python-dotenv", specifier = ">=1.0.1" }, { name = "python-dotenv", specifier = ">=1.0.1" },
{ name = "pytz", specifier = ">=2025.1" },
{ name = "sqlmodel", specifier = ">=0.0.22" }, { name = "sqlmodel", specifier = ">=0.0.22" },
] ]
@@ -445,6 +447,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 },
] ]
[[package]]
name = "pytz"
version = "2025.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5f/57/df1c9157c8d5a05117e455d66fd7cf6dbc46974f832b1058ed4856785d8a/pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e", size = 319617 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/eb/38/ac33370d784287baa1c3d538978b5e2ea064d4c1b93ffbd12826c190dd10/pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57", size = 507930 },
]
[[package]] [[package]]
name = "pyyaml" name = "pyyaml"
version = "6.0.2" version = "6.0.2"