From e083cb85eddfd251d6904c649e1587476b45025c Mon Sep 17 00:00:00 2001 From: tubleronchik Date: Mon, 24 Jun 2024 15:34:44 +0200 Subject: [PATCH] new structure of the project. new decription --- __init__.py => helpers/__init__.py | 0 {utils => helpers}/logger.py | 0 helpers/odoo.py | 104 ++++++++ main.py | 26 +- {src => registar}/__init__.py | 0 registar/registar.py | 22 ++ registar/src/__init__.py | 0 registar/src/http_server.py | 43 ++++ registar/src/odoo.py | 84 +++++++ registar/src/websocket.py | 84 +++++++ registar/utils/__init__.py | 0 registar/utils/messages.py | 20 ++ {utils => registar/utils}/parse_string.py | 0 {utils => registar/utils}/pinata.py | 0 registar/utils/robonomics.py | 25 ++ rrs_operator/__init__.py | 0 rrs_operator/rrs_operator.py | 11 + rrs_operator/src/__init__.py | 0 rrs_operator/src/ipfs.py | 78 ++++++ rrs_operator/src/odoo.py | 119 ++++++++++ rrs_operator/src/robonomics.py | 110 +++++++++ rrs_operator/utils/__init__.py | 0 rrs_operator/utils/read_file.py | 10 + src/http_server.py | 111 --------- src/odoo.py | 132 ----------- src/odoo_internal.py | 277 ---------------------- src/websocket.py | 68 ------ utils/decrypt_encrypt_msg.py | 34 --- utils/decryption.py | 58 +++++ utils/encryption.py | 62 +++++ utils/messages.py | 15 -- utils/robonomics.py | 21 -- 32 files changed, 835 insertions(+), 679 deletions(-) rename __init__.py => helpers/__init__.py (100%) rename {utils => helpers}/logger.py (100%) create mode 100644 helpers/odoo.py rename {src => registar}/__init__.py (100%) create mode 100644 registar/registar.py create mode 100644 registar/src/__init__.py create mode 100644 registar/src/http_server.py create mode 100644 registar/src/odoo.py create mode 100644 registar/src/websocket.py create mode 100644 registar/utils/__init__.py create mode 100644 registar/utils/messages.py rename {utils => registar/utils}/parse_string.py (100%) rename {utils => registar/utils}/pinata.py (100%) create mode 100644 registar/utils/robonomics.py create mode 100644 rrs_operator/__init__.py create mode 100644 rrs_operator/rrs_operator.py create mode 100644 rrs_operator/src/__init__.py create mode 100644 rrs_operator/src/ipfs.py create mode 100644 rrs_operator/src/odoo.py create mode 100644 rrs_operator/src/robonomics.py create mode 100644 rrs_operator/utils/__init__.py create mode 100644 rrs_operator/utils/read_file.py delete mode 100644 src/http_server.py delete mode 100644 src/odoo.py delete mode 100644 src/odoo_internal.py delete mode 100644 src/websocket.py delete mode 100644 utils/decrypt_encrypt_msg.py create mode 100644 utils/decryption.py create mode 100644 utils/encryption.py delete mode 100644 utils/messages.py delete mode 100644 utils/robonomics.py diff --git a/__init__.py b/helpers/__init__.py similarity index 100% rename from __init__.py rename to helpers/__init__.py diff --git a/utils/logger.py b/helpers/logger.py similarity index 100% rename from utils/logger.py rename to helpers/logger.py diff --git a/helpers/odoo.py b/helpers/odoo.py new file mode 100644 index 0000000..bef8e7f --- /dev/null +++ b/helpers/odoo.py @@ -0,0 +1,104 @@ +from dotenv import load_dotenv +import os +import xmlrpc.client +import typing as tp +from helpers.logger import Logger + +load_dotenv() + +ODOO_URL = os.getenv("ODOO_URL") +ODOO_DB = os.getenv("ODOO_DB") +ODOO_USER = os.getenv("ODOO_USER") +ODOO_PASSWORD = os.getenv("ODOO_PASSWORD") + + +class OdooHelper: + def __init__(self, name_of_the_user: str) -> None: + self._logger = Logger(f"odoo-helper-{name_of_the_user}") + self._connection, self._uid = self._connect_to_db() + + def _connect_to_db(self): + """Connect to Odoo db + + :return: Proxy to the object endpoint to call methods of the odoo models. + """ + try: + common = xmlrpc.client.ServerProxy("{}/xmlrpc/2/common".format(ODOO_URL), allow_none=1) + uid = common.authenticate(ODOO_DB, ODOO_USER, ODOO_PASSWORD, {}) + if uid == 0: + raise Exception("Credentials are wrong for remote system access") + else: + self._logger.debug("Connection Stablished Successfully") + connection = xmlrpc.client.ServerProxy("{}/xmlrpc/2/object".format(ODOO_URL)) + return connection, uid + except Exception as e: + self._logger.error(f"Couldn't connect to the db: {e}") + + def create(self, model: str, data: dict) -> tp.Optional[int]: + """Method to create a new record in any Odoo table + :param model: Name of the model in Odoo + :param data: Data to create record with + + :return: Id of the new record + """ + try: + record_id = self._connection.execute_kw( + ODOO_DB, + self._uid, + ODOO_PASSWORD, + model, + "create", + [data], + ) + return record_id + except Exception as e: + self._logger.error(f"Couldn't create a new record in {model}: {e}") + return None + + def update(self, model: str, record_id: int, data: dict) -> bool: + """Method to update the exhisting record. with the new data + :param model: Name of the model in Odoo + :param record_id: Id of the record to be updated. + :param data: Data to write + + :return: True if updated successfuly, False otherwise + """ + return self._connection.execute_kw( + ODOO_DB, + self._uid, + ODOO_PASSWORD, + model, + "write", + [[record_id], data], + ) + + def search(self, model: str, search_domains: list = []) -> list: + """Looking for a record in the model with the specified domain. + :param model: Name of the model in Odoo + :param search_domains: Optional: A list of tuples that define the search criteria. + Retrievs all records of the model if is empty. + + :return: List of record ids. If there are no records matching the domain, returns an empty list. + """ + ids = self._connection.execute_kw(ODOO_DB, self._uid, ODOO_PASSWORD, model, "search", [search_domains]) + return ids + + def read(self, model: str, record_ids: list, fields: list = []) -> list: + """Method to fetch details of the records corresponding to the ids. + :param model: Name of the model in Odoo + :param record_ids: Ids of the records to fetch details for + :param fields: Optional: Read only the fields. If emtpy, returns all fields + + :return: List of the records + """ + + data = self._connection.execute_kw( + ODOO_DB, + self._uid, + ODOO_PASSWORD, + model, + "read", + record_ids, + {"fields": fields}, + ) + return data \ No newline at end of file diff --git a/main.py b/main.py index 7f1288a..6daa803 100644 --- a/main.py +++ b/main.py @@ -1,26 +1,10 @@ -from flask import Flask -import threading -import os -from dotenv import load_dotenv -from src.odoo import OdooProxy -from src.websocket import WSClient -from src.http_server import OdooFlaskView, BaseView - -load_dotenv() - -FLASK_PORT = os.getenv("FLASK_PORT") - +from registar.registar import Registar +from rrs_operator.rrs_operator import Operator def main() -> None: - odoo = OdooProxy() - app = Flask(__name__) - ws = WSClient(odoo) - BaseView.initialize(odoo, ws) - OdooFlaskView.register(app, route_base="/") - flask_thread = threading.Thread(target=lambda: app.run(host="127.0.0.1", port=FLASK_PORT)) - flask_thread.start() - ws.run() - os._exit(0) + operator = Operator() + add_user_callback = operator.get_robonomics_add_user_callback() + registar = Registar(add_user_callback) if __name__ == "__main__": diff --git a/src/__init__.py b/registar/__init__.py similarity index 100% rename from src/__init__.py rename to registar/__init__.py diff --git a/registar/registar.py b/registar/registar.py new file mode 100644 index 0000000..aaa8a1c --- /dev/null +++ b/registar/registar.py @@ -0,0 +1,22 @@ +from registar.src.odoo import Odoo +from registar.src.http_server import OdooFlaskView, BaseView +from registar.src.websocket import WSClient +from flask import Flask +import threading +import os +from dotenv import load_dotenv + +load_dotenv() +FLASK_PORT = os.getenv("FLASK_PORT") + +class Registar: + def __init__(self, add_user_callback) -> None: + self.odoo = Odoo() + self.app = Flask(__name__) + self.ws = WSClient(self.odoo) + BaseView.initialize(add_user_callback) + OdooFlaskView.register(self.app, route_base="/") + flask_thread = threading.Thread(target=lambda: self.app.run(host="127.0.0.1", port=FLASK_PORT)) + flask_thread.start() + self.ws.run() + os._exit(0) \ No newline at end of file diff --git a/registar/src/__init__.py b/registar/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/registar/src/http_server.py b/registar/src/http_server.py new file mode 100644 index 0000000..b97b97d --- /dev/null +++ b/registar/src/http_server.py @@ -0,0 +1,43 @@ +from flask import request +from flask_classful import FlaskView, route +from dotenv import load_dotenv +import os + +from helpers.logger import Logger + +load_dotenv() + +PINATA_API_KEY = os.getenv("PINATA_API_KEY") +PINATA_API_SECRET = os.getenv("PINATA_API_SECRET") +STATUS_PAID_ID = os.getenv("ODOO_RRS_STATUS_PAID_ID") +STATUS_NOTPAID_ID = os.getenv("ODOO_RRS_STATUS_NOTPAID_ID") +ADMIN_SEED = os.getenv("ADMIN_SEED") +DONE_SATGE_ID = os.getenv("ODOO_HELPDESK_DONE_STAGE_ID") + + +class BaseView(FlaskView): + odoo = None + ws = None + _logger = None + + @classmethod + def initialize(cls, add_user_callback): + cls.set_logger() + cls.add_user_callback = add_user_callback + + @classmethod + def set_logger(cls): + cls._logger = Logger("flask") + + +class OdooFlaskView(BaseView): + def index(self): + return "

Welcome from Flask

" + + @route("/rrs/new-user", methods=["POST"]) + def new_user_handler(self): + request_data = request.get_json() + self._logger.debug(f"Data from new-user request: {request_data}") + address = request_data["address"] + self.add_user_callback(address) + return "ok" \ No newline at end of file diff --git a/registar/src/odoo.py b/registar/src/odoo.py new file mode 100644 index 0000000..e44f07e --- /dev/null +++ b/registar/src/odoo.py @@ -0,0 +1,84 @@ +from helpers.logger import Logger +from helpers.odoo import OdooHelper +import typing as tp +from tenacity import * + +class Odoo: + def __init__(self) -> None: + self.helper = OdooHelper("registar") + self._logger = Logger("odoo-registar") + + @retry(wait=wait_fixed(5)) + def create_rrs_user(self, email: str, sender_address: str) -> tp.Optional[int]: + """Creates user in Robonomics Report Service module and returns its id. + :param email: Customer's email address + :param sender_address: Customer's address in Robonomics parachain + + :return: User id + """ + try: + user_id = self.helper.create( + "rrs.register", + { + "address": sender_address, + "customer_email": email, + }, + ) + return user_id + except Exception as e: + self._logger.error(f"Couldn't create user: {e}") + raise Exception("Failed to create rrs user") + + @retry(wait=wait_fixed(5)) + def check_if_rrs_user_exists(self, sender_address: str) -> tp.Union[int, bool]: + """Looking for a rrs user id by the controller address. + :param sender_address: Customer's address in Robonomics parachain. + + :return: The user id or false. + """ + id = self.helper.search("rrs.register", [("address", "=", sender_address)]) + self._logger.debug(f"Find RRS user with id: {id}") + if id: + return id[0] + return False + + @retry(wait=wait_fixed(5)) + def update_rrs_user_with_pinata_creds(self, user_id: int, pinata_key: str, pinata_api_secret: str) -> bool: + """Update the customer profile with pinata credentials in RRS module. + :param customer_id: User id + :param pinata_key: Pinata API key + :param pinata_api_secret: Pinata API secret key + :return: bool + """ + try: + return self.helper.update( + "rrs.register", + user_id, + { + "pinata_key": pinata_key, + "pinata_secret": pinata_api_secret, + }, + ) + except Exception as e: + self._logger.error(f"Couldn't update user {user_id} with pinata creds {e}") + raise Exception("Failed to update the user") + + @retry(wait=wait_fixed(5)) + def retrieve_pinata_creds(self, sender_address: str, rrs_user_id: int) -> tuple: + """Retrieve pinata creds. + :param sender_address: Customer's address in Robonomics parachain + + :return: The Pinata creds or None. + """ + try: + rrs_user_data = self.helper.read("rrs.register", [rrs_user_id], ["pinata_key", "pinata_secret"]) + if rrs_user_data: + pinata_key = rrs_user_data[0]["pinata_key"] + pinata_secret = rrs_user_data[0]["pinata_secret"] + self._logger.debug(f"Found pinata creds for address: {sender_address}, pinata key: {pinata_key}") + return pinata_key, pinata_secret + else: + self._logger.error(f"Couldn't find pinata creds for {sender_address}") + except Exception as e: + self._logger.error(f"Couldn't get pinata creds for user: {sender_address}") + raise Exception(f"Couldn't retrieve pinata creds for {sender_address}") \ No newline at end of file diff --git a/registar/src/websocket.py b/registar/src/websocket.py new file mode 100644 index 0000000..3e239d3 --- /dev/null +++ b/registar/src/websocket.py @@ -0,0 +1,84 @@ +import websocket +import os +from dotenv import load_dotenv +import json +import rel +from helpers.logger import Logger +from utils.decryption import decrypt_message +from registar.utils.messages import message_with_pinata_creds, message_for_subscribing +from registar.utils.robonomics import add_device_to_subscription +from registar.utils.pinata import generate_pinata_keys + +load_dotenv() + +LIBP2P_WS_SERVER = os.getenv("LIBP2P_WS_SERVER") +ADMIN_SEED = os.getenv("ADMIN_SEED") +PINATA_API_KEY = os.getenv("PINATA_API_KEY") +PINATA_API_SECRET = os.getenv("PINATA_API_SECRET") + + +class WSClient: + def __init__(self, odoo) -> None: + self.odoo = odoo + self._logger = Logger("websocket") + self._connect2server() + + def _connect2server(self): + # websocket.enableTrace(True) + self.ws = websocket.WebSocketApp( + url=LIBP2P_WS_SERVER, + on_open=self._on_connection, + on_message=self._on_message, + on_error=self._on_error, + on_close=self._on_close, + ) + + def run(self) -> None: + self.ws.run_forever(dispatcher=rel, reconnect=5) + rel.signal(2, rel.abort) # Keyboard Interrupt + rel.dispatch() + + def _on_connection(self, ws): + self._logger.debug(f"Connected to {LIBP2P_WS_SERVER}") + msg = message_for_subscribing() + self._logger.debug(f"Connection msg: {msg}") + self.ws.send(msg) + + def _on_message(self, ws, message): + json_message = json.loads(message) + self._logger.debug(f"Got msg: {json_message}") + if "peerId" in json_message: + return + message_data = json_message["data"] + if "email" in message_data: + encrypted_email = message_data["email"] + sender_address = message_data["sender_address"] + decrypted_email = decrypt_message(encrypted_email, sender_address, self._logger) + rrs_user_id = self.odoo.check_if_rrs_user_exists(sender_address) + if rrs_user_id: + pinata_key, pinata_secret = self.odoo.retrieve_pinata_creds(sender_address, rrs_user_id) + if pinata_key: + msg = message_with_pinata_creds(pinata_key, pinata_secret, sender_address, self._logger) + self.ws.send(msg) + return + user_id = self.odoo.create_rrs_user(decrypted_email, sender_address) + hash = add_device_to_subscription(sender_address) + if hash: + self._logger.debug(f"Add {sender_address} to subscription") + pinata_keys = generate_pinata_keys(PINATA_API_KEY, PINATA_API_SECRET, sender_address) + pinata_key = pinata_keys["pinata_api_key"] + pinata_secret = pinata_keys["pinata_api_secret"] + self.odoo.update_rrs_user_with_pinata_creds(user_id, pinata_key, pinata_secret) + msg = message_with_pinata_creds(pinata_key, pinata_secret, sender_address, self._logger) + self.ws.send(msg) + + else: + self._logger.error(f"Couldn't add {sender_address} to subscription: {hash}") + + def _on_error(self, ws, error): + self._logger.error(f"{error}") + + def _on_close(self, ws, close_status_code, close_msg): + self._logger.debug(f"Connection closed with status code {close_status_code} and message {close_msg}") + + diff --git a/registar/utils/__init__.py b/registar/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/registar/utils/messages.py b/registar/utils/messages.py new file mode 100644 index 0000000..1828649 --- /dev/null +++ b/registar/utils/messages.py @@ -0,0 +1,20 @@ +import os +import json +from utils.encryption import encrypt_for_users + +ADMIN_SEED = os.getenv("ADMIN_SEED") + +def message_with_pinata_creds(pinata_key: str, pinata_secret: str, sender_address: str, logger) -> str: + pinata_key_encrypted = encrypt_for_users(pinata_key, [sender_address], logger) + pinata_secret_encrypted = encrypt_for_users(pinata_secret, [sender_address], logger) + msg = { + "protocol": f"/pinataCreds/{sender_address}", + "serverPeerId": "", + "save_data": False, + "data": {"data": {"public": pinata_key_encrypted, "private": pinata_secret_encrypted}}, + } + return json.dumps(msg) + +def message_for_subscribing() -> str: + msg = {"protocols_to_listen": ["/initialization"]} + return json.dumps(msg) diff --git a/utils/parse_string.py b/registar/utils/parse_string.py similarity index 100% rename from utils/parse_string.py rename to registar/utils/parse_string.py diff --git a/utils/pinata.py b/registar/utils/pinata.py similarity index 100% rename from utils/pinata.py rename to registar/utils/pinata.py diff --git a/registar/utils/robonomics.py b/registar/utils/robonomics.py new file mode 100644 index 0000000..9e039d0 --- /dev/null +++ b/registar/utils/robonomics.py @@ -0,0 +1,25 @@ +import robonomicsinterface as ri +import os +from dotenv import load_dotenv +from substrateinterface import KeypairType + +load_dotenv() +ADMIN_SEED = os.getenv("ADMIN_SEED") +WSS_ENDPOINT = os.getenv("WSS_ENDPOINT") +ADMIN_ADDRESS = os.getenv("ADMIN_ADDRESS") + +def add_device_to_subscription(address: str) -> bool: + """ Checks whether the address has an active subscription or not. + :param owner_address: Address of the subscription's owner. + + :return True if subscription is active, False otherwise + """ + account = ri.Account(remote_ws=WSS_ENDPOINT, seed=ADMIN_SEED, crypto_type=KeypairType.ED25519) + rws = ri.RWS(account) + devices = rws.get_devices(ADMIN_ADDRESS) + print(f"Devices before update: {devices}") + devices.append(address) + print(f"Devices after update: {devices}") + + return rws.set_devices(devices) + diff --git a/rrs_operator/__init__.py b/rrs_operator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rrs_operator/rrs_operator.py b/rrs_operator/rrs_operator.py new file mode 100644 index 0000000..d30279e --- /dev/null +++ b/rrs_operator/rrs_operator.py @@ -0,0 +1,11 @@ +from rrs_operator.src.robonomics import RobonomicsHelper +from rrs_operator.src.odoo import Odoo + +class Operator: + def __init__(self) -> None: + self.odoo = Odoo() + self.robonomics = RobonomicsHelper(self.odoo) + self.robonomics.subscribe() + + def get_robonomics_add_user_callback(self) -> None: + return self.robonomics.add_user_callback \ No newline at end of file diff --git a/rrs_operator/src/__init__.py b/rrs_operator/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rrs_operator/src/ipfs.py b/rrs_operator/src/ipfs.py new file mode 100644 index 0000000..131f04a --- /dev/null +++ b/rrs_operator/src/ipfs.py @@ -0,0 +1,78 @@ +import tempfile +import json +from dotenv import load_dotenv +import requests +import shutil +import os +from tenacity import * +from helpers.logger import Logger +from utils.decryption import decrypt_message + +load_dotenv() + +logs_name = ["issue_description.json", "home-assistant.log", "trace.saved_traces"] + +ADMIN_SEED = os.getenv("ADMIN_SEED") + + +class IPFSHelpder: + def __init__(self, sender_public_key: str) -> None: + self._logger = Logger("ipfs") + self.sender_public_key = sender_public_key + self.temp_dir = tempfile.mkdtemp() + + @retry(wait=wait_fixed(5)) + def _download_file(self, hash: str, file_name: str) -> None: + """Download file from IPFS + + :param hash: IPFS hash of the directory with the logs + :param file_name: Name of the file to download + """ + + try: + self._logger.debug(f"Downloading file {file_name} from IPFS...") + response = requests.get(f"https://gateway.pinata.cloud/ipfs/{hash}/{file_name}") + if response.status_code == 200: + self._logger.info("IPFS: Succesfully download logs from ipfs.") + with open(f"{self.temp_dir}/{file_name}", "w") as f: + decrypted_content = decrypt_message(response.text, self.sender_public_key, self._logger) + f.write(decrypted_content) + elif response.status_code == 404: + pass + else: + self._logger.error(f"Couldn't download logs from ipfs with response: {response}") + raise Exception("Couldn't download logs from ipfs") + + except Exception as e: + self._logger.error(f"Couldn't download logs {file_name} from ipfs: {e}") + raise (e) + + def _download_logs(self, hash: str) -> None: + """Download all the files from IPFS. + + :param hash: IPFS hash of the directory with the logs + """ + + for log in logs_name: + self._download_file(hash, log) + + with open(f"{self.temp_dir}/issue_description.json") as f: + metadata = json.load(f) + pictures_count = metadata["pictures_count"] + if int(pictures_count) > 0: + for i in range(1, pictures_count + 1): + self._download_file(hash, f"picture{i}") + + def parse_logs(self, hash) -> tuple: + """Parse description file.""" + self._download_logs(hash) + self._logger.info(f"IPFS: Parsing logs... Hash: {hash}") + with open(f"{self.temp_dir}/issue_description.json") as f: + issue = json.load(f) + phone = issue["phone_number"] + description = issue["description"] + return phone, description + + def clean_temp_dir(self) -> None: + """Remove the temporary directory and its content""" + shutil.rmtree(self.temp_dir) \ No newline at end of file diff --git a/rrs_operator/src/odoo.py b/rrs_operator/src/odoo.py new file mode 100644 index 0000000..587b795 --- /dev/null +++ b/rrs_operator/src/odoo.py @@ -0,0 +1,119 @@ +from helpers.odoo import OdooHelper +from helpers.logger import Logger +from tenacity import * +import typing as tp +import base64 +from rrs_operator.utils.read_file import read_file + + +class Odoo: + """Odoo for Operator""" + def __init__(self) -> None: + self.helper = OdooHelper("operator") + self._logger = Logger("odoo-registar") + + @retry(wait=wait_fixed(5)) + def create_ticket( + self, email: str, robonomics_address: str, phone: str, description: str, ipfs_hash: str + ) -> tp.Optional[int]: + """Creates ticket in Helpdesk module + + :param email: Customer's email address + :param robonomics_address: Customer's address in Robonomics parachain + :param phone: Customer's phone number + :param description: Problem's description from cusotmer + + :return: Ticket id + """ + + priority = "3" + channel_id = 5 + name = f"Issue from {robonomics_address}" + description = f"Issue from HA: {description}" + try: + ticket_id = self.helper.create( + model="helpdesk.ticket", + data={ + "name": name, + "description": description, + "priority": priority, + "channel_id": channel_id, + "partner_email": email, + "phone": phone, + }, + ) + self._logger.debug(f"Ticket created. Ticket id: {ticket_id}") + return ticket_id + except Exception as e: + self._logger.error(f"Couldn't create ticket: {e}") + raise Exception("Failed to create ticket") + + @retry(wait=wait_fixed(5)) + def create_note_with_attachment(self, ticket_id: int, file_name: str, file_path: str) -> tp.Optional[bool]: + """Create log with attachment in Odoo using logs from the customer + + :param ticket_id: Id of the ticket to which logs will be added + :param file_name: Name of the file + :param file_path: Path to the file + + :return: If the log note was created or no + """ + data = read_file(file_path) + try: + record = self.helper.create( + model="mail.message", + data={ + "body": "Logs from user", + "model": "helpdesk.ticket", + "res_id": ticket_id, + }, + ) + attachment = self.helper.create( + model="ir.attachment", + data={ + "name": file_name, + "datas": base64.b64encode(data).decode(), + "res_model": "helpdesk.ticket", + "res_id": ticket_id, + }, + ) + return self.helper.update( + model="mail.message", record_id=record, data={"attachment_ids": [(4, attachment)]} + ) + except Exception as e: + self._logger.error(f"Couldn't create note: {e}") + raise Exception("Failed to create note") + + @retry(wait=wait_fixed(5)) + def find_user_email(self, address) -> tp.Optional[str]: + """Find the user's email. + :param address: User's address in Robonomics parachain + + :return: The user email or None. + """ + self._logger.debug(f"start looking for email.. {address}") + try: + user_id = self._find_user_id(address) + self._logger.debug(f"user id: {user_id}") + if user_id: + user_data = self.helper.read(model="rrs.register", record_ids=user_id, fields=["customer_email"]) + email = user_data[0]['customer_email'] + self._logger.debug(f"Find user's email: {email}") + return email + else: + self._logger.error(f"Couldn't find user for {address}") + + except Exception as e: + self._logger.error(f"Couldn't find email {e}") + raise Exception("Failed to find email") + + + def _find_user_id(self, address: str) -> list: + """Find a user id by the parachain address. This id is used to retrive the user's email. + :param address: User's address in Robonomics parachain + + :return: The list with user id. + """ + id = self.helper.search(model="rrs.register", search_domains=[("address", "=", address)]) + self._logger.debug(f"Find user with id: {id}") + return id \ No newline at end of file diff --git a/rrs_operator/src/robonomics.py b/rrs_operator/src/robonomics.py new file mode 100644 index 0000000..41d9f3d --- /dev/null +++ b/rrs_operator/src/robonomics.py @@ -0,0 +1,110 @@ +import robonomicsinterface as ri +from robonomicsinterface.utils import ipfs_32_bytes_to_qm_hash +import typing as tp +from dotenv import load_dotenv +import threading +import os + +from rrs_operator.src.ipfs import IPFSHelpder +from helpers.logger import Logger + +load_dotenv() + +ADMIN_ADDRESS = os.getenv("ADMIN_ADDRESS") +WSS_ENDPOINT = os.getenv("WSS_ENDPOINT") + + +class RobonomicsHelper: + def __init__(self, odoo) -> None: + self.account = ri.Account(remote_ws=WSS_ENDPOINT) + self.rws = ri.RWS(self.account) + self._logger = Logger("robonomics") + self.odoo = odoo + self.users = self.rws.get_devices(ADMIN_ADDRESS) + self._track_free_weight() + + def add_user_callback(self, address: str) -> None: + """Updates the list of users to track new datalogs for + :param address: Address to add + """ + self.users.append(address) + + def subscribe(self) -> ri.Subscriber: + """Subscribe to the NewRecord event""" + + self._logger.debug("Susbcribed to NewRecord event") + self.subscriber = ri.Subscriber( + self.account, + ri.SubEvent.NewRecord, + subscription_handler=self._on_new_record, + ) + self._is_subscription_alive() + + def _on_new_record(self, data: tp.Tuple[tp.Union[str, tp.List[str]]]) -> None: + """NewRecord callback + + :param data: Data from the launch + """ + + try: + self._logger.debug(f"New datalogs: {data}") + self._logger.debug(f"users: {self.users}") + if data[0] in self.users: + hash = data[2] + self._logger.info(f"Ipfs hash: {hash}") + robonomics_address_from = data[0] + threading.Thread( + target=self._handle_data, + args=( + hash, + robonomics_address_from, + ), + ).start() + + except Exception as e: + self._logger.error(f"Problem in on new record: {e}") + + def _handle_data(self, ipfs_hash: str, robonomics_address_from: str) -> None: + """Handle data from the datalog: create ticket and add logs + + :param robonomics_address_from: User's address in Robonomics parachain + """ + ipfs = IPFSHelpder(robonomics_address_from) + email = self.odoo.find_user_email(robonomics_address_from) + phone, description = ipfs.parse_logs(ipfs_hash) + self._logger.debug(f"Data from ipfs: {email}, {phone}, {description}") + ticket_id = self.odoo.create_ticket(email, robonomics_address_from, phone, description, ipfs_hash) + if len(os.listdir(ipfs.temp_dir)) > 1: + for f in os.listdir(ipfs.temp_dir): + if f == "issue_description.json": + pass + else: + file_name = f + file_path = f"{ipfs.temp_dir}/{f}" + self.odoo.create_note_with_attachment(ticket_id, file_name, file_path) + ipfs.clean_temp_dir() + + def _resubscribe(self) -> None: + """Close the subscription and create a new one""" + + self._logger.debug("Resubscribe") + self.subscriber.cancel() + self.subscribe() + + def _is_subscription_alive(self) -> None: + """Ckeck every 15 sec if subscription is alive""" + + threading.Timer( + 15, + self._is_subscription_alive, + ).start() + if self.subscriber._subscription.is_alive(): + return + self._resubscribe() + + def _track_free_weight(self) -> None: + """Track free weight of the subscription""" + threading.Timer(60, self._track_free_weight,).start() + free_weight = self.rws.get_ledger(ADMIN_ADDRESS) + self._logger.debug(f"Free weight in subscription: {free_weight}") + diff --git a/rrs_operator/utils/__init__.py b/rrs_operator/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rrs_operator/utils/read_file.py b/rrs_operator/utils/read_file.py new file mode 100644 index 0000000..c907739 --- /dev/null +++ b/rrs_operator/utils/read_file.py @@ -0,0 +1,10 @@ +def read_file(file_path: str) -> bytes: + """Read file and return its content + + :param file_path: Path to the file to read + :return: File's content in bytes + """ + + with open(file_path, "rb") as f: + data = f.read() + return data diff --git a/src/http_server.py b/src/http_server.py deleted file mode 100644 index 05109ba..0000000 --- a/src/http_server.py +++ /dev/null @@ -1,111 +0,0 @@ -from flask import request -from flask_classful import FlaskView, route -from dotenv import load_dotenv -import os -import threading -import json - -from utils.logger import Logger -from utils.pinata import generate_pinata_keys, revoke_pinata_key, unpin_hash_from_pinata -from utils.robonomics import transfer_xrt_2buy_subscription -from utils.parse_string import extract_hash -from utils.messages import message_with_pinata_creds - -load_dotenv() - -PINATA_API_KEY = os.getenv("PINATA_API_KEY") -PINATA_API_SECRET = os.getenv("PINATA_API_SECRET") -STATUS_PAID_ID = os.getenv("ODOO_RRS_STATUS_PAID_ID") -STATUS_NOTPAID_ID = os.getenv("ODOO_RRS_STATUS_NOTPAID_ID") -ADMIN_SEED = os.getenv("ADMIN_SEED") -DONE_SATGE_ID = os.getenv("ODOO_HELPDESK_DONE_STAGE_ID") - - -class BaseView(FlaskView): - odoo = None - ws = None - _logger = None - - @classmethod - def initialize(cls, odoo, ws_client): - cls.odoo = odoo - cls.ws_client = ws_client - cls.set_logger() - - @classmethod - def set_logger(cls): - cls._logger = Logger("flask") - - -class OdooFlaskView(BaseView): - def index(self): - return "

Welcome from Flask

" - - @route("/rrs/payment", methods=["POST"]) - def payment_handler(self): - request_data = request.get_json() - self._logger.debug(f"Data from payment request: {request_data}") - if "status" in request_data.keys(): - status_id = int(request_data["status"]) - if status_id == int(STATUS_PAID_ID): - id = int(request_data["id"]) - controller_address = request_data["address"] - owner_address = str(request_data["owner_address"]) - pinata_keys = generate_pinata_keys(PINATA_API_KEY, PINATA_API_SECRET, owner_address) - threading.Thread( - target=self._odoo_request_add_pinata_creds, - args=(id, pinata_keys, controller_address, owner_address), - ).start() - return "ok" - - def _odoo_request_add_pinata_creds( - self, user_id: int, pinata_keys: dict, controller_address: str, owner_address: str - ): - pinata_key = pinata_keys["pinata_api_key"] - pinata_secret = pinata_keys["pinata_api_secret"] - self.odoo.update_rrs_user_with_pinata_creds(user_id, pinata_key, pinata_secret) - msg = message_with_pinata_creds(pinata_key, pinata_secret, controller_address) - self.ws_client.ws.send(msg) - try: - transaction_hash = transfer_xrt_2buy_subscription(owner_address) - if transaction_hash: - self._logger.debug(f"XRT has been sent to ${owner_address}") - self.odoo.update_rrs_user_with_subscription_status(user_id) - self.ws_client.ws.send(json.dumps(msg)) - except Exception as e: - self._logger.error(f"Couldn't transfer XRT: {e}") - - @route("/rrs/expired", methods=["POST"]) - def expired_subscription_handler(self): - request_data = request.get_json() - self._logger.debug(f"Data from expired request: {request_data}") - if "status" in request_data.keys(): - status_id = int(request_data["status"]) - if status_id == int(STATUS_NOTPAID_ID): - pinata_key = request_data["pinata_key"] - id = request_data["id"] - pinata_response = revoke_pinata_key(PINATA_API_KEY, PINATA_API_SECRET, pinata_key) - if pinata_response == {"message": "Revoked"}: - self._logger.debug(f"Succesfully removed key {pinata_key} from Pinata") - threading.Thread(target=self._odoo_request_revoke_pinata_creds, args=(id,)).start() - else: - self._logger.error(f"Couldn't remove key {pinata_key} from Pinata. Response: {pinata_response}") - return "ok" - - def _odoo_request_revoke_pinata_creds(self, user_id: int) -> None: - self.odoo.revoke_pinata_creds_from_rss_user(user_id) - - @route("/helpdesk/ticket-done", methods=["POST"]) - def ticket_done_handler(self): - request_data = request.get_json() - self._logger.debug(f"Data from ticket-done request: {request_data}") - if int(request_data["stage"]) == int(DONE_SATGE_ID): - description = request_data["description"] - hash = extract_hash(description) - if hash: - pinata_response = unpin_hash_from_pinata(PINATA_API_KEY, PINATA_API_SECRET, hash) - if pinata_response == {"message": "Removed"}: - self._logger.debug(f"Succesfully removed hash {hash} from Pinata") - else: - self._logger.error(f"Couldn't remove hash {hash} from Pinata. Response: {pinata_response}") - return "ok" diff --git a/src/odoo.py b/src/odoo.py deleted file mode 100644 index d0baa29..0000000 --- a/src/odoo.py +++ /dev/null @@ -1,132 +0,0 @@ -from .odoo_internal import OdooHelper -from tenacity import * -import typing as tp - - -class OdooProxy: - """Proxy to work with Odoo. Uses OdooHelper class and exapnds it with retrying decorators.""" - - def __init__(self) -> None: - self.odoo_helper = OdooHelper() - - @retry(wait=wait_fixed(5)) - def create_rrs_user(self, email: str, owner_address: str, controller_address: str) -> int: - """Creats user until it will be created. - :param email: Customer's email address - :param owner_address: Customer's address in Robonomics parachain - :param controller_address: Controller's address in Robonomics parachain - - :return: User id - """ - self.odoo_helper._logger.debug("Creating rrs user...") - user_id = self.odoo_helper.create_rrs_user(email, owner_address, controller_address) - if not user_id: - raise Exception("Failed to create rrs user") - self.odoo_helper._logger.debug(f"Rrs user created. User id: {user_id}") - return user_id - - @retry(wait=wait_fixed(5)) - def create_invoice(self, address: str, email: str) -> int: - """Creats invoice until it will be created. - :param address: Owner's address in Robonomics parachain for the reference - """ - self.odoo_helper._logger.debug("Creating invoice...") - customer_id = self._create_customer(email, address) - invoice_id = int(self.odoo_helper.create_invoice(address, customer_id)) - if not invoice_id: - raise Exception("Failed to create invoice") - self.odoo_helper._logger.debug(f"Invoice created.") - try: - is_posted = self.odoo_helper.post_invoice(invoice_id) - self.odoo_helper._logger.debug(f"Invoice posted: {is_posted}") - except Exception as e: - self.odoo_helper._logger.error(f"Couldn't post invoics: {e}") - return invoice_id - - @retry(wait=wait_fixed(5)) - def _create_customer(self, email: str, address: str) -> int: - """Creats customer until it will be created. - :param email: Customer's email address - :param address: Customer's address in Robonomics parachain for the reference - - :return: Customer id - """ - self.odoo_helper._logger.debug("Creating customer...") - customer_id = self.odoo_helper.create_customer(email, address) - if not customer_id: - raise Exception("Failed to create customer") - self.odoo_helper._logger.debug(f"Customer created.") - return int(customer_id) - - @retry(wait=wait_fixed(5)) - def update_rrs_user_with_pinata_creds(self, user_id: int, pinata_key: str, pinata_api_secret: str) -> None: - """Update the customer profile with pinata credentials in RRS module. - :param customer_id: Customer id - :param pinata_key: Pinata API key - :param pinata_api_secret: Pinata API secret key - :return: bool rrs_user_id = self.check_if_rrs_user_exists(controller_address) - """ - self.odoo_helper._logger.debug("Updating customer with pinata creds...") - is_updated = self.odoo_helper.update_rrs_user_with_pinata_creds(user_id, pinata_key, pinata_api_secret) - if not is_updated: - raise Exception("Failed to update the user") - self.odoo_helper._logger.debug(f"User updated with pinata creds.") - - @retry(wait=wait_fixed(5)) - def update_rrs_user_with_subscription_status(self, user_id: int) -> None: - """Update the customer profile with subscription status: true after the subscription was bought. - :param customer_id: User id - :return: bool - """ - self.odoo_helper._logger.debug("Updating customer with subscription status...") - is_updated = self.odoo_helper.update_rrs_user_with_subscription_status(user_id) - if not is_updated: - raise Exception("Failed to update the user") - self.odoo_helper._logger.debug(f"User updated with subscription status.") - - @retry(wait=wait_fixed(5)) - def revoke_pinata_creds_from_rss_user(self, user_id: int) -> None: - """Update the customer profile with subscription status: true after the subscription was bought. - :param customer_id: User id - :return: bool - """ - self.odoo_helper._logger.debug("Revoking pinata key from customer profile...") - is_updated = self.odoo_helper.revoke_pinata_creds_from_rss_user(user_id) - if not is_updated: - raise Exception("Failed to update the user") - self.odoo_helper._logger.debug(f"Keys revoked.") - - @retry(wait=wait_fixed(5)) - def retrieve_pinata_creds(self, controller_address: str, rrs_user_id: int) -> tuple: - """Retrieve pinata creds. - :param controller_address: Controller's address in Robonomics parachain - - :return: The Pinata creds or None. - """ - self.odoo_helper._logger.debug("Retrieving pinata creds from rrs user...") - pinata_key, pinata_secret = self.odoo_helper.retrieve_pinata_creds(controller_address, rrs_user_id) - if pinata_key is not None: - return pinata_key, pinata_secret - - @retry(wait=wait_fixed(5)) - def check_if_rrs_user_exists(self, controller_address: str) -> tp.Union[int, bool]: - """Looking for a rrs user id by the controller address. - :param controller_address: Controller's address in Robonomics parachain. - - :return: The user id or false. - """ - id = self.odoo_helper.check_if_rrs_user_exists(controller_address) - return id - - @retry(wait=wait_fixed(5)) - def check_if_invoice_posted(self, owner_address: str) -> tp.Optional[int]: - """Checks if invoice for this account is posted. - :param owner_address: Owner's address in Robonomics parachain for the reference - - :return: Invoice id - """ - self.odoo_helper._logger.debug("Checking if invoice is posted...") - id = self.odoo_helper.check_if_invoice_posted(owner_address) - if id: - return True - return False diff --git a/src/odoo_internal.py b/src/odoo_internal.py deleted file mode 100644 index 8117a13..0000000 --- a/src/odoo_internal.py +++ /dev/null @@ -1,277 +0,0 @@ -from dotenv import load_dotenv -import os -import xmlrpc.client -import typing as tp -import datetime -from utils.logger import Logger - -load_dotenv() - -ODOO_URL = os.getenv("ODOO_URL") -ODOO_DB = os.getenv("ODOO_DB") -ODOO_USER = os.getenv("ODOO_USER") -ODOO_PASSWORD = os.getenv("ODOO_PASSWORD") -ODOO_PRODUCT_SUBSCRIPTION_ID = os.getenv("ODOO_PRODUCT_SUBSCRIPTION_ID") -SUBSCRIPTION_PRICE = os.getenv("SUBSCRIPTION_PRICE") -STATUS_NOTPAID_ID = os.getenv("ODOO_RRS_STATUS_NOTPAID_ID") -STATUS_PAID_ID = os.getenv("ODOO_RRS_STATUS_PAID_ID") - - -class OdooHelper: - def __init__(self): - self._logger = Logger("odoo") - self._connection, self._uid = self._connect_to_db() - - def _connect_to_db(self): - """Connect to Odoo db - - :return: Proxy to the object endpoint to call methods of the odoo models. - """ - try: - common = xmlrpc.client.ServerProxy("{}/xmlrpc/2/common".format(ODOO_URL), allow_none=1) - uid = common.authenticate(ODOO_DB, ODOO_USER, ODOO_PASSWORD, {}) - if uid == 0: - raise Exception("Credentials are wrong for remote system access") - else: - self._logger.debug("Connection Stablished Successfully") - connection = xmlrpc.client.ServerProxy("{}/xmlrpc/2/object".format(ODOO_URL)) - return connection, uid - except Exception as e: - self._logger.error(f"Couldn't connect to the db: {e}") - - def create_rrs_user(self, email: str, owner_address: str, controller_address: str) -> tp.Optional[int]: - """Creates user in Robonomics Report Service module and returns its id. - :param email: Customer's email address - :param owner_address: Customer's address in Robonomics parachain - :param controller_address: Controller's address in Robonomics parachain - - :return: User id - """ - rrs_user_id = self.check_if_rrs_user_exists(controller_address) - if rrs_user_id: - return rrs_user_id - try: - user_id = self._connection.execute_kw( - ODOO_DB, - self._uid, - ODOO_PASSWORD, - "rrs.register", - "create", - [ - { - "address": controller_address, - "customer_email": email, - "status": STATUS_NOTPAID_ID, - "subscription": False, - "owner_address": owner_address, - } - ], - ) - return user_id - except Exception as e: - self._logger.error(f"Couldn't create ticket: {e}") - return None - - def _check_if_customer_exists(self, address: str) -> tp.Union[int, bool]: - """Looking for a partner id by the parachain address. This id is used in `create_invoice` function to - add a `customer` field. - :param address: Customer's address in Robonomics parachain - - :return: The partner id or false. - """ - id = self._connection.execute_kw( - ODOO_DB, self._uid, ODOO_PASSWORD, "res.partner", "search", [[("name", "=", address)]] - ) - self._logger.debug(f"Find ustomer with id: {id}") - return id - - def create_customer(self, email: str, address: str) -> tp.Optional[int]: - try: - customer_id = self._check_if_customer_exists(address) - if customer_id: - return customer_id[0] - customer_id = self._connection.execute_kw( - ODOO_DB, - self._uid, - ODOO_PASSWORD, - "res.partner", - "create", - [ - { - "name": address, - "is_company": False, - "email": email, - } - ], - ) - self._logger.debug(f"Create customer with id: {customer_id}") - return customer_id - except Exception as e: - self._logger.error(f"Couldn't create customer: {e}") - return None - - def create_invoice(self, owner_address: str, customer_id: str) -> tp.Optional[int]: - """Creates invoice in Invoicing module. - :param address: Owner's address in Robonomics parachain for the reference - - :return: Invoice id - """ - try: - line_ids = [ - ( - 0, - 0, - { - "product_id": ODOO_PRODUCT_SUBSCRIPTION_ID, - "name": "Robonomics Subscription 1 month", - "quantity": 1, - "price_unit": SUBSCRIPTION_PRICE, - }, - ) - ] - - invoice_id = self._connection.execute_kw( - ODOO_DB, - self._uid, - ODOO_PASSWORD, - "account.move", - "create", - [ - ( - { - "partner_id": customer_id, - "ref": owner_address, - "move_type": "out_invoice", - "invoice_date": str(datetime.datetime.today().date()), - "line_ids": line_ids, - } - ) - ], - ) - self._logger.debug(f"Create invoice with id: {invoice_id}") - return invoice_id - except Exception as e: - self._logger.error(f"Couldn't create invoice: {e}") - return None - - def post_invoice(self, invoice_id: int) -> bool: - """Post the invoice in Invoicing module. - :param invoice_id: Invoice id - :return: bool - """ - return self._connection.execute_kw( - ODOO_DB, self._uid, ODOO_PASSWORD, "account.move", "write", [[invoice_id], {"state": "posted"}] - ) - - def update_rrs_user_with_pinata_creds(self, user_id: int, pinata_key: str, pinata_api_secret: str) -> bool: - """Update the customer profile with pinata credentials in RRS module. - :param customer_id: User id - :param pinata_key: Pinata API key - :param pinata_api_secret: Pinata API secret key - :return: bool - """ - return self._connection.execute_kw( - ODOO_DB, - self._uid, - ODOO_PASSWORD, - "rrs.register", - "write", - [ - [user_id], - { - "pinata_key": pinata_key, - "pinata_secret": pinata_api_secret, - }, - ], - ) - - def update_rrs_user_with_subscription_status(self, user_id: int) -> bool: - """Update the customer profile with subscription status: true after the subscription was bought. - :param customer_id: User id - :return: bool - """ - current_date = datetime.datetime.today().date() - delta_time = datetime.timedelta(days=31) - end_date = current_date + delta_time - return self._connection.execute_kw( - ODOO_DB, - self._uid, - ODOO_PASSWORD, - "rrs.register", - "write", - [ - [user_id], - {"subscription": True, "started_date": str(current_date), "expired_date": str(end_date)}, - ], - ) - - def revoke_pinata_creds_from_rss_user(self, user_id: int) -> bool: - """Revoke the pinata credentials from the customer profile in RRS module. - :param customer_id: User id - :param pinata_key: Pinata API key - :param pinata_api_secret: Pinata API secret key - :return: bool - """ - return self._connection.execute_kw( - ODOO_DB, - self._uid, - ODOO_PASSWORD, - "rrs.register", - "write", - [ - [user_id], - { - "pinata_key": "", - "pinata_secret": "", - }, - ], - ) - - def check_if_rrs_user_exists(self, controller_address: str) -> tp.Union[int, bool]: - """Looking for a rrs user id by the controller address. - :param controller_address: Controller's address in Robonomics parachain. - - :return: The user id or false. - """ - id = self._connection.execute_kw( - ODOO_DB, self._uid, ODOO_PASSWORD, "rrs.register", "search", [[("address", "=", controller_address)]] - ) - self._logger.debug(f"Find RRS user with id: {id}") - return id - - def retrieve_pinata_creds(self, controller_address: str, rrs_user_id: int) -> tuple: - """Retrieve pinata creds. - :param controller_address: Controller's address in Robonomics parachain - - :return: The Pinata creds or None. - """ - if rrs_user_id: - rrs_user_data = self._connection.execute_kw( - ODOO_DB, - self._uid, - ODOO_PASSWORD, - "rrs.register", - "read", - [rrs_user_id[0]], - {"fields": ["pinata_key", "pinata_secret"]}, - ) - pinata_key = rrs_user_data[0]["pinata_key"] - pinata_secret = rrs_user_data[0]["pinata_secret"] - self._logger.debug(f"Found pinata creds for address: {controller_address}, pinata key: {pinata_key}") - return pinata_key, pinata_secret - else: - self._logger.error(f"Couldn't find pinata creds for {controller_address}") - - - def check_if_invoice_posted(self, owner_address: str) -> tp.Optional[int]: - """Checks if invoice for this account is posted. - :param owner_address: Owner's address in Robonomics parachain for the reference - - :return: Invoice id or None - """ - customer_id = self._check_if_customer_exists(owner_address) - id = self._connection.execute_kw( - ODOO_DB, self._uid, ODOO_PASSWORD, "account.move", "search", [[("partner_id", "=", customer_id)]] - ) - self._logger.debug(f"Find invoice with id: {id}") - return id \ No newline at end of file diff --git a/src/websocket.py b/src/websocket.py deleted file mode 100644 index 81bcac6..0000000 --- a/src/websocket.py +++ /dev/null @@ -1,68 +0,0 @@ -import websocket -import os -from dotenv import load_dotenv -import json -import rel -from utils.logger import Logger -from utils.decrypt_encrypt_msg import decrypt_message -from utils.messages import message_with_pinata_creds - -load_dotenv() - -LIBP2P_WS_SERVER = os.getenv("LIBP2P_WS_SERVER") -ADMIN_SEED = os.getenv("ADMIN_SEED") - - -class WSClient: - def __init__(self, odoo) -> None: - self.odoo = odoo - self._logger = Logger("websocket") - self._connect2server() - - def _connect2server(self): - # websocket.enableTrace(True) - self.ws = websocket.WebSocketApp( - url=LIBP2P_WS_SERVER, - on_open=self._on_connection, - on_message=self._on_message, - on_error=self._on_error, - on_close=self._on_close, - ) - - def run(self) -> None: - self.ws.run_forever(dispatcher=rel, reconnect=5) - rel.signal(2, rel.abort) # Keyboard Interrupt - rel.dispatch() - - def _on_connection(self, ws): - self._logger.debug(f"Connected to {LIBP2P_WS_SERVER}") - self.ws.send(json.dumps({"protocols_to_listen": ["/initialization"]})) - - def _on_message(self, ws, message): - json_message = json.loads(message) - self._logger.debug(f"Got msg: {json_message}") - if "email" in json_message: - decrypted_email = json_message["email"] - owner_address = json_message["owner_address"] - controller_address = json_message["controller_address"] - encrypted_email = decrypt_message(decrypted_email, controller_address, ADMIN_SEED) - rrs_user_id = self.odoo.check_if_rrs_user_exists(controller_address) - if rrs_user_id: - pinata_key, pinata_secret = self.odoo.retrieve_pinata_creds(controller_address, rrs_user_id) - if pinata_key: - msg = message_with_pinata_creds(pinata_key, pinata_secret, controller_address) - self.ws.send(msg) - return - else: - is_invoice_posted = self.odoo.check_if_invoice_posted(owner_address) - if is_invoice_posted: - return - self._logger.error(f"Couldn't get pinata creds. Creating a new one..") - self.odoo.create_rrs_user(encrypted_email, owner_address, controller_address) - self.odoo.create_invoice(owner_address, encrypted_email) - - def _on_error(self, ws, error): - self._logger.error(f"{error}") - - def _on_close(self, ws, close_status_code, close_msg): - self._logger.debug(f"Connection closed with status code {close_status_code} and message {close_msg}") diff --git a/utils/decrypt_encrypt_msg.py b/utils/decrypt_encrypt_msg.py deleted file mode 100644 index 5480d02..0000000 --- a/utils/decrypt_encrypt_msg.py +++ /dev/null @@ -1,34 +0,0 @@ -from robonomicsinterface import Account -from substrateinterface import KeypairType, Keypair - - -def decrypt_message(encrypted_text: str, sender_address: str, admin_seed: str) -> bytes: - """Decrypting data using admin private key and sender public key. - - :param encrypted_text: Encypted text from the file - :param sender_address: Sender's address - :param admin_seed: Admin's seed phrase - - :return: Decrypted text - """ - admin_keypair = Account(admin_seed, crypto_type=KeypairType.ED25519) - sender_kp = Keypair(ss58_address=sender_address, crypto_type=KeypairType.ED25519).public_key - if encrypted_text[:2] == "0x": - encrypted_text = encrypted_text[2:] - bytes_encrypted = bytes.fromhex(encrypted_text) - return admin_keypair.keypair.decrypt_message(bytes_encrypted, sender_kp) - - -def encrypt_message(text2decrypt: str, admin_seed: str, receiver_address: str) -> str: - """Encrypting data using admin private key and the receiver public key. - - :param text2decrypt: Text to encrypt - :param admin_seed: Admin's seed phrase - :param receiver_address: Receiver's address - - :return: Encrypted text - """ - admin_keypair = Account(admin_seed, crypto_type=KeypairType.ED25519).keypair - receiver_kp = Keypair(ss58_address=receiver_address, crypto_type=KeypairType.ED25519).public_key - encrypted = admin_keypair.encrypt_message(text2decrypt, receiver_kp) - return f"0x{encrypted.hex()}" diff --git a/utils/decryption.py b/utils/decryption.py new file mode 100644 index 0000000..2f24852 --- /dev/null +++ b/utils/decryption.py @@ -0,0 +1,58 @@ +from substrateinterface import Keypair, KeypairType +from robonomicsinterface import Account +import typing as tp +import json +import os +from dotenv import load_dotenv + +load_dotenv() +ADMIN_SEED = os.getenv("ADMIN_SEED") + + +def decrypt_message(data: tp.Union[str, dict], sender_address: str, logger) -> str: + """Decrypt message that was encrypted fo devices + + :param data: Ancrypted data + :param sender_address: Sender address + :param recipient_keypair: Recepient account keypair + + :return: Decrypted message + """ + try: + account = Account(ADMIN_SEED, crypto_type=KeypairType.ED25519) + admin_keypair = account.keypair + sender_public_key = Keypair(ss58_address=sender_address, crypto_type=KeypairType.ED25519).public_key + logger.debug(f"Start decrypt for device {admin_keypair.ss58_address}") + if isinstance(data, str): + data_json = json.loads(data) + else: + data_json = data + if admin_keypair.ss58_address in data_json: + decrypted_seed = _decrypt_message( + data_json[admin_keypair.ss58_address], + sender_public_key, + admin_keypair, + ).decode("utf-8") + decrypted_acc = Account(decrypted_seed, crypto_type=KeypairType.ED25519) + decrypted_data = _decrypt_message(data_json["data"], sender_public_key, decrypted_acc.keypair).decode( + "utf-8" + ) + return decrypted_data + else: + logger.error(f"Error in decrypt for devices: account is not in devices") + except Exception as e: + logger.error(f"Exception in decrypt for devices: {e}") + + +def _decrypt_message(encrypted_message: str, sender_public_key: bytes, admin_keypair) -> str: + """Decrypt message with recepient private key and sender puplic key + :param encrypted_message: Message to decrypt + :param sender_public_key: Sender public key + + :return: Decrypted message + """ + if encrypted_message[:2] == "0x": + encrypted_message = encrypted_message[2:] + bytes_encrypted = bytes.fromhex(encrypted_message) + + return admin_keypair.decrypt_message(bytes_encrypted, sender_public_key) \ No newline at end of file diff --git a/utils/encryption.py b/utils/encryption.py new file mode 100644 index 0000000..756485d --- /dev/null +++ b/utils/encryption.py @@ -0,0 +1,62 @@ +from substrateinterface import Keypair, KeypairType +from robonomicsinterface import Account +import typing as tp +import json +import os +from dotenv import load_dotenv + +load_dotenv() +ADMIN_SEED = os.getenv("ADMIN_SEED") + +def encrypt_for_users(data: str, users: tp.List[str], logger) -> str: + """ + Encrypt data for random generated private key, then encrypt this key for user from the list + + :param data: Data to encrypt + :param sender_kp: ED25519 account keypair that encrypts the data + :param users: List of ss58 ED25519 addresses + + :return: JSON string consists of encrypted data and a key encrypted for all accounts in the subscription + """ + try: + account = Account(ADMIN_SEED, crypto_type=KeypairType.ED25519) + admin_keypair = account.keypair + random_seed = Keypair.generate_mnemonic() + random_acc = Account(random_seed, crypto_type=KeypairType.ED25519) + encrypted_data = _encrypt_message( + str(data), admin_keypair, random_acc.keypair.public_key + ) + encrypted_keys = {} + for user in users: + try: + receiver_kp = Keypair( + ss58_address=user, crypto_type=KeypairType.ED25519 + ) + encrypted_key = _encrypt_message( + random_seed, admin_keypair, receiver_kp.public_key + ) + encrypted_keys[user] = encrypted_key + except Exception as e: + logger.error( + f"Faild to encrypt key for: {user} with error: {e}. Check your RWS users, you may have SR25519 address in users." + ) + encrypted_keys["data"] = encrypted_data + data_final = json.dumps(encrypted_keys) + return data_final + except Exception as e: + logger.error(f"Exception in encrypt for users: {e}") + +def _encrypt_message( + message: tp.Union[bytes, str], admin_keypair: Keypair, recipient_public_key: bytes +) -> str: + """Encrypt message with sender private key and recipient public key + + :param message: Message to encrypt + :param admin_keypair: Admin account Keypair + :param recipient_public_key: Recipient public key + + :return: encrypted message + """ + + encrypted = admin_keypair.encrypt_message(message, recipient_public_key) + return f"0x{encrypted.hex()}" \ No newline at end of file diff --git a/utils/messages.py b/utils/messages.py deleted file mode 100644 index 38b9bc4..0000000 --- a/utils/messages.py +++ /dev/null @@ -1,15 +0,0 @@ -import os -import json -from .decrypt_encrypt_msg import encrypt_message - -ADMIN_SEED = os.getenv("ADMIN_SEED") - -def message_with_pinata_creds(pinata_key: str, pinata_secret: str, controller_address: str) -> str: - pinata_key_encrypted = encrypt_message(pinata_key, ADMIN_SEED, controller_address) - pinata_secret_encrypted = encrypt_message(pinata_secret, ADMIN_SEED, controller_address) - msg = { - "protocol": f"/pinataCreds/{controller_address}", - "serverPeerId": "", - "data": {"public": pinata_key_encrypted, "private": pinata_secret_encrypted}, - } - return json.dumps(msg) diff --git a/utils/robonomics.py b/utils/robonomics.py deleted file mode 100644 index 401d96d..0000000 --- a/utils/robonomics.py +++ /dev/null @@ -1,21 +0,0 @@ -import robonomicsinterface as ri -import os -from dotenv import load_dotenv -from substrateinterface import KeypairType - -load_dotenv() -ADMIN_SEED = os.getenv("ADMIN_SEED") -WSS_ENDPOINT = os.getenv("WSS_ENDPOINT") - - -def transfer_xrt_2buy_subscription(owner_address: str) -> str: - """Sends XRT tokens to the owner address to buy a subscription. - - :param owner_address: Address of the subscription's owner. - - :return: Hash of the transfer transaction. - """ - account = ri.Account(remote_ws=WSS_ENDPOINT, seed=ADMIN_SEED, crypto_type=KeypairType.ED25519) - subscription_price = 10**9 + 3 - ri_common_functions = ri.CommonFunctions(account=account) - return ri_common_functions.transfer_tokens(target_address=owner_address, tokens=subscription_price)