summaryrefslogtreecommitdiffstats
path: root/bridge/mumble/mumble.go
diff options
context:
space:
mode:
Diffstat (limited to 'bridge/mumble/mumble.go')
-rw-r--r--bridge/mumble/mumble.go259
1 files changed, 259 insertions, 0 deletions
diff --git a/bridge/mumble/mumble.go b/bridge/mumble/mumble.go
new file mode 100644
index 00000000..2281d1c2
--- /dev/null
+++ b/bridge/mumble/mumble.go
@@ -0,0 +1,259 @@
+package bmumble
+
+import (
+ "crypto/tls"
+ "crypto/x509"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "net"
+ "strconv"
+ "time"
+
+ "layeh.com/gumble/gumble"
+ "layeh.com/gumble/gumbleutil"
+
+ "github.com/42wim/matterbridge/bridge"
+ "github.com/42wim/matterbridge/bridge/config"
+ "github.com/42wim/matterbridge/bridge/helper"
+ stripmd "github.com/writeas/go-strip-markdown"
+
+ // We need to import the 'data' package as an implicit dependency.
+ // See: https://godoc.org/github.com/paulrosania/go-charset/charset
+ _ "github.com/paulrosania/go-charset/data"
+)
+
+type Bmumble struct {
+ client *gumble.Client
+ Nick string
+ Host string
+ Channel *uint32
+ local chan config.Message
+ running chan error
+ connected chan gumble.DisconnectEvent
+ serverConfigUpdate chan gumble.ServerConfigEvent
+ serverConfig gumble.ServerConfigEvent
+ tlsConfig tls.Config
+
+ *bridge.Config
+}
+
+func New(cfg *bridge.Config) bridge.Bridger {
+ b := &Bmumble{}
+ b.Config = cfg
+ b.Nick = b.GetString("Nick")
+ b.local = make(chan config.Message)
+ b.running = make(chan error)
+ b.connected = make(chan gumble.DisconnectEvent)
+ b.serverConfigUpdate = make(chan gumble.ServerConfigEvent)
+ return b
+}
+
+func (b *Bmumble) Connect() error {
+ b.Log.Infof("Connecting %s", b.GetString("Server"))
+ host, portstr, err := net.SplitHostPort(b.GetString("Server"))
+ if err != nil {
+ return err
+ }
+ b.Host = host
+ _, err = strconv.Atoi(portstr)
+ if err != nil {
+ return err
+ }
+
+ if err = b.buildTLSConfig(); err != nil {
+ return err
+ }
+
+ go b.doSend()
+ go b.connectLoop()
+ err = <-b.running
+ return err
+}
+
+func (b *Bmumble) Disconnect() error {
+ return b.client.Disconnect()
+}
+
+func (b *Bmumble) JoinChannel(channel config.ChannelInfo) error {
+ cid, err := strconv.ParseUint(channel.Name, 10, 32)
+ if err != nil {
+ return err
+ }
+ channelID := uint32(cid)
+ if b.Channel != nil && *b.Channel != channelID {
+ b.Log.Fatalf("Cannot join channel ID '%d', already joined to channel ID %d", channelID, *b.Channel)
+ return errors.New("the Mumble bridge can only join a single channel")
+ }
+ b.Channel = &channelID
+ return b.doJoin(b.client, channelID)
+}
+
+func (b *Bmumble) Send(msg config.Message) (string, error) {
+ // Only process text messages
+ b.Log.Debugf("=> Received local message %#v", msg)
+ if msg.Event != "" && msg.Event != config.EventUserAction {
+ return "", nil
+ }
+
+ attachments := b.extractFiles(&msg)
+ b.local <- msg
+ for _, a := range attachments {
+ b.local <- a
+ }
+ return "", nil
+}
+
+func (b *Bmumble) buildTLSConfig() error {
+ b.tlsConfig = tls.Config{}
+ // Load TLS client certificate keypair required for registered user authentication
+ if cpath := b.GetString("TLSClientCertificate"); cpath != "" {
+ if ckey := b.GetString("TLSClientKey"); ckey != "" {
+ cert, err := tls.LoadX509KeyPair(cpath, ckey)
+ if err != nil {
+ return err
+ }
+ b.tlsConfig.Certificates = []tls.Certificate{cert}
+ }
+ }
+ // Load TLS CA used for server verification. If not provided, the Go system trust anchor is used
+ if capath := b.GetString("TLSCACertificate"); capath != "" {
+ ca, err := ioutil.ReadFile(capath)
+ if err != nil {
+ return err
+ }
+ b.tlsConfig.RootCAs = x509.NewCertPool()
+ b.tlsConfig.RootCAs.AppendCertsFromPEM(ca)
+ }
+ b.tlsConfig.InsecureSkipVerify = b.GetBool("SkipTLSVerify")
+ return nil
+}
+
+func (b *Bmumble) connectLoop() {
+ firstConnect := true
+ for {
+ err := b.doConnect()
+ if firstConnect {
+ b.running <- err
+ }
+ if err != nil {
+ b.Log.Errorf("Connection to server failed: %#v", err)
+ if firstConnect {
+ break
+ } else {
+ b.Log.Info("Retrying in 10s")
+ time.Sleep(10 * time.Second)
+ continue
+ }
+ }
+ firstConnect = false
+ d := <-b.connected
+ switch d.Type {
+ case gumble.DisconnectError:
+ b.Log.Errorf("Lost connection to the server (%s), attempting reconnect", d.String)
+ continue
+ case gumble.DisconnectKicked:
+ b.Log.Errorf("Kicked from the server (%s), attempting reconnect", d.String)
+ continue
+ case gumble.DisconnectBanned:
+ b.Log.Errorf("Banned from the server (%s), not attempting reconnect", d.String)
+ close(b.connected)
+ close(b.running)
+ return
+ case gumble.DisconnectUser:
+ b.Log.Infof("Disconnect successful")
+ close(b.connected)
+ close(b.running)
+ return
+ }
+ }
+}
+
+func (b *Bmumble) doConnect() error {
+ // Create new gumble config and attach event handlers
+ gumbleConfig := gumble.NewConfig()
+ gumbleConfig.Attach(gumbleutil.Listener{
+ ServerConfig: b.handleServerConfig,
+ TextMessage: b.handleTextMessage,
+ Connect: b.handleConnect,
+ Disconnect: b.handleDisconnect,
+ UserChange: b.handleUserChange,
+ })
+ gumbleConfig.Username = b.GetString("Nick")
+ if password := b.GetString("Password"); password != "" {
+ gumbleConfig.Password = password
+ }
+
+ client, err := gumble.DialWithDialer(new(net.Dialer), b.GetString("Server"), gumbleConfig, &b.tlsConfig)
+ if err != nil {
+ return err
+ }
+ b.client = client
+ return nil
+}
+
+func (b *Bmumble) doJoin(client *gumble.Client, channelID uint32) error {
+ channel, ok := client.Channels[channelID]
+ if !ok {
+ return fmt.Errorf("no channel with ID %d", channelID)
+ }
+ client.Self.Move(channel)
+ return nil
+}
+
+func (b *Bmumble) doSend() {
+ // Message sending loop that makes sure server-side
+ // restrictions and client-side message traits don't conflict
+ // with each other.
+ for {
+ select {
+ case serverConfig := <-b.serverConfigUpdate:
+ b.Log.Debugf("Received server config update: AllowHTML=%#v, MaximumMessageLength=%#v", serverConfig.AllowHTML, serverConfig.MaximumMessageLength)
+ b.serverConfig = serverConfig
+ case msg := <-b.local:
+ b.processMessage(&msg)
+ }
+ }
+}
+
+func (b *Bmumble) processMessage(msg *config.Message) {
+ b.Log.Debugf("Processing message %s", msg.Text)
+
+ allowHTML := true
+ if b.serverConfig.AllowHTML != nil {
+ allowHTML = *b.serverConfig.AllowHTML
+ }
+
+ // If this is a specially generated image message, send it unmodified
+ if msg.Event == "mumble_image" {
+ if allowHTML {
+ b.client.Self.Channel.Send(msg.Username+msg.Text, false)
+ } else {
+ b.Log.Info("Can't send image, server does not allow HTML messages")
+ }
+ return
+ }
+
+ // Don't process empty messages
+ if len(msg.Text) == 0 {
+ return
+ }
+ // If HTML is allowed, convert markdown into HTML, otherwise strip markdown
+ if allowHTML {
+ msg.Text = helper.ParseMarkdown(msg.Text)
+ } else {
+ msg.Text = stripmd.Strip(msg.Text)
+ }
+
+ // If there is a maximum message length, split and truncate the lines
+ var msgLines []string
+ if maxLength := b.serverConfig.MaximumMessageLength; maxLength != nil {
+ msgLines = helper.GetSubLines(msg.Text, *maxLength-len(msg.Username))
+ } else {
+ msgLines = helper.GetSubLines(msg.Text, 0)
+ }
+ // Send the individual lindes
+ for i := range msgLines {
+ b.client.Self.Channel.Send(msg.Username+msgLines[i], false)
+ }
+}