forked from lug/matterbridge
		
	 663850a2b8
			
		
	
	663850a2b8
	
	
	
		
			
			* Mumble: Implement a workaround to signal Opus support without pulling in the CGO gopus dependency. * mumble: lowercase error messages * mumble: Add link to #1750 in bridge/mumble/codec.go
		
			
				
	
	
		
			264 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			264 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package bmumble
 | |
| 
 | |
| import (
 | |
| 	"crypto/tls"
 | |
| 	"crypto/x509"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"io/ioutil"
 | |
| 	"net"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 	"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
 | |
| 	}
 | |
| 
 | |
| 	registerNullCodecAsOpus()
 | |
| 	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), b.GetString("MessageClipped"))
 | |
| 	} else {
 | |
| 		msgLines = helper.GetSubLines(msg.Text, 0, b.GetString("MessageClipped"))
 | |
| 	}
 | |
| 	// Send the individual lines
 | |
| 	for i := range msgLines {
 | |
| 		// Remove unnecessary newline character, since either way we're sending it as individual lines
 | |
| 		msgLines[i] = strings.TrimSuffix(msgLines[i], "\n")
 | |
| 		b.client.Self.Channel.Send(msg.Username+msgLines[i], false)
 | |
| 	}
 | |
| }
 |