Skip to content

Commit 9577570

Browse files
committed
initial commit
0 parents  commit 9577570

18 files changed

+1897
-0
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.DS_Store
2+
.vscode
3+
.env
4+
*.log
5+
*.out

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2024 Dmitry Sedykh <sedykh@gmail.com>
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.

README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# ESL
2+
3+
Another library for working with the FreeSWITCH server on Golang.
4+
5+
In this version, only the client connection is supported, and the outbound connection has not yet
6+
been implemented, because it simply was not necessary.
7+
8+
```golang
9+
// initialize buffered events channel
10+
events := make(chan esl.Event, 1)
11+
// read events
12+
go func() {
13+
for ev := range events {
14+
fmt.Println(ev.Name(), ev.Get("Job-UUID"))
15+
}
16+
}()
17+
18+
// connect to FreeSWITCH & init events channel with auto-close flag
19+
client, err := esl.Connect("10.10.61.76", "ClueCon",
20+
esl.WithEvents(events, true))
21+
if err != nil {
22+
panic(err)
23+
}
24+
defer client.Close()
25+
26+
// send a command
27+
msg, err := client.API("show calls count")
28+
if err != nil {
29+
panic(err)
30+
}
31+
fmt.Println(msg)
32+
33+
// subscribe to BACKGROUND_JOB events
34+
if err = client.Subscribe("BACKGROUND_JOB"); err != nil {
35+
panic(err)
36+
}
37+
38+
// send a background command
39+
if err = client.JobWithID("uptime s", "test-xxx"); err != nil {
40+
panic(err)
41+
}
42+
```

client.go

Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
package esl
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"io"
7+
"log/slog"
8+
"net"
9+
"runtime"
10+
"time"
11+
)
12+
13+
// spell-checker:words myevents bgapi noevents nixevent sendevent
14+
15+
type Client struct {
16+
conn *conn
17+
chErr chan error
18+
chResp chan response
19+
closer io.Closer
20+
}
21+
22+
// Default timeout options.
23+
var (
24+
DialTimeout = time.Second * 5
25+
AuthTimeout = time.Second * 2
26+
)
27+
28+
// Connect connects to the given address with an optional password and options.
29+
//
30+
// The address should include the host and port. If the port is missing, the default port 8021 will be used.
31+
// The password is optional and can be empty.
32+
// The options are variadic and can be used to customize the connection.
33+
//
34+
// Returns a new Client and an error if there was a failure in connecting.
35+
func Connect(addr, password string, opts ...Option) (*Client, error) {
36+
// If the address doesn't contain a port, use the default port
37+
if _, _, err := net.SplitHostPort(addr); err != nil {
38+
var addrErr *net.AddrError
39+
if !errors.As(err, &addrErr) || addrErr.Err != "missing port in address" {
40+
return nil, fmt.Errorf("bad address: %w", err)
41+
}
42+
43+
const defaultPort = "8021"
44+
addr = net.JoinHostPort(addr, defaultPort)
45+
}
46+
47+
conn, err := net.DialTimeout("tcp", addr, DialTimeout)
48+
if err != nil {
49+
return nil, fmt.Errorf("failed to dial: %w", err)
50+
}
51+
52+
return NewClient(conn, password, opts...)
53+
}
54+
55+
// NewClient creates a new Client instance.
56+
func NewClient(rwc io.ReadWriteCloser, password string, opts ...Option) (*Client, error) {
57+
cfg := getConfig(opts...)
58+
59+
conn := newConn(cfg.dumper(rwc), cfg.log)
60+
61+
if err := conn.AuthTimeout(password, AuthTimeout); err != nil {
62+
rwc.Close()
63+
return nil, fmt.Errorf("failed to auth: %w", err)
64+
}
65+
66+
client := &Client{
67+
conn: conn,
68+
chErr: make(chan error, 1),
69+
chResp: make(chan response),
70+
closer: rwc,
71+
}
72+
73+
go client.runReader(cfg.events, cfg.autoClose)
74+
runtime.Gosched()
75+
76+
return client, nil
77+
}
78+
79+
// Close closes the client connection.
80+
func (c *Client) Close() error {
81+
c.sendRecv(cmd("exit")) //nolint:errcheck // ignore send error
82+
return c.closer.Close()
83+
}
84+
85+
// API sends a command to the API and returns the response body or an error.
86+
//
87+
// Send a FreeSWITCH API command, blocking mode. That is, the FreeSWITCH
88+
// instance won't accept any new commands until the api command finished execution.
89+
func (c *Client) API(command string) (string, error) {
90+
resp, err := c.sendRecv(cmd("api", command))
91+
if err != nil {
92+
return "", err
93+
}
94+
95+
return resp.Body(), nil
96+
}
97+
98+
// Job sends a background command and returns the job-ID.
99+
//
100+
// Send a FreeSWITCH API command, non-blocking mode. This will let you execute a job
101+
// in the background.
102+
//
103+
// The same API commands available as with the api command, however the server
104+
// returns immediately and is available for processing more commands.
105+
//
106+
// When the command is done executing, FreeSWITCH fires an event with the result
107+
// and you can compare that to the Job-UUID to see what the result was. In order
108+
// to receive this event, you will need to subscribe to BACKGROUND_JOB events.
109+
func (c *Client) Job(command string) (id string, err error) {
110+
resp, err := c.sendRecv(cmd("bgapi", command))
111+
if err != nil {
112+
return "", err
113+
}
114+
115+
return resp.JobUUID(), nil
116+
}
117+
118+
// JobWithID sends a background command with a specified ID.
119+
//
120+
// Send a FreeSWITCH API command, non-blocking mode. This will let you execute a job
121+
// in the background, and the result will be sent as an event with an indicated UUID
122+
// to match the reply to the command.
123+
//
124+
// When the command is done executing, FreeSWITCH fires an event with the result
125+
// and you can compare that to the Job-UUID to see what the result was. In order
126+
// to receive this event, you will need to subscribe to BACKGROUND_JOB events.
127+
func (c *Client) JobWithID(command, id string) error {
128+
_, err := c.sendRecv(cmd("bgapi", command).WithJobUUID(id))
129+
return err
130+
}
131+
132+
// Subscribe is a function that subscribes the client to events with the given names.
133+
//
134+
// You may specify any number events on the same line that should be separated with spaces.
135+
//
136+
// Subsequent calls to event won't override the previous event sets.
137+
func (c *Client) Subscribe(names ...string) error {
138+
cmdNames := buildEventNamesCmd(names...)
139+
_, err := c.sendRecv(cmd("event", cmdNames))
140+
return err
141+
}
142+
143+
// Unsubscribe unsubscribes the client from one or more events.
144+
//
145+
// Suppress the specified type of event.
146+
// If name is empty then all events will be suppressed.
147+
func (c *Client) Unsubscribe(names ...string) (err error) {
148+
cmdNames := buildEventNamesCmd(names...)
149+
if cmdNames == eventAll {
150+
_, err = c.sendRecv(cmd("noevents"))
151+
} else {
152+
_, err = c.sendRecv(cmd("nixevent", cmdNames))
153+
}
154+
155+
return err
156+
}
157+
158+
// Filter performs a filter operation on the Client.
159+
//
160+
// Specify event types to listen for. Note, this is not a filter out but rather
161+
// a "filter in," that is, when a filter is applied only the filtered values are received.
162+
// Multiple filters on a socket connection are allowed.
163+
//
164+
// You can filter on any of the event headers. To filter for a specific channel
165+
// you will need to use the uuid:
166+
//
167+
// filter Unique-ID d29a070f-40ff-43d8-8b9d-d369b2389dfe
168+
//
169+
// This method is an alternative to the myevents event type. If you need only
170+
// the events for a specific channel then use myevents, otherwise use a combination
171+
// of filters to narrow down the events you wish to receive on the socket.
172+
//
173+
// To filter multiple unique IDs, you can just add another filter for events for
174+
// each UUID. This can be useful for example if you want to receive start/stop-talking
175+
// events for multiple users on a particular conference.
176+
func (c *Client) Filter(eventHeader, valueToFilter string) error {
177+
_, err := c.sendRecv(cmd("filter", eventHeader, valueToFilter))
178+
return err
179+
}
180+
181+
// FilterDelete removes a filter from the Client.
182+
//
183+
// Specify the events which you want to revoke the filter.
184+
// filter delete can be used when some filters are applied wrongly or when there
185+
// is no use of the filter.
186+
func (c *Client) FilterDelete(eventHeader, valueToFilter string) error {
187+
_, err := c.sendRecv(cmd("filter delete", eventHeader, valueToFilter))
188+
return err
189+
}
190+
191+
// The 'myevents' subscription allows your inbound socket connection to behave
192+
// like an outbound socket connect. It will "lock on" to the events for a particular
193+
// uuid and will ignore all other events, closing the socket when the channel goes
194+
// away or closing the channel when the socket disconnects and all applications
195+
// have finished executing.
196+
//
197+
// Once the socket connection has locked on to the events for this particular
198+
// uuid it will NEVER see any events that are not related to the channel, even
199+
// if subsequent event commands are sent. If you need to monitor a specific
200+
// channel/uuid and you need watch for other events as well then it is best to
201+
// use a filter.
202+
func (c *Client) MyEvent(uuid string) error {
203+
_, err := c.sendRecv(cmd("myevents", uuid))
204+
return err
205+
}
206+
207+
// spell-checker:words inputcallback gtalk
208+
209+
// The divert_events switch is available to allow events that an embedded script
210+
// would expect to get in the inputcallback to be diverted to the event socket.
211+
//
212+
// An inputcallback can be registered in an embedded script using setInputCallback().
213+
// Setting divert_events to "on" can be used for chat messages like gtalk channel,
214+
// ASR events and others.
215+
func (c *Client) DivertEvents(on ...bool) error {
216+
val := "off"
217+
if len(on) > 0 && on[0] {
218+
val = "on"
219+
}
220+
221+
_, err := c.sendRecv(cmd("divert_events", val))
222+
return err
223+
}
224+
225+
// Send an event into the event system.
226+
func (c *Client) SendEvent(name string, headers map[string]string, body string) error {
227+
_, err := c.sendRecv(
228+
cmd("sendevent", name).WithMessage(headers, body))
229+
return err
230+
}
231+
232+
// SendMsg is used to control the behavior of FreeSWITCH. UUID is mandatory,
233+
// and it refers to a specific call (i.e., a channel or call leg or session).
234+
func (c *Client) SendMsg(uuid string, headers map[string]string, body string) error {
235+
_, err := c.sendRecv(
236+
cmd("sendmsg", uuid).WithMessage(headers, body))
237+
return err
238+
}
239+
240+
// runReader is a method of the Client struct that reads responses from the connection and handles them accordingly.
241+
func (c *Client) runReader(events chan<- Event, autoClose bool) {
242+
c.conn.log.Info("esl: run response reading")
243+
defer func() {
244+
close(c.chResp)
245+
close(c.chErr)
246+
if autoClose && events != nil {
247+
close(events)
248+
}
249+
c.conn.log.Info("esl: response reader stopped")
250+
}()
251+
252+
for {
253+
resp, err := c.conn.Read()
254+
if err != nil {
255+
c.chErr <- err
256+
return // break on read error
257+
}
258+
259+
switch ct := resp.ContentType(); ct {
260+
case "api/response", "command/reply":
261+
c.chResp <- resp
262+
263+
case "text/event-plain":
264+
if events == nil {
265+
continue // ignore events if no events channel is provided
266+
}
267+
268+
event, err := resp.toEvent()
269+
if err != nil {
270+
c.conn.log.Error("esl: failed to parse event",
271+
slog.String("err", err.Error()))
272+
continue // ignore bad event
273+
}
274+
275+
c.conn.log.Info("esl: handle", slog.Any("event", event))
276+
events <- event
277+
278+
case "text/disconnect-notice":
279+
return // disconnect
280+
281+
default:
282+
c.conn.log.Warn("esl: unexpected response",
283+
slog.String("content-type", ct))
284+
}
285+
}
286+
}
287+
288+
// sendRecv sends a command to the server and returns the response.
289+
func (c *Client) sendRecv(cmd command) (response, error) {
290+
if err := c.conn.Write(cmd); err != nil {
291+
return response{}, err
292+
}
293+
294+
return c.read()
295+
}
296+
297+
// read reads the response from the client's channel and returns it along with any error.
298+
func (c *Client) read() (response, error) {
299+
select {
300+
case err, ok := <-c.chErr:
301+
if ok {
302+
return response{}, err
303+
}
304+
return response{}, io.EOF // connection closed
305+
306+
case resp := <-c.chResp:
307+
if err := resp.AsErr(); err != nil {
308+
return response{}, err // response with error message
309+
}
310+
return resp, nil
311+
}
312+
}

0 commit comments

Comments
 (0)