From fcd3e87793f44c20c92bdadb4c12c4915b7b411e Mon Sep 17 00:00:00 2001 From: luk3yx Date: Sat, 19 Mar 2022 17:12:19 +1300 Subject: Initial public commit --- currency.liteonly.py | 205 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 currency.liteonly.py (limited to 'currency.liteonly.py') diff --git a/currency.liteonly.py b/currency.liteonly.py new file mode 100644 index 0000000..7288676 --- /dev/null +++ b/currency.liteonly.py @@ -0,0 +1,205 @@ +""" +Based on currency.py - Sopel Currency Conversion Plugin +Copyright 2013, Elsie Powell, embolalia.com +Copyright 2019, Mikkel Jeppesen +Licensed under the Eiffel Forum License 2. + +Original currency.py: https://sopel.chat +""" + +import logging +import re +import time +import urllib.parse + +import requests + + +# Which data provider to use (some of which require no API key) +FIAT_PROVIDER = 'exchangerate.host' + +FIXER_IO_KEY = None +FIXER_USE_SSL = False + + +FIAT_PROVIDERS = { + 'exchangerate.host': 'https://api.exchangerate.host/latest?base=EUR', + 'fixer.io': '//data.fixer.io/api/latest?base=EUR&access_key={}', +} +CRYPTO_URL = 'https://api.coingecko.com/api/v3/exchange_rates' +EXCHANGE_REGEX = re.compile(r''' + ^(\d+(?:\.\d+)?) # Decimal number + \s*([a-zA-Z]{3}) # 3-letter currency code + \s+(?:in|as|of|to)\s+ # preposition + (([a-zA-Z]{3}$)|([a-zA-Z]{3})\s)+$ # one or more 3-letter currency code +''', re.VERBOSE) +LOGGER = logging.getLogger(__name__) +UNSUPPORTED_CURRENCY = "Sorry, {} isn't currently supported." +USAGE = "Usage: .cur 100 usd in btc cad eur" +UNRECOGNIZED_INPUT = f"Sorry, I didn't understand the input. {USAGE}" + +rates = {} +rates_updated = 0.0 + + +class FixerError(Exception): + """A Fixer.io API Error Exception""" + def __init__(self, status): + super().__init__("FixerError: {}".format(status)) + + +class UnsupportedCurrencyError(Exception): + """A currency is currently not supported by the API""" + def __init__(self, currency): + super().__init__(currency) + + +def build_reply(amount, base, target, out_string): + rate_raw = get_rate(base, target) + rate = float(rate_raw) + result = float(rate * amount) + + digits = 0 + # up to 10 (8+2) digits precision when result is less than 1 + # as smaller results need more precision + while digits < 8 and 1 / 10**digits > result: + digits += 1 + + digits += 2 + + out_string += ' {value:,.{precision}f} {currency},'.format( + value=result, precision=digits, currency=target + ) + + return out_string + + +def exchange(match): + """Show the exchange rate between two currencies""" + if not match: + return UNRECOGNIZED_INPUT + + # Try and update rates. Rate-limiting is done in update_rates() + try: + update_rates() + except requests.exceptions.RequestException as err: + traceback.print_exc() + return "Something went wrong while I was getting the exchange rate." + except (KeyError, ValueError) as err: + traceback.print_exc() + return "Error: Could not update exchange rates. Try again later." + except FixerError as err: + traceback.print_exc() + return 'Sorry, something went wrong with Fixer' + + query = match.string + amount_in, base, _, *targets = query.split() + + try: + amount = float(amount_in) + except ValueError: + return UNRECOGNIZED_INPUT + except OverflowError: + return "Sorry, input amount was out of range." + + if not amount: + return "Zero is zero, no matter what country you're in." + + out_string = '{} {} is'.format(amount_in, base.upper()) + + unsupported_currencies = [] + for target in targets: + try: + out_string = build_reply(amount, base.upper(), target.upper(), + out_string) + except ValueError: + return "Error: Raw rate wasn't a float" + except KeyError as err: + return "Error: Invalid rates" + except UnsupportedCurrencyError as cur: + unsupported_currencies.append(cur) + + if unsupported_currencies: + out_string = out_string + ' (unsupported:' + for target in unsupported_currencies: + out_string = out_string + ' {},'.format(target) + out_string = out_string[0:-1] + ')' + else: + out_string = out_string[0:-1] + + return out_string + + +def get_rate(base, target): + base = base.upper() + target = target.upper() + + if base not in rates: + raise UnsupportedCurrencyError(base) + + if target not in rates: + raise UnsupportedCurrencyError(target) + + return (1 / rates[base]) * rates[target] + + +def update_rates(): + global rates, rates_updated + + # If we have data that is less than 24h old, return + if time.time() - rates_updated < 24 * 60 * 60: + return + + # Update crypto rates + response = requests.get(CRYPTO_URL) + response.raise_for_status() + rates_crypto = response.json() + + # Update fiat rates + if FIXER_IO_KEY is not None: + assert FIAT_PROVIDER == 'fixer.io' + + proto = 'https:' if FIXER_USE_SSL else 'http:' + response = requests.get( + proto + + FIAT_PROVIDERS['fixer.io'].format( + urllib.parse.quote(FIXER_IO_KEY) + ) + ) + + if not response.json()['success']: + raise FixerError('Fixer.io request failed with error: {}'.format( + response.json()['error'] + )) + else: + LOGGER.debug('Updating fiat rates from %s', FIAT_PROVIDER) + response = requests.get(FIAT_PROVIDERS[FIAT_PROVIDER]) + + response.raise_for_status() + rates_fiat = response.json() + + rates = rates_fiat['rates'] + rates['EUR'] = 1.0 # Put this here to make logic easier + + eur_btc_rate = 1 / rates_crypto['rates']['eur']['value'] + + for rate in rates_crypto['rates']: + if rate.upper() not in rates: + rates[rate.upper()] = (rates_crypto['rates'][rate]['value'] * + eur_btc_rate) + + # if an error aborted the operation prematurely, we want the next call to + # retry updating rates therefore we'll update the stored timestamp at the + # last possible moment + rates_updated = time.time() + + +@register_command('cur', 'currency', 'exchange') +def exchange_cmd(irc, hostmask, is_admin, args): + """Show the exchange rate between two currencies.""" + if param := args[1].strip(): + msg = exchange(EXCHANGE_REGEX.match(param)) + else: + msg = f"No search term. {USAGE}" + + irc.msg(args[0], f'{hostmask[0]}: {msg}') -- cgit v1.2.3