Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Use Kong for CLI for run and simulate closes #5 #7

Merged
merged 1 commit into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
265 changes: 265 additions & 0 deletions cmd/dialoguss.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
package cmd

import (
"fmt"
"log"
"math/rand"
"net/http"
"os"
"sync"
"time"

"github.com/nndi-oss/dialoguss/pkg/africastalking"
"github.com/nndi-oss/dialoguss/pkg/core"
"github.com/nndi-oss/dialoguss/pkg/trueroute"
"gopkg.in/yaml.v2"
)

var (
interactive bool
file string
trurouteMode bool
defaultTimeout = 21 * time.Second
)

const (
ApiTypeAfricastalking = "AT_USSD"
ApiTypeTruroute = "TR_USSD"
InteractiveDialTemplate = `Dialing app using:

Phone: %s
Url: %s
SessionID:%s
API Type: %s
`
)

// Dialoguss the main type for interacting with dialoguss sessions
type Dialoguss core.Dialoguss

// UnexpectedResultError unexpected result from the USSD application
func UnexpectedResultError(want string, have string) error {
return fmt.Errorf("Unexpected result.\n\tWant: %s\n\tHave: %s", want, have)
}

// DialStep is the first step in the session, dials the USSD service
func DialStep(expect string) *core.Step {
return &core.Step{
StepNo: 0,
IsLast: false,
IsDial: true,
Text: "",
Expect: expect,
}
}

// NewStep a subsequent step in the session to the USSD service
func NewStep(i int, text string, expect string) *core.Step {
return &core.Step{
StepNo: i,
IsLast: false,
IsDial: false,
Text: text,
Expect: expect,
}
}

// Execute executes a step and returns the result of the request may return an empty string ("") upon failure
func ExecuteStep(s *core.Step, session *core.Session) (string, error) {
if trurouteMode {
step := trueroute.TrueRouteStep{Step: s}
return step.ExecuteAsTruRouteRequest(session)
}

step := africastalking.AfricasTalkingRouteStep{Step: s}
return step.ExecuteAsAfricasTalking(session)
}

// AddStep adds step to session
func AddStep(s *core.Session, step *core.Step) {
s.Steps = append(s.Steps, step)
}

// NewInteractiveSession creates a new interactive session that should be run using RunInteractive
func NewInteractiveSession(d core.DialogussConfig) *core.Session {
rand.Seed(time.Now().UnixNano())
apiType := ApiTypeAfricastalking
if trurouteMode {
apiType = ApiTypeTruroute
}
sessionTimeout := defaultTimeout
if d.Timeout > 0 {
sessionTimeout = time.Duration(d.Timeout) * time.Second
}
return &core.Session{
ID: fmt.Sprintf("DialogussSession__%d", rand.Uint64()),
PhoneNumber: d.PhoneNumber,
Description: "Interactive Session",
Steps: nil,
ServiceCode: d.Dial,
Url: d.URL,
Client: &http.Client{},
ApiType: apiType,
Timeout: sessionTimeout,
}
}

// Run runs the dialoguss session and executes the steps in each session
func Run(s *core.Session) error {
first := true
for i, step := range s.Steps {
if first {
ExecuteStep(step, s)
first = false
continue
}
step.StepNo = i
result, err := ExecuteStep(step, s)
if err != nil {
log.Printf("Failed to execute step %d", step.StepNo)
return err
}
if result != step.Expect {
return UnexpectedResultError(step.Expect, result)
}
}
log.Printf("All steps in session %s run successfully", s.ID)
return nil
}

func prompt() string {
var s string
fmt.Print("Enter value> ")
fmt.Scanln(&s)
return s
}

// promptCh writes users input into a channel
func promptCh(ch chan string) {
var value string
fmt.Print("Enter value> ")
fmt.Scanln(&value)
ch <- value
}

// RunInteractive runs and interactive session that prompts ufor user input
func RunInteractive(s *core.Session) error {
var input, output string
var err error
var step *core.Step
// First Step for the Session is to dial
step = DialStep("")
output, err = ExecuteStep(step, s)

apiTypeName := "AfricasTalking USSD"
if trurouteMode {
apiTypeName = "TNM TruRoute USSD"
}

fmt.Printf(InteractiveDialTemplate,
s.PhoneNumber,
s.Url,
s.ID,
apiTypeName,
)

fmt.Println()
if err != nil {
return err
}
fmt.Println(output)
// Execute other steps if we haven't received an "END" response
sessionLoop:
for i := 0; !step.IsLast; i++ {
inputCh := make(chan string, 1)

// Read the input or timeout after a few seconds (currently 21)
go promptCh(inputCh)

select {
case value := <-inputCh:
input = value
case <-time.After(s.Timeout):
fmt.Println("Session timed out!")
break sessionLoop
}

step = NewStep(i, input, "")
output, err = ExecuteStep(step, s)
if err != nil {
return err
}
fmt.Println(output)
if step.IsLast {
break
}
}

return nil
}

// LoadConfig loads configuration from YAML
func (d *Dialoguss) LoadConfig() error {
d.Config = core.DialogussConfig{Timeout: int(defaultTimeout)}
b, err := os.ReadFile(d.File)
if err != nil {
return err
}

return yaml.Unmarshal(b, &d.Config)
}

// RunAutomatedSessions Loads the sessions for this application
func (d *Dialoguss) RunAutomatedSessions() error {
var wg sync.WaitGroup
wg.Add(len(d.Config.Sessions))

sessionErrors := make(map[string]error)

apiType := ApiTypeAfricastalking
if trurouteMode {
apiType = ApiTypeTruroute
}

for _, session := range d.Config.Sessions {
steps := make([]*core.Step, len(session.Steps))
copy(steps, session.Steps)

s := &core.Session{
ID: session.ID,
Description: session.Description,
PhoneNumber: session.PhoneNumber,
Steps: steps,
Url: d.Config.URL,
Client: &http.Client{},
ApiType: apiType,
}

s.Client.Timeout = 10 * time.Second

go func() {
defer wg.Done()
err := Run(s)
if err != nil {
// sessionErrors <-fmt.Sprintf("Error in Session %s. Got: %s ", s.ID, err)
sessionErrors[s.ID] = err
}
}()
}
wg.Wait()
for key, val := range sessionErrors {
log.Printf("Got error in session %s: %s", key, val)
}
return nil
}

// Run executes the sessions
func (d *Dialoguss) Run() error {
// log.Print("Running dialoguss with config", d.config)
if d.IsInteractive {
session := NewInteractiveSession(d.Config)
return RunInteractive(session)
}

return d.RunAutomatedSessions()
}
Loading
Loading