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: