diff --git a/README.md b/README.md
index d31a63a..5996f66 100644
--- a/README.md
+++ b/README.md
@@ -10,12 +10,56 @@ API allows actions per one agent or group of agents.
- set agent not-ready state
- logout agent
-## Limitation
-The current version does not use XMPP communication with the Finesse server.
+## Connection
+Program used connection to Finesse API and XMPP for notification.
+Utilizes ports:
+
+- 8445 - HTTPS Finesse API
+- 7443 - WSS Finesse XMPP over HTTP notification (for secure)
+- 5222 - XMPP notification (non-secure - notice below)
+
+***Notice:**
+Cisco Finesse, Release 12.5(1) onward, the 5222 port (non-secure connection) is disabled
+by default. Set the `utils finesse set_property webservices enableInsecureOpenfirePort` to true
+to enable this port. For more information, see Service Properties section in
+[Cisco Finesse Administration Guide](https://www.cisco.com/c/en/us/support/customer-collaboration/finesse/products-maintenance-guides-list.html).*
+
+### Certificate
+Program need to add the Finesse Notification certificate to their respective trust stores.
+
+**Windows systems**:
+If you can use secure XMPP you must add valid server certificate to **Trusted Root Certification Authorities**.
+- right-click on the DER file and select **Install certificate**
+- select **Current User**
+- select **Please all certificates in following store**
+- click on **Browse...**
+- select **Trusted Root Certification Authorities**
+- finish
+
+**Ubuntu**:
+If you can use secure XMPP you must add valid server certificate to **Trusted Root Certification Authorities**.
+
+```shell
+sudo apt install ca-certificates
+sudo cp finesse.pem /usr/local/share/ca-certificates/finesse.crt
+sudo update-ca-certificates
+```
+
+#### How to download the certificate:
+
+1. Sign in to the Cisco Unified Operating System Administration through the URL (https://FQDN:8443/cmplatform, where FQDN is the fully qualified domain name of the primary Finesse server and 8443 is the port number).
+2. Click Security > Certificate Management.
+3. Click Find to get the list of all the certificates.
+4. In the Certificate List screen, choose Certificate from the Find Certificate List where drop-down menu, enter tomcat in the begins with option and click Find.
+5. Click the FQDN link which appears in the Common Name column parallel to the listed tomcat certificate.
+6. In the pop-up that appears, click the option Download .PEM or .DER File to save the file on your desktop.
+
+
+System support security XMPP over HTTP (WSS).
+The current version does not support XMPP secure 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.
@@ -47,4 +91,4 @@ state := server.LoginAgent("Name2")
states, err = server.ReadyAgentsParallelWithStatus(true)
```
-Program use standard logger library "github.com/sirupsen/logrus".
+Program use logger library "github.com/sirupsen/logrus".
diff --git a/XMPP/error-invalid-line.xml b/XMPP/error-invalid-line.xml
new file mode 100644
index 0000000..6c8ac37
--- /dev/null
+++ b/XMPP/error-invalid-line.xml
@@ -0,0 +1,28 @@
+
+
+
+
+ -
+
+<Update>
+ <data>
+ <apiErrors>
+ <apiError>
+ <peripheralErrorCode>10125</peripheralErrorCode>
+ <errorType>Invalid Device</errorType>
+ <errorMessage>CF_INVALID_LOGON_DEVICE_SPECIFIED</errorMessage>
+ <peripheralErrorText>A device target with the network target ID specified cannot be found. This could indicate either an internal error or a configuration error</peripheralErrorText>
+ <peripheralErrorMsg>PERERR_TELDRIVE_NODEVICETARGETFORNETTARGETID</peripheralErrorMsg>
+ <errorData>260</errorData>
+ </apiError>
+ </apiErrors>
+ </data>
+ <requestId></requestId>
+ <source>/finesse/api/User/6021</source>
+ <event>put</event>
+</Update>
+
+
+
+
+
diff --git a/XMPP/error-invalid-state.xml b/XMPP/error-invalid-state.xml
new file mode 100644
index 0000000..5794863
--- /dev/null
+++ b/XMPP/error-invalid-state.xml
@@ -0,0 +1,25 @@
+
+
+
+
+ -
+
+<Update>
+ <data>
+ <apiErrors>
+ <apiError>
+ <errorType>Invalid State</errorType>
+ <errorMessage>CF_INVALID_OBJECT_STATE</errorMessage>
+ <errorData>22</errorData>
+ </apiError>
+ </apiErrors>
+ </data>
+ <requestId></requestId>
+ <source>/finesse/api/User/6021</source>
+ <event>put</event>
+</Update>
+
+
+
+
+
diff --git a/XMPP/login.xml b/XMPP/login.xml
new file mode 100644
index 0000000..124415a
--- /dev/null
+++ b/XMPP/login.xml
@@ -0,0 +1,50 @@
+
+
+
+
+ -
+
+<Update>
+ <data>
+ <user>
+ <dialogs>/finesse/api/User/6021/Dialogs</dialogs>
+ <extension>2830</extension>
+ <firstName>LPU</firstName>
+ <lastName>Test 21</lastName>
+ <loginId>6021</loginId>
+ <loginName>lpu_test_21</loginName>
+ <mediaType>1</mediaType>
+ <pendingState></pendingState>
+ <reasonCodeId>-1</reasonCodeId>
+ <roles>
+ <role>Agent</role>
+ <role>Supervisor</role>
+ </roles>
+ <settings>
+ <wrapUpOnIncoming>OPTIONAL</wrapUpOnIncoming>
+ <wrapUpOnOutgoing>OPTIONAL</wrapUpOnOutgoing>
+ </settings>
+ <state>NOT_READY</state>
+ <stateChangeTime>2023-01-13T11:07:11.466Z</stateChangeTime>
+ <teamId>5005</teamId>
+ <teamName>LPU_test</teamName>
+ <teams>
+ <Team>
+ <id>5005</id>
+ <name>LPU_test</name>
+ <uri>/finesse/api/Team/5005</uri>
+ </Team>
+ </teams>
+ <uri>/finesse/api/User/6021</uri>
+ <wrapUpTimer>7200</wrapUpTimer>
+ </user>
+ </data>
+ <event>PUT</event>
+ <requestId></requestId>
+ <source>/finesse/api/User/6021</source>
+</Update>
+
+
+
+
+
diff --git a/XMPP/logout.xml b/XMPP/logout.xml
new file mode 100644
index 0000000..4bb0801
--- /dev/null
+++ b/XMPP/logout.xml
@@ -0,0 +1,50 @@
+
+
+
+
+ -
+
+<Update>
+ <data>
+ <user>
+ <dialogs>/finesse/api/User/6021/Dialogs</dialogs>
+ <extension></extension>
+ <firstName>LPU</firstName>
+ <lastName>Test 21</lastName>
+ <loginId>6021</loginId>
+ <loginName>lpu_test_21</loginName>
+ <mediaType>1</mediaType>
+ <pendingState></pendingState>
+ <reasonCodeId>-1</reasonCodeId>
+ <roles>
+ <role>Agent</role>
+ <role>Supervisor</role>
+ </roles>
+ <settings>
+ <wrapUpOnIncoming>OPTIONAL</wrapUpOnIncoming>
+ <wrapUpOnOutgoing>OPTIONAL</wrapUpOnOutgoing>
+ </settings>
+ <state>LOGOUT</state>
+ <stateChangeTime>2023-01-13T10:46:22.083Z</stateChangeTime>
+ <teamId>5005</teamId>
+ <teamName>LPU_test</teamName>
+ <teams>
+ <Team>
+ <id>5005</id>
+ <name>LPU_test</name>
+ <uri>/finesse/api/Team/5005</uri>
+ </Team>
+ </teams>
+ <uri>/finesse/api/User/6021</uri>
+ <wrapUpTimer>7200</wrapUpTimer>
+ </user>
+ </data>
+ <event>PUT</event>
+ <requestId></requestId>
+ <source>/finesse/api/User/6021</source>
+</Update>
+
+
+
+
+
diff --git a/XMPP/not-ready.xml b/XMPP/not-ready.xml
new file mode 100644
index 0000000..ec99be3
--- /dev/null
+++ b/XMPP/not-ready.xml
@@ -0,0 +1,50 @@
+
+
+
+
+ -
+
+<Update>
+ <data>
+ <user>
+ <dialogs>/finesse/api/User/6021/Dialogs</dialogs>
+ <extension>2830</extension>
+ <firstName>LPU</firstName>
+ <lastName>Test 21</lastName>
+ <loginId>6021</loginId>
+ <loginName>lpu_test_21</loginName>
+ <mediaType>1</mediaType>
+ <pendingState></pendingState>
+ <reasonCodeId>-1</reasonCodeId>
+ <roles>
+ <role>Agent</role>
+ <role>Supervisor</role>
+ </roles>
+ <settings>
+ <wrapUpOnIncoming>OPTIONAL</wrapUpOnIncoming>
+ <wrapUpOnOutgoing>OPTIONAL</wrapUpOnOutgoing>
+ </settings>
+ <state>NOT_READY</state>
+ <stateChangeTime>2023-01-13T11:21:47.026Z</stateChangeTime>
+ <teamId>5005</teamId>
+ <teamName>LPU_test</teamName>
+ <teams>
+ <Team>
+ <id>5005</id>
+ <name>LPU_test</name>
+ <uri>/finesse/api/Team/5005</uri>
+ </Team>
+ </teams>
+ <uri>/finesse/api/User/6021</uri>
+ <wrapUpTimer>7200</wrapUpTimer>
+ </user>
+ </data>
+ <event>PUT</event>
+ <requestId></requestId>
+ <source>/finesse/api/User/6021</source>
+</Update>
+
+
+
+
+
diff --git a/XMPP/ready.xml b/XMPP/ready.xml
new file mode 100644
index 0000000..d03c165
--- /dev/null
+++ b/XMPP/ready.xml
@@ -0,0 +1,49 @@
+
+
+
+
+ -
+
+<Update>
+ <data>
+ <user>
+ <dialogs>/finesse/api/User/6021/Dialogs</dialogs>
+ <extension>2830</extension>
+ <firstName>LPU</firstName>
+ <lastName>Test 21</lastName>
+ <loginId>6021</loginId>
+ <loginName>lpu_test_21</loginName>
+ <mediaType>1</mediaType>
+ <pendingState></pendingState>
+ <roles>
+ <role>Agent</role>
+ <role>Supervisor</role>
+ </roles>
+ <settings>
+ <wrapUpOnIncoming>OPTIONAL</wrapUpOnIncoming>
+ <wrapUpOnOutgoing>OPTIONAL</wrapUpOnOutgoing>
+ </settings>
+ <state>READY</state>
+ <stateChangeTime>2023-01-13T11:14:47.981Z</stateChangeTime>
+ <teamId>5005</teamId>
+ <teamName>LPU_test</teamName>
+ <teams>
+ <Team>
+ <id>5005</id>
+ <name>LPU_test</name>
+ <uri>/finesse/api/Team/5005</uri>
+ </Team>
+ </teams>
+ <uri>/finesse/api/User/6021</uri>
+ <wrapUpTimer>7200</wrapUpTimer>
+ </user>
+ </data>
+ <event>PUT</event>
+ <requestId></requestId>
+ <source>/finesse/api/User/6021</source>
+</Update>
+
+
+
+
+
diff --git a/agent-bulk.go b/agent-bulk.go
new file mode 100644
index 0000000..2e74d4a
--- /dev/null
+++ b/agent-bulk.go
@@ -0,0 +1,185 @@
+package finesse_api
+
+import (
+ "context"
+ "fmt"
+ log "github.com/sirupsen/logrus"
+ "sync"
+)
+
+// massive parallel processing sets agents into the same state
+
+type AgentGroup struct {
+ Agents []*Agent
+ ctx context.Context
+ cancelFunc context.CancelFunc
+ wg sync.WaitGroup
+ mutex sync.Mutex
+}
+
+type BulkAgent struct {
+ Name string
+ Password string
+ Line string
+}
+
+func NewAgentGroup() *AgentGroup {
+ ctx, cancelFunc := context.WithCancel(context.Background())
+ return &AgentGroup{
+ Agents: nil,
+ ctx: ctx,
+ cancelFunc: cancelFunc,
+ wg: sync.WaitGroup{},
+ }
+}
+
+func (group *AgentGroup) AddAgentToGroup(name string, pwd string, line string, server *Server) error {
+ agent := NewAgentNotify(group.ctx, name, pwd, line, server)
+ err := agent.getId()
+ if err != nil {
+ log.WithFields(log.Fields{logProc: "AddAgentToGroup", logAgent: name}).Errorf("can't get actual agent state")
+ return err
+ }
+ if err = agent.StartXmpp(); err != nil {
+ log.WithFields(log.Fields{logProc: "AddAgentToGroup", logId: agent.LoginId, logServer: server.name}).
+ Errorf("problem start XMPP for agent [%s] on server [%s]", agent.LoginName, server.name)
+ return err
+ }
+ log.WithFields(log.Fields{logProc: "AddAgentToGroup", logId: agent.LoginId, logServer: server.name}).
+ Tracef("start XMPP subroutine for agent [%s] on server [%s]", agent.LoginName, server.name)
+ group.Agents = append(group.Agents, agent)
+ return nil
+}
+
+func (group *AgentGroup) AddBulkAgents(agents []BulkAgent, server *Server) []OperationError {
+ log.WithFields(log.Fields{logProc: "AddBulkAgents"}).Tracef("start procees add bulk agents with it's status")
+ err := make(chan OperationError, len(agents))
+ for _, agent := range agents {
+ group.wg.Add(1)
+ go func(a BulkAgent, server *Server, wg *sync.WaitGroup, c chan OperationError) {
+ defer wg.Done()
+
+ ag, e := server.CreateAgent(group.ctx, a.Name, a.Password, a.Line)
+ if e != nil {
+ c <- OperationError{
+ Type: TypeErrorNoStatus,
+ Error: e,
+ }
+ return
+ }
+ e = ag.StartXmpp()
+ if e != nil {
+ c <- OperationError{
+ Type: TypeErrorNoStatus,
+ Error: e,
+ }
+ return
+ }
+ group.mutex.Lock()
+ group.Agents = append(group.Agents, ag)
+ group.mutex.Unlock()
+ c <- OperationError{
+ Type: TypeErrorNoError,
+ Error: nil,
+ }
+ }(agent, server, &group.wg, err)
+ }
+ log.WithFields(log.Fields{logProc: "AddBulkAgents"}).Trace("wait for finish all in group")
+ group.wg.Wait()
+ var ret []OperationError
+ log.WithFields(log.Fields{logProc: "AddBulkAgents"}).Trace("wait for finish all in group")
+ for len(err) > 0 {
+ ret = append(ret, <-err)
+ }
+ if len(ret) != len(agents) {
+ log.WithFields(log.Fields{logProc: "AddBulkAgents"}).
+ Errorf("Agents in AgentGroup [%d] different from number of responses [%d]", len(agents), len(err))
+ } else {
+ log.WithFields(log.Fields{logProc: "AddBulkAgents"}).
+ Trace("operation processed for all Agent in group")
+ }
+ return ret
+}
+
+func (group *AgentGroup) Login() []OperationError {
+ return group.doRequest(AgentStateLogin, false)
+}
+
+func (group *AgentGroup) Logout(forceLogout ...bool) []OperationError {
+ force := false
+ if len(forceLogout) > 0 {
+ force = forceLogout[0]
+ }
+ return group.doRequest(AgentStateLogout, force)
+}
+
+func (group *AgentGroup) Ready(forceLogout ...bool) []OperationError {
+ force := false
+ if len(forceLogout) > 0 {
+ force = forceLogout[0]
+ }
+ return group.doRequest(AgentStateReady, force)
+}
+
+func (group *AgentGroup) NotReady() []OperationError {
+ return group.doRequest(AgentStateNotReady, false)
+}
+
+func (group *AgentGroup) CancelFunction() {
+ if group.cancelFunc != nil {
+ log.WithFields(log.Fields{logProc: "CancelFunction"}).
+ Trace("call cancelFunc for all subroutines")
+ group.cancelFunc()
+ }
+}
+
+func (group *AgentGroup) doRequest(operation string, force bool) []OperationError {
+ lProc := "doRequest"
+ if group.Agents == nil || len(group.Agents) < 1 {
+ log.WithFields(log.Fields{logProc: lProc, logRequestType: operation}).
+ Warn("AgentGroup is empty")
+ return nil
+ }
+ err := make(chan OperationError, len(group.Agents))
+ for _, agent := range group.Agents {
+ group.wg.Add(1)
+ go agentOperation(operation, agent, &group.wg, err, force)
+ }
+ group.wg.Wait()
+ var ret []OperationError
+ for len(err) > 0 {
+ ret = append(ret, <-err)
+ }
+ if len(ret) != len(group.Agents) {
+ log.WithFields(log.Fields{logProc: lProc, logRequestType: operation}).
+ Errorf("Agents in AgentGroup [%d] different from number of responses [%d]", len(group.Agents), len(err))
+ } else {
+ log.WithFields(log.Fields{logProc: lProc, logRequestType: operation}).
+ Trace("operation processed for all Agent in group")
+ }
+ return ret
+}
+
+func agentOperation(operation string, a *Agent, wg *sync.WaitGroup, c chan OperationError, force bool) {
+ lProc := "agentOperation"
+ log.WithFields(log.Fields{logProc: lProc, logRequestType: operation, logAgent: a.LoginName}).
+ Tracef("process operation [%s] for agent [%s]", operation, a.LoginName)
+ defer wg.Done()
+ switch operation {
+ case AgentStateLogin:
+ c <- a.Login()
+ case AgentStateLogout:
+ c <- a.Logout(force)
+ case AgentStateReady:
+ c <- a.Ready(force)
+ case AgentStateNotReady:
+ c <- a.NotReady()
+ default:
+ log.WithFields(log.Fields{logProc: lProc, logRequestType: operation, logAgent: a.LoginName}).
+ Errorf("unknown operation [%s] for agent [%s]", operation, a.LoginName)
+ c <- OperationError{
+ Type: TypeErrorUnknownBulkCommand,
+ Error: fmt.Errorf("unknown AgentGroup operation [%s] for agent [%s]", operation, a.LoginName),
+ }
+ }
+}
diff --git a/agent-operation.go b/agent-operation.go
new file mode 100644
index 0000000..deed30b
--- /dev/null
+++ b/agent-operation.go
@@ -0,0 +1,174 @@
+package finesse_api
+
+import (
+ "encoding/xml"
+ "fmt"
+ log "github.com/sirupsen/logrus"
+ "time"
+)
+
+const (
+ XmppTimeout = 20
+ TypeErrorNoError = 0
+ TypeErrorWrongState = 1
+ TypeErrorRequest = 2
+ TypeErrorResponse = 3
+ TypeErrorNotifyTimeout = 4
+ TypeErrorAnalyzeResponse = 5
+ TypeErrorUnknownBulkCommand = 6
+ TypeErrorNoStatus = 7
+)
+
+type OperationError struct {
+ Type int
+ Error error
+}
+
+func (a *Agent) Login() OperationError {
+ if a.lastStatus.State != AgentStateLogout {
+ return OperationError{
+ Type: TypeErrorWrongState,
+ Error: fmt.Errorf("agent [%s] is in [%s] state", a.LoginName, a.lastStatus.State),
+ }
+ }
+ return a.doStateChange(AgentStateLogin)
+}
+
+func (a *Agent) Logout(forceLogout ...bool) OperationError {
+ force := false
+ if len(forceLogout) > 0 {
+ force = forceLogout[0]
+ }
+ if a.lastStatus.State == AgentStateReady && force {
+ errOp := a.NotReady()
+ if errOp.Type != TypeErrorNoError {
+ return errOp
+ }
+ }
+ if a.lastStatus.State != AgentStateNotReady {
+ return OperationError{
+ Type: TypeErrorWrongState,
+ Error: fmt.Errorf("agent [%s] is in [%s] state and not possible logout", a.LoginName, a.lastStatus.State),
+ }
+ }
+ return a.doStateChange(AgentStateLogout)
+}
+
+func (a *Agent) Ready(forceReady ...bool) OperationError {
+ force := false
+ if len(forceReady) > 0 {
+ force = forceReady[0]
+ }
+ _, ok := AgentReadyStates[a.lastStatus.State]
+ if ok {
+ return OperationError{
+ Type: TypeErrorWrongState,
+ Error: fmt.Errorf("agent [%s] is in [%s] state and not possible switch to ready", a.LoginName, a.lastStatus.State),
+ }
+ }
+
+ if a.lastStatus.State == AgentStateLogout && force {
+ errOp := a.Login()
+ if errOp.Type != TypeErrorNoError {
+ return errOp
+ }
+ }
+
+ return a.doStateChange(AgentStateReady)
+}
+
+func (a *Agent) NotReady() OperationError {
+ _, ok := AgentReadyStates[a.lastStatus.State]
+ if !ok {
+ return OperationError{
+ Type: TypeErrorWrongState,
+ Error: fmt.Errorf("agent [%s] is in [%s] state and not possible switch to ready", a.LoginName, a.lastStatus.State),
+ }
+ }
+ return a.doStateChange(AgentStateNotReady)
+}
+
+func (a *Agent) doStateChange(requestState string, reason ...int) OperationError {
+ var err error
+ request := a.newAgentRequest()
+ var requestBody []byte
+ if requestState == AgentStateNotReady && len(reason) > 0 {
+ state := userStateWithReasonRequest{
+ State: requestState,
+ ReasonCodeId: reason[0],
+ }
+ requestBody, err = state.getUserRequest()
+ } else if requestState == AgentStateLogout && len(reason) > 0 {
+ state := userStateWithReasonRequest{
+ State: requestState,
+ ReasonCodeId: reason[0],
+ }
+ requestBody, err = state.getUserRequest()
+
+ } else if requestState == AgentStateLogin {
+ state := userLoginRequest{
+ XMLName: xml.Name{},
+ Text: "",
+ State: AgentStateLogin,
+ Extension: a.Line,
+ }
+ 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: a.LoginName, logNewState: requestState}).
+ Errorf("change state to %s agent %s on line %s. Prolem is %s", requestState, a.LoginName, a.Line, err)
+ return OperationError{
+ Type: TypeErrorRequest,
+ Error: err,
+ }
+ }
+ // clean queue https://stackoverflow.com/a/26143288/4074126
+ for len(a.response) > 0 {
+ data := <-a.response
+ log.WithFields(log.Fields{logProc: "doStateChange", logId: request.id, logAgent: a.LoginName, logNewState: requestState}).Debugf("remove data from channel [%s]", data)
+ }
+
+ response := request.doRequest("PUT", a.server.urlString(request.id, "User", a.LoginId), requestBody)
+ msg, err := response.responseError()
+ if err != nil {
+ response.close()
+ log.WithFields(log.Fields{logProc: "doStateChange", logId: response.id, logAgent: a.LoginName, logNewState: requestState}).Error(msg)
+ return OperationError{
+ Type: TypeErrorResponse,
+ Error: err,
+ }
+ }
+ log.WithFields(log.Fields{logProc: "doStateChange", logId: response.id, logAgent: a.LoginName, logNewState: requestState}).
+ Tracef("agnet [%s] state change request", a.LoginName)
+
+ select {
+ case status := <-a.response:
+ log.WithFields(log.Fields{logProc: "doStateChange", logId: response.id, logAgent: a.LoginName, logNewState: requestState}).Tracef("get message from XMPP notification")
+ s, e := a.analyzeResponse(status)
+ if e != nil {
+ log.WithFields(log.Fields{logProc: "doStateChange", logId: response.id, logAgent: a.LoginName, logNewState: requestState}).Error(e)
+ return OperationError{
+ Type: TypeErrorAnalyzeResponse,
+ Error: e,
+ }
+ } else {
+ a.lastStatus = s
+ }
+ return OperationError{
+ Type: TypeErrorNoError,
+ Error: nil,
+ }
+ case <-time.After(time.Duration(XmppTimeout) * time.Second):
+ log.WithFields(log.Fields{logProc: "doStateChange", logId: response.id, logAgent: a.LoginName, logNewState: requestState}).Error("collect notify response form XMPP timeouts")
+ return OperationError{
+ Type: TypeErrorNotifyTimeout,
+ Error: fmt.Errorf("timeout collect notify response form XMPP for agnet [%s]", a.LoginName),
+ }
+ }
+}
diff --git a/finesse-data-struct.go b/agent-request-struct.go
similarity index 85%
rename from finesse-data-struct.go
rename to agent-request-struct.go
index 989e824..ae0e9e2 100644
--- a/finesse-data-struct.go
+++ b/agent-request-struct.go
@@ -19,8 +19,8 @@ type userStateRequest struct {
State string `xml:"state"`
}
-// userNotReadyWithReasonRequest structure for agent logout
-type userNotReadyWithReasonRequest struct {
+// userStateWithReasonRequest structure for agent logout
+type userStateWithReasonRequest struct {
XMLName xml.Name `xml:"User"`
Text string `xml:",chardata"`
State string `xml:"state"`
@@ -47,11 +47,10 @@ func (u *userStateRequest) getUserRequest() ([]byte, error) {
return data, nil
}
-func (u *userNotReadyWithReasonRequest) getUserRequest() ([]byte, error) {
+func (u *userStateWithReasonRequest) getUserRequest() ([]byte, error) {
data, err := xml.Marshal(u)
if err != nil {
return nil, err
}
return data, nil
}
-
diff --git a/finesse-agent-status.go b/agent-status.go
similarity index 100%
rename from finesse-agent-status.go
rename to agent-status.go
diff --git a/agent.go b/agent.go
new file mode 100644
index 0000000..0337a91
--- /dev/null
+++ b/agent.go
@@ -0,0 +1,280 @@
+package finesse_api
+
+import (
+ "context"
+ "crypto/tls"
+ "encoding/xml"
+ "fmt"
+ log "github.com/sirupsen/logrus"
+ "gosrc.io/xmpp"
+ "gosrc.io/xmpp/stanza"
+ "net/http"
+ "reflect"
+ "strings"
+ "time"
+)
+
+const (
+ XmppMessageBuffer = 10 // define channel buffer for collect message from Xmpp notify service
+)
+
+type Agent struct {
+ LoginName string // login name
+ LoginId string // login ID
+ Password string // password
+ Line string // phone line
+ lastStatus *XmppUser // latest agent response
+ httpClient *http.Client // prepared HTTP client
+ streamManagerService *xmpp.StreamManager // XMPP scream manager for user
+ ctx context.Context // context for graceful shutdown of notify subroutine
+ server *Server // associate finesse server
+ response chan string // channel for get strings
+}
+
+// NewAgentNotify create new agent object, but not create/start any additional service
+//
+// Better way is use function Server.CreateAgent, this creates agent and start necessary function
+func NewAgentNotify(ctx context.Context, name string, pwd string, line string, server *Server) *Agent {
+ return &Agent{
+ LoginName: name,
+ LoginId: "",
+ Password: pwd,
+ Line: line,
+ lastStatus: nil,
+ httpClient: nil,
+ streamManagerService: nil,
+ server: server,
+ ctx: ctx,
+ response: make(chan string, XmppMessageBuffer),
+ }
+}
+
+func (a *Agent) newAgentRequest() *AgentRequest {
+ r := AgentRequest{
+ id: randomString(),
+ client: a.server.getHttpClient(),
+ server: a.server,
+ request: nil,
+ loginName: a.LoginName,
+ password: a.Password,
+ line: a.Line,
+ }
+ log.WithFields(log.Fields{logProc: "NewRequest", logId: r.id, logServer: r.server.name}).Tracef("prepare new request for server [%s]", a.server.name)
+ return &r
+}
+
+// getId read ID from Finesse and store it if OK
+func (a *Agent) getId() error {
+ if a.LoginId != "" {
+ return nil
+ }
+ request := a.newAgentRequest()
+ response := request.doRequest("GET", a.server.urlString(request.id, "User", a.LoginName), nil)
+ defer response.close()
+ msg, err := response.responseError()
+ if err != nil {
+ log.WithFields(log.Fields{logProc: "getAgentId", logId: response.id, logAgent: a.LoginName}).Error(msg)
+ return err
+ }
+ log.WithFields(log.Fields{logProc: "getAgentId", logId: response.id, logAgent: a.LoginName}).Trace("success get data for agentId from name")
+ data, err := newXmppUser(response.GetResponseBody())
+ if err != nil {
+ log.WithFields(log.Fields{logProc: "getAgentId", logId: response.id, logAgent: a.LoginName}).Error(err)
+ return err
+ }
+ if len(data.LoginId) <= 0 {
+ log.WithFields(log.Fields{logProc: "getAgentId", logId: response.id, logAgent: a.LoginName}).Errorf("problem collect agentId from request")
+ return fmt.Errorf("agent ID is empty for agent name %s", a.LoginName)
+ }
+ a.lastStatus = data
+ a.LoginId = data.LoginId
+ log.WithFields(log.Fields{logProc: "getAgentId", logId: response.id, logAgent: a.LoginName}).Tracef("collect agentId [%s] for agent [%s]", data.LoginId, a.LoginName)
+ return nil
+}
+
+func (a *Agent) getDomain() string {
+ if strings.Contains(a.LoginName, "@") {
+ sp := strings.Split(a.LoginName, "@")
+ if len(sp) == 2 {
+ return sp[1]
+ }
+ }
+ return a.server.getDomain()
+}
+
+func (a *Agent) StartXmpp() error {
+ if a.streamManagerService != nil {
+ log.WithFields(log.Fields{logProc: "StartNotification", logAgent: a.LoginName}).Trace("start finesse_notifier - XMPP notifier is ready ")
+ return nil
+ }
+ if a.LoginId == "" {
+ log.WithFields(log.Fields{logProc: "StartNotification", logAgent: a.LoginName}).Errorf("XMPP not start missing agent login ID")
+ return fmt.Errorf("XMPP not start missing agent login ID")
+ }
+
+ log.WithFields(log.Fields{logProc: "StartNotification", logAgent: a.LoginName}).Trace("start finesse_notifier")
+
+ // setup WSS or XMPP connection parameters
+ t := &tls.Config{InsecureSkipVerify: a.server.ignore}
+ domain := a.getDomain()
+ server := fmt.Sprintf("wss://%s:%d/ws/", a.server.name, a.server.xmppPort)
+
+ if a.server.insecureXmpp {
+ server = fmt.Sprintf("%s:%d", a.server.name, a.server.xmppPort)
+ }
+ log.WithFields(log.Fields{logProc: "StartNotification", logAgent: a.LoginName}).
+ Debugf("finesse_notifier server [%s] with domain [%s] ignore certificate problem [%t]", server, domain, a.server.ignore)
+
+ config := xmpp.Config{
+ TransportConfiguration: xmpp.TransportConfiguration{
+ Address: server,
+ Domain: domain,
+ TLSConfig: t,
+ },
+ Jid: fmt.Sprintf("%s@%s", a.LoginId, a.server.name),
+ Credential: xmpp.Password(a.Password),
+ //StreamLogger: os.Stdout,
+ Insecure: a.server.ignore,
+ }
+
+ //goland:noinspection HttpUrlsUsage
+ stanza.TypeRegistry.MapExtension(stanza.PKTMessage, xml.Name{Space: "http://jabber.org/protocol/pubsub#event", Local: "event"}, stanza.PubSubEvent{})
+ router := xmpp.NewRouter()
+ //router.HandleFunc("message", a.messageHandler)
+ router.HandleFunc("message", func(s xmpp.Sender, p stanza.Packet) {
+ log.WithFields(log.Fields{logProc: "messageHandler", logAgent: a.LoginName}).Trace("handle XMPP message stream")
+ msg, ok := p.(stanza.Message)
+ if !ok {
+ log.WithFields(log.Fields{logProc: "messageHandler", logAgent: a.LoginName}).Tracef("ignore packet %T", p)
+ return
+ }
+ if len(msg.Extensions) > 0 {
+ for _, extension := range msg.Extensions {
+ if "*stanza.PubSubEvent" == reflect.TypeOf(extension).String() {
+ ext := extension.(*stanza.PubSubEvent)
+ if "*stanza.ItemsEvent" == reflect.TypeOf(ext.EventElement).String() {
+ element := ext.EventElement.(*stanza.ItemsEvent)
+ for _, item := range element.Items {
+ log.WithFields(log.Fields{logProc: "messageHandler", logAgent: a.LoginName}).Trace("success accept message")
+
+ select {
+ case a.response <- item.Any.Content:
+ log.WithFields(log.Fields{logProc: "messageHandler", logAgent: a.LoginName}).Trace("success send new data into buffered queue")
+ case <-time.After(time.Duration(20) * time.Second):
+ log.WithFields(log.Fields{logProc: "messageHandler", logAgent: a.LoginName}).Warnf("timeout send message to queue")
+ default:
+ log.WithFields(log.Fields{logProc: "messageHandler", logAgent: a.LoginName}).Warnf("response buffered queue is full. Data lost!")
+ }
+ }
+ } else {
+ log.WithFields(log.Fields{logProc: "messageHandler", logAgent: a.LoginName}).
+ Warnf("PubSubEvent doesnt contains unexpected type [%s]", reflect.TypeOf(ext.EventElement).String())
+ }
+ } else {
+ log.WithFields(log.Fields{logProc: "messageHandler", logAgent: a.LoginName}).
+ Warnf("unknown XMPP extension type [%s]", reflect.TypeOf(extension).String())
+ }
+ }
+ } else {
+ log.WithFields(log.Fields{logProc: "messageHandler", logAgent: a.LoginName}).Warnf("XMPP message without extension type")
+ }
+ })
+
+ 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}).Debugf("prepare XMPP client for agent [%s]", a.LoginName)
+
+ a.streamManagerService = xmpp.NewStreamManager(client, nil)
+ go func() {
+ log.WithFields(log.Fields{logProc: "StartNotification", logAgent: a.LoginName}).Debugf("start XMPP listener for agent [%s]", a.LoginName)
+ err = a.streamManagerService.Run()
+ if err != nil {
+ a.streamManagerService = nil
+ log.WithFields(log.Fields{logProc: "StartNotification", logAgent: a.LoginName}).Errorf("agent [%s] XMPP stream manager problem %s", a.LoginName, err)
+ return
+ }
+ log.WithFields(log.Fields{logProc: "StartNotification", logAgent: a.LoginName}).Tracef("started notify StreamManager for agent [%s]", a.LoginName)
+ // await for stop
+ <-a.ctx.Done()
+ log.WithFields(log.Fields{logProc: "StartNotification", logAgent: a.LoginName}).Tracef("stop notify subroutine for agent [%s]", a.LoginName)
+ if a.streamManagerService != nil {
+ a.streamManagerService.Stop()
+ a.streamManagerService = nil
+ }
+ }()
+ // wait for connect
+ time.Sleep(1 * time.Second)
+ return nil
+}
+
+// GetLastStatus get latest collected user status
+func (a *Agent) GetLastStatus() *XmppUser {
+ return a.lastStatus
+}
+
+// GetStatus geta actual agent status from finesse server
+func (a *Agent) GetStatus() (*XmppUser, error) {
+ request := a.newAgentRequest()
+ response := request.doRequest("GET", a.server.urlString(request.id, "User", a.LoginName), nil)
+ defer response.close()
+ msg, err := response.responseError()
+ if err != nil {
+ log.WithFields(log.Fields{logProc: "getAgentId", logId: response.id, logAgent: a.LoginName}).Error(msg)
+ return nil, err
+ }
+ log.WithFields(log.Fields{logProc: "getAgentId", logId: response.id, logAgent: a.LoginName}).Trace("success get data for agentId from name")
+ data, err := newXmppUser(response.GetResponseBody())
+ if err != nil {
+ log.WithFields(log.Fields{logProc: "getAgentId", logId: response.id, logAgent: a.LoginName}).Error(err)
+ return nil, err
+ }
+ a.lastStatus = data
+ return a.lastStatus, nil
+}
+
+func (a *Agent) FullString() string {
+ l := "Agent:"
+ l = fmt.Sprintf("%s\r\n Name: %s", l, a.LoginName)
+ l = fmt.Sprintf("%s\r\n Id: %s", l, a.LoginId)
+ l = fmt.Sprintf("%s\r\n Line: %s", l, a.Line)
+ if a.lastStatus != nil {
+ l = fmt.Sprintf("%s\r\n Status: %s", l, a.lastStatus.State)
+ } else {
+ l = fmt.Sprintf("%s\r\n Status: %s", l, "UNKNOWN")
+ }
+ return l
+}
+
+func (a *Agent) String() string {
+ if a.lastStatus != nil {
+ return fmt.Sprintf("%s (%s) => %s", a.LoginName, a.Line, a.lastStatus.State)
+ }
+ return fmt.Sprintf("%s (%s) => UNKNOWN", a.LoginName, a.Line)
+}
+
+func (a *Agent) analyzeResponse(data string) (*XmppUser, error) {
+ var envelope XmppUpdate
+ var err error
+ err = xml.Unmarshal([]byte(data), &envelope)
+ if err != nil {
+ log.WithFields(log.Fields{logProc: "analyzeResponse", logAgent: a.LoginName}).Warnf("problem with XML unmarshal envelope - %s", err)
+ return nil, fmt.Errorf("problem unmarhal response XML")
+ }
+ if envelope.Data.Error.ApiErrors != nil {
+ // problem here is error
+ log.WithFields(log.Fields{logProc: "analyzeResponse", logAgent: a.LoginName}).Warnf("request ends with error for XMPP User - %s", err)
+ return nil, fmt.Errorf("%s", envelope.Data.Error.ApiErrors[0].ErrorMessage)
+ }
+ if len(envelope.Data.User.URI) > 0 {
+ log.WithFields(log.Fields{logProc: "analyzeResponse", logAgent: a.LoginName}).Tracef("collect User data for XMPP")
+ usr := &envelope.Data.User
+ return usr, nil
+ }
+ if envelope.Data.Dialogs.Dialogs != nil {
+ log.WithFields(log.Fields{logProc: "analyzeResponse", logAgent: a.LoginName}).Warnf("collect dialogs data for XMPP User - %s", err)
+
+ }
+ log.WithFields(log.Fields{logProc: "analyzeResponse", logAgent: a.LoginName}).Warnf("unknown data body in request for XMPP User - %s", err)
+ return nil, fmt.Errorf("problem get right data from response for request %s", envelope.Source)
+}
diff --git a/finesse-agent-detail.go b/finesse-agent-detail.go
deleted file mode 100644
index a48b201..0000000
--- a/finesse-agent-detail.go
+++ /dev/null
@@ -1,135 +0,0 @@
-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.go b/finesse-agent.go
deleted file mode 100644
index 334fbba..0000000
--- a/finesse-agent.go
+++ /dev/null
@@ -1,41 +0,0 @@
-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-log-fileds.go b/finesse-log-fileds.go
deleted file mode 100644
index 78d8e0b..0000000
--- a/finesse-log-fileds.go
+++ /dev/null
@@ -1,13 +0,0 @@
-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
deleted file mode 100644
index 68122f9..0000000
--- a/finesse-notify.go
+++ /dev/null
@@ -1,75 +0,0 @@
-//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-server-parallel.go b/finesse-server-parallel.go
deleted file mode 100644
index 3010b3d..0000000
--- a/finesse-server-parallel.go
+++ /dev/null
@@ -1,372 +0,0 @@
-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
- if r.agentResponse != nil {
- status = append(status, r.agentResponse)
- if r.requestError == nil {
- success++
- }
- } else if r.requestError != nil {
- log.WithFields(log.Fields{logProc: mainName, logServer: f.name}).Error(r.requestError)
- }
- }
- 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
deleted file mode 100644
index 3febbef..0000000
--- a/finesse-server.go
+++ /dev/null
@@ -1,260 +0,0 @@
-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/log-fileds.go b/log-fileds.go
new file mode 100644
index 0000000..22b8824
--- /dev/null
+++ b/log-fileds.go
@@ -0,0 +1,12 @@
+package finesse_api
+
+const (
+ logProc = "proc"
+ logId = "requestId"
+ logServer = "server"
+ logAgent = "agentName"
+ logHttpStatus = "httpStatus"
+ logNewState = "agentState"
+ logBody = "body"
+ logRequestType = "requestType"
+)
diff --git a/notify-data.go b/notify-data.go
new file mode 100644
index 0000000..234d6a6
--- /dev/null
+++ b/notify-data.go
@@ -0,0 +1,16 @@
+package finesse_api
+
+type XmppUpdate struct {
+ Event string `xml:"event"`
+ RequestId string `xml:"requestId"`
+ Source string `xml:"source"`
+ Data struct {
+ User XmppUser `xml:"user,omitempty"`
+ Error XmppErrors `xml:"apiErrors,omitempty"`
+ Dialogs XmppDialogs `xml:"Dialog,omitempty"`
+ Devices XmppDevices `xml:"Devices,omitempty"`
+ Queue XmppQueue `xml:"Queue,omitempty"`
+ Team XmppTeam `xml:"Team,omitempty"`
+ TeamMessage XmppTeamMessage `xml:"TeamMessage,omitempty"`
+ } `xml:"data"`
+}
diff --git a/notify-device.go b/notify-device.go
new file mode 100644
index 0000000..cd52d40
--- /dev/null
+++ b/notify-device.go
@@ -0,0 +1,9 @@
+package finesse_api
+
+type XmppDevices struct {
+ Device []struct {
+ DeviceId string `xml:"deviceId"`
+ DeviceType string `xml:"deviceType"`
+ DeviceTypeName string `xml:"deviceTypeName"`
+ } `xml:"Device"`
+}
diff --git a/notify-dialog.go b/notify-dialog.go
new file mode 100644
index 0000000..7400544
--- /dev/null
+++ b/notify-dialog.go
@@ -0,0 +1,47 @@
+package finesse_api
+
+type XmppDialogs struct {
+ Dialogs []XmppDialog `xml:"Dialog"`
+}
+
+type XmppDialog struct {
+ AssociatedDialogUri string `xml:"associatedDialogUri"`
+ FromAddress string `xml:"fromAddress"`
+ ID string `xml:"id"`
+ SecondaryId string `xml:"secondaryId"`
+ MediaProperties struct {
+ MediaId string `xml:"mediaId"`
+ DNIS string `xml:"DNIS"`
+ CallType string `xml:"callType"`
+ DialedNumber string `xml:"dialedNumber"`
+ OutboundClassification string `xml:"outboundClassification"`
+ CallVariables struct {
+ CallVariable []struct {
+ Name string `xml:"name"`
+ Value string `xml:"value"`
+ } `xml:"CallVariable"`
+ } `xml:"callvariables"`
+ QueueNumber string `xml:"queueNumber"`
+ QueueName string `xml:"queueName"`
+ CallKeyCallId string `xml:"callKeyCallId"`
+ CallKeySequenceNum string `xml:"callKeySequenceNum"`
+ CallKeyPrefix string `xml:"callKeyPrefix"`
+ } `xml:"mediaProperties"`
+ MediaType string `xml:"mediaType"`
+ Participants struct {
+ Participant struct {
+ Actions struct {
+ Action []string `xml:"action"`
+ } `xml:"actions"`
+ MediaAddress string `xml:"mediaAddress"`
+ MediaAddressType string `xml:"mediaAddressType"`
+ StartTime string `xml:"startTime"`
+ State string `xml:"state"`
+ StateCause string `xml:"stateCause"`
+ StateChangeTime string `xml:"stateChangeTime"`
+ } `xml:"Participant"`
+ } `xml:"participants"`
+ State string `xml:"state"`
+ ToAddress string `xml:"toAddress"`
+ URI string `xml:"uri"`
+}
diff --git a/notify-error.go b/notify-error.go
new file mode 100644
index 0000000..3d475e9
--- /dev/null
+++ b/notify-error.go
@@ -0,0 +1,14 @@
+package finesse_api
+
+type XmppErrors struct {
+ ApiErrors []XmppError `xml:"apiError"`
+}
+
+type XmppError struct {
+ PeripheralErrorCode int `xml:"peripheralErrorCode,omitempty"`
+ ErrorType string `xml:"errorType,omitempty"`
+ ErrorMessage string `xml:"errorMessage,omitempty"`
+ PeripheralErrorText string `xml:"peripheralErrorText,omitempty"`
+ PeripheralErrorMsg string `xml:"peripheralErrorMsg,omitempty"`
+ ErrorData int `xml:"errorData,omitempty"`
+}
diff --git a/notify-queue.go b/notify-queue.go
new file mode 100644
index 0000000..16f061d
--- /dev/null
+++ b/notify-queue.go
@@ -0,0 +1,19 @@
+package finesse_api
+
+type XmppQueue struct {
+ URI string `xml:"uri"`
+ Name string `xml:"name"`
+ Statistics struct {
+ CallsInQueue string `xml:"callsInQueue"`
+ StartTimeOfLongestCallInQueue string `xml:"startTimeOfLongestCallInQueue"`
+ AgentsReady string `xml:"agentsReady"`
+ AgentsNotReady string `xml:"agentsNotReady"`
+ AgentsBusyOther string `xml:"agentsBusyOther"`
+ AgentsLoggedOn string `xml:"agentsLoggedOn"`
+ AgentsTalkingInbound string `xml:"agentsTalkingInbound"`
+ AgentsTalkingOutbound string `xml:"agentsTalkingOutbound"`
+ AgentsTalkingInternal string `xml:"agentsTalkingInternal"`
+ AgentsWrapUpNotReady string `xml:"agentsWrapUpNotReady"`
+ AgentsWrapUpReady string `xml:"agentsWrapUpReady"`
+ } `xml:"statistics"`
+}
diff --git a/notify-team.go b/notify-team.go
new file mode 100644
index 0000000..154a357
--- /dev/null
+++ b/notify-team.go
@@ -0,0 +1,43 @@
+package finesse_api
+
+type XmppTeam struct {
+ URI string `xml:"uri"`
+ ID string `xml:"id"`
+ Name string `xml:"name"`
+ Users struct {
+ User []struct {
+ URI string `xml:"uri"`
+ LoginId string `xml:"loginId"`
+ FirstName string `xml:"firstName"`
+ LastName string `xml:"lastName"`
+ Dialogs string `xml:"dialogs"`
+ Extension string `xml:"extension"`
+ PendingState string `xml:"pendingState"`
+ State string `xml:"state"`
+ StateChangeTime string `xml:"stateChangeTime"`
+ ReasonCode struct {
+ Category string `xml:"category"`
+ Code string `xml:"code"`
+ Label string `xml:"label"`
+ ID string `xml:"id"`
+ URI string `xml:"uri"`
+ } `xml:"reasonCode"`
+ } `xml:"User"`
+ } `xml:"users"`
+}
+
+type XmppTeamMessage struct {
+ URI string `xml:"uri"`
+ ID string `xml:"id"`
+ CreatedBy struct {
+ ID string `xml:"id"`
+ FirstName string `xml:"firstName"`
+ LastName string `xml:"lastName"`
+ } `xml:"createdBy"`
+ CreatedAt string `xml:"createdAt"`
+ Duration string `xml:"duration"`
+ Content string `xml:"content"`
+ Teams struct {
+ Team []string `xml:"team"`
+ } `xml:"teams"`
+}
diff --git a/notify-user.go b/notify-user.go
new file mode 100644
index 0000000..824774f
--- /dev/null
+++ b/notify-user.go
@@ -0,0 +1,67 @@
+package finesse_api
+
+import "encoding/xml"
+
+type XmppUser struct {
+ //XMLName xml.Name `xml:"User"`
+ 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 {
+ 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 {
+ Role []string `xml:"role"`
+ } `xml:"roles"`
+ Settings struct {
+ 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 {
+ Team []struct {
+ Id int `xml:"id"`
+ Name string `xml:"name"`
+ URI string `xml:"uri"`
+ } `xml:"Team"`
+ } `xml:"teams"`
+ MobileAgent struct {
+ Mode string `xml:"mode"`
+ DialNumber string `xml:"dialNumber"`
+ } `xml:"mobileAgent"`
+ ActiveDeviceId string `xml:"activeDeviceId"`
+ Devices struct {
+ Device []struct {
+ DeviceId string `xml:"deviceId"`
+ DeviceType string `xml:"deviceType"`
+ DeviceTypeName string `xml:"deviceTypeName"`
+ } `xml:"device"`
+ } `xml:"devices"`
+}
+
+func newXmppUser(data string) (*XmppUser, error) {
+ var u XmppUser
+ buffer := []byte(data)
+ err := xml.Unmarshal(buffer, &u)
+ if err != nil {
+ return nil, err
+ }
+ return &u, nil
+}
diff --git a/finesse-request.go b/request.go
similarity index 54%
rename from finesse-request.go
rename to request.go
index d652077..7459826 100644
--- a/finesse-request.go
+++ b/request.go
@@ -9,26 +9,19 @@ import (
"time"
)
-// FinesseRequest Structure for one API request
-type FinesseRequest struct {
- id string
- client *http.Client
- server *FinesseServer
- request *http.Request
+// AgentRequest Structure for one API request
+type AgentRequest struct {
+ id string
+ loginName string
+ password string
+ line string
+ client *http.Client
+ server *Server
+ 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) {
+// setHeader create request header
+func (f *AgentRequest) setHeader() {
if f.request.Method != "GET" {
f.request.Header.Set("Content-Type", "application/xml")
}
@@ -38,10 +31,11 @@ func (f *FinesseRequest) setHeader(agent *FinesseAgent) {
//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)
+ f.request.SetBasicAuth(f.loginName, f.password)
}
-func (f *FinesseRequest) httpClient() {
+// httpClient prepare httpClient for request
+func (f *AgentRequest) httpClient() {
if f.client == nil {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
@@ -51,21 +45,40 @@ func (f *FinesseRequest) httpClient() {
}
}
-func (f *FinesseRequest) doRequest(method string, url string, agent *FinesseAgent, data []byte) *FinesseResponse {
+// doRequest process one request
+func (f *AgentRequest) doRequest(method string, url string, data []byte) *AgentResponse {
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)
+ "problem create [%s %s] request for [%s] agent with error %s", method, url, f.loginName, err)
}
f.request = request
- f.setHeader(agent)
+ f.setHeader()
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.newResponse(resp, err, r)
+ }
+ return f.newResponse(resp, nil, "")
+}
+
+// newResponse Create new response structure
+func (f *AgentRequest) newResponse(response *http.Response, e error, message string) *AgentResponse {
+ r := new(AgentResponse)
+ 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"
}
- return f.NewFinesseResponse(resp, nil, "")
+ log.WithFields(log.Fields{logProc: "NewResponse", logId: r.id}).Tracef("response with status [%s]", r.statusMessage)
+ return r
}
diff --git a/finesse-response.go b/response.go
similarity index 62%
rename from finesse-response.go
rename to response.go
index f6520c9..ab47deb 100644
--- a/finesse-response.go
+++ b/response.go
@@ -7,8 +7,8 @@ import (
"net/http"
)
-// FinesseResponse Structure for one API response
-type FinesseResponse struct {
+// AgentResponse Structure for one API response
+type AgentResponse struct {
id string
response *http.Response
err error
@@ -18,25 +18,7 @@ type FinesseResponse struct {
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() {
+func (f *AgentResponse) close() {
if f.response != nil {
if f.response.Body != nil {
_ = f.response.Body.Close()
@@ -45,7 +27,7 @@ func (f *FinesseResponse) close() {
}
}
-func (f *FinesseResponse) responseError() (string, error) {
+func (f *AgentResponse) responseError() (string, error) {
if f.statusCode >= 200 && f.statusCode <= 299 {
return "", nil
}
@@ -56,7 +38,7 @@ func (f *FinesseResponse) responseError() (string, error) {
}
// GetResponseBody Read API response body
-func (f *FinesseResponse) GetResponseBody() string {
+func (f *AgentResponse) GetResponseBody() string {
if f.response == nil {
return f.body
}
@@ -67,7 +49,7 @@ func (f *FinesseResponse) GetResponseBody() string {
return f.body
}
-func (f *FinesseResponse) responseReturnData() error {
+func (f *AgentResponse) 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)
diff --git a/server.go b/server.go
new file mode 100644
index 0000000..ad0a781
--- /dev/null
+++ b/server.go
@@ -0,0 +1,116 @@
+package finesse_api
+
+import (
+ "context"
+ "crypto/tls"
+ "fmt"
+ log "github.com/sirupsen/logrus"
+ "net/http"
+ "path"
+ "strings"
+ "time"
+)
+
+// Server Structure for finesse server data
+type Server struct {
+ name string // name is FQDN of server or IP address
+ port int // port for finesse API
+ ignore bool // ignore invalid certificate
+ xmppPort int // port for XMPP notification
+ insecureXmpp bool // insecureXmpp for connect insecure direct XMPP instead of WSS
+ timeOut int // timeOut for API requests default is 30 sec
+}
+
+const (
+ DefaultServerHttpsPort = 8445 // DefaultServerHttpsPort standard Finesse API port
+ DefaultServerXmppPort = 7443 // DefaultServerXmppPort standard secure XMPP over WSS port (secure XMPP communication)
+ DefaultServerDirectXmppPort = 5222 // DefaultServerDirectXmppPort insecure XMPP port for direct communication (by default disabled on Finesse server)
+ DefaultServerTimeout = 30 // DefaultServerTimeout define timeout for API and Notify communication in seconds
+)
+
+// NewServer Creating new Finesse server structure connect on standard ports and manage if ignore certificate problems
+//
+// possible use repeat for different agents
+func NewServer(server string, ignoreCert bool, time ...int) *Server {
+ timeOut := DefaultServerTimeout
+ if len(time) > 0 {
+ timeOut = time[0]
+ }
+ return NewServerDetail(server, DefaultServerHttpsPort, ignoreCert, DefaultServerXmppPort, false, timeOut)
+}
+
+// NewServerDetail Create new Finesse server structure with required security and ports
+//
+// possible use repeat for different agents
+func NewServerDetail(server string, port int, ignore bool, xmppPort int, insecureXmpp bool, timeOut int) *Server {
+ return &Server{
+ name: server,
+ port: port,
+ ignore: ignore,
+ xmppPort: xmppPort,
+ insecureXmpp: insecureXmpp,
+ timeOut: timeOut,
+ }
+}
+
+// CreateAgent create new agent, read agent ID from Finesse server, start XMPP notify connection and return Agent or error if problem
+//
+// - ctx context.Context - used for graceful shutdown of XMPP connection
+func (s *Server) CreateAgent(ctx context.Context, name string, pwd string, line string) (*Agent, error) {
+ log.WithFields(log.Fields{logProc: "AddAgent", logAgent: name}).Tracef("prepare agent and try collect it's ID")
+ a := NewAgentNotify(ctx, name, pwd, line, s)
+ err := a.getId()
+ if err != nil {
+ log.WithFields(log.Fields{logProc: "AddAgent", logAgent: name}).Tracef("can't get actual agent state")
+ return nil, err
+ }
+
+ return a, nil
+}
+
+// getHttpClient create httpclient with setup from server configuration
+func (s *Server) getHttpClient() *http.Client {
+ customTransport := http.DefaultTransport.(*http.Transport).Clone()
+ customTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: s.ignore}
+ client := &http.Client{Transport: customTransport, Timeout: time.Duration(s.timeOut) * time.Second}
+
+ return client
+}
+
+// urlString create full API request path
+//
+// Expect:
+// - rId string - unique request identification
+// - pathPart ...string - one or more
+//
+// Example:
+// - urlString("xmp", "6350") => https://{server:port}/finesse/api/6350
+// - urlString("xmp", "6350", "Dialogs) => https://{server:port}/finesse/api/6350/Dialogs
+func (s *Server) 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 s.port != 80 {
+ url = fmt.Sprintf("https://%s:%d%s", s.name, s.port, restPath)
+ } else {
+ url = fmt.Sprintf("https://%s%s", s.name, restPath)
+ }
+ log.WithFields(log.Fields{logProc: "urlString", logServer: s.name, logId: rId}).Tracef("Request URI: %s", url)
+ return url
+}
+
+// getDomain get only domain from Finesse server FQDN, for IP address or only host returns empty string
+func (s *Server) getDomain() string {
+ if validIpAddress(s.name) {
+ return ""
+ }
+ l := strings.Split(s.name, ".")
+ if len(l) > 1 {
+ return strings.Join(l[1:], ".")
+ }
+ return ""
+}
diff --git a/finesse-utils.go b/utils.go
similarity index 84%
rename from finesse-utils.go
rename to utils.go
index 46ee1ae..732f759 100644
--- a/finesse-utils.go
+++ b/utils.go
@@ -15,9 +15,8 @@ const (
)
var (
- src = rand.NewSource(time.Now().UnixNano()) // randomize base string
- maxRandomSize = 10 // required size of random string
- shortBodyChars = 120 // Max length print from string
+ src = rand.NewSource(time.Now().UnixNano()) // randomize base string
+ maxRandomSize = 10 // required size of random string
)
func randomString() string {
@@ -41,10 +40,9 @@ func randomString() 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) {
+ if validIpAddress(srv) {
return true
}
if invalidAddress.MatchString(srv) {
@@ -52,3 +50,8 @@ func ValidServerNameIp(srv string) bool {
}
return dnsName.MatchString(srv)
}
+
+func validIpAddress(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])$`)
+ return ipAddress.MatchString(srv)
+}