USSD Hardening and Cleanups

This commit is contained in:
2022-01-04 16:16:00 +00:00
parent 03c7c1ddbc
commit 46f25e5678
40 changed files with 1582 additions and 1024 deletions

View File

@@ -19,34 +19,33 @@ from cic_ussd.account.metadata import get_cached_preferred_language
from cic_ussd.account.statement import (
get_cached_statement,
parse_statement_transactions,
query_statement,
statement_transaction_set
)
query_statement)
from cic_ussd.account.tokens import (create_account_tokens_list,
get_active_token_symbol,
get_cached_token_data,
get_cached_token_symbol_list,
get_cached_token_data_list,
parse_token_list,
token_list_set)
parse_token_list)
from cic_ussd.account.transaction import from_wei, to_wei
from cic_ussd.cache import cache_data_key, cache_data
from cic_ussd.cache import cache_data_key, cache_data, get_cached_data
from cic_ussd.db.models.account import Account
from cic_ussd.metadata import PersonMetadata
from cic_ussd.phone_number import Support
from cic_ussd.processor.util import parse_person_metadata
from cic_ussd.processor.util import parse_person_metadata, ussd_menu_list, wait_for_session_data
from cic_ussd.session.ussd_session import save_session_data
from cic_ussd.state_machine.logic.language import preferred_langauge_from_selection
from cic_ussd.translation import translation_for
from sqlalchemy.orm.session import Session
logg = logging.getLogger(__name__)
logg = logging.getLogger(__file__)
class MenuProcessor:
def __init__(self, account: Account, display_key: str, menu_name: str, session: Session, ussd_session: dict):
self.account = account
self.display_key = display_key
self.identifier = bytes.fromhex(self.account.blockchain_address)
if account:
self.identifier = bytes.fromhex(self.account.blockchain_address)
self.menu_name = menu_name
self.session = session
self.ussd_session = ussd_session
@@ -89,36 +88,29 @@ class MenuProcessor:
:rtype:
"""
cached_statement = get_cached_statement(self.account.blockchain_address)
transaction_sets = []
if cached_statement:
statement = json.loads(cached_statement)
statement_transactions = parse_statement_transactions(statement)
transaction_sets = [statement_transactions[tx:tx + 3] for tx in range(0, len(statement_transactions), 3)]
preferred_language = get_cached_preferred_language(self.account.blockchain_address)
if not preferred_language:
preferred_language = i18n.config.get('fallback')
no_transaction_history = statement_transaction_set(preferred_language, transaction_sets)
first_transaction_set = no_transaction_history
middle_transaction_set = no_transaction_history
last_transaction_set = no_transaction_history
if transaction_sets:
first_transaction_set = statement_transaction_set(preferred_language, transaction_sets[0])
if len(transaction_sets) >= 2:
middle_transaction_set = statement_transaction_set(preferred_language, transaction_sets[1])
if len(transaction_sets) >= 3:
last_transaction_set = statement_transaction_set(preferred_language, transaction_sets[2])
if self.display_key == 'ussd.kenya.first_transaction_set':
statement_list = []
if cached_statement:
statement_list = parse_statement_transactions(statement=json.loads(cached_statement))
fallback = translation_for('helpers.no_transaction_history', preferred_language)
transaction_sets = ussd_menu_list(fallback=fallback, menu_list=statement_list, split=3)
if self.display_key == 'ussd.first_transaction_set':
return translation_for(
self.display_key, preferred_language, first_transaction_set=first_transaction_set
self.display_key, preferred_language, first_transaction_set=transaction_sets[0]
)
if self.display_key == 'ussd.kenya.middle_transaction_set':
if self.display_key == 'ussd.middle_transaction_set':
return translation_for(
self.display_key, preferred_language, middle_transaction_set=middle_transaction_set
self.display_key, preferred_language, middle_transaction_set=transaction_sets[1]
)
if self.display_key == 'ussd.kenya.last_transaction_set':
if self.display_key == 'ussd.last_transaction_set':
return translation_for(
self.display_key, preferred_language, last_transaction_set=last_transaction_set
self.display_key, preferred_language, last_transaction_set=transaction_sets[2]
)
def add_guardian_pin_authorization(self):
@@ -129,7 +121,7 @@ class MenuProcessor:
preferred_language = get_cached_preferred_language(self.account.blockchain_address)
if not preferred_language:
preferred_language = i18n.config.get('fallback')
set_guardians = self.account.get_guardians()
set_guardians = self.account.get_guardians()[:3]
if set_guardians:
guardians_list = ''
guardians_list_header = translation_for('helpers.guardians_list_header', preferred_language)
@@ -145,36 +137,30 @@ class MenuProcessor:
def account_tokens(self) -> str:
cached_token_data_list = get_cached_token_data_list(self.account.blockchain_address)
token_data_list = parse_token_list(cached_token_data_list)
token_list_sets = [token_data_list[tds:tds + 3] for tds in range(0, len(token_data_list), 3)]
preferred_language = get_cached_preferred_language(self.account.blockchain_address)
if not preferred_language:
preferred_language = i18n.config.get('fallback')
no_token_list = token_list_set(preferred_language, [])
first_account_tokens_set = no_token_list
middle_account_tokens_set = no_token_list
last_account_tokens_set = no_token_list
if token_list_sets:
data = {
'account_tokens_list': cached_token_data_list
}
save_session_data(data=data, queue='cic-ussd', session=self.session, ussd_session=self.ussd_session)
first_account_tokens_set = token_list_set(preferred_language, token_list_sets[0])
if len(token_list_sets) >= 2:
middle_account_tokens_set = token_list_set(preferred_language, token_list_sets[1])
if len(token_list_sets) >= 3:
last_account_tokens_set = token_list_set(preferred_language, token_list_sets[2])
if self.display_key == 'ussd.kenya.first_account_tokens_set':
fallback = translation_for('helpers.no_tokens_list', preferred_language)
token_list_sets = ussd_menu_list(fallback=fallback, menu_list=token_data_list, split=3)
data = {
'account_tokens_list': cached_token_data_list
}
save_session_data(data=data, queue='cic-ussd', session=self.session, ussd_session=self.ussd_session)
if self.display_key == 'ussd.first_account_tokens_set':
return translation_for(
self.display_key, preferred_language, first_account_tokens_set=first_account_tokens_set
self.display_key, preferred_language, first_account_tokens_set=token_list_sets[0]
)
if self.display_key == 'ussd.kenya.middle_account_tokens_set':
if self.display_key == 'ussd.middle_account_tokens_set':
return translation_for(
self.display_key, preferred_language, middle_account_tokens_set=middle_account_tokens_set
self.display_key, preferred_language, middle_account_tokens_set=token_list_sets[1]
)
if self.display_key == 'ussd.kenya.last_account_tokens_set':
if self.display_key == 'ussd.last_account_tokens_set':
return translation_for(
self.display_key, preferred_language, last_account_tokens_set=last_account_tokens_set
self.display_key, preferred_language, last_account_tokens_set=token_list_sets[2]
)
def help(self) -> str:
@@ -222,7 +208,7 @@ class MenuProcessor:
remaining_attempts = 3
remaining_attempts -= self.account.failed_pin_attempts
retry_pin_entry = translation_for(
'ussd.kenya.retry_pin_entry', preferred_language, remaining_attempts=remaining_attempts
'ussd.retry_pin_entry', preferred_language, remaining_attempts=remaining_attempts
)
return translation_for(
f'{self.display_key}.retry', preferred_language, retry_pin_entry=retry_pin_entry
@@ -238,6 +224,38 @@ class MenuProcessor:
guardian = Account.get_by_phone_number(guardian_phone_number, self.session)
return guardian.standard_metadata_id()
def language(self):
key = cache_data_key('system:languages'.encode('utf-8'), MetadataPointer.NONE)
cached_system_languages = get_cached_data(key)
language_list: list = json.loads(cached_system_languages)
if self.account:
preferred_language = get_cached_preferred_language(self.account.blockchain_address)
else:
preferred_language = i18n.config.get('fallback')
fallback = translation_for('helpers.no_language_list', preferred_language)
language_list_sets = ussd_menu_list(fallback=fallback, menu_list=language_list, split=3)
if self.display_key in ['ussd.initial_language_selection', 'ussd.select_preferred_language']:
return translation_for(
self.display_key, preferred_language, first_language_set=language_list_sets[0]
)
if 'middle_language_set' in self.display_key:
return translation_for(
self.display_key, preferred_language, middle_language_set=language_list_sets[1]
)
if 'last_language_set' in self.display_key:
return translation_for(
self.display_key, preferred_language, last_language_set=language_list_sets[2]
)
def account_creation_prompt(self):
preferred_language = preferred_langauge_from_selection(self.ussd_session.get('user_input'))
return translation_for(self.display_key, preferred_language)
def reset_guarded_pin_authorization(self):
guarded_account_information = self.guarded_account_metadata()
return self.pin_authorization(guarded_account_information=guarded_account_information)
@@ -381,8 +399,9 @@ class MenuProcessor:
)
def exit_invalid_menu_option(self):
preferred_language = get_cached_preferred_language(self.account.blockchain_address)
if not preferred_language:
if self.account:
preferred_language = get_cached_preferred_language(self.account.blockchain_address)
else:
preferred_language = i18n.config.get('fallback')
return translation_for(self.display_key, preferred_language, support_phone=Support.phone_number)
@@ -390,7 +409,7 @@ class MenuProcessor:
preferred_language = get_cached_preferred_language(self.account.blockchain_address)
if not preferred_language:
preferred_language = i18n.config.get('fallback')
return translation_for('ussd.kenya.exit_pin_blocked', preferred_language, support_phone=Support.phone_number)
return translation_for('ussd.exit_pin_blocked', preferred_language, support_phone=Support.phone_number)
def exit_successful_token_selection(self) -> str:
selected_token = self.ussd_session.get('data').get('selected_token')
@@ -445,6 +464,9 @@ def response(account: Account, display_key: str, menu_name: str, session: Sessio
"""
menu_processor = MenuProcessor(account, display_key, menu_name, session, ussd_session)
if menu_name == 'account_creation_prompt':
return menu_processor.account_creation_prompt()
if menu_name == 'start':
return menu_processor.start_menu()
@@ -502,6 +524,9 @@ def response(account: Account, display_key: str, menu_name: str, session: Sessio
if 'account_tokens_set' in menu_name:
return menu_processor.account_tokens()
if 'language' in menu_name:
return menu_processor.language()
if menu_name == 'display_user_metadata':
return menu_processor.person_metadata()
@@ -515,5 +540,4 @@ def response(account: Account, display_key: str, menu_name: str, session: Sessio
return menu_processor.exit_successful_token_selection()
preferred_language = get_cached_preferred_language(account.blockchain_address)
return translation_for(display_key, preferred_language)

View File

@@ -8,7 +8,7 @@ from sqlalchemy.orm.session import Session
from tinydb.table import Document
# local imports
from cic_ussd.db.models.account import Account, create
from cic_ussd.db.models.account import Account
from cic_ussd.db.models.base import SessionBase
from cic_ussd.db.models.ussd_session import UssdSession
from cic_ussd.menu.ussd_menu import UssdMenu
@@ -16,7 +16,6 @@ from cic_ussd.processor.menu import response
from cic_ussd.processor.util import latest_input, resume_last_ussd_session
from cic_ussd.session.ussd_session import create_or_update_session, persist_ussd_session
from cic_ussd.state_machine import UssdStateMachine
from cic_ussd.translation import translation_for
from cic_ussd.validator import is_valid_response
@@ -36,9 +35,6 @@ def handle_menu(account: Account, session: Session) -> Document:
last_ussd_session = UssdSession.last_ussd_session(account.phone_number, session)
if last_ussd_session:
return resume_last_ussd_session(last_ussd_session.state)
elif not account.has_preferred_language():
return UssdMenu.find_by_name('initial_language_selection')
else:
return UssdMenu.find_by_name('initial_pin_entry')
@@ -71,16 +67,13 @@ def get_menu(account: Account,
return UssdMenu.find_by_name(state)
def handle_menu_operations(chain_str: str,
external_session_id: str,
def handle_menu_operations(external_session_id: str,
phone_number: str,
queue: str,
service_code: str,
session,
user_input: str):
"""
:param chain_str:
:type chain_str:
:param external_session_id:
:type external_session_id:
:param phone_number:
@@ -100,10 +93,38 @@ def handle_menu_operations(chain_str: str,
account: Account = Account.get_by_phone_number(phone_number, session)
if account:
return handle_account_menu_operations(account, external_session_id, queue, session, service_code, user_input)
create(chain_str, phone_number, session)
menu = UssdMenu.find_by_name('account_creation_prompt')
preferred_language = i18n.config.get('fallback')
create_or_update_session(
else:
return handle_no_account_menu_operations(
account, external_session_id, phone_number, queue, session, service_code, user_input)
def handle_no_account_menu_operations(account: Optional[Account],
external_session_id: str,
phone_number: str,
queue: str,
session: Session,
service_code: str,
user_input: str):
"""
:param account:
:type account:
:param external_session_id:
:type external_session_id:
:param phone_number:
:type phone_number:
:param queue:
:type queue:
:param session:
:type session:
:param service_code:
:type service_code:
:param user_input:
:type user_input:
:return:
:rtype:
"""
menu = UssdMenu.find_by_name('initial_language_selection')
ussd_session = create_or_update_session(
external_session_id=external_session_id,
msisdn=phone_number,
service_code=service_code,
@@ -111,7 +132,20 @@ def handle_menu_operations(chain_str: str,
session=session,
user_input=user_input)
persist_ussd_session(external_session_id, queue)
return translation_for('ussd.kenya.account_creation_prompt', preferred_language)
last_ussd_session: UssdSession = UssdSession.last_ussd_session(phone_number, session)
if last_ussd_session:
if not user_input:
menu = resume_last_ussd_session(last_ussd_session.state)
else:
session = SessionBase.bind_session(session)
state = next_state(account, session, user_input, last_ussd_session.to_json())
menu = UssdMenu.find_by_name(state)
return response(account=account,
display_key=menu.get('display_key'),
menu_name=menu.get('name'),
session=session,
ussd_session=ussd_session.to_json())
def handle_account_menu_operations(account: Account,
@@ -152,15 +186,12 @@ def handle_account_menu_operations(account: Account,
if last_ussd_session:
ussd_session = create_or_update_session(
external_session_id, phone_number, service_code, user_input, menu.get('name'), session,
last_ussd_session.data
)
last_ussd_session.data)
else:
ussd_session = create_or_update_session(
external_session_id, phone_number, service_code, user_input, menu.get('name'), session, None
)
external_session_id, phone_number, service_code, user_input, menu.get('name'), session, {})
menu_response = response(
account, menu.get('display_key'), menu.get('name'), session, ussd_session.to_json()
)
account, menu.get('display_key'), menu.get('name'), session, ussd_session.to_json())
if not is_valid_response(menu_response):
raise ValueError(f'Invalid response: {response}')
persist_ussd_session(external_session_id, queue)

View File

@@ -3,7 +3,7 @@ import datetime
import json
import logging
import time
from typing import Union
from typing import List, Union
# external imports
from cic_types.condiments import MetadataPointer
@@ -21,9 +21,7 @@ logg = logging.getLogger(__file__)
def latest_input(user_input: str) -> str:
"""
:param user_input:
:type user_input:
:return:
:rtype:
"""
return user_input.split('*')[-1]
@@ -85,6 +83,27 @@ def resume_last_ussd_session(last_state: str) -> Document:
return UssdMenu.find_by_name(last_state)
def ussd_menu_list(fallback: str, menu_list: list, split: int = 3) -> List[str]:
"""
:param fallback:
:type fallback:
:param menu_list:
:type menu_list:
:param split:
:type split:
:return:
:rtype:
"""
menu_list_sets = [menu_list[item:item + split] for item in range(0, len(menu_list), split)]
menu_list_reprs = []
for i in range(split):
try:
menu_list_reprs.append(''.join(f'{list_set_item}\n' for list_set_item in menu_list_sets[i]).rstrip('\n'))
except IndexError:
menu_list_reprs.append(fallback)
return menu_list_reprs
def wait_for_cache(identifier: Union[list, bytes], resource_name: str, salt: MetadataPointer, interval: int = 1, max_retry: int = 5):
"""
:param identifier:
@@ -132,17 +151,28 @@ def wait_for_session_data(resource_name: str, session_data_key: str, ussd_sessio
:return:
:rtype:
"""
session_data = ussd_session.get('data').get(session_data_key)
counter = 0
while session_data is None:
logg.debug(f'Waiting for: {resource_name}. Checking after: {interval} ...')
data = ussd_session.get('data')
data_poller = 0
while not data:
logg.debug(f'Waiting for data object on ussd session: {ussd_session.get("external_session_id")}')
logg.debug(f'Data poller at: {data_poller}. Checking again after: {interval} secs...')
time.sleep(interval)
counter += 1
session_data = ussd_session.get('data').get(session_data_key)
if session_data is not None:
logg.debug(f'{resource_name} now available.')
data_poller += 1
if data:
logg.debug(f'Data object found, proceeding to poll for: {session_data_key}')
break
else:
if counter == max_retry:
logg.debug(f'Could not find: {resource_name} within: {max_retry}')
if data:
session_data_poller = 0
session_data = data.get(session_data_key)
while not session_data_key:
logg.debug(
f'Session data poller at: {data_poller} with max retry at: {max_retry}. Checking again after: {interval} secs...')
time.sleep(interval)
session_data_poller += 1
if session_data:
logg.debug(f'{resource_name} now available.')
break
elif session_data_poller >= max_retry:
logg.debug(f'Could not find data object within: {max_retry}')