diff --git a/apps/cic-ussd/cic_ussd/error.py b/apps/cic-ussd/cic_ussd/error.py index 006a7a49..256e77c6 100644 --- a/apps/cic-ussd/cic_ussd/error.py +++ b/apps/cic-ussd/cic_ussd/error.py @@ -18,7 +18,7 @@ class ActionDataNotFoundError(OSError): pass -class UserMetadataNotFoundError(OSError): +class MetadataNotFoundError(OSError): """Raised when metadata is expected but not available in cache.""" pass @@ -31,3 +31,10 @@ class UnsupportedMethodError(OSError): class CachedDataNotFoundError(OSError): """Raised when the method passed to the make request function is unsupported.""" pass + + +class MetadataStoreError(Exception): + """Raised when metadata storage fails""" + pass + + diff --git a/apps/cic-ussd/cic_ussd/metadata/__init__.py b/apps/cic-ussd/cic_ussd/metadata/__init__.py index 1c90189e..44c4a217 100644 --- a/apps/cic-ussd/cic_ussd/metadata/__init__.py +++ b/apps/cic-ussd/cic_ussd/metadata/__init__.py @@ -3,7 +3,10 @@ # third-party imports import requests from chainlib.eth.address import to_checksum -from hexathon import add_0x +from hexathon import ( + add_0x, + strip_0x, + ) # local imports from cic_ussd.error import UnsupportedMethodError @@ -40,4 +43,4 @@ def blockchain_address_to_metadata_pointer(blockchain_address: str): :return: :rtype: """ - return bytes.fromhex(blockchain_address[2:]) + return bytes.fromhex(strip_0x(blockchain_address)) diff --git a/apps/cic-ussd/cic_ussd/metadata/phone.py b/apps/cic-ussd/cic_ussd/metadata/phone.py new file mode 100644 index 00000000..5ce1bdc6 --- /dev/null +++ b/apps/cic-ussd/cic_ussd/metadata/phone.py @@ -0,0 +1,47 @@ +# standard imports +import logging +import os + +# external imports +from cic_types.models.person import generate_metadata_pointer +from cic_ussd.metadata import make_request +from cic_ussd.metadata.signer import Signer + +# local imports +from cic_ussd.error import MetadataStoreError + +logg = logging.getLogger().getChild(__name__) + + +class PhonePointerMetadata: + + base_url = None + + def __init__(self, identifier: bytes, engine: str): + """ + :param identifier: + :type identifier: + """ + + self.headers = { + 'X-CIC-AUTOMERGE': 'server', + 'Content-Type': 'application/json' + } + self.identifier = identifier + self.metadata_pointer = generate_metadata_pointer( + identifier=self.identifier, + cic_type='cic.phone' + ) + if self.base_url: + self.url = os.path.join(self.base_url, self.metadata_pointer) + self.engine = engine + + + def create(self, data: str): + try: + result = make_request(method='POST', url=self.url, data=data, headers=self.headers) + metadata = result.content + self.edit(data=metadata, engine=self.engine) + result.raise_for_status() + except requests.exceptions.HTTPError as error: + raise MetadataStoreError(error) diff --git a/apps/cic-ussd/cic_ussd/metadata/user.py b/apps/cic-ussd/cic_ussd/metadata/user.py index 6a63b1bf..2485c1fd 100644 --- a/apps/cic-ussd/cic_ussd/metadata/user.py +++ b/apps/cic-ussd/cic_ussd/metadata/user.py @@ -12,6 +12,7 @@ from cic_ussd.chain import Chain from cic_ussd.metadata import make_request from cic_ussd.metadata.signer import Signer from cic_ussd.redis import cache_data +from cic_ussd.error import MetadataStoreError logg = logging.getLogger() @@ -28,7 +29,7 @@ class UserMetadata: :param identifier: :type identifier: """ - self. headers = { + self.headers = { 'X-CIC-AUTOMERGE': 'server', 'Content-Type': 'application/json' } @@ -49,7 +50,7 @@ class UserMetadata: logg.info(f'Get sign material response status: {result.status_code}') result.raise_for_status() except requests.exceptions.HTTPError as error: - raise RuntimeError(error) + raise MetadataStoreError(error) def edit(self, data: bytes, engine: str): """ @@ -79,7 +80,7 @@ class UserMetadata: logg.info(f'Signed content submission status: {result.status_code}.') result.raise_for_status() except requests.exceptions.HTTPError as error: - raise RuntimeError(error) + raise MetadataStoreError(error) def query(self): result = make_request(method='GET', url=self.url) @@ -99,4 +100,4 @@ class UserMetadata: logg.info('The data is not available and might need to be added.') result.raise_for_status() except requests.exceptions.HTTPError as error: - raise RuntimeError(error) + raise MetadataNotFoundError(error) diff --git a/apps/cic-ussd/cic_ussd/processor.py b/apps/cic-ussd/cic_ussd/processor.py index 5dbf7b7c..413ee0e0 100644 --- a/apps/cic-ussd/cic_ussd/processor.py +++ b/apps/cic-ussd/cic_ussd/processor.py @@ -15,7 +15,7 @@ from cic_ussd.balance import BalanceManager, compute_operational_balance, get_ca from cic_ussd.chain import Chain from cic_ussd.db.models.user import AccountStatus, User from cic_ussd.db.models.ussd_session import UssdSession -from cic_ussd.error import UserMetadataNotFoundError +from cic_ussd.error import MetadataNotFoundError from cic_ussd.menu.ussd_menu import UssdMenu from cic_ussd.metadata import blockchain_address_to_metadata_pointer from cic_ussd.phone_number import get_user_by_phone_number @@ -235,7 +235,7 @@ def process_display_user_metadata(user: User, display_key: str): products=products ) else: - raise UserMetadataNotFoundError(f'Expected user metadata but found none in cache for key: {user.blockchain_address}') + raise MetadataNotFoundError(f'Expected user metadata but found none in cache for key: {user.blockchain_address}') def process_account_statement(user: User, display_key: str, ussd_session: dict): diff --git a/apps/cic-ussd/cic_ussd/state_machine/logic/user.py b/apps/cic-ussd/cic_ussd/state_machine/logic/user.py index e85a26e6..5849bd77 100644 --- a/apps/cic-ussd/cic_ussd/state_machine/logic/user.py +++ b/apps/cic-ussd/cic_ussd/state_machine/logic/user.py @@ -11,7 +11,7 @@ from cic_types.models.person import generate_vcard_from_contact_data, manage_ide # local imports from cic_ussd.chain import Chain from cic_ussd.db.models.user import User -from cic_ussd.error import UserMetadataNotFoundError +from cic_ussd.error import MetadataNotFoundError from cic_ussd.metadata import blockchain_address_to_metadata_pointer from cic_ussd.operations import save_to_in_memory_ussd_session_data from cic_ussd.redis import get_cached_data @@ -181,7 +181,7 @@ def edit_user_metadata_attribute(state_machine_data: Tuple[str, dict, User]): user_metadata = get_cached_data(key=key) if not user_metadata: - raise UserMetadataNotFoundError(f'Expected user metadata but found none in cache for key: {blockchain_address}') + raise MetadataNotFoundError(f'Expected user metadata but found none in cache for key: {blockchain_address}') given_name = ussd_session.get('session_data').get('given_name') family_name = ussd_session.get('session_data').get('family_name') diff --git a/apps/cic-ussd/cic_ussd/tasks/base.py b/apps/cic-ussd/cic_ussd/tasks/base.py index 4b5e535b..fb50ffb0 100644 --- a/apps/cic-ussd/cic_ussd/tasks/base.py +++ b/apps/cic-ussd/cic_ussd/tasks/base.py @@ -5,6 +5,7 @@ import celery import sqlalchemy # local imports +from cic_ussd.error import MetadataStoreError class CriticalTask(celery.Task): @@ -18,3 +19,9 @@ class CriticalSQLAlchemyTask(CriticalTask): sqlalchemy.exc.DatabaseError, sqlalchemy.exc.TimeoutError, ) + + +class CriticalMetadataTask(CriticalTask): + autoretry_for = ( + MetadataStoreError, + ) diff --git a/apps/cic-ussd/cic_ussd/tasks/callback_handler.py b/apps/cic-ussd/cic_ussd/tasks/callback_handler.py index 0b569d04..e5f3f448 100644 --- a/apps/cic-ussd/cic_ussd/tasks/callback_handler.py +++ b/apps/cic-ussd/cic_ussd/tasks/callback_handler.py @@ -53,6 +53,18 @@ def process_account_creation_callback(self, result: str, url: str, status_code: session.add(user) session.commit() session.close() + + queue = self.request.delivery_info.get('routing_key') + s = celery.signature( + 'cic_ussd.tasks.metadata.add_phone_pointer', + [ + result, + phone_number, + 'pgp', + ] + queue=queue, + ) + s.apply_async() # expire cache cache.expire(task_id, timedelta(seconds=180)) diff --git a/apps/cic-ussd/cic_ussd/tasks/metadata.py b/apps/cic-ussd/cic_ussd/tasks/metadata.py index 4410148a..41d421a4 100644 --- a/apps/cic-ussd/cic_ussd/tasks/metadata.py +++ b/apps/cic-ussd/cic_ussd/tasks/metadata.py @@ -8,6 +8,8 @@ import celery # local imports from cic_ussd.metadata import blockchain_address_to_metadata_pointer from cic_ussd.metadata.user import UserMetadata +from cic_ussd.metadata.phone import PhonePointerMetadata +from cic_ussd.tasks.base import CriticalMetadataTask celery_app = celery.current_app logg = logging.getLogger() @@ -46,3 +48,10 @@ def edit_user_metadata(blockchain_address: str, data: bytes, engine: str): identifier = blockchain_address_to_metadata_pointer(blockchain_address=blockchain_address) user_metadata_client = UserMetadata(identifier=identifier) user_metadata_client.edit(data=data, engine=engine) + + +@celery_app.task(bind=True, base=CriticalMetadataTask) +def add_phone_pointer(blockchain_address: str, phone: str, engine: str): + identifier = blockchain_address_to_metadata_pointer(blockchain_address=blockchain_address) + phone_metadata_client = PhonePointerMetadata(identifier=identifier, engine=engine) + phone_metadata_client.create(data=phone)