Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
| 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() | |
| 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) | |
| 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() | |