mirror of
https://github.com/cwinfo/matterbridge.git
synced 2025-06-26 17:49:23 +00:00
Compare commits
33 Commits
Author | SHA1 | Date | |
---|---|---|---|
86865c6da5 | |||
45296100df | |||
1605fbc012 | |||
c6c92e273d | |||
467b373c43 | |||
72ce7f06e9 | |||
346a7284f7 | |||
ee4ac67081 | |||
5a93d14d75 | |||
96a47a60ad | |||
b24a47ad7f | |||
cd1fd1bb7c | |||
d44df7b6e6 | |||
9d1ac0c84b | |||
76af9cba5a | |||
b69fc30902 | |||
c3174f4de9 | |||
99ce68e9ba | |||
0cf73673a9 | |||
08f442dc7b | |||
8a8b95228c | |||
31a752fa21 | |||
a83831e68d | |||
a12a8d4fe2 | |||
e57f3a7e6c | |||
68fbed9281 | |||
8bfaa007d5 | |||
76360f89c1 | |||
d525230abd | |||
b4aa637d41 | |||
7c4334d0de | |||
062be8d7c9 | |||
db25ee59c5 |
26
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
Normal file
26
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
Normal 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))
|
17
.github/ISSUE_TEMPLATE/Feature_request.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/Feature_request.md
vendored
Normal 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.
|
@ -43,6 +43,8 @@ script:
|
||||
|
||||
deploy:
|
||||
provider: bintray
|
||||
edge:
|
||||
branch: v1.8.47
|
||||
file: ci/deploy.json
|
||||
user: 42wim
|
||||
key:
|
||||
|
13
README.md
13
README.md
@ -5,12 +5,15 @@ Click on one of the badges below to join the chat
|
||||
|
||||
[](https://github.com/42wim/matterbridge/releases/latest) [](https://bintray.com/42wim/nightly/Matterbridge/_latestVersion)
|
||||
|
||||

|
||||

|
||||
|
||||
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
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
170
bridge/zulip/zulip.go
Normal 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
|
||||
}
|
31
changelog.md
31
changelog.md
@ -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
|
||||
|
@ -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
BIN
img/matterbridge.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 77 KiB |
@ -13,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
version = "1.9.0"
|
||||
version = "1.10.0"
|
||||
githash string
|
||||
)
|
||||
|
||||
|
@ -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 <{NICK}>
|
||||
#
|
||||
#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"
|
||||
|
@ -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
256
vendor/github.com/matterbridge/gozulipbot/bot.go
generated
vendored
Normal 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
32
vendor/github.com/matterbridge/gozulipbot/flag.go
generated
vendored
Normal 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
263
vendor/github.com/matterbridge/gozulipbot/message.go
generated
vendored
Normal 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
203
vendor/github.com/matterbridge/gozulipbot/queue.go
generated
vendored
Normal 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
8
vendor/manifest
vendored
@ -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",
|
||||
|
Reference in New Issue
Block a user