import calendar import os import threading from datetime import datetime, timedelta from enum import Enum import discord import gradio as gr import stripe from discord.ext import commands # HF GUILD SETTINGS MY_GUILD_ID = int(os.environ.get("GUILD_ID")) MY_GUILD = discord.Object(id=MY_GUILD_ID) DISCORD_TOKEN = os.environ.get("DISCORD_TOKEN") stripe.api_key = os.environ.get("STRIPE_API_KEY") # ORDER DETAILS class ReachyMiniType(Enum): Wireless = "Reachy-Mini (with Onboard Compute and battery)" Lite = "Reachy Mini (Lite version)" def detect_reachy_types(inv) -> set[ReachyMiniType]: types = set() for line in inv.lines.data: desc = line.description or "" for rtype in ReachyMiniType: if rtype.value in desc: types.add(rtype) return types def format_order_items(inv) -> str: items = [] for line in inv.lines.data: items.append(f"• {line.description} (x{line.quantity})") return "\n".join(items) def get_invoice_digits(invoice_number: str) -> int: parts = invoice_number.split("-") return int(parts[-1]) if parts[-1].isdigit() else 0 def early_shipping_notes(types: set[ReachyMiniType], invoice_number: str, arrival_date: datetime) -> str: now = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) notes = [] if ReachyMiniType.Lite in types: if now < datetime(2026, 5, 1) and arrival_date > datetime(2026, 4, 30): notes.append( "📬 All Reachy Mini Lite orders will be shipped **late April 2026** " "— yours might arrive before the expected date!" ) if ReachyMiniType.Wireless in types: inv_num = get_invoice_digits(invoice_number) if inv_num < 7000: if now < datetime(2026, 4, 1) and arrival_date > datetime(2026, 3, 31): notes.append( "📬 Reachy Mini Wireless orders with invoice number before 7000 will be shipped **late March 2026/early April 2026** " "— yours might arrive before the expected date!" ) else: if now < datetime(2026, 7, 1) and arrival_date > datetime(2026, 6, 15): notes.append( "📬 Reachy Mini Wireless orders with invoice number 7000 and above will be shipped **early June 2026** " "— yours might arrive before the expected date!" ) elif now < datetime(2026, 7, 1) and arrival_date <= datetime(2026, 6, 1): notes.append( "⚠️ There is a chance your Reachy Mini Wireless shipment might be delayed until **early June 2026**. " "The Wireless version has been trickier to produce due to longer supply times " "for some components (especially the Raspberry Pi). We apologize for the inconvenience." ) notes.append( "🔗 You can manage your order through our Stripe customer portal:\n" "https://billing.stripe.com/p/login/7sY5kFal10614vB4W873G00" ) return "\n\n".join(notes) class Bot(commands.Bot): """This structure allows slash commands to work instantly.""" def __init__(self): super().__init__(command_prefix="/", intents=discord.Intents.default()) async def setup_hook(self): await self.tree.sync(guild=discord.Object(MY_GUILD_ID)) print(f"Synced slash commands for {self.user}.") client = Bot() @client.event async def on_ready(): print(f"Logged in as {client.user} (ID: {client.user.id})") print("------") MIN_DATE = datetime(2025, 7, 9) def format_eta_result(purchase_date: datetime) -> str: arrival_date = purchase_date + timedelta(days=90) today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) days_until = (arrival_date - today).days if days_until < -14: status = "✅ Your Reachy Mini must already have been delivered! Check with your carrier or contact sales@pollen-robotics.com if you haven't received it." elif days_until < 0: status = "⏳ The delivery seems to have been delayed a bit. It should arrive any day now — hang tight!" elif days_until <= 7: status = "🎉 The delivery is right around the corner! Your Reachy Mini will be there very soon." else: status = f"🚚 Your Reachy Mini is on its way! About **{days_until} days** to go." return ( f"🗓️ Purchase date: **{purchase_date.strftime('%B %d, %Y')}**\n" f"📦 Expected Reachy Mini arrival: **{arrival_date.strftime('%B %d, %Y')}** (90 days after purchase)\n\n" f"{status}" ) class DayButton(discord.ui.Button): def __init__(self, day: int, row: int, disabled: bool = False): super().__init__( label=str(day), style=discord.ButtonStyle.secondary, row=row, disabled=disabled, ) self.day = day async def callback(self, interaction: discord.Interaction): view: DatePickerView = self.view purchase_date = datetime(view.year, view.month, self.day) await interaction.response.edit_message( content=format_eta_result(purchase_date), view=None, ) def valid_months(year: int) -> list[int]: today = datetime.now() months = [] for m in range(1, 13): if (year, m) < (MIN_DATE.year, MIN_DATE.month): continue if (year, m) > (today.year, today.month): continue months.append(m) return months def valid_years() -> list[int]: today = datetime.now() return [y for y in range(MIN_DATE.year, today.year + 1)] def clamp_month(year: int, month: int) -> int: months = valid_months(year) if month in months: return month if month < months[0]: return months[0] return months[-1] def is_day_disabled(year: int, month: int, day: int) -> bool: today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) dt = datetime(year, month, day) return dt < MIN_DATE or dt > today class DatePickerView(discord.ui.View): FIRST_PAGE = 14 MID_PAGE = 13 def __init__(self): super().__init__(timeout=180) today = datetime.now() self.year = today.year self.month = today.month self.page = 0 self.rebuild() def rebuild(self): self.clear_items() self._build_selects() self._build_day_buttons() def _build_selects(self): today = datetime.now() year_options = [ discord.SelectOption( label=str(y), value=str(y), default=(y == self.year) ) for y in valid_years() ] year_select = discord.ui.Select(placeholder="Year", options=year_options, row=0) year_select.callback = self._on_year self.add_item(year_select) months = valid_months(self.year) month_options = [ discord.SelectOption( label=calendar.month_name[m], value=str(m), default=(m == self.month) ) for m in months ] month_select = discord.ui.Select(placeholder="Month", options=month_options, row=1) month_select.callback = self._on_month self.add_item(month_select) async def _on_year(self, interaction: discord.Interaction): self.year = int(interaction.data["values"][0]) self.month = clamp_month(self.year, self.month) self.page = 0 self.rebuild() await interaction.response.edit_message( content=f"🤖 Select your Reachy Mini purchase date:", view=self, ) async def _on_month(self, interaction: discord.Interaction): self.month = int(interaction.data["values"][0]) self.page = 0 self.rebuild() await interaction.response.edit_message( content=f"🤖 Select your Reachy Mini purchase date:", view=self, ) def _build_day_buttons(self): num_days = calendar.monthrange(self.year, self.month)[1] if self.page == 0: start, end = 1, min(self.FIRST_PAGE, num_days) else: start = self.FIRST_PAGE + 1 + (self.page - 1) * self.MID_PAGE end = min(start + self.MID_PAGE - 1, num_days) has_prev = self.page > 0 has_next = end < num_days for i, d in enumerate(range(start, end + 1)): disabled = is_day_disabled(self.year, self.month, d) self.add_item(DayButton(d, row=2 + i // 5, disabled=disabled)) day_count = end - start + 1 last_row = 2 + (day_count - 1) // 5 last_row_used = day_count % 5 or 5 nav_count = int(has_prev) + int(has_next) if nav_count > 0: if last_row_used + nav_count <= 5: nav_row = last_row else: nav_row = min(last_row + 1, 4) if has_prev: btn = discord.ui.Button(label="◂", style=discord.ButtonStyle.primary, row=nav_row) btn.callback = self._prev_page self.add_item(btn) if has_next: btn = discord.ui.Button(label="▸", style=discord.ButtonStyle.primary, row=nav_row) btn.callback = self._next_page self.add_item(btn) async def _next_page(self, interaction: discord.Interaction): self.page += 1 self.rebuild() await interaction.response.edit_message(view=self) async def _prev_page(self, interaction: discord.Interaction): self.page -= 1 self.rebuild() await interaction.response.edit_message(view=self) @client.tree.command(name="eta", description="Find out when your Reachy Mini will arrive", guild=MY_GUILD) async def eta(interaction: discord.Interaction, invoice: str = None): if invoice: await interaction.response.defer(ephemeral=True) if invoice.isdigit(): invoice = f"REACHYMINI-{invoice}" try: results = stripe.Invoice.search(query=f'number:"{invoice}"') except stripe.error.AuthenticationError: view = DatePickerView() await interaction.followup.send( "❌ Unable to verify invoice. Please select your purchase date manually:", view=view, ephemeral=True, ) return if not results.data: view = DatePickerView() await interaction.followup.send( "❌ Invoice not found — make sure you're entering the **invoice number** (not the order number). Please select your purchase date manually:", view=view, ephemeral=True, ) return inv = results.data[0] purchase_date = datetime.fromtimestamp(inv.created) purchase_date = purchase_date.replace(hour=0, minute=0, second=0, microsecond=0) arrival_date = purchase_date + timedelta(days=90) items_text = format_order_items(inv) types = detect_reachy_types(inv) shipping_notes = early_shipping_notes(types, invoice, arrival_date) message = f"🔎 Invoice number **{invoice}** found!\n\n" message += f"🛒 **Ordered items:**\n{items_text}\n\n" message += format_eta_result(purchase_date) if shipping_notes: message += f"\n\n{shipping_notes}" await interaction.followup.send(message, ephemeral=True) else: view = DatePickerView() await interaction.response.send_message( "🤖 Select your Reachy Mini purchase date:", view=view, ephemeral=True, ) def run_bot(): client.run(DISCORD_TOKEN) threading.Thread(target=run_bot).start() """This allows us to run the Discord bot in a Python thread""" with gr.Blocks() as demo: gr.Image("reachy-mailman.png") demo.queue(default_concurrency_limit=100, max_size=100) demo.launch()