4
0
mirror of https://github.com/cwinfo/matterbridge.git synced 2025-06-26 17:49:23 +00:00

Compare commits

...

33 Commits

Author SHA1 Message Date
Wim
86865c6da5 Release v1.10.0 2018-05-07 22:07:17 +02:00
Wim
45296100df Add initial zulip support 2018-05-07 21:35:48 +02:00
Wim
1605fbc012 Add vendor matterbridge/gozulipbot 2018-05-07 21:06:25 +02:00
Wim
c6c92e273d Use only alphanumeric for file uploads to mediaserver. Closes #416 2018-05-06 20:32:09 +02:00
Wim
467b373c43 Fix crash on invalid filenames 2018-05-06 20:14:16 +02:00
Wim
72ce7f06e9 Handle file comment better 2018-05-06 16:57:59 +02:00
Wim
346a7284f7 Handle file uploads to mediaserver (steam) 2018-05-06 16:32:24 +02:00
Wim
ee4ac67081 Fix possible nil when using channels (telegram). #410 2018-05-05 23:15:50 +02:00
Wim
5a93d14d75 Update issue templates 2018-05-05 18:04:03 +02:00
Wim
96a47a60ad Add support for reloading all settings automatically after changing config except connection and gateway configuration. Closes #373 2018-05-01 22:23:37 +02:00
Wim
b24a47ad7f Handle channel posts correctly (telegram) 2018-04-29 22:31:11 +02:00
Wim
cd1fd1bb7c Fix panic (telegram). Closes #410 2018-04-29 15:46:40 +02:00
Wim
d44df7b6e6 Fix alignment 2018-04-25 22:21:16 +02:00
Wim
9d1ac0c84b Add image to repo. Make more clear that mattermost is not required to run matterbridge 2018-04-25 22:20:06 +02:00
76af9cba5a Properly set Slack user who initiated slash command (#394)
* Properly set Slack user who initiated slash command
2018-04-25 21:27:34 +02:00
Wim
b69fc30902 Fix regression in ReplaceMessages and ReplaceNicks. Closes #407 2018-04-21 23:26:39 +02:00
Wim
c3174f4de9 Update GetFileLinks to API_V4 2018-04-21 20:49:44 +02:00
Wim
99ce68e9ba Use username if bot name is Slack API Tester (slack) 2018-04-20 01:01:45 +02:00
Wim
0cf73673a9 Bump version 2018-04-20 00:39:57 +02:00
Wim
08f442dc7b Release v1.9.1 2018-04-20 00:32:11 +02:00
Wim
8a8b95228c Remove message newline (telegram). #399 2018-04-19 22:05:00 +02:00
Wim
31a752fa21 Add missing import 2018-04-19 13:04:12 +02:00
Wim
a83831e68d Remove empty newlines from messages (telegram) #399 2018-04-19 12:53:49 +02:00
a12a8d4fe2 Send mediaserver link to Discord in Webhook mode (discord) (#405) 2018-04-17 23:52:48 +02:00
Wim
e57f3a7e6c Add QuoteDisable option (telegram). Closes #399 2018-04-17 23:26:41 +02:00
Wim
68fbed9281 Make our callbackid more unique. Fixes issue with running multiple matterbridge on the same channel (slack,mattermost) 2018-04-13 22:01:03 +02:00
Wim
8bfaa007d5 Add UpdateStatus function 2018-04-01 22:53:12 +02:00
76360f89c1 Strip markdown URLs with blank text (slack) (#392) 2018-03-22 22:28:27 +01:00
Wim
d525230abd Fix bintray build
See https://github.com/travis-ci/travis-ci/issues/9314
2018-03-17 23:13:27 +01:00
Wim
b4aa637d41 Add channel debug (discord) 2018-03-17 22:56:58 +01:00
Wim
7c4334d0de Remove unused import 2018-03-17 22:54:54 +01:00
Wim
062be8d7c9 Revert #378 2018-03-17 18:02:00 +01:00
db25ee59c5 Print list of valid team names when team not found (#390) 2018-03-15 20:50:32 +01:00
27 changed files with 1305 additions and 34 deletions

26
.github/ISSUE_TEMPLATE/Bug_report.md vendored Normal file
View File

@ -0,0 +1,26 @@
---
name: Bug report
about: Create a report to help us improve. (Check the FAQ on the wiki first)
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots/debug logs**
If applicable, add screenshots to help explain your problem.
Use logs from running `matterbridge -debug` if possible.
**Environment (please complete the following information):**
- OS: [e.g. linux]
- Matterbridge version: output of `matterbridge -version`
- If self compiled: output of `git rev-parse HEAD`
**Additional context**
Please add your configuration file (be sure to exclude or anonymize private data (tokens/passwords))

View File

@ -0,0 +1,17 @@
---
name: Feature request
about: Suggest an idea for this project
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@ -43,6 +43,8 @@ script:
deploy:
provider: bintray
edge:
branch: v1.8.47
file: ci/deploy.json
user: 42wim
key:

View File

@ -5,12 +5,15 @@ Click on one of the badges below to join the chat
[![Download stable](https://img.shields.io/github/release/42wim/matterbridge.svg?label=download%20stable)](https://github.com/42wim/matterbridge/releases/latest) [![Download dev](https://img.shields.io/bintray/v/42wim/nightly/Matterbridge.svg?label=download%20dev&colorB=007ec6)](https://bintray.com/42wim/nightly/Matterbridge/_latestVersion)
![matterbridge.gif](https://s15.postimg.org/qpjhp6y3f/matterbridge.gif)
![matterbridge.gif](https://github.com/42wim/matterbridge/blob/master/img/matterbridge.gif)
Simple bridge between Mattermost, IRC, XMPP, Gitter, Slack, Discord, Telegram, Rocket.Chat, Hipchat(via xmpp), Matrix, Steam and ssh-chat
Has a REST API.
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)
**Mattermost isn't required to run matterbridge. It bridges between any supported protocol.**
(The name matterbridge is a remnant when it was only bridging mattermost)
# Table of Contents
* [Features](https://github.com/42wim/matterbridge/wiki/Features)
* [Requirements](#requirements)
@ -59,13 +62,14 @@ 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
# Installing
## Binaries
* Latest stable release [v1.9.0](https://github.com/42wim/matterbridge/releases/latest)
* Latest stable release [v1.10.0](https://github.com/42wim/matterbridge/releases/latest)
* Development releases (follows master) can be downloaded [here](https://dl.bintray.com/42wim/nightly/)
## Building
@ -186,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

View File

@ -2,8 +2,10 @@ package config
import (
"bytes"
"github.com/fsnotify/fsnotify"
log "github.com/sirupsen/logrus"
"github.com/spf13/viper"
prefixed "github.com/x-cray/logrus-prefixed-formatter"
"os"
"strings"
"sync"
@ -92,6 +94,7 @@ type Protocol struct {
Password string // IRC,mattermost,XMPP,matrix
PrefixMessagesWithNick bool // mattemost, slack
Protocol string // all protocols
QuoteDisable bool // telegram
RejoinDelay int // IRC
ReplaceMessages [][]string // all protocols
ReplaceNicks [][]string // all protocols
@ -104,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
@ -156,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
@ -168,9 +173,13 @@ type Config struct {
}
func NewConfig(cfgfile string) *Config {
log.SetFormatter(&prefixed.TextFormatter{PrefixPadding: 13, DisableColors: true, FullTimestamp: false})
flog := log.WithFields(log.Fields{"prefix": "config"})
var cfg ConfigValues
viper.SetConfigType("toml")
viper.SetConfigFile(cfgfile)
viper.SetEnvPrefix("matterbridge")
viper.AddConfigPath(".")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()
f, err := os.Open(cfgfile)
@ -190,6 +199,11 @@ func NewConfig(cfgfile string) *Config {
if cfg.General.MediaDownloadSize == 0 {
cfg.General.MediaDownloadSize = 1000000
}
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
flog.Println("Config file changed:", e.Name)
})
mycfg.ConfigValues = &cfg
return mycfg
}
@ -242,11 +256,18 @@ func (c *Config) GetStringSlice(key string) []string {
func (c *Config) GetStringSlice2D(key string) [][]string {
c.RLock()
defer c.RUnlock()
if res, ok := c.v.Get(key).([][]string); ok {
return res
result := [][]string{}
if res, ok := c.v.Get(key).([]interface{}); ok {
for _, entry := range res {
result2 := []string{}
for _, entry2 := range entry.([]interface{}) {
result2 = append(result2, entry2.(string))
}
result = append(result, result2)
}
return result
}
// log.Debugf("getting StringSlice2D %s = %#v", key, c.v.Get(key))
return [][]string{}
return result
}
func GetIconURL(msg *Message, iconURL string) string {

View File

@ -80,6 +80,9 @@ func (b *Bdiscord) Connect() error {
}
}
}
for _, channel := range b.Channels {
b.Log.Debugf("found channel %#v", channel)
}
return nil
}
@ -127,6 +130,12 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) {
return "", nil
}
b.Log.Debugf("Broadcasting using Webhook")
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
if fi.URL != "" {
msg.Text += fi.URL + " "
}
}
err := b.c.WebhookExecute(
wID,
wToken,

View File

@ -168,6 +168,9 @@ func (b *Bgitter) handleUploadFile(msg *config.Message, roomID string) (string,
}
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 {

View File

@ -7,6 +7,7 @@ import (
log "github.com/sirupsen/logrus"
"io"
"net/http"
"strings"
"time"
)
@ -88,3 +89,14 @@ func HandleDownloadData(flog *log.Entry, msg *config.Message, name, comment, url
}
msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{Name: name, Data: data, URL: url, Comment: comment, Avatar: avatar})
}
func RemoveEmptyNewLines(msg string) string {
lines := ""
for _, line := range strings.Split(msg, "\n") {
if line != "" {
lines += line + "\n"
}
}
lines = strings.TrimRight(lines, "\n")
return lines
}

View File

@ -201,6 +201,9 @@ func (b *Birc) Send(msg config.Message) (string, error) {
}
if fi.URL != "" {
msg.Text = fi.URL
if fi.Comment != "" {
msg.Text = fi.Comment + ": " + fi.URL
}
}
b.Local <- config.Message{Text: msg.Text, Username: msg.Username, Channel: msg.Channel, Event: msg.Event}
}

View File

@ -366,7 +366,7 @@ func (b *Bmattermost) sendWebhook(msg config.Message) (string, error) {
// this sends a message only if we received a config.EVENT_FILE_FAILURE_SIZE
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
matterMessage := matterhook.OMessage{IconURL: b.GetString("IconURL"), Channel: rmsg.Channel, UserName: rmsg.Username, Text: rmsg.Text, Props: make(map[string]interface{})}
matterMessage.Props["matterbridge"] = true
matterMessage.Props["matterbridge_"+b.mc.User.Id] = true
b.mh.Send(matterMessage)
}
@ -385,7 +385,7 @@ func (b *Bmattermost) sendWebhook(msg config.Message) (string, error) {
if msg.Avatar != "" {
matterMessage.IconURL = msg.Avatar
}
matterMessage.Props["matterbridge"] = true
matterMessage.Props["matterbridge_"+b.mc.User.Id] = true
err := b.mh.Send(matterMessage)
if err != nil {
b.Log.Info(err)
@ -415,7 +415,7 @@ func (b *Bmattermost) skipMessage(message *matterclient.Message) bool {
// Ignore messages sent from matterbridge
if message.Post.Props != nil {
if _, ok := message.Post.Props["matterbridge"].(bool); ok {
if _, ok := message.Post.Props["matterbridge_"+b.mc.User.Id].(bool); ok {
b.Log.Debugf("sent by matterbridge, ignoring")
return true
}

View File

@ -4,16 +4,17 @@ import (
"bytes"
"errors"
"fmt"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/42wim/matterbridge/matterhook"
"github.com/nlopes/slack"
"html"
"regexp"
"strings"
"sync"
"time"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/42wim/matterbridge/matterhook"
"github.com/nlopes/slack"
)
type Bslack struct {
@ -176,7 +177,7 @@ func (b *Bslack) Send(msg config.Message) (string, error) {
np.IconURL = msg.Avatar
}
// add a callback ID so we can see we created it
np.Attachments = append(np.Attachments, slack.Attachment{CallbackID: "matterbridge"})
np.Attachments = append(np.Attachments, slack.Attachment{CallbackID: "matterbridge_" + b.si.User.ID})
// add file attachments
np.Attachments = append(np.Attachments, b.createAttach(msg.Extra)...)
// add slack attachments (from another slack bridge)
@ -387,7 +388,11 @@ func (b *Bslack) replaceVariable(text string) string {
func (b *Bslack) replaceURL(text string) string {
results := regexp.MustCompile(`<(.*?)(\|.*?)?>`).FindAllStringSubmatch(text, -1)
for _, r := range results {
text = strings.Replace(text, r[0], r[1], -1)
if len(strings.TrimSpace(r[2])) == 1 { // A display text separator was found, but the text was blank
text = strings.Replace(text, r[0], "", -1)
} else {
text = strings.Replace(text, r[0], r[1], -1)
}
}
return text
}
@ -480,7 +485,7 @@ func (b *Bslack) handleMessageEvent(ev *slack.MessageEvent) (*config.Message, er
rmsg := config.Message{Text: ev.Text, Channel: channel.Name, Account: b.Account, ID: "slack " + ev.Timestamp, Extra: make(map[string][]interface{})}
// find the user id and name
if ev.BotID == "" && ev.SubType != messageDeleted && ev.SubType != "file_comment" {
if ev.User != "" && ev.SubType != messageDeleted && ev.SubType != "file_comment" {
user, err := b.rtm.GetUserInfo(ev.User)
if err != nil {
return nil, err
@ -504,7 +509,7 @@ func (b *Bslack) handleMessageEvent(ev *slack.MessageEvent) (*config.Message, er
}
// when using webhookURL we can't check if it's our webhook or not for now
if ev.BotID != "" && b.GetString("WebhookURL") == "" {
if rmsg.Username == "" && ev.BotID != "" && b.GetString("WebhookURL") == "" {
bot, err := b.rtm.GetBotInfo(ev.BotID)
if err != nil {
return nil, err
@ -516,6 +521,19 @@ func (b *Bslack) handleMessageEvent(ev *slack.MessageEvent) (*config.Message, er
}
rmsg.UserID = bot.ID
}
// fixes issues with matterircd users
if bot.Name == "Slack API Tester" {
user, err := b.rtm.GetUserInfo(ev.User)
if err != nil {
return nil, err
}
rmsg.UserID = user.ID
rmsg.Username = user.Name
if user.Profile.DisplayName != "" {
rmsg.Username = user.Profile.DisplayName
}
}
}
// file comments are set by the system (because there is no username given)
@ -639,7 +657,7 @@ func (b *Bslack) skipMessageEvent(ev *slack.MessageEvent) bool {
// skip messages we made ourselves
if len(ev.Attachments) > 0 {
if ev.Attachments[0].CallbackID == "matterbridge" {
if ev.Attachments[0].CallbackID == "matterbridge_"+b.si.User.ID {
return true
}
}

View File

@ -68,6 +68,9 @@ func (b *Bsshchat) Send(msg config.Message) (string, error) {
}
if fi.URL != "" {
msg.Text = fi.URL
if fi.Comment != "" {
msg.Text = fi.Comment + ": " + fi.URL
}
}
b.w.Write([]byte(msg.Username + msg.Text))
}

View File

@ -2,8 +2,10 @@ package bsteam
import (
"fmt"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/Philipp15b/go-steam"
"github.com/Philipp15b/go-steam/protocol/steamlang"
"github.com/Philipp15b/go-steam/steamid"
@ -66,6 +68,30 @@ func (b *Bsteam) Send(msg config.Message) (string, error) {
if err != nil {
return "", err
}
// Handle files
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
b.c.Social.SendMessage(id, steamlang.EChatEntryType_ChatMsg, rmsg.Username+rmsg.Text)
}
if len(msg.Extra["file"]) > 0 {
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
}
}
b.c.Social.SendMessage(id, steamlang.EChatEntryType_ChatMsg, msg.Username+msg.Text)
}
return "", nil
}
}
b.c.Social.SendMessage(id, steamlang.EChatEntryType_ChatMsg, msg.Username+msg.Text)
return "", nil
}

View File

@ -1,7 +1,6 @@
package btelegram
import (
"html"
"regexp"
"strconv"
"strings"
@ -122,7 +121,7 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
for update := range updates {
b.Log.Debugf("== Receiving event: %#v", update.Message)
if update.Message == nil && update.ChannelPost == nil {
if update.Message == nil && update.ChannelPost == nil && update.EditedMessage == nil && update.EditedChannelPost == nil {
b.Log.Error("Getting nil messages, this shouldn't happen.")
continue
}
@ -134,6 +133,7 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
// handle channels
if update.ChannelPost != nil {
message = update.ChannelPost
rmsg.Text = message.Text
}
// edited channel message
@ -145,6 +145,7 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
// handle groups
if update.Message != nil {
message = update.Message
rmsg.Text = message.Text
}
// edited group message
@ -155,11 +156,11 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
// set the ID's from the channel or group message
rmsg.ID = strconv.Itoa(message.MessageID)
rmsg.UserID = strconv.Itoa(message.From.ID)
rmsg.Channel = strconv.FormatInt(message.Chat.ID, 10)
// handle username
if message.From != nil {
rmsg.UserID = strconv.Itoa(message.From.ID)
if b.GetBool("UseFirstName") {
rmsg.Username = message.From.FirstName
}
@ -169,7 +170,6 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
rmsg.Username = message.From.FirstName
}
}
rmsg.Text += message.Text
// only download avatars if we have a place to upload them (configured mediaserver)
if b.General.MediaServerUpload != "" {
b.handleDownloadAvatar(message.From.ID, rmsg.Channel)
@ -222,11 +222,17 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
if usernameReply == "" {
usernameReply = "unknown"
}
rmsg.Text = rmsg.Text + " (re @" + usernameReply + ":" + message.ReplyToMessage.Text + ")"
if !b.GetBool("QuoteDisable") {
rmsg.Text = rmsg.Text + " (re @" + usernameReply + ":" + message.ReplyToMessage.Text + ")"
}
}
if rmsg.Text != "" || len(rmsg.Extra) > 0 {
rmsg.Avatar = helper.GetAvatar(b.avatarMap, strconv.Itoa(message.From.ID), b.General)
rmsg.Text = helper.RemoveEmptyNewLines(rmsg.Text)
// channels don't have (always?) user information. see #410
if message.From != nil {
rmsg.Avatar = helper.GetAvatar(b.avatarMap, strconv.Itoa(message.From.ID), b.General)
}
b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
@ -385,7 +391,6 @@ func (b *Btelegram) sendMessage(chatid int64, username, text string) (string, er
m.Text = username + text
if b.GetString("MessageFormat") == "HTML" {
b.Log.Debug("Using mode HTML")
username = html.EscapeString(username)
m.Text = username + text
m.ParseMode = tgbotapi.ModeHTML
}

View File

@ -186,7 +186,10 @@ func (b *Bxmpp) handleUploadFile(msg *config.Message) (string, error) {
msg.Text += fi.Comment + ": "
}
if fi.URL != "" {
msg.Text += fi.URL
msg.Text = fi.URL
if fi.Comment != "" {
msg.Text = fi.Comment + ": " + fi.URL
}
}
_, err := b.xc.Send(xmpp.Chat{Type: "groupchat", Remote: msg.Channel + "@" + b.GetString("Muc"), Text: msg.Username + msg.Text})
if err != nil {

170
bridge/zulip/zulip.go Normal file
View File

@ -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
}

View File

@ -1,3 +1,34 @@
# v1.10.0
## New features
* general: Add support for reloading all settings automatically after changing config except connection and gateway configuration. Closes #373
* zulip: New protocol support added (https://zulipchat.com)
## Enhancements
* general: Handle file comment better
* steam: Handle file uploads to mediaserver (steam)
* slack: Properly set Slack user who initiated slash command (#394)
## Bugfix
* general: Use only alphanumeric for file uploads to mediaserver. Closes #416
* general: Fix crash on invalid filenames
* general: Fix regression in ReplaceMessages and ReplaceNicks. Closes #407
* telegram: Fix possible nil when using channels (telegram). #410
* telegram: Fix panic (telegram). Closes #410
* telegram: Handle channel posts correctly
* mattermost: Update GetFileLinks to API_V4
# v1.9.1
## New features
* telegram: Add QuoteDisable option (telegram). Closes #399. See QuoteDisable in matterbridge.toml.sample
## Enhancements
* discord: Send mediaserver link to Discord in Webhook mode (discord) (#405)
* mattermost: Print list of valid team names when team not found (#390)
* slack: Strip markdown URLs with blank text (slack) (#392)
## Bugfix
* slack/mattermost: Make our callbackid more unique. Fixes issue with running multiple matterbridge on the same channel (slack,mattermost)
* telegram: fix newlines in multiline messages #399
* telegram: Revert #378
# v1.9.0 (the refactor release)
## New features
* general: better debug messages

View File

@ -17,12 +17,14 @@ 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"
"github.com/hashicorp/golang-lru"
"github.com/peterhellberg/emojilib"
"net/http"
"path/filepath"
"regexp"
"strings"
"time"
@ -61,6 +63,7 @@ var bridgeMap = map[string]bridge.Factory{
"steam": bsteam.New,
"telegram": btelegram.New,
"xmpp": bxmpp.New,
"zulip": bzulip.New,
}
func init() {
@ -409,6 +412,7 @@ func (gw *Gateway) modifyMessage(msg *config.Message) {
}
func (gw *Gateway) handleFiles(msg *config.Message) {
reg := regexp.MustCompile("[^a-zA-Z0-9]+")
// if we don't have a attachfield or we don't have a mediaserver configured return
if msg.Extra == nil || gw.Config.General.MediaServerUpload == "" {
return
@ -421,15 +425,23 @@ func (gw *Gateway) handleFiles(msg *config.Message) {
}
for i, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
ext := filepath.Ext(fi.Name)
fi.Name = fi.Name[0 : len(fi.Name)-len(ext)]
fi.Name = reg.ReplaceAllString(fi.Name, "_")
fi.Name = fi.Name + ext
sha1sum := fmt.Sprintf("%x", sha1.Sum(*fi.Data))
reader := bytes.NewReader(*fi.Data)
url := gw.Config.General.MediaServerUpload + "/" + sha1sum + "/" + fi.Name
durl := gw.Config.General.MediaServerDownload + "/" + sha1sum + "/" + fi.Name
extra := msg.Extra["file"][i].(config.FileInfo)
extra.URL = durl
req, _ := http.NewRequest("PUT", url, reader)
req, err := http.NewRequest("PUT", url, reader)
if err != nil {
flog.Errorf("mediaserver upload failed: %#v", err)
continue
}
req.Header.Set("Content-Type", "binary/octet-stream")
_, err := client.Do(req)
_, err = client.Do(req)
if err != nil {
flog.Errorf("mediaserver upload failed: %#v", err)
continue

BIN
img/matterbridge.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

View File

@ -13,7 +13,7 @@ import (
)
var (
version = "1.9.0"
version = "1.10.0"
githash string
)

View File

@ -64,6 +64,9 @@ NickServPassword="secret"
#OPTIONAL only used for quakenet auth
NickServUsername="username"
## RELOADABLE SETTINGS
## Settings below can be reloaded by editing the file
#Flood control
#Delay in milliseconds between each message send to the IRC server
#OPTIONAL (default 1300)
@ -184,6 +187,9 @@ Nick="xmppbot"
#OPTIONAL (default false)
SkipTLSVerify=true
## 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
@ -265,6 +271,9 @@ Muc="conf.hipchat.com"
#REQUIRED
Nick="yourlogin"
## 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
@ -382,6 +391,9 @@ IconURL="http://youricon.png"
#OPTIONAL (default false)
SkipTLSVerify=true
## RELOADABLE SETTINGS
## Settings below can be reloaded by editing the file
#how to format the list of IRC nicks when displayed in mattermost.
#Possible options are "table" and "plain"
#OPTIONAL (default plain)
@ -482,6 +494,9 @@ ShowTopicChange=false
#REQUIRED
Token="Yourtokenhere"
## 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
@ -577,6 +592,9 @@ WebhookBindAddress="0.0.0.0:9999"
#OPTIONAL
IconURL="https://robohash.org/{NICK}.png?size=48x48"
## RELOADABLE SETTINGS
## Settings below can be reloaded by editing the file
#how to format the list of IRC nicks when displayed in slack
#Possible options are "table" and "plain"
#OPTIONAL (default plain)
@ -680,6 +698,9 @@ Token="Yourtokenhere"
#REQUIRED
Server="yourservername"
## RELOADABLE SETTINGS
## Settings below can be reloaded by editing the file
#Shows title, description and URL of embedded messages (sent by other bots)
#OPTIONAL (default false)
ShowEmbeds=false
@ -770,6 +791,9 @@ ShowTopicChange=false
#REQUIRED
Token="Yourtokenhere"
## RELOADABLE SETTINGS
## Settings below can be reloaded by editing the file
#OPTIONAL (default empty)
#Only supported format is "HTML", messages will be sent in html parsemode.
#See https://core.telegram.org/bots/api#html-style
@ -787,6 +811,10 @@ UseFirstName=false
#OPTIONAL (default false)
UseInsecureURL=false
#Disable quoted/reply messages
#OPTIONAL (default false)
QuoteDisable=false
#Disable sending of edits to other bridges
#OPTIONAL (default false)
EditDisable=false
@ -832,6 +860,11 @@ Label=""
#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
#
#WARNING: if you have set MessageFormat="HTML" be sure that this format matches the guidelines
#on https://core.telegram.org/bots/api#html-style otherwise the message will not go through to
#telegram! eg <{NICK}> should be &lt;{NICK}&gt;
#
#OPTIONAL (default empty)
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
@ -883,6 +916,9 @@ NoTLS=false
#OPTIONAL (default false)
SkipTLSVerify=true
## RELOADABLE SETTINGS
## Settings below can be reloaded by editing the file
#Whether to prefix messages from other bridges to rocketchat with the sender's nick.
#Useful if username overrides for incoming webhooks isn't enabled on the
#rocketchat server. If you set PrefixMessagesWithNick to true, each message
@ -970,6 +1006,9 @@ Password="yourpass"
#OPTIONAL (default false)
NoHomeServerSuffix=false
## RELOADABLE SETTINGS
## Settings below can be reloaded by editing the file
#Whether to prefix messages from other bridges to matrix with the sender's nick.
#Useful if username overrides for incoming webhooks isn't enabled on the
#matrix server. If you set PrefixMessagesWithNick to true, each message
@ -1051,6 +1090,9 @@ Password="yourpass"
#OPTIONAL
Authcode="ABCE12"
## RELOADABLE SETTINGS
## Settings below can be reloaded by editing the file
#Whether to prefix messages from other bridges to matrix with the sender's nick.
#Useful if username overrides for incoming webhooks isn't enabled on the
#matrix server. If you set PrefixMessagesWithNick to true, each message
@ -1113,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
###################################################################
@ -1153,6 +1279,10 @@ RemoteNickFormat="{NICK}"
###################################################################
# Settings here are defaults that each protocol can override
[general]
## RELOADABLE SETTINGS
## Settings below can be reloaded by editing the file
#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
@ -1237,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"

View File

@ -190,7 +190,11 @@ func (m *MMClient) Login() error {
}
if m.Team == nil {
return errors.New("team not found")
validTeamNames := make([]string, len(m.OtherTeams))
for i, t := range m.OtherTeams {
validTeamNames[i] = t.Team.Name
}
return fmt.Errorf("Team '%s' not found in %v", m.Credentials.Team, validTeamNames)
}
m.wsConnect()
@ -570,7 +574,7 @@ func (m *MMClient) GetFileLinks(filenames []string) []string {
res, resp := m.Client.GetFileLink(f)
if resp.Error != nil {
// public links is probably disabled, create the link ourselves
output = append(output, uriScheme+m.Credentials.Server+model.API_URL_SUFFIX_V3+"/files/"+f+"/get")
output = append(output, uriScheme+m.Credentials.Server+model.API_URL_SUFFIX_V4+"/files/"+f)
continue
}
output = append(output, res)
@ -768,6 +772,14 @@ func (m *MMClient) GetStatus(userId string) string {
return "offline"
}
func (m *MMClient) UpdateStatus(userId string, status string) error {
_, resp := m.Client.UpdateUserStatus(userId, &model.Status{Status: status})
if resp.Error != nil {
return resp.Error
}
return nil
}
func (m *MMClient) GetStatuses() map[string]string {
var ids []string
statuses := make(map[string]string)

256
vendor/github.com/matterbridge/gozulipbot/bot.go generated vendored Normal file
View File

@ -0,0 +1,256 @@
package gozulipbot
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"time"
)
type Bot struct {
APIKey string
APIURL string
Email string
Queues []*Queue
Streams []string
Client Doer
Backoff time.Duration
Retries int64
}
type Doer interface {
Do(*http.Request) (*http.Response, error)
}
// Init adds an http client to an existing bot struct.
func (b *Bot) Init() *Bot {
b.Client = &http.Client{}
return b
}
// GetStreamList gets the raw http response when requesting all public streams.
func (b *Bot) GetStreamList() (*http.Response, error) {
req, err := b.constructRequest("GET", "streams", "")
if err != nil {
return nil, err
}
return b.Client.Do(req)
}
type StreamJSON struct {
Msg string `json:"msg"`
Streams []struct {
StreamID int `json:"stream_id"`
InviteOnly bool `json:"invite_only"`
Description string `json:"description"`
Name string `json:"name"`
} `json:"streams"`
Result string `json:"result"`
}
// GetStreams returns a list of all public streams
func (b *Bot) GetStreams() ([]string, error) {
resp, err := b.GetStreamList()
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var sj StreamJSON
err = json.Unmarshal(body, &sj)
if err != nil {
return nil, err
}
var streams []string
for _, s := range sj.Streams {
streams = append(streams, s.Name)
}
return streams, nil
}
// GetStreams returns a list of all public streams
func (b *Bot) GetRawStreams() (StreamJSON, error) {
var sj StreamJSON
resp, err := b.GetStreamList()
if err != nil {
return sj, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return sj, err
}
err = json.Unmarshal(body, &sj)
if err != nil {
return sj, err
}
return sj, nil
}
// Subscribe will set the bot to receive messages from the given streams.
// If no streams are given, it will subscribe the bot to the streams in the bot struct.
func (b *Bot) Subscribe(streams []string) (*http.Response, error) {
if streams == nil {
streams = b.Streams
}
var toSubStreams []map[string]string
for _, name := range streams {
toSubStreams = append(toSubStreams, map[string]string{"name": name})
}
bodyBts, err := json.Marshal(toSubStreams)
if err != nil {
return nil, err
}
body := "subscriptions=" + string(bodyBts)
req, err := b.constructRequest("POST", "users/me/subscriptions", body)
if err != nil {
return nil, err
}
return b.Client.Do(req)
}
// Unsubscribe will remove the bot from the given streams.
// If no streams are given, nothing will happen and the function will error.
func (b *Bot) Unsubscribe(streams []string) (*http.Response, error) {
if len(streams) == 0 {
return nil, fmt.Errorf("No streams were provided")
}
body := `delete=["` + strings.Join(streams, `","`) + `"]`
req, err := b.constructRequest("PATCH", "users/me/subscriptions", body)
if err != nil {
return nil, err
}
return b.Client.Do(req)
}
func (b *Bot) ListSubscriptions() (*http.Response, error) {
req, err := b.constructRequest("GET", "users/me/subscriptions", "")
if err != nil {
return nil, err
}
return b.Client.Do(req)
}
type EventType string
const (
Messages EventType = "messages"
Subscriptions EventType = "subscriptions"
RealmUser EventType = "realm_user"
Pointer EventType = "pointer"
)
type Narrow string
const (
NarrowPrivate Narrow = `[["is", "private"]]`
NarrowAt Narrow = `[["is", "mentioned"]]`
)
// RegisterEvents adds a queue to the bot. It includes the EventTypes and
// Narrow given. If neither is given, it will default to all Messages.
func (b *Bot) RegisterEvents(ets []EventType, n Narrow) (*Queue, error) {
resp, err := b.RawRegisterEvents(ets, n)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
q := &Queue{Bot: b}
err = json.Unmarshal(body, q)
if err != nil {
return nil, err
}
if q.LastEventID < q.MaxMessageID {
q.LastEventID = q.MaxMessageID
}
b.Queues = append(b.Queues, q)
return q, nil
}
func (b *Bot) RegisterAll() (*Queue, error) {
return b.RegisterEvents(nil, "")
}
func (b *Bot) RegisterAt() (*Queue, error) {
return b.RegisterEvents(nil, NarrowAt)
}
func (b *Bot) RegisterPrivate() (*Queue, error) {
return b.RegisterEvents(nil, NarrowPrivate)
}
func (b *Bot) RegisterSubscriptions() (*Queue, error) {
events := []EventType{Subscriptions}
return b.RegisterEvents(events, "")
}
// RawRegisterEvents tells Zulip to include message events in the bots events queue.
// Passing nil as the slice of EventType will default to receiving Messages
func (b *Bot) RawRegisterEvents(ets []EventType, n Narrow) (*http.Response, error) {
// default to Messages if no EventTypes given
query := `event_types=["message"]`
if len(ets) != 0 {
query = `event_types=["`
for i, s := range ets {
query += fmt.Sprintf("%s", s)
if i != len(ets)-1 {
query += `", "`
}
}
query += `"]`
}
if n != "" {
query += fmt.Sprintf("&narrow=%s", n)
}
query += fmt.Sprintf("&all_public_streams=true")
req, err := b.constructRequest("POST", "register", query)
if err != nil {
return nil, err
}
return b.Client.Do(req)
}
// constructRequest makes a zulip request and ensures the proper headers are set.
func (b *Bot) constructRequest(method, endpoint, body string) (*http.Request, error) {
url := b.APIURL + endpoint
req, err := http.NewRequest(method, url, strings.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.SetBasicAuth(b.Email, b.APIKey)
return req, nil
}

32
vendor/github.com/matterbridge/gozulipbot/flag.go generated vendored Normal file
View File

@ -0,0 +1,32 @@
package gozulipbot
import (
"flag"
"fmt"
"time"
)
func (b *Bot) GetConfigFromFlags() error {
var (
apiKey = flag.String("apikey", "", "bot api key")
apiURL = flag.String("apiurl", "", "url of zulip server")
email = flag.String("email", "", "bot email address")
backoff = flag.Duration("backoff", 1*time.Second, "backoff base duration")
)
flag.Parse()
if *apiKey == "" {
return fmt.Errorf("--apikey is required")
}
if *apiURL == "" {
return fmt.Errorf("--apiurl is required")
}
if *email == "" {
return fmt.Errorf("--email is required")
}
b.APIKey = *apiKey
b.APIURL = *apiURL
b.Email = *email
b.Backoff = *backoff
return nil
}

263
vendor/github.com/matterbridge/gozulipbot/message.go generated vendored Normal file
View File

@ -0,0 +1,263 @@
package gozulipbot
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
)
// A Message is all of the necessary metadata to post on Zulip.
// It can be either a public message, where Topic is set, or a private message,
// where there is at least one element in Emails.
//
// If the length of Emails is not 0, functions will always assume it is a private message.
type Message struct {
Stream string
Topic string
Emails []string
Content string
}
type EventMessage struct {
AvatarURL string `json:"avatar_url"`
Client string `json:"client"`
Content string `json:"content"`
ContentType string `json:"content_type"`
DisplayRecipient DisplayRecipient `json:"display_recipient"`
GravatarHash string `json:"gravatar_hash"`
ID int `json:"id"`
RecipientID int `json:"recipient_id"`
SenderDomain string `json:"sender_domain"`
SenderEmail string `json:"sender_email"`
SenderFullName string `json:"sender_full_name"`
SenderID int `json:"sender_id"`
SenderShortName string `json:"sender_short_name"`
Subject string `json:"subject"`
SubjectLinks []interface{} `json:"subject_links"`
StreamID int `json:"stream_id"`
Timestamp int `json:"timestamp"`
Type string `json:"type"`
Queue *Queue `json:"-"`
}
type DisplayRecipient struct {
Users []User `json:"users,omitempty"`
Topic string `json:"topic,omitempty"`
}
type User struct {
Domain string `json:"domain"`
Email string `json:"email"`
FullName string `json:"full_name"`
ID int `json:"id"`
IsMirrorDummy bool `json:"is_mirror_dummy"`
ShortName string `json:"short_name"`
}
func (d *DisplayRecipient) UnmarshalJSON(b []byte) (err error) {
topic, users := "", make([]User, 1)
if err = json.Unmarshal(b, &topic); err == nil {
d.Topic = topic
return
}
if err = json.Unmarshal(b, &users); err == nil {
d.Users = users
return
}
return
}
// Message posts a message to Zulip. If any emails have been set on the message,
// the message will be re-routed to the PrivateMessage function.
func (b *Bot) Message(m Message) (*http.Response, error) {
if m.Content == "" {
return nil, fmt.Errorf("content cannot be empty")
}
// if any emails are set, this is a private message
if len(m.Emails) != 0 {
return b.PrivateMessage(m)
}
// otherwise it's a stream message
if m.Stream == "" {
return nil, fmt.Errorf("stream cannot be empty")
}
if m.Topic == "" {
return nil, fmt.Errorf("topic cannot be empty")
}
req, err := b.constructMessageRequest(m)
if err != nil {
return nil, err
}
return b.Client.Do(req)
}
// PrivateMessage sends a message to the users in the message email slice.
func (b *Bot) PrivateMessage(m Message) (*http.Response, error) {
if len(m.Emails) == 0 {
return nil, fmt.Errorf("there must be at least one recipient")
}
req, err := b.constructMessageRequest(m)
if err != nil {
return nil, err
}
return b.Client.Do(req)
}
// Respond sends a given message as a response to whatever context from which
// an EventMessage was received.
func (b *Bot) Respond(e EventMessage, response string) (*http.Response, error) {
if response == "" {
return nil, fmt.Errorf("Message response cannot be blank")
}
m := Message{
Stream: e.DisplayRecipient.Topic,
Topic: e.Subject,
Content: response,
}
if m.Topic != "" {
return b.Message(m)
}
// private message
if m.Stream == "" {
emails, err := b.privateResponseList(e)
if err != nil {
return nil, err
}
m.Emails = emails
return b.Message(m)
}
return nil, fmt.Errorf("EventMessage is not understood: %v\n", e)
}
// privateResponseList gets the list of other users in a private multiple
// message conversation.
func (b *Bot) privateResponseList(e EventMessage) ([]string, error) {
var out []string
for _, u := range e.DisplayRecipient.Users {
if u.Email != b.Email {
out = append(out, u.Email)
}
}
if len(out) == 0 {
return nil, fmt.Errorf("EventMessage had no Users within the DisplayRecipient")
}
return out, nil
}
// constructMessageRequest is a helper for simplifying sending a message.
func (b *Bot) constructMessageRequest(m Message) (*http.Request, error) {
to := m.Stream
mtype := "stream"
le := len(m.Emails)
if le != 0 {
mtype = "private"
}
if le == 1 {
to = m.Emails[0]
}
if le > 1 {
to = ""
for i, e := range m.Emails {
to += e
if i != le-1 {
to += ","
}
}
}
values := url.Values{}
values.Set("type", mtype)
values.Set("to", to)
values.Set("content", m.Content)
if mtype == "stream" {
values.Set("subject", m.Topic)
}
return b.constructRequest("POST", "messages", values.Encode())
}
func (b *Bot) UpdateMessage(id string, content string) (*http.Response, error) {
//mid, _ := strconv.Atoi(id)
values := url.Values{}
values.Set("content", content)
req, err := b.constructRequest("PATCH", "messages/"+id, values.Encode())
if err != nil {
return nil, err
}
return b.Client.Do(req)
}
// React adds an emoji reaction to an EventMessage.
func (b *Bot) React(e EventMessage, emoji string) (*http.Response, error) {
url := fmt.Sprintf("messages/%d/emoji_reactions/%s", e.ID, emoji)
req, err := b.constructRequest("PUT", url, "")
if err != nil {
return nil, err
}
return b.Client.Do(req)
}
// Unreact removes an emoji reaction from an EventMessage.
func (b *Bot) Unreact(e EventMessage, emoji string) (*http.Response, error) {
url := fmt.Sprintf("messages/%d/emoji_reactions/%s", e.ID, emoji)
req, err := b.constructRequest("DELETE", url, "")
if err != nil {
return nil, err
}
return b.Client.Do(req)
}
type Emoji struct {
Author string `json:"author"`
DisplayURL string `json:"display_url"`
SourceURL string `json:"source_url"`
}
type EmojiResponse struct {
Emoji map[string]*Emoji `json:"emoji"`
Msg string `json:"msg"`
Result string `json:"result"`
}
// RealmEmoji gets the custom emoji information for the Zulip instance.
func (b *Bot) RealmEmoji() (map[string]*Emoji, error) {
req, err := b.constructRequest("GET", "realm/emoji", "")
if err != nil {
return nil, err
}
resp, err := b.Client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var emjResp EmojiResponse
err = json.Unmarshal(body, &emjResp)
if err != nil {
return nil, err
}
return emjResp.Emoji, nil
}
// RealmEmojiSet makes a set of the names of the custom emoji in the Zulip instance.
func (b *Bot) RealmEmojiSet() (map[string]struct{}, error) {
emj, err := b.RealmEmoji()
if err != nil {
return nil, nil
}
out := map[string]struct{}{}
for k, _ := range emj {
out[k] = struct{}{}
}
return out, nil
}

203
vendor/github.com/matterbridge/gozulipbot/queue.go generated vendored Normal file
View File

@ -0,0 +1,203 @@
package gozulipbot
import (
"encoding/json"
"fmt"
"io/ioutil"
"math"
"net/http"
"net/url"
"strconv"
"sync/atomic"
"time"
)
var (
HeartbeatError = fmt.Errorf("EventMessage is a heartbeat")
UnauthorizedError = fmt.Errorf("Request is unauthorized")
BackoffError = fmt.Errorf("Too many requests")
UnknownError = fmt.Errorf("Error was unknown")
)
type Queue struct {
ID string `json:"queue_id"`
LastEventID int `json:"last_event_id"`
MaxMessageID int `json:"max_message_id"`
Bot *Bot `json:"-"`
}
func (q *Queue) EventsChan() (chan EventMessage, func()) {
end := false
endFunc := func() {
end = true
}
out := make(chan EventMessage)
go func() {
defer close(out)
for {
backoffTime := time.Now().Add(q.Bot.Backoff * time.Duration(math.Pow10(int(atomic.LoadInt64(&q.Bot.Retries)))))
minTime := time.Now().Add(q.Bot.Backoff)
if end {
return
}
ems, err := q.GetEvents()
switch {
case err == HeartbeatError:
time.Sleep(time.Until(minTime))
continue
case err == BackoffError:
time.Sleep(time.Until(backoffTime))
atomic.AddInt64(&q.Bot.Retries, 1)
case err == UnauthorizedError:
// TODO? have error channel when ending the continuously running process?
return
default:
atomic.StoreInt64(&q.Bot.Retries, 0)
}
if err != nil {
// TODO: handle unknown error
// For now, handle this like an UnauthorizedError and end the func.
return
}
for _, em := range ems {
out <- em
}
// Always make sure we wait the minimum time before asking again.
time.Sleep(time.Until(minTime))
}
}()
return out, endFunc
}
// EventsCallback will repeatedly call provided callback function with
// the output of continual queue.GetEvents calls.
// It returns a function which can be called to end the calls.
//
// It will end early if it receives an UnauthorizedError, or an unknown error.
// Note, it will never return a HeartbeatError.
func (q *Queue) EventsCallback(fn func(EventMessage, error)) func() {
end := false
endFunc := func() {
end = true
}
go func() {
for {
backoffTime := time.Now().Add(q.Bot.Backoff * time.Duration(math.Pow10(int(atomic.LoadInt64(&q.Bot.Retries)))))
minTime := time.Now().Add(q.Bot.Backoff)
if end {
return
}
ems, err := q.GetEvents()
switch {
case err == HeartbeatError:
time.Sleep(time.Until(minTime))
continue
case err == BackoffError:
time.Sleep(time.Until(backoffTime))
atomic.AddInt64(&q.Bot.Retries, 1)
case err == UnauthorizedError:
// TODO? have error channel when ending the continuously running process?
return
default:
atomic.StoreInt64(&q.Bot.Retries, 0)
}
if err != nil {
// TODO: handle unknown error
// For now, handle this like an UnauthorizedError and end the func.
return
}
for _, em := range ems {
fn(em, err)
}
// Always make sure we wait the minimum time before asking again.
time.Sleep(time.Until(minTime))
}
}()
return endFunc
}
// GetEvents is a blocking call that waits for and parses a list of EventMessages.
// There will usually only be one EventMessage returned.
// When a heartbeat is returned, GetEvents will return a HeartbeatError.
// When an http status code above 400 is returned, one of a BackoffError,
// UnauthorizedError, or UnknownError will be returned.
func (q *Queue) GetEvents() ([]EventMessage, error) {
resp, err := q.RawGetEvents()
if err != nil {
return nil, err
}
defer resp.Body.Close()
switch {
case resp.StatusCode == 429:
return nil, BackoffError
case resp.StatusCode == 403:
return nil, UnauthorizedError
case resp.StatusCode >= 400:
return nil, UnknownError
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
msgs, err := q.ParseEventMessages(body)
if err != nil {
return nil, err
}
return msgs, nil
}
// RawGetEvents is a blocking call that receives a response containing a list
// of events (a.k.a. received messages) since the last message id in the queue.
func (q *Queue) RawGetEvents() (*http.Response, error) {
values := url.Values{}
values.Set("queue_id", q.ID)
values.Set("last_event_id", strconv.Itoa(q.LastEventID))
url := "events?" + values.Encode()
req, err := q.Bot.constructRequest("GET", url, "")
if err != nil {
return nil, err
}
return q.Bot.Client.Do(req)
}
func (q *Queue) ParseEventMessages(rawEventResponse []byte) ([]EventMessage, error) {
rawResponse := map[string]json.RawMessage{}
err := json.Unmarshal(rawEventResponse, &rawResponse)
if err != nil {
return nil, err
}
events := []map[string]json.RawMessage{}
err = json.Unmarshal(rawResponse["events"], &events)
if err != nil {
return nil, err
}
messages := []EventMessage{}
for _, event := range events {
// if the event is a heartbeat, return a special error
if string(event["type"]) == `"heartbeat"` {
return nil, HeartbeatError
}
var msg EventMessage
err = json.Unmarshal(event["message"], &msg)
// TODO? should this check be here
if err != nil {
return nil, err
}
msg.Queue = q
messages = append(messages, msg)
}
return messages, nil
}

8
vendor/manifest vendored
View File

@ -405,6 +405,14 @@
"branch": "work",
"notests": true
},
{
"importpath": "github.com/matterbridge/gozulipbot",
"repository": "https://github.com/matterbridge/gozulipbot",
"vcs": "git",
"revision": "b6bb12d33544893aa68904652704cf1a86ea3d18",
"branch": "work",
"notests": true
},
{
"importpath": "github.com/mattermost/mattermost-server/einterfaces",
"repository": "https://github.com/mattermost/mattermost-server",