Use the Python logger instead of the Twisted one

This commit is contained in:
Mark Veidemanis 2022-03-05 21:52:31 +00:00
parent 80c696ef73
commit 539d6f1fbb
Signed by: m
GPG Key ID: 5ACFCEED46C0904F
15 changed files with 224 additions and 151 deletions

View File

@ -1,5 +1,4 @@
# Twisted/Klein imports # Twisted/Klein imports
from twisted.logger import Logger
from twisted.internet.task import LoopingCall from twisted.internet.task import LoopingCall
from twisted.internet.threads import deferToThread from twisted.internet.threads import deferToThread
@ -16,10 +15,8 @@ from datetime import datetime
from settings import settings from settings import settings
import util import util
log = Logger("agora.global")
class Agora(util.Base):
class Agora(object):
""" """
AgoraDesk API handler. AgoraDesk API handler.
""" """
@ -29,7 +26,7 @@ class Agora(object):
Initialise the AgoraDesk and CurrencyRates APIs. Initialise the AgoraDesk and CurrencyRates APIs.
Initialise the last_dash storage for detecting new trades. Initialise the last_dash storage for detecting new trades.
""" """
self.log = Logger("agora") super().__init__()
self.agora = AgoraDesk(settings.Agora.Token) self.agora = AgoraDesk(settings.Agora.Token)
self.cr = CurrencyRates() # TODO: remove this and defer to money self.cr = CurrencyRates() # TODO: remove this and defer to money
self.cg = CoinGeckoAPI() # 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(): if not dash.items():
return False return False
if "data" not in dash["response"].keys(): 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 return dash_tmp
if dash["response"]["data"]["contact_count"] > 0: if dash["response"]["data"]["contact_count"] > 0:
for contact in dash["response"]["data"]["contact_list"]: for contact in dash["response"]["data"]["contact_list"]:
@ -166,7 +163,7 @@ class Agora(object):
if not messages["success"]: if not messages["success"]:
return False return False
if "data" not in messages["response"]: 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 return False
open_tx = self.tx.get_ref_map().keys() open_tx = self.tx.get_ref_map().keys()
for message in messages["response"]["data"]["message_list"]: for message in messages["response"]["data"]["message_list"]:
@ -429,20 +426,15 @@ class Agora(object):
continue continue
else: else:
if "error_code" not in rtrn["response"]["error"]: 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 return
if rtrn["response"]["error"]["error_code"] == 429: if rtrn["response"]["error"]["error_code"] == 429:
throttled += 1 throttled += 1
sleep_time = pow(throttled, float(settings.Agora.SleepExponent)) sleep_time = pow(throttled, float(settings.Agora.SleepExponent))
self.log.info( self.log.info(f"Throttled {throttled} times while updating {ad_id}, sleeping for {sleep_time} seconds")
"Throttled {x} times while updating {id}, sleeping for {sleep} seconds",
x=throttled,
id=ad_id,
sleep=sleep_time,
)
# We're running in a thread, so this is fine # We're running in a thread, so this is fine
sleep(sleep_time) 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 continue
iterations += 1 iterations += 1
@ -645,9 +637,7 @@ class Agora(object):
if not float(wallet_xmr) > profit_usd_in_xmr: if not float(wallet_xmr) > profit_usd_in_xmr:
# Not enough funds to withdraw # Not enough funds to withdraw
self.log.error( self.log.error(f"Not enough funds to withdraw {profit_usd_in_xmr}, as wallet only contains {wallet_xmr}")
"Not enough funds to withdraw {profit}, as wallet only contains {wallet}", profit=profit_usd_in_xmr, wallet=wallet_xmr
)
self.irc.sendmsg(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 return

View File

@ -12,6 +12,9 @@ from typing import Union
import arrow import arrow
import httpx import httpx
# Project imports
import util
__author__ = "marvin8" __author__ = "marvin8"
__copyright__ = "(C) 2021 https://codeberg.org/MarvinsCryptoTools/agoradesk_py" __copyright__ = "(C) 2021 https://codeberg.org/MarvinsCryptoTools/agoradesk_py"
__version__ = "0.1.0" __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.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
logging.getLogger("requests.packages.urllib3").setLevel(logging.INFO) logging.getLogger("requests.packages.urllib3").setLevel(logging.INFO)
logging.getLogger("urllib3.connectionpool").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/" URI_API = "https://agoradesk.com/api/v1/"

View File

@ -1,6 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# Twisted/Klein imports # Twisted/Klein imports
from twisted.logger import Logger
from twisted.internet import reactor from twisted.internet import reactor
from klein import Klein from klein import Klein
@ -41,33 +40,20 @@ def cleanup(sig, frame):
signal(SIGINT, cleanup) # Handle Ctrl-C and run the cleanup routine signal(SIGINT, cleanup) # Handle Ctrl-C and run the cleanup routine
def convert(data): class WebApp(util.Base):
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):
""" """
Our Klein webapp. Our Klein webapp.
""" """
app = Klein() app = Klein()
def __init__(self):
self.log = Logger("webapp")
@app.route("/callback", methods=["POST"]) @app.route("/callback", methods=["POST"])
def callback(self, request): def callback(self, request):
content = request.content.read() content = request.content.read()
try: try:
parsed = loads(content) parsed = loads(content)
except JSONDecodeError: 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) return dumps(False)
self.log.info("Callback received: {parsed}", parsed=parsed["data"]["id"]) self.log.info("Callback received: {parsed}", parsed=parsed["data"]["id"])
# self.tx.transaction(parsed) # self.tx.transaction(parsed)
@ -76,19 +62,19 @@ class WebApp(object):
# set up another connection to a bank # set up another connection to a bank
@app.route("/signin", methods=["GET"]) @app.route("/signin", methods=["GET"])
def signin(self, request): def signin(self, request):
auth_url = self.truelayer.create_auth_url() auth_url = self.sinks.truelayer.create_auth_url()
return f'Please sign in <a href="{auth_url}" target="_blank">here.</a>' return f'Please sign in <a href="{auth_url}" target="_blank">here.</a>'
# endpoint called after we finish setting up a connection above # endpoint called after we finish setting up a connection above
@app.route("/callback-truelayer", methods=["POST"]) @app.route("/callback-truelayer", methods=["POST"])
def signin_callback(self, request): def signin_callback(self, request):
code = request.args[b"code"] code = request.args[b"code"]
self.truelayer.handle_authcode_received(code) self.sinks.truelayer.handle_authcode_received(code)
return dumps(True) return dumps(True)
@app.route("/accounts", methods=["GET"]) @app.route("/accounts", methods=["GET"])
def balance(self, request): def balance(self, request):
accounts = self.truelayer.get_accounts() accounts = self.sinks.truelayer.get_accounts()
return dumps(accounts, indent=2) return dumps(accounts, indent=2)

View File

@ -1,21 +1,16 @@
# Twisted/Klein imports
from twisted.logger import Logger
# Other library imports # Other library imports
from json import loads from json import loads
# Project imports # Project imports
from settings import settings from settings import settings
import util
class Markets(object): class Markets(util.Base):
""" " """ "
Markets handler for generic market functions. Markets handler for generic market functions.
""" """
def __init__(self):
self.log = Logger("markets")
def get_all_assets(self): def get_all_assets(self):
assets = loads(settings.Agora.AssetList) assets = loads(settings.Agora.AssetList)
return assets return assets

View File

@ -1,15 +1,13 @@
# Twisted/Klein imports
from twisted.logger import Logger
# Other library imports # Other library imports
from pycoingecko import CoinGeckoAPI from pycoingecko import CoinGeckoAPI
from forex_python.converter import CurrencyRates from forex_python.converter import CurrencyRates
# Project imports # Project imports
from settings import settings 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. Generic class for handling money-related matters that aren't Revolut or Agora.
""" """
@ -20,7 +18,7 @@ class Money(object):
Set the logger. Set the logger.
Initialise the CoinGecko API. Initialise the CoinGecko API.
""" """
self.log = Logger("money") super().__init__()
self.cr = CurrencyRates() self.cr = CurrencyRates()
self.cg = CoinGeckoAPI() self.cg = CoinGeckoAPI()
@ -41,6 +39,7 @@ class Money(object):
price = float(ad[2]) price = float(ad[2])
rate = round(price / base_currency_price, 2) rate = round(price / base_currency_price, 2)
ad.append(rate) ad.append(rate)
# TODO: sort?
return sorted(ads, key=lambda x: x[2]) return sorted(ads, key=lambda x: x[2])
def get_rates_all(self): def get_rates_all(self):

View File

@ -1,6 +1,3 @@
# Twisted/Klein imports
from twisted.logger import Logger
# Other library imports # Other library imports
# import requests # import requests
# from json import dumps # from json import dumps
@ -10,15 +7,16 @@ from twisted.logger import Logger
import sinks.fidor import sinks.fidor
import sinks.nordigen import sinks.nordigen
import sinks.truelayer import sinks.truelayer
import util
class Sinks(object): class Sinks(util.Base):
""" """
Class to manage calls to various sinks. Class to manage calls to various sinks.
""" """
def __init__(self): def __init__(self):
self.log = Logger("sinks") super().__init__()
self.fidor = sinks.fidor.Fidor() self.fidor = sinks.fidor.Fidor()
self.nordigen = sinks.nordigen.Nordigen() self.nordigen = sinks.nordigen.Nordigen()
self.truelayer = sinks.truelayer.TrueLayer() self.truelayer = sinks.truelayer.TrueLayer()

View File

@ -1,5 +1,4 @@
# Twisted/Klein imports # Twisted/Klein imports
from twisted.logger import Logger
# Other library imports # Other library imports
# import requests # import requests
@ -7,16 +6,14 @@ from twisted.logger import Logger
# Project imports # Project imports
# from settings import settings # from settings import settings
import util
class Fidor(object): class Fidor(util.Base):
""" """
Class to manage calls to the Fidor API. Class to manage calls to the Fidor API.
""" """
def __init__(self):
self.log = Logger("fidor")
def authorize(self): def authorize(self):
""" """
Perform initial authorization against Fidor API. Perform initial authorization against Fidor API.

View File

@ -1,6 +1,3 @@
# Twisted/Klein imports
from twisted.logger import Logger
# Other library imports # Other library imports
import requests import requests
from json import dumps from json import dumps
@ -8,15 +5,16 @@ from simplejson.errors import JSONDecodeError
# Project imports # Project imports
from settings import settings from settings import settings
import util
class Nordigen(object): class Nordigen(util.Base):
""" """
Class to manage calls to Open Banking APIs through Nordigen. Class to manage calls to Open Banking APIs through Nordigen.
""" """
def __init__(self): def __init__(self):
self.log = Logger("nordigen") super().__init__()
self.token = None self.token = None
self.get_access_token() self.get_access_token()
@ -36,11 +34,11 @@ class Nordigen(object):
try: try:
parsed = r.json() parsed = r.json()
except JSONDecodeError: 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 return False
if "access" in parsed: if "access" in parsed:
self.token = parsed["access"] 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): def get_institutions(self, country, filter_name=None):
""" """
@ -58,7 +56,7 @@ class Nordigen(object):
try: try:
parsed = r.json() parsed = r.json()
except JSONDecodeError: 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 return False
new_list = [] new_list = []
if filter_name: if filter_name:

View File

@ -1,5 +1,4 @@
# Twisted/Klein imports # Twisted/Klein imports
from twisted.logger import Logger
from twisted.internet.task import LoopingCall from twisted.internet.task import LoopingCall
# Other library imports # Other library imports
@ -10,15 +9,16 @@ import urllib
# Project imports # Project imports
from settings import settings from settings import settings
import util
class TrueLayer(object): class TrueLayer(util.Base):
""" """
Class to manage calls to Open Banking APIs through TrueLayer. Class to manage calls to Open Banking APIs through TrueLayer.
""" """
def __init__(self): def __init__(self):
self.log = Logger("truelayer") super().__init__()
self.token = None self.token = None
self.lc = LoopingCall(self.get_new_token) self.lc = LoopingCall(self.get_new_token)
self.lc.start(int(settings.TrueLayer.RefreshSec)) self.lc.start(int(settings.TrueLayer.RefreshSec))
@ -58,7 +58,7 @@ class TrueLayer(object):
settings.TrueLayer.AuthCode = authcode settings.TrueLayer.AuthCode = authcode
settings.write() settings.write()
self.token = parsed["access_token"] 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): def get_new_token(self):
""" """
@ -81,7 +81,7 @@ class TrueLayer(object):
if r.status_code == 200: if r.status_code == 200:
if "access_token" in parsed.keys(): if "access_token" in parsed.keys():
self.token = parsed["access_token"] self.token = parsed["access_token"]
self.log.info("Refreshed access token - TrueLayer") self.log.info("Refreshed access token")
return True return True
else: else:
self.log.error(f"Token refresh didn't contain access token: {parsed}", parsed=parsed) self.log.error(f"Token refresh didn't contain access token: {parsed}", parsed=parsed)
@ -100,7 +100,25 @@ class TrueLayer(object):
try: try:
parsed = r.json() parsed = r.json()
except JSONDecodeError: 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 False
return parsed 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"]

View File

@ -1,5 +1,4 @@
# Twisted/Klein imports # Twisted/Klein imports
from twisted.logger import Logger
from twisted.internet.task import LoopingCall from twisted.internet.task import LoopingCall
from twisted.internet.threads import deferToThread from twisted.internet.threads import deferToThread
@ -15,7 +14,7 @@ import logging
# Project imports # Project imports
from settings import settings from settings import settings
from db import r from db import r
from util import convert import util
# TODO: secure ES traffic properly # TODO: secure ES traffic properly
urllib3.disable_warnings() urllib3.disable_warnings()
@ -26,7 +25,7 @@ tracer = logging.getLogger("elastic_transport.transport")
tracer.setLevel(logging.CRITICAL) tracer.setLevel(logging.CRITICAL)
class Transactions(object): class Transactions(util.Base):
""" """
Handler class for incoming Revolut transactions. Handler class for incoming Revolut transactions.
""" """
@ -36,7 +35,7 @@ class Transactions(object):
Initialise the Transaction object. Initialise the Transaction object.
Set the logger. Set the logger.
""" """
self.log = Logger("transactions") super().__init__()
if settings.ES.Enabled == "1": if settings.ES.Enabled == "1":
self.es = Elasticsearch( self.es = Elasticsearch(
f"https://{settings.ES.Host}:9200", f"https://{settings.ES.Host}:9200",
@ -89,10 +88,10 @@ class Transactions(object):
# stored_trade here is actually TX # stored_trade here is actually TX
stored_trade = r.hgetall(f"tx.{txid}") stored_trade = r.hgetall(f"tx.{txid}")
if not stored_trade: 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 return
print("BEFORE CONVERT STORED TRADE", stored_trade) print("BEFORE CONVERT STORED TRADE", stored_trade)
stored_trade = convert(stored_trade) stored_trade = util.convert(stored_trade)
if "old_state" in inside: if "old_state" in inside:
if "new_state" in inside: if "new_state" in inside:
# We don't care unless we're being told a transaction is now completed # 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) r.hmset(f"tx.{txid}", stored_trade)
# Check it's all been previously validated # Check it's all been previously validated
if "valid" not in stored_trade: 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 return
if stored_trade["valid"] == "1": if stored_trade["valid"] == "1":
# Make it invalid immediately, as we're going to release now # Make it invalid immediately, as we're going to release now
@ -124,7 +123,7 @@ class Transactions(object):
else: else:
txtype = inside["type"] txtype = inside["type"]
if txtype == "card_payment": if txtype == "card_payment":
self.log.info("Ignoring card payment: {id}", id=txid) self.log.info(f"Ignoring card payment: {txid}")
return return
state = inside["state"] state = inside["state"]
@ -142,7 +141,7 @@ class Transactions(object):
amount = leg["amount"] amount = leg["amount"]
if amount <= 0: 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 return
currency = leg["currency"] currency = leg["currency"]
description = leg["description"] description = leg["description"]
@ -161,7 +160,7 @@ class Transactions(object):
"description": description, "description": description,
"valid": 0, # All checks passed and we can release escrow? "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}") self.irc.sendmsg(f"AUTO Incoming transaction: {amount}{currency} ({reference}) - {state} - {description}")
# Partial reference implementation # Partial reference implementation
# Account for silly people not removing the default string # 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 # Get all parts of the given reference split that match the existing references
stored_trade_reference = set(existing_refs).intersection(set(ref_split)) stored_trade_reference = set(existing_refs).intersection(set(ref_split))
if len(stored_trade_reference) > 1: 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}") self.irc.sendmsg(f"Multiple references valid for TXID {txid}: {reference}")
return return
@ -181,21 +180,16 @@ class Transactions(object):
# Amount/currency lookup implementation # Amount/currency lookup implementation
if not stored_trade_reference: 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}") 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) # Try checking just amount and currency, as some people (usually people buying small amounts)
# are unable to put in a reference properly. # 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}") self.irc.sendmsg(f"Checking against amount and currency for TXID {txid}")
stored_trade = self.find_trade(txid, currency, amount) stored_trade = self.find_trade(txid, currency, amount)
if not stored_trade: if not stored_trade:
self.log.info( self.log.info(f"Failed to get reference by amount and currency: {txid} {currency} {amount}")
"Failed to get reference by amount and currency: {txid} {currency} {amount}",
txid=txid,
currency=currency,
amount=amount,
)
self.irc.sendmsg(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 return
if currency == "USD": if currency == "USD":
@ -215,7 +209,7 @@ class Transactions(object):
if not stored_trade: if not stored_trade:
stored_trade = self.get_ref(stored_trade_reference.pop()) stored_trade = self.get_ref(stored_trade_reference.pop())
if not stored_trade: 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}") self.irc.sendmsg(f"No reference in DB for {reference}")
return return
@ -224,11 +218,7 @@ class Transactions(object):
# Make sure it was sent in the expected currency # Make sure it was sent in the expected currency
if not stored_trade["currency"] == currency: if not stored_trade["currency"] == currency:
self.log.info( self.log.info(f"Currency mismatch, Agora: {stored_trade['currency']} / Sink: {currency}")
"Currency mismatch, Agora: {currency_agora} / Sink: {currency}",
currency_agora=stored_trade["currency"],
currency=currency,
)
self.irc.sendmsg(f"Currency mismatch, Agora: {stored_trade['currency']} / Sink: {currency}") self.irc.sendmsg(f"Currency mismatch, Agora: {stored_trade['currency']} / Sink: {currency}")
return return
@ -238,19 +228,10 @@ class Transactions(object):
return return
# If the amount does not match exactly, get the min and max values for our given acceptable margins for trades # 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"]) min_amount, max_amount = self.money.get_acceptable_margins(currency, stored_trade["amount"])
self.log.info( self.log.info(f"Amount does not match exactly, trying with margins: min: {min_amount} / max: {max_amount}")
"Amount does not match exactly, trying with margins: min: {min_amount} / max: {max_amount}",
min_amount=min_amount,
max_amount=max_amount,
)
self.irc.sendmsg(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: if not min_amount < amount < max_amount:
self.log.info( self.log.info("Amount mismatch - not in margins: {stored_trade['amount']} (min: {min_amount} / max: {max_amount}")
"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.irc.sendmsg(f"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 return
@ -260,7 +241,7 @@ class Transactions(object):
# Store the trade ID so we can release it easily # Store the trade ID so we can release it easily
to_store["trade_id"] = stored_trade["id"] to_store["trade_id"] = stored_trade["id"]
if not state == "completed": 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) r.hmset(f"tx.{txid}", to_store)
# Don't procees further if state is not "completed" # Don't procees further if state is not "completed"
return return
@ -270,7 +251,7 @@ class Transactions(object):
self.ux.notify.notify_complete_trade(amount, currency) self.ux.notify.notify_complete_trade(amount, currency)
def release_funds(self, trade_id, reference): 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}") self.irc.sendmsg(f"All checks passed, releasing funds for {trade_id} / {reference}")
rtrn = self.agora.release_funds(trade_id) rtrn = self.agora.release_funds(trade_id)
self.agora.agora.contact_message_post(trade_id, "Thanks! Releasing now :)") self.agora.agora.contact_message_post(trade_id, "Thanks! Releasing now :)")
@ -300,14 +281,14 @@ class Transactions(object):
"reference": reference, "reference": reference,
"provider": provider, "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) r.hmset(f"trade.{reference}", to_store)
self.irc.sendmsg(f"Generated reference for {trade_id}: {reference}") self.irc.sendmsg(f"Generated reference for {trade_id}: {reference}")
self.ux.notify.notify_new_trade(amount, currency) self.ux.notify.notify_new_trade(amount, currency)
if settings.Agora.Send == "1": if settings.Agora.Send == "1":
self.agora.agora.contact_message_post(trade_id, f"Hi! When sending the payment please use reference code: {reference}") self.agora.agora.contact_message_post(trade_id, f"Hi! When sending the payment please use reference code: {reference}")
if existing_ref: if existing_ref:
return convert(existing_ref) return util.convert(existing_ref)
else: else:
return reference return reference
@ -332,7 +313,7 @@ class Transactions(object):
if stored_trade["currency"] == currency and float(stored_trade["amount"]) == float(amount): if stored_trade["currency"] == currency and float(stored_trade["amount"]) == float(amount):
matching_refs.append(stored_trade) matching_refs.append(stored_trade)
if len(matching_refs) != 1: 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 False
return matching_refs[0] return matching_refs[0]
@ -346,7 +327,7 @@ class Transactions(object):
ref_keys = r.keys("trade.*.reference") ref_keys = r.keys("trade.*.reference")
for key in ref_keys: for key in ref_keys:
references.append(r.get(key)) references.append(r.get(key))
return convert(references) return util.convert(references)
def get_ref_map(self): def get_ref_map(self):
""" """
@ -357,9 +338,9 @@ class Transactions(object):
references = {} references = {}
ref_keys = r.keys("trade.*.reference") ref_keys = r.keys("trade.*.reference")
for key in ref_keys: for key in ref_keys:
tx = convert(key).split(".")[1] tx = util.convert(key).split(".")[1]
references[tx] = r.get(key) references[tx] = r.get(key)
return convert(references) return util.convert(references)
def get_ref(self, reference): def get_ref(self, reference):
""" """
@ -370,7 +351,7 @@ class Transactions(object):
:rtype: dict :rtype: dict
""" """
ref_data = r.hgetall(f"trade.{reference}") ref_data = r.hgetall(f"trade.{reference}")
ref_data = convert(ref_data) ref_data = util.convert(ref_data)
if not ref_data: if not ref_data:
return False return False
return ref_data return ref_data
@ -394,7 +375,7 @@ class Transactions(object):
""" """
for tx, reference in self.get_ref_map().items(): for tx, reference in self.get_ref_map().items():
if reference not in references: 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.{tx}.reference", f"archive.trade.{tx}.reference")
r.rename(f"trade.{reference}", f"archive.trade.{reference}") r.rename(f"trade.{reference}", f"archive.trade.{reference}")
@ -408,7 +389,7 @@ class Transactions(object):
""" """
refs = self.get_refs() refs = self.get_refs()
for reference in 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: if not ref_data:
continue continue
if ref_data["id"] == tx: if ref_data["id"] == tx:
@ -422,7 +403,7 @@ class Transactions(object):
:return: trade ID :return: trade ID
:rtype: string :rtype: string
""" """
ref_data = convert(r.hgetall(f"trade.{reference}")) ref_data = util.convert(r.hgetall(f"trade.{reference}"))
if not ref_data: if not ref_data:
return False return False
return ref_data["id"] return ref_data["id"]
@ -448,12 +429,12 @@ class Transactions(object):
# Get the BTC -> USD exchange rate # Get the BTC -> USD exchange rate
btc_usd = self.agora.cg.get_price(ids="bitcoin", vs_currencies=["USD"]) 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 # Convert the Agora BTC total to USD
total_usd_agora_btc = float(total_btc_agora) * btc_usd["bitcoin"]["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 # Add it all up
total_usd_agora = total_usd_agora_xmr + total_usd_agora_btc total_usd_agora = total_usd_agora_xmr + total_usd_agora_btc
# total_usd = total_usd_agora + total_usd_revolut # total_usd = total_usd_agora + total_usd_revolut

View File

@ -1,11 +1,98 @@
# Twisted/Klein imports
from twisted.logger import Logger
# Other library imports # Other library imports
from httpx import ReadTimeout, ReadError, RemoteProtocolError from httpx import ReadTimeout, ReadError, RemoteProtocolError
from datetime import datetime 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): def xmerge_attrs(init_map):
@ -78,10 +165,10 @@ def handle_exceptions(func):
if "error_code" in rtrn["response"]["error"]: if "error_code" in rtrn["response"]["error"]:
code = rtrn["response"]["error"]["error_code"] code = rtrn["response"]["error"]["error_code"]
if not code == 136: if not code == 136:
log.error("API error: {code}", code=code) log.error(f"API error: {code}")
return False return False
else: else:
log.error("API error: {code}", code=rtrn["response"]["error"]) log.error(f"API error: {rtrn['response']['error']}")
return False return False
return rtrn return rtrn

View File

@ -1,6 +1,3 @@
# Twisted/Klein imports
from twisted.logger import Logger
# Other library imports # Other library imports
# import requests # import requests
# from json import dumps # from json import dumps
@ -20,7 +17,7 @@ class UX(object):
""" """
def __init__(self): def __init__(self):
self.log = Logger("ux") super().__init__()
self.irc = ux.irc.bot() self.irc = ux.irc.bot()
self.notify = ux.notify.Notify() self.notify = ux.notify.Notify()

View File

@ -447,5 +447,32 @@ class IRCCommands(object):
@staticmethod @staticmethod
def run(cmd, spl, length, authed, msg, agora, tx, ux): 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}") 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 <account_id>"
@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}")

View File

@ -1,5 +1,4 @@
# Twisted/Klein imports # Twisted/Klein imports
from twisted.logger import Logger
from twisted.words.protocols import irc from twisted.words.protocols import irc
from twisted.internet import protocol, reactor, ssl from twisted.internet import protocol, reactor, ssl
from twisted.internet.task import deferLater from twisted.internet.task import deferLater
@ -7,6 +6,7 @@ from twisted.internet.task import deferLater
# Project imports # Project imports
from settings import settings from settings import settings
from ux.commands import IRCCommands from ux.commands import IRCCommands
import util
class IRCBot(irc.IRCClient): class IRCBot(irc.IRCClient):
@ -106,7 +106,7 @@ class IRCBot(irc.IRCClient):
Called when we have signed on to IRC. Called when we have signed on to IRC.
Join our channel. 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) deferLater(reactor, 2, self.join, self.channel)
def joined(self, channel): def joined(self, channel):
@ -118,7 +118,7 @@ class IRCBot(irc.IRCClient):
:type channel: string :type channel: string
""" """
self.agora.setup_loop() self.agora.setup_loop()
self.log.info("Joined channel %s" % (channel)) self.log.info(f"Joined channel {channel}")
def privmsg(self, user, channel, msg): def privmsg(self, user, channel, msg):
""" """
@ -141,7 +141,7 @@ class IRCBot(irc.IRCClient):
ident = user.split("!")[1] ident = user.split("!")[1]
ident = ident.split("@")[0] 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 msg[0] == self.prefix:
if len(msg) > 1: if len(msg) > 1:
if msg.split()[0] != "!": if msg.split()[0] != "!":
@ -171,7 +171,8 @@ class IRCBot(irc.IRCClient):
class IRCBotFactory(protocol.ClientFactory): class IRCBotFactory(protocol.ClientFactory):
def __init__(self): def __init__(self):
self.log = Logger("irc") self.log = util.get_logger("IRC")
self.log.info("Class initialised")
def sendmsg(self, msg): def sendmsg(self, msg):
""" """
@ -180,7 +181,7 @@ class IRCBotFactory(protocol.ClientFactory):
if self.client: if self.client:
self.client.msg(self.client.channel, msg) self.client.msg(self.client.channel, msg)
else: 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 return
def buildProtocol(self, addr): def buildProtocol(self, addr):
@ -189,6 +190,7 @@ class IRCBotFactory(protocol.ClientFactory):
Passes through the Agora instance to IRC. Passes through the Agora instance to IRC.
:return: IRCBot Protocol instance :return: IRCBot Protocol instance
""" """
# Pass through the logger
prcol = IRCBot(self.log) prcol = IRCBot(self.log)
self.client = prcol self.client = prcol
setattr(self.client, "agora", self.agora) setattr(self.client, "agora", self.agora)
@ -205,7 +207,7 @@ class IRCBotFactory(protocol.ClientFactory):
:type connector: object :type connector: object
:type reason: string :type reason: string
""" """
self.log.error("Lost connection: {reason}, reconnecting", reason=reason) self.log.error(f"Lost connection: {reason}, reconnecting")
connector.connect() connector.connect()
def clientConnectionFailed(self, connector, reason): def clientConnectionFailed(self, connector, reason):
@ -216,7 +218,7 @@ class IRCBotFactory(protocol.ClientFactory):
:type connector: object :type connector: object
:type reason: string :type reason: string
""" """
self.log.error("Could not connect: {reason}", reason=reason) self.log.error(f"Could not connect: {reason}")
connector.connect() connector.connect()

View File

@ -1,21 +1,16 @@
# Twisted/Klein imports
from twisted.logger import Logger
# Other library imports # Other library imports
import requests import requests
# Project imports # Project imports
from settings import settings from settings import settings
import util
class Notify(object): class Notify(util.Base):
""" """
Class to handle more robust notifications. Class to handle more robust notifications.
""" """
def __init__(self):
self.log = Logger("notify")
def sendmsg(self, msg, title=None, priority=None, tags=None): def sendmsg(self, msg, title=None, priority=None, tags=None):
headers = {"Title": "Bot"} headers = {"Title": "Bot"}
if title: if title: