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) +}