Add Mumble bridge

This commit is contained in:
s3lph 2020-09-24 00:20:22 +02:00
parent 91c58ec027
commit 7c3e12cabd
6 changed files with 343 additions and 0 deletions

View File

@ -213,6 +213,7 @@ type BridgeValues struct {
WhatsApp map[string]Protocol // TODO is this struct used? Search for "SlackLegacy" for example didn't return any results
Zulip map[string]Protocol
Keybase map[string]Protocol
Mumble map[string]Protocol
General Protocol
Tengo Tengo
Gateway []Gateway

82
bridge/mumble/handlers.go Normal file
View File

@ -0,0 +1,82 @@
package bmumble
import (
"github.com/42wim/matterbridge/bridge/config"
"layeh.com/gumble/gumble"
"layeh.com/gumble/gumbleutil"
)
func (b *Bmumble) handleServerConfig(event *gumble.ServerConfigEvent) {
}
func (b *Bmumble) handleTextMessage(event *gumble.TextMessageEvent) {
rmsg := config.Message{
Text: event.TextMessage.Message,
Channel: event.Client.Self.Channel.Name,
Username: event.TextMessage.Sender.Name,
UserID: event.TextMessage.Sender.Name + "@" + b.Host,
Account: b.Account,
}
b.Log.Debug("<= Remote message is %+v", rmsg)
b.Remote <- rmsg
}
func (b *Bmumble) handleConnect(event *gumble.ConnectEvent) {
// Set the user's "bio"/comment
if comment := b.GetString("UserComment"); comment != "" {
event.Client.Self.SetComment(comment)
}
// No need to talk or listen
event.Client.Self.SetSelfDeafened(true)
event.Client.Self.SetSelfMuted(true)
// if the Channel variable is set, this is a reconnect -> rejoin channel
if b.Channel != "" {
b.doJoin(event.Client, b.Channel)
b.Remote <- config.Message{
Username: "system",
Text: "rejoin",
Channel: "",
Account: b.Account,
Event: config.EventRejoinChannels,
}
}
}
func (b *Bmumble) handleUserChange(event *gumble.UserChangeEvent) {
// Only care about changes to self
if event.User != event.Client.Self {
return
}
// Someone attempted to move the user out of the configured channel; attempt to join back
if b.Channel != "" && b.Channel != event.Client.Self.Channel.Name {
b.doJoin(event.Client, b.Channel)
}
}
func (b *Bmumble) handleDisconnect(event *gumble.DisconnectEvent) {
b.connected <- disconnect{event.Type, event.String}
}
func (b *Bmumble) makeDebugHandler() (*gumbleutil.Listener) {
handler := gumbleutil.Listener{
Connect: func(e *gumble.ConnectEvent) { b.Log.Debugf("Received connect event: %+v", e) },
Disconnect: func(e *gumble.DisconnectEvent) { b.Log.Debugf("Received disconnect event: %+v", e) },
TextMessage: func(e *gumble.TextMessageEvent) { b.Log.Debugf("Received textmessage event: %+v", e) },
UserChange: func(e *gumble.UserChangeEvent) { b.Log.Debugf("Received userchange event: %+v", e) },
ChannelChange: func(e *gumble.ChannelChangeEvent) { b.Log.Debugf("Received channelchange event: %+v", e) },
PermissionDenied: func(e *gumble.PermissionDeniedEvent) { b.Log.Debugf("Received permissiondenied event: %+v", e) },
UserList: func(e *gumble.UserListEvent) { b.Log.Debugf("Received userlist event: %+v", e) },
ACL: func(e *gumble.ACLEvent) { b.Log.Debugf("Received acl event: %+v", e) },
BanList: func(e *gumble.BanListEvent) { b.Log.Debugf("Received banlist event: %+v", e) },
ContextActionChange: func(e *gumble.ContextActionChangeEvent) { b.Log.Debugf("Received contextactionchange event: %+v", e) },
ServerConfig: func(e *gumble.ServerConfigEvent) { b.Log.Debugf("Received serverconfig event: %+v", e) },
}
return &handler
}

243
bridge/mumble/mumble.go Normal file
View File

@ -0,0 +1,243 @@
package bmumble
import (
"crypto/tls"
"crypto/x509"
"errors"
"io/ioutil"
"net"
"strconv"
// "strings"
"time"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"layeh.com/gumble/gumble"
"layeh.com/gumble/gumbleutil"
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 string
local chan config.Message
running chan error
connected chan disconnect
serverConfigUpdate chan *gumble.ServerConfigEvent
serverConfig gumble.ServerConfigEvent
tlsConfig tls.Config
*bridge.Config
}
type disconnect struct {
Reason gumble.DisconnectType
Message string
}
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 disconnect)
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
}
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")
go b.doSend()
go b.connectLoop()
err = <-b.running
return err
}
func (b *Bmumble) Disconnect() error {
b.client.Disconnect()
return nil
}
func (b *Bmumble) JoinChannel(channel config.ChannelInfo) error {
if b.Channel != "" {
b.Log.Fatalf("Cannot join channel '%s', already joined to channel '%'s", channel.Name, b.Channel)
return errors.New("The Mumble bridge can only join a single channel")
}
b.Channel = channel.Name
return b.doJoin(b.client, channel.Name)
}
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
}
b.local <- msg
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.Reason {
case gumble.DisconnectError:
b.Log.Errorf("Lost connection to the server (%s), attempting reconnect", d.Message)
continue
case gumble.DisconnectKicked:
b.Log.Errorf("Kicked from the server (%s), attempting reconnect", d.Message)
continue
case gumble.DisconnectBanned:
b.Log.Errorf("Banned from the server (%s), not attempting reconnect", d.Message)
break
case gumble.DisconnectUser:
b.Log.Infof("Disconnect successful")
break
}
}
close(b.connected)
close(b.running)
}
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,
})
if b.GetInt("DebugLevel") == 0 {
gumbleConfig.Attach(b.makeDebugHandler())
}
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, name string) error {
c := client.Channels.Find(name)
if c == nil {
return errors.New("No such channel: " + name)
}
client.Self.Move(c)
b.Channel = c.Name
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 config := <-b.serverConfigUpdate:
b.Log.Debugf("Received server config update: AllowHTML=%d, MaxMessageLength=%d", config.AllowHTML, config.MaximumMessageLength)
b.serverConfig = *config
case msg := <-b.local:
b.processMessage(&msg)
}
}
}
func (b *Bmumble) processMessage(msg *config.Message) {
b.Log.Debugf("Processing message %s", msg.Text)
// If HTML is allowed, convert markdown into HTML, otherwise strip markdown
if allowHtml := b.serverConfig.AllowHTML; allowHtml == nil || !*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)
} else {
msgLines = helper.GetSubLines(msg.Text, 0)
}
// Send the individual lindes
for i := range msgLines {
b.Log.Debugf("Sending line: %s", msgLines[i])
b.client.Self.Channel.Send(msg.Username + msgLines[i], false)
}
}

View File

@ -0,0 +1,11 @@
// +build !nomumble
package bridgemap
import (
bmumble "github.com/42wim/matterbridge/bridge/mumble"
)
func init() {
FullMap["mumble"] = bmumble.New
}

1
go.mod
View File

@ -51,6 +51,7 @@ require (
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43
gomod.garykim.dev/nc-talk v0.1.3
gopkg.in/olahol/melody.v1 v1.0.0-20170518105555-d52139073376
layeh.com/gumble v0.0.0-20200818122324-146f9205029b
)
go 1.13

5
go.sum
View File

@ -145,6 +145,7 @@ github.com/d5/tengo/v2 v2.6.0/go.mod h1:XRGjEs5I9jYIKTxly6HCF8oiiilk5E/RYXOZ5b0D
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dchote/go-openal v0.0.0-20171116030048-f4a9a141d372/go.mod h1:74z+CYu2/mx4N+mcIS/rsvfAxBPBV9uv8zRAnwyFkdI=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3/go.mod h1:hEfFauPHz7+NnjR/yHJGhrKo1Za+zStgwUETx3yzqgY=
@ -385,6 +386,7 @@ github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
github.com/layeh/gumble v0.0.0-20200818122324-146f9205029b h1:YsS4Skllep7D8lIg/BYKcVwfXjftkX8hZgTJzCZaUUc=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.4.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lrstanley/girc v0.0.0-20190801035559-4fc93959e1a7 h1:BS9tqL0OCiOGuy/CYYk2gc33fxqaqh5/rhqMKu4tcYA=
@ -1134,6 +1136,9 @@ honnef.co/go/tools v0.0.1-2020.1.3 h1:sXmLre5bzIR6ypkjXCDI3jHPssRhc8KD/Ome589sc3
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
layeh.com/gopus v0.0.0-20161224163843-0ebf989153aa/go.mod h1:AOef7vHz0+v4sWwJnr0jSyHiX/1NgsMoaxl+rEPz/I0=
layeh.com/gumble v0.0.0-20200818122324-146f9205029b h1:Kne6wkHqbqrygRsqs5XUNhSs84DFG5TYMeCkCbM56sY=
layeh.com/gumble v0.0.0-20200818122324-146f9205029b/go.mod h1:tWPVA9ZAfImNwabjcd9uDE+Mtz0Hfs7a7G3vxrnrwyc=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/goversion v1.2.0 h1:SPn+NLTiAG7w30IRK/DKp1BjvpWabYgxlLp/+kx5J8w=
rsc.io/goversion v1.2.0/go.mod h1:Eih9y/uIBS3ulggl7KNJ09xGSLcuNaLgmvvqa07sgfo=