diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f276daa --- /dev/null +++ b/.gitignore @@ -0,0 +1,92 @@ +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Go template +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +vendor/ +/Godeps/ +.idea/ +dump/ +data/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c268684 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 pokornyIt.cz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d31a63a --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# Finesse API + +Utilities for work with the Unified Cisco Contact Center agents through Finesse API. + +API allows actions per one agent or group of agents. + +- get agent status +- login agent +- set agent ready state +- set agent not-ready state +- logout agent + +## Limitation +The current version does not use XMPP communication with the Finesse server. +Program tested on version Finesse 12.5. + +## How use + +For any operation is necessary to create a server structure with an address and port. +It is necessary to register the agents with which the operations will take place on the server. + + +```go +// import finesse_api library +import ( + api "github.com/pokornyIt/finesse-api" +) + +// create Finesse server object +server := finesse_api.NewFinesseServer("finesse.server.fqdn", 8435) + +// add agents to server +agent := api.NewAgentName("Name1", "Password", "1000") +server.AddAgent(agent) +agent := api.NewAgentName("Name2", "Password", "1001") +server.AddAgent(agent) +agent := api.NewAgentName("Name3", "Password", "1002") +server.AddAgent(agent) + +// get status for all defined agent +states, err = server.GetStateAgentsParallel() + +// login agent Name2 to system +state := server.LoginAgent("Name2") + +// login all agents and set it to ready state +states, err = server.ReadyAgentsParallelWithStatus(true) +``` + +Program use standard logger library "github.com/sirupsen/logrus". diff --git a/finesse-agent-detail.go b/finesse-agent-detail.go new file mode 100644 index 0000000..a48b201 --- /dev/null +++ b/finesse-agent-detail.go @@ -0,0 +1,135 @@ +package finesse_api + +import ( + "encoding/xml" + "fmt" +) + +// UserDetailResponse Structure holds agent status +// Contains all possible values returned in the get agent state +type UserDetailResponse struct { + XMLName xml.Name `xml:"User"` + Text string `xml:",chardata"` + Dialogs string `xml:"dialogs"` + Extension string `xml:"extension"` + FirstName string `xml:"firstName"` + LastName string `xml:"lastName"` + LoginId string `xml:"loginId"` + LoginName string `xml:"loginName"` + MediaType string `xml:"mediaType"` + ReasonCodeId string `xml:"reasonCodeId"` + ReasonCode struct { + Text string `xml:",chardata"` + Category string `xml:"category"` + URL string `xml:"uri"` + Code string `xml:"code"` + Label string `xml:"label"` + ForAll bool `xml:"forAll"` + Id int `xml:"id"` + } `xml:"ReasonCode"` + Roles struct { + Text string `xml:",chardata"` + Role []string `xml:"role"` + } `xml:"roles"` + Settings struct { + Text string `xml:",chardata"` + WrapUpOnIncoming string `xml:"wrapUpOnIncoming"` + WrapUpOnOutgoing string `xml:"wrapUpOnOutgoing"` + DeviceSelection string `xml:"deviceSelection"` + } `xml:"settings"` + State string `xml:"state"` + StateChangeTime string `xml:"stateChangeTime"` + PendingState string `xml:"pendingState"` + TeamId string `xml:"teamId"` + TeamName string `xml:"teamName"` + SkillTargetId string `xml:"skillTargetId"` + URI string `xml:"uri"` + Teams struct { + Text string `xml:",chardata"` + Team []struct { + Text string `xml:",chardata"` + Id int `xml:"id"` + Name string `xml:"name"` + URI string `xml:"uri"` + } `xml:"Team"` + } `xml:"teams"` + MobileAgent struct { + Text string `xml:",chardata"` + Mode string `xml:"mode"` + DialNumber string `xml:"dialNumber"` + } `xml:"mobileAgent"` + ActiveDeviceId string `xml:"activeDeviceId"` + Devices struct { + Text string `xml:",chardata"` + Device []struct { + Text string `xml:",chardata"` + DeviceId string `xml:"deviceId"` + DeviceType string `xml:"deviceType"` + DeviceTypeName string `xml:"deviceTypeName"` + } `xml:"device"` + } `xml:"devices"` +} + +func newUserDetailResponse(data string) (*UserDetailResponse, error) { + var u UserDetailResponse + buffer := []byte(data) + err := xml.Unmarshal(buffer, &u) + if err != nil { + return nil, err + } + return &u, nil +} + +// ToString Returns the basic data from the status response as a printable string +func (u *UserDetailResponse) ToString() string { + s := fmt.Sprintf("Dialogs: %s\r\n", u.Dialogs) + s = fmt.Sprintf("%sExtension: %s\r\n", s, u.Extension) + s = fmt.Sprintf("%sFirst Name: %s\r\n", s, u.FirstName) + s = fmt.Sprintf("%sLast Name: %s\r\n", s, u.LastName) + s = fmt.Sprintf("%sLogin ID: %s\r\n", s, u.LoginId) + s = fmt.Sprintf("%sLogin name: %s\r\n", s, u.LoginName) + s = fmt.Sprintf("%sState: %s\r\n", s, u.State) + s = fmt.Sprintf("%sTeam name: %s\r\n", s, u.TeamName) + s = fmt.Sprintf("%sTeam ID: %s\r\n", s, u.TeamId) + s = fmt.Sprintf("%sPending state: %s\r\n", s, u.PendingState) + s = fmt.Sprintf("%sReason code ID: %s\r\n", s, u.ReasonCodeId) + s = fmt.Sprintf("%sRole: %s\n\r", s, u.getRoles()) + s = fmt.Sprintf("%sTeams: %s\n\r", s, u.getTeams()) + + return s +} + +// ToStingSimple Returns agent name with current state +func (u *UserDetailResponse) ToStingSimple() string { + return fmt.Sprintf("Agent: %-30s State: %-15s Pending state %s", u.LoginName, u.State, u.PendingState) +} + +func (u *UserDetailResponse) getRoles() string { + a := "" + sep := "" + for _, role := range u.Roles.Role { + a = fmt.Sprintf("%s%s%s", a, sep, role) + sep = ", " + } + return a +} + +func (u *UserDetailResponse) getTeams() string { + a := "" + sep := "" + for _, team := range u.Teams.Team { + a = fmt.Sprintf("%s%s%s", a, sep, team.Name) + sep = ", " + } + return a +} + +// IsLogIn Is agent logged in +func (u *UserDetailResponse) IsLogIn() bool { + return u.State != AgentStateLogout +} + +// IsPossibleToLogout Is agent ready for logout +func (u *UserDetailResponse) IsPossibleToLogout() bool { + return u.State == AgentStateNotReady +} diff --git a/finesse-agent-status.go b/finesse-agent-status.go new file mode 100644 index 0000000..50bd277 --- /dev/null +++ b/finesse-agent-status.go @@ -0,0 +1,42 @@ +package finesse_api + +const ( + AgentStateLogin string = "LOGIN" + AgentStateLogout = "LOGOUT" + AgentStateReady = "READY" + AgentStateNotReady = "NOT_READY" + AgentStateAvailable = "AVAILABLE" + AgentStateTalking = "TALKING" + AgentStateWorkNotReady = "WORK_NOT_READY" + AgentStateWorkReady = "WORK_READY" + AgentStateReserved = "RESERVED" + AgentStateUnknown = "UNKNOWN" + AgentStateHold = "HOLD" + AgentStateActive = "ACTIVE" + AgentStatePaused = "PAUSED" + AgentStateInterrupted = "INTERRUPTED" + AgentStateNotActive = "NOT_ACTIVE" +) + +// AgentStates All valid agent states +var AgentStates = []string{AgentStateLogin, AgentStateLogout, AgentStateReady, AgentStateNotReady, AgentStateAvailable, + AgentStateTalking, AgentStateWorkNotReady, AgentStateWorkReady, AgentStateReserved, AgentStateUnknown, AgentStateHold, + AgentStateActive, AgentStatePaused, AgentStateInterrupted, AgentStateNotActive} + +// AgentReadyStates States when agent is ready for work or work +var AgentReadyStates = map[string]string{AgentStateReady: AgentStateReady, AgentStateAvailable: AgentStateAvailable, + AgentStateTalking: AgentStateTalking, AgentStateWorkReady: AgentStateWorkReady, AgentStateReserved: AgentStateReserved, + AgentStateHold: AgentStateHold, AgentStateActive: AgentStateActive} + +// AgentLoginStates States when agent is logged in to the system +var AgentLoginStates = map[string]string{AgentStateLogin: AgentStateLogin, AgentStateReady: AgentStateReady, + AgentStateNotReady: AgentStateNotReady, AgentStateAvailable: AgentStateAvailable, AgentStateTalking: AgentStateTalking, + AgentStateWorkNotReady: AgentStateWorkNotReady, AgentStateWorkReady: AgentStateWorkReady, AgentStateReserved: AgentStateReserved, + AgentStateHold: AgentStateHold, AgentStateActive: AgentStateActive, AgentStatePaused: AgentStatePaused, + AgentStateInterrupted: AgentStateInterrupted, AgentStateNotActive: AgentStateNotActive} + +// AgentNotReadyStates States when agent is not-ready +var AgentNotReadyStates = map[string]string{AgentStateNotReady: AgentStateNotReady, AgentStateWorkNotReady: AgentStateWorkNotReady} + +// AgentLogoutState States when agent is not logged in to the system +var AgentLogoutState = map[string]string{AgentStateLogout: AgentStateLogout, AgentStateUnknown: AgentStateUnknown} diff --git a/finesse-agent.go b/finesse-agent.go new file mode 100644 index 0000000..334fbba --- /dev/null +++ b/finesse-agent.go @@ -0,0 +1,41 @@ +package finesse_api + +import ( + "encoding/xml" +) + +// FinesseAgent Login information of agent +type FinesseAgent struct { + LoginName string // login name + LoginId string // login ID + Password string // password + Line string // phone line +} + +// NewAgent Creating agent login data +func NewAgent(id string, name string, pwd string, line string) *FinesseAgent { + return &FinesseAgent{ + LoginId: id, + LoginName: name, + Password: pwd, + Line: line, + } +} + +// NewAgentName Creating agent login data without "agent Id" necessary for requests +func NewAgentName(name string, pwd string, line string) *FinesseAgent { + return NewAgent("", name, pwd, line) +} + +func (a *FinesseAgent) loginRequest() *userLoginRequest { + return &userLoginRequest{ + XMLName: xml.Name{}, + Text: "", + State: AgentStateLogin, + Extension: a.Line, + } +} + +func (a *FinesseAgent) setAgentId(newId string) { + a.LoginId = newId +} diff --git a/finesse-data-struct.go b/finesse-data-struct.go new file mode 100644 index 0000000..989e824 --- /dev/null +++ b/finesse-data-struct.go @@ -0,0 +1,57 @@ +package finesse_api + +import ( + "encoding/xml" +) + +// userLoginRequest structure for agent login +type userLoginRequest struct { + XMLName xml.Name `xml:"User"` + Text string `xml:",chardata"` + State string `xml:"state"` + Extension string `xml:"extension"` +} + +// userStateRequest structure for agent logout +type userStateRequest struct { + XMLName xml.Name `xml:"User"` + Text string `xml:",chardata"` + State string `xml:"state"` +} + +// userNotReadyWithReasonRequest structure for agent logout +type userNotReadyWithReasonRequest struct { + XMLName xml.Name `xml:"User"` + Text string `xml:",chardata"` + State string `xml:"state"` + ReasonCodeId int `xml:"reasonCodeId"` +} + +type userRequest interface { + getUserRequest() ([]byte, error) +} + +func (u *userLoginRequest) getUserRequest() ([]byte, error) { + data, err := xml.Marshal(u) + if err != nil { + return nil, err + } + return data, nil +} + +func (u *userStateRequest) getUserRequest() ([]byte, error) { + data, err := xml.Marshal(u) + if err != nil { + return nil, err + } + return data, nil +} + +func (u *userNotReadyWithReasonRequest) getUserRequest() ([]byte, error) { + data, err := xml.Marshal(u) + if err != nil { + return nil, err + } + return data, nil +} + diff --git a/finesse-log-fileds.go b/finesse-log-fileds.go new file mode 100644 index 0000000..78d8e0b --- /dev/null +++ b/finesse-log-fileds.go @@ -0,0 +1,13 @@ +package finesse_api + +const ( + logProc = "proc" + logId = "requestId" + logServer = "server" + logAgent = "agentName" + logHttpStatus = "httpStatus" + logNewState = "agentState" + runTimeDuration = "duration" + logBody = "body" + logRequestType = "requestType" +) diff --git a/finesse-notify.go b/finesse-notify.go new file mode 100644 index 0000000..68122f9 --- /dev/null +++ b/finesse-notify.go @@ -0,0 +1,75 @@ +//go:build ignore + +package finesse_api + +// +// this package is prepared for next use when identify how really work with Notification Service XMPP +// +import ( + "fmt" + log "github.com/sirupsen/logrus" + "gosrc.io/xmpp" + "gosrc.io/xmpp/stanza" + "os" + "sync" + "time" +) + +// FinesseAgentNotify Structure for agent state with wait for XMPP response +type FinesseAgentNotify struct { + *FinesseAgent + XmppClient *xmpp.Client + UserDetail *UserDetailResponse + Mutex sync.Mutex +} + +// NewAgentWithNotification Create necessary agent notify structure +func NewAgentWithNotification(id string, name string, pwd string, line string) *FinesseAgentNotify { + a := NewAgent(id, name, pwd, line) + return &FinesseAgentNotify{ + FinesseAgent: a, + XmppClient: nil, + UserDetail: nil, + Mutex: sync.Mutex{}, + } +} + +// StartNotification Test procedure for use XMPP +func (a *FinesseAgentNotify) StartNotification(finesseServer string) error { + if a.XmppClient != nil { + log.WithFields(log.Fields{logProc: "StartNotification", logAgent: a.LoginName}).Trace("start finesse_notifier - XMPP notifier is ready") + } + log.WithFields(log.Fields{logProc: "StartNotification", logAgent: a.LoginName}).Trace("start finesse_notifier") + config := xmpp.Config{ + TransportConfiguration: xmpp.TransportConfiguration{ + Address: fmt.Sprintf("%s:5223", finesseServer), + }, + Jid: a.LoginName, + Credential: xmpp.Password(a.Password), + StreamLogger: os.Stdout, + Insecure: true, + } + + router := xmpp.NewRouter() + router.HandleFunc("iq", func(s xmpp.Sender, p stanza.Packet) { + log.WithFields(log.Fields{logProc: "StartNotification", logAgent: a.LoginName}).Trace("handle func iq") + }) + router.HandleFunc("message", func(s xmpp.Sender, p stanza.Packet) { + log.WithFields(log.Fields{logProc: "StartNotification", logAgent: a.LoginName}).Trace("handle func message") + }) + + client, err := xmpp.NewClient(&config, router, func(err error) { + log.WithFields(log.Fields{logProc: "StartNotification", logAgent: a.LoginName}).Errorf("client error handler messages - %s", err) + }) + log.WithFields(log.Fields{logProc: "StartNotification", logAgent: a.LoginName}).Trace("prepare XMPP client") + + err = client.Connect() + if err != nil { + log.WithFields(log.Fields{logProc: "StartNotification", logAgent: a.LoginName}).Errorf("XMPP client not connect %s", err) + return err + } + // wait until response + time.Sleep(SleepDurationMilliSecond) + a.XmppClient = client + return nil +} diff --git a/finesse-request.go b/finesse-request.go new file mode 100644 index 0000000..d652077 --- /dev/null +++ b/finesse-request.go @@ -0,0 +1,71 @@ +package finesse_api + +import ( + "bytes" + "crypto/tls" + "fmt" + log "github.com/sirupsen/logrus" + "net/http" + "time" +) + +// FinesseRequest Structure for one API request +type FinesseRequest struct { + id string + client *http.Client + server *FinesseServer + request *http.Request +} + +func newFinesseRequest(server *FinesseServer) *FinesseRequest { + r := FinesseRequest{ + id: randomString(), + client: server.client, + server: server, + request: nil, + } + log.WithFields(log.Fields{logProc: "NewRequest", logId: r.id, logServer: r.server.name}).Tracef("prepare new request for server [%s]", server.name) + return &r +} + +func (f *FinesseRequest) setHeader(agent *FinesseAgent) { + if f.request.Method != "GET" { + f.request.Header.Set("Content-Type", "application/xml") + } + f.request.Header.Set("User-Agent", "Finesse/1.0") + f.request.Header.Set("Accept", "*/*") + f.request.Header.Set("Cache-Control", "no-cache") + //f.request.Header.Set("Pragma", "no-cache") + f.request.Header.Set("RequestId", f.id) + f.request.Host = f.server.name + f.request.SetBasicAuth(agent.LoginName, agent.Password) +} + +func (f *FinesseRequest) httpClient() { + if f.client == nil { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + f.client = &http.Client{Timeout: time.Duration(f.server.timeOut) * time.Second, Transport: tr} + log.WithFields(log.Fields{logProc: "httpClient", logId: f.id}).Debugf("prepare HTTP client for server [%s] in request", f.server.name) + } +} + +func (f *FinesseRequest) doRequest(method string, url string, agent *FinesseAgent, data []byte) *FinesseResponse { + log.WithFields(log.Fields{logProc: "doRequest", logId: f.id, logRequestType: method, logBody: string(data)}).Tracef("start process request [%s %s]", method, url) + request, err := http.NewRequest(method, url, bytes.NewBuffer(data)) + if err != nil { + log.WithFields(log.Fields{logProc: "doRequest", logId: f.id}).Errorf( + "problem create [%s %s] request for [%s] agent with error %s", method, url, agent.LoginName, err) + } + f.request = request + f.setHeader(agent) + f.httpClient() + resp, err := f.client.Do(f.request) + if err != nil { + r := fmt.Sprintf("problem request [%s %s]", f.request.Method, f.request.URL) + log.WithFields(log.Fields{logProc: "doRequest", logId: f.id}).Error(r) + return f.NewFinesseResponse(resp, err, r) + } + return f.NewFinesseResponse(resp, nil, "") +} diff --git a/finesse-response.go b/finesse-response.go new file mode 100644 index 0000000..f6520c9 --- /dev/null +++ b/finesse-response.go @@ -0,0 +1,87 @@ +package finesse_api + +import ( + "fmt" + log "github.com/sirupsen/logrus" + "io" + "net/http" +) + +// FinesseResponse Structure for one API response +type FinesseResponse struct { + id string + response *http.Response + err error + lastMessage string + body string + statusCode int + statusMessage string +} + +// NewFinesseResponse Create new response structure +func (f *FinesseRequest) NewFinesseResponse(response *http.Response, e error, message string) *FinesseResponse { + r := new(FinesseResponse) + r.id = f.id + r.response = response + r.err = e + r.lastMessage = message + if response != nil { + r.statusCode = response.StatusCode + r.statusMessage = response.Status + } else { + r.statusCode = 500 + r.statusMessage = "500 Problem Connect to server" + } + log.WithFields(log.Fields{logProc: "NewFinesseResponse", logId: r.id}).Tracef("response with status [%s]", r.statusMessage) + return r +} + +func (f *FinesseResponse) close() { + if f.response != nil { + if f.response.Body != nil { + _ = f.response.Body.Close() + } + f.response = nil + } +} + +func (f *FinesseResponse) responseError() (string, error) { + if f.statusCode >= 200 && f.statusCode <= 299 { + return "", nil + } + if len(f.lastMessage) > 0 { + return f.lastMessage, fmt.Errorf(f.lastMessage) + } + return f.lastMessage, fmt.Errorf("reponse with error [%s]", f.statusMessage) +} + +// GetResponseBody Read API response body +func (f *FinesseResponse) GetResponseBody() string { + if f.response == nil { + return f.body + } + err := f.responseReturnData() + if err != nil { + f.err = err + } + return f.body +} + +func (f *FinesseResponse) responseReturnData() error { + log.WithFields(log.Fields{logProc: "responseReturnData", logId: f.id, logHttpStatus: f.response.Status}). + Debugf("response status is [%s]", f.response.Status) + bodies, err := io.ReadAll(f.response.Body) + _ = f.response.Body.Close() + f.body = "" + + if err != nil { + log.WithFields(log.Fields{logProc: "responseReturnData", logId: f.id}).Errorf("problem get body from response [%s]", err) + return err + } + f.body = string(bodies) + log.WithFields(log.Fields{logProc: "responseReturnData", logId: f.id}).Tracef("body read success [%s %s]", f.response.Request.Method, f.response.Request.URL) + if f.statusCode > 299 { + return fmt.Errorf(f.statusMessage) + } + return nil +} diff --git a/finesse-server-parallel.go b/finesse-server-parallel.go new file mode 100644 index 0000000..b77f382 --- /dev/null +++ b/finesse-server-parallel.go @@ -0,0 +1,368 @@ +package finesse_api + +import ( + "fmt" + log "github.com/sirupsen/logrus" + "sync" + "time" +) + +// ResponseChan Structure for collect responses in parallel functions +type ResponseChan struct { + agentResponse *UserDetailResponse + requestError error +} + +// ToString Return formatted string with responses for agents. +// The format is Name, Current state, Pending State or Name, Current state, Error +func (r *ResponseChan) ToString() string { + if r.requestError != nil { + return fmt.Sprintf("Agent: %-30s State: %-15s Error: %s", r.agentResponse.LoginName, r.agentResponse.State, r.requestError) + } + return fmt.Sprintf("Agent: %-30s State: %-15s Pending state: %s", r.agentResponse.LoginName, r.agentResponse.State, r.agentResponse.PendingState) +} + +// LoginAllParallel Logging in all agents registered in the server. +// Every agent has own goroutine +func (f *FinesseServer) LoginAllParallel() error { + fn := f.loginAgentRoutine + return f.parallelProcessing(fn, "LoginAllParallel", "login") +} + +// ReadyAllAgentsParallel Setting all agents registered in the server to state ready. +// Every agent has own goroutine +func (f *FinesseServer) ReadyAllAgentsParallel() error { + fn := f.readyAgentRoutine + return f.parallelProcessing(fn, "ReadyAllAgentsParallel", "ready") +} + +func (f *FinesseServer) loginAgentRoutine(agentName string, c chan bool, wg *sync.WaitGroup) { + log.WithFields(log.Fields{logProc: "loginAgentRoutine", logAgent: agentName}).Tracef("try login agent %s in separate thread", agentName) + c <- f.LoginAgent(agentName) + time.Sleep(SleepDurationMilliSecond) + wg.Done() +} + +func (f *FinesseServer) readyAgentRoutine(agentName string, c chan bool, wg *sync.WaitGroup) { + c <- f.ReadyAgent(agentName) + time.Sleep(SleepDurationMilliSecond) + wg.Done() +} + +func (f *FinesseServer) parallelProcessing(fn func(agentName string, c chan bool, wg *sync.WaitGroup), mainName string, operation string) error { + ts := time.Now() + + defer logEndTimeProcess(ts, log.Fields{logProc: mainName, logServer: f.name}) + c := make(chan bool, len(f.agents)) + var wg sync.WaitGroup + log.WithFields(log.Fields{logProc: mainName, logServer: f.name}).Debugf("start coroutines for %s", operation) + for _, agent := range f.agents { + wg.Add(1) + go fn(agent.LoginName, c, &wg) + } + log.WithFields(log.Fields{logProc: mainName, logServer: f.name}).Debugf("wait for all coroutines %s ends", operation) + wg.Wait() + log.WithFields(log.Fields{logProc: mainName, logServer: f.name}).Debugf("all coroutines %s ends", operation) + success := 0 + for range f.agents { + if <-c { + success++ + } + } + close(c) + + if len(f.agents) > success { + log.WithFields(log.Fields{logProc: mainName, logServer: f.name}).Errorf("from %d agents success requests is %d", len(f.agents), success) + return fmt.Errorf("%d problem agents in %s", len(f.agents)-success, operation) + } + log.WithFields(log.Fields{logProc: mainName, logServer: f.name}).Infof("from %d agents success requests is %d", len(f.agents), success) + return nil +} + +// GetStateAgentsParallel Getting current statuses for all agents registered in the server. +// Every agent has own goroutine +func (f *FinesseServer) GetStateAgentsParallel() ([]*UserDetailResponse, error) { + fn := f.stateAgentRoutineWithStatus + return f.parallelProcessingWithState(fn, false, "GetStateAgentsParallel", "status") +} + +// LoginAgentsParallelWithStatus Logging in and getting current statuses for all agents registered in the server. +// Every agent has own goroutine +func (f *FinesseServer) LoginAgentsParallelWithStatus() ([]*UserDetailResponse, error) { + fn := f.loginAgentRoutineWithStatus + return f.parallelProcessingWithState(fn, false, "LoginAgentsParallelWithStatus", "login") +} + +// ReadyAgentsParallelWithStatus Setting ready and getting current statuses for all agents registered in the server. +// The optional force parameter defines if the program tries logging in not logged-in agents, before setts it to a ready state +// Every agent has own goroutine +func (f *FinesseServer) ReadyAgentsParallelWithStatus(force ...bool) ([]*UserDetailResponse, error) { + fo := false + if len(force) > 0 { + fo = force[0] + } + fn := f.readyAgentRoutineWithStatus + return f.parallelProcessingWithState(fn, fo, "ReadyAllAgentsParallelWithStatus", "ready") +} + +// NotReadyAgentsParallelWithStatus Setting not-ready and getting current statuses for all agents registered in the server. +// The optional force parameter defines if the program tries logging in not logged-in agents, before setts it to a not-ready state +// Every agent has own goroutine +func (f *FinesseServer) NotReadyAgentsParallelWithStatus(force ...bool) ([]*UserDetailResponse, error) { + fo := false + if len(force) > 0 { + fo = force[0] + } + fn := f.notReadyAgentRoutineWithStatus + return f.parallelProcessingWithState(fn, fo, "NotReadyAgentsParallelWithStatus", "not-ready") +} + +// LogoutAgentsParallelWithStatus Logging out and getting current statuses for all agents registered in the server. +// The optional force parameter defines if the program tries sets a not-ready state before logging out +// Every agent has own goroutine +func (f *FinesseServer) LogoutAgentsParallelWithStatus(force ...bool) ([]*UserDetailResponse, error) { + fo := false + if len(force) > 0 { + fo = force[0] + } + fn := f.logoutAgentRoutineWithStatus + return f.parallelProcessingWithState(fn, fo, "LogoutAgentsParallelWithStatus", "logout") +} + +func (f *FinesseServer) stateAgentRoutineWithStatus(agentName string, _ bool, c chan ResponseChan, wg *sync.WaitGroup) { + status, err := f.GetAgentStatusDetail(agentName) + defer wg.Done() + if err != nil { + c <- ResponseChan{ + agentResponse: status, + requestError: err, + } + } else { + c <- ResponseChan{ + agentResponse: status, + requestError: nil, + } + } +} + +func (f *FinesseServer) loginAgentRoutineWithStatus(agentName string, _ bool, c chan ResponseChan, wg *sync.WaitGroup) { + status, err := f.GetAgentStatusDetail(agentName) + defer wg.Done() + if err != nil { + c <- ResponseChan{ + agentResponse: nil, + requestError: err, + } + return + } + if status.State == AgentStateUnknown { + c <- ResponseChan{ + agentResponse: status, + requestError: fmt.Errorf("agent [%s] is not in state for set to ready, actual state [%s]", agentName, status.State), + } + return + } + _, exists := AgentLoginStates[status.State] + if exists { + c <- ResponseChan{ + agentResponse: status, + requestError: nil, + } + return + } + _ = f.LoginAgent(agentName) + time.Sleep(SleepDurationMilliSecond) + status, err = f.GetAgentStatusDetail(agentName) + _, exists = AgentLoginStates[status.State] + if exists { + c <- ResponseChan{ + agentResponse: status, + requestError: nil, + } + return + } + c <- ResponseChan{ + agentResponse: status, + requestError: fmt.Errorf("problem login agent [%s] to system", agentName), + } +} + +func (f *FinesseServer) readyAgentRoutineWithStatus(agentName string, force bool, c chan ResponseChan, wg *sync.WaitGroup) { + status, err := f.GetAgentStatusDetail(agentName) + defer wg.Done() + if err != nil { + c <- ResponseChan{ + agentResponse: nil, + requestError: err, + } + return + } + if !force && (status.State == AgentStateUnknown || status.State == AgentStateLogout) { + c <- ResponseChan{ + agentResponse: status, + requestError: fmt.Errorf("agent [%s] is not in state for set to ready, actual state [%s]", agentName, status.State), + } + return + } + if force && status.State == AgentStateLogout { + _ = f.LoginAgent(agentName) + time.Sleep(SleepDurationMilliSecond) + } + ready := f.ReadyAgent(agentName) + time.Sleep(SleepDurationMilliSecond) + status, err = f.GetAgentStatusDetail(agentName) + _, exists := AgentReadyStates[status.State] + if ready && exists { + c <- ResponseChan{ + agentResponse: status, + requestError: nil, + } + return + } + c <- ResponseChan{ + agentResponse: status, + requestError: fmt.Errorf("problem set agent [%s] to ready state", agentName), + } +} + +func (f *FinesseServer) notReadyAgentRoutineWithStatus(agentName string, force bool, c chan ResponseChan, wg *sync.WaitGroup) { + status, err := f.GetAgentStatusDetail(agentName) + defer wg.Done() + if err != nil { + c <- ResponseChan{ + agentResponse: nil, + requestError: err, + } + return + } + if !force && status.State == AgentStateUnknown { + c <- ResponseChan{ + agentResponse: status, + requestError: fmt.Errorf("agent [%s] is not in state for set to ready, actual state [%s]", agentName, status.State), + } + return + } + if status.State == AgentStateLogout { + _ = f.LoginAgent(agentName) + } else { + _, exists := AgentReadyStates[status.State] + if force && exists { + _ = f.NotReadyAgent(agentName) + } + } + time.Sleep(SleepDurationMilliSecond) + status, err = f.GetAgentStatusDetail(agentName) + + _, exists := AgentNotReadyStates[status.State] + _, exists1 := AgentNotReadyStates[status.PendingState] + if exists || exists1 { + c <- ResponseChan{ + agentResponse: status, + requestError: nil, + } + return + } + c <- ResponseChan{ + agentResponse: status, + requestError: fmt.Errorf("problem set agent [%s] to not-ready state", agentName), + } +} + +func (f *FinesseServer) logoutAgentRoutineWithStatus(agentName string, force bool, c chan ResponseChan, wg *sync.WaitGroup) { + status, err := f.GetAgentStatusDetail(agentName) + defer wg.Done() + if err != nil { + c <- ResponseChan{ + agentResponse: nil, + requestError: err, + } + return + } + if !force && status.State == AgentStateUnknown { + c <- ResponseChan{ + agentResponse: status, + requestError: fmt.Errorf("agent [%s] is not in state for set to ready, actual state [%s]", agentName, status.State), + } + return + } + if status.State == AgentStateLogout { + c <- ResponseChan{ + agentResponse: status, + requestError: nil, + } + return + } + _, exists := AgentReadyStates[status.State] + if force && exists { + nr := f.NotReadyAgent(agentName) + if !nr { + c <- ResponseChan{ + agentResponse: status, + requestError: fmt.Errorf("agent [%s] not possible switch to not ready state, actual state [%s]", agentName, status.State), + } + return + } + time.Sleep(SleepDurationMilliSecond) + exists = false + } + if exists { + c <- ResponseChan{ + agentResponse: status, + requestError: fmt.Errorf("agent [%s] not possible switch to not ready state, actual state [%s]", agentName, status.State), + } + return + } + _ = f.LogoutAgent(agentName) + time.Sleep(SleepDurationMilliSecond) + status, err = f.GetAgentStatusDetail(agentName) + + if status.State == AgentStateLogout { + c <- ResponseChan{ + agentResponse: status, + requestError: nil, + } + return + } + c <- ResponseChan{ + agentResponse: status, + requestError: fmt.Errorf("problem logout agent [%s]", agentName), + } +} + +func (f *FinesseServer) parallelProcessingWithState(fn func(agentName string, force bool, c chan ResponseChan, wg *sync.WaitGroup), force bool, mainName string, operation string) ([]*UserDetailResponse, error) { + ts := time.Now() + + var status []*UserDetailResponse + defer logEndTimeProcess(ts, log.Fields{logProc: mainName, logServer: f.name}) + c := make(chan ResponseChan, len(f.agents)) + var wg sync.WaitGroup + log.WithFields(log.Fields{logProc: mainName, logServer: f.name}).Debugf("start coroutines for %s", operation) + for _, agent := range f.agents { + wg.Add(1) + go fn(agent.LoginName, force, c, &wg) + } + log.WithFields(log.Fields{logProc: mainName, logServer: f.name}).Debugf("wait for all coroutines %s ends", operation) + wg.Wait() + log.WithFields(log.Fields{logProc: mainName, logServer: f.name}).Debugf("all coroutines %s ends", operation) + success := 0 + for range f.agents { + r := <-c + status = append(status, r.agentResponse) + if r.requestError == nil { + success++ + } + } + close(c) + + if len(f.agents) > success { + log.WithFields(log.Fields{logProc: mainName, logServer: f.name}).Errorf("from %d agents success requests is %d", len(f.agents), success) + return status, fmt.Errorf("%d problem agents in %s", len(f.agents)-success, operation) + } + log.WithFields(log.Fields{logProc: mainName, logServer: f.name}).Infof("from %d agents success requests is %d", len(f.agents), success) + return status, nil +} + +func logEndTimeProcess(ts time.Time, fields log.Fields) { + end := time.Now() + log.WithFields(fields).WithField(runTimeDuration, end.Sub(ts).String()).Tracef("processing duration: %s", end.Sub(ts)) +} diff --git a/finesse-server.go b/finesse-server.go new file mode 100644 index 0000000..3febbef --- /dev/null +++ b/finesse-server.go @@ -0,0 +1,260 @@ +package finesse_api + +import ( + "fmt" + log "github.com/sirupsen/logrus" + "net/http" + "path" + "time" +) + +// SleepDurationMilliSecond Define delay between for one agent in milliseconds +// Default value is 1500 ms +var SleepDurationMilliSecond = 1500 * time.Millisecond + +// FinesseServer Structure for finesse server data +type FinesseServer struct { + id string + name string + port int + timeOut int + agents map[string]*FinesseAgent + client *http.Client +} + +// NewFinesseServer Creating new Finesse server structure +func NewFinesseServer(server string, port int, time ...int) *FinesseServer { + id := randomString() + timeOut := 30 + if len(time) > 0 { + timeOut = time[0] + } + return &FinesseServer{ + id: id, + name: server, + agents: make(map[string]*FinesseAgent), + port: port, + timeOut: timeOut, + client: nil, + } +} + +// AddAgent Adding new agent as part of finesse server +func (f *FinesseServer) AddAgent(agent *FinesseAgent) { + f.agents[agent.LoginName] = agent +} + +// LoginAgent Logging in agent with the agentName. +// Agent name must be registered on this Finesse server with AddAgent function +func (f *FinesseServer) LoginAgent(agentName string) bool { + request := newFinesseRequest(f) + agent, err := f.getAgentFromName(agentName) + if err != nil { + log.WithFields(log.Fields{logProc: "loginAgent", logId: request.id, logAgent: agentName}). + Error(err) + return false + } + log.WithFields(log.Fields{logProc: "loginAgent", logId: request.id, logAgent: agentName}).Tracef("success get agentId from name") + if !f.getAgentId(agent) { + return false + } + + requestBody, err := agent.loginRequest().getUserRequest() + if err != nil { + log.WithFields(log.Fields{logProc: "loginAgent", logId: request.id, logAgent: agent.LoginName}). + Errorf("not login agent %s on line %s. Prolem is %s", agent.LoginName, agent.Line, err) + return false + } + response := request.doRequest("PUT", f.urlString(request.id, "User", agent.LoginId), agent, requestBody) + defer response.close() + msg, err := response.responseError() + if err != nil { + log.WithFields(log.Fields{logProc: "loginAgent", logId: response.id, logAgent: agent.LoginName}).Error(msg) + return false + } + log.WithFields(log.Fields{logProc: "loginAgent", logId: response.id, logAgent: agent.LoginName}). + Tracef("agnet [%s] success login request", agent.LoginName) + time.Sleep(SleepDurationMilliSecond) + detail, err := f.stateAfterChange(agent.LoginName) + if err != nil { + return false + } + if !detail.IsLogIn() { + log.WithFields(log.Fields{logProc: "loginAgent", logId: response.id, logAgent: agent.LoginName}). + Errorf("agent %s not logged into server %s", agent.LoginName, f.name) + return false + } + log.WithFields(log.Fields{logProc: "loginAgent", logId: response.id, logAgent: agent.LoginName}).Debugf("agnet [%s] login success", agent.LoginName) + return true +} + +// LogoutAgent Logging out agent with the agentName. +// Agent name must be registered on this Finesse server with AddAgent function +func (f *FinesseServer) LogoutAgent(agentName string) bool { + return f.doStateChange(agentName, "LOGOUT") +} + +// ReadyAgent Setting agent with the agentName to ready state. +// Agent name must be registered on this Finesse server with AddAgent function +func (f *FinesseServer) ReadyAgent(agentName string) bool { + return f.doStateChange(agentName, "READY") +} + +// NotReadyAgent Setting agent with the agentName to not-ready state. +// Agent name must be registered on this Finesse server with AddAgent function +func (f *FinesseServer) NotReadyAgent(agentName string) bool { + return f.doStateChange(agentName, "NOT_READY") +} + +// NotReadyWithReasonAgent Setting agent with the agentName to not-ready state with reason. +// Agent name must be registered on this Finesse server with AddAgent function +func (f *FinesseServer) NotReadyWithReasonAgent(agentName string, reason int) bool { + return f.doStateChange(agentName, "NOT_READY", reason) +} + +// GetAgentStatusDetail Getting information about agent with the agentName. +// Agent name must be registered on this Finesse server with AddAgent function +func (f *FinesseServer) GetAgentStatusDetail(agentName string) (*UserDetailResponse, error) { + return f.stateAfterChange(agentName) +} + +func (f *FinesseServer) getAgentId(agent *FinesseAgent) bool { + if len(agent.LoginId) <= 0 { + request := newFinesseRequest(f) + response := request.doRequest("GET", f.urlString(request.id, "User", agent.LoginName), agent, nil) + defer response.close() + time.Sleep(SleepDurationMilliSecond) + msg, err := response.responseError() + if err != nil { + log.WithFields(log.Fields{logProc: "loginAgent", logId: response.id, logAgent: agent.LoginName}).Error(msg) + return false + } + log.WithFields(log.Fields{logProc: "loginAgent", logId: response.id, logAgent: agent.LoginName}).Trace("success get data for agentId from name") + data, err := newUserDetailResponse(response.GetResponseBody()) + if err != nil { + log.WithFields(log.Fields{logProc: "loginAgent", logId: response.id, logAgent: agent.LoginName}).Error(err) + return false + } + if len(data.LoginId) <= 0 { + log.WithFields(log.Fields{logProc: "loginAgent", logId: response.id, logAgent: agent.LoginName}).Errorf("problem collect agentId from request") + return false + } + agent.setAgentId(data.LoginId) + log.WithFields(log.Fields{logProc: "loginAgent", logId: response.id, logAgent: agent.LoginName}).Tracef("success get agentId from name %s", data.LoginId) + } + return true +} + +func (f *FinesseServer) stateAfterChange(agentName string) (*UserDetailResponse, error) { + request := newFinesseRequest(f) + agent, err := f.getAgentFromName(agentName) + if err != nil { + log.WithFields(log.Fields{logProc: "GetAgentStatusDetail", logId: request.id, logAgent: agentName}). + Error(err) + return nil, err + } + if !f.getAgentId(agent) { + return nil, err + } + + response := request.doRequest("GET", f.urlString(request.id, "User", agent.LoginId), agent, nil) + msg, err := response.responseError() + if err != nil { + response.close() + log.WithFields(log.Fields{logProc: "getAgentDetail", logId: response.id, logAgent: agent.LoginName}).Error(msg) + return nil, err + } + data, err := newUserDetailResponse(response.GetResponseBody()) + if err != nil { + log.WithFields(log.Fields{logProc: "getAgentDetail", logId: response.id, logAgent: agent.LoginName}).Error(err) + return nil, err + } + return data, nil +} + +func (f *FinesseServer) getAgentFromName(agentName string) (*FinesseAgent, error) { + agent, found := f.agents[agentName] + if !found { + return nil, fmt.Errorf("agent [%s] not defined for server [%s]", agentName, f.name) + } + return agent, nil +} + +func (f *FinesseServer) GetAgentsList() map[string]*FinesseAgent { + return f.agents +} + +func (f *FinesseServer) doStateChange(agentName string, requestState string, reason ...int) bool { + request := newFinesseRequest(f) + agent, err := f.getAgentFromName(agentName) + if err != nil { + log.WithFields(log.Fields{logProc: "doStateChange", logId: request.id, logAgent: agentName}). + Error(err) + return false + } + if !f.getAgentId(agent) { + return false + } + + var requestBody []byte + if requestState == "NOT_READY" && len(reason) > 0 { + state := userNotReadyWithReasonRequest{ + State: requestState, + ReasonCodeId: reason[0], + } + requestBody, err = state.getUserRequest() + } else { + state := userStateRequest{ + State: requestState, + } + requestBody, err = state.getUserRequest() + } + + if err != nil { + log.WithFields(log.Fields{logProc: "doStateChange", logId: request.id, logAgent: agent.LoginName, logNewState: requestState}). + Errorf("change state to %s agent %s on line %s. Prolem is %s", requestState, agent.LoginName, agent.Line, err) + return false + } + response := request.doRequest("PUT", f.urlString(request.id, "User", agent.LoginId), agent, requestBody) + msg, err := response.responseError() + if err != nil { + response.close() + log.WithFields(log.Fields{logProc: "doStateChange", logId: response.id, logAgent: agent.LoginName, logNewState: requestState}).Error(msg) + return false + } + log.WithFields(log.Fields{logProc: "doStateChange", logId: response.id, logAgent: agent.LoginName}). + Tracef("agnet [%s] state change request", agent.LoginName) + time.Sleep(SleepDurationMilliSecond) + detail, err := f.stateAfterChange(agentName) + if err != nil { + return false + } + if detail.State != requestState { + log.WithFields(log.Fields{logProc: "doStateChange", logId: response.id, logAgent: agent.LoginName}). + Errorf("agent %s not change state to %s into server %s", agent.LoginName, requestState, f.name) + return false + } + log.WithFields(log.Fields{logProc: "doStateChange", logId: response.id, logAgent: agent.LoginName}).Debugf("agnet [%s] state change success", agent.LoginName) + return true +} + +func (f *FinesseServer) setHttpClient(client *http.Client) { + f.client = client +} + +func (f *FinesseServer) urlString(rId string, pathPart ...string) string { + restPath := "/finesse/api" + if len(pathPart) > 0 { + for _, s := range pathPart { + restPath = path.Join(restPath, s) + } + } + var url string + if f.port != 80 { + url = fmt.Sprintf("https://%s:%d%s", f.name, f.port, restPath) + } else { + url = fmt.Sprintf("https://%s%s", f.name, restPath) + } + log.WithFields(log.Fields{logProc: "urlString", logServer: f.name, logId: rId}).Tracef("Request URI: %s", url) + return url +} diff --git a/finesse-utils.go b/finesse-utils.go new file mode 100644 index 0000000..46ee1ae --- /dev/null +++ b/finesse-utils.go @@ -0,0 +1,54 @@ +package finesse_api + +import ( + "math/rand" + "regexp" + "strings" + "time" +) + +const ( + letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" // map for random string + letterIdxBits = 6 // 6 bits to represent a letter index + letterIdxMask = 1<= 0; { + if remain == 0 { + cache, remain = src.Int63(), letterIdxMax + } + if idx := int(cache & letterIdxMask); idx < len(letterBytes) { + sb.WriteByte(letterBytes[idx]) + i-- + } + cache >>= letterIdxBits + remain-- + } + + return sb.String() +} + +// ValidServerNameIp Validate if string is valid IPv4 address, hostname or FQDN +func ValidServerNameIp(srv string) bool { + ipAddress := regexp.MustCompile(`^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$`) + invalidAddress := regexp.MustCompile(`^((\d+)\.){3}(\d+)$`) + dnsName := regexp.MustCompile(`^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$`) + if ipAddress.MatchString(srv) { + return true + } + if invalidAddress.MatchString(srv) { + return false + } + return dnsName.MatchString(srv) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2b90e0e --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module github.com/pokornyIt/finesse-api + +go 1.19 + +require ( + github.com/sirupsen/logrus v1.9.0 + gosrc.io/xmpp v0.5.1 +) + +require ( + github.com/google/uuid v1.1.1 // indirect + golang.org/x/sys v0.3.0 // indirect + golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 // indirect + nhooyr.io/websocket v1.6.5 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ebab575 --- /dev/null +++ b/go.sum @@ -0,0 +1,119 @@ +github.com/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI= +github.com/chromedp/cdproto v0.0.0-20190614062957-d6d2f92b486d/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw= +github.com/chromedp/cdproto v0.0.0-20190621002710-8cbd498dd7a0/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw= +github.com/chromedp/cdproto v0.0.0-20190812224334-39ef923dcb8d/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0= +github.com/chromedp/cdproto v0.0.0-20190926234355-1b4886c6fad6/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0= +github.com/chromedp/chromedp v0.3.1-0.20190619195644-fd957a4d2901/go.mod h1:mJdvfrVn594N9tfiPecUidF6W5jPRKHymqHfzbobPsM= +github.com/chromedp/chromedp v0.4.0/go.mod h1:DC3QUn4mJ24dwjcaGQLoZrhm4X/uPHZ6spDbS2uFhm4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-interpreter/wagon v0.5.1-0.20190713202023-55a163980b6c/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc= +github.com/go-interpreter/wagon v0.6.0/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190908185732-236ed259b199/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc/go.mod h1:NoCfSFWosfqMqmmD7hApkirIK9ozpHjxRnRxs1l413A= +go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16/go.mod h1:iKV5yK9t+J5nG9O3uF6KYdPEz3dyfMyB15MN1rbQ8Qw= +go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190306220234-b354f8bf4d9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190618155005-516e3c20635f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gosrc.io/xmpp v0.5.1 h1:Rgrm5s2rt+npGggJH3HakQxQXR8ZZz3+QRzakRQqaq4= +gosrc.io/xmpp v0.5.1/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY= +gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY= +mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8= +nhooyr.io/websocket v1.6.5 h1:8TzpkldRfefda5JST+CnOH135bzVPz5uzfn/AF+gVKg= +nhooyr.io/websocket v1.6.5/go.mod h1:F259lAzPRAH0htX2y3ehpJe09ih1aSHN7udWki1defY=