forked from sass/tipibot
Buy tickets (200 coins each, max 100/draw); one winner is drawn daily at 21:00 Tallinn time weighted by ticket count and credited the whole pot. Coin-conserving by design: each ticket's cost is deducted at purchase and the winner is minted exactly the sum of all ticket spend - no shared pot record, so no cross-period race. Ticket state lives per-user keyed by draw period (full scan only at draw time and for the pot view). - New lottery.py: TICKET_COST/MAX_TICKETS/DRAW_HOUR, pure period_for, and do_buy_ticket / get_lottery_state / do_lottery_draw. New lottery_tickets + lottery_period schema fields (period added to _TEXT_FIELDS). - /lottery [kogus] command (view or buy) with full failure handling. - Scheduled lottery_draw_daily loop in bot.py (both profiles; each draws its own collection), announcing to the optional LOTTERY_CHANNEL_ID (config + .env). 14 tests: period boundary, buy/accumulate/reset/caps/guards, pot state, draw payout with coin conservation, and the more-tickets-wins-more weighting. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013VbAVsrZuYesea99mPMmPT
94 lines
3.6 KiB
Python
94 lines
3.6 KiB
Python
import os
|
|
from dotenv import load_dotenv
|
|
|
|
load_dotenv()
|
|
|
|
BOT_PROFILE = os.getenv("BOT_PROFILE", "dev").strip().lower() or "dev"
|
|
if BOT_PROFILE not in {"dev", "economy"}:
|
|
raise SystemExit("BOT_PROFILE must be either 'dev' or 'economy'.")
|
|
|
|
|
|
def _env_int(name: str, default: int) -> int:
|
|
raw = os.getenv(name)
|
|
if raw is None or not raw.strip():
|
|
return default
|
|
return int(raw)
|
|
|
|
|
|
_LEGACY_DISCORD_TOKEN = os.getenv("DISCORD_TOKEN", "")
|
|
DISCORD_TOKEN_DEV = os.getenv("DISCORD_TOKEN_DEV", "")
|
|
DISCORD_TOKEN_ECONOMY = os.getenv("DISCORD_TOKEN_ECONOMY", "")
|
|
DISCORD_TOKEN = (
|
|
DISCORD_TOKEN_ECONOMY if BOT_PROFILE == "economy" else DISCORD_TOKEN_DEV
|
|
) or _LEGACY_DISCORD_TOKEN
|
|
|
|
SHEET_ID = os.getenv("SHEET_ID")
|
|
GOOGLE_CREDS_PATH = os.getenv("GOOGLE_CREDS_PATH", "credentials.json")
|
|
|
|
_LEGACY_GUILD_ID = _env_int("GUILD_ID", 0)
|
|
GUILD_ID_DEV = _env_int("GUILD_ID_DEV", _LEGACY_GUILD_ID)
|
|
GUILD_ID_ECONOMY = _env_int("GUILD_ID_ECONOMY", _LEGACY_GUILD_ID)
|
|
GUILD_ID = GUILD_ID_ECONOMY if BOT_PROFILE == "economy" else GUILD_ID_DEV
|
|
|
|
_LEGACY_BIRTHDAY_CHANNEL_ID = _env_int("BIRTHDAY_CHANNEL_ID", 0)
|
|
BIRTHDAY_CHANNEL_ID_DEV = _env_int("BIRTHDAY_CHANNEL_ID_DEV", _LEGACY_BIRTHDAY_CHANNEL_ID)
|
|
BIRTHDAY_CHANNEL_ID_ECONOMY = _env_int("BIRTHDAY_CHANNEL_ID_ECONOMY", 0)
|
|
BIRTHDAY_CHANNEL_ID = (
|
|
BIRTHDAY_CHANNEL_ID_ECONOMY
|
|
if BOT_PROFILE == "economy"
|
|
else BIRTHDAY_CHANNEL_ID_DEV
|
|
)
|
|
|
|
BIRTHDAY_WINDOW_DAYS = int(os.getenv("BIRTHDAY_WINDOW_DAYS", "7"))
|
|
BASE_ROLE_IDS: list[int] = [1478304631930228779, 1478302278862766190]
|
|
|
|
# Channel where the daily lottery draw result is announced. Optional - if unset,
|
|
# the draw still runs and pays the winner, it just isn't announced.
|
|
_LEGACY_LOTTERY_CHANNEL_ID = _env_int("LOTTERY_CHANNEL_ID", 0)
|
|
LOTTERY_CHANNEL_ID_DEV = _env_int("LOTTERY_CHANNEL_ID_DEV", _LEGACY_LOTTERY_CHANNEL_ID)
|
|
LOTTERY_CHANNEL_ID_ECONOMY = _env_int("LOTTERY_CHANNEL_ID_ECONOMY", 0)
|
|
LOTTERY_CHANNEL_ID = (
|
|
LOTTERY_CHANNEL_ID_ECONOMY if BOT_PROFILE == "economy" else LOTTERY_CHANNEL_ID_DEV
|
|
)
|
|
|
|
|
|
def _parse_admin_roles(raw: str) -> dict[int, set[int]]:
|
|
"""Parse DISCORD_ADMIN_ROLES env var as "guild_id:role_id[:role_id...],guild_id:role_id...".
|
|
|
|
Multiple admin roles per guild are colon-separated; guild entries are comma-separated.
|
|
Repeating a guild_id across entries merges its roles.
|
|
"""
|
|
result: dict[int, set[int]] = {}
|
|
for entry in raw.split(","):
|
|
entry = entry.strip()
|
|
if not entry:
|
|
continue
|
|
parts = entry.split(":")
|
|
if len(parts) < 2 or not all(p.strip() for p in parts):
|
|
raise SystemExit(
|
|
f"DISCORD_ADMIN_ROLES: expected 'guild_id:role_id[:role_id...]', got {entry!r}"
|
|
)
|
|
guild_id = int(parts[0].strip())
|
|
result.setdefault(guild_id, set()).update(int(p.strip()) for p in parts[1:])
|
|
return result
|
|
|
|
|
|
BOT_ADMIN_ROLES: dict[int, set[int]] = _parse_admin_roles(os.getenv("DISCORD_ADMIN_ROLES", ""))
|
|
|
|
PB_URL = os.getenv("PB_URL", "http://127.0.0.1:8090")
|
|
PB_ADMIN_EMAIL = os.getenv("PB_ADMIN_EMAIL", "")
|
|
PB_ADMIN_PASSWORD = os.getenv("PB_ADMIN_PASSWORD", "")
|
|
|
|
_LEGACY_PB_COLLECTION = os.getenv("PB_ECONOMY_COLLECTION", "").strip()
|
|
PB_ECONOMY_COLLECTION_DEV = (
|
|
os.getenv("PB_ECONOMY_COLLECTION_DEV", "").strip()
|
|
or (_LEGACY_PB_COLLECTION if _LEGACY_PB_COLLECTION else "economy_users_dev")
|
|
)
|
|
PB_ECONOMY_COLLECTION_ECONOMY = (
|
|
os.getenv("PB_ECONOMY_COLLECTION_ECONOMY", "").strip()
|
|
or (_LEGACY_PB_COLLECTION if _LEGACY_PB_COLLECTION else "economy_users_prod")
|
|
)
|
|
PB_ECONOMY_COLLECTION = (
|
|
PB_ECONOMY_COLLECTION_ECONOMY if BOT_PROFILE == "economy" else PB_ECONOMY_COLLECTION_DEV
|
|
)
|