diff --git a/.github/workflows/deploy_pydata.yml b/.github/workflows/deploy_pydata.yml new file mode 100644 index 00000000..44f0c62e --- /dev/null +++ b/.github/workflows/deploy_pydata.yml @@ -0,0 +1,31 @@ +name: Deploy to server + +on: + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Deploy to some Server + uses: easingthemes/ssh-deploy@v4 + with: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY_DISCORD }} + SOURCE: "." + REMOTE_HOST: "136.243.153.146" + REMOTE_USER: "root" + TARGET: "/home/discord/" + EXCLUDE: "/dist/, /node_modules/" + + SCRIPT_BEFORE: | + whoami + ls -al + SCRIPT_AFTER: | + echo $RSYNC_STDOUT + cd /root/discord + docker compose down + docker compose up --detach --build diff --git a/.python-version b/.python-version new file mode 100644 index 00000000..0c7d5f5f --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.11.4 diff --git a/EuroPythonBot/bot.py b/EuroPythonBot/bot.py index e2ce2eff..8a6ea260 100644 --- a/EuroPythonBot/bot.py +++ b/EuroPythonBot/bot.py @@ -10,8 +10,10 @@ import configuration from cogs.ping import Ping -from cogs.registration import Registration -from helpers.pretix_connector import PretixOrder +# from cogs.registration import Registration +from cogs.registration_pydata import RegistrationPyData +# from helpers.pretix_connector import PretixOrder +from helpers.tito_connector import TitoOrder load_dotenv(Path(__file__).resolve().parent.parent / ".secrets") DISCORD_BOT_TOKEN = os.getenv("DISCORD_BOT_TOKEN") @@ -45,7 +47,7 @@ async def load_extension(self, name: str, *, package: str | None = None) -> None def _setup_logging() -> None: """Set up a basic logging configuration.""" - config = configuration.Config() + config = configuration.Config(testing=True) # Create a stream handler that logs to stdout (12-factor app) stream_handler = logging.StreamHandler(stream=sys.stdout) @@ -74,15 +76,16 @@ async def main(): _setup_logging() async with bot: await bot.add_cog(Ping(bot)) - await bot.add_cog(Registration(bot)) - await bot.load_extension("extensions.programme_notifications") - await bot.load_extension("extensions.organisers") + await bot.add_cog(RegistrationPyData(bot)) + # await bot.load_extension("extensions.programme_notifications") + # await bot.load_extension("extensions.organisers") await bot.start(DISCORD_BOT_TOKEN) if __name__ == "__main__": bot = Bot() - orders = PretixOrder() + # orders = PretixOrder() + orders = TitoOrder() try: asyncio.run(main()) except KeyboardInterrupt: diff --git a/EuroPythonBot/cogs/registration.py b/EuroPythonBot/cogs/registration.py index f10541c0..37777cfe 100644 --- a/EuroPythonBot/cogs/registration.py +++ b/EuroPythonBot/cogs/registration.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import logging import discord @@ -6,10 +8,13 @@ from configuration import Config from error import AlreadyRegisteredError, NotFoundError from helpers.channel_logging import log_to_channel -from helpers.pretix_connector import PretixOrder +# from helpers.pretix_connector import PretixOrder +from helpers.tito_connector import TitoOrder config = Config() -order_ins = PretixOrder() +order_ins = TitoOrder() # PretixOrder() + +CHANGE_NICKNAME = True EMOJI_POINT = "\N{WHITE LEFT POINTING BACKHAND INDEX}" ZERO_WIDTH_SPACE = "\N{ZERO WIDTH SPACE}" @@ -19,18 +24,26 @@ class RegistrationButton(discord.ui.Button["Registration"]): - def __init__(self, x: int, y: int, label: str, style: discord.ButtonStyle): + def __init__( + self, + registration_form: RegistrationForm, + x: int = 0, + y: int = 0, + label: str = f"Register here {EMOJI_POINT}", + style: discord.ButtonStyle = discord.ButtonStyle.green, + ): super().__init__(style=discord.ButtonStyle.secondary, label=ZERO_WIDTH_SPACE, row=y) self.x = x self.y = y self.label = label self.style = style + self.registration_form = registration_form async def callback(self, interaction: discord.Interaction) -> None: assert self.view is not None # Launch the modal form - await interaction.response.send_modal(RegistrationForm()) + await interaction.response.send_modal(self.registration_form()) class RegistrationForm(discord.ui.Modal, title="Europython 2023 Registration"): @@ -48,7 +61,7 @@ class RegistrationForm(discord.ui.Modal, title="Europython 2023 Registration"): min_length=3, max_length=50, style=discord.TextStyle.short, - placeholder="Your Full Name as printed on your ticket/badge", + placeholder="Your full name as printed on your ticket/badge", ) async def on_submit(self, interaction: discord.Interaction) -> None: @@ -62,8 +75,21 @@ async def on_submit(self, interaction: discord.Interaction) -> None: for role in roles: role = discord.utils.get(interaction.guild.roles, id=role) await interaction.user.add_roles(role) - nickname = self.name.value[:32] # Limit to the max length - await interaction.user.edit(nick=nickname) + changed_nickname = True + if CHANGE_NICKNAME: + try: + # TODO(dan): change nickname not working, because no admin permission? + nickname = self.name.value[:32] # Limit to the max length + await interaction.user.edit(nick=nickname) + except discord.errors.Forbidden as ex: + msg = f"Changing nickname for {self.name} did not work: {ex}" + _logger.error(msg) + await log_to_channel( + channel=interaction.client.get_channel(config.REG_LOG_CHANNEL_ID), + interaction=interaction, + error=ex, + ) + changed_nickname = False await log_to_channel( channel=interaction.client.get_channel(config.REG_LOG_CHANNEL_ID), interaction=interaction, @@ -71,14 +97,16 @@ async def on_submit(self, interaction: discord.Interaction) -> None: order=self.order.value, roles=roles, ) - await interaction.response.send_message( - f"Thank you {self.name.value}, you are now registered!\n\nAlso, your nickname was" - f"changed to the name you used to register your ticket. This is also the name that" - f" would be on your conference badge, which means that your nickname can be your" - f"'virtual conference badge'.", - ephemeral=True, - delete_after=20, + msg = f"Thank you {self.name.value}, you are now registered!" + + if CHANGE_NICKNAME and changed_nickname: + msg += ( + "\n\nAlso, your nickname was changed to the name you used to register your ticket. " + "This is also the name that would be on your conference badge, which means that your nickname can be " + "your 'virtual conference badge'." ) + + await interaction.response.send_message(msg, ephemeral=True, delete_after=20) async def on_error(self, interaction: discord.Interaction, error: Exception) -> None: # Make sure we know what the error actually is @@ -101,19 +129,36 @@ async def on_error(self, interaction: discord.Interaction, error: Exception) -> class RegistrationView(discord.ui.View): - def __init__(self): + def __init__( + self, + registration_button: RegistrationButton = RegistrationButton, + registration_form: RegistrationForm = RegistrationForm, + ): # We don't timeout to have a persistent View super().__init__(timeout=None) self.value = None - self.add_item( - RegistrationButton(0, 0, f"Register here {EMOJI_POINT}", discord.ButtonStyle.green) - ) + self.add_item(registration_button(registration_form=registration_form)) class Registration(commands.Cog): - def __init__(self, bot): + def __init__(self, bot, registration_view: RegistrationView = RegistrationView): self.bot = bot self.guild = None + self._title = "Welcome to EuroPython 2023 on Discord! 🎉🐍" + self._desc = ( + "Follow these steps to complete your registration:\n\n" + f'1️⃣ Click on the green "Register Here {EMOJI_POINT}" button.\n\n' + '2️⃣ Fill in the "Order" (found by clicking the order URL in your confirmation ' + 'email from support@pretix.eu with the Subject: Your order: XXXX) and "Full Name" ' + "(as printed on your ticket/badge).\n\n" + '3️⃣ Click "Submit". We\'ll verify your ticket and give you your role based on ' + "your ticket type.\n\n" + "Experiencing trouble? Ask for help in the registration-help channel or from a " + "volunteer in yellow t-shirt at the conference.\n\n" + "See you on the server! 🐍💻🎉" + ) + self.registration_view = registration_view + _logger.info("Cog 'Registration' has been initialized") @commands.Cog.listener() @@ -127,25 +172,10 @@ async def on_ready(self): await order_ins.fetch_data() order_ins.load_registered() - _title = "Welcome to EuroPython 2023 on Discord! 🎉🐍" - _desc = ( - "Follow these steps to complete your registration:\n\n" - f'1️⃣ Click on the green "Register Here {EMOJI_POINT}" button.\n\n' - '2️⃣ Fill in the "Order" (found by clicking the order URL in your confirmation ' - 'email from support@pretix.eu with the Subject: Your order: XXXX) and "Full Name" ' - "(as printed on your ticket/badge).\n\n" - '3️⃣ Click "Submit". We\'ll verify your ticket and give you your role based on ' - "your ticket type.\n\n" - "Experiencing trouble? Ask for help in the registration-help channel or from a " - "volunteer in yellow t-shirt at the conference.\n\n" - "See you on the server! 🐍💻🎉" - ) - - view = RegistrationView() embed = discord.Embed( - title=_title, - description=_desc, + title=self._title, + description=self._desc, colour=0xFF8331, ) - await reg_channel.send(embed=embed, view=view) + await reg_channel.send(embed=embed, view=self.registration_view()) diff --git a/EuroPythonBot/cogs/registration_pydata.py b/EuroPythonBot/cogs/registration_pydata.py new file mode 100644 index 00000000..55b7c453 --- /dev/null +++ b/EuroPythonBot/cogs/registration_pydata.py @@ -0,0 +1,53 @@ +# import logging + +import discord +from discord.ext import commands + +# from configuration import Config +# from error import AlreadyRegisteredError, NotFoundError +# from helpers.channel_logging import log_to_channel +# from helpers.tito_connector import TitoOrder +from cogs.registration import Registration, RegistrationButton, RegistrationForm, RegistrationView + +# config = Config() +# order_ins = TitoOrder() + +# CHANGE_NICKNAME = False + +EMOJI_POINT = "\N{WHITE LEFT POINTING BACKHAND INDEX}" + +# _logger = logging.getLogger(f"bot.{__name__}") + + +# TODO(dan): make pydata subclass with changes + +class RegistrationButtonPyData(RegistrationButton): + def __init__(self, registration_form: RegistrationForm,): + super().__init__(registration_form=RegistrationFormPyData) + +class RegistrationFormPyData(RegistrationForm): + def __init__(self): + super().__init__(title="PyConDE/PyData Berlin 2024 Registration") + + +class RegistrationViewPyData(RegistrationView): + def __init__(self): + super().__init__(registration_button=RegistrationButtonPyData, registration_form=RegistrationFormPyData) + +class RegistrationPyData(Registration, commands.Cog): + def __init__(self, bot): + super().__init__(bot, registration_view=RegistrationViewPyData) + self._title = _title = "Welcome to PyConDE / PyData Berlin 2024 on Discord! 🎉🐍" + # TODO(dan): update text + self._desc = ( + "Follow these steps to complete your registration:\n\n" + f'1️⃣ Click on the green "Register Here {EMOJI_POINT}" button.\n\n' + '2️⃣ Fill in the "Order" (found by clicking the order URL in your confirmation ' + 'email from support@pretix.eu with the Subject: Your order: XXXX) and "Full Name" ' + "(as printed on your ticket/badge).\n\n" + '3️⃣ Click "Submit". We\'ll verify your ticket and give you your role based on ' + "your ticket type.\n\n" + "Experiencing trouble? Ask for help in the registration-help channel or from a " + "volunteer in yellow t-shirt at the conference.\n\n" + "See you on the server! 🐍💻🎉" + ) diff --git a/EuroPythonBot/config.toml b/EuroPythonBot/config.toml index 2312e035..0a7fc05e 100644 --- a/EuroPythonBot/config.toml +++ b/EuroPythonBot/config.toml @@ -1,109 +1,111 @@ [server] -GUILD=1120766458528542794 +GUILD=955933777706762280 [roles] -ORGANISERS=1120775903274868857 -VOLUNTEERS=1120776478825664532 -VOLUNTEERS_ONSITE=1124379825826709544 -VOLUNTEERS_REMOTE=1124379970173681664 -SPEAKERS=1120776091393593426 -SPONSORS=1120776149644087406 -PARTICIPANTS=1122452618829107220 -PARTICIPANTS_ONSITE=1120774936655568896 -PARTICIPANTS_REMOTE=1120774557394014298 +ORGANISERS=1 +VOLUNTEERS=2 +VOLUNTEERS_ONSITE=3 +VOLUNTEERS_REMOTE=4 +SPEAKERS=5 +SPONSORS=6 +PARTICIPANTS=7 +PARTICIPANTS_ONSITE=8 +PARTICIPANTS_REMOTE=9 [registration] -REG_CHANNEL_ID=1120789543000477818 -REG_HELP_CHANNEL_ID=1122535065377837116 -REG_LOG_CHANNEL_ID=1120791994789265675 +REG_CHANNEL_ID=1216372858993901708 +REG_HELP_CHANNEL_ID=1216373397681078342 +REG_LOG_CHANNEL_ID=1216373483114598491 [pretix] -PRETIX_BASE_URL = "https://pretix.eu/api/v1/organizers/europython/events/ep2023" +# PRETIX_BASE_URL = "https://pretix.eu/api/v1/organizers/europython/events/ep2023" TICKET_TO_ROLES_JSON = "ticket_to_roles_prod.json" +[tito] +TITO_BASE_URL="x" + [logging] LOG_LEVEL = "INFO" [programme_notifications] -timezone = "Europe/Prague" +timezone = "Europe/Berlin" timewarp = false -conference_days_first = "2023-07-19" -conference_days_last = "2023-07-21" -pretalx_schedule_url = "https://pretalx.com/api/events/europython-2023/schedules/latest/" -europython_session_base_url = "https://ep2023.europython.eu/session/{slug}" -europython_api_session_url = "https://ep2023.europython.eu/api/session/{code}" - -[[programme_notifications.notification_channels]] -webhook_id = "PYTHON_DISCORD" -include_channel_in_embeds = false - -[[programme_notifications.notification_channels]] -webhook_id = "PROGRAMME_NOTIFICATIONS" -include_channel_in_embeds = true - -[programme_notifications.rooms.2189] -# Forum Hall -webhook_id = "ROOM_2189" -discord_channel_id = "1120780288755253338" - -[programme_notifications.rooms.2189.livestreams] -# Forum Hall Livestream ULRs -"2023-07-19" = "https://europython.eu/live/forum" -"2023-07-20" = "https://europython.eu/live/forum" -"2023-07-21" = "https://europython.eu/live/forum" - - -[programme_notifications.rooms.2190] -# South Hall 2A -webhook_id = "ROOM_2190" -discord_channel_id = "1120780345575477421" - -[programme_notifications.rooms.2190.livestreams] -# South Hall 2A Livestream URLs -"2023-07-19" = "https://europython.eu/live/south-hall-2a" -"2023-07-20" = "https://europython.eu/live/south-hall-2a" -"2023-07-21" = "https://europython.eu/live/south-hall-2a" - -[programme_notifications.rooms.2191] -# South Hall 2B -webhook_id = "ROOM_2191" -discord_channel_id = "1120780371622121612" - -[programme_notifications.rooms.2191.livestreams] -# South Hall 2B Livestream URLs -"2023-07-19" = "https://europython.eu/live/south-hall-2b" -"2023-07-20" = "https://europython.eu/live/south-hall-2b" -"2023-07-21" = "https://europython.eu/live/south-hall-2b" - -[programme_notifications.rooms.2194] -# North Hall -webhook_id = "ROOM_2194" -discord_channel_id = "1120780401791750315" - -[programme_notifications.rooms.2194.livestreams] -# North Hall Livestream URLs -"2023-07-19" = "https://europython.eu/live/north-hall" -"2023-07-20" = "https://europython.eu/live/north-hall" -"2023-07-21" = "https://europython.eu/live/north-hall" - -[programme_notifications.rooms.2192] -# Terrace 2A -webhook_id = "ROOM_2192" -discord_channel_id = "1120780461195657387" - -[programme_notifications.rooms.2192.livestreams] -# Terrace 2A Livestream URLs -"2023-07-19" = "https://europython.eu/live/terrace-2a" -"2023-07-20" = "https://europython.eu/live/terrace-2a" -"2023-07-21" = "https://europython.eu/live/terrace-2a" - -[programme_notifications.rooms.2193] -# Terrace 2B -webhook_id = "ROOM_2193" -discord_channel_id = "1120780490576777287" - -[programme_notifications.rooms.2193.livestreams] -# Terrace 2B Livestream URLs -"2023-07-19" = "https://europython.eu/live/terrace-2b" -"2023-07-20" = "https://europython.eu/live/terrace-2b" -"2023-07-21" = "https://europython.eu/live/terrace-2b" +conference_days_first = "2024-04-22" +conference_days_last = "2024-04-24" +# pretalx_schedule_url = "https://pretalx.com/api/events/europython-2023/schedules/latest/" +# europython_session_base_url = "https://ep2023.europython.eu/session/{slug}" +# europython_api_session_url = "https://ep2023.europython.eu/api/session/{code}" + +# [[programme_notifications.notification_channels]] +# webhook_id = "PYTHON_DISCORD" +# include_channel_in_embeds = false +# +# [[programme_notifications.notification_channels]] +# webhook_id = "PROGRAMME_NOTIFICATIONS" +# include_channel_in_embeds = true +# +# [programme_notifications.rooms.2189] +# # Forum Hall +# webhook_id = "ROOM_2189" +# discord_channel_id = "1120780288755253338" +# +# [programme_notifications.rooms.2189.livestreams] +# # Forum Hall Livestream ULRs +# "2023-07-19" = "https://europython.eu/live/forum" +# "2023-07-20" = "https://europython.eu/live/forum" +# "2023-07-21" = "https://europython.eu/live/forum" +# +# [programme_notifications.rooms.2190] +# # South Hall 2A +# webhook_id = "ROOM_2190" +# discord_channel_id = "1120780345575477421" +# +# [programme_notifications.rooms.2190.livestreams] +# # South Hall 2A Livestream URLs +# "2023-07-19" = "https://europython.eu/live/south-hall-2a" +# "2023-07-20" = "https://europython.eu/live/south-hall-2a" +# "2023-07-21" = "https://europython.eu/live/south-hall-2a" +# +# [programme_notifications.rooms.2191] +# # South Hall 2B +# webhook_id = "ROOM_2191" +# discord_channel_id = "1120780371622121612" +# +# [programme_notifications.rooms.2191.livestreams] +# # South Hall 2B Livestream URLs +# "2023-07-19" = "https://europython.eu/live/south-hall-2b" +# "2023-07-20" = "https://europython.eu/live/south-hall-2b" +# "2023-07-21" = "https://europython.eu/live/south-hall-2b" +# +# [programme_notifications.rooms.2194] +# # North Hall +# webhook_id = "ROOM_2194" +# discord_channel_id = "1120780401791750315" +# +# [programme_notifications.rooms.2194.livestreams] +# # North Hall Livestream URLs +# "2023-07-19" = "https://europython.eu/live/north-hall" +# "2023-07-20" = "https://europython.eu/live/north-hall" +# "2023-07-21" = "https://europython.eu/live/north-hall" +# +# [programme_notifications.rooms.2192] +# # Terrace 2A +# webhook_id = "ROOM_2192" +# discord_channel_id = "1120780461195657387" +# +# [programme_notifications.rooms.2192.livestreams] +# # Terrace 2A Livestream URLs +# "2023-07-19" = "https://europython.eu/live/terrace-2a" +# "2023-07-20" = "https://europython.eu/live/terrace-2a" +# "2023-07-21" = "https://europython.eu/live/terrace-2a" +# +# [programme_notifications.rooms.2193] +# # Terrace 2B +# webhook_id = "ROOM_2193" +# discord_channel_id = "1120780490576777287" +# +# [programme_notifications.rooms.2193.livestreams] +# # Terrace 2B Livestream URLs +# "2023-07-19" = "https://europython.eu/live/terrace-2b" +# "2023-07-20" = "https://europython.eu/live/terrace-2b" +# "2023-07-21" = "https://europython.eu/live/terrace-2b" diff --git a/EuroPythonBot/configuration.py b/EuroPythonBot/configuration.py index 470e7fba..f1fe92e4 100644 --- a/EuroPythonBot/configuration.py +++ b/EuroPythonBot/configuration.py @@ -21,7 +21,7 @@ class Config(metaclass=Singleton): _CONFIG_DEFAULT = "config.toml" _CONFIG_LOCAL = "config.local.toml" - def __init__(self): + def __init__(self, testing: bool = False): # Configuration file config = None self.BASE_PATH = Path(__file__).resolve().parent @@ -34,26 +34,27 @@ def __init__(self): sys.exit(-1) try: - # Server - self.GUILD = int(config["server"]["GUILD"]) - - # Registration - self.REG_CHANNEL_ID = int(config["registration"]["REG_CHANNEL_ID"]) - self.REG_HELP_CHANNEL_ID = int(config["registration"]["REG_HELP_CHANNEL_ID"]) - self.REG_LOG_CHANNEL_ID = int(config["registration"]["REG_LOG_CHANNEL_ID"]) - - # Pretix - self.PRETIX_BASE_URL = config["pretix"]["PRETIX_BASE_URL"] - self.TICKET_TO_ROLES_JSON = config["pretix"]["TICKET_TO_ROLES_JSON"] - # Logging self.LOG_LEVEL = config.get("logging", {}).get("LOG_LEVEL", "INFO") + + # Server + self.GUILD = int(config["server"]["GUILD"]) - # Mapping - with self.BASE_PATH.joinpath(self.TICKET_TO_ROLES_JSON).open() as ticket_to_roles_file: - ticket_to_roles = json.load(ticket_to_roles_file) - - self.TICKET_TO_ROLE = ticket_to_roles + if not testing: + # Registration + self.REG_CHANNEL_ID = int(config["registration"]["REG_CHANNEL_ID"]) + self.REG_HELP_CHANNEL_ID = int(config["registration"]["REG_HELP_CHANNEL_ID"]) + self.REG_LOG_CHANNEL_ID = int(config["registration"]["REG_LOG_CHANNEL_ID"]) + + # Pretix + self.PRETIX_BASE_URL = "" # config["pretix"]["PRETIX_BASE_URL"] + self.TICKET_TO_ROLES_JSON = config["pretix"]["TICKET_TO_ROLES_JSON"] + + # Mapping + with self.BASE_PATH.joinpath(self.TICKET_TO_ROLES_JSON).open() as ticket_to_roles_file: + ticket_to_roles = json.load(ticket_to_roles_file) + + self.TICKET_TO_ROLE = ticket_to_roles except KeyError: _logger.critical( diff --git a/EuroPythonBot/helpers/tito_connector.py b/EuroPythonBot/helpers/tito_connector.py new file mode 100644 index 00000000..2de5e34f --- /dev/null +++ b/EuroPythonBot/helpers/tito_connector.py @@ -0,0 +1,171 @@ +import logging +import os +from datetime import datetime +from http import HTTPStatus +from pathlib import Path +from time import time +from typing import Dict, List + +import aiofiles +import aiohttp +from dotenv import load_dotenv + +from configuration import Config, Singleton +from error import AlreadyRegisteredError, NotFoundError + +_logger = logging.getLogger(f"bot.{__name__}") + + +def sanitize_string(input_string: str) -> str: + """Process the name to make it more uniform.""" + return input_string.replace(" ", "").lower() + + +class TitoOrder(metaclass=Singleton): + def __init__(self): + self.config = Config() + load_dotenv(Path(__file__).resolve().parent.parent.parent / ".secrets") + # PRETIX_TOKEN = os.getenv("PRETIX_TOKEN") + # self.HEADERS = {"Authorization": f"Token {PRETIX_TOKEN}"} + + self.id_to_name = None + self.orders = {} + # TODO: fetch data every 5 minutes, triggered when validating tickets and checking last_fetch + self.last_fetch = None + + self.registered_file = getattr(self.config, "REGISTERED_LOG_FILE", "./registered_log.txt") + self.REGISTERED_SET = set() + + def load_registered(self): + try: + f = open(self.registered_file, "r") + registered = [reg.strip() for reg in f.readlines()] + self.REGISTERED_SET = set(registered) + f.close() + except Exception: + _logger.exception("Cannot load registered data, starting from scratch. Error:") + + async def fetch_data(self) -> None: + """Fetch data from Tito, store id_to_name mapping and formated orders internally""" + + _logger.info("Fetching IDs names from Tito") + # self.id_to_name = await self._get_id_to_name_map() + _logger.info("Done fetching IDs names from Tito") + + _logger.info("Fetching orders from Tito") + time_start = time() + results = [] # await self._fetch_all(f"{self.config.PRETIX_BASE_URL}/orders") + _logger.info("Fetched %r orders in %r seconds", len(results), time() - time_start) + + def flatten_concatenation(matrix): + flat_list = [] + for row in matrix: + flat_list += row + return flat_list + + orders = {} + for position in flatten_concatenation( + [result.get("positions") for result in results if result.get("status") == "p"] + ): + item = position.get("item") + if self.id_to_name.get(item) in [ + "T-shirt (free)", + "Childcare (Free)", + "Livestream Only", + ]: + continue + order = position.get("order") + attendee_name = sanitize_string(position.get("attendee_name")) + + orders[f"{order}-{attendee_name}"] = self.id_to_name.get(item) + + self.orders = orders + self.last_fetch = datetime.now() + + async def _get_id_to_name_map(self) -> Dict[int, str]: + return {7: "PARTICIPANT"} + url = f"{self.config.PRETIX_BASE_URL}/items" + + async with aiohttp.ClientSession() as session: + async with session.get(url, headers=self.HEADERS) as response: + if response.status != HTTPStatus.OK: + response.raise_for_status() + + data = await response.json() + + id_to_name = {} + for result in data.get("results"): + id = result.get("id") + name = result.get("name").get("en") + id_to_name[id] = name + for variation in result.get("variations"): + variation_id = variation.get("id") + variation_name = variation.get("value").get("en") + id_to_name[variation_id] = variation_name + return id_to_name + + async def _fetch(self, url, session): + async with session.get(url, headers=self.HEADERS) as response: + return await response.json() + + async def _fetch_all(self, url): + async with aiohttp.ClientSession() as session: + results = [] + while url: + data = await self._fetch(url, session) + results += data.get("results") + url = data.get("next") + return results + + async def get_ticket_type(self, order: str, full_name: str) -> str: + """With user input `order` and `full_name`, check for their ticket type""" + + return "Personal" + + key = f"{order}-{sanitize_string(input_string=full_name)}" + self.validate_key(key) + ticket_type = None + try: + ticket_type = self.orders[key] + self.REGISTERED_SET.add(key) + async with aiofiles.open(self.registered_file, mode="a") as f: + await f.write(f"{key}\n") + except KeyError: + async with aiohttp.ClientSession() as session: + async with session.get( + f"{self.config.PRETIX_BASE_URL}/orders", + headers=self.HEADERS, + params={ + "code": order, + "search": full_name, + }, + ) as request: + if request.status == HTTPStatus.OK: + data = await request.json() + # when using search params, pretix returns a list of results of size 1 + # with a list of positions of size 1 + if len(data.get("results")) > 0: + result = data.get("results")[0] + if result.get("status") != "p": + raise Exception("Order not paid") + ticket_type = self.id_to_name.get( + result.get("positions")[0].get("item") + ) + self.REGISTERED_SET.add(key) + async with aiofiles.open(self.registered_file, mode="a") as f: + await f.write(f"{key}\n") + else: + raise NotFoundError(f"No ticket found - inputs: {order=}, {full_name=}") + else: + _logger.error("Error occurred: Status %r", request.status) + + return ticket_type + + async def get_roles(self, name: str, order: str) -> List[int]: + ticket_type = await self.get_ticket_type(full_name=name, order=order) + return self.config.TICKET_TO_ROLE.get(ticket_type) + + def validate_key(self, key: str) -> bool: + if key in self.REGISTERED_SET: + raise AlreadyRegisteredError(f"Ticket already registered - id: {key}") + return True diff --git a/EuroPythonBot/ticket_to_roles_prod.json b/EuroPythonBot/ticket_to_roles_prod.json index ea22dc09..c2f3015d 100644 --- a/EuroPythonBot/ticket_to_roles_prod.json +++ b/EuroPythonBot/ticket_to_roles_prod.json @@ -1,44 +1,32 @@ { "Business": [ - 1122452618829107220, - 1120774936655568896 + 1, 2 ], "Community Contributors": [ - 1122452618829107220, - 1120774936655568896 + 1, 2 ], "Grant ticket": [ - 1122452618829107220, - 1120774936655568896 + 1, 2 ], "Personal": [ - 1122452618829107220, - 1120774936655568896 + 1218172880219668490 ], "Education": [ - 1122452618829107220, - 1120774936655568896 + 1, 2 ], "Remote Participation Ticket": [ - 1122452618829107220, - 1120774557394014298 + 1, 2 ], "Remote Community Organiser": [ - 1122452618829107220, - 1120774557394014298 + 1, 2 ], "Remote Grant ticket": [ - 1122452618829107220, - 1120774557394014298 + 1, 2 ], "Presenter": [ - 1122452618829107220, - 1120774936655568896, - 1120776091393593426 + 1, 2 ], "Sponsor Conference Pass": [ - 1122452618829107220, - 1120774936655568896, - 1120776149644087406 + 1, 2 ] } diff --git a/README.md b/README.md index 64de41eb..debfade8 100644 --- a/README.md +++ b/README.md @@ -45,3 +45,20 @@ or with docker: docker build --tag discord_bot . docker run --interactive --tty --env DISCORD_BOT_TOKEN=$DISCORD_BOT_TOKEN --env PRETIX_TOKEN=$PRETIX_TOKEN discord_bot ``` + +# Pydata deploy notes + +As we are not using ansible we need to rely to do some manual stuff on the ssh. + +```bash +# create log file +mkdir /home/bot +touch /home/bot/registered_log.txt +``` + +```bash +mkdir -p /etc/EuroPython/discord/ +touch /etc/EuroPython/discord/.secrets +# replace ... with the token :) +echo "DISCORD_BOT_TOKEN=..." > /etc/EuroPython/discord/.secrets +``` \ No newline at end of file diff --git a/registered_log.txt b/registered_log.txt new file mode 100644 index 00000000..e69de29b