"""Bar commands: Conjurer invents cocktails and keeps a growing menu. The single most in-character thing the bot does - the persona is literally a 200kg bartender who mixes strong drinks with intriguing names. `$nalej` asks the active AI backend (whatever $gadaj_teraz points at) to invent a themed cocktail in Conjurer's voice; every drink is appended to a menu.json that becomes emergent bar lore, browsable with `$menu`. """ import json import logging import random from datetime import datetime import discord from discord.ext import commands from ai_functions import handle_response from communication_subroutine import PREPPED_TRACKS from constants import ENCODING, GPT_SETTINGS, GUILD_ID, MENU_FILE # Keywords that mean "theme the drink on whatever is playing on the radio". _RADIO_THEMES = {"pod muzykę", "pod muzyke", "radio", "muzyka", "muzyką"} _MAX_MSG = 1900 # Discord caps messages at 2000 chars. _MENU_LIST_LIMIT = 15 # how many drink names $menu shows at once def _persona() -> str: """Conjurer's bartender persona (reused from the AI settings, not duplicated).""" try: if isinstance(GPT_SETTINGS, list) and GPT_SETTINGS and isinstance(GPT_SETTINGS[0], dict): return GPT_SETTINGS[0].get("content", "") or "" except Exception: # pylint: disable=broad-except pass return ( "Jesteś Conjurerem, 200-kilowym barmanem i wykidajłą w klimatycznym barze " "mechawojowników. Mówisz po polsku, jesteś rubaszny, wtrącasz staronorweskie " "i jidysz powiedzonka." ) def _load_menu() -> list: try: with open(MENU_FILE, "r", encoding=ENCODING) as handle: data = json.load(handle) return data if isinstance(data, list) else [] except (FileNotFoundError, json.JSONDecodeError, OSError): return [] def _append_menu(entry: dict) -> None: menu = _load_menu() menu.append(entry) try: with open(MENU_FILE, "w", encoding=ENCODING) as handle: json.dump(menu, handle, indent=2, ensure_ascii=False) except OSError as exc: logging.getLogger("discord").warning("Nie mogę zapisać menu do %s: %s", MENU_FILE, exc) def _extract_name(text: str) -> str: """First non-empty line, stripped of the name marker / markdown, as the drink name.""" for line in text.splitlines(): stripped = line.strip().lstrip("🍸#*-–—•0123456789. ").strip().strip("*_`") if stripped: return stripped[:120] return "Bezimienny drink" class BarModule(commands.Cog): """Drink generation + the bar menu.""" def __init__(self, bot): self.bot = bot self.logger = logging.getLogger("discord") async def _send_chunked(self, ctx, text: str) -> None: text = text or "" while text: await ctx.send(text[:_MAX_MSG]) text = text[_MAX_MSG:] @commands.hybrid_command( name="nalej", description="Conjurer wymyśla drinka (opcjonalnie na motyw, albo 'radio' pod obecny kawałek)", guild=discord.Object(id=GUILD_ID), ) async def nalej(self, ctx, *, motyw: str = ""): """Invent a cocktail themed on `motyw`. Empty = a surprise; 'radio' / 'pod muzykę' themes it on whatever is currently playing on the radio.""" async with ctx.channel.typing(): motyw = (motyw or "").strip() author = ctx.author.nick if getattr(ctx.author, "nick", None) else ctx.author.name if motyw.lower() in _RADIO_THEMES: now = (PREPPED_TRACKS.get("now_playing") or "").strip() if now: theme_desc = f"pod obecnie grany na radiu kawałek: „{now}”" stored_theme = f"radio: {now}" else: theme_desc = "zaskakujący, bo na radiu akurat cisza" stored_theme = "radio (cisza)" elif motyw: theme_desc = f"na motyw: „{motyw}”" stored_theme = motyw else: theme_desc = "całkowicie zaskakujący, wymyślony spontanicznie" stored_theme = "(niespodzianka)" instructions = ( f"Wymyśl JEDEN autorski koktajl {theme_desc}. Ma być mocny i mieć intrygującą, " "klimatyczną nazwę. Format odpowiedzi:\n" "- pierwsza linia: sama nazwa drinka (bez nagłówków, bez 'Nazwa:'),\n" "- potem lista składników,\n" "- potem krótki sposób przygotowania,\n" "- na końcu jedno-, dwuzdaniowy klimatyczny opis w Twoim stylu.\n" "Całość po polsku, w postaci Conjurera. Bądź zwięzły." ) # request_type NONE + a proper [system, user] message list: persona as # system (so the drink is in-character), instructions as user, and NO # write to the bar's conversation memory. prompt = [ {"role": "system", "content": _persona()}, {"role": "user", "content": instructions}, ] try: result, _ = await handle_response( "", True, True, [], author, "NONE", none_request=prompt ) except Exception as exc: # pylint: disable=broad-except self.logger.exception("nalej: AI failed: %s", exc) await ctx.send("*Conjurer upuszcza shaker* Coś mi się rozlało - spróbuj jeszcze raz.") return name = _extract_name(result) _append_menu( { "nazwa": name, "motyw": stored_theme, "autor": author, "kiedy": datetime.now().isoformat(timespec="seconds"), "tekst": result, } ) self.logger.info("Nowy drink w menu: %s (motyw: %s)", name, stored_theme) await self._send_chunked(ctx, result) @commands.hybrid_command( name="menu", description="Pokazuje menu wymyślonych do tej pory drinków", guild=discord.Object(id=GUILD_ID), ) async def menu(self, ctx): """List the bar's invented drinks (names), and pour one from the archive.""" async with ctx.channel.typing(): menu = _load_menu() if not menu: await ctx.send( "*Conjurer stuka w pustą tablicę* Menu jeszcze świeci pustkami. " "Rzuć `$nalej`, a coś wymyślę." ) return total = len(menu) recent = menu[-_MENU_LIST_LIMIT:] lines = [ f"🍸 **Menu Baru** — {total} drink(ów) w archiwum" + (f" (ostatnie {len(recent)}):" if total > len(recent) else ":") ] base = total - len(recent) for i, drink in enumerate(recent, start=1): lines.append(f"{base + i}. {drink.get('nazwa', '(bez nazwy)')} — od {drink.get('autor', '?')}") lines.append("\n*Conjurer poleca dziś z archiwum:*") await self._send_chunked(ctx, "\n".join(lines)) # ...and pour one at random from the whole archive, in full. pick = random.choice(menu) # nosec B311 - flavour, not security await self._send_chunked(ctx, pick.get("tekst", pick.get("nazwa", ""))) async def setup(bot): logger = logging.getLogger("discord") await bot.add_cog(BarModule(bot)) logger.info("Loading bar commands module done")