Thumbnail

rani/matterbridge.git

Clone URL: https://git.buni.party/rani/matterbridge.git

commit cc1c1fdd3fd9ddca28238bd2d1bbb9285908b268 Author: Wim <wim@42.be> Date: Wed Apr 05 23:39:15 2023 +0000 Remove gitter bridge (#2035) See https://blog.gitter.im/2023/02/13/gitter-has-fully-migrated-to-matrix/ diff --git a/README.md b/README.md index cc04db3..3e93753 100644 --- a/README.md +++ b/README.md @@ -4017 +4016 @@ Matterbridge wouldn't exist without these libraries:    - discord - <https://github.com/bwmarrin/discordgo>  - echo - <https://github.com/labstack/echo> -- gitter - <https://github.com/sromku/go-gitter>  - gops - <https://github.com/google/gops>  - gozulipbot - <https://github.com/ifo/gozulipbot>  - gumble - <https://github.com/layeh/gumble> diff --git a/bridge/gitter/gitter.go b/bridge/gitter/gitter.go deleted file mode 100644 index 486fe43..0000000 --- a/bridge/gitter/gitter.go +++ /dev/null @@ -1182 +00 @@ -package bgitter - -import ( - "fmt" - "strings" - - "github.com/42wim/go-gitter" - "github.com/42wim/matterbridge/bridge" - "github.com/42wim/matterbridge/bridge/config" - "github.com/42wim/matterbridge/bridge/helper" -) - -type Bgitter struct { - c *gitter.Gitter - User *gitter.User - Users []gitter.User - Rooms []gitter.Room - *bridge.Config -} - -func New(cfg *bridge.Config) bridge.Bridger { - return &Bgitter{Config: cfg} -} - -func (b *Bgitter) Connect() error { - var err error - b.Log.Info("Connecting") - b.c = gitter.New(b.GetString("Token")) - b.User, err = b.c.GetUser() - if err != nil { - return err - } - b.Rooms, err = b.c.GetRooms() - if err != nil { - return err - } - b.Log.Info("Connection succeeded") - return nil -} - -func (b *Bgitter) Disconnect() error { - return nil - -} - -func (b *Bgitter) JoinChannel(channel config.ChannelInfo) error { - roomID, err := b.c.GetRoomId(channel.Name) - if err != nil { - return fmt.Errorf("Could not find roomID for %v. Please create the room on gitter.im", channel.Name) - } - room, err := b.c.GetRoom(roomID) - if err != nil { - return err - } - b.Rooms = append(b.Rooms, *room) - user, err := b.c.GetUser() - if err != nil { - return err - } - _, err = b.c.JoinRoom(roomID, user.ID) - if err != nil { - return err - } - users, _ := b.c.GetUsersInRoom(roomID) - b.Users = append(b.Users, users...) - stream := b.c.Stream(roomID) - go b.c.Listen(stream) - - go func(stream *gitter.Stream, room string) { - for event := range stream.Event { - switch ev := event.Data.(type) { - case *gitter.MessageReceived: - // ignore message sent from ourselves - if ev.Message.From.ID != b.User.ID { - b.Log.Debugf("<= Sending message from %s on %s to gateway", ev.Message.From.Username, b.Account) - rmsg := config.Message{Username: ev.Message.From.Username, Text: ev.Message.Text, Channel: room, - Account: b.Account, Avatar: b.getAvatar(ev.Message.From.Username), UserID: ev.Message.From.ID, - ID: ev.Message.ID} - if strings.HasPrefix(ev.Message.Text, "@"+ev.Message.From.Username) { - rmsg.Event = config.EventUserAction - rmsg.Text = strings.Replace(rmsg.Text, "@"+ev.Message.From.Username+" ", "", -1) - } - b.Log.Debugf("<= Message is %#v", rmsg) - b.Remote <- rmsg - } - case *gitter.GitterConnectionClosed: - b.Log.Errorf("connection with gitter closed for room %s", room) - } - } - }(stream, room.URI) - return nil -} - -func (b *Bgitter) Send(msg config.Message) (string, error) { - b.Log.Debugf("=> Receiving %#v", msg) - roomID := b.getRoomID(msg.Channel) - if roomID == "" { - b.Log.Errorf("Could not find roomID for %v", msg.Channel) - return "", nil - } - - // Delete message - if msg.Event == config.EventMsgDelete { - if msg.ID == "" { - return "", nil - } - // gitter has no delete message api so we edit message to "" - _, err := b.c.UpdateMessage(roomID, msg.ID, "") - if err != nil { - return "", err - } - return "", nil - } - - // Upload a file (in gitter case send the upload URL because gitter has no native upload support) - if msg.Extra != nil { - for _, rmsg := range helper.HandleExtra(&msg, b.General) { - b.c.SendMessage(roomID, rmsg.Username+rmsg.Text) - } - if len(msg.Extra["file"]) > 0 { - return b.handleUploadFile(&msg, roomID) - } - } - - // Edit message - if msg.ID != "" { - b.Log.Debugf("updating message with id %s", msg.ID) - _, err := b.c.UpdateMessage(roomID, msg.ID, msg.Username+msg.Text) - if err != nil { - return "", err - } - return "", nil - } - - // Post normal message - resp, err := b.c.SendMessage(roomID, msg.Username+msg.Text) - if err != nil { - return "", err - } - return resp.ID, nil -} - -func (b *Bgitter) getRoomID(channel string) string { - for _, v := range b.Rooms { - if v.URI == channel { - return v.ID - } - } - return "" -} - -func (b *Bgitter) getAvatar(user string) string { - var avatar string - if b.Users != nil { - for _, u := range b.Users { - if user == u.Username { - return u.AvatarURLSmall - } - } - } - return avatar -} - -func (b *Bgitter) handleUploadFile(msg *config.Message, roomID string) (string, error) { - for _, f := range msg.Extra["file"] { - fi := f.(config.FileInfo) - if fi.Comment != "" { - msg.Text += fi.Comment + ": " - } - if fi.URL != "" { - msg.Text = fi.URL - if fi.Comment != "" { - msg.Text = fi.Comment + ": " + fi.URL - } - } - _, err := b.c.SendMessage(roomID, msg.Username+msg.Text) - if err != nil { - return "", err - } - } - return "", nil -} diff --git a/gateway/bridgemap/bgitter.go b/gateway/bridgemap/bgitter.go deleted file mode 100644 index 64b3c25..0000000 --- a/gateway/bridgemap/bgitter.go +++ /dev/null @@ -111 +00 @@ -// +build !nogitter - -package bridgemap - -import ( - bgitter "github.com/42wim/matterbridge/bridge/gitter" -) - -func init() { - FullMap["gitter"] = bgitter.New -} diff --git a/gateway/gateway_test.go b/gateway/gateway_test.go index 2170058..c1e4ab9 100644 --- a/gateway/gateway_test.go +++ b/gateway/gateway_test.go @@ -188 +186 @@ var testconfig = []byte(`  server=""  [mattermost.test]  server="" -[gitter.42wim] -server=""  [discord.test]  server=""  [slack.test] @@ -3311 +316 @@ server="" account = "irc.freenode" channel = "#wimtesting" - [[gateway.inout]] - account="gitter.42wim" - channel="42wim/testroom" - #channel="matterbridge/Lobby" - [[gateway.inout]] account = "discord.test" channel = "general" @@ -528 +456 @@ var testconfig2 = []byte(`  server=""  [mattermost.test]  server="" -[gitter.42wim] -server=""  [discord.test]  server=""  [slack.test] @@ -6710 +586 @@ server="" account = "irc.freenode" channel = "#wimtesting" - [[gateway.in]] - account="gitter.42wim" - channel="42wim/testroom" - [[gateway.inout]] account = "discord.test" channel = "general" @@ -8610 +736 @@ server="" account = "irc.freenode" channel = "#wimtesting2" - [[gateway.out]] - account="gitter.42wim" - channel="42wim/testroom" - [[gateway.out]] account = "discord.test" channel = "general2" @@ -18431 +16718 @@ func maketestRouter(input []byte) *Router {   }   return r  } +  func TestNewRouter(t *testing.T) {   r := maketestRouter(testconfig)   assert.Equal(t, 1, len(r.Gateways)) - assert.Equal(t, 4, len(r.Gateways["bridge1"].Bridges)) - assert.Equal(t, 4, len(r.Gateways["bridge1"].Channels)) + assert.Equal(t, 3, len(r.Gateways["bridge1"].Bridges)) + assert.Equal(t, 3, len(r.Gateways["bridge1"].Channels))   r = maketestRouter(testconfig2)   assert.Equal(t, 2, len(r.Gateways)) - assert.Equal(t, 4, len(r.Gateways["bridge1"].Bridges)) - assert.Equal(t, 3, len(r.Gateways["bridge2"].Bridges)) - assert.Equal(t, 4, len(r.Gateways["bridge1"].Channels)) - assert.Equal(t, 3, len(r.Gateways["bridge2"].Channels)) - assert.Equal(t, &config.ChannelInfo{ - Name: "42wim/testroom", - Direction: "out", - ID: "42wim/testroomgitter.42wim", - Account: "gitter.42wim", - SameChannel: map[string]bool{"bridge2": false}, - }, r.Gateways["bridge2"].Channels["42wim/testroomgitter.42wim"]) - assert.Equal(t, &config.ChannelInfo{ - Name: "42wim/testroom", - Direction: "in", - ID: "42wim/testroomgitter.42wim", - Account: "gitter.42wim", - SameChannel: map[string]bool{"bridge1": false}, - }, r.Gateways["bridge1"].Channels["42wim/testroomgitter.42wim"]) + assert.Equal(t, 3, len(r.Gateways["bridge1"].Bridges)) + assert.Equal(t, 2, len(r.Gateways["bridge2"].Bridges)) + assert.Equal(t, 3, len(r.Gateways["bridge1"].Channels)) + assert.Equal(t, 2, len(r.Gateways["bridge2"].Channels))   assert.Equal(t, &config.ChannelInfo{   Name: "general",   Direction: "inout", @@ -2418 +2116 @@ func TestGetDestChannel(t *testing.T) {   SameChannel: map[string]bool{"bridge1": false},   Options: config.ChannelOptions{Key: ""},   }}, r.Gateways["bridge1"].getDestChannel(msg, *br)) - case "gitter.42wim": - assert.Equal(t, []config.ChannelInfo(nil), r.Gateways["bridge1"].getDestChannel(msg, *br))   case "irc.freenode":   assert.Equal(t, []config.ChannelInfo(nil), r.Gateways["bridge1"].getDestChannel(msg, *br))   } @@ -4206 +3887 @@ func (s *ignoreTestSuite) SetupSuite() {   logger.SetOutput(ioutil.Discard)   s.gw = &Gateway{logger: logrus.NewEntry(logger)}  } +  func (s *ignoreTestSuite) TestIgnoreTextEmpty() {   extraFile := make(map[string][]interface{})   extraAttach := make(map[string][]interface{}) @@ -4617 +4306 @@ func (s *ignoreTestSuite) TestIgnoreTextEmpty() {   output := s.gw.ignoreTextEmpty(testcase.input)   s.Assert().Equalf(testcase.output, output, "case '%s' failed", testname)   } -  }    func (s *ignoreTestSuite) TestIgnoreTexts() { diff --git a/go.mod b/go.mod index e4f390c..10294bb 100644 --- a/go.mod +++ b/go.mod @@ -17 +16 @@  module github.com/42wim/matterbridge    require ( - github.com/42wim/go-gitter v0.0.0-20170828205020-017310c2d557   github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f   github.com/Benau/tgsconverter v0.0.0-20210809170556-99f4a4f6337f   github.com/Philipp15b/go-steam v1.0.1-0.20200727090957-6ae9b3c0a560 @@ -1008 +996 @@ require (   github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect   github.com/modern-go/reflect2 v1.0.2 // indirect   github.com/monaco-io/request v1.0.5 // indirect - github.com/mreiferson/go-httpclient v0.0.0-20201222173833-5e475fde3a4d // indirect - github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff // indirect   github.com/opentracing/opentracing-go v1.2.0 // indirect   github.com/pborman/uuid v1.2.1 // indirect   github.com/pelletier/go-toml v1.9.5 // indirect diff --git a/go.sum b/go.sum index c254220..e86bf96 100644 --- a/go.sum +++ b/go.sum @@ -648 +646 @@ filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5E  gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8=  git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=  git.apache.org/thrift.git v0.12.0/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= -github.com/42wim/go-gitter v0.0.0-20170828205020-017310c2d557 h1:IZtuWGfzQnKnCSu+vl8WGLhpVQ5Uvy3rlSwqXSg+sQg= -github.com/42wim/go-gitter v0.0.0-20170828205020-017310c2d557/go.mod h1:jL0YSXMs/txjtGJ4PWrmETOk6KUHMDPMshgQZlTeB3Y=  github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=  github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k=  github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= @@ -7717 +7696 @@ github.com/gopackage/ddp v0.0.3/go.mod h1:3hUXYG6C/6JsoxKsQaK7st09+GP9RZBFPzyAlU  github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=  github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=  github.com/gopherjs/gopherjs v0.0.0-20210621113107-84c6004145de/go.mod h1:MtKwTfDNYAP5EtbQSMYjTSqvj1aXJKQRASWq3bwaP+g= -github.com/gopherjs/gopherjs v0.0.0-20220221023154-0b2280d3ff96 h1:QJq7UBOuoynsywLk+aC75rC2Cbi2+lQRDaLaizhA+fA=  github.com/gopherjs/gopherjs v0.0.0-20220221023154-0b2280d3ff96/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=  github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=  github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= @@ -9587 +9556 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr  github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=  github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=  github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=  github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=  github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=  github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= @@ -120910 +12056 @@ github.com/monaco-io/request v1.0.5/go.mod h1:EmggwHktBsbJmCgwZXqy7o0H1NNsAstQBW  github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=  github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=  github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= -github.com/mreiferson/go-httpclient v0.0.0-20201222173833-5e475fde3a4d h1:tLWCMSjfL8XyZwpu1RzI2UpJSPbZCOZ6DVHQFnlpL7A= -github.com/mreiferson/go-httpclient v0.0.0-20201222173833-5e475fde3a4d/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= -github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff h1:HLGD5/9UxxfEuO9DtP8gnTmNtMxbPyhYltfxsITel8g= -github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff/go.mod h1:B8jLfIIPn2sKyWr0D7cL2v7tnrDD5z291s2Zypdu89E=  github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=  github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=  github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= @@ -152211 +15149 @@ github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9/go.mod h1:PLPIyL7i  github.com/slack-go/slack v0.12.1 h1:X97b9g2hnITDtNsNe5GkGx6O2/Sz/uC20ejRZN6QxOw=  github.com/slack-go/slack v0.12.1/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=  github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/assertions v1.0.0 h1:UVQPSSmc3qtTi+zPPkCXvZX9VvW/xT/NsRvKfwY81a8=  github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=  github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM=  github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=  github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=  github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs=  github.com/snowflakedb/glog v0.0.0-20180824191149-f5055e6f21ce/go.mod h1:EB/w24pR5VKI60ecFnKqXzxX3dOorz1rnVicQTQrGM0= diff --git a/matterbridge.toml.sample b/matterbridge.toml.sample index db41385..5932b26 100644 --- a/matterbridge.toml.sample +++ b/matterbridge.toml.sample @@ -51386 +5139 @@ ShowTopicChange=false    ###################################################################  #Gitter section -#Best to make a dedicated gitter account for the bot. +#Gitter has been moved to matrix - see https://github.com/42wim/matterbridge/issues/1969 how to migrate  ###################################################################   -[gitter] - -#You can configure multiple servers "[gitter.name]" or "[gitter.name2]" -#In this example we use [gitter.myproject] -#REQUIRED -[gitter.myproject] -#Token to connect with Gitter API -#You can get your token by going to https://developer.gitter.im/docs/welcome and SIGN IN -#REQUIRED -Token="Yourtokenhere" - -## RELOADABLE SETTINGS -## Settings below can be reloaded by editing the file - -#Nicks you want to ignore. -#Regular expressions supported -#Messages from those users will not be sent to other bridges. -#OPTIONAL -IgnoreNicks="ircspammer1 ircspammer2" - -#Messages you want to ignore. -#Messages matching these regexp will be ignored and not sent to other bridges -#See https://regex-golang.appspot.com/assets/html/index.html for more regex info -#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword -IgnoreMessages="^~~ badword" - -#messages you want to replace. -#it replaces outgoing messages from the bridge. -#so you need to place it by the sending bridge definition. -#regular expressions supported -#some examples: -#this replaces cat => dog and sleep => awake -#replacemessages=[ ["cat","dog"], ["sleep","awake"] ] -#this replaces every number with number. 123 => numbernumbernumber -#replacemessages=[ ["[0-9]","number"] ] -#optional (default empty) -ReplaceMessages=[ ["cat","dog"] ] - -#nicks you want to replace. -#see replacemessages for syntaxa -#optional (default empty) -ReplaceNicks=[ ["user--","user"] ] - -#Extractnicks is used to for example rewrite messages from other relaybots -#See https://github.com/42wim/matterbridge/issues/713 and https://github.com/42wim/matterbridge/issues/466 -#some examples: -#this replaces a message like "Relaybot: <relayeduser> something interesting" to "relayeduser: something interesting" -#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ] ] -#you can use multiple entries for multiplebots -#this also replaces a message like "otherbot: (relayeduser) something else" to "relayeduser: something else" -#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ],[ "otherbot","\\((.*?)\\)\\s+" ] -#OPTIONAL (default empty) -ExtractNicks=[ ["otherbot","<(.*?)>\\s+" ] ] - -#extra label that can be used in the RemoteNickFormat -#optional (default empty) -Label="" - -#RemoteNickFormat defines how remote users appear on this bridge -#See [general] config section for default options -RemoteNickFormat="[{PROTOCOL}] <{NICK}> " - -#Enable to show users joins/parts from other bridges -#Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord -#OPTIONAL (default false) -ShowJoinPart=false - -#StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285 -#It will strip other characters from the nick -#OPTIONAL (default false) -StripNick=false - -#Enable to show topic changes from other bridges -#Only works hiding/show topic changes from slack bridge for now -#OPTIONAL (default false) -ShowTopicChange=false -  ###################################################################  #  # Keybase