# meta developer: @matubuntu import time from datetime import datetime import aiohttp from .. import loader, utils _FLAGS = { "AUD": "๐Ÿ‡ฆ๐Ÿ‡บ", "AZN": "๐Ÿ‡ฆ๐Ÿ‡ฟ", "GBP": "๐Ÿ‡ฌ๐Ÿ‡ง", "AMD": "๐Ÿ‡ฆ๐Ÿ‡ฒ", "BYN": "๐Ÿ‡ง๐Ÿ‡พ", "BGN": "๐Ÿ‡ง๐Ÿ‡ฌ", "BRL": "๐Ÿ‡ง๐Ÿ‡ท", "HUF": "๐Ÿ‡ญ๐Ÿ‡บ", "VND": "๐Ÿ‡ป๐Ÿ‡ณ", "HKD": "๐Ÿ‡ญ๐Ÿ‡ฐ", "GEL": "๐Ÿ‡ฌ๐Ÿ‡ช", "DKK": "๐Ÿ‡ฉ๐Ÿ‡ฐ", "AED": "๐Ÿ‡ฆ๐Ÿ‡ช", "USD": "๐Ÿ‡บ๐Ÿ‡ธ", "EUR": "๐Ÿ‡ช๐Ÿ‡บ", "EGP": "๐Ÿ‡ช๐Ÿ‡ฌ", "INR": "๐Ÿ‡ฎ๐Ÿ‡ณ", "IDR": "๐Ÿ‡ฎ๐Ÿ‡ฉ", "KZT": "๐Ÿ‡ฐ๐Ÿ‡ฟ", "CAD": "๐Ÿ‡จ๐Ÿ‡ฆ", "QAR": "๐Ÿ‡ถ๐Ÿ‡ฆ", "KGS": "๐Ÿ‡ฐ๐Ÿ‡ฌ", "CNY": "๐Ÿ‡จ๐Ÿ‡ณ", "MDL": "๐Ÿ‡ฒ๐Ÿ‡ฉ", "NZD": "๐Ÿ‡ณ๐Ÿ‡ฟ", "NOK": "๐Ÿ‡ณ๐Ÿ‡ด", "PLN": "๐Ÿ‡ต๐Ÿ‡ฑ", "RON": "๐Ÿ‡ท๐Ÿ‡ด", "SGD": "๐Ÿ‡ธ๐Ÿ‡ฌ", "TJS": "๐Ÿ‡น๐Ÿ‡ฏ", "THB": "๐Ÿ‡น๐Ÿ‡ญ", "TRY": "๐Ÿ‡น๐Ÿ‡ท", "TMT": "๐Ÿ‡น๐Ÿ‡ฒ", "UZS": "๐Ÿ‡บ๐Ÿ‡ฟ", "UAH": "๐Ÿ‡บ๐Ÿ‡ฆ", "CZK": "๐Ÿ‡จ๐Ÿ‡ฟ", "SEK": "๐Ÿ‡ธ๐Ÿ‡ช", "CHF": "๐Ÿ‡จ๐Ÿ‡ญ", "RSD": "๐Ÿ‡ท๐Ÿ‡ธ", "ZAR": "๐Ÿ‡ฟ๐Ÿ‡ฆ", "KRW": "๐Ÿ‡ฐ๐Ÿ‡ท", "JPY": "๐Ÿ‡ฏ๐Ÿ‡ต", } _CRYPTO_EMOJIS = { "BTC": "๐Ÿ’ฐ", "ETH": "๐Ÿ’ฐ", "SOL": "๐Ÿ’ฐ", "TON": "๐Ÿ’ฐ", "USDT": "๐Ÿ’ฐ", "XRP": "๐Ÿ’ฐ", "USDC": "๐Ÿ’ฐ", "ADA": "๐Ÿ’ฐ", "DOGE": "๐Ÿ’ฐ", "TRX": "๐Ÿ’ฐ", "AVAX": "๐Ÿ’ฐ", "LTC": "๐Ÿ’ฐ", "BCH": "๐Ÿ’ฐ", "ATOM": "๐Ÿ’ฐ", "XLM": "๐Ÿ’ฐ", "SHIB": "๐Ÿ’ฐ", "UNI": "๐Ÿ’ฐ", "XMR": "๐Ÿ’ฐ", "LINK": "๐Ÿ’ฐ", "ETC": "๐Ÿ’ฐ", "SUI": "๐Ÿ’ฐ", "NEAR": "๐Ÿ’ฐ", "VET": "๐Ÿ’ฐ", "FIL": "๐Ÿ’ฐ", "XTZ": "๐Ÿ’ฐ", "ALGO": "๐Ÿ’ฐ", "THETA": "๐Ÿ’ฐ", "FTM": "๐Ÿ’ฐ", "XDAI": "๐Ÿ’ฐ", "RUNE": "๐Ÿ’ฐ", "DOT": "๐Ÿ’ฐ", } _CRYPTO_NAMES = { "BTC": "Bitcoin", "ETH": "Ethereum", "XMR": "Monero", "LTC": "Litecoin", "XRP": "XRP", "ADA": "Cardano", "DOGE": "Dogecoin", "SOL": "Solana", "DOT": "Polkadot", "USDT": "Tether", "TON": "Toncoin", "USDC": "USD Coin", "TRX": "TRON", "AVAX": "Avalanche", "BCH": "Bitcoin Cash", "ATOM": "Cosmos", "XLM": "Stellar", "SHIB": "Shiba Inu", "UNI": "Uniswap", "LINK": "Chainlink", "ETC": "Ethereum Classic", "SUI": "Sui", "NEAR": "NEAR Protocol", "VET": "VeChain", "FIL": "Filecoin", "XTZ": "Tezos", "ALGO": "Algorand", "THETA": "Theta Network", "FTM": "Fantom", "XDAI": "xDai", "RUNE": "THORChain", } _CBR_URL = "https://www.cbr.ru/scripts/XML_daily.asp" _CRYPTO_URL = "https://api.coinlore.net/api/tickers/?limit=100" CACHE_TTL = 300 # seconds def _fmt_num(value: float, decimals: int = 3) -> str: if decimals == 0: return f"{int(value):,}".replace(",", " ") rounded = round(value, decimals) int_part = int(rounded) dec_part = str(rounded - int_part)[2:2 + decimals].rstrip("0") int_str = f"{int_part:,}".replace(",", " ") return f"{int_str},{dec_part}" if dec_part else int_str def _parse_cbr_xml(xml_bytes: bytes) -> tuple[str | None, dict]: """Parse CBR XML without bs4/lxml โ€” pure stdlib ElementTree.""" import xml.etree.ElementTree as ET root = ET.fromstring(xml_bytes) date_str = root.attrib.get("Date", "") try: date = datetime.strptime(date_str, "%d.%m.%Y").strftime("%d.%m.%Y") except ValueError: date = date_str rates: dict[str, dict] = {} for valute in root.findall("Valute"): code = valute.findtext("CharCode", "").strip() if not code or code == "XDR": continue try: nominal = float(valute.findtext("Nominal", "1").replace(",", ".")) value = float(valute.findtext("Value", "0").replace(",", ".")) except ValueError: continue rates[code] = { "name": valute.findtext("Name", code).strip(), "nominal": nominal, "rub": value / nominal, } return date, rates @loader.tds class FinanceMod(loader.Module): """ะšัƒั€ัั‹ ะฒะฐะปัŽั‚ (ะฆะ‘ ะ ะค) ะธ ะบั€ะธะฟั‚ะพะฒะฐะปัŽั‚ (CoinLore)""" strings = {"name": "FinanceMod"} def __init__(self): self.config = loader.ModuleConfig( loader.ConfigValue( "crypto_currency", "USD", lambda: "ะ’ะฐะปัŽั‚ะฐ ะดะปั ะพั‚ะพะฑั€ะฐะถะตะฝะธั ะบั€ะธะฟั‚ั‹ (USD, RUB, EUR)", validator=loader.validators.Choice(["USD", "RUB", "EUR"]), ) ) # Simple in-process cache self._cbr_cache: tuple[float, str, dict] | None = None # (ts, date, rates) self._crypto_cache: tuple[float, list] | None = None # (ts, data) # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ HTTP helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ async def _fetch(self, url: str, *, as_json: bool = False): async with aiohttp.ClientSession() as session: async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as resp: resp.raise_for_status() return await resp.json() if as_json else await resp.read() # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ CBR data โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ async def _cbr_data(self) -> tuple[str | None, dict]: now = time.monotonic() if self._cbr_cache and now - self._cbr_cache[0] < CACHE_TTL: return self._cbr_cache[1], self._cbr_cache[2] try: raw = await self._fetch(_CBR_URL) date, rates = _parse_cbr_xml(raw) self._cbr_cache = (now, date, rates) return date, rates except Exception: if self._cbr_cache: return self._cbr_cache[1], self._cbr_cache[2] return None, {} # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Crypto data โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ async def _crypto_data(self) -> list: now = time.monotonic() if self._crypto_cache and now - self._crypto_cache[0] < CACHE_TTL: return self._crypto_cache[1] try: js = await self._fetch(_CRYPTO_URL, as_json=True) data = js.get("data", []) self._crypto_cache = (now, data) return data except Exception: return self._crypto_cache[1] if self._crypto_cache else [] # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Formatters โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ def _fmt_valute(self, code: str, info: dict, amount: float = 1.0) -> str: total = info["rub"] * amount flag = _FLAGS.get(code, "๐Ÿณ") return f"{flag} [{_fmt_num(amount, 0)}] {info['name']} ({code}) โ€” {_fmt_num(total, 3)} โ‚ฝ" def _fmt_crypto(self, coin: dict, rates: dict, amount: float = 1.0) -> str: symbol = coin["symbol"].upper() try: price_usd = float(coin["price_usd"]) except (KeyError, ValueError, TypeError): return "" currency = self.config["crypto_currency"] if currency == "RUB": usd_rate = rates.get("USD", {}).get("rub") if not usd_rate: return "" price = price_usd * usd_rate sign = "โ‚ฝ" elif currency == "EUR": usd_rate = rates.get("USD", {}).get("rub") eur_rate = rates.get("EUR", {}).get("rub") if not usd_rate or not eur_rate: return "" price = price_usd * (usd_rate / eur_rate) sign = "โ‚ฌ" else: price = price_usd sign = "$" total = price * amount emoji = _CRYPTO_EMOJIS.get(symbol, "๐Ÿ’ ") name = _CRYPTO_NAMES.get(symbol, coin.get("name", symbol)) return f"{emoji} [{_fmt_num(amount, 0)}] {name} ({symbol}) โ€” {_fmt_num(total, 3)}{sign}" # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Commands โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ @loader.command(ru_doc="[ะบะพะป-ะฒะพ] [ะบะพะด] โ€” ะบัƒั€ั ะฒะฐะปัŽั‚ั‹ ะฟะพ ะฆะ‘ ะ ะค") async def valutecmd(self, message): """[amount] [code] โ€” exchange rates from CBR""" args = utils.get_args(message) date, rates = await self._cbr_data() if not rates: return await utils.answer(message, "๐Ÿšซ ะะต ัƒะดะฐะปะพััŒ ะฟะพะปัƒั‡ะธั‚ัŒ ะดะฐะฝะฝั‹ะต ะฆะ‘ ะ ะค") header = ( f"๐Ÿ’ต ะšัƒั€ั ะฒะฐะปัŽั‚ั‹ ยท ะฆะ‘ ะ ะค\n" f"ะะบั‚ัƒะฐะปัŒะฝะพ ะฝะฐ {date}\n\n" ) # .valute โ€” ัะฟะธัะพะบ ะฒัะตั…, ะบะพะป-ะฒะพ = 1 if not args: lines = [self._fmt_valute(c, i) for c, i in rates.items()] return await utils.answer( message, header + f"
{chr(10).join(lines)}
", ) # ะŸะตั€ะฒั‹ะน ะฐั€ะณัƒะผะตะฝั‚: ั‡ะธัะปะพ ะธะปะธ ะบะพะด ะฒะฐะปัŽั‚ั‹? amount = 1.0 code = None arg0 = args[0].upper() if len(args) >= 2: # .valute 100 USD try: amount = float(args[0].replace(",", ".")) except ValueError: return await utils.answer(message, "๐Ÿšซ ะะตะบะพั€ั€ะตะบั‚ะฝะพะต ั‡ะธัะปะพ") code = args[1].upper() else: # .valute USD ะธะปะธ .valute 100 try: amount = float(arg0.replace(",", ".")) # ั‡ะธัะปะพ ะฑะตะท ะบะพะดะฐ โ€” ัะฟะธัะพะบ ั ัƒะผะฝะพะถะตะฝะธะตะผ except ValueError: code = arg0 if code: if code not in rates: return await utils.answer(message, f"๐Ÿšซ ะ’ะฐะปัŽั‚ะฐ {code} ะฝะต ะฝะฐะนะดะตะฝะฐ") line = self._fmt_valute(code, rates[code], amount) return await utils.answer(message, header + line) # ัะฟะธัะพะบ ั ะบะพะป-ะฒะพะผ lines = [self._fmt_valute(c, i, amount) for c, i in rates.items()] await utils.answer( message, header + f"
{chr(10).join(lines)}
", ) @loader.command(ru_doc="[ะบะพะป-ะฒะพ] [ะบะพะด] โ€” ะบัƒั€ั ะบั€ะธะฟั‚ั‹") async def cryptocmd(self, message): """[amount] [symbol] โ€” crypto rates from CoinLore""" args = utils.get_args(message) coins = await self._crypto_data() _, rates = await self._cbr_data() if not coins: return await utils.answer(message, "๐Ÿšซ ะะต ัƒะดะฐะปะพััŒ ะฟะพะปัƒั‡ะธั‚ัŒ ะดะฐะฝะฝั‹ะต ะบั€ะธะฟั‚ั‹") header = f"๐Ÿ’Ž ะšัƒั€ัั‹ ะบั€ะธะฟั‚ะพะฒะฐะปัŽั‚ ยท {self.config['crypto_currency']}\n\n" amount = 1.0 symbol = None if not args: pass # ัะฟะธัะพะบ, amount=1 elif len(args) == 1: try: amount = float(args[0].replace(",", ".")) except ValueError: symbol = args[0].upper() else: try: amount = float(args[0].replace(",", ".")) except ValueError: return await utils.answer(message, "๐Ÿšซ ะะตะบะพั€ั€ะตะบั‚ะฝะพะต ั‡ะธัะปะพ") symbol = args[1].upper() if symbol: coin = next((c for c in coins if c["symbol"].upper() == symbol), None) if not coin: return await utils.answer(message, f"๐Ÿšซ ะšั€ะธะฟั‚ะฐ {symbol} ะฝะต ะฝะฐะนะดะตะฝะฐ") line = self._fmt_crypto(coin, rates, amount) if not line: return await utils.answer(message, "๐Ÿšซ ะžัˆะธะฑะบะฐ ั„ะพั€ะผะฐั‚ะธั€ะพะฒะฐะฝะธั") return await utils.answer(message, header + line) # ัะฟะธัะพะบ ั‚ะพะปัŒะบะพ ะธะทะฒะตัั‚ะฝั‹ั… ะผะพะฝะตั‚ known = {c["symbol"].upper(): c for c in coins if c["symbol"].upper() in _CRYPTO_NAMES} # ัะพั€ั‚ะธั€ัƒะตะผ ะฟะพ ะฟะพั€ัะดะบัƒ _CRYPTO_NAMES lines = [] for sym in _CRYPTO_NAMES: if sym in known: line = self._fmt_crypto(known[sym], rates, amount) if line: lines.append(line) await utils.answer( message, header + f"
{chr(10).join(lines)}
", )