forked from lug/matterbridge
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86865c6da5 | ||
|
|
45296100df | ||
|
|
1605fbc012 | ||
|
|
c6c92e273d | ||
|
|
467b373c43 | ||
|
|
72ce7f06e9 | ||
|
|
346a7284f7 | ||
|
|
ee4ac67081 | ||
|
|
5a93d14d75 | ||
|
|
96a47a60ad | ||
|
|
b24a47ad7f | ||
|
|
cd1fd1bb7c | ||
|
|
d44df7b6e6 | ||
|
|
9d1ac0c84b | ||
|
|
76af9cba5a | ||
|
|
b69fc30902 | ||
|
|
c3174f4de9 | ||
|
|
99ce68e9ba | ||
|
|
0cf73673a9 |
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.
|
||||||
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)
|
[](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
|
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.
|
Has a REST API.
|
||||||
Minecraft server chat support via [MatterLink](https://github.com/elytra/MatterLink)
|
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
|
# Table of Contents
|
||||||
* [Features](https://github.com/42wim/matterbridge/wiki/Features)
|
* [Features](https://github.com/42wim/matterbridge/wiki/Features)
|
||||||
* [Requirements](#requirements)
|
* [Requirements](#requirements)
|
||||||
@@ -59,13 +62,14 @@ Accounts to one of the supported bridges
|
|||||||
* [Steam](https://store.steampowered.com/)
|
* [Steam](https://store.steampowered.com/)
|
||||||
* [Twitch](https://twitch.tv)
|
* [Twitch](https://twitch.tv)
|
||||||
* [Ssh-chat](https://github.com/shazow/ssh-chat)
|
* [Ssh-chat](https://github.com/shazow/ssh-chat)
|
||||||
|
* [Zulip](https://zulipchat.com)
|
||||||
|
|
||||||
# Screenshots
|
# Screenshots
|
||||||
See https://github.com/42wim/matterbridge/wiki
|
See https://github.com/42wim/matterbridge/wiki
|
||||||
|
|
||||||
# Installing
|
# Installing
|
||||||
## Binaries
|
## Binaries
|
||||||
* Latest stable release [v1.9.1](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/)
|
* Development releases (follows master) can be downloaded [here](https://dl.bintray.com/42wim/nightly/)
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
@@ -186,6 +190,7 @@ Matterbridge wouldn't exist without these libraries:
|
|||||||
* echo - https://github.com/labstack/echo
|
* echo - https://github.com/labstack/echo
|
||||||
* gitter - https://github.com/sromku/go-gitter
|
* gitter - https://github.com/sromku/go-gitter
|
||||||
* gops - https://github.com/google/gops
|
* gops - https://github.com/google/gops
|
||||||
|
* gozulipbot - https://github.com/ifo/gozulipbot
|
||||||
* irc - https://github.com/lrstanley/girc
|
* irc - https://github.com/lrstanley/girc
|
||||||
* mattermost - https://github.com/mattermost/platform
|
* mattermost - https://github.com/mattermost/platform
|
||||||
* matrix - https://github.com/matrix-org/gomatrix
|
* matrix - https://github.com/matrix-org/gomatrix
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
prefixed "github.com/x-cray/logrus-prefixed-formatter"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -105,6 +107,7 @@ type Protocol struct {
|
|||||||
StripNick bool // all protocols
|
StripNick bool // all protocols
|
||||||
Team string // mattermost
|
Team string // mattermost
|
||||||
Token string // gitter, slack, discord, api
|
Token string // gitter, slack, discord, api
|
||||||
|
Topic string // zulip
|
||||||
URL string // mattermost, slack // DEPRECATED
|
URL string // mattermost, slack // DEPRECATED
|
||||||
UseAPI bool // mattermost, slack
|
UseAPI bool // mattermost, slack
|
||||||
UseSASL bool // IRC
|
UseSASL bool // IRC
|
||||||
@@ -157,6 +160,7 @@ type ConfigValues struct {
|
|||||||
Telegram map[string]Protocol
|
Telegram map[string]Protocol
|
||||||
Rocketchat map[string]Protocol
|
Rocketchat map[string]Protocol
|
||||||
Sshchat map[string]Protocol
|
Sshchat map[string]Protocol
|
||||||
|
Zulip map[string]Protocol
|
||||||
General Protocol
|
General Protocol
|
||||||
Gateway []Gateway
|
Gateway []Gateway
|
||||||
SameChannelGateway []SameChannelGateway
|
SameChannelGateway []SameChannelGateway
|
||||||
@@ -169,9 +173,13 @@ type Config struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewConfig(cfgfile string) *Config {
|
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
|
var cfg ConfigValues
|
||||||
viper.SetConfigType("toml")
|
viper.SetConfigType("toml")
|
||||||
|
viper.SetConfigFile(cfgfile)
|
||||||
viper.SetEnvPrefix("matterbridge")
|
viper.SetEnvPrefix("matterbridge")
|
||||||
|
viper.AddConfigPath(".")
|
||||||
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||||
viper.AutomaticEnv()
|
viper.AutomaticEnv()
|
||||||
f, err := os.Open(cfgfile)
|
f, err := os.Open(cfgfile)
|
||||||
@@ -191,6 +199,11 @@ func NewConfig(cfgfile string) *Config {
|
|||||||
if cfg.General.MediaDownloadSize == 0 {
|
if cfg.General.MediaDownloadSize == 0 {
|
||||||
cfg.General.MediaDownloadSize = 1000000
|
cfg.General.MediaDownloadSize = 1000000
|
||||||
}
|
}
|
||||||
|
viper.WatchConfig()
|
||||||
|
viper.OnConfigChange(func(e fsnotify.Event) {
|
||||||
|
flog.Println("Config file changed:", e.Name)
|
||||||
|
})
|
||||||
|
|
||||||
mycfg.ConfigValues = &cfg
|
mycfg.ConfigValues = &cfg
|
||||||
return mycfg
|
return mycfg
|
||||||
}
|
}
|
||||||
@@ -243,11 +256,18 @@ func (c *Config) GetStringSlice(key string) []string {
|
|||||||
func (c *Config) GetStringSlice2D(key string) [][]string {
|
func (c *Config) GetStringSlice2D(key string) [][]string {
|
||||||
c.RLock()
|
c.RLock()
|
||||||
defer c.RUnlock()
|
defer c.RUnlock()
|
||||||
if res, ok := c.v.Get(key).([][]string); ok {
|
result := [][]string{}
|
||||||
return res
|
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 result
|
||||||
return [][]string{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetIconURL(msg *Message, iconURL string) string {
|
func GetIconURL(msg *Message, iconURL string) string {
|
||||||
|
|||||||
@@ -168,6 +168,9 @@ func (b *Bgitter) handleUploadFile(msg *config.Message, roomID string) (string,
|
|||||||
}
|
}
|
||||||
if fi.URL != "" {
|
if fi.URL != "" {
|
||||||
msg.Text = fi.URL
|
msg.Text = fi.URL
|
||||||
|
if fi.Comment != "" {
|
||||||
|
msg.Text = fi.Comment + ": " + fi.URL
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_, err := b.c.SendMessage(roomID, msg.Username+msg.Text)
|
_, err := b.c.SendMessage(roomID, msg.Username+msg.Text)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -201,6 +201,9 @@ func (b *Birc) Send(msg config.Message) (string, error) {
|
|||||||
}
|
}
|
||||||
if fi.URL != "" {
|
if fi.URL != "" {
|
||||||
msg.Text = 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}
|
b.Local <- config.Message{Text: msg.Text, Username: msg.Username, Channel: msg.Channel, Event: msg.Event}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -485,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{})}
|
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
|
// 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)
|
user, err := b.rtm.GetUserInfo(ev.User)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -509,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
|
// 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)
|
bot, err := b.rtm.GetBotInfo(ev.BotID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -521,6 +521,19 @@ func (b *Bslack) handleMessageEvent(ev *slack.MessageEvent) (*config.Message, er
|
|||||||
}
|
}
|
||||||
rmsg.UserID = bot.ID
|
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)
|
// file comments are set by the system (because there is no username given)
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ func (b *Bsshchat) Send(msg config.Message) (string, error) {
|
|||||||
}
|
}
|
||||||
if fi.URL != "" {
|
if fi.URL != "" {
|
||||||
msg.Text = fi.URL
|
msg.Text = fi.URL
|
||||||
|
if fi.Comment != "" {
|
||||||
|
msg.Text = fi.Comment + ": " + fi.URL
|
||||||
|
}
|
||||||
}
|
}
|
||||||
b.w.Write([]byte(msg.Username + msg.Text))
|
b.w.Write([]byte(msg.Username + msg.Text))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ package bsteam
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/42wim/matterbridge/bridge"
|
"github.com/42wim/matterbridge/bridge"
|
||||||
"github.com/42wim/matterbridge/bridge/config"
|
"github.com/42wim/matterbridge/bridge/config"
|
||||||
|
"github.com/42wim/matterbridge/bridge/helper"
|
||||||
"github.com/Philipp15b/go-steam"
|
"github.com/Philipp15b/go-steam"
|
||||||
"github.com/Philipp15b/go-steam/protocol/steamlang"
|
"github.com/Philipp15b/go-steam/protocol/steamlang"
|
||||||
"github.com/Philipp15b/go-steam/steamid"
|
"github.com/Philipp15b/go-steam/steamid"
|
||||||
@@ -66,6 +68,30 @@ func (b *Bsteam) Send(msg config.Message) (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
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)
|
b.c.Social.SendMessage(id, steamlang.EChatEntryType_ChatMsg, msg.Username+msg.Text)
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
|
|||||||
for update := range updates {
|
for update := range updates {
|
||||||
b.Log.Debugf("== Receiving event: %#v", update.Message)
|
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.")
|
b.Log.Error("Getting nil messages, this shouldn't happen.")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -133,6 +133,7 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
|
|||||||
// handle channels
|
// handle channels
|
||||||
if update.ChannelPost != nil {
|
if update.ChannelPost != nil {
|
||||||
message = update.ChannelPost
|
message = update.ChannelPost
|
||||||
|
rmsg.Text = message.Text
|
||||||
}
|
}
|
||||||
|
|
||||||
// edited channel message
|
// edited channel message
|
||||||
@@ -144,6 +145,7 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
|
|||||||
// handle groups
|
// handle groups
|
||||||
if update.Message != nil {
|
if update.Message != nil {
|
||||||
message = update.Message
|
message = update.Message
|
||||||
|
rmsg.Text = message.Text
|
||||||
}
|
}
|
||||||
|
|
||||||
// edited group message
|
// edited group message
|
||||||
@@ -154,11 +156,11 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
|
|||||||
|
|
||||||
// set the ID's from the channel or group message
|
// set the ID's from the channel or group message
|
||||||
rmsg.ID = strconv.Itoa(message.MessageID)
|
rmsg.ID = strconv.Itoa(message.MessageID)
|
||||||
rmsg.UserID = strconv.Itoa(message.From.ID)
|
|
||||||
rmsg.Channel = strconv.FormatInt(message.Chat.ID, 10)
|
rmsg.Channel = strconv.FormatInt(message.Chat.ID, 10)
|
||||||
|
|
||||||
// handle username
|
// handle username
|
||||||
if message.From != nil {
|
if message.From != nil {
|
||||||
|
rmsg.UserID = strconv.Itoa(message.From.ID)
|
||||||
if b.GetBool("UseFirstName") {
|
if b.GetBool("UseFirstName") {
|
||||||
rmsg.Username = message.From.FirstName
|
rmsg.Username = message.From.FirstName
|
||||||
}
|
}
|
||||||
@@ -168,7 +170,6 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
|
|||||||
rmsg.Username = message.From.FirstName
|
rmsg.Username = message.From.FirstName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rmsg.Text += message.Text
|
|
||||||
// only download avatars if we have a place to upload them (configured mediaserver)
|
// only download avatars if we have a place to upload them (configured mediaserver)
|
||||||
if b.General.MediaServerUpload != "" {
|
if b.General.MediaServerUpload != "" {
|
||||||
b.handleDownloadAvatar(message.From.ID, rmsg.Channel)
|
b.handleDownloadAvatar(message.From.ID, rmsg.Channel)
|
||||||
@@ -228,7 +229,10 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
|
|||||||
|
|
||||||
if rmsg.Text != "" || len(rmsg.Extra) > 0 {
|
if rmsg.Text != "" || len(rmsg.Extra) > 0 {
|
||||||
rmsg.Text = helper.RemoveEmptyNewLines(rmsg.Text)
|
rmsg.Text = helper.RemoveEmptyNewLines(rmsg.Text)
|
||||||
rmsg.Avatar = helper.GetAvatar(b.avatarMap, strconv.Itoa(message.From.ID), b.General)
|
// 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("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account)
|
||||||
b.Log.Debugf("<= Message is %#v", rmsg)
|
b.Log.Debugf("<= Message is %#v", rmsg)
|
||||||
|
|||||||
@@ -186,7 +186,10 @@ func (b *Bxmpp) handleUploadFile(msg *config.Message) (string, error) {
|
|||||||
msg.Text += fi.Comment + ": "
|
msg.Text += fi.Comment + ": "
|
||||||
}
|
}
|
||||||
if fi.URL != "" {
|
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})
|
_, err := b.xc.Send(xmpp.Chat{Type: "groupchat", Remote: msg.Channel + "@" + b.GetString("Muc"), Text: msg.Username + msg.Text})
|
||||||
if err != nil {
|
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
|
||||||
|
}
|
||||||
19
changelog.md
19
changelog.md
@@ -1,3 +1,22 @@
|
|||||||
|
# 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
|
# v1.9.1
|
||||||
## New features
|
## New features
|
||||||
* telegram: Add QuoteDisable option (telegram). Closes #399. See QuoteDisable in matterbridge.toml.sample
|
* telegram: Add QuoteDisable option (telegram). Closes #399. See QuoteDisable in matterbridge.toml.sample
|
||||||
|
|||||||
@@ -17,12 +17,14 @@ import (
|
|||||||
"github.com/42wim/matterbridge/bridge/steam"
|
"github.com/42wim/matterbridge/bridge/steam"
|
||||||
"github.com/42wim/matterbridge/bridge/telegram"
|
"github.com/42wim/matterbridge/bridge/telegram"
|
||||||
"github.com/42wim/matterbridge/bridge/xmpp"
|
"github.com/42wim/matterbridge/bridge/xmpp"
|
||||||
|
"github.com/42wim/matterbridge/bridge/zulip"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
// "github.com/davecgh/go-spew/spew"
|
// "github.com/davecgh/go-spew/spew"
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"github.com/hashicorp/golang-lru"
|
"github.com/hashicorp/golang-lru"
|
||||||
"github.com/peterhellberg/emojilib"
|
"github.com/peterhellberg/emojilib"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -61,6 +63,7 @@ var bridgeMap = map[string]bridge.Factory{
|
|||||||
"steam": bsteam.New,
|
"steam": bsteam.New,
|
||||||
"telegram": btelegram.New,
|
"telegram": btelegram.New,
|
||||||
"xmpp": bxmpp.New,
|
"xmpp": bxmpp.New,
|
||||||
|
"zulip": bzulip.New,
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -409,6 +412,7 @@ func (gw *Gateway) modifyMessage(msg *config.Message) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (gw *Gateway) handleFiles(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 we don't have a attachfield or we don't have a mediaserver configured return
|
||||||
if msg.Extra == nil || gw.Config.General.MediaServerUpload == "" {
|
if msg.Extra == nil || gw.Config.General.MediaServerUpload == "" {
|
||||||
return
|
return
|
||||||
@@ -421,15 +425,23 @@ func (gw *Gateway) handleFiles(msg *config.Message) {
|
|||||||
}
|
}
|
||||||
for i, f := range msg.Extra["file"] {
|
for i, f := range msg.Extra["file"] {
|
||||||
fi := f.(config.FileInfo)
|
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))
|
sha1sum := fmt.Sprintf("%x", sha1.Sum(*fi.Data))
|
||||||
reader := bytes.NewReader(*fi.Data)
|
reader := bytes.NewReader(*fi.Data)
|
||||||
url := gw.Config.General.MediaServerUpload + "/" + sha1sum + "/" + fi.Name
|
url := gw.Config.General.MediaServerUpload + "/" + sha1sum + "/" + fi.Name
|
||||||
durl := gw.Config.General.MediaServerDownload + "/" + sha1sum + "/" + fi.Name
|
durl := gw.Config.General.MediaServerDownload + "/" + sha1sum + "/" + fi.Name
|
||||||
extra := msg.Extra["file"][i].(config.FileInfo)
|
extra := msg.Extra["file"][i].(config.FileInfo)
|
||||||
extra.URL = durl
|
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")
|
req.Header.Set("Content-Type", "binary/octet-stream")
|
||||||
_, err := client.Do(req)
|
_, err = client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
flog.Errorf("mediaserver upload failed: %#v", err)
|
flog.Errorf("mediaserver upload failed: %#v", err)
|
||||||
continue
|
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 (
|
var (
|
||||||
version = "1.9.1"
|
version = "1.10.0"
|
||||||
githash string
|
githash string
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,9 @@ NickServPassword="secret"
|
|||||||
#OPTIONAL only used for quakenet auth
|
#OPTIONAL only used for quakenet auth
|
||||||
NickServUsername="username"
|
NickServUsername="username"
|
||||||
|
|
||||||
|
## RELOADABLE SETTINGS
|
||||||
|
## Settings below can be reloaded by editing the file
|
||||||
|
|
||||||
#Flood control
|
#Flood control
|
||||||
#Delay in milliseconds between each message send to the IRC server
|
#Delay in milliseconds between each message send to the IRC server
|
||||||
#OPTIONAL (default 1300)
|
#OPTIONAL (default 1300)
|
||||||
@@ -184,6 +187,9 @@ Nick="xmppbot"
|
|||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
SkipTLSVerify=true
|
SkipTLSVerify=true
|
||||||
|
|
||||||
|
## RELOADABLE SETTINGS
|
||||||
|
## Settings below can be reloaded by editing the file
|
||||||
|
|
||||||
#Nicks you want to ignore.
|
#Nicks you want to ignore.
|
||||||
#Messages from those users will not be sent to other bridges.
|
#Messages from those users will not be sent to other bridges.
|
||||||
#OPTIONAL
|
#OPTIONAL
|
||||||
@@ -265,6 +271,9 @@ Muc="conf.hipchat.com"
|
|||||||
#REQUIRED
|
#REQUIRED
|
||||||
Nick="yourlogin"
|
Nick="yourlogin"
|
||||||
|
|
||||||
|
## RELOADABLE SETTINGS
|
||||||
|
## Settings below can be reloaded by editing the file
|
||||||
|
|
||||||
#Nicks you want to ignore.
|
#Nicks you want to ignore.
|
||||||
#Messages from those users will not be sent to other bridges.
|
#Messages from those users will not be sent to other bridges.
|
||||||
#OPTIONAL
|
#OPTIONAL
|
||||||
@@ -382,6 +391,9 @@ IconURL="http://youricon.png"
|
|||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
SkipTLSVerify=true
|
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.
|
#how to format the list of IRC nicks when displayed in mattermost.
|
||||||
#Possible options are "table" and "plain"
|
#Possible options are "table" and "plain"
|
||||||
#OPTIONAL (default plain)
|
#OPTIONAL (default plain)
|
||||||
@@ -482,6 +494,9 @@ ShowTopicChange=false
|
|||||||
#REQUIRED
|
#REQUIRED
|
||||||
Token="Yourtokenhere"
|
Token="Yourtokenhere"
|
||||||
|
|
||||||
|
## RELOADABLE SETTINGS
|
||||||
|
## Settings below can be reloaded by editing the file
|
||||||
|
|
||||||
#Nicks you want to ignore.
|
#Nicks you want to ignore.
|
||||||
#Messages from those users will not be sent to other bridges.
|
#Messages from those users will not be sent to other bridges.
|
||||||
#OPTIONAL
|
#OPTIONAL
|
||||||
@@ -577,6 +592,9 @@ WebhookBindAddress="0.0.0.0:9999"
|
|||||||
#OPTIONAL
|
#OPTIONAL
|
||||||
IconURL="https://robohash.org/{NICK}.png?size=48x48"
|
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
|
#how to format the list of IRC nicks when displayed in slack
|
||||||
#Possible options are "table" and "plain"
|
#Possible options are "table" and "plain"
|
||||||
#OPTIONAL (default plain)
|
#OPTIONAL (default plain)
|
||||||
@@ -680,6 +698,9 @@ Token="Yourtokenhere"
|
|||||||
#REQUIRED
|
#REQUIRED
|
||||||
Server="yourservername"
|
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)
|
#Shows title, description and URL of embedded messages (sent by other bots)
|
||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
ShowEmbeds=false
|
ShowEmbeds=false
|
||||||
@@ -770,6 +791,9 @@ ShowTopicChange=false
|
|||||||
#REQUIRED
|
#REQUIRED
|
||||||
Token="Yourtokenhere"
|
Token="Yourtokenhere"
|
||||||
|
|
||||||
|
## RELOADABLE SETTINGS
|
||||||
|
## Settings below can be reloaded by editing the file
|
||||||
|
|
||||||
#OPTIONAL (default empty)
|
#OPTIONAL (default empty)
|
||||||
#Only supported format is "HTML", messages will be sent in html parsemode.
|
#Only supported format is "HTML", messages will be sent in html parsemode.
|
||||||
#See https://core.telegram.org/bots/api#html-style
|
#See https://core.telegram.org/bots/api#html-style
|
||||||
@@ -892,6 +916,9 @@ NoTLS=false
|
|||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
SkipTLSVerify=true
|
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.
|
#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
|
#Useful if username overrides for incoming webhooks isn't enabled on the
|
||||||
#rocketchat server. If you set PrefixMessagesWithNick to true, each message
|
#rocketchat server. If you set PrefixMessagesWithNick to true, each message
|
||||||
@@ -979,6 +1006,9 @@ Password="yourpass"
|
|||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
NoHomeServerSuffix=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.
|
#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
|
#Useful if username overrides for incoming webhooks isn't enabled on the
|
||||||
#matrix server. If you set PrefixMessagesWithNick to true, each message
|
#matrix server. If you set PrefixMessagesWithNick to true, each message
|
||||||
@@ -1060,6 +1090,9 @@ Password="yourpass"
|
|||||||
#OPTIONAL
|
#OPTIONAL
|
||||||
Authcode="ABCE12"
|
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.
|
#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
|
#Useful if username overrides for incoming webhooks isn't enabled on the
|
||||||
#matrix server. If you set PrefixMessagesWithNick to true, each message
|
#matrix server. If you set PrefixMessagesWithNick to true, each message
|
||||||
@@ -1122,6 +1155,90 @@ StripNick=false
|
|||||||
#OPTIONAL (default false)
|
#OPTIONAL (default false)
|
||||||
ShowTopicChange=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
|
#API
|
||||||
###################################################################
|
###################################################################
|
||||||
@@ -1162,6 +1279,10 @@ RemoteNickFormat="{NICK}"
|
|||||||
###################################################################
|
###################################################################
|
||||||
# Settings here are defaults that each protocol can override
|
# Settings here are defaults that each protocol can override
|
||||||
[general]
|
[general]
|
||||||
|
|
||||||
|
## RELOADABLE SETTINGS
|
||||||
|
## Settings below can be reloaded by editing the file
|
||||||
|
|
||||||
#RemoteNickFormat defines how remote users appear on this bridge
|
#RemoteNickFormat defines how remote users appear on this bridge
|
||||||
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username.
|
#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 "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
|
||||||
@@ -1246,6 +1367,7 @@ enable=true
|
|||||||
# - encrypted rooms are not supported in matrix
|
# - encrypted rooms are not supported in matrix
|
||||||
#steam - chatid (a large number).
|
#steam - chatid (a large number).
|
||||||
# The number in the URL when you click "enter chat room" in the browser
|
# The number in the URL when you click "enter chat room" in the browser
|
||||||
|
#zulip - stream (without the #)
|
||||||
#
|
#
|
||||||
#REQUIRED
|
#REQUIRED
|
||||||
channel="#testing"
|
channel="#testing"
|
||||||
|
|||||||
@@ -574,7 +574,7 @@ func (m *MMClient) GetFileLinks(filenames []string) []string {
|
|||||||
res, resp := m.Client.GetFileLink(f)
|
res, resp := m.Client.GetFileLink(f)
|
||||||
if resp.Error != nil {
|
if resp.Error != nil {
|
||||||
// public links is probably disabled, create the link ourselves
|
// 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
|
continue
|
||||||
}
|
}
|
||||||
output = append(output, res)
|
output = append(output, res)
|
||||||
|
|||||||
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",
|
"branch": "work",
|
||||||
"notests": true
|
"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",
|
"importpath": "github.com/mattermost/mattermost-server/einterfaces",
|
||||||
"repository": "https://github.com/mattermost/mattermost-server",
|
"repository": "https://github.com/mattermost/mattermost-server",
|
||||||
|
|||||||
Reference in New Issue
Block a user