diff --git a/bridge/config/config.go b/bridge/config/config.go index f34da511..a1bce8dc 100644 --- a/bridge/config/config.go +++ b/bridge/config/config.go @@ -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 diff --git a/bridge/mumble/handlers.go b/bridge/mumble/handlers.go new file mode 100644 index 00000000..02da72d9 --- /dev/null +++ b/bridge/mumble/handlers.go @@ -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 +} diff --git a/bridge/mumble/mumble.go b/bridge/mumble/mumble.go new file mode 100644 index 00000000..33f1dc94 --- /dev/null +++ b/bridge/mumble/mumble.go @@ -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) + } + +} diff --git a/gateway/bridgemap/bmumble.go b/gateway/bridgemap/bmumble.go new file mode 100644 index 00000000..7b9241fe --- /dev/null +++ b/gateway/bridgemap/bmumble.go @@ -0,0 +1,11 @@ +// +build !nomumble + +package bridgemap + +import ( + bmumble "github.com/42wim/matterbridge/bridge/mumble" +) + +func init() { + FullMap["mumble"] = bmumble.New +} diff --git a/go.mod b/go.mod index f0c244d2..1311a7ae 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 97cb5b85..b68b0d7d 100644 --- a/go.sum +++ b/go.sum @@ -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=