# 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)}
",
)