goesl

package module
v0.0.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 19, 2023 License: BSD-2-Clause Imports: 19 Imported by: 0

README

Description

goesl is a go library to connect to FreeSWITCH via event socket, only inbound now.

Installation

Installation can be done as usual:

$ go get github.com/genmzy/goesl

How it works

goesl.NewConnection create a new esl connection and take a ConnHandler interface which defines the callbacks to handle the event-socket events and freeswitch logs.

Example of use

  • A correct way to close connection immediately should like this:
conn, err := conn.Dial(addr, passwd, handler, opts...)
if err != nil {
	// do some error log
	return;
}
defer conn.Close()
ctx, cancel := context.WithCancel(context.Background())
// ...
go conn.HandleEvents(ctx, /* ... other params ... */)

// when time to quit:
if timeToQuit { // triggered should quit
	log.Println("exiting...")
	cancel()
	// close twice is allowed
	conn.Close()
}
  • Here is a fs_cli-like program based on goesl:
package main

import (
	"bufio"
	"bytes"
	"context"
	"errors"
	"flag"
	"fmt"
	"fs_monitor/goesl"
	"fs_monitor/goesl/ev_header"
	"fs_monitor/goesl/ev_name"
	"net"
	"os"
	"os/signal"
	"regexp"
	"strings"
	"syscall"
	"time"
)

type Handler struct {
	// just test
	CallId  string
	BgJobId string
}

var (
	host    = flag.String("H", "127.0.0.1", "Specify the host of freeswitch")
	port    = flag.Int("P", 8021, "Specify the port of freeswitch")
	passwd  = flag.String("p", "ClueCon", "Specify the default password of freeswitch")
	level   = flag.String("l", "debug", "Specify the log level of freeswitch")
	logPath = flag.String("log_file", "", "Specify the log path of fs_cli self")
)

func main() {
	flag.Parse()

	go signalCatch()

	opts := make([]goesl.Option, 0)
	opts = append(opts,
		goesl.WithDefaultAutoRedial(),
		goesl.WithHeartBeat(20*time.Second),
		goesl.WithNetDelay(2*time.Second),
		goesl.WithMaxRetries(-1),
		goesl.WithLogLevel(goesl.LevelDebug),
		goesl.WithLogPrefix("<fs_cli>"),
	)

	if *logPath != "" {
		logFile, err := os.OpenFile(*logPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0755)
		if err != nil {
			goesl.Warnf("error when open log file %s: %v, use stdout now ...", *logPath, err)
		} else {
			opts = append(opts, goesl.WithLogOutput(logFile))
			defer logFile.Close()
		}
	}

	handler := &Handler{}
	conn, err := goesl.Dial(fmt.Sprintf("%s:%d", *host, *port), *passwd, handler, opts...)
	if err != nil {
		goesl.Fatalf("connecting to freeswitch: %v", err)
	}
	// close twice is allowed
	defer conn.Close()

	ctx, cancel := context.WithCancel(context.Background())
	go messageSend(ctx, conn, cancel)

	err = conn.HandleEvents(ctx)
	if errors.Is(err, net.ErrClosed) || errors.Is(err, context.Canceled) {
		goesl.Noticef("process exiting...")
	} else {
		goesl.Errorf("exiting with error: %v", err)
	}
}

// just a test, it doesn't matter with fs_cli
func (h *Handler) testApi(ctx context.Context, conn *goesl.Connection) {
	const (
		Caller = "1002"
		Callee = "1001"
	)
	result := ""
	check := func(res string, err error) {
		if err != nil {
			goesl.Errorf("api: %v", err)
			return
		}
		result = res
		fmt.Println(res)
	}
	check(conn.Api(ctx, "stat"))
	check(conn.Api(ctx, "create_uuid"))
	check(conn.BgApi(ctx, "originate", "{origination_uuid="+h.CallId+",origination_caller_id_number="+
		Caller+"}user/"+Callee, "&echo()"))
	h.BgJobId = result
	goesl.Debugf("originate bg job id: %s", h.BgJobId)
}

func rawStatus2Profiles(raw string) [][]string {
	s := bufio.NewScanner(bytes.NewBuffer([]byte(raw)))
	start := false
	res := make([][]string, 0)
	for s.Scan() {
		t := s.Text()
		if strings.HasPrefix(t, "====") {
			if start {
				break
			} else {
				start = true
				continue
			}
		}
		if !start {
			continue
		}
		t = strings.TrimSpace(t)
		reg := regexp.MustCompile(`\s+`)
		sli := reg.Split(t, -1)
		res = append(res, sli)
	}
	return res
}

type SofiaEntry struct {
	Name   string
	Type   string // gateway or profile
	Url    string
	Status string
	Extra  string
}

func (se SofiaEntry) String() string {
	return fmt.Sprintf(`{"name":"%s","type":"%s","url":"%s","status":"%s","extra":"%s"}`,
		se.Name, se.Type, se.Url, se.Status, se.Extra)
}

// return sofia entry groups
func SofiaStatus(raw []byte) (entries []*SofiaEntry) {
	s := bufio.NewScanner(bytes.NewBuffer(raw))
	start := false
	for s.Scan() {
		t := s.Text()
		if t[0] == '=' { // ====================
			if start { // end line
				break
			} else { // start line
				start = true
				continue
			}
		}
		if !start {
			continue
		}
		// content line now
		t = strings.TrimSpace(t)
		reg := regexp.MustCompile(`\s+`)
		sli := reg.Split(t, -1)
		entry := &SofiaEntry{Name: sli[0], Type: sli[1], Url: sli[2], Status: sli[3]}
		if len(sli) > 4 {
			entry.Extra = sli[4]
		}
		entries = append(entries, entry)
	}
	return
}

func (h *Handler) OnConnect(conn *goesl.Connection) {
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()
	conn.Plain(ctx, []ev_name.EventName{ev_name.BACKGROUND_JOB, ev_name.API}, nil)
	conn.Fslog(ctx, goesl.FslogString2Level(*level))

	die := func(s string, err error) string {
		if err != nil {
			conn.Close()
			goesl.Fatalf("api: %v", err)
		}
		return s
	}

	core := die(conn.Api(ctx, "global_getvar", "core_uuid"))
	raw := die(conn.Api(ctx, "sofia", "status"))
	entries := SofiaStatus([]byte(raw))
	goesl.Noticef("connect and get freeswitch (core: %s with sofia entries: %v)", core, entries)
}

func (h *Handler) OnDisconnect(conn *goesl.Connection, ev goesl.Event) {
	goesl.Noticef("esl disconnected: %v", ev)
}

func (h *Handler) OnClose(con *goesl.Connection) {
	goesl.Noticef("esl connection closed")
}

func (h *Handler) OnEvent(ctx context.Context, conn *goesl.Connection, e goesl.Event) {
	fmt.Println(e.EventContent())
	goesl.Debugf("fire time: %s", e.FireTime().StdTime().Format("2006-01-02 15:04:05"))
	en := e.Name()
	app, appData := e.App()
	goesl.Debugf("%s - event %s %s %s", e.Uuid(), en, app, appData)
	switch en {
	case ev_name.BACKGROUND_JOB:
		goesl.Noticef("background job %s result: %s", e.BgJob(), e.GetTextBody())
	// HEARTBEAT be subscribed when WithDefaultAutoRedial called
	case ev_name.HEARTBEAT:
		hostname, err := conn.Send(ctx, "api global_getvar", "hostname")
		if err != nil {
			goesl.Errorf("send global_getvar error: %v", err)
			return
		}
		goesl.Noticef("receive %s of %s heartbeat", conn.Address, hostname)
	case ev_name.API:
		goesl.Noticef("receive api event: %s(%s)", e.Get(ev_header.API_Command), e.Get(ev_header.API_Command_Argument))
	}
}

var colorFormats = map[goesl.FslogLevel]string{
	goesl.FslogLevel_DEBUG:   "\033[0;33m%s\033[0m\n",
	goesl.FslogLevel_INFO:    "\033[0;32m%s\033[0m\n",
	goesl.FslogLevel_NOTICE:  "\033[0;36m%s\033[0m\n",
	goesl.FslogLevel_WARNING: "\033[0;35m%s\033[0m\n",
	goesl.FslogLevel_ERROR:   "\033[0;31m%s\033[0m\n",
	goesl.FslogLevel_CRIT:    "\033[0;31m%s\033[0m\n",
	goesl.FslogLevel_ALERT:   "\033[0;31m%s\033[0m\n",
}

func getColorFormat(lv goesl.FslogLevel) string {
	if format, ok := colorFormats[lv]; ok {
		return format
	} else {
		return "%s\n\n"
	}
}

func (h *Handler) OnFslog(ctx context.Context, conn *goesl.Connection, fslog goesl.Fslog) {
	var content string
	if fslog.UserData != "" {
		content = fmt.Sprintf("%s %s", fslog.UserData, fslog.Content)
	} else {
		content = fslog.Content
	}
	fmt.Printf(getColorFormat(fslog.Level), content)
}

func send(ctx context.Context, conn *goesl.Connection, cmd string) {
	check := func(res string, err error) {
		if err != nil {
			goesl.Errorf("api %s error: %v", cmd, err)
			return
		}
		fmt.Println(res)
	}

	if cmd[0] == '/' {
		check(conn.Send(ctx, cmd[1:]))
		return
	}
	if strings.Split(cmd, " ")[0] == "originate" {
		goesl.Noticef("now use bapi originate instead of originate")
		check(conn.BgApi(ctx, cmd))
		return
	}
	goesl.Debugf("send commands: %s", cmd)
	check(conn.Api(ctx, cmd))
}

func messageSend(ctx context.Context, conn *goesl.Connection, cancel func()) {
	s := bufio.NewScanner(os.Stdin)
	host, err := conn.Api(ctx, "global_getvar", "hostname")
	if err != nil {
		goesl.Errorf("api global_getvar hostname send: %v", err)
		return
	}
	cliPrefix := fmt.Sprintf("freeswitch/%s> ", host)
	fmt.Print(cliPrefix)
	for s.Scan() {
		fmt.Print(cliPrefix)
		switch cmd := s.Text(); cmd {
		case "":
		case "...", "/exit", "/bye", "/quit":
			cancel()
			// close twice is allowed
			conn.Close()
			return
		default:
			send(ctx, conn, cmd)
		}
	}
}

func signalCatch() {
	sigs := make(chan os.Signal, 1)
	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
	defer signal.Stop(sigs)
	<-sigs
	panic("show all goroutines")
}

TODO

  • add documentation
  • add tests
  • more usage examples

Documentation

Overview

This file convert from freeswitch 1.10.9 switch_log.c

Index

Constants

This section is empty.

Variables

View Source
var ErrMismatchEventType = errors.New("mismatch event type")

Functions

func Debugf added in v0.0.3

func Debugf(format string, v ...any)

func Errorf added in v0.0.3

func Errorf(format string, v ...any)

func Fatalf added in v0.0.3

func Fatalf(format string, v ...any)

func Infof added in v0.0.3

func Infof(format string, v ...any)

func Noticef added in v0.0.3

func Noticef(format string, v ...any)

func Warnf added in v0.0.3

func Warnf(format string, v ...any)

Types

type Command

type Command struct {
	Uuid  string
	App   string
	Args  string
	Sync  bool
	Loops uint
}

func (Command) Execute

func (cmd Command) Execute(ctx context.Context, conn *Connection) (string, error)

Execute sends Command cmd over Connection and waits for reply. Returns the command reply event pointer or an error if any.

func (*Command) Serialize

func (cmd *Command) Serialize() []byte

Serialize formats (serializes) the command as expected by freeswitch.

type ConnHandler

type ConnHandler interface {
	OnConnect(*Connection)
	OnDisconnect(*Connection, Event)
	OnEvent(context.Context, *Connection, Event)
	OnFslog(context.Context, *Connection, Fslog)
	OnClose(*Connection)
}

type Connection

type Connection struct {
	// reset when connection auto redial
	Handler ConnHandler

	Address  string
	Password string
	// contains filtered or unexported fields
}

func Dial added in v0.0.2

func Dial(addr, passwd string, handler ConnHandler, options ...Option) (*Connection, error)

Create a new event socket connection and take a ConnectionHandler interface. This will create a new 'event callback' goroutine this goroutine will exit when call goesl.*Connection.Close()

func (*Connection) Api

func (conn *Connection) Api(ctx context.Context, cmd string, args ...string) (string, error)

Send API command to FreeSWITCH by event socket, already start with `api ` get result in param `h` by calling method, esl.Event.GetTextBody(), result maybe start withh `-ERR `. NOTE: do not use block API such as orignate here, which will block fs from sending other events(e.g. HEARTBEAT, further make esl client automatic redial), so use (*Connection).BgApi

func (*Connection) BgApi

func (conn *Connection) BgApi(ctx context.Context, cmd string, args ...string) (string, error)

`bgapi` command will never response error, so just care Connection error handle This is a better way to use `api bgapi uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` instead of `bgapi ` and wait for job uuid

func (*Connection) Close

func (conn *Connection) Close()

Close the connection and make event callback exit as soon as possible ignore all errors here because may the connection already lost

Close is protected by sync.Once, so you can call this for twice or more

func (*Connection) Execute

func (conn *Connection) Execute(ctx context.Context, app string, uuid string, params ...string) (string, error)

Execute an app on leg `uuid` suggest: use RepJustCareError

func (*Connection) ExecuteLooped

func (conn *Connection) ExecuteLooped(ctx context.Context, app string, uuid string, loops uint, params ...string) (string, error)

Execute an app on leg `uuid` and for `loops` times suggest: use RepJustCareError

func (*Connection) ExecuteLoopedSync

func (conn *Connection) ExecuteLoopedSync(ctx context.Context, app string, uuid string,
	loops uint, params ...string,
) (string, error)

Execute an app on leg `uuid` and for `loops` times, this app will not be interrupted util finish. suggest: use RepJustCareError

func (*Connection) ExecuteSync

func (conn *Connection) ExecuteSync(ctx context.Context, app string, uuid string, params ...string) (string, error)

Execute an app on leg `uuid`, this app will not be interrupted util finish. suggest: use RepJustCareError

func (*Connection) Fslog added in v0.0.4

func (conn *Connection) Fslog(ctx context.Context, lv FslogLevel)

func (*Connection) HandleEvents

func (conn *Connection) HandleEvents(ctx context.Context) error

Receive events and handle them by `goesl.ConnectionHandler` if error returned is not `os.ErrClosed` or `context.ErrCanceled` that means unexpected error occurred. To distingush that, you can use: `errors.Is(err, net.ErrClosed) || errors.Is(err, context.Canceled)`

func (*Connection) MustSendOK

func (conn *Connection) MustSendOK(ctx context.Context, cmd string, args ...string)

must send and receive command, or fatal the process

func (*Connection) Plain added in v0.0.4

func (conn *Connection) Plain(ctx context.Context, ens []ev_name.EventName, subs []string)

func (*Connection) Send

func (conn *Connection) Send(ctx context.Context, cmd string, args ...string) (string, error)

Send to FreeSWITCH and handle result(error and event)

func (*Connection) SendBytes added in v0.0.4

func (conn *Connection) SendBytes(ctx context.Context, buf []byte) (string, error)

func (*Connection) SendEvent

func (conn *Connection) SendEvent(ctx context.Context, en ev_name.EventName, headers map[string]string, body []byte) error

Send event to FreeSWITCH, this is NOT a API or BgAPI command suggest: use RepJustCareError

type Event

type Event struct {
	Type EventType
	// contains filtered or unexported fields
}

func (Event) API added in v0.0.4

func (e Event) API() (string, string)

func (Event) App

func (e Event) App() (string, string)

app and data

func (Event) BgCommand added in v0.0.3

func (e Event) BgCommand() (string, string)

func (Event) BgJob added in v0.0.3

func (e Event) BgJob() string

func (Event) BgJobResponse added in v0.0.3

func (e Event) BgJobResponse() string

func (Event) CallDirection added in v0.0.3

func (e Event) CallDirection() string

func (Event) Callee added in v0.0.3

func (e Event) Callee() string

func (Event) Caller added in v0.0.3

func (e Event) Caller() string

func (Event) CoreIP added in v0.0.3

func (e Event) CoreIP() string

func (Event) CoreNetworkAddr added in v0.0.3

func (e Event) CoreNetworkAddr() string

func (Event) CoreUuid added in v0.0.3

func (e Event) CoreUuid() string

func (Event) CurrentApp added in v0.0.3

func (e Event) CurrentApp() (string, string)

current app and data

func (Event) Digits added in v0.0.3

func (e Event) Digits() string

func (Event) DigitsSource added in v0.0.3

func (e Event) DigitsSource() string

func (Event) ErrOrRes added in v0.0.4

func (e Event) ErrOrRes() (raw string, err error)

func (Event) EventContent added in v0.0.4

func (e Event) EventContent() string

func (Event) FireTime added in v0.0.3

func (e Event) FireTime() FireTime

func (Event) Get

func (e Event) Get(header string) string

Get retrieves the value of header from Event header or (if not found) from Event body. The value is returned unescaped and is empty if not found anywhere.

func (Event) GetRaw added in v0.0.4

func (e Event) GetRaw(header string) string

func (Event) GetTextBody

func (e Event) GetTextBody() string

func (Event) Name

func (e Event) Name() ev_name.EventName

func (Event) Sequence added in v0.0.3

func (e Event) Sequence() string

func (Event) SipFrom added in v0.0.3

func (e Event) SipFrom() string

func (Event) SipTo added in v0.0.4

func (e Event) SipTo() string

func (Event) String

func (e Event) String() string

func (Event) Uuid added in v0.0.4

func (e Event) Uuid() string

type EventType

type EventType int
const (
	EventInvalid EventType = iota
	EventState
	EventConnect
	EventAuth
	EventCommandReply
	EventApiResponse
	EventDisconnect
	EventLog
	EventGeneric
)

type FireTime

type FireTime uint64

func (FireTime) StdTime

func (t FireTime) StdTime() time.Time

type Fslog added in v0.0.4

type Fslog struct {
	Content  string
	File     string
	UserData string
	Func     string
	Line     int
	Level    FslogLevel
}

func Event2Fslog added in v0.0.4

func Event2Fslog(e Event) (fslog Fslog, err error)

type FslogLevel added in v0.0.4

type FslogLevel int
var (
	FslogLevel_DEBUG10 FslogLevel = 110  // SWITCH_LOG_DEBUG10
	FslogLevel_DEBUG9  FslogLevel = 109  // SWITCH_LOG_DEBUG9
	FslogLevel_DEBUG8  FslogLevel = 108  // SWITCH_LOG_DEBUG8
	FslogLevel_DEBUG7  FslogLevel = 107  // SWITCH_LOG_DEBUG7
	FslogLevel_DEBUG6  FslogLevel = 106  // SWITCH_LOG_DEBUG6
	FslogLevel_DEBUG5  FslogLevel = 105  // SWITCH_LOG_DEBUG5
	FslogLevel_DEBUG4  FslogLevel = 104  // SWITCH_LOG_DEBUG4
	FslogLevel_DEBUG3  FslogLevel = 103  // SWITCH_LOG_DEBUG3
	FslogLevel_DEBUG2  FslogLevel = 102  // SWITCH_LOG_DEBUG2
	FslogLevel_DEBUG1  FslogLevel = 101  // SWITCH_LOG_DEBUG1
	FslogLevel_DEBUG   FslogLevel = 7    // SWITCH_LOG_DEBUG
	FslogLevel_INFO    FslogLevel = 6    // SWITCH_LOG_INFO
	FslogLevel_NOTICE  FslogLevel = 5    // SWITCH_LOG_NOTICE
	FslogLevel_WARNING FslogLevel = 4    // SWITCH_LOG_WARNING
	FslogLevel_ERROR   FslogLevel = 3    // SWITCH_LOG_ERROR
	FslogLevel_CRIT    FslogLevel = 2    // SWITCH_LOG_CRIT
	FslogLevel_ALERT   FslogLevel = 1    // SWITCH_LOG_ALERT
	FslogLevel_CONSOLE FslogLevel = 0    // SWITCH_LOG_CONSOLE
	FslogLevel_DISABLE FslogLevel = -1   // SWITCH_LOG_DISABLE
	FslogLevel_INVALID FslogLevel = 64   // SWITCH_LOG_INVALID
	FslogLevel_UNINIT  FslogLevel = 1000 // SWITCH_LOG_UNINIT
)

func FslogString2Level added in v0.0.4

func FslogString2Level(s string) FslogLevel

func (FslogLevel) String added in v0.0.4

func (lv FslogLevel) String() string

type Level added in v0.0.2

type Level int

Level defines the priority of a log message. When a logger is configured with a level, any log message with a lower log level (smaller by integer comparison) will not be output.

const (
	LevelDebug Level = iota
	LevelInfo
	LevelNotice
	LevelWarn
	LevelError
	LevelFatal
)

The levels of logs.

func (Level) String added in v0.0.2

func (lv Level) String() string

type Logger added in v0.0.2

type Logger interface {
	Debugf(format string, v ...any)
	Infof(format string, v ...any)
	Noticef(format string, v ...any)
	Warnf(format string, v ...any)
	Errorf(format string, v ...any)
	Fatalf(format string, v ...any)
}

Logger is a logger interface that output logs with a format.

type Option added in v0.0.2

type Option struct {
	// contains filtered or unexported fields
}

func WithAutoRedial added in v0.0.2

func WithAutoRedial(strategy RedoStrategy) Option

need to pay attention that the `strategy` should handle that time.Duration is empty

func WithDefaultAutoRedial added in v0.0.2

func WithDefaultAutoRedial() Option

func WithDialTimeout added in v0.0.2

func WithDialTimeout(t time.Duration) Option

func WithHeartBeat added in v0.0.2

func WithHeartBeat(t time.Duration) Option

Set heartbeat interval time duration only take effect when set with `WithAutoRedial` event receiver regards connection lost when `heartbeat_interval + net_delay > time_wait`

func WithLogLevel added in v0.0.2

func WithLogLevel(lv Level) Option

Set logger level, ONLY set level of internal logger if a user defined logger, do nothing

func WithLogOutput added in v0.0.2

func WithLogOutput(w io.Writer) Option

Set logger output file, only set output file of set internal logger if a user defined logger, do nothing

func WithLogPrefix added in v0.0.3

func WithLogPrefix(s string) Option

func WithLogger added in v0.0.2

func WithLogger(logger Logger) Option

func WithMaxRetries added in v0.0.2

func WithMaxRetries(n int) Option

if n is -1, always retry

func WithNetDelay added in v0.0.2

func WithNetDelay(t time.Duration) Option

Set max network delay time duration used as connection write timeout, event callback ticker timeout suggest range: 1*time.Second <= t <= 5*time.Second valid range: 100*time.Milliseconds <= t <= 10*time.Second

func WithSendReplyCap added in v0.0.2

func WithSendReplyCap(n int) Option

Sender channel capacity

type Options added in v0.0.2

type Options struct {
	// contains filtered or unexported fields
}

type RedoStrategy added in v0.0.4

type RedoStrategy interface {
	NextRedoWait() time.Duration
	Reset()
}

Directories

Path Synopsis
examples

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL