sarafu-vise/handlers/application/menuhandler.go

1484 lines
49 KiB
Go

package application
import (
"bytes"
"context"
"errors"
"fmt"
"path"
"strconv"
"strings"
"unicode"
"gopkg.in/leonelquinteros/gotext.v1"
"git.defalsify.org/vise.git/asm"
"git.defalsify.org/vise.git/cache"
"git.defalsify.org/vise.git/db"
"git.defalsify.org/vise.git/lang"
"git.defalsify.org/vise.git/logging"
"git.defalsify.org/vise.git/persist"
"git.defalsify.org/vise.git/resource"
"git.defalsify.org/vise.git/state"
commonlang "git.grassecon.net/grassrootseconomics/common/lang"
"git.grassecon.net/grassrootseconomics/common/person"
"git.grassecon.net/grassrootseconomics/common/pin"
"git.grassecon.net/grassrootseconomics/sarafu-api/remote"
"git.grassecon.net/grassrootseconomics/sarafu-vise/config"
"git.grassecon.net/grassrootseconomics/sarafu-vise/internal/sms"
"git.grassecon.net/grassrootseconomics/sarafu-vise/profile"
"git.grassecon.net/grassrootseconomics/sarafu-vise/store"
storedb "git.grassecon.net/grassrootseconomics/sarafu-vise/store/db"
)
var (
logg = logging.NewVanilla().WithDomain("ussdmenuhandler").WithContextKey("SessionId")
scriptDir = path.Join("services", "registration")
translationDir = path.Join(scriptDir, "locale")
)
// TODO: this is only in use in testing, should be moved to test domain and/or replaced by asm.FlagParser
// FlagManager handles centralized flag management
type FlagManager struct {
*asm.FlagParser
}
// NewFlagManager creates a new FlagManager instance
func NewFlagManager(csvPath string) (*FlagManager, error) {
parser := asm.NewFlagParser()
_, err := parser.Load(csvPath)
if err != nil {
return nil, fmt.Errorf("failed to load flag parser: %v", err)
}
return &FlagManager{
FlagParser: parser,
}, nil
}
func (fm *FlagManager) SetDebug() {
fm.FlagParser = fm.FlagParser.WithDebug()
}
// GetFlag retrieves a flag value by its label
func (fm *FlagManager) GetFlag(label string) (uint32, error) {
return fm.FlagParser.GetFlag(label)
}
type MenuHandlers struct {
pe *persist.Persister
st *state.State
ca cache.Memory
userdataStore store.DataStore
flagManager *FlagManager
accountService remote.AccountService
prefixDb storedb.PrefixDb
smsService sms.SmsService
logDb store.LogDb
profile *profile.Profile
ReplaceSeparatorFunc func(string) string
}
// NewHandlers creates a new instance of the Handlers struct with the provided dependencies.
func NewMenuHandlers(appFlags *FlagManager, userdataStore db.Db, logdb db.Db, accountService remote.AccountService, replaceSeparatorFunc func(string) string) (*MenuHandlers, error) {
if userdataStore == nil {
return nil, fmt.Errorf("cannot create handler with nil userdata store")
}
userDb := &store.UserDataStore{
Db: userdataStore,
}
smsservice := sms.SmsService{
Accountservice: accountService,
Userdatastore: *userDb,
}
logDb := store.LogDb{
Db: logdb,
}
// Instantiate the SubPrefixDb with "DATATYPE_USERDATA" prefix
prefix := storedb.ToBytes(db.DATATYPE_USERDATA)
prefixDb := storedb.NewSubPrefixDb(userdataStore, prefix)
h := &MenuHandlers{
userdataStore: userDb,
flagManager: appFlags,
accountService: accountService,
smsService: smsservice,
prefixDb: prefixDb,
logDb: logDb,
profile: &profile.Profile{Max: 6},
ReplaceSeparatorFunc: replaceSeparatorFunc,
}
return h, nil
}
// SetPersister sets persister instance to the handlers.
func (h *MenuHandlers) SetPersister(pe *persist.Persister) {
if h.pe != nil {
panic("persister already set")
}
h.pe = pe
}
// Init initializes the handler for a new session.
func (h *MenuHandlers) Init(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var r resource.Result
if h.pe == nil {
logg.WarnCtxf(ctx, "handler init called before it is ready or more than once", "state", h.st, "cache", h.ca)
return r, nil
}
defer func() {
h.Exit()
}()
h.st = h.pe.GetState()
h.ca = h.pe.GetMemory()
sessionId, ok := ctx.Value("SessionId").(string)
if ok {
ctx = context.WithValue(ctx, "SessionId", sessionId)
}
if h.st == nil || h.ca == nil {
logg.ErrorCtxf(ctx, "perister fail in handler", "state", h.st, "cache", h.ca)
return r, fmt.Errorf("cannot get state and memory for handler")
}
logg.DebugCtxf(ctx, "handler has been initialized", "state", h.st, "cache", h.ca)
return r, nil
}
func (h *MenuHandlers) Exit() {
h.pe = nil
}
// retrieves language codes from the context that can be used for handling translations.
func codeFromCtx(ctx context.Context) string {
var code string
if ctx.Value("Language") != nil {
lang := ctx.Value("Language").(lang.Language)
code = lang.Code
}
return code
}
// SetLanguage sets the language across the menu.
func (h *MenuHandlers) SetLanguage(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
symbol, _ := h.st.Where()
code := strings.Split(symbol, "_")[1]
// TODO: Use defaultlanguage from config
if !commonlang.IsValidISO639(code) {
//Fallback to english instead?
code = "eng"
}
err := h.persistLanguageCode(ctx, code)
if err != nil {
return res, err
}
res.Content = code
res.FlagSet = append(res.FlagSet, state.FLAG_LANG)
languageSetFlag, err := h.flagManager.GetFlag("flag_language_set")
if err != nil {
logg.ErrorCtxf(ctx, "Error setting the languageSetFlag", "error", err)
return res, err
}
res.FlagSet = append(res.FlagSet, languageSetFlag)
return res, nil
}
func (h *MenuHandlers) CheckAccountCreated(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
flag_language_set, _ := h.flagManager.GetFlag("flag_language_set")
flag_account_created, _ := h.flagManager.GetFlag("flag_account_created")
store := h.userdataStore
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
_, err := store.ReadEntry(ctx, sessionId, storedb.DATA_PUBLIC_KEY)
if err != nil {
if db.IsNotFound(err) {
// reset major flags
res.FlagReset = append(res.FlagReset, flag_language_set)
res.FlagReset = append(res.FlagReset, flag_account_created)
return res, nil
}
return res, nil
}
res.FlagSet = append(res.FlagSet, flag_account_created)
return res, nil
}
// CheckBlockedStatus:
// 1. Checks whether the DATA_SELF_PIN_RESET is 1 and sets the flag_account_pin_reset
// 2. resets the account blocked flag if the PIN attempts have been reset by an admin.
func (h *MenuHandlers) CheckBlockedStatus(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
store := h.userdataStore
flag_account_blocked, _ := h.flagManager.GetFlag("flag_account_blocked")
flag_account_pin_reset, _ := h.flagManager.GetFlag("flag_account_pin_reset")
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
res.FlagReset = append(res.FlagReset, flag_account_pin_reset)
selfPinReset, err := store.ReadEntry(ctx, sessionId, storedb.DATA_SELF_PIN_RESET)
if err == nil {
pinResetValue, _ := strconv.ParseUint(string(selfPinReset), 0, 64)
if pinResetValue == 1 {
res.FlagSet = append(res.FlagSet, flag_account_pin_reset)
}
}
currentWrongPinAttempts, err := store.ReadEntry(ctx, sessionId, storedb.DATA_INCORRECT_PIN_ATTEMPTS)
if err != nil {
if !db.IsNotFound(err) {
return res, nil
}
}
pinAttemptsValue, _ := strconv.ParseUint(string(currentWrongPinAttempts), 0, 64)
if pinAttemptsValue == 0 {
res.FlagReset = append(res.FlagReset, flag_account_blocked)
return res, nil
}
return res, nil
}
// ResetApiCallFailure resets the api call failure flag
func (h *MenuHandlers) ResetApiCallFailure(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error")
res.FlagReset = append(res.FlagReset, flag_api_error)
return res, nil
}
// ResetUnregisteredNumber clears the unregistered number flag in the system,
// indicating that a number's registration status should no longer be marked as unregistered.
func (h *MenuHandlers) ResetUnregisteredNumber(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
flag_unregistered_number, _ := h.flagManager.GetFlag("flag_unregistered_number")
res.FlagReset = append(res.FlagReset, flag_unregistered_number)
return res, nil
}
// SaveFirstname updates the first name in the gdbm with the provided input.
func (h *MenuHandlers) SaveFirstname(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
var err error
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
firstName := string(input)
store := h.userdataStore
logdb := h.logDb
flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update")
flag_firstname_set, _ := h.flagManager.GetFlag("flag_firstname_set")
allowUpdate := h.st.MatchFlag(flag_allow_update, true)
firstNameSet := h.st.MatchFlag(flag_firstname_set, true)
if allowUpdate {
temporaryFirstName, _ := store.ReadEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE)
if len(temporaryFirstName) == 0 {
logg.ErrorCtxf(ctx, "temporaryFirstName is empty", "key", storedb.DATA_TEMPORARY_VALUE)
return res, fmt.Errorf("Data error encountered")
}
err = store.WriteEntry(ctx, sessionId, storedb.DATA_FIRST_NAME, []byte(temporaryFirstName))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write firstName entry with", "key", storedb.DATA_FIRST_NAME, "value", temporaryFirstName, "error", err)
return res, err
}
res.FlagSet = append(res.FlagSet, flag_firstname_set)
err = logdb.WriteLogEntry(ctx, sessionId, storedb.DATA_FIRST_NAME, []byte(temporaryFirstName))
if err != nil {
logg.DebugCtxf(ctx, "Failed to write firtname db log entry", "key", storedb.DATA_FIRST_NAME, "value", temporaryFirstName)
}
} else {
if firstNameSet {
err = store.WriteEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE, []byte(firstName))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write temporaryFirstName entry with", "key", storedb.DATA_TEMPORARY_VALUE, "value", firstName, "error", err)
return res, err
}
} else {
h.profile.InsertOrShift(0, firstName)
}
}
return res, nil
}
// SaveFamilyname updates the family name in the gdbm with the provided input.
func (h *MenuHandlers) SaveFamilyname(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
var err error
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
store := h.userdataStore
logdb := h.logDb
familyName := string(input)
flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update")
flag_familyname_set, _ := h.flagManager.GetFlag("flag_familyname_set")
allowUpdate := h.st.MatchFlag(flag_allow_update, true)
familyNameSet := h.st.MatchFlag(flag_familyname_set, true)
if allowUpdate {
temporaryFamilyName, _ := store.ReadEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE)
if len(temporaryFamilyName) == 0 {
logg.ErrorCtxf(ctx, "temporaryFamilyName is empty", "key", storedb.DATA_TEMPORARY_VALUE)
return res, fmt.Errorf("Data error encountered")
}
err = store.WriteEntry(ctx, sessionId, storedb.DATA_FAMILY_NAME, []byte(temporaryFamilyName))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write familyName entry with", "key", storedb.DATA_FAMILY_NAME, "value", temporaryFamilyName, "error", err)
return res, err
}
res.FlagSet = append(res.FlagSet, flag_familyname_set)
err = logdb.WriteLogEntry(ctx, sessionId, storedb.DATA_FAMILY_NAME, []byte(temporaryFamilyName))
if err != nil {
logg.DebugCtxf(ctx, "Failed to write firtname db log entry", "key", storedb.DATA_FAMILY_NAME, "value", temporaryFamilyName)
}
} else {
if familyNameSet {
err = store.WriteEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE, []byte(familyName))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write temporaryFamilyName entry with", "key", storedb.DATA_TEMPORARY_VALUE, "value", familyName, "error", err)
return res, err
}
} else {
h.profile.InsertOrShift(1, familyName)
}
}
return res, nil
}
// VerifyYob verifies the length of the given input.
func (h *MenuHandlers) VerifyYob(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
var err error
flag_incorrect_date_format, _ := h.flagManager.GetFlag("flag_incorrect_date_format")
date := string(input)
_, err = strconv.Atoi(date)
if err != nil {
// If conversion fails, input is not numeric
res.FlagSet = append(res.FlagSet, flag_incorrect_date_format)
return res, nil
}
if person.IsValidYOb(date) {
res.FlagReset = append(res.FlagReset, flag_incorrect_date_format)
} else {
res.FlagSet = append(res.FlagSet, flag_incorrect_date_format)
}
return res, nil
}
// ResetIncorrectYob resets the incorrect date format flag after a new attempt.
func (h *MenuHandlers) ResetIncorrectYob(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
flag_incorrect_date_format, _ := h.flagManager.GetFlag("flag_incorrect_date_format")
res.FlagReset = append(res.FlagReset, flag_incorrect_date_format)
return res, nil
}
// SaveYOB updates the Year of Birth(YOB) in the gdbm with the provided input.
func (h *MenuHandlers) SaveYob(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
var err error
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
yob := string(input)
store := h.userdataStore
logdb := h.logDb
flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update")
flag_yob_set, _ := h.flagManager.GetFlag("flag_yob_set")
allowUpdate := h.st.MatchFlag(flag_allow_update, true)
yobSet := h.st.MatchFlag(flag_yob_set, true)
if allowUpdate {
temporaryYob, _ := store.ReadEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE)
if len(temporaryYob) == 0 {
logg.ErrorCtxf(ctx, "temporaryYob is empty", "key", storedb.DATA_TEMPORARY_VALUE)
return res, fmt.Errorf("Data error encountered")
}
err = store.WriteEntry(ctx, sessionId, storedb.DATA_YOB, []byte(temporaryYob))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write yob entry with", "key", storedb.DATA_TEMPORARY_VALUE, "value", temporaryYob, "error", err)
return res, err
}
res.FlagSet = append(res.FlagSet, flag_yob_set)
err = logdb.WriteLogEntry(ctx, sessionId, storedb.DATA_YOB, []byte(temporaryYob))
if err != nil {
logg.DebugCtxf(ctx, "Failed to write yob db log entry", "key", storedb.DATA_YOB, "value", temporaryYob)
}
} else {
if yobSet {
err = store.WriteEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE, []byte(yob))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write temporaryYob entry with", "key", storedb.DATA_TEMPORARY_VALUE, "value", yob, "error", err)
return res, err
}
} else {
h.profile.InsertOrShift(3, yob)
}
}
return res, nil
}
// SaveLocation updates the location in the gdbm with the provided input.
func (h *MenuHandlers) SaveLocation(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
var err error
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
location := string(input)
store := h.userdataStore
logdb := h.logDb
flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update")
flag_location_set, _ := h.flagManager.GetFlag("flag_location_set")
allowUpdate := h.st.MatchFlag(flag_allow_update, true)
locationSet := h.st.MatchFlag(flag_location_set, true)
if allowUpdate {
temporaryLocation, _ := store.ReadEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE)
if len(temporaryLocation) == 0 {
logg.ErrorCtxf(ctx, "temporaryLocation is empty", "key", storedb.DATA_TEMPORARY_VALUE)
return res, fmt.Errorf("Data error encountered")
}
err = store.WriteEntry(ctx, sessionId, storedb.DATA_LOCATION, []byte(temporaryLocation))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write location entry with", "key", storedb.DATA_LOCATION, "value", temporaryLocation, "error", err)
return res, err
}
res.FlagSet = append(res.FlagSet, flag_location_set)
err = logdb.WriteLogEntry(ctx, sessionId, storedb.DATA_LOCATION, []byte(temporaryLocation))
if err != nil {
logg.DebugCtxf(ctx, "Failed to write location db log entry", "key", storedb.DATA_LOCATION, "value", temporaryLocation)
}
} else {
if locationSet {
err = store.WriteEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE, []byte(location))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write temporaryLocation entry with", "key", storedb.DATA_TEMPORARY_VALUE, "value", location, "error", err)
return res, err
}
res.FlagSet = append(res.FlagSet, flag_location_set)
} else {
h.profile.InsertOrShift(4, location)
}
}
return res, nil
}
// SaveGender updates the gender in the gdbm with the provided input.
func (h *MenuHandlers) SaveGender(ctx context.Context, sym string, input []byte) (resource.Result, error) {
symbol, _ := h.st.Where()
var res resource.Result
var err error
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
gender := strings.Split(symbol, "_")[1]
store := h.userdataStore
logdb := h.logDb
flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update")
flag_gender_set, _ := h.flagManager.GetFlag("flag_gender_set")
allowUpdate := h.st.MatchFlag(flag_allow_update, true)
genderSet := h.st.MatchFlag(flag_gender_set, true)
if allowUpdate {
temporaryGender, _ := store.ReadEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE)
if len(temporaryGender) == 0 {
logg.ErrorCtxf(ctx, "temporaryGender is empty", "key", storedb.DATA_TEMPORARY_VALUE)
return res, fmt.Errorf("Data error encountered")
}
err = store.WriteEntry(ctx, sessionId, storedb.DATA_GENDER, []byte(temporaryGender))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write gender entry with", "key", storedb.DATA_GENDER, "value", gender, "error", err)
return res, err
}
res.FlagSet = append(res.FlagSet, flag_gender_set)
err = logdb.WriteLogEntry(ctx, sessionId, storedb.DATA_GENDER, []byte(temporaryGender))
if err != nil {
logg.DebugCtxf(ctx, "Failed to write gender db log entry", "key", storedb.DATA_TEMPORARY_VALUE, "value", temporaryGender)
}
} else {
if genderSet {
err = store.WriteEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE, []byte(gender))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write temporaryGender entry with", "key", storedb.DATA_TEMPORARY_VALUE, "value", gender, "error", err)
return res, err
}
} else {
h.profile.InsertOrShift(2, gender)
}
}
return res, nil
}
// SaveOfferings updates the offerings(goods and services provided by the user) in the gdbm with the provided input.
func (h *MenuHandlers) SaveOfferings(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
var err error
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
offerings := string(input)
store := h.userdataStore
logdb := h.logDb
flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update")
flag_offerings_set, _ := h.flagManager.GetFlag("flag_offerings_set")
allowUpdate := h.st.MatchFlag(flag_allow_update, true)
offeringsSet := h.st.MatchFlag(flag_offerings_set, true)
if allowUpdate {
temporaryOfferings, _ := store.ReadEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE)
if len(temporaryOfferings) == 0 {
logg.ErrorCtxf(ctx, "temporaryOfferings is empty", "key", storedb.DATA_TEMPORARY_VALUE)
return res, fmt.Errorf("Data error encountered")
}
err = store.WriteEntry(ctx, sessionId, storedb.DATA_OFFERINGS, []byte(temporaryOfferings))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write offerings entry with", "key", storedb.DATA_TEMPORARY_VALUE, "value", offerings, "error", err)
return res, err
}
res.FlagSet = append(res.FlagSet, flag_offerings_set)
err = logdb.WriteLogEntry(ctx, sessionId, storedb.DATA_FIRST_NAME, []byte(temporaryOfferings))
if err != nil {
logg.DebugCtxf(ctx, "Failed to write offerings db log entry", "key", storedb.DATA_OFFERINGS, "value", offerings)
}
} else {
if offeringsSet {
err = store.WriteEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE, []byte(offerings))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write temporaryOfferings entry with", "key", storedb.DATA_TEMPORARY_VALUE, "value", offerings, "error", err)
return res, err
}
} else {
h.profile.InsertOrShift(5, offerings)
}
}
return res, nil
}
// ResetAllowUpdate resets the allowupdate flag that allows a user to update profile data.
func (h *MenuHandlers) ResetAllowUpdate(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update")
res.FlagReset = append(res.FlagReset, flag_allow_update)
return res, nil
}
// GetCurrentProfileInfo retrieves specific profile fields based on the current state of the USSD session.
// Uses flag management system to track profile field status and handle menu navigation.
func (h *MenuHandlers) GetCurrentProfileInfo(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
var profileInfo []byte
var defaultValue string
var err error
flag_firstname_set, _ := h.flagManager.GetFlag("flag_firstname_set")
flag_familyname_set, _ := h.flagManager.GetFlag("flag_familyname_set")
flag_yob_set, _ := h.flagManager.GetFlag("flag_yob_set")
flag_gender_set, _ := h.flagManager.GetFlag("flag_gender_set")
flag_location_set, _ := h.flagManager.GetFlag("flag_location_set")
flag_offerings_set, _ := h.flagManager.GetFlag("flag_offerings_set")
flag_back_set, _ := h.flagManager.GetFlag("flag_back_set")
res.FlagReset = append(res.FlagReset, flag_back_set)
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
language, ok := ctx.Value("Language").(lang.Language)
if !ok {
return res, fmt.Errorf("value for 'Language' is not of type lang.Language")
}
code := language.Code
if code == "swa" {
defaultValue = "Haipo"
} else {
defaultValue = "Not Provided"
}
sm, _ := h.st.Where()
parts := strings.SplitN(sm, "_", 2)
filename := parts[1]
dbKeyStr := "DATA_" + strings.ToUpper(filename)
logg.InfoCtxf(ctx, "GetCurrentProfileInfo", "filename", filename, "dbKeyStr:", dbKeyStr)
dbKey, err := storedb.StringToDataTyp(dbKeyStr)
if err != nil {
return res, err
}
store := h.userdataStore
switch dbKey {
case storedb.DATA_FIRST_NAME:
profileInfo, err = store.ReadEntry(ctx, sessionId, storedb.DATA_FIRST_NAME)
if err != nil {
if db.IsNotFound(err) {
res.Content = defaultValue
break
}
logg.ErrorCtxf(ctx, "Failed to read first name entry with", "key", "error", storedb.DATA_FIRST_NAME, err)
return res, err
}
res.FlagSet = append(res.FlagSet, flag_firstname_set)
res.Content = string(profileInfo)
case storedb.DATA_FAMILY_NAME:
profileInfo, err = store.ReadEntry(ctx, sessionId, storedb.DATA_FAMILY_NAME)
if err != nil {
if db.IsNotFound(err) {
res.Content = defaultValue
break
}
logg.ErrorCtxf(ctx, "Failed to read family name entry with", "key", "error", storedb.DATA_FAMILY_NAME, err)
return res, err
}
res.FlagSet = append(res.FlagSet, flag_familyname_set)
res.Content = string(profileInfo)
case storedb.DATA_GENDER:
profileInfo, err = store.ReadEntry(ctx, sessionId, storedb.DATA_GENDER)
if err != nil {
if db.IsNotFound(err) {
res.Content = defaultValue
break
}
logg.ErrorCtxf(ctx, "Failed to read gender entry with", "key", "error", storedb.DATA_GENDER, err)
return res, err
}
res.FlagSet = append(res.FlagSet, flag_gender_set)
res.Content = string(profileInfo)
case storedb.DATA_YOB:
profileInfo, err = store.ReadEntry(ctx, sessionId, storedb.DATA_YOB)
if err != nil {
if db.IsNotFound(err) {
res.Content = defaultValue
break
}
logg.ErrorCtxf(ctx, "Failed to read year of birth(yob) entry with", "key", "error", storedb.DATA_YOB, err)
return res, err
}
res.FlagSet = append(res.FlagSet, flag_yob_set)
res.Content = string(profileInfo)
case storedb.DATA_LOCATION:
profileInfo, err = store.ReadEntry(ctx, sessionId, storedb.DATA_LOCATION)
if err != nil {
if db.IsNotFound(err) {
res.Content = defaultValue
break
}
logg.ErrorCtxf(ctx, "Failed to read location entry with", "key", "error", storedb.DATA_LOCATION, err)
return res, err
}
res.FlagSet = append(res.FlagSet, flag_location_set)
res.Content = string(profileInfo)
case storedb.DATA_OFFERINGS:
profileInfo, err = store.ReadEntry(ctx, sessionId, storedb.DATA_OFFERINGS)
if err != nil {
if db.IsNotFound(err) {
res.Content = defaultValue
break
}
logg.ErrorCtxf(ctx, "Failed to read offerings entry with", "key", "error", storedb.DATA_OFFERINGS, err)
return res, err
}
res.FlagSet = append(res.FlagSet, flag_offerings_set)
res.Content = string(profileInfo)
case storedb.DATA_ACCOUNT_ALIAS:
profileInfo, err = store.ReadEntry(ctx, sessionId, storedb.DATA_ACCOUNT_ALIAS)
if err != nil {
if db.IsNotFound(err) {
res.Content = defaultValue
break
}
logg.ErrorCtxf(ctx, "Failed to read account alias entry with", "key", "error", storedb.DATA_ACCOUNT_ALIAS, err)
return res, err
}
alias := string(profileInfo)
if alias == "" {
res.Content = defaultValue
} else {
res.Content = alias
}
default:
break
}
return res, nil
}
// GetProfileInfo provides a comprehensive view of a user's profile.
func (h *MenuHandlers) GetProfileInfo(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
var defaultValue string
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
language, ok := ctx.Value("Language").(lang.Language)
if !ok {
return res, fmt.Errorf("value for 'Language' is not of type lang.Language")
}
code := language.Code
if code == "swa" {
defaultValue = "Haipo"
} else {
defaultValue = "Not Provided"
}
// Helper function to handle nil byte slices and convert them to string
getEntryOrDefault := func(entry []byte, err error) string {
if err != nil || entry == nil {
return defaultValue
}
return string(entry)
}
store := h.userdataStore
// Retrieve user data as strings with fallback to defaultValue
firstName := getEntryOrDefault(store.ReadEntry(ctx, sessionId, storedb.DATA_FIRST_NAME))
familyName := getEntryOrDefault(store.ReadEntry(ctx, sessionId, storedb.DATA_FAMILY_NAME))
yob := getEntryOrDefault(store.ReadEntry(ctx, sessionId, storedb.DATA_YOB))
gender := getEntryOrDefault(store.ReadEntry(ctx, sessionId, storedb.DATA_GENDER))
location := getEntryOrDefault(store.ReadEntry(ctx, sessionId, storedb.DATA_LOCATION))
offerings := getEntryOrDefault(store.ReadEntry(ctx, sessionId, storedb.DATA_OFFERINGS))
alias := getEntryOrDefault(store.ReadEntry(ctx, sessionId, storedb.DATA_ACCOUNT_ALIAS))
if alias != defaultValue && alias != "" {
alias = strings.Split(alias, ".")[0]
} else {
alias = defaultValue
}
// Construct the full name
name := person.ConstructName(firstName, familyName, defaultValue)
// Calculate age from year of birth
age := defaultValue
if yob != defaultValue {
if yobInt, err := strconv.Atoi(yob); err == nil {
age = strconv.Itoa(person.CalculateAgeWithYOB(yobInt))
} else {
return res, fmt.Errorf("invalid year of birth: %v", err)
}
}
switch language.Code {
case "eng":
res.Content = fmt.Sprintf(
"Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\nYour alias: %s\n",
name, gender, age, location, offerings, alias,
)
case "swa":
res.Content = fmt.Sprintf(
"Jina: %s\nJinsia: %s\nUmri: %s\nEneo: %s\nUnauza: %s\nLakabu yako: %s\n",
name, gender, age, location, offerings, alias,
)
default:
res.Content = fmt.Sprintf(
"Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\nYour alias: %s\n",
name, gender, age, location, offerings, alias,
)
}
return res, nil
}
// handles bulk updates of profile information.
func (h *MenuHandlers) insertProfileItems(ctx context.Context, sessionId string, res *resource.Result) error {
var err error
userStore := h.userdataStore
profileFlagNames := []string{
"flag_firstname_set",
"flag_familyname_set",
"flag_yob_set",
"flag_gender_set",
"flag_location_set",
"flag_offerings_set",
}
profileDataKeys := []storedb.DataTyp{
storedb.DATA_FIRST_NAME,
storedb.DATA_FAMILY_NAME,
storedb.DATA_GENDER,
storedb.DATA_YOB,
storedb.DATA_LOCATION,
storedb.DATA_OFFERINGS,
}
for index, profileItem := range h.profile.ProfileItems {
// Ensure the profileItem is not "0"(is set)
if profileItem != "0" {
flag, _ := h.flagManager.GetFlag(profileFlagNames[index])
isProfileItemSet := h.st.MatchFlag(flag, true)
if !isProfileItemSet {
err = userStore.WriteEntry(ctx, sessionId, profileDataKeys[index], []byte(profileItem))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write profile entry with", "key", profileDataKeys[index], "value", profileItem, "error", err)
return err
}
res.FlagSet = append(res.FlagSet, flag)
}
}
}
return nil
}
// UpdateAllProfileItems is used to persist all the new profile information and setup the required profile flags.
func (h *MenuHandlers) UpdateAllProfileItems(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
err := h.insertProfileItems(ctx, sessionId, &res)
if err != nil {
return res, err
}
return res, nil
}
// ResetAccountAuthorized resets the account authorization flag after a successful PIN entry.
func (h *MenuHandlers) ResetAccountAuthorized(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
flag_account_authorized, _ := h.flagManager.GetFlag("flag_account_authorized")
res.FlagReset = append(res.FlagReset, flag_account_authorized)
return res, nil
}
// CheckIdentifier retrieves the Public key from the userdatastore under the key: DATA_PUBLIC_KEY and triggers an sms that
// will be sent to the associated session id
func (h *MenuHandlers) CheckIdentifier(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
smsservice := h.smsService
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
store := h.userdataStore
publicKey, err := store.ReadEntry(ctx, sessionId, storedb.DATA_PUBLIC_KEY)
if err != nil {
logg.ErrorCtxf(ctx, "failed to read publicKey entry with", "key", storedb.DATA_PUBLIC_KEY, "error", err)
return res, err
}
res.Content = string(publicKey)
//trigger an address sms to be delivered to the associated session id
err = smsservice.SendAddressSMS(ctx)
if err != nil {
logg.DebugCtxf(ctx, "Failed to trigger an address sms", "error", err)
return res, nil
}
return res, nil
}
// Authorize attempts to unlock the next sequential nodes by verifying the provided PIN against the already set PIN.
// It sets the required flags that control the flow.
func (h *MenuHandlers) Authorize(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
var err error
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin")
flag_account_authorized, _ := h.flagManager.GetFlag("flag_account_authorized")
flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update")
pinInput := string(input)
if !pin.IsValidPIN(pinInput) {
res.FlagReset = append(res.FlagReset, flag_account_authorized, flag_allow_update)
return res, nil
}
store := h.userdataStore
AccountPin, err := store.ReadEntry(ctx, sessionId, storedb.DATA_ACCOUNT_PIN)
if err != nil {
logg.ErrorCtxf(ctx, "failed to read AccountPin entry with", "key", storedb.DATA_ACCOUNT_PIN, "error", err)
return res, err
}
// verify that the user provided the correct PIN
if pin.VerifyPIN(string(AccountPin), pinInput) {
// set the required flags for a valid PIN
res.FlagSet = append(res.FlagSet, flag_allow_update, flag_account_authorized)
res.FlagReset = append(res.FlagReset, flag_incorrect_pin)
err := h.resetIncorrectPINAttempts(ctx, sessionId)
if err != nil {
return res, err
}
} else {
// set the required flags for an incorrect PIN
res.FlagSet = append(res.FlagSet, flag_incorrect_pin)
res.FlagReset = append(res.FlagReset, flag_account_authorized, flag_allow_update)
err = h.incrementIncorrectPINAttempts(ctx, sessionId)
if err != nil {
return res, err
}
}
return res, nil
}
// Setback sets the flag_back_set flag when the navigation is back.
func (h *MenuHandlers) SetBack(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
flag_back_set, _ := h.flagManager.GetFlag("flag_back_set")
//TODO:
//Add check if the navigation is lateral nav instead of checking the input.
if string(input) == "0" {
res.FlagSet = append(res.FlagSet, flag_back_set)
} else {
res.FlagReset = append(res.FlagReset, flag_back_set)
}
return res, nil
}
// CheckAccountStatus queries the API using the TrackingId and sets flags
// based on the account status.
func (h *MenuHandlers) CheckAccountStatus(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
flag_account_success, _ := h.flagManager.GetFlag("flag_account_success")
flag_account_pending, _ := h.flagManager.GetFlag("flag_account_pending")
flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error")
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
store := h.userdataStore
publicKey, err := store.ReadEntry(ctx, sessionId, storedb.DATA_PUBLIC_KEY)
if err != nil {
logg.ErrorCtxf(ctx, "failed to read publicKey entry with", "key", storedb.DATA_PUBLIC_KEY, "error", err)
return res, err
}
r, err := h.accountService.TrackAccountStatus(ctx, string(publicKey))
if err != nil {
res.FlagSet = append(res.FlagSet, flag_api_error)
logg.ErrorCtxf(ctx, "failed on TrackAccountStatus", "error", err)
return res, nil
}
res.FlagReset = append(res.FlagReset, flag_api_error)
if r.Active {
res.FlagSet = append(res.FlagSet, flag_account_success)
res.FlagReset = append(res.FlagReset, flag_account_pending)
} else {
res.FlagReset = append(res.FlagReset, flag_account_success)
res.FlagSet = append(res.FlagSet, flag_account_pending)
}
return res, nil
}
// Quit displays the Thank you message and exits the menu.
func (h *MenuHandlers) Quit(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
flag_account_authorized, _ := h.flagManager.GetFlag("flag_account_authorized")
code := codeFromCtx(ctx)
l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
res.Content = l.Get("Thank you for using Sarafu. Goodbye!")
res.FlagReset = append(res.FlagReset, flag_account_authorized)
return res, nil
}
// QuitWithHelp displays helpline information then exits the menu.
func (h *MenuHandlers) QuitWithHelp(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
flag_account_authorized, _ := h.flagManager.GetFlag("flag_account_authorized")
code := codeFromCtx(ctx)
l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
res.Content = l.Get("For more help, please call: 0757628885")
res.FlagReset = append(res.FlagReset, flag_account_authorized)
return res, nil
}
// ShowBlockedAccount displays a message after an account has been blocked and how to reach support.
func (h *MenuHandlers) ShowBlockedAccount(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
code := codeFromCtx(ctx)
l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
res.Content = l.Get("Your account has been locked. For help on how to unblock your account, contact support at: 0757628885")
return res, nil
}
// loadUserContent loads the main user content in the main menu: the alias,balance associated with active voucher
func loadUserContent(ctx context.Context, activeSym string, balance string, alias string) (string, error) {
var content string
code := codeFromCtx(ctx)
l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
balFloat, err := strconv.ParseFloat(balance, 64)
if err != nil {
//Only exclude ErrSyntax error to avoid returning an error if the active bal is not available yet
if !errors.Is(err, strconv.ErrSyntax) {
logg.ErrorCtxf(ctx, "failed to parse activeBal as float", "value", balance, "error", err)
return "", err
}
balFloat = 0.00
}
// Format to 2 decimal places
balStr := fmt.Sprintf("%.2f %s", balFloat, activeSym)
if alias != "" {
content = l.Get("%s balance: %s\n", alias, balStr)
} else {
content = l.Get("Balance: %s\n", balStr)
}
return content, nil
}
// CheckBalance retrieves the balance of the active voucher and sets
// the balance as the result content.
func (h *MenuHandlers) CheckBalance(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var (
res resource.Result
err error
alias string
content string
)
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
store := h.userdataStore
// get the active sym and active balance
activeSym, err := store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_SYM)
if err != nil {
logg.InfoCtxf(ctx, "could not find the activeSym in checkBalance:", "err", err)
if !db.IsNotFound(err) {
logg.ErrorCtxf(ctx, "failed to read activeSym entry with", "key", storedb.DATA_ACTIVE_SYM, "error", err)
return res, err
}
}
activeBal, err := store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_BAL)
if err != nil {
if !db.IsNotFound(err) {
logg.ErrorCtxf(ctx, "failed to read activeBal entry with", "key", storedb.DATA_ACTIVE_BAL, "error", err)
return res, err
}
}
accAlias, err := store.ReadEntry(ctx, sessionId, storedb.DATA_ACCOUNT_ALIAS)
if err != nil {
if !db.IsNotFound(err) {
logg.ErrorCtxf(ctx, "failed to read account alias entry with", "key", storedb.DATA_ACCOUNT_ALIAS, "error", err)
return res, err
}
} else {
alias = strings.Split(string(accAlias), ".")[0]
}
content, err = loadUserContent(ctx, string(activeSym), string(activeBal), alias)
if err != nil {
return res, err
}
res.Content = content
return res, nil
}
// FetchCommunityBalance retrieves and displays the balance for community accounts in user's preferred language.
func (h *MenuHandlers) FetchCommunityBalance(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
// retrieve the language code from the context
code := codeFromCtx(ctx)
// Initialize the localization system with the appropriate translation directory
l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
//TODO:
//Check if the address is a community account,if then,get the actual balance
res.Content = l.Get("Community Balance: 0.00")
return res, nil
}
// GetDefaultPool returns the current user's Pool. If none is set, it returns the default config pool.
func (h *MenuHandlers) GetDefaultPool(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
userStore := h.userdataStore
activePoolSym, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_POOL_SYM)
if err != nil {
if db.IsNotFound(err) {
// set the default as the response
res.Content = config.DefaultPoolSymbol()
return res, nil
}
logg.ErrorCtxf(ctx, "failed to read the activePoolSym entry with", "key", storedb.DATA_ACTIVE_POOL_SYM, "error", err)
return res, err
}
res.Content = string(activePoolSym)
return res, nil
}
// ViewPool retrieves the pool details from the user store
// and displays it to the user for them to select it.
func (h *MenuHandlers) ViewPool(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
code := codeFromCtx(ctx)
l := gotext.NewLocale(translationDir, code)
l.AddDomain("default")
flag_incorrect_pool, _ := h.flagManager.GetFlag("flag_incorrect_pool")
inputStr := string(input)
poolData, err := store.GetPoolData(ctx, h.userdataStore, sessionId, inputStr)
if err != nil {
return res, fmt.Errorf("failed to retrieve pool data: %v", err)
}
if poolData == nil {
flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error")
// no match found. Call the API using the inputStr as the symbol
poolResp, err := h.accountService.RetrievePoolDetails(ctx, inputStr)
if err != nil {
res.FlagSet = append(res.FlagSet, flag_api_error)
return res, nil
}
if len(poolResp.PoolSymbol) == 0 {
// If the API does not return the data, set the flag
res.FlagSet = append(res.FlagSet, flag_incorrect_pool)
return res, nil
}
poolData = poolResp
}
if err := store.StoreTemporaryPool(ctx, h.userdataStore, sessionId, poolData); err != nil {
logg.ErrorCtxf(ctx, "failed on StoreTemporaryPool", "error", err)
return res, err
}
res.FlagReset = append(res.FlagReset, flag_incorrect_pool)
res.Content = l.Get("Name: %s\nSymbol: %s", poolData.PoolName, poolData.PoolSymbol)
return res, nil
}
// SetPool retrieves the temp pool data and sets it as the active data.
func (h *MenuHandlers) SetPool(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
// Get temporary data
tempData, err := store.GetTemporaryPoolData(ctx, h.userdataStore, sessionId)
if err != nil {
logg.ErrorCtxf(ctx, "failed on GetTemporaryPoolData", "error", err)
return res, err
}
// Set as active and clear temporary data
if err := store.UpdatePoolData(ctx, h.userdataStore, sessionId, tempData); err != nil {
logg.ErrorCtxf(ctx, "failed on UpdatePoolData", "error", err)
return res, err
}
res.Content = tempData.PoolSymbol
return res, nil
}
// persistInitialLanguageCode receives an initial language code and persists it to the store
func (h *MenuHandlers) persistInitialLanguageCode(ctx context.Context, sessionId string, code string) error {
store := h.userdataStore
_, err := store.ReadEntry(ctx, sessionId, storedb.DATA_INITIAL_LANGUAGE_CODE)
if err == nil {
return nil
}
if !db.IsNotFound(err) {
return err
}
err = store.WriteEntry(ctx, sessionId, storedb.DATA_INITIAL_LANGUAGE_CODE, []byte(code))
if err != nil {
logg.ErrorCtxf(ctx, "failed to persist initial language code", "key", storedb.DATA_INITIAL_LANGUAGE_CODE, "value", code, "error", err)
return err
}
return nil
}
// persistLanguageCode persists the selected ISO 639 language code
func (h *MenuHandlers) persistLanguageCode(ctx context.Context, code string) error {
store := h.userdataStore
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return fmt.Errorf("missing session")
}
err := store.WriteEntry(ctx, sessionId, storedb.DATA_SELECTED_LANGUAGE_CODE, []byte(code))
if err != nil {
logg.ErrorCtxf(ctx, "failed to persist language code", "key", storedb.DATA_SELECTED_LANGUAGE_CODE, "value", code, "error", err)
return err
}
return h.persistInitialLanguageCode(ctx, sessionId, code)
}
// constructAccountAlias retrieves and alias based on the first and family name
// and writes the result in DATA_ACCOUNT_ALIAS
func (h *MenuHandlers) constructAccountAlias(ctx context.Context) error {
var alias string
store := h.userdataStore
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return fmt.Errorf("missing session")
}
firstName, err := store.ReadEntry(ctx, sessionId, storedb.DATA_FIRST_NAME)
if err != nil {
if db.IsNotFound(err) {
return nil
}
return err
}
familyName, err := store.ReadEntry(ctx, sessionId, storedb.DATA_FAMILY_NAME)
if err != nil {
if db.IsNotFound(err) {
return nil
}
return err
}
pubKey, err := store.ReadEntry(ctx, sessionId, storedb.DATA_PUBLIC_KEY)
if err != nil {
if db.IsNotFound(err) {
return nil
}
return err
}
aliasInput := fmt.Sprintf("%s%s", firstName, familyName)
aliasResult, err := h.accountService.RequestAlias(ctx, string(pubKey), aliasInput)
if err != nil {
logg.ErrorCtxf(ctx, "failed to retrieve alias", "alias", aliasInput, "error_alias_request", err)
return fmt.Errorf("Failed to retrieve alias: %s", err.Error())
}
alias = aliasResult.Alias
//Store the alias
err = store.WriteEntry(ctx, sessionId, storedb.DATA_ACCOUNT_ALIAS, []byte(alias))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write account alias", "key", storedb.DATA_ACCOUNT_ALIAS, "value", alias, "error", err)
return err
}
return nil
}
// RequestCustomAlias requests an ENS based alias name based on a user's input,then saves it as temporary value
func (h *MenuHandlers) RequestCustomAlias(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
if string(input) == "0" {
return res, nil
}
flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error")
store := h.userdataStore
aliasHint, err := store.ReadEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE)
if err != nil {
if db.IsNotFound(err) {
return res, nil
}
return res, err
}
//Ensures that the call doesn't happen twice for the same alias hint
if !bytes.Equal(aliasHint, input) {
err = store.WriteEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE, []byte(string(input)))
if err != nil {
return res, err
}
pubKey, err := store.ReadEntry(ctx, sessionId, storedb.DATA_PUBLIC_KEY)
if err != nil {
if db.IsNotFound(err) {
return res, nil
}
}
sanitizedInput := sanitizeAliasHint(string(input))
aliasResult, err := h.accountService.RequestAlias(ctx, string(pubKey), sanitizedInput)
if err != nil {
res.FlagSet = append(res.FlagSet, flag_api_error)
logg.ErrorCtxf(ctx, "failed to retrieve alias", "alias", string(aliasHint), "error_alias_request", err)
return res, nil
}
res.FlagReset = append(res.FlagReset, flag_api_error)
alias := aliasResult.Alias
logg.InfoCtxf(ctx, "Suggested alias ", "alias", alias)
//Store the returned alias,wait for user to confirm it as new account alias
err = store.WriteEntry(ctx, sessionId, storedb.DATA_SUGGESTED_ALIAS, []byte(alias))
if err != nil {
logg.ErrorCtxf(ctx, "failed to write account alias", "key", storedb.DATA_TEMPORARY_VALUE, "value", alias, "error", err)
return res, err
}
}
return res, nil
}
func sanitizeAliasHint(input string) string {
for i, r := range input {
// Check if the character is a special character (non-alphanumeric)
if !unicode.IsLetter(r) && !unicode.IsNumber(r) {
return input[:i]
}
}
// If no special character is found, return the whole input
return input
}
// GetSuggestedAlias loads and displays the suggested alias name from the temporary value
func (h *MenuHandlers) GetSuggestedAlias(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
store := h.userdataStore
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
suggestedAlias, err := store.ReadEntry(ctx, sessionId, storedb.DATA_SUGGESTED_ALIAS)
if err != nil {
return res, nil
}
res.Content = string(suggestedAlias)
return res, nil
}
// ConfirmNewAlias reads the suggested alias from the [DATA_SUGGECTED_ALIAS] key and confirms it as the new account alias.
func (h *MenuHandlers) ConfirmNewAlias(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
store := h.userdataStore
logdb := h.logDb
flag_alias_set, _ := h.flagManager.GetFlag("flag_alias_set")
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
newAlias, err := store.ReadEntry(ctx, sessionId, storedb.DATA_SUGGESTED_ALIAS)
if err != nil {
return res, nil
}
logg.InfoCtxf(ctx, "Confirming new alias", "alias", string(newAlias))
err = store.WriteEntry(ctx, sessionId, storedb.DATA_ACCOUNT_ALIAS, []byte(string(newAlias)))
if err != nil {
logg.ErrorCtxf(ctx, "failed to clear DATA_ACCOUNT_ALIAS_VALUE entry with", "key", storedb.DATA_ACCOUNT_ALIAS, "value", "empty", "error", err)
return res, err
}
err = logdb.WriteLogEntry(ctx, sessionId, storedb.DATA_ACCOUNT_ALIAS, []byte(newAlias))
if err != nil {
logg.DebugCtxf(ctx, "Failed to write account alias db log entry", "key", storedb.DATA_ACCOUNT_ALIAS, "value", newAlias)
}
res.FlagSet = append(res.FlagSet, flag_alias_set)
return res, nil
}
// ClearTemporaryValue empties the DATA_TEMPORARY_VALUE at the main menu to prevent
// previously stored data from being accessed
func (h *MenuHandlers) ClearTemporaryValue(ctx context.Context, sym string, input []byte) (resource.Result, error) {
var res resource.Result
sessionId, ok := ctx.Value("SessionId").(string)
if !ok {
return res, fmt.Errorf("missing session")
}
userStore := h.userdataStore
// clear the temporary value at the start
err := userStore.WriteEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE, []byte(""))
if err != nil {
logg.ErrorCtxf(ctx, "failed to clear DATA_TEMPORARY_VALUE entry with", "key", storedb.DATA_TEMPORARY_VALUE, "value", "empty", "error", err)
return res, err
}
return res, nil
}