-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathbridge.go
238 lines (198 loc) · 8.12 KB
/
bridge.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
package zammadbridge
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/google/uuid"
"github.com/rs/zerolog/log"
)
type ZammadBridge struct {
Config *Config
Client3CX API3CX
ClientZammad http.Client
ongoingCalls map[json.Number]CallInformation
}
// NewZammadBridge initializes a new client that listens for 3CX calls and forwards to Zammad.
func NewZammadBridge(config *Config) (*ZammadBridge, error) {
client3CX, err := Create3CXClient(config)
if err != nil {
return nil, fmt.Errorf("unable to create 3CX client: %w", err)
}
return &ZammadBridge{
Config: config,
Client3CX: client3CX,
ongoingCalls: map[json.Number]CallInformation{},
}, nil
}
// Listen listens for calls and does not return unless something really bad happened.
func (z *ZammadBridge) Listen() error {
log.Info().Msg("Starting 3CX-Zammad bridge (fetching calls every " + strconv.FormatFloat(z.Config.Bridge.PollInterval, 'f', -1, 64) + " seconds)")
for {
err := z.RequestAndProcess()
if err != nil && (strings.Contains(err.Error(), "401") || strings.Contains(err.Error(), "403")) {
log.Trace().Err(err).Msg("Reconnecting due to authentication error")
// Authentication error
err = z.Client3CX.AuthenticateRetry(time.Second * 120)
if err != nil {
return fmt.Errorf("unable to authenticate: %w", err)
}
} else if err != nil {
log.Error().Err(err).Msg("Error processing calls")
}
// Wait until the next polling should occur
time.Sleep(time.Duration(float64(time.Second) * z.Config.Bridge.PollInterval))
}
}
// RequestAndProcess requests the current calls from 3CX and processes them to Zammad
func (z *ZammadBridge) RequestAndProcess() error {
calls, err := z.Client3CX.FetchCalls()
newCalls := make([]json.Number, 0, len(calls))
for _, c := range calls {
err = z.ProcessCall(&c)
if err != nil {
log.Warn().Err(err).Msg("Warning - error processing call")
}
newCalls = append(newCalls, c.Id)
}
var endedCalls []json.Number
endedCallLoop:
for callId, oldInfo := range z.ongoingCalls {
// Check if call is still ongoing
for _, newCallId := range newCalls {
if newCallId == callId {
continue endedCallLoop
}
}
// Apparently, the call has ended, because 3CX does not report it any longer
log.Trace().Str("call_id", oldInfo.CallUID).Str("direction", oldInfo.Direction).Str("from", oldInfo.CallFrom).Str("to", oldInfo.CallTo).Msg("Call ended (no longer reported by 3CX)")
endedCalls = append(endedCalls, callId)
if oldInfo.Status == "Routing" {
log.Info().Str("call_id", oldInfo.CallUID).Str("direction", oldInfo.Direction).Str("from", oldInfo.CallFrom).Str("to", oldInfo.CallTo).Msg("Call ended (hangup from routing)")
z.LogIfErr(z.ZammadHangup(&oldInfo, "cancel"), "hangup-from-routing")
} else if oldInfo.Status == "Talking" {
log.Info().Str("call_id", oldInfo.CallUID).Str("direction", oldInfo.Direction).Str("from", oldInfo.CallFrom).Str("to", oldInfo.CallTo).Msg("Call ended (hangup from talking)")
z.LogIfErr(z.ZammadHangup(&oldInfo, "normalClearing"), "hangup-from-talking")
} else if oldInfo.Status == "Transferring" && z.Config.Zammad.LogMissedQueueCalls {
log.Info().Str("call_id", oldInfo.CallUID).Str("direction", oldInfo.Direction).Str("from", oldInfo.CallFrom).Str("to", oldInfo.CallTo).Msg("Queue call was not answered")
oldInfo.AgentNumber = strconv.Itoa(z.Config.Phone3CX.QueueExtension)
z.LogIfErr(z.ZammadHangup(&oldInfo, "cancel"), "hangup-from-transferring")
}
}
for _, callId := range endedCalls {
delete(z.ongoingCalls, callId)
}
return err
}
// ProcessCall processes a single ongoing call from 3CX
func (z *ZammadBridge) ProcessCall(call *CallInformation) error {
if z.isOutboundCall(call) {
call.Direction = "Outbound"
call.AgentNumber = call.CallerNumber
call.AgentName = call.CallerName
call.ExternalNumber = z.ParsePhoneNumber(call.CalleeNumber + " " + call.CalleeName)
call.CallTo = call.ExternalNumber
call.CallFrom = call.AgentNumber
} else if z.isInboundCall(call) {
call.Direction = "Inbound"
call.AgentNumber = call.CalleeNumber
call.AgentName = call.CalleeName
call.ExternalNumber = z.ParsePhoneNumber(call.CallerNumber + " " + call.CallerName)
call.CallTo = call.AgentNumber
call.CallFrom = call.ExternalNumber
} else {
log.Trace().Str("call_id", call.CallUID).Str("direction", call.Direction).Str("from", call.CallFrom).Str("to", call.CallTo).Msg("Call is not relevant")
return nil
}
log.Trace().Str("call_id", call.CallUID).Str("direction", call.Direction).Str("from", call.CallFrom).Str("to", call.CallTo).Msg("Processing call")
if z.isNewCall(call) {
// Save it for the first time
call.CallUID = uuid.New().String()
// Notify all active Zammad clients that someone is calling
log.Info().Str("call_id", call.CallUID).Str("direction", call.Direction).Str("from", call.CallFrom).Str("to", call.CallTo).Msg("New call")
z.LogIfErr(z.ZammadNewCall(call), "new-call")
} else {
// Update call information
previous := z.ongoingCalls[call.Id]
call.CallUID = previous.CallUID
call.ZammadInitialized = previous.ZammadInitialized
call.ZammadAnswered = previous.ZammadAnswered
// If the call is now "Talking", it means we are currently talking to someone. It is with someone of our loaded
// extensions due to the early-return that otherwise would have happened.
// We should then, for once, let Zammad know we answered this call. Since the "Talking" status can be present
// every tick, we need to check if we already notified Zammad and only notify Zammad as-needed.
if call.Status == "Talking" {
if !previous.ZammadAnswered {
log.Info().Str("call_id", call.CallUID).Str("direction", call.Direction).Str("from", call.CallFrom).Str("to", call.CallTo).Msg("Call answered")
z.LogIfErr(z.ZammadAnswer(call), "answer")
}
}
}
z.ongoingCalls[call.Id] = *call
return nil
}
// isInboundCall checks whether the given call is an inbound call.
func (z *ZammadBridge) isInboundCall(call *CallInformation) bool {
if len(call.CallerNumber) != z.Config.Phone3CX.TrunkDigits {
return false
}
if len(call.CalleeNumber) != z.Config.Phone3CX.ExtensionDigits {
return false
}
if !z.Client3CX.IsExtension(call.CalleeNumber) {
return false
}
return true
}
// isOutboundCall checks whether the given call is an outbound call.
func (z *ZammadBridge) isOutboundCall(call *CallInformation) bool {
if len(call.CallerNumber) != z.Config.Phone3CX.ExtensionDigits {
return false
}
if len(call.CalleeNumber) != z.Config.Phone3CX.TrunkDigits {
return false
}
if !z.Client3CX.IsExtension(call.CallerNumber) {
return false
}
return true
}
// isNewCall checks whether the given call is already ongoing and previously detected by the bridge.
func (z *ZammadBridge) isNewCall(call *CallInformation) bool {
_, ok := z.ongoingCalls[call.Id]
return !ok
}
// ParsePhoneNumber parses the phone number into a format acceptable to Zammad
func (z *ZammadBridge) ParsePhoneNumber(number string) string {
// Number is between two brackets, e.g. (0123)
if strings.Contains(number, "(") {
number = number[strings.Index(number, "(")+1 : strings.LastIndex(number, ")")]
}
// If no prefix is configured, we cannot do anything
if z.Config.Phone3CX.CountryPrefix == "" {
return number
}
// If it starts with e.g., +49, we remove that prefix and add a 0 instead
if strings.HasPrefix(number, "+"+z.Config.Phone3CX.CountryPrefix) {
return "0" + number[len(z.Config.Phone3CX.CountryPrefix)+1:]
}
// If it starts with e.g., 0049, we remove that prefix and add a 0 instead
if strings.HasPrefix(number, "00"+z.Config.Phone3CX.CountryPrefix) {
return "0" + number[len(z.Config.Phone3CX.CountryPrefix)+2:]
}
// If it starts with e.g., 49, we remove that prefix and add a 0 instead
if strings.HasPrefix(number, z.Config.Phone3CX.CountryPrefix) {
return "0" + number[len(z.Config.Phone3CX.CountryPrefix):]
}
// Apparently the number doesn't start with any of that, so we assume it is already in the correct format
return number
}
// LogIfErr logs to stderr when an error occurs, doing nothing when err is nil.
func (z *ZammadBridge) LogIfErr(err error, context string) {
if err == nil {
return
}
log.Error().Err(err).Msg(context)
}