diff --git a/handler/agora.py b/handler/agora.py index 97df3d3..3626f6c 100644 --- a/handler/agora.py +++ b/handler/agora.py @@ -1,5 +1,4 @@ # Twisted/Klein imports -from twisted.logger import Logger from twisted.internet.task import LoopingCall from twisted.internet.threads import deferToThread @@ -16,10 +15,8 @@ from datetime import datetime from settings import settings import util -log = Logger("agora.global") - -class Agora(object): +class Agora(util.Base): """ AgoraDesk API handler. """ @@ -29,7 +26,7 @@ class Agora(object): Initialise the AgoraDesk and CurrencyRates APIs. Initialise the last_dash storage for detecting new trades. """ - self.log = Logger("agora") + super().__init__() self.agora = AgoraDesk(settings.Agora.Token) self.cr = CurrencyRates() # TODO: remove this and defer to money self.cg = CoinGeckoAPI() # TODO: remove this and defer to money @@ -66,7 +63,7 @@ class Agora(object): if not dash.items(): return False if "data" not in dash["response"].keys(): - self.log.error("Data not in dashboard response: {content}", content=dash) + self.log.error(f"Data not in dashboard response: {dash}") return dash_tmp if dash["response"]["data"]["contact_count"] > 0: for contact in dash["response"]["data"]["contact_list"]: @@ -166,7 +163,7 @@ class Agora(object): if not messages["success"]: return False if "data" not in messages["response"]: - self.log.error("Data not in messages response: {content}", content=messages["response"]) + self.log.error(f"Data not in messages response: {messages['response']}") return False open_tx = self.tx.get_ref_map().keys() for message in messages["response"]["data"]["message_list"]: @@ -429,20 +426,15 @@ class Agora(object): continue else: if "error_code" not in rtrn["response"]["error"]: - self.log.error("Error code not in return for ad {ad_id}: {response}", ad_id=ad_id, response=rtrn["response"]) + self.log.error(f"Error code not in return for ad {ad_id}: {rtrn['response']}") return if rtrn["response"]["error"]["error_code"] == 429: throttled += 1 sleep_time = pow(throttled, float(settings.Agora.SleepExponent)) - self.log.info( - "Throttled {x} times while updating {id}, sleeping for {sleep} seconds", - x=throttled, - id=ad_id, - sleep=sleep_time, - ) + self.log.info(f"Throttled {throttled} times while updating {ad_id}, sleeping for {sleep_time} seconds") # We're running in a thread, so this is fine sleep(sleep_time) - self.log.error("Error updating ad {ad_id}: {response}", ad_id=ad_id, response=rtrn["response"]) + self.log.error(f"Error updating ad {ad_id}: {rtrn['response']}") continue iterations += 1 @@ -645,9 +637,7 @@ class Agora(object): if not float(wallet_xmr) > profit_usd_in_xmr: # Not enough funds to withdraw - self.log.error( - "Not enough funds to withdraw {profit}, as wallet only contains {wallet}", profit=profit_usd_in_xmr, wallet=wallet_xmr - ) + self.log.error(f"Not enough funds to withdraw {profit_usd_in_xmr}, as wallet only contains {wallet_xmr}") self.irc.sendmsg(f"Not enough funds to withdraw {profit_usd_in_xmr}, as wallet only contains {wallet_xmr}") return diff --git a/handler/agoradesk_py.py b/handler/agoradesk_py.py index ad4d776..46644b4 100644 --- a/handler/agoradesk_py.py +++ b/handler/agoradesk_py.py @@ -12,6 +12,9 @@ from typing import Union import arrow import httpx +# Project imports +import util + __author__ = "marvin8" __copyright__ = "(C) 2021 https://codeberg.org/MarvinsCryptoTools/agoradesk_py" __version__ = "0.1.0" @@ -20,7 +23,7 @@ __version__ = "0.1.0" logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") logging.getLogger("requests.packages.urllib3").setLevel(logging.INFO) logging.getLogger("urllib3.connectionpool").setLevel(logging.INFO) -logger = logging.getLogger(__name__) +logger = util.get_logger(__name__) URI_API = "https://agoradesk.com/api/v1/" diff --git a/handler/app.py b/handler/app.py index 9d7472d..01e6a63 100755 --- a/handler/app.py +++ b/handler/app.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 # Twisted/Klein imports -from twisted.logger import Logger from twisted.internet import reactor from klein import Klein @@ -41,33 +40,20 @@ def cleanup(sig, frame): signal(SIGINT, cleanup) # Handle Ctrl-C and run the cleanup routine -def convert(data): - if isinstance(data, bytes): - return data.decode("ascii") - if isinstance(data, dict): - return dict(map(convert, data.items())) - if isinstance(data, tuple): - return map(convert, data) - return data - - -class WebApp(object): +class WebApp(util.Base): """ Our Klein webapp. """ app = Klein() - def __init__(self): - self.log = Logger("webapp") - @app.route("/callback", methods=["POST"]) def callback(self, request): content = request.content.read() try: parsed = loads(content) except JSONDecodeError: - self.log.error("Failed to parse JSON callback: {content}", content=content) + self.log.error(f"Failed to parse JSON callback: {content}") return dumps(False) self.log.info("Callback received: {parsed}", parsed=parsed["data"]["id"]) # self.tx.transaction(parsed) @@ -76,19 +62,19 @@ class WebApp(object): # set up another connection to a bank @app.route("/signin", methods=["GET"]) def signin(self, request): - auth_url = self.truelayer.create_auth_url() + auth_url = self.sinks.truelayer.create_auth_url() return f'Please sign in here.' # endpoint called after we finish setting up a connection above @app.route("/callback-truelayer", methods=["POST"]) def signin_callback(self, request): code = request.args[b"code"] - self.truelayer.handle_authcode_received(code) + self.sinks.truelayer.handle_authcode_received(code) return dumps(True) @app.route("/accounts", methods=["GET"]) def balance(self, request): - accounts = self.truelayer.get_accounts() + accounts = self.sinks.truelayer.get_accounts() return dumps(accounts, indent=2) diff --git a/handler/markets.py b/handler/markets.py index c1ab740..cce9409 100644 --- a/handler/markets.py +++ b/handler/markets.py @@ -1,21 +1,16 @@ -# Twisted/Klein imports -from twisted.logger import Logger - # Other library imports from json import loads # Project imports from settings import settings +import util -class Markets(object): +class Markets(util.Base): """ " Markets handler for generic market functions. """ - def __init__(self): - self.log = Logger("markets") - def get_all_assets(self): assets = loads(settings.Agora.AssetList) return assets diff --git a/handler/money.py b/handler/money.py index ce14ed1..c94e03f 100644 --- a/handler/money.py +++ b/handler/money.py @@ -1,15 +1,13 @@ -# Twisted/Klein imports -from twisted.logger import Logger - # Other library imports from pycoingecko import CoinGeckoAPI from forex_python.converter import CurrencyRates # Project imports from settings import settings +import util -class Money(object): +class Money(util.Base): """ Generic class for handling money-related matters that aren't Revolut or Agora. """ @@ -20,7 +18,7 @@ class Money(object): Set the logger. Initialise the CoinGecko API. """ - self.log = Logger("money") + super().__init__() self.cr = CurrencyRates() self.cg = CoinGeckoAPI() @@ -41,6 +39,7 @@ class Money(object): price = float(ad[2]) rate = round(price / base_currency_price, 2) ad.append(rate) + # TODO: sort? return sorted(ads, key=lambda x: x[2]) def get_rates_all(self): diff --git a/handler/sinks/__init__.py b/handler/sinks/__init__.py index 1fc1824..b9e5396 100644 --- a/handler/sinks/__init__.py +++ b/handler/sinks/__init__.py @@ -1,6 +1,3 @@ -# Twisted/Klein imports -from twisted.logger import Logger - # Other library imports # import requests # from json import dumps @@ -10,15 +7,16 @@ from twisted.logger import Logger import sinks.fidor import sinks.nordigen import sinks.truelayer +import util -class Sinks(object): +class Sinks(util.Base): """ Class to manage calls to various sinks. """ def __init__(self): - self.log = Logger("sinks") + super().__init__() self.fidor = sinks.fidor.Fidor() self.nordigen = sinks.nordigen.Nordigen() self.truelayer = sinks.truelayer.TrueLayer() diff --git a/handler/sinks/fidor.py b/handler/sinks/fidor.py index 8add9a3..3423a91 100644 --- a/handler/sinks/fidor.py +++ b/handler/sinks/fidor.py @@ -1,5 +1,4 @@ # Twisted/Klein imports -from twisted.logger import Logger # Other library imports # import requests @@ -7,16 +6,14 @@ from twisted.logger import Logger # Project imports # from settings import settings +import util -class Fidor(object): +class Fidor(util.Base): """ Class to manage calls to the Fidor API. """ - def __init__(self): - self.log = Logger("fidor") - def authorize(self): """ Perform initial authorization against Fidor API. diff --git a/handler/sinks/nordigen.py b/handler/sinks/nordigen.py index 2ff15c3..a9032da 100644 --- a/handler/sinks/nordigen.py +++ b/handler/sinks/nordigen.py @@ -1,6 +1,3 @@ -# Twisted/Klein imports -from twisted.logger import Logger - # Other library imports import requests from json import dumps @@ -8,15 +5,16 @@ from simplejson.errors import JSONDecodeError # Project imports from settings import settings +import util -class Nordigen(object): +class Nordigen(util.Base): """ Class to manage calls to Open Banking APIs through Nordigen. """ def __init__(self): - self.log = Logger("nordigen") + super().__init__() self.token = None self.get_access_token() @@ -36,11 +34,11 @@ class Nordigen(object): try: parsed = r.json() except JSONDecodeError: - self.log.error("Error parsing access token response: {content}", content=r.content) + self.log.error(f"Error parsing access token response: {r.content}") return False if "access" in parsed: self.token = parsed["access"] - self.log.info("Refreshed access token - Nordigen") + self.log.info("Refreshed access token") def get_institutions(self, country, filter_name=None): """ @@ -58,7 +56,7 @@ class Nordigen(object): try: parsed = r.json() except JSONDecodeError: - self.log.error("Error parsing institutions response: {content}", content=r.content) + self.log.error(f"Error parsing institutions response: {r.content}") return False new_list = [] if filter_name: diff --git a/handler/sinks/truelayer.py b/handler/sinks/truelayer.py index bd4addb..c140843 100644 --- a/handler/sinks/truelayer.py +++ b/handler/sinks/truelayer.py @@ -1,5 +1,4 @@ # Twisted/Klein imports -from twisted.logger import Logger from twisted.internet.task import LoopingCall # Other library imports @@ -10,15 +9,16 @@ import urllib # Project imports from settings import settings +import util -class TrueLayer(object): +class TrueLayer(util.Base): """ Class to manage calls to Open Banking APIs through TrueLayer. """ def __init__(self): - self.log = Logger("truelayer") + super().__init__() self.token = None self.lc = LoopingCall(self.get_new_token) self.lc.start(int(settings.TrueLayer.RefreshSec)) @@ -58,7 +58,7 @@ class TrueLayer(object): settings.TrueLayer.AuthCode = authcode settings.write() self.token = parsed["access_token"] - self.log.info("Retrieved access/refresh tokens - TrueLayer") + self.log.info("Retrieved access/refresh tokens") def get_new_token(self): """ @@ -81,7 +81,7 @@ class TrueLayer(object): if r.status_code == 200: if "access_token" in parsed.keys(): self.token = parsed["access_token"] - self.log.info("Refreshed access token - TrueLayer") + self.log.info("Refreshed access token") return True else: self.log.error(f"Token refresh didn't contain access token: {parsed}", parsed=parsed) @@ -100,7 +100,25 @@ class TrueLayer(object): try: parsed = r.json() except JSONDecodeError: - self.log.error("Error parsing institutions response: {content}", content=r.content) + self.log.error("Error parsing accounts response: {content}", content=r.content) return False return parsed + + def get_transactions(self, account_id): + """ + Get a list of transactions from an account. + :param account_id: account to fetch transactions for + :return: list of transactions + :rtype: dict + """ + headers = {"Authorization": f"Bearer {self.token}"} + path = f"{settings.TrueLayer.DataBase}/accounts/{account_id}/transactions" + r = requests.get(path, headers=headers) + try: + parsed = r.json() + except JSONDecodeError: + self.log.error("Error parsing transactions response: {content}", content=r.content) + return False + + return parsed["results"] diff --git a/handler/transactions.py b/handler/transactions.py index 51cd034..5731eb1 100644 --- a/handler/transactions.py +++ b/handler/transactions.py @@ -1,5 +1,4 @@ # Twisted/Klein imports -from twisted.logger import Logger from twisted.internet.task import LoopingCall from twisted.internet.threads import deferToThread @@ -15,7 +14,7 @@ import logging # Project imports from settings import settings from db import r -from util import convert +import util # TODO: secure ES traffic properly urllib3.disable_warnings() @@ -26,7 +25,7 @@ tracer = logging.getLogger("elastic_transport.transport") tracer.setLevel(logging.CRITICAL) -class Transactions(object): +class Transactions(util.Base): """ Handler class for incoming Revolut transactions. """ @@ -36,7 +35,7 @@ class Transactions(object): Initialise the Transaction object. Set the logger. """ - self.log = Logger("transactions") + super().__init__() if settings.ES.Enabled == "1": self.es = Elasticsearch( f"https://{settings.ES.Host}:9200", @@ -89,10 +88,10 @@ class Transactions(object): # stored_trade here is actually TX stored_trade = r.hgetall(f"tx.{txid}") if not stored_trade: - self.log.error("Could not find entry in DB for typeless transaction: {id}", id=txid) + self.log.error(f"Could not find entry in DB for typeless transaction: {txid}") return print("BEFORE CONVERT STORED TRADE", stored_trade) - stored_trade = convert(stored_trade) + stored_trade = util.convert(stored_trade) if "old_state" in inside: if "new_state" in inside: # We don't care unless we're being told a transaction is now completed @@ -109,7 +108,7 @@ class Transactions(object): r.hmset(f"tx.{txid}", stored_trade) # Check it's all been previously validated if "valid" not in stored_trade: - self.log.error("Valid not in stored trade for {txid}, aborting.", txid=txid) + self.log.error(f"Valid not in stored trade for {txid}, aborting.") return if stored_trade["valid"] == "1": # Make it invalid immediately, as we're going to release now @@ -124,7 +123,7 @@ class Transactions(object): else: txtype = inside["type"] if txtype == "card_payment": - self.log.info("Ignoring card payment: {id}", id=txid) + self.log.info(f"Ignoring card payment: {txid}") return state = inside["state"] @@ -142,7 +141,7 @@ class Transactions(object): amount = leg["amount"] if amount <= 0: - self.log.info("Ignoring transaction with negative/zero amount: {id}", id=txid) + self.log.info(f"Ignoring transaction with negative/zero amount: {txid}") return currency = leg["currency"] description = leg["description"] @@ -161,7 +160,7 @@ class Transactions(object): "description": description, "valid": 0, # All checks passed and we can release escrow? } - self.log.info("Transaction processed: {formatted}", formatted=dumps(to_store, indent=2)) + self.log.info(f"Transaction processed: {dumps(to_store, indent=2)}") self.irc.sendmsg(f"AUTO Incoming transaction: {amount}{currency} ({reference}) - {state} - {description}") # Partial reference implementation # Account for silly people not removing the default string @@ -172,7 +171,7 @@ class Transactions(object): # Get all parts of the given reference split that match the existing references stored_trade_reference = set(existing_refs).intersection(set(ref_split)) if len(stored_trade_reference) > 1: - self.log.error("Multiple references valid for TXID {txid}: {reference}", txid=txid, reference=reference) + self.log.error(f"Multiple references valid for TXID {txid}: {reference}") self.irc.sendmsg(f"Multiple references valid for TXID {txid}: {reference}") return @@ -181,21 +180,16 @@ class Transactions(object): # Amount/currency lookup implementation if not stored_trade_reference: - self.log.info(f"No reference in DB refs for {reference}", reference=reference) + self.log.info(f"No reference in DB refs for {reference}") self.irc.sendmsg(f"No reference in DB refs for {reference}") # Try checking just amount and currency, as some people (usually people buying small amounts) # are unable to put in a reference properly. - self.log.info("Checking against amount and currency for TXID {txid}", txid=txid) + self.log.info(f"Checking against amount and currency for TXID {txid}") self.irc.sendmsg(f"Checking against amount and currency for TXID {txid}") stored_trade = self.find_trade(txid, currency, amount) if not stored_trade: - self.log.info( - "Failed to get reference by amount and currency: {txid} {currency} {amount}", - txid=txid, - currency=currency, - amount=amount, - ) + self.log.info(f"Failed to get reference by amount and currency: {txid} {currency} {amount}") self.irc.sendmsg(f"Failed to get reference by amount and currency: {txid} {currency} {amount}") return if currency == "USD": @@ -215,7 +209,7 @@ class Transactions(object): if not stored_trade: stored_trade = self.get_ref(stored_trade_reference.pop()) if not stored_trade: - self.log.info("No reference in DB for {reference}", reference=reference) + self.log.info(f"No reference in DB for {reference}") self.irc.sendmsg(f"No reference in DB for {reference}") return @@ -224,11 +218,7 @@ class Transactions(object): # Make sure it was sent in the expected currency if not stored_trade["currency"] == currency: - self.log.info( - "Currency mismatch, Agora: {currency_agora} / Sink: {currency}", - currency_agora=stored_trade["currency"], - currency=currency, - ) + self.log.info(f"Currency mismatch, Agora: {stored_trade['currency']} / Sink: {currency}") self.irc.sendmsg(f"Currency mismatch, Agora: {stored_trade['currency']} / Sink: {currency}") return @@ -238,19 +228,10 @@ class Transactions(object): return # If the amount does not match exactly, get the min and max values for our given acceptable margins for trades min_amount, max_amount = self.money.get_acceptable_margins(currency, stored_trade["amount"]) - self.log.info( - "Amount does not match exactly, trying with margins: min: {min_amount} / max: {max_amount}", - min_amount=min_amount, - max_amount=max_amount, - ) + self.log.info(f"Amount does not match exactly, trying with margins: min: {min_amount} / max: {max_amount}") self.irc.sendmsg(f"Amount does not match exactly, trying with margins: min: {min_amount} / max: {max_amount}") if not min_amount < amount < max_amount: - self.log.info( - "Amount mismatch - not in margins: {amount} (min: {min_amount} / max: {max_amount}", - amount=stored_trade["amount"], - min_amount=min_amount, - max_amount=max_amount, - ) + self.log.info("Amount mismatch - not in margins: {stored_trade['amount']} (min: {min_amount} / max: {max_amount}") self.irc.sendmsg(f"Amount mismatch - not in margins: {stored_trade['amount']} (min: {min_amount} / max: {max_amount}") return @@ -260,7 +241,7 @@ class Transactions(object): # Store the trade ID so we can release it easily to_store["trade_id"] = stored_trade["id"] if not state == "completed": - self.log.info("Storing incomplete trade: {id}", id=txid) + self.log.info(f"Storing incomplete trade: {txid}") r.hmset(f"tx.{txid}", to_store) # Don't procees further if state is not "completed" return @@ -270,7 +251,7 @@ class Transactions(object): self.ux.notify.notify_complete_trade(amount, currency) def release_funds(self, trade_id, reference): - self.log.info("All checks passed, releasing funds for {trade_id} {reference}", trade_id=trade_id, reference=reference) + self.log.info(f"All checks passed, releasing funds for {trade_id} {reference}") self.irc.sendmsg(f"All checks passed, releasing funds for {trade_id} / {reference}") rtrn = self.agora.release_funds(trade_id) self.agora.agora.contact_message_post(trade_id, "Thanks! Releasing now :)") @@ -300,14 +281,14 @@ class Transactions(object): "reference": reference, "provider": provider, } - self.log.info("Storing trade information: {info}", info=str(to_store)) + self.log.info(f"Storing trade information: {str(to_store)}") r.hmset(f"trade.{reference}", to_store) self.irc.sendmsg(f"Generated reference for {trade_id}: {reference}") self.ux.notify.notify_new_trade(amount, currency) if settings.Agora.Send == "1": self.agora.agora.contact_message_post(trade_id, f"Hi! When sending the payment please use reference code: {reference}") if existing_ref: - return convert(existing_ref) + return util.convert(existing_ref) else: return reference @@ -332,7 +313,7 @@ class Transactions(object): if stored_trade["currency"] == currency and float(stored_trade["amount"]) == float(amount): matching_refs.append(stored_trade) if len(matching_refs) != 1: - self.log.error("Find trade returned multiple results for TXID {txid}: {matching_refs}", txid=txid, matching_refs=matching_refs) + self.log.error(f"Find trade returned multiple results for TXID {txid}: {matching_refs}") return False return matching_refs[0] @@ -346,7 +327,7 @@ class Transactions(object): ref_keys = r.keys("trade.*.reference") for key in ref_keys: references.append(r.get(key)) - return convert(references) + return util.convert(references) def get_ref_map(self): """ @@ -357,9 +338,9 @@ class Transactions(object): references = {} ref_keys = r.keys("trade.*.reference") for key in ref_keys: - tx = convert(key).split(".")[1] + tx = util.convert(key).split(".")[1] references[tx] = r.get(key) - return convert(references) + return util.convert(references) def get_ref(self, reference): """ @@ -370,7 +351,7 @@ class Transactions(object): :rtype: dict """ ref_data = r.hgetall(f"trade.{reference}") - ref_data = convert(ref_data) + ref_data = util.convert(ref_data) if not ref_data: return False return ref_data @@ -394,7 +375,7 @@ class Transactions(object): """ for tx, reference in self.get_ref_map().items(): if reference not in references: - self.log.info("Archiving trade reference: {reference} / TX: {tx}", reference=reference, tx=tx) + self.log.info(f"Archiving trade reference: {reference} / TX: {tx}") r.rename(f"trade.{tx}.reference", f"archive.trade.{tx}.reference") r.rename(f"trade.{reference}", f"archive.trade.{reference}") @@ -408,7 +389,7 @@ class Transactions(object): """ refs = self.get_refs() for reference in refs: - ref_data = convert(r.hgetall(f"trade.{reference}")) + ref_data = util.convert(r.hgetall(f"trade.{reference}")) if not ref_data: continue if ref_data["id"] == tx: @@ -422,7 +403,7 @@ class Transactions(object): :return: trade ID :rtype: string """ - ref_data = convert(r.hgetall(f"trade.{reference}")) + ref_data = util.convert(r.hgetall(f"trade.{reference}")) if not ref_data: return False return ref_data["id"] @@ -448,12 +429,12 @@ class Transactions(object): # Get the BTC -> USD exchange rate btc_usd = self.agora.cg.get_price(ids="bitcoin", vs_currencies=["USD"]) - # Convert the Agora XMR total to USD - total_usd_agora_xmr = float(total_xmr_agora) * xmr_usd["monero"]["usd"] - # Convert the Agora BTC total to USD total_usd_agora_btc = float(total_btc_agora) * btc_usd["bitcoin"]["usd"] + # Convert the Agora XMR total to USD + total_usd_agora_xmr = float(total_xmr_agora) * xmr_usd["monero"]["usd"] + # Add it all up total_usd_agora = total_usd_agora_xmr + total_usd_agora_btc # total_usd = total_usd_agora + total_usd_revolut diff --git a/handler/util.py b/handler/util.py index f5a9a91..e544c8a 100644 --- a/handler/util.py +++ b/handler/util.py @@ -1,11 +1,98 @@ -# Twisted/Klein imports -from twisted.logger import Logger - # Other library imports from httpx import ReadTimeout, ReadError, RemoteProtocolError from datetime import datetime +import logging -log = Logger("util.global") +log = logging.getLogger("util") + + +BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8) + +# The background is set with 40 plus the number of the color, and the foreground with 30 + +# These are the sequences need to get colored ouput +RESET_SEQ = "\033[0m" +COLOR_SEQ = "\033[1;%dm" +BOLD_SEQ = "\033[1m" + + +def formatter_message(message, use_color=True): + if use_color: + message = message.replace("$RESET", RESET_SEQ).replace("$BOLD", BOLD_SEQ) + else: + message = message.replace("$RESET", "").replace("$BOLD", "") + return message + + +COLORS = {"WARNING": YELLOW, "INFO": WHITE, "DEBUG": BLUE, "CRITICAL": YELLOW, "ERROR": RED} + + +class ColoredFormatter(logging.Formatter): + def __init__(self, msg, use_color=True): + logging.Formatter.__init__(self, msg) + self.use_color = use_color + + def format(self, record): + levelname = record.levelname + if self.use_color and levelname in COLORS: + levelname_color = COLOR_SEQ % (30 + COLORS[levelname]) + levelname + RESET_SEQ + record.levelname = levelname_color + return logging.Formatter.format(self, record) + + +def get_logger(name): + + # Define the logging format + FORMAT = "%(asctime)s %(levelname)s $BOLD%(name)13s$RESET - %(message)s" + COLOR_FORMAT = formatter_message(FORMAT, True) + color_formatter = ColoredFormatter(COLOR_FORMAT) + # formatter = logging.Formatter( + + # Why is this so complicated? + ch = logging.StreamHandler() + ch.setLevel(logging.DEBUG) + # ch.setFormatter(formatter) + ch.setFormatter(color_formatter) + + # Define the logger on the base class + log = logging.getLogger(name) + + # Add the handler and stop it being silly and printing everything twice + log.addHandler(ch) + log.propagate = False + return log + + +class ColoredLogger(logging.Logger): + FORMAT = "[$BOLD%(name)-20s$RESET][%(levelname)-18s] %(message)s ($BOLD%(filename)s$RESET:%(lineno)d)" + COLOR_FORMAT = formatter_message(FORMAT, True) + + def __init__(self, name): + logging.Logger.__init__(self, name, logging.DEBUG) + + color_formatter = ColoredFormatter(self.COLOR_FORMAT) + + console = logging.StreamHandler() + console.setFormatter(color_formatter) + + self.addHandler(console) + return + + +class Base(object): + def __init__(self): + name = self.__class__.__name__ + + # Set up all the logging stuff + self._setup_logger(name) + + self.log.info("Class initialised") + + def _setup_logger(self, name): + """ + Set up the logging handlers. + """ + self.log = get_logger(name) def xmerge_attrs(init_map): @@ -78,10 +165,10 @@ def handle_exceptions(func): if "error_code" in rtrn["response"]["error"]: code = rtrn["response"]["error"]["error_code"] if not code == 136: - log.error("API error: {code}", code=code) + log.error(f"API error: {code}") return False else: - log.error("API error: {code}", code=rtrn["response"]["error"]) + log.error(f"API error: {rtrn['response']['error']}") return False return rtrn diff --git a/handler/ux/__init__.py b/handler/ux/__init__.py index b1e1fd3..27899c8 100644 --- a/handler/ux/__init__.py +++ b/handler/ux/__init__.py @@ -1,6 +1,3 @@ -# Twisted/Klein imports -from twisted.logger import Logger - # Other library imports # import requests # from json import dumps @@ -20,7 +17,7 @@ class UX(object): """ def __init__(self): - self.log = Logger("ux") + super().__init__() self.irc = ux.irc.bot() self.notify = ux.notify.Notify() diff --git a/handler/ux/commands.py b/handler/ux/commands.py index 6d6ff65..2a3df34 100644 --- a/handler/ux/commands.py +++ b/handler/ux/commands.py @@ -447,5 +447,32 @@ class IRCCommands(object): @staticmethod def run(cmd, spl, length, authed, msg, agora, tx, ux): - auth_url = agora.truelayer.create_auth_url() + auth_url = tx.truelayer.create_auth_url() msg(f"Auth URL: {auth_url}") + + class accounts(object): + name = "accounts" + authed = True + helptext = "Get a list of acccounts." + + @staticmethod + def run(cmd, spl, length, authed, msg, agora, tx, ux): + accounts = tx.sinks.truelayer.get_accounts() + msg(dumps(accounts)) + + class transactions(object): + name = "transactions" + authed = True + helptext = "Get a list of transactions. Usage: transactions " + + @staticmethod + def run(cmd, spl, length, authed, msg, agora, tx, ux): + if length == 2: + account_id = spl[1] + transactions = tx.sinks.truelayer.get_transactions(account_id) + for transaction in transactions: + timestamp = transaction["timestamp"] + amount = transaction["amount"] + currency = transaction["currency"] + recipient = transaction["counter_party_preferred_name"] + msg(f"{timestamp} {amount}{currency} {recipient}") diff --git a/handler/ux/irc.py b/handler/ux/irc.py index 061c52e..fb9f53b 100644 --- a/handler/ux/irc.py +++ b/handler/ux/irc.py @@ -1,5 +1,4 @@ # Twisted/Klein imports -from twisted.logger import Logger from twisted.words.protocols import irc from twisted.internet import protocol, reactor, ssl from twisted.internet.task import deferLater @@ -7,6 +6,7 @@ from twisted.internet.task import deferLater # Project imports from settings import settings from ux.commands import IRCCommands +import util class IRCBot(irc.IRCClient): @@ -106,7 +106,7 @@ class IRCBot(irc.IRCClient): Called when we have signed on to IRC. Join our channel. """ - self.log.info("Signed on as %s" % (self.nickname)) + self.log.info(f"Signed on as {self.nickname}") deferLater(reactor, 2, self.join, self.channel) def joined(self, channel): @@ -118,7 +118,7 @@ class IRCBot(irc.IRCClient): :type channel: string """ self.agora.setup_loop() - self.log.info("Joined channel %s" % (channel)) + self.log.info(f"Joined channel {channel}") def privmsg(self, user, channel, msg): """ @@ -141,7 +141,7 @@ class IRCBot(irc.IRCClient): ident = user.split("!")[1] ident = ident.split("@")[0] - self.log.info("(%s) %s: %s" % (channel, user, msg)) + self.log.info(f"({channel}) {user}: {msg}") if msg[0] == self.prefix: if len(msg) > 1: if msg.split()[0] != "!": @@ -171,7 +171,8 @@ class IRCBot(irc.IRCClient): class IRCBotFactory(protocol.ClientFactory): def __init__(self): - self.log = Logger("irc") + self.log = util.get_logger("IRC") + self.log.info("Class initialised") def sendmsg(self, msg): """ @@ -180,7 +181,7 @@ class IRCBotFactory(protocol.ClientFactory): if self.client: self.client.msg(self.client.channel, msg) else: - self.log.error("Trying to send a message without connected client: {msg}", msg=msg) + self.log.error(f"Trying to send a message without connected client: {msg}") return def buildProtocol(self, addr): @@ -189,6 +190,7 @@ class IRCBotFactory(protocol.ClientFactory): Passes through the Agora instance to IRC. :return: IRCBot Protocol instance """ + # Pass through the logger prcol = IRCBot(self.log) self.client = prcol setattr(self.client, "agora", self.agora) @@ -205,7 +207,7 @@ class IRCBotFactory(protocol.ClientFactory): :type connector: object :type reason: string """ - self.log.error("Lost connection: {reason}, reconnecting", reason=reason) + self.log.error(f"Lost connection: {reason}, reconnecting") connector.connect() def clientConnectionFailed(self, connector, reason): @@ -216,7 +218,7 @@ class IRCBotFactory(protocol.ClientFactory): :type connector: object :type reason: string """ - self.log.error("Could not connect: {reason}", reason=reason) + self.log.error(f"Could not connect: {reason}") connector.connect() diff --git a/handler/ux/notify.py b/handler/ux/notify.py index f78374a..255e5c6 100644 --- a/handler/ux/notify.py +++ b/handler/ux/notify.py @@ -1,21 +1,16 @@ -# Twisted/Klein imports -from twisted.logger import Logger - # Other library imports import requests # Project imports from settings import settings +import util -class Notify(object): +class Notify(util.Base): """ Class to handle more robust notifications. """ - def __init__(self): - self.log = Logger("notify") - def sendmsg(self, msg, title=None, priority=None, tags=None): headers = {"Title": "Bot"} if title: