[Added] Spielverabredungen für Tabletop Sachsen

This commit is contained in:
Paul Glaß 2026-01-02 23:19:24 +01:00
parent 85207037ba
commit b9703ce354
3 changed files with 131 additions and 3 deletions

5
.gitignore vendored
View File

@ -1,4 +1,5 @@
.env
.venv
**/fregbot-birthday.log
.idea/**
**/*.log
.idea/**
uv.lock

View File

@ -9,8 +9,12 @@ dependencies = [
"discord.py",
"pydantic",
]
[project.optional-dependencies]
spielverabredungen = [
"click",
]
[tool.setuptools_scm]
[project.scripts]
fregbot-birthday = "birthday:main"
fregbot-birthday = "birthday:main"

123
src/spielverabredungen.py Normal file
View File

@ -0,0 +1,123 @@
import asyncio
import datetime
import locale
import logging
from datetime import timedelta
from typing import Iterable
import click
import discord
from discord import Client, ForumChannel
from pydantic import BaseModel, PositiveInt
class SpielverabredungenConfig(BaseModel):
guild_id: int
channel_id: int
start: datetime.datetime
end: datetime.datetime
go_back_threads_days: PositiveInt
@property
def dates(self) -> Iterable[datetime.date]:
start, end = self.start.date(), self.end.date()
assert start <= end, f'Start date {self.start} must be smaller than end date {self.end}'
curr_date = start
while curr_date <= end:
yield curr_date
curr_date += timedelta(days=1)
class SpielverabredungenClient(Client):
config_object: SpielverabredungenConfig = None
def __get_channel(self) -> ForumChannel:
guild = self.get_guild(self.config_object.guild_id)
assert guild, f'Guild with ID {self.config_object.guild_id} not found'
channel = discord.utils.get(guild.channels, id=self.config_object.channel_id)
assert channel, f'Channel with ID {self.config_object.channel_id} not found in guild {self.config_object.guild_id}'
assert isinstance(channel,
ForumChannel), f'Channel with ID {self.config_object.channel_id} is not a forum channel'
return channel
def __check_duplicate_channels(self, channel: ForumChannel, dates: Iterable[datetime.date],
thread_lookback_days: int) -> Iterable[datetime.date]:
skipped_dates = 0
potential_threadnames = {self.__get_thread_name(date): date for date in dates}
max_lookback_date = datetime.date.today() - datetime.timedelta(days=thread_lookback_days)
for thread in channel.threads:
if thread.locked:
continue
if thread.created_at.date() < max_lookback_date:
break
if thread.name in potential_threadnames:
del potential_threadnames[thread.name]
skipped_dates += 1
logging.info('Skipping %d days from posting, as the threads were already found', skipped_dates)
yield from potential_threadnames.values()
@staticmethod
def __get_thread_name(date: datetime.date) -> str:
return date.strftime('%Y-%m-%d %A')
async def __create_thread(self, channel: ForumChannel, date: datetime.date) -> bool:
name = self.__get_thread_name(date)
thread = await channel.create_thread(name=name,
content="Tragt hier eure Spielverabredungen ein für oben genanntes Datum.",
reason='Spielverabredungen - Erstellt von Bot')
return thread is not None
async def on_ready(self):
logging.info(f'Logging in as {self.user}')
try:
channel = self.__get_channel()
logging.info('Channel obtained')
dates = self.__check_duplicate_channels(channel, self.config_object.dates,
self.config_object.go_back_threads_days)
for index, date in enumerate(dates, start=1):
if index % 10 == 0:
logging.info('Attempting Thread creation number %d for %s', index, date)
if not await self.__create_thread(channel, date):
logging.error('Could not create thread for %s', date)
await asyncio.sleep(1)
except Exception as e:
logging.error(f'Error: {e}')
finally:
await self.close()
@click.command()
@click.option('--guild', '-g', type=int, help='ID of the guild (server) to post in', default=812043029967536188,
show_default=True)
@click.option('--channel', '-c', type=int, help='ID of the channel to post threads in', default=1389312271397818509,
show_default=True)
@click.option('--start', '-s', type=click.DateTime(), help='Date to start the threads for. Defaults to today',
default=datetime.datetime.today(), show_default=True)
@click.option('--end', '-e', type=click.DateTime(), help='Date to end the threads for. Defaults to today + 90 days',
default=datetime.datetime.today() + datetime.timedelta(days=90))
@click.option('--thread-lookback', type=int, default=180, help='How many days to look back for duplicate threads',
show_default=True)
@click.option('--token', envvar='SPIELVERABREDUNGEN_TOKEN', help='Token to authenticate the bot', required=True)
@click.option('--debug', is_flag=True, help='Write debug messages to log')
def main(guild: int, channel: int, start: datetime.datetime, end: datetime.datetime, token: str,
thread_lookback: int, debug: bool) -> None:
intents = discord.Intents.default()
intents.messages = True
log_handler = logging.FileHandler(filename='spielabredungen.log', encoding='utf-8', mode='w')
if debug:
log_handler.setLevel(logging.DEBUG)
else:
log_handler.setLevel(logging.INFO)
locale.setlocale(locale.LC_ALL, 'de_DE.UTF-8')
logging.debug('Setting locale to "de_DE.UTF-8"')
client = SpielverabredungenClient(intents=intents)
client.config_object = SpielverabredungenConfig(guild_id=guild, channel_id=channel, start=start, end=end,
go_back_threads_days=thread_lookback)
client.run(token, log_handler=log_handler, root_logger=True)
if __name__ == '__main__':
main()