diff --git a/app.py b/app.py index faf51d2..845f9a9 100644 --- a/app.py +++ b/app.py @@ -119,6 +119,7 @@ def proxy(slug): .strftime('%d.%m.%y %H:%M UTC'))) + # socket endpoint to receive payment event @socketio.on('await_payment') def await_payment(data): @@ -128,6 +129,14 @@ def await_payment(data): iota_listener.socket_session_ids[user_token_hash] = request.sid +# socket endpoint for manual check on payment +@socketio.on('check_payment') +def await_payment(data): + + socketio.start_background_task(iota_listener.manual_payment_check, data['iota_address'], data['user_token_hash']) + + + if __name__ == '__main__': diff --git a/data.py b/data.py index bd837a5..5074436 100644 --- a/data.py +++ b/data.py @@ -55,8 +55,8 @@ def user_token_hash_exists(user_token_hash): def user_token_hash_valid(user_token_hash): return datetime.fromisoformat(paid_db[user_token_hash]) > datetime.utcnow() -def add_to_paid_db(user_token_hash, lifetime): - paid_db[user_token_hash] = (datetime.utcnow() + timedelta(hours = lifetime)).isoformat() +def add_to_paid_db(user_token_hash, exp_time): + paid_db[user_token_hash] = exp_time.isoformat() def pop_from_paid_db(user_token_hash): return paid_db.pop(user_token_hash) diff --git a/iota.py b/iota.py index fb78b19..517e836 100644 --- a/iota.py +++ b/iota.py @@ -1,7 +1,9 @@ +from datetime import date, datetime import iota_client import queue import json import logging +from datetime import datetime, timedelta from dotenv import load_dotenv from data import get_iota_listening_addresses, add_to_paid_db, is_own_address import os @@ -41,6 +43,9 @@ def __init__(self, socketio): # link user_token_hashes to session ids self.socket_session_ids = {} + # manual payment checks + self.manual_payment_checks = set() + # create the iota client self.client = iota_client.Client(nodes_name_password=[[node_url]], mqtt_broker_options=broker_options) @@ -63,6 +68,10 @@ def start(self): def add_listening_address(self, iota_address): + ''' + Adds an iota_address to the topics of the mqtt listener + ''' + self.client.subscribe_topic('addresses/%s/outputs' % iota_address, self.on_mqtt_event) @@ -79,16 +88,13 @@ def mqtt_worker(self): message = self.client.get_message_data(json.loads(event['payload'])['messageId']) - if self.check_payment(message): + if self.payment_valid(message): # this must be easier to access within value transfers user_token_hash = bytes(message['payload']['transaction'][0]['essence']['payload']['indexation'][0]['data']).decode() - add_to_paid_db(user_token_hash, session_lifetime) - - if user_token_hash in self.socket_session_ids.keys(): + self.unlock_content(user_token_hash) - # emit pamyent received event to the user - self.socketio.emit('payment_received', room=self.socket_session_ids.pop(user_token_hash)) + except Exception as e: LOG.warning(e) @@ -96,7 +102,28 @@ def mqtt_worker(self): self.q.task_done() - def check_payment(self, message): + def unlock_content(self, user_token_hash, exp_time=None): + ''' + Sends the user_token_hash to the db with the right expiration time and informs the user via socket + ''' + + if exp_time is None: + + exp_time = datetime.utcnow() + timedelta(hours = session_lifetime) + + add_to_paid_db(user_token_hash, exp_time) + + # prevent key errors + if user_token_hash in self.socket_session_ids.keys(): + + # emit pamyent received event to the user + self.socketio.emit('payment_received', room=self.socket_session_ids.pop(user_token_hash)) + + + def payment_valid(self, message): + ''' + Check if the right amount arrived on the rigth address + ''' for output in message['payload']['transaction'][0]['essence']['outputs']: @@ -108,10 +135,62 @@ def check_payment(self, message): return False + + def manual_payment_check(self, address, user_token_hash): + ''' + Triggers a crawl on the designated address to find a payment in the past and add it to the db + ''' + + # easy checks first to prevent overload + if user_token_hash not in self.manual_payment_checks and is_own_address(address): + + self.manual_payment_checks.add(user_token_hash) + + outputs = self.client.find_outputs(addresses=[address]) + + for output in outputs: + + message = self.client.get_message_data(output['message_id']) + + if user_token_hash == bytes(message['payload']['transaction'][0]['essence']['payload']['indexation'][0]['data']).decode(): + + if self.payment_valid(message): + + exp_time = self.get_payment_expiry(output['message_id']) + + if exp_time > datetime.utcnow(): + + self.unlock_content(user_token_hash, exp_time) + + # prevent key errors + if user_token_hash in self.socket_session_ids.keys(): + + # emit pamyent not found + self.socketio.emit('payment_not_found', room=self.socket_session_ids[user_token_hash]) + + self.manual_payment_checks.remove(user_token_hash) + + def get_payment_expiry(self, message_id): + ''' + Fetches the tangle to get the timestamp of the milstone referencing the message + ''' + + milestone_index = self.client.get_message_metadata(message_id)['referenced_by_milestone_index'] + + message_timestamp = datetime.fromtimestamp(self.client.get_milestone(milestone_index)['timestamp']) + + return message_timestamp + timedelta(hours = session_lifetime) + + + def stop(self): - self.q.put(self.STOP) - LOG.info('MQTT worker stopped') + ''' + Stops the iota listener gracefully + ''' + self.client.disconnect() LOG.info('MQTT client stopped') + self.q.put(self.STOP) + LOG.info('MQTT worker stopped') self.q.queue.clear() LOG.info('Working queue cleared') diff --git a/templates/pay.html b/templates/pay.html index 154ee74..4fcdb84 100644 --- a/templates/pay.html +++ b/templates/pay.html @@ -13,8 +13,18 @@ socket.on('payment_received', function() { location.reload(); }); - + socket.on('payment_not_found', function() { + console.log('Payment not found'); + }); + $('#check_payment_button').on('click', function(){ + socket.emit('check_payment', {user_token_hash: '{{ user_token_hash }}', iota_address: '{{ iota_address }}'}); + }); + // show manual payment check after 30 seconds time + setTimeout("showCheckPayment()", 30000); }); + function showCheckPayment() { + document.getElementById("check_payment").style.display = "inline"; + }