zammadbridge

package module
v0.8.0 Latest Latest
Warning

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

Go to latest
Published: Oct 30, 2025 License: EUPL-1.2 Imports: 18 Imported by: 0

README

[!CAUTION] Support for v20 and above is experimental and may not work as expected. Please report any issues you encounter. Currently, only monitoring of extensions is supported. Monitoring of groups is not supported due to limitations of the 3CX permissions model.

3cx-zammad-bridge

Monitors calls in 3CX and communicates this to Zammad accordingly.

Requirements

  • Linux x86_64

Installation

  • Download the latest release binary from releases.
    • Copy the binary into /usr/local/bin
  • chmod +x zammadbridge

Configuration

All configuration is done through the config.yaml file, that may appear in these locations:

  • /etc/3cx-zammad-bridge/config.yaml
  • /opt/3cx-zammad-bridge/config.yaml
  • config.yaml (within the working directory of this 3cx bridge process)

The first (found) configuration file will be used. Also refer to the config.yaml.dist file.

For 3CX versions 20 and above, it's important that you create a client ID and secret in the 3CX web interface. You have to add all extensions that you want to monitor to the Call Control API permissions in the 3CX web interface for the client ID you create. Note that monitoring groups this way is not supported (due to limitations of the 3CX permissions model). You have to add all extensions manually.

Example configuration:

Bridge:
  poll_interval: 0.5 # decimal; The number of seconds to wait in between polling 3CX for calls

3CX:
    # For versions below v20, define these two:
    user: "the username of a 3CX admin account"
    pass: "the password of a 3CX admin account"
    group: "the name of the 3CX group that should be monitored, for example Support"
    # For versions v20 and above, define these two:
    client_id: "the client ID you created in 'Admin' -> 'Integrations' -> 'API'"
    client_secret: "the secret that was shown once"
    # Always define these:
    host: "the URL of your 3CX server, including https://"
    extension_digits: 3 # numeric; How many digits the internal extensions have 
    trunk_digits: 5 # numeric; How many digits the numbers in the trunk have
    queue_extension: 816 # numeric; The number of the queue that the bridge should also listen to
    country_prefix: 49 # numeric; optional; The country dialing prefix to remove from the numbers

Zammad:
    endpoint: https://zammad.example.com/api/v1/cti/secret # The URL of your Zammad server, including the secret in the URL
    log_missed_queue_calls: true # boolean; Whether or not you want to log missed calls to your queue

Running

Run the release binary to run the daemon.

Example supervisord config:

[program:3cx-zammad-bridge]
command = /usr/local/bin/zammadbridge
autostart = true
autorestart = true
startretries = 10
stderr_logfile = /var/log/3cx-zammad-bridge.err.log
stdout_logfile = /var/log/3cx-zammad-bridge.out.log

# Optionally specify a user
user = zammad-bridge

Example systemd service:

[Unit]
Description=3cx-zammad-bridge
After=network.target

# One might want to wait for the 3CXGatewayService to be up and running
# before starting this service, but during updates the 3CGatewayService
# is *stopped* and later started. This results in this 3cx-zammad-bridge
# to be stopped but never started again.
#PartOf=3CXGatewayService.service

[Service]
User=zammad-bridge
Group=zammad-bridge
ExecStart=/usr/local/bin/zammadbridge
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Help

3cx-zammad-bridge is a bridge that listens on 3cx to forward information to zammad

Usage:
  zammadbridge [flags]

Flags:
  -c, --config string       custom config file path (default "/etc/3cx-zammad-bridge/config.yaml")
  -h, --help                help for zammadbridge
  -f, --log-format string   log format: "json" or "plain" (default "json")
      --trace               trace output, super verbose
  -v, --verbose             verbose output

Development

You can build the binary by running make build

Theoretically, this should also run on Windows. You can compile it yourself and report possible issues.

Support

Premium support is available at Q-MEX Networks GmbH

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type API3CX added in v0.7.0

type API3CX interface {
	// AuthenticateRetry authenticates the client and retries in case of offline status
	// maxOffline specifies the maximum duration to wait for the client to come online.
	AuthenticateRetry(maxOffline time.Duration) error

	// FetchCalls retrieves information about current calls from the 3CX API.
	// It returns a slice of CallInformation structs and an error if the API call fails.
	FetchCalls() ([]CallInformation, error)

	// IsExtension checks if a given phone number is a valid extension that is being monitored.
	IsExtension(number string) bool
}

API3CX abstracts away the differences in API versions from before v20 and after v20 of 3CX.

func Create3CXClient added in v0.7.0

func Create3CXClient(c *Config) (API3CX, error)

Create3CXClient creates a 3CX client based on the provided configuration.

It first attempts to create a v20 client, and if it fails with an HTTP 404 error, it falls back to creating a pre-v20 client. The function returns an API3CX interface and an error if the client creation fails.

The client is created with a cookiejar and authenticated using the AuthenticateRetry method, which waits for the client to come online for a maximum duration of two minutes.

type CallControlResponse added in v0.7.0

type CallControlResponse []CallControlResponseEntry

type CallControlResponseEntry added in v0.7.0

type CallControlResponseEntry struct {
	DN           string            `json:"dn"`
	Participants []CallParticipant `json:"participants"`
}

type CallInformation

type CallInformation struct {
	ID     json.Number `json:"Id"`
	Caller string      `json:"Caller"`
	Callee string      `json:"Callee"`

	// Status has possible values: "Talking", "Transferring", "Routing"
	Status            string `json:"Status"`
	ZammadInitialized bool
	ZammadAnswered    bool

	// Timestamps
	LastChangeStatus time.Time `json:"LastChangeStatus"`
	EstablishedAt    time.Time `json:"EstablishedAt"`

	// Various processed fields
	CallerName     string
	CallerNumber   string
	CalleeName     string
	CalleeNumber   string
	CallUID        string
	Direction      string
	AgentName      string
	AgentNumber    string
	CallFrom       string
	CallTo         string
	ExternalNumber string
}

type CallParticipant added in v0.7.0

type CallParticipant struct {
	ID int `json:"id"`

	// Status is the status of the call. Possible values include: "Dialing", "Ringing", "Connected"
	Status string `json:"status"`

	// DN is the extension number of the participant.
	DN string `json:"dn"`

	// PartyCallerName is the name of the caller or callee. Can be empty.
	PartyCallerName string `json:"party_caller_name"`

	// PartyDN is the extension number of the caller. E.g. 10007
	PartyDN string `json:"party_dn"`

	// PartyCallerID is the caller ID of the caller. E.g. 0123456789
	PartyCallerID string `json:"party_caller_id"`

	// PartyDID is the DID of the caller. Can be empty.
	PartyDID string `json:"party_did"`

	// CallID is the unique ID of the call.
	CallID int `json:"callid"`
}

type Client3CXPost20 added in v0.7.0

type Client3CXPost20 struct {
	Config *Config
	// contains filtered or unexported fields
}

func (*Client3CXPost20) Authenticate added in v0.7.0

func (z *Client3CXPost20) Authenticate() error

Authenticate attempts to login to 3CX and stores a token for future API calls. It then loads all extensions we are configured to monitor.

func (*Client3CXPost20) AuthenticateRetry added in v0.7.0

func (z *Client3CXPost20) AuthenticateRetry(maxOffline time.Duration) error

AuthenticateRetry retries logging in a while (defined in maxOffline). It waits five seconds for every failed attempt.

func (*Client3CXPost20) FetchCalls added in v0.7.0

func (z *Client3CXPost20) FetchCalls() ([]CallInformation, error)

func (*Client3CXPost20) IsExtension added in v0.7.0

func (z *Client3CXPost20) IsExtension(_ string) bool

type Client3CXPre20 added in v0.7.0

type Client3CXPre20 struct {
	Config *Config
	// contains filtered or unexported fields
}

func (*Client3CXPre20) Authenticate added in v0.7.0

func (z *Client3CXPre20) Authenticate() error

Authenticate attempts to login to 3CX and retrieve a valid cookie session.

func (*Client3CXPre20) AuthenticateRetry added in v0.7.0

func (z *Client3CXPre20) AuthenticateRetry(maxOffline time.Duration) error

AuthenticateRetry retries logging in a while (defined in maxOffline). It waits five seconds for every failed attempt.

func (*Client3CXPre20) FetchCalls added in v0.7.0

func (z *Client3CXPre20) FetchCalls() ([]CallInformation, error)

func (*Client3CXPre20) IsExtension added in v0.7.0

func (z *Client3CXPre20) IsExtension(number string) bool

type Config

type Config struct {
	Bridge struct {
		PollInterval float64 `yaml:"poll_interval"`
	} `yaml:"Bridge"`
	Phone3CX struct {
		User            string `yaml:"user"`
		Pass            string `yaml:"pass"`
		ClientID        string `yaml:"client_id"`
		ClientSecret    string `yaml:"client_secret"`
		Host            string `yaml:"host"`
		Group           string `yaml:"group"`
		ExtensionDigits int    `yaml:"extension_digits"`
		TrunkDigits     int    `yaml:"trunk_digits"`
		QueueExtension  int    `yaml:"queue_extension"`
		CountryPrefix   string `yaml:"country_prefix"`
	} `yaml:"3CX"`
	Zammad struct {
		Endpoint            string `yaml:"endpoint"`
		LogMissedQueueCalls bool   `yaml:"log_missed_queue_calls"`
	} `yaml:"Zammad"`
}

func LoadConfigFromYaml

func LoadConfigFromYaml(filenames ...string) (*Config, error)

LoadConfigFromYaml tries the provided files for a valid YAML configuration file. It uses the first file it can parse, and only that file.

type GroupListEntryObject

type GroupListEntryObject struct {
	Members struct {
		Selected []struct {
			Number struct {
				Value string `json:"_value"`
			} `json:"Number"`
		} `json:"selected"`
	} `json:"Members"`
}

type GroupListResponseEntry

type GroupListResponseEntry struct {
	Item GroupListEntryObject `json:"Item"`
}

type WebsocketEventType added in v0.7.0

type WebsocketEventType int
const (
	WebsocketEventTypeUpsert     WebsocketEventType = 0
	WebsocketEventTypeDelete     WebsocketEventType = 1
	WebsocketEventTypeDTMFstring WebsocketEventType = 2
	WebsocketEventTypeResponse   WebsocketEventType = 4
)

type WebsocketResponse added in v0.7.0

type WebsocketResponse struct {
	Sequence int `json:"sequence"`
	Event    struct {
		EventType    WebsocketEventType `json:"event_type"`
		Entity       string             `json:"entity"`
		AttachedData *json.RawMessage   `json:"attached_data"`
	} `json:"event"`
}

type ZammadApiRequest

type ZammadApiRequest struct {
	Event           string `json:"event"`
	From            string `json:"from"`
	To              string `json:"to"`
	Direction       string `json:"direction"`
	CallId          string `json:"call_id"`
	CallIdDuplicate string `json:"callid"`
	Cause           string `json:"cause,omitempty"`
	AnsweringNumber string `json:"answeringNumber,omitempty"`
	User            string `json:"user,omitempty"`
}

type ZammadBridge

type ZammadBridge struct {
	Config *Config

	Client3CX    API3CX
	ClientZammad http.Client
	// contains filtered or unexported fields
}

func NewZammadBridge

func NewZammadBridge(config *Config) (*ZammadBridge, error)

NewZammadBridge initializes a new client that listens for 3CX calls and forwards to Zammad.

func (*ZammadBridge) Listen

func (z *ZammadBridge) Listen() error

Listen listens for calls and does not return unless something really bad happened.

func (*ZammadBridge) LogIfErr

func (z *ZammadBridge) LogIfErr(err error, context string)

LogIfErr logs to stderr when an error occurs, doing nothing when err is nil.

func (*ZammadBridge) ParsePhoneNumber

func (z *ZammadBridge) ParsePhoneNumber(number string) string

ParsePhoneNumber parses the phone number into a format acceptable to Zammad

func (*ZammadBridge) ProcessCall

func (z *ZammadBridge) ProcessCall(call *CallInformation) error

ProcessCall processes a single ongoing call from 3CX

func (*ZammadBridge) RequestAndProcess

func (z *ZammadBridge) RequestAndProcess() error

RequestAndProcess requests the current calls from 3CX and processes them to Zammad

func (*ZammadBridge) ZammadAnswer

func (z *ZammadBridge) ZammadAnswer(call *CallInformation) error

ZammadAnswer notifies Zammad that the existing call was now answered by an agent.

func (*ZammadBridge) ZammadHangup

func (z *ZammadBridge) ZammadHangup(call *CallInformation, cause string) error

ZammadHangup notifies Zammad that the call was finished with a given cause. Possible values for `cause` are: "cancel", "normalClearing"

func (*ZammadBridge) ZammadNewCall

func (z *ZammadBridge) ZammadNewCall(call *CallInformation) error

ZammadNewCall notifies Zammad that a new call came in. This is the first call required to process calls using Zammad.

func (*ZammadBridge) ZammadPost

func (z *ZammadBridge) ZammadPost(payload ZammadApiRequest) error

ZammadPost makes a POST Request to Zammad with the given payload

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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