diff --git a/handlers/application/menuhandler.go b/handlers/application/menuhandler.go index 4c7b4ec..696d230 100644 --- a/handlers/application/menuhandler.go +++ b/handlers/application/menuhandler.go @@ -253,6 +253,1201 @@ func (h *MenuHandlers) ShowBlockedAccount(ctx context.Context, sym string, input return res, nil } +// loadUserContent loads the main user content in the main menu: the alias, balance and active symbol 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") + + // Format the balance to 2 decimal places or default to 0.00 + formattedAmount, err := store.TruncateDecimalString(balance, 2) + if err != nil { + formattedAmount = "0.00" + } + + // format the final output + balStr := fmt.Sprintf("%s %s", formattedAmount, activeSym) + if alias != "" { + content = l.Get("%s\nBalance: %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 + 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 + } + } + content, err = loadUserContent(ctx, string(activeSym), string(activeBal), string(accAlias)) + 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 +} + +// ValidateRecipient validates that the given input is valid. +// +// TODO: split up functino +func (h *MenuHandlers) ValidateRecipient(ctx context.Context, sym string, input []byte) (resource.Result, error) { + var res resource.Result + var AliasAddressResult string + var AliasAddress *models.AliasAddress + store := h.userdataStore + + sessionId, ok := ctx.Value("SessionId").(string) + if !ok { + return res, fmt.Errorf("missing session") + } + flag_invalid_recipient, _ := h.flagManager.GetFlag("flag_invalid_recipient") + flag_invalid_recipient_with_invite, _ := h.flagManager.GetFlag("flag_invalid_recipient_with_invite") + flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error") + + // remove white spaces + recipient := strings.ReplaceAll(string(input), " ", "") + + if recipient != "0" { + recipientType, err := identity.CheckRecipient(recipient) + if err != nil { + // Invalid recipient format (not a phone number, address, or valid alias format) + res.FlagSet = append(res.FlagSet, flag_invalid_recipient) + res.Content = recipient + + return res, nil + } + + // save the recipient as the temporaryRecipient + err = store.WriteEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE, []byte(recipient)) + if err != nil { + logg.ErrorCtxf(ctx, "failed to write temporaryRecipient entry with", "key", storedb.DATA_TEMPORARY_VALUE, "value", recipient, "error", err) + return res, err + } + + switch recipientType { + case "phone number": + // format the phone number + formattedNumber, err := phone.FormatPhoneNumber(recipient) + if err != nil { + logg.ErrorCtxf(ctx, "Failed to format the phone number: %s", recipient, "error", err) + return res, err + } + + // Check if the phone number is registered + publicKey, err := store.ReadEntry(ctx, formattedNumber, storedb.DATA_PUBLIC_KEY) + if err != nil { + if db.IsNotFound(err) { + logg.InfoCtxf(ctx, "Unregistered phone number: %s", recipient) + res.FlagSet = append(res.FlagSet, flag_invalid_recipient_with_invite) + res.Content = recipient + return res, nil + } + + logg.ErrorCtxf(ctx, "failed to read publicKey entry with", "key", storedb.DATA_PUBLIC_KEY, "error", err) + return res, err + } + + // Save the publicKey as the recipient + err = store.WriteEntry(ctx, sessionId, storedb.DATA_RECIPIENT, publicKey) + if err != nil { + logg.ErrorCtxf(ctx, "failed to write recipient entry with", "key", storedb.DATA_RECIPIENT, "value", string(publicKey), "error", err) + return res, err + } + + case "address": + // checksum the address + address := ethutils.ChecksumAddress(recipient) + + // Save the valid Ethereum address as the recipient + err = store.WriteEntry(ctx, sessionId, storedb.DATA_RECIPIENT, []byte(address)) + if err != nil { + logg.ErrorCtxf(ctx, "failed to write recipient entry with", "key", storedb.DATA_RECIPIENT, "value", recipient, "error", err) + return res, err + } + + case "alias": + if strings.Contains(recipient, ".") { + AliasAddress, err = h.accountService.CheckAliasAddress(ctx, recipient) + if err == nil { + AliasAddressResult = AliasAddress.Address + } else { + logg.ErrorCtxf(ctx, "failed to resolve alias", "alias", recipient, "error_alias_check", err) + } + } else { + //Perform a search for each search domain,break on first match + for _, domain := range config.SearchDomains() { + fqdn := fmt.Sprintf("%s.%s", recipient, domain) + logg.InfoCtxf(ctx, "Resolving with fqdn alias", "alias", fqdn) + AliasAddress, err = h.accountService.CheckAliasAddress(ctx, fqdn) + if err == nil { + res.FlagReset = append(res.FlagReset, flag_api_error) + AliasAddressResult = AliasAddress.Address + continue + } else { + res.FlagSet = append(res.FlagSet, flag_api_error) + logg.ErrorCtxf(ctx, "failed to resolve alias", "alias", recipient, "error_alias_check", err) + return res, nil + } + } + } + if AliasAddressResult == "" { + res.Content = recipient + res.FlagSet = append(res.FlagSet, flag_invalid_recipient) + return res, nil + } else { + err = store.WriteEntry(ctx, sessionId, storedb.DATA_RECIPIENT, []byte(AliasAddressResult)) + if err != nil { + logg.ErrorCtxf(ctx, "failed to write recipient entry with", "key", storedb.DATA_RECIPIENT, "value", AliasAddressResult, "error", err) + return res, err + } + } + } + } + + return res, nil +} + +// TransactionReset resets the previous transaction data (Recipient and Amount) +// as well as the invalid flags. +func (h *MenuHandlers) TransactionReset(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_invalid_recipient, _ := h.flagManager.GetFlag("flag_invalid_recipient") + flag_invalid_recipient_with_invite, _ := h.flagManager.GetFlag("flag_invalid_recipient_with_invite") + store := h.userdataStore + err = store.WriteEntry(ctx, sessionId, storedb.DATA_AMOUNT, []byte("")) + if err != nil { + return res, nil + } + + err = store.WriteEntry(ctx, sessionId, storedb.DATA_RECIPIENT, []byte("")) + if err != nil { + return res, nil + } + + res.FlagReset = append(res.FlagReset, flag_invalid_recipient, flag_invalid_recipient_with_invite) + + return res, nil +} + +// InviteValidRecipient sends an invitation to the valid phone number. +func (h *MenuHandlers) InviteValidRecipient(ctx context.Context, sym string, input []byte) (resource.Result, error) { + var res resource.Result + store := h.userdataStore + smsservice := h.smsService + + 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") + + recipient, err := store.ReadEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE) + if err != nil { + logg.ErrorCtxf(ctx, "Failed to read invalid recipient info", "error", err) + return res, err + } + + if !phone.IsValidPhoneNumber(string(recipient)) { + logg.InfoCtxf(ctx, "corrupted recipient", "key", storedb.DATA_TEMPORARY_VALUE, "recipient", recipient) + return res, nil + } + + _, err = smsservice.Accountservice.SendUpsellSMS(ctx, sessionId, string(recipient)) + if err != nil { + res.Content = l.Get("Your invite request for %s to Sarafu Network failed. Please try again later.", string(recipient)) + return res, nil + } + res.Content = l.Get("Your invitation to %s to join Sarafu Network has been sent.", string(recipient)) + return res, nil +} + +// ResetTransactionAmount resets the transaction amount and invalid flag. +func (h *MenuHandlers) ResetTransactionAmount(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_invalid_amount, _ := h.flagManager.GetFlag("flag_invalid_amount") + store := h.userdataStore + err = store.WriteEntry(ctx, sessionId, storedb.DATA_AMOUNT, []byte("")) + if err != nil { + return res, nil + } + + res.FlagReset = append(res.FlagReset, flag_invalid_amount) + + return res, nil +} + +// MaxAmount gets the current balance from the API and sets it as +// the result content. +func (h *MenuHandlers) MaxAmount(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 + + activeBal, err := store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_BAL) + if err != nil { + logg.ErrorCtxf(ctx, "failed to read activeBal entry with", "key", storedb.DATA_ACTIVE_BAL, "error", err) + return res, err + } + + res.Content = string(activeBal) + + return res, nil +} + +// ValidateAmount ensures that the given input is a valid amount and that +// it is not more than the current balance. +func (h *MenuHandlers) ValidateAmount(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") + } + flag_invalid_amount, _ := h.flagManager.GetFlag("flag_invalid_amount") + userStore := h.userdataStore + + var balanceValue float64 + + // retrieve the active balance + activeBal, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_BAL) + if err != nil { + logg.ErrorCtxf(ctx, "failed to read activeBal entry with", "key", storedb.DATA_ACTIVE_BAL, "error", err) + return res, err + } + balanceValue, err = strconv.ParseFloat(string(activeBal), 64) + if err != nil { + logg.ErrorCtxf(ctx, "Failed to convert the activeBal to a float", "error", err) + return res, err + } + + // Extract numeric part from the input amount + amountStr := strings.TrimSpace(string(input)) + inputAmount, err := strconv.ParseFloat(amountStr, 64) + if err != nil { + res.FlagSet = append(res.FlagSet, flag_invalid_amount) + res.Content = amountStr + return res, nil + } + + if inputAmount > balanceValue { + res.FlagSet = append(res.FlagSet, flag_invalid_amount) + res.Content = amountStr + return res, nil + } + + // Format the amount to 2 decimal places before saving (truncated) + formattedAmount, err := store.TruncateDecimalString(amountStr, 2) + if err != nil { + res.FlagSet = append(res.FlagSet, flag_invalid_amount) + res.Content = amountStr + return res, nil + } + + err = userStore.WriteEntry(ctx, sessionId, storedb.DATA_AMOUNT, []byte(formattedAmount)) + if err != nil { + logg.ErrorCtxf(ctx, "failed to write amount entry with", "key", storedb.DATA_AMOUNT, "value", formattedAmount, "error", err) + return res, err + } + + res.Content = formattedAmount + return res, nil +} + +// GetRecipient returns the transaction recipient phone number from the gdbm. +func (h *MenuHandlers) GetRecipient(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") + } + store := h.userdataStore + recipient, _ := store.ReadEntry(ctx, sessionId, storedb.DATA_TEMPORARY_VALUE) + if len(recipient) == 0 { + logg.ErrorCtxf(ctx, "recipient is empty", "key", storedb.DATA_TEMPORARY_VALUE) + return res, fmt.Errorf("Data error encountered") + } + + res.Content = string(recipient) + + return res, nil +} + +// RetrieveBlockedNumber gets the current number during the pin reset for other's is in progress. +func (h *MenuHandlers) RetrieveBlockedNumber(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") + } + store := h.userdataStore + blockedNumber, _ := store.ReadEntry(ctx, sessionId, storedb.DATA_BLOCKED_NUMBER) + + res.Content = string(blockedNumber) + + return res, nil +} + +// GetSender returns the sessionId (phoneNumber). +func (h *MenuHandlers) GetSender(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") + } + + res.Content = sessionId + + return res, nil +} + +// GetAmount retrieves the amount from teh Gdbm Db. +func (h *MenuHandlers) GetAmount(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") + } + store := h.userdataStore + + // retrieve the active symbol + activeSym, err := store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_SYM) + if err != nil { + logg.ErrorCtxf(ctx, "failed to read activeSym entry with", "key", storedb.DATA_ACTIVE_SYM, "error", err) + return res, err + } + + amount, _ := store.ReadEntry(ctx, sessionId, storedb.DATA_AMOUNT) + + res.Content = fmt.Sprintf("%s %s", string(amount), string(activeSym)) + + return res, nil +} + +// InitiateTransaction calls the TokenTransfer and returns a confirmation based on the result. +func (h *MenuHandlers) InitiateTransaction(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_account_authorized, _ := h.flagManager.GetFlag("flag_account_authorized") + + code := codeFromCtx(ctx) + l := gotext.NewLocale(translationDir, code) + l.AddDomain("default") + + data, err := store.ReadTransactionData(ctx, h.userdataStore, sessionId) + if err != nil { + return res, err + } + + finalAmountStr, err := store.ParseAndScaleAmount(data.Amount, data.ActiveDecimal) + if err != nil { + return res, err + } + + // Call TokenTransfer + r, err := h.accountService.TokenTransfer(ctx, finalAmountStr, data.PublicKey, data.Recipient, data.ActiveAddress) + if err != nil { + flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error") + res.FlagSet = append(res.FlagSet, flag_api_error) + res.Content = l.Get("Your request failed. Please try again later.") + logg.ErrorCtxf(ctx, "failed on TokenTransfer", "error", err) + return res, nil + } + + trackingId := r.TrackingId + logg.InfoCtxf(ctx, "TokenTransfer", "trackingId", trackingId) + + res.Content = l.Get( + "Your request has been sent. %s will receive %s %s from %s.", + data.TemporaryValue, + data.Amount, + data.ActiveSym, + sessionId, + ) + + res.FlagReset = append(res.FlagReset, flag_account_authorized) + return res, nil +} + +// ManageVouchers retrieves the token holdings from the API using the "PublicKey" and +// 1. sets the first as the default voucher if no active voucher is set. +// 2. Stores list of vouchers +// 3. updates the balance of the active voucher +func (h *MenuHandlers) ManageVouchers(ctx context.Context, sym string, input []byte) (resource.Result, error) { + var res resource.Result + userStore := h.userdataStore + logdb := h.logDb + + sessionId, ok := ctx.Value("SessionId").(string) + if !ok { + return res, fmt.Errorf("missing session") + } + + flag_no_active_voucher, _ := h.flagManager.GetFlag("flag_no_active_voucher") + flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error") + + publicKey, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_PUBLIC_KEY) + if err != nil { + logg.ErrorCtxf(ctx, "failed to read publicKey entry", "key", storedb.DATA_PUBLIC_KEY, "error", err) + return res, err + } + + // Fetch vouchers from API + vouchersResp, err := h.accountService.FetchVouchers(ctx, string(publicKey)) + if err != nil { + res.FlagSet = append(res.FlagSet, flag_api_error) + return res, nil + } + res.FlagReset = append(res.FlagReset, flag_api_error) + + if len(vouchersResp) == 0 { + res.FlagSet = append(res.FlagSet, flag_no_active_voucher) + return res, nil + } + + res.FlagReset = append(res.FlagReset, flag_no_active_voucher) + + // Check if user has an active voucher with proper error handling + activeSym, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_SYM) + if err != nil { + if db.IsNotFound(err) { + // No active voucher, set the first one as default + firstVoucher := vouchersResp[0] + defaultSym := firstVoucher.TokenSymbol + defaultBal := firstVoucher.Balance + defaultDec := firstVoucher.TokenDecimals + defaultAddr := firstVoucher.TokenAddress + + // Scale down the balance + scaledBalance := store.ScaleDownBalance(defaultBal, defaultDec) + + firstVoucherMap := map[storedb.DataTyp]string{ + storedb.DATA_ACTIVE_SYM: defaultSym, + storedb.DATA_ACTIVE_BAL: scaledBalance, + storedb.DATA_ACTIVE_DECIMAL: defaultDec, + storedb.DATA_ACTIVE_ADDRESS: defaultAddr, + } + + for key, value := range firstVoucherMap { + if err := userStore.WriteEntry(ctx, sessionId, key, []byte(value)); err != nil { + logg.ErrorCtxf(ctx, "Failed to write active voucher data", "key", key, "error", err) + return res, err + } + err = logdb.WriteLogEntry(ctx, sessionId, key, []byte(value)) + if err != nil { + logg.DebugCtxf(ctx, "Failed to write voucher db log entry", "key", key, "value", value) + } + } + + logg.InfoCtxf(ctx, "Default voucher set", "symbol", defaultSym, "balance", defaultBal, "decimals", defaultDec, "address", defaultAddr) + } else { + logg.ErrorCtxf(ctx, "failed to read activeSym entry with", "key", storedb.DATA_ACTIVE_SYM, "error", err) + return res, err + } + } else { + // Update active voucher balance + activeSymStr := string(activeSym) + + // Find the matching voucher data + var activeData *dataserviceapi.TokenHoldings + for _, voucher := range vouchersResp { + if voucher.TokenSymbol == activeSymStr { + activeData = &voucher + break + } + } + + if activeData == nil { + logg.ErrorCtxf(ctx, "activeSym not found in vouchers, setting the first voucher as the default", "activeSym", activeSymStr) + firstVoucher := vouchersResp[0] + activeData = &firstVoucher + } + + // Scale down the balance + scaledBalance := store.ScaleDownBalance(activeData.Balance, activeData.TokenDecimals) + + // Update the balance field with the scaled value + activeData.Balance = scaledBalance + + // Pass the matching voucher data to UpdateVoucherData + if err := store.UpdateVoucherData(ctx, h.userdataStore, sessionId, activeData); err != nil { + logg.ErrorCtxf(ctx, "failed on UpdateVoucherData", "error", err) + return res, err + } + } + + // Store all voucher data + data := store.ProcessVouchers(vouchersResp) + dataMap := map[storedb.DataTyp]string{ + storedb.DATA_VOUCHER_SYMBOLS: data.Symbols, + storedb.DATA_VOUCHER_BALANCES: data.Balances, + storedb.DATA_VOUCHER_DECIMALS: data.Decimals, + storedb.DATA_VOUCHER_ADDRESSES: data.Addresses, + } + + // Write data entries + for key, value := range dataMap { + if err := userStore.WriteEntry(ctx, sessionId, key, []byte(value)); err != nil { + logg.ErrorCtxf(ctx, "Failed to write data entry for sessionId: %s", sessionId, "key", key, "error", err) + continue + } + } + + return res, nil +} + +// GetVoucherList fetches the list of vouchers and formats them. +func (h *MenuHandlers) GetVoucherList(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 + + // Read vouchers from the store + voucherData, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_VOUCHER_SYMBOLS) + logg.InfoCtxf(ctx, "reading voucherData in GetVoucherList", "sessionId", sessionId, "key", storedb.DATA_VOUCHER_SYMBOLS, "voucherData", voucherData) + if err != nil { + logg.ErrorCtxf(ctx, "failed to read voucherData entires with", "key", storedb.DATA_VOUCHER_SYMBOLS, "error", err) + return res, err + } + + voucherBalances, err := userStore.ReadEntry(ctx, sessionId, storedb.DATA_VOUCHER_BALANCES) + logg.InfoCtxf(ctx, "reading voucherBalances in GetVoucherList", "sessionId", sessionId, "key", storedb.DATA_VOUCHER_BALANCES, "voucherBalances", voucherBalances) + if err != nil { + logg.ErrorCtxf(ctx, "failed to read voucherData entires with", "key", storedb.DATA_VOUCHER_BALANCES, "error", err) + return res, err + } + + formattedVoucherList := store.FormatVoucherList(ctx, string(voucherData), string(voucherBalances)) + finalOutput := strings.Join(formattedVoucherList, "\n") + + logg.InfoCtxf(ctx, "final output for GetVoucherList", "sessionId", sessionId, "finalOutput", finalOutput) + + res.Content = finalOutput + + return res, nil +} + +// ViewVoucher retrieves the token holding and balance from the subprefixDB +// and displays it to the user for them to select it. +func (h *MenuHandlers) ViewVoucher(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_voucher, _ := h.flagManager.GetFlag("flag_incorrect_voucher") + + inputStr := string(input) + + metadata, err := store.GetVoucherData(ctx, h.userdataStore, sessionId, inputStr) + if err != nil { + return res, fmt.Errorf("failed to retrieve voucher data: %v", err) + } + + if metadata == nil { + res.FlagSet = append(res.FlagSet, flag_incorrect_voucher) + return res, nil + } + + if err := store.StoreTemporaryVoucher(ctx, h.userdataStore, sessionId, metadata); err != nil { + logg.ErrorCtxf(ctx, "failed on StoreTemporaryVoucher", "error", err) + return res, err + } + + res.FlagReset = append(res.FlagReset, flag_incorrect_voucher) + res.Content = l.Get("Symbol: %s\nBalance: %s", metadata.TokenSymbol, metadata.Balance) + + return res, nil +} + +// SetVoucher retrieves the temp voucher data and sets it as the active data. +func (h *MenuHandlers) SetVoucher(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.GetTemporaryVoucherData(ctx, h.userdataStore, sessionId) + if err != nil { + logg.ErrorCtxf(ctx, "failed on GetTemporaryVoucherData", "error", err) + return res, err + } + + // Set as active and clear temporary data + if err := store.UpdateVoucherData(ctx, h.userdataStore, sessionId, tempData); err != nil { + logg.ErrorCtxf(ctx, "failed on UpdateVoucherData", "error", err) + return res, err + } + + res.Content = tempData.TokenSymbol + return res, nil +} + +// GetVoucherDetails retrieves the voucher details. +func (h *MenuHandlers) GetVoucherDetails(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") + } + flag_api_error, _ := h.flagManager.GetFlag("flag_api_call_error") + + // get the active address + activeAddress, err := store.ReadEntry(ctx, sessionId, storedb.DATA_ACTIVE_ADDRESS) + if err != nil { + logg.ErrorCtxf(ctx, "failed to read activeAddress entry with", "key", storedb.DATA_ACTIVE_ADDRESS, "error", err) + return res, err + } + + // use the voucher contract address to get the data from the API + voucherData, err := h.accountService.VoucherData(ctx, string(activeAddress)) + if err != nil { + res.FlagSet = append(res.FlagSet, flag_api_error) + return res, nil + } + res.FlagReset = append(res.FlagReset, flag_api_error) + + res.Content = fmt.Sprintf( + "Name: %s\nSymbol: %s\nCommodity: %s\nLocation: %s", voucherData.TokenName, voucherData.TokenSymbol, voucherData.TokenCommodity, voucherData.TokenLocation, + ) + + 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 +} + +// CheckTransactions retrieves the transactions from the API using the "PublicKey" and stores to prefixDb. +func (h *MenuHandlers) CheckTransactions(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") + } + + flag_no_transfers, _ := h.flagManager.GetFlag("flag_no_transfers") + flag_api_error, _ := h.flagManager.GetFlag("flag_api_error") + + userStore := h.userdataStore + logdb := h.logDb + publicKey, err := userStore.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 + } + + // Fetch transactions from the API using the public key + transactionsResp, err := h.accountService.FetchTransactions(ctx, string(publicKey)) + if err != nil { + res.FlagSet = append(res.FlagSet, flag_api_error) + logg.ErrorCtxf(ctx, "failed on FetchTransactions", "error", err) + return res, err + } + res.FlagReset = append(res.FlagReset, flag_api_error) + + // Return if there are no transactions + if len(transactionsResp) == 0 { + res.FlagSet = append(res.FlagSet, flag_no_transfers) + return res, nil + } + + data := store.ProcessTransfers(transactionsResp) + + // Store all transaction data + dataMap := map[storedb.DataTyp]string{ + storedb.DATA_TX_SENDERS: data.Senders, + storedb.DATA_TX_RECIPIENTS: data.Recipients, + storedb.DATA_TX_VALUES: data.TransferValues, + storedb.DATA_TX_ADDRESSES: data.Addresses, + storedb.DATA_TX_HASHES: data.TxHashes, + storedb.DATA_TX_DATES: data.Dates, + storedb.DATA_TX_SYMBOLS: data.Symbols, + storedb.DATA_TX_DECIMALS: data.Decimals, + } + + for key, value := range dataMap { + if err := h.prefixDb.Put(ctx, []byte(storedb.ToBytes(key)), []byte(value)); err != nil { + logg.ErrorCtxf(ctx, "failed to write to prefixDb", "error", err) + return res, err + } + err = logdb.WriteLogEntry(ctx, sessionId, key, []byte(value)) + if err != nil { + logg.DebugCtxf(ctx, "Failed to write tx db log entry", "key", key, "value", value) + } + } + + res.FlagReset = append(res.FlagReset, flag_no_transfers) + + return res, nil +} + +// GetTransactionsList fetches the list of transactions and formats them. +func (h *MenuHandlers) GetTransactionsList(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 + publicKey, err := userStore.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 + } + + // Read transactions from the store and format them + TransactionSenders, err := h.prefixDb.Get(ctx, storedb.ToBytes(storedb.DATA_TX_SENDERS)) + if err != nil { + logg.ErrorCtxf(ctx, "Failed to read the TransactionSenders from prefixDb", "error", err) + return res, err + } + TransactionSyms, err := h.prefixDb.Get(ctx, storedb.ToBytes(storedb.DATA_TX_SYMBOLS)) + if err != nil { + logg.ErrorCtxf(ctx, "Failed to read the TransactionSyms from prefixDb", "error", err) + return res, err + } + TransactionValues, err := h.prefixDb.Get(ctx, storedb.ToBytes(storedb.DATA_TX_VALUES)) + if err != nil { + logg.ErrorCtxf(ctx, "Failed to read the TransactionValues from prefixDb", "error", err) + return res, err + } + TransactionDates, err := h.prefixDb.Get(ctx, storedb.ToBytes(storedb.DATA_TX_DATES)) + if err != nil { + logg.ErrorCtxf(ctx, "Failed to read the TransactionDates from prefixDb", "error", err) + return res, err + } + + // Parse the data + senders := strings.Split(string(TransactionSenders), "\n") + syms := strings.Split(string(TransactionSyms), "\n") + values := strings.Split(string(TransactionValues), "\n") + dates := strings.Split(string(TransactionDates), "\n") + + var formattedTransactions []string + for i := 0; i < len(senders); i++ { + sender := strings.TrimSpace(senders[i]) + sym := strings.TrimSpace(syms[i]) + value := strings.TrimSpace(values[i]) + date := strings.Split(strings.TrimSpace(dates[i]), " ")[0] + + status := "Received" + if sender == string(publicKey) { + status = "Sent" + } + + // Use the ReplaceSeparator function for the menu separator + transactionLine := fmt.Sprintf("%d%s%s %s %s %s", i+1, h.ReplaceSeparatorFunc(":"), status, value, sym, date) + formattedTransactions = append(formattedTransactions, transactionLine) + } + + res.Content = strings.Join(formattedTransactions, "\n") + + return res, nil +} + +// ViewTransactionStatement retrieves the transaction statement +// and displays it to the user. +func (h *MenuHandlers) ViewTransactionStatement(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 + publicKey, err := userStore.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 + } + + flag_incorrect_statement, _ := h.flagManager.GetFlag("flag_incorrect_statement") + + inputStr := string(input) + if inputStr == "0" || inputStr == "99" || inputStr == "11" || inputStr == "22" { + res.FlagReset = append(res.FlagReset, flag_incorrect_statement) + return res, nil + } + + // Convert input string to integer + index, err := strconv.Atoi(strings.TrimSpace(inputStr)) + if err != nil { + return res, fmt.Errorf("invalid input: must be a number between 1 and 10") + } + + if index < 1 || index > 10 { + return res, fmt.Errorf("invalid input: index must be between 1 and 10") + } + + statement, err := store.GetTransferData(ctx, h.prefixDb, string(publicKey), index) + if err != nil { + return res, fmt.Errorf("failed to retrieve transfer data: %v", err) + } + + if statement == "" { + res.FlagSet = append(res.FlagSet, flag_incorrect_statement) + return res, nil + } + + res.FlagReset = append(res.FlagReset, flag_incorrect_statement) + res.Content = statement + + 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) +} + +// 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 + var alias string + 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") + flag_alias_unavailable, _ := h.flagManager.GetFlag("flag_alias_unavailable") + + 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 + } + publicKey, err := store.ReadEntry(ctx, sessionId, storedb.DATA_PUBLIC_KEY) + if err != nil { + if db.IsNotFound(err) { + return res, nil + } + } + sanitizedInput := sanitizeAliasHint(string(input)) + // Check if an alias already exists + existingAlias, err := store.ReadEntry(ctx, sessionId, storedb.DATA_ACCOUNT_ALIAS) + if err == nil && len(existingAlias) > 0 { + logg.InfoCtxf(ctx, "Current alias", "alias", string(existingAlias)) + + unavailable, err := h.isAliasUnavailable(ctx, sanitizedInput) + if err == nil && unavailable { + res.FlagSet = append(res.FlagSet, flag_alias_unavailable) + res.FlagReset = append(res.FlagReset, flag_api_error) + return res, nil + } + + res.FlagReset = append(res.FlagReset, flag_alias_unavailable) + + // Update existing alias + aliasResult, err := h.accountService.UpdateAlias(ctx, sanitizedInput, string(publicKey)) + if err != nil { + res.FlagSet = append(res.FlagSet, flag_api_error) + logg.ErrorCtxf(ctx, "failed to update alias", "alias", sanitizedInput, "error", err) + return res, nil + } + alias = aliasResult.Alias + logg.InfoCtxf(ctx, "Updated alias", "alias", alias) + } else { + logg.InfoCtxf(ctx, "Registering a new alias", "err", err) + + unavailable, err := h.isAliasUnavailable(ctx, sanitizedInput) + if err == nil && unavailable { + res.FlagSet = append(res.FlagSet, flag_alias_unavailable) + res.FlagReset = append(res.FlagReset, flag_api_error) + return res, nil + } + + res.FlagReset = append(res.FlagReset, flag_alias_unavailable) + + // Register a new alias + aliasResult, err := h.accountService.RequestAlias(ctx, string(publicKey), sanitizedInput) + if err != nil { + res.FlagSet = append(res.FlagSet, flag_api_error) + logg.ErrorCtxf(ctx, "failed to retrieve alias", "alias", sanitizedInput, "error_alias_request", err) + return res, nil + } + res.FlagReset = append(res.FlagReset, flag_api_error) + + alias = aliasResult.Alias + logg.InfoCtxf(ctx, "Registered alias", "alias", alias) + } + + //Store the new account alias + logg.InfoCtxf(ctx, "Final registered alias", "alias", 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 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 +} + +func (h *MenuHandlers) isAliasUnavailable(ctx context.Context, alias string) (bool, error) { + fqdn := fmt.Sprintf("%s.%s", alias, "sarafu.eth") + logg.InfoCtxf(ctx, "Checking if the fqdn alias is taken", "fqdn", fqdn) + + aliasAddress, err := h.accountService.CheckAliasAddress(ctx, fqdn) + if err != nil { + return false, err + } + if len(aliasAddress.Address) > 0 { + return true, nil + } + + return false, 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) { diff --git a/handlers/local.go b/handlers/local.go index 4ff1e13..ed64020 100644 --- a/handlers/local.go +++ b/handlers/local.go @@ -130,8 +130,6 @@ func (ls *LocalHandlerService) GetHandler(accountService remote.AccountService) ls.DbRs.AddLocalFunc("clear_temporary_value", appHandlers.ClearTemporaryValue) ls.DbRs.AddLocalFunc("reset_invalid_pin", appHandlers.ResetInvalidPIN) ls.DbRs.AddLocalFunc("request_custom_alias", appHandlers.RequestCustomAlias) - ls.DbRs.AddLocalFunc("get_suggested_alias", appHandlers.GetSuggestedAlias) - ls.DbRs.AddLocalFunc("confirm_new_alias", appHandlers.ConfirmNewAlias) ls.DbRs.AddLocalFunc("check_account_created", appHandlers.CheckAccountCreated) ls.DbRs.AddLocalFunc("reset_api_call_failure", appHandlers.ResetApiCallFailure) ls.DbRs.AddLocalFunc("swap_to_list", appHandlers.LoadSwapToList) diff --git a/services/registration/update_alias b/services/registration/alias_updated similarity index 100% rename from services/registration/update_alias rename to services/registration/alias_updated diff --git a/services/registration/update_alias.vis b/services/registration/alias_updated.vis similarity index 50% rename from services/registration/update_alias.vis rename to services/registration/alias_updated.vis index fcbfc17..832ef22 100644 --- a/services/registration/update_alias.vis +++ b/services/registration/alias_updated.vis @@ -1,5 +1,3 @@ -LOAD confirm_new_alias 0 -RELOAD confirm_new_alias MOUT back 0 MOUT quit 9 HALT diff --git a/services/registration/update_alias_swa b/services/registration/alias_updated_swa similarity index 100% rename from services/registration/update_alias_swa rename to services/registration/alias_updated_swa diff --git a/services/registration/confirm_new_alias b/services/registration/confirm_new_alias deleted file mode 100644 index 6cc2cc3..0000000 --- a/services/registration/confirm_new_alias +++ /dev/null @@ -1,2 +0,0 @@ -Your full alias will be: {{.get_suggested_alias}} -Please enter your PIN to confirm: diff --git a/services/registration/confirm_new_alias.vis b/services/registration/confirm_new_alias.vis deleted file mode 100644 index ea79412..0000000 --- a/services/registration/confirm_new_alias.vis +++ /dev/null @@ -1,12 +0,0 @@ -LOAD reset_invalid_pin 6 -RELOAD reset_invalid_pin -LOAD get_suggested_alias 0 -RELOAD get_suggested_alias -MAP get_suggested_alias -MOUT back 0 -HALT -INCMP _ 0 -RELOAD authorize_account -CATCH incorrect_pin flag_incorrect_pin 1 -CATCH invalid_pin flag_invalid_pin 1 -CATCH update_alias flag_allow_update 1 diff --git a/services/registration/confirm_new_alias_swa b/services/registration/confirm_new_alias_swa deleted file mode 100644 index e622e9c..0000000 --- a/services/registration/confirm_new_alias_swa +++ /dev/null @@ -1,2 +0,0 @@ -Lakabu yako kamili itakuwa: {{.get_suggested_alias}} -Tafadhali weka PIN yako ili kuthibitisha: diff --git a/services/registration/home_menu b/services/registration/home_menu new file mode 100644 index 0000000..357dd48 --- /dev/null +++ b/services/registration/home_menu @@ -0,0 +1 @@ +Home \ No newline at end of file diff --git a/services/registration/home_menu_swa b/services/registration/home_menu_swa new file mode 100644 index 0000000..88dc089 --- /dev/null +++ b/services/registration/home_menu_swa @@ -0,0 +1 @@ +Mwanzo \ No newline at end of file diff --git a/services/registration/my_account_alias.vis b/services/registration/my_account_alias.vis index c44cce1..f4b18d9 100644 --- a/services/registration/my_account_alias.vis +++ b/services/registration/my_account_alias.vis @@ -1,3 +1,7 @@ +LOAD reset_account_authorized 0 +LOAD reset_incorrect_pin 0 +CATCH incorrect_pin flag_incorrect_pin 1 +CATCH pin_entry flag_account_authorized 0 LOAD get_current_profile_info 0 MAP get_current_profile_info MOUT back 0 @@ -5,5 +9,7 @@ HALT INCMP _ 0 LOAD request_custom_alias 0 RELOAD request_custom_alias +MAP request_custom_alias +CATCH unavailable_alias flag_alias_unavailable 1 CATCH api_failure flag_api_call_error 1 -INCMP confirm_new_alias * +INCMP alias_updated * diff --git a/services/registration/pp.csv b/services/registration/pp.csv index fa67938..1e476c1 100644 --- a/services/registration/pp.csv +++ b/services/registration/pp.csv @@ -34,4 +34,4 @@ flag,flag_alias_set,40,this is set when an account alias has been assigned to a flag,flag_account_pin_reset,41,this is set on an account when an admin triggers a PIN reset for themflag,flag_incorrect_pool,39,this is set when the user selects an invalid pool flag,flag_incorrect_pool,42,this is set when the user selects an invalid pool flag,flag_low_swap_amount,43,this is set when the swap max limit is less than 0.1 - +flag,flag_alias_unavailable,44,this is set when the preferred alias is not available diff --git a/services/registration/unavailable_alias b/services/registration/unavailable_alias new file mode 100644 index 0000000..6f1efd9 --- /dev/null +++ b/services/registration/unavailable_alias @@ -0,0 +1 @@ +The alias {{.request_custom_alias}} isn't available \ No newline at end of file diff --git a/services/registration/unavailable_alias.vis b/services/registration/unavailable_alias.vis new file mode 100644 index 0000000..e2f8831 --- /dev/null +++ b/services/registration/unavailable_alias.vis @@ -0,0 +1,8 @@ +MAP request_custom_alias +MOUT back 0 +MOUT home 9 +MOUT quit 99 +HALT +INCMP _ 0 +INCMP ^ 9 +INCMP quit 99 diff --git a/services/registration/unavailable_alias_swa b/services/registration/unavailable_alias_swa new file mode 100644 index 0000000..8119c46 --- /dev/null +++ b/services/registration/unavailable_alias_swa @@ -0,0 +1 @@ +Lakabu ulilochagua {{.request_custom_alias}} halipatikani \ No newline at end of file