diff --git a/README.md b/README.md index db451dab..42189ed8 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Click on one of the badges below to join the chat ![matterbridge.gif](https://github.com/42wim/matterbridge/blob/master/img/matterbridge.gif) -Simple bridge between IRC, XMPP, Gitter, Mattermost, Slack, Discord, Telegram, Rocket.Chat, Hipchat(via xmpp), Matrix, Steam and ssh-chat +Simple bridge between IRC, XMPP, Gitter, Mattermost, Slack, Discord, Telegram, Rocket.Chat, Hipchat(via xmpp), Matrix, Steam, ssh-chat and Zulip Has a REST API. Minecraft server chat support via [MatterLink](https://github.com/elytra/MatterLink) @@ -62,6 +62,7 @@ Accounts to one of the supported bridges * [Steam](https://store.steampowered.com/) * [Twitch](https://twitch.tv) * [Ssh-chat](https://github.com/shazow/ssh-chat) +* [Zulip](https://zulipchat.com) # Screenshots See https://github.com/42wim/matterbridge/wiki @@ -189,6 +190,7 @@ Matterbridge wouldn't exist without these libraries: * 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 * irc - https://github.com/lrstanley/girc * mattermost - https://github.com/mattermost/platform * matrix - https://github.com/matrix-org/gomatrix diff --git a/bridge/config/config.go b/bridge/config/config.go index 1b92f83c..0fc41412 100644 --- a/bridge/config/config.go +++ b/bridge/config/config.go @@ -107,6 +107,7 @@ type Protocol struct { StripNick bool // all protocols Team string // mattermost Token string // gitter, slack, discord, api + Topic string // zulip URL string // mattermost, slack // DEPRECATED UseAPI bool // mattermost, slack UseSASL bool // IRC @@ -159,6 +160,7 @@ type ConfigValues struct { Telegram map[string]Protocol Rocketchat map[string]Protocol Sshchat map[string]Protocol + Zulip map[string]Protocol General Protocol Gateway []Gateway SameChannelGateway []SameChannelGateway diff --git a/bridge/zulip/zulip.go b/bridge/zulip/zulip.go new file mode 100644 index 00000000..ebeabc1c --- /dev/null +++ b/bridge/zulip/zulip.go @@ -0,0 +1,170 @@ +package bzulip + +import ( + "encoding/json" + "io/ioutil" + "strconv" + "time" + + "github.com/42wim/matterbridge/bridge" + "github.com/42wim/matterbridge/bridge/config" + "github.com/42wim/matterbridge/bridge/helper" + gzb "github.com/matterbridge/gozulipbot" +) + +type Bzulip struct { + q *gzb.Queue + bot *gzb.Bot + streams map[int]string + *bridge.Config +} + +func New(cfg *bridge.Config) bridge.Bridger { + return &Bzulip{Config: cfg, streams: make(map[int]string)} +} + +func (b *Bzulip) Connect() error { + bot := gzb.Bot{APIKey: b.GetString("token"), APIURL: b.GetString("server") + "/api/v1/", Email: b.GetString("login")} + bot.Init() + q, err := bot.RegisterAll() + b.q = q + b.bot = &bot + if err != nil { + b.Log.Errorf("Connect() %#v", err) + return err + } + // init stream + b.getChannel(0) + b.Log.Info("Connection succeeded") + go b.handleQueue() + return nil +} + +func (b *Bzulip) Disconnect() error { + return nil +} + +func (b *Bzulip) JoinChannel(channel config.ChannelInfo) error { + return nil +} + +func (b *Bzulip) Send(msg config.Message) (string, error) { + b.Log.Debugf("=> Receiving %#v", msg) + + // Delete message + if msg.Event == config.EVENT_MSG_DELETE { + if msg.ID == "" { + return "", nil + } + _, err := b.bot.UpdateMessage(msg.ID, "") + return "", err + } + + // Upload a file if it exists + if msg.Extra != nil { + for _, rmsg := range helper.HandleExtra(&msg, b.General) { + b.sendMessage(rmsg) + } + if len(msg.Extra["file"]) > 0 { + return b.handleUploadFile(&msg) + } + } + + // edit the message if we have a msg ID + if msg.ID != "" { + _, err := b.bot.UpdateMessage(msg.ID, msg.Username+msg.Text) + return "", err + } + + // Post normal message + return b.sendMessage(msg) +} + +func (b *Bzulip) getChannel(id int) string { + if name, ok := b.streams[id]; ok { + return name + } + streams, err := b.bot.GetRawStreams() + if err != nil { + b.Log.Errorf("getChannel: %#v", err) + return "" + } + for _, stream := range streams.Streams { + b.streams[stream.StreamID] = stream.Name + } + if name, ok := b.streams[id]; ok { + return name + } + return "" +} + +func (b *Bzulip) handleQueue() error { + for { + messages, _ := b.q.GetEvents() + for _, m := range messages { + b.Log.Debugf("== Receiving %#v", m) + // ignore our own messages + if m.SenderEmail == b.GetString("login") { + continue + } + rmsg := config.Message{Username: m.SenderFullName, Text: m.Content, Channel: b.getChannel(m.StreamID), Account: b.Account, UserID: strconv.Itoa(m.SenderID), Avatar: m.AvatarURL} + b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account) + b.Log.Debugf("<= Message is %#v", rmsg) + b.Remote <- rmsg + b.q.LastEventID = m.ID + } + time.Sleep(time.Second * 3) + } +} + +func (b *Bzulip) sendMessage(msg config.Message) (string, error) { + topic := "matterbridge" + if b.GetString("topic") != "" { + topic = b.GetString("topic") + } + m := gzb.Message{ + Stream: msg.Channel, + Topic: topic, + Content: msg.Username + msg.Text, + } + resp, err := b.bot.Message(m) + if err != nil { + return "", err + } + if resp != nil { + defer resp.Body.Close() + res, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + var jr struct { + ID int `json:"id"` + } + err = json.Unmarshal(res, &jr) + if err != nil { + return "", err + } + return strconv.Itoa(jr.ID), nil + } + return "", nil +} + +func (b *Bzulip) handleUploadFile(msg *config.Message) (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.sendMessage(*msg) + if err != nil { + return "", err + } + } + return "", nil +} diff --git a/gateway/gateway.go b/gateway/gateway.go index 0dddf897..eb5bbe92 100644 --- a/gateway/gateway.go +++ b/gateway/gateway.go @@ -17,6 +17,7 @@ import ( "github.com/42wim/matterbridge/bridge/steam" "github.com/42wim/matterbridge/bridge/telegram" "github.com/42wim/matterbridge/bridge/xmpp" + "github.com/42wim/matterbridge/bridge/zulip" log "github.com/sirupsen/logrus" // "github.com/davecgh/go-spew/spew" "crypto/sha1" @@ -62,6 +63,7 @@ var bridgeMap = map[string]bridge.Factory{ "steam": bsteam.New, "telegram": btelegram.New, "xmpp": bxmpp.New, + "zulip": bzulip.New, } func init() { diff --git a/matterbridge.toml.sample b/matterbridge.toml.sample index f814f1a7..d28dc8bd 100644 --- a/matterbridge.toml.sample +++ b/matterbridge.toml.sample @@ -1155,6 +1155,90 @@ StripNick=false #OPTIONAL (default false) ShowTopicChange=false +################################################################### +#zulip section +################################################################### +[zulip] +#You can configure multiple servers "[zulip.name]" or "[zulip.name2]" +#In this example we use [zulip.streamchat] +#REQUIRED + +[zulip.streamchat] +#Token to connect with zulip API (called bot API key in Settings - Your bots) +#REQUIRED +Token="Yourtokenhere" + +#Username of the bot, normally called yourbot-bot@yourserver.zulipchat.com +#See username in Settings - Your bots +#REQUIRED +Login="yourbot-bot@yourserver.zulipchat.com" + +#Servername of your zulip instance +#REQUIRED +Server="https://yourserver.zulipchat.com" + +#Topic of the messages matterbridge will use +#OPTIONAL (default "matterbridge") +Topic="matterbridge" + +## RELOADABLE SETTINGS +## Settings below can be reloaded by editing the file + +#Nicks you want to ignore. +#Messages from those users will not be sent to other bridges. +#OPTIONAL +IgnoreNicks="spammer1 spammer2" + +#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"] ] + +#extra label that can be used in the RemoteNickFormat +#optional (default empty) +Label="" + +#RemoteNickFormat defines how remote users appear on this bridge +#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. +#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge +#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge +#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge +#OPTIONAL (default empty) +RemoteNickFormat="[{PROTOCOL}] <{NICK}> " + +#Enable to show users joins/parts from other bridges +#Currently works for messages from the following bridges: irc, mattermost, slack +#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 + ################################################################### #API ################################################################### @@ -1283,6 +1367,7 @@ enable=true # - encrypted rooms are not supported in matrix #steam - chatid (a large number). # The number in the URL when you click "enter chat room" in the browser + #zulip - stream (without the #) # #REQUIRED channel="#testing"