2021-02-06 16:13:47 +01:00
|
|
|
# standard imports
|
|
|
|
import logging
|
|
|
|
from typing import Optional
|
|
|
|
import json
|
|
|
|
|
2021-08-06 18:29:01 +02:00
|
|
|
# external imports
|
|
|
|
import celery
|
2021-02-06 16:13:47 +01:00
|
|
|
from redis import Redis
|
2021-08-06 18:29:01 +02:00
|
|
|
from sqlalchemy.orm.session import Session
|
2021-02-06 16:13:47 +01:00
|
|
|
|
2021-08-06 18:29:01 +02:00
|
|
|
# local imports
|
|
|
|
from cic_ussd.cache import Cache
|
|
|
|
from cic_ussd.db.models.base import SessionBase
|
|
|
|
from cic_ussd.db.models.ussd_session import UssdSession as DbUssdSession
|
2021-02-06 16:13:47 +01:00
|
|
|
|
2021-11-29 16:04:50 +01:00
|
|
|
logg = logging.getLogger(__file__)
|
2021-02-06 16:13:47 +01:00
|
|
|
|
|
|
|
|
|
|
|
class UssdSession:
|
|
|
|
"""
|
|
|
|
This class defines the USSD session object that is called whenever a user interacts with the system.
|
2021-08-06 18:29:01 +02:00
|
|
|
:cvar store: The in-memory redis cache.
|
|
|
|
:type store: Redis
|
2021-02-06 16:13:47 +01:00
|
|
|
"""
|
2021-08-06 18:29:01 +02:00
|
|
|
store: Redis = None
|
2021-02-06 16:13:47 +01:00
|
|
|
|
|
|
|
def __init__(self,
|
|
|
|
external_session_id: str,
|
|
|
|
msisdn: str,
|
2021-08-06 18:29:01 +02:00
|
|
|
service_code: str,
|
2021-02-06 16:13:47 +01:00
|
|
|
state: str,
|
2021-08-06 18:29:01 +02:00
|
|
|
user_input: str,
|
|
|
|
data: Optional[dict] = None):
|
2021-02-06 16:13:47 +01:00
|
|
|
"""
|
|
|
|
This function is called whenever a USSD session object is created and saves the instance to a JSON DB.
|
|
|
|
:param external_session_id: The Africa's Talking session ID.
|
|
|
|
:type external_session_id: str.
|
|
|
|
:param service_code: The USSD service code from which the user used to gain access to the system.
|
|
|
|
:type service_code: str.
|
|
|
|
:param msisdn: The user's phone number.
|
|
|
|
:type msisdn: str.
|
|
|
|
:param user_input: The data or choice the user has made while interacting with the system.
|
|
|
|
:type user_input: str.
|
|
|
|
:param state: The name of the USSD menu that the user was interacting with.
|
|
|
|
:type state: str.
|
2021-08-06 18:29:01 +02:00
|
|
|
:param data: Any additional data that was persisted during the user's interaction with the system.
|
|
|
|
:type data: dict.
|
2021-02-06 16:13:47 +01:00
|
|
|
"""
|
2021-08-06 18:29:01 +02:00
|
|
|
self.data = data
|
2021-02-06 16:13:47 +01:00
|
|
|
self.external_session_id = external_session_id
|
|
|
|
self.msisdn = msisdn
|
2021-08-06 18:29:01 +02:00
|
|
|
self.service_code = service_code
|
2021-02-06 16:13:47 +01:00
|
|
|
self.state = state
|
2021-08-06 18:29:01 +02:00
|
|
|
self.user_input = user_input
|
|
|
|
|
|
|
|
session = self.store.get(external_session_id)
|
2021-02-06 16:13:47 +01:00
|
|
|
if session:
|
|
|
|
session = json.loads(session)
|
|
|
|
self.version = session.get('version') + 1
|
|
|
|
else:
|
|
|
|
self.version = 1
|
|
|
|
|
|
|
|
self.session = {
|
2021-08-06 18:29:01 +02:00
|
|
|
'data': self.data,
|
2021-02-06 16:13:47 +01:00
|
|
|
'external_session_id': self.external_session_id,
|
|
|
|
'msisdn': self.msisdn,
|
2021-08-06 18:29:01 +02:00
|
|
|
'service_code': self.service_code,
|
2021-02-06 16:13:47 +01:00
|
|
|
'state': self.state,
|
2021-08-06 18:29:01 +02:00
|
|
|
'user_input': self.user_input,
|
2021-02-06 16:13:47 +01:00
|
|
|
'version': self.version
|
|
|
|
}
|
2021-08-06 18:29:01 +02:00
|
|
|
self.store.set(self.external_session_id, json.dumps(self.session))
|
|
|
|
self.store.persist(self.external_session_id)
|
2021-02-06 16:13:47 +01:00
|
|
|
|
|
|
|
def set_data(self, key: str, value: str) -> None:
|
|
|
|
"""
|
|
|
|
This function adds or updates data to the session data.
|
|
|
|
:param key: The name used to identify the data.
|
|
|
|
:type key: str.
|
|
|
|
:param value: The actual data to be stored in the session data.
|
|
|
|
:type value: str.
|
|
|
|
"""
|
2021-08-06 18:29:01 +02:00
|
|
|
if self.data is None:
|
|
|
|
self.data = {}
|
|
|
|
self.data[key] = value
|
|
|
|
self.store.set(self.external_session_id, json.dumps(self.session))
|
2021-02-06 16:13:47 +01:00
|
|
|
|
|
|
|
def get_data(self, key: str) -> Optional[str]:
|
|
|
|
"""
|
|
|
|
This function attempts to fetch data from the session data using the identifier for the specific data.
|
|
|
|
:param key: The name used as the identifier for the specific data.
|
|
|
|
:type key: str.
|
|
|
|
:return: This function returns the queried data if found, else it doesn't return any value.
|
|
|
|
:rtype: str.
|
|
|
|
"""
|
2021-08-06 18:29:01 +02:00
|
|
|
if self.data is not None:
|
|
|
|
return self.data.get(key)
|
2021-02-06 16:13:47 +01:00
|
|
|
else:
|
|
|
|
return None
|
|
|
|
|
|
|
|
def to_json(self):
|
|
|
|
""" This function serializes the in memory ussd session object to a JSON object
|
|
|
|
:return: A JSON object of a ussd session in memory
|
|
|
|
:rtype: dict
|
|
|
|
"""
|
|
|
|
return {
|
2021-08-06 18:29:01 +02:00
|
|
|
"data": self.data,
|
2021-02-06 16:13:47 +01:00
|
|
|
"external_session_id": self.external_session_id,
|
|
|
|
"msisdn": self.msisdn,
|
|
|
|
"user_input": self.user_input,
|
2021-08-06 18:29:01 +02:00
|
|
|
"service_code": self.service_code,
|
2021-02-06 16:13:47 +01:00
|
|
|
"state": self.state,
|
|
|
|
"version": self.version
|
|
|
|
}
|
2021-08-06 18:29:01 +02:00
|
|
|
|
|
|
|
|
|
|
|
def create_ussd_session(
|
|
|
|
state: str,
|
|
|
|
external_session_id: str,
|
|
|
|
msisdn: str,
|
|
|
|
service_code: str,
|
|
|
|
user_input: str,
|
|
|
|
data: Optional[dict] = None) -> UssdSession:
|
|
|
|
"""
|
|
|
|
:param state:
|
|
|
|
:type state:
|
|
|
|
:param external_session_id:
|
|
|
|
:type external_session_id:
|
|
|
|
:param msisdn:
|
|
|
|
:type msisdn:
|
|
|
|
:param service_code:
|
|
|
|
:type service_code:
|
|
|
|
:param user_input:
|
|
|
|
:type user_input:
|
|
|
|
:param data:
|
|
|
|
:type data:
|
|
|
|
:return:
|
|
|
|
:rtype:
|
|
|
|
"""
|
|
|
|
return UssdSession(external_session_id=external_session_id,
|
|
|
|
msisdn=msisdn,
|
|
|
|
user_input=user_input,
|
|
|
|
state=state,
|
|
|
|
service_code=service_code,
|
|
|
|
data=data
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2021-08-29 11:55:47 +02:00
|
|
|
def update_ussd_session(ussd_session: DbUssdSession,
|
2021-08-06 18:29:01 +02:00
|
|
|
user_input: str,
|
|
|
|
state: str,
|
|
|
|
data: Optional[dict] = None) -> UssdSession:
|
|
|
|
""""""
|
|
|
|
if data is None:
|
|
|
|
data = ussd_session.data
|
|
|
|
|
|
|
|
return UssdSession(
|
|
|
|
external_session_id=ussd_session.external_session_id,
|
|
|
|
msisdn=ussd_session.msisdn,
|
|
|
|
user_input=user_input,
|
|
|
|
state=state,
|
|
|
|
service_code=ussd_session.service_code,
|
|
|
|
data=data
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def create_or_update_session(external_session_id: str,
|
|
|
|
msisdn: str,
|
|
|
|
service_code: str,
|
|
|
|
user_input: str,
|
|
|
|
state: str,
|
|
|
|
session,
|
|
|
|
data: Optional[dict] = None) -> UssdSession:
|
|
|
|
"""
|
|
|
|
:param external_session_id:
|
|
|
|
:type external_session_id:
|
|
|
|
:param msisdn:
|
|
|
|
:type msisdn:
|
|
|
|
:param service_code:
|
|
|
|
:type service_code:
|
|
|
|
:param user_input:
|
|
|
|
:type user_input:
|
|
|
|
:param state:
|
|
|
|
:type state:
|
|
|
|
:param session:
|
|
|
|
:type session:
|
|
|
|
:param data:
|
|
|
|
:type data:
|
|
|
|
:return:
|
|
|
|
:rtype:
|
|
|
|
"""
|
|
|
|
session = SessionBase.bind_session(session=session)
|
|
|
|
existing_ussd_session = session.query(DbUssdSession).filter_by(
|
|
|
|
external_session_id=external_session_id).first()
|
|
|
|
|
|
|
|
if existing_ussd_session:
|
|
|
|
ussd_session = update_ussd_session(ussd_session=existing_ussd_session,
|
|
|
|
state=state,
|
|
|
|
user_input=user_input,
|
|
|
|
data=data
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
ussd_session = create_ussd_session(external_session_id=external_session_id,
|
|
|
|
msisdn=msisdn,
|
|
|
|
service_code=service_code,
|
|
|
|
user_input=user_input,
|
|
|
|
state=state,
|
|
|
|
data=data
|
|
|
|
)
|
|
|
|
SessionBase.release_session(session=session)
|
|
|
|
return ussd_session
|
|
|
|
|
|
|
|
|
|
|
|
def persist_ussd_session(external_session_id: str, queue: Optional[str]):
|
|
|
|
"""This function asynchronously retrieves a cached ussd session object matching an external ussd session id and adds
|
|
|
|
it to persistent storage.
|
|
|
|
:param external_session_id: Session id value provided by ussd service provided.
|
|
|
|
:type external_session_id: str
|
|
|
|
:param queue: Name of worker queue to submit tasks to.
|
|
|
|
:type queue: str
|
|
|
|
"""
|
|
|
|
s_persist_ussd_session = celery.signature(
|
|
|
|
'cic_ussd.tasks.ussd_session.persist_session_to_db',
|
|
|
|
[external_session_id],
|
|
|
|
queue=queue
|
|
|
|
)
|
|
|
|
s_persist_ussd_session.apply_async()
|
|
|
|
|
|
|
|
|
|
|
|
def save_session_data(queue: Optional[str], session: Session, data: dict, ussd_session: dict):
|
|
|
|
"""This function is stores information to the session data attribute of a cached ussd session object.
|
|
|
|
:param data: A dictionary containing data for a specific ussd session in redis that needs to be saved
|
|
|
|
temporarily.
|
|
|
|
:type data: dict
|
|
|
|
:param queue: The queue on which the celery task should run.
|
|
|
|
:type queue: str
|
|
|
|
:param session: Database session object.
|
|
|
|
:type session: Session
|
|
|
|
:param ussd_session: A ussd session passed to the state machine.
|
|
|
|
:type ussd_session: UssdSession
|
|
|
|
"""
|
2021-11-29 16:04:50 +01:00
|
|
|
logg.debug(f'Saving: {data} session data to: {ussd_session}')
|
2021-08-06 18:29:01 +02:00
|
|
|
cache = Cache.store
|
|
|
|
external_session_id = ussd_session.get('external_session_id')
|
|
|
|
existing_session_data = ussd_session.get('data')
|
|
|
|
if existing_session_data:
|
2021-11-29 16:04:50 +01:00
|
|
|
# replace session data entry
|
|
|
|
keys = data.keys()
|
|
|
|
for key in keys:
|
|
|
|
existing_session_data[key] = data[key]
|
|
|
|
data = existing_session_data
|
2021-08-06 18:29:01 +02:00
|
|
|
in_redis_ussd_session = cache.get(external_session_id)
|
|
|
|
in_redis_ussd_session = json.loads(in_redis_ussd_session)
|
|
|
|
create_or_update_session(
|
|
|
|
external_session_id=external_session_id,
|
|
|
|
msisdn=in_redis_ussd_session.get('msisdn'),
|
|
|
|
service_code=in_redis_ussd_session.get('service_code'),
|
|
|
|
user_input=in_redis_ussd_session.get('user_input'),
|
|
|
|
state=in_redis_ussd_session.get('state'),
|
|
|
|
session=session,
|
|
|
|
data=data
|
|
|
|
)
|
|
|
|
persist_ussd_session(external_session_id=external_session_id, queue=queue)
|