├── .gitignore ├── LICENSE ├── README.md ├── ami.go ├── ami_test.go ├── amigo.go ├── amigo_test.go ├── examples └── concurrent │ └── main.go ├── go.mod └── uuid └── uuid.go /.gitignore: -------------------------------------------------------------------------------- 1 | examples/concurrent/concurrent -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Evgeniy Ivakha 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # amigo 2 | Asterisk AMI connector on golang. 3 | 4 | Attention! 5 | API has been changed in v0.1.2. Please read the godoc. 6 | 7 | Usage is pretty simple. 8 | 9 | ## Installation: 10 | 11 | `go get github.com/ivahaev/amigo` 12 | 13 | ## Using 14 | Import module to your project: 15 | ```go 16 | import "github.com/ivahaev/amigo" 17 | ``` 18 | 19 | Then use: 20 | ```go 21 | package main 22 | 23 | import ( 24 | "fmt" 25 | 26 | "github.com/ivahaev/amigo" 27 | ) 28 | 29 | // Creating hanlder functions 30 | func DeviceStateChangeHandler(m map[string]string) { 31 | fmt.Printf("DeviceStateChange event received: %v\n", m) 32 | } 33 | 34 | func DefaultHandler(m map[string]string) { 35 | fmt.Printf("Event received: %v\n", m) 36 | } 37 | 38 | func main() { 39 | fmt.Println("Init Amigo") 40 | 41 | settings := &amigo.Settings{Username: "username", Password: "password", Host: "host"} 42 | a := amigo.New(settings) 43 | 44 | a.Connect() 45 | 46 | // Listen for connection events 47 | a.On("connect", func(message string) { 48 | fmt.Println("Connected", message) 49 | }) 50 | a.On("error", func(message string) { 51 | fmt.Println("Connection error:", message) 52 | }) 53 | 54 | // Registering handler function for event "DeviceStateChange" 55 | a.RegisterHandler("DeviceStateChange", DeviceStateChangeHandler) 56 | 57 | // Registering default handler function for all events. 58 | a.RegisterDefaultHandler(DefaultHandler) 59 | 60 | // Optionally create channel to receiving all events 61 | // and set created channel to receive all events 62 | c := make(chan map[string]string, 100) 63 | a.SetEventChannel(c) 64 | 65 | // Check if connected with Asterisk, will send Action "QueueSummary" 66 | if a.Connected() { 67 | result, err := a.Action(map[string]string{"Action": "QueueSummary", "ActionID": "Init"}) 68 | // If not error, processing result. Response on Action will follow in defined events. 69 | // You need to catch them in event channel, DefaultHandler or specified HandlerFunction 70 | fmt.Println(result, err) 71 | } 72 | 73 | ch := make(chan bool) 74 | <-ch 75 | } 76 | ``` 77 | 78 | ## SIC! 79 | You should not modify received events, because it can be read in another amigo goroutine. 80 | If you need to modify, you should copy all values to another map and modify it. 81 | -------------------------------------------------------------------------------- /ami.go: -------------------------------------------------------------------------------- 1 | package amigo 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "net" 9 | "strconv" 10 | "sync" 11 | "sync/atomic" 12 | "time" 13 | 14 | "github.com/ivahaev/amigo/uuid" 15 | ) 16 | 17 | const ( 18 | pingActionID = "AmigoPing" 19 | amigoConnIDKey = "AmigoConnID" 20 | commandResponseKey = "CommandResponse" 21 | ) 22 | 23 | var ( 24 | actionTimeout = 3 * time.Second 25 | dialTimeout = 10 * time.Second 26 | pingInterval = 5 * time.Second 27 | sequence uint64 28 | ) 29 | 30 | type amiAdapter struct { 31 | id string 32 | eventsChan chan map[string]string 33 | 34 | dialString string 35 | username string 36 | password string 37 | 38 | connected bool 39 | reconnect bool 40 | actionTimeout time.Duration 41 | dialTimeout time.Duration 42 | 43 | actionsChan chan map[string]string 44 | responseChans map[string]chan map[string]string 45 | pingerChan chan struct{} 46 | mutex *sync.RWMutex 47 | emitEvent func(string, string) 48 | stopChan chan struct{} 49 | } 50 | 51 | func newAMIAdapter(s *Settings, eventEmitter func(string, string)) (*amiAdapter, error) { 52 | var a = new(amiAdapter) 53 | a.dialString = fmt.Sprintf("%s:%s", s.Host, s.Port) 54 | a.username = s.Username 55 | a.password = s.Password 56 | a.actionTimeout = s.ActionTimeout 57 | a.dialTimeout = s.DialTimeout 58 | a.mutex = &sync.RWMutex{} 59 | a.emitEvent = eventEmitter 60 | a.reconnect = true 61 | 62 | a.actionsChan = make(chan map[string]string) 63 | a.responseChans = make(map[string]chan map[string]string) 64 | a.eventsChan = make(chan map[string]string, 8192) 65 | a.pingerChan = make(chan struct{}) 66 | a.stopChan = make(chan struct{}) 67 | 68 | go func() { 69 | for { 70 | select { 71 | case <-a.stopChan: 72 | return 73 | default: 74 | } 75 | if !a.reconnect { 76 | break 77 | } 78 | func() { 79 | if !a.reconnect { 80 | return 81 | } 82 | a.id = nextID() 83 | var err error 84 | var conn net.Conn 85 | readErrChan := make(chan error) 86 | writeErrChan := make(chan error) 87 | pingErrChan := make(chan error) 88 | chanStop := make(chan struct{}) 89 | for { 90 | conn, err = a.openConnection() 91 | if err == nil { 92 | defer conn.Close() 93 | greetings := make([]byte, 100) 94 | n, err := conn.Read(greetings) 95 | if err != nil { 96 | go a.emitEvent("error", fmt.Sprintf("Asterisk connection error: %s", err.Error())) 97 | time.Sleep(s.ReconnectInterval) 98 | return 99 | } 100 | 101 | err = a.login(conn) 102 | if err != nil { 103 | go a.emitEvent("error", fmt.Sprintf("Asterisk login error: %s", err.Error())) 104 | time.Sleep(s.ReconnectInterval) 105 | return 106 | } 107 | 108 | if n > 2 { 109 | greetings = greetings[:n-2] 110 | } 111 | 112 | go a.emitEvent("connect", string(greetings)) 113 | break 114 | } 115 | 116 | a.emitEvent("error", "AMI Reconnect failed") 117 | select { 118 | case <-time.After(s.ReconnectInterval): 119 | case <-a.stopChan: 120 | } 121 | return 122 | } 123 | 124 | a.mutex.Lock() 125 | a.connected = true 126 | a.mutex.Unlock() 127 | go a.reader(conn, chanStop, readErrChan) 128 | go a.writer(conn, chanStop, writeErrChan) 129 | if s.Keepalive { 130 | go a.pinger(chanStop, pingErrChan) 131 | } 132 | 133 | select { 134 | case <-a.stopChan: 135 | println("STOPPED") 136 | close(chanStop) 137 | return 138 | case err = <-readErrChan: 139 | case err = <-writeErrChan: 140 | case err = <-pingErrChan: 141 | } 142 | 143 | close(chanStop) 144 | a.mutex.Lock() 145 | a.connected = false 146 | a.mutex.Unlock() 147 | 148 | go a.emitEvent("error", fmt.Sprintf("AMI TCP ERROR: %s", err.Error())) 149 | time.Sleep(s.ReconnectInterval) 150 | }() 151 | } 152 | }() 153 | 154 | return a, nil 155 | } 156 | 157 | func nextID() string { 158 | i := atomic.AddUint64(&sequence, 1) 159 | return strconv.Itoa(int(i)) 160 | } 161 | 162 | func (a *amiAdapter) pinger(stop <-chan struct{}, errChan chan error) { 163 | ticker := time.NewTicker(pingInterval) 164 | defer ticker.Stop() 165 | ping := map[string]string{"Action": "Ping", "ActionID": pingActionID, amigoConnIDKey: a.id} 166 | for { 167 | select { 168 | case <-stop: 169 | return 170 | default: 171 | } 172 | 173 | select { 174 | case <-stop: 175 | return 176 | case <-ticker.C: 177 | } 178 | 179 | if !a.online() { 180 | // when stop chan didn't received before ticker 181 | return 182 | } 183 | 184 | a.actionsChan <- ping 185 | timer := time.NewTimer(3 * time.Second) 186 | select { 187 | case <-a.pingerChan: 188 | timer.Stop() 189 | continue 190 | case <-timer.C: 191 | errChan <- errors.New("ping timeout") 192 | return 193 | } 194 | } 195 | } 196 | 197 | func (a *amiAdapter) writer(conn net.Conn, stop <-chan struct{}, writeErrChan chan error) { 198 | for { 199 | select { 200 | case <-stop: 201 | return 202 | default: 203 | } 204 | 205 | select { 206 | case <-stop: 207 | return 208 | case action := <-a.actionsChan: 209 | if action[amigoConnIDKey] != a.id { 210 | // action sent before reconnect, need to be ignored 211 | continue 212 | } 213 | data := serialize(action) 214 | _, err := conn.Write(data) 215 | if err != nil { 216 | writeErrChan <- err 217 | return 218 | } 219 | } 220 | } 221 | } 222 | 223 | func (a *amiAdapter) distribute(event map[string]string) { 224 | actionID := event["ActionID"] 225 | if actionID == pingActionID { 226 | a.pingerChan <- struct{}{} 227 | return 228 | } 229 | 230 | if len(a.eventsChan) == cap(a.eventsChan) { 231 | a.emitEvent("error", "events chan is full") 232 | } 233 | 234 | // TODO: Need to decide to send or not to send action responses to eventsChan 235 | a.eventsChan <- event 236 | if len(actionID) > 0 { 237 | a.mutex.RLock() 238 | resChan := a.responseChans[actionID] 239 | a.mutex.RUnlock() 240 | if resChan != nil { 241 | a.mutex.Lock() 242 | resChan = a.responseChans[actionID] 243 | if resChan == nil { 244 | a.mutex.Unlock() 245 | return 246 | } 247 | 248 | delete(a.responseChans, actionID) 249 | a.mutex.Unlock() 250 | resChan <- event 251 | } 252 | } 253 | } 254 | 255 | func (a *amiAdapter) exec(action map[string]string) map[string]string { 256 | action[amigoConnIDKey] = a.id 257 | actionID := action["ActionID"] 258 | if actionID == "" { 259 | actionID = uuid.NewV4() 260 | action["ActionID"] = actionID 261 | } 262 | 263 | // TODO: parse multi-message response 264 | resChan := make(chan map[string]string) 265 | a.mutex.Lock() 266 | a.responseChans[actionID] = resChan 267 | a.mutex.Unlock() 268 | 269 | a.actionsChan <- action 270 | 271 | time.AfterFunc(a.actionTimeout, func() { 272 | a.mutex.RLock() 273 | _, ok := a.responseChans[actionID] 274 | a.mutex.RUnlock() 275 | if ok { 276 | a.mutex.Lock() 277 | if ch, ok := a.responseChans[actionID]; ok { 278 | delete(a.responseChans, actionID) 279 | a.mutex.Unlock() 280 | ch <- map[string]string{"Error": "Timeout"} 281 | return 282 | } 283 | a.mutex.Unlock() 284 | } 285 | }) 286 | 287 | response := <-resChan 288 | 289 | return response 290 | } 291 | 292 | func (a *amiAdapter) login(conn net.Conn) error { 293 | var action = map[string]string{ 294 | "Action": "Login", 295 | "Username": a.username, 296 | "Secret": a.password, 297 | } 298 | 299 | serialized := serialize(action) 300 | _, err := conn.Write(serialized) 301 | if err != nil { 302 | return err 303 | } 304 | 305 | reader := bufio.NewReader(conn) 306 | result, err := readMessage(reader) 307 | if err != nil { 308 | return err 309 | } 310 | 311 | if result["Response"] != "Success" && result["Message"] != "Authentication accepted" { 312 | return errors.New(result["Message"]) 313 | } 314 | 315 | return nil 316 | } 317 | 318 | func (a *amiAdapter) online() bool { 319 | a.mutex.RLock() 320 | defer a.mutex.RUnlock() 321 | return a.connected 322 | } 323 | 324 | func (a *amiAdapter) openConnection() (net.Conn, error) { 325 | return net.DialTimeout("tcp", a.dialString, a.dialTimeout) 326 | } 327 | 328 | func readMessage(r *bufio.Reader) (m map[string]string, err error) { 329 | m = make(map[string]string) 330 | var responseFollows bool 331 | var outputExist = false 332 | var completeLine bytes.Buffer // Buffer to hold incomplete lines 333 | 334 | for { 335 | tmpkv, isprefix, err := r.ReadLine() 336 | if err != nil { 337 | return m, err 338 | } 339 | if len(tmpkv) == 0 { 340 | return m, err 341 | } 342 | 343 | // Append the current line to the complete line buffer 344 | completeLine.Write(tmpkv) 345 | 346 | if isprefix { 347 | // If the line is a prefix, continue reading more 348 | continue 349 | } 350 | 351 | // We have a complete line now 352 | kv := completeLine.Bytes() 353 | completeLine.Reset() // Reset the buffer for the next line 354 | 355 | var key string 356 | i := bytes.IndexByte(kv, ':') 357 | if i >= 0 { 358 | endKey := i 359 | for endKey > 0 && kv[endKey-1] == ' ' { 360 | endKey-- 361 | } 362 | key = string(kv[:endKey]) 363 | } 364 | 365 | if key == "" && !responseFollows { 366 | continue 367 | } 368 | 369 | if responseFollows && key != "Privilege" && key != "ActionID" { 370 | if string(kv) != "--END COMMAND--" { 371 | if len(m[commandResponseKey]) == 0 { 372 | m[commandResponseKey] = string(kv) 373 | } else { 374 | m[commandResponseKey] = fmt.Sprintf("%s\n%s", m[commandResponseKey], string(kv)) 375 | } 376 | } 377 | continue 378 | } 379 | 380 | i++ 381 | for i < len(kv) && (kv[i] == ' ' || kv[i] == '\t') { 382 | i++ 383 | } 384 | value := string(kv[i:]) 385 | 386 | if key == "Response" && value == "Follows" { 387 | responseFollows = true 388 | } 389 | 390 | if key == "Output" && !outputExist { 391 | m["RealOutput"] = value 392 | outputExist = true 393 | } else { 394 | m[key] = value 395 | } 396 | } 397 | } 398 | 399 | func serialize(data map[string]string) []byte { 400 | var outBuf bytes.Buffer 401 | 402 | for key := range data { 403 | outBuf.WriteString(key) 404 | outBuf.WriteString(": ") 405 | outBuf.WriteString(data[key]) 406 | outBuf.WriteString("\r\n") 407 | } 408 | outBuf.WriteString("\r\n") 409 | 410 | return outBuf.Bytes() 411 | } 412 | 413 | func (a *amiAdapter) reader(conn net.Conn, stop <-chan struct{}, readErrChan chan error) { 414 | chanErr := make(chan error) 415 | chanEvents := make(chan map[string]string) 416 | go func() { 417 | bufReader := bufio.NewReaderSize(conn, 8192) 418 | for i := 0; ; i++ { 419 | var event map[string]string 420 | var err error 421 | event, err = readMessage(bufReader) 422 | if err != nil { 423 | chanErr <- err 424 | return 425 | } 426 | 427 | event["#"] = strconv.Itoa(i) 428 | event["TimeReceived"] = time.Now().Format(time.RFC3339Nano) 429 | chanEvents <- event 430 | } 431 | }() 432 | 433 | for { 434 | select { 435 | case <-stop: 436 | return 437 | default: 438 | } 439 | 440 | select { 441 | case <-stop: 442 | return 443 | case err := <-chanErr: 444 | readErrChan <- err 445 | case event := <-chanEvents: 446 | a.distribute(event) 447 | } 448 | } 449 | } 450 | -------------------------------------------------------------------------------- /ami_test.go: -------------------------------------------------------------------------------- 1 | package amigo 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "io" 7 | "strconv" 8 | "testing" 9 | ) 10 | 11 | var messagePairs = []struct { 12 | message string 13 | expected map[string]string 14 | err error 15 | }{ 16 | { 17 | `Event: EndpointList 18 | ObjectType: endpoint 19 | ObjectName: XXXXX 20 | Transport: transport-udp 21 | Aor: XXX 22 | Auths: 23 | OutboundAuths : looong untrimed value 24 | Contacts: XXXX/sip:XXXX 25 | DeviceState: Unavailable 26 | ActiveChannels: 27 | 28 | `, 29 | map[string]string{ 30 | "Event": "EndpointList", 31 | "ObjectType": "endpoint", 32 | "ObjectName": "XXXXX", 33 | "Transport": "transport-udp", 34 | "Aor": "XXX", 35 | "Auth": "", 36 | "OutboundAuths": "looong untrimed value", 37 | "Contacts": "XXXX/sip:XXXX", 38 | "DeviceState": "Unavailable", 39 | "ActiveChannels": "", 40 | }, 41 | nil, 42 | }, 43 | { 44 | `Response: Follows 45 | Privilege: Command 46 | No such command 'core show hi' (type 'core show help core show hi' for other possible commands) 47 | 48 | `, 49 | map[string]string{ 50 | "Response": "Follows", 51 | "Privilege": "Command", 52 | "CommandResponse": "No such command 'core show hi' (type 'core show help core show hi' for other possible commands)", 53 | }, 54 | nil, 55 | }, 56 | { 57 | `Response: Follows 58 | Privilege: Command 59 | No such command 'core show hi' (type 'core show help core show hi' for other possible commands) 60 | --END COMMAND-- 61 | 62 | `, 63 | map[string]string{ 64 | "Response": "Follows", 65 | "Privilege": "Command", 66 | "CommandResponse": "No such command 'core show hi' (type 'core show help core show hi' for other possible commands)", 67 | }, 68 | nil, 69 | }, 70 | { 71 | `Response: Follows 72 | Privilege: Command 73 | No such command 'core show hi' 74 | (type 'core show help core show hi' for other possible commands) 75 | --END COMMAND-- 76 | `, 77 | map[string]string{ 78 | "Response": "Follows", 79 | "Privilege": "Command", 80 | "CommandResponse": "No such command 'core show hi'\n(type 'core show help core show hi' for other possible commands)", 81 | }, 82 | io.EOF, 83 | }, 84 | } 85 | 86 | func TestReadMessage(t *testing.T) { 87 | for i, pair := range messagePairs { 88 | t.Run("pair "+strconv.Itoa(i), func(t *testing.T) { 89 | buf := bytes.NewBuffer([]byte(pair.message)) 90 | reader := bufio.NewReader(buf) 91 | message, err := readMessage(reader) 92 | if err != pair.err { 93 | t.Fatalf("readMessage error mismatched. Expected '%v', got '%v'", pair.err, err) 94 | } 95 | for k, v := range message { 96 | if pair.expected[k] != v { 97 | t.Fatalf("readMessage error. Key '%s', Expected value '%s', got '%s'", k, pair.expected[k], v) 98 | } 99 | } 100 | }) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /amigo.go: -------------------------------------------------------------------------------- 1 | package amigo 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | "sync" 8 | "time" 9 | 10 | "github.com/ivahaev/amigo/uuid" 11 | ) 12 | 13 | var ( 14 | version = "0.1.9" 15 | 16 | // TODO: implement function to clear old data in handlers. 17 | agiCommandsHandlers = make(map[string]agiCommand) 18 | agiCommandsMutex = &sync.Mutex{} 19 | errNotConnected = errors.New("Not connected to Asterisk") 20 | ) 21 | 22 | type handlerFunc func(map[string]string) 23 | type eventHandlerFunc func(string) 24 | 25 | // Amigo is a main package struct 26 | type Amigo struct { 27 | settings *Settings 28 | ami *amiAdapter 29 | defaultChannel chan map[string]string 30 | defaultHandler handlerFunc 31 | handlers map[string]handlerFunc 32 | eventHandlers map[string][]eventHandlerFunc 33 | capitalizeProps bool 34 | connectCalled bool 35 | mutex *sync.RWMutex 36 | handlerMutex *sync.RWMutex 37 | } 38 | 39 | // Settings represents connection settings for Amigo. 40 | // Default: 41 | // Username = admin, 42 | // Password = amp111, 43 | // Host = 127.0.0.1, 44 | // Port = 5038, 45 | // ActionTimeout = 3s 46 | // DialTimeout = 10s 47 | type Settings struct { 48 | Username string 49 | Password string 50 | Host string 51 | Port string 52 | ActionTimeout time.Duration 53 | DialTimeout time.Duration 54 | ReconnectInterval time.Duration 55 | Keepalive bool 56 | } 57 | 58 | type agiCommand struct { 59 | c chan map[string]string 60 | dateTime time.Time 61 | } 62 | 63 | // New creates new Amigo struct with credentials provided and returns pointer to it 64 | // Usage: New(username string, secret string, [host string, [port string]]) 65 | func New(settings *Settings) *Amigo { 66 | prepareSettings(settings) 67 | 68 | var ami *amiAdapter 69 | return &Amigo{ 70 | settings: settings, 71 | ami: ami, 72 | handlers: map[string]handlerFunc{}, 73 | eventHandlers: map[string][]eventHandlerFunc{}, 74 | mutex: &sync.RWMutex{}, 75 | handlerMutex: &sync.RWMutex{}, 76 | } 77 | } 78 | 79 | // CapitalizeProps used to capitalise all prop's names when true provided. 80 | func (a *Amigo) CapitalizeProps(c bool) { 81 | a.capitalizeProps = c 82 | } 83 | 84 | // Action used to execute Actions in Asterisk. Returns immediately response from asterisk. Full response will follow. 85 | // Usage amigo.Action(action map[string]string) 86 | func (a *Amigo) Action(action map[string]string) (map[string]string, error) { 87 | if !a.Connected() { 88 | return nil, errNotConnected 89 | } 90 | 91 | a.mutex.Lock() 92 | defer a.mutex.Unlock() 93 | result := a.ami.exec(action) 94 | if a.capitalizeProps { 95 | e := map[string]string{} 96 | for k, v := range result { 97 | e[strings.ToUpper(k)] = v 98 | } 99 | return e, nil 100 | } 101 | 102 | if strings.ToLower(action["Action"]) == "logoff" { 103 | a.ami.reconnect = false 104 | } 105 | return result, nil 106 | } 107 | 108 | // AgiAction used to execute Agi Actions in Asterisk. Returns full response. 109 | // Usage amigo.AgiAction(channel, command string) 110 | func (a *Amigo) AgiAction(channel, command string) (map[string]string, error) { 111 | if !a.Connected() { 112 | return nil, errNotConnected 113 | } 114 | 115 | commandID := uuid.NewV4() 116 | action := map[string]string{ 117 | "Action": "AGI", 118 | "Channel": channel, 119 | "Command": command, 120 | "CommandID": commandID, 121 | } 122 | 123 | ac := agiCommand{make(chan map[string]string), time.Now()} 124 | agiCommandsMutex.Lock() 125 | agiCommandsHandlers[commandID] = ac 126 | agiCommandsMutex.Unlock() 127 | 128 | a.mutex.Lock() 129 | result := a.ami.exec(action) 130 | a.mutex.Unlock() 131 | if result["Response"] != "Success" { 132 | return result, errors.New("Fail with command") 133 | } 134 | result = <-ac.c 135 | delete(result, "CommandID") 136 | if a.capitalizeProps { 137 | for k, v := range result { 138 | result[strings.ToUpper(k)] = v 139 | delete(result, k) 140 | } 141 | } 142 | 143 | return result, nil 144 | } 145 | 146 | // Connect with Asterisk. 147 | // If connect fails, will try to reconnect every second. 148 | func (a *Amigo) Connect() { 149 | var connectCalled bool 150 | a.mutex.RLock() 151 | connectCalled = a.connectCalled 152 | a.mutex.RUnlock() 153 | if connectCalled { 154 | return 155 | } 156 | 157 | a.mutex.Lock() 158 | a.connectCalled = true 159 | a.mutex.Unlock() 160 | 161 | for { 162 | am, err := newAMIAdapter(a.settings, a.emitEvent) 163 | if err != nil { 164 | go a.emitEvent("error", fmt.Sprintf("AMI Connect error: %s", err.Error())) 165 | } else { 166 | a.mutex.Lock() 167 | a.ami = am 168 | a.mutex.Unlock() 169 | break 170 | } 171 | time.Sleep(time.Second) 172 | } 173 | 174 | go func() { 175 | for { 176 | select { 177 | case <-a.ami.stopChan: 178 | return 179 | case e := <-a.ami.eventsChan: 180 | a.handlerMutex.RLock() 181 | 182 | if a.defaultChannel != nil { 183 | a.defaultChannel <- e 184 | } 185 | 186 | var event = strings.ToUpper(e["Event"]) 187 | if len(event) != 0 && (a.handlers[event] != nil || a.defaultHandler != nil) { 188 | if a.capitalizeProps { 189 | ev := map[string]string{} 190 | for k, v := range e { 191 | ev[strings.ToUpper(k)] = v 192 | } 193 | 194 | if a.handlers[event] != nil { 195 | a.handlers[event](ev) 196 | } 197 | 198 | if a.defaultHandler != nil { 199 | a.defaultHandler(ev) 200 | } 201 | } else { 202 | if a.defaultHandler != nil { 203 | a.defaultHandler(e) 204 | } 205 | 206 | if a.handlers[event] != nil { 207 | a.handlers[event](e) 208 | } 209 | } 210 | } 211 | 212 | if event == "ASYNCAGI" { 213 | commandID, ok := e["CommandID"] 214 | if !ok { 215 | a.handlerMutex.RUnlock() 216 | continue 217 | } 218 | agiCommandsMutex.Lock() 219 | ac, ok := agiCommandsHandlers[commandID] 220 | if ok { 221 | delete(agiCommandsHandlers, commandID) 222 | agiCommandsMutex.Unlock() 223 | ac.c <- e 224 | } else { 225 | agiCommandsMutex.Unlock() 226 | } 227 | } 228 | 229 | a.handlerMutex.RUnlock() 230 | } 231 | } 232 | }() 233 | 234 | } 235 | 236 | func (a *Amigo) Close() { 237 | if a == nil { 238 | return 239 | } 240 | select { 241 | case <-a.ami.stopChan: 242 | default: 243 | a.ami.reconnect = false 244 | close(a.ami.stopChan) 245 | } 246 | } 247 | 248 | // Connected returns true if successfully connected and logged in Asterisk and false otherwise. 249 | func (a *Amigo) Connected() bool { 250 | a.mutex.RLock() 251 | defer a.mutex.RUnlock() 252 | return a.ami != nil && a.ami.online() 253 | } 254 | 255 | // On register handler for package events. Now amigo will emit two types of events: 256 | // "connect" fired on connection success and "error" on any error occured. 257 | func (a *Amigo) On(event string, handler func(string)) { 258 | a.handlerMutex.Lock() 259 | defer a.handlerMutex.Unlock() 260 | 261 | if _, ok := a.eventHandlers[event]; !ok { 262 | a.eventHandlers[event] = []eventHandlerFunc{} 263 | } 264 | a.eventHandlers[event] = append(a.eventHandlers[event], handler) 265 | } 266 | 267 | // RegisterDefaultHandler registers handler function that will called on each event 268 | func (a *Amigo) RegisterDefaultHandler(f handlerFunc) error { 269 | a.handlerMutex.Lock() 270 | defer a.handlerMutex.Unlock() 271 | 272 | if a.defaultHandler != nil { 273 | return errors.New("DefaultHandler already registered") 274 | } 275 | a.defaultHandler = f 276 | return nil 277 | } 278 | 279 | // RegisterHandler registers handler function for provided event name 280 | func (a *Amigo) RegisterHandler(event string, f handlerFunc) error { 281 | event = strings.ToUpper(event) 282 | a.handlerMutex.Lock() 283 | defer a.handlerMutex.Unlock() 284 | if a.handlers[event] != nil { 285 | return errors.New("Handler already registered") 286 | } 287 | a.handlers[event] = f 288 | return nil 289 | } 290 | 291 | // SetEventChannel sets channel for receiving all events 292 | func (a *Amigo) SetEventChannel(c chan map[string]string) { 293 | a.handlerMutex.Lock() 294 | defer a.handlerMutex.Unlock() 295 | a.defaultChannel = c 296 | } 297 | 298 | // UnregisterDefaultHandler removes default handler function 299 | func (a *Amigo) UnregisterDefaultHandler(f handlerFunc) error { 300 | a.handlerMutex.Lock() 301 | defer a.handlerMutex.Unlock() 302 | if a.defaultHandler == nil { 303 | return errors.New("DefaultHandler not registered") 304 | } 305 | a.defaultHandler = nil 306 | return nil 307 | } 308 | 309 | // EventsChanLength returns the current size of eventsChan 310 | func (a *Amigo) EventsChanLength() int { 311 | return len(a.ami.eventsChan) 312 | } 313 | 314 | // UnregisterHandler removes handler function for provided event name 315 | func (a *Amigo) UnregisterHandler(event string, f handlerFunc) error { 316 | event = strings.ToUpper(event) 317 | a.handlerMutex.Lock() 318 | defer a.handlerMutex.Unlock() 319 | if a.handlers[event] == nil { 320 | return errors.New("Handler not registered") 321 | } 322 | a.handlers[event] = nil 323 | return nil 324 | } 325 | 326 | func (a *Amigo) emitEvent(name, message string) { 327 | a.handlerMutex.RLock() 328 | defer a.handlerMutex.RUnlock() 329 | 330 | if len(a.eventHandlers) == 0 { 331 | return 332 | } 333 | 334 | handlers, ok := a.eventHandlers[name] 335 | if !ok { 336 | return 337 | } 338 | 339 | for _, h := range handlers { 340 | h(message) 341 | } 342 | } 343 | 344 | func prepareSettings(settings *Settings) { 345 | if settings.Username == "" { 346 | settings.Username = "admin" 347 | } 348 | if settings.Password == "" { 349 | settings.Password = "amp111" 350 | } 351 | if settings.Host == "" { 352 | settings.Host = "127.0.0.1" 353 | } 354 | if settings.Port == "" { 355 | settings.Port = "5038" 356 | } 357 | if settings.ActionTimeout == 0 { 358 | settings.ActionTimeout = actionTimeout 359 | } 360 | if settings.DialTimeout == 0 { 361 | settings.DialTimeout = dialTimeout 362 | } 363 | if settings.ReconnectInterval == 0 { 364 | settings.ReconnectInterval = time.Second 365 | } 366 | } 367 | -------------------------------------------------------------------------------- /amigo_test.go: -------------------------------------------------------------------------------- 1 | package amigo 2 | 3 | import ( 4 | "runtime" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestAmigo(t *testing.T) { 10 | t.Run("#New", func(t *testing.T) { 11 | t.Run("should return pointer to a new Amigo struct with username and password filled and default host and port settings", func(t *testing.T) { 12 | a := New(&Settings{Username: "username", Password: "secret"}) 13 | if a.settings.Username != "username" { 14 | t.Fatal("username mismatched") 15 | } 16 | if a.settings.Password != "secret" { 17 | t.Fatal("secret mismatched") 18 | } 19 | }) 20 | t.Run("should return pointer to a new Amigo struct with username and password filled and provided host and default port settings", func(t *testing.T) { 21 | a := New(&Settings{Username: "username", Password: "secret", Host: "amigo"}) 22 | if a.settings.Username != "username" { 23 | t.Fatal("username mismatched") 24 | } 25 | if a.settings.Password != "secret" { 26 | t.Fatal("secret mismatched") 27 | } 28 | if a.settings.Host != "amigo" { 29 | t.Fatal("host mismatched") 30 | } 31 | }) 32 | }) 33 | } 34 | 35 | func TestAmigoClose(t *testing.T) { 36 | a := New(&Settings{Username: "username", Password: "secret"}) 37 | a.Connect() 38 | a.Close() 39 | a.Close() 40 | 41 | time.Sleep(time.Second * 1) 42 | routines := runtime.NumGoroutine() 43 | if routines > 2 { 44 | t.Fatalf("too many go routines, expected 2 got %d", routines) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/concurrent/main.go: -------------------------------------------------------------------------------- 1 | // Created by ivahaev at 2017-06-25 2 | // 3 | // This example demonstrates how you can handle events in goroutines. 4 | // Only three events handled in this example — Dial.Begin, Dial.End and Hangup. 5 | // On each call we create new channel and start new goroutine with channel reader. 6 | // On Hangup event we close channel and it kills created goroutine. 7 | // Also it demonstrates how we can expose AMI actions (func setVar) 8 | 9 | package main 10 | 11 | import ( 12 | "fmt" 13 | "os" 14 | "time" 15 | 16 | "github.com/ivahaev/amigo" 17 | ) 18 | 19 | var a *amigo.Amigo 20 | 21 | func setVar(channel, variable, value string) error { 22 | _, err := a.Action(map[string]string{"Action": "SetVar", "Channel": channel, "Variable": variable, "Value": value}) 23 | 24 | return err 25 | } 26 | 27 | func dialBeginHandler(e map[string]string) { 28 | fmt.Println("DialBegin event: ", e) 29 | // emulating long handling 30 | time.Sleep(5 * time.Second) 31 | } 32 | func dialEndHandler(e map[string]string) { 33 | fmt.Println("DialEnd event: ", e) 34 | // emulating very long handling 35 | time.Sleep(10 * time.Second) 36 | } 37 | func hangupHandler(e map[string]string) { 38 | fmt.Println("Hangup event: ", e) 39 | } 40 | 41 | func eventsProcessor(ch <-chan map[string]string) { 42 | for e := range ch { 43 | switch e["Event"] { 44 | case "Dial": 45 | switch e["SubEvent"] { 46 | case "Begin": 47 | dialBeginHandler(e) 48 | case "End": 49 | dialEndHandler(e) 50 | default: 51 | fmt.Printf("Unknown Dial SubEvent: %s\n", e["SubEvent"]) 52 | } 53 | case "DialBegin": 54 | dialBeginHandler(e) 55 | case "DialEnd": 56 | dialEndHandler(e) 57 | case "Hangup": 58 | hangupHandler(e) 59 | } 60 | } 61 | } 62 | 63 | func main() { 64 | settings := &amigo.Settings{Host: "127.0.0.1", Port: "5038", Username: "admin", Password: "amp111"} 65 | if e := os.Getenv("AMIGO_HOST"); len(e) > 0 { 66 | settings.Host = e 67 | } 68 | if e := os.Getenv("AMIGO_PORT"); len(e) > 0 { 69 | settings.Port = e 70 | } 71 | if e := os.Getenv("AMIGO_USERNAME"); len(e) > 0 { 72 | settings.Username = e 73 | } 74 | if e := os.Getenv("AMIGO_PASSWORD"); len(e) > 0 { 75 | settings.Password = e 76 | } 77 | 78 | a = amigo.New(settings) 79 | a.On("connect", func(message string) { 80 | fmt.Println("Connected", message) 81 | }) 82 | a.On("error", func(message string) { 83 | fmt.Println("Connection error:", message) 84 | }) 85 | 86 | a.Connect() 87 | 88 | chans := map[string]chan map[string]string{} 89 | c := make(chan map[string]string, 100) 90 | a.SetEventChannel(c) 91 | 92 | for e := range c { 93 | uniqueID := e["UniqueID"] 94 | if len(uniqueID) == 0 { 95 | uniqueID = e["Uniqueid"] 96 | } 97 | 98 | switch e["Event"] { 99 | case "Dial": 100 | { 101 | if ch, ok := chans[uniqueID]; ok { 102 | ch <- e 103 | continue 104 | } 105 | 106 | ch := make(chan map[string]string, 3) // capacity is 3 because only 3 events will handled 107 | ch <- e 108 | chans[uniqueID] = ch 109 | go eventsProcessor(ch) 110 | } 111 | case "DialBegin": // asterisk 13 112 | { 113 | ch := make(chan map[string]string, 3) 114 | ch <- e 115 | chans[uniqueID] = ch 116 | go eventsProcessor(ch) 117 | } 118 | case "DialEnd": // asterisk 13 119 | if ch, ok := chans[uniqueID]; ok { 120 | ch <- e 121 | continue 122 | } 123 | 124 | fmt.Printf("Unknown UniqueID on DialEnd event: %s\n", uniqueID) 125 | case "Hangup": 126 | if ch, ok := chans[uniqueID]; ok { 127 | ch <- e 128 | close(ch) 129 | delete(chans, uniqueID) 130 | continue 131 | } 132 | 133 | fmt.Printf("Unknown UniqueID on Hangup event: %s\n", uniqueID) 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ivahaev/amigo 2 | 3 | go 1.13 4 | -------------------------------------------------------------------------------- /uuid/uuid.go: -------------------------------------------------------------------------------- 1 | package uuid 2 | 3 | import ( 4 | "crypto/rand" 5 | "fmt" 6 | ) 7 | 8 | // Returns a new uuid v4 9 | func NewV4() string { 10 | u := [16]byte{} 11 | _, err := rand.Read(u[:16]) 12 | if err != nil { 13 | panic(err) 14 | } 15 | 16 | u[8] = (u[8] | 0x80) & 0xBf 17 | u[6] = (u[6] | 0x40) & 0x4f 18 | 19 | return fmt.Sprintf("%x-%x-%x-%x-%x", u[:4], u[4:6], u[6:8], u[8:10], u[10:]) 20 | } --------------------------------------------------------------------------------